mirror of
https://github.com/anomalyco/opencode.git
synced 2026-05-06 00:12:11 +00:00
Compare commits
553 commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f5c3d352a1 | ||
|
|
e117397d0f | ||
|
|
1fbc13a1b4 | ||
|
|
6409aceb1a | ||
|
|
837cc92586 | ||
|
|
ca77b8f8e9 | ||
|
|
25547e9337 | ||
|
|
12f3d1f505 | ||
|
|
8e182c7782 | ||
|
|
8a797ed9a1 | ||
|
|
25ecf0af6b | ||
|
|
576480b5dc | ||
|
|
fdb4b7c4a5 | ||
|
|
726ae6f541 | ||
|
|
773078e81f | ||
|
|
811954880e | ||
|
|
465c83cf82 | ||
|
|
bb9b81aa37 | ||
|
|
a20446fcc8 | ||
|
|
292c2aa458 | ||
|
|
52bb088753 | ||
|
|
b8f8f5d3a8 | ||
|
|
8df3ef10fc | ||
|
|
301ab36159 | ||
|
|
03544a26cd | ||
|
|
b4147c8d08 | ||
|
|
6f7d63e9ce | ||
|
|
07f1c8c0ac | ||
|
|
2d0a757eb2 | ||
|
|
75d141b574 | ||
|
|
39c88f9afb | ||
|
|
0df2bb0f3b | ||
|
|
f6a3615f59 | ||
|
|
edd480f56b | ||
|
|
2740d398fa | ||
|
|
f33b17e8ac | ||
|
|
22a4a9df8b | ||
|
|
84afd2bef8 | ||
|
|
ca2411d332 | ||
|
|
6b852774e1 | ||
|
|
f14784d531 | ||
|
|
6a5e329427 | ||
|
|
4b65b1e053 | ||
|
|
d431a0e4b4 | ||
|
|
5720883d5d | ||
|
|
007b57f078 | ||
|
|
fb07c2070c | ||
|
|
25dc6f09bc | ||
|
|
b70e2700ef | ||
|
|
1aed6b1d8b | ||
|
|
c1f607d206 | ||
|
|
2c819f290f | ||
|
|
6e9f10ad3f | ||
|
|
1251a870cb | ||
|
|
67047fa766 | ||
|
|
a366128a93 | ||
|
|
9f708e748a | ||
|
|
7bc26dafae | ||
|
|
ce89bcb8e2 | ||
|
|
c2b1974ddd | ||
|
|
ca6150d6f0 | ||
|
|
825ab2e38d | ||
|
|
755cd561ec | ||
|
|
6312c55d55 | ||
|
|
a9dc0fae3d | ||
|
|
7749d8e85f | ||
|
|
28112fbd12 | ||
|
|
387220f368 | ||
|
|
adb7cb1037 | ||
|
|
c06af70ab0 | ||
|
|
40dc2fa3c1 | ||
|
|
df7dd06a0f | ||
|
|
57d5c095d8 | ||
|
|
13ac849db5 | ||
|
|
8694c5b68f | ||
|
|
0a7d02c87c | ||
|
|
e77867ef05 | ||
|
|
fb224d8974 | ||
|
|
101566131d | ||
|
|
8433e8b433 | ||
|
|
379600b5ab | ||
|
|
7a503de606 | ||
|
|
2ad1eb56d3 | ||
|
|
a43f767abb | ||
|
|
0ee3b87289 | ||
|
|
3c9f3c5786 | ||
|
|
ca75ac6681 | ||
|
|
d1f597b5b5 | ||
|
|
8299fb3e2b | ||
|
|
4f7f90133d | ||
|
|
b205e104f6 | ||
|
|
252e2f98e6 | ||
|
|
e2afdc1202 | ||
|
|
a08e4c9651 | ||
|
|
7ccab8d272 | ||
|
|
fc57eb3b8e | ||
|
|
9179bafd54 | ||
|
|
2df8eda8a3 | ||
|
|
bd32252a7e | ||
|
|
1717d636a2 | ||
|
|
8e016b4703 | ||
|
|
b89d48a2a4 | ||
|
|
33312bfd1b | ||
|
|
3f1ce36418 | ||
|
|
0e13279545 | ||
|
|
5f03d892c0 | ||
|
|
bdabb102fe | ||
|
|
a79a6594b0 | ||
|
|
a3d282a4c2 | ||
|
|
db24f89313 | ||
|
|
31cb0bfa4f | ||
|
|
af9fdf0a1c | ||
|
|
be88cd5cb9 | ||
|
|
b4cc7d13b6 | ||
|
|
0ba013f8de | ||
|
|
0956b15c52 | ||
|
|
61150f6391 | ||
|
|
7409dcc6bd | ||
|
|
2829943ad1 | ||
|
|
c4311dda31 | ||
|
|
ad05a46d74 | ||
|
|
a6cadba814 | ||
|
|
a3bc5d35b0 | ||
|
|
1409a0715c | ||
|
|
e98c291866 | ||
|
|
e709dc34fb | ||
|
|
9293cddb3a | ||
|
|
68b3448b09 | ||
|
|
80f2b13a55 | ||
|
|
7d91d3b1ed | ||
|
|
a6464062b7 | ||
|
|
fd01dc9c89 | ||
|
|
d10fb88b66 | ||
|
|
6b68b1020e | ||
|
|
85bb9007ba | ||
|
|
9bef88e3b0 | ||
|
|
f98053c34e | ||
|
|
36007aecf4 | ||
|
|
4de44bbbef | ||
|
|
9d03d4419e | ||
|
|
7ab1c1c74a | ||
|
|
3f459819ba | ||
|
|
1986a6e817 | ||
|
|
dfe1325fca | ||
|
|
c1686c6ddc | ||
|
|
79b6ce5db4 | ||
|
|
0c816eb4b1 | ||
|
|
e318e173d8 | ||
|
|
b314781a1a | ||
|
|
8396d6b016 | ||
|
|
43e20874f4 | ||
|
|
c444e971b0 | ||
|
|
430bde9e9b | ||
|
|
05b82a6a30 | ||
|
|
6cd02c05c2 | ||
|
|
b3a7513765 | ||
|
|
f8738c9002 | ||
|
|
b460db15d7 | ||
|
|
ff4779ca11 | ||
|
|
146ff8ad85 | ||
|
|
0d0ec7dc46 | ||
|
|
1ea6e6cd4b | ||
|
|
96061222d2 | ||
|
|
3b9155714d | ||
|
|
7371db5cc6 | ||
|
|
b09b7d28b8 | ||
|
|
31ed4602e1 | ||
|
|
6a76346734 | ||
|
|
78b3000031 | ||
|
|
4c4860fb24 | ||
|
|
5242a1c6b4 | ||
|
|
075f876e6f | ||
|
|
a849812e9f | ||
|
|
d99dde6306 | ||
|
|
becf57ee6a | ||
|
|
f33aec1139 | ||
|
|
1571933096 | ||
|
|
160928a9a9 | ||
|
|
d297c29f22 | ||
|
|
0b498dd448 | ||
|
|
cec9c6122a | ||
|
|
51e310c9ce | ||
|
|
478156456e | ||
|
|
6252412d94 | ||
|
|
c2609cbf04 | ||
|
|
2115df57bf | ||
|
|
29ec07700c | ||
|
|
bcae852d28 | ||
|
|
16ddf5f559 | ||
|
|
8c79c58c4d | ||
|
|
97ed9ba624 | ||
|
|
a6b6395c8a | ||
|
|
21f8027ef7 | ||
|
|
a5aa72bd7d | ||
|
|
563177c6ac | ||
|
|
4eae8ec037 | ||
|
|
08895c396e | ||
|
|
4e451a4b0f | ||
|
|
163290bcf0 | ||
|
|
c68c33d4fe | ||
|
|
3615d8e226 | ||
|
|
2283979199 | ||
|
|
33f7f593ee | ||
|
|
461e7345b3 | ||
|
|
6bd91c68e8 | ||
|
|
ff55a40749 | ||
|
|
8b56d77ea1 | ||
|
|
dd3aa96730 | ||
|
|
8b56d1712f | ||
|
|
3c24d22d42 | ||
|
|
4c70ea28d2 | ||
|
|
5ba68a28c0 | ||
|
|
bce4def2db | ||
|
|
3544ea0244 | ||
|
|
6434918794 | ||
|
|
5984d917dc | ||
|
|
c2a97a7a6c | ||
|
|
a083c88e87 | ||
|
|
ce3b0988c4 | ||
|
|
e8a194a2bb | ||
|
|
8aa8798e07 | ||
|
|
6d4629b566 | ||
|
|
a9d399699e | ||
|
|
bc805b3001 | ||
|
|
668d77bb4e | ||
|
|
5c2e06f353 | ||
|
|
a499fe2b17 | ||
|
|
451650b584 | ||
|
|
1b76bec0e2 | ||
|
|
96f4da1e1d | ||
|
|
96a0dd6b04 | ||
|
|
2dd1f2d453 | ||
|
|
510f01674a | ||
|
|
e3134a2a99 | ||
|
|
8805104b8d | ||
|
|
fc155e9fc5 | ||
|
|
3aaac0098e | ||
|
|
a12333310f | ||
|
|
247284b9af | ||
|
|
e0305e47f3 | ||
|
|
76a0f0f619 | ||
|
|
560baae15d | ||
|
|
5518ecaefe | ||
|
|
924ba97055 | ||
|
|
b80f52f8ad | ||
|
|
feb275d08b | ||
|
|
fbcbd24063 | ||
|
|
3250b814ce | ||
|
|
0e9d9282c6 | ||
|
|
b315a70773 | ||
|
|
cedff6fb89 | ||
|
|
87cd9446d8 | ||
|
|
f4ce240a2e | ||
|
|
320527a3e4 | ||
|
|
19271fca2d | ||
|
|
feeebbe7d4 | ||
|
|
f384675c01 | ||
|
|
ec3ab4a00c | ||
|
|
e4ac936eb9 | ||
|
|
79e23b7eb9 | ||
|
|
92e80b4660 | ||
|
|
ce63ca4d7a | ||
|
|
fef7981942 | ||
|
|
ffe0314c47 | ||
|
|
375444a149 | ||
|
|
65c15afe9f | ||
|
|
8f57a2a462 | ||
|
|
53e9cac383 | ||
|
|
fe0c182747 | ||
|
|
29b1060c67 | ||
|
|
dddfcbf0d8 | ||
|
|
62e1335388 | ||
|
|
908e28175f | ||
|
|
3398fd7719 | ||
|
|
9bddf7f3ef | ||
|
|
8ba374fefa | ||
|
|
3ef0aaf768 | ||
|
|
d7701dbfb6 | ||
|
|
c49bf0b402 | ||
|
|
cee9610d26 | ||
|
|
38adc13295 | ||
|
|
4fe14abb8c | ||
|
|
9052e8a1ba | ||
|
|
de78dedceb | ||
|
|
6f508d574e | ||
|
|
61dfae31e7 | ||
|
|
ac6aa43e3b | ||
|
|
ea89925042 | ||
|
|
12cbfe5b64 | ||
|
|
d7b7be1909 | ||
|
|
a740d2c667 | ||
|
|
588261076a | ||
|
|
639e27c3ce | ||
|
|
1124ae17b4 | ||
|
|
9db5890ce5 | ||
|
|
293877cb7e | ||
|
|
c480006554 | ||
|
|
6aa8e894b1 | ||
|
|
00bb9836a6 | ||
|
|
71f9189607 | ||
|
|
a3f7ea2555 | ||
|
|
d3df8e1180 | ||
|
|
df147b65fd | ||
|
|
6015084fa2 | ||
|
|
65ba1f6c13 | ||
|
|
d37e5af57d | ||
|
|
d71b827d8c | ||
|
|
504ca3d3d8 | ||
|
|
a8c74c04de | ||
|
|
f6b4f54216 | ||
|
|
fc0e3c65b3 | ||
|
|
23b8ed788e | ||
|
|
3bd890f46b | ||
|
|
9fbeafb63e | ||
|
|
91bd295209 | ||
|
|
d4bf70be06 | ||
|
|
ae8904c4ff | ||
|
|
9209c04370 | ||
|
|
379e7f3f20 | ||
|
|
366d11e1f8 | ||
|
|
58836e75f0 | ||
|
|
0acac216ae | ||
|
|
276d162044 | ||
|
|
1b0ed983c5 | ||
|
|
2e8d690ab1 | ||
|
|
1ff8d289af | ||
|
|
d54ffbda1c | ||
|
|
c00058ed7a | ||
|
|
2c2fc3499b | ||
|
|
ea3c6c3481 | ||
|
|
9b68b7195a | ||
|
|
7739cc53b4 | ||
|
|
3fa78a8b01 | ||
|
|
e57d0c2fee | ||
|
|
2a4f2bf527 | ||
|
|
aa07f38b07 | ||
|
|
9d1f17d836 | ||
|
|
b420952e59 | ||
|
|
bb9e445257 | ||
|
|
528fb1d404 | ||
|
|
c8d9f7aa89 | ||
|
|
cd7ec93cdf | ||
|
|
796b652d2b | ||
|
|
4d74849c1a | ||
|
|
937a7c48a5 | ||
|
|
704eb00de4 | ||
|
|
bad4599bf9 | ||
|
|
892fd85ba7 | ||
|
|
0eaa47d857 | ||
|
|
faca24d487 | ||
|
|
c103202ad5 | ||
|
|
ce78a4265d | ||
|
|
c4a2353ac3 | ||
|
|
576efed196 | ||
|
|
dfc0075f90 | ||
|
|
acd15dcc8a | ||
|
|
139c4fd555 | ||
|
|
e0f3df8252 | ||
|
|
9cd2e3a1c3 | ||
|
|
f584f80219 | ||
|
|
45eac589f8 | ||
|
|
fab1768826 | ||
|
|
51fc10e407 | ||
|
|
7a1c8465f5 | ||
|
|
5290e9ca7e | ||
|
|
c361c2953f | ||
|
|
ccb7669736 | ||
|
|
f25f1485d5 | ||
|
|
55ecb06748 | ||
|
|
dc6991e5a8 | ||
|
|
738b3065dc | ||
|
|
26cc537cb1 | ||
|
|
ede354b0e6 | ||
|
|
61eabfc60c | ||
|
|
2789b770aa | ||
|
|
8718b98ee1 | ||
|
|
c8b2f987f9 | ||
|
|
52b55b826f | ||
|
|
e8c20235b8 | ||
|
|
17701628bd | ||
|
|
0efc6163f1 | ||
|
|
1e191ba815 | ||
|
|
f19d863689 | ||
|
|
4a1ef327ca | ||
|
|
025a6392ce | ||
|
|
e578c442be | ||
|
|
0cecb1bff2 | ||
|
|
5d8971c1ed | ||
|
|
a9b62d67df | ||
|
|
3525e61906 | ||
|
|
059e6c46db | ||
|
|
5cf195e0af | ||
|
|
244d1debe4 | ||
|
|
35734b42fe | ||
|
|
a3128e32c5 | ||
|
|
5f8a72bfc4 | ||
|
|
418a1cf5f3 | ||
|
|
60ebd074ac | ||
|
|
216dd363e8 | ||
|
|
141f33d24b | ||
|
|
c4d8a8183e | ||
|
|
58244eb687 | ||
|
|
e9071b0a80 | ||
|
|
c68907ece2 | ||
|
|
af3998c8a6 | ||
|
|
fad2618757 | ||
|
|
21e01dbe04 | ||
|
|
3beadeebff | ||
|
|
dcee1c3642 | ||
|
|
00d1a7e090 | ||
|
|
bbb56c2a88 | ||
|
|
5186c6964b | ||
|
|
79c66e353f | ||
|
|
41f5e8a861 | ||
|
|
c5b67927af | ||
|
|
301ecb185e | ||
|
|
151df05eeb | ||
|
|
55adcdfd07 | ||
|
|
daaa2e5911 | ||
|
|
daff119fe4 | ||
|
|
e0d1ff42c0 | ||
|
|
de413c56ae | ||
|
|
da61b0290a | ||
|
|
d03e6cedde | ||
|
|
7feb6ab962 | ||
|
|
854d4b7a53 | ||
|
|
aa5999b188 | ||
|
|
37c5eab6f8 | ||
|
|
6daa2b9aeb | ||
|
|
5a5a2e5fa0 | ||
|
|
95c43fc675 | ||
|
|
e7053c41f4 | ||
|
|
fc6d4b4010 | ||
|
|
2893588016 | ||
|
|
f2d4d816fb | ||
|
|
097d930668 | ||
|
|
7cab6824d1 | ||
|
|
f77277a69e | ||
|
|
450128f9be | ||
|
|
3e35c974a4 | ||
|
|
a14c22d4e9 | ||
|
|
58c65874ba | ||
|
|
27b0877714 | ||
|
|
5904f599a9 | ||
|
|
df9e1d9854 | ||
|
|
75a22f82bd | ||
|
|
a369130226 | ||
|
|
474024f9e6 | ||
|
|
b4f4134e81 | ||
|
|
cd64b67038 | ||
|
|
9af46df535 | ||
|
|
b749866f0b | ||
|
|
60fa708f0b | ||
|
|
3b74077437 | ||
|
|
95d4bb2130 | ||
|
|
f5dce6d960 | ||
|
|
1e98167b0e | ||
|
|
3eee2f6afa | ||
|
|
ff4b60e1f3 | ||
|
|
f91b73b938 | ||
|
|
05661c60ff | ||
|
|
625aca49de | ||
|
|
3bc0c36ace | ||
|
|
eb0219988b | ||
|
|
705f792e87 | ||
|
|
716cf74190 | ||
|
|
fc8dae2422 | ||
|
|
27353df0cc | ||
|
|
1a734adb4d | ||
|
|
a9740b9133 | ||
|
|
62651c7114 | ||
|
|
1d728fc627 | ||
|
|
62ef2a2207 | ||
|
|
37aa8442dc | ||
|
|
5b0e828c10 | ||
|
|
d5bfaef53d | ||
|
|
bad732c26a | ||
|
|
1b92c95425 | ||
|
|
d748c71845 | ||
|
|
fc88ed1262 | ||
|
|
66f93035b0 | ||
|
|
9ff999cc2b | ||
|
|
4877eccc0d | ||
|
|
f7d527cd28 | ||
|
|
e29058c346 | ||
|
|
49894330d9 | ||
|
|
cdc7d5f2ea | ||
|
|
ec201623fb | ||
|
|
386091b79a | ||
|
|
1e4b7b5451 | ||
|
|
5cd178ba70 | ||
|
|
97eb9fdee8 | ||
|
|
5a04de231e | ||
|
|
bb3509b5ff | ||
|
|
4a67905266 | ||
|
|
c4e33d3168 | ||
|
|
872cdff2ab | ||
|
|
361d7001d0 | ||
|
|
7a02eee0f4 | ||
|
|
cf45a8d807 | ||
|
|
435becbea0 | ||
|
|
0405bc74e9 | ||
|
|
4dab2a8555 | ||
|
|
3776d85e15 | ||
|
|
d01ad4c499 | ||
|
|
28025a0f36 | ||
|
|
1220f784fe | ||
|
|
28f7d31e72 | ||
|
|
66936b0fff | ||
|
|
3a5507de95 | ||
|
|
86715fecc4 | ||
|
|
3062d3e070 | ||
|
|
d89bfc32ac | ||
|
|
011c23761b | ||
|
|
a8c8d2dd79 | ||
|
|
9f7ecd65e5 | ||
|
|
f8e939d96f | ||
|
|
923af96d26 | ||
|
|
a882e958b3 | ||
|
|
2cda629c8d | ||
|
|
f033d2d8fb | ||
|
|
a4bd88ab97 | ||
|
|
f4616c8268 | ||
|
|
4712f0f3c1 | ||
|
|
6c1268f3b1 | ||
|
|
2e156b8990 | ||
|
|
3bfe6a1ef6 | ||
|
|
5c5069b622 | ||
|
|
f8c6ddd4cb | ||
|
|
e50a688ca3 | ||
|
|
334ab4707c | ||
|
|
87321942fe | ||
|
|
a771859362 | ||
|
|
31d01d404a | ||
|
|
814e83ffec | ||
|
|
4c3e65c877 | ||
|
|
98ea5b6e7e | ||
|
|
3f8c659056 | ||
|
|
3910a6e527 | ||
|
|
24892559ae | ||
|
|
cd93533b1f | ||
|
|
0590452456 | ||
|
|
93940a1859 | ||
|
|
1e439b8226 | ||
|
|
8b2f8355b2 | ||
|
|
aed03078f8 | ||
|
|
c50d65b4d6 | ||
|
|
353532b1c1 | ||
|
|
9df7c78ebe | ||
|
|
eb7555d3c6 | ||
|
|
2cd89d64e9 | ||
|
|
0517ab4695 | ||
|
|
bbf67d0fff |
1430 changed files with 74880 additions and 52149 deletions
1
.github/ISSUE_TEMPLATE/bug-report.yml
vendored
1
.github/ISSUE_TEMPLATE/bug-report.yml
vendored
|
|
@ -1,6 +1,5 @@
|
||||||
name: Bug report
|
name: Bug report
|
||||||
description: Report an issue that should be fixed
|
description: Report an issue that should be fixed
|
||||||
labels: ["bug"]
|
|
||||||
body:
|
body:
|
||||||
- type: textarea
|
- type: textarea
|
||||||
id: description
|
id: description
|
||||||
|
|
|
||||||
1
.github/ISSUE_TEMPLATE/feature-request.yml
vendored
1
.github/ISSUE_TEMPLATE/feature-request.yml
vendored
|
|
@ -1,6 +1,5 @@
|
||||||
name: 🚀 Feature Request
|
name: 🚀 Feature Request
|
||||||
description: Suggest an idea, feature, or enhancement
|
description: Suggest an idea, feature, or enhancement
|
||||||
labels: [discussion]
|
|
||||||
title: "[FEATURE]:"
|
title: "[FEATURE]:"
|
||||||
|
|
||||||
body:
|
body:
|
||||||
|
|
|
||||||
1
.github/ISSUE_TEMPLATE/question.yml
vendored
1
.github/ISSUE_TEMPLATE/question.yml
vendored
|
|
@ -1,6 +1,5 @@
|
||||||
name: Question
|
name: Question
|
||||||
description: Ask a question
|
description: Ask a question
|
||||||
labels: ["question"]
|
|
||||||
body:
|
body:
|
||||||
- type: textarea
|
- type: textarea
|
||||||
id: question
|
id: question
|
||||||
|
|
|
||||||
1
.github/TEAM_MEMBERS
vendored
1
.github/TEAM_MEMBERS
vendored
|
|
@ -11,6 +11,5 @@ MrMushrooooom
|
||||||
nexxeln
|
nexxeln
|
||||||
R44VC0RP
|
R44VC0RP
|
||||||
rekram1-node
|
rekram1-node
|
||||||
RhysSullivan
|
|
||||||
thdxr
|
thdxr
|
||||||
simonklee
|
simonklee
|
||||||
|
|
|
||||||
35
.github/VOUCHED.td
vendored
35
.github/VOUCHED.td
vendored
|
|
@ -1,35 +0,0 @@
|
||||||
# Vouched contributors for this project.
|
|
||||||
#
|
|
||||||
# See https://github.com/mitchellh/vouch for details.
|
|
||||||
#
|
|
||||||
# Syntax:
|
|
||||||
# - One handle per line (without @), sorted alphabetically.
|
|
||||||
# - Optional platform prefix: platform:username (e.g., github:user).
|
|
||||||
# - Denounce with minus prefix: -username or -platform:username.
|
|
||||||
# - Optional details after a space following the handle.
|
|
||||||
adamdotdevin
|
|
||||||
-agusbasari29 AI PR slop
|
|
||||||
ariane-emory
|
|
||||||
-atharvau AI review spamming literally every PR
|
|
||||||
-borealbytes
|
|
||||||
-danieljoshuanazareth
|
|
||||||
-danieljoshuanazareth
|
|
||||||
edemaine
|
|
||||||
-florianleibert
|
|
||||||
fwang
|
|
||||||
iamdavidhill
|
|
||||||
jayair
|
|
||||||
kitlangton
|
|
||||||
kommander
|
|
||||||
-opencode2026
|
|
||||||
-opencodeengineer bot that spams issues
|
|
||||||
r44vc0rp
|
|
||||||
rekram1-node
|
|
||||||
-ricardo-m-l
|
|
||||||
-robinmordasiewicz
|
|
||||||
rubdos
|
|
||||||
shantur
|
|
||||||
simonklee
|
|
||||||
-spider-yamet clawdbot/llm psychosis, spam pinging the team
|
|
||||||
thdxr
|
|
||||||
-toastythebot
|
|
||||||
9
.github/actions/setup-bun/action.yml
vendored
9
.github/actions/setup-bun/action.yml
vendored
|
|
@ -1,5 +1,10 @@
|
||||||
name: "Setup Bun"
|
name: "Setup Bun"
|
||||||
description: "Setup Bun with caching and install dependencies"
|
description: "Setup Bun with caching and install dependencies"
|
||||||
|
inputs:
|
||||||
|
install-flags:
|
||||||
|
description: "Additional flags to pass to 'bun install'"
|
||||||
|
required: false
|
||||||
|
default: ""
|
||||||
runs:
|
runs:
|
||||||
using: "composite"
|
using: "composite"
|
||||||
steps:
|
steps:
|
||||||
|
|
@ -46,8 +51,8 @@ runs:
|
||||||
# e.g. ./patches/ for standard-openapi
|
# e.g. ./patches/ for standard-openapi
|
||||||
# https://github.com/oven-sh/bun/issues/28147
|
# https://github.com/oven-sh/bun/issues/28147
|
||||||
if [ "$RUNNER_OS" = "Windows" ]; then
|
if [ "$RUNNER_OS" = "Windows" ]; then
|
||||||
bun install --linker hoisted
|
bun install --linker hoisted ${{ inputs.install-flags }}
|
||||||
else
|
else
|
||||||
bun install
|
bun install ${{ inputs.install-flags }}
|
||||||
fi
|
fi
|
||||||
shell: bash
|
shell: bash
|
||||||
|
|
|
||||||
170
.github/workflows/daily-issues-recap.yml
vendored
170
.github/workflows/daily-issues-recap.yml
vendored
|
|
@ -1,170 +0,0 @@
|
||||||
name: daily-issues-recap
|
|
||||||
|
|
||||||
on:
|
|
||||||
schedule:
|
|
||||||
# Run at 6 PM EST (23:00 UTC, or 22:00 UTC during daylight saving)
|
|
||||||
- cron: "0 23 * * *"
|
|
||||||
workflow_dispatch: # Allow manual trigger for testing
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
daily-recap:
|
|
||||||
runs-on: blacksmith-4vcpu-ubuntu-2404
|
|
||||||
permissions:
|
|
||||||
contents: read
|
|
||||||
issues: read
|
|
||||||
steps:
|
|
||||||
- name: Checkout repository
|
|
||||||
uses: actions/checkout@v4
|
|
||||||
with:
|
|
||||||
fetch-depth: 1
|
|
||||||
|
|
||||||
- uses: ./.github/actions/setup-bun
|
|
||||||
|
|
||||||
- name: Install opencode
|
|
||||||
run: curl -fsSL https://opencode.ai/install | bash
|
|
||||||
|
|
||||||
- name: Generate daily issues recap
|
|
||||||
id: recap
|
|
||||||
env:
|
|
||||||
OPENCODE_API_KEY: ${{ secrets.OPENCODE_API_KEY }}
|
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
OPENCODE_PERMISSION: |
|
|
||||||
{
|
|
||||||
"bash": {
|
|
||||||
"*": "deny",
|
|
||||||
"gh issue*": "allow",
|
|
||||||
"gh search*": "allow"
|
|
||||||
},
|
|
||||||
"webfetch": "deny",
|
|
||||||
"edit": "deny",
|
|
||||||
"write": "deny"
|
|
||||||
}
|
|
||||||
run: |
|
|
||||||
# Get today's date range
|
|
||||||
TODAY=$(date -u +%Y-%m-%d)
|
|
||||||
|
|
||||||
opencode run -m opencode/claude-sonnet-4-5 "Generate a daily issues recap for the OpenCode repository.
|
|
||||||
|
|
||||||
TODAY'S DATE: ${TODAY}
|
|
||||||
|
|
||||||
STEP 1: Gather today's issues
|
|
||||||
Search for all OPEN issues created today (${TODAY}) using:
|
|
||||||
gh issue list --repo ${{ github.repository }} --state open --search \"created:${TODAY}\" --json number,title,body,labels,state,comments,createdAt,author --limit 500
|
|
||||||
|
|
||||||
IMPORTANT: EXCLUDE all issues authored by Anomaly team members. Filter out issues where the author login matches ANY of these:
|
|
||||||
adamdotdevin, Brendonovich, fwang, Hona, iamdavidhill, jayair, kitlangton, kommander, MrMushrooooom, R44VC0RP, rekram1-node, thdxr
|
|
||||||
This recap is specifically for COMMUNITY (external) issues only.
|
|
||||||
|
|
||||||
STEP 2: Analyze and categorize
|
|
||||||
For each issue created today, categorize it:
|
|
||||||
|
|
||||||
**Severity Assessment:**
|
|
||||||
- CRITICAL: Crashes, data loss, security issues, blocks major functionality
|
|
||||||
- HIGH: Significant bugs affecting many users, important features broken
|
|
||||||
- MEDIUM: Bugs with workarounds, minor features broken
|
|
||||||
- LOW: Minor issues, cosmetic, nice-to-haves
|
|
||||||
|
|
||||||
**Activity Assessment:**
|
|
||||||
- Note issues with high comment counts or engagement
|
|
||||||
- Note issues from repeat reporters (check if author has filed before)
|
|
||||||
|
|
||||||
STEP 3: Cross-reference with existing issues
|
|
||||||
For issues that seem like feature requests or recurring bugs:
|
|
||||||
- Search for similar older issues to identify patterns
|
|
||||||
- Note if this is a frequently requested feature
|
|
||||||
- Identify any issues that are duplicates of long-standing requests
|
|
||||||
|
|
||||||
STEP 4: Generate the recap
|
|
||||||
Create a structured recap with these sections:
|
|
||||||
|
|
||||||
===DISCORD_START===
|
|
||||||
**Daily Issues Recap - ${TODAY}**
|
|
||||||
|
|
||||||
**Summary Stats**
|
|
||||||
- Total issues opened today: [count]
|
|
||||||
- By category: [bugs/features/questions]
|
|
||||||
|
|
||||||
**Critical/High Priority Issues**
|
|
||||||
[List any CRITICAL or HIGH severity issues with brief descriptions and issue numbers]
|
|
||||||
|
|
||||||
**Most Active/Discussed**
|
|
||||||
[Issues with significant engagement or from active community members]
|
|
||||||
|
|
||||||
**Trending Topics**
|
|
||||||
[Patterns noticed - e.g., 'Multiple reports about X', 'Continued interest in Y feature']
|
|
||||||
|
|
||||||
**Duplicates & Related**
|
|
||||||
[Issues that relate to existing open issues]
|
|
||||||
===DISCORD_END===
|
|
||||||
|
|
||||||
STEP 5: Format for Discord
|
|
||||||
Format the recap as a Discord-compatible message:
|
|
||||||
- Use Discord markdown (**, __, etc.)
|
|
||||||
- BE EXTREMELY CONCISE - this is an EOD summary, not a detailed report
|
|
||||||
- Use hyperlinked issue numbers with suppressed embeds: [#1234](<https://github.com/${{ github.repository }}/issues/1234>)
|
|
||||||
- Group related issues on single lines where possible
|
|
||||||
- Add emoji sparingly for critical items only
|
|
||||||
- HARD LIMIT: Keep under 1800 characters total
|
|
||||||
- Skip sections that have nothing notable (e.g., if no critical issues, omit that section)
|
|
||||||
- Prioritize signal over completeness - only surface what matters
|
|
||||||
|
|
||||||
OUTPUT: Output ONLY the content between ===DISCORD_START=== and ===DISCORD_END=== markers. Include the markers so I can extract it." > /tmp/recap_raw.txt
|
|
||||||
|
|
||||||
# Extract only the Discord message between markers
|
|
||||||
sed -n '/===DISCORD_START===/,/===DISCORD_END===/p' /tmp/recap_raw.txt | grep -v '===DISCORD' > /tmp/recap.txt
|
|
||||||
|
|
||||||
echo "recap_file=/tmp/recap.txt" >> $GITHUB_OUTPUT
|
|
||||||
|
|
||||||
- name: Post to Discord
|
|
||||||
env:
|
|
||||||
DISCORD_WEBHOOK_URL: ${{ secrets.DISCORD_ISSUES_WEBHOOK_URL }}
|
|
||||||
run: |
|
|
||||||
if [ -z "$DISCORD_WEBHOOK_URL" ]; then
|
|
||||||
echo "Warning: DISCORD_ISSUES_WEBHOOK_URL secret not set, skipping Discord post"
|
|
||||||
cat /tmp/recap.txt
|
|
||||||
exit 0
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Read the recap
|
|
||||||
RECAP_RAW=$(cat /tmp/recap.txt)
|
|
||||||
RECAP_LENGTH=${#RECAP_RAW}
|
|
||||||
|
|
||||||
echo "Recap length: ${RECAP_LENGTH} chars"
|
|
||||||
|
|
||||||
# Function to post a message to Discord
|
|
||||||
post_to_discord() {
|
|
||||||
local msg="$1"
|
|
||||||
local content=$(echo "$msg" | jq -Rs '.')
|
|
||||||
curl -s -H "Content-Type: application/json" \
|
|
||||||
-X POST \
|
|
||||||
-d "{\"content\": ${content}}" \
|
|
||||||
"$DISCORD_WEBHOOK_URL"
|
|
||||||
sleep 1
|
|
||||||
}
|
|
||||||
|
|
||||||
# If under limit, send as single message
|
|
||||||
if [ "$RECAP_LENGTH" -le 1950 ]; then
|
|
||||||
post_to_discord "$RECAP_RAW"
|
|
||||||
else
|
|
||||||
echo "Splitting into multiple messages..."
|
|
||||||
remaining="$RECAP_RAW"
|
|
||||||
while [ ${#remaining} -gt 0 ]; do
|
|
||||||
if [ ${#remaining} -le 1950 ]; then
|
|
||||||
post_to_discord "$remaining"
|
|
||||||
break
|
|
||||||
else
|
|
||||||
chunk="${remaining:0:1900}"
|
|
||||||
last_newline=$(echo "$chunk" | grep -bo $'\n' | tail -1 | cut -d: -f1)
|
|
||||||
if [ -n "$last_newline" ] && [ "$last_newline" -gt 500 ]; then
|
|
||||||
chunk="${remaining:0:$last_newline}"
|
|
||||||
remaining="${remaining:$((last_newline+1))}"
|
|
||||||
else
|
|
||||||
chunk="${remaining:0:1900}"
|
|
||||||
remaining="${remaining:1900}"
|
|
||||||
fi
|
|
||||||
post_to_discord "$chunk"
|
|
||||||
fi
|
|
||||||
done
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo "Posted daily recap to Discord"
|
|
||||||
173
.github/workflows/daily-pr-recap.yml
vendored
173
.github/workflows/daily-pr-recap.yml
vendored
|
|
@ -1,173 +0,0 @@
|
||||||
name: daily-pr-recap
|
|
||||||
|
|
||||||
on:
|
|
||||||
schedule:
|
|
||||||
# Run at 5pm EST (22:00 UTC, or 21:00 UTC during daylight saving)
|
|
||||||
- cron: "0 22 * * *"
|
|
||||||
workflow_dispatch: # Allow manual trigger for testing
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
pr-recap:
|
|
||||||
runs-on: blacksmith-4vcpu-ubuntu-2404
|
|
||||||
permissions:
|
|
||||||
contents: read
|
|
||||||
pull-requests: read
|
|
||||||
steps:
|
|
||||||
- name: Checkout repository
|
|
||||||
uses: actions/checkout@v4
|
|
||||||
with:
|
|
||||||
fetch-depth: 1
|
|
||||||
|
|
||||||
- uses: ./.github/actions/setup-bun
|
|
||||||
|
|
||||||
- name: Install opencode
|
|
||||||
run: curl -fsSL https://opencode.ai/install | bash
|
|
||||||
|
|
||||||
- name: Generate daily PR recap
|
|
||||||
id: recap
|
|
||||||
env:
|
|
||||||
OPENCODE_API_KEY: ${{ secrets.OPENCODE_API_KEY }}
|
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
OPENCODE_PERMISSION: |
|
|
||||||
{
|
|
||||||
"bash": {
|
|
||||||
"*": "deny",
|
|
||||||
"gh pr*": "allow",
|
|
||||||
"gh search*": "allow"
|
|
||||||
},
|
|
||||||
"webfetch": "deny",
|
|
||||||
"edit": "deny",
|
|
||||||
"write": "deny"
|
|
||||||
}
|
|
||||||
run: |
|
|
||||||
TODAY=$(date -u +%Y-%m-%d)
|
|
||||||
|
|
||||||
opencode run -m opencode/claude-sonnet-4-5 "Generate a daily PR activity recap for the OpenCode repository.
|
|
||||||
|
|
||||||
TODAY'S DATE: ${TODAY}
|
|
||||||
|
|
||||||
STEP 1: Gather PR data
|
|
||||||
Run these commands to gather PR information. ONLY include OPEN PRs created or updated TODAY (${TODAY}):
|
|
||||||
|
|
||||||
# Open PRs created today
|
|
||||||
gh pr list --repo ${{ github.repository }} --state open --search \"created:${TODAY}\" --json number,title,author,labels,createdAt,updatedAt,reviewDecision,isDraft,additions,deletions --limit 100
|
|
||||||
|
|
||||||
# Open PRs with activity today (updated today)
|
|
||||||
gh pr list --repo ${{ github.repository }} --state open --search \"updated:${TODAY}\" --json number,title,author,labels,createdAt,updatedAt,reviewDecision,isDraft,additions,deletions --limit 100
|
|
||||||
|
|
||||||
IMPORTANT: EXCLUDE all PRs authored by Anomaly team members. Filter out PRs where the author login matches ANY of these:
|
|
||||||
adamdotdevin, Brendonovich, fwang, Hona, iamdavidhill, jayair, kitlangton, kommander, MrMushrooooom, R44VC0RP, rekram1-node, thdxr
|
|
||||||
This recap is specifically for COMMUNITY (external) contributions only.
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
STEP 2: For high-activity PRs, check comment counts
|
|
||||||
For promising PRs, run:
|
|
||||||
gh pr view [NUMBER] --repo ${{ github.repository }} --json comments --jq '[.comments[] | select(.author.login != \"copilot-pull-request-reviewer\" and .author.login != \"github-actions\")] | length'
|
|
||||||
|
|
||||||
IMPORTANT: When counting comments/activity, EXCLUDE these bot accounts:
|
|
||||||
- copilot-pull-request-reviewer
|
|
||||||
- github-actions
|
|
||||||
|
|
||||||
STEP 3: Identify what matters (ONLY from today's PRs)
|
|
||||||
|
|
||||||
**Bug Fixes From Today:**
|
|
||||||
- PRs with 'fix' or 'bug' in title created/updated today
|
|
||||||
- Small bug fixes (< 100 lines changed) that are easy to review
|
|
||||||
- Bug fixes from community contributors
|
|
||||||
|
|
||||||
**High Activity Today:**
|
|
||||||
- PRs with significant human comments today (excluding bots listed above)
|
|
||||||
- PRs with back-and-forth discussion today
|
|
||||||
|
|
||||||
**Quick Wins:**
|
|
||||||
- Small PRs (< 50 lines) that are approved or nearly approved
|
|
||||||
- PRs that just need a final review
|
|
||||||
|
|
||||||
STEP 4: Generate the recap
|
|
||||||
Create a structured recap:
|
|
||||||
|
|
||||||
===DISCORD_START===
|
|
||||||
**Daily PR Recap - ${TODAY}**
|
|
||||||
|
|
||||||
**New PRs Today**
|
|
||||||
[PRs opened today - group by type: bug fixes, features, etc.]
|
|
||||||
|
|
||||||
**Active PRs Today**
|
|
||||||
[PRs with activity/updates today - significant discussion]
|
|
||||||
|
|
||||||
**Quick Wins**
|
|
||||||
[Small PRs ready to merge]
|
|
||||||
===DISCORD_END===
|
|
||||||
|
|
||||||
STEP 5: Format for Discord
|
|
||||||
- Use Discord markdown (**, __, etc.)
|
|
||||||
- BE EXTREMELY CONCISE - surface what we might miss
|
|
||||||
- Use hyperlinked PR numbers with suppressed embeds: [#1234](<https://github.com/${{ github.repository }}/pull/1234>)
|
|
||||||
- Include PR author: [#1234](<url>) (@author)
|
|
||||||
- For bug fixes, add brief description of what it fixes
|
|
||||||
- Show line count for quick wins: \"(+15/-3 lines)\"
|
|
||||||
- HARD LIMIT: Keep under 1800 characters total
|
|
||||||
- Skip empty sections
|
|
||||||
- Focus on PRs that need human eyes
|
|
||||||
|
|
||||||
OUTPUT: Output ONLY the content between ===DISCORD_START=== and ===DISCORD_END=== markers. Include the markers so I can extract it." > /tmp/pr_recap_raw.txt
|
|
||||||
|
|
||||||
# Extract only the Discord message between markers
|
|
||||||
sed -n '/===DISCORD_START===/,/===DISCORD_END===/p' /tmp/pr_recap_raw.txt | grep -v '===DISCORD' > /tmp/pr_recap.txt
|
|
||||||
|
|
||||||
echo "recap_file=/tmp/pr_recap.txt" >> $GITHUB_OUTPUT
|
|
||||||
|
|
||||||
- name: Post to Discord
|
|
||||||
env:
|
|
||||||
DISCORD_WEBHOOK_URL: ${{ secrets.DISCORD_ISSUES_WEBHOOK_URL }}
|
|
||||||
run: |
|
|
||||||
if [ -z "$DISCORD_WEBHOOK_URL" ]; then
|
|
||||||
echo "Warning: DISCORD_ISSUES_WEBHOOK_URL secret not set, skipping Discord post"
|
|
||||||
cat /tmp/pr_recap.txt
|
|
||||||
exit 0
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Read the recap
|
|
||||||
RECAP_RAW=$(cat /tmp/pr_recap.txt)
|
|
||||||
RECAP_LENGTH=${#RECAP_RAW}
|
|
||||||
|
|
||||||
echo "Recap length: ${RECAP_LENGTH} chars"
|
|
||||||
|
|
||||||
# Function to post a message to Discord
|
|
||||||
post_to_discord() {
|
|
||||||
local msg="$1"
|
|
||||||
local content=$(echo "$msg" | jq -Rs '.')
|
|
||||||
curl -s -H "Content-Type: application/json" \
|
|
||||||
-X POST \
|
|
||||||
-d "{\"content\": ${content}}" \
|
|
||||||
"$DISCORD_WEBHOOK_URL"
|
|
||||||
sleep 1
|
|
||||||
}
|
|
||||||
|
|
||||||
# If under limit, send as single message
|
|
||||||
if [ "$RECAP_LENGTH" -le 1950 ]; then
|
|
||||||
post_to_discord "$RECAP_RAW"
|
|
||||||
else
|
|
||||||
echo "Splitting into multiple messages..."
|
|
||||||
remaining="$RECAP_RAW"
|
|
||||||
while [ ${#remaining} -gt 0 ]; do
|
|
||||||
if [ ${#remaining} -le 1950 ]; then
|
|
||||||
post_to_discord "$remaining"
|
|
||||||
break
|
|
||||||
else
|
|
||||||
chunk="${remaining:0:1900}"
|
|
||||||
last_newline=$(echo "$chunk" | grep -bo $'\n' | tail -1 | cut -d: -f1)
|
|
||||||
if [ -n "$last_newline" ] && [ "$last_newline" -gt 500 ]; then
|
|
||||||
chunk="${remaining:0:$last_newline}"
|
|
||||||
remaining="${remaining:$((last_newline+1))}"
|
|
||||||
else
|
|
||||||
chunk="${remaining:0:1900}"
|
|
||||||
remaining="${remaining:1900}"
|
|
||||||
fi
|
|
||||||
post_to_discord "$chunk"
|
|
||||||
fi
|
|
||||||
done
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo "Posted daily PR recap to Discord"
|
|
||||||
8
.github/workflows/deploy.yml
vendored
8
.github/workflows/deploy.yml
vendored
|
|
@ -36,3 +36,11 @@ jobs:
|
||||||
PLANETSCALE_SERVICE_TOKEN_NAME: ${{ secrets.PLANETSCALE_SERVICE_TOKEN_NAME }}
|
PLANETSCALE_SERVICE_TOKEN_NAME: ${{ secrets.PLANETSCALE_SERVICE_TOKEN_NAME }}
|
||||||
PLANETSCALE_SERVICE_TOKEN: ${{ secrets.PLANETSCALE_SERVICE_TOKEN }}
|
PLANETSCALE_SERVICE_TOKEN: ${{ secrets.PLANETSCALE_SERVICE_TOKEN }}
|
||||||
STRIPE_SECRET_KEY: ${{ github.ref_name == 'production' && secrets.STRIPE_SECRET_KEY_PROD || secrets.STRIPE_SECRET_KEY_DEV }}
|
STRIPE_SECRET_KEY: ${{ github.ref_name == 'production' && secrets.STRIPE_SECRET_KEY_PROD || secrets.STRIPE_SECRET_KEY_DEV }}
|
||||||
|
HONEYCOMB_API_KEY: ${{ secrets.HONEYCOMB_API_KEY }}
|
||||||
|
INCIDENT_API_KEY: ${{ secrets.INCIDENT_API_KEY }}
|
||||||
|
SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
|
||||||
|
SENTRY_ORG: ${{ vars.SENTRY_ORG }}
|
||||||
|
SENTRY_PROJECT: ${{ vars.WEB_SENTRY_PROJECT }}
|
||||||
|
SENTRY_RELEASE: web@${{ github.sha }}
|
||||||
|
VITE_SENTRY_DSN: ${{ vars.WEB_SENTRY_DSN }}
|
||||||
|
VITE_SENTRY_RELEASE: web@${{ github.sha }}
|
||||||
|
|
|
||||||
254
.github/workflows/publish.yml
vendored
254
.github/workflows/publish.yml
vendored
|
|
@ -88,7 +88,7 @@ jobs:
|
||||||
- name: Build
|
- name: Build
|
||||||
id: build
|
id: build
|
||||||
run: |
|
run: |
|
||||||
./packages/opencode/script/build.ts
|
./packages/opencode/script/build.ts ${{ (github.ref_name == 'beta' && '--sourcemaps') || '' }}
|
||||||
env:
|
env:
|
||||||
OPENCODE_VERSION: ${{ needs.version.outputs.version }}
|
OPENCODE_VERSION: ${{ needs.version.outputs.version }}
|
||||||
OPENCODE_RELEASE: ${{ needs.version.outputs.release }}
|
OPENCODE_RELEASE: ${{ needs.version.outputs.release }}
|
||||||
|
|
@ -209,182 +209,6 @@ jobs:
|
||||||
packages/opencode/dist/opencode-windows-x64
|
packages/opencode/dist/opencode-windows-x64
|
||||||
packages/opencode/dist/opencode-windows-x64-baseline
|
packages/opencode/dist/opencode-windows-x64-baseline
|
||||||
|
|
||||||
build-tauri:
|
|
||||||
needs:
|
|
||||||
- build-cli
|
|
||||||
- version
|
|
||||||
continue-on-error: false
|
|
||||||
env:
|
|
||||||
AZURE_CLIENT_ID: ${{ secrets.AZURE_CLIENT_ID }}
|
|
||||||
AZURE_TENANT_ID: ${{ secrets.AZURE_TENANT_ID }}
|
|
||||||
AZURE_SUBSCRIPTION_ID: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
|
|
||||||
AZURE_TRUSTED_SIGNING_ACCOUNT_NAME: ${{ secrets.AZURE_TRUSTED_SIGNING_ACCOUNT_NAME }}
|
|
||||||
AZURE_TRUSTED_SIGNING_CERTIFICATE_PROFILE: ${{ secrets.AZURE_TRUSTED_SIGNING_CERTIFICATE_PROFILE }}
|
|
||||||
AZURE_TRUSTED_SIGNING_ENDPOINT: ${{ secrets.AZURE_TRUSTED_SIGNING_ENDPOINT }}
|
|
||||||
strategy:
|
|
||||||
fail-fast: false
|
|
||||||
matrix:
|
|
||||||
settings:
|
|
||||||
- host: macos-latest
|
|
||||||
target: x86_64-apple-darwin
|
|
||||||
- host: macos-latest
|
|
||||||
target: aarch64-apple-darwin
|
|
||||||
# github-hosted: blacksmith lacks ARM64 MSVC cross-compilation toolchain
|
|
||||||
- host: windows-2025
|
|
||||||
target: aarch64-pc-windows-msvc
|
|
||||||
- host: blacksmith-4vcpu-windows-2025
|
|
||||||
target: x86_64-pc-windows-msvc
|
|
||||||
- host: blacksmith-4vcpu-ubuntu-2404
|
|
||||||
target: x86_64-unknown-linux-gnu
|
|
||||||
- host: blacksmith-8vcpu-ubuntu-2404-arm
|
|
||||||
target: aarch64-unknown-linux-gnu
|
|
||||||
runs-on: ${{ matrix.settings.host }}
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v3
|
|
||||||
with:
|
|
||||||
fetch-tags: true
|
|
||||||
|
|
||||||
- uses: apple-actions/import-codesign-certs@v2
|
|
||||||
if: ${{ runner.os == 'macOS' }}
|
|
||||||
with:
|
|
||||||
keychain: build
|
|
||||||
p12-file-base64: ${{ secrets.APPLE_CERTIFICATE }}
|
|
||||||
p12-password: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }}
|
|
||||||
|
|
||||||
- name: Verify Certificate
|
|
||||||
if: ${{ runner.os == 'macOS' }}
|
|
||||||
run: |
|
|
||||||
CERT_INFO=$(security find-identity -v -p codesigning build.keychain | grep "Developer ID Application")
|
|
||||||
CERT_ID=$(echo "$CERT_INFO" | awk -F'"' '{print $2}')
|
|
||||||
echo "CERT_ID=$CERT_ID" >> $GITHUB_ENV
|
|
||||||
echo "Certificate imported."
|
|
||||||
|
|
||||||
- name: Setup Apple API Key
|
|
||||||
if: ${{ runner.os == 'macOS' }}
|
|
||||||
run: |
|
|
||||||
echo "${{ secrets.APPLE_API_KEY_PATH }}" > $RUNNER_TEMP/apple-api-key.p8
|
|
||||||
|
|
||||||
- uses: ./.github/actions/setup-bun
|
|
||||||
|
|
||||||
- name: Azure login
|
|
||||||
if: runner.os == 'Windows'
|
|
||||||
uses: azure/login@v2
|
|
||||||
with:
|
|
||||||
client-id: ${{ env.AZURE_CLIENT_ID }}
|
|
||||||
tenant-id: ${{ env.AZURE_TENANT_ID }}
|
|
||||||
subscription-id: ${{ env.AZURE_SUBSCRIPTION_ID }}
|
|
||||||
|
|
||||||
- uses: actions/setup-node@v4
|
|
||||||
with:
|
|
||||||
node-version: "24"
|
|
||||||
|
|
||||||
- name: Cache apt packages
|
|
||||||
if: contains(matrix.settings.host, 'ubuntu')
|
|
||||||
uses: actions/cache@v4
|
|
||||||
with:
|
|
||||||
path: ~/apt-cache
|
|
||||||
key: ${{ runner.os }}-${{ matrix.settings.target }}-apt-${{ hashFiles('.github/workflows/publish.yml') }}
|
|
||||||
restore-keys: |
|
|
||||||
${{ runner.os }}-${{ matrix.settings.target }}-apt-
|
|
||||||
|
|
||||||
- name: install dependencies (ubuntu only)
|
|
||||||
if: contains(matrix.settings.host, 'ubuntu')
|
|
||||||
run: |
|
|
||||||
mkdir -p ~/apt-cache && chmod -R a+rw ~/apt-cache
|
|
||||||
sudo apt-get update
|
|
||||||
sudo apt-get install -y --no-install-recommends -o dir::cache::archives="$HOME/apt-cache" libwebkit2gtk-4.1-dev libappindicator3-dev librsvg2-dev patchelf
|
|
||||||
sudo chmod -R a+rw ~/apt-cache
|
|
||||||
|
|
||||||
- name: install Rust stable
|
|
||||||
uses: dtolnay/rust-toolchain@stable
|
|
||||||
with:
|
|
||||||
targets: ${{ matrix.settings.target }}
|
|
||||||
|
|
||||||
- uses: Swatinem/rust-cache@v2
|
|
||||||
with:
|
|
||||||
workspaces: packages/desktop/src-tauri
|
|
||||||
shared-key: ${{ matrix.settings.target }}
|
|
||||||
|
|
||||||
- name: Prepare
|
|
||||||
run: |
|
|
||||||
cd packages/desktop
|
|
||||||
bun ./scripts/prepare.ts
|
|
||||||
env:
|
|
||||||
OPENCODE_VERSION: ${{ needs.version.outputs.version }}
|
|
||||||
GITHUB_TOKEN: ${{ steps.committer.outputs.token }}
|
|
||||||
OPENCODE_CLI_ARTIFACT: ${{ (runner.os == 'Windows' && 'opencode-cli-windows') || 'opencode-cli' }}
|
|
||||||
RUST_TARGET: ${{ matrix.settings.target }}
|
|
||||||
GH_TOKEN: ${{ github.token }}
|
|
||||||
GITHUB_RUN_ID: ${{ github.run_id }}
|
|
||||||
|
|
||||||
- name: Resolve tauri portable SHA
|
|
||||||
if: contains(matrix.settings.host, 'ubuntu')
|
|
||||||
run: echo "TAURI_PORTABLE_SHA=$(git ls-remote https://github.com/tauri-apps/tauri.git refs/heads/feat/truly-portable-appimage | cut -f1)" >> "$GITHUB_ENV"
|
|
||||||
|
|
||||||
# Fixes AppImage build issues, can be removed when https://github.com/tauri-apps/tauri/pull/12491 is released
|
|
||||||
- name: Install tauri-cli from portable appimage branch
|
|
||||||
uses: taiki-e/cache-cargo-install-action@v3
|
|
||||||
if: contains(matrix.settings.host, 'ubuntu')
|
|
||||||
with:
|
|
||||||
tool: tauri-cli
|
|
||||||
git: https://github.com/tauri-apps/tauri
|
|
||||||
# branch: feat/truly-portable-appimage
|
|
||||||
rev: ${{ env.TAURI_PORTABLE_SHA }}
|
|
||||||
|
|
||||||
- name: Show tauri-cli version
|
|
||||||
if: contains(matrix.settings.host, 'ubuntu')
|
|
||||||
run: cargo tauri --version
|
|
||||||
|
|
||||||
- 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: Build and upload artifacts
|
|
||||||
uses: tauri-apps/tauri-action@390cbe447412ced1303d35abe75287949e43437a
|
|
||||||
timeout-minutes: 60
|
|
||||||
with:
|
|
||||||
projectPath: packages/desktop
|
|
||||||
uploadWorkflowArtifacts: true
|
|
||||||
tauriScript: ${{ (contains(matrix.settings.host, 'ubuntu') && 'cargo tauri') || '' }}
|
|
||||||
args: --target ${{ matrix.settings.target }} --config ${{ (github.ref_name == 'beta' && './src-tauri/tauri.beta.conf.json') || './src-tauri/tauri.prod.conf.json' }} --verbose
|
|
||||||
updaterJsonPreferNsis: true
|
|
||||||
releaseId: ${{ needs.version.outputs.release }}
|
|
||||||
tagName: ${{ needs.version.outputs.tag }}
|
|
||||||
releaseDraft: true
|
|
||||||
releaseAssetNamePattern: opencode-desktop-[platform]-[arch][ext]
|
|
||||||
repo: ${{ (github.ref_name == 'beta' && 'opencode-beta') || '' }}
|
|
||||||
releaseCommitish: ${{ github.sha }}
|
|
||||||
env:
|
|
||||||
GITHUB_TOKEN: ${{ steps.committer.outputs.token }}
|
|
||||||
TAURI_BUNDLER_NEW_APPIMAGE_FORMAT: true
|
|
||||||
TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }}
|
|
||||||
TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY_PASSWORD }}
|
|
||||||
APPLE_CERTIFICATE: ${{ secrets.APPLE_CERTIFICATE }}
|
|
||||||
APPLE_CERTIFICATE_PASSWORD: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }}
|
|
||||||
APPLE_SIGNING_IDENTITY: ${{ env.CERT_ID }}
|
|
||||||
APPLE_API_ISSUER: ${{ secrets.APPLE_API_ISSUER }}
|
|
||||||
APPLE_API_KEY: ${{ secrets.APPLE_API_KEY }}
|
|
||||||
APPLE_API_KEY_PATH: ${{ runner.temp }}/apple-api-key.p8
|
|
||||||
|
|
||||||
- name: Verify signed Windows desktop artifacts
|
|
||||||
if: runner.os == 'Windows'
|
|
||||||
shell: pwsh
|
|
||||||
run: |
|
|
||||||
$files = @(
|
|
||||||
"${{ github.workspace }}\packages\desktop\src-tauri\sidecars\opencode-cli-${{ matrix.settings.target }}.exe"
|
|
||||||
)
|
|
||||||
$files += Get-ChildItem "${{ github.workspace }}\packages\desktop\src-tauri\target\${{ matrix.settings.target }}\release\bundle\nsis\*.exe" | Select-Object -ExpandProperty FullName
|
|
||||||
|
|
||||||
foreach ($file in $files) {
|
|
||||||
$sig = Get-AuthenticodeSignature $file
|
|
||||||
if ($sig.Status -ne "Valid") {
|
|
||||||
throw "Invalid signature for ${file}: $($sig.Status)"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
build-electron:
|
build-electron:
|
||||||
needs:
|
needs:
|
||||||
- build-cli
|
- build-cli
|
||||||
|
|
@ -402,12 +226,14 @@ jobs:
|
||||||
fail-fast: false
|
fail-fast: false
|
||||||
matrix:
|
matrix:
|
||||||
settings:
|
settings:
|
||||||
- host: macos-latest
|
- host: macos-26-intel
|
||||||
target: x86_64-apple-darwin
|
target: x86_64-apple-darwin
|
||||||
platform_flag: --mac --x64
|
platform_flag: --mac --x64
|
||||||
- host: macos-latest
|
bun_install_flags: --os=darwin --cpu=x64
|
||||||
|
- host: macos-26
|
||||||
target: aarch64-apple-darwin
|
target: aarch64-apple-darwin
|
||||||
platform_flag: --mac --arm64
|
platform_flag: --mac --arm64
|
||||||
|
bun_install_flags: --os=darwin --cpu=arm64
|
||||||
# github-hosted: blacksmith lacks ARM64 MSVC cross-compilation toolchain
|
# github-hosted: blacksmith lacks ARM64 MSVC cross-compilation toolchain
|
||||||
- host: "windows-2025"
|
- host: "windows-2025"
|
||||||
target: aarch64-pc-windows-msvc
|
target: aarch64-pc-windows-msvc
|
||||||
|
|
@ -437,6 +263,8 @@ jobs:
|
||||||
run: echo "${{ secrets.APPLE_API_KEY_PATH }}" > $RUNNER_TEMP/apple-api-key.p8
|
run: echo "${{ secrets.APPLE_API_KEY_PATH }}" > $RUNNER_TEMP/apple-api-key.p8
|
||||||
|
|
||||||
- uses: ./.github/actions/setup-bun
|
- uses: ./.github/actions/setup-bun
|
||||||
|
with:
|
||||||
|
install-flags: ${{ matrix.settings.bun_install_flags }}
|
||||||
|
|
||||||
- name: Azure login
|
- name: Azure login
|
||||||
if: runner.os == 'Windows'
|
if: runner.os == 'Windows'
|
||||||
|
|
@ -476,7 +304,7 @@ jobs:
|
||||||
|
|
||||||
- name: Prepare
|
- name: Prepare
|
||||||
run: bun ./scripts/prepare.ts
|
run: bun ./scripts/prepare.ts
|
||||||
working-directory: packages/desktop-electron
|
working-directory: packages/desktop
|
||||||
env:
|
env:
|
||||||
OPENCODE_VERSION: ${{ needs.version.outputs.version }}
|
OPENCODE_VERSION: ${{ needs.version.outputs.version }}
|
||||||
OPENCODE_CHANNEL: ${{ (github.ref_name == 'beta' && 'beta') || 'prod' }}
|
OPENCODE_CHANNEL: ${{ (github.ref_name == 'beta' && 'beta') || 'prod' }}
|
||||||
|
|
@ -487,14 +315,21 @@ jobs:
|
||||||
|
|
||||||
- name: Build
|
- name: Build
|
||||||
run: bun run build
|
run: bun run build
|
||||||
working-directory: packages/desktop-electron
|
working-directory: packages/desktop
|
||||||
env:
|
env:
|
||||||
OPENCODE_CHANNEL: ${{ (github.ref_name == 'beta' && 'beta') || 'prod' }}
|
OPENCODE_CHANNEL: ${{ (github.ref_name == 'beta' && 'beta') || 'prod' }}
|
||||||
|
SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
|
||||||
|
SENTRY_ORG: ${{ vars.SENTRY_ORG }}
|
||||||
|
SENTRY_PROJECT: ${{ vars.WEB_SENTRY_PROJECT }}
|
||||||
|
SENTRY_RELEASE: desktop@${{ needs.version.outputs.version }}
|
||||||
|
VITE_SENTRY_DSN: ${{ vars.WEB_SENTRY_DSN }}
|
||||||
|
VITE_SENTRY_ENVIRONMENT: ${{ (github.ref_name == 'beta' && 'beta') || 'production' }}
|
||||||
|
VITE_SENTRY_RELEASE: desktop@${{ needs.version.outputs.version }}
|
||||||
|
|
||||||
- name: Package and publish
|
- name: Package and publish
|
||||||
if: needs.version.outputs.release
|
if: needs.version.outputs.release
|
||||||
run: npx electron-builder ${{ matrix.settings.platform_flag }} --publish always --config electron-builder.config.ts
|
run: npx electron-builder ${{ matrix.settings.platform_flag }} --publish always --config electron-builder.config.ts
|
||||||
working-directory: packages/desktop-electron
|
working-directory: packages/desktop
|
||||||
timeout-minutes: 60
|
timeout-minutes: 60
|
||||||
env:
|
env:
|
||||||
OPENCODE_CHANNEL: ${{ (github.ref_name == 'beta' && 'beta') || 'prod' }}
|
OPENCODE_CHANNEL: ${{ (github.ref_name == 'beta' && 'beta') || 'prod' }}
|
||||||
|
|
@ -508,19 +343,43 @@ jobs:
|
||||||
- name: Package (no publish)
|
- name: Package (no publish)
|
||||||
if: ${{ !needs.version.outputs.release }}
|
if: ${{ !needs.version.outputs.release }}
|
||||||
run: npx electron-builder ${{ matrix.settings.platform_flag }} --publish never --config electron-builder.config.ts
|
run: npx electron-builder ${{ matrix.settings.platform_flag }} --publish never --config electron-builder.config.ts
|
||||||
working-directory: packages/desktop-electron
|
working-directory: packages/desktop
|
||||||
timeout-minutes: 60
|
timeout-minutes: 60
|
||||||
env:
|
env:
|
||||||
OPENCODE_CHANNEL: ${{ (github.ref_name == 'beta' && 'beta') || 'prod' }}
|
OPENCODE_CHANNEL: ${{ (github.ref_name == 'beta' && 'beta') || 'prod' }}
|
||||||
|
|
||||||
|
- name: Create and upload macOS .app.tar.gz
|
||||||
|
if: runner.os == 'macOS' && needs.version.outputs.release
|
||||||
|
working-directory: packages/desktop/dist
|
||||||
|
env:
|
||||||
|
GH_TOKEN: ${{ steps.committer.outputs.token }}
|
||||||
|
run: |
|
||||||
|
if [[ "${{ matrix.settings.target }}" == "x86_64-apple-darwin" ]]; then
|
||||||
|
APP_DIR="mac"
|
||||||
|
OUT_NAME="opencode-desktop-mac-x64.app.tar.gz"
|
||||||
|
elif [[ "${{ matrix.settings.target }}" == "aarch64-apple-darwin" ]]; then
|
||||||
|
APP_DIR="mac-arm64"
|
||||||
|
OUT_NAME="opencode-desktop-mac-arm64.app.tar.gz"
|
||||||
|
else
|
||||||
|
echo "Unknown macOS target: ${{ matrix.settings.target }}"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
APP_PATH=$(find "$APP_DIR" -maxdepth 1 -name "*.app" -type d | head -1)
|
||||||
|
if [ -z "$APP_PATH" ]; then
|
||||||
|
echo "No .app bundle found in $APP_DIR"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
tar -czf "$OUT_NAME" -C "$(dirname "$APP_PATH")" "$(basename "$APP_PATH")"
|
||||||
|
gh release upload "v${{ needs.version.outputs.version }}" "$OUT_NAME" --clobber --repo "${{ needs.version.outputs.repo }}"
|
||||||
|
|
||||||
- name: Verify signed Windows Electron artifacts
|
- name: Verify signed Windows Electron artifacts
|
||||||
if: runner.os == 'Windows'
|
if: runner.os == 'Windows'
|
||||||
shell: pwsh
|
shell: pwsh
|
||||||
run: |
|
run: |
|
||||||
$files = @()
|
$files = @()
|
||||||
$files += Get-ChildItem "${{ github.workspace }}\packages\desktop-electron\dist\*.exe" | Select-Object -ExpandProperty FullName
|
$files += Get-ChildItem "${{ github.workspace }}\packages\desktop\dist\*.exe" | Select-Object -ExpandProperty FullName
|
||||||
$files += Get-ChildItem "${{ github.workspace }}\packages\desktop-electron\dist\*unpacked\*.exe" | Select-Object -ExpandProperty FullName
|
$files += Get-ChildItem "${{ github.workspace }}\packages\desktop\dist\*unpacked\*.exe" | Select-Object -ExpandProperty FullName
|
||||||
$files += Get-ChildItem "${{ github.workspace }}\packages\desktop-electron\dist\*unpacked\resources\opencode-cli.exe" -ErrorAction SilentlyContinue | Select-Object -ExpandProperty FullName
|
$files += Get-ChildItem "${{ github.workspace }}\packages\desktop\dist\*unpacked\resources\opencode-cli.exe" -ErrorAction SilentlyContinue | Select-Object -ExpandProperty FullName
|
||||||
|
|
||||||
foreach ($file in $files | Select-Object -Unique) {
|
foreach ($file in $files | Select-Object -Unique) {
|
||||||
$sig = Get-AuthenticodeSignature $file
|
$sig = Get-AuthenticodeSignature $file
|
||||||
|
|
@ -531,21 +390,20 @@ jobs:
|
||||||
|
|
||||||
- uses: actions/upload-artifact@v4
|
- uses: actions/upload-artifact@v4
|
||||||
with:
|
with:
|
||||||
name: opencode-electron-${{ matrix.settings.target }}
|
name: opencode-desktop-${{ matrix.settings.target }}
|
||||||
path: packages/desktop-electron/dist/*
|
path: packages/desktop/dist/*
|
||||||
|
|
||||||
- uses: actions/upload-artifact@v4
|
- uses: actions/upload-artifact@v4
|
||||||
if: needs.version.outputs.release
|
if: needs.version.outputs.release
|
||||||
with:
|
with:
|
||||||
name: latest-yml-${{ matrix.settings.target }}
|
name: latest-yml-${{ matrix.settings.target }}
|
||||||
path: packages/desktop-electron/dist/latest*.yml
|
path: packages/desktop/dist/latest*.yml
|
||||||
|
|
||||||
publish:
|
publish:
|
||||||
needs:
|
needs:
|
||||||
- version
|
- version
|
||||||
- build-cli
|
- build-cli
|
||||||
- sign-cli-windows
|
- sign-cli-windows
|
||||||
- build-tauri
|
|
||||||
- build-electron
|
- build-electron
|
||||||
if: always() && !failure() && !cancelled()
|
if: always() && !failure() && !cancelled()
|
||||||
runs-on: blacksmith-4vcpu-ubuntu-2404
|
runs-on: blacksmith-4vcpu-ubuntu-2404
|
||||||
|
|
@ -572,13 +430,6 @@ jobs:
|
||||||
node-version: "24"
|
node-version: "24"
|
||||||
registry-url: "https://registry.npmjs.org"
|
registry-url: "https://registry.npmjs.org"
|
||||||
|
|
||||||
- name: Setup git committer
|
|
||||||
id: committer
|
|
||||||
uses: ./.github/actions/setup-git-committer
|
|
||||||
with:
|
|
||||||
opencode-app-id: ${{ vars.OPENCODE_APP_ID }}
|
|
||||||
opencode-app-secret: ${{ secrets.OPENCODE_APP_SECRET }}
|
|
||||||
|
|
||||||
- uses: actions/download-artifact@v4
|
- uses: actions/download-artifact@v4
|
||||||
with:
|
with:
|
||||||
name: opencode-cli
|
name: opencode-cli
|
||||||
|
|
@ -600,6 +451,13 @@ jobs:
|
||||||
pattern: latest-yml-*
|
pattern: latest-yml-*
|
||||||
path: /tmp/latest-yml
|
path: /tmp/latest-yml
|
||||||
|
|
||||||
|
- 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: Cache apt packages (AUR)
|
- name: Cache apt packages (AUR)
|
||||||
uses: actions/cache@v4
|
uses: actions/cache@v4
|
||||||
with:
|
with:
|
||||||
|
|
@ -628,3 +486,5 @@ jobs:
|
||||||
GH_REPO: ${{ needs.version.outputs.repo }}
|
GH_REPO: ${{ needs.version.outputs.repo }}
|
||||||
NPM_CONFIG_PROVENANCE: false
|
NPM_CONFIG_PROVENANCE: false
|
||||||
LATEST_YML_DIR: /tmp/latest-yml
|
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 }}
|
||||||
|
|
|
||||||
4
.github/workflows/review.yml
vendored
4
.github/workflows/review.yml
vendored
|
|
@ -45,13 +45,13 @@ jobs:
|
||||||
|
|
||||||
- name: Check PR guidelines compliance
|
- name: Check PR guidelines compliance
|
||||||
env:
|
env:
|
||||||
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
|
OPENCODE_API_KEY: ${{ secrets.OPENCODE_API_KEY }}
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
OPENCODE_PERMISSION: '{ "bash": { "*": "deny", "gh*": "allow", "gh pr review*": "deny" } }'
|
OPENCODE_PERMISSION: '{ "bash": { "*": "deny", "gh*": "allow", "gh pr review*": "deny" } }'
|
||||||
PR_TITLE: ${{ steps.pr-details.outputs.title }}
|
PR_TITLE: ${{ steps.pr-details.outputs.title }}
|
||||||
run: |
|
run: |
|
||||||
PR_BODY=$(jq -r .body pr_data.json)
|
PR_BODY=$(jq -r .body pr_data.json)
|
||||||
opencode run -m anthropic/claude-opus-4-5 "A new pull request has been created: '${PR_TITLE}'
|
opencode run -m opencode/gpt-5.5 --variant medium "A new pull request has been created: '${PR_TITLE}'
|
||||||
|
|
||||||
<pr-number>
|
<pr-number>
|
||||||
${{ steps.pr-number.outputs.number }}
|
${{ steps.pr-number.outputs.number }}
|
||||||
|
|
|
||||||
116
.github/workflows/vouch-check-issue.yml
vendored
116
.github/workflows/vouch-check-issue.yml
vendored
|
|
@ -1,116 +0,0 @@
|
||||||
name: vouch-check-issue
|
|
||||||
|
|
||||||
on:
|
|
||||||
issues:
|
|
||||||
types: [opened]
|
|
||||||
|
|
||||||
permissions:
|
|
||||||
contents: read
|
|
||||||
issues: write
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
check:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- name: Check if issue author is denounced
|
|
||||||
uses: actions/github-script@v7
|
|
||||||
with:
|
|
||||||
script: |
|
|
||||||
const author = context.payload.issue.user.login;
|
|
||||||
const issueNumber = context.payload.issue.number;
|
|
||||||
|
|
||||||
// Skip bots
|
|
||||||
if (author.endsWith('[bot]')) {
|
|
||||||
core.info(`Skipping bot: ${author}`);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Read the VOUCHED.td file via API (no checkout needed)
|
|
||||||
let content;
|
|
||||||
try {
|
|
||||||
const response = await github.rest.repos.getContent({
|
|
||||||
owner: context.repo.owner,
|
|
||||||
repo: context.repo.repo,
|
|
||||||
path: '.github/VOUCHED.td',
|
|
||||||
});
|
|
||||||
content = Buffer.from(response.data.content, 'base64').toString('utf-8');
|
|
||||||
} catch (error) {
|
|
||||||
if (error.status === 404) {
|
|
||||||
core.info('No .github/VOUCHED.td file found, skipping check.');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Parse the .td file for vouched and denounced users
|
|
||||||
const vouched = new Set();
|
|
||||||
const denounced = new Map();
|
|
||||||
for (const line of content.split('\n')) {
|
|
||||||
const trimmed = line.trim();
|
|
||||||
if (!trimmed || trimmed.startsWith('#')) continue;
|
|
||||||
|
|
||||||
const isDenounced = trimmed.startsWith('-');
|
|
||||||
const rest = isDenounced ? trimmed.slice(1).trim() : trimmed;
|
|
||||||
if (!rest) continue;
|
|
||||||
|
|
||||||
const spaceIdx = rest.indexOf(' ');
|
|
||||||
const handle = spaceIdx === -1 ? rest : rest.slice(0, spaceIdx);
|
|
||||||
const reason = spaceIdx === -1 ? null : rest.slice(spaceIdx + 1).trim();
|
|
||||||
|
|
||||||
// Handle platform:username or bare username
|
|
||||||
// Only match bare usernames or github: prefix (skip other platforms)
|
|
||||||
const colonIdx = handle.indexOf(':');
|
|
||||||
if (colonIdx !== -1) {
|
|
||||||
const platform = handle.slice(0, colonIdx).toLowerCase();
|
|
||||||
if (platform !== 'github') continue;
|
|
||||||
}
|
|
||||||
const username = colonIdx === -1 ? handle : handle.slice(colonIdx + 1);
|
|
||||||
if (!username) continue;
|
|
||||||
|
|
||||||
if (isDenounced) {
|
|
||||||
denounced.set(username.toLowerCase(), reason);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
vouched.add(username.toLowerCase());
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if the author is denounced
|
|
||||||
const reason = denounced.get(author.toLowerCase());
|
|
||||||
if (reason !== undefined) {
|
|
||||||
// Author is denounced — close the issue
|
|
||||||
const body = 'This issue has been automatically closed.';
|
|
||||||
|
|
||||||
await github.rest.issues.createComment({
|
|
||||||
owner: context.repo.owner,
|
|
||||||
repo: context.repo.repo,
|
|
||||||
issue_number: issueNumber,
|
|
||||||
body,
|
|
||||||
});
|
|
||||||
|
|
||||||
await github.rest.issues.update({
|
|
||||||
owner: context.repo.owner,
|
|
||||||
repo: context.repo.repo,
|
|
||||||
issue_number: issueNumber,
|
|
||||||
state: 'closed',
|
|
||||||
state_reason: 'not_planned',
|
|
||||||
});
|
|
||||||
|
|
||||||
core.info(`Closed issue #${issueNumber} from denounced user ${author}`);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Author is positively vouched — add label
|
|
||||||
if (!vouched.has(author.toLowerCase())) {
|
|
||||||
core.info(`User ${author} is not denounced or vouched. Allowing issue.`);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
await github.rest.issues.addLabels({
|
|
||||||
owner: context.repo.owner,
|
|
||||||
repo: context.repo.repo,
|
|
||||||
issue_number: issueNumber,
|
|
||||||
labels: ['Vouched'],
|
|
||||||
});
|
|
||||||
|
|
||||||
core.info(`Added vouched label to issue #${issueNumber} from ${author}`);
|
|
||||||
114
.github/workflows/vouch-check-pr.yml
vendored
114
.github/workflows/vouch-check-pr.yml
vendored
|
|
@ -1,114 +0,0 @@
|
||||||
name: vouch-check-pr
|
|
||||||
|
|
||||||
on:
|
|
||||||
pull_request_target:
|
|
||||||
types: [opened]
|
|
||||||
|
|
||||||
permissions:
|
|
||||||
contents: read
|
|
||||||
issues: write
|
|
||||||
pull-requests: write
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
check:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- name: Check if PR author is denounced
|
|
||||||
uses: actions/github-script@v7
|
|
||||||
with:
|
|
||||||
script: |
|
|
||||||
const author = context.payload.pull_request.user.login;
|
|
||||||
const prNumber = context.payload.pull_request.number;
|
|
||||||
|
|
||||||
// Skip bots
|
|
||||||
if (author.endsWith('[bot]')) {
|
|
||||||
core.info(`Skipping bot: ${author}`);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Read the VOUCHED.td file via API (no checkout needed)
|
|
||||||
let content;
|
|
||||||
try {
|
|
||||||
const response = await github.rest.repos.getContent({
|
|
||||||
owner: context.repo.owner,
|
|
||||||
repo: context.repo.repo,
|
|
||||||
path: '.github/VOUCHED.td',
|
|
||||||
});
|
|
||||||
content = Buffer.from(response.data.content, 'base64').toString('utf-8');
|
|
||||||
} catch (error) {
|
|
||||||
if (error.status === 404) {
|
|
||||||
core.info('No .github/VOUCHED.td file found, skipping check.');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Parse the .td file for vouched and denounced users
|
|
||||||
const vouched = new Set();
|
|
||||||
const denounced = new Map();
|
|
||||||
for (const line of content.split('\n')) {
|
|
||||||
const trimmed = line.trim();
|
|
||||||
if (!trimmed || trimmed.startsWith('#')) continue;
|
|
||||||
|
|
||||||
const isDenounced = trimmed.startsWith('-');
|
|
||||||
const rest = isDenounced ? trimmed.slice(1).trim() : trimmed;
|
|
||||||
if (!rest) continue;
|
|
||||||
|
|
||||||
const spaceIdx = rest.indexOf(' ');
|
|
||||||
const handle = spaceIdx === -1 ? rest : rest.slice(0, spaceIdx);
|
|
||||||
const reason = spaceIdx === -1 ? null : rest.slice(spaceIdx + 1).trim();
|
|
||||||
|
|
||||||
// Handle platform:username or bare username
|
|
||||||
// Only match bare usernames or github: prefix (skip other platforms)
|
|
||||||
const colonIdx = handle.indexOf(':');
|
|
||||||
if (colonIdx !== -1) {
|
|
||||||
const platform = handle.slice(0, colonIdx).toLowerCase();
|
|
||||||
if (platform !== 'github') continue;
|
|
||||||
}
|
|
||||||
const username = colonIdx === -1 ? handle : handle.slice(colonIdx + 1);
|
|
||||||
if (!username) continue;
|
|
||||||
|
|
||||||
if (isDenounced) {
|
|
||||||
denounced.set(username.toLowerCase(), reason);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
vouched.add(username.toLowerCase());
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if the author is denounced
|
|
||||||
const reason = denounced.get(author.toLowerCase());
|
|
||||||
if (reason !== undefined) {
|
|
||||||
// Author is denounced — close the PR
|
|
||||||
await github.rest.issues.createComment({
|
|
||||||
owner: context.repo.owner,
|
|
||||||
repo: context.repo.repo,
|
|
||||||
issue_number: prNumber,
|
|
||||||
body: 'This pull request has been automatically closed.',
|
|
||||||
});
|
|
||||||
|
|
||||||
await github.rest.pulls.update({
|
|
||||||
owner: context.repo.owner,
|
|
||||||
repo: context.repo.repo,
|
|
||||||
pull_number: prNumber,
|
|
||||||
state: 'closed',
|
|
||||||
});
|
|
||||||
|
|
||||||
core.info(`Closed PR #${prNumber} from denounced user ${author}`);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Author is positively vouched — add label
|
|
||||||
if (!vouched.has(author.toLowerCase())) {
|
|
||||||
core.info(`User ${author} is not denounced or vouched. Allowing PR.`);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
await github.rest.issues.addLabels({
|
|
||||||
owner: context.repo.owner,
|
|
||||||
repo: context.repo.repo,
|
|
||||||
issue_number: prNumber,
|
|
||||||
labels: ['Vouched'],
|
|
||||||
});
|
|
||||||
|
|
||||||
core.info(`Added vouched label to PR #${prNumber} from ${author}`);
|
|
||||||
38
.github/workflows/vouch-manage-by-issue.yml
vendored
38
.github/workflows/vouch-manage-by-issue.yml
vendored
|
|
@ -1,38 +0,0 @@
|
||||||
name: vouch-manage-by-issue
|
|
||||||
|
|
||||||
on:
|
|
||||||
issue_comment:
|
|
||||||
types: [created]
|
|
||||||
|
|
||||||
concurrency:
|
|
||||||
group: vouch-manage
|
|
||||||
cancel-in-progress: false
|
|
||||||
|
|
||||||
permissions:
|
|
||||||
contents: write
|
|
||||||
issues: write
|
|
||||||
pull-requests: read
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
manage:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v4
|
|
||||||
with:
|
|
||||||
persist-credentials: false
|
|
||||||
fetch-depth: 0
|
|
||||||
|
|
||||||
- name: Setup git committer
|
|
||||||
id: committer
|
|
||||||
uses: ./.github/actions/setup-git-committer
|
|
||||||
with:
|
|
||||||
opencode-app-id: ${{ vars.OPENCODE_APP_ID }}
|
|
||||||
opencode-app-secret: ${{ secrets.OPENCODE_APP_SECRET }}
|
|
||||||
|
|
||||||
- uses: mitchellh/vouch/action/manage-by-issue@main
|
|
||||||
with:
|
|
||||||
issue-id: ${{ github.event.issue.number }}
|
|
||||||
comment-id: ${{ github.event.comment.id }}
|
|
||||||
roles: admin,maintain,write
|
|
||||||
env:
|
|
||||||
GITHUB_TOKEN: ${{ steps.committer.outputs.token }}
|
|
||||||
|
|
@ -1,899 +0,0 @@
|
||||||
---
|
|
||||||
description: Translate content for a specified locale while preserving technical terms
|
|
||||||
mode: subagent
|
|
||||||
model: opencode/gpt-5.4
|
|
||||||
---
|
|
||||||
|
|
||||||
You are a professional translator and localization specialist.
|
|
||||||
|
|
||||||
Translate the user's content into the requested target locale (language + region, e.g. fr-FR, de-DE).
|
|
||||||
|
|
||||||
Requirements:
|
|
||||||
|
|
||||||
- Preserve meaning, intent, tone, and formatting (including Markdown/MDX structure).
|
|
||||||
- Preserve all technical terms and artifacts exactly: product/company names, API names, identifiers, code, commands/flags, file paths, URLs, versions, error messages, config keys/values, and anything inside inline code or code blocks.
|
|
||||||
- Also preserve every term listed in the Do-Not-Translate glossary below.
|
|
||||||
- Also apply locale-specific guidance from `.opencode/glossary/<locale>.md` when available (for example, `zh-cn.md`).
|
|
||||||
- Do not modify fenced code blocks.
|
|
||||||
- Output ONLY the translation (no commentary).
|
|
||||||
|
|
||||||
If the target locale is missing, ask the user to provide it.
|
|
||||||
If no locale-specific glossary exists, use the global glossary only.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
# Locale-Specific Glossaries
|
|
||||||
|
|
||||||
When a locale glossary exists, use it to:
|
|
||||||
|
|
||||||
- Apply preferred wording for recurring UI/docs terms in that locale
|
|
||||||
- Preserve locale-specific do-not-translate terms and casing decisions
|
|
||||||
- Prefer natural phrasing over literal translation when the locale file calls it out
|
|
||||||
- If the repo uses a locale alias slug, apply that file too (for example, `pt-BR` maps to `br.md` in this repo)
|
|
||||||
|
|
||||||
Locale guidance does not override code/command preservation rules or the global Do-Not-Translate glossary below.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
# Do-Not-Translate Terms (OpenCode Docs)
|
|
||||||
|
|
||||||
Generated from: `packages/web/src/content/docs/*.mdx` (default English docs)
|
|
||||||
Generated on: 2026-02-10
|
|
||||||
|
|
||||||
Use this as a translation QA checklist / glossary. Preserve listed terms exactly (spelling, casing, punctuation).
|
|
||||||
|
|
||||||
General rules (verbatim, even if not listed below):
|
|
||||||
|
|
||||||
- Anything inside inline code (single backticks) or fenced code blocks (triple backticks)
|
|
||||||
- MDX/JS code in docs: `import ... from "..."`, component tags, identifiers
|
|
||||||
- CLI commands, flags, config keys/values, file paths, URLs/domains, and env vars
|
|
||||||
|
|
||||||
## Proper nouns and product names
|
|
||||||
|
|
||||||
Additional (not reliably captured via link text):
|
|
||||||
|
|
||||||
```text
|
|
||||||
Astro
|
|
||||||
Bun
|
|
||||||
Chocolatey
|
|
||||||
Cursor
|
|
||||||
Docker
|
|
||||||
Git
|
|
||||||
GitHub Actions
|
|
||||||
GitLab CI
|
|
||||||
GNOME Terminal
|
|
||||||
Homebrew
|
|
||||||
Mise
|
|
||||||
Neovim
|
|
||||||
Node.js
|
|
||||||
npm
|
|
||||||
Obsidian
|
|
||||||
opencode
|
|
||||||
opencode-ai
|
|
||||||
Paru
|
|
||||||
pnpm
|
|
||||||
ripgrep
|
|
||||||
Scoop
|
|
||||||
SST
|
|
||||||
Starlight
|
|
||||||
Visual Studio Code
|
|
||||||
VS Code
|
|
||||||
VSCodium
|
|
||||||
Windsurf
|
|
||||||
Windows Terminal
|
|
||||||
Yarn
|
|
||||||
Zellij
|
|
||||||
Zed
|
|
||||||
anomalyco
|
|
||||||
```
|
|
||||||
|
|
||||||
Extracted from link labels in the English docs (review and prune as desired):
|
|
||||||
|
|
||||||
```text
|
|
||||||
@openspoon/subtask2
|
|
||||||
302.AI console
|
|
||||||
ACP progress report
|
|
||||||
Agent Client Protocol
|
|
||||||
Agent Skills
|
|
||||||
Agentic
|
|
||||||
AGENTS.md
|
|
||||||
AI SDK
|
|
||||||
Alacritty
|
|
||||||
Anthropic
|
|
||||||
Anthropic's Data Policies
|
|
||||||
Atom One
|
|
||||||
Avante.nvim
|
|
||||||
Ayu
|
|
||||||
Azure AI Foundry
|
|
||||||
Azure portal
|
|
||||||
Baseten
|
|
||||||
built-in GITHUB_TOKEN
|
|
||||||
Bun.$
|
|
||||||
Catppuccin
|
|
||||||
Cerebras console
|
|
||||||
ChatGPT Plus or Pro
|
|
||||||
Cloudflare dashboard
|
|
||||||
CodeCompanion.nvim
|
|
||||||
CodeNomad
|
|
||||||
Configuring Adapters: Environment Variables
|
|
||||||
Context7 MCP server
|
|
||||||
Cortecs console
|
|
||||||
Deep Infra dashboard
|
|
||||||
DeepSeek console
|
|
||||||
Duo Agent Platform
|
|
||||||
Everforest
|
|
||||||
Fireworks AI console
|
|
||||||
Firmware dashboard
|
|
||||||
Ghostty
|
|
||||||
GitLab CLI agents docs
|
|
||||||
GitLab docs
|
|
||||||
GitLab User Settings > Access Tokens
|
|
||||||
Granular Rules (Object Syntax)
|
|
||||||
Grep by Vercel
|
|
||||||
Groq console
|
|
||||||
Gruvbox
|
|
||||||
Helicone
|
|
||||||
Helicone documentation
|
|
||||||
Helicone Header Directory
|
|
||||||
Helicone's Model Directory
|
|
||||||
Hugging Face Inference Providers
|
|
||||||
Hugging Face settings
|
|
||||||
install WSL
|
|
||||||
IO.NET console
|
|
||||||
JetBrains IDE
|
|
||||||
Kanagawa
|
|
||||||
Kitty
|
|
||||||
MiniMax API Console
|
|
||||||
Models.dev
|
|
||||||
Moonshot AI console
|
|
||||||
Nebius Token Factory console
|
|
||||||
Nord
|
|
||||||
OAuth
|
|
||||||
Ollama integration docs
|
|
||||||
OpenAI's Data Policies
|
|
||||||
OpenChamber
|
|
||||||
OpenCode
|
|
||||||
OpenCode config
|
|
||||||
OpenCode Config
|
|
||||||
OpenCode TUI with the opencode theme
|
|
||||||
OpenCode Web - Active Session
|
|
||||||
OpenCode Web - New Session
|
|
||||||
OpenCode Web - See Servers
|
|
||||||
OpenCode Zen
|
|
||||||
OpenCode-Obsidian
|
|
||||||
OpenRouter dashboard
|
|
||||||
OpenWork
|
|
||||||
OVHcloud panel
|
|
||||||
Pro+ subscription
|
|
||||||
SAP BTP Cockpit
|
|
||||||
Scaleway Console IAM settings
|
|
||||||
Scaleway Generative APIs
|
|
||||||
SDK documentation
|
|
||||||
Sentry MCP server
|
|
||||||
shell API
|
|
||||||
Together AI console
|
|
||||||
Tokyonight
|
|
||||||
Unified Billing
|
|
||||||
Venice AI console
|
|
||||||
Vercel dashboard
|
|
||||||
WezTerm
|
|
||||||
Windows Subsystem for Linux (WSL)
|
|
||||||
WSL
|
|
||||||
WSL (Windows Subsystem for Linux)
|
|
||||||
WSL extension
|
|
||||||
xAI console
|
|
||||||
Z.AI API console
|
|
||||||
Zed
|
|
||||||
ZenMux dashboard
|
|
||||||
Zod
|
|
||||||
```
|
|
||||||
|
|
||||||
## Acronyms and initialisms
|
|
||||||
|
|
||||||
```text
|
|
||||||
ACP
|
|
||||||
AGENTS
|
|
||||||
AI
|
|
||||||
AI21
|
|
||||||
ANSI
|
|
||||||
API
|
|
||||||
AST
|
|
||||||
AWS
|
|
||||||
BTP
|
|
||||||
CD
|
|
||||||
CDN
|
|
||||||
CI
|
|
||||||
CLI
|
|
||||||
CMD
|
|
||||||
CORS
|
|
||||||
DEBUG
|
|
||||||
EKS
|
|
||||||
ERROR
|
|
||||||
FAQ
|
|
||||||
GLM
|
|
||||||
GNOME
|
|
||||||
GPT
|
|
||||||
HTML
|
|
||||||
HTTP
|
|
||||||
HTTPS
|
|
||||||
IAM
|
|
||||||
ID
|
|
||||||
IDE
|
|
||||||
INFO
|
|
||||||
IO
|
|
||||||
IP
|
|
||||||
IRSA
|
|
||||||
JS
|
|
||||||
JSON
|
|
||||||
JSONC
|
|
||||||
K2
|
|
||||||
LLM
|
|
||||||
LM
|
|
||||||
LSP
|
|
||||||
M2
|
|
||||||
MCP
|
|
||||||
MR
|
|
||||||
NET
|
|
||||||
NPM
|
|
||||||
NTLM
|
|
||||||
OIDC
|
|
||||||
OS
|
|
||||||
PAT
|
|
||||||
PATH
|
|
||||||
PHP
|
|
||||||
PR
|
|
||||||
PTY
|
|
||||||
README
|
|
||||||
RFC
|
|
||||||
RPC
|
|
||||||
SAP
|
|
||||||
SDK
|
|
||||||
SKILL
|
|
||||||
SSE
|
|
||||||
SSO
|
|
||||||
TS
|
|
||||||
TTY
|
|
||||||
TUI
|
|
||||||
UI
|
|
||||||
URL
|
|
||||||
US
|
|
||||||
UX
|
|
||||||
VCS
|
|
||||||
VPC
|
|
||||||
VPN
|
|
||||||
VS
|
|
||||||
WARN
|
|
||||||
WSL
|
|
||||||
X11
|
|
||||||
YAML
|
|
||||||
```
|
|
||||||
|
|
||||||
## Code identifiers used in prose (CamelCase, mixedCase)
|
|
||||||
|
|
||||||
```text
|
|
||||||
apiKey
|
|
||||||
AppleScript
|
|
||||||
AssistantMessage
|
|
||||||
baseURL
|
|
||||||
BurntSushi
|
|
||||||
ChatGPT
|
|
||||||
ClangFormat
|
|
||||||
CodeCompanion
|
|
||||||
CodeNomad
|
|
||||||
DeepSeek
|
|
||||||
DefaultV2
|
|
||||||
FileContent
|
|
||||||
FileDiff
|
|
||||||
FileNode
|
|
||||||
fineGrained
|
|
||||||
FormatterStatus
|
|
||||||
GitHub
|
|
||||||
GitLab
|
|
||||||
iTerm2
|
|
||||||
JavaScript
|
|
||||||
JetBrains
|
|
||||||
macOS
|
|
||||||
mDNS
|
|
||||||
MiniMax
|
|
||||||
NeuralNomadsAI
|
|
||||||
NickvanDyke
|
|
||||||
NoeFabris
|
|
||||||
OpenAI
|
|
||||||
OpenAPI
|
|
||||||
OpenChamber
|
|
||||||
OpenCode
|
|
||||||
OpenRouter
|
|
||||||
OpenTUI
|
|
||||||
OpenWork
|
|
||||||
ownUserPermissions
|
|
||||||
PowerShell
|
|
||||||
ProviderAuthAuthorization
|
|
||||||
ProviderAuthMethod
|
|
||||||
ProviderInitError
|
|
||||||
SessionStatus
|
|
||||||
TabItem
|
|
||||||
tokenType
|
|
||||||
ToolIDs
|
|
||||||
ToolList
|
|
||||||
TypeScript
|
|
||||||
typesUrl
|
|
||||||
UserMessage
|
|
||||||
VcsInfo
|
|
||||||
WebView2
|
|
||||||
WezTerm
|
|
||||||
xAI
|
|
||||||
ZenMux
|
|
||||||
```
|
|
||||||
|
|
||||||
## OpenCode CLI commands (as shown in docs)
|
|
||||||
|
|
||||||
```text
|
|
||||||
opencode
|
|
||||||
opencode [project]
|
|
||||||
opencode /path/to/project
|
|
||||||
opencode acp
|
|
||||||
opencode agent [command]
|
|
||||||
opencode agent create
|
|
||||||
opencode agent list
|
|
||||||
opencode attach [url]
|
|
||||||
opencode attach http://10.20.30.40:4096
|
|
||||||
opencode attach http://localhost:4096
|
|
||||||
opencode auth [command]
|
|
||||||
opencode auth list
|
|
||||||
opencode auth login
|
|
||||||
opencode auth logout
|
|
||||||
opencode auth ls
|
|
||||||
opencode export [sessionID]
|
|
||||||
opencode github [command]
|
|
||||||
opencode github install
|
|
||||||
opencode github run
|
|
||||||
opencode import <file>
|
|
||||||
opencode import https://opncd.ai/s/abc123
|
|
||||||
opencode import session.json
|
|
||||||
opencode mcp [command]
|
|
||||||
opencode mcp add
|
|
||||||
opencode mcp auth [name]
|
|
||||||
opencode mcp auth list
|
|
||||||
opencode mcp auth ls
|
|
||||||
opencode mcp auth my-oauth-server
|
|
||||||
opencode mcp auth sentry
|
|
||||||
opencode mcp debug <name>
|
|
||||||
opencode mcp debug my-oauth-server
|
|
||||||
opencode mcp list
|
|
||||||
opencode mcp logout [name]
|
|
||||||
opencode mcp logout my-oauth-server
|
|
||||||
opencode mcp ls
|
|
||||||
opencode models --refresh
|
|
||||||
opencode models [provider]
|
|
||||||
opencode models anthropic
|
|
||||||
opencode run [message..]
|
|
||||||
opencode run Explain the use of context in Go
|
|
||||||
opencode serve
|
|
||||||
opencode serve --cors http://localhost:5173 --cors https://app.example.com
|
|
||||||
opencode serve --hostname 0.0.0.0 --port 4096
|
|
||||||
opencode serve [--port <number>] [--hostname <string>] [--cors <origin>]
|
|
||||||
opencode session [command]
|
|
||||||
opencode session list
|
|
||||||
opencode session delete <sessionID>
|
|
||||||
opencode stats
|
|
||||||
opencode uninstall
|
|
||||||
opencode upgrade
|
|
||||||
opencode upgrade [target]
|
|
||||||
opencode upgrade v0.1.48
|
|
||||||
opencode web
|
|
||||||
opencode web --cors https://example.com
|
|
||||||
opencode web --hostname 0.0.0.0
|
|
||||||
opencode web --mdns
|
|
||||||
opencode web --mdns --mdns-domain myproject.local
|
|
||||||
opencode web --port 4096
|
|
||||||
opencode web --port 4096 --hostname 0.0.0.0
|
|
||||||
opencode.server.close()
|
|
||||||
```
|
|
||||||
|
|
||||||
## Slash commands and routes
|
|
||||||
|
|
||||||
```text
|
|
||||||
/agent
|
|
||||||
/auth/:id
|
|
||||||
/clear
|
|
||||||
/command
|
|
||||||
/config
|
|
||||||
/config/providers
|
|
||||||
/connect
|
|
||||||
/continue
|
|
||||||
/doc
|
|
||||||
/editor
|
|
||||||
/event
|
|
||||||
/experimental/tool?provider=<p>&model=<m>
|
|
||||||
/experimental/tool/ids
|
|
||||||
/export
|
|
||||||
/file?path=<path>
|
|
||||||
/file/content?path=<p>
|
|
||||||
/file/status
|
|
||||||
/find?pattern=<pat>
|
|
||||||
/find/file
|
|
||||||
/find/file?query=<q>
|
|
||||||
/find/symbol?query=<q>
|
|
||||||
/formatter
|
|
||||||
/global/event
|
|
||||||
/global/health
|
|
||||||
/help
|
|
||||||
/init
|
|
||||||
/instance/dispose
|
|
||||||
/log
|
|
||||||
/lsp
|
|
||||||
/mcp
|
|
||||||
/mnt/
|
|
||||||
/mnt/c/
|
|
||||||
/mnt/d/
|
|
||||||
/models
|
|
||||||
/oc
|
|
||||||
/opencode
|
|
||||||
/path
|
|
||||||
/project
|
|
||||||
/project/current
|
|
||||||
/provider
|
|
||||||
/provider/{id}/oauth/authorize
|
|
||||||
/provider/{id}/oauth/callback
|
|
||||||
/provider/auth
|
|
||||||
/q
|
|
||||||
/quit
|
|
||||||
/redo
|
|
||||||
/resume
|
|
||||||
/session
|
|
||||||
/session/:id
|
|
||||||
/session/:id/abort
|
|
||||||
/session/:id/children
|
|
||||||
/session/:id/command
|
|
||||||
/session/:id/diff
|
|
||||||
/session/:id/fork
|
|
||||||
/session/:id/init
|
|
||||||
/session/:id/message
|
|
||||||
/session/:id/message/:messageID
|
|
||||||
/session/:id/permissions/:permissionID
|
|
||||||
/session/:id/prompt_async
|
|
||||||
/session/:id/revert
|
|
||||||
/session/:id/share
|
|
||||||
/session/:id/shell
|
|
||||||
/session/:id/summarize
|
|
||||||
/session/:id/todo
|
|
||||||
/session/:id/unrevert
|
|
||||||
/session/status
|
|
||||||
/share
|
|
||||||
/summarize
|
|
||||||
/theme
|
|
||||||
/tui
|
|
||||||
/tui/append-prompt
|
|
||||||
/tui/clear-prompt
|
|
||||||
/tui/control/next
|
|
||||||
/tui/control/response
|
|
||||||
/tui/execute-command
|
|
||||||
/tui/open-help
|
|
||||||
/tui/open-models
|
|
||||||
/tui/open-sessions
|
|
||||||
/tui/open-themes
|
|
||||||
/tui/show-toast
|
|
||||||
/tui/submit-prompt
|
|
||||||
/undo
|
|
||||||
/Users/username
|
|
||||||
/Users/username/projects/*
|
|
||||||
/vcs
|
|
||||||
```
|
|
||||||
|
|
||||||
## CLI flags and short options
|
|
||||||
|
|
||||||
```text
|
|
||||||
--agent
|
|
||||||
--attach
|
|
||||||
--command
|
|
||||||
--continue
|
|
||||||
--cors
|
|
||||||
--cwd
|
|
||||||
--days
|
|
||||||
--dir
|
|
||||||
--dry-run
|
|
||||||
--event
|
|
||||||
--file
|
|
||||||
--force
|
|
||||||
--fork
|
|
||||||
--format
|
|
||||||
--help
|
|
||||||
--hostname
|
|
||||||
--hostname 0.0.0.0
|
|
||||||
--keep-config
|
|
||||||
--keep-data
|
|
||||||
--log-level
|
|
||||||
--max-count
|
|
||||||
--mdns
|
|
||||||
--mdns-domain
|
|
||||||
--method
|
|
||||||
--model
|
|
||||||
--models
|
|
||||||
--port
|
|
||||||
--print-logs
|
|
||||||
--project
|
|
||||||
--prompt
|
|
||||||
--refresh
|
|
||||||
--session
|
|
||||||
--share
|
|
||||||
--title
|
|
||||||
--token
|
|
||||||
--tools
|
|
||||||
--verbose
|
|
||||||
--version
|
|
||||||
--wait
|
|
||||||
|
|
||||||
-c
|
|
||||||
-d
|
|
||||||
-f
|
|
||||||
-h
|
|
||||||
-m
|
|
||||||
-n
|
|
||||||
-s
|
|
||||||
-v
|
|
||||||
```
|
|
||||||
|
|
||||||
## Environment variables
|
|
||||||
|
|
||||||
```text
|
|
||||||
AI_API_URL
|
|
||||||
AI_FLOW_CONTEXT
|
|
||||||
AI_FLOW_EVENT
|
|
||||||
AI_FLOW_INPUT
|
|
||||||
AICORE_DEPLOYMENT_ID
|
|
||||||
AICORE_RESOURCE_GROUP
|
|
||||||
AICORE_SERVICE_KEY
|
|
||||||
ANTHROPIC_API_KEY
|
|
||||||
AWS_ACCESS_KEY_ID
|
|
||||||
AWS_BEARER_TOKEN_BEDROCK
|
|
||||||
AWS_PROFILE
|
|
||||||
AWS_REGION
|
|
||||||
AWS_ROLE_ARN
|
|
||||||
AWS_SECRET_ACCESS_KEY
|
|
||||||
AWS_WEB_IDENTITY_TOKEN_FILE
|
|
||||||
AZURE_COGNITIVE_SERVICES_RESOURCE_NAME
|
|
||||||
AZURE_RESOURCE_NAME
|
|
||||||
CI_PROJECT_DIR
|
|
||||||
CI_SERVER_FQDN
|
|
||||||
CI_WORKLOAD_REF
|
|
||||||
CLOUDFLARE_ACCOUNT_ID
|
|
||||||
CLOUDFLARE_API_TOKEN
|
|
||||||
CLOUDFLARE_GATEWAY_ID
|
|
||||||
CONTEXT7_API_KEY
|
|
||||||
GITHUB_TOKEN
|
|
||||||
GITLAB_AI_GATEWAY_URL
|
|
||||||
GITLAB_HOST
|
|
||||||
GITLAB_INSTANCE_URL
|
|
||||||
GITLAB_OAUTH_CLIENT_ID
|
|
||||||
GITLAB_TOKEN
|
|
||||||
GITLAB_TOKEN_OPENCODE
|
|
||||||
GOOGLE_APPLICATION_CREDENTIALS
|
|
||||||
GOOGLE_CLOUD_PROJECT
|
|
||||||
HTTP_PROXY
|
|
||||||
HTTPS_PROXY
|
|
||||||
K2_
|
|
||||||
MY_API_KEY
|
|
||||||
MY_ENV_VAR
|
|
||||||
MY_MCP_CLIENT_ID
|
|
||||||
MY_MCP_CLIENT_SECRET
|
|
||||||
NO_PROXY
|
|
||||||
NODE_ENV
|
|
||||||
NODE_EXTRA_CA_CERTS
|
|
||||||
NPM_AUTH_TOKEN
|
|
||||||
OC_ALLOW_WAYLAND
|
|
||||||
OPENCODE_API_KEY
|
|
||||||
OPENCODE_AUTH_JSON
|
|
||||||
OPENCODE_AUTO_SHARE
|
|
||||||
OPENCODE_CLIENT
|
|
||||||
OPENCODE_CONFIG
|
|
||||||
OPENCODE_CONFIG_CONTENT
|
|
||||||
OPENCODE_CONFIG_DIR
|
|
||||||
OPENCODE_DISABLE_AUTOCOMPACT
|
|
||||||
OPENCODE_DISABLE_AUTOUPDATE
|
|
||||||
OPENCODE_DISABLE_CLAUDE_CODE
|
|
||||||
OPENCODE_DISABLE_CLAUDE_CODE_PROMPT
|
|
||||||
OPENCODE_DISABLE_CLAUDE_CODE_SKILLS
|
|
||||||
OPENCODE_DISABLE_DEFAULT_PLUGINS
|
|
||||||
OPENCODE_DISABLE_LSP_DOWNLOAD
|
|
||||||
OPENCODE_DISABLE_MODELS_FETCH
|
|
||||||
OPENCODE_DISABLE_PRUNE
|
|
||||||
OPENCODE_DISABLE_TERMINAL_TITLE
|
|
||||||
OPENCODE_ENABLE_EXA
|
|
||||||
OPENCODE_ENABLE_EXPERIMENTAL_MODELS
|
|
||||||
OPENCODE_EXPERIMENTAL
|
|
||||||
OPENCODE_EXPERIMENTAL_BASH_DEFAULT_TIMEOUT_MS
|
|
||||||
OPENCODE_EXPERIMENTAL_DISABLE_COPY_ON_SELECT
|
|
||||||
OPENCODE_EXPERIMENTAL_DISABLE_FILEWATCHER
|
|
||||||
OPENCODE_EXPERIMENTAL_EXA
|
|
||||||
OPENCODE_EXPERIMENTAL_FILEWATCHER
|
|
||||||
OPENCODE_EXPERIMENTAL_ICON_DISCOVERY
|
|
||||||
OPENCODE_EXPERIMENTAL_LSP_TOOL
|
|
||||||
OPENCODE_EXPERIMENTAL_LSP_TY
|
|
||||||
OPENCODE_EXPERIMENTAL_MARKDOWN
|
|
||||||
OPENCODE_EXPERIMENTAL_OUTPUT_TOKEN_MAX
|
|
||||||
OPENCODE_EXPERIMENTAL_OXFMT
|
|
||||||
OPENCODE_EXPERIMENTAL_PLAN_MODE
|
|
||||||
OPENCODE_ENABLE_QUESTION_TOOL
|
|
||||||
OPENCODE_FAKE_VCS
|
|
||||||
OPENCODE_GIT_BASH_PATH
|
|
||||||
OPENCODE_MODEL
|
|
||||||
OPENCODE_MODELS_URL
|
|
||||||
OPENCODE_PERMISSION
|
|
||||||
OPENCODE_PORT
|
|
||||||
OPENCODE_SERVER_PASSWORD
|
|
||||||
OPENCODE_SERVER_USERNAME
|
|
||||||
PROJECT_ROOT
|
|
||||||
RESOURCE_NAME
|
|
||||||
RUST_LOG
|
|
||||||
VARIABLE_NAME
|
|
||||||
VERTEX_LOCATION
|
|
||||||
XDG_CONFIG_HOME
|
|
||||||
```
|
|
||||||
|
|
||||||
## Package/module identifiers
|
|
||||||
|
|
||||||
```text
|
|
||||||
../../../config.mjs
|
|
||||||
@astrojs/starlight/components
|
|
||||||
@opencode-ai/plugin
|
|
||||||
@opencode-ai/sdk
|
|
||||||
path
|
|
||||||
shescape
|
|
||||||
zod
|
|
||||||
|
|
||||||
@
|
|
||||||
@ai-sdk/anthropic
|
|
||||||
@ai-sdk/cerebras
|
|
||||||
@ai-sdk/google
|
|
||||||
@ai-sdk/openai
|
|
||||||
@ai-sdk/openai-compatible
|
|
||||||
@File#L37-42
|
|
||||||
@modelcontextprotocol/server-everything
|
|
||||||
@opencode
|
|
||||||
```
|
|
||||||
|
|
||||||
## GitHub owner/repo slugs referenced in docs
|
|
||||||
|
|
||||||
```text
|
|
||||||
24601/opencode-zellij-namer
|
|
||||||
angristan/opencode-wakatime
|
|
||||||
anomalyco/opencode
|
|
||||||
apps/opencode-agent
|
|
||||||
athal7/opencode-devcontainers
|
|
||||||
awesome-opencode/awesome-opencode
|
|
||||||
backnotprop/plannotator
|
|
||||||
ben-vargas/ai-sdk-provider-opencode-sdk
|
|
||||||
btriapitsyn/openchamber
|
|
||||||
BurntSushi/ripgrep
|
|
||||||
Cluster444/agentic
|
|
||||||
code-yeongyu/oh-my-opencode
|
|
||||||
darrenhinde/opencode-agents
|
|
||||||
different-ai/opencode-scheduler
|
|
||||||
different-ai/openwork
|
|
||||||
features/copilot
|
|
||||||
folke/tokyonight.nvim
|
|
||||||
franlol/opencode-md-table-formatter
|
|
||||||
ggml-org/llama.cpp
|
|
||||||
ghoulr/opencode-websearch-cited.git
|
|
||||||
H2Shami/opencode-helicone-session
|
|
||||||
hosenur/portal
|
|
||||||
jamesmurdza/daytona
|
|
||||||
jenslys/opencode-gemini-auth
|
|
||||||
JRedeker/opencode-morph-fast-apply
|
|
||||||
JRedeker/opencode-shell-strategy
|
|
||||||
kdcokenny/ocx
|
|
||||||
kdcokenny/opencode-background-agents
|
|
||||||
kdcokenny/opencode-notify
|
|
||||||
kdcokenny/opencode-workspace
|
|
||||||
kdcokenny/opencode-worktree
|
|
||||||
login/device
|
|
||||||
mohak34/opencode-notifier
|
|
||||||
morhetz/gruvbox
|
|
||||||
mtymek/opencode-obsidian
|
|
||||||
NeuralNomadsAI/CodeNomad
|
|
||||||
nick-vi/opencode-type-inject
|
|
||||||
NickvanDyke/opencode.nvim
|
|
||||||
NoeFabris/opencode-antigravity-auth
|
|
||||||
nordtheme/nord
|
|
||||||
numman-ali/opencode-openai-codex-auth
|
|
||||||
olimorris/codecompanion.nvim
|
|
||||||
panta82/opencode-notificator
|
|
||||||
rebelot/kanagawa.nvim
|
|
||||||
remorses/kimaki
|
|
||||||
sainnhe/everforest
|
|
||||||
shekohex/opencode-google-antigravity-auth
|
|
||||||
shekohex/opencode-pty.git
|
|
||||||
spoons-and-mirrors/subtask2
|
|
||||||
sudo-tee/opencode.nvim
|
|
||||||
supermemoryai/opencode-supermemory
|
|
||||||
Tarquinen/opencode-dynamic-context-pruning
|
|
||||||
Th3Whit3Wolf/one-nvim
|
|
||||||
upstash/context7
|
|
||||||
vtemian/micode
|
|
||||||
vtemian/octto
|
|
||||||
yetone/avante.nvim
|
|
||||||
zenobi-us/opencode-plugin-template
|
|
||||||
zenobi-us/opencode-skillful
|
|
||||||
```
|
|
||||||
|
|
||||||
## Paths, filenames, globs, and URLs
|
|
||||||
|
|
||||||
```text
|
|
||||||
./.opencode/themes/*.json
|
|
||||||
./<project-slug>/storage/
|
|
||||||
./config/#custom-directory
|
|
||||||
./global/storage/
|
|
||||||
.agents/skills/*/SKILL.md
|
|
||||||
.agents/skills/<name>/SKILL.md
|
|
||||||
.clang-format
|
|
||||||
.claude
|
|
||||||
.claude/skills
|
|
||||||
.claude/skills/*/SKILL.md
|
|
||||||
.claude/skills/<name>/SKILL.md
|
|
||||||
.env
|
|
||||||
.github/workflows/opencode.yml
|
|
||||||
.gitignore
|
|
||||||
.gitlab-ci.yml
|
|
||||||
.ignore
|
|
||||||
.NET SDK
|
|
||||||
.npmrc
|
|
||||||
.ocamlformat
|
|
||||||
.opencode
|
|
||||||
.opencode/
|
|
||||||
.opencode/agents/
|
|
||||||
.opencode/commands/
|
|
||||||
.opencode/commands/test.md
|
|
||||||
.opencode/modes/
|
|
||||||
.opencode/plans/*.md
|
|
||||||
.opencode/plugins/
|
|
||||||
.opencode/skills/<name>/SKILL.md
|
|
||||||
.opencode/skills/git-release/SKILL.md
|
|
||||||
.opencode/tools/
|
|
||||||
.well-known/opencode
|
|
||||||
{ type: "raw" \| "patch", content: string }
|
|
||||||
{file:path/to/file}
|
|
||||||
**/*.js
|
|
||||||
%USERPROFILE%/intelephense/license.txt
|
|
||||||
%USERPROFILE%\.cache\opencode
|
|
||||||
%USERPROFILE%\.config\opencode\opencode.jsonc
|
|
||||||
%USERPROFILE%\.config\opencode\plugins
|
|
||||||
%USERPROFILE%\.local\share\opencode
|
|
||||||
%USERPROFILE%\.local\share\opencode\log
|
|
||||||
<project-root>/.opencode/themes/*.json
|
|
||||||
<providerId>/<modelId>
|
|
||||||
<your-project>/.opencode/plugins/
|
|
||||||
~
|
|
||||||
~/...
|
|
||||||
~/.agents/skills/*/SKILL.md
|
|
||||||
~/.agents/skills/<name>/SKILL.md
|
|
||||||
~/.aws/credentials
|
|
||||||
~/.bashrc
|
|
||||||
~/.cache/opencode
|
|
||||||
~/.cache/opencode/node_modules/
|
|
||||||
~/.claude/CLAUDE.md
|
|
||||||
~/.claude/skills/
|
|
||||||
~/.claude/skills/*/SKILL.md
|
|
||||||
~/.claude/skills/<name>/SKILL.md
|
|
||||||
~/.config/opencode
|
|
||||||
~/.config/opencode/AGENTS.md
|
|
||||||
~/.config/opencode/agents/
|
|
||||||
~/.config/opencode/commands/
|
|
||||||
~/.config/opencode/modes/
|
|
||||||
~/.config/opencode/opencode.json
|
|
||||||
~/.config/opencode/opencode.jsonc
|
|
||||||
~/.config/opencode/plugins/
|
|
||||||
~/.config/opencode/skills/*/SKILL.md
|
|
||||||
~/.config/opencode/skills/<name>/SKILL.md
|
|
||||||
~/.config/opencode/themes/*.json
|
|
||||||
~/.config/opencode/tools/
|
|
||||||
~/.config/zed/settings.json
|
|
||||||
~/.local/share
|
|
||||||
~/.local/share/opencode/
|
|
||||||
~/.local/share/opencode/auth.json
|
|
||||||
~/.local/share/opencode/log/
|
|
||||||
~/.local/share/opencode/mcp-auth.json
|
|
||||||
~/.local/share/opencode/opencode.jsonc
|
|
||||||
~/.npmrc
|
|
||||||
~/.zshrc
|
|
||||||
~/code/
|
|
||||||
~/Library/Application Support
|
|
||||||
~/projects/*
|
|
||||||
~/projects/personal/
|
|
||||||
${config.github}/blob/dev/packages/sdk/js/src/gen/types.gen.ts
|
|
||||||
$HOME/intelephense/license.txt
|
|
||||||
$HOME/projects/*
|
|
||||||
$XDG_CONFIG_HOME/opencode/themes/*.json
|
|
||||||
agent/
|
|
||||||
agents/
|
|
||||||
build/
|
|
||||||
commands/
|
|
||||||
dist/
|
|
||||||
http://<wsl-ip>:4096
|
|
||||||
http://127.0.0.1:8080/callback
|
|
||||||
http://localhost:<port>
|
|
||||||
http://localhost:4096
|
|
||||||
http://localhost:4096/doc
|
|
||||||
https://app.example.com
|
|
||||||
https://AZURE_COGNITIVE_SERVICES_RESOURCE_NAME.cognitiveservices.azure.com/
|
|
||||||
https://opencode.ai/zen/v1/chat/completions
|
|
||||||
https://opencode.ai/zen/v1/messages
|
|
||||||
https://opencode.ai/zen/v1/models/gemini-3-flash
|
|
||||||
https://opencode.ai/zen/v1/models/gemini-3-pro
|
|
||||||
https://opencode.ai/zen/v1/responses
|
|
||||||
https://RESOURCE_NAME.openai.azure.com/
|
|
||||||
laravel/pint
|
|
||||||
log/
|
|
||||||
model: "anthropic/claude-sonnet-4-5"
|
|
||||||
modes/
|
|
||||||
node_modules/
|
|
||||||
openai/gpt-4.1
|
|
||||||
opencode.ai/config.json
|
|
||||||
opencode/<model-id>
|
|
||||||
opencode/gpt-5.1-codex
|
|
||||||
opencode/gpt-5.2-codex
|
|
||||||
opencode/kimi-k2
|
|
||||||
openrouter/google/gemini-2.5-flash
|
|
||||||
opncd.ai/s/<share-id>
|
|
||||||
packages/*/AGENTS.md
|
|
||||||
plugins/
|
|
||||||
project/
|
|
||||||
provider_id/model_id
|
|
||||||
provider/model
|
|
||||||
provider/model-id
|
|
||||||
rm -rf ~/.cache/opencode
|
|
||||||
skills/
|
|
||||||
skills/*/SKILL.md
|
|
||||||
src/**/*.ts
|
|
||||||
themes/
|
|
||||||
tools/
|
|
||||||
```
|
|
||||||
|
|
||||||
## Keybind strings
|
|
||||||
|
|
||||||
```text
|
|
||||||
alt+b
|
|
||||||
Alt+Ctrl+K
|
|
||||||
alt+d
|
|
||||||
alt+f
|
|
||||||
Cmd+Esc
|
|
||||||
Cmd+Option+K
|
|
||||||
Cmd+Shift+Esc
|
|
||||||
Cmd+Shift+G
|
|
||||||
Cmd+Shift+P
|
|
||||||
ctrl+a
|
|
||||||
ctrl+b
|
|
||||||
ctrl+d
|
|
||||||
ctrl+e
|
|
||||||
Ctrl+Esc
|
|
||||||
ctrl+f
|
|
||||||
ctrl+g
|
|
||||||
ctrl+k
|
|
||||||
Ctrl+Shift+Esc
|
|
||||||
Ctrl+Shift+P
|
|
||||||
ctrl+t
|
|
||||||
ctrl+u
|
|
||||||
ctrl+w
|
|
||||||
ctrl+x
|
|
||||||
DELETE
|
|
||||||
Shift+Enter
|
|
||||||
WIN+R
|
|
||||||
```
|
|
||||||
|
|
||||||
## Model ID strings referenced
|
|
||||||
|
|
||||||
```text
|
|
||||||
{env:OPENCODE_MODEL}
|
|
||||||
anthropic/claude-3-5-sonnet-20241022
|
|
||||||
anthropic/claude-haiku-4-20250514
|
|
||||||
anthropic/claude-haiku-4-5
|
|
||||||
anthropic/claude-sonnet-4-20250514
|
|
||||||
anthropic/claude-sonnet-4-5
|
|
||||||
gitlab/duo-chat-haiku-4-5
|
|
||||||
lmstudio/google/gemma-3n-e4b
|
|
||||||
openai/gpt-4.1
|
|
||||||
openai/gpt-5
|
|
||||||
opencode/gpt-5.1-codex
|
|
||||||
opencode/gpt-5.2-codex
|
|
||||||
opencode/kimi-k2
|
|
||||||
openrouter/google/gemini-2.5-flash
|
|
||||||
```
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
---
|
---
|
||||||
mode: primary
|
mode: primary
|
||||||
hidden: true
|
hidden: true
|
||||||
model: opencode/minimax-m2.5
|
model: opencode/gpt-5.4-nano
|
||||||
color: "#44BA81"
|
color: "#44BA81"
|
||||||
tools:
|
tools:
|
||||||
"*": false
|
"*": false
|
||||||
|
|
@ -14,127 +14,30 @@ Use your github-triage tool to triage issues.
|
||||||
|
|
||||||
This file is the source of truth for ownership/routing rules.
|
This file is the source of truth for ownership/routing rules.
|
||||||
|
|
||||||
## Labels
|
Assign issues by choosing the team with the strongest overlap. The github-triage tool will assign a random member from that team.
|
||||||
|
|
||||||
### windows
|
Do not add labels to issues. Only assign an owner.
|
||||||
|
|
||||||
Use for any issue that mentions Windows (the OS). Be sure they are saying that they are on Windows.
|
When calling github-triage, pass one of these team values: tui, desktop_web, core, inference, windows.
|
||||||
|
|
||||||
- Use if they mention WSL too
|
## Teams
|
||||||
|
|
||||||
#### perf
|
### TUI
|
||||||
|
|
||||||
Performance-related issues:
|
Terminal UI issues, including rendering, keybindings, scrolling, terminal compatibility, SSH behavior, crashes in the TUI, and low-level TUI performance.
|
||||||
|
|
||||||
- Slow performance
|
### Desktop / Web
|
||||||
- High RAM usage
|
|
||||||
- High CPU usage
|
|
||||||
|
|
||||||
**Only** add if it's likely a RAM or CPU issue. **Do not** add for LLM slowness.
|
Desktop application and browser-based app issues, including `opencode web`, desktop-specific UI behavior, packaging, and web view problems.
|
||||||
|
|
||||||
#### desktop
|
### Core
|
||||||
|
|
||||||
Desktop app issues:
|
Core opencode server and harness issues, including sqlite, snapshots, memory, API behavior, agent context construction, tool execution, provider integrations, model behavior, documentation, and larger architectural features.
|
||||||
|
|
||||||
- `opencode web` command
|
### Inference
|
||||||
- The desktop app itself
|
|
||||||
|
|
||||||
**Only** add if it's specifically about the Desktop application or `opencode web` view. **Do not** add for terminal, TUI, or general opencode issues.
|
OpenCode Zen, OpenCode Go, and billing issues.
|
||||||
|
|
||||||
#### nix
|
### Windows
|
||||||
|
|
||||||
**Only** add if the issue explicitly mentions nix.
|
Windows-specific issues, including native Windows behavior, WSL interactions, path handling, shell compatibility, and installation or runtime problems that only happen on Windows.
|
||||||
|
|
||||||
If the issue does not mention nix, do not add nix.
|
|
||||||
|
|
||||||
If the issue mentions nix, assign to `rekram1-node`.
|
|
||||||
|
|
||||||
#### zen
|
|
||||||
|
|
||||||
**Only** add if the issue mentions "zen" or "opencode zen" or "opencode black".
|
|
||||||
|
|
||||||
If the issue doesn't have "zen" or "opencode black" in it then don't add zen label
|
|
||||||
|
|
||||||
#### core
|
|
||||||
|
|
||||||
Use for core server issues in `packages/opencode/`, excluding `packages/opencode/src/cli/cmd/tui/`.
|
|
||||||
|
|
||||||
Examples:
|
|
||||||
|
|
||||||
- LSP server behavior
|
|
||||||
- Harness behavior (agent + tools)
|
|
||||||
- Feature requests for server behavior
|
|
||||||
- Agent context construction
|
|
||||||
- API endpoints
|
|
||||||
- Provider integration issues
|
|
||||||
- New, broken, or poor-quality models
|
|
||||||
|
|
||||||
#### acp
|
|
||||||
|
|
||||||
If the issue mentions acp support, assign acp label.
|
|
||||||
|
|
||||||
#### docs
|
|
||||||
|
|
||||||
Add if the issue requests better documentation or docs updates.
|
|
||||||
|
|
||||||
#### opentui
|
|
||||||
|
|
||||||
TUI issues potentially caused by our underlying TUI library:
|
|
||||||
|
|
||||||
- Keybindings not working
|
|
||||||
- Scroll speed issues (too fast/slow/laggy)
|
|
||||||
- Screen flickering
|
|
||||||
- Crashes with opentui in the log
|
|
||||||
|
|
||||||
**Do not** add for general TUI bugs.
|
|
||||||
|
|
||||||
When assigning to people here are the following rules:
|
|
||||||
|
|
||||||
Desktop / Web:
|
|
||||||
Use for desktop-labeled issues only.
|
|
||||||
|
|
||||||
- adamdotdevin
|
|
||||||
- iamdavidhill
|
|
||||||
- Brendonovich
|
|
||||||
- nexxeln
|
|
||||||
|
|
||||||
Zen:
|
|
||||||
ONLY assign if the issue will have the "zen" label.
|
|
||||||
|
|
||||||
- fwang
|
|
||||||
- MrMushrooooom
|
|
||||||
|
|
||||||
TUI (`packages/opencode/src/cli/cmd/tui/...`):
|
|
||||||
|
|
||||||
- thdxr for TUI UX/UI product decisions and interaction flow
|
|
||||||
- kommander for OpenTUI engine issues: rendering artifacts, keybind handling, terminal compatibility, SSH behavior, and low-level perf bottlenecks
|
|
||||||
- rekram1-node for TUI bugs that are not clearly OpenTUI engine issues
|
|
||||||
|
|
||||||
Core (`packages/opencode/...`, excluding TUI subtree):
|
|
||||||
|
|
||||||
- thdxr for sqlite/snapshot/memory bugs and larger architectural core features
|
|
||||||
- jlongster for opencode server + API feature work (tool currently remaps jlongster -> thdxr until assignable)
|
|
||||||
- rekram1-node for harness issues, provider issues, and other bug-squashing
|
|
||||||
|
|
||||||
For core bugs that do not clearly map, either thdxr or rekram1-node is acceptable.
|
|
||||||
|
|
||||||
Docs:
|
|
||||||
|
|
||||||
- R44VC0RP
|
|
||||||
|
|
||||||
Windows:
|
|
||||||
|
|
||||||
- Hona (assign any issue that mentions Windows or is likely Windows-specific)
|
|
||||||
|
|
||||||
Determinism rules:
|
|
||||||
|
|
||||||
- If title + body does not contain "zen", do not add the "zen" label
|
|
||||||
- If "nix" label is added but title + body does not mention nix/nixos, the tool will drop "nix"
|
|
||||||
- If title + body mentions nix/nixos, assign to `rekram1-node`
|
|
||||||
- If "desktop" label is added, the tool will override assignee and randomly pick one Desktop / Web owner
|
|
||||||
|
|
||||||
In all other cases, choose the team/section with the most overlap with the issue and assign a member from that team at random.
|
|
||||||
|
|
||||||
ACP:
|
|
||||||
|
|
||||||
- rekram1-node (assign any acp issues to rekram1-node)
|
|
||||||
|
|
|
||||||
|
|
@ -18,9 +18,12 @@ Do not use `git log` or author metadata when deciding attribution.
|
||||||
|
|
||||||
Rules:
|
Rules:
|
||||||
|
|
||||||
- Write the final file with sections in this order:
|
- Write the final file with release sections in this order:
|
||||||
`## Core`, `## TUI`, `## Desktop`, `## SDK`, `## Extensions`
|
`## Core`, `## TUI`, `## Desktop`, `## SDK`, `## Extensions`
|
||||||
- Only include sections that have at least one notable entry
|
- Only include sections that have at least one notable entry
|
||||||
|
- Within each release section, keep bug fixes grouped under `### Bugfixes`
|
||||||
|
- Keep other notable entries under `### Improvements` when a section has bug fixes too
|
||||||
|
- Omit empty subsections
|
||||||
- Keep one bullet per commit you keep
|
- Keep one bullet per commit you keep
|
||||||
- Skip commits that are entirely internal, CI, tests, refactors, or otherwise not user-facing
|
- Skip commits that are entirely internal, CI, tests, refactors, or otherwise not user-facing
|
||||||
- Start each bullet with a capital letter
|
- Start each bullet with a capital letter
|
||||||
|
|
|
||||||
14
.opencode/command/translate.md
Normal file
14
.opencode/command/translate.md
Normal file
|
|
@ -0,0 +1,14 @@
|
||||||
|
---
|
||||||
|
description: translate English to other languages
|
||||||
|
model: opencode/claude-opus-4-7
|
||||||
|
---
|
||||||
|
|
||||||
|
run git diff and translate changed english doc and UI copy files to other international languages. Translate all languages in parallel to save time.
|
||||||
|
|
||||||
|
Requirements:
|
||||||
|
|
||||||
|
- Preserve meaning, intent, tone, and formatting (including Markdown/MDX structure).
|
||||||
|
- Preserve all technical terms and artifacts exactly: product/company names, API names, identifiers, code, commands/flags, file paths, URLs, versions, error messages, config keys/values, and anything inside inline code or code blocks.
|
||||||
|
- Also preserve every term listed in the Do-Not-Translate glossary below.
|
||||||
|
- Also apply locale-specific guidance from `.opencode/glossary/<locale>.md` when available (for example, `zh-cn.md`).
|
||||||
|
- Do not modify fenced code blocks.
|
||||||
|
|
@ -1,21 +1,38 @@
|
||||||
---
|
---
|
||||||
name: effect
|
name: effect
|
||||||
description: Answer questions about the Effect framework
|
description: Work with Effect v4 / effect-smol TypeScript code in this repo
|
||||||
---
|
---
|
||||||
|
|
||||||
# Effect
|
# Effect
|
||||||
|
|
||||||
This codebase uses Effect, a framework for writing typescript.
|
This codebase uses Effect for typed, composable TypeScript services, schemas, and workflows.
|
||||||
|
|
||||||
## How to Answer Effect Questions
|
## Source Of Truth
|
||||||
|
|
||||||
1. Clone the Effect repository: `https://github.com/Effect-TS/effect-smol` to
|
Use the current Effect v4 / effect-smol source, not memory or older Effect v2/v3 examples.
|
||||||
`.opencode/references/effect-smol` in this project NOT the skill folder.
|
|
||||||
2. Use the explore agent to search the codebase for answers about Effect patterns, APIs, and concepts
|
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.
|
||||||
3. Provide responses based on the actual Effect source code and documentation
|
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.
|
||||||
|
|
||||||
## Guidelines
|
## Guidelines
|
||||||
|
|
||||||
- Always use the explore agent with the cloned repository when answering Effect-related questions
|
- Prefer current Effect v4 APIs and project-local patterns over old blog posts, examples, or package-memory guesses.
|
||||||
- Reference specific files and patterns found in the Effect codebase
|
- Use `Effect.gen(function* () { ... })` for multi-step workflows.
|
||||||
- Do not answer from memory - always verify against the source
|
- Use `Effect.fn("Name")` or `Effect.fnUntraced(...)` for named effects when adding reusable service methods or important workflows.
|
||||||
|
- Prefer Effect `Schema` for API and domain data shapes. Use branded schemas for IDs and `Schema.TaggedErrorClass` for typed domain errors when modeling new error surfaces.
|
||||||
|
- Keep HTTP handlers thin: decode input, read request context, call services, and map transport errors. Put business rules in services.
|
||||||
|
- In Effect service code, prefer Effect-aware platform abstractions and dependencies over ad hoc promises where the surrounding code already does so.
|
||||||
|
- 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-smol` or nearby code first.
|
||||||
|
|
||||||
|
## Testing Patterns
|
||||||
|
|
||||||
|
- 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/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,16 +1,14 @@
|
||||||
/// <reference path="../env.d.ts" />
|
/// <reference path="../env.d.ts" />
|
||||||
import { tool } from "@opencode-ai/plugin"
|
import { tool } from "@opencode-ai/plugin"
|
||||||
|
|
||||||
const TEAM = {
|
const TEAM = {
|
||||||
desktop: ["adamdotdevin", "iamdavidhill", "Brendonovich", "nexxeln"],
|
tui: ["kommander", "simonklee"],
|
||||||
zen: ["fwang", "MrMushrooooom"],
|
desktop_web: ["Hona", "Brendonovich"],
|
||||||
tui: ["thdxr", "kommander", "rekram1-node"],
|
core: ["jlongster", "rekram1-node", "nexxeln", "kitlangton"],
|
||||||
core: ["thdxr", "rekram1-node", "jlongster"],
|
inference: ["fwang", "MrMushrooooom"],
|
||||||
docs: ["R44VC0RP"],
|
|
||||||
windows: ["Hona"],
|
windows: ["Hona"],
|
||||||
} as const
|
} as const
|
||||||
|
|
||||||
const ASSIGNEES = [...new Set(Object.values(TEAM).flat())]
|
|
||||||
|
|
||||||
function pick<T>(items: readonly T[]) {
|
function pick<T>(items: readonly T[]) {
|
||||||
return items[Math.floor(Math.random() * items.length)]!
|
return items[Math.floor(Math.random() * items.length)]!
|
||||||
}
|
}
|
||||||
|
|
@ -38,79 +36,25 @@ async function githubFetch(endpoint: string, options: RequestInit = {}) {
|
||||||
}
|
}
|
||||||
|
|
||||||
export default tool({
|
export default tool({
|
||||||
description: `Use this tool to assign and/or label a GitHub issue.
|
description: `Use this tool to assign a GitHub issue.
|
||||||
|
|
||||||
Choose labels and assignee using the current triage policy and ownership rules.
|
Provide the team that should own the issue. This tool picks a random assignee from that team and does not apply labels.`,
|
||||||
Pick the most fitting labels for the issue and assign one owner.
|
|
||||||
|
|
||||||
If unsure, choose the team/section with the most overlap with the issue and assign a member from that team at random.`,
|
|
||||||
args: {
|
args: {
|
||||||
assignee: tool.schema
|
team: tool.schema
|
||||||
.enum(ASSIGNEES as [string, ...string[]])
|
.enum(Object.keys(TEAM) as [keyof typeof TEAM, ...(keyof typeof TEAM)[]])
|
||||||
.describe("The username of the assignee")
|
.describe("The owning team"),
|
||||||
.default("rekram1-node"),
|
|
||||||
labels: tool.schema
|
|
||||||
.array(tool.schema.enum(["nix", "opentui", "perf", "web", "desktop", "zen", "docs", "windows", "core"]))
|
|
||||||
.describe("The labels(s) to add to the issue")
|
|
||||||
.default([]),
|
|
||||||
},
|
},
|
||||||
async execute(args) {
|
async execute(args) {
|
||||||
const issue = getIssueNumber()
|
const issue = getIssueNumber()
|
||||||
const owner = "anomalyco"
|
const owner = "anomalyco"
|
||||||
const repo = "opencode"
|
const repo = "opencode"
|
||||||
|
const assignee = pick(TEAM[args.team])
|
||||||
const results: string[] = []
|
|
||||||
let labels = [...new Set(args.labels.map((x) => (x === "desktop" ? "web" : x)))]
|
|
||||||
const web = labels.includes("web")
|
|
||||||
const text = `${process.env.ISSUE_TITLE ?? ""}\n${process.env.ISSUE_BODY ?? ""}`.toLowerCase()
|
|
||||||
const zen = /\bzen\b/.test(text) || text.includes("opencode black")
|
|
||||||
const nix = /\bnix(os)?\b/.test(text)
|
|
||||||
|
|
||||||
if (labels.includes("nix") && !nix) {
|
|
||||||
labels = labels.filter((x) => x !== "nix")
|
|
||||||
results.push("Dropped label: nix (issue does not mention nix)")
|
|
||||||
}
|
|
||||||
|
|
||||||
const assignee = nix ? "rekram1-node" : web ? pick(TEAM.desktop) : args.assignee
|
|
||||||
|
|
||||||
if (labels.includes("zen") && !zen) {
|
|
||||||
throw new Error("Only add the zen label when issue title/body contains 'zen'")
|
|
||||||
}
|
|
||||||
|
|
||||||
if (web && !nix && !(TEAM.desktop as readonly string[]).includes(assignee)) {
|
|
||||||
throw new Error("Web issues must be assigned to adamdotdevin, iamdavidhill, Brendonovich, or nexxeln")
|
|
||||||
}
|
|
||||||
|
|
||||||
if ((TEAM.zen as readonly string[]).includes(assignee) && !labels.includes("zen")) {
|
|
||||||
throw new Error("Only zen issues should be assigned to fwang or MrMushrooooom")
|
|
||||||
}
|
|
||||||
|
|
||||||
if (assignee === "Hona" && !labels.includes("windows")) {
|
|
||||||
throw new Error("Only windows issues should be assigned to Hona")
|
|
||||||
}
|
|
||||||
|
|
||||||
if (assignee === "R44VC0RP" && !labels.includes("docs")) {
|
|
||||||
throw new Error("Only docs issues should be assigned to R44VC0RP")
|
|
||||||
}
|
|
||||||
|
|
||||||
if (assignee === "kommander" && !labels.includes("opentui")) {
|
|
||||||
throw new Error("Only opentui issues should be assigned to kommander")
|
|
||||||
}
|
|
||||||
|
|
||||||
await githubFetch(`/repos/${owner}/${repo}/issues/${issue}/assignees`, {
|
await githubFetch(`/repos/${owner}/${repo}/issues/${issue}/assignees`, {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
body: JSON.stringify({ assignees: [assignee] }),
|
body: JSON.stringify({ assignees: [assignee] }),
|
||||||
})
|
})
|
||||||
results.push(`Assigned @${assignee} to issue #${issue}`)
|
|
||||||
|
|
||||||
if (labels.length > 0) {
|
return `Assigned @${assignee} from ${args.team} to issue #${issue}`
|
||||||
await githubFetch(`/repos/${owner}/${repo}/issues/${issue}/labels`, {
|
|
||||||
method: "POST",
|
|
||||||
body: JSON.stringify({ labels }),
|
|
||||||
})
|
|
||||||
results.push(`Added labels: ${labels.join(", ")}`)
|
|
||||||
}
|
|
||||||
|
|
||||||
return results.join("\n")
|
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
|
||||||
|
|
@ -132,7 +132,7 @@ It's very similar to Claude Code in terms of capability. Here are the key differ
|
||||||
|
|
||||||
- 100% open source
|
- 100% open source
|
||||||
- Not coupled to any provider. Although we recommend the models we provide through [OpenCode Zen](https://opencode.ai/zen), OpenCode can be used with Claude, OpenAI, Google, or even local models. As models evolve, the gaps between them will close and pricing will drop, so being provider-agnostic is important.
|
- Not coupled to any provider. Although we recommend the models we provide through [OpenCode Zen](https://opencode.ai/zen), OpenCode can be used with Claude, OpenAI, Google, or even local models. As models evolve, the gaps between them will close and pricing will drop, so being provider-agnostic is important.
|
||||||
- Out-of-the-box LSP support
|
- Built-in opt-in LSP support
|
||||||
- A focus on TUI. OpenCode is built by neovim users and the creators of [terminal.shop](https://terminal.shop); we are going to push the limits of what's possible in the terminal.
|
- A focus on TUI. OpenCode is built by neovim users and the creators of [terminal.shop](https://terminal.shop); we are going to push the limits of what's possible in the terminal.
|
||||||
- A client/server architecture. This, for example, can allow OpenCode to run on your computer while you drive it remotely from a mobile app, meaning that the TUI frontend is just one of the possible clients.
|
- A client/server architecture. This, for example, can allow OpenCode to run on your computer while you drive it remotely from a mobile app, meaning that the TUI frontend is just one of the possible clients.
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -115,6 +115,27 @@ const zenLiteCouponFirstMonth100 = new stripe.Coupon("ZenLiteCouponFirstMonth100
|
||||||
appliesToProducts: [zenLiteProduct.id],
|
appliesToProducts: [zenLiteProduct.id],
|
||||||
duration: "once",
|
duration: "once",
|
||||||
})
|
})
|
||||||
|
const zenLiteCouponThreeMonths100 = new stripe.Coupon("ZenLiteCoupon3Months100", {
|
||||||
|
name: "3 months 100% off",
|
||||||
|
percentOff: 100,
|
||||||
|
appliesToProducts: [zenLiteProduct.id],
|
||||||
|
duration: "repeating",
|
||||||
|
durationInMonths: 3,
|
||||||
|
})
|
||||||
|
const zenLiteCouponSixMonths100 = new stripe.Coupon("ZenLiteCoupon6Months100", {
|
||||||
|
name: "6 months 100% off",
|
||||||
|
percentOff: 100,
|
||||||
|
appliesToProducts: [zenLiteProduct.id],
|
||||||
|
duration: "repeating",
|
||||||
|
durationInMonths: 6,
|
||||||
|
})
|
||||||
|
const zenLiteCouponTwelveMonths100 = new stripe.Coupon("ZenLiteCoupon12Months100", {
|
||||||
|
name: "12 months 100% off",
|
||||||
|
percentOff: 100,
|
||||||
|
appliesToProducts: [zenLiteProduct.id],
|
||||||
|
duration: "repeating",
|
||||||
|
durationInMonths: 12,
|
||||||
|
})
|
||||||
const zenLitePrice = new stripe.Price("ZenLitePrice", {
|
const zenLitePrice = new stripe.Price("ZenLitePrice", {
|
||||||
product: zenLiteProduct.id,
|
product: zenLiteProduct.id,
|
||||||
currency: "usd",
|
currency: "usd",
|
||||||
|
|
@ -131,6 +152,9 @@ const ZEN_LITE_PRICE = new sst.Linkable("ZEN_LITE_PRICE", {
|
||||||
priceInr: 92900,
|
priceInr: 92900,
|
||||||
firstMonth50Coupon: zenLiteCouponFirstMonth50.id,
|
firstMonth50Coupon: zenLiteCouponFirstMonth50.id,
|
||||||
firstMonth100Coupon: zenLiteCouponFirstMonth100.id,
|
firstMonth100Coupon: zenLiteCouponFirstMonth100.id,
|
||||||
|
threeMonths100Coupon: zenLiteCouponThreeMonths100.id,
|
||||||
|
sixMonths100Coupon: zenLiteCouponSixMonths100.id,
|
||||||
|
twelveMonths100Coupon: zenLiteCouponTwelveMonths100.id,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
@ -197,6 +221,9 @@ const AUTH_API_URL = new sst.Linkable("AUTH_API_URL", {
|
||||||
const STRIPE_WEBHOOK_SECRET = new sst.Linkable("STRIPE_WEBHOOK_SECRET", {
|
const STRIPE_WEBHOOK_SECRET = new sst.Linkable("STRIPE_WEBHOOK_SECRET", {
|
||||||
properties: { value: stripeWebhook.secret },
|
properties: { value: stripeWebhook.secret },
|
||||||
})
|
})
|
||||||
|
const INCIDENT_WEBHOOK_SIGNING_SECRET = new sst.Secret("INCIDENT_WEBHOOK_SIGNING_SECRET")
|
||||||
|
const DISCORD_INCIDENT_WEBHOOK_URL = new sst.Secret("DISCORD_INCIDENT_WEBHOOK_URL")
|
||||||
|
|
||||||
const gatewayKv = new sst.cloudflare.Kv("GatewayKv")
|
const gatewayKv = new sst.cloudflare.Kv("GatewayKv")
|
||||||
|
|
||||||
////////////////
|
////////////////
|
||||||
|
|
@ -227,6 +254,8 @@ new sst.cloudflare.x.SolidStart("Console", {
|
||||||
database,
|
database,
|
||||||
AUTH_API_URL,
|
AUTH_API_URL,
|
||||||
STRIPE_WEBHOOK_SECRET,
|
STRIPE_WEBHOOK_SECRET,
|
||||||
|
INCIDENT_WEBHOOK_SIGNING_SECRET,
|
||||||
|
DISCORD_INCIDENT_WEBHOOK_URL,
|
||||||
STRIPE_SECRET_KEY,
|
STRIPE_SECRET_KEY,
|
||||||
EMAILOCTOPUS_API_KEY,
|
EMAILOCTOPUS_API_KEY,
|
||||||
AWS_SES_ACCESS_KEY_ID,
|
AWS_SES_ACCESS_KEY_ID,
|
||||||
|
|
|
||||||
320
infra/monitoring.ts
Normal file
320
infra/monitoring.ts
Normal file
|
|
@ -0,0 +1,320 @@
|
||||||
|
const displayName = (s: string) =>
|
||||||
|
s
|
||||||
|
.split("-")
|
||||||
|
.map((w) => w.charAt(0).toUpperCase() + w.slice(1))
|
||||||
|
.join(" ")
|
||||||
|
.replace(/(?<=\d) (?=\d)/g, ".")
|
||||||
|
|
||||||
|
const resourceName = (s: string) => displayName(s).replace(/[^a-zA-Z0-9]/g, "")
|
||||||
|
|
||||||
|
const varSpec = (label: string, name: string) =>
|
||||||
|
$jsonStringify({
|
||||||
|
content: [
|
||||||
|
{
|
||||||
|
content: [
|
||||||
|
{
|
||||||
|
attrs: {
|
||||||
|
name,
|
||||||
|
label,
|
||||||
|
missing: false,
|
||||||
|
},
|
||||||
|
type: "varSpec",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
type: "paragraph",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
type: "doc",
|
||||||
|
})
|
||||||
|
|
||||||
|
const fields = {
|
||||||
|
model: incident.getAlertAttributeOutput({ name: "Model" }),
|
||||||
|
product: incident.getAlertAttributeOutput({ name: "Product" }),
|
||||||
|
}
|
||||||
|
|
||||||
|
const alertSource = new incident.AlertSource("HoneycombAlertSource", {
|
||||||
|
name: $app.stage === "production" ? "Honeycomb" : `Honeycomb (${$app.stage})`,
|
||||||
|
sourceType: "honeycomb",
|
||||||
|
template: {
|
||||||
|
title: {
|
||||||
|
literal: varSpec("Payload -> Title", "title"),
|
||||||
|
},
|
||||||
|
description: {
|
||||||
|
literal: varSpec("Payload -> Description", "description"),
|
||||||
|
},
|
||||||
|
attributes: [
|
||||||
|
{
|
||||||
|
alertAttributeId: fields.model.id,
|
||||||
|
binding: {
|
||||||
|
value: {
|
||||||
|
reference: 'expressions["model"]',
|
||||||
|
},
|
||||||
|
mergeStrategy: "first_wins",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
alertAttributeId: fields.product.id,
|
||||||
|
binding: {
|
||||||
|
value: {
|
||||||
|
reference: 'expressions["product"]',
|
||||||
|
},
|
||||||
|
mergeStrategy: "first_wins",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
expressions: [
|
||||||
|
{
|
||||||
|
label: "Model",
|
||||||
|
operations: [
|
||||||
|
{
|
||||||
|
operationType: "parse",
|
||||||
|
parse: {
|
||||||
|
returns: {
|
||||||
|
array: false,
|
||||||
|
type: fields.model.type,
|
||||||
|
},
|
||||||
|
source: "$['model']",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
reference: "model",
|
||||||
|
rootReference: "payload",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Product",
|
||||||
|
operations: [
|
||||||
|
{
|
||||||
|
operationType: "parse",
|
||||||
|
parse: {
|
||||||
|
returns: {
|
||||||
|
array: false,
|
||||||
|
type: fields.product.type,
|
||||||
|
},
|
||||||
|
source: "$['product']",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
reference: "product",
|
||||||
|
rootReference: "payload",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
const webhookRecipient = new honeycomb.WebhookRecipient(`IncidentWebhook`, {
|
||||||
|
name: $app.stage === "production" ? "Incident.io" : `Incident.io (${$app.stage})`,
|
||||||
|
url: alertSource.alertEventsUrl,
|
||||||
|
secret: alertSource.secretToken,
|
||||||
|
templates: [
|
||||||
|
{
|
||||||
|
type: "trigger",
|
||||||
|
body: $jsonStringify({
|
||||||
|
title: "{{ .Name }}",
|
||||||
|
description: "{{ .Description }}",
|
||||||
|
status: "{{ .Alert.Status }}",
|
||||||
|
deduplication_key: "{{ .Alert.InstanceID }}",
|
||||||
|
source_url: "{{ .Result.URL }}",
|
||||||
|
model: "{{ .Vars.model }}",
|
||||||
|
product: "{{ .Vars.product }}",
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
variables: [
|
||||||
|
{
|
||||||
|
name: "model",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "product",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
})
|
||||||
|
|
||||||
|
new incident.AlertRoute("HoneycombAlertRoute", {
|
||||||
|
name: $app.stage === "production" ? "Honeycomb" : `Honeycomb (${$app.stage})`,
|
||||||
|
enabled: true,
|
||||||
|
isPrivate: false,
|
||||||
|
alertSources: [
|
||||||
|
{
|
||||||
|
alertSourceId: alertSource.id,
|
||||||
|
conditionGroups: [
|
||||||
|
{
|
||||||
|
conditions: [
|
||||||
|
{
|
||||||
|
subject: "alert.title",
|
||||||
|
operation: "is_set",
|
||||||
|
paramBindings: [],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
conditionGroups: [
|
||||||
|
{
|
||||||
|
conditions: [
|
||||||
|
{
|
||||||
|
subject: "alert.title",
|
||||||
|
operation: "is_set",
|
||||||
|
paramBindings: [],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
expressions: [],
|
||||||
|
escalationConfig: {
|
||||||
|
autoCancelEscalations: true,
|
||||||
|
escalationTargets: [],
|
||||||
|
},
|
||||||
|
incidentConfig: {
|
||||||
|
autoDeclineEnabled: true,
|
||||||
|
enabled: true,
|
||||||
|
conditionGroups: [],
|
||||||
|
deferTimeSeconds: 0,
|
||||||
|
groupingKeys: [
|
||||||
|
{
|
||||||
|
reference: $interpolate`alert.attributes.${fields.model.id}`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
reference: $interpolate`alert.attributes.${fields.product.id}`,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
groupingWindowSeconds: 900,
|
||||||
|
},
|
||||||
|
incidentTemplate: {
|
||||||
|
name: {
|
||||||
|
value: {
|
||||||
|
literal: varSpec("Alert -> Title", "alert.title"),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
summary: {
|
||||||
|
value: {
|
||||||
|
literal: varSpec("Alert -> Description", "alert.description"),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
startInTriage: {
|
||||||
|
value: {
|
||||||
|
literal: "true",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
severity: {
|
||||||
|
mergeStrategy: "first-wins",
|
||||||
|
},
|
||||||
|
incidentMode: {
|
||||||
|
value: {
|
||||||
|
literal: $app.stage === "production" ? "standard" : "test",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
type Product = "go" | "zen"
|
||||||
|
|
||||||
|
type Trigger = (opts: { model: string; product: Product }) => {
|
||||||
|
id: string
|
||||||
|
title: string
|
||||||
|
description: string
|
||||||
|
json: honeycomb.GetQuerySpecificationOutputArgs
|
||||||
|
threshold: { op: ">=" | "<="; value: number }
|
||||||
|
baseline: 3600 | 86400
|
||||||
|
}
|
||||||
|
|
||||||
|
type Model = { id: string; products: Product[]; triggers: Trigger[] }
|
||||||
|
|
||||||
|
const httpErrors: Trigger = ({ model, product }) => ({
|
||||||
|
id: "increased-http-errors",
|
||||||
|
title: `Increased HTTP Errors for ${displayName(model)} on ${displayName(product)}`,
|
||||||
|
description: `Detected increased rate of HTTP errors for ${displayName(model)} on OpenCode ${displayName(product)}`,
|
||||||
|
json: {
|
||||||
|
calculations: [
|
||||||
|
{
|
||||||
|
op: "COUNT",
|
||||||
|
name: "TOTAL",
|
||||||
|
filterCombination: "AND",
|
||||||
|
filters: [
|
||||||
|
{ column: "model", op: "=", value: model },
|
||||||
|
{ column: "isGoTier", op: "=", value: product === "go" ? "true" : "false" },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
op: "COUNT",
|
||||||
|
name: "FAILED",
|
||||||
|
filterCombination: "AND",
|
||||||
|
filters: [
|
||||||
|
{ column: "model", op: "=", value: model },
|
||||||
|
{ column: "isGoTier", op: "=", value: product === "go" ? "true" : "false" },
|
||||||
|
{ column: "status", op: ">=", value: "400" },
|
||||||
|
{ column: "status", op: "!=", value: "401" },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
formulas: [{ name: "ERROR", expression: "$FAILED / $TOTAL" }],
|
||||||
|
timeRange: 900,
|
||||||
|
},
|
||||||
|
// Alert when errors surge 50% compared to the previous period
|
||||||
|
threshold: { op: ">=", value: 50 },
|
||||||
|
// What previous time period to evaluate against
|
||||||
|
baseline: 3600,
|
||||||
|
})
|
||||||
|
|
||||||
|
const models: Model[] = [
|
||||||
|
{ id: "kimi-k2.6", products: ["go", "zen"], triggers: [httpErrors] },
|
||||||
|
{ id: "kimi-k2.5", products: ["go", "zen"], triggers: [httpErrors] },
|
||||||
|
{ id: "deepseek-v4-flash", products: ["go", "zen"], triggers: [httpErrors] },
|
||||||
|
{ id: "deepseek-v4-pro", products: ["go", "zen"], triggers: [httpErrors] },
|
||||||
|
{ id: "glm-5.1", products: ["go", "zen"], triggers: [httpErrors] },
|
||||||
|
// { id: "glm-5", products: ["go"], triggers: [httpErrors] },
|
||||||
|
{ id: "qwen3.6-plus", products: ["go", "zen"], triggers: [httpErrors] },
|
||||||
|
{ id: "qwen3.5-plus", products: ["go"], triggers: [httpErrors] },
|
||||||
|
{ id: "minimax-m2.7", products: ["go", "zen"], triggers: [httpErrors] },
|
||||||
|
// { id: "minimax-m2.5", products: ["go", "zen"], triggers: [httpErrors] },
|
||||||
|
{ id: "mimo-v2.5-pro", products: ["go"], triggers: [httpErrors] },
|
||||||
|
// { id: "mimo-v2.5", products: ["go"], triggers: [httpErrors] },
|
||||||
|
// { id: "mimo-v2-omni", products: ["go"], triggers: [httpErrors] },
|
||||||
|
// { id: "mimo-v2-pro", products: ["go"], triggers: [httpErrors] },
|
||||||
|
{ id: "claude-opus-4-7", products: ["zen"], triggers: [httpErrors] },
|
||||||
|
// { id: "claude-opus-4-6", products: ["zen"], triggers: [httpErrors] },
|
||||||
|
// { id: "claude-sonnet-4-6", products: ["zen"], triggers: [httpErrors] },
|
||||||
|
{ id: "gpt-5.5", products: ["zen"], triggers: [httpErrors] },
|
||||||
|
{ id: "big-pickle", products: ["zen"], triggers: [httpErrors] },
|
||||||
|
// { id: "minimax-m2.5-free", products: ["zen"], triggers: [httpErrors] },
|
||||||
|
// { id: "hy3-preview-free", products: ["zen"], triggers: [httpErrors] },
|
||||||
|
// { id: "nemotron-3-super-free", products: ["zen"], triggers: [httpErrors] },
|
||||||
|
// { id: "trinity-large-preview-free", products: ["zen"], triggers: [httpErrors] },
|
||||||
|
// { id: "ling-2.6-flash-free", products: ["zen"], triggers: [httpErrors] },
|
||||||
|
]
|
||||||
|
|
||||||
|
if ($app.stage !== "production") {
|
||||||
|
models.splice(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const model of models) {
|
||||||
|
for (const product of model.products) {
|
||||||
|
for (const trigger of model.triggers) {
|
||||||
|
const spec = trigger({ model: model.id, product })
|
||||||
|
|
||||||
|
new honeycomb.Trigger(resourceName(`${spec.id}-${product}-${model.id}`), {
|
||||||
|
name: spec.title,
|
||||||
|
description: spec.description,
|
||||||
|
queryJson: honeycomb.getQuerySpecificationOutput(spec.json).json,
|
||||||
|
alertType: "on_change",
|
||||||
|
// This is the minimum when using % change detection
|
||||||
|
frequency: 900,
|
||||||
|
baselineDetails: [{ type: "percentage", offsetMinutes: spec.baseline / 60 }],
|
||||||
|
thresholds: [{ ...spec.threshold, exceededLimit: 1 }],
|
||||||
|
recipients: [
|
||||||
|
{
|
||||||
|
id: webhookRecipient.id,
|
||||||
|
notificationDetails: [
|
||||||
|
{
|
||||||
|
variables: [
|
||||||
|
{ name: "model", value: model.id },
|
||||||
|
{ name: "product", value: product },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,8 +1,8 @@
|
||||||
{
|
{
|
||||||
"nodeModules": {
|
"nodeModules": {
|
||||||
"x86_64-linux": "sha256-AgHhYsiygxbsBo3JN4HqHXKAwh8n1qeuSCe2qqxlxW4=",
|
"x86_64-linux": "sha256-Oo27Xkoo5HOzLaRs7FmSobzb1SNyidKIqk1+/BWtcqg=",
|
||||||
"aarch64-linux": "sha256-h2lpWRQ5EDYnjpqZXtUAp1mxKLQxJ4m8MspgSY8Ev78=",
|
"aarch64-linux": "sha256-/d3ukZERWvV7egmc2Rtxg5vroZaXkCs7yVcIjIa4CUE=",
|
||||||
"aarch64-darwin": "sha256-xnd91+WyeAqn06run2ajsekxJvTMiLsnqNPe/rR8VTM=",
|
"aarch64-darwin": "sha256-1CX6n+9Wo2vAuPLekGsdjByReHQBbpKHwuK3L7Pfous=",
|
||||||
"x86_64-darwin": "sha256-rXpz45IOjGEk73xhP9VY86eOj2CZBg2l1vzwzTIOOOQ="
|
"x86_64-darwin": "sha256-Jqx3LDSoLSy8em7c/455xLEy9Pn4DmoYLHDemA1i+9w="
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -55,7 +55,6 @@ stdenvNoCC.mkDerivation {
|
||||||
--filter './packages/opencode' \
|
--filter './packages/opencode' \
|
||||||
--filter './packages/desktop' \
|
--filter './packages/desktop' \
|
||||||
--filter './packages/app' \
|
--filter './packages/app' \
|
||||||
--filter './packages/shared' \
|
|
||||||
--frozen-lockfile \
|
--frozen-lockfile \
|
||||||
--ignore-scripts \
|
--ignore-scripts \
|
||||||
--no-progress
|
--no-progress
|
||||||
|
|
|
||||||
|
|
@ -64,7 +64,7 @@ stdenvNoCC.mkDerivation (finalAttrs: {
|
||||||
[
|
[
|
||||||
ripgrep
|
ripgrep
|
||||||
]
|
]
|
||||||
# bun runs sysctl to detect if dunning on rosetta2
|
# bun runs sysctl to detect if running on rosetta2
|
||||||
++ lib.optional stdenvNoCC.hostPlatform.isDarwin sysctl
|
++ lib.optional stdenvNoCC.hostPlatform.isDarwin sysctl
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
17
package.json
17
package.json
|
|
@ -7,7 +7,7 @@
|
||||||
"packageManager": "bun@1.3.13",
|
"packageManager": "bun@1.3.13",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "bun run --cwd packages/opencode --conditions=browser src/index.ts",
|
"dev": "bun run --cwd packages/opencode --conditions=browser src/index.ts",
|
||||||
"dev:desktop": "bun --cwd packages/desktop-electron dev",
|
"dev:desktop": "bun --cwd packages/desktop dev",
|
||||||
"dev:web": "bun --cwd packages/app dev",
|
"dev:web": "bun --cwd packages/app dev",
|
||||||
"dev:console": "ulimit -n 10240 2>/dev/null; bun run --cwd packages/console/app dev",
|
"dev:console": "ulimit -n 10240 2>/dev/null; bun run --cwd packages/console/app dev",
|
||||||
"dev:storybook": "bun --cwd packages/storybook storybook",
|
"dev:storybook": "bun --cwd packages/storybook storybook",
|
||||||
|
|
@ -27,32 +27,33 @@
|
||||||
"packages/slack"
|
"packages/slack"
|
||||||
],
|
],
|
||||||
"catalog": {
|
"catalog": {
|
||||||
"@effect/opentelemetry": "4.0.0-beta.48",
|
"@effect/opentelemetry": "4.0.0-beta.57",
|
||||||
"@effect/platform-node": "4.0.0-beta.48",
|
"@effect/platform-node": "4.0.0-beta.57",
|
||||||
"@npmcli/arborist": "9.4.0",
|
"@npmcli/arborist": "9.4.0",
|
||||||
"@types/bun": "1.3.12",
|
"@types/bun": "1.3.12",
|
||||||
"@types/cross-spawn": "6.0.6",
|
"@types/cross-spawn": "6.0.6",
|
||||||
"@octokit/rest": "22.0.0",
|
"@octokit/rest": "22.0.0",
|
||||||
"@hono/zod-validator": "0.4.2",
|
"@hono/zod-validator": "0.4.2",
|
||||||
"@opentui/core": "0.1.99",
|
"@opentui/core": "0.2.2",
|
||||||
"@opentui/solid": "0.1.99",
|
"@opentui/solid": "0.2.2",
|
||||||
"ulid": "3.0.1",
|
"ulid": "3.0.1",
|
||||||
"@kobalte/core": "0.13.11",
|
"@kobalte/core": "0.13.11",
|
||||||
"@types/luxon": "3.7.1",
|
"@types/luxon": "3.7.1",
|
||||||
"@types/node": "22.13.9",
|
"@types/node": "24.12.2",
|
||||||
"@types/semver": "7.7.1",
|
"@types/semver": "7.7.1",
|
||||||
"@tsconfig/node22": "22.0.2",
|
"@tsconfig/node22": "22.0.2",
|
||||||
"@tsconfig/bun": "1.0.9",
|
"@tsconfig/bun": "1.0.9",
|
||||||
"@cloudflare/workers-types": "4.20251008.0",
|
"@cloudflare/workers-types": "4.20251008.0",
|
||||||
"@openauthjs/openauth": "0.0.0-20250322224806",
|
"@openauthjs/openauth": "0.0.0-20250322224806",
|
||||||
"@pierre/diffs": "1.1.0-beta.18",
|
"@pierre/diffs": "1.1.0-beta.18",
|
||||||
|
"opentui-spinner": "0.0.6",
|
||||||
"@solid-primitives/storage": "4.3.3",
|
"@solid-primitives/storage": "4.3.3",
|
||||||
"@tailwindcss/vite": "4.1.11",
|
"@tailwindcss/vite": "4.1.11",
|
||||||
"diff": "8.0.2",
|
"diff": "8.0.2",
|
||||||
"dompurify": "3.3.1",
|
"dompurify": "3.3.1",
|
||||||
"drizzle-kit": "1.0.0-beta.19-d95b7a4",
|
"drizzle-kit": "1.0.0-beta.19-d95b7a4",
|
||||||
"drizzle-orm": "1.0.0-beta.19-d95b7a4",
|
"drizzle-orm": "1.0.0-beta.19-d95b7a4",
|
||||||
"effect": "4.0.0-beta.48",
|
"effect": "4.0.0-beta.59",
|
||||||
"ai": "6.0.168",
|
"ai": "6.0.168",
|
||||||
"cross-spawn": "7.0.6",
|
"cross-spawn": "7.0.6",
|
||||||
"hono": "4.10.7",
|
"hono": "4.10.7",
|
||||||
|
|
@ -76,6 +77,8 @@
|
||||||
"@solidjs/meta": "0.29.4",
|
"@solidjs/meta": "0.29.4",
|
||||||
"@solidjs/router": "0.15.4",
|
"@solidjs/router": "0.15.4",
|
||||||
"@solidjs/start": "https://pkg.pr.new/@solidjs/start@dfb2020",
|
"@solidjs/start": "https://pkg.pr.new/@solidjs/start@dfb2020",
|
||||||
|
"@sentry/solid": "10.36.0",
|
||||||
|
"@sentry/vite-plugin": "4.6.0",
|
||||||
"solid-js": "1.9.10",
|
"solid-js": "1.9.10",
|
||||||
"vite-plugin-solid": "2.11.10",
|
"vite-plugin-solid": "2.11.10",
|
||||||
"@lydell/node-pty": "1.2.0-beta.10"
|
"@lydell/node-pty": "1.2.0-beta.10"
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "@opencode-ai/app",
|
"name": "@opencode-ai/app",
|
||||||
"version": "1.14.21",
|
"version": "1.14.39",
|
||||||
"description": "",
|
"description": "",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"exports": {
|
"exports": {
|
||||||
|
|
@ -27,6 +27,7 @@
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@happy-dom/global-registrator": "20.0.11",
|
"@happy-dom/global-registrator": "20.0.11",
|
||||||
"@playwright/test": "catalog:",
|
"@playwright/test": "catalog:",
|
||||||
|
"@sentry/vite-plugin": "catalog:",
|
||||||
"@tailwindcss/vite": "catalog:",
|
"@tailwindcss/vite": "catalog:",
|
||||||
"@tsconfig/bun": "1.0.9",
|
"@tsconfig/bun": "1.0.9",
|
||||||
"@types/bun": "catalog:",
|
"@types/bun": "catalog:",
|
||||||
|
|
@ -40,9 +41,10 @@
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@kobalte/core": "catalog:",
|
"@kobalte/core": "catalog:",
|
||||||
|
"@sentry/solid": "catalog:",
|
||||||
"@opencode-ai/sdk": "workspace:*",
|
"@opencode-ai/sdk": "workspace:*",
|
||||||
"@opencode-ai/ui": "workspace:*",
|
"@opencode-ai/ui": "workspace:*",
|
||||||
"@opencode-ai/shared": "workspace:*",
|
"@opencode-ai/core": "workspace:*",
|
||||||
"@shikijs/transformers": "3.9.2",
|
"@shikijs/transformers": "3.9.2",
|
||||||
"@solid-primitives/active-element": "2.1.3",
|
"@solid-primitives/active-element": "2.1.3",
|
||||||
"@solid-primitives/audio": "1.4.2",
|
"@solid-primitives/audio": "1.4.2",
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
import "@/index.css"
|
import "@/index.css"
|
||||||
|
import * as Sentry from "@sentry/solid"
|
||||||
import { I18nProvider } from "@opencode-ai/ui/context"
|
import { I18nProvider } from "@opencode-ai/ui/context"
|
||||||
import { DialogProvider } from "@opencode-ai/ui/context/dialog"
|
import { DialogProvider } from "@opencode-ai/ui/context/dialog"
|
||||||
import { FileComponentProvider } from "@opencode-ai/ui/context/file"
|
import { FileComponentProvider } from "@opencode-ai/ui/context/file"
|
||||||
|
|
@ -82,7 +83,15 @@ declare global {
|
||||||
}
|
}
|
||||||
|
|
||||||
function QueryProvider(props: ParentProps) {
|
function QueryProvider(props: ParentProps) {
|
||||||
const client = new QueryClient()
|
const client = new QueryClient({
|
||||||
|
defaultOptions: {
|
||||||
|
queries: {
|
||||||
|
refetchOnReconnect: false,
|
||||||
|
refetchOnMount: false,
|
||||||
|
refetchOnWindowFocus: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
return <QueryClientProvider client={client}>{props.children}</QueryClientProvider>
|
return <QueryClientProvider client={client}>{props.children}</QueryClientProvider>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -140,12 +149,19 @@ export function AppBaseProviders(props: ParentProps<{ locale?: Locale }>) {
|
||||||
>
|
>
|
||||||
<LanguageProvider locale={props.locale}>
|
<LanguageProvider locale={props.locale}>
|
||||||
<UiI18nBridge>
|
<UiI18nBridge>
|
||||||
<ErrorBoundary fallback={(error) => <ErrorPage error={error} />}>
|
<ErrorBoundary
|
||||||
<DialogProvider>
|
fallback={(error) => {
|
||||||
<MarkedProvider>
|
Sentry.captureException(error)
|
||||||
<FileComponentProvider component={File}>{props.children}</FileComponentProvider>
|
return <ErrorPage error={error} />
|
||||||
</MarkedProvider>
|
}}
|
||||||
</DialogProvider>
|
>
|
||||||
|
<QueryProvider>
|
||||||
|
<DialogProvider>
|
||||||
|
<MarkedProvider>
|
||||||
|
<FileComponentProvider component={File}>{props.children}</FileComponentProvider>
|
||||||
|
</MarkedProvider>
|
||||||
|
</DialogProvider>
|
||||||
|
</QueryProvider>
|
||||||
</ErrorBoundary>
|
</ErrorBoundary>
|
||||||
</UiI18nBridge>
|
</UiI18nBridge>
|
||||||
</LanguageProvider>
|
</LanguageProvider>
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,7 @@ import { createStore } from "solid-js/store"
|
||||||
import { useGlobalSDK } from "@/context/global-sdk"
|
import { useGlobalSDK } from "@/context/global-sdk"
|
||||||
import { useGlobalSync } from "@/context/global-sync"
|
import { useGlobalSync } from "@/context/global-sync"
|
||||||
import { type LocalProject, getAvatarColors } from "@/context/layout"
|
import { type LocalProject, getAvatarColors } from "@/context/layout"
|
||||||
import { getFilename } from "@opencode-ai/shared/util/path"
|
import { getFilename } from "@opencode-ai/core/util/path"
|
||||||
import { Avatar } from "@opencode-ai/ui/avatar"
|
import { Avatar } from "@opencode-ai/ui/avatar"
|
||||||
import { useLanguage } from "@/context/language"
|
import { useLanguage } from "@/context/language"
|
||||||
import { getProjectAvatarSource } from "@/pages/layout/sidebar-items"
|
import { getProjectAvatarSource } from "@/pages/layout/sidebar-items"
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,7 @@ import { List } from "@opencode-ai/ui/list"
|
||||||
import { showToast } from "@opencode-ai/ui/toast"
|
import { showToast } from "@opencode-ai/ui/toast"
|
||||||
import { extractPromptFromParts } from "@/utils/prompt"
|
import { extractPromptFromParts } from "@/utils/prompt"
|
||||||
import type { TextPart as SDKTextPart } from "@opencode-ai/sdk/v2/client"
|
import type { TextPart as SDKTextPart } from "@opencode-ai/sdk/v2/client"
|
||||||
import { base64Encode } from "@opencode-ai/shared/util/encode"
|
import { base64Encode } from "@opencode-ai/core/util/encode"
|
||||||
import { useLanguage } from "@/context/language"
|
import { useLanguage } from "@/context/language"
|
||||||
|
|
||||||
interface ForkableMessage {
|
interface ForkableMessage {
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@ import { Dialog } from "@opencode-ai/ui/dialog"
|
||||||
import { FileIcon } from "@opencode-ai/ui/file-icon"
|
import { FileIcon } from "@opencode-ai/ui/file-icon"
|
||||||
import { List } from "@opencode-ai/ui/list"
|
import { List } from "@opencode-ai/ui/list"
|
||||||
import type { ListRef } from "@opencode-ai/ui/list"
|
import type { ListRef } from "@opencode-ai/ui/list"
|
||||||
import { getDirectory, getFilename } from "@opencode-ai/shared/util/path"
|
import { getDirectory, getFilename } from "@opencode-ai/core/util/path"
|
||||||
import fuzzysort from "fuzzysort"
|
import fuzzysort from "fuzzysort"
|
||||||
import { createMemo, createResource, createSignal } from "solid-js"
|
import { createMemo, createResource, createSignal } from "solid-js"
|
||||||
import { useGlobalSDK } from "@/context/global-sdk"
|
import { useGlobalSDK } from "@/context/global-sdk"
|
||||||
|
|
|
||||||
|
|
@ -4,8 +4,8 @@ import { FileIcon } from "@opencode-ai/ui/file-icon"
|
||||||
import { Icon } from "@opencode-ai/ui/icon"
|
import { Icon } from "@opencode-ai/ui/icon"
|
||||||
import { Keybind } from "@opencode-ai/ui/keybind"
|
import { Keybind } from "@opencode-ai/ui/keybind"
|
||||||
import { List } from "@opencode-ai/ui/list"
|
import { List } from "@opencode-ai/ui/list"
|
||||||
import { base64Encode } from "@opencode-ai/shared/util/encode"
|
import { base64Encode } from "@opencode-ai/core/util/encode"
|
||||||
import { getDirectory, getFilename } from "@opencode-ai/shared/util/path"
|
import { getDirectory, getFilename } from "@opencode-ai/core/util/path"
|
||||||
import { useNavigate } from "@solidjs/router"
|
import { useNavigate } from "@solidjs/router"
|
||||||
import { createMemo, createSignal, Match, onCleanup, Show, Switch } from "solid-js"
|
import { createMemo, createSignal, Match, onCleanup, Show, Switch } from "solid-js"
|
||||||
import { formatKeybind, useCommand, type CommandOption } from "@/context/command"
|
import { formatKeybind, useCommand, type CommandOption } from "@/context/command"
|
||||||
|
|
|
||||||
|
|
@ -1,13 +1,12 @@
|
||||||
import { useMutation } from "@tanstack/solid-query"
|
import { useMutation, useQueryClient } from "@tanstack/solid-query"
|
||||||
import { Component, createEffect, createMemo, on, Show } from "solid-js"
|
import { Component, createMemo, Show } from "solid-js"
|
||||||
import { createStore } from "solid-js/store"
|
|
||||||
import { useSync } from "@/context/sync"
|
import { useSync } from "@/context/sync"
|
||||||
import { useSDK } from "@/context/sdk"
|
import { useSDK } from "@/context/sdk"
|
||||||
import { Dialog } from "@opencode-ai/ui/dialog"
|
import { Dialog } from "@opencode-ai/ui/dialog"
|
||||||
import { List } from "@opencode-ai/ui/list"
|
import { List } from "@opencode-ai/ui/list"
|
||||||
import { Switch } from "@opencode-ai/ui/switch"
|
import { Switch } from "@opencode-ai/ui/switch"
|
||||||
import { showToast } from "@opencode-ai/ui/toast"
|
|
||||||
import { useLanguage } from "@/context/language"
|
import { useLanguage } from "@/context/language"
|
||||||
|
import { mcpQueryKey } from "@/context/global-sync"
|
||||||
|
|
||||||
const statusLabels = {
|
const statusLabels = {
|
||||||
connected: "mcp.status.connected",
|
connected: "mcp.status.connected",
|
||||||
|
|
@ -20,48 +19,7 @@ export const DialogSelectMcp: Component = () => {
|
||||||
const sync = useSync()
|
const sync = useSync()
|
||||||
const sdk = useSDK()
|
const sdk = useSDK()
|
||||||
const language = useLanguage()
|
const language = useLanguage()
|
||||||
const [state, setState] = createStore({
|
const queryClient = useQueryClient()
|
||||||
done: false,
|
|
||||||
loading: false,
|
|
||||||
})
|
|
||||||
|
|
||||||
createEffect(
|
|
||||||
on(
|
|
||||||
() => sync.data.mcp_ready,
|
|
||||||
(ready, prev) => {
|
|
||||||
if (!ready && prev) setState("done", false)
|
|
||||||
},
|
|
||||||
{ defer: true },
|
|
||||||
),
|
|
||||||
)
|
|
||||||
|
|
||||||
createEffect(() => {
|
|
||||||
if (state.done || state.loading) return
|
|
||||||
if (sync.data.mcp_ready) {
|
|
||||||
setState("done", true)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
setState("loading", true)
|
|
||||||
void sdk.client.mcp
|
|
||||||
.status()
|
|
||||||
.then((result) => {
|
|
||||||
sync.set("mcp", result.data ?? {})
|
|
||||||
sync.set("mcp_ready", true)
|
|
||||||
setState("done", true)
|
|
||||||
})
|
|
||||||
.catch((err) => {
|
|
||||||
setState("done", true)
|
|
||||||
showToast({
|
|
||||||
variant: "error",
|
|
||||||
title: language.t("common.requestFailed"),
|
|
||||||
description: err instanceof Error ? err.message : String(err),
|
|
||||||
})
|
|
||||||
})
|
|
||||||
.finally(() => {
|
|
||||||
setState("loading", false)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
const items = createMemo(() =>
|
const items = createMemo(() =>
|
||||||
Object.entries(sync.data.mcp ?? {})
|
Object.entries(sync.data.mcp ?? {})
|
||||||
|
|
@ -71,16 +29,10 @@ export const DialogSelectMcp: Component = () => {
|
||||||
|
|
||||||
const toggle = useMutation(() => ({
|
const toggle = useMutation(() => ({
|
||||||
mutationFn: async (name: string) => {
|
mutationFn: async (name: string) => {
|
||||||
const status = sync.data.mcp[name]
|
if (sync.data.mcp[name]?.status === "connected") await sdk.client.mcp.disconnect({ name })
|
||||||
if (status?.status === "connected") {
|
else await sdk.client.mcp.connect({ name })
|
||||||
await sdk.client.mcp.disconnect({ name })
|
|
||||||
} else {
|
|
||||||
await sdk.client.mcp.connect({ name })
|
|
||||||
}
|
|
||||||
|
|
||||||
const result = await sdk.client.mcp.status()
|
|
||||||
if (result.data) sync.set("mcp", result.data)
|
|
||||||
},
|
},
|
||||||
|
onSuccess: () => queryClient.refetchQueries({ queryKey: mcpQueryKey(sync.directory) }),
|
||||||
}))
|
}))
|
||||||
|
|
||||||
const enabledCount = createMemo(() => items().filter((i) => i.status === "connected").length)
|
const enabledCount = createMemo(() => items().filter((i) => i.status === "connected").length)
|
||||||
|
|
|
||||||
|
|
@ -16,6 +16,7 @@ import {
|
||||||
} from "@/context/prompt"
|
} from "@/context/prompt"
|
||||||
import { useLayout } from "@/context/layout"
|
import { useLayout } from "@/context/layout"
|
||||||
import { useSDK } from "@/context/sdk"
|
import { useSDK } from "@/context/sdk"
|
||||||
|
import { useGlobalSDK } from "@/context/global-sdk"
|
||||||
import { useSync } from "@/context/sync"
|
import { useSync } from "@/context/sync"
|
||||||
import { useComments } from "@/context/comments"
|
import { useComments } from "@/context/comments"
|
||||||
import { Button } from "@opencode-ai/ui/button"
|
import { Button } from "@opencode-ai/ui/button"
|
||||||
|
|
@ -102,6 +103,7 @@ const NON_EMPTY_TEXT = /[^\s\u200B]/
|
||||||
|
|
||||||
export const PromptInput: Component<PromptInputProps> = (props) => {
|
export const PromptInput: Component<PromptInputProps> = (props) => {
|
||||||
const sdk = useSDK()
|
const sdk = useSDK()
|
||||||
|
const globalSDK = useGlobalSDK()
|
||||||
|
|
||||||
const sync = useSync()
|
const sync = useSync()
|
||||||
const local = useLocal()
|
const local = useLocal()
|
||||||
|
|
@ -270,7 +272,7 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
|
||||||
const buttonsSpring = useSpring(() => (store.mode === "normal" ? 1 : 0), { visualDuration: 0.2, bounce: 0 })
|
const buttonsSpring = useSpring(() => (store.mode === "normal" ? 1 : 0), { visualDuration: 0.2, bounce: 0 })
|
||||||
const motion = (value: number) => ({
|
const motion = (value: number) => ({
|
||||||
opacity: value,
|
opacity: value,
|
||||||
transform: `scale(${0.95 + value * 0.05})`,
|
transform: `scale(${0.98 + value * 0.02})`,
|
||||||
filter: `blur(${(1 - value) * 2}px)`,
|
filter: `blur(${(1 - value) * 2}px)`,
|
||||||
"pointer-events": value > 0.5 ? ("auto" as const) : ("none" as const),
|
"pointer-events": value > 0.5 ? ("auto" as const) : ("none" as const),
|
||||||
})
|
})
|
||||||
|
|
@ -345,7 +347,7 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
|
||||||
promptPlaceholder({
|
promptPlaceholder({
|
||||||
mode: store.mode,
|
mode: store.mode,
|
||||||
commentCount: commentCount(),
|
commentCount: commentCount(),
|
||||||
example: suggest() ? language.t(EXAMPLES[store.placeholder]) : "",
|
example: suggest() ? (store.mode === "shell" ? "git status" : language.t(EXAMPLES[store.placeholder])) : "",
|
||||||
suggest: suggest(),
|
suggest: suggest(),
|
||||||
t: (key, params) => language.t(key as Parameters<typeof language.t>[0], params as never),
|
t: (key, params) => language.t(key as Parameters<typeof language.t>[0], params as never),
|
||||||
}),
|
}),
|
||||||
|
|
@ -1253,7 +1255,11 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
|
||||||
}
|
}
|
||||||
|
|
||||||
const [agentsQuery, globalProvidersQuery, providersQuery] = useQueries(() => ({
|
const [agentsQuery, globalProvidersQuery, providersQuery] = useQueries(() => ({
|
||||||
queries: [loadAgentsQuery(sdk.directory), loadProvidersQuery(null), loadProvidersQuery(sdk.directory)],
|
queries: [
|
||||||
|
loadAgentsQuery(sdk.directory, sdk.client),
|
||||||
|
loadProvidersQuery(null, globalSDK.client),
|
||||||
|
loadProvidersQuery(sdk.directory, sdk.client),
|
||||||
|
],
|
||||||
}))
|
}))
|
||||||
|
|
||||||
const agentsLoading = () => agentsQuery.isLoading
|
const agentsLoading = () => agentsQuery.isLoading
|
||||||
|
|
@ -1403,12 +1409,11 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
|
||||||
<IconButton
|
<IconButton
|
||||||
data-action="prompt-submit"
|
data-action="prompt-submit"
|
||||||
type="submit"
|
type="submit"
|
||||||
disabled={store.mode !== "normal" || (!working() && blank())}
|
disabled={!working() && blank()}
|
||||||
tabIndex={store.mode === "normal" ? undefined : -1}
|
tabIndex={store.mode === "normal" ? undefined : -1}
|
||||||
icon={stopping() ? "stop" : "arrow-up"}
|
icon={stopping() ? "stop" : store.mode === "shell" ? "arrow-undo-down" : "arrow-up"}
|
||||||
variant="primary"
|
variant="primary"
|
||||||
class="size-8"
|
class="size-8"
|
||||||
style={buttons()}
|
|
||||||
aria-label={stopping() ? language.t("prompt.action.stop") : language.t("prompt.action.send")}
|
aria-label={stopping() ? language.t("prompt.action.stop") : language.t("prompt.action.send")}
|
||||||
/>
|
/>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
|
|
@ -1451,14 +1456,24 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
|
||||||
<div class="px-1.75 pt-5.5 pb-2 flex items-center gap-2 min-w-0">
|
<div class="px-1.75 pt-5.5 pb-2 flex items-center gap-2 min-w-0">
|
||||||
<div class="flex items-center gap-1.5 min-w-0 flex-1 relative">
|
<div class="flex items-center gap-1.5 min-w-0 flex-1 relative">
|
||||||
<div
|
<div
|
||||||
class="h-7 flex items-center gap-1.5 max-w-[160px] min-w-0 absolute inset-y-0 left-0"
|
class="h-7 flex items-center gap-1.5 min-w-0 absolute inset-0"
|
||||||
style={{
|
style={{
|
||||||
padding: "0 4px 0 8px",
|
padding: "0 0px 0 8px",
|
||||||
...shell(),
|
...shell(),
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<span class="truncate text-13-medium text-text-strong">{language.t("prompt.mode.shell")}</span>
|
<Icon name="console" />
|
||||||
<div class="size-4 shrink-0" />
|
<span class="truncate text-13-medium text-text-base">{language.t("prompt.mode.shell")}</span>
|
||||||
|
<div class="flex-1" />
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
class="text-text-base"
|
||||||
|
onClick={() => {
|
||||||
|
setStore("mode", "normal")
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{language.t("common.cancel")}
|
||||||
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex items-center gap-1.5 min-w-0 flex-1 h-7">
|
<div class="flex items-center gap-1.5 min-w-0 flex-1 h-7">
|
||||||
<Show when={!agentsLoading()}>
|
<Show when={!agentsLoading()}>
|
||||||
|
|
@ -1565,33 +1580,35 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
|
||||||
</TooltipKeybind>
|
</TooltipKeybind>
|
||||||
</Show>
|
</Show>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<Show when={variants().length > 2}>
|
||||||
data-component="prompt-variant-control"
|
<div
|
||||||
style={providersShouldFadeIn() ? { animation: "fade-in 0.3s" } : undefined}
|
data-component="prompt-variant-control"
|
||||||
>
|
style={providersShouldFadeIn() ? { animation: "fade-in 0.3s" } : undefined}
|
||||||
<TooltipKeybind
|
|
||||||
placement="top"
|
|
||||||
gutter={4}
|
|
||||||
title={language.t("command.model.variant.cycle")}
|
|
||||||
keybind={command.keybind("model.variant.cycle")}
|
|
||||||
>
|
>
|
||||||
<Select
|
<TooltipKeybind
|
||||||
size="normal"
|
placement="top"
|
||||||
options={variants()}
|
gutter={4}
|
||||||
current={local.model.variant.current() ?? "default"}
|
title={language.t("command.model.variant.cycle")}
|
||||||
label={(x) => (x === "default" ? language.t("common.default") : x)}
|
keybind={command.keybind("model.variant.cycle")}
|
||||||
onSelect={(value) => {
|
>
|
||||||
local.model.variant.set(value === "default" ? undefined : value)
|
<Select
|
||||||
restoreFocus()
|
size="normal"
|
||||||
}}
|
options={variants()}
|
||||||
class="capitalize max-w-[160px] text-text-base"
|
current={local.model.variant.current() ?? "default"}
|
||||||
valueClass="truncate text-13-regular text-text-base"
|
label={(x) => (x === "default" ? language.t("common.default") : x)}
|
||||||
triggerStyle={control()}
|
onSelect={(value) => {
|
||||||
triggerProps={{ "data-action": "prompt-model-variant" }}
|
local.model.variant.set(value === "default" ? undefined : value)
|
||||||
variant="ghost"
|
restoreFocus()
|
||||||
/>
|
}}
|
||||||
</TooltipKeybind>
|
class="capitalize max-w-[160px] text-text-base"
|
||||||
</div>
|
valueClass="truncate text-13-regular text-text-base"
|
||||||
|
triggerStyle={control()}
|
||||||
|
triggerProps={{ "data-action": "prompt-model-variant" }}
|
||||||
|
variant="ghost"
|
||||||
|
/>
|
||||||
|
</TooltipKeybind>
|
||||||
|
</div>
|
||||||
|
</Show>
|
||||||
</Show>
|
</Show>
|
||||||
</Show>
|
</Show>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import { getFilename } from "@opencode-ai/shared/util/path"
|
import { getFilename } from "@opencode-ai/core/util/path"
|
||||||
import { type AgentPartInput, type FilePartInput, type Part, type TextPartInput } from "@opencode-ai/sdk/v2/client"
|
import { type AgentPartInput, type FilePartInput, type Part, type TextPartInput } from "@opencode-ai/sdk/v2/client"
|
||||||
import type { FileSelection } from "@/context/file"
|
import type { FileSelection } from "@/context/file"
|
||||||
import { encodeFilePath } from "@/context/file/path"
|
import { encodeFilePath } from "@/context/file/path"
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@ import { Component, For, Show } from "solid-js"
|
||||||
import { FileIcon } from "@opencode-ai/ui/file-icon"
|
import { FileIcon } from "@opencode-ai/ui/file-icon"
|
||||||
import { IconButton } from "@opencode-ai/ui/icon-button"
|
import { IconButton } from "@opencode-ai/ui/icon-button"
|
||||||
import { Tooltip } from "@opencode-ai/ui/tooltip"
|
import { Tooltip } from "@opencode-ai/ui/tooltip"
|
||||||
import { getDirectory, getFilename, getFilenameTruncated } from "@opencode-ai/shared/util/path"
|
import { getDirectory, getFilename, getFilenameTruncated } from "@opencode-ai/core/util/path"
|
||||||
import type { ContextItem } from "@/context/prompt"
|
import type { ContextItem } from "@/context/prompt"
|
||||||
|
|
||||||
type PromptContextItem = ContextItem & { key: string }
|
type PromptContextItem = ContextItem & { key: string }
|
||||||
|
|
|
||||||
|
|
@ -12,7 +12,7 @@ describe("promptPlaceholder", () => {
|
||||||
suggest: true,
|
suggest: true,
|
||||||
t,
|
t,
|
||||||
})
|
})
|
||||||
expect(value).toBe("prompt.placeholder.shell")
|
expect(value).toBe("prompt.placeholder.shell:example")
|
||||||
})
|
})
|
||||||
|
|
||||||
test("returns summarize placeholders for comment context", () => {
|
test("returns summarize placeholders for comment context", () => {
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,7 @@ type PromptPlaceholderInput = {
|
||||||
}
|
}
|
||||||
|
|
||||||
export function promptPlaceholder(input: PromptPlaceholderInput) {
|
export function promptPlaceholder(input: PromptPlaceholderInput) {
|
||||||
if (input.mode === "shell") return input.t("prompt.placeholder.shell")
|
if (input.mode === "shell") return input.t("prompt.placeholder.shell", { example: input.example })
|
||||||
if (input.commentCount > 1) return input.t("prompt.placeholder.summarizeComments")
|
if (input.commentCount > 1) return input.t("prompt.placeholder.summarizeComments")
|
||||||
if (input.commentCount === 1) return input.t("prompt.placeholder.summarizeComment")
|
if (input.commentCount === 1) return input.t("prompt.placeholder.summarizeComment")
|
||||||
if (!input.suggest) return input.t("prompt.placeholder.simple")
|
if (!input.suggest) return input.t("prompt.placeholder.simple")
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
import { Component, For, Match, Show, Switch } from "solid-js"
|
import { Component, For, Match, Show, Switch } from "solid-js"
|
||||||
import { FileIcon } from "@opencode-ai/ui/file-icon"
|
import { FileIcon } from "@opencode-ai/ui/file-icon"
|
||||||
import { Icon } from "@opencode-ai/ui/icon"
|
import { Icon } from "@opencode-ai/ui/icon"
|
||||||
import { getDirectory, getFilename } from "@opencode-ai/shared/util/path"
|
import { getDirectory, getFilename } from "@opencode-ai/core/util/path"
|
||||||
|
|
||||||
export type AtOption =
|
export type AtOption =
|
||||||
| { type: "agent"; name: string; display: string }
|
| { type: "agent"; name: string; display: string }
|
||||||
|
|
|
||||||
|
|
@ -74,7 +74,7 @@ beforeAll(async () => {
|
||||||
showToast: () => 0,
|
showToast: () => 0,
|
||||||
}))
|
}))
|
||||||
|
|
||||||
mock.module("@opencode-ai/shared/util/encode", () => ({
|
mock.module("@opencode-ai/core/util/encode", () => ({
|
||||||
base64Encode: (value: string) => value,
|
base64Encode: (value: string) => value,
|
||||||
}))
|
}))
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
import type { Message, Session } from "@opencode-ai/sdk/v2/client"
|
import type { Message, Session } from "@opencode-ai/sdk/v2/client"
|
||||||
import { showToast } from "@opencode-ai/ui/toast"
|
import { showToast } from "@opencode-ai/ui/toast"
|
||||||
import { base64Encode } from "@opencode-ai/shared/util/encode"
|
import { base64Encode } from "@opencode-ai/core/util/encode"
|
||||||
import { Binary } from "@opencode-ai/shared/util/binary"
|
import { Binary } from "@opencode-ai/core/util/binary"
|
||||||
import { useNavigate, useParams } from "@solidjs/router"
|
import { useNavigate, useParams } from "@solidjs/router"
|
||||||
import { batch, type Accessor } from "solid-js"
|
import { batch, type Accessor } from "solid-js"
|
||||||
import type { FileSelection } from "@/context/file"
|
import type { FileSelection } from "@/context/file"
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,8 @@
|
||||||
import { createMemo, createEffect, on, onCleanup, For, Show } from "solid-js"
|
import { createMemo, createEffect, on, onCleanup, For, Show } from "solid-js"
|
||||||
import type { JSX } from "solid-js"
|
import type { JSX } from "solid-js"
|
||||||
import { useSync } from "@/context/sync"
|
import { useSync } from "@/context/sync"
|
||||||
import { checksum } from "@opencode-ai/shared/util/encode"
|
import { checksum } from "@opencode-ai/core/util/encode"
|
||||||
import { findLast } from "@opencode-ai/shared/util/array"
|
import { findLast } from "@opencode-ai/core/util/array"
|
||||||
import { same } from "@/utils/same"
|
import { same } from "@/utils/same"
|
||||||
import { Icon } from "@opencode-ai/ui/icon"
|
import { Icon } from "@opencode-ai/ui/icon"
|
||||||
import { Accordion } from "@opencode-ai/ui/accordion"
|
import { Accordion } from "@opencode-ai/ui/accordion"
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,7 @@ import { Keybind } from "@opencode-ai/ui/keybind"
|
||||||
import { Spinner } from "@opencode-ai/ui/spinner"
|
import { Spinner } from "@opencode-ai/ui/spinner"
|
||||||
import { showToast } from "@opencode-ai/ui/toast"
|
import { showToast } from "@opencode-ai/ui/toast"
|
||||||
import { Tooltip, TooltipKeybind } from "@opencode-ai/ui/tooltip"
|
import { Tooltip, TooltipKeybind } from "@opencode-ai/ui/tooltip"
|
||||||
import { getFilename } from "@opencode-ai/shared/util/path"
|
import { getFilename } from "@opencode-ai/core/util/path"
|
||||||
import { createEffect, createMemo, createSignal, For, onMount, Show } from "solid-js"
|
import { createEffect, createMemo, createSignal, For, onMount, Show } from "solid-js"
|
||||||
import { createStore } from "solid-js/store"
|
import { createStore } from "solid-js/store"
|
||||||
import { Portal } from "solid-js/web"
|
import { Portal } from "solid-js/web"
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,7 @@ import { useSDK } from "@/context/sdk"
|
||||||
import { useLanguage } from "@/context/language"
|
import { useLanguage } from "@/context/language"
|
||||||
import { Icon } from "@opencode-ai/ui/icon"
|
import { Icon } from "@opencode-ai/ui/icon"
|
||||||
import { Mark } from "@opencode-ai/ui/logo"
|
import { Mark } from "@opencode-ai/ui/logo"
|
||||||
import { getDirectory, getFilename } from "@opencode-ai/shared/util/path"
|
import { getDirectory, getFilename } from "@opencode-ai/core/util/path"
|
||||||
|
|
||||||
const MAIN_WORKTREE = "main"
|
const MAIN_WORKTREE = "main"
|
||||||
const CREATE_WORKTREE = "create"
|
const CREATE_WORKTREE = "create"
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,7 @@ import { FileIcon } from "@opencode-ai/ui/file-icon"
|
||||||
import { IconButton } from "@opencode-ai/ui/icon-button"
|
import { IconButton } from "@opencode-ai/ui/icon-button"
|
||||||
import { TooltipKeybind } from "@opencode-ai/ui/tooltip"
|
import { TooltipKeybind } from "@opencode-ai/ui/tooltip"
|
||||||
import { Tabs } from "@opencode-ai/ui/tabs"
|
import { Tabs } from "@opencode-ai/ui/tabs"
|
||||||
import { getFilename } from "@opencode-ai/shared/util/path"
|
import { getFilename } from "@opencode-ai/core/util/path"
|
||||||
import { useFile } from "@/context/file"
|
import { useFile } from "@/context/file"
|
||||||
import { useLanguage } from "@/context/language"
|
import { useLanguage } from "@/context/language"
|
||||||
import { useCommand } from "@/context/command"
|
import { useCommand } from "@/context/command"
|
||||||
|
|
|
||||||
|
|
@ -11,7 +11,9 @@ import { showToast } from "@opencode-ai/ui/toast"
|
||||||
import { useParams } from "@solidjs/router"
|
import { useParams } from "@solidjs/router"
|
||||||
import { useLanguage } from "@/context/language"
|
import { useLanguage } from "@/context/language"
|
||||||
import { usePermission } from "@/context/permission"
|
import { usePermission } from "@/context/permission"
|
||||||
import { usePlatform } from "@/context/platform"
|
import { usePlatform, type DisplayBackend } from "@/context/platform"
|
||||||
|
import { useGlobalSync } from "@/context/global-sync"
|
||||||
|
import { useGlobalSDK } from "@/context/global-sdk"
|
||||||
import {
|
import {
|
||||||
monoDefault,
|
monoDefault,
|
||||||
monoFontFamily,
|
monoFontFamily,
|
||||||
|
|
@ -40,6 +42,18 @@ type ThemeOption = {
|
||||||
name: string
|
name: string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type ShellOption = {
|
||||||
|
path: string
|
||||||
|
name: string
|
||||||
|
acceptable: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
type ShellSelectOption = {
|
||||||
|
id: string
|
||||||
|
value: string
|
||||||
|
label: string
|
||||||
|
}
|
||||||
|
|
||||||
// To prevent audio from overlapping/playing very quickly when navigating the settings menus,
|
// To prevent audio from overlapping/playing very quickly when navigating the settings menus,
|
||||||
// delay the playback by 100ms during quick selection changes and pause existing sounds.
|
// delay the playback by 100ms during quick selection changes and pause existing sounds.
|
||||||
const stopDemoSound = () => {
|
const stopDemoSound = () => {
|
||||||
|
|
@ -75,10 +89,6 @@ export const SettingsGeneral: Component = () => {
|
||||||
const params = useParams()
|
const params = useParams()
|
||||||
const settings = useSettings()
|
const settings = useSettings()
|
||||||
|
|
||||||
onMount(() => {
|
|
||||||
void theme.loadThemes()
|
|
||||||
})
|
|
||||||
|
|
||||||
const [store, setStore] = createStore({
|
const [store, setStore] = createStore({
|
||||||
checking: false,
|
checking: false,
|
||||||
})
|
})
|
||||||
|
|
@ -128,27 +138,25 @@ export const SettingsGeneral: Component = () => {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
const actions =
|
const actions = platform.updateAndRestart
|
||||||
platform.update && platform.restart
|
? [
|
||||||
? [
|
{
|
||||||
{
|
label: language.t("toast.update.action.installRestart"),
|
||||||
label: language.t("toast.update.action.installRestart"),
|
onClick: async () => {
|
||||||
onClick: async () => {
|
await platform.updateAndRestart!()
|
||||||
await platform.update!()
|
|
||||||
await platform.restart!()
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
{
|
},
|
||||||
label: language.t("toast.update.action.notYet"),
|
{
|
||||||
onClick: "dismiss" as const,
|
label: language.t("toast.update.action.notYet"),
|
||||||
},
|
onClick: "dismiss" as const,
|
||||||
]
|
},
|
||||||
: [
|
]
|
||||||
{
|
: [
|
||||||
label: language.t("toast.update.action.notYet"),
|
{
|
||||||
onClick: "dismiss" as const,
|
label: language.t("toast.update.action.notYet"),
|
||||||
},
|
onClick: "dismiss" as const,
|
||||||
]
|
},
|
||||||
|
]
|
||||||
|
|
||||||
showToast({
|
showToast({
|
||||||
persistent: true,
|
persistent: true,
|
||||||
|
|
@ -167,6 +175,70 @@ export const SettingsGeneral: Component = () => {
|
||||||
|
|
||||||
const themeOptions = createMemo<ThemeOption[]>(() => theme.ids().map((id) => ({ id, name: theme.name(id) })))
|
const themeOptions = createMemo<ThemeOption[]>(() => theme.ids().map((id) => ({ id, name: theme.name(id) })))
|
||||||
|
|
||||||
|
const globalSync = useGlobalSync()
|
||||||
|
const globalSdk = useGlobalSDK()
|
||||||
|
|
||||||
|
const [shells] = createResource(
|
||||||
|
() =>
|
||||||
|
globalSdk.client.pty
|
||||||
|
.shells()
|
||||||
|
.then((res) => res.data ?? [])
|
||||||
|
.catch(() => [] as ShellOption[]),
|
||||||
|
{ initialValue: [] as ShellOption[] },
|
||||||
|
)
|
||||||
|
|
||||||
|
const [displayBackend, { refetch: refetchDisplayBackend }] = createResource(
|
||||||
|
() => (linux() && platform.getDisplayBackend ? true : false),
|
||||||
|
() => Promise.resolve(platform.getDisplayBackend?.() ?? null).catch(() => null as DisplayBackend | null),
|
||||||
|
{ initialValue: null as DisplayBackend | null },
|
||||||
|
)
|
||||||
|
|
||||||
|
onMount(() => {
|
||||||
|
void theme.loadThemes()
|
||||||
|
})
|
||||||
|
|
||||||
|
const autoOption = { id: "auto", value: "", label: language.t("settings.general.row.shell.autoDefault") }
|
||||||
|
const currentShell = createMemo(() => globalSync.data.config.shell ?? "")
|
||||||
|
|
||||||
|
const shellOptions = createMemo<ShellSelectOption[]>(() => {
|
||||||
|
const list = shells.latest
|
||||||
|
const current = globalSync.data.config.shell
|
||||||
|
|
||||||
|
const nameCounts = new Map<string, number>()
|
||||||
|
for (const s of list) {
|
||||||
|
nameCounts.set(s.name, (nameCounts.get(s.name) || 0) + 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
const options = [
|
||||||
|
autoOption,
|
||||||
|
...list.map((s) => {
|
||||||
|
const ambiguousName = (nameCounts.get(s.name) || 0) > 1
|
||||||
|
const text = ambiguousName ? s.path : s.name
|
||||||
|
const label = s.acceptable ? text : `${text} (${language.t("settings.general.row.shell.terminalOnly")})`
|
||||||
|
return {
|
||||||
|
id: s.path,
|
||||||
|
// Prefer name over path - "bash" is much cleaner than the explicit full route even when it may change due to PATH.
|
||||||
|
value: ambiguousName ? s.path : s.name,
|
||||||
|
label,
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
]
|
||||||
|
|
||||||
|
if (current && !options.some((o) => o.value === current)) {
|
||||||
|
options.push({ id: current, value: current, label: current })
|
||||||
|
}
|
||||||
|
|
||||||
|
return options
|
||||||
|
})
|
||||||
|
|
||||||
|
const onDisplayBackendChange = (checked: boolean) => {
|
||||||
|
const update = platform.setDisplayBackend?.(checked ? "wayland" : "auto")
|
||||||
|
if (!update) return
|
||||||
|
void update.finally(() => {
|
||||||
|
void refetchDisplayBackend()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
const colorSchemeOptions = createMemo((): { value: ColorScheme; label: string }[] => [
|
const colorSchemeOptions = createMemo((): { value: ColorScheme; label: string }[] => [
|
||||||
{ value: "system", label: language.t("theme.scheme.system") },
|
{ value: "system", label: language.t("theme.scheme.system") },
|
||||||
{ value: "light", label: language.t("theme.scheme.light") },
|
{ value: "light", label: language.t("theme.scheme.light") },
|
||||||
|
|
@ -245,6 +317,28 @@ export const SettingsGeneral: Component = () => {
|
||||||
</div>
|
</div>
|
||||||
</SettingsRow>
|
</SettingsRow>
|
||||||
|
|
||||||
|
<SettingsRow
|
||||||
|
title={language.t("settings.general.row.shell.title")}
|
||||||
|
description={language.t("settings.general.row.shell.description")}
|
||||||
|
>
|
||||||
|
<Select
|
||||||
|
data-action="settings-shell"
|
||||||
|
options={shellOptions()}
|
||||||
|
current={shellOptions().find((o) => o.value === currentShell()) ?? autoOption}
|
||||||
|
value={(o) => o.id}
|
||||||
|
label={(o) => o.label}
|
||||||
|
onSelect={(option) => {
|
||||||
|
if (!option) return
|
||||||
|
if (option.value === currentShell()) return
|
||||||
|
globalSync.updateConfig({ shell: option.value })
|
||||||
|
}}
|
||||||
|
variant="secondary"
|
||||||
|
size="small"
|
||||||
|
triggerVariant="settings"
|
||||||
|
triggerStyle={{ "min-width": "180px" }}
|
||||||
|
/>
|
||||||
|
</SettingsRow>
|
||||||
|
|
||||||
<SettingsRow
|
<SettingsRow
|
||||||
title={language.t("settings.general.row.reasoningSummaries.title")}
|
title={language.t("settings.general.row.reasoningSummaries.title")}
|
||||||
description={language.t("settings.general.row.reasoningSummaries.description")}
|
description={language.t("settings.general.row.reasoningSummaries.description")}
|
||||||
|
|
@ -653,70 +747,32 @@ export const SettingsGeneral: Component = () => {
|
||||||
|
|
||||||
<SoundsSection />
|
<SoundsSection />
|
||||||
|
|
||||||
{/*<Show when={platform.platform === "desktop" && platform.os === "windows" && platform.getWslEnabled}>
|
|
||||||
{(_) => {
|
|
||||||
const [enabledResource, actions] = createResource(() => platform.getWslEnabled?.())
|
|
||||||
const enabled = () => (enabledResource.state === "pending" ? undefined : enabledResource.latest)
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div class="flex flex-col gap-1">
|
|
||||||
<h3 class="text-14-medium text-text-strong pb-2">{language.t("settings.desktop.section.wsl")}</h3>
|
|
||||||
|
|
||||||
<SettingsList>
|
|
||||||
<SettingsRow
|
|
||||||
title={language.t("settings.desktop.wsl.title")}
|
|
||||||
description={language.t("settings.desktop.wsl.description")}
|
|
||||||
>
|
|
||||||
<div data-action="settings-wsl">
|
|
||||||
<Switch
|
|
||||||
checked={enabled() ?? false}
|
|
||||||
disabled={enabledResource.state === "pending"}
|
|
||||||
onChange={(checked) => platform.setWslEnabled?.(checked)?.finally(() => actions.refetch())}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</SettingsRow>
|
|
||||||
</SettingsList>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}}
|
|
||||||
</Show>*/}
|
|
||||||
|
|
||||||
<UpdatesSection />
|
<UpdatesSection />
|
||||||
|
|
||||||
<Show when={linux()}>
|
<Show when={linux()}>
|
||||||
{(_) => {
|
<div class="flex flex-col gap-1">
|
||||||
const [valueResource, actions] = createResource(() => platform.getDisplayBackend?.())
|
<h3 class="text-14-medium text-text-strong pb-2">{language.t("settings.general.section.display")}</h3>
|
||||||
const value = () => (valueResource.state === "pending" ? undefined : valueResource.latest)
|
|
||||||
|
|
||||||
const onChange = (checked: boolean) =>
|
<SettingsList>
|
||||||
platform.setDisplayBackend?.(checked ? "wayland" : "auto").finally(() => actions.refetch())
|
<SettingsRow
|
||||||
|
title={
|
||||||
return (
|
<div class="flex items-center gap-2">
|
||||||
<div class="flex flex-col gap-1">
|
<span>{language.t("settings.general.row.wayland.title")}</span>
|
||||||
<h3 class="text-14-medium text-text-strong pb-2">{language.t("settings.general.section.display")}</h3>
|
<Tooltip value={language.t("settings.general.row.wayland.tooltip")} placement="top">
|
||||||
|
<span class="text-text-weak">
|
||||||
<SettingsList>
|
<Icon name="help" size="small" />
|
||||||
<SettingsRow
|
</span>
|
||||||
title={
|
</Tooltip>
|
||||||
<div class="flex items-center gap-2">
|
</div>
|
||||||
<span>{language.t("settings.general.row.wayland.title")}</span>
|
}
|
||||||
<Tooltip value={language.t("settings.general.row.wayland.tooltip")} placement="top">
|
description={language.t("settings.general.row.wayland.description")}
|
||||||
<span class="text-text-weak">
|
>
|
||||||
<Icon name="help" size="small" />
|
<div data-action="settings-wayland">
|
||||||
</span>
|
<Switch checked={displayBackend.latest === "wayland"} onChange={onDisplayBackendChange} />
|
||||||
</Tooltip>
|
</div>
|
||||||
</div>
|
</SettingsRow>
|
||||||
}
|
</SettingsList>
|
||||||
description={language.t("settings.general.row.wayland.description")}
|
</div>
|
||||||
>
|
|
||||||
<div data-action="settings-wayland">
|
|
||||||
<Switch checked={value() === "wayland"} onChange={onChange} />
|
|
||||||
</div>
|
|
||||||
</SettingsRow>
|
|
||||||
</SettingsList>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}}
|
|
||||||
</Show>
|
</Show>
|
||||||
|
|
||||||
<Show when={desktop() && import.meta.env.VITE_OPENCODE_CHANNEL === "beta"}>
|
<Show when={desktop() && import.meta.env.VITE_OPENCODE_CHANNEL === "beta"}>
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@ import { useDialog } from "@opencode-ai/ui/context/dialog"
|
||||||
import { Icon } from "@opencode-ai/ui/icon"
|
import { Icon } from "@opencode-ai/ui/icon"
|
||||||
import { Switch } from "@opencode-ai/ui/switch"
|
import { Switch } from "@opencode-ai/ui/switch"
|
||||||
import { Tabs } from "@opencode-ai/ui/tabs"
|
import { Tabs } from "@opencode-ai/ui/tabs"
|
||||||
import { useMutation } from "@tanstack/solid-query"
|
import { useMutation, useQueryClient } from "@tanstack/solid-query"
|
||||||
import { showToast } from "@opencode-ai/ui/toast"
|
import { showToast } from "@opencode-ai/ui/toast"
|
||||||
import { useNavigate } from "@solidjs/router"
|
import { useNavigate } from "@solidjs/router"
|
||||||
import { type Accessor, createEffect, createMemo, For, type JSXElement, onCleanup, Show } from "solid-js"
|
import { type Accessor, createEffect, createMemo, For, type JSXElement, onCleanup, Show } from "solid-js"
|
||||||
|
|
@ -15,6 +15,7 @@ import { useSDK } from "@/context/sdk"
|
||||||
import { normalizeServerUrl, ServerConnection, useServer } from "@/context/server"
|
import { normalizeServerUrl, ServerConnection, useServer } from "@/context/server"
|
||||||
import { useSync } from "@/context/sync"
|
import { useSync } from "@/context/sync"
|
||||||
import { useCheckServerHealth, type ServerHealth } from "@/utils/server-health"
|
import { useCheckServerHealth, type ServerHealth } from "@/utils/server-health"
|
||||||
|
import { mcpQueryKey } from "@/context/global-sync"
|
||||||
|
|
||||||
const pollMs = 10_000
|
const pollMs = 10_000
|
||||||
|
|
||||||
|
|
@ -137,14 +138,14 @@ const useMcpToggleMutation = () => {
|
||||||
const sync = useSync()
|
const sync = useSync()
|
||||||
const sdk = useSDK()
|
const sdk = useSDK()
|
||||||
const language = useLanguage()
|
const language = useLanguage()
|
||||||
|
const queryClient = useQueryClient()
|
||||||
|
|
||||||
return useMutation(() => ({
|
return useMutation(() => ({
|
||||||
mutationFn: async (name: string) => {
|
mutationFn: async (name: string) => {
|
||||||
const status = sync.data.mcp[name]
|
const status = sync.data.mcp[name]
|
||||||
await (status?.status === "connected" ? sdk.client.mcp.disconnect({ name }) : sdk.client.mcp.connect({ name }))
|
await (status?.status === "connected" ? sdk.client.mcp.disconnect({ name }) : sdk.client.mcp.connect({ name }))
|
||||||
const result = await sdk.client.mcp.status()
|
|
||||||
if (result.data) sync.set("mcp", result.data)
|
|
||||||
},
|
},
|
||||||
|
onSuccess: () => queryClient.refetchQueries({ queryKey: mcpQueryKey(sync.directory) }),
|
||||||
onError: (err) => {
|
onError: (err) => {
|
||||||
showToast({
|
showToast({
|
||||||
variant: "error",
|
variant: "error",
|
||||||
|
|
@ -162,14 +163,6 @@ export function StatusPopoverBody(props: { shown: Accessor<boolean> }) {
|
||||||
const dialog = useDialog()
|
const dialog = useDialog()
|
||||||
const language = useLanguage()
|
const language = useLanguage()
|
||||||
const navigate = useNavigate()
|
const navigate = useNavigate()
|
||||||
const sdk = useSDK()
|
|
||||||
|
|
||||||
const [load, setLoad] = createStore({
|
|
||||||
lspDone: false,
|
|
||||||
lspLoading: false,
|
|
||||||
mcpDone: false,
|
|
||||||
mcpLoading: false,
|
|
||||||
})
|
|
||||||
|
|
||||||
const fail = (err: unknown) => {
|
const fail = (err: unknown) => {
|
||||||
showToast({
|
showToast({
|
||||||
|
|
@ -181,40 +174,6 @@ export function StatusPopoverBody(props: { shown: Accessor<boolean> }) {
|
||||||
|
|
||||||
createEffect(() => {
|
createEffect(() => {
|
||||||
if (!props.shown()) return
|
if (!props.shown()) return
|
||||||
|
|
||||||
if (!sync.data.mcp_ready && !load.mcpDone && !load.mcpLoading) {
|
|
||||||
setLoad("mcpLoading", true)
|
|
||||||
void sdk.client.mcp
|
|
||||||
.status()
|
|
||||||
.then((result) => {
|
|
||||||
sync.set("mcp", result.data ?? {})
|
|
||||||
sync.set("mcp_ready", true)
|
|
||||||
})
|
|
||||||
.catch((err) => {
|
|
||||||
setLoad("mcpDone", true)
|
|
||||||
fail(err)
|
|
||||||
})
|
|
||||||
.finally(() => {
|
|
||||||
setLoad("mcpLoading", false)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!sync.data.lsp_ready && !load.lspDone && !load.lspLoading) {
|
|
||||||
setLoad("lspLoading", true)
|
|
||||||
void sdk.client.lsp
|
|
||||||
.status()
|
|
||||||
.then((result) => {
|
|
||||||
sync.set("lsp", result.data ?? [])
|
|
||||||
sync.set("lsp_ready", true)
|
|
||||||
})
|
|
||||||
.catch((err) => {
|
|
||||||
setLoad("lspDone", true)
|
|
||||||
fail(err)
|
|
||||||
})
|
|
||||||
.finally(() => {
|
|
||||||
setLoad("lspLoading", false)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
|
|
||||||
let dialogRun = 0
|
let dialogRun = 0
|
||||||
|
|
|
||||||
|
|
@ -15,6 +15,7 @@ import { terminalFontFamily, useSettings } from "@/context/settings"
|
||||||
import type { LocalPTY } from "@/context/terminal"
|
import type { LocalPTY } from "@/context/terminal"
|
||||||
import { disposeIfDisposable, getHoveredLinkText, setOptionIfSupported } from "@/utils/runtime-adapters"
|
import { disposeIfDisposable, getHoveredLinkText, setOptionIfSupported } from "@/utils/runtime-adapters"
|
||||||
import { terminalWriter } from "@/utils/terminal-writer"
|
import { terminalWriter } from "@/utils/terminal-writer"
|
||||||
|
import { terminalWebSocketURL } from "@/utils/terminal-websocket-url"
|
||||||
|
|
||||||
const TOGGLE_TERMINAL_ID = "terminal.toggle"
|
const TOGGLE_TERMINAL_ID = "terminal.toggle"
|
||||||
const DEFAULT_TOGGLE_TERMINAL_KEYBIND = "ctrl+`"
|
const DEFAULT_TOGGLE_TERMINAL_KEYBIND = "ctrl+`"
|
||||||
|
|
@ -67,13 +68,6 @@ const debugTerminal = (...values: unknown[]) => {
|
||||||
console.debug("[terminal]", ...values)
|
console.debug("[terminal]", ...values)
|
||||||
}
|
}
|
||||||
|
|
||||||
const errorName = (err: unknown) => {
|
|
||||||
if (!err || typeof err !== "object") return
|
|
||||||
if (!("name" in err)) return
|
|
||||||
const errorName = err.name
|
|
||||||
return typeof errorName === "string" ? errorName : undefined
|
|
||||||
}
|
|
||||||
|
|
||||||
const useTerminalUiBindings = (input: {
|
const useTerminalUiBindings = (input: {
|
||||||
container: HTMLDivElement
|
container: HTMLDivElement
|
||||||
term: Term
|
term: Term
|
||||||
|
|
@ -478,14 +472,34 @@ export const Terminal = (props: TerminalProps) => {
|
||||||
|
|
||||||
const gone = () =>
|
const gone = () =>
|
||||||
client.pty
|
client.pty
|
||||||
.get({ ptyID: id })
|
.get({ ptyID: id }, { throwOnError: false })
|
||||||
.then(() => false)
|
.then((result) => result.response.status === 404)
|
||||||
.catch((err) => {
|
.catch((err) => {
|
||||||
if (errorName(err) === "NotFoundError") return true
|
|
||||||
debugTerminal("failed to inspect terminal session", err)
|
debugTerminal("failed to inspect terminal session", err)
|
||||||
return false
|
return false
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const connectToken = async () => {
|
||||||
|
const result = await client.pty
|
||||||
|
.connectToken(
|
||||||
|
{ ptyID: id, directory },
|
||||||
|
{
|
||||||
|
throwOnError: false,
|
||||||
|
headers: { "x-opencode-ticket": "1" },
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.catch((err: unknown) => {
|
||||||
|
if (err instanceof Error && err.message.includes("Request is not supported")) return
|
||||||
|
throw err
|
||||||
|
})
|
||||||
|
if (!result) return
|
||||||
|
if (result.response.status === 200 && result.data?.ticket) return result.data.ticket
|
||||||
|
if (result.response.status === 404 || result.response.status === 405) return
|
||||||
|
if (result.response.status === 403)
|
||||||
|
throw new Error("PTY connect ticket rejected by origin or CSRF checks. Check the server CORS config.")
|
||||||
|
throw new Error(`PTY connect ticket failed with ${result.response.status}`)
|
||||||
|
}
|
||||||
|
|
||||||
const retry = (err: unknown) => {
|
const retry = (err: unknown) => {
|
||||||
if (disposed) return
|
if (disposed) return
|
||||||
if (reconn !== undefined) return
|
if (reconn !== undefined) return
|
||||||
|
|
@ -505,22 +519,30 @@ export const Terminal = (props: TerminalProps) => {
|
||||||
}, ms)
|
}, ms)
|
||||||
}
|
}
|
||||||
|
|
||||||
const open = () => {
|
const open = async () => {
|
||||||
if (disposed) return
|
if (disposed) return
|
||||||
drop?.()
|
drop?.()
|
||||||
|
|
||||||
const next = new URL(url + `/pty/${id}/connect`)
|
const ticket = await connectToken().catch((err) => {
|
||||||
next.searchParams.set("directory", directory)
|
fail(err)
|
||||||
next.searchParams.set("cursor", String(seek))
|
return undefined
|
||||||
next.protocol = next.protocol === "https:" ? "wss:" : "ws:"
|
})
|
||||||
if (!sameOrigin && password) {
|
if (once.value) return
|
||||||
next.searchParams.set("auth_token", btoa(`${username}:${password}`))
|
if (disposed) return
|
||||||
// For same-origin requests, let the browser reuse the page's existing auth.
|
|
||||||
next.username = username
|
|
||||||
next.password = password
|
|
||||||
}
|
|
||||||
|
|
||||||
const socket = new WebSocket(next)
|
const socket = new WebSocket(
|
||||||
|
terminalWebSocketURL({
|
||||||
|
url,
|
||||||
|
id,
|
||||||
|
directory,
|
||||||
|
cursor: seek,
|
||||||
|
ticket,
|
||||||
|
sameOrigin,
|
||||||
|
username,
|
||||||
|
password,
|
||||||
|
authToken: server.current?.type === "http" ? server.current.authToken : false,
|
||||||
|
}),
|
||||||
|
)
|
||||||
socket.binaryType = "arraybuffer"
|
socket.binaryType = "arraybuffer"
|
||||||
ws = socket
|
ws = socket
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -35,6 +35,9 @@ type TauriApi = {
|
||||||
const tauriApi = () => (window as unknown as { __TAURI__?: TauriApi }).__TAURI__
|
const tauriApi = () => (window as unknown as { __TAURI__?: TauriApi }).__TAURI__
|
||||||
const currentDesktopWindow = () => tauriApi()?.window?.getCurrentWindow?.()
|
const currentDesktopWindow = () => tauriApi()?.window?.getCurrentWindow?.()
|
||||||
const currentThemeWindow = () => tauriApi()?.webviewWindow?.getCurrentWebviewWindow?.()
|
const currentThemeWindow = () => tauriApi()?.webviewWindow?.getCurrentWebviewWindow?.()
|
||||||
|
const titlebarHeight = 40
|
||||||
|
const minTitlebarZoom = 0.25
|
||||||
|
const windowsControlsBaseWidth = 138 // 3 native Windows caption buttons at 46px each.
|
||||||
|
|
||||||
export function Titlebar() {
|
export function Titlebar() {
|
||||||
const layout = useLayout()
|
const layout = useLayout()
|
||||||
|
|
@ -51,7 +54,14 @@ export function Titlebar() {
|
||||||
const windows = createMemo(() => platform.platform === "desktop" && platform.os === "windows")
|
const windows = createMemo(() => platform.platform === "desktop" && platform.os === "windows")
|
||||||
const web = createMemo(() => platform.platform === "web")
|
const web = createMemo(() => platform.platform === "web")
|
||||||
const zoom = () => platform.webviewZoom?.() ?? 1
|
const zoom = () => platform.webviewZoom?.() ?? 1
|
||||||
const minHeight = () => (mac() ? `${40 / zoom()}px` : undefined)
|
const titlebarZoom = () => (windows() ? Math.max(zoom(), minTitlebarZoom) : zoom())
|
||||||
|
const counterZoom = () => (windows() && titlebarZoom() < 1 ? 1 / titlebarZoom() : 1)
|
||||||
|
const minHeight = () => {
|
||||||
|
if (mac()) return `${titlebarHeight / zoom()}px`
|
||||||
|
if (windows()) return `${titlebarHeight / Math.min(titlebarZoom(), 1)}px`
|
||||||
|
return undefined
|
||||||
|
}
|
||||||
|
const windowsControlsWidth = () => `${windowsControlsBaseWidth / Math.max(titlebarZoom(), 1)}px`
|
||||||
|
|
||||||
const [history, setHistory] = createStore({
|
const [history, setHistory] = createStore({
|
||||||
stack: [] as string[],
|
stack: [] as string[],
|
||||||
|
|
@ -165,156 +175,161 @@ export function Titlebar() {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<header
|
<header
|
||||||
class="h-10 shrink-0 bg-background-base relative grid grid-cols-[minmax(0,1fr)_auto_minmax(0,1fr)] items-center"
|
class="h-10 shrink-0 bg-background-base relative overflow-hidden"
|
||||||
style={{ "min-height": minHeight() }}
|
style={{ "min-height": minHeight() }}
|
||||||
data-tauri-drag-region
|
data-tauri-drag-region
|
||||||
onMouseDown={drag}
|
onMouseDown={drag}
|
||||||
onDblClick={maximize}
|
onDblClick={maximize}
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
classList={{
|
class="grid h-full min-h-full w-full grid-cols-[minmax(0,1fr)_auto_minmax(0,1fr)] items-center"
|
||||||
"flex items-center min-w-0": true,
|
style={{ zoom: counterZoom() }}
|
||||||
"pl-2": !mac(),
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
<Show when={mac()}>
|
<div
|
||||||
<div class="h-full shrink-0" style={{ width: `${72 / zoom()}px` }} />
|
classList={{
|
||||||
<div class="xl:hidden w-10 shrink-0 flex items-center justify-center">
|
"flex items-center min-w-0": true,
|
||||||
<IconButton
|
"pl-2": !mac(),
|
||||||
icon="menu"
|
}}
|
||||||
variant="ghost"
|
>
|
||||||
class="titlebar-icon rounded-md"
|
<Show when={mac()}>
|
||||||
onClick={layout.mobileSidebar.toggle}
|
<div class="h-full shrink-0" style={{ width: `${72 / zoom()}px` }} />
|
||||||
aria-label={language.t("sidebar.menu.toggle")}
|
<div class="xl:hidden w-10 shrink-0 flex items-center justify-center">
|
||||||
aria-expanded={layout.mobileSidebar.opened()}
|
<IconButton
|
||||||
/>
|
icon="menu"
|
||||||
</div>
|
variant="ghost"
|
||||||
</Show>
|
class="titlebar-icon rounded-md"
|
||||||
<Show when={!mac()}>
|
onClick={layout.mobileSidebar.toggle}
|
||||||
<div class="xl:hidden w-[48px] shrink-0 flex items-center justify-center">
|
aria-label={language.t("sidebar.menu.toggle")}
|
||||||
<IconButton
|
aria-expanded={layout.mobileSidebar.opened()}
|
||||||
icon="menu"
|
/>
|
||||||
variant="ghost"
|
</div>
|
||||||
class="titlebar-icon rounded-md"
|
</Show>
|
||||||
onClick={layout.mobileSidebar.toggle}
|
<Show when={!mac()}>
|
||||||
aria-label={language.t("sidebar.menu.toggle")}
|
<div class="xl:hidden w-[48px] shrink-0 flex items-center justify-center">
|
||||||
aria-expanded={layout.mobileSidebar.opened()}
|
<IconButton
|
||||||
/>
|
icon="menu"
|
||||||
</div>
|
variant="ghost"
|
||||||
</Show>
|
class="titlebar-icon rounded-md"
|
||||||
<div class="flex items-center gap-1 shrink-0">
|
onClick={layout.mobileSidebar.toggle}
|
||||||
<TooltipKeybind
|
aria-label={language.t("sidebar.menu.toggle")}
|
||||||
class={web() ? "hidden xl:flex shrink-0 ml-14" : "hidden xl:flex shrink-0 ml-2"}
|
aria-expanded={layout.mobileSidebar.opened()}
|
||||||
placement="bottom"
|
/>
|
||||||
title={language.t("command.sidebar.toggle")}
|
</div>
|
||||||
keybind={command.keybind("sidebar.toggle")}
|
</Show>
|
||||||
>
|
<div class="flex items-center gap-1 shrink-0">
|
||||||
<Button
|
<TooltipKeybind
|
||||||
variant="ghost"
|
class={web() ? "hidden xl:flex shrink-0 ml-14" : "hidden xl:flex shrink-0 ml-2"}
|
||||||
class="group/sidebar-toggle titlebar-icon w-8 h-6 p-0 box-border"
|
placement="bottom"
|
||||||
onClick={layout.sidebar.toggle}
|
title={language.t("command.sidebar.toggle")}
|
||||||
aria-label={language.t("command.sidebar.toggle")}
|
keybind={command.keybind("sidebar.toggle")}
|
||||||
aria-expanded={layout.sidebar.opened()}
|
|
||||||
>
|
>
|
||||||
<Icon size="small" name={layout.sidebar.opened() ? "sidebar-active" : "sidebar"} />
|
<Button
|
||||||
</Button>
|
variant="ghost"
|
||||||
</TooltipKeybind>
|
class="group/sidebar-toggle titlebar-icon w-8 h-6 p-0 box-border"
|
||||||
<div class="hidden xl:flex items-center shrink-0">
|
onClick={layout.sidebar.toggle}
|
||||||
<Show when={params.dir}>
|
aria-label={language.t("command.sidebar.toggle")}
|
||||||
<div
|
aria-expanded={layout.sidebar.opened()}
|
||||||
class="flex items-center shrink-0 w-8 mr-1"
|
|
||||||
aria-hidden={layout.sidebar.opened() ? "true" : undefined}
|
|
||||||
>
|
>
|
||||||
|
<Icon size="small" name={layout.sidebar.opened() ? "sidebar-active" : "sidebar"} />
|
||||||
|
</Button>
|
||||||
|
</TooltipKeybind>
|
||||||
|
<div class="hidden xl:flex items-center shrink-0">
|
||||||
|
<Show when={params.dir}>
|
||||||
<div
|
<div
|
||||||
class="transition-opacity"
|
class="flex items-center shrink-0 w-8 mr-1"
|
||||||
classList={{
|
aria-hidden={layout.sidebar.opened() ? "true" : undefined}
|
||||||
"opacity-100 duration-120 ease-out": !layout.sidebar.opened(),
|
|
||||||
"opacity-0 duration-120 ease-in delay-0 pointer-events-none": layout.sidebar.opened(),
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
<TooltipKeybind
|
<div
|
||||||
placement="bottom"
|
class="transition-opacity"
|
||||||
title={language.t("command.session.new")}
|
classList={{
|
||||||
keybind={command.keybind("session.new")}
|
"opacity-100 duration-120 ease-out": !layout.sidebar.opened(),
|
||||||
openDelay={2000}
|
"opacity-0 duration-120 ease-in delay-0 pointer-events-none": layout.sidebar.opened(),
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
<Button
|
<TooltipKeybind
|
||||||
variant="ghost"
|
placement="bottom"
|
||||||
icon={creating() ? "new-session-active" : "new-session"}
|
title={language.t("command.session.new")}
|
||||||
class="titlebar-icon w-8 h-6 p-0 box-border"
|
keybind={command.keybind("session.new")}
|
||||||
disabled={layout.sidebar.opened()}
|
openDelay={2000}
|
||||||
tabIndex={layout.sidebar.opened() ? -1 : undefined}
|
>
|
||||||
onClick={() => {
|
<Button
|
||||||
if (!params.dir) return
|
variant="ghost"
|
||||||
navigate(`/${params.dir}/session`)
|
icon={creating() ? "new-session-active" : "new-session"}
|
||||||
}}
|
class="titlebar-icon w-8 h-6 p-0 box-border"
|
||||||
aria-label={language.t("command.session.new")}
|
disabled={layout.sidebar.opened()}
|
||||||
aria-current={creating() ? "page" : undefined}
|
tabIndex={layout.sidebar.opened() ? -1 : undefined}
|
||||||
/>
|
onClick={() => {
|
||||||
</TooltipKeybind>
|
if (!params.dir) return
|
||||||
</div>
|
navigate(`/${params.dir}/session`)
|
||||||
</div>
|
}}
|
||||||
</Show>
|
aria-label={language.t("command.session.new")}
|
||||||
<div
|
aria-current={creating() ? "page" : undefined}
|
||||||
class="flex items-center shrink-0"
|
/>
|
||||||
classList={{
|
</TooltipKeybind>
|
||||||
"-translate-x-[36px]": layout.sidebar.opened() && !!params.dir,
|
</div>
|
||||||
"duration-180 ease-out": !layout.sidebar.opened(),
|
|
||||||
"duration-180 ease-in": layout.sidebar.opened(),
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Show when={hasProjects() && nav()}>
|
|
||||||
<div class="flex items-center gap-0 transition-transform">
|
|
||||||
<Tooltip placement="bottom" value={language.t("common.goBack")} openDelay={2000}>
|
|
||||||
<Button
|
|
||||||
variant="ghost"
|
|
||||||
icon="chevron-left"
|
|
||||||
class="titlebar-icon w-6 h-6 p-0 box-border"
|
|
||||||
disabled={!canBack()}
|
|
||||||
onClick={back}
|
|
||||||
aria-label={language.t("common.goBack")}
|
|
||||||
/>
|
|
||||||
</Tooltip>
|
|
||||||
<Tooltip placement="bottom" value={language.t("common.goForward")} openDelay={2000}>
|
|
||||||
<Button
|
|
||||||
variant="ghost"
|
|
||||||
icon="chevron-right"
|
|
||||||
class="titlebar-icon w-6 h-6 p-0 box-border"
|
|
||||||
disabled={!canForward()}
|
|
||||||
onClick={forward}
|
|
||||||
aria-label={language.t("common.goForward")}
|
|
||||||
/>
|
|
||||||
</Tooltip>
|
|
||||||
</div>
|
</div>
|
||||||
</Show>
|
</Show>
|
||||||
<div id="opencode-titlebar-left" class="flex items-center gap-3 min-w-0 px-2" />
|
<div
|
||||||
{["beta", "dev"].includes(import.meta.env.VITE_OPENCODE_CHANNEL) && (
|
class="flex items-center shrink-0"
|
||||||
<div class="bg-icon-interactive-base text-[#FFF] font-medium px-2 rounded-sm uppercase font-mono">
|
classList={{
|
||||||
{import.meta.env.VITE_OPENCODE_CHANNEL.toUpperCase()}
|
"-translate-x-[36px]": layout.sidebar.opened() && !!params.dir,
|
||||||
</div>
|
"duration-180 ease-out": !layout.sidebar.opened(),
|
||||||
)}
|
"duration-180 ease-in": layout.sidebar.opened(),
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Show when={hasProjects() && nav()}>
|
||||||
|
<div class="flex items-center gap-0 transition-transform">
|
||||||
|
<Tooltip placement="bottom" value={language.t("common.goBack")} openDelay={2000}>
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
icon="chevron-left"
|
||||||
|
class="titlebar-icon w-6 h-6 p-0 box-border"
|
||||||
|
disabled={!canBack()}
|
||||||
|
onClick={back}
|
||||||
|
aria-label={language.t("common.goBack")}
|
||||||
|
/>
|
||||||
|
</Tooltip>
|
||||||
|
<Tooltip placement="bottom" value={language.t("common.goForward")} openDelay={2000}>
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
icon="chevron-right"
|
||||||
|
class="titlebar-icon w-6 h-6 p-0 box-border"
|
||||||
|
disabled={!canForward()}
|
||||||
|
onClick={forward}
|
||||||
|
aria-label={language.t("common.goForward")}
|
||||||
|
/>
|
||||||
|
</Tooltip>
|
||||||
|
</div>
|
||||||
|
</Show>
|
||||||
|
<div id="opencode-titlebar-left" class="flex items-center gap-3 min-w-0 px-2" />
|
||||||
|
{["beta", "dev"].includes(import.meta.env.VITE_OPENCODE_CHANNEL) && (
|
||||||
|
<div class="bg-icon-interactive-base text-[#FFF] font-medium px-2 rounded-sm uppercase font-mono">
|
||||||
|
{import.meta.env.VITE_OPENCODE_CHANNEL.toUpperCase()}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="min-w-0 flex items-center justify-center pointer-events-none">
|
<div class="min-w-0 flex items-center justify-center pointer-events-none">
|
||||||
<div id="opencode-titlebar-center" class="pointer-events-auto min-w-0 flex justify-center w-fit max-w-full" />
|
<div id="opencode-titlebar-center" class="pointer-events-auto min-w-0 flex justify-center w-fit max-w-full" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
classList={{
|
classList={{
|
||||||
"flex items-center min-w-0 justify-end": true,
|
"flex items-center min-w-0 justify-end": true,
|
||||||
"pr-2": !windows(),
|
"pr-2": !windows(),
|
||||||
}}
|
}}
|
||||||
data-tauri-drag-region
|
data-tauri-drag-region
|
||||||
onMouseDown={drag}
|
onMouseDown={drag}
|
||||||
>
|
>
|
||||||
<div id="opencode-titlebar-right" class="flex items-center gap-1 shrink-0 justify-end" />
|
<div id="opencode-titlebar-right" class="flex items-center gap-1 shrink-0 justify-end" />
|
||||||
<Show when={windows()}>
|
<Show when={windows()}>
|
||||||
{!tauriApi() && <div class="w-36 shrink-0" />}
|
{!tauriApi() && <div class="shrink-0" style={{ width: windowsControlsWidth() }} />}
|
||||||
<div data-tauri-decorum-tb class="flex flex-row" />
|
<div data-tauri-decorum-tb class="flex flex-row" />
|
||||||
</Show>
|
</Show>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@ import { createStore, produce, reconcile } from "solid-js/store"
|
||||||
import { createSimpleContext } from "@opencode-ai/ui/context"
|
import { createSimpleContext } from "@opencode-ai/ui/context"
|
||||||
import { showToast } from "@opencode-ai/ui/toast"
|
import { showToast } from "@opencode-ai/ui/toast"
|
||||||
import { useParams } from "@solidjs/router"
|
import { useParams } from "@solidjs/router"
|
||||||
import { getFilename } from "@opencode-ai/shared/util/path"
|
import { getFilename } from "@opencode-ai/core/util/path"
|
||||||
import { useSDK } from "./sdk"
|
import { useSDK } from "./sdk"
|
||||||
import { useSync } from "./sync"
|
import { useSync } from "./sync"
|
||||||
import { useLanguage } from "@/context/language"
|
import { useLanguage } from "@/context/language"
|
||||||
|
|
|
||||||
|
|
@ -8,23 +8,31 @@ import type {
|
||||||
Todo,
|
Todo,
|
||||||
} from "@opencode-ai/sdk/v2/client"
|
} from "@opencode-ai/sdk/v2/client"
|
||||||
import { showToast } from "@opencode-ai/ui/toast"
|
import { showToast } from "@opencode-ai/ui/toast"
|
||||||
import { getFilename } from "@opencode-ai/shared/util/path"
|
import { getFilename } from "@opencode-ai/core/util/path"
|
||||||
import { batch, createContext, getOwner, onCleanup, onMount, type ParentProps, untrack, useContext } from "solid-js"
|
import { batch, createContext, getOwner, onCleanup, onMount, type ParentProps, untrack, useContext } from "solid-js"
|
||||||
import { createStore, produce, reconcile } from "solid-js/store"
|
import { createStore, produce, reconcile } from "solid-js/store"
|
||||||
import { useLanguage } from "@/context/language"
|
import { useLanguage } from "@/context/language"
|
||||||
import type { InitError } from "../pages/error"
|
import type { InitError } from "../pages/error"
|
||||||
import { useGlobalSDK } from "./global-sdk"
|
import { useGlobalSDK } from "./global-sdk"
|
||||||
import { bootstrapDirectory, bootstrapGlobal, clearProviderRev } from "./global-sync/bootstrap"
|
import {
|
||||||
|
bootstrapDirectory,
|
||||||
|
bootstrapGlobal,
|
||||||
|
clearProviderRev,
|
||||||
|
loadGlobalConfigQuery,
|
||||||
|
loadPathQuery,
|
||||||
|
loadProvidersQuery,
|
||||||
|
} from "./global-sync/bootstrap"
|
||||||
import { createChildStoreManager } from "./global-sync/child-store"
|
import { createChildStoreManager } from "./global-sync/child-store"
|
||||||
import { applyDirectoryEvent, applyGlobalEvent, cleanupDroppedSessionCaches } from "./global-sync/event-reducer"
|
import { applyDirectoryEvent, applyGlobalEvent, cleanupDroppedSessionCaches } from "./global-sync/event-reducer"
|
||||||
import { createRefreshQueue } from "./global-sync/queue"
|
|
||||||
import { clearSessionPrefetchDirectory } from "./global-sync/session-prefetch"
|
import { clearSessionPrefetchDirectory } from "./global-sync/session-prefetch"
|
||||||
import { estimateRootSessionTotal, loadRootSessionsWithFallback } from "./global-sync/session-load"
|
import { estimateRootSessionTotal, loadRootSessionsWithFallback } from "./global-sync/session-load"
|
||||||
import { trimSessions } from "./global-sync/session-trim"
|
import { trimSessions } from "./global-sync/session-trim"
|
||||||
import type { ProjectMeta } from "./global-sync/types"
|
import type { ProjectMeta } from "./global-sync/types"
|
||||||
import { SESSION_RECENT_LIMIT } from "./global-sync/types"
|
import { SESSION_RECENT_LIMIT } from "./global-sync/types"
|
||||||
import { formatServerError } from "@/utils/server-errors"
|
import { formatServerError } from "@/utils/server-errors"
|
||||||
import { queryOptions, skipToken, useQueryClient } from "@tanstack/solid-query"
|
import { queryOptions, useMutation, useQueries, useQuery, useQueryClient } from "@tanstack/solid-query"
|
||||||
|
import { createRefreshQueue } from "./global-sync/queue"
|
||||||
|
import { directoryKey } from "./global-sync/utils"
|
||||||
|
|
||||||
type GlobalStore = {
|
type GlobalStore = {
|
||||||
ready: boolean
|
ready: boolean
|
||||||
|
|
@ -40,8 +48,23 @@ type GlobalStore = {
|
||||||
reload: undefined | "pending" | "complete"
|
reload: undefined | "pending" | "complete"
|
||||||
}
|
}
|
||||||
|
|
||||||
export const loadSessionsQuery = (directory: string) =>
|
export const loadSessionsQueryKey = (directory: string) => [directory, "loadSessions"] as const
|
||||||
queryOptions<null>({ queryKey: [directory, "loadSessions"], queryFn: skipToken })
|
|
||||||
|
export const mcpQueryKey = (directory: string) => [directory, "mcp"] as const
|
||||||
|
|
||||||
|
export const loadMcpQuery = (directory: string, sdk: OpencodeClient) =>
|
||||||
|
queryOptions({
|
||||||
|
queryKey: mcpQueryKey(directory),
|
||||||
|
queryFn: () => sdk.mcp.status().then((r) => r.data ?? {}),
|
||||||
|
})
|
||||||
|
|
||||||
|
export const lspQueryKey = (directory: string) => [directory, "lsp"] as const
|
||||||
|
|
||||||
|
export const loadLspQuery = (directory: string, sdk: OpencodeClient) =>
|
||||||
|
queryOptions({
|
||||||
|
queryKey: lspQueryKey(directory),
|
||||||
|
queryFn: () => sdk.lsp.status().then((r) => r.data ?? []),
|
||||||
|
})
|
||||||
|
|
||||||
function createGlobalSync() {
|
function createGlobalSync() {
|
||||||
const globalSDK = useGlobalSDK()
|
const globalSDK = useGlobalSDK()
|
||||||
|
|
@ -54,15 +77,38 @@ function createGlobalSync() {
|
||||||
const sessionLoads = new Map<string, Promise<void>>()
|
const sessionLoads = new Map<string, Promise<void>>()
|
||||||
const sessionMeta = new Map<string, { limit: number }>()
|
const sessionMeta = new Map<string, { limit: number }>()
|
||||||
|
|
||||||
|
const [configQuery, providerQuery, pathQuery] = useQueries(() => ({
|
||||||
|
queries: [
|
||||||
|
loadGlobalConfigQuery(globalSDK.client),
|
||||||
|
loadProvidersQuery(null, globalSDK.client),
|
||||||
|
loadPathQuery(null, globalSDK.client),
|
||||||
|
],
|
||||||
|
}))
|
||||||
|
|
||||||
const [globalStore, setGlobalStore] = createStore<GlobalStore>({
|
const [globalStore, setGlobalStore] = createStore<GlobalStore>({
|
||||||
ready: false,
|
get ready() {
|
||||||
path: { state: "", config: "", worktree: "", directory: "", home: "" },
|
return bootstrap.isPending
|
||||||
|
},
|
||||||
project: [],
|
project: [],
|
||||||
session_todo: {},
|
session_todo: {},
|
||||||
provider: { all: [], connected: [], default: {} },
|
|
||||||
provider_auth: {},
|
provider_auth: {},
|
||||||
config: {},
|
get path() {
|
||||||
reload: undefined,
|
const EMPTY = { state: "", config: "", worktree: "", directory: "", home: "" }
|
||||||
|
if (pathQuery.isLoading) return EMPTY
|
||||||
|
return pathQuery.data ?? EMPTY
|
||||||
|
},
|
||||||
|
get provider() {
|
||||||
|
const EMPTY = { all: [], connected: [], default: {} }
|
||||||
|
if (providerQuery.isLoading) return EMPTY
|
||||||
|
return providerQuery.data ?? EMPTY
|
||||||
|
},
|
||||||
|
get config() {
|
||||||
|
if (configQuery.isLoading) return {}
|
||||||
|
return configQuery.data ?? {}
|
||||||
|
},
|
||||||
|
get reload() {
|
||||||
|
return updateConfigMutation.isPending ? "pending" : undefined
|
||||||
|
},
|
||||||
})
|
})
|
||||||
const queryClient = useQueryClient()
|
const queryClient = useQueryClient()
|
||||||
|
|
||||||
|
|
@ -88,6 +134,22 @@ function createGlobalSync() {
|
||||||
return (setGlobalStore as (...args: unknown[]) => unknown)(...input)
|
return (setGlobalStore as (...args: unknown[]) => unknown)(...input)
|
||||||
}) as typeof setGlobalStore
|
}) as typeof setGlobalStore
|
||||||
|
|
||||||
|
const bootstrap = useQuery(() => ({
|
||||||
|
queryKey: ["bootstrap"],
|
||||||
|
queryFn: async () => {
|
||||||
|
await bootstrapGlobal({
|
||||||
|
globalSDK: globalSDK.client,
|
||||||
|
requestFailedTitle: language.t("common.requestFailed"),
|
||||||
|
translate: language.t,
|
||||||
|
formatMoreCount: (count) => language.t("common.moreCountSuffix", { count }),
|
||||||
|
setGlobalStore: setBootStore,
|
||||||
|
queryClient,
|
||||||
|
})
|
||||||
|
bootedAt = Date.now()
|
||||||
|
return bootedAt
|
||||||
|
},
|
||||||
|
}))
|
||||||
|
|
||||||
const set = ((...input: unknown[]) => {
|
const set = ((...input: unknown[]) => {
|
||||||
if (input[0] === "project" && (Array.isArray(input[1]) || typeof input[1] === "function")) {
|
if (input[0] === "project" && (Array.isArray(input[1]) || typeof input[1] === "function")) {
|
||||||
setProjects(input[1] as Project[] | ((draft: Project[]) => Project[]))
|
setProjects(input[1] as Project[] | ((draft: Project[]) => Project[]))
|
||||||
|
|
@ -114,10 +176,23 @@ function createGlobalSync() {
|
||||||
|
|
||||||
const queue = createRefreshQueue({
|
const queue = createRefreshQueue({
|
||||||
paused,
|
paused,
|
||||||
bootstrap,
|
key: directoryKey,
|
||||||
|
bootstrap: () => queryClient.fetchQuery({ queryKey: ["bootstrap"] }),
|
||||||
bootstrapInstance,
|
bootstrapInstance,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const sdkFor = (directory: string) => {
|
||||||
|
const key = directoryKey(directory)
|
||||||
|
const cached = sdkCache.get(key)
|
||||||
|
if (cached) return cached
|
||||||
|
const sdk = globalSDK.createClient({
|
||||||
|
directory,
|
||||||
|
throwOnError: true,
|
||||||
|
})
|
||||||
|
sdkCache.set(key, sdk)
|
||||||
|
return sdk
|
||||||
|
}
|
||||||
|
|
||||||
const children = createChildStoreManager({
|
const children = createChildStoreManager({
|
||||||
owner,
|
owner,
|
||||||
isBooting: (directory) => booting.has(directory),
|
isBooting: (directory) => booting.has(directory),
|
||||||
|
|
@ -126,33 +201,28 @@ function createGlobalSync() {
|
||||||
void bootstrapInstance(directory)
|
void bootstrapInstance(directory)
|
||||||
},
|
},
|
||||||
onDispose: (directory) => {
|
onDispose: (directory) => {
|
||||||
queue.clear(directory)
|
const key = directoryKey(directory)
|
||||||
sessionMeta.delete(directory)
|
queue.clear(key)
|
||||||
sdkCache.delete(directory)
|
sessionMeta.delete(key)
|
||||||
clearProviderRev(directory)
|
sdkCache.delete(key)
|
||||||
clearSessionPrefetchDirectory(directory)
|
clearProviderRev(key)
|
||||||
|
clearSessionPrefetchDirectory(key)
|
||||||
},
|
},
|
||||||
translate: language.t,
|
translate: language.t,
|
||||||
|
getSdk: sdkFor,
|
||||||
|
global: {
|
||||||
|
provider: globalStore.provider,
|
||||||
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
const sdkFor = (directory: string) => {
|
|
||||||
const cached = sdkCache.get(directory)
|
|
||||||
if (cached) return cached
|
|
||||||
const sdk = globalSDK.createClient({
|
|
||||||
directory,
|
|
||||||
throwOnError: true,
|
|
||||||
})
|
|
||||||
sdkCache.set(directory, sdk)
|
|
||||||
return sdk
|
|
||||||
}
|
|
||||||
|
|
||||||
async function loadSessions(directory: string) {
|
async function loadSessions(directory: string) {
|
||||||
const pending = sessionLoads.get(directory)
|
const key = directoryKey(directory)
|
||||||
|
const pending = sessionLoads.get(key)
|
||||||
if (pending) return pending
|
if (pending) return pending
|
||||||
|
|
||||||
children.pin(directory)
|
children.pin(key)
|
||||||
const [store, setStore] = children.child(directory, { bootstrap: false })
|
const [store, setStore] = children.child(directory, { bootstrap: false })
|
||||||
const meta = sessionMeta.get(directory)
|
const meta = sessionMeta.get(key)
|
||||||
if (meta && meta.limit >= store.limit) {
|
if (meta && meta.limit >= store.limit) {
|
||||||
const next = trimSessions(store.session, {
|
const next = trimSessions(store.session, {
|
||||||
limit: store.limit,
|
limit: store.limit,
|
||||||
|
|
@ -162,14 +232,14 @@ function createGlobalSync() {
|
||||||
setStore("session", reconcile(next, { key: "id" }))
|
setStore("session", reconcile(next, { key: "id" }))
|
||||||
cleanupDroppedSessionCaches(store, setStore, next, setSessionTodo)
|
cleanupDroppedSessionCaches(store, setStore, next, setSessionTodo)
|
||||||
}
|
}
|
||||||
children.unpin(directory)
|
children.unpin(key)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
const limit = Math.max(store.limit + SESSION_RECENT_LIMIT, SESSION_RECENT_LIMIT)
|
const limit = Math.max(store.limit + SESSION_RECENT_LIMIT, SESSION_RECENT_LIMIT)
|
||||||
const promise = queryClient
|
const promise = queryClient
|
||||||
.fetchQuery({
|
.fetchQuery({
|
||||||
...loadSessionsQuery(directory),
|
queryKey: loadSessionsQueryKey(key),
|
||||||
queryFn: () =>
|
queryFn: () =>
|
||||||
loadRootSessionsWithFallback({
|
loadRootSessionsWithFallback({
|
||||||
directory,
|
directory,
|
||||||
|
|
@ -199,7 +269,7 @@ function createGlobalSync() {
|
||||||
setStore("session", reconcile(sessions, { key: "id" }))
|
setStore("session", reconcile(sessions, { key: "id" }))
|
||||||
cleanupDroppedSessionCaches(store, setStore, sessions, setSessionTodo)
|
cleanupDroppedSessionCaches(store, setStore, sessions, setSessionTodo)
|
||||||
})
|
})
|
||||||
sessionMeta.set(directory, { limit })
|
sessionMeta.set(key, { limit })
|
||||||
})
|
})
|
||||||
.catch((err) => {
|
.catch((err) => {
|
||||||
console.error("Failed to load sessions", err)
|
console.error("Failed to load sessions", err)
|
||||||
|
|
@ -214,23 +284,24 @@ function createGlobalSync() {
|
||||||
})
|
})
|
||||||
.then(() => {})
|
.then(() => {})
|
||||||
|
|
||||||
sessionLoads.set(directory, promise)
|
sessionLoads.set(key, promise)
|
||||||
void promise.finally(() => {
|
void promise.finally(() => {
|
||||||
sessionLoads.delete(directory)
|
sessionLoads.delete(key)
|
||||||
children.unpin(directory)
|
children.unpin(key)
|
||||||
})
|
})
|
||||||
return promise
|
return promise
|
||||||
}
|
}
|
||||||
|
|
||||||
async function bootstrapInstance(directory: string) {
|
async function bootstrapInstance(directory: string) {
|
||||||
if (!directory) return
|
const key = directoryKey(directory)
|
||||||
const pending = booting.get(directory)
|
if (!key) return
|
||||||
|
const pending = booting.get(key)
|
||||||
if (pending) return pending
|
if (pending) return pending
|
||||||
|
|
||||||
children.pin(directory)
|
children.pin(key)
|
||||||
const promise = Promise.resolve().then(async () => {
|
const promise = Promise.resolve().then(async () => {
|
||||||
const child = children.ensureChild(directory)
|
const child = children.ensureChild(directory)
|
||||||
const cache = children.vcsCache.get(directory)
|
const cache = children.vcsCache.get(key)
|
||||||
if (!cache) return
|
if (!cache) return
|
||||||
const sdk = sdkFor(directory)
|
const sdk = sdkFor(directory)
|
||||||
await bootstrapDirectory({
|
await bootstrapDirectory({
|
||||||
|
|
@ -251,39 +322,27 @@ function createGlobalSync() {
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
booting.set(directory, promise)
|
booting.set(key, promise)
|
||||||
void promise.finally(() => {
|
void promise.finally(() => {
|
||||||
booting.delete(directory)
|
booting.delete(key)
|
||||||
children.unpin(directory)
|
children.unpin(key)
|
||||||
})
|
})
|
||||||
return promise
|
return promise
|
||||||
}
|
}
|
||||||
|
|
||||||
const unsub = globalSDK.event.listen((e) => {
|
const unsub = globalSDK.event.listen((e) => {
|
||||||
const directory = e.name
|
const directory = e.name
|
||||||
|
const key = directoryKey(directory)
|
||||||
const event = e.details
|
const event = e.details
|
||||||
const recent = bootingRoot || Date.now() - bootedAt < 1500
|
const recent = bootingRoot || Date.now() - bootedAt < 1500
|
||||||
|
|
||||||
if (event.type === "session.error") {
|
|
||||||
const error = event.properties.error
|
|
||||||
if (error?.name !== "MessageAbortedError") {
|
|
||||||
console.error("[global-sync] session error", {
|
|
||||||
scope: directory === "global" ? "global" : "workspace",
|
|
||||||
directory: directory === "global" ? undefined : directory,
|
|
||||||
project: directory === "global" ? undefined : getFilename(directory),
|
|
||||||
sessionID: event.properties.sessionID,
|
|
||||||
error,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (directory === "global") {
|
if (directory === "global") {
|
||||||
applyGlobalEvent({
|
applyGlobalEvent({
|
||||||
event,
|
event,
|
||||||
project: globalStore.project,
|
project: globalStore.project,
|
||||||
refresh: () => {
|
refresh: () => {
|
||||||
if (recent) return
|
if (recent) return
|
||||||
queue.refresh()
|
bootstrap.refetch()
|
||||||
},
|
},
|
||||||
setGlobalProject: setProjects,
|
setGlobalProject: setProjects,
|
||||||
})
|
})
|
||||||
|
|
@ -296,9 +355,9 @@ function createGlobalSync() {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
const existing = children.children[directory]
|
const existing = children.children[key]
|
||||||
if (!existing) return
|
if (!existing) return
|
||||||
children.mark(directory)
|
children.mark(key)
|
||||||
const [store, setStore] = existing
|
const [store, setStore] = existing
|
||||||
applyDirectoryEvent({
|
applyDirectoryEvent({
|
||||||
event,
|
event,
|
||||||
|
|
@ -307,14 +366,9 @@ function createGlobalSync() {
|
||||||
setStore,
|
setStore,
|
||||||
push: queue.push,
|
push: queue.push,
|
||||||
setSessionTodo,
|
setSessionTodo,
|
||||||
vcsCache: children.vcsCache.get(directory),
|
vcsCache: children.vcsCache.get(key),
|
||||||
loadLsp: () => {
|
loadLsp: () => {
|
||||||
void sdkFor(directory)
|
void queryClient.fetchQuery(loadLspQuery(key, sdkFor(directory)))
|
||||||
.lsp.status()
|
|
||||||
.then((x) => {
|
|
||||||
setStore("lsp", x.data ?? [])
|
|
||||||
setStore("lsp_ready", true)
|
|
||||||
})
|
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
@ -325,27 +379,10 @@ function createGlobalSync() {
|
||||||
})
|
})
|
||||||
onCleanup(() => {
|
onCleanup(() => {
|
||||||
for (const directory of Object.keys(children.children)) {
|
for (const directory of Object.keys(children.children)) {
|
||||||
children.disposeDirectory(directory)
|
children.disposeDirectory(directoryKey(directory))
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
async function bootstrap() {
|
|
||||||
bootingRoot = true
|
|
||||||
try {
|
|
||||||
await bootstrapGlobal({
|
|
||||||
globalSDK: globalSDK.client,
|
|
||||||
requestFailedTitle: language.t("common.requestFailed"),
|
|
||||||
translate: language.t,
|
|
||||||
formatMoreCount: (count) => language.t("common.moreCountSuffix", { count }),
|
|
||||||
setGlobalStore: setBootStore,
|
|
||||||
queryClient,
|
|
||||||
})
|
|
||||||
bootedAt = Date.now()
|
|
||||||
} finally {
|
|
||||||
bootingRoot = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
onMount(() => {
|
onMount(() => {
|
||||||
if (typeof requestAnimationFrame === "function") {
|
if (typeof requestAnimationFrame === "function") {
|
||||||
eventFrame = requestAnimationFrame(() => {
|
eventFrame = requestAnimationFrame(() => {
|
||||||
|
|
@ -361,7 +398,6 @@ function createGlobalSync() {
|
||||||
void globalSDK.event.start()
|
void globalSDK.event.start()
|
||||||
}, 0)
|
}, 0)
|
||||||
}
|
}
|
||||||
void bootstrap()
|
|
||||||
})
|
})
|
||||||
|
|
||||||
const projectApi = {
|
const projectApi = {
|
||||||
|
|
@ -374,21 +410,10 @@ function createGlobalSync() {
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
const updateConfig = async (config: Config) => {
|
const updateConfigMutation = useMutation(() => ({
|
||||||
setGlobalStore("reload", "pending")
|
mutationFn: (config: Config) => globalSDK.client.global.config.update({ config }),
|
||||||
return globalSDK.client.global.config
|
onSuccess: () => bootstrap.refetch(),
|
||||||
.update({ config })
|
}))
|
||||||
.then(bootstrap)
|
|
||||||
.then(() => {
|
|
||||||
queue.refresh()
|
|
||||||
setGlobalStore("reload", undefined)
|
|
||||||
queue.refresh()
|
|
||||||
})
|
|
||||||
.catch((error) => {
|
|
||||||
setGlobalStore("reload", undefined)
|
|
||||||
throw error
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
data: globalStore,
|
data: globalStore,
|
||||||
|
|
@ -401,8 +426,8 @@ function createGlobalSync() {
|
||||||
},
|
},
|
||||||
child: children.child,
|
child: children.child,
|
||||||
peek: children.peek,
|
peek: children.peek,
|
||||||
bootstrap,
|
// bootstrap,
|
||||||
updateConfig,
|
updateConfig: updateConfigMutation.mutateAsync,
|
||||||
project: projectApi,
|
project: projectApi,
|
||||||
todo: {
|
todo: {
|
||||||
set: setSessionTodo,
|
set: setSessionTodo,
|
||||||
|
|
|
||||||
|
|
@ -11,14 +11,15 @@ import type {
|
||||||
Todo,
|
Todo,
|
||||||
} from "@opencode-ai/sdk/v2/client"
|
} from "@opencode-ai/sdk/v2/client"
|
||||||
import { showToast } from "@opencode-ai/ui/toast"
|
import { showToast } from "@opencode-ai/ui/toast"
|
||||||
import { getFilename } from "@opencode-ai/shared/util/path"
|
import { getFilename } from "@opencode-ai/core/util/path"
|
||||||
import { retry } from "@opencode-ai/shared/util/retry"
|
import { retry } from "@opencode-ai/core/util/retry"
|
||||||
import { batch } from "solid-js"
|
import { batch } from "solid-js"
|
||||||
import { reconcile, type SetStoreFunction, type Store } from "solid-js/store"
|
import { reconcile, type SetStoreFunction, type Store } from "solid-js/store"
|
||||||
import type { State, VcsCache } from "./types"
|
import type { State, VcsCache } from "./types"
|
||||||
import { cmp, normalizeAgentList, normalizeProviderList } from "./utils"
|
import { cmp, normalizeAgentList, normalizeProviderList } from "./utils"
|
||||||
import { formatServerError } from "@/utils/server-errors"
|
import { formatServerError } from "@/utils/server-errors"
|
||||||
import { QueryClient, queryOptions, skipToken } from "@tanstack/solid-query"
|
import { QueryClient, queryOptions } from "@tanstack/solid-query"
|
||||||
|
import { loadMcpQuery } from "../global-sync"
|
||||||
|
|
||||||
type GlobalStore = {
|
type GlobalStore = {
|
||||||
ready: boolean
|
ready: boolean
|
||||||
|
|
@ -66,6 +67,43 @@ function runAll(list: Array<() => Promise<unknown>>) {
|
||||||
return Promise.allSettled(list.map((item) => item()))
|
return Promise.allSettled(list.map((item) => item()))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function showErrors(input: {
|
||||||
|
errors: unknown[]
|
||||||
|
title: string
|
||||||
|
translate: (key: string, vars?: Record<string, string | number>) => string
|
||||||
|
formatMoreCount: (count: number) => string
|
||||||
|
}) {
|
||||||
|
if (input.errors.length === 0) return
|
||||||
|
const message = formatServerError(input.errors[0], input.translate)
|
||||||
|
const more = input.errors.length > 1 ? input.formatMoreCount(input.errors.length - 1) : ""
|
||||||
|
showToast({
|
||||||
|
variant: "error",
|
||||||
|
title: input.title,
|
||||||
|
description: message + more,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export const loadGlobalConfigQuery = (sdk: OpencodeClient) =>
|
||||||
|
queryOptions({
|
||||||
|
queryKey: ["config"],
|
||||||
|
queryFn: () => retry(() => sdk.global.config.get().then((x) => x.data!)),
|
||||||
|
})
|
||||||
|
|
||||||
|
export const loadProjectsQuery = (sdk: OpencodeClient) =>
|
||||||
|
queryOptions({
|
||||||
|
queryKey: ["project"],
|
||||||
|
queryFn: () =>
|
||||||
|
retry(() =>
|
||||||
|
sdk.project.list().then((x) => {
|
||||||
|
return (x.data ?? [])
|
||||||
|
.filter((p) => !!p?.id)
|
||||||
|
.filter((p) => !!p.worktree && !p.worktree.includes("opencode-test"))
|
||||||
|
.slice()
|
||||||
|
.sort((a, b) => cmp(a.id, b.id))
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
})
|
||||||
|
|
||||||
export async function bootstrapGlobal(input: {
|
export async function bootstrapGlobal(input: {
|
||||||
globalSDK: OpencodeClient
|
globalSDK: OpencodeClient
|
||||||
requestFailedTitle: string
|
requestFailedTitle: string
|
||||||
|
|
@ -74,53 +112,15 @@ export async function bootstrapGlobal(input: {
|
||||||
setGlobalStore: SetStoreFunction<GlobalStore>
|
setGlobalStore: SetStoreFunction<GlobalStore>
|
||||||
queryClient: QueryClient
|
queryClient: QueryClient
|
||||||
}) {
|
}) {
|
||||||
const fast = [
|
|
||||||
() =>
|
|
||||||
retry(() =>
|
|
||||||
input.globalSDK.global.config.get().then((x) => {
|
|
||||||
input.setGlobalStore("config", x.data!)
|
|
||||||
}),
|
|
||||||
),
|
|
||||||
]
|
|
||||||
|
|
||||||
const slow = [
|
const slow = [
|
||||||
|
() => input.queryClient.fetchQuery(loadGlobalConfigQuery(input.globalSDK)),
|
||||||
|
() => input.queryClient.fetchQuery(loadProvidersQuery(null, input.globalSDK)),
|
||||||
|
() => input.queryClient.fetchQuery(loadPathQuery(null, input.globalSDK)),
|
||||||
() =>
|
() =>
|
||||||
input.queryClient.fetchQuery({
|
input.queryClient
|
||||||
...loadProvidersQuery(null),
|
.fetchQuery(loadProjectsQuery(input.globalSDK))
|
||||||
queryFn: () =>
|
.then((data) => input.setGlobalStore("project", data)),
|
||||||
retry(() =>
|
|
||||||
input.globalSDK.provider.list().then((x) => {
|
|
||||||
input.setGlobalStore("provider", normalizeProviderList(x.data!))
|
|
||||||
return null
|
|
||||||
}),
|
|
||||||
),
|
|
||||||
}),
|
|
||||||
() =>
|
|
||||||
retry(() =>
|
|
||||||
input.globalSDK.path.get().then((x) => {
|
|
||||||
input.setGlobalStore("path", x.data!)
|
|
||||||
}),
|
|
||||||
),
|
|
||||||
() =>
|
|
||||||
retry(() =>
|
|
||||||
input.globalSDK.project.list().then((x) => {
|
|
||||||
const projects = (x.data ?? [])
|
|
||||||
.filter((p) => !!p?.id)
|
|
||||||
.filter((p) => !!p.worktree && !p.worktree.includes("opencode-test"))
|
|
||||||
.slice()
|
|
||||||
.sort((a, b) => cmp(a.id, b.id))
|
|
||||||
input.setGlobalStore("project", projects)
|
|
||||||
}),
|
|
||||||
),
|
|
||||||
]
|
]
|
||||||
await runAll(fast)
|
|
||||||
// showErrors({
|
|
||||||
// errors: errors(await runAll(fast)),
|
|
||||||
// title: input.requestFailedTitle,
|
|
||||||
// translate: input.translate,
|
|
||||||
// formatMoreCount: input.formatMoreCount,
|
|
||||||
// })
|
|
||||||
await waitForPaint()
|
|
||||||
await runAll(slow)
|
await runAll(slow)
|
||||||
// showErrors({
|
// showErrors({
|
||||||
// errors: errors(),
|
// errors: errors(),
|
||||||
|
|
@ -128,7 +128,6 @@ export async function bootstrapGlobal(input: {
|
||||||
// translate: input.translate,
|
// translate: input.translate,
|
||||||
// formatMoreCount: input.formatMoreCount,
|
// formatMoreCount: input.formatMoreCount,
|
||||||
// })
|
// })
|
||||||
input.setGlobalStore("ready", true)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function groupBySession<T extends { id: string; sessionID: string }>(input: T[]) {
|
function groupBySession<T extends { id: string; sessionID: string }>(input: T[]) {
|
||||||
|
|
@ -179,45 +178,22 @@ function warmSessions(input: {
|
||||||
).then(() => undefined)
|
).then(() => undefined)
|
||||||
}
|
}
|
||||||
|
|
||||||
export const loadProvidersQuery = (directory: string | null) =>
|
export const loadProvidersQuery = (directory: string | null, sdk: OpencodeClient) =>
|
||||||
queryOptions<null>({ queryKey: [directory, "providers"], queryFn: skipToken })
|
queryOptions({
|
||||||
|
queryKey: [directory, "providers"],
|
||||||
export const loadAgentsQuery = (
|
queryFn: () => retry(() => sdk.provider.list().then((x) => normalizeProviderList(x.data!))),
|
||||||
directory: string | null,
|
|
||||||
sdk?: OpencodeClient,
|
|
||||||
transform?: (x: Awaited<ReturnType<OpencodeClient["app"]["agents"]>>) => void,
|
|
||||||
) =>
|
|
||||||
queryOptions<null>({
|
|
||||||
queryKey: [directory, "agents"],
|
|
||||||
queryFn:
|
|
||||||
sdk && transform
|
|
||||||
? () =>
|
|
||||||
retry(() =>
|
|
||||||
sdk.app
|
|
||||||
.agents()
|
|
||||||
.then(transform)
|
|
||||||
.then(() => null),
|
|
||||||
)
|
|
||||||
: skipToken,
|
|
||||||
})
|
})
|
||||||
|
|
||||||
export const loadPathQuery = (
|
export const loadAgentsQuery = (directory: string | null, sdk: OpencodeClient) =>
|
||||||
directory: string | null,
|
queryOptions({
|
||||||
sdk?: OpencodeClient,
|
queryKey: [directory, "agents"],
|
||||||
transform?: (x: Awaited<ReturnType<OpencodeClient["path"]["get"]>>) => void,
|
queryFn: () => retry(() => sdk.app.agents().then((x) => normalizeAgentList(x.data))),
|
||||||
) =>
|
})
|
||||||
|
|
||||||
|
export const loadPathQuery = (directory: string | null, sdk: OpencodeClient) =>
|
||||||
queryOptions<Path>({
|
queryOptions<Path>({
|
||||||
queryKey: [directory, "path"],
|
queryKey: [directory, "path"],
|
||||||
queryFn:
|
queryFn: () => retry(() => sdk.path.get().then((x) => x.data!)),
|
||||||
sdk && transform
|
|
||||||
? () =>
|
|
||||||
retry(() =>
|
|
||||||
sdk.path.get().then(async (x) => {
|
|
||||||
transform(x)
|
|
||||||
return x.data!
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
: skipToken,
|
|
||||||
})
|
})
|
||||||
|
|
||||||
export async function bootstrapDirectory(input: {
|
export async function bootstrapDirectory(input: {
|
||||||
|
|
@ -241,19 +217,9 @@ export async function bootstrapDirectory(input: {
|
||||||
const seededPath = input.global.path.directory === input.directory ? input.global.path : undefined
|
const seededPath = input.global.path.directory === input.directory ? input.global.path : undefined
|
||||||
if (seededProject) input.setStore("project", seededProject)
|
if (seededProject) input.setStore("project", seededProject)
|
||||||
if (seededPath) input.setStore("path", seededPath)
|
if (seededPath) input.setStore("path", seededPath)
|
||||||
if (input.store.provider.all.length === 0 && input.global.provider.all.length > 0) {
|
|
||||||
input.setStore("provider", input.global.provider)
|
|
||||||
}
|
|
||||||
if (Object.keys(input.store.config).length === 0 && Object.keys(input.global.config).length > 0) {
|
if (Object.keys(input.store.config).length === 0 && Object.keys(input.global.config).length > 0) {
|
||||||
input.setStore("config", input.global.config)
|
input.setStore("config", reconcile(input.global.config, { merge: false }))
|
||||||
}
|
}
|
||||||
if (loading || input.store.provider.all.length === 0) {
|
|
||||||
input.setStore("provider_ready", false)
|
|
||||||
}
|
|
||||||
input.setStore("mcp_ready", false)
|
|
||||||
input.setStore("mcp", {})
|
|
||||||
input.setStore("lsp_ready", false)
|
|
||||||
input.setStore("lsp", [])
|
|
||||||
if (loading) input.setStore("status", "partial")
|
if (loading) input.setStore("status", "partial")
|
||||||
|
|
||||||
const rev = (providerRev.get(input.directory) ?? 0) + 1
|
const rev = (providerRev.get(input.directory) ?? 0) + 1
|
||||||
|
|
@ -262,21 +228,20 @@ export async function bootstrapDirectory(input: {
|
||||||
const slow = [
|
const slow = [
|
||||||
() => Promise.resolve(input.loadSessions(input.directory)),
|
() => Promise.resolve(input.loadSessions(input.directory)),
|
||||||
() =>
|
() =>
|
||||||
input.queryClient.ensureQueryData(
|
input.queryClient
|
||||||
loadAgentsQuery(input.directory, input.sdk, (x) => input.setStore("agent", normalizeAgentList(x.data))),
|
.ensureQueryData(loadAgentsQuery(input.directory, input.sdk))
|
||||||
),
|
.then((data) => input.setStore("agent", data)),
|
||||||
() => retry(() => input.sdk.config.get().then((x) => input.setStore("config", x.data!))),
|
() =>
|
||||||
|
retry(() => input.sdk.config.get().then((x) => input.setStore("config", reconcile(x.data!, { merge: false })))),
|
||||||
() => retry(() => input.sdk.session.status().then((x) => input.setStore("session_status", x.data!))),
|
() => retry(() => input.sdk.session.status().then((x) => input.setStore("session_status", x.data!))),
|
||||||
!seededProject &&
|
!seededProject &&
|
||||||
(() => retry(() => input.sdk.project.current()).then((x) => input.setStore("project", x.data!.id))),
|
(() => retry(() => input.sdk.project.current()).then((x) => input.setStore("project", x.data!.id))),
|
||||||
!seededPath &&
|
!seededPath &&
|
||||||
(() =>
|
(() =>
|
||||||
input.queryClient.ensureQueryData(
|
input.queryClient.ensureQueryData(loadPathQuery(input.directory, input.sdk)).then((data) => {
|
||||||
loadPathQuery(input.directory, input.sdk, (x) => {
|
const next = projectID(data.directory ?? input.directory, input.global.project)
|
||||||
const next = projectID(x.data?.directory ?? input.directory, input.global.project)
|
if (next) input.setStore("project", next)
|
||||||
if (next) input.setStore("project", next)
|
})),
|
||||||
}),
|
|
||||||
)),
|
|
||||||
() =>
|
() =>
|
||||||
retry(() =>
|
retry(() =>
|
||||||
input.sdk.vcs.get().then((x) => {
|
input.sdk.vcs.get().then((x) => {
|
||||||
|
|
@ -339,33 +304,15 @@ export async function bootstrapDirectory(input: {
|
||||||
}),
|
}),
|
||||||
),
|
),
|
||||||
() => Promise.resolve(input.loadSessions(input.directory)),
|
() => Promise.resolve(input.loadSessions(input.directory)),
|
||||||
|
() => input.queryClient.fetchQuery(loadMcpQuery(input.directory, input.sdk)),
|
||||||
() =>
|
() =>
|
||||||
retry(() =>
|
input.queryClient.fetchQuery(loadProvidersQuery(input.directory, input.sdk)).catch((err) => {
|
||||||
input.sdk.mcp.status().then((x) => {
|
const project = getFilename(input.directory)
|
||||||
input.setStore("mcp", x.data!)
|
showToast({
|
||||||
input.setStore("mcp_ready", true)
|
variant: "error",
|
||||||
}),
|
title: input.translate("toast.project.reloadFailed.title", { project }),
|
||||||
),
|
description: formatServerError(err, input.translate),
|
||||||
() =>
|
})
|
||||||
input.queryClient.ensureQueryData({
|
|
||||||
...loadProvidersQuery(input.directory),
|
|
||||||
queryFn: () =>
|
|
||||||
retry(() => input.sdk.provider.list())
|
|
||||||
.then((x) => {
|
|
||||||
if (providerRev.get(input.directory) !== rev) return
|
|
||||||
input.setStore("provider", normalizeProviderList(x.data!))
|
|
||||||
input.setStore("provider_ready", true)
|
|
||||||
})
|
|
||||||
.catch((err) => {
|
|
||||||
if (providerRev.get(input.directory) !== rev) console.error("Failed to refresh provider list", err)
|
|
||||||
const project = getFilename(input.directory)
|
|
||||||
showToast({
|
|
||||||
variant: "error",
|
|
||||||
title: input.translate("toast.project.reloadFailed.title", { project }),
|
|
||||||
description: formatServerError(err, input.translate),
|
|
||||||
})
|
|
||||||
})
|
|
||||||
.then(() => null),
|
|
||||||
}),
|
}),
|
||||||
].filter(Boolean) as (() => Promise<any>)[]
|
].filter(Boolean) as (() => Promise<any>)[]
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -22,6 +22,8 @@ describe("createChildStoreManager", () => {
|
||||||
onBootstrap() {},
|
onBootstrap() {},
|
||||||
onDispose() {},
|
onDispose() {},
|
||||||
translate: (key) => key,
|
translate: (key) => key,
|
||||||
|
getSdk: () => null!,
|
||||||
|
global: { provider: null! },
|
||||||
})
|
})
|
||||||
|
|
||||||
Array.from({ length: 30 }, (_, index) => `/pinned-${index}`).forEach((directory) => {
|
Array.from({ length: 30 }, (_, index) => `/pinned-${index}`).forEach((directory) => {
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
import { createRoot, getOwner, onCleanup, runWithOwner, type Owner } from "solid-js"
|
import { createRoot, getOwner, onCleanup, runWithOwner, type Owner } from "solid-js"
|
||||||
import { createStore, type SetStoreFunction, type Store } from "solid-js/store"
|
import { createStore, type SetStoreFunction, type Store } from "solid-js/store"
|
||||||
import { Persist, persisted } from "@/utils/persist"
|
import { Persist, persisted } from "@/utils/persist"
|
||||||
import type { VcsInfo } from "@opencode-ai/sdk/v2/client"
|
import type { OpencodeClient, ProviderListResponse, VcsInfo } from "@opencode-ai/sdk/v2/client"
|
||||||
import {
|
import {
|
||||||
DIR_IDLE_TTL_MS,
|
DIR_IDLE_TTL_MS,
|
||||||
MAX_DIR_STORES,
|
MAX_DIR_STORES,
|
||||||
|
|
@ -14,8 +14,10 @@ import {
|
||||||
type VcsCache,
|
type VcsCache,
|
||||||
} from "./types"
|
} from "./types"
|
||||||
import { canDisposeDirectory, pickDirectoriesToEvict } from "./eviction"
|
import { canDisposeDirectory, pickDirectoriesToEvict } from "./eviction"
|
||||||
import { useQuery } from "@tanstack/solid-query"
|
import { useQueries } from "@tanstack/solid-query"
|
||||||
import { loadPathQuery } from "./bootstrap"
|
import { loadPathQuery, loadProvidersQuery } from "./bootstrap"
|
||||||
|
import { loadLspQuery, loadMcpQuery } from "../global-sync"
|
||||||
|
import { directoryKey, type DirectoryKey } from "./utils"
|
||||||
|
|
||||||
export function createChildStoreManager(input: {
|
export function createChildStoreManager(input: {
|
||||||
owner: Owner
|
owner: Owner
|
||||||
|
|
@ -24,6 +26,10 @@ export function createChildStoreManager(input: {
|
||||||
onBootstrap: (directory: string) => void
|
onBootstrap: (directory: string) => void
|
||||||
onDispose: (directory: string) => void
|
onDispose: (directory: string) => void
|
||||||
translate: (key: string, vars?: Record<string, string | number>) => string
|
translate: (key: string, vars?: Record<string, string | number>) => string
|
||||||
|
getSdk: (directory: string) => OpencodeClient
|
||||||
|
global: {
|
||||||
|
provider: ProviderListResponse
|
||||||
|
}
|
||||||
}) {
|
}) {
|
||||||
const children: Record<string, [Store<State>, SetStoreFunction<State>]> = {}
|
const children: Record<string, [Store<State>, SetStoreFunction<State>]> = {}
|
||||||
const vcsCache = new Map<string, VcsCache>()
|
const vcsCache = new Map<string, VcsCache>()
|
||||||
|
|
@ -34,30 +40,37 @@ export function createChildStoreManager(input: {
|
||||||
const ownerPins = new WeakMap<object, Set<string>>()
|
const ownerPins = new WeakMap<object, Set<string>>()
|
||||||
const disposers = new Map<string, () => void>()
|
const disposers = new Map<string, () => void>()
|
||||||
|
|
||||||
|
const markKey = (key: DirectoryKey) => {
|
||||||
|
if (!key) return
|
||||||
|
lifecycle.set(key, { lastAccessAt: Date.now() })
|
||||||
|
runEviction(key)
|
||||||
|
}
|
||||||
|
|
||||||
const mark = (directory: string) => {
|
const mark = (directory: string) => {
|
||||||
if (!directory) return
|
const key = directoryKey(directory)
|
||||||
lifecycle.set(directory, { lastAccessAt: Date.now() })
|
markKey(key)
|
||||||
runEviction(directory)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const pin = (directory: string) => {
|
const pin = (directory: string) => {
|
||||||
if (!directory) return
|
const key = directoryKey(directory)
|
||||||
pins.set(directory, (pins.get(directory) ?? 0) + 1)
|
if (!key) return
|
||||||
mark(directory)
|
pins.set(key, (pins.get(key) ?? 0) + 1)
|
||||||
|
markKey(key)
|
||||||
}
|
}
|
||||||
|
|
||||||
const unpin = (directory: string) => {
|
const unpin = (directory: string) => {
|
||||||
if (!directory) return
|
const key = directoryKey(directory)
|
||||||
const next = (pins.get(directory) ?? 0) - 1
|
if (!key) return
|
||||||
|
const next = (pins.get(key) ?? 0) - 1
|
||||||
if (next > 0) {
|
if (next > 0) {
|
||||||
pins.set(directory, next)
|
pins.set(key, next)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
pins.delete(directory)
|
pins.delete(key)
|
||||||
runEviction()
|
runEviction()
|
||||||
}
|
}
|
||||||
|
|
||||||
const pinned = (directory: string) => (pins.get(directory) ?? 0) > 0
|
const pinned = (directory: string) => (pins.get(directoryKey(directory)) ?? 0) > 0
|
||||||
|
|
||||||
const pinForOwner = (directory: string) => {
|
const pinForOwner = (directory: string) => {
|
||||||
const current = getOwner()
|
const current = getOwner()
|
||||||
|
|
@ -79,30 +92,31 @@ export function createChildStoreManager(input: {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
function disposeDirectory(directory: string) {
|
function disposeDirectory(directory: DirectoryKey) {
|
||||||
|
const key = directory
|
||||||
if (
|
if (
|
||||||
!canDisposeDirectory({
|
!canDisposeDirectory({
|
||||||
directory,
|
directory: key,
|
||||||
hasStore: !!children[directory],
|
hasStore: !!children[key],
|
||||||
pinned: pinned(directory),
|
pinned: pinned(key),
|
||||||
booting: input.isBooting(directory),
|
booting: input.isBooting(key),
|
||||||
loadingSessions: input.isLoadingSessions(directory),
|
loadingSessions: input.isLoadingSessions(key),
|
||||||
})
|
})
|
||||||
) {
|
) {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
vcsCache.delete(directory)
|
vcsCache.delete(key)
|
||||||
metaCache.delete(directory)
|
metaCache.delete(key)
|
||||||
iconCache.delete(directory)
|
iconCache.delete(key)
|
||||||
lifecycle.delete(directory)
|
lifecycle.delete(key)
|
||||||
const dispose = disposers.get(directory)
|
const dispose = disposers.get(key)
|
||||||
if (dispose) {
|
if (dispose) {
|
||||||
dispose()
|
dispose()
|
||||||
disposers.delete(directory)
|
disposers.delete(key)
|
||||||
}
|
}
|
||||||
delete children[directory]
|
delete children[key]
|
||||||
input.onDispose(directory)
|
input.onDispose(key)
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -119,13 +133,14 @@ export function createChildStoreManager(input: {
|
||||||
}).filter((directory) => directory !== skip)
|
}).filter((directory) => directory !== skip)
|
||||||
if (list.length === 0) return
|
if (list.length === 0) return
|
||||||
for (const directory of list) {
|
for (const directory of list) {
|
||||||
if (!disposeDirectory(directory)) continue
|
if (!disposeDirectory(directoryKey(directory))) continue
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function ensureChild(directory: string) {
|
function ensureChild(directory: string) {
|
||||||
if (!directory) console.error("No directory provided")
|
const key = directoryKey(directory)
|
||||||
if (!children[directory]) {
|
if (!key) console.error("No directory provided")
|
||||||
|
if (!children[key]) {
|
||||||
const vcs = runWithOwner(input.owner, () =>
|
const vcs = runWithOwner(input.owner, () =>
|
||||||
persisted(
|
persisted(
|
||||||
Persist.workspace(directory, "vcs", ["vcs.v1"]),
|
Persist.workspace(directory, "vcs", ["vcs.v1"]),
|
||||||
|
|
@ -134,7 +149,7 @@ export function createChildStoreManager(input: {
|
||||||
)
|
)
|
||||||
if (!vcs) throw new Error(input.translate("error.childStore.persistedCacheCreateFailed"))
|
if (!vcs) throw new Error(input.translate("error.childStore.persistedCacheCreateFailed"))
|
||||||
const vcsStore = vcs[0]
|
const vcsStore = vcs[0]
|
||||||
vcsCache.set(directory, { store: vcsStore, setStore: vcs[1], ready: vcs[3] })
|
vcsCache.set(key, { store: vcsStore, setStore: vcs[1], ready: vcs[3] })
|
||||||
|
|
||||||
const meta = runWithOwner(input.owner, () =>
|
const meta = runWithOwner(input.owner, () =>
|
||||||
persisted(
|
persisted(
|
||||||
|
|
@ -143,7 +158,7 @@ export function createChildStoreManager(input: {
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
if (!meta) throw new Error(input.translate("error.childStore.persistedProjectMetadataCreateFailed"))
|
if (!meta) throw new Error(input.translate("error.childStore.persistedProjectMetadataCreateFailed"))
|
||||||
metaCache.set(directory, { store: meta[0], setStore: meta[1], ready: meta[3] })
|
metaCache.set(key, { store: meta[0], setStore: meta[1], ready: meta[3] })
|
||||||
|
|
||||||
const icon = runWithOwner(input.owner, () =>
|
const icon = runWithOwner(input.owner, () =>
|
||||||
persisted(
|
persisted(
|
||||||
|
|
@ -152,19 +167,38 @@ export function createChildStoreManager(input: {
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
if (!icon) throw new Error(input.translate("error.childStore.persistedProjectIconCreateFailed"))
|
if (!icon) throw new Error(input.translate("error.childStore.persistedProjectIconCreateFailed"))
|
||||||
iconCache.set(directory, { store: icon[0], setStore: icon[1], ready: icon[3] })
|
iconCache.set(key, { store: icon[0], setStore: icon[1], ready: icon[3] })
|
||||||
|
|
||||||
const init = () =>
|
const init = () =>
|
||||||
createRoot((dispose) => {
|
createRoot((dispose) => {
|
||||||
|
const sdk = input.getSdk(directory)
|
||||||
|
|
||||||
|
const initialMeta = meta[0].value
|
||||||
const initialIcon = icon[0].value
|
const initialIcon = icon[0].value
|
||||||
|
|
||||||
const pathQuery = useQuery(() => loadPathQuery(directory))
|
const [pathQuery, mcpQuery, lspQuery, providerQuery] = useQueries(() => ({
|
||||||
|
queries: [
|
||||||
|
loadPathQuery(key, sdk),
|
||||||
|
loadMcpQuery(key, sdk),
|
||||||
|
loadLspQuery(key, sdk),
|
||||||
|
loadProvidersQuery(key, sdk),
|
||||||
|
],
|
||||||
|
}))
|
||||||
|
|
||||||
const child = createStore<State>({
|
const child = createStore<State>({
|
||||||
project: "",
|
project: "",
|
||||||
projectMeta: undefined,
|
projectMeta: initialMeta,
|
||||||
icon: initialIcon,
|
icon: initialIcon,
|
||||||
provider_ready: false,
|
get provider_ready() {
|
||||||
provider: { all: [], connected: [], default: {} },
|
return !providerQuery.isLoading
|
||||||
|
},
|
||||||
|
get provider() {
|
||||||
|
const EMPTY = { all: [], connected: [], default: {} }
|
||||||
|
if (providerQuery.isLoading) return EMPTY
|
||||||
|
if (providerQuery.data?.all.length === 0 && input.global.provider.all.length > 0)
|
||||||
|
return input.global.provider
|
||||||
|
return providerQuery.data ?? EMPTY
|
||||||
|
},
|
||||||
config: {},
|
config: {},
|
||||||
get path() {
|
get path() {
|
||||||
if (pathQuery.isLoading || !pathQuery.data)
|
if (pathQuery.isLoading || !pathQuery.data)
|
||||||
|
|
@ -181,22 +215,30 @@ export function createChildStoreManager(input: {
|
||||||
todo: {},
|
todo: {},
|
||||||
permission: {},
|
permission: {},
|
||||||
question: {},
|
question: {},
|
||||||
mcp_ready: false,
|
get mcp_ready() {
|
||||||
mcp: {},
|
return !mcpQuery.isLoading
|
||||||
lsp_ready: false,
|
},
|
||||||
lsp: [],
|
get mcp() {
|
||||||
|
return mcpQuery.isLoading ? {} : (mcpQuery.data ?? {})
|
||||||
|
},
|
||||||
|
get lsp_ready() {
|
||||||
|
return !lspQuery.isLoading
|
||||||
|
},
|
||||||
|
get lsp() {
|
||||||
|
return lspQuery.isLoading ? [] : (lspQuery.data ?? [])
|
||||||
|
},
|
||||||
vcs: vcsStore.value,
|
vcs: vcsStore.value,
|
||||||
limit: 5,
|
limit: 5,
|
||||||
message: {},
|
message: {},
|
||||||
part: {},
|
part: {},
|
||||||
})
|
})
|
||||||
children[directory] = child
|
children[key] = child
|
||||||
disposers.set(directory, dispose)
|
disposers.set(key, dispose)
|
||||||
|
|
||||||
const onPersistedInit = (init: Promise<string> | string | null, run: () => void) => {
|
const onPersistedInit = (init: Promise<string> | string | null, run: () => void) => {
|
||||||
if (!(init instanceof Promise)) return
|
if (!(init instanceof Promise)) return
|
||||||
void init.then(() => {
|
void init.then(() => {
|
||||||
if (children[directory] !== child) return
|
if (children[key] !== child) return
|
||||||
run()
|
run()
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
@ -207,6 +249,11 @@ export function createChildStoreManager(input: {
|
||||||
child[1]("vcs", (value) => value ?? cached)
|
child[1]("vcs", (value) => value ?? cached)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
onPersistedInit(meta[2], () => {
|
||||||
|
if (child[0].projectMeta !== initialMeta) return
|
||||||
|
child[1]("projectMeta", meta[0].value)
|
||||||
|
})
|
||||||
|
|
||||||
onPersistedInit(icon[2], () => {
|
onPersistedInit(icon[2], () => {
|
||||||
if (child[0].icon !== initialIcon) return
|
if (child[0].icon !== initialIcon) return
|
||||||
child[1]("icon", icon[0].value)
|
child[1]("icon", icon[0].value)
|
||||||
|
|
@ -215,15 +262,16 @@ export function createChildStoreManager(input: {
|
||||||
|
|
||||||
runWithOwner(input.owner, init)
|
runWithOwner(input.owner, init)
|
||||||
}
|
}
|
||||||
mark(directory)
|
markKey(key)
|
||||||
const childStore = children[directory]
|
const childStore = children[key]
|
||||||
if (!childStore) throw new Error(input.translate("error.childStore.storeCreateFailed"))
|
if (!childStore) throw new Error(input.translate("error.childStore.storeCreateFailed"))
|
||||||
return childStore
|
return childStore
|
||||||
}
|
}
|
||||||
|
|
||||||
function child(directory: string, options: ChildOptions = {}) {
|
function child(directory: string, options: ChildOptions = {}) {
|
||||||
|
const key = directoryKey(directory)
|
||||||
const childStore = ensureChild(directory)
|
const childStore = ensureChild(directory)
|
||||||
pinForOwner(directory)
|
pinForOwner(key)
|
||||||
const shouldBootstrap = options.bootstrap ?? true
|
const shouldBootstrap = options.bootstrap ?? true
|
||||||
if (shouldBootstrap && childStore[0].status === "loading") {
|
if (shouldBootstrap && childStore[0].status === "loading") {
|
||||||
input.onBootstrap(directory)
|
input.onBootstrap(directory)
|
||||||
|
|
@ -232,6 +280,7 @@ export function createChildStoreManager(input: {
|
||||||
}
|
}
|
||||||
|
|
||||||
function peek(directory: string, options: ChildOptions = {}) {
|
function peek(directory: string, options: ChildOptions = {}) {
|
||||||
|
const key = directoryKey(directory)
|
||||||
const childStore = ensureChild(directory)
|
const childStore = ensureChild(directory)
|
||||||
const shouldBootstrap = options.bootstrap ?? true
|
const shouldBootstrap = options.bootstrap ?? true
|
||||||
if (shouldBootstrap && childStore[0].status === "loading") {
|
if (shouldBootstrap && childStore[0].status === "loading") {
|
||||||
|
|
@ -241,8 +290,9 @@ export function createChildStoreManager(input: {
|
||||||
}
|
}
|
||||||
|
|
||||||
function projectMeta(directory: string, patch: ProjectMeta) {
|
function projectMeta(directory: string, patch: ProjectMeta) {
|
||||||
|
const key = directoryKey(directory)
|
||||||
const [store, setStore] = ensureChild(directory)
|
const [store, setStore] = ensureChild(directory)
|
||||||
const cached = metaCache.get(directory)
|
const cached = metaCache.get(key)
|
||||||
if (!cached) return
|
if (!cached) return
|
||||||
const previous = store.projectMeta ?? {}
|
const previous = store.projectMeta ?? {}
|
||||||
const icon = patch.icon ? { ...previous.icon, ...patch.icon } : previous.icon
|
const icon = patch.icon ? { ...previous.icon, ...patch.icon } : previous.icon
|
||||||
|
|
@ -258,8 +308,9 @@ export function createChildStoreManager(input: {
|
||||||
}
|
}
|
||||||
|
|
||||||
function projectIcon(directory: string, value: string | undefined) {
|
function projectIcon(directory: string, value: string | undefined) {
|
||||||
|
const key = directoryKey(directory)
|
||||||
const [store, setStore] = ensureChild(directory)
|
const [store, setStore] = ensureChild(directory)
|
||||||
const cached = iconCache.get(directory)
|
const cached = iconCache.get(key)
|
||||||
if (!cached) return
|
if (!cached) return
|
||||||
if (store.icon === value) return
|
if (store.icon === value) return
|
||||||
cached.setStore("value", value)
|
cached.setStore("value", value)
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import { Binary } from "@opencode-ai/shared/util/binary"
|
import { Binary } from "@opencode-ai/core/util/binary"
|
||||||
import { produce, reconcile, type SetStoreFunction, type Store } from "solid-js/store"
|
import { produce, reconcile, type SetStoreFunction, type Store } from "solid-js/store"
|
||||||
import type {
|
import type {
|
||||||
Message,
|
Message,
|
||||||
|
|
|
||||||
46
packages/app/src/context/global-sync/queue.test.ts
Normal file
46
packages/app/src/context/global-sync/queue.test.ts
Normal file
|
|
@ -0,0 +1,46 @@
|
||||||
|
import { describe, expect, test } from "bun:test"
|
||||||
|
import { createRefreshQueue } from "./queue"
|
||||||
|
import { directoryKey } from "./utils"
|
||||||
|
|
||||||
|
const tick = () => new Promise((resolve) => setTimeout(resolve, 10))
|
||||||
|
|
||||||
|
describe("createRefreshQueue", () => {
|
||||||
|
test("clears queued directories by normalized key", async () => {
|
||||||
|
const calls: string[] = []
|
||||||
|
const queue = createRefreshQueue({
|
||||||
|
paused: () => false,
|
||||||
|
key: directoryKey,
|
||||||
|
bootstrap: async () => {},
|
||||||
|
bootstrapInstance: (directory) => {
|
||||||
|
calls.push(directory)
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
queue.push("C:\\tmp\\demo")
|
||||||
|
queue.clear("C:/tmp/demo")
|
||||||
|
|
||||||
|
await tick()
|
||||||
|
|
||||||
|
expect(calls).toEqual([])
|
||||||
|
queue.dispose()
|
||||||
|
})
|
||||||
|
|
||||||
|
test("passes the original directory to bootstrapInstance", async () => {
|
||||||
|
const calls: string[] = []
|
||||||
|
const queue = createRefreshQueue({
|
||||||
|
paused: () => false,
|
||||||
|
key: directoryKey,
|
||||||
|
bootstrap: async () => {},
|
||||||
|
bootstrapInstance: (directory) => {
|
||||||
|
calls.push(directory)
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
queue.push("C:\\tmp\\demo")
|
||||||
|
|
||||||
|
await tick()
|
||||||
|
|
||||||
|
expect(calls).toEqual(["C:\\tmp\\demo"])
|
||||||
|
queue.dispose()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
@ -2,22 +2,25 @@ type QueueInput = {
|
||||||
paused: () => boolean
|
paused: () => boolean
|
||||||
bootstrap: () => Promise<void>
|
bootstrap: () => Promise<void>
|
||||||
bootstrapInstance: (directory: string) => Promise<void> | void
|
bootstrapInstance: (directory: string) => Promise<void> | void
|
||||||
|
key?: (directory: string) => string
|
||||||
}
|
}
|
||||||
|
|
||||||
export function createRefreshQueue(input: QueueInput) {
|
export function createRefreshQueue(input: QueueInput) {
|
||||||
const queued = new Set<string>()
|
const queued = new Map<string, string>()
|
||||||
let root = false
|
let root = false
|
||||||
let running = false
|
let running = false
|
||||||
let timer: ReturnType<typeof setTimeout> | undefined
|
let timer: ReturnType<typeof setTimeout> | undefined
|
||||||
|
|
||||||
|
const key = input.key ?? ((directory: string) => directory)
|
||||||
|
|
||||||
const tick = () => new Promise<void>((resolve) => setTimeout(resolve, 0))
|
const tick = () => new Promise<void>((resolve) => setTimeout(resolve, 0))
|
||||||
|
|
||||||
const take = (count: number) => {
|
const take = (count: number) => {
|
||||||
if (queued.size === 0) return [] as string[]
|
if (queued.size === 0) return [] as string[]
|
||||||
const items: string[] = []
|
const items: string[] = []
|
||||||
for (const item of queued) {
|
for (const [id, directory] of queued) {
|
||||||
queued.delete(item)
|
queued.delete(id)
|
||||||
items.push(item)
|
items.push(directory)
|
||||||
if (items.length >= count) break
|
if (items.length >= count) break
|
||||||
}
|
}
|
||||||
return items
|
return items
|
||||||
|
|
@ -33,7 +36,7 @@ export function createRefreshQueue(input: QueueInput) {
|
||||||
|
|
||||||
const push = (directory: string) => {
|
const push = (directory: string) => {
|
||||||
if (!directory) return
|
if (!directory) return
|
||||||
queued.add(directory)
|
queued.set(key(directory), directory)
|
||||||
if (input.paused()) return
|
if (input.paused()) return
|
||||||
schedule()
|
schedule()
|
||||||
}
|
}
|
||||||
|
|
@ -73,7 +76,7 @@ export function createRefreshQueue(input: QueueInput) {
|
||||||
push,
|
push,
|
||||||
refresh,
|
refresh,
|
||||||
clear(directory: string) {
|
clear(directory: string) {
|
||||||
queued.delete(directory)
|
queued.delete(key(directory))
|
||||||
},
|
},
|
||||||
dispose() {
|
dispose() {
|
||||||
if (!timer) return
|
if (!timer) return
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
import { describe, expect, test } from "bun:test"
|
import { describe, expect, test } from "bun:test"
|
||||||
import type { Agent } from "@opencode-ai/sdk/v2/client"
|
import type { Agent } from "@opencode-ai/sdk/v2/client"
|
||||||
import { normalizeAgentList } from "./utils"
|
import { directoryKey, normalizeAgentList } from "./utils"
|
||||||
|
|
||||||
const agent = (name = "build") =>
|
const agent = (name = "build") =>
|
||||||
({
|
({
|
||||||
|
|
@ -33,3 +33,20 @@ describe("normalizeAgentList", () => {
|
||||||
expect(normalizeAgentList([{ name: "build" }, agent("docs")])).toEqual([agent("docs")])
|
expect(normalizeAgentList([{ name: "build" }, agent("docs")])).toEqual([agent("docs")])
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
describe("directoryKey", () => {
|
||||||
|
test("normalizes slashes", () => {
|
||||||
|
expect(String(directoryKey("C:\\Repos\\sst\\opencode"))).toBe("C:/Repos/sst/opencode")
|
||||||
|
expect(String(directoryKey("C:/Repos/sst/opencode"))).toBe("C:/Repos/sst/opencode")
|
||||||
|
})
|
||||||
|
|
||||||
|
test("preserves backslashes in posix paths", () => {
|
||||||
|
expect(String(directoryKey("/tmp/foo\\bar"))).toBe("/tmp/foo\\bar")
|
||||||
|
})
|
||||||
|
|
||||||
|
test("trims trailing slashes without breaking roots", () => {
|
||||||
|
expect(String(directoryKey("C:/Repos/sst/opencode/"))).toBe("C:/Repos/sst/opencode")
|
||||||
|
expect(String(directoryKey("C:/"))).toBe("C:/")
|
||||||
|
expect(String(directoryKey("/"))).toBe("/")
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
import type { Agent, Project, ProviderListResponse } from "@opencode-ai/sdk/v2/client"
|
import type { Agent, Project, ProviderListResponse } from "@opencode-ai/sdk/v2/client"
|
||||||
|
export { pathKey as directoryKey, type PathKey as DirectoryKey } from "@/utils/path-key"
|
||||||
|
|
||||||
export const cmp = (a: string, b: string) => (a < b ? -1 : a > b ? 1 : 0)
|
export const cmp = (a: string, b: string) => (a < b ? -1 : a > b ? 1 : 0)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -391,7 +391,14 @@ export const { use: useLayout, provider: LayoutProvider } = createSimpleContext(
|
||||||
? globalSync.data.project.find((x) => x.id === projectID)
|
? globalSync.data.project.find((x) => x.id === projectID)
|
||||||
: globalSync.data.project.find((x) => x.worktree === project.worktree)
|
: globalSync.data.project.find((x) => x.worktree === project.worktree)
|
||||||
|
|
||||||
return { ...metadata, ...project }
|
// Preserve local icon override from per-workspace localStorage cache (childStore.icon).
|
||||||
|
// Without this, different subdirectories of the same git repo would share the same
|
||||||
|
// icon from the database instead of using their individual overrides.
|
||||||
|
const base = { ...metadata, ...project }
|
||||||
|
if (childStore.icon) {
|
||||||
|
return { ...base, icon: { ...base.icon, override: childStore.icon } }
|
||||||
|
}
|
||||||
|
return base
|
||||||
}
|
}
|
||||||
|
|
||||||
const roots = createMemo(() => {
|
const roots = createMemo(() => {
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import { createSimpleContext } from "@opencode-ai/ui/context"
|
import { createSimpleContext } from "@opencode-ai/ui/context"
|
||||||
import { base64Encode } from "@opencode-ai/shared/util/encode"
|
import { base64Encode } from "@opencode-ai/core/util/encode"
|
||||||
import { useParams } from "@solidjs/router"
|
import { useParams } from "@solidjs/router"
|
||||||
import { batch, createEffect, createMemo } from "solid-js"
|
import { batch, createEffect, createMemo } from "solid-js"
|
||||||
import { createStore } from "solid-js/store"
|
import { createStore } from "solid-js/store"
|
||||||
|
|
@ -382,7 +382,7 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({
|
||||||
setSaved("session", session, {
|
setSaved("session", session, {
|
||||||
agent: msg.agent,
|
agent: msg.agent,
|
||||||
model: msg.model,
|
model: msg.model,
|
||||||
variant: msg.model.variant ?? null,
|
variant: msg.model?.variant ?? null,
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -7,8 +7,8 @@ import { useGlobalSync } from "./global-sync"
|
||||||
import { usePlatform } from "@/context/platform"
|
import { usePlatform } from "@/context/platform"
|
||||||
import { useLanguage } from "@/context/language"
|
import { useLanguage } from "@/context/language"
|
||||||
import { useSettings } from "@/context/settings"
|
import { useSettings } from "@/context/settings"
|
||||||
import { Binary } from "@opencode-ai/shared/util/binary"
|
import { Binary } from "@opencode-ai/core/util/binary"
|
||||||
import { base64Encode } from "@opencode-ai/shared/util/encode"
|
import { base64Encode } from "@opencode-ai/core/util/encode"
|
||||||
import { decode64 } from "@/utils/base64"
|
import { decode64 } from "@/utils/base64"
|
||||||
import { EventSessionError } from "@opencode-ai/sdk/v2"
|
import { EventSessionError } from "@opencode-ai/sdk/v2"
|
||||||
import { Persist, persisted } from "@/utils/persist"
|
import { Persist, persisted } from "@/utils/persist"
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
import { describe, expect, test } from "bun:test"
|
import { describe, expect, test } from "bun:test"
|
||||||
import type { PermissionRequest, Session } from "@opencode-ai/sdk/v2/client"
|
import type { PermissionRequest, Session } from "@opencode-ai/sdk/v2/client"
|
||||||
import { base64Encode } from "@opencode-ai/shared/util/encode"
|
import { base64Encode } from "@opencode-ai/core/util/encode"
|
||||||
import { autoRespondsPermission, isDirectoryAutoAccepting } from "./permission-auto-respond"
|
import { autoRespondsPermission, isDirectoryAutoAccepting } from "./permission-auto-respond"
|
||||||
|
|
||||||
const session = (input: { id: string; parentID?: string }) =>
|
const session = (input: { id: string; parentID?: string }) =>
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import { base64Encode } from "@opencode-ai/shared/util/encode"
|
import { base64Encode } from "@opencode-ai/core/util/encode"
|
||||||
|
|
||||||
export function acceptKey(sessionID: string, directory?: string) {
|
export function acceptKey(sessionID: string, directory?: string) {
|
||||||
if (!directory) return sessionID
|
if (!directory) return sessionID
|
||||||
|
|
|
||||||
|
|
@ -49,11 +49,11 @@ export type Platform = {
|
||||||
/** Storage mechanism, defaults to localStorage */
|
/** Storage mechanism, defaults to localStorage */
|
||||||
storage?: (name?: string) => SyncStorage | AsyncStorage
|
storage?: (name?: string) => SyncStorage | AsyncStorage
|
||||||
|
|
||||||
/** Check for updates (Tauri only) */
|
/** Check for a downloadable desktop update */
|
||||||
checkUpdate?(): Promise<UpdateInfo>
|
checkUpdate?(): Promise<UpdateInfo>
|
||||||
|
|
||||||
/** Install updates (Tauri only) */
|
/** Install the downloaded update using the platform restart flow */
|
||||||
update?(): Promise<void>
|
updateAndRestart?(): Promise<void>
|
||||||
|
|
||||||
/** Fetch override */
|
/** Fetch override */
|
||||||
fetch?: typeof fetch
|
fetch?: typeof fetch
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import { createSimpleContext } from "@opencode-ai/ui/context"
|
import { createSimpleContext } from "@opencode-ai/ui/context"
|
||||||
import { checksum } from "@opencode-ai/shared/util/encode"
|
import { checksum } from "@opencode-ai/core/util/encode"
|
||||||
import { useParams } from "@solidjs/router"
|
import { useParams } from "@solidjs/router"
|
||||||
import { batch, createMemo, createRoot, getOwner, onCleanup } from "solid-js"
|
import { batch, createMemo, createRoot, getOwner, onCleanup } from "solid-js"
|
||||||
import { createStore, type SetStoreFunction } from "solid-js/store"
|
import { createStore, type SetStoreFunction } from "solid-js/store"
|
||||||
|
|
|
||||||
53
packages/app/src/context/server.test.ts
Normal file
53
packages/app/src/context/server.test.ts
Normal file
|
|
@ -0,0 +1,53 @@
|
||||||
|
import { describe, expect, test } from "bun:test"
|
||||||
|
import { resolveServerList, ServerConnection } from "./server"
|
||||||
|
|
||||||
|
describe("resolveServerList", () => {
|
||||||
|
test("lets startup auth_token credentials override a persisted same-url server", () => {
|
||||||
|
const list = resolveServerList({
|
||||||
|
stored: [{ url: "https://server.example.test" }],
|
||||||
|
props: [
|
||||||
|
{
|
||||||
|
type: "http",
|
||||||
|
authToken: true,
|
||||||
|
http: {
|
||||||
|
url: "https://server.example.test",
|
||||||
|
username: "opencode",
|
||||||
|
password: "secret",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(list).toHaveLength(1)
|
||||||
|
expect(list[0]?.type).toBe("http")
|
||||||
|
expect(list[0]?.http).toEqual({
|
||||||
|
url: "https://server.example.test",
|
||||||
|
username: "opencode",
|
||||||
|
password: "secret",
|
||||||
|
})
|
||||||
|
expect(list[0]?.type === "http" ? list[0].authToken : false).toBe(true)
|
||||||
|
expect(ServerConnection.key(list[0]!) as string).toBe("https://server.example.test")
|
||||||
|
})
|
||||||
|
|
||||||
|
test("keeps persisted credentials when startup has no auth_token", () => {
|
||||||
|
const list = resolveServerList({
|
||||||
|
stored: [
|
||||||
|
{
|
||||||
|
url: "https://server.example.test",
|
||||||
|
username: "opencode",
|
||||||
|
password: "saved",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
props: [{ type: "http", http: { url: "https://server.example.test" } }],
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(list).toHaveLength(1)
|
||||||
|
expect(list[0]?.type).toBe("http")
|
||||||
|
expect(list[0]?.http).toEqual({
|
||||||
|
url: "https://server.example.test",
|
||||||
|
username: "opencode",
|
||||||
|
password: "saved",
|
||||||
|
})
|
||||||
|
expect(list[0]?.type === "http" ? list[0].authToken : true).toBeUndefined()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
@ -33,6 +33,33 @@ function isLocalHost(url: string) {
|
||||||
if (host === "localhost" || host === "127.0.0.1") return "local"
|
if (host === "localhost" || host === "127.0.0.1") return "local"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function resolveServerList(input: {
|
||||||
|
props?: Array<ServerConnection.Any>
|
||||||
|
stored: StoredServer[]
|
||||||
|
}): Array<ServerConnection.Any> {
|
||||||
|
const servers = [
|
||||||
|
...input.stored.map((value) =>
|
||||||
|
typeof value === "string"
|
||||||
|
? {
|
||||||
|
type: "http" as const,
|
||||||
|
http: { url: value },
|
||||||
|
}
|
||||||
|
: value,
|
||||||
|
),
|
||||||
|
...(input.props ?? []),
|
||||||
|
]
|
||||||
|
|
||||||
|
const deduped = new Map<ServerConnection.Key, ServerConnection.Any>()
|
||||||
|
for (const value of servers) {
|
||||||
|
const conn: ServerConnection.Any = "type" in value ? value : { type: "http", http: value }
|
||||||
|
const key = ServerConnection.key(conn)
|
||||||
|
if (deduped.has(key) && conn.type === "http" && !conn.authToken) continue
|
||||||
|
deduped.set(key, conn)
|
||||||
|
}
|
||||||
|
|
||||||
|
return [...deduped.values()]
|
||||||
|
}
|
||||||
|
|
||||||
export namespace ServerConnection {
|
export namespace ServerConnection {
|
||||||
type Base = { displayName?: string }
|
type Base = { displayName?: string }
|
||||||
|
|
||||||
|
|
@ -46,6 +73,7 @@ export namespace ServerConnection {
|
||||||
export type Http = {
|
export type Http = {
|
||||||
type: "http"
|
type: "http"
|
||||||
http: HttpBase
|
http: HttpBase
|
||||||
|
authToken?: boolean
|
||||||
} & Base
|
} & Base
|
||||||
|
|
||||||
export type Sidecar = {
|
export type Sidecar = {
|
||||||
|
|
@ -113,26 +141,7 @@ export const { use: useServer, provider: ServerProvider } = createSimpleContext(
|
||||||
const url = (x: StoredServer) => (typeof x === "string" ? x : "type" in x ? x.http.url : x.url)
|
const url = (x: StoredServer) => (typeof x === "string" ? x : "type" in x ? x.http.url : x.url)
|
||||||
|
|
||||||
const allServers = createMemo((): Array<ServerConnection.Any> => {
|
const allServers = createMemo((): Array<ServerConnection.Any> => {
|
||||||
const servers = [
|
return resolveServerList({ stored: store.list, props: props.servers })
|
||||||
...(props.servers ?? []),
|
|
||||||
...store.list.map((value) =>
|
|
||||||
typeof value === "string"
|
|
||||||
? {
|
|
||||||
type: "http" as const,
|
|
||||||
http: { url: value },
|
|
||||||
}
|
|
||||||
: value,
|
|
||||||
),
|
|
||||||
]
|
|
||||||
|
|
||||||
const deduped = new Map(
|
|
||||||
servers.map((value) => {
|
|
||||||
const conn: ServerConnection.Any = "type" in value ? value : { type: "http", http: value }
|
|
||||||
return [ServerConnection.key(conn), conn]
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
|
|
||||||
return [...deduped.values()]
|
|
||||||
})
|
})
|
||||||
|
|
||||||
const [state, setState] = createStore({
|
const [state, setState] = createStore({
|
||||||
|
|
@ -174,7 +183,7 @@ export const { use: useServer, provider: ServerProvider } = createSimpleContext(
|
||||||
function add(input: ServerConnection.Http) {
|
function add(input: ServerConnection.Http) {
|
||||||
const url_ = normalizeServerUrl(input.http.url)
|
const url_ = normalizeServerUrl(input.http.url)
|
||||||
if (!url_) return
|
if (!url_) return
|
||||||
const conn = { ...input, http: { ...input.http, url: url_ } }
|
const conn: ServerConnection.Http = { ...input, authToken: undefined, http: { ...input.http, url: url_ } }
|
||||||
return batch(() => {
|
return batch(() => {
|
||||||
const existing = store.list.findIndex((x) => url(x) === url_)
|
const existing = store.list.findIndex((x) => url(x) === url_)
|
||||||
if (existing !== -1) {
|
if (existing !== -1) {
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
import { batch, createMemo } from "solid-js"
|
import { batch, createMemo } from "solid-js"
|
||||||
import { createStore, produce, reconcile } from "solid-js/store"
|
import { createStore, produce, reconcile } from "solid-js/store"
|
||||||
import { Binary } from "@opencode-ai/shared/util/binary"
|
import { Binary } from "@opencode-ai/core/util/binary"
|
||||||
import { retry } from "@opencode-ai/shared/util/retry"
|
import { retry } from "@opencode-ai/core/util/retry"
|
||||||
import { createSimpleContext } from "@opencode-ai/ui/context"
|
import { createSimpleContext } from "@opencode-ai/ui/context"
|
||||||
import {
|
import {
|
||||||
clearSessionPrefetch,
|
clearSessionPrefetch,
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,9 @@
|
||||||
import { beforeAll, describe, expect, mock, test } from "bun:test"
|
import { beforeAll, describe, expect, mock, test } from "bun:test"
|
||||||
|
|
||||||
let getWorkspaceTerminalCacheKey: (dir: string) => string
|
type ServerKey = Parameters<typeof import("./terminal").getTerminalServerScope>[1]
|
||||||
|
|
||||||
|
let getWorkspaceTerminalCacheKey: (dir: string, scope?: string) => string
|
||||||
|
let getTerminalServerScope: typeof import("./terminal").getTerminalServerScope
|
||||||
let getLegacyTerminalStorageKeys: (dir: string, legacySessionID?: string) => string[]
|
let getLegacyTerminalStorageKeys: (dir: string, legacySessionID?: string) => string[]
|
||||||
let migrateTerminalState: (value: unknown) => unknown
|
let migrateTerminalState: (value: unknown) => unknown
|
||||||
|
|
||||||
|
|
@ -17,6 +20,7 @@ beforeAll(async () => {
|
||||||
}))
|
}))
|
||||||
const mod = await import("./terminal")
|
const mod = await import("./terminal")
|
||||||
getWorkspaceTerminalCacheKey = mod.getWorkspaceTerminalCacheKey
|
getWorkspaceTerminalCacheKey = mod.getWorkspaceTerminalCacheKey
|
||||||
|
getTerminalServerScope = mod.getTerminalServerScope
|
||||||
getLegacyTerminalStorageKeys = mod.getLegacyTerminalStorageKeys
|
getLegacyTerminalStorageKeys = mod.getLegacyTerminalStorageKeys
|
||||||
migrateTerminalState = mod.migrateTerminalState
|
migrateTerminalState = mod.migrateTerminalState
|
||||||
})
|
})
|
||||||
|
|
@ -25,6 +29,45 @@ describe("getWorkspaceTerminalCacheKey", () => {
|
||||||
test("uses workspace-only directory cache key", () => {
|
test("uses workspace-only directory cache key", () => {
|
||||||
expect(getWorkspaceTerminalCacheKey("/repo")).toBe("/repo:__workspace__")
|
expect(getWorkspaceTerminalCacheKey("/repo")).toBe("/repo:__workspace__")
|
||||||
})
|
})
|
||||||
|
|
||||||
|
test("can include a server scope", () => {
|
||||||
|
expect(getWorkspaceTerminalCacheKey("/repo", "wsl:Debian")).toBe("wsl:Debian:/repo:__workspace__")
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe("getTerminalServerScope", () => {
|
||||||
|
test("preserves local server keys", () => {
|
||||||
|
expect(
|
||||||
|
getTerminalServerScope(
|
||||||
|
{ type: "sidecar", variant: "base", http: { url: "http://127.0.0.1:4096" } },
|
||||||
|
"sidecar" as ServerKey,
|
||||||
|
),
|
||||||
|
).toBeUndefined()
|
||||||
|
expect(
|
||||||
|
getTerminalServerScope(
|
||||||
|
{ type: "http", http: { url: "http://localhost:4096" } },
|
||||||
|
"http://localhost:4096" as ServerKey,
|
||||||
|
),
|
||||||
|
).toBeUndefined()
|
||||||
|
expect(
|
||||||
|
getTerminalServerScope({ type: "http", http: { url: "http://[::1]:4096" } }, "http://[::1]:4096" as ServerKey),
|
||||||
|
).toBeUndefined()
|
||||||
|
})
|
||||||
|
|
||||||
|
test("scopes non-local server keys", () => {
|
||||||
|
expect(
|
||||||
|
getTerminalServerScope(
|
||||||
|
{ type: "sidecar", variant: "wsl", distro: "Debian", http: { url: "http://127.0.0.1:4096" } },
|
||||||
|
"wsl:Debian" as ServerKey,
|
||||||
|
),
|
||||||
|
).toBe("wsl:Debian" as ServerKey)
|
||||||
|
expect(
|
||||||
|
getTerminalServerScope(
|
||||||
|
{ type: "http", http: { url: "https://example.com" } },
|
||||||
|
"https://example.com" as ServerKey,
|
||||||
|
),
|
||||||
|
).toBe("https://example.com" as ServerKey)
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
describe("getLegacyTerminalStorageKeys", () => {
|
describe("getLegacyTerminalStorageKeys", () => {
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@ import { batch, createEffect, createMemo, createRoot, on, onCleanup } from "soli
|
||||||
import { useParams } from "@solidjs/router"
|
import { useParams } from "@solidjs/router"
|
||||||
import { useSDK } from "./sdk"
|
import { useSDK } from "./sdk"
|
||||||
import type { Platform } from "./platform"
|
import type { Platform } from "./platform"
|
||||||
|
import { ServerConnection, useServer } from "./server"
|
||||||
import { defaultTitle, titleNumber } from "./terminal-title"
|
import { defaultTitle, titleNumber } from "./terminal-title"
|
||||||
import { Persist, persisted, removePersisted } from "@/utils/persist"
|
import { Persist, persisted, removePersisted } from "@/utils/persist"
|
||||||
|
|
||||||
|
|
@ -82,10 +83,31 @@ export function migrateTerminalState(value: unknown) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getWorkspaceTerminalCacheKey(dir: string) {
|
export function getWorkspaceTerminalCacheKey(dir: string, scope?: string) {
|
||||||
|
if (scope) return `${scope}:${dir}:${WORKSPACE_KEY}`
|
||||||
return `${dir}:${WORKSPACE_KEY}`
|
return `${dir}:${WORKSPACE_KEY}`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function getTerminalServerScope(conn: ServerConnection.Any | undefined, key: ServerConnection.Key) {
|
||||||
|
if (!conn) return
|
||||||
|
if (conn.type === "sidecar" && conn.variant === "base") return
|
||||||
|
if (conn.type === "http") {
|
||||||
|
try {
|
||||||
|
const url = new URL(conn.http.url)
|
||||||
|
if (
|
||||||
|
url.hostname === "localhost" ||
|
||||||
|
url.hostname === "127.0.0.1" ||
|
||||||
|
url.hostname === "::1" ||
|
||||||
|
url.hostname === "[::1]"
|
||||||
|
)
|
||||||
|
return
|
||||||
|
} catch {
|
||||||
|
return key
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return key
|
||||||
|
}
|
||||||
|
|
||||||
export function getLegacyTerminalStorageKeys(dir: string, legacySessionID?: string) {
|
export function getLegacyTerminalStorageKeys(dir: string, legacySessionID?: string) {
|
||||||
if (!legacySessionID) return [`${dir}/terminal.v1`]
|
if (!legacySessionID) return [`${dir}/terminal.v1`]
|
||||||
return [`${dir}/terminal/${legacySessionID}.v1`, `${dir}/terminal.v1`]
|
return [`${dir}/terminal/${legacySessionID}.v1`, `${dir}/terminal.v1`]
|
||||||
|
|
@ -110,15 +132,16 @@ const trimTerminal = (pty: LocalPTY) => {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function clearWorkspaceTerminals(dir: string, sessionIDs?: string[], platform?: Platform) {
|
export function clearWorkspaceTerminals(dir: string, sessionIDs?: string[], platform?: Platform, scope?: string) {
|
||||||
const key = getWorkspaceTerminalCacheKey(dir)
|
const key = getWorkspaceTerminalCacheKey(dir, scope)
|
||||||
for (const cache of caches) {
|
for (const cache of caches) {
|
||||||
const entry = cache.get(key)
|
const entry = cache.get(key)
|
||||||
entry?.value.clear()
|
entry?.value.clear()
|
||||||
}
|
}
|
||||||
|
|
||||||
void removePersisted(Persist.workspace(dir, "terminal"), platform)
|
void removePersisted(Persist.workspace(dir, scope ? `terminal:${scope}` : "terminal"), platform)
|
||||||
|
|
||||||
|
if (scope) return
|
||||||
const legacy = new Set(getLegacyTerminalStorageKeys(dir))
|
const legacy = new Set(getLegacyTerminalStorageKeys(dir))
|
||||||
for (const id of sessionIDs ?? []) {
|
for (const id of sessionIDs ?? []) {
|
||||||
for (const key of getLegacyTerminalStorageKeys(dir, id)) {
|
for (const key of getLegacyTerminalStorageKeys(dir, id)) {
|
||||||
|
|
@ -130,12 +153,17 @@ export function clearWorkspaceTerminals(dir: string, sessionIDs?: string[], plat
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function createWorkspaceTerminalSession(sdk: ReturnType<typeof useSDK>, dir: string, legacySessionID?: string) {
|
function createWorkspaceTerminalSession(
|
||||||
const legacy = getLegacyTerminalStorageKeys(dir, legacySessionID)
|
sdk: ReturnType<typeof useSDK>,
|
||||||
|
dir: string,
|
||||||
|
legacySessionID?: string,
|
||||||
|
scope?: string,
|
||||||
|
) {
|
||||||
|
const legacy = scope ? [] : getLegacyTerminalStorageKeys(dir, legacySessionID)
|
||||||
|
|
||||||
const [store, setStore, _, ready] = persisted(
|
const [store, setStore, _, ready] = persisted(
|
||||||
{
|
{
|
||||||
...Persist.workspace(dir, "terminal", legacy),
|
...Persist.workspace(dir, scope ? `terminal:${scope}` : "terminal", legacy),
|
||||||
migrate: migrateTerminalState,
|
migrate: migrateTerminalState,
|
||||||
},
|
},
|
||||||
createStore<{
|
createStore<{
|
||||||
|
|
@ -357,8 +385,12 @@ export const { use: useTerminal, provider: TerminalProvider } = createSimpleCont
|
||||||
gate: false,
|
gate: false,
|
||||||
init: () => {
|
init: () => {
|
||||||
const sdk = useSDK()
|
const sdk = useSDK()
|
||||||
|
const server = useServer()
|
||||||
const params = useParams()
|
const params = useParams()
|
||||||
const cache = new Map<string, TerminalCacheEntry>()
|
const cache = new Map<string, TerminalCacheEntry>()
|
||||||
|
const scope = createMemo(() => {
|
||||||
|
return getTerminalServerScope(server.current, server.key)
|
||||||
|
})
|
||||||
|
|
||||||
caches.add(cache)
|
caches.add(cache)
|
||||||
onCleanup(() => caches.delete(cache))
|
onCleanup(() => caches.delete(cache))
|
||||||
|
|
@ -382,9 +414,9 @@ export const { use: useTerminal, provider: TerminalProvider } = createSimpleCont
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const loadWorkspace = (dir: string, legacySessionID?: string) => {
|
const loadWorkspace = (dir: string, legacySessionID: string | undefined, serverScope: string | undefined) => {
|
||||||
// Terminals are workspace-scoped so tabs persist while switching sessions in the same directory.
|
// Terminals are workspace-scoped so tabs persist while switching sessions in the same directory.
|
||||||
const key = getWorkspaceTerminalCacheKey(dir)
|
const key = getWorkspaceTerminalCacheKey(dir, serverScope)
|
||||||
const existing = cache.get(key)
|
const existing = cache.get(key)
|
||||||
if (existing) {
|
if (existing) {
|
||||||
cache.delete(key)
|
cache.delete(key)
|
||||||
|
|
@ -393,7 +425,7 @@ export const { use: useTerminal, provider: TerminalProvider } = createSimpleCont
|
||||||
}
|
}
|
||||||
|
|
||||||
const entry = createRoot((dispose) => ({
|
const entry = createRoot((dispose) => ({
|
||||||
value: createWorkspaceTerminalSession(sdk, dir, legacySessionID),
|
value: createWorkspaceTerminalSession(sdk, dir, legacySessionID, serverScope),
|
||||||
dispose,
|
dispose,
|
||||||
}))
|
}))
|
||||||
|
|
||||||
|
|
@ -402,16 +434,16 @@ export const { use: useTerminal, provider: TerminalProvider } = createSimpleCont
|
||||||
return entry.value
|
return entry.value
|
||||||
}
|
}
|
||||||
|
|
||||||
const workspace = createMemo(() => loadWorkspace(params.dir!, params.id))
|
const workspace = createMemo(() => loadWorkspace(params.dir!, params.id, scope()))
|
||||||
|
|
||||||
createEffect(
|
createEffect(
|
||||||
on(
|
on(
|
||||||
() => ({ dir: params.dir, id: params.id }),
|
() => ({ dir: params.dir, id: params.id, scope: scope() }),
|
||||||
(next, prev) => {
|
(next, prev) => {
|
||||||
if (!prev?.dir) return
|
if (!prev?.dir) return
|
||||||
if (next.dir === prev.dir && next.id === prev.id) return
|
if (next.dir === prev.dir && next.id === prev.id && next.scope === prev.scope) return
|
||||||
if (next.dir === prev.dir && next.id) return
|
if (next.dir === prev.dir && next.id && next.scope === prev.scope) return
|
||||||
loadWorkspace(prev.dir, prev.id).trimAll()
|
loadWorkspace(prev.dir, prev.id, prev.scope).trimAll()
|
||||||
},
|
},
|
||||||
{ defer: true },
|
{ defer: true },
|
||||||
),
|
),
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,13 @@
|
||||||
// @refresh reload
|
// @refresh reload
|
||||||
|
|
||||||
|
import * as Sentry from "@sentry/solid"
|
||||||
import { render } from "solid-js/web"
|
import { render } from "solid-js/web"
|
||||||
import { AppBaseProviders, AppInterface } from "@/app"
|
import { AppBaseProviders, AppInterface } from "@/app"
|
||||||
import { type Platform, PlatformProvider } from "@/context/platform"
|
import { type Platform, PlatformProvider } from "@/context/platform"
|
||||||
import { dict as en } from "@/i18n/en"
|
import { dict as en } from "@/i18n/en"
|
||||||
import { dict as zh } from "@/i18n/zh"
|
import { dict as zh } from "@/i18n/zh"
|
||||||
import { handleNotificationClick } from "@/utils/notification-click"
|
import { handleNotificationClick } from "@/utils/notification-click"
|
||||||
|
import { authFromToken } from "@/utils/server"
|
||||||
import pkg from "../package.json"
|
import pkg from "../package.json"
|
||||||
import { ServerConnection } from "./context/server"
|
import { ServerConnection } from "./context/server"
|
||||||
|
|
||||||
|
|
@ -110,6 +112,13 @@ const getDefaultUrl = () => {
|
||||||
return getCurrentUrl()
|
return getCurrentUrl()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const clearAuthToken = () => {
|
||||||
|
const params = new URLSearchParams(location.search)
|
||||||
|
if (!params.has("auth_token")) return
|
||||||
|
params.delete("auth_token")
|
||||||
|
history.replaceState(null, "", location.pathname + (params.size ? `?${params}` : "") + location.hash)
|
||||||
|
}
|
||||||
|
|
||||||
const platform: Platform = {
|
const platform: Platform = {
|
||||||
platform: "web",
|
platform: "web",
|
||||||
version: pkg.version,
|
version: pkg.version,
|
||||||
|
|
@ -125,8 +134,36 @@ const platform: Platform = {
|
||||||
setDefaultServer: writeDefaultServerUrl,
|
setDefaultServer: writeDefaultServerUrl,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (import.meta.env.VITE_SENTRY_DSN) {
|
||||||
|
Sentry.init({
|
||||||
|
dsn: import.meta.env.VITE_SENTRY_DSN,
|
||||||
|
environment: import.meta.env.VITE_SENTRY_ENVIRONMENT ?? import.meta.env.MODE,
|
||||||
|
release: import.meta.env.VITE_SENTRY_RELEASE ?? `web@${pkg.version}`,
|
||||||
|
initialScope: {
|
||||||
|
tags: {
|
||||||
|
platform: "web",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
integrations: (integrations) => {
|
||||||
|
return integrations.filter(
|
||||||
|
(i) =>
|
||||||
|
i.name !== "Breadcrumbs" && !(import.meta.env.OPENCODE_CHANNEL === "prod" && i.name === "GlobalHandlers"),
|
||||||
|
)
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
if (root instanceof HTMLElement) {
|
if (root instanceof HTMLElement) {
|
||||||
const server: ServerConnection.Http = { type: "http", http: { url: getCurrentUrl() } }
|
const auth = authFromToken(new URLSearchParams(location.search).get("auth_token"))
|
||||||
|
clearAuthToken()
|
||||||
|
const server: ServerConnection.Http = {
|
||||||
|
type: "http",
|
||||||
|
authToken: !!auth,
|
||||||
|
http: {
|
||||||
|
url: getCurrentUrl(),
|
||||||
|
...auth,
|
||||||
|
},
|
||||||
|
}
|
||||||
render(
|
render(
|
||||||
() => (
|
() => (
|
||||||
<PlatformProvider value={platform}>
|
<PlatformProvider value={platform}>
|
||||||
|
|
|
||||||
4
packages/app/src/env.d.ts
vendored
4
packages/app/src/env.d.ts
vendored
|
|
@ -2,6 +2,10 @@ interface ImportMetaEnv {
|
||||||
readonly VITE_OPENCODE_SERVER_HOST: string
|
readonly VITE_OPENCODE_SERVER_HOST: string
|
||||||
readonly VITE_OPENCODE_SERVER_PORT: string
|
readonly VITE_OPENCODE_SERVER_PORT: string
|
||||||
readonly VITE_OPENCODE_CHANNEL?: "dev" | "beta" | "prod"
|
readonly VITE_OPENCODE_CHANNEL?: "dev" | "beta" | "prod"
|
||||||
|
|
||||||
|
readonly VITE_SENTRY_DSN?: string
|
||||||
|
readonly VITE_SENTRY_ENVIRONMENT?: string
|
||||||
|
readonly VITE_SENTRY_RELEASE?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
interface ImportMeta {
|
interface ImportMeta {
|
||||||
|
|
|
||||||
|
|
@ -210,7 +210,7 @@ export const dict = {
|
||||||
"common.saving": "جارٍ الحفظ...",
|
"common.saving": "جارٍ الحفظ...",
|
||||||
"common.default": "افتراضي",
|
"common.default": "افتراضي",
|
||||||
"common.attachment": "مرفق",
|
"common.attachment": "مرفق",
|
||||||
"prompt.placeholder.shell": "أدخل أمر shell...",
|
"prompt.placeholder.shell": "أدخل أمر shell... {{example}}",
|
||||||
"prompt.placeholder.normal": 'اسأل أي شيء... "{{example}}"',
|
"prompt.placeholder.normal": 'اسأل أي شيء... "{{example}}"',
|
||||||
"prompt.placeholder.simple": "اسأل أي شيء...",
|
"prompt.placeholder.simple": "اسأل أي شيء...",
|
||||||
"prompt.placeholder.summarizeComments": "لخّص التعليقات…",
|
"prompt.placeholder.summarizeComments": "لخّص التعليقات…",
|
||||||
|
|
@ -402,6 +402,8 @@ export const dict = {
|
||||||
"error.page.description": "حدث خطأ أثناء تحميل التطبيق.",
|
"error.page.description": "حدث خطأ أثناء تحميل التطبيق.",
|
||||||
"error.page.details.label": "تفاصيل الخطأ",
|
"error.page.details.label": "تفاصيل الخطأ",
|
||||||
"error.page.action.restart": "إعادة تشغيل",
|
"error.page.action.restart": "إعادة تشغيل",
|
||||||
|
"error.page.action.report": "الإبلاغ عن الخطأ",
|
||||||
|
"error.page.action.reported": "تم الإبلاغ عن الخطأ",
|
||||||
"error.page.action.checking": "جارٍ التحقق...",
|
"error.page.action.checking": "جارٍ التحقق...",
|
||||||
"error.page.action.checkUpdates": "التحقق من وجود تحديثات",
|
"error.page.action.checkUpdates": "التحقق من وجود تحديثات",
|
||||||
"error.page.action.updateTo": "تحديث إلى {{version}}",
|
"error.page.action.updateTo": "تحديث إلى {{version}}",
|
||||||
|
|
@ -721,8 +723,6 @@ export const dict = {
|
||||||
"settings.permissions.tool.webfetch.description": "جلب محتوى من عنوان URL",
|
"settings.permissions.tool.webfetch.description": "جلب محتوى من عنوان URL",
|
||||||
"settings.permissions.tool.websearch.title": "بحث الويب",
|
"settings.permissions.tool.websearch.title": "بحث الويب",
|
||||||
"settings.permissions.tool.websearch.description": "البحث في الويب",
|
"settings.permissions.tool.websearch.description": "البحث في الويب",
|
||||||
"settings.permissions.tool.codesearch.title": "بحث الكود",
|
|
||||||
"settings.permissions.tool.codesearch.description": "البحث عن كود على الويب",
|
|
||||||
"settings.permissions.tool.external_directory.title": "دليل خارجي",
|
"settings.permissions.tool.external_directory.title": "دليل خارجي",
|
||||||
"settings.permissions.tool.external_directory.description": "الوصول إلى الملفات خارج دليل المشروع",
|
"settings.permissions.tool.external_directory.description": "الوصول إلى الملفات خارج دليل المشروع",
|
||||||
"settings.permissions.tool.doom_loop.title": "حلقة الموت",
|
"settings.permissions.tool.doom_loop.title": "حلقة الموت",
|
||||||
|
|
|
||||||
|
|
@ -210,7 +210,7 @@ export const dict = {
|
||||||
"common.saving": "Salvando...",
|
"common.saving": "Salvando...",
|
||||||
"common.default": "Padrão",
|
"common.default": "Padrão",
|
||||||
"common.attachment": "anexo",
|
"common.attachment": "anexo",
|
||||||
"prompt.placeholder.shell": "Digite comando do shell...",
|
"prompt.placeholder.shell": "Digite comando do shell... {{example}}",
|
||||||
"prompt.placeholder.normal": 'Pergunte qualquer coisa... "{{example}}"',
|
"prompt.placeholder.normal": 'Pergunte qualquer coisa... "{{example}}"',
|
||||||
"prompt.placeholder.simple": "Pergunte qualquer coisa...",
|
"prompt.placeholder.simple": "Pergunte qualquer coisa...",
|
||||||
"prompt.placeholder.summarizeComments": "Resumir comentários…",
|
"prompt.placeholder.summarizeComments": "Resumir comentários…",
|
||||||
|
|
@ -403,6 +403,8 @@ export const dict = {
|
||||||
"error.page.description": "Ocorreu um erro ao carregar a aplicação.",
|
"error.page.description": "Ocorreu um erro ao carregar a aplicação.",
|
||||||
"error.page.details.label": "Detalhes do Erro",
|
"error.page.details.label": "Detalhes do Erro",
|
||||||
"error.page.action.restart": "Reiniciar",
|
"error.page.action.restart": "Reiniciar",
|
||||||
|
"error.page.action.report": "Reportar erro",
|
||||||
|
"error.page.action.reported": "Erro reportado",
|
||||||
"error.page.action.checking": "Verificando...",
|
"error.page.action.checking": "Verificando...",
|
||||||
"error.page.action.checkUpdates": "Verificar atualizações",
|
"error.page.action.checkUpdates": "Verificar atualizações",
|
||||||
"error.page.action.updateTo": "Atualizar para {{version}}",
|
"error.page.action.updateTo": "Atualizar para {{version}}",
|
||||||
|
|
@ -732,8 +734,6 @@ export const dict = {
|
||||||
"settings.permissions.tool.webfetch.description": "Buscar conteúdo de uma URL",
|
"settings.permissions.tool.webfetch.description": "Buscar conteúdo de uma URL",
|
||||||
"settings.permissions.tool.websearch.title": "Pesquisa Web",
|
"settings.permissions.tool.websearch.title": "Pesquisa Web",
|
||||||
"settings.permissions.tool.websearch.description": "Pesquisar na web",
|
"settings.permissions.tool.websearch.description": "Pesquisar na web",
|
||||||
"settings.permissions.tool.codesearch.title": "Pesquisa de Código",
|
|
||||||
"settings.permissions.tool.codesearch.description": "Pesquisar código na web",
|
|
||||||
"settings.permissions.tool.external_directory.title": "Diretório Externo",
|
"settings.permissions.tool.external_directory.title": "Diretório Externo",
|
||||||
"settings.permissions.tool.external_directory.description": "Acessar arquivos fora do diretório do projeto",
|
"settings.permissions.tool.external_directory.description": "Acessar arquivos fora do diretório do projeto",
|
||||||
"settings.permissions.tool.doom_loop.title": "Loop Infinito",
|
"settings.permissions.tool.doom_loop.title": "Loop Infinito",
|
||||||
|
|
|
||||||
|
|
@ -228,7 +228,7 @@ export const dict = {
|
||||||
"common.default": "Podrazumijevano",
|
"common.default": "Podrazumijevano",
|
||||||
"common.attachment": "prilog",
|
"common.attachment": "prilog",
|
||||||
|
|
||||||
"prompt.placeholder.shell": "Unesi shell naredbu...",
|
"prompt.placeholder.shell": "Unesi shell naredbu... {{example}}",
|
||||||
"prompt.placeholder.normal": 'Pitaj bilo šta... "{{example}}"',
|
"prompt.placeholder.normal": 'Pitaj bilo šta... "{{example}}"',
|
||||||
"prompt.placeholder.simple": "Pitaj bilo šta...",
|
"prompt.placeholder.simple": "Pitaj bilo šta...",
|
||||||
"prompt.placeholder.summarizeComments": "Sažmi komentare…",
|
"prompt.placeholder.summarizeComments": "Sažmi komentare…",
|
||||||
|
|
@ -449,6 +449,8 @@ export const dict = {
|
||||||
"error.page.description": "Došlo je do greške prilikom učitavanja aplikacije.",
|
"error.page.description": "Došlo je do greške prilikom učitavanja aplikacije.",
|
||||||
"error.page.details.label": "Detalji greške",
|
"error.page.details.label": "Detalji greške",
|
||||||
"error.page.action.restart": "Restartuj",
|
"error.page.action.restart": "Restartuj",
|
||||||
|
"error.page.action.report": "Prijavi grešku",
|
||||||
|
"error.page.action.reported": "Greška prijavljena",
|
||||||
"error.page.action.checking": "Provjera...",
|
"error.page.action.checking": "Provjera...",
|
||||||
"error.page.action.checkUpdates": "Provjeri ažuriranja",
|
"error.page.action.checkUpdates": "Provjeri ažuriranja",
|
||||||
"error.page.action.updateTo": "Ažuriraj na {{version}}",
|
"error.page.action.updateTo": "Ažuriraj na {{version}}",
|
||||||
|
|
@ -806,8 +808,6 @@ export const dict = {
|
||||||
"settings.permissions.tool.webfetch.description": "Preuzmi sadržaj sa URL-a",
|
"settings.permissions.tool.webfetch.description": "Preuzmi sadržaj sa URL-a",
|
||||||
"settings.permissions.tool.websearch.title": "Web pretraga",
|
"settings.permissions.tool.websearch.title": "Web pretraga",
|
||||||
"settings.permissions.tool.websearch.description": "Pretražuj web",
|
"settings.permissions.tool.websearch.description": "Pretražuj web",
|
||||||
"settings.permissions.tool.codesearch.title": "Pretraga koda",
|
|
||||||
"settings.permissions.tool.codesearch.description": "Pretraži kod na webu",
|
|
||||||
"settings.permissions.tool.external_directory.title": "Vanjski direktorij",
|
"settings.permissions.tool.external_directory.title": "Vanjski direktorij",
|
||||||
"settings.permissions.tool.external_directory.description": "Pristup datotekama izvan direktorija projekta",
|
"settings.permissions.tool.external_directory.description": "Pristup datotekama izvan direktorija projekta",
|
||||||
"settings.permissions.tool.doom_loop.title": "Beskonačna petlja",
|
"settings.permissions.tool.doom_loop.title": "Beskonačna petlja",
|
||||||
|
|
|
||||||
|
|
@ -226,7 +226,7 @@ export const dict = {
|
||||||
"common.default": "Standard",
|
"common.default": "Standard",
|
||||||
"common.attachment": "vedhæftning",
|
"common.attachment": "vedhæftning",
|
||||||
|
|
||||||
"prompt.placeholder.shell": "Indtast shell-kommando...",
|
"prompt.placeholder.shell": "Indtast shell-kommando... {{example}}",
|
||||||
"prompt.placeholder.normal": 'Spørg om hvad som helst... "{{example}}"',
|
"prompt.placeholder.normal": 'Spørg om hvad som helst... "{{example}}"',
|
||||||
"prompt.placeholder.simple": "Spørg om hvad som helst...",
|
"prompt.placeholder.simple": "Spørg om hvad som helst...",
|
||||||
"prompt.placeholder.summarizeComments": "Opsummér kommentarer…",
|
"prompt.placeholder.summarizeComments": "Opsummér kommentarer…",
|
||||||
|
|
@ -446,6 +446,8 @@ export const dict = {
|
||||||
"error.page.description": "Der opstod en fejl under indlæsning af applikationen.",
|
"error.page.description": "Der opstod en fejl under indlæsning af applikationen.",
|
||||||
"error.page.details.label": "Fejldetaljer",
|
"error.page.details.label": "Fejldetaljer",
|
||||||
"error.page.action.restart": "Genstart",
|
"error.page.action.restart": "Genstart",
|
||||||
|
"error.page.action.report": "Rapportér fejl",
|
||||||
|
"error.page.action.reported": "Fejl rapporteret",
|
||||||
"error.page.action.checking": "Tjekker...",
|
"error.page.action.checking": "Tjekker...",
|
||||||
"error.page.action.checkUpdates": "Tjek for opdateringer",
|
"error.page.action.checkUpdates": "Tjek for opdateringer",
|
||||||
"error.page.action.updateTo": "Opdater til {{version}}",
|
"error.page.action.updateTo": "Opdater til {{version}}",
|
||||||
|
|
@ -800,8 +802,6 @@ export const dict = {
|
||||||
"settings.permissions.tool.webfetch.description": "Hent indhold fra en URL",
|
"settings.permissions.tool.webfetch.description": "Hent indhold fra en URL",
|
||||||
"settings.permissions.tool.websearch.title": "Websøgning",
|
"settings.permissions.tool.websearch.title": "Websøgning",
|
||||||
"settings.permissions.tool.websearch.description": "Søg på nettet",
|
"settings.permissions.tool.websearch.description": "Søg på nettet",
|
||||||
"settings.permissions.tool.codesearch.title": "Kodesøgning",
|
|
||||||
"settings.permissions.tool.codesearch.description": "Søg kode på nettet",
|
|
||||||
"settings.permissions.tool.external_directory.title": "Ekstern mappe",
|
"settings.permissions.tool.external_directory.title": "Ekstern mappe",
|
||||||
"settings.permissions.tool.external_directory.description": "Få adgang til filer uden for projektmappen",
|
"settings.permissions.tool.external_directory.description": "Få adgang til filer uden for projektmappen",
|
||||||
"settings.permissions.tool.doom_loop.title": "Doom Loop",
|
"settings.permissions.tool.doom_loop.title": "Doom Loop",
|
||||||
|
|
|
||||||
|
|
@ -215,7 +215,7 @@ export const dict = {
|
||||||
"common.saving": "Speichert...",
|
"common.saving": "Speichert...",
|
||||||
"common.default": "Standard",
|
"common.default": "Standard",
|
||||||
"common.attachment": "Anhang",
|
"common.attachment": "Anhang",
|
||||||
"prompt.placeholder.shell": "Shell-Befehl eingeben...",
|
"prompt.placeholder.shell": "Shell-Befehl eingeben... {{example}}",
|
||||||
"prompt.placeholder.normal": 'Fragen Sie alles... "{{example}}"',
|
"prompt.placeholder.normal": 'Fragen Sie alles... "{{example}}"',
|
||||||
"prompt.placeholder.simple": "Fragen Sie alles...",
|
"prompt.placeholder.simple": "Fragen Sie alles...",
|
||||||
"prompt.placeholder.summarizeComments": "Kommentare zusammenfassen…",
|
"prompt.placeholder.summarizeComments": "Kommentare zusammenfassen…",
|
||||||
|
|
@ -410,6 +410,8 @@ export const dict = {
|
||||||
"error.page.description": "Beim Laden der Anwendung ist ein Fehler aufgetreten.",
|
"error.page.description": "Beim Laden der Anwendung ist ein Fehler aufgetreten.",
|
||||||
"error.page.details.label": "Fehlerdetails",
|
"error.page.details.label": "Fehlerdetails",
|
||||||
"error.page.action.restart": "Neustart",
|
"error.page.action.restart": "Neustart",
|
||||||
|
"error.page.action.report": "Fehler melden",
|
||||||
|
"error.page.action.reported": "Fehler gemeldet",
|
||||||
"error.page.action.checking": "Prüfen...",
|
"error.page.action.checking": "Prüfen...",
|
||||||
"error.page.action.checkUpdates": "Nach Updates suchen",
|
"error.page.action.checkUpdates": "Nach Updates suchen",
|
||||||
"error.page.action.updateTo": "Auf {{version}} aktualisieren",
|
"error.page.action.updateTo": "Auf {{version}} aktualisieren",
|
||||||
|
|
@ -743,8 +745,6 @@ export const dict = {
|
||||||
"settings.permissions.tool.webfetch.description": "Inhalt von einer URL abrufen",
|
"settings.permissions.tool.webfetch.description": "Inhalt von einer URL abrufen",
|
||||||
"settings.permissions.tool.websearch.title": "Web-Suche",
|
"settings.permissions.tool.websearch.title": "Web-Suche",
|
||||||
"settings.permissions.tool.websearch.description": "Das Web durchsuchen",
|
"settings.permissions.tool.websearch.description": "Das Web durchsuchen",
|
||||||
"settings.permissions.tool.codesearch.title": "Code-Suche",
|
|
||||||
"settings.permissions.tool.codesearch.description": "Code im Web durchsuchen",
|
|
||||||
"settings.permissions.tool.external_directory.title": "Externes Verzeichnis",
|
"settings.permissions.tool.external_directory.title": "Externes Verzeichnis",
|
||||||
"settings.permissions.tool.external_directory.description": "Zugriff auf Dateien außerhalb des Projektverzeichnisses",
|
"settings.permissions.tool.external_directory.description": "Zugriff auf Dateien außerhalb des Projektverzeichnisses",
|
||||||
"settings.permissions.tool.doom_loop.title": "Doom Loop",
|
"settings.permissions.tool.doom_loop.title": "Doom Loop",
|
||||||
|
|
|
||||||
|
|
@ -230,7 +230,7 @@ export const dict = {
|
||||||
"common.default": "Default",
|
"common.default": "Default",
|
||||||
"common.attachment": "attachment",
|
"common.attachment": "attachment",
|
||||||
|
|
||||||
"prompt.placeholder.shell": "Enter shell command...",
|
"prompt.placeholder.shell": "Enter shell command... {{example}}",
|
||||||
"prompt.placeholder.normal": 'Ask anything... "{{example}}"',
|
"prompt.placeholder.normal": 'Ask anything... "{{example}}"',
|
||||||
"prompt.placeholder.simple": "Ask anything...",
|
"prompt.placeholder.simple": "Ask anything...",
|
||||||
"prompt.placeholder.summarizeComments": "Summarize comments…",
|
"prompt.placeholder.summarizeComments": "Summarize comments…",
|
||||||
|
|
@ -465,6 +465,8 @@ export const dict = {
|
||||||
"error.page.description": "An error occurred while loading the application.",
|
"error.page.description": "An error occurred while loading the application.",
|
||||||
"error.page.details.label": "Error Details",
|
"error.page.details.label": "Error Details",
|
||||||
"error.page.action.restart": "Restart",
|
"error.page.action.restart": "Restart",
|
||||||
|
"error.page.action.report": "Report Error",
|
||||||
|
"error.page.action.reported": "Error Reported",
|
||||||
"error.page.action.checking": "Checking...",
|
"error.page.action.checking": "Checking...",
|
||||||
"error.page.action.checkUpdates": "Check for updates",
|
"error.page.action.checkUpdates": "Check for updates",
|
||||||
"error.page.action.updateTo": "Update to {{version}}",
|
"error.page.action.updateTo": "Update to {{version}}",
|
||||||
|
|
@ -728,6 +730,11 @@ export const dict = {
|
||||||
|
|
||||||
"settings.general.row.language.title": "Language",
|
"settings.general.row.language.title": "Language",
|
||||||
"settings.general.row.language.description": "Change the display language for OpenCode",
|
"settings.general.row.language.description": "Change the display language for OpenCode",
|
||||||
|
"settings.general.row.shell.title": "Terminal Shell",
|
||||||
|
"settings.general.row.shell.description":
|
||||||
|
"Choose the shell used for your terminal. Compatible shells are also used for agent tool calls.",
|
||||||
|
"settings.general.row.shell.autoDefault": "Auto (Default)",
|
||||||
|
"settings.general.row.shell.terminalOnly": "terminal only",
|
||||||
"settings.general.row.appearance.title": "Appearance",
|
"settings.general.row.appearance.title": "Appearance",
|
||||||
"settings.general.row.appearance.description": "Customise how OpenCode looks on your device",
|
"settings.general.row.appearance.description": "Customise how OpenCode looks on your device",
|
||||||
"settings.general.row.colorScheme.title": "Color scheme",
|
"settings.general.row.colorScheme.title": "Color scheme",
|
||||||
|
|
@ -915,8 +922,6 @@ export const dict = {
|
||||||
"settings.permissions.tool.webfetch.description": "Fetch content from a URL",
|
"settings.permissions.tool.webfetch.description": "Fetch content from a URL",
|
||||||
"settings.permissions.tool.websearch.title": "Web Search",
|
"settings.permissions.tool.websearch.title": "Web Search",
|
||||||
"settings.permissions.tool.websearch.description": "Search the web",
|
"settings.permissions.tool.websearch.description": "Search the web",
|
||||||
"settings.permissions.tool.codesearch.title": "Code Search",
|
|
||||||
"settings.permissions.tool.codesearch.description": "Search code on the web",
|
|
||||||
"settings.permissions.tool.external_directory.title": "External Directory",
|
"settings.permissions.tool.external_directory.title": "External Directory",
|
||||||
"settings.permissions.tool.external_directory.description": "Access files outside the project directory",
|
"settings.permissions.tool.external_directory.description": "Access files outside the project directory",
|
||||||
"settings.permissions.tool.doom_loop.title": "Doom Loop",
|
"settings.permissions.tool.doom_loop.title": "Doom Loop",
|
||||||
|
|
|
||||||
|
|
@ -227,7 +227,7 @@ export const dict = {
|
||||||
"common.default": "Predeterminado",
|
"common.default": "Predeterminado",
|
||||||
"common.attachment": "adjunto",
|
"common.attachment": "adjunto",
|
||||||
|
|
||||||
"prompt.placeholder.shell": "Introduce comando de shell...",
|
"prompt.placeholder.shell": "Introduce comando de shell... {{example}}",
|
||||||
"prompt.placeholder.normal": 'Pregunta cualquier cosa... "{{example}}"',
|
"prompt.placeholder.normal": 'Pregunta cualquier cosa... "{{example}}"',
|
||||||
"prompt.placeholder.simple": "Pregunta cualquier cosa...",
|
"prompt.placeholder.simple": "Pregunta cualquier cosa...",
|
||||||
"prompt.placeholder.summarizeComments": "Resumir comentarios…",
|
"prompt.placeholder.summarizeComments": "Resumir comentarios…",
|
||||||
|
|
@ -449,6 +449,8 @@ export const dict = {
|
||||||
"error.page.description": "Ocurrió un error al cargar la aplicación.",
|
"error.page.description": "Ocurrió un error al cargar la aplicación.",
|
||||||
"error.page.details.label": "Detalles del error",
|
"error.page.details.label": "Detalles del error",
|
||||||
"error.page.action.restart": "Reiniciar",
|
"error.page.action.restart": "Reiniciar",
|
||||||
|
"error.page.action.report": "Informar error",
|
||||||
|
"error.page.action.reported": "Error informado",
|
||||||
"error.page.action.checking": "Comprobando...",
|
"error.page.action.checking": "Comprobando...",
|
||||||
"error.page.action.checkUpdates": "Buscar actualizaciones",
|
"error.page.action.checkUpdates": "Buscar actualizaciones",
|
||||||
"error.page.action.updateTo": "Actualizar a {{version}}",
|
"error.page.action.updateTo": "Actualizar a {{version}}",
|
||||||
|
|
@ -813,8 +815,6 @@ export const dict = {
|
||||||
"settings.permissions.tool.webfetch.description": "Obtener contenido de una URL",
|
"settings.permissions.tool.webfetch.description": "Obtener contenido de una URL",
|
||||||
"settings.permissions.tool.websearch.title": "Búsqueda Web",
|
"settings.permissions.tool.websearch.title": "Búsqueda Web",
|
||||||
"settings.permissions.tool.websearch.description": "Buscar en la web",
|
"settings.permissions.tool.websearch.description": "Buscar en la web",
|
||||||
"settings.permissions.tool.codesearch.title": "Búsqueda de Código",
|
|
||||||
"settings.permissions.tool.codesearch.description": "Buscar código en la web",
|
|
||||||
"settings.permissions.tool.external_directory.title": "Directorio Externo",
|
"settings.permissions.tool.external_directory.title": "Directorio Externo",
|
||||||
"settings.permissions.tool.external_directory.description": "Acceder a archivos fuera del directorio del proyecto",
|
"settings.permissions.tool.external_directory.description": "Acceder a archivos fuera del directorio del proyecto",
|
||||||
"settings.permissions.tool.doom_loop.title": "Bucle Infinito",
|
"settings.permissions.tool.doom_loop.title": "Bucle Infinito",
|
||||||
|
|
|
||||||
|
|
@ -210,7 +210,7 @@ export const dict = {
|
||||||
"common.saving": "Enregistrement...",
|
"common.saving": "Enregistrement...",
|
||||||
"common.default": "Défaut",
|
"common.default": "Défaut",
|
||||||
"common.attachment": "pièce jointe",
|
"common.attachment": "pièce jointe",
|
||||||
"prompt.placeholder.shell": "Entrez une commande shell...",
|
"prompt.placeholder.shell": "Entrez une commande shell... {{example}}",
|
||||||
"prompt.placeholder.normal": 'Demandez n\'importe quoi... "{{example}}"',
|
"prompt.placeholder.normal": 'Demandez n\'importe quoi... "{{example}}"',
|
||||||
"prompt.placeholder.simple": "Demandez n'importe quoi...",
|
"prompt.placeholder.simple": "Demandez n'importe quoi...",
|
||||||
"prompt.placeholder.summarizeComments": "Résumer les commentaires…",
|
"prompt.placeholder.summarizeComments": "Résumer les commentaires…",
|
||||||
|
|
@ -406,6 +406,8 @@ export const dict = {
|
||||||
"error.page.description": "Une erreur s'est produite lors du chargement de l'application.",
|
"error.page.description": "Une erreur s'est produite lors du chargement de l'application.",
|
||||||
"error.page.details.label": "Détails de l'erreur",
|
"error.page.details.label": "Détails de l'erreur",
|
||||||
"error.page.action.restart": "Redémarrer",
|
"error.page.action.restart": "Redémarrer",
|
||||||
|
"error.page.action.report": "Signaler l'erreur",
|
||||||
|
"error.page.action.reported": "Erreur signalée",
|
||||||
"error.page.action.checking": "Vérification...",
|
"error.page.action.checking": "Vérification...",
|
||||||
"error.page.action.checkUpdates": "Vérifier les mises à jour",
|
"error.page.action.checkUpdates": "Vérifier les mises à jour",
|
||||||
"error.page.action.updateTo": "Mettre à jour vers {{version}}",
|
"error.page.action.updateTo": "Mettre à jour vers {{version}}",
|
||||||
|
|
@ -741,8 +743,6 @@ export const dict = {
|
||||||
"settings.permissions.tool.webfetch.description": "Récupérer le contenu d'une URL",
|
"settings.permissions.tool.webfetch.description": "Récupérer le contenu d'une URL",
|
||||||
"settings.permissions.tool.websearch.title": "Recherche Web",
|
"settings.permissions.tool.websearch.title": "Recherche Web",
|
||||||
"settings.permissions.tool.websearch.description": "Rechercher sur le web",
|
"settings.permissions.tool.websearch.description": "Rechercher sur le web",
|
||||||
"settings.permissions.tool.codesearch.title": "Recherche de code",
|
|
||||||
"settings.permissions.tool.codesearch.description": "Rechercher du code sur le web",
|
|
||||||
"settings.permissions.tool.external_directory.title": "Répertoire externe",
|
"settings.permissions.tool.external_directory.title": "Répertoire externe",
|
||||||
"settings.permissions.tool.external_directory.description": "Accéder aux fichiers en dehors du répertoire du projet",
|
"settings.permissions.tool.external_directory.description": "Accéder aux fichiers en dehors du répertoire du projet",
|
||||||
"settings.permissions.tool.doom_loop.title": "Boucle infernale",
|
"settings.permissions.tool.doom_loop.title": "Boucle infernale",
|
||||||
|
|
|
||||||
|
|
@ -209,7 +209,7 @@ export const dict = {
|
||||||
"common.saving": "保存中...",
|
"common.saving": "保存中...",
|
||||||
"common.default": "デフォルト",
|
"common.default": "デフォルト",
|
||||||
"common.attachment": "添付ファイル",
|
"common.attachment": "添付ファイル",
|
||||||
"prompt.placeholder.shell": "シェルコマンドを入力...",
|
"prompt.placeholder.shell": "シェルコマンドを入力... {{example}}",
|
||||||
"prompt.placeholder.normal": '何でも聞いてください... "{{example}}"',
|
"prompt.placeholder.normal": '何でも聞いてください... "{{example}}"',
|
||||||
"prompt.placeholder.simple": "何でも聞いてください...",
|
"prompt.placeholder.simple": "何でも聞いてください...",
|
||||||
"prompt.placeholder.summarizeComments": "コメントを要約…",
|
"prompt.placeholder.summarizeComments": "コメントを要約…",
|
||||||
|
|
@ -402,6 +402,8 @@ export const dict = {
|
||||||
"error.page.description": "アプリケーションの読み込み中にエラーが発生しました。",
|
"error.page.description": "アプリケーションの読み込み中にエラーが発生しました。",
|
||||||
"error.page.details.label": "エラー詳細",
|
"error.page.details.label": "エラー詳細",
|
||||||
"error.page.action.restart": "再起動",
|
"error.page.action.restart": "再起動",
|
||||||
|
"error.page.action.report": "エラーを報告",
|
||||||
|
"error.page.action.reported": "エラーを報告しました",
|
||||||
"error.page.action.checking": "確認中...",
|
"error.page.action.checking": "確認中...",
|
||||||
"error.page.action.checkUpdates": "アップデートを確認",
|
"error.page.action.checkUpdates": "アップデートを確認",
|
||||||
"error.page.action.updateTo": "{{version}}にアップデート",
|
"error.page.action.updateTo": "{{version}}にアップデート",
|
||||||
|
|
@ -727,8 +729,6 @@ export const dict = {
|
||||||
"settings.permissions.tool.webfetch.description": "URLからコンテンツを取得",
|
"settings.permissions.tool.webfetch.description": "URLからコンテンツを取得",
|
||||||
"settings.permissions.tool.websearch.title": "Web検索",
|
"settings.permissions.tool.websearch.title": "Web検索",
|
||||||
"settings.permissions.tool.websearch.description": "ウェブを検索",
|
"settings.permissions.tool.websearch.description": "ウェブを検索",
|
||||||
"settings.permissions.tool.codesearch.title": "コード検索",
|
|
||||||
"settings.permissions.tool.codesearch.description": "ウェブ上のコードを検索",
|
|
||||||
"settings.permissions.tool.external_directory.title": "外部ディレクトリ",
|
"settings.permissions.tool.external_directory.title": "外部ディレクトリ",
|
||||||
"settings.permissions.tool.external_directory.description": "プロジェクトディレクトリ外のファイルへのアクセス",
|
"settings.permissions.tool.external_directory.description": "プロジェクトディレクトリ外のファイルへのアクセス",
|
||||||
"settings.permissions.tool.doom_loop.title": "無限ループ",
|
"settings.permissions.tool.doom_loop.title": "無限ループ",
|
||||||
|
|
|
||||||
|
|
@ -209,7 +209,7 @@ export const dict = {
|
||||||
"common.saving": "저장 중...",
|
"common.saving": "저장 중...",
|
||||||
"common.default": "기본값",
|
"common.default": "기본값",
|
||||||
"common.attachment": "첨부 파일",
|
"common.attachment": "첨부 파일",
|
||||||
"prompt.placeholder.shell": "셸 명령어 입력...",
|
"prompt.placeholder.shell": "셸 명령어 입력... {{example}}",
|
||||||
"prompt.placeholder.normal": '무엇이든 물어보세요... "{{example}}"',
|
"prompt.placeholder.normal": '무엇이든 물어보세요... "{{example}}"',
|
||||||
"prompt.placeholder.simple": "무엇이든 물어보세요...",
|
"prompt.placeholder.simple": "무엇이든 물어보세요...",
|
||||||
"prompt.placeholder.summarizeComments": "댓글 요약…",
|
"prompt.placeholder.summarizeComments": "댓글 요약…",
|
||||||
|
|
@ -401,6 +401,8 @@ export const dict = {
|
||||||
"error.page.description": "애플리케이션을 로드하는 동안 오류가 발생했습니다.",
|
"error.page.description": "애플리케이션을 로드하는 동안 오류가 발생했습니다.",
|
||||||
"error.page.details.label": "오류 세부 정보",
|
"error.page.details.label": "오류 세부 정보",
|
||||||
"error.page.action.restart": "다시 시작",
|
"error.page.action.restart": "다시 시작",
|
||||||
|
"error.page.action.report": "오류 신고",
|
||||||
|
"error.page.action.reported": "오류가 신고됨",
|
||||||
"error.page.action.checking": "확인 중...",
|
"error.page.action.checking": "확인 중...",
|
||||||
"error.page.action.checkUpdates": "업데이트 확인",
|
"error.page.action.checkUpdates": "업데이트 확인",
|
||||||
"error.page.action.updateTo": "{{version}} 버전으로 업데이트",
|
"error.page.action.updateTo": "{{version}} 버전으로 업데이트",
|
||||||
|
|
@ -722,8 +724,6 @@ export const dict = {
|
||||||
"settings.permissions.tool.webfetch.description": "URL에서 콘텐츠 가져오기",
|
"settings.permissions.tool.webfetch.description": "URL에서 콘텐츠 가져오기",
|
||||||
"settings.permissions.tool.websearch.title": "웹 검색",
|
"settings.permissions.tool.websearch.title": "웹 검색",
|
||||||
"settings.permissions.tool.websearch.description": "웹 검색",
|
"settings.permissions.tool.websearch.description": "웹 검색",
|
||||||
"settings.permissions.tool.codesearch.title": "코드 검색",
|
|
||||||
"settings.permissions.tool.codesearch.description": "웹에서 코드 검색",
|
|
||||||
"settings.permissions.tool.external_directory.title": "외부 디렉터리",
|
"settings.permissions.tool.external_directory.title": "외부 디렉터리",
|
||||||
"settings.permissions.tool.external_directory.description": "프로젝트 디렉터리 외부의 파일에 액세스",
|
"settings.permissions.tool.external_directory.description": "프로젝트 디렉터리 외부의 파일에 액세스",
|
||||||
"settings.permissions.tool.doom_loop.title": "무한 반복",
|
"settings.permissions.tool.doom_loop.title": "무한 반복",
|
||||||
|
|
|
||||||
|
|
@ -230,7 +230,7 @@ export const dict = {
|
||||||
"common.default": "Standard",
|
"common.default": "Standard",
|
||||||
"common.attachment": "vedlegg",
|
"common.attachment": "vedlegg",
|
||||||
|
|
||||||
"prompt.placeholder.shell": "Skriv inn shell-kommando...",
|
"prompt.placeholder.shell": "Skriv inn shell-kommando... {{example}}",
|
||||||
"prompt.placeholder.normal": 'Spør om hva som helst... "{{example}}"',
|
"prompt.placeholder.normal": 'Spør om hva som helst... "{{example}}"',
|
||||||
"prompt.placeholder.simple": "Spør om hva som helst...",
|
"prompt.placeholder.simple": "Spør om hva som helst...",
|
||||||
"prompt.placeholder.summarizeComments": "Oppsummer kommentarer…",
|
"prompt.placeholder.summarizeComments": "Oppsummer kommentarer…",
|
||||||
|
|
@ -450,6 +450,8 @@ export const dict = {
|
||||||
"error.page.description": "Det oppstod en feil under lasting av applikasjonen.",
|
"error.page.description": "Det oppstod en feil under lasting av applikasjonen.",
|
||||||
"error.page.details.label": "Feildetaljer",
|
"error.page.details.label": "Feildetaljer",
|
||||||
"error.page.action.restart": "Start på nytt",
|
"error.page.action.restart": "Start på nytt",
|
||||||
|
"error.page.action.report": "Rapporter feil",
|
||||||
|
"error.page.action.reported": "Feil rapportert",
|
||||||
"error.page.action.checking": "Sjekker...",
|
"error.page.action.checking": "Sjekker...",
|
||||||
"error.page.action.checkUpdates": "Se etter oppdateringer",
|
"error.page.action.checkUpdates": "Se etter oppdateringer",
|
||||||
"error.page.action.updateTo": "Oppdater til {{version}}",
|
"error.page.action.updateTo": "Oppdater til {{version}}",
|
||||||
|
|
@ -807,8 +809,6 @@ export const dict = {
|
||||||
"settings.permissions.tool.webfetch.description": "Hent innhold fra en URL",
|
"settings.permissions.tool.webfetch.description": "Hent innhold fra en URL",
|
||||||
"settings.permissions.tool.websearch.title": "Websøk",
|
"settings.permissions.tool.websearch.title": "Websøk",
|
||||||
"settings.permissions.tool.websearch.description": "Søk på nettet",
|
"settings.permissions.tool.websearch.description": "Søk på nettet",
|
||||||
"settings.permissions.tool.codesearch.title": "Kodesøk",
|
|
||||||
"settings.permissions.tool.codesearch.description": "Søk etter kode på nettet",
|
|
||||||
"settings.permissions.tool.external_directory.title": "Ekstern mappe",
|
"settings.permissions.tool.external_directory.title": "Ekstern mappe",
|
||||||
"settings.permissions.tool.external_directory.description": "Få tilgang til filer utenfor prosjektmappen",
|
"settings.permissions.tool.external_directory.description": "Få tilgang til filer utenfor prosjektmappen",
|
||||||
"settings.permissions.tool.doom_loop.title": "Doom Loop",
|
"settings.permissions.tool.doom_loop.title": "Doom Loop",
|
||||||
|
|
|
||||||
|
|
@ -211,7 +211,7 @@ export const dict = {
|
||||||
"common.saving": "Zapisywanie...",
|
"common.saving": "Zapisywanie...",
|
||||||
"common.default": "Domyślny",
|
"common.default": "Domyślny",
|
||||||
"common.attachment": "załącznik",
|
"common.attachment": "załącznik",
|
||||||
"prompt.placeholder.shell": "Wpisz polecenie terminala...",
|
"prompt.placeholder.shell": "Wpisz polecenie terminala... {{example}}",
|
||||||
"prompt.placeholder.normal": 'Zapytaj o cokolwiek... "{{example}}"',
|
"prompt.placeholder.normal": 'Zapytaj o cokolwiek... "{{example}}"',
|
||||||
"prompt.placeholder.simple": "Zapytaj o cokolwiek...",
|
"prompt.placeholder.simple": "Zapytaj o cokolwiek...",
|
||||||
"prompt.placeholder.summarizeComments": "Podsumuj komentarze…",
|
"prompt.placeholder.summarizeComments": "Podsumuj komentarze…",
|
||||||
|
|
@ -403,6 +403,8 @@ export const dict = {
|
||||||
"error.page.description": "Wystąpił błąd podczas ładowania aplikacji.",
|
"error.page.description": "Wystąpił błąd podczas ładowania aplikacji.",
|
||||||
"error.page.details.label": "Szczegóły błędu",
|
"error.page.details.label": "Szczegóły błędu",
|
||||||
"error.page.action.restart": "Restartuj",
|
"error.page.action.restart": "Restartuj",
|
||||||
|
"error.page.action.report": "Zgłoś błąd",
|
||||||
|
"error.page.action.reported": "Błąd zgłoszony",
|
||||||
"error.page.action.checking": "Sprawdzanie...",
|
"error.page.action.checking": "Sprawdzanie...",
|
||||||
"error.page.action.checkUpdates": "Sprawdź aktualizacje",
|
"error.page.action.checkUpdates": "Sprawdź aktualizacje",
|
||||||
"error.page.action.updateTo": "Zaktualizuj do {{version}}",
|
"error.page.action.updateTo": "Zaktualizuj do {{version}}",
|
||||||
|
|
@ -729,8 +731,6 @@ export const dict = {
|
||||||
"settings.permissions.tool.webfetch.description": "Pobieranie zawartości z adresu URL",
|
"settings.permissions.tool.webfetch.description": "Pobieranie zawartości z adresu URL",
|
||||||
"settings.permissions.tool.websearch.title": "Wyszukiwanie w sieci",
|
"settings.permissions.tool.websearch.title": "Wyszukiwanie w sieci",
|
||||||
"settings.permissions.tool.websearch.description": "Przeszukiwanie sieci",
|
"settings.permissions.tool.websearch.description": "Przeszukiwanie sieci",
|
||||||
"settings.permissions.tool.codesearch.title": "Wyszukiwanie kodu",
|
|
||||||
"settings.permissions.tool.codesearch.description": "Przeszukiwanie kodu w sieci",
|
|
||||||
"settings.permissions.tool.external_directory.title": "Katalog zewnętrzny",
|
"settings.permissions.tool.external_directory.title": "Katalog zewnętrzny",
|
||||||
"settings.permissions.tool.external_directory.description": "Dostęp do plików poza katalogiem projektu",
|
"settings.permissions.tool.external_directory.description": "Dostęp do plików poza katalogiem projektu",
|
||||||
"settings.permissions.tool.doom_loop.title": "Zapętlenie",
|
"settings.permissions.tool.doom_loop.title": "Zapętlenie",
|
||||||
|
|
|
||||||
|
|
@ -227,7 +227,7 @@ export const dict = {
|
||||||
"common.default": "По умолчанию",
|
"common.default": "По умолчанию",
|
||||||
"common.attachment": "вложение",
|
"common.attachment": "вложение",
|
||||||
|
|
||||||
"prompt.placeholder.shell": "Введите команду оболочки...",
|
"prompt.placeholder.shell": "Введите команду оболочки... {{example}}",
|
||||||
"prompt.placeholder.normal": 'Спросите что угодно... "{{example}}"',
|
"prompt.placeholder.normal": 'Спросите что угодно... "{{example}}"',
|
||||||
"prompt.placeholder.simple": "Спросите что угодно...",
|
"prompt.placeholder.simple": "Спросите что угодно...",
|
||||||
"prompt.placeholder.summarizeComments": "Суммировать комментарии…",
|
"prompt.placeholder.summarizeComments": "Суммировать комментарии…",
|
||||||
|
|
@ -448,6 +448,8 @@ export const dict = {
|
||||||
"error.page.description": "Произошла ошибка при загрузке приложения.",
|
"error.page.description": "Произошла ошибка при загрузке приложения.",
|
||||||
"error.page.details.label": "Детали ошибки",
|
"error.page.details.label": "Детали ошибки",
|
||||||
"error.page.action.restart": "Перезапустить",
|
"error.page.action.restart": "Перезапустить",
|
||||||
|
"error.page.action.report": "Сообщить об ошибке",
|
||||||
|
"error.page.action.reported": "Об ошибке сообщено",
|
||||||
"error.page.action.checking": "Проверка...",
|
"error.page.action.checking": "Проверка...",
|
||||||
"error.page.action.checkUpdates": "Проверить обновления",
|
"error.page.action.checkUpdates": "Проверить обновления",
|
||||||
"error.page.action.updateTo": "Обновить до {{version}}",
|
"error.page.action.updateTo": "Обновить до {{version}}",
|
||||||
|
|
@ -808,8 +810,6 @@ export const dict = {
|
||||||
"settings.permissions.tool.webfetch.description": "Получение контента по URL",
|
"settings.permissions.tool.webfetch.description": "Получение контента по URL",
|
||||||
"settings.permissions.tool.websearch.title": "Web Search",
|
"settings.permissions.tool.websearch.title": "Web Search",
|
||||||
"settings.permissions.tool.websearch.description": "Поиск в интернете",
|
"settings.permissions.tool.websearch.description": "Поиск в интернете",
|
||||||
"settings.permissions.tool.codesearch.title": "Code Search",
|
|
||||||
"settings.permissions.tool.codesearch.description": "Поиск кода в интернете",
|
|
||||||
"settings.permissions.tool.external_directory.title": "Внешняя директория",
|
"settings.permissions.tool.external_directory.title": "Внешняя директория",
|
||||||
"settings.permissions.tool.external_directory.description": "Доступ к файлам вне директории проекта",
|
"settings.permissions.tool.external_directory.description": "Доступ к файлам вне директории проекта",
|
||||||
"settings.permissions.tool.doom_loop.title": "Doom Loop",
|
"settings.permissions.tool.doom_loop.title": "Doom Loop",
|
||||||
|
|
|
||||||
|
|
@ -227,7 +227,7 @@ export const dict = {
|
||||||
"common.default": "ค่าเริ่มต้น",
|
"common.default": "ค่าเริ่มต้น",
|
||||||
"common.attachment": "ไฟล์แนบ",
|
"common.attachment": "ไฟล์แนบ",
|
||||||
|
|
||||||
"prompt.placeholder.shell": "ป้อนคำสั่งเชลล์...",
|
"prompt.placeholder.shell": "ป้อนคำสั่งเชลล์... {{example}}",
|
||||||
"prompt.placeholder.normal": 'ถามอะไรก็ได้... "{{example}}"',
|
"prompt.placeholder.normal": 'ถามอะไรก็ได้... "{{example}}"',
|
||||||
"prompt.placeholder.simple": "ถามอะไรก็ได้...",
|
"prompt.placeholder.simple": "ถามอะไรก็ได้...",
|
||||||
"prompt.placeholder.summarizeComments": "สรุปความคิดเห็น…",
|
"prompt.placeholder.summarizeComments": "สรุปความคิดเห็น…",
|
||||||
|
|
@ -447,6 +447,8 @@ export const dict = {
|
||||||
"error.page.description": "เกิดข้อผิดพลาดระหว่างการโหลดแอปพลิเคชัน",
|
"error.page.description": "เกิดข้อผิดพลาดระหว่างการโหลดแอปพลิเคชัน",
|
||||||
"error.page.details.label": "รายละเอียดข้อผิดพลาด",
|
"error.page.details.label": "รายละเอียดข้อผิดพลาด",
|
||||||
"error.page.action.restart": "รีสตาร์ท",
|
"error.page.action.restart": "รีสตาร์ท",
|
||||||
|
"error.page.action.report": "รายงานข้อผิดพลาด",
|
||||||
|
"error.page.action.reported": "รายงานข้อผิดพลาดแล้ว",
|
||||||
"error.page.action.checking": "กำลังตรวจสอบ...",
|
"error.page.action.checking": "กำลังตรวจสอบ...",
|
||||||
"error.page.action.checkUpdates": "ตรวจสอบการอัปเดต",
|
"error.page.action.checkUpdates": "ตรวจสอบการอัปเดต",
|
||||||
"error.page.action.updateTo": "อัปเดตเป็น {{version}}",
|
"error.page.action.updateTo": "อัปเดตเป็น {{version}}",
|
||||||
|
|
@ -796,8 +798,6 @@ export const dict = {
|
||||||
"settings.permissions.tool.webfetch.description": "ดึงเนื้อหาจาก URL",
|
"settings.permissions.tool.webfetch.description": "ดึงเนื้อหาจาก URL",
|
||||||
"settings.permissions.tool.websearch.title": "ค้นหาเว็บ",
|
"settings.permissions.tool.websearch.title": "ค้นหาเว็บ",
|
||||||
"settings.permissions.tool.websearch.description": "ค้นหาบนเว็บ",
|
"settings.permissions.tool.websearch.description": "ค้นหาบนเว็บ",
|
||||||
"settings.permissions.tool.codesearch.title": "ค้นหาโค้ด",
|
|
||||||
"settings.permissions.tool.codesearch.description": "ค้นหาโค้ดบนเว็บ",
|
|
||||||
"settings.permissions.tool.external_directory.title": "ไดเรกทอรีภายนอก",
|
"settings.permissions.tool.external_directory.title": "ไดเรกทอรีภายนอก",
|
||||||
"settings.permissions.tool.external_directory.description": "เข้าถึงไฟล์นอกไดเรกทอรีโปรเจกต์",
|
"settings.permissions.tool.external_directory.description": "เข้าถึงไฟล์นอกไดเรกทอรีโปรเจกต์",
|
||||||
"settings.permissions.tool.doom_loop.title": "Doom Loop",
|
"settings.permissions.tool.doom_loop.title": "Doom Loop",
|
||||||
|
|
|
||||||
|
|
@ -232,7 +232,7 @@ export const dict = {
|
||||||
"common.default": "Varsayılan",
|
"common.default": "Varsayılan",
|
||||||
"common.attachment": "ek",
|
"common.attachment": "ek",
|
||||||
|
|
||||||
"prompt.placeholder.shell": "Kabuk komutu girin...",
|
"prompt.placeholder.shell": "Kabuk komutu girin... {{example}}",
|
||||||
"prompt.placeholder.normal": 'Bir şeyler sorun... "{{example}}"',
|
"prompt.placeholder.normal": 'Bir şeyler sorun... "{{example}}"',
|
||||||
"prompt.placeholder.simple": "Bir şeyler sorun...",
|
"prompt.placeholder.simple": "Bir şeyler sorun...",
|
||||||
"prompt.placeholder.summarizeComments": "Yorumları özetle…",
|
"prompt.placeholder.summarizeComments": "Yorumları özetle…",
|
||||||
|
|
@ -452,6 +452,8 @@ export const dict = {
|
||||||
"error.page.description": "Uygulama yüklenirken bir hata oluştu.",
|
"error.page.description": "Uygulama yüklenirken bir hata oluştu.",
|
||||||
"error.page.details.label": "Hata Detayları",
|
"error.page.details.label": "Hata Detayları",
|
||||||
"error.page.action.restart": "Yeniden Başlat",
|
"error.page.action.restart": "Yeniden Başlat",
|
||||||
|
"error.page.action.report": "Hatayı Bildir",
|
||||||
|
"error.page.action.reported": "Hata Bildirildi",
|
||||||
"error.page.action.checking": "Kontrol ediliyor...",
|
"error.page.action.checking": "Kontrol ediliyor...",
|
||||||
"error.page.action.checkUpdates": "Güncellemeleri kontrol et",
|
"error.page.action.checkUpdates": "Güncellemeleri kontrol et",
|
||||||
"error.page.action.updateTo": "{{version}} sürümüne güncelle",
|
"error.page.action.updateTo": "{{version}} sürümüne güncelle",
|
||||||
|
|
@ -816,8 +818,6 @@ export const dict = {
|
||||||
"settings.permissions.tool.webfetch.description": "Bir URL'den içerik getir",
|
"settings.permissions.tool.webfetch.description": "Bir URL'den içerik getir",
|
||||||
"settings.permissions.tool.websearch.title": "Web Ara",
|
"settings.permissions.tool.websearch.title": "Web Ara",
|
||||||
"settings.permissions.tool.websearch.description": "Web'de ara",
|
"settings.permissions.tool.websearch.description": "Web'de ara",
|
||||||
"settings.permissions.tool.codesearch.title": "Kod Ara",
|
|
||||||
"settings.permissions.tool.codesearch.description": "Web'de kod ara",
|
|
||||||
"settings.permissions.tool.external_directory.title": "Harici Dizin",
|
"settings.permissions.tool.external_directory.title": "Harici Dizin",
|
||||||
"settings.permissions.tool.external_directory.description": "Proje dizini dışındaki dosyalara eriş",
|
"settings.permissions.tool.external_directory.description": "Proje dizini dışındaki dosyalara eriş",
|
||||||
"settings.permissions.tool.doom_loop.title": "Sonsuz Döngü",
|
"settings.permissions.tool.doom_loop.title": "Sonsuz Döngü",
|
||||||
|
|
|
||||||
|
|
@ -249,7 +249,7 @@ export const dict = {
|
||||||
"common.default": "默认",
|
"common.default": "默认",
|
||||||
"common.attachment": "附件",
|
"common.attachment": "附件",
|
||||||
|
|
||||||
"prompt.placeholder.shell": "输入 shell 命令...",
|
"prompt.placeholder.shell": "输入 shell 命令... {{example}}",
|
||||||
"prompt.placeholder.normal": '随便问点什么... "{{example}}"',
|
"prompt.placeholder.normal": '随便问点什么... "{{example}}"',
|
||||||
"prompt.placeholder.simple": "随便问点什么...",
|
"prompt.placeholder.simple": "随便问点什么...",
|
||||||
"prompt.placeholder.summarizeComments": "总结评论…",
|
"prompt.placeholder.summarizeComments": "总结评论…",
|
||||||
|
|
@ -452,6 +452,8 @@ export const dict = {
|
||||||
"error.page.description": "加载应用程序时发生错误。",
|
"error.page.description": "加载应用程序时发生错误。",
|
||||||
"error.page.details.label": "错误详情",
|
"error.page.details.label": "错误详情",
|
||||||
"error.page.action.restart": "重启",
|
"error.page.action.restart": "重启",
|
||||||
|
"error.page.action.report": "上报错误",
|
||||||
|
"error.page.action.reported": "错误已上报",
|
||||||
"error.page.action.checking": "检查中...",
|
"error.page.action.checking": "检查中...",
|
||||||
"error.page.action.checkUpdates": "检查更新",
|
"error.page.action.checkUpdates": "检查更新",
|
||||||
"error.page.action.updateTo": "更新到 {{version}}",
|
"error.page.action.updateTo": "更新到 {{version}}",
|
||||||
|
|
@ -793,8 +795,6 @@ export const dict = {
|
||||||
"settings.permissions.tool.webfetch.description": "从 URL 获取内容",
|
"settings.permissions.tool.webfetch.description": "从 URL 获取内容",
|
||||||
"settings.permissions.tool.websearch.title": "网页搜索",
|
"settings.permissions.tool.websearch.title": "网页搜索",
|
||||||
"settings.permissions.tool.websearch.description": "搜索网页",
|
"settings.permissions.tool.websearch.description": "搜索网页",
|
||||||
"settings.permissions.tool.codesearch.title": "代码搜索",
|
|
||||||
"settings.permissions.tool.codesearch.description": "在网上搜索代码",
|
|
||||||
"settings.permissions.tool.external_directory.title": "外部目录",
|
"settings.permissions.tool.external_directory.title": "外部目录",
|
||||||
"settings.permissions.tool.external_directory.description": "访问项目目录之外的文件",
|
"settings.permissions.tool.external_directory.description": "访问项目目录之外的文件",
|
||||||
"settings.permissions.tool.doom_loop.title": "死循环",
|
"settings.permissions.tool.doom_loop.title": "死循环",
|
||||||
|
|
|
||||||
|
|
@ -227,7 +227,7 @@ export const dict = {
|
||||||
"common.default": "預設",
|
"common.default": "預設",
|
||||||
"common.attachment": "附件",
|
"common.attachment": "附件",
|
||||||
|
|
||||||
"prompt.placeholder.shell": "輸入 shell 命令...",
|
"prompt.placeholder.shell": "輸入 shell 命令... {{example}}",
|
||||||
"prompt.placeholder.normal": '隨便問點什麼... "{{example}}"',
|
"prompt.placeholder.normal": '隨便問點什麼... "{{example}}"',
|
||||||
"prompt.placeholder.simple": "隨便問點什麼...",
|
"prompt.placeholder.simple": "隨便問點什麼...",
|
||||||
"prompt.placeholder.summarizeComments": "摘要評論…",
|
"prompt.placeholder.summarizeComments": "摘要評論…",
|
||||||
|
|
@ -445,6 +445,8 @@ export const dict = {
|
||||||
"error.page.description": "載入應用程式時發生錯誤。",
|
"error.page.description": "載入應用程式時發生錯誤。",
|
||||||
"error.page.details.label": "錯誤詳情",
|
"error.page.details.label": "錯誤詳情",
|
||||||
"error.page.action.restart": "重新啟動",
|
"error.page.action.restart": "重新啟動",
|
||||||
|
"error.page.action.report": "回報錯誤",
|
||||||
|
"error.page.action.reported": "已回報錯誤",
|
||||||
"error.page.action.checking": "檢查中...",
|
"error.page.action.checking": "檢查中...",
|
||||||
"error.page.action.checkUpdates": "檢查更新",
|
"error.page.action.checkUpdates": "檢查更新",
|
||||||
"error.page.action.updateTo": "更新到 {{version}}",
|
"error.page.action.updateTo": "更新到 {{version}}",
|
||||||
|
|
@ -789,8 +791,6 @@ export const dict = {
|
||||||
"settings.permissions.tool.webfetch.description": "從 URL 取得內容",
|
"settings.permissions.tool.webfetch.description": "從 URL 取得內容",
|
||||||
"settings.permissions.tool.websearch.title": "Web Search",
|
"settings.permissions.tool.websearch.title": "Web Search",
|
||||||
"settings.permissions.tool.websearch.description": "搜尋網頁",
|
"settings.permissions.tool.websearch.description": "搜尋網頁",
|
||||||
"settings.permissions.tool.codesearch.title": "Code Search",
|
|
||||||
"settings.permissions.tool.codesearch.description": "在網路上搜尋程式碼",
|
|
||||||
"settings.permissions.tool.external_directory.title": "外部目錄",
|
"settings.permissions.tool.external_directory.title": "外部目錄",
|
||||||
"settings.permissions.tool.external_directory.description": "存取專案目錄之外的檔案",
|
"settings.permissions.tool.external_directory.description": "存取專案目錄之外的檔案",
|
||||||
"settings.permissions.tool.doom_loop.title": "Doom Loop",
|
"settings.permissions.tool.doom_loop.title": "Doom Loop",
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
import { DataProvider } from "@opencode-ai/ui/context"
|
import { DataProvider } from "@opencode-ai/ui/context"
|
||||||
import { showToast } from "@opencode-ai/ui/toast"
|
import { showToast } from "@opencode-ai/ui/toast"
|
||||||
import { base64Encode } from "@opencode-ai/shared/util/encode"
|
import { base64Encode } from "@opencode-ai/core/util/encode"
|
||||||
import { useLocation, useNavigate, useParams } from "@solidjs/router"
|
import { useLocation, useNavigate, useParams } from "@solidjs/router"
|
||||||
import { createEffect, createMemo, createResource, type ParentProps, Show } from "solid-js"
|
import { createEffect, createMemo, createResource, type ParentProps, Show } from "solid-js"
|
||||||
import { useLanguage } from "@/context/language"
|
import { useLanguage } from "@/context/language"
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,8 @@
|
||||||
import { TextField } from "@opencode-ai/ui/text-field"
|
import { TextField } from "@opencode-ai/ui/text-field"
|
||||||
|
import * as Sentry from "@sentry/solid"
|
||||||
import { Logo } from "@opencode-ai/ui/logo"
|
import { Logo } from "@opencode-ai/ui/logo"
|
||||||
import { Button } from "@opencode-ai/ui/button"
|
import { Button } from "@opencode-ai/ui/button"
|
||||||
import { Component, Show } from "solid-js"
|
import { Component, createSignal, Show } from "solid-js"
|
||||||
import { createStore } from "solid-js/store"
|
import { createStore } from "solid-js/store"
|
||||||
import { usePlatform } from "@/context/platform"
|
import { usePlatform } from "@/context/platform"
|
||||||
import { useLanguage } from "@/context/language"
|
import { useLanguage } from "@/context/language"
|
||||||
|
|
@ -244,10 +245,9 @@ export const ErrorPage: Component<ErrorPageProps> = (props) => {
|
||||||
}
|
}
|
||||||
|
|
||||||
async function installUpdate() {
|
async function installUpdate() {
|
||||||
if (!platform.update || !platform.restart) return
|
if (!platform.updateAndRestart) return
|
||||||
await platform
|
await platform
|
||||||
.update()
|
.updateAndRestart()
|
||||||
.then(() => platform.restart!())
|
|
||||||
.then(() => setStore("actionError", undefined))
|
.then(() => setStore("actionError", undefined))
|
||||||
.catch((err) => {
|
.catch((err) => {
|
||||||
setStore("actionError", formatError(err, language.t))
|
setStore("actionError", formatError(err, language.t))
|
||||||
|
|
@ -271,10 +271,27 @@ export const ErrorPage: Component<ErrorPageProps> = (props) => {
|
||||||
label={language.t("error.page.details.label")}
|
label={language.t("error.page.details.label")}
|
||||||
hideLabel
|
hideLabel
|
||||||
/>
|
/>
|
||||||
<div class="flex items-center gap-3">
|
<div class="flex flex-row items-center justify-center gap-3 flex-wrap max-w-64">
|
||||||
<Button size="large" onClick={platform.restart}>
|
<Button size="large" onClick={platform.restart}>
|
||||||
{language.t("error.page.action.restart")}
|
{language.t("error.page.action.restart")}
|
||||||
</Button>
|
</Button>
|
||||||
|
<Show when={Sentry.isEnabled}>
|
||||||
|
{(_) => {
|
||||||
|
const [reported, setReported] = createSignal(false)
|
||||||
|
return (
|
||||||
|
<Button
|
||||||
|
size="large"
|
||||||
|
disabled={reported()}
|
||||||
|
onClick={() => {
|
||||||
|
Sentry.captureException(props.error)
|
||||||
|
setReported(true)
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{language.t(reported() ? "error.page.action.reported" : "error.page.action.report")}
|
||||||
|
</Button>
|
||||||
|
)
|
||||||
|
}}
|
||||||
|
</Show>
|
||||||
<Show when={platform.checkUpdate}>
|
<Show when={platform.checkUpdate}>
|
||||||
<Show
|
<Show
|
||||||
when={store.version}
|
when={store.version}
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@ import { Button } from "@opencode-ai/ui/button"
|
||||||
import { Logo } from "@opencode-ai/ui/logo"
|
import { Logo } from "@opencode-ai/ui/logo"
|
||||||
import { useLayout } from "@/context/layout"
|
import { useLayout } from "@/context/layout"
|
||||||
import { useNavigate } from "@solidjs/router"
|
import { useNavigate } from "@solidjs/router"
|
||||||
import { base64Encode } from "@opencode-ai/shared/util/encode"
|
import { base64Encode } from "@opencode-ai/core/util/encode"
|
||||||
import { Icon } from "@opencode-ai/ui/icon"
|
import { Icon } from "@opencode-ai/ui/icon"
|
||||||
import { usePlatform } from "@/context/platform"
|
import { usePlatform } from "@/context/platform"
|
||||||
import { DateTime } from "luxon"
|
import { DateTime } from "luxon"
|
||||||
|
|
|
||||||
|
|
@ -17,7 +17,7 @@ import { useLocation, useNavigate, useParams } from "@solidjs/router"
|
||||||
import { useLayout, LocalProject } from "@/context/layout"
|
import { useLayout, LocalProject } from "@/context/layout"
|
||||||
import { useGlobalSync } from "@/context/global-sync"
|
import { useGlobalSync } from "@/context/global-sync"
|
||||||
import { Persist, persisted } from "@/utils/persist"
|
import { Persist, persisted } from "@/utils/persist"
|
||||||
import { base64Encode } from "@opencode-ai/shared/util/encode"
|
import { base64Encode } from "@opencode-ai/core/util/encode"
|
||||||
import { decode64 } from "@/utils/base64"
|
import { decode64 } from "@/utils/base64"
|
||||||
import { ResizeHandle } from "@opencode-ai/ui/resize-handle"
|
import { ResizeHandle } from "@opencode-ai/ui/resize-handle"
|
||||||
import { Button } from "@opencode-ai/ui/button"
|
import { Button } from "@opencode-ai/ui/button"
|
||||||
|
|
@ -25,7 +25,7 @@ import { IconButton } from "@opencode-ai/ui/icon-button"
|
||||||
import { Tooltip } from "@opencode-ai/ui/tooltip"
|
import { Tooltip } from "@opencode-ai/ui/tooltip"
|
||||||
import { DropdownMenu } from "@opencode-ai/ui/dropdown-menu"
|
import { DropdownMenu } from "@opencode-ai/ui/dropdown-menu"
|
||||||
import { Dialog } from "@opencode-ai/ui/dialog"
|
import { Dialog } from "@opencode-ai/ui/dialog"
|
||||||
import { getFilename } from "@opencode-ai/shared/util/path"
|
import { getFilename } from "@opencode-ai/core/util/path"
|
||||||
import { Session, type Message } from "@opencode-ai/sdk/v2/client"
|
import { Session, type Message } from "@opencode-ai/sdk/v2/client"
|
||||||
import { usePlatform } from "@/context/platform"
|
import { usePlatform } from "@/context/platform"
|
||||||
import { useSettings } from "@/context/settings"
|
import { useSettings } from "@/context/settings"
|
||||||
|
|
@ -35,7 +35,7 @@ import type { DragEvent } from "@thisbeyond/solid-dnd"
|
||||||
import { useProviders } from "@/hooks/use-providers"
|
import { useProviders } from "@/hooks/use-providers"
|
||||||
import { showToast, Toast, toaster } from "@opencode-ai/ui/toast"
|
import { showToast, Toast, toaster } from "@opencode-ai/ui/toast"
|
||||||
import { useGlobalSDK } from "@/context/global-sdk"
|
import { useGlobalSDK } from "@/context/global-sdk"
|
||||||
import { clearWorkspaceTerminals } from "@/context/terminal"
|
import { clearWorkspaceTerminals, getTerminalServerScope } from "@/context/terminal"
|
||||||
import { dropSessionCaches, pickSessionCacheEvictions } from "@/context/global-sync/session-cache"
|
import { dropSessionCaches, pickSessionCacheEvictions } from "@/context/global-sync/session-cache"
|
||||||
import {
|
import {
|
||||||
clearSessionPrefetchInflight,
|
clearSessionPrefetchInflight,
|
||||||
|
|
@ -48,8 +48,8 @@ import {
|
||||||
} from "@/context/global-sync/session-prefetch"
|
} from "@/context/global-sync/session-prefetch"
|
||||||
import { useNotification } from "@/context/notification"
|
import { useNotification } from "@/context/notification"
|
||||||
import { usePermission } from "@/context/permission"
|
import { usePermission } from "@/context/permission"
|
||||||
import { Binary } from "@opencode-ai/shared/util/binary"
|
import { Binary } from "@opencode-ai/core/util/binary"
|
||||||
import { retry } from "@opencode-ai/shared/util/retry"
|
import { retry } from "@opencode-ai/core/util/retry"
|
||||||
import { playSoundById } from "@/utils/sound"
|
import { playSoundById } from "@/utils/sound"
|
||||||
import { createAim } from "@/utils/aim"
|
import { createAim } from "@/utils/aim"
|
||||||
import { setNavigate } from "@/utils/notification-click"
|
import { setNavigate } from "@/utils/notification-click"
|
||||||
|
|
@ -64,13 +64,13 @@ import { DebugBar } from "@/components/debug-bar"
|
||||||
import { Titlebar } from "@/components/titlebar"
|
import { Titlebar } from "@/components/titlebar"
|
||||||
import { useServer } from "@/context/server"
|
import { useServer } from "@/context/server"
|
||||||
import { useLanguage, type Locale } from "@/context/language"
|
import { useLanguage, type Locale } from "@/context/language"
|
||||||
|
import { pathKey } from "@/utils/path-key"
|
||||||
import {
|
import {
|
||||||
displayName,
|
displayName,
|
||||||
effectiveWorkspaceOrder,
|
effectiveWorkspaceOrder,
|
||||||
errorMessage,
|
errorMessage,
|
||||||
latestRootSession,
|
latestRootSession,
|
||||||
sortedRootSessions,
|
sortedRootSessions,
|
||||||
workspaceKey,
|
|
||||||
} from "./layout/helpers"
|
} from "./layout/helpers"
|
||||||
import {
|
import {
|
||||||
collectNewSessionDeepLinks,
|
collectNewSessionDeepLinks,
|
||||||
|
|
@ -164,7 +164,7 @@ export default function Layout(props: ParentProps) {
|
||||||
|
|
||||||
const editor = createInlineEditorController()
|
const editor = createInlineEditorController()
|
||||||
const setBusy = (directory: string, value: boolean) => {
|
const setBusy = (directory: string, value: boolean) => {
|
||||||
const key = workspaceKey(directory)
|
const key = pathKey(directory)
|
||||||
if (value) {
|
if (value) {
|
||||||
setState("busyWorkspaces", key, true)
|
setState("busyWorkspaces", key, true)
|
||||||
return
|
return
|
||||||
|
|
@ -176,7 +176,7 @@ export default function Layout(props: ParentProps) {
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
const isBusy = (directory: string) => !!state.busyWorkspaces[workspaceKey(directory)]
|
const isBusy = (directory: string) => !!state.busyWorkspaces[pathKey(directory)]
|
||||||
const navLeave = { current: undefined as number | undefined }
|
const navLeave = { current: undefined as number | undefined }
|
||||||
const sortNow = () => state.sortNow
|
const sortNow = () => state.sortNow
|
||||||
let sizet: number | undefined
|
let sizet: number | undefined
|
||||||
|
|
@ -366,7 +366,7 @@ export default function Layout(props: ParentProps) {
|
||||||
|
|
||||||
const useUpdatePolling = () =>
|
const useUpdatePolling = () =>
|
||||||
onMount(() => {
|
onMount(() => {
|
||||||
if (!platform.checkUpdate || !platform.update || !platform.restart) return
|
if (!platform.checkUpdate || !platform.updateAndRestart) return
|
||||||
|
|
||||||
let toastId: number | undefined
|
let toastId: number | undefined
|
||||||
let interval: ReturnType<typeof setInterval> | undefined
|
let interval: ReturnType<typeof setInterval> | undefined
|
||||||
|
|
@ -384,8 +384,7 @@ export default function Layout(props: ParentProps) {
|
||||||
{
|
{
|
||||||
label: language.t("toast.update.action.installRestart"),
|
label: language.t("toast.update.action.installRestart"),
|
||||||
onClick: async () => {
|
onClick: async () => {
|
||||||
await platform.update!()
|
await platform.updateAndRestart!()
|
||||||
await platform.restart!()
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|
@ -498,8 +497,8 @@ export default function Layout(props: ParentProps) {
|
||||||
}
|
}
|
||||||
|
|
||||||
const currentSession = params.id
|
const currentSession = params.id
|
||||||
if (workspaceKey(directory) === workspaceKey(currentDir()) && props.sessionID === currentSession) return
|
if (pathKey(directory) === pathKey(currentDir()) && props.sessionID === currentSession) return
|
||||||
if (workspaceKey(directory) === workspaceKey(currentDir()) && session?.parentID === currentSession) return
|
if (pathKey(directory) === pathKey(currentDir()) && session?.parentID === currentSession) return
|
||||||
|
|
||||||
dismissSessionAlert(sessionKey)
|
dismissSessionAlert(sessionKey)
|
||||||
|
|
||||||
|
|
@ -557,14 +556,14 @@ export default function Layout(props: ParentProps) {
|
||||||
const currentProject = createMemo(() => {
|
const currentProject = createMemo(() => {
|
||||||
const directory = currentDir()
|
const directory = currentDir()
|
||||||
if (!directory) return
|
if (!directory) return
|
||||||
const key = workspaceKey(directory)
|
const key = pathKey(directory)
|
||||||
|
|
||||||
const projects = layout.projects.list()
|
const projects = layout.projects.list()
|
||||||
|
|
||||||
const sandbox = projects.find((p) => p.sandboxes?.some((item) => workspaceKey(item) === key))
|
const sandbox = projects.find((p) => p.sandboxes?.some((item) => pathKey(item) === key))
|
||||||
if (sandbox) return sandbox
|
if (sandbox) return sandbox
|
||||||
|
|
||||||
const direct = projects.find((p) => workspaceKey(p.worktree) === key)
|
const direct = projects.find((p) => pathKey(p.worktree) === key)
|
||||||
if (direct) return direct
|
if (direct) return direct
|
||||||
|
|
||||||
const [child] = globalSync.child(directory, { bootstrap: false })
|
const [child] = globalSync.child(directory, { bootstrap: false })
|
||||||
|
|
@ -597,7 +596,7 @@ export default function Layout(props: ParentProps) {
|
||||||
})
|
})
|
||||||
|
|
||||||
const workspaceName = (directory: string, projectId?: string, branch?: string) => {
|
const workspaceName = (directory: string, projectId?: string, branch?: string) => {
|
||||||
const key = workspaceKey(directory)
|
const key = pathKey(directory)
|
||||||
const direct = store.workspaceName[key] ?? store.workspaceName[directory]
|
const direct = store.workspaceName[key] ?? store.workspaceName[directory]
|
||||||
if (direct) return direct
|
if (direct) return direct
|
||||||
if (!projectId) return
|
if (!projectId) return
|
||||||
|
|
@ -606,7 +605,7 @@ export default function Layout(props: ParentProps) {
|
||||||
}
|
}
|
||||||
|
|
||||||
const setWorkspaceName = (directory: string, next: string, projectId?: string, branch?: string) => {
|
const setWorkspaceName = (directory: string, next: string, projectId?: string, branch?: string) => {
|
||||||
const key = workspaceKey(directory)
|
const key = pathKey(directory)
|
||||||
setStore("workspaceName", key, next)
|
setStore("workspaceName", key, next)
|
||||||
if (!projectId) return
|
if (!projectId) return
|
||||||
if (!branch) return
|
if (!branch) return
|
||||||
|
|
@ -634,7 +633,7 @@ export default function Layout(props: ParentProps) {
|
||||||
const activeDir = currentDir()
|
const activeDir = currentDir()
|
||||||
return workspaceIds(project).filter((directory) => {
|
return workspaceIds(project).filter((directory) => {
|
||||||
const expanded = store.workspaceExpanded[directory] ?? directory === project.worktree
|
const expanded = store.workspaceExpanded[directory] ?? directory === project.worktree
|
||||||
const active = workspaceKey(directory) === workspaceKey(activeDir)
|
const active = pathKey(directory) === pathKey(activeDir)
|
||||||
return expanded || active
|
return expanded || active
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
@ -645,10 +644,9 @@ export default function Layout(props: ParentProps) {
|
||||||
const projects = layout.projects.list()
|
const projects = layout.projects.list()
|
||||||
for (const [directory, expanded] of Object.entries(store.workspaceExpanded)) {
|
for (const [directory, expanded] of Object.entries(store.workspaceExpanded)) {
|
||||||
if (!expanded) continue
|
if (!expanded) continue
|
||||||
const key = workspaceKey(directory)
|
const key = pathKey(directory)
|
||||||
const project = projects.find(
|
const project = projects.find(
|
||||||
(item) =>
|
(item) => pathKey(item.worktree) === key || item.sandboxes?.some((sandbox) => pathKey(sandbox) === key),
|
||||||
workspaceKey(item.worktree) === key || item.sandboxes?.some((sandbox) => workspaceKey(sandbox) === key),
|
|
||||||
)
|
)
|
||||||
if (!project) continue
|
if (!project) continue
|
||||||
if (project.vcs === "git" && layout.sidebar.workspaces(project.worktree)()) continue
|
if (project.vcs === "git" && layout.sidebar.workspaces(project.worktree)()) continue
|
||||||
|
|
@ -701,7 +699,7 @@ export default function Layout(props: ParentProps) {
|
||||||
seen: lru,
|
seen: lru,
|
||||||
keep: sessionID,
|
keep: sessionID,
|
||||||
limit: PREFETCH_MAX_SESSIONS_PER_DIR,
|
limit: PREFETCH_MAX_SESSIONS_PER_DIR,
|
||||||
preserve: params.id && workspaceKey(directory) === workspaceKey(currentDir()) ? [params.id] : undefined,
|
preserve: params.id && pathKey(directory) === pathKey(currentDir()) ? [params.id] : undefined,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1222,17 +1220,14 @@ export default function Layout(props: ParentProps) {
|
||||||
}
|
}
|
||||||
|
|
||||||
function projectRoot(directory: string) {
|
function projectRoot(directory: string) {
|
||||||
const key = workspaceKey(directory)
|
const key = pathKey(directory)
|
||||||
const project = layout.projects
|
const project = layout.projects
|
||||||
.list()
|
.list()
|
||||||
.find(
|
.find((item) => pathKey(item.worktree) === key || item.sandboxes?.some((sandbox) => pathKey(sandbox) === key))
|
||||||
(item) =>
|
|
||||||
workspaceKey(item.worktree) === key || item.sandboxes?.some((sandbox) => workspaceKey(sandbox) === key),
|
|
||||||
)
|
|
||||||
if (project) return project.worktree
|
if (project) return project.worktree
|
||||||
|
|
||||||
const known = Object.entries(store.workspaceOrder).find(
|
const known = Object.entries(store.workspaceOrder).find(
|
||||||
([root, dirs]) => workspaceKey(root) === key || dirs.some((item) => workspaceKey(item) === key),
|
([root, dirs]) => pathKey(root) === key || dirs.some((item) => pathKey(item) === key),
|
||||||
)
|
)
|
||||||
if (known) return known[0]
|
if (known) return known[0]
|
||||||
|
|
||||||
|
|
@ -1284,7 +1279,7 @@ export default function Layout(props: ParentProps) {
|
||||||
: [root]
|
: [root]
|
||||||
const canOpen = (value: string | undefined) => {
|
const canOpen = (value: string | undefined) => {
|
||||||
if (!value) return false
|
if (!value) return false
|
||||||
return dirs.some((item) => workspaceKey(item) === workspaceKey(value))
|
return dirs.some((item) => pathKey(item) === pathKey(value))
|
||||||
}
|
}
|
||||||
const refreshDirs = async (target?: string) => {
|
const refreshDirs = async (target?: string) => {
|
||||||
if (!target || target === root || canOpen(target)) return canOpen(target)
|
if (!target || target === root || canOpen(target)) return canOpen(target)
|
||||||
|
|
@ -1410,9 +1405,9 @@ export default function Layout(props: ParentProps) {
|
||||||
|
|
||||||
function closeProject(directory: string) {
|
function closeProject(directory: string) {
|
||||||
const list = layout.projects.list()
|
const list = layout.projects.list()
|
||||||
const key = workspaceKey(directory)
|
const key = pathKey(directory)
|
||||||
const index = list.findIndex((x) => workspaceKey(x.worktree) === key)
|
const index = list.findIndex((x) => pathKey(x.worktree) === key)
|
||||||
const active = workspaceKey(currentProject()?.worktree ?? "") === key
|
const active = pathKey(currentProject()?.worktree ?? "") === key
|
||||||
if (index === -1) return
|
if (index === -1) return
|
||||||
const next = list[index + 1]
|
const next = list[index + 1]
|
||||||
|
|
||||||
|
|
@ -1486,8 +1481,8 @@ export default function Layout(props: ParentProps) {
|
||||||
if (directory === root) return
|
if (directory === root) return
|
||||||
|
|
||||||
const current = currentDir()
|
const current = currentDir()
|
||||||
const currentKey = workspaceKey(current)
|
const currentKey = pathKey(current)
|
||||||
const deletedKey = workspaceKey(directory)
|
const deletedKey = pathKey(directory)
|
||||||
const shouldLeave = leaveDeletedWorkspace || (!!params.dir && currentKey === deletedKey)
|
const shouldLeave = leaveDeletedWorkspace || (!!params.dir && currentKey === deletedKey)
|
||||||
if (!leaveDeletedWorkspace && shouldLeave) {
|
if (!leaveDeletedWorkspace && shouldLeave) {
|
||||||
navigateWithSidebarReset(`/${base64Encode(root)}/session`)
|
navigateWithSidebarReset(`/${base64Encode(root)}/session`)
|
||||||
|
|
@ -1510,7 +1505,7 @@ export default function Layout(props: ParentProps) {
|
||||||
|
|
||||||
if (!result) return
|
if (!result) return
|
||||||
|
|
||||||
if (workspaceKey(store.lastProjectSession[root]?.directory ?? "") === workspaceKey(directory)) {
|
if (pathKey(store.lastProjectSession[root]?.directory ?? "") === pathKey(directory)) {
|
||||||
clearLastProjectSession(root)
|
clearLastProjectSession(root)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1530,12 +1525,12 @@ export default function Layout(props: ParentProps) {
|
||||||
if (shouldLeave) return
|
if (shouldLeave) return
|
||||||
|
|
||||||
const nextCurrent = currentDir()
|
const nextCurrent = currentDir()
|
||||||
const nextKey = workspaceKey(nextCurrent)
|
const nextKey = pathKey(nextCurrent)
|
||||||
const project = layout.projects.list().find((item) => item.worktree === root)
|
const project = layout.projects.list().find((item) => item.worktree === root)
|
||||||
const dirs = project
|
const dirs = project
|
||||||
? effectiveWorkspaceOrder(root, [root, ...(project.sandboxes ?? [])], store.workspaceOrder[root])
|
? effectiveWorkspaceOrder(root, [root, ...(project.sandboxes ?? [])], store.workspaceOrder[root])
|
||||||
: [root]
|
: [root]
|
||||||
const valid = dirs.some((item) => workspaceKey(item) === nextKey)
|
const valid = dirs.some((item) => pathKey(item) === nextKey)
|
||||||
|
|
||||||
if (params.dir && projectRoot(nextCurrent) === root && !valid) {
|
if (params.dir && projectRoot(nextCurrent) === root && !valid) {
|
||||||
navigateWithSidebarReset(`/${base64Encode(root)}/session`)
|
navigateWithSidebarReset(`/${base64Encode(root)}/session`)
|
||||||
|
|
@ -1562,6 +1557,7 @@ export default function Layout(props: ParentProps) {
|
||||||
directory,
|
directory,
|
||||||
sessions.map((s) => s.id),
|
sessions.map((s) => s.id),
|
||||||
platform,
|
platform,
|
||||||
|
getTerminalServerScope(server.current, server.key),
|
||||||
)
|
)
|
||||||
await globalSDK.client.instance.dispose({ directory }).catch(() => undefined)
|
await globalSDK.client.instance.dispose({ directory }).catch(() => undefined)
|
||||||
|
|
||||||
|
|
@ -1641,7 +1637,7 @@ export default function Layout(props: ParentProps) {
|
||||||
})
|
})
|
||||||
|
|
||||||
const handleDelete = () => {
|
const handleDelete = () => {
|
||||||
const leaveDeletedWorkspace = !!params.dir && workspaceKey(currentDir()) === workspaceKey(props.directory)
|
const leaveDeletedWorkspace = !!params.dir && pathKey(currentDir()) === pathKey(props.directory)
|
||||||
if (leaveDeletedWorkspace) {
|
if (leaveDeletedWorkspace) {
|
||||||
navigateWithSidebarReset(`/${base64Encode(props.root)}/session`)
|
navigateWithSidebarReset(`/${base64Encode(props.root)}/session`)
|
||||||
}
|
}
|
||||||
|
|
@ -1868,11 +1864,9 @@ export default function Layout(props: ParentProps) {
|
||||||
const local = project.worktree
|
const local = project.worktree
|
||||||
const dirs = [local, ...(project.sandboxes ?? [])]
|
const dirs = [local, ...(project.sandboxes ?? [])]
|
||||||
const active = currentProject()
|
const active = currentProject()
|
||||||
const directory = workspaceKey(active?.worktree ?? "") === workspaceKey(project.worktree) ? currentDir() : undefined
|
const directory = pathKey(active?.worktree ?? "") === pathKey(project.worktree) ? currentDir() : undefined
|
||||||
const extra =
|
const extra =
|
||||||
directory &&
|
directory && pathKey(directory) !== pathKey(local) && !dirs.some((item) => pathKey(item) === pathKey(directory))
|
||||||
workspaceKey(directory) !== workspaceKey(local) &&
|
|
||||||
!dirs.some((item) => workspaceKey(item) === workspaceKey(directory))
|
|
||||||
? directory
|
? directory
|
||||||
: undefined
|
: undefined
|
||||||
const pending = extra ? WorktreeState.get(extra)?.status === "pending" : false
|
const pending = extra ? WorktreeState.get(extra)?.status === "pending" : false
|
||||||
|
|
@ -1917,7 +1911,7 @@ export default function Layout(props: ParentProps) {
|
||||||
setStore(
|
setStore(
|
||||||
"workspaceOrder",
|
"workspaceOrder",
|
||||||
project.worktree,
|
project.worktree,
|
||||||
result.filter((directory) => workspaceKey(directory) !== workspaceKey(project.worktree)),
|
result.filter((directory) => pathKey(directory) !== pathKey(project.worktree)),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1943,8 +1937,8 @@ export default function Layout(props: ParentProps) {
|
||||||
setWorkspaceName(created.directory, created.branch, project.id, created.branch)
|
setWorkspaceName(created.directory, created.branch, project.id, created.branch)
|
||||||
|
|
||||||
const local = project.worktree
|
const local = project.worktree
|
||||||
const key = workspaceKey(created.directory)
|
const key = pathKey(created.directory)
|
||||||
const root = workspaceKey(local)
|
const root = pathKey(local)
|
||||||
|
|
||||||
setBusy(created.directory, true)
|
setBusy(created.directory, true)
|
||||||
WorktreeState.pending(created.directory)
|
WorktreeState.pending(created.directory)
|
||||||
|
|
@ -1955,7 +1949,7 @@ export default function Layout(props: ParentProps) {
|
||||||
setStore("workspaceOrder", project.worktree, (prev) => {
|
setStore("workspaceOrder", project.worktree, (prev) => {
|
||||||
const existing = prev ?? []
|
const existing = prev ?? []
|
||||||
const next = existing.filter((item) => {
|
const next = existing.filter((item) => {
|
||||||
const id = workspaceKey(item)
|
const id = pathKey(item)
|
||||||
return id !== root && id !== key
|
return id !== root && id !== key
|
||||||
})
|
})
|
||||||
return [created.directory, ...next]
|
return [created.directory, ...next]
|
||||||
|
|
|
||||||
|
|
@ -14,8 +14,8 @@ import {
|
||||||
errorMessage,
|
errorMessage,
|
||||||
hasProjectPermissions,
|
hasProjectPermissions,
|
||||||
latestRootSession,
|
latestRootSession,
|
||||||
workspaceKey,
|
|
||||||
} from "./helpers"
|
} from "./helpers"
|
||||||
|
import { pathKey } from "@/utils/path-key"
|
||||||
|
|
||||||
const session = (input: Partial<Session> & Pick<Session, "id" | "directory">) =>
|
const session = (input: Partial<Session> & Pick<Session, "id" | "directory">) =>
|
||||||
({
|
({
|
||||||
|
|
@ -104,16 +104,16 @@ describe("layout deep links", () => {
|
||||||
|
|
||||||
describe("layout workspace helpers", () => {
|
describe("layout workspace helpers", () => {
|
||||||
test("normalizes trailing slash in workspace key", () => {
|
test("normalizes trailing slash in workspace key", () => {
|
||||||
expect(workspaceKey("/tmp/demo///")).toBe("/tmp/demo")
|
expect(String(pathKey("/tmp/demo///"))).toBe("/tmp/demo")
|
||||||
expect(workspaceKey("C:\\tmp\\demo\\\\")).toBe("C:/tmp/demo")
|
expect(String(pathKey("C:\\tmp\\demo\\\\"))).toBe("C:/tmp/demo")
|
||||||
})
|
})
|
||||||
|
|
||||||
test("preserves posix and drive roots in workspace key", () => {
|
test("preserves posix and drive roots in workspace key", () => {
|
||||||
expect(workspaceKey("/")).toBe("/")
|
expect(String(pathKey("/"))).toBe("/")
|
||||||
expect(workspaceKey("///")).toBe("/")
|
expect(String(pathKey("///"))).toBe("/")
|
||||||
expect(workspaceKey("C:\\")).toBe("C:/")
|
expect(String(pathKey("C:\\"))).toBe("C:/")
|
||||||
expect(workspaceKey("C://")).toBe("C:/")
|
expect(String(pathKey("C://"))).toBe("C:/")
|
||||||
expect(workspaceKey("C:///")).toBe("C:/")
|
expect(String(pathKey("C:///"))).toBe("C:/")
|
||||||
})
|
})
|
||||||
|
|
||||||
test("keeps local first while preserving known order", () => {
|
test("keeps local first while preserving known order", () => {
|
||||||
|
|
|
||||||
|
|
@ -1,19 +1,12 @@
|
||||||
import { getFilename } from "@opencode-ai/shared/util/path"
|
import { getFilename } from "@opencode-ai/core/util/path"
|
||||||
import { type Session } from "@opencode-ai/sdk/v2/client"
|
import { type Session } from "@opencode-ai/sdk/v2/client"
|
||||||
|
import { pathKey } from "@/utils/path-key"
|
||||||
|
|
||||||
type SessionStore = {
|
type SessionStore = {
|
||||||
session?: Session[]
|
session?: Session[]
|
||||||
path: { directory: string }
|
path: { directory: string }
|
||||||
}
|
}
|
||||||
|
|
||||||
export const workspaceKey = (directory: string) => {
|
|
||||||
const value = directory.replaceAll("\\", "/")
|
|
||||||
const drive = value.match(/^([A-Za-z]:)\/+$/)
|
|
||||||
if (drive) return `${drive[1]}/`
|
|
||||||
if (/^\/+$/i.test(value)) return "/"
|
|
||||||
return value.replace(/\/+$/, "")
|
|
||||||
}
|
|
||||||
|
|
||||||
function sortSessions(now: number) {
|
function sortSessions(now: number) {
|
||||||
const oneMinuteAgo = now - 60 * 1000
|
const oneMinuteAgo = now - 60 * 1000
|
||||||
return (a: Session, b: Session) => {
|
return (a: Session, b: Session) => {
|
||||||
|
|
@ -29,7 +22,7 @@ function sortSessions(now: number) {
|
||||||
}
|
}
|
||||||
|
|
||||||
const isRootVisibleSession = (session: Session, directory: string) =>
|
const isRootVisibleSession = (session: Session, directory: string) =>
|
||||||
workspaceKey(session.directory) === workspaceKey(directory) && !session.parentID && !session.time?.archived
|
pathKey(session.directory) === pathKey(directory) && !session.parentID && !session.time?.archived
|
||||||
|
|
||||||
export const roots = (store: SessionStore) =>
|
export const roots = (store: SessionStore) =>
|
||||||
(store.session ?? []).filter((session) => isRootVisibleSession(session, store.path.directory))
|
(store.session ?? []).filter((session) => isRootVisibleSession(session, store.path.directory))
|
||||||
|
|
@ -72,11 +65,11 @@ export const errorMessage = (err: unknown, fallback: string) => {
|
||||||
}
|
}
|
||||||
|
|
||||||
export const effectiveWorkspaceOrder = (local: string, dirs: string[], persisted?: string[]) => {
|
export const effectiveWorkspaceOrder = (local: string, dirs: string[], persisted?: string[]) => {
|
||||||
const root = workspaceKey(local)
|
const root = pathKey(local)
|
||||||
const live = new Map<string, string>()
|
const live = new Map<string, string>()
|
||||||
|
|
||||||
for (const dir of dirs) {
|
for (const dir of dirs) {
|
||||||
const key = workspaceKey(dir)
|
const key = pathKey(dir)
|
||||||
if (key === root) continue
|
if (key === root) continue
|
||||||
if (!live.has(key)) live.set(key, dir)
|
if (!live.has(key)) live.set(key, dir)
|
||||||
}
|
}
|
||||||
|
|
@ -85,7 +78,7 @@ export const effectiveWorkspaceOrder = (local: string, dirs: string[], persisted
|
||||||
|
|
||||||
const result = [local]
|
const result = [local]
|
||||||
for (const dir of persisted) {
|
for (const dir of persisted) {
|
||||||
const key = workspaceKey(dir)
|
const key = pathKey(dir)
|
||||||
if (key === root) continue
|
if (key === root) continue
|
||||||
const match = live.get(key)
|
const match = live.get(key)
|
||||||
if (!match) continue
|
if (!match) continue
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,7 @@ import { Icon } from "@opencode-ai/ui/icon"
|
||||||
import { IconButton } from "@opencode-ai/ui/icon-button"
|
import { IconButton } from "@opencode-ai/ui/icon-button"
|
||||||
import { Spinner } from "@opencode-ai/ui/spinner"
|
import { Spinner } from "@opencode-ai/ui/spinner"
|
||||||
import { Tooltip } from "@opencode-ai/ui/tooltip"
|
import { Tooltip } from "@opencode-ai/ui/tooltip"
|
||||||
import { getFilename } from "@opencode-ai/shared/util/path"
|
import { getFilename } from "@opencode-ai/core/util/path"
|
||||||
import { A, useParams } from "@solidjs/router"
|
import { A, useParams } from "@solidjs/router"
|
||||||
import { type Accessor, createMemo, For, type JSX, Match, Show, Switch } from "solid-js"
|
import { type Accessor, createMemo, For, type JSX, Match, Show, Switch } from "solid-js"
|
||||||
import { useGlobalSync } from "@/context/global-sync"
|
import { useGlobalSync } from "@/context/global-sync"
|
||||||
|
|
@ -20,9 +20,10 @@ import { childSessionOnPath, hasProjectPermissions } from "./helpers"
|
||||||
const OPENCODE_PROJECT_ID = "4b0ea68d7af9a6031a7ffda7ad66e0cb83315750"
|
const OPENCODE_PROJECT_ID = "4b0ea68d7af9a6031a7ffda7ad66e0cb83315750"
|
||||||
|
|
||||||
export function getProjectAvatarSource(id?: string, icon?: { color?: string; url?: string; override?: string }) {
|
export function getProjectAvatarSource(id?: string, icon?: { color?: string; url?: string; override?: string }) {
|
||||||
return id === OPENCODE_PROJECT_ID
|
if (id === OPENCODE_PROJECT_ID) return "https://opencode.ai/favicon.svg"
|
||||||
? "https://opencode.ai/favicon.svg"
|
if (icon?.override) return icon?.override
|
||||||
: (icon?.override ?? (icon?.color ? undefined : icon?.url))
|
if (icon?.color) return undefined
|
||||||
|
return icon?.url
|
||||||
}
|
}
|
||||||
|
|
||||||
export const ProjectIcon = (props: { project: LocalProject; class?: string; notify?: boolean }): JSX.Element => {
|
export const ProjectIcon = (props: { project: LocalProject; class?: string; notify?: boolean }): JSX.Element => {
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
import { createMemo, For, Show, type Accessor, type JSX } from "solid-js"
|
import { createMemo, For, Show, type Accessor, type JSX } from "solid-js"
|
||||||
import { createStore } from "solid-js/store"
|
import { createStore } from "solid-js/store"
|
||||||
import { base64Encode } from "@opencode-ai/shared/util/encode"
|
import { base64Encode } from "@opencode-ai/core/util/encode"
|
||||||
import { Button } from "@opencode-ai/ui/button"
|
import { Button } from "@opencode-ai/ui/button"
|
||||||
import { ContextMenu } from "@opencode-ai/ui/context-menu"
|
import { ContextMenu } from "@opencode-ai/ui/context-menu"
|
||||||
import { HoverCard } from "@opencode-ai/ui/hover-card"
|
import { HoverCard } from "@opencode-ai/ui/hover-card"
|
||||||
|
|
|
||||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue