Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
dc31828006 | ||
|
|
aa481b8f56 | ||
|
|
63cf236140 | ||
|
|
8bb2ccf829 | ||
|
|
0112a92c41 | ||
|
|
62ac31eb7a | ||
|
|
28e13d9fd0 | ||
|
|
82ea3a3a63 | ||
|
|
e9f8a210b9 | ||
|
|
9b4882db54 | ||
|
|
97a86b7677 | ||
|
|
2fa3363c92 | ||
|
|
f66b86ceec | ||
|
|
7945de2089 | ||
|
|
8d05153965 | ||
|
|
d1dc00d83d | ||
|
|
90853b2121 | ||
|
|
d03d6e28d6 | ||
|
|
09a3aa0b4c | ||
|
|
7f964bbb00 | ||
|
|
ad6c72c706 | ||
|
|
75e1e7ae31 | ||
|
|
3c893f0a16 | ||
|
|
b76bbb3692 | ||
|
|
f416138844 | ||
|
|
083ed266e0 | ||
|
|
acb6859b06 | ||
|
|
9f9e73ae0f | ||
|
|
c1919a3477 | ||
|
|
482a1cdf0d | ||
|
|
725e4ba4b8 | ||
|
|
fed9d78791 | ||
|
|
ddbeaa17c0 | ||
|
|
03e67171ab | ||
|
|
d6963bdf24 | ||
|
|
661b7c5548 | ||
|
|
90e65205f6 | ||
|
|
1eacc1bdb9 | ||
|
|
35fc7a776c | ||
|
|
b471c2b449 | ||
|
|
a42f393c85 | ||
|
|
b65de4d694 | ||
|
|
696f41bc8e | ||
|
|
29f07e0c69 | ||
|
|
adee738d1e | ||
|
|
543c686c0d | ||
|
|
34aa427434 | ||
|
|
16c56fe5ec | ||
|
|
6df0d5d951 | ||
|
|
0f549842ee | ||
|
|
0027387dc5 | ||
|
|
5fcfb06f94 | ||
|
|
79e8eee5cf | ||
|
|
82d4c89031 | ||
|
|
1d6c3c0e29 | ||
|
|
610df0b566 | ||
|
|
7cb044ee89 | ||
|
|
1b4a6dbc65 | ||
|
|
18ef3cc7c5 | ||
|
|
45719acebb | ||
|
|
2406400f0a | ||
|
|
fe51b0b19a | ||
|
|
6ea95e5e6f | ||
|
|
fc827ea8d5 | ||
|
|
3a35b45db8 | ||
|
|
fe3f3a41f7 | ||
|
|
f5ce4f881e | ||
|
|
f54ce313b9 | ||
|
|
32808ad5f1 | ||
|
|
ba341c6cac | ||
|
|
e059ac5918 | ||
|
|
70a24697ea | ||
|
|
cf494c2029 | ||
|
|
c10134729d | ||
|
|
0e3dfd1769 | ||
|
|
8bf288ecb1 | ||
|
|
17b7678875 | ||
|
|
d870e22c70 | ||
|
|
45ad8dc38a | ||
|
|
8b2de7317b | ||
|
|
ac88182b3e | ||
|
|
ebb7b76eca | ||
|
|
11be5bc29c | ||
|
|
83abc64a5c | ||
|
|
fee476bb90 | ||
|
|
ae93d4afb3 | ||
|
|
9a53565790 | ||
|
|
61eae8811a | ||
|
|
34b4c3cd9f | ||
|
|
3c7f191972 | ||
|
|
7c22dbbaf9 | ||
|
|
6da6d47aab | ||
|
|
f1aacaba22 | ||
|
|
4e1c49630a | ||
|
|
a6cad7743f | ||
|
|
ae2da69004 | ||
|
|
285cff53da | ||
|
|
5f9d9187c0 | ||
|
|
1573a7b608 | ||
|
|
3dd1b30539 | ||
|
|
e80db11160 | ||
|
|
a7f2373a05 | ||
|
|
b02acc1e30 | ||
|
|
5a8335857b | ||
|
|
88c6c7abc7 | ||
|
|
a6183af6f3 | ||
|
|
2e018f70f2 | ||
|
|
1364769e51 | ||
|
|
350c726aa8 | ||
|
|
ee91f190c5 | ||
|
|
501ff62cbf | ||
|
|
e03db9bc69 | ||
|
|
403627b031 | ||
|
|
5cf4e13588 | ||
|
|
4ae176520d | ||
|
|
a74c472ffb | ||
|
|
a97622c801 | ||
|
|
7f209437b5 | ||
|
|
228e9095ba | ||
|
|
df23b7f948 | ||
|
|
631f67a9f3 | ||
|
|
199a4cdbea | ||
|
|
a453386e9d | ||
|
|
ca34cecc03 | ||
|
|
95daf90670 | ||
|
|
0b934e9516 | ||
|
|
5de2f241c6 | ||
|
|
193de13a88 | ||
|
|
43e89ea167 | ||
|
|
b7ca4f91d9 | ||
|
|
b3f1a96c6d | ||
|
|
859106eb17 | ||
|
|
486e8460b1 | ||
|
|
28a62b7148 | ||
|
|
a9a6fad0fa | ||
|
|
b6914b39db | ||
|
|
72635a3720 | ||
|
|
9f8db119fc | ||
|
|
f69beceaff | ||
|
|
830d5eb535 | ||
|
|
5cd8e68fdd | ||
|
|
dff8fbc149 | ||
|
|
ac1758c0e6 | ||
|
|
d6855b6b47 | ||
|
|
eebd85f5d1 | ||
|
|
ecbc6ccac8 | ||
|
|
0b082b065d | ||
|
|
57ef382843 | ||
|
|
f914cac3d4 | ||
|
|
53fec37d8d | ||
|
|
a03bba5f2c | ||
|
|
56d4fb24a1 | ||
|
|
13e3744f76 | ||
|
|
e207624c48 | ||
|
|
ea2d59d7ca | ||
|
|
bec9ee41af | ||
|
|
c470c79513 | ||
|
|
23ec4f55c8 | ||
|
|
337fd144d2 | ||
|
|
1ddaeec480 | ||
|
|
7c2199d84a | ||
|
|
bbd72fb8b0 | ||
|
|
e2894562f8 | ||
|
|
70b4ca8c18 | ||
|
|
4db1eec321 | ||
|
|
79903a4cf7 | ||
|
|
e8548ae8b2 | ||
|
|
7774580e61 | ||
|
|
5b1e31988e | ||
|
|
02a167e048 | ||
|
|
500c46ec79 | ||
|
|
5cf9f517cf | ||
|
|
5d5c35ee71 | ||
|
|
51f86c8537 | ||
|
|
20a7743876 | ||
|
|
31afdd5938 | ||
|
|
4178fd74f1 | ||
|
|
3f31139064 | ||
|
|
475b408119 | ||
|
|
70f74112e3 | ||
|
|
03cb632435 | ||
|
|
dd417f1a6b | ||
|
|
8b9f89e7e8 | ||
|
|
c0f09afef5 | ||
|
|
8a6cf2c9aa | ||
|
|
a935432b5c | ||
|
|
7561b4a050 | ||
|
|
d8eb3b80fb | ||
|
|
08c483dc36 | ||
|
|
d2efd81fb3 | ||
|
|
79d503150c | ||
|
|
f12e14cf16 | ||
|
|
b578b7261f | ||
|
|
bbe4c952d7 | ||
|
|
3a9d4e78b6 | ||
|
|
8d1f8916d3 | ||
|
|
05028334b2 | ||
|
|
b04697366f | ||
|
|
4eb29a64f0 | ||
|
|
68abdce1a0 | ||
|
|
ef2792511d | ||
|
|
9a71624d2d | ||
|
|
ffbdee7b17 | ||
|
|
77aec36da1 | ||
|
|
50efc055de | ||
|
|
69c172e8a7 | ||
|
|
82b665075b | ||
|
|
8e0f1c253b | ||
|
|
8ff796f133 | ||
|
|
8100c68b50 | ||
|
|
86387e90b2 | ||
|
|
c10d6767c9 | ||
|
|
765ae641d7 | ||
|
|
c066339d80 | ||
|
|
1542195217 | ||
|
|
af1f9e6269 | ||
|
|
2961956c74 | ||
|
|
3f39a329c3 | ||
|
|
dc8753f82a | ||
|
|
4502ee568e | ||
|
|
55c54d14b8 | ||
|
|
2da5a4b034 | ||
|
|
216ba8f05f | ||
|
|
df6aecdbc5 | ||
|
|
1ce281b7ab | ||
|
|
5341a5e442 | ||
|
|
ebece6efd7 | ||
|
|
1ead9e3d7f | ||
|
|
be3b703a7b | ||
|
|
f7da00f35e | ||
|
|
5c5c709fee | ||
|
|
2386fcec75 | ||
|
|
ba790579ea | ||
|
|
04284921ac | ||
|
|
b639de07ac | ||
|
|
26ff3ed3d3 | ||
|
|
9f69463f1d | ||
|
|
10765ff2a9 | ||
|
|
be53e17e19 | ||
|
|
dc4449df0d | ||
|
|
62a2f0e617 | ||
|
|
df35e842f5 | ||
|
|
1be9fd55a9 | ||
|
|
755ebdb94e | ||
|
|
c2e39bb556 | ||
|
|
733562e92a | ||
|
|
8a7cc0c0ff | ||
|
|
19db518e0a | ||
|
|
15537a41d2 | ||
|
|
790fb5b86f | ||
|
|
517ee736b3 | ||
|
|
c77100a40c | ||
|
|
03afae5b95 | ||
|
|
05ea5073be | ||
|
|
5f5ea53afb | ||
|
|
1120d0704e | ||
|
|
6568a82455 | ||
|
|
c2eacd72af | ||
|
|
c5ef753d28 | ||
|
|
530535c6ea | ||
|
|
023620b57e | ||
|
|
902e67eba9 | ||
|
|
830aaf2059 | ||
|
|
a0f36c9df7 | ||
|
|
1216c55094 | ||
|
|
ec25388937 | ||
|
|
c7134cbb01 | ||
|
|
ba4d0ea8bf | ||
|
|
1cc53890dc | ||
|
|
ae2ea3c723 | ||
|
|
3f31551fad | ||
|
|
13c27598d3 | ||
|
|
2564a4f172 | ||
|
|
fd9bd448a2 | ||
|
|
b72b50006b | ||
|
|
ac1c048e64 | ||
|
|
8615731d46 | ||
|
|
a7444bf944 | ||
|
|
69aaa22793 | ||
|
|
322e2b9dd5 | ||
|
|
afd6f3b064 | ||
|
|
1e86be2bc5 | ||
|
|
a57230b80b | ||
|
|
6bb1a76e58 | ||
|
|
bdcb6be649 | ||
|
|
2f36ffe35d | ||
|
|
31c409a865 | ||
|
|
d0ceaef6aa | ||
|
|
3ef72fe8f6 | ||
|
|
51070b6f59 | ||
|
|
18b4cb6819 | ||
|
|
0561bac189 | ||
|
|
f4019cab3e | ||
|
|
f8b4dd70ac | ||
|
|
611cc73d84 | ||
|
|
7cde8329bc | ||
|
|
be15db5861 | ||
|
|
55f984126c | ||
|
|
2a6be0a03b | ||
|
|
2a36236132 | ||
|
|
105b398c2a | ||
|
|
9fa27bd41c | ||
|
|
6bb772215b | ||
|
|
03521003fa | ||
|
|
754bb7e390 | ||
|
|
4371298fff | ||
|
|
3f2e0e89ec | ||
|
|
4dbeeddc77 | ||
|
|
4161695885 | ||
|
|
7bbfe425f6 | ||
|
|
f2a1d547f1 | ||
|
|
b1ce938ebe | ||
|
|
03bba464d4 | ||
|
|
63a883a4f7 | ||
|
|
b3bad6b581 | ||
|
|
ca10088bdf | ||
|
|
dd3f915956 | ||
|
|
c11c41bd86 | ||
|
|
fa117558ee | ||
|
|
17b47301ba | ||
|
|
3d31e4bcec | ||
|
|
bb72277407 | ||
|
|
32c3637da0 | ||
|
|
9d466cd849 | ||
|
|
e3bd6e0947 | ||
|
|
dc13c6bb3d | ||
|
|
3a31c4ea80 | ||
|
|
e00890c672 | ||
|
|
3a4c253969 | ||
|
|
34a83b27c3 | ||
|
|
ff3ef6e3e6 | ||
|
|
e2ec62d073 | ||
|
|
bcf1103a8c | ||
|
|
9cb6fb6529 | ||
|
|
1b937c860b | ||
|
|
ad0bb6d9a3 | ||
|
|
487d584402 | ||
|
|
57fa34f235 | ||
|
|
0af9dd61d2 | ||
|
|
8ecd4c21bf | ||
|
|
361a71ffad | ||
|
|
fa8f17014c | ||
|
|
813e6f3cec | ||
|
|
ba72a6ff2b | ||
|
|
b6a1f95858 | ||
|
|
f357e70779 | ||
|
|
a9cac91d60 | ||
|
|
35fe5b7212 | ||
|
|
62cb3f77bd | ||
|
|
e0b9e68a68 | ||
|
|
1e4c153aed | ||
|
|
c9dc1a1b3d | ||
|
|
40282c1d4d | ||
|
|
e11dbd0206 | ||
|
|
7b11ef5a5e | ||
|
|
2859603cbb | ||
|
|
c313504c82 | ||
|
|
e49772a8b4 | ||
|
|
08faeb3893 | ||
|
|
5e75e5e990 | ||
|
|
5cc5b622f0 | ||
|
|
11e8110f9e | ||
|
|
82dee24302 | ||
|
|
ad192a59b5 | ||
|
|
71d08e94d5 | ||
|
|
b155b15694 | ||
|
|
d06bf487a5 | ||
|
|
ccccd75328 | ||
|
|
03d65f36d3 | ||
|
|
f4a89683da | ||
|
|
f715478a13 | ||
|
|
e0e9bd7d5f | ||
|
|
5ccb1e88e1 | ||
|
|
be469f328c | ||
|
|
fc80874f45 | ||
|
|
3d26b12251 | ||
|
|
1b77242d40 | ||
|
|
4c5960dc4b | ||
|
|
e2505d434a | ||
|
|
a466a357b6 | ||
|
|
6386e67949 | ||
|
|
16af134053 | ||
|
|
d545d8fba5 | ||
|
|
da4730e4a4 | ||
|
|
18fc3eea7c | ||
|
|
101ff6d1a2 | ||
|
|
20ad497bc8 | ||
|
|
1b18a50418 | ||
|
|
0a6397272c | ||
|
|
3477d28a87 | ||
|
|
fb772e8a02 | ||
|
|
4e29e04f24 | ||
|
|
fbcd4faa61 | ||
|
|
da3ba63872 | ||
|
|
e2a2bc301d | ||
|
|
72824fe65d | ||
|
|
64b4f7df4c | ||
|
|
7774461bbf | ||
|
|
8b65fa2ef6 | ||
|
|
0033bb3559 | ||
|
|
ad905f8e6c | ||
|
|
9b0dd36cda | ||
|
|
4e81a0b73f | ||
|
|
32320409b1 | ||
|
|
040b856140 | ||
|
|
65c35977bd | ||
|
|
57075d8cdc | ||
|
|
e14acea581 | ||
|
|
7af274a921 | ||
|
|
a97fec8af5 | ||
|
|
2cba7e227d | ||
|
|
4d68d30b48 | ||
|
|
5a0e07efcf | ||
|
|
cba6b5f2f7 | ||
|
|
1c965451b5 | ||
|
|
a0f8dccbfe | ||
|
|
fb8344f3c2 | ||
|
|
3fd77ae980 | ||
|
|
976c185172 | ||
|
|
4643e65ad6 | ||
|
|
e23586af26 | ||
|
|
92d29ba4a3 | ||
|
|
886fd98f52 | ||
|
|
722e717e99 | ||
|
|
6d635007ab | ||
|
|
0e3474509a | ||
|
|
d8bf79225f | ||
|
|
8a55ba75b5 | ||
|
|
2449581543 | ||
|
|
3e25e80f7a | ||
|
|
f06e9491e1 | ||
|
|
d0c2b41adf | ||
|
|
c7af47f9ed | ||
|
|
6c035e1fd7 | ||
|
|
ab7cbc808f | ||
|
|
62387f39d4 | ||
|
|
864889ab9f | ||
|
|
cc4b456129 | ||
|
|
14b37df391 | ||
|
|
91df883231 | ||
|
|
6fea419feb | ||
|
|
beeabe2e4b | ||
|
|
502310f4df | ||
|
|
37fe5c83dc | ||
|
|
dab2637217 | ||
|
|
39fb919a05 | ||
|
|
999be62662 | ||
|
|
521906f5fa | ||
|
|
069165eb5b | ||
|
|
df09c3ec61 | ||
|
|
6d3ae4d63d | ||
|
|
284187ac55 | ||
|
|
7e0353cca9 | ||
|
|
959c8bd498 | ||
|
|
ca3df21b7f | ||
|
|
8571a922db | ||
|
|
d92d1e654b | ||
|
|
46a14e685a | ||
|
|
1f94d8a3c8 | ||
|
|
c78986831c | ||
|
|
d470434746 | ||
|
|
561afb401a | ||
|
|
2b8a5969e9 | ||
|
|
36b205370d | ||
|
|
6afef2f881 | ||
|
|
0d927ba03f | ||
|
|
9fdd4824d3 | ||
|
|
d041eee55c | ||
|
|
d62e50075a | ||
|
|
5d953482ab | ||
|
|
3a90639cb5 | ||
|
|
b9f3b382fc | ||
|
|
550d1ffd24 | ||
|
|
941e71dbbb | ||
|
|
d90532a595 | ||
|
|
0bff28de09 | ||
|
|
38e10eb140 | ||
|
|
fe82a1b6ca | ||
|
|
2ea728f731 | ||
|
|
284214c78d | ||
|
|
b05a0d1fbe | ||
|
|
be25c905fa | ||
|
|
5347b5e009 | ||
|
|
b467518dae | ||
|
|
1ec6bdc8c6 | ||
|
|
265472ef72 | ||
|
|
325529761b | ||
|
|
741244b69d | ||
|
|
4abddbbfdd | ||
|
|
5aa5cb3523 | ||
|
|
9113255114 | ||
|
|
23cc677108 | ||
|
|
28bcc0e4f4 | ||
|
|
8bf5062b89 | ||
|
|
20750c332e | ||
|
|
db581e47a3 | ||
|
|
a54a693af2 | ||
|
|
d468201952 | ||
|
|
31c5c4e924 | ||
|
|
8d65dbdd04 | ||
|
|
b7f9363393 | ||
|
|
69f2cbaa3a | ||
|
|
dc898ca2c3 | ||
|
|
03bff6500a | ||
|
|
b379d71d19 | ||
|
|
def7220bfc | ||
|
|
f6c5afe5a9 | ||
|
|
847771fe06 | ||
|
|
b8bd88901a | ||
|
|
f1adabcddc | ||
|
|
24470e52a5 | ||
|
|
23bbc5cd14 | ||
|
|
ebf6fc07a1 | ||
|
|
04513d9692 | ||
|
|
146720e197 | ||
|
|
b1f8cc04af | ||
|
|
082fe93e16 | ||
|
|
709c195905 | ||
|
|
3355b78d91 | ||
|
|
b84c63d034 | ||
|
|
057b5a9dee | ||
|
|
61aefc0759 | ||
|
|
f929f8f100 | ||
|
|
4a57013cf8 | ||
|
|
2f17fc9613 | ||
|
|
b8ea3ea091 | ||
|
|
82a5796159 | ||
|
|
9f38562237 | ||
|
|
5b4fb1f770 | ||
|
|
cb88db6ce3 | ||
|
|
66fdd51f0d | ||
|
|
98dd65cd60 | ||
|
|
f0afb6750e | ||
|
|
703d09f306 | ||
|
|
aefaf140c1 | ||
|
|
44614c79c4 | ||
|
|
dc67b7a3be | ||
|
|
de1bd36a47 | ||
|
|
6c97a46dae | ||
|
|
c387fe190b | ||
|
|
8721a2710b | ||
|
|
c75e3b5bae | ||
|
|
edc8d1d91e | ||
|
|
e85735028c | ||
|
|
6c3299103c | ||
|
|
60e23fee16 | ||
|
|
0c380f31ac | ||
|
|
26aefa68f5 | ||
|
|
638788f8d0 | ||
|
|
b005a15a4a | ||
|
|
0efa33cf33 | ||
|
|
7e94ddafe8 | ||
|
|
7fe993879f | ||
|
|
def14d96ef | ||
|
|
b0b1149233 | ||
|
|
7becbbf711 | ||
|
|
87481f2bb6 | ||
|
|
9535a8f929 | ||
|
|
e917c12c80 | ||
|
|
89130db6b0 | ||
|
|
9465a6c97d | ||
|
|
53fd02970d | ||
|
|
6ff0adef22 | ||
|
|
3307f99f1f | ||
|
|
8de7a3c739 | ||
|
|
28d0637406 | ||
|
|
ef7f712867 | ||
|
|
3016830e25 | ||
|
|
e9e7472456 | ||
|
|
9f9fbf815c | ||
|
|
c88facb2cc | ||
|
|
d4b85f8d83 | ||
|
|
1882c33827 | ||
|
|
01624c8c87 | ||
|
|
0891ecd8e0 | ||
|
|
32f278b48f | ||
|
|
bb6bec9070 | ||
|
|
0eac0964af | ||
|
|
f67e80c275 | ||
|
|
19231fce4b | ||
|
|
d0a7ca6d1b | ||
|
|
d80e86be9d | ||
|
|
e4bd9757a3 | ||
|
|
4087cf1a97 | ||
|
|
14f0bf64a1 | ||
|
|
2df47ee394 | ||
|
|
ad82616445 | ||
|
|
d4ad650f73 | ||
|
|
2039c90c06 | ||
|
|
da59457ca4 | ||
|
|
db4dbaa289 | ||
|
|
d3f30df180 | ||
|
|
7ea343cabd | ||
|
|
e024e2ef92 | ||
|
|
ceb4890ca3 | ||
|
|
3f239b3834 | ||
|
|
c1ee3c6e34 | ||
|
|
a1ab489e61 | ||
|
|
8c38d260eb | ||
|
|
a4f25a94b4 | ||
|
|
ff0382e971 | ||
|
|
ac5380614f | ||
|
|
c08350b83e | ||
|
|
c5cf416633 | ||
|
|
3dbce3dade | ||
|
|
a277557a5f | ||
|
|
005bf088c9 | ||
|
|
eca5e68a5e | ||
|
|
5b11635d97 | ||
|
|
a9eda2e99e | ||
|
|
73009f0b09 | ||
|
|
f720490219 | ||
|
|
341c64cc97 | ||
|
|
1e17856ba4 | ||
|
|
a6f7fe739c | ||
|
|
02f50a6e80 | ||
|
|
7565e03536 | ||
|
|
035a5a1a67 | ||
|
|
e8b0992788 | ||
|
|
8cbea4fbb7 | ||
|
|
f256a4c538 | ||
|
|
a45c2b917e | ||
|
|
f28d72d15e | ||
|
|
d8a9d8a76d | ||
|
|
982a9044c5 | ||
|
|
7edefb3347 | ||
|
|
017a5977d2 | ||
|
|
7336cc5a7b | ||
|
|
e1587588c4 | ||
|
|
be93cec9ba | ||
|
|
7c898249a2 | ||
|
|
9e432a6785 | ||
|
|
1ead8d84a2 | ||
|
|
8c81f9a40b | ||
|
|
95636bd3ca | ||
|
|
7dae9a1083 | ||
|
|
d46be565aa | ||
|
|
8021dbd80f | ||
|
|
c8487bac54 | ||
|
|
484f00ebf4 | ||
|
|
c3be6c4965 | ||
|
|
172d08cb98 | ||
|
|
5e39051183 | ||
|
|
d50b9e8f9a | ||
|
|
921b1c6a34 | ||
|
|
9c8060d96d | ||
|
|
87db7a7f0a | ||
|
|
237e694df0 | ||
|
|
3f9dad3fd0 | ||
|
|
3cc70160de | ||
|
|
b72d7b6fa2 | ||
|
|
029f5aa3a0 | ||
|
|
284e97068c | ||
|
|
970c38257c | ||
|
|
c39ad38424 | ||
|
|
91508f6c88 | ||
|
|
1274408b89 | ||
|
|
7ae1be5bf6 | ||
|
|
40e4d730ca | ||
|
|
8dc539b4ae | ||
|
|
8e8d38c3cd | ||
|
|
a9bebd3653 | ||
|
|
78f5764319 | ||
|
|
55b0211690 | ||
|
|
4f62295e44 | ||
|
|
e1c41941a9 | ||
|
|
0b4edfc64c | ||
|
|
4469cecc4e | ||
|
|
b06768aa8e | ||
|
|
bc2d3df05f | ||
|
|
35075bb466 | ||
|
|
a9afed0515 | ||
|
|
09afffeead | ||
|
|
de78da8a23 | ||
|
|
9ca2b44235 | ||
|
|
7ffc22c0ef | ||
|
|
759f6afa80 | ||
|
|
1ad63cfc6c | ||
|
|
7d8195e8fe | ||
|
|
7534d23551 | ||
|
|
0a6637e17a | ||
|
|
2b2b69d668 | ||
|
|
9e8b2171a5 | ||
|
|
5e2a6257b2 | ||
|
|
065dc274ec | ||
|
|
a85d8d23aa | ||
|
|
2b2aacc939 | ||
|
|
7840562d1b | ||
|
|
53669cab2b | ||
|
|
4b19ea2a71 | ||
|
|
553b42fcc7 | ||
|
|
e62b09e6fc | ||
|
|
f51665191a | ||
|
|
e63996919b | ||
|
|
909db63265 | ||
|
|
66495a2a22 | ||
|
|
9ba82a1b8c | ||
|
|
b62806683e | ||
|
|
ae4be983cb | ||
|
|
ad78ef5a4c | ||
|
|
67a04787bf | ||
|
|
aaa42fe3bf | ||
|
|
3337495427 | ||
|
|
80a4fe8f39 | ||
|
|
91ed2567ef | ||
|
|
c4545ab12f | ||
|
|
57ddfeb756 | ||
|
|
ce7f54d5e7 | ||
|
|
bce2992729 | ||
|
|
3819848cf2 | ||
|
|
55f4a2691a | ||
|
|
a48912cbb1 | ||
|
|
2ea4bb793e | ||
|
|
d07323ef59 | ||
|
|
5ce89dc2ad | ||
|
|
589ef16128 | ||
|
|
386afb77e0 | ||
|
|
29af2e39ff | ||
|
|
090a26a301 | ||
|
|
ce9a875181 | ||
|
|
db88c42335 | ||
|
|
adba484df4 | ||
|
|
37c263e153 | ||
|
|
204f48de8b | ||
|
|
743f6410f2 | ||
|
|
20589d66d5 | ||
|
|
62e4641235 | ||
|
|
e59ba24b80 | ||
|
|
347510a73b | ||
|
|
d03e0c5e54 | ||
|
|
84c79c1399 | ||
|
|
e45210c6d2 | ||
|
|
92cede0541 | ||
|
|
fada1a538f | ||
|
|
542ba88602 | ||
|
|
411eff73f0 | ||
|
|
50eee1f5a4 | ||
|
|
0a601cf334 | ||
|
|
c9db6e9a1f | ||
|
|
130038eb63 | ||
|
|
4438f69aac | ||
|
|
76ced5418f | ||
|
|
c40e3e74a7 | ||
|
|
0317531906 | ||
|
|
5f241f1cc1 | ||
|
|
b513fafe83 | ||
|
|
21c4c93770 | ||
|
|
abc4b83f58 | ||
|
|
cb562b2c62 | ||
|
|
2d2339d37d | ||
|
|
849c2598ab | ||
|
|
3033afba51 | ||
|
|
5a8ee27254 | ||
|
|
4cc022481c | ||
|
|
d36a2d8981 | ||
|
|
96ff82a11f | ||
|
|
4872c48c23 | ||
|
|
47fc6f2991 | ||
|
|
da312b009c | ||
|
|
70535a1bad | ||
|
|
43e472bba7 | ||
|
|
b67fda133a | ||
|
|
e2531dff0f | ||
|
|
cc3615b535 | ||
|
|
5542415b64 | ||
|
|
8bec40a816 | ||
|
|
4a81e8392b | ||
|
|
a19b52e85b | ||
|
|
9105f35966 | ||
|
|
3ba3954aa0 | ||
|
|
3fc5af6dd1 | ||
|
|
5586f9675e | ||
|
|
3b719aba48 | ||
|
|
20a3a2138e | ||
|
|
cd46f22d51 | ||
|
|
cfa134f042 | ||
|
|
61653a20a5 | ||
|
|
2a097f3af7 | ||
|
|
ba4b8e21f4 | ||
|
|
7985c2066a | ||
|
|
67caf894e0 | ||
|
|
78587c141b | ||
|
|
f5573281ca | ||
|
|
4817228f96 | ||
|
|
5a5117b36c | ||
|
|
6a288426b8 | ||
|
|
573ac85983 | ||
|
|
1221bbbe14 | ||
|
|
6bea0f4fa4 | ||
|
|
cc34084dfe | ||
|
|
b8142c7aa8 | ||
|
|
ba3f966ed0 | ||
|
|
b95fe7b2d7 | ||
|
|
9da9ed9de6 | ||
|
|
fab2133129 | ||
|
|
901c9e7329 | ||
|
|
3476e6baa5 | ||
|
|
86e04d4174 | ||
|
|
45cd8d7692 | ||
|
|
a46374e90e | ||
|
|
41405c9eb0 | ||
|
|
49d2dd8a38 | ||
|
|
0df2f6245a | ||
|
|
38989c57a0 | ||
|
|
69a80663a2 | ||
|
|
518b0bf9e3 | ||
|
|
be08207a88 | ||
|
|
efb6cc2d4b | ||
|
|
4bffbb655f | ||
|
|
ba6cf38607 | ||
|
|
86978e7a8c | ||
|
|
82a3270cf2 | ||
|
|
c0a258b22a | ||
|
|
ed926be253 | ||
|
|
b527f605d9 | ||
|
|
aca52ba198 | ||
|
|
f7d5a1cbc7 | ||
|
|
3238daa851 | ||
|
|
90291fc6df | ||
|
|
a414e06d0c | ||
|
|
52f65f3a0b | ||
|
|
3a1c6df9e2 | ||
|
|
4436678f7d | ||
|
|
08fb473735 | ||
|
|
51213520f5 | ||
|
|
0bc9a28b5e | ||
|
|
1d2a7b4c86 | ||
|
|
453b61e27b | ||
|
|
95ebf50ace | ||
|
|
d8a1b932eb | ||
|
|
0c91e6a620 | ||
|
|
ef3b673084 | ||
|
|
c69abee0c7 | ||
|
|
dc3ff4b7aa | ||
|
|
958e08e109 | ||
|
|
590a11ed98 | ||
|
|
17544802c3 | ||
|
|
4a760b5743 | ||
|
|
888c4cb504 | ||
|
|
4394b324c9 | ||
|
|
a27ffb20f0 | ||
|
|
81141dc562 | ||
|
|
285d315b4e | ||
|
|
656f299017 | ||
|
|
04bdf7732b | ||
|
|
ab7dfb1edc | ||
|
|
05c3e40a4e | ||
|
|
b6478dcebf | ||
|
|
571e7b852f | ||
|
|
888dd26e1d | ||
|
|
279c8bdd7f | ||
|
|
865e4d5c86 | ||
|
|
64950cd358 | ||
|
|
36157938a8 | ||
|
|
c0dea334fb | ||
|
|
9b49daf3a9 | ||
|
|
d6f1b08ce8 | ||
|
|
75cf4cc8a8 | ||
|
|
4a181c3574 | ||
|
|
bfddf05782 | ||
|
|
cdee625a13 | ||
|
|
5f7091ab4e | ||
|
|
e7e7a97648 | ||
|
|
f58b8cb67b | ||
|
|
775f687ca9 | ||
|
|
265a93927d | ||
|
|
3a938bb6d8 | ||
|
|
6f9a4e70da | ||
|
|
dedb4133dc | ||
|
|
21f8184c58 | ||
|
|
59fe350972 | ||
|
|
5bdfcaac3c | ||
|
|
323d687173 | ||
|
|
cdca203be2 | ||
|
|
77a31d2b6e | ||
|
|
a1ed99278a | ||
|
|
729bd438e4 | ||
|
|
7304bb2751 | ||
|
|
456521f689 | ||
|
|
ee69079bb4 | ||
|
|
de14bb2e0f | ||
|
|
0539d56fc4 | ||
|
|
729adde022 | ||
|
|
b7111d5c62 | ||
|
|
1669713fbc | ||
|
|
cb8be9ba12 | ||
|
|
15046658ba | ||
|
|
7255ce9a57 | ||
|
|
2b9cf5d388 | ||
|
|
a625d35f7b | ||
|
|
449c64928b | ||
|
|
35c88c3fc8 | ||
|
|
b5e09024d5 | ||
|
|
9a51765bd2 | ||
|
|
bb31c9b92c | ||
|
|
5117679148 | ||
|
|
0d5798c0bf | ||
|
|
808677a612 | ||
|
|
fe41e9d482 | ||
|
|
0768c4655e | ||
|
|
1586a26594 | ||
|
|
f026c1422c | ||
|
|
b7fdb595df | ||
|
|
049ee1ce5e | ||
|
|
e71fbb6d48 | ||
|
|
c9976d69c0 | ||
|
|
27734409e4 | ||
|
|
e434ce01d3 | ||
|
|
c2f93ae2ad | ||
|
|
92cc8f553a | ||
|
|
49d997aec3 | ||
|
|
67aa9cee23 | ||
|
|
7ec3d67829 | ||
|
|
5401ebaede | ||
|
|
99668cfdce | ||
|
|
b3a012cbdd | ||
|
|
51b9c726cf | ||
|
|
b4e49d5b32 | ||
|
|
2b29854589 | ||
|
|
8ce5038bd8 | ||
|
|
446510a6a9 | ||
|
|
49028b6245 | ||
|
|
d595c7e265 | ||
|
|
17cd4a8c32 | ||
|
|
9bbf7918de | ||
|
|
97f502e79a | ||
|
|
ac5e249083 | ||
|
|
140e796b21 | ||
|
|
dd02cea9e7 | ||
|
|
a8062ea314 | ||
|
|
f47684787a | ||
|
|
8168f0f0f6 | ||
|
|
8036440693 | ||
|
|
cf7503687a | ||
|
|
6f8e1dda15 | ||
|
|
4dcfd9182c | ||
|
|
d7c0db8cee | ||
|
|
184da0e42e | ||
|
|
a244d82aba | ||
|
|
34e5809059 | ||
|
|
2db96c9b7e | ||
|
|
9976269ab1 | ||
|
|
5df822be11 | ||
|
|
233f069070 | ||
|
|
4a1982f5c9 | ||
|
|
1c7f65f105 | ||
|
|
a84151e482 | ||
|
|
0340a4ff77 | ||
|
|
f4faacbe87 | ||
|
|
7dbfd0b934 | ||
|
|
08a2a623dc | ||
|
|
60c88336b9 | ||
|
|
8a03fc265b | ||
|
|
d0ba538924 | ||
|
|
772e976d80 | ||
|
|
c769380776 | ||
|
|
01131c75d8 | ||
|
|
ea982c3832 | ||
|
|
d09d5612f3 | ||
|
|
85ef35dd64 | ||
|
|
251177d56c | ||
|
|
13b9e55c72 | ||
|
|
b94e55c7c0 | ||
|
|
9c6bb4ff72 | ||
|
|
c2e019ba7d | ||
|
|
b8374b5a7c | ||
|
|
b4665a8bf8 | ||
|
|
08096b5e61 | ||
|
|
2e43d41af0 | ||
|
|
518772c2ba | ||
|
|
fc9021ccec | ||
|
|
3bebfe6b1b | ||
|
|
3b18c64782 | ||
|
|
c6c599b872 | ||
|
|
6b41ae910c | ||
|
|
5c860d4142 | ||
|
|
4f9207daac | ||
|
|
ae7d63272c | ||
|
|
5cc3a51357 | ||
|
|
e357c90212 | ||
|
|
fb1edc4918 | ||
|
|
353e0eef8a | ||
|
|
a4c17f08b1 | ||
|
|
3cd9ee5a73 | ||
|
|
7c56319aa4 | ||
|
|
13fa4937b2 | ||
|
|
1db5c2402c | ||
|
|
0abbcddac2 | ||
|
|
dd25d143c5 | ||
|
|
d8a57ee15c | ||
|
|
aa52d30d7f | ||
|
|
83d633fd13 | ||
|
|
824455e2f1 | ||
|
|
62de2b3695 | ||
|
|
7550aa4b89 | ||
|
|
c33a2011c4 | ||
|
|
fff6764ed4 | ||
|
|
9644a008a4 | ||
|
|
ccb6b7c3ea | ||
|
|
f95f877e5f | ||
|
|
fe96d4aebd | ||
|
|
3a95d56144 | ||
|
|
ecd8be7593 | ||
|
|
a72c3880bb | ||
|
|
ccd8676bee | ||
|
|
5a533e366f | ||
|
|
77429f5982 | ||
|
|
6cfe4bc34f | ||
|
|
95013d2936 | ||
|
|
14a5529793 | ||
|
|
0b2e8707e3 | ||
|
|
adf178a6b9 | ||
|
|
78f85b1cd6 | ||
|
|
888dc67cc3 | ||
|
|
fd9ee435ec | ||
|
|
1c25b2f298 | ||
|
|
ea9fffce2e | ||
|
|
516f0266b8 | ||
|
|
39397deb8d | ||
|
|
65fd2e5c91 | ||
|
|
c5fe32fbb1 | ||
|
|
c48b77d6f9 | ||
|
|
64fd7a2027 | ||
|
|
ae259d87f0 | ||
|
|
50374abab8 | ||
|
|
0497e8badf | ||
|
|
2a08f8f5f4 | ||
|
|
9353559088 | ||
|
|
6aff2ee169 | ||
|
|
254a481e5d | ||
|
|
26885e7118 | ||
|
|
dba08013d1 | ||
|
|
fc09646973 | ||
|
|
c3b9c9d888 | ||
|
|
203885d241 | ||
|
|
728e86b287 | ||
|
|
d5aa79c73a | ||
|
|
eb6ff0c1e0 | ||
|
|
0df44c4e91 | ||
|
|
561070fbc2 | ||
|
|
b0e41ff2c4 | ||
|
|
977a40af68 | ||
|
|
dffecb6478 | ||
|
|
7f57d2a9ac | ||
|
|
377d5d2287 | ||
|
|
38bb38ecb2 | ||
|
|
3a149ba71c | ||
|
|
14df88eab5 | ||
|
|
e12cb7fb6b | ||
|
|
d4f7039932 | ||
|
|
e0ec9be238 | ||
|
|
2b34df94fa | ||
|
|
68f225a11d | ||
|
|
e9f5d34096 | ||
|
|
d3459eb740 | ||
|
|
be73f465df | ||
|
|
f14eafe9db | ||
|
|
b7e4f1ef74 | ||
|
|
efd5f0a80f | ||
|
|
7135bc4fb5 | ||
|
|
7cf8d1ca6d | ||
|
|
78bca5c707 | ||
|
|
82db86e2a1 | ||
|
|
476e3fc382 | ||
|
|
a12d50e15a | ||
|
|
1b9b260458 | ||
|
|
a8983bd2c7 | ||
|
|
709af58612 | ||
|
|
bcbbf32569 | ||
|
|
7a8e7c88f4 | ||
|
|
a22676720d | ||
|
|
f63a451b6f | ||
|
|
8b6a2b173b | ||
|
|
933dbfdb36 | ||
|
|
b44bc0ad07 | ||
|
|
436cd39d14 | ||
|
|
ed6dc879be | ||
|
|
96d53c6716 | ||
|
|
911fb7063a | ||
|
|
27e8e2e22b | ||
|
|
39c6dd1c32 | ||
|
|
a09447bc9b | ||
|
|
16815473a4 | ||
|
|
1f47bbfce2 | ||
|
|
33ed95f09c | ||
|
|
2152624f37 | ||
|
|
7d84cb05ef | ||
|
|
eba0bd0397 | ||
|
|
9aab24e95e | ||
|
|
a4fed69a82 | ||
|
|
41a3cfcdd9 | ||
|
|
72845e09fc | ||
|
|
abaab29cb3 | ||
|
|
30936a9bca | ||
|
|
a9144eccf8 | ||
|
|
458ec7b372 | ||
|
|
d46c02ba73 | ||
|
|
caedf36844 | ||
|
|
4c6e2a92ce | ||
|
|
bf58fae51f | ||
|
|
eb3476660f | ||
|
|
7d48a22a63 | ||
|
|
3f0fc22b6e | ||
|
|
2409c7a3d5 | ||
|
|
379adee35c | ||
|
|
2ef1a5991c | ||
|
|
83c638eaac | ||
|
|
cb93114424 | ||
|
|
04d236ceed | ||
|
|
5455fed78e | ||
|
|
6dba0cc498 | ||
|
|
c07ac0db2f | ||
|
|
20b090e493 | ||
|
|
cb54824f2f | ||
|
|
3adfb970bf | ||
|
|
5ecd19db9f | ||
|
|
299daa2815 | ||
|
|
7457139849 | ||
|
|
373cd08b98 | ||
|
|
f52424e05f | ||
|
|
4a42caef2c | ||
|
|
7d2618637f | ||
|
|
fbb95a6ee3 | ||
|
|
40233595b0 | ||
|
|
403164a6ac | ||
|
|
1fb2ecc721 | ||
|
|
27c9b130c7 | ||
|
|
5fecf7ae9e | ||
|
|
a0d64b12c9 | ||
|
|
d6b897188a | ||
|
|
39dfbb53d0 | ||
|
|
a39db9c6f6 | ||
|
|
6ca60d9204 | ||
|
|
b3934992d1 | ||
|
|
4de7e90d8d | ||
|
|
917eca4f55 | ||
|
|
3cf71808c4 | ||
|
|
f266e829cf | ||
|
|
3df5556223 | ||
|
|
fc29f99190 | ||
|
|
beb586a383 | ||
|
|
5668d36477 | ||
|
|
3706b33ee0 | ||
|
|
85e8ac3b36 | ||
|
|
91ca75c4dc | ||
|
|
262ef4fdc2 | ||
|
|
74e5644646 | ||
|
|
7de5aa08ff | ||
|
|
9495e3e903 | ||
|
|
1e73b76ea6 | ||
|
|
6697cf3fd8 | ||
|
|
87aa62d1e2 | ||
|
|
3136b1ba97 | ||
|
|
246dea3a58 | ||
|
|
4b1c7f28a0 | ||
|
|
9410eda4e3 | ||
|
|
f014686b07 | ||
|
|
8b68dc0d7f | ||
|
|
2f4d36d64e | ||
|
|
6f2ff35656 | ||
|
|
cd00422298 | ||
|
|
34ee94232f | ||
|
|
4d91f0cf28 | ||
|
|
bf18cc971f | ||
|
|
5e1a1ed2e2 | ||
|
|
bd7f7ad98b | ||
|
|
af72cec799 | ||
|
|
55552c521f | ||
|
|
2b611a5b14 | ||
|
|
45a437cbb2 | ||
|
|
439ec9b76f | ||
|
|
ae3fefb42a | ||
|
|
f1407e41c4 | ||
|
|
c88ca972cf | ||
|
|
928c37bc02 | ||
|
|
afff74eb2c | ||
|
|
3a669d528b | ||
|
|
6789214b31 | ||
|
|
6636683323 | ||
|
|
3af9f64265 | ||
|
|
5a23bdcb69 | ||
|
|
e78726854a | ||
|
|
472d0f376e | ||
|
|
c8fde60ad4 | ||
|
|
a4b6047e64 | ||
|
|
6387f955cf | ||
|
|
5691f36e98 | ||
|
|
74e7a378bb | ||
|
|
fced9c5a2f | ||
|
|
20445ca031 | ||
|
|
6fc9405618 | ||
|
|
c86066efef | ||
|
|
fa9ba2938e | ||
|
|
63e24880e5 | ||
|
|
eaf42c4ab0 | ||
|
|
8e10ab0aa7 | ||
|
|
b329fcceb2 | ||
|
|
bd83177c48 | ||
|
|
06cb0de0b1 | ||
|
|
3ca89ac796 | ||
|
|
3aa2860058 | ||
|
|
ad63cf1590 | ||
|
|
aa56750f8b | ||
|
|
4aaed42640 | ||
|
|
f69f677a32 | ||
|
|
8289883de8 | ||
|
|
003c22b4a3 | ||
|
|
1982d9832b | ||
|
|
90f0576222 | ||
|
|
e34822db6b | ||
|
|
b19ae6547f | ||
|
|
6b074ed718 | ||
|
|
e687eb936b | ||
|
|
797cb530e5 | ||
|
|
0a5bed2bc2 | ||
|
|
ba41dadc91 | ||
|
|
451876b0bd | ||
|
|
91561bbdfb | ||
|
|
018ce34b39 | ||
|
|
ce82b21073 | ||
|
|
1f37c26574 | ||
|
|
cb3e28d207 | ||
|
|
8f1e13f299 | ||
|
|
2fe68b5e91 | ||
|
|
2ace237938 | ||
|
|
5b62211167 | ||
|
|
60b6229c4e | ||
|
|
4caec80e67 | ||
|
|
e9d4ca7e0f | ||
|
|
d71454c701 | ||
|
|
884c256033 | ||
|
|
78235385dd | ||
|
|
fd213e6df6 | ||
|
|
3726052307 | ||
|
|
7d33a6f7c9 | ||
|
|
7a035d7fc0 | ||
|
|
9151af7045 | ||
|
|
15bcbb1d7a | ||
|
|
4d3294727c | ||
|
|
aae0e89519 | ||
|
|
93a7b4ab76 | ||
|
|
0ebe74b625 | ||
|
|
b9dae8593c | ||
|
|
d8f9388610 | ||
|
|
be14739cce | ||
|
|
762588c251 | ||
|
|
d6e54e9042 | ||
|
|
d4fd528152 | ||
|
|
de185559dd | ||
|
|
bc5ce5eab1 | ||
|
|
b10d617c80 | ||
|
|
18466b8020 | ||
|
|
71ec022b47 | ||
|
|
f7eeb08942 | ||
|
|
9205dfe724 | ||
|
|
a3776429aa | ||
|
|
c0e43c0c65 | ||
|
|
08c5a2a5e8 | ||
|
|
7077c70d60 | ||
|
|
1fd8bf526d | ||
|
|
6d9539f469 | ||
|
|
e5101d9651 | ||
|
|
b0151e1d02 | ||
|
|
c3637753bc | ||
|
|
48fc9e3cc3 | ||
|
|
82a482b36d | ||
|
|
7fac84319d | ||
|
|
0a5e617da8 | ||
|
|
b5f92c9f48 | ||
|
|
2070fd9bc2 | ||
|
|
48dc05eee7 | ||
|
|
84bb706537 | ||
|
|
be8cfa7e08 | ||
|
|
56a789d926 | ||
|
|
5409151ad4 | ||
|
|
f90d154465 | ||
|
|
078385d386 | ||
|
|
beaaa174ea | ||
|
|
fb59606bb4 | ||
|
|
846d548154 | ||
|
|
84336e4f91 | ||
|
|
01a5c69244 | ||
|
|
d78f91afeb | ||
|
|
33762292f7 | ||
|
|
5d6aa3b41a | ||
|
|
683aca5dbe | ||
|
|
92025e9a47 | ||
|
|
411e053572 | ||
|
|
b862d178bf | ||
|
|
bda0ddc207 | ||
|
|
58ba99e505 | ||
|
|
6ee817d041 | ||
|
|
dfeb1b5051 | ||
|
|
61a7f6db35 | ||
|
|
6446b8ae3b | ||
|
|
ae53163cad | ||
|
|
41202819a4 | ||
|
|
a31698f99b | ||
|
|
6248542c49 | ||
|
|
d25c91e5eb | ||
|
|
5d63020dcd | ||
|
|
062f54590e | ||
|
|
a76c6918d2 | ||
|
|
2b91a6f210 | ||
|
|
10579cceb2 | ||
|
|
25702e01ce | ||
|
|
ecc5c44d9a | ||
|
|
f5a0b920a2 | ||
|
|
2caa016fe1 | ||
|
|
6861fedd09 | ||
|
|
3d072112ce | ||
|
|
bdfea046db | ||
|
|
a2d08fb63b | ||
|
|
5a55135d89 | ||
|
|
8870d36e0f | ||
|
|
eb923c27ca | ||
|
|
9903abc704 | ||
|
|
225a1fbf35 | ||
|
|
93159bccbf | ||
|
|
fab8ec4f54 | ||
|
|
a3035c53ea | ||
|
|
36c416e143 | ||
|
|
e1e0304a96 | ||
|
|
71c3a7c8f2 | ||
|
|
ecdfff5a42 | ||
|
|
4b948c5d74 | ||
|
|
cd56c51e2d | ||
|
|
43e39d7f68 | ||
|
|
7a17925495 | ||
|
|
5acb2530b4 | ||
|
|
44a6787359 | ||
|
|
42e6b7db32 | ||
|
|
2c02f8bace | ||
|
|
2ec20e576b | ||
|
|
20f47fec7a | ||
|
|
65210f2d97 | ||
|
|
af0b7ffae7 |
No files matched your search
@@ -1,5 +1,2 @@
|
||||
packages/core/migration/**/snapshot.json linguist-generated
|
||||
packages/core/src/database/migration.gen.ts linguist-generated
|
||||
packages/core/src/models-dev/snapshot.txt linguist-generated
|
||||
packages/core/src/**/*.txt text eol=lf
|
||||
packages/httpapi-codegen/test/generated/*.ts text eol=lf
|
||||
@@ -20,3 +20,4 @@ Slickstef11
|
||||
usrnk1
|
||||
vimtor
|
||||
StarpTech
|
||||
vaprdev
|
||||
@@ -1,10 +1,6 @@
|
||||
name: "Setup Bun"
|
||||
description: "Setup Bun with caching and install dependencies"
|
||||
inputs:
|
||||
bun-version:
|
||||
description: "Bun version to install instead of the root packageManager version"
|
||||
required: false
|
||||
default: ""
|
||||
install-flags:
|
||||
description: "Additional flags to pass to 'bun install'"
|
||||
required: false
|
||||
@@ -19,57 +15,50 @@ runs:
|
||||
with:
|
||||
node-version: "24"
|
||||
|
||||
- name: Get baseline download URL
|
||||
- name: Get Bun version and baseline download URL
|
||||
id: bun-url
|
||||
shell: bash
|
||||
run: |
|
||||
V=$(node -p "require('./package.json').packageManager.split('@')[1]")
|
||||
# Bun 1.4.2 includes the patched peer-variant fix in oven-sh/bun#33646.
|
||||
if [ "$RUNNER_OS" = "Windows" ]; then V=1.4.2; fi
|
||||
echo "version=$V" >> "$GITHUB_OUTPUT"
|
||||
if [ "$RUNNER_ARCH" = "X64" ]; then
|
||||
V="${{ inputs.bun-version }}"
|
||||
if [ -z "$V" ]; then V=$(node -p "require('./package.json').packageManager.split('@')[1]"); fi
|
||||
TAG=$([ "$V" = "canary" ] && echo "canary" || echo "bun-v${V}")
|
||||
case "$RUNNER_OS" in
|
||||
macOS) OS=darwin ;;
|
||||
Linux) OS=linux ;;
|
||||
Windows) OS=windows ;;
|
||||
esac
|
||||
echo "url=https://github.com/oven-sh/bun/releases/download/${TAG}/bun-${OS}-x64-baseline.zip" >> "$GITHUB_OUTPUT"
|
||||
echo "url=https://github.com/oven-sh/bun/releases/download/bun-v${V}/bun-${OS}-x64-baseline.zip" >> "$GITHUB_OUTPUT"
|
||||
fi
|
||||
|
||||
- name: Setup Bun
|
||||
id: setup-bun
|
||||
uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2.2.0
|
||||
with:
|
||||
bun-version: ${{ !steps.bun-url.outputs.url && inputs.bun-version || '' }}
|
||||
bun-version-file: ${{ !steps.bun-url.outputs.url && !inputs.bun-version && 'package.json' || '' }}
|
||||
bun-version: ${{ !steps.bun-url.outputs.url && steps.bun-url.outputs.version || '' }}
|
||||
bun-download-url: ${{ steps.bun-url.outputs.url }}
|
||||
|
||||
- name: Get cache directory
|
||||
id: cache
|
||||
shell: bash
|
||||
run: |
|
||||
echo "dir=$(bun pm cache)" >> "$GITHUB_OUTPUT"
|
||||
echo "version=$(bun --version)" >> "$GITHUB_OUTPUT"
|
||||
run: echo "dir=$(bun pm cache)" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Restore Bun dependencies
|
||||
id: bun-cache
|
||||
uses: actions/cache/restore@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0
|
||||
with:
|
||||
path: ${{ steps.cache.outputs.dir }}
|
||||
key: ${{ runner.os }}-${{ runner.arch }}-bun-${{ steps.cache.outputs.version }}-${{ hashFiles('bun.lock', 'patches/**') }}
|
||||
key: ${{ runner.os }}-bun-${{ steps.setup-bun.outputs.bun-version }}-${{ hashFiles('**/bun.lock') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-bun-${{ steps.setup-bun.outputs.bun-version }}-
|
||||
|
||||
- name: Install setuptools for distutils compatibility
|
||||
run: python3 -m pip install setuptools || pip install setuptools || true
|
||||
shell: bash
|
||||
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
# Workaround for patched peer variants
|
||||
# e.g. ./patches/ for standard-openapi
|
||||
# https://github.com/oven-sh/bun/issues/28147
|
||||
if [ "$RUNNER_OS" = "Windows" ]; then
|
||||
bun install --frozen-lockfile --linker hoisted ${{ inputs.install-flags }}
|
||||
else
|
||||
bun install --frozen-lockfile ${{ inputs.install-flags }}
|
||||
fi
|
||||
run: bun install ${{ inputs.install-flags }}
|
||||
shell: bash
|
||||
|
||||
- name: Save Bun dependencies
|
||||
@@ -77,4 +66,4 @@ runs:
|
||||
uses: actions/cache/save@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0
|
||||
with:
|
||||
path: ${{ steps.cache.outputs.dir }}
|
||||
key: ${{ steps.bun-cache.outputs.cache-primary-key }}
|
||||
key: ${{ runner.os }}-bun-${{ steps.setup-bun.outputs.bun-version }}-${{ hashFiles('**/bun.lock') }}
|
||||
@@ -1,26 +0,0 @@
|
||||
name: check
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [dev, v2]
|
||||
pull_request:
|
||||
branches: [dev, v2]
|
||||
workflow_dispatch:
|
||||
|
||||
concurrency:
|
||||
group: ${{ case(github.ref == 'refs/heads/dev', format('{0}-{1}', github.workflow, github.run_id), format('{0}-{1}', github.workflow, github.event.pull_request.number || github.ref)) }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
check:
|
||||
name: typecheck
|
||||
runs-on: blacksmith-4vcpu-ubuntu-2404
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
|
||||
|
||||
- name: Setup Bun
|
||||
uses: ./.github/actions/setup-bun
|
||||
|
||||
- name: Run checks
|
||||
run: bun run check
|
||||
@@ -1,34 +0,0 @@
|
||||
name: deploy-files
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- dev
|
||||
- v2
|
||||
workflow_dispatch:
|
||||
|
||||
concurrency:
|
||||
group: deploy-files-${{ github.ref_name }}
|
||||
cancel-in-progress: false
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
deploy:
|
||||
if: github.repository == 'anomalyco/opencode' && (github.ref_name == 'dev' || github.ref_name == 'v2')
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@f43a0e5ff2bd294095638e18286ca9a3d1956744 # v3.6.0
|
||||
|
||||
- uses: ./.github/actions/setup-bun
|
||||
|
||||
- name: Typecheck
|
||||
working-directory: services/files
|
||||
run: bun typecheck
|
||||
|
||||
- name: Deploy
|
||||
working-directory: services/files
|
||||
run: bun run deploy --env ${{ github.ref_name == 'v2' && 'production' || 'dev' }}
|
||||
env:
|
||||
CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
|
||||
@@ -1,37 +0,0 @@
|
||||
name: deploy-posts
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- v2
|
||||
paths:
|
||||
- packages/posts/**
|
||||
- bun.lock
|
||||
- .github/workflows/deploy-posts.yml
|
||||
workflow_dispatch:
|
||||
|
||||
concurrency:
|
||||
group: deploy-posts-${{ github.ref_name }}
|
||||
cancel-in-progress: false
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
deploy:
|
||||
if: github.repository == 'anomalyco/opencode' && github.ref_name == 'v2'
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@f43a0e5ff2bd294095638e18286ca9a3d1956744 # v3.6.0
|
||||
|
||||
- uses: ./.github/actions/setup-bun
|
||||
|
||||
- name: Build
|
||||
working-directory: packages/posts
|
||||
run: bun run build
|
||||
|
||||
- name: Deploy
|
||||
working-directory: packages/posts
|
||||
run: bun run deploy
|
||||
env:
|
||||
CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
|
||||
@@ -1,36 +0,0 @@
|
||||
name: deploy-www
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- dev
|
||||
- v2
|
||||
workflow_dispatch:
|
||||
|
||||
concurrency:
|
||||
group: deploy-www-${{ github.ref_name }}
|
||||
cancel-in-progress: false
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
deploy:
|
||||
if: github.repository == 'anomalyco/opencode' && (github.ref_name == 'dev' || github.ref_name == 'v2')
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@f43a0e5ff2bd294095638e18286ca9a3d1956744 # v3.6.0
|
||||
|
||||
- uses: ./.github/actions/setup-bun
|
||||
|
||||
- name: Build
|
||||
working-directory: services/www
|
||||
run: bun run build
|
||||
env:
|
||||
CLOUDFLARE_ENV: ${{ github.ref_name == 'v2' && 'production' || 'dev' }}
|
||||
|
||||
- name: Deploy
|
||||
working-directory: services/www
|
||||
run: bun run deploy
|
||||
env:
|
||||
CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
|
||||
@@ -5,7 +5,6 @@ on:
|
||||
branches:
|
||||
- dev
|
||||
- production
|
||||
- beta
|
||||
workflow_dispatch:
|
||||
|
||||
concurrency: ${{ github.workflow }}-${{ github.ref }}
|
||||
@@ -16,7 +15,7 @@ permissions:
|
||||
|
||||
jobs:
|
||||
deploy:
|
||||
if: github.repository == 'anomalyco/opencode' && (github.ref_name == 'dev' || github.ref_name == 'production' || github.ref_name == 'beta')
|
||||
if: github.repository == 'anomalyco/opencode' && (github.ref_name == 'dev' || github.ref_name == 'production')
|
||||
runs-on: ubuntu-latest
|
||||
environment: ${{ github.ref_name }}
|
||||
steps:
|
||||
@@ -29,7 +28,6 @@ jobs:
|
||||
node-version: "24"
|
||||
|
||||
- uses: aws-actions/configure-aws-credentials@7474bc4690e29a8392af63c5b98e7449536d5c3a # v4.3.1
|
||||
if: github.ref_name != 'beta'
|
||||
with:
|
||||
role-to-assume: ${{ vars.AWS_DEPLOY_ROLE_ARN }}
|
||||
role-session-name: opencode-${{ github.run_id }}
|
||||
@@ -37,6 +35,7 @@ jobs:
|
||||
|
||||
- run: bun sst deploy --stage=${{ github.ref_name }}
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ github.token }}
|
||||
CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
|
||||
PLANETSCALE_SERVICE_TOKEN_NAME: ${{ secrets.PLANETSCALE_SERVICE_TOKEN_NAME }}
|
||||
PLANETSCALE_SERVICE_TOKEN: ${{ secrets.PLANETSCALE_SERVICE_TOKEN }}
|
||||
|
||||
@@ -54,7 +54,7 @@ jobs:
|
||||
"webfetch": "deny"
|
||||
}
|
||||
run: |
|
||||
opencode run -m opencode/claude-sonnet-4-6 "A new issue has been created:
|
||||
opencode run -m opencode/gpt-6-luna --variant high "A new issue has been created:
|
||||
|
||||
Issue number: ${{ github.event.issue.number }}
|
||||
Issue author association: ${{ github.event.issue.author_association }}
|
||||
@@ -188,7 +188,7 @@ jobs:
|
||||
"webfetch": "deny"
|
||||
}
|
||||
run: |
|
||||
opencode run -m opencode/claude-sonnet-4-6 "Issue #${{ github.event.issue.number }} was previously flagged as non-compliant and has been edited.
|
||||
opencode run -m opencode/gpt-6-luna --variant high "Issue #${{ github.event.issue.number }} was previously flagged as non-compliant and has been edited.
|
||||
Issue author association: ${{ github.event.issue.author_association }}
|
||||
|
||||
Lookup this issue with gh issue view ${{ github.event.issue.number }}.
|
||||
|
||||
@@ -0,0 +1,51 @@
|
||||
name: generate
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- dev
|
||||
|
||||
jobs:
|
||||
generate:
|
||||
runs-on: blacksmith-4vcpu-ubuntu-2404
|
||||
permissions:
|
||||
contents: write
|
||||
pull-requests: write
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
|
||||
|
||||
- name: Setup Bun
|
||||
uses: ./.github/actions/setup-bun
|
||||
|
||||
- name: Setup git committer
|
||||
id: committer
|
||||
uses: ./.github/actions/setup-git-committer
|
||||
with:
|
||||
opencode-app-id: ${{ vars.OPENCODE_APP_ID }}
|
||||
opencode-app-secret: ${{ secrets.OPENCODE_APP_SECRET }}
|
||||
|
||||
- name: 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 add -A
|
||||
git commit -m "chore: generate" --allow-empty
|
||||
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
|
||||
@@ -0,0 +1,62 @@
|
||||
name: models-snapshot
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
schedule:
|
||||
- cron: "0 12 * * *"
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}
|
||||
cancel-in-progress: false
|
||||
|
||||
jobs:
|
||||
refresh:
|
||||
if: github.repository == 'anomalyco/opencode'
|
||||
runs-on: blacksmith-4vcpu-ubuntu-2404
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
|
||||
with:
|
||||
persist-credentials: false
|
||||
ref: v2
|
||||
|
||||
# The refresh script only needs `fetch`; skip the monorepo install.
|
||||
- name: Setup Bun
|
||||
uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2.2.0
|
||||
with:
|
||||
bun-version-file: package.json
|
||||
|
||||
- name: Setup git committer
|
||||
uses: ./.github/actions/setup-git-committer
|
||||
with:
|
||||
opencode-app-id: ${{ vars.OPENCODE_APP_ID }}
|
||||
opencode-app-secret: ${{ secrets.OPENCODE_APP_SECRET }}
|
||||
|
||||
- name: Refresh snapshot
|
||||
run: bun packages/core/script/update-models-snapshot.ts
|
||||
|
||||
- name: Commit changes
|
||||
run: |
|
||||
set -euo pipefail
|
||||
|
||||
FILE="packages/core/src/models-dev/snapshot.txt"
|
||||
|
||||
if [ -z "$(git status --short -- "$FILE")" ]; then
|
||||
echo "No changes to commit"
|
||||
echo "### models.dev snapshot" >> "$GITHUB_STEP_SUMMARY"
|
||||
echo "Status: no changes" >> "$GITHUB_STEP_SUMMARY"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
git add "$FILE"
|
||||
git commit -m "chore(core): refresh bundled models.dev snapshot"
|
||||
|
||||
git pull --rebase --autostash origin v2
|
||||
git push origin HEAD:v2 --no-verify
|
||||
|
||||
echo "### models.dev snapshot" >> "$GITHUB_STEP_SUMMARY"
|
||||
echo "Status: committed $(git rev-parse --short HEAD)" >> "$GITHUB_STEP_SUMMARY"
|
||||
@@ -37,7 +37,7 @@ jobs:
|
||||
echo "=== Flake structure ==="
|
||||
nix flake show --all-systems
|
||||
|
||||
SYSTEMS="x86_64-linux aarch64-linux aarch64-darwin"
|
||||
SYSTEMS="x86_64-linux aarch64-linux x86_64-darwin aarch64-darwin"
|
||||
PACKAGES="opencode"
|
||||
# TODO: move 'desktop' to PACKAGES when #11755 is fixed
|
||||
OPTIONAL_PACKAGES="desktop"
|
||||
|
||||
@@ -6,12 +6,11 @@ permissions:
|
||||
on:
|
||||
workflow_dispatch:
|
||||
push:
|
||||
branches: [dev, beta, v2]
|
||||
branches: [dev, beta]
|
||||
paths:
|
||||
- "bun.lock"
|
||||
- "package.json"
|
||||
- "packages/*/package.json"
|
||||
- "services/*/package.json"
|
||||
- "flake.lock"
|
||||
- "nix/node_modules.nix"
|
||||
- "nix/scripts/**"
|
||||
@@ -34,6 +33,8 @@ jobs:
|
||||
runner: blacksmith-4vcpu-ubuntu-2404
|
||||
- system: aarch64-linux
|
||||
runner: blacksmith-4vcpu-ubuntu-2404-arm
|
||||
- system: x86_64-darwin
|
||||
runner: macos-15-intel
|
||||
- system: aarch64-darwin
|
||||
runner: macos-latest
|
||||
runs-on: ${{ matrix.runner }}
|
||||
@@ -124,7 +125,7 @@ jobs:
|
||||
|
||||
[ -f "$HASH_FILE" ] || echo '{"nodeModules":{}}' > "$HASH_FILE"
|
||||
|
||||
for SYSTEM in x86_64-linux aarch64-linux aarch64-darwin; do
|
||||
for SYSTEM in x86_64-linux aarch64-linux x86_64-darwin aarch64-darwin; do
|
||||
FILE="hashes/hash-${SYSTEM}/hash.txt"
|
||||
if [ -f "$FILE" ]; then
|
||||
HASH="$(tr -d '[:space:]' < "$FILE")"
|
||||
|
||||
@@ -135,16 +135,7 @@ jobs:
|
||||
|
||||
const linkedIssues = result.repository.pullRequest.closingIssuesReferences.totalCount;
|
||||
|
||||
// GitHub only populates closingIssuesReferences when a PR targets the repository's
|
||||
// default branch (dev). PRs targeting other branches like v2 always return totalCount 0.
|
||||
// Fall back to checking the PR description for closing keywords (e.g. Closes #123).
|
||||
const body = pr.body || '';
|
||||
const issueMatch = body.match(/### Issue for this PR\s*\n([\s\S]*?)(?=###|$)/);
|
||||
const issueContent = issueMatch ? issueMatch[1].trim() : body;
|
||||
const hasBodyIssueRef = /(closes|fixes|resolves)\s+#\d+/i.test(issueContent) || /#\d+/.test(issueContent);
|
||||
const hasLinkedIssue = linkedIssues > 0 || hasBodyIssueRef;
|
||||
|
||||
if (!hasLinkedIssue) {
|
||||
if (linkedIssues === 0) {
|
||||
await addLabel('needs:issue');
|
||||
await comment('issue', `Thanks for your contribution!
|
||||
|
||||
|
||||
@@ -7,7 +7,6 @@ on:
|
||||
- ci
|
||||
- dev
|
||||
- beta
|
||||
- v2
|
||||
- fix/npm-native-binary-install
|
||||
- snapshot-*
|
||||
workflow_dispatch:
|
||||
@@ -24,21 +23,14 @@ on:
|
||||
description: "Override version (optional)"
|
||||
required: false
|
||||
type: string
|
||||
release_notes:
|
||||
description: "Reviewed V2 release notes for the Discord announcement (optional)"
|
||||
required: false
|
||||
type: string
|
||||
|
||||
concurrency: ${{ github.workflow }}-${{ github.ref }}-${{ (github.ref_name == 'v2' && (inputs.version || inputs.bump) && 'release') || inputs.version || inputs.bump }}
|
||||
concurrency: ${{ github.workflow }}-${{ github.ref }}-${{ inputs.version || inputs.bump }}
|
||||
|
||||
permissions:
|
||||
id-token: write
|
||||
contents: write
|
||||
packages: write
|
||||
|
||||
env:
|
||||
OPENCODE_CHANNEL: ${{ (github.ref_name == 'v2' && !inputs.bump && !inputs.version && 'dev') || '' }}
|
||||
|
||||
jobs:
|
||||
version:
|
||||
runs-on: blacksmith-4vcpu-ubuntu-2404
|
||||
@@ -50,13 +42,6 @@ jobs:
|
||||
|
||||
- uses: ./.github/actions/setup-bun
|
||||
|
||||
- name: Deploy update service
|
||||
if: github.ref_name == 'v2'
|
||||
working-directory: services/update
|
||||
run: bun run deploy
|
||||
env:
|
||||
CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
|
||||
|
||||
- name: Setup git committer
|
||||
id: committer
|
||||
uses: ./.github/actions/setup-git-committer
|
||||
@@ -86,7 +71,6 @@ jobs:
|
||||
build-cli:
|
||||
needs: version
|
||||
runs-on: blacksmith-4vcpu-ubuntu-2404
|
||||
timeout-minutes: 30
|
||||
if: github.repository == 'anomalyco/opencode'
|
||||
steps:
|
||||
- uses: actions/checkout@f43a0e5ff2bd294095638e18286ca9a3d1956744 # v3.6.0
|
||||
@@ -94,8 +78,6 @@ jobs:
|
||||
fetch-tags: true
|
||||
|
||||
- uses: ./.github/actions/setup-bun
|
||||
with:
|
||||
bun-version: 1.4.2
|
||||
|
||||
- name: Setup git committer
|
||||
id: committer
|
||||
@@ -104,27 +86,18 @@ jobs:
|
||||
opencode-app-id: ${{ vars.OPENCODE_APP_ID }}
|
||||
opencode-app-secret: ${{ secrets.OPENCODE_APP_SECRET }}
|
||||
|
||||
- name: Build legacy CLI
|
||||
if: github.ref_name != 'v2' && github.ref_name != 'beta'
|
||||
run: ./packages/opencode/script/build.ts ${{ (github.ref_name == 'beta' && '--sourcemaps') || '' }}
|
||||
env:
|
||||
OPENCODE_VERSION: ${{ needs.version.outputs.version }}
|
||||
OPENCODE_RELEASE: ${{ needs.version.outputs.release }}
|
||||
GH_REPO: ${{ needs.version.outputs.repo }}
|
||||
GH_TOKEN: ${{ steps.committer.outputs.token }}
|
||||
|
||||
- name: Build preview CLI
|
||||
- name: Build
|
||||
id: build
|
||||
run: ./packages/cli/script/build.ts ${{ (github.ref_name == 'beta' && '--sourcemaps') || '' }}
|
||||
run: |
|
||||
./packages/opencode/script/build.ts ${{ (github.ref_name == 'beta' && '--sourcemaps') || '' }}
|
||||
./packages/cli/script/build.ts ${{ (github.ref_name == 'beta' && '--sourcemaps') || '' }}
|
||||
env:
|
||||
BUN_COMPILE_RELEASE: bun-v1.4.2
|
||||
OPENCODE_VERSION: ${{ needs.version.outputs.version }}
|
||||
OPENCODE_RELEASE: ${{ needs.version.outputs.release }}
|
||||
GH_REPO: ${{ needs.version.outputs.repo }}
|
||||
GH_TOKEN: ${{ steps.committer.outputs.token }}
|
||||
|
||||
- uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
|
||||
if: github.ref_name != 'v2' && github.ref_name != 'beta'
|
||||
with:
|
||||
name: opencode-cli
|
||||
path: |
|
||||
@@ -132,21 +105,22 @@ jobs:
|
||||
packages/opencode/dist/opencode-linux*
|
||||
|
||||
- uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
|
||||
if: github.ref_name != 'v2' && github.ref_name != 'beta'
|
||||
with:
|
||||
name: opencode-cli-windows
|
||||
path: packages/opencode/dist/opencode-windows*
|
||||
|
||||
- uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
|
||||
with:
|
||||
name: opencode-preview-cli-unsigned
|
||||
name: opencode-preview-cli
|
||||
path: packages/cli/dist/cli-*
|
||||
|
||||
outputs:
|
||||
version: ${{ needs.version.outputs.version }}
|
||||
|
||||
sign-cli-macos:
|
||||
needs: build-cli
|
||||
needs:
|
||||
- build-cli
|
||||
- version
|
||||
runs-on: macos-26
|
||||
if: github.repository == 'anomalyco/opencode'
|
||||
steps:
|
||||
@@ -160,8 +134,15 @@ jobs:
|
||||
|
||||
- uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0
|
||||
with:
|
||||
name: opencode-preview-cli-unsigned
|
||||
path: packages/cli/dist
|
||||
name: opencode-cli
|
||||
path: packages/opencode/dist
|
||||
|
||||
- name: Setup git committer
|
||||
id: committer
|
||||
uses: ./.github/actions/setup-git-committer
|
||||
with:
|
||||
opencode-app-id: ${{ vars.OPENCODE_APP_ID }}
|
||||
opencode-app-secret: ${{ secrets.OPENCODE_APP_SECRET }}
|
||||
|
||||
- name: Sign macOS CLI binaries
|
||||
run: |
|
||||
@@ -171,118 +152,59 @@ jobs:
|
||||
exit 1
|
||||
fi
|
||||
|
||||
found=0
|
||||
for file in packages/cli/dist/cli-darwin-*/bin/opencode; do
|
||||
if [ ! -f "$file" ]; then
|
||||
continue
|
||||
fi
|
||||
found=1
|
||||
for name in opencode-darwin-arm64 opencode-darwin-x64 opencode-darwin-x64-baseline; do
|
||||
file="packages/opencode/dist/$name/bin/opencode"
|
||||
chmod +x "$file"
|
||||
codesign \
|
||||
--force \
|
||||
--timestamp \
|
||||
--options runtime \
|
||||
--entitlements packages/cli/script/entitlements.plist \
|
||||
--entitlements packages/opencode/script/entitlements.plist \
|
||||
--sign "$identity" \
|
||||
"$file"
|
||||
codesign --verify --deep --strict --verbose=4 "$file"
|
||||
codesign --display --requirements - "$file"
|
||||
done
|
||||
|
||||
if [ "$found" -eq 0 ]; then
|
||||
echo "No macOS CLI binaries found"
|
||||
exit 1
|
||||
fi
|
||||
- name: Smoke test signed binary
|
||||
run: ./packages/opencode/dist/opencode-darwin-arm64/bin/opencode --version
|
||||
|
||||
- uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
|
||||
with:
|
||||
name: opencode-preview-cli-macos
|
||||
path: packages/cli/dist/cli-*
|
||||
if-no-files-found: error
|
||||
- name: Repack macOS CLI archives
|
||||
working-directory: packages/opencode/dist
|
||||
run: |
|
||||
for name in opencode-darwin-arm64 opencode-darwin-x64 opencode-darwin-x64-baseline; do
|
||||
rm -f "$name.zip"
|
||||
(cd "$name/bin" && zip -r "../../$name.zip" *)
|
||||
done
|
||||
|
||||
build-node-app-archive:
|
||||
needs: version
|
||||
runs-on: blacksmith-4vcpu-ubuntu-2404
|
||||
timeout-minutes: 30
|
||||
if: github.repository == 'anomalyco/opencode' && !(github.ref_name == 'v2' && (inputs.bump || inputs.version))
|
||||
steps:
|
||||
- uses: actions/checkout@f43a0e5ff2bd294095638e18286ca9a3d1956744 # v3.6.0
|
||||
|
||||
- uses: ./.github/actions/setup-bun
|
||||
|
||||
- name: Build app archive
|
||||
run: bun packages/cli/script/build-node.ts --app-archive-only --app-archive=.cache/app-archive.bin --skip-install
|
||||
- name: Upload signed macOS CLI release assets
|
||||
if: needs.version.outputs.release != ''
|
||||
env:
|
||||
OPENCODE_VERSION: ${{ needs.version.outputs.version }}
|
||||
OPENCODE_RELEASE: ${{ needs.version.outputs.release }}
|
||||
GH_TOKEN: ${{ steps.committer.outputs.token }}
|
||||
run: |
|
||||
gh release upload "v${{ needs.version.outputs.version }}" \
|
||||
packages/opencode/dist/opencode-darwin-arm64.zip \
|
||||
packages/opencode/dist/opencode-darwin-x64.zip \
|
||||
packages/opencode/dist/opencode-darwin-x64-baseline.zip \
|
||||
--clobber \
|
||||
--repo "${{ needs.version.outputs.repo }}"
|
||||
|
||||
- uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
|
||||
with:
|
||||
name: opencode-node-app-archive
|
||||
path: packages/cli/.cache/app-archive.bin
|
||||
if-no-files-found: error
|
||||
|
||||
build-node-cli:
|
||||
needs:
|
||||
- version
|
||||
- build-node-app-archive
|
||||
if: github.repository == 'anomalyco/opencode' && !(github.ref_name == 'v2' && (inputs.bump || inputs.version))
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
settings:
|
||||
- target: linux-arm64
|
||||
host: blacksmith-4vcpu-ubuntu-2404-arm
|
||||
- target: linux-x64
|
||||
host: blacksmith-4vcpu-ubuntu-2404
|
||||
- target: darwin-arm64
|
||||
host: macos-26
|
||||
- target: windows-arm64
|
||||
host: blacksmith-4vcpu-windows-2025
|
||||
bun_install_flags: --cpu=*
|
||||
- target: windows-x64
|
||||
host: blacksmith-4vcpu-windows-2025
|
||||
runs-on: ${{ matrix.settings.host }}
|
||||
defaults:
|
||||
run:
|
||||
shell: bash
|
||||
steps:
|
||||
- uses: actions/checkout@f43a0e5ff2bd294095638e18286ca9a3d1956744 # v3.6.0
|
||||
|
||||
- uses: ./.github/actions/setup-bun
|
||||
with:
|
||||
install-flags: ${{ matrix.settings.bun_install_flags }}
|
||||
|
||||
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
|
||||
with:
|
||||
node-version: "26.4.0"
|
||||
|
||||
- uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0
|
||||
with:
|
||||
name: opencode-node-app-archive
|
||||
path: packages/cli/.cache
|
||||
|
||||
- name: Build
|
||||
run: bun packages/cli/script/build-node.ts --target=${{ matrix.settings.target }} --skip-install --outdir=dist/node --app-archive=.cache/app-archive.bin
|
||||
env:
|
||||
OPENCODE_VERSION: ${{ needs.version.outputs.version }}
|
||||
OPENCODE_RELEASE: ${{ needs.version.outputs.release }}
|
||||
|
||||
- name: Verify service lifecycle
|
||||
if: matrix.settings.target != 'windows-arm64'
|
||||
working-directory: packages/cli
|
||||
run: bun run script/service-smoke.ts --node
|
||||
|
||||
- uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
|
||||
with:
|
||||
name: opencode-node-cli-${{ matrix.settings.target }}
|
||||
path: packages/cli/dist/node/cli-node-*
|
||||
name: opencode-cli-signed-macos
|
||||
path: |
|
||||
packages/opencode/dist/opencode-darwin-arm64
|
||||
packages/opencode/dist/opencode-darwin-x64
|
||||
packages/opencode/dist/opencode-darwin-x64-baseline
|
||||
packages/opencode/dist/opencode-darwin-*.zip
|
||||
if-no-files-found: error
|
||||
|
||||
sign-cli-windows:
|
||||
needs:
|
||||
- sign-cli-macos
|
||||
- build-cli
|
||||
- version
|
||||
runs-on: blacksmith-4vcpu-windows-2025
|
||||
if: github.repository == 'anomalyco/opencode' && (github.ref_name == 'v2' || github.ref_name == 'beta')
|
||||
if: github.repository == 'anomalyco/opencode'
|
||||
env:
|
||||
AZURE_CLIENT_ID: ${{ secrets.AZURE_CLIENT_ID }}
|
||||
AZURE_TENANT_ID: ${{ secrets.AZURE_TENANT_ID }}
|
||||
@@ -295,8 +217,15 @@ jobs:
|
||||
|
||||
- uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0
|
||||
with:
|
||||
name: opencode-preview-cli-macos
|
||||
path: packages/cli/dist
|
||||
name: opencode-cli-windows
|
||||
path: packages/opencode/dist
|
||||
|
||||
- name: Setup git committer
|
||||
id: committer
|
||||
uses: ./.github/actions/setup-git-committer
|
||||
with:
|
||||
opencode-app-id: ${{ vars.OPENCODE_APP_ID }}
|
||||
opencode-app-secret: ${{ secrets.OPENCODE_APP_SECRET }}
|
||||
|
||||
- name: Azure login
|
||||
uses: azure/login@a457da9ea143d694b1b9c7c869ebb04ebe844ef5 # v2.3.0
|
||||
@@ -311,9 +240,9 @@ jobs:
|
||||
signing-account-name: ${{ env.AZURE_TRUSTED_SIGNING_ACCOUNT_NAME }}
|
||||
certificate-profile-name: ${{ env.AZURE_TRUSTED_SIGNING_CERTIFICATE_PROFILE }}
|
||||
files: |
|
||||
${{ github.workspace }}\packages\cli\dist\cli-windows-arm64\bin\opencode.exe
|
||||
${{ github.workspace }}\packages\cli\dist\cli-windows-x64\bin\opencode.exe
|
||||
${{ github.workspace }}\packages\cli\dist\cli-windows-x64-baseline\bin\opencode.exe
|
||||
${{ github.workspace }}\packages\opencode\dist\opencode-windows-arm64\bin\opencode.exe
|
||||
${{ github.workspace }}\packages\opencode\dist\opencode-windows-x64\bin\opencode.exe
|
||||
${{ github.workspace }}\packages\opencode\dist\opencode-windows-x64-baseline\bin\opencode.exe
|
||||
exclude-environment-credential: true
|
||||
exclude-workload-identity-credential: true
|
||||
exclude-managed-identity-credential: true
|
||||
@@ -329,9 +258,9 @@ jobs:
|
||||
shell: pwsh
|
||||
run: |
|
||||
$files = @(
|
||||
"${{ github.workspace }}\packages\cli\dist\cli-windows-arm64\bin\opencode.exe",
|
||||
"${{ github.workspace }}\packages\cli\dist\cli-windows-x64\bin\opencode.exe",
|
||||
"${{ github.workspace }}\packages\cli\dist\cli-windows-x64-baseline\bin\opencode.exe"
|
||||
"${{ github.workspace }}\packages\opencode\dist\opencode-windows-arm64\bin\opencode.exe",
|
||||
"${{ github.workspace }}\packages\opencode\dist\opencode-windows-x64\bin\opencode.exe",
|
||||
"${{ github.workspace }}\packages\opencode\dist\opencode-windows-x64-baseline\bin\opencode.exe"
|
||||
)
|
||||
|
||||
foreach ($file in $files) {
|
||||
@@ -341,17 +270,39 @@ jobs:
|
||||
}
|
||||
}
|
||||
|
||||
- name: Repack Windows CLI archives
|
||||
working-directory: packages/opencode/dist
|
||||
shell: pwsh
|
||||
run: |
|
||||
Compress-Archive -Path "opencode-windows-arm64\bin\*" -DestinationPath "opencode-windows-arm64.zip" -Force
|
||||
Compress-Archive -Path "opencode-windows-x64\bin\*" -DestinationPath "opencode-windows-x64.zip" -Force
|
||||
Compress-Archive -Path "opencode-windows-x64-baseline\bin\*" -DestinationPath "opencode-windows-x64-baseline.zip" -Force
|
||||
|
||||
- name: Upload signed Windows CLI release assets
|
||||
if: needs.version.outputs.release != ''
|
||||
shell: pwsh
|
||||
env:
|
||||
GH_TOKEN: ${{ steps.committer.outputs.token }}
|
||||
run: |
|
||||
gh release upload "v${{ needs.version.outputs.version }}" `
|
||||
"${{ github.workspace }}\packages\opencode\dist\opencode-windows-arm64.zip" `
|
||||
"${{ github.workspace }}\packages\opencode\dist\opencode-windows-x64.zip" `
|
||||
"${{ github.workspace }}\packages\opencode\dist\opencode-windows-x64-baseline.zip" `
|
||||
--clobber `
|
||||
--repo "${{ needs.version.outputs.repo }}"
|
||||
|
||||
- uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
|
||||
with:
|
||||
name: opencode-preview-cli
|
||||
path: packages/cli/dist/cli-*
|
||||
if-no-files-found: error
|
||||
name: opencode-cli-signed-windows
|
||||
path: |
|
||||
packages/opencode/dist/opencode-windows-arm64
|
||||
packages/opencode/dist/opencode-windows-x64
|
||||
packages/opencode/dist/opencode-windows-x64-baseline
|
||||
|
||||
build-electron:
|
||||
needs:
|
||||
- version
|
||||
- sign-cli-windows
|
||||
if: github.repository == 'anomalyco/opencode' && (github.ref_name != 'v2' || needs.version.outputs.release != '')
|
||||
if: github.repository == 'anomalyco/opencode'
|
||||
continue-on-error: false
|
||||
env:
|
||||
AZURE_CLIENT_ID: ${{ secrets.AZURE_CLIENT_ID }}
|
||||
@@ -389,11 +340,6 @@ jobs:
|
||||
steps:
|
||||
- uses: actions/checkout@f43a0e5ff2bd294095638e18286ca9a3d1956744 # v3.6.0
|
||||
|
||||
- uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0
|
||||
with:
|
||||
name: opencode-preview-cli
|
||||
path: packages/cli/dist
|
||||
|
||||
- uses: apple-actions/import-codesign-certs@8f3fb608891dd2244cdab3d69cd68c0d37a7fe93 # v2.0.0
|
||||
if: runner.os == 'macOS'
|
||||
with:
|
||||
@@ -451,8 +397,7 @@ jobs:
|
||||
env:
|
||||
OPENCODE_VERSION: ${{ needs.version.outputs.version }}
|
||||
OPENCODE_CHANNEL: ${{ (github.ref_name == 'beta' && 'beta') || 'prod' }}
|
||||
OPENCODE_CLI_TARGET: ${{ matrix.settings.target }}
|
||||
OPENCODE_CLI_DIST: ${{ github.workspace }}/packages/cli/dist
|
||||
RUST_TARGET: ${{ matrix.settings.target }}
|
||||
|
||||
- name: Build
|
||||
run: bun run build
|
||||
@@ -468,8 +413,6 @@ jobs:
|
||||
VITE_SENTRY_DSN: ${{ vars.WEB_SENTRY_DSN }}
|
||||
VITE_SENTRY_ENVIRONMENT: ${{ (github.ref_name == 'beta' && 'beta') || 'production' }}
|
||||
VITE_SENTRY_RELEASE: desktop@${{ needs.version.outputs.version }}
|
||||
OPENCODE_CLI_TARGET: ${{ matrix.settings.target }}
|
||||
OPENCODE_CLI_DIST: ${{ github.workspace }}/packages/cli/dist
|
||||
|
||||
- name: Package
|
||||
if: needs.version.outputs.release
|
||||
@@ -545,8 +488,6 @@ jobs:
|
||||
- version
|
||||
- build-cli
|
||||
- sign-cli-macos
|
||||
- build-node-app-archive
|
||||
- build-node-cli
|
||||
- sign-cli-windows
|
||||
- build-electron
|
||||
if: always() && !failure() && !cancelled()
|
||||
@@ -575,19 +516,24 @@ jobs:
|
||||
registry-url: "https://registry.npmjs.org"
|
||||
|
||||
- uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0
|
||||
if: github.ref_name != 'v2' && github.ref_name != 'beta'
|
||||
with:
|
||||
name: opencode-cli
|
||||
path: packages/opencode/dist
|
||||
|
||||
- name: Remove unsigned macOS CLI binaries
|
||||
run: rm -rf packages/opencode/dist/opencode-darwin*
|
||||
|
||||
- uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0
|
||||
with:
|
||||
name: opencode-cli-signed-macos
|
||||
path: packages/opencode/dist
|
||||
|
||||
- uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0
|
||||
if: github.ref_name != 'v2' && github.ref_name != 'beta'
|
||||
with:
|
||||
name: opencode-cli-windows
|
||||
path: packages/opencode/dist
|
||||
|
||||
- uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0
|
||||
if: github.ref_name != 'v2' && github.ref_name != 'beta'
|
||||
with:
|
||||
name: opencode-cli-signed-windows
|
||||
path: packages/opencode/dist
|
||||
@@ -597,13 +543,6 @@ jobs:
|
||||
name: opencode-preview-cli
|
||||
path: packages/cli/dist
|
||||
|
||||
- uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0
|
||||
if: needs.build-node-cli.result == 'success'
|
||||
with:
|
||||
pattern: opencode-node-cli-*
|
||||
path: packages/cli/dist/node
|
||||
merge-multiple: true
|
||||
|
||||
- uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0
|
||||
if: needs.version.outputs.release
|
||||
with:
|
||||
@@ -643,6 +582,19 @@ jobs:
|
||||
git config --global user.name "opencode"
|
||||
ssh-keyscan -H aur.archlinux.org >> ~/.ssh/known_hosts || true
|
||||
|
||||
- name: Upload desktop release assets
|
||||
if: needs.version.outputs.release
|
||||
env:
|
||||
GH_TOKEN: ${{ steps.committer.outputs.token }}
|
||||
run: |
|
||||
shopt -s nullglob
|
||||
files=(/tmp/desktop/*.{exe,blockmap,dmg,zip,AppImage,deb,rpm} /tmp/desktop/*.app.tar.gz)
|
||||
if (( ${#files[@]} == 0 )); then
|
||||
echo "No desktop release assets found"
|
||||
exit 1
|
||||
fi
|
||||
gh release upload "v${{ needs.version.outputs.version }}" "${files[@]}" --clobber --repo "${{ needs.version.outputs.repo }}"
|
||||
|
||||
- run: ./script/publish.ts
|
||||
env:
|
||||
OPENCODE_VERSION: ${{ needs.version.outputs.version }}
|
||||
@@ -654,22 +606,3 @@ jobs:
|
||||
LATEST_YML_DIR: /tmp/latest-yml
|
||||
TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }}
|
||||
TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY_PASSWORD }}
|
||||
OPENCODE_DESKTOP_DIST: /tmp/desktop
|
||||
CLOUDFLARE_ACCOUNT_ID: 15d29c8639fd3733b1b5486a2acfd968
|
||||
CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
|
||||
|
||||
notify-discord-v2:
|
||||
needs:
|
||||
- version
|
||||
- publish
|
||||
if: ${{ !cancelled() && github.repository == 'anomalyco/opencode' && github.ref_name == 'v2' && needs.version.outputs.release && needs.publish.result == 'success' }}
|
||||
runs-on: blacksmith-4vcpu-ubuntu-2404
|
||||
steps:
|
||||
# Unlike dev, V2 publishes a tag rather than a GitHub Release event.
|
||||
- name: Announce V2 release in Discord
|
||||
uses: SethCohen/github-releases-to-discord@24d166886aee4646d448c8a389ff9e1ebcab3682 # v1.20.0
|
||||
with:
|
||||
webhook_url: ${{ secrets.DISCORD_WEBHOOK }}
|
||||
release_name: OpenCode V2 ${{ needs.version.outputs.tag }}
|
||||
release_body: ${{ inputs.release_notes }}
|
||||
release_html_url: https://github.com/${{ github.repository }}/tree/${{ needs.version.outputs.tag }}
|
||||
@@ -4,7 +4,6 @@ on:
|
||||
push:
|
||||
branches:
|
||||
- dev
|
||||
- v2
|
||||
pull_request:
|
||||
workflow_dispatch:
|
||||
|
||||
@@ -22,36 +21,6 @@ env:
|
||||
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
|
||||
|
||||
jobs:
|
||||
affected:
|
||||
name: affected packages
|
||||
runs-on: blacksmith-4vcpu-ubuntu-2404
|
||||
outputs:
|
||||
app: ${{ steps.packages.outputs.app }}
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
|
||||
with:
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Setup Bun
|
||||
uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2.2.0
|
||||
with:
|
||||
bun-version-file: package.json
|
||||
|
||||
- name: Find affected packages
|
||||
id: packages
|
||||
env:
|
||||
TURBO_SCM_BASE: ${{ github.event_name == 'pull_request' && format('{0}^1', github.sha) || github.event.before }}
|
||||
TURBO_SCM_HEAD: ${{ github.sha }}
|
||||
run: |
|
||||
if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then
|
||||
echo "app=true" >> "$GITHUB_OUTPUT"
|
||||
exit 0
|
||||
fi
|
||||
bun x turbo@2.10.2 ls --affected --filter=@opencode/app --output=json > affected.json
|
||||
bun -e 'const result = await Bun.file("affected.json").json(); console.log(`app=${result.packages.count > 0}`)' >> "$GITHUB_OUTPUT"
|
||||
|
||||
unit:
|
||||
name: unit (${{ matrix.settings.name }})
|
||||
strategy:
|
||||
@@ -71,7 +40,6 @@ jobs:
|
||||
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
|
||||
with:
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
|
||||
@@ -81,14 +49,6 @@ jobs:
|
||||
- name: Setup Bun
|
||||
uses: ./.github/actions/setup-bun
|
||||
|
||||
- name: Test Effect simplification rules
|
||||
if: runner.os == 'Linux'
|
||||
run: bun run test:effect-simplification-rules
|
||||
|
||||
- name: Check Effect simplifications
|
||||
if: runner.os == 'Linux'
|
||||
run: bun run lint:effect-simplifications
|
||||
|
||||
- name: Configure git identity
|
||||
run: |
|
||||
git config --global user.email "bot@opencode.ai"
|
||||
@@ -105,75 +65,22 @@ jobs:
|
||||
|
||||
- name: Run unit tests
|
||||
timeout-minutes: 20
|
||||
run: |
|
||||
# The runners have four vCPUs, and each Bun test process performs its own concurrent work.
|
||||
if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then
|
||||
GITHUB_ACTIONS=false bun turbo test --concurrency=3
|
||||
exit 0
|
||||
fi
|
||||
GITHUB_ACTIONS=false bun turbo test --affected --concurrency=3
|
||||
run: GITHUB_ACTIONS=false bun turbo test
|
||||
env:
|
||||
OPENCODE_EXPERIMENTAL_DISABLE_FILEWATCHER: ${{ runner.os == 'Windows' && 'true' || 'false' }}
|
||||
TURBO_SCM_BASE: ${{ github.event_name == 'pull_request' && format('{0}^1', github.sha) || github.event.before }}
|
||||
TURBO_SCM_HEAD: ${{ github.sha }}
|
||||
|
||||
- name: Verify published codemode package
|
||||
if: runner.os == 'Linux'
|
||||
working-directory: packages/codemode
|
||||
run: bun run script/publish.ts --dry-run
|
||||
|
||||
- name: Verify packed workerd SDK
|
||||
if: runner.os == 'Linux'
|
||||
timeout-minutes: 15
|
||||
run: |
|
||||
if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then
|
||||
bun turbo verify:package --filter=@opencode/sdk
|
||||
exit 0
|
||||
fi
|
||||
bun turbo verify:package --affected --filter=@opencode/sdk
|
||||
env:
|
||||
TURBO_SCM_BASE: ${{ github.event_name == 'pull_request' && format('{0}^1', github.sha) || github.event.before }}
|
||||
TURBO_SCM_HEAD: ${{ github.sha }}
|
||||
|
||||
- name: Verify compiled service lifecycle
|
||||
if: always()
|
||||
timeout-minutes: 10
|
||||
working-directory: packages/cli
|
||||
env:
|
||||
NODE_OPTIONS: ${{ runner.os == 'Windows' && '--max-old-space-size=4096' || '' }}
|
||||
run: |
|
||||
bun run script/build.ts --single --skip-install
|
||||
bun run script/service-smoke.ts
|
||||
|
||||
- name: Setup Node build runtime
|
||||
if: always()
|
||||
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
|
||||
with:
|
||||
node-version: "26.4.0"
|
||||
|
||||
- name: Verify Node build
|
||||
if: always()
|
||||
timeout-minutes: 15
|
||||
working-directory: packages/cli
|
||||
env:
|
||||
NODE_OPTIONS: ${{ runner.os == 'Windows' && '--max-old-space-size=4096' || '' }}
|
||||
run: |
|
||||
bun run script/build-node.ts --single --skip-install --outdir=dist/node
|
||||
bun run script/service-smoke.ts --node
|
||||
|
||||
- name: Check generated client
|
||||
if: runner.os == 'Linux'
|
||||
working-directory: packages/client
|
||||
run: bun run check:generated
|
||||
|
||||
- name: Check generated documentation
|
||||
- name: Run HttpApi exerciser gates
|
||||
if: runner.os == 'Linux'
|
||||
working-directory: services/www
|
||||
run: bun run check:generated
|
||||
working-directory: packages/opencode
|
||||
run: bun run test:httpapi
|
||||
|
||||
e2e:
|
||||
name: e2e (${{ matrix.settings.name }})
|
||||
needs: affected
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
@@ -184,38 +91,32 @@ jobs:
|
||||
host: blacksmith-4vcpu-windows-2025
|
||||
runs-on: ${{ matrix.settings.host }}
|
||||
env:
|
||||
E2E_ENABLED: ${{ needs.affected.outputs.app == 'true' && github.ref_name != 'v2' && github.head_ref != 'v2' }}
|
||||
PLAYWRIGHT_BROWSERS_PATH: ${{ github.workspace }}/.playwright-browsers
|
||||
defaults:
|
||||
run:
|
||||
shell: bash
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
if: env.E2E_ENABLED == 'true'
|
||||
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
|
||||
with:
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Setup Node
|
||||
if: env.E2E_ENABLED == 'true'
|
||||
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
|
||||
with:
|
||||
# Playwright 1.59 hangs while extracting Chromium with Node 24.16.
|
||||
node-version: "24.15"
|
||||
|
||||
- name: Setup Bun
|
||||
if: env.E2E_ENABLED == 'true'
|
||||
uses: ./.github/actions/setup-bun
|
||||
|
||||
- name: Read Playwright version
|
||||
if: env.E2E_ENABLED == 'true'
|
||||
id: playwright-version
|
||||
run: |
|
||||
version=$(node -e 'console.log(require("./package.json").workspaces.catalog["@playwright/test"])')
|
||||
echo "version=$version" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Cache Playwright browsers
|
||||
if: env.E2E_ENABLED == 'true'
|
||||
id: playwright-cache
|
||||
uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0
|
||||
with:
|
||||
@@ -223,37 +124,23 @@ jobs:
|
||||
key: ${{ runner.os }}-${{ runner.arch }}-playwright-${{ steps.playwright-version.outputs.version }}-chromium
|
||||
|
||||
- name: Install Playwright system dependencies
|
||||
if: env.E2E_ENABLED == 'true' && runner.os == 'Linux'
|
||||
if: runner.os == 'Linux'
|
||||
working-directory: packages/app
|
||||
run: bunx playwright install-deps chromium
|
||||
|
||||
- name: Install Playwright browsers
|
||||
if: env.E2E_ENABLED == 'true' && steps.playwright-cache.outputs.cache-hit != 'true'
|
||||
if: steps.playwright-cache.outputs.cache-hit != 'true'
|
||||
working-directory: packages/app
|
||||
run: bunx playwright install chromium
|
||||
|
||||
- name: Run app e2e tests against production build
|
||||
if: env.E2E_ENABLED == 'true'
|
||||
run: bun --cwd packages/app test:e2e:built
|
||||
- name: Run app e2e tests
|
||||
run: bun --cwd packages/app test:e2e:local
|
||||
env:
|
||||
CI: true
|
||||
timeout-minutes: 30
|
||||
|
||||
- name: Verify service worker precaching and upgrades
|
||||
if: env.E2E_ENABLED == 'true'
|
||||
working-directory: packages/app
|
||||
run: bunx playwright test --config e2e/service-worker/playwright.config.ts
|
||||
timeout-minutes: 5
|
||||
|
||||
- name: Run session UI component tests
|
||||
if: ${{ !cancelled() && env.E2E_ENABLED == 'true' }}
|
||||
run: bun --cwd packages/session-ui test:components
|
||||
env:
|
||||
CI: true
|
||||
timeout-minutes: 15
|
||||
|
||||
- name: Upload Playwright artifacts
|
||||
if: always() && env.E2E_ENABLED == 'true'
|
||||
if: always()
|
||||
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
|
||||
with:
|
||||
name: playwright-${{ matrix.settings.name }}-${{ github.run_attempt }}
|
||||
@@ -262,5 +149,3 @@ jobs:
|
||||
path: |
|
||||
packages/app/e2e/test-results
|
||||
packages/app/e2e/playwright-report
|
||||
packages/session-ui/component-tests/test-results
|
||||
packages/session-ui/component-tests/playwright-report
|
||||
@@ -49,7 +49,7 @@ jobs:
|
||||
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:
|
||||
opencode run --agent triage --variant high "The following issue was just opened, triage it:
|
||||
|
||||
Title: $ISSUE_TITLE
|
||||
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
name: typecheck
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [dev]
|
||||
pull_request:
|
||||
branches: [dev]
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
typecheck:
|
||||
runs-on: blacksmith-4vcpu-ubuntu-2404
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
|
||||
|
||||
- name: Setup Bun
|
||||
uses: ./.github/actions/setup-bun
|
||||
|
||||
- name: Run typecheck
|
||||
run: bun typecheck
|
||||
@@ -0,0 +1,52 @@
|
||||
name: unlock
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
stage:
|
||||
description: SST stage to unlock
|
||||
required: true
|
||||
type: choice
|
||||
options:
|
||||
- dev
|
||||
- production
|
||||
|
||||
concurrency: deploy-${{ inputs.stage }}
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
id-token: write
|
||||
|
||||
jobs:
|
||||
unlock:
|
||||
runs-on: ubuntu-latest
|
||||
environment: ${{ inputs.stage }}
|
||||
steps:
|
||||
- uses: actions/checkout@f43a0e5ff2bd294095638e18286ca9a3d1956744 # v3.6.0
|
||||
|
||||
- uses: ./.github/actions/setup-bun
|
||||
|
||||
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
|
||||
with:
|
||||
node-version: "24"
|
||||
|
||||
- uses: aws-actions/configure-aws-credentials@7474bc4690e29a8392af63c5b98e7449536d5c3a # v4.3.1
|
||||
with:
|
||||
role-to-assume: ${{ vars.AWS_DEPLOY_ROLE_ARN }}
|
||||
role-session-name: opencode-${{ github.run_id }}
|
||||
aws-region: us-east-1
|
||||
|
||||
- run: bun sst unlock --stage=${{ inputs.stage }}
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ github.token }}
|
||||
CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
|
||||
PLANETSCALE_SERVICE_TOKEN_NAME: ${{ secrets.PLANETSCALE_SERVICE_TOKEN_NAME }}
|
||||
PLANETSCALE_SERVICE_TOKEN: ${{ secrets.PLANETSCALE_SERVICE_TOKEN }}
|
||||
STRIPE_SECRET_KEY: ${{ inputs.stage == 'production' && secrets.STRIPE_SECRET_KEY_PROD || secrets.STRIPE_SECRET_KEY_DEV }}
|
||||
HONEYCOMB_API_KEY: ${{ secrets.HONEYCOMB_API_KEY }}
|
||||
SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
|
||||
SENTRY_ORG: ${{ vars.SENTRY_ORG }}
|
||||
SENTRY_PROJECT: ${{ vars.WEB_SENTRY_PROJECT }}
|
||||
SENTRY_RELEASE: unlock@${{ github.sha }}
|
||||
VITE_SENTRY_DSN: ${{ vars.WEB_SENTRY_DSN }}
|
||||
VITE_SENTRY_RELEASE: unlock@${{ github.sha }}
|
||||
@@ -11,10 +11,8 @@ node_modules
|
||||
playground
|
||||
tmp
|
||||
dist
|
||||
dist-node
|
||||
ts-dist
|
||||
.turbo
|
||||
.typecheck-profiles
|
||||
**/.serena
|
||||
.serena/
|
||||
**/.omo
|
||||
@@ -26,13 +24,11 @@ Session.vim
|
||||
a.out
|
||||
target
|
||||
.scripts
|
||||
.cache
|
||||
.direnv/
|
||||
|
||||
# Local dev files
|
||||
opencode-dev
|
||||
UPCOMING_CHANGELOG.md
|
||||
RELEASE_REVIEW.md
|
||||
logs/
|
||||
*.bun-build
|
||||
tsconfig.tsbuildinfo
|
||||
@@ -17,4 +17,4 @@ if (process.versions.bun !== expectedBunVersion) {
|
||||
console.warn(`Warning: Bun version ${process.versions.bun} differs from expected ${expectedBunVersion}`);
|
||||
}
|
||||
'
|
||||
bun run check
|
||||
bun typecheck
|
||||
@@ -1,7 +1,7 @@
|
||||
---
|
||||
mode: primary
|
||||
hidden: true
|
||||
model: opencode/gpt-5.4-mini
|
||||
model: opencode/gpt-6-luna
|
||||
color: "#44BA81"
|
||||
tools:
|
||||
"*": false
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
description: "Bump AI sdk dependencies minor / patch versions only"
|
||||
---
|
||||
|
||||
Please read @package.json and @packages/core/package.json.
|
||||
Please read @package.json and @packages/opencode/package.json.
|
||||
|
||||
Your job is to look into AI SDK dependencies, figure out if they have versions that can be upgraded (minor or patch versions ONLY no major ignore major changes).
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
---
|
||||
model: opencode/gpt-5.4
|
||||
model: opencode/gpt-6-luna
|
||||
---
|
||||
|
||||
Create `UPCOMING_CHANGELOG.md` from the structured changelog input below.
|
||||
|
||||
@@ -6,7 +6,15 @@ subtask: true
|
||||
|
||||
commit and push
|
||||
|
||||
Use `type(scope): summary` with one of these types: `feat`, `fix`, `docs`, `chore`, `refactor`, or `test`. The scope is optional.
|
||||
make sure it includes a prefix like
|
||||
docs:
|
||||
tui:
|
||||
core:
|
||||
ci:
|
||||
ignore:
|
||||
wip:
|
||||
|
||||
For anything in the packages/web use the docs: prefix.
|
||||
|
||||
prefer to explain WHY something was done from an end user perspective instead of
|
||||
WHAT was done.
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
description: Remove AI code slop
|
||||
---
|
||||
|
||||
Check the diff against `origin/v2`, and remove all AI generated slop introduced in this branch.
|
||||
Check the diff against dev, and remove all AI generated slop introduced in this branch.
|
||||
|
||||
This includes:
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
---
|
||||
description: translate English to other languages
|
||||
model: opencode/gpt-5.6-sol
|
||||
model: opencode/gpt-6-sol
|
||||
---
|
||||
|
||||
run git diff and translate changed english doc and UI copy files to other international languages. Translate all languages in parallel to save time.
|
||||
|
||||
@@ -1,48 +0,0 @@
|
||||
# he Glossary
|
||||
|
||||
## Sources
|
||||
|
||||
- Hebrew Academy approved IT terminology: https://terms.hebrew-academy.org.il/Millonim/ShowMillon?KodMillon=192
|
||||
- Firefox Hebrew localization corpus: https://github.com/mozilla-l10n/firefox-l10n/tree/main/he
|
||||
- KDE Hebrew localization team and corpus: https://l10n.kde.org/team-infos.php?teamcode=he
|
||||
- Community-maintained VS Code Hebrew language pack: https://github.com/AMAARETS/vscode-language-pack-he
|
||||
- Microsoft Hebrew developer documentation for Git terminology: https://learn.microsoft.com/he-il/power-platform/alm/tutorials/github-actions-deploy
|
||||
- W3C guidance for bidirectional text: https://www.w3.org/International/articles/strings-and-bidi/
|
||||
|
||||
## Do Not Translate (Locale Additions)
|
||||
|
||||
- `OpenCode` (preserve casing in prose and UI copy)
|
||||
- `API`, `MCP`, `LSP`, `OAuth`, `Git`, model names, and provider names
|
||||
- Commands, flags, keyboard shortcuts, file paths, URLs, identifiers, hashes, and code literals
|
||||
- Keep `commit` and `diff` when they name the exact Git artifact or operation
|
||||
|
||||
## Preferred Terms
|
||||
|
||||
| English / Context | Preferred | Notes |
|
||||
| ----------------- | ------------- | ------------------------------------------------------------------------- |
|
||||
| session | `הפעלה` | Use `שיחה` only when the source specifically means a chat or conversation |
|
||||
| workspace | `סביבת עבודה` | |
|
||||
| terminal | `מסוף` | Prefer the established Hebrew term over transliteration |
|
||||
| command | `פקודה` | |
|
||||
| provider | `ספק` | Use `ספק מודלים` where the bare noun is ambiguous |
|
||||
| model | `מודל` | |
|
||||
| API key | `מפתח API` | Keep the acronym in Latin letters |
|
||||
| plugin | `תוסף` | |
|
||||
| repository | `מאגר` | Use `מאגר Git` where context is ambiguous |
|
||||
| branch | `ענף` | |
|
||||
| context | `הקשר` | Use `חלון הקשר` for context window |
|
||||
| tokens | `אסימונים` | |
|
||||
|
||||
## Guidance
|
||||
|
||||
- Prefer natural modern Israeli Hebrew over word-for-word translation or obscure coined terms.
|
||||
- Use short action verbs for controls and translate complete phrases in context.
|
||||
- Keep recognized developer acronyms and exact Git vocabulary in Latin script instead of phonetic transliteration.
|
||||
- Treat embedded code, paths, commands, shortcuts, hashes, model IDs, and other Latin technical artifacts as LTR content inside the RTL interface.
|
||||
- Keep recurring concepts consistent and do not collapse session, chat, run, and launch into one Hebrew term.
|
||||
|
||||
## Avoid
|
||||
|
||||
- Avoid transliterations such as `טרמינל`, `פלאגין`, and `קומנד` when `מסוף`, `תוסף`, and `פקודה` are clear.
|
||||
- Avoid translating `commit` as `התחייבות`.
|
||||
- Avoid inventing Hebrew expansions for `API`, `MCP`, or `LSP`.
|
||||
@@ -1,6 +1,6 @@
|
||||
---
|
||||
name: effect
|
||||
description: Work with Effect v4 TypeScript code in this repo
|
||||
description: Work with Effect v4 / effect-smol TypeScript code in this repo
|
||||
---
|
||||
|
||||
# Effect
|
||||
@@ -9,10 +9,10 @@ This codebase uses Effect for typed, composable TypeScript services, schemas, an
|
||||
|
||||
## Source Of Truth
|
||||
|
||||
Use the current Effect v4 source, not memory or older Effect v2/v3 examples.
|
||||
Use the current Effect v4 / effect-smol source, not memory or older Effect v2/v3 examples.
|
||||
|
||||
1. If `.opencode/references/effect` is missing, clone `https://github.com/Effect-TS/effect` there. Do this in the project, not in the skill folder.
|
||||
2. Search `.opencode/references/effect` for exact APIs, examples, tests, and naming patterns before answering or implementing Effect-specific code.
|
||||
1. If `.opencode/references/effect-smol` is missing, clone `https://github.com/Effect-TS/effect-smol` there. Do this in the project, not in the skill folder.
|
||||
2. Search `.opencode/references/effect-smol` for exact APIs, examples, tests, and naming patterns before answering or implementing Effect-specific code.
|
||||
3. Also inspect existing repo code for local house style before introducing new patterns.
|
||||
4. Prefer answers and implementations backed by specific source files or nearby repo examples.
|
||||
|
||||
@@ -27,12 +27,12 @@ Use the current Effect v4 source, not memory or older Effect v2/v3 examples.
|
||||
- Keep layer composition explicit. Avoid broad hidden provisioning that makes missing dependencies hard to see.
|
||||
- In tests, prefer the repo's existing Effect test helpers and live tests for filesystem, git, child process, locks, or timing behavior.
|
||||
- Do not introduce `any`, non-null assertions, unchecked casts, or older Effect APIs just to satisfy types.
|
||||
- Do not answer from memory. Verify against `.opencode/references/effect` or nearby code first.
|
||||
- Do not answer from memory. Verify against `.opencode/references/effect-smol` or nearby code first.
|
||||
|
||||
## Testing Patterns
|
||||
|
||||
- Use `testEffect(...)` from `packages/core/test/lib/effect.ts` for tests that exercise Effect services, layers, runtime context, scoped resources, or platform integrations.
|
||||
- Use `testEffect(...)` from `packages/opencode/test/lib/effect.ts` for tests that exercise Effect services, layers, runtime context, scoped resources, or platform integrations.
|
||||
- Use `it.live(...)` for filesystem, git repositories, HTTP servers, sockets, child processes, locks, real time, and other live platform behavior.
|
||||
- Run tests from package directories such as `packages/core`; never run package tests from the repo root.
|
||||
- Run tests from package directories such as `packages/opencode`; never run package tests from the repo root.
|
||||
- Prefer explicit test layers over ad hoc managed runtimes. Keep dependency provisioning visible in the test file.
|
||||
- Use scoped fixtures and finalizers for resources that must be cleaned up, including temporary directories, flags, databases, fibers, servers, and global state.
|
||||
@@ -1,231 +0,0 @@
|
||||
---
|
||||
name: opencode-dev
|
||||
description: Use when interactively running, debugging, or verifying opencode's own V2 CLI/TUI or server during development in this repo — starting the dev TUI, driving it with termctrl, comparing V2 against the legacy TUI, hitting the V2 server/API directly, reading log files, or attaching Bun's inspector.
|
||||
---
|
||||
|
||||
# Debugging opencode itself
|
||||
|
||||
Workflow for interactively exercising the V2 CLI/TUI and server while developing in this repo. All commands below run from `packages/cli` unless noted otherwise.
|
||||
|
||||
## Server/client model
|
||||
|
||||
opencode V2 is a client/server system, not a single monolithic process:
|
||||
|
||||
- **Server process** runs the Effect HTTP API (`packages/server`) and owns all domain state: sessions, database, plugins, permissions, Location services. It's started by the `serve` command (`packages/cli/src/commands/handlers/serve.ts`).
|
||||
- **TUI process** is a separate process that runs no application logic itself — it's an HTTP/SSE client of the server via the generated SDK (`createOpencodeClient` / `sdk.client.v2`).
|
||||
- **Discovery**: CLI processes find the shared server through a JSON registration file at `~/.local/state/opencode/service.json` (or `service-local.json` for the local/dev channel) containing `{id, version, url, pid}`. A separate password file under `~/.config/opencode/service.json` provides HTTP Basic auth. Before reusing a registration, the client calls `GET /health` to confirm the server is alive, authenticated, and version-compatible.
|
||||
- **Sharing**: because of this registration/health-check dance, many concurrent `opencode`/TUI invocations converge on one shared background daemon rather than each spawning their own. If no compatible healthy daemon is found, a new one is spawned detached (`serve --service`) and registers itself.
|
||||
- **`bun dev service start|status|stop|restart`** manages this shared background daemon's lifecycle directly — useful when you need to force a fresh server, confirm one is running, or kill a stuck one.
|
||||
- **Standalone mode** (`--standalone`) opts a single invocation out of the shared daemon: it spawns a private one-off `serve --stdio --port 0` child tied to that invocation's lifetime, with its own random password. Use this to isolate a debugging session from your other running opencode sessions.
|
||||
- Every log line is tagged `role=server` or `role=cli` and a per-process `run=<id>`, so you can distinguish server-side and client-side activity in one shared log file (see "Logs" below) even when both roles are interleaved from concurrent processes.
|
||||
|
||||
## Starting the dev TUI
|
||||
|
||||
- This package is the V2 CLI adapter. Run its `dev` script when testing the TUI; do not use the repository-root `bun dev`, which launches the legacy `packages/opencode` CLI.
|
||||
- Run commands from `packages/cli`. Use `bun dev` for most debugging so the TUI starts with a private V2 server.
|
||||
|
||||
## Interactive debugging with termctrl
|
||||
|
||||
- Use `termctrl` for interactive checks instead of starting the TUI as a blocking foreground process. It provides a real PTY, handles OpenTUI's host handshake, and can save reviewable screenshots.
|
||||
- Use a dedicated session name and do not reuse or kill an unrelated session.
|
||||
|
||||
```bash
|
||||
termctrl start opencode-v2-dev --host opentui --cols 112 --rows 34 -- bun dev
|
||||
termctrl wait opencode-v2-dev "Ask anything" --timeout 20000
|
||||
termctrl show opencode-v2-dev
|
||||
```
|
||||
|
||||
- Wait for visible text before interacting instead of relying on fixed sleeps. Use the text expected from the screen under test, such as `Ask anything` or `Connect a provider`.
|
||||
- Drive the running TUI with `termctrl send`. Prefix typed input with `text:` and send control keys separately so the interaction matches real terminal input.
|
||||
|
||||
```bash
|
||||
termctrl send opencode-v2-dev 'text:example prompt' enter
|
||||
termctrl send opencode-v2-dev ctrl-c
|
||||
```
|
||||
|
||||
- Use `termctrl show` after each meaningful interaction and inspect the full visible screen for rendering errors, stale state, error toasts, and unexpected exits.
|
||||
- Save PNG evidence for every user-visible bug and fix. Do not save text captures; inspect the rendered PNG. Write temporary captures outside the repository unless the artifact is intended to be committed.
|
||||
|
||||
```bash
|
||||
termctrl save opencode-v2-dev --format png --out /tmp/opencode/v2-tui.png
|
||||
```
|
||||
|
||||
- For resize-sensitive changes, resize the viewport, wait for the expected content, and capture the screen again:
|
||||
|
||||
```bash
|
||||
termctrl resize opencode-v2-dev --cols 100 --rows 30
|
||||
termctrl show opencode-v2-dev
|
||||
```
|
||||
|
||||
- Source changes may require restarting the process. Use `termctrl restart opencode-v2-dev` rather than assuming the running TUI reloaded the change.
|
||||
- To exercise background-service behavior, use `bun dev service start`, `bun dev service status`, and `bun dev service stop`.
|
||||
- Always clean up the Terminal Control session when the check is complete:
|
||||
|
||||
```bash
|
||||
termctrl stop opencode-v2-dev
|
||||
```
|
||||
|
||||
## Comparing V2 against the legacy TUI
|
||||
|
||||
Run both versions in separate Terminal Control sessions and save PNG-only captures at equivalent states:
|
||||
|
||||
```bash
|
||||
# From packages/cli: local V2 TUI
|
||||
termctrl start opencode-v2-dev --host opentui --cols 112 --rows 34 -- bun dev
|
||||
|
||||
# Released legacy TUI behavior reference
|
||||
termctrl start opencode-legacy --host opentui --cols 112 --rows 34 -- bunx opencode-ai@latest
|
||||
|
||||
termctrl save opencode-v2-dev --format png --out /tmp/opencode/v2.png
|
||||
termctrl save opencode-legacy --format png --out /tmp/opencode/legacy.png
|
||||
```
|
||||
|
||||
- Use the same viewport and send equivalent inputs to both sessions before comparing screenshots. The released CLI is a behavioral reference, not a source of V2 API design; keep the local implementation on V2 endpoints.
|
||||
- Stop both sessions after comparison: `termctrl stop opencode-v2-dev` and `termctrl stop opencode-legacy`.
|
||||
|
||||
## Server/API debugging
|
||||
|
||||
- Use `bun dev api --help` from `packages/cli` to inspect the API debugging command. It sends one request to the V2 server using the same daemon discovery/auth path as the CLI.
|
||||
- Use `bun dev api` to introspect the server-side data backing the TUI. This is useful when debugging UI bugs: compare what the screen renders with the raw session, message, event, agent, or health data returned by the API to determine whether the bug is in the server state, the client data layer, or the TUI rendering.
|
||||
- `bun dev api` accepts either an OpenAPI operation ID or a raw HTTP method plus path:
|
||||
|
||||
```bash
|
||||
bun dev api get /health
|
||||
bun dev api get /openapi.json
|
||||
bun dev api <operationId> --param key=value
|
||||
```
|
||||
|
||||
- Pass JSON request bodies with `--data`/`-d`; the command sets `content-type: application/json` automatically unless you provide a header. Add extra headers with `--header`/`-H name:value`.
|
||||
- If no compatible background server is registered, `bun dev api` starts one through the daemon service. Use `bun dev service status`, `bun dev service restart`, and `bun dev service stop` when you need explicit lifecycle control.
|
||||
- Prefer raw method/path calls for quick server debugging and operation IDs when exercising documented OpenAPI routes with path or query parameters.
|
||||
|
||||
## Auditing installed `opencode2` sessions
|
||||
|
||||
Installed next-channel sessions normally use `~/.local/share/opencode/opencode-next.db` and `~/.local/share/opencode/log/opencode.log`; `OPENCODE_DB` can override the database. Before calling `opencode2 api`, inspect `~/.local/state/opencode/service.json` because the command may start a daemon when none is healthy.
|
||||
|
||||
For a supplied `ses_...` ID, compare three sources:
|
||||
|
||||
- `opencode2 api get /api/session/active` and the Session/message endpoints for live server state.
|
||||
- The database's ordered `event` rows for durable history.
|
||||
- `packages/tui/src/context/data.tsx` and the relevant route for client projection and rendering.
|
||||
|
||||
Locate an uncertain database without modifying it:
|
||||
|
||||
```bash
|
||||
SESSION=ses_...
|
||||
for db in ~/.local/share/opencode/*.db; do
|
||||
sqlite3 "file:$db?mode=ro" "select 1 from session where id='$SESSION' limit 1" 2>/dev/null | grep -q 1 && printf '%s\n' "$db"
|
||||
done
|
||||
```
|
||||
|
||||
## Logs
|
||||
|
||||
- Log files live under `~/.local/share/opencode/log/`. In a local/dev checkout the active file is `opencode-local.log`; `opencode.log` is used for non-local (released) channel installs. Both are append-only, shared across every CLI and server process on the machine.
|
||||
- Each line is structured `key=value` text: `timestamp`, `level`, `run=<id>` (per-process run ID), `message`, and a `role=cli` or `role=server` tag. Use `run=` to isolate one process's activity and `role=` to separate client-side from server-side log lines, since a shared daemon interleaves many processes' output in one file.
|
||||
- Tail the live file while reproducing an issue instead of guessing from stale output:
|
||||
|
||||
```bash
|
||||
tail -f ~/.local/share/opencode/log/opencode-local.log
|
||||
```
|
||||
|
||||
- Filter to one run or role when the file is noisy:
|
||||
|
||||
```bash
|
||||
grep 'run=8fc3b1d5' ~/.local/share/opencode/log/opencode-local.log
|
||||
grep 'role=server' ~/.local/share/opencode/log/opencode-local.log
|
||||
```
|
||||
|
||||
- `OPENCODE_LOG_LEVEL` controls verbosity (default `INFO`); set it before starting `bun dev` or `serve` to get `DEBUG` output for a specific repro.
|
||||
- `OPENCODE_PRINT_LOGS=1` additionally tees log output to stderr of the process that emitted it, which is useful when a process fails before you'd think to check the shared log file.
|
||||
- `termctrl logs <session>` surfaces stdout/stderr for a Terminal Control session specifically (e.g. inspector output or startup failures before the TUI renderer starts) — use the log file above for anything emitted by a separate server/daemon process instead.
|
||||
|
||||
## Heap snapshots
|
||||
|
||||
The CLI installs a `SIGUSR1` listener on non-Windows processes in `packages/cli/src/heap.ts`. Use it to capture the installed `opencode2` server without restarting it or attaching an inspector.
|
||||
|
||||
1. Find the processes and inspect their roles and memory:
|
||||
|
||||
```bash
|
||||
pgrep -a -f 'opencode2\.exe|opencode2'
|
||||
ps -o pid,ppid,rss,vsz,lstart,etime,cmd -p <pid>,<pid>
|
||||
```
|
||||
|
||||
2. Signal the process whose heap needs investigation. For shared-service memory, target the `opencode2.exe serve --service` child, not the short wrapper/TUI process:
|
||||
|
||||
```bash
|
||||
kill -USR1 <server-pid>
|
||||
```
|
||||
|
||||
3. Wait for `heap snapshot written` in the channel's log before opening the file. Snapshots are written to the same log directory as `heap-<pid>-<timestamp>.heapsnapshot`; writing a large heap can take several seconds and the file is incomplete until the completion message appears:
|
||||
|
||||
```bash
|
||||
grep 'heap snapshot' ~/.local/share/opencode/log/opencode.log | tail
|
||||
find ~/.local/share/opencode/log -maxdepth 1 -name 'heap-<server-pid>-*.heapsnapshot' -printf '%T@ %s %p\n' | sort -nr | head
|
||||
```
|
||||
|
||||
Use `opencode-local.log` instead for a local/dev channel process. The log's `path=` field is authoritative.
|
||||
|
||||
4. Analyze the snapshot with Chrome DevTools, a V8 heap snapshot parser, or a temporary tool installed outside the repository. Start with the largest retained objects, dominators, object counts grouped by constructor/name, and retainer paths back to GC roots. Relate suspicious names and paths back to the source tree rather than treating large shallow allocations as leaks.
|
||||
|
||||
For command-line analysis, install tooling under `/tmp/opencode`, not in the repository. For example, MemLab can rank dominators and trace a reported heap object ID back to a GC root:
|
||||
|
||||
```bash
|
||||
npm install --prefix /tmp/opencode/heap-analysis @memlab/cli
|
||||
/tmp/opencode/heap-analysis/node_modules/.bin/memlab analyze object-size --snapshot <snapshot>
|
||||
/tmp/opencode/heap-analysis/node_modules/.bin/memlab analyze shape --snapshot <snapshot>
|
||||
/tmp/opencode/heap-analysis/node_modules/.bin/memlab trace --snapshot <snapshot> --node-id=<id>
|
||||
```
|
||||
|
||||
A single snapshot explains what retains memory at one point in time, but does not by itself prove a leak. For leak confirmation, capture a baseline, perform a controlled repeated workload, allow idle cleanup/GC when possible, capture another snapshot, and compare growth and retainer paths. Also compare snapshot heap size with process RSS: a large difference can indicate native allocations, database mappings, allocator fragmentation, or other memory outside the JavaScript heap.
|
||||
|
||||
```bash
|
||||
cat /proc/<pid>/smaps_rollup
|
||||
pmap -x <pid> | sort -k3 -nr | head -25
|
||||
```
|
||||
|
||||
Heap serialization itself can temporarily increase RSS and allocator high-water marks, so record `ps`/`smaps_rollup` both before and after capture. Large anonymous mappings with a comparatively small live heap require native-allocation or allocator investigation; they cannot be explained from JavaScript retainer paths alone.
|
||||
|
||||
## CPU profiles
|
||||
|
||||
The CLI installs a `SIGPROF` listener on non-Windows processes in `packages/cli/src/cpu-profile.ts`. One signal starts a ten-second CPU profile and stops it automatically; additional signals are ignored while a profile is active. There is no CPU profile CLI flag or environment variable.
|
||||
|
||||
1. Get the PID from the health endpoint. For shared-service performance, target the server PID returned here rather than the short wrapper or TUI process:
|
||||
|
||||
```bash
|
||||
opencode2 api get /api/health
|
||||
```
|
||||
|
||||
Use `bun dev api get /api/health` instead when targeting the local/dev channel.
|
||||
|
||||
2. Start the capture:
|
||||
|
||||
```bash
|
||||
kill -PROF <server-pid>
|
||||
```
|
||||
|
||||
3. Wait for `CPU profile written` in the channel's log before opening the file. Profiles are written to the same log directory as `cpu-<pid>-<timestamp>.cpuprofile`; the log's `path=` field is authoritative:
|
||||
|
||||
```bash
|
||||
grep 'CPU profile' ~/.local/share/opencode/log/opencode.log | tail
|
||||
find ~/.local/share/opencode/log -maxdepth 1 -name 'cpu-<server-pid>-*.cpuprofile' -printf '%T@ %s %p\n' | sort -nr | head
|
||||
```
|
||||
|
||||
Use `opencode-local.log` for a local/dev process. Load the completed `.cpuprofile` in Chrome DevTools or another V8 CPU profile viewer and inspect the hottest functions, call stacks, and self time during the controlled workload.
|
||||
|
||||
## Debugger
|
||||
|
||||
- To debug the V2 CLI or TUI with Bun's inspector, launch the CLI entrypoint through Terminal Control with an inspector URL, then attach a debugger to that URL:
|
||||
|
||||
```bash
|
||||
termctrl start opencode-v2-debug --host opentui --cols 112 --rows 34 -- \
|
||||
bun run --inspect=ws://localhost:6499/ src/index.ts
|
||||
```
|
||||
|
||||
- Use `--inspect-wait` or `--inspect-brk` when execution must pause until a debugger attaches.
|
||||
- Use `termctrl logs opencode-v2-debug` for inspector output or startup failures emitted before the TUI renderer starts. Use `termctrl show` for the visible full-screen TUI.
|
||||
|
||||
## Verification
|
||||
|
||||
- Run `bun typecheck` from `packages/cli` after CLI adapter changes.
|
||||
- Run `bun typecheck` and `bun test` from `packages/tui` after shared TUI changes. Do not run tests from the repository root.
|
||||
- Treat automated checks and Terminal Control smoke tests as complementary. For user-visible changes, verify initial render, the changed interaction, Ctrl-C exit behavior, and save a screenshot of the corrected state.
|
||||
@@ -1,31 +0,0 @@
|
||||
---
|
||||
name: sample-skill
|
||||
description: Use when the user says sample skill, skill demo, or asks how an opencode SKILL.md should be structured; demonstrates a tiny project-local skill with practical assistant workflow guidance.
|
||||
---
|
||||
|
||||
# Sample Skill
|
||||
|
||||
This is a minimal project-local opencode skill. It exists as a reference for how a skill is structured and as a tiny reusable workflow the assistant can load when the user asks for a skill example.
|
||||
|
||||
## When To Use
|
||||
|
||||
- Use when the user asks for a sample skill or skill template.
|
||||
- Use when demonstrating the required `SKILL.md` frontmatter and body format.
|
||||
- Do not use for unrelated coding tasks just because a skill exists.
|
||||
|
||||
## Workflow
|
||||
|
||||
- Confirm the specific outcome the user wants if the request is ambiguous.
|
||||
- Inspect the relevant files before changing anything.
|
||||
- Make the smallest correct change.
|
||||
- Verify the result with a focused read, typecheck, test, or other lightweight check when available.
|
||||
- Summarize the changed files and any required restart or reload step.
|
||||
|
||||
## Example Response Style
|
||||
|
||||
When this skill is relevant, keep responses direct and actionable:
|
||||
|
||||
```text
|
||||
I created a project-local skill at .opencode/skills/sample-skill/SKILL.md.
|
||||
Restart opencode for the new skill to be discovered by future sessions.
|
||||
```
|
||||
@@ -1,5 +1,5 @@
|
||||
/// <reference path="../env.d.ts" />
|
||||
import { tool } from "@opencode/plugin"
|
||||
import { tool } from "@opencode-ai/plugin"
|
||||
async function githubFetch(endpoint: string, options: RequestInit = {}) {
|
||||
const response = await fetch(`https://api.github.com${endpoint}`, {
|
||||
...options,
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
/// <reference path="../env.d.ts" />
|
||||
import { tool } from "@opencode/plugin"
|
||||
import { tool } from "@opencode-ai/plugin"
|
||||
|
||||
const TEAM = {
|
||||
tui: ["kommander", "simonklee"],
|
||||
desktop_web: ["Hona", "Brendonovich"],
|
||||
core: ["jlongster", "rekram1-node", "neriousy", "nexxeln", "kitlangton"],
|
||||
inference: ["fwang", "MrMushrooooom", "starptech"],
|
||||
inference: ["fwang", "vaprdev", "vimtor"],
|
||||
windows: ["Hona"],
|
||||
} as const
|
||||
|
||||
|
||||
@@ -1,3 +1,19 @@
|
||||
{
|
||||
"$schema": "https://opencode.ai/tui.json"
|
||||
"$schema": "https://opencode.ai/tui.json",
|
||||
"plugin": [
|
||||
[
|
||||
"./plugins/tui-smoke.tsx",
|
||||
{
|
||||
"enabled": false,
|
||||
"label": "workspace",
|
||||
"keybinds": {
|
||||
"smoke_modal": "ctrl+alt+m",
|
||||
"smoke_screen": "ctrl+alt+o",
|
||||
"smoke_screen_home": "escape,ctrl+shift+h",
|
||||
"smoke_screen_modal": "ctrl+alt+m",
|
||||
"smoke_dialog_close": "escape,q"
|
||||
}
|
||||
}
|
||||
]
|
||||
]
|
||||
}
|
||||
@@ -1,115 +1,51 @@
|
||||
{
|
||||
"$schema": "https://raw.githubusercontent.com/nicolo-ribaudo/oxc-project.github.io/refs/heads/json-schema/src/public/.oxlintrc.schema.json",
|
||||
"jsPlugins": [
|
||||
{ "name": "anti-slop", "specifier": "./script/oxlint/anti-slop/index.ts" },
|
||||
{ "name": "anti-slop-effect", "specifier": "./script/oxlint/anti-slop/effect/index.ts" }
|
||||
],
|
||||
"options": {
|
||||
"typeAware": true
|
||||
},
|
||||
"categories": {
|
||||
"correctness": "off",
|
||||
"suspicious": "off",
|
||||
"pedantic": "off",
|
||||
"perf": "off",
|
||||
"style": "off",
|
||||
"restriction": "off",
|
||||
"nursery": "off"
|
||||
"suspicious": "warn"
|
||||
},
|
||||
"rules": {
|
||||
"no-restricted-globals": [
|
||||
"error",
|
||||
{
|
||||
"name": "Reflect",
|
||||
"message": "Use typed property access or direct invocation. Suppress this rule only for genuine reflection."
|
||||
}
|
||||
]
|
||||
"typescript/no-base-to-string": "warn",
|
||||
// Effect uses `function*` with Effect.gen/Effect.fnUntraced that don't always yield
|
||||
"require-yield": "off",
|
||||
// SolidJS uses `let ref: T | undefined` for JSX ref bindings assigned at runtime
|
||||
"no-unassigned-vars": "off",
|
||||
// SolidJS tracks reactive deps by reading properties inside createEffect
|
||||
"no-unused-expressions": "off",
|
||||
// Intentional control char matching (ANSI escapes, null byte sanitization)
|
||||
"no-control-regex": "off",
|
||||
// SST and plugin tools require triple-slash references
|
||||
"triple-slash-reference": "off",
|
||||
|
||||
// Suspicious category: suppress noisy rules
|
||||
// Effect's nested function* closures inherently shadow outer scope
|
||||
"no-shadow": "off",
|
||||
// Namespace-heavy codebase makes this too noisy
|
||||
"unicorn/consistent-function-scoping": "off",
|
||||
// Opinionated — .sort()/.reverse() mutation is fine in this codebase
|
||||
"unicorn/no-array-sort": "off",
|
||||
"unicorn/no-array-reverse": "off",
|
||||
// Not relevant — this isn't a DOM event handler codebase
|
||||
"unicorn/prefer-add-event-listener": "off",
|
||||
// Bundler handles module resolution
|
||||
"unicorn/require-module-specifiers": "off",
|
||||
// postMessage target origin not relevant for this codebase
|
||||
"unicorn/require-post-message-target-origin": "off",
|
||||
// Side-effectful constructors are intentional in some places
|
||||
"no-new": "off",
|
||||
|
||||
// Type-aware: catch unhandled promises
|
||||
"typescript/no-floating-promises": "warn",
|
||||
// Warn when spreading non-plain objects (Headers, class instances, etc.)
|
||||
"typescript/no-misused-spread": "warn"
|
||||
},
|
||||
"overrides": [
|
||||
{
|
||||
"files": [
|
||||
"packages/app/**",
|
||||
"packages/desktop/**",
|
||||
"packages/gui-extensions/**",
|
||||
"packages/ui/**",
|
||||
"packages/session-ui/**"
|
||||
],
|
||||
"rules": {
|
||||
"oxc/no-accumulating-spread": "warn",
|
||||
"anti-slop/no-array-filter-map": "warn",
|
||||
"anti-slop/no-reduce-accumulator-copy": "warn",
|
||||
"anti-slop/no-chained-type-assertions": "warn",
|
||||
"anti-slop/no-conditional-empty-object-spread": "warn",
|
||||
"anti-slop/no-known-value-widening": "warn",
|
||||
"anti-slop/no-module-mocking": "warn",
|
||||
"anti-slop/no-object-parameters": "warn",
|
||||
"anti-slop/no-reflect-apply": "warn",
|
||||
"anti-slop/no-reflect-get": "warn",
|
||||
"anti-slop/no-runtime-typeof": "warn",
|
||||
"anti-slop/no-shape-in-symbol-names": "warn",
|
||||
"anti-slop/no-unknown-parameters": "warn",
|
||||
"anti-slop/no-unknown-returns": "warn",
|
||||
"anti-slop/no-unknown-type-aliases": "warn",
|
||||
"anti-slop/no-unsafe-dictionary-type": "warn",
|
||||
"anti-slop/no-widen-then-assert": "warn",
|
||||
"anti-slop/require-readable-spacing": "warn",
|
||||
"anti-slop/require-safety-comment-for-type-assertion": "warn"
|
||||
}
|
||||
},
|
||||
{
|
||||
"files": ["packages/app/**", "packages/desktop/**", "packages/gui-extensions/**", "packages/session-ui/**"],
|
||||
"rules": {
|
||||
"anti-slop-effect/no-manual-effect-error-tag": "warn",
|
||||
"anti-slop-effect/no-manual-tag-comparison": "warn",
|
||||
"anti-slop-effect/no-manual-tagged-construction": "warn",
|
||||
"anti-slop-effect/no-service-constructor-imports": "warn",
|
||||
"anti-slop-effect/prefer-effect-match": "warn"
|
||||
}
|
||||
},
|
||||
{
|
||||
"files": ["packages/gui-extensions/src/*/**"],
|
||||
"rules": {
|
||||
"no-restricted-imports": [
|
||||
"error",
|
||||
{
|
||||
"patterns": [
|
||||
{
|
||||
"regex": "^@opencode/(app|desktop)(/|$)",
|
||||
"message": "GUI extensions never import the app or desktop packages. Use the SDK."
|
||||
},
|
||||
{
|
||||
"regex": "^@/",
|
||||
"message": "GUI extensions never import app internals. Use the SDK."
|
||||
},
|
||||
{
|
||||
"group": ["../*/*", "!../*/contract", "!../sdk/*"],
|
||||
"message": "Import another extension only through its contract.ts."
|
||||
},
|
||||
{
|
||||
"regex": "\\.css$",
|
||||
"message": "Import CSS with ?inline and contribute it with ctx.add(Style, css)."
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
],
|
||||
"ignorePatterns": [
|
||||
"**/node_modules",
|
||||
"**/dist",
|
||||
"**/.build",
|
||||
"**/.sst",
|
||||
"**/*.d.ts",
|
||||
"**/sdk.gen.ts",
|
||||
".agent/**",
|
||||
".agents/**",
|
||||
".claude/**",
|
||||
".codex/**",
|
||||
".continue/**",
|
||||
".cursor/**",
|
||||
".gemini/**",
|
||||
".opencode/**",
|
||||
".pi/**",
|
||||
".roo/**",
|
||||
".windsurf/**",
|
||||
"script/oxlint/anti-slop/**"
|
||||
]
|
||||
"options": {
|
||||
"typeAware": true
|
||||
},
|
||||
"options": {
|
||||
"typeAware": true
|
||||
},
|
||||
"ignorePatterns": ["**/node_modules", "**/dist", "**/.build", "**/.sst", "**/*.d.ts", "**/sdk.gen.ts"]
|
||||
}
|
||||
@@ -1,31 +1,8 @@
|
||||
- After changing the public Protocol or Server `HttpApi`, run `bun run generate` from `packages/client`. Do not edit generated client files directly.
|
||||
- Keep runtime dependencies directed from Schema to Core and Protocol, then from Core and Protocol to Server. Client runtime code may depend on Schema and Protocol but never Core or Server; `sdk` composes Client, Core, and Server.
|
||||
- Current implementation changes belong in `packages/core`, `packages/cli`, `packages/server`, `packages/protocol`, `packages/schema`, and related generated client surfaces when required.
|
||||
- This repository does not use Changesets. Do not add `.changeset` files; follow the existing release workflow instead.
|
||||
- The default branch in this repo is `v2`.
|
||||
- Default new branches and worktrees to `v2`, or `origin/v2` when the local `v2` ref is unavailable, and default pull requests to target `v2`. Use another base or target branch when the requester explicitly instructs it.
|
||||
- Local `main` ref may not exist; use `v2` or `origin/v2` for diffs.
|
||||
|
||||
## Live V2 TUI Testing
|
||||
|
||||
- Run `bun run dev:live` from a development worktree to test its TUI against the currently elected `opencode` background server and live sessions.
|
||||
- Pass a directory after the script when needed, for example `bun run dev:live /path/to/project`.
|
||||
- The script discovers the server with `opencode service status`, injects its private local credential from `opencode service get password`, and uses the `dev` TUI storage channel so tabs and other client-local state match the installed client.
|
||||
- Prefer `dev:live` over plain `bun run dev` for this workflow. An implicit managed-service connection may replace the live server when the worktree client version differs; explicit `--server` warns and continues without replacing it.
|
||||
|
||||
## V2 TUI Stories
|
||||
|
||||
- When a user asks for a TUI story, add a fixture-driven story under `packages/tui/src/feature-plugins/system/storybook` and register it in `index.tsx`.
|
||||
- Render the real production component rather than a visual copy. Keep submissions and other side effects local to the story so it is safe to explore repeatedly.
|
||||
- Expose the meaningful state dimensions through story keybindings and list them in `StoryFooter`; include a reset command when combinations can leave the fixture in a confusing state.
|
||||
- Run a specific story with `OPENCODE_STORY=<story-id> bun run dev:live` from the development worktree, and exercise narrow and wide terminal sizes when layout is relevant.
|
||||
|
||||
## TUI Theme Tokens
|
||||
|
||||
- Choose theme tokens by semantic role, not by their current color. Do not use raw `theme.hue` values or borrow an unrelated semantic token to achieve a preferred appearance.
|
||||
- Use `text.feedback` and `background.feedback` only for outcome or status feedback such as errors, warnings, success messages, and informational messages. Use `formfield` states for form-control text, ordinals, and selection markers, and `action` states for actions.
|
||||
- If the theme does not expose a token for the required semantic role, extend the theme schema, defaults, resolution, and types with that role before using it in a component. Do not repurpose the nearest-looking existing token.
|
||||
- When changing the public theme token surface, verify the built-in light and dark defaults and the custom-theme fallback path in addition to the affected TUI component.
|
||||
- To regenerate the legacy JavaScript SDK, run `./packages/sdk/js/script/build.ts`.
|
||||
- After changing the public Protocol or Server `HttpApi`, run `bun run generate` from `packages/client`. Do not edit `src/generated` or `src/generated-effect` directly.
|
||||
- Keep runtime dependencies directed from Schema to Core and Protocol, then from Core and Protocol to Server. Client runtime code may depend on Schema and Protocol but never Core or Server; `sdk-next` composes Client, Core, and Server.
|
||||
- The default branch in this repo is `dev`.
|
||||
- Local `main` ref may not exist; use `dev` or `origin/dev` for diffs.
|
||||
|
||||
## Branch Names
|
||||
|
||||
@@ -46,9 +23,7 @@ Examples: `fix(tui): simplify thinking toggle styling`, `docs: update contributi
|
||||
### General Principles
|
||||
|
||||
- Keep things in one function unless composable or reusable
|
||||
- Validate unknown values once at the boundary that owns them. Pass typed values inward instead of repeating `typeof value === "object"` and property-existence checks. Do not defensively revalidate values already guaranteed by a schema, constructor, or internal type.
|
||||
- Do not extract single-use helpers preemptively. Inline the logic at the call site unless the helper is reused, hides a genuinely complex boundary, or has a clear independent name that improves the caller.
|
||||
- Before adding complexity for a speculative or vanishingly unlikely race or security edge case, explain the concrete failure mode, likelihood, and complexity cost to the user and get their buy-in. Do not silently expand scope for theoretical robustness.
|
||||
- Avoid `try`/`catch` where possible
|
||||
- Avoid using the `any` type
|
||||
- Use Bun APIs when possible, like `Bun.file()`
|
||||
@@ -84,9 +59,8 @@ const { a, b } = obj
|
||||
### Imports
|
||||
|
||||
- Never alias imports. Do not use `import { foo as bar } from "..."` or renamed imports like `resolve as pathResolve`.
|
||||
- Never use type-position `import("...")` references such as `Schema.declare<import("@opencode/plugin/effect/plugin").Plugin["effect"]>`. Only when two imports genuinely collide on a name and no other option exists, an aliased type import (`import type { Plugin as PluginDefinition } from "..."`) is permitted as a last resort — still strongly preferred not to.
|
||||
- Never use star imports. Do not use `import * as Foo from "..."` or `import type * as Foo from "..."`.
|
||||
- If a namespace-style value is needed, import the module's own exported namespace by name, for example `import { Project } from "@opencode/core/project"`, then reference `Project.ID`.
|
||||
- If a namespace-style value is needed, import the module's own exported namespace by name, for example `import { Project } from "@opencode-ai/core/project"`, then reference `Project.ID`.
|
||||
- Prefer dynamic imports for heavy modules that are only needed in selected code paths, especially in startup-sensitive entrypoints. Destructure dynamic import bindings near the top of the narrowest scope that needs them so they read like normal imports. Avoid inline chains such as `await import("./module").then((mod) => mod.value())` or `(await import("./module")).value()`. Keep branch-specific imports inside the branch that needs them to preserve lazy loading.
|
||||
|
||||
### Variables
|
||||
@@ -168,25 +142,20 @@ const table = sqliteTable("session", {
|
||||
|
||||
- Avoid mocks as much as possible, you shouldn't be using globalThis.\* at all unless it's the only option.
|
||||
- Test actual implementation, do not duplicate logic into tests
|
||||
- Tests cannot run from repo root (guard: `do-not-run-tests-from-root`); run from package directories such as `packages/core`.
|
||||
- Tests cannot run from repo root (guard: `do-not-run-tests-from-root`); run from package dirs like `packages/opencode`.
|
||||
|
||||
## Checks
|
||||
## Type Checking
|
||||
|
||||
- Run `bun run check` from the repository root as the canonical full lint and type-check verification.
|
||||
- During focused iteration, run `bun typecheck` from the affected package directory (for example, `packages/core`). Never run `tsc` directly.
|
||||
- Always run `bun typecheck` from package directories (e.g., `packages/opencode`), never `tsc` directly.
|
||||
|
||||
## V2 Session Core
|
||||
|
||||
- Keep durable events minimal: record irreducible new facts and do not repeat state derivable by folding the ordered aggregate history. Enrich projections and read models with previous or derived state when consumers need self-contained views.
|
||||
- Keep durable prompt admission separate from model execution. `Session.prompt(...)` publishes `session.inbox.enqueued`, whose projection inserts one durable `session_inbox` row, before scheduling advisory `SessionExecution.wake(sessionID)` unless `resume: false` requests admit-only behavior. Delivery publishes `session.inbox.delivered`; its projection consumes the inbox row and inserts the visible message in the same transaction. `session_inbox` stores only unconsumed work.
|
||||
- Reusing a Session ID adopts the existing Session. Reusing a user or synthetic inbox item ID is idempotent when Session and type match: the first admission wins and the retried payload, metadata, and delivery mode are ignored, whether the item is still pending or already delivered (reconciled from the projected message without retained enqueue history). Cross-Session or cross-type reuse fails. Control items keep their operation-specific conflict behavior.
|
||||
- Keep `SessionExecution` process-global and Session-ID based. Its local implementation owns the process-local Session coordinator and discovers placement through `SessionStore` plus `LocationServiceMap.get(session.location)` only when a drain starts; no layer should take a Session ID. V2 interruption targets the active process-local ownership chain for that Session; interruption of a known but idle or locally unowned Session is a no-op, while the public API rejects an unknown Session.
|
||||
- Keep durable prompt admission separate from model execution. `SessionV2.prompt(...)` admits one durable `session_input` row before scheduling advisory `SessionExecution.wake(sessionID)` unless `resume: false` requests admit-only behavior. The serialized runner promotes admitted inputs into visible user messages at safe boundaries.
|
||||
- Reusing a Session ID adopts the existing Session. Reusing a prompt message ID reconciles an exact retry only when Session, prompt, and delivery mode match; conflicting reuse fails. Historical projected prompts lazily synthesize promoted inbox records during exact retry.
|
||||
- Keep `SessionExecution` process-global and Session-ID based. Its local implementation owns the process-local Session coordinator and discovers placement through `SessionStore` plus `LocationServiceMap.get(session.location)` only when a drain starts; no layer should take a Session ID. V2 interruption targets the active process-local ownership chain for that Session; idle or missing interruption is a no-op.
|
||||
- Keep `SessionRunner`, model resolution, tool registry, permissions, and filesystem Location-scoped. Omitted `Location.workspaceID` means implicit-local placement; explicit workspace identity remains reserved for future placement semantics.
|
||||
- Preserve one explicit `llm.stream(request)` call per Physical Attempt and reload projected history before durable continuation. A logical Step may use generic pre-output retries, one full-context retry after continuation rejection, incomplete-stream continuation, or one overflow-compaction rebuild. Generic retries retain the logical step number and do not consume another agent-step allowance. Do not delegate orchestration to an in-memory tool loop.
|
||||
- Keep local Session drains process-local until clustering is implemented. `SessionRunCoordinator` joins explicit same-Session resumes, coalesces prompt wakeups, and allows different Sessions to run concurrently. A write-ahead execution claim marks a process-local busy period for restart recovery: terminal completion, failure, or user interruption releases it, while shutdown interruption and process death preserve it. Startup recovery resumes claimed top-level Sessions with durable per-execution attempt accounting. The claim is a recovery marker, not clustered ownership, fencing, or an exactly-once guarantee.
|
||||
- Keep provider-specific native compaction mechanisms in `@opencode/ai` behind `LLMClient.compact`. `SessionCompaction` chooses a summary or native compaction from the model's `compaction` setting and owns route provenance, request shrinking, the retry policy, interruption, usage accounting, and checkpoint persistence.
|
||||
- Keep delivery vocabulary explicit. Prompts steer by default. At safe step boundaries, steered compaction takes priority up to the first steered move control; other steers retain enqueue order. At an idle boundary, steers take priority; otherwise exactly one queued item delivers before the runner reevaluates continuation. Inbox items may be cancelled or changed between queue and steer before delivery. Promoting new user input resets the selected agent's step allowance; a batch of steers resets it once.
|
||||
- One step is one logical LLM call; its durable record covers only the model-visible span. Do not write "provider turn", and do not use bare "turn" for a single call: "turn" is reserved for the future assistant-turn unit containing all steps from prompt promotion until the session would go idle.
|
||||
- Keep event replay ownership separate from clustered Session execution ownership.
|
||||
- Keep the Instructions algebra and built-ins in `src/instructions`; keep instruction producers with their observed domains, and keep Session History selection plus `InstructionState` and `InstructionEntry` persistence Session-owned. `InstructionDiscovery` observes ambient global and upward-project instructions. The runner composes built-ins, discovery, guidance, and entries explicitly in `loadInstructions`; there is no instruction registry.
|
||||
- `session.instructions.updated` stores changed source keys and content hashes and may freeze rendered chronological update text. Blob values live once in `instruction_blob`; the projected `instruction_state` row is the normal boundary-processing source of current and initial values. Request assembly renders the epoch baseline from stored values, while later frozen updates enter history as durable System messages. Completed compaction moves the instruction epoch; Session movement retains it so destination instruction changes are chronological, while committed revert clears it. Forks adopt the parent's newest instruction values even when copied message history ends at an earlier boundary. Unavailable sources retain the last value and block only the initial complete delta.
|
||||
- Preserve one explicit `llm.stream(request)` call per provider turn and reload projected history before durable continuation. Do not bridge through legacy `SessionPrompt.loop(...)` or delegate orchestration to an in-memory tool loop.
|
||||
- Keep local Session drains process-local until clustering is implemented. `SessionRunCoordinator` joins explicit same-Session resumes, coalesces prompt wakeups, and allows different Sessions to run concurrently. Advisory wakes drain eligible durable inbox rows only; post-crash continuation recovery requires a separate explicit design before it may retry provider work. A drain has no durable identity or transcript boundary.
|
||||
- Keep delivery vocabulary explicit. Prompts steer by default and promote at the next safe provider-turn boundary while the current drain requires continuation. An explicit `queue` input remains pending until the Session would otherwise become idle; promote one queued input at that boundary, then reevaluate continuation before promoting another. Promoting any new user input resets the selected agent's provider-turn allowance; a batch of steers resets it once.
|
||||
- Keep EventV2 replay owner claims separate from clustered Session execution ownership.
|
||||
- Keep the System Context algebra, registry, and built-ins in `src/system-context`; keep Context Source producers with their observed domains, and keep Session History selection plus Context Epoch persistence Session-owned.
|
||||
@@ -0,0 +1,225 @@
|
||||
# OpenCode Session Runtime
|
||||
|
||||
OpenCode sessions preserve durable conversational history while assembling the runtime context an agent needs to act correctly in its current environment.
|
||||
|
||||
## Language
|
||||
|
||||
**System Context**:
|
||||
The structured collection of contextual facts presented to the model as initial instructions and chronological updates.
|
||||
_Avoid_: System prompt
|
||||
|
||||
**Session History**:
|
||||
The projected chronological conversation selected for a provider turn after applying the active compaction and **Context Epoch** cutoffs.
|
||||
_Avoid_: Session Context
|
||||
|
||||
**Context Source**:
|
||||
One independently observed typed value within the **System Context**, represented by a stable key, JSON codec, infallible loader, pure baseline/update renderers, and an optional removal renderer for dynamic sources.
|
||||
_Avoid_: Prompt fragment
|
||||
|
||||
**System Context Registry**:
|
||||
The Location-scoped registry of ordered, scoped producers that contribute to the current **System Context**.
|
||||
|
||||
**Mid-Conversation System Message**:
|
||||
A durable chronological instruction that tells the model the newly effective state of a changed **Context Source**.
|
||||
_Avoid_: System update, system notification, raw text diff
|
||||
|
||||
**Context Epoch**:
|
||||
The span during which one initially rendered **System Context** remains the immutable provider-cache baseline, ending at completed compaction, Session movement, or an incompatible context transition that requires a fresh baseline.
|
||||
|
||||
**Baseline System Context**:
|
||||
The full **System Context** rendered at the start of a **Context Epoch**.
|
||||
_Avoid_: Live system prompt
|
||||
|
||||
**Context Snapshot**:
|
||||
The overwriteable model-hidden JSON state used to compare each **Context Source** with the value last admitted to a provider turn.
|
||||
|
||||
**Unavailable Context**:
|
||||
An expected temporary inability to observe a **Context Source** value; the runtime retains its prior effective state and emits no update, or omits it until first successfully loaded.
|
||||
|
||||
**Safe Provider-Turn Boundary**:
|
||||
The point immediately before a provider call, after durable input promotion and any required tool settlement, where context changes may be admitted chronologically.
|
||||
|
||||
**Admitted Prompt**:
|
||||
A durable user input accepted into the Session inbox but not yet included in **Session History**.
|
||||
|
||||
**Prompt Promotion**:
|
||||
The durable transition that removes an **Admitted Prompt** from pending input and appends its user message to **Session History**.
|
||||
|
||||
**Provider Turn**:
|
||||
One request to a model provider and the response projected from that request.
|
||||
|
||||
**Session Drain**:
|
||||
One process-local execution span that promotes eligible input and runs required **Provider Turns** until no immediate continuation remains. A Session Drain has no durable identity or transcript boundary.
|
||||
|
||||
**Model Tool Output**:
|
||||
The bounded projection of a Core-executed tool result persisted in Session history and replayed to the model. A tool may shape this projection semantically, but the Tool Registry enforces the final size limit.
|
||||
|
||||
**Managed Tool Output File**:
|
||||
A temporary file created under OpenCode's shared tool-output directory to retain complete output that was too large for Session history.
|
||||
|
||||
**Model Request Options**:
|
||||
Provider-semantic model settings selected from the Catalog and active Session variant before the LLM protocol adapter encodes them for a provider request.
|
||||
_Avoid_: Request body, wire options
|
||||
|
||||
**Generation Controls**:
|
||||
Provider-neutral sampling and output controls, partitioned from provider semantics and compatibility wire fields when model metadata enters the Catalog.
|
||||
|
||||
**Native Continuation Metadata**:
|
||||
Opaque protocol-shaped data attached to assistant content and required to continue that content natively with a compatible model, such as a reasoning signature or provider-hosted item identifier.
|
||||
|
||||
**PTY Environment**:
|
||||
The host-supplied environment overlay applied by the server when creating a PTY, observed for the request Location and resolved PTY working directory.
|
||||
|
||||
**OpenCode Client**:
|
||||
The generated Promise and Effect APIs derived from the public `HttpApi`; **Embedded OpenCode** shares the Effect API through an in-memory `HttpClient` against the same router and handlers.
|
||||
_Avoid_: Remote client
|
||||
|
||||
**SDK Contract IR**:
|
||||
The runtime-neutral compiled representation of the authoritative `HttpApi`, preserving encoded and decoded type projections plus transport metadata so independent SDK emitters can choose their public value model and runtime interpreter.
|
||||
|
||||
**Embedded OpenCode**:
|
||||
A scoped in-process host that structurally extends the **OpenCode Client**, supplies an in-memory HTTP transport, and exposes additional same-process capabilities directly.
|
||||
_Avoid_: Local implementation
|
||||
|
||||
**Page**:
|
||||
A bounded ordered result containing `items` and opaque `previous` and `next` cursor links for navigating the same query in either direction.
|
||||
_Avoid_: Response envelope
|
||||
|
||||
## Relationships
|
||||
|
||||
- A **System Context** is an opaque carrier composed from zero or more **Context Sources**.
|
||||
- **Session History** contains projected conversational messages and admitted **Mid-Conversation System Messages**; the active **Baseline System Context** remains separate provider-request state.
|
||||
- The **System Context Registry** uses stable-keyed scoped contributions to assemble the current **System Context**; contributor removal naturally removes its sources at the next **Safe Provider-Turn Boundary**.
|
||||
- A changed **Context Source** may produce one **Mid-Conversation System Message** containing its newly effective state.
|
||||
- A **Mid-Conversation System Message** persists the exact combined rendered text sent to the model.
|
||||
- The current **Context Snapshot** advances atomically with the corresponding durable **Mid-Conversation System Message**.
|
||||
- A **Context Snapshot** stores one codec-encoded JSON value and, for removable dynamic sources, a pre-rendered removal message per stable **Context Source** key.
|
||||
- Changes from multiple **Context Sources** admitted at one safe boundary combine into one **Mid-Conversation System Message**.
|
||||
- Context changes are sampled and admitted lazily at a **Safe Provider-Turn Boundary**, never pushed asynchronously when their source changes.
|
||||
- At a **Safe Provider-Turn Boundary**, newly promoted user input or settled tool results precede any combined **Mid-Conversation System Message**.
|
||||
- An **Admitted Prompt** is replayable pending input, not yet model-visible **Session History**.
|
||||
- **Prompt Promotion** atomically consumes the pending inbox entry and appends its model-visible user message.
|
||||
- Steering prompts promote at the next **Safe Provider-Turn Boundary** while the current **Session Drain** still requires continuation. Promoting any newly admitted user input resets the selected agent's provider-turn allowance; multiple prompts promoted at one boundary reset it once.
|
||||
- A queued prompt does not promote while the current **Session Drain** requires continuation. The runner promotes one queued prompt when the Session would otherwise become idle, then reevaluates continuation before promoting another.
|
||||
- A **Session Drain** is process-local coordination rather than a durable domain entity. Durable recovery must reason from prompts, projected history, provider attempts, and tool state rather than inventing an enclosing execution identity.
|
||||
- The first provider turn renders the latest complete **Baseline System Context** and initializes its **Context Snapshot** without emitting a redundant **Mid-Conversation System Message**; unavailable initial context blocks the turn instead of persisting an incomplete baseline.
|
||||
- Initial **System Context** preparation precedes the first durable input promotion so an unavailable baseline leaves that input pending and retryable; ordinary reconciliation remains after promotion.
|
||||
- Compaction starts a new **Context Epoch** with a freshly rendered **Baseline System Context** and **Context Snapshot**; prior **Mid-Conversation System Messages** remain durable audit history but leave projected model history.
|
||||
- A newly registered core or plugin-defined **Context Source** absent from the current snapshot emits its baseline rendering once at the next **Safe Provider-Turn Boundary**.
|
||||
- **Context Source** keys are stable and namespaced; duplicate keys fail composition. `SystemContext.combine(...)` preserves caller order; the **System Context Registry** evaluates producers concurrently and combines them in stable contribution-key order so rendered context remains deterministic.
|
||||
- Each **Context Source** loader returns one coherent typed value. `SystemContext.make(...)` hides that value type so differently typed sources compose uniformly. Its codec compares and stores that value; its pure renderers produce model-visible baseline, update, and removal text only when needed.
|
||||
- `SystemContext.initialize(...)` observes a composed **System Context** once and produces a fresh **Baseline System Context** with its **Context Snapshot**.
|
||||
- `SystemContext.reconcile(...)` observes a composed **System Context** once and returns exactly one next action: unchanged, updated, replacement ready, or replacement blocked.
|
||||
- `SystemContext.replace(...)` renders a fresh generation after completed compaction or another baseline-replacing transition; it reports replacement blocked while previously admitted context is unavailable.
|
||||
- **Unavailable Context** uses stale-while-revalidate semantics and is distinct from a successfully loaded absence, which may emit removal text.
|
||||
- Ordinary **Context Source** loaders return values directly; loaders that intentionally use stale-while-revalidate may explicitly return **Unavailable Context**.
|
||||
- Nested project instruction discovery after successful reads remains a follow-up; when implemented, discovered instructions must be admitted durably at the next **Safe Provider-Turn Boundary**.
|
||||
- Location-scoped services naturally re-resolve effective context when a moved session next runs in its destination location.
|
||||
- Moving a Session clears its active **Context Epoch**, so the destination must initialize a complete baseline before another prompt can promote.
|
||||
- Instruction discovery, source identity, persistence, and file loading belong to the instruction service; the **System Context** abstraction only composes effectful producers and renders loaded values.
|
||||
- The first instruction-service slice observes global and upward project `AGENTS.md` files as one ordered aggregate **Context Source** at each **Safe Provider-Turn Boundary**.
|
||||
- Built-in and instruction context producers register through the **System Context Registry** with stable contribution keys. Plugin-defined context registration and hot-reload lifecycle remain a follow-up built on the same scoped registry seam.
|
||||
- Selected-agent available-skill guidance is a **Context Source** composed with Location-wide registry sources immediately before Context Epoch admission. It lists only names and descriptions permitted for that agent; skill bodies and locations are exposed only through the permission-checked `skill` tool.
|
||||
- The selected agent and model are sampled when a provider turn starts. Changes admitted after that boundary apply to the next provider turn and do not restart the current turn.
|
||||
- Selected-agent available-skill guidance remains a **Context Source**. An agent switch that changes that guidance produces a **Mid-Conversation System Message** while preserving the current baseline.
|
||||
- Local tool authorization and pending permission requests retain the effective agent of the provider turn that issued the call; a later agent switch cannot change that call's policy.
|
||||
- Context source changes never wake idle sessions; the next naturally scheduled **Safe Provider-Turn Boundary** loads and compares current values lazily.
|
||||
- Once admitted, a **Mid-Conversation System Message** remains durable even if the following provider attempt fails and is replayed unchanged on retry.
|
||||
- **Mid-Conversation System Messages** remain durable Session-message history; normal user-facing transcript surfaces may hide them.
|
||||
- The date **Context Source** initially preserves host-local calendar-date behavior; a configured user timezone may replace that default later.
|
||||
- A **Context Epoch** begins with one immutable **Baseline System Context**.
|
||||
- A **Baseline System Context** is stored durably and reused verbatim across process restarts within its **Context Epoch**.
|
||||
- A **Baseline System Context** durably preserves the exact joined text used for the active provider-cache prefix.
|
||||
- Completed compaction starts a new **Context Epoch** on the next provider attempt, folding the current complete **System Context** into a fresh baseline and removing earlier **Mid-Conversation System Messages** from active model history.
|
||||
- A model/provider switch preserves the current **Context Epoch** and chronological conversation history; the new selection applies to the next provider turn.
|
||||
- **Native Continuation Metadata** remains in durable history. Provider-turn projection includes it only for a successful exact originating provider/model match; failed turns and incompatible models omit opaque metadata, while non-empty visible reasoning lowers to ordinary assistant text after a model switch. This conservative relation may widen only when recorded provider tests establish compatibility.
|
||||
- **Model Request Options** remain provider-semantic through Catalog resolution. The Session runner maps them into the LLM package's provider-option namespace; the selected protocol adapter alone owns provider wire encoding.
|
||||
- **Generation Controls**, protocol-semantic **Model Request Options**, and compatibility request body fields are separate Catalog domains. A shared ingestion adapter partitions legacy and models.dev AI-SDK-shaped options before routing.
|
||||
- The **PTY Environment** is a server concern rather than a Core PTY concern. PTY creation merges caller values, then the host overlay, then Core-forced terminal invariants such as `TERM` and `OPENCODE_TERMINAL`.
|
||||
- Networked and **Embedded OpenCode** use the same **OpenCode Client** and preserve the full HTTP encoding, routing, middleware, and decoding boundary; only the `HttpClient` transport differs.
|
||||
- The Effect-native network constructor obtains `HttpClient.HttpClient` from its environment so callers own transport selection, recording, tracing, retries, and tests. Convenience runtimes may provide a fetch transport separately.
|
||||
- Creating **Embedded OpenCode** is scoped. Closing its owning Scope releases the in-process server resources, database resources, registrations, and fibers.
|
||||
- **Embedded OpenCode** exposes shared client capabilities and embedded-only capabilities on one object; consumers do not navigate through a nested `.client` property.
|
||||
- The beta **OpenCode Client** currently uses plural consumer-facing capability groups such as `sessions`; whether the stable Session namespace should instead be singular `session` must be settled before stabilization. Internal server identifiers do not implicitly define public client names.
|
||||
- Server's concrete `HttpApi` is authoritative for shared **OpenCode Client** capabilities. Codegen compiles its Session group directly; the Effect runtime uses an equivalent Protocol-only projection so generated artifacts remain independent of Core and Server.
|
||||
- SDK generation reflects the public `HttpApi` once into an **SDK Contract IR**. Promise and Effect emitters share endpoint structure and transport metadata without being required to expose identical public values: an emitter may select encoded wire types, decoded domain types, compile-time brands, runtime validation, and its own execution abstraction independently.
|
||||
- The first Effect emitter is the rich projection: it exposes decoded Effect-native values, preserves brands and schema transformations, performs runtime schema decoding, and delegates transport interpretation to `HttpApiClient`. Lighter wire-shaped Effect output remains possible through another emitter policy rather than constraining the shared IR.
|
||||
- The rich Effect emitter regenerates private executable schemas when the **SDK Contract IR** proves that their transport semantics can be reproduced exactly. Contracts with authoritative custom transformations use the import-based Effect emitter against a Protocol-only client projection whose generated transport output is tested against Server's concrete API; the Promise emitter still derives zero-Effect structural wire types from the same IR.
|
||||
- `@opencode-ai/protocol` owns Session endpoint construction and middleware placement. Server supplies concrete middleware keys to produce the authoritative build-time API; the client projection supplies transport-only keys without importing Core or Server at runtime.
|
||||
- The first Promise emitter targets the same clean domain-oriented method organization rather than Hey API source compatibility. It returns unwrapped values directly, rejects declared and infrastructure failures, and begins with minimal client-level transport configuration; result wrappers, interceptors, and legacy generated signatures are outside the initial surface.
|
||||
- The first Promise emitter parses response syntax and trusts its generated structural types; it does not perform runtime structural validation. Malformed payload syntax fails, while a syntactically valid shape mismatch is not detected at the SDK boundary. Standalone validator generation remains an optional future emitter policy.
|
||||
- Declared Promise-client failures retain their tagged structural wire values and have generated type guards. Consumers do not depend on generated `Error` subclass identity, preserving discrimination across package copies and realms while remaining structurally aligned with Effect domain errors.
|
||||
- Promise-client infrastructure failures use one generated `ClientError` class with a structured reason such as transport failure, unexpected status, unsupported content type, or malformed response. Promise methods reject with either a tagged declared domain failure or `ClientError`, matching the Effect client's conceptual domain/infrastructure error division.
|
||||
- Promise methods accept a separate optional per-call transport-options argument containing `AbortSignal` and header overrides. Cancellation and transport metadata do not enter the domain input object; broader interceptor and response-mode APIs remain deferred.
|
||||
- Promise streaming methods return a lazy `AsyncIterable` directly rather than a Promise-wrapped stream object. Iteration opens the connection, `AbortSignal` cancels it, and ending iteration closes the underlying request; the Effect emitter analogously returns `Stream` directly.
|
||||
- Promise SSE connection establishment, declared HTTP failures, and infrastructure failures occur during `AsyncIterable` iteration, beginning with its first `next()` call, rather than during synchronous method construction.
|
||||
- Neither generated streaming runtime automatically reconnects after disconnection. Promise `AsyncIterable` and Effect `Stream` fail explicitly; live consumers refresh and resubscribe, while durable sequence-based resume remains explicit composition above the generated client.
|
||||
- Promise client construction is synchronous and network-free. It requires `baseUrl`, defaults to `globalThis.fetch`, accepts client-level headers, and merges them with per-call header overrides.
|
||||
- Effect client construction accepts an explicit `baseUrl` and obtains `HttpClient.HttpClient` from the Effect environment. It does not install fetch or duplicate per-call transport policy; callers transform/provide the client for headers, tracing, retries, recording, and tests, while fiber interruption owns cancellation.
|
||||
- Promise and Effect emitters each own their generated public type modules. The **SDK Contract IR**, not a physically shared generated type package, is the common source; this permits zero-Effect wire types and rich decoded Effect types to evolve independently.
|
||||
- Promise and Effect network clients ship from `@opencode-ai/client` behind isolated root and `/effect` exports. The root has no runtime path to Effect; `/effect` imports only Effect, Schema, and Protocol.
|
||||
- The Effect-native scoped host belongs to `@opencode-ai/sdk-next`, which will assume the existing `@opencode-ai/sdk` name after legacy consumers migrate. Client remains network-only and SDK depends one-way on Client.
|
||||
- SDK executes Server's assembled `HttpRouter` in memory. It opens no listener and performs no network I/O, while preserving Server routing, middleware, codecs, handlers, and errors.
|
||||
- The Effect Client and SDK re-export their decoded datatype facade from Schema so callers do not depend on internal package locations or Core's versioned names.
|
||||
- A capability intended for both networked and **Embedded OpenCode** belongs in the authoritative public `HttpApi`; embedded-only same-process capabilities extend **Embedded OpenCode** separately.
|
||||
- `sessions.events({ sessionID, after })` is a public durable Session event stream. It verifies the Session, replays durable events after the optional aggregate sequence, continues with newly committed durable events, excludes live-only fragments, and is transported as SSE in both networked and embedded modes.
|
||||
- `events.subscribe()` is a distinct public instance-wide live stream for Session and non-Session activity. It has no replay guarantee and includes connection, heartbeat, and instance-disposal lifecycle events; consumers recover from disconnection by refreshing authoritative state.
|
||||
- A Session ID is not an optional filter on `events.subscribe()`: instance-wide live events and durable Session events have different schemas, replay guarantees, cursors, lifecycle events, and failure behavior.
|
||||
- The initial common OpenCode Client does not expose server-global event aggregation. `events.subscribe()` is bounded to the connected OpenCode instance or workspace; any future cross-instance administrative stream requires a separately designed API.
|
||||
- `events.subscribe()` does not automatically reconnect after transport loss. The live-only stream fails with `ClientError`; consumers refresh authoritative state before explicitly opening a new subscription because events missed during disconnection cannot be replayed.
|
||||
- `sessions.events({ sessionID, after })` returns the generated HTTP client's cold durable event stream and does not build reconnection policy into the endpoint or client constructor. Transport loss fails the stream with `ClientError`. Callers may compose an explicit resuming stream above it by retaining the last observed durable sequence and opening a new subscription with `after`; any reusable resume helper remains a separate API design question.
|
||||
- The stable `sessions.list(...)` design returns a **Page** in both networked and **Embedded OpenCode**; embedded execution does not define a separate unbounded array-returning list operation. The beta client currently preserves the existing HTTP `{ data, cursor }` envelope until emitter-level Page projection is implemented.
|
||||
- Session list cursors are opaque branded values carrying continuation query and ordering state. Consumers pass them back unchanged and do not inspect storage anchors or encoded filter fields.
|
||||
- A Session list continuation accepts only its opaque cursor. Scope, filters, ordering, and page size are fixed by the initial query and carried by that cursor.
|
||||
- `sessions.messages(...)` returns a **Page** and uses the same cursor discipline as `sessions.list(...)`: the initial request supplies `sessionID`, ordering, and page size; continuation supplies `sessionID` plus only an opaque branded message cursor carrying ordering, page size, direction, and message anchor. Using a cursor with another Session is invalid.
|
||||
- `sessions.message({ sessionID, messageID })` is a required resource lookup. An unknown Session fails with `SessionNotFoundError`; a known Session with an absent or differently owned message fails with `MessageNotFoundError` without disclosing cross-Session ownership. Absence is not represented as `undefined` across the public HTTP boundary.
|
||||
- `sessions.interrupt({ sessionID })` first verifies that the durable Session exists, failing with `SessionNotFoundError` otherwise. For a known Session, interruption is idempotent: idle, already-settled, or locally unowned execution is a no-op.
|
||||
- `sessions.active()` snapshots the current process's foreground Session drain registry as a record of Session IDs to `{ type: "running" }`. Missing IDs are inactive; background subagents and tasks do not make their parent Session active, and process restart clears the registry.
|
||||
- `sessions.context({ sessionID })` preserves the existing message-only operation. It returns projected conversational messages selected as Session context; it does not include or represent the complete provider request context, whose baseline system context and other contributions remain separate.
|
||||
- **Open question**: Should a future, separately named operation expose the complete provider request context, including baseline system context, selected source contributions, and context-epoch metadata?
|
||||
- `sessions.prompt(...)` exposes `resume?: boolean`. Omitting it preserves durable admission followed by an advisory execution wake; `resume: false` requests durable admit-only behavior.
|
||||
- The public operation remains `sessions.prompt(...)`; `SessionInput.admit` is the internal primitive, while the public `Admission` result and `resume` option express its durable admission semantics.
|
||||
- `sessions.create(...)` accepts an optional `location`. Omission resolves through the connected OpenCode instance's default or current location; an explicit value selects a known location. Networked and embedded transports use the same handler semantics.
|
||||
- `sessions.switchAgent({ sessionID, agent })` is part of the common client alongside `sessions.switchModel(...)`. It affects subsequent Session activity and fails with `SessionNotFoundError` for an unknown Session.
|
||||
- The **Embedded OpenCode** Layer delegates to the same scoped creation path; it does not define a second implementation.
|
||||
- A **PTY Environment** adapter observes plugins in the request Location while passing the resolved PTY working directory to the hook; standalone servers use an empty adapter.
|
||||
- A **Mid-Conversation System Message** lowers to the provider's native chronological instruction role when supported and to a wrapped chronological fallback otherwise.
|
||||
- When the effective aggregate instruction set changes, its **Mid-Conversation System Message** includes the complete current ordered set and supersedes the prior aggregate value; when no ambient instructions remain, the message states that previously loaded instructions no longer apply.
|
||||
- Ambient project instruction discovery honors `OPENCODE_DISABLE_PROJECT_CONFIG`; global instructions remain eligible.
|
||||
- Oversized textual **Model Tool Output** retains a bounded preview in Session history while its complete text moves to managed tool-output storage. Arbitrary structured-result size is a separate concern.
|
||||
- One tool settlement receives one aggregate textual limit, using the configured maximum lines or UTF-8 bytes, whichever is reached first. The limit is provider-independent; token pressure belongs to context assembly and compaction.
|
||||
- Generic truncation preserves the beginning and end of textual output. Tools may apply a more meaningful strategy before the Tool Registry enforces the final limit.
|
||||
- A truncated **Model Tool Output** identifies its complete text both in the bounded model-visible preview and as a typed managed output path. Managed output paths do not modify the tool's validated structured result.
|
||||
- A **Managed Tool Output File** is temporary and may expire after its retention period. The bounded **Model Tool Output**, not the file, is the durable replayable record.
|
||||
- Failure to retain a **Managed Tool Output File** does not change a successful tool operation into a failed one. The Session records an explicitly lossy bounded output without a path, while operators receive diagnostics for the storage failure.
|
||||
- Once a tool operation succeeds, bounding its **Model Tool Output** and publishing its one durable settlement form an interruption-safe completion region. Raw oversized success is never published before a later correction.
|
||||
- When a structured-only result would exceed the **Model Tool Output** limit, its validated structured value remains unchanged for Session consumers while model replay uses a bounded textual JSON preview and optional managed output path.
|
||||
- Existing tool-managed output paths survive generic bounding. A fallback file retains exactly the complete projected text received by the Tool Registry and never claims to reconstruct output already discarded by tool-specific shaping.
|
||||
- **Managed Tool Output Files** use globally unique names in one shared flat directory. Their absolute paths are readable and searchable by ordinary tools; other absolute paths remain outside Location-scoped filesystem authority.
|
||||
- Provider-executed tool results remain provider-native transcript facts outside generic Tool Registry bounding. Their context control requires provider-aware pruning or compaction because some providers require exact structured round-trip payloads.
|
||||
|
||||
## Client contract architecture
|
||||
|
||||
Semantic values that mean the same thing internally and publicly live in the lightweight Schema leaf. Core consumes Schema for domain behavior; Protocol composes Schema values into paths, payloads, envelopes, errors, cursors, and streams; Server imports both, hosts Protocol's exact groups, and owns protocol/domain adaptation. The root Promise client remains zero-Effect, `/effect` depends on Effect plus Schema and Protocol, and `@opencode-ai/sdk-next` composes the scoped in-process host above Client, Core, and Server.
|
||||
|
||||
Shared public records are plain objects declared with `Schema.Struct`. A same-name inferred interface gives object records readable TypeScript signatures without constructors, prototypes, or nominal identity; unions retain explicit type aliases.
|
||||
|
||||
Before stabilizing the client API:
|
||||
|
||||
- Keep additional public schemas in Schema and additional network groups in Protocol; neither package may transitively load databases, Drizzle, Session execution, providers, watchers, native modules, or WASM.
|
||||
- Keep concrete Location middleware keys in Server while Protocol owns their placement. Client projections may supply transport-only keys, but must prove generated equivalence with Server's concrete API.
|
||||
- Project the existing list response envelope to the stable client **Page** shape and enforce separate initial-query and cursor-continuation inputs without changing the hosted V2 wire contract.
|
||||
- Settle the stable consumer namespace (`session` versus the current beta `sessions`) and use an explicit codegen annotation if the consumer name should differ from the server group identifier.
|
||||
- Preserve V2 route paths, operation IDs, codecs, errors, middleware behavior, and OpenAPI output while making this change.
|
||||
- Preserve browser-safe `@opencode-ai/client` and `@opencode-ai/client/effect` bundles through import-boundary tests.
|
||||
- Define embedded-host placement before supporting multiple hosts over one database. Hosts that share durable Session storage must also share process-local Session execution coordination, or each host must receive isolated storage explicitly.
|
||||
- Keep an embedded request scope alive until any streamed response body finishes. The initial non-streaming Session surface does not exercise this lifetime boundary; Session and instance event streams must do so before joining the embedded client.
|
||||
|
||||
## Example dialogue
|
||||
|
||||
> **Dev:** "The date changed while the session was active. Should the **Mid-Conversation System Message** say what the old date was?"
|
||||
> **Domain expert:** "No. Emit the newly effective date so the agent can act on the current **System Context**."
|
||||
|
||||
## Flagged ambiguities
|
||||
|
||||
- Legacy `experimental.chat.system.transform` can mutate the assembled baseline system prompt arbitrarily, but V2 plugins do not yet expose an equivalent hook. Decide separately whether to port it, replace dynamic uses with plugin-defined **Context Sources**, or narrow its semantics.
|
||||
@@ -1,112 +1,272 @@
|
||||
# Contributing to OpenCode
|
||||
|
||||
The changes most likely to be accepted are:
|
||||
We want to make it easy for you to contribute to OpenCode. Here are the most common type of changes that get merged:
|
||||
|
||||
- Bug fixes
|
||||
- Additional LSPs and formatters
|
||||
- LLM performance improvements
|
||||
- Environment-specific fixes
|
||||
- Additional LSPs / Formatters
|
||||
- Improvements to LLM performance
|
||||
- Support for new providers
|
||||
- Fixes for environment-specific quirks
|
||||
- Missing standard behavior
|
||||
- Documentation improvements
|
||||
|
||||
UI and core product features require design review before implementation. If you are unsure whether a change fits, ask a maintainer or choose an issue labeled [`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), or [`perf`](https://github.com/anomalyco/opencode/issues?q=is%3Aopen%20is%3Aissue%20label%3A%22perf%22).
|
||||
However, any UI or core product feature must go through a design review with the core team before implementation.
|
||||
|
||||
Want to take on an issue? Leave a comment and a maintainer may assign it unless it is already being worked on.
|
||||
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/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.
|
||||
|
||||
## Adding Providers
|
||||
Want to take on an issue? Leave a comment and a maintainer may assign it to you unless it is something we are already working on.
|
||||
|
||||
New providers should rarely require OpenCode changes. Add the provider to [models.dev](https://github.com/anomalyco/models.dev) first.
|
||||
## Adding New Providers
|
||||
|
||||
## Development
|
||||
New providers shouldn't require many if ANY code changes, but if you want to add support for a new provider first make a PR to:
|
||||
https://github.com/anomalyco/models.dev
|
||||
|
||||
OpenCode requires Bun 1.3 or newer. From the repository root:
|
||||
## Developing OpenCode
|
||||
|
||||
- Requirements: Bun 1.3+
|
||||
- Install dependencies and start the dev server from the repo root:
|
||||
|
||||
```bash
|
||||
bun install
|
||||
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 install
|
||||
bun dev [directory]
|
||||
bun dev <directory>
|
||||
```
|
||||
|
||||
`bun dev` runs the V2 CLI and TUI. Pass a directory to open another project, or `.` to open this repository.
|
||||
|
||||
To test a development TUI against your installed OpenCode V2 background service and live sessions:
|
||||
To run OpenCode in the root of the opencode repo itself:
|
||||
|
||||
```bash
|
||||
bun run dev:live [directory]
|
||||
bun dev .
|
||||
```
|
||||
|
||||
For web development, run the backend and app in separate terminals. Other interfaces have root scripts:
|
||||
### Building a "localcode"
|
||||
|
||||
To compile a standalone executable:
|
||||
|
||||
```bash
|
||||
bun dev serve --port 4096
|
||||
bun run dev:web
|
||||
bun run dev:desktop
|
||||
bun run dev:www
|
||||
./packages/opencode/script/build.ts --single
|
||||
```
|
||||
|
||||
### Packages
|
||||
|
||||
- `packages/schema`: shared wire and storage contracts
|
||||
- `packages/core`: domain behavior and persistence
|
||||
- `packages/protocol`: public API definitions
|
||||
- `packages/server`: HTTP server and runtime composition
|
||||
- `packages/client`: generated TypeScript clients
|
||||
- `packages/cli`: command-line entrypoint and service lifecycle
|
||||
- `packages/tui`: terminal interface
|
||||
- `packages/app`: shared web interface
|
||||
- `packages/desktop`: Electron desktop application
|
||||
- `packages/plugin`: plugin API
|
||||
|
||||
### Verification
|
||||
|
||||
Run typechecks, and tests where defined, from the affected package rather than the repository root:
|
||||
Then run it with:
|
||||
|
||||
```bash
|
||||
cd packages/core
|
||||
bun run test
|
||||
bun typecheck
|
||||
./packages/opencode/dist/opencode-<platform>/bin/opencode
|
||||
```
|
||||
|
||||
Follow package-specific instructions in nearby `AGENTS.md` files. After changing the public Protocol or Server `HttpApi`, run `bun run generate` from `packages/client`; never edit generated client files directly.
|
||||
Replace `<platform>` with your platform (e.g., `darwin-arm64`, `linux-x64`).
|
||||
|
||||
Follow the repository [style guide](./AGENTS.md).
|
||||
- Core pieces:
|
||||
- `packages/opencode`: OpenCode core business logic & server.
|
||||
- `packages/opencode/src/cli/cmd/tui/`: The TUI code, written in SolidJS with [opentui](https://github.com/sst/opentui)
|
||||
- `packages/app`: The shared web UI components, written in SolidJS
|
||||
- `packages/desktop`: The native desktop app, built with Electron (wraps `packages/app`)
|
||||
- `packages/plugin`: Source for `@opencode-ai/plugin`
|
||||
|
||||
## Pull Requests
|
||||
### Understanding bun dev vs opencode
|
||||
|
||||
### Link Issues When Required
|
||||
During development, `bun dev` is the local equivalent of the built `opencode` command. Both run the same CLI interface:
|
||||
|
||||
Bug fixes, chores, and tests must reference an existing issue. Documentation, refactor, and feature PRs are exempt from the automated linked-issue check. When required, use `Fixes #123` or `Closes #123` in the PR description.
|
||||
```bash
|
||||
# Development (from project root)
|
||||
bun dev --help # Show all available commands
|
||||
bun dev serve # Start headless API server
|
||||
bun dev web # Start server + open web interface
|
||||
bun dev <directory> # Start TUI in specific directory
|
||||
|
||||
Before implementing new functionality, open a feature request describing the problem, why it belongs in OpenCode, and your proposed approach if you have one. Wait for design approval before opening the implementation PR.
|
||||
# Production
|
||||
opencode --help # Show all available commands
|
||||
opencode serve # Start headless API server
|
||||
opencode web # Start server + open web interface
|
||||
opencode <directory> # Start TUI in specific directory
|
||||
```
|
||||
|
||||
Base branches on `v2`, not `dev`, and complete the provided pull request template.
|
||||
### Running the API Server
|
||||
|
||||
### Keep It Focused
|
||||
To start the OpenCode headless API server:
|
||||
|
||||
- Keep PRs small and focused.
|
||||
- Explain the problem and why the change fixes it.
|
||||
- Check whether the functionality already exists.
|
||||
- For UI changes, include before-and-after screenshots or video.
|
||||
- For logic changes, explain what you tested and how a reviewer can verify it.
|
||||
```bash
|
||||
bun dev serve
|
||||
```
|
||||
|
||||
### Keep It Brief
|
||||
This starts the headless server on port 4096 by default. You can specify a different port:
|
||||
|
||||
Long, AI-generated PR descriptions and issues may be ignored. Write a short explanation in your own words. If the change cannot be explained briefly, the PR may be too large.
|
||||
```bash
|
||||
bun dev serve --port 8080
|
||||
```
|
||||
|
||||
### Use Conventional Titles
|
||||
### Running the Web App
|
||||
|
||||
Use `type(scope): summary`. Supported types are `feat`, `fix`, `docs`, `chore`, `refactor`, and `test`. The scope is optional.
|
||||
To test UI changes during development:
|
||||
|
||||
1. **First, start the OpenCode server** (see [Running the API Server](#running-the-api-server) section above)
|
||||
2. **Then run the web app:**
|
||||
|
||||
```bash
|
||||
bun run --cwd packages/app dev
|
||||
```
|
||||
|
||||
This starts a local dev server at http://localhost:5173 (or similar port shown in output). Most UI changes can be tested here, but the server must be running for full functionality.
|
||||
|
||||
### Running the Desktop App
|
||||
|
||||
The desktop app is an Electron application that wraps the web UI.
|
||||
|
||||
To run the desktop app in development:
|
||||
|
||||
```bash
|
||||
bun run --cwd packages/desktop dev
|
||||
```
|
||||
|
||||
To create a production build and package the app:
|
||||
|
||||
```bash
|
||||
bun run --cwd packages/desktop build
|
||||
bun run --cwd packages/desktop package
|
||||
```
|
||||
|
||||
> [!NOTE]
|
||||
> If you make changes to the API or SDK (e.g. `packages/opencode/src/server/server.ts`), run `./script/generate.ts` to regenerate the SDK and related files.
|
||||
|
||||
Please try to follow the [style guide](./AGENTS.md)
|
||||
|
||||
### Setting up a Debugger
|
||||
|
||||
Bun debugging is currently rough around the edges. We hope this guide helps you get set up and avoid some pain points.
|
||||
|
||||
The most reliable way to debug OpenCode is to run it manually in a terminal via `bun run --inspect=<url> dev ...` and attach
|
||||
your debugger via that URL. Other methods can result in breakpoints being mapped incorrectly, at least in VSCode (YMMV).
|
||||
|
||||
Caveats:
|
||||
|
||||
- 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/ --cwd packages/opencode ./src/index.ts serve --port 4096`,
|
||||
then attach TUI with `opencode attach http://localhost:4096`
|
||||
- Debug TUI: `bun run --inspect=ws://localhost:6499/ --cwd packages/opencode ./src/index.ts`
|
||||
|
||||
Other tips and tricks:
|
||||
|
||||
- You might want to use `--inspect-wait` or `--inspect-brk` instead of `--inspect`, depending on your workflow
|
||||
- Specifying `--inspect=ws://localhost:6499/` on every invocation can be tiresome, you may want to `export BUN_OPTIONS=--inspect=ws://localhost:6499/` instead
|
||||
|
||||
#### VSCode Setup
|
||||
|
||||
If you use VSCode, you can use our example configurations [.vscode/settings.example.json](.vscode/settings.example.json) and [.vscode/launch.example.json](.vscode/launch.example.json).
|
||||
|
||||
Some debug methods that can be problematic:
|
||||
|
||||
- Debug configurations with `"request": "launch"` can have breakpoints incorrectly mapped and thus unusable
|
||||
- The same problem arises when running OpenCode in the VSCode `JavaScript Debug Terminal`
|
||||
|
||||
With that said, you may want to try these methods, as they might work for you.
|
||||
|
||||
## Pull Request Expectations
|
||||
|
||||
### Issue First Policy
|
||||
|
||||
**All PRs must reference an existing issue.** Before opening a PR, open an issue describing the bug or feature. This helps maintainers triage and prevents duplicate work. PRs without a linked issue may be closed without review.
|
||||
|
||||
- Use `Fixes #123` or `Closes #123` in your PR description to link the issue
|
||||
- For small fixes, a brief issue is fine - just enough context for maintainers to understand the problem
|
||||
|
||||
### General Requirements
|
||||
|
||||
- Keep pull requests small and focused
|
||||
- Explain the issue and why your change fixes it
|
||||
- Before adding new functionality, ensure it doesn't already exist elsewhere in the codebase
|
||||
|
||||
### UI Changes
|
||||
|
||||
If your PR includes UI changes, please include screenshots or videos showing the before and after. This helps maintainers review faster and gives you quicker feedback.
|
||||
|
||||
### Logic Changes
|
||||
|
||||
For non-UI changes (bug fixes, new features, refactors), explain **how you verified it works**:
|
||||
|
||||
- What did you test?
|
||||
- How can a reviewer reproduce/confirm the fix?
|
||||
|
||||
### No AI-Generated Walls of Text
|
||||
|
||||
Long, AI-generated PR descriptions and issues are not acceptable and may be ignored. Respect the maintainers' time:
|
||||
|
||||
- Write short, focused descriptions
|
||||
- Explain what changed and why in your own words
|
||||
- If you can't explain it briefly, your PR might be too large
|
||||
|
||||
### PR Titles
|
||||
|
||||
PR titles should follow conventional commit standards:
|
||||
|
||||
- `feat:` new feature or functionality
|
||||
- `fix:` bug fix
|
||||
- `docs:` documentation or README changes
|
||||
- `chore:` maintenance tasks, dependency updates, etc.
|
||||
- `refactor:` code refactoring without changing behavior
|
||||
- `test:` adding or updating tests
|
||||
|
||||
You can optionally include a scope to indicate which package is affected:
|
||||
|
||||
- `feat(app):` feature in the app package
|
||||
- `fix(desktop):` bug fix in the desktop package
|
||||
- `chore(opencode):` maintenance in the opencode package
|
||||
|
||||
Examples:
|
||||
|
||||
- `docs: update contributing guide`
|
||||
- `fix(tui): restore scroll position`
|
||||
- `feat(app): add workspace search`
|
||||
- `docs: update contributing guidelines`
|
||||
- `fix: resolve crash on startup`
|
||||
- `feat: add dark mode support`
|
||||
- `feat(app): add dark mode support`
|
||||
- `fix(desktop): resolve crash on startup`
|
||||
- `chore: bump dependency versions`
|
||||
|
||||
## Issues
|
||||
### Style Preferences
|
||||
|
||||
Bug reports and feature requests must use their issue templates. Blank issues are not allowed; ask support and how-to questions in the [Discord community](https://discord.gg/opencode).
|
||||
These are not strictly enforced, they are just general guidelines:
|
||||
|
||||
Automated checks flag missing templates, placeholder text, AI-generated walls of text, and missing meaningful content. You have two hours to correct a flagged issue before it closes automatically. Ask a maintainer if an issue was flagged incorrectly.
|
||||
- **Functions:** Keep logic within a single function unless breaking it out adds clear reuse or composition benefits.
|
||||
- **Destructuring:** Do not do unnecessary destructuring of variables.
|
||||
- **Control flow:** Avoid `else` statements.
|
||||
- **Error handling:** Prefer `.catch(...)` instead of `try`/`catch` when possible.
|
||||
- **Types:** Reach for precise types and avoid `any`.
|
||||
- **Variables:** Stick to immutable patterns and avoid `let`.
|
||||
- **Naming:** Choose concise single-word identifiers when they remain descriptive.
|
||||
- **Runtime APIs:** Use Bun helpers such as `Bun.file()` when they fit the use case.
|
||||
|
||||
## Feature Requests
|
||||
|
||||
For net-new functionality, start with a design conversation. Open an issue describing the problem, your proposed approach (optional), and why it belongs in OpenCode. The core team will help decide whether it should move forward; please wait for that approval instead of opening a feature PR directly.
|
||||
|
||||
## Issue Requirements
|
||||
|
||||
All issues **must** use one of our issue templates:
|
||||
|
||||
- **Bug report** — for reporting bugs (requires a description)
|
||||
- **Feature request** — for suggesting enhancements (requires verification checkbox and description)
|
||||
- **Question** — for asking questions (requires the question)
|
||||
|
||||
Blank issues are not allowed. When a new issue is opened, an automated check verifies that it follows a template and meets our contributing guidelines. If an issue doesn't meet the requirements, you'll receive a comment explaining what needs to be fixed and have **72 hours** to edit the issue. After that, it will be automatically closed.
|
||||
|
||||
Issues may be flagged for:
|
||||
|
||||
- Not using a template
|
||||
- Required fields left empty or filled with placeholder text
|
||||
- AI-generated walls of text
|
||||
- Missing meaningful content
|
||||
|
||||
If you believe your issue was incorrectly flagged, let a maintainer know.
|
||||
@@ -1,279 +0,0 @@
|
||||
# V2 HTTP API audit checklist
|
||||
|
||||
**Source:** `packages/protocol/openapi.json`
|
||||
**Current endpoint count:** 139
|
||||
**Last regenerated:** 2026-09-13
|
||||
|
||||
## How to use this checklist
|
||||
|
||||
Review endpoints in document order. For each endpoint, select one disposition and capture rationale or follow-up work in Notes. Mark **Reviewed** only after the disposition is agreed.
|
||||
|
||||
### Review criteria
|
||||
|
||||
- Resource and operation naming
|
||||
- HTTP method and idempotency
|
||||
- Request parameters and location scope
|
||||
- Response shape and error taxonomy
|
||||
- Authentication and authorization
|
||||
- Current production consumers
|
||||
- Stability level: public, experimental, or internal
|
||||
- Whether the generated client API is intuitive
|
||||
|
||||
### Disposition legend
|
||||
|
||||
- **Keep:** ship unchanged as a supported V2 API
|
||||
- **Change:** retain after a defined contract change
|
||||
- **Remove:** exclude from the official V2 API
|
||||
- **Experimental-only:** retain outside the stable API commitment
|
||||
|
||||
## Progress
|
||||
|
||||
- [x] Group 1: Foundation and placement (4)
|
||||
- [x] Group 2: Configuration and capability catalogs (16)
|
||||
- [x] Group 3: Credentials, integrations, MCP, and web search (22)
|
||||
- [x] Group 4: Session lifecycle (12)
|
||||
- [x] Group 5: Session execution and inputs (11)
|
||||
- [x] Group 6: Session history and recovery (13)
|
||||
- [x] Group 7: Inbox, permissions, and forms (19)
|
||||
- [x] Group 8: Filesystem, worktrees, and VCS (12)
|
||||
- [x] Group 9: PTYs, persistent terminals, and shells (24)
|
||||
- [x] Group 10: Events, RPC, and experimental operations (6)
|
||||
|
||||
## Resolved during audit
|
||||
|
||||
### [x] `POST /api/plugin/await-activation`
|
||||
|
||||
- **Decision:** Remove
|
||||
- **Notes:** Activation timing is an internal server concern. Catalog reads remain non-blocking.
|
||||
|
||||
### [x] Location response wrappers
|
||||
|
||||
- **Decision:** Reduce generic endpoint response locations to `{ directory }`.
|
||||
- **Notes:** Full project metadata remains available from `GET /api/location`; no consumers used it from wrapped responses.
|
||||
|
||||
### [x] `GET /api/health` and `GET /api/server`
|
||||
|
||||
- **Decision:** Merge and rename
|
||||
- **Replacement:** `GET /api/info` with operation ID `server.info`.
|
||||
- **Notes:** Returns `version`, `pid`, and connection `urls`; readiness is conveyed by HTTP status.
|
||||
|
||||
### [x] `GET /api/project/current`
|
||||
|
||||
- **Decision:** Remove
|
||||
- **Replacement:** `GET /api/location`, using `project` from the response.
|
||||
- **Notes:** The endpoint duplicated `Location.Info.project`; production callers were migrated.
|
||||
|
||||
### [x] `POST /api/workspace` and `DELETE /api/workspace/{workspaceID}`
|
||||
|
||||
- **Decision:** Remove
|
||||
- **Notes:** Provider-backed workspaces are not part of the V2 HTTP contract and can be introduced later. Core and the embedded SDK retain internal workspace support.
|
||||
|
||||
## Group 1: Foundation and placement
|
||||
|
||||
**Endpoints:** 4
|
||||
|
||||
| Done | Method | Path | Operation ID | Decision | Notes |
|
||||
|---|---|---|---|---|---|
|
||||
| [x] 001–002 | `GET` | `/api/info` | `server.info` | Keep | Replaces the former health and server endpoints. |
|
||||
| [x] 003 | `GET` | `/api/location` | `location.get` | Keep | Workspace selectors and response fields removed until workspace support ships. |
|
||||
| [x] 004 | `GET` | `/api/project` | `project.list` | Keep | Removed unused `time.initialized`; the database column remains for migration data. |
|
||||
| [x] 005 | `PATCH` | `/api/project/{projectID}` | `project.update` | Keep | Request and response accepted as-is. |
|
||||
|
||||
## Group 2: Configuration and capability catalogs
|
||||
|
||||
**Endpoints:** 16
|
||||
|
||||
| Done | Method | Path | Operation ID | Decision | Notes |
|
||||
|---|---|---|---|---|---|
|
||||
| [x] 008 | `GET` | `/api/agent` | `agent.list` | Keep | Request and response accepted as-is. |
|
||||
| [x] 009 | `GET` | `/api/agent/{agentID}` | `agent.get` | Keep | Request, response, and not-found error accepted as-is. |
|
||||
| [x] 010 | `GET` | `/api/plugin` | `plugin.list` | Keep | Request and response accepted as-is. |
|
||||
| [x] 012 | `POST` | `/api/plugin/check` | `plugin.check` | Keep | Request and response accepted as-is. |
|
||||
| [x] 013 | `POST` | `/api/plugin/update` | `plugin.update` | Keep | Request and errors accepted as-is. |
|
||||
| [x] 014 | `GET` | `/api/model` | `model.list` | Keep | Request and response accepted as-is. |
|
||||
| [x] 015 | `GET` | `/api/model/default` | `model.default` | Keep | Request and nullable response accepted as-is. |
|
||||
| [x] 016 | `GET` | `/api/provider` | `provider.list` | Keep | Request and response accepted as-is. |
|
||||
| [x] 017 | `GET` | `/api/provider/{providerID}` | `provider.get` | Keep | Request, response, and not-found error accepted as-is. |
|
||||
| [x] 018 | `GET` | `/api/command` | `command.list` | Keep | Request and response accepted as-is. |
|
||||
| [x] 019 | `GET` | `/api/skill` | `skill.list` | Keep | Renamed `location` to `path`; removed the skill-specific `slash` flag and slash-command behavior. |
|
||||
| [x] 020 | `GET` | `/api/reference` | `reference.list` | Keep | Removed duplicate `description` and `hidden` fields from nested `source`. |
|
||||
| [x] 021 | `GET` | `/api/config` | `config.get` | Keep | Compatibility entries removed; response now contains only documents and OpenCode directories. |
|
||||
| [x] 022 | `GET` | `/api/config/preferences` | `config.preferences` | Remove | Redundant special projection of global config. |
|
||||
| [x] 023 | `PATCH` | `/api/config/preferences` | `config.updatePreferences` | Remove | Redundant field-specific config mutation API. |
|
||||
| [x] 024 | `GET` | `/api/config/shell` | `config.shells` | Keep | Required by the server Terminal shell setting. |
|
||||
| [x] 024a | `PATCH` | `/api/experimental/config` | `experimental.config.update` | Change | Experimental global config mutation; initially accepts only `shell`. |
|
||||
|
||||
## Group 3: Credentials, integrations, MCP, and web search
|
||||
|
||||
**Endpoints:** 22
|
||||
|
||||
| Done | Method | Path | Operation ID | Decision | Notes |
|
||||
|---|---|---|---|---|---|
|
||||
| [x] 025 | `GET` | `/api/integration` | `integration.list` | Keep | Full integration inventory is consumed by authentication and integration-selection clients. |
|
||||
| [x] 026 | `GET` | `/api/integration/{integrationID}` | `integration.get` | Change | Missing integration now returns typed `404` instead of optional data. |
|
||||
| [x] 027 | `POST` | `/api/experimental/integration/wellknown` | `experimental.integration.wellknown.add` | Experimental-only | Retained outside the stable API commitment. |
|
||||
| [x] 028 | `POST` | `/api/integration/{integrationID}/connect/key` | `integration.connect.key` | Change | Missing integration returns typed `404`; key form answers retained. |
|
||||
| [x] 029 | `POST` | `/api/integration/{integrationID}/connect/oauth` | `integration.oauth.connect` | Keep | OAuth connection start contract retained. |
|
||||
| [x] 030 | `GET` | `/api/integration/{integrationID}/connect/oauth/{attemptID}` | `integration.oauth.status` | Change | Missing integration or OAuth attempt returns typed `404`. |
|
||||
| [x] 031 | `DELETE` | `/api/integration/{integrationID}/connect/oauth/{attemptID}` | `integration.oauth.cancel` | Keep | Idempotent cancellation remains a no-op for unavailable or terminal attempts. |
|
||||
| [x] 032 | `POST` | `/api/integration/{integrationID}/connect/oauth/{attemptID}/complete` | `integration.oauth.complete` | Change | Missing integration or OAuth attempt returns typed `404`; code remains mode-dependent. |
|
||||
| [x] 033 | `POST` | `/api/integration/{integrationID}/connect/command` | `integration.command.connect` | Change | Missing integration or command method returns typed `404`. |
|
||||
| [x] 034 | `GET` | `/api/integration/{integrationID}/connect/command/{attemptID}` | `integration.command.status` | Change | Missing integration or command attempt returns typed `404`. |
|
||||
| [x] 035 | `DELETE` | `/api/integration/{integrationID}/connect/command/{attemptID}` | `integration.command.cancel` | Keep | Idempotent cancellation remains a no-op for unavailable or terminal attempts. |
|
||||
| [x] 036 | `GET` | `/api/mcp` | `mcp.list` | Keep | MCP inventory and connection status retained. |
|
||||
| [x] 037 | `PUT` | `/api/experimental/mcp/{server}` | `experimental.mcp.add` | Experimental-only | Runtime-only MCP override; does not persist configuration. |
|
||||
| [x] 038 | `DELETE` | `/api/experimental/mcp/{server}` | `experimental.mcp.remove` | Experimental-only | Runtime removal override; missing server returns `404`. |
|
||||
| [x] 039 | `POST` | `/api/experimental/mcp/{server}/connect` | `experimental.mcp.connect` | Experimental-only | Runtime connection override retained outside the stable API. |
|
||||
| [x] 040 | `POST` | `/api/experimental/mcp/{server}/disconnect` | `experimental.mcp.disconnect` | Experimental-only | Runtime disconnection override retained outside the stable API. |
|
||||
| [x] 041 | `GET` | `/api/mcp/resource` | `mcp.resource.catalog` | Keep | Reviewed separately by coworker. |
|
||||
| [x] 042 | `PATCH` | `/api/credential/{credentialID}` | `credential.update` | Change | Removed redundant location query; credentials and events are global. |
|
||||
| [x] 043 | `DELETE` | `/api/credential/{credentialID}` | `credential.remove` | Change | Removed redundant location query; credentials and events are global. |
|
||||
| [x] 044 | `POST` | `/api/credential/{credentialID}/activate` | `credential.activate` | Change | Removed redundant location query; credentials and events are global. |
|
||||
| [x] 045 | `GET` | `/api/websearch/provider` | `websearch.providers` | Keep | Provider availability remains location-scoped; singular resource path retained. |
|
||||
| [x] 046 | `POST` | `/api/websearch` | `websearch.query` | Keep | Unknown provider remains an invalid request; published time documented as Unix epoch milliseconds. |
|
||||
|
||||
## Group 4: Session lifecycle
|
||||
|
||||
**Endpoints:** 12
|
||||
|
||||
| Done | Method | Path | Operation ID | Decision | Notes |
|
||||
|---|---|---|---|---|---|
|
||||
| [x] 047 | `GET` | `/api/session` | `session.list` | Keep | Existing filtering, ordering, and cursor contract retained for now. |
|
||||
| [x] 048 | `POST` | `/api/session` | `session.create` | Keep | Existing creation contract retained; model reference includes optional variant. |
|
||||
| [x] 049 | `GET` | `/api/experimental/session/stats` | `experimental.session.stats` | Experimental-only | Session analytics retained outside the stable API commitment. |
|
||||
| [x] 050 | `GET` | `/api/session/active` | `session.active` | Keep | Status record retained for future active-state expansion. |
|
||||
| [x] 051 | `GET` | `/api/session/{sessionID}` | `session.get` | Keep | Specific session read and typed `404` retained. |
|
||||
| [x] 052 | `DELETE` | `/api/session/{sessionID}` | `session.remove` | Keep | Session and child deletion with typed `404` retained. |
|
||||
| [x] 053 | `POST` | `/api/session/{sessionID}/fork` | `session.fork` | Change | Request now accepts optional branded `before` message ID; omission copies full history. |
|
||||
| [x] 054 | `POST` | `/api/session/{sessionID}/agent` | `session.switchAgent` | Keep | Subsequent-execution agent selection retained. |
|
||||
| [x] 055 | `POST` | `/api/session/{sessionID}/model` | `session.switchModel` | Keep | Subsequent-execution model and optional variant selection retained. |
|
||||
| [x] 056 | `PATCH` | `/api/session/{sessionID}` | `session.update` | Change | General session patch updates title and permissions; rules emit `session.permissions`. |
|
||||
| [x] 057 | `POST` | `/api/session/{sessionID}/move` | `session.move` | Change | Removed inaccurate local-change transfer claim; delivery behavior retained. |
|
||||
| [x] 058 | `POST` | `/api/session/{sessionID}/background` | `session.background` | Keep | Backgroundable foreground tools transition to background observation; idle requests remain no-ops. |
|
||||
|
||||
## Group 5: Session execution and inputs
|
||||
|
||||
**Endpoints:** 11
|
||||
|
||||
| Done | Method | Path | Operation ID | Decision | Notes |
|
||||
|---|---|---|---|---|---|
|
||||
| [x] 059 | `POST` | `/api/session/{sessionID}/prompt` | `session.prompt` | Keep | Durable admission, delivery mode, and admit-only resume control retained. |
|
||||
| [x] 060 | `POST` | `/api/session/{sessionID}/command` | `session.command` | Change | Renamed request field from `command` to `name`; `204` retained. |
|
||||
| [x] 061 | `POST` | `/api/experimental/session/{sessionID}/skill` | `experimental.session.skill` | Experimental-only | Skill ID is now the `id` field; standalone activation remains experimental. |
|
||||
| [x] 062 | `POST` | `/api/session/{sessionID}/synthetic` | `session.synthetic` | Keep | Durable synthetic admission and delivery controls retained. |
|
||||
| [x] 063 | `POST` | `/api/session/{sessionID}/shell` | `session.shell` | Change | Caller ID is now the optimistic shell message ID; server derives its event ID. |
|
||||
| [x] 064 | `POST` | `/api/session/{sessionID}/compact` | `session.compact` | Keep | Durable compaction admission and delivery controls retained. |
|
||||
| [x] 065 | `POST` | `/api/experimental/session/{sessionID}/wait` | `experimental.session.wait` | Experimental-only | Race-free idle barrier retained outside the stable API. |
|
||||
| [x] 066 | `POST` | `/api/session/{sessionID}/generate` | `session.generate` | Keep | Transient generation from session context retained. |
|
||||
| [x] 067 | `POST` | `/api/session/{sessionID}/interrupt` | `session.interrupt` | Change | Renamed `continue` to `resume` across public and internal interruption APIs. |
|
||||
| [x] 068 | `PUT` | `/api/session/{sessionID}/environment` | `session.environment` | Keep | Process-local environment replacement retained in the stable API. |
|
||||
| [x] 069 | `POST` | `/api/session/{sessionID}/view` | `session.view` | Change | Idle watermark now uses the standard epoch-millisecond timestamp schema. |
|
||||
|
||||
## Group 6: Session history and recovery
|
||||
|
||||
**Endpoints:** 13
|
||||
|
||||
| Done | Method | Path | Operation ID | Decision | Notes |
|
||||
|---|---|---|---|---|---|
|
||||
| [x] 070 | `POST` | `/api/experimental/session/import` | `experimental.session.import` | Experimental-only | Existing projected transcript import contract retained outside the stable API. |
|
||||
| [x] 071 | `GET` | `/api/experimental/session/{sessionID}/export` | `experimental.session.export` | Experimental-only | Existing projected transcript export contract retained outside the stable API. |
|
||||
| [x] 072 | `POST` | `/api/session/{sessionID}/revert/stage` | `session.revert.stage` | Keep | Existing staged history and optional file restoration behavior retained. |
|
||||
| [x] 073 | `DELETE` | `/api/session/{sessionID}/revert` | `session.revert.clear` | Change | Clearing staged revert now deletes the session revert resource. |
|
||||
| [x] 074 | `POST` | `/api/session/{sessionID}/revert/commit` | `session.revert.commit` | Keep | Explicit staged-revert commit action retained. |
|
||||
| [x] 075 | `GET` | `/api/session/{sessionID}/context` | `session.context` | Keep | Active model-context projection retained. |
|
||||
| [x] 076 | `GET` | `/api/session/{sessionID}/diff` | `session.diff` | Keep | Turn-range structured diff contract retained. |
|
||||
| [x] 077 | `GET` | `/api/experimental/session/{sessionID}/instructions/entries` | `experimental.session.instructions.entry.list` | Experimental-only | API-managed durable context entries retained outside the stable API. |
|
||||
| [x] 078 | `PUT` | `/api/experimental/session/{sessionID}/instructions/entries/{key}` | `experimental.session.instructions.entry.put` | Experimental-only | API-managed durable context entries retained outside the stable API. |
|
||||
| [x] 079 | `DELETE` | `/api/experimental/session/{sessionID}/instructions/entries/{key}` | `experimental.session.instructions.entry.remove` | Experimental-only | API-managed durable context entries retained outside the stable API. |
|
||||
| [x] 080 | `GET` | `/api/experimental/session/{sessionID}/log` | `session.log` | Experimental-only | Retained outside the stable API commitment. |
|
||||
| [x] 081 | `GET` | `/api/session/{sessionID}/message/{messageID}` | `session.message.get` | Change | Normalized specific-message operation ID. |
|
||||
| [x] 082 | `GET` | `/api/session/{sessionID}/message` | `session.message.list` | Change | Normalized session-scoped message-list operation ID. |
|
||||
|
||||
## Group 7: Inbox, permissions, and forms
|
||||
|
||||
**Endpoints:** 19
|
||||
|
||||
| Done | Method | Path | Operation ID | Decision | Notes |
|
||||
|---|---|---|---|---|---|
|
||||
| [x] 083 | `GET` | `/api/session/{sessionID}/inbox` | `session.inbox.list` | Change | Inbox timestamps now use the standard nested `time.created` shape. |
|
||||
| [x] 084 | `DELETE` | `/api/session/{sessionID}/inbox/{inboxID}` | `session.inbox.cancel` | Change | Cancellation is idempotent and returns `204` when the session exists. |
|
||||
| [x] 085 | `PATCH` | `/api/session/{sessionID}/inbox/{inboxID}` | `session.inbox.update` | Change | Consolidated delivery mutation with `delivery: "steer" | "queue"`. |
|
||||
| [x] 086 | — | — | — | Remove | Replaced by `session.inbox.update`. |
|
||||
| [x] 087 | `GET` | `/api/form` | `form.list` | Change | Removed redundant `request` path and operation namespace. |
|
||||
| [x] 088 | `GET` | `/api/session/{sessionID}/form` | `session.form.list` | Keep | Pending session form list retained with temporary MCP sentinel compatibility. |
|
||||
| [x] 089 | `POST` | `/api/session/{sessionID}/form` | `session.form.create` | Keep | External form creation and temporary MCP sentinel ownership retained. |
|
||||
| [x] 090 | `GET` | `/api/session/{sessionID}/form/{formID}` | `session.form.get` | Change | Form definition and lifecycle state are now returned together. |
|
||||
| [x] 091 | — | — | — | Remove | State is included by `session.form.get`. |
|
||||
| [x] 092 | `POST` | `/api/session/{sessionID}/form/{formID}/reply` | `session.form.reply` | Keep | One-shot validated form reply retained. |
|
||||
| [x] 093 | `DELETE` | `/api/session/{sessionID}/form/{formID}` | `session.form.cancel` | Change | Form cancellation now deletes the pending form resource. |
|
||||
| [x] 094 | `GET` | `/api/permission/request` | `permission.request.list` | Keep | Pending-request namespace retained alongside saved permissions. |
|
||||
| [x] 095 | `GET` | `/api/permission/saved` | `permission.saved.list` | Change | Added persisted creation and update timestamps under `time`. |
|
||||
| [x] 096 | `DELETE` | `/api/permission/saved/{id}` | `permission.saved.remove` | Keep | Idempotent saved-permission deletion retained. |
|
||||
| [x] 097 | `POST` | `/api/session/{sessionID}/permission` | `session.permission.create` | Keep | Non-blocking permission evaluation and pending-request creation retained. |
|
||||
| [x] 098 | `GET` | `/api/session/{sessionID}/permission` | `session.permission.list` | Keep | Pending session permission list retained. |
|
||||
| [x] 099 | `GET` | `/api/session/{sessionID}/permission/{requestID}` | `session.permission.get` | Keep | Specific pending permission read with ownership validation retained. |
|
||||
| [x] 100 | `POST` | `/api/session/{sessionID}/permission/{requestID}/reply` | `session.permission.reply` | Change | Renamed request field from `reply` to `decision`. |
|
||||
| [x] 101 | — | — | — | Remove | Permission rules are updated through `session.update`. |
|
||||
|
||||
## Group 8: Filesystem, worktrees, and VCS
|
||||
|
||||
**Endpoints:** 12
|
||||
|
||||
| Done | Method | Path | Operation ID | Decision | Notes |
|
||||
|---|---|---|---|---|---|
|
||||
| [x] 102 | `GET` | `/api/fs/read/*` | `fs.read` | Keep | Relative wildcard file reads and raw byte responses retained. |
|
||||
| [x] 103 | `GET` | `/api/fs/list` | `fs.list` | Keep | Existing path scope and minimal entry metadata retained. |
|
||||
| [x] 104 | `GET` | `/api/fs/find` | `fs.find` | Keep | Existing ranked filesystem search retained. |
|
||||
| [x] 105 | `GET` | `/api/worktree` | `worktree.list` | Keep | Reviewed separately by coworker. |
|
||||
| [x] 106 | `POST` | `/api/worktree` | `worktree.create` | Keep | Reviewed separately by coworker. |
|
||||
| [x] 107 | `DELETE` | `/api/worktree` | `worktree.remove` | Keep | Reviewed separately by coworker. |
|
||||
| [x] 108 | `POST` | `/api/worktree/refresh` | `worktree.refresh` | Keep | Reviewed separately by coworker. |
|
||||
| [x] 109 | `GET` | `/api/vcs` | `vcs.get` | Change | Preserved branch nesting and added selected VCS provider ID. |
|
||||
| [x] 110 | `GET` | `/api/vcs/base` | `vcs.base` | Keep | Review-base inference and nullable unavailable state retained. |
|
||||
| [x] 111 | `GET` | `/api/vcs/status` | `vcs.status` | Keep | Existing working-copy status shape retained for now. |
|
||||
| [x] 112 | `GET` | `/api/vcs/branch` | `vcs.branch.list` | Change | Singular collection path and normalized operation ID. |
|
||||
| [x] 113 | `GET` | `/api/vcs/diff` | `vcs.diff` | Keep | Existing working, branch, and committed comparison modes retained. |
|
||||
|
||||
## Group 9: PTYs, persistent terminals, and shells
|
||||
|
||||
**Endpoints:** 24
|
||||
|
||||
| Done | Method | Path | Operation ID | Decision | Notes |
|
||||
|---|---|---|---|---|---|
|
||||
| [x] 114 | `GET` | `/api/pty` | `pty.list` | Keep | PTY endpoints reviewed together and retained. |
|
||||
| [x] 115 | `POST` | `/api/pty` | `pty.create` | Keep | PTY endpoints reviewed together and retained. |
|
||||
| [x] 116 | `GET` | `/api/pty/{ptyID}` | `pty.get` | Keep | PTY endpoints reviewed together and retained. |
|
||||
| [x] 117 | `PUT` | `/api/pty/{ptyID}` | `pty.update` | Keep | PTY endpoints reviewed together and retained. |
|
||||
| [x] 118 | `DELETE` | `/api/pty/{ptyID}` | `pty.remove` | Keep | PTY endpoints reviewed together and retained. |
|
||||
| [x] 119 | `POST` | `/api/pty/{ptyID}/connect-token` | `pty.connect.token` | Keep | PTY endpoints reviewed together and retained. |
|
||||
| [x] 120 | `GET` | `/api/pty/{ptyID}/connect` | `pty.connect` | Keep | PTY endpoints reviewed together and retained. |
|
||||
| [x] 121 | `GET` | `/api/experimental/session/{sessionID}/terminal/read` | `server.experimental.persistentPty.read` | Experimental-only | Retained outside the stable API commitment. |
|
||||
| [x] 122 | `GET` | `/api/experimental/session/{sessionID}/terminal` | `server.experimental.persistentPty.list` | Experimental-only | Retained outside the stable API commitment. |
|
||||
| [x] 123 | `POST` | `/api/experimental/session/{sessionID}/terminal` | `server.experimental.persistentPty.create` | Experimental-only | Retained outside the stable API commitment. |
|
||||
| [x] 124 | `POST` | `/api/experimental/persistent-pty/shutdown` | `server.experimental.persistentPty.shutdown` | Experimental-only | Retained outside the stable API commitment. |
|
||||
| [x] 125 | `POST` | `/api/experimental/persistent-pty/handoff` | `server.experimental.persistentPty.handoff` | Experimental-only | Retained outside the stable API commitment. |
|
||||
| [x] 126 | `GET` | `/api/experimental/persistent-pty/{ptyID}` | `server.experimental.persistentPty.get` | Experimental-only | Retained outside the stable API commitment. |
|
||||
| [x] 127 | `PUT` | `/api/experimental/persistent-pty/{ptyID}` | `server.experimental.persistentPty.update` | Experimental-only | Retained outside the stable API commitment. |
|
||||
| [x] 128 | `DELETE` | `/api/experimental/persistent-pty/{ptyID}` | `server.experimental.persistentPty.remove` | Experimental-only | Retained outside the stable API commitment. |
|
||||
| [x] 129 | `GET` | `/api/experimental/persistent-pty/{ptyID}/snapshot` | `server.experimental.persistentPty.snapshot` | Experimental-only | Retained outside the stable API commitment. |
|
||||
| [x] 130 | `POST` | `/api/experimental/persistent-pty/{ptyID}/connect-token` | `server.experimental.persistentPty.connectToken` | Experimental-only | Retained outside the stable API commitment. |
|
||||
| [x] 131 | `GET` | `/api/experimental/persistent-pty/{ptyID}/connect` | `persistentPty.connect` | Experimental-only | Retained outside the stable API commitment. |
|
||||
| [x] 132 | `GET` | `/api/shell` | `shell.list` | Change | Stable shell inventory retained; numeric timestamps documented as epoch milliseconds. |
|
||||
| [x] 133 | `POST` | `/api/shell` | `shell.create` | Change | Timeout is optional and defaults to zero; caller metadata retained. |
|
||||
| [x] 134 | `GET` | `/api/shell/{id}` | `shell.get` | Keep | Specific running or retained shell read retained. |
|
||||
| [x] 135 | `DELETE` | `/api/shell/{id}` | `shell.remove` | Change | Shell deletion is idempotent and returns `204` when already absent. |
|
||||
| [x] 136 | — | — | — | Remove | Timeout mutation remains an internal Core shell operation. |
|
||||
| [x] 137 | `GET` | `/api/shell/{id}/output` | `shell.output` | Keep | Existing byte-cursor text output paging retained. |
|
||||
|
||||
## Group 10: Events, RPC, and experimental operations
|
||||
|
||||
**Endpoints:** 6
|
||||
|
||||
| Done | Method | Path | Operation ID | Decision | Notes |
|
||||
|---|---|---|---|---|---|
|
||||
| [x] 138 | `POST` | `/api/experimental/generate` | `experimental.generate.text` | Experimental-only | Stateless generation retained alongside session generation. |
|
||||
| [x] 139 | `POST` | `/api/rpc/{rpcID}/{method}` | `rpc.call` | Keep | Generic typed-error plugin RPC transport retained. |
|
||||
| [x] 140 | `GET` | `/api/event` | `event.subscribe` | Keep | Unified native and dynamic plugin event stream retained. |
|
||||
| [x] 141 | `GET` | `/api/debug/location` | `debug.location.list` | Keep | Loaded-location debug inventory retained. |
|
||||
| [x] 142 | `DELETE` | `/api/debug/location` | `debug.location.evict` | Keep | Idempotent loaded-location eviction retained. |
|
||||
| [x] 143 | `GET` | `/api/experimental/migration/v1` | `experimental.migration.v1.status` | Experimental-only | Retained outside the stable API commitment. |
|
||||
|
Before Width: | Height: | Size: 16 KiB |
|
Before Width: | Height: | Size: 17 KiB |
|
Before Width: | Height: | Size: 40 KiB |
|
Before Width: | Height: | Size: 62 KiB |
@@ -2,7 +2,7 @@
|
||||
exact = true
|
||||
# Only install newly resolved package versions published at least 3 days ago.
|
||||
minimumReleaseAge = 259200
|
||||
minimumReleaseAgeExcludes = ["@ai-sdk/amazon-bedrock", "@ai-sdk/anthropic", "@brendonovich/vite-plugin-opencode", "@opencode/sdk", "@opencode-ai/pty", "@opencode-ai/pty-darwin-arm64", "@opencode-ai/pty-darwin-x64", "@opencode-ai/pty-linux-arm64-gnu", "@opencode-ai/pty-linux-arm64-musl", "@opencode-ai/pty-linux-x64-gnu", "@opencode-ai/pty-linux-x64-musl", "@opentui/core", "@opentui/core-darwin-arm64", "@opentui/core-darwin-x64", "@opentui/core-linux-arm64", "@opentui/core-linux-arm64-musl", "@opentui/core-linux-x64", "@opentui/core-linux-x64-musl", "@opentui/core-win32-arm64", "@opentui/core-win32-x64", "@opentui/keymap", "@opentui/solid", "opentui-spinner", "gitlab-ai-provider", "opencode-gitlab-auth", "@ff-labs/fff-node", "@ff-labs/fff-bun", "@ff-labs/fff-bin-darwin-arm64", "@ff-labs/fff-bin-darwin-x64", "@ff-labs/fff-bin-linux-arm64-gnu", "@ff-labs/fff-bin-linux-arm64-musl", "@ff-labs/fff-bin-linux-x64-gnu", "@ff-labs/fff-bin-linux-x64-musl", "@ff-labs/fff-bin-win32-arm64", "@ff-labs/fff-bin-win32-x64", "@pierre/diffs", "@pierre/theming", "app-builder-lib", "dmg-builder", "electron", "electron-builder", "electron-publish", "blume", "mermaid"]
|
||||
minimumReleaseAgeExcludes = ["@ai-sdk/amazon-bedrock", "@ai-sdk/anthropic", "@opentui/core", "@opentui/core-darwin-arm64", "@opentui/core-darwin-x64", "@opentui/core-linux-arm64", "@opentui/core-linux-arm64-musl", "@opentui/core-linux-x64", "@opentui/core-linux-x64-musl", "@opentui/core-win32-arm64", "@opentui/core-win32-x64", "@opentui/keymap", "@opentui/solid", "opentui-spinner", "gitlab-ai-provider", "opencode-gitlab-auth", "@ff-labs/fff-node", "@ff-labs/fff-bun", "@ff-labs/fff-bin-darwin-arm64", "@ff-labs/fff-bin-darwin-x64", "@ff-labs/fff-bin-linux-arm64-gnu", "@ff-labs/fff-bin-linux-arm64-musl", "@ff-labs/fff-bin-linux-x64-gnu", "@ff-labs/fff-bin-linux-x64-musl", "@ff-labs/fff-bin-win32-arm64", "@ff-labs/fff-bin-win32-x64", "@pierre/diffs", "@pierre/theming", "app-builder-lib", "dmg-builder", "electron-builder", "electron-publish"]
|
||||
|
||||
[test]
|
||||
root = "./do-not-run-tests-from-root"
|
||||
@@ -1,37 +0,0 @@
|
||||
// BEGIN: baseline — first-line change
|
||||
type ReviewState = "pending" | "ready" | "failed"
|
||||
type ReviewProps = { title: string; count: number; state: ReviewState }
|
||||
|
||||
export const heading = "Review the old checkout experience"
|
||||
export const count = 8
|
||||
export const obsolete = "delete this isolated row"
|
||||
export const anchor = "unchanged between deletion and addition"
|
||||
|
||||
// Contiguous span versus several separated small spans.
|
||||
export const contiguous = "Keep the original checkout message readable"
|
||||
export const scattered = "alpha: old; beta: cold; gamma: slow"
|
||||
export const oneCharacter = "item-7"
|
||||
export const atStart = "before middle remains ending remains"
|
||||
export const atMiddle = "start remains before ending remains"
|
||||
export const atEnd = "start remains middle remains before"
|
||||
export const punctuation = { label: "Ready", enabled: true }
|
||||
export const spaces = "one two three"
|
||||
export const indent = "indent-only change"
|
||||
export const trailing = "trailing-space-only change"
|
||||
|
||||
// Syntax competition: comment, keyword, type, boolean, string, number.
|
||||
export function ReviewCard(props: ReviewProps) {
|
||||
const status: ReviewState = "pending"
|
||||
const enabled = false
|
||||
const retries = 3
|
||||
// Keep this quiet explanation readable on a tinted row.
|
||||
return <section aria-label="Old review" data-state={status}>
|
||||
<h2>{props.title}</h2>
|
||||
<span className="text-muted">{props.count} old items</span>
|
||||
<button disabled={!enabled}>Continue checkout</button>
|
||||
</section>
|
||||
}
|
||||
|
||||
export const longLine = "Start unchanged | The old checkout flow waits for manual confirmation before showing the receipt | Middle unchanged with punctuation: brackets [one, two], braces {three}, quotes 'four', slash /five/ | The old final instruction asks the reviewer to close the window | End unchanged"
|
||||
|
||||
// END: baseline — last-line change
|
||||
@@ -1,66 +0,0 @@
|
||||
// BEGIN baseline
|
||||
export const first = "old"
|
||||
// quiet context A01
|
||||
// quiet context A02
|
||||
// quiet context A03
|
||||
// quiet context A04
|
||||
// quiet context A05
|
||||
// quiet context A06
|
||||
// quiet context A07
|
||||
// quiet context A08
|
||||
// quiet context A09
|
||||
// quiet context A10
|
||||
// quiet context A11
|
||||
// quiet context A12
|
||||
// quiet context A13
|
||||
// quiet context A14
|
||||
// quiet context A15
|
||||
// quiet context A16
|
||||
// quiet context A17
|
||||
// quiet context A18
|
||||
// quiet context A19
|
||||
// quiet context A20
|
||||
export const second = "old"
|
||||
// quiet context B01
|
||||
// quiet context B02
|
||||
// quiet context B03
|
||||
// quiet context B04
|
||||
// quiet context B05
|
||||
// quiet context B06
|
||||
// quiet context B07
|
||||
// quiet context B08
|
||||
// quiet context B09
|
||||
// quiet context B10
|
||||
// quiet context B11
|
||||
// quiet context B12
|
||||
// quiet context B13
|
||||
// quiet context B14
|
||||
// quiet context B15
|
||||
// quiet context B16
|
||||
// quiet context B17
|
||||
// quiet context B18
|
||||
// quiet context B19
|
||||
// quiet context B20
|
||||
export const third = "old"
|
||||
// quiet context C01
|
||||
// quiet context C02
|
||||
// quiet context C03
|
||||
// quiet context C04
|
||||
// quiet context C05
|
||||
// quiet context C06
|
||||
// quiet context C07
|
||||
// quiet context C08
|
||||
// quiet context C09
|
||||
// quiet context C10
|
||||
// quiet context C11
|
||||
// quiet context C12
|
||||
// quiet context C13
|
||||
// quiet context C14
|
||||
// quiet context C15
|
||||
// quiet context C16
|
||||
// quiet context C17
|
||||
// quiet context C18
|
||||
// quiet context C19
|
||||
// quiet context C20
|
||||
export const fourth = "old"
|
||||
// END baseline
|
||||
@@ -1,21 +0,0 @@
|
||||
# Before: plain-text review
|
||||
|
||||
Unchanged prose should remain quiet, not compete with changed rows.
|
||||
Replace one contiguous phrase: the original checkout experience stays here.
|
||||
Several little edits: red apple, cold tea, slow train.
|
||||
One character: ticket 7.
|
||||
before — the middle and the end remain unchanged.
|
||||
The start remains — before — the end remains.
|
||||
The start and the middle remain — before
|
||||
Punctuation only: hello, world!
|
||||
Whitespace only: one two three
|
||||
Trailing whitespace only.
|
||||
Delete this sentence without a replacement.
|
||||
|
||||
This unchanged sentence separates a deletion from an addition.
|
||||
|
||||
Long prose: The original checkout flow asks the reader to review the old confirmation message before proceeding through the receipt screen, while the unchanged middle of this deliberately long paragraph checks whether horizontal scrolling or line wrapping keeps small inline edits visible at a narrow viewport; the final old phrase appears near the far right edge.
|
||||
|
||||
**Syntax-like Markdown** competes with `inline code`, [links](https://example.com/old), and _emphasis_.
|
||||
|
||||
Final sentence before.
|
||||
@@ -1,63 +0,0 @@
|
||||
// Large file: unchanged prefix and suffix surround a dense block.
|
||||
export const records = [
|
||||
{ id: "row-001", state: "pending", count: 1 },
|
||||
{ id: "row-002", state: "pending", count: 2 },
|
||||
{ id: "row-003", state: "pending", count: 3 },
|
||||
{ id: "row-004", state: "pending", count: 4 },
|
||||
{ id: "row-005", state: "pending", count: 5 },
|
||||
{ id: "row-006", state: "pending", count: 6 },
|
||||
{ id: "row-007", state: "pending", count: 7 },
|
||||
{ id: "row-008", state: "pending", count: 8 },
|
||||
{ id: "row-009", state: "pending", count: 9 },
|
||||
{ id: "row-010", state: "pending", count: 10 },
|
||||
{ id: "row-011", state: "pending", count: 11 },
|
||||
{ id: "row-012", state: "pending", count: 12 },
|
||||
{ id: "row-013", state: "pending", count: 13 },
|
||||
{ id: "row-014", state: "pending", count: 14 },
|
||||
{ id: "row-015", state: "pending", count: 15 },
|
||||
{ id: "row-016", state: "pending", count: 16 },
|
||||
{ id: "row-017", state: "pending", count: 17 },
|
||||
{ id: "row-018", state: "pending", count: 18 },
|
||||
{ id: "row-019", state: "pending", count: 19 },
|
||||
{ id: "row-020", state: "pending", count: 20 },
|
||||
{ id: "row-021", state: "pending", count: 21 },
|
||||
{ id: "row-022", state: "pending", count: 22 },
|
||||
{ id: "row-023", state: "pending", count: 23 },
|
||||
{ id: "row-024", state: "pending", count: 24 },
|
||||
{ id: "row-025", state: "pending", count: 25 },
|
||||
{ id: "row-026", state: "pending", count: 26 },
|
||||
{ id: "row-027", state: "pending", count: 27 },
|
||||
{ id: "row-028", state: "pending", count: 28 },
|
||||
{ id: "row-029", state: "pending", count: 29 },
|
||||
{ id: "row-030", state: "pending", count: 30 },
|
||||
{ id: "row-031", state: "pending", count: 31 },
|
||||
{ id: "row-032", state: "pending", count: 32 },
|
||||
{ id: "row-033", state: "pending", count: 33 },
|
||||
{ id: "row-034", state: "pending", count: 34 },
|
||||
{ id: "row-035", state: "pending", count: 35 },
|
||||
{ id: "row-036", state: "pending", count: 36 },
|
||||
{ id: "row-037", state: "pending", count: 37 },
|
||||
{ id: "row-038", state: "pending", count: 38 },
|
||||
{ id: "row-039", state: "pending", count: 39 },
|
||||
{ id: "row-040", state: "pending", count: 40 },
|
||||
{ id: "row-041", state: "pending", count: 41 },
|
||||
{ id: "row-042", state: "pending", count: 42 },
|
||||
{ id: "row-043", state: "pending", count: 43 },
|
||||
{ id: "row-044", state: "pending", count: 44 },
|
||||
{ id: "row-045", state: "pending", count: 45 },
|
||||
{ id: "row-046", state: "pending", count: 46 },
|
||||
{ id: "row-047", state: "pending", count: 47 },
|
||||
{ id: "row-048", state: "pending", count: 48 },
|
||||
{ id: "row-049", state: "pending", count: 49 },
|
||||
{ id: "row-050", state: "pending", count: 50 },
|
||||
{ id: "row-051", state: "pending", count: 51 },
|
||||
{ id: "row-052", state: "pending", count: 52 },
|
||||
{ id: "row-053", state: "pending", count: 53 },
|
||||
{ id: "row-054", state: "pending", count: 54 },
|
||||
{ id: "row-055", state: "pending", count: 55 },
|
||||
{ id: "row-056", state: "pending", count: 56 },
|
||||
{ id: "row-057", state: "pending", count: 57 },
|
||||
{ id: "row-058", state: "pending", count: 58 },
|
||||
{ id: "row-059", state: "pending", count: 59 },
|
||||
{ id: "row-060", state: "pending", count: 60 },
|
||||
]
|
||||
@@ -1,4 +0,0 @@
|
||||
This entire tracked file will be deleted after the baseline commit.
|
||||
Only deletion tint should be present.
|
||||
No syntax colors should compete with this text.
|
||||
Last deleted row.
|
||||
@@ -1,41 +0,0 @@
|
||||
/* Baseline stylesheet: colors, units, selectors, at-rules. */
|
||||
:root {
|
||||
--brand: #3b5cf6;
|
||||
--radius: 6px;
|
||||
--gap: 8px;
|
||||
}
|
||||
|
||||
.card {
|
||||
display: flex;
|
||||
gap: var(--gap);
|
||||
padding: 12px 16px;
|
||||
border: 1px solid rgba(0, 0, 0, 0.1);
|
||||
border-radius: var(--radius);
|
||||
background: #ffffff;
|
||||
color: #161616;
|
||||
}
|
||||
|
||||
.card:hover {
|
||||
background: #fafafa;
|
||||
}
|
||||
|
||||
.card[data-state="error"] {
|
||||
color: #b82d35;
|
||||
border-color: #f2bbb7;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.card {
|
||||
flex-direction: column;
|
||||
padding: 8px;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes fade-in {
|
||||
from {
|
||||
opacity: 0;
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
@@ -1,21 +0,0 @@
|
||||
{
|
||||
"name": "diff-color-fixture",
|
||||
"version": "1.0.0",
|
||||
"private": true,
|
||||
"settings": {
|
||||
"theme": "light",
|
||||
"fontSize": 13,
|
||||
"lineHeight": 20,
|
||||
"wrap": false,
|
||||
"tabs": ["review", "files", "terminal"]
|
||||
},
|
||||
"features": {
|
||||
"inlineHighlights": true,
|
||||
"foldUnchanged": true,
|
||||
"splitView": false
|
||||
},
|
||||
"limits": {
|
||||
"maxFiles": 100,
|
||||
"maxLineLength": 1000
|
||||
}
|
||||
}
|
||||
@@ -1,38 +0,0 @@
|
||||
"""Baseline Python service: decorators, f-strings, indentation, comments."""
|
||||
|
||||
import asyncio
|
||||
from dataclasses import dataclass
|
||||
|
||||
|
||||
@dataclass
|
||||
class Review:
|
||||
id: str
|
||||
title: str
|
||||
approved: bool = False
|
||||
|
||||
|
||||
class ReviewService:
|
||||
def __init__(self, timeout: float = 2.0) -> None:
|
||||
self.timeout = timeout
|
||||
self.cache: dict[str, Review] = {}
|
||||
|
||||
async def load(self, review_id: str) -> Review | None:
|
||||
# Return cached reviews before touching the network.
|
||||
if review_id in self.cache:
|
||||
return self.cache[review_id]
|
||||
await asyncio.sleep(self.timeout)
|
||||
return None
|
||||
|
||||
def summary(self, review: Review) -> str:
|
||||
status = "approved" if review.approved else "pending"
|
||||
return f"Review {review.id}: {review.title} ({status})"
|
||||
|
||||
|
||||
def main() -> None:
|
||||
service = ReviewService()
|
||||
review = Review(id="r-1", title="Checkout")
|
||||
print(service.summary(review))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -1,19 +0,0 @@
|
||||
// Moved and reordered blocks: the same code appears as deletion here and addition elsewhere.
|
||||
export function first() {
|
||||
return "first"
|
||||
}
|
||||
|
||||
export function second() {
|
||||
const values = [1, 2, 3]
|
||||
return values.map((value) => value * 2)
|
||||
}
|
||||
|
||||
export function third() {
|
||||
return "third"
|
||||
}
|
||||
|
||||
export function fourth() {
|
||||
return "fourth"
|
||||
}
|
||||
|
||||
export const order = ["first", "second", "third", "fourth"]
|
||||
@@ -1,21 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
# Baseline shell script: variables, quoting, conditionals.
|
||||
set -euo pipefail
|
||||
|
||||
ENVIRONMENT="${1:-staging}"
|
||||
REGION="us-east-1"
|
||||
RETRIES=3
|
||||
|
||||
log() {
|
||||
echo "[deploy] $*"
|
||||
}
|
||||
|
||||
if [[ "$ENVIRONMENT" == "production" ]]; then
|
||||
log "Deploying to production in $REGION"
|
||||
else
|
||||
log "Deploying to $ENVIRONMENT"
|
||||
fi
|
||||
|
||||
for attempt in $(seq 1 "$RETRIES"); do
|
||||
log "Attempt $attempt of $RETRIES"
|
||||
done
|
||||
@@ -1,19 +0,0 @@
|
||||
name: review
|
||||
on:
|
||||
push:
|
||||
branches: [v2]
|
||||
pull_request:
|
||||
|
||||
jobs:
|
||||
check:
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 10
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Install
|
||||
run: bun install
|
||||
- name: Check
|
||||
run: bun run check
|
||||
env:
|
||||
CI: true
|
||||
LOG_LEVEL: info
|
||||
@@ -1,12 +0,0 @@
|
||||
# Unicode and mixed scripts
|
||||
|
||||
Emoji: ✅ ready, ⚠️ warning, ❌ failed.
|
||||
CJK: 代码审查 完成。
|
||||
Japanese: レビューを開始します。
|
||||
Korean: 변경 사항을 검토합니다.
|
||||
Arabic (RTL): مراجعة التغييرات
|
||||
Hebrew (RTL): סקירת שינויים
|
||||
Accents: café, naïve, résumé.
|
||||
Math: a ≤ b, x ≠ y, ∑ values.
|
||||
Box: ┌─┐ │ │ └─┘
|
||||
Combining: e\u0301 vs é
|
||||
@@ -1,18 +0,0 @@
|
||||
package fixture
|
||||
|
||||
import "fmt"
|
||||
|
||||
// Tab-indented Go: tab-width rendering and tab-only changes.
|
||||
type Review struct {
|
||||
ID string
|
||||
Title string
|
||||
Approved bool
|
||||
}
|
||||
|
||||
func (r Review) Summary() string {
|
||||
status := "pending"
|
||||
if r.Approved {
|
||||
status = "approved"
|
||||
}
|
||||
return fmt.Sprintf("%s: %s (%s)", r.ID, r.Title, status)
|
||||
}
|
||||
@@ -1,3 +0,0 @@
|
||||
This tracked file keeps existing but loses all content.
|
||||
Every row should render as a deletion.
|
||||
The file itself is not deleted.
|
||||
@@ -1,2 +0,0 @@
|
||||
First line
|
||||
Last line without trailing newline
|
||||
@@ -1,14 +0,0 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<title>Review</title>
|
||||
</head>
|
||||
<body class="light">
|
||||
<main id="app" data-state="idle">
|
||||
<h1>Old heading</h1>
|
||||
<button type="button" disabled>Continue</button>
|
||||
<!-- baseline comment -->
|
||||
</main>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,6 +0,0 @@
|
||||
SELECT id, title, status
|
||||
FROM reviews
|
||||
WHERE status = 'pending'
|
||||
AND created_at > NOW() - INTERVAL '7 days'
|
||||
ORDER BY created_at DESC
|
||||
LIMIT 50;
|
||||
@@ -1,15 +0,0 @@
|
||||
# Temporary diff color review
|
||||
|
||||
Local-only visual fixture. Not imported by product code. Do not format these files.
|
||||
|
||||
After the baseline commit, leave all example edits uncommitted.
|
||||
|
||||
- `01-inline.tsx`: adjacent rows, inline spans, one character, syntax, whitespace, long line.
|
||||
- `02-folded.ts`: isolated hunks and multiple collapsed unchanged sections.
|
||||
- `03-prose.md`: plain prose, punctuation, spaces, file boundaries, long prose.
|
||||
- `04-many-rows.ts`: larger dense changed region.
|
||||
- `05-deleted.txt` / `06-added.txt`: whole-file deletion / addition.
|
||||
|
||||
Open this worktree in OpenCode, open Review, select Uncommitted changes, and choose a file.
|
||||
Compare the default OpenCode theme in Light and Dark with Unified and Split layouts.
|
||||
Use a 1440 × 1000 viewport for comparisons and 800 × 1000 for the narrow case.
|
||||
@@ -1,147 +0,0 @@
|
||||
# File diff color tokens and CSS variables
|
||||
|
||||
Inventory for this worktree's V2 web/desktop file viewer. Source inspection only; no colors or behavior changed. Includes diff-specific colors, syntax colors, and the selection/search/comment colors used inside the viewer—not every unrelated global app variable. Unified and split use the same color system.
|
||||
|
||||
## Main controls and their wiring
|
||||
|
||||
| Visual role | Renderer variable | OpenCode source |
|
||||
|---|---|---|
|
||||
| Neutral file background | `--diffs-bg` | `--opencode-diffs-bg`, falling back to `--color-background-stronger` (alias of `--background-stronger`) |
|
||||
| Default text | `--fg` / `--diffs-fg` | Registered theme foreground: `--text-base`; syntax spans override it |
|
||||
| Addition seed / gutter bar | `--diffs-addition-color` → `--diffs-addition-base` | `--syntax-diff-add` |
|
||||
| Deletion seed / gutter bar | `--diffs-deletion-color` → `--diffs-deletion-base` | `--syntax-diff-delete` |
|
||||
| Modified seed | `--diffs-modified-color` → `--diffs-modified-base` | `--syntax-diff-unknown` |
|
||||
| Added row | `--diffs-bg-addition`, `--diffs-bg-addition-override` | Mix of neutral background and addition seed |
|
||||
| Deleted row | `--diffs-bg-deletion`, `--diffs-bg-deletion-override` | Mix of neutral background and deletion seed |
|
||||
| Added inline span | `--diffs-bg-addition-emphasis`, `--diffs-bg-addition-emphasis-override` | Alpha of addition seed |
|
||||
| Deleted inline span | `--diffs-bg-deletion-emphasis`, `--diffs-bg-deletion-emphasis-override` | Alpha of deletion seed; softened on selected deletion rows |
|
||||
| Folded “unmodified lines” row | `--diffs-bg-separator`, `--diffs-bg-separator-override` | Mix of neutral background and `--diffs-mixer` |
|
||||
| Ordinary line numbers / fold text | `--diffs-fg-number`, `--diffs-fg-number-override` | Foreground/background mix |
|
||||
| Selection background | `--diffs-selection-base` → `--diffs-bg-selection` | `--v2-background-bg-accent` |
|
||||
| Selected line-number text | `--diffs-selection-number-fg` | `--v2-text-text-accent` |
|
||||
| Comment annotation background | `--diffs-comment-bg` | Alpha of `--v2-background-bg-accent` |
|
||||
| Search match / current match | CSS highlight backgrounds | Alpha of `--surface-warning-base` / `--surface-warning-strong` |
|
||||
|
||||
**Important:** `--surface-diff-add-*`, `--surface-diff-delete-*`, and `--surface-diff-hidden-*` exist in the global theme, but are not the direct row/inline/fold controls in the current Pierre rendering path. The older/custom separator CSS in `components/file.css` does use `--surface-diff-hidden-base` and `--surface-diff-hidden-strong`.
|
||||
|
||||
## OpenCode diff-specific theme tokens — complete families
|
||||
|
||||
Each name below is a CSS custom property. Theme JSON override keys omit the leading `--`. Tailwind's `--color-…` aliases are defined in `packages/ui/src/styles/tailwind/colors.css` (for example `--color-surface-diff-add-base` → `--surface-diff-add-base`); these are aliases, not separate color decisions.
|
||||
|
||||
### Surfaces
|
||||
|
||||
```text
|
||||
--surface-diff-unchanged-base
|
||||
--surface-diff-skip-base
|
||||
--surface-diff-hidden-base
|
||||
--surface-diff-hidden-weak
|
||||
--surface-diff-hidden-weaker
|
||||
--surface-diff-hidden-strong
|
||||
--surface-diff-hidden-stronger
|
||||
--surface-diff-add-base
|
||||
--surface-diff-add-weak
|
||||
--surface-diff-add-weaker
|
||||
--surface-diff-add-strong
|
||||
--surface-diff-add-stronger
|
||||
--surface-diff-delete-base
|
||||
--surface-diff-delete-weak
|
||||
--surface-diff-delete-weaker
|
||||
--surface-diff-delete-strong
|
||||
--surface-diff-delete-stronger
|
||||
```
|
||||
|
||||
### Text, icons, and highlighter diff seeds
|
||||
|
||||
```text
|
||||
--text-diff-add-base
|
||||
--text-diff-add-strong
|
||||
--text-diff-delete-base
|
||||
--text-diff-delete-strong
|
||||
--icon-diff-add-base
|
||||
--icon-diff-add-hover
|
||||
--icon-diff-add-active
|
||||
--icon-diff-delete-base
|
||||
--icon-diff-delete-hover
|
||||
--icon-diff-modified-base
|
||||
--syntax-diff-add
|
||||
--syntax-diff-delete
|
||||
--syntax-diff-unknown
|
||||
```
|
||||
|
||||
The built-in OpenCode theme maps `--syntax-diff-add` and `--text-diff-add-base` to `--v2-state-fg-success`, and their delete counterparts to `--v2-state-fg-danger`. Optional palette seeds `diffAdd` and `diffDelete` also exist for theme resolution; they are theme inputs, not CSS variable names, and built-in overrides can supersede generated results.
|
||||
|
||||
## Syntax foreground tokens
|
||||
|
||||
```text
|
||||
--syntax-comment
|
||||
--syntax-regexp
|
||||
--syntax-string
|
||||
--syntax-keyword
|
||||
--syntax-primitive
|
||||
--syntax-operator
|
||||
--syntax-variable
|
||||
--syntax-property
|
||||
--syntax-type
|
||||
--syntax-constant
|
||||
--syntax-punctuation
|
||||
--syntax-object
|
||||
--syntax-success
|
||||
--syntax-warning
|
||||
--syntax-critical
|
||||
--syntax-info
|
||||
--syntax-unknown
|
||||
```
|
||||
|
||||
`--syntax-unknown` is referenced by the registered highlighter, but no definition was found in the current UI theme sources: treat it as an unresolved reference, not an existing resolved theme token. `--syntax-success` is available globally, although not directly used by that registered theme's current token-color rules.
|
||||
|
||||
Built-in syntax overrides additionally reference `--v2-text-text-muted`, `--v2-pink-800`, `--v2-green-800`, `--v2-orange-800`, `--v2-purple-800`, and `--v2-red-800` (with separate dark/light resolutions). Other syntax tokens use the ordinary text tokens or theme-resolved colors.
|
||||
|
||||
## Pierre renderer color variables — complete relevant inventory
|
||||
|
||||
These come from the installed `@pierre/diffs` **1.5.1** stylesheet plus OpenCode's injected CSS. `*-override` variables are hooks; other variables include derived outputs and internal implementation details, not independent OpenCode theme tokens. Some optional hooks are unset by default.
|
||||
|
||||
| Group | Variables |
|
||||
|---|---|
|
||||
| Base | `--diffs-bg`, `--diffs-fg`, `--diffs-mixer`, `--opencode-diffs-bg`, `--bg`, `--fg` |
|
||||
| Theme foreground/background variants | `--diffs-light`, `--diffs-dark`, `--diffs-light-bg`, `--diffs-dark-bg` |
|
||||
| Neutral backgrounds | `--diffs-bg-buffer`, `--diffs-bg-buffer-override`, `--diffs-bg-context`, `--diffs-bg-context-override`, `--diffs-bg-context-gutter`, `--diffs-bg-context-gutter-override`, `--diffs-bg-separator`, `--diffs-bg-separator-override` |
|
||||
| Number / conflict-marker foreground | `--diffs-fg-number`, `--diffs-fg-number-override`, `--diffs-fg-number-addition-override`, `--diffs-fg-number-deletion-override`, `--diffs-fg-conflict-marker`, `--diffs-fg-conflict-marker-override` |
|
||||
| Addition seed | `--diffs-addition-base`, `--diffs-addition-color`, `--diffs-addition-color-override`, `--diffs-light-addition-color`, `--diffs-dark-addition-color` |
|
||||
| Deletion seed | `--diffs-deletion-base`, `--diffs-deletion-color`, `--diffs-deletion-color-override`, `--diffs-light-deletion-color`, `--diffs-dark-deletion-color` |
|
||||
| Modified seed | `--diffs-modified-base`, `--diffs-modified-color`, `--diffs-modified-color-override`, `--diffs-light-modified-color`, `--diffs-dark-modified-color` |
|
||||
| Renderer fallback colors | `--diffs-added-light`, `--diffs-added-dark`, `--diffs-deleted-light`, `--diffs-deleted-dark`, `--diffs-modified-light`, `--diffs-modified-dark`, `--diffs-warning-light`, `--diffs-warning-dark` |
|
||||
| Added row/gutter/inline | `--diffs-bg-addition`, `--diffs-bg-addition-override`, `--diffs-bg-addition-number-override`, `--diffs-bg-addition-emphasis`, `--diffs-bg-addition-emphasis-override` |
|
||||
| Deleted row/gutter/inline | `--diffs-bg-deletion`, `--diffs-bg-deletion-override`, `--diffs-bg-deletion-number-override`, `--diffs-bg-deletion-emphasis`, `--diffs-bg-deletion-emphasis-override` |
|
||||
| Selection | `--diffs-selection-base`, `--diffs-selection-number-fg`, `--diffs-bg-selection`, `--diffs-bg-selection-override`, `--diffs-bg-selection-number`, `--diffs-bg-selection-number-override`, `--diffs-bg-selection-text` |
|
||||
| Hover | `--diffs-bg-hover-override`, `--diffs-hover-mix-target` |
|
||||
| Comments / decoration | `--diffs-comment-bg`, `--diffs-annotation-bg`, `--diffs-decoration-bg`, `--diffs-decoration-bar-color` |
|
||||
| Internal background pipeline | `--diffs-computed-decoration-bg`, `--diffs-computed-diff-line-bg`, `--diffs-computed-selected-line-bg`, `--diffs-computed-editor-active-line-bg`, `--diffs-computed-hovered-line-bg`, `--diffs-line-bg` |
|
||||
| Internal mix targets | `--diffs-diff-line-mix-target`, `--diffs-selection-mix-target`, `--diffs-selection-emphasis-mix-target` |
|
||||
| Per-token light/dark colors | `--diffs-token-light`, `--diffs-token-dark`, `--diffs-token-light-bg`, `--diffs-token-dark-bg` |
|
||||
| Conflict backgrounds (library support; not exercised by the fixture) | `--conflict-bg-current-header-override`, `--conflict-bg-current-number-override`, `--conflict-bg-current-override`, `--conflict-bg-incoming-header-override`, `--conflict-bg-incoming-number-override`, `--conflict-bg-incoming-override` |
|
||||
|
||||
Related mix controls are **percentages, not colors**: `--mix-light`, `--mix-dark`, `--mix-deco-light`, `--mix-deco-dark`, `--mix-selection-light`, `--mix-selection-dark`, and `--diffs-editor-active-line-source-mix`.
|
||||
|
||||
## Other color tokens used inside the file viewer
|
||||
|
||||
- Text: `--text-base`, `--text-weak`, `--text-strong`.
|
||||
- Background fallback: `--background-stronger`, `--color-background-stronger`; comment actions also use `--background-base`.
|
||||
- Selection: `--v2-background-bg-accent`, `--v2-text-text-accent`.
|
||||
- Search: `--surface-warning-base`, `--surface-warning-strong`.
|
||||
- Custom fold UI: `--surface-diff-hidden-base`, `--surface-diff-hidden-strong`, `--icon-strong-base`; `--text-mix-blend-mode` affects compositing but is not a color.
|
||||
- Comment controls: `--icon-interactive-base`, `--white`, `--surface-raised-stronger-non-alpha`, `--surface-base`, `--surface-raised-base-hover`, `--border-base`, `--text-weak`, `--text-strong`, `--background-base`.
|
||||
- Comment shadows (composite shadow tokens, not single colors): `--shadow-xs`, `--shadow-xs-border-focus`, `--shadow-xxs-border`, `--shadow-xs-border-select`.
|
||||
|
||||
Typography, sizing, gaps, gutter widths, font weight/style, and text-decoration variables are deliberately excluded from this color inventory.
|
||||
|
||||
## Source locations
|
||||
|
||||
- `packages/session-ui/src/pierre/index.ts`: viewer color overrides, selection, search, comment tint.
|
||||
- `packages/ui/src/context/marked-theme.tsx`: registered foreground/background, diff seeds, syntax mappings.
|
||||
- `packages/ui/src/theme/resolve.ts`: global diff/syntax token generation.
|
||||
- `packages/ui/src/theme/themes/oc-2.json`: built-in light/dark mappings.
|
||||
- `packages/ui/src/styles/theme.css` and `styles/tailwind/colors.css`: defaults and aliases.
|
||||
- `packages/session-ui/src/components/file.css`: custom fold-separator styling.
|
||||
- `packages/session-ui/src/components/line-comment-styles.ts`: comment controls.
|
||||
- Installed `@pierre/diffs/dist/style.js`: renderer variables and derived backgrounds.
|
||||
- Installed `@pierre/diffs/dist/utils/getHighlighterThemeStyles.js`: highlighter-to-renderer seed bridge.
|
||||
@@ -1,36 +0,0 @@
|
||||
# Diff color review (temporary)
|
||||
|
||||
Review-only material for the diff color PR. This whole commit is dropped before merge: `diff-color-fixture/`,
|
||||
`diff-color-review-artifacts/`, `packages/session-ui/src/pierre/diff-color-tuning.ts`, and its import in
|
||||
`packages/session-ui/src/pierre/index.ts`.
|
||||
|
||||
## See the example diffs
|
||||
|
||||
The fixture baselines are committed; their edits ship as a patch so they show up as uncommitted Git changes.
|
||||
|
||||
```sh
|
||||
git apply diff-color-review-artifacts/fixture-edits.patch
|
||||
```
|
||||
|
||||
Open a session in this worktree, open **Review → Git changes**, and pick a file in `diff-color-fixture/`.
|
||||
Undo with `git checkout -- diff-color-fixture && git clean -fd diff-color-fixture`.
|
||||
|
||||
## Tune colors yourself
|
||||
|
||||
With the desktop dev app running from this worktree (`bun run dev:desktop`):
|
||||
|
||||
```sh
|
||||
bun run diff-color-review-artifacts/tuner/server.ts
|
||||
```
|
||||
|
||||
Open http://127.0.0.1:4455 (any browser, or the OpenCode browser pane).
|
||||
|
||||
- Every change saves to `diff-color-tuning.ts` (hot-reloaded) and is pushed live into open diffs through the dev
|
||||
app's debug port (9222; override with `TUNER_CDP`).
|
||||
- **Override set**: `Reference` is the committed, read-only set used for this PR (it matches the branch colors, so it
|
||||
previews as no change). Editing it saves a copy; use **New** for a blank set. Your sets stay local and untracked.
|
||||
- “Current” leaves the branch colors untouched. Slots can be an exact color or a v2 token, with opacity.
|
||||
- **Copy CSS** / **Paste CSS** share a set with someone else; Undo/Redo and per-category toggles are in the toolbar.
|
||||
|
||||
`REPORT.md` and the screenshots document the colors before this change; `COLOR-TOKENS.md` lists the diff view's
|
||||
color tokens and Pierre variables.
|
||||
@@ -1,69 +0,0 @@
|
||||
# OpenCode V2 diff color review — temporary fixture
|
||||
|
||||
[File diff color token / CSS variable inventory](COLOR-TOKENS.md)
|
||||
|
||||
## Location and refs
|
||||
|
||||
- Worktree: `/Users/usrnk1/.local/share/opencode/worktree/012780/diff-color-review`
|
||||
- Branch: `diff-color-review` (local only).
|
||||
- Fetched `origin/v2`: `7e42a897bc6dbbba580392d779e47137038bcafb`.
|
||||
- Local baseline / HEAD: `5e14ede13c260b3b1bd2e16c5bb25722d12f366b`.
|
||||
- Fixture: `diff-color-fixture/01-inline.tsx`, `02-folded.ts`, `03-prose.md`, `04-many-rows.ts`, `05-deleted.txt` (deleted), `06-added.txt` (untracked addition).
|
||||
- `diff-color-fixture/README.md` is tracked and unchanged.
|
||||
- Only fixtures changed. No renderer, token, behavior, formatter, push, PR, or deployment changes.
|
||||
|
||||
## Actual UI and how to open
|
||||
|
||||
The captures use the real source-backed OpenCode web app from this worktree, connected to the existing V2 background server (2.0.21). No mocked data or copied renderer. The app dev server remains available at `http://127.0.0.1:4444` and was started with `VITE_OPENCODE_SERVER_PORT=49374 bun run dev -- --port 4444` from `packages/app`. Existing app/server processes were not restarted.
|
||||
|
||||
Open:
|
||||
|
||||
`http://127.0.0.1:4444/server/aHR0cDovLzEyNy4wLjAuMTo0OTM3NA/session/ses_f085f0176ffeiNj9364c2CnHqm`
|
||||
|
||||
Alternatively open this session in OpenCode Desktop; its location is now this worktree. Toggle Review, choose **Git changes**, and select a fixture file. This UI calls the working-tree source “Git changes,” not “Uncommitted changes.” Use the file-tree toggle if the list is hidden. Unified/Split controls are in the review toolbar.
|
||||
|
||||
Captures: 1440 × 1000 CSS pixels, default OpenCode (`oc-2`) theme, system light/dark media preference. Chat pane was resized to its minimum and the file tree hidden to give the diff about 964 pixels. Wrapping was enabled by the production viewer. Close-ups below are unchanged crops of those screenshots, not re-rendered mockups.
|
||||
|
||||
## Representative screenshots
|
||||
|
||||
- [Dark inline close-up](dark-inline-closeup.png) / [Light inline close-up](light-inline-closeup.png)
|
||||
- [Dark split inline](dark-split-inline.png) / [Light split inline](light-split-inline.png)
|
||||
- [Dark split folded: all three bars](dark-split-folded.png) / [Light split folded](light-split-folded.png)
|
||||
- [Dark unified folded](dark-unified-folded.png) / [Light unified folded](light-unified-folded.png)
|
||||
- [Dark syntax and wrapped long line](dark-split-syntax-long.png) / [Light syntax and wrapped long line](light-split-syntax-long.png)
|
||||
- [Dark Markdown](dark-split-prose.png) / [Light Markdown](light-split-prose.png)
|
||||
|
||||
Additional unified captures are in this same directory.
|
||||
|
||||
## Case notes
|
||||
|
||||
| Case | Unchanged sections | Red/green scan | Inline emphasis | Text readability |
|
||||
|---|---|---|---|---|
|
||||
| Single additions/deletions/replacements, adjacent rows (`01`, `03`) | Neutral background recedes; unchanged syntax can still draw the eye | Light is clearer; dark relies more on gutter bars and line numbers | Distinct but restrained | TS strings stay green even on red deletion rows, competing with change meaning |
|
||||
| Contiguous versus separated edits (`01` lines 11–12; long line) | Common words inside an inline span do **not** recede | Row direction is still clear from gutter and tint | Current production `word-line` rendering joins separated edits into one broad span; multiple independent highlight islands were not available in this state | Broad wrapped emphasis adds visual weight without improving precision |
|
||||
| One-character, start/middle/end edits (`01` lines 13–16; `03`) | Surrounding text remains readable | Large words are easy to find; one-character changes need deliberate attention | One-character patch is visible but easy to miss at normal scan speed | No observed loss of string readability |
|
||||
| Punctuation and spaces/indent/trailing spaces (`01` lines 17–20; `03`) | Quiet surroundings | Row markers expose that something changed | Small semicolon/space blocks are visible on close inspection; blank blocks give little explanation without visible whitespace glyphs | Readable; punctuation has less salience than colored keywords |
|
||||
| Isolated hunks and collapsed sections (`02`) | Neutral context recedes; three bars visible simultaneously in split | Distinct isolated rows, but dark tints are weak | Small changed-word blocks don't dominate rows | Fold-label text is unusually faint, especially dark; context comments are clearer than the fold labels |
|
||||
| TS/TSX types, keywords, comments and JSX (`01`) | Syntax in unchanged rows remains fairly prominent | Pink keywords/green strings compete with the red/green layer | Emphasis is generally subordinate; the long merged span is the exception | Comments/strings/types readable overall; muted JSX words like “old,” “new,” and button text are weak on tinted/highlighted backgrounds in dark mode |
|
||||
| Markdown and wrapped prose (`03`) | Plain prose is visually quieter than code | Light tints are clearer than dark | Long spans emphasize unchanged middle words as well as changes | Plain prose remains readable but looks muted; bold/heading syntax draws attention more strongly than small edit spans |
|
||||
| Long/wrapped lines (`01`, `03`) | Common middle text is swallowed by merged emphasis | Tinted multi-line blocks are apparent | Broad highlights repeat across wraps and visually dominate small changes | Wrapping works in unified and split; side-by-side requires more vertical scanning |
|
||||
| File beginning/end (`01`, `02`, `03`) | Neutral surrounding rows recede | Changes shown at first/last lines | Same inline behavior as middle edits | Readable |
|
||||
| Dense larger file (`04`: 63 lines, 40 replacements / 80 changed rows) | **Not visually checked** | Not checked | Not checked | Fixture ready for review |
|
||||
| Added/deleted whole files (`05`, `06`) | Viewer lists both with D/A badges | **File contents not visually checked** | Not checked | Fixture ready for review |
|
||||
| Narrow viewport (planned 800 × 1000) | **Not checked** | Not checked | Not checked | Stopped expanding capture scope at user request |
|
||||
|
||||
## Specific problems to carry into a later design pass
|
||||
|
||||
1. Dark row backgrounds have weak separation from neutral context; syntax hue is often more salient than change direction.
|
||||
2. Green string syntax appears on deleted rows too, weakening red/green semantic scanning. Pink keywords similarly attract attention independently of change status.
|
||||
3. Collapsed-section labels recede too far: their subdued text is harder to read than surrounding context comments.
|
||||
4. One-character, punctuation, and whitespace highlights are easy to miss without focused inspection. Their issue is subtlety/size, not overpowering brightness.
|
||||
5. Muted JSX text on dark inline backgrounds is less readable than the surrounding syntax. Broad inline spans across wrapped lines add disproportionate visual mass.
|
||||
|
||||
The merged-span behavior is a renderer observation, not a proposed color fix. No final values selected, no fixes implemented, and no contrast-ratio compliance claim made.
|
||||
|
||||
## GitHub Desktop comparison boundary
|
||||
|
||||
The referenced discussion/image was not included in this session. A precise comparison to that direction is therefore **not verified**. As a provisional hierarchy comparison only: quiet context → recognizable changed row → localized stronger inline emphasis is the useful target. OpenCode already keeps ordinary inline backgrounds subordinate to rows, but dark row separation, syntax competition, faint fold controls, and merged broad spans weaken that hierarchy. This is not a claim that the specific GitHub Desktop reference was inspected.
|
||||
|
||||
Scope was intentionally stopped after the user's request to do less. No full lint/typecheck was run: these are standalone visual examples with deliberate whitespace/formatting changes, not product code.
|
||||
|
Before Width: | Height: | Size: 118 KiB |
|
Before Width: | Height: | Size: 222 KiB |
|
Before Width: | Height: | Size: 309 KiB |
|
Before Width: | Height: | Size: 291 KiB |
|
Before Width: | Height: | Size: 322 KiB |
|
Before Width: | Height: | Size: 209 KiB |
|
Before Width: | Height: | Size: 299 KiB |
|
Before Width: | Height: | Size: 252 KiB |
|
Before Width: | Height: | Size: 249 KiB |
|
Before Width: | Height: | Size: 289 KiB |
@@ -1,601 +0,0 @@
|
||||
diff --git a/diff-color-fixture/01-inline.tsx b/diff-color-fixture/01-inline.tsx
|
||||
index 1d16049d61..5074a3bc83 100644
|
||||
--- a/diff-color-fixture/01-inline.tsx
|
||||
+++ b/diff-color-fixture/01-inline.tsx
|
||||
@@ -1,37 +1,37 @@
|
||||
-// BEGIN: baseline — first-line change
|
||||
+// BEGIN: edited — first-line change
|
||||
type ReviewState = "pending" | "ready" | "failed"
|
||||
type ReviewProps = { title: string; count: number; state: ReviewState }
|
||||
|
||||
-export const heading = "Review the old checkout experience"
|
||||
-export const count = 8
|
||||
-export const obsolete = "delete this isolated row"
|
||||
+export const heading = "Review the new checkout experience"
|
||||
+export const count = 9
|
||||
export const anchor = "unchanged between deletion and addition"
|
||||
+export const added = "add this isolated row"
|
||||
|
||||
// Contiguous span versus several separated small spans.
|
||||
-export const contiguous = "Keep the original checkout message readable"
|
||||
-export const scattered = "alpha: old; beta: cold; gamma: slow"
|
||||
-export const oneCharacter = "item-7"
|
||||
-export const atStart = "before middle remains ending remains"
|
||||
-export const atMiddle = "start remains before ending remains"
|
||||
-export const atEnd = "start remains middle remains before"
|
||||
-export const punctuation = { label: "Ready", enabled: true }
|
||||
-export const spaces = "one two three"
|
||||
-export const indent = "indent-only change"
|
||||
-export const trailing = "trailing-space-only change"
|
||||
+export const contiguous = "Keep the redesigned payment summary readable"
|
||||
+export const scattered = "alpha: new; beta: warm; gamma: fast"
|
||||
+export const oneCharacter = "item-8"
|
||||
+export const atStart = "after middle remains ending remains"
|
||||
+export const atMiddle = "start remains after ending remains"
|
||||
+export const atEnd = "start remains middle remains after"
|
||||
+export const punctuation = { label: "Ready", enabled: true };
|
||||
+export const spaces = "one two three"
|
||||
+ export const indent = "indent-only change"
|
||||
+export const trailing = "trailing-space-only change"
|
||||
|
||||
// Syntax competition: comment, keyword, type, boolean, string, number.
|
||||
export function ReviewCard(props: ReviewProps) {
|
||||
- const status: ReviewState = "pending"
|
||||
- const enabled = false
|
||||
- const retries = 3
|
||||
- // Keep this quiet explanation readable on a tinted row.
|
||||
- return <section aria-label="Old review" data-state={status}>
|
||||
+ const status: ReviewState | undefined = "ready"
|
||||
+ const enabled = true
|
||||
+ const retries = 4
|
||||
+ // Keep this revised explanation readable on a tinted row.
|
||||
+ return <article aria-label="New review" data-state={status}>
|
||||
<h2>{props.title}</h2>
|
||||
- <span className="text-muted">{props.count} old items</span>
|
||||
- <button disabled={!enabled}>Continue checkout</button>
|
||||
- </section>
|
||||
+ <span className="text-primary">{props.count} new items</span>
|
||||
+ <button disabled={!enabled}>Confirm payment</button>
|
||||
+ </article>
|
||||
}
|
||||
|
||||
-export const longLine = "Start unchanged | The old checkout flow waits for manual confirmation before showing the receipt | Middle unchanged with punctuation: brackets [one, two], braces {three}, quotes 'four', slash /five/ | The old final instruction asks the reviewer to close the window | End unchanged"
|
||||
+export const longLine = "Start unchanged | The new payment flow waits for automatic confirmation before showing the receipt | Middle unchanged with punctuation: brackets [one, two], braces {three}, quotes 'four', slash /five/ | The new final instruction asks the reviewer to keep the window | End unchanged"
|
||||
|
||||
-// END: baseline — last-line change
|
||||
+// END: edited — last-line change
|
||||
diff --git a/diff-color-fixture/02-folded.ts b/diff-color-fixture/02-folded.ts
|
||||
index 07541ab49d..a5b9b354c3 100644
|
||||
--- a/diff-color-fixture/02-folded.ts
|
||||
+++ b/diff-color-fixture/02-folded.ts
|
||||
@@ -1,5 +1,5 @@
|
||||
-// BEGIN baseline
|
||||
-export const first = "old"
|
||||
+// BEGIN edited
|
||||
+export const first = "new"
|
||||
// quiet context A01
|
||||
// quiet context A02
|
||||
// quiet context A03
|
||||
@@ -20,7 +20,7 @@ export const first = "old"
|
||||
// quiet context A18
|
||||
// quiet context A19
|
||||
// quiet context A20
|
||||
-export const second = "old"
|
||||
+export const second = "new"
|
||||
// quiet context B01
|
||||
// quiet context B02
|
||||
// quiet context B03
|
||||
@@ -41,7 +41,7 @@ export const second = "old"
|
||||
// quiet context B18
|
||||
// quiet context B19
|
||||
// quiet context B20
|
||||
-export const third = "old"
|
||||
+export const third = "new"
|
||||
// quiet context C01
|
||||
// quiet context C02
|
||||
// quiet context C03
|
||||
@@ -62,5 +62,5 @@ export const third = "old"
|
||||
// quiet context C18
|
||||
// quiet context C19
|
||||
// quiet context C20
|
||||
-export const fourth = "old"
|
||||
-// END baseline
|
||||
+export const fourth = "new"
|
||||
+// END edited
|
||||
diff --git a/diff-color-fixture/03-prose.md b/diff-color-fixture/03-prose.md
|
||||
index 4d5b31e3b9..7d01146b6a 100644
|
||||
--- a/diff-color-fixture/03-prose.md
|
||||
+++ b/diff-color-fixture/03-prose.md
|
||||
@@ -1,21 +1,21 @@
|
||||
-# Before: plain-text review
|
||||
+# After: plain-text review
|
||||
|
||||
Unchanged prose should remain quiet, not compete with changed rows.
|
||||
-Replace one contiguous phrase: the original checkout experience stays here.
|
||||
-Several little edits: red apple, cold tea, slow train.
|
||||
-One character: ticket 7.
|
||||
-before — the middle and the end remain unchanged.
|
||||
-The start remains — before — the end remains.
|
||||
-The start and the middle remain — before
|
||||
-Punctuation only: hello, world!
|
||||
-Whitespace only: one two three
|
||||
-Trailing whitespace only.
|
||||
-Delete this sentence without a replacement.
|
||||
+Replace one contiguous phrase: the redesigned payment summary stays here.
|
||||
+Several little edits: green apple, warm tea, fast train.
|
||||
+One character: ticket 8.
|
||||
+after — the middle and the end remain unchanged.
|
||||
+The start remains — after — the end remains.
|
||||
+The start and the middle remain — after
|
||||
+Punctuation only: hello; world?
|
||||
+Whitespace only: one two three
|
||||
+Trailing whitespace only.
|
||||
|
||||
This unchanged sentence separates a deletion from an addition.
|
||||
+Add this sentence without a corresponding deletion.
|
||||
|
||||
-Long prose: The original checkout flow asks the reader to review the old confirmation message before proceeding through the receipt screen, while the unchanged middle of this deliberately long paragraph checks whether horizontal scrolling or line wrapping keeps small inline edits visible at a narrow viewport; the final old phrase appears near the far right edge.
|
||||
+Long prose: The redesigned payment flow asks the reader to review the new confirmation message before proceeding through the receipt screen, while the unchanged middle of this deliberately long paragraph checks whether horizontal scrolling or line wrapping keeps small inline edits visible at a narrow viewport; the final new phrase appears near the far right edge.
|
||||
|
||||
-**Syntax-like Markdown** competes with `inline code`, [links](https://example.com/old), and _emphasis_.
|
||||
+**Syntax-rich Markdown** competes with `inline code`, [links](https://example.com/new), and _emphasis_.
|
||||
|
||||
-Final sentence before.
|
||||
+Final sentence after.
|
||||
diff --git a/diff-color-fixture/04-many-rows.ts b/diff-color-fixture/04-many-rows.ts
|
||||
index 7a8c6ecd49..b3e544ae5f 100644
|
||||
--- a/diff-color-fixture/04-many-rows.ts
|
||||
+++ b/diff-color-fixture/04-many-rows.ts
|
||||
@@ -10,46 +10,46 @@ export const records = [
|
||||
{ id: "row-008", state: "pending", count: 8 },
|
||||
{ id: "row-009", state: "pending", count: 9 },
|
||||
{ id: "row-010", state: "pending", count: 10 },
|
||||
- { id: "row-011", state: "pending", count: 11 },
|
||||
- { id: "row-012", state: "pending", count: 12 },
|
||||
- { id: "row-013", state: "pending", count: 13 },
|
||||
- { id: "row-014", state: "pending", count: 14 },
|
||||
- { id: "row-015", state: "pending", count: 15 },
|
||||
- { id: "row-016", state: "pending", count: 16 },
|
||||
- { id: "row-017", state: "pending", count: 17 },
|
||||
- { id: "row-018", state: "pending", count: 18 },
|
||||
- { id: "row-019", state: "pending", count: 19 },
|
||||
- { id: "row-020", state: "pending", count: 20 },
|
||||
- { id: "row-021", state: "pending", count: 21 },
|
||||
- { id: "row-022", state: "pending", count: 22 },
|
||||
- { id: "row-023", state: "pending", count: 23 },
|
||||
- { id: "row-024", state: "pending", count: 24 },
|
||||
- { id: "row-025", state: "pending", count: 25 },
|
||||
- { id: "row-026", state: "pending", count: 26 },
|
||||
- { id: "row-027", state: "pending", count: 27 },
|
||||
- { id: "row-028", state: "pending", count: 28 },
|
||||
- { id: "row-029", state: "pending", count: 29 },
|
||||
- { id: "row-030", state: "pending", count: 30 },
|
||||
- { id: "row-031", state: "pending", count: 31 },
|
||||
- { id: "row-032", state: "pending", count: 32 },
|
||||
- { id: "row-033", state: "pending", count: 33 },
|
||||
- { id: "row-034", state: "pending", count: 34 },
|
||||
- { id: "row-035", state: "pending", count: 35 },
|
||||
- { id: "row-036", state: "pending", count: 36 },
|
||||
- { id: "row-037", state: "pending", count: 37 },
|
||||
- { id: "row-038", state: "pending", count: 38 },
|
||||
- { id: "row-039", state: "pending", count: 39 },
|
||||
- { id: "row-040", state: "pending", count: 40 },
|
||||
- { id: "row-041", state: "pending", count: 41 },
|
||||
- { id: "row-042", state: "pending", count: 42 },
|
||||
- { id: "row-043", state: "pending", count: 43 },
|
||||
- { id: "row-044", state: "pending", count: 44 },
|
||||
- { id: "row-045", state: "pending", count: 45 },
|
||||
- { id: "row-046", state: "pending", count: 46 },
|
||||
- { id: "row-047", state: "pending", count: 47 },
|
||||
- { id: "row-048", state: "pending", count: 48 },
|
||||
- { id: "row-049", state: "pending", count: 49 },
|
||||
- { id: "row-050", state: "pending", count: 50 },
|
||||
+ { id: "row-011", state: "ready", count: 111 },
|
||||
+ { id: "row-012", state: "ready", count: 112 },
|
||||
+ { id: "row-013", state: "ready", count: 113 },
|
||||
+ { id: "row-014", state: "ready", count: 114 },
|
||||
+ { id: "row-015", state: "ready", count: 115 },
|
||||
+ { id: "row-016", state: "ready", count: 116 },
|
||||
+ { id: "row-017", state: "ready", count: 117 },
|
||||
+ { id: "row-018", state: "ready", count: 118 },
|
||||
+ { id: "row-019", state: "ready", count: 119 },
|
||||
+ { id: "row-020", state: "ready", count: 120 },
|
||||
+ { id: "row-021", state: "ready", count: 121 },
|
||||
+ { id: "row-022", state: "ready", count: 122 },
|
||||
+ { id: "row-023", state: "ready", count: 123 },
|
||||
+ { id: "row-024", state: "ready", count: 124 },
|
||||
+ { id: "row-025", state: "ready", count: 125 },
|
||||
+ { id: "row-026", state: "ready", count: 126 },
|
||||
+ { id: "row-027", state: "ready", count: 127 },
|
||||
+ { id: "row-028", state: "ready", count: 128 },
|
||||
+ { id: "row-029", state: "ready", count: 129 },
|
||||
+ { id: "row-030", state: "ready", count: 130 },
|
||||
+ { id: "row-031", state: "ready", count: 131 },
|
||||
+ { id: "row-032", state: "ready", count: 132 },
|
||||
+ { id: "row-033", state: "ready", count: 133 },
|
||||
+ { id: "row-034", state: "ready", count: 134 },
|
||||
+ { id: "row-035", state: "ready", count: 135 },
|
||||
+ { id: "row-036", state: "ready", count: 136 },
|
||||
+ { id: "row-037", state: "ready", count: 137 },
|
||||
+ { id: "row-038", state: "ready", count: 138 },
|
||||
+ { id: "row-039", state: "ready", count: 139 },
|
||||
+ { id: "row-040", state: "ready", count: 140 },
|
||||
+ { id: "row-041", state: "ready", count: 141 },
|
||||
+ { id: "row-042", state: "ready", count: 142 },
|
||||
+ { id: "row-043", state: "ready", count: 143 },
|
||||
+ { id: "row-044", state: "ready", count: 144 },
|
||||
+ { id: "row-045", state: "ready", count: 145 },
|
||||
+ { id: "row-046", state: "ready", count: 146 },
|
||||
+ { id: "row-047", state: "ready", count: 147 },
|
||||
+ { id: "row-048", state: "ready", count: 148 },
|
||||
+ { id: "row-049", state: "ready", count: 149 },
|
||||
+ { id: "row-050", state: "ready", count: 150 },
|
||||
{ id: "row-051", state: "pending", count: 51 },
|
||||
{ id: "row-052", state: "pending", count: 52 },
|
||||
{ id: "row-053", state: "pending", count: 53 },
|
||||
diff --git a/diff-color-fixture/05-deleted.txt b/diff-color-fixture/05-deleted.txt
|
||||
deleted file mode 100644
|
||||
index e80ad6b276..0000000000
|
||||
--- a/diff-color-fixture/05-deleted.txt
|
||||
+++ /dev/null
|
||||
@@ -1,4 +0,0 @@
|
||||
-This entire tracked file will be deleted after the baseline commit.
|
||||
-Only deletion tint should be present.
|
||||
-No syntax colors should compete with this text.
|
||||
-Last deleted row.
|
||||
diff --git a/diff-color-fixture/06-added.txt b/diff-color-fixture/06-added.txt
|
||||
new file mode 100644
|
||||
index 0000000000..f27ed60c9b
|
||||
--- /dev/null
|
||||
+++ b/diff-color-fixture/06-added.txt
|
||||
@@ -0,0 +1,5 @@
|
||||
+This entire file is newly added, not a replacement for the deleted example.
|
||||
+Only addition tint should be present.
|
||||
+Plain text: keep the words readable against the changed-row background.
|
||||
+A deliberately long added line checks the horizontal extent of the green row while the reviewer compares the quiet surrounding application surface against the color used for new content in both dark and light modes.
|
||||
+Last added row.
|
||||
diff --git a/diff-color-fixture/07-styles.css b/diff-color-fixture/07-styles.css
|
||||
index a757ef7085..4f4937480e 100644
|
||||
--- a/diff-color-fixture/07-styles.css
|
||||
+++ b/diff-color-fixture/07-styles.css
|
||||
@@ -1,32 +1,35 @@
|
||||
-/* Baseline stylesheet: colors, units, selectors, at-rules. */
|
||||
+/* Edited stylesheet: colors, units, selectors, at-rules. */
|
||||
:root {
|
||||
- --brand: #3b5cf6;
|
||||
- --radius: 6px;
|
||||
+ --brand: #0b34f4;
|
||||
+ --radius: 8px;
|
||||
--gap: 8px;
|
||||
+ --shadow: 0 1px 2px rgba(0, 0, 0, 0.08);
|
||||
}
|
||||
|
||||
.card {
|
||||
- display: flex;
|
||||
+ display: grid;
|
||||
gap: var(--gap);
|
||||
- padding: 12px 16px;
|
||||
- border: 1px solid rgba(0, 0, 0, 0.1);
|
||||
+ padding: 12px 20px;
|
||||
+ border: 1px solid rgba(0, 0, 0, 0.12);
|
||||
border-radius: var(--radius);
|
||||
background: #ffffff;
|
||||
color: #161616;
|
||||
+ box-shadow: var(--shadow);
|
||||
}
|
||||
|
||||
-.card:hover {
|
||||
- background: #fafafa;
|
||||
+.card:hover,
|
||||
+.card:focus-visible {
|
||||
+ background: #f5f5f5;
|
||||
}
|
||||
|
||||
.card[data-state="error"] {
|
||||
- color: #b82d35;
|
||||
+ color: #d92e3c;
|
||||
border-color: #f2bbb7;
|
||||
}
|
||||
|
||||
-@media (max-width: 768px) {
|
||||
+@media (max-width: 640px) {
|
||||
.card {
|
||||
- flex-direction: column;
|
||||
+ grid-template-columns: 1fr;
|
||||
padding: 8px;
|
||||
}
|
||||
}
|
||||
@@ -34,8 +37,10 @@
|
||||
@keyframes fade-in {
|
||||
from {
|
||||
opacity: 0;
|
||||
+ transform: translateY(4px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
+ transform: none;
|
||||
}
|
||||
}
|
||||
diff --git a/diff-color-fixture/08-config.json b/diff-color-fixture/08-config.json
|
||||
index 09631d931c..8b6f1485fd 100644
|
||||
--- a/diff-color-fixture/08-config.json
|
||||
+++ b/diff-color-fixture/08-config.json
|
||||
@@ -1,21 +1,22 @@
|
||||
{
|
||||
"name": "diff-color-fixture",
|
||||
- "version": "1.0.0",
|
||||
+ "version": "1.1.0",
|
||||
"private": true,
|
||||
"settings": {
|
||||
- "theme": "light",
|
||||
+ "theme": "dark",
|
||||
"fontSize": 13,
|
||||
- "lineHeight": 20,
|
||||
- "wrap": false,
|
||||
- "tabs": ["review", "files", "terminal"]
|
||||
+ "lineHeight": 24,
|
||||
+ "wrap": true,
|
||||
+ "tabs": ["review", "files", "terminal", "browser"]
|
||||
},
|
||||
"features": {
|
||||
"inlineHighlights": true,
|
||||
"foldUnchanged": true,
|
||||
- "splitView": false
|
||||
+ "splitView": true,
|
||||
+ "wordDiff": "word-line"
|
||||
},
|
||||
"limits": {
|
||||
- "maxFiles": 100,
|
||||
+ "maxFiles": 250,
|
||||
"maxLineLength": 1000
|
||||
}
|
||||
}
|
||||
diff --git a/diff-color-fixture/09-service.py b/diff-color-fixture/09-service.py
|
||||
index 64fe32db80..5da0737cf5 100644
|
||||
--- a/diff-color-fixture/09-service.py
|
||||
+++ b/diff-color-fixture/09-service.py
|
||||
@@ -1,36 +1,44 @@
|
||||
-"""Baseline Python service: decorators, f-strings, indentation, comments."""
|
||||
+"""Edited Python service: decorators, f-strings, indentation, comments."""
|
||||
|
||||
import asyncio
|
||||
-from dataclasses import dataclass
|
||||
+import logging
|
||||
+from dataclasses import dataclass, field
|
||||
|
||||
+logger = logging.getLogger(__name__)
|
||||
|
||||
-@dataclass
|
||||
+
|
||||
+@dataclass(frozen=True)
|
||||
class Review:
|
||||
id: str
|
||||
title: str
|
||||
approved: bool = False
|
||||
+ tags: list[str] = field(default_factory=list)
|
||||
|
||||
|
||||
class ReviewService:
|
||||
- def __init__(self, timeout: float = 2.0) -> None:
|
||||
+ def __init__(self, timeout: float = 5.0) -> None:
|
||||
self.timeout = timeout
|
||||
self.cache: dict[str, Review] = {}
|
||||
|
||||
async def load(self, review_id: str) -> Review | None:
|
||||
# Return cached reviews before touching the network.
|
||||
if review_id in self.cache:
|
||||
+ logger.debug("cache hit %s", review_id)
|
||||
return self.cache[review_id]
|
||||
- await asyncio.sleep(self.timeout)
|
||||
+ try:
|
||||
+ await asyncio.wait_for(asyncio.sleep(0), timeout=self.timeout)
|
||||
+ except asyncio.TimeoutError:
|
||||
+ return None
|
||||
return None
|
||||
|
||||
def summary(self, review: Review) -> str:
|
||||
- status = "approved" if review.approved else "pending"
|
||||
- return f"Review {review.id}: {review.title} ({status})"
|
||||
+ status = "approved" if review.approved else "needs review"
|
||||
+ return f"Review {review.id}: {review.title!r} ({status})"
|
||||
|
||||
|
||||
def main() -> None:
|
||||
service = ReviewService()
|
||||
- review = Review(id="r-1", title="Checkout")
|
||||
+ review = Review(id="r-1", title="Checkout", tags=["ui"])
|
||||
print(service.summary(review))
|
||||
|
||||
|
||||
diff --git a/diff-color-fixture/10-moved-block.ts b/diff-color-fixture/10-moved-block.ts
|
||||
index fbd5ecba84..214094d8eb 100644
|
||||
--- a/diff-color-fixture/10-moved-block.ts
|
||||
+++ b/diff-color-fixture/10-moved-block.ts
|
||||
@@ -3,11 +3,6 @@ export function first() {
|
||||
return "first"
|
||||
}
|
||||
|
||||
-export function second() {
|
||||
- const values = [1, 2, 3]
|
||||
- return values.map((value) => value * 2)
|
||||
-}
|
||||
-
|
||||
export function third() {
|
||||
return "third"
|
||||
}
|
||||
@@ -16,4 +11,9 @@ export function fourth() {
|
||||
return "fourth"
|
||||
}
|
||||
|
||||
-export const order = ["first", "second", "third", "fourth"]
|
||||
+export function second() {
|
||||
+ const values = [1, 2, 3]
|
||||
+ return values.map((value) => value * 2)
|
||||
+}
|
||||
+
|
||||
+export const order = ["first", "third", "fourth", "second"]
|
||||
diff --git a/diff-color-fixture/11-deploy.sh b/diff-color-fixture/11-deploy.sh
|
||||
index 5390c2fe9f..ae367067d6 100644
|
||||
--- a/diff-color-fixture/11-deploy.sh
|
||||
+++ b/diff-color-fixture/11-deploy.sh
|
||||
@@ -1,17 +1,20 @@
|
||||
#!/usr/bin/env bash
|
||||
-# Baseline shell script: variables, quoting, conditionals.
|
||||
+# Edited shell script: variables, quoting, conditionals.
|
||||
set -euo pipefail
|
||||
|
||||
ENVIRONMENT="${1:-staging}"
|
||||
-REGION="us-east-1"
|
||||
-RETRIES=3
|
||||
+REGION="${REGION:-eu-west-1}"
|
||||
+RETRIES=5
|
||||
|
||||
log() {
|
||||
- echo "[deploy] $*"
|
||||
+ printf '[deploy] %s\n' "$*" >&2
|
||||
}
|
||||
|
||||
if [[ "$ENVIRONMENT" == "production" ]]; then
|
||||
log "Deploying to production in $REGION"
|
||||
+elif [[ "$ENVIRONMENT" == "preview" ]]; then
|
||||
+ log "Skipping preview deploy"
|
||||
+ exit 0
|
||||
else
|
||||
log "Deploying to $ENVIRONMENT"
|
||||
fi
|
||||
diff --git a/diff-color-fixture/12-pipeline.yml b/diff-color-fixture/12-pipeline.yml
|
||||
index 1fbd5aa75c..251a7e4372 100644
|
||||
--- a/diff-color-fixture/12-pipeline.yml
|
||||
+++ b/diff-color-fixture/12-pipeline.yml
|
||||
@@ -1,19 +1,20 @@
|
||||
name: review
|
||||
on:
|
||||
push:
|
||||
- branches: [v2]
|
||||
+ branches: [v2, dev]
|
||||
pull_request:
|
||||
|
||||
jobs:
|
||||
check:
|
||||
- runs-on: ubuntu-latest
|
||||
- timeout-minutes: 10
|
||||
+ runs-on: ubuntu-24.04
|
||||
+ timeout-minutes: 15
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
+ - uses: oven-sh/setup-bun@v2
|
||||
- name: Install
|
||||
- run: bun install
|
||||
+ run: bun install --frozen-lockfile
|
||||
- name: Check
|
||||
run: bun run check
|
||||
env:
|
||||
CI: true
|
||||
- LOG_LEVEL: info
|
||||
+ LOG_LEVEL: debug
|
||||
diff --git a/diff-color-fixture/13-unicode.md b/diff-color-fixture/13-unicode.md
|
||||
index 1470ef9294..5bd43c3b1f 100644
|
||||
--- a/diff-color-fixture/13-unicode.md
|
||||
+++ b/diff-color-fixture/13-unicode.md
|
||||
@@ -1,12 +1,12 @@
|
||||
# Unicode and mixed scripts
|
||||
|
||||
-Emoji: ✅ ready, ⚠️ warning, ❌ failed.
|
||||
-CJK: 代码审查 完成。
|
||||
-Japanese: レビューを開始します。
|
||||
+Emoji: ✅ ready, ⚠️ warning, ❌ failed, 🚀 shipped.
|
||||
+CJK: 代码审查 已完成。
|
||||
+Japanese: レビューを完了しました。
|
||||
Korean: 변경 사항을 검토합니다.
|
||||
-Arabic (RTL): مراجعة التغييرات
|
||||
+Arabic (RTL): مراجعة جميع التغييرات
|
||||
Hebrew (RTL): סקירת שינויים
|
||||
-Accents: café, naïve, résumé.
|
||||
-Math: a ≤ b, x ≠ y, ∑ values.
|
||||
+Accents: café, naïve, résumé, déjà vu.
|
||||
+Math: a ≥ b, x ≠ y, ∏ values.
|
||||
Box: ┌─┐ │ │ └─┘
|
||||
Combining: e\u0301 vs é
|
||||
diff --git a/diff-color-fixture/14-tabs.go b/diff-color-fixture/14-tabs.go
|
||||
index ab9aca86dd..53e5e11a7c 100644
|
||||
--- a/diff-color-fixture/14-tabs.go
|
||||
+++ b/diff-color-fixture/14-tabs.go
|
||||
@@ -7,11 +7,12 @@ type Review struct {
|
||||
ID string
|
||||
Title string
|
||||
Approved bool
|
||||
+ Tags []string
|
||||
}
|
||||
|
||||
func (r Review) Summary() string {
|
||||
status := "pending"
|
||||
- if r.Approved {
|
||||
+ if r.Approved {
|
||||
status = "approved"
|
||||
}
|
||||
return fmt.Sprintf("%s: %s (%s)", r.ID, r.Title, status)
|
||||
diff --git a/diff-color-fixture/15-empty-to-content.txt b/diff-color-fixture/15-empty-to-content.txt
|
||||
index e69de29bb2..aa4be0acdb 100644
|
||||
--- a/diff-color-fixture/15-empty-to-content.txt
|
||||
+++ b/diff-color-fixture/15-empty-to-content.txt
|
||||
@@ -0,0 +1,2 @@
|
||||
+This tracked file was empty in the baseline.
|
||||
+Every row should render as an addition, without a file-added badge.
|
||||
diff --git a/diff-color-fixture/16-content-to-empty.txt b/diff-color-fixture/16-content-to-empty.txt
|
||||
index bb6e474b37..e69de29bb2 100644
|
||||
--- a/diff-color-fixture/16-content-to-empty.txt
|
||||
+++ b/diff-color-fixture/16-content-to-empty.txt
|
||||
@@ -1,3 +0,0 @@
|
||||
-This tracked file keeps existing but loses all content.
|
||||
-Every row should render as a deletion.
|
||||
-The file itself is not deleted.
|
||||
diff --git a/diff-color-fixture/17-no-newline.txt b/diff-color-fixture/17-no-newline.txt
|
||||
index 51b84a61e2..dc6945a1fa 100644
|
||||
--- a/diff-color-fixture/17-no-newline.txt
|
||||
+++ b/diff-color-fixture/17-no-newline.txt
|
||||
@@ -1,2 +1,2 @@
|
||||
First line
|
||||
-Last line without trailing newline
|
||||
\ No newline at end of file
|
||||
+Last line now with trailing newline
|
||||
diff --git a/diff-color-fixture/18-page.html b/diff-color-fixture/18-page.html
|
||||
index bc1b2d9daa..9f4df11c24 100644
|
||||
--- a/diff-color-fixture/18-page.html
|
||||
+++ b/diff-color-fixture/18-page.html
|
||||
@@ -2,13 +2,14 @@
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
- <title>Review</title>
|
||||
+ <meta name="viewport" content="width=device-width" />
|
||||
+ <title>Review changes</title>
|
||||
</head>
|
||||
- <body class="light">
|
||||
- <main id="app" data-state="idle">
|
||||
- <h1>Old heading</h1>
|
||||
- <button type="button" disabled>Continue</button>
|
||||
- <!-- baseline comment -->
|
||||
+ <body class="dark">
|
||||
+ <main id="app" data-state="ready">
|
||||
+ <h1>New heading</h1>
|
||||
+ <button type="button">Continue</button>
|
||||
+ <!-- edited comment -->
|
||||
</main>
|
||||
</body>
|
||||
</html>
|
||||
diff --git a/diff-color-fixture/19-query.sql b/diff-color-fixture/19-query.sql
|
||||
index b078ad0869..88b64001e0 100644
|
||||
--- a/diff-color-fixture/19-query.sql
|
||||
+++ b/diff-color-fixture/19-query.sql
|
||||
@@ -1,6 +1,6 @@
|
||||
-SELECT id, title, status
|
||||
+SELECT id, title, status, author_id
|
||||
FROM reviews
|
||||
-WHERE status = 'pending'
|
||||
- AND created_at > NOW() - INTERVAL '7 days'
|
||||
-ORDER BY created_at DESC
|
||||
-LIMIT 50;
|
||||
+WHERE status IN ('pending', 'blocked')
|
||||
+ AND created_at > NOW() - INTERVAL '14 days'
|
||||
+ORDER BY updated_at DESC
|
||||
+LIMIT 100;
|
||||
|
Before Width: | Height: | Size: 117 KiB |
|
Before Width: | Height: | Size: 222 KiB |
|
Before Width: | Height: | Size: 309 KiB |
|
Before Width: | Height: | Size: 293 KiB |
|
Before Width: | Height: | Size: 323 KiB |
|
Before Width: | Height: | Size: 210 KiB |
|
Before Width: | Height: | Size: 298 KiB |
|
Before Width: | Height: | Size: 252 KiB |
|
Before Width: | Height: | Size: 250 KiB |
|
Before Width: | Height: | Size: 290 KiB |
|
Before Width: | Height: | Size: 110 KiB |
|
Before Width: | Height: | Size: 197 KiB |
|
Before Width: | Height: | Size: 109 KiB |
|
Before Width: | Height: | Size: 194 KiB |
|
Before Width: | Height: | Size: 109 KiB |
|
Before Width: | Height: | Size: 195 KiB |
|
Before Width: | Height: | Size: 109 KiB |
|
Before Width: | Height: | Size: 194 KiB |
@@ -1 +0,0 @@
|
||||
local.json
|
||||
@@ -1,697 +0,0 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<title>Diff color tuner</title>
|
||||
<style>
|
||||
:root {
|
||||
color-scheme: light dark;
|
||||
--bg: light-dark(#fafafa, #121212);
|
||||
--panel: light-dark(#ffffff, #1b1b1b);
|
||||
--line: light-dark(#e6e6e6, #2c2c2c);
|
||||
--text: light-dark(#161616, #ededed);
|
||||
--muted: light-dark(#6b6b6b, #9a9a9a);
|
||||
--accent: #3b5cf6;
|
||||
font: 13px/20px Inter, ui-sans-serif, system-ui, -apple-system, sans-serif;
|
||||
}
|
||||
* { box-sizing: border-box; }
|
||||
body { margin: 0; background: var(--bg); color: var(--text); }
|
||||
header {
|
||||
position: sticky; top: 0; z-index: 5; display: flex; gap: 12px; align-items: center; flex-wrap: wrap;
|
||||
padding: 12px 16px; background: var(--panel); border-bottom: 1px solid var(--line);
|
||||
}
|
||||
header h1 { font-size: 14px; line-height: 20px; margin: 0 8px 0 0; font-weight: 600; }
|
||||
button, select, input[type="text"] {
|
||||
font: inherit; color: inherit; background: var(--panel); border: 1px solid var(--line);
|
||||
border-radius: 6px; height: 28px; padding: 0 8px;
|
||||
}
|
||||
button { cursor: default; }
|
||||
button:hover { background: light-dark(#f2f2f2, #242424); }
|
||||
label.toggle { display: inline-flex; gap: 6px; align-items: center; }
|
||||
#status { color: var(--muted); margin-left: auto; font-variant-numeric: tabular-nums; }
|
||||
main { padding: 12px 16px 48px; max-width: 1180px; }
|
||||
h2 { font-size: 12px; line-height: 16px; text-transform: uppercase; letter-spacing: .04em; color: var(--muted); margin: 20px 0 8px; font-weight: 600; }
|
||||
dialog { width: min(720px, calc(100vw - 32px)); padding: 16px; color: var(--text); background: var(--panel); border: 1px solid var(--line); border-radius: 10px; box-shadow: 0 12px 32px #0004; }
|
||||
.sets { display: flex; flex-wrap: wrap; gap: 8px; align-items: center; margin: 4px 0 8px; }
|
||||
.sets label { font-weight: 600; }
|
||||
#set { min-width: 200px; }
|
||||
#name-input { width: 100%; margin: 8px 0 12px; }
|
||||
dialog::backdrop { background: #0006; }
|
||||
dialog h3 { margin: 0 0 4px; font-size: 14px; line-height: 20px; }
|
||||
#paste-text { width: 100%; height: 280px; margin-top: 8px; padding: 8px; resize: vertical; color: var(--text); background: var(--bg);
|
||||
border: 1px solid var(--line); border-radius: 6px; font: 12px/18px "IBM Plex Mono", ui-monospace, monospace; }
|
||||
#paste-result { min-height: 16px; }
|
||||
#paste-result[data-error] { color: #d92e3c; }
|
||||
.dialog-actions { display: flex; justify-content: flex-end; gap: 8px; }
|
||||
button.primary { background: var(--accent); border-color: var(--accent); color: #fff; }
|
||||
h2 { display: flex; align-items: center; gap: 12px; }
|
||||
.group-toggle { text-transform: none; letter-spacing: 0; font-weight: 500; font-size: 12px; color: var(--text); }
|
||||
.grid.off { opacity: .45; }
|
||||
.grid { display: grid; grid-template-columns: minmax(220px, 1.1fr) 1fr 1fr; border: 1px solid var(--line); border-radius: 8px; background: var(--panel); overflow: hidden; }
|
||||
.grid > div { padding: 8px 10px; border-top: 1px solid var(--line); min-width: 0; }
|
||||
.grid > .head { border-top: 0; font-weight: 600; color: var(--muted); font-size: 12px; line-height: 16px; }
|
||||
.slot-label { font-weight: 500; }
|
||||
.slot-help { color: var(--muted); font-size: 12px; line-height: 16px; }
|
||||
.cell { display: flex; flex-wrap: wrap; gap: 6px; align-items: center; }
|
||||
.cell[data-mode="dark"] { background: light-dark(#f6f6f6, #161616); }
|
||||
.swatch { width: 28px; height: 28px; border-radius: 6px; border: 1px solid var(--line); flex: none;
|
||||
background-image: linear-gradient(45deg, #ccc 25%, transparent 25%, transparent 75%, #ccc 75%), linear-gradient(45deg, #ccc 25%, transparent 25%, transparent 75%, #ccc 75%);
|
||||
background-size: 8px 8px; background-position: 0 0, 4px 4px; position: relative; overflow: hidden; }
|
||||
.swatch::after { content: ""; position: absolute; inset: 0; background: var(--swatch, transparent); }
|
||||
.swatch[data-current]::after { background: repeating-linear-gradient(-45deg, transparent 0 4px, light-dark(#0001, #fff1) 4px 8px); }
|
||||
input[type="color"] { width: 36px; height: 28px; padding: 0 2px; border: 1px solid var(--line); border-radius: 6px; background: var(--panel); }
|
||||
input[type="range"] { width: 90px; }
|
||||
button.token { display: inline-flex; align-items: center; gap: 6px; min-width: 200px; max-width: 100%; text-align: left; }
|
||||
button.token .token-name { flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font: 12px/16px "IBM Plex Mono", ui-monospace, monospace; }
|
||||
button.token .caret { color: var(--muted); }
|
||||
.mini { width: 14px; height: 14px; border-radius: 3px; border: 1px solid var(--line); background: var(--swatch, transparent); flex: none; }
|
||||
#picker { position: fixed; z-index: 50; width: 560px; max-width: calc(100vw - 16px); max-height: min(520px, calc(100vh - 16px)); display: flex; flex-direction: column;
|
||||
background: var(--panel); border: 1px solid var(--line); border-radius: 10px; box-shadow: 0 12px 32px #0003; }
|
||||
.picker-head { display: flex; gap: 8px; align-items: center; padding: 8px; border-bottom: 1px solid var(--line); }
|
||||
#picker-search { flex: 1; }
|
||||
#picker-mode { color: var(--muted); font-size: 12px; }
|
||||
#picker-body { overflow: auto; padding: 4px 8px 8px; }
|
||||
.family { margin-top: 8px; }
|
||||
.family-name { color: var(--muted); font-size: 11px; line-height: 16px; text-transform: uppercase; letter-spacing: .04em; margin-bottom: 4px; }
|
||||
.chips { display: flex; flex-wrap: wrap; gap: 4px; }
|
||||
.chip { width: 36px; height: 28px; padding: 0; border-radius: 5px; border: 1px solid var(--line); position: relative; overflow: hidden;
|
||||
background-image: linear-gradient(45deg, #ccc 25%, transparent 25%, transparent 75%, #ccc 75%), linear-gradient(45deg, #ccc 25%, transparent 25%, transparent 75%, #ccc 75%);
|
||||
background-size: 8px 8px; background-position: 0 0, 4px 4px; }
|
||||
.chip::after { content: ""; position: absolute; inset: 0; background: var(--swatch); }
|
||||
.chip span { position: absolute; z-index: 1; inset: auto 0 1px; font-size: 9px; line-height: 10px; text-align: center; color: var(--chip-text); }
|
||||
.chip[aria-selected="true"] { outline: 2px solid var(--accent); outline-offset: 1px; }
|
||||
.rows { display: grid; grid-template-columns: 1fr 1fr; gap: 2px 8px; }
|
||||
.row { display: flex; align-items: center; gap: 8px; height: 28px; padding: 0 6px; border: 0; background: transparent; text-align: left; min-width: 0; }
|
||||
.row:hover, .row[aria-selected="true"] { background: light-dark(#f0f0f0, #262626); }
|
||||
.row[aria-selected="true"] { box-shadow: inset 0 0 0 1px var(--accent); }
|
||||
.row .name { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font: 12px/16px "IBM Plex Mono", ui-monospace, monospace; }
|
||||
.alpha { display: inline-flex; align-items: center; gap: 2px; color: var(--muted); }
|
||||
.alpha-input { width: 56px; height: 28px; padding: 0 6px; font: inherit; font-variant-numeric: tabular-nums; color: var(--text);
|
||||
background: var(--panel); border: 1px solid var(--line); border-radius: 6px; text-align: right; }
|
||||
.hidden { display: none !important; }
|
||||
pre { background: var(--panel); border: 1px solid var(--line); border-radius: 8px; padding: 12px; overflow: auto; font: 12px/18px "IBM Plex Mono", ui-monospace, monospace; max-height: 360px; }
|
||||
.note { color: var(--muted); max-width: 820px; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<header>
|
||||
<h1>Diff color tuner</h1>
|
||||
<label class="toggle"><input type="checkbox" id="enabled" checked /> Overrides on</label>
|
||||
<button id="undo" disabled title="Undo last change (⌘Z)">Undo</button>
|
||||
<button id="redo" disabled title="Redo (⇧⌘Z)">Redo</button>
|
||||
<button id="reset">Reset all</button>
|
||||
<button id="copy">Copy CSS</button>
|
||||
<button id="paste">Paste CSS</button>
|
||||
<span id="status">Loading…</span>
|
||||
</header>
|
||||
<main>
|
||||
<div class="sets">
|
||||
<label for="set">Override set</label>
|
||||
<select id="set"></select>
|
||||
<button id="set-new">New</button>
|
||||
<button id="set-duplicate">Duplicate</button>
|
||||
<button id="set-rename">Rename</button>
|
||||
<button id="set-delete">Delete</button>
|
||||
<span id="set-note" class="slot-help"></span>
|
||||
</div>
|
||||
<p class="note">
|
||||
Each change is pushed live into open diff views in the dev app and saved to
|
||||
<code>packages/session-ui/src/pierre/diff-color-tuning.ts</code>. “Current” leaves the branch's colors untouched.
|
||||
Picked fills are exact final colors; tokens resolve per mode from OpenCode v2 tokens.
|
||||
</p>
|
||||
<div id="groups"></div>
|
||||
<h2>Generated CSS</h2>
|
||||
<pre id="css"></pre>
|
||||
</main>
|
||||
<dialog id="name-dialog">
|
||||
<form method="dialog">
|
||||
<h3 id="name-title">Name</h3>
|
||||
<input type="text" id="name-input" spellcheck="false" autocomplete="off" />
|
||||
<div class="dialog-actions">
|
||||
<button value="cancel">Cancel</button>
|
||||
<button id="name-ok" value="ok" class="primary">Save</button>
|
||||
</div>
|
||||
</form>
|
||||
</dialog>
|
||||
<dialog id="paste-dialog">
|
||||
<form method="dialog">
|
||||
<h3>Paste CSS</h3>
|
||||
<p class="slot-help">Paste CSS copied from this tuner. It replaces the current settings; Undo restores them.</p>
|
||||
<textarea id="paste-text" spellcheck="false" placeholder="/* diff-color-tuner: … */"></textarea>
|
||||
<p id="paste-result" class="slot-help"></p>
|
||||
<div class="dialog-actions">
|
||||
<button value="cancel">Cancel</button>
|
||||
<button id="paste-apply" value="apply" class="primary">Apply</button>
|
||||
</div>
|
||||
</form>
|
||||
</dialog>
|
||||
<div id="picker" class="hidden" role="dialog" aria-label="Choose a v2 token">
|
||||
<div class="picker-head">
|
||||
<input type="text" id="picker-search" placeholder="Search tokens, e.g. red, state, 1200" spellcheck="false" autocomplete="off" />
|
||||
<span id="picker-mode"></span>
|
||||
</div>
|
||||
<div id="picker-body"></div>
|
||||
</div>
|
||||
<script type="module">
|
||||
const LIGHT = ':host(:not([data-color-scheme="dark"]))'
|
||||
const DARK = ':host([data-color-scheme="dark"])'
|
||||
const ADD = '[data-line-type="change-addition"]'
|
||||
const DEL = '[data-line-type="change-deletion"]'
|
||||
const CONTEXT = ':is([data-line-type="context"], [data-line-type="context-expanded"])'
|
||||
const ROW = ':is([data-line], [data-no-newline])'
|
||||
const GUTTER = ':is([data-column-number], [data-gutter-buffer])'
|
||||
const exactRow = (sel) => (host, v) => `${host} ${sel} { --diffs-diff-line-mix-target: ${v}; --mix-light: 0%; --mix-dark: 0%; }`
|
||||
const hostVar = (name) => (host, v) => `${host} { ${name}: ${v}; }`
|
||||
const scopedVar = (name) => (host, v) => `${host} :is([data-diff], [data-file]) { ${name}: ${v}; }`
|
||||
const direct = (sel, decl) => (host, v) => `${host} ${sel} { ${decl(v)} }`
|
||||
// Syntax colors are inline style attributes on the token spans, so only !important can override them.
|
||||
const inlineText = (line) => (host, v) =>
|
||||
`${host} ${line} [data-diff-span], ${host} ${line} [data-diff-span] * { color: ${v} !important; }`
|
||||
|
||||
const GROUPS = [
|
||||
["Backgrounds", [
|
||||
{ id: "bg", label: "File background", help: "--diffs-bg. Base for context rows and Pierre's derived mixes.", rule: hostVar("--diffs-bg"), light: "v2-grey-50", dark: "v2-grey-1100" },
|
||||
{ id: "context-row", label: "Unchanged row", help: "Context code rows.", rule: exactRow(`${CONTEXT}${ROW}`), light: "v2-grey-50", dark: "v2-grey-1100" },
|
||||
{ id: "context-gutter", label: "Unchanged gutter", help: "Context line-number cells.", rule: exactRow(`${CONTEXT}${GUTTER}`), light: "v2-grey-50", dark: "v2-grey-1100" },
|
||||
{ id: "fold-row", label: "Folded “unmodified lines” row", help: "--diffs-bg-separator-override.", rule: hostVar("--diffs-bg-separator-override"), light: "v2-grey-100", dark: "v2-grey-1000" },
|
||||
{ id: "buffer-bg", label: "Split empty side", help: "--diffs-bg-context-gutter-override (blank side of split rows).", rule: hostVar("--diffs-bg-context-gutter-override"), light: "v2-grey-100", dark: "v2-grey-1000" },
|
||||
{ id: "buffer-hatch", label: "Split empty side hatch", help: "--diffs-bg-buffer-override (Pierre stripes; OpenCode currently hides them).", rule: hostVar("--diffs-bg-buffer-override"), light: "v2-grey-200", dark: "v2-grey-900" },
|
||||
{ id: "hover", label: "Hover tint", help: "--diffs-bg-hover-override. Mixed in at ~3% light / ~9% dark, not exact.", rule: hostVar("--diffs-bg-hover-override"), light: "v2-grey-1200", dark: "v2-grey-50" },
|
||||
{ id: "selection", label: "Selected lines", help: "--diffs-selection-base (row and number tints derive from it).", rule: scopedVar("--diffs-selection-base"), light: "v2-background-bg-accent", dark: "v2-background-bg-accent" },
|
||||
{ id: "comment", label: "Comment annotation", help: "--diffs-comment-bg.", rule: scopedVar("--diffs-comment-bg"), light: "v2-blue-100", dark: "v2-blue-1200" },
|
||||
]],
|
||||
["Additions", [
|
||||
{ id: "add-row", label: "Added row", help: "Exact fill of added code rows.", rule: exactRow(`${ADD}${ROW}`), light: "v2-green-100", dark: "v2-green-1200" },
|
||||
{ id: "add-gutter", label: "Added gutter", help: "Exact fill of added line-number cells.", rule: exactRow(`${ADD}${GUTTER}`), light: "v2-green-100", dark: "v2-green-1200" },
|
||||
{ id: "add-inline", label: "Added inline highlight", help: "Word/char emphasis span, painted over the row.", rule: direct(`${ADD} [data-diff-span]`, (v) => `background-color: ${v};`), light: "v2-green-200", dark: "v2-green-1100" },
|
||||
{ id: "add-inline-text", label: "Added inline text", help: "Text color inside the highlight. Replaces syntax colors there.", rule: inlineText(ADD), light: "v2-green-1000", dark: "v2-green-300" },
|
||||
{ id: "add-bar", label: "Added change bar", help: "2px gutter bar.", rule: direct(`[data-column-number]${ADD}::before`, (v) => `background-color: ${v}; background-image: none;`), light: "v2-state-fg-success", dark: "v2-state-fg-success" },
|
||||
{ id: "add-number", label: "Added line-number text", help: "--diffs-fg-number-addition-override.", rule: hostVar("--diffs-fg-number-addition-override"), light: "v2-state-fg-success", dark: "v2-state-fg-success" },
|
||||
{ id: "add-seed", label: "Addition seed", help: "--diffs-addition-color-override. Feeds anything above left on Current.", rule: hostVar("--diffs-addition-color-override"), light: "v2-state-fg-success", dark: "v2-state-fg-success" },
|
||||
]],
|
||||
["Deletions", [
|
||||
{ id: "del-row", label: "Deleted row", help: "Exact fill of deleted code rows.", rule: exactRow(`${DEL}${ROW}`), light: "v2-red-100", dark: "v2-red-1200" },
|
||||
{ id: "del-gutter", label: "Deleted gutter", help: "Exact fill of deleted line-number cells.", rule: exactRow(`${DEL}${GUTTER}`), light: "v2-red-100", dark: "v2-red-1200" },
|
||||
{ id: "del-inline", label: "Deleted inline highlight", help: "Word/char emphasis span, painted over the row.", rule: direct(`${DEL} [data-diff-span]`, (v) => `background-color: ${v};`), light: "v2-red-200", dark: "v2-red-1100" },
|
||||
{ id: "del-inline-text", label: "Deleted inline text", help: "Text color inside the highlight. Replaces syntax colors there.", rule: inlineText(DEL), light: "v2-red-1000", dark: "v2-red-300" },
|
||||
{ id: "del-bar", label: "Deleted change bar", help: "2px gutter bar.", rule: direct(`[data-column-number]${DEL}::before`, (v) => `background-color: ${v}; background-image: none;`), light: "v2-state-fg-danger", dark: "v2-state-fg-danger" },
|
||||
{ id: "del-number", label: "Deleted line-number text", help: "--diffs-fg-number-deletion-override.", rule: hostVar("--diffs-fg-number-deletion-override"), light: "v2-state-fg-danger", dark: "v2-state-fg-danger" },
|
||||
{ id: "del-seed", label: "Deletion seed", help: "--diffs-deletion-color-override. Feeds anything above left on Current.", rule: hostVar("--diffs-deletion-color-override"), light: "v2-state-fg-danger", dark: "v2-state-fg-danger" },
|
||||
]],
|
||||
["Text", [
|
||||
{ id: "number", label: "Unchanged line-number text", help: "--diffs-fg-number-override. Also fold labels unless set below.", rule: hostVar("--diffs-fg-number-override"), light: "v2-text-text-faint", dark: "v2-text-text-faint" },
|
||||
{ id: "fold-text", label: "Fold label and expand icon", help: "“N unmodified lines” text and expand button.", rule: direct(":is([data-separator-content], [data-expand-button])", (v) => `color: ${v};`), light: "v2-text-text-muted", dark: "v2-text-text-muted" },
|
||||
]],
|
||||
]
|
||||
const SLOTS = GROUPS.flatMap((group) => group[1])
|
||||
|
||||
let tokens = []
|
||||
let tokenMap = new Map()
|
||||
let state = { enabled: true, slots: {} }
|
||||
let liveTimer, saveTimer, liveBusy = false, livePending = false
|
||||
|
||||
const $ = (sel) => document.querySelector(sel)
|
||||
const status = (text) => ($("#status").textContent = text)
|
||||
|
||||
function hex8(hex, alpha) {
|
||||
return alpha >= 100 ? hex : hex + Math.round((alpha / 100) * 255).toString(16).padStart(2, "0")
|
||||
}
|
||||
function tokenHex(name, mode) {
|
||||
const value = tokenMap.get(name)?.[mode]
|
||||
return value && /^#[0-9a-f]{6}/i.test(value) ? value.slice(0, 7) : "#888888"
|
||||
}
|
||||
function valueOf(entry) {
|
||||
if (!entry || entry.kind === "current") return null
|
||||
if (entry.kind === "color") return hex8(entry.hex, entry.alpha)
|
||||
if (!tokenMap.has(entry.token)) return null
|
||||
return entry.alpha >= 100 ? `var(--${entry.token})` : `rgb(from var(--${entry.token}) r g b / ${entry.alpha / 100})`
|
||||
}
|
||||
function swatchOf(entry, mode) {
|
||||
if (!entry || entry.kind === "current") return null
|
||||
if (entry.kind === "color") return hex8(entry.hex, entry.alpha)
|
||||
const value = tokenMap.get(entry.token)?.[mode]
|
||||
if (!value) return null
|
||||
return entry.alpha >= 100 ? value : `rgb(from ${value} r g b / ${entry.alpha / 100})`
|
||||
}
|
||||
|
||||
function buildCSS() {
|
||||
if (!state.enabled) return ""
|
||||
const active = GROUPS.filter(([title]) => !state.disabledGroups?.[title]).flatMap((group) => group[1])
|
||||
return [["light", LIGHT], ["dark", DARK]]
|
||||
.flatMap(([mode, host]) =>
|
||||
active.flatMap((slot) => {
|
||||
const value = valueOf(state.slots[slot.id]?.[mode])
|
||||
return value ? [`/* ${mode}: ${slot.label} */\n${slot.rule(host, value)}`] : []
|
||||
}),
|
||||
)
|
||||
.join("\n")
|
||||
}
|
||||
|
||||
function entryFor(slot, mode) {
|
||||
state.slots[slot.id] ??= {}
|
||||
state.slots[slot.id][mode] ??= { kind: "current" }
|
||||
return state.slots[slot.id][mode]
|
||||
}
|
||||
|
||||
function render() {
|
||||
const root = $("#groups")
|
||||
root.textContent = ""
|
||||
for (const [title, slots] of GROUPS) {
|
||||
const heading = document.createElement("h2")
|
||||
heading.innerHTML = `<span></span><label class="toggle group-toggle"><input type="checkbox" /> <span>On</span></label>`
|
||||
heading.firstElementChild.textContent = title
|
||||
const enabled = !state.disabledGroups?.[title]
|
||||
const toggle = heading.querySelector("input")
|
||||
toggle.checked = enabled
|
||||
heading.querySelector(".group-toggle span").textContent = enabled ? "On" : "Off"
|
||||
toggle.onchange = () => {
|
||||
state.disabledGroups ??= {}
|
||||
if (toggle.checked) delete state.disabledGroups[title]
|
||||
else state.disabledGroups[title] = true
|
||||
render()
|
||||
changed(true)
|
||||
}
|
||||
const grid = document.createElement("div")
|
||||
grid.className = "grid"
|
||||
grid.classList.toggle("off", !enabled)
|
||||
grid.innerHTML = `<div class="head">Slot</div><div class="head">Light</div><div class="head">Dark</div>`
|
||||
for (const slot of slots) {
|
||||
const info = document.createElement("div")
|
||||
info.innerHTML = `<div class="slot-label"></div><div class="slot-help"></div>`
|
||||
info.children[0].textContent = slot.label
|
||||
info.children[1].textContent = slot.help
|
||||
grid.append(info, cell(slot, "light"), cell(slot, "dark"))
|
||||
}
|
||||
root.append(heading, grid)
|
||||
}
|
||||
$("#enabled").checked = state.enabled
|
||||
$("#css").textContent = buildCSS() || "/* No overrides */"
|
||||
}
|
||||
|
||||
function cell(slot, mode) {
|
||||
const entry = entryFor(slot, mode)
|
||||
const el = document.createElement("div")
|
||||
el.className = "cell"
|
||||
el.dataset.mode = mode
|
||||
el.innerHTML = `
|
||||
<span class="swatch"></span>
|
||||
<select class="kind"><option value="current">Current</option><option value="color">Color</option><option value="token">v2 token</option></select>
|
||||
<input type="color" class="hex" />
|
||||
<button type="button" class="token"><span class="mini"></span><span class="token-name"></span><span class="caret">▾</span></button>
|
||||
<input type="range" class="alpha-range" min="0" max="100" step="1" />
|
||||
<span class="alpha"><input type="number" class="alpha-input" min="0" max="100" step="any" aria-label="Opacity percent" /><span>%</span></span>`
|
||||
const swatch = el.querySelector(".swatch")
|
||||
const kind = el.querySelector(".kind")
|
||||
const hex = el.querySelector(".hex")
|
||||
const token = el.querySelector(".token")
|
||||
const alpha = el.querySelector(".alpha-range")
|
||||
const alphaText = el.querySelector(".alpha")
|
||||
const alphaInput = el.querySelector(".alpha-input")
|
||||
const sync = () => {
|
||||
kind.value = entry.kind
|
||||
hex.classList.toggle("hidden", entry.kind !== "color")
|
||||
token.classList.toggle("hidden", entry.kind !== "token")
|
||||
alpha.classList.toggle("hidden", entry.kind === "current")
|
||||
alphaText.classList.toggle("hidden", entry.kind === "current")
|
||||
if (entry.kind === "color") hex.value = entry.hex
|
||||
if (entry.kind === "token") {
|
||||
token.querySelector(".token-name").textContent = entry.token
|
||||
token.querySelector(".mini").style.setProperty("--swatch", tokenMap.get(entry.token)?.[mode] ?? "transparent")
|
||||
}
|
||||
alpha.value = entry.alpha ?? 100
|
||||
if (document.activeElement !== alphaInput) alphaInput.value = entry.alpha ?? 100
|
||||
const color = swatchOf(entry, mode)
|
||||
if (color) swatch.style.setProperty("--swatch", color)
|
||||
swatch.toggleAttribute("data-current", !color)
|
||||
swatch.title = entry.kind === "token" ? `${entry.token}: ${tokenMap.get(entry.token)?.[mode] ?? "unknown token"}` : (color ?? "Current (unchanged)")
|
||||
token.style.outline = entry.kind === "token" && !tokenMap.has(entry.token) ? "1px solid #d92e3c" : ""
|
||||
}
|
||||
kind.onchange = () => {
|
||||
const previous = { ...entry }
|
||||
entry.kind = kind.value
|
||||
entry.alpha ??= 100
|
||||
if (entry.kind === "color") entry.hex = previous.kind === "token" ? tokenHex(previous.token, mode) : (previous.hex ?? tokenHex(slot[mode], mode))
|
||||
if (entry.kind === "token") entry.token = previous.token ?? slot[mode]
|
||||
sync()
|
||||
changed(true)
|
||||
}
|
||||
hex.oninput = () => { entry.hex = hex.value; sync(); changed(false) }
|
||||
hex.onchange = () => changed(true)
|
||||
token.onclick = () => openPicker(token, mode, entry.token, (name) => { entry.token = name; sync(); changed(true) })
|
||||
alpha.oninput = () => { entry.alpha = Number(alpha.value); sync(); changed(false) }
|
||||
alpha.onchange = () => changed(true)
|
||||
alphaInput.oninput = () => {
|
||||
const value = Number(alphaInput.value)
|
||||
if (alphaInput.value === "" || !Number.isFinite(value)) return
|
||||
entry.alpha = Math.min(100, Math.max(0, value))
|
||||
sync()
|
||||
changed(false)
|
||||
}
|
||||
alphaInput.onchange = () => { alphaInput.value = entry.alpha ?? 100; changed(true) }
|
||||
alphaInput.onkeydown = (event) => { if (event.key === "Enter") alphaInput.blur() }
|
||||
sync()
|
||||
return el
|
||||
}
|
||||
|
||||
const RAMP_ORDER = ["grey", "red", "orange", "yellow", "green", "cyan", "blue", "purple", "pink", "alpha-dark", "alpha-light"]
|
||||
const picker = { anchor: null, mode: "light", selected: "", onPick: null }
|
||||
|
||||
function familyOf(name) {
|
||||
const ramp = /^v2-([a-z]+(?:-dark|-light)?)-(\d+)$/.exec(name)
|
||||
if (ramp && RAMP_ORDER.includes(ramp[1])) return { family: ramp[1], step: ramp[2], ramp: true }
|
||||
return { family: name.split("-")[1] ?? "other", step: "", ramp: false }
|
||||
}
|
||||
function readableOn(value) {
|
||||
const hex = /^#([0-9a-f]{6})/i.exec(value)?.[1]
|
||||
if (!hex) return "#000"
|
||||
const [r, g, b] = [0, 2, 4].map((i) => parseInt(hex.slice(i, i + 2), 16) / 255)
|
||||
return 0.2126 * r + 0.7152 * g + 0.0722 * b > 0.55 ? "#0009" : "#fffb"
|
||||
}
|
||||
|
||||
function openPicker(anchor, mode, selected, onPick) {
|
||||
Object.assign(picker, { anchor, mode, selected, onPick })
|
||||
$("#picker-mode").textContent = `${mode} values`
|
||||
$("#picker-search").value = ""
|
||||
$("#picker").classList.remove("hidden")
|
||||
renderPicker()
|
||||
placePicker()
|
||||
$("#picker-search").focus()
|
||||
const current = $(`#picker-body [aria-selected="true"]`)
|
||||
$("#picker-body").scrollTop = current ? current.offsetTop - $("#picker-body").offsetTop - 60 : 0
|
||||
}
|
||||
function closePicker() {
|
||||
$("#picker").classList.add("hidden")
|
||||
picker.anchor?.focus()
|
||||
picker.anchor = null
|
||||
}
|
||||
function placePicker() {
|
||||
const el = $("#picker")
|
||||
const rect = picker.anchor.getBoundingClientRect()
|
||||
const height = el.offsetHeight
|
||||
const top = rect.bottom + 4 + height > innerHeight ? Math.max(8, rect.top - 4 - height) : rect.bottom + 4
|
||||
el.style.top = `${top}px`
|
||||
el.style.left = `${Math.min(Math.max(8, rect.left), innerWidth - el.offsetWidth - 8)}px`
|
||||
}
|
||||
function renderPicker() {
|
||||
const terms = $("#picker-search").value.toLowerCase().split(/\s+/).filter(Boolean)
|
||||
const matches = tokens.filter((token) => terms.every((term) => token.name.includes(term)))
|
||||
const groups = new Map()
|
||||
for (const token of matches) {
|
||||
const info = familyOf(token.name)
|
||||
if (!groups.has(info.family)) groups.set(info.family, { ramp: info.ramp, items: [] })
|
||||
groups.get(info.family).items.push({ token, step: info.step })
|
||||
}
|
||||
const order = [...groups.keys()].sort((a, b) => {
|
||||
const ia = RAMP_ORDER.indexOf(a), ib = RAMP_ORDER.indexOf(b)
|
||||
if (ia !== -1 || ib !== -1) return (ia === -1 ? 99 : ia) - (ib === -1 ? 99 : ib)
|
||||
return a.localeCompare(b)
|
||||
})
|
||||
const body = $("#picker-body")
|
||||
body.textContent = ""
|
||||
if (!matches.length) body.innerHTML = `<p class="slot-help">No tokens match.</p>`
|
||||
for (const family of order) {
|
||||
const group = groups.get(family)
|
||||
const section = document.createElement("div")
|
||||
section.className = "family"
|
||||
const title = document.createElement("div")
|
||||
title.className = "family-name"
|
||||
title.textContent = family
|
||||
const list = document.createElement("div")
|
||||
list.className = group.ramp ? "chips" : "rows"
|
||||
for (const item of group.items) {
|
||||
const value = item.token[picker.mode]
|
||||
const button = document.createElement("button")
|
||||
button.type = "button"
|
||||
button.title = `${item.token.name}\nlight ${item.token.light}\ndark ${item.token.dark}`
|
||||
button.setAttribute("aria-selected", String(item.token.name === picker.selected))
|
||||
button.style.setProperty("--swatch", value)
|
||||
if (group.ramp) {
|
||||
button.className = "chip"
|
||||
button.style.setProperty("--chip-text", readableOn(value))
|
||||
button.innerHTML = `<span>${item.step}</span>`
|
||||
} else {
|
||||
button.className = "row"
|
||||
button.innerHTML = `<span class="mini"></span><span class="name"></span>`
|
||||
button.querySelector(".name").textContent = item.token.name.replace(/^v2-/, "")
|
||||
}
|
||||
button.onclick = () => {
|
||||
picker.onPick(item.token.name)
|
||||
closePicker()
|
||||
}
|
||||
list.append(button)
|
||||
}
|
||||
section.append(title, list)
|
||||
body.append(section)
|
||||
}
|
||||
}
|
||||
$("#picker-search").oninput = renderPicker
|
||||
$("#picker-search").onkeydown = (event) => {
|
||||
if (event.key === "Escape") closePicker()
|
||||
if (event.key === "Enter") $("#picker-body button")?.click()
|
||||
}
|
||||
document.addEventListener("mousedown", (event) => {
|
||||
if (!picker.anchor || $("#picker").contains(event.target) || picker.anchor.contains(event.target)) return
|
||||
closePicker()
|
||||
})
|
||||
window.addEventListener("resize", () => picker.anchor && placePicker())
|
||||
document.addEventListener("scroll", () => picker.anchor && placePicker(), true)
|
||||
|
||||
// History holds committed snapshots only, so one drag of a picker or slider is one undo step.
|
||||
const history = { undo: [], redo: [], committed: "" }
|
||||
function record() {
|
||||
const snapshot = JSON.stringify(state)
|
||||
if (snapshot === history.committed) return
|
||||
history.undo.push(history.committed)
|
||||
history.redo = []
|
||||
history.committed = snapshot
|
||||
syncHistory()
|
||||
}
|
||||
function travel(from, to) {
|
||||
if (!from.length) return
|
||||
to.push(history.committed)
|
||||
history.committed = from.pop()
|
||||
state = JSON.parse(history.committed)
|
||||
render()
|
||||
syncHistory()
|
||||
changed(false)
|
||||
clearTimeout(saveTimer)
|
||||
saveTimer = setTimeout(save, 150)
|
||||
}
|
||||
function syncHistory() {
|
||||
$("#undo").disabled = !history.undo.length
|
||||
$("#redo").disabled = !history.redo.length
|
||||
}
|
||||
$("#undo").onclick = () => travel(history.undo, history.redo)
|
||||
$("#redo").onclick = () => travel(history.redo, history.undo)
|
||||
document.addEventListener("keydown", (event) => {
|
||||
if (!(event.metaKey || event.ctrlKey) || event.key.toLowerCase() !== "z") return
|
||||
if (event.target instanceof HTMLInputElement && ["text", "number"].includes(event.target.type)) return
|
||||
event.preventDefault()
|
||||
if (event.shiftKey) travel(history.redo, history.undo)
|
||||
else travel(history.undo, history.redo)
|
||||
})
|
||||
|
||||
// Live pushes stay responsive while dragging; file writes are debounced to limit dev-server hot reloads.
|
||||
function changed(commit) {
|
||||
if (commit) record()
|
||||
const css = buildCSS()
|
||||
$("#css").textContent = css || "/* No overrides */"
|
||||
clearTimeout(liveTimer)
|
||||
liveTimer = setTimeout(() => live(css), 30)
|
||||
clearTimeout(saveTimer)
|
||||
saveTimer = setTimeout(save, commit ? 150 : 700)
|
||||
status("Saving…")
|
||||
}
|
||||
async function live(css) {
|
||||
if (liveBusy) { livePending = true; return }
|
||||
liveBusy = true
|
||||
await fetch("/api/live", { method: "POST", body: JSON.stringify({ css }) }).catch(() => {})
|
||||
liveBusy = false
|
||||
if (livePending) { livePending = false; live(buildCSS()) }
|
||||
}
|
||||
async function save() {
|
||||
// The committed reference set is read-only; the first edit forks it so the reference stays intact.
|
||||
if (current.readonly) {
|
||||
const name = `${state.name} copy`
|
||||
const created = await fetch("/api/sets", { method: "POST", body: JSON.stringify({ name, set: state }) }).then((r) => r.json())
|
||||
state.name = name
|
||||
state.readonly = false
|
||||
await loadSets(created.id)
|
||||
$("#set-note").textContent = `Reference is read-only, so your edits were saved as “${name}”.`
|
||||
}
|
||||
const response = await fetch(`/api/sets/${current.id}`, { method: "PUT", body: JSON.stringify({ set: state, css: buildCSS() }) }).catch(() => null)
|
||||
if (!response?.ok) return status("Save failed — is server.ts running?")
|
||||
const result = await response.json()
|
||||
status(`Saved ${new Date().toLocaleTimeString()} · live in ${result.live} diff view${result.live === 1 ? "" : "s"}`)
|
||||
}
|
||||
|
||||
$("#enabled").onchange = () => { state.enabled = $("#enabled").checked; render(); changed(true) }
|
||||
$("#reset").onclick = () => { if (!confirm("Reset every slot to Current?")) return; state = { ...state, enabled: true, disabledGroups: {}, slots: {} }; render(); changed(true) }
|
||||
// Shared CSS carries the exact settings in a header so pasting restores token choices, opacity and category toggles.
|
||||
function shareableCSS() {
|
||||
const css = buildCSS()
|
||||
if (!css) return ""
|
||||
const slots = Object.fromEntries(
|
||||
Object.entries(state.slots).flatMap(([id, modes]) => {
|
||||
const active = Object.fromEntries(Object.entries(modes).filter(([, entry]) => entry.kind !== "current"))
|
||||
return Object.keys(active).length ? [[id, active]] : []
|
||||
}),
|
||||
)
|
||||
const settings = { enabled: state.enabled, disabledGroups: state.disabledGroups ?? {}, slots }
|
||||
return `/* diff-color-tuner: ${JSON.stringify(settings)} */\n${css}`
|
||||
}
|
||||
|
||||
function parseShared(text) {
|
||||
// Chat apps often render "*…*" as Markdown emphasis and drop the asterisks, so don't require comment delimiters.
|
||||
// The settings JSON is always on one line, so a greedy match within the line is safe.
|
||||
const header = /diff-color-tuner:\s*(\{.*\})/.exec(text)
|
||||
if (header) {
|
||||
const settings = JSON.parse(header[1])
|
||||
const slots = Object.fromEntries(Object.entries(settings.slots ?? {}).filter(([id]) => SLOTS.some((slot) => slot.id === id)))
|
||||
return { state: { enabled: settings.enabled ?? true, disabledGroups: settings.disabledGroups ?? {}, slots }, count: Object.values(slots).reduce((sum, modes) => sum + Object.keys(modes).length, 0) }
|
||||
}
|
||||
// Fallback for CSS without the header: read each "/* mode: Label */" comment and the first color value after it.
|
||||
const slots = {}
|
||||
let count = 0
|
||||
for (const match of text.matchAll(/^\s*\/\*?\s*(light|dark):\s*(.+?)\s*\*?\/\s*\n\s*([^\n]+)/gm)) {
|
||||
const label = (value) => value.replace(/[“”"]/g, '"').replace(/\s*\*$/, "")
|
||||
const slot = SLOTS.find((candidate) => label(candidate.label) === label(match[2]))
|
||||
const entry = slot && parseValue(match[3].slice(match[3].indexOf("{")))
|
||||
if (!entry) continue
|
||||
slots[slot.id] ??= {}
|
||||
slots[slot.id][match[1]] = entry
|
||||
count++
|
||||
}
|
||||
return count ? { state: { enabled: true, disabledGroups: {}, slots }, count } : null
|
||||
}
|
||||
|
||||
function parseValue(rule) {
|
||||
const alphaToken = /rgb\(from var\(--([\w-]+)\) r g b \/ ([\d.]+)\)/.exec(rule)
|
||||
if (alphaToken && tokenMap.has(alphaToken[1])) return { kind: "token", token: alphaToken[1], alpha: Number(alphaToken[2]) * 100 }
|
||||
const token = /var\(--(v2-[\w-]+)\)/.exec(rule)
|
||||
if (token && tokenMap.has(token[1])) return { kind: "token", token: token[1], alpha: 100 }
|
||||
const hex = /#([0-9a-f]{6})([0-9a-f]{2})?\b/i.exec(rule)
|
||||
if (hex) return { kind: "color", hex: `#${hex[1].toLowerCase()}`, alpha: hex[2] ? Math.round((parseInt(hex[2], 16) / 255) * 1000) / 10 : 100 }
|
||||
return null
|
||||
}
|
||||
|
||||
$("#paste").onclick = () => {
|
||||
$("#paste-text").value = ""
|
||||
$("#paste-result").textContent = ""
|
||||
$("#paste-result").removeAttribute("data-error")
|
||||
$("#paste-dialog").showModal()
|
||||
$("#paste-text").focus()
|
||||
}
|
||||
$("#paste-apply").onclick = (event) => {
|
||||
let parsed = null
|
||||
try {
|
||||
parsed = parseShared($("#paste-text").value)
|
||||
} catch {}
|
||||
if (!parsed) {
|
||||
event.preventDefault()
|
||||
$("#paste-result").textContent = "No tuner rules found in that text."
|
||||
$("#paste-result").setAttribute("data-error", "")
|
||||
return
|
||||
}
|
||||
state = { ...parsed.state, name: state.name, readonly: state.readonly }
|
||||
render()
|
||||
changed(true)
|
||||
status(`Applied ${parsed.count} pasted override${parsed.count === 1 ? "" : "s"}`)
|
||||
}
|
||||
|
||||
$("#copy").onclick = async () => {
|
||||
const css = shareableCSS()
|
||||
if (!css) return status("Nothing to copy — no active overrides")
|
||||
const result = await fetch("/api/copy", { method: "POST", body: css }).then((r) => r.json()).catch(() => null)
|
||||
if (result?.ok) return status(`Copied ${css.split("\n").filter((line) => /^\/\* (light|dark):/.test(line)).length} rules`)
|
||||
const copied = await navigator.clipboard.writeText(css).then(() => true, () => false)
|
||||
status(copied ? "CSS copied" : "Copy failed — select the CSS at the bottom manually")
|
||||
}
|
||||
|
||||
let current = { id: "reference", readonly: true }
|
||||
|
||||
async function loadSets(selected) {
|
||||
const data = await fetch("/api/sets").then((r) => r.json())
|
||||
const id = selected ?? data.active
|
||||
$("#set").innerHTML = ""
|
||||
for (const set of data.sets) {
|
||||
const option = document.createElement("option")
|
||||
option.value = set.id
|
||||
option.textContent = set.readonly ? `${set.name} (read-only)` : set.name
|
||||
$("#set").append(option)
|
||||
}
|
||||
const match = data.sets.find((set) => set.id === id) ?? data.sets[0]
|
||||
current = { id: match.id, readonly: match.readonly }
|
||||
$("#set").value = current.id
|
||||
$("#set-rename").disabled = current.readonly
|
||||
$("#set-delete").disabled = current.readonly
|
||||
return current.id
|
||||
}
|
||||
|
||||
async function switchSet(id) {
|
||||
clearTimeout(saveTimer)
|
||||
await loadSets(id)
|
||||
state = await fetch(`/api/sets/${current.id}`).then((r) => r.json())
|
||||
history.undo = []
|
||||
history.redo = []
|
||||
history.committed = JSON.stringify(state)
|
||||
syncHistory()
|
||||
render()
|
||||
$("#set-note").textContent = current.readonly ? "Edits to the reference are saved as a new set." : ""
|
||||
const result = await fetch("/api/active", { method: "POST", body: JSON.stringify({ id: current.id, css: buildCSS() }) }).then((r) => r.json())
|
||||
status(`Showing “${state.name}” · live in ${result.live} diff view${result.live === 1 ? "" : "s"}`)
|
||||
}
|
||||
|
||||
// window.prompt isn't available in Electron, so names come from an in-page dialog.
|
||||
function askName(title, value) {
|
||||
return new Promise((resolve) => {
|
||||
const dialog = $("#name-dialog")
|
||||
$("#name-title").textContent = title
|
||||
$("#name-input").value = value
|
||||
dialog.onclose = () => resolve(dialog.returnValue === "ok" ? $("#name-input").value.trim() || null : null)
|
||||
dialog.returnValue = ""
|
||||
$("#name-input").onkeydown = (event) => {
|
||||
if (event.key !== "Enter") return
|
||||
event.preventDefault()
|
||||
dialog.close("ok")
|
||||
}
|
||||
dialog.showModal()
|
||||
$("#name-input").select()
|
||||
})
|
||||
}
|
||||
|
||||
async function createSet(name, set) {
|
||||
const created = await fetch("/api/sets", { method: "POST", body: JSON.stringify({ name, set }) }).then((r) => r.json())
|
||||
await switchSet(created.id)
|
||||
}
|
||||
|
||||
$("#set").onchange = () => switchSet($("#set").value)
|
||||
$("#set-new").onclick = async () => {
|
||||
const name = await askName("New override set", "My overrides")
|
||||
if (name) await createSet(name, { enabled: true, disabledGroups: {}, slots: {} })
|
||||
}
|
||||
$("#set-duplicate").onclick = async () => {
|
||||
const name = await askName("Duplicate set", `${state.name} copy`)
|
||||
if (name) await createSet(name, state)
|
||||
}
|
||||
$("#set-rename").onclick = async () => {
|
||||
const name = await askName("Rename set", state.name)
|
||||
if (!name) return
|
||||
state.name = name
|
||||
await save()
|
||||
await loadSets(current.id)
|
||||
}
|
||||
$("#set-delete").onclick = async () => {
|
||||
if (!confirm(`Delete “${state.name}”?`)) return
|
||||
await fetch(`/api/sets/${current.id}`, { method: "DELETE" })
|
||||
await switchSet("reference")
|
||||
}
|
||||
|
||||
const tokenList = await fetch("/api/tokens").then((r) => r.json())
|
||||
tokens = tokenList
|
||||
tokenMap = new Map(tokens.map((token) => [token.name, token]))
|
||||
await switchSet()
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,201 +0,0 @@
|
||||
// Temporary diff color tuner. Delete this directory, packages/session-ui/src/pierre/diff-color-tuning.ts,
|
||||
// and the matching import/interpolation in packages/session-ui/src/pierre/index.ts when finished.
|
||||
import path from "node:path"
|
||||
import { resolveThemeVariantV2 } from "../../packages/ui/src/theme/v2/resolve"
|
||||
|
||||
const root = path.resolve(import.meta.dir, "../..")
|
||||
const setsDir = path.join(import.meta.dir, "sets")
|
||||
const localPath = path.join(import.meta.dir, "local.json")
|
||||
const outputPath = path.join(root, "packages/session-ui/src/pierre/diff-color-tuning.ts")
|
||||
const cdp = process.env.TUNER_CDP ?? "http://127.0.0.1:9222"
|
||||
const port = Number(process.env.TUNER_PORT ?? 4455)
|
||||
|
||||
const theme = await Bun.file(path.join(root, "packages/ui/src/theme/themes/oc-2.json")).json()
|
||||
// Alpha ramps are static CSS, not part of the resolved theme.
|
||||
const alpha = Object.fromEntries(
|
||||
[...(await Bun.file(path.join(root, "packages/ui/src/styles/tokens/colors.css")).text()).matchAll(
|
||||
/--(v2-alpha-(?:dark|light)-\d+):\s*([^;]+);/g,
|
||||
)].map((match) => [match[1]!, match[2]!.trim()]),
|
||||
)
|
||||
const tokens = buildTokens()
|
||||
|
||||
const server = Bun.serve({
|
||||
port,
|
||||
hostname: "127.0.0.1",
|
||||
routes: {
|
||||
"/": () => new Response(Bun.file(path.join(import.meta.dir, "index.html"))),
|
||||
"/api/tokens": Response.json(tokens),
|
||||
"/api/sets": {
|
||||
GET: async () => Response.json({ active: await activeSet(), sets: await listSets() }),
|
||||
POST: async (request) => {
|
||||
const body = await request.json()
|
||||
const id = await uniqueID(body.name)
|
||||
await writeSet(id, { ...body.set, name: body.name, readonly: false })
|
||||
return Response.json({ id })
|
||||
},
|
||||
},
|
||||
"/api/sets/:id": {
|
||||
GET: async (request) => {
|
||||
const file = Bun.file(setPath(request.params.id))
|
||||
return (await file.exists()) ? Response.json(await file.json()) : new Response("Not found", { status: 404 })
|
||||
},
|
||||
PUT: async (request) => {
|
||||
const id = request.params.id
|
||||
const current = await readSet(id)
|
||||
if (current?.readonly) return new Response("Read-only set", { status: 403 })
|
||||
const body = await request.json()
|
||||
await writeSet(id, { ...body.set, name: body.set.name ?? current?.name ?? id, readonly: false })
|
||||
return Response.json(await apply(id, body.css))
|
||||
},
|
||||
DELETE: async (request) => {
|
||||
const current = await readSet(request.params.id)
|
||||
if (!current || current.readonly) return new Response("Cannot delete", { status: 403 })
|
||||
await Bun.file(setPath(request.params.id)).delete()
|
||||
return Response.json({ ok: true })
|
||||
},
|
||||
},
|
||||
"/api/active": {
|
||||
POST: async (request) => {
|
||||
const body = await request.json()
|
||||
return Response.json(await apply(body.id, body.css))
|
||||
},
|
||||
},
|
||||
// The embedded browser pane may deny navigator.clipboard, so copy through the OS clipboard instead.
|
||||
"/api/copy": {
|
||||
POST: async (request) => {
|
||||
const process = Bun.spawn(["pbcopy"], { stdin: new Blob([await request.text()]) })
|
||||
return Response.json({ ok: (await process.exited) === 0 })
|
||||
},
|
||||
},
|
||||
"/api/live": {
|
||||
POST: async (request) => Response.json({ live: await push((await request.json()).css) }),
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
console.log(`Diff color tuner: http://127.0.0.1:${server.port}`)
|
||||
|
||||
type OverrideSet = { name: string; readonly?: boolean; enabled: boolean; disabledGroups?: Record<string, boolean>; slots: Record<string, unknown> }
|
||||
|
||||
function setPath(id: string) {
|
||||
return path.join(setsDir, `${id.replace(/[^\w-]/g, "")}.json`)
|
||||
}
|
||||
|
||||
async function readSet(id: string): Promise<OverrideSet | undefined> {
|
||||
const file = Bun.file(setPath(id))
|
||||
return (await file.exists()) ? file.json() : undefined
|
||||
}
|
||||
|
||||
function writeSet(id: string, set: OverrideSet) {
|
||||
return Bun.write(setPath(id), JSON.stringify(set, null, 2) + "\n")
|
||||
}
|
||||
|
||||
async function listSets() {
|
||||
const ids = [...new Bun.Glob("*.json").scanSync(setsDir)].map((file) => file.slice(0, -5))
|
||||
const sets = await Promise.all(ids.map(async (id) => ({ id, ...(await readSet(id))! })))
|
||||
return sets
|
||||
.map((set) => ({ id: set.id, name: set.name, readonly: Boolean(set.readonly) }))
|
||||
.sort((a, b) => Number(b.readonly) - Number(a.readonly) || a.name.localeCompare(b.name))
|
||||
}
|
||||
|
||||
async function activeSet() {
|
||||
const file = Bun.file(localPath)
|
||||
const active = (await file.exists()) ? (await file.json()).active : undefined
|
||||
return active && (await readSet(active)) ? active : "reference"
|
||||
}
|
||||
|
||||
async function uniqueID(name: string) {
|
||||
const base = name.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "") || "set"
|
||||
const taken = new Set((await listSets()).map((set) => set.id))
|
||||
return Array.from({ length: 100 }, (_, i) => (i ? `${base}-${i + 1}` : base)).find((id) => !taken.has(id))!
|
||||
}
|
||||
|
||||
// Activating a set always rewrites the tuning module and pushes live CSS, so the dev app shows exactly the selected set.
|
||||
async function apply(id: string, css: string) {
|
||||
await Bun.write(localPath, JSON.stringify({ active: id }, null, 2) + "\n")
|
||||
await Bun.write(outputPath, renderModule(css))
|
||||
return { live: await push(css) }
|
||||
}
|
||||
|
||||
function renderModule(css: string) {
|
||||
const escaped = css.replaceAll("\\", "\\\\").replaceAll("`", "\\`").replaceAll("${", "\\${")
|
||||
return `// Temporary diff color tuning output. Generated by diff-color-review-artifacts/tuner; delete with the tuner.\nexport const diffColorTuningCSS = \`${escaped}\`\n`
|
||||
}
|
||||
|
||||
function buildTokens() {
|
||||
const light = { ...alpha, ...resolveThemeVariantV2(theme.light, false) }
|
||||
const dark = { ...alpha, ...resolveThemeVariantV2(theme.dark, true) }
|
||||
const resolve = (map: Record<string, string>, value: string, depth = 0): string => {
|
||||
const match = /^var\(--([\w-]+)\)$/.exec(value.trim())
|
||||
if (!match || depth > 8) return value
|
||||
return map[match[1]!] ? resolve(map, map[match[1]!]!, depth + 1) : value
|
||||
}
|
||||
return Object.keys(light)
|
||||
.filter((name) => !name.includes("elevation") && !name.includes("font"))
|
||||
.map((name) => ({ name, light: resolve(light, light[name]!), dark: resolve(dark, dark[name] ?? light[name]!) }))
|
||||
.filter((token) => /^(#|rgb|hsl|oklch)/.test(token.light))
|
||||
.sort((a, b) => a.name.localeCompare(b.name, undefined, { numeric: true }))
|
||||
}
|
||||
|
||||
// Live preview: Pierre reads unsafeCSS when a viewer mounts, so already-open diffs need the CSS pushed into their shadow roots.
|
||||
async function push(css: string) {
|
||||
const targets: { type: string; webSocketDebuggerUrl: string }[] = await fetch(`${cdp}/json/list`)
|
||||
.then((response) => response.json())
|
||||
.catch(() => [])
|
||||
const counts = await Promise.all(
|
||||
targets.filter((target) => target.type === "page").map((target) => evaluate(target.webSocketDebuggerUrl, injector(css))),
|
||||
)
|
||||
return counts.reduce<number>((sum, count) => sum + (typeof count === "number" ? count : 0), 0)
|
||||
}
|
||||
|
||||
function injector(css: string) {
|
||||
return `(() => {
|
||||
window.__diffColorTuning = ${JSON.stringify(css)}
|
||||
const apply = (host) => {
|
||||
const root = host.shadowRoot
|
||||
if (!root) return
|
||||
let style = root.querySelector("style[data-diff-color-tuning]")
|
||||
if (!style) {
|
||||
style = document.createElement("style")
|
||||
style.dataset.diffColorTuning = ""
|
||||
root.appendChild(style)
|
||||
}
|
||||
if (style.textContent !== window.__diffColorTuning) style.textContent = window.__diffColorTuning
|
||||
}
|
||||
const all = () => document.querySelectorAll("diffs-container").forEach(apply)
|
||||
all()
|
||||
if (!window.__diffColorTuningObserver) {
|
||||
let queued = false
|
||||
window.__diffColorTuningObserver = new MutationObserver(() => {
|
||||
if (queued) return
|
||||
queued = true
|
||||
requestAnimationFrame(() => { queued = false; all() })
|
||||
})
|
||||
window.__diffColorTuningObserver.observe(document.body, { childList: true, subtree: true })
|
||||
}
|
||||
return document.querySelectorAll("diffs-container").length
|
||||
})()`
|
||||
}
|
||||
|
||||
function evaluate(url: string, expression: string) {
|
||||
return new Promise<unknown>((resolve) => {
|
||||
const socket = new WebSocket(url)
|
||||
const timer = setTimeout(() => {
|
||||
socket.close()
|
||||
resolve(undefined)
|
||||
}, 2000)
|
||||
socket.onopen = () =>
|
||||
socket.send(JSON.stringify({ id: 1, method: "Runtime.evaluate", params: { expression, returnByValue: true } }))
|
||||
socket.onmessage = (event) => {
|
||||
const message = JSON.parse(String(event.data))
|
||||
if (message.id !== 1) return
|
||||
clearTimeout(timer)
|
||||
socket.close()
|
||||
resolve(message.result?.result?.value)
|
||||
}
|
||||
socket.onerror = () => {
|
||||
clearTimeout(timer)
|
||||
resolve(undefined)
|
||||
}
|
||||
})
|
||||
}
|
||||