mirror of
https://github.com/anomalyco/opencode.git
synced 2026-02-24 01:34:18 +00:00
Compare commits
582 Commits
fix-markdo
...
v1.1.31
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
59ceca3e51 | ||
|
|
877b0412c9 | ||
|
|
a0d71bf8ef | ||
|
|
19fe3e265a | ||
|
|
20b6cc279f | ||
|
|
80c808d186 | ||
|
|
a132b2a138 | ||
|
|
936f3ebe95 | ||
|
|
23daac2170 | ||
|
|
383c2787f9 | ||
|
|
c89f6e7ac6 | ||
|
|
17a5f75b54 | ||
|
|
5ca28b6454 | ||
|
|
09d2fd57ff | ||
|
|
bcdec15fb4 | ||
|
|
0b63cae1ae | ||
|
|
b7b2eae20c | ||
|
|
1b98f26794 | ||
|
|
fa91337723 | ||
|
|
6d656e4827 | ||
|
|
ae8cff22e5 | ||
|
|
52535654e7 | ||
|
|
7e609cc612 | ||
|
|
416aaff488 | ||
|
|
aa599b4a7d | ||
|
|
b33cec485a | ||
|
|
3ba1111ed0 | ||
|
|
6f7a1c69a5 | ||
|
|
13405aedea | ||
|
|
df2ed99231 | ||
|
|
c69e3bbde7 | ||
|
|
2a370f8038 | ||
|
|
d9f0287d74 | ||
|
|
301e74d953 | ||
|
|
51126f081d | ||
|
|
d03cac2351 | ||
|
|
1820569818 | ||
|
|
8df09abb1b | ||
|
|
95b17bcf5e | ||
|
|
c4c489a5bc | ||
|
|
cd34f5e07c | ||
|
|
621550ac77 | ||
|
|
b746c006cf | ||
|
|
850d50eb64 | ||
|
|
ab3d412a81 | ||
|
|
0e1a8a1839 | ||
|
|
178767af70 | ||
|
|
b8a0e420f8 | ||
|
|
bfbcbc8863 | ||
|
|
fd77d31b49 | ||
|
|
9f02ffe02d | ||
|
|
b10f423743 | ||
|
|
0059fdc1f5 | ||
|
|
f7f2d9700a | ||
|
|
97e0e79f1a | ||
|
|
4fc7bcf09e | ||
|
|
63da3a338a | ||
|
|
f736751a8c | ||
|
|
6ac8c85b34 | ||
|
|
19f68382fd | ||
|
|
368cd2af4c | ||
|
|
d00b8df770 | ||
|
|
7ed448a7e8 | ||
|
|
87d91c29e2 | ||
|
|
259b2a3c2d | ||
|
|
ab705bbc31 | ||
|
|
e237f06c96 | ||
|
|
bb710e9ea1 | ||
|
|
34e4d077cd | ||
|
|
4b8335160b | ||
|
|
8b0353cb2a | ||
|
|
4a386906dd | ||
|
|
efff52714d | ||
|
|
09a9556c70 | ||
|
|
118b4f65da | ||
|
|
e6438aa3f6 | ||
|
|
64c80f1b51 | ||
|
|
7b8fad6202 | ||
|
|
996eeb1f68 | ||
|
|
2e5fe6d5c8 | ||
|
|
8e8fb6a54b | ||
|
|
79aa931a05 | ||
|
|
cf6ad4c407 | ||
|
|
d6caaee816 | ||
|
|
65938baf00 | ||
|
|
a639961973 | ||
|
|
0f979bb87c | ||
|
|
96e9c89cc6 | ||
|
|
a18ae2c8b7 | ||
|
|
c9ea966805 | ||
|
|
2049af4d6f | ||
|
|
74bd52e8a7 | ||
|
|
9dc95c4c69 | ||
|
|
b93f33eaa4 | ||
|
|
34d473c0f5 | ||
|
|
217e4850db | ||
|
|
be9a0bfee7 | ||
|
|
dac73572e0 | ||
|
|
cbe20d22d3 | ||
|
|
3723e1b8d2 | ||
|
|
65d9e829e7 | ||
|
|
6793b4a6fd | ||
|
|
a71c40c717 | ||
|
|
489f2d3709 | ||
|
|
20c8624bb7 | ||
|
|
bb8bf32abe | ||
|
|
233d003b49 | ||
|
|
6037e88ddf | ||
|
|
b13c269162 | ||
|
|
ef36af0e55 | ||
|
|
f86c37f579 | ||
|
|
9b7d9c8173 | ||
|
|
0f2e8ea2b4 | ||
|
|
be493e8be0 | ||
|
|
7e8e4d9938 | ||
|
|
7a359ff67c | ||
|
|
835fea6bb1 | ||
|
|
7138bd021c | ||
|
|
a68e5a1c17 | ||
|
|
92beae1410 | ||
|
|
0470717c7f | ||
|
|
7f50b27996 | ||
|
|
021e42c0bb | ||
|
|
0c4ffec857 | ||
|
|
5c3e9cfa2c | ||
|
|
85ef23a098 | ||
|
|
3b46f90124 | ||
|
|
80dc74a0ec | ||
|
|
a0636fcd50 | ||
|
|
d2fcdef571 | ||
|
|
8137e4dd9c | ||
|
|
7be6671e6e | ||
|
|
575cc59b37 | ||
|
|
4350b8fd6b | ||
|
|
2111473746 | ||
|
|
8c5c377680 | ||
|
|
d51089b52f | ||
|
|
694695050a | ||
|
|
1f3b2b5951 | ||
|
|
de87694867 | ||
|
|
ecd28fd520 | ||
|
|
bf9047ccd1 | ||
|
|
96a9744347 | ||
|
|
c4594c4c1f | ||
|
|
eea70be21a | ||
|
|
e83c01ad36 | ||
|
|
7eb724f4e9 | ||
|
|
7653e2d4d8 | ||
|
|
261b1eca2e | ||
|
|
dbc15d4816 | ||
|
|
602b6be4d4 | ||
|
|
7f4277695d | ||
|
|
39afc055bf | ||
|
|
73b1bc42f4 | ||
|
|
3eea1d424e | ||
|
|
1092cf4034 | ||
|
|
0c270b4743 | ||
|
|
715860f997 | ||
|
|
26f66b5f5d | ||
|
|
f250a229c9 | ||
|
|
c2c2bb1fa9 | ||
|
|
19ac6f1948 | ||
|
|
0d9ce6ad7b | ||
|
|
2b95956132 | ||
|
|
36bbe809fa | ||
|
|
a8113ee0df | ||
|
|
bcb8d970f1 | ||
|
|
c57491ba48 | ||
|
|
9ffb7141e5 | ||
|
|
f3b0f312bf | ||
|
|
09a6107649 | ||
|
|
af8d91117c | ||
|
|
0ffc2c2b39 | ||
|
|
f9c951aa8b | ||
|
|
78bcbda2fa | ||
|
|
262aca1bca | ||
|
|
0cbbe5af77 | ||
|
|
ecae24f426 | ||
|
|
745206ffbb | ||
|
|
83557e9b66 | ||
|
|
1a4abe85e8 | ||
|
|
175313922b | ||
|
|
74ad6dd4c9 | ||
|
|
a94667e8e7 | ||
|
|
ff8abd8c2b | ||
|
|
95e9407e63 | ||
|
|
1466b43c5c | ||
|
|
f73d7e67d3 | ||
|
|
1ac0980c80 | ||
|
|
1d6f650f53 | ||
|
|
0b9b85ea6e | ||
|
|
5521d66bb8 | ||
|
|
281c9d1870 | ||
|
|
80481c2247 | ||
|
|
156ce54362 | ||
|
|
1bc919dc74 | ||
|
|
17438a2e90 | ||
|
|
17c4202ea8 | ||
|
|
7170983ef2 | ||
|
|
b05d88a730 | ||
|
|
a3a06ffc4f | ||
|
|
68e41a1ee7 | ||
|
|
5622c53e1f | ||
|
|
dfe6ce211d | ||
|
|
8639b0767a | ||
|
|
5f67e6fd12 | ||
|
|
86b2002deb | ||
|
|
7f862533d8 | ||
|
|
8f62d4a5e3 | ||
|
|
733226de9d | ||
|
|
e8b0a65c63 | ||
|
|
cd2125eecd | ||
|
|
8595dae1a4 | ||
|
|
c365f0a7c1 | ||
|
|
01b12949e3 | ||
|
|
ac7e674a87 | ||
|
|
47fa496701 | ||
|
|
d77cbf9c46 | ||
|
|
340285575b | ||
|
|
dd5b5f5482 | ||
|
|
924fc9ed80 | ||
|
|
df094a10ff | ||
|
|
de3641e8eb | ||
|
|
8bcbfd6396 | ||
|
|
e521fee002 | ||
|
|
04e60f2b3d | ||
|
|
23d71e125c | ||
|
|
27406cf8ef | ||
|
|
0596b02f19 | ||
|
|
5145b72c4a | ||
|
|
347cd8ac63 | ||
|
|
b711ca57f2 | ||
|
|
353115a895 | ||
|
|
5f0372183a | ||
|
|
616329ae97 | ||
|
|
9706aaf552 | ||
|
|
8b379329a6 | ||
|
|
68d1755a9e | ||
|
|
419004992d | ||
|
|
0d49df46ef | ||
|
|
36f5ba52e9 | ||
|
|
088b537657 | ||
|
|
4ddfa86e7f | ||
|
|
b91b76e9eb | ||
|
|
6ed656a615 | ||
|
|
7b336add88 | ||
|
|
7f9ffe57f9 | ||
|
|
ad31b555a8 | ||
|
|
a05c334702 | ||
|
|
cf284e32aa | ||
|
|
054ccee78d | ||
|
|
bfa986d45e | ||
|
|
aa4b06e165 | ||
|
|
2542693f7b | ||
|
|
bec294b781 | ||
|
|
1ee8a9c0b2 | ||
|
|
4e04bee0c9 | ||
|
|
673e79f457 | ||
|
|
79ae749ed8 | ||
|
|
d605a78a05 | ||
|
|
69b3b35ea5 | ||
|
|
3173ba1288 | ||
|
|
a4d1824412 | ||
|
|
cac35bc52d | ||
|
|
ecc51ddb4e | ||
|
|
769c97af08 | ||
|
|
e29120317f | ||
|
|
88c5a7fe9e | ||
|
|
091e88c1e1 | ||
|
|
d19e76d96c | ||
|
|
c3393ecc6c | ||
|
|
889c60d63b | ||
|
|
c47699536f | ||
|
|
c2f9fd5fef | ||
|
|
3fd0043d19 | ||
|
|
092428633f | ||
|
|
fc50b2962c | ||
|
|
dd0906be8c | ||
|
|
b72a00eaa3 | ||
|
|
2dbdd18483 | ||
|
|
b0794172bf | ||
|
|
9fbf2e72b4 | ||
|
|
494e8d5be9 | ||
|
|
e12b94d91a | ||
|
|
89be504abc | ||
|
|
c7f0cb3d2d | ||
|
|
eb779a7cc5 | ||
|
|
c720a2163c | ||
|
|
7811e01c8e | ||
|
|
befd0f1636 | ||
|
|
1f11a8a6ea | ||
|
|
d5ae8e0bef | ||
|
|
453417ed47 | ||
|
|
72cb7ccc00 | ||
|
|
4ee540309f | ||
|
|
5b86724632 | ||
|
|
b1684f3d12 | ||
|
|
29e206b6c6 | ||
|
|
31864cadb4 | ||
|
|
843d76191e | ||
|
|
3186e7ec7c | ||
|
|
1ba7c606e6 | ||
|
|
f00f18b926 | ||
|
|
e9ede70793 | ||
|
|
2b086f0584 | ||
|
|
b90315bc7e | ||
|
|
182c43a78f | ||
|
|
f1daf3b430 | ||
|
|
dd19c3d8f2 | ||
|
|
f5eb90514a | ||
|
|
6bc823bd40 | ||
|
|
7621c5cafb | ||
|
|
91a708b12e | ||
|
|
19d15ca4df | ||
|
|
03d7467ea2 | ||
|
|
23e9c02a7f | ||
|
|
51804a47e9 | ||
|
|
55739b7aa1 | ||
|
|
295f290efd | ||
|
|
1a262c4ca8 | ||
|
|
dca2540ca7 | ||
|
|
fcfe6d3d26 | ||
|
|
093a3e7876 | ||
|
|
f26de6c52f | ||
|
|
06d03dec3b | ||
|
|
08005d755b | ||
|
|
13276aee82 | ||
|
|
4299450d7d | ||
|
|
3515b4ff7d | ||
|
|
4a7809f600 | ||
|
|
9d1803d000 | ||
|
|
91787ceb3e | ||
|
|
86df915df0 | ||
|
|
6f847a794b | ||
|
|
260ab60c0b | ||
|
|
e2f1f4d81e | ||
|
|
fc6c9cbbd2 | ||
|
|
6b481b5fb0 | ||
|
|
2fc4ab9687 | ||
|
|
d939a3ad54 | ||
|
|
bee2f65409 | ||
|
|
e81bb86795 | ||
|
|
b4d4a1ea7d | ||
|
|
0d8e706fac | ||
|
|
d841e70d26 | ||
|
|
19cf9344e1 | ||
|
|
c29d44fcef | ||
|
|
38c641a2fc | ||
|
|
501ef2d989 | ||
|
|
bfd2f91d5b | ||
|
|
dac099a489 | ||
|
|
5009f10406 | ||
|
|
095a64291d | ||
|
|
f7fef99ddd | ||
|
|
2dcca4755d | ||
|
|
ad2e03284b | ||
|
|
6c0991d162 | ||
|
|
5c9cc9c748 | ||
|
|
06bc4dcb06 | ||
|
|
0ccf9bd9ac | ||
|
|
ee4ea65311 | ||
|
|
bef1f66281 | ||
|
|
d13c0ea915 | ||
|
|
3591372c45 | ||
|
|
90f848fbc6 | ||
|
|
b7ad6bd839 | ||
|
|
10433cb45b | ||
|
|
073f9d99b5 | ||
|
|
bfb8c531c2 | ||
|
|
052f887a9a | ||
|
|
759e68616e | ||
|
|
93e43d8e5e | ||
|
|
53c77e29df | ||
|
|
260739a227 | ||
|
|
c3ab76c8ad | ||
|
|
389d97ece9 | ||
|
|
e36b3433fc | ||
|
|
ded9bd26bb | ||
|
|
c890853992 | ||
|
|
2a4e8bc01c | ||
|
|
c19d031144 | ||
|
|
0cc9a22a42 | ||
|
|
b4075cd856 | ||
|
|
53227bfc2a | ||
|
|
d3baaf7408 | ||
|
|
0384e6b0e1 | ||
|
|
c3d33562c7 | ||
|
|
f3513bacff | ||
|
|
3aff88c23d | ||
|
|
58f7da6e9f | ||
|
|
5a199b04cb | ||
|
|
eb968a6651 | ||
|
|
a813fcb41c | ||
|
|
a58d1be822 | ||
|
|
07dc8d8ce4 | ||
|
|
d377246491 | ||
|
|
7030f49a74 | ||
|
|
c4e4f2a058 | ||
|
|
2729705594 | ||
|
|
ea13b6e8aa | ||
|
|
85ab9798c6 | ||
|
|
33290c54cd | ||
|
|
5d613a038d | ||
|
|
db78a59f03 | ||
|
|
7c3eeeb0fa | ||
|
|
e8357a87b0 | ||
|
|
06c543e938 | ||
|
|
759ce8fb8e | ||
|
|
38847e13bb | ||
|
|
e0c6459faa | ||
|
|
80b278ddab | ||
|
|
ef7ef6538e | ||
|
|
d23c21023a | ||
|
|
dfa2a9f225 | ||
|
|
6f78a71fa7 | ||
|
|
f8f1f46a4f | ||
|
|
ab705dacfa | ||
|
|
d1b93616f7 | ||
|
|
69215d456c | ||
|
|
54e52896a4 | ||
|
|
b18fb16e9c | ||
|
|
1250486ddf | ||
|
|
d645e8bbe1 | ||
|
|
cad415872e | ||
|
|
e8746ddb1d | ||
|
|
80020ade2e | ||
|
|
08ef97b162 | ||
|
|
1aedb265dd | ||
|
|
5c13b209aa | ||
|
|
43a9c50389 | ||
|
|
55224d64a2 | ||
|
|
c325aa1142 | ||
|
|
6e020ef9ef | ||
|
|
aca1eb6b5b | ||
|
|
3d095e7fe7 | ||
|
|
632f20558a | ||
|
|
f96c4badd8 | ||
|
|
cbe1c81470 | ||
|
|
c25155586c | ||
|
|
08b94a6890 | ||
|
|
8cddc9ea55 | ||
|
|
578239e0d0 | ||
|
|
626fa1462b | ||
|
|
968239bb76 | ||
|
|
8c24879246 | ||
|
|
9127055ae7 | ||
|
|
f5a6a4af7f | ||
|
|
6e00348bd7 | ||
|
|
95f7403daf | ||
|
|
14d1e20287 | ||
|
|
b8e2895dfc | ||
|
|
6e028ec2dc | ||
|
|
8e0ddd1ac9 | ||
|
|
da78b758d4 | ||
|
|
360765c591 | ||
|
|
db0078bf17 | ||
|
|
98578d3a7b | ||
|
|
bc3616d9c6 | ||
|
|
71306cbd1f | ||
|
|
0866034946 | ||
|
|
2ccaa10e79 | ||
|
|
e92d5b592c | ||
|
|
00ec29dae6 | ||
|
|
438916de5f | ||
|
|
8d4a67324e | ||
|
|
0d683eaa8e | ||
|
|
8fd1b92e6e | ||
|
|
22e3240296 | ||
|
|
e1d0b2ba6e | ||
|
|
ccc27e23df | ||
|
|
ad4bdd9f0f | ||
|
|
5479928a9d | ||
|
|
40836e9683 | ||
|
|
9a48f8e9e3 | ||
|
|
91b8ba2186 | ||
|
|
d075c097ac | ||
|
|
88fd6a294b | ||
|
|
ea8ef37d50 | ||
|
|
d510bd52a4 | ||
|
|
e0a854f035 | ||
|
|
bd914a8c06 | ||
|
|
a49102db01 | ||
|
|
21012fab4b | ||
|
|
9a71a73f50 | ||
|
|
2190e8c656 | ||
|
|
1fd496a5e2 | ||
|
|
74d584af34 | ||
|
|
46f415ecb0 | ||
|
|
d0399045da | ||
|
|
4be0ba19ca | ||
|
|
d5a5e6e062 | ||
|
|
e8dad85233 | ||
|
|
f197b8a0cd | ||
|
|
efaf854e09 | ||
|
|
704276753b | ||
|
|
2c5437791b | ||
|
|
94ab87ffad | ||
|
|
416f419a81 | ||
|
|
3ba03a97dc | ||
|
|
b1a22e08f5 | ||
|
|
c551a4b6e3 | ||
|
|
524ea95a00 | ||
|
|
0e9664d300 | ||
|
|
fffa718f5e | ||
|
|
cce4f64e0b | ||
|
|
ce6e9a822d | ||
|
|
f66e6d7033 | ||
|
|
de2de099b4 | ||
|
|
0233dd1b39 | ||
|
|
40b275d7e6 | ||
|
|
e4a34beb8b | ||
|
|
ac54535486 | ||
|
|
1a43e5fe87 | ||
|
|
46be47d0be | ||
|
|
4af9defb89 | ||
|
|
12b621068a | ||
|
|
07e7ebdb8e | ||
|
|
5092b5f07b | ||
|
|
d8ef9f808d | ||
|
|
d7192d6af9 | ||
|
|
68e6c540bb | ||
|
|
b572c68100 | ||
|
|
25cb03dbe5 | ||
|
|
d47510785a | ||
|
|
657f3d5089 | ||
|
|
49939c4d8d | ||
|
|
529eb6e147 | ||
|
|
a7cae8f674 | ||
|
|
12ae80856e | ||
|
|
7e619a9302 | ||
|
|
2abafbcd2f | ||
|
|
8b08d340ac | ||
|
|
81983d4a2e | ||
|
|
7443b99295 | ||
|
|
306fc05c00 | ||
|
|
9d8d0e97ec | ||
|
|
f4086ac459 | ||
|
|
b9b5d42bd8 | ||
|
|
83ed1adcbd | ||
|
|
9b57db30d1 | ||
|
|
df8e6e6014 | ||
|
|
472a6cc83e | ||
|
|
47d43aaf2d | ||
|
|
da3dea0429 | ||
|
|
9862303eed | ||
|
|
b14622352e | ||
|
|
ea643f1e3f | ||
|
|
f5fd54598f | ||
|
|
0f7b17b1b4 | ||
|
|
4d3e983edb | ||
|
|
d3fc29bdec | ||
|
|
fe58c649cb | ||
|
|
af2a09940c | ||
|
|
7e016fdda6 | ||
|
|
beb97d21ff | ||
|
|
b0345284f9 | ||
|
|
d71153eae6 | ||
|
|
e60ded01df | ||
|
|
4b2a14c154 | ||
|
|
b4717d8092 | ||
|
|
dc8f8cc567 | ||
|
|
99110d12c4 | ||
|
|
74b1349cf6 | ||
|
|
3b3505cfe8 | ||
|
|
55bd6e487e | ||
|
|
1ee916a3c3 | ||
|
|
a5d47f076e | ||
|
|
acd1eb574d | ||
|
|
a71dcc189e | ||
|
|
3789a31423 | ||
|
|
bb6e350d68 | ||
|
|
f9a441d4f4 | ||
|
|
1c05ebaea2 | ||
|
|
520c47e81d | ||
|
|
e5b08da0f1 | ||
|
|
fe2cc0cff1 | ||
|
|
fbc8f6eba9 | ||
|
|
8cba7d7f53 | ||
|
|
6450ba1b79 | ||
|
|
dc1c25cff5 | ||
|
|
3f3550a16e | ||
|
|
161e3db795 |
166
.github/workflows/daily-issues-recap.yml
vendored
Normal file
166
.github/workflows/daily-issues-recap.yml
vendored
Normal file
@@ -0,0 +1,166 @@
|
||||
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 issues created today (${TODAY}) using:
|
||||
gh issue list --repo ${{ github.repository }} --state all --search \"created:${TODAY}\" --json number,title,body,labels,state,comments,createdAt,author --limit 500
|
||||
|
||||
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"
|
||||
174
.github/workflows/daily-pr-recap.yml
vendored
Normal file
174
.github/workflows/daily-pr-recap.yml
vendored
Normal file
@@ -0,0 +1,174 @@
|
||||
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:
|
||||
|
||||
# Open PRs with bug fix labels or 'fix' in title
|
||||
gh pr list --repo ${{ github.repository }} --state open --search \"fix in:title\" --json number,title,author,labels,createdAt,updatedAt,reviewDecision,isDraft,additions,deletions --limit 100
|
||||
|
||||
# PRs with high activity (get comments separately to filter bots)
|
||||
gh pr list --repo ${{ github.repository }} --state open --json number,title,author,labels,createdAt,updatedAt,reviewDecision,isDraft --limit 100
|
||||
|
||||
# Recently merged bug fixes
|
||||
gh pr list --repo ${{ github.repository }} --state merged --search \"merged:${TODAY} fix in:title\" --json number,title,author,mergedAt --limit 50
|
||||
|
||||
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
|
||||
|
||||
**Bug Fixes We Might Miss:**
|
||||
- PRs with 'fix' or 'bug' in title that have been open 2+ days
|
||||
- Small bug fixes (< 100 lines changed) that are easy to review
|
||||
- Bug fixes from community contributors (not core team)
|
||||
|
||||
**High Activity PRs:**
|
||||
- PRs with 5+ human comments (excluding bots listed above)
|
||||
- PRs with back-and-forth discussion
|
||||
- Controversial or complex changes getting attention
|
||||
|
||||
**Quick Wins:**
|
||||
- Small PRs (< 50 lines) that are approved or nearly approved
|
||||
- Bug fixes that just need a final review
|
||||
|
||||
STEP 4: Generate the recap
|
||||
Create a structured recap:
|
||||
|
||||
===DISCORD_START===
|
||||
**Daily PR Recap - ${TODAY}**
|
||||
|
||||
**Bug Fixes Needing Attention**
|
||||
[PRs fixing bugs that might be overlooked - prioritize by age and size]
|
||||
|
||||
**High Activity** (5+ human comments)
|
||||
[PRs with significant discussion - exclude bot comments]
|
||||
|
||||
**Quick Wins** (small, ready to merge)
|
||||
[Easy PRs that just need a review/merge]
|
||||
|
||||
**Merged Bug Fixes Today**
|
||||
[What bug fixes shipped]
|
||||
===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"
|
||||
6
.github/workflows/nix-desktop.yml
vendored
6
.github/workflows/nix-desktop.yml
vendored
@@ -9,6 +9,7 @@ on:
|
||||
- "nix/**"
|
||||
- "packages/app/**"
|
||||
- "packages/desktop/**"
|
||||
- ".github/workflows/nix-desktop.yml"
|
||||
pull_request:
|
||||
paths:
|
||||
- "flake.nix"
|
||||
@@ -16,6 +17,7 @@ on:
|
||||
- "nix/**"
|
||||
- "packages/app/**"
|
||||
- "packages/desktop/**"
|
||||
- ".github/workflows/nix-desktop.yml"
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
@@ -25,6 +27,8 @@ jobs:
|
||||
matrix:
|
||||
os:
|
||||
- blacksmith-4vcpu-ubuntu-2404
|
||||
- blacksmith-4vcpu-ubuntu-2404-arm
|
||||
- macos-15-intel
|
||||
- macos-latest
|
||||
runs-on: ${{ matrix.os }}
|
||||
timeout-minutes: 60
|
||||
@@ -33,7 +37,7 @@ jobs:
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Setup Nix
|
||||
uses: DeterminateSystems/nix-installer-action@v21
|
||||
uses: nixbuild/nix-quick-install-action@v34
|
||||
|
||||
- name: Build desktop via flake
|
||||
run: |
|
||||
|
||||
120
.github/workflows/test.yml
vendored
120
.github/workflows/test.yml
vendored
@@ -8,7 +8,29 @@ on:
|
||||
workflow_dispatch:
|
||||
jobs:
|
||||
test:
|
||||
runs-on: blacksmith-4vcpu-ubuntu-2404
|
||||
name: test (${{ matrix.settings.name }})
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
settings:
|
||||
- name: linux
|
||||
host: blacksmith-4vcpu-ubuntu-2404
|
||||
playwright: bunx playwright install --with-deps
|
||||
workdir: .
|
||||
command: |
|
||||
git config --global user.email "bot@opencode.ai"
|
||||
git config --global user.name "opencode"
|
||||
bun turbo typecheck
|
||||
bun turbo test
|
||||
- name: windows
|
||||
host: windows-latest
|
||||
playwright: bunx playwright install
|
||||
workdir: packages/app
|
||||
command: bun test:e2e:local
|
||||
runs-on: ${{ matrix.settings.host }}
|
||||
defaults:
|
||||
run:
|
||||
shell: bash
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
@@ -18,11 +40,97 @@ jobs:
|
||||
- name: Setup Bun
|
||||
uses: ./.github/actions/setup-bun
|
||||
|
||||
- name: run
|
||||
- name: Install Playwright browsers
|
||||
working-directory: packages/app
|
||||
run: ${{ matrix.settings.playwright }}
|
||||
|
||||
- name: Set OS-specific paths
|
||||
run: |
|
||||
git config --global user.email "bot@opencode.ai"
|
||||
git config --global user.name "opencode"
|
||||
bun turbo typecheck
|
||||
bun turbo test
|
||||
if [ "${{ runner.os }}" = "Windows" ]; then
|
||||
printf '%s\n' "OPENCODE_E2E_ROOT=${{ runner.temp }}\\opencode-e2e" >> "$GITHUB_ENV"
|
||||
printf '%s\n' "OPENCODE_TEST_HOME=${{ runner.temp }}\\opencode-e2e\\home" >> "$GITHUB_ENV"
|
||||
printf '%s\n' "XDG_DATA_HOME=${{ runner.temp }}\\opencode-e2e\\share" >> "$GITHUB_ENV"
|
||||
printf '%s\n' "XDG_CACHE_HOME=${{ runner.temp }}\\opencode-e2e\\cache" >> "$GITHUB_ENV"
|
||||
printf '%s\n' "XDG_CONFIG_HOME=${{ runner.temp }}\\opencode-e2e\\config" >> "$GITHUB_ENV"
|
||||
printf '%s\n' "XDG_STATE_HOME=${{ runner.temp }}\\opencode-e2e\\state" >> "$GITHUB_ENV"
|
||||
printf '%s\n' "MODELS_DEV_API_JSON=${{ github.workspace }}\\packages\\opencode\\test\\tool\\fixtures\\models-api.json" >> "$GITHUB_ENV"
|
||||
else
|
||||
printf '%s\n' "OPENCODE_E2E_ROOT=${{ runner.temp }}/opencode-e2e" >> "$GITHUB_ENV"
|
||||
printf '%s\n' "OPENCODE_TEST_HOME=${{ runner.temp }}/opencode-e2e/home" >> "$GITHUB_ENV"
|
||||
printf '%s\n' "XDG_DATA_HOME=${{ runner.temp }}/opencode-e2e/share" >> "$GITHUB_ENV"
|
||||
printf '%s\n' "XDG_CACHE_HOME=${{ runner.temp }}/opencode-e2e/cache" >> "$GITHUB_ENV"
|
||||
printf '%s\n' "XDG_CONFIG_HOME=${{ runner.temp }}/opencode-e2e/config" >> "$GITHUB_ENV"
|
||||
printf '%s\n' "XDG_STATE_HOME=${{ runner.temp }}/opencode-e2e/state" >> "$GITHUB_ENV"
|
||||
printf '%s\n' "MODELS_DEV_API_JSON=${{ github.workspace }}/packages/opencode/test/tool/fixtures/models-api.json" >> "$GITHUB_ENV"
|
||||
fi
|
||||
|
||||
- name: Seed opencode data
|
||||
if: matrix.settings.name != 'windows'
|
||||
working-directory: packages/opencode
|
||||
run: bun script/seed-e2e.ts
|
||||
env:
|
||||
MODELS_DEV_API_JSON: ${{ env.MODELS_DEV_API_JSON }}
|
||||
OPENCODE_DISABLE_MODELS_FETCH: "true"
|
||||
OPENCODE_DISABLE_SHARE: "true"
|
||||
OPENCODE_DISABLE_LSP_DOWNLOAD: "true"
|
||||
OPENCODE_DISABLE_DEFAULT_PLUGINS: "true"
|
||||
OPENCODE_EXPERIMENTAL_DISABLE_FILEWATCHER: "true"
|
||||
OPENCODE_TEST_HOME: ${{ env.OPENCODE_TEST_HOME }}
|
||||
XDG_DATA_HOME: ${{ env.XDG_DATA_HOME }}
|
||||
XDG_CACHE_HOME: ${{ env.XDG_CACHE_HOME }}
|
||||
XDG_CONFIG_HOME: ${{ env.XDG_CONFIG_HOME }}
|
||||
XDG_STATE_HOME: ${{ env.XDG_STATE_HOME }}
|
||||
OPENCODE_E2E_PROJECT_DIR: ${{ github.workspace }}
|
||||
OPENCODE_E2E_SESSION_TITLE: "E2E Session"
|
||||
OPENCODE_E2E_MESSAGE: "Seeded for UI e2e"
|
||||
OPENCODE_E2E_MODEL: "opencode/gpt-5-nano"
|
||||
|
||||
- name: Run opencode server
|
||||
if: matrix.settings.name != 'windows'
|
||||
working-directory: packages/opencode
|
||||
run: bun dev -- --print-logs --log-level WARN serve --port 4096 --hostname 127.0.0.1 &
|
||||
env:
|
||||
MODELS_DEV_API_JSON: ${{ env.MODELS_DEV_API_JSON }}
|
||||
OPENCODE_DISABLE_MODELS_FETCH: "true"
|
||||
OPENCODE_DISABLE_SHARE: "true"
|
||||
OPENCODE_DISABLE_LSP_DOWNLOAD: "true"
|
||||
OPENCODE_DISABLE_DEFAULT_PLUGINS: "true"
|
||||
OPENCODE_EXPERIMENTAL_DISABLE_FILEWATCHER: "true"
|
||||
OPENCODE_TEST_HOME: ${{ env.OPENCODE_TEST_HOME }}
|
||||
XDG_DATA_HOME: ${{ env.XDG_DATA_HOME }}
|
||||
XDG_CACHE_HOME: ${{ env.XDG_CACHE_HOME }}
|
||||
XDG_CONFIG_HOME: ${{ env.XDG_CONFIG_HOME }}
|
||||
XDG_STATE_HOME: ${{ env.XDG_STATE_HOME }}
|
||||
OPENCODE_CLIENT: "app"
|
||||
|
||||
- name: Wait for opencode server
|
||||
if: matrix.settings.name != 'windows'
|
||||
run: |
|
||||
for i in {1..120}; do
|
||||
curl -fsS "http://127.0.0.1:4096/global/health" > /dev/null && exit 0
|
||||
sleep 1
|
||||
done
|
||||
exit 1
|
||||
|
||||
- name: run
|
||||
working-directory: ${{ matrix.settings.workdir }}
|
||||
run: ${{ matrix.settings.command }}
|
||||
env:
|
||||
CI: true
|
||||
MODELS_DEV_API_JSON: ${{ env.MODELS_DEV_API_JSON }}
|
||||
OPENCODE_DISABLE_MODELS_FETCH: "true"
|
||||
OPENCODE_DISABLE_SHARE: "true"
|
||||
OPENCODE_DISABLE_LSP_DOWNLOAD: "true"
|
||||
OPENCODE_DISABLE_DEFAULT_PLUGINS: "true"
|
||||
OPENCODE_EXPERIMENTAL_DISABLE_FILEWATCHER: "true"
|
||||
OPENCODE_TEST_HOME: ${{ env.OPENCODE_TEST_HOME }}
|
||||
XDG_DATA_HOME: ${{ env.XDG_DATA_HOME }}
|
||||
XDG_CACHE_HOME: ${{ env.XDG_CACHE_HOME }}
|
||||
XDG_CONFIG_HOME: ${{ env.XDG_CONFIG_HOME }}
|
||||
XDG_STATE_HOME: ${{ env.XDG_STATE_HOME }}
|
||||
PLAYWRIGHT_SERVER_HOST: "127.0.0.1"
|
||||
PLAYWRIGHT_SERVER_PORT: "4096"
|
||||
VITE_OPENCODE_SERVER_HOST: "127.0.0.1"
|
||||
VITE_OPENCODE_SERVER_PORT: "4096"
|
||||
OPENCODE_CLIENT: "app"
|
||||
timeout-minutes: 30
|
||||
|
||||
167
.github/workflows/update-nix-hashes.yml
vendored
167
.github/workflows/update-nix-hashes.yml
vendored
@@ -10,22 +10,26 @@ on:
|
||||
- "bun.lock"
|
||||
- "package.json"
|
||||
- "packages/*/package.json"
|
||||
- "flake.lock"
|
||||
- ".github/workflows/update-nix-hashes.yml"
|
||||
pull_request:
|
||||
paths:
|
||||
- "bun.lock"
|
||||
- "package.json"
|
||||
- "packages/*/package.json"
|
||||
- "flake.lock"
|
||||
- ".github/workflows/update-nix-hashes.yml"
|
||||
|
||||
jobs:
|
||||
update-linux:
|
||||
update-node-modules-hashes:
|
||||
if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository
|
||||
runs-on: blacksmith-4vcpu-ubuntu-2404
|
||||
env:
|
||||
SYSTEM: x86_64-linux
|
||||
TITLE: node_modules hashes
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
fetch-depth: 0
|
||||
@@ -33,92 +37,7 @@ jobs:
|
||||
repository: ${{ github.event.pull_request.head.repo.full_name || github.repository }}
|
||||
|
||||
- name: Setup Nix
|
||||
uses: DeterminateSystems/nix-installer-action@v20
|
||||
|
||||
- name: Configure git
|
||||
run: |
|
||||
git config --global user.email "action@github.com"
|
||||
git config --global user.name "Github Action"
|
||||
|
||||
- name: Update flake.lock
|
||||
run: |
|
||||
set -euo pipefail
|
||||
echo "📦 Updating flake.lock..."
|
||||
nix flake update
|
||||
echo "✅ flake.lock updated successfully"
|
||||
|
||||
- name: Update node_modules hash for x86_64-linux
|
||||
run: |
|
||||
set -euo pipefail
|
||||
echo "🔄 Updating node_modules hash for x86_64-linux..."
|
||||
nix/scripts/update-hashes.sh
|
||||
echo "✅ node_modules hash for x86_64-linux updated successfully"
|
||||
|
||||
- name: Commit Linux hash changes
|
||||
env:
|
||||
TARGET_BRANCH: ${{ github.head_ref || github.ref_name }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
|
||||
echo "🔍 Checking for changes in tracked Nix files..."
|
||||
|
||||
summarize() {
|
||||
local status="$1"
|
||||
{
|
||||
echo "### Nix Hash Update (x86_64-linux)"
|
||||
echo ""
|
||||
echo "- ref: ${GITHUB_REF_NAME}"
|
||||
echo "- status: ${status}"
|
||||
} >> "$GITHUB_STEP_SUMMARY"
|
||||
if [ -n "${GITHUB_SERVER_URL:-}" ] && [ -n "${GITHUB_REPOSITORY:-}" ] && [ -n "${GITHUB_RUN_ID:-}" ]; then
|
||||
echo "- run: ${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}/actions/runs/${GITHUB_RUN_ID}" >> "$GITHUB_STEP_SUMMARY"
|
||||
fi
|
||||
echo "" >> "$GITHUB_STEP_SUMMARY"
|
||||
}
|
||||
|
||||
FILES=(flake.lock flake.nix nix/node-modules.nix nix/hashes.json)
|
||||
STATUS="$(git status --short -- "${FILES[@]}" || true)"
|
||||
if [ -z "$STATUS" ]; then
|
||||
echo "✅ No changes detected. Hashes are already up to date."
|
||||
summarize "no changes"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
echo "📝 Changes detected:"
|
||||
echo "$STATUS"
|
||||
echo "🔗 Staging files..."
|
||||
git add "${FILES[@]}"
|
||||
echo "💾 Committing changes..."
|
||||
git commit -m "Update Nix flake.lock and x86_64-linux hash"
|
||||
echo "✅ Changes committed"
|
||||
|
||||
BRANCH="${TARGET_BRANCH:-${GITHUB_REF_NAME}}"
|
||||
echo "🌳 Pulling latest from branch: $BRANCH"
|
||||
git pull --rebase origin "$BRANCH"
|
||||
echo "🚀 Pushing changes to branch: $BRANCH"
|
||||
git push origin HEAD:"$BRANCH"
|
||||
echo "✅ Changes pushed successfully"
|
||||
|
||||
summarize "committed $(git rev-parse --short HEAD)"
|
||||
|
||||
update-macos:
|
||||
needs: update-linux
|
||||
if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository
|
||||
runs-on: macos-latest
|
||||
env:
|
||||
SYSTEM: aarch64-darwin
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
fetch-depth: 0
|
||||
ref: ${{ github.head_ref || github.ref_name }}
|
||||
repository: ${{ github.event.pull_request.head.repo.full_name || github.repository }}
|
||||
|
||||
- name: Setup Nix
|
||||
uses: DeterminateSystems/nix-installer-action@v20
|
||||
uses: nixbuild/nix-quick-install-action@v34
|
||||
|
||||
- name: Configure git
|
||||
run: |
|
||||
@@ -130,27 +49,64 @@ jobs:
|
||||
TARGET_BRANCH: ${{ github.head_ref || github.ref_name }}
|
||||
run: |
|
||||
BRANCH="${TARGET_BRANCH:-${GITHUB_REF_NAME}}"
|
||||
git pull origin "$BRANCH"
|
||||
git pull --rebase --autostash origin "$BRANCH"
|
||||
|
||||
- name: Update node_modules hash for aarch64-darwin
|
||||
- name: Compute all node_modules hashes
|
||||
run: |
|
||||
set -euo pipefail
|
||||
echo "🔄 Updating node_modules hash for aarch64-darwin..."
|
||||
nix/scripts/update-hashes.sh
|
||||
echo "✅ node_modules hash for aarch64-darwin updated successfully"
|
||||
|
||||
- name: Commit macOS hash changes
|
||||
HASH_FILE="nix/hashes.json"
|
||||
SYSTEMS="x86_64-linux aarch64-linux x86_64-darwin aarch64-darwin"
|
||||
|
||||
if [ ! -f "$HASH_FILE" ]; then
|
||||
mkdir -p "$(dirname "$HASH_FILE")"
|
||||
echo '{"nodeModules":{}}' > "$HASH_FILE"
|
||||
fi
|
||||
|
||||
for SYSTEM in $SYSTEMS; do
|
||||
echo "Computing hash for ${SYSTEM}..."
|
||||
BUILD_LOG=$(mktemp)
|
||||
trap 'rm -f "$BUILD_LOG"' EXIT
|
||||
|
||||
# The updater derivations use fakeHash, so they will fail and reveal the correct hash
|
||||
UPDATER_ATTR=".#packages.x86_64-linux.${SYSTEM}_node_modules"
|
||||
|
||||
nix build "$UPDATER_ATTR" --no-link 2>&1 | tee "$BUILD_LOG" || true
|
||||
|
||||
CORRECT_HASH="$(grep -E 'got:\s+sha256-[A-Za-z0-9+/=]+' "$BUILD_LOG" | awk '{print $2}' | head -n1 || true)"
|
||||
|
||||
if [ -z "$CORRECT_HASH" ]; then
|
||||
CORRECT_HASH="$(grep -A2 'hash mismatch' "$BUILD_LOG" | grep 'got:' | awk '{print $2}' | sed 's/sha256:/sha256-/' || true)"
|
||||
fi
|
||||
|
||||
if [ -z "$CORRECT_HASH" ]; then
|
||||
echo "Failed to determine correct node_modules hash for ${SYSTEM}."
|
||||
cat "$BUILD_LOG"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo " ${SYSTEM}: ${CORRECT_HASH}"
|
||||
jq --arg sys "$SYSTEM" --arg h "$CORRECT_HASH" \
|
||||
'.nodeModules[$sys] = $h' "$HASH_FILE" > "${HASH_FILE}.tmp"
|
||||
mv "${HASH_FILE}.tmp" "$HASH_FILE"
|
||||
done
|
||||
|
||||
echo "All hashes computed:"
|
||||
cat "$HASH_FILE"
|
||||
|
||||
- name: Commit ${{ env.TITLE }} changes
|
||||
env:
|
||||
TARGET_BRANCH: ${{ github.head_ref || github.ref_name }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
|
||||
echo "🔍 Checking for changes in tracked Nix files..."
|
||||
HASH_FILE="nix/hashes.json"
|
||||
echo "Checking for changes..."
|
||||
|
||||
summarize() {
|
||||
local status="$1"
|
||||
{
|
||||
echo "### Nix Hash Update (aarch64-darwin)"
|
||||
echo "### Nix $TITLE"
|
||||
echo ""
|
||||
echo "- ref: ${GITHUB_REF_NAME}"
|
||||
echo "- status: ${status}"
|
||||
@@ -161,27 +117,22 @@ jobs:
|
||||
echo "" >> "$GITHUB_STEP_SUMMARY"
|
||||
}
|
||||
|
||||
FILES=(nix/hashes.json)
|
||||
FILES=("$HASH_FILE")
|
||||
STATUS="$(git status --short -- "${FILES[@]}" || true)"
|
||||
if [ -z "$STATUS" ]; then
|
||||
echo "✅ No changes detected. Hash is already up to date."
|
||||
echo "No changes detected."
|
||||
summarize "no changes"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
echo "📝 Changes detected:"
|
||||
echo "Changes detected:"
|
||||
echo "$STATUS"
|
||||
echo "🔗 Staging files..."
|
||||
git add "${FILES[@]}"
|
||||
echo "💾 Committing changes..."
|
||||
git commit -m "Update aarch64-darwin hash"
|
||||
echo "✅ Changes committed"
|
||||
git commit -m "chore: update nix node_modules hashes"
|
||||
|
||||
BRANCH="${TARGET_BRANCH:-${GITHUB_REF_NAME}}"
|
||||
echo "🌳 Pulling latest from branch: $BRANCH"
|
||||
git pull --rebase origin "$BRANCH"
|
||||
echo "🚀 Pushing changes to branch: $BRANCH"
|
||||
git pull --rebase --autostash origin "$BRANCH"
|
||||
git push origin HEAD:"$BRANCH"
|
||||
echo "✅ Changes pushed successfully"
|
||||
echo "Changes pushed successfully"
|
||||
|
||||
summarize "committed $(git rev-parse --short HEAD)"
|
||||
|
||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -20,6 +20,7 @@ opencode.json
|
||||
a.out
|
||||
target
|
||||
.scripts
|
||||
.direnv/
|
||||
|
||||
# Local dev files
|
||||
opencode-dev
|
||||
|
||||
@@ -1,9 +1,20 @@
|
||||
#!/bin/sh
|
||||
set -e
|
||||
# Check if bun version matches package.json
|
||||
EXPECTED_VERSION=$(grep '"packageManager"' package.json | sed 's/.*"bun@\([^"]*\)".*/\1/')
|
||||
CURRENT_VERSION=$(bun --version)
|
||||
if [ "$CURRENT_VERSION" != "$EXPECTED_VERSION" ]; then
|
||||
echo "Error: Bun version $CURRENT_VERSION does not match expected version $EXPECTED_VERSION from package.json"
|
||||
exit 1
|
||||
fi
|
||||
# keep in sync with packages/script/src/index.ts semver qualifier
|
||||
bun -e '
|
||||
import { semver } from "bun";
|
||||
const pkg = await Bun.file("package.json").json();
|
||||
const expectedBunVersion = pkg.packageManager?.split("@")[1];
|
||||
if (!expectedBunVersion) {
|
||||
throw new Error("packageManager field not found in root package.json");
|
||||
}
|
||||
const expectedBunVersionRange = `^${expectedBunVersion}`;
|
||||
if (!semver.satisfies(process.versions.bun, expectedBunVersionRange)) {
|
||||
throw new Error(`This script requires bun@${expectedBunVersionRange}, but you are using bun@${process.versions.bun}`);
|
||||
}
|
||||
if (process.versions.bun !== expectedBunVersion) {
|
||||
console.warn(`Warning: Bun version ${process.versions.bun} differs from expected ${expectedBunVersion}`);
|
||||
}
|
||||
'
|
||||
bun typecheck
|
||||
|
||||
39
.opencode/skill/bun-file-io/SKILL.md
Normal file
39
.opencode/skill/bun-file-io/SKILL.md
Normal file
@@ -0,0 +1,39 @@
|
||||
---
|
||||
name: bun-file-io
|
||||
description: Use this when you are working on file operations like reading, writing, scanning, or deleting files. It summarizes the preferred file APIs and patterns used in this repo. It also notes when to use filesystem helpers for directories.
|
||||
---
|
||||
|
||||
## Use this when
|
||||
|
||||
- Editing file I/O or scans in `packages/opencode`
|
||||
- Handling directory operations or external tools
|
||||
|
||||
## Bun file APIs (from Bun docs)
|
||||
|
||||
- `Bun.file(path)` is lazy; call `text`, `json`, `stream`, `arrayBuffer`, `bytes`, `exists` to read.
|
||||
- Metadata: `file.size`, `file.type`, `file.name`.
|
||||
- `Bun.write(dest, input)` writes strings, buffers, Blobs, Responses, or files.
|
||||
- `Bun.file(...).delete()` deletes a file.
|
||||
- `file.writer()` returns a FileSink for incremental writes.
|
||||
- `Bun.Glob` + `Array.fromAsync(glob.scan({ cwd, absolute, onlyFiles, dot }))` for scans.
|
||||
- Use `Bun.which` to find a binary, then `Bun.spawn` to run it.
|
||||
- `Bun.readableStreamToText/Bytes/JSON` for stream output.
|
||||
|
||||
## When to use node:fs
|
||||
|
||||
- Use `node:fs/promises` for directories (`mkdir`, `readdir`, recursive operations).
|
||||
|
||||
## Repo patterns
|
||||
|
||||
- Prefer Bun APIs over Node `fs` for file access.
|
||||
- Check `Bun.file(...).exists()` before reading.
|
||||
- For binary/large files use `arrayBuffer()` and MIME checks via `file.type`.
|
||||
- Use `Bun.Glob` + `Array.fromAsync` for scans.
|
||||
- Decode tool stderr with `Bun.readableStreamToText`.
|
||||
- For large writes, use `Bun.write(Bun.file(path), text)`.
|
||||
|
||||
## Quick checklist
|
||||
|
||||
- Use Bun APIs first.
|
||||
- Use `path.join`/`path.resolve` for paths.
|
||||
- Prefer promise `.catch(...)` over `try/catch` when possible.
|
||||
@@ -1,6 +0,0 @@
|
||||
---
|
||||
name: test-skill
|
||||
description: use this when asked to test skill
|
||||
---
|
||||
|
||||
woah this is a test skill
|
||||
@@ -26,7 +26,7 @@ curl -fsSL https://opencode.ai/install | bash
|
||||
|
||||
# Package managers
|
||||
npm i -g opencode-ai@latest # or bun/pnpm/yarn
|
||||
scoop bucket add extras; scoop install extras/opencode # Windows
|
||||
scoop install opencode # Windows
|
||||
choco install opencode # Windows
|
||||
brew install anomalyco/tap/opencode # macOS and Linux (recommended, always up to date)
|
||||
brew install opencode # macOS and Linux (official brew formula, updated less)
|
||||
@@ -52,6 +52,8 @@ OpenCode is also available as a desktop application. Download directly from the
|
||||
```bash
|
||||
# macOS (Homebrew)
|
||||
brew install --cask opencode-desktop
|
||||
# Windows (Scoop)
|
||||
scoop bucket add extras; scoop install extras/opencode-desktop
|
||||
```
|
||||
|
||||
#### Installation Directory
|
||||
|
||||
@@ -26,7 +26,7 @@ curl -fsSL https://opencode.ai/install | bash
|
||||
|
||||
# 软件包管理器
|
||||
npm i -g opencode-ai@latest # 也可使用 bun/pnpm/yarn
|
||||
scoop bucket add extras; scoop install extras/opencode # Windows
|
||||
scoop install opencode # Windows
|
||||
choco install opencode # Windows
|
||||
brew install anomalyco/tap/opencode # macOS 和 Linux(推荐,始终保持最新)
|
||||
brew install opencode # macOS 和 Linux(官方 brew formula,更新频率较低)
|
||||
@@ -52,6 +52,8 @@ OpenCode 也提供桌面版应用。可直接从 [发布页 (releases page)](htt
|
||||
```bash
|
||||
# macOS (Homebrew Cask)
|
||||
brew install --cask opencode-desktop
|
||||
# Windows (Scoop)
|
||||
scoop bucket add extras; scoop install extras/opencode-desktop
|
||||
```
|
||||
|
||||
#### 安装目录
|
||||
|
||||
@@ -26,7 +26,7 @@ curl -fsSL https://opencode.ai/install | bash
|
||||
|
||||
# 套件管理員
|
||||
npm i -g opencode-ai@latest # 也可使用 bun/pnpm/yarn
|
||||
scoop bucket add extras; scoop install extras/opencode # Windows
|
||||
scoop install opencode # Windows
|
||||
choco install opencode # Windows
|
||||
brew install anomalyco/tap/opencode # macOS 與 Linux(推薦,始終保持最新)
|
||||
brew install opencode # macOS 與 Linux(官方 brew formula,更新頻率較低)
|
||||
@@ -52,6 +52,8 @@ OpenCode 也提供桌面版應用程式。您可以直接從 [發佈頁面 (rele
|
||||
```bash
|
||||
# macOS (Homebrew Cask)
|
||||
brew install --cask opencode-desktop
|
||||
# Windows (Scoop)
|
||||
scoop bucket add extras; scoop install extras/opencode-desktop
|
||||
```
|
||||
|
||||
#### 安裝目錄
|
||||
|
||||
@@ -24,6 +24,7 @@ Server mode is opt-in only. When enabled, set `OPENCODE_SERVER_PASSWORD` to requ
|
||||
| **Sandbox escapes** | The permission system is not a sandbox (see above) |
|
||||
| **LLM provider data handling** | Data sent to your configured LLM provider is governed by their policies |
|
||||
| **MCP server behavior** | External MCP servers you configure are outside our trust boundary |
|
||||
| **Malicious config files** | Users control their own config; modifying it is not an attack vector |
|
||||
|
||||
---
|
||||
|
||||
|
||||
408
STATS.md
408
STATS.md
@@ -1,203 +1,209 @@
|
||||
# Download Stats
|
||||
|
||||
| Date | GitHub Downloads | npm Downloads | Total |
|
||||
| ---------- | -------------------- | ------------------- | -------------------- |
|
||||
| 2025-06-29 | 18,789 (+0) | 39,420 (+0) | 58,209 (+0) |
|
||||
| 2025-06-30 | 20,127 (+1,338) | 41,059 (+1,639) | 61,186 (+2,977) |
|
||||
| 2025-07-01 | 22,108 (+1,981) | 43,745 (+2,686) | 65,853 (+4,667) |
|
||||
| 2025-07-02 | 24,814 (+2,706) | 46,168 (+2,423) | 70,982 (+5,129) |
|
||||
| 2025-07-03 | 27,834 (+3,020) | 49,955 (+3,787) | 77,789 (+6,807) |
|
||||
| 2025-07-04 | 30,608 (+2,774) | 54,758 (+4,803) | 85,366 (+7,577) |
|
||||
| 2025-07-05 | 32,524 (+1,916) | 58,371 (+3,613) | 90,895 (+5,529) |
|
||||
| 2025-07-06 | 33,766 (+1,242) | 59,694 (+1,323) | 93,460 (+2,565) |
|
||||
| 2025-07-08 | 38,052 (+4,286) | 64,468 (+4,774) | 102,520 (+9,060) |
|
||||
| 2025-07-09 | 40,924 (+2,872) | 67,935 (+3,467) | 108,859 (+6,339) |
|
||||
| 2025-07-10 | 43,796 (+2,872) | 71,402 (+3,467) | 115,198 (+6,339) |
|
||||
| 2025-07-11 | 46,982 (+3,186) | 77,462 (+6,060) | 124,444 (+9,246) |
|
||||
| 2025-07-12 | 49,302 (+2,320) | 82,177 (+4,715) | 131,479 (+7,035) |
|
||||
| 2025-07-13 | 50,803 (+1,501) | 86,394 (+4,217) | 137,197 (+5,718) |
|
||||
| 2025-07-14 | 53,283 (+2,480) | 87,860 (+1,466) | 141,143 (+3,946) |
|
||||
| 2025-07-15 | 57,590 (+4,307) | 91,036 (+3,176) | 148,626 (+7,483) |
|
||||
| 2025-07-16 | 62,313 (+4,723) | 95,258 (+4,222) | 157,571 (+8,945) |
|
||||
| 2025-07-17 | 66,684 (+4,371) | 100,048 (+4,790) | 166,732 (+9,161) |
|
||||
| 2025-07-18 | 70,379 (+3,695) | 102,587 (+2,539) | 172,966 (+6,234) |
|
||||
| 2025-07-19 | 73,497 (+3,117) | 105,904 (+3,317) | 179,401 (+6,434) |
|
||||
| 2025-07-20 | 76,453 (+2,956) | 109,044 (+3,140) | 185,497 (+6,096) |
|
||||
| 2025-07-21 | 80,197 (+3,744) | 113,537 (+4,493) | 193,734 (+8,237) |
|
||||
| 2025-07-22 | 84,251 (+4,054) | 118,073 (+4,536) | 202,324 (+8,590) |
|
||||
| 2025-07-23 | 88,589 (+4,338) | 121,436 (+3,363) | 210,025 (+7,701) |
|
||||
| 2025-07-24 | 92,469 (+3,880) | 124,091 (+2,655) | 216,560 (+6,535) |
|
||||
| 2025-07-25 | 96,417 (+3,948) | 126,985 (+2,894) | 223,402 (+6,842) |
|
||||
| 2025-07-26 | 100,646 (+4,229) | 131,411 (+4,426) | 232,057 (+8,655) |
|
||||
| 2025-07-27 | 102,644 (+1,998) | 134,736 (+3,325) | 237,380 (+5,323) |
|
||||
| 2025-07-28 | 105,446 (+2,802) | 136,016 (+1,280) | 241,462 (+4,082) |
|
||||
| 2025-07-29 | 108,998 (+3,552) | 137,542 (+1,526) | 246,540 (+5,078) |
|
||||
| 2025-07-30 | 113,544 (+4,546) | 140,317 (+2,775) | 253,861 (+7,321) |
|
||||
| 2025-07-31 | 118,339 (+4,795) | 143,344 (+3,027) | 261,683 (+7,822) |
|
||||
| 2025-08-01 | 123,539 (+5,200) | 146,680 (+3,336) | 270,219 (+8,536) |
|
||||
| 2025-08-02 | 127,864 (+4,325) | 149,236 (+2,556) | 277,100 (+6,881) |
|
||||
| 2025-08-03 | 131,397 (+3,533) | 150,451 (+1,215) | 281,848 (+4,748) |
|
||||
| 2025-08-04 | 136,266 (+4,869) | 153,260 (+2,809) | 289,526 (+7,678) |
|
||||
| 2025-08-05 | 141,596 (+5,330) | 155,752 (+2,492) | 297,348 (+7,822) |
|
||||
| 2025-08-06 | 147,067 (+5,471) | 158,309 (+2,557) | 305,376 (+8,028) |
|
||||
| 2025-08-07 | 152,591 (+5,524) | 160,889 (+2,580) | 313,480 (+8,104) |
|
||||
| 2025-08-08 | 158,187 (+5,596) | 163,448 (+2,559) | 321,635 (+8,155) |
|
||||
| 2025-08-09 | 162,770 (+4,583) | 165,721 (+2,273) | 328,491 (+6,856) |
|
||||
| 2025-08-10 | 165,695 (+2,925) | 167,109 (+1,388) | 332,804 (+4,313) |
|
||||
| 2025-08-11 | 169,297 (+3,602) | 167,953 (+844) | 337,250 (+4,446) |
|
||||
| 2025-08-12 | 176,307 (+7,010) | 171,876 (+3,923) | 348,183 (+10,933) |
|
||||
| 2025-08-13 | 182,997 (+6,690) | 177,182 (+5,306) | 360,179 (+11,996) |
|
||||
| 2025-08-14 | 189,063 (+6,066) | 179,741 (+2,559) | 368,804 (+8,625) |
|
||||
| 2025-08-15 | 193,608 (+4,545) | 181,792 (+2,051) | 375,400 (+6,596) |
|
||||
| 2025-08-16 | 198,118 (+4,510) | 184,558 (+2,766) | 382,676 (+7,276) |
|
||||
| 2025-08-17 | 201,299 (+3,181) | 186,269 (+1,711) | 387,568 (+4,892) |
|
||||
| 2025-08-18 | 204,559 (+3,260) | 187,399 (+1,130) | 391,958 (+4,390) |
|
||||
| 2025-08-19 | 209,814 (+5,255) | 189,668 (+2,269) | 399,482 (+7,524) |
|
||||
| 2025-08-20 | 214,497 (+4,683) | 191,481 (+1,813) | 405,978 (+6,496) |
|
||||
| 2025-08-21 | 220,465 (+5,968) | 194,784 (+3,303) | 415,249 (+9,271) |
|
||||
| 2025-08-22 | 225,899 (+5,434) | 197,204 (+2,420) | 423,103 (+7,854) |
|
||||
| 2025-08-23 | 229,005 (+3,106) | 199,238 (+2,034) | 428,243 (+5,140) |
|
||||
| 2025-08-24 | 232,098 (+3,093) | 201,157 (+1,919) | 433,255 (+5,012) |
|
||||
| 2025-08-25 | 236,607 (+4,509) | 202,650 (+1,493) | 439,257 (+6,002) |
|
||||
| 2025-08-26 | 242,783 (+6,176) | 205,242 (+2,592) | 448,025 (+8,768) |
|
||||
| 2025-08-27 | 248,409 (+5,626) | 205,242 (+0) | 453,651 (+5,626) |
|
||||
| 2025-08-28 | 252,796 (+4,387) | 205,242 (+0) | 458,038 (+4,387) |
|
||||
| 2025-08-29 | 256,045 (+3,249) | 211,075 (+5,833) | 467,120 (+9,082) |
|
||||
| 2025-08-30 | 258,863 (+2,818) | 212,397 (+1,322) | 471,260 (+4,140) |
|
||||
| 2025-08-31 | 262,004 (+3,141) | 213,944 (+1,547) | 475,948 (+4,688) |
|
||||
| 2025-09-01 | 265,359 (+3,355) | 215,115 (+1,171) | 480,474 (+4,526) |
|
||||
| 2025-09-02 | 270,483 (+5,124) | 217,075 (+1,960) | 487,558 (+7,084) |
|
||||
| 2025-09-03 | 274,793 (+4,310) | 219,755 (+2,680) | 494,548 (+6,990) |
|
||||
| 2025-09-04 | 280,430 (+5,637) | 222,103 (+2,348) | 502,533 (+7,985) |
|
||||
| 2025-09-05 | 283,769 (+3,339) | 223,793 (+1,690) | 507,562 (+5,029) |
|
||||
| 2025-09-06 | 286,245 (+2,476) | 225,036 (+1,243) | 511,281 (+3,719) |
|
||||
| 2025-09-07 | 288,623 (+2,378) | 225,866 (+830) | 514,489 (+3,208) |
|
||||
| 2025-09-08 | 293,341 (+4,718) | 227,073 (+1,207) | 520,414 (+5,925) |
|
||||
| 2025-09-09 | 300,036 (+6,695) | 229,788 (+2,715) | 529,824 (+9,410) |
|
||||
| 2025-09-10 | 307,287 (+7,251) | 233,435 (+3,647) | 540,722 (+10,898) |
|
||||
| 2025-09-11 | 314,083 (+6,796) | 237,356 (+3,921) | 551,439 (+10,717) |
|
||||
| 2025-09-12 | 321,046 (+6,963) | 240,728 (+3,372) | 561,774 (+10,335) |
|
||||
| 2025-09-13 | 324,894 (+3,848) | 245,539 (+4,811) | 570,433 (+8,659) |
|
||||
| 2025-09-14 | 328,876 (+3,982) | 248,245 (+2,706) | 577,121 (+6,688) |
|
||||
| 2025-09-15 | 334,201 (+5,325) | 250,983 (+2,738) | 585,184 (+8,063) |
|
||||
| 2025-09-16 | 342,609 (+8,408) | 255,264 (+4,281) | 597,873 (+12,689) |
|
||||
| 2025-09-17 | 351,117 (+8,508) | 260,970 (+5,706) | 612,087 (+14,214) |
|
||||
| 2025-09-18 | 358,717 (+7,600) | 266,922 (+5,952) | 625,639 (+13,552) |
|
||||
| 2025-09-19 | 365,401 (+6,684) | 271,859 (+4,937) | 637,260 (+11,621) |
|
||||
| 2025-09-20 | 372,092 (+6,691) | 276,917 (+5,058) | 649,009 (+11,749) |
|
||||
| 2025-09-21 | 377,079 (+4,987) | 280,261 (+3,344) | 657,340 (+8,331) |
|
||||
| 2025-09-22 | 382,492 (+5,413) | 284,009 (+3,748) | 666,501 (+9,161) |
|
||||
| 2025-09-23 | 387,008 (+4,516) | 289,129 (+5,120) | 676,137 (+9,636) |
|
||||
| 2025-09-24 | 393,325 (+6,317) | 294,927 (+5,798) | 688,252 (+12,115) |
|
||||
| 2025-09-25 | 398,879 (+5,554) | 301,663 (+6,736) | 700,542 (+12,290) |
|
||||
| 2025-09-26 | 404,334 (+5,455) | 306,713 (+5,050) | 711,047 (+10,505) |
|
||||
| 2025-09-27 | 411,618 (+7,284) | 317,763 (+11,050) | 729,381 (+18,334) |
|
||||
| 2025-09-28 | 414,910 (+3,292) | 322,522 (+4,759) | 737,432 (+8,051) |
|
||||
| 2025-09-29 | 419,919 (+5,009) | 328,033 (+5,511) | 747,952 (+10,520) |
|
||||
| 2025-09-30 | 427,991 (+8,072) | 336,472 (+8,439) | 764,463 (+16,511) |
|
||||
| 2025-10-01 | 433,591 (+5,600) | 341,742 (+5,270) | 775,333 (+10,870) |
|
||||
| 2025-10-02 | 440,852 (+7,261) | 348,099 (+6,357) | 788,951 (+13,618) |
|
||||
| 2025-10-03 | 446,829 (+5,977) | 359,937 (+11,838) | 806,766 (+17,815) |
|
||||
| 2025-10-04 | 452,561 (+5,732) | 370,386 (+10,449) | 822,947 (+16,181) |
|
||||
| 2025-10-05 | 455,559 (+2,998) | 374,745 (+4,359) | 830,304 (+7,357) |
|
||||
| 2025-10-06 | 460,927 (+5,368) | 379,489 (+4,744) | 840,416 (+10,112) |
|
||||
| 2025-10-07 | 467,336 (+6,409) | 385,438 (+5,949) | 852,774 (+12,358) |
|
||||
| 2025-10-08 | 474,643 (+7,307) | 394,139 (+8,701) | 868,782 (+16,008) |
|
||||
| 2025-10-09 | 479,203 (+4,560) | 400,526 (+6,387) | 879,729 (+10,947) |
|
||||
| 2025-10-10 | 484,374 (+5,171) | 406,015 (+5,489) | 890,389 (+10,660) |
|
||||
| 2025-10-11 | 488,427 (+4,053) | 414,699 (+8,684) | 903,126 (+12,737) |
|
||||
| 2025-10-12 | 492,125 (+3,698) | 418,745 (+4,046) | 910,870 (+7,744) |
|
||||
| 2025-10-14 | 505,130 (+13,005) | 429,286 (+10,541) | 934,416 (+23,546) |
|
||||
| 2025-10-15 | 512,717 (+7,587) | 439,290 (+10,004) | 952,007 (+17,591) |
|
||||
| 2025-10-16 | 517,719 (+5,002) | 447,137 (+7,847) | 964,856 (+12,849) |
|
||||
| 2025-10-17 | 526,239 (+8,520) | 457,467 (+10,330) | 983,706 (+18,850) |
|
||||
| 2025-10-18 | 531,564 (+5,325) | 465,272 (+7,805) | 996,836 (+13,130) |
|
||||
| 2025-10-19 | 536,209 (+4,645) | 469,078 (+3,806) | 1,005,287 (+8,451) |
|
||||
| 2025-10-20 | 541,264 (+5,055) | 472,952 (+3,874) | 1,014,216 (+8,929) |
|
||||
| 2025-10-21 | 548,721 (+7,457) | 479,703 (+6,751) | 1,028,424 (+14,208) |
|
||||
| 2025-10-22 | 557,949 (+9,228) | 491,395 (+11,692) | 1,049,344 (+20,920) |
|
||||
| 2025-10-23 | 564,716 (+6,767) | 498,736 (+7,341) | 1,063,452 (+14,108) |
|
||||
| 2025-10-24 | 572,692 (+7,976) | 506,905 (+8,169) | 1,079,597 (+16,145) |
|
||||
| 2025-10-25 | 578,927 (+6,235) | 516,129 (+9,224) | 1,095,056 (+15,459) |
|
||||
| 2025-10-26 | 584,409 (+5,482) | 521,179 (+5,050) | 1,105,588 (+10,532) |
|
||||
| 2025-10-27 | 589,999 (+5,590) | 526,001 (+4,822) | 1,116,000 (+10,412) |
|
||||
| 2025-10-28 | 595,776 (+5,777) | 532,438 (+6,437) | 1,128,214 (+12,214) |
|
||||
| 2025-10-29 | 606,259 (+10,483) | 542,064 (+9,626) | 1,148,323 (+20,109) |
|
||||
| 2025-10-30 | 613,746 (+7,487) | 542,064 (+0) | 1,155,810 (+7,487) |
|
||||
| 2025-10-30 | 617,846 (+4,100) | 555,026 (+12,962) | 1,172,872 (+17,062) |
|
||||
| 2025-10-31 | 626,612 (+8,766) | 564,579 (+9,553) | 1,191,191 (+18,319) |
|
||||
| 2025-11-01 | 636,100 (+9,488) | 581,806 (+17,227) | 1,217,906 (+26,715) |
|
||||
| 2025-11-02 | 644,067 (+7,967) | 590,004 (+8,198) | 1,234,071 (+16,165) |
|
||||
| 2025-11-03 | 653,130 (+9,063) | 597,139 (+7,135) | 1,250,269 (+16,198) |
|
||||
| 2025-11-04 | 663,912 (+10,782) | 608,056 (+10,917) | 1,271,968 (+21,699) |
|
||||
| 2025-11-05 | 675,074 (+11,162) | 619,690 (+11,634) | 1,294,764 (+22,796) |
|
||||
| 2025-11-06 | 686,252 (+11,178) | 630,885 (+11,195) | 1,317,137 (+22,373) |
|
||||
| 2025-11-07 | 696,646 (+10,394) | 642,146 (+11,261) | 1,338,792 (+21,655) |
|
||||
| 2025-11-08 | 706,035 (+9,389) | 653,489 (+11,343) | 1,359,524 (+20,732) |
|
||||
| 2025-11-09 | 713,462 (+7,427) | 660,459 (+6,970) | 1,373,921 (+14,397) |
|
||||
| 2025-11-10 | 722,288 (+8,826) | 668,225 (+7,766) | 1,390,513 (+16,592) |
|
||||
| 2025-11-11 | 729,769 (+7,481) | 677,501 (+9,276) | 1,407,270 (+16,757) |
|
||||
| 2025-11-12 | 740,180 (+10,411) | 686,454 (+8,953) | 1,426,634 (+19,364) |
|
||||
| 2025-11-13 | 749,905 (+9,725) | 696,157 (+9,703) | 1,446,062 (+19,428) |
|
||||
| 2025-11-14 | 759,928 (+10,023) | 705,237 (+9,080) | 1,465,165 (+19,103) |
|
||||
| 2025-11-15 | 765,955 (+6,027) | 712,870 (+7,633) | 1,478,825 (+13,660) |
|
||||
| 2025-11-16 | 771,069 (+5,114) | 716,596 (+3,726) | 1,487,665 (+8,840) |
|
||||
| 2025-11-17 | 780,161 (+9,092) | 723,339 (+6,743) | 1,503,500 (+15,835) |
|
||||
| 2025-11-18 | 791,563 (+11,402) | 732,544 (+9,205) | 1,524,107 (+20,607) |
|
||||
| 2025-11-19 | 804,409 (+12,846) | 747,624 (+15,080) | 1,552,033 (+27,926) |
|
||||
| 2025-11-20 | 814,620 (+10,211) | 757,907 (+10,283) | 1,572,527 (+20,494) |
|
||||
| 2025-11-21 | 826,309 (+11,689) | 769,307 (+11,400) | 1,595,616 (+23,089) |
|
||||
| 2025-11-22 | 837,269 (+10,960) | 780,996 (+11,689) | 1,618,265 (+22,649) |
|
||||
| 2025-11-23 | 846,609 (+9,340) | 795,069 (+14,073) | 1,641,678 (+23,413) |
|
||||
| 2025-11-24 | 856,733 (+10,124) | 804,033 (+8,964) | 1,660,766 (+19,088) |
|
||||
| 2025-11-25 | 869,423 (+12,690) | 817,339 (+13,306) | 1,686,762 (+25,996) |
|
||||
| 2025-11-26 | 881,414 (+11,991) | 832,518 (+15,179) | 1,713,932 (+27,170) |
|
||||
| 2025-11-27 | 893,960 (+12,546) | 846,180 (+13,662) | 1,740,140 (+26,208) |
|
||||
| 2025-11-28 | 901,741 (+7,781) | 856,482 (+10,302) | 1,758,223 (+18,083) |
|
||||
| 2025-11-29 | 908,689 (+6,948) | 863,361 (+6,879) | 1,772,050 (+13,827) |
|
||||
| 2025-11-30 | 916,116 (+7,427) | 870,194 (+6,833) | 1,786,310 (+14,260) |
|
||||
| 2025-12-01 | 925,898 (+9,782) | 876,500 (+6,306) | 1,802,398 (+16,088) |
|
||||
| 2025-12-02 | 939,250 (+13,352) | 890,919 (+14,419) | 1,830,169 (+27,771) |
|
||||
| 2025-12-03 | 952,249 (+12,999) | 903,713 (+12,794) | 1,855,962 (+25,793) |
|
||||
| 2025-12-04 | 965,611 (+13,362) | 916,471 (+12,758) | 1,882,082 (+26,120) |
|
||||
| 2025-12-05 | 977,996 (+12,385) | 930,616 (+14,145) | 1,908,612 (+26,530) |
|
||||
| 2025-12-06 | 987,884 (+9,888) | 943,773 (+13,157) | 1,931,657 (+23,045) |
|
||||
| 2025-12-07 | 994,046 (+6,162) | 951,425 (+7,652) | 1,945,471 (+13,814) |
|
||||
| 2025-12-08 | 1,000,898 (+6,852) | 957,149 (+5,724) | 1,958,047 (+12,576) |
|
||||
| 2025-12-09 | 1,011,488 (+10,590) | 973,922 (+16,773) | 1,985,410 (+27,363) |
|
||||
| 2025-12-10 | 1,025,891 (+14,403) | 991,708 (+17,786) | 2,017,599 (+32,189) |
|
||||
| 2025-12-11 | 1,045,110 (+19,219) | 1,010,559 (+18,851) | 2,055,669 (+38,070) |
|
||||
| 2025-12-12 | 1,061,340 (+16,230) | 1,030,838 (+20,279) | 2,092,178 (+36,509) |
|
||||
| 2025-12-13 | 1,073,561 (+12,221) | 1,044,608 (+13,770) | 2,118,169 (+25,991) |
|
||||
| 2025-12-14 | 1,082,042 (+8,481) | 1,052,425 (+7,817) | 2,134,467 (+16,298) |
|
||||
| 2025-12-15 | 1,093,632 (+11,590) | 1,059,078 (+6,653) | 2,152,710 (+18,243) |
|
||||
| 2025-12-16 | 1,120,477 (+26,845) | 1,078,022 (+18,944) | 2,198,499 (+45,789) |
|
||||
| 2025-12-17 | 1,151,067 (+30,590) | 1,097,661 (+19,639) | 2,248,728 (+50,229) |
|
||||
| 2025-12-18 | 1,178,658 (+27,591) | 1,113,418 (+15,757) | 2,292,076 (+43,348) |
|
||||
| 2025-12-19 | 1,203,485 (+24,827) | 1,129,698 (+16,280) | 2,333,183 (+41,107) |
|
||||
| 2025-12-20 | 1,223,000 (+19,515) | 1,146,258 (+16,560) | 2,369,258 (+36,075) |
|
||||
| 2025-12-21 | 1,242,675 (+19,675) | 1,158,909 (+12,651) | 2,401,584 (+32,326) |
|
||||
| 2025-12-22 | 1,262,522 (+19,847) | 1,169,121 (+10,212) | 2,431,643 (+30,059) |
|
||||
| 2025-12-23 | 1,286,548 (+24,026) | 1,186,439 (+17,318) | 2,472,987 (+41,344) |
|
||||
| 2025-12-24 | 1,309,323 (+22,775) | 1,203,767 (+17,328) | 2,513,090 (+40,103) |
|
||||
| 2025-12-25 | 1,333,032 (+23,709) | 1,217,283 (+13,516) | 2,550,315 (+37,225) |
|
||||
| 2025-12-26 | 1,352,411 (+19,379) | 1,227,615 (+10,332) | 2,580,026 (+29,711) |
|
||||
| 2025-12-27 | 1,371,771 (+19,360) | 1,238,236 (+10,621) | 2,610,007 (+29,981) |
|
||||
| 2025-12-28 | 1,390,388 (+18,617) | 1,245,690 (+7,454) | 2,636,078 (+26,071) |
|
||||
| 2025-12-29 | 1,415,560 (+25,172) | 1,257,101 (+11,411) | 2,672,661 (+36,583) |
|
||||
| 2025-12-30 | 1,445,450 (+29,890) | 1,272,689 (+15,588) | 2,718,139 (+45,478) |
|
||||
| 2025-12-31 | 1,479,598 (+34,148) | 1,293,235 (+20,546) | 2,772,833 (+54,694) |
|
||||
| 2026-01-01 | 1,508,883 (+29,285) | 1,309,874 (+16,639) | 2,818,757 (+45,924) |
|
||||
| 2026-01-02 | 1,563,474 (+54,591) | 1,320,959 (+11,085) | 2,884,433 (+65,676) |
|
||||
| 2026-01-03 | 1,618,065 (+54,591) | 1,331,914 (+10,955) | 2,949,979 (+65,546) |
|
||||
| 2026-01-04 | 1,672,656 (+39,702) | 1,339,883 (+7,969) | 3,012,539 (+62,560) |
|
||||
| 2026-01-05 | 1,738,171 (+65,515) | 1,353,043 (+13,160) | 3,091,214 (+78,675) |
|
||||
| 2026-01-06 | 1,960,988 (+222,817) | 1,377,377 (+24,334) | 3,338,365 (+247,151) |
|
||||
| 2026-01-07 | 2,123,239 (+162,251) | 1,398,648 (+21,271) | 3,521,887 (+183,522) |
|
||||
| 2026-01-08 | 2,272,630 (+149,391) | 1,432,480 (+33,832) | 3,705,110 (+183,223) |
|
||||
| 2026-01-09 | 2,443,565 (+170,935) | 1,469,451 (+36,971) | 3,913,016 (+207,906) |
|
||||
| 2026-01-10 | 2,632,023 (+188,458) | 1,503,670 (+34,219) | 4,135,693 (+222,677) |
|
||||
| 2026-01-11 | 2,836,394 (+204,371) | 1,530,479 (+26,809) | 4,366,873 (+231,180) |
|
||||
| 2026-01-12 | 3,053,594 (+217,200) | 1,553,671 (+23,192) | 4,607,265 (+240,392) |
|
||||
| 2026-01-13 | 3,297,078 (+243,484) | 1,595,062 (+41,391) | 4,892,140 (+284,875) |
|
||||
| 2026-01-14 | 3,568,928 (+271,850) | 1,645,362 (+50,300) | 5,214,290 (+322,150) |
|
||||
| Date | GitHub Downloads | npm Downloads | Total |
|
||||
| ---------- | -------------------- | -------------------- | -------------------- |
|
||||
| 2025-06-29 | 18,789 (+0) | 39,420 (+0) | 58,209 (+0) |
|
||||
| 2025-06-30 | 20,127 (+1,338) | 41,059 (+1,639) | 61,186 (+2,977) |
|
||||
| 2025-07-01 | 22,108 (+1,981) | 43,745 (+2,686) | 65,853 (+4,667) |
|
||||
| 2025-07-02 | 24,814 (+2,706) | 46,168 (+2,423) | 70,982 (+5,129) |
|
||||
| 2025-07-03 | 27,834 (+3,020) | 49,955 (+3,787) | 77,789 (+6,807) |
|
||||
| 2025-07-04 | 30,608 (+2,774) | 54,758 (+4,803) | 85,366 (+7,577) |
|
||||
| 2025-07-05 | 32,524 (+1,916) | 58,371 (+3,613) | 90,895 (+5,529) |
|
||||
| 2025-07-06 | 33,766 (+1,242) | 59,694 (+1,323) | 93,460 (+2,565) |
|
||||
| 2025-07-08 | 38,052 (+4,286) | 64,468 (+4,774) | 102,520 (+9,060) |
|
||||
| 2025-07-09 | 40,924 (+2,872) | 67,935 (+3,467) | 108,859 (+6,339) |
|
||||
| 2025-07-10 | 43,796 (+2,872) | 71,402 (+3,467) | 115,198 (+6,339) |
|
||||
| 2025-07-11 | 46,982 (+3,186) | 77,462 (+6,060) | 124,444 (+9,246) |
|
||||
| 2025-07-12 | 49,302 (+2,320) | 82,177 (+4,715) | 131,479 (+7,035) |
|
||||
| 2025-07-13 | 50,803 (+1,501) | 86,394 (+4,217) | 137,197 (+5,718) |
|
||||
| 2025-07-14 | 53,283 (+2,480) | 87,860 (+1,466) | 141,143 (+3,946) |
|
||||
| 2025-07-15 | 57,590 (+4,307) | 91,036 (+3,176) | 148,626 (+7,483) |
|
||||
| 2025-07-16 | 62,313 (+4,723) | 95,258 (+4,222) | 157,571 (+8,945) |
|
||||
| 2025-07-17 | 66,684 (+4,371) | 100,048 (+4,790) | 166,732 (+9,161) |
|
||||
| 2025-07-18 | 70,379 (+3,695) | 102,587 (+2,539) | 172,966 (+6,234) |
|
||||
| 2025-07-19 | 73,497 (+3,117) | 105,904 (+3,317) | 179,401 (+6,434) |
|
||||
| 2025-07-20 | 76,453 (+2,956) | 109,044 (+3,140) | 185,497 (+6,096) |
|
||||
| 2025-07-21 | 80,197 (+3,744) | 113,537 (+4,493) | 193,734 (+8,237) |
|
||||
| 2025-07-22 | 84,251 (+4,054) | 118,073 (+4,536) | 202,324 (+8,590) |
|
||||
| 2025-07-23 | 88,589 (+4,338) | 121,436 (+3,363) | 210,025 (+7,701) |
|
||||
| 2025-07-24 | 92,469 (+3,880) | 124,091 (+2,655) | 216,560 (+6,535) |
|
||||
| 2025-07-25 | 96,417 (+3,948) | 126,985 (+2,894) | 223,402 (+6,842) |
|
||||
| 2025-07-26 | 100,646 (+4,229) | 131,411 (+4,426) | 232,057 (+8,655) |
|
||||
| 2025-07-27 | 102,644 (+1,998) | 134,736 (+3,325) | 237,380 (+5,323) |
|
||||
| 2025-07-28 | 105,446 (+2,802) | 136,016 (+1,280) | 241,462 (+4,082) |
|
||||
| 2025-07-29 | 108,998 (+3,552) | 137,542 (+1,526) | 246,540 (+5,078) |
|
||||
| 2025-07-30 | 113,544 (+4,546) | 140,317 (+2,775) | 253,861 (+7,321) |
|
||||
| 2025-07-31 | 118,339 (+4,795) | 143,344 (+3,027) | 261,683 (+7,822) |
|
||||
| 2025-08-01 | 123,539 (+5,200) | 146,680 (+3,336) | 270,219 (+8,536) |
|
||||
| 2025-08-02 | 127,864 (+4,325) | 149,236 (+2,556) | 277,100 (+6,881) |
|
||||
| 2025-08-03 | 131,397 (+3,533) | 150,451 (+1,215) | 281,848 (+4,748) |
|
||||
| 2025-08-04 | 136,266 (+4,869) | 153,260 (+2,809) | 289,526 (+7,678) |
|
||||
| 2025-08-05 | 141,596 (+5,330) | 155,752 (+2,492) | 297,348 (+7,822) |
|
||||
| 2025-08-06 | 147,067 (+5,471) | 158,309 (+2,557) | 305,376 (+8,028) |
|
||||
| 2025-08-07 | 152,591 (+5,524) | 160,889 (+2,580) | 313,480 (+8,104) |
|
||||
| 2025-08-08 | 158,187 (+5,596) | 163,448 (+2,559) | 321,635 (+8,155) |
|
||||
| 2025-08-09 | 162,770 (+4,583) | 165,721 (+2,273) | 328,491 (+6,856) |
|
||||
| 2025-08-10 | 165,695 (+2,925) | 167,109 (+1,388) | 332,804 (+4,313) |
|
||||
| 2025-08-11 | 169,297 (+3,602) | 167,953 (+844) | 337,250 (+4,446) |
|
||||
| 2025-08-12 | 176,307 (+7,010) | 171,876 (+3,923) | 348,183 (+10,933) |
|
||||
| 2025-08-13 | 182,997 (+6,690) | 177,182 (+5,306) | 360,179 (+11,996) |
|
||||
| 2025-08-14 | 189,063 (+6,066) | 179,741 (+2,559) | 368,804 (+8,625) |
|
||||
| 2025-08-15 | 193,608 (+4,545) | 181,792 (+2,051) | 375,400 (+6,596) |
|
||||
| 2025-08-16 | 198,118 (+4,510) | 184,558 (+2,766) | 382,676 (+7,276) |
|
||||
| 2025-08-17 | 201,299 (+3,181) | 186,269 (+1,711) | 387,568 (+4,892) |
|
||||
| 2025-08-18 | 204,559 (+3,260) | 187,399 (+1,130) | 391,958 (+4,390) |
|
||||
| 2025-08-19 | 209,814 (+5,255) | 189,668 (+2,269) | 399,482 (+7,524) |
|
||||
| 2025-08-20 | 214,497 (+4,683) | 191,481 (+1,813) | 405,978 (+6,496) |
|
||||
| 2025-08-21 | 220,465 (+5,968) | 194,784 (+3,303) | 415,249 (+9,271) |
|
||||
| 2025-08-22 | 225,899 (+5,434) | 197,204 (+2,420) | 423,103 (+7,854) |
|
||||
| 2025-08-23 | 229,005 (+3,106) | 199,238 (+2,034) | 428,243 (+5,140) |
|
||||
| 2025-08-24 | 232,098 (+3,093) | 201,157 (+1,919) | 433,255 (+5,012) |
|
||||
| 2025-08-25 | 236,607 (+4,509) | 202,650 (+1,493) | 439,257 (+6,002) |
|
||||
| 2025-08-26 | 242,783 (+6,176) | 205,242 (+2,592) | 448,025 (+8,768) |
|
||||
| 2025-08-27 | 248,409 (+5,626) | 205,242 (+0) | 453,651 (+5,626) |
|
||||
| 2025-08-28 | 252,796 (+4,387) | 205,242 (+0) | 458,038 (+4,387) |
|
||||
| 2025-08-29 | 256,045 (+3,249) | 211,075 (+5,833) | 467,120 (+9,082) |
|
||||
| 2025-08-30 | 258,863 (+2,818) | 212,397 (+1,322) | 471,260 (+4,140) |
|
||||
| 2025-08-31 | 262,004 (+3,141) | 213,944 (+1,547) | 475,948 (+4,688) |
|
||||
| 2025-09-01 | 265,359 (+3,355) | 215,115 (+1,171) | 480,474 (+4,526) |
|
||||
| 2025-09-02 | 270,483 (+5,124) | 217,075 (+1,960) | 487,558 (+7,084) |
|
||||
| 2025-09-03 | 274,793 (+4,310) | 219,755 (+2,680) | 494,548 (+6,990) |
|
||||
| 2025-09-04 | 280,430 (+5,637) | 222,103 (+2,348) | 502,533 (+7,985) |
|
||||
| 2025-09-05 | 283,769 (+3,339) | 223,793 (+1,690) | 507,562 (+5,029) |
|
||||
| 2025-09-06 | 286,245 (+2,476) | 225,036 (+1,243) | 511,281 (+3,719) |
|
||||
| 2025-09-07 | 288,623 (+2,378) | 225,866 (+830) | 514,489 (+3,208) |
|
||||
| 2025-09-08 | 293,341 (+4,718) | 227,073 (+1,207) | 520,414 (+5,925) |
|
||||
| 2025-09-09 | 300,036 (+6,695) | 229,788 (+2,715) | 529,824 (+9,410) |
|
||||
| 2025-09-10 | 307,287 (+7,251) | 233,435 (+3,647) | 540,722 (+10,898) |
|
||||
| 2025-09-11 | 314,083 (+6,796) | 237,356 (+3,921) | 551,439 (+10,717) |
|
||||
| 2025-09-12 | 321,046 (+6,963) | 240,728 (+3,372) | 561,774 (+10,335) |
|
||||
| 2025-09-13 | 324,894 (+3,848) | 245,539 (+4,811) | 570,433 (+8,659) |
|
||||
| 2025-09-14 | 328,876 (+3,982) | 248,245 (+2,706) | 577,121 (+6,688) |
|
||||
| 2025-09-15 | 334,201 (+5,325) | 250,983 (+2,738) | 585,184 (+8,063) |
|
||||
| 2025-09-16 | 342,609 (+8,408) | 255,264 (+4,281) | 597,873 (+12,689) |
|
||||
| 2025-09-17 | 351,117 (+8,508) | 260,970 (+5,706) | 612,087 (+14,214) |
|
||||
| 2025-09-18 | 358,717 (+7,600) | 266,922 (+5,952) | 625,639 (+13,552) |
|
||||
| 2025-09-19 | 365,401 (+6,684) | 271,859 (+4,937) | 637,260 (+11,621) |
|
||||
| 2025-09-20 | 372,092 (+6,691) | 276,917 (+5,058) | 649,009 (+11,749) |
|
||||
| 2025-09-21 | 377,079 (+4,987) | 280,261 (+3,344) | 657,340 (+8,331) |
|
||||
| 2025-09-22 | 382,492 (+5,413) | 284,009 (+3,748) | 666,501 (+9,161) |
|
||||
| 2025-09-23 | 387,008 (+4,516) | 289,129 (+5,120) | 676,137 (+9,636) |
|
||||
| 2025-09-24 | 393,325 (+6,317) | 294,927 (+5,798) | 688,252 (+12,115) |
|
||||
| 2025-09-25 | 398,879 (+5,554) | 301,663 (+6,736) | 700,542 (+12,290) |
|
||||
| 2025-09-26 | 404,334 (+5,455) | 306,713 (+5,050) | 711,047 (+10,505) |
|
||||
| 2025-09-27 | 411,618 (+7,284) | 317,763 (+11,050) | 729,381 (+18,334) |
|
||||
| 2025-09-28 | 414,910 (+3,292) | 322,522 (+4,759) | 737,432 (+8,051) |
|
||||
| 2025-09-29 | 419,919 (+5,009) | 328,033 (+5,511) | 747,952 (+10,520) |
|
||||
| 2025-09-30 | 427,991 (+8,072) | 336,472 (+8,439) | 764,463 (+16,511) |
|
||||
| 2025-10-01 | 433,591 (+5,600) | 341,742 (+5,270) | 775,333 (+10,870) |
|
||||
| 2025-10-02 | 440,852 (+7,261) | 348,099 (+6,357) | 788,951 (+13,618) |
|
||||
| 2025-10-03 | 446,829 (+5,977) | 359,937 (+11,838) | 806,766 (+17,815) |
|
||||
| 2025-10-04 | 452,561 (+5,732) | 370,386 (+10,449) | 822,947 (+16,181) |
|
||||
| 2025-10-05 | 455,559 (+2,998) | 374,745 (+4,359) | 830,304 (+7,357) |
|
||||
| 2025-10-06 | 460,927 (+5,368) | 379,489 (+4,744) | 840,416 (+10,112) |
|
||||
| 2025-10-07 | 467,336 (+6,409) | 385,438 (+5,949) | 852,774 (+12,358) |
|
||||
| 2025-10-08 | 474,643 (+7,307) | 394,139 (+8,701) | 868,782 (+16,008) |
|
||||
| 2025-10-09 | 479,203 (+4,560) | 400,526 (+6,387) | 879,729 (+10,947) |
|
||||
| 2025-10-10 | 484,374 (+5,171) | 406,015 (+5,489) | 890,389 (+10,660) |
|
||||
| 2025-10-11 | 488,427 (+4,053) | 414,699 (+8,684) | 903,126 (+12,737) |
|
||||
| 2025-10-12 | 492,125 (+3,698) | 418,745 (+4,046) | 910,870 (+7,744) |
|
||||
| 2025-10-14 | 505,130 (+13,005) | 429,286 (+10,541) | 934,416 (+23,546) |
|
||||
| 2025-10-15 | 512,717 (+7,587) | 439,290 (+10,004) | 952,007 (+17,591) |
|
||||
| 2025-10-16 | 517,719 (+5,002) | 447,137 (+7,847) | 964,856 (+12,849) |
|
||||
| 2025-10-17 | 526,239 (+8,520) | 457,467 (+10,330) | 983,706 (+18,850) |
|
||||
| 2025-10-18 | 531,564 (+5,325) | 465,272 (+7,805) | 996,836 (+13,130) |
|
||||
| 2025-10-19 | 536,209 (+4,645) | 469,078 (+3,806) | 1,005,287 (+8,451) |
|
||||
| 2025-10-20 | 541,264 (+5,055) | 472,952 (+3,874) | 1,014,216 (+8,929) |
|
||||
| 2025-10-21 | 548,721 (+7,457) | 479,703 (+6,751) | 1,028,424 (+14,208) |
|
||||
| 2025-10-22 | 557,949 (+9,228) | 491,395 (+11,692) | 1,049,344 (+20,920) |
|
||||
| 2025-10-23 | 564,716 (+6,767) | 498,736 (+7,341) | 1,063,452 (+14,108) |
|
||||
| 2025-10-24 | 572,692 (+7,976) | 506,905 (+8,169) | 1,079,597 (+16,145) |
|
||||
| 2025-10-25 | 578,927 (+6,235) | 516,129 (+9,224) | 1,095,056 (+15,459) |
|
||||
| 2025-10-26 | 584,409 (+5,482) | 521,179 (+5,050) | 1,105,588 (+10,532) |
|
||||
| 2025-10-27 | 589,999 (+5,590) | 526,001 (+4,822) | 1,116,000 (+10,412) |
|
||||
| 2025-10-28 | 595,776 (+5,777) | 532,438 (+6,437) | 1,128,214 (+12,214) |
|
||||
| 2025-10-29 | 606,259 (+10,483) | 542,064 (+9,626) | 1,148,323 (+20,109) |
|
||||
| 2025-10-30 | 613,746 (+7,487) | 542,064 (+0) | 1,155,810 (+7,487) |
|
||||
| 2025-10-30 | 617,846 (+4,100) | 555,026 (+12,962) | 1,172,872 (+17,062) |
|
||||
| 2025-10-31 | 626,612 (+8,766) | 564,579 (+9,553) | 1,191,191 (+18,319) |
|
||||
| 2025-11-01 | 636,100 (+9,488) | 581,806 (+17,227) | 1,217,906 (+26,715) |
|
||||
| 2025-11-02 | 644,067 (+7,967) | 590,004 (+8,198) | 1,234,071 (+16,165) |
|
||||
| 2025-11-03 | 653,130 (+9,063) | 597,139 (+7,135) | 1,250,269 (+16,198) |
|
||||
| 2025-11-04 | 663,912 (+10,782) | 608,056 (+10,917) | 1,271,968 (+21,699) |
|
||||
| 2025-11-05 | 675,074 (+11,162) | 619,690 (+11,634) | 1,294,764 (+22,796) |
|
||||
| 2025-11-06 | 686,252 (+11,178) | 630,885 (+11,195) | 1,317,137 (+22,373) |
|
||||
| 2025-11-07 | 696,646 (+10,394) | 642,146 (+11,261) | 1,338,792 (+21,655) |
|
||||
| 2025-11-08 | 706,035 (+9,389) | 653,489 (+11,343) | 1,359,524 (+20,732) |
|
||||
| 2025-11-09 | 713,462 (+7,427) | 660,459 (+6,970) | 1,373,921 (+14,397) |
|
||||
| 2025-11-10 | 722,288 (+8,826) | 668,225 (+7,766) | 1,390,513 (+16,592) |
|
||||
| 2025-11-11 | 729,769 (+7,481) | 677,501 (+9,276) | 1,407,270 (+16,757) |
|
||||
| 2025-11-12 | 740,180 (+10,411) | 686,454 (+8,953) | 1,426,634 (+19,364) |
|
||||
| 2025-11-13 | 749,905 (+9,725) | 696,157 (+9,703) | 1,446,062 (+19,428) |
|
||||
| 2025-11-14 | 759,928 (+10,023) | 705,237 (+9,080) | 1,465,165 (+19,103) |
|
||||
| 2025-11-15 | 765,955 (+6,027) | 712,870 (+7,633) | 1,478,825 (+13,660) |
|
||||
| 2025-11-16 | 771,069 (+5,114) | 716,596 (+3,726) | 1,487,665 (+8,840) |
|
||||
| 2025-11-17 | 780,161 (+9,092) | 723,339 (+6,743) | 1,503,500 (+15,835) |
|
||||
| 2025-11-18 | 791,563 (+11,402) | 732,544 (+9,205) | 1,524,107 (+20,607) |
|
||||
| 2025-11-19 | 804,409 (+12,846) | 747,624 (+15,080) | 1,552,033 (+27,926) |
|
||||
| 2025-11-20 | 814,620 (+10,211) | 757,907 (+10,283) | 1,572,527 (+20,494) |
|
||||
| 2025-11-21 | 826,309 (+11,689) | 769,307 (+11,400) | 1,595,616 (+23,089) |
|
||||
| 2025-11-22 | 837,269 (+10,960) | 780,996 (+11,689) | 1,618,265 (+22,649) |
|
||||
| 2025-11-23 | 846,609 (+9,340) | 795,069 (+14,073) | 1,641,678 (+23,413) |
|
||||
| 2025-11-24 | 856,733 (+10,124) | 804,033 (+8,964) | 1,660,766 (+19,088) |
|
||||
| 2025-11-25 | 869,423 (+12,690) | 817,339 (+13,306) | 1,686,762 (+25,996) |
|
||||
| 2025-11-26 | 881,414 (+11,991) | 832,518 (+15,179) | 1,713,932 (+27,170) |
|
||||
| 2025-11-27 | 893,960 (+12,546) | 846,180 (+13,662) | 1,740,140 (+26,208) |
|
||||
| 2025-11-28 | 901,741 (+7,781) | 856,482 (+10,302) | 1,758,223 (+18,083) |
|
||||
| 2025-11-29 | 908,689 (+6,948) | 863,361 (+6,879) | 1,772,050 (+13,827) |
|
||||
| 2025-11-30 | 916,116 (+7,427) | 870,194 (+6,833) | 1,786,310 (+14,260) |
|
||||
| 2025-12-01 | 925,898 (+9,782) | 876,500 (+6,306) | 1,802,398 (+16,088) |
|
||||
| 2025-12-02 | 939,250 (+13,352) | 890,919 (+14,419) | 1,830,169 (+27,771) |
|
||||
| 2025-12-03 | 952,249 (+12,999) | 903,713 (+12,794) | 1,855,962 (+25,793) |
|
||||
| 2025-12-04 | 965,611 (+13,362) | 916,471 (+12,758) | 1,882,082 (+26,120) |
|
||||
| 2025-12-05 | 977,996 (+12,385) | 930,616 (+14,145) | 1,908,612 (+26,530) |
|
||||
| 2025-12-06 | 987,884 (+9,888) | 943,773 (+13,157) | 1,931,657 (+23,045) |
|
||||
| 2025-12-07 | 994,046 (+6,162) | 951,425 (+7,652) | 1,945,471 (+13,814) |
|
||||
| 2025-12-08 | 1,000,898 (+6,852) | 957,149 (+5,724) | 1,958,047 (+12,576) |
|
||||
| 2025-12-09 | 1,011,488 (+10,590) | 973,922 (+16,773) | 1,985,410 (+27,363) |
|
||||
| 2025-12-10 | 1,025,891 (+14,403) | 991,708 (+17,786) | 2,017,599 (+32,189) |
|
||||
| 2025-12-11 | 1,045,110 (+19,219) | 1,010,559 (+18,851) | 2,055,669 (+38,070) |
|
||||
| 2025-12-12 | 1,061,340 (+16,230) | 1,030,838 (+20,279) | 2,092,178 (+36,509) |
|
||||
| 2025-12-13 | 1,073,561 (+12,221) | 1,044,608 (+13,770) | 2,118,169 (+25,991) |
|
||||
| 2025-12-14 | 1,082,042 (+8,481) | 1,052,425 (+7,817) | 2,134,467 (+16,298) |
|
||||
| 2025-12-15 | 1,093,632 (+11,590) | 1,059,078 (+6,653) | 2,152,710 (+18,243) |
|
||||
| 2025-12-16 | 1,120,477 (+26,845) | 1,078,022 (+18,944) | 2,198,499 (+45,789) |
|
||||
| 2025-12-17 | 1,151,067 (+30,590) | 1,097,661 (+19,639) | 2,248,728 (+50,229) |
|
||||
| 2025-12-18 | 1,178,658 (+27,591) | 1,113,418 (+15,757) | 2,292,076 (+43,348) |
|
||||
| 2025-12-19 | 1,203,485 (+24,827) | 1,129,698 (+16,280) | 2,333,183 (+41,107) |
|
||||
| 2025-12-20 | 1,223,000 (+19,515) | 1,146,258 (+16,560) | 2,369,258 (+36,075) |
|
||||
| 2025-12-21 | 1,242,675 (+19,675) | 1,158,909 (+12,651) | 2,401,584 (+32,326) |
|
||||
| 2025-12-22 | 1,262,522 (+19,847) | 1,169,121 (+10,212) | 2,431,643 (+30,059) |
|
||||
| 2025-12-23 | 1,286,548 (+24,026) | 1,186,439 (+17,318) | 2,472,987 (+41,344) |
|
||||
| 2025-12-24 | 1,309,323 (+22,775) | 1,203,767 (+17,328) | 2,513,090 (+40,103) |
|
||||
| 2025-12-25 | 1,333,032 (+23,709) | 1,217,283 (+13,516) | 2,550,315 (+37,225) |
|
||||
| 2025-12-26 | 1,352,411 (+19,379) | 1,227,615 (+10,332) | 2,580,026 (+29,711) |
|
||||
| 2025-12-27 | 1,371,771 (+19,360) | 1,238,236 (+10,621) | 2,610,007 (+29,981) |
|
||||
| 2025-12-28 | 1,390,388 (+18,617) | 1,245,690 (+7,454) | 2,636,078 (+26,071) |
|
||||
| 2025-12-29 | 1,415,560 (+25,172) | 1,257,101 (+11,411) | 2,672,661 (+36,583) |
|
||||
| 2025-12-30 | 1,445,450 (+29,890) | 1,272,689 (+15,588) | 2,718,139 (+45,478) |
|
||||
| 2025-12-31 | 1,479,598 (+34,148) | 1,293,235 (+20,546) | 2,772,833 (+54,694) |
|
||||
| 2026-01-01 | 1,508,883 (+29,285) | 1,309,874 (+16,639) | 2,818,757 (+45,924) |
|
||||
| 2026-01-02 | 1,563,474 (+54,591) | 1,320,959 (+11,085) | 2,884,433 (+65,676) |
|
||||
| 2026-01-03 | 1,618,065 (+54,591) | 1,331,914 (+10,955) | 2,949,979 (+65,546) |
|
||||
| 2026-01-04 | 1,672,656 (+39,702) | 1,339,883 (+7,969) | 3,012,539 (+62,560) |
|
||||
| 2026-01-05 | 1,738,171 (+65,515) | 1,353,043 (+13,160) | 3,091,214 (+78,675) |
|
||||
| 2026-01-06 | 1,960,988 (+222,817) | 1,377,377 (+24,334) | 3,338,365 (+247,151) |
|
||||
| 2026-01-07 | 2,123,239 (+162,251) | 1,398,648 (+21,271) | 3,521,887 (+183,522) |
|
||||
| 2026-01-08 | 2,272,630 (+149,391) | 1,432,480 (+33,832) | 3,705,110 (+183,223) |
|
||||
| 2026-01-09 | 2,443,565 (+170,935) | 1,469,451 (+36,971) | 3,913,016 (+207,906) |
|
||||
| 2026-01-10 | 2,632,023 (+188,458) | 1,503,670 (+34,219) | 4,135,693 (+222,677) |
|
||||
| 2026-01-11 | 2,836,394 (+204,371) | 1,530,479 (+26,809) | 4,366,873 (+231,180) |
|
||||
| 2026-01-12 | 3,053,594 (+217,200) | 1,553,671 (+23,192) | 4,607,265 (+240,392) |
|
||||
| 2026-01-13 | 3,297,078 (+243,484) | 1,595,062 (+41,391) | 4,892,140 (+284,875) |
|
||||
| 2026-01-14 | 3,568,928 (+271,850) | 1,645,362 (+50,300) | 5,214,290 (+322,150) |
|
||||
| 2026-01-16 | 4,121,550 (+552,622) | 1,754,418 (+109,056) | 5,875,968 (+661,678) |
|
||||
| 2026-01-17 | 4,389,558 (+268,008) | 1,805,315 (+50,897) | 6,194,873 (+318,905) |
|
||||
| 2026-01-18 | 4,627,623 (+238,065) | 1,839,171 (+33,856) | 6,466,794 (+271,921) |
|
||||
| 2026-01-19 | 4,861,108 (+233,485) | 1,863,112 (+23,941) | 6,724,220 (+257,426) |
|
||||
| 2026-01-20 | 5,128,999 (+267,891) | 1,903,665 (+40,553) | 7,032,664 (+308,444) |
|
||||
| 2026-01-21 | 5,444,842 (+315,843) | 1,962,531 (+58,866) | 7,407,373 (+374,709) |
|
||||
|
||||
117
bun.lock
117
bun.lock
@@ -16,13 +16,14 @@
|
||||
"@tsconfig/bun": "catalog:",
|
||||
"husky": "9.1.7",
|
||||
"prettier": "3.6.2",
|
||||
"semver": "^7.6.0",
|
||||
"sst": "3.17.23",
|
||||
"turbo": "2.5.6",
|
||||
},
|
||||
},
|
||||
"packages/app": {
|
||||
"name": "@opencode-ai/app",
|
||||
"version": "1.1.21",
|
||||
"version": "1.1.31",
|
||||
"dependencies": {
|
||||
"@kobalte/core": "catalog:",
|
||||
"@opencode-ai/sdk": "workspace:*",
|
||||
@@ -32,6 +33,7 @@
|
||||
"@solid-primitives/active-element": "2.1.3",
|
||||
"@solid-primitives/audio": "1.4.2",
|
||||
"@solid-primitives/event-bus": "1.1.2",
|
||||
"@solid-primitives/i18n": "2.2.1",
|
||||
"@solid-primitives/media": "2.3.3",
|
||||
"@solid-primitives/resize-observer": "2.1.3",
|
||||
"@solid-primitives/scroll": "2.1.3",
|
||||
@@ -56,6 +58,7 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@happy-dom/global-registrator": "20.0.11",
|
||||
"@playwright/test": "1.57.0",
|
||||
"@tailwindcss/vite": "catalog:",
|
||||
"@tsconfig/bun": "1.0.9",
|
||||
"@types/bun": "catalog:",
|
||||
@@ -70,7 +73,7 @@
|
||||
},
|
||||
"packages/console/app": {
|
||||
"name": "@opencode-ai/console-app",
|
||||
"version": "1.1.21",
|
||||
"version": "1.1.31",
|
||||
"dependencies": {
|
||||
"@cloudflare/vite-plugin": "1.15.2",
|
||||
"@ibm/plex": "6.4.1",
|
||||
@@ -81,6 +84,8 @@
|
||||
"@opencode-ai/console-mail": "workspace:*",
|
||||
"@opencode-ai/console-resource": "workspace:*",
|
||||
"@opencode-ai/ui": "workspace:*",
|
||||
"@smithy/eventstream-codec": "4.2.7",
|
||||
"@smithy/util-utf8": "4.2.0",
|
||||
"@solidjs/meta": "catalog:",
|
||||
"@solidjs/router": "catalog:",
|
||||
"@solidjs/start": "catalog:",
|
||||
@@ -95,13 +100,14 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@typescript/native-preview": "catalog:",
|
||||
"@webgpu/types": "0.1.54",
|
||||
"typescript": "catalog:",
|
||||
"wrangler": "4.50.0",
|
||||
},
|
||||
},
|
||||
"packages/console/core": {
|
||||
"name": "@opencode-ai/console-core",
|
||||
"version": "1.1.21",
|
||||
"version": "1.1.31",
|
||||
"dependencies": {
|
||||
"@aws-sdk/client-sts": "3.782.0",
|
||||
"@jsx-email/render": "1.1.1",
|
||||
@@ -128,7 +134,7 @@
|
||||
},
|
||||
"packages/console/function": {
|
||||
"name": "@opencode-ai/console-function",
|
||||
"version": "1.1.21",
|
||||
"version": "1.1.31",
|
||||
"dependencies": {
|
||||
"@ai-sdk/anthropic": "2.0.0",
|
||||
"@ai-sdk/openai": "2.0.2",
|
||||
@@ -152,7 +158,7 @@
|
||||
},
|
||||
"packages/console/mail": {
|
||||
"name": "@opencode-ai/console-mail",
|
||||
"version": "1.1.21",
|
||||
"version": "1.1.31",
|
||||
"dependencies": {
|
||||
"@jsx-email/all": "2.2.3",
|
||||
"@jsx-email/cli": "1.4.3",
|
||||
@@ -176,7 +182,7 @@
|
||||
},
|
||||
"packages/desktop": {
|
||||
"name": "@opencode-ai/desktop",
|
||||
"version": "1.1.21",
|
||||
"version": "1.1.31",
|
||||
"dependencies": {
|
||||
"@opencode-ai/app": "workspace:*",
|
||||
"@opencode-ai/ui": "workspace:*",
|
||||
@@ -205,7 +211,7 @@
|
||||
},
|
||||
"packages/enterprise": {
|
||||
"name": "@opencode-ai/enterprise",
|
||||
"version": "1.1.21",
|
||||
"version": "1.1.31",
|
||||
"dependencies": {
|
||||
"@opencode-ai/ui": "workspace:*",
|
||||
"@opencode-ai/util": "workspace:*",
|
||||
@@ -234,7 +240,7 @@
|
||||
},
|
||||
"packages/function": {
|
||||
"name": "@opencode-ai/function",
|
||||
"version": "1.1.21",
|
||||
"version": "1.1.31",
|
||||
"dependencies": {
|
||||
"@octokit/auth-app": "8.0.1",
|
||||
"@octokit/rest": "catalog:",
|
||||
@@ -250,14 +256,14 @@
|
||||
},
|
||||
"packages/opencode": {
|
||||
"name": "opencode",
|
||||
"version": "1.1.21",
|
||||
"version": "1.1.31",
|
||||
"bin": {
|
||||
"opencode": "./bin/opencode",
|
||||
},
|
||||
"dependencies": {
|
||||
"@actions/core": "1.11.1",
|
||||
"@actions/github": "6.0.1",
|
||||
"@agentclientprotocol/sdk": "0.5.1",
|
||||
"@agentclientprotocol/sdk": "0.12.0",
|
||||
"@ai-sdk/amazon-bedrock": "3.0.73",
|
||||
"@ai-sdk/anthropic": "2.0.57",
|
||||
"@ai-sdk/azure": "2.0.91",
|
||||
@@ -278,7 +284,7 @@
|
||||
"@ai-sdk/vercel": "1.0.31",
|
||||
"@ai-sdk/xai": "2.0.51",
|
||||
"@clack/prompts": "1.0.0-alpha.1",
|
||||
"@gitlab/gitlab-ai-provider": "3.1.1",
|
||||
"@gitlab/gitlab-ai-provider": "3.1.3",
|
||||
"@hono/standard-validator": "0.1.5",
|
||||
"@hono/zod-validator": "catalog:",
|
||||
"@modelcontextprotocol/sdk": "1.25.2",
|
||||
@@ -290,8 +296,8 @@
|
||||
"@opencode-ai/sdk": "workspace:*",
|
||||
"@opencode-ai/util": "workspace:*",
|
||||
"@openrouter/ai-sdk-provider": "1.5.2",
|
||||
"@opentui/core": "0.1.73",
|
||||
"@opentui/solid": "0.1.73",
|
||||
"@opentui/core": "0.1.74",
|
||||
"@opentui/solid": "0.1.74",
|
||||
"@parcel/watcher": "2.5.1",
|
||||
"@pierre/diffs": "catalog:",
|
||||
"@solid-primitives/event-bus": "1.1.2",
|
||||
@@ -354,7 +360,7 @@
|
||||
},
|
||||
"packages/plugin": {
|
||||
"name": "@opencode-ai/plugin",
|
||||
"version": "1.1.21",
|
||||
"version": "1.1.31",
|
||||
"dependencies": {
|
||||
"@opencode-ai/sdk": "workspace:*",
|
||||
"zod": "catalog:",
|
||||
@@ -374,9 +380,9 @@
|
||||
},
|
||||
"packages/sdk/js": {
|
||||
"name": "@opencode-ai/sdk",
|
||||
"version": "1.1.21",
|
||||
"version": "1.1.31",
|
||||
"devDependencies": {
|
||||
"@hey-api/openapi-ts": "0.88.1",
|
||||
"@hey-api/openapi-ts": "0.90.4",
|
||||
"@tsconfig/node22": "catalog:",
|
||||
"@types/node": "catalog:",
|
||||
"@typescript/native-preview": "catalog:",
|
||||
@@ -385,7 +391,7 @@
|
||||
},
|
||||
"packages/slack": {
|
||||
"name": "@opencode-ai/slack",
|
||||
"version": "1.1.21",
|
||||
"version": "1.1.31",
|
||||
"dependencies": {
|
||||
"@opencode-ai/sdk": "workspace:*",
|
||||
"@slack/bolt": "^3.17.1",
|
||||
@@ -398,7 +404,7 @@
|
||||
},
|
||||
"packages/ui": {
|
||||
"name": "@opencode-ai/ui",
|
||||
"version": "1.1.21",
|
||||
"version": "1.1.31",
|
||||
"dependencies": {
|
||||
"@kobalte/core": "catalog:",
|
||||
"@opencode-ai/sdk": "workspace:*",
|
||||
@@ -410,7 +416,7 @@
|
||||
"@solid-primitives/resize-observer": "2.1.3",
|
||||
"@solidjs/meta": "catalog:",
|
||||
"@typescript/native-preview": "catalog:",
|
||||
"dompurify": "catalog:",
|
||||
"dompurify": "3.3.1",
|
||||
"fuzzysort": "catalog:",
|
||||
"katex": "0.16.27",
|
||||
"luxon": "catalog:",
|
||||
@@ -421,6 +427,7 @@
|
||||
"shiki": "catalog:",
|
||||
"solid-js": "catalog:",
|
||||
"solid-list": "catalog:",
|
||||
"strip-ansi": "7.1.2",
|
||||
"virtua": "catalog:",
|
||||
},
|
||||
"devDependencies": {
|
||||
@@ -438,7 +445,7 @@
|
||||
},
|
||||
"packages/util": {
|
||||
"name": "@opencode-ai/util",
|
||||
"version": "1.1.21",
|
||||
"version": "1.1.31",
|
||||
"dependencies": {
|
||||
"zod": "catalog:",
|
||||
},
|
||||
@@ -449,7 +456,7 @@
|
||||
},
|
||||
"packages/web": {
|
||||
"name": "@opencode-ai/web",
|
||||
"version": "1.1.21",
|
||||
"version": "1.1.31",
|
||||
"dependencies": {
|
||||
"@astrojs/cloudflare": "12.6.3",
|
||||
"@astrojs/markdown-remark": "6.3.1",
|
||||
@@ -498,6 +505,7 @@
|
||||
"@octokit/rest": "22.0.0",
|
||||
"@openauthjs/openauth": "0.0.0-20250322224806",
|
||||
"@pierre/diffs": "1.0.2",
|
||||
"@playwright/test": "1.51.0",
|
||||
"@solid-primitives/storage": "4.3.3",
|
||||
"@solidjs/meta": "0.29.4",
|
||||
"@solidjs/router": "0.15.4",
|
||||
@@ -508,6 +516,7 @@
|
||||
"@types/bun": "1.3.5",
|
||||
"@types/luxon": "3.7.1",
|
||||
"@types/node": "22.13.9",
|
||||
"@types/semver": "7.7.1",
|
||||
"@typescript/native-preview": "7.0.0-dev.20251207.1",
|
||||
"ai": "5.0.119",
|
||||
"diff": "8.0.2",
|
||||
@@ -545,7 +554,7 @@
|
||||
|
||||
"@adobe/css-tools": ["@adobe/css-tools@4.4.4", "", {}, "sha512-Elp+iwUx5rN5+Y8xLt5/GRoG20WGoDCQ/1Fb+1LiGtvwbDavuSk0jhD/eZdckHAuzcDzccnkv+rEjyWfRx18gg=="],
|
||||
|
||||
"@agentclientprotocol/sdk": ["@agentclientprotocol/sdk@0.5.1", "", { "dependencies": { "zod": "^3.0.0" } }, "sha512-9bq2TgjhLBSUSC5jE04MEe+Hqw8YePzKghhYZ9QcjOyonY3q2oJfX6GoSO83hURpEnsqEPIrex6VZN3+61fBJg=="],
|
||||
"@agentclientprotocol/sdk": ["@agentclientprotocol/sdk@0.12.0", "", { "peerDependencies": { "zod": "^3.25.0 || ^4.0.0" } }, "sha512-V8uH/KK1t7utqyJmTA7y7DzKu6+jKFIXM+ZVouz8E55j8Ej2RV42rEvPKn3/PpBJlliI5crcGk1qQhZ7VwaepA=="],
|
||||
|
||||
"@ai-sdk/amazon-bedrock": ["@ai-sdk/amazon-bedrock@3.0.73", "", { "dependencies": { "@ai-sdk/anthropic": "2.0.57", "@ai-sdk/provider": "2.0.1", "@ai-sdk/provider-utils": "3.0.20", "@smithy/eventstream-codec": "^4.0.1", "@smithy/util-utf8": "^4.0.0", "aws4fetch": "^1.0.20" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-EAAGJ/dfbAZaqIhK3w52hq6cftSLZwXdC6uHKh8Cls1T0N4MxS6ykDf54UyFO3bZWkQxR+Mdw1B3qireGOxtJQ=="],
|
||||
|
||||
@@ -913,17 +922,17 @@
|
||||
|
||||
"@fontsource/inter": ["@fontsource/inter@5.2.8", "", {}, "sha512-P6r5WnJoKiNVV+zvW2xM13gNdFhAEpQ9dQJHt3naLvfg+LkF2ldgSLiF4T41lf1SQCM9QmkqPTn4TH568IRagg=="],
|
||||
|
||||
"@gitlab/gitlab-ai-provider": ["@gitlab/gitlab-ai-provider@3.1.1", "", { "dependencies": { "@anthropic-ai/sdk": "^0.71.0", "@anycable/core": "^0.9.2", "graphql-request": "^6.1.0", "isomorphic-ws": "^5.0.0", "socket.io-client": "^4.8.1", "vscode-jsonrpc": "^8.2.1", "zod": "^3.25.76" }, "peerDependencies": { "@ai-sdk/provider": ">=2.0.0", "@ai-sdk/provider-utils": ">=3.0.0" } }, "sha512-7AtFrCflq2NzC99bj7YaqbQDCZyaScM1+L4ujllV5syiRTFE239Uhnd/yEkPXa7sUAnNRfN3CWusCkQ2zK/q9g=="],
|
||||
"@gitlab/gitlab-ai-provider": ["@gitlab/gitlab-ai-provider@3.1.3", "", { "dependencies": { "@anthropic-ai/sdk": "^0.71.0", "@anycable/core": "^0.9.2", "graphql-request": "^6.1.0", "isomorphic-ws": "^5.0.0", "socket.io-client": "^4.8.1", "vscode-jsonrpc": "^8.2.1", "zod": "^3.25.76" }, "peerDependencies": { "@ai-sdk/provider": ">=2.0.0", "@ai-sdk/provider-utils": ">=3.0.0" } }, "sha512-ikumi4PZN/S0f+j/5rb5dBRtORyT41Pl/tj8vHhnpFtpYcxXsaNv2RvCKBVf2/PovvSz2pYMOcpujIU4MdGfyQ=="],
|
||||
|
||||
"@graphql-typed-document-node/core": ["@graphql-typed-document-node/core@3.2.0", "", { "peerDependencies": { "graphql": "^0.8.0 || ^0.9.0 || ^0.10.0 || ^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0" } }, "sha512-mB9oAsNCm9aM3/SOv4YtBMqZbYj10R7dkq8byBqxGY/ncFwhf2oQzMV+LCRlWoDSEBJ3COiR1yeDvMtsoOsuFQ=="],
|
||||
|
||||
"@happy-dom/global-registrator": ["@happy-dom/global-registrator@20.0.11", "", { "dependencies": { "@types/node": "^20.0.0", "happy-dom": "^20.0.11" } }, "sha512-GqNqiShBT/lzkHTMC/slKBrvN0DsD4Di8ssBk4aDaVgEn+2WMzE6DXxq701ndSXj7/0cJ8mNT71pM7Bnrr6JRw=="],
|
||||
|
||||
"@hey-api/codegen-core": ["@hey-api/codegen-core@0.3.3", "", { "peerDependencies": { "typescript": ">=5.5.3" } }, "sha512-vArVDtrvdzFewu1hnjUm4jX1NBITlSCeO81EdWq676MxQbyxsGcDPAgohaSA+Wvr4HjPSvsg2/1s2zYxUtXebg=="],
|
||||
"@hey-api/codegen-core": ["@hey-api/codegen-core@0.5.2", "", { "dependencies": { "ansi-colors": "4.1.3", "color-support": "1.1.3" }, "peerDependencies": { "typescript": ">=5.5.3" } }, "sha512-88cqrrB2cLXN8nMOHidQTcVOnZsJ5kebEbBefjMCifaUCwTA30ouSSWvTZqrOX4O104zjJyu7M8Gcv/NNYQuaA=="],
|
||||
|
||||
"@hey-api/json-schema-ref-parser": ["@hey-api/json-schema-ref-parser@1.2.2", "", { "dependencies": { "@jsdevtools/ono": "^7.1.3", "@types/json-schema": "^7.0.15", "js-yaml": "^4.1.1", "lodash": "^4.17.21" } }, "sha512-oS+5yAdwnK20lSeFO1d53Ku+yaGCsY8PcrmSq2GtSs3bsBfRnHAbpPKSVzQcaxAOrzj5NB+f34WhZglVrNayBA=="],
|
||||
|
||||
"@hey-api/openapi-ts": ["@hey-api/openapi-ts@0.88.1", "", { "dependencies": { "@hey-api/codegen-core": "^0.3.3", "@hey-api/json-schema-ref-parser": "1.2.2", "ansi-colors": "4.1.3", "c12": "3.3.2", "color-support": "1.1.3", "commander": "14.0.2", "open": "11.0.0", "semver": "7.7.2" }, "peerDependencies": { "typescript": ">=5.5.3" }, "bin": { "openapi-ts": "bin/run.js" } }, "sha512-x/nDTupOnV9VuSeNIiJpgIpc915GHduhyseJeMTnI0JMsXaObmpa0rgPr3ASVEYMLgpvqozIEG1RTOOnal6zLQ=="],
|
||||
"@hey-api/openapi-ts": ["@hey-api/openapi-ts@0.90.4", "", { "dependencies": { "@hey-api/codegen-core": "^0.5.2", "@hey-api/json-schema-ref-parser": "1.2.2", "ansi-colors": "4.1.3", "c12": "3.3.3", "color-support": "1.1.3", "commander": "14.0.2", "open": "11.0.0", "semver": "7.7.3" }, "peerDependencies": { "typescript": ">=5.5.3" }, "bin": { "openapi-ts": "bin/run.js" } }, "sha512-9l++kjcb0ui4JqPlueZ6OZ9zKn6eK/8//Z2jHcIXb5MRwDRgubOOSpTU5llEv3uvWfT10VzcMp99dySWq0AASw=="],
|
||||
|
||||
"@hono/node-server": ["@hono/node-server@1.19.7", "", { "peerDependencies": { "hono": "^4" } }, "sha512-vUcD0uauS7EU2caukW8z5lJKtoGMokxNbJtBiwHgpqxEXokaHCBkQUmCHhjFB1VUTWdqj25QoMkMKzgjq+uhrw=="],
|
||||
|
||||
@@ -1215,21 +1224,21 @@
|
||||
|
||||
"@opentelemetry/api": ["@opentelemetry/api@1.9.0", "", {}, "sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg=="],
|
||||
|
||||
"@opentui/core": ["@opentui/core@0.1.73", "", { "dependencies": { "bun-ffi-structs": "0.1.2", "diff": "8.0.2", "jimp": "1.6.0", "yoga-layout": "3.2.1" }, "optionalDependencies": { "@dimforge/rapier2d-simd-compat": "^0.17.3", "@opentui/core-darwin-arm64": "0.1.73", "@opentui/core-darwin-x64": "0.1.73", "@opentui/core-linux-arm64": "0.1.73", "@opentui/core-linux-x64": "0.1.73", "@opentui/core-win32-arm64": "0.1.73", "@opentui/core-win32-x64": "0.1.73", "bun-webgpu": "0.1.4", "planck": "^1.4.2", "three": "0.177.0" }, "peerDependencies": { "web-tree-sitter": "0.25.10" } }, "sha512-1OqLlArzUh3QjrYXGro5WKNgoCcacGJaaFvwOHg5lAOoSigFQRiqEUEEJLbSo3pyV8u7XEdC3M0rOP6K+oThzw=="],
|
||||
"@opentui/core": ["@opentui/core@0.1.74", "", { "dependencies": { "bun-ffi-structs": "0.1.2", "diff": "8.0.2", "jimp": "1.6.0", "yoga-layout": "3.2.1" }, "optionalDependencies": { "@dimforge/rapier2d-simd-compat": "^0.17.3", "@opentui/core-darwin-arm64": "0.1.74", "@opentui/core-darwin-x64": "0.1.74", "@opentui/core-linux-arm64": "0.1.74", "@opentui/core-linux-x64": "0.1.74", "@opentui/core-win32-arm64": "0.1.74", "@opentui/core-win32-x64": "0.1.74", "bun-webgpu": "0.1.4", "planck": "^1.4.2", "three": "0.177.0" }, "peerDependencies": { "web-tree-sitter": "0.25.10" } }, "sha512-g4W16ymv12JdgZ+9B4t7mpIICvzWy2+eHERfmDf80ALduOQCUedKQdULcBFhVCYUXIkDRtIy6CID5thMAah3FA=="],
|
||||
|
||||
"@opentui/core-darwin-arm64": ["@opentui/core-darwin-arm64@0.1.73", "", { "os": "darwin", "cpu": "arm64" }, "sha512-Xnc8S6kGIVcdwqqTq6jk50UVe1QtOXp+B0v4iH85iNW1Ljf198OoA7RcVA+edFb6o01PVwnhIIPtpkB/A4710w=="],
|
||||
"@opentui/core-darwin-arm64": ["@opentui/core-darwin-arm64@0.1.74", "", { "os": "darwin", "cpu": "arm64" }, "sha512-rfmlDLtm/u17CnuhJgCxPeYMvOST+A2MOdVOk46IurtHO849bdYqK6iudKNlFRs1FOrymgSKF9GlWBHAOKeRjg=="],
|
||||
|
||||
"@opentui/core-darwin-x64": ["@opentui/core-darwin-x64@0.1.73", "", { "os": "darwin", "cpu": "x64" }, "sha512-RlgxQxu+kxsCZzeXRnpYrqbrpxbG8M/lnDf4sTPWmhXUiuDvY5BdB4YiBY5bv8eNdJ1j9HiMLtx6ZxElEviidA=="],
|
||||
"@opentui/core-darwin-x64": ["@opentui/core-darwin-x64@0.1.74", "", { "os": "darwin", "cpu": "x64" }, "sha512-WAD8orsDV0ZdW/5GwjOOB4FY96772xbkz+rcV7WRzEFUVaqoBaC04IuqYzS9d5s+cjkbT5Cpj47hrVYkkVQKng=="],
|
||||
|
||||
"@opentui/core-linux-arm64": ["@opentui/core-linux-arm64@0.1.73", "", { "os": "linux", "cpu": "arm64" }, "sha512-9I88BdZMB3qtDPtDzFTg1EEt6sAGFSpOEmIIMB3MhqZqoq9+WSEyJZxM0/kff5vt4RJnqG7vz4fKMVRwNrUPGA=="],
|
||||
"@opentui/core-linux-arm64": ["@opentui/core-linux-arm64@0.1.74", "", { "os": "linux", "cpu": "arm64" }, "sha512-lgmHzrzLy4e+rgBS+lhtsMLLgIMLbtLNMm6EzVPyYVDlLDGjM7+ulXMem7AtpaRrWrUUl4REiG9BoQUsCFDwYA=="],
|
||||
|
||||
"@opentui/core-linux-x64": ["@opentui/core-linux-x64@0.1.73", "", { "os": "linux", "cpu": "x64" }, "sha512-50cGZkCh/i3nzijsjUnkmtWJtnJ6l9WpdIwSJsO2Id7nZdzupT1b6AkgGZdOgNl23MHXpAitmb+MhEAjAimCRA=="],
|
||||
"@opentui/core-linux-x64": ["@opentui/core-linux-x64@0.1.74", "", { "os": "linux", "cpu": "x64" }, "sha512-8Mn2WbdBQ29xCThuPZezjDhd1N3+fXwKkGvCBOdTI0le6h2A/vCNbfUVjwfr/EGZSRXxCG+Yapol34BAULGpOA=="],
|
||||
|
||||
"@opentui/core-win32-arm64": ["@opentui/core-win32-arm64@0.1.73", "", { "os": "win32", "cpu": "arm64" }, "sha512-mFiEeoiim5cmi6qu8CDfeecl9ivuMilfby/GnqTsr9G8e52qfT6nWF2m9Nevh9ebhXK+D/VnVhJIbObc0WIchA=="],
|
||||
"@opentui/core-win32-arm64": ["@opentui/core-win32-arm64@0.1.74", "", { "os": "win32", "cpu": "arm64" }, "sha512-dvYUXz03avnI6ZluyLp00HPmR0UT/IE/6QS97XBsgJlUTtpnbKkBtB5jD1NHwWkElaRj1Qv2QP36ngFoJqbl9g=="],
|
||||
|
||||
"@opentui/core-win32-x64": ["@opentui/core-win32-x64@0.1.73", "", { "os": "win32", "cpu": "x64" }, "sha512-vzWHUi2vgwImuyxl+hlmK0aeCbnwozeuicIcHJE0orPOwp2PAKyR9WO330szAvfIO5ZPbNkjWfh6xIYnASM0lQ=="],
|
||||
"@opentui/core-win32-x64": ["@opentui/core-win32-x64@0.1.74", "", { "os": "win32", "cpu": "x64" }, "sha512-3wfWXaAKOIlDQz6ZZIESf2M+YGZ7uFHijjTEM8w/STRlLw8Y6+QyGYi1myHSM4d6RSO+/s2EMDxvjDf899W9vQ=="],
|
||||
|
||||
"@opentui/solid": ["@opentui/solid@0.1.73", "", { "dependencies": { "@babel/core": "7.28.0", "@babel/preset-typescript": "7.27.1", "@opentui/core": "0.1.73", "babel-plugin-module-resolver": "5.0.2", "babel-preset-solid": "1.9.9", "s-js": "^0.4.9" }, "peerDependencies": { "solid-js": "1.9.9" } }, "sha512-FBSTiuWl+hHqFxmrJfC93cbJ0PJ4QoFbvRFuD6Gzrea5rH+G7BidjyI8YZuCcNnriDuIYaXTJdvBqe15lgKR1A=="],
|
||||
"@opentui/solid": ["@opentui/solid@0.1.74", "", { "dependencies": { "@babel/core": "7.28.0", "@babel/preset-typescript": "7.27.1", "@opentui/core": "0.1.74", "babel-plugin-module-resolver": "5.0.2", "babel-preset-solid": "1.9.9", "s-js": "^0.4.9" }, "peerDependencies": { "solid-js": "1.9.9" } }, "sha512-Vz82cI8T9YeJjGsVg4ULp6ral4N+xyt1j9A6Tbu3aaQgEKiB74LW03EXREehfjPr1irOFxtKfWPbx5NKH0Upag=="],
|
||||
|
||||
"@oslojs/asn1": ["@oslojs/asn1@1.0.0", "", { "dependencies": { "@oslojs/binary": "1.0.0" } }, "sha512-zw/wn0sj0j0QKbIXfIlnEcTviaCzYOY3V5rAyjR6YtOByFtJiT574+8p9Wlach0lZH9fddD4yb9laEAIl4vXQA=="],
|
||||
|
||||
@@ -1351,6 +1360,8 @@
|
||||
|
||||
"@planetscale/database": ["@planetscale/database@1.19.0", "", {}, "sha512-Tv4jcFUFAFjOWrGSio49H6R2ijALv0ZzVBfJKIdm+kl9X046Fh4LLawrF9OMsglVbK6ukqMJsUCeucGAFTBcMA=="],
|
||||
|
||||
"@playwright/test": ["@playwright/test@1.57.0", "", { "dependencies": { "playwright": "1.57.0" }, "bin": { "playwright": "cli.js" } }, "sha512-6TyEnHgd6SArQO8UO2OMTxshln3QMWBtPGrOCgs3wVEmQmwyuNtB10IZMfmYDE0riwNR1cu4q+pPcxMVtaG3TA=="],
|
||||
|
||||
"@poppinss/colors": ["@poppinss/colors@4.1.5", "", { "dependencies": { "kleur": "^4.1.5" } }, "sha512-FvdDqtcRCtz6hThExcFOgW0cWX+xwSMWcRuQe5ZEb2m7cVQOAVZOIMt+/v9RxGiD9/OY16qJBXK4CVKWAPalBw=="],
|
||||
|
||||
"@poppinss/dumper": ["@poppinss/dumper@0.6.5", "", { "dependencies": { "@poppinss/colors": "^4.1.5", "@sindresorhus/is": "^7.0.2", "supports-color": "^10.0.0" } }, "sha512-NBdYIb90J7LfOI32dOewKI1r7wnkiH6m920puQ3qHUeZkxNkQiFnXVWoE6YtFSv6QOiPPf7ys6i+HWWecDz7sw=="],
|
||||
@@ -1521,7 +1532,7 @@
|
||||
|
||||
"@smithy/credential-provider-imds": ["@smithy/credential-provider-imds@4.2.5", "", { "dependencies": { "@smithy/node-config-provider": "^4.3.5", "@smithy/property-provider": "^4.2.5", "@smithy/types": "^4.9.0", "@smithy/url-parser": "^4.2.5", "tslib": "^2.6.2" } }, "sha512-BZwotjoZWn9+36nimwm/OLIcVe+KYRwzMjfhd4QT7QxPm9WY0HiOV8t/Wlh+HVUif0SBVV7ksq8//hPaBC/okQ=="],
|
||||
|
||||
"@smithy/eventstream-codec": ["@smithy/eventstream-codec@4.2.5", "", { "dependencies": { "@aws-crypto/crc32": "5.2.0", "@smithy/types": "^4.9.0", "@smithy/util-hex-encoding": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-Ogt4Zi9hEbIP17oQMd68qYOHUzmH47UkK7q7Gl55iIm9oKt27MUGrC5JfpMroeHjdkOliOA4Qt3NQ1xMq/nrlA=="],
|
||||
"@smithy/eventstream-codec": ["@smithy/eventstream-codec@4.2.7", "", { "dependencies": { "@aws-crypto/crc32": "5.2.0", "@smithy/types": "^4.11.0", "@smithy/util-hex-encoding": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-DrpkEoM3j9cBBWhufqBwnbbn+3nf1N9FP6xuVJ+e220jbactKuQgaZwjwP5CP1t+O94brm2JgVMD2atMGX3xIQ=="],
|
||||
|
||||
"@smithy/eventstream-serde-browser": ["@smithy/eventstream-serde-browser@4.2.5", "", { "dependencies": { "@smithy/eventstream-serde-universal": "^4.2.5", "@smithy/types": "^4.9.0", "tslib": "^2.6.2" } }, "sha512-HohfmCQZjppVnKX2PnXlf47CW3j92Ki6T/vkAT2DhBR47e89pen3s4fIa7otGTtrVxmj7q+IhH0RnC5kpR8wtw=="],
|
||||
|
||||
@@ -1623,6 +1634,8 @@
|
||||
|
||||
"@solid-primitives/event-listener": ["@solid-primitives/event-listener@2.4.3", "", { "dependencies": { "@solid-primitives/utils": "^6.3.2" }, "peerDependencies": { "solid-js": "^1.6.12" } }, "sha512-h4VqkYFv6Gf+L7SQj+Y6puigL/5DIi7x5q07VZET7AWcS+9/G3WfIE9WheniHWJs51OEkRB43w6lDys5YeFceg=="],
|
||||
|
||||
"@solid-primitives/i18n": ["@solid-primitives/i18n@2.2.1", "", { "peerDependencies": { "solid-js": "^1.6.12" } }, "sha512-TnTnE2Ku11MGYZ1JzhJ8pYscwg1fr9MteoYxPwsfxWfh9Jp5K7RRJncJn9BhOHvNLwROjqOHZ46PT7sPHqbcXw=="],
|
||||
|
||||
"@solid-primitives/keyed": ["@solid-primitives/keyed@1.5.2", "", { "peerDependencies": { "solid-js": "^1.6.12" } }, "sha512-BgoEdqPw48URnI+L5sZIHdF4ua4Las1eWEBBPaoSFs42kkhnHue+rwCBPL2Z9ebOyQ75sUhUfOETdJfmv0D6Kg=="],
|
||||
|
||||
"@solid-primitives/map": ["@solid-primitives/map@0.4.13", "", { "dependencies": { "@solid-primitives/trigger": "^1.1.0" }, "peerDependencies": { "solid-js": "^1.6.12" } }, "sha512-B1zyFbsiTQvqPr+cuPCXO72sRuczG9Swncqk5P74NCGw1VE8qa/Ry9GlfI1e/VdeQYHjan+XkbE3rO2GW/qKew=="],
|
||||
@@ -1903,7 +1916,7 @@
|
||||
|
||||
"@vitest/utils": ["@vitest/utils@4.0.16", "", { "dependencies": { "@vitest/pretty-format": "4.0.16", "tinyrainbow": "^3.0.3" } }, "sha512-h8z9yYhV3e1LEfaQ3zdypIrnAg/9hguReGZoS7Gl0aBG5xgA410zBqECqmaF/+RkTggRsfnzc1XaAHA6bmUufA=="],
|
||||
|
||||
"@webgpu/types": ["@webgpu/types@0.1.66", "", {}, "sha512-YA2hLrwLpDsRueNDXIMqN9NTzD6bCDkuXbOSe0heS+f8YE8usA6Gbv1prj81pzVHrbaAma7zObnIC+I6/sXJgA=="],
|
||||
"@webgpu/types": ["@webgpu/types@0.1.54", "", {}, "sha512-81oaalC8LFrXjhsczomEQ0u3jG+TqE6V9QHLA8GNZq/Rnot0KDugu3LhSYSlie8tSdooAN1Hov05asrUUp9qgg=="],
|
||||
|
||||
"@zip.js/zip.js": ["@zip.js/zip.js@2.7.62", "", {}, "sha512-OaLvZ8j4gCkLn048ypkZu29KX30r8/OfFF2w4Jo5WXFr+J04J+lzJ5TKZBVgFXhlvSkqNFQdfnY1Q8TMTCyBVA=="],
|
||||
|
||||
@@ -2091,7 +2104,7 @@
|
||||
|
||||
"bytes": ["bytes@3.1.2", "", {}, "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg=="],
|
||||
|
||||
"c12": ["c12@3.3.2", "", { "dependencies": { "chokidar": "^4.0.3", "confbox": "^0.2.2", "defu": "^6.1.4", "dotenv": "^17.2.3", "exsolve": "^1.0.8", "giget": "^2.0.0", "jiti": "^2.6.1", "ohash": "^2.0.11", "pathe": "^2.0.3", "perfect-debounce": "^2.0.0", "pkg-types": "^2.3.0", "rc9": "^2.1.2" }, "peerDependencies": { "magicast": "*" }, "optionalPeers": ["magicast"] }, "sha512-QkikB2X5voO1okL3QsES0N690Sn/K9WokXqUsDQsWy5SnYb+psYQFGA10iy1bZHj3fjISKsI67Q90gruvWWM3A=="],
|
||||
"c12": ["c12@3.3.3", "", { "dependencies": { "chokidar": "^5.0.0", "confbox": "^0.2.2", "defu": "^6.1.4", "dotenv": "^17.2.3", "exsolve": "^1.0.8", "giget": "^2.0.0", "jiti": "^2.6.1", "ohash": "^2.0.11", "pathe": "^2.0.3", "perfect-debounce": "^2.0.0", "pkg-types": "^2.3.0", "rc9": "^2.1.2" }, "peerDependencies": { "magicast": "*" }, "optionalPeers": ["magicast"] }, "sha512-750hTRvgBy5kcMNPdh95Qo+XUBeGo8C7nsKSmedDmaQI+E0r82DwHeM6vBewDe4rGFbnxoa4V9pw+sPh5+Iz8Q=="],
|
||||
|
||||
"call-bind": ["call-bind@1.0.8", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.0", "es-define-property": "^1.0.0", "get-intrinsic": "^1.2.4", "set-function-length": "^1.2.2" } }, "sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww=="],
|
||||
|
||||
@@ -3287,6 +3300,10 @@
|
||||
|
||||
"planck": ["planck@1.4.2", "", { "peerDependencies": { "stage-js": "^1.0.0-alpha.12" } }, "sha512-mNbhnV3g8X2rwGxzcesjmN8BDA6qfXgQxXVMkWau9MCRlQY0RLNEkyHlVp6yFy/X6qrzAXyNONCnZ1cGDLrNew=="],
|
||||
|
||||
"playwright": ["playwright@1.57.0", "", { "dependencies": { "playwright-core": "1.57.0" }, "optionalDependencies": { "fsevents": "2.3.2" }, "bin": { "playwright": "cli.js" } }, "sha512-ilYQj1s8sr2ppEJ2YVadYBN0Mb3mdo9J0wQ+UuDhzYqURwSoW4n1Xs5vs7ORwgDGmyEh33tRMeS8KhdkMoLXQw=="],
|
||||
|
||||
"playwright-core": ["playwright-core@1.57.0", "", { "bin": { "playwright-core": "cli.js" } }, "sha512-agTcKlMw/mjBWOnD6kFZttAAGHgi/Nw0CZ2o6JqWSbMlI219lAFLZZCyqByTsvVAJq5XA5H8cA6PrvBRpBWEuQ=="],
|
||||
|
||||
"pngjs": ["pngjs@7.0.0", "", {}, "sha512-LKWqWJRhstyYo9pGvgor/ivk2w94eSjE3RGVuzLGlr3NmD8bf7RcYGze1mNdEHRP6TRP6rMuDHk5t44hnTRyow=="],
|
||||
|
||||
"possible-typed-array-names": ["possible-typed-array-names@1.1.0", "", {}, "sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg=="],
|
||||
@@ -3491,7 +3508,7 @@
|
||||
|
||||
"selderee": ["selderee@0.11.0", "", { "dependencies": { "parseley": "^0.12.0" } }, "sha512-5TF+l7p4+OsnP8BCCvSyZiSPc4x4//p5uPwK8TCnVPJYRmU2aYKMpOXvw8zM5a5JvuuCGN1jmsMwuU2W02ukfA=="],
|
||||
|
||||
"semver": ["semver@7.7.2", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA=="],
|
||||
"semver": ["semver@7.7.3", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q=="],
|
||||
|
||||
"send": ["send@0.19.0", "", { "dependencies": { "debug": "2.6.9", "depd": "2.0.0", "destroy": "1.2.0", "encodeurl": "~1.0.2", "escape-html": "~1.0.3", "etag": "~1.8.1", "fresh": "0.5.2", "http-errors": "2.0.0", "mime": "1.6.0", "ms": "2.1.3", "on-finished": "2.4.1", "range-parser": "~1.2.1", "statuses": "2.0.1" } }, "sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw=="],
|
||||
|
||||
@@ -3961,10 +3978,10 @@
|
||||
|
||||
"@actions/http-client/undici": ["undici@5.29.0", "", { "dependencies": { "@fastify/busboy": "^2.0.0" } }, "sha512-raqeBD6NQK4SkWhQzeYKd1KmIG6dllBOTt55Rmkt4HtI9mwdWtJljnrXjAFUBLTSN67HWrOIZ3EPF4kjUw80Bg=="],
|
||||
|
||||
"@agentclientprotocol/sdk/zod": ["zod@3.25.76", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="],
|
||||
|
||||
"@ai-sdk/amazon-bedrock/@ai-sdk/anthropic": ["@ai-sdk/anthropic@2.0.57", "", { "dependencies": { "@ai-sdk/provider": "2.0.1", "@ai-sdk/provider-utils": "3.0.20" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-DREpYqW2pylgaj69gZ+K8u92bo9DaMgFdictYnY+IwYeY3bawQ4zI7l/o1VkDsBDljAx8iYz5lPURwVZNu+Xpg=="],
|
||||
|
||||
"@ai-sdk/amazon-bedrock/@smithy/eventstream-codec": ["@smithy/eventstream-codec@4.2.5", "", { "dependencies": { "@aws-crypto/crc32": "5.2.0", "@smithy/types": "^4.9.0", "@smithy/util-hex-encoding": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-Ogt4Zi9hEbIP17oQMd68qYOHUzmH47UkK7q7Gl55iIm9oKt27MUGrC5JfpMroeHjdkOliOA4Qt3NQ1xMq/nrlA=="],
|
||||
|
||||
"@ai-sdk/anthropic/@ai-sdk/provider": ["@ai-sdk/provider@2.0.0", "", { "dependencies": { "json-schema": "^0.4.0" } }, "sha512-6o7Y2SeO9vFKB8lArHXehNuusnpddKPk7xqL7T2/b+OvXMRIXUO1rR4wcv1hAFUAT9avGZshty3Wlua/XA7TvA=="],
|
||||
|
||||
"@ai-sdk/anthropic/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@3.0.0", "", { "dependencies": { "@ai-sdk/provider": "2.0.0", "@standard-schema/spec": "^1.0.0", "eventsource-parser": "^3.0.3", "zod-to-json-schema": "^3.24.1" }, "peerDependencies": { "zod": "^3.25.76 || ^4" } }, "sha512-BoQZtGcBxkeSH1zK+SRYNDtJPIPpacTeiMZqnG4Rv6xXjEwM0FH4MGs9c+PlhyEWmQCzjRM2HAotEydFhD4dYw=="],
|
||||
@@ -4235,6 +4252,10 @@
|
||||
|
||||
"@slack/web-api/p-queue": ["p-queue@6.6.2", "", { "dependencies": { "eventemitter3": "^4.0.4", "p-timeout": "^3.2.0" } }, "sha512-RwFpb72c/BhQLEXIZ5K2e+AhgNVmIejGlTgiB9MzZ0e93GRvqZ7uSi0dvRF7/XIXDeNkra2fNHBxTyPDGySpjQ=="],
|
||||
|
||||
"@smithy/eventstream-codec/@smithy/types": ["@smithy/types@4.11.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-mlrmL0DRDVe3mNrjTcVcZEgkFmufITfUAPBEA+AHYiIeYyJebso/He1qLbP3PssRe22KUzLRpQSdBPbXdgZ2VA=="],
|
||||
|
||||
"@smithy/eventstream-serde-universal/@smithy/eventstream-codec": ["@smithy/eventstream-codec@4.2.5", "", { "dependencies": { "@aws-crypto/crc32": "5.2.0", "@smithy/types": "^4.9.0", "@smithy/util-hex-encoding": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-Ogt4Zi9hEbIP17oQMd68qYOHUzmH47UkK7q7Gl55iIm9oKt27MUGrC5JfpMroeHjdkOliOA4Qt3NQ1xMq/nrlA=="],
|
||||
|
||||
"@solidjs/start/esbuild": ["esbuild@0.25.12", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.25.12", "@esbuild/android-arm": "0.25.12", "@esbuild/android-arm64": "0.25.12", "@esbuild/android-x64": "0.25.12", "@esbuild/darwin-arm64": "0.25.12", "@esbuild/darwin-x64": "0.25.12", "@esbuild/freebsd-arm64": "0.25.12", "@esbuild/freebsd-x64": "0.25.12", "@esbuild/linux-arm": "0.25.12", "@esbuild/linux-arm64": "0.25.12", "@esbuild/linux-ia32": "0.25.12", "@esbuild/linux-loong64": "0.25.12", "@esbuild/linux-mips64el": "0.25.12", "@esbuild/linux-ppc64": "0.25.12", "@esbuild/linux-riscv64": "0.25.12", "@esbuild/linux-s390x": "0.25.12", "@esbuild/linux-x64": "0.25.12", "@esbuild/netbsd-arm64": "0.25.12", "@esbuild/netbsd-x64": "0.25.12", "@esbuild/openbsd-arm64": "0.25.12", "@esbuild/openbsd-x64": "0.25.12", "@esbuild/openharmony-arm64": "0.25.12", "@esbuild/sunos-x64": "0.25.12", "@esbuild/win32-arm64": "0.25.12", "@esbuild/win32-ia32": "0.25.12", "@esbuild/win32-x64": "0.25.12" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg=="],
|
||||
|
||||
"@solidjs/start/path-to-regexp": ["path-to-regexp@8.3.0", "", {}, "sha512-7jdwVIRtsP8MYpdXSwOS0YdD0Du+qOoF/AEPIt88PcCFrZCzx41oxku1jD88hZBwbNUIEfpqvuhjFaMAqMTWnA=="],
|
||||
@@ -4271,6 +4292,8 @@
|
||||
|
||||
"astro/diff": ["diff@5.2.0", "", {}, "sha512-uIFDxqpRZGZ6ThOk84hEfqWoHx2devRFvpTZcTHur85vImfaxUbTW9Ryh4CpCuDnToOP1CEtXKIgytHBPVff5A=="],
|
||||
|
||||
"astro/semver": ["semver@7.7.2", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA=="],
|
||||
|
||||
"astro/shiki": ["shiki@3.15.0", "", { "dependencies": { "@shikijs/core": "3.15.0", "@shikijs/engine-javascript": "3.15.0", "@shikijs/engine-oniguruma": "3.15.0", "@shikijs/langs": "3.15.0", "@shikijs/themes": "3.15.0", "@shikijs/types": "3.15.0", "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4" } }, "sha512-kLdkY6iV3dYbtPwS9KXU7mjfmDm25f5m0IPNFnaXO7TBPcvbUOY72PYXSuSqDzwp+vlH/d7MXpHlKO/x+QoLXw=="],
|
||||
|
||||
"astro/unstorage": ["unstorage@1.17.3", "", { "dependencies": { "anymatch": "^3.1.3", "chokidar": "^4.0.3", "destr": "^2.0.5", "h3": "^1.15.4", "lru-cache": "^10.4.3", "node-fetch-native": "^1.6.7", "ofetch": "^1.5.1", "ufo": "^1.6.1" }, "peerDependencies": { "@azure/app-configuration": "^1.8.0", "@azure/cosmos": "^4.2.0", "@azure/data-tables": "^13.3.0", "@azure/identity": "^4.6.0", "@azure/keyvault-secrets": "^4.9.0", "@azure/storage-blob": "^12.26.0", "@capacitor/preferences": "^6.0.3 || ^7.0.0", "@deno/kv": ">=0.9.0", "@netlify/blobs": "^6.5.0 || ^7.0.0 || ^8.1.0 || ^9.0.0 || ^10.0.0", "@planetscale/database": "^1.19.0", "@upstash/redis": "^1.34.3", "@vercel/blob": ">=0.27.1", "@vercel/functions": "^2.2.12 || ^3.0.0", "@vercel/kv": "^1.0.1", "aws4fetch": "^1.0.20", "db0": ">=0.2.1", "idb-keyval": "^6.2.1", "ioredis": "^5.4.2", "uploadthing": "^7.4.4" }, "optionalPeers": ["@azure/app-configuration", "@azure/cosmos", "@azure/data-tables", "@azure/identity", "@azure/keyvault-secrets", "@azure/storage-blob", "@capacitor/preferences", "@deno/kv", "@netlify/blobs", "@planetscale/database", "@upstash/redis", "@vercel/blob", "@vercel/functions", "@vercel/kv", "aws4fetch", "db0", "idb-keyval", "ioredis", "uploadthing"] }, "sha512-i+JYyy0DoKmQ3FximTHbGadmIYb8JEpq7lxUjnjeB702bCPum0vzo6oy5Mfu0lpqISw7hCyMW2yj4nWC8bqJ3Q=="],
|
||||
@@ -4291,6 +4314,10 @@
|
||||
|
||||
"body-parser/qs": ["qs@6.13.0", "", { "dependencies": { "side-channel": "^1.0.6" } }, "sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg=="],
|
||||
|
||||
"bun-webgpu/@webgpu/types": ["@webgpu/types@0.1.66", "", {}, "sha512-YA2hLrwLpDsRueNDXIMqN9NTzD6bCDkuXbOSe0heS+f8YE8usA6Gbv1prj81pzVHrbaAma7zObnIC+I6/sXJgA=="],
|
||||
|
||||
"c12/chokidar": ["chokidar@5.0.0", "", { "dependencies": { "readdirp": "^5.0.0" } }, "sha512-TQMmc3w+5AxjpL8iIiwebF73dRDF4fBIieAqGn9RGCWaEVwQ6Fb2cGe31Yns0RRIzii5goJ1Y7xbMwo1TxMplw=="],
|
||||
|
||||
"clean-css/source-map": ["source-map@0.6.1", "", {}, "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g=="],
|
||||
|
||||
"compress-commons/is-stream": ["is-stream@2.0.1", "", {}, "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg=="],
|
||||
@@ -4307,6 +4334,8 @@
|
||||
|
||||
"editorconfig/minimatch": ["minimatch@9.0.1", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-0jWhJpD/MdhPXwPuiRkCbfYfSKp2qnn2eOc279qI7f+osl/l+prKSrvhg157zSYvx/1nmgn2NqdT6k2Z7zSH9w=="],
|
||||
|
||||
"editorconfig/semver": ["semver@7.7.2", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA=="],
|
||||
|
||||
"engine.io-client/ws": ["ws@8.18.3", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg=="],
|
||||
|
||||
"es-get-iterator/isarray": ["isarray@2.0.5", "", {}, "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw=="],
|
||||
@@ -4333,6 +4362,8 @@
|
||||
|
||||
"gaxios/node-fetch": ["node-fetch@3.3.2", "", { "dependencies": { "data-uri-to-buffer": "^4.0.0", "fetch-blob": "^3.1.4", "formdata-polyfill": "^4.0.10" } }, "sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA=="],
|
||||
|
||||
"gel/semver": ["semver@7.7.2", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA=="],
|
||||
|
||||
"glob/minimatch": ["minimatch@10.1.1", "", { "dependencies": { "@isaacs/brace-expansion": "^5.0.0" } }, "sha512-enIvLvRAFZYXJzkCYG5RKmPfrFArdLv+R+lbQ53BmIMLIry74bjKzX6iHAm8WYamJkhSSEabrWN5D97XnKObjQ=="],
|
||||
|
||||
"globby/ignore": ["ignore@5.3.2", "", {}, "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g=="],
|
||||
@@ -4347,6 +4378,8 @@
|
||||
|
||||
"jsonwebtoken/jws": ["jws@3.2.2", "", { "dependencies": { "jwa": "^1.4.1", "safe-buffer": "^5.0.1" } }, "sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA=="],
|
||||
|
||||
"jsonwebtoken/semver": ["semver@7.7.2", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA=="],
|
||||
|
||||
"katex/commander": ["commander@8.3.0", "", {}, "sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww=="],
|
||||
|
||||
"lazystream/readable-stream": ["readable-stream@2.3.8", "", { "dependencies": { "core-util-is": "~1.0.0", "inherits": "~2.0.3", "isarray": "~1.0.0", "process-nextick-args": "~2.0.0", "safe-buffer": "~5.1.1", "string_decoder": "~1.1.1", "util-deprecate": "~1.0.1" } }, "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA=="],
|
||||
@@ -4405,6 +4438,8 @@
|
||||
|
||||
"pkg-up/find-up": ["find-up@3.0.0", "", { "dependencies": { "locate-path": "^3.0.0" } }, "sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg=="],
|
||||
|
||||
"playwright/fsevents": ["fsevents@2.3.2", "", { "os": "darwin" }, "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA=="],
|
||||
|
||||
"postcss-load-config/lilconfig": ["lilconfig@3.1.3", "", {}, "sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw=="],
|
||||
|
||||
"prompts/kleur": ["kleur@3.0.3", "", {}, "sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w=="],
|
||||
@@ -4431,6 +4466,8 @@
|
||||
|
||||
"sharp/detect-libc": ["detect-libc@2.1.2", "", {}, "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ=="],
|
||||
|
||||
"sharp/semver": ["semver@7.7.2", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA=="],
|
||||
|
||||
"shiki/@shikijs/core": ["@shikijs/core@3.20.0", "", { "dependencies": { "@shikijs/types": "3.20.0", "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4", "hast-util-to-html": "^9.0.5" } }, "sha512-f2ED7HYV4JEk827mtMDwe/yQ25pRiXZmtHjWF8uzZKuKiEsJR7Ce1nuQ+HhV9FzDcbIo4ObBCD9GPTzNuy9S1g=="],
|
||||
|
||||
"shiki/@shikijs/types": ["@shikijs/types@3.20.0", "", { "dependencies": { "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4" } }, "sha512-lhYAATn10nkZcBQ0BlzSbJA3wcmL5MXUUF8d2Zzon6saZDlToKaiRX60n2+ZaHJCmXEcZRWNzn+k9vplr8Jhsw=="],
|
||||
@@ -4909,6 +4946,8 @@
|
||||
|
||||
"body-parser/debug/ms": ["ms@2.0.0", "", {}, "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="],
|
||||
|
||||
"c12/chokidar/readdirp": ["readdirp@5.0.0", "", {}, "sha512-9u/XQ1pvrQtYyMpZe7DXKv2p5CNvyVwzUB6uhLAnQwHMSgKMBR62lc7AHljaeteeHXn11XTAaLLUVZYVZyuRBQ=="],
|
||||
|
||||
"cross-spawn/which/isexe": ["isexe@2.0.0", "", {}, "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="],
|
||||
|
||||
"drizzle-kit/esbuild/@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.19.12", "", { "os": "aix", "cpu": "ppc64" }, "sha512-bmoCYyWdEL3wDQIVbcyzRyeKLgk2WtWLTWz1ZIAZF/EGbNOwSA6ew3PftJ1PqMiOOGu0OyFMzG53L0zqIpPeNA=="],
|
||||
|
||||
6
flake.lock
generated
6
flake.lock
generated
@@ -2,11 +2,11 @@
|
||||
"nodes": {
|
||||
"nixpkgs": {
|
||||
"locked": {
|
||||
"lastModified": 1768395095,
|
||||
"narHash": "sha256-ZhuYJbwbZT32QA95tSkXd9zXHcdZj90EzHpEXBMabaw=",
|
||||
"lastModified": 1768302833,
|
||||
"narHash": "sha256-h5bRFy9bco+8QcK7rGoOiqMxMbmn21moTACofNLRMP4=",
|
||||
"owner": "NixOS",
|
||||
"repo": "nixpkgs",
|
||||
"rev": "13868c071cc73a5e9f610c47d7bb08e5da64fdd5",
|
||||
"rev": "61db79b0c6b838d9894923920b612048e1201926",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
|
||||
152
flake.nix
152
flake.nix
@@ -6,10 +6,7 @@
|
||||
};
|
||||
|
||||
outputs =
|
||||
{
|
||||
nixpkgs,
|
||||
...
|
||||
}:
|
||||
{ self, nixpkgs, ... }:
|
||||
let
|
||||
systems = [
|
||||
"aarch64-linux"
|
||||
@@ -17,123 +14,56 @@
|
||||
"aarch64-darwin"
|
||||
"x86_64-darwin"
|
||||
];
|
||||
inherit (nixpkgs) lib;
|
||||
forEachSystem = lib.genAttrs systems;
|
||||
pkgsFor = system: nixpkgs.legacyPackages.${system};
|
||||
packageJson = builtins.fromJSON (builtins.readFile ./packages/opencode/package.json);
|
||||
bunTarget = {
|
||||
"aarch64-linux" = "bun-linux-arm64";
|
||||
"x86_64-linux" = "bun-linux-x64";
|
||||
"aarch64-darwin" = "bun-darwin-arm64";
|
||||
"x86_64-darwin" = "bun-darwin-x64";
|
||||
};
|
||||
|
||||
# Parse "bun-{os}-{cpu}" to {os, cpu}
|
||||
parseBunTarget =
|
||||
target:
|
||||
let
|
||||
parts = lib.splitString "-" target;
|
||||
in
|
||||
{
|
||||
os = builtins.elemAt parts 1;
|
||||
cpu = builtins.elemAt parts 2;
|
||||
};
|
||||
|
||||
hashesFile = "${./nix}/hashes.json";
|
||||
hashesData =
|
||||
if builtins.pathExists hashesFile then builtins.fromJSON (builtins.readFile hashesFile) else { };
|
||||
# Lookup hash: supports per-system ({system: hash}) or legacy single hash
|
||||
nodeModulesHashFor =
|
||||
system:
|
||||
if builtins.isAttrs hashesData.nodeModules then
|
||||
hashesData.nodeModules.${system}
|
||||
else
|
||||
hashesData.nodeModules;
|
||||
modelsDev = forEachSystem (
|
||||
system:
|
||||
let
|
||||
pkgs = pkgsFor system;
|
||||
in
|
||||
pkgs."models-dev"
|
||||
);
|
||||
forEachSystem = f: nixpkgs.lib.genAttrs systems (system: f nixpkgs.legacyPackages.${system});
|
||||
rev = self.shortRev or self.dirtyShortRev or "dirty";
|
||||
in
|
||||
{
|
||||
devShells = forEachSystem (
|
||||
system:
|
||||
let
|
||||
pkgs = pkgsFor system;
|
||||
in
|
||||
{
|
||||
default = pkgs.mkShell {
|
||||
packages = with pkgs; [
|
||||
bun
|
||||
nodejs_20
|
||||
pkg-config
|
||||
openssl
|
||||
git
|
||||
];
|
||||
};
|
||||
}
|
||||
);
|
||||
devShells = forEachSystem (pkgs: {
|
||||
default = pkgs.mkShell {
|
||||
packages = with pkgs; [
|
||||
bun
|
||||
nodejs_20
|
||||
pkg-config
|
||||
openssl
|
||||
git
|
||||
];
|
||||
};
|
||||
});
|
||||
|
||||
packages = forEachSystem (
|
||||
system:
|
||||
pkgs:
|
||||
let
|
||||
pkgs = pkgsFor system;
|
||||
bunPlatform = parseBunTarget bunTarget.${system};
|
||||
mkNodeModules = pkgs.callPackage ./nix/node-modules.nix {
|
||||
hash = nodeModulesHashFor system;
|
||||
bunCpu = bunPlatform.cpu;
|
||||
bunOs = bunPlatform.os;
|
||||
node_modules = pkgs.callPackage ./nix/node_modules.nix {
|
||||
inherit rev;
|
||||
};
|
||||
mkOpencode = pkgs.callPackage ./nix/opencode.nix { };
|
||||
mkDesktop = pkgs.callPackage ./nix/desktop.nix { };
|
||||
|
||||
opencodePkg = mkOpencode {
|
||||
inherit (packageJson) version;
|
||||
src = ./.;
|
||||
scripts = ./nix/scripts;
|
||||
target = bunTarget.${system};
|
||||
modelsDev = "${modelsDev.${system}}/dist/_api.json";
|
||||
inherit mkNodeModules;
|
||||
opencode = pkgs.callPackage ./nix/opencode.nix {
|
||||
inherit node_modules;
|
||||
};
|
||||
|
||||
desktopPkg = mkDesktop {
|
||||
inherit (packageJson) version;
|
||||
src = ./.;
|
||||
scripts = ./nix/scripts;
|
||||
mkNodeModules = mkNodeModules;
|
||||
opencode = opencodePkg;
|
||||
desktop = pkgs.callPackage ./nix/desktop.nix {
|
||||
inherit opencode;
|
||||
};
|
||||
# nixpkgs cpu naming to bun cpu naming
|
||||
cpuMap = { x86_64 = "x64"; aarch64 = "arm64"; };
|
||||
# matrix of node_modules builds - these will always fail due to fakeHash usage
|
||||
# but allow computation of the correct hash from any build machine for any cpu/os
|
||||
# see the update-nix-hashes workflow for usage
|
||||
moduleUpdaters = pkgs.lib.listToAttrs (
|
||||
pkgs.lib.concatMap (cpu:
|
||||
map (os: {
|
||||
name = "${cpu}-${os}_node_modules";
|
||||
value = node_modules.override {
|
||||
bunCpu = cpuMap.${cpu};
|
||||
bunOs = os;
|
||||
hash = pkgs.lib.fakeHash;
|
||||
};
|
||||
}) [ "linux" "darwin" ]
|
||||
) [ "x86_64" "aarch64" ]
|
||||
);
|
||||
in
|
||||
{
|
||||
default = opencodePkg;
|
||||
desktop = desktopPkg;
|
||||
}
|
||||
);
|
||||
|
||||
apps = forEachSystem (
|
||||
system:
|
||||
let
|
||||
pkgs = pkgsFor system;
|
||||
in
|
||||
{
|
||||
opencode-dev = {
|
||||
type = "app";
|
||||
meta = {
|
||||
description = "Nix devshell shell for OpenCode";
|
||||
runtimeInputs = [ pkgs.bun ];
|
||||
};
|
||||
program = "${
|
||||
pkgs.writeShellApplication {
|
||||
name = "opencode-dev";
|
||||
text = ''
|
||||
exec bun run dev "$@"
|
||||
'';
|
||||
}
|
||||
}/bin/opencode-dev";
|
||||
};
|
||||
}
|
||||
default = opencode;
|
||||
inherit opencode desktop;
|
||||
} // moduleUpdaters
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
@@ -91,8 +91,10 @@ This will walk you through installing the GitHub app, creating the workflow, and
|
||||
uses: anomalyco/opencode/github@latest
|
||||
env:
|
||||
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
with:
|
||||
model: anthropic/claude-sonnet-4-20250514
|
||||
use_github_token: true
|
||||
```
|
||||
|
||||
3. Store the API keys in secrets. In your organization or project **settings**, expand **Secrets and variables** on the left and select **Actions**. Add the required API keys.
|
||||
|
||||
15
infra/app.ts
15
infra/app.ts
@@ -4,6 +4,10 @@ const GITHUB_APP_ID = new sst.Secret("GITHUB_APP_ID")
|
||||
const GITHUB_APP_PRIVATE_KEY = new sst.Secret("GITHUB_APP_PRIVATE_KEY")
|
||||
export const EMAILOCTOPUS_API_KEY = new sst.Secret("EMAILOCTOPUS_API_KEY")
|
||||
const ADMIN_SECRET = new sst.Secret("ADMIN_SECRET")
|
||||
const DISCORD_SUPPORT_BOT_TOKEN = new sst.Secret("DISCORD_SUPPORT_BOT_TOKEN")
|
||||
const DISCORD_SUPPORT_CHANNEL_ID = new sst.Secret("DISCORD_SUPPORT_CHANNEL_ID")
|
||||
const FEISHU_APP_ID = new sst.Secret("FEISHU_APP_ID")
|
||||
const FEISHU_APP_SECRET = new sst.Secret("FEISHU_APP_SECRET")
|
||||
const bucket = new sst.cloudflare.Bucket("Bucket")
|
||||
|
||||
export const api = new sst.cloudflare.Worker("Api", {
|
||||
@@ -13,7 +17,16 @@ export const api = new sst.cloudflare.Worker("Api", {
|
||||
WEB_DOMAIN: domain,
|
||||
},
|
||||
url: true,
|
||||
link: [bucket, GITHUB_APP_ID, GITHUB_APP_PRIVATE_KEY, ADMIN_SECRET],
|
||||
link: [
|
||||
bucket,
|
||||
GITHUB_APP_ID,
|
||||
GITHUB_APP_PRIVATE_KEY,
|
||||
ADMIN_SECRET,
|
||||
DISCORD_SUPPORT_BOT_TOKEN,
|
||||
DISCORD_SUPPORT_CHANNEL_ID,
|
||||
FEISHU_APP_ID,
|
||||
FEISHU_APP_SECRET,
|
||||
],
|
||||
transform: {
|
||||
worker: (args) => {
|
||||
args.logpush = true
|
||||
|
||||
@@ -119,6 +119,7 @@ const ZEN_MODELS = [
|
||||
new sst.Secret("ZEN_MODELS5"),
|
||||
new sst.Secret("ZEN_MODELS6"),
|
||||
new sst.Secret("ZEN_MODELS7"),
|
||||
new sst.Secret("ZEN_MODELS8"),
|
||||
]
|
||||
const ZEN_BLACK = new sst.Secret("ZEN_BLACK")
|
||||
const STRIPE_SECRET_KEY = new sst.Secret("STRIPE_SECRET_KEY")
|
||||
|
||||
@@ -1,40 +0,0 @@
|
||||
#!/usr/bin/env bun
|
||||
|
||||
import solidPlugin from "./node_modules/@opentui/solid/scripts/solid-plugin"
|
||||
import path from "path"
|
||||
import fs from "fs"
|
||||
|
||||
const dir = process.cwd()
|
||||
const parser = fs.realpathSync(path.join(dir, "node_modules/@opentui/core/parser.worker.js"))
|
||||
const worker = "./src/cli/cmd/tui/worker.ts"
|
||||
const version = process.env.OPENCODE_VERSION ?? "local"
|
||||
const channel = process.env.OPENCODE_CHANNEL ?? "local"
|
||||
|
||||
fs.rmSync(path.join(dir, "dist"), { recursive: true, force: true })
|
||||
|
||||
const result = await Bun.build({
|
||||
entrypoints: ["./src/index.ts", worker, parser],
|
||||
outdir: "./dist",
|
||||
target: "bun",
|
||||
sourcemap: "none",
|
||||
tsconfig: "./tsconfig.json",
|
||||
plugins: [solidPlugin],
|
||||
external: ["@opentui/core"],
|
||||
define: {
|
||||
OPENCODE_VERSION: `'${version}'`,
|
||||
OPENCODE_CHANNEL: `'${channel}'`,
|
||||
// Leave undefined so runtime picks bundled/dist worker or fallback in code.
|
||||
OPENCODE_WORKER_PATH: "undefined",
|
||||
OTUI_TREE_SITTER_WORKER_PATH: 'new URL("./cli/cmd/tui/parser.worker.js", import.meta.url).href',
|
||||
},
|
||||
})
|
||||
|
||||
if (!result.success) {
|
||||
console.error("bundle failed")
|
||||
for (const log of result.logs) console.error(log)
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
const parserOut = path.join(dir, "dist/src/cli/cmd/tui/parser.worker.js")
|
||||
fs.mkdirSync(path.dirname(parserOut), { recursive: true })
|
||||
await Bun.write(parserOut, Bun.file(parser))
|
||||
169
nix/desktop.nix
169
nix/desktop.nix
@@ -2,144 +2,99 @@
|
||||
lib,
|
||||
stdenv,
|
||||
rustPlatform,
|
||||
bun,
|
||||
pkg-config,
|
||||
dbus ? null,
|
||||
openssl,
|
||||
glib ? null,
|
||||
gtk3 ? null,
|
||||
libsoup_3 ? null,
|
||||
webkitgtk_4_1 ? null,
|
||||
librsvg ? null,
|
||||
libappindicator-gtk3 ? null,
|
||||
cargo-tauri,
|
||||
bun,
|
||||
nodejs,
|
||||
cargo,
|
||||
rustc,
|
||||
makeBinaryWrapper,
|
||||
nodejs,
|
||||
jq,
|
||||
wrapGAppsHook4,
|
||||
makeWrapper,
|
||||
dbus,
|
||||
glib,
|
||||
gtk4,
|
||||
libsoup_3,
|
||||
librsvg,
|
||||
libappindicator,
|
||||
glib-networking,
|
||||
openssl,
|
||||
webkitgtk_4_1,
|
||||
gst_all_1,
|
||||
opencode,
|
||||
}:
|
||||
args:
|
||||
let
|
||||
scripts = args.scripts;
|
||||
mkModules =
|
||||
attrs:
|
||||
args.mkNodeModules (
|
||||
attrs
|
||||
// {
|
||||
canonicalizeScript = scripts + "/canonicalize-node-modules.ts";
|
||||
normalizeBinsScript = scripts + "/normalize-bun-binaries.ts";
|
||||
}
|
||||
);
|
||||
in
|
||||
rustPlatform.buildRustPackage rec {
|
||||
rustPlatform.buildRustPackage (finalAttrs: {
|
||||
pname = "opencode-desktop";
|
||||
version = args.version;
|
||||
inherit (opencode)
|
||||
version
|
||||
src
|
||||
node_modules
|
||||
patches
|
||||
;
|
||||
|
||||
src = args.src;
|
||||
|
||||
# We need to set the root for cargo, but we also need access to the whole repo.
|
||||
postUnpack = ''
|
||||
# Update sourceRoot to point to the tauri app
|
||||
sourceRoot+=/packages/desktop/src-tauri
|
||||
'';
|
||||
|
||||
cargoLock = {
|
||||
lockFile = ../packages/desktop/src-tauri/Cargo.lock;
|
||||
allowBuiltinFetchGit = true;
|
||||
};
|
||||
|
||||
node_modules = mkModules {
|
||||
version = version;
|
||||
src = src;
|
||||
};
|
||||
cargoRoot = "packages/desktop/src-tauri";
|
||||
cargoLock.lockFile = ../packages/desktop/src-tauri/Cargo.lock;
|
||||
buildAndTestSubdir = finalAttrs.cargoRoot;
|
||||
|
||||
nativeBuildInputs = [
|
||||
pkg-config
|
||||
cargo-tauri.hook
|
||||
bun
|
||||
makeBinaryWrapper
|
||||
nodejs # for patchShebangs node_modules
|
||||
cargo
|
||||
rustc
|
||||
nodejs
|
||||
jq
|
||||
];
|
||||
|
||||
buildInputs = [
|
||||
openssl
|
||||
makeWrapper
|
||||
]
|
||||
++ lib.optionals stdenv.isLinux [
|
||||
++ lib.optionals stdenv.hostPlatform.isLinux [ wrapGAppsHook4 ];
|
||||
|
||||
buildInputs = lib.optionals stdenv.isLinux [
|
||||
dbus
|
||||
glib
|
||||
gtk3
|
||||
gtk4
|
||||
libsoup_3
|
||||
webkitgtk_4_1
|
||||
librsvg
|
||||
libappindicator-gtk3
|
||||
libappindicator
|
||||
glib-networking
|
||||
openssl
|
||||
webkitgtk_4_1
|
||||
gst_all_1.gstreamer
|
||||
gst_all_1.gst-plugins-base
|
||||
gst_all_1.gst-plugins-good
|
||||
];
|
||||
|
||||
strictDeps = true;
|
||||
|
||||
preBuild = ''
|
||||
# Restore node_modules
|
||||
pushd ../../..
|
||||
|
||||
# Copy node_modules from the fixed-output derivation
|
||||
# We use cp -r --no-preserve=mode to ensure we can write to them if needed,
|
||||
# though we usually just read.
|
||||
cp -r ${node_modules}/node_modules .
|
||||
cp -r ${node_modules}/packages .
|
||||
|
||||
# Ensure node_modules is writable so patchShebangs can update script headers
|
||||
chmod -R u+w node_modules
|
||||
# Ensure workspace packages are writable for tsgo incremental outputs (.tsbuildinfo)
|
||||
chmod -R u+w packages
|
||||
# Patch shebangs so scripts can run
|
||||
cp -a ${finalAttrs.node_modules}/{node_modules,packages} .
|
||||
chmod -R u+w node_modules packages
|
||||
patchShebangs node_modules
|
||||
patchShebangs packages/desktop/node_modules
|
||||
|
||||
# Copy sidecar
|
||||
mkdir -p packages/desktop/src-tauri/sidecars
|
||||
targetTriple=${stdenv.hostPlatform.rust.rustcTarget}
|
||||
cp ${args.opencode}/bin/opencode packages/desktop/src-tauri/sidecars/opencode-cli-$targetTriple
|
||||
|
||||
# Merge prod config into tauri.conf.json
|
||||
if ! jq -s '.[0] * .[1]' \
|
||||
packages/desktop/src-tauri/tauri.conf.json \
|
||||
packages/desktop/src-tauri/tauri.prod.conf.json \
|
||||
> packages/desktop/src-tauri/tauri.conf.json.tmp; then
|
||||
echo "Error: failed to merge tauri.conf.json with tauri.prod.conf.json" >&2
|
||||
exit 1
|
||||
fi
|
||||
mv packages/desktop/src-tauri/tauri.conf.json.tmp packages/desktop/src-tauri/tauri.conf.json
|
||||
|
||||
# Build the frontend
|
||||
cd packages/desktop
|
||||
|
||||
# The 'build' script runs 'bun run typecheck && vite build'.
|
||||
bun run build
|
||||
|
||||
popd
|
||||
cp ${opencode}/bin/opencode packages/desktop/src-tauri/sidecars/opencode-cli-${stdenv.hostPlatform.rust.rustcTarget}
|
||||
'';
|
||||
|
||||
# Tauri bundles the assets during the rust build phase (which happens after preBuild).
|
||||
# It looks for them in the location specified in tauri.conf.json.
|
||||
# see publish-tauri job in .github/workflows/publish.yml
|
||||
tauriBuildFlags = [
|
||||
"--config"
|
||||
"tauri.prod.conf.json"
|
||||
"--no-sign" # no code signing or auto updates
|
||||
];
|
||||
|
||||
postInstall = lib.optionalString stdenv.isLinux ''
|
||||
# Wrap the binary to ensure it finds the libraries
|
||||
wrapProgram $out/bin/opencode-desktop \
|
||||
--prefix LD_LIBRARY_PATH : ${
|
||||
lib.makeLibraryPath [
|
||||
gtk3
|
||||
webkitgtk_4_1
|
||||
librsvg
|
||||
glib
|
||||
libsoup_3
|
||||
]
|
||||
}
|
||||
# FIXME: workaround for concerns about case insensitive filesystems
|
||||
# should be removed once binary is renamed or decided otherwise
|
||||
# darwin output is a .app bundle so no conflict
|
||||
postFixup = lib.optionalString stdenv.hostPlatform.isLinux ''
|
||||
mv $out/bin/OpenCode $out/bin/opencode-desktop
|
||||
sed -i 's|^Exec=OpenCode$|Exec=opencode-desktop|' $out/share/applications/OpenCode.desktop
|
||||
'';
|
||||
|
||||
meta = with lib; {
|
||||
meta = {
|
||||
description = "OpenCode Desktop App";
|
||||
homepage = "https://opencode.ai";
|
||||
license = licenses.mit;
|
||||
maintainers = with maintainers; [ ];
|
||||
license = lib.licenses.mit;
|
||||
mainProgram = "opencode-desktop";
|
||||
platforms = platforms.linux ++ platforms.darwin;
|
||||
inherit (opencode.meta) platforms;
|
||||
};
|
||||
}
|
||||
})
|
||||
@@ -1,6 +1,8 @@
|
||||
{
|
||||
"nodeModules": {
|
||||
"x86_64-linux": "sha256-XP1DXs1Fcfog99rjMryki9mMqn1g1H4ykHx7WDsnrnw=",
|
||||
"aarch64-darwin": "sha256-fupiqvXkW3Cl44K+n1cDz81vOboMXIHPHTey6TewX70="
|
||||
"x86_64-linux": "sha256-sH6zUk9G4vC6btPZIR9aiSHX0F4aGyUZB7fKbpDUcpE=",
|
||||
"aarch64-linux": "sha256-CVpdXnFns34hmGwwlRrrI6Uk6B/jZUxfnH4HC2NanEo=",
|
||||
"aarch64-darwin": "sha256-khP27Iiq+FAZRlzUy7rGXc2MviZjirFH1ShRyd7q1bY=",
|
||||
"x86_64-darwin": "sha256-nE2p62Tld64sQVMq7j0YNT5Zwjqp22H997+K8xfi1ag="
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,62 +0,0 @@
|
||||
{
|
||||
hash,
|
||||
lib,
|
||||
stdenvNoCC,
|
||||
bun,
|
||||
cacert,
|
||||
curl,
|
||||
bunCpu,
|
||||
bunOs,
|
||||
}:
|
||||
args:
|
||||
stdenvNoCC.mkDerivation {
|
||||
pname = "opencode-node_modules";
|
||||
inherit (args) version src;
|
||||
|
||||
impureEnvVars = lib.fetchers.proxyImpureEnvVars ++ [
|
||||
"GIT_PROXY_COMMAND"
|
||||
"SOCKS_SERVER"
|
||||
];
|
||||
|
||||
nativeBuildInputs = [
|
||||
bun
|
||||
cacert
|
||||
curl
|
||||
];
|
||||
|
||||
dontConfigure = true;
|
||||
|
||||
buildPhase = ''
|
||||
runHook preBuild
|
||||
export HOME=$(mktemp -d)
|
||||
export BUN_INSTALL_CACHE_DIR=$(mktemp -d)
|
||||
bun install \
|
||||
--cpu="${bunCpu}" \
|
||||
--os="${bunOs}" \
|
||||
--frozen-lockfile \
|
||||
--ignore-scripts \
|
||||
--no-progress \
|
||||
--linker=isolated
|
||||
bun --bun ${args.canonicalizeScript}
|
||||
bun --bun ${args.normalizeBinsScript}
|
||||
runHook postBuild
|
||||
'';
|
||||
|
||||
installPhase = ''
|
||||
runHook preInstall
|
||||
mkdir -p $out
|
||||
while IFS= read -r dir; do
|
||||
rel="''${dir#./}"
|
||||
dest="$out/$rel"
|
||||
mkdir -p "$(dirname "$dest")"
|
||||
cp -R "$dir" "$dest"
|
||||
done < <(find . -type d -name node_modules -prune | sort)
|
||||
runHook postInstall
|
||||
'';
|
||||
|
||||
dontFixup = true;
|
||||
|
||||
outputHashAlgo = "sha256";
|
||||
outputHashMode = "recursive";
|
||||
outputHash = hash;
|
||||
}
|
||||
85
nix/node_modules.nix
Normal file
85
nix/node_modules.nix
Normal file
@@ -0,0 +1,85 @@
|
||||
{
|
||||
lib,
|
||||
stdenvNoCC,
|
||||
bun,
|
||||
bunCpu ? if stdenvNoCC.hostPlatform.isAarch64 then "arm64" else "x64",
|
||||
bunOs ? if stdenvNoCC.hostPlatform.isLinux then "linux" else "darwin",
|
||||
rev ? "dirty",
|
||||
hash ?
|
||||
(lib.pipe ./hashes.json [
|
||||
builtins.readFile
|
||||
builtins.fromJSON
|
||||
]).nodeModules.${stdenvNoCC.hostPlatform.system},
|
||||
}:
|
||||
let
|
||||
packageJson = lib.pipe ../packages/opencode/package.json [
|
||||
builtins.readFile
|
||||
builtins.fromJSON
|
||||
];
|
||||
in
|
||||
stdenvNoCC.mkDerivation {
|
||||
pname = "opencode-node_modules";
|
||||
version = "${packageJson.version}-${rev}";
|
||||
|
||||
src = lib.fileset.toSource {
|
||||
root = ../.;
|
||||
fileset = lib.fileset.intersection (lib.fileset.fromSource (lib.sources.cleanSource ../.)) (
|
||||
lib.fileset.unions [
|
||||
../packages
|
||||
../bun.lock
|
||||
../package.json
|
||||
../patches
|
||||
../install
|
||||
]
|
||||
);
|
||||
};
|
||||
|
||||
impureEnvVars = lib.fetchers.proxyImpureEnvVars ++ [
|
||||
"GIT_PROXY_COMMAND"
|
||||
"SOCKS_SERVER"
|
||||
];
|
||||
|
||||
nativeBuildInputs = [
|
||||
bun
|
||||
];
|
||||
|
||||
dontConfigure = true;
|
||||
|
||||
buildPhase = ''
|
||||
runHook preBuild
|
||||
export HOME=$(mktemp -d)
|
||||
export BUN_INSTALL_CACHE_DIR=$(mktemp -d)
|
||||
bun install \
|
||||
--cpu="${bunCpu}" \
|
||||
--os="${bunOs}" \
|
||||
--frozen-lockfile \
|
||||
--ignore-scripts \
|
||||
--no-progress \
|
||||
--linker=isolated
|
||||
bun --bun ${./scripts/canonicalize-node-modules.ts}
|
||||
bun --bun ${./scripts/normalize-bun-binaries.ts}
|
||||
runHook postBuild
|
||||
'';
|
||||
|
||||
installPhase = ''
|
||||
runHook preInstall
|
||||
|
||||
mkdir -p $out
|
||||
find . -type d -name node_modules -exec cp -R --parents {} $out \;
|
||||
|
||||
runHook postInstall
|
||||
'';
|
||||
|
||||
dontFixup = true;
|
||||
|
||||
outputHashAlgo = "sha256";
|
||||
outputHashMode = "recursive";
|
||||
outputHash = hash;
|
||||
|
||||
meta.platforms = [
|
||||
"aarch64-linux"
|
||||
"x86_64-linux"
|
||||
"aarch64-darwin"
|
||||
"x86_64-darwin"
|
||||
];
|
||||
}
|
||||
158
nix/opencode.nix
158
nix/opencode.nix
@@ -1,61 +1,48 @@
|
||||
{
|
||||
lib,
|
||||
stdenvNoCC,
|
||||
callPackage,
|
||||
bun,
|
||||
ripgrep,
|
||||
sysctl,
|
||||
makeBinaryWrapper,
|
||||
models-dev,
|
||||
ripgrep,
|
||||
installShellFiles,
|
||||
versionCheckHook,
|
||||
writableTmpDirAsHomeHook,
|
||||
node_modules ? callPackage ./node-modules.nix { },
|
||||
}:
|
||||
args:
|
||||
let
|
||||
inherit (args) scripts;
|
||||
mkModules =
|
||||
attrs:
|
||||
args.mkNodeModules (
|
||||
attrs
|
||||
// {
|
||||
canonicalizeScript = scripts + "/canonicalize-node-modules.ts";
|
||||
normalizeBinsScript = scripts + "/normalize-bun-binaries.ts";
|
||||
}
|
||||
);
|
||||
in
|
||||
stdenvNoCC.mkDerivation (finalAttrs: {
|
||||
pname = "opencode";
|
||||
inherit (args) version src;
|
||||
|
||||
node_modules = mkModules {
|
||||
inherit (finalAttrs) version src;
|
||||
};
|
||||
inherit (node_modules) version src;
|
||||
inherit node_modules;
|
||||
|
||||
nativeBuildInputs = [
|
||||
bun
|
||||
installShellFiles
|
||||
makeBinaryWrapper
|
||||
models-dev
|
||||
writableTmpDirAsHomeHook
|
||||
];
|
||||
|
||||
env.MODELS_DEV_API_JSON = args.modelsDev;
|
||||
env.OPENCODE_VERSION = args.version;
|
||||
env.OPENCODE_CHANNEL = "stable";
|
||||
dontConfigure = true;
|
||||
configurePhase = ''
|
||||
runHook preConfigure
|
||||
|
||||
cp -R ${finalAttrs.node_modules}/. .
|
||||
|
||||
runHook postConfigure
|
||||
'';
|
||||
|
||||
env.MODELS_DEV_API_JSON = "${models-dev}/dist/_api.json";
|
||||
env.OPENCODE_VERSION = finalAttrs.version;
|
||||
env.OPENCODE_CHANNEL = "local";
|
||||
|
||||
buildPhase = ''
|
||||
runHook preBuild
|
||||
|
||||
cp -r ${finalAttrs.node_modules}/node_modules .
|
||||
cp -r ${finalAttrs.node_modules}/packages .
|
||||
|
||||
(
|
||||
cd packages/opencode
|
||||
|
||||
chmod -R u+w ./node_modules
|
||||
mkdir -p ./node_modules/@opencode-ai
|
||||
rm -f ./node_modules/@opencode-ai/{script,sdk,plugin}
|
||||
ln -s $(pwd)/../../packages/script ./node_modules/@opencode-ai/script
|
||||
ln -s $(pwd)/../../packages/sdk/js ./node_modules/@opencode-ai/sdk
|
||||
ln -s $(pwd)/../../packages/plugin ./node_modules/@opencode-ai/plugin
|
||||
|
||||
cp ${./bundle.ts} ./bundle.ts
|
||||
chmod +x ./bundle.ts
|
||||
bun run ./bundle.ts
|
||||
)
|
||||
cd ./packages/opencode
|
||||
bun --bun ./script/build.ts --single --skip-install
|
||||
bun --bun ./script/schema.ts schema.json
|
||||
|
||||
runHook postBuild
|
||||
'';
|
||||
@@ -63,76 +50,47 @@ stdenvNoCC.mkDerivation (finalAttrs: {
|
||||
installPhase = ''
|
||||
runHook preInstall
|
||||
|
||||
cd packages/opencode
|
||||
if [ ! -d dist ]; then
|
||||
echo "ERROR: dist directory missing after bundle step"
|
||||
exit 1
|
||||
fi
|
||||
install -Dm755 dist/opencode-*/bin/opencode $out/bin/opencode
|
||||
install -Dm644 schema.json $out/share/opencode/schema.json
|
||||
|
||||
mkdir -p $out/lib/opencode
|
||||
cp -r dist $out/lib/opencode/
|
||||
chmod -R u+w $out/lib/opencode/dist
|
||||
|
||||
# Select bundled worker assets deterministically (sorted find output)
|
||||
worker_file=$(find "$out/lib/opencode/dist" -type f \( -path '*/tui/worker.*' -o -name 'worker.*' \) | sort | head -n1)
|
||||
parser_worker_file=$(find "$out/lib/opencode/dist" -type f -name 'parser.worker.*' | sort | head -n1)
|
||||
if [ -z "$worker_file" ]; then
|
||||
echo "ERROR: bundled worker not found"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
main_wasm=$(printf '%s\n' "$out"/lib/opencode/dist/tree-sitter-*.wasm | sort | head -n1)
|
||||
wasm_list=$(find "$out/lib/opencode/dist" -maxdepth 1 -name 'tree-sitter-*.wasm' -print)
|
||||
for patch_file in "$worker_file" "$parser_worker_file"; do
|
||||
[ -z "$patch_file" ] && continue
|
||||
[ ! -f "$patch_file" ] && continue
|
||||
if [ -n "$wasm_list" ] && grep -q 'tree-sitter' "$patch_file"; then
|
||||
# Rewrite wasm references to absolute store paths to avoid runtime resolve failures.
|
||||
bun --bun ${scripts + "/patch-wasm.ts"} "$patch_file" "$main_wasm" $wasm_list
|
||||
fi
|
||||
done
|
||||
|
||||
mkdir -p $out/lib/opencode/node_modules
|
||||
cp -r ../../node_modules/.bun $out/lib/opencode/node_modules/
|
||||
mkdir -p $out/lib/opencode/node_modules/@opentui
|
||||
|
||||
mkdir -p $out/bin
|
||||
makeWrapper ${bun}/bin/bun $out/bin/opencode \
|
||||
--add-flags "run" \
|
||||
--add-flags "$out/lib/opencode/dist/src/index.js" \
|
||||
--prefix PATH : ${lib.makeBinPath [ ripgrep ]} \
|
||||
--argv0 opencode
|
||||
wrapProgram $out/bin/opencode \
|
||||
--prefix PATH : ${
|
||||
lib.makeBinPath (
|
||||
[
|
||||
ripgrep
|
||||
]
|
||||
# bun runs sysctl to detect if dunning on rosetta2
|
||||
++ lib.optional stdenvNoCC.hostPlatform.isDarwin sysctl
|
||||
)
|
||||
}
|
||||
|
||||
runHook postInstall
|
||||
'';
|
||||
|
||||
postInstall = ''
|
||||
for pkg in $out/lib/opencode/node_modules/.bun/@opentui+core-* $out/lib/opencode/node_modules/.bun/@opentui+solid-* $out/lib/opencode/node_modules/.bun/@opentui+core@* $out/lib/opencode/node_modules/.bun/@opentui+solid@*; do
|
||||
if [ -d "$pkg" ]; then
|
||||
pkgName=$(basename "$pkg" | sed 's/@opentui+\([^@]*\)@.*/\1/')
|
||||
ln -sf ../.bun/$(basename "$pkg")/node_modules/@opentui/$pkgName \
|
||||
$out/lib/opencode/node_modules/@opentui/$pkgName
|
||||
fi
|
||||
done
|
||||
postInstall = lib.optionalString (stdenvNoCC.buildPlatform.canExecute stdenvNoCC.hostPlatform) ''
|
||||
# trick yargs into also generating zsh completions
|
||||
installShellCompletion --cmd opencode \
|
||||
--bash <($out/bin/opencode completion) \
|
||||
--zsh <(SHELL=/bin/zsh $out/bin/opencode completion)
|
||||
'';
|
||||
|
||||
dontFixup = true;
|
||||
nativeInstallCheckInputs = [
|
||||
versionCheckHook
|
||||
writableTmpDirAsHomeHook
|
||||
];
|
||||
doInstallCheck = true;
|
||||
versionCheckKeepEnvironment = [ "HOME" ];
|
||||
versionCheckProgramArg = "--version";
|
||||
|
||||
passthru = {
|
||||
jsonschema = "${placeholder "out"}/share/opencode/schema.json";
|
||||
};
|
||||
|
||||
meta = {
|
||||
description = "AI coding agent built for the terminal";
|
||||
longDescription = ''
|
||||
OpenCode is a terminal-based agent that can build anything.
|
||||
It combines a TypeScript/JavaScript core with a Go-based TUI
|
||||
to provide an interactive AI coding experience.
|
||||
'';
|
||||
homepage = "https://github.com/anomalyco/opencode";
|
||||
description = "The open source coding agent";
|
||||
homepage = "https://opencode.ai/";
|
||||
license = lib.licenses.mit;
|
||||
platforms = [
|
||||
"aarch64-linux"
|
||||
"x86_64-linux"
|
||||
"aarch64-darwin"
|
||||
"x86_64-darwin"
|
||||
];
|
||||
mainProgram = "opencode";
|
||||
inherit (node_modules.meta) platforms;
|
||||
};
|
||||
})
|
||||
|
||||
@@ -1,120 +0,0 @@
|
||||
import solidPlugin from "./packages/opencode/node_modules/@opentui/solid/scripts/solid-plugin"
|
||||
import path from "path"
|
||||
import fs from "fs"
|
||||
|
||||
const version = "@VERSION@"
|
||||
const pkg = path.join(process.cwd(), "packages/opencode")
|
||||
const parser = fs.realpathSync(path.join(pkg, "./node_modules/@opentui/core/parser.worker.js"))
|
||||
const worker = "./src/cli/cmd/tui/worker.ts"
|
||||
const target = process.env["BUN_COMPILE_TARGET"]
|
||||
|
||||
if (!target) {
|
||||
throw new Error("BUN_COMPILE_TARGET not set")
|
||||
}
|
||||
|
||||
process.chdir(pkg)
|
||||
|
||||
const manifestName = "opencode-assets.manifest"
|
||||
const manifestPath = path.join(pkg, manifestName)
|
||||
|
||||
const readTrackedAssets = () => {
|
||||
if (!fs.existsSync(manifestPath)) return []
|
||||
return fs
|
||||
.readFileSync(manifestPath, "utf8")
|
||||
.split("\n")
|
||||
.map((line) => line.trim())
|
||||
.filter((line) => line.length > 0)
|
||||
}
|
||||
|
||||
const removeTrackedAssets = () => {
|
||||
for (const file of readTrackedAssets()) {
|
||||
const filePath = path.join(pkg, file)
|
||||
if (fs.existsSync(filePath)) {
|
||||
fs.rmSync(filePath, { force: true })
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const assets = new Set<string>()
|
||||
|
||||
const addAsset = async (p: string) => {
|
||||
const file = path.basename(p)
|
||||
const dest = path.join(pkg, file)
|
||||
await Bun.write(dest, Bun.file(p))
|
||||
assets.add(file)
|
||||
}
|
||||
|
||||
removeTrackedAssets()
|
||||
|
||||
const result = await Bun.build({
|
||||
conditions: ["browser"],
|
||||
tsconfig: "./tsconfig.json",
|
||||
plugins: [solidPlugin],
|
||||
sourcemap: "external",
|
||||
entrypoints: ["./src/index.ts", parser, worker],
|
||||
define: {
|
||||
OPENCODE_VERSION: `'@VERSION@'`,
|
||||
OTUI_TREE_SITTER_WORKER_PATH: "/$bunfs/root/" + path.relative(pkg, parser).replace(/\\/g, "/"),
|
||||
OPENCODE_CHANNEL: "'latest'",
|
||||
},
|
||||
compile: {
|
||||
target,
|
||||
outfile: "opencode",
|
||||
autoloadBunfig: false,
|
||||
autoloadDotenv: false,
|
||||
//@ts-ignore (bun types aren't up to date)
|
||||
autoloadTsconfig: true,
|
||||
autoloadPackageJson: true,
|
||||
execArgv: ["--user-agent=opencode/" + version, "--use-system-ca", "--"],
|
||||
windows: {},
|
||||
},
|
||||
})
|
||||
|
||||
if (!result.success) {
|
||||
console.error("Build failed!")
|
||||
for (const log of result.logs) {
|
||||
console.error(log)
|
||||
}
|
||||
throw new Error("Compilation failed")
|
||||
}
|
||||
|
||||
const assetOutputs = result.outputs?.filter((x) => x.kind === "asset") ?? []
|
||||
for (const x of assetOutputs) {
|
||||
await addAsset(x.path)
|
||||
}
|
||||
|
||||
const bundle = await Bun.build({
|
||||
entrypoints: [worker],
|
||||
tsconfig: "./tsconfig.json",
|
||||
plugins: [solidPlugin],
|
||||
target: "bun",
|
||||
outdir: "./.opencode-worker",
|
||||
sourcemap: "none",
|
||||
})
|
||||
|
||||
if (!bundle.success) {
|
||||
console.error("Worker build failed!")
|
||||
for (const log of bundle.logs) {
|
||||
console.error(log)
|
||||
}
|
||||
throw new Error("Worker compilation failed")
|
||||
}
|
||||
|
||||
const workerAssets = bundle.outputs?.filter((x) => x.kind === "asset") ?? []
|
||||
for (const x of workerAssets) {
|
||||
await addAsset(x.path)
|
||||
}
|
||||
|
||||
const output = bundle.outputs.find((x) => x.kind === "entry-point")
|
||||
if (!output) {
|
||||
throw new Error("Worker build produced no entry-point output")
|
||||
}
|
||||
|
||||
const dest = path.join(pkg, "opencode-worker.js")
|
||||
await Bun.write(dest, Bun.file(output.path))
|
||||
fs.rmSync(path.dirname(output.path), { recursive: true, force: true })
|
||||
|
||||
const list = Array.from(assets)
|
||||
await Bun.write(manifestPath, list.length > 0 ? list.join("\n") + "\n" : "")
|
||||
|
||||
console.log("Build successful!")
|
||||
@@ -1,43 +0,0 @@
|
||||
#!/usr/bin/env bun
|
||||
|
||||
import fs from "fs"
|
||||
import path from "path"
|
||||
|
||||
/**
|
||||
* Rewrite tree-sitter wasm references inside a JS file to absolute paths.
|
||||
* argv: [node, script, file, mainWasm, ...wasmPaths]
|
||||
*/
|
||||
const [, , file, mainWasm, ...wasmPaths] = process.argv
|
||||
|
||||
if (!file || !mainWasm) {
|
||||
console.error("usage: patch-wasm <file> <mainWasm> [wasmPaths...]")
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
const content = fs.readFileSync(file, "utf8")
|
||||
const byName = new Map<string, string>()
|
||||
|
||||
for (const wasm of wasmPaths) {
|
||||
const name = path.basename(wasm)
|
||||
byName.set(name, wasm)
|
||||
}
|
||||
|
||||
let next = content
|
||||
|
||||
for (const [name, wasmPath] of byName) {
|
||||
next = next.replaceAll(name, wasmPath)
|
||||
}
|
||||
|
||||
next = next.replaceAll("tree-sitter.wasm", mainWasm).replaceAll("web-tree-sitter/tree-sitter.wasm", mainWasm)
|
||||
|
||||
// Collapse any relative prefixes before absolute store paths (e.g., "../../../..//nix/store/...")
|
||||
const nixStorePrefix = process.env.NIX_STORE || "/nix/store"
|
||||
next = next.replace(/(\.\/)+/g, "./")
|
||||
next = next.replace(
|
||||
new RegExp(`(\\.\\.\\/)+\\/{1,2}(${nixStorePrefix.replace(/^\//, "").replace(/\//g, "\\/")}[^"']+)`, "g"),
|
||||
"/$2",
|
||||
)
|
||||
next = next.replace(new RegExp(`(["'])\\/{2,}(\\/${nixStorePrefix.replace(/\//g, "\\/")}[^"']+)(["'])`, "g"), "$1$2$3")
|
||||
next = next.replace(new RegExp(`(["'])\\/\\/(${nixStorePrefix.replace(/\//g, "\\/")}[^"']+)(["'])`, "g"), "$1$2$3")
|
||||
|
||||
if (next !== content) fs.writeFileSync(file, next)
|
||||
@@ -1,119 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
DUMMY="sha256-AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA="
|
||||
SYSTEM=${SYSTEM:-x86_64-linux}
|
||||
DEFAULT_HASH_FILE=${MODULES_HASH_FILE:-nix/hashes.json}
|
||||
HASH_FILE=${HASH_FILE:-$DEFAULT_HASH_FILE}
|
||||
|
||||
if [ ! -f "$HASH_FILE" ]; then
|
||||
cat >"$HASH_FILE" <<EOF
|
||||
{
|
||||
"nodeModules": {}
|
||||
}
|
||||
EOF
|
||||
fi
|
||||
|
||||
if git rev-parse --is-inside-work-tree >/dev/null 2>&1; then
|
||||
if ! git ls-files --error-unmatch "$HASH_FILE" >/dev/null 2>&1; then
|
||||
git add -N "$HASH_FILE" >/dev/null 2>&1 || true
|
||||
fi
|
||||
fi
|
||||
|
||||
export DUMMY
|
||||
export NIX_KEEP_OUTPUTS=1
|
||||
export NIX_KEEP_DERIVATIONS=1
|
||||
|
||||
cleanup() {
|
||||
rm -f "${JSON_OUTPUT:-}" "${BUILD_LOG:-}" "${TMP_EXPR:-}"
|
||||
}
|
||||
|
||||
trap cleanup EXIT
|
||||
|
||||
write_node_modules_hash() {
|
||||
local value="$1"
|
||||
local system="${2:-$SYSTEM}"
|
||||
local temp
|
||||
temp=$(mktemp)
|
||||
|
||||
if jq -e '.nodeModules | type == "object"' "$HASH_FILE" >/dev/null 2>&1; then
|
||||
jq --arg system "$system" --arg value "$value" '.nodeModules[$system] = $value' "$HASH_FILE" >"$temp"
|
||||
else
|
||||
jq --arg system "$system" --arg value "$value" '.nodeModules = {($system): $value}' "$HASH_FILE" >"$temp"
|
||||
fi
|
||||
|
||||
mv "$temp" "$HASH_FILE"
|
||||
}
|
||||
|
||||
TARGET="packages.${SYSTEM}.default"
|
||||
MODULES_ATTR=".#packages.${SYSTEM}.default.node_modules"
|
||||
CORRECT_HASH=""
|
||||
|
||||
DRV_PATH="$(nix eval --raw "${MODULES_ATTR}.drvPath")"
|
||||
|
||||
echo "Setting dummy node_modules outputHash for ${SYSTEM}..."
|
||||
write_node_modules_hash "$DUMMY"
|
||||
|
||||
BUILD_LOG=$(mktemp)
|
||||
JSON_OUTPUT=$(mktemp)
|
||||
|
||||
echo "Building node_modules for ${SYSTEM} to discover correct outputHash..."
|
||||
echo "Attempting to realize derivation: ${DRV_PATH}"
|
||||
REALISE_OUT=$(nix-store --realise "$DRV_PATH" --keep-failed 2>&1 | tee "$BUILD_LOG" || true)
|
||||
|
||||
BUILD_PATH=$(echo "$REALISE_OUT" | grep "^/nix/store/" | head -n1 || true)
|
||||
if [ -n "$BUILD_PATH" ] && [ -d "$BUILD_PATH" ]; then
|
||||
echo "Realized node_modules output: $BUILD_PATH"
|
||||
CORRECT_HASH=$(nix hash path --sri "$BUILD_PATH" 2>/dev/null || true)
|
||||
fi
|
||||
|
||||
if [ -z "$CORRECT_HASH" ]; then
|
||||
CORRECT_HASH="$(grep -E 'got:\s+sha256-[A-Za-z0-9+/=]+' "$BUILD_LOG" | awk '{print $2}' | head -n1 || true)"
|
||||
|
||||
if [ -z "$CORRECT_HASH" ]; then
|
||||
CORRECT_HASH="$(grep -A2 'hash mismatch' "$BUILD_LOG" | grep 'got:' | awk '{print $2}' | sed 's/sha256:/sha256-/' || true)"
|
||||
fi
|
||||
|
||||
if [ -z "$CORRECT_HASH" ]; then
|
||||
echo "Searching for kept failed build directory..."
|
||||
KEPT_DIR=$(grep -oE "build directory.*'[^']+'" "$BUILD_LOG" | grep -oE "'/[^']+'" | tr -d "'" | head -n1)
|
||||
|
||||
if [ -z "$KEPT_DIR" ]; then
|
||||
KEPT_DIR=$(grep -oE '/nix/var/nix/builds/[^ ]+' "$BUILD_LOG" | head -n1)
|
||||
fi
|
||||
|
||||
if [ -n "$KEPT_DIR" ] && [ -d "$KEPT_DIR" ]; then
|
||||
echo "Found kept build directory: $KEPT_DIR"
|
||||
if [ -d "$KEPT_DIR/build" ]; then
|
||||
HASH_PATH="$KEPT_DIR/build"
|
||||
else
|
||||
HASH_PATH="$KEPT_DIR"
|
||||
fi
|
||||
|
||||
echo "Attempting to hash: $HASH_PATH"
|
||||
ls -la "$HASH_PATH" || true
|
||||
|
||||
if [ -d "$HASH_PATH/node_modules" ]; then
|
||||
CORRECT_HASH=$(nix hash path --sri "$HASH_PATH" 2>/dev/null || true)
|
||||
echo "Computed hash from kept build: $CORRECT_HASH"
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
|
||||
if [ -z "$CORRECT_HASH" ]; then
|
||||
echo "Failed to determine correct node_modules hash for ${SYSTEM}."
|
||||
echo "Build log:"
|
||||
cat "$BUILD_LOG"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
write_node_modules_hash "$CORRECT_HASH"
|
||||
|
||||
jq -e --arg system "$SYSTEM" --arg hash "$CORRECT_HASH" '.nodeModules[$system] == $hash' "$HASH_FILE" >/dev/null
|
||||
|
||||
echo "node_modules hash updated for ${SYSTEM}: $CORRECT_HASH"
|
||||
|
||||
rm -f "$BUILD_LOG"
|
||||
unset BUILD_LOG
|
||||
@@ -28,6 +28,7 @@
|
||||
"@kobalte/core": "0.13.11",
|
||||
"@types/luxon": "3.7.1",
|
||||
"@types/node": "22.13.9",
|
||||
"@types/semver": "7.7.1",
|
||||
"@tsconfig/node22": "22.0.2",
|
||||
"@tsconfig/bun": "1.0.9",
|
||||
"@cloudflare/workers-types": "4.20251008.0",
|
||||
@@ -44,6 +45,7 @@
|
||||
"luxon": "3.6.1",
|
||||
"marked": "17.0.1",
|
||||
"marked-shiki": "1.2.1",
|
||||
"@playwright/test": "1.51.0",
|
||||
"typescript": "5.8.2",
|
||||
"@typescript/native-preview": "7.0.0-dev.20251207.1",
|
||||
"zod": "4.1.8",
|
||||
@@ -65,6 +67,7 @@
|
||||
"@tsconfig/bun": "catalog:",
|
||||
"husky": "9.1.7",
|
||||
"prettier": "3.6.2",
|
||||
"semver": "^7.6.0",
|
||||
"sst": "3.17.23",
|
||||
"turbo": "2.5.6"
|
||||
},
|
||||
|
||||
2
packages/app/.gitignore
vendored
2
packages/app/.gitignore
vendored
@@ -1 +1,3 @@
|
||||
src/assets/theme.css
|
||||
e2e/test-results
|
||||
e2e/playwright-report
|
||||
|
||||
@@ -1,9 +1,15 @@
|
||||
## Debugging
|
||||
|
||||
- To test the opencode app, use the playwright MCP server, the app is already
|
||||
running at http://localhost:3000
|
||||
- NEVER try to restart the app, or the server process, EVER.
|
||||
|
||||
## Local Dev
|
||||
|
||||
- `opencode dev web` proxies `https://app.opencode.ai`, so local UI/CSS changes will not show there.
|
||||
- For local UI changes, run the backend and app dev servers separately.
|
||||
- Backend (from `packages/opencode`): `bun run --conditions=browser ./src/index.ts serve --port 4096`
|
||||
- App (from `packages/app`): `bun dev -- --port 4444`
|
||||
- Open `http://localhost:4444` to verify UI changes (it targets the backend at `http://localhost:4096`).
|
||||
|
||||
## SolidJS
|
||||
|
||||
- Always prefer `createStore` over multiple `createSignal` calls
|
||||
@@ -11,3 +17,14 @@
|
||||
## Tool Calling
|
||||
|
||||
- ALWAYS USE PARALLEL TOOLS WHEN APPLICABLE.
|
||||
|
||||
## Browser Automation
|
||||
|
||||
Use `agent-browser` for web automation. Run `agent-browser --help` for all commands.
|
||||
|
||||
Core workflow:
|
||||
|
||||
1. `agent-browser open <url>` - Navigate to page
|
||||
2. `agent-browser snapshot -i` - Get interactive elements with refs (@e1, @e2)
|
||||
3. `agent-browser click @e1` / `fill @e2 "text"` - Interact using refs
|
||||
4. Re-snapshot after page changes
|
||||
|
||||
@@ -29,6 +29,23 @@ It correctly bundles Solid in production mode and optimizes the build for the be
|
||||
The build is minified and the filenames include the hashes.<br>
|
||||
Your app is ready to be deployed!
|
||||
|
||||
## E2E Testing
|
||||
|
||||
Playwright starts the Vite dev server automatically via `webServer`, and UI tests need an opencode backend (defaults to `localhost:4096`).
|
||||
Use the local runner to create a temp sandbox, seed data, and run the tests.
|
||||
|
||||
```bash
|
||||
bunx playwright install
|
||||
bun run test:e2e:local
|
||||
bun run test:e2e:local -- --grep "settings"
|
||||
```
|
||||
|
||||
Environment options:
|
||||
|
||||
- `PLAYWRIGHT_SERVER_HOST` / `PLAYWRIGHT_SERVER_PORT` (backend address, default: `localhost:4096`)
|
||||
- `PLAYWRIGHT_PORT` (Vite dev server port, default: `3000`)
|
||||
- `PLAYWRIGHT_BASE_URL` (override base URL, default: `http://localhost:<PLAYWRIGHT_PORT>`)
|
||||
|
||||
## Deployment
|
||||
|
||||
You can deploy the `dist` folder to any static host provider (netlify, surge, now, etc.)
|
||||
|
||||
45
packages/app/e2e/context.spec.ts
Normal file
45
packages/app/e2e/context.spec.ts
Normal file
@@ -0,0 +1,45 @@
|
||||
import { test, expect } from "./fixtures"
|
||||
import { promptSelector } from "./utils"
|
||||
|
||||
test("context panel can be opened from the prompt", async ({ page, sdk, gotoSession }) => {
|
||||
const title = `e2e smoke context ${Date.now()}`
|
||||
const created = await sdk.session.create({ title }).then((r) => r.data)
|
||||
|
||||
if (!created?.id) throw new Error("Session create did not return an id")
|
||||
const sessionID = created.id
|
||||
|
||||
try {
|
||||
await sdk.session.promptAsync({
|
||||
sessionID,
|
||||
noReply: true,
|
||||
parts: [
|
||||
{
|
||||
type: "text",
|
||||
text: "seed context",
|
||||
},
|
||||
],
|
||||
})
|
||||
|
||||
await expect
|
||||
.poll(async () => {
|
||||
const messages = await sdk.session.messages({ sessionID, limit: 1 }).then((r) => r.data ?? [])
|
||||
return messages.length
|
||||
})
|
||||
.toBeGreaterThan(0)
|
||||
|
||||
await gotoSession(sessionID)
|
||||
|
||||
const contextButton = page
|
||||
.locator('[data-component="button"]')
|
||||
.filter({ has: page.locator('[data-component="progress-circle"]').first() })
|
||||
.first()
|
||||
|
||||
await expect(contextButton).toBeVisible()
|
||||
await contextButton.click()
|
||||
|
||||
const tabs = page.locator('[data-component="tabs"][data-variant="normal"]')
|
||||
await expect(tabs.getByRole("tab", { name: "Context" })).toBeVisible()
|
||||
} finally {
|
||||
await sdk.session.delete({ sessionID }).catch(() => undefined)
|
||||
}
|
||||
})
|
||||
23
packages/app/e2e/file-open.spec.ts
Normal file
23
packages/app/e2e/file-open.spec.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
import { test, expect } from "./fixtures"
|
||||
import { modKey } from "./utils"
|
||||
|
||||
test("can open a file tab from the search palette", async ({ page, gotoSession }) => {
|
||||
await gotoSession()
|
||||
|
||||
await page.keyboard.press(`${modKey}+P`)
|
||||
|
||||
const dialog = page.getByRole("dialog")
|
||||
await expect(dialog).toBeVisible()
|
||||
|
||||
const input = dialog.getByRole("textbox").first()
|
||||
await input.fill("package.json")
|
||||
|
||||
const fileItem = dialog.locator('[data-slot="list-item"][data-key^="file:"]').first()
|
||||
await expect(fileItem).toBeVisible()
|
||||
await fileItem.click()
|
||||
|
||||
await expect(dialog).toHaveCount(0)
|
||||
|
||||
const tabs = page.locator('[data-component="tabs"][data-variant="normal"]')
|
||||
await expect(tabs.locator('[data-slot="tabs-trigger"]').first()).toBeVisible()
|
||||
})
|
||||
40
packages/app/e2e/fixtures.ts
Normal file
40
packages/app/e2e/fixtures.ts
Normal file
@@ -0,0 +1,40 @@
|
||||
import { test as base, expect } from "@playwright/test"
|
||||
import { createSdk, dirSlug, getWorktree, promptSelector, sessionPath } from "./utils"
|
||||
|
||||
type TestFixtures = {
|
||||
sdk: ReturnType<typeof createSdk>
|
||||
gotoSession: (sessionID?: string) => Promise<void>
|
||||
}
|
||||
|
||||
type WorkerFixtures = {
|
||||
directory: string
|
||||
slug: string
|
||||
}
|
||||
|
||||
export const test = base.extend<TestFixtures, WorkerFixtures>({
|
||||
directory: [
|
||||
async ({}, use) => {
|
||||
const directory = await getWorktree()
|
||||
await use(directory)
|
||||
},
|
||||
{ scope: "worker" },
|
||||
],
|
||||
slug: [
|
||||
async ({ directory }, use) => {
|
||||
await use(dirSlug(directory))
|
||||
},
|
||||
{ scope: "worker" },
|
||||
],
|
||||
sdk: async ({ directory }, use) => {
|
||||
await use(createSdk(directory))
|
||||
},
|
||||
gotoSession: async ({ page, directory }, use) => {
|
||||
const gotoSession = async (sessionID?: string) => {
|
||||
await page.goto(sessionPath(directory, sessionID))
|
||||
await expect(page.locator(promptSelector)).toBeVisible()
|
||||
}
|
||||
await use(gotoSession)
|
||||
},
|
||||
})
|
||||
|
||||
export { expect }
|
||||
21
packages/app/e2e/home.spec.ts
Normal file
21
packages/app/e2e/home.spec.ts
Normal file
@@ -0,0 +1,21 @@
|
||||
import { test, expect } from "./fixtures"
|
||||
import { serverName } from "./utils"
|
||||
|
||||
test("home renders and shows core entrypoints", async ({ page }) => {
|
||||
await page.goto("/")
|
||||
|
||||
await expect(page.getByRole("button", { name: "Open project" }).first()).toBeVisible()
|
||||
await expect(page.getByRole("button", { name: serverName })).toBeVisible()
|
||||
})
|
||||
|
||||
test("server picker dialog opens from home", async ({ page }) => {
|
||||
await page.goto("/")
|
||||
|
||||
const trigger = page.getByRole("button", { name: serverName })
|
||||
await expect(trigger).toBeVisible()
|
||||
await trigger.click()
|
||||
|
||||
const dialog = page.getByRole("dialog")
|
||||
await expect(dialog).toBeVisible()
|
||||
await expect(dialog.getByRole("textbox").first()).toBeVisible()
|
||||
})
|
||||
9
packages/app/e2e/navigation.spec.ts
Normal file
9
packages/app/e2e/navigation.spec.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
import { test, expect } from "./fixtures"
|
||||
import { dirPath, promptSelector } from "./utils"
|
||||
|
||||
test("project route redirects to /session", async ({ page, directory, slug }) => {
|
||||
await page.goto(dirPath(directory))
|
||||
|
||||
await expect(page).toHaveURL(new RegExp(`/${slug}/session`))
|
||||
await expect(page.locator(promptSelector)).toBeVisible()
|
||||
})
|
||||
15
packages/app/e2e/palette.spec.ts
Normal file
15
packages/app/e2e/palette.spec.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
import { test, expect } from "./fixtures"
|
||||
import { modKey } from "./utils"
|
||||
|
||||
test("search palette opens and closes", async ({ page, gotoSession }) => {
|
||||
await gotoSession()
|
||||
|
||||
await page.keyboard.press(`${modKey}+P`)
|
||||
|
||||
const dialog = page.getByRole("dialog")
|
||||
await expect(dialog).toBeVisible()
|
||||
await expect(dialog.getByRole("textbox").first()).toBeVisible()
|
||||
|
||||
await page.keyboard.press("Escape")
|
||||
await expect(dialog).toHaveCount(0)
|
||||
})
|
||||
62
packages/app/e2e/prompt.spec.ts
Normal file
62
packages/app/e2e/prompt.spec.ts
Normal file
@@ -0,0 +1,62 @@
|
||||
import { test, expect } from "./fixtures"
|
||||
import { promptSelector } from "./utils"
|
||||
|
||||
function sessionIDFromUrl(url: string) {
|
||||
const match = /\/session\/([^/?#]+)/.exec(url)
|
||||
return match?.[1]
|
||||
}
|
||||
|
||||
test("can send a prompt and receive a reply", async ({ page, sdk, gotoSession }) => {
|
||||
test.setTimeout(120_000)
|
||||
|
||||
const pageErrors: string[] = []
|
||||
const onPageError = (err: Error) => {
|
||||
pageErrors.push(err.message)
|
||||
}
|
||||
page.on("pageerror", onPageError)
|
||||
|
||||
await gotoSession()
|
||||
|
||||
const token = `E2E_OK_${Date.now()}`
|
||||
|
||||
const prompt = page.locator(promptSelector)
|
||||
await prompt.click()
|
||||
await page.keyboard.type(`Reply with exactly: ${token}`)
|
||||
await page.keyboard.press("Enter")
|
||||
|
||||
await expect(page).toHaveURL(/\/session\/[^/?#]+/, { timeout: 30_000 })
|
||||
|
||||
const sessionID = (() => {
|
||||
const id = sessionIDFromUrl(page.url())
|
||||
if (!id) throw new Error(`Failed to parse session id from url: ${page.url()}`)
|
||||
return id
|
||||
})()
|
||||
|
||||
try {
|
||||
await expect
|
||||
.poll(
|
||||
async () => {
|
||||
const messages = await sdk.session.messages({ sessionID, limit: 50 }).then((r) => r.data ?? [])
|
||||
return messages
|
||||
.filter((m) => m.info.role === "assistant")
|
||||
.flatMap((m) => m.parts)
|
||||
.filter((p) => p.type === "text")
|
||||
.map((p) => p.text)
|
||||
.join("\n")
|
||||
},
|
||||
{ timeout: 90_000 },
|
||||
)
|
||||
|
||||
.toContain(token)
|
||||
|
||||
const reply = page.locator('[data-slot="session-turn-summary-section"]').filter({ hasText: token }).first()
|
||||
await expect(reply).toBeVisible({ timeout: 90_000 })
|
||||
} finally {
|
||||
page.off("pageerror", onPageError)
|
||||
await sdk.session.delete({ sessionID }).catch(() => undefined)
|
||||
}
|
||||
|
||||
if (pageErrors.length > 0) {
|
||||
throw new Error(`Page error(s):\n${pageErrors.join("\n")}`)
|
||||
}
|
||||
})
|
||||
21
packages/app/e2e/session.spec.ts
Normal file
21
packages/app/e2e/session.spec.ts
Normal file
@@ -0,0 +1,21 @@
|
||||
import { test, expect } from "./fixtures"
|
||||
import { promptSelector } from "./utils"
|
||||
|
||||
test("can open an existing session and type into the prompt", async ({ page, sdk, gotoSession }) => {
|
||||
const title = `e2e smoke ${Date.now()}`
|
||||
const created = await sdk.session.create({ title }).then((r) => r.data)
|
||||
|
||||
if (!created?.id) throw new Error("Session create did not return an id")
|
||||
const sessionID = created.id
|
||||
|
||||
try {
|
||||
await gotoSession(sessionID)
|
||||
|
||||
const prompt = page.locator(promptSelector)
|
||||
await prompt.click()
|
||||
await page.keyboard.type("hello from e2e")
|
||||
await expect(prompt).toContainText("hello from e2e")
|
||||
} finally {
|
||||
await sdk.session.delete({ sessionID }).catch(() => undefined)
|
||||
}
|
||||
})
|
||||
21
packages/app/e2e/sidebar.spec.ts
Normal file
21
packages/app/e2e/sidebar.spec.ts
Normal file
@@ -0,0 +1,21 @@
|
||||
import { test, expect } from "./fixtures"
|
||||
import { modKey } from "./utils"
|
||||
|
||||
test("sidebar can be collapsed and expanded", async ({ page, gotoSession }) => {
|
||||
await gotoSession()
|
||||
|
||||
const main = page.locator("main")
|
||||
const closedClass = /xl:border-l/
|
||||
const isClosed = await main.evaluate((node) => node.className.includes("xl:border-l"))
|
||||
|
||||
if (isClosed) {
|
||||
await page.keyboard.press(`${modKey}+B`)
|
||||
await expect(main).not.toHaveClass(closedClass)
|
||||
}
|
||||
|
||||
await page.keyboard.press(`${modKey}+B`)
|
||||
await expect(main).toHaveClass(closedClass)
|
||||
|
||||
await page.keyboard.press(`${modKey}+B`)
|
||||
await expect(main).not.toHaveClass(closedClass)
|
||||
})
|
||||
16
packages/app/e2e/terminal.spec.ts
Normal file
16
packages/app/e2e/terminal.spec.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
import { test, expect } from "./fixtures"
|
||||
import { terminalSelector, terminalToggleKey } from "./utils"
|
||||
|
||||
test("terminal panel can be toggled", async ({ page, gotoSession }) => {
|
||||
await gotoSession()
|
||||
|
||||
const terminal = page.locator(terminalSelector)
|
||||
const initiallyOpen = await terminal.isVisible()
|
||||
if (initiallyOpen) {
|
||||
await page.keyboard.press(terminalToggleKey)
|
||||
await expect(terminal).toHaveCount(0)
|
||||
}
|
||||
|
||||
await page.keyboard.press(terminalToggleKey)
|
||||
await expect(terminal).toBeVisible()
|
||||
})
|
||||
38
packages/app/e2e/utils.ts
Normal file
38
packages/app/e2e/utils.ts
Normal file
@@ -0,0 +1,38 @@
|
||||
import { createOpencodeClient } from "@opencode-ai/sdk/v2/client"
|
||||
import { base64Encode } from "@opencode-ai/util/encode"
|
||||
|
||||
export const serverHost = process.env.PLAYWRIGHT_SERVER_HOST ?? "localhost"
|
||||
export const serverPort = process.env.PLAYWRIGHT_SERVER_PORT ?? "4096"
|
||||
|
||||
export const serverUrl = `http://${serverHost}:${serverPort}`
|
||||
export const serverName = `${serverHost}:${serverPort}`
|
||||
|
||||
export const modKey = process.platform === "darwin" ? "Meta" : "Control"
|
||||
export const terminalToggleKey = "Control+Backquote"
|
||||
|
||||
export const promptSelector = '[data-component="prompt-input"]'
|
||||
export const terminalSelector = '[data-component="terminal"]'
|
||||
|
||||
export function createSdk(directory?: string) {
|
||||
return createOpencodeClient({ baseUrl: serverUrl, directory, throwOnError: true })
|
||||
}
|
||||
|
||||
export async function getWorktree() {
|
||||
const sdk = createSdk()
|
||||
const result = await sdk.path.get()
|
||||
const data = result.data
|
||||
if (!data?.worktree) throw new Error(`Failed to resolve a worktree from ${serverUrl}/path`)
|
||||
return data.worktree
|
||||
}
|
||||
|
||||
export function dirSlug(directory: string) {
|
||||
return base64Encode(directory)
|
||||
}
|
||||
|
||||
export function dirPath(directory: string) {
|
||||
return `/${dirSlug(directory)}`
|
||||
}
|
||||
|
||||
export function sessionPath(directory: string, sessionID?: string) {
|
||||
return `${dirPath(directory)}/session${sessionID ? `/${sessionID}` : ""}`
|
||||
}
|
||||
@@ -4,10 +4,10 @@
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<title>OpenCode</title>
|
||||
<link rel="icon" type="image/png" href="/favicon-96x96.png" sizes="96x96" />
|
||||
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
||||
<link rel="shortcut icon" href="/favicon.ico" />
|
||||
<link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png" />
|
||||
<link rel="icon" type="image/png" href="/favicon-96x96-v3.png" sizes="96x96" />
|
||||
<link rel="icon" type="image/svg+xml" href="/favicon-v3.svg" />
|
||||
<link rel="shortcut icon" href="/favicon-v3.ico" />
|
||||
<link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon-v3.png" />
|
||||
<link rel="manifest" href="/site.webmanifest" />
|
||||
<meta name="theme-color" content="#F8F7F7" />
|
||||
<meta name="theme-color" content="#131010" media="(prefers-color-scheme: dark)" />
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@opencode-ai/app",
|
||||
"version": "1.1.21",
|
||||
"version": "1.1.31",
|
||||
"description": "",
|
||||
"type": "module",
|
||||
"exports": {
|
||||
@@ -12,11 +12,17 @@
|
||||
"start": "vite",
|
||||
"dev": "vite",
|
||||
"build": "vite build",
|
||||
"serve": "vite preview"
|
||||
"serve": "vite preview",
|
||||
"test": "playwright test",
|
||||
"test:e2e": "playwright test",
|
||||
"test:e2e:local": "bun script/e2e-local.ts",
|
||||
"test:e2e:ui": "playwright test --ui",
|
||||
"test:e2e:report": "playwright show-report e2e/playwright-report"
|
||||
},
|
||||
"license": "MIT",
|
||||
"devDependencies": {
|
||||
"@happy-dom/global-registrator": "20.0.11",
|
||||
"@playwright/test": "1.57.0",
|
||||
"@tailwindcss/vite": "catalog:",
|
||||
"@tsconfig/bun": "1.0.9",
|
||||
"@types/bun": "catalog:",
|
||||
@@ -36,6 +42,7 @@
|
||||
"@shikijs/transformers": "3.9.2",
|
||||
"@solid-primitives/active-element": "2.1.3",
|
||||
"@solid-primitives/audio": "1.4.2",
|
||||
"@solid-primitives/i18n": "2.2.1",
|
||||
"@solid-primitives/event-bus": "1.1.2",
|
||||
"@solid-primitives/media": "2.3.3",
|
||||
"@solid-primitives/resize-observer": "2.1.3",
|
||||
|
||||
43
packages/app/playwright.config.ts
Normal file
43
packages/app/playwright.config.ts
Normal file
@@ -0,0 +1,43 @@
|
||||
import { defineConfig, devices } from "@playwright/test"
|
||||
|
||||
const port = Number(process.env.PLAYWRIGHT_PORT ?? 3000)
|
||||
const baseURL = process.env.PLAYWRIGHT_BASE_URL ?? `http://localhost:${port}`
|
||||
const serverHost = process.env.PLAYWRIGHT_SERVER_HOST ?? "localhost"
|
||||
const serverPort = process.env.PLAYWRIGHT_SERVER_PORT ?? "4096"
|
||||
const command = `bun run dev -- --host 0.0.0.0 --port ${port}`
|
||||
const reuse = !process.env.CI
|
||||
|
||||
export default defineConfig({
|
||||
testDir: "./e2e",
|
||||
outputDir: "./e2e/test-results",
|
||||
timeout: 60_000,
|
||||
expect: {
|
||||
timeout: 10_000,
|
||||
},
|
||||
fullyParallel: true,
|
||||
forbidOnly: !!process.env.CI,
|
||||
retries: process.env.CI ? 2 : 0,
|
||||
reporter: [["html", { outputFolder: "e2e/playwright-report", open: "never" }], ["line"]],
|
||||
webServer: {
|
||||
command,
|
||||
url: baseURL,
|
||||
reuseExistingServer: reuse,
|
||||
timeout: 120_000,
|
||||
env: {
|
||||
VITE_OPENCODE_SERVER_HOST: serverHost,
|
||||
VITE_OPENCODE_SERVER_PORT: serverPort,
|
||||
},
|
||||
},
|
||||
use: {
|
||||
baseURL,
|
||||
trace: "on-first-retry",
|
||||
screenshot: "only-on-failure",
|
||||
video: "retain-on-failure",
|
||||
},
|
||||
projects: [
|
||||
{
|
||||
name: "chromium",
|
||||
use: { ...devices["Desktop Chrome"] },
|
||||
},
|
||||
],
|
||||
})
|
||||
1
packages/app/public/apple-touch-icon-v3.png
Symbolic link
1
packages/app/public/apple-touch-icon-v3.png
Symbolic link
@@ -0,0 +1 @@
|
||||
../../ui/src/assets/favicon/apple-touch-icon-v3.png
|
||||
1
packages/app/public/favicon-96x96-v3.png
Symbolic link
1
packages/app/public/favicon-96x96-v3.png
Symbolic link
@@ -0,0 +1 @@
|
||||
../../ui/src/assets/favicon/favicon-96x96-v3.png
|
||||
1
packages/app/public/favicon-v3.ico
Symbolic link
1
packages/app/public/favicon-v3.ico
Symbolic link
@@ -0,0 +1 @@
|
||||
../../ui/src/assets/favicon/favicon-v3.ico
|
||||
1
packages/app/public/favicon-v3.svg
Symbolic link
1
packages/app/public/favicon-v3.svg
Symbolic link
@@ -0,0 +1 @@
|
||||
../../ui/src/assets/favicon/favicon-v3.svg
|
||||
143
packages/app/script/e2e-local.ts
Normal file
143
packages/app/script/e2e-local.ts
Normal file
@@ -0,0 +1,143 @@
|
||||
import fs from "node:fs/promises"
|
||||
import net from "node:net"
|
||||
import os from "node:os"
|
||||
import path from "node:path"
|
||||
|
||||
async function freePort() {
|
||||
return await new Promise<number>((resolve, reject) => {
|
||||
const server = net.createServer()
|
||||
server.once("error", reject)
|
||||
server.listen(0, () => {
|
||||
const address = server.address()
|
||||
if (!address || typeof address === "string") {
|
||||
server.close(() => reject(new Error("Failed to acquire a free port")))
|
||||
return
|
||||
}
|
||||
server.close((err) => {
|
||||
if (err) {
|
||||
reject(err)
|
||||
return
|
||||
}
|
||||
resolve(address.port)
|
||||
})
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
async function waitForHealth(url: string) {
|
||||
const timeout = Date.now() + 120_000
|
||||
const errors: string[] = []
|
||||
while (Date.now() < timeout) {
|
||||
const result = await fetch(url)
|
||||
.then((r) => ({ ok: r.ok, error: undefined }))
|
||||
.catch((error) => ({
|
||||
ok: false,
|
||||
error: error instanceof Error ? error.message : String(error),
|
||||
}))
|
||||
if (result.ok) return
|
||||
if (result.error) errors.push(result.error)
|
||||
await new Promise((r) => setTimeout(r, 250))
|
||||
}
|
||||
const last = errors.length ? ` (last error: ${errors[errors.length - 1]})` : ""
|
||||
throw new Error(`Timed out waiting for server health: ${url}${last}`)
|
||||
}
|
||||
|
||||
const appDir = process.cwd()
|
||||
const repoDir = path.resolve(appDir, "../..")
|
||||
const opencodeDir = path.join(repoDir, "packages", "opencode")
|
||||
const modelsJson = path.join(opencodeDir, "test", "tool", "fixtures", "models-api.json")
|
||||
|
||||
const extraArgs = (() => {
|
||||
const args = process.argv.slice(2)
|
||||
if (args[0] === "--") return args.slice(1)
|
||||
return args
|
||||
})()
|
||||
|
||||
const [serverPort, webPort] = await Promise.all([freePort(), freePort()])
|
||||
|
||||
const sandbox = await fs.mkdtemp(path.join(os.tmpdir(), "opencode-e2e-"))
|
||||
|
||||
const serverEnv = {
|
||||
...process.env,
|
||||
MODELS_DEV_API_JSON: modelsJson,
|
||||
OPENCODE_DISABLE_MODELS_FETCH: "true",
|
||||
OPENCODE_DISABLE_SHARE: "true",
|
||||
OPENCODE_DISABLE_LSP_DOWNLOAD: "true",
|
||||
OPENCODE_DISABLE_DEFAULT_PLUGINS: "true",
|
||||
OPENCODE_EXPERIMENTAL_DISABLE_FILEWATCHER: "true",
|
||||
OPENCODE_TEST_HOME: path.join(sandbox, "home"),
|
||||
XDG_DATA_HOME: path.join(sandbox, "share"),
|
||||
XDG_CACHE_HOME: path.join(sandbox, "cache"),
|
||||
XDG_CONFIG_HOME: path.join(sandbox, "config"),
|
||||
XDG_STATE_HOME: path.join(sandbox, "state"),
|
||||
OPENCODE_E2E_PROJECT_DIR: repoDir,
|
||||
OPENCODE_E2E_SESSION_TITLE: "E2E Session",
|
||||
OPENCODE_E2E_MESSAGE: "Seeded for UI e2e",
|
||||
OPENCODE_E2E_MODEL: "opencode/gpt-5-nano",
|
||||
OPENCODE_CLIENT: "app",
|
||||
} satisfies Record<string, string>
|
||||
|
||||
const runnerEnv = {
|
||||
...serverEnv,
|
||||
PLAYWRIGHT_SERVER_HOST: "127.0.0.1",
|
||||
PLAYWRIGHT_SERVER_PORT: String(serverPort),
|
||||
VITE_OPENCODE_SERVER_HOST: "127.0.0.1",
|
||||
VITE_OPENCODE_SERVER_PORT: String(serverPort),
|
||||
PLAYWRIGHT_PORT: String(webPort),
|
||||
} satisfies Record<string, string>
|
||||
|
||||
const seed = Bun.spawn(["bun", "script/seed-e2e.ts"], {
|
||||
cwd: opencodeDir,
|
||||
env: serverEnv,
|
||||
stdout: "inherit",
|
||||
stderr: "inherit",
|
||||
})
|
||||
|
||||
const seedExit = await seed.exited
|
||||
if (seedExit !== 0) {
|
||||
process.exit(seedExit)
|
||||
}
|
||||
|
||||
Object.assign(process.env, serverEnv)
|
||||
process.env.AGENT = "1"
|
||||
process.env.OPENCODE = "1"
|
||||
|
||||
const log = await import("../../opencode/src/util/log")
|
||||
const install = await import("../../opencode/src/installation")
|
||||
await log.Log.init({
|
||||
print: true,
|
||||
dev: install.Installation.isLocal(),
|
||||
level: "WARN",
|
||||
})
|
||||
|
||||
const servermod = await import("../../opencode/src/server/server")
|
||||
const inst = await import("../../opencode/src/project/instance")
|
||||
const server = servermod.Server.listen({ port: serverPort, hostname: "127.0.0.1" })
|
||||
console.log(`opencode server listening on http://127.0.0.1:${serverPort}`)
|
||||
|
||||
const result = await (async () => {
|
||||
try {
|
||||
await waitForHealth(`http://127.0.0.1:${serverPort}/global/health`)
|
||||
|
||||
const runner = Bun.spawn(["bun", "test:e2e", ...extraArgs], {
|
||||
cwd: appDir,
|
||||
env: runnerEnv,
|
||||
stdout: "inherit",
|
||||
stderr: "inherit",
|
||||
})
|
||||
|
||||
return { code: await runner.exited }
|
||||
} catch (error) {
|
||||
return { error }
|
||||
} finally {
|
||||
await inst.Instance.disposeAll()
|
||||
await server.stop()
|
||||
}
|
||||
})()
|
||||
|
||||
if ("error" in result) {
|
||||
console.error(result.error)
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
process.exit(result.code)
|
||||
@@ -6,6 +6,7 @@ import { Font } from "@opencode-ai/ui/font"
|
||||
import { MarkedProvider } from "@opencode-ai/ui/context/marked"
|
||||
import { DiffComponentProvider } from "@opencode-ai/ui/context/diff"
|
||||
import { CodeComponentProvider } from "@opencode-ai/ui/context/code"
|
||||
import { I18nProvider } from "@opencode-ai/ui/context"
|
||||
import { Diff } from "@opencode-ai/ui/diff"
|
||||
import { Code } from "@opencode-ai/ui/code"
|
||||
import { ThemeProvider } from "@opencode-ai/ui/theme"
|
||||
@@ -14,12 +15,14 @@ import { PermissionProvider } from "@/context/permission"
|
||||
import { LayoutProvider } from "@/context/layout"
|
||||
import { GlobalSDKProvider } from "@/context/global-sdk"
|
||||
import { ServerProvider, useServer } from "@/context/server"
|
||||
import { SettingsProvider } from "@/context/settings"
|
||||
import { TerminalProvider } from "@/context/terminal"
|
||||
import { PromptProvider } from "@/context/prompt"
|
||||
import { FileProvider } from "@/context/file"
|
||||
import { NotificationProvider } from "@/context/notification"
|
||||
import { DialogProvider } from "@opencode-ai/ui/context/dialog"
|
||||
import { CommandProvider } from "@/context/command"
|
||||
import { LanguageProvider, useLanguage } from "@/context/language"
|
||||
import { Logo } from "@opencode-ai/ui/logo"
|
||||
import Layout from "@/pages/layout"
|
||||
import DirectoryLayout from "@/pages/directory-layout"
|
||||
@@ -29,7 +32,12 @@ import { Suspense } from "solid-js"
|
||||
|
||||
const Home = lazy(() => import("@/pages/home"))
|
||||
const Session = lazy(() => import("@/pages/session"))
|
||||
const Loading = () => <div class="size-full flex items-center justify-center text-text-weak">Loading...</div>
|
||||
const Loading = () => <div class="size-full" />
|
||||
|
||||
function UiI18nBridge(props: ParentProps) {
|
||||
const language = useLanguage()
|
||||
return <I18nProvider value={{ locale: language.locale, t: language.t }}>{props.children}</I18nProvider>
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface Window {
|
||||
@@ -42,15 +50,19 @@ export function AppBaseProviders(props: ParentProps) {
|
||||
<MetaProvider>
|
||||
<Font />
|
||||
<ThemeProvider>
|
||||
<ErrorBoundary fallback={(error) => <ErrorPage error={error} />}>
|
||||
<DialogProvider>
|
||||
<MarkedProvider>
|
||||
<DiffComponentProvider component={Diff}>
|
||||
<CodeComponentProvider component={Code}>{props.children}</CodeComponentProvider>
|
||||
</DiffComponentProvider>
|
||||
</MarkedProvider>
|
||||
</DialogProvider>
|
||||
</ErrorBoundary>
|
||||
<LanguageProvider>
|
||||
<UiI18nBridge>
|
||||
<ErrorBoundary fallback={(error) => <ErrorPage error={error} />}>
|
||||
<DialogProvider>
|
||||
<MarkedProvider>
|
||||
<DiffComponentProvider component={Diff}>
|
||||
<CodeComponentProvider component={Code}>{props.children}</CodeComponentProvider>
|
||||
</DiffComponentProvider>
|
||||
</MarkedProvider>
|
||||
</DialogProvider>
|
||||
</ErrorBoundary>
|
||||
</UiI18nBridge>
|
||||
</LanguageProvider>
|
||||
</ThemeProvider>
|
||||
</MetaProvider>
|
||||
)
|
||||
@@ -82,15 +94,17 @@ export function AppInterface(props: { defaultUrl?: string }) {
|
||||
<GlobalSyncProvider>
|
||||
<Router
|
||||
root={(props) => (
|
||||
<PermissionProvider>
|
||||
<LayoutProvider>
|
||||
<NotificationProvider>
|
||||
<CommandProvider>
|
||||
<Layout>{props.children}</Layout>
|
||||
</CommandProvider>
|
||||
</NotificationProvider>
|
||||
</LayoutProvider>
|
||||
</PermissionProvider>
|
||||
<SettingsProvider>
|
||||
<PermissionProvider>
|
||||
<LayoutProvider>
|
||||
<NotificationProvider>
|
||||
<CommandProvider>
|
||||
<Layout>{props.children}</Layout>
|
||||
</CommandProvider>
|
||||
</NotificationProvider>
|
||||
</LayoutProvider>
|
||||
</PermissionProvider>
|
||||
</SettingsProvider>
|
||||
)}
|
||||
>
|
||||
<Route
|
||||
@@ -105,16 +119,18 @@ export function AppInterface(props: { defaultUrl?: string }) {
|
||||
<Route path="/" component={() => <Navigate href="session" />} />
|
||||
<Route
|
||||
path="/session/:id?"
|
||||
component={() => (
|
||||
<TerminalProvider>
|
||||
<FileProvider>
|
||||
<PromptProvider>
|
||||
<Suspense fallback={<Loading />}>
|
||||
<Session />
|
||||
</Suspense>
|
||||
</PromptProvider>
|
||||
</FileProvider>
|
||||
</TerminalProvider>
|
||||
component={(p) => (
|
||||
<Show when={p.params.id ?? "new"} keyed>
|
||||
<TerminalProvider>
|
||||
<FileProvider>
|
||||
<PromptProvider>
|
||||
<Suspense fallback={<Loading />}>
|
||||
<Session />
|
||||
</Suspense>
|
||||
</PromptProvider>
|
||||
</FileProvider>
|
||||
</TerminalProvider>
|
||||
</Show>
|
||||
)}
|
||||
/>
|
||||
</Route>
|
||||
|
||||
@@ -14,6 +14,7 @@ import { iife } from "@opencode-ai/util/iife"
|
||||
import { createMemo, Match, onCleanup, onMount, Switch } from "solid-js"
|
||||
import { createStore, produce } from "solid-js/store"
|
||||
import { Link } from "@/components/link"
|
||||
import { useLanguage } from "@/context/language"
|
||||
import { useGlobalSDK } from "@/context/global-sdk"
|
||||
import { useGlobalSync } from "@/context/global-sync"
|
||||
import { usePlatform } from "@/context/platform"
|
||||
@@ -25,13 +26,14 @@ export function DialogConnectProvider(props: { provider: string }) {
|
||||
const globalSync = useGlobalSync()
|
||||
const globalSDK = useGlobalSDK()
|
||||
const platform = usePlatform()
|
||||
const language = useLanguage()
|
||||
const provider = createMemo(() => globalSync.data.provider.all.find((x) => x.id === props.provider)!)
|
||||
const methods = createMemo(
|
||||
() =>
|
||||
globalSync.data.provider_auth[props.provider] ?? [
|
||||
{
|
||||
type: "api",
|
||||
label: "API key",
|
||||
label: language.t("provider.connect.method.apiKey"),
|
||||
},
|
||||
],
|
||||
)
|
||||
@@ -44,6 +46,12 @@ export function DialogConnectProvider(props: { provider: string }) {
|
||||
|
||||
const method = createMemo(() => (store.methodIndex !== undefined ? methods().at(store.methodIndex!) : undefined))
|
||||
|
||||
const methodLabel = (value?: { type?: string; label?: string }) => {
|
||||
if (!value) return ""
|
||||
if (value.type === "api") return language.t("provider.connect.method.apiKey")
|
||||
return value.label ?? ""
|
||||
}
|
||||
|
||||
async function selectMethod(index: number) {
|
||||
const method = methods()[index]
|
||||
setStore(
|
||||
@@ -112,8 +120,8 @@ export function DialogConnectProvider(props: { provider: string }) {
|
||||
showToast({
|
||||
variant: "success",
|
||||
icon: "circle-check",
|
||||
title: `${provider().name} connected`,
|
||||
description: `${provider().name} models are now available to use.`,
|
||||
title: language.t("provider.connect.toast.connected.title", { provider: provider().name }),
|
||||
description: language.t("provider.connect.toast.connected.description", { provider: provider().name }),
|
||||
})
|
||||
}
|
||||
|
||||
@@ -142,16 +150,18 @@ export function DialogConnectProvider(props: { provider: string }) {
|
||||
<div class="text-16-medium text-text-strong">
|
||||
<Switch>
|
||||
<Match when={props.provider === "anthropic" && method()?.label?.toLowerCase().includes("max")}>
|
||||
Login with Claude Pro/Max
|
||||
{language.t("provider.connect.title.anthropicProMax")}
|
||||
</Match>
|
||||
<Match when={true}>Connect {provider().name}</Match>
|
||||
<Match when={true}>{language.t("provider.connect.title", { provider: provider().name })}</Match>
|
||||
</Switch>
|
||||
</div>
|
||||
</div>
|
||||
<div class="px-2.5 pb-10 flex flex-col gap-6">
|
||||
<Switch>
|
||||
<Match when={store.methodIndex === undefined}>
|
||||
<div class="text-14-regular text-text-base">Select login method for {provider().name}.</div>
|
||||
<div class="text-14-regular text-text-base">
|
||||
{language.t("provider.connect.selectMethod", { provider: provider().name })}
|
||||
</div>
|
||||
<div class="">
|
||||
<List
|
||||
ref={(ref) => {
|
||||
@@ -169,7 +179,7 @@ export function DialogConnectProvider(props: { provider: string }) {
|
||||
<div class="w-4 h-2 rounded-[1px] bg-input-base shadow-xs-border-base flex items-center justify-center">
|
||||
<div class="w-2.5 h-0.5 bg-icon-strong-base hidden" data-slot="list-item-extra-icon" />
|
||||
</div>
|
||||
<span>{i.label}</span>
|
||||
<span>{methodLabel(i)}</span>
|
||||
</div>
|
||||
)}
|
||||
</List>
|
||||
@@ -179,7 +189,7 @@ export function DialogConnectProvider(props: { provider: string }) {
|
||||
<div class="text-14-regular text-text-base">
|
||||
<div class="flex items-center gap-x-2">
|
||||
<Spinner />
|
||||
<span>Authorization in progress...</span>
|
||||
<span>{language.t("provider.connect.status.inProgress")}</span>
|
||||
</div>
|
||||
</div>
|
||||
</Match>
|
||||
@@ -187,7 +197,7 @@ export function DialogConnectProvider(props: { provider: string }) {
|
||||
<div class="text-14-regular text-text-base">
|
||||
<div class="flex items-center gap-x-2">
|
||||
<Icon name="circle-ban-sign" class="text-icon-critical-base" />
|
||||
<span>Authorization failed: {store.error}</span>
|
||||
<span>{language.t("provider.connect.status.failed", { error: store.error ?? "" })}</span>
|
||||
</div>
|
||||
</div>
|
||||
</Match>
|
||||
@@ -206,7 +216,7 @@ export function DialogConnectProvider(props: { provider: string }) {
|
||||
const apiKey = formData.get("apiKey") as string
|
||||
|
||||
if (!apiKey?.trim()) {
|
||||
setFormStore("error", "API key is required")
|
||||
setFormStore("error", language.t("provider.connect.apiKey.required"))
|
||||
return
|
||||
}
|
||||
|
||||
@@ -227,25 +237,23 @@ export function DialogConnectProvider(props: { provider: string }) {
|
||||
<Match when={provider().id === "opencode"}>
|
||||
<div class="flex flex-col gap-4">
|
||||
<div class="text-14-regular text-text-base">
|
||||
OpenCode Zen gives you access to a curated set of reliable optimized models for coding
|
||||
agents.
|
||||
{language.t("provider.connect.opencodeZen.line1")}
|
||||
</div>
|
||||
<div class="text-14-regular text-text-base">
|
||||
With a single API key you'll get access to models such as Claude, GPT, Gemini, GLM and more.
|
||||
{language.t("provider.connect.opencodeZen.line2")}
|
||||
</div>
|
||||
<div class="text-14-regular text-text-base">
|
||||
Visit{" "}
|
||||
{language.t("provider.connect.opencodeZen.visit.prefix")}
|
||||
<Link href="https://opencode.ai/zen" tabIndex={-1}>
|
||||
opencode.ai/zen
|
||||
</Link>{" "}
|
||||
to collect your API key.
|
||||
{language.t("provider.connect.opencodeZen.visit.link")}
|
||||
</Link>
|
||||
{language.t("provider.connect.opencodeZen.visit.suffix")}
|
||||
</div>
|
||||
</div>
|
||||
</Match>
|
||||
<Match when={true}>
|
||||
<div class="text-14-regular text-text-base">
|
||||
Enter your {provider().name} API key to connect your account and use {provider().name} models
|
||||
in OpenCode.
|
||||
{language.t("provider.connect.apiKey.description", { provider: provider().name })}
|
||||
</div>
|
||||
</Match>
|
||||
</Switch>
|
||||
@@ -253,8 +261,8 @@ export function DialogConnectProvider(props: { provider: string }) {
|
||||
<TextField
|
||||
autofocus
|
||||
type="text"
|
||||
label={`${provider().name} API key`}
|
||||
placeholder="API key"
|
||||
label={language.t("provider.connect.apiKey.label", { provider: provider().name })}
|
||||
placeholder={language.t("provider.connect.apiKey.placeholder")}
|
||||
name="apiKey"
|
||||
value={formStore.value}
|
||||
onChange={setFormStore.bind(null, "value")}
|
||||
@@ -262,7 +270,7 @@ export function DialogConnectProvider(props: { provider: string }) {
|
||||
error={formStore.error}
|
||||
/>
|
||||
<Button class="w-auto" type="submit" size="large" variant="primary">
|
||||
Submit
|
||||
{language.t("common.submit")}
|
||||
</Button>
|
||||
</form>
|
||||
</div>
|
||||
@@ -292,35 +300,44 @@ export function DialogConnectProvider(props: { provider: string }) {
|
||||
const code = formData.get("code") as string
|
||||
|
||||
if (!code?.trim()) {
|
||||
setFormStore("error", "Authorization code is required")
|
||||
setFormStore("error", language.t("provider.connect.oauth.code.required"))
|
||||
return
|
||||
}
|
||||
|
||||
setFormStore("error", undefined)
|
||||
const { error } = await globalSDK.client.provider.oauth.callback({
|
||||
providerID: props.provider,
|
||||
method: store.methodIndex,
|
||||
code,
|
||||
})
|
||||
if (!error) {
|
||||
const result = await globalSDK.client.provider.oauth
|
||||
.callback({
|
||||
providerID: props.provider,
|
||||
method: store.methodIndex,
|
||||
code,
|
||||
})
|
||||
.then((value) =>
|
||||
value.error ? { ok: false as const, error: value.error } : { ok: true as const },
|
||||
)
|
||||
.catch((error) => ({ ok: false as const, error }))
|
||||
if (result.ok) {
|
||||
await complete()
|
||||
return
|
||||
}
|
||||
setFormStore("error", "Invalid authorization code")
|
||||
const message = result.error instanceof Error ? result.error.message : String(result.error)
|
||||
setFormStore("error", message || language.t("provider.connect.oauth.code.invalid"))
|
||||
}
|
||||
|
||||
return (
|
||||
<div class="flex flex-col gap-6">
|
||||
<div class="text-14-regular text-text-base">
|
||||
Visit <Link href={store.authorization!.url}>this link</Link> to collect your authorization
|
||||
code to connect your account and use {provider().name} models in OpenCode.
|
||||
{language.t("provider.connect.oauth.code.visit.prefix")}
|
||||
<Link href={store.authorization!.url}>
|
||||
{language.t("provider.connect.oauth.code.visit.link")}
|
||||
</Link>
|
||||
{language.t("provider.connect.oauth.code.visit.suffix", { provider: provider().name })}
|
||||
</div>
|
||||
<form onSubmit={handleSubmit} class="flex flex-col items-start gap-4">
|
||||
<TextField
|
||||
autofocus
|
||||
type="text"
|
||||
label={`${method()?.label} authorization code`}
|
||||
placeholder="Authorization code"
|
||||
label={language.t("provider.connect.oauth.code.label", { method: method()?.label ?? "" })}
|
||||
placeholder={language.t("provider.connect.oauth.code.placeholder")}
|
||||
name="code"
|
||||
value={formStore.value}
|
||||
onChange={setFormStore.bind(null, "value")}
|
||||
@@ -328,7 +345,7 @@ export function DialogConnectProvider(props: { provider: string }) {
|
||||
error={formStore.error}
|
||||
/>
|
||||
<Button class="w-auto" type="submit" size="large" variant="primary">
|
||||
Submit
|
||||
{language.t("common.submit")}
|
||||
</Button>
|
||||
</form>
|
||||
</div>
|
||||
@@ -346,13 +363,19 @@ export function DialogConnectProvider(props: { provider: string }) {
|
||||
})
|
||||
|
||||
onMount(async () => {
|
||||
const result = await globalSDK.client.provider.oauth.callback({
|
||||
providerID: props.provider,
|
||||
method: store.methodIndex,
|
||||
})
|
||||
if (result.error) {
|
||||
// TODO: show error
|
||||
dialog.close()
|
||||
const result = await globalSDK.client.provider.oauth
|
||||
.callback({
|
||||
providerID: props.provider,
|
||||
method: store.methodIndex,
|
||||
})
|
||||
.then((value) =>
|
||||
value.error ? { ok: false as const, error: value.error } : { ok: true as const },
|
||||
)
|
||||
.catch((error) => ({ ok: false as const, error }))
|
||||
if (!result.ok) {
|
||||
const message = result.error instanceof Error ? result.error.message : String(result.error)
|
||||
setStore("state", "error")
|
||||
setStore("error", message)
|
||||
return
|
||||
}
|
||||
await complete()
|
||||
@@ -361,13 +384,22 @@ export function DialogConnectProvider(props: { provider: string }) {
|
||||
return (
|
||||
<div class="flex flex-col gap-6">
|
||||
<div class="text-14-regular text-text-base">
|
||||
Visit <Link href={store.authorization!.url}>this link</Link> and enter the code below to
|
||||
connect your account and use {provider().name} models in OpenCode.
|
||||
{language.t("provider.connect.oauth.auto.visit.prefix")}
|
||||
<Link href={store.authorization!.url}>
|
||||
{language.t("provider.connect.oauth.auto.visit.link")}
|
||||
</Link>
|
||||
{language.t("provider.connect.oauth.auto.visit.suffix", { provider: provider().name })}
|
||||
</div>
|
||||
<TextField label="Confirmation code" class="font-mono" value={code()} readOnly copyable />
|
||||
<TextField
|
||||
label={language.t("provider.connect.oauth.auto.confirmationCode")}
|
||||
class="font-mono"
|
||||
value={code()}
|
||||
readOnly
|
||||
copyable
|
||||
/>
|
||||
<div class="text-14-regular text-text-base flex items-center gap-4">
|
||||
<Spinner />
|
||||
<span>Waiting for authorization...</span>
|
||||
<span>{language.t("provider.connect.status.waiting")}</span>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
||||
@@ -9,12 +9,14 @@ import { useGlobalSDK } from "@/context/global-sdk"
|
||||
import { type LocalProject, getAvatarColors } from "@/context/layout"
|
||||
import { getFilename } from "@opencode-ai/util/path"
|
||||
import { Avatar } from "@opencode-ai/ui/avatar"
|
||||
import { useLanguage } from "@/context/language"
|
||||
|
||||
const AVATAR_COLOR_KEYS = ["pink", "mint", "orange", "purple", "cyan", "lime"] as const
|
||||
|
||||
export function DialogEditProject(props: { project: LocalProject }) {
|
||||
const dialog = useDialog()
|
||||
const globalSDK = useGlobalSDK()
|
||||
const language = useLanguage()
|
||||
|
||||
const folderName = createMemo(() => getFilename(props.project.worktree))
|
||||
const defaultName = createMemo(() => props.project.name || folderName())
|
||||
@@ -22,16 +24,20 @@ export function DialogEditProject(props: { project: LocalProject }) {
|
||||
const [store, setStore] = createStore({
|
||||
name: defaultName(),
|
||||
color: props.project.icon?.color || "pink",
|
||||
iconUrl: props.project.icon?.url || "",
|
||||
iconUrl: props.project.icon?.override || "",
|
||||
saving: false,
|
||||
})
|
||||
|
||||
const [dragOver, setDragOver] = createSignal(false)
|
||||
const [iconHover, setIconHover] = createSignal(false)
|
||||
|
||||
function handleFileSelect(file: File) {
|
||||
if (!file.type.startsWith("image/")) return
|
||||
const reader = new FileReader()
|
||||
reader.onload = (e) => setStore("iconUrl", e.target?.result as string)
|
||||
reader.onload = (e) => {
|
||||
setStore("iconUrl", e.target?.result as string)
|
||||
setIconHover(false)
|
||||
}
|
||||
reader.readAsDataURL(file)
|
||||
}
|
||||
|
||||
@@ -69,40 +75,48 @@ export function DialogEditProject(props: { project: LocalProject }) {
|
||||
const name = store.name.trim() === folderName() ? "" : store.name.trim()
|
||||
await globalSDK.client.project.update({
|
||||
projectID: props.project.id,
|
||||
directory: props.project.worktree,
|
||||
name,
|
||||
icon: { color: store.color, url: store.iconUrl },
|
||||
icon: { color: store.color, override: store.iconUrl },
|
||||
})
|
||||
setStore("saving", false)
|
||||
dialog.close()
|
||||
}
|
||||
|
||||
return (
|
||||
<Dialog title="Edit project">
|
||||
<form onSubmit={handleSubmit} class="flex flex-col gap-6 px-2.5 pb-3">
|
||||
<Dialog title={language.t("dialog.project.edit.title")} class="w-full max-w-[480px] mx-auto">
|
||||
<form onSubmit={handleSubmit} class="flex flex-col gap-6 p-6 pt-0">
|
||||
<div class="flex flex-col gap-4">
|
||||
<TextField
|
||||
autofocus
|
||||
type="text"
|
||||
label="Name"
|
||||
label={language.t("dialog.project.edit.name")}
|
||||
placeholder={folderName()}
|
||||
value={store.name}
|
||||
onChange={(v) => setStore("name", v)}
|
||||
/>
|
||||
|
||||
<div class="flex flex-col gap-2">
|
||||
<label class="text-12-medium text-text-weak">Icon</label>
|
||||
<label class="text-12-medium text-text-weak">{language.t("dialog.project.edit.icon")}</label>
|
||||
<div class="flex gap-3 items-start">
|
||||
<div class="relative">
|
||||
<div class="relative" onMouseEnter={() => setIconHover(true)} onMouseLeave={() => setIconHover(false)}>
|
||||
<div
|
||||
class="size-16 rounded-lg overflow-hidden border border-dashed transition-colors cursor-pointer"
|
||||
class="relative size-16 rounded-md transition-colors cursor-pointer"
|
||||
classList={{
|
||||
"border-text-interactive-base bg-surface-info-base/20": dragOver(),
|
||||
"border-border-base hover:border-border-strong": !dragOver(),
|
||||
"overflow-hidden": !!store.iconUrl,
|
||||
}}
|
||||
onDrop={handleDrop}
|
||||
onDragOver={handleDragOver}
|
||||
onDragLeave={handleDragLeave}
|
||||
onClick={() => document.getElementById("icon-upload")?.click()}
|
||||
onClick={() => {
|
||||
if (store.iconUrl && iconHover()) {
|
||||
clearIcon()
|
||||
} else {
|
||||
document.getElementById("icon-upload")?.click()
|
||||
}
|
||||
}}
|
||||
>
|
||||
<Show
|
||||
when={store.iconUrl}
|
||||
@@ -112,48 +126,87 @@ export function DialogEditProject(props: { project: LocalProject }) {
|
||||
fallback={store.name || defaultName()}
|
||||
{...getAvatarColors(store.color)}
|
||||
class="size-full"
|
||||
style={{ "font-size": "32px" }}
|
||||
/>
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<img src={store.iconUrl} alt="Project icon" class="size-full object-cover" />
|
||||
<img
|
||||
src={store.iconUrl}
|
||||
alt={language.t("dialog.project.edit.icon.alt")}
|
||||
class="size-full object-cover"
|
||||
/>
|
||||
</Show>
|
||||
</div>
|
||||
<Show when={store.iconUrl}>
|
||||
<button
|
||||
type="button"
|
||||
class="absolute -top-1.5 -right-1.5 size-5 rounded-full bg-surface-raised-base border border-border-base flex items-center justify-center hover:bg-surface-raised-base-hover"
|
||||
onClick={clearIcon}
|
||||
>
|
||||
<Icon name="close" class="size-3 text-icon-base" />
|
||||
</button>
|
||||
</Show>
|
||||
<div
|
||||
style={{
|
||||
position: "absolute",
|
||||
top: 0,
|
||||
left: 0,
|
||||
width: "64px",
|
||||
height: "64px",
|
||||
background: "rgba(0,0,0,0.6)",
|
||||
"border-radius": "6px",
|
||||
"z-index": 10,
|
||||
"pointer-events": "none",
|
||||
opacity: iconHover() && !store.iconUrl ? 1 : 0,
|
||||
display: "flex",
|
||||
"align-items": "center",
|
||||
"justify-content": "center",
|
||||
}}
|
||||
>
|
||||
<Icon name="cloud-upload" size="large" class="text-icon-invert-base" />
|
||||
</div>
|
||||
<div
|
||||
style={{
|
||||
position: "absolute",
|
||||
top: 0,
|
||||
left: 0,
|
||||
width: "64px",
|
||||
height: "64px",
|
||||
background: "rgba(0,0,0,0.6)",
|
||||
"border-radius": "6px",
|
||||
"z-index": 10,
|
||||
"pointer-events": "none",
|
||||
opacity: iconHover() && store.iconUrl ? 1 : 0,
|
||||
display: "flex",
|
||||
"align-items": "center",
|
||||
"justify-content": "center",
|
||||
}}
|
||||
>
|
||||
<Icon name="trash" size="large" class="text-icon-invert-base" />
|
||||
</div>
|
||||
</div>
|
||||
<input id="icon-upload" type="file" accept="image/*" class="hidden" onChange={handleInputChange} />
|
||||
<div class="flex flex-col gap-1.5 text-12-regular text-text-weak">
|
||||
<span>Click or drag an image</span>
|
||||
<span>Recommended: 128x128px</span>
|
||||
<div class="flex flex-col gap-1.5 text-12-regular text-text-weak self-center">
|
||||
<span>{language.t("dialog.project.edit.icon.hint")}</span>
|
||||
<span>{language.t("dialog.project.edit.icon.recommended")}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Show when={!store.iconUrl}>
|
||||
<div class="flex flex-col gap-2">
|
||||
<label class="text-12-medium text-text-weak">Color</label>
|
||||
<div class="flex gap-2">
|
||||
<label class="text-12-medium text-text-weak">{language.t("dialog.project.edit.color")}</label>
|
||||
<div class="flex gap-1.5">
|
||||
<For each={AVATAR_COLOR_KEYS}>
|
||||
{(color) => (
|
||||
<button
|
||||
type="button"
|
||||
class="relative size-8 rounded-md transition-all"
|
||||
classList={{
|
||||
"ring-2 ring-offset-2 ring-offset-surface-base ring-text-interactive-base":
|
||||
"flex items-center justify-center size-10 p-0.5 rounded-lg overflow-hidden transition-colors cursor-default": true,
|
||||
"bg-transparent border-2 border-icon-strong-base hover:bg-surface-base-hover":
|
||||
store.color === color,
|
||||
"bg-transparent border border-transparent hover:bg-surface-base-hover hover:border-border-weak-base":
|
||||
store.color !== color,
|
||||
}}
|
||||
style={{ background: getAvatarColors(color).background }}
|
||||
onClick={() => setStore("color", color)}
|
||||
>
|
||||
<Avatar fallback={store.name || defaultName()} {...getAvatarColors(color)} class="size-full" />
|
||||
<Avatar
|
||||
fallback={store.name || defaultName()}
|
||||
{...getAvatarColors(color)}
|
||||
class="size-full rounded"
|
||||
/>
|
||||
</button>
|
||||
)}
|
||||
</For>
|
||||
@@ -164,10 +217,10 @@ export function DialogEditProject(props: { project: LocalProject }) {
|
||||
|
||||
<div class="flex justify-end gap-2">
|
||||
<Button type="button" variant="ghost" size="large" onClick={() => dialog.close()}>
|
||||
Cancel
|
||||
{language.t("common.cancel")}
|
||||
</Button>
|
||||
<Button type="submit" variant="primary" size="large" disabled={store.saving}>
|
||||
{store.saving ? "Saving..." : "Save"}
|
||||
{store.saving ? language.t("common.saving") : language.t("common.save")}
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
@@ -9,6 +9,7 @@ import { List } from "@opencode-ai/ui/list"
|
||||
import { extractPromptFromParts } from "@/utils/prompt"
|
||||
import type { TextPart as SDKTextPart } from "@opencode-ai/sdk/v2/client"
|
||||
import { base64Encode } from "@opencode-ai/util/encode"
|
||||
import { useLanguage } from "@/context/language"
|
||||
|
||||
interface ForkableMessage {
|
||||
id: string
|
||||
@@ -27,6 +28,7 @@ export const DialogFork: Component = () => {
|
||||
const sdk = useSDK()
|
||||
const prompt = usePrompt()
|
||||
const dialog = useDialog()
|
||||
const language = useLanguage()
|
||||
|
||||
const messages = createMemo((): ForkableMessage[] => {
|
||||
const sessionID = params.id
|
||||
@@ -59,7 +61,10 @@ export const DialogFork: Component = () => {
|
||||
if (!sessionID) return
|
||||
|
||||
const parts = sync.data.part[item.id] ?? []
|
||||
const restored = extractPromptFromParts(parts, { directory: sdk.directory })
|
||||
const restored = extractPromptFromParts(parts, {
|
||||
directory: sdk.directory,
|
||||
attachmentName: language.t("common.attachment"),
|
||||
})
|
||||
|
||||
dialog.close()
|
||||
|
||||
@@ -73,11 +78,11 @@ export const DialogFork: Component = () => {
|
||||
}
|
||||
|
||||
return (
|
||||
<Dialog title="Fork from message">
|
||||
<Dialog title={language.t("command.session.fork")}>
|
||||
<List
|
||||
class="flex-1 min-h-0 [&_[data-slot=list-scroll]]:flex-1 [&_[data-slot=list-scroll]]:min-h-0"
|
||||
search={{ placeholder: "Search", autofocus: true }}
|
||||
emptyMessage="No messages to fork from"
|
||||
search={{ placeholder: language.t("common.search.placeholder"), autofocus: true }}
|
||||
emptyMessage={language.t("dialog.fork.empty")}
|
||||
key={(x) => x.id}
|
||||
items={messages}
|
||||
filterKeys={["text"]}
|
||||
|
||||
@@ -4,14 +4,16 @@ import { Switch } from "@opencode-ai/ui/switch"
|
||||
import type { Component } from "solid-js"
|
||||
import { useLocal } from "@/context/local"
|
||||
import { popularProviders } from "@/hooks/use-providers"
|
||||
import { useLanguage } from "@/context/language"
|
||||
|
||||
export const DialogManageModels: Component = () => {
|
||||
const local = useLocal()
|
||||
const language = useLanguage()
|
||||
return (
|
||||
<Dialog title="Manage models" description="Customize which models appear in the model selector.">
|
||||
<Dialog title={language.t("dialog.model.manage")} description={language.t("dialog.model.manage.description")}>
|
||||
<List
|
||||
search={{ placeholder: "Search models", autofocus: true }}
|
||||
emptyMessage="No model results"
|
||||
search={{ placeholder: language.t("dialog.model.search.placeholder"), autofocus: true }}
|
||||
emptyMessage={language.t("dialog.model.empty")}
|
||||
key={(x) => `${x?.provider?.id}:${x?.id}`}
|
||||
items={local.model.list()}
|
||||
filterKeys={["provider.name", "name", "id"]}
|
||||
|
||||
@@ -6,6 +6,7 @@ import { getDirectory, getFilename } from "@opencode-ai/util/path"
|
||||
import { createMemo } from "solid-js"
|
||||
import { useGlobalSDK } from "@/context/global-sdk"
|
||||
import { useGlobalSync } from "@/context/global-sync"
|
||||
import { useLanguage } from "@/context/language"
|
||||
|
||||
interface DialogSelectDirectoryProps {
|
||||
title?: string
|
||||
@@ -17,6 +18,7 @@ export function DialogSelectDirectory(props: DialogSelectDirectoryProps) {
|
||||
const sync = useGlobalSync()
|
||||
const sdk = useGlobalSDK()
|
||||
const dialog = useDialog()
|
||||
const language = useLanguage()
|
||||
|
||||
const home = createMemo(() => sync.data.path.home)
|
||||
const root = createMemo(() => sync.data.path.home || sync.data.path.directory)
|
||||
@@ -81,10 +83,11 @@ export function DialogSelectDirectory(props: DialogSelectDirectoryProps) {
|
||||
}
|
||||
|
||||
return (
|
||||
<Dialog title={props.title ?? "Open project"}>
|
||||
<Dialog title={props.title ?? language.t("command.project.open")}>
|
||||
<List
|
||||
search={{ placeholder: "Search folders", autofocus: true }}
|
||||
emptyMessage="No folders found"
|
||||
search={{ placeholder: language.t("dialog.directory.search.placeholder"), autofocus: true }}
|
||||
emptyMessage={language.t("dialog.directory.empty")}
|
||||
loadingMessage={language.t("common.loading")}
|
||||
items={directories}
|
||||
key={(x) => x}
|
||||
onSelect={(path) => {
|
||||
|
||||
@@ -1,14 +1,32 @@
|
||||
import { useDialog } from "@opencode-ai/ui/context/dialog"
|
||||
import { Dialog } from "@opencode-ai/ui/dialog"
|
||||
import { FileIcon } from "@opencode-ai/ui/file-icon"
|
||||
import { Keybind } from "@opencode-ai/ui/keybind"
|
||||
import { List } from "@opencode-ai/ui/list"
|
||||
import { getDirectory, getFilename } from "@opencode-ai/util/path"
|
||||
import { useParams } from "@solidjs/router"
|
||||
import { createMemo } from "solid-js"
|
||||
import { createMemo, createSignal, onCleanup, Show } from "solid-js"
|
||||
import { formatKeybind, useCommand, type CommandOption } from "@/context/command"
|
||||
import { useLayout } from "@/context/layout"
|
||||
import { useFile } from "@/context/file"
|
||||
import { useLanguage } from "@/context/language"
|
||||
|
||||
type EntryType = "command" | "file"
|
||||
|
||||
type Entry = {
|
||||
id: string
|
||||
type: EntryType
|
||||
title: string
|
||||
description?: string
|
||||
keybind?: string
|
||||
category: string
|
||||
option?: CommandOption
|
||||
path?: string
|
||||
}
|
||||
|
||||
export function DialogSelectFile() {
|
||||
const command = useCommand()
|
||||
const language = useLanguage()
|
||||
const layout = useLayout()
|
||||
const file = useFile()
|
||||
const dialog = useDialog()
|
||||
@@ -16,35 +34,161 @@ export function DialogSelectFile() {
|
||||
const sessionKey = createMemo(() => `${params.dir}${params.id ? "/" + params.id : ""}`)
|
||||
const tabs = createMemo(() => layout.tabs(sessionKey()))
|
||||
const view = createMemo(() => layout.view(sessionKey()))
|
||||
const state = { cleanup: undefined as (() => void) | void, committed: false }
|
||||
const [grouped, setGrouped] = createSignal(false)
|
||||
const common = [
|
||||
"session.new",
|
||||
"workspace.new",
|
||||
"session.previous",
|
||||
"session.next",
|
||||
"terminal.toggle",
|
||||
"review.toggle",
|
||||
]
|
||||
const limit = 5
|
||||
|
||||
const allowed = createMemo(() =>
|
||||
command.options.filter(
|
||||
(option) => !option.disabled && !option.id.startsWith("suggested.") && option.id !== "file.open",
|
||||
),
|
||||
)
|
||||
|
||||
const commandItem = (option: CommandOption): Entry => ({
|
||||
id: "command:" + option.id,
|
||||
type: "command",
|
||||
title: option.title,
|
||||
description: option.description,
|
||||
keybind: option.keybind,
|
||||
category: language.t("palette.group.commands"),
|
||||
option,
|
||||
})
|
||||
|
||||
const fileItem = (path: string): Entry => ({
|
||||
id: "file:" + path,
|
||||
type: "file",
|
||||
title: path,
|
||||
category: language.t("palette.group.files"),
|
||||
path,
|
||||
})
|
||||
|
||||
const list = createMemo(() => allowed().map(commandItem))
|
||||
|
||||
const picks = createMemo(() => {
|
||||
const all = allowed()
|
||||
const order = new Map(common.map((id, index) => [id, index]))
|
||||
const picked = all.filter((option) => order.has(option.id))
|
||||
const base = picked.length ? picked : all.slice(0, limit)
|
||||
const sorted = picked.length ? [...base].sort((a, b) => (order.get(a.id) ?? 0) - (order.get(b.id) ?? 0)) : base
|
||||
return sorted.map(commandItem)
|
||||
})
|
||||
|
||||
const recent = createMemo(() => {
|
||||
const all = tabs().all()
|
||||
const active = tabs().active()
|
||||
const order = active ? [active, ...all.filter((item) => item !== active)] : all
|
||||
const seen = new Set<string>()
|
||||
const items: Entry[] = []
|
||||
|
||||
for (const item of order) {
|
||||
const path = file.pathFromTab(item)
|
||||
if (!path) continue
|
||||
if (seen.has(path)) continue
|
||||
seen.add(path)
|
||||
items.push(fileItem(path))
|
||||
}
|
||||
|
||||
return items.slice(0, limit)
|
||||
})
|
||||
|
||||
const items = async (filter: string) => {
|
||||
const query = filter.trim()
|
||||
setGrouped(query.length > 0)
|
||||
if (!query) return [...picks(), ...recent()]
|
||||
const files = await file.searchFiles(query)
|
||||
const entries = files.map(fileItem)
|
||||
return [...list(), ...entries]
|
||||
}
|
||||
|
||||
const handleMove = (item: Entry | undefined) => {
|
||||
state.cleanup?.()
|
||||
if (!item) return
|
||||
if (item.type !== "command") return
|
||||
state.cleanup = item.option?.onHighlight?.()
|
||||
}
|
||||
|
||||
const open = (path: string) => {
|
||||
const value = file.tab(path)
|
||||
tabs().open(value)
|
||||
file.load(path)
|
||||
view().reviewPanel.open()
|
||||
}
|
||||
|
||||
const handleSelect = (item: Entry | undefined) => {
|
||||
if (!item) return
|
||||
state.committed = true
|
||||
state.cleanup = undefined
|
||||
dialog.close()
|
||||
|
||||
if (item.type === "command") {
|
||||
item.option?.onSelect?.("palette")
|
||||
return
|
||||
}
|
||||
|
||||
if (!item.path) return
|
||||
open(item.path)
|
||||
}
|
||||
|
||||
onCleanup(() => {
|
||||
if (state.committed) return
|
||||
state.cleanup?.()
|
||||
})
|
||||
|
||||
return (
|
||||
<Dialog title="Select file">
|
||||
<Dialog class="pt-3 pb-0 !max-h-[480px]">
|
||||
<List
|
||||
search={{ placeholder: "Search files", autofocus: true }}
|
||||
emptyMessage="No files found"
|
||||
items={file.searchFiles}
|
||||
key={(x) => x}
|
||||
onSelect={(path) => {
|
||||
if (path) {
|
||||
const value = file.tab(path)
|
||||
tabs().open(value)
|
||||
file.load(path)
|
||||
view().reviewPanel.open()
|
||||
}
|
||||
dialog.close()
|
||||
search={{
|
||||
placeholder: language.t("palette.search.placeholder"),
|
||||
autofocus: true,
|
||||
hideIcon: true,
|
||||
class: "pl-3 pr-2 !mb-0",
|
||||
}}
|
||||
emptyMessage={language.t("palette.empty")}
|
||||
loadingMessage={language.t("common.loading")}
|
||||
items={items}
|
||||
key={(item) => item.id}
|
||||
filterKeys={["title", "description", "category"]}
|
||||
groupBy={(item) => item.category}
|
||||
onMove={handleMove}
|
||||
onSelect={handleSelect}
|
||||
>
|
||||
{(i) => (
|
||||
<div class="w-full flex items-center justify-between rounded-md">
|
||||
<div class="flex items-center gap-x-3 grow min-w-0">
|
||||
<FileIcon node={{ path: i, type: "file" }} class="shrink-0 size-4" />
|
||||
<div class="flex items-center text-14-regular">
|
||||
<span class="text-text-weak whitespace-nowrap overflow-hidden overflow-ellipsis truncate min-w-0">
|
||||
{getDirectory(i)}
|
||||
</span>
|
||||
<span class="text-text-strong whitespace-nowrap">{getFilename(i)}</span>
|
||||
{(item) => (
|
||||
<Show
|
||||
when={item.type === "command"}
|
||||
fallback={
|
||||
<div class="w-full flex items-center justify-between rounded-md pl-1">
|
||||
<div class="flex items-center gap-x-3 grow min-w-0">
|
||||
<FileIcon node={{ path: item.path ?? "", type: "file" }} class="shrink-0 size-4" />
|
||||
<div class="flex items-center text-14-regular">
|
||||
<span class="text-text-weak whitespace-nowrap overflow-hidden overflow-ellipsis truncate min-w-0">
|
||||
{getDirectory(item.path ?? "")}
|
||||
</span>
|
||||
<span class="text-text-strong whitespace-nowrap">{getFilename(item.path ?? "")}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<div class="w-full flex items-center justify-between gap-4 pl-1">
|
||||
<div class="flex items-center gap-2 min-w-0">
|
||||
<span class="text-14-regular text-text-strong whitespace-nowrap">{item.title}</span>
|
||||
<Show when={item.description}>
|
||||
<span class="text-14-regular text-text-weak truncate">{item.description}</span>
|
||||
</Show>
|
||||
</div>
|
||||
<Show when={item.keybind}>
|
||||
<Keybind class="rounded-[4px]">{formatKeybind(item.keybind ?? "")}</Keybind>
|
||||
</Show>
|
||||
</div>
|
||||
</div>
|
||||
</Show>
|
||||
)}
|
||||
</List>
|
||||
</Dialog>
|
||||
|
||||
@@ -4,10 +4,12 @@ import { useSDK } from "@/context/sdk"
|
||||
import { Dialog } from "@opencode-ai/ui/dialog"
|
||||
import { List } from "@opencode-ai/ui/list"
|
||||
import { Switch } from "@opencode-ai/ui/switch"
|
||||
import { useLanguage } from "@/context/language"
|
||||
|
||||
export const DialogSelectMcp: Component = () => {
|
||||
const sync = useSync()
|
||||
const sdk = useSDK()
|
||||
const language = useLanguage()
|
||||
const [loading, setLoading] = createSignal<string | null>(null)
|
||||
|
||||
const items = createMemo(() =>
|
||||
@@ -34,10 +36,13 @@ export const DialogSelectMcp: Component = () => {
|
||||
const totalCount = createMemo(() => items().length)
|
||||
|
||||
return (
|
||||
<Dialog title="MCPs" description={`${enabledCount()} of ${totalCount()} enabled`}>
|
||||
<Dialog
|
||||
title={language.t("dialog.mcp.title")}
|
||||
description={language.t("dialog.mcp.description", { enabled: enabledCount(), total: totalCount() })}
|
||||
>
|
||||
<List
|
||||
search={{ placeholder: "Search", autofocus: true }}
|
||||
emptyMessage="No MCPs configured"
|
||||
search={{ placeholder: language.t("common.search.placeholder"), autofocus: true }}
|
||||
emptyMessage={language.t("dialog.mcp.empty")}
|
||||
key={(x) => x?.name ?? ""}
|
||||
items={items}
|
||||
filterKeys={["name", "status"]}
|
||||
@@ -60,19 +65,19 @@ export const DialogSelectMcp: Component = () => {
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="truncate">{i.name}</span>
|
||||
<Show when={status() === "connected"}>
|
||||
<span class="text-11-regular text-text-weaker">connected</span>
|
||||
<span class="text-11-regular text-text-weaker">{language.t("mcp.status.connected")}</span>
|
||||
</Show>
|
||||
<Show when={status() === "failed"}>
|
||||
<span class="text-11-regular text-text-weaker">failed</span>
|
||||
<span class="text-11-regular text-text-weaker">{language.t("mcp.status.failed")}</span>
|
||||
</Show>
|
||||
<Show when={status() === "needs_auth"}>
|
||||
<span class="text-11-regular text-text-weaker">needs auth</span>
|
||||
<span class="text-11-regular text-text-weaker">{language.t("mcp.status.needs_auth")}</span>
|
||||
</Show>
|
||||
<Show when={status() === "disabled"}>
|
||||
<span class="text-11-regular text-text-weaker">disabled</span>
|
||||
<span class="text-11-regular text-text-weaker">{language.t("mcp.status.disabled")}</span>
|
||||
</Show>
|
||||
<Show when={loading() === i.name}>
|
||||
<span class="text-11-regular text-text-weak">...</span>
|
||||
<span class="text-11-regular text-text-weak">{language.t("common.loading.ellipsis")}</span>
|
||||
</Show>
|
||||
</div>
|
||||
<Show when={error()}>
|
||||
|
||||
@@ -5,16 +5,20 @@ import type { IconName } from "@opencode-ai/ui/icons/provider"
|
||||
import { List, type ListRef } from "@opencode-ai/ui/list"
|
||||
import { ProviderIcon } from "@opencode-ai/ui/provider-icon"
|
||||
import { Tag } from "@opencode-ai/ui/tag"
|
||||
import { Tooltip } from "@opencode-ai/ui/tooltip"
|
||||
import { type Component, onCleanup, onMount, Show } from "solid-js"
|
||||
import { useLocal } from "@/context/local"
|
||||
import { popularProviders, useProviders } from "@/hooks/use-providers"
|
||||
import { DialogConnectProvider } from "./dialog-connect-provider"
|
||||
import { DialogSelectProvider } from "./dialog-select-provider"
|
||||
import { ModelTooltip } from "./model-tooltip"
|
||||
import { useLanguage } from "@/context/language"
|
||||
|
||||
export const DialogSelectModelUnpaid: Component = () => {
|
||||
const local = useLocal()
|
||||
const dialog = useDialog()
|
||||
const providers = useProviders()
|
||||
const language = useLanguage()
|
||||
|
||||
let listRef: ListRef | undefined
|
||||
const handleKey = (e: KeyboardEvent) => {
|
||||
@@ -30,14 +34,30 @@ export const DialogSelectModelUnpaid: Component = () => {
|
||||
})
|
||||
|
||||
return (
|
||||
<Dialog title="Select model">
|
||||
<Dialog title={language.t("dialog.model.select.title")}>
|
||||
<div class="flex flex-col gap-3 px-2.5">
|
||||
<div class="text-14-medium text-text-base px-2.5">Free models provided by OpenCode</div>
|
||||
<div class="text-14-medium text-text-base px-2.5">{language.t("dialog.model.unpaid.freeModels.title")}</div>
|
||||
<List
|
||||
ref={(ref) => (listRef = ref)}
|
||||
items={local.model.list}
|
||||
current={local.model.current()}
|
||||
key={(x) => `${x.provider.id}:${x.id}`}
|
||||
itemWrapper={(item, node) => (
|
||||
<Tooltip
|
||||
class="w-full"
|
||||
placement="right-start"
|
||||
gutter={12}
|
||||
value={
|
||||
<ModelTooltip
|
||||
model={item}
|
||||
latest={item.latest}
|
||||
free={item.provider.id === "opencode" && (!item.cost || item.cost.input === 0)}
|
||||
/>
|
||||
}
|
||||
>
|
||||
{node}
|
||||
</Tooltip>
|
||||
)}
|
||||
onSelect={(x) => {
|
||||
local.model.set(x ? { modelID: x.id, providerID: x.provider.id } : undefined, {
|
||||
recent: true,
|
||||
@@ -48,9 +68,9 @@ export const DialogSelectModelUnpaid: Component = () => {
|
||||
{(i) => (
|
||||
<div class="w-full flex items-center gap-x-2.5">
|
||||
<span>{i.name}</span>
|
||||
<Tag>Free</Tag>
|
||||
<Tag>{language.t("model.tag.free")}</Tag>
|
||||
<Show when={i.latest}>
|
||||
<Tag>Latest</Tag>
|
||||
<Tag>{language.t("model.tag.latest")}</Tag>
|
||||
</Show>
|
||||
</div>
|
||||
)}
|
||||
@@ -61,7 +81,7 @@ export const DialogSelectModelUnpaid: Component = () => {
|
||||
<div class="px-1.5 pb-1.5">
|
||||
<div class="w-full rounded-sm border border-border-weak-base bg-surface-raised-base">
|
||||
<div class="w-full flex flex-col items-start gap-4 px-1.5 pt-4 pb-4">
|
||||
<div class="px-2 text-14-medium text-text-base">Add more models from popular providers</div>
|
||||
<div class="px-2 text-14-medium text-text-base">{language.t("dialog.model.unpaid.addMore.title")}</div>
|
||||
<div class="w-full">
|
||||
<List
|
||||
class="w-full px-0"
|
||||
@@ -83,10 +103,10 @@ export const DialogSelectModelUnpaid: Component = () => {
|
||||
<ProviderIcon data-slot="list-item-extra-icon" id={i.id as IconName} />
|
||||
<span>{i.name}</span>
|
||||
<Show when={i.id === "opencode"}>
|
||||
<Tag>Recommended</Tag>
|
||||
<Tag>{language.t("dialog.provider.tag.recommended")}</Tag>
|
||||
</Show>
|
||||
<Show when={i.id === "anthropic"}>
|
||||
<div class="text-14-regular text-text-weak">Connect with Claude Pro/Max or API key</div>
|
||||
<div class="text-14-regular text-text-weak">{language.t("dialog.provider.anthropic.note")}</div>
|
||||
</Show>
|
||||
</div>
|
||||
)}
|
||||
@@ -99,7 +119,7 @@ export const DialogSelectModelUnpaid: Component = () => {
|
||||
dialog.show(() => <DialogSelectProvider />)
|
||||
}}
|
||||
>
|
||||
View all providers
|
||||
{language.t("dialog.provider.viewAll")}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -4,18 +4,24 @@ import { useLocal } from "@/context/local"
|
||||
import { useDialog } from "@opencode-ai/ui/context/dialog"
|
||||
import { popularProviders } from "@/hooks/use-providers"
|
||||
import { Button } from "@opencode-ai/ui/button"
|
||||
import { IconButton } from "@opencode-ai/ui/icon-button"
|
||||
import { Tag } from "@opencode-ai/ui/tag"
|
||||
import { Dialog } from "@opencode-ai/ui/dialog"
|
||||
import { List } from "@opencode-ai/ui/list"
|
||||
import { Tooltip } from "@opencode-ai/ui/tooltip"
|
||||
import { DialogSelectProvider } from "./dialog-select-provider"
|
||||
import { DialogManageModels } from "./dialog-manage-models"
|
||||
import { ModelTooltip } from "./model-tooltip"
|
||||
import { useLanguage } from "@/context/language"
|
||||
|
||||
const ModelList: Component<{
|
||||
provider?: string
|
||||
class?: string
|
||||
onSelect: () => void
|
||||
action?: JSX.Element
|
||||
}> = (props) => {
|
||||
const local = useLocal()
|
||||
const language = useLanguage()
|
||||
|
||||
const models = createMemo(() =>
|
||||
local.model
|
||||
@@ -27,8 +33,8 @@ const ModelList: Component<{
|
||||
return (
|
||||
<List
|
||||
class={`flex-1 min-h-0 [&_[data-slot=list-scroll]]:flex-1 [&_[data-slot=list-scroll]]:min-h-0 ${props.class ?? ""}`}
|
||||
search={{ placeholder: "Search models", autofocus: true }}
|
||||
emptyMessage="No model results"
|
||||
search={{ placeholder: language.t("dialog.model.search.placeholder"), autofocus: true, action: props.action }}
|
||||
emptyMessage={language.t("dialog.model.empty")}
|
||||
key={(x) => `${x.provider.id}:${x.id}`}
|
||||
items={models}
|
||||
current={local.model.current()}
|
||||
@@ -36,14 +42,28 @@ const ModelList: Component<{
|
||||
sortBy={(a, b) => a.name.localeCompare(b.name)}
|
||||
groupBy={(x) => x.provider.name}
|
||||
sortGroupsBy={(a, b) => {
|
||||
if (a.category === "Recent" && b.category !== "Recent") return -1
|
||||
if (b.category === "Recent" && a.category !== "Recent") return 1
|
||||
const aProvider = a.items[0].provider.id
|
||||
const bProvider = b.items[0].provider.id
|
||||
if (popularProviders.includes(aProvider) && !popularProviders.includes(bProvider)) return -1
|
||||
if (!popularProviders.includes(aProvider) && popularProviders.includes(bProvider)) return 1
|
||||
return popularProviders.indexOf(aProvider) - popularProviders.indexOf(bProvider)
|
||||
}}
|
||||
itemWrapper={(item, node) => (
|
||||
<Tooltip
|
||||
class="w-full"
|
||||
placement="right-start"
|
||||
gutter={12}
|
||||
value={
|
||||
<ModelTooltip
|
||||
model={item}
|
||||
latest={item.latest}
|
||||
free={item.provider.id === "opencode" && (!item.cost || item.cost.input === 0)}
|
||||
/>
|
||||
}
|
||||
>
|
||||
{node}
|
||||
</Tooltip>
|
||||
)}
|
||||
onSelect={(x) => {
|
||||
local.model.set(x ? { modelID: x.id, providerID: x.provider.id } : undefined, {
|
||||
recent: true,
|
||||
@@ -55,10 +75,10 @@ const ModelList: Component<{
|
||||
<div class="w-full flex items-center gap-x-2 text-13-regular">
|
||||
<span class="truncate">{i.name}</span>
|
||||
<Show when={i.provider.id === "opencode" && (!i.cost || i.cost?.input === 0)}>
|
||||
<Tag>Free</Tag>
|
||||
<Tag>{language.t("model.tag.free")}</Tag>
|
||||
</Show>
|
||||
<Show when={i.latest}>
|
||||
<Tag>Latest</Tag>
|
||||
<Tag>{language.t("model.tag.latest")}</Tag>
|
||||
</Show>
|
||||
</div>
|
||||
)}
|
||||
@@ -71,14 +91,36 @@ export const ModelSelectorPopover: Component<{
|
||||
children: JSX.Element
|
||||
}> = (props) => {
|
||||
const [open, setOpen] = createSignal(false)
|
||||
const dialog = useDialog()
|
||||
|
||||
const handleManage = () => {
|
||||
setOpen(false)
|
||||
dialog.show(() => <DialogManageModels />)
|
||||
}
|
||||
const language = useLanguage()
|
||||
|
||||
return (
|
||||
<Kobalte open={open()} onOpenChange={setOpen} placement="top-start" gutter={8}>
|
||||
<Kobalte.Trigger as="div">{props.children}</Kobalte.Trigger>
|
||||
<Kobalte.Portal>
|
||||
<Kobalte.Content class="w-72 h-80 flex flex-col rounded-md border border-border-base bg-surface-raised-stronger-non-alpha shadow-md z-50 outline-none overflow-hidden">
|
||||
<Kobalte.Title class="sr-only">Select model</Kobalte.Title>
|
||||
<ModelList provider={props.provider} onSelect={() => setOpen(false)} class="p-1" />
|
||||
<Kobalte.Title class="sr-only">{language.t("dialog.model.select.title")}</Kobalte.Title>
|
||||
<ModelList
|
||||
provider={props.provider}
|
||||
onSelect={() => setOpen(false)}
|
||||
class="p-1"
|
||||
action={
|
||||
<IconButton
|
||||
icon="sliders"
|
||||
variant="ghost"
|
||||
iconSize="normal"
|
||||
class="size-6"
|
||||
aria-label={language.t("dialog.model.manage")}
|
||||
title={language.t("dialog.model.manage")}
|
||||
onClick={handleManage}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
</Kobalte.Content>
|
||||
</Kobalte.Portal>
|
||||
</Kobalte>
|
||||
@@ -87,10 +129,11 @@ export const ModelSelectorPopover: Component<{
|
||||
|
||||
export const DialogSelectModel: Component<{ provider?: string }> = (props) => {
|
||||
const dialog = useDialog()
|
||||
const language = useLanguage()
|
||||
|
||||
return (
|
||||
<Dialog
|
||||
title="Select model"
|
||||
title={language.t("dialog.model.select.title")}
|
||||
action={
|
||||
<Button
|
||||
class="h-7 -my-1 text-14-medium"
|
||||
@@ -98,7 +141,7 @@ export const DialogSelectModel: Component<{ provider?: string }> = (props) => {
|
||||
tabIndex={-1}
|
||||
onClick={() => dialog.show(() => <DialogSelectProvider />)}
|
||||
>
|
||||
Connect provider
|
||||
{language.t("command.provider.connect")}
|
||||
</Button>
|
||||
}
|
||||
>
|
||||
@@ -108,7 +151,7 @@ export const DialogSelectModel: Component<{ provider?: string }> = (props) => {
|
||||
class="ml-3 mt-5 mb-6 text-text-base self-start"
|
||||
onClick={() => dialog.show(() => <DialogManageModels />)}
|
||||
>
|
||||
Manage models
|
||||
{language.t("dialog.model.manage")}
|
||||
</Button>
|
||||
</Dialog>
|
||||
)
|
||||
|
||||
@@ -7,28 +7,38 @@ import { Tag } from "@opencode-ai/ui/tag"
|
||||
import { ProviderIcon } from "@opencode-ai/ui/provider-icon"
|
||||
import { IconName } from "@opencode-ai/ui/icons/provider"
|
||||
import { DialogConnectProvider } from "./dialog-connect-provider"
|
||||
import { useLanguage } from "@/context/language"
|
||||
|
||||
export const DialogSelectProvider: Component = () => {
|
||||
const dialog = useDialog()
|
||||
const providers = useProviders()
|
||||
const language = useLanguage()
|
||||
|
||||
const popularGroup = () => language.t("dialog.provider.group.popular")
|
||||
const otherGroup = () => language.t("dialog.provider.group.other")
|
||||
|
||||
return (
|
||||
<Dialog title="Connect provider">
|
||||
<Dialog title={language.t("command.provider.connect")}>
|
||||
<List
|
||||
search={{ placeholder: "Search providers", autofocus: true }}
|
||||
search={{ placeholder: language.t("dialog.provider.search.placeholder"), autofocus: true }}
|
||||
emptyMessage={language.t("dialog.provider.empty")}
|
||||
activeIcon="plus-small"
|
||||
key={(x) => x?.id}
|
||||
items={providers.all}
|
||||
items={() => {
|
||||
language.locale()
|
||||
return providers.all()
|
||||
}}
|
||||
filterKeys={["id", "name"]}
|
||||
groupBy={(x) => (popularProviders.includes(x.id) ? "Popular" : "Other")}
|
||||
groupBy={(x) => (popularProviders.includes(x.id) ? popularGroup() : otherGroup())}
|
||||
sortBy={(a, b) => {
|
||||
if (popularProviders.includes(a.id) && popularProviders.includes(b.id))
|
||||
return popularProviders.indexOf(a.id) - popularProviders.indexOf(b.id)
|
||||
return a.name.localeCompare(b.name)
|
||||
}}
|
||||
sortGroupsBy={(a, b) => {
|
||||
if (a.category === "Popular" && b.category !== "Popular") return -1
|
||||
if (b.category === "Popular" && a.category !== "Popular") return 1
|
||||
const popular = popularGroup()
|
||||
if (a.category === popular && b.category !== popular) return -1
|
||||
if (b.category === popular && a.category !== popular) return 1
|
||||
return 0
|
||||
}}
|
||||
onSelect={(x) => {
|
||||
@@ -41,10 +51,10 @@ export const DialogSelectProvider: Component = () => {
|
||||
<ProviderIcon data-slot="list-item-extra-icon" id={i.id as IconName} />
|
||||
<span>{i.name}</span>
|
||||
<Show when={i.id === "opencode"}>
|
||||
<Tag>Recommended</Tag>
|
||||
<Tag>{language.t("dialog.provider.tag.recommended")}</Tag>
|
||||
</Show>
|
||||
<Show when={i.id === "anthropic"}>
|
||||
<div class="text-14-regular text-text-weak">Connect with Claude Pro/Max or API key</div>
|
||||
<div class="text-14-regular text-text-weak">{language.t("dialog.provider.anthropic.note")}</div>
|
||||
</Show>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -10,6 +10,7 @@ import { normalizeServerUrl, serverDisplayName, useServer } from "@/context/serv
|
||||
import { usePlatform } from "@/context/platform"
|
||||
import { createOpencodeClient } from "@opencode-ai/sdk/v2/client"
|
||||
import { useNavigate } from "@solidjs/router"
|
||||
import { useLanguage } from "@/context/language"
|
||||
|
||||
type ServerStatus = { healthy: boolean; version?: string }
|
||||
|
||||
@@ -30,6 +31,7 @@ export function DialogSelectServer() {
|
||||
const dialog = useDialog()
|
||||
const server = useServer()
|
||||
const platform = usePlatform()
|
||||
const language = useLanguage()
|
||||
const [store, setStore] = createStore({
|
||||
url: "",
|
||||
adding: false,
|
||||
@@ -109,7 +111,7 @@ export function DialogSelectServer() {
|
||||
setStore("adding", false)
|
||||
|
||||
if (!result.healthy) {
|
||||
setStore("error", "Could not connect to server")
|
||||
setStore("error", language.t("dialog.server.add.error"))
|
||||
return
|
||||
}
|
||||
|
||||
@@ -122,11 +124,11 @@ export function DialogSelectServer() {
|
||||
}
|
||||
|
||||
return (
|
||||
<Dialog title="Servers" description="Switch which OpenCode server this app connects to.">
|
||||
<Dialog title={language.t("dialog.server.title")} description={language.t("dialog.server.description")}>
|
||||
<div class="flex flex-col gap-4 pb-4">
|
||||
<List
|
||||
search={{ placeholder: "Search servers", autofocus: true }}
|
||||
emptyMessage="No servers yet"
|
||||
search={{ placeholder: language.t("dialog.server.search.placeholder"), autofocus: true }}
|
||||
emptyMessage={language.t("dialog.server.empty")}
|
||||
items={sortedItems}
|
||||
key={(x) => x}
|
||||
current={current()}
|
||||
@@ -168,16 +170,16 @@ export function DialogSelectServer() {
|
||||
|
||||
<div class="mt-6 px-3 flex flex-col gap-1.5">
|
||||
<div class="px-3">
|
||||
<h3 class="text-14-regular text-text-weak">Add a server</h3>
|
||||
<h3 class="text-14-regular text-text-weak">{language.t("dialog.server.add.title")}</h3>
|
||||
</div>
|
||||
<form onSubmit={handleSubmit}>
|
||||
<div class="flex items-start gap-2">
|
||||
<div class="flex-1 min-w-0 h-auto">
|
||||
<TextField
|
||||
type="text"
|
||||
label="Server URL"
|
||||
label={language.t("dialog.server.add.url")}
|
||||
hideLabel
|
||||
placeholder="http://localhost:4096"
|
||||
placeholder={language.t("dialog.server.add.placeholder")}
|
||||
value={store.url}
|
||||
onChange={(v) => {
|
||||
setStore("url", v)
|
||||
@@ -188,7 +190,7 @@ export function DialogSelectServer() {
|
||||
/>
|
||||
</div>
|
||||
<Button type="submit" variant="secondary" icon="plus-small" size="large" disabled={store.adding}>
|
||||
{store.adding ? "Checking..." : "Add"}
|
||||
{store.adding ? language.t("dialog.server.add.checking") : language.t("dialog.server.add.button")}
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
@@ -197,10 +199,8 @@ export function DialogSelectServer() {
|
||||
<Show when={isDesktop}>
|
||||
<div class="mt-6 px-3 flex flex-col gap-1.5">
|
||||
<div class="px-3">
|
||||
<h3 class="text-14-regular text-text-weak">Default server</h3>
|
||||
<p class="text-12-regular text-text-weak mt-1">
|
||||
Connect to this server on app launch instead of starting a local server. Requires restart.
|
||||
</p>
|
||||
<h3 class="text-14-regular text-text-weak">{language.t("dialog.server.default.title")}</h3>
|
||||
<p class="text-12-regular text-text-weak mt-1">{language.t("dialog.server.default.description")}</p>
|
||||
</div>
|
||||
<div class="flex items-center gap-2 px-3 py-2">
|
||||
<Show
|
||||
@@ -208,7 +208,9 @@ export function DialogSelectServer() {
|
||||
fallback={
|
||||
<Show
|
||||
when={server.url}
|
||||
fallback={<span class="text-14-regular text-text-weak">No server selected</span>}
|
||||
fallback={
|
||||
<span class="text-14-regular text-text-weak">{language.t("dialog.server.default.none")}</span>
|
||||
}
|
||||
>
|
||||
<Button
|
||||
variant="secondary"
|
||||
@@ -218,7 +220,7 @@ export function DialogSelectServer() {
|
||||
defaultUrlActions.refetch(server.url)
|
||||
}}
|
||||
>
|
||||
Set current server as default
|
||||
{language.t("dialog.server.default.set")}
|
||||
</Button>
|
||||
</Show>
|
||||
}
|
||||
@@ -234,7 +236,7 @@ export function DialogSelectServer() {
|
||||
defaultUrlActions.refetch()
|
||||
}}
|
||||
>
|
||||
Clear
|
||||
{language.t("dialog.server.default.clear")}
|
||||
</Button>
|
||||
</Show>
|
||||
</div>
|
||||
|
||||
97
packages/app/src/components/dialog-settings.tsx
Normal file
97
packages/app/src/components/dialog-settings.tsx
Normal file
@@ -0,0 +1,97 @@
|
||||
import { Component } from "solid-js"
|
||||
import { Dialog } from "@opencode-ai/ui/dialog"
|
||||
import { Tabs } from "@opencode-ai/ui/tabs"
|
||||
import { Icon } from "@opencode-ai/ui/icon"
|
||||
import { useLanguage } from "@/context/language"
|
||||
import { SettingsGeneral } from "./settings-general"
|
||||
import { SettingsKeybinds } from "./settings-keybinds"
|
||||
import { SettingsPermissions } from "./settings-permissions"
|
||||
import { SettingsProviders } from "./settings-providers"
|
||||
import { SettingsModels } from "./settings-models"
|
||||
import { SettingsAgents } from "./settings-agents"
|
||||
import { SettingsCommands } from "./settings-commands"
|
||||
import { SettingsMcp } from "./settings-mcp"
|
||||
|
||||
export const DialogSettings: Component = () => {
|
||||
const language = useLanguage()
|
||||
|
||||
return (
|
||||
<Dialog size="x-large">
|
||||
<Tabs orientation="vertical" variant="settings" defaultValue="general" class="h-full settings-dialog">
|
||||
<Tabs.List>
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
"flex-direction": "column",
|
||||
gap: "12px",
|
||||
width: "100%",
|
||||
"padding-top": "12px",
|
||||
"padding-bottom": "12px",
|
||||
}}
|
||||
>
|
||||
<Tabs.SectionTitle>{language.t("settings.section.desktop")}</Tabs.SectionTitle>
|
||||
<div style={{ display: "flex", "flex-direction": "column", gap: "6px", width: "100%" }}>
|
||||
<Tabs.Trigger value="general">
|
||||
<Icon name="sliders" />
|
||||
{language.t("settings.tab.general")}
|
||||
</Tabs.Trigger>
|
||||
<Tabs.Trigger value="shortcuts">
|
||||
<Icon name="keyboard" />
|
||||
{language.t("settings.tab.shortcuts")}
|
||||
</Tabs.Trigger>
|
||||
</div>
|
||||
</div>
|
||||
{/* <Tabs.SectionTitle>Server</Tabs.SectionTitle> */}
|
||||
{/* <Tabs.Trigger value="permissions"> */}
|
||||
{/* <Icon name="checklist" /> */}
|
||||
{/* Permissions */}
|
||||
{/* </Tabs.Trigger> */}
|
||||
{/* <Tabs.Trigger value="providers"> */}
|
||||
{/* <Icon name="server" /> */}
|
||||
{/* Providers */}
|
||||
{/* </Tabs.Trigger> */}
|
||||
{/* <Tabs.Trigger value="models"> */}
|
||||
{/* <Icon name="brain" /> */}
|
||||
{/* Models */}
|
||||
{/* </Tabs.Trigger> */}
|
||||
{/* <Tabs.Trigger value="agents"> */}
|
||||
{/* <Icon name="task" /> */}
|
||||
{/* Agents */}
|
||||
{/* </Tabs.Trigger> */}
|
||||
{/* <Tabs.Trigger value="commands"> */}
|
||||
{/* <Icon name="console" /> */}
|
||||
{/* Commands */}
|
||||
{/* </Tabs.Trigger> */}
|
||||
{/* <Tabs.Trigger value="mcp"> */}
|
||||
{/* <Icon name="mcp" /> */}
|
||||
{/* MCP */}
|
||||
{/* </Tabs.Trigger> */}
|
||||
</Tabs.List>
|
||||
<Tabs.Content value="general" class="no-scrollbar">
|
||||
<SettingsGeneral />
|
||||
</Tabs.Content>
|
||||
<Tabs.Content value="shortcuts" class="no-scrollbar">
|
||||
<SettingsKeybinds />
|
||||
</Tabs.Content>
|
||||
{/* <Tabs.Content value="permissions" class="no-scrollbar"> */}
|
||||
{/* <SettingsPermissions /> */}
|
||||
{/* </Tabs.Content> */}
|
||||
{/* <Tabs.Content value="providers" class="no-scrollbar"> */}
|
||||
{/* <SettingsProviders /> */}
|
||||
{/* </Tabs.Content> */}
|
||||
{/* <Tabs.Content value="models" class="no-scrollbar"> */}
|
||||
{/* <SettingsModels /> */}
|
||||
{/* </Tabs.Content> */}
|
||||
{/* <Tabs.Content value="agents" class="no-scrollbar"> */}
|
||||
{/* <SettingsAgents /> */}
|
||||
{/* </Tabs.Content> */}
|
||||
{/* <Tabs.Content value="commands" class="no-scrollbar"> */}
|
||||
{/* <SettingsCommands /> */}
|
||||
{/* </Tabs.Content> */}
|
||||
{/* <Tabs.Content value="mcp" class="no-scrollbar"> */}
|
||||
{/* <SettingsMcp /> */}
|
||||
{/* </Tabs.Content> */}
|
||||
</Tabs>
|
||||
</Dialog>
|
||||
)
|
||||
}
|
||||
91
packages/app/src/components/model-tooltip.tsx
Normal file
91
packages/app/src/components/model-tooltip.tsx
Normal file
@@ -0,0 +1,91 @@
|
||||
import { Show, type Component } from "solid-js"
|
||||
import { useLanguage } from "@/context/language"
|
||||
|
||||
type InputKey = "text" | "image" | "audio" | "video" | "pdf"
|
||||
type InputMap = Record<InputKey, boolean>
|
||||
|
||||
type ModelInfo = {
|
||||
id: string
|
||||
name: string
|
||||
provider: {
|
||||
name: string
|
||||
}
|
||||
capabilities?: {
|
||||
reasoning: boolean
|
||||
input: InputMap
|
||||
}
|
||||
modalities?: {
|
||||
input: Array<string>
|
||||
}
|
||||
reasoning?: boolean
|
||||
limit: {
|
||||
context: number
|
||||
}
|
||||
}
|
||||
|
||||
export const ModelTooltip: Component<{ model: ModelInfo; latest?: boolean; free?: boolean }> = (props) => {
|
||||
const language = useLanguage()
|
||||
const sourceName = (model: ModelInfo) => {
|
||||
const value = `${model.id} ${model.name}`.toLowerCase()
|
||||
|
||||
if (/claude|anthropic/.test(value)) return language.t("model.provider.anthropic")
|
||||
if (/gpt|o[1-4]|codex|openai/.test(value)) return language.t("model.provider.openai")
|
||||
if (/gemini|palm|bard|google/.test(value)) return language.t("model.provider.google")
|
||||
if (/grok|xai/.test(value)) return language.t("model.provider.xai")
|
||||
if (/llama|meta/.test(value)) return language.t("model.provider.meta")
|
||||
|
||||
return model.provider.name
|
||||
}
|
||||
const inputLabel = (value: string) => {
|
||||
if (value === "text") return language.t("model.input.text")
|
||||
if (value === "image") return language.t("model.input.image")
|
||||
if (value === "audio") return language.t("model.input.audio")
|
||||
if (value === "video") return language.t("model.input.video")
|
||||
if (value === "pdf") return language.t("model.input.pdf")
|
||||
return value
|
||||
}
|
||||
const title = () => {
|
||||
const tags: Array<string> = []
|
||||
if (props.latest) tags.push(language.t("model.tag.latest"))
|
||||
if (props.free) tags.push(language.t("model.tag.free"))
|
||||
const suffix = tags.length ? ` (${tags.join(", ")})` : ""
|
||||
return `${sourceName(props.model)} ${props.model.name}${suffix}`
|
||||
}
|
||||
const inputs = () => {
|
||||
if (props.model.capabilities) {
|
||||
const input = props.model.capabilities.input
|
||||
const order: Array<InputKey> = ["text", "image", "audio", "video", "pdf"]
|
||||
const entries = order.filter((key) => input[key]).map((key) => inputLabel(key))
|
||||
return entries.length ? entries.join(", ") : undefined
|
||||
}
|
||||
const raw = props.model.modalities?.input
|
||||
if (!raw) return
|
||||
const entries = raw.map((value) => inputLabel(value))
|
||||
return entries.length ? entries.join(", ") : undefined
|
||||
}
|
||||
const reasoning = () => {
|
||||
if (props.model.capabilities)
|
||||
return props.model.capabilities.reasoning
|
||||
? language.t("model.tooltip.reasoning.allowed")
|
||||
: language.t("model.tooltip.reasoning.none")
|
||||
return props.model.reasoning
|
||||
? language.t("model.tooltip.reasoning.allowed")
|
||||
: language.t("model.tooltip.reasoning.none")
|
||||
}
|
||||
const context = () => language.t("model.tooltip.context", { limit: props.model.limit.context.toLocaleString() })
|
||||
|
||||
return (
|
||||
<div class="flex flex-col gap-1 py-1">
|
||||
<div class="text-13-medium">{title()}</div>
|
||||
<Show when={inputs()}>
|
||||
{(value) => (
|
||||
<div class="text-12-regular text-text-invert-base">
|
||||
{language.t("model.tooltip.allows", { inputs: value() })}
|
||||
</div>
|
||||
)}
|
||||
</Show>
|
||||
<div class="text-12-regular text-text-invert-base">{reasoning()}</div>
|
||||
<div class="text-12-regular text-text-invert-base">{context()}</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -49,6 +49,7 @@ import { Persist, persisted } from "@/utils/persist"
|
||||
import { Identifier } from "@/utils/id"
|
||||
import { SessionContextUsage } from "@/components/session-context-usage"
|
||||
import { usePermission } from "@/context/permission"
|
||||
import { useLanguage } from "@/context/language"
|
||||
import { useGlobalSync } from "@/context/global-sync"
|
||||
import { usePlatform } from "@/context/platform"
|
||||
import { createOpencodeClient, type Message, type Part } from "@opencode-ai/sdk/v2/client"
|
||||
@@ -66,33 +67,33 @@ interface PromptInputProps {
|
||||
onNewSessionWorktreeReset?: () => void
|
||||
}
|
||||
|
||||
const PLACEHOLDERS = [
|
||||
"Fix a TODO in the codebase",
|
||||
"What is the tech stack of this project?",
|
||||
"Fix broken tests",
|
||||
"Explain how authentication works",
|
||||
"Find and fix security vulnerabilities",
|
||||
"Add unit tests for the user service",
|
||||
"Refactor this function to be more readable",
|
||||
"What does this error mean?",
|
||||
"Help me debug this issue",
|
||||
"Generate API documentation",
|
||||
"Optimize database queries",
|
||||
"Add input validation",
|
||||
"Create a new component for...",
|
||||
"How do I deploy this project?",
|
||||
"Review my code for best practices",
|
||||
"Add error handling to this function",
|
||||
"Explain this regex pattern",
|
||||
"Convert this to TypeScript",
|
||||
"Add logging throughout the codebase",
|
||||
"What dependencies are outdated?",
|
||||
"Help me write a migration script",
|
||||
"Implement caching for this endpoint",
|
||||
"Add pagination to this list",
|
||||
"Create a CLI command for...",
|
||||
"How do environment variables work here?",
|
||||
]
|
||||
const EXAMPLES = [
|
||||
"prompt.example.1",
|
||||
"prompt.example.2",
|
||||
"prompt.example.3",
|
||||
"prompt.example.4",
|
||||
"prompt.example.5",
|
||||
"prompt.example.6",
|
||||
"prompt.example.7",
|
||||
"prompt.example.8",
|
||||
"prompt.example.9",
|
||||
"prompt.example.10",
|
||||
"prompt.example.11",
|
||||
"prompt.example.12",
|
||||
"prompt.example.13",
|
||||
"prompt.example.14",
|
||||
"prompt.example.15",
|
||||
"prompt.example.16",
|
||||
"prompt.example.17",
|
||||
"prompt.example.18",
|
||||
"prompt.example.19",
|
||||
"prompt.example.20",
|
||||
"prompt.example.21",
|
||||
"prompt.example.22",
|
||||
"prompt.example.23",
|
||||
"prompt.example.24",
|
||||
"prompt.example.25",
|
||||
] as const
|
||||
|
||||
interface SlashCommand {
|
||||
id: string
|
||||
@@ -118,6 +119,7 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
|
||||
const providers = useProviders()
|
||||
const command = useCommand()
|
||||
const permission = usePermission()
|
||||
const language = useLanguage()
|
||||
let editorRef!: HTMLDivElement
|
||||
let fileInputRef!: HTMLInputElement
|
||||
let scrollRef!: HTMLDivElement
|
||||
@@ -184,7 +186,7 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
|
||||
popover: null,
|
||||
historyIndex: -1,
|
||||
savedPrompt: null,
|
||||
placeholder: Math.floor(Math.random() * PLACEHOLDERS.length),
|
||||
placeholder: Math.floor(Math.random() * EXAMPLES.length),
|
||||
dragging: false,
|
||||
mode: "normal",
|
||||
applyingHistory: false,
|
||||
@@ -255,10 +257,9 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
|
||||
|
||||
createEffect(() => {
|
||||
params.id
|
||||
editorRef.focus()
|
||||
if (params.id) return
|
||||
const interval = setInterval(() => {
|
||||
setStore("placeholder", (prev) => (prev + 1) % PLACEHOLDERS.length)
|
||||
setStore("placeholder", (prev) => (prev + 1) % EXAMPLES.length)
|
||||
}, 6500)
|
||||
onCleanup(() => clearInterval(interval))
|
||||
})
|
||||
@@ -300,7 +301,8 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
|
||||
event.stopPropagation()
|
||||
|
||||
const items = Array.from(clipboardData.items)
|
||||
const imageItems = items.filter((item) => ACCEPTED_FILE_TYPES.includes(item.type))
|
||||
const fileItems = items.filter((item) => item.kind === "file")
|
||||
const imageItems = fileItems.filter((item) => ACCEPTED_FILE_TYPES.includes(item.type))
|
||||
|
||||
if (imageItems.length > 0) {
|
||||
for (const item of imageItems) {
|
||||
@@ -310,7 +312,16 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
|
||||
return
|
||||
}
|
||||
|
||||
if (fileItems.length > 0) {
|
||||
showToast({
|
||||
title: language.t("prompt.toast.pasteUnsupported.title"),
|
||||
description: language.t("prompt.toast.pasteUnsupported.description"),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
const plainText = clipboardData.getData("text/plain") ?? ""
|
||||
if (!plainText) return
|
||||
addPart({ type: "text", content: plainText, start: 0, end: 0 })
|
||||
}
|
||||
|
||||
@@ -539,6 +550,25 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
|
||||
})
|
||||
})
|
||||
|
||||
const selectPopoverActive = () => {
|
||||
if (store.popover === "at") {
|
||||
const items = atFlat()
|
||||
if (items.length === 0) return
|
||||
const active = atActive()
|
||||
const item = items.find((entry) => atKey(entry) === active) ?? items[0]
|
||||
handleAtSelect(item)
|
||||
return
|
||||
}
|
||||
|
||||
if (store.popover === "slash") {
|
||||
const items = slashFlat()
|
||||
if (items.length === 0) return
|
||||
const active = slashActive()
|
||||
const item = items.find((entry) => entry.id === active) ?? items[0]
|
||||
handleSlashSelect(item)
|
||||
}
|
||||
}
|
||||
|
||||
createEffect(
|
||||
on(
|
||||
() => prompt.current(),
|
||||
@@ -899,14 +929,24 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
|
||||
return
|
||||
}
|
||||
|
||||
if (store.popover && (event.key === "ArrowUp" || event.key === "ArrowDown" || event.key === "Enter")) {
|
||||
if (store.popover === "at") {
|
||||
atOnKeyDown(event)
|
||||
} else {
|
||||
slashOnKeyDown(event)
|
||||
if (store.popover) {
|
||||
if (event.key === "Tab") {
|
||||
selectPopoverActive()
|
||||
event.preventDefault()
|
||||
return
|
||||
}
|
||||
if (event.key === "ArrowUp" || event.key === "ArrowDown" || event.key === "Enter") {
|
||||
if (store.popover === "at") {
|
||||
atOnKeyDown(event)
|
||||
event.preventDefault()
|
||||
return
|
||||
}
|
||||
if (store.popover === "slash") {
|
||||
slashOnKeyDown(event)
|
||||
}
|
||||
event.preventDefault()
|
||||
return
|
||||
}
|
||||
event.preventDefault()
|
||||
return
|
||||
}
|
||||
|
||||
const ctrl = event.ctrlKey && !event.metaKey && !event.altKey && !event.shiftKey
|
||||
@@ -988,8 +1028,8 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
|
||||
const currentAgent = local.agent.current()
|
||||
if (!currentModel || !currentAgent) {
|
||||
showToast({
|
||||
title: "Select an agent and model",
|
||||
description: "Choose an agent and model before sending a prompt.",
|
||||
title: language.t("prompt.toast.modelAgentRequired.title"),
|
||||
description: language.t("prompt.toast.modelAgentRequired.description"),
|
||||
})
|
||||
return
|
||||
}
|
||||
@@ -1000,7 +1040,7 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
|
||||
if (data?.message) return data.message
|
||||
}
|
||||
if (err instanceof Error) return err.message
|
||||
return "Request failed"
|
||||
return language.t("common.requestFailed")
|
||||
}
|
||||
|
||||
addToHistory(currentPrompt, mode)
|
||||
@@ -1021,7 +1061,7 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
|
||||
.then((x) => x.data)
|
||||
.catch((err) => {
|
||||
showToast({
|
||||
title: "Failed to create worktree",
|
||||
title: language.t("prompt.toast.worktreeCreateFailed.title"),
|
||||
description: errorMessage(err),
|
||||
})
|
||||
return undefined
|
||||
@@ -1029,8 +1069,8 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
|
||||
|
||||
if (!createdWorktree?.directory) {
|
||||
showToast({
|
||||
title: "Failed to create worktree",
|
||||
description: "Request failed",
|
||||
title: language.t("prompt.toast.worktreeCreateFailed.title"),
|
||||
description: language.t("common.requestFailed"),
|
||||
})
|
||||
return
|
||||
}
|
||||
@@ -1056,7 +1096,16 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
|
||||
|
||||
let session = info()
|
||||
if (!session && isNewSession) {
|
||||
session = await client.session.create().then((x) => x.data ?? undefined)
|
||||
session = await client.session
|
||||
.create()
|
||||
.then((x) => x.data ?? undefined)
|
||||
.catch((err) => {
|
||||
showToast({
|
||||
title: language.t("prompt.toast.sessionCreateFailed.title"),
|
||||
description: errorMessage(err),
|
||||
})
|
||||
return undefined
|
||||
})
|
||||
if (session) navigate(`/${base64Encode(sessionDirectory)}/session/${session.id}`)
|
||||
}
|
||||
if (!session) return
|
||||
@@ -1096,7 +1145,7 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
|
||||
})
|
||||
.catch((err) => {
|
||||
showToast({
|
||||
title: "Failed to send shell command",
|
||||
title: language.t("prompt.toast.shellSendFailed.title"),
|
||||
description: errorMessage(err),
|
||||
})
|
||||
restoreInput()
|
||||
@@ -1128,7 +1177,7 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
|
||||
})
|
||||
.catch((err) => {
|
||||
showToast({
|
||||
title: "Failed to send command",
|
||||
title: language.t("prompt.toast.commandSendFailed.title"),
|
||||
description: errorMessage(err),
|
||||
})
|
||||
restoreInput()
|
||||
@@ -1296,7 +1345,7 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
|
||||
})
|
||||
.catch((err) => {
|
||||
showToast({
|
||||
title: "Failed to send prompt",
|
||||
title: language.t("prompt.toast.promptSendFailed.title"),
|
||||
description: errorMessage(err),
|
||||
})
|
||||
removeOptimisticMessage()
|
||||
@@ -1320,7 +1369,7 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
|
||||
<Match when={store.popover === "at"}>
|
||||
<Show
|
||||
when={atFlat().length > 0}
|
||||
fallback={<div class="text-text-weak px-2 py-1">No matching results</div>}
|
||||
fallback={<div class="text-text-weak px-2 py-1">{language.t("prompt.popover.emptyResults")}</div>}
|
||||
>
|
||||
<For each={atFlat().slice(0, 10)}>
|
||||
{(item) => (
|
||||
@@ -1366,7 +1415,7 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
|
||||
<Match when={store.popover === "slash"}>
|
||||
<Show
|
||||
when={slashFlat().length > 0}
|
||||
fallback={<div class="text-text-weak px-2 py-1">No matching commands</div>}
|
||||
fallback={<div class="text-text-weak px-2 py-1">{language.t("prompt.popover.emptyCommands")}</div>}
|
||||
>
|
||||
<For each={slashFlat()}>
|
||||
{(cmd) => (
|
||||
@@ -1388,7 +1437,7 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
|
||||
<div class="flex items-center gap-2 shrink-0">
|
||||
<Show when={cmd.type === "custom"}>
|
||||
<span class="text-11-regular text-text-subtle px-1.5 py-0.5 bg-surface-base rounded">
|
||||
custom
|
||||
{language.t("prompt.slash.badge.custom")}
|
||||
</span>
|
||||
</Show>
|
||||
<Show when={command.keybind(cmd.id)}>
|
||||
@@ -1417,7 +1466,7 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
|
||||
<div class="absolute inset-0 z-10 flex items-center justify-center bg-surface-raised-stronger-non-alpha/90 pointer-events-none">
|
||||
<div class="flex flex-col items-center gap-2 text-text-weak">
|
||||
<Icon name="photo" class="size-8" />
|
||||
<span class="text-14-regular">Drop images or PDFs here</span>
|
||||
<span class="text-14-regular">{language.t("prompt.dropzone.label")}</span>
|
||||
</div>
|
||||
</div>
|
||||
</Show>
|
||||
@@ -1430,7 +1479,7 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
|
||||
<div class="flex items-center text-12-regular min-w-0">
|
||||
<span class="text-text-weak whitespace-nowrap truncate min-w-0">{getDirectory(path())}</span>
|
||||
<span class="text-text-strong whitespace-nowrap">{getFilename(path())}</span>
|
||||
<span class="text-text-weak whitespace-nowrap ml-1">active</span>
|
||||
<span class="text-text-weak whitespace-nowrap ml-1">{language.t("prompt.context.active")}</span>
|
||||
</div>
|
||||
<IconButton
|
||||
type="button"
|
||||
@@ -1449,7 +1498,7 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
|
||||
onClick={() => prompt.context.addActive()}
|
||||
>
|
||||
<Icon name="plus-small" size="small" />
|
||||
<span>Include active file</span>
|
||||
<span>{language.t("prompt.context.includeActiveFile")}</span>
|
||||
</button>
|
||||
</Show>
|
||||
<For each={prompt.context.items()}>
|
||||
@@ -1542,8 +1591,8 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
|
||||
<Show when={!prompt.dirty()}>
|
||||
<div class="absolute top-0 inset-x-0 px-5 py-3 pr-12 text-14-regular text-text-weak pointer-events-none whitespace-nowrap truncate">
|
||||
{store.mode === "shell"
|
||||
? "Enter shell command..."
|
||||
: `Ask anything... "${PLACEHOLDERS[store.placeholder]}"`}
|
||||
? language.t("prompt.placeholder.shell")
|
||||
: language.t("prompt.placeholder.normal", { example: language.t(EXAMPLES[store.placeholder]) })}
|
||||
</div>
|
||||
</Show>
|
||||
</div>
|
||||
@@ -1553,12 +1602,16 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
|
||||
<Match when={store.mode === "shell"}>
|
||||
<div class="flex items-center gap-2 px-2 h-6">
|
||||
<Icon name="console" size="small" class="text-icon-primary" />
|
||||
<span class="text-12-regular text-text-primary">Shell</span>
|
||||
<span class="text-12-regular text-text-weak">esc to exit</span>
|
||||
<span class="text-12-regular text-text-primary">{language.t("prompt.mode.shell")}</span>
|
||||
<span class="text-12-regular text-text-weak">{language.t("prompt.mode.shell.exit")}</span>
|
||||
</div>
|
||||
</Match>
|
||||
<Match when={store.mode === "normal"}>
|
||||
<TooltipKeybind placement="top" title="Cycle agent" keybind={command.keybind("agent.cycle")}>
|
||||
<TooltipKeybind
|
||||
placement="top"
|
||||
title={language.t("command.agent.cycle")}
|
||||
keybind={command.keybind("agent.cycle")}
|
||||
>
|
||||
<Select
|
||||
options={local.agent.list().map((agent) => agent.name)}
|
||||
current={local.agent.current()?.name ?? ""}
|
||||
@@ -1570,24 +1623,32 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
|
||||
<Show
|
||||
when={providers.paid().length > 0}
|
||||
fallback={
|
||||
<TooltipKeybind placement="top" title="Choose model" keybind={command.keybind("model.choose")}>
|
||||
<TooltipKeybind
|
||||
placement="top"
|
||||
title={language.t("command.model.choose")}
|
||||
keybind={command.keybind("model.choose")}
|
||||
>
|
||||
<Button as="div" variant="ghost" onClick={() => dialog.show(() => <DialogSelectModelUnpaid />)}>
|
||||
<Show when={local.model.current()?.provider?.id}>
|
||||
<ProviderIcon id={local.model.current()!.provider.id as IconName} class="size-4 shrink-0" />
|
||||
</Show>
|
||||
{local.model.current()?.name ?? "Select model"}
|
||||
{local.model.current()?.name ?? language.t("dialog.model.select.title")}
|
||||
<Icon name="chevron-down" size="small" />
|
||||
</Button>
|
||||
</TooltipKeybind>
|
||||
}
|
||||
>
|
||||
<ModelSelectorPopover>
|
||||
<TooltipKeybind placement="top" title="Choose model" keybind={command.keybind("model.choose")}>
|
||||
<TooltipKeybind
|
||||
placement="top"
|
||||
title={language.t("command.model.choose")}
|
||||
keybind={command.keybind("model.choose")}
|
||||
>
|
||||
<Button as="div" variant="ghost">
|
||||
<Show when={local.model.current()?.provider?.id}>
|
||||
<ProviderIcon id={local.model.current()!.provider.id as IconName} class="size-4 shrink-0" />
|
||||
</Show>
|
||||
{local.model.current()?.name ?? "Select model"}
|
||||
{local.model.current()?.name ?? language.t("dialog.model.select.title")}
|
||||
<Icon name="chevron-down" size="small" />
|
||||
</Button>
|
||||
</TooltipKeybind>
|
||||
@@ -1596,7 +1657,7 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
|
||||
<Show when={local.model.variant.list().length > 0}>
|
||||
<TooltipKeybind
|
||||
placement="top"
|
||||
title="Thinking effort"
|
||||
title={language.t("command.model.variant.cycle")}
|
||||
keybind={command.keybind("model.variant.cycle")}
|
||||
>
|
||||
<Button
|
||||
@@ -1604,14 +1665,14 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
|
||||
class="text-text-base _hidden group-hover/prompt-input:inline-block capitalize text-12-regular"
|
||||
onClick={() => local.model.variant.cycle()}
|
||||
>
|
||||
{local.model.variant.current() ?? "Default"}
|
||||
{local.model.variant.current() ?? language.t("common.default")}
|
||||
</Button>
|
||||
</TooltipKeybind>
|
||||
</Show>
|
||||
<Show when={permission.permissionsEnabled() && params.id}>
|
||||
<TooltipKeybind
|
||||
placement="top"
|
||||
title="Auto-accept edits"
|
||||
title={language.t("command.permissions.autoaccept.enable")}
|
||||
keybind={command.keybind("permissions.autoaccept")}
|
||||
>
|
||||
<Button
|
||||
@@ -1649,7 +1710,7 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
|
||||
<div class="flex items-center gap-2">
|
||||
<SessionContextUsage />
|
||||
<Show when={store.mode === "normal"}>
|
||||
<Tooltip placement="top" value="Attach file">
|
||||
<Tooltip placement="top" value={language.t("prompt.action.attachFile")}>
|
||||
<Button type="button" variant="ghost" class="size-6" onClick={() => fileInputRef.click()}>
|
||||
<Icon name="photo" class="size-4.5" />
|
||||
</Button>
|
||||
@@ -1663,13 +1724,13 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
|
||||
<Switch>
|
||||
<Match when={working()}>
|
||||
<div class="flex items-center gap-2">
|
||||
<span>Stop</span>
|
||||
<span class="text-icon-base text-12-medium text-[10px]!">ESC</span>
|
||||
<span>{language.t("prompt.action.stop")}</span>
|
||||
<span class="text-icon-base text-12-medium text-[10px]!">{language.t("common.key.esc")}</span>
|
||||
</div>
|
||||
</Match>
|
||||
<Match when={true}>
|
||||
<div class="flex items-center gap-2">
|
||||
<span>Send</span>
|
||||
<span>{language.t("prompt.action.send")}</span>
|
||||
<Icon name="enter" size="small" class="text-icon-base" />
|
||||
</div>
|
||||
</Match>
|
||||
|
||||
@@ -7,6 +7,7 @@ import { AssistantMessage } from "@opencode-ai/sdk/v2/client"
|
||||
|
||||
import { useLayout } from "@/context/layout"
|
||||
import { useSync } from "@/context/sync"
|
||||
import { useLanguage } from "@/context/language"
|
||||
|
||||
interface SessionContextUsageProps {
|
||||
variant?: "button" | "indicator"
|
||||
@@ -16,6 +17,7 @@ export function SessionContextUsage(props: SessionContextUsageProps) {
|
||||
const sync = useSync()
|
||||
const params = useParams()
|
||||
const layout = useLayout()
|
||||
const language = useLanguage()
|
||||
|
||||
const variant = createMemo(() => props.variant ?? "button")
|
||||
const sessionKey = createMemo(() => `${params.dir}${params.id ? "/" + params.id : ""}`)
|
||||
@@ -24,14 +26,16 @@ export function SessionContextUsage(props: SessionContextUsageProps) {
|
||||
const messages = createMemo(() => (params.id ? (sync.data.message[params.id] ?? []) : []))
|
||||
|
||||
const cost = createMemo(() => {
|
||||
const locale = language.locale()
|
||||
const total = messages().reduce((sum, x) => sum + (x.role === "assistant" ? x.cost : 0), 0)
|
||||
return new Intl.NumberFormat("en-US", {
|
||||
return new Intl.NumberFormat(locale, {
|
||||
style: "currency",
|
||||
currency: "USD",
|
||||
}).format(total)
|
||||
})
|
||||
|
||||
const context = createMemo(() => {
|
||||
const locale = language.locale()
|
||||
const last = messages().findLast((x) => {
|
||||
if (x.role !== "assistant") return false
|
||||
const total = x.tokens.input + x.tokens.output + x.tokens.reasoning + x.tokens.cache.read + x.tokens.cache.write
|
||||
@@ -42,7 +46,7 @@ export function SessionContextUsage(props: SessionContextUsageProps) {
|
||||
last.tokens.input + last.tokens.output + last.tokens.reasoning + last.tokens.cache.read + last.tokens.cache.write
|
||||
const model = sync.data.provider.all.find((x) => x.id === last.providerID)?.models[last.modelID]
|
||||
return {
|
||||
tokens: total.toLocaleString(),
|
||||
tokens: total.toLocaleString(locale),
|
||||
percentage: model?.limit.context ? Math.round((total / model.limit.context) * 100) : null,
|
||||
}
|
||||
})
|
||||
@@ -67,21 +71,21 @@ export function SessionContextUsage(props: SessionContextUsageProps) {
|
||||
<>
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="text-text-invert-strong">{ctx().tokens}</span>
|
||||
<span class="text-text-invert-base">Tokens</span>
|
||||
<span class="text-text-invert-base">{language.t("context.usage.tokens")}</span>
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="text-text-invert-strong">{ctx().percentage ?? 0}%</span>
|
||||
<span class="text-text-invert-base">Usage</span>
|
||||
<span class="text-text-invert-base">{language.t("context.usage.usage")}</span>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</Show>
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="text-text-invert-strong">{cost()}</span>
|
||||
<span class="text-text-invert-base">Cost</span>
|
||||
<span class="text-text-invert-base">{language.t("context.usage.cost")}</span>
|
||||
</div>
|
||||
<Show when={variant() === "button"}>
|
||||
<div class="text-11-regular text-text-invert-base mt-1">Click to view context</div>
|
||||
<div class="text-11-regular text-text-invert-base mt-1">{language.t("context.usage.clickToView")}</div>
|
||||
</Show>
|
||||
</div>
|
||||
)
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
import { createMemo, Show } from "solid-js"
|
||||
import { useSync } from "@/context/sync"
|
||||
import { useLanguage } from "@/context/language"
|
||||
import { Tooltip } from "@opencode-ai/ui/tooltip"
|
||||
|
||||
export function SessionLspIndicator() {
|
||||
const sync = useSync()
|
||||
const language = useLanguage()
|
||||
|
||||
const lspStats = createMemo(() => {
|
||||
const lsp = sync.data.lsp ?? []
|
||||
@@ -15,7 +17,7 @@ export function SessionLspIndicator() {
|
||||
|
||||
const tooltipContent = createMemo(() => {
|
||||
const lsp = sync.data.lsp ?? []
|
||||
if (lsp.length === 0) return "No LSP servers"
|
||||
if (lsp.length === 0) return language.t("lsp.tooltip.none")
|
||||
return lsp.map((s) => s.name).join(", ")
|
||||
})
|
||||
|
||||
@@ -30,7 +32,9 @@ export function SessionLspIndicator() {
|
||||
"bg-icon-success-base": !lspStats().hasError && lspStats().connected > 0,
|
||||
}}
|
||||
/>
|
||||
<span class="text-12-regular text-text-weak">{lspStats().connected} LSP</span>
|
||||
<span class="text-12-regular text-text-weak">
|
||||
{language.t("lsp.label.connected", { count: lspStats().connected })}
|
||||
</span>
|
||||
</div>
|
||||
</Tooltip>
|
||||
</Show>
|
||||
|
||||
@@ -11,6 +11,7 @@ import { StickyAccordionHeader } from "@opencode-ai/ui/sticky-accordion-header"
|
||||
import { Code } from "@opencode-ai/ui/code"
|
||||
import { Markdown } from "@opencode-ai/ui/markdown"
|
||||
import type { AssistantMessage, Message, Part, UserMessage } from "@opencode-ai/sdk/v2/client"
|
||||
import { useLanguage } from "@/context/language"
|
||||
|
||||
interface SessionContextTabProps {
|
||||
messages: () => Message[]
|
||||
@@ -22,6 +23,7 @@ interface SessionContextTabProps {
|
||||
export function SessionContextTab(props: SessionContextTabProps) {
|
||||
const params = useParams()
|
||||
const sync = useSync()
|
||||
const language = useLanguage()
|
||||
|
||||
const ctx = createMemo(() => {
|
||||
const last = props.messages().findLast((x) => {
|
||||
@@ -59,8 +61,9 @@ export function SessionContextTab(props: SessionContextTabProps) {
|
||||
})
|
||||
|
||||
const cost = createMemo(() => {
|
||||
const locale = language.locale()
|
||||
const total = props.messages().reduce((sum, x) => sum + (x.role === "assistant" ? x.cost : 0), 0)
|
||||
return new Intl.NumberFormat("en-US", {
|
||||
return new Intl.NumberFormat(locale, {
|
||||
style: "currency",
|
||||
currency: "USD",
|
||||
}).format(total)
|
||||
@@ -89,18 +92,18 @@ export function SessionContextTab(props: SessionContextTabProps) {
|
||||
const number = (value: number | null | undefined) => {
|
||||
if (value === undefined) return "—"
|
||||
if (value === null) return "—"
|
||||
return value.toLocaleString()
|
||||
return value.toLocaleString(language.locale())
|
||||
}
|
||||
|
||||
const percent = (value: number | null | undefined) => {
|
||||
if (value === undefined) return "—"
|
||||
if (value === null) return "—"
|
||||
return value.toString() + "%"
|
||||
return value.toLocaleString(language.locale()) + "%"
|
||||
}
|
||||
|
||||
const time = (value: number | undefined) => {
|
||||
if (!value) return "—"
|
||||
return DateTime.fromMillis(value).toLocaleString(DateTime.DATETIME_MED)
|
||||
return DateTime.fromMillis(value).setLocale(language.locale()).toLocaleString(DateTime.DATETIME_MED)
|
||||
}
|
||||
|
||||
const providerLabel = createMemo(() => {
|
||||
@@ -172,7 +175,7 @@ export function SessionContextTab(props: SessionContextTabProps) {
|
||||
return [
|
||||
{
|
||||
key: "system",
|
||||
label: "System",
|
||||
label: language.t("context.breakdown.system"),
|
||||
tokens: tokens.system,
|
||||
width: pct(tokens.system),
|
||||
percent: pctLabel(tokens.system),
|
||||
@@ -180,7 +183,7 @@ export function SessionContextTab(props: SessionContextTabProps) {
|
||||
},
|
||||
{
|
||||
key: "user",
|
||||
label: "User",
|
||||
label: language.t("context.breakdown.user"),
|
||||
tokens: tokens.user,
|
||||
width: pct(tokens.user),
|
||||
percent: pctLabel(tokens.user),
|
||||
@@ -188,7 +191,7 @@ export function SessionContextTab(props: SessionContextTabProps) {
|
||||
},
|
||||
{
|
||||
key: "assistant",
|
||||
label: "Assistant",
|
||||
label: language.t("context.breakdown.assistant"),
|
||||
tokens: tokens.assistant,
|
||||
width: pct(tokens.assistant),
|
||||
percent: pctLabel(tokens.assistant),
|
||||
@@ -196,7 +199,7 @@ export function SessionContextTab(props: SessionContextTabProps) {
|
||||
},
|
||||
{
|
||||
key: "tool",
|
||||
label: "Tool Calls",
|
||||
label: language.t("context.breakdown.tool"),
|
||||
tokens: tokens.tool,
|
||||
width: pct(tokens.tool),
|
||||
percent: pctLabel(tokens.tool),
|
||||
@@ -204,7 +207,7 @@ export function SessionContextTab(props: SessionContextTabProps) {
|
||||
},
|
||||
{
|
||||
key: "other",
|
||||
label: "Other",
|
||||
label: language.t("context.breakdown.other"),
|
||||
tokens: tokens.other,
|
||||
width: pct(tokens.other),
|
||||
percent: pctLabel(tokens.other),
|
||||
@@ -243,22 +246,28 @@ export function SessionContextTab(props: SessionContextTabProps) {
|
||||
const c = ctx()
|
||||
const count = counts()
|
||||
return [
|
||||
{ label: "Session", value: props.info()?.title ?? params.id ?? "—" },
|
||||
{ label: "Messages", value: count.all.toLocaleString() },
|
||||
{ label: "Provider", value: providerLabel() },
|
||||
{ label: "Model", value: modelLabel() },
|
||||
{ label: "Context Limit", value: number(c?.limit) },
|
||||
{ label: "Total Tokens", value: number(c?.total) },
|
||||
{ label: "Usage", value: percent(c?.usage) },
|
||||
{ label: "Input Tokens", value: number(c?.input) },
|
||||
{ label: "Output Tokens", value: number(c?.output) },
|
||||
{ label: "Reasoning Tokens", value: number(c?.reasoning) },
|
||||
{ label: "Cache Tokens (read/write)", value: `${number(c?.cacheRead)} / ${number(c?.cacheWrite)}` },
|
||||
{ label: "User Messages", value: count.user.toLocaleString() },
|
||||
{ label: "Assistant Messages", value: count.assistant.toLocaleString() },
|
||||
{ label: "Total Cost", value: cost() },
|
||||
{ label: "Session Created", value: time(props.info()?.time.created) },
|
||||
{ label: "Last Activity", value: time(c?.message.time.created) },
|
||||
{ label: language.t("context.stats.session"), value: props.info()?.title ?? params.id ?? "—" },
|
||||
{ label: language.t("context.stats.messages"), value: count.all.toLocaleString(language.locale()) },
|
||||
{ label: language.t("context.stats.provider"), value: providerLabel() },
|
||||
{ label: language.t("context.stats.model"), value: modelLabel() },
|
||||
{ label: language.t("context.stats.limit"), value: number(c?.limit) },
|
||||
{ label: language.t("context.stats.totalTokens"), value: number(c?.total) },
|
||||
{ label: language.t("context.stats.usage"), value: percent(c?.usage) },
|
||||
{ label: language.t("context.stats.inputTokens"), value: number(c?.input) },
|
||||
{ label: language.t("context.stats.outputTokens"), value: number(c?.output) },
|
||||
{ label: language.t("context.stats.reasoningTokens"), value: number(c?.reasoning) },
|
||||
{
|
||||
label: language.t("context.stats.cacheTokens"),
|
||||
value: `${number(c?.cacheRead)} / ${number(c?.cacheWrite)}`,
|
||||
},
|
||||
{ label: language.t("context.stats.userMessages"), value: count.user.toLocaleString(language.locale()) },
|
||||
{
|
||||
label: language.t("context.stats.assistantMessages"),
|
||||
value: count.assistant.toLocaleString(language.locale()),
|
||||
},
|
||||
{ label: language.t("context.stats.totalCost"), value: cost() },
|
||||
{ label: language.t("context.stats.sessionCreated"), value: time(props.info()?.time.created) },
|
||||
{ label: language.t("context.stats.lastActivity"), value: time(c?.message.time.created) },
|
||||
] satisfies { label: string; value: JSX.Element }[]
|
||||
})
|
||||
|
||||
@@ -371,7 +380,7 @@ export function SessionContextTab(props: SessionContextTabProps) {
|
||||
|
||||
<Show when={breakdown().length > 0}>
|
||||
<div class="flex flex-col gap-2">
|
||||
<div class="text-12-regular text-text-weak">Context Breakdown</div>
|
||||
<div class="text-12-regular text-text-weak">{language.t("context.breakdown.title")}</div>
|
||||
<div class="h-2 w-full rounded-full bg-surface-base overflow-hidden flex">
|
||||
<For each={breakdown()}>
|
||||
{(segment) => (
|
||||
@@ -396,16 +405,14 @@ export function SessionContextTab(props: SessionContextTabProps) {
|
||||
)}
|
||||
</For>
|
||||
</div>
|
||||
<div class="hidden text-11-regular text-text-weaker">
|
||||
Approximate breakdown of input tokens. "Other" includes tool definitions and overhead.
|
||||
</div>
|
||||
<div class="hidden text-11-regular text-text-weaker">{language.t("context.breakdown.note")}</div>
|
||||
</div>
|
||||
</Show>
|
||||
|
||||
<Show when={systemPrompt()}>
|
||||
{(prompt) => (
|
||||
<div class="flex flex-col gap-2">
|
||||
<div class="text-12-regular text-text-weak">System Prompt</div>
|
||||
<div class="text-12-regular text-text-weak">{language.t("context.systemPrompt.title")}</div>
|
||||
<div class="border border-border-base rounded-md bg-surface-base px-3 py-2">
|
||||
<Markdown text={prompt()} class="text-12-regular" />
|
||||
</div>
|
||||
@@ -414,7 +421,7 @@ export function SessionContextTab(props: SessionContextTabProps) {
|
||||
</Show>
|
||||
|
||||
<div class="flex flex-col gap-2">
|
||||
<div class="text-12-regular text-text-weak">Raw messages</div>
|
||||
<div class="text-12-regular text-text-weak">{language.t("context.rawMessages.title")}</div>
|
||||
<Accordion multiple>
|
||||
<For each={props.messages()}>{(message) => <RawMessage message={message} />}</For>
|
||||
</Accordion>
|
||||
|
||||
@@ -1,21 +1,25 @@
|
||||
import { createMemo, createResource, Show } from "solid-js"
|
||||
import { createEffect, createMemo, onCleanup, Show } from "solid-js"
|
||||
import { createStore } from "solid-js/store"
|
||||
import { Portal } from "solid-js/web"
|
||||
import { useParams } from "@solidjs/router"
|
||||
import { useLayout } from "@/context/layout"
|
||||
import { useCommand } from "@/context/command"
|
||||
import { useLanguage } from "@/context/language"
|
||||
// import { useServer } from "@/context/server"
|
||||
// import { useDialog } from "@opencode-ai/ui/context/dialog"
|
||||
import { usePlatform } from "@/context/platform"
|
||||
import { useSync } from "@/context/sync"
|
||||
import { useGlobalSDK } from "@/context/global-sdk"
|
||||
import { getFilename } from "@opencode-ai/util/path"
|
||||
import { base64Decode } from "@opencode-ai/util/encode"
|
||||
import { iife } from "@opencode-ai/util/iife"
|
||||
|
||||
import { Icon } from "@opencode-ai/ui/icon"
|
||||
import { IconButton } from "@opencode-ai/ui/icon-button"
|
||||
import { Button } from "@opencode-ai/ui/button"
|
||||
import { Tooltip, TooltipKeybind } from "@opencode-ai/ui/tooltip"
|
||||
import { Popover } from "@opencode-ai/ui/popover"
|
||||
import { TextField } from "@opencode-ai/ui/text-field"
|
||||
import { Keybind } from "@opencode-ai/ui/keybind"
|
||||
|
||||
export function SessionHeader() {
|
||||
const globalSDK = useGlobalSDK()
|
||||
@@ -25,6 +29,8 @@ export function SessionHeader() {
|
||||
// const server = useServer()
|
||||
// const dialog = useDialog()
|
||||
const sync = useSync()
|
||||
const platform = usePlatform()
|
||||
const language = useLanguage()
|
||||
|
||||
const projectDirectory = createMemo(() => base64Decode(params.dir ?? ""))
|
||||
const project = createMemo(() => {
|
||||
@@ -41,9 +47,83 @@ export function SessionHeader() {
|
||||
|
||||
const currentSession = createMemo(() => sync.data.session.find((s) => s.id === params.id))
|
||||
const shareEnabled = createMemo(() => sync.data.config.share !== "disabled")
|
||||
const showReview = createMemo(() => !!currentSession()?.summary?.files)
|
||||
const showShare = createMemo(() => shareEnabled() && !!currentSession())
|
||||
const sessionKey = createMemo(() => `${params.dir}${params.id ? "/" + params.id : ""}`)
|
||||
const view = createMemo(() => layout.view(sessionKey()))
|
||||
|
||||
const [state, setState] = createStore({
|
||||
share: false,
|
||||
unshare: false,
|
||||
copied: false,
|
||||
timer: undefined as number | undefined,
|
||||
})
|
||||
const shareUrl = createMemo(() => currentSession()?.share?.url)
|
||||
|
||||
createEffect(() => {
|
||||
const url = shareUrl()
|
||||
if (url) return
|
||||
if (state.timer) window.clearTimeout(state.timer)
|
||||
setState({ copied: false, timer: undefined })
|
||||
})
|
||||
|
||||
onCleanup(() => {
|
||||
if (state.timer) window.clearTimeout(state.timer)
|
||||
})
|
||||
|
||||
function shareSession() {
|
||||
const session = currentSession()
|
||||
if (!session || state.share) return
|
||||
setState("share", true)
|
||||
globalSDK.client.session
|
||||
.share({ sessionID: session.id, directory: projectDirectory() })
|
||||
.catch((error) => {
|
||||
console.error("Failed to share session", error)
|
||||
})
|
||||
.finally(() => {
|
||||
setState("share", false)
|
||||
})
|
||||
}
|
||||
|
||||
function unshareSession() {
|
||||
const session = currentSession()
|
||||
if (!session || state.unshare) return
|
||||
setState("unshare", true)
|
||||
globalSDK.client.session
|
||||
.unshare({ sessionID: session.id, directory: projectDirectory() })
|
||||
.catch((error) => {
|
||||
console.error("Failed to unshare session", error)
|
||||
})
|
||||
.finally(() => {
|
||||
setState("unshare", false)
|
||||
})
|
||||
}
|
||||
|
||||
function copyLink() {
|
||||
const url = shareUrl()
|
||||
if (!url) return
|
||||
navigator.clipboard
|
||||
.writeText(url)
|
||||
.then(() => {
|
||||
if (state.timer) window.clearTimeout(state.timer)
|
||||
setState("copied", true)
|
||||
const timer = window.setTimeout(() => {
|
||||
setState("copied", false)
|
||||
setState("timer", undefined)
|
||||
}, 3000)
|
||||
setState("timer", timer)
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error("Failed to copy share link", error)
|
||||
})
|
||||
}
|
||||
|
||||
function viewShare() {
|
||||
const url = shareUrl()
|
||||
if (!url) return
|
||||
platform.openLink(url)
|
||||
}
|
||||
|
||||
const centerMount = createMemo(() => document.getElementById("opencode-titlebar-center"))
|
||||
const rightMount = createMemo(() => document.getElementById("opencode-titlebar-right"))
|
||||
|
||||
@@ -54,18 +134,17 @@ export function SessionHeader() {
|
||||
<Portal mount={mount()}>
|
||||
<button
|
||||
type="button"
|
||||
class="hidden md:flex w-[320px] h-7 px-1.5 items-center gap-2 rounded-md border border-border-weak-base bg-surface-raised-base transition-colors cursor-default hover:bg-surface-raised-base-hover focus:bg-surface-raised-base-hover active:bg-surface-raised-base-active"
|
||||
class="hidden md:flex w-[320px] p-1 pl-1.5 items-center gap-2 justify-between rounded-md border border-border-weak-base bg-surface-raised-base transition-colors cursor-default hover:bg-surface-raised-base-hover focus:bg-surface-raised-base-hover active:bg-surface-raised-base-active"
|
||||
onClick={() => command.trigger("file.open")}
|
||||
>
|
||||
<Icon name="magnifying-glass" size="small" class="text-text-weak" />
|
||||
<span class="flex-1 min-w-0 text-14-regular text-text-weak truncate">Search {name()}</span>
|
||||
<Show when={hotkey()}>
|
||||
{(keybind) => (
|
||||
<span class="shrink-0 flex items-center justify-center h-5 px-2 rounded-md border border-border-weak-base bg-surface-base text-12-medium text-text-weak">
|
||||
{keybind()}
|
||||
</span>
|
||||
)}
|
||||
</Show>
|
||||
<div class="flex min-w-0 flex-1 items-center gap-2 overflow-visible">
|
||||
<Icon name="magnifying-glass" size="normal" class="icon-base shrink-0" />
|
||||
<span class="flex-1 min-w-0 text-14-regular text-text-weak truncate h-4.5 flex items-center">
|
||||
{language.t("session.header.search.placeholder", { project: name() })}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<Show when={hotkey()}>{(keybind) => <Keybind class="shrink-0">{keybind()}</Keybind>}</Show>
|
||||
</button>
|
||||
</Portal>
|
||||
)}
|
||||
@@ -97,10 +176,15 @@ export function SessionHeader() {
|
||||
{/* <SessionMcpIndicator /> */}
|
||||
{/* </div> */}
|
||||
<div class="flex items-center gap-1">
|
||||
<Show when={currentSession()?.summary?.files}>
|
||||
<div
|
||||
class="hidden md:block shrink-0"
|
||||
classList={{
|
||||
"opacity-0 pointer-events-none": !showReview(),
|
||||
}}
|
||||
aria-hidden={!showReview()}
|
||||
>
|
||||
<TooltipKeybind
|
||||
class="hidden md:block shrink-0"
|
||||
title="Toggle review"
|
||||
title={language.t("command.review.toggle")}
|
||||
keybind={command.keybind("review.toggle")}
|
||||
>
|
||||
<Button
|
||||
@@ -110,32 +194,32 @@ export function SessionHeader() {
|
||||
>
|
||||
<div class="relative flex items-center justify-center size-4 [&>*]:absolute [&>*]:inset-0">
|
||||
<Icon
|
||||
name={view().reviewPanel.opened() ? "layout-right" : "layout-left"}
|
||||
size="small"
|
||||
name={view().reviewPanel.opened() ? "layout-right-full" : "layout-right"}
|
||||
class="group-hover/review-toggle:hidden"
|
||||
/>
|
||||
<Icon
|
||||
name={view().reviewPanel.opened() ? "layout-right-partial" : "layout-left-partial"}
|
||||
size="small"
|
||||
name="layout-right-partial"
|
||||
class="hidden group-hover/review-toggle:inline-block"
|
||||
/>
|
||||
<Icon
|
||||
name={view().reviewPanel.opened() ? "layout-right-full" : "layout-left-full"}
|
||||
size="small"
|
||||
name={view().reviewPanel.opened() ? "layout-right" : "layout-right-full"}
|
||||
class="hidden group-active/review-toggle:inline-block"
|
||||
/>
|
||||
</div>
|
||||
</Button>
|
||||
</TooltipKeybind>
|
||||
</Show>
|
||||
</div>
|
||||
<TooltipKeybind
|
||||
class="hidden md:block shrink-0"
|
||||
title="Toggle terminal"
|
||||
title={language.t("command.terminal.toggle")}
|
||||
keybind={command.keybind("terminal.toggle")}
|
||||
>
|
||||
<Button
|
||||
variant="ghost"
|
||||
class="group/terminal-toggle size-6 p-0"
|
||||
class="group/terminal-toggle size-8 rounded-md"
|
||||
onClick={() => view().terminal.toggle()}
|
||||
>
|
||||
<div class="relative flex items-center justify-center size-4 [&>*]:absolute [&>*]:inset-0">
|
||||
@@ -158,42 +242,97 @@ export function SessionHeader() {
|
||||
</Button>
|
||||
</TooltipKeybind>
|
||||
</div>
|
||||
<Show when={shareEnabled() && currentSession()}>
|
||||
<div
|
||||
class="flex items-center"
|
||||
classList={{
|
||||
"opacity-0 pointer-events-none": !showShare(),
|
||||
}}
|
||||
aria-hidden={!showShare()}
|
||||
>
|
||||
<Popover
|
||||
title="Share session"
|
||||
title={language.t("session.share.popover.title")}
|
||||
description={
|
||||
shareUrl()
|
||||
? language.t("session.share.popover.description.shared")
|
||||
: language.t("session.share.popover.description.unshared")
|
||||
}
|
||||
trigger={
|
||||
<Tooltip class="shrink-0" value="Share session">
|
||||
<IconButton icon="share" variant="ghost" class="" />
|
||||
<Tooltip class="shrink-0" value={language.t("command.session.share")}>
|
||||
<Button
|
||||
variant="secondary"
|
||||
classList={{ "rounded-r-none": shareUrl() !== undefined }}
|
||||
style={{ scale: 1 }}
|
||||
>
|
||||
{language.t("session.share.action.share")}
|
||||
</Button>
|
||||
</Tooltip>
|
||||
}
|
||||
>
|
||||
{iife(() => {
|
||||
const [url] = createResource(
|
||||
() => currentSession(),
|
||||
async (session) => {
|
||||
if (!session) return
|
||||
let shareURL = session.share?.url
|
||||
if (!shareURL) {
|
||||
shareURL = await globalSDK.client.session
|
||||
.share({ sessionID: session.id, directory: projectDirectory() })
|
||||
.then((r) => r.data?.share?.url)
|
||||
.catch((e) => {
|
||||
console.error("Failed to share session", e)
|
||||
return undefined
|
||||
})
|
||||
}
|
||||
return shareURL
|
||||
},
|
||||
{ initialValue: "" },
|
||||
)
|
||||
return (
|
||||
<Show when={url.latest}>
|
||||
{(shareUrl) => <TextField value={shareUrl()} readOnly copyable class="w-72" />}
|
||||
</Show>
|
||||
)
|
||||
})}
|
||||
<div class="flex flex-col gap-2">
|
||||
<Show
|
||||
when={shareUrl()}
|
||||
fallback={
|
||||
<div class="flex">
|
||||
<Button
|
||||
size="large"
|
||||
variant="primary"
|
||||
class="w-1/2"
|
||||
onClick={shareSession}
|
||||
disabled={state.share}
|
||||
>
|
||||
{state.share
|
||||
? language.t("session.share.action.publishing")
|
||||
: language.t("session.share.action.publish")}
|
||||
</Button>
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<div class="flex flex-col gap-2 w-72">
|
||||
<TextField value={shareUrl() ?? ""} readOnly copyable class="w-full" />
|
||||
<div class="grid grid-cols-2 gap-2">
|
||||
<Button
|
||||
size="large"
|
||||
variant="secondary"
|
||||
class="w-full shadow-none border border-border-weak-base"
|
||||
onClick={unshareSession}
|
||||
disabled={state.unshare}
|
||||
>
|
||||
{state.unshare
|
||||
? language.t("session.share.action.unpublishing")
|
||||
: language.t("session.share.action.unpublish")}
|
||||
</Button>
|
||||
<Button
|
||||
size="large"
|
||||
variant="primary"
|
||||
class="w-full"
|
||||
onClick={viewShare}
|
||||
disabled={state.unshare}
|
||||
>
|
||||
{language.t("session.share.action.view")}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</Show>
|
||||
</div>
|
||||
</Popover>
|
||||
</Show>
|
||||
<Show when={shareUrl()} fallback={<div class="size-6" aria-hidden="true" />}>
|
||||
<Tooltip
|
||||
value={
|
||||
state.copied ? language.t("session.share.copy.copied") : language.t("session.share.copy.copyLink")
|
||||
}
|
||||
placement="top"
|
||||
gutter={8}
|
||||
>
|
||||
<IconButton
|
||||
icon={state.copied ? "check" : "copy"}
|
||||
variant="secondary"
|
||||
class="rounded-l-none"
|
||||
onClick={copyLink}
|
||||
disabled={state.unshare}
|
||||
/>
|
||||
</Tooltip>
|
||||
</Show>
|
||||
</div>
|
||||
</div>
|
||||
</Portal>
|
||||
)}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { Show, createMemo } from "solid-js"
|
||||
import { DateTime } from "luxon"
|
||||
import { useSync } from "@/context/sync"
|
||||
import { useLanguage } from "@/context/language"
|
||||
import { Icon } from "@opencode-ai/ui/icon"
|
||||
import { getDirectory, getFilename } from "@opencode-ai/util/path"
|
||||
import { Select } from "@opencode-ai/ui/select"
|
||||
@@ -15,6 +16,7 @@ interface NewSessionViewProps {
|
||||
|
||||
export function NewSessionView(props: NewSessionViewProps) {
|
||||
const sync = useSync()
|
||||
const language = useLanguage()
|
||||
|
||||
const sandboxes = createMemo(() => sync.project?.sandboxes ?? [])
|
||||
const options = createMemo(() => [MAIN_WORKTREE, ...sandboxes(), CREATE_WORKTREE])
|
||||
@@ -32,13 +34,13 @@ export function NewSessionView(props: NewSessionViewProps) {
|
||||
|
||||
const label = (value: string) => {
|
||||
if (value === MAIN_WORKTREE) {
|
||||
if (isWorktree()) return "Main branch"
|
||||
if (isWorktree()) return language.t("session.new.worktree.main")
|
||||
const branch = sync.data.vcs?.branch
|
||||
if (branch) return `Main branch (${branch})`
|
||||
return "Main branch"
|
||||
if (branch) return language.t("session.new.worktree.mainWithBranch", { branch })
|
||||
return language.t("session.new.worktree.main")
|
||||
}
|
||||
|
||||
if (value === CREATE_WORKTREE) return "Create new worktree"
|
||||
if (value === CREATE_WORKTREE) return language.t("session.new.worktree.create")
|
||||
|
||||
return getFilename(value)
|
||||
}
|
||||
@@ -48,10 +50,10 @@ export function NewSessionView(props: NewSessionViewProps) {
|
||||
class="size-full flex flex-col pb-45 justify-end items-start gap-4 flex-[1_0_0] self-stretch max-w-200 mx-auto px-6"
|
||||
style={{ "padding-bottom": "calc(var(--prompt-height, 11.25rem) + 64px)" }}
|
||||
>
|
||||
<div class="text-20-medium text-text-weaker">New session</div>
|
||||
<div class="text-20-medium text-text-weaker">{language.t("command.session.new")}</div>
|
||||
<div class="flex justify-center items-center gap-3">
|
||||
<Icon name="folder" size="small" />
|
||||
<div class="text-12-medium text-text-weak">
|
||||
<div class="text-12-medium text-text-weak select-text">
|
||||
{getDirectory(projectRoot())}
|
||||
<span class="text-text-strong">{getFilename(projectRoot())}</span>
|
||||
</div>
|
||||
@@ -76,9 +78,11 @@ export function NewSessionView(props: NewSessionViewProps) {
|
||||
<div class="flex justify-center items-center gap-3">
|
||||
<Icon name="pencil-line" size="small" />
|
||||
<div class="text-12-medium text-text-weak">
|
||||
Last modified
|
||||
{language.t("session.new.lastModified")}
|
||||
<span class="text-text-strong">
|
||||
{DateTime.fromMillis(project().time.updated ?? project().time.created).toRelative()}
|
||||
{DateTime.fromMillis(project().time.updated ?? project().time.created)
|
||||
.setLocale(language.locale())
|
||||
.toRelative()}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -7,6 +7,7 @@ import { Tooltip } from "@opencode-ai/ui/tooltip"
|
||||
import { Tabs } from "@opencode-ai/ui/tabs"
|
||||
import { getFilename } from "@opencode-ai/util/path"
|
||||
import { useFile } from "@/context/file"
|
||||
import { useLanguage } from "@/context/language"
|
||||
|
||||
export function FileVisual(props: { path: string; active?: boolean }): JSX.Element {
|
||||
return (
|
||||
@@ -25,6 +26,7 @@ export function FileVisual(props: { path: string; active?: boolean }): JSX.Eleme
|
||||
|
||||
export function SortableTab(props: { tab: string; onTabClose: (tab: string) => void }): JSX.Element {
|
||||
const file = useFile()
|
||||
const language = useLanguage()
|
||||
const sortable = createSortable(props.tab)
|
||||
const path = createMemo(() => file.pathFromTab(props.tab))
|
||||
return (
|
||||
@@ -34,7 +36,7 @@ export function SortableTab(props: { tab: string; onTabClose: (tab: string) => v
|
||||
<Tabs.Trigger
|
||||
value={props.tab}
|
||||
closeButton={
|
||||
<Tooltip value="Close tab" placement="bottom">
|
||||
<Tooltip value={language.t("common.closeTab")} placement="bottom">
|
||||
<IconButton icon="close" variant="ghost" onClick={() => props.onTabClose(props.tab)} />
|
||||
</Tooltip>
|
||||
}
|
||||
|
||||
@@ -1,26 +1,185 @@
|
||||
import type { JSX } from "solid-js"
|
||||
import { createSignal, Show } from "solid-js"
|
||||
import { createSortable } from "@thisbeyond/solid-dnd"
|
||||
import { IconButton } from "@opencode-ai/ui/icon-button"
|
||||
import { Tabs } from "@opencode-ai/ui/tabs"
|
||||
import { DropdownMenu } from "@opencode-ai/ui/dropdown-menu"
|
||||
import { Icon } from "@opencode-ai/ui/icon"
|
||||
import { useTerminal, type LocalPTY } from "@/context/terminal"
|
||||
import { useLanguage } from "@/context/language"
|
||||
|
||||
export function SortableTerminalTab(props: { terminal: LocalPTY }): JSX.Element {
|
||||
export function SortableTerminalTab(props: { terminal: LocalPTY; onClose?: () => void }): JSX.Element {
|
||||
const terminal = useTerminal()
|
||||
const language = useLanguage()
|
||||
const sortable = createSortable(props.terminal.id)
|
||||
const [editing, setEditing] = createSignal(false)
|
||||
const [title, setTitle] = createSignal(props.terminal.title)
|
||||
const [menuOpen, setMenuOpen] = createSignal(false)
|
||||
const [menuPosition, setMenuPosition] = createSignal({ x: 0, y: 0 })
|
||||
const [blurEnabled, setBlurEnabled] = createSignal(false)
|
||||
|
||||
const isDefaultTitle = () => {
|
||||
const number = props.terminal.titleNumber
|
||||
if (!Number.isFinite(number) || number <= 0) return false
|
||||
const match = props.terminal.title.match(/^Terminal (\d+)$/)
|
||||
if (!match) return false
|
||||
const parsed = Number(match[1])
|
||||
if (!Number.isFinite(parsed) || parsed <= 0) return false
|
||||
return parsed === number
|
||||
}
|
||||
|
||||
const label = () => {
|
||||
language.locale()
|
||||
if (props.terminal.title && !isDefaultTitle()) return props.terminal.title
|
||||
|
||||
const number = props.terminal.titleNumber
|
||||
if (Number.isFinite(number) && number > 0) return language.t("terminal.title.numbered", { number })
|
||||
if (props.terminal.title) return props.terminal.title
|
||||
return language.t("terminal.title")
|
||||
}
|
||||
|
||||
const close = () => {
|
||||
const count = terminal.all().length
|
||||
terminal.close(props.terminal.id)
|
||||
if (count === 1) {
|
||||
props.onClose?.()
|
||||
}
|
||||
}
|
||||
|
||||
const focus = () => {
|
||||
if (editing()) return
|
||||
|
||||
if (document.activeElement instanceof HTMLElement) {
|
||||
document.activeElement.blur()
|
||||
}
|
||||
const wrapper = document.getElementById(`terminal-wrapper-${props.terminal.id}`)
|
||||
const element = wrapper?.querySelector('[data-component="terminal"]') as HTMLElement
|
||||
if (!element) return
|
||||
|
||||
const textarea = element.querySelector("textarea") as HTMLTextAreaElement
|
||||
if (textarea) {
|
||||
textarea.focus()
|
||||
return
|
||||
}
|
||||
element.focus()
|
||||
element.dispatchEvent(new PointerEvent("pointerdown", { bubbles: true, cancelable: true }))
|
||||
}
|
||||
|
||||
const edit = (e?: Event) => {
|
||||
if (e) {
|
||||
e.stopPropagation()
|
||||
e.preventDefault()
|
||||
}
|
||||
|
||||
setBlurEnabled(false)
|
||||
setTitle(props.terminal.title)
|
||||
setEditing(true)
|
||||
setTimeout(() => {
|
||||
const input = document.getElementById(`terminal-title-input-${props.terminal.id}`) as HTMLInputElement
|
||||
if (!input) return
|
||||
input.focus()
|
||||
input.select()
|
||||
setTimeout(() => setBlurEnabled(true), 100)
|
||||
}, 10)
|
||||
}
|
||||
|
||||
const save = () => {
|
||||
if (!blurEnabled()) return
|
||||
|
||||
const value = title().trim()
|
||||
if (value && value !== props.terminal.title) {
|
||||
terminal.update({ id: props.terminal.id, title: value })
|
||||
}
|
||||
setEditing(false)
|
||||
}
|
||||
|
||||
const keydown = (e: KeyboardEvent) => {
|
||||
if (e.key === "Enter") {
|
||||
e.preventDefault()
|
||||
save()
|
||||
return
|
||||
}
|
||||
if (e.key === "Escape") {
|
||||
e.preventDefault()
|
||||
setEditing(false)
|
||||
}
|
||||
}
|
||||
|
||||
const menu = (e: MouseEvent) => {
|
||||
e.preventDefault()
|
||||
setMenuPosition({ x: e.clientX, y: e.clientY })
|
||||
setMenuOpen(true)
|
||||
}
|
||||
|
||||
return (
|
||||
// @ts-ignore
|
||||
<div use:sortable classList={{ "h-full": true, "opacity-0": sortable.isActiveDraggable }}>
|
||||
<div
|
||||
// @ts-ignore
|
||||
use:sortable
|
||||
class="outline-none focus:outline-none focus-visible:outline-none"
|
||||
classList={{
|
||||
"h-full": true,
|
||||
"opacity-0": sortable.isActiveDraggable,
|
||||
}}
|
||||
>
|
||||
<div class="relative h-full">
|
||||
<Tabs.Trigger
|
||||
value={props.terminal.id}
|
||||
onClick={focus}
|
||||
onMouseDown={(e) => e.preventDefault()}
|
||||
onContextMenu={menu}
|
||||
class="!shadow-none"
|
||||
classes={{
|
||||
button: "border-0 outline-none focus:outline-none focus-visible:outline-none !shadow-none !ring-0",
|
||||
}}
|
||||
closeButton={
|
||||
terminal.all().length > 1 && (
|
||||
<IconButton icon="close" variant="ghost" onClick={() => terminal.close(props.terminal.id)} />
|
||||
)
|
||||
<IconButton
|
||||
icon="close"
|
||||
variant="ghost"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
close()
|
||||
}}
|
||||
/>
|
||||
}
|
||||
>
|
||||
{props.terminal.title}
|
||||
<span onDblClick={edit} style={{ visibility: editing() ? "hidden" : "visible" }}>
|
||||
{label()}
|
||||
</span>
|
||||
</Tabs.Trigger>
|
||||
<Show when={editing()}>
|
||||
<div class="absolute inset-0 flex items-center px-3 bg-muted z-10 pointer-events-auto">
|
||||
<input
|
||||
id={`terminal-title-input-${props.terminal.id}`}
|
||||
type="text"
|
||||
value={title()}
|
||||
onInput={(e) => setTitle(e.currentTarget.value)}
|
||||
onBlur={save}
|
||||
onKeyDown={keydown}
|
||||
onMouseDown={(e) => e.stopPropagation()}
|
||||
class="bg-transparent border-none outline-none text-sm min-w-0 flex-1"
|
||||
/>
|
||||
</div>
|
||||
</Show>
|
||||
<DropdownMenu open={menuOpen()} onOpenChange={setMenuOpen}>
|
||||
<DropdownMenu.Portal>
|
||||
<DropdownMenu.Content
|
||||
style={{
|
||||
position: "fixed",
|
||||
left: `${menuPosition().x}px`,
|
||||
top: `${menuPosition().y}px`,
|
||||
}}
|
||||
>
|
||||
<DropdownMenu.Item onSelect={edit}>
|
||||
<Icon name="edit" class="w-4 h-4 mr-2" />
|
||||
{language.t("common.rename")}
|
||||
</DropdownMenu.Item>
|
||||
<DropdownMenu.Item onSelect={close}>
|
||||
<Icon name="close" class="w-4 h-4 mr-2" />
|
||||
{language.t("common.close")}
|
||||
</DropdownMenu.Item>
|
||||
</DropdownMenu.Content>
|
||||
</DropdownMenu.Portal>
|
||||
</DropdownMenu>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
||||
15
packages/app/src/components/settings-agents.tsx
Normal file
15
packages/app/src/components/settings-agents.tsx
Normal file
@@ -0,0 +1,15 @@
|
||||
import { Component } from "solid-js"
|
||||
import { useLanguage } from "@/context/language"
|
||||
|
||||
export const SettingsAgents: Component = () => {
|
||||
const language = useLanguage()
|
||||
|
||||
return (
|
||||
<div class="flex flex-col h-full overflow-y-auto">
|
||||
<div class="flex flex-col gap-6 p-6 max-w-[600px]">
|
||||
<h2 class="text-16-medium text-text-strong">{language.t("settings.agents.title")}</h2>
|
||||
<p class="text-14-regular text-text-weak">{language.t("settings.agents.description")}</p>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
15
packages/app/src/components/settings-commands.tsx
Normal file
15
packages/app/src/components/settings-commands.tsx
Normal file
@@ -0,0 +1,15 @@
|
||||
import { Component } from "solid-js"
|
||||
import { useLanguage } from "@/context/language"
|
||||
|
||||
export const SettingsCommands: Component = () => {
|
||||
const language = useLanguage()
|
||||
|
||||
return (
|
||||
<div class="flex flex-col h-full overflow-y-auto">
|
||||
<div class="flex flex-col gap-6 p-6 max-w-[600px]">
|
||||
<h2 class="text-16-medium text-text-strong">{language.t("settings.commands.title")}</h2>
|
||||
<p class="text-14-regular text-text-weak">{language.t("settings.commands.description")}</p>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
297
packages/app/src/components/settings-general.tsx
Normal file
297
packages/app/src/components/settings-general.tsx
Normal file
@@ -0,0 +1,297 @@
|
||||
import { Component, createMemo, type JSX } from "solid-js"
|
||||
import { Select } from "@opencode-ai/ui/select"
|
||||
import { Switch } from "@opencode-ai/ui/switch"
|
||||
import { useTheme, type ColorScheme } from "@opencode-ai/ui/theme"
|
||||
import { useLanguage } from "@/context/language"
|
||||
import { useSettings, monoFontFamily } from "@/context/settings"
|
||||
import { playSound, SOUND_OPTIONS } from "@/utils/sound"
|
||||
|
||||
export const SettingsGeneral: Component = () => {
|
||||
const theme = useTheme()
|
||||
const language = useLanguage()
|
||||
const settings = useSettings()
|
||||
|
||||
const themeOptions = createMemo(() =>
|
||||
Object.entries(theme.themes()).map(([id, def]) => ({ id, name: def.name ?? id })),
|
||||
)
|
||||
|
||||
const colorSchemeOptions = createMemo((): { value: ColorScheme; label: string }[] => [
|
||||
{ value: "system", label: language.t("theme.scheme.system") },
|
||||
{ value: "light", label: language.t("theme.scheme.light") },
|
||||
{ value: "dark", label: language.t("theme.scheme.dark") },
|
||||
])
|
||||
|
||||
const languageOptions = createMemo(() =>
|
||||
language.locales.map((locale) => ({
|
||||
value: locale,
|
||||
label: language.label(locale),
|
||||
})),
|
||||
)
|
||||
|
||||
const fontOptions = [
|
||||
{ value: "ibm-plex-mono", label: "font.option.ibmPlexMono" },
|
||||
{ value: "cascadia-code", label: "font.option.cascadiaCode" },
|
||||
{ value: "fira-code", label: "font.option.firaCode" },
|
||||
{ value: "hack", label: "font.option.hack" },
|
||||
{ value: "inconsolata", label: "font.option.inconsolata" },
|
||||
{ value: "intel-one-mono", label: "font.option.intelOneMono" },
|
||||
{ value: "jetbrains-mono", label: "font.option.jetbrainsMono" },
|
||||
{ value: "meslo-lgs", label: "font.option.mesloLgs" },
|
||||
{ value: "roboto-mono", label: "font.option.robotoMono" },
|
||||
{ value: "source-code-pro", label: "font.option.sourceCodePro" },
|
||||
{ value: "ubuntu-mono", label: "font.option.ubuntuMono" },
|
||||
] as const
|
||||
const fontOptionsList = [...fontOptions]
|
||||
|
||||
const soundOptions = [...SOUND_OPTIONS]
|
||||
|
||||
return (
|
||||
<div class="flex flex-col h-full overflow-y-auto no-scrollbar" style={{ padding: "0 40px 40px 40px" }}>
|
||||
<div
|
||||
class="sticky top-0 z-10"
|
||||
style={{
|
||||
background:
|
||||
"linear-gradient(to bottom, var(--surface-raised-stronger-non-alpha) calc(100% - 24px), transparent)",
|
||||
}}
|
||||
>
|
||||
<div class="flex flex-col gap-1 pt-6 pb-8">
|
||||
<h2 class="text-16-medium text-text-strong">{language.t("settings.tab.general")}</h2>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col gap-8 w-full">
|
||||
{/* Appearance Section */}
|
||||
<div class="flex flex-col gap-1">
|
||||
<h3 class="text-14-medium text-text-strong pb-2">{language.t("settings.general.section.appearance")}</h3>
|
||||
|
||||
<div class="bg-surface-raised-base px-4 rounded-lg">
|
||||
<SettingsRow
|
||||
title={language.t("settings.general.row.language.title")}
|
||||
description={language.t("settings.general.row.language.description")}
|
||||
>
|
||||
<Select
|
||||
options={languageOptions()}
|
||||
current={languageOptions().find((o) => o.value === language.locale())}
|
||||
value={(o) => o.value}
|
||||
label={(o) => o.label}
|
||||
onSelect={(option) => option && language.setLocale(option.value)}
|
||||
variant="secondary"
|
||||
size="small"
|
||||
triggerVariant="settings"
|
||||
/>
|
||||
</SettingsRow>
|
||||
|
||||
<SettingsRow
|
||||
title={language.t("settings.general.row.appearance.title")}
|
||||
description={language.t("settings.general.row.appearance.description")}
|
||||
>
|
||||
<Select
|
||||
options={colorSchemeOptions()}
|
||||
current={colorSchemeOptions().find((o) => o.value === theme.colorScheme())}
|
||||
value={(o) => o.value}
|
||||
label={(o) => o.label}
|
||||
onSelect={(option) => option && theme.setColorScheme(option.value)}
|
||||
onHighlight={(option) => {
|
||||
if (!option) return
|
||||
theme.previewColorScheme(option.value)
|
||||
return () => theme.cancelPreview()
|
||||
}}
|
||||
variant="secondary"
|
||||
size="small"
|
||||
triggerVariant="settings"
|
||||
/>
|
||||
</SettingsRow>
|
||||
|
||||
<SettingsRow
|
||||
title={language.t("settings.general.row.theme.title")}
|
||||
description={
|
||||
<>
|
||||
{language.t("settings.general.row.theme.description")}{" "}
|
||||
<a href="#" class="text-text-interactive-base">
|
||||
{language.t("common.learnMore")}
|
||||
</a>
|
||||
</>
|
||||
}
|
||||
>
|
||||
<Select
|
||||
options={themeOptions()}
|
||||
current={themeOptions().find((o) => o.id === theme.themeId())}
|
||||
value={(o) => o.id}
|
||||
label={(o) => o.name}
|
||||
onSelect={(option) => {
|
||||
if (!option) return
|
||||
theme.setTheme(option.id)
|
||||
}}
|
||||
onHighlight={(option) => {
|
||||
if (!option) return
|
||||
theme.previewTheme(option.id)
|
||||
return () => theme.cancelPreview()
|
||||
}}
|
||||
variant="secondary"
|
||||
size="small"
|
||||
triggerVariant="settings"
|
||||
/>
|
||||
</SettingsRow>
|
||||
|
||||
<SettingsRow
|
||||
title={language.t("settings.general.row.font.title")}
|
||||
description={language.t("settings.general.row.font.description")}
|
||||
>
|
||||
<Select
|
||||
options={fontOptionsList}
|
||||
current={fontOptionsList.find((o) => o.value === settings.appearance.font())}
|
||||
value={(o) => o.value}
|
||||
label={(o) => language.t(o.label)}
|
||||
onSelect={(option) => option && settings.appearance.setFont(option.value)}
|
||||
variant="secondary"
|
||||
size="small"
|
||||
triggerVariant="settings"
|
||||
triggerStyle={{ "font-family": monoFontFamily(settings.appearance.font()), "min-width": "180px" }}
|
||||
>
|
||||
{(option) => (
|
||||
<span style={{ "font-family": monoFontFamily(option?.value) }}>
|
||||
{option ? language.t(option.label) : ""}
|
||||
</span>
|
||||
)}
|
||||
</Select>
|
||||
</SettingsRow>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* System notifications Section */}
|
||||
<div class="flex flex-col gap-1">
|
||||
<h3 class="text-14-medium text-text-strong pb-2">{language.t("settings.general.section.notifications")}</h3>
|
||||
|
||||
<div class="bg-surface-raised-base px-4 rounded-lg">
|
||||
<SettingsRow
|
||||
title={language.t("settings.general.notifications.agent.title")}
|
||||
description={language.t("settings.general.notifications.agent.description")}
|
||||
>
|
||||
<Switch
|
||||
checked={settings.notifications.agent()}
|
||||
onChange={(checked) => settings.notifications.setAgent(checked)}
|
||||
/>
|
||||
</SettingsRow>
|
||||
|
||||
<SettingsRow
|
||||
title={language.t("settings.general.notifications.permissions.title")}
|
||||
description={language.t("settings.general.notifications.permissions.description")}
|
||||
>
|
||||
<Switch
|
||||
checked={settings.notifications.permissions()}
|
||||
onChange={(checked) => settings.notifications.setPermissions(checked)}
|
||||
/>
|
||||
</SettingsRow>
|
||||
|
||||
<SettingsRow
|
||||
title={language.t("settings.general.notifications.errors.title")}
|
||||
description={language.t("settings.general.notifications.errors.description")}
|
||||
>
|
||||
<Switch
|
||||
checked={settings.notifications.errors()}
|
||||
onChange={(checked) => settings.notifications.setErrors(checked)}
|
||||
/>
|
||||
</SettingsRow>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Sound effects Section */}
|
||||
<div class="flex flex-col gap-1">
|
||||
<h3 class="text-14-medium text-text-strong pb-2">{language.t("settings.general.section.sounds")}</h3>
|
||||
|
||||
<div class="bg-surface-raised-base px-4 rounded-lg">
|
||||
<SettingsRow
|
||||
title={language.t("settings.general.sounds.agent.title")}
|
||||
description={language.t("settings.general.sounds.agent.description")}
|
||||
>
|
||||
<Select
|
||||
options={soundOptions}
|
||||
current={soundOptions.find((o) => o.id === settings.sounds.agent())}
|
||||
value={(o) => o.id}
|
||||
label={(o) => language.t(o.label)}
|
||||
onHighlight={(option) => {
|
||||
if (!option) return
|
||||
playSound(option.src)
|
||||
}}
|
||||
onSelect={(option) => {
|
||||
if (!option) return
|
||||
settings.sounds.setAgent(option.id)
|
||||
playSound(option.src)
|
||||
}}
|
||||
variant="secondary"
|
||||
size="small"
|
||||
triggerVariant="settings"
|
||||
/>
|
||||
</SettingsRow>
|
||||
|
||||
<SettingsRow
|
||||
title={language.t("settings.general.sounds.permissions.title")}
|
||||
description={language.t("settings.general.sounds.permissions.description")}
|
||||
>
|
||||
<Select
|
||||
options={soundOptions}
|
||||
current={soundOptions.find((o) => o.id === settings.sounds.permissions())}
|
||||
value={(o) => o.id}
|
||||
label={(o) => language.t(o.label)}
|
||||
onHighlight={(option) => {
|
||||
if (!option) return
|
||||
playSound(option.src)
|
||||
}}
|
||||
onSelect={(option) => {
|
||||
if (!option) return
|
||||
settings.sounds.setPermissions(option.id)
|
||||
playSound(option.src)
|
||||
}}
|
||||
variant="secondary"
|
||||
size="small"
|
||||
triggerVariant="settings"
|
||||
/>
|
||||
</SettingsRow>
|
||||
|
||||
<SettingsRow
|
||||
title={language.t("settings.general.sounds.errors.title")}
|
||||
description={language.t("settings.general.sounds.errors.description")}
|
||||
>
|
||||
<Select
|
||||
options={soundOptions}
|
||||
current={soundOptions.find((o) => o.id === settings.sounds.errors())}
|
||||
value={(o) => o.id}
|
||||
label={(o) => language.t(o.label)}
|
||||
onHighlight={(option) => {
|
||||
if (!option) return
|
||||
playSound(option.src)
|
||||
}}
|
||||
onSelect={(option) => {
|
||||
if (!option) return
|
||||
settings.sounds.setErrors(option.id)
|
||||
playSound(option.src)
|
||||
}}
|
||||
variant="secondary"
|
||||
size="small"
|
||||
triggerVariant="settings"
|
||||
/>
|
||||
</SettingsRow>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
interface SettingsRowProps {
|
||||
title: string
|
||||
description: string | JSX.Element
|
||||
children: JSX.Element
|
||||
}
|
||||
|
||||
const SettingsRow: Component<SettingsRowProps> = (props) => {
|
||||
return (
|
||||
<div class="flex items-center justify-between gap-4 py-3 border-b border-border-weak-base last:border-none">
|
||||
<div class="flex flex-col gap-0.5">
|
||||
<span class="text-14-medium text-text-strong">{props.title}</span>
|
||||
<span class="text-12-regular text-text-weak">{props.description}</span>
|
||||
</div>
|
||||
<div class="flex-shrink-0">{props.children}</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
437
packages/app/src/components/settings-keybinds.tsx
Normal file
437
packages/app/src/components/settings-keybinds.tsx
Normal file
@@ -0,0 +1,437 @@
|
||||
import { Component, For, Show, createMemo, createSignal, onCleanup, onMount } from "solid-js"
|
||||
import { Button } from "@opencode-ai/ui/button"
|
||||
import { Icon } from "@opencode-ai/ui/icon"
|
||||
import { IconButton } from "@opencode-ai/ui/icon-button"
|
||||
import { TextField } from "@opencode-ai/ui/text-field"
|
||||
import { showToast } from "@opencode-ai/ui/toast"
|
||||
import fuzzysort from "fuzzysort"
|
||||
import { formatKeybind, parseKeybind, useCommand } from "@/context/command"
|
||||
import { useLanguage } from "@/context/language"
|
||||
import { useSettings } from "@/context/settings"
|
||||
|
||||
const IS_MAC = typeof navigator === "object" && /(Mac|iPod|iPhone|iPad)/.test(navigator.platform)
|
||||
const PALETTE_ID = "command.palette"
|
||||
const DEFAULT_PALETTE_KEYBIND = "mod+shift+p"
|
||||
|
||||
type KeybindGroup = "General" | "Session" | "Navigation" | "Model and agent" | "Terminal" | "Prompt"
|
||||
|
||||
type KeybindMeta = {
|
||||
title: string
|
||||
group: KeybindGroup
|
||||
}
|
||||
|
||||
const GROUPS: KeybindGroup[] = ["General", "Session", "Navigation", "Model and agent", "Terminal", "Prompt"]
|
||||
|
||||
type GroupKey =
|
||||
| "settings.shortcuts.group.general"
|
||||
| "settings.shortcuts.group.session"
|
||||
| "settings.shortcuts.group.navigation"
|
||||
| "settings.shortcuts.group.modelAndAgent"
|
||||
| "settings.shortcuts.group.terminal"
|
||||
| "settings.shortcuts.group.prompt"
|
||||
|
||||
const groupKey: Record<KeybindGroup, GroupKey> = {
|
||||
General: "settings.shortcuts.group.general",
|
||||
Session: "settings.shortcuts.group.session",
|
||||
Navigation: "settings.shortcuts.group.navigation",
|
||||
"Model and agent": "settings.shortcuts.group.modelAndAgent",
|
||||
Terminal: "settings.shortcuts.group.terminal",
|
||||
Prompt: "settings.shortcuts.group.prompt",
|
||||
}
|
||||
|
||||
function groupFor(id: string): KeybindGroup {
|
||||
if (id === PALETTE_ID) return "General"
|
||||
if (id.startsWith("terminal.")) return "Terminal"
|
||||
if (id.startsWith("model.") || id.startsWith("agent.") || id.startsWith("mcp.")) return "Model and agent"
|
||||
if (id.startsWith("file.")) return "Navigation"
|
||||
if (id.startsWith("prompt.")) return "Prompt"
|
||||
if (
|
||||
id.startsWith("session.") ||
|
||||
id.startsWith("message.") ||
|
||||
id.startsWith("permissions.") ||
|
||||
id.startsWith("steps.") ||
|
||||
id.startsWith("review.")
|
||||
)
|
||||
return "Session"
|
||||
|
||||
return "General"
|
||||
}
|
||||
|
||||
function isModifier(key: string) {
|
||||
return key === "Shift" || key === "Control" || key === "Alt" || key === "Meta"
|
||||
}
|
||||
|
||||
function normalizeKey(key: string) {
|
||||
if (key === ",") return "comma"
|
||||
if (key === "+") return "plus"
|
||||
if (key === " ") return "space"
|
||||
return key.toLowerCase()
|
||||
}
|
||||
|
||||
function recordKeybind(event: KeyboardEvent) {
|
||||
if (isModifier(event.key)) return
|
||||
|
||||
const parts: string[] = []
|
||||
|
||||
const mod = IS_MAC ? event.metaKey : event.ctrlKey
|
||||
if (mod) parts.push("mod")
|
||||
|
||||
if (IS_MAC && event.ctrlKey) parts.push("ctrl")
|
||||
if (!IS_MAC && event.metaKey) parts.push("meta")
|
||||
if (event.altKey) parts.push("alt")
|
||||
if (event.shiftKey) parts.push("shift")
|
||||
|
||||
const key = normalizeKey(event.key)
|
||||
if (!key) return
|
||||
parts.push(key)
|
||||
|
||||
return parts.join("+")
|
||||
}
|
||||
|
||||
function signatures(config: string | undefined) {
|
||||
if (!config) return []
|
||||
const sigs: string[] = []
|
||||
|
||||
for (const kb of parseKeybind(config)) {
|
||||
const parts: string[] = []
|
||||
if (kb.ctrl) parts.push("ctrl")
|
||||
if (kb.alt) parts.push("alt")
|
||||
if (kb.shift) parts.push("shift")
|
||||
if (kb.meta) parts.push("meta")
|
||||
if (kb.key) parts.push(kb.key)
|
||||
if (parts.length === 0) continue
|
||||
sigs.push(parts.join("+"))
|
||||
}
|
||||
|
||||
return sigs
|
||||
}
|
||||
|
||||
export const SettingsKeybinds: Component = () => {
|
||||
const command = useCommand()
|
||||
const language = useLanguage()
|
||||
const settings = useSettings()
|
||||
|
||||
const [active, setActive] = createSignal<string | null>(null)
|
||||
const [filter, setFilter] = createSignal("")
|
||||
|
||||
const stop = () => {
|
||||
if (!active()) return
|
||||
setActive(null)
|
||||
command.keybinds(true)
|
||||
}
|
||||
|
||||
const start = (id: string) => {
|
||||
if (active() === id) {
|
||||
stop()
|
||||
return
|
||||
}
|
||||
|
||||
if (active()) stop()
|
||||
|
||||
setActive(id)
|
||||
command.keybinds(false)
|
||||
}
|
||||
|
||||
const hasOverrides = createMemo(() => {
|
||||
const keybinds = settings.current.keybinds as Record<string, string | undefined> | undefined
|
||||
if (!keybinds) return false
|
||||
return Object.values(keybinds).some((x) => typeof x === "string")
|
||||
})
|
||||
|
||||
const resetAll = () => {
|
||||
stop()
|
||||
settings.keybinds.resetAll()
|
||||
showToast({
|
||||
title: language.t("settings.shortcuts.reset.toast.title"),
|
||||
description: language.t("settings.shortcuts.reset.toast.description"),
|
||||
})
|
||||
}
|
||||
|
||||
const list = createMemo(() => {
|
||||
language.locale()
|
||||
const out = new Map<string, KeybindMeta>()
|
||||
out.set(PALETTE_ID, { title: language.t("command.palette"), group: "General" })
|
||||
|
||||
for (const opt of command.catalog) {
|
||||
if (opt.id.startsWith("suggested.")) continue
|
||||
out.set(opt.id, { title: opt.title, group: groupFor(opt.id) })
|
||||
}
|
||||
|
||||
for (const opt of command.options) {
|
||||
if (opt.id.startsWith("suggested.")) continue
|
||||
out.set(opt.id, { title: opt.title, group: groupFor(opt.id) })
|
||||
}
|
||||
|
||||
const keybinds = settings.current.keybinds as Record<string, string | undefined> | undefined
|
||||
if (keybinds) {
|
||||
for (const [id, value] of Object.entries(keybinds)) {
|
||||
if (typeof value !== "string") continue
|
||||
if (out.has(id)) continue
|
||||
out.set(id, { title: id, group: groupFor(id) })
|
||||
}
|
||||
}
|
||||
|
||||
return out
|
||||
})
|
||||
|
||||
const title = (id: string) => list().get(id)?.title ?? ""
|
||||
|
||||
const grouped = createMemo(() => {
|
||||
const map = list()
|
||||
const out = new Map<KeybindGroup, string[]>()
|
||||
|
||||
for (const group of GROUPS) out.set(group, [])
|
||||
|
||||
for (const [id, item] of map) {
|
||||
const ids = out.get(item.group)
|
||||
if (!ids) continue
|
||||
ids.push(id)
|
||||
}
|
||||
|
||||
for (const group of GROUPS) {
|
||||
const ids = out.get(group)
|
||||
if (!ids) continue
|
||||
|
||||
ids.sort((a, b) => {
|
||||
const at = map.get(a)?.title ?? ""
|
||||
const bt = map.get(b)?.title ?? ""
|
||||
return at.localeCompare(bt)
|
||||
})
|
||||
}
|
||||
|
||||
return out
|
||||
})
|
||||
|
||||
const filtered = createMemo(() => {
|
||||
const query = filter().toLowerCase().trim()
|
||||
if (!query) return grouped()
|
||||
|
||||
const map = list()
|
||||
const out = new Map<KeybindGroup, string[]>()
|
||||
|
||||
for (const group of GROUPS) out.set(group, [])
|
||||
|
||||
const items = Array.from(map.entries()).map(([id, meta]) => ({
|
||||
id,
|
||||
title: meta.title,
|
||||
group: meta.group,
|
||||
keybind: command.keybind(id) || "",
|
||||
}))
|
||||
|
||||
const results = fuzzysort.go(query, items, {
|
||||
keys: ["title", "keybind"],
|
||||
threshold: -10000,
|
||||
})
|
||||
|
||||
for (const result of results) {
|
||||
const item = result.obj
|
||||
const ids = out.get(item.group)
|
||||
if (!ids) continue
|
||||
ids.push(item.id)
|
||||
}
|
||||
|
||||
return out
|
||||
})
|
||||
|
||||
const hasResults = createMemo(() => {
|
||||
for (const group of GROUPS) {
|
||||
const ids = filtered().get(group) ?? []
|
||||
if (ids.length > 0) return true
|
||||
}
|
||||
return false
|
||||
})
|
||||
|
||||
const used = createMemo(() => {
|
||||
const map = new Map<string, { id: string; title: string }[]>()
|
||||
|
||||
const add = (key: string, value: { id: string; title: string }) => {
|
||||
const list = map.get(key)
|
||||
if (!list) {
|
||||
map.set(key, [value])
|
||||
return
|
||||
}
|
||||
list.push(value)
|
||||
}
|
||||
|
||||
const palette = settings.keybinds.get(PALETTE_ID) ?? DEFAULT_PALETTE_KEYBIND
|
||||
for (const sig of signatures(palette)) {
|
||||
add(sig, { id: PALETTE_ID, title: title(PALETTE_ID) })
|
||||
}
|
||||
|
||||
const valueFor = (id: string) => {
|
||||
const custom = settings.keybinds.get(id)
|
||||
if (typeof custom === "string") return custom
|
||||
|
||||
const live = command.options.find((x) => x.id === id)
|
||||
if (live?.keybind) return live.keybind
|
||||
|
||||
const meta = command.catalog.find((x) => x.id === id)
|
||||
return meta?.keybind
|
||||
}
|
||||
|
||||
for (const id of list().keys()) {
|
||||
if (id === PALETTE_ID) continue
|
||||
for (const sig of signatures(valueFor(id))) {
|
||||
add(sig, { id, title: title(id) })
|
||||
}
|
||||
}
|
||||
|
||||
return map
|
||||
})
|
||||
|
||||
const setKeybind = (id: string, keybind: string) => {
|
||||
settings.keybinds.set(id, keybind)
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
const handle = (event: KeyboardEvent) => {
|
||||
const id = active()
|
||||
if (!id) return
|
||||
|
||||
event.preventDefault()
|
||||
event.stopPropagation()
|
||||
event.stopImmediatePropagation()
|
||||
|
||||
if (event.key === "Escape") {
|
||||
stop()
|
||||
return
|
||||
}
|
||||
|
||||
const clear =
|
||||
(event.key === "Backspace" || event.key === "Delete") &&
|
||||
!event.ctrlKey &&
|
||||
!event.metaKey &&
|
||||
!event.altKey &&
|
||||
!event.shiftKey
|
||||
if (clear) {
|
||||
setKeybind(id, "none")
|
||||
stop()
|
||||
return
|
||||
}
|
||||
|
||||
const next = recordKeybind(event)
|
||||
if (!next) return
|
||||
|
||||
const map = used()
|
||||
const conflicts = new Map<string, string>()
|
||||
|
||||
for (const sig of signatures(next)) {
|
||||
const list = map.get(sig) ?? []
|
||||
for (const item of list) {
|
||||
if (item.id === id) continue
|
||||
conflicts.set(item.id, item.title)
|
||||
}
|
||||
}
|
||||
|
||||
if (conflicts.size > 0) {
|
||||
showToast({
|
||||
title: language.t("settings.shortcuts.conflict.title"),
|
||||
description: language.t("settings.shortcuts.conflict.description", {
|
||||
keybind: formatKeybind(next),
|
||||
titles: [...conflicts.values()].join(", "),
|
||||
}),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
setKeybind(id, next)
|
||||
stop()
|
||||
}
|
||||
|
||||
document.addEventListener("keydown", handle, true)
|
||||
onCleanup(() => {
|
||||
document.removeEventListener("keydown", handle, true)
|
||||
})
|
||||
})
|
||||
|
||||
onCleanup(() => {
|
||||
if (active()) command.keybinds(true)
|
||||
})
|
||||
|
||||
return (
|
||||
<div class="flex flex-col h-full overflow-y-auto no-scrollbar" style={{ padding: "0 40px 40px 40px" }}>
|
||||
<div
|
||||
class="sticky top-0 z-10"
|
||||
style={{
|
||||
background:
|
||||
"linear-gradient(to bottom, var(--surface-raised-stronger-non-alpha) calc(100% - 24px), transparent)",
|
||||
}}
|
||||
>
|
||||
<div class="flex flex-col gap-4 pt-6 pb-6 max-w-[720px]">
|
||||
<div class="flex items-center justify-between gap-4">
|
||||
<h2 class="text-16-medium text-text-strong">{language.t("settings.shortcuts.title")}</h2>
|
||||
<Button size="small" variant="secondary" onClick={resetAll} disabled={!hasOverrides()}>
|
||||
{language.t("settings.shortcuts.reset.button")}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center gap-2 px-3 h-9 rounded-lg bg-surface-base">
|
||||
<Icon name="magnifying-glass" class="text-icon-weak-base flex-shrink-0" />
|
||||
<TextField
|
||||
variant="ghost"
|
||||
type="text"
|
||||
value={filter()}
|
||||
onChange={setFilter}
|
||||
placeholder={language.t("settings.shortcuts.search.placeholder")}
|
||||
spellcheck={false}
|
||||
autocorrect="off"
|
||||
autocomplete="off"
|
||||
autocapitalize="off"
|
||||
class="flex-1"
|
||||
/>
|
||||
<Show when={filter()}>
|
||||
<IconButton icon="circle-x" variant="ghost" onClick={() => setFilter("")} />
|
||||
</Show>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col gap-8 max-w-[720px]">
|
||||
<For each={GROUPS}>
|
||||
{(group) => (
|
||||
<Show when={(filtered().get(group) ?? []).length > 0}>
|
||||
<div class="flex flex-col gap-1">
|
||||
<h3 class="text-14-medium text-text-strong pb-2">{language.t(groupKey[group])}</h3>
|
||||
<div class="bg-surface-raised-base px-4 rounded-lg">
|
||||
<For each={filtered().get(group) ?? []}>
|
||||
{(id) => (
|
||||
<div class="flex items-center justify-between gap-4 py-3 border-b border-border-weak-base last:border-none">
|
||||
<span class="text-14-regular text-text-strong">{title(id)}</span>
|
||||
<button
|
||||
type="button"
|
||||
classList={{
|
||||
"h-8 px-3 rounded-md text-12-regular": true,
|
||||
"bg-surface-base text-text-subtle hover:bg-surface-raised-base-hover active:bg-surface-raised-base-active":
|
||||
active() !== id,
|
||||
"border border-border-weak-base bg-surface-inset-base text-text-weak": active() === id,
|
||||
}}
|
||||
onClick={() => start(id)}
|
||||
>
|
||||
<Show
|
||||
when={active() === id}
|
||||
fallback={command.keybind(id) || language.t("settings.shortcuts.unassigned")}
|
||||
>
|
||||
{language.t("settings.shortcuts.pressKeys")}
|
||||
</Show>
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</For>
|
||||
</div>
|
||||
</div>
|
||||
</Show>
|
||||
)}
|
||||
</For>
|
||||
|
||||
<Show when={filter() && !hasResults()}>
|
||||
<div class="flex flex-col items-center justify-center py-12 text-center">
|
||||
<span class="text-14-regular text-text-weak">{language.t("settings.shortcuts.search.empty")}</span>
|
||||
<Show when={filter()}>
|
||||
<span class="text-14-regular text-text-strong mt-1">"{filter()}"</span>
|
||||
</Show>
|
||||
</div>
|
||||
</Show>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
15
packages/app/src/components/settings-mcp.tsx
Normal file
15
packages/app/src/components/settings-mcp.tsx
Normal file
@@ -0,0 +1,15 @@
|
||||
import { Component } from "solid-js"
|
||||
import { useLanguage } from "@/context/language"
|
||||
|
||||
export const SettingsMcp: Component = () => {
|
||||
const language = useLanguage()
|
||||
|
||||
return (
|
||||
<div class="flex flex-col h-full overflow-y-auto">
|
||||
<div class="flex flex-col gap-6 p-6 max-w-[600px]">
|
||||
<h2 class="text-16-medium text-text-strong">{language.t("settings.mcp.title")}</h2>
|
||||
<p class="text-14-regular text-text-weak">{language.t("settings.mcp.description")}</p>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
15
packages/app/src/components/settings-models.tsx
Normal file
15
packages/app/src/components/settings-models.tsx
Normal file
@@ -0,0 +1,15 @@
|
||||
import { Component } from "solid-js"
|
||||
import { useLanguage } from "@/context/language"
|
||||
|
||||
export const SettingsModels: Component = () => {
|
||||
const language = useLanguage()
|
||||
|
||||
return (
|
||||
<div class="flex flex-col h-full overflow-y-auto">
|
||||
<div class="flex flex-col gap-6 p-6 max-w-[600px]">
|
||||
<h2 class="text-16-medium text-text-strong">{language.t("settings.models.title")}</h2>
|
||||
<p class="text-14-regular text-text-weak">{language.t("settings.models.description")}</p>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
234
packages/app/src/components/settings-permissions.tsx
Normal file
234
packages/app/src/components/settings-permissions.tsx
Normal file
@@ -0,0 +1,234 @@
|
||||
import { Select } from "@opencode-ai/ui/select"
|
||||
import { showToast } from "@opencode-ai/ui/toast"
|
||||
import { Component, For, createMemo, type JSX } from "solid-js"
|
||||
import { useGlobalSync } from "@/context/global-sync"
|
||||
import { useLanguage } from "@/context/language"
|
||||
|
||||
type PermissionAction = "allow" | "ask" | "deny"
|
||||
|
||||
type PermissionObject = Record<string, PermissionAction>
|
||||
type PermissionValue = PermissionAction | PermissionObject | string[] | undefined
|
||||
type PermissionMap = Record<string, PermissionValue>
|
||||
|
||||
type PermissionItem = {
|
||||
id: string
|
||||
title: string
|
||||
description: string
|
||||
}
|
||||
|
||||
const ACTIONS = [
|
||||
{ value: "allow", label: "settings.permissions.action.allow" },
|
||||
{ value: "ask", label: "settings.permissions.action.ask" },
|
||||
{ value: "deny", label: "settings.permissions.action.deny" },
|
||||
] as const
|
||||
|
||||
const ITEMS = [
|
||||
{
|
||||
id: "read",
|
||||
title: "settings.permissions.tool.read.title",
|
||||
description: "settings.permissions.tool.read.description",
|
||||
},
|
||||
{
|
||||
id: "edit",
|
||||
title: "settings.permissions.tool.edit.title",
|
||||
description: "settings.permissions.tool.edit.description",
|
||||
},
|
||||
{
|
||||
id: "glob",
|
||||
title: "settings.permissions.tool.glob.title",
|
||||
description: "settings.permissions.tool.glob.description",
|
||||
},
|
||||
{
|
||||
id: "grep",
|
||||
title: "settings.permissions.tool.grep.title",
|
||||
description: "settings.permissions.tool.grep.description",
|
||||
},
|
||||
{
|
||||
id: "list",
|
||||
title: "settings.permissions.tool.list.title",
|
||||
description: "settings.permissions.tool.list.description",
|
||||
},
|
||||
{
|
||||
id: "bash",
|
||||
title: "settings.permissions.tool.bash.title",
|
||||
description: "settings.permissions.tool.bash.description",
|
||||
},
|
||||
{
|
||||
id: "task",
|
||||
title: "settings.permissions.tool.task.title",
|
||||
description: "settings.permissions.tool.task.description",
|
||||
},
|
||||
{
|
||||
id: "skill",
|
||||
title: "settings.permissions.tool.skill.title",
|
||||
description: "settings.permissions.tool.skill.description",
|
||||
},
|
||||
{
|
||||
id: "lsp",
|
||||
title: "settings.permissions.tool.lsp.title",
|
||||
description: "settings.permissions.tool.lsp.description",
|
||||
},
|
||||
{
|
||||
id: "todoread",
|
||||
title: "settings.permissions.tool.todoread.title",
|
||||
description: "settings.permissions.tool.todoread.description",
|
||||
},
|
||||
{
|
||||
id: "todowrite",
|
||||
title: "settings.permissions.tool.todowrite.title",
|
||||
description: "settings.permissions.tool.todowrite.description",
|
||||
},
|
||||
{
|
||||
id: "webfetch",
|
||||
title: "settings.permissions.tool.webfetch.title",
|
||||
description: "settings.permissions.tool.webfetch.description",
|
||||
},
|
||||
{
|
||||
id: "websearch",
|
||||
title: "settings.permissions.tool.websearch.title",
|
||||
description: "settings.permissions.tool.websearch.description",
|
||||
},
|
||||
{
|
||||
id: "codesearch",
|
||||
title: "settings.permissions.tool.codesearch.title",
|
||||
description: "settings.permissions.tool.codesearch.description",
|
||||
},
|
||||
{
|
||||
id: "external_directory",
|
||||
title: "settings.permissions.tool.external_directory.title",
|
||||
description: "settings.permissions.tool.external_directory.description",
|
||||
},
|
||||
{
|
||||
id: "doom_loop",
|
||||
title: "settings.permissions.tool.doom_loop.title",
|
||||
description: "settings.permissions.tool.doom_loop.description",
|
||||
},
|
||||
] as const
|
||||
|
||||
const VALID_ACTIONS = new Set<PermissionAction>(["allow", "ask", "deny"])
|
||||
|
||||
function toMap(value: unknown): PermissionMap {
|
||||
if (value && typeof value === "object" && !Array.isArray(value)) return value as PermissionMap
|
||||
|
||||
const action = getAction(value)
|
||||
if (action) return { "*": action }
|
||||
|
||||
return {}
|
||||
}
|
||||
|
||||
function getAction(value: unknown): PermissionAction | undefined {
|
||||
if (typeof value === "string" && VALID_ACTIONS.has(value as PermissionAction)) return value as PermissionAction
|
||||
return
|
||||
}
|
||||
|
||||
function getRuleDefault(value: unknown): PermissionAction | undefined {
|
||||
const action = getAction(value)
|
||||
if (action) return action
|
||||
|
||||
if (!value || typeof value !== "object" || Array.isArray(value)) return
|
||||
|
||||
return getAction((value as Record<string, unknown>)["*"])
|
||||
}
|
||||
|
||||
export const SettingsPermissions: Component = () => {
|
||||
const globalSync = useGlobalSync()
|
||||
const language = useLanguage()
|
||||
|
||||
const actions = createMemo(
|
||||
(): Array<{ value: PermissionAction; label: string }> =>
|
||||
ACTIONS.map((action) => ({
|
||||
value: action.value,
|
||||
label: language.t(action.label),
|
||||
})),
|
||||
)
|
||||
|
||||
const permission = createMemo(() => {
|
||||
return toMap(globalSync.data.config.permission)
|
||||
})
|
||||
|
||||
const actionFor = (id: string): PermissionAction => {
|
||||
const value = permission()[id]
|
||||
const direct = getRuleDefault(value)
|
||||
if (direct) return direct
|
||||
|
||||
const wildcard = getRuleDefault(permission()["*"])
|
||||
if (wildcard) return wildcard
|
||||
|
||||
return "allow"
|
||||
}
|
||||
|
||||
const setPermission = async (id: string, action: PermissionAction) => {
|
||||
const before = globalSync.data.config.permission
|
||||
const map = toMap(before)
|
||||
const existing = map[id]
|
||||
|
||||
const nextValue =
|
||||
existing && typeof existing === "object" && !Array.isArray(existing) ? { ...existing, "*": action } : action
|
||||
|
||||
globalSync.set("config", "permission", { ...map, [id]: nextValue })
|
||||
globalSync.updateConfig({ permission: { [id]: nextValue } }).catch((err: unknown) => {
|
||||
globalSync.set("config", "permission", before)
|
||||
const message = err instanceof Error ? err.message : String(err)
|
||||
showToast({ title: language.t("settings.permissions.toast.updateFailed.title"), description: message })
|
||||
})
|
||||
}
|
||||
|
||||
return (
|
||||
<div class="flex flex-col h-full overflow-y-auto no-scrollbar">
|
||||
<div
|
||||
class="sticky top-0 z-10"
|
||||
style={{
|
||||
background:
|
||||
"linear-gradient(to bottom, var(--surface-raised-stronger-non-alpha) calc(100% - 24px), transparent)",
|
||||
}}
|
||||
>
|
||||
<div class="flex flex-col gap-1 p-8 max-w-[720px]">
|
||||
<h2 class="text-16-medium text-text-strong">{language.t("settings.permissions.title")}</h2>
|
||||
<p class="text-14-regular text-text-weak">{language.t("settings.permissions.description")}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col gap-6 p-8 pt-6 max-w-[720px]">
|
||||
<div class="flex flex-col gap-2">
|
||||
<h3 class="text-14-medium text-text-strong">{language.t("settings.permissions.section.tools")}</h3>
|
||||
<div class="border border-border-weak-base rounded-lg overflow-hidden">
|
||||
<For each={ITEMS}>
|
||||
{(item) => (
|
||||
<SettingsRow title={language.t(item.title)} description={language.t(item.description)}>
|
||||
<Select
|
||||
options={actions()}
|
||||
current={actions().find((o) => o.value === actionFor(item.id))}
|
||||
value={(o) => o.value}
|
||||
label={(o) => o.label}
|
||||
onSelect={(option) => option && setPermission(item.id, option.value)}
|
||||
variant="secondary"
|
||||
size="small"
|
||||
triggerVariant="settings"
|
||||
/>
|
||||
</SettingsRow>
|
||||
)}
|
||||
</For>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
interface SettingsRowProps {
|
||||
title: string
|
||||
description: string
|
||||
children: JSX.Element
|
||||
}
|
||||
|
||||
const SettingsRow: Component<SettingsRowProps> = (props) => {
|
||||
return (
|
||||
<div class="flex items-center justify-between gap-4 px-4 py-3 border-b border-border-weak-base last:border-none">
|
||||
<div class="flex flex-col gap-0.5">
|
||||
<span class="text-14-medium text-text-strong">{props.title}</span>
|
||||
<span class="text-12-regular text-text-weak">{props.description}</span>
|
||||
</div>
|
||||
<div class="flex-shrink-0">{props.children}</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
15
packages/app/src/components/settings-providers.tsx
Normal file
15
packages/app/src/components/settings-providers.tsx
Normal file
@@ -0,0 +1,15 @@
|
||||
import { Component } from "solid-js"
|
||||
import { useLanguage } from "@/context/language"
|
||||
|
||||
export const SettingsProviders: Component = () => {
|
||||
const language = useLanguage()
|
||||
|
||||
return (
|
||||
<div class="flex flex-col h-full overflow-y-auto">
|
||||
<div class="flex flex-col gap-6 p-6 max-w-[600px]">
|
||||
<h2 class="text-16-medium text-text-strong">{language.t("settings.providers.title")}</h2>
|
||||
<p class="text-14-regular text-text-weak">{language.t("settings.providers.description")}</p>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
import type { Ghostty, Terminal as Term, FitAddon } from "ghostty-web"
|
||||
import { ComponentProps, createEffect, createSignal, onCleanup, onMount, splitProps } from "solid-js"
|
||||
import { useSDK } from "@/context/sdk"
|
||||
import { monoFontFamily, useSettings } from "@/context/settings"
|
||||
import { SerializeAddon } from "@/addons/serialize"
|
||||
import { LocalPTY } from "@/context/terminal"
|
||||
import { resolveThemeVariant, useTheme, withAlpha, type HexColor } from "@opencode-ai/ui/theme"
|
||||
@@ -9,6 +10,7 @@ export interface TerminalProps extends ComponentProps<"div"> {
|
||||
pty: LocalPTY
|
||||
onSubmit?: () => void
|
||||
onCleanup?: (pty: LocalPTY) => void
|
||||
onConnect?: () => void
|
||||
onConnectError?: (error: unknown) => void
|
||||
}
|
||||
|
||||
@@ -36,9 +38,10 @@ const DEFAULT_TERMINAL_COLORS: Record<"light" | "dark", TerminalColors> = {
|
||||
|
||||
export const Terminal = (props: TerminalProps) => {
|
||||
const sdk = useSDK()
|
||||
const settings = useSettings()
|
||||
const theme = useTheme()
|
||||
let container!: HTMLDivElement
|
||||
const [local, others] = splitProps(props, ["pty", "class", "classList", "onConnectError"])
|
||||
const [local, others] = splitProps(props, ["pty", "class", "classList", "onConnect", "onConnectError"])
|
||||
let ws: WebSocket | undefined
|
||||
let term: Term | undefined
|
||||
let ghostty: Ghostty
|
||||
@@ -82,6 +85,14 @@ export const Terminal = (props: TerminalProps) => {
|
||||
setOption("theme", colors)
|
||||
})
|
||||
|
||||
createEffect(() => {
|
||||
const font = monoFontFamily(settings.appearance.font())
|
||||
if (!term) return
|
||||
const setOption = (term as unknown as { setOption?: (key: string, value: string) => void }).setOption
|
||||
if (!setOption) return
|
||||
setOption("fontFamily", font)
|
||||
})
|
||||
|
||||
const focusTerminal = () => {
|
||||
const t = term
|
||||
if (!t) return
|
||||
@@ -112,7 +123,7 @@ export const Terminal = (props: TerminalProps) => {
|
||||
cursorBlink: true,
|
||||
cursorStyle: "bar",
|
||||
fontSize: 14,
|
||||
fontFamily: "IBM Plex Mono, monospace",
|
||||
fontFamily: monoFontFamily(settings.appearance.font()),
|
||||
allowTransparency: true,
|
||||
theme: terminalColors(),
|
||||
scrollback: 10_000,
|
||||
@@ -231,7 +242,7 @@ export const Terminal = (props: TerminalProps) => {
|
||||
// console.log("Scroll position:", ydisp)
|
||||
// })
|
||||
socket.addEventListener("open", () => {
|
||||
console.log("WebSocket connected")
|
||||
local.onConnect?.()
|
||||
sdk.client.pty
|
||||
.update({
|
||||
ptyID: local.pty.id,
|
||||
@@ -246,15 +257,22 @@ export const Terminal = (props: TerminalProps) => {
|
||||
t.write(event.data)
|
||||
})
|
||||
socket.addEventListener("error", (error) => {
|
||||
if (disposed) return
|
||||
console.error("WebSocket error:", error)
|
||||
props.onConnectError?.(error)
|
||||
local.onConnectError?.(error)
|
||||
})
|
||||
socket.addEventListener("close", () => {
|
||||
console.log("WebSocket disconnected")
|
||||
socket.addEventListener("close", (event) => {
|
||||
if (disposed) return
|
||||
// Normal closure (code 1000) means PTY process exited - server event handles cleanup
|
||||
// For other codes (network issues, server restart), trigger error handler
|
||||
if (event.code !== 1000) {
|
||||
local.onConnectError?.(new Error(`WebSocket closed abnormally: ${event.code}`))
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
onCleanup(() => {
|
||||
disposed = true
|
||||
if (handleResize) {
|
||||
window.removeEventListener("resize", handleResize)
|
||||
}
|
||||
@@ -283,6 +301,7 @@ export const Terminal = (props: TerminalProps) => {
|
||||
ref={container}
|
||||
data-component="terminal"
|
||||
data-prevent-autofocus
|
||||
tabIndex={-1}
|
||||
style={{ "background-color": terminalColors().background }}
|
||||
classList={{
|
||||
...(local.classList ?? {}),
|
||||
|
||||
@@ -1,22 +1,28 @@
|
||||
import { createEffect, createMemo, Show } from "solid-js"
|
||||
import { IconButton } from "@opencode-ai/ui/icon-button"
|
||||
import { Icon } from "@opencode-ai/ui/icon"
|
||||
import { Button } from "@opencode-ai/ui/button"
|
||||
import { TooltipKeybind } from "@opencode-ai/ui/tooltip"
|
||||
import { useTheme } from "@opencode-ai/ui/theme"
|
||||
|
||||
import { useLayout } from "@/context/layout"
|
||||
import { usePlatform } from "@/context/platform"
|
||||
import { useCommand } from "@/context/command"
|
||||
import { useLanguage } from "@/context/language"
|
||||
|
||||
export function Titlebar() {
|
||||
const layout = useLayout()
|
||||
const platform = usePlatform()
|
||||
const command = useCommand()
|
||||
const language = useLanguage()
|
||||
const theme = useTheme()
|
||||
|
||||
const mac = createMemo(() => platform.platform === "desktop" && platform.os === "macos")
|
||||
const windows = createMemo(() => platform.platform === "desktop" && platform.os === "windows")
|
||||
const reserve = createMemo(
|
||||
() => platform.platform === "desktop" && (platform.os === "windows" || platform.os === "linux"),
|
||||
)
|
||||
const web = createMemo(() => platform.platform === "web")
|
||||
|
||||
const getWin = () => {
|
||||
if (platform.platform !== "desktop") return
|
||||
@@ -70,41 +76,76 @@ export function Titlebar() {
|
||||
}
|
||||
|
||||
return (
|
||||
<header class="h-10 shrink-0 bg-background-base flex items-center relative">
|
||||
<header class="h-10 shrink-0 bg-background-base flex items-center relative" data-tauri-drag-region>
|
||||
<div
|
||||
classList={{
|
||||
"flex items-center w-full min-w-0 pr-2": true,
|
||||
"flex items-center w-full min-w-0": true,
|
||||
"pl-2": !mac(),
|
||||
"pr-2": !windows(),
|
||||
}}
|
||||
onMouseDown={drag}
|
||||
data-tauri-drag-region
|
||||
>
|
||||
<Show when={mac()}>
|
||||
<div class="w-[72px] h-full shrink-0" data-tauri-drag-region />
|
||||
<div class="xl:hidden w-10 shrink-0 flex items-center justify-center">
|
||||
<IconButton
|
||||
icon="menu"
|
||||
variant="ghost"
|
||||
class="size-8 rounded-md"
|
||||
onClick={layout.mobileSidebar.toggle}
|
||||
aria-label={language.t("sidebar.menu.toggle")}
|
||||
/>
|
||||
</div>
|
||||
</Show>
|
||||
<Show when={!mac()}>
|
||||
<div class="xl:hidden w-[48px] shrink-0 flex items-center justify-center">
|
||||
<IconButton
|
||||
icon="menu"
|
||||
variant="ghost"
|
||||
class="size-8 rounded-md"
|
||||
onClick={layout.mobileSidebar.toggle}
|
||||
aria-label={language.t("sidebar.menu.toggle")}
|
||||
/>
|
||||
</div>
|
||||
</Show>
|
||||
<IconButton
|
||||
icon="menu"
|
||||
variant="ghost"
|
||||
class="xl:hidden size-8 rounded-md"
|
||||
onClick={layout.mobileSidebar.toggle}
|
||||
/>
|
||||
<TooltipKeybind
|
||||
class="hidden xl:flex shrink-0"
|
||||
class={web() ? "hidden xl:flex shrink-0 ml-14" : "hidden xl:flex shrink-0"}
|
||||
placement="bottom"
|
||||
title="Toggle sidebar"
|
||||
title={language.t("command.sidebar.toggle")}
|
||||
keybind={command.keybind("sidebar.toggle")}
|
||||
>
|
||||
<IconButton
|
||||
icon={layout.sidebar.opened() ? "layout-left" : "layout-right"}
|
||||
<Button
|
||||
variant="ghost"
|
||||
class="size-8 rounded-md"
|
||||
class="group/sidebar-toggle size-6 p-0"
|
||||
onClick={layout.sidebar.toggle}
|
||||
/>
|
||||
aria-label={language.t("command.sidebar.toggle")}
|
||||
aria-expanded={layout.sidebar.opened()}
|
||||
>
|
||||
<div class="relative flex items-center justify-center size-4 [&>*]:absolute [&>*]:inset-0">
|
||||
<Icon
|
||||
size="small"
|
||||
name={layout.sidebar.opened() ? "layout-left-full" : "layout-left"}
|
||||
class="group-hover/sidebar-toggle:hidden"
|
||||
/>
|
||||
<Icon size="small" name="layout-left-partial" class="hidden group-hover/sidebar-toggle:inline-block" />
|
||||
<Icon
|
||||
size="small"
|
||||
name={layout.sidebar.opened() ? "layout-left" : "layout-left-full"}
|
||||
class="hidden group-active/sidebar-toggle:inline-block"
|
||||
/>
|
||||
</div>
|
||||
</Button>
|
||||
</TooltipKeybind>
|
||||
<div id="opencode-titlebar-left" class="flex items-center gap-3 min-w-0 px-2" />
|
||||
<div id="opencode-titlebar-left" class="flex items-center gap-3 min-w-0 px-2" data-tauri-drag-region />
|
||||
<div class="flex-1 h-full" data-tauri-drag-region />
|
||||
<div id="opencode-titlebar-right" class="flex items-center gap-3 shrink-0" />
|
||||
<Show when={reserve()}>
|
||||
<div class="w-[120px] h-full shrink-0" data-tauri-drag-region />
|
||||
<div
|
||||
id="opencode-titlebar-right"
|
||||
class="flex items-center gap-3 shrink-0 flex-1 justify-end"
|
||||
data-tauri-drag-region
|
||||
/>
|
||||
<Show when={windows()}>
|
||||
<div data-tauri-decorum-tb class="flex flex-row" />
|
||||
</Show>
|
||||
</div>
|
||||
<div class="absolute inset-0 flex items-center justify-center pointer-events-none">
|
||||
|
||||
@@ -1,11 +1,29 @@
|
||||
import { createMemo, createSignal, onCleanup, onMount, Show, type Accessor } from "solid-js"
|
||||
import { createEffect, createMemo, createSignal, onCleanup, onMount, type Accessor } from "solid-js"
|
||||
import { createStore } from "solid-js/store"
|
||||
import { createSimpleContext } from "@opencode-ai/ui/context"
|
||||
import { useDialog } from "@opencode-ai/ui/context/dialog"
|
||||
import { Dialog } from "@opencode-ai/ui/dialog"
|
||||
import { List } from "@opencode-ai/ui/list"
|
||||
import { useLanguage } from "@/context/language"
|
||||
import { useSettings } from "@/context/settings"
|
||||
import { Persist, persisted } from "@/utils/persist"
|
||||
|
||||
const IS_MAC = typeof navigator === "object" && /(Mac|iPod|iPhone|iPad)/.test(navigator.platform)
|
||||
|
||||
const PALETTE_ID = "command.palette"
|
||||
const DEFAULT_PALETTE_KEYBIND = "mod+shift+p"
|
||||
const SUGGESTED_PREFIX = "suggested."
|
||||
|
||||
function actionId(id: string) {
|
||||
if (!id.startsWith(SUGGESTED_PREFIX)) return id
|
||||
return id.slice(SUGGESTED_PREFIX.length)
|
||||
}
|
||||
|
||||
function normalizeKey(key: string) {
|
||||
if (key === ",") return "comma"
|
||||
if (key === "+") return "plus"
|
||||
if (key === " ") return "space"
|
||||
return key.toLowerCase()
|
||||
}
|
||||
|
||||
export type KeybindConfig = string
|
||||
|
||||
export interface Keybind {
|
||||
@@ -29,6 +47,14 @@ export interface CommandOption {
|
||||
onHighlight?: () => (() => void) | void
|
||||
}
|
||||
|
||||
export type CommandCatalogItem = {
|
||||
title: string
|
||||
description?: string
|
||||
category?: string
|
||||
keybind?: KeybindConfig
|
||||
slash?: string
|
||||
}
|
||||
|
||||
export function parseKeybind(config: string): Keybind[] {
|
||||
if (!config || config === "none") return []
|
||||
|
||||
@@ -75,7 +101,7 @@ export function parseKeybind(config: string): Keybind[] {
|
||||
}
|
||||
|
||||
export function matchKeybind(keybinds: Keybind[], event: KeyboardEvent): boolean {
|
||||
const eventKey = event.key.toLowerCase()
|
||||
const eventKey = normalizeKey(event.key)
|
||||
|
||||
for (const kb of keybinds) {
|
||||
const keyMatch = kb.key === eventKey
|
||||
@@ -107,76 +133,45 @@ export function formatKeybind(config: string): string {
|
||||
if (kb.meta) parts.push(IS_MAC ? "⌘" : "Meta")
|
||||
|
||||
if (kb.key) {
|
||||
const displayKey = kb.key.length === 1 ? kb.key.toUpperCase() : kb.key.charAt(0).toUpperCase() + kb.key.slice(1)
|
||||
const keys: Record<string, string> = {
|
||||
arrowup: "↑",
|
||||
arrowdown: "↓",
|
||||
arrowleft: "←",
|
||||
arrowright: "→",
|
||||
comma: ",",
|
||||
plus: "+",
|
||||
space: "Space",
|
||||
}
|
||||
const key = kb.key.toLowerCase()
|
||||
const displayKey = keys[key] ?? (key.length === 1 ? key.toUpperCase() : key.charAt(0).toUpperCase() + key.slice(1))
|
||||
parts.push(displayKey)
|
||||
}
|
||||
|
||||
return IS_MAC ? parts.join("") : parts.join("+")
|
||||
}
|
||||
|
||||
function DialogCommand(props: { options: CommandOption[] }) {
|
||||
const dialog = useDialog()
|
||||
let cleanup: (() => void) | void
|
||||
let committed = false
|
||||
|
||||
const handleMove = (option: CommandOption | undefined) => {
|
||||
cleanup?.()
|
||||
cleanup = option?.onHighlight?.()
|
||||
}
|
||||
|
||||
const handleSelect = (option: CommandOption | undefined) => {
|
||||
if (option) {
|
||||
committed = true
|
||||
cleanup = undefined
|
||||
dialog.close()
|
||||
option.onSelect?.("palette")
|
||||
}
|
||||
}
|
||||
|
||||
onCleanup(() => {
|
||||
if (!committed) {
|
||||
cleanup?.()
|
||||
}
|
||||
})
|
||||
|
||||
return (
|
||||
<Dialog title="Commands">
|
||||
<List
|
||||
search={{ placeholder: "Search commands", autofocus: true }}
|
||||
emptyMessage="No commands found"
|
||||
items={() => props.options.filter((x) => !x.id.startsWith("suggested.") || !x.disabled)}
|
||||
key={(x) => x?.id}
|
||||
filterKeys={["title", "description", "category"]}
|
||||
groupBy={(x) => x.category ?? ""}
|
||||
onMove={handleMove}
|
||||
onSelect={handleSelect}
|
||||
>
|
||||
{(option) => (
|
||||
<div class="w-full flex items-center justify-between gap-4">
|
||||
<div class="flex items-center gap-2 min-w-0">
|
||||
<span class="text-14-regular text-text-strong whitespace-nowrap">{option.title}</span>
|
||||
<Show when={option.description}>
|
||||
<span class="text-14-regular text-text-weak truncate">{option.description}</span>
|
||||
</Show>
|
||||
</div>
|
||||
<Show when={option.keybind}>
|
||||
<span class="text-12-regular text-text-subtle shrink-0">{formatKeybind(option.keybind!)}</span>
|
||||
</Show>
|
||||
</div>
|
||||
)}
|
||||
</List>
|
||||
</Dialog>
|
||||
)
|
||||
}
|
||||
|
||||
export const { use: useCommand, provider: CommandProvider } = createSimpleContext({
|
||||
name: "Command",
|
||||
init: () => {
|
||||
const dialog = useDialog()
|
||||
const settings = useSettings()
|
||||
const language = useLanguage()
|
||||
const [registrations, setRegistrations] = createSignal<Accessor<CommandOption[]>[]>([])
|
||||
const [suspendCount, setSuspendCount] = createSignal(0)
|
||||
const dialog = useDialog()
|
||||
|
||||
const options = createMemo(() => {
|
||||
const [catalog, setCatalog, _, catalogReady] = persisted(
|
||||
Persist.global("command.catalog.v1"),
|
||||
createStore<Record<string, CommandCatalogItem>>({}),
|
||||
)
|
||||
|
||||
const bind = (id: string, def: KeybindConfig | undefined) => {
|
||||
const custom = settings.keybinds.get(actionId(id))
|
||||
const config = custom ?? def
|
||||
if (!config || config === "none") return
|
||||
return config
|
||||
}
|
||||
|
||||
const registered = createMemo(() => {
|
||||
const seen = new Set<string>()
|
||||
const all: CommandOption[] = []
|
||||
|
||||
@@ -188,30 +183,63 @@ export const { use: useCommand, provider: CommandProvider } = createSimpleContex
|
||||
}
|
||||
}
|
||||
|
||||
const suggested = all.filter((x) => x.suggested && !x.disabled)
|
||||
return all
|
||||
})
|
||||
|
||||
createEffect(() => {
|
||||
if (!catalogReady()) return
|
||||
|
||||
for (const opt of registered()) {
|
||||
const id = actionId(opt.id)
|
||||
setCatalog(id, {
|
||||
title: opt.title,
|
||||
description: opt.description,
|
||||
category: opt.category,
|
||||
keybind: opt.keybind,
|
||||
slash: opt.slash,
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
const catalogOptions = createMemo(() => Object.entries(catalog).map(([id, meta]) => ({ id, ...meta })))
|
||||
|
||||
const options = createMemo(() => {
|
||||
const resolved = registered().map((opt) => ({
|
||||
...opt,
|
||||
keybind: bind(opt.id, opt.keybind),
|
||||
}))
|
||||
|
||||
const suggested = resolved.filter((x) => x.suggested && !x.disabled)
|
||||
|
||||
return [
|
||||
...suggested.map((x) => ({
|
||||
...x,
|
||||
id: "suggested." + x.id,
|
||||
category: "Suggested",
|
||||
id: SUGGESTED_PREFIX + x.id,
|
||||
category: language.t("command.category.suggested"),
|
||||
})),
|
||||
...all,
|
||||
...resolved,
|
||||
]
|
||||
})
|
||||
|
||||
const suspended = () => suspendCount() > 0
|
||||
|
||||
const showPalette = () => {
|
||||
if (!dialog.active) {
|
||||
dialog.show(() => <DialogCommand options={options().filter((x) => !x.disabled)} />)
|
||||
const run = (id: string, source?: "palette" | "keybind" | "slash") => {
|
||||
for (const option of options()) {
|
||||
if (option.id === id || option.id === "suggested." + id) {
|
||||
option.onSelect?.(source)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const handleKeyDown = (event: KeyboardEvent) => {
|
||||
if (suspended()) return
|
||||
const showPalette = () => {
|
||||
run("file.open", "palette")
|
||||
}
|
||||
|
||||
const paletteKeybinds = parseKeybind("mod+shift+p")
|
||||
const handleKeyDown = (event: KeyboardEvent) => {
|
||||
if (suspended() || dialog.active) return
|
||||
|
||||
const paletteKeybinds = parseKeybind(settings.keybinds.get(PALETTE_ID) ?? DEFAULT_PALETTE_KEYBIND)
|
||||
if (matchKeybind(paletteKeybinds, event)) {
|
||||
event.preventDefault()
|
||||
showPalette()
|
||||
@@ -248,23 +276,30 @@ export const { use: useCommand, provider: CommandProvider } = createSimpleContex
|
||||
})
|
||||
},
|
||||
trigger(id: string, source?: "palette" | "keybind" | "slash") {
|
||||
for (const option of options()) {
|
||||
if (option.id === id || option.id === "suggested." + id) {
|
||||
option.onSelect?.(source)
|
||||
return
|
||||
}
|
||||
}
|
||||
run(id, source)
|
||||
},
|
||||
keybind(id: string) {
|
||||
const option = options().find((x) => x.id === id || x.id === "suggested." + id)
|
||||
if (!option?.keybind) return ""
|
||||
return formatKeybind(option.keybind)
|
||||
if (id === PALETTE_ID) {
|
||||
return formatKeybind(settings.keybinds.get(PALETTE_ID) ?? DEFAULT_PALETTE_KEYBIND)
|
||||
}
|
||||
|
||||
const base = actionId(id)
|
||||
const option = options().find((x) => actionId(x.id) === base)
|
||||
if (option?.keybind) return formatKeybind(option.keybind)
|
||||
|
||||
const meta = catalog[base]
|
||||
const config = bind(base, meta?.keybind)
|
||||
if (!config) return ""
|
||||
return formatKeybind(config)
|
||||
},
|
||||
show: showPalette,
|
||||
keybinds(enabled: boolean) {
|
||||
setSuspendCount((count) => count + (enabled ? -1 : 1))
|
||||
},
|
||||
suspended,
|
||||
get catalog() {
|
||||
return catalogOptions()
|
||||
},
|
||||
get options() {
|
||||
return options()
|
||||
},
|
||||
|
||||
@@ -7,6 +7,7 @@ import { useParams } from "@solidjs/router"
|
||||
import { getFilename } from "@opencode-ai/util/path"
|
||||
import { useSDK } from "./sdk"
|
||||
import { useSync } from "./sync"
|
||||
import { useLanguage } from "@/context/language"
|
||||
import { Persist, persisted } from "@/utils/persist"
|
||||
|
||||
export type FileSelection = {
|
||||
@@ -186,6 +187,7 @@ export const { use: useFile, provider: FileProvider } = createSimpleContext({
|
||||
const sdk = useSDK()
|
||||
const sync = useSync()
|
||||
const params = useParams()
|
||||
const language = useLanguage()
|
||||
|
||||
const directory = createMemo(() => sync.data.path.directory)
|
||||
|
||||
@@ -193,7 +195,20 @@ export const { use: useFile, provider: FileProvider } = createSimpleContext({
|
||||
const root = directory()
|
||||
const prefix = root.endsWith("/") ? root : root + "/"
|
||||
|
||||
let path = stripQueryAndHash(stripFileProtocol(input))
|
||||
let path = input
|
||||
|
||||
// Only strip protocol and decode if it's a file URI
|
||||
if (path.startsWith("file://")) {
|
||||
const raw = stripQueryAndHash(stripFileProtocol(path))
|
||||
try {
|
||||
// Attempt to treat as a standard URI
|
||||
path = decodeURIComponent(raw)
|
||||
} catch {
|
||||
// Fallback for legacy paths that might contain invalid URI sequences (e.g. "100%")
|
||||
// In this case, we treat the path as raw, but still strip the protocol
|
||||
path = raw
|
||||
}
|
||||
}
|
||||
|
||||
if (path.startsWith(prefix)) {
|
||||
path = path.slice(prefix.length)
|
||||
@@ -216,7 +231,8 @@ export const { use: useFile, provider: FileProvider } = createSimpleContext({
|
||||
|
||||
function tab(input: string) {
|
||||
const path = normalize(input)
|
||||
return `file://${path}`
|
||||
const encoded = path.split("/").map(encodeURIComponent).join("/")
|
||||
return `file://${encoded}`
|
||||
}
|
||||
|
||||
function pathFromTab(tabValue: string) {
|
||||
@@ -323,7 +339,7 @@ export const { use: useFile, provider: FileProvider } = createSimpleContext({
|
||||
)
|
||||
showToast({
|
||||
variant: "error",
|
||||
title: "Failed to load file",
|
||||
title: language.t("toast.file.loadFailed.title"),
|
||||
description: e.message,
|
||||
})
|
||||
})
|
||||
|
||||
@@ -19,15 +19,30 @@ import {
|
||||
type QuestionRequest,
|
||||
createOpencodeClient,
|
||||
} from "@opencode-ai/sdk/v2/client"
|
||||
import { createStore, produce, reconcile } from "solid-js/store"
|
||||
import { createStore, produce, reconcile, type SetStoreFunction, type Store } from "solid-js/store"
|
||||
import { Binary } from "@opencode-ai/util/binary"
|
||||
import { retry } from "@opencode-ai/util/retry"
|
||||
import { useGlobalSDK } from "./global-sdk"
|
||||
import { ErrorPage, type InitError } from "../pages/error"
|
||||
import { batch, createContext, useContext, onCleanup, onMount, type ParentProps, Switch, Match } from "solid-js"
|
||||
import {
|
||||
batch,
|
||||
createContext,
|
||||
createEffect,
|
||||
getOwner,
|
||||
runWithOwner,
|
||||
useContext,
|
||||
onCleanup,
|
||||
onMount,
|
||||
type Accessor,
|
||||
type ParentProps,
|
||||
Switch,
|
||||
Match,
|
||||
} from "solid-js"
|
||||
import { showToast } from "@opencode-ai/ui/toast"
|
||||
import { getFilename } from "@opencode-ai/util/path"
|
||||
import { usePlatform } from "./platform"
|
||||
import { useLanguage } from "@/context/language"
|
||||
import { Persist, persisted } from "@/utils/persist"
|
||||
|
||||
type State = {
|
||||
status: "loading" | "partial" | "complete"
|
||||
@@ -68,9 +83,23 @@ type State = {
|
||||
}
|
||||
}
|
||||
|
||||
type VcsCache = {
|
||||
store: Store<{ value: VcsInfo | undefined }>
|
||||
setStore: SetStoreFunction<{ value: VcsInfo | undefined }>
|
||||
ready: Accessor<boolean>
|
||||
}
|
||||
|
||||
type ChildOptions = {
|
||||
bootstrap?: boolean
|
||||
}
|
||||
|
||||
function createGlobalSync() {
|
||||
const globalSDK = useGlobalSDK()
|
||||
const platform = usePlatform()
|
||||
const language = useLanguage()
|
||||
const owner = getOwner()
|
||||
if (!owner) throw new Error("GlobalSync must be created within owner")
|
||||
const vcsCache = new Map<string, VcsCache>()
|
||||
const [globalStore, setGlobalStore] = createStore<{
|
||||
ready: boolean
|
||||
error?: InitError
|
||||
@@ -78,50 +107,98 @@ function createGlobalSync() {
|
||||
project: Project[]
|
||||
provider: ProviderListResponse
|
||||
provider_auth: ProviderAuthResponse
|
||||
config: Config
|
||||
reload: undefined | "pending" | "complete"
|
||||
}>({
|
||||
ready: false,
|
||||
path: { state: "", config: "", worktree: "", directory: "", home: "" },
|
||||
project: [],
|
||||
provider: { all: [], connected: [], default: {} },
|
||||
provider_auth: {},
|
||||
config: {},
|
||||
reload: undefined,
|
||||
})
|
||||
let bootstrapQueue: string[] = []
|
||||
|
||||
createEffect(async () => {
|
||||
if (globalStore.reload !== "complete") return
|
||||
if (bootstrapQueue.length) {
|
||||
for (const directory of bootstrapQueue) {
|
||||
bootstrapInstance(directory)
|
||||
}
|
||||
bootstrap()
|
||||
}
|
||||
bootstrapQueue = []
|
||||
setGlobalStore("reload", undefined)
|
||||
})
|
||||
|
||||
const children: Record<string, ReturnType<typeof createStore<State>>> = {}
|
||||
function child(directory: string) {
|
||||
const children: Record<string, [Store<State>, SetStoreFunction<State>]> = {}
|
||||
const booting = new Map<string, Promise<void>>()
|
||||
const sessionLoads = new Map<string, Promise<void>>()
|
||||
const sessionMeta = new Map<string, { limit: number }>()
|
||||
|
||||
function ensureChild(directory: string) {
|
||||
if (!directory) console.error("No directory provided")
|
||||
if (!children[directory]) {
|
||||
children[directory] = createStore<State>({
|
||||
project: "",
|
||||
provider: { all: [], connected: [], default: {} },
|
||||
config: {},
|
||||
path: { state: "", config: "", worktree: "", directory: "", home: "" },
|
||||
status: "loading" as const,
|
||||
agent: [],
|
||||
command: [],
|
||||
session: [],
|
||||
sessionTotal: 0,
|
||||
session_status: {},
|
||||
session_diff: {},
|
||||
todo: {},
|
||||
permission: {},
|
||||
question: {},
|
||||
mcp: {},
|
||||
lsp: [],
|
||||
vcs: undefined,
|
||||
limit: 5,
|
||||
message: {},
|
||||
part: {},
|
||||
})
|
||||
bootstrapInstance(directory)
|
||||
const cache = runWithOwner(owner, () =>
|
||||
persisted(
|
||||
Persist.workspace(directory, "vcs", ["vcs.v1"]),
|
||||
createStore({ value: undefined as VcsInfo | undefined }),
|
||||
),
|
||||
)
|
||||
if (!cache) throw new Error("Failed to create persisted cache")
|
||||
vcsCache.set(directory, { store: cache[0], setStore: cache[1], ready: cache[3] })
|
||||
|
||||
const init = () => {
|
||||
children[directory] = createStore<State>({
|
||||
project: "",
|
||||
provider: { all: [], connected: [], default: {} },
|
||||
config: {},
|
||||
path: { state: "", config: "", worktree: "", directory: "", home: "" },
|
||||
status: "loading" as const,
|
||||
agent: [],
|
||||
command: [],
|
||||
session: [],
|
||||
sessionTotal: 0,
|
||||
session_status: {},
|
||||
session_diff: {},
|
||||
todo: {},
|
||||
permission: {},
|
||||
question: {},
|
||||
mcp: {},
|
||||
lsp: [],
|
||||
vcs: cache[0].value,
|
||||
limit: 5,
|
||||
message: {},
|
||||
part: {},
|
||||
})
|
||||
}
|
||||
|
||||
runWithOwner(owner, init)
|
||||
}
|
||||
return children[directory]
|
||||
const childStore = children[directory]
|
||||
if (!childStore) throw new Error("Failed to create store")
|
||||
return childStore
|
||||
}
|
||||
|
||||
function child(directory: string, options: ChildOptions = {}) {
|
||||
const childStore = ensureChild(directory)
|
||||
const shouldBootstrap = options.bootstrap ?? true
|
||||
if (shouldBootstrap && childStore[0].status === "loading") {
|
||||
void bootstrapInstance(directory)
|
||||
}
|
||||
return childStore
|
||||
}
|
||||
|
||||
async function loadSessions(directory: string) {
|
||||
const [store, setStore] = child(directory)
|
||||
const limit = store.limit
|
||||
const pending = sessionLoads.get(directory)
|
||||
if (pending) return pending
|
||||
|
||||
return globalSDK.client.session
|
||||
const [store, setStore] = child(directory, { bootstrap: false })
|
||||
const meta = sessionMeta.get(directory)
|
||||
if (meta && meta.limit >= store.limit) return
|
||||
|
||||
const promise = globalSDK.client.session
|
||||
.list({ directory, roots: true })
|
||||
.then((x) => {
|
||||
const nonArchived = (x.data ?? [])
|
||||
@@ -130,9 +207,15 @@ function createGlobalSync() {
|
||||
.slice()
|
||||
.sort((a, b) => a.id.localeCompare(b.id))
|
||||
|
||||
// Read the current limit at resolve-time so callers that bump the limit while
|
||||
// a request is in-flight still get the expanded result.
|
||||
const limit = store.limit
|
||||
|
||||
const sandboxWorkspace = globalStore.project.some((p) => (p.sandboxes ?? []).includes(directory))
|
||||
if (sandboxWorkspace) {
|
||||
setStore("sessionTotal", nonArchived.length)
|
||||
setStore("session", reconcile(nonArchived, { key: "id" }))
|
||||
sessionMeta.set(directory, { limit })
|
||||
return
|
||||
}
|
||||
|
||||
@@ -146,123 +229,164 @@ function createGlobalSync() {
|
||||
// Store total session count (used for "load more" pagination)
|
||||
setStore("sessionTotal", nonArchived.length)
|
||||
setStore("session", reconcile(sessions, { key: "id" }))
|
||||
sessionMeta.set(directory, { limit })
|
||||
})
|
||||
.catch((err) => {
|
||||
console.error("Failed to load sessions", err)
|
||||
const project = getFilename(directory)
|
||||
showToast({ title: `Failed to load sessions for ${project}`, description: err.message })
|
||||
showToast({ title: language.t("toast.session.listFailed.title", { project }), description: err.message })
|
||||
})
|
||||
|
||||
sessionLoads.set(directory, promise)
|
||||
promise.finally(() => {
|
||||
sessionLoads.delete(directory)
|
||||
})
|
||||
return promise
|
||||
}
|
||||
|
||||
async function bootstrapInstance(directory: string) {
|
||||
if (!directory) return
|
||||
const [store, setStore] = child(directory)
|
||||
const sdk = createOpencodeClient({
|
||||
baseUrl: globalSDK.url,
|
||||
fetch: platform.fetch,
|
||||
directory,
|
||||
throwOnError: true,
|
||||
})
|
||||
const pending = booting.get(directory)
|
||||
if (pending) return pending
|
||||
|
||||
const blockingRequests = {
|
||||
project: () => sdk.project.current().then((x) => setStore("project", x.data!.id)),
|
||||
provider: () =>
|
||||
sdk.provider.list().then((x) => {
|
||||
const data = x.data!
|
||||
setStore("provider", {
|
||||
...data,
|
||||
all: data.all.map((provider) => ({
|
||||
...provider,
|
||||
models: Object.fromEntries(
|
||||
Object.entries(provider.models).filter(([, info]) => info.status !== "deprecated"),
|
||||
),
|
||||
})),
|
||||
const promise = (async () => {
|
||||
const [store, setStore] = ensureChild(directory)
|
||||
const cache = vcsCache.get(directory)
|
||||
if (!cache) return
|
||||
const sdk = createOpencodeClient({
|
||||
baseUrl: globalSDK.url,
|
||||
fetch: platform.fetch,
|
||||
directory,
|
||||
throwOnError: true,
|
||||
})
|
||||
|
||||
setStore("status", "loading")
|
||||
|
||||
createEffect(() => {
|
||||
if (!cache.ready()) return
|
||||
const cached = cache.store.value
|
||||
if (!cached?.branch) return
|
||||
setStore("vcs", (value) => value ?? cached)
|
||||
})
|
||||
|
||||
const blockingRequests = {
|
||||
project: () => sdk.project.current().then((x) => setStore("project", x.data!.id)),
|
||||
provider: () =>
|
||||
sdk.provider.list().then((x) => {
|
||||
const data = x.data!
|
||||
setStore("provider", {
|
||||
...data,
|
||||
all: data.all.map((provider) => ({
|
||||
...provider,
|
||||
models: Object.fromEntries(
|
||||
Object.entries(provider.models).filter(([, info]) => info.status !== "deprecated"),
|
||||
),
|
||||
})),
|
||||
})
|
||||
}),
|
||||
agent: () => sdk.app.agents().then((x) => setStore("agent", x.data ?? [])),
|
||||
config: () => sdk.config.get().then((x) => setStore("config", x.data!)),
|
||||
}
|
||||
|
||||
try {
|
||||
await Promise.all(Object.values(blockingRequests).map((p) => retry(p)))
|
||||
} catch (err) {
|
||||
console.error("Failed to bootstrap instance", err)
|
||||
const project = getFilename(directory)
|
||||
const message = err instanceof Error ? err.message : String(err)
|
||||
showToast({ title: `Failed to reload ${project}`, description: message })
|
||||
setStore("status", "partial")
|
||||
return
|
||||
}
|
||||
|
||||
if (store.status !== "complete") setStore("status", "partial")
|
||||
|
||||
Promise.all([
|
||||
sdk.path.get().then((x) => setStore("path", x.data!)),
|
||||
sdk.command.list().then((x) => setStore("command", x.data ?? [])),
|
||||
sdk.session.status().then((x) => setStore("session_status", x.data!)),
|
||||
loadSessions(directory),
|
||||
sdk.mcp.status().then((x) => setStore("mcp", x.data!)),
|
||||
sdk.lsp.status().then((x) => setStore("lsp", x.data!)),
|
||||
sdk.vcs.get().then((x) => {
|
||||
const next = x.data ?? store.vcs
|
||||
setStore("vcs", next)
|
||||
if (next?.branch) cache.setStore("value", next)
|
||||
}),
|
||||
sdk.permission.list().then((x) => {
|
||||
const grouped: Record<string, PermissionRequest[]> = {}
|
||||
for (const perm of x.data ?? []) {
|
||||
if (!perm?.id || !perm.sessionID) continue
|
||||
const existing = grouped[perm.sessionID]
|
||||
if (existing) {
|
||||
existing.push(perm)
|
||||
continue
|
||||
}
|
||||
grouped[perm.sessionID] = [perm]
|
||||
}
|
||||
|
||||
batch(() => {
|
||||
for (const sessionID of Object.keys(store.permission)) {
|
||||
if (grouped[sessionID]) continue
|
||||
setStore("permission", sessionID, [])
|
||||
}
|
||||
for (const [sessionID, permissions] of Object.entries(grouped)) {
|
||||
setStore(
|
||||
"permission",
|
||||
sessionID,
|
||||
reconcile(
|
||||
permissions
|
||||
.filter((p) => !!p?.id)
|
||||
.slice()
|
||||
.sort((a, b) => a.id.localeCompare(b.id)),
|
||||
{ key: "id" },
|
||||
),
|
||||
)
|
||||
}
|
||||
})
|
||||
}),
|
||||
agent: () => sdk.app.agents().then((x) => setStore("agent", x.data ?? [])),
|
||||
config: () => sdk.config.get().then((x) => setStore("config", x.data!)),
|
||||
}
|
||||
await Promise.all(Object.values(blockingRequests).map((p) => retry(p).catch((e) => setGlobalStore("error", e))))
|
||||
.then(() => {
|
||||
if (store.status !== "complete") setStore("status", "partial")
|
||||
// non-blocking
|
||||
Promise.all([
|
||||
sdk.path.get().then((x) => setStore("path", x.data!)),
|
||||
sdk.command.list().then((x) => setStore("command", x.data ?? [])),
|
||||
sdk.session.status().then((x) => setStore("session_status", x.data!)),
|
||||
loadSessions(directory),
|
||||
sdk.mcp.status().then((x) => setStore("mcp", x.data!)),
|
||||
sdk.lsp.status().then((x) => setStore("lsp", x.data!)),
|
||||
sdk.vcs.get().then((x) => setStore("vcs", x.data)),
|
||||
sdk.permission.list().then((x) => {
|
||||
const grouped: Record<string, PermissionRequest[]> = {}
|
||||
for (const perm of x.data ?? []) {
|
||||
if (!perm?.id || !perm.sessionID) continue
|
||||
const existing = grouped[perm.sessionID]
|
||||
if (existing) {
|
||||
existing.push(perm)
|
||||
continue
|
||||
}
|
||||
grouped[perm.sessionID] = [perm]
|
||||
sdk.question.list().then((x) => {
|
||||
const grouped: Record<string, QuestionRequest[]> = {}
|
||||
for (const question of x.data ?? []) {
|
||||
if (!question?.id || !question.sessionID) continue
|
||||
const existing = grouped[question.sessionID]
|
||||
if (existing) {
|
||||
existing.push(question)
|
||||
continue
|
||||
}
|
||||
grouped[question.sessionID] = [question]
|
||||
}
|
||||
|
||||
batch(() => {
|
||||
for (const sessionID of Object.keys(store.permission)) {
|
||||
if (grouped[sessionID]) continue
|
||||
setStore("permission", sessionID, [])
|
||||
}
|
||||
for (const [sessionID, permissions] of Object.entries(grouped)) {
|
||||
setStore(
|
||||
"permission",
|
||||
sessionID,
|
||||
reconcile(
|
||||
permissions
|
||||
.filter((p) => !!p?.id)
|
||||
.slice()
|
||||
.sort((a, b) => a.id.localeCompare(b.id)),
|
||||
{ key: "id" },
|
||||
),
|
||||
)
|
||||
}
|
||||
})
|
||||
}),
|
||||
sdk.question.list().then((x) => {
|
||||
const grouped: Record<string, QuestionRequest[]> = {}
|
||||
for (const question of x.data ?? []) {
|
||||
if (!question?.id || !question.sessionID) continue
|
||||
const existing = grouped[question.sessionID]
|
||||
if (existing) {
|
||||
existing.push(question)
|
||||
continue
|
||||
}
|
||||
grouped[question.sessionID] = [question]
|
||||
batch(() => {
|
||||
for (const sessionID of Object.keys(store.question)) {
|
||||
if (grouped[sessionID]) continue
|
||||
setStore("question", sessionID, [])
|
||||
}
|
||||
|
||||
batch(() => {
|
||||
for (const sessionID of Object.keys(store.question)) {
|
||||
if (grouped[sessionID]) continue
|
||||
setStore("question", sessionID, [])
|
||||
}
|
||||
for (const [sessionID, questions] of Object.entries(grouped)) {
|
||||
setStore(
|
||||
"question",
|
||||
sessionID,
|
||||
reconcile(
|
||||
questions
|
||||
.filter((q) => !!q?.id)
|
||||
.slice()
|
||||
.sort((a, b) => a.id.localeCompare(b.id)),
|
||||
{ key: "id" },
|
||||
),
|
||||
)
|
||||
}
|
||||
})
|
||||
}),
|
||||
]).then(() => {
|
||||
setStore("status", "complete")
|
||||
})
|
||||
for (const [sessionID, questions] of Object.entries(grouped)) {
|
||||
setStore(
|
||||
"question",
|
||||
sessionID,
|
||||
reconcile(
|
||||
questions
|
||||
.filter((q) => !!q?.id)
|
||||
.slice()
|
||||
.sort((a, b) => a.id.localeCompare(b.id)),
|
||||
{ key: "id" },
|
||||
),
|
||||
)
|
||||
}
|
||||
})
|
||||
}),
|
||||
]).then(() => {
|
||||
setStore("status", "complete")
|
||||
})
|
||||
.catch((e) => setGlobalStore("error", e))
|
||||
})()
|
||||
|
||||
booting.set(directory, promise)
|
||||
promise.finally(() => {
|
||||
booting.delete(directory)
|
||||
})
|
||||
return promise
|
||||
}
|
||||
|
||||
const unsub = globalSDK.event.listen((e) => {
|
||||
@@ -272,6 +396,7 @@ function createGlobalSync() {
|
||||
if (directory === "global") {
|
||||
switch (event?.type) {
|
||||
case "global.disposed": {
|
||||
if (globalStore.reload) return
|
||||
bootstrap()
|
||||
break
|
||||
}
|
||||
@@ -293,12 +418,36 @@ function createGlobalSync() {
|
||||
return
|
||||
}
|
||||
|
||||
const [store, setStore] = child(directory)
|
||||
const existing = children[directory]
|
||||
if (!existing) return
|
||||
|
||||
const [store, setStore] = existing
|
||||
switch (event.type) {
|
||||
case "server.instance.disposed": {
|
||||
if (globalStore.reload) {
|
||||
bootstrapQueue.push(directory)
|
||||
return
|
||||
}
|
||||
bootstrapInstance(directory)
|
||||
break
|
||||
}
|
||||
case "session.created": {
|
||||
const result = Binary.search(store.session, event.properties.info.id, (s) => s.id)
|
||||
if (result.found) {
|
||||
setStore("session", result.index, reconcile(event.properties.info))
|
||||
break
|
||||
}
|
||||
setStore(
|
||||
"session",
|
||||
produce((draft) => {
|
||||
draft.splice(result.index, 0, event.properties.info)
|
||||
}),
|
||||
)
|
||||
if (!event.properties.info.parentID) {
|
||||
setStore("sessionTotal", store.sessionTotal + 1)
|
||||
}
|
||||
break
|
||||
}
|
||||
case "session.updated": {
|
||||
const result = Binary.search(store.session, event.properties.info.id, (s) => s.id)
|
||||
if (event.properties.info.time.archived) {
|
||||
@@ -310,6 +459,8 @@ function createGlobalSync() {
|
||||
}),
|
||||
)
|
||||
}
|
||||
if (event.properties.info.parentID) break
|
||||
setStore("sessionTotal", (value) => Math.max(0, value - 1))
|
||||
break
|
||||
}
|
||||
if (result.found) {
|
||||
@@ -406,7 +557,10 @@ function createGlobalSync() {
|
||||
break
|
||||
}
|
||||
case "vcs.branch.updated": {
|
||||
setStore("vcs", { branch: event.properties.branch })
|
||||
const next = { branch: event.properties.branch }
|
||||
setStore("vcs", next)
|
||||
const cache = vcsCache.get(directory)
|
||||
if (cache) cache.setStore("value", next)
|
||||
break
|
||||
}
|
||||
case "permission.asked": {
|
||||
@@ -504,10 +658,7 @@ function createGlobalSync() {
|
||||
.then((x) => x.data)
|
||||
.catch(() => undefined)
|
||||
if (!health?.healthy) {
|
||||
setGlobalStore(
|
||||
"error",
|
||||
new Error(`Could not connect to server. Is there a server running at \`${globalSDK.url}\`?`),
|
||||
)
|
||||
setGlobalStore("error", new Error(language.t("error.globalSync.connectFailed", { url: globalSDK.url })))
|
||||
return
|
||||
}
|
||||
|
||||
@@ -517,6 +668,11 @@ function createGlobalSync() {
|
||||
setGlobalStore("path", x.data!)
|
||||
}),
|
||||
),
|
||||
retry(() =>
|
||||
globalSDK.client.config.get().then((x) => {
|
||||
setGlobalStore("config", x.data!)
|
||||
}),
|
||||
),
|
||||
retry(() =>
|
||||
globalSDK.client.project.list().then(async (x) => {
|
||||
const projects = (x.data ?? [])
|
||||
@@ -557,6 +713,7 @@ function createGlobalSync() {
|
||||
|
||||
return {
|
||||
data: globalStore,
|
||||
set: setGlobalStore,
|
||||
get ready() {
|
||||
return globalStore.ready
|
||||
},
|
||||
@@ -565,6 +722,14 @@ function createGlobalSync() {
|
||||
},
|
||||
child,
|
||||
bootstrap,
|
||||
updateConfig: async (config: Config) => {
|
||||
setGlobalStore("reload", "pending")
|
||||
const response = await globalSDK.client.config.update({ config })
|
||||
setTimeout(() => {
|
||||
setGlobalStore("reload", "complete")
|
||||
}, 1000)
|
||||
return response
|
||||
},
|
||||
project: {
|
||||
loadSessions,
|
||||
},
|
||||
|
||||
138
packages/app/src/context/language.tsx
Normal file
138
packages/app/src/context/language.tsx
Normal file
@@ -0,0 +1,138 @@
|
||||
import * as i18n from "@solid-primitives/i18n"
|
||||
import { createEffect, createMemo } from "solid-js"
|
||||
import { createStore } from "solid-js/store"
|
||||
import { createSimpleContext } from "@opencode-ai/ui/context"
|
||||
import { Persist, persisted } from "@/utils/persist"
|
||||
import { dict as en } from "@/i18n/en"
|
||||
import { dict as zh } from "@/i18n/zh"
|
||||
import { dict as zht } from "@/i18n/zht"
|
||||
import { dict as ko } from "@/i18n/ko"
|
||||
import { dict as de } from "@/i18n/de"
|
||||
import { dict as es } from "@/i18n/es"
|
||||
import { dict as fr } from "@/i18n/fr"
|
||||
import { dict as da } from "@/i18n/da"
|
||||
import { dict as ja } from "@/i18n/ja"
|
||||
import { dict as pl } from "@/i18n/pl"
|
||||
import { dict as ru } from "@/i18n/ru"
|
||||
import { dict as uiEn } from "@opencode-ai/ui/i18n/en"
|
||||
import { dict as uiZh } from "@opencode-ai/ui/i18n/zh"
|
||||
import { dict as uiZht } from "@opencode-ai/ui/i18n/zht"
|
||||
import { dict as uiKo } from "@opencode-ai/ui/i18n/ko"
|
||||
import { dict as uiDe } from "@opencode-ai/ui/i18n/de"
|
||||
import { dict as uiEs } from "@opencode-ai/ui/i18n/es"
|
||||
import { dict as uiFr } from "@opencode-ai/ui/i18n/fr"
|
||||
import { dict as uiDa } from "@opencode-ai/ui/i18n/da"
|
||||
import { dict as uiJa } from "@opencode-ai/ui/i18n/ja"
|
||||
import { dict as uiPl } from "@opencode-ai/ui/i18n/pl"
|
||||
import { dict as uiRu } from "@opencode-ai/ui/i18n/ru"
|
||||
|
||||
export type Locale = "en" | "zh" | "zht" | "ko" | "de" | "es" | "fr" | "da" | "ja" | "pl" | "ru"
|
||||
|
||||
type RawDictionary = typeof en & typeof uiEn
|
||||
type Dictionary = i18n.Flatten<RawDictionary>
|
||||
|
||||
const LOCALES: readonly Locale[] = ["en", "zh", "zht", "ko", "de", "es", "fr", "da", "ja", "pl", "ru"]
|
||||
|
||||
function detectLocale(): Locale {
|
||||
if (typeof navigator !== "object") return "en"
|
||||
|
||||
const languages = navigator.languages?.length ? navigator.languages : [navigator.language]
|
||||
for (const language of languages) {
|
||||
if (!language) continue
|
||||
if (language.toLowerCase().startsWith("zh")) {
|
||||
if (language.toLowerCase().includes("hant")) return "zht"
|
||||
return "zh"
|
||||
}
|
||||
if (language.toLowerCase().startsWith("ko")) return "ko"
|
||||
if (language.toLowerCase().startsWith("de")) return "de"
|
||||
if (language.toLowerCase().startsWith("es")) return "es"
|
||||
if (language.toLowerCase().startsWith("fr")) return "fr"
|
||||
if (language.toLowerCase().startsWith("da")) return "da"
|
||||
if (language.toLowerCase().startsWith("ja")) return "ja"
|
||||
if (language.toLowerCase().startsWith("pl")) return "pl"
|
||||
if (language.toLowerCase().startsWith("ru")) return "ru"
|
||||
}
|
||||
|
||||
return "en"
|
||||
}
|
||||
|
||||
export const { use: useLanguage, provider: LanguageProvider } = createSimpleContext({
|
||||
name: "Language",
|
||||
init: () => {
|
||||
const [store, setStore, _, ready] = persisted(
|
||||
Persist.global("language", ["language.v1"]),
|
||||
createStore({
|
||||
locale: detectLocale() as Locale,
|
||||
}),
|
||||
)
|
||||
|
||||
const locale = createMemo<Locale>(() => {
|
||||
if (store.locale === "zh") return "zh"
|
||||
if (store.locale === "zht") return "zht"
|
||||
if (store.locale === "ko") return "ko"
|
||||
if (store.locale === "de") return "de"
|
||||
if (store.locale === "es") return "es"
|
||||
if (store.locale === "fr") return "fr"
|
||||
if (store.locale === "da") return "da"
|
||||
if (store.locale === "ja") return "ja"
|
||||
if (store.locale === "pl") return "pl"
|
||||
if (store.locale === "ru") return "ru"
|
||||
return "en"
|
||||
})
|
||||
|
||||
createEffect(() => {
|
||||
const current = locale()
|
||||
if (store.locale === current) return
|
||||
setStore("locale", current)
|
||||
})
|
||||
|
||||
const base = i18n.flatten({ ...en, ...uiEn })
|
||||
const dict = createMemo<Dictionary>(() => {
|
||||
if (locale() === "en") return base
|
||||
if (locale() === "zh") return { ...base, ...i18n.flatten({ ...zh, ...uiZh }) }
|
||||
if (locale() === "zht") return { ...base, ...i18n.flatten({ ...zht, ...uiZht }) }
|
||||
if (locale() === "de") return { ...base, ...i18n.flatten({ ...de, ...uiDe }) }
|
||||
if (locale() === "es") return { ...base, ...i18n.flatten({ ...es, ...uiEs }) }
|
||||
if (locale() === "fr") return { ...base, ...i18n.flatten({ ...fr, ...uiFr }) }
|
||||
if (locale() === "da") return { ...base, ...i18n.flatten({ ...da, ...uiDa }) }
|
||||
if (locale() === "ja") return { ...base, ...i18n.flatten({ ...ja, ...uiJa }) }
|
||||
if (locale() === "pl") return { ...base, ...i18n.flatten({ ...pl, ...uiPl }) }
|
||||
if (locale() === "ru") return { ...base, ...i18n.flatten({ ...ru, ...uiRu }) }
|
||||
return { ...base, ...i18n.flatten({ ...ko, ...uiKo }) }
|
||||
})
|
||||
|
||||
const t = i18n.translator(dict, i18n.resolveTemplate)
|
||||
|
||||
const labelKey: Record<Locale, keyof Dictionary> = {
|
||||
en: "language.en",
|
||||
zh: "language.zh",
|
||||
zht: "language.zht",
|
||||
ko: "language.ko",
|
||||
de: "language.de",
|
||||
es: "language.es",
|
||||
fr: "language.fr",
|
||||
da: "language.da",
|
||||
ja: "language.ja",
|
||||
pl: "language.pl",
|
||||
ru: "language.ru",
|
||||
}
|
||||
|
||||
const label = (value: Locale) => t(labelKey[value])
|
||||
|
||||
createEffect(() => {
|
||||
if (typeof document !== "object") return
|
||||
document.documentElement.lang = locale()
|
||||
})
|
||||
|
||||
return {
|
||||
ready,
|
||||
locale,
|
||||
locales: LOCALES,
|
||||
label,
|
||||
t,
|
||||
setLocale(next: Locale) {
|
||||
setStore("locale", next)
|
||||
},
|
||||
}
|
||||
},
|
||||
})
|
||||
@@ -33,8 +33,6 @@ type SessionTabs = {
|
||||
type SessionView = {
|
||||
scroll: Record<string, SessionScroll>
|
||||
reviewOpen?: string[]
|
||||
terminalOpened?: boolean
|
||||
reviewPanelOpened?: boolean
|
||||
}
|
||||
|
||||
export type LocalProject = Partial<Project> & { worktree: string; expanded: boolean }
|
||||
@@ -72,15 +70,17 @@ export const { use: useLayout, provider: LayoutProvider } = createSimpleContext(
|
||||
createStore({
|
||||
sidebar: {
|
||||
opened: false,
|
||||
width: 280,
|
||||
width: 344,
|
||||
workspaces: {} as Record<string, boolean>,
|
||||
workspacesDefault: false,
|
||||
},
|
||||
terminal: {
|
||||
height: 280,
|
||||
opened: false,
|
||||
},
|
||||
review: {
|
||||
diffStyle: "split" as ReviewDiffStyle,
|
||||
panelOpened: true,
|
||||
},
|
||||
session: {
|
||||
width: 600,
|
||||
@@ -172,7 +172,7 @@ export const { use: useLayout, provider: LayoutProvider } = createSimpleContext(
|
||||
const current = store.sessionView[sessionKey]
|
||||
const keep = meta.active ?? sessionKey
|
||||
if (!current) {
|
||||
setStore("sessionView", sessionKey, { scroll: next, terminalOpened: false, reviewPanelOpened: true })
|
||||
setStore("sessionView", sessionKey, { scroll: next })
|
||||
prune(keep)
|
||||
return
|
||||
}
|
||||
@@ -208,10 +208,10 @@ export const { use: useLayout, provider: LayoutProvider } = createSimpleContext(
|
||||
})
|
||||
})
|
||||
|
||||
const usedColors = new Set<AvatarColorKey>()
|
||||
const [colors, setColors] = createStore<Record<string, AvatarColorKey>>({})
|
||||
|
||||
function pickAvailableColor(): AvatarColorKey {
|
||||
const available = AVATAR_COLOR_KEYS.filter((c) => !usedColors.has(c))
|
||||
function pickAvailableColor(used: Set<string>): AvatarColorKey {
|
||||
const available = AVATAR_COLOR_KEYS.filter((c) => !used.has(c))
|
||||
if (available.length === 0) return AVATAR_COLOR_KEYS[Math.floor(Math.random() * AVATAR_COLOR_KEYS.length)]
|
||||
return available[Math.floor(Math.random() * available.length)]
|
||||
}
|
||||
@@ -222,24 +222,15 @@ export const { use: useLayout, provider: LayoutProvider } = createSimpleContext(
|
||||
const metadata = projectID
|
||||
? globalSync.data.project.find((x) => x.id === projectID)
|
||||
: globalSync.data.project.find((x) => x.worktree === project.worktree)
|
||||
return [
|
||||
{
|
||||
...(metadata ?? {}),
|
||||
...project,
|
||||
icon: { url: metadata?.icon?.url, color: metadata?.icon?.color },
|
||||
return {
|
||||
...(metadata ?? {}),
|
||||
...project,
|
||||
icon: {
|
||||
url: metadata?.icon?.url,
|
||||
override: metadata?.icon?.override,
|
||||
color: metadata?.icon?.color,
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
function colorize(project: LocalProject) {
|
||||
if (project.icon?.color) return project
|
||||
const color = pickAvailableColor()
|
||||
usedColors.add(color)
|
||||
project.icon = { ...project.icon, color }
|
||||
if (project.id) {
|
||||
globalSdk.client.project.update({ projectID: project.id, icon: { color } })
|
||||
}
|
||||
return project
|
||||
}
|
||||
|
||||
const roots = createMemo(() => {
|
||||
@@ -277,8 +268,37 @@ export const { use: useLayout, provider: LayoutProvider } = createSimpleContext(
|
||||
})
|
||||
})
|
||||
|
||||
const enriched = createMemo(() => server.projects.list().flatMap(enrich))
|
||||
const list = createMemo(() => enriched().flatMap(colorize))
|
||||
const enriched = createMemo(() => server.projects.list().map(enrich))
|
||||
const list = createMemo(() => {
|
||||
const projects = enriched()
|
||||
return projects.map((project) => {
|
||||
const color = project.icon?.color ?? colors[project.worktree]
|
||||
if (!color) return project
|
||||
const icon = project.icon ? { ...project.icon, color } : { color }
|
||||
return { ...project, icon }
|
||||
})
|
||||
})
|
||||
|
||||
createEffect(() => {
|
||||
const projects = enriched()
|
||||
if (projects.length === 0) return
|
||||
|
||||
const used = new Set<string>()
|
||||
for (const project of projects) {
|
||||
const color = project.icon?.color ?? colors[project.worktree]
|
||||
if (color) used.add(color)
|
||||
}
|
||||
|
||||
for (const project of projects) {
|
||||
if (project.icon?.color) continue
|
||||
if (colors[project.worktree]) continue
|
||||
const color = pickAvailableColor(used)
|
||||
used.add(color)
|
||||
setColors(project.worktree, color)
|
||||
if (!project.id) continue
|
||||
void globalSdk.client.project.update({ projectID: project.id, directory: project.worktree, icon: { color } })
|
||||
}
|
||||
})
|
||||
|
||||
onMount(() => {
|
||||
Promise.all(
|
||||
@@ -379,31 +399,31 @@ export const { use: useLayout, provider: LayoutProvider } = createSimpleContext(
|
||||
touch(sessionKey)
|
||||
scroll.seed(sessionKey)
|
||||
const s = createMemo(() => store.sessionView[sessionKey] ?? { scroll: {} })
|
||||
const terminalOpened = createMemo(() => s().terminalOpened ?? false)
|
||||
const reviewPanelOpened = createMemo(() => s().reviewPanelOpened ?? true)
|
||||
const terminalOpened = createMemo(() => store.terminal?.opened ?? false)
|
||||
const reviewPanelOpened = createMemo(() => store.review?.panelOpened ?? true)
|
||||
|
||||
function setTerminalOpened(next: boolean) {
|
||||
const current = store.sessionView[sessionKey]
|
||||
const current = store.terminal
|
||||
if (!current) {
|
||||
setStore("sessionView", sessionKey, { scroll: {}, terminalOpened: next, reviewPanelOpened: true })
|
||||
setStore("terminal", { height: 280, opened: next })
|
||||
return
|
||||
}
|
||||
|
||||
const value = current.terminalOpened ?? false
|
||||
const value = current.opened ?? false
|
||||
if (value === next) return
|
||||
setStore("sessionView", sessionKey, "terminalOpened", next)
|
||||
setStore("terminal", "opened", next)
|
||||
}
|
||||
|
||||
function setReviewPanelOpened(next: boolean) {
|
||||
const current = store.sessionView[sessionKey]
|
||||
const current = store.review
|
||||
if (!current) {
|
||||
setStore("sessionView", sessionKey, { scroll: {}, terminalOpened: false, reviewPanelOpened: next })
|
||||
setStore("review", { diffStyle: "split" as ReviewDiffStyle, panelOpened: next })
|
||||
return
|
||||
}
|
||||
|
||||
const value = current.reviewPanelOpened ?? true
|
||||
const value = current.panelOpened ?? true
|
||||
if (value === next) return
|
||||
setStore("sessionView", sessionKey, "reviewPanelOpened", next)
|
||||
setStore("review", "panelOpened", next)
|
||||
}
|
||||
|
||||
return {
|
||||
@@ -444,8 +464,6 @@ export const { use: useLayout, provider: LayoutProvider } = createSimpleContext(
|
||||
if (!current) {
|
||||
setStore("sessionView", sessionKey, {
|
||||
scroll: {},
|
||||
terminalOpened: false,
|
||||
reviewPanelOpened: true,
|
||||
reviewOpen: open,
|
||||
})
|
||||
return
|
||||
|
||||
@@ -10,6 +10,7 @@ import { useProviders } from "@/hooks/use-providers"
|
||||
import { DateTime } from "luxon"
|
||||
import { Persist, persisted } from "@/utils/persist"
|
||||
import { showToast } from "@opencode-ai/ui/toast"
|
||||
import { useLanguage } from "@/context/language"
|
||||
|
||||
export type LocalFile = FileNode &
|
||||
Partial<{
|
||||
@@ -42,6 +43,7 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({
|
||||
const sdk = useSDK()
|
||||
const sync = useSync()
|
||||
const providers = useProviders()
|
||||
const language = useLanguage()
|
||||
|
||||
function isModelValid(model: ModelKey) {
|
||||
const provider = providers.all().find((x) => x.id === model.providerID)
|
||||
@@ -409,7 +411,7 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({
|
||||
.catch((e) => {
|
||||
showToast({
|
||||
variant: "error",
|
||||
title: "Failed to load file",
|
||||
title: language.t("toast.file.loadFailed.title"),
|
||||
description: e.message,
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,16 +1,17 @@
|
||||
import { createStore } from "solid-js/store"
|
||||
import { createEffect, onCleanup } from "solid-js"
|
||||
import { useParams } from "@solidjs/router"
|
||||
import { createSimpleContext } from "@opencode-ai/ui/context"
|
||||
import { useGlobalSDK } from "./global-sdk"
|
||||
import { useGlobalSync } from "./global-sync"
|
||||
import { usePlatform } from "@/context/platform"
|
||||
import { useLanguage } from "@/context/language"
|
||||
import { useSettings } from "@/context/settings"
|
||||
import { Binary } from "@opencode-ai/util/binary"
|
||||
import { base64Encode } from "@opencode-ai/util/encode"
|
||||
import { base64Decode, base64Encode } from "@opencode-ai/util/encode"
|
||||
import { EventSessionError } from "@opencode-ai/sdk/v2"
|
||||
import { makeAudioPlayer } from "@solid-primitives/audio"
|
||||
import idleSound from "@opencode-ai/ui/audio/staplebops-01.aac"
|
||||
import errorSound from "@opencode-ai/ui/audio/nope-03.aac"
|
||||
import { Persist, persisted } from "@/utils/persist"
|
||||
import { playSound, soundSrc } from "@/utils/sound"
|
||||
|
||||
type NotificationBase = {
|
||||
directory?: string
|
||||
@@ -44,19 +45,12 @@ function pruneNotifications(list: Notification[]) {
|
||||
export const { use: useNotification, provider: NotificationProvider } = createSimpleContext({
|
||||
name: "Notification",
|
||||
init: () => {
|
||||
let idlePlayer: ReturnType<typeof makeAudioPlayer> | undefined
|
||||
let errorPlayer: ReturnType<typeof makeAudioPlayer> | undefined
|
||||
|
||||
try {
|
||||
idlePlayer = makeAudioPlayer(idleSound)
|
||||
errorPlayer = makeAudioPlayer(errorSound)
|
||||
} catch (err) {
|
||||
console.log("Failed to load audio", err)
|
||||
}
|
||||
|
||||
const params = useParams()
|
||||
const globalSDK = useGlobalSDK()
|
||||
const globalSync = useGlobalSync()
|
||||
const platform = usePlatform()
|
||||
const settings = useSettings()
|
||||
const language = useLanguage()
|
||||
|
||||
const [store, setStore, _, ready] = persisted(
|
||||
Persist.global("notification", ["notification.v1"]),
|
||||
@@ -81,10 +75,15 @@ export const { use: useNotification, provider: NotificationProvider } = createSi
|
||||
const unsub = globalSDK.event.listen((e) => {
|
||||
const directory = e.name
|
||||
const event = e.details
|
||||
const base = {
|
||||
directory,
|
||||
time: Date.now(),
|
||||
viewed: false,
|
||||
const time = Date.now()
|
||||
const activeDirectory = params.dir ? base64Decode(params.dir) : undefined
|
||||
const activeSession = params.id
|
||||
const viewed = (sessionID?: string) => {
|
||||
if (!activeDirectory) return false
|
||||
if (!activeSession) return false
|
||||
if (!sessionID) return false
|
||||
if (directory !== activeDirectory) return false
|
||||
return sessionID === activeSession
|
||||
}
|
||||
switch (event.type) {
|
||||
case "session.idle": {
|
||||
@@ -93,16 +92,25 @@ export const { use: useNotification, provider: NotificationProvider } = createSi
|
||||
const match = Binary.search(syncStore.session, sessionID, (s) => s.id)
|
||||
const session = match.found ? syncStore.session[match.index] : undefined
|
||||
if (session?.parentID) break
|
||||
try {
|
||||
idlePlayer?.play()
|
||||
} catch {}
|
||||
|
||||
playSound(soundSrc(settings.sounds.agent()))
|
||||
|
||||
append({
|
||||
...base,
|
||||
directory,
|
||||
time,
|
||||
viewed: viewed(sessionID),
|
||||
type: "turn-complete",
|
||||
session: sessionID,
|
||||
})
|
||||
|
||||
const href = `/${base64Encode(directory)}/session/${sessionID}`
|
||||
void platform.notify("Response ready", session?.title ?? sessionID, href)
|
||||
if (settings.notifications.agent()) {
|
||||
void platform.notify(
|
||||
language.t("notification.session.responseReady.title"),
|
||||
session?.title ?? sessionID,
|
||||
href,
|
||||
)
|
||||
}
|
||||
break
|
||||
}
|
||||
case "session.error": {
|
||||
@@ -111,19 +119,25 @@ export const { use: useNotification, provider: NotificationProvider } = createSi
|
||||
const match = sessionID ? Binary.search(syncStore.session, sessionID, (s) => s.id) : undefined
|
||||
const session = sessionID && match?.found ? syncStore.session[match.index] : undefined
|
||||
if (session?.parentID) break
|
||||
try {
|
||||
errorPlayer?.play()
|
||||
} catch {}
|
||||
|
||||
playSound(soundSrc(settings.sounds.errors()))
|
||||
|
||||
const error = "error" in event.properties ? event.properties.error : undefined
|
||||
append({
|
||||
...base,
|
||||
directory,
|
||||
time,
|
||||
viewed: viewed(sessionID),
|
||||
type: "error",
|
||||
session: sessionID ?? "global",
|
||||
error,
|
||||
})
|
||||
const description = session?.title ?? (typeof error === "string" ? error : "An error occurred")
|
||||
const description =
|
||||
session?.title ??
|
||||
(typeof error === "string" ? error : language.t("notification.session.error.fallbackDescription"))
|
||||
const href = sessionID ? `/${base64Encode(directory)}/session/${sessionID}` : `/${base64Encode(directory)}`
|
||||
void platform.notify("Session error", description, href)
|
||||
if (settings.notifications.errors()) {
|
||||
void platform.notify(language.t("notification.session.error.title"), description, href)
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
@@ -36,6 +36,7 @@ export const { use: useServer, provider: ServerProvider } = createSimpleContext(
|
||||
createStore({
|
||||
list: [] as string[],
|
||||
projects: {} as Record<string, StoredProject[]>,
|
||||
lastProject: {} as Record<string, string>,
|
||||
}),
|
||||
)
|
||||
|
||||
@@ -197,6 +198,16 @@ export const { use: useServer, provider: ServerProvider } = createSimpleContext(
|
||||
result.splice(toIndex, 0, item)
|
||||
setStore("projects", key, result)
|
||||
},
|
||||
last() {
|
||||
const key = origin()
|
||||
if (!key) return
|
||||
return store.lastProject[key]
|
||||
},
|
||||
touch(directory: string) {
|
||||
const key = origin()
|
||||
if (!key) return
|
||||
setStore("lastProject", key, directory)
|
||||
},
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
158
packages/app/src/context/settings.tsx
Normal file
158
packages/app/src/context/settings.tsx
Normal file
@@ -0,0 +1,158 @@
|
||||
import { createStore, reconcile } from "solid-js/store"
|
||||
import { createEffect, createMemo } from "solid-js"
|
||||
import { createSimpleContext } from "@opencode-ai/ui/context"
|
||||
import { persisted } from "@/utils/persist"
|
||||
|
||||
export interface NotificationSettings {
|
||||
agent: boolean
|
||||
permissions: boolean
|
||||
errors: boolean
|
||||
}
|
||||
|
||||
export interface SoundSettings {
|
||||
agent: string
|
||||
permissions: string
|
||||
errors: string
|
||||
}
|
||||
|
||||
export interface Settings {
|
||||
general: {
|
||||
autoSave: boolean
|
||||
}
|
||||
appearance: {
|
||||
fontSize: number
|
||||
font: string
|
||||
}
|
||||
keybinds: Record<string, string>
|
||||
permissions: {
|
||||
autoApprove: boolean
|
||||
}
|
||||
notifications: NotificationSettings
|
||||
sounds: SoundSettings
|
||||
}
|
||||
|
||||
const defaultSettings: Settings = {
|
||||
general: {
|
||||
autoSave: true,
|
||||
},
|
||||
appearance: {
|
||||
fontSize: 14,
|
||||
font: "ibm-plex-mono",
|
||||
},
|
||||
keybinds: {},
|
||||
permissions: {
|
||||
autoApprove: false,
|
||||
},
|
||||
notifications: {
|
||||
agent: true,
|
||||
permissions: true,
|
||||
errors: false,
|
||||
},
|
||||
sounds: {
|
||||
agent: "staplebops-01",
|
||||
permissions: "staplebops-02",
|
||||
errors: "nope-03",
|
||||
},
|
||||
}
|
||||
|
||||
const monoFallback =
|
||||
'ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace'
|
||||
|
||||
const monoFonts: Record<string, string> = {
|
||||
"ibm-plex-mono": `"IBM Plex Mono", "IBM Plex Mono Fallback", ${monoFallback}`,
|
||||
"cascadia-code": `"Cascadia Code Nerd Font", "IBM Plex Mono", "IBM Plex Mono Fallback", ${monoFallback}`,
|
||||
"fira-code": `"Fira Code Nerd Font", "IBM Plex Mono", "IBM Plex Mono Fallback", ${monoFallback}`,
|
||||
hack: `"Hack Nerd Font", "IBM Plex Mono", "IBM Plex Mono Fallback", ${monoFallback}`,
|
||||
inconsolata: `"Inconsolata Nerd Font", "IBM Plex Mono", "IBM Plex Mono Fallback", ${monoFallback}`,
|
||||
"intel-one-mono": `"Intel One Mono Nerd Font", "IBM Plex Mono", "IBM Plex Mono Fallback", ${monoFallback}`,
|
||||
"jetbrains-mono": `"JetBrains Mono Nerd Font", "IBM Plex Mono", "IBM Plex Mono Fallback", ${monoFallback}`,
|
||||
"meslo-lgs": `"Meslo LGS Nerd Font", "IBM Plex Mono", "IBM Plex Mono Fallback", ${monoFallback}`,
|
||||
"roboto-mono": `"Roboto Mono Nerd Font", "IBM Plex Mono", "IBM Plex Mono Fallback", ${monoFallback}`,
|
||||
"source-code-pro": `"Source Code Pro Nerd Font", "IBM Plex Mono", "IBM Plex Mono Fallback", ${monoFallback}`,
|
||||
"ubuntu-mono": `"Ubuntu Mono Nerd Font", "IBM Plex Mono", "IBM Plex Mono Fallback", ${monoFallback}`,
|
||||
}
|
||||
|
||||
export function monoFontFamily(font: string | undefined) {
|
||||
return monoFonts[font ?? defaultSettings.appearance.font] ?? monoFonts[defaultSettings.appearance.font]
|
||||
}
|
||||
|
||||
export const { use: useSettings, provider: SettingsProvider } = createSimpleContext({
|
||||
name: "Settings",
|
||||
init: () => {
|
||||
const [store, setStore, _, ready] = persisted("settings.v3", createStore<Settings>(defaultSettings))
|
||||
|
||||
createEffect(() => {
|
||||
if (typeof document === "undefined") return
|
||||
document.documentElement.style.setProperty("--font-family-mono", monoFontFamily(store.appearance?.font))
|
||||
})
|
||||
|
||||
return {
|
||||
ready,
|
||||
get current() {
|
||||
return store
|
||||
},
|
||||
general: {
|
||||
autoSave: createMemo(() => store.general?.autoSave ?? defaultSettings.general.autoSave),
|
||||
setAutoSave(value: boolean) {
|
||||
setStore("general", "autoSave", value)
|
||||
},
|
||||
},
|
||||
appearance: {
|
||||
fontSize: createMemo(() => store.appearance?.fontSize ?? defaultSettings.appearance.fontSize),
|
||||
setFontSize(value: number) {
|
||||
setStore("appearance", "fontSize", value)
|
||||
},
|
||||
font: createMemo(() => store.appearance?.font ?? defaultSettings.appearance.font),
|
||||
setFont(value: string) {
|
||||
setStore("appearance", "font", value)
|
||||
},
|
||||
},
|
||||
keybinds: {
|
||||
get: (action: string) => store.keybinds?.[action],
|
||||
set(action: string, keybind: string) {
|
||||
setStore("keybinds", action, keybind)
|
||||
},
|
||||
reset(action: string) {
|
||||
setStore("keybinds", action, undefined!)
|
||||
},
|
||||
resetAll() {
|
||||
setStore("keybinds", reconcile({}))
|
||||
},
|
||||
},
|
||||
permissions: {
|
||||
autoApprove: createMemo(() => store.permissions?.autoApprove ?? defaultSettings.permissions.autoApprove),
|
||||
setAutoApprove(value: boolean) {
|
||||
setStore("permissions", "autoApprove", value)
|
||||
},
|
||||
},
|
||||
notifications: {
|
||||
agent: createMemo(() => store.notifications?.agent ?? defaultSettings.notifications.agent),
|
||||
setAgent(value: boolean) {
|
||||
setStore("notifications", "agent", value)
|
||||
},
|
||||
permissions: createMemo(() => store.notifications?.permissions ?? defaultSettings.notifications.permissions),
|
||||
setPermissions(value: boolean) {
|
||||
setStore("notifications", "permissions", value)
|
||||
},
|
||||
errors: createMemo(() => store.notifications?.errors ?? defaultSettings.notifications.errors),
|
||||
setErrors(value: boolean) {
|
||||
setStore("notifications", "errors", value)
|
||||
},
|
||||
},
|
||||
sounds: {
|
||||
agent: createMemo(() => store.sounds?.agent ?? defaultSettings.sounds.agent),
|
||||
setAgent(value: string) {
|
||||
setStore("sounds", "agent", value)
|
||||
},
|
||||
permissions: createMemo(() => store.sounds?.permissions ?? defaultSettings.sounds.permissions),
|
||||
setPermissions(value: string) {
|
||||
setStore("sounds", "permissions", value)
|
||||
},
|
||||
errors: createMemo(() => store.sounds?.errors ?? defaultSettings.sounds.errors),
|
||||
setErrors(value: string) {
|
||||
setStore("sounds", "errors", value)
|
||||
},
|
||||
},
|
||||
}
|
||||
},
|
||||
})
|
||||
@@ -1,6 +1,6 @@
|
||||
import { createStore, produce } from "solid-js/store"
|
||||
import { createSimpleContext } from "@opencode-ai/ui/context"
|
||||
import { batch, createMemo, createRoot, onCleanup } from "solid-js"
|
||||
import { batch, createEffect, createMemo, createRoot, onCleanup } from "solid-js"
|
||||
import { useParams } from "@solidjs/router"
|
||||
import { useSDK } from "./sdk"
|
||||
import { Persist, persisted } from "@/utils/persist"
|
||||
@@ -13,6 +13,7 @@ export type LocalPTY = {
|
||||
cols?: number
|
||||
buffer?: string
|
||||
scrollY?: number
|
||||
error?: boolean
|
||||
}
|
||||
|
||||
const WORKSPACE_KEY = "__workspace__"
|
||||
@@ -25,11 +26,19 @@ type TerminalCacheEntry = {
|
||||
dispose: VoidFunction
|
||||
}
|
||||
|
||||
function createTerminalSession(sdk: ReturnType<typeof useSDK>, dir: string, id: string | undefined) {
|
||||
const legacy = `${dir}/terminal${id ? "/" + id : ""}.v1`
|
||||
function createTerminalSession(sdk: ReturnType<typeof useSDK>, dir: string, session?: string) {
|
||||
const legacy = session ? [`${dir}/terminal/${session}.v1`, `${dir}/terminal.v1`] : [`${dir}/terminal.v1`]
|
||||
|
||||
const numberFromTitle = (title: string) => {
|
||||
const match = title.match(/^Terminal (\d+)$/)
|
||||
if (!match) return
|
||||
const value = Number(match[1])
|
||||
if (!Number.isFinite(value) || value <= 0) return
|
||||
return value
|
||||
}
|
||||
|
||||
const [store, setStore, _, ready] = persisted(
|
||||
Persist.scoped(dir, id, "terminal", [legacy]),
|
||||
Persist.workspace(dir, "terminal", legacy),
|
||||
createStore<{
|
||||
active?: string
|
||||
all: LocalPTY[]
|
||||
@@ -38,36 +47,76 @@ function createTerminalSession(sdk: ReturnType<typeof useSDK>, dir: string, id:
|
||||
}),
|
||||
)
|
||||
|
||||
const unsub = sdk.event.on("pty.exited", (event) => {
|
||||
const id = event.properties.id
|
||||
if (!store.all.some((x) => x.id === id)) return
|
||||
batch(() => {
|
||||
setStore(
|
||||
"all",
|
||||
store.all.filter((x) => x.id !== id),
|
||||
)
|
||||
if (store.active === id) {
|
||||
const remaining = store.all.filter((x) => x.id !== id)
|
||||
setStore("active", remaining[0]?.id)
|
||||
}
|
||||
})
|
||||
})
|
||||
onCleanup(unsub)
|
||||
|
||||
const meta = { migrated: false }
|
||||
|
||||
createEffect(() => {
|
||||
if (!ready()) return
|
||||
if (meta.migrated) return
|
||||
meta.migrated = true
|
||||
|
||||
setStore("all", (all) => {
|
||||
const next = all.map((pty) => {
|
||||
const direct = Number.isFinite(pty.titleNumber) && pty.titleNumber > 0 ? pty.titleNumber : undefined
|
||||
if (direct !== undefined) return pty
|
||||
const parsed = numberFromTitle(pty.title)
|
||||
if (parsed === undefined) return pty
|
||||
return { ...pty, titleNumber: parsed }
|
||||
})
|
||||
if (next.every((pty, index) => pty === all[index])) return all
|
||||
return next
|
||||
})
|
||||
})
|
||||
|
||||
return {
|
||||
ready,
|
||||
all: createMemo(() => Object.values(store.all)),
|
||||
active: createMemo(() => store.active),
|
||||
new() {
|
||||
const existingTitleNumbers = new Set(
|
||||
store.all.map((pty) => {
|
||||
const match = pty.titleNumber
|
||||
return match
|
||||
store.all.flatMap((pty) => {
|
||||
const direct = Number.isFinite(pty.titleNumber) && pty.titleNumber > 0 ? pty.titleNumber : undefined
|
||||
if (direct !== undefined) return [direct]
|
||||
const parsed = numberFromTitle(pty.title)
|
||||
if (parsed === undefined) return []
|
||||
return [parsed]
|
||||
}),
|
||||
)
|
||||
|
||||
let nextNumber = 1
|
||||
while (existingTitleNumbers.has(nextNumber)) {
|
||||
nextNumber++
|
||||
}
|
||||
const nextNumber =
|
||||
Array.from({ length: existingTitleNumbers.size + 1 }, (_, index) => index + 1).find(
|
||||
(number) => !existingTitleNumbers.has(number),
|
||||
) ?? 1
|
||||
|
||||
sdk.client.pty
|
||||
.create({ title: `Terminal ${nextNumber}` })
|
||||
.then((pty) => {
|
||||
const id = pty.data?.id
|
||||
if (!id) return
|
||||
setStore("all", [
|
||||
...store.all,
|
||||
{
|
||||
id,
|
||||
title: pty.data?.title ?? "Terminal",
|
||||
titleNumber: nextNumber,
|
||||
},
|
||||
])
|
||||
const newTerminal = {
|
||||
id,
|
||||
title: pty.data?.title ?? "Terminal",
|
||||
titleNumber: nextNumber,
|
||||
}
|
||||
setStore("all", (all) => {
|
||||
const newAll = [...all, newTerminal]
|
||||
return newAll
|
||||
})
|
||||
setStore("active", id)
|
||||
})
|
||||
.catch((e) => {
|
||||
@@ -75,7 +124,10 @@ function createTerminalSession(sdk: ReturnType<typeof useSDK>, dir: string, id:
|
||||
})
|
||||
},
|
||||
update(pty: Partial<LocalPTY> & { id: string }) {
|
||||
setStore("all", (x) => x.map((x) => (x.id === pty.id ? { ...x, ...pty } : x)))
|
||||
const index = store.all.findIndex((x) => x.id === pty.id)
|
||||
if (index !== -1) {
|
||||
setStore("all", index, (existing) => ({ ...existing, ...pty }))
|
||||
}
|
||||
sdk.client.pty
|
||||
.update({
|
||||
ptyID: pty.id,
|
||||
@@ -110,18 +162,29 @@ function createTerminalSession(sdk: ReturnType<typeof useSDK>, dir: string, id:
|
||||
open(id: string) {
|
||||
setStore("active", id)
|
||||
},
|
||||
next() {
|
||||
const index = store.all.findIndex((x) => x.id === store.active)
|
||||
if (index === -1) return
|
||||
const nextIndex = (index + 1) % store.all.length
|
||||
setStore("active", store.all[nextIndex]?.id)
|
||||
},
|
||||
previous() {
|
||||
const index = store.all.findIndex((x) => x.id === store.active)
|
||||
if (index === -1) return
|
||||
const prevIndex = index === 0 ? store.all.length - 1 : index - 1
|
||||
setStore("active", store.all[prevIndex]?.id)
|
||||
},
|
||||
async close(id: string) {
|
||||
batch(() => {
|
||||
setStore(
|
||||
"all",
|
||||
store.all.filter((x) => x.id !== id),
|
||||
)
|
||||
const filtered = store.all.filter((x) => x.id !== id)
|
||||
if (store.active === id) {
|
||||
const index = store.all.findIndex((f) => f.id === id)
|
||||
const previous = store.all[Math.max(0, index - 1)]
|
||||
setStore("active", previous?.id)
|
||||
const next = index > 0 ? index - 1 : 0
|
||||
setStore("active", filtered[next]?.id)
|
||||
}
|
||||
setStore("all", filtered)
|
||||
})
|
||||
|
||||
await sdk.client.pty.remove({ ptyID: id }).catch((e) => {
|
||||
console.error("Failed to close terminal", e)
|
||||
})
|
||||
@@ -166,8 +229,8 @@ export const { use: useTerminal, provider: TerminalProvider } = createSimpleCont
|
||||
}
|
||||
}
|
||||
|
||||
const load = (dir: string, id: string | undefined) => {
|
||||
const key = `${dir}:${id ?? WORKSPACE_KEY}`
|
||||
const load = (dir: string, session?: string) => {
|
||||
const key = `${dir}:${WORKSPACE_KEY}`
|
||||
const existing = cache.get(key)
|
||||
if (existing) {
|
||||
cache.delete(key)
|
||||
@@ -176,7 +239,7 @@ export const { use: useTerminal, provider: TerminalProvider } = createSimpleCont
|
||||
}
|
||||
|
||||
const entry = createRoot((dispose) => ({
|
||||
value: createTerminalSession(sdk, dir, id),
|
||||
value: createTerminalSession(sdk, dir, session),
|
||||
dispose,
|
||||
}))
|
||||
|
||||
@@ -185,18 +248,20 @@ export const { use: useTerminal, provider: TerminalProvider } = createSimpleCont
|
||||
return entry.value
|
||||
}
|
||||
|
||||
const session = createMemo(() => load(params.dir!, params.id))
|
||||
const workspace = createMemo(() => load(params.dir!, params.id))
|
||||
|
||||
return {
|
||||
ready: () => session().ready(),
|
||||
all: () => session().all(),
|
||||
active: () => session().active(),
|
||||
new: () => session().new(),
|
||||
update: (pty: Partial<LocalPTY> & { id: string }) => session().update(pty),
|
||||
clone: (id: string) => session().clone(id),
|
||||
open: (id: string) => session().open(id),
|
||||
close: (id: string) => session().close(id),
|
||||
move: (id: string, to: number) => session().move(id, to),
|
||||
ready: () => workspace().ready(),
|
||||
all: () => workspace().all(),
|
||||
active: () => workspace().active(),
|
||||
new: () => workspace().new(),
|
||||
update: (pty: Partial<LocalPTY> & { id: string }) => workspace().update(pty),
|
||||
clone: (id: string) => workspace().clone(id),
|
||||
open: (id: string) => workspace().open(id),
|
||||
close: (id: string) => workspace().close(id),
|
||||
move: (id: string, to: number) => workspace().move(id, to),
|
||||
next: () => workspace().next(),
|
||||
previous: () => workspace().previous(),
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
@@ -2,13 +2,25 @@
|
||||
import { render } from "solid-js/web"
|
||||
import { AppBaseProviders, AppInterface } from "@/app"
|
||||
import { Platform, PlatformProvider } from "@/context/platform"
|
||||
import { dict as en } from "@/i18n/en"
|
||||
import { dict as zh } from "@/i18n/zh"
|
||||
import pkg from "../package.json"
|
||||
|
||||
const root = document.getElementById("root")
|
||||
if (import.meta.env.DEV && !(root instanceof HTMLElement)) {
|
||||
throw new Error(
|
||||
"Root element not found. Did you forget to add it to your index.html? Or maybe the id attribute got misspelled?",
|
||||
)
|
||||
const locale = (() => {
|
||||
if (typeof navigator !== "object") return "en" as const
|
||||
const languages = navigator.languages?.length ? navigator.languages : [navigator.language]
|
||||
for (const language of languages) {
|
||||
if (!language) continue
|
||||
if (language.toLowerCase().startsWith("zh")) return "zh" as const
|
||||
}
|
||||
return "en" as const
|
||||
})()
|
||||
|
||||
const key = "error.dev.rootNotFound" as const
|
||||
const message = locale === "zh" ? (zh[key] ?? en[key]) : en[key]
|
||||
throw new Error(message)
|
||||
}
|
||||
|
||||
const platform: Platform = {
|
||||
@@ -37,7 +49,7 @@ const platform: Platform = {
|
||||
.then(() => {
|
||||
const notification = new Notification(title, {
|
||||
body: description ?? "",
|
||||
icon: "https://opencode.ai/favicon-96x96.png",
|
||||
icon: "https://opencode.ai/favicon-96x96-v3.png",
|
||||
})
|
||||
notification.onclick = () => {
|
||||
window.focus()
|
||||
|
||||
560
packages/app/src/i18n/da.ts
Normal file
560
packages/app/src/i18n/da.ts
Normal file
@@ -0,0 +1,560 @@
|
||||
export const dict = {
|
||||
"command.category.suggested": "Foreslået",
|
||||
"command.category.view": "Vis",
|
||||
"command.category.project": "Projekt",
|
||||
"command.category.provider": "Udbyder",
|
||||
"command.category.server": "Server",
|
||||
"command.category.session": "Session",
|
||||
"command.category.theme": "Tema",
|
||||
"command.category.language": "Sprog",
|
||||
"command.category.file": "Fil",
|
||||
"command.category.terminal": "Terminal",
|
||||
"command.category.model": "Model",
|
||||
"command.category.mcp": "MCP",
|
||||
"command.category.agent": "Agent",
|
||||
"command.category.permissions": "Tilladelser",
|
||||
"command.category.workspace": "Arbejdsområde",
|
||||
|
||||
"theme.scheme.system": "System",
|
||||
"theme.scheme.light": "Lys",
|
||||
"theme.scheme.dark": "Mørk",
|
||||
|
||||
"command.sidebar.toggle": "Skift sidebjælke",
|
||||
"command.project.open": "Åbn projekt",
|
||||
"command.provider.connect": "Tilslut udbyder",
|
||||
"command.server.switch": "Skift server",
|
||||
"command.session.previous": "Forrige session",
|
||||
"command.session.next": "Næste session",
|
||||
"command.session.archive": "Arkivér session",
|
||||
|
||||
"command.palette": "Kommandopalette",
|
||||
|
||||
"command.theme.cycle": "Skift tema",
|
||||
"command.theme.set": "Brug tema: {{theme}}",
|
||||
"command.theme.scheme.cycle": "Skift farveskema",
|
||||
"command.theme.scheme.set": "Brug farveskema: {{scheme}}",
|
||||
|
||||
"command.language.cycle": "Skift sprog",
|
||||
"command.language.set": "Brug sprog: {{language}}",
|
||||
|
||||
"command.session.new": "Ny session",
|
||||
"command.file.open": "Åbn fil",
|
||||
"command.file.open.description": "Søg i filer og kommandoer",
|
||||
"command.terminal.toggle": "Skift terminal",
|
||||
"command.review.toggle": "Skift gennemgang",
|
||||
"command.terminal.new": "Ny terminal",
|
||||
"command.terminal.new.description": "Opret en ny terminalfane",
|
||||
"command.steps.toggle": "Skift trin",
|
||||
"command.steps.toggle.description": "Vis eller skjul trin for den aktuelle besked",
|
||||
"command.message.previous": "Forrige besked",
|
||||
"command.message.previous.description": "Gå til den forrige brugerbesked",
|
||||
"command.message.next": "Næste besked",
|
||||
"command.message.next.description": "Gå til den næste brugerbesked",
|
||||
"command.model.choose": "Vælg model",
|
||||
"command.model.choose.description": "Vælg en anden model",
|
||||
"command.mcp.toggle": "Skift MCP'er",
|
||||
"command.mcp.toggle.description": "Skift MCP'er",
|
||||
"command.agent.cycle": "Skift agent",
|
||||
"command.agent.cycle.description": "Skift til næste agent",
|
||||
"command.agent.cycle.reverse": "Skift agent baglæns",
|
||||
"command.agent.cycle.reverse.description": "Skift til forrige agent",
|
||||
"command.model.variant.cycle": "Skift tænkeindsats",
|
||||
"command.model.variant.cycle.description": "Skift til næste indsatsniveau",
|
||||
"command.permissions.autoaccept.enable": "Accepter ændringer automatisk",
|
||||
"command.permissions.autoaccept.disable": "Stop automatisk accept af ændringer",
|
||||
"command.session.undo": "Fortryd",
|
||||
"command.session.undo.description": "Fortryd den sidste besked",
|
||||
"command.session.redo": "Omgør",
|
||||
"command.session.redo.description": "Omgør den sidste fortrudte besked",
|
||||
"command.session.compact": "Komprimér session",
|
||||
"command.session.compact.description": "Opsummer sessionen for at reducere kontekststørrelsen",
|
||||
"command.session.fork": "Forgren fra besked",
|
||||
"command.session.fork.description": "Opret en ny session fra en tidligere besked",
|
||||
"command.session.share": "Del session",
|
||||
"command.session.share.description": "Del denne session og kopier URL'en til udklipsholderen",
|
||||
"command.session.unshare": "Stop deling af session",
|
||||
"command.session.unshare.description": "Stop med at dele denne session",
|
||||
|
||||
"palette.search.placeholder": "Søg i filer og kommandoer",
|
||||
"palette.empty": "Ingen resultater fundet",
|
||||
"palette.group.commands": "Kommandoer",
|
||||
"palette.group.files": "Filer",
|
||||
|
||||
"dialog.provider.search.placeholder": "Søg udbydere",
|
||||
"dialog.provider.empty": "Ingen udbydere fundet",
|
||||
"dialog.provider.group.popular": "Populære",
|
||||
"dialog.provider.group.other": "Andre",
|
||||
"dialog.provider.tag.recommended": "Anbefalet",
|
||||
"dialog.provider.anthropic.note": "Forbind med Claude Pro/Max eller API-nøgle",
|
||||
|
||||
"dialog.model.select.title": "Vælg model",
|
||||
"dialog.model.search.placeholder": "Søg modeller",
|
||||
"dialog.model.empty": "Ingen modeller fundet",
|
||||
"dialog.model.manage": "Administrer modeller",
|
||||
"dialog.model.manage.description": "Tilpas hvilke modeller der vises i modelvælgeren.",
|
||||
|
||||
"dialog.model.unpaid.freeModels.title": "Gratis modeller leveret af OpenCode",
|
||||
"dialog.model.unpaid.addMore.title": "Tilføj flere modeller fra populære udbydere",
|
||||
|
||||
"dialog.provider.viewAll": "Vis alle udbydere",
|
||||
|
||||
"provider.connect.title": "Forbind {{provider}}",
|
||||
"provider.connect.title.anthropicProMax": "Log ind med Claude Pro/Max",
|
||||
"provider.connect.selectMethod": "Vælg loginmetode for {{provider}}.",
|
||||
"provider.connect.method.apiKey": "API-nøgle",
|
||||
"provider.connect.status.inProgress": "Godkendelse i gang...",
|
||||
"provider.connect.status.waiting": "Venter på godkendelse...",
|
||||
"provider.connect.status.failed": "Godkendelse mislykkedes: {{error}}",
|
||||
"provider.connect.apiKey.description":
|
||||
"Indtast din {{provider}} API-nøgle for at forbinde din konto og bruge {{provider}} modeller i OpenCode.",
|
||||
"provider.connect.apiKey.label": "{{provider}} API-nøgle",
|
||||
"provider.connect.apiKey.placeholder": "API-nøgle",
|
||||
"provider.connect.apiKey.required": "API-nøgle er påkrævet",
|
||||
"provider.connect.opencodeZen.line1":
|
||||
"OpenCode Zen giver dig adgang til et udvalg af pålidelige optimerede modeller til kodningsagenter.",
|
||||
"provider.connect.opencodeZen.line2":
|
||||
"Med en enkelt API-nøgle får du adgang til modeller som Claude, GPT, Gemini, GLM og flere.",
|
||||
"provider.connect.opencodeZen.visit.prefix": "Besøg ",
|
||||
"provider.connect.opencodeZen.visit.suffix": " for at hente din API-nøgle.",
|
||||
"provider.connect.oauth.code.visit.prefix": "Besøg ",
|
||||
"provider.connect.oauth.code.visit.link": "dette link",
|
||||
"provider.connect.oauth.code.visit.suffix":
|
||||
" for at hente din godkendelseskode for at forbinde din konto og bruge {{provider}} modeller i OpenCode.",
|
||||
"provider.connect.oauth.code.label": "{{method}} godkendelseskode",
|
||||
"provider.connect.oauth.code.placeholder": "Godkendelseskode",
|
||||
"provider.connect.oauth.code.required": "Godkendelseskode er påkrævet",
|
||||
"provider.connect.oauth.code.invalid": "Ugyldig godkendelseskode",
|
||||
"provider.connect.oauth.auto.visit.prefix": "Besøg ",
|
||||
"provider.connect.oauth.auto.visit.link": "dette link",
|
||||
"provider.connect.oauth.auto.visit.suffix":
|
||||
" og indtast koden nedenfor for at forbinde din konto og bruge {{provider}} modeller i OpenCode.",
|
||||
"provider.connect.oauth.auto.confirmationCode": "Bekræftelseskode",
|
||||
"provider.connect.toast.connected.title": "{{provider}} forbundet",
|
||||
"provider.connect.toast.connected.description": "{{provider}} modeller er nu tilgængelige.",
|
||||
|
||||
"model.tag.free": "Gratis",
|
||||
"model.tag.latest": "Nyeste",
|
||||
|
||||
"common.search.placeholder": "Søg",
|
||||
"common.loading": "Indlæser",
|
||||
"common.cancel": "Annuller",
|
||||
"common.submit": "Indsend",
|
||||
"common.save": "Gem",
|
||||
"common.saving": "Gemmer...",
|
||||
"common.default": "Standard",
|
||||
"common.attachment": "vedhæftning",
|
||||
|
||||
"prompt.placeholder.shell": "Indtast shell-kommando...",
|
||||
"prompt.placeholder.normal": 'Spørg om hvad som helst... "{{example}}"',
|
||||
"prompt.mode.shell": "Shell",
|
||||
"prompt.mode.shell.exit": "esc for at afslutte",
|
||||
|
||||
"prompt.example.1": "Ret en TODO i koden",
|
||||
"prompt.example.2": "Hvad er teknologistakken for dette projekt?",
|
||||
"prompt.example.3": "Ret ødelagte tests",
|
||||
"prompt.example.4": "Forklar hvordan godkendelse fungerer",
|
||||
"prompt.example.5": "Find og ret sikkerhedshuller",
|
||||
"prompt.example.6": "Tilføj enhedstests for brugerservice",
|
||||
"prompt.example.7": "Refaktorer denne funktion så den er mere læsbar",
|
||||
"prompt.example.8": "Hvad betyder denne fejl?",
|
||||
"prompt.example.9": "Hjælp mig med at debugge dette problem",
|
||||
"prompt.example.10": "Generer API-dokumentation",
|
||||
"prompt.example.11": "Optimer databaseforespørgsler",
|
||||
"prompt.example.12": "Tilføj validering af input",
|
||||
"prompt.example.13": "Opret en ny komponent til...",
|
||||
"prompt.example.14": "Hvordan deployerer jeg dette projekt?",
|
||||
"prompt.example.15": "Gennemgå min kode for bedste praksis",
|
||||
"prompt.example.16": "Tilføj fejlhåndtering til denne funktion",
|
||||
"prompt.example.17": "Forklar dette regex-mønster",
|
||||
"prompt.example.18": "Konverter dette til TypeScript",
|
||||
"prompt.example.19": "Tilføj logning i hele koden",
|
||||
"prompt.example.20": "Hvilke afhængigheder er forældede?",
|
||||
"prompt.example.21": "Hjælp mig med at skrive et migreringsscript",
|
||||
"prompt.example.22": "Implementer caching for dette endpoint",
|
||||
"prompt.example.23": "Tilføj sideinddeling til denne liste",
|
||||
"prompt.example.24": "Opret en CLI-kommando til...",
|
||||
"prompt.example.25": "Hvordan fungerer miljøvariabler her?",
|
||||
|
||||
"prompt.popover.emptyResults": "Ingen matchende resultater",
|
||||
"prompt.popover.emptyCommands": "Ingen matchende kommandoer",
|
||||
"prompt.dropzone.label": "Slip billeder eller PDF'er her",
|
||||
"prompt.slash.badge.custom": "brugerdefineret",
|
||||
"prompt.context.active": "aktiv",
|
||||
"prompt.context.includeActiveFile": "Inkluder aktiv fil",
|
||||
"prompt.action.attachFile": "Vedhæft fil",
|
||||
"prompt.action.send": "Send",
|
||||
"prompt.action.stop": "Stop",
|
||||
|
||||
"prompt.toast.pasteUnsupported.title": "Ikke understøttet indsæt",
|
||||
"prompt.toast.pasteUnsupported.description": "Kun billeder eller PDF'er kan indsættes her.",
|
||||
"prompt.toast.modelAgentRequired.title": "Vælg en agent og model",
|
||||
"prompt.toast.modelAgentRequired.description": "Vælg en agent og model før du sender en forespørgsel.",
|
||||
"prompt.toast.worktreeCreateFailed.title": "Kunne ikke oprette worktree",
|
||||
"prompt.toast.sessionCreateFailed.title": "Kunne ikke oprette session",
|
||||
"prompt.toast.shellSendFailed.title": "Kunne ikke sende shell-kommando",
|
||||
"prompt.toast.commandSendFailed.title": "Kunne ikke sende kommando",
|
||||
"prompt.toast.promptSendFailed.title": "Kunne ikke sende forespørgsel",
|
||||
|
||||
"dialog.mcp.title": "MCP'er",
|
||||
"dialog.mcp.description": "{{enabled}} af {{total}} aktiveret",
|
||||
"dialog.mcp.empty": "Ingen MCP'er konfigureret",
|
||||
|
||||
"mcp.status.connected": "forbundet",
|
||||
"mcp.status.failed": "mislykkedes",
|
||||
"mcp.status.needs_auth": "kræver godkendelse",
|
||||
"mcp.status.disabled": "deaktiveret",
|
||||
|
||||
"dialog.fork.empty": "Ingen beskeder at forgrene fra",
|
||||
|
||||
"dialog.directory.search.placeholder": "Søg mapper",
|
||||
"dialog.directory.empty": "Ingen mapper fundet",
|
||||
|
||||
"dialog.server.title": "Servere",
|
||||
"dialog.server.description": "Skift hvilken OpenCode-server denne app forbinder til.",
|
||||
"dialog.server.search.placeholder": "Søg servere",
|
||||
"dialog.server.empty": "Ingen servere endnu",
|
||||
"dialog.server.add.title": "Tilføj en server",
|
||||
"dialog.server.add.url": "Server URL",
|
||||
"dialog.server.add.placeholder": "http://localhost:4096",
|
||||
"dialog.server.add.error": "Kunne ikke forbinde til server",
|
||||
"dialog.server.add.checking": "Tjekker...",
|
||||
"dialog.server.add.button": "Tilføj",
|
||||
"dialog.server.default.title": "Standardserver",
|
||||
"dialog.server.default.description":
|
||||
"Forbind til denne server ved start af app i stedet for at starte en lokal server. Kræver genstart.",
|
||||
"dialog.server.default.none": "Ingen server valgt",
|
||||
"dialog.server.default.set": "Sæt nuværende server som standard",
|
||||
"dialog.server.default.clear": "Ryd",
|
||||
|
||||
"dialog.project.edit.title": "Rediger projekt",
|
||||
"dialog.project.edit.name": "Navn",
|
||||
"dialog.project.edit.icon": "Ikon",
|
||||
"dialog.project.edit.icon.alt": "Projektikon",
|
||||
"dialog.project.edit.icon.hint": "Klik eller træk et billede",
|
||||
"dialog.project.edit.icon.recommended": "Anbefalet: 128x128px",
|
||||
"dialog.project.edit.color": "Farve",
|
||||
|
||||
"context.breakdown.title": "Kontekstfordeling",
|
||||
"context.breakdown.note":
|
||||
'Omtrentlig fordeling af input-tokens. "Andre" inkluderer værktøjsdefinitioner og overhead.',
|
||||
"context.breakdown.system": "System",
|
||||
"context.breakdown.user": "Bruger",
|
||||
"context.breakdown.assistant": "Assistent",
|
||||
"context.breakdown.tool": "Værktøjskald",
|
||||
"context.breakdown.other": "Andre",
|
||||
|
||||
"context.systemPrompt.title": "Systemprompt",
|
||||
"context.rawMessages.title": "Rå beskeder",
|
||||
|
||||
"context.stats.session": "Session",
|
||||
"context.stats.messages": "Beskeder",
|
||||
"context.stats.provider": "Udbyder",
|
||||
"context.stats.model": "Model",
|
||||
"context.stats.limit": "Kontekstgrænse",
|
||||
"context.stats.totalTokens": "Total Tokens",
|
||||
"context.stats.usage": "Forbrug",
|
||||
"context.stats.inputTokens": "Input Tokens",
|
||||
"context.stats.outputTokens": "Output Tokens",
|
||||
"context.stats.reasoningTokens": "Tænke Tokens",
|
||||
"context.stats.cacheTokens": "Cache Tokens (læs/skriv)",
|
||||
"context.stats.userMessages": "Brugerbeskeder",
|
||||
"context.stats.assistantMessages": "Assistentbeskeder",
|
||||
"context.stats.totalCost": "Samlede omkostninger",
|
||||
"context.stats.sessionCreated": "Session oprettet",
|
||||
"context.stats.lastActivity": "Seneste aktivitet",
|
||||
|
||||
"context.usage.tokens": "Tokens",
|
||||
"context.usage.usage": "Forbrug",
|
||||
"context.usage.cost": "Omkostning",
|
||||
"context.usage.clickToView": "Klik for at se kontekst",
|
||||
|
||||
"language.en": "Engelsk",
|
||||
"language.zh": "Kinesisk (forenklet)",
|
||||
"language.zht": "Kinesisk (traditionelt)",
|
||||
"language.ko": "Koreansk",
|
||||
"language.de": "Tysk",
|
||||
"language.es": "Spansk",
|
||||
"language.fr": "Fransk",
|
||||
"language.ja": "Japansk",
|
||||
"language.da": "Dansk",
|
||||
"language.ru": "Russisk",
|
||||
"language.pl": "Polsk",
|
||||
|
||||
"toast.language.title": "Sprog",
|
||||
"toast.language.description": "Skiftede til {{language}}",
|
||||
|
||||
"toast.theme.title": "Tema skiftet",
|
||||
"toast.scheme.title": "Farveskema",
|
||||
|
||||
"toast.permissions.autoaccept.on.title": "Accepterer ændringer automatisk",
|
||||
"toast.permissions.autoaccept.on.description": "Redigerings- og skrivetilladelser vil automatisk blive godkendt",
|
||||
"toast.permissions.autoaccept.off.title": "Stoppede automatisk accept af ændringer",
|
||||
"toast.permissions.autoaccept.off.description": "Redigerings- og skrivetilladelser vil kræve godkendelse",
|
||||
|
||||
"toast.model.none.title": "Ingen model valgt",
|
||||
"toast.model.none.description": "Forbind en udbyder for at opsummere denne session",
|
||||
|
||||
"toast.file.loadFailed.title": "Kunne ikke indlæse fil",
|
||||
|
||||
"toast.session.share.copyFailed.title": "Kunne ikke kopiere URL til udklipsholder",
|
||||
"toast.session.share.success.title": "Session delt",
|
||||
"toast.session.share.success.description": "Delings-URL kopieret til udklipsholder!",
|
||||
"toast.session.share.failed.title": "Kunne ikke dele session",
|
||||
"toast.session.share.failed.description": "Der opstod en fejl under deling af sessionen",
|
||||
|
||||
"toast.session.unshare.success.title": "Deling af session stoppet",
|
||||
"toast.session.unshare.success.description": "Deling af session blev stoppet!",
|
||||
"toast.session.unshare.failed.title": "Kunne ikke stoppe deling af session",
|
||||
"toast.session.unshare.failed.description": "Der opstod en fejl under stop af sessionsdeling",
|
||||
|
||||
"toast.session.listFailed.title": "Kunne ikke indlæse sessioner for {{project}}",
|
||||
|
||||
"toast.update.title": "Opdatering tilgængelig",
|
||||
"toast.update.description": "En ny version af OpenCode ({{version}}) er nu tilgængelig til installation.",
|
||||
"toast.update.action.installRestart": "Installer og genstart",
|
||||
"toast.update.action.notYet": "Ikke endnu",
|
||||
|
||||
"error.page.title": "Noget gik galt",
|
||||
"error.page.description": "Der opstod en fejl under indlæsning af applikationen.",
|
||||
"error.page.details.label": "Fejldetaljer",
|
||||
"error.page.action.restart": "Genstart",
|
||||
"error.page.action.checking": "Tjekker...",
|
||||
"error.page.action.checkUpdates": "Tjek for opdateringer",
|
||||
"error.page.action.updateTo": "Opdater til {{version}}",
|
||||
"error.page.report.prefix": "Rapporter venligst denne fejl til OpenCode-teamet",
|
||||
"error.page.report.discord": "på Discord",
|
||||
"error.page.version": "Version: {{version}}",
|
||||
|
||||
"error.dev.rootNotFound":
|
||||
"Rodelement ikke fundet. Har du glemt at tilføje det til din index.html? Eller måske er id-attributten stavet forkert?",
|
||||
|
||||
"error.globalSync.connectFailed": "Kunne ikke forbinde til server. Kører der en server på `{{url}}`?",
|
||||
|
||||
"error.chain.unknown": "Ukendt fejl",
|
||||
"error.chain.causedBy": "Forårsaget af:",
|
||||
"error.chain.apiError": "API-fejl",
|
||||
"error.chain.status": "Status: {{status}}",
|
||||
"error.chain.retryable": "Kan forsøges igen: {{retryable}}",
|
||||
"error.chain.responseBody": "Svarindhold:\n{{body}}",
|
||||
"error.chain.didYouMean": "Mente du: {{suggestions}}",
|
||||
"error.chain.modelNotFound": "Model ikke fundet: {{provider}}/{{model}}",
|
||||
"error.chain.checkConfig": "Tjek dine konfigurations (opencode.json) udbyder/modelnavne",
|
||||
"error.chain.mcpFailed": 'MCP-server "{{name}}" fejlede. Bemærk, OpenCode understøtter ikke MCP-godkendelse endnu.',
|
||||
"error.chain.providerAuthFailed": "Udbydergodkendelse mislykkedes ({{provider}}): {{message}}",
|
||||
"error.chain.providerInitFailed":
|
||||
'Kunne ikke initialisere udbyder "{{provider}}". Tjek legitimationsoplysninger og konfiguration.',
|
||||
"error.chain.configJsonInvalid": "Konfigurationsfil på {{path}} er ikke gyldig JSON(C)",
|
||||
"error.chain.configJsonInvalidWithMessage": "Konfigurationsfil på {{path}} er ikke gyldig JSON(C): {{message}}",
|
||||
"error.chain.configDirectoryTypo":
|
||||
'Mappe "{{dir}}" i {{path}} er ikke gyldig. Omdøb mappen til "{{suggestion}}" eller fjern den. Dette er en almindelig slåfejl.',
|
||||
"error.chain.configFrontmatterError": "Kunne ikke parse frontmatter i {{path}}:\n{{message}}",
|
||||
"error.chain.configInvalid": "Konfigurationsfil på {{path}} er ugyldig",
|
||||
"error.chain.configInvalidWithMessage": "Konfigurationsfil på {{path}} er ugyldig: {{message}}",
|
||||
|
||||
"notification.permission.title": "Tilladelse påkrævet",
|
||||
"notification.permission.description": "{{sessionTitle}} i {{projectName}} kræver tilladelse",
|
||||
"notification.question.title": "Spørgsmål",
|
||||
"notification.question.description": "{{sessionTitle}} i {{projectName}} har et spørgsmål",
|
||||
"notification.action.goToSession": "Gå til session",
|
||||
|
||||
"notification.session.responseReady.title": "Svar klar",
|
||||
"notification.session.error.title": "Sessionsfejl",
|
||||
"notification.session.error.fallbackDescription": "Der opstod en fejl",
|
||||
|
||||
"home.recentProjects": "Seneste projekter",
|
||||
"home.empty.title": "Ingen seneste projekter",
|
||||
"home.empty.description": "Kom i gang ved at åbne et lokalt projekt",
|
||||
|
||||
"session.tab.session": "Session",
|
||||
"session.tab.review": "Gennemgang",
|
||||
"session.tab.context": "Kontekst",
|
||||
"session.review.filesChanged": "{{count}} Filer ændret",
|
||||
"session.review.loadingChanges": "Indlæser ændringer...",
|
||||
"session.review.empty": "Ingen ændringer i denne session endnu",
|
||||
"session.messages.renderEarlier": "Vis tidligere beskeder",
|
||||
"session.messages.loadingEarlier": "Indlæser tidligere beskeder...",
|
||||
"session.messages.loadEarlier": "Indlæs tidligere beskeder",
|
||||
"session.messages.loading": "Indlæser beskeder...",
|
||||
|
||||
"session.context.addToContext": "Tilføj {{selection}} til kontekst",
|
||||
|
||||
"session.new.worktree.main": "Hovedgren",
|
||||
"session.new.worktree.mainWithBranch": "Hovedgren ({{branch}})",
|
||||
"session.new.worktree.create": "Opret nyt worktree",
|
||||
"session.new.lastModified": "Sidst ændret",
|
||||
|
||||
"session.header.search.placeholder": "Søg {{project}}",
|
||||
|
||||
"session.share.popover.title": "Udgiv på nettet",
|
||||
"session.share.popover.description.shared":
|
||||
"Denne session er offentlig på nettet. Den er tilgængelig for alle med linket.",
|
||||
"session.share.popover.description.unshared":
|
||||
"Del session offentligt på nettet. Den vil være tilgængelig for alle med linket.",
|
||||
"session.share.action.share": "Del",
|
||||
"session.share.action.publish": "Udgiv",
|
||||
"session.share.action.publishing": "Udgiver...",
|
||||
"session.share.action.unpublish": "Afpublicer",
|
||||
"session.share.action.unpublishing": "Afpublicerer...",
|
||||
"session.share.action.view": "Vis",
|
||||
"session.share.copy.copied": "Kopieret",
|
||||
"session.share.copy.copyLink": "Kopier link",
|
||||
|
||||
"lsp.tooltip.none": "Ingen LSP-servere",
|
||||
"lsp.label.connected": "{{count}} LSP",
|
||||
|
||||
"prompt.loading": "Indlæser prompt...",
|
||||
"terminal.loading": "Indlæser terminal...",
|
||||
"terminal.title": "Terminal",
|
||||
"terminal.title.numbered": "Terminal {{number}}",
|
||||
|
||||
"common.closeTab": "Luk fane",
|
||||
"common.dismiss": "Afvis",
|
||||
"common.requestFailed": "Forespørgsel mislykkedes",
|
||||
"common.moreOptions": "Flere muligheder",
|
||||
"common.learnMore": "Lær mere",
|
||||
"common.rename": "Omdøb",
|
||||
"common.reset": "Nulstil",
|
||||
"common.delete": "Slet",
|
||||
"common.close": "Luk",
|
||||
"common.edit": "Rediger",
|
||||
"common.loadMore": "Indlæs flere",
|
||||
|
||||
"sidebar.settings": "Indstillinger",
|
||||
"sidebar.help": "Hjælp",
|
||||
"sidebar.workspaces.enable": "Aktiver arbejdsområder",
|
||||
"sidebar.workspaces.disable": "Deaktiver arbejdsområder",
|
||||
"sidebar.gettingStarted.title": "Kom i gang",
|
||||
"sidebar.gettingStarted.line1": "OpenCode inkluderer gratis modeller så du kan starte med det samme.",
|
||||
"sidebar.gettingStarted.line2": "Forbind enhver udbyder for at bruge modeller, inkl. Claude, GPT, Gemini osv.",
|
||||
"sidebar.project.recentSessions": "Seneste sessioner",
|
||||
"sidebar.project.viewAllSessions": "Vis alle sessioner",
|
||||
|
||||
"settings.section.desktop": "Desktop",
|
||||
"settings.tab.general": "Generelt",
|
||||
"settings.tab.shortcuts": "Genveje",
|
||||
|
||||
"settings.general.section.appearance": "Udseende",
|
||||
"settings.general.section.notifications": "Systemmeddelelser",
|
||||
"settings.general.section.sounds": "Lydeffekter",
|
||||
|
||||
"settings.general.row.language.title": "Sprog",
|
||||
"settings.general.row.language.description": "Ændr visningssproget for OpenCode",
|
||||
"settings.general.row.appearance.title": "Udseende",
|
||||
"settings.general.row.appearance.description": "Tilpas hvordan OpenCode ser ud på din enhed",
|
||||
"settings.general.row.theme.title": "Tema",
|
||||
"settings.general.row.theme.description": "Tilpas hvordan OpenCode er temabestemt.",
|
||||
"settings.general.row.font.title": "Skrifttype",
|
||||
"settings.general.row.font.description": "Tilpas mono-skrifttypen brugt i kodeblokke",
|
||||
|
||||
"settings.general.notifications.agent.title": "Agent",
|
||||
"settings.general.notifications.agent.description":
|
||||
"Vis systemmeddelelse når agenten er færdig eller kræver opmærksomhed",
|
||||
"settings.general.notifications.permissions.title": "Tilladelser",
|
||||
"settings.general.notifications.permissions.description": "Vis systemmeddelelse når en tilladelse er påkrævet",
|
||||
"settings.general.notifications.errors.title": "Fejl",
|
||||
"settings.general.notifications.errors.description": "Vis systemmeddelelse når der opstår en fejl",
|
||||
|
||||
"settings.general.sounds.agent.title": "Agent",
|
||||
"settings.general.sounds.agent.description": "Afspil lyd når agenten er færdig eller kræver opmærksomhed",
|
||||
"settings.general.sounds.permissions.title": "Tilladelser",
|
||||
"settings.general.sounds.permissions.description": "Afspil lyd når en tilladelse er påkrævet",
|
||||
"settings.general.sounds.errors.title": "Fejl",
|
||||
"settings.general.sounds.errors.description": "Afspil lyd når der opstår en fejl",
|
||||
|
||||
"settings.shortcuts.title": "Tastaturgenveje",
|
||||
"settings.shortcuts.reset.button": "Nulstil til standard",
|
||||
"settings.shortcuts.reset.toast.title": "Genveje nulstillet",
|
||||
"settings.shortcuts.reset.toast.description": "Tastaturgenveje er blevet nulstillet til standard.",
|
||||
"settings.shortcuts.conflict.title": "Genvej allerede i brug",
|
||||
"settings.shortcuts.conflict.description": "{{keybind}} er allerede tildelt til {{titles}}.",
|
||||
"settings.shortcuts.unassigned": "Ikke tildelt",
|
||||
"settings.shortcuts.pressKeys": "Tryk på taster",
|
||||
"settings.shortcuts.search.placeholder": "Søg genveje",
|
||||
"settings.shortcuts.search.empty": "Ingen genveje fundet",
|
||||
|
||||
"settings.shortcuts.group.general": "Generelt",
|
||||
"settings.shortcuts.group.session": "Session",
|
||||
"settings.shortcuts.group.navigation": "Navigation",
|
||||
"settings.shortcuts.group.modelAndAgent": "Model og agent",
|
||||
"settings.shortcuts.group.terminal": "Terminal",
|
||||
"settings.shortcuts.group.prompt": "Prompt",
|
||||
|
||||
"settings.providers.title": "Udbydere",
|
||||
"settings.providers.description": "Udbyderindstillinger vil kunne konfigureres her.",
|
||||
"settings.models.title": "Modeller",
|
||||
"settings.models.description": "Modelindstillinger vil kunne konfigureres her.",
|
||||
"settings.agents.title": "Agenter",
|
||||
"settings.agents.description": "Agentindstillinger vil kunne konfigureres her.",
|
||||
"settings.commands.title": "Kommandoer",
|
||||
"settings.commands.description": "Kommandoindstillinger vil kunne konfigureres her.",
|
||||
"settings.mcp.title": "MCP",
|
||||
"settings.mcp.description": "MCP-indstillinger vil kunne konfigureres her.",
|
||||
|
||||
"settings.permissions.title": "Tilladelser",
|
||||
"settings.permissions.description": "Styr hvilke værktøjer serveren kan bruge som standard.",
|
||||
"settings.permissions.section.tools": "Værktøjer",
|
||||
"settings.permissions.toast.updateFailed.title": "Kunne ikke opdatere tilladelser",
|
||||
|
||||
"settings.permissions.action.allow": "Tillad",
|
||||
"settings.permissions.action.ask": "Spørg",
|
||||
"settings.permissions.action.deny": "Afvis",
|
||||
|
||||
"settings.permissions.tool.read.title": "Læs",
|
||||
"settings.permissions.tool.read.description": "Læsning af en fil (matcher filstien)",
|
||||
"settings.permissions.tool.edit.title": "Rediger",
|
||||
"settings.permissions.tool.edit.description":
|
||||
"Ændre filer, herunder redigeringer, skrivninger, patches og multi-redigeringer",
|
||||
"settings.permissions.tool.glob.title": "Glob",
|
||||
"settings.permissions.tool.glob.description": "Match filer ved hjælp af glob-mønstre",
|
||||
"settings.permissions.tool.grep.title": "Grep",
|
||||
"settings.permissions.tool.grep.description": "Søg i filindhold ved hjælp af regulære udtryk",
|
||||
"settings.permissions.tool.list.title": "Liste",
|
||||
"settings.permissions.tool.list.description": "List filer i en mappe",
|
||||
"settings.permissions.tool.bash.title": "Bash",
|
||||
"settings.permissions.tool.bash.description": "Kør shell-kommandoer",
|
||||
"settings.permissions.tool.task.title": "Opgave",
|
||||
"settings.permissions.tool.task.description": "Start underagenter",
|
||||
"settings.permissions.tool.skill.title": "Færdighed",
|
||||
"settings.permissions.tool.skill.description": "Indlæs en færdighed efter navn",
|
||||
"settings.permissions.tool.lsp.title": "LSP",
|
||||
"settings.permissions.tool.lsp.description": "Kør sprogserverforespørgsler",
|
||||
"settings.permissions.tool.todoread.title": "Læs To-do",
|
||||
"settings.permissions.tool.todoread.description": "Læs to-do listen",
|
||||
"settings.permissions.tool.todowrite.title": "Skriv To-do",
|
||||
"settings.permissions.tool.todowrite.description": "Opdater to-do listen",
|
||||
"settings.permissions.tool.webfetch.title": "Webhentning",
|
||||
"settings.permissions.tool.webfetch.description": "Hent indhold fra en URL",
|
||||
"settings.permissions.tool.websearch.title": "Websøgning",
|
||||
"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.description": "Få adgang til filer uden for projektmappen",
|
||||
"settings.permissions.tool.doom_loop.title": "Doom Loop",
|
||||
"settings.permissions.tool.doom_loop.description": "Opdag gentagne værktøjskald med identisk input",
|
||||
|
||||
"workspace.new": "Nyt arbejdsområde",
|
||||
"workspace.type.local": "lokal",
|
||||
"workspace.type.sandbox": "sandkasse",
|
||||
"workspace.create.failed.title": "Kunne ikke oprette arbejdsområde",
|
||||
"workspace.delete.failed.title": "Kunne ikke slette arbejdsområde",
|
||||
"workspace.resetting.title": "Nulstiller arbejdsområde",
|
||||
"workspace.resetting.description": "Dette kan tage et minut.",
|
||||
"workspace.reset.failed.title": "Kunne ikke nulstille arbejdsområde",
|
||||
"workspace.reset.success.title": "Arbejdsområde nulstillet",
|
||||
"workspace.reset.success.description": "Arbejdsområdet matcher nu hovedgrenen.",
|
||||
"workspace.status.checking": "Tjekker for uflettede ændringer...",
|
||||
"workspace.status.error": "Kunne ikke bekræfte git-status.",
|
||||
"workspace.status.clean": "Ingen uflettede ændringer fundet.",
|
||||
"workspace.status.dirty": "Uflettede ændringer fundet i dette arbejdsområde.",
|
||||
"workspace.delete.title": "Slet arbejdsområde",
|
||||
"workspace.delete.confirm": 'Slet arbejdsområde "{{name}}"?',
|
||||
"workspace.delete.button": "Slet arbejdsområde",
|
||||
"workspace.reset.title": "Nulstil arbejdsområde",
|
||||
"workspace.reset.confirm": 'Nulstil arbejdsområde "{{name}}"?',
|
||||
"workspace.reset.button": "Nulstil arbejdsområde",
|
||||
"workspace.reset.archived.none": "Ingen aktive sessioner vil blive arkiveret.",
|
||||
"workspace.reset.archived.one": "1 session vil blive arkiveret.",
|
||||
"workspace.reset.archived.many": "{{count}} sessioner vil blive arkiveret.",
|
||||
"workspace.reset.note": "Dette vil nulstille arbejdsområdet til at matche hovedgrenen.",
|
||||
}
|
||||
569
packages/app/src/i18n/de.ts
Normal file
569
packages/app/src/i18n/de.ts
Normal file
@@ -0,0 +1,569 @@
|
||||
import { dict as en } from "./en"
|
||||
|
||||
type Keys = keyof typeof en
|
||||
|
||||
export const dict = {
|
||||
"command.category.suggested": "Vorgeschlagen",
|
||||
"command.category.view": "Ansicht",
|
||||
"command.category.project": "Projekt",
|
||||
"command.category.provider": "Anbieter",
|
||||
"command.category.server": "Server",
|
||||
"command.category.session": "Sitzung",
|
||||
"command.category.theme": "Thema",
|
||||
"command.category.language": "Sprache",
|
||||
"command.category.file": "Datei",
|
||||
"command.category.terminal": "Terminal",
|
||||
"command.category.model": "Modell",
|
||||
"command.category.mcp": "MCP",
|
||||
"command.category.agent": "Agent",
|
||||
"command.category.permissions": "Berechtigungen",
|
||||
"command.category.workspace": "Arbeitsbereich",
|
||||
|
||||
"theme.scheme.system": "System",
|
||||
"theme.scheme.light": "Hell",
|
||||
"theme.scheme.dark": "Dunkel",
|
||||
|
||||
"command.sidebar.toggle": "Seitenleiste umschalten",
|
||||
"command.project.open": "Projekt öffnen",
|
||||
"command.provider.connect": "Anbieter verbinden",
|
||||
"command.server.switch": "Server wechseln",
|
||||
"command.session.previous": "Vorherige Sitzung",
|
||||
"command.session.next": "Nächste Sitzung",
|
||||
"command.session.archive": "Sitzung archivieren",
|
||||
|
||||
"command.palette": "Befehlspalette",
|
||||
|
||||
"command.theme.cycle": "Thema wechseln",
|
||||
"command.theme.set": "Thema verwenden: {{theme}}",
|
||||
"command.theme.scheme.cycle": "Farbschema wechseln",
|
||||
"command.theme.scheme.set": "Farbschema verwenden: {{scheme}}",
|
||||
|
||||
"command.language.cycle": "Sprache wechseln",
|
||||
"command.language.set": "Sprache verwenden: {{language}}",
|
||||
|
||||
"command.session.new": "Neue Sitzung",
|
||||
"command.file.open": "Datei öffnen",
|
||||
"command.file.open.description": "Dateien und Befehle durchsuchen",
|
||||
"command.terminal.toggle": "Terminal umschalten",
|
||||
"command.review.toggle": "Überprüfung umschalten",
|
||||
"command.terminal.new": "Neues Terminal",
|
||||
"command.terminal.new.description": "Neuen Terminal-Tab erstellen",
|
||||
"command.steps.toggle": "Schritte umschalten",
|
||||
"command.steps.toggle.description": "Schritte für die aktuelle Nachricht anzeigen oder ausblenden",
|
||||
"command.message.previous": "Vorherige Nachricht",
|
||||
"command.message.previous.description": "Zur vorherigen Benutzernachricht gehen",
|
||||
"command.message.next": "Nächste Nachricht",
|
||||
"command.message.next.description": "Zur nächsten Benutzernachricht gehen",
|
||||
"command.model.choose": "Modell wählen",
|
||||
"command.model.choose.description": "Ein anderes Modell auswählen",
|
||||
"command.mcp.toggle": "MCPs umschalten",
|
||||
"command.mcp.toggle.description": "MCPs umschalten",
|
||||
"command.agent.cycle": "Agent wechseln",
|
||||
"command.agent.cycle.description": "Zum nächsten Agenten wechseln",
|
||||
"command.agent.cycle.reverse": "Agent rückwärts wechseln",
|
||||
"command.agent.cycle.reverse.description": "Zum vorherigen Agenten wechseln",
|
||||
"command.model.variant.cycle": "Denkaufwand wechseln",
|
||||
"command.model.variant.cycle.description": "Zum nächsten Aufwandslevel wechseln",
|
||||
"command.permissions.autoaccept.enable": "Änderungen automatisch akzeptieren",
|
||||
"command.permissions.autoaccept.disable": "Automatische Annahme von Änderungen stoppen",
|
||||
"command.session.undo": "Rückgängig",
|
||||
"command.session.undo.description": "Letzte Nachricht rückgängig machen",
|
||||
"command.session.redo": "Wiederherstellen",
|
||||
"command.session.redo.description": "Letzte rückgängig gemachte Nachricht wiederherstellen",
|
||||
"command.session.compact": "Sitzung komprimieren",
|
||||
"command.session.compact.description": "Sitzung zusammenfassen, um die Kontextgröße zu reduzieren",
|
||||
"command.session.fork": "Von Nachricht abzweigen",
|
||||
"command.session.fork.description": "Neue Sitzung aus einer früheren Nachricht erstellen",
|
||||
"command.session.share": "Sitzung teilen",
|
||||
"command.session.share.description": "Diese Sitzung teilen und URL in die Zwischenablage kopieren",
|
||||
"command.session.unshare": "Teilen der Sitzung aufheben",
|
||||
"command.session.unshare.description": "Teilen dieser Sitzung beenden",
|
||||
|
||||
"palette.search.placeholder": "Dateien und Befehle durchsuchen",
|
||||
"palette.empty": "Keine Ergebnisse gefunden",
|
||||
"palette.group.commands": "Befehle",
|
||||
"palette.group.files": "Dateien",
|
||||
|
||||
"dialog.provider.search.placeholder": "Anbieter durchsuchen",
|
||||
"dialog.provider.empty": "Keine Anbieter gefunden",
|
||||
"dialog.provider.group.popular": "Beliebt",
|
||||
"dialog.provider.group.other": "Andere",
|
||||
"dialog.provider.tag.recommended": "Empfohlen",
|
||||
"dialog.provider.anthropic.note": "Mit Claude Pro/Max oder API-Schlüssel verbinden",
|
||||
|
||||
"dialog.model.select.title": "Modell auswählen",
|
||||
"dialog.model.search.placeholder": "Modelle durchsuchen",
|
||||
"dialog.model.empty": "Keine Modellergebnisse",
|
||||
"dialog.model.manage": "Modelle verwalten",
|
||||
"dialog.model.manage.description": "Anpassen, welche Modelle in der Modellauswahl erscheinen.",
|
||||
|
||||
"dialog.model.unpaid.freeModels.title": "Kostenlose Modelle von OpenCode",
|
||||
"dialog.model.unpaid.addMore.title": "Weitere Modelle von beliebten Anbietern hinzufügen",
|
||||
|
||||
"dialog.provider.viewAll": "Alle Anbieter anzeigen",
|
||||
|
||||
"provider.connect.title": "{{provider}} verbinden",
|
||||
"provider.connect.title.anthropicProMax": "Mit Claude Pro/Max anmelden",
|
||||
"provider.connect.selectMethod": "Anmeldemethode für {{provider}} auswählen.",
|
||||
"provider.connect.method.apiKey": "API-Schlüssel",
|
||||
"provider.connect.status.inProgress": "Autorisierung läuft...",
|
||||
"provider.connect.status.waiting": "Warten auf Autorisierung...",
|
||||
"provider.connect.status.failed": "Autorisierung fehlgeschlagen: {{error}}",
|
||||
"provider.connect.apiKey.description":
|
||||
"Geben Sie Ihren {{provider}} API-Schlüssel ein, um Ihr Konto zu verbinden und {{provider}} Modelle in OpenCode zu nutzen.",
|
||||
"provider.connect.apiKey.label": "{{provider}} API-Schlüssel",
|
||||
"provider.connect.apiKey.placeholder": "API-Schlüssel",
|
||||
"provider.connect.apiKey.required": "API-Schlüssel ist erforderlich",
|
||||
"provider.connect.opencodeZen.line1":
|
||||
"OpenCode Zen bietet Ihnen Zugriff auf eine kuratierte Auswahl zuverlässiger, optimierter Modelle für Coding-Agenten.",
|
||||
"provider.connect.opencodeZen.line2":
|
||||
"Mit einem einzigen API-Schlüssel erhalten Sie Zugriff auf Modelle wie Claude, GPT, Gemini, GLM und mehr.",
|
||||
"provider.connect.opencodeZen.visit.prefix": "Besuchen Sie ",
|
||||
"provider.connect.opencodeZen.visit.suffix": ", um Ihren API-Schlüssel zu erhalten.",
|
||||
"provider.connect.oauth.code.visit.prefix": "Besuchen Sie ",
|
||||
"provider.connect.oauth.code.visit.link": "diesen Link",
|
||||
"provider.connect.oauth.code.visit.suffix":
|
||||
", um Ihren Autorisierungscode zu erhalten, Ihr Konto zu verbinden und {{provider}} Modelle in OpenCode zu nutzen.",
|
||||
"provider.connect.oauth.code.label": "{{method}} Autorisierungscode",
|
||||
"provider.connect.oauth.code.placeholder": "Autorisierungscode",
|
||||
"provider.connect.oauth.code.required": "Autorisierungscode ist erforderlich",
|
||||
"provider.connect.oauth.code.invalid": "Ungültiger Autorisierungscode",
|
||||
"provider.connect.oauth.auto.visit.prefix": "Besuchen Sie ",
|
||||
"provider.connect.oauth.auto.visit.link": "diesen Link",
|
||||
"provider.connect.oauth.auto.visit.suffix":
|
||||
" und geben Sie den untenstehenden Code ein, um Ihr Konto zu verbinden und {{provider}} Modelle in OpenCode zu nutzen.",
|
||||
"provider.connect.oauth.auto.confirmationCode": "Bestätigungscode",
|
||||
"provider.connect.toast.connected.title": "{{provider}} verbunden",
|
||||
"provider.connect.toast.connected.description": "{{provider}} Modelle sind jetzt verfügbar.",
|
||||
|
||||
"model.tag.free": "Kostenlos",
|
||||
"model.tag.latest": "Neueste",
|
||||
|
||||
"common.search.placeholder": "Suchen",
|
||||
"common.loading": "Laden",
|
||||
"common.cancel": "Abbrechen",
|
||||
"common.submit": "Absenden",
|
||||
"common.save": "Speichern",
|
||||
"common.saving": "Speichert...",
|
||||
"common.default": "Standard",
|
||||
"common.attachment": "Anhang",
|
||||
|
||||
"prompt.placeholder.shell": "Shell-Befehl eingeben...",
|
||||
"prompt.placeholder.normal": 'Fragen Sie alles... "{{example}}"',
|
||||
"prompt.mode.shell": "Shell",
|
||||
"prompt.mode.shell.exit": "esc zum Verlassen",
|
||||
|
||||
"prompt.example.1": "Ein TODO in der Codebasis beheben",
|
||||
"prompt.example.2": "Was ist der Tech-Stack dieses Projekts?",
|
||||
"prompt.example.3": "Fehlerhafte Tests beheben",
|
||||
"prompt.example.4": "Erkläre, wie die Authentifizierung funktioniert",
|
||||
"prompt.example.5": "Sicherheitslücken finden und beheben",
|
||||
"prompt.example.6": "Unit-Tests für den Benutzerdienst hinzufügen",
|
||||
"prompt.example.7": "Diese Funktion lesbarer gestalten",
|
||||
"prompt.example.8": "Was bedeutet dieser Fehler?",
|
||||
"prompt.example.9": "Hilf mir, dieses Problem zu debuggen",
|
||||
"prompt.example.10": "API-Dokumentation generieren",
|
||||
"prompt.example.11": "Datenbankabfragen optimieren",
|
||||
"prompt.example.12": "Eingabevalidierung hinzufügen",
|
||||
"prompt.example.13": "Neue Komponente erstellen für...",
|
||||
"prompt.example.14": "Wie deploye ich dieses Projekt?",
|
||||
"prompt.example.15": "Meinen Code auf Best Practices überprüfen",
|
||||
"prompt.example.16": "Fehlerbehandlung zu dieser Funktion hinzufügen",
|
||||
"prompt.example.17": "Erkläre dieses Regex-Muster",
|
||||
"prompt.example.18": "Dies in TypeScript konvertieren",
|
||||
"prompt.example.19": "Logging in der gesamten Codebasis hinzufügen",
|
||||
"prompt.example.20": "Welche Abhängigkeiten sind veraltet?",
|
||||
"prompt.example.21": "Hilf mir, ein Migrationsskript zu schreiben",
|
||||
"prompt.example.22": "Caching für diesen Endpunkt implementieren",
|
||||
"prompt.example.23": "Paginierung zu dieser Liste hinzufügen",
|
||||
"prompt.example.24": "CLI-Befehl erstellen für...",
|
||||
"prompt.example.25": "Wie funktionieren Umgebungsvariablen hier?",
|
||||
|
||||
"prompt.popover.emptyResults": "Keine passenden Ergebnisse",
|
||||
"prompt.popover.emptyCommands": "Keine passenden Befehle",
|
||||
"prompt.dropzone.label": "Bilder oder PDFs hier ablegen",
|
||||
"prompt.slash.badge.custom": "benutzerdefiniert",
|
||||
"prompt.context.active": "aktiv",
|
||||
"prompt.context.includeActiveFile": "Aktive Datei einbeziehen",
|
||||
"prompt.action.attachFile": "Datei anhängen",
|
||||
"prompt.action.send": "Senden",
|
||||
"prompt.action.stop": "Stopp",
|
||||
|
||||
"prompt.toast.pasteUnsupported.title": "Nicht unterstütztes Einfügen",
|
||||
"prompt.toast.pasteUnsupported.description": "Hier können nur Bilder oder PDFs eingefügt werden.",
|
||||
"prompt.toast.modelAgentRequired.title": "Wählen Sie einen Agenten und ein Modell",
|
||||
"prompt.toast.modelAgentRequired.description":
|
||||
"Wählen Sie einen Agenten und ein Modell, bevor Sie eine Eingabe senden.",
|
||||
"prompt.toast.worktreeCreateFailed.title": "Worktree konnte nicht erstellt werden",
|
||||
"prompt.toast.sessionCreateFailed.title": "Sitzung konnte nicht erstellt werden",
|
||||
"prompt.toast.shellSendFailed.title": "Shell-Befehl konnte nicht gesendet werden",
|
||||
"prompt.toast.commandSendFailed.title": "Befehl konnte nicht gesendet werden",
|
||||
"prompt.toast.promptSendFailed.title": "Eingabe konnte nicht gesendet werden",
|
||||
|
||||
"dialog.mcp.title": "MCPs",
|
||||
"dialog.mcp.description": "{{enabled}} von {{total}} aktiviert",
|
||||
"dialog.mcp.empty": "Keine MCPs konfiguriert",
|
||||
|
||||
"mcp.status.connected": "verbunden",
|
||||
"mcp.status.failed": "fehlgeschlagen",
|
||||
"mcp.status.needs_auth": "benötigt Authentifizierung",
|
||||
"mcp.status.disabled": "deaktiviert",
|
||||
|
||||
"dialog.fork.empty": "Keine Nachrichten zum Abzweigen vorhanden",
|
||||
|
||||
"dialog.directory.search.placeholder": "Ordner durchsuchen",
|
||||
"dialog.directory.empty": "Keine Ordner gefunden",
|
||||
|
||||
"dialog.server.title": "Server",
|
||||
"dialog.server.description": "Wechseln Sie den OpenCode-Server, mit dem sich diese App verbindet.",
|
||||
"dialog.server.search.placeholder": "Server durchsuchen",
|
||||
"dialog.server.empty": "Noch keine Server",
|
||||
"dialog.server.add.title": "Server hinzufügen",
|
||||
"dialog.server.add.url": "Server-URL",
|
||||
"dialog.server.add.placeholder": "http://localhost:4096",
|
||||
"dialog.server.add.error": "Verbindung zum Server fehlgeschlagen",
|
||||
"dialog.server.add.checking": "Prüfen...",
|
||||
"dialog.server.add.button": "Hinzufügen",
|
||||
"dialog.server.default.title": "Standardserver",
|
||||
"dialog.server.default.description":
|
||||
"Beim App-Start mit diesem Server verbinden, anstatt einen lokalen Server zu starten. Erfordert Neustart.",
|
||||
"dialog.server.default.none": "Kein Server ausgewählt",
|
||||
"dialog.server.default.set": "Aktuellen Server als Standard setzen",
|
||||
"dialog.server.default.clear": "Löschen",
|
||||
|
||||
"dialog.project.edit.title": "Projekt bearbeiten",
|
||||
"dialog.project.edit.name": "Name",
|
||||
"dialog.project.edit.icon": "Icon",
|
||||
"dialog.project.edit.icon.alt": "Projekt-Icon",
|
||||
"dialog.project.edit.icon.hint": "Klicken oder Bild ziehen",
|
||||
"dialog.project.edit.icon.recommended": "Empfohlen: 128x128px",
|
||||
"dialog.project.edit.color": "Farbe",
|
||||
|
||||
"context.breakdown.title": "Kontext-Aufschlüsselung",
|
||||
"context.breakdown.note":
|
||||
'Ungefähre Aufschlüsselung der Eingabe-Token. "Andere" beinhaltet Werkzeugdefinitionen und Overhead.',
|
||||
"context.breakdown.system": "System",
|
||||
"context.breakdown.user": "Benutzer",
|
||||
"context.breakdown.assistant": "Assistent",
|
||||
"context.breakdown.tool": "Werkzeugaufrufe",
|
||||
"context.breakdown.other": "Andere",
|
||||
|
||||
"context.systemPrompt.title": "System-Prompt",
|
||||
"context.rawMessages.title": "Rohdaten der Nachrichten",
|
||||
|
||||
"context.stats.session": "Sitzung",
|
||||
"context.stats.messages": "Nachrichten",
|
||||
"context.stats.provider": "Anbieter",
|
||||
"context.stats.model": "Modell",
|
||||
"context.stats.limit": "Kontextlimit",
|
||||
"context.stats.totalTokens": "Gesamt-Token",
|
||||
"context.stats.usage": "Nutzung",
|
||||
"context.stats.inputTokens": "Eingabe-Token",
|
||||
"context.stats.outputTokens": "Ausgabe-Token",
|
||||
"context.stats.reasoningTokens": "Reasoning-Token",
|
||||
"context.stats.cacheTokens": "Cache-Token (lesen/schreiben)",
|
||||
"context.stats.userMessages": "Benutzernachrichten",
|
||||
"context.stats.assistantMessages": "Assistentennachrichten",
|
||||
"context.stats.totalCost": "Gesamtkosten",
|
||||
"context.stats.sessionCreated": "Sitzung erstellt",
|
||||
"context.stats.lastActivity": "Letzte Aktivität",
|
||||
|
||||
"context.usage.tokens": "Token",
|
||||
"context.usage.usage": "Nutzung",
|
||||
"context.usage.cost": "Kosten",
|
||||
"context.usage.clickToView": "Klicken, um Kontext anzuzeigen",
|
||||
|
||||
"language.en": "Englisch",
|
||||
"language.zh": "Chinesisch (Vereinfacht)",
|
||||
"language.zht": "Chinesisch (Traditionell)",
|
||||
"language.ko": "Koreanisch",
|
||||
"language.de": "Deutsch",
|
||||
"language.es": "Spanisch",
|
||||
"language.fr": "Französisch",
|
||||
"language.ja": "Japanisch",
|
||||
"language.da": "Dänisch",
|
||||
"language.ru": "Russisch",
|
||||
"language.pl": "Polnisch",
|
||||
|
||||
"toast.language.title": "Sprache",
|
||||
"toast.language.description": "Zu {{language}} gewechselt",
|
||||
|
||||
"toast.theme.title": "Thema gewechselt",
|
||||
"toast.scheme.title": "Farbschema",
|
||||
|
||||
"toast.permissions.autoaccept.on.title": "Änderungen werden automatisch akzeptiert",
|
||||
"toast.permissions.autoaccept.on.description": "Bearbeitungs- und Schreibrechte werden automatisch genehmigt",
|
||||
"toast.permissions.autoaccept.off.title": "Automatische Annahme von Änderungen gestoppt",
|
||||
"toast.permissions.autoaccept.off.description": "Bearbeitungs- und Schreibrechte erfordern Genehmigung",
|
||||
|
||||
"toast.model.none.title": "Kein Modell ausgewählt",
|
||||
"toast.model.none.description": "Verbinden Sie einen Anbieter, um diese Sitzung zusammenzufassen",
|
||||
|
||||
"toast.file.loadFailed.title": "Datei konnte nicht geladen werden",
|
||||
|
||||
"toast.session.share.copyFailed.title": "URL konnte nicht in die Zwischenablage kopiert werden",
|
||||
"toast.session.share.success.title": "Sitzung geteilt",
|
||||
"toast.session.share.success.description": "Teilen-URL in die Zwischenablage kopiert!",
|
||||
"toast.session.share.failed.title": "Sitzung konnte nicht geteilt werden",
|
||||
"toast.session.share.failed.description": "Beim Teilen der Sitzung ist ein Fehler aufgetreten",
|
||||
|
||||
"toast.session.unshare.success.title": "Teilen der Sitzung aufgehoben",
|
||||
"toast.session.unshare.success.description": "Teilen der Sitzung erfolgreich aufgehoben!",
|
||||
"toast.session.unshare.failed.title": "Aufheben des Teilens fehlgeschlagen",
|
||||
"toast.session.unshare.failed.description": "Beim Aufheben des Teilens ist ein Fehler aufgetreten",
|
||||
|
||||
"toast.session.listFailed.title": "Sitzungen für {{project}} konnten nicht geladen werden",
|
||||
|
||||
"toast.update.title": "Update verfügbar",
|
||||
"toast.update.description": "Eine neue Version von OpenCode ({{version}}) ist zur Installation verfügbar.",
|
||||
"toast.update.action.installRestart": "Installieren und neu starten",
|
||||
"toast.update.action.notYet": "Noch nicht",
|
||||
|
||||
"error.page.title": "Etwas ist schiefgelaufen",
|
||||
"error.page.description": "Beim Laden der Anwendung ist ein Fehler aufgetreten.",
|
||||
"error.page.details.label": "Fehlerdetails",
|
||||
"error.page.action.restart": "Neustart",
|
||||
"error.page.action.checking": "Prüfen...",
|
||||
"error.page.action.checkUpdates": "Nach Updates suchen",
|
||||
"error.page.action.updateTo": "Auf {{version}} aktualisieren",
|
||||
"error.page.report.prefix": "Bitte melden Sie diesen Fehler dem OpenCode-Team",
|
||||
"error.page.report.discord": "auf Discord",
|
||||
"error.page.version": "Version: {{version}}",
|
||||
|
||||
"error.dev.rootNotFound":
|
||||
"Wurzelelement nicht gefunden. Haben Sie vergessen, es in Ihre index.html aufzunehmen? Oder wurde das id-Attribut falsch geschrieben?",
|
||||
|
||||
"error.globalSync.connectFailed": "Verbindung zum Server fehlgeschlagen. Läuft ein Server unter `{{url}}`?",
|
||||
|
||||
"error.chain.unknown": "Unbekannter Fehler",
|
||||
"error.chain.causedBy": "Verursacht durch:",
|
||||
"error.chain.apiError": "API-Fehler",
|
||||
"error.chain.status": "Status: {{status}}",
|
||||
"error.chain.retryable": "Wiederholbar: {{retryable}}",
|
||||
"error.chain.responseBody": "Antwort-Body:\n{{body}}",
|
||||
"error.chain.didYouMean": "Meinten Sie: {{suggestions}}",
|
||||
"error.chain.modelNotFound": "Modell nicht gefunden: {{provider}}/{{model}}",
|
||||
"error.chain.checkConfig": "Überprüfen Sie Ihre Konfiguration (opencode.json) auf Anbieter-/Modellnamen",
|
||||
"error.chain.mcpFailed":
|
||||
'MCP-Server "{{name}}" fehlgeschlagen. Hinweis: OpenCode unterstützt noch keine MCP-Authentifizierung.',
|
||||
"error.chain.providerAuthFailed": "Anbieter-Authentifizierung fehlgeschlagen ({{provider}}): {{message}}",
|
||||
"error.chain.providerInitFailed":
|
||||
'Anbieter "{{provider}}" konnte nicht initialisiert werden. Überprüfen Sie Anmeldeinformationen und Konfiguration.',
|
||||
"error.chain.configJsonInvalid": "Konfigurationsdatei unter {{path}} ist kein gültiges JSON(C)",
|
||||
"error.chain.configJsonInvalidWithMessage":
|
||||
"Konfigurationsdatei unter {{path}} ist kein gültiges JSON(C): {{message}}",
|
||||
"error.chain.configDirectoryTypo":
|
||||
'Verzeichnis "{{dir}}" in {{path}} ist ungültig. Benennen Sie das Verzeichnis in "{{suggestion}}" um oder entfernen Sie es. Dies ist ein häufiger Tippfehler.',
|
||||
"error.chain.configFrontmatterError": "Frontmatter in {{path}} konnte nicht geparst werden:\n{{message}}",
|
||||
"error.chain.configInvalid": "Konfigurationsdatei unter {{path}} ist ungültig",
|
||||
"error.chain.configInvalidWithMessage": "Konfigurationsdatei unter {{path}} ist ungültig: {{message}}",
|
||||
|
||||
"notification.permission.title": "Berechtigung erforderlich",
|
||||
"notification.permission.description": "{{sessionTitle}} in {{projectName}} benötigt Berechtigung",
|
||||
"notification.question.title": "Frage",
|
||||
"notification.question.description": "{{sessionTitle}} in {{projectName}} hat eine Frage",
|
||||
"notification.action.goToSession": "Zur Sitzung gehen",
|
||||
|
||||
"notification.session.responseReady.title": "Antwort bereit",
|
||||
"notification.session.error.title": "Sitzungsfehler",
|
||||
"notification.session.error.fallbackDescription": "Ein Fehler ist aufgetreten",
|
||||
|
||||
"home.recentProjects": "Letzte Projekte",
|
||||
"home.empty.title": "Keine letzten Projekte",
|
||||
"home.empty.description": "Starten Sie, indem Sie ein lokales Projekt öffnen",
|
||||
|
||||
"session.tab.session": "Sitzung",
|
||||
"session.tab.review": "Überprüfung",
|
||||
"session.tab.context": "Kontext",
|
||||
"session.review.filesChanged": "{{count}} Dateien geändert",
|
||||
"session.review.loadingChanges": "Lade Änderungen...",
|
||||
"session.review.empty": "Noch keine Änderungen in dieser Sitzung",
|
||||
"session.messages.renderEarlier": "Frühere Nachrichten rendern",
|
||||
"session.messages.loadingEarlier": "Lade frühere Nachrichten...",
|
||||
"session.messages.loadEarlier": "Frühere Nachrichten laden",
|
||||
"session.messages.loading": "Lade Nachrichten...",
|
||||
|
||||
"session.context.addToContext": "{{selection}} zum Kontext hinzufügen",
|
||||
|
||||
"session.new.worktree.main": "Haupt-Branch",
|
||||
"session.new.worktree.mainWithBranch": "Haupt-Branch ({{branch}})",
|
||||
"session.new.worktree.create": "Neuen Worktree erstellen",
|
||||
"session.new.lastModified": "Zuletzt geändert",
|
||||
|
||||
"session.header.search.placeholder": "{{project}} durchsuchen",
|
||||
|
||||
"session.share.popover.title": "Im Web veröffentlichen",
|
||||
"session.share.popover.description.shared":
|
||||
"Diese Sitzung ist öffentlich im Web. Sie ist für jeden mit dem Link zugänglich.",
|
||||
"session.share.popover.description.unshared":
|
||||
"Sitzung öffentlich im Web teilen. Sie wird für jeden mit dem Link zugänglich sein.",
|
||||
"session.share.action.share": "Teilen",
|
||||
"session.share.action.publish": "Veröffentlichen",
|
||||
"session.share.action.publishing": "Veröffentliche...",
|
||||
"session.share.action.unpublish": "Veröffentlichung aufheben",
|
||||
"session.share.action.unpublishing": "Hebe Veröffentlichung auf...",
|
||||
"session.share.action.view": "Ansehen",
|
||||
"session.share.copy.copied": "Kopiert",
|
||||
"session.share.copy.copyLink": "Link kopieren",
|
||||
|
||||
"lsp.tooltip.none": "Keine LSP-Server",
|
||||
"lsp.label.connected": "{{count}} LSP",
|
||||
|
||||
"prompt.loading": "Lade Prompt...",
|
||||
"terminal.loading": "Lade Terminal...",
|
||||
"terminal.title": "Terminal",
|
||||
"terminal.title.numbered": "Terminal {{number}}",
|
||||
|
||||
"common.closeTab": "Tab schließen",
|
||||
"common.dismiss": "Verwerfen",
|
||||
"common.requestFailed": "Anfrage fehlgeschlagen",
|
||||
"common.moreOptions": "Weitere Optionen",
|
||||
"common.learnMore": "Mehr erfahren",
|
||||
"common.rename": "Umbenennen",
|
||||
"common.reset": "Zurücksetzen",
|
||||
"common.delete": "Löschen",
|
||||
"common.close": "Schließen",
|
||||
"common.edit": "Bearbeiten",
|
||||
"common.loadMore": "Mehr laden",
|
||||
|
||||
"sidebar.settings": "Einstellungen",
|
||||
"sidebar.help": "Hilfe",
|
||||
"sidebar.workspaces.enable": "Arbeitsbereiche aktivieren",
|
||||
"sidebar.workspaces.disable": "Arbeitsbereiche deaktivieren",
|
||||
"sidebar.gettingStarted.title": "Erste Schritte",
|
||||
"sidebar.gettingStarted.line1": "OpenCode enthält kostenlose Modelle, damit Sie sofort loslegen können.",
|
||||
"sidebar.gettingStarted.line2":
|
||||
"Verbinden Sie einen beliebigen Anbieter, um Modelle wie Claude, GPT, Gemini usw. zu nutzen.",
|
||||
"sidebar.project.recentSessions": "Letzte Sitzungen",
|
||||
"sidebar.project.viewAllSessions": "Alle Sitzungen anzeigen",
|
||||
|
||||
"settings.section.desktop": "Desktop",
|
||||
"settings.tab.general": "Allgemein",
|
||||
"settings.tab.shortcuts": "Tastenkombinationen",
|
||||
|
||||
"settings.general.section.appearance": "Erscheinungsbild",
|
||||
"settings.general.section.notifications": "Systembenachrichtigungen",
|
||||
"settings.general.section.sounds": "Soundeffekte",
|
||||
|
||||
"settings.general.row.language.title": "Sprache",
|
||||
"settings.general.row.language.description": "Die Anzeigesprache für OpenCode ändern",
|
||||
"settings.general.row.appearance.title": "Erscheinungsbild",
|
||||
"settings.general.row.appearance.description": "Anpassen, wie OpenCode auf Ihrem Gerät aussieht",
|
||||
"settings.general.row.theme.title": "Thema",
|
||||
"settings.general.row.theme.description": "Das Thema von OpenCode anpassen.",
|
||||
"settings.general.row.font.title": "Schriftart",
|
||||
"settings.general.row.font.description": "Die in Codeblöcken verwendete Monospace-Schriftart anpassen",
|
||||
|
||||
"settings.general.notifications.agent.title": "Agent",
|
||||
"settings.general.notifications.agent.description":
|
||||
"Systembenachrichtigung anzeigen, wenn der Agent fertig ist oder Aufmerksamkeit benötigt",
|
||||
"settings.general.notifications.permissions.title": "Berechtigungen",
|
||||
"settings.general.notifications.permissions.description":
|
||||
"Systembenachrichtigung anzeigen, wenn eine Berechtigung erforderlich ist",
|
||||
"settings.general.notifications.errors.title": "Fehler",
|
||||
"settings.general.notifications.errors.description": "Systembenachrichtigung anzeigen, wenn ein Fehler auftritt",
|
||||
|
||||
"settings.general.sounds.agent.title": "Agent",
|
||||
"settings.general.sounds.agent.description": "Ton abspielen, wenn der Agent fertig ist oder Aufmerksamkeit benötigt",
|
||||
"settings.general.sounds.permissions.title": "Berechtigungen",
|
||||
"settings.general.sounds.permissions.description": "Ton abspielen, wenn eine Berechtigung erforderlich ist",
|
||||
"settings.general.sounds.errors.title": "Fehler",
|
||||
"settings.general.sounds.errors.description": "Ton abspielen, wenn ein Fehler auftritt",
|
||||
|
||||
"settings.shortcuts.title": "Tastenkombinationen",
|
||||
"settings.shortcuts.reset.button": "Auf Standard zurücksetzen",
|
||||
"settings.shortcuts.reset.toast.title": "Tastenkombinationen zurückgesetzt",
|
||||
"settings.shortcuts.reset.toast.description": "Die Tastenkombinationen wurden auf die Standardwerte zurückgesetzt.",
|
||||
"settings.shortcuts.conflict.title": "Tastenkombination bereits in Verwendung",
|
||||
"settings.shortcuts.conflict.description": "{{keybind}} ist bereits {{titles}} zugewiesen.",
|
||||
"settings.shortcuts.unassigned": "Nicht zugewiesen",
|
||||
"settings.shortcuts.pressKeys": "Tasten drücken",
|
||||
"settings.shortcuts.search.placeholder": "Tastenkürzel suchen",
|
||||
"settings.shortcuts.search.empty": "Keine Tastenkürzel gefunden",
|
||||
|
||||
"settings.shortcuts.group.general": "Allgemein",
|
||||
"settings.shortcuts.group.session": "Sitzung",
|
||||
"settings.shortcuts.group.navigation": "Navigation",
|
||||
"settings.shortcuts.group.modelAndAgent": "Modell und Agent",
|
||||
"settings.shortcuts.group.terminal": "Terminal",
|
||||
"settings.shortcuts.group.prompt": "Prompt",
|
||||
|
||||
"settings.providers.title": "Anbieter",
|
||||
"settings.providers.description": "Anbietereinstellungen können hier konfiguriert werden.",
|
||||
"settings.models.title": "Modelle",
|
||||
"settings.models.description": "Modelleinstellungen können hier konfiguriert werden.",
|
||||
"settings.agents.title": "Agenten",
|
||||
"settings.agents.description": "Agenteneinstellungen können hier konfiguriert werden.",
|
||||
"settings.commands.title": "Befehle",
|
||||
"settings.commands.description": "Befehlseinstellungen können hier konfiguriert werden.",
|
||||
"settings.mcp.title": "MCP",
|
||||
"settings.mcp.description": "MCP-Einstellungen können hier konfiguriert werden.",
|
||||
|
||||
"settings.permissions.title": "Berechtigungen",
|
||||
"settings.permissions.description": "Steuern Sie, welche Tools der Server standardmäßig verwenden darf.",
|
||||
"settings.permissions.section.tools": "Tools",
|
||||
"settings.permissions.toast.updateFailed.title": "Berechtigungen konnten nicht aktualisiert werden",
|
||||
|
||||
"settings.permissions.action.allow": "Erlauben",
|
||||
"settings.permissions.action.ask": "Fragen",
|
||||
"settings.permissions.action.deny": "Verweigern",
|
||||
|
||||
"settings.permissions.tool.read.title": "Lesen",
|
||||
"settings.permissions.tool.read.description": "Lesen einer Datei (stimmt mit dem Dateipfad überein)",
|
||||
"settings.permissions.tool.edit.title": "Bearbeiten",
|
||||
"settings.permissions.tool.edit.description":
|
||||
"Dateien ändern, einschließlich Bearbeitungen, Schreibvorgängen, Patches und Mehrfachbearbeitungen",
|
||||
"settings.permissions.tool.glob.title": "Glob",
|
||||
"settings.permissions.tool.glob.description": "Dateien mithilfe von Glob-Mustern abgleichen",
|
||||
"settings.permissions.tool.grep.title": "Grep",
|
||||
"settings.permissions.tool.grep.description": "Dateiinhalte mit regulären Ausdrücken durchsuchen",
|
||||
"settings.permissions.tool.list.title": "Auflisten",
|
||||
"settings.permissions.tool.list.description": "Dateien in einem Verzeichnis auflisten",
|
||||
"settings.permissions.tool.bash.title": "Bash",
|
||||
"settings.permissions.tool.bash.description": "Shell-Befehle ausführen",
|
||||
"settings.permissions.tool.task.title": "Aufgabe",
|
||||
"settings.permissions.tool.task.description": "Unteragenten starten",
|
||||
"settings.permissions.tool.skill.title": "Fähigkeit",
|
||||
"settings.permissions.tool.skill.description": "Eine Fähigkeit nach Namen laden",
|
||||
"settings.permissions.tool.lsp.title": "LSP",
|
||||
"settings.permissions.tool.lsp.description": "Language-Server-Abfragen ausführen",
|
||||
"settings.permissions.tool.todoread.title": "Todo lesen",
|
||||
"settings.permissions.tool.todoread.description": "Die Todo-Liste lesen",
|
||||
"settings.permissions.tool.todowrite.title": "Todo schreiben",
|
||||
"settings.permissions.tool.todowrite.description": "Die Todo-Liste aktualisieren",
|
||||
"settings.permissions.tool.webfetch.title": "Web-Abruf",
|
||||
"settings.permissions.tool.webfetch.description": "Inhalt von einer URL abrufen",
|
||||
"settings.permissions.tool.websearch.title": "Web-Suche",
|
||||
"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.description": "Zugriff auf Dateien außerhalb des Projektverzeichnisses",
|
||||
"settings.permissions.tool.doom_loop.title": "Doom Loop",
|
||||
"settings.permissions.tool.doom_loop.description": "Wiederholte Tool-Aufrufe mit identischer Eingabe erkennen",
|
||||
|
||||
"workspace.new": "Neuer Arbeitsbereich",
|
||||
"workspace.type.local": "lokal",
|
||||
"workspace.type.sandbox": "Sandbox",
|
||||
"workspace.create.failed.title": "Arbeitsbereich konnte nicht erstellt werden",
|
||||
"workspace.delete.failed.title": "Arbeitsbereich konnte nicht gelöscht werden",
|
||||
"workspace.resetting.title": "Arbeitsbereich wird zurückgesetzt",
|
||||
"workspace.resetting.description": "Dies kann eine Minute dauern.",
|
||||
"workspace.reset.failed.title": "Arbeitsbereich konnte nicht zurückgesetzt werden",
|
||||
"workspace.reset.success.title": "Arbeitsbereich zurückgesetzt",
|
||||
"workspace.reset.success.description": "Der Arbeitsbereich entspricht jetzt dem Standard-Branch.",
|
||||
"workspace.status.checking": "Suche nach nicht zusammengeführten Änderungen...",
|
||||
"workspace.status.error": "Git-Status konnte nicht überprüft werden.",
|
||||
"workspace.status.clean": "Keine nicht zusammengeführten Änderungen erkannt.",
|
||||
"workspace.status.dirty": "Nicht zusammengeführte Änderungen in diesem Arbeitsbereich erkannt.",
|
||||
"workspace.delete.title": "Arbeitsbereich löschen",
|
||||
"workspace.delete.confirm": 'Arbeitsbereich "{{name}}" löschen?',
|
||||
"workspace.delete.button": "Arbeitsbereich löschen",
|
||||
"workspace.reset.title": "Arbeitsbereich zurücksetzen",
|
||||
"workspace.reset.confirm": 'Arbeitsbereich "{{name}}" zurücksetzen?',
|
||||
"workspace.reset.button": "Arbeitsbereich zurücksetzen",
|
||||
"workspace.reset.archived.none": "Keine aktiven Sitzungen werden archiviert.",
|
||||
"workspace.reset.archived.one": "1 Sitzung wird archiviert.",
|
||||
"workspace.reset.archived.many": "{{count}} Sitzungen werden archiviert.",
|
||||
"workspace.reset.note": "Dadurch wird der Arbeitsbereich auf den Standard-Branch zurückgesetzt.",
|
||||
} satisfies Partial<Record<Keys, string>>
|
||||
638
packages/app/src/i18n/en.ts
Normal file
638
packages/app/src/i18n/en.ts
Normal file
@@ -0,0 +1,638 @@
|
||||
export const dict = {
|
||||
"command.category.suggested": "Suggested",
|
||||
"command.category.view": "View",
|
||||
"command.category.project": "Project",
|
||||
"command.category.provider": "Provider",
|
||||
"command.category.server": "Server",
|
||||
"command.category.session": "Session",
|
||||
"command.category.theme": "Theme",
|
||||
"command.category.language": "Language",
|
||||
"command.category.file": "File",
|
||||
"command.category.terminal": "Terminal",
|
||||
"command.category.model": "Model",
|
||||
"command.category.mcp": "MCP",
|
||||
"command.category.agent": "Agent",
|
||||
"command.category.permissions": "Permissions",
|
||||
"command.category.workspace": "Workspace",
|
||||
"command.category.settings": "Settings",
|
||||
|
||||
"theme.scheme.system": "System",
|
||||
"theme.scheme.light": "Light",
|
||||
"theme.scheme.dark": "Dark",
|
||||
|
||||
"command.sidebar.toggle": "Toggle sidebar",
|
||||
"command.project.open": "Open project",
|
||||
"command.provider.connect": "Connect provider",
|
||||
"command.server.switch": "Switch server",
|
||||
"command.settings.open": "Open settings",
|
||||
"command.session.previous": "Previous session",
|
||||
"command.session.next": "Next session",
|
||||
"command.session.archive": "Archive session",
|
||||
|
||||
"command.palette": "Command palette",
|
||||
|
||||
"command.theme.cycle": "Cycle theme",
|
||||
"command.theme.set": "Use theme: {{theme}}",
|
||||
"command.theme.scheme.cycle": "Cycle color scheme",
|
||||
"command.theme.scheme.set": "Use color scheme: {{scheme}}",
|
||||
|
||||
"command.language.cycle": "Cycle language",
|
||||
"command.language.set": "Use language: {{language}}",
|
||||
|
||||
"command.session.new": "New session",
|
||||
"command.file.open": "Open file",
|
||||
"command.file.open.description": "Search files and commands",
|
||||
"command.terminal.toggle": "Toggle terminal",
|
||||
"command.review.toggle": "Toggle review",
|
||||
"command.terminal.new": "New terminal",
|
||||
"command.terminal.new.description": "Create a new terminal tab",
|
||||
"command.steps.toggle": "Toggle steps",
|
||||
"command.steps.toggle.description": "Show or hide steps for the current message",
|
||||
"command.message.previous": "Previous message",
|
||||
"command.message.previous.description": "Go to the previous user message",
|
||||
"command.message.next": "Next message",
|
||||
"command.message.next.description": "Go to the next user message",
|
||||
"command.model.choose": "Choose model",
|
||||
"command.model.choose.description": "Select a different model",
|
||||
"command.mcp.toggle": "Toggle MCPs",
|
||||
"command.mcp.toggle.description": "Toggle MCPs",
|
||||
"command.agent.cycle": "Cycle agent",
|
||||
"command.agent.cycle.description": "Switch to the next agent",
|
||||
"command.agent.cycle.reverse": "Cycle agent backwards",
|
||||
"command.agent.cycle.reverse.description": "Switch to the previous agent",
|
||||
"command.model.variant.cycle": "Cycle thinking effort",
|
||||
"command.model.variant.cycle.description": "Switch to the next effort level",
|
||||
"command.permissions.autoaccept.enable": "Auto-accept edits",
|
||||
"command.permissions.autoaccept.disable": "Stop auto-accepting edits",
|
||||
"command.session.undo": "Undo",
|
||||
"command.session.undo.description": "Undo the last message",
|
||||
"command.session.redo": "Redo",
|
||||
"command.session.redo.description": "Redo the last undone message",
|
||||
"command.session.compact": "Compact session",
|
||||
"command.session.compact.description": "Summarize the session to reduce context size",
|
||||
"command.session.fork": "Fork from message",
|
||||
"command.session.fork.description": "Create a new session from a previous message",
|
||||
"command.session.share": "Share session",
|
||||
"command.session.share.description": "Share this session and copy the URL to clipboard",
|
||||
"command.session.unshare": "Unshare session",
|
||||
"command.session.unshare.description": "Stop sharing this session",
|
||||
|
||||
"palette.search.placeholder": "Search files and commands",
|
||||
"palette.empty": "No results found",
|
||||
"palette.group.commands": "Commands",
|
||||
"palette.group.files": "Files",
|
||||
|
||||
"dialog.provider.search.placeholder": "Search providers",
|
||||
"dialog.provider.empty": "No providers found",
|
||||
"dialog.provider.group.popular": "Popular",
|
||||
"dialog.provider.group.other": "Other",
|
||||
"dialog.provider.tag.recommended": "Recommended",
|
||||
"dialog.provider.anthropic.note": "Connect with Claude Pro/Max or API key",
|
||||
|
||||
"dialog.model.select.title": "Select model",
|
||||
"dialog.model.search.placeholder": "Search models",
|
||||
"dialog.model.empty": "No model results",
|
||||
"dialog.model.manage": "Manage models",
|
||||
"dialog.model.manage.description": "Customize which models appear in the model selector.",
|
||||
|
||||
"dialog.model.unpaid.freeModels.title": "Free models provided by OpenCode",
|
||||
"dialog.model.unpaid.addMore.title": "Add more models from popular providers",
|
||||
|
||||
"dialog.provider.viewAll": "View all providers",
|
||||
|
||||
"provider.connect.title": "Connect {{provider}}",
|
||||
"provider.connect.title.anthropicProMax": "Login with Claude Pro/Max",
|
||||
"provider.connect.selectMethod": "Select login method for {{provider}}.",
|
||||
"provider.connect.method.apiKey": "API key",
|
||||
"provider.connect.status.inProgress": "Authorization in progress...",
|
||||
"provider.connect.status.waiting": "Waiting for authorization...",
|
||||
"provider.connect.status.failed": "Authorization failed: {{error}}",
|
||||
"provider.connect.apiKey.description":
|
||||
"Enter your {{provider}} API key to connect your account and use {{provider}} models in OpenCode.",
|
||||
"provider.connect.apiKey.label": "{{provider}} API key",
|
||||
"provider.connect.apiKey.placeholder": "API key",
|
||||
"provider.connect.apiKey.required": "API key is required",
|
||||
"provider.connect.opencodeZen.line1":
|
||||
"OpenCode Zen gives you access to a curated set of reliable optimized models for coding agents.",
|
||||
"provider.connect.opencodeZen.line2":
|
||||
"With a single API key you'll get access to models such as Claude, GPT, Gemini, GLM and more.",
|
||||
"provider.connect.opencodeZen.visit.prefix": "Visit ",
|
||||
"provider.connect.opencodeZen.visit.link": "opencode.ai/zen",
|
||||
"provider.connect.opencodeZen.visit.suffix": " to collect your API key.",
|
||||
"provider.connect.oauth.code.visit.prefix": "Visit ",
|
||||
"provider.connect.oauth.code.visit.link": "this link",
|
||||
"provider.connect.oauth.code.visit.suffix":
|
||||
" to collect your authorization code to connect your account and use {{provider}} models in OpenCode.",
|
||||
"provider.connect.oauth.code.label": "{{method}} authorization code",
|
||||
"provider.connect.oauth.code.placeholder": "Authorization code",
|
||||
"provider.connect.oauth.code.required": "Authorization code is required",
|
||||
"provider.connect.oauth.code.invalid": "Invalid authorization code",
|
||||
"provider.connect.oauth.auto.visit.prefix": "Visit ",
|
||||
"provider.connect.oauth.auto.visit.link": "this link",
|
||||
"provider.connect.oauth.auto.visit.suffix":
|
||||
" and enter the code below to connect your account and use {{provider}} models in OpenCode.",
|
||||
"provider.connect.oauth.auto.confirmationCode": "Confirmation code",
|
||||
"provider.connect.toast.connected.title": "{{provider}} connected",
|
||||
"provider.connect.toast.connected.description": "{{provider}} models are now available to use.",
|
||||
|
||||
"model.tag.free": "Free",
|
||||
"model.tag.latest": "Latest",
|
||||
"model.provider.anthropic": "Anthropic",
|
||||
"model.provider.openai": "OpenAI",
|
||||
"model.provider.google": "Google",
|
||||
"model.provider.xai": "xAI",
|
||||
"model.provider.meta": "Meta",
|
||||
"model.input.text": "text",
|
||||
"model.input.image": "image",
|
||||
"model.input.audio": "audio",
|
||||
"model.input.video": "video",
|
||||
"model.input.pdf": "pdf",
|
||||
"model.tooltip.allows": "Allows: {{inputs}}",
|
||||
"model.tooltip.reasoning.allowed": "Allows reasoning",
|
||||
"model.tooltip.reasoning.none": "No reasoning",
|
||||
"model.tooltip.context": "Context limit {{limit}}",
|
||||
|
||||
"common.search.placeholder": "Search",
|
||||
"common.loading": "Loading",
|
||||
"common.loading.ellipsis": "...",
|
||||
"common.cancel": "Cancel",
|
||||
"common.submit": "Submit",
|
||||
"common.save": "Save",
|
||||
"common.saving": "Saving...",
|
||||
"common.default": "Default",
|
||||
"common.attachment": "attachment",
|
||||
|
||||
"prompt.placeholder.shell": "Enter shell command...",
|
||||
"prompt.placeholder.normal": 'Ask anything... "{{example}}"',
|
||||
"prompt.mode.shell": "Shell",
|
||||
"prompt.mode.shell.exit": "esc to exit",
|
||||
|
||||
"prompt.example.1": "Fix a TODO in the codebase",
|
||||
"prompt.example.2": "What is the tech stack of this project?",
|
||||
"prompt.example.3": "Fix broken tests",
|
||||
"prompt.example.4": "Explain how authentication works",
|
||||
"prompt.example.5": "Find and fix security vulnerabilities",
|
||||
"prompt.example.6": "Add unit tests for the user service",
|
||||
"prompt.example.7": "Refactor this function to be more readable",
|
||||
"prompt.example.8": "What does this error mean?",
|
||||
"prompt.example.9": "Help me debug this issue",
|
||||
"prompt.example.10": "Generate API documentation",
|
||||
"prompt.example.11": "Optimize database queries",
|
||||
"prompt.example.12": "Add input validation",
|
||||
"prompt.example.13": "Create a new component for...",
|
||||
"prompt.example.14": "How do I deploy this project?",
|
||||
"prompt.example.15": "Review my code for best practices",
|
||||
"prompt.example.16": "Add error handling to this function",
|
||||
"prompt.example.17": "Explain this regex pattern",
|
||||
"prompt.example.18": "Convert this to TypeScript",
|
||||
"prompt.example.19": "Add logging throughout the codebase",
|
||||
"prompt.example.20": "What dependencies are outdated?",
|
||||
"prompt.example.21": "Help me write a migration script",
|
||||
"prompt.example.22": "Implement caching for this endpoint",
|
||||
"prompt.example.23": "Add pagination to this list",
|
||||
"prompt.example.24": "Create a CLI command for...",
|
||||
"prompt.example.25": "How do environment variables work here?",
|
||||
|
||||
"prompt.popover.emptyResults": "No matching results",
|
||||
"prompt.popover.emptyCommands": "No matching commands",
|
||||
"prompt.dropzone.label": "Drop images or PDFs here",
|
||||
"prompt.slash.badge.custom": "custom",
|
||||
"prompt.context.active": "active",
|
||||
"prompt.context.includeActiveFile": "Include active file",
|
||||
"prompt.action.attachFile": "Attach file",
|
||||
"prompt.action.send": "Send",
|
||||
"prompt.action.stop": "Stop",
|
||||
|
||||
"prompt.toast.pasteUnsupported.title": "Unsupported paste",
|
||||
"prompt.toast.pasteUnsupported.description": "Only images or PDFs can be pasted here.",
|
||||
"prompt.toast.modelAgentRequired.title": "Select an agent and model",
|
||||
"prompt.toast.modelAgentRequired.description": "Choose an agent and model before sending a prompt.",
|
||||
"prompt.toast.worktreeCreateFailed.title": "Failed to create worktree",
|
||||
"prompt.toast.sessionCreateFailed.title": "Failed to create session",
|
||||
"prompt.toast.shellSendFailed.title": "Failed to send shell command",
|
||||
"prompt.toast.commandSendFailed.title": "Failed to send command",
|
||||
"prompt.toast.promptSendFailed.title": "Failed to send prompt",
|
||||
|
||||
"dialog.mcp.title": "MCPs",
|
||||
"dialog.mcp.description": "{{enabled}} of {{total}} enabled",
|
||||
"dialog.mcp.empty": "No MCPs configured",
|
||||
|
||||
"mcp.status.connected": "connected",
|
||||
"mcp.status.failed": "failed",
|
||||
"mcp.status.needs_auth": "needs auth",
|
||||
"mcp.status.disabled": "disabled",
|
||||
|
||||
"dialog.fork.empty": "No messages to fork from",
|
||||
|
||||
"dialog.directory.search.placeholder": "Search folders",
|
||||
"dialog.directory.empty": "No folders found",
|
||||
|
||||
"dialog.server.title": "Servers",
|
||||
"dialog.server.description": "Switch which OpenCode server this app connects to.",
|
||||
"dialog.server.search.placeholder": "Search servers",
|
||||
"dialog.server.empty": "No servers yet",
|
||||
"dialog.server.add.title": "Add a server",
|
||||
"dialog.server.add.url": "Server URL",
|
||||
"dialog.server.add.placeholder": "http://localhost:4096",
|
||||
"dialog.server.add.error": "Could not connect to server",
|
||||
"dialog.server.add.checking": "Checking...",
|
||||
"dialog.server.add.button": "Add",
|
||||
"dialog.server.default.title": "Default server",
|
||||
"dialog.server.default.description":
|
||||
"Connect to this server on app launch instead of starting a local server. Requires restart.",
|
||||
"dialog.server.default.none": "No server selected",
|
||||
"dialog.server.default.set": "Set current server as default",
|
||||
"dialog.server.default.clear": "Clear",
|
||||
|
||||
"dialog.project.edit.title": "Edit project",
|
||||
"dialog.project.edit.name": "Name",
|
||||
"dialog.project.edit.icon": "Icon",
|
||||
"dialog.project.edit.icon.alt": "Project icon",
|
||||
"dialog.project.edit.icon.hint": "Click or drag an image",
|
||||
"dialog.project.edit.icon.recommended": "Recommended: 128x128px",
|
||||
"dialog.project.edit.color": "Color",
|
||||
|
||||
"context.breakdown.title": "Context Breakdown",
|
||||
"context.breakdown.note": 'Approximate breakdown of input tokens. "Other" includes tool definitions and overhead.',
|
||||
"context.breakdown.system": "System",
|
||||
"context.breakdown.user": "User",
|
||||
"context.breakdown.assistant": "Assistant",
|
||||
"context.breakdown.tool": "Tool Calls",
|
||||
"context.breakdown.other": "Other",
|
||||
|
||||
"context.systemPrompt.title": "System Prompt",
|
||||
"context.rawMessages.title": "Raw messages",
|
||||
|
||||
"context.stats.session": "Session",
|
||||
"context.stats.messages": "Messages",
|
||||
"context.stats.provider": "Provider",
|
||||
"context.stats.model": "Model",
|
||||
"context.stats.limit": "Context Limit",
|
||||
"context.stats.totalTokens": "Total Tokens",
|
||||
"context.stats.usage": "Usage",
|
||||
"context.stats.inputTokens": "Input Tokens",
|
||||
"context.stats.outputTokens": "Output Tokens",
|
||||
"context.stats.reasoningTokens": "Reasoning Tokens",
|
||||
"context.stats.cacheTokens": "Cache Tokens (read/write)",
|
||||
"context.stats.userMessages": "User Messages",
|
||||
"context.stats.assistantMessages": "Assistant Messages",
|
||||
"context.stats.totalCost": "Total Cost",
|
||||
"context.stats.sessionCreated": "Session Created",
|
||||
"context.stats.lastActivity": "Last Activity",
|
||||
|
||||
"context.usage.tokens": "Tokens",
|
||||
"context.usage.usage": "Usage",
|
||||
"context.usage.cost": "Cost",
|
||||
"context.usage.clickToView": "Click to view context",
|
||||
|
||||
"language.en": "English",
|
||||
"language.zh": "Chinese (Simplified)",
|
||||
"language.zht": "Chinese (Traditional)",
|
||||
"language.ko": "Korean",
|
||||
"language.de": "German",
|
||||
"language.es": "Spanish",
|
||||
"language.fr": "French",
|
||||
"language.ja": "Japanese",
|
||||
"language.da": "Danish",
|
||||
"language.ru": "Russian",
|
||||
"language.pl": "Polish",
|
||||
|
||||
"toast.language.title": "Language",
|
||||
"toast.language.description": "Switched to {{language}}",
|
||||
|
||||
"toast.theme.title": "Theme switched",
|
||||
"toast.scheme.title": "Color scheme",
|
||||
|
||||
"toast.permissions.autoaccept.on.title": "Auto-accepting edits",
|
||||
"toast.permissions.autoaccept.on.description": "Edit and write permissions will be automatically approved",
|
||||
"toast.permissions.autoaccept.off.title": "Stopped auto-accepting edits",
|
||||
"toast.permissions.autoaccept.off.description": "Edit and write permissions will require approval",
|
||||
|
||||
"toast.model.none.title": "No model selected",
|
||||
"toast.model.none.description": "Connect a provider to summarize this session",
|
||||
|
||||
"toast.file.loadFailed.title": "Failed to load file",
|
||||
|
||||
"toast.session.share.copyFailed.title": "Failed to copy URL to clipboard",
|
||||
"toast.session.share.success.title": "Session shared",
|
||||
"toast.session.share.success.description": "Share URL copied to clipboard!",
|
||||
"toast.session.share.failed.title": "Failed to share session",
|
||||
"toast.session.share.failed.description": "An error occurred while sharing the session",
|
||||
|
||||
"toast.session.unshare.success.title": "Session unshared",
|
||||
"toast.session.unshare.success.description": "Session unshared successfully!",
|
||||
"toast.session.unshare.failed.title": "Failed to unshare session",
|
||||
"toast.session.unshare.failed.description": "An error occurred while unsharing the session",
|
||||
|
||||
"toast.session.listFailed.title": "Failed to load sessions for {{project}}",
|
||||
|
||||
"toast.update.title": "Update available",
|
||||
"toast.update.description": "A new version of OpenCode ({{version}}) is now available to install.",
|
||||
"toast.update.action.installRestart": "Install and restart",
|
||||
"toast.update.action.notYet": "Not yet",
|
||||
|
||||
"error.page.title": "Something went wrong",
|
||||
"error.page.description": "An error occurred while loading the application.",
|
||||
"error.page.details.label": "Error Details",
|
||||
"error.page.action.restart": "Restart",
|
||||
"error.page.action.checking": "Checking...",
|
||||
"error.page.action.checkUpdates": "Check for updates",
|
||||
"error.page.action.updateTo": "Update to {{version}}",
|
||||
"error.page.report.prefix": "Please report this error to the OpenCode team",
|
||||
"error.page.report.discord": "on Discord",
|
||||
"error.page.version": "Version: {{version}}",
|
||||
|
||||
"error.dev.rootNotFound":
|
||||
"Root element not found. Did you forget to add it to your index.html? Or maybe the id attribute got misspelled?",
|
||||
|
||||
"error.globalSync.connectFailed": "Could not connect to server. Is there a server running at `{{url}}`?",
|
||||
|
||||
"error.chain.unknown": "Unknown error",
|
||||
"error.chain.causedBy": "Caused by:",
|
||||
"error.chain.apiError": "API error",
|
||||
"error.chain.status": "Status: {{status}}",
|
||||
"error.chain.retryable": "Retryable: {{retryable}}",
|
||||
"error.chain.responseBody": "Response body:\n{{body}}",
|
||||
"error.chain.didYouMean": "Did you mean: {{suggestions}}",
|
||||
"error.chain.modelNotFound": "Model not found: {{provider}}/{{model}}",
|
||||
"error.chain.checkConfig": "Check your config (opencode.json) provider/model names",
|
||||
"error.chain.mcpFailed": 'MCP server "{{name}}" failed. Note, OpenCode does not support MCP authentication yet.',
|
||||
"error.chain.providerAuthFailed": "Provider authentication failed ({{provider}}): {{message}}",
|
||||
"error.chain.providerInitFailed":
|
||||
'Failed to initialize provider "{{provider}}". Check credentials and configuration.',
|
||||
"error.chain.configJsonInvalid": "Config file at {{path}} is not valid JSON(C)",
|
||||
"error.chain.configJsonInvalidWithMessage": "Config file at {{path}} is not valid JSON(C): {{message}}",
|
||||
"error.chain.configDirectoryTypo":
|
||||
'Directory "{{dir}}" in {{path}} is not valid. Rename the directory to "{{suggestion}}" or remove it. This is a common typo.',
|
||||
"error.chain.configFrontmatterError": "Failed to parse frontmatter in {{path}}:\n{{message}}",
|
||||
"error.chain.configInvalid": "Config file at {{path}} is invalid",
|
||||
"error.chain.configInvalidWithMessage": "Config file at {{path}} is invalid: {{message}}",
|
||||
|
||||
"notification.permission.title": "Permission required",
|
||||
"notification.permission.description": "{{sessionTitle}} in {{projectName}} needs permission",
|
||||
"notification.question.title": "Question",
|
||||
"notification.question.description": "{{sessionTitle}} in {{projectName}} has a question",
|
||||
"notification.action.goToSession": "Go to session",
|
||||
|
||||
"notification.session.responseReady.title": "Response ready",
|
||||
"notification.session.error.title": "Session error",
|
||||
"notification.session.error.fallbackDescription": "An error occurred",
|
||||
|
||||
"home.recentProjects": "Recent projects",
|
||||
"home.empty.title": "No recent projects",
|
||||
"home.empty.description": "Get started by opening a local project",
|
||||
|
||||
"session.tab.session": "Session",
|
||||
"session.tab.review": "Review",
|
||||
"session.tab.context": "Context",
|
||||
"session.review.filesChanged": "{{count}} Files Changed",
|
||||
"session.review.loadingChanges": "Loading changes...",
|
||||
"session.review.empty": "No changes in this session yet",
|
||||
"session.messages.renderEarlier": "Render earlier messages",
|
||||
"session.messages.loadingEarlier": "Loading earlier messages...",
|
||||
"session.messages.loadEarlier": "Load earlier messages",
|
||||
"session.messages.loading": "Loading messages...",
|
||||
"session.messages.jumpToLatest": "Jump to latest",
|
||||
|
||||
"session.context.addToContext": "Add {{selection}} to context",
|
||||
|
||||
"session.new.worktree.main": "Main branch",
|
||||
"session.new.worktree.mainWithBranch": "Main branch ({{branch}})",
|
||||
"session.new.worktree.create": "Create new worktree",
|
||||
"session.new.lastModified": "Last modified",
|
||||
|
||||
"session.header.search.placeholder": "Search {{project}}",
|
||||
|
||||
"session.share.popover.title": "Publish on web",
|
||||
"session.share.popover.description.shared":
|
||||
"This session is public on the web. It is accessible to anyone with the link.",
|
||||
"session.share.popover.description.unshared":
|
||||
"Share session publicly on the web. It will be accessible to anyone with the link.",
|
||||
"session.share.action.share": "Share",
|
||||
"session.share.action.publish": "Publish",
|
||||
"session.share.action.publishing": "Publishing...",
|
||||
"session.share.action.unpublish": "Unpublish",
|
||||
"session.share.action.unpublishing": "Unpublishing...",
|
||||
"session.share.action.view": "View",
|
||||
"session.share.copy.copied": "Copied",
|
||||
"session.share.copy.copyLink": "Copy link",
|
||||
|
||||
"lsp.tooltip.none": "No LSP servers",
|
||||
"lsp.label.connected": "{{count}} LSP",
|
||||
|
||||
"prompt.loading": "Loading prompt...",
|
||||
"terminal.loading": "Loading terminal...",
|
||||
"terminal.title": "Terminal",
|
||||
"terminal.title.numbered": "Terminal {{number}}",
|
||||
"terminal.connectionLost.title": "Connection Lost",
|
||||
"terminal.connectionLost.description":
|
||||
"The terminal connection was interrupted. This can happen when the server restarts.",
|
||||
|
||||
"common.closeTab": "Close tab",
|
||||
"common.dismiss": "Dismiss",
|
||||
"common.requestFailed": "Request failed",
|
||||
"common.moreOptions": "More options",
|
||||
"common.learnMore": "Learn more",
|
||||
"common.rename": "Rename",
|
||||
"common.reset": "Reset",
|
||||
"common.delete": "Delete",
|
||||
"common.close": "Close",
|
||||
"common.edit": "Edit",
|
||||
"common.loadMore": "Load more",
|
||||
"common.key.esc": "ESC",
|
||||
|
||||
"sidebar.menu.toggle": "Toggle menu",
|
||||
"sidebar.settings": "Settings",
|
||||
"sidebar.help": "Help",
|
||||
"sidebar.workspaces.enable": "Enable workspaces",
|
||||
"sidebar.workspaces.disable": "Disable workspaces",
|
||||
"sidebar.gettingStarted.title": "Getting started",
|
||||
"sidebar.gettingStarted.line1": "OpenCode includes free models so you can start immediately.",
|
||||
"sidebar.gettingStarted.line2": "Connect any provider to use models, inc. Claude, GPT, Gemini etc.",
|
||||
"sidebar.project.recentSessions": "Recent sessions",
|
||||
"sidebar.project.viewAllSessions": "View all sessions",
|
||||
|
||||
"settings.section.desktop": "Desktop",
|
||||
"settings.tab.general": "General",
|
||||
"settings.tab.shortcuts": "Shortcuts",
|
||||
|
||||
"settings.general.section.appearance": "Appearance",
|
||||
"settings.general.section.notifications": "System notifications",
|
||||
"settings.general.section.sounds": "Sound effects",
|
||||
|
||||
"settings.general.row.language.title": "Language",
|
||||
"settings.general.row.language.description": "Change the display language for OpenCode",
|
||||
"settings.general.row.appearance.title": "Appearance",
|
||||
"settings.general.row.appearance.description": "Customise how OpenCode looks on your device",
|
||||
"settings.general.row.theme.title": "Theme",
|
||||
"settings.general.row.theme.description": "Customise how OpenCode is themed.",
|
||||
"settings.general.row.font.title": "Font",
|
||||
"settings.general.row.font.description": "Customise the mono font used in code blocks",
|
||||
"font.option.ibmPlexMono": "IBM Plex Mono",
|
||||
"font.option.cascadiaCode": "Cascadia Code",
|
||||
"font.option.firaCode": "Fira Code",
|
||||
"font.option.hack": "Hack",
|
||||
"font.option.inconsolata": "Inconsolata",
|
||||
"font.option.intelOneMono": "Intel One Mono",
|
||||
"font.option.jetbrainsMono": "JetBrains Mono",
|
||||
"font.option.mesloLgs": "Meslo LGS",
|
||||
"font.option.robotoMono": "Roboto Mono",
|
||||
"font.option.sourceCodePro": "Source Code Pro",
|
||||
"font.option.ubuntuMono": "Ubuntu Mono",
|
||||
"sound.option.alert01": "Alert 01",
|
||||
"sound.option.alert02": "Alert 02",
|
||||
"sound.option.alert03": "Alert 03",
|
||||
"sound.option.alert04": "Alert 04",
|
||||
"sound.option.alert05": "Alert 05",
|
||||
"sound.option.alert06": "Alert 06",
|
||||
"sound.option.alert07": "Alert 07",
|
||||
"sound.option.alert08": "Alert 08",
|
||||
"sound.option.alert09": "Alert 09",
|
||||
"sound.option.alert10": "Alert 10",
|
||||
"sound.option.bipbop01": "Bip-bop 01",
|
||||
"sound.option.bipbop02": "Bip-bop 02",
|
||||
"sound.option.bipbop03": "Bip-bop 03",
|
||||
"sound.option.bipbop04": "Bip-bop 04",
|
||||
"sound.option.bipbop05": "Bip-bop 05",
|
||||
"sound.option.bipbop06": "Bip-bop 06",
|
||||
"sound.option.bipbop07": "Bip-bop 07",
|
||||
"sound.option.bipbop08": "Bip-bop 08",
|
||||
"sound.option.bipbop09": "Bip-bop 09",
|
||||
"sound.option.bipbop10": "Bip-bop 10",
|
||||
"sound.option.staplebops01": "Staplebops 01",
|
||||
"sound.option.staplebops02": "Staplebops 02",
|
||||
"sound.option.staplebops03": "Staplebops 03",
|
||||
"sound.option.staplebops04": "Staplebops 04",
|
||||
"sound.option.staplebops05": "Staplebops 05",
|
||||
"sound.option.staplebops06": "Staplebops 06",
|
||||
"sound.option.staplebops07": "Staplebops 07",
|
||||
"sound.option.nope01": "Nope 01",
|
||||
"sound.option.nope02": "Nope 02",
|
||||
"sound.option.nope03": "Nope 03",
|
||||
"sound.option.nope04": "Nope 04",
|
||||
"sound.option.nope05": "Nope 05",
|
||||
"sound.option.nope06": "Nope 06",
|
||||
"sound.option.nope07": "Nope 07",
|
||||
"sound.option.nope08": "Nope 08",
|
||||
"sound.option.nope09": "Nope 09",
|
||||
"sound.option.nope10": "Nope 10",
|
||||
"sound.option.nope11": "Nope 11",
|
||||
"sound.option.nope12": "Nope 12",
|
||||
"sound.option.yup01": "Yup 01",
|
||||
"sound.option.yup02": "Yup 02",
|
||||
"sound.option.yup03": "Yup 03",
|
||||
"sound.option.yup04": "Yup 04",
|
||||
"sound.option.yup05": "Yup 05",
|
||||
"sound.option.yup06": "Yup 06",
|
||||
|
||||
"settings.general.notifications.agent.title": "Agent",
|
||||
"settings.general.notifications.agent.description":
|
||||
"Show system notification when the agent is complete or needs attention",
|
||||
"settings.general.notifications.permissions.title": "Permissions",
|
||||
"settings.general.notifications.permissions.description": "Show system notification when a permission is required",
|
||||
"settings.general.notifications.errors.title": "Errors",
|
||||
"settings.general.notifications.errors.description": "Show system notification when an error occurs",
|
||||
|
||||
"settings.general.sounds.agent.title": "Agent",
|
||||
"settings.general.sounds.agent.description": "Play sound when the agent is complete or needs attention",
|
||||
"settings.general.sounds.permissions.title": "Permissions",
|
||||
"settings.general.sounds.permissions.description": "Play sound when a permission is required",
|
||||
"settings.general.sounds.errors.title": "Errors",
|
||||
"settings.general.sounds.errors.description": "Play sound when an error occurs",
|
||||
|
||||
"settings.shortcuts.title": "Keyboard shortcuts",
|
||||
"settings.shortcuts.reset.button": "Reset to defaults",
|
||||
"settings.shortcuts.reset.toast.title": "Shortcuts reset",
|
||||
"settings.shortcuts.reset.toast.description": "Keyboard shortcuts have been reset to defaults.",
|
||||
"settings.shortcuts.conflict.title": "Shortcut already in use",
|
||||
"settings.shortcuts.conflict.description": "{{keybind}} is already assigned to {{titles}}.",
|
||||
"settings.shortcuts.unassigned": "Unassigned",
|
||||
"settings.shortcuts.pressKeys": "Press keys",
|
||||
"settings.shortcuts.search.placeholder": "Search shortcuts",
|
||||
"settings.shortcuts.search.empty": "No shortcuts found",
|
||||
|
||||
"settings.shortcuts.group.general": "General",
|
||||
"settings.shortcuts.group.session": "Session",
|
||||
"settings.shortcuts.group.navigation": "Navigation",
|
||||
"settings.shortcuts.group.modelAndAgent": "Model and agent",
|
||||
"settings.shortcuts.group.terminal": "Terminal",
|
||||
"settings.shortcuts.group.prompt": "Prompt",
|
||||
|
||||
"settings.providers.title": "Providers",
|
||||
"settings.providers.description": "Provider settings will be configurable here.",
|
||||
"settings.models.title": "Models",
|
||||
"settings.models.description": "Model settings will be configurable here.",
|
||||
"settings.agents.title": "Agents",
|
||||
"settings.agents.description": "Agent settings will be configurable here.",
|
||||
"settings.commands.title": "Commands",
|
||||
"settings.commands.description": "Command settings will be configurable here.",
|
||||
"settings.mcp.title": "MCP",
|
||||
"settings.mcp.description": "MCP settings will be configurable here.",
|
||||
|
||||
"settings.permissions.title": "Permissions",
|
||||
"settings.permissions.description": "Control what tools the server can use by default.",
|
||||
"settings.permissions.section.tools": "Tools",
|
||||
"settings.permissions.toast.updateFailed.title": "Failed to update permissions",
|
||||
|
||||
"settings.permissions.action.allow": "Allow",
|
||||
"settings.permissions.action.ask": "Ask",
|
||||
"settings.permissions.action.deny": "Deny",
|
||||
|
||||
"settings.permissions.tool.read.title": "Read",
|
||||
"settings.permissions.tool.read.description": "Reading a file (matches the file path)",
|
||||
"settings.permissions.tool.edit.title": "Edit",
|
||||
"settings.permissions.tool.edit.description": "Modify files, including edits, writes, patches, and multi-edits",
|
||||
"settings.permissions.tool.glob.title": "Glob",
|
||||
"settings.permissions.tool.glob.description": "Match files using glob patterns",
|
||||
"settings.permissions.tool.grep.title": "Grep",
|
||||
"settings.permissions.tool.grep.description": "Search file contents using regular expressions",
|
||||
"settings.permissions.tool.list.title": "List",
|
||||
"settings.permissions.tool.list.description": "List files within a directory",
|
||||
"settings.permissions.tool.bash.title": "Bash",
|
||||
"settings.permissions.tool.bash.description": "Run shell commands",
|
||||
"settings.permissions.tool.task.title": "Task",
|
||||
"settings.permissions.tool.task.description": "Launch sub-agents",
|
||||
"settings.permissions.tool.skill.title": "Skill",
|
||||
"settings.permissions.tool.skill.description": "Load a skill by name",
|
||||
"settings.permissions.tool.lsp.title": "LSP",
|
||||
"settings.permissions.tool.lsp.description": "Run language server queries",
|
||||
"settings.permissions.tool.todoread.title": "Todo Read",
|
||||
"settings.permissions.tool.todoread.description": "Read the todo list",
|
||||
"settings.permissions.tool.todowrite.title": "Todo Write",
|
||||
"settings.permissions.tool.todowrite.description": "Update the todo list",
|
||||
"settings.permissions.tool.webfetch.title": "Web Fetch",
|
||||
"settings.permissions.tool.webfetch.description": "Fetch content from a URL",
|
||||
"settings.permissions.tool.websearch.title": "Web Search",
|
||||
"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.description": "Access files outside the project directory",
|
||||
"settings.permissions.tool.doom_loop.title": "Doom Loop",
|
||||
"settings.permissions.tool.doom_loop.description": "Detect repeated tool calls with identical input",
|
||||
|
||||
"workspace.new": "New workspace",
|
||||
"workspace.type.local": "local",
|
||||
"workspace.type.sandbox": "sandbox",
|
||||
"workspace.create.failed.title": "Failed to create workspace",
|
||||
"workspace.delete.failed.title": "Failed to delete workspace",
|
||||
"workspace.resetting.title": "Resetting workspace",
|
||||
"workspace.resetting.description": "This may take a minute.",
|
||||
"workspace.reset.failed.title": "Failed to reset workspace",
|
||||
"workspace.reset.success.title": "Workspace reset",
|
||||
"workspace.reset.success.description": "Workspace now matches the default branch.",
|
||||
"workspace.status.checking": "Checking for unmerged changes...",
|
||||
"workspace.status.error": "Unable to verify git status.",
|
||||
"workspace.status.clean": "No unmerged changes detected.",
|
||||
"workspace.status.dirty": "Unmerged changes detected in this workspace.",
|
||||
"workspace.delete.title": "Delete workspace",
|
||||
"workspace.delete.confirm": 'Delete workspace "{{name}}"?',
|
||||
"workspace.delete.button": "Delete workspace",
|
||||
"workspace.reset.title": "Reset workspace",
|
||||
"workspace.reset.confirm": 'Reset workspace "{{name}}"?',
|
||||
"workspace.reset.button": "Reset workspace",
|
||||
"workspace.reset.archived.none": "No active sessions will be archived.",
|
||||
"workspace.reset.archived.one": "1 session will be archived.",
|
||||
"workspace.reset.archived.many": "{{count}} sessions will be archived.",
|
||||
"workspace.reset.note": "This will reset the workspace to match the default branch.",
|
||||
}
|
||||
563
packages/app/src/i18n/es.ts
Normal file
563
packages/app/src/i18n/es.ts
Normal file
@@ -0,0 +1,563 @@
|
||||
export const dict = {
|
||||
"command.category.suggested": "Sugerido",
|
||||
"command.category.view": "Ver",
|
||||
"command.category.project": "Proyecto",
|
||||
"command.category.provider": "Proveedor",
|
||||
"command.category.server": "Servidor",
|
||||
"command.category.session": "Sesión",
|
||||
"command.category.theme": "Tema",
|
||||
"command.category.language": "Idioma",
|
||||
"command.category.file": "Archivo",
|
||||
"command.category.terminal": "Terminal",
|
||||
"command.category.model": "Modelo",
|
||||
"command.category.mcp": "MCP",
|
||||
"command.category.agent": "Agente",
|
||||
"command.category.permissions": "Permisos",
|
||||
"command.category.workspace": "Espacio de trabajo",
|
||||
|
||||
"theme.scheme.system": "Sistema",
|
||||
"theme.scheme.light": "Claro",
|
||||
"theme.scheme.dark": "Oscuro",
|
||||
|
||||
"command.sidebar.toggle": "Alternar barra lateral",
|
||||
"command.project.open": "Abrir proyecto",
|
||||
"command.provider.connect": "Conectar proveedor",
|
||||
"command.server.switch": "Cambiar servidor",
|
||||
"command.session.previous": "Sesión anterior",
|
||||
"command.session.next": "Siguiente sesión",
|
||||
"command.session.archive": "Archivar sesión",
|
||||
|
||||
"command.palette": "Paleta de comandos",
|
||||
|
||||
"command.theme.cycle": "Alternar tema",
|
||||
"command.theme.set": "Usar tema: {{theme}}",
|
||||
"command.theme.scheme.cycle": "Alternar esquema de color",
|
||||
"command.theme.scheme.set": "Usar esquema de color: {{scheme}}",
|
||||
|
||||
"command.language.cycle": "Alternar idioma",
|
||||
"command.language.set": "Usar idioma: {{language}}",
|
||||
|
||||
"command.session.new": "Nueva sesión",
|
||||
"command.file.open": "Abrir archivo",
|
||||
"command.file.open.description": "Buscar archivos y comandos",
|
||||
"command.terminal.toggle": "Alternar terminal",
|
||||
"command.review.toggle": "Alternar revisión",
|
||||
"command.terminal.new": "Nueva terminal",
|
||||
"command.terminal.new.description": "Crear una nueva pestaña de terminal",
|
||||
"command.steps.toggle": "Alternar pasos",
|
||||
"command.steps.toggle.description": "Mostrar u ocultar pasos para el mensaje actual",
|
||||
"command.message.previous": "Mensaje anterior",
|
||||
"command.message.previous.description": "Ir al mensaje de usuario anterior",
|
||||
"command.message.next": "Siguiente mensaje",
|
||||
"command.message.next.description": "Ir al siguiente mensaje de usuario",
|
||||
"command.model.choose": "Elegir modelo",
|
||||
"command.model.choose.description": "Seleccionar un modelo diferente",
|
||||
"command.mcp.toggle": "Alternar MCPs",
|
||||
"command.mcp.toggle.description": "Alternar MCPs",
|
||||
"command.agent.cycle": "Alternar agente",
|
||||
"command.agent.cycle.description": "Cambiar al siguiente agente",
|
||||
"command.agent.cycle.reverse": "Alternar agente hacia atrás",
|
||||
"command.agent.cycle.reverse.description": "Cambiar al agente anterior",
|
||||
"command.model.variant.cycle": "Alternar esfuerzo de pensamiento",
|
||||
"command.model.variant.cycle.description": "Cambiar al siguiente nivel de esfuerzo",
|
||||
"command.permissions.autoaccept.enable": "Aceptar ediciones automáticamente",
|
||||
"command.permissions.autoaccept.disable": "Dejar de aceptar ediciones automáticamente",
|
||||
"command.session.undo": "Deshacer",
|
||||
"command.session.undo.description": "Deshacer el último mensaje",
|
||||
"command.session.redo": "Rehacer",
|
||||
"command.session.redo.description": "Rehacer el último mensaje deshecho",
|
||||
"command.session.compact": "Compactar sesión",
|
||||
"command.session.compact.description": "Resumir la sesión para reducir el tamaño del contexto",
|
||||
"command.session.fork": "Bifurcar desde mensaje",
|
||||
"command.session.fork.description": "Crear una nueva sesión desde un mensaje anterior",
|
||||
"command.session.share": "Compartir sesión",
|
||||
"command.session.share.description": "Compartir esta sesión y copiar la URL al portapapeles",
|
||||
"command.session.unshare": "Dejar de compartir sesión",
|
||||
"command.session.unshare.description": "Dejar de compartir esta sesión",
|
||||
|
||||
"palette.search.placeholder": "Buscar archivos y comandos",
|
||||
"palette.empty": "No se encontraron resultados",
|
||||
"palette.group.commands": "Comandos",
|
||||
"palette.group.files": "Archivos",
|
||||
|
||||
"dialog.provider.search.placeholder": "Buscar proveedores",
|
||||
"dialog.provider.empty": "No se encontraron proveedores",
|
||||
"dialog.provider.group.popular": "Popular",
|
||||
"dialog.provider.group.other": "Otro",
|
||||
"dialog.provider.tag.recommended": "Recomendado",
|
||||
"dialog.provider.anthropic.note": "Conectar con Claude Pro/Max o clave API",
|
||||
|
||||
"dialog.model.select.title": "Seleccionar modelo",
|
||||
"dialog.model.search.placeholder": "Buscar modelos",
|
||||
"dialog.model.empty": "Sin resultados de modelos",
|
||||
"dialog.model.manage": "Gestionar modelos",
|
||||
"dialog.model.manage.description": "Personalizar qué modelos aparecen en el selector de modelos.",
|
||||
|
||||
"dialog.model.unpaid.freeModels.title": "Modelos gratuitos proporcionados por OpenCode",
|
||||
"dialog.model.unpaid.addMore.title": "Añadir más modelos de proveedores populares",
|
||||
|
||||
"dialog.provider.viewAll": "Ver todos los proveedores",
|
||||
|
||||
"provider.connect.title": "Conectar {{provider}}",
|
||||
"provider.connect.title.anthropicProMax": "Iniciar sesión con Claude Pro/Max",
|
||||
"provider.connect.selectMethod": "Seleccionar método de inicio de sesión para {{provider}}.",
|
||||
"provider.connect.method.apiKey": "Clave API",
|
||||
"provider.connect.status.inProgress": "Autorización en progreso...",
|
||||
"provider.connect.status.waiting": "Esperando autorización...",
|
||||
"provider.connect.status.failed": "Autorización fallida: {{error}}",
|
||||
"provider.connect.apiKey.description":
|
||||
"Introduce tu clave API de {{provider}} para conectar tu cuenta y usar modelos de {{provider}} en OpenCode.",
|
||||
"provider.connect.apiKey.label": "Clave API de {{provider}}",
|
||||
"provider.connect.apiKey.placeholder": "Clave API",
|
||||
"provider.connect.apiKey.required": "La clave API es obligatoria",
|
||||
"provider.connect.opencodeZen.line1":
|
||||
"OpenCode Zen te da acceso a un conjunto curado de modelos fiables optimizados para agentes de programación.",
|
||||
"provider.connect.opencodeZen.line2":
|
||||
"Con una sola clave API obtendrás acceso a modelos como Claude, GPT, Gemini, GLM y más.",
|
||||
"provider.connect.opencodeZen.visit.prefix": "Visita ",
|
||||
"provider.connect.opencodeZen.visit.suffix": " para obtener tu clave API.",
|
||||
"provider.connect.oauth.code.visit.prefix": "Visita ",
|
||||
"provider.connect.oauth.code.visit.link": "este enlace",
|
||||
"provider.connect.oauth.code.visit.suffix":
|
||||
" para obtener tu código de autorización para conectar tu cuenta y usar modelos de {{provider}} en OpenCode.",
|
||||
"provider.connect.oauth.code.label": "Código de autorización {{method}}",
|
||||
"provider.connect.oauth.code.placeholder": "Código de autorización",
|
||||
"provider.connect.oauth.code.required": "El código de autorización es obligatorio",
|
||||
"provider.connect.oauth.code.invalid": "Código de autorización inválido",
|
||||
"provider.connect.oauth.auto.visit.prefix": "Visita ",
|
||||
"provider.connect.oauth.auto.visit.link": "este enlace",
|
||||
"provider.connect.oauth.auto.visit.suffix":
|
||||
" e introduce el código a continuación para conectar tu cuenta y usar modelos de {{provider}} en OpenCode.",
|
||||
"provider.connect.oauth.auto.confirmationCode": "Código de confirmación",
|
||||
"provider.connect.toast.connected.title": "{{provider}} conectado",
|
||||
"provider.connect.toast.connected.description": "Los modelos de {{provider}} ahora están disponibles para usar.",
|
||||
|
||||
"model.tag.free": "Gratis",
|
||||
"model.tag.latest": "Último",
|
||||
|
||||
"common.search.placeholder": "Buscar",
|
||||
"common.loading": "Cargando",
|
||||
"common.cancel": "Cancelar",
|
||||
"common.submit": "Enviar",
|
||||
"common.save": "Guardar",
|
||||
"common.saving": "Guardando...",
|
||||
"common.default": "Predeterminado",
|
||||
"common.attachment": "adjunto",
|
||||
|
||||
"prompt.placeholder.shell": "Introduce comando de shell...",
|
||||
"prompt.placeholder.normal": 'Pregunta cualquier cosa... "{{example}}"',
|
||||
"prompt.mode.shell": "Shell",
|
||||
"prompt.mode.shell.exit": "esc para salir",
|
||||
|
||||
"prompt.example.1": "Arreglar un TODO en el código",
|
||||
"prompt.example.2": "¿Cuál es el stack tecnológico de este proyecto?",
|
||||
"prompt.example.3": "Arreglar pruebas rotas",
|
||||
"prompt.example.4": "Explicar cómo funciona la autenticación",
|
||||
"prompt.example.5": "Encontrar y arreglar vulnerabilidades de seguridad",
|
||||
"prompt.example.6": "Añadir pruebas unitarias para el servicio de usuario",
|
||||
"prompt.example.7": "Refactorizar esta función para que sea más legible",
|
||||
"prompt.example.8": "¿Qué significa este error?",
|
||||
"prompt.example.9": "Ayúdame a depurar este problema",
|
||||
"prompt.example.10": "Generar documentación de API",
|
||||
"prompt.example.11": "Optimizar consultas a la base de datos",
|
||||
"prompt.example.12": "Añadir validación de entrada",
|
||||
"prompt.example.13": "Crear un nuevo componente para...",
|
||||
"prompt.example.14": "¿Cómo despliego este proyecto?",
|
||||
"prompt.example.15": "Revisar mi código para mejores prácticas",
|
||||
"prompt.example.16": "Añadir manejo de errores a esta función",
|
||||
"prompt.example.17": "Explicar este patrón de regex",
|
||||
"prompt.example.18": "Convertir esto a TypeScript",
|
||||
"prompt.example.19": "Añadir logging en todo el código",
|
||||
"prompt.example.20": "¿Qué dependencias están desactualizadas?",
|
||||
"prompt.example.21": "Ayúdame a escribir un script de migración",
|
||||
"prompt.example.22": "Implementar caché para este endpoint",
|
||||
"prompt.example.23": "Añadir paginación a esta lista",
|
||||
"prompt.example.24": "Crear un comando CLI para...",
|
||||
"prompt.example.25": "¿Cómo funcionan las variables de entorno aquí?",
|
||||
|
||||
"prompt.popover.emptyResults": "Sin resultados coincidentes",
|
||||
"prompt.popover.emptyCommands": "Sin comandos coincidentes",
|
||||
"prompt.dropzone.label": "Suelta imágenes o PDFs aquí",
|
||||
"prompt.slash.badge.custom": "personalizado",
|
||||
"prompt.context.active": "activo",
|
||||
"prompt.context.includeActiveFile": "Incluir archivo activo",
|
||||
"prompt.action.attachFile": "Adjuntar archivo",
|
||||
"prompt.action.send": "Enviar",
|
||||
"prompt.action.stop": "Detener",
|
||||
|
||||
"prompt.toast.pasteUnsupported.title": "Pegado no soportado",
|
||||
"prompt.toast.pasteUnsupported.description": "Solo se pueden pegar imágenes o PDFs aquí.",
|
||||
"prompt.toast.modelAgentRequired.title": "Selecciona un agente y modelo",
|
||||
"prompt.toast.modelAgentRequired.description": "Elige un agente y modelo antes de enviar un prompt.",
|
||||
"prompt.toast.worktreeCreateFailed.title": "Fallo al crear el árbol de trabajo",
|
||||
"prompt.toast.sessionCreateFailed.title": "Fallo al crear la sesión",
|
||||
"prompt.toast.shellSendFailed.title": "Fallo al enviar comando de shell",
|
||||
"prompt.toast.commandSendFailed.title": "Fallo al enviar comando",
|
||||
"prompt.toast.promptSendFailed.title": "Fallo al enviar prompt",
|
||||
|
||||
"dialog.mcp.title": "MCPs",
|
||||
"dialog.mcp.description": "{{enabled}} de {{total}} habilitados",
|
||||
"dialog.mcp.empty": "No hay MCPs configurados",
|
||||
|
||||
"mcp.status.connected": "conectado",
|
||||
"mcp.status.failed": "fallido",
|
||||
"mcp.status.needs_auth": "necesita auth",
|
||||
"mcp.status.disabled": "deshabilitado",
|
||||
|
||||
"dialog.fork.empty": "No hay mensajes desde donde bifurcar",
|
||||
|
||||
"dialog.directory.search.placeholder": "Buscar carpetas",
|
||||
"dialog.directory.empty": "No se encontraron carpetas",
|
||||
|
||||
"dialog.server.title": "Servidores",
|
||||
"dialog.server.description": "Cambiar a qué servidor de OpenCode se conecta esta app.",
|
||||
"dialog.server.search.placeholder": "Buscar servidores",
|
||||
"dialog.server.empty": "No hay servidores aún",
|
||||
"dialog.server.add.title": "Añadir un servidor",
|
||||
"dialog.server.add.url": "URL del servidor",
|
||||
"dialog.server.add.placeholder": "http://localhost:4096",
|
||||
"dialog.server.add.error": "No se pudo conectar al servidor",
|
||||
"dialog.server.add.checking": "Comprobando...",
|
||||
"dialog.server.add.button": "Añadir",
|
||||
"dialog.server.default.title": "Servidor predeterminado",
|
||||
"dialog.server.default.description":
|
||||
"Conectar a este servidor al iniciar la app en lugar de iniciar un servidor local. Requiere reinicio.",
|
||||
"dialog.server.default.none": "Ningún servidor seleccionado",
|
||||
"dialog.server.default.set": "Establecer servidor actual como predeterminado",
|
||||
"dialog.server.default.clear": "Limpiar",
|
||||
|
||||
"dialog.project.edit.title": "Editar proyecto",
|
||||
"dialog.project.edit.name": "Nombre",
|
||||
"dialog.project.edit.icon": "Icono",
|
||||
"dialog.project.edit.icon.alt": "Icono del proyecto",
|
||||
"dialog.project.edit.icon.hint": "Haz clic o arrastra una imagen",
|
||||
"dialog.project.edit.icon.recommended": "Recomendado: 128x128px",
|
||||
"dialog.project.edit.color": "Color",
|
||||
|
||||
"context.breakdown.title": "Desglose de Contexto",
|
||||
"context.breakdown.note":
|
||||
'Desglose aproximado de tokens de entrada. "Otro" incluye definiciones de herramientas y sobrecarga.',
|
||||
"context.breakdown.system": "Sistema",
|
||||
"context.breakdown.user": "Usuario",
|
||||
"context.breakdown.assistant": "Asistente",
|
||||
"context.breakdown.tool": "Llamadas a herramientas",
|
||||
"context.breakdown.other": "Otro",
|
||||
|
||||
"context.systemPrompt.title": "Prompt del Sistema",
|
||||
"context.rawMessages.title": "Mensajes en bruto",
|
||||
|
||||
"context.stats.session": "Sesión",
|
||||
"context.stats.messages": "Mensajes",
|
||||
"context.stats.provider": "Proveedor",
|
||||
"context.stats.model": "Modelo",
|
||||
"context.stats.limit": "Límite de Contexto",
|
||||
"context.stats.totalTokens": "Tokens Totales",
|
||||
"context.stats.usage": "Uso",
|
||||
"context.stats.inputTokens": "Tokens de Entrada",
|
||||
"context.stats.outputTokens": "Tokens de Salida",
|
||||
"context.stats.reasoningTokens": "Tokens de Razonamiento",
|
||||
"context.stats.cacheTokens": "Tokens de Caché (lectura/escritura)",
|
||||
"context.stats.userMessages": "Mensajes de Usuario",
|
||||
"context.stats.assistantMessages": "Mensajes de Asistente",
|
||||
"context.stats.totalCost": "Costo Total",
|
||||
"context.stats.sessionCreated": "Sesión Creada",
|
||||
"context.stats.lastActivity": "Última Actividad",
|
||||
|
||||
"context.usage.tokens": "Tokens",
|
||||
"context.usage.usage": "Uso",
|
||||
"context.usage.cost": "Costo",
|
||||
"context.usage.clickToView": "Haz clic para ver contexto",
|
||||
|
||||
"language.en": "Inglés",
|
||||
"language.zh": "Chino (simplificado)",
|
||||
"language.zht": "Chino (tradicional)",
|
||||
"language.ko": "Coreano",
|
||||
"language.de": "Alemán",
|
||||
"language.es": "Español",
|
||||
"language.fr": "Francés",
|
||||
"language.ja": "Japonés",
|
||||
"language.da": "Danés",
|
||||
"language.ru": "Ruso",
|
||||
"language.pl": "Polaco",
|
||||
|
||||
"toast.language.title": "Idioma",
|
||||
"toast.language.description": "Cambiado a {{language}}",
|
||||
|
||||
"toast.theme.title": "Tema cambiado",
|
||||
"toast.scheme.title": "Esquema de color",
|
||||
|
||||
"toast.permissions.autoaccept.on.title": "Aceptando ediciones automáticamente",
|
||||
"toast.permissions.autoaccept.on.description": "Los permisos de edición y escritura serán aprobados automáticamente",
|
||||
"toast.permissions.autoaccept.off.title": "Se dejó de aceptar ediciones automáticamente",
|
||||
"toast.permissions.autoaccept.off.description": "Los permisos de edición y escritura requerirán aprobación",
|
||||
|
||||
"toast.model.none.title": "Ningún modelo seleccionado",
|
||||
"toast.model.none.description": "Conecta un proveedor para resumir esta sesión",
|
||||
|
||||
"toast.file.loadFailed.title": "Fallo al cargar archivo",
|
||||
|
||||
"toast.session.share.copyFailed.title": "Fallo al copiar URL al portapapeles",
|
||||
"toast.session.share.success.title": "Sesión compartida",
|
||||
"toast.session.share.success.description": "¡URL compartida copiada al portapapeles!",
|
||||
"toast.session.share.failed.title": "Fallo al compartir sesión",
|
||||
"toast.session.share.failed.description": "Ocurrió un error al compartir la sesión",
|
||||
|
||||
"toast.session.unshare.success.title": "Sesión dejó de compartirse",
|
||||
"toast.session.unshare.success.description": "¡La sesión dejó de compartirse exitosamente!",
|
||||
"toast.session.unshare.failed.title": "Fallo al dejar de compartir sesión",
|
||||
"toast.session.unshare.failed.description": "Ocurrió un error al dejar de compartir la sesión",
|
||||
|
||||
"toast.session.listFailed.title": "Fallo al cargar sesiones para {{project}}",
|
||||
|
||||
"toast.update.title": "Actualización disponible",
|
||||
"toast.update.description": "Una nueva versión de OpenCode ({{version}}) está disponible para instalar.",
|
||||
"toast.update.action.installRestart": "Instalar y reiniciar",
|
||||
"toast.update.action.notYet": "Todavía no",
|
||||
|
||||
"error.page.title": "Algo salió mal",
|
||||
"error.page.description": "Ocurrió un error al cargar la aplicación.",
|
||||
"error.page.details.label": "Detalles del error",
|
||||
"error.page.action.restart": "Reiniciar",
|
||||
"error.page.action.checking": "Comprobando...",
|
||||
"error.page.action.checkUpdates": "Buscar actualizaciones",
|
||||
"error.page.action.updateTo": "Actualizar a {{version}}",
|
||||
"error.page.report.prefix": "Por favor reporta este error al equipo de OpenCode",
|
||||
"error.page.report.discord": "en Discord",
|
||||
"error.page.version": "Versión: {{version}}",
|
||||
|
||||
"error.dev.rootNotFound":
|
||||
"Elemento raíz no encontrado. ¿Olvidaste añadirlo a tu index.html? ¿O tal vez el atributo id está mal escrito?",
|
||||
|
||||
"error.globalSync.connectFailed": "No se pudo conectar al servidor. ¿Hay un servidor ejecutándose en `{{url}}`?",
|
||||
|
||||
"error.chain.unknown": "Error desconocido",
|
||||
"error.chain.causedBy": "Causado por:",
|
||||
"error.chain.apiError": "Error de API",
|
||||
"error.chain.status": "Estado: {{status}}",
|
||||
"error.chain.retryable": "Reintentable: {{retryable}}",
|
||||
"error.chain.responseBody": "Cuerpo de la respuesta:\n{{body}}",
|
||||
"error.chain.didYouMean": "¿Quisiste decir: {{suggestions}}",
|
||||
"error.chain.modelNotFound": "Modelo no encontrado: {{provider}}/{{model}}",
|
||||
"error.chain.checkConfig": "Comprueba los nombres de proveedor/modelo en tu configuración (opencode.json)",
|
||||
"error.chain.mcpFailed": 'El servidor MCP "{{name}}" falló. Nota, OpenCode no soporta autenticación MCP todavía.',
|
||||
"error.chain.providerAuthFailed": "Autenticación de proveedor fallida ({{provider}}): {{message}}",
|
||||
"error.chain.providerInitFailed":
|
||||
'Fallo al inicializar proveedor "{{provider}}". Comprueba credenciales y configuración.',
|
||||
"error.chain.configJsonInvalid": "El archivo de configuración en {{path}} no es un JSON(C) válido",
|
||||
"error.chain.configJsonInvalidWithMessage":
|
||||
"El archivo de configuración en {{path}} no es un JSON(C) válido: {{message}}",
|
||||
"error.chain.configDirectoryTypo":
|
||||
'El directorio "{{dir}}" en {{path}} no es válido. Renombra el directorio a "{{suggestion}}" o elimínalo. Esto es un error tipográfico común.',
|
||||
"error.chain.configFrontmatterError": "Fallo al analizar frontmatter en {{path}}:\n{{message}}",
|
||||
"error.chain.configInvalid": "El archivo de configuración en {{path}} es inválido",
|
||||
"error.chain.configInvalidWithMessage": "El archivo de configuración en {{path}} es inválido: {{message}}",
|
||||
|
||||
"notification.permission.title": "Permiso requerido",
|
||||
"notification.permission.description": "{{sessionTitle}} en {{projectName}} necesita permiso",
|
||||
"notification.question.title": "Pregunta",
|
||||
"notification.question.description": "{{sessionTitle}} en {{projectName}} tiene una pregunta",
|
||||
"notification.action.goToSession": "Ir a sesión",
|
||||
|
||||
"notification.session.responseReady.title": "Respuesta lista",
|
||||
"notification.session.error.title": "Error de sesión",
|
||||
"notification.session.error.fallbackDescription": "Ocurrió un error",
|
||||
|
||||
"home.recentProjects": "Proyectos recientes",
|
||||
"home.empty.title": "Sin proyectos recientes",
|
||||
"home.empty.description": "Empieza abriendo un proyecto local",
|
||||
|
||||
"session.tab.session": "Sesión",
|
||||
"session.tab.review": "Revisión",
|
||||
"session.tab.context": "Contexto",
|
||||
"session.review.filesChanged": "{{count}} Archivos Cambiados",
|
||||
"session.review.loadingChanges": "Cargando cambios...",
|
||||
"session.review.empty": "No hay cambios en esta sesión aún",
|
||||
"session.messages.renderEarlier": "Renderizar mensajes anteriores",
|
||||
"session.messages.loadingEarlier": "Cargando mensajes anteriores...",
|
||||
"session.messages.loadEarlier": "Cargar mensajes anteriores",
|
||||
"session.messages.loading": "Cargando mensajes...",
|
||||
|
||||
"session.context.addToContext": "Añadir {{selection}} al contexto",
|
||||
|
||||
"session.new.worktree.main": "Rama principal",
|
||||
"session.new.worktree.mainWithBranch": "Rama principal ({{branch}})",
|
||||
"session.new.worktree.create": "Crear nuevo árbol de trabajo",
|
||||
"session.new.lastModified": "Última modificación",
|
||||
|
||||
"session.header.search.placeholder": "Buscar {{project}}",
|
||||
|
||||
"session.share.popover.title": "Publicar en web",
|
||||
"session.share.popover.description.shared":
|
||||
"Esta sesión es pública en la web. Es accesible para cualquiera con el enlace.",
|
||||
"session.share.popover.description.unshared":
|
||||
"Compartir sesión públicamente en la web. Será accesible para cualquiera con el enlace.",
|
||||
"session.share.action.share": "Compartir",
|
||||
"session.share.action.publish": "Publicar",
|
||||
"session.share.action.publishing": "Publicando...",
|
||||
"session.share.action.unpublish": "Despublicar",
|
||||
"session.share.action.unpublishing": "Despublicando...",
|
||||
"session.share.action.view": "Ver",
|
||||
"session.share.copy.copied": "Copiado",
|
||||
"session.share.copy.copyLink": "Copiar enlace",
|
||||
|
||||
"lsp.tooltip.none": "Sin servidores LSP",
|
||||
"lsp.label.connected": "{{count}} LSP",
|
||||
|
||||
"prompt.loading": "Cargando prompt...",
|
||||
"terminal.loading": "Cargando terminal...",
|
||||
"terminal.title": "Terminal",
|
||||
"terminal.title.numbered": "Terminal {{number}}",
|
||||
|
||||
"common.closeTab": "Cerrar pestaña",
|
||||
"common.dismiss": "Descartar",
|
||||
"common.requestFailed": "Solicitud fallida",
|
||||
"common.moreOptions": "Más opciones",
|
||||
"common.learnMore": "Saber más",
|
||||
"common.rename": "Renombrar",
|
||||
"common.reset": "Restablecer",
|
||||
"common.delete": "Eliminar",
|
||||
"common.close": "Cerrar",
|
||||
"common.edit": "Editar",
|
||||
"common.loadMore": "Cargar más",
|
||||
|
||||
"sidebar.settings": "Ajustes",
|
||||
"sidebar.help": "Ayuda",
|
||||
"sidebar.workspaces.enable": "Habilitar espacios de trabajo",
|
||||
"sidebar.workspaces.disable": "Deshabilitar espacios de trabajo",
|
||||
"sidebar.gettingStarted.title": "Empezando",
|
||||
"sidebar.gettingStarted.line1": "OpenCode incluye modelos gratuitos para que puedas empezar inmediatamente.",
|
||||
"sidebar.gettingStarted.line2": "Conecta cualquier proveedor para usar modelos, inc. Claude, GPT, Gemini etc.",
|
||||
"sidebar.project.recentSessions": "Sesiones recientes",
|
||||
"sidebar.project.viewAllSessions": "Ver todas las sesiones",
|
||||
|
||||
"settings.section.desktop": "Escritorio",
|
||||
"settings.tab.general": "General",
|
||||
"settings.tab.shortcuts": "Atajos",
|
||||
|
||||
"settings.general.section.appearance": "Apariencia",
|
||||
"settings.general.section.notifications": "Notificaciones del sistema",
|
||||
"settings.general.section.sounds": "Efectos de sonido",
|
||||
|
||||
"settings.general.row.language.title": "Idioma",
|
||||
"settings.general.row.language.description": "Cambiar el idioma de visualización para OpenCode",
|
||||
"settings.general.row.appearance.title": "Apariencia",
|
||||
"settings.general.row.appearance.description": "Personaliza cómo se ve OpenCode en tu dispositivo",
|
||||
"settings.general.row.theme.title": "Tema",
|
||||
"settings.general.row.theme.description": "Personaliza el tema de OpenCode.",
|
||||
"settings.general.row.font.title": "Fuente",
|
||||
"settings.general.row.font.description": "Personaliza la fuente mono usada en bloques de código",
|
||||
|
||||
"settings.general.notifications.agent.title": "Agente",
|
||||
"settings.general.notifications.agent.description":
|
||||
"Mostrar notificación del sistema cuando el agente termine o necesite atención",
|
||||
"settings.general.notifications.permissions.title": "Permisos",
|
||||
"settings.general.notifications.permissions.description":
|
||||
"Mostrar notificación del sistema cuando se requiera un permiso",
|
||||
"settings.general.notifications.errors.title": "Errores",
|
||||
"settings.general.notifications.errors.description": "Mostrar notificación del sistema cuando ocurra un error",
|
||||
|
||||
"settings.general.sounds.agent.title": "Agente",
|
||||
"settings.general.sounds.agent.description": "Reproducir sonido cuando el agente termine o necesite atención",
|
||||
"settings.general.sounds.permissions.title": "Permisos",
|
||||
"settings.general.sounds.permissions.description": "Reproducir sonido cuando se requiera un permiso",
|
||||
"settings.general.sounds.errors.title": "Errores",
|
||||
"settings.general.sounds.errors.description": "Reproducir sonido cuando ocurra un error",
|
||||
|
||||
"settings.shortcuts.title": "Atajos de teclado",
|
||||
"settings.shortcuts.reset.button": "Restablecer a valores predeterminados",
|
||||
"settings.shortcuts.reset.toast.title": "Atajos restablecidos",
|
||||
"settings.shortcuts.reset.toast.description":
|
||||
"Los atajos de teclado han sido restablecidos a los valores predeterminados.",
|
||||
"settings.shortcuts.conflict.title": "Atajo ya en uso",
|
||||
"settings.shortcuts.conflict.description": "{{keybind}} ya está asignado a {{titles}}.",
|
||||
"settings.shortcuts.unassigned": "Sin asignar",
|
||||
"settings.shortcuts.pressKeys": "Presiona teclas",
|
||||
"settings.shortcuts.search.placeholder": "Buscar atajos",
|
||||
"settings.shortcuts.search.empty": "No se encontraron atajos",
|
||||
|
||||
"settings.shortcuts.group.general": "General",
|
||||
"settings.shortcuts.group.session": "Sesión",
|
||||
"settings.shortcuts.group.navigation": "Navegación",
|
||||
"settings.shortcuts.group.modelAndAgent": "Modelo y agente",
|
||||
"settings.shortcuts.group.terminal": "Terminal",
|
||||
"settings.shortcuts.group.prompt": "Prompt",
|
||||
|
||||
"settings.providers.title": "Proveedores",
|
||||
"settings.providers.description": "La configuración de proveedores estará disponible aquí.",
|
||||
"settings.models.title": "Modelos",
|
||||
"settings.models.description": "La configuración de modelos estará disponible aquí.",
|
||||
"settings.agents.title": "Agentes",
|
||||
"settings.agents.description": "La configuración de agentes estará disponible aquí.",
|
||||
"settings.commands.title": "Comandos",
|
||||
"settings.commands.description": "La configuración de comandos estará disponible aquí.",
|
||||
"settings.mcp.title": "MCP",
|
||||
"settings.mcp.description": "La configuración de MCP estará disponible aquí.",
|
||||
|
||||
"settings.permissions.title": "Permisos",
|
||||
"settings.permissions.description": "Controla qué herramientas puede usar el servidor por defecto.",
|
||||
"settings.permissions.section.tools": "Herramientas",
|
||||
"settings.permissions.toast.updateFailed.title": "Fallo al actualizar permisos",
|
||||
|
||||
"settings.permissions.action.allow": "Permitir",
|
||||
"settings.permissions.action.ask": "Preguntar",
|
||||
"settings.permissions.action.deny": "Denegar",
|
||||
|
||||
"settings.permissions.tool.read.title": "Leer",
|
||||
"settings.permissions.tool.read.description": "Leer un archivo (coincide con la ruta del archivo)",
|
||||
"settings.permissions.tool.edit.title": "Editar",
|
||||
"settings.permissions.tool.edit.description":
|
||||
"Modificar archivos, incluyendo ediciones, escrituras, parches y multi-ediciones",
|
||||
"settings.permissions.tool.glob.title": "Glob",
|
||||
"settings.permissions.tool.glob.description": "Coincidir archivos usando patrones glob",
|
||||
"settings.permissions.tool.grep.title": "Grep",
|
||||
"settings.permissions.tool.grep.description": "Buscar contenidos de archivo usando expresiones regulares",
|
||||
"settings.permissions.tool.list.title": "Listar",
|
||||
"settings.permissions.tool.list.description": "Listar archivos dentro de un directorio",
|
||||
"settings.permissions.tool.bash.title": "Bash",
|
||||
"settings.permissions.tool.bash.description": "Ejecutar comandos de shell",
|
||||
"settings.permissions.tool.task.title": "Tarea",
|
||||
"settings.permissions.tool.task.description": "Lanzar sub-agentes",
|
||||
"settings.permissions.tool.skill.title": "Habilidad",
|
||||
"settings.permissions.tool.skill.description": "Cargar una habilidad por nombre",
|
||||
"settings.permissions.tool.lsp.title": "LSP",
|
||||
"settings.permissions.tool.lsp.description": "Ejecutar consultas de servidor de lenguaje",
|
||||
"settings.permissions.tool.todoread.title": "Leer Todo",
|
||||
"settings.permissions.tool.todoread.description": "Leer la lista de tareas",
|
||||
"settings.permissions.tool.todowrite.title": "Escribir Todo",
|
||||
"settings.permissions.tool.todowrite.description": "Actualizar la lista de tareas",
|
||||
"settings.permissions.tool.webfetch.title": "Web Fetch",
|
||||
"settings.permissions.tool.webfetch.description": "Obtener contenido de una URL",
|
||||
"settings.permissions.tool.websearch.title": "Búsqueda 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.description": "Acceder a archivos fuera del directorio del proyecto",
|
||||
"settings.permissions.tool.doom_loop.title": "Bucle Infinito",
|
||||
"settings.permissions.tool.doom_loop.description": "Detectar llamadas a herramientas repetidas con entrada idéntica",
|
||||
|
||||
"workspace.new": "Nuevo espacio de trabajo",
|
||||
"workspace.type.local": "local",
|
||||
"workspace.type.sandbox": "sandbox",
|
||||
"workspace.create.failed.title": "Fallo al crear espacio de trabajo",
|
||||
"workspace.delete.failed.title": "Fallo al eliminar espacio de trabajo",
|
||||
"workspace.resetting.title": "Restableciendo espacio de trabajo",
|
||||
"workspace.resetting.description": "Esto puede tomar un minuto.",
|
||||
"workspace.reset.failed.title": "Fallo al restablecer espacio de trabajo",
|
||||
"workspace.reset.success.title": "Espacio de trabajo restablecido",
|
||||
"workspace.reset.success.description": "El espacio de trabajo ahora coincide con la rama predeterminada.",
|
||||
"workspace.status.checking": "Comprobando cambios no fusionados...",
|
||||
"workspace.status.error": "No se pudo verificar el estado de git.",
|
||||
"workspace.status.clean": "No se detectaron cambios no fusionados.",
|
||||
"workspace.status.dirty": "Cambios no fusionados detectados en este espacio de trabajo.",
|
||||
"workspace.delete.title": "Eliminar espacio de trabajo",
|
||||
"workspace.delete.confirm": '¿Eliminar espacio de trabajo "{{name}}"?',
|
||||
"workspace.delete.button": "Eliminar espacio de trabajo",
|
||||
"workspace.reset.title": "Restablecer espacio de trabajo",
|
||||
"workspace.reset.confirm": '¿Restablecer espacio de trabajo "{{name}}"?',
|
||||
"workspace.reset.button": "Restablecer espacio de trabajo",
|
||||
"workspace.reset.archived.none": "No se archivarán sesiones activas.",
|
||||
"workspace.reset.archived.one": "1 sesión será archivada.",
|
||||
"workspace.reset.archived.many": "{{count}} sesiones serán archivadas.",
|
||||
"workspace.reset.note": "Esto restablecerá el espacio de trabajo para coincidir con la rama predeterminada.",
|
||||
}
|
||||
570
packages/app/src/i18n/fr.ts
Normal file
570
packages/app/src/i18n/fr.ts
Normal file
@@ -0,0 +1,570 @@
|
||||
export const dict = {
|
||||
"command.category.suggested": "Suggéré",
|
||||
"command.category.view": "Affichage",
|
||||
"command.category.project": "Projet",
|
||||
"command.category.provider": "Fournisseur",
|
||||
"command.category.server": "Serveur",
|
||||
"command.category.session": "Session",
|
||||
"command.category.theme": "Thème",
|
||||
"command.category.language": "Langue",
|
||||
"command.category.file": "Fichier",
|
||||
"command.category.terminal": "Terminal",
|
||||
"command.category.model": "Modèle",
|
||||
"command.category.mcp": "MCP",
|
||||
"command.category.agent": "Agent",
|
||||
"command.category.permissions": "Permissions",
|
||||
"command.category.workspace": "Espace de travail",
|
||||
|
||||
"theme.scheme.system": "Système",
|
||||
"theme.scheme.light": "Clair",
|
||||
"theme.scheme.dark": "Sombre",
|
||||
|
||||
"command.sidebar.toggle": "Basculer la barre latérale",
|
||||
"command.project.open": "Ouvrir un projet",
|
||||
"command.provider.connect": "Connecter un fournisseur",
|
||||
"command.server.switch": "Changer de serveur",
|
||||
"command.session.previous": "Session précédente",
|
||||
"command.session.next": "Session suivante",
|
||||
"command.session.archive": "Archiver la session",
|
||||
|
||||
"command.palette": "Palette de commandes",
|
||||
|
||||
"command.theme.cycle": "Changer de thème",
|
||||
"command.theme.set": "Utiliser le thème : {{theme}}",
|
||||
"command.theme.scheme.cycle": "Changer de schéma de couleurs",
|
||||
"command.theme.scheme.set": "Utiliser le schéma de couleurs : {{scheme}}",
|
||||
|
||||
"command.language.cycle": "Changer de langue",
|
||||
"command.language.set": "Utiliser la langue : {{language}}",
|
||||
|
||||
"command.session.new": "Nouvelle session",
|
||||
"command.file.open": "Ouvrir un fichier",
|
||||
"command.file.open.description": "Rechercher des fichiers et des commandes",
|
||||
"command.terminal.toggle": "Basculer le terminal",
|
||||
"command.review.toggle": "Basculer la revue",
|
||||
"command.terminal.new": "Nouveau terminal",
|
||||
"command.terminal.new.description": "Créer un nouvel onglet de terminal",
|
||||
"command.steps.toggle": "Basculer les étapes",
|
||||
"command.steps.toggle.description": "Afficher ou masquer les étapes du message actuel",
|
||||
"command.message.previous": "Message précédent",
|
||||
"command.message.previous.description": "Aller au message utilisateur précédent",
|
||||
"command.message.next": "Message suivant",
|
||||
"command.message.next.description": "Aller au message utilisateur suivant",
|
||||
"command.model.choose": "Choisir le modèle",
|
||||
"command.model.choose.description": "Sélectionner un modèle différent",
|
||||
"command.mcp.toggle": "Basculer MCP",
|
||||
"command.mcp.toggle.description": "Basculer les MCPs",
|
||||
"command.agent.cycle": "Changer d'agent",
|
||||
"command.agent.cycle.description": "Passer à l'agent suivant",
|
||||
"command.agent.cycle.reverse": "Changer d'agent (inverse)",
|
||||
"command.agent.cycle.reverse.description": "Passer à l'agent précédent",
|
||||
"command.model.variant.cycle": "Changer l'effort de réflexion",
|
||||
"command.model.variant.cycle.description": "Passer au niveau d'effort suivant",
|
||||
"command.permissions.autoaccept.enable": "Accepter automatiquement les modifications",
|
||||
"command.permissions.autoaccept.disable": "Arrêter l'acceptation automatique des modifications",
|
||||
"command.session.undo": "Annuler",
|
||||
"command.session.undo.description": "Annuler le dernier message",
|
||||
"command.session.redo": "Rétablir",
|
||||
"command.session.redo.description": "Rétablir le dernier message annulé",
|
||||
"command.session.compact": "Compacter la session",
|
||||
"command.session.compact.description": "Résumer la session pour réduire la taille du contexte",
|
||||
"command.session.fork": "Bifurquer à partir du message",
|
||||
"command.session.fork.description": "Créer une nouvelle session à partir d'un message précédent",
|
||||
"command.session.share": "Partager la session",
|
||||
"command.session.share.description": "Partager cette session et copier l'URL dans le presse-papiers",
|
||||
"command.session.unshare": "Ne plus partager la session",
|
||||
"command.session.unshare.description": "Arrêter de partager cette session",
|
||||
|
||||
"palette.search.placeholder": "Rechercher des fichiers et des commandes",
|
||||
"palette.empty": "Aucun résultat trouvé",
|
||||
"palette.group.commands": "Commandes",
|
||||
"palette.group.files": "Fichiers",
|
||||
|
||||
"dialog.provider.search.placeholder": "Rechercher des fournisseurs",
|
||||
"dialog.provider.empty": "Aucun fournisseur trouvé",
|
||||
"dialog.provider.group.popular": "Populaire",
|
||||
"dialog.provider.group.other": "Autre",
|
||||
"dialog.provider.tag.recommended": "Recommandé",
|
||||
"dialog.provider.anthropic.note": "Connectez-vous avec Claude Pro/Max ou une clé API",
|
||||
|
||||
"dialog.model.select.title": "Sélectionner un modèle",
|
||||
"dialog.model.search.placeholder": "Rechercher des modèles",
|
||||
"dialog.model.empty": "Aucun résultat de modèle",
|
||||
"dialog.model.manage": "Gérer les modèles",
|
||||
"dialog.model.manage.description": "Personnalisez les modèles qui apparaissent dans le sélecteur.",
|
||||
|
||||
"dialog.model.unpaid.freeModels.title": "Modèles gratuits fournis par OpenCode",
|
||||
"dialog.model.unpaid.addMore.title": "Ajouter plus de modèles de fournisseurs populaires",
|
||||
|
||||
"dialog.provider.viewAll": "Voir tous les fournisseurs",
|
||||
|
||||
"provider.connect.title": "Connecter {{provider}}",
|
||||
"provider.connect.title.anthropicProMax": "Connexion avec Claude Pro/Max",
|
||||
"provider.connect.selectMethod": "Sélectionnez la méthode de connexion pour {{provider}}.",
|
||||
"provider.connect.method.apiKey": "Clé API",
|
||||
"provider.connect.status.inProgress": "Autorisation en cours...",
|
||||
"provider.connect.status.waiting": "En attente d'autorisation...",
|
||||
"provider.connect.status.failed": "Échec de l'autorisation : {{error}}",
|
||||
"provider.connect.apiKey.description":
|
||||
"Entrez votre clé API {{provider}} pour connecter votre compte et utiliser les modèles {{provider}} dans OpenCode.",
|
||||
"provider.connect.apiKey.label": "Clé API {{provider}}",
|
||||
"provider.connect.apiKey.placeholder": "Clé API",
|
||||
"provider.connect.apiKey.required": "La clé API est requise",
|
||||
"provider.connect.opencodeZen.line1":
|
||||
"OpenCode Zen vous donne accès à un ensemble sélectionné de modèles fiables et optimisés pour les agents de codage.",
|
||||
"provider.connect.opencodeZen.line2":
|
||||
"Avec une seule clé API, vous aurez accès à des modèles tels que Claude, GPT, Gemini, GLM et plus encore.",
|
||||
"provider.connect.opencodeZen.visit.prefix": "Visitez ",
|
||||
"provider.connect.opencodeZen.visit.suffix": " pour récupérer votre clé API.",
|
||||
"provider.connect.oauth.code.visit.prefix": "Visitez ",
|
||||
"provider.connect.oauth.code.visit.link": "ce lien",
|
||||
"provider.connect.oauth.code.visit.suffix":
|
||||
" pour récupérer votre code d'autorisation afin de connecter votre compte et utiliser les modèles {{provider}} dans OpenCode.",
|
||||
"provider.connect.oauth.code.label": "Code d'autorisation {{method}}",
|
||||
"provider.connect.oauth.code.placeholder": "Code d'autorisation",
|
||||
"provider.connect.oauth.code.required": "Le code d'autorisation est requis",
|
||||
"provider.connect.oauth.code.invalid": "Code d'autorisation invalide",
|
||||
"provider.connect.oauth.auto.visit.prefix": "Visitez ",
|
||||
"provider.connect.oauth.auto.visit.link": "ce lien",
|
||||
"provider.connect.oauth.auto.visit.suffix":
|
||||
" et entrez le code ci-dessous pour connecter votre compte et utiliser les modèles {{provider}} dans OpenCode.",
|
||||
"provider.connect.oauth.auto.confirmationCode": "Code de confirmation",
|
||||
"provider.connect.toast.connected.title": "{{provider}} connecté",
|
||||
"provider.connect.toast.connected.description": "Les modèles {{provider}} sont maintenant disponibles.",
|
||||
|
||||
"model.tag.free": "Gratuit",
|
||||
"model.tag.latest": "Dernier",
|
||||
|
||||
"common.search.placeholder": "Rechercher",
|
||||
"common.loading": "Chargement",
|
||||
"common.cancel": "Annuler",
|
||||
"common.submit": "Soumettre",
|
||||
"common.save": "Enregistrer",
|
||||
"common.saving": "Enregistrement...",
|
||||
"common.default": "Défaut",
|
||||
"common.attachment": "pièce jointe",
|
||||
|
||||
"prompt.placeholder.shell": "Entrez une commande shell...",
|
||||
"prompt.placeholder.normal": 'Demandez n\'importe quoi... "{{example}}"',
|
||||
"prompt.mode.shell": "Shell",
|
||||
"prompt.mode.shell.exit": "esc pour quitter",
|
||||
|
||||
"prompt.example.1": "Corriger un TODO dans la base de code",
|
||||
"prompt.example.2": "Quelle est la pile technique de ce projet ?",
|
||||
"prompt.example.3": "Réparer les tests échoués",
|
||||
"prompt.example.4": "Expliquer comment fonctionne l'authentification",
|
||||
"prompt.example.5": "Trouver et corriger les vulnérabilités de sécurité",
|
||||
"prompt.example.6": "Ajouter des tests unitaires pour le service utilisateur",
|
||||
"prompt.example.7": "Refactoriser cette fonction pour être plus lisible",
|
||||
"prompt.example.8": "Que signifie cette erreur ?",
|
||||
"prompt.example.9": "Aidez-moi à déboguer ce problème",
|
||||
"prompt.example.10": "Générer la documentation de l'API",
|
||||
"prompt.example.11": "Optimiser les requêtes de base de données",
|
||||
"prompt.example.12": "Ajouter une validation d'entrée",
|
||||
"prompt.example.13": "Créer un nouveau composant pour...",
|
||||
"prompt.example.14": "Comment déployer ce projet ?",
|
||||
"prompt.example.15": "Vérifier mon code pour les meilleures pratiques",
|
||||
"prompt.example.16": "Ajouter la gestion des erreurs à cette fonction",
|
||||
"prompt.example.17": "Expliquer ce modèle regex",
|
||||
"prompt.example.18": "Convertir ceci en TypeScript",
|
||||
"prompt.example.19": "Ajouter des logs dans toute la base de code",
|
||||
"prompt.example.20": "Quelles dépendances sont obsolètes ?",
|
||||
"prompt.example.21": "Aidez-moi à écrire un script de migration",
|
||||
"prompt.example.22": "Implémenter la mise en cache pour ce point de terminaison",
|
||||
"prompt.example.23": "Ajouter la pagination à cette liste",
|
||||
"prompt.example.24": "Créer une commande CLI pour...",
|
||||
"prompt.example.25": "Comment fonctionnent les variables d'environnement ici ?",
|
||||
|
||||
"prompt.popover.emptyResults": "Aucun résultat correspondant",
|
||||
"prompt.popover.emptyCommands": "Aucune commande correspondante",
|
||||
"prompt.dropzone.label": "Déposez des images ou des PDF ici",
|
||||
"prompt.slash.badge.custom": "personnalisé",
|
||||
"prompt.context.active": "actif",
|
||||
"prompt.context.includeActiveFile": "Inclure le fichier actif",
|
||||
"prompt.action.attachFile": "Joindre un fichier",
|
||||
"prompt.action.send": "Envoyer",
|
||||
"prompt.action.stop": "Arrêter",
|
||||
|
||||
"prompt.toast.pasteUnsupported.title": "Collage non supporté",
|
||||
"prompt.toast.pasteUnsupported.description": "Seules les images ou les PDF peuvent être collés ici.",
|
||||
"prompt.toast.modelAgentRequired.title": "Sélectionnez un agent et un modèle",
|
||||
"prompt.toast.modelAgentRequired.description": "Choisissez un agent et un modèle avant d'envoyer un message.",
|
||||
"prompt.toast.worktreeCreateFailed.title": "Échec de la création de l'arbre de travail",
|
||||
"prompt.toast.sessionCreateFailed.title": "Échec de la création de la session",
|
||||
"prompt.toast.shellSendFailed.title": "Échec de l'envoi de la commande shell",
|
||||
"prompt.toast.commandSendFailed.title": "Échec de l'envoi de la commande",
|
||||
"prompt.toast.promptSendFailed.title": "Échec de l'envoi du message",
|
||||
|
||||
"dialog.mcp.title": "MCPs",
|
||||
"dialog.mcp.description": "{{enabled}} sur {{total}} activés",
|
||||
"dialog.mcp.empty": "Aucun MCP configuré",
|
||||
|
||||
"mcp.status.connected": "connecté",
|
||||
"mcp.status.failed": "échoué",
|
||||
"mcp.status.needs_auth": "nécessite auth",
|
||||
"mcp.status.disabled": "désactivé",
|
||||
|
||||
"dialog.fork.empty": "Aucun message à partir duquel bifurquer",
|
||||
|
||||
"dialog.directory.search.placeholder": "Rechercher des dossiers",
|
||||
"dialog.directory.empty": "Aucun dossier trouvé",
|
||||
|
||||
"dialog.server.title": "Serveurs",
|
||||
"dialog.server.description": "Changez le serveur OpenCode auquel cette application se connecte.",
|
||||
"dialog.server.search.placeholder": "Rechercher des serveurs",
|
||||
"dialog.server.empty": "Aucun serveur pour l'instant",
|
||||
"dialog.server.add.title": "Ajouter un serveur",
|
||||
"dialog.server.add.url": "URL du serveur",
|
||||
"dialog.server.add.placeholder": "http://localhost:4096",
|
||||
"dialog.server.add.error": "Impossible de se connecter au serveur",
|
||||
"dialog.server.add.checking": "Vérification...",
|
||||
"dialog.server.add.button": "Ajouter",
|
||||
"dialog.server.default.title": "Serveur par défaut",
|
||||
"dialog.server.default.description":
|
||||
"Se connecter à ce serveur au lancement de l'application au lieu de démarrer un serveur local. Nécessite un redémarrage.",
|
||||
"dialog.server.default.none": "Aucun serveur sélectionné",
|
||||
"dialog.server.default.set": "Définir le serveur actuel comme défaut",
|
||||
"dialog.server.default.clear": "Effacer",
|
||||
|
||||
"dialog.project.edit.title": "Modifier le projet",
|
||||
"dialog.project.edit.name": "Nom",
|
||||
"dialog.project.edit.icon": "Icône",
|
||||
"dialog.project.edit.icon.alt": "Icône du projet",
|
||||
"dialog.project.edit.icon.hint": "Cliquez ou faites glisser une image",
|
||||
"dialog.project.edit.icon.recommended": "Recommandé : 128x128px",
|
||||
"dialog.project.edit.color": "Couleur",
|
||||
|
||||
"context.breakdown.title": "Répartition du contexte",
|
||||
"context.breakdown.note":
|
||||
"Répartition approximative des jetons d'entrée. \"Autre\" inclut les définitions d'outils et les frais généraux.",
|
||||
"context.breakdown.system": "Système",
|
||||
"context.breakdown.user": "Utilisateur",
|
||||
"context.breakdown.assistant": "Assistant",
|
||||
"context.breakdown.tool": "Appels d'outils",
|
||||
"context.breakdown.other": "Autre",
|
||||
|
||||
"context.systemPrompt.title": "Prompt système",
|
||||
"context.rawMessages.title": "Messages bruts",
|
||||
|
||||
"context.stats.session": "Session",
|
||||
"context.stats.messages": "Messages",
|
||||
"context.stats.provider": "Fournisseur",
|
||||
"context.stats.model": "Modèle",
|
||||
"context.stats.limit": "Limite de contexte",
|
||||
"context.stats.totalTokens": "Total des jetons",
|
||||
"context.stats.usage": "Utilisation",
|
||||
"context.stats.inputTokens": "Jetons d'entrée",
|
||||
"context.stats.outputTokens": "Jetons de sortie",
|
||||
"context.stats.reasoningTokens": "Jetons de raisonnement",
|
||||
"context.stats.cacheTokens": "Jetons de cache (lecture/écriture)",
|
||||
"context.stats.userMessages": "Messages utilisateur",
|
||||
"context.stats.assistantMessages": "Messages assistant",
|
||||
"context.stats.totalCost": "Coût total",
|
||||
"context.stats.sessionCreated": "Session créée",
|
||||
"context.stats.lastActivity": "Dernière activité",
|
||||
|
||||
"context.usage.tokens": "Jetons",
|
||||
"context.usage.usage": "Utilisation",
|
||||
"context.usage.cost": "Coût",
|
||||
"context.usage.clickToView": "Cliquez pour voir le contexte",
|
||||
|
||||
"language.en": "Anglais",
|
||||
"language.zh": "Chinois (simplifié)",
|
||||
"language.zht": "Chinois (traditionnel)",
|
||||
"language.ko": "Coréen",
|
||||
"language.de": "Allemand",
|
||||
"language.es": "Espagnol",
|
||||
"language.fr": "Français",
|
||||
"language.ja": "Japonais",
|
||||
"language.da": "Danois",
|
||||
"language.ru": "Russe",
|
||||
"language.pl": "Polonais",
|
||||
|
||||
"toast.language.title": "Langue",
|
||||
"toast.language.description": "Passé à {{language}}",
|
||||
|
||||
"toast.theme.title": "Thème changé",
|
||||
"toast.scheme.title": "Schéma de couleurs",
|
||||
|
||||
"toast.permissions.autoaccept.on.title": "Acceptation auto des modifications",
|
||||
"toast.permissions.autoaccept.on.description":
|
||||
"Les permissions de modification et d'écriture seront automatiquement approuvées",
|
||||
"toast.permissions.autoaccept.off.title": "Arrêt acceptation auto des modifications",
|
||||
"toast.permissions.autoaccept.off.description":
|
||||
"Les permissions de modification et d'écriture nécessiteront une approbation",
|
||||
|
||||
"toast.model.none.title": "Aucun modèle sélectionné",
|
||||
"toast.model.none.description": "Connectez un fournisseur pour résumer cette session",
|
||||
|
||||
"toast.file.loadFailed.title": "Échec du chargement du fichier",
|
||||
|
||||
"toast.session.share.copyFailed.title": "Échec de la copie de l'URL dans le presse-papiers",
|
||||
"toast.session.share.success.title": "Session partagée",
|
||||
"toast.session.share.success.description": "URL de partage copiée dans le presse-papiers !",
|
||||
"toast.session.share.failed.title": "Échec du partage de la session",
|
||||
"toast.session.share.failed.description": "Une erreur s'est produite lors du partage de la session",
|
||||
|
||||
"toast.session.unshare.success.title": "Session non partagée",
|
||||
"toast.session.unshare.success.description": "Session non partagée avec succès !",
|
||||
"toast.session.unshare.failed.title": "Échec de l'annulation du partage",
|
||||
"toast.session.unshare.failed.description": "Une erreur s'est produite lors de l'annulation du partage de la session",
|
||||
|
||||
"toast.session.listFailed.title": "Échec du chargement des sessions pour {{project}}",
|
||||
|
||||
"toast.update.title": "Mise à jour disponible",
|
||||
"toast.update.description":
|
||||
"Une nouvelle version d'OpenCode ({{version}}) est maintenant disponible pour installation.",
|
||||
"toast.update.action.installRestart": "Installer et redémarrer",
|
||||
"toast.update.action.notYet": "Pas encore",
|
||||
|
||||
"error.page.title": "Quelque chose s'est mal passé",
|
||||
"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.action.restart": "Redémarrer",
|
||||
"error.page.action.checking": "Vérification...",
|
||||
"error.page.action.checkUpdates": "Vérifier les mises à jour",
|
||||
"error.page.action.updateTo": "Mettre à jour vers {{version}}",
|
||||
"error.page.report.prefix": "Veuillez signaler cette erreur à l'équipe OpenCode",
|
||||
"error.page.report.discord": "sur Discord",
|
||||
"error.page.version": "Version : {{version}}",
|
||||
|
||||
"error.dev.rootNotFound":
|
||||
"Élément racine introuvable. Avez-vous oublié de l'ajouter à votre index.html ? Ou peut-être que l'attribut id est mal orthographié ?",
|
||||
|
||||
"error.globalSync.connectFailed":
|
||||
"Impossible de se connecter au serveur. Y a-t-il un serveur en cours d'exécution à `{{url}}` ?",
|
||||
|
||||
"error.chain.unknown": "Erreur inconnue",
|
||||
"error.chain.causedBy": "Causé par :",
|
||||
"error.chain.apiError": "Erreur API",
|
||||
"error.chain.status": "Statut : {{status}}",
|
||||
"error.chain.retryable": "Réessayable : {{retryable}}",
|
||||
"error.chain.responseBody": "Corps de la réponse :\n{{body}}",
|
||||
"error.chain.didYouMean": "Vouliez-vous dire : {{suggestions}}",
|
||||
"error.chain.modelNotFound": "Modèle introuvable : {{provider}}/{{model}}",
|
||||
"error.chain.checkConfig": "Vérifiez votre configuration (opencode.json) pour les noms de fournisseur/modèle",
|
||||
"error.chain.mcpFailed":
|
||||
"Le serveur MCP \"{{name}}\" a échoué. Notez qu'OpenCode ne supporte pas encore l'authentification MCP.",
|
||||
"error.chain.providerAuthFailed": "Échec de l'authentification du fournisseur ({{provider}}) : {{message}}",
|
||||
"error.chain.providerInitFailed":
|
||||
'Échec de l\'initialisation du fournisseur "{{provider}}". Vérifiez les identifiants et la configuration.',
|
||||
"error.chain.configJsonInvalid": "Le fichier de configuration à {{path}} n'est pas un JSON(C) valide",
|
||||
"error.chain.configJsonInvalidWithMessage":
|
||||
"Le fichier de configuration à {{path}} n'est pas un JSON(C) valide : {{message}}",
|
||||
"error.chain.configDirectoryTypo":
|
||||
'Le répertoire "{{dir}}" dans {{path}} n\'est pas valide. Renommez le répertoire en "{{suggestion}}" ou supprimez-le. C\'est une faute de frappe courante.',
|
||||
"error.chain.configFrontmatterError": "Échec de l'analyse du frontmatter dans {{path}} :\n{{message}}",
|
||||
"error.chain.configInvalid": "Le fichier de configuration à {{path}} est invalide",
|
||||
"error.chain.configInvalidWithMessage": "Le fichier de configuration à {{path}} est invalide : {{message}}",
|
||||
|
||||
"notification.permission.title": "Permission requise",
|
||||
"notification.permission.description": "{{sessionTitle}} dans {{projectName}} a besoin d'une permission",
|
||||
"notification.question.title": "Question",
|
||||
"notification.question.description": "{{sessionTitle}} dans {{projectName}} a une question",
|
||||
"notification.action.goToSession": "Aller à la session",
|
||||
|
||||
"notification.session.responseReady.title": "Réponse prête",
|
||||
"notification.session.error.title": "Erreur de session",
|
||||
"notification.session.error.fallbackDescription": "Une erreur s'est produite",
|
||||
|
||||
"home.recentProjects": "Projets récents",
|
||||
"home.empty.title": "Aucun projet récent",
|
||||
"home.empty.description": "Commencez par ouvrir un projet local",
|
||||
|
||||
"session.tab.session": "Session",
|
||||
"session.tab.review": "Revue",
|
||||
"session.tab.context": "Contexte",
|
||||
"session.review.filesChanged": "{{count}} fichiers modifiés",
|
||||
"session.review.loadingChanges": "Chargement des modifications...",
|
||||
"session.review.empty": "Aucune modification dans cette session pour l'instant",
|
||||
"session.messages.renderEarlier": "Afficher les messages précédents",
|
||||
"session.messages.loadingEarlier": "Chargement des messages précédents...",
|
||||
"session.messages.loadEarlier": "Charger les messages précédents",
|
||||
"session.messages.loading": "Chargement des messages...",
|
||||
|
||||
"session.context.addToContext": "Ajouter {{selection}} au contexte",
|
||||
|
||||
"session.new.worktree.main": "Branche principale",
|
||||
"session.new.worktree.mainWithBranch": "Branche principale ({{branch}})",
|
||||
"session.new.worktree.create": "Créer un nouvel arbre de travail",
|
||||
"session.new.lastModified": "Dernière modification",
|
||||
|
||||
"session.header.search.placeholder": "Rechercher {{project}}",
|
||||
|
||||
"session.share.popover.title": "Publier sur le web",
|
||||
"session.share.popover.description.shared":
|
||||
"Cette session est publique sur le web. Elle est accessible à toute personne disposant du lien.",
|
||||
"session.share.popover.description.unshared":
|
||||
"Partager la session publiquement sur le web. Elle sera accessible à toute personne disposant du lien.",
|
||||
"session.share.action.share": "Partager",
|
||||
"session.share.action.publish": "Publier",
|
||||
"session.share.action.publishing": "Publication...",
|
||||
"session.share.action.unpublish": "Dépublier",
|
||||
"session.share.action.unpublishing": "Dépublication...",
|
||||
"session.share.action.view": "Voir",
|
||||
"session.share.copy.copied": "Copié",
|
||||
"session.share.copy.copyLink": "Copier le lien",
|
||||
|
||||
"lsp.tooltip.none": "Aucun serveur LSP",
|
||||
"lsp.label.connected": "{{count}} LSP",
|
||||
|
||||
"prompt.loading": "Chargement du prompt...",
|
||||
"terminal.loading": "Chargement du terminal...",
|
||||
"terminal.title": "Terminal",
|
||||
"terminal.title.numbered": "Terminal {{number}}",
|
||||
|
||||
"common.closeTab": "Fermer l'onglet",
|
||||
"common.dismiss": "Ignorer",
|
||||
"common.requestFailed": "La demande a échoué",
|
||||
"common.moreOptions": "Plus d'options",
|
||||
"common.learnMore": "En savoir plus",
|
||||
"common.rename": "Renommer",
|
||||
"common.reset": "Réinitialiser",
|
||||
"common.delete": "Supprimer",
|
||||
"common.close": "Fermer",
|
||||
"common.edit": "Modifier",
|
||||
"common.loadMore": "Charger plus",
|
||||
|
||||
"sidebar.settings": "Paramètres",
|
||||
"sidebar.help": "Aide",
|
||||
"sidebar.workspaces.enable": "Activer les espaces de travail",
|
||||
"sidebar.workspaces.disable": "Désactiver les espaces de travail",
|
||||
"sidebar.gettingStarted.title": "Commencer",
|
||||
"sidebar.gettingStarted.line1":
|
||||
"OpenCode inclut des modèles gratuits pour que vous puissiez commencer immédiatement.",
|
||||
"sidebar.gettingStarted.line2":
|
||||
"Connectez n'importe quel fournisseur pour utiliser des modèles, y compris Claude, GPT, Gemini etc.",
|
||||
"sidebar.project.recentSessions": "Sessions récentes",
|
||||
"sidebar.project.viewAllSessions": "Voir toutes les sessions",
|
||||
|
||||
"settings.section.desktop": "Bureau",
|
||||
"settings.tab.general": "Général",
|
||||
"settings.tab.shortcuts": "Raccourcis",
|
||||
|
||||
"settings.general.section.appearance": "Apparence",
|
||||
"settings.general.section.notifications": "Notifications système",
|
||||
"settings.general.section.sounds": "Effets sonores",
|
||||
|
||||
"settings.general.row.language.title": "Langue",
|
||||
"settings.general.row.language.description": "Changer la langue d'affichage pour OpenCode",
|
||||
"settings.general.row.appearance.title": "Apparence",
|
||||
"settings.general.row.appearance.description": "Personnaliser l'apparence d'OpenCode sur votre appareil",
|
||||
"settings.general.row.theme.title": "Thème",
|
||||
"settings.general.row.theme.description": "Personnaliser le thème d'OpenCode.",
|
||||
"settings.general.row.font.title": "Police",
|
||||
"settings.general.row.font.description": "Personnaliser la police mono utilisée dans les blocs de code",
|
||||
|
||||
"settings.general.notifications.agent.title": "Agent",
|
||||
"settings.general.notifications.agent.description":
|
||||
"Afficher une notification système lorsque l'agent a terminé ou nécessite une attention",
|
||||
"settings.general.notifications.permissions.title": "Permissions",
|
||||
"settings.general.notifications.permissions.description":
|
||||
"Afficher une notification système lorsqu'une permission est requise",
|
||||
"settings.general.notifications.errors.title": "Erreurs",
|
||||
"settings.general.notifications.errors.description": "Afficher une notification système lorsqu'une erreur se produit",
|
||||
|
||||
"settings.general.sounds.agent.title": "Agent",
|
||||
"settings.general.sounds.agent.description": "Jouer un son lorsque l'agent a terminé ou nécessite une attention",
|
||||
"settings.general.sounds.permissions.title": "Permissions",
|
||||
"settings.general.sounds.permissions.description": "Jouer un son lorsqu'une permission est requise",
|
||||
"settings.general.sounds.errors.title": "Erreurs",
|
||||
"settings.general.sounds.errors.description": "Jouer un son lorsqu'une erreur se produit",
|
||||
|
||||
"settings.shortcuts.title": "Raccourcis clavier",
|
||||
"settings.shortcuts.reset.button": "Rétablir les défauts",
|
||||
"settings.shortcuts.reset.toast.title": "Raccourcis réinitialisés",
|
||||
"settings.shortcuts.reset.toast.description": "Les raccourcis clavier ont été réinitialisés aux valeurs par défaut.",
|
||||
"settings.shortcuts.conflict.title": "Raccourci déjà utilisé",
|
||||
"settings.shortcuts.conflict.description": "{{keybind}} est déjà assigné à {{titles}}.",
|
||||
"settings.shortcuts.unassigned": "Non assigné",
|
||||
"settings.shortcuts.pressKeys": "Appuyez sur les touches",
|
||||
"settings.shortcuts.search.placeholder": "Rechercher des raccourcis",
|
||||
"settings.shortcuts.search.empty": "Aucun raccourci trouvé",
|
||||
|
||||
"settings.shortcuts.group.general": "Général",
|
||||
"settings.shortcuts.group.session": "Session",
|
||||
"settings.shortcuts.group.navigation": "Navigation",
|
||||
"settings.shortcuts.group.modelAndAgent": "Modèle et agent",
|
||||
"settings.shortcuts.group.terminal": "Terminal",
|
||||
"settings.shortcuts.group.prompt": "Prompt",
|
||||
|
||||
"settings.providers.title": "Fournisseurs",
|
||||
"settings.providers.description": "Les paramètres des fournisseurs seront configurables ici.",
|
||||
"settings.models.title": "Modèles",
|
||||
"settings.models.description": "Les paramètres des modèles seront configurables ici.",
|
||||
"settings.agents.title": "Agents",
|
||||
"settings.agents.description": "Les paramètres des agents seront configurables ici.",
|
||||
"settings.commands.title": "Commandes",
|
||||
"settings.commands.description": "Les paramètres des commandes seront configurables ici.",
|
||||
"settings.mcp.title": "MCP",
|
||||
"settings.mcp.description": "Les paramètres MCP seront configurables ici.",
|
||||
|
||||
"settings.permissions.title": "Permissions",
|
||||
"settings.permissions.description": "Contrôlez les outils que le serveur peut utiliser par défaut.",
|
||||
"settings.permissions.section.tools": "Outils",
|
||||
"settings.permissions.toast.updateFailed.title": "Échec de la mise à jour des permissions",
|
||||
|
||||
"settings.permissions.action.allow": "Autoriser",
|
||||
"settings.permissions.action.ask": "Demander",
|
||||
"settings.permissions.action.deny": "Refuser",
|
||||
|
||||
"settings.permissions.tool.read.title": "Lire",
|
||||
"settings.permissions.tool.read.description": "Lecture d'un fichier (correspond au chemin du fichier)",
|
||||
"settings.permissions.tool.edit.title": "Modifier",
|
||||
"settings.permissions.tool.edit.description":
|
||||
"Modifier des fichiers, y compris les modifications, écritures, patchs et multi-modifications",
|
||||
"settings.permissions.tool.glob.title": "Glob",
|
||||
"settings.permissions.tool.glob.description": "Correspondre aux fichiers utilisant des modèles glob",
|
||||
"settings.permissions.tool.grep.title": "Grep",
|
||||
"settings.permissions.tool.grep.description":
|
||||
"Rechercher dans le contenu des fichiers à l'aide d'expressions régulières",
|
||||
"settings.permissions.tool.list.title": "Lister",
|
||||
"settings.permissions.tool.list.description": "Lister les fichiers dans un répertoire",
|
||||
"settings.permissions.tool.bash.title": "Bash",
|
||||
"settings.permissions.tool.bash.description": "Exécuter des commandes shell",
|
||||
"settings.permissions.tool.task.title": "Tâche",
|
||||
"settings.permissions.tool.task.description": "Lancer des sous-agents",
|
||||
"settings.permissions.tool.skill.title": "Compétence",
|
||||
"settings.permissions.tool.skill.description": "Charger une compétence par son nom",
|
||||
"settings.permissions.tool.lsp.title": "LSP",
|
||||
"settings.permissions.tool.lsp.description": "Exécuter des requêtes de serveur de langage",
|
||||
"settings.permissions.tool.todoread.title": "Lire Todo",
|
||||
"settings.permissions.tool.todoread.description": "Lire la liste de tâches",
|
||||
"settings.permissions.tool.todowrite.title": "Écrire Todo",
|
||||
"settings.permissions.tool.todowrite.description": "Mettre à jour la liste de tâches",
|
||||
"settings.permissions.tool.webfetch.title": "Récupération Web",
|
||||
"settings.permissions.tool.webfetch.description": "Récupérer le contenu d'une URL",
|
||||
"settings.permissions.tool.websearch.title": "Recherche 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.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.description": "Détecter les appels d'outils répétés avec une entrée identique",
|
||||
|
||||
"workspace.new": "Nouvel espace de travail",
|
||||
"workspace.type.local": "local",
|
||||
"workspace.type.sandbox": "bac à sable",
|
||||
"workspace.create.failed.title": "Échec de la création de l'espace de travail",
|
||||
"workspace.delete.failed.title": "Échec de la suppression de l'espace de travail",
|
||||
"workspace.resetting.title": "Réinitialisation de l'espace de travail",
|
||||
"workspace.resetting.description": "Cela peut prendre une minute.",
|
||||
"workspace.reset.failed.title": "Échec de la réinitialisation de l'espace de travail",
|
||||
"workspace.reset.success.title": "Espace de travail réinitialisé",
|
||||
"workspace.reset.success.description": "L'espace de travail correspond maintenant à la branche par défaut.",
|
||||
"workspace.status.checking": "Vérification des modifications non fusionnées...",
|
||||
"workspace.status.error": "Impossible de vérifier le statut git.",
|
||||
"workspace.status.clean": "Aucune modification non fusionnée détectée.",
|
||||
"workspace.status.dirty": "Modifications non fusionnées détectées dans cet espace de travail.",
|
||||
"workspace.delete.title": "Supprimer l'espace de travail",
|
||||
"workspace.delete.confirm": 'Supprimer l\'espace de travail "{{name}}" ?',
|
||||
"workspace.delete.button": "Supprimer l'espace de travail",
|
||||
"workspace.reset.title": "Réinitialiser l'espace de travail",
|
||||
"workspace.reset.confirm": 'Réinitialiser l\'espace de travail "{{name}}" ?',
|
||||
"workspace.reset.button": "Réinitialiser l'espace de travail",
|
||||
"workspace.reset.archived.none": "Aucune session active ne sera archivée.",
|
||||
"workspace.reset.archived.one": "1 session sera archivée.",
|
||||
"workspace.reset.archived.many": "{{count}} sessions seront archivées.",
|
||||
"workspace.reset.note": "Cela réinitialisera l'espace de travail pour correspondre à la branche par défaut.",
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user