Compare commits
441
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8d2385ad49 | ||
|
|
7f6a5bb2c8 | ||
|
|
537cc32bf0 | ||
|
|
82da702f64 | ||
|
|
90469bbb7e | ||
|
|
4ff0fbc043 | ||
|
|
e24369eaf1 | ||
|
|
825f51c39f | ||
|
|
191a747405 | ||
|
|
cc412f3014 | ||
|
|
bb039496d5 | ||
|
|
f2fa1a681d | ||
|
|
6bd340492c | ||
|
|
21ec3207e7 | ||
|
|
123123b6c3 | ||
|
|
6ea467b0ac | ||
|
|
459fbc99a8 | ||
|
|
d6d4446f46 | ||
|
|
26cc924ea2 | ||
|
|
4dd866d5c4 | ||
|
|
beab4cc2c2 | ||
|
|
567a91191a | ||
|
|
434d82bbe2 | ||
|
|
2929774acb | ||
|
|
6e61a46a84 | ||
|
|
2daf4b805a | ||
|
|
7342e650c0 | ||
|
|
8c2e2ecc95 | ||
|
|
25a2b739e6 | ||
|
|
85c16926c4 | ||
|
|
2e78fdec43 | ||
|
|
1fcb920eb4 | ||
|
|
b1e89c344b | ||
|
|
befbedacdc | ||
|
|
2cc738fb17 | ||
|
|
71b20698bb | ||
|
|
3df18dcde1 | ||
|
|
a898c2ea3a | ||
|
|
bf777298c8 | ||
|
|
93fad99f7f | ||
|
|
057848deb8 | ||
|
|
1de06452d3 | ||
|
|
58f60629a1 | ||
|
|
39a47c9b8c | ||
|
|
ea88044f2e | ||
|
|
e6f6f7aff1 | ||
|
|
48e97b47af | ||
|
|
fe120e3cbf | ||
|
|
f2dd774660 | ||
|
|
e7ff0f17c8 | ||
|
|
2ed756c72c | ||
|
|
054f4be185 | ||
|
|
e3e1e9af50 | ||
|
|
c8389cf96d | ||
|
|
c5442d418d | ||
|
|
fa95a61c4e | ||
|
|
9f3c2bd861 | ||
|
|
c2f78224ae | ||
|
|
14f9e21d5c | ||
|
|
8e4bab5181 | ||
|
|
3c32013eb1 | ||
|
|
47d2ab120a | ||
|
|
186af2723d | ||
|
|
6926fe1c74 | ||
|
|
ee018d5c82 | ||
|
|
0465579d6b | ||
|
|
196a03caff | ||
|
|
b234370080 | ||
|
|
5d2dc8888c | ||
|
|
0b1018f6dd | ||
|
|
afb6abff73 | ||
|
|
e7f94f9b9a | ||
|
|
72c77d0e7b | ||
|
|
5c15755a10 | ||
|
|
3a4bfeb5b5 | ||
|
|
1037c72d99 | ||
|
|
ba00e9a993 | ||
|
|
963dad75ef | ||
|
|
7e9b721e97 | ||
|
|
a5b1dc081d | ||
|
|
0bc2f99f2d | ||
|
|
55895d0663 | ||
|
|
72cb9dfa31 | ||
|
|
f0a9075fdf | ||
|
|
fee1e25ab4 | ||
|
|
a94ac5aa2c | ||
|
|
62ac45a9c9 | ||
|
|
f7c2ef876f | ||
|
|
6639f92739 | ||
|
|
36aeb32159 | ||
|
|
ff37d7c2df | ||
|
|
4f96eb239f | ||
|
|
38af99dcb4 | ||
|
|
772059acb5 | ||
|
|
1f290fc1ba | ||
|
|
77d4f99497 | ||
|
|
aa2d753e7e | ||
|
|
860531c275 | ||
|
|
2b86b36c8c | ||
|
|
8ac2fbbd12 | ||
|
|
26382c6216 | ||
|
|
0981b8eb71 | ||
|
|
aa9ed001d3 | ||
|
|
6086072567 | ||
|
|
6c14ea1d22 | ||
|
|
c3a9ec4a99 | ||
|
|
41b0d03f6a | ||
|
|
81eb6e670b | ||
|
|
8446719b13 | ||
|
|
15a8c22a26 | ||
|
|
48326e8d9c | ||
|
|
43bc5551e8 | ||
|
|
f736116967 | ||
|
|
82fc493520 | ||
|
|
2145d97f18 | ||
|
|
f3997d8082 | ||
|
|
02b19bc3d7 | ||
|
|
5cd54ec345 | ||
|
|
c8909908f5 | ||
|
|
4b9660b211 | ||
|
|
e5f0e813b6 | ||
|
|
c33d9996f0 | ||
|
|
7a7643c86a | ||
|
|
6f5b70e681 | ||
|
|
ff13524a53 | ||
|
|
e973bbf54a | ||
|
|
d36b38e4a6 | ||
|
|
bdd7829c68 | ||
|
|
a93374c48f | ||
|
|
af2ccc94eb | ||
|
|
a76be695c7 | ||
|
|
e528ed5d86 | ||
|
|
bb8d2cdd10 | ||
|
|
decb5e68ee | ||
|
|
21023337fa | ||
|
|
6274b0677c | ||
|
|
d8ad8338f5 | ||
|
|
7b44918149 | ||
|
|
d2bfa92e74 | ||
|
|
3fb60d05e5 | ||
|
|
d341499684 | ||
|
|
771525270a | ||
|
|
e96eead32e | ||
|
|
b242a8d8e4 | ||
|
|
9c6f1edfd7 | ||
|
|
ef7d1f7efa | ||
|
|
b7a06e1939 | ||
|
|
311ba4179a | ||
|
|
ad3b350672 | ||
|
|
590523dcd1 | ||
|
|
b8fb75a94a | ||
|
|
98a31e30cc | ||
|
|
c333e914ee | ||
|
|
c7760b433b | ||
|
|
2e6ac8ff49 | ||
|
|
1ebc92fd36 | ||
|
|
9f94bdb496 | ||
|
|
28f5176ffd | ||
|
|
38450443b1 | ||
|
|
da1d37274f | ||
|
|
17e8f577d6 | ||
|
|
c7d23098d1 | ||
|
|
bcf18edde4 | ||
|
|
9a2482ac09 | ||
|
|
54443bfb7e | ||
|
|
ec20efc11a | ||
|
|
83ed1c4414 | ||
|
|
1d363fa19f | ||
|
|
1b028d0632 | ||
|
|
d500a8432a | ||
|
|
2d502d6ffe | ||
|
|
2ad190e482 | ||
|
|
16742af7f3 | ||
|
|
652313e036 | ||
|
|
1a4a6eabe2 | ||
|
|
ba244a6e62 | ||
|
|
7cb690d7e5 | ||
|
|
31ad6e85ba | ||
|
|
ea04b23745 | ||
|
|
05c3cfb2aa | ||
|
|
f54e4b60cc | ||
|
|
97c15a087d | ||
|
|
b90de755f9 | ||
|
|
8864fdce2f | ||
|
|
5179b87aef | ||
|
|
66a56551be | ||
|
|
7123aad5a8 | ||
|
|
d6fc5f414b | ||
|
|
77fc88c8ad | ||
|
|
cafc2b204b | ||
|
|
36709aae5f | ||
|
|
fac0dd8862 | ||
|
|
73e107250d | ||
|
|
b746aec493 | ||
|
|
ad40b65b0b | ||
|
|
971383661a | ||
|
|
b0017bf1b9 | ||
|
|
0c0c6f3bdb | ||
|
|
b480a38d31 | ||
|
|
4167e25c7e | ||
|
|
1041ae91d1 | ||
|
|
898456a25c | ||
|
|
53d0b58ebf | ||
|
|
2b0baf97bd | ||
|
|
0dbfefa080 | ||
|
|
d1c49ba210 | ||
|
|
3ea72aec21 | ||
|
|
9717383823 | ||
|
|
5d9e780029 | ||
|
|
aa11fa865d | ||
|
|
9a64bdb539 | ||
|
|
71693cc24b | ||
|
|
700f57112a | ||
|
|
0a80ef4278 | ||
|
|
4f9667c4bb | ||
|
|
be142b00bd | ||
|
|
45c2573979 | ||
|
|
79e9d19019 | ||
|
|
958a80cc05 | ||
|
|
4647aa80ac | ||
|
|
a379eb3867 | ||
|
|
cbe1337f24 | ||
|
|
50f6aa3763 | ||
|
|
0dcdf5f529 | ||
|
|
4586b41ffd | ||
|
|
35884defd8 | ||
|
|
15dc33d1a3 | ||
|
|
1398674e53 | ||
|
|
afc4c831eb | ||
|
|
ec64ceabec | ||
|
|
56644be95a | ||
|
|
00d3b831fc | ||
|
|
b848b7ebae | ||
|
|
e837dcc1c5 | ||
|
|
024979f3fd | ||
|
|
bc608fb081 | ||
|
|
9838f56a6f | ||
|
|
98b3340cee | ||
|
|
5e684c6e80 | ||
|
|
2c1d8a90d5 | ||
|
|
8994cbfc0f | ||
|
|
42a773481e | ||
|
|
539b01f20f | ||
|
|
814a515a8a | ||
|
|
235a82aea9 | ||
|
|
9330bc5339 | ||
|
|
1238d1f61a | ||
|
|
1d3232b388 | ||
|
|
5c1bb5de86 | ||
|
|
7c5ed771c3 | ||
|
|
31c4a4fb47 | ||
|
|
037077285a | ||
|
|
41c77ccb33 | ||
|
|
546748a461 | ||
|
|
c9c93eac00 | ||
|
|
3f1a4abe6d | ||
|
|
431e0586ad | ||
|
|
fde201c286 | ||
|
|
d3debc191f | ||
|
|
34f43fff89 | ||
|
|
49623aa519 | ||
|
|
f1340472ec | ||
|
|
a8b28826a0 | ||
|
|
a03a2b6eab | ||
|
|
ad78b79b8a | ||
|
|
9a006d8700 | ||
|
|
3a0bf2f39f | ||
|
|
b556979634 | ||
|
|
691644eeeb | ||
|
|
4aebaaf067 | ||
|
|
77b3b46788 | ||
|
|
36dfe1646b | ||
|
|
6926dc26d1 | ||
|
|
eb74e4a6d2 | ||
|
|
85d8e143bf | ||
|
|
8e1b53b32c | ||
|
|
0a7dfc03ee | ||
|
|
4c27e7fc64 | ||
|
|
0f5626d2e4 | ||
|
|
5ea95451dd | ||
|
|
9239d877b9 | ||
|
|
fc68c24433 | ||
|
|
db9619dad6 | ||
|
|
84d9b38873 | ||
|
|
8035c3435b | ||
|
|
71e7603d71 | ||
|
|
40e49c5b49 | ||
|
|
afe9b97274 | ||
|
|
3b3549902d | ||
|
|
e9a9c75c1f | ||
|
|
2b171828b0 | ||
|
|
8dd817023a | ||
|
|
0d6c601365 | ||
|
|
5460bf9989 | ||
|
|
eb3bfffad4 | ||
|
|
e2d03ce38c | ||
|
|
32f9dc6383 | ||
|
|
c529529f84 | ||
|
|
13bac9c91a | ||
|
|
fe53af4819 | ||
|
|
e82c5a9a28 | ||
|
|
3236f228fb | ||
|
|
0e0e7a4a4b | ||
|
|
10a3d6c54e | ||
|
|
832b8e252e | ||
|
|
040f551c57 | ||
|
|
cc818f8032 | ||
|
|
d5337b41f4 | ||
|
|
9f7a76d6c0 | ||
|
|
6a16db4b92 | ||
|
|
9ad6588f3e | ||
|
|
fb6bf0b35e | ||
|
|
f80343b875 | ||
|
|
9b805e1cc4 | ||
|
|
2e0d5d2308 | ||
|
|
38e0dc9ccd | ||
|
|
40aeaa120d | ||
|
|
6a64177589 | ||
|
|
5dc47905a9 | ||
|
|
dc0044882c | ||
|
|
45ae7dc653 | ||
|
|
129fe1e350 | ||
|
|
214a6c6cf1 | ||
|
|
3f249aba6d | ||
|
|
5c6ec1caac | ||
|
|
24f9df5463 | ||
|
|
12b8e1c2be | ||
|
|
d70099b059 | ||
|
|
ce845a0b1b | ||
|
|
05d3e65f76 | ||
|
|
51618e9cef | ||
|
|
e78944e9a4 | ||
|
|
bfdc38e421 | ||
|
|
83023e4f0f | ||
|
|
d0a57305ef | ||
|
|
27a70ad70f | ||
|
|
0bbf26a1ce | ||
|
|
83cdb4de64 | ||
|
|
4989632245 | ||
|
|
d460614cd7 | ||
|
|
7866dbcfcc | ||
|
|
e71a21e0a8 | ||
|
|
1071aca91f | ||
|
|
b3d0446d13 | ||
|
|
949191ab74 | ||
|
|
92cd908fb5 | ||
|
|
6fcc970def | ||
|
|
52a7a04ad8 | ||
|
|
37b8662a9d | ||
|
|
ddcb32ae0b | ||
|
|
2c056c90da | ||
|
|
812d1bb32a | ||
|
|
9a58c43ef4 | ||
|
|
63585db6a7 | ||
|
|
bd44489ada | ||
|
|
a6ef9e9206 | ||
|
|
6e09a1d904 | ||
|
|
4f21757e0d | ||
|
|
2dbcd79fd2 | ||
|
|
48a7f0fd93 | ||
|
|
d69962b0f7 | ||
|
|
a6f23cb08e | ||
|
|
0540751897 | ||
|
|
baa204193c | ||
|
|
aeece6166b | ||
|
|
0d7e62a532 | ||
|
|
41aa254db4 | ||
|
|
d178d8249f | ||
|
|
e6f5214779 | ||
|
|
84f60d97a0 | ||
|
|
cbf4b68fee | ||
|
|
bd4527b4f2 | ||
|
|
f4a9fe29a3 | ||
|
|
5a0bfa7061 | ||
|
|
1ac1a0287c | ||
|
|
8e09e8c612 | ||
|
|
84e62fc662 | ||
|
|
a7ea93528b | ||
|
|
d90e3a2833 | ||
|
|
1c74c2741a | ||
|
|
5d2f8d77f9 | ||
|
|
81be544981 | ||
|
|
773c1192dc | ||
|
|
5ddfe4ada5 | ||
|
|
a93d98bd94 | ||
|
|
54ed87d53c | ||
|
|
8ee939c741 | ||
|
|
1b0096bf61 | ||
|
|
8006c29db3 | ||
|
|
3f1c96a0bb | ||
|
|
3558deba4a | ||
|
|
c3ddc85cca | ||
|
|
a800583aea | ||
|
|
171e69c2fc | ||
|
|
822bb7b336 | ||
|
|
47cf267c23 | ||
|
|
976aae7e42 | ||
|
|
0ca51eebcf | ||
|
|
3256886e25 | ||
|
|
d2194f6dde | ||
|
|
bfd4787fcd | ||
|
|
58dce0148a | ||
|
|
79635b8b41 | ||
|
|
331dacf9db | ||
|
|
4ba7d3b406 | ||
|
|
a43783a6d4 | ||
|
|
37c5295111 | ||
|
|
56102ff642 | ||
|
|
1b86c27fb8 | ||
|
|
fe43bdb699 | ||
|
|
a849a17e93 | ||
|
|
0292f1b559 | ||
|
|
5dfe86dcb1 | ||
|
|
4b4dd2b882 | ||
|
|
bc949af623 | ||
|
|
9e7c136de7 | ||
|
|
fee3c196c5 | ||
|
|
6c047391bb | ||
|
|
350df0b261 | ||
|
|
fbabc97c4c | ||
|
|
7daea69e13 | ||
|
|
0772a95918 | ||
|
|
dadddc9c8c | ||
|
|
6708c3f6cf | ||
|
|
ba22976568 | ||
|
|
0afeaea21f | ||
|
|
b07b5a9b7f | ||
|
|
dbbe931a18 | ||
|
|
e14e874e51 | ||
|
|
544315dff7 | ||
|
|
f13da808ff | ||
|
|
e416e59ea6 | ||
|
|
cb69501098 | ||
|
|
a64f604d54 | ||
|
|
d7093abf61 | ||
|
|
60af447908 | ||
|
|
1cdc558ac0 | ||
|
|
3849822769 | ||
|
|
e9a17e4480 | ||
|
|
68809365df | ||
|
|
8da511dfa8 |
@@ -10,6 +10,9 @@
|
|||||||
adamdotdevin
|
adamdotdevin
|
||||||
-agusbasari29 AI PR slop
|
-agusbasari29 AI PR slop
|
||||||
ariane-emory
|
ariane-emory
|
||||||
|
-atharvau AI review spamming literally every PR
|
||||||
|
-danieljoshuanazareth
|
||||||
|
-danieljoshuanazareth
|
||||||
edemaine
|
edemaine
|
||||||
-florianleibert
|
-florianleibert
|
||||||
fwang
|
fwang
|
||||||
@@ -17,7 +20,10 @@ iamdavidhill
|
|||||||
jayair
|
jayair
|
||||||
kitlangton
|
kitlangton
|
||||||
kommander
|
kommander
|
||||||
|
-opencode2026
|
||||||
|
-opencodeengineer bot that spams issues
|
||||||
r44vc0rp
|
r44vc0rp
|
||||||
rekram1-node
|
rekram1-node
|
||||||
|
-robinmordasiewicz
|
||||||
-spider-yamet clawdbot/llm psychosis, spam pinging the team
|
-spider-yamet clawdbot/llm psychosis, spam pinging the team
|
||||||
thdxr
|
thdxr
|
||||||
|
|||||||
@@ -0,0 +1,24 @@
|
|||||||
|
name: close-issues
|
||||||
|
|
||||||
|
on:
|
||||||
|
schedule:
|
||||||
|
- cron: "0 2 * * *" # Daily at 2:00 AM
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
close:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
issues: write
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- uses: oven-sh/setup-bun@v2
|
||||||
|
with:
|
||||||
|
bun-version: latest
|
||||||
|
|
||||||
|
- name: Close stale issues
|
||||||
|
env:
|
||||||
|
GITHUB_TOKEN: ${{ github.token }}
|
||||||
|
run: bun script/github/close-issues.ts
|
||||||
@@ -11,7 +11,7 @@ concurrency: ${{ github.workflow }}-${{ github.ref }}
|
|||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
deploy:
|
deploy:
|
||||||
runs-on: blacksmith-4vcpu-ubuntu-2404
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v3
|
||||||
|
|
||||||
|
|||||||
@@ -9,7 +9,8 @@ on:
|
|||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
sync-locales:
|
sync-locales:
|
||||||
if: github.actor != 'opencode-agent[bot]'
|
if: false
|
||||||
|
#if: github.actor != 'opencode-agent[bot]'
|
||||||
runs-on: blacksmith-4vcpu-ubuntu-2404
|
runs-on: blacksmith-4vcpu-ubuntu-2404
|
||||||
permissions:
|
permissions:
|
||||||
contents: write
|
contents: write
|
||||||
@@ -34,7 +35,7 @@ jobs:
|
|||||||
- name: Compute changed English docs
|
- name: Compute changed English docs
|
||||||
id: changes
|
id: changes
|
||||||
run: |
|
run: |
|
||||||
FILES=$(git diff --name-only "${{ github.event.before }}" "${{ github.sha }}" -- 'packages/web/src/content/docs/*.mdx' || true)
|
FILES=$(git diff --name-only "${{ github.event.before }}" "${{ github.sha }}" -- ':(glob)packages/web/src/content/docs/*.mdx' || true)
|
||||||
if [ -z "$FILES" ]; then
|
if [ -z "$FILES" ]; then
|
||||||
echo "has_changes=false" >> "$GITHUB_OUTPUT"
|
echo "has_changes=false" >> "$GITHUB_OUTPUT"
|
||||||
echo "No English docs changed in push range"
|
echo "No English docs changed in push range"
|
||||||
|
|||||||
@@ -17,6 +17,10 @@ on:
|
|||||||
- "patches/**"
|
- "patches/**"
|
||||||
- ".github/workflows/nix-hashes.yml"
|
- ".github/workflows/nix-hashes.yml"
|
||||||
|
|
||||||
|
concurrency:
|
||||||
|
group: ${{ github.workflow }}-${{ github.ref }}
|
||||||
|
cancel-in-progress: true
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
# Native runners required: bun install cross-compilation flags (--os/--cpu)
|
# Native runners required: bun install cross-compilation flags (--os/--cpu)
|
||||||
# do not produce byte-identical node_modules as native installs.
|
# do not produce byte-identical node_modules as native installs.
|
||||||
@@ -56,7 +60,7 @@ jobs:
|
|||||||
nix build ".#packages.${SYSTEM}.node_modules_updater" --no-link 2>&1 | tee "$BUILD_LOG" || true
|
nix build ".#packages.${SYSTEM}.node_modules_updater" --no-link 2>&1 | tee "$BUILD_LOG" || true
|
||||||
|
|
||||||
# Extract hash from build log with portability
|
# Extract hash from build log with portability
|
||||||
HASH="$(grep -oE 'sha256-[A-Za-z0-9+/=]+' "$BUILD_LOG" | tail -n1 || true)"
|
HASH="$(nix run --inputs-from . nixpkgs#gnugrep -- -oP 'got:\s*\Ksha256-[A-Za-z0-9+/=]+' "$BUILD_LOG" | tail -n1 || true)"
|
||||||
|
|
||||||
if [ -z "$HASH" ]; then
|
if [ -z "$HASH" ]; then
|
||||||
echo "::error::Failed to compute hash for ${SYSTEM}"
|
echo "::error::Failed to compute hash for ${SYSTEM}"
|
||||||
|
|||||||
@@ -98,15 +98,129 @@ jobs:
|
|||||||
- uses: actions/upload-artifact@v4
|
- uses: actions/upload-artifact@v4
|
||||||
with:
|
with:
|
||||||
name: opencode-cli
|
name: opencode-cli
|
||||||
path: packages/opencode/dist
|
path: |
|
||||||
|
packages/opencode/dist/opencode-darwin*
|
||||||
|
packages/opencode/dist/opencode-linux*
|
||||||
|
|
||||||
|
- uses: actions/upload-artifact@v4
|
||||||
|
with:
|
||||||
|
name: opencode-cli-windows
|
||||||
|
path: packages/opencode/dist/opencode-windows*
|
||||||
outputs:
|
outputs:
|
||||||
version: ${{ needs.version.outputs.version }}
|
version: ${{ needs.version.outputs.version }}
|
||||||
|
|
||||||
|
sign-cli-windows:
|
||||||
|
needs:
|
||||||
|
- build-cli
|
||||||
|
- version
|
||||||
|
runs-on: blacksmith-4vcpu-windows-2025
|
||||||
|
if: github.repository == 'anomalyco/opencode'
|
||||||
|
env:
|
||||||
|
AZURE_CLIENT_ID: ${{ secrets.AZURE_CLIENT_ID }}
|
||||||
|
AZURE_TENANT_ID: ${{ secrets.AZURE_TENANT_ID }}
|
||||||
|
AZURE_SUBSCRIPTION_ID: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
|
||||||
|
AZURE_TRUSTED_SIGNING_ACCOUNT_NAME: ${{ secrets.AZURE_TRUSTED_SIGNING_ACCOUNT_NAME }}
|
||||||
|
AZURE_TRUSTED_SIGNING_CERTIFICATE_PROFILE: ${{ secrets.AZURE_TRUSTED_SIGNING_CERTIFICATE_PROFILE }}
|
||||||
|
AZURE_TRUSTED_SIGNING_ENDPOINT: ${{ secrets.AZURE_TRUSTED_SIGNING_ENDPOINT }}
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v3
|
||||||
|
|
||||||
|
- uses: actions/download-artifact@v4
|
||||||
|
with:
|
||||||
|
name: opencode-cli-windows
|
||||||
|
path: packages/opencode/dist
|
||||||
|
|
||||||
|
- name: Setup git committer
|
||||||
|
id: committer
|
||||||
|
uses: ./.github/actions/setup-git-committer
|
||||||
|
with:
|
||||||
|
opencode-app-id: ${{ vars.OPENCODE_APP_ID }}
|
||||||
|
opencode-app-secret: ${{ secrets.OPENCODE_APP_SECRET }}
|
||||||
|
|
||||||
|
- name: Azure login
|
||||||
|
uses: azure/login@v2
|
||||||
|
with:
|
||||||
|
client-id: ${{ env.AZURE_CLIENT_ID }}
|
||||||
|
tenant-id: ${{ env.AZURE_TENANT_ID }}
|
||||||
|
subscription-id: ${{ env.AZURE_SUBSCRIPTION_ID }}
|
||||||
|
|
||||||
|
- uses: azure/artifact-signing-action@v1
|
||||||
|
with:
|
||||||
|
endpoint: ${{ env.AZURE_TRUSTED_SIGNING_ENDPOINT }}
|
||||||
|
signing-account-name: ${{ env.AZURE_TRUSTED_SIGNING_ACCOUNT_NAME }}
|
||||||
|
certificate-profile-name: ${{ env.AZURE_TRUSTED_SIGNING_CERTIFICATE_PROFILE }}
|
||||||
|
files: |
|
||||||
|
${{ github.workspace }}\packages\opencode\dist\opencode-windows-arm64\bin\opencode.exe
|
||||||
|
${{ github.workspace }}\packages\opencode\dist\opencode-windows-x64\bin\opencode.exe
|
||||||
|
${{ github.workspace }}\packages\opencode\dist\opencode-windows-x64-baseline\bin\opencode.exe
|
||||||
|
exclude-environment-credential: true
|
||||||
|
exclude-workload-identity-credential: true
|
||||||
|
exclude-managed-identity-credential: true
|
||||||
|
exclude-shared-token-cache-credential: true
|
||||||
|
exclude-visual-studio-credential: true
|
||||||
|
exclude-visual-studio-code-credential: true
|
||||||
|
exclude-azure-cli-credential: false
|
||||||
|
exclude-azure-powershell-credential: true
|
||||||
|
exclude-azure-developer-cli-credential: true
|
||||||
|
exclude-interactive-browser-credential: true
|
||||||
|
|
||||||
|
- name: Verify Windows CLI signatures
|
||||||
|
shell: pwsh
|
||||||
|
run: |
|
||||||
|
$files = @(
|
||||||
|
"${{ github.workspace }}\packages\opencode\dist\opencode-windows-arm64\bin\opencode.exe",
|
||||||
|
"${{ github.workspace }}\packages\opencode\dist\opencode-windows-x64\bin\opencode.exe",
|
||||||
|
"${{ github.workspace }}\packages\opencode\dist\opencode-windows-x64-baseline\bin\opencode.exe"
|
||||||
|
)
|
||||||
|
|
||||||
|
foreach ($file in $files) {
|
||||||
|
$sig = Get-AuthenticodeSignature $file
|
||||||
|
if ($sig.Status -ne "Valid") {
|
||||||
|
throw "Invalid signature for ${file}: $($sig.Status)"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
- name: Repack Windows CLI archives
|
||||||
|
working-directory: packages/opencode/dist
|
||||||
|
shell: pwsh
|
||||||
|
run: |
|
||||||
|
Compress-Archive -Path "opencode-windows-arm64\bin\*" -DestinationPath "opencode-windows-arm64.zip" -Force
|
||||||
|
Compress-Archive -Path "opencode-windows-x64\bin\*" -DestinationPath "opencode-windows-x64.zip" -Force
|
||||||
|
Compress-Archive -Path "opencode-windows-x64-baseline\bin\*" -DestinationPath "opencode-windows-x64-baseline.zip" -Force
|
||||||
|
|
||||||
|
- name: Upload signed Windows CLI release assets
|
||||||
|
if: needs.version.outputs.release != ''
|
||||||
|
shell: pwsh
|
||||||
|
env:
|
||||||
|
GH_TOKEN: ${{ steps.committer.outputs.token }}
|
||||||
|
run: |
|
||||||
|
gh release upload "v${{ needs.version.outputs.version }}" `
|
||||||
|
"${{ github.workspace }}\packages\opencode\dist\opencode-windows-arm64.zip" `
|
||||||
|
"${{ github.workspace }}\packages\opencode\dist\opencode-windows-x64.zip" `
|
||||||
|
"${{ github.workspace }}\packages\opencode\dist\opencode-windows-x64-baseline.zip" `
|
||||||
|
--clobber `
|
||||||
|
--repo "${{ needs.version.outputs.repo }}"
|
||||||
|
|
||||||
|
- uses: actions/upload-artifact@v4
|
||||||
|
with:
|
||||||
|
name: opencode-cli-signed-windows
|
||||||
|
path: |
|
||||||
|
packages/opencode/dist/opencode-windows-arm64
|
||||||
|
packages/opencode/dist/opencode-windows-x64
|
||||||
|
packages/opencode/dist/opencode-windows-x64-baseline
|
||||||
|
|
||||||
build-tauri:
|
build-tauri:
|
||||||
needs:
|
needs:
|
||||||
- build-cli
|
- build-cli
|
||||||
- version
|
- version
|
||||||
continue-on-error: false
|
continue-on-error: false
|
||||||
|
env:
|
||||||
|
AZURE_CLIENT_ID: ${{ secrets.AZURE_CLIENT_ID }}
|
||||||
|
AZURE_TENANT_ID: ${{ secrets.AZURE_TENANT_ID }}
|
||||||
|
AZURE_SUBSCRIPTION_ID: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
|
||||||
|
AZURE_TRUSTED_SIGNING_ACCOUNT_NAME: ${{ secrets.AZURE_TRUSTED_SIGNING_ACCOUNT_NAME }}
|
||||||
|
AZURE_TRUSTED_SIGNING_CERTIFICATE_PROFILE: ${{ secrets.AZURE_TRUSTED_SIGNING_CERTIFICATE_PROFILE }}
|
||||||
|
AZURE_TRUSTED_SIGNING_ENDPOINT: ${{ secrets.AZURE_TRUSTED_SIGNING_ENDPOINT }}
|
||||||
strategy:
|
strategy:
|
||||||
fail-fast: false
|
fail-fast: false
|
||||||
matrix:
|
matrix:
|
||||||
@@ -152,6 +266,14 @@ jobs:
|
|||||||
|
|
||||||
- uses: ./.github/actions/setup-bun
|
- uses: ./.github/actions/setup-bun
|
||||||
|
|
||||||
|
- name: Azure login
|
||||||
|
if: runner.os == 'Windows'
|
||||||
|
uses: azure/login@v2
|
||||||
|
with:
|
||||||
|
client-id: ${{ env.AZURE_CLIENT_ID }}
|
||||||
|
tenant-id: ${{ env.AZURE_TENANT_ID }}
|
||||||
|
subscription-id: ${{ env.AZURE_SUBSCRIPTION_ID }}
|
||||||
|
|
||||||
- uses: actions/setup-node@v4
|
- uses: actions/setup-node@v4
|
||||||
with:
|
with:
|
||||||
node-version: "24"
|
node-version: "24"
|
||||||
@@ -190,6 +312,7 @@ jobs:
|
|||||||
env:
|
env:
|
||||||
OPENCODE_VERSION: ${{ needs.version.outputs.version }}
|
OPENCODE_VERSION: ${{ needs.version.outputs.version }}
|
||||||
GITHUB_TOKEN: ${{ steps.committer.outputs.token }}
|
GITHUB_TOKEN: ${{ steps.committer.outputs.token }}
|
||||||
|
OPENCODE_CLI_ARTIFACT: ${{ (runner.os == 'Windows' && 'opencode-cli-windows') || 'opencode-cli' }}
|
||||||
RUST_TARGET: ${{ matrix.settings.target }}
|
RUST_TARGET: ${{ matrix.settings.target }}
|
||||||
GH_TOKEN: ${{ github.token }}
|
GH_TOKEN: ${{ github.token }}
|
||||||
GITHUB_RUN_ID: ${{ github.run_id }}
|
GITHUB_RUN_ID: ${{ github.run_id }}
|
||||||
@@ -246,11 +369,34 @@ jobs:
|
|||||||
APPLE_API_KEY: ${{ secrets.APPLE_API_KEY }}
|
APPLE_API_KEY: ${{ secrets.APPLE_API_KEY }}
|
||||||
APPLE_API_KEY_PATH: ${{ runner.temp }}/apple-api-key.p8
|
APPLE_API_KEY_PATH: ${{ runner.temp }}/apple-api-key.p8
|
||||||
|
|
||||||
|
- name: Verify signed Windows desktop artifacts
|
||||||
|
if: runner.os == 'Windows'
|
||||||
|
shell: pwsh
|
||||||
|
run: |
|
||||||
|
$files = @(
|
||||||
|
"${{ github.workspace }}\packages\desktop\src-tauri\sidecars\opencode-cli-${{ matrix.settings.target }}.exe"
|
||||||
|
)
|
||||||
|
$files += Get-ChildItem "${{ github.workspace }}\packages\desktop\src-tauri\target\${{ matrix.settings.target }}\release\bundle\nsis\*.exe" | Select-Object -ExpandProperty FullName
|
||||||
|
|
||||||
|
foreach ($file in $files) {
|
||||||
|
$sig = Get-AuthenticodeSignature $file
|
||||||
|
if ($sig.Status -ne "Valid") {
|
||||||
|
throw "Invalid signature for ${file}: $($sig.Status)"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
build-electron:
|
build-electron:
|
||||||
needs:
|
needs:
|
||||||
- build-cli
|
- build-cli
|
||||||
- version
|
- version
|
||||||
continue-on-error: false
|
continue-on-error: false
|
||||||
|
env:
|
||||||
|
AZURE_CLIENT_ID: ${{ secrets.AZURE_CLIENT_ID }}
|
||||||
|
AZURE_TENANT_ID: ${{ secrets.AZURE_TENANT_ID }}
|
||||||
|
AZURE_SUBSCRIPTION_ID: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
|
||||||
|
AZURE_TRUSTED_SIGNING_ACCOUNT_NAME: ${{ secrets.AZURE_TRUSTED_SIGNING_ACCOUNT_NAME }}
|
||||||
|
AZURE_TRUSTED_SIGNING_CERTIFICATE_PROFILE: ${{ secrets.AZURE_TRUSTED_SIGNING_CERTIFICATE_PROFILE }}
|
||||||
|
AZURE_TRUSTED_SIGNING_ENDPOINT: ${{ secrets.AZURE_TRUSTED_SIGNING_ENDPOINT }}
|
||||||
strategy:
|
strategy:
|
||||||
fail-fast: false
|
fail-fast: false
|
||||||
matrix:
|
matrix:
|
||||||
@@ -292,6 +438,14 @@ jobs:
|
|||||||
|
|
||||||
- uses: ./.github/actions/setup-bun
|
- uses: ./.github/actions/setup-bun
|
||||||
|
|
||||||
|
- name: Azure login
|
||||||
|
if: runner.os == 'Windows'
|
||||||
|
uses: azure/login@v2
|
||||||
|
with:
|
||||||
|
client-id: ${{ env.AZURE_CLIENT_ID }}
|
||||||
|
tenant-id: ${{ env.AZURE_TENANT_ID }}
|
||||||
|
subscription-id: ${{ env.AZURE_SUBSCRIPTION_ID }}
|
||||||
|
|
||||||
- uses: actions/setup-node@v4
|
- uses: actions/setup-node@v4
|
||||||
with:
|
with:
|
||||||
node-version: "24"
|
node-version: "24"
|
||||||
@@ -326,6 +480,7 @@ jobs:
|
|||||||
env:
|
env:
|
||||||
OPENCODE_VERSION: ${{ needs.version.outputs.version }}
|
OPENCODE_VERSION: ${{ needs.version.outputs.version }}
|
||||||
OPENCODE_CHANNEL: ${{ (github.ref_name == 'beta' && 'beta') || 'prod' }}
|
OPENCODE_CHANNEL: ${{ (github.ref_name == 'beta' && 'beta') || 'prod' }}
|
||||||
|
OPENCODE_CLI_ARTIFACT: ${{ (runner.os == 'Windows' && 'opencode-cli-windows') || 'opencode-cli' }}
|
||||||
RUST_TARGET: ${{ matrix.settings.target }}
|
RUST_TARGET: ${{ matrix.settings.target }}
|
||||||
GH_TOKEN: ${{ github.token }}
|
GH_TOKEN: ${{ github.token }}
|
||||||
GITHUB_RUN_ID: ${{ github.run_id }}
|
GITHUB_RUN_ID: ${{ github.run_id }}
|
||||||
@@ -358,6 +513,22 @@ jobs:
|
|||||||
env:
|
env:
|
||||||
OPENCODE_CHANNEL: ${{ (github.ref_name == 'beta' && 'beta') || 'prod' }}
|
OPENCODE_CHANNEL: ${{ (github.ref_name == 'beta' && 'beta') || 'prod' }}
|
||||||
|
|
||||||
|
- name: Verify signed Windows Electron artifacts
|
||||||
|
if: runner.os == 'Windows'
|
||||||
|
shell: pwsh
|
||||||
|
run: |
|
||||||
|
$files = @()
|
||||||
|
$files += Get-ChildItem "${{ github.workspace }}\packages\desktop-electron\dist\*.exe" | Select-Object -ExpandProperty FullName
|
||||||
|
$files += Get-ChildItem "${{ github.workspace }}\packages\desktop-electron\dist\*unpacked\*.exe" | Select-Object -ExpandProperty FullName
|
||||||
|
$files += Get-ChildItem "${{ github.workspace }}\packages\desktop-electron\dist\*unpacked\resources\opencode-cli.exe" -ErrorAction SilentlyContinue | Select-Object -ExpandProperty FullName
|
||||||
|
|
||||||
|
foreach ($file in $files | Select-Object -Unique) {
|
||||||
|
$sig = Get-AuthenticodeSignature $file
|
||||||
|
if ($sig.Status -ne "Valid") {
|
||||||
|
throw "Invalid signature for ${file}: $($sig.Status)"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
- uses: actions/upload-artifact@v4
|
- uses: actions/upload-artifact@v4
|
||||||
with:
|
with:
|
||||||
name: opencode-electron-${{ matrix.settings.target }}
|
name: opencode-electron-${{ matrix.settings.target }}
|
||||||
@@ -373,6 +544,7 @@ jobs:
|
|||||||
needs:
|
needs:
|
||||||
- version
|
- version
|
||||||
- build-cli
|
- build-cli
|
||||||
|
- sign-cli-windows
|
||||||
- build-tauri
|
- build-tauri
|
||||||
- build-electron
|
- build-electron
|
||||||
runs-on: blacksmith-4vcpu-ubuntu-2404
|
runs-on: blacksmith-4vcpu-ubuntu-2404
|
||||||
@@ -411,6 +583,16 @@ jobs:
|
|||||||
name: opencode-cli
|
name: opencode-cli
|
||||||
path: packages/opencode/dist
|
path: packages/opencode/dist
|
||||||
|
|
||||||
|
- uses: actions/download-artifact@v4
|
||||||
|
with:
|
||||||
|
name: opencode-cli-windows
|
||||||
|
path: packages/opencode/dist
|
||||||
|
|
||||||
|
- uses: actions/download-artifact@v4
|
||||||
|
with:
|
||||||
|
name: opencode-cli-signed-windows
|
||||||
|
path: packages/opencode/dist
|
||||||
|
|
||||||
- uses: actions/download-artifact@v4
|
- uses: actions/download-artifact@v4
|
||||||
if: needs.version.outputs.release
|
if: needs.version.outputs.release
|
||||||
with:
|
with:
|
||||||
|
|||||||
@@ -1,54 +0,0 @@
|
|||||||
name: sign-cli
|
|
||||||
|
|
||||||
on:
|
|
||||||
push:
|
|
||||||
branches:
|
|
||||||
- brendan/desktop-signpath
|
|
||||||
workflow_dispatch:
|
|
||||||
|
|
||||||
permissions:
|
|
||||||
contents: read
|
|
||||||
actions: read
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
sign-cli:
|
|
||||||
runs-on: blacksmith-4vcpu-ubuntu-2404
|
|
||||||
if: github.repository == 'anomalyco/opencode'
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v3
|
|
||||||
with:
|
|
||||||
fetch-tags: true
|
|
||||||
|
|
||||||
- uses: ./.github/actions/setup-bun
|
|
||||||
|
|
||||||
- name: Build
|
|
||||||
run: |
|
|
||||||
./packages/opencode/script/build.ts
|
|
||||||
|
|
||||||
- name: Upload unsigned Windows CLI
|
|
||||||
id: upload_unsigned_windows_cli
|
|
||||||
uses: actions/upload-artifact@v4
|
|
||||||
with:
|
|
||||||
name: unsigned-opencode-windows-cli
|
|
||||||
path: packages/opencode/dist/opencode-windows-x64/bin/opencode.exe
|
|
||||||
if-no-files-found: error
|
|
||||||
|
|
||||||
- name: Submit SignPath signing request
|
|
||||||
id: submit_signpath_signing_request
|
|
||||||
uses: signpath/github-action-submit-signing-request@v1
|
|
||||||
with:
|
|
||||||
api-token: ${{ secrets.SIGNPATH_API_KEY }}
|
|
||||||
organization-id: ${{ secrets.SIGNPATH_ORGANIZATION_ID }}
|
|
||||||
project-slug: ${{ secrets.SIGNPATH_PROJECT_SLUG }}
|
|
||||||
signing-policy-slug: ${{ secrets.SIGNPATH_SIGNING_POLICY_SLUG }}
|
|
||||||
artifact-configuration-slug: ${{ secrets.SIGNPATH_ARTIFACT_CONFIGURATION_SLUG }}
|
|
||||||
github-artifact-id: ${{ steps.upload_unsigned_windows_cli.outputs.artifact-id }}
|
|
||||||
wait-for-completion: true
|
|
||||||
output-artifact-directory: signed-opencode-cli
|
|
||||||
|
|
||||||
- name: Upload signed Windows CLI
|
|
||||||
uses: actions/upload-artifact@v4
|
|
||||||
with:
|
|
||||||
name: signed-opencode-windows-cli
|
|
||||||
path: signed-opencode-cli/*.exe
|
|
||||||
if-no-files-found: error
|
|
||||||
@@ -1,33 +0,0 @@
|
|||||||
name: stale-issues
|
|
||||||
|
|
||||||
on:
|
|
||||||
schedule:
|
|
||||||
- cron: "30 1 * * *" # Daily at 1:30 AM
|
|
||||||
workflow_dispatch:
|
|
||||||
|
|
||||||
env:
|
|
||||||
DAYS_BEFORE_STALE: 90
|
|
||||||
DAYS_BEFORE_CLOSE: 7
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
stale:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
permissions:
|
|
||||||
issues: write
|
|
||||||
steps:
|
|
||||||
- uses: actions/stale@v10
|
|
||||||
with:
|
|
||||||
days-before-stale: ${{ env.DAYS_BEFORE_STALE }}
|
|
||||||
days-before-close: ${{ env.DAYS_BEFORE_CLOSE }}
|
|
||||||
stale-issue-label: "stale"
|
|
||||||
close-issue-message: |
|
|
||||||
[automated] Closing due to ${{ env.DAYS_BEFORE_STALE }}+ days of inactivity.
|
|
||||||
|
|
||||||
Feel free to reopen if you still need this!
|
|
||||||
stale-issue-message: |
|
|
||||||
[automated] This issue has had no activity for ${{ env.DAYS_BEFORE_STALE }} days.
|
|
||||||
|
|
||||||
It will be closed in ${{ env.DAYS_BEFORE_CLOSE }} days if there's no new activity.
|
|
||||||
remove-stale-when-updated: true
|
|
||||||
exempt-issue-labels: "pinned,security,feature-request,on-hold"
|
|
||||||
start-date: "2025-12-27"
|
|
||||||
@@ -50,20 +50,17 @@ jobs:
|
|||||||
|
|
||||||
e2e:
|
e2e:
|
||||||
name: e2e (${{ matrix.settings.name }})
|
name: e2e (${{ matrix.settings.name }})
|
||||||
needs: unit
|
|
||||||
strategy:
|
strategy:
|
||||||
fail-fast: false
|
fail-fast: false
|
||||||
matrix:
|
matrix:
|
||||||
settings:
|
settings:
|
||||||
- name: linux
|
- name: linux
|
||||||
host: blacksmith-4vcpu-ubuntu-2404
|
host: blacksmith-4vcpu-ubuntu-2404
|
||||||
playwright: bunx playwright install --with-deps
|
|
||||||
- name: windows
|
- name: windows
|
||||||
host: blacksmith-4vcpu-windows-2025
|
host: blacksmith-4vcpu-windows-2025
|
||||||
playwright: bunx playwright install
|
|
||||||
runs-on: ${{ matrix.settings.host }}
|
runs-on: ${{ matrix.settings.host }}
|
||||||
env:
|
env:
|
||||||
PLAYWRIGHT_BROWSERS_PATH: 0
|
PLAYWRIGHT_BROWSERS_PATH: ${{ github.workspace }}/.playwright-browsers
|
||||||
defaults:
|
defaults:
|
||||||
run:
|
run:
|
||||||
shell: bash
|
shell: bash
|
||||||
@@ -76,14 +73,36 @@ jobs:
|
|||||||
- name: Setup Bun
|
- name: Setup Bun
|
||||||
uses: ./.github/actions/setup-bun
|
uses: ./.github/actions/setup-bun
|
||||||
|
|
||||||
- name: Install Playwright browsers
|
- name: Read Playwright version
|
||||||
|
id: playwright-version
|
||||||
|
run: |
|
||||||
|
version=$(node -e 'console.log(require("./packages/app/package.json").devDependencies["@playwright/test"])')
|
||||||
|
echo "version=$version" >> "$GITHUB_OUTPUT"
|
||||||
|
|
||||||
|
- name: Cache Playwright browsers
|
||||||
|
id: playwright-cache
|
||||||
|
uses: actions/cache@v4
|
||||||
|
with:
|
||||||
|
path: ${{ github.workspace }}/.playwright-browsers
|
||||||
|
key: ${{ runner.os }}-${{ runner.arch }}-playwright-${{ steps.playwright-version.outputs.version }}-chromium
|
||||||
|
|
||||||
|
- name: Install Playwright system dependencies
|
||||||
|
if: runner.os == 'Linux'
|
||||||
working-directory: packages/app
|
working-directory: packages/app
|
||||||
run: ${{ matrix.settings.playwright }}
|
run: bunx playwright install-deps chromium
|
||||||
|
|
||||||
|
- name: Install Playwright browsers
|
||||||
|
if: steps.playwright-cache.outputs.cache-hit != 'true'
|
||||||
|
working-directory: packages/app
|
||||||
|
run: bunx playwright install chromium
|
||||||
|
|
||||||
- name: Run app e2e tests
|
- name: Run app e2e tests
|
||||||
run: bun --cwd packages/app test:e2e:local
|
run: bun --cwd packages/app test:e2e:local
|
||||||
env:
|
env:
|
||||||
CI: true
|
CI: true
|
||||||
|
OPENCODE_API_KEY: ${{ secrets.OPENCODE_API_KEY }}
|
||||||
|
OPENCODE_E2E_MODEL: opencode/claude-haiku-4-5
|
||||||
|
OPENCODE_E2E_REQUIRE_PAID: "true"
|
||||||
timeout-minutes: 30
|
timeout-minutes: 30
|
||||||
|
|
||||||
- name: Upload Playwright artifacts
|
- name: Upload Playwright artifacts
|
||||||
|
|||||||
@@ -33,6 +33,6 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
issue-id: ${{ github.event.issue.number }}
|
issue-id: ${{ github.event.issue.number }}
|
||||||
comment-id: ${{ github.event.comment.id }}
|
comment-id: ${{ github.event.comment.id }}
|
||||||
roles: admin,maintain
|
roles: admin,maintain,write
|
||||||
env:
|
env:
|
||||||
GITHUB_TOKEN: ${{ steps.committer.outputs.token }}
|
GITHUB_TOKEN: ${{ steps.committer.outputs.token }}
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ target
|
|||||||
|
|
||||||
# Local dev files
|
# Local dev files
|
||||||
opencode-dev
|
opencode-dev
|
||||||
|
UPCOMING_CHANGELOG.md
|
||||||
logs/
|
logs/
|
||||||
*.bun-build
|
*.bun-build
|
||||||
tsconfig.tsbuildinfo
|
tsconfig.tsbuildinfo
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
plans/
|
node_modules
|
||||||
bun.lock
|
plans
|
||||||
package.json
|
package.json
|
||||||
|
bun.lock
|
||||||
|
.gitignore
|
||||||
package-lock.json
|
package-lock.json
|
||||||
@@ -1,34 +0,0 @@
|
|||||||
---
|
|
||||||
description: ALWAYS use this when writing docs
|
|
||||||
color: "#38A3EE"
|
|
||||||
---
|
|
||||||
|
|
||||||
You are an expert technical documentation writer
|
|
||||||
|
|
||||||
You are not verbose
|
|
||||||
|
|
||||||
Use a relaxed and friendly tone
|
|
||||||
|
|
||||||
The title of the page should be a word or a 2-3 word phrase
|
|
||||||
|
|
||||||
The description should be one short line, should not start with "The", should
|
|
||||||
avoid repeating the title of the page, should be 5-10 words long
|
|
||||||
|
|
||||||
Chunks of text should not be more than 2 sentences long
|
|
||||||
|
|
||||||
Each section is separated by a divider of 3 dashes
|
|
||||||
|
|
||||||
The section titles are short with only the first letter of the word capitalized
|
|
||||||
|
|
||||||
The section titles are in the imperative mood
|
|
||||||
|
|
||||||
The section titles should not repeat the term used in the page title, for
|
|
||||||
example, if the page title is "Models", avoid using a section title like "Add
|
|
||||||
new models". This might be unavoidable in some cases, but try to avoid it.
|
|
||||||
|
|
||||||
Check out the /packages/web/src/content/docs/docs/index.mdx as an example.
|
|
||||||
|
|
||||||
For JS or TS code snippets remove trailing semicolons and any trailing commas
|
|
||||||
that might not be needed.
|
|
||||||
|
|
||||||
If you are making a commit prefix the commit message with `docs:`
|
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
---
|
---
|
||||||
description: Translate content for a specified locale while preserving technical terms
|
description: Translate content for a specified locale while preserving technical terms
|
||||||
mode: subagent
|
mode: subagent
|
||||||
model: opencode/gemini-3-pro
|
model: opencode/gpt-5.4
|
||||||
---
|
---
|
||||||
|
|
||||||
You are a professional translator and localization specialist.
|
You are a professional translator and localization specialist.
|
||||||
|
|||||||
@@ -0,0 +1,46 @@
|
|||||||
|
---
|
||||||
|
model: opencode/gpt-5.4
|
||||||
|
---
|
||||||
|
|
||||||
|
Create `UPCOMING_CHANGELOG.md` from the structured changelog input below.
|
||||||
|
If `UPCOMING_CHANGELOG.md` already exists, ignore its current contents completely.
|
||||||
|
Do not preserve, merge, or reuse text from the existing file.
|
||||||
|
|
||||||
|
The input already contains the exact commit range since the last non-draft release.
|
||||||
|
The commits are already filtered to the release-relevant packages and grouped into
|
||||||
|
the release sections. Do not fetch GitHub releases, PRs, or build your own commit list.
|
||||||
|
The input may also include a `## Community Contributors Input` section.
|
||||||
|
|
||||||
|
Before writing any entry you keep, inspect the real diff with
|
||||||
|
`git show --stat --format='' <hash>` or `git show --format='' <hash>` so you can
|
||||||
|
understand the actual code changes and not just the commit message (they may be misleading).
|
||||||
|
Do not use `git log` or author metadata when deciding attribution.
|
||||||
|
|
||||||
|
Rules:
|
||||||
|
|
||||||
|
- Write the final file with sections in this order:
|
||||||
|
`## Core`, `## TUI`, `## Desktop`, `## SDK`, `## Extensions`
|
||||||
|
- Only include sections that have at least one notable entry
|
||||||
|
- Keep one bullet per commit you keep
|
||||||
|
- Skip commits that are entirely internal, CI, tests, refactors, or otherwise not user-facing
|
||||||
|
- Start each bullet with a capital letter
|
||||||
|
- Prefer what changed for users over what code changed internally
|
||||||
|
- Do not copy raw commit prefixes like `fix:` or `feat:` or trailing PR numbers like `(#123)`
|
||||||
|
- Community attribution is deterministic: only preserve an existing `(@username)` suffix from the changelog input
|
||||||
|
- If an input bullet has no `(@username)` suffix, do not add one
|
||||||
|
- Never add a new `(@username)` suffix from `git show`, commit authors, names, or email addresses
|
||||||
|
- If no notable entries remain and there is no contributor block, write exactly `No notable changes.`
|
||||||
|
- If no notable entries remain but there is a contributor block, omit all release sections and return only the contributor block
|
||||||
|
- If the input contains `## Community Contributors Input`, append the block below that heading to the end of the final file verbatim
|
||||||
|
- Do not add, remove, rewrite, or reorder contributor names or commit titles in that block
|
||||||
|
- Do not derive the thank-you section from the main summary bullets
|
||||||
|
- Do not include the heading `## Community Contributors Input` in the final file
|
||||||
|
- Focus on writing the least words to get your point across - users will skim read the changelog, so we should be precise
|
||||||
|
|
||||||
|
**Importantly, the changelog is for users (who are at least slightly technical), they may use the TUI, Desktop, SDK, Plugins and so forth. Be thorough in understanding flow on effects may not be immediately apparent. e.g. a package upgrade looks internal but may patch a bug. Or a refactor may also stabilise some race condition that fixes bugs for users. The PR title/body + commit message will give you the authors context, usually containing the outcome not just technical detail**
|
||||||
|
|
||||||
|
<changelog_input>
|
||||||
|
|
||||||
|
!`bun script/raw-changelog.ts $ARGUMENTS`
|
||||||
|
|
||||||
|
</changelog_input>
|
||||||
@@ -0,0 +1,223 @@
|
|||||||
|
{
|
||||||
|
"$schema": "https://opencode.ai/theme.json",
|
||||||
|
"defs": {
|
||||||
|
"nord0": "#2E3440",
|
||||||
|
"nord1": "#3B4252",
|
||||||
|
"nord2": "#434C5E",
|
||||||
|
"nord3": "#4C566A",
|
||||||
|
"nord4": "#D8DEE9",
|
||||||
|
"nord5": "#E5E9F0",
|
||||||
|
"nord6": "#ECEFF4",
|
||||||
|
"nord7": "#8FBCBB",
|
||||||
|
"nord8": "#88C0D0",
|
||||||
|
"nord9": "#81A1C1",
|
||||||
|
"nord10": "#5E81AC",
|
||||||
|
"nord11": "#BF616A",
|
||||||
|
"nord12": "#D08770",
|
||||||
|
"nord13": "#EBCB8B",
|
||||||
|
"nord14": "#A3BE8C",
|
||||||
|
"nord15": "#B48EAD"
|
||||||
|
},
|
||||||
|
"theme": {
|
||||||
|
"primary": {
|
||||||
|
"dark": "nord10",
|
||||||
|
"light": "nord9"
|
||||||
|
},
|
||||||
|
"secondary": {
|
||||||
|
"dark": "nord9",
|
||||||
|
"light": "nord9"
|
||||||
|
},
|
||||||
|
"accent": {
|
||||||
|
"dark": "nord7",
|
||||||
|
"light": "nord7"
|
||||||
|
},
|
||||||
|
"error": {
|
||||||
|
"dark": "nord11",
|
||||||
|
"light": "nord11"
|
||||||
|
},
|
||||||
|
"warning": {
|
||||||
|
"dark": "nord12",
|
||||||
|
"light": "nord12"
|
||||||
|
},
|
||||||
|
"success": {
|
||||||
|
"dark": "nord14",
|
||||||
|
"light": "nord14"
|
||||||
|
},
|
||||||
|
"info": {
|
||||||
|
"dark": "nord8",
|
||||||
|
"light": "nord10"
|
||||||
|
},
|
||||||
|
"text": {
|
||||||
|
"dark": "nord6",
|
||||||
|
"light": "nord0"
|
||||||
|
},
|
||||||
|
"textMuted": {
|
||||||
|
"dark": "#8B95A7",
|
||||||
|
"light": "nord1"
|
||||||
|
},
|
||||||
|
"background": {
|
||||||
|
"dark": "nord0",
|
||||||
|
"light": "nord6"
|
||||||
|
},
|
||||||
|
"backgroundPanel": {
|
||||||
|
"dark": "nord1",
|
||||||
|
"light": "nord5"
|
||||||
|
},
|
||||||
|
"backgroundElement": {
|
||||||
|
"dark": "nord2",
|
||||||
|
"light": "nord4"
|
||||||
|
},
|
||||||
|
"border": {
|
||||||
|
"dark": "nord2",
|
||||||
|
"light": "nord3"
|
||||||
|
},
|
||||||
|
"borderActive": {
|
||||||
|
"dark": "nord3",
|
||||||
|
"light": "nord2"
|
||||||
|
},
|
||||||
|
"borderSubtle": {
|
||||||
|
"dark": "nord2",
|
||||||
|
"light": "nord3"
|
||||||
|
},
|
||||||
|
"diffAdded": {
|
||||||
|
"dark": "nord14",
|
||||||
|
"light": "nord14"
|
||||||
|
},
|
||||||
|
"diffRemoved": {
|
||||||
|
"dark": "nord11",
|
||||||
|
"light": "nord11"
|
||||||
|
},
|
||||||
|
"diffContext": {
|
||||||
|
"dark": "#8B95A7",
|
||||||
|
"light": "nord3"
|
||||||
|
},
|
||||||
|
"diffHunkHeader": {
|
||||||
|
"dark": "#8B95A7",
|
||||||
|
"light": "nord3"
|
||||||
|
},
|
||||||
|
"diffHighlightAdded": {
|
||||||
|
"dark": "nord14",
|
||||||
|
"light": "nord14"
|
||||||
|
},
|
||||||
|
"diffHighlightRemoved": {
|
||||||
|
"dark": "nord11",
|
||||||
|
"light": "nord11"
|
||||||
|
},
|
||||||
|
"diffAddedBg": {
|
||||||
|
"dark": "#36413C",
|
||||||
|
"light": "#E6EBE7"
|
||||||
|
},
|
||||||
|
"diffRemovedBg": {
|
||||||
|
"dark": "#43393D",
|
||||||
|
"light": "#ECE6E8"
|
||||||
|
},
|
||||||
|
"diffContextBg": {
|
||||||
|
"dark": "nord1",
|
||||||
|
"light": "nord5"
|
||||||
|
},
|
||||||
|
"diffLineNumber": {
|
||||||
|
"dark": "nord2",
|
||||||
|
"light": "nord4"
|
||||||
|
},
|
||||||
|
"diffAddedLineNumberBg": {
|
||||||
|
"dark": "#303A35",
|
||||||
|
"light": "#DDE4DF"
|
||||||
|
},
|
||||||
|
"diffRemovedLineNumberBg": {
|
||||||
|
"dark": "#3C3336",
|
||||||
|
"light": "#E4DDE0"
|
||||||
|
},
|
||||||
|
"markdownText": {
|
||||||
|
"dark": "nord4",
|
||||||
|
"light": "nord0"
|
||||||
|
},
|
||||||
|
"markdownHeading": {
|
||||||
|
"dark": "nord8",
|
||||||
|
"light": "nord10"
|
||||||
|
},
|
||||||
|
"markdownLink": {
|
||||||
|
"dark": "nord9",
|
||||||
|
"light": "nord9"
|
||||||
|
},
|
||||||
|
"markdownLinkText": {
|
||||||
|
"dark": "nord7",
|
||||||
|
"light": "nord7"
|
||||||
|
},
|
||||||
|
"markdownCode": {
|
||||||
|
"dark": "nord14",
|
||||||
|
"light": "nord14"
|
||||||
|
},
|
||||||
|
"markdownBlockQuote": {
|
||||||
|
"dark": "#8B95A7",
|
||||||
|
"light": "nord3"
|
||||||
|
},
|
||||||
|
"markdownEmph": {
|
||||||
|
"dark": "nord12",
|
||||||
|
"light": "nord12"
|
||||||
|
},
|
||||||
|
"markdownStrong": {
|
||||||
|
"dark": "nord13",
|
||||||
|
"light": "nord13"
|
||||||
|
},
|
||||||
|
"markdownHorizontalRule": {
|
||||||
|
"dark": "#8B95A7",
|
||||||
|
"light": "nord3"
|
||||||
|
},
|
||||||
|
"markdownListItem": {
|
||||||
|
"dark": "nord8",
|
||||||
|
"light": "nord10"
|
||||||
|
},
|
||||||
|
"markdownListEnumeration": {
|
||||||
|
"dark": "nord7",
|
||||||
|
"light": "nord7"
|
||||||
|
},
|
||||||
|
"markdownImage": {
|
||||||
|
"dark": "nord9",
|
||||||
|
"light": "nord9"
|
||||||
|
},
|
||||||
|
"markdownImageText": {
|
||||||
|
"dark": "nord7",
|
||||||
|
"light": "nord7"
|
||||||
|
},
|
||||||
|
"markdownCodeBlock": {
|
||||||
|
"dark": "nord4",
|
||||||
|
"light": "nord0"
|
||||||
|
},
|
||||||
|
"syntaxComment": {
|
||||||
|
"dark": "#8B95A7",
|
||||||
|
"light": "nord3"
|
||||||
|
},
|
||||||
|
"syntaxKeyword": {
|
||||||
|
"dark": "nord9",
|
||||||
|
"light": "nord9"
|
||||||
|
},
|
||||||
|
"syntaxFunction": {
|
||||||
|
"dark": "nord8",
|
||||||
|
"light": "nord8"
|
||||||
|
},
|
||||||
|
"syntaxVariable": {
|
||||||
|
"dark": "nord7",
|
||||||
|
"light": "nord7"
|
||||||
|
},
|
||||||
|
"syntaxString": {
|
||||||
|
"dark": "nord14",
|
||||||
|
"light": "nord14"
|
||||||
|
},
|
||||||
|
"syntaxNumber": {
|
||||||
|
"dark": "nord15",
|
||||||
|
"light": "nord15"
|
||||||
|
},
|
||||||
|
"syntaxType": {
|
||||||
|
"dark": "nord7",
|
||||||
|
"light": "nord7"
|
||||||
|
},
|
||||||
|
"syntaxOperator": {
|
||||||
|
"dark": "nord9",
|
||||||
|
"light": "nord9"
|
||||||
|
},
|
||||||
|
"syntaxPunctuation": {
|
||||||
|
"dark": "nord4",
|
||||||
|
"light": "nord0"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,891 @@
|
|||||||
|
/** @jsxImportSource @opentui/solid */
|
||||||
|
import { useKeyboard, useTerminalDimensions, type JSX } from "@opentui/solid"
|
||||||
|
import { RGBA, VignetteEffect } from "@opentui/core"
|
||||||
|
import type {
|
||||||
|
TuiKeybindSet,
|
||||||
|
TuiPlugin,
|
||||||
|
TuiPluginApi,
|
||||||
|
TuiPluginMeta,
|
||||||
|
TuiPluginModule,
|
||||||
|
TuiSlotPlugin,
|
||||||
|
} from "@opencode-ai/plugin/tui"
|
||||||
|
|
||||||
|
const tabs = ["overview", "counter", "help"]
|
||||||
|
const bind = {
|
||||||
|
modal: "ctrl+shift+m",
|
||||||
|
screen: "ctrl+shift+o",
|
||||||
|
home: "escape,ctrl+h",
|
||||||
|
left: "left,h",
|
||||||
|
right: "right,l",
|
||||||
|
up: "up,k",
|
||||||
|
down: "down,j",
|
||||||
|
alert: "a",
|
||||||
|
confirm: "c",
|
||||||
|
prompt: "p",
|
||||||
|
select: "s",
|
||||||
|
modal_accept: "enter,return",
|
||||||
|
modal_close: "escape",
|
||||||
|
dialog_close: "escape",
|
||||||
|
local: "x",
|
||||||
|
local_push: "enter,return",
|
||||||
|
local_close: "q,backspace",
|
||||||
|
host: "z",
|
||||||
|
}
|
||||||
|
|
||||||
|
const pick = (value: unknown, fallback: string) => {
|
||||||
|
if (typeof value !== "string") return fallback
|
||||||
|
if (!value.trim()) return fallback
|
||||||
|
return value
|
||||||
|
}
|
||||||
|
|
||||||
|
const num = (value: unknown, fallback: number) => {
|
||||||
|
if (typeof value !== "number") return fallback
|
||||||
|
return value
|
||||||
|
}
|
||||||
|
|
||||||
|
const rec = (value: unknown) => {
|
||||||
|
if (!value || typeof value !== "object" || Array.isArray(value)) return
|
||||||
|
return Object.fromEntries(Object.entries(value))
|
||||||
|
}
|
||||||
|
|
||||||
|
type Cfg = {
|
||||||
|
label: string
|
||||||
|
route: string
|
||||||
|
vignette: number
|
||||||
|
keybinds: Record<string, unknown> | undefined
|
||||||
|
}
|
||||||
|
|
||||||
|
type Route = {
|
||||||
|
modal: string
|
||||||
|
screen: string
|
||||||
|
}
|
||||||
|
|
||||||
|
type State = {
|
||||||
|
tab: number
|
||||||
|
count: number
|
||||||
|
source: string
|
||||||
|
note: string
|
||||||
|
selected: string
|
||||||
|
local: number
|
||||||
|
}
|
||||||
|
|
||||||
|
const cfg = (options: Record<string, unknown> | undefined) => {
|
||||||
|
return {
|
||||||
|
label: pick(options?.label, "smoke"),
|
||||||
|
route: pick(options?.route, "workspace-smoke"),
|
||||||
|
vignette: Math.max(0, num(options?.vignette, 0.35)),
|
||||||
|
keybinds: rec(options?.keybinds),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const names = (input: Cfg) => {
|
||||||
|
return {
|
||||||
|
modal: `${input.route}.modal`,
|
||||||
|
screen: `${input.route}.screen`,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type Keys = TuiKeybindSet
|
||||||
|
const ui = {
|
||||||
|
panel: "#1d1d1d",
|
||||||
|
border: "#4a4a4a",
|
||||||
|
text: "#f0f0f0",
|
||||||
|
muted: "#a5a5a5",
|
||||||
|
accent: "#5f87ff",
|
||||||
|
}
|
||||||
|
|
||||||
|
type Color = RGBA | string
|
||||||
|
|
||||||
|
const ink = (map: Record<string, unknown>, name: string, fallback: string): Color => {
|
||||||
|
const value = map[name]
|
||||||
|
if (typeof value === "string") return value
|
||||||
|
if (value instanceof RGBA) return value
|
||||||
|
return fallback
|
||||||
|
}
|
||||||
|
|
||||||
|
const look = (map: Record<string, unknown>) => {
|
||||||
|
return {
|
||||||
|
panel: ink(map, "backgroundPanel", ui.panel),
|
||||||
|
border: ink(map, "border", ui.border),
|
||||||
|
text: ink(map, "text", ui.text),
|
||||||
|
muted: ink(map, "textMuted", ui.muted),
|
||||||
|
accent: ink(map, "primary", ui.accent),
|
||||||
|
selected: ink(map, "selectedListItemText", ui.text),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const tone = (api: TuiPluginApi) => {
|
||||||
|
return look(api.theme.current)
|
||||||
|
}
|
||||||
|
|
||||||
|
type Skin = {
|
||||||
|
panel: Color
|
||||||
|
border: Color
|
||||||
|
text: Color
|
||||||
|
muted: Color
|
||||||
|
accent: Color
|
||||||
|
selected: Color
|
||||||
|
}
|
||||||
|
|
||||||
|
const Btn = (props: { txt: string; run: () => void; skin: Skin; on?: boolean }) => {
|
||||||
|
return (
|
||||||
|
<box
|
||||||
|
onMouseUp={() => {
|
||||||
|
props.run()
|
||||||
|
}}
|
||||||
|
backgroundColor={props.on ? props.skin.accent : props.skin.border}
|
||||||
|
paddingLeft={1}
|
||||||
|
paddingRight={1}
|
||||||
|
>
|
||||||
|
<text fg={props.on ? props.skin.selected : props.skin.text}>{props.txt}</text>
|
||||||
|
</box>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const parse = (params: Record<string, unknown> | undefined) => {
|
||||||
|
const tab = typeof params?.tab === "number" ? params.tab : 0
|
||||||
|
const count = typeof params?.count === "number" ? params.count : 0
|
||||||
|
const source = typeof params?.source === "string" ? params.source : "unknown"
|
||||||
|
const note = typeof params?.note === "string" ? params.note : ""
|
||||||
|
const selected = typeof params?.selected === "string" ? params.selected : ""
|
||||||
|
const local = typeof params?.local === "number" ? params.local : 0
|
||||||
|
return {
|
||||||
|
tab: Math.max(0, Math.min(tab, tabs.length - 1)),
|
||||||
|
count,
|
||||||
|
source,
|
||||||
|
note,
|
||||||
|
selected,
|
||||||
|
local: Math.max(0, local),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const current = (api: TuiPluginApi, route: Route) => {
|
||||||
|
const value = api.route.current
|
||||||
|
const ok = Object.values(route).includes(value.name)
|
||||||
|
if (!ok) return parse(undefined)
|
||||||
|
if (!("params" in value)) return parse(undefined)
|
||||||
|
return parse(value.params)
|
||||||
|
}
|
||||||
|
|
||||||
|
const opts = [
|
||||||
|
{
|
||||||
|
title: "Overview",
|
||||||
|
value: 0,
|
||||||
|
description: "Switch to overview tab",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Counter",
|
||||||
|
value: 1,
|
||||||
|
description: "Switch to counter tab",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Help",
|
||||||
|
value: 2,
|
||||||
|
description: "Switch to help tab",
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
const host = (api: TuiPluginApi, input: Cfg, skin: Skin) => {
|
||||||
|
api.ui.dialog.setSize("medium")
|
||||||
|
api.ui.dialog.replace(() => (
|
||||||
|
<box paddingBottom={1} paddingLeft={2} paddingRight={2} gap={1} flexDirection="column">
|
||||||
|
<text fg={skin.text}>
|
||||||
|
<b>{input.label} host overlay</b>
|
||||||
|
</text>
|
||||||
|
<text fg={skin.muted}>Using api.ui.dialog stack with built-in backdrop</text>
|
||||||
|
<text fg={skin.muted}>esc closes · depth {api.ui.dialog.depth}</text>
|
||||||
|
<box flexDirection="row" gap={1}>
|
||||||
|
<Btn txt="close" run={() => api.ui.dialog.clear()} skin={skin} on />
|
||||||
|
</box>
|
||||||
|
</box>
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
const warn = (api: TuiPluginApi, route: Route, value: State) => {
|
||||||
|
const DialogAlert = api.ui.DialogAlert
|
||||||
|
api.ui.dialog.setSize("medium")
|
||||||
|
api.ui.dialog.replace(() => (
|
||||||
|
<DialogAlert
|
||||||
|
title="Smoke alert"
|
||||||
|
message="Testing built-in alert dialog"
|
||||||
|
onConfirm={() => api.route.navigate(route.screen, { ...value, source: "alert" })}
|
||||||
|
/>
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
const check = (api: TuiPluginApi, route: Route, value: State) => {
|
||||||
|
const DialogConfirm = api.ui.DialogConfirm
|
||||||
|
api.ui.dialog.setSize("medium")
|
||||||
|
api.ui.dialog.replace(() => (
|
||||||
|
<DialogConfirm
|
||||||
|
title="Smoke confirm"
|
||||||
|
message="Apply +1 to counter?"
|
||||||
|
onConfirm={() => api.route.navigate(route.screen, { ...value, count: value.count + 1, source: "confirm" })}
|
||||||
|
onCancel={() => api.route.navigate(route.screen, { ...value, source: "confirm-cancel" })}
|
||||||
|
/>
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
const entry = (api: TuiPluginApi, route: Route, value: State) => {
|
||||||
|
const DialogPrompt = api.ui.DialogPrompt
|
||||||
|
api.ui.dialog.setSize("medium")
|
||||||
|
api.ui.dialog.replace(() => (
|
||||||
|
<DialogPrompt
|
||||||
|
title="Smoke prompt"
|
||||||
|
value={value.note}
|
||||||
|
onConfirm={(note) => {
|
||||||
|
api.ui.dialog.clear()
|
||||||
|
api.route.navigate(route.screen, { ...value, note, source: "prompt" })
|
||||||
|
}}
|
||||||
|
onCancel={() => {
|
||||||
|
api.ui.dialog.clear()
|
||||||
|
api.route.navigate(route.screen, value)
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
const picker = (api: TuiPluginApi, route: Route, value: State) => {
|
||||||
|
const DialogSelect = api.ui.DialogSelect
|
||||||
|
api.ui.dialog.setSize("medium")
|
||||||
|
api.ui.dialog.replace(() => (
|
||||||
|
<DialogSelect
|
||||||
|
title="Smoke select"
|
||||||
|
options={opts}
|
||||||
|
current={value.tab}
|
||||||
|
onSelect={(item) => {
|
||||||
|
api.ui.dialog.clear()
|
||||||
|
api.route.navigate(route.screen, {
|
||||||
|
...value,
|
||||||
|
tab: typeof item.value === "number" ? item.value : value.tab,
|
||||||
|
selected: item.title,
|
||||||
|
source: "select",
|
||||||
|
})
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
const Screen = (props: {
|
||||||
|
api: TuiPluginApi
|
||||||
|
input: Cfg
|
||||||
|
route: Route
|
||||||
|
keys: Keys
|
||||||
|
meta: TuiPluginMeta
|
||||||
|
params?: Record<string, unknown>
|
||||||
|
}) => {
|
||||||
|
const dim = useTerminalDimensions()
|
||||||
|
const value = parse(props.params)
|
||||||
|
const skin = tone(props.api)
|
||||||
|
const set = (local: number, base?: State) => {
|
||||||
|
const next = base ?? current(props.api, props.route)
|
||||||
|
props.api.route.navigate(props.route.screen, { ...next, local: Math.max(0, local), source: "local" })
|
||||||
|
}
|
||||||
|
const push = (base?: State) => {
|
||||||
|
const next = base ?? current(props.api, props.route)
|
||||||
|
set(next.local + 1, next)
|
||||||
|
}
|
||||||
|
const open = () => {
|
||||||
|
const next = current(props.api, props.route)
|
||||||
|
if (next.local > 0) return
|
||||||
|
set(1, next)
|
||||||
|
}
|
||||||
|
const pop = (base?: State) => {
|
||||||
|
const next = base ?? current(props.api, props.route)
|
||||||
|
const local = Math.max(0, next.local - 1)
|
||||||
|
set(local, next)
|
||||||
|
}
|
||||||
|
const show = () => {
|
||||||
|
setTimeout(() => {
|
||||||
|
open()
|
||||||
|
}, 0)
|
||||||
|
}
|
||||||
|
useKeyboard((evt) => {
|
||||||
|
if (props.api.route.current.name !== props.route.screen) return
|
||||||
|
const next = current(props.api, props.route)
|
||||||
|
if (props.api.ui.dialog.open) {
|
||||||
|
if (props.keys.match("dialog_close", evt)) {
|
||||||
|
evt.preventDefault()
|
||||||
|
evt.stopPropagation()
|
||||||
|
props.api.ui.dialog.clear()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (next.local > 0) {
|
||||||
|
if (evt.name === "escape" || props.keys.match("local_close", evt)) {
|
||||||
|
evt.preventDefault()
|
||||||
|
evt.stopPropagation()
|
||||||
|
pop(next)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (props.keys.match("local_push", evt)) {
|
||||||
|
evt.preventDefault()
|
||||||
|
evt.stopPropagation()
|
||||||
|
push(next)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (props.keys.match("home", evt)) {
|
||||||
|
evt.preventDefault()
|
||||||
|
evt.stopPropagation()
|
||||||
|
props.api.route.navigate("home")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (props.keys.match("left", evt)) {
|
||||||
|
evt.preventDefault()
|
||||||
|
evt.stopPropagation()
|
||||||
|
props.api.route.navigate(props.route.screen, { ...next, tab: (next.tab - 1 + tabs.length) % tabs.length })
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (props.keys.match("right", evt)) {
|
||||||
|
evt.preventDefault()
|
||||||
|
evt.stopPropagation()
|
||||||
|
props.api.route.navigate(props.route.screen, { ...next, tab: (next.tab + 1) % tabs.length })
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (props.keys.match("up", evt)) {
|
||||||
|
evt.preventDefault()
|
||||||
|
evt.stopPropagation()
|
||||||
|
props.api.route.navigate(props.route.screen, { ...next, count: next.count + 1 })
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (props.keys.match("down", evt)) {
|
||||||
|
evt.preventDefault()
|
||||||
|
evt.stopPropagation()
|
||||||
|
props.api.route.navigate(props.route.screen, { ...next, count: next.count - 1 })
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (props.keys.match("modal", evt)) {
|
||||||
|
evt.preventDefault()
|
||||||
|
evt.stopPropagation()
|
||||||
|
props.api.route.navigate(props.route.modal, next)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (props.keys.match("local", evt)) {
|
||||||
|
evt.preventDefault()
|
||||||
|
evt.stopPropagation()
|
||||||
|
open()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (props.keys.match("host", evt)) {
|
||||||
|
evt.preventDefault()
|
||||||
|
evt.stopPropagation()
|
||||||
|
host(props.api, props.input, skin)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (props.keys.match("alert", evt)) {
|
||||||
|
evt.preventDefault()
|
||||||
|
evt.stopPropagation()
|
||||||
|
warn(props.api, props.route, next)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (props.keys.match("confirm", evt)) {
|
||||||
|
evt.preventDefault()
|
||||||
|
evt.stopPropagation()
|
||||||
|
check(props.api, props.route, next)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (props.keys.match("prompt", evt)) {
|
||||||
|
evt.preventDefault()
|
||||||
|
evt.stopPropagation()
|
||||||
|
entry(props.api, props.route, next)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (props.keys.match("select", evt)) {
|
||||||
|
evt.preventDefault()
|
||||||
|
evt.stopPropagation()
|
||||||
|
picker(props.api, props.route, next)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
return (
|
||||||
|
<box width={dim().width} height={dim().height} backgroundColor={skin.panel} position="relative">
|
||||||
|
<box
|
||||||
|
flexDirection="column"
|
||||||
|
width="100%"
|
||||||
|
height="100%"
|
||||||
|
paddingTop={1}
|
||||||
|
paddingBottom={1}
|
||||||
|
paddingLeft={2}
|
||||||
|
paddingRight={2}
|
||||||
|
>
|
||||||
|
<box flexDirection="row" justifyContent="space-between" paddingBottom={1}>
|
||||||
|
<text fg={skin.text}>
|
||||||
|
<b>{props.input.label} screen</b>
|
||||||
|
<span style={{ fg: skin.muted }}> plugin route</span>
|
||||||
|
</text>
|
||||||
|
<text fg={skin.muted}>{props.keys.print("home")} home</text>
|
||||||
|
</box>
|
||||||
|
|
||||||
|
<box flexDirection="row" gap={1} paddingBottom={1}>
|
||||||
|
{tabs.map((item, i) => {
|
||||||
|
const on = value.tab === i
|
||||||
|
return (
|
||||||
|
<Btn
|
||||||
|
txt={item}
|
||||||
|
run={() => props.api.route.navigate(props.route.screen, { ...value, tab: i })}
|
||||||
|
skin={skin}
|
||||||
|
on={on}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
</box>
|
||||||
|
|
||||||
|
<box
|
||||||
|
border
|
||||||
|
borderColor={skin.border}
|
||||||
|
paddingTop={1}
|
||||||
|
paddingBottom={1}
|
||||||
|
paddingLeft={2}
|
||||||
|
paddingRight={2}
|
||||||
|
flexGrow={1}
|
||||||
|
>
|
||||||
|
{value.tab === 0 ? (
|
||||||
|
<box flexDirection="column" gap={1}>
|
||||||
|
<text fg={skin.text}>Route: {props.route.screen}</text>
|
||||||
|
<text fg={skin.muted}>plugin state: {props.meta.state}</text>
|
||||||
|
<text fg={skin.muted}>
|
||||||
|
first: {props.meta.state === "first" ? "yes" : "no"} · updated:{" "}
|
||||||
|
{props.meta.state === "updated" ? "yes" : "no"} · loads: {props.meta.load_count}
|
||||||
|
</text>
|
||||||
|
<text fg={skin.muted}>plugin source: {props.meta.source}</text>
|
||||||
|
<text fg={skin.muted}>source: {value.source}</text>
|
||||||
|
<text fg={skin.muted}>note: {value.note || "(none)"}</text>
|
||||||
|
<text fg={skin.muted}>selected: {value.selected || "(none)"}</text>
|
||||||
|
<text fg={skin.muted}>local stack depth: {value.local}</text>
|
||||||
|
<text fg={skin.muted}>host stack open: {props.api.ui.dialog.open ? "yes" : "no"}</text>
|
||||||
|
</box>
|
||||||
|
) : null}
|
||||||
|
|
||||||
|
{value.tab === 1 ? (
|
||||||
|
<box flexDirection="column" gap={1}>
|
||||||
|
<text fg={skin.text}>Counter: {value.count}</text>
|
||||||
|
<text fg={skin.muted}>
|
||||||
|
{props.keys.print("up")} / {props.keys.print("down")} change value
|
||||||
|
</text>
|
||||||
|
</box>
|
||||||
|
) : null}
|
||||||
|
|
||||||
|
{value.tab === 2 ? (
|
||||||
|
<box flexDirection="column" gap={1}>
|
||||||
|
<text fg={skin.muted}>
|
||||||
|
{props.keys.print("modal")} modal | {props.keys.print("alert")} alert | {props.keys.print("confirm")}{" "}
|
||||||
|
confirm | {props.keys.print("prompt")} prompt | {props.keys.print("select")} select
|
||||||
|
</text>
|
||||||
|
<text fg={skin.muted}>
|
||||||
|
{props.keys.print("local")} local stack | {props.keys.print("host")} host stack
|
||||||
|
</text>
|
||||||
|
<text fg={skin.muted}>
|
||||||
|
local open: {props.keys.print("local_push")} push nested · esc or {props.keys.print("local_close")}{" "}
|
||||||
|
close
|
||||||
|
</text>
|
||||||
|
<text fg={skin.muted}>{props.keys.print("home")} returns home</text>
|
||||||
|
</box>
|
||||||
|
) : null}
|
||||||
|
</box>
|
||||||
|
|
||||||
|
<box flexDirection="row" gap={1} paddingTop={1}>
|
||||||
|
<Btn txt="go home" run={() => props.api.route.navigate("home")} skin={skin} />
|
||||||
|
<Btn txt="modal" run={() => props.api.route.navigate(props.route.modal, value)} skin={skin} on />
|
||||||
|
<Btn txt="local overlay" run={show} skin={skin} />
|
||||||
|
<Btn txt="host overlay" run={() => host(props.api, props.input, skin)} skin={skin} />
|
||||||
|
<Btn txt="alert" run={() => warn(props.api, props.route, value)} skin={skin} />
|
||||||
|
<Btn txt="confirm" run={() => check(props.api, props.route, value)} skin={skin} />
|
||||||
|
<Btn txt="prompt" run={() => entry(props.api, props.route, value)} skin={skin} />
|
||||||
|
<Btn txt="select" run={() => picker(props.api, props.route, value)} skin={skin} />
|
||||||
|
</box>
|
||||||
|
</box>
|
||||||
|
|
||||||
|
<box
|
||||||
|
visible={value.local > 0}
|
||||||
|
width={dim().width}
|
||||||
|
height={dim().height}
|
||||||
|
alignItems="center"
|
||||||
|
position="absolute"
|
||||||
|
zIndex={3000}
|
||||||
|
paddingTop={dim().height / 4}
|
||||||
|
left={0}
|
||||||
|
top={0}
|
||||||
|
backgroundColor={RGBA.fromInts(0, 0, 0, 160)}
|
||||||
|
onMouseUp={() => {
|
||||||
|
pop()
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<box
|
||||||
|
onMouseUp={(evt) => {
|
||||||
|
evt.stopPropagation()
|
||||||
|
}}
|
||||||
|
width={60}
|
||||||
|
maxWidth={dim().width - 2}
|
||||||
|
backgroundColor={skin.panel}
|
||||||
|
border
|
||||||
|
borderColor={skin.border}
|
||||||
|
paddingTop={1}
|
||||||
|
paddingBottom={1}
|
||||||
|
paddingLeft={2}
|
||||||
|
paddingRight={2}
|
||||||
|
gap={1}
|
||||||
|
flexDirection="column"
|
||||||
|
>
|
||||||
|
<text fg={skin.text}>
|
||||||
|
<b>{props.input.label} local overlay</b>
|
||||||
|
</text>
|
||||||
|
<text fg={skin.muted}>Plugin-owned stack depth: {value.local}</text>
|
||||||
|
<text fg={skin.muted}>
|
||||||
|
{props.keys.print("local_push")} push nested · {props.keys.print("local_close")} pop/close
|
||||||
|
</text>
|
||||||
|
<box flexDirection="row" gap={1}>
|
||||||
|
<Btn txt="push" run={push} skin={skin} on />
|
||||||
|
<Btn txt="pop" run={pop} skin={skin} />
|
||||||
|
</box>
|
||||||
|
</box>
|
||||||
|
</box>
|
||||||
|
</box>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const Modal = (props: {
|
||||||
|
api: TuiPluginApi
|
||||||
|
input: Cfg
|
||||||
|
route: Route
|
||||||
|
keys: Keys
|
||||||
|
params?: Record<string, unknown>
|
||||||
|
}) => {
|
||||||
|
const Dialog = props.api.ui.Dialog
|
||||||
|
const value = parse(props.params)
|
||||||
|
const skin = tone(props.api)
|
||||||
|
|
||||||
|
useKeyboard((evt) => {
|
||||||
|
if (props.api.route.current.name !== props.route.modal) return
|
||||||
|
|
||||||
|
if (props.keys.match("modal_accept", evt)) {
|
||||||
|
evt.preventDefault()
|
||||||
|
evt.stopPropagation()
|
||||||
|
props.api.route.navigate(props.route.screen, { ...value, source: "modal" })
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (props.keys.match("modal_close", evt)) {
|
||||||
|
evt.preventDefault()
|
||||||
|
evt.stopPropagation()
|
||||||
|
props.api.route.navigate("home")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
return (
|
||||||
|
<box width="100%" height="100%" backgroundColor={skin.panel}>
|
||||||
|
<Dialog onClose={() => props.api.route.navigate("home")}>
|
||||||
|
<box paddingBottom={1} paddingLeft={2} paddingRight={2} gap={1} flexDirection="column">
|
||||||
|
<text fg={skin.text}>
|
||||||
|
<b>{props.input.label} modal</b>
|
||||||
|
</text>
|
||||||
|
<text fg={skin.muted}>{props.keys.print("modal")} modal command</text>
|
||||||
|
<text fg={skin.muted}>{props.keys.print("screen")} screen command</text>
|
||||||
|
<text fg={skin.muted}>
|
||||||
|
{props.keys.print("modal_accept")} opens screen · {props.keys.print("modal_close")} closes
|
||||||
|
</text>
|
||||||
|
<box flexDirection="row" gap={1}>
|
||||||
|
<Btn
|
||||||
|
txt="open screen"
|
||||||
|
run={() => props.api.route.navigate(props.route.screen, { ...value, source: "modal" })}
|
||||||
|
skin={skin}
|
||||||
|
on
|
||||||
|
/>
|
||||||
|
<Btn txt="cancel" run={() => props.api.route.navigate("home")} skin={skin} />
|
||||||
|
</box>
|
||||||
|
</box>
|
||||||
|
</Dialog>
|
||||||
|
</box>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const home = (api: TuiPluginApi, input: Cfg) => ({
|
||||||
|
slots: {
|
||||||
|
home_logo(ctx) {
|
||||||
|
const map = ctx.theme.current
|
||||||
|
const skin = look(map)
|
||||||
|
const art = [
|
||||||
|
" $$\\",
|
||||||
|
" $$ |",
|
||||||
|
" $$$$$$$\\ $$$$$$\\$$$$\\ $$$$$$\\ $$ | $$\\ $$$$$$\\",
|
||||||
|
"$$ _____|$$ _$$ _$$\\ $$ __$$\\ $$ | $$ |$$ __$$\\",
|
||||||
|
"\\$$$$$$\\ $$ / $$ / $$ |$$ / $$ |$$$$$$ / $$$$$$$$ |",
|
||||||
|
" \\____$$\\ $$ | $$ | $$ |$$ | $$ |$$ _$$< $$ ____|",
|
||||||
|
"$$$$$$$ |$$ | $$ | $$ |\\$$$$$$ |$$ | \\$$\\ \\$$$$$$$\\",
|
||||||
|
"\\_______/ \\__| \\__| \\__| \\______/ \\__| \\__| \\_______|",
|
||||||
|
]
|
||||||
|
const fill = [
|
||||||
|
skin.accent,
|
||||||
|
skin.muted,
|
||||||
|
ink(map, "info", ui.accent),
|
||||||
|
skin.text,
|
||||||
|
ink(map, "success", ui.accent),
|
||||||
|
ink(map, "warning", ui.accent),
|
||||||
|
ink(map, "secondary", ui.accent),
|
||||||
|
ink(map, "error", ui.accent),
|
||||||
|
]
|
||||||
|
|
||||||
|
return (
|
||||||
|
<box flexDirection="column">
|
||||||
|
{art.map((line, i) => (
|
||||||
|
<text fg={fill[i]}>{line}</text>
|
||||||
|
))}
|
||||||
|
</box>
|
||||||
|
)
|
||||||
|
},
|
||||||
|
home_prompt(ctx, value) {
|
||||||
|
const skin = look(ctx.theme.current)
|
||||||
|
type Prompt = (props: {
|
||||||
|
workspaceID?: string
|
||||||
|
hint?: JSX.Element
|
||||||
|
placeholders?: {
|
||||||
|
normal?: string[]
|
||||||
|
shell?: string[]
|
||||||
|
}
|
||||||
|
}) => JSX.Element
|
||||||
|
if (!("Prompt" in api.ui)) return null
|
||||||
|
const view = api.ui.Prompt
|
||||||
|
if (typeof view !== "function") return null
|
||||||
|
const Prompt = view as Prompt
|
||||||
|
const normal = [
|
||||||
|
`[SMOKE] route check for ${input.label}`,
|
||||||
|
"[SMOKE] confirm home_prompt slot override",
|
||||||
|
"[SMOKE] verify api.ui.Prompt rendering",
|
||||||
|
]
|
||||||
|
const shell = ["printf '[SMOKE] home prompt\n'", "git status --short", "bun --version"]
|
||||||
|
const Hint = (
|
||||||
|
<box flexShrink={0} flexDirection="row" gap={1}>
|
||||||
|
<text fg={skin.muted}>
|
||||||
|
<span style={{ fg: skin.accent }}>•</span> smoke home prompt
|
||||||
|
</text>
|
||||||
|
</box>
|
||||||
|
)
|
||||||
|
|
||||||
|
return <Prompt workspaceID={value.workspace_id} hint={Hint} placeholders={{ normal, shell }} />
|
||||||
|
},
|
||||||
|
home_bottom(ctx) {
|
||||||
|
const skin = look(ctx.theme.current)
|
||||||
|
const text = "extra content in the unified home bottom slot"
|
||||||
|
|
||||||
|
return (
|
||||||
|
<box width="100%" maxWidth={75} alignItems="center" paddingTop={1} flexShrink={0} gap={1}>
|
||||||
|
<box
|
||||||
|
border
|
||||||
|
borderColor={skin.border}
|
||||||
|
backgroundColor={skin.panel}
|
||||||
|
paddingTop={1}
|
||||||
|
paddingBottom={1}
|
||||||
|
paddingLeft={2}
|
||||||
|
paddingRight={2}
|
||||||
|
width="100%"
|
||||||
|
>
|
||||||
|
<text fg={skin.muted}>
|
||||||
|
<span style={{ fg: skin.accent }}>{input.label}</span> {text}
|
||||||
|
</text>
|
||||||
|
</box>
|
||||||
|
</box>
|
||||||
|
)
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
const block = (input: Cfg, order: number, title: string, text: string): TuiSlotPlugin => ({
|
||||||
|
order,
|
||||||
|
slots: {
|
||||||
|
sidebar_content(ctx, value) {
|
||||||
|
const skin = look(ctx.theme.current)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<box
|
||||||
|
border
|
||||||
|
borderColor={skin.border}
|
||||||
|
backgroundColor={skin.panel}
|
||||||
|
paddingTop={1}
|
||||||
|
paddingBottom={1}
|
||||||
|
paddingLeft={2}
|
||||||
|
paddingRight={2}
|
||||||
|
flexDirection="column"
|
||||||
|
gap={1}
|
||||||
|
>
|
||||||
|
<text fg={skin.accent}>
|
||||||
|
<b>{title}</b>
|
||||||
|
</text>
|
||||||
|
<text fg={skin.text}>{text}</text>
|
||||||
|
<text fg={skin.muted}>
|
||||||
|
{input.label} order {order} · session {value.session_id.slice(0, 8)}
|
||||||
|
</text>
|
||||||
|
</box>
|
||||||
|
)
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
const slot = (api: TuiPluginApi, input: Cfg): TuiSlotPlugin[] => [
|
||||||
|
home(api, input),
|
||||||
|
block(input, 50, "Smoke above", "renders above internal sidebar blocks"),
|
||||||
|
block(input, 250, "Smoke between", "renders between internal sidebar blocks"),
|
||||||
|
block(input, 650, "Smoke below", "renders below internal sidebar blocks"),
|
||||||
|
]
|
||||||
|
|
||||||
|
const reg = (api: TuiPluginApi, input: Cfg, keys: Keys) => {
|
||||||
|
const route = names(input)
|
||||||
|
api.command.register(() => [
|
||||||
|
{
|
||||||
|
title: `${input.label} modal`,
|
||||||
|
value: "plugin.smoke.modal",
|
||||||
|
keybind: keys.get("modal"),
|
||||||
|
category: "Plugin",
|
||||||
|
slash: {
|
||||||
|
name: "smoke",
|
||||||
|
},
|
||||||
|
onSelect: () => {
|
||||||
|
api.route.navigate(route.modal, { source: "command" })
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: `${input.label} screen`,
|
||||||
|
value: "plugin.smoke.screen",
|
||||||
|
keybind: keys.get("screen"),
|
||||||
|
category: "Plugin",
|
||||||
|
slash: {
|
||||||
|
name: "smoke-screen",
|
||||||
|
},
|
||||||
|
onSelect: () => {
|
||||||
|
api.route.navigate(route.screen, { source: "command", tab: 0, count: 0 })
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: `${input.label} alert dialog`,
|
||||||
|
value: "plugin.smoke.alert",
|
||||||
|
category: "Plugin",
|
||||||
|
slash: {
|
||||||
|
name: "smoke-alert",
|
||||||
|
},
|
||||||
|
onSelect: () => {
|
||||||
|
warn(api, route, current(api, route))
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: `${input.label} confirm dialog`,
|
||||||
|
value: "plugin.smoke.confirm",
|
||||||
|
category: "Plugin",
|
||||||
|
slash: {
|
||||||
|
name: "smoke-confirm",
|
||||||
|
},
|
||||||
|
onSelect: () => {
|
||||||
|
check(api, route, current(api, route))
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: `${input.label} prompt dialog`,
|
||||||
|
value: "plugin.smoke.prompt",
|
||||||
|
category: "Plugin",
|
||||||
|
slash: {
|
||||||
|
name: "smoke-prompt",
|
||||||
|
},
|
||||||
|
onSelect: () => {
|
||||||
|
entry(api, route, current(api, route))
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: `${input.label} select dialog`,
|
||||||
|
value: "plugin.smoke.select",
|
||||||
|
category: "Plugin",
|
||||||
|
slash: {
|
||||||
|
name: "smoke-select",
|
||||||
|
},
|
||||||
|
onSelect: () => {
|
||||||
|
picker(api, route, current(api, route))
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: `${input.label} host overlay`,
|
||||||
|
value: "plugin.smoke.host",
|
||||||
|
category: "Plugin",
|
||||||
|
slash: {
|
||||||
|
name: "smoke-host",
|
||||||
|
},
|
||||||
|
onSelect: () => {
|
||||||
|
host(api, input, tone(api))
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: `${input.label} go home`,
|
||||||
|
value: "plugin.smoke.home",
|
||||||
|
category: "Plugin",
|
||||||
|
enabled: api.route.current.name !== "home",
|
||||||
|
onSelect: () => {
|
||||||
|
api.route.navigate("home")
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: `${input.label} toast`,
|
||||||
|
value: "plugin.smoke.toast",
|
||||||
|
category: "Plugin",
|
||||||
|
onSelect: () => {
|
||||||
|
api.ui.toast({
|
||||||
|
variant: "info",
|
||||||
|
title: "Smoke",
|
||||||
|
message: "Plugin toast works",
|
||||||
|
duration: 2000,
|
||||||
|
})
|
||||||
|
},
|
||||||
|
},
|
||||||
|
])
|
||||||
|
}
|
||||||
|
|
||||||
|
const tui: TuiPlugin = async (api, options, meta) => {
|
||||||
|
if (options?.enabled === false) return
|
||||||
|
|
||||||
|
await api.theme.install("./smoke-theme.json")
|
||||||
|
api.theme.set("smoke-theme")
|
||||||
|
|
||||||
|
const value = cfg(options ?? undefined)
|
||||||
|
const route = names(value)
|
||||||
|
const keys = api.keybind.create(bind, value.keybinds)
|
||||||
|
const fx = new VignetteEffect(value.vignette)
|
||||||
|
const post = fx.apply.bind(fx)
|
||||||
|
api.renderer.addPostProcessFn(post)
|
||||||
|
api.lifecycle.onDispose(() => {
|
||||||
|
api.renderer.removePostProcessFn(post)
|
||||||
|
})
|
||||||
|
|
||||||
|
api.route.register([
|
||||||
|
{
|
||||||
|
name: route.screen,
|
||||||
|
render: ({ params }) => <Screen api={api} input={value} route={route} keys={keys} meta={meta} params={params} />,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: route.modal,
|
||||||
|
render: ({ params }) => <Modal api={api} input={value} route={route} keys={keys} params={params} />,
|
||||||
|
},
|
||||||
|
])
|
||||||
|
|
||||||
|
reg(api, value, keys)
|
||||||
|
for (const item of slot(api, value)) {
|
||||||
|
api.slots.register(item)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const plugin: TuiPluginModule & { id: string } = {
|
||||||
|
id: "tui-smoke",
|
||||||
|
tui,
|
||||||
|
}
|
||||||
|
|
||||||
|
export default plugin
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
smoke-theme.json
|
||||||
@@ -1,7 +1,5 @@
|
|||||||
/// <reference path="../env.d.ts" />
|
/// <reference path="../env.d.ts" />
|
||||||
import { tool } from "@opencode-ai/plugin"
|
import { tool } from "@opencode-ai/plugin"
|
||||||
import DESCRIPTION from "./github-pr-search.txt"
|
|
||||||
|
|
||||||
async function githubFetch(endpoint: string, options: RequestInit = {}) {
|
async function githubFetch(endpoint: string, options: RequestInit = {}) {
|
||||||
const response = await fetch(`https://api.github.com${endpoint}`, {
|
const response = await fetch(`https://api.github.com${endpoint}`, {
|
||||||
...options,
|
...options,
|
||||||
@@ -24,7 +22,16 @@ interface PR {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export default tool({
|
export default tool({
|
||||||
description: DESCRIPTION,
|
description: `Use this tool to search GitHub pull requests by title and description.
|
||||||
|
|
||||||
|
This tool searches PRs in the anomalyco/opencode repository and returns LLM-friendly results including:
|
||||||
|
- PR number and title
|
||||||
|
- Author
|
||||||
|
- State (open/closed/merged)
|
||||||
|
- Labels
|
||||||
|
- Description snippet
|
||||||
|
|
||||||
|
Use the query parameter to search for keywords that might appear in PR titles or descriptions.`,
|
||||||
args: {
|
args: {
|
||||||
query: tool.schema.string().describe("Search query for PR titles and descriptions"),
|
query: tool.schema.string().describe("Search query for PR titles and descriptions"),
|
||||||
limit: tool.schema.number().describe("Maximum number of results to return").default(10),
|
limit: tool.schema.number().describe("Maximum number of results to return").default(10),
|
||||||
|
|||||||
@@ -1,10 +0,0 @@
|
|||||||
Use this tool to search GitHub pull requests by title and description.
|
|
||||||
|
|
||||||
This tool searches PRs in the anomalyco/opencode repository and returns LLM-friendly results including:
|
|
||||||
- PR number and title
|
|
||||||
- Author
|
|
||||||
- State (open/closed/merged)
|
|
||||||
- Labels
|
|
||||||
- Description snippet
|
|
||||||
|
|
||||||
Use the query parameter to search for keywords that might appear in PR titles or descriptions.
|
|
||||||
@@ -1,7 +1,5 @@
|
|||||||
/// <reference path="../env.d.ts" />
|
/// <reference path="../env.d.ts" />
|
||||||
import { tool } from "@opencode-ai/plugin"
|
import { tool } from "@opencode-ai/plugin"
|
||||||
import DESCRIPTION from "./github-triage.txt"
|
|
||||||
|
|
||||||
const TEAM = {
|
const TEAM = {
|
||||||
desktop: ["adamdotdevin", "iamdavidhill", "Brendonovich", "nexxeln"],
|
desktop: ["adamdotdevin", "iamdavidhill", "Brendonovich", "nexxeln"],
|
||||||
zen: ["fwang", "MrMushrooooom"],
|
zen: ["fwang", "MrMushrooooom"],
|
||||||
@@ -40,7 +38,12 @@ async function githubFetch(endpoint: string, options: RequestInit = {}) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export default tool({
|
export default tool({
|
||||||
description: DESCRIPTION,
|
description: `Use this tool to assign and/or label a GitHub issue.
|
||||||
|
|
||||||
|
Choose labels and assignee using the current triage policy and ownership rules.
|
||||||
|
Pick the most fitting labels for the issue and assign one owner.
|
||||||
|
|
||||||
|
If unsure, choose the team/section with the most overlap with the issue and assign a member from that team at random.`,
|
||||||
args: {
|
args: {
|
||||||
assignee: tool.schema
|
assignee: tool.schema
|
||||||
.enum(ASSIGNEES as [string, ...string[]])
|
.enum(ASSIGNEES as [string, ...string[]])
|
||||||
|
|||||||
@@ -1,6 +0,0 @@
|
|||||||
Use this tool to assign and/or label a GitHub issue.
|
|
||||||
|
|
||||||
Choose labels and assignee using the current triage policy and ownership rules.
|
|
||||||
Pick the most fitting labels for the issue and assign one owner.
|
|
||||||
|
|
||||||
If unsure, choose the team/section with the most overlap with the issue and assign a member from that team at random.
|
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
{
|
||||||
|
"$schema": "https://opencode.ai/tui.json",
|
||||||
|
"plugin": [
|
||||||
|
[
|
||||||
|
"./plugins/tui-smoke.tsx",
|
||||||
|
{
|
||||||
|
"enabled": false,
|
||||||
|
"label": "workspace",
|
||||||
|
"keybinds": {
|
||||||
|
"modal": "ctrl+alt+m",
|
||||||
|
"screen": "ctrl+alt+o",
|
||||||
|
"home": "escape,ctrl+shift+h",
|
||||||
|
"dialog_close": "escape,q"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
github-policies:
|
|
||||||
runners:
|
|
||||||
allowed_groups:
|
|
||||||
- "GitHub Actions"
|
|
||||||
- "blacksmith runners 01kbd5v56sg8tz7rea39b7ygpt"
|
|
||||||
+1
-1
@@ -137,4 +137,4 @@ OpenCode 内置两种 Agent,可用 `Tab` 键快速切换:
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
**加入我们的社区** [飞书](https://applink.feishu.cn/client/chat/chatter/add_by_link?link_token=de8k6664-1b5e-43f2-8efd-21d6772647b5&qr_code=true) | [X.com](https://x.com/opencode)
|
**加入我们的社区** [飞书](https://applink.feishu.cn/client/chat/chatter/add_by_link?link_token=738j8655-cd59-4633-a30a-1124e0096789&qr_code=true) | [X.com](https://x.com/opencode)
|
||||||
|
|||||||
+1
-1
@@ -137,4 +137,4 @@ OpenCode 內建了兩種 Agent,您可以使用 `Tab` 鍵快速切換。
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
**加入我們的社群** [飞书](https://applink.feishu.cn/client/chat/chatter/add_by_link?link_token=de8k6664-1b5e-43f2-8efd-21d6772647b5&qr_code=true) | [X.com](https://x.com/opencode)
|
**加入我們的社群** [飞书](https://applink.feishu.cn/client/chat/chatter/add_by_link?link_token=738j8655-cd59-4633-a30a-1124e0096789&qr_code=true) | [X.com](https://x.com/opencode)
|
||||||
|
|||||||
@@ -26,7 +26,7 @@
|
|||||||
},
|
},
|
||||||
"packages/app": {
|
"packages/app": {
|
||||||
"name": "@opencode-ai/app",
|
"name": "@opencode-ai/app",
|
||||||
"version": "1.2.27",
|
"version": "1.3.11",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@kobalte/core": "catalog:",
|
"@kobalte/core": "catalog:",
|
||||||
"@opencode-ai/sdk": "workspace:*",
|
"@opencode-ai/sdk": "workspace:*",
|
||||||
@@ -41,12 +41,14 @@
|
|||||||
"@solid-primitives/resize-observer": "2.1.3",
|
"@solid-primitives/resize-observer": "2.1.3",
|
||||||
"@solid-primitives/scroll": "2.1.3",
|
"@solid-primitives/scroll": "2.1.3",
|
||||||
"@solid-primitives/storage": "catalog:",
|
"@solid-primitives/storage": "catalog:",
|
||||||
|
"@solid-primitives/timer": "1.4.4",
|
||||||
"@solid-primitives/websocket": "1.3.1",
|
"@solid-primitives/websocket": "1.3.1",
|
||||||
"@solidjs/meta": "catalog:",
|
"@solidjs/meta": "catalog:",
|
||||||
"@solidjs/router": "catalog:",
|
"@solidjs/router": "catalog:",
|
||||||
|
"@tanstack/solid-query": "5.91.4",
|
||||||
"@thisbeyond/solid-dnd": "0.7.5",
|
"@thisbeyond/solid-dnd": "0.7.5",
|
||||||
"diff": "catalog:",
|
"diff": "catalog:",
|
||||||
"effect": "4.0.0-beta.31",
|
"effect": "catalog:",
|
||||||
"fuzzysort": "catalog:",
|
"fuzzysort": "catalog:",
|
||||||
"ghostty-web": "github:anomalyco/ghostty-web#main",
|
"ghostty-web": "github:anomalyco/ghostty-web#main",
|
||||||
"luxon": "catalog:",
|
"luxon": "catalog:",
|
||||||
@@ -77,7 +79,7 @@
|
|||||||
},
|
},
|
||||||
"packages/console/app": {
|
"packages/console/app": {
|
||||||
"name": "@opencode-ai/console-app",
|
"name": "@opencode-ai/console-app",
|
||||||
"version": "1.2.27",
|
"version": "1.3.11",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@cloudflare/vite-plugin": "1.15.2",
|
"@cloudflare/vite-plugin": "1.15.2",
|
||||||
"@ibm/plex": "6.4.1",
|
"@ibm/plex": "6.4.1",
|
||||||
@@ -111,7 +113,7 @@
|
|||||||
},
|
},
|
||||||
"packages/console/core": {
|
"packages/console/core": {
|
||||||
"name": "@opencode-ai/console-core",
|
"name": "@opencode-ai/console-core",
|
||||||
"version": "1.2.27",
|
"version": "1.3.11",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@aws-sdk/client-sts": "3.782.0",
|
"@aws-sdk/client-sts": "3.782.0",
|
||||||
"@jsx-email/render": "1.1.1",
|
"@jsx-email/render": "1.1.1",
|
||||||
@@ -128,7 +130,7 @@
|
|||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@cloudflare/workers-types": "catalog:",
|
"@cloudflare/workers-types": "catalog:",
|
||||||
"@tsconfig/node22": "22.0.2",
|
"@tsconfig/node22": "22.0.2",
|
||||||
"@types/bun": "1.3.0",
|
"@types/bun": "catalog:",
|
||||||
"@types/node": "catalog:",
|
"@types/node": "catalog:",
|
||||||
"@typescript/native-preview": "catalog:",
|
"@typescript/native-preview": "catalog:",
|
||||||
"drizzle-kit": "catalog:",
|
"drizzle-kit": "catalog:",
|
||||||
@@ -138,11 +140,11 @@
|
|||||||
},
|
},
|
||||||
"packages/console/function": {
|
"packages/console/function": {
|
||||||
"name": "@opencode-ai/console-function",
|
"name": "@opencode-ai/console-function",
|
||||||
"version": "1.2.27",
|
"version": "1.3.11",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@ai-sdk/anthropic": "2.0.0",
|
"@ai-sdk/anthropic": "3.0.64",
|
||||||
"@ai-sdk/openai": "2.0.2",
|
"@ai-sdk/openai": "3.0.48",
|
||||||
"@ai-sdk/openai-compatible": "1.0.1",
|
"@ai-sdk/openai-compatible": "2.0.37",
|
||||||
"@hono/zod-validator": "catalog:",
|
"@hono/zod-validator": "catalog:",
|
||||||
"@openauthjs/openauth": "0.0.0-20250322224806",
|
"@openauthjs/openauth": "0.0.0-20250322224806",
|
||||||
"@opencode-ai/console-core": "workspace:*",
|
"@opencode-ai/console-core": "workspace:*",
|
||||||
@@ -162,7 +164,7 @@
|
|||||||
},
|
},
|
||||||
"packages/console/mail": {
|
"packages/console/mail": {
|
||||||
"name": "@opencode-ai/console-mail",
|
"name": "@opencode-ai/console-mail",
|
||||||
"version": "1.2.27",
|
"version": "1.3.11",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@jsx-email/all": "2.2.3",
|
"@jsx-email/all": "2.2.3",
|
||||||
"@jsx-email/cli": "1.4.3",
|
"@jsx-email/cli": "1.4.3",
|
||||||
@@ -186,7 +188,7 @@
|
|||||||
},
|
},
|
||||||
"packages/desktop": {
|
"packages/desktop": {
|
||||||
"name": "@opencode-ai/desktop",
|
"name": "@opencode-ai/desktop",
|
||||||
"version": "1.2.27",
|
"version": "1.3.11",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@opencode-ai/app": "workspace:*",
|
"@opencode-ai/app": "workspace:*",
|
||||||
"@opencode-ai/ui": "workspace:*",
|
"@opencode-ai/ui": "workspace:*",
|
||||||
@@ -219,7 +221,7 @@
|
|||||||
},
|
},
|
||||||
"packages/desktop-electron": {
|
"packages/desktop-electron": {
|
||||||
"name": "@opencode-ai/desktop-electron",
|
"name": "@opencode-ai/desktop-electron",
|
||||||
"version": "1.2.27",
|
"version": "1.3.11",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@opencode-ai/app": "workspace:*",
|
"@opencode-ai/app": "workspace:*",
|
||||||
"@opencode-ai/ui": "workspace:*",
|
"@opencode-ai/ui": "workspace:*",
|
||||||
@@ -227,7 +229,7 @@
|
|||||||
"@solid-primitives/storage": "catalog:",
|
"@solid-primitives/storage": "catalog:",
|
||||||
"@solidjs/meta": "catalog:",
|
"@solidjs/meta": "catalog:",
|
||||||
"@solidjs/router": "0.15.4",
|
"@solidjs/router": "0.15.4",
|
||||||
"effect": "4.0.0-beta.31",
|
"effect": "catalog:",
|
||||||
"electron-log": "^5",
|
"electron-log": "^5",
|
||||||
"electron-store": "^10",
|
"electron-store": "^10",
|
||||||
"electron-updater": "^6",
|
"electron-updater": "^6",
|
||||||
@@ -250,7 +252,7 @@
|
|||||||
},
|
},
|
||||||
"packages/enterprise": {
|
"packages/enterprise": {
|
||||||
"name": "@opencode-ai/enterprise",
|
"name": "@opencode-ai/enterprise",
|
||||||
"version": "1.2.27",
|
"version": "1.3.11",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@opencode-ai/ui": "workspace:*",
|
"@opencode-ai/ui": "workspace:*",
|
||||||
"@opencode-ai/util": "workspace:*",
|
"@opencode-ai/util": "workspace:*",
|
||||||
@@ -279,7 +281,7 @@
|
|||||||
},
|
},
|
||||||
"packages/function": {
|
"packages/function": {
|
||||||
"name": "@opencode-ai/function",
|
"name": "@opencode-ai/function",
|
||||||
"version": "1.2.27",
|
"version": "1.3.11",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@octokit/auth-app": "8.0.1",
|
"@octokit/auth-app": "8.0.1",
|
||||||
"@octokit/rest": "catalog:",
|
"@octokit/rest": "catalog:",
|
||||||
@@ -295,7 +297,7 @@
|
|||||||
},
|
},
|
||||||
"packages/opencode": {
|
"packages/opencode": {
|
||||||
"name": "opencode",
|
"name": "opencode",
|
||||||
"version": "1.2.27",
|
"version": "1.3.11",
|
||||||
"bin": {
|
"bin": {
|
||||||
"opencode": "./bin/opencode",
|
"opencode": "./bin/opencode",
|
||||||
},
|
},
|
||||||
@@ -303,33 +305,31 @@
|
|||||||
"@actions/core": "1.11.1",
|
"@actions/core": "1.11.1",
|
||||||
"@actions/github": "6.0.1",
|
"@actions/github": "6.0.1",
|
||||||
"@agentclientprotocol/sdk": "0.14.1",
|
"@agentclientprotocol/sdk": "0.14.1",
|
||||||
"@ai-sdk/amazon-bedrock": "3.0.82",
|
"@ai-sdk/amazon-bedrock": "4.0.83",
|
||||||
"@ai-sdk/anthropic": "2.0.65",
|
"@ai-sdk/anthropic": "3.0.64",
|
||||||
"@ai-sdk/azure": "2.0.91",
|
"@ai-sdk/azure": "3.0.49",
|
||||||
"@ai-sdk/cerebras": "1.0.36",
|
"@ai-sdk/cerebras": "2.0.41",
|
||||||
"@ai-sdk/cohere": "2.0.22",
|
"@ai-sdk/cohere": "3.0.27",
|
||||||
"@ai-sdk/deepinfra": "1.0.36",
|
"@ai-sdk/deepinfra": "2.0.41",
|
||||||
"@ai-sdk/gateway": "2.0.30",
|
"@ai-sdk/gateway": "3.0.80",
|
||||||
"@ai-sdk/google": "2.0.54",
|
"@ai-sdk/google": "3.0.53",
|
||||||
"@ai-sdk/google-vertex": "3.0.106",
|
"@ai-sdk/google-vertex": "4.0.95",
|
||||||
"@ai-sdk/groq": "2.0.34",
|
"@ai-sdk/groq": "3.0.31",
|
||||||
"@ai-sdk/mistral": "2.0.27",
|
"@ai-sdk/mistral": "3.0.27",
|
||||||
"@ai-sdk/openai": "2.0.89",
|
"@ai-sdk/openai": "3.0.48",
|
||||||
"@ai-sdk/openai-compatible": "1.0.32",
|
"@ai-sdk/openai-compatible": "2.0.37",
|
||||||
"@ai-sdk/perplexity": "2.0.23",
|
"@ai-sdk/perplexity": "3.0.26",
|
||||||
"@ai-sdk/provider": "2.0.1",
|
"@ai-sdk/provider": "3.0.8",
|
||||||
"@ai-sdk/provider-utils": "3.0.21",
|
"@ai-sdk/provider-utils": "4.0.21",
|
||||||
"@ai-sdk/togetherai": "1.0.34",
|
"@ai-sdk/togetherai": "2.0.41",
|
||||||
"@ai-sdk/vercel": "1.0.33",
|
"@ai-sdk/vercel": "2.0.39",
|
||||||
"@ai-sdk/xai": "2.0.51",
|
"@ai-sdk/xai": "3.0.75",
|
||||||
"@aws-sdk/credential-providers": "3.993.0",
|
"@aws-sdk/credential-providers": "3.993.0",
|
||||||
"@clack/prompts": "1.0.0-alpha.1",
|
"@clack/prompts": "1.0.0-alpha.1",
|
||||||
"@effect/platform-node": "4.0.0-beta.31",
|
"@effect/platform-node": "catalog:",
|
||||||
"@gitlab/gitlab-ai-provider": "3.6.0",
|
|
||||||
"@gitlab/opencode-gitlab-auth": "1.3.3",
|
|
||||||
"@hono/standard-validator": "0.1.5",
|
"@hono/standard-validator": "0.1.5",
|
||||||
"@hono/zod-validator": "catalog:",
|
"@hono/zod-validator": "catalog:",
|
||||||
"@modelcontextprotocol/sdk": "1.25.2",
|
"@modelcontextprotocol/sdk": "1.27.1",
|
||||||
"@octokit/graphql": "9.0.2",
|
"@octokit/graphql": "9.0.2",
|
||||||
"@octokit/rest": "catalog:",
|
"@octokit/rest": "catalog:",
|
||||||
"@openauthjs/openauth": "catalog:",
|
"@openauthjs/openauth": "catalog:",
|
||||||
@@ -337,9 +337,9 @@
|
|||||||
"@opencode-ai/script": "workspace:*",
|
"@opencode-ai/script": "workspace:*",
|
||||||
"@opencode-ai/sdk": "workspace:*",
|
"@opencode-ai/sdk": "workspace:*",
|
||||||
"@opencode-ai/util": "workspace:*",
|
"@opencode-ai/util": "workspace:*",
|
||||||
"@openrouter/ai-sdk-provider": "1.5.4",
|
"@openrouter/ai-sdk-provider": "2.3.3",
|
||||||
"@opentui/core": "0.1.87",
|
"@opentui/core": "0.1.93",
|
||||||
"@opentui/solid": "0.1.87",
|
"@opentui/solid": "0.1.93",
|
||||||
"@parcel/watcher": "2.5.1",
|
"@parcel/watcher": "2.5.1",
|
||||||
"@pierre/diffs": "catalog:",
|
"@pierre/diffs": "catalog:",
|
||||||
"@solid-primitives/event-bus": "1.1.2",
|
"@solid-primitives/event-bus": "1.1.2",
|
||||||
@@ -347,16 +347,18 @@
|
|||||||
"@standard-schema/spec": "1.0.0",
|
"@standard-schema/spec": "1.0.0",
|
||||||
"@zip.js/zip.js": "2.7.62",
|
"@zip.js/zip.js": "2.7.62",
|
||||||
"ai": "catalog:",
|
"ai": "catalog:",
|
||||||
"ai-gateway-provider": "2.3.1",
|
"ai-gateway-provider": "3.1.2",
|
||||||
"bonjour-service": "1.3.0",
|
"bonjour-service": "1.3.0",
|
||||||
"bun-pty": "0.4.8",
|
"bun-pty": "0.4.8",
|
||||||
"chokidar": "4.0.3",
|
"chokidar": "4.0.3",
|
||||||
"clipboardy": "4.0.0",
|
"clipboardy": "4.0.0",
|
||||||
|
"cross-spawn": "^7.0.6",
|
||||||
"decimal.js": "10.5.0",
|
"decimal.js": "10.5.0",
|
||||||
"diff": "catalog:",
|
"diff": "catalog:",
|
||||||
"drizzle-orm": "1.0.0-beta.16-ea816b6",
|
"drizzle-orm": "catalog:",
|
||||||
"effect": "catalog:",
|
"effect": "catalog:",
|
||||||
"fuzzysort": "3.1.0",
|
"fuzzysort": "3.1.0",
|
||||||
|
"gitlab-ai-provider": "6.0.0",
|
||||||
"glob": "13.0.5",
|
"glob": "13.0.5",
|
||||||
"google-auth-library": "10.5.0",
|
"google-auth-library": "10.5.0",
|
||||||
"gray-matter": "4.0.3",
|
"gray-matter": "4.0.3",
|
||||||
@@ -367,6 +369,8 @@
|
|||||||
"mime-types": "3.0.2",
|
"mime-types": "3.0.2",
|
||||||
"minimatch": "10.0.3",
|
"minimatch": "10.0.3",
|
||||||
"open": "10.1.2",
|
"open": "10.1.2",
|
||||||
|
"opencode-gitlab-auth": "2.0.1",
|
||||||
|
"opencode-poe-auth": "0.0.1",
|
||||||
"opentui-spinner": "0.0.6",
|
"opentui-spinner": "0.0.6",
|
||||||
"partial-json": "0.1.7",
|
"partial-json": "0.1.7",
|
||||||
"remeda": "catalog:",
|
"remeda": "catalog:",
|
||||||
@@ -374,6 +378,7 @@
|
|||||||
"solid-js": "catalog:",
|
"solid-js": "catalog:",
|
||||||
"strip-ansi": "7.1.2",
|
"strip-ansi": "7.1.2",
|
||||||
"tree-sitter-bash": "0.25.0",
|
"tree-sitter-bash": "0.25.0",
|
||||||
|
"tree-sitter-powershell": "0.25.10",
|
||||||
"turndown": "7.2.0",
|
"turndown": "7.2.0",
|
||||||
"ulid": "catalog:",
|
"ulid": "catalog:",
|
||||||
"vscode-jsonrpc": "8.2.1",
|
"vscode-jsonrpc": "8.2.1",
|
||||||
@@ -401,14 +406,15 @@
|
|||||||
"@tsconfig/bun": "catalog:",
|
"@tsconfig/bun": "catalog:",
|
||||||
"@types/babel__core": "7.20.5",
|
"@types/babel__core": "7.20.5",
|
||||||
"@types/bun": "catalog:",
|
"@types/bun": "catalog:",
|
||||||
|
"@types/cross-spawn": "6.0.6",
|
||||||
"@types/mime-types": "3.0.1",
|
"@types/mime-types": "3.0.1",
|
||||||
"@types/semver": "^7.5.8",
|
"@types/semver": "^7.5.8",
|
||||||
"@types/turndown": "5.0.5",
|
"@types/turndown": "5.0.5",
|
||||||
"@types/which": "3.0.4",
|
"@types/which": "3.0.4",
|
||||||
"@types/yargs": "17.0.33",
|
"@types/yargs": "17.0.33",
|
||||||
"@typescript/native-preview": "catalog:",
|
"@typescript/native-preview": "catalog:",
|
||||||
"drizzle-kit": "1.0.0-beta.16-ea816b6",
|
"drizzle-kit": "catalog:",
|
||||||
"drizzle-orm": "1.0.0-beta.16-ea816b6",
|
"drizzle-orm": "catalog:",
|
||||||
"typescript": "catalog:",
|
"typescript": "catalog:",
|
||||||
"vscode-languageserver-types": "3.17.5",
|
"vscode-languageserver-types": "3.17.5",
|
||||||
"why-is-node-running": "3.2.2",
|
"why-is-node-running": "3.2.2",
|
||||||
@@ -417,17 +423,27 @@
|
|||||||
},
|
},
|
||||||
"packages/plugin": {
|
"packages/plugin": {
|
||||||
"name": "@opencode-ai/plugin",
|
"name": "@opencode-ai/plugin",
|
||||||
"version": "1.2.27",
|
"version": "1.3.11",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@opencode-ai/sdk": "workspace:*",
|
"@opencode-ai/sdk": "workspace:*",
|
||||||
"zod": "catalog:",
|
"zod": "catalog:",
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@opentui/core": "0.1.93",
|
||||||
|
"@opentui/solid": "0.1.93",
|
||||||
"@tsconfig/node22": "catalog:",
|
"@tsconfig/node22": "catalog:",
|
||||||
"@types/node": "catalog:",
|
"@types/node": "catalog:",
|
||||||
"@typescript/native-preview": "catalog:",
|
"@typescript/native-preview": "catalog:",
|
||||||
"typescript": "catalog:",
|
"typescript": "catalog:",
|
||||||
},
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@opentui/core": ">=0.1.93",
|
||||||
|
"@opentui/solid": ">=0.1.93",
|
||||||
|
},
|
||||||
|
"optionalPeers": [
|
||||||
|
"@opentui/core",
|
||||||
|
"@opentui/solid",
|
||||||
|
],
|
||||||
},
|
},
|
||||||
"packages/script": {
|
"packages/script": {
|
||||||
"name": "@opencode-ai/script",
|
"name": "@opencode-ai/script",
|
||||||
@@ -441,7 +457,7 @@
|
|||||||
},
|
},
|
||||||
"packages/sdk/js": {
|
"packages/sdk/js": {
|
||||||
"name": "@opencode-ai/sdk",
|
"name": "@opencode-ai/sdk",
|
||||||
"version": "1.2.27",
|
"version": "1.3.11",
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@hey-api/openapi-ts": "0.90.10",
|
"@hey-api/openapi-ts": "0.90.10",
|
||||||
"@tsconfig/node22": "catalog:",
|
"@tsconfig/node22": "catalog:",
|
||||||
@@ -452,7 +468,7 @@
|
|||||||
},
|
},
|
||||||
"packages/slack": {
|
"packages/slack": {
|
||||||
"name": "@opencode-ai/slack",
|
"name": "@opencode-ai/slack",
|
||||||
"version": "1.2.27",
|
"version": "1.3.11",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@opencode-ai/sdk": "workspace:*",
|
"@opencode-ai/sdk": "workspace:*",
|
||||||
"@slack/bolt": "^3.17.1",
|
"@slack/bolt": "^3.17.1",
|
||||||
@@ -487,7 +503,7 @@
|
|||||||
},
|
},
|
||||||
"packages/ui": {
|
"packages/ui": {
|
||||||
"name": "@opencode-ai/ui",
|
"name": "@opencode-ai/ui",
|
||||||
"version": "1.2.27",
|
"version": "1.3.11",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@kobalte/core": "catalog:",
|
"@kobalte/core": "catalog:",
|
||||||
"@opencode-ai/sdk": "workspace:*",
|
"@opencode-ai/sdk": "workspace:*",
|
||||||
@@ -511,6 +527,7 @@
|
|||||||
"motion-dom": "12.34.3",
|
"motion-dom": "12.34.3",
|
||||||
"motion-utils": "12.29.2",
|
"motion-utils": "12.29.2",
|
||||||
"remeda": "catalog:",
|
"remeda": "catalog:",
|
||||||
|
"remend": "catalog:",
|
||||||
"shiki": "catalog:",
|
"shiki": "catalog:",
|
||||||
"solid-js": "catalog:",
|
"solid-js": "catalog:",
|
||||||
"solid-list": "catalog:",
|
"solid-list": "catalog:",
|
||||||
@@ -533,7 +550,7 @@
|
|||||||
},
|
},
|
||||||
"packages/util": {
|
"packages/util": {
|
||||||
"name": "@opencode-ai/util",
|
"name": "@opencode-ai/util",
|
||||||
"version": "1.2.27",
|
"version": "1.3.11",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"zod": "catalog:",
|
"zod": "catalog:",
|
||||||
},
|
},
|
||||||
@@ -544,7 +561,7 @@
|
|||||||
},
|
},
|
||||||
"packages/web": {
|
"packages/web": {
|
||||||
"name": "@opencode-ai/web",
|
"name": "@opencode-ai/web",
|
||||||
"version": "1.2.27",
|
"version": "1.3.11",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@astrojs/cloudflare": "12.6.3",
|
"@astrojs/cloudflare": "12.6.3",
|
||||||
"@astrojs/markdown-remark": "6.3.1",
|
"@astrojs/markdown-remark": "6.3.1",
|
||||||
@@ -577,14 +594,17 @@
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
"trustedDependencies": [
|
"trustedDependencies": [
|
||||||
"electron",
|
|
||||||
"esbuild",
|
"esbuild",
|
||||||
|
"tree-sitter-powershell",
|
||||||
|
"electron",
|
||||||
"web-tree-sitter",
|
"web-tree-sitter",
|
||||||
"tree-sitter-bash",
|
"tree-sitter-bash",
|
||||||
],
|
],
|
||||||
"patchedDependencies": {
|
"patchedDependencies": {
|
||||||
"@openrouter/ai-sdk-provider@1.5.4": "patches/@openrouter%2Fai-sdk-provider@1.5.4.patch",
|
"solid-js@1.9.10": "patches/solid-js@1.9.10.patch",
|
||||||
"@standard-community/standard-openapi@0.2.9": "patches/@standard-community%2Fstandard-openapi@0.2.9.patch",
|
"@standard-community/standard-openapi@0.2.9": "patches/@standard-community%2Fstandard-openapi@0.2.9.patch",
|
||||||
|
"@ai-sdk/anthropic@3.0.64": "patches/@ai-sdk%2Fanthropic@3.0.64.patch",
|
||||||
|
"@ai-sdk/provider-utils@4.0.21": "patches/@ai-sdk%2Fprovider-utils@4.0.21.patch",
|
||||||
},
|
},
|
||||||
"overrides": {
|
"overrides": {
|
||||||
"@types/bun": "catalog:",
|
"@types/bun": "catalog:",
|
||||||
@@ -592,6 +612,7 @@
|
|||||||
},
|
},
|
||||||
"catalog": {
|
"catalog": {
|
||||||
"@cloudflare/workers-types": "4.20251008.0",
|
"@cloudflare/workers-types": "4.20251008.0",
|
||||||
|
"@effect/platform-node": "4.0.0-beta.42",
|
||||||
"@hono/zod-validator": "0.4.2",
|
"@hono/zod-validator": "0.4.2",
|
||||||
"@kobalte/core": "0.13.11",
|
"@kobalte/core": "0.13.11",
|
||||||
"@octokit/rest": "22.0.0",
|
"@octokit/rest": "22.0.0",
|
||||||
@@ -605,17 +626,17 @@
|
|||||||
"@tailwindcss/vite": "4.1.11",
|
"@tailwindcss/vite": "4.1.11",
|
||||||
"@tsconfig/bun": "1.0.9",
|
"@tsconfig/bun": "1.0.9",
|
||||||
"@tsconfig/node22": "22.0.2",
|
"@tsconfig/node22": "22.0.2",
|
||||||
"@types/bun": "1.3.9",
|
"@types/bun": "1.3.11",
|
||||||
"@types/luxon": "3.7.1",
|
"@types/luxon": "3.7.1",
|
||||||
"@types/node": "22.13.9",
|
"@types/node": "22.13.9",
|
||||||
"@types/semver": "7.7.1",
|
"@types/semver": "7.7.1",
|
||||||
"@typescript/native-preview": "7.0.0-dev.20251207.1",
|
"@typescript/native-preview": "7.0.0-dev.20251207.1",
|
||||||
"ai": "5.0.124",
|
"ai": "6.0.138",
|
||||||
"diff": "8.0.2",
|
"diff": "8.0.2",
|
||||||
"dompurify": "3.3.1",
|
"dompurify": "3.3.1",
|
||||||
"drizzle-kit": "1.0.0-beta.16-ea816b6",
|
"drizzle-kit": "1.0.0-beta.19-d95b7a4",
|
||||||
"drizzle-orm": "1.0.0-beta.16-ea816b6",
|
"drizzle-orm": "1.0.0-beta.19-d95b7a4",
|
||||||
"effect": "4.0.0-beta.31",
|
"effect": "4.0.0-beta.42",
|
||||||
"fuzzysort": "3.1.0",
|
"fuzzysort": "3.1.0",
|
||||||
"hono": "4.10.7",
|
"hono": "4.10.7",
|
||||||
"hono-openapi": "1.1.2",
|
"hono-openapi": "1.1.2",
|
||||||
@@ -623,6 +644,7 @@
|
|||||||
"marked": "17.0.1",
|
"marked": "17.0.1",
|
||||||
"marked-shiki": "1.2.1",
|
"marked-shiki": "1.2.1",
|
||||||
"remeda": "2.26.0",
|
"remeda": "2.26.0",
|
||||||
|
"remend": "1.3.0",
|
||||||
"shiki": "3.20.0",
|
"shiki": "3.20.0",
|
||||||
"solid-js": "1.9.10",
|
"solid-js": "1.9.10",
|
||||||
"solid-list": "0.3.0",
|
"solid-list": "0.3.0",
|
||||||
@@ -653,51 +675,51 @@
|
|||||||
|
|
||||||
"@agentclientprotocol/sdk": ["@agentclientprotocol/sdk@0.14.1", "", { "peerDependencies": { "zod": "^3.25.0 || ^4.0.0" } }, "sha512-b6r3PS3Nly+Wyw9U+0nOr47bV8tfS476EgyEMhoKvJCZLbgqoDFN7DJwkxL88RR0aiOqOYV1ZnESHqb+RmdH8w=="],
|
"@agentclientprotocol/sdk": ["@agentclientprotocol/sdk@0.14.1", "", { "peerDependencies": { "zod": "^3.25.0 || ^4.0.0" } }, "sha512-b6r3PS3Nly+Wyw9U+0nOr47bV8tfS476EgyEMhoKvJCZLbgqoDFN7DJwkxL88RR0aiOqOYV1ZnESHqb+RmdH8w=="],
|
||||||
|
|
||||||
"@ai-sdk/amazon-bedrock": ["@ai-sdk/amazon-bedrock@3.0.82", "", { "dependencies": { "@ai-sdk/anthropic": "2.0.65", "@ai-sdk/provider": "2.0.1", "@ai-sdk/provider-utils": "3.0.21", "@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-yb1EkRCMWex0tnpHPLGQxoJEiJvMGOizuxzlXFOpuGFiYgE679NsWE/F8pHwtoAWsqLlylgGAJvJDIJ8us8LEw=="],
|
"@ai-sdk/amazon-bedrock": ["@ai-sdk/amazon-bedrock@4.0.83", "", { "dependencies": { "@ai-sdk/anthropic": "3.0.64", "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.21", "@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-DoRpvIWGU/r83UeJAM9L93Lca8Kf/yP5fIhfEOltMPGP/PXrGe0BZaz0maLSRn8djJ6+HzWIsgu5ZI6bZqXEXg=="],
|
||||||
|
|
||||||
"@ai-sdk/anthropic": ["@ai-sdk/anthropic@2.0.0", "", { "dependencies": { "@ai-sdk/provider": "2.0.0", "@ai-sdk/provider-utils": "3.0.0" }, "peerDependencies": { "zod": "^3.25.76 || ^4" } }, "sha512-uyyaO4KhxoIKZztREqLPh+6/K3ZJx/rp72JKoUEL9/kC+vfQTThUfPnY/bUryUpcnawx8IY/tSoYNOi/8PCv7w=="],
|
"@ai-sdk/anthropic": ["@ai-sdk/anthropic@3.0.64", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.21" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-rwLi/Rsuj2pYniQXIrvClHvXDzgM4UQHHnvHTWEF14efnlKclG/1ghpNC+adsRujAbCTr6gRsSbDE2vEqriV7g=="],
|
||||||
|
|
||||||
"@ai-sdk/azure": ["@ai-sdk/azure@2.0.91", "", { "dependencies": { "@ai-sdk/openai": "2.0.89", "@ai-sdk/provider": "2.0.1", "@ai-sdk/provider-utils": "3.0.20" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-9tznVSs6LGQNKKxb8pKd7CkBV9yk+a/ENpFicHCj2CmBUKefxzwJ9JbUqrlK3VF6dGZw3LXq0dWxt7/Yekaj1w=="],
|
"@ai-sdk/azure": ["@ai-sdk/azure@3.0.49", "", { "dependencies": { "@ai-sdk/openai": "3.0.48", "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.21" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-wskgAL+OmrHG7by/iWIxEBQCEdc1mDudha/UZav46i0auzdFfsDB/k2rXZaC4/3nWSgMZkxr0W3ncyouEGX/eg=="],
|
||||||
|
|
||||||
"@ai-sdk/cerebras": ["@ai-sdk/cerebras@1.0.36", "", { "dependencies": { "@ai-sdk/openai-compatible": "1.0.32", "@ai-sdk/provider": "2.0.1", "@ai-sdk/provider-utils": "3.0.20" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-zoJYL33+ieyd86FSP0Whm86D79d1lKPR7wUzh1SZ1oTxwYmsGyvIrmMf2Ll0JA9Ds2Es6qik4VaFCrjwGYRTIQ=="],
|
"@ai-sdk/cerebras": ["@ai-sdk/cerebras@2.0.41", "", { "dependencies": { "@ai-sdk/openai-compatible": "2.0.37", "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.21" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-kDMEpjaRdRXIUi1EH8WHwLRahyDTYv9SAJnP6VCCeq8X+tVqZbMLCqqxSG5dRknrI65ucjvzQt+FiDKTAa7AHg=="],
|
||||||
|
|
||||||
"@ai-sdk/cohere": ["@ai-sdk/cohere@2.0.22", "", { "dependencies": { "@ai-sdk/provider": "2.0.1", "@ai-sdk/provider-utils": "3.0.20" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-yJ9kP5cEDJwo8qpITq5TQFD8YNfNtW+HbyvWwrKMbFzmiMvIZuk95HIaFXE7PCTuZsqMA05yYu+qX/vQ3rNKjA=="],
|
"@ai-sdk/cohere": ["@ai-sdk/cohere@3.0.27", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.21" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-OqcCq2PiFY1dbK/0Ck45KuvE8jfdxRuuAE9Y5w46dAk6U+9vPOeg1CDcmR+ncqmrYrhRl3nmyDttyDahyjCzAw=="],
|
||||||
|
|
||||||
"@ai-sdk/deepgram": ["@ai-sdk/deepgram@1.0.24", "", { "dependencies": { "@ai-sdk/provider": "2.0.1", "@ai-sdk/provider-utils": "3.0.22" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-E+wzGPSa/XHmajO3WtX8mtq0ewy04tsHSpU6/SGwqbiykwWba/emi7ayZ4ir89s5OzbAen2g7T9zZiEchMfkHQ=="],
|
"@ai-sdk/deepgram": ["@ai-sdk/deepgram@2.0.24", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.19" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-htT1Y7vBN0cRu/1pGnhx6DNH3xaNr0o0MjDkmii48X2+6S/WkOzVNtMjn7V3vLWEQIWNio5vw1hG/F43K8WLHA=="],
|
||||||
|
|
||||||
"@ai-sdk/deepinfra": ["@ai-sdk/deepinfra@1.0.36", "", { "dependencies": { "@ai-sdk/openai-compatible": "1.0.33", "@ai-sdk/provider": "2.0.1", "@ai-sdk/provider-utils": "3.0.21" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-LndvRktEgY2IFu4peDJMEXcjhHEEFtM0upLx/J64kCpFHCifalXpK4PPSX3PVndnn0bJzvamO5+fc0z2ooqBZw=="],
|
"@ai-sdk/deepinfra": ["@ai-sdk/deepinfra@2.0.41", "", { "dependencies": { "@ai-sdk/openai-compatible": "2.0.37", "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.21" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-y6RoOP7DGWmDSiSxrUSt5p18sbz+Ixe5lMVPmdE7x+Tr5rlrzvftyHhjWHfqlAtoYERZTGFbP6tPW1OfQcrb4A=="],
|
||||||
|
|
||||||
"@ai-sdk/deepseek": ["@ai-sdk/deepseek@1.0.35", "", { "dependencies": { "@ai-sdk/provider": "2.0.1", "@ai-sdk/provider-utils": "3.0.22" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-Qvh2yxL5zJS9RO/Bf12pyYBIDmn+9GR1hT6e28IYWQWnt2Xq0h9XGps6XagLAv3VYYFg8c/ozkWVd4kXLZ25HA=="],
|
"@ai-sdk/deepseek": ["@ai-sdk/deepseek@2.0.24", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.19" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-4vOEekW4TAYVHN0qgiwoUOQZhguGwZBiEw8LDeUmpWBm07QkLRAtxYCaSoMiA4hZZojao5mj6NRGEBW1CnDPtg=="],
|
||||||
|
|
||||||
"@ai-sdk/elevenlabs": ["@ai-sdk/elevenlabs@1.0.24", "", { "dependencies": { "@ai-sdk/provider": "2.0.1", "@ai-sdk/provider-utils": "3.0.22" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-ee2At5jgV+SqC6nrtPq20iH7N/aN+O36LrA4gkzVM4cmhM7bvQKVkOXhC1XxG+wsYG6UZi3Nekoi8MEjNWuRrw=="],
|
"@ai-sdk/elevenlabs": ["@ai-sdk/elevenlabs@2.0.24", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.19" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-K+1YprVMO8R6vTcNhqTqUWhOzX5V/hEY0pFx9KQL0/+MJjOgRi6DcOLoNBd7ONcjxYTyiFLRfk/0a/pHTtSgFA=="],
|
||||||
|
|
||||||
"@ai-sdk/fireworks": ["@ai-sdk/fireworks@1.0.35", "", { "dependencies": { "@ai-sdk/openai-compatible": "1.0.34", "@ai-sdk/provider": "2.0.1", "@ai-sdk/provider-utils": "3.0.22" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-inUq29XvSVDer6JIeOkwAmCFxOtHPU0OZEhwaWoe3PI59naHIW4RIFA9wppLLV5fJI9WQcAfDKy0ZHW9nV3UJw=="],
|
"@ai-sdk/fireworks": ["@ai-sdk/fireworks@2.0.40", "", { "dependencies": { "@ai-sdk/openai-compatible": "2.0.35", "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.19" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-ARjygiBQtVSgNBp3Sag+Bkwn68ub+cZPC05UpRGG+VY8/Q896K2yU1j4I0+S1eU0BQW/9DKbRG04d9Ayi2DUmA=="],
|
||||||
|
|
||||||
"@ai-sdk/gateway": ["@ai-sdk/gateway@2.0.30", "", { "dependencies": { "@ai-sdk/provider": "2.0.1", "@ai-sdk/provider-utils": "3.0.20", "@vercel/oidc": "3.1.0" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-5Nrkj8B4MzkkOfjjA+Cs5pamkbkK4lI11bx80QV7TFcen/hWA8wEC+UVzwuM5H2zpekoNMjvl6GonHnR62XIZw=="],
|
"@ai-sdk/gateway": ["@ai-sdk/gateway@3.0.80", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.21", "@vercel/oidc": "3.1.0" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-uM7kpZB5l977lW7+2X1+klBUxIZQ78+1a9jHlaHFEzcOcmmslTl3sdP0QqfuuBcO0YBM2gwOiqVdp8i4TRQYcw=="],
|
||||||
|
|
||||||
"@ai-sdk/google": ["@ai-sdk/google@2.0.54", "", { "dependencies": { "@ai-sdk/provider": "2.0.1", "@ai-sdk/provider-utils": "3.0.21" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-VKguP0x/PUYpdQyuA/uy5pDGJy6reL0X/yDKxHfL207aCUXpFIBmyMhVs4US39dkEVhtmIFSwXauY0Pt170JRw=="],
|
"@ai-sdk/google": ["@ai-sdk/google@3.0.53", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.21" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-uz8tIlkDgQJG9Js2Wh9JHzd4kI9+hYJqf9XXJLx60vyN5mRIqhr49iwR5zGP5Gl8odp2PeR3Gh2k+5bh3Z1HHw=="],
|
||||||
|
|
||||||
"@ai-sdk/google-vertex": ["@ai-sdk/google-vertex@3.0.106", "", { "dependencies": { "@ai-sdk/anthropic": "2.0.65", "@ai-sdk/google": "2.0.54", "@ai-sdk/provider": "2.0.1", "@ai-sdk/provider-utils": "3.0.21", "google-auth-library": "^10.5.0" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-f9sA66bmhgJoTwa+pHWFSdYxPa0lgdQ/MgYNxZptzVyGptoziTf1a9EIXEL3jiCD0qIBAg+IhDAaYalbvZaDqQ=="],
|
"@ai-sdk/google-vertex": ["@ai-sdk/google-vertex@4.0.95", "", { "dependencies": { "@ai-sdk/anthropic": "3.0.64", "@ai-sdk/google": "3.0.53", "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.21", "google-auth-library": "^10.5.0" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-xL44fHlTtDM7RLkMTgyqMfkfthA38JS91bbMaHItObIhte1PAIY936ZV1PLl/Z9A/oBAXjHWbXo5xDoHzB7LEg=="],
|
||||||
|
|
||||||
"@ai-sdk/groq": ["@ai-sdk/groq@2.0.34", "", { "dependencies": { "@ai-sdk/provider": "2.0.1", "@ai-sdk/provider-utils": "3.0.20" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-wfCYkVgmVjxNA32T57KbLabVnv9aFUflJ4urJ7eWgTwbnmGQHElCTu+rJ3ydxkXSqxOkXPwMOttDm7XNrvPjmg=="],
|
"@ai-sdk/groq": ["@ai-sdk/groq@3.0.31", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.21" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-XbbugpnFmXGu2TlXiq8KUJskP6/VVbuFcnFIGDzDIB/Chg6XHsNnqrTF80Zxkh0Pd3+NvbM+2Uqrtsndk6bDAg=="],
|
||||||
|
|
||||||
"@ai-sdk/mistral": ["@ai-sdk/mistral@2.0.27", "", { "dependencies": { "@ai-sdk/provider": "2.0.1", "@ai-sdk/provider-utils": "3.0.20" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-gaptHgaXjMw3+eA0Q4FABcsj5nQNP6EpFaGUR+Pj5WJy7Kn6mApl975/x57224MfeJIShNpt8wFKK3tvh5ewKg=="],
|
"@ai-sdk/mistral": ["@ai-sdk/mistral@3.0.27", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.21" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-ZXe7nZQgliDdjz5ufH5RKpHWxbN72AzmzzKGbF/z+0K9GN5tUCnftrQRvTRFHA5jAzTapcm2BEevmGLVbMkW+A=="],
|
||||||
|
|
||||||
"@ai-sdk/openai": ["@ai-sdk/openai@2.0.2", "", { "dependencies": { "@ai-sdk/provider": "2.0.0", "@ai-sdk/provider-utils": "3.0.0" }, "peerDependencies": { "zod": "^3.25.76 || ^4" } }, "sha512-D4zYz2uR90aooKQvX1XnS00Z7PkbrcY+snUvPfm5bCabTG7bzLrVtD56nJ5bSaZG8lmuOMfXpyiEEArYLyWPpw=="],
|
"@ai-sdk/openai": ["@ai-sdk/openai@3.0.48", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.21" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-ALmj/53EXpcRqMbGpPJPP4UOSWw0q4VGpnDo7YctvsynjkrKDmoneDG/1a7VQnSPYHnJp6tTRMf5ZdxZ5whulg=="],
|
||||||
|
|
||||||
"@ai-sdk/openai-compatible": ["@ai-sdk/openai-compatible@1.0.1", "", { "dependencies": { "@ai-sdk/provider": "2.0.0", "@ai-sdk/provider-utils": "3.0.0" }, "peerDependencies": { "zod": "^3.25.76 || ^4" } }, "sha512-luHVcU+yKzwv3ekKgbP3v+elUVxb2Rt+8c6w9qi7g2NYG2/pEL21oIrnaEnc6UtTZLLZX9EFBcpq2N1FQKDIMw=="],
|
"@ai-sdk/openai-compatible": ["@ai-sdk/openai-compatible@2.0.37", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.21" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-+POSFVcgiu47BK64dhsI6OpcDC0/VAE2ZSaXdXGNNhpC/ava++uSRJYks0k2bpfY0wwCTgpAWZsXn/dG2Yppiw=="],
|
||||||
|
|
||||||
"@ai-sdk/perplexity": ["@ai-sdk/perplexity@2.0.23", "", { "dependencies": { "@ai-sdk/provider": "2.0.1", "@ai-sdk/provider-utils": "3.0.20" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-aiaRvnc6mhQZKhTTSXPCjPH8Iqr5D/PfCN1hgVP/3RGTBbJtsd9HemIBSABeSdAKbsMH/PwJxgnqH75HEamcBA=="],
|
"@ai-sdk/perplexity": ["@ai-sdk/perplexity@3.0.26", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.21" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-dXzrVsLR5f6tr+U04jq4AXoRroGFBTvODnLgss0SWbzNjGGQg3XqtQ9j7rCLo6o8qbYGuAHvqUrIpUCuiscuFg=="],
|
||||||
|
|
||||||
"@ai-sdk/provider": ["@ai-sdk/provider@2.0.1", "", { "dependencies": { "json-schema": "^0.4.0" } }, "sha512-KCUwswvsC5VsW2PWFqF8eJgSCu5Ysj7m1TxiHTVA6g7k360bk0RNQENT8KTMAYEs+8fWPD3Uu4dEmzGHc+jGng=="],
|
"@ai-sdk/provider": ["@ai-sdk/provider@3.0.8", "", { "dependencies": { "json-schema": "^0.4.0" } }, "sha512-oGMAgGoQdBXbZqNG0Ze56CHjDZ1IDYOwGYxYjO5KLSlz5HiNQ9udIXsPZ61VWaHGZ5XW/jyjmr6t2xz2jGVwbQ=="],
|
||||||
|
|
||||||
"@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@3.0.21", "", { "dependencies": { "@ai-sdk/provider": "2.0.1", "@standard-schema/spec": "^1.0.0", "eventsource-parser": "^3.0.6" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-veuMwTLxsgh31Jjn0SnBABnM1f7ebHhRWcV2ZuY3hP3iJDCZ8VXBaYqcHXoOQDqUXTCas08sKQcHyWK+zl882Q=="],
|
"@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@4.0.21", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@standard-schema/spec": "^1.1.0", "eventsource-parser": "^3.0.6" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-MtFUYI1/8mgDvRmaBDjbLJPFFrMG777AvSgyIFQtZHIMzm88R/12vYBBpnk7pfiWLFE1DSZzY4WDYzGbKAcmiw=="],
|
||||||
|
|
||||||
"@ai-sdk/togetherai": ["@ai-sdk/togetherai@1.0.34", "", { "dependencies": { "@ai-sdk/openai-compatible": "1.0.32", "@ai-sdk/provider": "2.0.1", "@ai-sdk/provider-utils": "3.0.20" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-jjJmJms6kdEc4nC3MDGFJfhV8F1ifY4nolV2dbnT7BM4ab+Wkskc0GwCsJ7G7WdRMk7xDbFh4he3DPL8KJ/cyA=="],
|
"@ai-sdk/togetherai": ["@ai-sdk/togetherai@2.0.41", "", { "dependencies": { "@ai-sdk/openai-compatible": "2.0.37", "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.21" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-k3p9e3k0/gpDDyTtvafsK4HYR4D/aUQW/kzCwWo1+CzdBU84i4L14gWISC/mv6tgSicMXHcEUd521fPufQwNlg=="],
|
||||||
|
|
||||||
"@ai-sdk/vercel": ["@ai-sdk/vercel@1.0.33", "", { "dependencies": { "@ai-sdk/openai-compatible": "1.0.32", "@ai-sdk/provider": "2.0.1", "@ai-sdk/provider-utils": "3.0.20" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-Qwjm+HdwKasu7L9bDUryBMGKDMscIEzMUkjw/33uGdJpktzyNW13YaNIObOZ2HkskqDMIQJSd4Ao2BBT8fEYLw=="],
|
"@ai-sdk/vercel": ["@ai-sdk/vercel@2.0.39", "", { "dependencies": { "@ai-sdk/openai-compatible": "2.0.37", "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.21" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-8eu3ljJpkCTP4ppcyYB+NcBrkcBoSOFthCSgk5VnjaxnDaOJFaxnPwfddM7wx3RwMk2CiK1O61Px/LlqNc7QkQ=="],
|
||||||
|
|
||||||
"@ai-sdk/xai": ["@ai-sdk/xai@2.0.51", "", { "dependencies": { "@ai-sdk/openai-compatible": "1.0.30", "@ai-sdk/provider": "2.0.1", "@ai-sdk/provider-utils": "3.0.20" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-AI3le03qiegkZvn9hpnpDwez49lOvQLj4QUBT8H41SMbrdTYOxn3ktTwrsSu90cNDdzKGMvoH0u2GHju1EdnCg=="],
|
"@ai-sdk/xai": ["@ai-sdk/xai@3.0.75", "", { "dependencies": { "@ai-sdk/openai-compatible": "2.0.37", "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.21" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-V8UKK4fNpI9cnrtsZBvUp9O9J6Y9fTKBRoSLyEaNGPirACewixmLDbXsSgAeownPVWiWpK34bFysd+XouI5Ywg=="],
|
||||||
|
|
||||||
"@alloc/quick-lru": ["@alloc/quick-lru@5.2.0", "", {}, "sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw=="],
|
"@alloc/quick-lru": ["@alloc/quick-lru@5.2.0", "", {}, "sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw=="],
|
||||||
|
|
||||||
@@ -973,9 +995,9 @@
|
|||||||
|
|
||||||
"@effect/language-service": ["@effect/language-service@0.79.0", "", { "bin": { "effect-language-service": "cli.js" } }, "sha512-DEmIOsg1GjjP6s9HXH1oJrW+gDmzkhVv9WOZl6to5eNyyCrjz1S2PDqQ7aYrW/HuifhfwI5Bik1pK4pj7Z+lrg=="],
|
"@effect/language-service": ["@effect/language-service@0.79.0", "", { "bin": { "effect-language-service": "cli.js" } }, "sha512-DEmIOsg1GjjP6s9HXH1oJrW+gDmzkhVv9WOZl6to5eNyyCrjz1S2PDqQ7aYrW/HuifhfwI5Bik1pK4pj7Z+lrg=="],
|
||||||
|
|
||||||
"@effect/platform-node": ["@effect/platform-node@4.0.0-beta.31", "", { "dependencies": { "@effect/platform-node-shared": "^4.0.0-beta.31", "mime": "^4.1.0", "undici": "^7.20.0" }, "peerDependencies": { "effect": "^4.0.0-beta.31", "ioredis": "^5.7.0" } }, "sha512-KmVZwGsQRBMZZYPJwpL2vj6sxjBzfXhyA8RgsH5/cmckDTsZpVTyqODQ/FFzmCnMWuYjZoJGPghTDrVVDn/6ZA=="],
|
"@effect/platform-node": ["@effect/platform-node@4.0.0-beta.42", "", { "dependencies": { "@effect/platform-node-shared": "^4.0.0-beta.42", "mime": "^4.1.0", "undici": "^7.24.0" }, "peerDependencies": { "effect": "^4.0.0-beta.42", "ioredis": "^5.7.0" } }, "sha512-kbdRML2FBa4q8U8rZQcnmLKZ5zN/z1bAA7t5D1/UsBHZqJgnfRgu1CP6kaEfb1Nie6YyaWshxTktZQryjvW/Yg=="],
|
||||||
|
|
||||||
"@effect/platform-node-shared": ["@effect/platform-node-shared@4.0.0-beta.33", "", { "dependencies": { "@types/ws": "^8.18.1", "ws": "^8.19.0" }, "peerDependencies": { "effect": "^4.0.0-beta.33" } }, "sha512-jaJnvYz1IiPZyN//fCJsvwnmujJS5KD8noCVVLhb4ZGCWKhQpt0x2iuax6HFzMlPEQSfl04GLU+PVKh0nkzPyA=="],
|
"@effect/platform-node-shared": ["@effect/platform-node-shared@4.0.0-beta.42", "", { "dependencies": { "@types/ws": "^8.18.1", "ws": "^8.19.0" }, "peerDependencies": { "effect": "^4.0.0-beta.42" } }, "sha512-PC+lxLsrwob3+nBChAPrQq32olCeyApgXBvs1NrRsoArLViNT76T/68CttuCAksCZj5e1bZ1ZibLPel3vUmx2g=="],
|
||||||
|
|
||||||
"@electron/asar": ["@electron/asar@3.4.1", "", { "dependencies": { "commander": "^5.0.0", "glob": "^7.1.6", "minimatch": "^3.0.4" }, "bin": { "asar": "bin/asar.js" } }, "sha512-i4/rNPRS84t0vSRa2HorerGRXWyF4vThfHesw0dmcWHp+cspK743UanA0suA5Q5y8kzY2y6YKrvbIUn69BCAiA=="],
|
"@electron/asar": ["@electron/asar@3.4.1", "", { "dependencies": { "commander": "^5.0.0", "glob": "^7.1.6", "minimatch": "^3.0.4" }, "bin": { "asar": "bin/asar.js" } }, "sha512-i4/rNPRS84t0vSRa2HorerGRXWyF4vThfHesw0dmcWHp+cspK743UanA0suA5Q5y8kzY2y6YKrvbIUn69BCAiA=="],
|
||||||
|
|
||||||
@@ -1105,10 +1127,6 @@
|
|||||||
|
|
||||||
"@fontsource/inter": ["@fontsource/inter@5.2.8", "", {}, "sha512-P6r5WnJoKiNVV+zvW2xM13gNdFhAEpQ9dQJHt3naLvfg+LkF2ldgSLiF4T41lf1SQCM9QmkqPTn4TH568IRagg=="],
|
"@fontsource/inter": ["@fontsource/inter@5.2.8", "", {}, "sha512-P6r5WnJoKiNVV+zvW2xM13gNdFhAEpQ9dQJHt3naLvfg+LkF2ldgSLiF4T41lf1SQCM9QmkqPTn4TH568IRagg=="],
|
||||||
|
|
||||||
"@gitlab/gitlab-ai-provider": ["@gitlab/gitlab-ai-provider@3.6.0", "", { "dependencies": { "@anthropic-ai/sdk": "^0.71.0", "@anycable/core": "^0.9.2", "graphql-request": "^6.1.0", "isomorphic-ws": "^5.0.0", "openai": "^6.16.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-8LmcIQ86xkMtC7L4P1/QYVEC+yKMTRerfPeniaaQGalnzXKtX6iMHLjLPOL9Rxp55lOXi6ed0WrFuJzZx+fNRg=="],
|
|
||||||
|
|
||||||
"@gitlab/opencode-gitlab-auth": ["@gitlab/opencode-gitlab-auth@1.3.3", "", { "dependencies": { "@fastify/rate-limit": "^10.2.0", "@opencode-ai/plugin": "*", "fastify": "^5.2.0", "open": "^10.0.0" } }, "sha512-FT+KsCmAJjtqWr1YAq0MywGgL9kaLQ4apmsoowAXrPqHtoYf2i/nY10/A+L06kNj22EATeEDRpbB1NWXMto/SA=="],
|
|
||||||
|
|
||||||
"@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=="],
|
"@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=="],
|
"@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=="],
|
||||||
@@ -1321,7 +1339,7 @@
|
|||||||
|
|
||||||
"@mixmark-io/domino": ["@mixmark-io/domino@2.2.0", "", {}, "sha512-Y28PR25bHXUg88kCV7nivXrP2Nj2RueZ3/l/jdx6J9f8J4nsEGcgX0Qe6lt7Pa+J79+kPiJU3LguR6O/6zrLOw=="],
|
"@mixmark-io/domino": ["@mixmark-io/domino@2.2.0", "", {}, "sha512-Y28PR25bHXUg88kCV7nivXrP2Nj2RueZ3/l/jdx6J9f8J4nsEGcgX0Qe6lt7Pa+J79+kPiJU3LguR6O/6zrLOw=="],
|
||||||
|
|
||||||
"@modelcontextprotocol/sdk": ["@modelcontextprotocol/sdk@1.25.2", "", { "dependencies": { "@hono/node-server": "^1.19.7", "ajv": "^8.17.1", "ajv-formats": "^3.0.1", "content-type": "^1.0.5", "cors": "^2.8.5", "cross-spawn": "^7.0.5", "eventsource": "^3.0.2", "eventsource-parser": "^3.0.0", "express": "^5.0.1", "express-rate-limit": "^7.5.0", "jose": "^6.1.1", "json-schema-typed": "^8.0.2", "pkce-challenge": "^5.0.0", "raw-body": "^3.0.0", "zod": "^3.25 || ^4.0", "zod-to-json-schema": "^3.25.0" }, "peerDependencies": { "@cfworker/json-schema": "^4.1.1" }, "optionalPeers": ["@cfworker/json-schema"] }, "sha512-LZFeo4F9M5qOhC/Uc1aQSrBHxMrvxett+9KLHt7OhcExtoiRN9DKgbZffMP/nxjutWDQpfMDfP3nkHI4X9ijww=="],
|
"@modelcontextprotocol/sdk": ["@modelcontextprotocol/sdk@1.27.1", "", { "dependencies": { "@hono/node-server": "^1.19.9", "ajv": "^8.17.1", "ajv-formats": "^3.0.1", "content-type": "^1.0.5", "cors": "^2.8.5", "cross-spawn": "^7.0.5", "eventsource": "^3.0.2", "eventsource-parser": "^3.0.0", "express": "^5.2.1", "express-rate-limit": "^8.2.1", "hono": "^4.11.4", "jose": "^6.1.3", "json-schema-typed": "^8.0.2", "pkce-challenge": "^5.0.0", "raw-body": "^3.0.0", "zod": "^3.25 || ^4.0", "zod-to-json-schema": "^3.25.1" }, "peerDependencies": { "@cfworker/json-schema": "^4.1.1" }, "optionalPeers": ["@cfworker/json-schema"] }, "sha512-sr6GbP+4edBwFndLbM60gf07z0FQ79gaExpnsjMGePXqFcSSb7t6iscpjk9DhFhwd+mTEQrzNafGP8/iGGFYaA=="],
|
||||||
|
|
||||||
"@motionone/animation": ["@motionone/animation@10.18.0", "", { "dependencies": { "@motionone/easing": "^10.18.0", "@motionone/types": "^10.17.1", "@motionone/utils": "^10.18.0", "tslib": "^2.3.1" } }, "sha512-9z2p5GFGCm0gBsZbi8rVMOAJCtw1WqBTIPw3ozk06gDvZInBPIsQcHgYogEJ4yuHJ+akuW8g1SEIOpTOvYs8hw=="],
|
"@motionone/animation": ["@motionone/animation@10.18.0", "", { "dependencies": { "@motionone/easing": "^10.18.0", "@motionone/types": "^10.17.1", "@motionone/utils": "^10.18.0", "tslib": "^2.3.1" } }, "sha512-9z2p5GFGCm0gBsZbi8rVMOAJCtw1WqBTIPw3ozk06gDvZInBPIsQcHgYogEJ4yuHJ+akuW8g1SEIOpTOvYs8hw=="],
|
||||||
|
|
||||||
@@ -1439,27 +1457,25 @@
|
|||||||
|
|
||||||
"@opencode-ai/web": ["@opencode-ai/web@workspace:packages/web"],
|
"@opencode-ai/web": ["@opencode-ai/web@workspace:packages/web"],
|
||||||
|
|
||||||
"@openrouter/ai-sdk-provider": ["@openrouter/ai-sdk-provider@1.5.4", "", { "dependencies": { "@openrouter/sdk": "^0.1.27" }, "peerDependencies": { "ai": "^5.0.0", "zod": "^3.24.1 || ^v4" } }, "sha512-xrSQPUIH8n9zuyYZR0XK7Ba0h2KsjJcMkxnwaYfmv13pKs3sDkjPzVPPhlhzqBGddHb5cFEwJ9VFuFeDcxCDSw=="],
|
"@openrouter/ai-sdk-provider": ["@openrouter/ai-sdk-provider@2.3.3", "", { "peerDependencies": { "ai": "^6.0.0", "zod": "^3.25.0 || ^4.0.0" } }, "sha512-4fVteGkVedc7fGoA9+qJs4tpYwALezMq14m2Sjub3KmyRlksCbK+WJf67NPdGem8+NZrV2tAN42A1NU3+SiV3w=="],
|
||||||
|
|
||||||
"@openrouter/sdk": ["@openrouter/sdk@0.1.27", "", { "dependencies": { "zod": "^3.25.0 || ^4.0.0" } }, "sha512-RH//L10bSmc81q25zAZudiI4kNkLgxF2E+WU42vghp3N6TEvZ6F0jK7uT3tOxkEn91gzmMw9YVmDENy7SJsajQ=="],
|
|
||||||
|
|
||||||
"@opentelemetry/api": ["@opentelemetry/api@1.9.0", "", {}, "sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg=="],
|
"@opentelemetry/api": ["@opentelemetry/api@1.9.0", "", {}, "sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg=="],
|
||||||
|
|
||||||
"@opentui/core": ["@opentui/core@0.1.87", "", { "dependencies": { "bun-ffi-structs": "0.1.2", "diff": "8.0.2", "jimp": "1.6.0", "marked": "17.0.1", "yoga-layout": "3.2.1" }, "optionalDependencies": { "@dimforge/rapier2d-simd-compat": "^0.17.3", "@opentui/core-darwin-arm64": "0.1.87", "@opentui/core-darwin-x64": "0.1.87", "@opentui/core-linux-arm64": "0.1.87", "@opentui/core-linux-x64": "0.1.87", "@opentui/core-win32-arm64": "0.1.87", "@opentui/core-win32-x64": "0.1.87", "bun-webgpu": "0.1.5", "planck": "^1.4.2", "three": "0.177.0" }, "peerDependencies": { "web-tree-sitter": "0.25.10" } }, "sha512-dhsmMv0IqKftwG7J/pBrLBj2armsYIg5R3LBvciRQI/6X89GufP4l1u0+QTACAx6iR4SYJJNVNQ2tdX8LM9rMw=="],
|
"@opentui/core": ["@opentui/core@0.1.93", "", { "dependencies": { "bun-ffi-structs": "0.1.2", "diff": "8.0.2", "jimp": "1.6.0", "marked": "17.0.1", "yoga-layout": "3.2.1" }, "optionalDependencies": { "@dimforge/rapier2d-simd-compat": "^0.17.3", "@opentui/core-darwin-arm64": "0.1.93", "@opentui/core-darwin-x64": "0.1.93", "@opentui/core-linux-arm64": "0.1.93", "@opentui/core-linux-x64": "0.1.93", "@opentui/core-win32-arm64": "0.1.93", "@opentui/core-win32-x64": "0.1.93", "bun-webgpu": "0.1.5", "planck": "^1.4.2", "three": "0.177.0" }, "peerDependencies": { "web-tree-sitter": "0.25.10" } }, "sha512-HlTM16ZiBKN0mPBNMHSILkSrbzNku6Pg/ovIpVVkEPqLeWeSC2bfZS4Uhc0Ej1sckVVVoU9HKBJanfHvpP+pMg=="],
|
||||||
|
|
||||||
"@opentui/core-darwin-arm64": ["@opentui/core-darwin-arm64@0.1.87", "", { "os": "darwin", "cpu": "arm64" }, "sha512-G8oq85diOfkU6n0T1CxCle7oDmpKxwhcdhZ9khBMU5IrfLx9ZDuCM3F6MsiRQWdvPPCq2oomNbd64bYkPamYgw=="],
|
"@opentui/core-darwin-arm64": ["@opentui/core-darwin-arm64@0.1.93", "", { "os": "darwin", "cpu": "arm64" }, "sha512-4I2mwhXLqRNUv7tu88hA6cBGaGpLZXkAa8W0VqBiGDV+Tx337x4T+vbQ7G57OwKXT787oTrEOF9rOOrGLov6qw=="],
|
||||||
|
|
||||||
"@opentui/core-darwin-x64": ["@opentui/core-darwin-x64@0.1.87", "", { "os": "darwin", "cpu": "x64" }, "sha512-MYTFQfOHm6qO7YaY4GHK9u/oJlXY6djaaxl5I+k4p2mk3vvuFIl/AP1ypITwBFjyV5gyp7PRWFp4nGfY9oN8bw=="],
|
"@opentui/core-darwin-x64": ["@opentui/core-darwin-x64@0.1.93", "", { "os": "darwin", "cpu": "x64" }, "sha512-jvYMgcg47a5qLhSv1DnQiafEWBQ1UukGutmsYV1TvNuhWtuDXYLVy2AhKIHPzbB9JNrV0IpjbxUC8QnJaP3n8g=="],
|
||||||
|
|
||||||
"@opentui/core-linux-arm64": ["@opentui/core-linux-arm64@0.1.87", "", { "os": "linux", "cpu": "arm64" }, "sha512-he8o1h5M6oskRJ7wE+xKJgmWnv5ZwN6gB3M/Z+SeHtOMPa5cZmi3TefTjG54llEgFfx0F9RcqHof7TJ/GNxRkw=="],
|
"@opentui/core-linux-arm64": ["@opentui/core-linux-arm64@0.1.93", "", { "os": "linux", "cpu": "arm64" }, "sha512-bvFqRcPftmg14iYmMc3d63XC9rhe4yF7pJRApH6klLBKp27WX/LU0iSO4mvyX7qhy65gcmyy4Sj9dl5jNJ+vlA=="],
|
||||||
|
|
||||||
"@opentui/core-linux-x64": ["@opentui/core-linux-x64@0.1.87", "", { "os": "linux", "cpu": "x64" }, "sha512-aiUwjPlH4yDcB8/6YDKSmMkaoGAAltL0Xo0AzXyAtJXWK5tkCSaYjEVwzJ/rYRkr4Magnad+Mjth4AQUWdR2AA=="],
|
"@opentui/core-linux-x64": ["@opentui/core-linux-x64@0.1.93", "", { "os": "linux", "cpu": "x64" }, "sha512-/wJXhwtNxdcpshrRl1KouyGE54ODAHxRQgBHtnlM/F4bB8cjzOlq2Yc+5cv5DxRz4Q0nQZFCPefwpg2U6ZwNdA=="],
|
||||||
|
|
||||||
"@opentui/core-win32-arm64": ["@opentui/core-win32-arm64@0.1.87", "", { "os": "win32", "cpu": "arm64" }, "sha512-cmP0pOyREjWGniHqbDmaMY7U+1AyagrD8VseJbU0cGpNgVpG2/gbrJUGdfdLB0SNb+mzLdx6SOjdxtrElwRCQA=="],
|
"@opentui/core-win32-arm64": ["@opentui/core-win32-arm64@0.1.93", "", { "os": "win32", "cpu": "arm64" }, "sha512-g3PQobfM2yFPSzkBKRKFp8FgTG4ulWyJcU+GYXjyYmxQIT+ZbOU7UfR//ImRq3/FxUAfUC/MhC6WwjqccjEqBw=="],
|
||||||
|
|
||||||
"@opentui/core-win32-x64": ["@opentui/core-win32-x64@0.1.87", "", { "os": "win32", "cpu": "x64" }, "sha512-N2GErAAP8iODf2RPp86pilPaVKiD6G4pkpZL5nLGbKsl0bndrVTpSqZcn8+/nQwFZDPD/AsiRTYNOfWOblhzOw=="],
|
"@opentui/core-win32-x64": ["@opentui/core-win32-x64@0.1.93", "", { "os": "win32", "cpu": "x64" }, "sha512-Spllte2W7q+WfB1zVHgHilVJNp+jpp77PkkxTWyMQNvT7vJNt9LABMNjGTGiJBBMkAuKvO0GgFNKxrda7tFKrQ=="],
|
||||||
|
|
||||||
"@opentui/solid": ["@opentui/solid@0.1.87", "", { "dependencies": { "@babel/core": "7.28.0", "@babel/preset-typescript": "7.27.1", "@opentui/core": "0.1.87", "babel-plugin-module-resolver": "5.0.2", "babel-preset-solid": "1.9.9", "entities": "7.0.1", "s-js": "^0.4.9" }, "peerDependencies": { "solid-js": "1.9.9" } }, "sha512-lRT9t30l8+FtgOjjWJcdb2MT6hP8/RKqwGgYwTI7fXrOqdhxxwdP2SM+rH2l3suHeASheiTdlvPAo230iUcsvg=="],
|
"@opentui/solid": ["@opentui/solid@0.1.93", "", { "dependencies": { "@babel/core": "7.28.0", "@babel/preset-typescript": "7.27.1", "@opentui/core": "0.1.93", "babel-plugin-module-resolver": "5.0.2", "babel-preset-solid": "1.9.10", "entities": "7.0.1", "s-js": "^0.4.9" }, "peerDependencies": { "solid-js": "1.9.11" } }, "sha512-Qx+4qoLSjnRGoo/YY4sZJMyXj09Y5kaAMpVO+65Ax58MMj4TjABN4bOOiRT2KV7sKOMTjxiAgXAIaBuqBBJ0Qg=="],
|
||||||
|
|
||||||
"@oslojs/asn1": ["@oslojs/asn1@1.0.0", "", { "dependencies": { "@oslojs/binary": "1.0.0" } }, "sha512-zw/wn0sj0j0QKbIXfIlnEcTviaCzYOY3V5rAyjR6YtOByFtJiT574+8p9Wlach0lZH9fddD4yb9laEAIl4vXQA=="],
|
"@oslojs/asn1": ["@oslojs/asn1@1.0.0", "", { "dependencies": { "@oslojs/binary": "1.0.0" } }, "sha512-zw/wn0sj0j0QKbIXfIlnEcTviaCzYOY3V5rAyjR6YtOByFtJiT574+8p9Wlach0lZH9fddD4yb9laEAIl4vXQA=="],
|
||||||
|
|
||||||
@@ -1887,6 +1903,8 @@
|
|||||||
|
|
||||||
"@solid-primitives/storage": ["@solid-primitives/storage@4.3.3", "", { "dependencies": { "@solid-primitives/utils": "^6.3.2" }, "peerDependencies": { "@tauri-apps/plugin-store": "*", "solid-js": "^1.6.12" }, "optionalPeers": ["@tauri-apps/plugin-store"] }, "sha512-ACbNwMZ1s8VAvld6EUXkDkX/US3IhtlPLxg6+B2s9MwNUugwdd51I98LPEaHrdLpqPmyzqgoJe0TxEFlf3Dqrw=="],
|
"@solid-primitives/storage": ["@solid-primitives/storage@4.3.3", "", { "dependencies": { "@solid-primitives/utils": "^6.3.2" }, "peerDependencies": { "@tauri-apps/plugin-store": "*", "solid-js": "^1.6.12" }, "optionalPeers": ["@tauri-apps/plugin-store"] }, "sha512-ACbNwMZ1s8VAvld6EUXkDkX/US3IhtlPLxg6+B2s9MwNUugwdd51I98LPEaHrdLpqPmyzqgoJe0TxEFlf3Dqrw=="],
|
||||||
|
|
||||||
|
"@solid-primitives/timer": ["@solid-primitives/timer@1.4.4", "", { "peerDependencies": { "solid-js": "^1.6.12" } }, "sha512-Ayjyb3+v1hyU92vuLUN0tVHq2mmTCPGxSDLGJMsDydRqx9ZfJIc9xj6cxK4XvdY3pif3ps2mIv52pjgToybEpQ=="],
|
||||||
|
|
||||||
"@solid-primitives/trigger": ["@solid-primitives/trigger@1.2.3", "", { "dependencies": { "@solid-primitives/utils": "^6.4.0" }, "peerDependencies": { "solid-js": "^1.6.12" } }, "sha512-Za2JebEiDyfamjmDwRaESYqBBYOlgYGzB8kHYH0QrkXyLf2qNADlKdGN+z3vWSLCTDcKxChS43Kssjuc0OZhng=="],
|
"@solid-primitives/trigger": ["@solid-primitives/trigger@1.2.3", "", { "dependencies": { "@solid-primitives/utils": "^6.4.0" }, "peerDependencies": { "solid-js": "^1.6.12" } }, "sha512-Za2JebEiDyfamjmDwRaESYqBBYOlgYGzB8kHYH0QrkXyLf2qNADlKdGN+z3vWSLCTDcKxChS43Kssjuc0OZhng=="],
|
||||||
|
|
||||||
"@solid-primitives/utils": ["@solid-primitives/utils@6.4.0", "", { "peerDependencies": { "solid-js": "^1.6.12" } }, "sha512-AeGTBg8Wtkh/0s+evyLtP8piQoS4wyqqQaAFs2HJcFMMjYAtUgo+ZPduRXLjPlqKVc2ejeR544oeqpbn8Egn8A=="],
|
"@solid-primitives/utils": ["@solid-primitives/utils@6.4.0", "", { "peerDependencies": { "solid-js": "^1.6.12" } }, "sha512-AeGTBg8Wtkh/0s+evyLtP8piQoS4wyqqQaAFs2HJcFMMjYAtUgo+ZPduRXLjPlqKVc2ejeR544oeqpbn8Egn8A=="],
|
||||||
@@ -1965,10 +1983,14 @@
|
|||||||
|
|
||||||
"@tanstack/directive-functions-plugin": ["@tanstack/directive-functions-plugin@1.134.5", "", { "dependencies": { "@babel/code-frame": "7.27.1", "@babel/core": "^7.27.7", "@babel/traverse": "^7.27.7", "@babel/types": "^7.27.7", "@tanstack/router-utils": "1.133.19", "babel-dead-code-elimination": "^1.0.10", "pathe": "^2.0.3", "tiny-invariant": "^1.3.3" }, "peerDependencies": { "vite": ">=6.0.0 || >=7.0.0" } }, "sha512-J3oawV8uBRBbPoLgMdyHt+LxzTNuWRKNJJuCLWsm/yq6v0IQSvIVCgfD2+liIiSnDPxGZ8ExduPXy8IzS70eXw=="],
|
"@tanstack/directive-functions-plugin": ["@tanstack/directive-functions-plugin@1.134.5", "", { "dependencies": { "@babel/code-frame": "7.27.1", "@babel/core": "^7.27.7", "@babel/traverse": "^7.27.7", "@babel/types": "^7.27.7", "@tanstack/router-utils": "1.133.19", "babel-dead-code-elimination": "^1.0.10", "pathe": "^2.0.3", "tiny-invariant": "^1.3.3" }, "peerDependencies": { "vite": ">=6.0.0 || >=7.0.0" } }, "sha512-J3oawV8uBRBbPoLgMdyHt+LxzTNuWRKNJJuCLWsm/yq6v0IQSvIVCgfD2+liIiSnDPxGZ8ExduPXy8IzS70eXw=="],
|
||||||
|
|
||||||
|
"@tanstack/query-core": ["@tanstack/query-core@5.91.2", "", {}, "sha512-Uz2pTgPC1mhqrrSGg18RKCWT/pkduAYtxbcyIyKBhw7dTWjXZIzqmpzO2lBkyWr4hlImQgpu1m1pei3UnkFRWw=="],
|
||||||
|
|
||||||
"@tanstack/router-utils": ["@tanstack/router-utils@1.133.19", "", { "dependencies": { "@babel/core": "^7.27.4", "@babel/generator": "^7.27.5", "@babel/parser": "^7.27.5", "@babel/preset-typescript": "^7.27.1", "ansis": "^4.1.0", "diff": "^8.0.2", "pathe": "^2.0.3", "tinyglobby": "^0.2.15" } }, "sha512-WEp5D2gPxvlLDRXwD/fV7RXjYtqaqJNXKB/L6OyZEbT+9BG/Ib2d7oG9GSUZNNMGPGYAlhBUOi3xutySsk6rxA=="],
|
"@tanstack/router-utils": ["@tanstack/router-utils@1.133.19", "", { "dependencies": { "@babel/core": "^7.27.4", "@babel/generator": "^7.27.5", "@babel/parser": "^7.27.5", "@babel/preset-typescript": "^7.27.1", "ansis": "^4.1.0", "diff": "^8.0.2", "pathe": "^2.0.3", "tinyglobby": "^0.2.15" } }, "sha512-WEp5D2gPxvlLDRXwD/fV7RXjYtqaqJNXKB/L6OyZEbT+9BG/Ib2d7oG9GSUZNNMGPGYAlhBUOi3xutySsk6rxA=="],
|
||||||
|
|
||||||
"@tanstack/server-functions-plugin": ["@tanstack/server-functions-plugin@1.134.5", "", { "dependencies": { "@babel/code-frame": "7.27.1", "@babel/core": "^7.27.7", "@babel/plugin-syntax-jsx": "^7.27.1", "@babel/plugin-syntax-typescript": "^7.27.1", "@babel/template": "^7.27.2", "@babel/traverse": "^7.27.7", "@babel/types": "^7.27.7", "@tanstack/directive-functions-plugin": "1.134.5", "babel-dead-code-elimination": "^1.0.9", "tiny-invariant": "^1.3.3" } }, "sha512-2sWxq70T+dOEUlE3sHlXjEPhaFZfdPYlWTSkHchWXrFGw2YOAa+hzD6L9wHMjGDQezYd03ue8tQlHG+9Jzbzgw=="],
|
"@tanstack/server-functions-plugin": ["@tanstack/server-functions-plugin@1.134.5", "", { "dependencies": { "@babel/code-frame": "7.27.1", "@babel/core": "^7.27.7", "@babel/plugin-syntax-jsx": "^7.27.1", "@babel/plugin-syntax-typescript": "^7.27.1", "@babel/template": "^7.27.2", "@babel/traverse": "^7.27.7", "@babel/types": "^7.27.7", "@tanstack/directive-functions-plugin": "1.134.5", "babel-dead-code-elimination": "^1.0.9", "tiny-invariant": "^1.3.3" } }, "sha512-2sWxq70T+dOEUlE3sHlXjEPhaFZfdPYlWTSkHchWXrFGw2YOAa+hzD6L9wHMjGDQezYd03ue8tQlHG+9Jzbzgw=="],
|
||||||
|
|
||||||
|
"@tanstack/solid-query": ["@tanstack/solid-query@5.91.4", "", { "dependencies": { "@tanstack/query-core": "5.91.2" }, "peerDependencies": { "solid-js": "^1.6.0" } }, "sha512-oCEgn8iT7WnF/7ISd7usBpUK1C9EdvQfg8ZUpKNKZ4edVClICZrCX6f3/Bp8ZlwQnL21KLc2rp+CejEuehlRxg=="],
|
||||||
|
|
||||||
"@tauri-apps/api": ["@tauri-apps/api@2.10.1", "", {}, "sha512-hKL/jWf293UDSUN09rR69hrToyIXBb8CjGaWC7gfinvnQrBVvnLr08FeFi38gxtugAVyVcTa5/FD/Xnkb1siBw=="],
|
"@tauri-apps/api": ["@tauri-apps/api@2.10.1", "", {}, "sha512-hKL/jWf293UDSUN09rR69hrToyIXBb8CjGaWC7gfinvnQrBVvnLr08FeFi38gxtugAVyVcTa5/FD/Xnkb1siBw=="],
|
||||||
|
|
||||||
"@tauri-apps/cli": ["@tauri-apps/cli@2.10.1", "", { "optionalDependencies": { "@tauri-apps/cli-darwin-arm64": "2.10.1", "@tauri-apps/cli-darwin-x64": "2.10.1", "@tauri-apps/cli-linux-arm-gnueabihf": "2.10.1", "@tauri-apps/cli-linux-arm64-gnu": "2.10.1", "@tauri-apps/cli-linux-arm64-musl": "2.10.1", "@tauri-apps/cli-linux-riscv64-gnu": "2.10.1", "@tauri-apps/cli-linux-x64-gnu": "2.10.1", "@tauri-apps/cli-linux-x64-musl": "2.10.1", "@tauri-apps/cli-win32-arm64-msvc": "2.10.1", "@tauri-apps/cli-win32-ia32-msvc": "2.10.1", "@tauri-apps/cli-win32-x64-msvc": "2.10.1" }, "bin": { "tauri": "tauri.js" } }, "sha512-jQNGF/5quwORdZSSLtTluyKQ+o6SMa/AUICfhf4egCGFdMHqWssApVgYSbg+jmrZoc8e1DscNvjTnXtlHLS11g=="],
|
"@tauri-apps/cli": ["@tauri-apps/cli@2.10.1", "", { "optionalDependencies": { "@tauri-apps/cli-darwin-arm64": "2.10.1", "@tauri-apps/cli-darwin-x64": "2.10.1", "@tauri-apps/cli-linux-arm-gnueabihf": "2.10.1", "@tauri-apps/cli-linux-arm64-gnu": "2.10.1", "@tauri-apps/cli-linux-arm64-musl": "2.10.1", "@tauri-apps/cli-linux-riscv64-gnu": "2.10.1", "@tauri-apps/cli-linux-x64-gnu": "2.10.1", "@tauri-apps/cli-linux-x64-musl": "2.10.1", "@tauri-apps/cli-win32-arm64-msvc": "2.10.1", "@tauri-apps/cli-win32-ia32-msvc": "2.10.1", "@tauri-apps/cli-win32-x64-msvc": "2.10.1" }, "bin": { "tauri": "tauri.js" } }, "sha512-jQNGF/5quwORdZSSLtTluyKQ+o6SMa/AUICfhf4egCGFdMHqWssApVgYSbg+jmrZoc8e1DscNvjTnXtlHLS11g=="],
|
||||||
@@ -2051,7 +2073,7 @@
|
|||||||
|
|
||||||
"@types/braces": ["@types/braces@3.0.5", "", {}, "sha512-SQFof9H+LXeWNz8wDe7oN5zu7ket0qwMu5vZubW4GCJ8Kkeh6nBWUz87+KTz/G3Kqsrp0j/W253XJb3KMEeg3w=="],
|
"@types/braces": ["@types/braces@3.0.5", "", {}, "sha512-SQFof9H+LXeWNz8wDe7oN5zu7ket0qwMu5vZubW4GCJ8Kkeh6nBWUz87+KTz/G3Kqsrp0j/W253XJb3KMEeg3w=="],
|
||||||
|
|
||||||
"@types/bun": ["@types/bun@1.3.9", "", { "dependencies": { "bun-types": "1.3.9" } }, "sha512-KQ571yULOdWJiMH+RIWIOZ7B2RXQGpL1YQrBtLIV3FqDcCu6FsbFUBwhdKUlCKUpS3PJDsHlJ1QKlpxoVR+xtw=="],
|
"@types/bun": ["@types/bun@1.3.11", "", { "dependencies": { "bun-types": "1.3.11" } }, "sha512-5vPne5QvtpjGpsGYXiFyycfpDF2ECyPcTSsFBMa0fraoxiQyMJ3SmuQIGhzPg2WJuWxVBoxWJ2kClYTcw/4fAg=="],
|
||||||
|
|
||||||
"@types/cacheable-request": ["@types/cacheable-request@6.0.3", "", { "dependencies": { "@types/http-cache-semantics": "*", "@types/keyv": "^3.1.4", "@types/node": "*", "@types/responselike": "^1.0.0" } }, "sha512-IQ3EbTzGxIigb1I3qPZc1rWJnH0BmSKv5QYTalEwweFvyBDLSAe24zP0le/hyi7ecGfZVlIVAg4BZqb8WBwKqw=="],
|
"@types/cacheable-request": ["@types/cacheable-request@6.0.3", "", { "dependencies": { "@types/http-cache-semantics": "*", "@types/keyv": "^3.1.4", "@types/node": "*", "@types/responselike": "^1.0.0" } }, "sha512-IQ3EbTzGxIigb1I3qPZc1rWJnH0BmSKv5QYTalEwweFvyBDLSAe24zP0le/hyi7ecGfZVlIVAg4BZqb8WBwKqw=="],
|
||||||
|
|
||||||
@@ -2059,6 +2081,8 @@
|
|||||||
|
|
||||||
"@types/connect": ["@types/connect@3.4.38", "", { "dependencies": { "@types/node": "*" } }, "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug=="],
|
"@types/connect": ["@types/connect@3.4.38", "", { "dependencies": { "@types/node": "*" } }, "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug=="],
|
||||||
|
|
||||||
|
"@types/cross-spawn": ["@types/cross-spawn@6.0.6", "", { "dependencies": { "@types/node": "*" } }, "sha512-fXRhhUkG4H3TQk5dBhQ7m/JDdSNHKwR2BBia62lhwEIq9xGiQKLxd6LymNhn47SjXhsUEPmxi+PKw2OkW4LLjA=="],
|
||||||
|
|
||||||
"@types/debug": ["@types/debug@4.1.12", "", { "dependencies": { "@types/ms": "*" } }, "sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ=="],
|
"@types/debug": ["@types/debug@4.1.12", "", { "dependencies": { "@types/ms": "*" } }, "sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ=="],
|
||||||
|
|
||||||
"@types/deep-eql": ["@types/deep-eql@4.0.2", "", {}, "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw=="],
|
"@types/deep-eql": ["@types/deep-eql@4.0.2", "", {}, "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw=="],
|
||||||
@@ -2247,9 +2271,9 @@
|
|||||||
|
|
||||||
"agentkeepalive": ["agentkeepalive@4.6.0", "", { "dependencies": { "humanize-ms": "^1.2.1" } }, "sha512-kja8j7PjmncONqaTsB8fQ+wE2mSU2DJ9D4XKoJ5PFWIdRMa6SLSN1ff4mOr4jCbfRSsxR4keIiySJU0N9T5hIQ=="],
|
"agentkeepalive": ["agentkeepalive@4.6.0", "", { "dependencies": { "humanize-ms": "^1.2.1" } }, "sha512-kja8j7PjmncONqaTsB8fQ+wE2mSU2DJ9D4XKoJ5PFWIdRMa6SLSN1ff4mOr4jCbfRSsxR4keIiySJU0N9T5hIQ=="],
|
||||||
|
|
||||||
"ai": ["ai@5.0.124", "", { "dependencies": { "@ai-sdk/gateway": "2.0.30", "@ai-sdk/provider": "2.0.1", "@ai-sdk/provider-utils": "3.0.20", "@opentelemetry/api": "1.9.0" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-Li6Jw9F9qsvFJXZPBfxj38ddP2iURCnMs96f9Q3OeQzrDVcl1hvtwSEAuxA/qmfh6SDV2ERqFUOFzigvr0697g=="],
|
"ai": ["ai@6.0.138", "", { "dependencies": { "@ai-sdk/gateway": "3.0.80", "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.21", "@opentelemetry/api": "1.9.0" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-49OfPe0f5uxJ6jUdA5BBXjIinP6+ZdYfAtpF2aEH64GA5wPcxH2rf/TBUQQ0bbamBz/D+TLMV18xilZqOC+zaA=="],
|
||||||
|
|
||||||
"ai-gateway-provider": ["ai-gateway-provider@2.3.1", "", { "dependencies": { "@ai-sdk/provider": "^2.0.0", "@ai-sdk/provider-utils": "^3.0.19", "ai": "^5.0.116" }, "optionalDependencies": { "@ai-sdk/amazon-bedrock": "^3.0.71", "@ai-sdk/anthropic": "^2.0.56", "@ai-sdk/azure": "^2.0.90", "@ai-sdk/cerebras": "^1.0.33", "@ai-sdk/cohere": "^2.0.21", "@ai-sdk/deepgram": "^1.0.21", "@ai-sdk/deepseek": "^1.0.32", "@ai-sdk/elevenlabs": "^1.0.21", "@ai-sdk/fireworks": "^1.0.30", "@ai-sdk/google": "^2.0.51", "@ai-sdk/google-vertex": "3.0.90", "@ai-sdk/groq": "^2.0.33", "@ai-sdk/mistral": "^2.0.26", "@ai-sdk/openai": "^2.0.88", "@ai-sdk/perplexity": "^2.0.22", "@ai-sdk/xai": "^2.0.42", "@openrouter/ai-sdk-provider": "^1.5.3" }, "peerDependencies": { "@ai-sdk/openai-compatible": "^1.0.29" } }, "sha512-PqI6TVNEDNwr7kOhy7XUGnA8XJB1SpeA9aLqGjr0CyWkKgH+y+ofPm8MZGZ74DOwVejDF+POZq0Qs9jKEKUeYg=="],
|
"ai-gateway-provider": ["ai-gateway-provider@3.1.2", "", { "optionalDependencies": { "@ai-sdk/amazon-bedrock": "^4.0.62", "@ai-sdk/anthropic": "^3.0.46", "@ai-sdk/azure": "^3.0.31", "@ai-sdk/cerebras": "^2.0.34", "@ai-sdk/cohere": "^3.0.21", "@ai-sdk/deepgram": "^2.0.20", "@ai-sdk/deepseek": "^2.0.20", "@ai-sdk/elevenlabs": "^2.0.20", "@ai-sdk/fireworks": "^2.0.34", "@ai-sdk/google": "^3.0.30", "@ai-sdk/google-vertex": "^4.0.61", "@ai-sdk/groq": "^3.0.24", "@ai-sdk/mistral": "^3.0.20", "@ai-sdk/openai": "^3.0.30", "@ai-sdk/perplexity": "^3.0.19", "@ai-sdk/xai": "^3.0.57", "@openrouter/ai-sdk-provider": "^2.2.3" }, "peerDependencies": { "@ai-sdk/openai-compatible": "^2.0.0", "@ai-sdk/provider": "^3.0.0", "@ai-sdk/provider-utils": "^4.0.0", "ai": "^6.0.0" } }, "sha512-krGNnJSoO/gJ7Hbe5nQDlsBpDUGIBGtMQTRUaW7s1MylsfvLduba0TLWzQaGtOmNRkP0pGhtGlwsnS6FNQMlyw=="],
|
||||||
|
|
||||||
"ajv": ["ajv@8.18.0", "", { "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", "json-schema-traverse": "^1.0.0", "require-from-string": "^2.0.2" } }, "sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A=="],
|
"ajv": ["ajv@8.18.0", "", { "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", "json-schema-traverse": "^1.0.0", "require-from-string": "^2.0.2" } }, "sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A=="],
|
||||||
|
|
||||||
@@ -2445,7 +2469,7 @@
|
|||||||
|
|
||||||
"bun-pty": ["bun-pty@0.4.8", "", {}, "sha512-rO70Mrbr13+jxHHHu2YBkk2pNqrJE5cJn29WE++PUr+GFA0hq/VgtQPZANJ8dJo6d7XImvBk37Innt8GM7O28w=="],
|
"bun-pty": ["bun-pty@0.4.8", "", {}, "sha512-rO70Mrbr13+jxHHHu2YBkk2pNqrJE5cJn29WE++PUr+GFA0hq/VgtQPZANJ8dJo6d7XImvBk37Innt8GM7O28w=="],
|
||||||
|
|
||||||
"bun-types": ["bun-types@1.3.9", "", { "dependencies": { "@types/node": "*" } }, "sha512-+UBWWOakIP4Tswh0Bt0QD0alpTY8cb5hvgiYeWCMet9YukHbzuruIEeXC2D7nMJPB12kbh8C7XJykSexEqGKJg=="],
|
"bun-types": ["bun-types@1.3.11", "", { "dependencies": { "@types/node": "*" } }, "sha512-1KGPpoxQWl9f6wcZh57LvrPIInQMn2TQ7jsgxqpRzg+l0QPOFvJVH7HmvHo/AiPgwXy+/Thf6Ov3EdVn1vOabg=="],
|
||||||
|
|
||||||
"bun-webgpu": ["bun-webgpu@0.1.5", "", { "dependencies": { "@webgpu/types": "^0.1.60" }, "optionalDependencies": { "bun-webgpu-darwin-arm64": "^0.1.5", "bun-webgpu-darwin-x64": "^0.1.5", "bun-webgpu-linux-x64": "^0.1.5", "bun-webgpu-win32-x64": "^0.1.5" } }, "sha512-91/K6S5whZKX7CWAm9AylhyKrLGRz6BUiiPiM/kXadSnD4rffljCD/q9cNFftm5YXhx4MvLqw33yEilxogJvwA=="],
|
"bun-webgpu": ["bun-webgpu@0.1.5", "", { "dependencies": { "@webgpu/types": "^0.1.60" }, "optionalDependencies": { "bun-webgpu-darwin-arm64": "^0.1.5", "bun-webgpu-darwin-x64": "^0.1.5", "bun-webgpu-linux-x64": "^0.1.5", "bun-webgpu-win32-x64": "^0.1.5" } }, "sha512-91/K6S5whZKX7CWAm9AylhyKrLGRz6BUiiPiM/kXadSnD4rffljCD/q9cNFftm5YXhx4MvLqw33yEilxogJvwA=="],
|
||||||
|
|
||||||
@@ -2731,9 +2755,9 @@
|
|||||||
|
|
||||||
"dotenv-expand": ["dotenv-expand@11.0.7", "", { "dependencies": { "dotenv": "^16.4.5" } }, "sha512-zIHwmZPRshsCdpMDyVsqGmgyP0yT8GAgXUnkdAoJisxvf33k7yO6OuoKmcTGuXPWSsm8Oh88nZicRLA9Y0rUeA=="],
|
"dotenv-expand": ["dotenv-expand@11.0.7", "", { "dependencies": { "dotenv": "^16.4.5" } }, "sha512-zIHwmZPRshsCdpMDyVsqGmgyP0yT8GAgXUnkdAoJisxvf33k7yO6OuoKmcTGuXPWSsm8Oh88nZicRLA9Y0rUeA=="],
|
||||||
|
|
||||||
"drizzle-kit": ["drizzle-kit@1.0.0-beta.16-ea816b6", "", { "dependencies": { "@drizzle-team/brocli": "^0.11.0", "@js-temporal/polyfill": "^0.5.1", "esbuild": "^0.25.10", "jiti": "^2.6.1" }, "bin": { "drizzle-kit": "bin.cjs" } }, "sha512-GiJQqCNPZP8Kk+i7/sFa3rtXbq26tLDNi3LbMx9aoLuwF2ofk8CS7cySUGdI+r4J3q0a568quC8FZeaFTCw4IA=="],
|
"drizzle-kit": ["drizzle-kit@1.0.0-beta.19-d95b7a4", "", { "dependencies": { "@drizzle-team/brocli": "^0.11.0", "@js-temporal/polyfill": "^0.5.1", "esbuild": "^0.25.10", "get-tsconfig": "^4.13.6", "jiti": "^2.6.1" }, "bin": { "drizzle-kit": "bin.cjs" } }, "sha512-M0sqc+42TYBod6kEZ3AsW6+JWe3+76gR1aDFbHH5DmuLKEwewmbzlhBG6qnvV6YA1cIIbkuam3dC7r6PREOCXw=="],
|
||||||
|
|
||||||
"drizzle-orm": ["drizzle-orm@1.0.0-beta.16-ea816b6", "", { "peerDependencies": { "@aws-sdk/client-rds-data": ">=3", "@cloudflare/workers-types": ">=4", "@effect/sql": "^0.48.5", "@effect/sql-pg": "^0.49.7", "@electric-sql/pglite": ">=0.2.0", "@libsql/client": ">=0.10.0", "@libsql/client-wasm": ">=0.10.0", "@neondatabase/serverless": ">=0.10.0", "@op-engineering/op-sqlite": ">=2", "@opentelemetry/api": "^1.4.1", "@planetscale/database": ">=1.13", "@prisma/client": "*", "@sinclair/typebox": ">=0.34.8", "@sqlitecloud/drivers": ">=1.0.653", "@tidbcloud/serverless": "*", "@tursodatabase/database": ">=0.2.1", "@tursodatabase/database-common": ">=0.2.1", "@tursodatabase/database-wasm": ">=0.2.1", "@types/better-sqlite3": "*", "@types/mssql": "^9.1.4", "@types/pg": "*", "@types/sql.js": "*", "@upstash/redis": ">=1.34.7", "@vercel/postgres": ">=0.8.0", "@xata.io/client": "*", "arktype": ">=2.0.0", "better-sqlite3": ">=9.3.0", "bun-types": "*", "expo-sqlite": ">=14.0.0", "gel": ">=2", "mssql": "^11.0.1", "mysql2": ">=2", "pg": ">=8", "postgres": ">=3", "sql.js": ">=1", "sqlite3": ">=5", "typebox": ">=1.0.0", "valibot": ">=1.0.0-beta.7", "zod": "^3.25.0 || ^4.0.0" }, "optionalPeers": ["@aws-sdk/client-rds-data", "@cloudflare/workers-types", "@effect/sql", "@effect/sql-pg", "@electric-sql/pglite", "@libsql/client", "@libsql/client-wasm", "@neondatabase/serverless", "@op-engineering/op-sqlite", "@opentelemetry/api", "@planetscale/database", "@prisma/client", "@sinclair/typebox", "@sqlitecloud/drivers", "@tidbcloud/serverless", "@tursodatabase/database", "@tursodatabase/database-common", "@tursodatabase/database-wasm", "@types/better-sqlite3", "@types/pg", "@types/sql.js", "@upstash/redis", "@vercel/postgres", "@xata.io/client", "arktype", "better-sqlite3", "bun-types", "expo-sqlite", "gel", "mysql2", "pg", "postgres", "sql.js", "sqlite3", "typebox", "valibot", "zod"] }, "sha512-k9gT4f0O9Qvah5YK/zL+FZonQ8TPyVxcG/ojN4dzO0fHP8hs8tBno8lqmJo53g0JLWv3Q2nsTUoyBRKM2TljFw=="],
|
"drizzle-orm": ["drizzle-orm@1.0.0-beta.19-d95b7a4", "", { "peerDependencies": { "@aws-sdk/client-rds-data": ">=3", "@cloudflare/workers-types": ">=4", "@effect/sql": "^0.48.5", "@effect/sql-pg": "^0.49.7", "@electric-sql/pglite": ">=0.2.0", "@libsql/client": ">=0.10.0", "@libsql/client-wasm": ">=0.10.0", "@neondatabase/serverless": ">=0.10.0", "@op-engineering/op-sqlite": ">=2", "@opentelemetry/api": "^1.4.1", "@planetscale/database": ">=1.13", "@sinclair/typebox": ">=0.34.8", "@sqlitecloud/drivers": ">=1.0.653", "@tidbcloud/serverless": "*", "@tursodatabase/database": ">=0.2.1", "@tursodatabase/database-common": ">=0.2.1", "@tursodatabase/database-wasm": ">=0.2.1", "@types/better-sqlite3": "*", "@types/mssql": "^9.1.4", "@types/pg": "*", "@types/sql.js": "*", "@upstash/redis": ">=1.34.7", "@vercel/postgres": ">=0.8.0", "@xata.io/client": "*", "arktype": ">=2.0.0", "better-sqlite3": ">=9.3.0", "bun-types": "*", "expo-sqlite": ">=14.0.0", "gel": ">=2", "mssql": "^11.0.1", "mysql2": ">=2", "pg": ">=8", "postgres": ">=3", "sql.js": ">=1", "sqlite3": ">=5", "typebox": ">=1.0.0", "valibot": ">=1.0.0-beta.7", "zod": "^3.25.0 || ^4.0.0" }, "optionalPeers": ["@aws-sdk/client-rds-data", "@cloudflare/workers-types", "@effect/sql", "@effect/sql-pg", "@electric-sql/pglite", "@libsql/client", "@libsql/client-wasm", "@neondatabase/serverless", "@op-engineering/op-sqlite", "@opentelemetry/api", "@planetscale/database", "@sinclair/typebox", "@sqlitecloud/drivers", "@tidbcloud/serverless", "@tursodatabase/database", "@tursodatabase/database-common", "@tursodatabase/database-wasm", "@types/better-sqlite3", "@types/pg", "@types/sql.js", "@upstash/redis", "@vercel/postgres", "@xata.io/client", "arktype", "better-sqlite3", "bun-types", "expo-sqlite", "gel", "mysql2", "pg", "postgres", "sql.js", "sqlite3", "typebox", "valibot", "zod"] }, "sha512-bZZKKeoRKrMVU6zKTscjrSH0+WNb1WEi3N0Jl4wEyQ7aQpTgHzdYY6IJQ1P0M74HuSJVeX4UpkFB/S6dtqLEJg=="],
|
||||||
|
|
||||||
"dset": ["dset@3.1.4", "", {}, "sha512-2QF/g9/zTaPDc3BjNcVTGoBbXBgYfMTTceLaYcFJ/W9kggFUkhxD/hMEeuLKbugyef9SqAx8cpgwlIP/jinUTA=="],
|
"dset": ["dset@3.1.4", "", {}, "sha512-2QF/g9/zTaPDc3BjNcVTGoBbXBgYfMTTceLaYcFJ/W9kggFUkhxD/hMEeuLKbugyef9SqAx8cpgwlIP/jinUTA=="],
|
||||||
|
|
||||||
@@ -2747,7 +2771,7 @@
|
|||||||
|
|
||||||
"ee-first": ["ee-first@1.1.1", "", {}, "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow=="],
|
"ee-first": ["ee-first@1.1.1", "", {}, "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow=="],
|
||||||
|
|
||||||
"effect": ["effect@4.0.0-beta.31", "", { "dependencies": { "@standard-schema/spec": "^1.1.0", "fast-check": "^4.5.3", "find-my-way-ts": "^0.1.6", "ini": "^6.0.0", "kubernetes-types": "^1.30.0", "msgpackr": "^1.11.8", "multipasta": "^0.2.7", "toml": "^3.0.0", "uuid": "^13.0.0", "yaml": "^2.8.2" } }, "sha512-w3QwJnlaLtWWiUSzhCXUTIisnULPsxLzpO6uqaBFjXybKx6FvCqsLJT6v4dV7G9eA9jeTtG6Gv7kF+jGe3HxzA=="],
|
"effect": ["effect@4.0.0-beta.42", "", { "dependencies": { "@standard-schema/spec": "^1.1.0", "fast-check": "^4.5.3", "find-my-way-ts": "^0.1.6", "ini": "^6.0.0", "kubernetes-types": "^1.30.0", "msgpackr": "^1.11.8", "multipasta": "^0.2.7", "toml": "^3.0.0", "uuid": "^13.0.0", "yaml": "^2.8.2" } }, "sha512-c1UrRP+tLzyHb4Fepl8XBDJlLQLkrcMXrRBba441GQRxMbeQ/aIOSFcBwSda1iMJ5l9F0lYc3Bhe33/whrmavQ=="],
|
||||||
|
|
||||||
"ejs": ["ejs@3.1.10", "", { "dependencies": { "jake": "^10.8.5" }, "bin": { "ejs": "bin/cli.js" } }, "sha512-UeJmFfOrAQS8OJWPZ4qtgHyWExa088/MtK5UEyoJGFH67cDEXkZSviOiKRCZ4Xij0zxI3JECgYs3oKx+AizQBA=="],
|
"ejs": ["ejs@3.1.10", "", { "dependencies": { "jake": "^10.8.5" }, "bin": { "ejs": "bin/cli.js" } }, "sha512-UeJmFfOrAQS8OJWPZ4qtgHyWExa088/MtK5UEyoJGFH67cDEXkZSviOiKRCZ4Xij0zxI3JECgYs3oKx+AizQBA=="],
|
||||||
|
|
||||||
@@ -2877,7 +2901,7 @@
|
|||||||
|
|
||||||
"express": ["express@4.22.1", "", { "dependencies": { "accepts": "~1.3.8", "array-flatten": "1.1.1", "body-parser": "~1.20.3", "content-disposition": "~0.5.4", "content-type": "~1.0.4", "cookie": "~0.7.1", "cookie-signature": "~1.0.6", "debug": "2.6.9", "depd": "2.0.0", "encodeurl": "~2.0.0", "escape-html": "~1.0.3", "etag": "~1.8.1", "finalhandler": "~1.3.1", "fresh": "~0.5.2", "http-errors": "~2.0.0", "merge-descriptors": "1.0.3", "methods": "~1.1.2", "on-finished": "~2.4.1", "parseurl": "~1.3.3", "path-to-regexp": "~0.1.12", "proxy-addr": "~2.0.7", "qs": "~6.14.0", "range-parser": "~1.2.1", "safe-buffer": "5.2.1", "send": "~0.19.0", "serve-static": "~1.16.2", "setprototypeof": "1.2.0", "statuses": "~2.0.1", "type-is": "~1.6.18", "utils-merge": "1.0.1", "vary": "~1.1.2" } }, "sha512-F2X8g9P1X7uCPZMA3MVf9wcTqlyNp7IhH5qPCI0izhaOIYXaW9L535tGA3qmjRzpH+bZczqq7hVKxTR4NWnu+g=="],
|
"express": ["express@4.22.1", "", { "dependencies": { "accepts": "~1.3.8", "array-flatten": "1.1.1", "body-parser": "~1.20.3", "content-disposition": "~0.5.4", "content-type": "~1.0.4", "cookie": "~0.7.1", "cookie-signature": "~1.0.6", "debug": "2.6.9", "depd": "2.0.0", "encodeurl": "~2.0.0", "escape-html": "~1.0.3", "etag": "~1.8.1", "finalhandler": "~1.3.1", "fresh": "~0.5.2", "http-errors": "~2.0.0", "merge-descriptors": "1.0.3", "methods": "~1.1.2", "on-finished": "~2.4.1", "parseurl": "~1.3.3", "path-to-regexp": "~0.1.12", "proxy-addr": "~2.0.7", "qs": "~6.14.0", "range-parser": "~1.2.1", "safe-buffer": "5.2.1", "send": "~0.19.0", "serve-static": "~1.16.2", "setprototypeof": "1.2.0", "statuses": "~2.0.1", "type-is": "~1.6.18", "utils-merge": "1.0.1", "vary": "~1.1.2" } }, "sha512-F2X8g9P1X7uCPZMA3MVf9wcTqlyNp7IhH5qPCI0izhaOIYXaW9L535tGA3qmjRzpH+bZczqq7hVKxTR4NWnu+g=="],
|
||||||
|
|
||||||
"express-rate-limit": ["express-rate-limit@7.5.1", "", { "peerDependencies": { "express": ">= 4.11" } }, "sha512-7iN8iPMDzOMHPUYllBEsQdWVB6fPDMPqwjBaFrgr4Jgr/+okjvzAy+UHlYYL/Vs0OsOrMkwS6PJDkFlJwoxUnw=="],
|
"express-rate-limit": ["express-rate-limit@8.3.1", "", { "dependencies": { "ip-address": "10.1.0" }, "peerDependencies": { "express": ">= 4.11" } }, "sha512-D1dKN+cmyPWuvB+G2SREQDzPY1agpBIcTa9sJxOPMCNeH3gwzhqJRDWCXW3gg0y//+LQ/8j52JbMROWyrKdMdw=="],
|
||||||
|
|
||||||
"expressive-code": ["expressive-code@0.41.7", "", { "dependencies": { "@expressive-code/core": "^0.41.7", "@expressive-code/plugin-frames": "^0.41.7", "@expressive-code/plugin-shiki": "^0.41.7", "@expressive-code/plugin-text-markers": "^0.41.7" } }, "sha512-2wZjC8OQ3TaVEMcBtYY4Va3lo6J+Ai9jf3d4dbhURMJcU4Pbqe6EcHe424MIZI0VHUA1bR6xdpoHYi3yxokWqA=="],
|
"expressive-code": ["expressive-code@0.41.7", "", { "dependencies": { "@expressive-code/core": "^0.41.7", "@expressive-code/plugin-frames": "^0.41.7", "@expressive-code/plugin-shiki": "^0.41.7", "@expressive-code/plugin-text-markers": "^0.41.7" } }, "sha512-2wZjC8OQ3TaVEMcBtYY4Va3lo6J+Ai9jf3d4dbhURMJcU4Pbqe6EcHe424MIZI0VHUA1bR6xdpoHYi3yxokWqA=="],
|
||||||
|
|
||||||
@@ -3015,6 +3039,8 @@
|
|||||||
|
|
||||||
"get-symbol-description": ["get-symbol-description@1.1.0", "", { "dependencies": { "call-bound": "^1.0.3", "es-errors": "^1.3.0", "get-intrinsic": "^1.2.6" } }, "sha512-w9UMqWwJxHNOvoNzSJ2oPF5wvYcvP7jUvYzhp67yEhTi17ZDBBC1z9pTdGuzjD+EFIqLSYRweZjqfiPzQ06Ebg=="],
|
"get-symbol-description": ["get-symbol-description@1.1.0", "", { "dependencies": { "call-bound": "^1.0.3", "es-errors": "^1.3.0", "get-intrinsic": "^1.2.6" } }, "sha512-w9UMqWwJxHNOvoNzSJ2oPF5wvYcvP7jUvYzhp67yEhTi17ZDBBC1z9pTdGuzjD+EFIqLSYRweZjqfiPzQ06Ebg=="],
|
||||||
|
|
||||||
|
"get-tsconfig": ["get-tsconfig@4.13.6", "", { "dependencies": { "resolve-pkg-maps": "^1.0.0" } }, "sha512-shZT/QMiSHc/YBLxxOkMtgSid5HFoauqCE3/exfsEcwg1WkeqjG+V40yBbBrsD+jW2HDXcs28xOfcbm2jI8Ddw=="],
|
||||||
|
|
||||||
"ghostty-web": ["ghostty-web@github:anomalyco/ghostty-web#4af877d", {}, "anomalyco-ghostty-web-4af877d", "sha512-fbEK8mtr7ar4ySsF+JUGjhaZrane7dKphanN+SxHt5XXI6yLMAh/Hpf6sNCOyyVa2UlGCd7YpXG/T2v2RUAX+A=="],
|
"ghostty-web": ["ghostty-web@github:anomalyco/ghostty-web#4af877d", {}, "anomalyco-ghostty-web-4af877d", "sha512-fbEK8mtr7ar4ySsF+JUGjhaZrane7dKphanN+SxHt5XXI6yLMAh/Hpf6sNCOyyVa2UlGCd7YpXG/T2v2RUAX+A=="],
|
||||||
|
|
||||||
"gifwrap": ["gifwrap@0.10.1", "", { "dependencies": { "image-q": "^4.0.0", "omggif": "^1.0.10" } }, "sha512-2760b1vpJHNmLzZ/ubTtNnEx5WApN/PYWJvXvgS+tL1egTTthayFYIQQNi136FLEDcN/IyEY2EcGpIITD6eYUw=="],
|
"gifwrap": ["gifwrap@0.10.1", "", { "dependencies": { "image-q": "^4.0.0", "omggif": "^1.0.10" } }, "sha512-2760b1vpJHNmLzZ/ubTtNnEx5WApN/PYWJvXvgS+tL1egTTthayFYIQQNi136FLEDcN/IyEY2EcGpIITD6eYUw=="],
|
||||||
@@ -3023,6 +3049,8 @@
|
|||||||
|
|
||||||
"github-slugger": ["github-slugger@2.0.0", "", {}, "sha512-IaOQ9puYtjrkq7Y0Ygl9KDZnrf/aiUJYUpVf89y8kyaxbRG7Y1SrX/jaumrv81vc61+kiMempujsM3Yw7w5qcw=="],
|
"github-slugger": ["github-slugger@2.0.0", "", {}, "sha512-IaOQ9puYtjrkq7Y0Ygl9KDZnrf/aiUJYUpVf89y8kyaxbRG7Y1SrX/jaumrv81vc61+kiMempujsM3Yw7w5qcw=="],
|
||||||
|
|
||||||
|
"gitlab-ai-provider": ["gitlab-ai-provider@6.0.0", "", { "dependencies": { "@anthropic-ai/sdk": "^0.71.0", "@anycable/core": "^0.9.2", "graphql-request": "^6.1.0", "isomorphic-ws": "^5.0.0", "openai": "^6.16.0", "socket.io-client": "^4.8.1", "vscode-jsonrpc": "^8.2.1", "zod": "^3.25.76" }, "peerDependencies": { "@ai-sdk/provider": ">=3.0.0", "@ai-sdk/provider-utils": ">=4.0.0" } }, "sha512-683GcJdrer/GhnljkbVcGsndCEhvGB8f9fUdCxQBlkuyt8rzf0G9DpSh+iMBYp9HpcSvYmYG0Qv5ks9dLrNxwQ=="],
|
||||||
|
|
||||||
"glob": ["glob@13.0.5", "", { "dependencies": { "minimatch": "^10.2.1", "minipass": "^7.1.2", "path-scurry": "^2.0.0" } }, "sha512-BzXxZg24Ibra1pbQ/zE7Kys4Ua1ks7Bn6pKLkVPZ9FZe4JQS6/Q7ef3LG1H+k7lUf5l4T3PLSyYyYJVYUvfgTw=="],
|
"glob": ["glob@13.0.5", "", { "dependencies": { "minimatch": "^10.2.1", "minipass": "^7.1.2", "path-scurry": "^2.0.0" } }, "sha512-BzXxZg24Ibra1pbQ/zE7Kys4Ua1ks7Bn6pKLkVPZ9FZe4JQS6/Q7ef3LG1H+k7lUf5l4T3PLSyYyYJVYUvfgTw=="],
|
||||||
|
|
||||||
"glob-parent": ["glob-parent@5.1.2", "", { "dependencies": { "is-glob": "^4.0.1" } }, "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow=="],
|
"glob-parent": ["glob-parent@5.1.2", "", { "dependencies": { "is-glob": "^4.0.1" } }, "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow=="],
|
||||||
@@ -3775,6 +3803,10 @@
|
|||||||
|
|
||||||
"opencode": ["opencode@workspace:packages/opencode"],
|
"opencode": ["opencode@workspace:packages/opencode"],
|
||||||
|
|
||||||
|
"opencode-gitlab-auth": ["opencode-gitlab-auth@2.0.1", "", { "dependencies": { "@fastify/rate-limit": "^10.2.0", "@opencode-ai/plugin": "*", "fastify": "^5.2.0", "open": "^10.0.0" } }, "sha512-1EMZHdbADLMVaTVLQ6C/V8uVMDr6MP++osj2lmOecowtn46AafP/w6ADkV4AN/ddjA1rob5cWpMuf/iME6DI6A=="],
|
||||||
|
|
||||||
|
"opencode-poe-auth": ["opencode-poe-auth@0.0.1", "", { "dependencies": { "open": "^10.0.0", "poe-oauth": "*" }, "peerDependencies": { "@opencode-ai/plugin": "*" } }, "sha512-cXqTlS6AXHzo1oBdosnxbT47ZJEZ9WXn050X8Re6wZ1vaNnTpB/l2fMQt90evT7RBK0fB8UjXQUDMKyd7bbiqg=="],
|
||||||
|
|
||||||
"opencontrol": ["opencontrol@0.0.6", "", { "dependencies": { "@modelcontextprotocol/sdk": "1.6.1", "@tsconfig/bun": "1.0.7", "hono": "4.7.4", "zod": "3.24.2", "zod-to-json-schema": "3.24.3" }, "bin": { "opencontrol": "bin/index.mjs" } }, "sha512-QeCrpOK5D15QV8kjnGVeD/BHFLwcVr+sn4T6KKmP0WAMs2pww56e4h+eOGHb5iPOufUQXbdbBKi6WV2kk7tefQ=="],
|
"opencontrol": ["opencontrol@0.0.6", "", { "dependencies": { "@modelcontextprotocol/sdk": "1.6.1", "@tsconfig/bun": "1.0.7", "hono": "4.7.4", "zod": "3.24.2", "zod-to-json-schema": "3.24.3" }, "bin": { "opencontrol": "bin/index.mjs" } }, "sha512-QeCrpOK5D15QV8kjnGVeD/BHFLwcVr+sn4T6KKmP0WAMs2pww56e4h+eOGHb5iPOufUQXbdbBKi6WV2kk7tefQ=="],
|
||||||
|
|
||||||
"openid-client": ["openid-client@5.6.4", "", { "dependencies": { "jose": "^4.15.4", "lru-cache": "^6.0.0", "object-hash": "^2.2.0", "oidc-token-hash": "^5.0.3" } }, "sha512-T1h3B10BRPKfcObdBklX639tVz+xh34O7GjofqrqiAQdm7eHsQ00ih18x6wuJ/E6FxdtS2u3FmUGPDeEcMwzNA=="],
|
"openid-client": ["openid-client@5.6.4", "", { "dependencies": { "jose": "^4.15.4", "lru-cache": "^6.0.0", "object-hash": "^2.2.0", "oidc-token-hash": "^5.0.3" } }, "sha512-T1h3B10BRPKfcObdBklX639tVz+xh34O7GjofqrqiAQdm7eHsQ00ih18x6wuJ/E6FxdtS2u3FmUGPDeEcMwzNA=="],
|
||||||
@@ -3815,7 +3847,7 @@
|
|||||||
|
|
||||||
"pagefind": ["pagefind@1.4.0", "", { "optionalDependencies": { "@pagefind/darwin-arm64": "1.4.0", "@pagefind/darwin-x64": "1.4.0", "@pagefind/freebsd-x64": "1.4.0", "@pagefind/linux-arm64": "1.4.0", "@pagefind/linux-x64": "1.4.0", "@pagefind/windows-x64": "1.4.0" }, "bin": { "pagefind": "lib/runner/bin.cjs" } }, "sha512-z2kY1mQlL4J8q5EIsQkLzQjilovKzfNVhX8De6oyE6uHpfFtyBaqUpcl/XzJC/4fjD8vBDyh1zolimIcVrCn9g=="],
|
"pagefind": ["pagefind@1.4.0", "", { "optionalDependencies": { "@pagefind/darwin-arm64": "1.4.0", "@pagefind/darwin-x64": "1.4.0", "@pagefind/freebsd-x64": "1.4.0", "@pagefind/linux-arm64": "1.4.0", "@pagefind/linux-x64": "1.4.0", "@pagefind/windows-x64": "1.4.0" }, "bin": { "pagefind": "lib/runner/bin.cjs" } }, "sha512-z2kY1mQlL4J8q5EIsQkLzQjilovKzfNVhX8De6oyE6uHpfFtyBaqUpcl/XzJC/4fjD8vBDyh1zolimIcVrCn9g=="],
|
||||||
|
|
||||||
"pako": ["pako@0.2.9", "", {}, "sha512-NUcwaKxUxWrZLpDG+z/xZaCgQITkA/Dv4V/T6bw7VON6l1Xz/VnrBqrYjZQ12TamKHzITTfOEIYUj48y2KXImA=="],
|
"pako": ["pako@1.0.11", "", {}, "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw=="],
|
||||||
|
|
||||||
"param-case": ["param-case@3.0.4", "", { "dependencies": { "dot-case": "^3.0.4", "tslib": "^2.0.3" } }, "sha512-RXlj7zCYokReqWpOPH9oYivUzLYZ5vAPIfEmCTNViosC78F8F0H9y7T7gG2M39ymgutxF5gcFEsyZQSph9Bp3A=="],
|
"param-case": ["param-case@3.0.4", "", { "dependencies": { "dot-case": "^3.0.4", "tslib": "^2.0.3" } }, "sha512-RXlj7zCYokReqWpOPH9oYivUzLYZ5vAPIfEmCTNViosC78F8F0H9y7T7gG2M39ymgutxF5gcFEsyZQSph9Bp3A=="],
|
||||||
|
|
||||||
@@ -3907,6 +3939,8 @@
|
|||||||
|
|
||||||
"pngjs": ["pngjs@7.0.0", "", {}, "sha512-LKWqWJRhstyYo9pGvgor/ivk2w94eSjE3RGVuzLGlr3NmD8bf7RcYGze1mNdEHRP6TRP6rMuDHk5t44hnTRyow=="],
|
"pngjs": ["pngjs@7.0.0", "", {}, "sha512-LKWqWJRhstyYo9pGvgor/ivk2w94eSjE3RGVuzLGlr3NmD8bf7RcYGze1mNdEHRP6TRP6rMuDHk5t44hnTRyow=="],
|
||||||
|
|
||||||
|
"poe-oauth": ["poe-oauth@0.0.3", "", {}, "sha512-KgxDylcuq/mov8URSplrBGjrIjkQwjN/Ml8BhqaGsAvHzYN3yhuROdv1sDRfwqncg7TT8XzJvMeJAWmv/4NDLw=="],
|
||||||
|
|
||||||
"possible-typed-array-names": ["possible-typed-array-names@1.1.0", "", {}, "sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg=="],
|
"possible-typed-array-names": ["possible-typed-array-names@1.1.0", "", {}, "sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg=="],
|
||||||
|
|
||||||
"postcss": ["postcss@8.5.8", "", { "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-OW/rX8O/jXnm82Ey1k44pObPtdblfiuWnrd8X7GJ7emImCOstunGbXUpp7HdBrFQX6rJzn3sPT397Wp5aCwCHg=="],
|
"postcss": ["postcss@8.5.8", "", { "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-OW/rX8O/jXnm82Ey1k44pObPtdblfiuWnrd8X7GJ7emImCOstunGbXUpp7HdBrFQX6rJzn3sPT397Wp5aCwCHg=="],
|
||||||
@@ -4085,6 +4119,8 @@
|
|||||||
|
|
||||||
"remeda": ["remeda@2.26.0", "", { "dependencies": { "type-fest": "^4.41.0" } }, "sha512-lmNNwtaC6Co4m0WTTNoZ/JlpjEqAjPZO0+czC9YVRQUpkbS4x8Hmh+Mn9HPfJfiXqUQ5IXXgSXSOB2pBKAytdA=="],
|
"remeda": ["remeda@2.26.0", "", { "dependencies": { "type-fest": "^4.41.0" } }, "sha512-lmNNwtaC6Co4m0WTTNoZ/JlpjEqAjPZO0+czC9YVRQUpkbS4x8Hmh+Mn9HPfJfiXqUQ5IXXgSXSOB2pBKAytdA=="],
|
||||||
|
|
||||||
|
"remend": ["remend@1.3.0", "", {}, "sha512-iIhggPkhW3hFImKtB10w0dz4EZbs28mV/dmbcYVonWEJ6UGHHpP+bFZnTh6GNWJONg5m+U56JrL+8IxZRdgWjw=="],
|
||||||
|
|
||||||
"request-light": ["request-light@0.7.0", "", {}, "sha512-lMbBMrDoxgsyO+yB3sDcrDuX85yYt7sS8BfQd11jtbW/z5ZWgLZRcEGLsLoYw7I0WSUGQBs8CC8ScIxkTX1+6Q=="],
|
"request-light": ["request-light@0.7.0", "", {}, "sha512-lMbBMrDoxgsyO+yB3sDcrDuX85yYt7sS8BfQd11jtbW/z5ZWgLZRcEGLsLoYw7I0WSUGQBs8CC8ScIxkTX1+6Q=="],
|
||||||
|
|
||||||
"require-directory": ["require-directory@2.1.1", "", {}, "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q=="],
|
"require-directory": ["require-directory@2.1.1", "", {}, "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q=="],
|
||||||
@@ -4103,6 +4139,8 @@
|
|||||||
|
|
||||||
"resolve-from": ["resolve-from@5.0.0", "", {}, "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw=="],
|
"resolve-from": ["resolve-from@5.0.0", "", {}, "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw=="],
|
||||||
|
|
||||||
|
"resolve-pkg-maps": ["resolve-pkg-maps@1.0.0", "", {}, "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw=="],
|
||||||
|
|
||||||
"responselike": ["responselike@2.0.1", "", { "dependencies": { "lowercase-keys": "^2.0.0" } }, "sha512-4gl03wn3hj1HP3yzgdI7d3lCkF95F21Pz4BPGvKHinyQzALR5CapwC8yIi0Rh58DEMQ/SguC03wFj2k0M/mHhw=="],
|
"responselike": ["responselike@2.0.1", "", { "dependencies": { "lowercase-keys": "^2.0.0" } }, "sha512-4gl03wn3hj1HP3yzgdI7d3lCkF95F21Pz4BPGvKHinyQzALR5CapwC8yIi0Rh58DEMQ/SguC03wFj2k0M/mHhw=="],
|
||||||
|
|
||||||
"restore-cursor": ["restore-cursor@3.1.0", "", { "dependencies": { "onetime": "^5.1.0", "signal-exit": "^3.0.2" } }, "sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA=="],
|
"restore-cursor": ["restore-cursor@3.1.0", "", { "dependencies": { "onetime": "^5.1.0", "signal-exit": "^3.0.2" } }, "sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA=="],
|
||||||
@@ -4235,7 +4273,7 @@
|
|||||||
|
|
||||||
"socket.io-client": ["socket.io-client@4.8.3", "", { "dependencies": { "@socket.io/component-emitter": "~3.1.0", "debug": "~4.4.1", "engine.io-client": "~6.6.1", "socket.io-parser": "~4.2.4" } }, "sha512-uP0bpjWrjQmUt5DTHq9RuoCBdFJF10cdX9X+a368j/Ft0wmaVgxlrjvK3kjvgCODOMMOz9lcaRzxmso0bTWZ/g=="],
|
"socket.io-client": ["socket.io-client@4.8.3", "", { "dependencies": { "@socket.io/component-emitter": "~3.1.0", "debug": "~4.4.1", "engine.io-client": "~6.6.1", "socket.io-parser": "~4.2.4" } }, "sha512-uP0bpjWrjQmUt5DTHq9RuoCBdFJF10cdX9X+a368j/Ft0wmaVgxlrjvK3kjvgCODOMMOz9lcaRzxmso0bTWZ/g=="],
|
||||||
|
|
||||||
"socket.io-parser": ["socket.io-parser@4.2.5", "", { "dependencies": { "@socket.io/component-emitter": "~3.1.0", "debug": "~4.4.1" } }, "sha512-bPMmpy/5WWKHea5Y/jYAP6k74A+hvmRCQaJuJB6I/ML5JZq/KfNieUVo/3Mh7SAqn7TyFdIo6wqYHInG1MU1bQ=="],
|
"socket.io-parser": ["socket.io-parser@4.2.6", "", { "dependencies": { "@socket.io/component-emitter": "~3.1.0", "debug": "~4.4.1" } }, "sha512-asJqbVBDsBCJx0pTqw3WfesSY0iRX+2xzWEWzrpcH7L6fLzrhyF8WPI8UaeM4YCuDfpwA/cgsdugMsmtz8EJeg=="],
|
||||||
|
|
||||||
"socks": ["socks@2.8.7", "", { "dependencies": { "ip-address": "^10.0.1", "smart-buffer": "^4.2.0" } }, "sha512-HLpt+uLy/pxB+bum/9DzAgiKS8CX1EvbWxI4zlmgGCExImLdiad2iCwXT5Z4c9c3Eq8rP2318mPW2c+QbtjK8A=="],
|
"socks": ["socks@2.8.7", "", { "dependencies": { "ip-address": "^10.0.1", "smart-buffer": "^4.2.0" } }, "sha512-HLpt+uLy/pxB+bum/9DzAgiKS8CX1EvbWxI4zlmgGCExImLdiad2iCwXT5Z4c9c3Eq8rP2318mPW2c+QbtjK8A=="],
|
||||||
|
|
||||||
@@ -4449,6 +4487,8 @@
|
|||||||
|
|
||||||
"tree-sitter-bash": ["tree-sitter-bash@0.25.0", "", { "dependencies": { "node-addon-api": "^8.2.1", "node-gyp-build": "^4.8.2" }, "peerDependencies": { "tree-sitter": "^0.25.0" }, "optionalPeers": ["tree-sitter"] }, "sha512-gZtlj9+qFS81qKxpLfD6H0UssQ3QBc/F0nKkPsiFDyfQF2YBqYvglFJUzchrPpVhZe9kLZTrJ9n2J6lmka69Vg=="],
|
"tree-sitter-bash": ["tree-sitter-bash@0.25.0", "", { "dependencies": { "node-addon-api": "^8.2.1", "node-gyp-build": "^4.8.2" }, "peerDependencies": { "tree-sitter": "^0.25.0" }, "optionalPeers": ["tree-sitter"] }, "sha512-gZtlj9+qFS81qKxpLfD6H0UssQ3QBc/F0nKkPsiFDyfQF2YBqYvglFJUzchrPpVhZe9kLZTrJ9n2J6lmka69Vg=="],
|
||||||
|
|
||||||
|
"tree-sitter-powershell": ["tree-sitter-powershell@0.25.10", "", { "dependencies": { "node-addon-api": "^7.1.0", "node-gyp-build": "^4.8.0" }, "peerDependencies": { "tree-sitter": "^0.25.0" }, "optionalPeers": ["tree-sitter"] }, "sha512-bEt8QoySpGFnU3aa8WedQyNMaN6aTwy/WUbvIVt0JSKF+BbJoSHNHu+wCbhj7xLMsfB0AuffmiJm+B8gzva8Lg=="],
|
||||||
|
|
||||||
"trim-lines": ["trim-lines@3.0.1", "", {}, "sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg=="],
|
"trim-lines": ["trim-lines@3.0.1", "", {}, "sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg=="],
|
||||||
|
|
||||||
"trough": ["trough@2.2.0", "", {}, "sha512-tmMpK00BjZiUyVyvrBK7knerNgmgvcV/KLVyuma/SC+TQN167GrMRciANTz09+k3zW8L8t60jWO1GpfkZdjTaw=="],
|
"trough": ["trough@2.2.0", "", {}, "sha512-tmMpK00BjZiUyVyvrBK7knerNgmgvcV/KLVyuma/SC+TQN167GrMRciANTz09+k3zW8L8t60jWO1GpfkZdjTaw=="],
|
||||||
@@ -4761,63 +4801,21 @@
|
|||||||
|
|
||||||
"@actions/http-client/undici": ["undici@6.23.0", "", {}, "sha512-VfQPToRA5FZs/qJxLIinmU59u0r7LXqoJkCzinq3ckNJp3vKEh7jTWN589YQ5+aoAC/TGRLyJLCPKcLQbM8r9g=="],
|
"@actions/http-client/undici": ["undici@6.23.0", "", {}, "sha512-VfQPToRA5FZs/qJxLIinmU59u0r7LXqoJkCzinq3ckNJp3vKEh7jTWN589YQ5+aoAC/TGRLyJLCPKcLQbM8r9g=="],
|
||||||
|
|
||||||
"@ai-sdk/amazon-bedrock/@ai-sdk/anthropic": ["@ai-sdk/anthropic@2.0.65", "", { "dependencies": { "@ai-sdk/provider": "2.0.1", "@ai-sdk/provider-utils": "3.0.21" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-HqTPP59mLQ9U6jXQcx6EORkdc5FyZu34Sitkg6jNpyMYcRjStvfx4+NWq/qaR+OTwBFcccv8hvVii0CYkH2Lag=="],
|
"@ai-sdk/amazon-bedrock/@smithy/eventstream-codec": ["@smithy/eventstream-codec@4.2.11", "", { "dependencies": { "@aws-crypto/crc32": "5.2.0", "@smithy/types": "^4.13.0", "@smithy/util-hex-encoding": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-Sf39Ml0iVX+ba/bgMPxaXWAAFmHqYLTmbjAPfLPLY8CrYkRDEqZdUsKC1OwVMCdJXfAt0v4j49GIJ8DoSYAe6w=="],
|
||||||
|
|
||||||
"@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/amazon-bedrock/@smithy/util-utf8": ["@smithy/util-utf8@4.2.2", "", { "dependencies": { "@smithy/util-buffer-from": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-75MeYpjdWRe8M5E3AW0O4Cx3UadweS+cwdXjwYGBW5h/gxxnbeZ877sLPX/ZJA9GVTlL/qG0dXP29JWFCD1Ayw=="],
|
||||||
|
|
||||||
"@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=="],
|
"@ai-sdk/deepgram/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@4.0.19", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@standard-schema/spec": "^1.1.0", "eventsource-parser": "^3.0.6" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-3eG55CrSWCu2SXlqq2QCsFjo3+E7+Gmg7i/oRVoSZzIodTuDSfLb3MRje67xE9RFea73Zao7Lm4mADIfUETKGg=="],
|
||||||
|
|
||||||
"@ai-sdk/azure/@ai-sdk/openai": ["@ai-sdk/openai@2.0.89", "", { "dependencies": { "@ai-sdk/provider": "2.0.1", "@ai-sdk/provider-utils": "3.0.20" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-4+qWkBCbL9HPKbgrUO/F2uXZ8GqrYxHa8SWEYIzxEJ9zvWw3ISr3t1/27O1i8MGSym+PzEyHBT48EV4LAwWaEw=="],
|
"@ai-sdk/deepseek/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@4.0.19", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@standard-schema/spec": "^1.1.0", "eventsource-parser": "^3.0.6" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-3eG55CrSWCu2SXlqq2QCsFjo3+E7+Gmg7i/oRVoSZzIodTuDSfLb3MRje67xE9RFea73Zao7Lm4mADIfUETKGg=="],
|
||||||
|
|
||||||
"@ai-sdk/azure/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@3.0.20", "", { "dependencies": { "@ai-sdk/provider": "2.0.1", "@standard-schema/spec": "^1.0.0", "eventsource-parser": "^3.0.6" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-iXHVe0apM2zUEzauqJwqmpC37A5rihrStAih5Ks+JE32iTe4LZ58y17UGBjpQQTCRw9YxMeo2UFLxLpBluyvLQ=="],
|
"@ai-sdk/elevenlabs/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@4.0.19", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@standard-schema/spec": "^1.1.0", "eventsource-parser": "^3.0.6" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-3eG55CrSWCu2SXlqq2QCsFjo3+E7+Gmg7i/oRVoSZzIodTuDSfLb3MRje67xE9RFea73Zao7Lm4mADIfUETKGg=="],
|
||||||
|
|
||||||
"@ai-sdk/cerebras/@ai-sdk/openai-compatible": ["@ai-sdk/openai-compatible@1.0.32", "", { "dependencies": { "@ai-sdk/provider": "2.0.1", "@ai-sdk/provider-utils": "3.0.20" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-YspqqyJPzHjqWrjt4y/Wgc2aJgCcQj5uIJgZpq2Ar/lH30cEVhgE+keePDbjKpetD9UwNggCj7u6kO3unS23OQ=="],
|
"@ai-sdk/fireworks/@ai-sdk/openai-compatible": ["@ai-sdk/openai-compatible@2.0.35", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.19" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-g3wA57IAQFb+3j4YuFndgkUdXyRETZVvbfAWM+UX7bZSxA3xjes0v3XKgIdKdekPtDGsh4ZX2byHD0gJIMPfiA=="],
|
||||||
|
|
||||||
"@ai-sdk/cerebras/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@3.0.20", "", { "dependencies": { "@ai-sdk/provider": "2.0.1", "@standard-schema/spec": "^1.0.0", "eventsource-parser": "^3.0.6" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-iXHVe0apM2zUEzauqJwqmpC37A5rihrStAih5Ks+JE32iTe4LZ58y17UGBjpQQTCRw9YxMeo2UFLxLpBluyvLQ=="],
|
"@ai-sdk/fireworks/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@4.0.19", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@standard-schema/spec": "^1.1.0", "eventsource-parser": "^3.0.6" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-3eG55CrSWCu2SXlqq2QCsFjo3+E7+Gmg7i/oRVoSZzIodTuDSfLb3MRje67xE9RFea73Zao7Lm4mADIfUETKGg=="],
|
||||||
|
|
||||||
"@ai-sdk/cohere/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@3.0.20", "", { "dependencies": { "@ai-sdk/provider": "2.0.1", "@standard-schema/spec": "^1.0.0", "eventsource-parser": "^3.0.6" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-iXHVe0apM2zUEzauqJwqmpC37A5rihrStAih5Ks+JE32iTe4LZ58y17UGBjpQQTCRw9YxMeo2UFLxLpBluyvLQ=="],
|
"@ai-sdk/provider-utils/@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="],
|
||||||
|
|
||||||
"@ai-sdk/deepgram/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@3.0.22", "", { "dependencies": { "@ai-sdk/provider": "2.0.1", "@standard-schema/spec": "^1.0.0", "eventsource-parser": "^3.0.6" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-fFT1KfUUKktfAFm5mClJhS1oux9tP2qgzmEZVl5UdwltQ1LO/s8hd7znVrgKzivwv1s1FIPza0s9OpJaNB/vHw=="],
|
|
||||||
|
|
||||||
"@ai-sdk/deepinfra/@ai-sdk/openai-compatible": ["@ai-sdk/openai-compatible@1.0.33", "", { "dependencies": { "@ai-sdk/provider": "2.0.1", "@ai-sdk/provider-utils": "3.0.21" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-2KMcR2xAul3u5dGZD7gONgbIki3Hg7Ey+sFu7gsiJ4U2iRU0GDV3ccNq79dTuAEXPDFcOWCUpW8A8jXc0kxJxQ=="],
|
|
||||||
|
|
||||||
"@ai-sdk/deepseek/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@3.0.22", "", { "dependencies": { "@ai-sdk/provider": "2.0.1", "@standard-schema/spec": "^1.0.0", "eventsource-parser": "^3.0.6" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-fFT1KfUUKktfAFm5mClJhS1oux9tP2qgzmEZVl5UdwltQ1LO/s8hd7znVrgKzivwv1s1FIPza0s9OpJaNB/vHw=="],
|
|
||||||
|
|
||||||
"@ai-sdk/elevenlabs/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@3.0.22", "", { "dependencies": { "@ai-sdk/provider": "2.0.1", "@standard-schema/spec": "^1.0.0", "eventsource-parser": "^3.0.6" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-fFT1KfUUKktfAFm5mClJhS1oux9tP2qgzmEZVl5UdwltQ1LO/s8hd7znVrgKzivwv1s1FIPza0s9OpJaNB/vHw=="],
|
|
||||||
|
|
||||||
"@ai-sdk/fireworks/@ai-sdk/openai-compatible": ["@ai-sdk/openai-compatible@1.0.34", "", { "dependencies": { "@ai-sdk/provider": "2.0.1", "@ai-sdk/provider-utils": "3.0.22" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-AnGoxVNZ/E3EU4lW12rrufI6riqL2cEv4jk3OrjJ/i54XwR0CJU1V26jXAwxb+Pc+uZmYG++HM+gzXxPQZkMNQ=="],
|
|
||||||
|
|
||||||
"@ai-sdk/fireworks/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@3.0.22", "", { "dependencies": { "@ai-sdk/provider": "2.0.1", "@standard-schema/spec": "^1.0.0", "eventsource-parser": "^3.0.6" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-fFT1KfUUKktfAFm5mClJhS1oux9tP2qgzmEZVl5UdwltQ1LO/s8hd7znVrgKzivwv1s1FIPza0s9OpJaNB/vHw=="],
|
|
||||||
|
|
||||||
"@ai-sdk/gateway/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@3.0.20", "", { "dependencies": { "@ai-sdk/provider": "2.0.1", "@standard-schema/spec": "^1.0.0", "eventsource-parser": "^3.0.6" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-iXHVe0apM2zUEzauqJwqmpC37A5rihrStAih5Ks+JE32iTe4LZ58y17UGBjpQQTCRw9YxMeo2UFLxLpBluyvLQ=="],
|
|
||||||
|
|
||||||
"@ai-sdk/google-vertex/@ai-sdk/anthropic": ["@ai-sdk/anthropic@2.0.65", "", { "dependencies": { "@ai-sdk/provider": "2.0.1", "@ai-sdk/provider-utils": "3.0.21" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-HqTPP59mLQ9U6jXQcx6EORkdc5FyZu34Sitkg6jNpyMYcRjStvfx4+NWq/qaR+OTwBFcccv8hvVii0CYkH2Lag=="],
|
|
||||||
|
|
||||||
"@ai-sdk/groq/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@3.0.20", "", { "dependencies": { "@ai-sdk/provider": "2.0.1", "@standard-schema/spec": "^1.0.0", "eventsource-parser": "^3.0.6" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-iXHVe0apM2zUEzauqJwqmpC37A5rihrStAih5Ks+JE32iTe4LZ58y17UGBjpQQTCRw9YxMeo2UFLxLpBluyvLQ=="],
|
|
||||||
|
|
||||||
"@ai-sdk/mistral/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@3.0.20", "", { "dependencies": { "@ai-sdk/provider": "2.0.1", "@standard-schema/spec": "^1.0.0", "eventsource-parser": "^3.0.6" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-iXHVe0apM2zUEzauqJwqmpC37A5rihrStAih5Ks+JE32iTe4LZ58y17UGBjpQQTCRw9YxMeo2UFLxLpBluyvLQ=="],
|
|
||||||
|
|
||||||
"@ai-sdk/openai/@ai-sdk/provider": ["@ai-sdk/provider@2.0.0", "", { "dependencies": { "json-schema": "^0.4.0" } }, "sha512-6o7Y2SeO9vFKB8lArHXehNuusnpddKPk7xqL7T2/b+OvXMRIXUO1rR4wcv1hAFUAT9avGZshty3Wlua/XA7TvA=="],
|
|
||||||
|
|
||||||
"@ai-sdk/openai/@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=="],
|
|
||||||
|
|
||||||
"@ai-sdk/openai-compatible/@ai-sdk/provider": ["@ai-sdk/provider@2.0.0", "", { "dependencies": { "json-schema": "^0.4.0" } }, "sha512-6o7Y2SeO9vFKB8lArHXehNuusnpddKPk7xqL7T2/b+OvXMRIXUO1rR4wcv1hAFUAT9avGZshty3Wlua/XA7TvA=="],
|
|
||||||
|
|
||||||
"@ai-sdk/openai-compatible/@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=="],
|
|
||||||
|
|
||||||
"@ai-sdk/perplexity/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@3.0.20", "", { "dependencies": { "@ai-sdk/provider": "2.0.1", "@standard-schema/spec": "^1.0.0", "eventsource-parser": "^3.0.6" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-iXHVe0apM2zUEzauqJwqmpC37A5rihrStAih5Ks+JE32iTe4LZ58y17UGBjpQQTCRw9YxMeo2UFLxLpBluyvLQ=="],
|
|
||||||
|
|
||||||
"@ai-sdk/togetherai/@ai-sdk/openai-compatible": ["@ai-sdk/openai-compatible@1.0.32", "", { "dependencies": { "@ai-sdk/provider": "2.0.1", "@ai-sdk/provider-utils": "3.0.20" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-YspqqyJPzHjqWrjt4y/Wgc2aJgCcQj5uIJgZpq2Ar/lH30cEVhgE+keePDbjKpetD9UwNggCj7u6kO3unS23OQ=="],
|
|
||||||
|
|
||||||
"@ai-sdk/togetherai/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@3.0.20", "", { "dependencies": { "@ai-sdk/provider": "2.0.1", "@standard-schema/spec": "^1.0.0", "eventsource-parser": "^3.0.6" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-iXHVe0apM2zUEzauqJwqmpC37A5rihrStAih5Ks+JE32iTe4LZ58y17UGBjpQQTCRw9YxMeo2UFLxLpBluyvLQ=="],
|
|
||||||
|
|
||||||
"@ai-sdk/vercel/@ai-sdk/openai-compatible": ["@ai-sdk/openai-compatible@1.0.32", "", { "dependencies": { "@ai-sdk/provider": "2.0.1", "@ai-sdk/provider-utils": "3.0.20" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-YspqqyJPzHjqWrjt4y/Wgc2aJgCcQj5uIJgZpq2Ar/lH30cEVhgE+keePDbjKpetD9UwNggCj7u6kO3unS23OQ=="],
|
|
||||||
|
|
||||||
"@ai-sdk/vercel/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@3.0.20", "", { "dependencies": { "@ai-sdk/provider": "2.0.1", "@standard-schema/spec": "^1.0.0", "eventsource-parser": "^3.0.6" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-iXHVe0apM2zUEzauqJwqmpC37A5rihrStAih5Ks+JE32iTe4LZ58y17UGBjpQQTCRw9YxMeo2UFLxLpBluyvLQ=="],
|
|
||||||
|
|
||||||
"@ai-sdk/xai/@ai-sdk/openai-compatible": ["@ai-sdk/openai-compatible@1.0.30", "", { "dependencies": { "@ai-sdk/provider": "2.0.1", "@ai-sdk/provider-utils": "3.0.20" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-thubwhRtv9uicAxSWwNpinM7hiL/0CkhL/ymPaHuKvI494J7HIzn8KQZQ2ymRz284WTIZnI7VMyyejxW4RMM6w=="],
|
|
||||||
|
|
||||||
"@ai-sdk/xai/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@3.0.20", "", { "dependencies": { "@ai-sdk/provider": "2.0.1", "@standard-schema/spec": "^1.0.0", "eventsource-parser": "^3.0.6" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-iXHVe0apM2zUEzauqJwqmpC37A5rihrStAih5Ks+JE32iTe4LZ58y17UGBjpQQTCRw9YxMeo2UFLxLpBluyvLQ=="],
|
|
||||||
|
|
||||||
"@astrojs/check/yargs": ["yargs@17.7.2", "", { "dependencies": { "cliui": "^8.0.1", "escalade": "^3.1.1", "get-caller-file": "^2.0.5", "require-directory": "^2.1.1", "string-width": "^4.2.3", "y18n": "^5.0.5", "yargs-parser": "^21.1.1" } }, "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w=="],
|
"@astrojs/check/yargs": ["yargs@17.7.2", "", { "dependencies": { "cliui": "^8.0.1", "escalade": "^3.1.1", "get-caller-file": "^2.0.5", "require-directory": "^2.1.1", "string-width": "^4.2.3", "y18n": "^5.0.5", "yargs-parser": "^21.1.1" } }, "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w=="],
|
||||||
|
|
||||||
@@ -5015,6 +5013,8 @@
|
|||||||
|
|
||||||
"@dot/log/chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="],
|
"@dot/log/chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="],
|
||||||
|
|
||||||
|
"@effect/platform-node/undici": ["undici@7.24.4", "", {}, "sha512-BM/JzwwaRXxrLdElV2Uo6cTLEjhSb3WXboncJamZ15NgUURmvlXvxa6xkwIOILIjPNo9i8ku136ZvWV0Uly8+w=="],
|
||||||
|
|
||||||
"@effect/platform-node-shared/ws": ["ws@8.19.0", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-blAT2mjOEIi0ZzruJfIhb3nps74PRWTCz1IjglWEEpQl5XS/UNama6u2/rjFkDDouqr4L67ry+1aGIALViWjDg=="],
|
"@effect/platform-node-shared/ws": ["ws@8.19.0", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-blAT2mjOEIi0ZzruJfIhb3nps74PRWTCz1IjglWEEpQl5XS/UNama6u2/rjFkDDouqr4L67ry+1aGIALViWjDg=="],
|
||||||
|
|
||||||
"@electron/asar/commander": ["commander@5.1.0", "", {}, "sha512-P0CysNDQ7rtVw4QIQtm+MRxV66vKFSvlsQvGYXZWR3qFU0jlMKHZZZgw8e+8DSah4UDKMqnknRDQz+xuQXQ/Zg=="],
|
"@electron/asar/commander": ["commander@5.1.0", "", {}, "sha512-P0CysNDQ7rtVw4QIQtm+MRxV66vKFSvlsQvGYXZWR3qFU0jlMKHZZZgw8e+8DSah4UDKMqnknRDQz+xuQXQ/Zg=="],
|
||||||
@@ -5047,10 +5047,6 @@
|
|||||||
|
|
||||||
"@fastify/proxy-addr/ipaddr.js": ["ipaddr.js@2.3.0", "", {}, "sha512-Zv/pA+ciVFbCSBBjGfaKUya/CcGmUHzTydLMaTwrUUEM2DIEO3iZvueGxmacvmN50fGpGVKeTXpb2LcYQxeVdg=="],
|
"@fastify/proxy-addr/ipaddr.js": ["ipaddr.js@2.3.0", "", {}, "sha512-Zv/pA+ciVFbCSBBjGfaKUya/CcGmUHzTydLMaTwrUUEM2DIEO3iZvueGxmacvmN50fGpGVKeTXpb2LcYQxeVdg=="],
|
||||||
|
|
||||||
"@gitlab/gitlab-ai-provider/openai": ["openai@6.27.0", "", { "peerDependencies": { "ws": "^8.18.0", "zod": "^3.25 || ^4.0" }, "optionalPeers": ["ws", "zod"], "bin": { "openai": "bin/cli" } }, "sha512-osTKySlrdYrLYTt0zjhY8yp0JUBmWDCN+Q+QxsV4xMQnnoVFpylgKGgxwN8sSdTNw0G4y+WUXs4eCMWpyDNWZQ=="],
|
|
||||||
|
|
||||||
"@gitlab/gitlab-ai-provider/zod": ["zod@3.25.76", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="],
|
|
||||||
|
|
||||||
"@hey-api/openapi-ts/open": ["open@11.0.0", "", { "dependencies": { "default-browser": "^5.4.0", "define-lazy-prop": "^3.0.0", "is-in-ssh": "^1.0.0", "is-inside-container": "^1.0.0", "powershell-utils": "^0.1.0", "wsl-utils": "^0.3.0" } }, "sha512-smsWv2LzFjP03xmvFoJ331ss6h+jixfA4UUV/Bsiyuu4YJPfN+FIQGOIiv4w9/+MoHkfkJ22UIaQWRVFRfH6Vw=="],
|
"@hey-api/openapi-ts/open": ["open@11.0.0", "", { "dependencies": { "default-browser": "^5.4.0", "define-lazy-prop": "^3.0.0", "is-in-ssh": "^1.0.0", "is-inside-container": "^1.0.0", "powershell-utils": "^0.1.0", "wsl-utils": "^0.3.0" } }, "sha512-smsWv2LzFjP03xmvFoJ331ss6h+jixfA4UUV/Bsiyuu4YJPfN+FIQGOIiv4w9/+MoHkfkJ22UIaQWRVFRfH6Vw=="],
|
||||||
|
|
||||||
"@hey-api/openapi-ts/semver": ["semver@7.7.3", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q=="],
|
"@hey-api/openapi-ts/semver": ["semver@7.7.3", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q=="],
|
||||||
@@ -5107,6 +5103,8 @@
|
|||||||
|
|
||||||
"@modelcontextprotocol/sdk/express": ["express@5.2.1", "", { "dependencies": { "accepts": "^2.0.0", "body-parser": "^2.2.1", "content-disposition": "^1.0.0", "content-type": "^1.0.5", "cookie": "^0.7.1", "cookie-signature": "^1.2.1", "debug": "^4.4.0", "depd": "^2.0.0", "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "etag": "^1.8.1", "finalhandler": "^2.1.0", "fresh": "^2.0.0", "http-errors": "^2.0.0", "merge-descriptors": "^2.0.0", "mime-types": "^3.0.0", "on-finished": "^2.4.1", "once": "^1.4.0", "parseurl": "^1.3.3", "proxy-addr": "^2.0.7", "qs": "^6.14.0", "range-parser": "^1.2.1", "router": "^2.2.0", "send": "^1.1.0", "serve-static": "^2.2.0", "statuses": "^2.0.1", "type-is": "^2.0.1", "vary": "^1.1.2" } }, "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw=="],
|
"@modelcontextprotocol/sdk/express": ["express@5.2.1", "", { "dependencies": { "accepts": "^2.0.0", "body-parser": "^2.2.1", "content-disposition": "^1.0.0", "content-type": "^1.0.5", "cookie": "^0.7.1", "cookie-signature": "^1.2.1", "debug": "^4.4.0", "depd": "^2.0.0", "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "etag": "^1.8.1", "finalhandler": "^2.1.0", "fresh": "^2.0.0", "http-errors": "^2.0.0", "merge-descriptors": "^2.0.0", "mime-types": "^3.0.0", "on-finished": "^2.4.1", "once": "^1.4.0", "parseurl": "^1.3.3", "proxy-addr": "^2.0.7", "qs": "^6.14.0", "range-parser": "^1.2.1", "router": "^2.2.0", "send": "^1.1.0", "serve-static": "^2.2.0", "statuses": "^2.0.1", "type-is": "^2.0.1", "vary": "^1.1.2" } }, "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw=="],
|
||||||
|
|
||||||
|
"@modelcontextprotocol/sdk/hono": ["hono@4.12.9", "", {}, "sha512-wy3T8Zm2bsEvxKZM5w21VdHDDcwVS1yUFFY6i8UobSsKfFceT7TOwhbhfKsDyx7tYQlmRM5FLpIuYvNFyjctiA=="],
|
||||||
|
|
||||||
"@modelcontextprotocol/sdk/jose": ["jose@6.2.1", "", {}, "sha512-jUaKr1yrbfaImV7R2TN/b3IcZzsw38/chqMpo2XJ7i2F8AfM/lA4G1goC3JVEwg0H7UldTmSt3P68nt31W7/mw=="],
|
"@modelcontextprotocol/sdk/jose": ["jose@6.2.1", "", {}, "sha512-jUaKr1yrbfaImV7R2TN/b3IcZzsw38/chqMpo2XJ7i2F8AfM/lA4G1goC3JVEwg0H7UldTmSt3P68nt31W7/mw=="],
|
||||||
|
|
||||||
"@modelcontextprotocol/sdk/raw-body": ["raw-body@3.0.2", "", { "dependencies": { "bytes": "~3.1.2", "http-errors": "~2.0.1", "iconv-lite": "~0.7.0", "unpipe": "~1.0.0" } }, "sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA=="],
|
"@modelcontextprotocol/sdk/raw-body": ["raw-body@3.0.2", "", { "dependencies": { "bytes": "~3.1.2", "http-errors": "~2.0.1", "iconv-lite": "~0.7.0", "unpipe": "~1.0.0" } }, "sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA=="],
|
||||||
@@ -5189,8 +5187,6 @@
|
|||||||
|
|
||||||
"@opentui/solid/@babel/core": ["@babel/core@7.28.0", "", { "dependencies": { "@ampproject/remapping": "^2.2.0", "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.28.0", "@babel/helper-compilation-targets": "^7.27.2", "@babel/helper-module-transforms": "^7.27.3", "@babel/helpers": "^7.27.6", "@babel/parser": "^7.28.0", "@babel/template": "^7.27.2", "@babel/traverse": "^7.28.0", "@babel/types": "^7.28.0", "convert-source-map": "^2.0.0", "debug": "^4.1.0", "gensync": "^1.0.0-beta.2", "json5": "^2.2.3", "semver": "^6.3.1" } }, "sha512-UlLAnTPrFdNGoFtbSXwcGFQBtQZJCNjaN6hQNP3UPvuNXT1i82N26KL3dZeIpNalWywr9IuQuncaAfUaS1g6sQ=="],
|
"@opentui/solid/@babel/core": ["@babel/core@7.28.0", "", { "dependencies": { "@ampproject/remapping": "^2.2.0", "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.28.0", "@babel/helper-compilation-targets": "^7.27.2", "@babel/helper-module-transforms": "^7.27.3", "@babel/helpers": "^7.27.6", "@babel/parser": "^7.28.0", "@babel/template": "^7.27.2", "@babel/traverse": "^7.28.0", "@babel/types": "^7.28.0", "convert-source-map": "^2.0.0", "debug": "^4.1.0", "gensync": "^1.0.0-beta.2", "json5": "^2.2.3", "semver": "^6.3.1" } }, "sha512-UlLAnTPrFdNGoFtbSXwcGFQBtQZJCNjaN6hQNP3UPvuNXT1i82N26KL3dZeIpNalWywr9IuQuncaAfUaS1g6sQ=="],
|
||||||
|
|
||||||
"@opentui/solid/babel-preset-solid": ["babel-preset-solid@1.9.9", "", { "dependencies": { "babel-plugin-jsx-dom-expressions": "^0.40.1" }, "peerDependencies": { "@babel/core": "^7.0.0", "solid-js": "^1.9.8" }, "optionalPeers": ["solid-js"] }, "sha512-pCnxWrciluXCeli/dj5PIEHgbNzim3evtTn12snjqqg8QZWJNMjH1AWIp4iG/tbVjqQ72aBEymMSagvmgxubXw=="],
|
|
||||||
|
|
||||||
"@oslojs/jwt/@oslojs/encoding": ["@oslojs/encoding@0.4.1", "", {}, "sha512-hkjo6MuIK/kQR5CrGNdAPZhS01ZCXuWDRJ187zh6qqF2+yMHZpD9fAYpX8q2bOO6Ryhl3XpCT6kUX76N8hhm4Q=="],
|
"@oslojs/jwt/@oslojs/encoding": ["@oslojs/encoding@0.4.1", "", {}, "sha512-hkjo6MuIK/kQR5CrGNdAPZhS01ZCXuWDRJ187zh6qqF2+yMHZpD9fAYpX8q2bOO6Ryhl3XpCT6kUX76N8hhm4Q=="],
|
||||||
|
|
||||||
"@pierre/diffs/@shikijs/transformers": ["@shikijs/transformers@3.20.0", "", { "dependencies": { "@shikijs/core": "3.20.0", "@shikijs/types": "3.20.0" } }, "sha512-PrHHMRr3Q5W1qB/42kJW6laqFyWdhrPF2hNR9qjOm1xcSiAO3hAHo7HaVyHE6pMyevmy3i51O8kuGGXC78uK3g=="],
|
"@pierre/diffs/@shikijs/transformers": ["@shikijs/transformers@3.20.0", "", { "dependencies": { "@shikijs/core": "3.20.0", "@shikijs/types": "3.20.0" } }, "sha512-PrHHMRr3Q5W1qB/42kJW6laqFyWdhrPF2hNR9qjOm1xcSiAO3hAHo7HaVyHE6pMyevmy3i51O8kuGGXC78uK3g=="],
|
||||||
@@ -5293,15 +5289,7 @@
|
|||||||
|
|
||||||
"accepts/mime-types": ["mime-types@2.1.35", "", { "dependencies": { "mime-db": "1.52.0" } }, "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw=="],
|
"accepts/mime-types": ["mime-types@2.1.35", "", { "dependencies": { "mime-db": "1.52.0" } }, "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw=="],
|
||||||
|
|
||||||
"ai/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@3.0.20", "", { "dependencies": { "@ai-sdk/provider": "2.0.1", "@standard-schema/spec": "^1.0.0", "eventsource-parser": "^3.0.6" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-iXHVe0apM2zUEzauqJwqmpC37A5rihrStAih5Ks+JE32iTe4LZ58y17UGBjpQQTCRw9YxMeo2UFLxLpBluyvLQ=="],
|
"ai-gateway-provider/@ai-sdk/xai": ["@ai-sdk/xai@3.0.74", "", { "dependencies": { "@ai-sdk/openai-compatible": "2.0.37", "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.21" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-HDDLsT+QrzE3c2QZLRV/HKAwMtXDb0PMDdk1PYUXLJ3r9Qv76zGKGyvJLX7Pu6c8TOHD1mwLrOVYrsTpC/eTMw=="],
|
||||||
|
|
||||||
"ai-gateway-provider/@ai-sdk/anthropic": ["@ai-sdk/anthropic@2.0.65", "", { "dependencies": { "@ai-sdk/provider": "2.0.1", "@ai-sdk/provider-utils": "3.0.21" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-HqTPP59mLQ9U6jXQcx6EORkdc5FyZu34Sitkg6jNpyMYcRjStvfx4+NWq/qaR+OTwBFcccv8hvVii0CYkH2Lag=="],
|
|
||||||
|
|
||||||
"ai-gateway-provider/@ai-sdk/google-vertex": ["@ai-sdk/google-vertex@3.0.90", "", { "dependencies": { "@ai-sdk/anthropic": "2.0.56", "@ai-sdk/google": "2.0.46", "@ai-sdk/provider": "2.0.0", "@ai-sdk/provider-utils": "3.0.19", "google-auth-library": "^10.5.0" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-C9MLe1KZGg1ZbupV2osygHtL5qngyCDA6ATatunyfTbIe8TXKG8HGni/3O6ifbnI5qxTidIn150Ox7eIFZVMYg=="],
|
|
||||||
|
|
||||||
"ai-gateway-provider/@ai-sdk/openai": ["@ai-sdk/openai@2.0.89", "", { "dependencies": { "@ai-sdk/provider": "2.0.1", "@ai-sdk/provider-utils": "3.0.20" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-4+qWkBCbL9HPKbgrUO/F2uXZ8GqrYxHa8SWEYIzxEJ9zvWw3ISr3t1/27O1i8MGSym+PzEyHBT48EV4LAwWaEw=="],
|
|
||||||
|
|
||||||
"ai-gateway-provider/@ai-sdk/openai-compatible": ["@ai-sdk/openai-compatible@1.0.34", "", { "dependencies": { "@ai-sdk/provider": "2.0.1", "@ai-sdk/provider-utils": "3.0.22" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-AnGoxVNZ/E3EU4lW12rrufI6riqL2cEv4jk3OrjJ/i54XwR0CJU1V26jXAwxb+Pc+uZmYG++HM+gzXxPQZkMNQ=="],
|
|
||||||
|
|
||||||
"ajv-keywords/ajv": ["ajv@6.14.0", "", { "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", "json-schema-traverse": "^0.4.1", "uri-js": "^4.2.2" } }, "sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw=="],
|
"ajv-keywords/ajv": ["ajv@6.14.0", "", { "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", "json-schema-traverse": "^0.4.1", "uri-js": "^4.2.2" } }, "sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw=="],
|
||||||
|
|
||||||
@@ -5379,6 +5367,8 @@
|
|||||||
|
|
||||||
"cross-spawn/which": ["which@2.0.2", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "./bin/node-which" } }, "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA=="],
|
"cross-spawn/which": ["which@2.0.2", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "./bin/node-which" } }, "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA=="],
|
||||||
|
|
||||||
|
"db0/drizzle-orm": ["drizzle-orm@1.0.0-beta.16-ea816b6", "", { "peerDependencies": { "@aws-sdk/client-rds-data": ">=3", "@cloudflare/workers-types": ">=4", "@effect/sql": "^0.48.5", "@effect/sql-pg": "^0.49.7", "@electric-sql/pglite": ">=0.2.0", "@libsql/client": ">=0.10.0", "@libsql/client-wasm": ">=0.10.0", "@neondatabase/serverless": ">=0.10.0", "@op-engineering/op-sqlite": ">=2", "@opentelemetry/api": "^1.4.1", "@planetscale/database": ">=1.13", "@prisma/client": "*", "@sinclair/typebox": ">=0.34.8", "@sqlitecloud/drivers": ">=1.0.653", "@tidbcloud/serverless": "*", "@tursodatabase/database": ">=0.2.1", "@tursodatabase/database-common": ">=0.2.1", "@tursodatabase/database-wasm": ">=0.2.1", "@types/better-sqlite3": "*", "@types/mssql": "^9.1.4", "@types/pg": "*", "@types/sql.js": "*", "@upstash/redis": ">=1.34.7", "@vercel/postgres": ">=0.8.0", "@xata.io/client": "*", "arktype": ">=2.0.0", "better-sqlite3": ">=9.3.0", "bun-types": "*", "expo-sqlite": ">=14.0.0", "gel": ">=2", "mssql": "^11.0.1", "mysql2": ">=2", "pg": ">=8", "postgres": ">=3", "sql.js": ">=1", "sqlite3": ">=5", "typebox": ">=1.0.0", "valibot": ">=1.0.0-beta.7", "zod": "^3.25.0 || ^4.0.0" }, "optionalPeers": ["@aws-sdk/client-rds-data", "@cloudflare/workers-types", "@effect/sql", "@effect/sql-pg", "@electric-sql/pglite", "@libsql/client", "@libsql/client-wasm", "@neondatabase/serverless", "@op-engineering/op-sqlite", "@opentelemetry/api", "@planetscale/database", "@prisma/client", "@sinclair/typebox", "@sqlitecloud/drivers", "@tidbcloud/serverless", "@tursodatabase/database", "@tursodatabase/database-common", "@tursodatabase/database-wasm", "@types/better-sqlite3", "@types/pg", "@types/sql.js", "@upstash/redis", "@vercel/postgres", "@xata.io/client", "arktype", "better-sqlite3", "bun-types", "expo-sqlite", "gel", "mysql2", "pg", "postgres", "sql.js", "sqlite3", "typebox", "valibot", "zod"] }, "sha512-k9gT4f0O9Qvah5YK/zL+FZonQ8TPyVxcG/ojN4dzO0fHP8hs8tBno8lqmJo53g0JLWv3Q2nsTUoyBRKM2TljFw=="],
|
||||||
|
|
||||||
"defaults/clone": ["clone@1.0.4", "", {}, "sha512-JQHZ2QMW6l3aH/j6xCqQThY/9OH4D/9ls34cgkUBiEeocRTU04tHfKPBsUK1PqZCUQM7GiA0IIXJSuXHI64Kbg=="],
|
"defaults/clone": ["clone@1.0.4", "", {}, "sha512-JQHZ2QMW6l3aH/j6xCqQThY/9OH4D/9ls34cgkUBiEeocRTU04tHfKPBsUK1PqZCUQM7GiA0IIXJSuXHI64Kbg=="],
|
||||||
|
|
||||||
"dir-compare/minimatch": ["minimatch@3.1.5", "", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w=="],
|
"dir-compare/minimatch": ["minimatch@3.1.5", "", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w=="],
|
||||||
@@ -5445,6 +5435,10 @@
|
|||||||
|
|
||||||
"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=="],
|
"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=="],
|
||||||
|
|
||||||
|
"gitlab-ai-provider/openai": ["openai@6.32.0", "", { "peerDependencies": { "ws": "^8.18.0", "zod": "^3.25 || ^4.0" }, "optionalPeers": ["ws", "zod"], "bin": { "openai": "bin/cli" } }, "sha512-j3k+BjydAf8yQlcOI7WUQMQTbbF5GEIMAE2iZYCOzwwB3S2pCheaWYp+XZRNAch4jWVc52PMDGRRjutao3lLCg=="],
|
||||||
|
|
||||||
|
"gitlab-ai-provider/zod": ["zod@3.25.76", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="],
|
||||||
|
|
||||||
"glob/minimatch": ["minimatch@10.2.4", "", { "dependencies": { "brace-expansion": "^5.0.2" } }, "sha512-oRjTw/97aTBN0RHbYCdtF1MQfvusSIBQM0IZEgzl6426+8jSC0nF1a/GmnVLpfB9yyr6g6FTqWqiZVbxrtaCIg=="],
|
"glob/minimatch": ["minimatch@10.2.4", "", { "dependencies": { "brace-expansion": "^5.0.2" } }, "sha512-oRjTw/97aTBN0RHbYCdtF1MQfvusSIBQM0IZEgzl6426+8jSC0nF1a/GmnVLpfB9yyr6g6FTqWqiZVbxrtaCIg=="],
|
||||||
|
|
||||||
"globby/ignore": ["ignore@5.3.2", "", {}, "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g=="],
|
"globby/ignore": ["ignore@5.3.2", "", {}, "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g=="],
|
||||||
@@ -5515,11 +5509,9 @@
|
|||||||
|
|
||||||
"nypm/tinyexec": ["tinyexec@1.0.2", "", {}, "sha512-W/KYk+NFhkmsYpuHq5JykngiOCnxeVL8v8dFnqxSD8qEEdRfXk1SDM6JzNqcERbcGYj9tMrDQBYV9cjgnunFIg=="],
|
"nypm/tinyexec": ["tinyexec@1.0.2", "", {}, "sha512-W/KYk+NFhkmsYpuHq5JykngiOCnxeVL8v8dFnqxSD8qEEdRfXk1SDM6JzNqcERbcGYj9tMrDQBYV9cjgnunFIg=="],
|
||||||
|
|
||||||
"opencode/@ai-sdk/anthropic": ["@ai-sdk/anthropic@2.0.65", "", { "dependencies": { "@ai-sdk/provider": "2.0.1", "@ai-sdk/provider-utils": "3.0.21" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-HqTPP59mLQ9U6jXQcx6EORkdc5FyZu34Sitkg6jNpyMYcRjStvfx4+NWq/qaR+OTwBFcccv8hvVii0CYkH2Lag=="],
|
"opencode-gitlab-auth/open": ["open@10.2.0", "", { "dependencies": { "default-browser": "^5.2.1", "define-lazy-prop": "^3.0.0", "is-inside-container": "^1.0.0", "wsl-utils": "^0.1.0" } }, "sha512-YgBpdJHPyQ2UE5x+hlSXcnejzAvD0b22U2OuAP+8OnlJT+PjWPxtgmGqKKc+RgTM63U9gN0YzrYc71R2WT/hTA=="],
|
||||||
|
|
||||||
"opencode/@ai-sdk/openai": ["@ai-sdk/openai@2.0.89", "", { "dependencies": { "@ai-sdk/provider": "2.0.1", "@ai-sdk/provider-utils": "3.0.20" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-4+qWkBCbL9HPKbgrUO/F2uXZ8GqrYxHa8SWEYIzxEJ9zvWw3ISr3t1/27O1i8MGSym+PzEyHBT48EV4LAwWaEw=="],
|
"opencode-poe-auth/open": ["open@10.2.0", "", { "dependencies": { "default-browser": "^5.2.1", "define-lazy-prop": "^3.0.0", "is-inside-container": "^1.0.0", "wsl-utils": "^0.1.0" } }, "sha512-YgBpdJHPyQ2UE5x+hlSXcnejzAvD0b22U2OuAP+8OnlJT+PjWPxtgmGqKKc+RgTM63U9gN0YzrYc71R2WT/hTA=="],
|
||||||
|
|
||||||
"opencode/@ai-sdk/openai-compatible": ["@ai-sdk/openai-compatible@1.0.32", "", { "dependencies": { "@ai-sdk/provider": "2.0.1", "@ai-sdk/provider-utils": "3.0.20" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-YspqqyJPzHjqWrjt4y/Wgc2aJgCcQj5uIJgZpq2Ar/lH30cEVhgE+keePDbjKpetD9UwNggCj7u6kO3unS23OQ=="],
|
|
||||||
|
|
||||||
"opencontrol/@modelcontextprotocol/sdk": ["@modelcontextprotocol/sdk@1.6.1", "", { "dependencies": { "content-type": "^1.0.5", "cors": "^2.8.5", "eventsource": "^3.0.2", "express": "^5.0.1", "express-rate-limit": "^7.5.0", "pkce-challenge": "^4.1.0", "raw-body": "^3.0.0", "zod": "^3.23.8", "zod-to-json-schema": "^3.24.1" } }, "sha512-oxzMzYCkZHMntzuyerehK3fV6A2Kwh5BD6CGEJSVDU2QNEhfLOptf2X7esQgaHZXHZY0oHmMsOtIDLP71UJXgA=="],
|
"opencontrol/@modelcontextprotocol/sdk": ["@modelcontextprotocol/sdk@1.6.1", "", { "dependencies": { "content-type": "^1.0.5", "cors": "^2.8.5", "eventsource": "^3.0.2", "express": "^5.0.1", "express-rate-limit": "^7.5.0", "pkce-challenge": "^4.1.0", "raw-body": "^3.0.0", "zod": "^3.23.8", "zod-to-json-schema": "^3.24.1" } }, "sha512-oxzMzYCkZHMntzuyerehK3fV6A2Kwh5BD6CGEJSVDU2QNEhfLOptf2X7esQgaHZXHZY0oHmMsOtIDLP71UJXgA=="],
|
||||||
|
|
||||||
@@ -5641,12 +5633,12 @@
|
|||||||
|
|
||||||
"type-is/mime-types": ["mime-types@2.1.35", "", { "dependencies": { "mime-db": "1.52.0" } }, "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw=="],
|
"type-is/mime-types": ["mime-types@2.1.35", "", { "dependencies": { "mime-db": "1.52.0" } }, "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw=="],
|
||||||
|
|
||||||
|
"unicode-trie/pako": ["pako@0.2.9", "", {}, "sha512-NUcwaKxUxWrZLpDG+z/xZaCgQITkA/Dv4V/T6bw7VON6l1Xz/VnrBqrYjZQ12TamKHzITTfOEIYUj48y2KXImA=="],
|
||||||
|
|
||||||
"unifont/ofetch": ["ofetch@1.5.1", "", { "dependencies": { "destr": "^2.0.5", "node-fetch-native": "^1.6.7", "ufo": "^1.6.1" } }, "sha512-2W4oUZlVaqAPAil6FUg/difl6YhqhUR7x2eZY4bQCko22UXg3hptq9KLQdqFClV+Wu85UX7hNtdGTngi/1BxcA=="],
|
"unifont/ofetch": ["ofetch@1.5.1", "", { "dependencies": { "destr": "^2.0.5", "node-fetch-native": "^1.6.7", "ufo": "^1.6.1" } }, "sha512-2W4oUZlVaqAPAil6FUg/difl6YhqhUR7x2eZY4bQCko22UXg3hptq9KLQdqFClV+Wu85UX7hNtdGTngi/1BxcA=="],
|
||||||
|
|
||||||
"uri-js/punycode": ["punycode@2.3.1", "", {}, "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg=="],
|
"uri-js/punycode": ["punycode@2.3.1", "", {}, "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg=="],
|
||||||
|
|
||||||
"utif2/pako": ["pako@1.0.11", "", {}, "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw=="],
|
|
||||||
|
|
||||||
"vite-plugin-icons-spritesheet/glob": ["glob@11.1.0", "", { "dependencies": { "foreground-child": "^3.3.1", "jackspeak": "^4.1.1", "minimatch": "^10.1.1", "minipass": "^7.1.2", "package-json-from-dist": "^1.0.0", "path-scurry": "^2.0.0" }, "bin": { "glob": "dist/esm/bin.mjs" } }, "sha512-vuNwKSaKiqm7g0THUBu2x7ckSs3XJLXE+2ssL7/MfTGPLLcrJQ/4Uq1CjPTtO5cCIiRxqvN6Twy1qOwhL0Xjcw=="],
|
"vite-plugin-icons-spritesheet/glob": ["glob@11.1.0", "", { "dependencies": { "foreground-child": "^3.3.1", "jackspeak": "^4.1.1", "minimatch": "^10.1.1", "minipass": "^7.1.2", "package-json-from-dist": "^1.0.0", "path-scurry": "^2.0.0" }, "bin": { "glob": "dist/esm/bin.mjs" } }, "sha512-vuNwKSaKiqm7g0THUBu2x7ckSs3XJLXE+2ssL7/MfTGPLLcrJQ/4Uq1CjPTtO5cCIiRxqvN6Twy1qOwhL0Xjcw=="],
|
||||||
|
|
||||||
"vitest/@vitest/expect": ["@vitest/expect@4.0.18", "", { "dependencies": { "@standard-schema/spec": "^1.0.0", "@types/chai": "^5.2.2", "@vitest/spy": "4.0.18", "@vitest/utils": "4.0.18", "chai": "^6.2.1", "tinyrainbow": "^3.0.3" } }, "sha512-8sCWUyckXXYvx4opfzVY03EOiYVxyNrHS5QxX3DAIi5dpJAAkyJezHCP77VMX4HKA2LDT/Jpfo8i2r5BE3GnQQ=="],
|
"vitest/@vitest/expect": ["@vitest/expect@4.0.18", "", { "dependencies": { "@standard-schema/spec": "^1.0.0", "@types/chai": "^5.2.2", "@vitest/spy": "4.0.18", "@vitest/utils": "4.0.18", "chai": "^6.2.1", "tinyrainbow": "^3.0.3" } }, "sha512-8sCWUyckXXYvx4opfzVY03EOiYVxyNrHS5QxX3DAIi5dpJAAkyJezHCP77VMX4HKA2LDT/Jpfo8i2r5BE3GnQQ=="],
|
||||||
@@ -5693,16 +5685,6 @@
|
|||||||
|
|
||||||
"@actions/github/@octokit/plugin-rest-endpoint-methods/@octokit/types": ["@octokit/types@12.6.0", "", { "dependencies": { "@octokit/openapi-types": "^20.0.0" } }, "sha512-1rhSOfRa6H9w4YwK0yrf5faDaDTb+yLyBUKOCV4xtCDB5VmIPqd/v9yr9o6SAzOAlRxMiRiCic6JVM1/kunVkw=="],
|
"@actions/github/@octokit/plugin-rest-endpoint-methods/@octokit/types": ["@octokit/types@12.6.0", "", { "dependencies": { "@octokit/openapi-types": "^20.0.0" } }, "sha512-1rhSOfRa6H9w4YwK0yrf5faDaDTb+yLyBUKOCV4xtCDB5VmIPqd/v9yr9o6SAzOAlRxMiRiCic6JVM1/kunVkw=="],
|
||||||
|
|
||||||
"@ai-sdk/anthropic/@ai-sdk/provider-utils/@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="],
|
|
||||||
|
|
||||||
"@ai-sdk/anthropic/@ai-sdk/provider-utils/zod-to-json-schema": ["zod-to-json-schema@3.25.1", "", { "peerDependencies": { "zod": "^3.25 || ^4" } }, "sha512-pM/SU9d3YAggzi6MtR4h7ruuQlqKtad8e9S0fmxcMi+ueAK5Korys/aWcV9LIIHTVbj01NdzxcnXSN+O74ZIVA=="],
|
|
||||||
|
|
||||||
"@ai-sdk/azure/@ai-sdk/provider-utils/@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="],
|
|
||||||
|
|
||||||
"@ai-sdk/cerebras/@ai-sdk/provider-utils/@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="],
|
|
||||||
|
|
||||||
"@ai-sdk/cohere/@ai-sdk/provider-utils/@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="],
|
|
||||||
|
|
||||||
"@ai-sdk/deepgram/@ai-sdk/provider-utils/@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="],
|
"@ai-sdk/deepgram/@ai-sdk/provider-utils/@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="],
|
||||||
|
|
||||||
"@ai-sdk/deepseek/@ai-sdk/provider-utils/@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="],
|
"@ai-sdk/deepseek/@ai-sdk/provider-utils/@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="],
|
||||||
@@ -5711,28 +5693,6 @@
|
|||||||
|
|
||||||
"@ai-sdk/fireworks/@ai-sdk/provider-utils/@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="],
|
"@ai-sdk/fireworks/@ai-sdk/provider-utils/@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="],
|
||||||
|
|
||||||
"@ai-sdk/gateway/@ai-sdk/provider-utils/@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="],
|
|
||||||
|
|
||||||
"@ai-sdk/groq/@ai-sdk/provider-utils/@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="],
|
|
||||||
|
|
||||||
"@ai-sdk/mistral/@ai-sdk/provider-utils/@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="],
|
|
||||||
|
|
||||||
"@ai-sdk/openai-compatible/@ai-sdk/provider-utils/@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="],
|
|
||||||
|
|
||||||
"@ai-sdk/openai-compatible/@ai-sdk/provider-utils/zod-to-json-schema": ["zod-to-json-schema@3.25.1", "", { "peerDependencies": { "zod": "^3.25 || ^4" } }, "sha512-pM/SU9d3YAggzi6MtR4h7ruuQlqKtad8e9S0fmxcMi+ueAK5Korys/aWcV9LIIHTVbj01NdzxcnXSN+O74ZIVA=="],
|
|
||||||
|
|
||||||
"@ai-sdk/openai/@ai-sdk/provider-utils/@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="],
|
|
||||||
|
|
||||||
"@ai-sdk/openai/@ai-sdk/provider-utils/zod-to-json-schema": ["zod-to-json-schema@3.25.1", "", { "peerDependencies": { "zod": "^3.25 || ^4" } }, "sha512-pM/SU9d3YAggzi6MtR4h7ruuQlqKtad8e9S0fmxcMi+ueAK5Korys/aWcV9LIIHTVbj01NdzxcnXSN+O74ZIVA=="],
|
|
||||||
|
|
||||||
"@ai-sdk/perplexity/@ai-sdk/provider-utils/@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="],
|
|
||||||
|
|
||||||
"@ai-sdk/togetherai/@ai-sdk/provider-utils/@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="],
|
|
||||||
|
|
||||||
"@ai-sdk/vercel/@ai-sdk/provider-utils/@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="],
|
|
||||||
|
|
||||||
"@ai-sdk/xai/@ai-sdk/provider-utils/@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="],
|
|
||||||
|
|
||||||
"@astrojs/check/yargs/cliui": ["cliui@8.0.1", "", { "dependencies": { "string-width": "^4.2.0", "strip-ansi": "^6.0.1", "wrap-ansi": "^7.0.0" } }, "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ=="],
|
"@astrojs/check/yargs/cliui": ["cliui@8.0.1", "", { "dependencies": { "string-width": "^4.2.0", "strip-ansi": "^6.0.1", "wrap-ansi": "^7.0.0" } }, "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ=="],
|
||||||
|
|
||||||
"@astrojs/check/yargs/string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="],
|
"@astrojs/check/yargs/string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="],
|
||||||
@@ -6165,20 +6125,6 @@
|
|||||||
|
|
||||||
"accepts/mime-types/mime-db": ["mime-db@1.52.0", "", {}, "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg=="],
|
"accepts/mime-types/mime-db": ["mime-db@1.52.0", "", {}, "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg=="],
|
||||||
|
|
||||||
"ai-gateway-provider/@ai-sdk/google-vertex/@ai-sdk/anthropic": ["@ai-sdk/anthropic@2.0.56", "", { "dependencies": { "@ai-sdk/provider": "2.0.0", "@ai-sdk/provider-utils": "3.0.19" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-XHJKu0Yvfu9SPzRfsAFESa+9T7f2YJY6TxykKMfRsAwpeWAiX/Gbx5J5uM15AzYC3Rw8tVP3oH+j7jEivENirQ=="],
|
|
||||||
|
|
||||||
"ai-gateway-provider/@ai-sdk/google-vertex/@ai-sdk/google": ["@ai-sdk/google@2.0.46", "", { "dependencies": { "@ai-sdk/provider": "2.0.0", "@ai-sdk/provider-utils": "3.0.19" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-8PK6u4sGE/kXebd7ZkTp+0aya4kNqzoqpS5m7cHY2NfTK6fhPc6GNvE+MZIZIoHQTp5ed86wGBdeBPpFaaUtyg=="],
|
|
||||||
|
|
||||||
"ai-gateway-provider/@ai-sdk/google-vertex/@ai-sdk/provider": ["@ai-sdk/provider@2.0.0", "", { "dependencies": { "json-schema": "^0.4.0" } }, "sha512-6o7Y2SeO9vFKB8lArHXehNuusnpddKPk7xqL7T2/b+OvXMRIXUO1rR4wcv1hAFUAT9avGZshty3Wlua/XA7TvA=="],
|
|
||||||
|
|
||||||
"ai-gateway-provider/@ai-sdk/google-vertex/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@3.0.19", "", { "dependencies": { "@ai-sdk/provider": "2.0.0", "@standard-schema/spec": "^1.0.0", "eventsource-parser": "^3.0.6" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-W41Wc9/jbUVXVwCN/7bWa4IKe8MtxO3EyA0Hfhx6grnmiYlCvpI8neSYWFE0zScXJkgA/YK3BRybzgyiXuu6JA=="],
|
|
||||||
|
|
||||||
"ai-gateway-provider/@ai-sdk/openai/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@3.0.20", "", { "dependencies": { "@ai-sdk/provider": "2.0.1", "@standard-schema/spec": "^1.0.0", "eventsource-parser": "^3.0.6" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-iXHVe0apM2zUEzauqJwqmpC37A5rihrStAih5Ks+JE32iTe4LZ58y17UGBjpQQTCRw9YxMeo2UFLxLpBluyvLQ=="],
|
|
||||||
|
|
||||||
"ai-gateway-provider/@ai-sdk/openai-compatible/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@3.0.22", "", { "dependencies": { "@ai-sdk/provider": "2.0.1", "@standard-schema/spec": "^1.0.0", "eventsource-parser": "^3.0.6" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-fFT1KfUUKktfAFm5mClJhS1oux9tP2qgzmEZVl5UdwltQ1LO/s8hd7znVrgKzivwv1s1FIPza0s9OpJaNB/vHw=="],
|
|
||||||
|
|
||||||
"ai/@ai-sdk/provider-utils/@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="],
|
|
||||||
|
|
||||||
"ajv-keywords/ajv/json-schema-traverse": ["json-schema-traverse@0.4.1", "", {}, "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg=="],
|
"ajv-keywords/ajv/json-schema-traverse": ["json-schema-traverse@0.4.1", "", {}, "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg=="],
|
||||||
|
|
||||||
"ansi-align/string-width/emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="],
|
"ansi-align/string-width/emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="],
|
||||||
@@ -6271,12 +6217,14 @@
|
|||||||
|
|
||||||
"node-gyp/which/isexe": ["isexe@3.1.5", "", {}, "sha512-6B3tLtFqtQS4ekarvLVMZ+X+VlvQekbe4taUkf/rhVO3d/h0M2rfARm/pXLcPEsjjMsFgrFgSrhQIxcSVrBz8w=="],
|
"node-gyp/which/isexe": ["isexe@3.1.5", "", {}, "sha512-6B3tLtFqtQS4ekarvLVMZ+X+VlvQekbe4taUkf/rhVO3d/h0M2rfARm/pXLcPEsjjMsFgrFgSrhQIxcSVrBz8w=="],
|
||||||
|
|
||||||
"opencode/@ai-sdk/openai/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@3.0.20", "", { "dependencies": { "@ai-sdk/provider": "2.0.1", "@standard-schema/spec": "^1.0.0", "eventsource-parser": "^3.0.6" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-iXHVe0apM2zUEzauqJwqmpC37A5rihrStAih5Ks+JE32iTe4LZ58y17UGBjpQQTCRw9YxMeo2UFLxLpBluyvLQ=="],
|
"opencode-gitlab-auth/open/wsl-utils": ["wsl-utils@0.1.0", "", { "dependencies": { "is-wsl": "^3.1.0" } }, "sha512-h3Fbisa2nKGPxCpm89Hk33lBLsnaGBvctQopaBSOW/uIs6FTe1ATyAnKFJrzVs9vpGdsTe73WF3V4lIsk4Gacw=="],
|
||||||
|
|
||||||
"opencode/@ai-sdk/openai-compatible/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@3.0.20", "", { "dependencies": { "@ai-sdk/provider": "2.0.1", "@standard-schema/spec": "^1.0.0", "eventsource-parser": "^3.0.6" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-iXHVe0apM2zUEzauqJwqmpC37A5rihrStAih5Ks+JE32iTe4LZ58y17UGBjpQQTCRw9YxMeo2UFLxLpBluyvLQ=="],
|
"opencode-poe-auth/open/wsl-utils": ["wsl-utils@0.1.0", "", { "dependencies": { "is-wsl": "^3.1.0" } }, "sha512-h3Fbisa2nKGPxCpm89Hk33lBLsnaGBvctQopaBSOW/uIs6FTe1ATyAnKFJrzVs9vpGdsTe73WF3V4lIsk4Gacw=="],
|
||||||
|
|
||||||
"opencontrol/@modelcontextprotocol/sdk/express": ["express@5.2.1", "", { "dependencies": { "accepts": "^2.0.0", "body-parser": "^2.2.1", "content-disposition": "^1.0.0", "content-type": "^1.0.5", "cookie": "^0.7.1", "cookie-signature": "^1.2.1", "debug": "^4.4.0", "depd": "^2.0.0", "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "etag": "^1.8.1", "finalhandler": "^2.1.0", "fresh": "^2.0.0", "http-errors": "^2.0.0", "merge-descriptors": "^2.0.0", "mime-types": "^3.0.0", "on-finished": "^2.4.1", "once": "^1.4.0", "parseurl": "^1.3.3", "proxy-addr": "^2.0.7", "qs": "^6.14.0", "range-parser": "^1.2.1", "router": "^2.2.0", "send": "^1.1.0", "serve-static": "^2.2.0", "statuses": "^2.0.1", "type-is": "^2.0.1", "vary": "^1.1.2" } }, "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw=="],
|
"opencontrol/@modelcontextprotocol/sdk/express": ["express@5.2.1", "", { "dependencies": { "accepts": "^2.0.0", "body-parser": "^2.2.1", "content-disposition": "^1.0.0", "content-type": "^1.0.5", "cookie": "^0.7.1", "cookie-signature": "^1.2.1", "debug": "^4.4.0", "depd": "^2.0.0", "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "etag": "^1.8.1", "finalhandler": "^2.1.0", "fresh": "^2.0.0", "http-errors": "^2.0.0", "merge-descriptors": "^2.0.0", "mime-types": "^3.0.0", "on-finished": "^2.4.1", "once": "^1.4.0", "parseurl": "^1.3.3", "proxy-addr": "^2.0.7", "qs": "^6.14.0", "range-parser": "^1.2.1", "router": "^2.2.0", "send": "^1.1.0", "serve-static": "^2.2.0", "statuses": "^2.0.1", "type-is": "^2.0.1", "vary": "^1.1.2" } }, "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw=="],
|
||||||
|
|
||||||
|
"opencontrol/@modelcontextprotocol/sdk/express-rate-limit": ["express-rate-limit@7.5.1", "", { "peerDependencies": { "express": ">= 4.11" } }, "sha512-7iN8iPMDzOMHPUYllBEsQdWVB6fPDMPqwjBaFrgr4Jgr/+okjvzAy+UHlYYL/Vs0OsOrMkwS6PJDkFlJwoxUnw=="],
|
||||||
|
|
||||||
"opencontrol/@modelcontextprotocol/sdk/pkce-challenge": ["pkce-challenge@4.1.0", "", {}, "sha512-ZBmhE1C9LcPoH9XZSdwiPtbPHZROwAnMy+kIFQVrnMCxY4Cudlz3gBOpzilgc0jOgRaiT3sIWfpMomW2ar2orQ=="],
|
"opencontrol/@modelcontextprotocol/sdk/pkce-challenge": ["pkce-challenge@4.1.0", "", {}, "sha512-ZBmhE1C9LcPoH9XZSdwiPtbPHZROwAnMy+kIFQVrnMCxY4Cudlz3gBOpzilgc0jOgRaiT3sIWfpMomW2ar2orQ=="],
|
||||||
|
|
||||||
"opencontrol/@modelcontextprotocol/sdk/raw-body": ["raw-body@3.0.2", "", { "dependencies": { "bytes": "~3.1.2", "http-errors": "~2.0.1", "iconv-lite": "~0.7.0", "unpipe": "~1.0.0" } }, "sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA=="],
|
"opencontrol/@modelcontextprotocol/sdk/raw-body": ["raw-body@3.0.2", "", { "dependencies": { "bytes": "~3.1.2", "http-errors": "~2.0.1", "iconv-lite": "~0.7.0", "unpipe": "~1.0.0" } }, "sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA=="],
|
||||||
@@ -6529,12 +6477,6 @@
|
|||||||
|
|
||||||
"@solidjs/start/shiki/@shikijs/engine-javascript/oniguruma-to-es": ["oniguruma-to-es@2.3.0", "", { "dependencies": { "emoji-regex-xs": "^1.0.0", "regex": "^5.1.1", "regex-recursion": "^5.1.1" } }, "sha512-bwALDxriqfKGfUufKGGepCzu9x7nJQuoRoAFp4AnwehhC2crqrDIAP/uN2qdlsAvSMpeRC3+Yzhqc7hLmle5+g=="],
|
"@solidjs/start/shiki/@shikijs/engine-javascript/oniguruma-to-es": ["oniguruma-to-es@2.3.0", "", { "dependencies": { "emoji-regex-xs": "^1.0.0", "regex": "^5.1.1", "regex-recursion": "^5.1.1" } }, "sha512-bwALDxriqfKGfUufKGGepCzu9x7nJQuoRoAFp4AnwehhC2crqrDIAP/uN2qdlsAvSMpeRC3+Yzhqc7hLmle5+g=="],
|
||||||
|
|
||||||
"ai-gateway-provider/@ai-sdk/google-vertex/@ai-sdk/provider-utils/@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="],
|
|
||||||
|
|
||||||
"ai-gateway-provider/@ai-sdk/openai-compatible/@ai-sdk/provider-utils/@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="],
|
|
||||||
|
|
||||||
"ai-gateway-provider/@ai-sdk/openai/@ai-sdk/provider-utils/@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="],
|
|
||||||
|
|
||||||
"ansi-align/string-width/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="],
|
"ansi-align/string-width/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="],
|
||||||
|
|
||||||
"app-builder-lib/@electron/get/fs-extra/universalify": ["universalify@0.1.2", "", {}, "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg=="],
|
"app-builder-lib/@electron/get/fs-extra/universalify": ["universalify@0.1.2", "", {}, "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg=="],
|
||||||
@@ -6587,10 +6529,6 @@
|
|||||||
|
|
||||||
"js-beautify/glob/path-scurry/lru-cache": ["lru-cache@10.4.3", "", {}, "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ=="],
|
"js-beautify/glob/path-scurry/lru-cache": ["lru-cache@10.4.3", "", {}, "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ=="],
|
||||||
|
|
||||||
"opencode/@ai-sdk/openai-compatible/@ai-sdk/provider-utils/@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="],
|
|
||||||
|
|
||||||
"opencode/@ai-sdk/openai/@ai-sdk/provider-utils/@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="],
|
|
||||||
|
|
||||||
"opencontrol/@modelcontextprotocol/sdk/express/accepts": ["accepts@2.0.0", "", { "dependencies": { "mime-types": "^3.0.0", "negotiator": "^1.0.0" } }, "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng=="],
|
"opencontrol/@modelcontextprotocol/sdk/express/accepts": ["accepts@2.0.0", "", { "dependencies": { "mime-types": "^3.0.0", "negotiator": "^1.0.0" } }, "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng=="],
|
||||||
|
|
||||||
"opencontrol/@modelcontextprotocol/sdk/express/body-parser": ["body-parser@2.2.2", "", { "dependencies": { "bytes": "^3.1.2", "content-type": "^1.0.5", "debug": "^4.4.3", "http-errors": "^2.0.0", "iconv-lite": "^0.7.0", "on-finished": "^2.4.1", "qs": "^6.14.1", "raw-body": "^3.0.1", "type-is": "^2.0.1" } }, "sha512-oP5VkATKlNwcgvxi0vM0p/D3n2C3EReYVX+DNYs5TjZFn/oQt2j+4sVJtSMr18pdRr8wjTcBl6LoV+FUwzPmNA=="],
|
"opencontrol/@modelcontextprotocol/sdk/express/body-parser": ["body-parser@2.2.2", "", { "dependencies": { "bytes": "^3.1.2", "content-type": "^1.0.5", "debug": "^4.4.3", "http-errors": "^2.0.0", "iconv-lite": "^0.7.0", "on-finished": "^2.4.1", "qs": "^6.14.1", "raw-body": "^3.0.1", "type-is": "^2.0.1" } }, "sha512-oP5VkATKlNwcgvxi0vM0p/D3n2C3EReYVX+DNYs5TjZFn/oQt2j+4sVJtSMr18pdRr8wjTcBl6LoV+FUwzPmNA=="],
|
||||||
|
|||||||
Generated
+3
-3
@@ -2,11 +2,11 @@
|
|||||||
"nodes": {
|
"nodes": {
|
||||||
"nixpkgs": {
|
"nixpkgs": {
|
||||||
"locked": {
|
"locked": {
|
||||||
"lastModified": 1772091128,
|
"lastModified": 1773909469,
|
||||||
"narHash": "sha256-TnrYykX8Mf/Ugtkix6V+PjW7miU2yClA6uqWl/v6KWM=",
|
"narHash": "sha256-vglVrLfHjFIzIdV9A27Ugul6rh3I1qHbbitGW7dk420=",
|
||||||
"owner": "NixOS",
|
"owner": "NixOS",
|
||||||
"repo": "nixpkgs",
|
"repo": "nixpkgs",
|
||||||
"rev": "3f0336406035444b4a24b942788334af5f906259",
|
"rev": "7149c06513f335be57f26fcbbbe34afda923882b",
|
||||||
"type": "github"
|
"type": "github"
|
||||||
},
|
},
|
||||||
"original": {
|
"original": {
|
||||||
|
|||||||
@@ -496,7 +496,6 @@ async function subscribeSessionEvents() {
|
|||||||
|
|
||||||
const TOOL: Record<string, [string, string]> = {
|
const TOOL: Record<string, [string, string]> = {
|
||||||
todowrite: ["Todo", "\x1b[33m\x1b[1m"],
|
todowrite: ["Todo", "\x1b[33m\x1b[1m"],
|
||||||
todoread: ["Todo", "\x1b[33m\x1b[1m"],
|
|
||||||
bash: ["Bash", "\x1b[31m\x1b[1m"],
|
bash: ["Bash", "\x1b[31m\x1b[1m"],
|
||||||
edit: ["Edit", "\x1b[32m\x1b[1m"],
|
edit: ["Edit", "\x1b[32m\x1b[1m"],
|
||||||
glob: ["Glob", "\x1b[34m\x1b[1m"],
|
glob: ["Glob", "\x1b[34m\x1b[1m"],
|
||||||
|
|||||||
@@ -122,6 +122,7 @@ const ZEN_LITE_PRICE = new sst.Linkable("ZEN_LITE_PRICE", {
|
|||||||
properties: {
|
properties: {
|
||||||
product: zenLiteProduct.id,
|
product: zenLiteProduct.id,
|
||||||
price: zenLitePrice.id,
|
price: zenLitePrice.id,
|
||||||
|
priceInr: 92900,
|
||||||
firstMonth50Coupon: zenLiteCouponFirstMonth50.id,
|
firstMonth50Coupon: zenLiteCouponFirstMonth50.id,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
@@ -201,6 +202,10 @@ const bucketNew = new sst.cloudflare.Bucket("ZenDataNew")
|
|||||||
const AWS_SES_ACCESS_KEY_ID = new sst.Secret("AWS_SES_ACCESS_KEY_ID")
|
const AWS_SES_ACCESS_KEY_ID = new sst.Secret("AWS_SES_ACCESS_KEY_ID")
|
||||||
const AWS_SES_SECRET_ACCESS_KEY = new sst.Secret("AWS_SES_SECRET_ACCESS_KEY")
|
const AWS_SES_SECRET_ACCESS_KEY = new sst.Secret("AWS_SES_SECRET_ACCESS_KEY")
|
||||||
|
|
||||||
|
const SALESFORCE_CLIENT_ID = new sst.Secret("SALESFORCE_CLIENT_ID")
|
||||||
|
const SALESFORCE_CLIENT_SECRET = new sst.Secret("SALESFORCE_CLIENT_SECRET")
|
||||||
|
const SALESFORCE_INSTANCE_URL = new sst.Secret("SALESFORCE_INSTANCE_URL")
|
||||||
|
|
||||||
const logProcessor = new sst.cloudflare.Worker("LogProcessor", {
|
const logProcessor = new sst.cloudflare.Worker("LogProcessor", {
|
||||||
handler: "packages/console/function/src/log-processor.ts",
|
handler: "packages/console/function/src/log-processor.ts",
|
||||||
link: [new sst.Secret("HONEYCOMB_API_KEY")],
|
link: [new sst.Secret("HONEYCOMB_API_KEY")],
|
||||||
@@ -219,6 +224,9 @@ new sst.cloudflare.x.SolidStart("Console", {
|
|||||||
EMAILOCTOPUS_API_KEY,
|
EMAILOCTOPUS_API_KEY,
|
||||||
AWS_SES_ACCESS_KEY_ID,
|
AWS_SES_ACCESS_KEY_ID,
|
||||||
AWS_SES_SECRET_ACCESS_KEY,
|
AWS_SES_SECRET_ACCESS_KEY,
|
||||||
|
SALESFORCE_CLIENT_ID,
|
||||||
|
SALESFORCE_CLIENT_SECRET,
|
||||||
|
SALESFORCE_INSTANCE_URL,
|
||||||
ZEN_BLACK_PRICE,
|
ZEN_BLACK_PRICE,
|
||||||
ZEN_LITE_PRICE,
|
ZEN_LITE_PRICE,
|
||||||
new sst.Secret("ZEN_LIMITS"),
|
new sst.Secret("ZEN_LIMITS"),
|
||||||
|
|||||||
+4
-4
@@ -1,8 +1,8 @@
|
|||||||
{
|
{
|
||||||
"nodeModules": {
|
"nodeModules": {
|
||||||
"x86_64-linux": "sha256-WJgo6UclmtQOEubnKMZybdIEhZ1uRTucF61yojjd+l0=",
|
"x86_64-linux": "sha256-UuVbB5lTRB4bIcaKMc8CLSbQW7m9EjXgxYvxp/uO7Co=",
|
||||||
"aarch64-linux": "sha256-QfZ/g7EZFpe6ndR3dG8WvVfMj5Kyd/R/4kkTJfGJxL4=",
|
"aarch64-linux": "sha256-8D7ReLRVb7NDd5PQTVxFhRLmlLbfjK007XgIhhpNKoE=",
|
||||||
"aarch64-darwin": "sha256-ezr/R70XJr9eN5l3mgb7HzLF6QsofNEKUOtuxbfli80=",
|
"aarch64-darwin": "sha256-M+z7C/eXfVqwDiGiiwKo/LT/m4dvCjL1Pblsr1kxoyI=",
|
||||||
"x86_64-darwin": "sha256-MbsBGS415uEU/n1RQ/5H5pqh+udLY3+oimJ+eS5uJVI="
|
"x86_64-darwin": "sha256-RzZS6GMwYVDPK0W+K/mlebixNMs2+JRkMG9n8OFhd0c="
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ let
|
|||||||
in
|
in
|
||||||
stdenvNoCC.mkDerivation {
|
stdenvNoCC.mkDerivation {
|
||||||
pname = "opencode-node_modules";
|
pname = "opencode-node_modules";
|
||||||
version = "${packageJson.version}-${rev}";
|
version = "${packageJson.version}+${lib.replaceString "-" "." rev}";
|
||||||
|
|
||||||
src = lib.fileset.toSource {
|
src = lib.fileset.toSource {
|
||||||
root = ../.;
|
root = ../.;
|
||||||
@@ -54,6 +54,7 @@ stdenvNoCC.mkDerivation {
|
|||||||
--filter '!./' \
|
--filter '!./' \
|
||||||
--filter './packages/opencode' \
|
--filter './packages/opencode' \
|
||||||
--filter './packages/desktop' \
|
--filter './packages/desktop' \
|
||||||
|
--filter './packages/app' \
|
||||||
--frozen-lockfile \
|
--frozen-lockfile \
|
||||||
--ignore-scripts \
|
--ignore-scripts \
|
||||||
--no-progress
|
--no-progress
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
stdenvNoCC,
|
stdenvNoCC,
|
||||||
callPackage,
|
callPackage,
|
||||||
bun,
|
bun,
|
||||||
|
nodejs,
|
||||||
sysctl,
|
sysctl,
|
||||||
makeBinaryWrapper,
|
makeBinaryWrapper,
|
||||||
models-dev,
|
models-dev,
|
||||||
@@ -19,6 +20,7 @@ stdenvNoCC.mkDerivation (finalAttrs: {
|
|||||||
|
|
||||||
nativeBuildInputs = [
|
nativeBuildInputs = [
|
||||||
bun
|
bun
|
||||||
|
nodejs # for patchShebangs node_modules
|
||||||
installShellFiles
|
installShellFiles
|
||||||
makeBinaryWrapper
|
makeBinaryWrapper
|
||||||
models-dev
|
models-dev
|
||||||
@@ -29,6 +31,8 @@ stdenvNoCC.mkDerivation (finalAttrs: {
|
|||||||
runHook preConfigure
|
runHook preConfigure
|
||||||
|
|
||||||
cp -R ${finalAttrs.node_modules}/. .
|
cp -R ${finalAttrs.node_modules}/. .
|
||||||
|
patchShebangs node_modules
|
||||||
|
patchShebangs packages/*/node_modules
|
||||||
|
|
||||||
runHook postConfigure
|
runHook postConfigure
|
||||||
'';
|
'';
|
||||||
|
|||||||
+13
-7
@@ -4,11 +4,12 @@
|
|||||||
"description": "AI-powered development tool",
|
"description": "AI-powered development tool",
|
||||||
"private": true,
|
"private": true,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"packageManager": "bun@1.3.10",
|
"packageManager": "bun@1.3.11",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "bun run --cwd packages/opencode --conditions=browser src/index.ts",
|
"dev": "bun run --cwd packages/opencode --conditions=browser src/index.ts",
|
||||||
"dev:desktop": "bun --cwd packages/desktop tauri dev",
|
"dev:desktop": "bun --cwd packages/desktop tauri dev",
|
||||||
"dev:web": "bun --cwd packages/app dev",
|
"dev:web": "bun --cwd packages/app dev",
|
||||||
|
"dev:console": "ulimit -n 10240 2>/dev/null; bun run --cwd packages/console/app dev",
|
||||||
"dev:storybook": "bun --cwd packages/storybook storybook",
|
"dev:storybook": "bun --cwd packages/storybook storybook",
|
||||||
"typecheck": "bun turbo typecheck",
|
"typecheck": "bun turbo typecheck",
|
||||||
"prepare": "husky",
|
"prepare": "husky",
|
||||||
@@ -24,7 +25,8 @@
|
|||||||
"packages/slack"
|
"packages/slack"
|
||||||
],
|
],
|
||||||
"catalog": {
|
"catalog": {
|
||||||
"@types/bun": "1.3.9",
|
"@effect/platform-node": "4.0.0-beta.42",
|
||||||
|
"@types/bun": "1.3.11",
|
||||||
"@octokit/rest": "22.0.0",
|
"@octokit/rest": "22.0.0",
|
||||||
"@hono/zod-validator": "0.4.2",
|
"@hono/zod-validator": "0.4.2",
|
||||||
"ulid": "3.0.1",
|
"ulid": "3.0.1",
|
||||||
@@ -41,16 +43,17 @@
|
|||||||
"@tailwindcss/vite": "4.1.11",
|
"@tailwindcss/vite": "4.1.11",
|
||||||
"diff": "8.0.2",
|
"diff": "8.0.2",
|
||||||
"dompurify": "3.3.1",
|
"dompurify": "3.3.1",
|
||||||
"drizzle-kit": "1.0.0-beta.16-ea816b6",
|
"drizzle-kit": "1.0.0-beta.19-d95b7a4",
|
||||||
"drizzle-orm": "1.0.0-beta.16-ea816b6",
|
"drizzle-orm": "1.0.0-beta.19-d95b7a4",
|
||||||
"effect": "4.0.0-beta.31",
|
"effect": "4.0.0-beta.42",
|
||||||
"ai": "5.0.124",
|
"ai": "6.0.138",
|
||||||
"hono": "4.10.7",
|
"hono": "4.10.7",
|
||||||
"hono-openapi": "1.1.2",
|
"hono-openapi": "1.1.2",
|
||||||
"fuzzysort": "3.1.0",
|
"fuzzysort": "3.1.0",
|
||||||
"luxon": "3.6.1",
|
"luxon": "3.6.1",
|
||||||
"marked": "17.0.1",
|
"marked": "17.0.1",
|
||||||
"marked-shiki": "1.2.1",
|
"marked-shiki": "1.2.1",
|
||||||
|
"remend": "1.3.0",
|
||||||
"@playwright/test": "1.51.0",
|
"@playwright/test": "1.51.0",
|
||||||
"typescript": "5.8.2",
|
"typescript": "5.8.2",
|
||||||
"@typescript/native-preview": "7.0.0-dev.20251207.1",
|
"@typescript/native-preview": "7.0.0-dev.20251207.1",
|
||||||
@@ -101,6 +104,7 @@
|
|||||||
"protobufjs",
|
"protobufjs",
|
||||||
"tree-sitter",
|
"tree-sitter",
|
||||||
"tree-sitter-bash",
|
"tree-sitter-bash",
|
||||||
|
"tree-sitter-powershell",
|
||||||
"web-tree-sitter",
|
"web-tree-sitter",
|
||||||
"electron"
|
"electron"
|
||||||
],
|
],
|
||||||
@@ -110,6 +114,8 @@
|
|||||||
},
|
},
|
||||||
"patchedDependencies": {
|
"patchedDependencies": {
|
||||||
"@standard-community/standard-openapi@0.2.9": "patches/@standard-community%2Fstandard-openapi@0.2.9.patch",
|
"@standard-community/standard-openapi@0.2.9": "patches/@standard-community%2Fstandard-openapi@0.2.9.patch",
|
||||||
"@openrouter/ai-sdk-provider@1.5.4": "patches/@openrouter%2Fai-sdk-provider@1.5.4.patch"
|
"solid-js@1.9.10": "patches/solid-js@1.9.10.patch",
|
||||||
|
"@ai-sdk/provider-utils@4.0.21": "patches/@ai-sdk%2Fprovider-utils@4.0.21.patch",
|
||||||
|
"@ai-sdk/anthropic@3.0.64": "patches/@ai-sdk%2Fanthropic@3.0.64.patch"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -174,6 +174,8 @@ await page.keyboard.press(`${modKey}+Comma`) // Open settings
|
|||||||
- In terminal tests, type through the browser. Do not write to the PTY through the SDK.
|
- In terminal tests, type through the browser. Do not write to the PTY through the SDK.
|
||||||
- Use `waitTerminalReady(page, { term? })` and `runTerminal(page, { cmd, token, term?, timeout? })` from `actions.ts`.
|
- Use `waitTerminalReady(page, { term? })` and `runTerminal(page, { cmd, token, term?, timeout? })` from `actions.ts`.
|
||||||
- These helpers use the fixture-enabled test-only terminal driver and wait for output after the terminal writer settles.
|
- These helpers use the fixture-enabled test-only terminal driver and wait for output after the terminal writer settles.
|
||||||
|
- After opening the terminal, use `waitTerminalFocusIdle(...)` before the next keyboard action when prompt focus or keyboard routing matters.
|
||||||
|
- This avoids racing terminal mount, focus handoff, and prompt readiness when the next step types or sends shortcuts.
|
||||||
- Avoid `waitForTimeout` and custom DOM or `data-*` readiness checks.
|
- Avoid `waitForTimeout` and custom DOM or `data-*` readiness checks.
|
||||||
|
|
||||||
### Wait on state
|
### Wait on state
|
||||||
@@ -182,6 +184,9 @@ await page.keyboard.press(`${modKey}+Comma`) // Open settings
|
|||||||
- Avoid race-prone flows that assume work is finished after an action
|
- Avoid race-prone flows that assume work is finished after an action
|
||||||
- Wait or poll on observable state with `expect(...)`, `expect.poll(...)`, or existing helpers
|
- Wait or poll on observable state with `expect(...)`, `expect.poll(...)`, or existing helpers
|
||||||
- Prefer locator assertions like `toBeVisible()`, `toHaveCount(0)`, and `toHaveAttribute(...)` for normal UI state, and reserve `expect.poll(...)` for probe, mock, or backend state
|
- Prefer locator assertions like `toBeVisible()`, `toHaveCount(0)`, and `toHaveAttribute(...)` for normal UI state, and reserve `expect.poll(...)` for probe, mock, or backend state
|
||||||
|
- Prefer semantic app state over transient DOM visibility when behavior depends on active selection, focus ownership, or async retry loops
|
||||||
|
- Do not treat a visible element as proof that the app will route the next action to it
|
||||||
|
- When fixing a flake, validate with `--repeat-each` and multiple workers when practical
|
||||||
|
|
||||||
### Add hooks
|
### Add hooks
|
||||||
|
|
||||||
@@ -189,11 +194,16 @@ await page.keyboard.press(`${modKey}+Comma`) // Open settings
|
|||||||
- Keep these hooks minimal and purpose-built, following the style of `packages/app/src/testing/terminal.ts`
|
- Keep these hooks minimal and purpose-built, following the style of `packages/app/src/testing/terminal.ts`
|
||||||
- Test-only hooks must be inert unless explicitly enabled; do not add normal-runtime listeners, reactive subscriptions, or per-update allocations for e2e ceremony
|
- Test-only hooks must be inert unless explicitly enabled; do not add normal-runtime listeners, reactive subscriptions, or per-update allocations for e2e ceremony
|
||||||
- When mocking routes or APIs, expose explicit mock state and wait on that before asserting post-action UI
|
- When mocking routes or APIs, expose explicit mock state and wait on that before asserting post-action UI
|
||||||
|
- Add minimal test-only probes for semantic state like the active list item or selected command when DOM intermediates are unstable
|
||||||
|
- Prefer probing committed app state over asserting on transient highlight, visibility, or animation states
|
||||||
|
|
||||||
### Prefer helpers
|
### Prefer helpers
|
||||||
|
|
||||||
- Prefer fluent helpers and drivers when they make intent obvious and reduce locator-heavy noise
|
- Prefer fluent helpers and drivers when they make intent obvious and reduce locator-heavy noise
|
||||||
- Use direct locators when the interaction is simple and a helper would not add clarity
|
- Use direct locators when the interaction is simple and a helper would not add clarity
|
||||||
|
- Prefer helpers that both perform an action and verify the app consumed it
|
||||||
|
- Avoid composing helpers redundantly when one already includes the other or already waits for the resulting state
|
||||||
|
- If a helper already covers the required wait or verification, use it directly instead of layering extra clicks, keypresses, or assertions
|
||||||
|
|
||||||
## Writing New Tests
|
## Writing New Tests
|
||||||
|
|
||||||
|
|||||||
+245
-9
@@ -1,3 +1,4 @@
|
|||||||
|
import { base64Decode, base64Encode } from "@opencode-ai/util/encode"
|
||||||
import { expect, type Locator, type Page } from "@playwright/test"
|
import { expect, type Locator, type Page } from "@playwright/test"
|
||||||
import fs from "node:fs/promises"
|
import fs from "node:fs/promises"
|
||||||
import os from "node:os"
|
import os from "node:os"
|
||||||
@@ -8,6 +9,7 @@ import { createSdk, modKey, resolveDirectory, serverUrl } from "./utils"
|
|||||||
import {
|
import {
|
||||||
dropdownMenuTriggerSelector,
|
dropdownMenuTriggerSelector,
|
||||||
dropdownMenuContentSelector,
|
dropdownMenuContentSelector,
|
||||||
|
projectSwitchSelector,
|
||||||
projectMenuTriggerSelector,
|
projectMenuTriggerSelector,
|
||||||
projectCloseMenuSelector,
|
projectCloseMenuSelector,
|
||||||
projectWorkspacesToggleSelector,
|
projectWorkspacesToggleSelector,
|
||||||
@@ -16,11 +18,22 @@ import {
|
|||||||
listItemSelector,
|
listItemSelector,
|
||||||
listItemKeySelector,
|
listItemKeySelector,
|
||||||
listItemKeyStartsWithSelector,
|
listItemKeyStartsWithSelector,
|
||||||
|
promptSelector,
|
||||||
terminalSelector,
|
terminalSelector,
|
||||||
workspaceItemSelector,
|
workspaceItemSelector,
|
||||||
workspaceMenuTriggerSelector,
|
workspaceMenuTriggerSelector,
|
||||||
} from "./selectors"
|
} from "./selectors"
|
||||||
|
|
||||||
|
const phase = new WeakMap<Page, "test" | "cleanup">()
|
||||||
|
|
||||||
|
export function setHealthPhase(page: Page, value: "test" | "cleanup") {
|
||||||
|
phase.set(page, value)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function healthPhase(page: Page) {
|
||||||
|
return phase.get(page) ?? "test"
|
||||||
|
}
|
||||||
|
|
||||||
export async function defocus(page: Page) {
|
export async function defocus(page: Page) {
|
||||||
await page
|
await page
|
||||||
.evaluate(() => {
|
.evaluate(() => {
|
||||||
@@ -61,6 +74,15 @@ async function terminalReady(page: Page, term?: Locator) {
|
|||||||
}, id)
|
}, id)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function terminalFocusIdle(page: Page, term?: Locator) {
|
||||||
|
const next = term ?? page.locator(terminalSelector).first()
|
||||||
|
const id = await terminalID(next)
|
||||||
|
return page.evaluate((id) => {
|
||||||
|
const state = (window as E2EWindow).__opencode_e2e?.terminal?.terminals?.[id]
|
||||||
|
return (state?.focusing ?? 0) === 0
|
||||||
|
}, id)
|
||||||
|
}
|
||||||
|
|
||||||
async function terminalHas(page: Page, input: { term?: Locator; token: string }) {
|
async function terminalHas(page: Page, input: { term?: Locator; token: string }) {
|
||||||
const next = input.term ?? page.locator(terminalSelector).first()
|
const next = input.term ?? page.locator(terminalSelector).first()
|
||||||
const id = await terminalID(next)
|
const id = await terminalID(next)
|
||||||
@@ -73,6 +95,29 @@ async function terminalHas(page: Page, input: { term?: Locator; token: string })
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function promptSlashActive(page: Page, id: string) {
|
||||||
|
return page.evaluate((id) => {
|
||||||
|
const state = (window as E2EWindow).__opencode_e2e?.prompt?.current
|
||||||
|
if (state?.popover !== "slash") return false
|
||||||
|
if (!state.slash.ids.includes(id)) return false
|
||||||
|
return state.slash.active === id
|
||||||
|
}, id)
|
||||||
|
}
|
||||||
|
|
||||||
|
async function promptSlashSelects(page: Page) {
|
||||||
|
return page.evaluate(() => {
|
||||||
|
return (window as E2EWindow).__opencode_e2e?.prompt?.current?.selects ?? 0
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
async function promptSlashSelected(page: Page, input: { id: string; count: number }) {
|
||||||
|
return page.evaluate((input) => {
|
||||||
|
const state = (window as E2EWindow).__opencode_e2e?.prompt?.current
|
||||||
|
if (!state) return false
|
||||||
|
return state.selected === input.id && state.selects >= input.count
|
||||||
|
}, input)
|
||||||
|
}
|
||||||
|
|
||||||
export async function waitTerminalReady(page: Page, input?: { term?: Locator; timeout?: number }) {
|
export async function waitTerminalReady(page: Page, input?: { term?: Locator; timeout?: number }) {
|
||||||
const term = input?.term ?? page.locator(terminalSelector).first()
|
const term = input?.term ?? page.locator(terminalSelector).first()
|
||||||
const timeout = input?.timeout ?? 10_000
|
const timeout = input?.timeout ?? 10_000
|
||||||
@@ -81,6 +126,43 @@ export async function waitTerminalReady(page: Page, input?: { term?: Locator; ti
|
|||||||
await expect.poll(() => terminalReady(page, term), { timeout }).toBe(true)
|
await expect.poll(() => terminalReady(page, term), { timeout }).toBe(true)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function waitTerminalFocusIdle(page: Page, input?: { term?: Locator; timeout?: number }) {
|
||||||
|
const term = input?.term ?? page.locator(terminalSelector).first()
|
||||||
|
const timeout = input?.timeout ?? 10_000
|
||||||
|
await waitTerminalReady(page, { term, timeout })
|
||||||
|
await expect.poll(() => terminalFocusIdle(page, term), { timeout }).toBe(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function showPromptSlash(
|
||||||
|
page: Page,
|
||||||
|
input: { id: string; text: string; prompt?: Locator; timeout?: number },
|
||||||
|
) {
|
||||||
|
const prompt = input.prompt ?? page.locator(promptSelector)
|
||||||
|
const timeout = input.timeout ?? 10_000
|
||||||
|
await expect
|
||||||
|
.poll(
|
||||||
|
async () => {
|
||||||
|
await prompt.click().catch(() => false)
|
||||||
|
await prompt.fill(input.text).catch(() => false)
|
||||||
|
return promptSlashActive(page, input.id).catch(() => false)
|
||||||
|
},
|
||||||
|
{ timeout },
|
||||||
|
)
|
||||||
|
.toBe(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function runPromptSlash(
|
||||||
|
page: Page,
|
||||||
|
input: { id: string; text: string; prompt?: Locator; timeout?: number },
|
||||||
|
) {
|
||||||
|
const prompt = input.prompt ?? page.locator(promptSelector)
|
||||||
|
const timeout = input.timeout ?? 10_000
|
||||||
|
const count = await promptSlashSelects(page)
|
||||||
|
await showPromptSlash(page, input)
|
||||||
|
await prompt.press("Enter")
|
||||||
|
await expect.poll(() => promptSlashSelected(page, { id: input.id, count: count + 1 }), { timeout }).toBe(true)
|
||||||
|
}
|
||||||
|
|
||||||
export async function runTerminal(page: Page, input: { cmd: string; token: string; term?: Locator; timeout?: number }) {
|
export async function runTerminal(page: Page, input: { cmd: string; token: string; term?: Locator; timeout?: number }) {
|
||||||
const term = input.term ?? page.locator(terminalSelector).first()
|
const term = input.term ?? page.locator(terminalSelector).first()
|
||||||
const timeout = input.timeout ?? 10_000
|
const timeout = input.timeout ?? 10_000
|
||||||
@@ -93,9 +175,9 @@ export async function runTerminal(page: Page, input: { cmd: string; token: strin
|
|||||||
await expect.poll(() => terminalHas(page, { term, token: input.token }), { timeout }).toBe(true)
|
await expect.poll(() => terminalHas(page, { term, token: input.token }), { timeout }).toBe(true)
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function openPalette(page: Page) {
|
export async function openPalette(page: Page, key = "K") {
|
||||||
await defocus(page)
|
await defocus(page)
|
||||||
await page.keyboard.press(`${modKey}+P`)
|
await page.keyboard.press(`${modKey}+${key}`)
|
||||||
|
|
||||||
const dialog = page.getByRole("dialog")
|
const dialog = page.getByRole("dialog")
|
||||||
await expect(dialog).toBeVisible()
|
await expect(dialog).toBeVisible()
|
||||||
@@ -125,11 +207,51 @@ export async function closeDialog(page: Page, dialog: Locator) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export async function isSidebarClosed(page: Page) {
|
export async function isSidebarClosed(page: Page) {
|
||||||
const button = page.getByRole("button", { name: /toggle sidebar/i }).first()
|
const button = await waitSidebarButton(page, "isSidebarClosed")
|
||||||
await expect(button).toBeVisible()
|
|
||||||
return (await button.getAttribute("aria-expanded")) !== "true"
|
return (await button.getAttribute("aria-expanded")) !== "true"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function errorBoundaryText(page: Page) {
|
||||||
|
const title = page.getByRole("heading", { name: /something went wrong/i }).first()
|
||||||
|
if (!(await title.isVisible().catch(() => false))) return
|
||||||
|
|
||||||
|
const description = await page
|
||||||
|
.getByText(/an error occurred while loading the application\./i)
|
||||||
|
.first()
|
||||||
|
.textContent()
|
||||||
|
.catch(() => "")
|
||||||
|
const detail = await page
|
||||||
|
.getByRole("textbox", { name: /error details/i })
|
||||||
|
.first()
|
||||||
|
.inputValue()
|
||||||
|
.catch(async () =>
|
||||||
|
(
|
||||||
|
(await page
|
||||||
|
.getByRole("textbox", { name: /error details/i })
|
||||||
|
.first()
|
||||||
|
.textContent()
|
||||||
|
.catch(() => "")) ?? ""
|
||||||
|
).trim(),
|
||||||
|
)
|
||||||
|
|
||||||
|
return [title ? "Error boundary" : "", description ?? "", detail ?? ""].filter(Boolean).join("\n")
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function assertHealthy(page: Page, context: string) {
|
||||||
|
const text = await errorBoundaryText(page)
|
||||||
|
if (!text) return
|
||||||
|
console.log(`[e2e:error-boundary][${context}]\n${text}`)
|
||||||
|
throw new Error(`Error boundary during ${context}\n${text}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
async function waitSidebarButton(page: Page, context: string) {
|
||||||
|
const button = page.getByRole("button", { name: /toggle sidebar/i }).first()
|
||||||
|
const boundary = page.getByRole("heading", { name: /something went wrong/i }).first()
|
||||||
|
await button.or(boundary).first().waitFor({ state: "visible", timeout: 10_000 })
|
||||||
|
await assertHealthy(page, context)
|
||||||
|
return button
|
||||||
|
}
|
||||||
|
|
||||||
export async function toggleSidebar(page: Page) {
|
export async function toggleSidebar(page: Page) {
|
||||||
await defocus(page)
|
await defocus(page)
|
||||||
await page.keyboard.press(`${modKey}+B`)
|
await page.keyboard.press(`${modKey}+B`)
|
||||||
@@ -138,7 +260,7 @@ export async function toggleSidebar(page: Page) {
|
|||||||
export async function openSidebar(page: Page) {
|
export async function openSidebar(page: Page) {
|
||||||
if (!(await isSidebarClosed(page))) return
|
if (!(await isSidebarClosed(page))) return
|
||||||
|
|
||||||
const button = page.getByRole("button", { name: /toggle sidebar/i }).first()
|
const button = await waitSidebarButton(page, "openSidebar")
|
||||||
await button.click()
|
await button.click()
|
||||||
|
|
||||||
const opened = await expect(button)
|
const opened = await expect(button)
|
||||||
@@ -155,7 +277,7 @@ export async function openSidebar(page: Page) {
|
|||||||
export async function closeSidebar(page: Page) {
|
export async function closeSidebar(page: Page) {
|
||||||
if (await isSidebarClosed(page)) return
|
if (await isSidebarClosed(page)) return
|
||||||
|
|
||||||
const button = page.getByRole("button", { name: /toggle sidebar/i }).first()
|
const button = await waitSidebarButton(page, "closeSidebar")
|
||||||
await button.click()
|
await button.click()
|
||||||
|
|
||||||
const closed = await expect(button)
|
const closed = await expect(button)
|
||||||
@@ -170,6 +292,7 @@ export async function closeSidebar(page: Page) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export async function openSettings(page: Page) {
|
export async function openSettings(page: Page) {
|
||||||
|
await assertHealthy(page, "openSettings")
|
||||||
await defocus(page)
|
await defocus(page)
|
||||||
|
|
||||||
const dialog = page.getByRole("dialog")
|
const dialog = page.getByRole("dialog")
|
||||||
@@ -182,6 +305,8 @@ export async function openSettings(page: Page) {
|
|||||||
|
|
||||||
if (opened) return dialog
|
if (opened) return dialog
|
||||||
|
|
||||||
|
await assertHealthy(page, "openSettings")
|
||||||
|
|
||||||
await page.getByRole("button", { name: "Settings" }).first().click()
|
await page.getByRole("button", { name: "Settings" }).first().click()
|
||||||
await expect(dialog).toBeVisible()
|
await expect(dialog).toBeVisible()
|
||||||
return dialog
|
return dialog
|
||||||
@@ -243,10 +368,12 @@ export async function seedProjects(page: Page, input: { directory: string; extra
|
|||||||
|
|
||||||
export async function createTestProject() {
|
export async function createTestProject() {
|
||||||
const root = await fs.mkdtemp(path.join(os.tmpdir(), "opencode-e2e-project-"))
|
const root = await fs.mkdtemp(path.join(os.tmpdir(), "opencode-e2e-project-"))
|
||||||
|
const id = `e2e-${path.basename(root)}`
|
||||||
|
|
||||||
await fs.writeFile(path.join(root, "README.md"), "# e2e\n")
|
await fs.writeFile(path.join(root, "README.md"), `# e2e\n\n${id}\n`)
|
||||||
|
|
||||||
execSync("git init", { cwd: root, stdio: "ignore" })
|
execSync("git init", { cwd: root, stdio: "ignore" })
|
||||||
|
await fs.writeFile(path.join(root, ".git", "opencode"), id)
|
||||||
execSync("git config core.fsmonitor false", { cwd: root, stdio: "ignore" })
|
execSync("git config core.fsmonitor false", { cwd: root, stdio: "ignore" })
|
||||||
execSync("git add -A", { cwd: root, stdio: "ignore" })
|
execSync("git add -A", { cwd: root, stdio: "ignore" })
|
||||||
execSync('git -c user.name="e2e" -c user.email="e2e@example.com" commit -m "init" --allow-empty', {
|
execSync('git -c user.name="e2e" -c user.email="e2e@example.com" commit -m "init" --allow-empty', {
|
||||||
@@ -268,12 +395,24 @@ export function slugFromUrl(url: string) {
|
|||||||
return /\/([^/]+)\/session(?:[/?#]|$)/.exec(url)?.[1] ?? ""
|
return /\/([^/]+)\/session(?:[/?#]|$)/.exec(url)?.[1] ?? ""
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function probeSession(page: Page) {
|
||||||
|
return page
|
||||||
|
.evaluate(() => {
|
||||||
|
const win = window as E2EWindow
|
||||||
|
const current = win.__opencode_e2e?.model?.current
|
||||||
|
if (!current) return null
|
||||||
|
return { dir: current.dir, sessionID: current.sessionID }
|
||||||
|
})
|
||||||
|
.catch(() => null as { dir?: string; sessionID?: string } | null)
|
||||||
|
}
|
||||||
|
|
||||||
export async function waitSlug(page: Page, skip: string[] = []) {
|
export async function waitSlug(page: Page, skip: string[] = []) {
|
||||||
let prev = ""
|
let prev = ""
|
||||||
let next = ""
|
let next = ""
|
||||||
await expect
|
await expect
|
||||||
.poll(
|
.poll(
|
||||||
() => {
|
async () => {
|
||||||
|
await assertHealthy(page, "waitSlug")
|
||||||
const slug = slugFromUrl(page.url())
|
const slug = slugFromUrl(page.url())
|
||||||
if (!slug) return ""
|
if (!slug) return ""
|
||||||
if (skip.includes(slug)) return ""
|
if (skip.includes(slug)) return ""
|
||||||
@@ -291,6 +430,97 @@ export async function waitSlug(page: Page, skip: string[] = []) {
|
|||||||
return next
|
return next
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function resolveSlug(slug: string) {
|
||||||
|
const directory = base64Decode(slug)
|
||||||
|
if (!directory) throw new Error(`Failed to decode workspace slug: ${slug}`)
|
||||||
|
const resolved = await resolveDirectory(directory)
|
||||||
|
return { directory: resolved, slug: base64Encode(resolved), raw: slug }
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function waitDir(page: Page, directory: string) {
|
||||||
|
const target = await resolveDirectory(directory)
|
||||||
|
await expect
|
||||||
|
.poll(
|
||||||
|
async () => {
|
||||||
|
await assertHealthy(page, "waitDir")
|
||||||
|
const slug = slugFromUrl(page.url())
|
||||||
|
if (!slug) return ""
|
||||||
|
return resolveSlug(slug)
|
||||||
|
.then((item) => item.directory)
|
||||||
|
.catch(() => "")
|
||||||
|
},
|
||||||
|
{ timeout: 45_000 },
|
||||||
|
)
|
||||||
|
.toBe(target)
|
||||||
|
return { directory: target, slug: base64Encode(target) }
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function waitSession(page: Page, input: { directory: string; sessionID?: string }) {
|
||||||
|
const target = await resolveDirectory(input.directory)
|
||||||
|
await expect
|
||||||
|
.poll(
|
||||||
|
async () => {
|
||||||
|
await assertHealthy(page, "waitSession")
|
||||||
|
const slug = slugFromUrl(page.url())
|
||||||
|
if (!slug) return false
|
||||||
|
const resolved = await resolveSlug(slug).catch(() => undefined)
|
||||||
|
if (!resolved || resolved.directory !== target) return false
|
||||||
|
const current = sessionIDFromUrl(page.url())
|
||||||
|
if (input.sessionID && current !== input.sessionID) return false
|
||||||
|
if (!input.sessionID && current) return false
|
||||||
|
|
||||||
|
const state = await probeSession(page)
|
||||||
|
if (input.sessionID && (!state || state.sessionID !== input.sessionID)) return false
|
||||||
|
if (!input.sessionID && state?.sessionID) return false
|
||||||
|
if (state?.dir) {
|
||||||
|
const dir = await resolveDirectory(state.dir).catch(() => state.dir ?? "")
|
||||||
|
if (dir !== target) return false
|
||||||
|
}
|
||||||
|
|
||||||
|
return page
|
||||||
|
.locator(promptSelector)
|
||||||
|
.first()
|
||||||
|
.isVisible()
|
||||||
|
.catch(() => false)
|
||||||
|
},
|
||||||
|
{ timeout: 45_000 },
|
||||||
|
)
|
||||||
|
.toBe(true)
|
||||||
|
return { directory: target, slug: base64Encode(target) }
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function waitSessionSaved(directory: string, sessionID: string, timeout = 30_000) {
|
||||||
|
const sdk = createSdk(directory)
|
||||||
|
const target = await resolveDirectory(directory)
|
||||||
|
|
||||||
|
await expect
|
||||||
|
.poll(
|
||||||
|
async () => {
|
||||||
|
const data = await sdk.session
|
||||||
|
.get({ sessionID })
|
||||||
|
.then((x) => x.data)
|
||||||
|
.catch(() => undefined)
|
||||||
|
if (!data?.directory) return ""
|
||||||
|
return resolveDirectory(data.directory).catch(() => data.directory)
|
||||||
|
},
|
||||||
|
{ timeout },
|
||||||
|
)
|
||||||
|
.toBe(target)
|
||||||
|
|
||||||
|
await expect
|
||||||
|
.poll(
|
||||||
|
async () => {
|
||||||
|
const items = await sdk.session
|
||||||
|
.messages({ sessionID, limit: 20 })
|
||||||
|
.then((x) => x.data ?? [])
|
||||||
|
.catch(() => [])
|
||||||
|
return items.some((item) => item.info.role === "user")
|
||||||
|
},
|
||||||
|
{ timeout },
|
||||||
|
)
|
||||||
|
.toBe(true)
|
||||||
|
}
|
||||||
|
|
||||||
export function sessionIDFromUrl(url: string) {
|
export function sessionIDFromUrl(url: string) {
|
||||||
const match = /\/session\/([^/?#]+)/.exec(url)
|
const match = /\/session\/([^/?#]+)/.exec(url)
|
||||||
return match?.[1]
|
return match?.[1]
|
||||||
@@ -702,8 +932,14 @@ export async function openStatusPopover(page: Page) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export async function openProjectMenu(page: Page, projectSlug: string) {
|
export async function openProjectMenu(page: Page, projectSlug: string) {
|
||||||
|
await openSidebar(page)
|
||||||
|
const item = page.locator(projectSwitchSelector(projectSlug)).first()
|
||||||
|
await expect(item).toBeVisible()
|
||||||
|
await item.hover()
|
||||||
|
|
||||||
const trigger = page.locator(projectMenuTriggerSelector(projectSlug)).first()
|
const trigger = page.locator(projectMenuTriggerSelector(projectSlug)).first()
|
||||||
await expect(trigger).toHaveCount(1)
|
await expect(trigger).toHaveCount(1)
|
||||||
|
await expect(trigger).toBeVisible()
|
||||||
|
|
||||||
const menu = page
|
const menu = page
|
||||||
.locator(dropdownMenuContentSelector)
|
.locator(dropdownMenuContentSelector)
|
||||||
@@ -712,7 +948,7 @@ export async function openProjectMenu(page: Page, projectSlug: string) {
|
|||||||
const close = menu.locator(projectCloseMenuSelector(projectSlug)).first()
|
const close = menu.locator(projectCloseMenuSelector(projectSlug)).first()
|
||||||
|
|
||||||
const clicked = await trigger
|
const clicked = await trigger
|
||||||
.click({ timeout: 1500 })
|
.click({ force: true, timeout: 1500 })
|
||||||
.then(() => true)
|
.then(() => true)
|
||||||
.catch(() => false)
|
.catch(() => false)
|
||||||
|
|
||||||
|
|||||||
@@ -3,8 +3,11 @@ import { serverNamePattern } from "../utils"
|
|||||||
|
|
||||||
test("home renders and shows core entrypoints", async ({ page }) => {
|
test("home renders and shows core entrypoints", async ({ page }) => {
|
||||||
await page.goto("/")
|
await page.goto("/")
|
||||||
|
const nav = page.locator('[data-component="sidebar-nav-desktop"]')
|
||||||
|
|
||||||
await expect(page.getByRole("button", { name: "Open project" }).first()).toBeVisible()
|
await expect(page.getByRole("button", { name: "Open project" }).first()).toBeVisible()
|
||||||
|
await expect(nav.getByText("No projects open")).toBeVisible()
|
||||||
|
await expect(nav.getByText("Open a project to get started")).toBeVisible()
|
||||||
await expect(page.getByRole("button", { name: serverNamePattern })).toBeVisible()
|
await expect(page.getByRole("button", { name: serverNamePattern })).toBeVisible()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { test, expect } from "../fixtures"
|
import { test, expect } from "../fixtures"
|
||||||
import { openPalette } from "../actions"
|
import { closeDialog, openPalette } from "../actions"
|
||||||
|
|
||||||
test("search palette opens and closes", async ({ page, gotoSession }) => {
|
test("search palette opens and closes", async ({ page, gotoSession }) => {
|
||||||
await gotoSession()
|
await gotoSession()
|
||||||
@@ -9,3 +9,12 @@ test("search palette opens and closes", async ({ page, gotoSession }) => {
|
|||||||
await page.keyboard.press("Escape")
|
await page.keyboard.press("Escape")
|
||||||
await expect(dialog).toHaveCount(0)
|
await expect(dialog).toHaveCount(0)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
test("search palette also opens with cmd+p", async ({ page, gotoSession }) => {
|
||||||
|
await gotoSession()
|
||||||
|
|
||||||
|
const dialog = await openPalette(page, "P")
|
||||||
|
|
||||||
|
await closeDialog(page, dialog)
|
||||||
|
await expect(dialog).toHaveCount(0)
|
||||||
|
})
|
||||||
|
|||||||
@@ -1,11 +1,30 @@
|
|||||||
import { test as base, expect, type Page } from "@playwright/test"
|
import { test as base, expect, type Page } from "@playwright/test"
|
||||||
import type { E2EWindow } from "../src/testing/terminal"
|
import type { E2EWindow } from "../src/testing/terminal"
|
||||||
import { cleanupSession, cleanupTestProject, createTestProject, seedProjects, sessionIDFromUrl } from "./actions"
|
import {
|
||||||
import { promptSelector } from "./selectors"
|
healthPhase,
|
||||||
|
cleanupSession,
|
||||||
|
cleanupTestProject,
|
||||||
|
createTestProject,
|
||||||
|
setHealthPhase,
|
||||||
|
seedProjects,
|
||||||
|
sessionIDFromUrl,
|
||||||
|
waitSlug,
|
||||||
|
waitSession,
|
||||||
|
} from "./actions"
|
||||||
import { createSdk, dirSlug, getWorktree, sessionPath } from "./utils"
|
import { createSdk, dirSlug, getWorktree, sessionPath } from "./utils"
|
||||||
|
|
||||||
export const settingsKey = "settings.v3"
|
export const settingsKey = "settings.v3"
|
||||||
|
|
||||||
|
const seedModel = (() => {
|
||||||
|
const [providerID = "opencode", modelID = "big-pickle"] = (
|
||||||
|
process.env.OPENCODE_E2E_MODEL ?? "opencode/big-pickle"
|
||||||
|
).split("/")
|
||||||
|
return {
|
||||||
|
providerID: providerID || "opencode",
|
||||||
|
modelID: modelID || "big-pickle",
|
||||||
|
}
|
||||||
|
})()
|
||||||
|
|
||||||
type TestFixtures = {
|
type TestFixtures = {
|
||||||
sdk: ReturnType<typeof createSdk>
|
sdk: ReturnType<typeof createSdk>
|
||||||
gotoSession: (sessionID?: string) => Promise<void>
|
gotoSession: (sessionID?: string) => Promise<void>
|
||||||
@@ -27,6 +46,29 @@ type WorkerFixtures = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const test = base.extend<TestFixtures, WorkerFixtures>({
|
export const test = base.extend<TestFixtures, WorkerFixtures>({
|
||||||
|
page: async ({ page }, use) => {
|
||||||
|
let boundary: string | undefined
|
||||||
|
setHealthPhase(page, "test")
|
||||||
|
const consoleHandler = (msg: { text(): string }) => {
|
||||||
|
const text = msg.text()
|
||||||
|
if (!text.includes("[e2e:error-boundary]")) return
|
||||||
|
if (healthPhase(page) === "cleanup") {
|
||||||
|
console.warn(`[e2e:error-boundary][cleanup-warning]\n${text}`)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
boundary ||= text
|
||||||
|
console.log(text)
|
||||||
|
}
|
||||||
|
const pageErrorHandler = (err: Error) => {
|
||||||
|
console.log(`[e2e:pageerror] ${err.stack || err.message}`)
|
||||||
|
}
|
||||||
|
page.on("console", consoleHandler)
|
||||||
|
page.on("pageerror", pageErrorHandler)
|
||||||
|
await use(page)
|
||||||
|
page.off("console", consoleHandler)
|
||||||
|
page.off("pageerror", pageErrorHandler)
|
||||||
|
if (boundary) throw new Error(boundary)
|
||||||
|
},
|
||||||
directory: [
|
directory: [
|
||||||
async ({}, use) => {
|
async ({}, use) => {
|
||||||
const directory = await getWorktree()
|
const directory = await getWorktree()
|
||||||
@@ -48,21 +90,20 @@ export const test = base.extend<TestFixtures, WorkerFixtures>({
|
|||||||
|
|
||||||
const gotoSession = async (sessionID?: string) => {
|
const gotoSession = async (sessionID?: string) => {
|
||||||
await page.goto(sessionPath(directory, sessionID))
|
await page.goto(sessionPath(directory, sessionID))
|
||||||
await expect(page.locator(promptSelector)).toBeVisible()
|
await waitSession(page, { directory, sessionID })
|
||||||
}
|
}
|
||||||
await use(gotoSession)
|
await use(gotoSession)
|
||||||
},
|
},
|
||||||
withProject: async ({ page }, use) => {
|
withProject: async ({ page }, use) => {
|
||||||
await use(async (callback, options) => {
|
await use(async (callback, options) => {
|
||||||
const root = await createTestProject()
|
const root = await createTestProject()
|
||||||
const slug = dirSlug(root)
|
|
||||||
const sessions = new Map<string, string>()
|
const sessions = new Map<string, string>()
|
||||||
const dirs = new Set<string>()
|
const dirs = new Set<string>()
|
||||||
await seedStorage(page, { directory: root, extra: options?.extra })
|
await seedStorage(page, { directory: root, extra: options?.extra })
|
||||||
|
|
||||||
const gotoSession = async (sessionID?: string) => {
|
const gotoSession = async (sessionID?: string) => {
|
||||||
await page.goto(sessionPath(root, sessionID))
|
await page.goto(sessionPath(root, sessionID))
|
||||||
await expect(page.locator(promptSelector)).toBeVisible()
|
await waitSession(page, { directory: root, sessionID })
|
||||||
const current = sessionIDFromUrl(page.url())
|
const current = sessionIDFromUrl(page.url())
|
||||||
if (current) trackSession(current)
|
if (current) trackSession(current)
|
||||||
}
|
}
|
||||||
@@ -77,13 +118,16 @@ export const test = base.extend<TestFixtures, WorkerFixtures>({
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
await gotoSession()
|
await gotoSession()
|
||||||
|
const slug = await waitSlug(page)
|
||||||
return await callback({ directory: root, slug, gotoSession, trackSession, trackDirectory })
|
return await callback({ directory: root, slug, gotoSession, trackSession, trackDirectory })
|
||||||
} finally {
|
} finally {
|
||||||
|
setHealthPhase(page, "cleanup")
|
||||||
await Promise.allSettled(
|
await Promise.allSettled(
|
||||||
Array.from(sessions, ([sessionID, directory]) => cleanupSession({ sessionID, directory })),
|
Array.from(sessions, ([sessionID, directory]) => cleanupSession({ sessionID, directory })),
|
||||||
)
|
)
|
||||||
await Promise.allSettled(Array.from(dirs, (directory) => cleanupTestProject(directory)))
|
await Promise.allSettled(Array.from(dirs, (directory) => cleanupTestProject(directory)))
|
||||||
await cleanupTestProject(root)
|
await cleanupTestProject(root)
|
||||||
|
setHealthPhase(page, "test")
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
@@ -91,13 +135,16 @@ export const test = base.extend<TestFixtures, WorkerFixtures>({
|
|||||||
|
|
||||||
async function seedStorage(page: Page, input: { directory: string; extra?: string[] }) {
|
async function seedStorage(page: Page, input: { directory: string; extra?: string[] }) {
|
||||||
await seedProjects(page, input)
|
await seedProjects(page, input)
|
||||||
await page.addInitScript(() => {
|
await page.addInitScript((model: { providerID: string; modelID: string }) => {
|
||||||
const win = window as E2EWindow
|
const win = window as E2EWindow
|
||||||
win.__opencode_e2e = {
|
win.__opencode_e2e = {
|
||||||
...win.__opencode_e2e,
|
...win.__opencode_e2e,
|
||||||
model: {
|
model: {
|
||||||
enabled: true,
|
enabled: true,
|
||||||
},
|
},
|
||||||
|
prompt: {
|
||||||
|
enabled: true,
|
||||||
|
},
|
||||||
terminal: {
|
terminal: {
|
||||||
enabled: true,
|
enabled: true,
|
||||||
terminals: {},
|
terminals: {},
|
||||||
@@ -106,12 +153,12 @@ async function seedStorage(page: Page, input: { directory: string; extra?: strin
|
|||||||
localStorage.setItem(
|
localStorage.setItem(
|
||||||
"opencode.global.dat:model",
|
"opencode.global.dat:model",
|
||||||
JSON.stringify({
|
JSON.stringify({
|
||||||
recent: [{ providerID: "opencode", modelID: "big-pickle" }],
|
recent: [model],
|
||||||
user: [],
|
user: [],
|
||||||
variant: {},
|
variant: {},
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
})
|
}, seedModel)
|
||||||
}
|
}
|
||||||
|
|
||||||
export { expect }
|
export { expect }
|
||||||
|
|||||||
@@ -1,41 +1,19 @@
|
|||||||
import { base64Decode } from "@opencode-ai/util/encode"
|
import { base64Decode } from "@opencode-ai/util/encode"
|
||||||
import type { Page } from "@playwright/test"
|
|
||||||
import { test, expect } from "../fixtures"
|
import { test, expect } from "../fixtures"
|
||||||
import { defocus, createTestProject, cleanupTestProject, openSidebar, sessionIDFromUrl, waitSlug } from "../actions"
|
import {
|
||||||
|
defocus,
|
||||||
|
createTestProject,
|
||||||
|
cleanupTestProject,
|
||||||
|
openSidebar,
|
||||||
|
sessionIDFromUrl,
|
||||||
|
setWorkspacesEnabled,
|
||||||
|
waitSession,
|
||||||
|
waitSessionSaved,
|
||||||
|
waitSlug,
|
||||||
|
} from "../actions"
|
||||||
import { projectSwitchSelector, promptSelector, workspaceItemSelector, workspaceNewSessionSelector } from "../selectors"
|
import { projectSwitchSelector, promptSelector, workspaceItemSelector, workspaceNewSessionSelector } from "../selectors"
|
||||||
import { dirSlug, resolveDirectory } from "../utils"
|
import { dirSlug, resolveDirectory } from "../utils"
|
||||||
|
|
||||||
async function workspaces(page: Page, directory: string, enabled: boolean) {
|
|
||||||
await page.evaluate(
|
|
||||||
({ directory, enabled }: { directory: string; enabled: boolean }) => {
|
|
||||||
const key = "opencode.global.dat:layout"
|
|
||||||
const raw = localStorage.getItem(key)
|
|
||||||
const data = raw ? JSON.parse(raw) : {}
|
|
||||||
const sidebar = data.sidebar && typeof data.sidebar === "object" ? data.sidebar : {}
|
|
||||||
const current =
|
|
||||||
sidebar.workspaces && typeof sidebar.workspaces === "object" && !Array.isArray(sidebar.workspaces)
|
|
||||||
? sidebar.workspaces
|
|
||||||
: {}
|
|
||||||
const next = { ...current }
|
|
||||||
|
|
||||||
if (enabled) next[directory] = true
|
|
||||||
if (!enabled) delete next[directory]
|
|
||||||
|
|
||||||
localStorage.setItem(
|
|
||||||
key,
|
|
||||||
JSON.stringify({
|
|
||||||
...data,
|
|
||||||
sidebar: {
|
|
||||||
...sidebar,
|
|
||||||
workspaces: next,
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
},
|
|
||||||
{ directory, enabled },
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
test("can switch between projects from sidebar", async ({ page, withProject }) => {
|
test("can switch between projects from sidebar", async ({ page, withProject }) => {
|
||||||
await page.setViewportSize({ width: 1400, height: 800 })
|
await page.setViewportSize({ width: 1400, height: 800 })
|
||||||
|
|
||||||
@@ -76,9 +54,7 @@ test("switching back to a project opens the latest workspace session", async ({
|
|||||||
await withProject(
|
await withProject(
|
||||||
async ({ directory, slug, trackSession, trackDirectory }) => {
|
async ({ directory, slug, trackSession, trackDirectory }) => {
|
||||||
await defocus(page)
|
await defocus(page)
|
||||||
await workspaces(page, directory, true)
|
await setWorkspacesEnabled(page, slug, true)
|
||||||
await page.reload()
|
|
||||||
await expect(page.locator(promptSelector)).toBeVisible()
|
|
||||||
await openSidebar(page)
|
await openSidebar(page)
|
||||||
await expect(page.getByRole("button", { name: "New workspace" }).first()).toBeVisible()
|
await expect(page.getByRole("button", { name: "New workspace" }).first()).toBeVisible()
|
||||||
|
|
||||||
@@ -100,11 +76,7 @@ test("switching back to a project opens the latest workspace session", async ({
|
|||||||
await expect(btn).toBeVisible()
|
await expect(btn).toBeVisible()
|
||||||
await btn.click({ force: true })
|
await btn.click({ force: true })
|
||||||
|
|
||||||
// A new workspace can be discovered via a transient slug before the route and sidebar
|
await waitSession(page, { directory: space })
|
||||||
// settle to the canonical workspace path on Windows, so interact with either and assert
|
|
||||||
// against the resolved workspace slug.
|
|
||||||
await waitSlug(page)
|
|
||||||
await expect(page).toHaveURL(new RegExp(`/${next}/session(?:[/?#]|$)`))
|
|
||||||
|
|
||||||
// Create a session by sending a prompt
|
// Create a session by sending a prompt
|
||||||
const prompt = page.locator(promptSelector)
|
const prompt = page.locator(promptSelector)
|
||||||
@@ -118,6 +90,7 @@ test("switching back to a project opens the latest workspace session", async ({
|
|||||||
const created = sessionIDFromUrl(page.url())
|
const created = sessionIDFromUrl(page.url())
|
||||||
if (!created) throw new Error(`Failed to get session ID from url: ${page.url()}`)
|
if (!created) throw new Error(`Failed to get session ID from url: ${page.url()}`)
|
||||||
trackSession(created, space)
|
trackSession(created, space)
|
||||||
|
await waitSessionSaved(space, created)
|
||||||
|
|
||||||
await expect(page).toHaveURL(new RegExp(`/${next}/session/${created}(?:[/?#]|$)`))
|
await expect(page).toHaveURL(new RegExp(`/${next}/session/${created}(?:[/?#]|$)`))
|
||||||
|
|
||||||
@@ -125,14 +98,14 @@ test("switching back to a project opens the latest workspace session", async ({
|
|||||||
|
|
||||||
const otherButton = page.locator(projectSwitchSelector(otherSlug)).first()
|
const otherButton = page.locator(projectSwitchSelector(otherSlug)).first()
|
||||||
await expect(otherButton).toBeVisible()
|
await expect(otherButton).toBeVisible()
|
||||||
await otherButton.click()
|
await otherButton.click({ force: true })
|
||||||
await expect(page).toHaveURL(new RegExp(`/${otherSlug}/session`))
|
await waitSession(page, { directory: other })
|
||||||
|
|
||||||
const rootButton = page.locator(projectSwitchSelector(slug)).first()
|
const rootButton = page.locator(projectSwitchSelector(slug)).first()
|
||||||
await expect(rootButton).toBeVisible()
|
await expect(rootButton).toBeVisible()
|
||||||
await rootButton.click()
|
await rootButton.click({ force: true })
|
||||||
|
|
||||||
await expect.poll(() => sessionIDFromUrl(page.url()) ?? "").toBe(created)
|
await waitSession(page, { directory: space, sessionID: created })
|
||||||
await expect(page).toHaveURL(new RegExp(`/session/${created}(?:[/?#]|$)`))
|
await expect(page).toHaveURL(new RegExp(`/session/${created}(?:[/?#]|$)`))
|
||||||
},
|
},
|
||||||
{ extra: [other] },
|
{ extra: [other] },
|
||||||
|
|||||||
@@ -1,109 +1,94 @@
|
|||||||
import { base64Decode } from "@opencode-ai/util/encode"
|
|
||||||
import type { Page } from "@playwright/test"
|
import type { Page } from "@playwright/test"
|
||||||
import { test, expect } from "../fixtures"
|
import { test, expect } from "../fixtures"
|
||||||
import { openSidebar, sessionIDFromUrl, setWorkspacesEnabled, slugFromUrl, waitSlug } from "../actions"
|
import {
|
||||||
|
openSidebar,
|
||||||
|
resolveSlug,
|
||||||
|
sessionIDFromUrl,
|
||||||
|
setWorkspacesEnabled,
|
||||||
|
waitDir,
|
||||||
|
waitSession,
|
||||||
|
waitSessionSaved,
|
||||||
|
waitSlug,
|
||||||
|
} from "../actions"
|
||||||
import { promptSelector, workspaceItemSelector, workspaceNewSessionSelector } from "../selectors"
|
import { promptSelector, workspaceItemSelector, workspaceNewSessionSelector } from "../selectors"
|
||||||
import { createSdk } from "../utils"
|
import { createSdk } from "../utils"
|
||||||
|
|
||||||
async function waitWorkspaceReady(page: Page, slug: string) {
|
function item(space: { slug: string; raw: string }) {
|
||||||
|
return `${workspaceItemSelector(space.slug)}, ${workspaceItemSelector(space.raw)}`
|
||||||
|
}
|
||||||
|
|
||||||
|
function button(space: { slug: string; raw: string }) {
|
||||||
|
return `${workspaceNewSessionSelector(space.slug)}, ${workspaceNewSessionSelector(space.raw)}`
|
||||||
|
}
|
||||||
|
|
||||||
|
async function waitWorkspaceReady(page: Page, space: { slug: string; raw: string }) {
|
||||||
await openSidebar(page)
|
await openSidebar(page)
|
||||||
await expect
|
await expect(page.locator(item(space)).first()).toBeVisible({ timeout: 60_000 })
|
||||||
.poll(
|
|
||||||
async () => {
|
|
||||||
const item = page.locator(workspaceItemSelector(slug)).first()
|
|
||||||
try {
|
|
||||||
await item.hover({ timeout: 500 })
|
|
||||||
return true
|
|
||||||
} catch {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{ timeout: 60_000 },
|
|
||||||
)
|
|
||||||
.toBe(true)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function createWorkspace(page: Page, root: string, seen: string[]) {
|
async function createWorkspace(page: Page, root: string, seen: string[]) {
|
||||||
await openSidebar(page)
|
await openSidebar(page)
|
||||||
await page.getByRole("button", { name: "New workspace" }).first().click()
|
await page.getByRole("button", { name: "New workspace" }).first().click()
|
||||||
|
|
||||||
const slug = await waitSlug(page, [root, ...seen])
|
const next = await resolveSlug(await waitSlug(page, [root, ...seen]))
|
||||||
const directory = base64Decode(slug)
|
await waitDir(page, next.directory)
|
||||||
if (!directory) throw new Error(`Failed to decode workspace slug: ${slug}`)
|
|
||||||
return { slug, directory }
|
|
||||||
}
|
|
||||||
|
|
||||||
async function openWorkspaceNewSession(page: Page, slug: string) {
|
|
||||||
await waitWorkspaceReady(page, slug)
|
|
||||||
|
|
||||||
const item = page.locator(workspaceItemSelector(slug)).first()
|
|
||||||
await item.hover()
|
|
||||||
|
|
||||||
const button = page.locator(workspaceNewSessionSelector(slug)).first()
|
|
||||||
await expect(button).toBeVisible()
|
|
||||||
await button.click({ force: true })
|
|
||||||
|
|
||||||
const next = await waitSlug(page)
|
|
||||||
await expect(page).toHaveURL(new RegExp(`/${next}/session(?:[/?#]|$)`))
|
|
||||||
return next
|
return next
|
||||||
}
|
}
|
||||||
|
|
||||||
async function createSessionFromWorkspace(page: Page, slug: string, text: string) {
|
async function openWorkspaceNewSession(page: Page, space: { slug: string; raw: string; directory: string }) {
|
||||||
const next = await openWorkspaceNewSession(page, slug)
|
await waitWorkspaceReady(page, space)
|
||||||
|
|
||||||
|
const row = page.locator(item(space)).first()
|
||||||
|
await row.hover()
|
||||||
|
|
||||||
|
const next = page.locator(button(space)).first()
|
||||||
|
await expect(next).toBeVisible()
|
||||||
|
await next.click({ force: true })
|
||||||
|
|
||||||
|
await waitSession(page, { directory: space.directory })
|
||||||
|
await expect.poll(() => sessionIDFromUrl(page.url()) ?? "").toBe("")
|
||||||
|
}
|
||||||
|
|
||||||
|
async function createSessionFromWorkspace(
|
||||||
|
page: Page,
|
||||||
|
space: { slug: string; raw: string; directory: string },
|
||||||
|
text: string,
|
||||||
|
) {
|
||||||
|
await openWorkspaceNewSession(page, space)
|
||||||
|
|
||||||
const prompt = page.locator(promptSelector)
|
const prompt = page.locator(promptSelector)
|
||||||
await expect(prompt).toBeVisible()
|
await expect(prompt).toBeVisible()
|
||||||
await expect(prompt).toBeEditable()
|
|
||||||
await prompt.click()
|
|
||||||
await expect(prompt).toBeFocused()
|
|
||||||
await prompt.fill(text)
|
await prompt.fill(text)
|
||||||
await expect.poll(async () => ((await prompt.textContent()) ?? "").trim()).toContain(text)
|
await page.keyboard.press("Enter")
|
||||||
await prompt.press("Enter")
|
|
||||||
|
|
||||||
await expect.poll(() => slugFromUrl(page.url())).toBe(next)
|
|
||||||
await expect.poll(() => sessionIDFromUrl(page.url()) ?? "", { timeout: 30_000 }).not.toBe("")
|
|
||||||
|
|
||||||
|
await expect.poll(() => sessionIDFromUrl(page.url()) ?? "", { timeout: 15_000 }).not.toBe("")
|
||||||
const sessionID = sessionIDFromUrl(page.url())
|
const sessionID = sessionIDFromUrl(page.url())
|
||||||
if (!sessionID) throw new Error(`Failed to parse session id from url: ${page.url()}`)
|
if (!sessionID) throw new Error(`Failed to parse session id from url: ${page.url()}`)
|
||||||
await expect(page).toHaveURL(new RegExp(`/${next}/session/${sessionID}(?:[/?#]|$)`))
|
|
||||||
return { sessionID, slug: next }
|
|
||||||
}
|
|
||||||
|
|
||||||
async function sessionDirectory(directory: string, sessionID: string) {
|
await waitSessionSaved(space.directory, sessionID)
|
||||||
const info = await createSdk(directory)
|
await createSdk(space.directory)
|
||||||
.session.get({ sessionID })
|
.session.abort({ sessionID })
|
||||||
.then((x) => x.data)
|
|
||||||
.catch(() => undefined)
|
.catch(() => undefined)
|
||||||
if (!info) return ""
|
return sessionID
|
||||||
return info.directory
|
|
||||||
}
|
}
|
||||||
|
|
||||||
test("new sessions from sidebar workspace actions stay in selected workspace", async ({ page, withProject }) => {
|
test("new sessions from sidebar workspace actions stay in selected workspace", async ({ page, withProject }) => {
|
||||||
await page.setViewportSize({ width: 1400, height: 800 })
|
await page.setViewportSize({ width: 1400, height: 800 })
|
||||||
|
|
||||||
await withProject(async ({ directory, slug: root, trackSession, trackDirectory }) => {
|
await withProject(async ({ slug: root, trackDirectory, trackSession }) => {
|
||||||
await openSidebar(page)
|
await openSidebar(page)
|
||||||
await setWorkspacesEnabled(page, root, true)
|
await setWorkspacesEnabled(page, root, true)
|
||||||
|
|
||||||
const first = await createWorkspace(page, root, [])
|
const first = await createWorkspace(page, root, [])
|
||||||
trackDirectory(first.directory)
|
trackDirectory(first.directory)
|
||||||
await waitWorkspaceReady(page, first.slug)
|
await waitWorkspaceReady(page, first)
|
||||||
|
|
||||||
const second = await createWorkspace(page, root, [first.slug])
|
const second = await createWorkspace(page, root, [first.slug])
|
||||||
trackDirectory(second.directory)
|
trackDirectory(second.directory)
|
||||||
await waitWorkspaceReady(page, second.slug)
|
await waitWorkspaceReady(page, second)
|
||||||
|
|
||||||
const firstSession = await createSessionFromWorkspace(page, first.slug, `workspace one ${Date.now()}`)
|
trackSession(await createSessionFromWorkspace(page, first, `workspace one ${Date.now()}`), first.directory)
|
||||||
trackSession(firstSession.sessionID, first.directory)
|
trackSession(await createSessionFromWorkspace(page, second, `workspace two ${Date.now()}`), second.directory)
|
||||||
|
trackSession(await createSessionFromWorkspace(page, first, `workspace one again ${Date.now()}`), first.directory)
|
||||||
const secondSession = await createSessionFromWorkspace(page, second.slug, `workspace two ${Date.now()}`)
|
|
||||||
trackSession(secondSession.sessionID, second.directory)
|
|
||||||
|
|
||||||
const thirdSession = await createSessionFromWorkspace(page, first.slug, `workspace one again ${Date.now()}`)
|
|
||||||
trackSession(thirdSession.sessionID, first.directory)
|
|
||||||
|
|
||||||
await expect.poll(() => sessionDirectory(first.directory, firstSession.sessionID)).toBe(first.directory)
|
|
||||||
await expect.poll(() => sessionDirectory(second.directory, secondSession.sessionID)).toBe(second.directory)
|
|
||||||
await expect.poll(() => sessionDirectory(first.directory, thirdSession.sessionID)).toBe(first.directory)
|
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { base64Decode } from "@opencode-ai/util/encode"
|
|
||||||
import fs from "node:fs/promises"
|
import fs from "node:fs/promises"
|
||||||
import os from "node:os"
|
import os from "node:os"
|
||||||
import path from "node:path"
|
import path from "node:path"
|
||||||
|
import { base64Decode } from "@opencode-ai/util/encode"
|
||||||
import type { Page } from "@playwright/test"
|
import type { Page } from "@playwright/test"
|
||||||
|
|
||||||
import { test, expect } from "../fixtures"
|
import { test, expect } from "../fixtures"
|
||||||
@@ -13,8 +13,10 @@ import {
|
|||||||
confirmDialog,
|
confirmDialog,
|
||||||
openSidebar,
|
openSidebar,
|
||||||
openWorkspaceMenu,
|
openWorkspaceMenu,
|
||||||
|
resolveSlug,
|
||||||
setWorkspacesEnabled,
|
setWorkspacesEnabled,
|
||||||
slugFromUrl,
|
slugFromUrl,
|
||||||
|
waitDir,
|
||||||
waitSlug,
|
waitSlug,
|
||||||
} from "../actions"
|
} from "../actions"
|
||||||
import { dropdownMenuContentSelector, inlineInputSelector, workspaceItemSelector } from "../selectors"
|
import { dropdownMenuContentSelector, inlineInputSelector, workspaceItemSelector } from "../selectors"
|
||||||
@@ -27,15 +29,15 @@ async function setupWorkspaceTest(page: Page, project: { slug: string }) {
|
|||||||
await setWorkspacesEnabled(page, rootSlug, true)
|
await setWorkspacesEnabled(page, rootSlug, true)
|
||||||
|
|
||||||
await page.getByRole("button", { name: "New workspace" }).first().click()
|
await page.getByRole("button", { name: "New workspace" }).first().click()
|
||||||
const slug = await waitSlug(page, [rootSlug])
|
const next = await resolveSlug(await waitSlug(page, [rootSlug]))
|
||||||
const dir = base64Decode(slug)
|
await waitDir(page, next.directory)
|
||||||
|
|
||||||
await openSidebar(page)
|
await openSidebar(page)
|
||||||
|
|
||||||
await expect
|
await expect
|
||||||
.poll(
|
.poll(
|
||||||
async () => {
|
async () => {
|
||||||
const item = page.locator(workspaceItemSelector(slug)).first()
|
const item = page.locator(workspaceItemSelector(next.slug)).first()
|
||||||
try {
|
try {
|
||||||
await item.hover({ timeout: 500 })
|
await item.hover({ timeout: 500 })
|
||||||
return true
|
return true
|
||||||
@@ -47,7 +49,7 @@ async function setupWorkspaceTest(page: Page, project: { slug: string }) {
|
|||||||
)
|
)
|
||||||
.toBe(true)
|
.toBe(true)
|
||||||
|
|
||||||
return { rootSlug, slug, directory: dir }
|
return { rootSlug, slug: next.slug, directory: next.directory }
|
||||||
}
|
}
|
||||||
|
|
||||||
test("can enable and disable workspaces from project menu", async ({ page, withProject }) => {
|
test("can enable and disable workspaces from project menu", async ({ page, withProject }) => {
|
||||||
@@ -79,15 +81,15 @@ test("can create a workspace", async ({ page, withProject }) => {
|
|||||||
await expect(page.getByRole("button", { name: "New workspace" }).first()).toBeVisible()
|
await expect(page.getByRole("button", { name: "New workspace" }).first()).toBeVisible()
|
||||||
|
|
||||||
await page.getByRole("button", { name: "New workspace" }).first().click()
|
await page.getByRole("button", { name: "New workspace" }).first().click()
|
||||||
const workspaceSlug = await waitSlug(page, [slug])
|
const next = await resolveSlug(await waitSlug(page, [slug]))
|
||||||
const workspaceDir = base64Decode(workspaceSlug)
|
await waitDir(page, next.directory)
|
||||||
|
|
||||||
await openSidebar(page)
|
await openSidebar(page)
|
||||||
|
|
||||||
await expect
|
await expect
|
||||||
.poll(
|
.poll(
|
||||||
async () => {
|
async () => {
|
||||||
const item = page.locator(workspaceItemSelector(workspaceSlug)).first()
|
const item = page.locator(workspaceItemSelector(next.slug)).first()
|
||||||
try {
|
try {
|
||||||
await item.hover({ timeout: 500 })
|
await item.hover({ timeout: 500 })
|
||||||
return true
|
return true
|
||||||
@@ -99,9 +101,9 @@ test("can create a workspace", async ({ page, withProject }) => {
|
|||||||
)
|
)
|
||||||
.toBe(true)
|
.toBe(true)
|
||||||
|
|
||||||
await expect(page.locator(workspaceItemSelector(workspaceSlug)).first()).toBeVisible()
|
await expect(page.locator(workspaceItemSelector(next.slug)).first()).toBeVisible()
|
||||||
|
|
||||||
await cleanupTestProject(workspaceDir)
|
await cleanupTestProject(next.directory)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -119,7 +121,7 @@ test("non-git projects keep workspace mode disabled", async ({ page, withProject
|
|||||||
|
|
||||||
await expect.poll(() => slugFromUrl(page.url()), { timeout: 30_000 }).not.toBe("")
|
await expect.poll(() => slugFromUrl(page.url()), { timeout: 30_000 }).not.toBe("")
|
||||||
|
|
||||||
const activeDir = base64Decode(slugFromUrl(page.url()))
|
const activeDir = await resolveSlug(slugFromUrl(page.url())).then((item) => item.directory)
|
||||||
expect(path.basename(activeDir)).toContain("opencode-e2e-project-nongit-")
|
expect(path.basename(activeDir)).toContain("opencode-e2e-project-nongit-")
|
||||||
|
|
||||||
await openSidebar(page)
|
await openSidebar(page)
|
||||||
@@ -331,9 +333,9 @@ test("can reorder workspaces by drag and drop", async ({ page, withProject }) =>
|
|||||||
for (const _ of [0, 1]) {
|
for (const _ of [0, 1]) {
|
||||||
const prev = slugFromUrl(page.url())
|
const prev = slugFromUrl(page.url())
|
||||||
await page.getByRole("button", { name: "New workspace" }).first().click()
|
await page.getByRole("button", { name: "New workspace" }).first().click()
|
||||||
const slug = await waitSlug(page, [rootSlug, prev])
|
const next = await resolveSlug(await waitSlug(page, [rootSlug, prev]))
|
||||||
const dir = base64Decode(slug)
|
await waitDir(page, next.directory)
|
||||||
workspaces.push({ slug, directory: dir })
|
workspaces.push(next)
|
||||||
|
|
||||||
await openSidebar(page)
|
await openSidebar(page)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -108,7 +108,10 @@ test("prompt history restores unsent draft with arrow navigation", async ({ page
|
|||||||
await page.keyboard.type(draft)
|
await page.keyboard.type(draft)
|
||||||
await wait(page, draft)
|
await wait(page, draft)
|
||||||
|
|
||||||
await edge(page, "start")
|
// Clear the draft before navigating history (ArrowUp only works when prompt is empty)
|
||||||
|
await prompt.fill("")
|
||||||
|
await wait(page, "")
|
||||||
|
|
||||||
await page.keyboard.press("ArrowUp")
|
await page.keyboard.press("ArrowUp")
|
||||||
await wait(page, second)
|
await wait(page, second)
|
||||||
|
|
||||||
@@ -119,7 +122,7 @@ test("prompt history restores unsent draft with arrow navigation", async ({ page
|
|||||||
await wait(page, second)
|
await wait(page, second)
|
||||||
|
|
||||||
await page.keyboard.press("ArrowDown")
|
await page.keyboard.press("ArrowDown")
|
||||||
await wait(page, draft)
|
await wait(page, "")
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -7,12 +7,18 @@ test("shift+enter inserts a newline without submitting", async ({ page, gotoSess
|
|||||||
await expect(page).toHaveURL(/\/session\/?$/)
|
await expect(page).toHaveURL(/\/session\/?$/)
|
||||||
|
|
||||||
const prompt = page.locator(promptSelector)
|
const prompt = page.locator(promptSelector)
|
||||||
await prompt.click()
|
await prompt.focus()
|
||||||
await page.keyboard.type("line one")
|
await expect(prompt).toBeFocused()
|
||||||
await page.keyboard.press("Shift+Enter")
|
|
||||||
await page.keyboard.type("line two")
|
await prompt.pressSequentially("line one")
|
||||||
|
await expect(prompt).toBeFocused()
|
||||||
|
|
||||||
|
await prompt.press("Shift+Enter")
|
||||||
|
await expect(page).toHaveURL(/\/session\/?$/)
|
||||||
|
await expect(prompt).toBeFocused()
|
||||||
|
|
||||||
|
await prompt.pressSequentially("line two")
|
||||||
|
|
||||||
await expect(page).toHaveURL(/\/session\/?$/)
|
await expect(page).toHaveURL(/\/session\/?$/)
|
||||||
await expect(prompt).toContainText("line one")
|
await expect.poll(() => prompt.evaluate((el) => el.innerText)).toBe("line one\nline two")
|
||||||
await expect(prompt).toContainText("line two")
|
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { test, expect } from "../fixtures"
|
import { test, expect } from "../fixtures"
|
||||||
import { waitTerminalReady } from "../actions"
|
import { runPromptSlash, waitTerminalFocusIdle } from "../actions"
|
||||||
import { promptSelector, terminalSelector } from "../selectors"
|
import { promptSelector, terminalSelector } from "../selectors"
|
||||||
|
|
||||||
test("/terminal toggles the terminal panel", async ({ page, gotoSession }) => {
|
test("/terminal toggles the terminal panel", async ({ page, gotoSession }) => {
|
||||||
@@ -7,29 +7,12 @@ test("/terminal toggles the terminal panel", async ({ page, gotoSession }) => {
|
|||||||
|
|
||||||
const prompt = page.locator(promptSelector)
|
const prompt = page.locator(promptSelector)
|
||||||
const terminal = page.locator(terminalSelector)
|
const terminal = page.locator(terminalSelector)
|
||||||
const slash = page.locator('[data-slash-id="terminal.toggle"]').first()
|
|
||||||
|
|
||||||
await expect(terminal).not.toBeVisible()
|
await expect(terminal).not.toBeVisible()
|
||||||
|
|
||||||
await prompt.fill("/terminal")
|
await runPromptSlash(page, { prompt, text: "/terminal", id: "terminal.toggle" })
|
||||||
await expect(slash).toBeVisible()
|
await waitTerminalFocusIdle(page, { term: terminal })
|
||||||
await page.keyboard.press("Enter")
|
|
||||||
await waitTerminalReady(page, { term: terminal })
|
|
||||||
|
|
||||||
// Terminal panel retries focus (immediate, RAF, 120ms, 240ms) after opening,
|
await runPromptSlash(page, { prompt, text: "/terminal", id: "terminal.toggle" })
|
||||||
// which can steal focus from the prompt and prevent fill() from triggering
|
|
||||||
// the slash popover. Re-attempt click+fill until all retries are exhausted
|
|
||||||
// and the popover appears.
|
|
||||||
await expect
|
|
||||||
.poll(
|
|
||||||
async () => {
|
|
||||||
await prompt.click().catch(() => false)
|
|
||||||
await prompt.fill("/terminal").catch(() => false)
|
|
||||||
return slash.isVisible().catch(() => false)
|
|
||||||
},
|
|
||||||
{ timeout: 10_000 },
|
|
||||||
)
|
|
||||||
.toBe(true)
|
|
||||||
await page.keyboard.press("Enter")
|
|
||||||
await expect(terminal).not.toBeVisible()
|
await expect(terminal).not.toBeVisible()
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -19,7 +19,8 @@ export const promptVariantSelector = '[data-component="prompt-variant-control"]'
|
|||||||
export const settingsLanguageSelectSelector = '[data-action="settings-language"]'
|
export const settingsLanguageSelectSelector = '[data-action="settings-language"]'
|
||||||
export const settingsColorSchemeSelector = '[data-action="settings-color-scheme"]'
|
export const settingsColorSchemeSelector = '[data-action="settings-color-scheme"]'
|
||||||
export const settingsThemeSelector = '[data-action="settings-theme"]'
|
export const settingsThemeSelector = '[data-action="settings-theme"]'
|
||||||
export const settingsFontSelector = '[data-action="settings-font"]'
|
export const settingsCodeFontSelector = '[data-action="settings-code-font"]'
|
||||||
|
export const settingsUIFontSelector = '[data-action="settings-ui-font"]'
|
||||||
export const settingsNotificationsAgentSelector = '[data-action="settings-notifications-agent"]'
|
export const settingsNotificationsAgentSelector = '[data-action="settings-notifications-agent"]'
|
||||||
export const settingsNotificationsPermissionsSelector = '[data-action="settings-notifications-permissions"]'
|
export const settingsNotificationsPermissionsSelector = '[data-action="settings-notifications-permissions"]'
|
||||||
export const settingsNotificationsErrorsSelector = '[data-action="settings-notifications-errors"]'
|
export const settingsNotificationsErrorsSelector = '[data-action="settings-notifications-errors"]'
|
||||||
|
|||||||
@@ -93,7 +93,7 @@ async function todoDock(page: any, sessionID: string) {
|
|||||||
|
|
||||||
const write = async (driver: ComposerDriverState | undefined) => {
|
const write = async (driver: ComposerDriverState | undefined) => {
|
||||||
await page.evaluate(
|
await page.evaluate(
|
||||||
(input) => {
|
(input: { event: string; sessionID: string; driver: ComposerDriverState | undefined }) => {
|
||||||
const win = window as ComposerWindow
|
const win = window as ComposerWindow
|
||||||
const composer = win.__opencode_e2e?.composer
|
const composer = win.__opencode_e2e?.composer
|
||||||
if (!composer?.enabled) throw new Error("Composer e2e driver is not enabled")
|
if (!composer?.enabled) throw new Error("Composer e2e driver is not enabled")
|
||||||
@@ -118,7 +118,7 @@ async function todoDock(page: any, sessionID: string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const read = () =>
|
const read = () =>
|
||||||
page.evaluate((sessionID) => {
|
page.evaluate((sessionID: string) => {
|
||||||
const win = window as ComposerWindow
|
const win = window as ComposerWindow
|
||||||
return win.__opencode_e2e?.composer?.sessions?.[sessionID]?.probe ?? null
|
return win.__opencode_e2e?.composer?.sessions?.[sessionID]?.probe ?? null
|
||||||
}, sessionID) as Promise<ComposerProbeState | null>
|
}, sessionID) as Promise<ComposerProbeState | null>
|
||||||
@@ -186,6 +186,8 @@ async function withMockPermission<T>(
|
|||||||
opts: { child?: any } | undefined,
|
opts: { child?: any } | undefined,
|
||||||
fn: (state: { resolved: () => Promise<void> }) => Promise<T>,
|
fn: (state: { resolved: () => Promise<void> }) => Promise<T>,
|
||||||
) {
|
) {
|
||||||
|
const listUrl = /\/permission(?:\?.*)?$/
|
||||||
|
const replyUrls = [/\/session\/[^/]+\/permissions\/[^/?]+(?:\?.*)?$/, /\/permission\/[^/]+\/reply(?:\?.*)?$/]
|
||||||
let pending = [
|
let pending = [
|
||||||
{
|
{
|
||||||
...request,
|
...request,
|
||||||
@@ -204,7 +206,8 @@ async function withMockPermission<T>(
|
|||||||
|
|
||||||
const reply = async (route: any) => {
|
const reply = async (route: any) => {
|
||||||
const url = new URL(route.request().url())
|
const url = new URL(route.request().url())
|
||||||
const id = url.pathname.split("/").pop()
|
const parts = url.pathname.split("/").filter(Boolean)
|
||||||
|
const id = parts.at(-1) === "reply" ? parts.at(-2) : parts.at(-1)
|
||||||
pending = pending.filter((item) => item.id !== id)
|
pending = pending.filter((item) => item.id !== id)
|
||||||
await route.fulfill({
|
await route.fulfill({
|
||||||
status: 200,
|
status: 200,
|
||||||
@@ -213,8 +216,10 @@ async function withMockPermission<T>(
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
await page.route("**/permission", list)
|
await page.route(listUrl, list)
|
||||||
await page.route("**/session/*/permissions/*", reply)
|
for (const item of replyUrls) {
|
||||||
|
await page.route(item, reply)
|
||||||
|
}
|
||||||
|
|
||||||
const sessionList = opts?.child
|
const sessionList = opts?.child
|
||||||
? async (route: any) => {
|
? async (route: any) => {
|
||||||
@@ -242,8 +247,10 @@ async function withMockPermission<T>(
|
|||||||
try {
|
try {
|
||||||
return await fn(state)
|
return await fn(state)
|
||||||
} finally {
|
} finally {
|
||||||
await page.unroute("**/permission", list)
|
await page.unroute(listUrl, list)
|
||||||
await page.unroute("**/session/*/permissions/*", reply)
|
for (const item of replyUrls) {
|
||||||
|
await page.unroute(item, reply)
|
||||||
|
}
|
||||||
if (sessionList) await page.unroute("**/session?*", sessionList)
|
if (sessionList) await page.unroute("**/session?*", sessionList)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,14 @@
|
|||||||
import { base64Decode } from "@opencode-ai/util/encode"
|
|
||||||
import type { Locator, Page } from "@playwright/test"
|
import type { Locator, Page } from "@playwright/test"
|
||||||
import { test, expect } from "../fixtures"
|
import { test, expect } from "../fixtures"
|
||||||
import { openSidebar, sessionIDFromUrl, setWorkspacesEnabled, waitSessionIdle, waitSlug } from "../actions"
|
import {
|
||||||
|
openSidebar,
|
||||||
|
resolveSlug,
|
||||||
|
sessionIDFromUrl,
|
||||||
|
setWorkspacesEnabled,
|
||||||
|
waitSession,
|
||||||
|
waitSessionIdle,
|
||||||
|
waitSlug,
|
||||||
|
} from "../actions"
|
||||||
import {
|
import {
|
||||||
promptAgentSelector,
|
promptAgentSelector,
|
||||||
promptModelSelector,
|
promptModelSelector,
|
||||||
@@ -21,7 +28,17 @@ type Footer = {
|
|||||||
type Probe = {
|
type Probe = {
|
||||||
dir?: string
|
dir?: string
|
||||||
sessionID?: string
|
sessionID?: string
|
||||||
model?: { providerID: string; modelID: string }
|
agent?: string
|
||||||
|
model?: { providerID: string; modelID: string; name?: string }
|
||||||
|
variant?: string | null
|
||||||
|
pick?: {
|
||||||
|
agent?: string
|
||||||
|
model?: { providerID: string; modelID: string }
|
||||||
|
variant?: string | null
|
||||||
|
}
|
||||||
|
variants?: string[]
|
||||||
|
models?: Array<{ providerID: string; modelID: string; name: string }>
|
||||||
|
agents?: Array<{ name: string }>
|
||||||
}
|
}
|
||||||
|
|
||||||
const escape = (value: string) => value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")
|
const escape = (value: string) => value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")
|
||||||
@@ -30,8 +47,6 @@ const text = async (locator: Locator) => ((await locator.textContent()) ?? "").t
|
|||||||
|
|
||||||
const modelKey = (state: Probe | null) => (state?.model ? `${state.model.providerID}:${state.model.modelID}` : null)
|
const modelKey = (state: Probe | null) => (state?.model ? `${state.model.providerID}:${state.model.modelID}` : null)
|
||||||
|
|
||||||
const dirKey = (state: Probe | null) => state?.dir ?? ""
|
|
||||||
|
|
||||||
async function probe(page: Page): Promise<Probe | null> {
|
async function probe(page: Page): Promise<Probe | null> {
|
||||||
return page.evaluate(() => {
|
return page.evaluate(() => {
|
||||||
const win = window as Window & {
|
const win = window as Window & {
|
||||||
@@ -45,19 +60,84 @@ async function probe(page: Page): Promise<Probe | null> {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
async function currentDir(page: Page) {
|
async function currentModel(page: Page) {
|
||||||
let hit = ""
|
await expect.poll(() => probe(page).then(modelKey), { timeout: 30_000 }).not.toBe(null)
|
||||||
|
const value = await probe(page).then(modelKey)
|
||||||
|
if (!value) throw new Error("Failed to resolve current model key")
|
||||||
|
return value
|
||||||
|
}
|
||||||
|
|
||||||
|
async function waitControl(page: Page, key: "setAgent" | "setModel" | "setVariant") {
|
||||||
await expect
|
await expect
|
||||||
.poll(
|
.poll(
|
||||||
async () => {
|
() =>
|
||||||
const next = dirKey(await probe(page))
|
page.evaluate((key) => {
|
||||||
if (next) hit = next
|
const win = window as Window & {
|
||||||
return next
|
__opencode_e2e?: {
|
||||||
},
|
model?: {
|
||||||
|
controls?: Record<string, unknown>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return !!win.__opencode_e2e?.model?.controls?.[key]
|
||||||
|
}, key),
|
||||||
{ timeout: 30_000 },
|
{ timeout: 30_000 },
|
||||||
)
|
)
|
||||||
.not.toBe("")
|
.toBe(true)
|
||||||
return hit
|
}
|
||||||
|
|
||||||
|
async function pickAgent(page: Page, value: string) {
|
||||||
|
await waitControl(page, "setAgent")
|
||||||
|
await page.evaluate((value) => {
|
||||||
|
const win = window as Window & {
|
||||||
|
__opencode_e2e?: {
|
||||||
|
model?: {
|
||||||
|
controls?: {
|
||||||
|
setAgent?: (value: string | undefined) => void
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const fn = win.__opencode_e2e?.model?.controls?.setAgent
|
||||||
|
if (!fn) throw new Error("Model e2e agent control is not enabled")
|
||||||
|
fn(value)
|
||||||
|
}, value)
|
||||||
|
}
|
||||||
|
|
||||||
|
async function pickModel(page: Page, value: { providerID: string; modelID: string }) {
|
||||||
|
await waitControl(page, "setModel")
|
||||||
|
await page.evaluate((value) => {
|
||||||
|
const win = window as Window & {
|
||||||
|
__opencode_e2e?: {
|
||||||
|
model?: {
|
||||||
|
controls?: {
|
||||||
|
setModel?: (value: { providerID: string; modelID: string } | undefined) => void
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const fn = win.__opencode_e2e?.model?.controls?.setModel
|
||||||
|
if (!fn) throw new Error("Model e2e model control is not enabled")
|
||||||
|
fn(value)
|
||||||
|
}, value)
|
||||||
|
}
|
||||||
|
|
||||||
|
async function pickVariant(page: Page, value: string) {
|
||||||
|
await waitControl(page, "setVariant")
|
||||||
|
await page.evaluate((value) => {
|
||||||
|
const win = window as Window & {
|
||||||
|
__opencode_e2e?: {
|
||||||
|
model?: {
|
||||||
|
controls?: {
|
||||||
|
setVariant?: (value: string | undefined) => void
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const fn = win.__opencode_e2e?.model?.controls?.setVariant
|
||||||
|
if (!fn) throw new Error("Model e2e variant control is not enabled")
|
||||||
|
fn(value)
|
||||||
|
}, value)
|
||||||
}
|
}
|
||||||
|
|
||||||
async function read(page: Page): Promise<Footer> {
|
async function read(page: Page): Promise<Footer> {
|
||||||
@@ -92,31 +172,15 @@ async function waitModel(page: Page, value: string) {
|
|||||||
async function choose(page: Page, root: string, value: string) {
|
async function choose(page: Page, root: string, value: string) {
|
||||||
const select = page.locator(root)
|
const select = page.locator(root)
|
||||||
await expect(select).toBeVisible()
|
await expect(select).toBeVisible()
|
||||||
await select.locator('[data-action], [data-slot="select-select-trigger"]').first().click()
|
await pickAgent(page, value)
|
||||||
const item = page
|
|
||||||
.locator('[data-slot="select-select-item"]')
|
|
||||||
.filter({ hasText: new RegExp(`^\\s*${escape(value)}\\s*$`) })
|
|
||||||
.first()
|
|
||||||
await expect(item).toBeVisible()
|
|
||||||
await item.click()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function variantCount(page: Page) {
|
async function variantCount(page: Page) {
|
||||||
const select = page.locator(promptVariantSelector)
|
return (await probe(page))?.variants?.length ?? 0
|
||||||
await expect(select).toBeVisible()
|
|
||||||
await select.locator('[data-slot="select-select-trigger"]').click()
|
|
||||||
const count = await page.locator('[data-slot="select-select-item"]').count()
|
|
||||||
await page.keyboard.press("Escape")
|
|
||||||
return count
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function agents(page: Page) {
|
async function agents(page: Page) {
|
||||||
const select = page.locator(promptAgentSelector)
|
return ((await probe(page))?.agents ?? []).map((item) => item.name).filter(Boolean)
|
||||||
await expect(select).toBeVisible()
|
|
||||||
await select.locator('[data-action], [data-slot="select-select-trigger"]').first().click()
|
|
||||||
const labels = await page.locator('[data-slot="select-select-item-label"]').allTextContents()
|
|
||||||
await page.keyboard.press("Escape")
|
|
||||||
return labels.map((item) => item.trim()).filter(Boolean)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function ensureVariant(page: Page, directory: string): Promise<Footer> {
|
async function ensureVariant(page: Page, directory: string): Promise<Footer> {
|
||||||
@@ -142,54 +206,28 @@ async function ensureVariant(page: Page, directory: string): Promise<Footer> {
|
|||||||
|
|
||||||
async function chooseDifferentVariant(page: Page): Promise<Footer> {
|
async function chooseDifferentVariant(page: Page): Promise<Footer> {
|
||||||
const current = await read(page)
|
const current = await read(page)
|
||||||
const select = page.locator(promptVariantSelector)
|
const next = (await probe(page))?.variants?.find((item) => item !== current.variant)
|
||||||
await expect(select).toBeVisible()
|
if (!next) throw new Error("Current model has no alternate variant to select")
|
||||||
await select.locator('[data-slot="select-select-trigger"]').click()
|
|
||||||
|
|
||||||
const items = page.locator('[data-slot="select-select-item"]')
|
await pickVariant(page, next)
|
||||||
const count = await items.count()
|
return waitFooter(page, { agent: current.agent, model: current.model, variant: next })
|
||||||
if (count < 2) throw new Error("Current model has no alternate variant to select")
|
|
||||||
|
|
||||||
for (let i = 0; i < count; i++) {
|
|
||||||
const item = items.nth(i)
|
|
||||||
const next = await text(item.locator('[data-slot="select-select-item-label"]').first())
|
|
||||||
if (!next || next === current.variant) continue
|
|
||||||
await item.click()
|
|
||||||
return waitFooter(page, { agent: current.agent, model: current.model, variant: next })
|
|
||||||
}
|
|
||||||
|
|
||||||
throw new Error("Failed to choose a different variant")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function chooseOtherModel(page: Page): Promise<Footer> {
|
async function chooseOtherModel(page: Page, skip: string[] = []): Promise<Footer> {
|
||||||
const current = await read(page)
|
const current = await currentModel(page)
|
||||||
const button = page.locator(`${promptModelSelector} [data-action="prompt-model"]`)
|
const next = (await probe(page))?.models?.find((item) => {
|
||||||
await expect(button).toBeVisible()
|
const key = `${item.providerID}:${item.modelID}`
|
||||||
await button.click()
|
return key !== current && !skip.includes(key)
|
||||||
|
})
|
||||||
const dialog = page.getByRole("dialog")
|
if (!next) throw new Error("Failed to choose a different model")
|
||||||
await expect(dialog).toBeVisible()
|
await pickModel(page, { providerID: next.providerID, modelID: next.modelID })
|
||||||
const items = dialog.locator('[data-slot="list-item"]')
|
await expect.poll(async () => (await read(page)).model, { timeout: 30_000 }).toBe(next.name)
|
||||||
const count = await items.count()
|
return read(page)
|
||||||
expect(count).toBeGreaterThan(1)
|
|
||||||
|
|
||||||
for (let i = 0; i < count; i++) {
|
|
||||||
const item = items.nth(i)
|
|
||||||
const selected = (await item.getAttribute("data-selected")) === "true"
|
|
||||||
if (selected) continue
|
|
||||||
await item.click()
|
|
||||||
await expect(dialog).toHaveCount(0)
|
|
||||||
await expect.poll(async () => (await read(page)).model !== current.model, { timeout: 30_000 }).toBe(true)
|
|
||||||
return read(page)
|
|
||||||
}
|
|
||||||
|
|
||||||
throw new Error("Failed to choose a different model")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function goto(page: Page, directory: string, sessionID?: string) {
|
async function goto(page: Page, directory: string, sessionID?: string) {
|
||||||
await page.goto(sessionPath(directory, sessionID))
|
await page.goto(sessionPath(directory, sessionID))
|
||||||
await expect(page.locator(promptSelector)).toBeVisible()
|
await waitSession(page, { directory, sessionID })
|
||||||
await expect.poll(async () => dirKey(await probe(page)), { timeout: 30_000 }).toBe(directory)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function submit(page: Page, value: string) {
|
async function submit(page: Page, value: string) {
|
||||||
@@ -224,10 +262,9 @@ async function createWorkspace(page: Page, root: string, seen: string[]) {
|
|||||||
await openSidebar(page)
|
await openSidebar(page)
|
||||||
await page.getByRole("button", { name: "New workspace" }).first().click()
|
await page.getByRole("button", { name: "New workspace" }).first().click()
|
||||||
|
|
||||||
const slug = await waitSlug(page, [root, ...seen])
|
const next = await resolveSlug(await waitSlug(page, [root, ...seen]))
|
||||||
const directory = base64Decode(slug)
|
await waitSession(page, { directory: next.directory })
|
||||||
if (!directory) throw new Error(`Failed to decode workspace slug: ${slug}`)
|
return next
|
||||||
return { slug, directory }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function waitWorkspace(page: Page, slug: string) {
|
async function waitWorkspace(page: Page, slug: string) {
|
||||||
@@ -257,36 +294,31 @@ async function newWorkspaceSession(page: Page, slug: string) {
|
|||||||
await expect(button).toBeVisible()
|
await expect(button).toBeVisible()
|
||||||
await button.click({ force: true })
|
await button.click({ force: true })
|
||||||
|
|
||||||
const next = await waitSlug(page)
|
const next = await resolveSlug(await waitSlug(page))
|
||||||
await expect(page).toHaveURL(new RegExp(`/${next}/session(?:[/?#]|$)`))
|
return waitSession(page, { directory: next.directory }).then((item) => item.directory)
|
||||||
await expect(page.locator(promptSelector)).toBeVisible()
|
|
||||||
return currentDir(page)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
test("session model and variant restore per session without leaking into new sessions", async ({
|
test("session model restore per session without leaking into new sessions", async ({ page, withProject }) => {
|
||||||
page,
|
|
||||||
withProject,
|
|
||||||
}) => {
|
|
||||||
await page.setViewportSize({ width: 1440, height: 900 })
|
await page.setViewportSize({ width: 1440, height: 900 })
|
||||||
|
|
||||||
await withProject(async ({ directory, gotoSession, trackSession }) => {
|
await withProject(async ({ directory, gotoSession, trackSession }) => {
|
||||||
await gotoSession()
|
await gotoSession()
|
||||||
|
|
||||||
await ensureVariant(page, directory)
|
const firstState = await chooseOtherModel(page)
|
||||||
const firstState = await chooseDifferentVariant(page)
|
const firstKey = await currentModel(page)
|
||||||
const first = await submit(page, `session variant ${Date.now()}`)
|
const first = await submit(page, `session variant ${Date.now()}`)
|
||||||
trackSession(first)
|
trackSession(first)
|
||||||
await waitUser(directory, first)
|
await waitUser(directory, first)
|
||||||
|
|
||||||
await page.reload()
|
await page.reload()
|
||||||
await expect(page.locator(promptSelector)).toBeVisible()
|
await waitSession(page, { directory, sessionID: first })
|
||||||
await waitFooter(page, firstState)
|
await waitFooter(page, firstState)
|
||||||
|
|
||||||
await gotoSession()
|
await gotoSession()
|
||||||
const fresh = await ensureVariant(page, directory)
|
const fresh = await read(page)
|
||||||
expect(fresh.variant).not.toBe(firstState.variant)
|
expect(fresh.model).not.toBe(firstState.model)
|
||||||
|
|
||||||
const secondState = await chooseOtherModel(page)
|
const secondState = await chooseOtherModel(page, [firstKey])
|
||||||
const second = await submit(page, `session model ${Date.now()}`)
|
const second = await submit(page, `session model ${Date.now()}`)
|
||||||
trackSession(second)
|
trackSession(second)
|
||||||
await waitUser(directory, second)
|
await waitUser(directory, second)
|
||||||
@@ -308,8 +340,8 @@ test("session model restore across workspaces", async ({ page, withProject }) =>
|
|||||||
await withProject(async ({ directory: root, slug, gotoSession, trackDirectory, trackSession }) => {
|
await withProject(async ({ directory: root, slug, gotoSession, trackDirectory, trackSession }) => {
|
||||||
await gotoSession()
|
await gotoSession()
|
||||||
|
|
||||||
await ensureVariant(page, root)
|
const firstState = await chooseOtherModel(page)
|
||||||
const firstState = await chooseDifferentVariant(page)
|
const firstKey = await currentModel(page)
|
||||||
const first = await submit(page, `root session ${Date.now()}`)
|
const first = await submit(page, `root session ${Date.now()}`)
|
||||||
trackSession(first, root)
|
trackSession(first, root)
|
||||||
await waitUser(root, first)
|
await waitUser(root, first)
|
||||||
@@ -321,7 +353,8 @@ test("session model restore across workspaces", async ({ page, withProject }) =>
|
|||||||
const oneDir = await newWorkspaceSession(page, one.slug)
|
const oneDir = await newWorkspaceSession(page, one.slug)
|
||||||
trackDirectory(oneDir)
|
trackDirectory(oneDir)
|
||||||
|
|
||||||
const secondState = await chooseOtherModel(page)
|
const secondState = await chooseOtherModel(page, [firstKey])
|
||||||
|
const secondKey = await currentModel(page)
|
||||||
const second = await submit(page, `workspace one ${Date.now()}`)
|
const second = await submit(page, `workspace one ${Date.now()}`)
|
||||||
trackSession(second, oneDir)
|
trackSession(second, oneDir)
|
||||||
await waitUser(oneDir, second)
|
await waitUser(oneDir, second)
|
||||||
@@ -330,8 +363,7 @@ test("session model restore across workspaces", async ({ page, withProject }) =>
|
|||||||
const twoDir = await newWorkspaceSession(page, two.slug)
|
const twoDir = await newWorkspaceSession(page, two.slug)
|
||||||
trackDirectory(twoDir)
|
trackDirectory(twoDir)
|
||||||
|
|
||||||
await ensureVariant(page, twoDir)
|
const thirdState = await chooseOtherModel(page, [firstKey, secondKey])
|
||||||
const thirdState = await chooseDifferentVariant(page)
|
|
||||||
const third = await submit(page, `workspace two ${Date.now()}`)
|
const third = await submit(page, `workspace two ${Date.now()}`)
|
||||||
trackSession(third, twoDir)
|
trackSession(third, twoDir)
|
||||||
await waitUser(twoDir, third)
|
await waitUser(twoDir, third)
|
||||||
|
|||||||
@@ -123,6 +123,217 @@ async function spot(page: Parameters<typeof test>[0]["page"], file: string) {
|
|||||||
}, file)
|
}, file)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function comment(page: Parameters<typeof test>[0]["page"], file: string, note: string) {
|
||||||
|
const row = page.locator(`[data-file="${file}"]`).first()
|
||||||
|
await expect(row).toBeVisible()
|
||||||
|
|
||||||
|
const line = row.locator('diffs-container [data-line="2"]').first()
|
||||||
|
await expect(line).toBeVisible()
|
||||||
|
await line.hover()
|
||||||
|
|
||||||
|
const add = row.getByRole("button", { name: /^Comment$/ }).first()
|
||||||
|
await expect(add).toBeVisible()
|
||||||
|
await add.click()
|
||||||
|
|
||||||
|
const area = row.locator('[data-slot="line-comment-textarea"]').first()
|
||||||
|
await expect(area).toBeVisible()
|
||||||
|
await area.fill(note)
|
||||||
|
|
||||||
|
const submit = row.locator('[data-slot="line-comment-action"][data-variant="primary"]').first()
|
||||||
|
await expect(submit).toBeEnabled()
|
||||||
|
await submit.click()
|
||||||
|
|
||||||
|
await expect(row.locator('[data-slot="line-comment-content"]').filter({ hasText: note }).first()).toBeVisible()
|
||||||
|
await expect(row.locator('[data-slot="line-comment-tools"]').first()).toBeVisible()
|
||||||
|
}
|
||||||
|
|
||||||
|
async function overflow(page: Parameters<typeof test>[0]["page"], file: string) {
|
||||||
|
const row = page.locator(`[data-file="${file}"]`).first()
|
||||||
|
const view = page.locator('[data-slot="session-review-scroll"] .scroll-view__viewport').first()
|
||||||
|
const pop = row.locator('[data-slot="line-comment-popover"][data-inline-body]').first()
|
||||||
|
const tools = row.locator('[data-slot="line-comment-tools"]').first()
|
||||||
|
|
||||||
|
const [width, viewBox, popBox, toolsBox] = await Promise.all([
|
||||||
|
view.evaluate((el) => el.scrollWidth - el.clientWidth),
|
||||||
|
view.boundingBox(),
|
||||||
|
pop.boundingBox(),
|
||||||
|
tools.boundingBox(),
|
||||||
|
])
|
||||||
|
|
||||||
|
if (!viewBox || !popBox || !toolsBox) return null
|
||||||
|
|
||||||
|
return {
|
||||||
|
width,
|
||||||
|
pop: popBox.x + popBox.width - (viewBox.x + viewBox.width),
|
||||||
|
tools: toolsBox.x + toolsBox.width - (viewBox.x + viewBox.width),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function openReviewFile(page: Parameters<typeof test>[0]["page"], file: string) {
|
||||||
|
const row = page.locator(`[data-file="${file}"]`).first()
|
||||||
|
await expect(row).toBeVisible()
|
||||||
|
await row.hover()
|
||||||
|
|
||||||
|
const open = row.getByRole("button", { name: /^Open file$/i }).first()
|
||||||
|
await expect(open).toBeVisible()
|
||||||
|
await open.click()
|
||||||
|
|
||||||
|
const tab = page.getByRole("tab", { name: file }).first()
|
||||||
|
await expect(tab).toBeVisible()
|
||||||
|
await tab.click()
|
||||||
|
|
||||||
|
const viewer = page.locator('[data-component="file"][data-mode="text"]').first()
|
||||||
|
await expect(viewer).toBeVisible()
|
||||||
|
return viewer
|
||||||
|
}
|
||||||
|
|
||||||
|
async function fileComment(page: Parameters<typeof test>[0]["page"], note: string) {
|
||||||
|
const viewer = page.locator('[data-component="file"][data-mode="text"]').first()
|
||||||
|
await expect(viewer).toBeVisible()
|
||||||
|
|
||||||
|
const line = viewer.locator('diffs-container [data-line="2"]').first()
|
||||||
|
await expect(line).toBeVisible()
|
||||||
|
await line.hover()
|
||||||
|
|
||||||
|
const add = viewer.getByRole("button", { name: /^Comment$/ }).first()
|
||||||
|
await expect(add).toBeVisible()
|
||||||
|
await add.click()
|
||||||
|
|
||||||
|
const area = viewer.locator('[data-slot="line-comment-textarea"]').first()
|
||||||
|
await expect(area).toBeVisible()
|
||||||
|
await area.fill(note)
|
||||||
|
|
||||||
|
const submit = viewer.locator('[data-slot="line-comment-action"][data-variant="primary"]').first()
|
||||||
|
await expect(submit).toBeEnabled()
|
||||||
|
await submit.click()
|
||||||
|
|
||||||
|
await expect(viewer.locator('[data-slot="line-comment-content"]').filter({ hasText: note }).first()).toBeVisible()
|
||||||
|
await expect(viewer.locator('[data-slot="line-comment-tools"]').first()).toBeVisible()
|
||||||
|
}
|
||||||
|
|
||||||
|
async function fileOverflow(page: Parameters<typeof test>[0]["page"]) {
|
||||||
|
const viewer = page.locator('[data-component="file"][data-mode="text"]').first()
|
||||||
|
const view = page.locator('[role="tabpanel"] .scroll-view__viewport').first()
|
||||||
|
const pop = viewer.locator('[data-slot="line-comment-popover"][data-inline-body]').first()
|
||||||
|
const tools = viewer.locator('[data-slot="line-comment-tools"]').first()
|
||||||
|
|
||||||
|
const [width, viewBox, popBox, toolsBox] = await Promise.all([
|
||||||
|
view.evaluate((el) => el.scrollWidth - el.clientWidth),
|
||||||
|
view.boundingBox(),
|
||||||
|
pop.boundingBox(),
|
||||||
|
tools.boundingBox(),
|
||||||
|
])
|
||||||
|
|
||||||
|
if (!viewBox || !popBox || !toolsBox) return null
|
||||||
|
|
||||||
|
return {
|
||||||
|
width,
|
||||||
|
pop: popBox.x + popBox.width - (viewBox.x + viewBox.width),
|
||||||
|
tools: toolsBox.x + toolsBox.width - (viewBox.x + viewBox.width),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
test("review applies inline comment clicks without horizontal overflow", async ({ page, withProject }) => {
|
||||||
|
test.skip(true, "Flaky in CI for now.")
|
||||||
|
test.setTimeout(180_000)
|
||||||
|
|
||||||
|
const tag = `review-comment-${Date.now()}`
|
||||||
|
const file = `review-comment-${tag}.txt`
|
||||||
|
const note = `comment ${tag}`
|
||||||
|
|
||||||
|
await page.setViewportSize({ width: 1280, height: 900 })
|
||||||
|
|
||||||
|
await withProject(async (project) => {
|
||||||
|
const sdk = createSdk(project.directory)
|
||||||
|
|
||||||
|
await withSession(sdk, `e2e review comment ${tag}`, async (session) => {
|
||||||
|
await patch(sdk, session.id, seed([{ file, mark: tag }]))
|
||||||
|
|
||||||
|
await expect
|
||||||
|
.poll(
|
||||||
|
async () => {
|
||||||
|
const diff = await sdk.session.diff({ sessionID: session.id }).then((res) => res.data ?? [])
|
||||||
|
return diff.length
|
||||||
|
},
|
||||||
|
{ timeout: 60_000 },
|
||||||
|
)
|
||||||
|
.toBe(1)
|
||||||
|
|
||||||
|
await project.gotoSession(session.id)
|
||||||
|
await show(page)
|
||||||
|
|
||||||
|
const tab = page.getByRole("tab", { name: /Review/i }).first()
|
||||||
|
await expect(tab).toBeVisible()
|
||||||
|
await tab.click()
|
||||||
|
|
||||||
|
await expand(page)
|
||||||
|
await waitMark(page, file, tag)
|
||||||
|
await comment(page, file, note)
|
||||||
|
|
||||||
|
await expect
|
||||||
|
.poll(async () => (await overflow(page, file))?.width ?? Number.POSITIVE_INFINITY, { timeout: 10_000 })
|
||||||
|
.toBeLessThanOrEqual(1)
|
||||||
|
await expect
|
||||||
|
.poll(async () => (await overflow(page, file))?.pop ?? Number.POSITIVE_INFINITY, { timeout: 10_000 })
|
||||||
|
.toBeLessThanOrEqual(1)
|
||||||
|
await expect
|
||||||
|
.poll(async () => (await overflow(page, file))?.tools ?? Number.POSITIVE_INFINITY, { timeout: 10_000 })
|
||||||
|
.toBeLessThanOrEqual(1)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
test("review file comments submit on click without clipping actions", async ({ page, withProject }) => {
|
||||||
|
test.skip(true, "Flaky in CI for now.")
|
||||||
|
test.setTimeout(180_000)
|
||||||
|
|
||||||
|
const tag = `review-file-comment-${Date.now()}`
|
||||||
|
const file = `review-file-comment-${tag}.txt`
|
||||||
|
const note = `comment ${tag}`
|
||||||
|
|
||||||
|
await page.setViewportSize({ width: 1280, height: 900 })
|
||||||
|
|
||||||
|
await withProject(async (project) => {
|
||||||
|
const sdk = createSdk(project.directory)
|
||||||
|
|
||||||
|
await withSession(sdk, `e2e review file comment ${tag}`, async (session) => {
|
||||||
|
await patch(sdk, session.id, seed([{ file, mark: tag }]))
|
||||||
|
|
||||||
|
await expect
|
||||||
|
.poll(
|
||||||
|
async () => {
|
||||||
|
const diff = await sdk.session.diff({ sessionID: session.id }).then((res) => res.data ?? [])
|
||||||
|
return diff.length
|
||||||
|
},
|
||||||
|
{ timeout: 60_000 },
|
||||||
|
)
|
||||||
|
.toBe(1)
|
||||||
|
|
||||||
|
await project.gotoSession(session.id)
|
||||||
|
await show(page)
|
||||||
|
|
||||||
|
const tab = page.getByRole("tab", { name: /Review/i }).first()
|
||||||
|
await expect(tab).toBeVisible()
|
||||||
|
await tab.click()
|
||||||
|
|
||||||
|
await expand(page)
|
||||||
|
await waitMark(page, file, tag)
|
||||||
|
await openReviewFile(page, file)
|
||||||
|
await fileComment(page, note)
|
||||||
|
|
||||||
|
await expect
|
||||||
|
.poll(async () => (await fileOverflow(page))?.width ?? Number.POSITIVE_INFINITY, { timeout: 10_000 })
|
||||||
|
.toBeLessThanOrEqual(1)
|
||||||
|
await expect
|
||||||
|
.poll(async () => (await fileOverflow(page))?.pop ?? Number.POSITIVE_INFINITY, { timeout: 10_000 })
|
||||||
|
.toBeLessThanOrEqual(1)
|
||||||
|
await expect
|
||||||
|
.poll(async () => (await fileOverflow(page))?.tools ?? Number.POSITIVE_INFINITY, { timeout: 10_000 })
|
||||||
|
.toBeLessThanOrEqual(1)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
test("review keeps scroll position after a live diff update", async ({ page, withProject }) => {
|
test("review keeps scroll position after a live diff update", async ({ page, withProject }) => {
|
||||||
test.skip(Boolean(process.env.CI), "Flaky in CI for now.")
|
test.skip(Boolean(process.env.CI), "Flaky in CI for now.")
|
||||||
test.setTimeout(180_000)
|
test.setTimeout(180_000)
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { test, expect } from "../fixtures"
|
import { test, expect } from "../fixtures"
|
||||||
import { openSettings, closeDialog, waitTerminalReady, withSession } from "../actions"
|
import { openSettings, closeDialog, waitTerminalFocusIdle, withSession } from "../actions"
|
||||||
import { keybindButtonSelector, terminalSelector } from "../selectors"
|
import { keybindButtonSelector, terminalSelector } from "../selectors"
|
||||||
import { modKey } from "../utils"
|
import { modKey } from "../utils"
|
||||||
|
|
||||||
@@ -241,7 +241,7 @@ test("changing file open keybind works", async ({ page, gotoSession }) => {
|
|||||||
await expect(keybindButton).toBeVisible()
|
await expect(keybindButton).toBeVisible()
|
||||||
|
|
||||||
const initialKeybind = await keybindButton.textContent()
|
const initialKeybind = await keybindButton.textContent()
|
||||||
expect(initialKeybind).toContain("P")
|
expect(initialKeybind).toContain("K")
|
||||||
|
|
||||||
await keybindButton.click()
|
await keybindButton.click()
|
||||||
await expect(keybindButton).toHaveText(/press/i)
|
await expect(keybindButton).toHaveText(/press/i)
|
||||||
@@ -302,7 +302,7 @@ test("changing terminal toggle keybind works", async ({ page, gotoSession }) =>
|
|||||||
await expect(terminal).not.toBeVisible()
|
await expect(terminal).not.toBeVisible()
|
||||||
|
|
||||||
await page.keyboard.press(`${modKey}+Y`)
|
await page.keyboard.press(`${modKey}+Y`)
|
||||||
await waitTerminalReady(page, { term: terminal })
|
await waitTerminalFocusIdle(page, { term: terminal })
|
||||||
|
|
||||||
await page.keyboard.press(`${modKey}+Y`)
|
await page.keyboard.press(`${modKey}+Y`)
|
||||||
await expect(terminal).not.toBeVisible()
|
await expect(terminal).not.toBeVisible()
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import { test, expect, settingsKey } from "../fixtures"
|
|||||||
import { closeDialog, openSettings } from "../actions"
|
import { closeDialog, openSettings } from "../actions"
|
||||||
import {
|
import {
|
||||||
settingsColorSchemeSelector,
|
settingsColorSchemeSelector,
|
||||||
settingsFontSelector,
|
settingsCodeFontSelector,
|
||||||
settingsLanguageSelectSelector,
|
settingsLanguageSelectSelector,
|
||||||
settingsNotificationsAgentSelector,
|
settingsNotificationsAgentSelector,
|
||||||
settingsNotificationsErrorsSelector,
|
settingsNotificationsErrorsSelector,
|
||||||
@@ -12,6 +12,7 @@ import {
|
|||||||
settingsSoundsErrorsSelector,
|
settingsSoundsErrorsSelector,
|
||||||
settingsSoundsPermissionsSelector,
|
settingsSoundsPermissionsSelector,
|
||||||
settingsThemeSelector,
|
settingsThemeSelector,
|
||||||
|
settingsUIFontSelector,
|
||||||
settingsUpdatesStartupSelector,
|
settingsUpdatesStartupSelector,
|
||||||
} from "../selectors"
|
} from "../selectors"
|
||||||
|
|
||||||
@@ -152,74 +153,28 @@ test("legacy oc-1 theme migrates to oc-2", async ({ page, gotoSession }) => {
|
|||||||
.toBeNull()
|
.toBeNull()
|
||||||
})
|
})
|
||||||
|
|
||||||
test("changing font persists in localStorage and updates CSS variable", async ({ page, gotoSession }) => {
|
test("typing a code font with spaces persists and updates CSS variable", async ({ page, gotoSession }) => {
|
||||||
await gotoSession()
|
await gotoSession()
|
||||||
|
|
||||||
const dialog = await openSettings(page)
|
const dialog = await openSettings(page)
|
||||||
const select = dialog.locator(settingsFontSelector)
|
const input = dialog.locator(settingsCodeFontSelector)
|
||||||
await expect(select).toBeVisible()
|
await expect(input).toBeVisible()
|
||||||
|
await expect(input).toHaveAttribute("placeholder", "System Mono")
|
||||||
|
|
||||||
const initialFontFamily = await page.evaluate(() => {
|
const initialFontFamily = await page.evaluate(() =>
|
||||||
return getComputedStyle(document.documentElement).getPropertyValue("--font-family-mono")
|
getComputedStyle(document.documentElement).getPropertyValue("--font-family-mono").trim(),
|
||||||
})
|
)
|
||||||
expect(initialFontFamily).toContain("IBM Plex Mono")
|
const initialUIFamily = await page.evaluate(() =>
|
||||||
|
getComputedStyle(document.documentElement).getPropertyValue("--font-family-sans").trim(),
|
||||||
|
)
|
||||||
|
expect(initialFontFamily).toContain("ui-monospace")
|
||||||
|
|
||||||
await select.locator('[data-slot="select-select-trigger"]').click()
|
const next = "Test Mono"
|
||||||
|
|
||||||
const items = page.locator('[data-slot="select-select-item"]')
|
await input.click()
|
||||||
await items.nth(2).click()
|
await input.clear()
|
||||||
|
await input.pressSequentially(next)
|
||||||
await page.waitForTimeout(100)
|
await expect(input).toHaveValue(next)
|
||||||
|
|
||||||
const stored = await page.evaluate((key) => {
|
|
||||||
const raw = localStorage.getItem(key)
|
|
||||||
return raw ? JSON.parse(raw) : null
|
|
||||||
}, settingsKey)
|
|
||||||
|
|
||||||
expect(stored?.appearance?.font).not.toBe("ibm-plex-mono")
|
|
||||||
|
|
||||||
const newFontFamily = await page.evaluate(() => {
|
|
||||||
return getComputedStyle(document.documentElement).getPropertyValue("--font-family-mono")
|
|
||||||
})
|
|
||||||
expect(newFontFamily).not.toBe(initialFontFamily)
|
|
||||||
})
|
|
||||||
|
|
||||||
test("color scheme and font rehydrate after reload", async ({ page, gotoSession }) => {
|
|
||||||
await gotoSession()
|
|
||||||
|
|
||||||
const dialog = await openSettings(page)
|
|
||||||
|
|
||||||
const colorSchemeSelect = dialog.locator(settingsColorSchemeSelector)
|
|
||||||
await expect(colorSchemeSelect).toBeVisible()
|
|
||||||
await colorSchemeSelect.locator('[data-slot="select-select-trigger"]').click()
|
|
||||||
await page.locator('[data-slot="select-select-item"]').filter({ hasText: "Dark" }).click()
|
|
||||||
await expect(page.locator("html")).toHaveAttribute("data-color-scheme", "dark")
|
|
||||||
|
|
||||||
const fontSelect = dialog.locator(settingsFontSelector)
|
|
||||||
await expect(fontSelect).toBeVisible()
|
|
||||||
|
|
||||||
const initialFontFamily = await page.evaluate(() => {
|
|
||||||
return getComputedStyle(document.documentElement).getPropertyValue("--font-family-mono").trim()
|
|
||||||
})
|
|
||||||
|
|
||||||
const initialSettings = await page.evaluate((key) => {
|
|
||||||
const raw = localStorage.getItem(key)
|
|
||||||
return raw ? JSON.parse(raw) : null
|
|
||||||
}, settingsKey)
|
|
||||||
|
|
||||||
const currentFont =
|
|
||||||
(await fontSelect.locator('[data-slot="select-select-trigger-value"]').textContent())?.trim() ?? ""
|
|
||||||
await fontSelect.locator('[data-slot="select-select-trigger"]').click()
|
|
||||||
|
|
||||||
const fontItems = page.locator('[data-slot="select-select-item"]')
|
|
||||||
expect(await fontItems.count()).toBeGreaterThan(1)
|
|
||||||
|
|
||||||
if (currentFont) {
|
|
||||||
await fontItems.filter({ hasNotText: currentFont }).first().click()
|
|
||||||
}
|
|
||||||
if (!currentFont) {
|
|
||||||
await fontItems.nth(1).click()
|
|
||||||
}
|
|
||||||
|
|
||||||
await expect
|
await expect
|
||||||
.poll(async () => {
|
.poll(async () => {
|
||||||
@@ -230,7 +185,218 @@ test("color scheme and font rehydrate after reload", async ({ page, gotoSession
|
|||||||
})
|
})
|
||||||
.toMatchObject({
|
.toMatchObject({
|
||||||
appearance: {
|
appearance: {
|
||||||
font: expect.any(String),
|
mono: next,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
const newFontFamily = await page.evaluate(() =>
|
||||||
|
getComputedStyle(document.documentElement).getPropertyValue("--font-family-mono").trim(),
|
||||||
|
)
|
||||||
|
const newUIFamily = await page.evaluate(() =>
|
||||||
|
getComputedStyle(document.documentElement).getPropertyValue("--font-family-sans").trim(),
|
||||||
|
)
|
||||||
|
expect(newFontFamily).toContain(next)
|
||||||
|
expect(newFontFamily).not.toBe(initialFontFamily)
|
||||||
|
expect(newUIFamily).toBe(initialUIFamily)
|
||||||
|
})
|
||||||
|
|
||||||
|
test("typing a UI font with spaces persists and updates CSS variable", async ({ page, gotoSession }) => {
|
||||||
|
await gotoSession()
|
||||||
|
|
||||||
|
const dialog = await openSettings(page)
|
||||||
|
const input = dialog.locator(settingsUIFontSelector)
|
||||||
|
await expect(input).toBeVisible()
|
||||||
|
await expect(input).toHaveAttribute("placeholder", "System Sans")
|
||||||
|
|
||||||
|
const initialFontFamily = await page.evaluate(() =>
|
||||||
|
getComputedStyle(document.documentElement).getPropertyValue("--font-family-sans").trim(),
|
||||||
|
)
|
||||||
|
const initialCodeFamily = await page.evaluate(() =>
|
||||||
|
getComputedStyle(document.documentElement).getPropertyValue("--font-family-mono").trim(),
|
||||||
|
)
|
||||||
|
expect(initialFontFamily).toContain("ui-sans-serif")
|
||||||
|
|
||||||
|
const next = "Test Sans"
|
||||||
|
|
||||||
|
await input.click()
|
||||||
|
await input.clear()
|
||||||
|
await input.pressSequentially(next)
|
||||||
|
await expect(input).toHaveValue(next)
|
||||||
|
|
||||||
|
await expect
|
||||||
|
.poll(async () => {
|
||||||
|
return await page.evaluate((key) => {
|
||||||
|
const raw = localStorage.getItem(key)
|
||||||
|
return raw ? JSON.parse(raw) : null
|
||||||
|
}, settingsKey)
|
||||||
|
})
|
||||||
|
.toMatchObject({
|
||||||
|
appearance: {
|
||||||
|
sans: next,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
const newFontFamily = await page.evaluate(() =>
|
||||||
|
getComputedStyle(document.documentElement).getPropertyValue("--font-family-sans").trim(),
|
||||||
|
)
|
||||||
|
const newCodeFamily = await page.evaluate(() =>
|
||||||
|
getComputedStyle(document.documentElement).getPropertyValue("--font-family-mono").trim(),
|
||||||
|
)
|
||||||
|
expect(newFontFamily).toContain(next)
|
||||||
|
expect(newFontFamily).not.toBe(initialFontFamily)
|
||||||
|
expect(newCodeFamily).toBe(initialCodeFamily)
|
||||||
|
})
|
||||||
|
|
||||||
|
test("clearing the code font field restores the default placeholder and stack", async ({ page, gotoSession }) => {
|
||||||
|
await gotoSession()
|
||||||
|
|
||||||
|
const dialog = await openSettings(page)
|
||||||
|
const input = dialog.locator(settingsCodeFontSelector)
|
||||||
|
await expect(input).toBeVisible()
|
||||||
|
|
||||||
|
await input.click()
|
||||||
|
await input.clear()
|
||||||
|
await input.pressSequentially("Reset Mono")
|
||||||
|
|
||||||
|
await expect
|
||||||
|
.poll(async () => {
|
||||||
|
return await page.evaluate((key) => {
|
||||||
|
const raw = localStorage.getItem(key)
|
||||||
|
return raw ? JSON.parse(raw) : null
|
||||||
|
}, settingsKey)
|
||||||
|
})
|
||||||
|
.toMatchObject({
|
||||||
|
appearance: {
|
||||||
|
mono: "Reset Mono",
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
await input.clear()
|
||||||
|
await input.press("Space")
|
||||||
|
await expect(input).toHaveValue("")
|
||||||
|
await expect(input).toHaveAttribute("placeholder", "System Mono")
|
||||||
|
|
||||||
|
await expect
|
||||||
|
.poll(async () => {
|
||||||
|
return await page.evaluate((key) => {
|
||||||
|
const raw = localStorage.getItem(key)
|
||||||
|
return raw ? JSON.parse(raw) : null
|
||||||
|
}, settingsKey)
|
||||||
|
})
|
||||||
|
.toMatchObject({
|
||||||
|
appearance: {
|
||||||
|
mono: "",
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
const fontFamily = await page.evaluate(() =>
|
||||||
|
getComputedStyle(document.documentElement).getPropertyValue("--font-family-mono").trim(),
|
||||||
|
)
|
||||||
|
expect(fontFamily).toContain("ui-monospace")
|
||||||
|
expect(fontFamily).not.toContain("Reset Mono")
|
||||||
|
})
|
||||||
|
|
||||||
|
test("clearing the UI font field restores the default placeholder and stack", async ({ page, gotoSession }) => {
|
||||||
|
await gotoSession()
|
||||||
|
|
||||||
|
const dialog = await openSettings(page)
|
||||||
|
const input = dialog.locator(settingsUIFontSelector)
|
||||||
|
await expect(input).toBeVisible()
|
||||||
|
|
||||||
|
await input.click()
|
||||||
|
await input.clear()
|
||||||
|
await input.pressSequentially("Reset Sans")
|
||||||
|
|
||||||
|
await expect
|
||||||
|
.poll(async () => {
|
||||||
|
return await page.evaluate((key) => {
|
||||||
|
const raw = localStorage.getItem(key)
|
||||||
|
return raw ? JSON.parse(raw) : null
|
||||||
|
}, settingsKey)
|
||||||
|
})
|
||||||
|
.toMatchObject({
|
||||||
|
appearance: {
|
||||||
|
sans: "Reset Sans",
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
await input.clear()
|
||||||
|
await input.press("Space")
|
||||||
|
await expect(input).toHaveValue("")
|
||||||
|
await expect(input).toHaveAttribute("placeholder", "System Sans")
|
||||||
|
|
||||||
|
await expect
|
||||||
|
.poll(async () => {
|
||||||
|
return await page.evaluate((key) => {
|
||||||
|
const raw = localStorage.getItem(key)
|
||||||
|
return raw ? JSON.parse(raw) : null
|
||||||
|
}, settingsKey)
|
||||||
|
})
|
||||||
|
.toMatchObject({
|
||||||
|
appearance: {
|
||||||
|
sans: "",
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
const fontFamily = await page.evaluate(() =>
|
||||||
|
getComputedStyle(document.documentElement).getPropertyValue("--font-family-sans").trim(),
|
||||||
|
)
|
||||||
|
expect(fontFamily).toContain("ui-sans-serif")
|
||||||
|
expect(fontFamily).not.toContain("Reset Sans")
|
||||||
|
})
|
||||||
|
|
||||||
|
test("color scheme, code font, and UI font rehydrate after reload", async ({ page, gotoSession }) => {
|
||||||
|
await gotoSession()
|
||||||
|
|
||||||
|
const dialog = await openSettings(page)
|
||||||
|
|
||||||
|
const colorSchemeSelect = dialog.locator(settingsColorSchemeSelector)
|
||||||
|
await expect(colorSchemeSelect).toBeVisible()
|
||||||
|
await colorSchemeSelect.locator('[data-slot="select-select-trigger"]').click()
|
||||||
|
await page.locator('[data-slot="select-select-item"]').filter({ hasText: "Dark" }).click()
|
||||||
|
await expect(page.locator("html")).toHaveAttribute("data-color-scheme", "dark")
|
||||||
|
|
||||||
|
const code = dialog.locator(settingsCodeFontSelector)
|
||||||
|
const ui = dialog.locator(settingsUIFontSelector)
|
||||||
|
await expect(code).toBeVisible()
|
||||||
|
await expect(ui).toBeVisible()
|
||||||
|
|
||||||
|
const initialMono = await page.evaluate(() =>
|
||||||
|
getComputedStyle(document.documentElement).getPropertyValue("--font-family-mono").trim(),
|
||||||
|
)
|
||||||
|
const initialSans = await page.evaluate(() =>
|
||||||
|
getComputedStyle(document.documentElement).getPropertyValue("--font-family-sans").trim(),
|
||||||
|
)
|
||||||
|
|
||||||
|
const initialSettings = await page.evaluate((key) => {
|
||||||
|
const raw = localStorage.getItem(key)
|
||||||
|
return raw ? JSON.parse(raw) : null
|
||||||
|
}, settingsKey)
|
||||||
|
|
||||||
|
const mono = initialSettings?.appearance?.mono === "Reload Mono" ? "Reload Mono 2" : "Reload Mono"
|
||||||
|
const sans = initialSettings?.appearance?.sans === "Reload Sans" ? "Reload Sans 2" : "Reload Sans"
|
||||||
|
|
||||||
|
await code.click()
|
||||||
|
await code.clear()
|
||||||
|
await code.pressSequentially(mono)
|
||||||
|
await expect(code).toHaveValue(mono)
|
||||||
|
|
||||||
|
await ui.click()
|
||||||
|
await ui.clear()
|
||||||
|
await ui.pressSequentially(sans)
|
||||||
|
await expect(ui).toHaveValue(sans)
|
||||||
|
|
||||||
|
await expect
|
||||||
|
.poll(async () => {
|
||||||
|
return await page.evaluate((key) => {
|
||||||
|
const raw = localStorage.getItem(key)
|
||||||
|
return raw ? JSON.parse(raw) : null
|
||||||
|
}, settingsKey)
|
||||||
|
})
|
||||||
|
.toMatchObject({
|
||||||
|
appearance: {
|
||||||
|
mono,
|
||||||
|
sans,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -239,11 +405,18 @@ test("color scheme and font rehydrate after reload", async ({ page, gotoSession
|
|||||||
return raw ? JSON.parse(raw) : null
|
return raw ? JSON.parse(raw) : null
|
||||||
}, settingsKey)
|
}, settingsKey)
|
||||||
|
|
||||||
const updatedFontFamily = await page.evaluate(() => {
|
const updatedMono = await page.evaluate(() =>
|
||||||
return getComputedStyle(document.documentElement).getPropertyValue("--font-family-mono").trim()
|
getComputedStyle(document.documentElement).getPropertyValue("--font-family-mono").trim(),
|
||||||
})
|
)
|
||||||
expect(updatedFontFamily).not.toBe(initialFontFamily)
|
const updatedSans = await page.evaluate(() =>
|
||||||
expect(updatedSettings?.appearance?.font).not.toBe(initialSettings?.appearance?.font)
|
getComputedStyle(document.documentElement).getPropertyValue("--font-family-sans").trim(),
|
||||||
|
)
|
||||||
|
expect(updatedMono).toContain(mono)
|
||||||
|
expect(updatedMono).not.toBe(initialMono)
|
||||||
|
expect(updatedSans).toContain(sans)
|
||||||
|
expect(updatedSans).not.toBe(initialSans)
|
||||||
|
expect(updatedSettings?.appearance?.mono).toBe(mono)
|
||||||
|
expect(updatedSettings?.appearance?.sans).toBe(sans)
|
||||||
|
|
||||||
await closeDialog(page, dialog)
|
await closeDialog(page, dialog)
|
||||||
await page.reload()
|
await page.reload()
|
||||||
@@ -259,7 +432,8 @@ test("color scheme and font rehydrate after reload", async ({ page, gotoSession
|
|||||||
})
|
})
|
||||||
.toMatchObject({
|
.toMatchObject({
|
||||||
appearance: {
|
appearance: {
|
||||||
font: updatedSettings?.appearance?.font,
|
mono,
|
||||||
|
sans,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -270,17 +444,32 @@ test("color scheme and font rehydrate after reload", async ({ page, gotoSession
|
|||||||
|
|
||||||
await expect
|
await expect
|
||||||
.poll(async () => {
|
.poll(async () => {
|
||||||
return await page.evaluate(() => {
|
return await page.evaluate(() =>
|
||||||
return getComputedStyle(document.documentElement).getPropertyValue("--font-family-mono").trim()
|
getComputedStyle(document.documentElement).getPropertyValue("--font-family-mono").trim(),
|
||||||
})
|
)
|
||||||
})
|
})
|
||||||
.not.toBe(initialFontFamily)
|
.toContain(mono)
|
||||||
|
|
||||||
const rehydratedFontFamily = await page.evaluate(() => {
|
await expect
|
||||||
return getComputedStyle(document.documentElement).getPropertyValue("--font-family-mono").trim()
|
.poll(async () => {
|
||||||
})
|
return await page.evaluate(() =>
|
||||||
expect(rehydratedFontFamily).not.toBe(initialFontFamily)
|
getComputedStyle(document.documentElement).getPropertyValue("--font-family-sans").trim(),
|
||||||
expect(rehydratedSettings?.appearance?.font).toBe(updatedSettings?.appearance?.font)
|
)
|
||||||
|
})
|
||||||
|
.toContain(sans)
|
||||||
|
|
||||||
|
const rehydratedMono = await page.evaluate(() =>
|
||||||
|
getComputedStyle(document.documentElement).getPropertyValue("--font-family-mono").trim(),
|
||||||
|
)
|
||||||
|
const rehydratedSans = await page.evaluate(() =>
|
||||||
|
getComputedStyle(document.documentElement).getPropertyValue("--font-family-sans").trim(),
|
||||||
|
)
|
||||||
|
expect(rehydratedMono).toContain(mono)
|
||||||
|
expect(rehydratedMono).not.toBe(initialMono)
|
||||||
|
expect(rehydratedSans).toContain(sans)
|
||||||
|
expect(rehydratedSans).not.toBe(initialSans)
|
||||||
|
expect(rehydratedSettings?.appearance?.mono).toBe(mono)
|
||||||
|
expect(rehydratedSettings?.appearance?.sans).toBe(sans)
|
||||||
})
|
})
|
||||||
|
|
||||||
test("toggling notification agent switch updates localStorage", async ({ page, gotoSession }) => {
|
test("toggling notification agent switch updates localStorage", async ({ page, gotoSession }) => {
|
||||||
|
|||||||
@@ -1,6 +1,16 @@
|
|||||||
import { test, expect } from "../fixtures"
|
import { test, expect } from "../fixtures"
|
||||||
import { cleanupSession, closeSidebar, hoverSessionItem } from "../actions"
|
import {
|
||||||
|
defocus,
|
||||||
|
cleanupSession,
|
||||||
|
cleanupTestProject,
|
||||||
|
closeSidebar,
|
||||||
|
createTestProject,
|
||||||
|
hoverSessionItem,
|
||||||
|
openSidebar,
|
||||||
|
waitSession,
|
||||||
|
} from "../actions"
|
||||||
import { projectSwitchSelector } from "../selectors"
|
import { projectSwitchSelector } from "../selectors"
|
||||||
|
import { dirSlug } from "../utils"
|
||||||
|
|
||||||
test("collapsed sidebar popover stays open when archiving a session", async ({ page, slug, sdk, gotoSession }) => {
|
test("collapsed sidebar popover stays open when archiving a session", async ({ page, slug, sdk, gotoSession }) => {
|
||||||
const stamp = Date.now()
|
const stamp = Date.now()
|
||||||
@@ -37,3 +47,72 @@ test("collapsed sidebar popover stays open when archiving a session", async ({ p
|
|||||||
await cleanupSession({ sdk, sessionID: two.id })
|
await cleanupSession({ sdk, sessionID: two.id })
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
test("open sidebar project popover stays closed after clicking avatar", async ({ page, withProject }) => {
|
||||||
|
await page.setViewportSize({ width: 1400, height: 800 })
|
||||||
|
|
||||||
|
const other = await createTestProject()
|
||||||
|
const slug = dirSlug(other)
|
||||||
|
|
||||||
|
try {
|
||||||
|
await withProject(
|
||||||
|
async () => {
|
||||||
|
await openSidebar(page)
|
||||||
|
|
||||||
|
const project = page.locator(projectSwitchSelector(slug)).first()
|
||||||
|
const card = page.locator('[data-component="hover-card-content"]')
|
||||||
|
|
||||||
|
await expect(project).toBeVisible()
|
||||||
|
await project.hover()
|
||||||
|
await expect(card.getByText(/recent sessions/i)).toBeVisible()
|
||||||
|
|
||||||
|
await page.mouse.down()
|
||||||
|
await expect(card).toHaveCount(0)
|
||||||
|
await page.mouse.up()
|
||||||
|
|
||||||
|
await waitSession(page, { directory: other })
|
||||||
|
await expect(card).toHaveCount(0)
|
||||||
|
},
|
||||||
|
{ extra: [other] },
|
||||||
|
)
|
||||||
|
} finally {
|
||||||
|
await cleanupTestProject(other)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
test("open sidebar project switch activates on first tabbed enter", async ({ page, withProject }) => {
|
||||||
|
await page.setViewportSize({ width: 1400, height: 800 })
|
||||||
|
|
||||||
|
const other = await createTestProject()
|
||||||
|
const slug = dirSlug(other)
|
||||||
|
|
||||||
|
try {
|
||||||
|
await withProject(
|
||||||
|
async () => {
|
||||||
|
await openSidebar(page)
|
||||||
|
await defocus(page)
|
||||||
|
|
||||||
|
const project = page.locator(projectSwitchSelector(slug)).first()
|
||||||
|
|
||||||
|
await expect(project).toBeVisible()
|
||||||
|
|
||||||
|
let hit = false
|
||||||
|
for (let i = 0; i < 20; i++) {
|
||||||
|
hit = await project.evaluate((el) => {
|
||||||
|
return el.matches(":focus") || !!el.parentElement?.matches(":focus")
|
||||||
|
})
|
||||||
|
if (hit) break
|
||||||
|
await page.keyboard.press("Tab")
|
||||||
|
}
|
||||||
|
|
||||||
|
expect(hit).toBe(true)
|
||||||
|
|
||||||
|
await page.keyboard.press("Enter")
|
||||||
|
await waitSession(page, { directory: other })
|
||||||
|
},
|
||||||
|
{ extra: [other] },
|
||||||
|
)
|
||||||
|
} finally {
|
||||||
|
await cleanupTestProject(other)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { test, expect } from "../fixtures"
|
import { test, expect } from "../fixtures"
|
||||||
import { waitTerminalReady } from "../actions"
|
import { waitTerminalFocusIdle, waitTerminalReady } from "../actions"
|
||||||
import { promptSelector, terminalSelector } from "../selectors"
|
import { promptSelector, terminalSelector } from "../selectors"
|
||||||
import { terminalToggleKey } from "../utils"
|
import { terminalToggleKey } from "../utils"
|
||||||
|
|
||||||
@@ -14,7 +14,7 @@ test("smoke terminal mounts and can create a second tab", async ({ page, gotoSes
|
|||||||
await page.keyboard.press(terminalToggleKey)
|
await page.keyboard.press(terminalToggleKey)
|
||||||
}
|
}
|
||||||
|
|
||||||
await waitTerminalReady(page, { term: terminals.first() })
|
await waitTerminalFocusIdle(page, { term: terminals.first() })
|
||||||
await expect(terminals).toHaveCount(1)
|
await expect(terminals).toHaveCount(1)
|
||||||
|
|
||||||
// Ghostty captures a lot of keybinds when focused; move focus back
|
// Ghostty captures a lot of keybinds when focused; move focus back
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import type { Page } from "@playwright/test"
|
import type { Page } from "@playwright/test"
|
||||||
import { runTerminal, waitTerminalReady } from "../actions"
|
import { runTerminal, waitTerminalReady } from "../actions"
|
||||||
import { test, expect } from "../fixtures"
|
import { test, expect } from "../fixtures"
|
||||||
import { terminalSelector } from "../selectors"
|
import { dropdownMenuContentSelector, terminalSelector } from "../selectors"
|
||||||
import { terminalToggleKey, workspacePersistKey } from "../utils"
|
import { terminalToggleKey, workspacePersistKey } from "../utils"
|
||||||
|
|
||||||
type State = {
|
type State = {
|
||||||
@@ -130,3 +130,39 @@ test("closing the active terminal tab falls back to the previous tab", async ({
|
|||||||
.toEqual({ count: 1, first: true })
|
.toEqual({ count: 1, first: true })
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
test("terminal tab can be renamed from the context menu", async ({ page, withProject }) => {
|
||||||
|
await withProject(async ({ directory, gotoSession }) => {
|
||||||
|
const key = workspacePersistKey(directory, "terminal")
|
||||||
|
const rename = `E2E term ${Date.now()}`
|
||||||
|
const tab = page.locator('#terminal-panel [data-slot="tabs-trigger"]').first()
|
||||||
|
|
||||||
|
await gotoSession()
|
||||||
|
await open(page)
|
||||||
|
|
||||||
|
await expect(tab).toContainText(/Terminal 1/)
|
||||||
|
await tab.click({ button: "right" })
|
||||||
|
|
||||||
|
const menu = page.locator(dropdownMenuContentSelector).first()
|
||||||
|
await expect(menu).toBeVisible()
|
||||||
|
await menu.getByRole("menuitem", { name: /^Rename$/i }).click()
|
||||||
|
await expect(menu).toHaveCount(0)
|
||||||
|
|
||||||
|
const input = page.locator('#terminal-panel input[type="text"]').first()
|
||||||
|
await expect(input).toBeVisible()
|
||||||
|
await input.fill(rename)
|
||||||
|
await input.press("Enter")
|
||||||
|
|
||||||
|
await expect(input).toHaveCount(0)
|
||||||
|
await expect(tab).toContainText(rename)
|
||||||
|
await expect
|
||||||
|
.poll(
|
||||||
|
async () => {
|
||||||
|
const state = await store(page, key)
|
||||||
|
return state?.all[0]?.title
|
||||||
|
},
|
||||||
|
{ timeout: 5_000 },
|
||||||
|
)
|
||||||
|
.toBe(rename)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
<html lang="en" style="background-color: var(--background-base)">
|
<html lang="en" style="background-color: var(--background-base)">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8" />
|
<meta charset="utf-8" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
<meta name="viewport" content="width=device-width, initial-scale=1, interactive-widget=resizes-content" />
|
||||||
<title>OpenCode</title>
|
<title>OpenCode</title>
|
||||||
<link rel="icon" type="image/png" href="/favicon-96x96-v3.png" sizes="96x96" />
|
<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="icon" type="image/svg+xml" href="/favicon-v3.svg" />
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@opencode-ai/app",
|
"name": "@opencode-ai/app",
|
||||||
"version": "1.2.27",
|
"version": "1.3.11",
|
||||||
"description": "",
|
"description": "",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"exports": {
|
"exports": {
|
||||||
@@ -51,12 +51,14 @@
|
|||||||
"@solid-primitives/resize-observer": "2.1.3",
|
"@solid-primitives/resize-observer": "2.1.3",
|
||||||
"@solid-primitives/scroll": "2.1.3",
|
"@solid-primitives/scroll": "2.1.3",
|
||||||
"@solid-primitives/storage": "catalog:",
|
"@solid-primitives/storage": "catalog:",
|
||||||
|
"@solid-primitives/timer": "1.4.4",
|
||||||
"@solid-primitives/websocket": "1.3.1",
|
"@solid-primitives/websocket": "1.3.1",
|
||||||
"@solidjs/meta": "catalog:",
|
"@solidjs/meta": "catalog:",
|
||||||
"@solidjs/router": "catalog:",
|
"@solidjs/router": "catalog:",
|
||||||
|
"@tanstack/solid-query": "5.91.4",
|
||||||
"@thisbeyond/solid-dnd": "0.7.5",
|
"@thisbeyond/solid-dnd": "0.7.5",
|
||||||
"diff": "catalog:",
|
"diff": "catalog:",
|
||||||
"effect": "4.0.0-beta.31",
|
"effect": "catalog:",
|
||||||
"fuzzysort": "catalog:",
|
"fuzzysort": "catalog:",
|
||||||
"ghostty-web": "github:anomalyco/ghostty-web#main",
|
"ghostty-web": "github:anomalyco/ghostty-web#main",
|
||||||
"luxon": "catalog:",
|
"luxon": "catalog:",
|
||||||
|
|||||||
@@ -71,7 +71,7 @@ const serverEnv = {
|
|||||||
OPENCODE_E2E_PROJECT_DIR: repoDir,
|
OPENCODE_E2E_PROJECT_DIR: repoDir,
|
||||||
OPENCODE_E2E_SESSION_TITLE: "E2E Session",
|
OPENCODE_E2E_SESSION_TITLE: "E2E Session",
|
||||||
OPENCODE_E2E_MESSAGE: "Seeded for UI e2e",
|
OPENCODE_E2E_MESSAGE: "Seeded for UI e2e",
|
||||||
OPENCODE_E2E_MODEL: "opencode/gpt-5-nano",
|
OPENCODE_E2E_MODEL: process.env.OPENCODE_E2E_MODEL ?? "opencode/gpt-5-nano",
|
||||||
OPENCODE_CLIENT: "app",
|
OPENCODE_CLIENT: "app",
|
||||||
OPENCODE_STRICT_CONFIG_DEPS: "true",
|
OPENCODE_STRICT_CONFIG_DEPS: "true",
|
||||||
} satisfies Record<string, string>
|
} satisfies Record<string, string>
|
||||||
|
|||||||
+57
-41
@@ -6,9 +6,10 @@ import { MarkedProvider } from "@opencode-ai/ui/context/marked"
|
|||||||
import { File } from "@opencode-ai/ui/file"
|
import { File } from "@opencode-ai/ui/file"
|
||||||
import { Font } from "@opencode-ai/ui/font"
|
import { Font } from "@opencode-ai/ui/font"
|
||||||
import { Splash } from "@opencode-ai/ui/logo"
|
import { Splash } from "@opencode-ai/ui/logo"
|
||||||
import { ThemeProvider } from "@opencode-ai/ui/theme"
|
import { ThemeProvider } from "@opencode-ai/ui/theme/context"
|
||||||
import { MetaProvider } from "@solidjs/meta"
|
import { MetaProvider } from "@solidjs/meta"
|
||||||
import { type BaseRouterProps, Navigate, Route, Router } from "@solidjs/router"
|
import { type BaseRouterProps, Navigate, Route, Router } from "@solidjs/router"
|
||||||
|
import { QueryClient, QueryClientProvider } from "@tanstack/solid-query"
|
||||||
import { type Duration, Effect } from "effect"
|
import { type Duration, Effect } from "effect"
|
||||||
import {
|
import {
|
||||||
type Component,
|
type Component,
|
||||||
@@ -31,12 +32,11 @@ import { FileProvider } from "@/context/file"
|
|||||||
import { GlobalSDKProvider } from "@/context/global-sdk"
|
import { GlobalSDKProvider } from "@/context/global-sdk"
|
||||||
import { GlobalSyncProvider } from "@/context/global-sync"
|
import { GlobalSyncProvider } from "@/context/global-sync"
|
||||||
import { HighlightsProvider } from "@/context/highlights"
|
import { HighlightsProvider } from "@/context/highlights"
|
||||||
import { LanguageProvider, useLanguage } from "@/context/language"
|
import { LanguageProvider, type Locale, useLanguage } from "@/context/language"
|
||||||
import { LayoutProvider } from "@/context/layout"
|
import { LayoutProvider } from "@/context/layout"
|
||||||
import { ModelsProvider } from "@/context/models"
|
import { ModelsProvider } from "@/context/models"
|
||||||
import { NotificationProvider } from "@/context/notification"
|
import { NotificationProvider } from "@/context/notification"
|
||||||
import { PermissionProvider } from "@/context/permission"
|
import { PermissionProvider } from "@/context/permission"
|
||||||
import { usePlatform } from "@/context/platform"
|
|
||||||
import { PromptProvider } from "@/context/prompt"
|
import { PromptProvider } from "@/context/prompt"
|
||||||
import { ServerConnection, ServerProvider, serverName, useServer } from "@/context/server"
|
import { ServerConnection, ServerProvider, serverName, useServer } from "@/context/server"
|
||||||
import { SettingsProvider } from "@/context/settings"
|
import { SettingsProvider } from "@/context/settings"
|
||||||
@@ -46,21 +46,18 @@ import Layout from "@/pages/layout"
|
|||||||
import { ErrorPage } from "./pages/error"
|
import { ErrorPage } from "./pages/error"
|
||||||
import { useCheckServerHealth } from "./utils/server-health"
|
import { useCheckServerHealth } from "./utils/server-health"
|
||||||
|
|
||||||
const Home = lazy(() => import("@/pages/home"))
|
const HomeRoute = lazy(() => import("@/pages/home"))
|
||||||
const Session = lazy(() => import("@/pages/session"))
|
const loadSession = () => import("@/pages/session")
|
||||||
|
const Session = lazy(loadSession)
|
||||||
const Loading = () => <div class="size-full" />
|
const Loading = () => <div class="size-full" />
|
||||||
|
|
||||||
const HomeRoute = () => (
|
if (typeof location === "object" && /\/session(?:\/|$)/.test(location.pathname)) {
|
||||||
<Suspense fallback={<Loading />}>
|
void loadSession()
|
||||||
<Home />
|
}
|
||||||
</Suspense>
|
|
||||||
)
|
|
||||||
|
|
||||||
const SessionRoute = () => (
|
const SessionRoute = () => (
|
||||||
<SessionProviders>
|
<SessionProviders>
|
||||||
<Suspense fallback={<Loading />}>
|
<Session />
|
||||||
<Session />
|
|
||||||
</Suspense>
|
|
||||||
</SessionProviders>
|
</SessionProviders>
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -84,9 +81,9 @@ declare global {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function MarkedProviderWithNativeParser(props: ParentProps) {
|
function QueryProvider(props: ParentProps) {
|
||||||
const platform = usePlatform()
|
const client = new QueryClient()
|
||||||
return <MarkedProvider nativeParser={platform.parseMarkdown}>{props.children}</MarkedProvider>
|
return <QueryClientProvider client={client}>{props.children}</QueryClientProvider>
|
||||||
}
|
}
|
||||||
|
|
||||||
function AppShellProviders(props: ParentProps) {
|
function AppShellProviders(props: ParentProps) {
|
||||||
@@ -124,13 +121,15 @@ function SessionProviders(props: ParentProps) {
|
|||||||
function RouterRoot(props: ParentProps<{ appChildren?: JSX.Element }>) {
|
function RouterRoot(props: ParentProps<{ appChildren?: JSX.Element }>) {
|
||||||
return (
|
return (
|
||||||
<AppShellProviders>
|
<AppShellProviders>
|
||||||
{props.appChildren}
|
<Suspense fallback={<Loading />}>
|
||||||
{props.children}
|
{props.appChildren}
|
||||||
|
{props.children}
|
||||||
|
</Suspense>
|
||||||
</AppShellProviders>
|
</AppShellProviders>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export function AppBaseProviders(props: ParentProps) {
|
export function AppBaseProviders(props: ParentProps<{ locale?: Locale }>) {
|
||||||
return (
|
return (
|
||||||
<MetaProvider>
|
<MetaProvider>
|
||||||
<Font />
|
<Font />
|
||||||
@@ -139,14 +138,16 @@ export function AppBaseProviders(props: ParentProps) {
|
|||||||
void window.api?.setTitlebar?.({ mode })
|
void window.api?.setTitlebar?.({ mode })
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<LanguageProvider>
|
<LanguageProvider locale={props.locale}>
|
||||||
<UiI18nBridge>
|
<UiI18nBridge>
|
||||||
<ErrorBoundary fallback={(error) => <ErrorPage error={error} />}>
|
<ErrorBoundary fallback={(error) => <ErrorPage error={error} />}>
|
||||||
<DialogProvider>
|
<QueryProvider>
|
||||||
<MarkedProviderWithNativeParser>
|
<DialogProvider>
|
||||||
<FileComponentProvider component={File}>{props.children}</FileComponentProvider>
|
<MarkedProvider>
|
||||||
</MarkedProviderWithNativeParser>
|
<FileComponentProvider component={File}>{props.children}</FileComponentProvider>
|
||||||
</DialogProvider>
|
</MarkedProvider>
|
||||||
|
</DialogProvider>
|
||||||
|
</QueryProvider>
|
||||||
</ErrorBoundary>
|
</ErrorBoundary>
|
||||||
</UiI18nBridge>
|
</UiI18nBridge>
|
||||||
</LanguageProvider>
|
</LanguageProvider>
|
||||||
@@ -182,7 +183,7 @@ function ConnectionGate(props: ParentProps<{ disableHealthCheck?: boolean }>) {
|
|||||||
}
|
}
|
||||||
}).pipe(
|
}).pipe(
|
||||||
effectMinDuration(checkMode() === "blocking" ? "1.2 seconds" : 0),
|
effectMinDuration(checkMode() === "blocking" ? "1.2 seconds" : 0),
|
||||||
Effect.timeoutOrElse({ duration: "10 seconds", onTimeout: () => Effect.succeed(false) }),
|
Effect.timeoutOrElse({ duration: "10 seconds", orElse: () => Effect.succeed(false) }),
|
||||||
Effect.ensuring(Effect.sync(() => setCheckMode("background"))),
|
Effect.ensuring(Effect.sync(() => setCheckMode("background"))),
|
||||||
Effect.runPromise,
|
Effect.runPromise,
|
||||||
),
|
),
|
||||||
@@ -265,6 +266,15 @@ function ConnectionError(props: { onRetry?: () => void; onServerSelected?: (key:
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function ServerKey(props: ParentProps) {
|
||||||
|
const server = useServer()
|
||||||
|
return (
|
||||||
|
<Show when={server.key} keyed>
|
||||||
|
{props.children}
|
||||||
|
</Show>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
export function AppInterface(props: {
|
export function AppInterface(props: {
|
||||||
children?: JSX.Element
|
children?: JSX.Element
|
||||||
defaultServer: ServerConnection.Key
|
defaultServer: ServerConnection.Key
|
||||||
@@ -273,22 +283,28 @@ export function AppInterface(props: {
|
|||||||
disableHealthCheck?: boolean
|
disableHealthCheck?: boolean
|
||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
<ServerProvider defaultServer={props.defaultServer} servers={props.servers}>
|
<ServerProvider
|
||||||
|
defaultServer={props.defaultServer}
|
||||||
|
disableHealthCheck={props.disableHealthCheck}
|
||||||
|
servers={props.servers}
|
||||||
|
>
|
||||||
<ConnectionGate disableHealthCheck={props.disableHealthCheck}>
|
<ConnectionGate disableHealthCheck={props.disableHealthCheck}>
|
||||||
<GlobalSDKProvider>
|
<ServerKey>
|
||||||
<GlobalSyncProvider>
|
<GlobalSDKProvider>
|
||||||
<Dynamic
|
<GlobalSyncProvider>
|
||||||
component={props.router ?? Router}
|
<Dynamic
|
||||||
root={(routerProps) => <RouterRoot appChildren={props.children}>{routerProps.children}</RouterRoot>}
|
component={props.router ?? Router}
|
||||||
>
|
root={(routerProps) => <RouterRoot appChildren={props.children}>{routerProps.children}</RouterRoot>}
|
||||||
<Route path="/" component={HomeRoute} />
|
>
|
||||||
<Route path="/:dir" component={DirectoryLayout}>
|
<Route path="/" component={HomeRoute} />
|
||||||
<Route path="/" component={SessionIndexRoute} />
|
<Route path="/:dir" component={DirectoryLayout}>
|
||||||
<Route path="/session/:id?" component={SessionRoute} />
|
<Route path="/" component={SessionIndexRoute} />
|
||||||
</Route>
|
<Route path="/session/:id?" component={SessionRoute} />
|
||||||
</Dynamic>
|
</Route>
|
||||||
</GlobalSyncProvider>
|
</Dynamic>
|
||||||
</GlobalSDKProvider>
|
</GlobalSyncProvider>
|
||||||
|
</GlobalSDKProvider>
|
||||||
|
</ServerKey>
|
||||||
</ConnectionGate>
|
</ConnectionGate>
|
||||||
</ServerProvider>
|
</ServerProvider>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -55,7 +55,7 @@ function Cell(props: { bad?: boolean; dim?: boolean; label: string; tip: string;
|
|||||||
<Tooltip value={props.tip} placement="top">
|
<Tooltip value={props.tip} placement="top">
|
||||||
<div
|
<div
|
||||||
classList={{
|
classList={{
|
||||||
"flex min-h-[42px] w-full min-w-0 flex-col items-center justify-center rounded-[8px] bg-white/5 px-0.5 py-1 text-center": true,
|
"flex min-h-[42px] w-full min-w-0 flex-col items-center justify-center rounded-[8px] px-0.5 py-1 text-center": true,
|
||||||
"col-span-2": !!props.wide,
|
"col-span-2": !!props.wide,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
@@ -363,11 +363,7 @@ export function DebugBar() {
|
|||||||
return (
|
return (
|
||||||
<aside
|
<aside
|
||||||
aria-label={language.t("debugBar.ariaLabel")}
|
aria-label={language.t("debugBar.ariaLabel")}
|
||||||
class="pointer-events-auto fixed bottom-3 right-3 z-50 w-[308px] max-w-[calc(100vw-1.5rem)] overflow-hidden rounded-xl border p-0.5 text-text-on-interactive-base shadow-[var(--shadow-lg-border-base)] sm:bottom-4 sm:right-4 sm:w-[324px]"
|
class="pointer-events-auto fixed bottom-3 right-3 z-50 w-[308px] max-w-[calc(100vw-1.5rem)] overflow-hidden rounded-xl border border-border-base bg-surface-raised-stronger-non-alpha p-0.5 text-text-strong shadow-[var(--shadow-lg-border-base)] sm:bottom-4 sm:right-4 sm:w-[324px]"
|
||||||
style={{
|
|
||||||
"background-color": "color-mix(in srgb, var(--icon-interactive-base) 42%, black)",
|
|
||||||
"border-color": "color-mix(in srgb, white 14%, transparent)",
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
<div class="grid grid-cols-5 gap-px font-mono">
|
<div class="grid grid-cols-5 gap-px font-mono">
|
||||||
<Cell
|
<Cell
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import type { ProviderAuthAuthorization } from "@opencode-ai/sdk/v2/client"
|
import type { ProviderAuthAuthorization, ProviderAuthMethod } from "@opencode-ai/sdk/v2/client"
|
||||||
import { Button } from "@opencode-ai/ui/button"
|
import { Button } from "@opencode-ai/ui/button"
|
||||||
import { useDialog } from "@opencode-ai/ui/context/dialog"
|
import { useDialog } from "@opencode-ai/ui/context/dialog"
|
||||||
import { Dialog } from "@opencode-ai/ui/dialog"
|
import { Dialog } from "@opencode-ai/ui/dialog"
|
||||||
@@ -9,22 +9,26 @@ import { ProviderIcon } from "@opencode-ai/ui/provider-icon"
|
|||||||
import { Spinner } from "@opencode-ai/ui/spinner"
|
import { Spinner } from "@opencode-ai/ui/spinner"
|
||||||
import { TextField } from "@opencode-ai/ui/text-field"
|
import { TextField } from "@opencode-ai/ui/text-field"
|
||||||
import { showToast } from "@opencode-ai/ui/toast"
|
import { showToast } from "@opencode-ai/ui/toast"
|
||||||
import { createMemo, Match, onCleanup, onMount, Switch } from "solid-js"
|
import { createEffect, createMemo, createResource, Match, onCleanup, onMount, Switch } from "solid-js"
|
||||||
import { createStore, produce } from "solid-js/store"
|
import { createStore, produce } from "solid-js/store"
|
||||||
import { Link } from "@/components/link"
|
import { Link } from "@/components/link"
|
||||||
import { useLanguage } from "@/context/language"
|
|
||||||
import { useGlobalSDK } from "@/context/global-sdk"
|
import { useGlobalSDK } from "@/context/global-sdk"
|
||||||
import { useGlobalSync } from "@/context/global-sync"
|
import { useGlobalSync } from "@/context/global-sync"
|
||||||
import { usePlatform } from "@/context/platform"
|
import { useLanguage } from "@/context/language"
|
||||||
import { DialogSelectModel } from "./dialog-select-model"
|
import { useProviders } from "@/hooks/use-providers"
|
||||||
import { DialogSelectProvider } from "./dialog-select-provider"
|
|
||||||
|
|
||||||
export function DialogConnectProvider(props: { provider: string }) {
|
export function DialogConnectProvider(props: { provider: string }) {
|
||||||
const dialog = useDialog()
|
const dialog = useDialog()
|
||||||
const globalSync = useGlobalSync()
|
const globalSync = useGlobalSync()
|
||||||
const globalSDK = useGlobalSDK()
|
const globalSDK = useGlobalSDK()
|
||||||
const platform = usePlatform()
|
|
||||||
const language = useLanguage()
|
const language = useLanguage()
|
||||||
|
const providers = useProviders()
|
||||||
|
|
||||||
|
const all = () => {
|
||||||
|
void import("./dialog-select-provider").then((x) => {
|
||||||
|
dialog.show(() => <x.DialogSelectProvider />)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
const alive = { value: true }
|
const alive = { value: true }
|
||||||
const timer = { current: undefined as ReturnType<typeof setTimeout> | undefined }
|
const timer = { current: undefined as ReturnType<typeof setTimeout> | undefined }
|
||||||
@@ -36,26 +40,41 @@ export function DialogConnectProvider(props: { provider: string }) {
|
|||||||
timer.current = undefined
|
timer.current = undefined
|
||||||
})
|
})
|
||||||
|
|
||||||
const provider = createMemo(() => globalSync.data.provider.all.find((x) => x.id === props.provider)!)
|
const provider = createMemo(
|
||||||
const methods = createMemo(
|
|
||||||
() =>
|
() =>
|
||||||
globalSync.data.provider_auth[props.provider] ?? [
|
providers.all().find((x) => x.id === props.provider) ??
|
||||||
{
|
globalSync.data.provider.all.find((x) => x.id === props.provider)!,
|
||||||
type: "api",
|
|
||||||
label: language.t("provider.connect.method.apiKey"),
|
|
||||||
},
|
|
||||||
],
|
|
||||||
)
|
)
|
||||||
|
const fallback = createMemo<ProviderAuthMethod[]>(() => [
|
||||||
|
{
|
||||||
|
type: "api" as const,
|
||||||
|
label: language.t("provider.connect.method.apiKey"),
|
||||||
|
},
|
||||||
|
])
|
||||||
|
const [auth] = createResource(
|
||||||
|
() => props.provider,
|
||||||
|
async () => {
|
||||||
|
const cached = globalSync.data.provider_auth[props.provider]
|
||||||
|
if (cached) return cached
|
||||||
|
const res = await globalSDK.client.provider.auth()
|
||||||
|
if (!alive.value) return fallback()
|
||||||
|
globalSync.set("provider_auth", res.data ?? {})
|
||||||
|
return res.data?.[props.provider] ?? fallback()
|
||||||
|
},
|
||||||
|
)
|
||||||
|
const loading = createMemo(() => auth.loading && !globalSync.data.provider_auth[props.provider])
|
||||||
|
const methods = createMemo(() => auth.latest ?? globalSync.data.provider_auth[props.provider] ?? fallback())
|
||||||
const [store, setStore] = createStore({
|
const [store, setStore] = createStore({
|
||||||
methodIndex: undefined as undefined | number,
|
methodIndex: undefined as undefined | number,
|
||||||
authorization: undefined as undefined | ProviderAuthAuthorization,
|
authorization: undefined as undefined | ProviderAuthAuthorization,
|
||||||
state: "pending" as undefined | "pending" | "complete" | "error",
|
state: "pending" as undefined | "pending" | "complete" | "error" | "prompt",
|
||||||
error: undefined as string | undefined,
|
error: undefined as string | undefined,
|
||||||
})
|
})
|
||||||
|
|
||||||
type Action =
|
type Action =
|
||||||
| { type: "method.select"; index: number }
|
| { type: "method.select"; index: number }
|
||||||
| { type: "method.reset" }
|
| { type: "method.reset" }
|
||||||
|
| { type: "auth.prompt" }
|
||||||
| { type: "auth.pending" }
|
| { type: "auth.pending" }
|
||||||
| { type: "auth.complete"; authorization: ProviderAuthAuthorization }
|
| { type: "auth.complete"; authorization: ProviderAuthAuthorization }
|
||||||
| { type: "auth.error"; error: string }
|
| { type: "auth.error"; error: string }
|
||||||
@@ -77,6 +96,11 @@ export function DialogConnectProvider(props: { provider: string }) {
|
|||||||
draft.error = undefined
|
draft.error = undefined
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
if (action.type === "auth.prompt") {
|
||||||
|
draft.state = "prompt"
|
||||||
|
draft.error = undefined
|
||||||
|
return
|
||||||
|
}
|
||||||
if (action.type === "auth.pending") {
|
if (action.type === "auth.pending") {
|
||||||
draft.state = "pending"
|
draft.state = "pending"
|
||||||
draft.error = undefined
|
draft.error = undefined
|
||||||
@@ -120,7 +144,7 @@ export function DialogConnectProvider(props: { provider: string }) {
|
|||||||
return fallback
|
return fallback
|
||||||
}
|
}
|
||||||
|
|
||||||
async function selectMethod(index: number) {
|
async function selectMethod(index: number, inputs?: Record<string, string>) {
|
||||||
if (timer.current !== undefined) {
|
if (timer.current !== undefined) {
|
||||||
clearTimeout(timer.current)
|
clearTimeout(timer.current)
|
||||||
timer.current = undefined
|
timer.current = undefined
|
||||||
@@ -130,6 +154,10 @@ export function DialogConnectProvider(props: { provider: string }) {
|
|||||||
dispatch({ type: "method.select", index })
|
dispatch({ type: "method.select", index })
|
||||||
|
|
||||||
if (method.type === "oauth") {
|
if (method.type === "oauth") {
|
||||||
|
if (method.prompts?.length && !inputs) {
|
||||||
|
dispatch({ type: "auth.prompt" })
|
||||||
|
return
|
||||||
|
}
|
||||||
dispatch({ type: "auth.pending" })
|
dispatch({ type: "auth.pending" })
|
||||||
const start = Date.now()
|
const start = Date.now()
|
||||||
await globalSDK.client.provider.oauth
|
await globalSDK.client.provider.oauth
|
||||||
@@ -137,6 +165,7 @@ export function DialogConnectProvider(props: { provider: string }) {
|
|||||||
{
|
{
|
||||||
providerID: props.provider,
|
providerID: props.provider,
|
||||||
method: index,
|
method: index,
|
||||||
|
inputs,
|
||||||
},
|
},
|
||||||
{ throwOnError: true },
|
{ throwOnError: true },
|
||||||
)
|
)
|
||||||
@@ -163,6 +192,126 @@ export function DialogConnectProvider(props: { provider: string }) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function OAuthPromptsView() {
|
||||||
|
const [formStore, setFormStore] = createStore({
|
||||||
|
value: {} as Record<string, string>,
|
||||||
|
index: 0,
|
||||||
|
})
|
||||||
|
|
||||||
|
const prompts = createMemo<NonNullable<ProviderAuthMethod["prompts"]>>(() => {
|
||||||
|
const value = method()
|
||||||
|
if (value?.type !== "oauth") return []
|
||||||
|
return value.prompts ?? []
|
||||||
|
})
|
||||||
|
const matches = (prompt: NonNullable<ReturnType<typeof prompts>[number]>, value: Record<string, string>) => {
|
||||||
|
if (!prompt.when) return true
|
||||||
|
const actual = value[prompt.when.key]
|
||||||
|
if (actual === undefined) return false
|
||||||
|
return prompt.when.op === "eq" ? actual === prompt.when.value : actual !== prompt.when.value
|
||||||
|
}
|
||||||
|
const current = createMemo(() => {
|
||||||
|
const all = prompts()
|
||||||
|
const index = all.findIndex((prompt, index) => index >= formStore.index && matches(prompt, formStore.value))
|
||||||
|
if (index === -1) return
|
||||||
|
return {
|
||||||
|
index,
|
||||||
|
prompt: all[index],
|
||||||
|
}
|
||||||
|
})
|
||||||
|
const valid = createMemo(() => {
|
||||||
|
const item = current()
|
||||||
|
if (!item || item.prompt.type !== "text") return false
|
||||||
|
const value = formStore.value[item.prompt.key] ?? ""
|
||||||
|
return value.trim().length > 0
|
||||||
|
})
|
||||||
|
|
||||||
|
async function next(index: number, value: Record<string, string>) {
|
||||||
|
if (store.methodIndex === undefined) return
|
||||||
|
const next = prompts().findIndex((prompt, i) => i > index && matches(prompt, value))
|
||||||
|
if (next !== -1) {
|
||||||
|
setFormStore("index", next)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
await selectMethod(store.methodIndex, value)
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleSubmit(e: SubmitEvent) {
|
||||||
|
e.preventDefault()
|
||||||
|
const item = current()
|
||||||
|
if (!item || item.prompt.type !== "text") return
|
||||||
|
if (!valid()) return
|
||||||
|
await next(item.index, formStore.value)
|
||||||
|
}
|
||||||
|
|
||||||
|
const item = () => current()
|
||||||
|
const text = createMemo(() => {
|
||||||
|
const prompt = item()?.prompt
|
||||||
|
if (!prompt || prompt.type !== "text") return
|
||||||
|
return prompt
|
||||||
|
})
|
||||||
|
const select = createMemo(() => {
|
||||||
|
const prompt = item()?.prompt
|
||||||
|
if (!prompt || prompt.type !== "select") return
|
||||||
|
return prompt
|
||||||
|
})
|
||||||
|
|
||||||
|
return (
|
||||||
|
<form onSubmit={handleSubmit} class="flex flex-col items-start gap-4">
|
||||||
|
<Switch>
|
||||||
|
<Match when={item()?.prompt.type === "text"}>
|
||||||
|
<TextField
|
||||||
|
type="text"
|
||||||
|
label={text()?.message ?? ""}
|
||||||
|
placeholder={text()?.placeholder}
|
||||||
|
value={text() ? (formStore.value[text()!.key] ?? "") : ""}
|
||||||
|
onChange={(value) => {
|
||||||
|
const prompt = text()
|
||||||
|
if (!prompt) return
|
||||||
|
setFormStore("value", prompt.key, value)
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<Button class="w-auto" type="submit" size="large" variant="primary" disabled={!valid()}>
|
||||||
|
{language.t("common.continue")}
|
||||||
|
</Button>
|
||||||
|
</Match>
|
||||||
|
<Match when={item()?.prompt.type === "select"}>
|
||||||
|
<div class="w-full flex flex-col gap-1.5">
|
||||||
|
<div class="text-14-regular text-text-base">{select()?.message}</div>
|
||||||
|
<div>
|
||||||
|
<List
|
||||||
|
items={select()?.options ?? []}
|
||||||
|
key={(x) => x.value}
|
||||||
|
current={select()?.options.find((x) => x.value === formStore.value[select()!.key])}
|
||||||
|
onSelect={(value) => {
|
||||||
|
if (!value) return
|
||||||
|
const prompt = select()
|
||||||
|
if (!prompt) return
|
||||||
|
const nextValue = {
|
||||||
|
...formStore.value,
|
||||||
|
[prompt.key]: value.value,
|
||||||
|
}
|
||||||
|
setFormStore("value", prompt.key, value.value)
|
||||||
|
void next(item()!.index, nextValue)
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{(option) => (
|
||||||
|
<div class="w-full flex items-center gap-x-2">
|
||||||
|
<div class="w-4 h-2 rounded-[1px] bg-input-base shadow-xs-border-base flex items-center justify-center">
|
||||||
|
<div class="w-2.5 h-0.5 ml-0 bg-icon-strong-base hidden" data-slot="list-item-extra-icon" />
|
||||||
|
</div>
|
||||||
|
<span>{option.label}</span>
|
||||||
|
<span class="text-14-regular text-text-weak">{option.hint}</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</List>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Match>
|
||||||
|
</Switch>
|
||||||
|
</form>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
let listRef: ListRef | undefined
|
let listRef: ListRef | undefined
|
||||||
function handleKey(e: KeyboardEvent) {
|
function handleKey(e: KeyboardEvent) {
|
||||||
if (e.key === "Enter" && e.target instanceof HTMLInputElement) {
|
if (e.key === "Enter" && e.target instanceof HTMLInputElement) {
|
||||||
@@ -172,8 +321,12 @@ export function DialogConnectProvider(props: { provider: string }) {
|
|||||||
listRef?.onKeyDown(e)
|
listRef?.onKeyDown(e)
|
||||||
}
|
}
|
||||||
|
|
||||||
onMount(() => {
|
let auto = false
|
||||||
|
createEffect(() => {
|
||||||
|
if (auto) return
|
||||||
|
if (loading()) return
|
||||||
if (methods().length === 1) {
|
if (methods().length === 1) {
|
||||||
|
auto = true
|
||||||
selectMethod(0)
|
selectMethod(0)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@@ -191,7 +344,7 @@ export function DialogConnectProvider(props: { provider: string }) {
|
|||||||
|
|
||||||
function goBack() {
|
function goBack() {
|
||||||
if (methods().length === 1) {
|
if (methods().length === 1) {
|
||||||
dialog.show(() => <DialogSelectProvider />)
|
all()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if (store.authorization) {
|
if (store.authorization) {
|
||||||
@@ -202,7 +355,7 @@ export function DialogConnectProvider(props: { provider: string }) {
|
|||||||
dispatch({ type: "method.reset" })
|
dispatch({ type: "method.reset" })
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
dialog.show(() => <DialogSelectProvider />)
|
all()
|
||||||
}
|
}
|
||||||
|
|
||||||
function MethodSelection() {
|
function MethodSelection() {
|
||||||
@@ -301,7 +454,7 @@ export function DialogConnectProvider(props: { provider: string }) {
|
|||||||
error={formStore.error}
|
error={formStore.error}
|
||||||
/>
|
/>
|
||||||
<Button class="w-auto" type="submit" size="large" variant="primary">
|
<Button class="w-auto" type="submit" size="large" variant="primary">
|
||||||
{language.t("common.submit")}
|
{language.t("common.continue")}
|
||||||
</Button>
|
</Button>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
@@ -314,12 +467,6 @@ export function DialogConnectProvider(props: { provider: string }) {
|
|||||||
error: undefined as string | undefined,
|
error: undefined as string | undefined,
|
||||||
})
|
})
|
||||||
|
|
||||||
onMount(() => {
|
|
||||||
if (store.authorization?.method === "code" && store.authorization?.url) {
|
|
||||||
platform.openLink(store.authorization.url)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
async function handleSubmit(e: SubmitEvent) {
|
async function handleSubmit(e: SubmitEvent) {
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
|
|
||||||
@@ -368,7 +515,7 @@ export function DialogConnectProvider(props: { provider: string }) {
|
|||||||
error={formStore.error}
|
error={formStore.error}
|
||||||
/>
|
/>
|
||||||
<Button class="w-auto" type="submit" size="large" variant="primary">
|
<Button class="w-auto" type="submit" size="large" variant="primary">
|
||||||
{language.t("common.submit")}
|
{language.t("common.continue")}
|
||||||
</Button>
|
</Button>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
@@ -386,10 +533,6 @@ export function DialogConnectProvider(props: { provider: string }) {
|
|||||||
|
|
||||||
onMount(() => {
|
onMount(() => {
|
||||||
void (async () => {
|
void (async () => {
|
||||||
if (store.authorization?.url) {
|
|
||||||
platform.openLink(store.authorization.url)
|
|
||||||
}
|
|
||||||
|
|
||||||
const result = await globalSDK.client.provider.oauth
|
const result = await globalSDK.client.provider.oauth
|
||||||
.callback({
|
.callback({
|
||||||
providerID: props.provider,
|
providerID: props.provider,
|
||||||
@@ -459,6 +602,14 @@ export function DialogConnectProvider(props: { provider: string }) {
|
|||||||
<div class="px-2.5 pb-10 flex flex-col gap-6">
|
<div class="px-2.5 pb-10 flex flex-col gap-6">
|
||||||
<div onKeyDown={handleKey} tabIndex={0} autofocus={store.methodIndex === undefined ? true : undefined}>
|
<div onKeyDown={handleKey} tabIndex={0} autofocus={store.methodIndex === undefined ? true : undefined}>
|
||||||
<Switch>
|
<Switch>
|
||||||
|
<Match when={loading()}>
|
||||||
|
<div class="text-14-regular text-text-base">
|
||||||
|
<div class="flex items-center gap-x-2">
|
||||||
|
<Spinner />
|
||||||
|
<span>{language.t("provider.connect.status.inProgress")}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Match>
|
||||||
<Match when={store.methodIndex === undefined}>
|
<Match when={store.methodIndex === undefined}>
|
||||||
<MethodSelection />
|
<MethodSelection />
|
||||||
</Match>
|
</Match>
|
||||||
@@ -470,6 +621,9 @@ export function DialogConnectProvider(props: { provider: string }) {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Match>
|
</Match>
|
||||||
|
<Match when={store.state === "prompt"}>
|
||||||
|
<OAuthPromptsView />
|
||||||
|
</Match>
|
||||||
<Match when={store.state === "error"}>
|
<Match when={store.state === "error"}>
|
||||||
<div class="text-14-regular text-text-base">
|
<div class="text-14-regular text-text-base">
|
||||||
<div class="flex items-center gap-x-2">
|
<div class="flex items-center gap-x-2">
|
||||||
|
|||||||
@@ -34,7 +34,6 @@ export type FormState = {
|
|||||||
apiKey: string
|
apiKey: string
|
||||||
models: ModelRow[]
|
models: ModelRow[]
|
||||||
headers: HeaderRow[]
|
headers: HeaderRow[]
|
||||||
saving: boolean
|
|
||||||
err: {
|
err: {
|
||||||
providerID?: string
|
providerID?: string
|
||||||
name?: string
|
name?: string
|
||||||
|
|||||||
@@ -16,7 +16,6 @@ describe("validateCustomProvider", () => {
|
|||||||
{ row: "h0", key: " X-Test ", value: " enabled ", err: {} },
|
{ row: "h0", key: " X-Test ", value: " enabled ", err: {} },
|
||||||
{ row: "h1", key: "", value: "", err: {} },
|
{ row: "h1", key: "", value: "", err: {} },
|
||||||
],
|
],
|
||||||
saving: false,
|
|
||||||
err: {},
|
err: {},
|
||||||
},
|
},
|
||||||
t,
|
t,
|
||||||
@@ -60,7 +59,6 @@ describe("validateCustomProvider", () => {
|
|||||||
{ row: "h0", key: "Authorization", value: "one", err: {} },
|
{ row: "h0", key: "Authorization", value: "one", err: {} },
|
||||||
{ row: "h1", key: "authorization", value: "two", err: {} },
|
{ row: "h1", key: "authorization", value: "two", err: {} },
|
||||||
],
|
],
|
||||||
saving: false,
|
|
||||||
err: {},
|
err: {},
|
||||||
},
|
},
|
||||||
t,
|
t,
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { useDialog } from "@opencode-ai/ui/context/dialog"
|
|||||||
import { Dialog } from "@opencode-ai/ui/dialog"
|
import { Dialog } from "@opencode-ai/ui/dialog"
|
||||||
import { IconButton } from "@opencode-ai/ui/icon-button"
|
import { IconButton } from "@opencode-ai/ui/icon-button"
|
||||||
import { ProviderIcon } from "@opencode-ai/ui/provider-icon"
|
import { ProviderIcon } from "@opencode-ai/ui/provider-icon"
|
||||||
|
import { useMutation } from "@tanstack/solid-query"
|
||||||
import { TextField } from "@opencode-ai/ui/text-field"
|
import { TextField } from "@opencode-ai/ui/text-field"
|
||||||
import { showToast } from "@opencode-ai/ui/toast"
|
import { showToast } from "@opencode-ai/ui/toast"
|
||||||
import { batch, For } from "solid-js"
|
import { batch, For } from "solid-js"
|
||||||
@@ -31,7 +32,6 @@ export function DialogCustomProvider(props: Props) {
|
|||||||
apiKey: "",
|
apiKey: "",
|
||||||
models: [modelRow()],
|
models: [modelRow()],
|
||||||
headers: [headerRow()],
|
headers: [headerRow()],
|
||||||
saving: false,
|
|
||||||
err: {},
|
err: {},
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -116,48 +116,49 @@ export function DialogCustomProvider(props: Props) {
|
|||||||
return output.result
|
return output.result
|
||||||
}
|
}
|
||||||
|
|
||||||
const save = async (e: SubmitEvent) => {
|
const saveMutation = useMutation(() => ({
|
||||||
e.preventDefault()
|
mutationFn: async (result: NonNullable<ReturnType<typeof validate>>) => {
|
||||||
if (form.saving) return
|
const disabledProviders = globalSync.data.config.disabled_providers ?? []
|
||||||
|
const nextDisabled = disabledProviders.filter((id) => id !== result.providerID)
|
||||||
|
|
||||||
const result = validate()
|
if (result.key) {
|
||||||
if (!result) return
|
await globalSDK.client.auth.set({
|
||||||
|
|
||||||
setForm("saving", true)
|
|
||||||
|
|
||||||
const disabledProviders = globalSync.data.config.disabled_providers ?? []
|
|
||||||
const nextDisabled = disabledProviders.filter((id) => id !== result.providerID)
|
|
||||||
|
|
||||||
const auth = result.key
|
|
||||||
? globalSDK.client.auth.set({
|
|
||||||
providerID: result.providerID,
|
providerID: result.providerID,
|
||||||
auth: {
|
auth: {
|
||||||
type: "api",
|
type: "api",
|
||||||
key: result.key,
|
key: result.key,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
: Promise.resolve()
|
}
|
||||||
|
|
||||||
auth
|
await globalSync.updateConfig({
|
||||||
.then(() =>
|
provider: { [result.providerID]: result.config },
|
||||||
globalSync.updateConfig({ provider: { [result.providerID]: result.config }, disabled_providers: nextDisabled }),
|
disabled_providers: nextDisabled,
|
||||||
)
|
|
||||||
.then(() => {
|
|
||||||
dialog.close()
|
|
||||||
showToast({
|
|
||||||
variant: "success",
|
|
||||||
icon: "circle-check",
|
|
||||||
title: language.t("provider.connect.toast.connected.title", { provider: result.name }),
|
|
||||||
description: language.t("provider.connect.toast.connected.description", { provider: result.name }),
|
|
||||||
})
|
|
||||||
})
|
})
|
||||||
.catch((err: unknown) => {
|
return result
|
||||||
const message = err instanceof Error ? err.message : String(err)
|
},
|
||||||
showToast({ title: language.t("common.requestFailed"), description: message })
|
onSuccess: (result) => {
|
||||||
})
|
dialog.close()
|
||||||
.finally(() => {
|
showToast({
|
||||||
setForm("saving", false)
|
variant: "success",
|
||||||
|
icon: "circle-check",
|
||||||
|
title: language.t("provider.connect.toast.connected.title", { provider: result.name }),
|
||||||
|
description: language.t("provider.connect.toast.connected.description", { provider: result.name }),
|
||||||
})
|
})
|
||||||
|
},
|
||||||
|
onError: (err) => {
|
||||||
|
const message = err instanceof Error ? err.message : String(err)
|
||||||
|
showToast({ title: language.t("common.requestFailed"), description: message })
|
||||||
|
},
|
||||||
|
}))
|
||||||
|
|
||||||
|
const save = (e: SubmitEvent) => {
|
||||||
|
e.preventDefault()
|
||||||
|
if (saveMutation.isPending) return
|
||||||
|
|
||||||
|
const result = validate()
|
||||||
|
if (!result) return
|
||||||
|
saveMutation.mutate(result)
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -312,8 +313,14 @@ export function DialogCustomProvider(props: Props) {
|
|||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Button class="w-auto self-start" type="submit" size="large" variant="primary" disabled={form.saving}>
|
<Button
|
||||||
{form.saving ? language.t("common.saving") : language.t("common.submit")}
|
class="w-auto self-start"
|
||||||
|
type="submit"
|
||||||
|
size="large"
|
||||||
|
variant="primary"
|
||||||
|
disabled={saveMutation.isPending}
|
||||||
|
>
|
||||||
|
{saveMutation.isPending ? language.t("common.saving") : language.t("common.submit")}
|
||||||
</Button>
|
</Button>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { Button } from "@opencode-ai/ui/button"
|
|||||||
import { useDialog } from "@opencode-ai/ui/context/dialog"
|
import { useDialog } from "@opencode-ai/ui/context/dialog"
|
||||||
import { Dialog } from "@opencode-ai/ui/dialog"
|
import { Dialog } from "@opencode-ai/ui/dialog"
|
||||||
import { TextField } from "@opencode-ai/ui/text-field"
|
import { TextField } from "@opencode-ai/ui/text-field"
|
||||||
|
import { useMutation } from "@tanstack/solid-query"
|
||||||
import { Icon } from "@opencode-ai/ui/icon"
|
import { Icon } from "@opencode-ai/ui/icon"
|
||||||
import { createMemo, For, Show } from "solid-js"
|
import { createMemo, For, Show } from "solid-js"
|
||||||
import { createStore } from "solid-js/store"
|
import { createStore } from "solid-js/store"
|
||||||
@@ -28,7 +29,6 @@ export function DialogEditProject(props: { project: LocalProject }) {
|
|||||||
color: props.project.icon?.color || "pink",
|
color: props.project.icon?.color || "pink",
|
||||||
iconUrl: props.project.icon?.override || "",
|
iconUrl: props.project.icon?.override || "",
|
||||||
startup: props.project.commands?.start ?? "",
|
startup: props.project.commands?.start ?? "",
|
||||||
saving: false,
|
|
||||||
dragOver: false,
|
dragOver: false,
|
||||||
iconHover: false,
|
iconHover: false,
|
||||||
})
|
})
|
||||||
@@ -71,38 +71,37 @@ export function DialogEditProject(props: { project: LocalProject }) {
|
|||||||
setStore("iconUrl", "")
|
setStore("iconUrl", "")
|
||||||
}
|
}
|
||||||
|
|
||||||
async function handleSubmit(e: SubmitEvent) {
|
const saveMutation = useMutation(() => ({
|
||||||
e.preventDefault()
|
mutationFn: async () => {
|
||||||
|
const name = store.name.trim() === folderName() ? "" : store.name.trim()
|
||||||
|
const start = store.startup.trim()
|
||||||
|
|
||||||
await Promise.resolve()
|
if (props.project.id && props.project.id !== "global") {
|
||||||
.then(async () => {
|
await globalSDK.client.project.update({
|
||||||
setStore("saving", true)
|
projectID: props.project.id,
|
||||||
const name = store.name.trim() === folderName() ? "" : store.name.trim()
|
directory: props.project.worktree,
|
||||||
const start = store.startup.trim()
|
|
||||||
|
|
||||||
if (props.project.id && props.project.id !== "global") {
|
|
||||||
await globalSDK.client.project.update({
|
|
||||||
projectID: props.project.id,
|
|
||||||
directory: props.project.worktree,
|
|
||||||
name,
|
|
||||||
icon: { color: store.color, override: store.iconUrl },
|
|
||||||
commands: { start },
|
|
||||||
})
|
|
||||||
globalSync.project.icon(props.project.worktree, store.iconUrl || undefined)
|
|
||||||
dialog.close()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
globalSync.project.meta(props.project.worktree, {
|
|
||||||
name,
|
name,
|
||||||
icon: { color: store.color, override: store.iconUrl || undefined },
|
icon: { color: store.color, override: store.iconUrl },
|
||||||
commands: { start: start || undefined },
|
commands: { start },
|
||||||
})
|
})
|
||||||
|
globalSync.project.icon(props.project.worktree, store.iconUrl || undefined)
|
||||||
dialog.close()
|
dialog.close()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
globalSync.project.meta(props.project.worktree, {
|
||||||
|
name,
|
||||||
|
icon: { color: store.color, override: store.iconUrl || undefined },
|
||||||
|
commands: { start: start || undefined },
|
||||||
})
|
})
|
||||||
.finally(() => {
|
dialog.close()
|
||||||
setStore("saving", false)
|
},
|
||||||
})
|
}))
|
||||||
|
|
||||||
|
function handleSubmit(e: SubmitEvent) {
|
||||||
|
e.preventDefault()
|
||||||
|
if (saveMutation.isPending) return
|
||||||
|
saveMutation.mutate()
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -246,8 +245,8 @@ export function DialogEditProject(props: { project: LocalProject }) {
|
|||||||
<Button type="button" variant="ghost" size="large" onClick={() => dialog.close()}>
|
<Button type="button" variant="ghost" size="large" onClick={() => dialog.close()}>
|
||||||
{language.t("common.cancel")}
|
{language.t("common.cancel")}
|
||||||
</Button>
|
</Button>
|
||||||
<Button type="submit" variant="primary" size="large" disabled={store.saving}>
|
<Button type="submit" variant="primary" size="large" disabled={saveMutation.isPending}>
|
||||||
{store.saving ? language.t("common.saving") : language.t("common.save")}
|
{saveMutation.isPending ? language.t("common.saving") : language.t("common.save")}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
|||||||
@@ -1,9 +1,12 @@
|
|||||||
import { Component, createMemo, createSignal, Show } from "solid-js"
|
import { useMutation } from "@tanstack/solid-query"
|
||||||
|
import { Component, createEffect, createMemo, on, Show } from "solid-js"
|
||||||
|
import { createStore } from "solid-js/store"
|
||||||
import { useSync } from "@/context/sync"
|
import { useSync } from "@/context/sync"
|
||||||
import { useSDK } from "@/context/sdk"
|
import { useSDK } from "@/context/sdk"
|
||||||
import { Dialog } from "@opencode-ai/ui/dialog"
|
import { Dialog } from "@opencode-ai/ui/dialog"
|
||||||
import { List } from "@opencode-ai/ui/list"
|
import { List } from "@opencode-ai/ui/list"
|
||||||
import { Switch } from "@opencode-ai/ui/switch"
|
import { Switch } from "@opencode-ai/ui/switch"
|
||||||
|
import { showToast } from "@opencode-ai/ui/toast"
|
||||||
import { useLanguage } from "@/context/language"
|
import { useLanguage } from "@/context/language"
|
||||||
|
|
||||||
const statusLabels = {
|
const statusLabels = {
|
||||||
@@ -17,7 +20,48 @@ export const DialogSelectMcp: Component = () => {
|
|||||||
const sync = useSync()
|
const sync = useSync()
|
||||||
const sdk = useSDK()
|
const sdk = useSDK()
|
||||||
const language = useLanguage()
|
const language = useLanguage()
|
||||||
const [loading, setLoading] = createSignal<string | null>(null)
|
const [state, setState] = createStore({
|
||||||
|
done: false,
|
||||||
|
loading: false,
|
||||||
|
})
|
||||||
|
|
||||||
|
createEffect(
|
||||||
|
on(
|
||||||
|
() => sync.data.mcp_ready,
|
||||||
|
(ready, prev) => {
|
||||||
|
if (!ready && prev) setState("done", false)
|
||||||
|
},
|
||||||
|
{ defer: true },
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
createEffect(() => {
|
||||||
|
if (state.done || state.loading) return
|
||||||
|
if (sync.data.mcp_ready) {
|
||||||
|
setState("done", true)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
setState("loading", true)
|
||||||
|
void sdk.client.mcp
|
||||||
|
.status()
|
||||||
|
.then((result) => {
|
||||||
|
sync.set("mcp", result.data ?? {})
|
||||||
|
sync.set("mcp_ready", true)
|
||||||
|
setState("done", true)
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
setState("done", true)
|
||||||
|
showToast({
|
||||||
|
variant: "error",
|
||||||
|
title: language.t("common.requestFailed"),
|
||||||
|
description: err instanceof Error ? err.message : String(err),
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
setState("loading", false)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
const items = createMemo(() =>
|
const items = createMemo(() =>
|
||||||
Object.entries(sync.data.mcp ?? {})
|
Object.entries(sync.data.mcp ?? {})
|
||||||
@@ -25,10 +69,8 @@ export const DialogSelectMcp: Component = () => {
|
|||||||
.sort((a, b) => a.name.localeCompare(b.name)),
|
.sort((a, b) => a.name.localeCompare(b.name)),
|
||||||
)
|
)
|
||||||
|
|
||||||
const toggle = async (name: string) => {
|
const toggle = useMutation(() => ({
|
||||||
if (loading()) return
|
mutationFn: async (name: string) => {
|
||||||
setLoading(name)
|
|
||||||
try {
|
|
||||||
const status = sync.data.mcp[name]
|
const status = sync.data.mcp[name]
|
||||||
if (status?.status === "connected") {
|
if (status?.status === "connected") {
|
||||||
await sdk.client.mcp.disconnect({ name })
|
await sdk.client.mcp.disconnect({ name })
|
||||||
@@ -38,10 +80,8 @@ export const DialogSelectMcp: Component = () => {
|
|||||||
|
|
||||||
const result = await sdk.client.mcp.status()
|
const result = await sdk.client.mcp.status()
|
||||||
if (result.data) sync.set("mcp", result.data)
|
if (result.data) sync.set("mcp", result.data)
|
||||||
} finally {
|
},
|
||||||
setLoading(null)
|
}))
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const enabledCount = createMemo(() => items().filter((i) => i.status === "connected").length)
|
const enabledCount = createMemo(() => items().filter((i) => i.status === "connected").length)
|
||||||
const totalCount = createMemo(() => items().length)
|
const totalCount = createMemo(() => items().length)
|
||||||
@@ -59,7 +99,8 @@ export const DialogSelectMcp: Component = () => {
|
|||||||
filterKeys={["name", "status"]}
|
filterKeys={["name", "status"]}
|
||||||
sortBy={(a, b) => a.name.localeCompare(b.name)}
|
sortBy={(a, b) => a.name.localeCompare(b.name)}
|
||||||
onSelect={(x) => {
|
onSelect={(x) => {
|
||||||
if (x) toggle(x.name)
|
if (!x || toggle.isPending) return
|
||||||
|
toggle.mutate(x.name)
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{(i) => {
|
{(i) => {
|
||||||
@@ -83,7 +124,7 @@ export const DialogSelectMcp: Component = () => {
|
|||||||
<Show when={statusLabel()}>
|
<Show when={statusLabel()}>
|
||||||
<span class="text-11-regular text-text-weaker">{statusLabel()}</span>
|
<span class="text-11-regular text-text-weaker">{statusLabel()}</span>
|
||||||
</Show>
|
</Show>
|
||||||
<Show when={loading() === i.name}>
|
<Show when={toggle.isPending && toggle.variables === i.name}>
|
||||||
<span class="text-11-regular text-text-weak">{language.t("common.loading.ellipsis")}</span>
|
<span class="text-11-regular text-text-weak">{language.t("common.loading.ellipsis")}</span>
|
||||||
</Show>
|
</Show>
|
||||||
</div>
|
</div>
|
||||||
@@ -92,7 +133,14 @@ export const DialogSelectMcp: Component = () => {
|
|||||||
</Show>
|
</Show>
|
||||||
</div>
|
</div>
|
||||||
<div onClick={(e) => e.stopPropagation()}>
|
<div onClick={(e) => e.stopPropagation()}>
|
||||||
<Switch checked={enabled()} disabled={loading() === i.name} onChange={() => toggle(i.name)} />
|
<Switch
|
||||||
|
checked={enabled()}
|
||||||
|
disabled={toggle.isPending && toggle.variables === i.name}
|
||||||
|
onChange={() => {
|
||||||
|
if (toggle.isPending) return
|
||||||
|
toggle.mutate(i.name)
|
||||||
|
}}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -8,8 +8,6 @@ import { Tooltip } from "@opencode-ai/ui/tooltip"
|
|||||||
import { type Component, Show } from "solid-js"
|
import { type Component, Show } from "solid-js"
|
||||||
import { useLocal } from "@/context/local"
|
import { useLocal } from "@/context/local"
|
||||||
import { popularProviders, useProviders } from "@/hooks/use-providers"
|
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 { ModelTooltip } from "./model-tooltip"
|
||||||
import { useLanguage } from "@/context/language"
|
import { useLanguage } from "@/context/language"
|
||||||
|
|
||||||
@@ -21,6 +19,18 @@ export const DialogSelectModelUnpaid: Component<{ model?: ModelState }> = (props
|
|||||||
const providers = useProviders()
|
const providers = useProviders()
|
||||||
const language = useLanguage()
|
const language = useLanguage()
|
||||||
|
|
||||||
|
const connect = (provider: string) => {
|
||||||
|
void import("./dialog-connect-provider").then((x) => {
|
||||||
|
dialog.show(() => <x.DialogConnectProvider provider={provider} />)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const all = () => {
|
||||||
|
void import("./dialog-select-provider").then((x) => {
|
||||||
|
dialog.show(() => <x.DialogSelectProvider />)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
let listRef: ListRef | undefined
|
let listRef: ListRef | undefined
|
||||||
const handleKeyDown = (e: KeyboardEvent) => {
|
const handleKeyDown = (e: KeyboardEvent) => {
|
||||||
if (e.key === "Escape") return
|
if (e.key === "Escape") return
|
||||||
@@ -91,7 +101,7 @@ export const DialogSelectModelUnpaid: Component<{ model?: ModelState }> = (props
|
|||||||
}}
|
}}
|
||||||
onSelect={(x) => {
|
onSelect={(x) => {
|
||||||
if (!x) return
|
if (!x) return
|
||||||
dialog.show(() => <DialogConnectProvider provider={x.id} />)
|
connect(x.id)
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{(i) => (
|
{(i) => (
|
||||||
@@ -122,9 +132,7 @@ export const DialogSelectModelUnpaid: Component<{ model?: ModelState }> = (props
|
|||||||
variant="ghost"
|
variant="ghost"
|
||||||
class="w-full justify-start px-[11px] py-3.5 gap-4.5 text-14-medium"
|
class="w-full justify-start px-[11px] py-3.5 gap-4.5 text-14-medium"
|
||||||
icon="dot-grid"
|
icon="dot-grid"
|
||||||
onClick={() => {
|
onClick={all}
|
||||||
dialog.show(() => <DialogSelectProvider />)
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
{language.t("dialog.provider.viewAll")}
|
{language.t("dialog.provider.viewAll")}
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
@@ -10,8 +10,6 @@ import { Tag } from "@opencode-ai/ui/tag"
|
|||||||
import { Dialog } from "@opencode-ai/ui/dialog"
|
import { Dialog } from "@opencode-ai/ui/dialog"
|
||||||
import { List } from "@opencode-ai/ui/list"
|
import { List } from "@opencode-ai/ui/list"
|
||||||
import { Tooltip } from "@opencode-ai/ui/tooltip"
|
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 { ModelTooltip } from "./model-tooltip"
|
||||||
import { useLanguage } from "@/context/language"
|
import { useLanguage } from "@/context/language"
|
||||||
|
|
||||||
@@ -107,12 +105,16 @@ export function ModelSelectorPopover(props: {
|
|||||||
|
|
||||||
const handleManage = () => {
|
const handleManage = () => {
|
||||||
setStore("open", false)
|
setStore("open", false)
|
||||||
dialog.show(() => <DialogManageModels />)
|
void import("./dialog-manage-models").then((x) => {
|
||||||
|
dialog.show(() => <x.DialogManageModels />)
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleConnectProvider = () => {
|
const handleConnectProvider = () => {
|
||||||
setStore("open", false)
|
setStore("open", false)
|
||||||
dialog.show(() => <DialogSelectProvider />)
|
void import("./dialog-select-provider").then((x) => {
|
||||||
|
dialog.show(() => <x.DialogSelectProvider />)
|
||||||
|
})
|
||||||
}
|
}
|
||||||
const language = useLanguage()
|
const language = useLanguage()
|
||||||
|
|
||||||
@@ -193,26 +195,29 @@ export const DialogSelectModel: Component<{ provider?: string; model?: ModelStat
|
|||||||
const dialog = useDialog()
|
const dialog = useDialog()
|
||||||
const language = useLanguage()
|
const language = useLanguage()
|
||||||
|
|
||||||
|
const provider = () => {
|
||||||
|
void import("./dialog-select-provider").then((x) => {
|
||||||
|
dialog.show(() => <x.DialogSelectProvider />)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const manage = () => {
|
||||||
|
void import("./dialog-manage-models").then((x) => {
|
||||||
|
dialog.show(() => <x.DialogManageModels />)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog
|
<Dialog
|
||||||
title={language.t("dialog.model.select.title")}
|
title={language.t("dialog.model.select.title")}
|
||||||
action={
|
action={
|
||||||
<Button
|
<Button class="h-7 -my-1 text-14-medium" icon="plus-small" tabIndex={-1} onClick={provider}>
|
||||||
class="h-7 -my-1 text-14-medium"
|
|
||||||
icon="plus-small"
|
|
||||||
tabIndex={-1}
|
|
||||||
onClick={() => dialog.show(() => <DialogSelectProvider />)}
|
|
||||||
>
|
|
||||||
{language.t("command.provider.connect")}
|
{language.t("command.provider.connect")}
|
||||||
</Button>
|
</Button>
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<ModelList provider={props.provider} model={props.model} onSelect={() => dialog.close()} />
|
<ModelList provider={props.provider} model={props.model} onSelect={() => dialog.close()} />
|
||||||
<Button
|
<Button variant="ghost" class="ml-3 mt-5 mb-6 text-text-base self-start" onClick={manage}>
|
||||||
variant="ghost"
|
|
||||||
class="ml-3 mt-5 mb-6 text-text-base self-start"
|
|
||||||
onClick={() => dialog.show(() => <DialogManageModels />)}
|
|
||||||
>
|
|
||||||
{language.t("dialog.model.manage")}
|
{language.t("dialog.model.manage")}
|
||||||
</Button>
|
</Button>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import { Icon } from "@opencode-ai/ui/icon"
|
|||||||
import { IconButton } from "@opencode-ai/ui/icon-button"
|
import { IconButton } from "@opencode-ai/ui/icon-button"
|
||||||
import { List } from "@opencode-ai/ui/list"
|
import { List } from "@opencode-ai/ui/list"
|
||||||
import { TextField } from "@opencode-ai/ui/text-field"
|
import { TextField } from "@opencode-ai/ui/text-field"
|
||||||
|
import { useMutation } from "@tanstack/solid-query"
|
||||||
import { showToast } from "@opencode-ai/ui/toast"
|
import { showToast } from "@opencode-ai/ui/toast"
|
||||||
import { useNavigate } from "@solidjs/router"
|
import { useNavigate } from "@solidjs/router"
|
||||||
import { createEffect, createMemo, createResource, onCleanup, Show } from "solid-js"
|
import { createEffect, createMemo, createResource, onCleanup, Show } from "solid-js"
|
||||||
@@ -186,7 +187,6 @@ export function DialogSelectServer() {
|
|||||||
name: "",
|
name: "",
|
||||||
username: DEFAULT_USERNAME,
|
username: DEFAULT_USERNAME,
|
||||||
password: "",
|
password: "",
|
||||||
adding: false,
|
|
||||||
error: "",
|
error: "",
|
||||||
showForm: false,
|
showForm: false,
|
||||||
status: undefined as boolean | undefined,
|
status: undefined as boolean | undefined,
|
||||||
@@ -198,7 +198,6 @@ export function DialogSelectServer() {
|
|||||||
username: "",
|
username: "",
|
||||||
password: "",
|
password: "",
|
||||||
error: "",
|
error: "",
|
||||||
busy: false,
|
|
||||||
status: undefined as boolean | undefined,
|
status: undefined as boolean | undefined,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
@@ -209,7 +208,6 @@ export function DialogSelectServer() {
|
|||||||
name: "",
|
name: "",
|
||||||
username: DEFAULT_USERNAME,
|
username: DEFAULT_USERNAME,
|
||||||
password: "",
|
password: "",
|
||||||
adding: false,
|
|
||||||
error: "",
|
error: "",
|
||||||
showForm: false,
|
showForm: false,
|
||||||
status: undefined,
|
status: undefined,
|
||||||
@@ -224,10 +222,78 @@ export function DialogSelectServer() {
|
|||||||
password: "",
|
password: "",
|
||||||
error: "",
|
error: "",
|
||||||
status: undefined,
|
status: undefined,
|
||||||
busy: false,
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const addMutation = useMutation(() => ({
|
||||||
|
mutationFn: async (value: string) => {
|
||||||
|
const normalized = normalizeServerUrl(value)
|
||||||
|
if (!normalized) {
|
||||||
|
resetAdd()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const conn: ServerConnection.Http = {
|
||||||
|
type: "http",
|
||||||
|
http: { url: normalized },
|
||||||
|
}
|
||||||
|
if (store.addServer.name.trim()) conn.displayName = store.addServer.name.trim()
|
||||||
|
if (store.addServer.password) conn.http.password = store.addServer.password
|
||||||
|
if (store.addServer.password && store.addServer.username) conn.http.username = store.addServer.username
|
||||||
|
const result = await checkServerHealth(conn.http)
|
||||||
|
if (!result.healthy) {
|
||||||
|
setStore("addServer", { error: language.t("dialog.server.add.error") })
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
resetAdd()
|
||||||
|
await select(conn, true)
|
||||||
|
},
|
||||||
|
}))
|
||||||
|
|
||||||
|
const editMutation = useMutation(() => ({
|
||||||
|
mutationFn: async (input: { original: ServerConnection.Any; value: string }) => {
|
||||||
|
if (input.original.type !== "http") return
|
||||||
|
const normalized = normalizeServerUrl(input.value)
|
||||||
|
if (!normalized) {
|
||||||
|
resetEdit()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const name = store.editServer.name.trim() || undefined
|
||||||
|
const username = store.editServer.username || undefined
|
||||||
|
const password = store.editServer.password || undefined
|
||||||
|
const existingName = input.original.displayName
|
||||||
|
if (
|
||||||
|
normalized === input.original.http.url &&
|
||||||
|
name === existingName &&
|
||||||
|
username === input.original.http.username &&
|
||||||
|
password === input.original.http.password
|
||||||
|
) {
|
||||||
|
resetEdit()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const conn: ServerConnection.Http = {
|
||||||
|
type: "http",
|
||||||
|
displayName: name,
|
||||||
|
http: { url: normalized, username, password },
|
||||||
|
}
|
||||||
|
const result = await checkServerHealth(conn.http)
|
||||||
|
if (!result.healthy) {
|
||||||
|
setStore("editServer", { error: language.t("dialog.server.add.error") })
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (normalized === input.original.http.url) {
|
||||||
|
server.add(conn)
|
||||||
|
} else {
|
||||||
|
replaceServer(input.original, conn)
|
||||||
|
}
|
||||||
|
|
||||||
|
resetEdit()
|
||||||
|
},
|
||||||
|
}))
|
||||||
|
|
||||||
const replaceServer = (original: ServerConnection.Http, next: ServerConnection.Http) => {
|
const replaceServer = (original: ServerConnection.Http, next: ServerConnection.Http) => {
|
||||||
const active = server.key
|
const active = server.key
|
||||||
const newConn = server.add(next)
|
const newConn = server.add(next)
|
||||||
@@ -291,12 +357,12 @@ export function DialogSelectServer() {
|
|||||||
navigate("/")
|
navigate("/")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
server.setActive(ServerConnection.key(conn))
|
|
||||||
navigate("/")
|
navigate("/")
|
||||||
|
queueMicrotask(() => server.setActive(ServerConnection.key(conn)))
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleAddChange = (value: string) => {
|
const handleAddChange = (value: string) => {
|
||||||
if (store.addServer.adding) return
|
if (addMutation.isPending) return
|
||||||
setStore("addServer", { url: value, error: "" })
|
setStore("addServer", { url: value, error: "" })
|
||||||
void previewStatus(value, store.addServer.username, store.addServer.password, (next) =>
|
void previewStatus(value, store.addServer.username, store.addServer.password, (next) =>
|
||||||
setStore("addServer", { status: next }),
|
setStore("addServer", { status: next }),
|
||||||
@@ -304,12 +370,12 @@ export function DialogSelectServer() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const handleAddNameChange = (value: string) => {
|
const handleAddNameChange = (value: string) => {
|
||||||
if (store.addServer.adding) return
|
if (addMutation.isPending) return
|
||||||
setStore("addServer", { name: value, error: "" })
|
setStore("addServer", { name: value, error: "" })
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleAddUsernameChange = (value: string) => {
|
const handleAddUsernameChange = (value: string) => {
|
||||||
if (store.addServer.adding) return
|
if (addMutation.isPending) return
|
||||||
setStore("addServer", { username: value, error: "" })
|
setStore("addServer", { username: value, error: "" })
|
||||||
void previewStatus(store.addServer.url, value, store.addServer.password, (next) =>
|
void previewStatus(store.addServer.url, value, store.addServer.password, (next) =>
|
||||||
setStore("addServer", { status: next }),
|
setStore("addServer", { status: next }),
|
||||||
@@ -317,7 +383,7 @@ export function DialogSelectServer() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const handleAddPasswordChange = (value: string) => {
|
const handleAddPasswordChange = (value: string) => {
|
||||||
if (store.addServer.adding) return
|
if (addMutation.isPending) return
|
||||||
setStore("addServer", { password: value, error: "" })
|
setStore("addServer", { password: value, error: "" })
|
||||||
void previewStatus(store.addServer.url, store.addServer.username, value, (next) =>
|
void previewStatus(store.addServer.url, store.addServer.username, value, (next) =>
|
||||||
setStore("addServer", { status: next }),
|
setStore("addServer", { status: next }),
|
||||||
@@ -325,7 +391,7 @@ export function DialogSelectServer() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const handleEditChange = (value: string) => {
|
const handleEditChange = (value: string) => {
|
||||||
if (store.editServer.busy) return
|
if (editMutation.isPending) return
|
||||||
setStore("editServer", { value, error: "" })
|
setStore("editServer", { value, error: "" })
|
||||||
void previewStatus(value, store.editServer.username, store.editServer.password, (next) =>
|
void previewStatus(value, store.editServer.username, store.editServer.password, (next) =>
|
||||||
setStore("editServer", { status: next }),
|
setStore("editServer", { status: next }),
|
||||||
@@ -333,12 +399,12 @@ export function DialogSelectServer() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const handleEditNameChange = (value: string) => {
|
const handleEditNameChange = (value: string) => {
|
||||||
if (store.editServer.busy) return
|
if (editMutation.isPending) return
|
||||||
setStore("editServer", { name: value, error: "" })
|
setStore("editServer", { name: value, error: "" })
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleEditUsernameChange = (value: string) => {
|
const handleEditUsernameChange = (value: string) => {
|
||||||
if (store.editServer.busy) return
|
if (editMutation.isPending) return
|
||||||
setStore("editServer", { username: value, error: "" })
|
setStore("editServer", { username: value, error: "" })
|
||||||
void previewStatus(store.editServer.value, value, store.editServer.password, (next) =>
|
void previewStatus(store.editServer.value, value, store.editServer.password, (next) =>
|
||||||
setStore("editServer", { status: next }),
|
setStore("editServer", { status: next }),
|
||||||
@@ -346,85 +412,13 @@ export function DialogSelectServer() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const handleEditPasswordChange = (value: string) => {
|
const handleEditPasswordChange = (value: string) => {
|
||||||
if (store.editServer.busy) return
|
if (editMutation.isPending) return
|
||||||
setStore("editServer", { password: value, error: "" })
|
setStore("editServer", { password: value, error: "" })
|
||||||
void previewStatus(store.editServer.value, store.editServer.username, value, (next) =>
|
void previewStatus(store.editServer.value, store.editServer.username, value, (next) =>
|
||||||
setStore("editServer", { status: next }),
|
setStore("editServer", { status: next }),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
async function handleAdd(value: string) {
|
|
||||||
if (store.addServer.adding) return
|
|
||||||
const normalized = normalizeServerUrl(value)
|
|
||||||
if (!normalized) {
|
|
||||||
resetAdd()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
setStore("addServer", { adding: true, error: "" })
|
|
||||||
|
|
||||||
const conn: ServerConnection.Http = {
|
|
||||||
type: "http",
|
|
||||||
http: { url: normalized },
|
|
||||||
}
|
|
||||||
if (store.addServer.name.trim()) conn.displayName = store.addServer.name.trim()
|
|
||||||
if (store.addServer.password) conn.http.password = store.addServer.password
|
|
||||||
if (store.addServer.password && store.addServer.username) conn.http.username = store.addServer.username
|
|
||||||
const result = await checkServerHealth(conn.http)
|
|
||||||
setStore("addServer", { adding: false })
|
|
||||||
if (!result.healthy) {
|
|
||||||
setStore("addServer", { error: language.t("dialog.server.add.error") })
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
resetAdd()
|
|
||||||
await select(conn, true)
|
|
||||||
}
|
|
||||||
|
|
||||||
async function handleEdit(original: ServerConnection.Any, value: string) {
|
|
||||||
if (store.editServer.busy || original.type !== "http") return
|
|
||||||
const normalized = normalizeServerUrl(value)
|
|
||||||
if (!normalized) {
|
|
||||||
resetEdit()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
const name = store.editServer.name.trim() || undefined
|
|
||||||
const username = store.editServer.username || undefined
|
|
||||||
const password = store.editServer.password || undefined
|
|
||||||
const existingName = original.displayName
|
|
||||||
if (
|
|
||||||
normalized === original.http.url &&
|
|
||||||
name === existingName &&
|
|
||||||
username === original.http.username &&
|
|
||||||
password === original.http.password
|
|
||||||
) {
|
|
||||||
resetEdit()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
setStore("editServer", { busy: true, error: "" })
|
|
||||||
|
|
||||||
const conn: ServerConnection.Http = {
|
|
||||||
type: "http",
|
|
||||||
displayName: name,
|
|
||||||
http: { url: normalized, username, password },
|
|
||||||
}
|
|
||||||
const result = await checkServerHealth(conn.http)
|
|
||||||
setStore("editServer", { busy: false })
|
|
||||||
if (!result.healthy) {
|
|
||||||
setStore("editServer", { error: language.t("dialog.server.add.error") })
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if (normalized === original.http.url) {
|
|
||||||
server.add(conn)
|
|
||||||
} else {
|
|
||||||
replaceServer(original, conn)
|
|
||||||
}
|
|
||||||
|
|
||||||
resetEdit()
|
|
||||||
}
|
|
||||||
|
|
||||||
const mode = createMemo<"list" | "add" | "edit">(() => {
|
const mode = createMemo<"list" | "add" | "edit">(() => {
|
||||||
if (store.editServer.id) return "edit"
|
if (store.editServer.id) return "edit"
|
||||||
if (store.addServer.showForm) return "add"
|
if (store.addServer.showForm) return "add"
|
||||||
@@ -464,23 +458,26 @@ export function DialogSelectServer() {
|
|||||||
password: conn.http.password ?? "",
|
password: conn.http.password ?? "",
|
||||||
error: "",
|
error: "",
|
||||||
status: store.status[ServerConnection.key(conn)]?.healthy,
|
status: store.status[ServerConnection.key(conn)]?.healthy,
|
||||||
busy: false,
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
const submitForm = () => {
|
const submitForm = () => {
|
||||||
if (mode() === "add") {
|
if (mode() === "add") {
|
||||||
void handleAdd(store.addServer.url)
|
if (addMutation.isPending) return
|
||||||
|
setStore("addServer", { error: "" })
|
||||||
|
addMutation.mutate(store.addServer.url)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
const original = editing()
|
const original = editing()
|
||||||
if (!original) return
|
if (!original) return
|
||||||
void handleEdit(original, store.editServer.value)
|
if (editMutation.isPending) return
|
||||||
|
setStore("editServer", { error: "" })
|
||||||
|
editMutation.mutate({ original, value: store.editServer.value })
|
||||||
}
|
}
|
||||||
|
|
||||||
const isFormMode = createMemo(() => mode() !== "list")
|
const isFormMode = createMemo(() => mode() !== "list")
|
||||||
const isAddMode = createMemo(() => mode() === "add")
|
const isAddMode = createMemo(() => mode() === "add")
|
||||||
const formBusy = createMemo(() => (isAddMode() ? store.addServer.adding : store.editServer.busy))
|
const formBusy = createMemo(() => (isAddMode() ? addMutation.isPending : editMutation.isPending))
|
||||||
|
|
||||||
const formTitle = createMemo(() => {
|
const formTitle = createMemo(() => {
|
||||||
if (!isFormMode()) return language.t("dialog.server.title")
|
if (!isFormMode()) return language.t("dialog.server.title")
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { useFilteredList } from "@opencode-ai/ui/hooks"
|
import { useFilteredList } from "@opencode-ai/ui/hooks"
|
||||||
import { useSpring } from "@opencode-ai/ui/motion-spring"
|
import { useSpring } from "@opencode-ai/ui/motion-spring"
|
||||||
import { createEffect, on, Component, Show, onCleanup, Switch, Match, createMemo, createSignal } from "solid-js"
|
import { createEffect, on, Component, Show, onCleanup, createMemo, createSignal } from "solid-js"
|
||||||
import { createStore } from "solid-js/store"
|
import { createStore } from "solid-js/store"
|
||||||
import { useLocal } from "@/context/local"
|
import { useLocal } from "@/context/local"
|
||||||
import { selectionFromLines, type SelectedLineRange, useFile } from "@/context/file"
|
import { selectionFromLines, type SelectedLineRange, useFile } from "@/context/file"
|
||||||
@@ -27,7 +27,6 @@ import { IconButton } from "@opencode-ai/ui/icon-button"
|
|||||||
import { Select } from "@opencode-ai/ui/select"
|
import { Select } from "@opencode-ai/ui/select"
|
||||||
import { useDialog } from "@opencode-ai/ui/context/dialog"
|
import { useDialog } from "@opencode-ai/ui/context/dialog"
|
||||||
import { ModelSelectorPopover } from "@/components/dialog-select-model"
|
import { ModelSelectorPopover } from "@/components/dialog-select-model"
|
||||||
import { DialogSelectModelUnpaid } from "@/components/dialog-select-model-unpaid"
|
|
||||||
import { useProviders } from "@/hooks/use-providers"
|
import { useProviders } from "@/hooks/use-providers"
|
||||||
import { useCommand } from "@/context/command"
|
import { useCommand } from "@/context/command"
|
||||||
import { Persist, persisted } from "@/utils/persist"
|
import { Persist, persisted } from "@/utils/persist"
|
||||||
@@ -36,6 +35,7 @@ import { useLanguage } from "@/context/language"
|
|||||||
import { usePlatform } from "@/context/platform"
|
import { usePlatform } from "@/context/platform"
|
||||||
import { useSessionLayout } from "@/pages/session/session-layout"
|
import { useSessionLayout } from "@/pages/session/session-layout"
|
||||||
import { createSessionTabs } from "@/pages/session/helpers"
|
import { createSessionTabs } from "@/pages/session/helpers"
|
||||||
|
import { promptEnabled, promptProbe } from "@/testing/prompt"
|
||||||
import { createTextFragment, getCursorPosition, setCursorPosition, setRangeEdge } from "./prompt-input/editor-dom"
|
import { createTextFragment, getCursorPosition, setCursorPosition, setRangeEdge } from "./prompt-input/editor-dom"
|
||||||
import { createPromptAttachments } from "./prompt-input/attachments"
|
import { createPromptAttachments } from "./prompt-input/attachments"
|
||||||
import { ACCEPTED_FILE_TYPES } from "./prompt-input/files"
|
import { ACCEPTED_FILE_TYPES } from "./prompt-input/files"
|
||||||
@@ -243,6 +243,23 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
|
|||||||
},
|
},
|
||||||
)
|
)
|
||||||
const working = createMemo(() => status()?.type !== "idle")
|
const working = createMemo(() => status()?.type !== "idle")
|
||||||
|
const tip = () => {
|
||||||
|
if (working()) {
|
||||||
|
return (
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<span>{language.t("prompt.action.stop")}</span>
|
||||||
|
<span class="text-icon-base text-12-medium text-[10px]!">{language.t("common.key.esc")}</span>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<span>{language.t("prompt.action.send")}</span>
|
||||||
|
<Icon name="enter" size="small" class="text-icon-base" />
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
const imageAttachments = createMemo(() =>
|
const imageAttachments = createMemo(() =>
|
||||||
prompt.current().filter((part): part is ImageAttachmentPart => part.type === "image"),
|
prompt.current().filter((part): part is ImageAttachmentPart => part.type === "image"),
|
||||||
)
|
)
|
||||||
@@ -554,6 +571,7 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
|
|||||||
const open = recent()
|
const open = recent()
|
||||||
const seen = new Set(open)
|
const seen = new Set(open)
|
||||||
const pinned: AtOption[] = open.map((path) => ({ type: "file", path, display: path, recent: true }))
|
const pinned: AtOption[] = open.map((path) => ({ type: "file", path, display: path, recent: true }))
|
||||||
|
if (!query.trim()) return [...agents, ...pinned]
|
||||||
const paths = await files.searchFilesAndDirectories(query)
|
const paths = await files.searchFilesAndDirectories(query)
|
||||||
const fileOptions: AtOption[] = paths
|
const fileOptions: AtOption[] = paths
|
||||||
.filter((path) => !seen.has(path))
|
.filter((path) => !seen.has(path))
|
||||||
@@ -604,18 +622,20 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
|
|||||||
|
|
||||||
const handleSlashSelect = (cmd: SlashCommand | undefined) => {
|
const handleSlashSelect = (cmd: SlashCommand | undefined) => {
|
||||||
if (!cmd) return
|
if (!cmd) return
|
||||||
|
promptProbe.select(cmd.id)
|
||||||
closePopover()
|
closePopover()
|
||||||
|
const images = imageAttachments()
|
||||||
|
|
||||||
if (cmd.type === "custom") {
|
if (cmd.type === "custom") {
|
||||||
const text = `/${cmd.trigger} `
|
const text = `/${cmd.trigger} `
|
||||||
setEditorText(text)
|
setEditorText(text)
|
||||||
prompt.set([{ type: "text", content: text, start: 0, end: text.length }], text.length)
|
prompt.set([{ type: "text", content: text, start: 0, end: text.length }, ...images], text.length)
|
||||||
focusEditorEnd()
|
focusEditorEnd()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
clearEditor()
|
clearEditor()
|
||||||
prompt.set([{ type: "text", content: "", start: 0, end: 0 }], 0)
|
prompt.set([...DEFAULT_PROMPT, ...images], 0)
|
||||||
command.trigger(cmd.id, "slash")
|
command.trigger(cmd.id, "slash")
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -692,6 +712,20 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
if (promptEnabled()) {
|
||||||
|
createEffect(() => {
|
||||||
|
promptProbe.set({
|
||||||
|
popover: store.popover,
|
||||||
|
slash: {
|
||||||
|
active: slashActive() ?? null,
|
||||||
|
ids: slashFlat().map((cmd) => cmd.id),
|
||||||
|
},
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
onCleanup(() => promptProbe.clear())
|
||||||
|
}
|
||||||
|
|
||||||
const selectPopoverActive = () => {
|
const selectPopoverActive = () => {
|
||||||
if (store.popover === "at") {
|
if (store.popover === "at") {
|
||||||
const items = atFlat()
|
const items = atFlat()
|
||||||
@@ -1010,7 +1044,7 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
|
|||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
const { addAttachment, removeAttachment, handlePaste } = createPromptAttachments({
|
const { addAttachments, removeAttachment, handlePaste } = createPromptAttachments({
|
||||||
editor: () => editorRef,
|
editor: () => editorRef,
|
||||||
isDialogActive: () => !!dialog.active,
|
isDialogActive: () => !!dialog.active,
|
||||||
setDraggingType: (type) => setStore("draggingType", type),
|
setDraggingType: (type) => setStore("draggingType", type),
|
||||||
@@ -1208,6 +1242,20 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
|
|||||||
|
|
||||||
// Note: Shift+Enter is handled earlier, before IME check
|
// Note: Shift+Enter is handled earlier, before IME check
|
||||||
if (event.key === "Enter" && !event.shiftKey) {
|
if (event.key === "Enter" && !event.shiftKey) {
|
||||||
|
event.preventDefault()
|
||||||
|
if (event.repeat) return
|
||||||
|
if (
|
||||||
|
working() &&
|
||||||
|
prompt
|
||||||
|
.current()
|
||||||
|
.map((part) => ("content" in part ? part.content : ""))
|
||||||
|
.join("")
|
||||||
|
.trim().length === 0 &&
|
||||||
|
imageAttachments().length === 0 &&
|
||||||
|
commentCount() === 0
|
||||||
|
) {
|
||||||
|
return
|
||||||
|
}
|
||||||
handleSubmit(event)
|
handleSubmit(event)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1336,36 +1384,18 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
|
|||||||
<input
|
<input
|
||||||
ref={fileInputRef}
|
ref={fileInputRef}
|
||||||
type="file"
|
type="file"
|
||||||
|
multiple
|
||||||
accept={ACCEPTED_FILE_TYPES.join(",")}
|
accept={ACCEPTED_FILE_TYPES.join(",")}
|
||||||
class="hidden"
|
class="hidden"
|
||||||
onChange={(e) => {
|
onChange={(e) => {
|
||||||
const file = e.currentTarget.files?.[0]
|
const list = e.currentTarget.files
|
||||||
if (file) void addAttachment(file)
|
if (list) void addAttachments(Array.from(list))
|
||||||
e.currentTarget.value = ""
|
e.currentTarget.value = ""
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div class="flex items-center gap-1 pointer-events-auto">
|
<div class="flex items-center gap-1 pointer-events-auto">
|
||||||
<Tooltip
|
<Tooltip placement="top" inactive={!prompt.dirty() && !working()} value={tip()}>
|
||||||
placement="top"
|
|
||||||
inactive={!prompt.dirty() && !working()}
|
|
||||||
value={
|
|
||||||
<Switch>
|
|
||||||
<Match when={working()}>
|
|
||||||
<div class="flex items-center gap-2">
|
|
||||||
<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>{language.t("prompt.action.send")}</span>
|
|
||||||
<Icon name="enter" size="small" class="text-icon-base" />
|
|
||||||
</div>
|
|
||||||
</Match>
|
|
||||||
</Switch>
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<IconButton
|
<IconButton
|
||||||
data-action="prompt-submit"
|
data-action="prompt-submit"
|
||||||
type="submit"
|
type="submit"
|
||||||
@@ -1464,11 +1494,15 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
|
|||||||
size="normal"
|
size="normal"
|
||||||
class="min-w-0 max-w-[320px] text-13-regular text-text-base group"
|
class="min-w-0 max-w-[320px] text-13-regular text-text-base group"
|
||||||
style={control()}
|
style={control()}
|
||||||
onClick={() => dialog.show(() => <DialogSelectModelUnpaid model={local.model} />)}
|
onClick={() => {
|
||||||
|
void import("@/components/dialog-select-model-unpaid").then((x) => {
|
||||||
|
dialog.show(() => <x.DialogSelectModelUnpaid model={local.model} />)
|
||||||
|
})
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
<Show when={local.model.current()?.provider?.id}>
|
<Show when={local.model.current()?.provider?.id}>
|
||||||
<ProviderIcon
|
<ProviderIcon
|
||||||
id={local.model.current()!.provider.id}
|
id={local.model.current()?.provider?.id ?? ""}
|
||||||
class="size-4 shrink-0 opacity-40 group-hover:opacity-100 transition-opacity duration-150"
|
class="size-4 shrink-0 opacity-40 group-hover:opacity-100 transition-opacity duration-150"
|
||||||
style={{ "will-change": "opacity", transform: "translateZ(0)" }}
|
style={{ "will-change": "opacity", transform: "translateZ(0)" }}
|
||||||
/>
|
/>
|
||||||
@@ -1500,7 +1534,7 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
|
|||||||
>
|
>
|
||||||
<Show when={local.model.current()?.provider?.id}>
|
<Show when={local.model.current()?.provider?.id}>
|
||||||
<ProviderIcon
|
<ProviderIcon
|
||||||
id={local.model.current()!.provider.id}
|
id={local.model.current()?.provider?.id ?? ""}
|
||||||
class="size-4 shrink-0 opacity-40 group-hover:opacity-100 transition-opacity duration-150"
|
class="size-4 shrink-0 opacity-40 group-hover:opacity-100 transition-opacity duration-150"
|
||||||
style={{ "will-change": "opacity", transform: "translateZ(0)" }}
|
style={{ "will-change": "opacity", transform: "translateZ(0)" }}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -71,6 +71,18 @@ export function createPromptAttachments(input: PromptAttachmentsInput) {
|
|||||||
|
|
||||||
const addAttachment = (file: File) => add(file)
|
const addAttachment = (file: File) => add(file)
|
||||||
|
|
||||||
|
const addAttachments = async (files: File[], toast = true) => {
|
||||||
|
let found = false
|
||||||
|
|
||||||
|
for (const file of files) {
|
||||||
|
const ok = await add(file, false)
|
||||||
|
if (ok) found = true
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!found && files.length > 0 && toast) warn()
|
||||||
|
return found
|
||||||
|
}
|
||||||
|
|
||||||
const removeAttachment = (id: string) => {
|
const removeAttachment = (id: string) => {
|
||||||
const current = prompt.current()
|
const current = prompt.current()
|
||||||
const next = current.filter((part) => part.type !== "image" || part.id !== id)
|
const next = current.filter((part) => part.type !== "image" || part.id !== id)
|
||||||
@@ -84,18 +96,14 @@ export function createPromptAttachments(input: PromptAttachmentsInput) {
|
|||||||
event.preventDefault()
|
event.preventDefault()
|
||||||
event.stopPropagation()
|
event.stopPropagation()
|
||||||
|
|
||||||
const items = Array.from(clipboardData.items)
|
const files = Array.from(clipboardData.items).flatMap((item) => {
|
||||||
const fileItems = items.filter((item) => item.kind === "file")
|
if (item.kind !== "file") return []
|
||||||
|
const file = item.getAsFile()
|
||||||
|
return file ? [file] : []
|
||||||
|
})
|
||||||
|
|
||||||
if (fileItems.length > 0) {
|
if (files.length > 0) {
|
||||||
let found = false
|
await addAttachments(files)
|
||||||
for (const item of fileItems) {
|
|
||||||
const file = item.getAsFile()
|
|
||||||
if (!file) continue
|
|
||||||
const ok = await add(file, false)
|
|
||||||
if (ok) found = true
|
|
||||||
}
|
|
||||||
if (!found) warn()
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -169,12 +177,7 @@ export function createPromptAttachments(input: PromptAttachmentsInput) {
|
|||||||
const dropped = event.dataTransfer?.files
|
const dropped = event.dataTransfer?.files
|
||||||
if (!dropped) return
|
if (!dropped) return
|
||||||
|
|
||||||
let found = false
|
await addAttachments(Array.from(dropped))
|
||||||
for (const file of Array.from(dropped)) {
|
|
||||||
const ok = await add(file, false)
|
|
||||||
if (ok) found = true
|
|
||||||
}
|
|
||||||
if (!found && dropped.length > 0) warn()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
onMount(() => {
|
onMount(() => {
|
||||||
@@ -191,6 +194,7 @@ export function createPromptAttachments(input: PromptAttachmentsInput) {
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
addAttachment,
|
addAttachment,
|
||||||
|
addAttachments,
|
||||||
removeAttachment,
|
removeAttachment,
|
||||||
handlePaste,
|
handlePaste,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -49,6 +49,32 @@ describe("buildRequestParts", () => {
|
|||||||
expect(result.optimisticParts.every((part) => part.sessionID === "ses_1" && part.messageID === "msg_1")).toBe(true)
|
expect(result.optimisticParts.every((part) => part.sessionID === "ses_1" && part.messageID === "msg_1")).toBe(true)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
test("keeps multiple uploaded attachments in order", () => {
|
||||||
|
const result = buildRequestParts({
|
||||||
|
prompt: [{ type: "text", content: "check these", start: 0, end: 11 }],
|
||||||
|
context: [],
|
||||||
|
images: [
|
||||||
|
{ type: "image", id: "img_1", filename: "a.png", mime: "image/png", dataUrl: "data:image/png;base64,AAA" },
|
||||||
|
{
|
||||||
|
type: "image",
|
||||||
|
id: "img_2",
|
||||||
|
filename: "b.pdf",
|
||||||
|
mime: "application/pdf",
|
||||||
|
dataUrl: "data:application/pdf;base64,BBB",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
text: "check these",
|
||||||
|
messageID: "msg_multi",
|
||||||
|
sessionID: "ses_multi",
|
||||||
|
sessionDirectory: "/repo",
|
||||||
|
})
|
||||||
|
|
||||||
|
const files = result.requestParts.filter((part) => part.type === "file" && part.url.startsWith("data:"))
|
||||||
|
|
||||||
|
expect(files).toHaveLength(2)
|
||||||
|
expect(files.map((part) => (part.type === "file" ? part.filename : ""))).toEqual(["a.png", "b.pdf"])
|
||||||
|
})
|
||||||
|
|
||||||
test("deduplicates context files when prompt already includes same path", () => {
|
test("deduplicates context files when prompt already includes same path", () => {
|
||||||
const prompt: Prompt = [{ type: "file", path: "src/foo.ts", content: "@src/foo.ts", start: 0, end: 11 }]
|
const prompt: Prompt = [{ type: "file", path: "src/foo.ts", content: "@src/foo.ts", start: 0, end: 11 }]
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
export const ACCEPTED_IMAGE_TYPES = ["image/png", "image/jpeg", "image/gif", "image/webp"]
|
import { ACCEPTED_FILE_TYPES, ACCEPTED_IMAGE_TYPES } from "@/constants/file-picker"
|
||||||
|
|
||||||
|
export { ACCEPTED_FILE_TYPES }
|
||||||
|
|
||||||
const IMAGE_MIMES = new Set(ACCEPTED_IMAGE_TYPES)
|
const IMAGE_MIMES = new Set(ACCEPTED_IMAGE_TYPES)
|
||||||
const IMAGE_EXTS = new Map([
|
const IMAGE_EXTS = new Map([
|
||||||
@@ -18,61 +20,6 @@ const TEXT_MIMES = new Set([
|
|||||||
"application/yaml",
|
"application/yaml",
|
||||||
])
|
])
|
||||||
|
|
||||||
export const ACCEPTED_FILE_TYPES = [
|
|
||||||
...ACCEPTED_IMAGE_TYPES,
|
|
||||||
"application/pdf",
|
|
||||||
"text/*",
|
|
||||||
"application/json",
|
|
||||||
"application/ld+json",
|
|
||||||
"application/toml",
|
|
||||||
"application/x-toml",
|
|
||||||
"application/x-yaml",
|
|
||||||
"application/xml",
|
|
||||||
"application/yaml",
|
|
||||||
".c",
|
|
||||||
".cc",
|
|
||||||
".cjs",
|
|
||||||
".conf",
|
|
||||||
".cpp",
|
|
||||||
".css",
|
|
||||||
".csv",
|
|
||||||
".cts",
|
|
||||||
".env",
|
|
||||||
".go",
|
|
||||||
".gql",
|
|
||||||
".graphql",
|
|
||||||
".h",
|
|
||||||
".hh",
|
|
||||||
".hpp",
|
|
||||||
".htm",
|
|
||||||
".html",
|
|
||||||
".ini",
|
|
||||||
".java",
|
|
||||||
".js",
|
|
||||||
".json",
|
|
||||||
".jsx",
|
|
||||||
".log",
|
|
||||||
".md",
|
|
||||||
".mdx",
|
|
||||||
".mjs",
|
|
||||||
".mts",
|
|
||||||
".py",
|
|
||||||
".rb",
|
|
||||||
".rs",
|
|
||||||
".sass",
|
|
||||||
".scss",
|
|
||||||
".sh",
|
|
||||||
".sql",
|
|
||||||
".toml",
|
|
||||||
".ts",
|
|
||||||
".tsx",
|
|
||||||
".txt",
|
|
||||||
".xml",
|
|
||||||
".yaml",
|
|
||||||
".yml",
|
|
||||||
".zsh",
|
|
||||||
]
|
|
||||||
|
|
||||||
const SAMPLE = 4096
|
const SAMPLE = 4096
|
||||||
|
|
||||||
function kind(type: string) {
|
function kind(type: string) {
|
||||||
|
|||||||
@@ -126,7 +126,7 @@ describe("prompt-input history", () => {
|
|||||||
test("canNavigateHistoryAtCursor only allows prompt boundaries", () => {
|
test("canNavigateHistoryAtCursor only allows prompt boundaries", () => {
|
||||||
const value = "a\nb\nc"
|
const value = "a\nb\nc"
|
||||||
|
|
||||||
expect(canNavigateHistoryAtCursor("up", value, 0)).toBe(true)
|
expect(canNavigateHistoryAtCursor("up", value, 0)).toBe(false)
|
||||||
expect(canNavigateHistoryAtCursor("down", value, 0)).toBe(false)
|
expect(canNavigateHistoryAtCursor("down", value, 0)).toBe(false)
|
||||||
|
|
||||||
expect(canNavigateHistoryAtCursor("up", value, 2)).toBe(false)
|
expect(canNavigateHistoryAtCursor("up", value, 2)).toBe(false)
|
||||||
@@ -135,11 +135,14 @@ describe("prompt-input history", () => {
|
|||||||
expect(canNavigateHistoryAtCursor("up", value, 5)).toBe(false)
|
expect(canNavigateHistoryAtCursor("up", value, 5)).toBe(false)
|
||||||
expect(canNavigateHistoryAtCursor("down", value, 5)).toBe(true)
|
expect(canNavigateHistoryAtCursor("down", value, 5)).toBe(true)
|
||||||
|
|
||||||
expect(canNavigateHistoryAtCursor("up", "abc", 0)).toBe(true)
|
expect(canNavigateHistoryAtCursor("up", "abc", 0)).toBe(false)
|
||||||
expect(canNavigateHistoryAtCursor("down", "abc", 3)).toBe(true)
|
expect(canNavigateHistoryAtCursor("down", "abc", 3)).toBe(true)
|
||||||
expect(canNavigateHistoryAtCursor("up", "abc", 1)).toBe(false)
|
expect(canNavigateHistoryAtCursor("up", "abc", 1)).toBe(false)
|
||||||
expect(canNavigateHistoryAtCursor("down", "abc", 1)).toBe(false)
|
expect(canNavigateHistoryAtCursor("down", "abc", 1)).toBe(false)
|
||||||
|
|
||||||
|
expect(canNavigateHistoryAtCursor("up", "", 0)).toBe(true)
|
||||||
|
expect(canNavigateHistoryAtCursor("down", "", 0)).toBe(true)
|
||||||
|
|
||||||
expect(canNavigateHistoryAtCursor("up", "abc", 0, true)).toBe(true)
|
expect(canNavigateHistoryAtCursor("up", "abc", 0, true)).toBe(true)
|
||||||
expect(canNavigateHistoryAtCursor("up", "abc", 3, true)).toBe(true)
|
expect(canNavigateHistoryAtCursor("up", "abc", 3, true)).toBe(true)
|
||||||
expect(canNavigateHistoryAtCursor("down", "abc", 0, true)).toBe(true)
|
expect(canNavigateHistoryAtCursor("down", "abc", 0, true)).toBe(true)
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ export function canNavigateHistoryAtCursor(direction: "up" | "down", text: strin
|
|||||||
const atStart = position === 0
|
const atStart = position === 0
|
||||||
const atEnd = position === text.length
|
const atEnd = position === text.length
|
||||||
if (inHistory) return atStart || atEnd
|
if (inHistory) return atStart || atEnd
|
||||||
if (direction === "up") return position === 0
|
if (direction === "up") return position === 0 && text.length === 0
|
||||||
return position === text.length
|
return position === text.length
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import { useFile } from "@/context/file"
|
|||||||
import { useLayout } from "@/context/layout"
|
import { useLayout } from "@/context/layout"
|
||||||
import { useSync } from "@/context/sync"
|
import { useSync } from "@/context/sync"
|
||||||
import { useLanguage } from "@/context/language"
|
import { useLanguage } from "@/context/language"
|
||||||
|
import { useProviders } from "@/hooks/use-providers"
|
||||||
import { getSessionContextMetrics } from "@/components/session/session-context-metrics"
|
import { getSessionContextMetrics } from "@/components/session/session-context-metrics"
|
||||||
import { useSessionLayout } from "@/pages/session/session-layout"
|
import { useSessionLayout } from "@/pages/session/session-layout"
|
||||||
import { createSessionTabs } from "@/pages/session/helpers"
|
import { createSessionTabs } from "@/pages/session/helpers"
|
||||||
@@ -32,6 +33,7 @@ export function SessionContextUsage(props: SessionContextUsageProps) {
|
|||||||
const file = useFile()
|
const file = useFile()
|
||||||
const layout = useLayout()
|
const layout = useLayout()
|
||||||
const language = useLanguage()
|
const language = useLanguage()
|
||||||
|
const providers = useProviders()
|
||||||
const { params, tabs, view } = useSessionLayout()
|
const { params, tabs, view } = useSessionLayout()
|
||||||
|
|
||||||
const variant = createMemo(() => props.variant ?? "button")
|
const variant = createMemo(() => props.variant ?? "button")
|
||||||
@@ -50,7 +52,7 @@ export function SessionContextUsage(props: SessionContextUsageProps) {
|
|||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
|
|
||||||
const metrics = createMemo(() => getSessionContextMetrics(messages(), sync.data.provider.all))
|
const metrics = createMemo(() => getSessionContextMetrics(messages(), providers.all()))
|
||||||
const context = createMemo(() => metrics().context)
|
const context = createMemo(() => metrics().context)
|
||||||
const cost = createMemo(() => {
|
const cost = createMemo(() => {
|
||||||
return usd().format(metrics().totalCost)
|
return usd().format(metrics().totalCost)
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import { Markdown } from "@opencode-ai/ui/markdown"
|
|||||||
import { ScrollView } from "@opencode-ai/ui/scroll-view"
|
import { ScrollView } from "@opencode-ai/ui/scroll-view"
|
||||||
import type { Message, Part, UserMessage } from "@opencode-ai/sdk/v2/client"
|
import type { Message, Part, UserMessage } from "@opencode-ai/sdk/v2/client"
|
||||||
import { useLanguage } from "@/context/language"
|
import { useLanguage } from "@/context/language"
|
||||||
|
import { useProviders } from "@/hooks/use-providers"
|
||||||
import { useSessionLayout } from "@/pages/session/session-layout"
|
import { useSessionLayout } from "@/pages/session/session-layout"
|
||||||
import { getSessionContextMetrics } from "./session-context-metrics"
|
import { getSessionContextMetrics } from "./session-context-metrics"
|
||||||
import { estimateSessionContextBreakdown, type SessionContextBreakdownKey } from "./session-context-breakdown"
|
import { estimateSessionContextBreakdown, type SessionContextBreakdownKey } from "./session-context-breakdown"
|
||||||
@@ -92,6 +93,7 @@ const emptyUserMessages: UserMessage[] = []
|
|||||||
export function SessionContextTab() {
|
export function SessionContextTab() {
|
||||||
const sync = useSync()
|
const sync = useSync()
|
||||||
const language = useLanguage()
|
const language = useLanguage()
|
||||||
|
const providers = useProviders()
|
||||||
const { params, view } = useSessionLayout()
|
const { params, view } = useSessionLayout()
|
||||||
|
|
||||||
const info = createMemo(() => (params.id ? sync.session.get(params.id) : undefined))
|
const info = createMemo(() => (params.id ? sync.session.get(params.id) : undefined))
|
||||||
@@ -130,7 +132,7 @@ export function SessionContextTab() {
|
|||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
|
|
||||||
const metrics = createMemo(() => getSessionContextMetrics(messages(), sync.data.provider.all))
|
const metrics = createMemo(() => getSessionContextMetrics(messages(), providers.all()))
|
||||||
const ctx = createMemo(() => metrics().context)
|
const ctx = createMemo(() => metrics().context)
|
||||||
const formatter = createMemo(() => createSessionContextFormatter(language.intl()))
|
const formatter = createMemo(() => createSessionContextFormatter(language.intl()))
|
||||||
|
|
||||||
@@ -267,14 +269,14 @@ export function SessionContextTab() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<ScrollView
|
<ScrollView
|
||||||
class="@container h-full pb-10"
|
class="@container h-full"
|
||||||
viewportRef={(el) => {
|
viewportRef={(el) => {
|
||||||
scroll = el
|
scroll = el
|
||||||
restoreScroll()
|
restoreScroll()
|
||||||
}}
|
}}
|
||||||
onScroll={handleScroll}
|
onScroll={handleScroll}
|
||||||
>
|
>
|
||||||
<div class="px-6 pt-4 flex flex-col gap-10">
|
<div class="px-6 pt-4 pb-10 flex flex-col gap-10">
|
||||||
<div class="grid grid-cols-1 @[32rem]:grid-cols-2 gap-4">
|
<div class="grid grid-cols-1 @[32rem]:grid-cols-2 gap-4">
|
||||||
<For each={stats}>
|
<For each={stats}>
|
||||||
{(stat) => <Stat label={language.t(stat.label as Parameters<typeof language.t>[0])} value={stat.value()} />}
|
{(stat) => <Stat label={language.t(stat.label as Parameters<typeof language.t>[0])} value={stat.value()} />}
|
||||||
|
|||||||
@@ -274,12 +274,11 @@ export function SessionHeader() {
|
|||||||
type="button"
|
type="button"
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
size="small"
|
size="small"
|
||||||
class="hidden md:flex w-[240px] max-w-full min-w-0 pl-0.5 pr-2 items-center gap-2 justify-between rounded-md border border-border-weak-base bg-surface-panel shadow-none cursor-default"
|
class="hidden md:flex w-[240px] max-w-full min-w-0 items-center gap-2 justify-between rounded-md border border-border-weak-base bg-surface-panel shadow-none cursor-default"
|
||||||
onClick={() => command.trigger("file.open")}
|
onClick={() => command.trigger("file.open")}
|
||||||
aria-label={language.t("session.header.searchFiles")}
|
aria-label={language.t("session.header.searchFiles")}
|
||||||
>
|
>
|
||||||
<div class="flex min-w-0 flex-1 items-center gap-1.5 overflow-visible">
|
<div class="flex min-w-0 flex-1 items-center overflow-visible">
|
||||||
<Icon name="magnifying-glass" size="small" class="icon-base shrink-0 size-4" />
|
|
||||||
<span class="flex-1 min-w-0 text-12-regular text-text-weak truncate text-left">
|
<span class="flex-1 min-w-0 text-12-regular text-text-weak truncate text-left">
|
||||||
{language.t("session.header.search.placeholder", {
|
{language.t("session.header.search.placeholder", {
|
||||||
project: name(),
|
project: name(),
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ export function SortableTerminalTab(props: { terminal: LocalPTY; onClose?: () =>
|
|||||||
})
|
})
|
||||||
let input: HTMLInputElement | undefined
|
let input: HTMLInputElement | undefined
|
||||||
let blurFrame: number | undefined
|
let blurFrame: number | undefined
|
||||||
|
let editRequested = false
|
||||||
|
|
||||||
const isDefaultTitle = () => {
|
const isDefaultTitle = () => {
|
||||||
const number = props.terminal.titleNumber
|
const number = props.terminal.titleNumber
|
||||||
@@ -168,8 +169,14 @@ export function SortableTerminalTab(props: { terminal: LocalPTY; onClose?: () =>
|
|||||||
left: `${store.menuPosition.x}px`,
|
left: `${store.menuPosition.x}px`,
|
||||||
top: `${store.menuPosition.y}px`,
|
top: `${store.menuPosition.y}px`,
|
||||||
}}
|
}}
|
||||||
|
onCloseAutoFocus={(e) => {
|
||||||
|
if (!editRequested) return
|
||||||
|
e.preventDefault()
|
||||||
|
editRequested = false
|
||||||
|
requestAnimationFrame(() => edit())
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
<DropdownMenu.Item onSelect={edit}>
|
<DropdownMenu.Item onSelect={() => (editRequested = true)}>
|
||||||
<Icon name="edit" class="w-4 h-4 mr-2" />
|
<Icon name="edit" class="w-4 h-4 mr-2" />
|
||||||
{language.t("common.rename")}
|
{language.t("common.rename")}
|
||||||
</DropdownMenu.Item>
|
</DropdownMenu.Item>
|
||||||
|
|||||||
@@ -1,27 +1,43 @@
|
|||||||
import { Component, Show, createMemo, createResource, type JSX } from "solid-js"
|
import { Component, Show, createMemo, createResource, onMount, type JSX } from "solid-js"
|
||||||
import { createStore } from "solid-js/store"
|
import { createStore } from "solid-js/store"
|
||||||
import { Button } from "@opencode-ai/ui/button"
|
import { Button } from "@opencode-ai/ui/button"
|
||||||
import { Icon } from "@opencode-ai/ui/icon"
|
import { Icon } from "@opencode-ai/ui/icon"
|
||||||
import { Select } from "@opencode-ai/ui/select"
|
import { Select } from "@opencode-ai/ui/select"
|
||||||
import { Switch } from "@opencode-ai/ui/switch"
|
import { Switch } from "@opencode-ai/ui/switch"
|
||||||
|
import { TextField } from "@opencode-ai/ui/text-field"
|
||||||
import { Tooltip } from "@opencode-ai/ui/tooltip"
|
import { Tooltip } from "@opencode-ai/ui/tooltip"
|
||||||
import { useTheme, type ColorScheme } from "@opencode-ai/ui/theme"
|
import { useTheme, type ColorScheme } from "@opencode-ai/ui/theme/context"
|
||||||
import { showToast } from "@opencode-ai/ui/toast"
|
import { showToast } from "@opencode-ai/ui/toast"
|
||||||
import { useLanguage } from "@/context/language"
|
import { useLanguage } from "@/context/language"
|
||||||
import { usePlatform } from "@/context/platform"
|
import { usePlatform } from "@/context/platform"
|
||||||
import { useSettings, monoFontFamily } from "@/context/settings"
|
import {
|
||||||
import { playSound, SOUND_OPTIONS } from "@/utils/sound"
|
monoDefault,
|
||||||
|
monoFontFamily,
|
||||||
|
monoInput,
|
||||||
|
sansDefault,
|
||||||
|
sansFontFamily,
|
||||||
|
sansInput,
|
||||||
|
useSettings,
|
||||||
|
} from "@/context/settings"
|
||||||
|
import { playSoundById, SOUND_OPTIONS } from "@/utils/sound"
|
||||||
import { Link } from "./link"
|
import { Link } from "./link"
|
||||||
import { SettingsList } from "./settings-list"
|
import { SettingsList } from "./settings-list"
|
||||||
|
|
||||||
let demoSoundState = {
|
let demoSoundState = {
|
||||||
cleanup: undefined as (() => void) | undefined,
|
cleanup: undefined as (() => void) | undefined,
|
||||||
timeout: undefined as NodeJS.Timeout | undefined,
|
timeout: undefined as NodeJS.Timeout | undefined,
|
||||||
|
run: 0,
|
||||||
|
}
|
||||||
|
|
||||||
|
type ThemeOption = {
|
||||||
|
id: string
|
||||||
|
name: string
|
||||||
}
|
}
|
||||||
|
|
||||||
// To prevent audio from overlapping/playing very quickly when navigating the settings menus,
|
// To prevent audio from overlapping/playing very quickly when navigating the settings menus,
|
||||||
// delay the playback by 100ms during quick selection changes and pause existing sounds.
|
// delay the playback by 100ms during quick selection changes and pause existing sounds.
|
||||||
const stopDemoSound = () => {
|
const stopDemoSound = () => {
|
||||||
|
demoSoundState.run += 1
|
||||||
if (demoSoundState.cleanup) {
|
if (demoSoundState.cleanup) {
|
||||||
demoSoundState.cleanup()
|
demoSoundState.cleanup()
|
||||||
}
|
}
|
||||||
@@ -29,12 +45,19 @@ const stopDemoSound = () => {
|
|||||||
demoSoundState.cleanup = undefined
|
demoSoundState.cleanup = undefined
|
||||||
}
|
}
|
||||||
|
|
||||||
const playDemoSound = (src: string | undefined) => {
|
const playDemoSound = (id: string | undefined) => {
|
||||||
stopDemoSound()
|
stopDemoSound()
|
||||||
if (!src) return
|
if (!id) return
|
||||||
|
|
||||||
|
const run = ++demoSoundState.run
|
||||||
demoSoundState.timeout = setTimeout(() => {
|
demoSoundState.timeout = setTimeout(() => {
|
||||||
demoSoundState.cleanup = playSound(src)
|
void playSoundById(id).then((cleanup) => {
|
||||||
|
if (demoSoundState.run !== run) {
|
||||||
|
cleanup?.()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
demoSoundState.cleanup = cleanup
|
||||||
|
})
|
||||||
}, 100)
|
}, 100)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -44,6 +67,10 @@ export const SettingsGeneral: Component = () => {
|
|||||||
const platform = usePlatform()
|
const platform = usePlatform()
|
||||||
const settings = useSettings()
|
const settings = useSettings()
|
||||||
|
|
||||||
|
onMount(() => {
|
||||||
|
void theme.loadThemes()
|
||||||
|
})
|
||||||
|
|
||||||
const [store, setStore] = createStore({
|
const [store, setStore] = createStore({
|
||||||
checking: false,
|
checking: false,
|
||||||
})
|
})
|
||||||
@@ -104,9 +131,7 @@ export const SettingsGeneral: Component = () => {
|
|||||||
.finally(() => setStore("checking", false))
|
.finally(() => setStore("checking", false))
|
||||||
}
|
}
|
||||||
|
|
||||||
const themeOptions = createMemo(() =>
|
const themeOptions = createMemo<ThemeOption[]>(() => theme.ids().map((id) => ({ id, name: theme.name(id) })))
|
||||||
Object.entries(theme.themes()).map(([id, def]) => ({ id, name: def.name ?? id })),
|
|
||||||
)
|
|
||||||
|
|
||||||
const colorSchemeOptions = createMemo((): { value: ColorScheme; label: string }[] => [
|
const colorSchemeOptions = createMemo((): { value: ColorScheme; label: string }[] => [
|
||||||
{ value: "system", label: language.t("theme.scheme.system") },
|
{ value: "system", label: language.t("theme.scheme.system") },
|
||||||
@@ -126,25 +151,10 @@ export const SettingsGeneral: Component = () => {
|
|||||||
})),
|
})),
|
||||||
)
|
)
|
||||||
|
|
||||||
const fontOptions = [
|
const noneSound = { id: "none", label: "sound.option.none" } as const
|
||||||
{ 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: "iosevka", label: "font.option.iosevka" },
|
|
||||||
{ 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" },
|
|
||||||
{ value: "geist-mono", label: "font.option.geistMono" },
|
|
||||||
] as const
|
|
||||||
const fontOptionsList = [...fontOptions]
|
|
||||||
|
|
||||||
const noneSound = { id: "none", label: "sound.option.none", src: undefined } as const
|
|
||||||
const soundOptions = [noneSound, ...SOUND_OPTIONS]
|
const soundOptions = [noneSound, ...SOUND_OPTIONS]
|
||||||
|
const mono = () => monoInput(settings.appearance.font())
|
||||||
|
const sans = () => sansInput(settings.appearance.uiFont())
|
||||||
|
|
||||||
const soundSelectProps = (
|
const soundSelectProps = (
|
||||||
enabled: () => boolean,
|
enabled: () => boolean,
|
||||||
@@ -158,7 +168,7 @@ export const SettingsGeneral: Component = () => {
|
|||||||
label: (o: (typeof soundOptions)[number]) => language.t(o.label),
|
label: (o: (typeof soundOptions)[number]) => language.t(o.label),
|
||||||
onHighlight: (option: (typeof soundOptions)[number] | undefined) => {
|
onHighlight: (option: (typeof soundOptions)[number] | undefined) => {
|
||||||
if (!option) return
|
if (!option) return
|
||||||
playDemoSound(option.src)
|
playDemoSound(option.id === "none" ? undefined : option.id)
|
||||||
},
|
},
|
||||||
onSelect: (option: (typeof soundOptions)[number] | undefined) => {
|
onSelect: (option: (typeof soundOptions)[number] | undefined) => {
|
||||||
if (!option) return
|
if (!option) return
|
||||||
@@ -169,7 +179,7 @@ export const SettingsGeneral: Component = () => {
|
|||||||
}
|
}
|
||||||
setEnabled(true)
|
setEnabled(true)
|
||||||
set(option.id)
|
set(option.id)
|
||||||
playDemoSound(option.src)
|
playDemoSound(option.id)
|
||||||
},
|
},
|
||||||
variant: "secondary" as const,
|
variant: "secondary" as const,
|
||||||
size: "small" as const,
|
size: "small" as const,
|
||||||
@@ -311,28 +321,50 @@ export const SettingsGeneral: Component = () => {
|
|||||||
/>
|
/>
|
||||||
</SettingsRow>
|
</SettingsRow>
|
||||||
|
|
||||||
|
<SettingsRow
|
||||||
|
title={language.t("settings.general.row.uiFont.title")}
|
||||||
|
description={language.t("settings.general.row.uiFont.description")}
|
||||||
|
>
|
||||||
|
<div class="w-full sm:w-[220px]">
|
||||||
|
<TextField
|
||||||
|
data-action="settings-ui-font"
|
||||||
|
label={language.t("settings.general.row.uiFont.title")}
|
||||||
|
hideLabel
|
||||||
|
type="text"
|
||||||
|
value={sans()}
|
||||||
|
onChange={(value) => settings.appearance.setUIFont(value)}
|
||||||
|
placeholder={sansDefault}
|
||||||
|
spellcheck={false}
|
||||||
|
autocorrect="off"
|
||||||
|
autocomplete="off"
|
||||||
|
autocapitalize="off"
|
||||||
|
class="text-12-regular"
|
||||||
|
style={{ "font-family": sansFontFamily(settings.appearance.uiFont()) }}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</SettingsRow>
|
||||||
|
|
||||||
<SettingsRow
|
<SettingsRow
|
||||||
title={language.t("settings.general.row.font.title")}
|
title={language.t("settings.general.row.font.title")}
|
||||||
description={language.t("settings.general.row.font.description")}
|
description={language.t("settings.general.row.font.description")}
|
||||||
>
|
>
|
||||||
<Select
|
<div class="w-full sm:w-[220px]">
|
||||||
data-action="settings-font"
|
<TextField
|
||||||
options={fontOptionsList}
|
data-action="settings-code-font"
|
||||||
current={fontOptionsList.find((o) => o.value === settings.appearance.font())}
|
label={language.t("settings.general.row.font.title")}
|
||||||
value={(o) => o.value}
|
hideLabel
|
||||||
label={(o) => language.t(o.label)}
|
type="text"
|
||||||
onSelect={(option) => option && settings.appearance.setFont(option.value)}
|
value={mono()}
|
||||||
variant="secondary"
|
onChange={(value) => settings.appearance.setFont(value)}
|
||||||
size="small"
|
placeholder={monoDefault}
|
||||||
triggerVariant="settings"
|
spellcheck={false}
|
||||||
triggerStyle={{ "font-family": monoFontFamily(settings.appearance.font()), "min-width": "180px" }}
|
autocorrect="off"
|
||||||
>
|
autocomplete="off"
|
||||||
{(option) => (
|
autocapitalize="off"
|
||||||
<span style={{ "font-family": monoFontFamily(option?.value) }}>
|
class="text-12-regular"
|
||||||
{option ? language.t(option.label) : ""}
|
style={{ "font-family": monoFontFamily(settings.appearance.font()) }}
|
||||||
</span>
|
/>
|
||||||
)}
|
</div>
|
||||||
</Select>
|
|
||||||
</SettingsRow>
|
</SettingsRow>
|
||||||
</SettingsList>
|
</SettingsList>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -0,0 +1,445 @@
|
|||||||
|
import { Button } from "@opencode-ai/ui/button"
|
||||||
|
import { useDialog } from "@opencode-ai/ui/context/dialog"
|
||||||
|
import { Icon } from "@opencode-ai/ui/icon"
|
||||||
|
import { Switch } from "@opencode-ai/ui/switch"
|
||||||
|
import { Tabs } from "@opencode-ai/ui/tabs"
|
||||||
|
import { useMutation } from "@tanstack/solid-query"
|
||||||
|
import { showToast } from "@opencode-ai/ui/toast"
|
||||||
|
import { useNavigate } from "@solidjs/router"
|
||||||
|
import { type Accessor, createEffect, createMemo, For, type JSXElement, onCleanup, Show } from "solid-js"
|
||||||
|
import { createStore, reconcile } from "solid-js/store"
|
||||||
|
import { ServerHealthIndicator, ServerRow } from "@/components/server/server-row"
|
||||||
|
import { useLanguage } from "@/context/language"
|
||||||
|
import { usePlatform } from "@/context/platform"
|
||||||
|
import { useSDK } from "@/context/sdk"
|
||||||
|
import { normalizeServerUrl, ServerConnection, useServer } from "@/context/server"
|
||||||
|
import { useSync } from "@/context/sync"
|
||||||
|
import { useCheckServerHealth, type ServerHealth } from "@/utils/server-health"
|
||||||
|
|
||||||
|
const pollMs = 10_000
|
||||||
|
|
||||||
|
const pluginEmptyMessage = (value: string, file: string): JSXElement => {
|
||||||
|
const parts = value.split(file)
|
||||||
|
if (parts.length === 1) return value
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{parts[0]}
|
||||||
|
<code class="bg-surface-raised-base px-1.5 py-0.5 rounded-sm text-text-base">{file}</code>
|
||||||
|
{parts.slice(1).join(file)}
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const listServersByHealth = (
|
||||||
|
list: ServerConnection.Any[],
|
||||||
|
active: ServerConnection.Key | undefined,
|
||||||
|
status: Record<ServerConnection.Key, ServerHealth | undefined>,
|
||||||
|
) => {
|
||||||
|
if (!list.length) return list
|
||||||
|
const order = new Map(list.map((url, index) => [url, index] as const))
|
||||||
|
const rank = (value?: ServerHealth) => {
|
||||||
|
if (value?.healthy === true) return 0
|
||||||
|
if (value?.healthy === false) return 2
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
return list.slice().sort((a, b) => {
|
||||||
|
if (ServerConnection.key(a) === active) return -1
|
||||||
|
if (ServerConnection.key(b) === active) return 1
|
||||||
|
const diff = rank(status[ServerConnection.key(a)]) - rank(status[ServerConnection.key(b)])
|
||||||
|
if (diff !== 0) return diff
|
||||||
|
return (order.get(a) ?? 0) - (order.get(b) ?? 0)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const useServerHealth = (servers: Accessor<ServerConnection.Any[]>, enabled: Accessor<boolean>) => {
|
||||||
|
const checkServerHealth = useCheckServerHealth()
|
||||||
|
const [status, setStatus] = createStore({} as Record<ServerConnection.Key, ServerHealth | undefined>)
|
||||||
|
|
||||||
|
createEffect(() => {
|
||||||
|
if (!enabled()) {
|
||||||
|
setStatus(reconcile({}))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const list = servers()
|
||||||
|
let dead = false
|
||||||
|
|
||||||
|
const refresh = async () => {
|
||||||
|
const results: Record<string, ServerHealth> = {}
|
||||||
|
await Promise.all(
|
||||||
|
list.map(async (conn) => {
|
||||||
|
results[ServerConnection.key(conn)] = await checkServerHealth(conn.http)
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
if (dead) return
|
||||||
|
setStatus(reconcile(results))
|
||||||
|
}
|
||||||
|
|
||||||
|
void refresh()
|
||||||
|
const id = setInterval(() => void refresh(), pollMs)
|
||||||
|
onCleanup(() => {
|
||||||
|
dead = true
|
||||||
|
clearInterval(id)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
return status
|
||||||
|
}
|
||||||
|
|
||||||
|
const useDefaultServerKey = (
|
||||||
|
get: (() => string | Promise<string | null | undefined> | null | undefined) | undefined,
|
||||||
|
) => {
|
||||||
|
const [state, setState] = createStore({
|
||||||
|
url: undefined as string | undefined,
|
||||||
|
tick: 0,
|
||||||
|
})
|
||||||
|
|
||||||
|
createEffect(() => {
|
||||||
|
state.tick
|
||||||
|
let dead = false
|
||||||
|
const result = get?.()
|
||||||
|
if (!result) {
|
||||||
|
setState("url", undefined)
|
||||||
|
onCleanup(() => {
|
||||||
|
dead = true
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (result instanceof Promise) {
|
||||||
|
void result.then((next) => {
|
||||||
|
if (dead) return
|
||||||
|
setState("url", next ? normalizeServerUrl(next) : undefined)
|
||||||
|
})
|
||||||
|
onCleanup(() => {
|
||||||
|
dead = true
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
setState("url", normalizeServerUrl(result))
|
||||||
|
onCleanup(() => {
|
||||||
|
dead = true
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
return {
|
||||||
|
key: () => {
|
||||||
|
const u = state.url
|
||||||
|
if (!u) return
|
||||||
|
return ServerConnection.key({ type: "http", http: { url: u } })
|
||||||
|
},
|
||||||
|
refresh: () => setState("tick", (value) => value + 1),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const useMcpToggleMutation = () => {
|
||||||
|
const sync = useSync()
|
||||||
|
const sdk = useSDK()
|
||||||
|
const language = useLanguage()
|
||||||
|
|
||||||
|
return useMutation(() => ({
|
||||||
|
mutationFn: async (name: string) => {
|
||||||
|
const status = sync.data.mcp[name]
|
||||||
|
await (status?.status === "connected" ? sdk.client.mcp.disconnect({ name }) : sdk.client.mcp.connect({ name }))
|
||||||
|
const result = await sdk.client.mcp.status()
|
||||||
|
if (result.data) sync.set("mcp", result.data)
|
||||||
|
},
|
||||||
|
onError: (err) => {
|
||||||
|
showToast({
|
||||||
|
variant: "error",
|
||||||
|
title: language.t("common.requestFailed"),
|
||||||
|
description: err instanceof Error ? err.message : String(err),
|
||||||
|
})
|
||||||
|
},
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
export function StatusPopoverBody(props: { shown: Accessor<boolean> }) {
|
||||||
|
const sync = useSync()
|
||||||
|
const server = useServer()
|
||||||
|
const platform = usePlatform()
|
||||||
|
const dialog = useDialog()
|
||||||
|
const language = useLanguage()
|
||||||
|
const navigate = useNavigate()
|
||||||
|
const sdk = useSDK()
|
||||||
|
|
||||||
|
const [load, setLoad] = createStore({
|
||||||
|
lspDone: false,
|
||||||
|
lspLoading: false,
|
||||||
|
mcpDone: false,
|
||||||
|
mcpLoading: false,
|
||||||
|
})
|
||||||
|
|
||||||
|
const fail = (err: unknown) => {
|
||||||
|
showToast({
|
||||||
|
variant: "error",
|
||||||
|
title: language.t("common.requestFailed"),
|
||||||
|
description: err instanceof Error ? err.message : String(err),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
createEffect(() => {
|
||||||
|
if (!props.shown()) return
|
||||||
|
|
||||||
|
if (!sync.data.mcp_ready && !load.mcpDone && !load.mcpLoading) {
|
||||||
|
setLoad("mcpLoading", true)
|
||||||
|
void sdk.client.mcp
|
||||||
|
.status()
|
||||||
|
.then((result) => {
|
||||||
|
sync.set("mcp", result.data ?? {})
|
||||||
|
sync.set("mcp_ready", true)
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
setLoad("mcpDone", true)
|
||||||
|
fail(err)
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
setLoad("mcpLoading", false)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!sync.data.lsp_ready && !load.lspDone && !load.lspLoading) {
|
||||||
|
setLoad("lspLoading", true)
|
||||||
|
void sdk.client.lsp
|
||||||
|
.status()
|
||||||
|
.then((result) => {
|
||||||
|
sync.set("lsp", result.data ?? [])
|
||||||
|
sync.set("lsp_ready", true)
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
setLoad("lspDone", true)
|
||||||
|
fail(err)
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
setLoad("lspLoading", false)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
let dialogRun = 0
|
||||||
|
let dialogDead = false
|
||||||
|
onCleanup(() => {
|
||||||
|
dialogDead = true
|
||||||
|
dialogRun += 1
|
||||||
|
})
|
||||||
|
const servers = createMemo(() => {
|
||||||
|
const current = server.current
|
||||||
|
const list = server.list
|
||||||
|
if (!current) return list
|
||||||
|
if (list.every((item) => ServerConnection.key(item) !== ServerConnection.key(current))) return [current, ...list]
|
||||||
|
return [current, ...list.filter((item) => ServerConnection.key(item) !== ServerConnection.key(current))]
|
||||||
|
})
|
||||||
|
const health = useServerHealth(servers, props.shown)
|
||||||
|
const sortedServers = createMemo(() => listServersByHealth(servers(), server.key, health))
|
||||||
|
const toggleMcp = useMcpToggleMutation()
|
||||||
|
const defaultServer = useDefaultServerKey(platform.getDefaultServer)
|
||||||
|
const mcpNames = createMemo(() => Object.keys(sync.data.mcp ?? {}).sort((a, b) => a.localeCompare(b)))
|
||||||
|
const mcpStatus = (name: string) => sync.data.mcp?.[name]?.status
|
||||||
|
const mcpConnected = createMemo(() => mcpNames().filter((name) => mcpStatus(name) === "connected").length)
|
||||||
|
const lspItems = createMemo(() => sync.data.lsp ?? [])
|
||||||
|
const lspCount = createMemo(() => lspItems().length)
|
||||||
|
const plugins = createMemo(() =>
|
||||||
|
(sync.data.config.plugin ?? []).map((item) => (typeof item === "string" ? item : item[0])),
|
||||||
|
)
|
||||||
|
const pluginCount = createMemo(() => plugins().length)
|
||||||
|
const pluginEmpty = createMemo(() => pluginEmptyMessage(language.t("dialog.plugins.empty"), "opencode.json"))
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div class="flex items-center gap-1 w-[360px] rounded-xl shadow-[var(--shadow-lg-border-base)]">
|
||||||
|
<Tabs
|
||||||
|
aria-label={language.t("status.popover.ariaLabel")}
|
||||||
|
class="tabs bg-background-strong rounded-xl overflow-hidden"
|
||||||
|
data-component="tabs"
|
||||||
|
data-active="servers"
|
||||||
|
defaultValue="servers"
|
||||||
|
variant="alt"
|
||||||
|
>
|
||||||
|
<Tabs.List data-slot="tablist" class="bg-transparent border-b-0 px-4 pt-2 pb-0 gap-4 h-10">
|
||||||
|
<Tabs.Trigger value="servers" data-slot="tab" class="text-12-regular">
|
||||||
|
{sortedServers().length > 0 ? `${sortedServers().length} ` : ""}
|
||||||
|
{language.t("status.popover.tab.servers")}
|
||||||
|
</Tabs.Trigger>
|
||||||
|
<Tabs.Trigger value="mcp" data-slot="tab" class="text-12-regular">
|
||||||
|
{mcpConnected() > 0 ? `${mcpConnected()} ` : ""}
|
||||||
|
{language.t("status.popover.tab.mcp")}
|
||||||
|
</Tabs.Trigger>
|
||||||
|
<Tabs.Trigger value="lsp" data-slot="tab" class="text-12-regular">
|
||||||
|
{lspCount() > 0 ? `${lspCount()} ` : ""}
|
||||||
|
{language.t("status.popover.tab.lsp")}
|
||||||
|
</Tabs.Trigger>
|
||||||
|
<Tabs.Trigger value="plugins" data-slot="tab" class="text-12-regular">
|
||||||
|
{pluginCount() > 0 ? `${pluginCount()} ` : ""}
|
||||||
|
{language.t("status.popover.tab.plugins")}
|
||||||
|
</Tabs.Trigger>
|
||||||
|
</Tabs.List>
|
||||||
|
|
||||||
|
<Tabs.Content value="servers">
|
||||||
|
<div class="flex flex-col px-2 pb-2">
|
||||||
|
<div class="flex flex-col p-3 bg-background-base rounded-sm min-h-14">
|
||||||
|
<For each={sortedServers()}>
|
||||||
|
{(s) => {
|
||||||
|
const key = ServerConnection.key(s)
|
||||||
|
const blocked = () => health[key]?.healthy === false
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="flex items-center gap-2 w-full h-8 pl-3 pr-1.5 py-1.5 rounded-md transition-colors text-left"
|
||||||
|
classList={{
|
||||||
|
"hover:bg-surface-raised-base-hover": !blocked(),
|
||||||
|
"cursor-not-allowed": blocked(),
|
||||||
|
}}
|
||||||
|
aria-disabled={blocked()}
|
||||||
|
onClick={() => {
|
||||||
|
if (blocked()) return
|
||||||
|
navigate("/")
|
||||||
|
queueMicrotask(() => server.setActive(key))
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<ServerHealthIndicator health={health[key]} />
|
||||||
|
<ServerRow
|
||||||
|
conn={s}
|
||||||
|
dimmed={blocked()}
|
||||||
|
status={health[key]}
|
||||||
|
class="flex items-center gap-2 w-full min-w-0"
|
||||||
|
nameClass="text-14-regular text-text-base truncate"
|
||||||
|
versionClass="text-12-regular text-text-weak truncate"
|
||||||
|
badge={
|
||||||
|
<Show when={key === defaultServer.key()}>
|
||||||
|
<span class="text-11-regular text-text-base bg-surface-base px-1.5 py-0.5 rounded-md">
|
||||||
|
{language.t("common.default")}
|
||||||
|
</span>
|
||||||
|
</Show>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<div class="flex-1" />
|
||||||
|
<Show when={server.current && key === ServerConnection.key(server.current)}>
|
||||||
|
<Icon name="check" size="small" class="text-icon-weak shrink-0" />
|
||||||
|
</Show>
|
||||||
|
</ServerRow>
|
||||||
|
</button>
|
||||||
|
)
|
||||||
|
}}
|
||||||
|
</For>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
variant="secondary"
|
||||||
|
class="mt-3 self-start h-8 px-3 py-1.5"
|
||||||
|
onClick={() => {
|
||||||
|
const run = ++dialogRun
|
||||||
|
void import("./dialog-select-server").then((x) => {
|
||||||
|
if (dialogDead || dialogRun !== run) return
|
||||||
|
dialog.show(() => <x.DialogSelectServer />, defaultServer.refresh)
|
||||||
|
})
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{language.t("status.popover.action.manageServers")}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Tabs.Content>
|
||||||
|
|
||||||
|
<Tabs.Content value="mcp">
|
||||||
|
<div class="flex flex-col px-2 pb-2">
|
||||||
|
<div class="flex flex-col p-3 bg-background-base rounded-sm min-h-14">
|
||||||
|
<Show
|
||||||
|
when={mcpNames().length > 0}
|
||||||
|
fallback={
|
||||||
|
<div class="text-14-regular text-text-base text-center my-auto">{language.t("dialog.mcp.empty")}</div>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<For each={mcpNames()}>
|
||||||
|
{(name) => {
|
||||||
|
const status = () => mcpStatus(name)
|
||||||
|
const enabled = () => status() === "connected"
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="flex items-center gap-2 w-full h-8 pl-3 pr-2 py-1 rounded-md hover:bg-surface-raised-base-hover transition-colors text-left"
|
||||||
|
onClick={() => {
|
||||||
|
if (toggleMcp.isPending) return
|
||||||
|
toggleMcp.mutate(name)
|
||||||
|
}}
|
||||||
|
disabled={toggleMcp.isPending && toggleMcp.variables === name}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
classList={{
|
||||||
|
"size-1.5 rounded-full shrink-0": true,
|
||||||
|
"bg-icon-success-base": status() === "connected",
|
||||||
|
"bg-icon-critical-base": status() === "failed",
|
||||||
|
"bg-border-weak-base": status() === "disabled",
|
||||||
|
"bg-icon-warning-base":
|
||||||
|
status() === "needs_auth" || status() === "needs_client_registration",
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<span class="text-14-regular text-text-base truncate flex-1">{name}</span>
|
||||||
|
<div onClick={(event) => event.stopPropagation()}>
|
||||||
|
<Switch
|
||||||
|
checked={enabled()}
|
||||||
|
disabled={toggleMcp.isPending && toggleMcp.variables === name}
|
||||||
|
onChange={() => {
|
||||||
|
if (toggleMcp.isPending) return
|
||||||
|
toggleMcp.mutate(name)
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
)
|
||||||
|
}}
|
||||||
|
</For>
|
||||||
|
</Show>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Tabs.Content>
|
||||||
|
|
||||||
|
<Tabs.Content value="lsp">
|
||||||
|
<div class="flex flex-col px-2 pb-2">
|
||||||
|
<div class="flex flex-col p-3 bg-background-base rounded-sm min-h-14">
|
||||||
|
<Show
|
||||||
|
when={lspItems().length > 0}
|
||||||
|
fallback={
|
||||||
|
<div class="text-14-regular text-text-base text-center my-auto">{language.t("dialog.lsp.empty")}</div>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<For each={lspItems()}>
|
||||||
|
{(item) => (
|
||||||
|
<div class="flex items-center gap-2 w-full px-2 py-1">
|
||||||
|
<div
|
||||||
|
classList={{
|
||||||
|
"size-1.5 rounded-full shrink-0": true,
|
||||||
|
"bg-icon-success-base": item.status === "connected",
|
||||||
|
"bg-icon-critical-base": item.status === "error",
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<span class="text-14-regular text-text-base truncate">{item.name || item.id}</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</For>
|
||||||
|
</Show>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Tabs.Content>
|
||||||
|
|
||||||
|
<Tabs.Content value="plugins">
|
||||||
|
<div class="flex flex-col px-2 pb-2">
|
||||||
|
<div class="flex flex-col p-3 bg-background-base rounded-sm min-h-14">
|
||||||
|
<Show
|
||||||
|
when={plugins().length > 0}
|
||||||
|
fallback={<div class="text-14-regular text-text-base text-center my-auto">{pluginEmpty()}</div>}
|
||||||
|
>
|
||||||
|
<For each={plugins()}>
|
||||||
|
{(plugin) => (
|
||||||
|
<div class="flex items-center gap-2 w-full px-2 py-1">
|
||||||
|
<div class="size-1.5 rounded-full shrink-0 bg-icon-success-base" />
|
||||||
|
<span class="text-14-regular text-text-base truncate">{plugin}</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</For>
|
||||||
|
</Show>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Tabs.Content>
|
||||||
|
</Tabs>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -1,203 +1,24 @@
|
|||||||
import { Button } from "@opencode-ai/ui/button"
|
import { Button } from "@opencode-ai/ui/button"
|
||||||
import { useDialog } from "@opencode-ai/ui/context/dialog"
|
|
||||||
import { Icon } from "@opencode-ai/ui/icon"
|
import { Icon } from "@opencode-ai/ui/icon"
|
||||||
import { Popover } from "@opencode-ai/ui/popover"
|
import { Popover } from "@opencode-ai/ui/popover"
|
||||||
import { Switch } from "@opencode-ai/ui/switch"
|
import { Suspense, createMemo, createSignal, lazy, Show } from "solid-js"
|
||||||
import { Tabs } from "@opencode-ai/ui/tabs"
|
|
||||||
import { showToast } from "@opencode-ai/ui/toast"
|
|
||||||
import { useNavigate } from "@solidjs/router"
|
|
||||||
import { type Accessor, createEffect, createMemo, createSignal, For, type JSXElement, onCleanup, Show } from "solid-js"
|
|
||||||
import { createStore, reconcile } from "solid-js/store"
|
|
||||||
import { ServerHealthIndicator, ServerRow } from "@/components/server/server-row"
|
|
||||||
import { useLanguage } from "@/context/language"
|
import { useLanguage } from "@/context/language"
|
||||||
import { usePlatform } from "@/context/platform"
|
import { useServer } from "@/context/server"
|
||||||
import { useSDK } from "@/context/sdk"
|
|
||||||
import { normalizeServerUrl, ServerConnection, useServer } from "@/context/server"
|
|
||||||
import { useSync } from "@/context/sync"
|
import { useSync } from "@/context/sync"
|
||||||
import { useCheckServerHealth, type ServerHealth } from "@/utils/server-health"
|
|
||||||
import { DialogSelectServer } from "./dialog-select-server"
|
|
||||||
|
|
||||||
const pollMs = 10_000
|
const Body = lazy(() => import("./status-popover-body").then((x) => ({ default: x.StatusPopoverBody })))
|
||||||
|
|
||||||
const pluginEmptyMessage = (value: string, file: string): JSXElement => {
|
|
||||||
const parts = value.split(file)
|
|
||||||
if (parts.length === 1) return value
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
{parts[0]}
|
|
||||||
<code class="bg-surface-raised-base px-1.5 py-0.5 rounded-sm text-text-base">{file}</code>
|
|
||||||
{parts.slice(1).join(file)}
|
|
||||||
</>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
const listServersByHealth = (
|
|
||||||
list: ServerConnection.Any[],
|
|
||||||
active: ServerConnection.Key | undefined,
|
|
||||||
status: Record<ServerConnection.Key, ServerHealth | undefined>,
|
|
||||||
) => {
|
|
||||||
if (!list.length) return list
|
|
||||||
const order = new Map(list.map((url, index) => [url, index] as const))
|
|
||||||
const rank = (value?: ServerHealth) => {
|
|
||||||
if (value?.healthy === true) return 0
|
|
||||||
if (value?.healthy === false) return 2
|
|
||||||
return 1
|
|
||||||
}
|
|
||||||
|
|
||||||
return list.slice().sort((a, b) => {
|
|
||||||
if (ServerConnection.key(a) === active) return -1
|
|
||||||
if (ServerConnection.key(b) === active) return 1
|
|
||||||
const diff = rank(status[ServerConnection.key(a)]) - rank(status[ServerConnection.key(b)])
|
|
||||||
if (diff !== 0) return diff
|
|
||||||
return (order.get(a) ?? 0) - (order.get(b) ?? 0)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
const useServerHealth = (servers: Accessor<ServerConnection.Any[]>) => {
|
|
||||||
const checkServerHealth = useCheckServerHealth()
|
|
||||||
const [status, setStatus] = createStore({} as Record<ServerConnection.Key, ServerHealth | undefined>)
|
|
||||||
|
|
||||||
createEffect(() => {
|
|
||||||
const list = servers()
|
|
||||||
let dead = false
|
|
||||||
|
|
||||||
const refresh = async () => {
|
|
||||||
const results: Record<string, ServerHealth> = {}
|
|
||||||
await Promise.all(
|
|
||||||
list.map(async (conn) => {
|
|
||||||
results[ServerConnection.key(conn)] = await checkServerHealth(conn.http)
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
if (dead) return
|
|
||||||
setStatus(reconcile(results))
|
|
||||||
}
|
|
||||||
|
|
||||||
void refresh()
|
|
||||||
const id = setInterval(() => void refresh(), pollMs)
|
|
||||||
onCleanup(() => {
|
|
||||||
dead = true
|
|
||||||
clearInterval(id)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
return status
|
|
||||||
}
|
|
||||||
|
|
||||||
const useDefaultServerKey = (
|
|
||||||
get: (() => string | Promise<string | null | undefined> | null | undefined) | undefined,
|
|
||||||
) => {
|
|
||||||
const [state, setState] = createStore({
|
|
||||||
url: undefined as string | undefined,
|
|
||||||
tick: 0,
|
|
||||||
})
|
|
||||||
|
|
||||||
createEffect(() => {
|
|
||||||
state.tick
|
|
||||||
let dead = false
|
|
||||||
const result = get?.()
|
|
||||||
if (!result) {
|
|
||||||
setState("url", undefined)
|
|
||||||
onCleanup(() => {
|
|
||||||
dead = true
|
|
||||||
})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if (result instanceof Promise) {
|
|
||||||
void result.then((next) => {
|
|
||||||
if (dead) return
|
|
||||||
setState("url", next ? normalizeServerUrl(next) : undefined)
|
|
||||||
})
|
|
||||||
onCleanup(() => {
|
|
||||||
dead = true
|
|
||||||
})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
setState("url", normalizeServerUrl(result))
|
|
||||||
onCleanup(() => {
|
|
||||||
dead = true
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
return {
|
|
||||||
key: () => {
|
|
||||||
const u = state.url
|
|
||||||
if (!u) return
|
|
||||||
return ServerConnection.key({ type: "http", http: { url: u } })
|
|
||||||
},
|
|
||||||
refresh: () => setState("tick", (value) => value + 1),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const useMcpToggle = (input: {
|
|
||||||
sync: ReturnType<typeof useSync>
|
|
||||||
sdk: ReturnType<typeof useSDK>
|
|
||||||
language: ReturnType<typeof useLanguage>
|
|
||||||
}) => {
|
|
||||||
const [loading, setLoading] = createSignal<string | null>(null)
|
|
||||||
|
|
||||||
const toggle = async (name: string) => {
|
|
||||||
if (loading()) return
|
|
||||||
setLoading(name)
|
|
||||||
|
|
||||||
try {
|
|
||||||
const status = input.sync.data.mcp[name]
|
|
||||||
await (status?.status === "connected"
|
|
||||||
? input.sdk.client.mcp.disconnect({ name })
|
|
||||||
: input.sdk.client.mcp.connect({ name }))
|
|
||||||
const result = await input.sdk.client.mcp.status()
|
|
||||||
if (result.data) input.sync.set("mcp", result.data)
|
|
||||||
} catch (err) {
|
|
||||||
showToast({
|
|
||||||
variant: "error",
|
|
||||||
title: input.language.t("common.requestFailed"),
|
|
||||||
description: err instanceof Error ? err.message : String(err),
|
|
||||||
})
|
|
||||||
} finally {
|
|
||||||
setLoading(null)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return { loading, toggle }
|
|
||||||
}
|
|
||||||
|
|
||||||
export function StatusPopover() {
|
export function StatusPopover() {
|
||||||
const sync = useSync()
|
|
||||||
const sdk = useSDK()
|
|
||||||
const server = useServer()
|
|
||||||
const platform = usePlatform()
|
|
||||||
const dialog = useDialog()
|
|
||||||
const language = useLanguage()
|
const language = useLanguage()
|
||||||
const navigate = useNavigate()
|
const server = useServer()
|
||||||
|
const sync = useSync()
|
||||||
const [shown, setShown] = createSignal(false)
|
const [shown, setShown] = createSignal(false)
|
||||||
const servers = createMemo(() => {
|
const ready = createMemo(() => server.healthy() === false || sync.data.mcp_ready)
|
||||||
const current = server.current
|
const healthy = createMemo(() => {
|
||||||
const list = server.list
|
|
||||||
if (!current) return list
|
|
||||||
if (list.every((item) => ServerConnection.key(item) !== ServerConnection.key(current))) return [current, ...list]
|
|
||||||
return [current, ...list.filter((item) => ServerConnection.key(item) !== ServerConnection.key(current))]
|
|
||||||
})
|
|
||||||
const health = useServerHealth(servers)
|
|
||||||
const sortedServers = createMemo(() => listServersByHealth(servers(), server.key, health))
|
|
||||||
const mcp = useMcpToggle({ sync, sdk, language })
|
|
||||||
const defaultServer = useDefaultServerKey(platform.getDefaultServer)
|
|
||||||
const mcpNames = createMemo(() => Object.keys(sync.data.mcp ?? {}).sort((a, b) => a.localeCompare(b)))
|
|
||||||
const mcpStatus = (name: string) => sync.data.mcp?.[name]?.status
|
|
||||||
const mcpConnected = createMemo(() => mcpNames().filter((name) => mcpStatus(name) === "connected").length)
|
|
||||||
const lspItems = createMemo(() => sync.data.lsp ?? [])
|
|
||||||
const lspCount = createMemo(() => lspItems().length)
|
|
||||||
const plugins = createMemo(() => sync.data.config.plugin ?? [])
|
|
||||||
const pluginCount = createMemo(() => plugins().length)
|
|
||||||
const pluginEmpty = createMemo(() => pluginEmptyMessage(language.t("dialog.plugins.empty"), "opencode.json"))
|
|
||||||
const overallHealthy = createMemo(() => {
|
|
||||||
const serverHealthy = server.healthy() === true
|
const serverHealthy = server.healthy() === true
|
||||||
const anyMcpIssue = mcpNames().some((name) => {
|
const mcp = Object.values(sync.data.mcp ?? {})
|
||||||
const status = mcpStatus(name)
|
const issue = mcp.some((item) => item.status !== "connected" && item.status !== "disabled")
|
||||||
return status !== "connected" && status !== "disabled"
|
return serverHealthy && !issue
|
||||||
})
|
|
||||||
return serverHealthy && !anyMcpIssue
|
|
||||||
})
|
})
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -219,9 +40,9 @@ export function StatusPopover() {
|
|||||||
<div
|
<div
|
||||||
classList={{
|
classList={{
|
||||||
"absolute -top-px -right-px size-1.5 rounded-full": true,
|
"absolute -top-px -right-px size-1.5 rounded-full": true,
|
||||||
"bg-icon-success-base": overallHealthy(),
|
"bg-icon-success-base": ready() && healthy(),
|
||||||
"bg-icon-critical-base": !overallHealthy() && server.healthy() !== undefined,
|
"bg-icon-critical-base": server.healthy() === false || (ready() && !healthy()),
|
||||||
"bg-border-weak-base": server.healthy() === undefined,
|
"bg-border-weak-base": server.healthy() === undefined || !ready(),
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@@ -231,193 +52,15 @@ export function StatusPopover() {
|
|||||||
placement="bottom-end"
|
placement="bottom-end"
|
||||||
shift={-168}
|
shift={-168}
|
||||||
>
|
>
|
||||||
<div class="flex items-center gap-1 w-[360px] rounded-xl shadow-[var(--shadow-lg-border-base)]">
|
<Show when={shown()}>
|
||||||
<Tabs
|
<Suspense
|
||||||
aria-label={language.t("status.popover.ariaLabel")}
|
fallback={
|
||||||
class="tabs bg-background-strong rounded-xl overflow-hidden"
|
<div class="w-[360px] h-14 rounded-xl bg-background-strong shadow-[var(--shadow-lg-border-base)]" />
|
||||||
data-component="tabs"
|
}
|
||||||
data-active="servers"
|
|
||||||
defaultValue="servers"
|
|
||||||
variant="alt"
|
|
||||||
>
|
>
|
||||||
<Tabs.List data-slot="tablist" class="bg-transparent border-b-0 px-4 pt-2 pb-0 gap-4 h-10">
|
<Body shown={shown} />
|
||||||
<Tabs.Trigger value="servers" data-slot="tab" class="text-12-regular">
|
</Suspense>
|
||||||
{sortedServers().length > 0 ? `${sortedServers().length} ` : ""}
|
</Show>
|
||||||
{language.t("status.popover.tab.servers")}
|
|
||||||
</Tabs.Trigger>
|
|
||||||
<Tabs.Trigger value="mcp" data-slot="tab" class="text-12-regular">
|
|
||||||
{mcpConnected() > 0 ? `${mcpConnected()} ` : ""}
|
|
||||||
{language.t("status.popover.tab.mcp")}
|
|
||||||
</Tabs.Trigger>
|
|
||||||
<Tabs.Trigger value="lsp" data-slot="tab" class="text-12-regular">
|
|
||||||
{lspCount() > 0 ? `${lspCount()} ` : ""}
|
|
||||||
{language.t("status.popover.tab.lsp")}
|
|
||||||
</Tabs.Trigger>
|
|
||||||
<Tabs.Trigger value="plugins" data-slot="tab" class="text-12-regular">
|
|
||||||
{pluginCount() > 0 ? `${pluginCount()} ` : ""}
|
|
||||||
{language.t("status.popover.tab.plugins")}
|
|
||||||
</Tabs.Trigger>
|
|
||||||
</Tabs.List>
|
|
||||||
|
|
||||||
<Tabs.Content value="servers">
|
|
||||||
<div class="flex flex-col px-2 pb-2">
|
|
||||||
<div class="flex flex-col p-3 bg-background-base rounded-sm min-h-14">
|
|
||||||
<For each={sortedServers()}>
|
|
||||||
{(s) => {
|
|
||||||
const key = ServerConnection.key(s)
|
|
||||||
const isBlocked = () => health[key]?.healthy === false
|
|
||||||
return (
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
class="flex items-center gap-2 w-full h-8 pl-3 pr-1.5 py-1.5 rounded-md transition-colors text-left"
|
|
||||||
classList={{
|
|
||||||
"hover:bg-surface-raised-base-hover": !isBlocked(),
|
|
||||||
"cursor-not-allowed": isBlocked(),
|
|
||||||
}}
|
|
||||||
aria-disabled={isBlocked()}
|
|
||||||
onClick={() => {
|
|
||||||
if (isBlocked()) return
|
|
||||||
server.setActive(key)
|
|
||||||
navigate("/")
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<ServerHealthIndicator health={health[key]} />
|
|
||||||
<ServerRow
|
|
||||||
conn={s}
|
|
||||||
dimmed={isBlocked()}
|
|
||||||
status={health[key]}
|
|
||||||
class="flex items-center gap-2 w-full min-w-0"
|
|
||||||
nameClass="text-14-regular text-text-base truncate"
|
|
||||||
versionClass="text-12-regular text-text-weak truncate"
|
|
||||||
badge={
|
|
||||||
<Show when={key === defaultServer.key()}>
|
|
||||||
<span class="text-11-regular text-text-base bg-surface-base px-1.5 py-0.5 rounded-md">
|
|
||||||
{language.t("common.default")}
|
|
||||||
</span>
|
|
||||||
</Show>
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<div class="flex-1" />
|
|
||||||
<Show when={server.current && key === ServerConnection.key(server.current)}>
|
|
||||||
<Icon name="check" size="small" class="text-icon-weak shrink-0" />
|
|
||||||
</Show>
|
|
||||||
</ServerRow>
|
|
||||||
</button>
|
|
||||||
)
|
|
||||||
}}
|
|
||||||
</For>
|
|
||||||
|
|
||||||
<Button
|
|
||||||
variant="secondary"
|
|
||||||
class="mt-3 self-start h-8 px-3 py-1.5"
|
|
||||||
onClick={() => dialog.show(() => <DialogSelectServer />, defaultServer.refresh)}
|
|
||||||
>
|
|
||||||
{language.t("status.popover.action.manageServers")}
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Tabs.Content>
|
|
||||||
|
|
||||||
<Tabs.Content value="mcp">
|
|
||||||
<div class="flex flex-col px-2 pb-2">
|
|
||||||
<div class="flex flex-col p-3 bg-background-base rounded-sm min-h-14">
|
|
||||||
<Show
|
|
||||||
when={mcpNames().length > 0}
|
|
||||||
fallback={
|
|
||||||
<div class="text-14-regular text-text-base text-center my-auto">
|
|
||||||
{language.t("dialog.mcp.empty")}
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<For each={mcpNames()}>
|
|
||||||
{(name) => {
|
|
||||||
const status = () => mcpStatus(name)
|
|
||||||
const enabled = () => status() === "connected"
|
|
||||||
return (
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
class="flex items-center gap-2 w-full h-8 pl-3 pr-2 py-1 rounded-md hover:bg-surface-raised-base-hover transition-colors text-left"
|
|
||||||
onClick={() => mcp.toggle(name)}
|
|
||||||
disabled={mcp.loading() === name}
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
classList={{
|
|
||||||
"size-1.5 rounded-full shrink-0": true,
|
|
||||||
"bg-icon-success-base": status() === "connected",
|
|
||||||
"bg-icon-critical-base": status() === "failed",
|
|
||||||
"bg-border-weak-base": status() === "disabled",
|
|
||||||
"bg-icon-warning-base":
|
|
||||||
status() === "needs_auth" || status() === "needs_client_registration",
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<span class="text-14-regular text-text-base truncate flex-1">{name}</span>
|
|
||||||
<div onClick={(event) => event.stopPropagation()}>
|
|
||||||
<Switch
|
|
||||||
checked={enabled()}
|
|
||||||
disabled={mcp.loading() === name}
|
|
||||||
onChange={() => mcp.toggle(name)}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</button>
|
|
||||||
)
|
|
||||||
}}
|
|
||||||
</For>
|
|
||||||
</Show>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Tabs.Content>
|
|
||||||
|
|
||||||
<Tabs.Content value="lsp">
|
|
||||||
<div class="flex flex-col px-2 pb-2">
|
|
||||||
<div class="flex flex-col p-3 bg-background-base rounded-sm min-h-14">
|
|
||||||
<Show
|
|
||||||
when={lspItems().length > 0}
|
|
||||||
fallback={
|
|
||||||
<div class="text-14-regular text-text-base text-center my-auto">
|
|
||||||
{language.t("dialog.lsp.empty")}
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<For each={lspItems()}>
|
|
||||||
{(item) => (
|
|
||||||
<div class="flex items-center gap-2 w-full px-2 py-1">
|
|
||||||
<div
|
|
||||||
classList={{
|
|
||||||
"size-1.5 rounded-full shrink-0": true,
|
|
||||||
"bg-icon-success-base": item.status === "connected",
|
|
||||||
"bg-icon-critical-base": item.status === "error",
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<span class="text-14-regular text-text-base truncate">{item.name || item.id}</span>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</For>
|
|
||||||
</Show>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Tabs.Content>
|
|
||||||
|
|
||||||
<Tabs.Content value="plugins">
|
|
||||||
<div class="flex flex-col px-2 pb-2">
|
|
||||||
<div class="flex flex-col p-3 bg-background-base rounded-sm min-h-14">
|
|
||||||
<Show
|
|
||||||
when={plugins().length > 0}
|
|
||||||
fallback={<div class="text-14-regular text-text-base text-center my-auto">{pluginEmpty()}</div>}
|
|
||||||
>
|
|
||||||
<For each={plugins()}>
|
|
||||||
{(plugin) => (
|
|
||||||
<div class="flex items-center gap-2 w-full px-2 py-1">
|
|
||||||
<div class="size-1.5 rounded-full shrink-0 bg-icon-success-base" />
|
|
||||||
<span class="text-14-regular text-text-base truncate">{plugin}</span>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</For>
|
|
||||||
</Show>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Tabs.Content>
|
|
||||||
</Tabs>
|
|
||||||
</div>
|
|
||||||
</Popover>
|
</Popover>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,7 @@
|
|||||||
import { type HexColor, resolveThemeVariant, useTheme, withAlpha } from "@opencode-ai/ui/theme"
|
import { withAlpha } from "@opencode-ai/ui/theme/color"
|
||||||
|
import { useTheme } from "@opencode-ai/ui/theme/context"
|
||||||
|
import { resolveThemeVariant } from "@opencode-ai/ui/theme/resolve"
|
||||||
|
import type { HexColor } from "@opencode-ai/ui/theme/types"
|
||||||
import { showToast } from "@opencode-ai/ui/toast"
|
import { showToast } from "@opencode-ai/ui/toast"
|
||||||
import type { FitAddon, Ghostty, Terminal as Term } from "ghostty-web"
|
import type { FitAddon, Ghostty, Terminal as Term } from "ghostty-web"
|
||||||
import { type ComponentProps, createEffect, createMemo, onCleanup, onMount, splitProps } from "solid-js"
|
import { type ComponentProps, createEffect, createMemo, onCleanup, onMount, splitProps } from "solid-js"
|
||||||
@@ -65,14 +68,11 @@ const debugTerminal = (...values: unknown[]) => {
|
|||||||
console.debug("[terminal]", ...values)
|
console.debug("[terminal]", ...values)
|
||||||
}
|
}
|
||||||
|
|
||||||
const errorStatus = (err: unknown) => {
|
const errorName = (err: unknown) => {
|
||||||
if (!err || typeof err !== "object") return
|
if (!err || typeof err !== "object") return
|
||||||
if (!("data" in err)) return
|
if (!("name" in err)) return
|
||||||
const data = err.data
|
const errorName = err.name
|
||||||
if (!data || typeof data !== "object") return
|
return typeof errorName === "string" ? errorName : undefined
|
||||||
if (!("statusCode" in data)) return
|
|
||||||
const status = data.statusCode
|
|
||||||
return typeof status === "number" ? status : undefined
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const useTerminalUiBindings = (input: {
|
const useTerminalUiBindings = (input: {
|
||||||
@@ -168,6 +168,12 @@ export const Terminal = (props: TerminalProps) => {
|
|||||||
const theme = useTheme()
|
const theme = useTheme()
|
||||||
const language = useLanguage()
|
const language = useLanguage()
|
||||||
const server = useServer()
|
const server = useServer()
|
||||||
|
const directory = sdk.directory
|
||||||
|
const client = sdk.client
|
||||||
|
const url = sdk.url
|
||||||
|
const auth = server.current?.http
|
||||||
|
const username = auth?.username ?? "opencode"
|
||||||
|
const password = auth?.password ?? ""
|
||||||
let container!: HTMLDivElement
|
let container!: HTMLDivElement
|
||||||
const [local, others] = splitProps(props, ["pty", "class", "classList", "autoFocus", "onConnect", "onConnectError"])
|
const [local, others] = splitProps(props, ["pty", "class", "classList", "autoFocus", "onConnect", "onConnectError"])
|
||||||
const id = local.pty.id
|
const id = local.pty.id
|
||||||
@@ -218,7 +224,7 @@ export const Terminal = (props: TerminalProps) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const pushSize = (cols: number, rows: number) => {
|
const pushSize = (cols: number, rows: number) => {
|
||||||
return sdk.client.pty
|
return client.pty
|
||||||
.update({
|
.update({
|
||||||
ptyID: id,
|
ptyID: id,
|
||||||
size: { cols, rows },
|
size: { cols, rows },
|
||||||
@@ -477,11 +483,11 @@ export const Terminal = (props: TerminalProps) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const gone = () =>
|
const gone = () =>
|
||||||
sdk.client.pty
|
client.pty
|
||||||
.get({ ptyID: id })
|
.get({ ptyID: id })
|
||||||
.then(() => false)
|
.then(() => false)
|
||||||
.catch((err) => {
|
.catch((err) => {
|
||||||
if (errorStatus(err) === 404) return true
|
if (errorName(err) === "NotFoundError") return true
|
||||||
debugTerminal("failed to inspect terminal session", err)
|
debugTerminal("failed to inspect terminal session", err)
|
||||||
return false
|
return false
|
||||||
})
|
})
|
||||||
@@ -509,14 +515,14 @@ export const Terminal = (props: TerminalProps) => {
|
|||||||
if (disposed) return
|
if (disposed) return
|
||||||
drop?.()
|
drop?.()
|
||||||
|
|
||||||
const url = new URL(sdk.url + `/pty/${id}/connect`)
|
const next = new URL(url + `/pty/${id}/connect`)
|
||||||
url.searchParams.set("directory", sdk.directory)
|
next.searchParams.set("directory", directory)
|
||||||
url.searchParams.set("cursor", String(seek))
|
next.searchParams.set("cursor", String(seek))
|
||||||
url.protocol = url.protocol === "https:" ? "wss:" : "ws:"
|
next.protocol = next.protocol === "https:" ? "wss:" : "ws:"
|
||||||
url.username = server.current?.http.username ?? "opencode"
|
next.username = username
|
||||||
url.password = server.current?.http.password ?? ""
|
next.password = password
|
||||||
|
|
||||||
const socket = new WebSocket(url)
|
const socket = new WebSocket(next)
|
||||||
socket.binaryType = "arraybuffer"
|
socket.binaryType = "arraybuffer"
|
||||||
ws = socket
|
ws = socket
|
||||||
|
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import { IconButton } from "@opencode-ai/ui/icon-button"
|
|||||||
import { Icon } from "@opencode-ai/ui/icon"
|
import { Icon } from "@opencode-ai/ui/icon"
|
||||||
import { Button } from "@opencode-ai/ui/button"
|
import { Button } from "@opencode-ai/ui/button"
|
||||||
import { Tooltip, TooltipKeybind } from "@opencode-ai/ui/tooltip"
|
import { Tooltip, TooltipKeybind } from "@opencode-ai/ui/tooltip"
|
||||||
import { useTheme } from "@opencode-ai/ui/theme"
|
import { useTheme } from "@opencode-ai/ui/theme/context"
|
||||||
|
|
||||||
import { useLayout } from "@/context/layout"
|
import { useLayout } from "@/context/layout"
|
||||||
import { usePlatform } from "@/context/platform"
|
import { usePlatform } from "@/context/platform"
|
||||||
@@ -77,6 +77,7 @@ export function Titlebar() {
|
|||||||
|
|
||||||
const canBack = createMemo(() => history.index > 0)
|
const canBack = createMemo(() => history.index > 0)
|
||||||
const canForward = createMemo(() => history.index < history.stack.length - 1)
|
const canForward = createMemo(() => history.index < history.stack.length - 1)
|
||||||
|
const hasProjects = createMemo(() => layout.projects.list().length > 0)
|
||||||
|
|
||||||
const back = () => {
|
const back = () => {
|
||||||
const next = backPath(history)
|
const next = backPath(history)
|
||||||
@@ -251,36 +252,38 @@ export function Titlebar() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Show>
|
</Show>
|
||||||
<div
|
<Show when={hasProjects()}>
|
||||||
class="flex items-center gap-0 transition-transform"
|
<div
|
||||||
classList={{
|
class="flex items-center gap-0 transition-transform"
|
||||||
"translate-x-0": !layout.sidebar.opened(),
|
classList={{
|
||||||
"-translate-x-[36px]": layout.sidebar.opened(),
|
"translate-x-0": !layout.sidebar.opened(),
|
||||||
"duration-180 ease-out": !layout.sidebar.opened(),
|
"-translate-x-[36px]": layout.sidebar.opened(),
|
||||||
"duration-180 ease-in": layout.sidebar.opened(),
|
"duration-180 ease-out": !layout.sidebar.opened(),
|
||||||
}}
|
"duration-180 ease-in": layout.sidebar.opened(),
|
||||||
>
|
}}
|
||||||
<Tooltip placement="bottom" value={language.t("common.goBack")} openDelay={2000}>
|
>
|
||||||
<Button
|
<Tooltip placement="bottom" value={language.t("common.goBack")} openDelay={2000}>
|
||||||
variant="ghost"
|
<Button
|
||||||
icon="chevron-left"
|
variant="ghost"
|
||||||
class="titlebar-icon w-6 h-6 p-0 box-border"
|
icon="chevron-left"
|
||||||
disabled={!canBack()}
|
class="titlebar-icon w-6 h-6 p-0 box-border"
|
||||||
onClick={back}
|
disabled={!canBack()}
|
||||||
aria-label={language.t("common.goBack")}
|
onClick={back}
|
||||||
/>
|
aria-label={language.t("common.goBack")}
|
||||||
</Tooltip>
|
/>
|
||||||
<Tooltip placement="bottom" value={language.t("common.goForward")} openDelay={2000}>
|
</Tooltip>
|
||||||
<Button
|
<Tooltip placement="bottom" value={language.t("common.goForward")} openDelay={2000}>
|
||||||
variant="ghost"
|
<Button
|
||||||
icon="chevron-right"
|
variant="ghost"
|
||||||
class="titlebar-icon w-6 h-6 p-0 box-border"
|
icon="chevron-right"
|
||||||
disabled={!canForward()}
|
class="titlebar-icon w-6 h-6 p-0 box-border"
|
||||||
onClick={forward}
|
disabled={!canForward()}
|
||||||
aria-label={language.t("common.goForward")}
|
onClick={forward}
|
||||||
/>
|
aria-label={language.t("common.goForward")}
|
||||||
</Tooltip>
|
/>
|
||||||
</div>
|
</Tooltip>
|
||||||
|
</div>
|
||||||
|
</Show>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<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" />
|
||||||
|
|||||||
@@ -0,0 +1,89 @@
|
|||||||
|
export const ACCEPTED_IMAGE_TYPES = ["image/png", "image/jpeg", "image/gif", "image/webp"]
|
||||||
|
|
||||||
|
export const ACCEPTED_FILE_TYPES = [
|
||||||
|
...ACCEPTED_IMAGE_TYPES,
|
||||||
|
"application/pdf",
|
||||||
|
"text/*",
|
||||||
|
"application/json",
|
||||||
|
"application/ld+json",
|
||||||
|
"application/toml",
|
||||||
|
"application/x-toml",
|
||||||
|
"application/x-yaml",
|
||||||
|
"application/xml",
|
||||||
|
"application/yaml",
|
||||||
|
".c",
|
||||||
|
".cc",
|
||||||
|
".cjs",
|
||||||
|
".conf",
|
||||||
|
".cpp",
|
||||||
|
".css",
|
||||||
|
".csv",
|
||||||
|
".cts",
|
||||||
|
".env",
|
||||||
|
".go",
|
||||||
|
".gql",
|
||||||
|
".graphql",
|
||||||
|
".h",
|
||||||
|
".hh",
|
||||||
|
".hpp",
|
||||||
|
".htm",
|
||||||
|
".html",
|
||||||
|
".ini",
|
||||||
|
".java",
|
||||||
|
".js",
|
||||||
|
".json",
|
||||||
|
".jsx",
|
||||||
|
".log",
|
||||||
|
".md",
|
||||||
|
".mdx",
|
||||||
|
".mjs",
|
||||||
|
".mts",
|
||||||
|
".py",
|
||||||
|
".rb",
|
||||||
|
".rs",
|
||||||
|
".sass",
|
||||||
|
".scss",
|
||||||
|
".sh",
|
||||||
|
".sql",
|
||||||
|
".toml",
|
||||||
|
".ts",
|
||||||
|
".tsx",
|
||||||
|
".txt",
|
||||||
|
".xml",
|
||||||
|
".yaml",
|
||||||
|
".yml",
|
||||||
|
".zsh",
|
||||||
|
]
|
||||||
|
|
||||||
|
const MIME_EXT = new Map([
|
||||||
|
["image/png", "png"],
|
||||||
|
["image/jpeg", "jpg"],
|
||||||
|
["image/gif", "gif"],
|
||||||
|
["image/webp", "webp"],
|
||||||
|
["application/pdf", "pdf"],
|
||||||
|
["application/json", "json"],
|
||||||
|
["application/ld+json", "jsonld"],
|
||||||
|
["application/toml", "toml"],
|
||||||
|
["application/x-toml", "toml"],
|
||||||
|
["application/x-yaml", "yaml"],
|
||||||
|
["application/xml", "xml"],
|
||||||
|
["application/yaml", "yaml"],
|
||||||
|
])
|
||||||
|
|
||||||
|
const TEXT_EXT = ["txt", "text", "md", "markdown", "log", "csv"]
|
||||||
|
|
||||||
|
export const ACCEPTED_FILE_EXTENSIONS = Array.from(
|
||||||
|
new Set(
|
||||||
|
ACCEPTED_FILE_TYPES.flatMap((item) => {
|
||||||
|
if (item.startsWith(".")) return [item.slice(1)]
|
||||||
|
if (item === "text/*") return TEXT_EXT
|
||||||
|
const out = MIME_EXT.get(item)
|
||||||
|
return out ? [out] : []
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
).sort()
|
||||||
|
|
||||||
|
export function filePickerFilters(ext?: string[]) {
|
||||||
|
if (!ext || ext.length === 0) return undefined
|
||||||
|
return [{ name: "Files", extensions: ext }]
|
||||||
|
}
|
||||||
@@ -32,6 +32,25 @@ describe("command keybind helpers", () => {
|
|||||||
expect(matchKeybind(keybinds, new KeyboardEvent("keydown", { key: ",", ctrlKey: true, altKey: true }))).toBe(false)
|
expect(matchKeybind(keybinds, new KeyboardEvent("keydown", { key: ",", ctrlKey: true, altKey: true }))).toBe(false)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
test("matchKeybind supports bracket keys", () => {
|
||||||
|
const keybinds = parseKeybind("mod+alt+[, mod+alt+]")
|
||||||
|
const prev = keybinds[0]
|
||||||
|
const next = keybinds[1]
|
||||||
|
|
||||||
|
expect(
|
||||||
|
matchKeybind(
|
||||||
|
keybinds,
|
||||||
|
new KeyboardEvent("keydown", { key: "[", ctrlKey: prev?.ctrl, metaKey: prev?.meta, altKey: true }),
|
||||||
|
),
|
||||||
|
).toBe(true)
|
||||||
|
expect(
|
||||||
|
matchKeybind(
|
||||||
|
keybinds,
|
||||||
|
new KeyboardEvent("keydown", { key: "]", ctrlKey: next?.ctrl, metaKey: next?.meta, altKey: true }),
|
||||||
|
),
|
||||||
|
).toBe(true)
|
||||||
|
})
|
||||||
|
|
||||||
test("formatKeybind returns human readable output", () => {
|
test("formatKeybind returns human readable output", () => {
|
||||||
const display = formatKeybind("ctrl+alt+arrowup")
|
const display = formatKeybind("ctrl+alt+arrowup")
|
||||||
|
|
||||||
@@ -40,4 +59,11 @@ describe("command keybind helpers", () => {
|
|||||||
expect(display.includes("Alt") || display.includes("⌥")).toBe(true)
|
expect(display.includes("Alt") || display.includes("⌥")).toBe(true)
|
||||||
expect(formatKeybind("none")).toBe("")
|
expect(formatKeybind("none")).toBe("")
|
||||||
})
|
})
|
||||||
|
|
||||||
|
test("formatKeybind prefers the first combo", () => {
|
||||||
|
const display = formatKeybind("mod+k,mod+p")
|
||||||
|
|
||||||
|
expect(display.includes("K") || display.includes("k")).toBe(true)
|
||||||
|
expect(display.includes("P") || display.includes("p")).toBe(false)
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
import { createEffect, createMemo, onCleanup, onMount, type Accessor } from "solid-js"
|
|
||||||
import { createStore } from "solid-js/store"
|
|
||||||
import { createSimpleContext } from "@opencode-ai/ui/context"
|
import { createSimpleContext } from "@opencode-ai/ui/context"
|
||||||
import { useDialog } from "@opencode-ai/ui/context/dialog"
|
import { useDialog } from "@opencode-ai/ui/context/dialog"
|
||||||
import { dict as en } from "@/i18n/en"
|
import { type Accessor, createEffect, createMemo, onCleanup, onMount } from "solid-js"
|
||||||
|
import { createStore } from "solid-js/store"
|
||||||
import { useLanguage } from "@/context/language"
|
import { useLanguage } from "@/context/language"
|
||||||
import { useSettings } from "@/context/settings"
|
import { useSettings } from "@/context/settings"
|
||||||
|
import { dict as en } from "@/i18n/en"
|
||||||
import { Persist, persisted } from "@/utils/persist"
|
import { Persist, persisted } from "@/utils/persist"
|
||||||
|
|
||||||
const IS_MAC = typeof navigator === "object" && /(Mac|iPod|iPhone|iPad)/.test(navigator.platform)
|
const IS_MAC = typeof navigator === "object" && /(Mac|iPod|iPhone|iPad)/.test(navigator.platform)
|
||||||
@@ -238,9 +238,10 @@ export const { use: useCommand, provider: CommandProvider } = createSimpleContex
|
|||||||
})
|
})
|
||||||
const warnedDuplicates = new Set<string>()
|
const warnedDuplicates = new Set<string>()
|
||||||
|
|
||||||
|
type CommandCatalog = Record<string, CommandCatalogItem>
|
||||||
const [catalog, setCatalog, _, catalogReady] = persisted(
|
const [catalog, setCatalog, _, catalogReady] = persisted(
|
||||||
Persist.global("command.catalog.v1"),
|
Persist.global("command.catalog.v1"),
|
||||||
createStore<Record<string, CommandCatalogItem>>({}),
|
createStore<CommandCatalog>({}),
|
||||||
)
|
)
|
||||||
|
|
||||||
const bind = (id: string, def: KeybindConfig | undefined) => {
|
const bind = (id: string, def: KeybindConfig | undefined) => {
|
||||||
@@ -259,7 +260,7 @@ export const { use: useCommand, provider: CommandProvider } = createSimpleContex
|
|||||||
if (seen.has(opt.id)) {
|
if (seen.has(opt.id)) {
|
||||||
if (import.meta.env.DEV && !warnedDuplicates.has(opt.id)) {
|
if (import.meta.env.DEV && !warnedDuplicates.has(opt.id)) {
|
||||||
warnedDuplicates.add(opt.id)
|
warnedDuplicates.add(opt.id)
|
||||||
console.warn(`[command] duplicate command id \"${opt.id}\" registered; keeping first entry`)
|
console.warn(`[command] duplicate command id "${opt.id}" registered; keeping first entry`)
|
||||||
}
|
}
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
@@ -274,16 +275,19 @@ export const { use: useCommand, provider: CommandProvider } = createSimpleContex
|
|||||||
createEffect(() => {
|
createEffect(() => {
|
||||||
if (!catalogReady()) return
|
if (!catalogReady()) return
|
||||||
|
|
||||||
for (const opt of registered()) {
|
setCatalog(
|
||||||
const id = actionId(opt.id)
|
registered().reduce((acc, opt) => {
|
||||||
setCatalog(id, {
|
const id = actionId(opt.id)
|
||||||
title: opt.title,
|
acc[id] = {
|
||||||
description: opt.description,
|
title: opt.title,
|
||||||
category: opt.category,
|
description: opt.description,
|
||||||
keybind: opt.keybind,
|
category: opt.category,
|
||||||
slash: opt.slash,
|
keybind: opt.keybind,
|
||||||
})
|
slash: opt.slash,
|
||||||
}
|
}
|
||||||
|
return acc
|
||||||
|
}, {} as CommandCatalog),
|
||||||
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
const catalogOptions = createMemo(() => Object.entries(catalog).map(([id, meta]) => ({ id, ...meta })))
|
const catalogOptions = createMemo(() => Object.entries(catalog).map(([id, meta]) => ({ id, ...meta })))
|
||||||
|
|||||||
@@ -105,6 +105,8 @@ export const { use: useGlobalSDK, provider: GlobalSDKProvider } = createSimpleCo
|
|||||||
const aborted = (error: unknown) => abortError.safeParse(error).success
|
const aborted = (error: unknown) => abortError.safeParse(error).success
|
||||||
|
|
||||||
let attempt: AbortController | undefined
|
let attempt: AbortController | undefined
|
||||||
|
let run: Promise<void> | undefined
|
||||||
|
let started = false
|
||||||
const HEARTBEAT_TIMEOUT_MS = 15_000
|
const HEARTBEAT_TIMEOUT_MS = 15_000
|
||||||
let lastEventAt = Date.now()
|
let lastEventAt = Date.now()
|
||||||
let heartbeat: ReturnType<typeof setTimeout> | undefined
|
let heartbeat: ReturnType<typeof setTimeout> | undefined
|
||||||
@@ -121,78 +123,93 @@ export const { use: useGlobalSDK, provider: GlobalSDKProvider } = createSimpleCo
|
|||||||
heartbeat = undefined
|
heartbeat = undefined
|
||||||
}
|
}
|
||||||
|
|
||||||
void (async () => {
|
const start = () => {
|
||||||
while (!abort.signal.aborted) {
|
if (started) return run
|
||||||
attempt = new AbortController()
|
started = true
|
||||||
lastEventAt = Date.now()
|
run = (async () => {
|
||||||
const onAbort = () => {
|
while (!abort.signal.aborted && started) {
|
||||||
attempt?.abort()
|
attempt = new AbortController()
|
||||||
}
|
lastEventAt = Date.now()
|
||||||
abort.signal.addEventListener("abort", onAbort)
|
const onAbort = () => {
|
||||||
try {
|
attempt?.abort()
|
||||||
const events = await eventSdk.global.event({
|
}
|
||||||
signal: attempt.signal,
|
abort.signal.addEventListener("abort", onAbort)
|
||||||
onSseError: (error) => {
|
try {
|
||||||
if (aborted(error)) return
|
const events = await eventSdk.global.event({
|
||||||
if (streamErrorLogged) return
|
signal: attempt.signal,
|
||||||
|
onSseError: (error) => {
|
||||||
|
if (aborted(error)) return
|
||||||
|
if (streamErrorLogged) return
|
||||||
|
streamErrorLogged = true
|
||||||
|
console.error("[global-sdk] event stream error", {
|
||||||
|
url: currentServer.http.url,
|
||||||
|
fetch: eventFetch ? "platform" : "webview",
|
||||||
|
error,
|
||||||
|
})
|
||||||
|
},
|
||||||
|
})
|
||||||
|
let yielded = Date.now()
|
||||||
|
resetHeartbeat()
|
||||||
|
for await (const event of events.stream) {
|
||||||
|
resetHeartbeat()
|
||||||
|
streamErrorLogged = false
|
||||||
|
const directory = event.directory ?? "global"
|
||||||
|
const payload = event.payload
|
||||||
|
const k = key(directory, payload)
|
||||||
|
if (k) {
|
||||||
|
const i = coalesced.get(k)
|
||||||
|
if (i !== undefined) {
|
||||||
|
queue[i] = { directory, payload }
|
||||||
|
if (payload.type === "message.part.updated") {
|
||||||
|
const part = payload.properties.part
|
||||||
|
staleDeltas.add(deltaKey(directory, part.messageID, part.id))
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
coalesced.set(k, queue.length)
|
||||||
|
}
|
||||||
|
queue.push({ directory, payload })
|
||||||
|
schedule()
|
||||||
|
|
||||||
|
if (Date.now() - yielded < STREAM_YIELD_MS) continue
|
||||||
|
yielded = Date.now()
|
||||||
|
await wait(0)
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
if (!aborted(error) && !streamErrorLogged) {
|
||||||
streamErrorLogged = true
|
streamErrorLogged = true
|
||||||
console.error("[global-sdk] event stream error", {
|
console.error("[global-sdk] event stream failed", {
|
||||||
url: currentServer.http.url,
|
url: currentServer.http.url,
|
||||||
fetch: eventFetch ? "platform" : "webview",
|
fetch: eventFetch ? "platform" : "webview",
|
||||||
error,
|
error,
|
||||||
})
|
})
|
||||||
},
|
|
||||||
})
|
|
||||||
let yielded = Date.now()
|
|
||||||
resetHeartbeat()
|
|
||||||
for await (const event of events.stream) {
|
|
||||||
resetHeartbeat()
|
|
||||||
streamErrorLogged = false
|
|
||||||
const directory = event.directory ?? "global"
|
|
||||||
const payload = event.payload
|
|
||||||
const k = key(directory, payload)
|
|
||||||
if (k) {
|
|
||||||
const i = coalesced.get(k)
|
|
||||||
if (i !== undefined) {
|
|
||||||
queue[i] = { directory, payload }
|
|
||||||
if (payload.type === "message.part.updated") {
|
|
||||||
const part = payload.properties.part
|
|
||||||
staleDeltas.add(deltaKey(directory, part.messageID, part.id))
|
|
||||||
}
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
coalesced.set(k, queue.length)
|
|
||||||
}
|
}
|
||||||
queue.push({ directory, payload })
|
} finally {
|
||||||
schedule()
|
abort.signal.removeEventListener("abort", onAbort)
|
||||||
|
attempt = undefined
|
||||||
|
clearHeartbeat()
|
||||||
|
}
|
||||||
|
|
||||||
if (Date.now() - yielded < STREAM_YIELD_MS) continue
|
if (abort.signal.aborted || !started) return
|
||||||
yielded = Date.now()
|
await wait(RECONNECT_DELAY_MS)
|
||||||
await wait(0)
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
if (!aborted(error) && !streamErrorLogged) {
|
|
||||||
streamErrorLogged = true
|
|
||||||
console.error("[global-sdk] event stream failed", {
|
|
||||||
url: currentServer.http.url,
|
|
||||||
fetch: eventFetch ? "platform" : "webview",
|
|
||||||
error,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
} finally {
|
|
||||||
abort.signal.removeEventListener("abort", onAbort)
|
|
||||||
attempt = undefined
|
|
||||||
clearHeartbeat()
|
|
||||||
}
|
}
|
||||||
|
})().finally(() => {
|
||||||
|
run = undefined
|
||||||
|
flush()
|
||||||
|
})
|
||||||
|
return run
|
||||||
|
}
|
||||||
|
|
||||||
if (abort.signal.aborted) return
|
const stop = () => {
|
||||||
await wait(RECONNECT_DELAY_MS)
|
started = false
|
||||||
}
|
attempt?.abort()
|
||||||
})().finally(flush)
|
clearHeartbeat()
|
||||||
|
}
|
||||||
|
|
||||||
const onVisibility = () => {
|
const onVisibility = () => {
|
||||||
if (typeof document === "undefined") return
|
if (typeof document === "undefined") return
|
||||||
if (document.visibilityState !== "visible") return
|
if (document.visibilityState !== "visible") return
|
||||||
|
if (!started) return
|
||||||
if (Date.now() - lastEventAt < HEARTBEAT_TIMEOUT_MS) return
|
if (Date.now() - lastEventAt < HEARTBEAT_TIMEOUT_MS) return
|
||||||
attempt?.abort()
|
attempt?.abort()
|
||||||
}
|
}
|
||||||
@@ -204,6 +221,7 @@ export const { use: useGlobalSDK, provider: GlobalSDKProvider } = createSimpleCo
|
|||||||
if (typeof document !== "undefined") {
|
if (typeof document !== "undefined") {
|
||||||
document.removeEventListener("visibilitychange", onVisibility)
|
document.removeEventListener("visibilitychange", onVisibility)
|
||||||
}
|
}
|
||||||
|
stop()
|
||||||
abort.abort()
|
abort.abort()
|
||||||
flush()
|
flush()
|
||||||
})
|
})
|
||||||
@@ -217,7 +235,11 @@ export const { use: useGlobalSDK, provider: GlobalSDKProvider } = createSimpleCo
|
|||||||
return {
|
return {
|
||||||
url: currentServer.http.url,
|
url: currentServer.http.url,
|
||||||
client: sdk,
|
client: sdk,
|
||||||
event: emitter,
|
event: {
|
||||||
|
on: emitter.on.bind(emitter),
|
||||||
|
listen: emitter.listen.bind(emitter),
|
||||||
|
start,
|
||||||
|
},
|
||||||
createClient(opts: Omit<Parameters<typeof createSdkForServer>[0], "server" | "fetch">) {
|
createClient(opts: Omit<Parameters<typeof createSdkForServer>[0], "server" | "fetch">) {
|
||||||
const s = server.current
|
const s = server.current
|
||||||
if (!s) throw new Error(language.t("error.globalSDK.serverNotAvailable"))
|
if (!s) throw new Error(language.t("error.globalSDK.serverNotAvailable"))
|
||||||
|
|||||||
@@ -9,23 +9,13 @@ import type {
|
|||||||
} from "@opencode-ai/sdk/v2/client"
|
} from "@opencode-ai/sdk/v2/client"
|
||||||
import { showToast } from "@opencode-ai/ui/toast"
|
import { showToast } from "@opencode-ai/ui/toast"
|
||||||
import { getFilename } from "@opencode-ai/util/path"
|
import { getFilename } from "@opencode-ai/util/path"
|
||||||
import {
|
import { createContext, getOwner, onCleanup, onMount, type ParentProps, untrack, useContext } from "solid-js"
|
||||||
createContext,
|
|
||||||
getOwner,
|
|
||||||
Match,
|
|
||||||
onCleanup,
|
|
||||||
onMount,
|
|
||||||
type ParentProps,
|
|
||||||
Switch,
|
|
||||||
untrack,
|
|
||||||
useContext,
|
|
||||||
} from "solid-js"
|
|
||||||
import { createStore, produce, reconcile } from "solid-js/store"
|
import { createStore, produce, reconcile } from "solid-js/store"
|
||||||
import { useLanguage } from "@/context/language"
|
import { useLanguage } from "@/context/language"
|
||||||
import { Persist, persisted } from "@/utils/persist"
|
import { Persist, persisted } from "@/utils/persist"
|
||||||
import type { InitError } from "../pages/error"
|
import type { InitError } from "../pages/error"
|
||||||
import { useGlobalSDK } from "./global-sdk"
|
import { useGlobalSDK } from "./global-sdk"
|
||||||
import { bootstrapDirectory, bootstrapGlobal } from "./global-sync/bootstrap"
|
import { bootstrapDirectory, bootstrapGlobal, clearProviderRev } from "./global-sync/bootstrap"
|
||||||
import { createChildStoreManager } from "./global-sync/child-store"
|
import { createChildStoreManager } from "./global-sync/child-store"
|
||||||
import { applyDirectoryEvent, applyGlobalEvent, cleanupDroppedSessionCaches } from "./global-sync/event-reducer"
|
import { applyDirectoryEvent, applyGlobalEvent, cleanupDroppedSessionCaches } from "./global-sync/event-reducer"
|
||||||
import { createRefreshQueue } from "./global-sync/queue"
|
import { createRefreshQueue } from "./global-sync/queue"
|
||||||
@@ -80,10 +70,18 @@ function createGlobalSync() {
|
|||||||
|
|
||||||
let active = true
|
let active = true
|
||||||
let projectWritten = false
|
let projectWritten = false
|
||||||
|
let bootedAt = 0
|
||||||
|
let bootingRoot = false
|
||||||
|
let eventFrame: number | undefined
|
||||||
|
let eventTimer: ReturnType<typeof setTimeout> | undefined
|
||||||
|
|
||||||
onCleanup(() => {
|
onCleanup(() => {
|
||||||
active = false
|
active = false
|
||||||
})
|
})
|
||||||
|
onCleanup(() => {
|
||||||
|
if (eventFrame !== undefined) cancelAnimationFrame(eventFrame)
|
||||||
|
if (eventTimer !== undefined) clearTimeout(eventTimer)
|
||||||
|
})
|
||||||
|
|
||||||
const cacheProjects = () => {
|
const cacheProjects = () => {
|
||||||
setProjectCache(
|
setProjectCache(
|
||||||
@@ -162,6 +160,7 @@ function createGlobalSync() {
|
|||||||
queue.clear(directory)
|
queue.clear(directory)
|
||||||
sessionMeta.delete(directory)
|
sessionMeta.delete(directory)
|
||||||
sdkCache.delete(directory)
|
sdkCache.delete(directory)
|
||||||
|
clearProviderRev(directory)
|
||||||
clearSessionPrefetchDirectory(directory)
|
clearSessionPrefetchDirectory(directory)
|
||||||
},
|
},
|
||||||
translate: language.t,
|
translate: language.t,
|
||||||
@@ -258,6 +257,12 @@ function createGlobalSync() {
|
|||||||
const sdk = sdkFor(directory)
|
const sdk = sdkFor(directory)
|
||||||
await bootstrapDirectory({
|
await bootstrapDirectory({
|
||||||
directory,
|
directory,
|
||||||
|
global: {
|
||||||
|
config: globalStore.config,
|
||||||
|
path: globalStore.path,
|
||||||
|
project: globalStore.project,
|
||||||
|
provider: globalStore.provider,
|
||||||
|
},
|
||||||
sdk,
|
sdk,
|
||||||
store: child[0],
|
store: child[0],
|
||||||
setStore: child[1],
|
setStore: child[1],
|
||||||
@@ -278,15 +283,20 @@ function createGlobalSync() {
|
|||||||
const unsub = globalSDK.event.listen((e) => {
|
const unsub = globalSDK.event.listen((e) => {
|
||||||
const directory = e.name
|
const directory = e.name
|
||||||
const event = e.details
|
const event = e.details
|
||||||
|
const recent = bootingRoot || Date.now() - bootedAt < 1500
|
||||||
|
|
||||||
if (directory === "global") {
|
if (directory === "global") {
|
||||||
applyGlobalEvent({
|
applyGlobalEvent({
|
||||||
event,
|
event,
|
||||||
project: globalStore.project,
|
project: globalStore.project,
|
||||||
refresh: queue.refresh,
|
refresh: () => {
|
||||||
|
if (recent) return
|
||||||
|
queue.refresh()
|
||||||
|
},
|
||||||
setGlobalProject: setProjects,
|
setGlobalProject: setProjects,
|
||||||
})
|
})
|
||||||
if (event.type === "server.connected" || event.type === "global.disposed") {
|
if (event.type === "server.connected" || event.type === "global.disposed") {
|
||||||
|
if (recent) return
|
||||||
for (const directory of Object.keys(children.children)) {
|
for (const directory of Object.keys(children.children)) {
|
||||||
queue.push(directory)
|
queue.push(directory)
|
||||||
}
|
}
|
||||||
@@ -309,7 +319,10 @@ function createGlobalSync() {
|
|||||||
loadLsp: () => {
|
loadLsp: () => {
|
||||||
sdkFor(directory)
|
sdkFor(directory)
|
||||||
.lsp.status()
|
.lsp.status()
|
||||||
.then((x) => setStore("lsp", x.data ?? []))
|
.then((x) => {
|
||||||
|
setStore("lsp", x.data ?? [])
|
||||||
|
setStore("lsp_ready", true)
|
||||||
|
})
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
@@ -325,20 +338,36 @@ function createGlobalSync() {
|
|||||||
})
|
})
|
||||||
|
|
||||||
async function bootstrap() {
|
async function bootstrap() {
|
||||||
await bootstrapGlobal({
|
bootingRoot = true
|
||||||
globalSDK: globalSDK.client,
|
try {
|
||||||
connectErrorTitle: language.t("dialog.server.add.error"),
|
await bootstrapGlobal({
|
||||||
connectErrorDescription: language.t("error.globalSync.connectFailed", {
|
globalSDK: globalSDK.client,
|
||||||
url: globalSDK.url,
|
requestFailedTitle: language.t("common.requestFailed"),
|
||||||
}),
|
translate: language.t,
|
||||||
requestFailedTitle: language.t("common.requestFailed"),
|
formatMoreCount: (count) => language.t("common.moreCountSuffix", { count }),
|
||||||
translate: language.t,
|
setGlobalStore: setBootStore,
|
||||||
formatMoreCount: (count) => language.t("common.moreCountSuffix", { count }),
|
})
|
||||||
setGlobalStore: setBootStore,
|
bootedAt = Date.now()
|
||||||
})
|
} finally {
|
||||||
|
bootingRoot = false
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
onMount(() => {
|
onMount(() => {
|
||||||
|
if (typeof requestAnimationFrame === "function") {
|
||||||
|
eventFrame = requestAnimationFrame(() => {
|
||||||
|
eventFrame = undefined
|
||||||
|
eventTimer = setTimeout(() => {
|
||||||
|
eventTimer = undefined
|
||||||
|
globalSDK.event.start()
|
||||||
|
}, 0)
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
eventTimer = setTimeout(() => {
|
||||||
|
eventTimer = undefined
|
||||||
|
globalSDK.event.start()
|
||||||
|
}, 0)
|
||||||
|
}
|
||||||
void bootstrap()
|
void bootstrap()
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -378,6 +407,7 @@ function createGlobalSync() {
|
|||||||
return globalStore.error
|
return globalStore.error
|
||||||
},
|
},
|
||||||
child: children.child,
|
child: children.child,
|
||||||
|
peek: children.peek,
|
||||||
bootstrap,
|
bootstrap,
|
||||||
updateConfig,
|
updateConfig,
|
||||||
project: projectApi,
|
project: projectApi,
|
||||||
@@ -391,13 +421,7 @@ const GlobalSyncContext = createContext<ReturnType<typeof createGlobalSync>>()
|
|||||||
|
|
||||||
export function GlobalSyncProvider(props: ParentProps) {
|
export function GlobalSyncProvider(props: ParentProps) {
|
||||||
const value = createGlobalSync()
|
const value = createGlobalSync()
|
||||||
return (
|
return <GlobalSyncContext.Provider value={value}>{props.children}</GlobalSyncContext.Provider>
|
||||||
<Switch>
|
|
||||||
<Match when={value.ready}>
|
|
||||||
<GlobalSyncContext.Provider value={value}>{props.children}</GlobalSyncContext.Provider>
|
|
||||||
</Match>
|
|
||||||
</Switch>
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useGlobalSync() {
|
export function useGlobalSync() {
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import type {
|
|||||||
ProviderAuthResponse,
|
ProviderAuthResponse,
|
||||||
ProviderListResponse,
|
ProviderListResponse,
|
||||||
QuestionRequest,
|
QuestionRequest,
|
||||||
|
Session,
|
||||||
Todo,
|
Todo,
|
||||||
} from "@opencode-ai/sdk/v2/client"
|
} from "@opencode-ai/sdk/v2/client"
|
||||||
import { showToast } from "@opencode-ai/ui/toast"
|
import { showToast } from "@opencode-ai/ui/toast"
|
||||||
@@ -15,7 +16,7 @@ import { retry } from "@opencode-ai/util/retry"
|
|||||||
import { batch } from "solid-js"
|
import { batch } from "solid-js"
|
||||||
import { reconcile, type SetStoreFunction, type Store } from "solid-js/store"
|
import { reconcile, type SetStoreFunction, type Store } from "solid-js/store"
|
||||||
import type { State, VcsCache } from "./types"
|
import type { State, VcsCache } from "./types"
|
||||||
import { cmp, normalizeProviderList } from "./utils"
|
import { cmp, normalizeAgentList, normalizeProviderList } from "./utils"
|
||||||
import { formatServerError } from "@/utils/server-errors"
|
import { formatServerError } from "@/utils/server-errors"
|
||||||
|
|
||||||
type GlobalStore = {
|
type GlobalStore = {
|
||||||
@@ -31,73 +32,110 @@ type GlobalStore = {
|
|||||||
reload: undefined | "pending" | "complete"
|
reload: undefined | "pending" | "complete"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function waitForPaint() {
|
||||||
|
return new Promise<void>((resolve) => {
|
||||||
|
let done = false
|
||||||
|
const finish = () => {
|
||||||
|
if (done) return
|
||||||
|
done = true
|
||||||
|
resolve()
|
||||||
|
}
|
||||||
|
const timer = setTimeout(finish, 50)
|
||||||
|
if (typeof requestAnimationFrame !== "function") return
|
||||||
|
requestAnimationFrame(() => {
|
||||||
|
setTimeout(() => {
|
||||||
|
clearTimeout(timer)
|
||||||
|
finish()
|
||||||
|
}, 0)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function errors(list: PromiseSettledResult<unknown>[]) {
|
||||||
|
return list.filter((item): item is PromiseRejectedResult => item.status === "rejected").map((item) => item.reason)
|
||||||
|
}
|
||||||
|
|
||||||
|
const providerRev = new Map<string, number>()
|
||||||
|
|
||||||
|
export function clearProviderRev(directory: string) {
|
||||||
|
providerRev.delete(directory)
|
||||||
|
}
|
||||||
|
|
||||||
|
function runAll(list: Array<() => Promise<unknown>>) {
|
||||||
|
return Promise.allSettled(list.map((item) => item()))
|
||||||
|
}
|
||||||
|
|
||||||
|
function showErrors(input: {
|
||||||
|
errors: unknown[]
|
||||||
|
title: string
|
||||||
|
translate: (key: string, vars?: Record<string, string | number>) => string
|
||||||
|
formatMoreCount: (count: number) => string
|
||||||
|
}) {
|
||||||
|
if (input.errors.length === 0) return
|
||||||
|
const message = formatServerError(input.errors[0], input.translate)
|
||||||
|
const more = input.errors.length > 1 ? input.formatMoreCount(input.errors.length - 1) : ""
|
||||||
|
showToast({
|
||||||
|
variant: "error",
|
||||||
|
title: input.title,
|
||||||
|
description: message + more,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
export async function bootstrapGlobal(input: {
|
export async function bootstrapGlobal(input: {
|
||||||
globalSDK: OpencodeClient
|
globalSDK: OpencodeClient
|
||||||
connectErrorTitle: string
|
|
||||||
connectErrorDescription: string
|
|
||||||
requestFailedTitle: string
|
requestFailedTitle: string
|
||||||
translate: (key: string, vars?: Record<string, string | number>) => string
|
translate: (key: string, vars?: Record<string, string | number>) => string
|
||||||
formatMoreCount: (count: number) => string
|
formatMoreCount: (count: number) => string
|
||||||
setGlobalStore: SetStoreFunction<GlobalStore>
|
setGlobalStore: SetStoreFunction<GlobalStore>
|
||||||
}) {
|
}) {
|
||||||
const health = await input.globalSDK.global
|
const fast = [
|
||||||
.health()
|
() =>
|
||||||
.then((x) => x.data)
|
retry(() =>
|
||||||
.catch(() => undefined)
|
input.globalSDK.global.config.get().then((x) => {
|
||||||
if (!health?.healthy) {
|
input.setGlobalStore("config", x.data!)
|
||||||
showToast({
|
}),
|
||||||
variant: "error",
|
),
|
||||||
title: input.connectErrorTitle,
|
() =>
|
||||||
description: input.connectErrorDescription,
|
retry(() =>
|
||||||
})
|
input.globalSDK.provider.list().then((x) => {
|
||||||
input.setGlobalStore("ready", true)
|
input.setGlobalStore("provider", normalizeProviderList(x.data!))
|
||||||
return
|
}),
|
||||||
}
|
),
|
||||||
|
|
||||||
const tasks = [
|
|
||||||
retry(() =>
|
|
||||||
input.globalSDK.path.get().then((x) => {
|
|
||||||
input.setGlobalStore("path", x.data!)
|
|
||||||
}),
|
|
||||||
),
|
|
||||||
retry(() =>
|
|
||||||
input.globalSDK.global.config.get().then((x) => {
|
|
||||||
input.setGlobalStore("config", x.data!)
|
|
||||||
}),
|
|
||||||
),
|
|
||||||
retry(() =>
|
|
||||||
input.globalSDK.project.list().then((x) => {
|
|
||||||
const projects = (x.data ?? [])
|
|
||||||
.filter((p) => !!p?.id)
|
|
||||||
.filter((p) => !!p.worktree && !p.worktree.includes("opencode-test"))
|
|
||||||
.slice()
|
|
||||||
.sort((a, b) => cmp(a.id, b.id))
|
|
||||||
input.setGlobalStore("project", projects)
|
|
||||||
}),
|
|
||||||
),
|
|
||||||
retry(() =>
|
|
||||||
input.globalSDK.provider.list().then((x) => {
|
|
||||||
input.setGlobalStore("provider", normalizeProviderList(x.data!))
|
|
||||||
}),
|
|
||||||
),
|
|
||||||
retry(() =>
|
|
||||||
input.globalSDK.provider.auth().then((x) => {
|
|
||||||
input.setGlobalStore("provider_auth", x.data ?? {})
|
|
||||||
}),
|
|
||||||
),
|
|
||||||
]
|
]
|
||||||
|
|
||||||
const results = await Promise.allSettled(tasks)
|
const slow = [
|
||||||
const errors = results.filter((r): r is PromiseRejectedResult => r.status === "rejected").map((r) => r.reason)
|
() =>
|
||||||
if (errors.length) {
|
retry(() =>
|
||||||
const message = formatServerError(errors[0], input.translate)
|
input.globalSDK.path.get().then((x) => {
|
||||||
const more = errors.length > 1 ? input.formatMoreCount(errors.length - 1) : ""
|
input.setGlobalStore("path", x.data!)
|
||||||
showToast({
|
}),
|
||||||
variant: "error",
|
),
|
||||||
title: input.requestFailedTitle,
|
() =>
|
||||||
description: message + more,
|
retry(() =>
|
||||||
})
|
input.globalSDK.project.list().then((x) => {
|
||||||
}
|
const projects = (x.data ?? [])
|
||||||
|
.filter((p) => !!p?.id)
|
||||||
|
.filter((p) => !!p.worktree && !p.worktree.includes("opencode-test"))
|
||||||
|
.slice()
|
||||||
|
.sort((a, b) => cmp(a.id, b.id))
|
||||||
|
input.setGlobalStore("project", projects)
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
]
|
||||||
|
|
||||||
|
showErrors({
|
||||||
|
errors: errors(await runAll(fast)),
|
||||||
|
title: input.requestFailedTitle,
|
||||||
|
translate: input.translate,
|
||||||
|
formatMoreCount: input.formatMoreCount,
|
||||||
|
})
|
||||||
|
await waitForPaint()
|
||||||
|
showErrors({
|
||||||
|
errors: errors(await runAll(slow)),
|
||||||
|
title: input.requestFailedTitle,
|
||||||
|
translate: input.translate,
|
||||||
|
formatMoreCount: input.formatMoreCount,
|
||||||
|
})
|
||||||
input.setGlobalStore("ready", true)
|
input.setGlobalStore("ready", true)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -111,6 +149,44 @@ function groupBySession<T extends { id: string; sessionID: string }>(input: T[])
|
|||||||
}, {})
|
}, {})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function projectID(directory: string, projects: Project[]) {
|
||||||
|
return projects.find((project) => project.worktree === directory || project.sandboxes?.includes(directory))?.id
|
||||||
|
}
|
||||||
|
|
||||||
|
function mergeSession(setStore: SetStoreFunction<State>, session: Session) {
|
||||||
|
setStore("session", (list) => {
|
||||||
|
const next = list.slice()
|
||||||
|
const idx = next.findIndex((item) => item.id >= session.id)
|
||||||
|
if (idx === -1) return [...next, session]
|
||||||
|
if (next[idx]?.id === session.id) {
|
||||||
|
next[idx] = session
|
||||||
|
return next
|
||||||
|
}
|
||||||
|
next.splice(idx, 0, session)
|
||||||
|
return next
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function warmSessions(input: {
|
||||||
|
ids: string[]
|
||||||
|
store: Store<State>
|
||||||
|
setStore: SetStoreFunction<State>
|
||||||
|
sdk: OpencodeClient
|
||||||
|
}) {
|
||||||
|
const known = new Set(input.store.session.map((item) => item.id))
|
||||||
|
const ids = [...new Set(input.ids)].filter((id) => !!id && !known.has(id))
|
||||||
|
if (ids.length === 0) return Promise.resolve()
|
||||||
|
return Promise.all(
|
||||||
|
ids.map((sessionID) =>
|
||||||
|
retry(() => input.sdk.session.get({ sessionID })).then((x) => {
|
||||||
|
const session = x.data
|
||||||
|
if (!session?.id) return
|
||||||
|
mergeSession(input.setStore, session)
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
).then(() => undefined)
|
||||||
|
}
|
||||||
|
|
||||||
export async function bootstrapDirectory(input: {
|
export async function bootstrapDirectory(input: {
|
||||||
directory: string
|
directory: string
|
||||||
sdk: OpencodeClient
|
sdk: OpencodeClient
|
||||||
@@ -119,88 +195,166 @@ export async function bootstrapDirectory(input: {
|
|||||||
vcsCache: VcsCache
|
vcsCache: VcsCache
|
||||||
loadSessions: (directory: string) => Promise<void> | void
|
loadSessions: (directory: string) => Promise<void> | void
|
||||||
translate: (key: string, vars?: Record<string, string | number>) => string
|
translate: (key: string, vars?: Record<string, string | number>) => string
|
||||||
}) {
|
global: {
|
||||||
if (input.store.status !== "complete") input.setStore("status", "loading")
|
config: Config
|
||||||
|
path: Path
|
||||||
const blockingRequests = {
|
project: Project[]
|
||||||
project: () => input.sdk.project.current().then((x) => input.setStore("project", x.data!.id)),
|
provider: ProviderListResponse
|
||||||
provider: () =>
|
|
||||||
input.sdk.provider.list().then((x) => {
|
|
||||||
input.setStore("provider", normalizeProviderList(x.data!))
|
|
||||||
}),
|
|
||||||
agent: () => input.sdk.app.agents().then((x) => input.setStore("agent", x.data ?? [])),
|
|
||||||
config: () => input.sdk.config.get().then((x) => input.setStore("config", x.data!)),
|
|
||||||
}
|
}
|
||||||
|
}) {
|
||||||
|
const loading = input.store.status !== "complete"
|
||||||
|
const seededProject = projectID(input.directory, input.global.project)
|
||||||
|
const seededPath = input.global.path.directory === input.directory ? input.global.path : undefined
|
||||||
|
if (seededProject) input.setStore("project", seededProject)
|
||||||
|
if (seededPath) input.setStore("path", seededPath)
|
||||||
|
if (input.store.provider.all.length === 0 && input.global.provider.all.length > 0) {
|
||||||
|
input.setStore("provider", input.global.provider)
|
||||||
|
}
|
||||||
|
if (Object.keys(input.store.config).length === 0 && Object.keys(input.global.config).length > 0) {
|
||||||
|
input.setStore("config", input.global.config)
|
||||||
|
}
|
||||||
|
if (loading || input.store.provider.all.length === 0) {
|
||||||
|
input.setStore("provider_ready", false)
|
||||||
|
}
|
||||||
|
input.setStore("mcp_ready", false)
|
||||||
|
input.setStore("mcp", {})
|
||||||
|
input.setStore("lsp_ready", false)
|
||||||
|
input.setStore("lsp", [])
|
||||||
|
if (loading) input.setStore("status", "partial")
|
||||||
|
|
||||||
try {
|
const fast = [
|
||||||
await Promise.all(Object.values(blockingRequests).map((p) => retry(p)))
|
() => retry(() => input.sdk.app.agents().then((x) => input.setStore("agent", normalizeAgentList(x.data)))),
|
||||||
} catch (err) {
|
() => retry(() => input.sdk.config.get().then((x) => input.setStore("config", x.data!))),
|
||||||
console.error("Failed to bootstrap instance", err)
|
() => retry(() => input.sdk.session.status().then((x) => input.setStore("session_status", x.data!))),
|
||||||
|
]
|
||||||
|
|
||||||
|
const slow = [
|
||||||
|
() =>
|
||||||
|
seededProject
|
||||||
|
? Promise.resolve()
|
||||||
|
: retry(() => input.sdk.project.current()).then((x) => input.setStore("project", x.data!.id)),
|
||||||
|
() =>
|
||||||
|
seededPath
|
||||||
|
? Promise.resolve()
|
||||||
|
: retry(() =>
|
||||||
|
input.sdk.path.get().then((x) => {
|
||||||
|
input.setStore("path", x.data!)
|
||||||
|
const next = projectID(x.data?.directory ?? input.directory, input.global.project)
|
||||||
|
if (next) input.setStore("project", next)
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
() =>
|
||||||
|
retry(() =>
|
||||||
|
input.sdk.vcs.get().then((x) => {
|
||||||
|
const next = x.data ?? input.store.vcs
|
||||||
|
input.setStore("vcs", next)
|
||||||
|
if (next?.branch) input.vcsCache.setStore("value", next)
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
() => retry(() => input.sdk.command.list().then((x) => input.setStore("command", x.data ?? []))),
|
||||||
|
() =>
|
||||||
|
retry(() =>
|
||||||
|
input.sdk.permission.list().then((x) => {
|
||||||
|
const ids = (x.data ?? []).map((perm) => perm?.sessionID).filter((id): id is string => !!id)
|
||||||
|
const grouped = groupBySession(
|
||||||
|
(x.data ?? []).filter((perm): perm is PermissionRequest => !!perm?.id && !!perm.sessionID),
|
||||||
|
)
|
||||||
|
return warmSessions({ ids, store: input.store, setStore: input.setStore, sdk: input.sdk }).then(() =>
|
||||||
|
batch(() => {
|
||||||
|
for (const sessionID of Object.keys(input.store.permission)) {
|
||||||
|
if (grouped[sessionID]) continue
|
||||||
|
input.setStore("permission", sessionID, [])
|
||||||
|
}
|
||||||
|
for (const [sessionID, permissions] of Object.entries(grouped)) {
|
||||||
|
input.setStore(
|
||||||
|
"permission",
|
||||||
|
sessionID,
|
||||||
|
reconcile(
|
||||||
|
permissions.filter((p) => !!p?.id).sort((a, b) => cmp(a.id, b.id)),
|
||||||
|
{ key: "id" },
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
() =>
|
||||||
|
retry(() =>
|
||||||
|
input.sdk.question.list().then((x) => {
|
||||||
|
const ids = (x.data ?? []).map((question) => question?.sessionID).filter((id): id is string => !!id)
|
||||||
|
const grouped = groupBySession((x.data ?? []).filter((q): q is QuestionRequest => !!q?.id && !!q.sessionID))
|
||||||
|
return warmSessions({ ids, store: input.store, setStore: input.setStore, sdk: input.sdk }).then(() =>
|
||||||
|
batch(() => {
|
||||||
|
for (const sessionID of Object.keys(input.store.question)) {
|
||||||
|
if (grouped[sessionID]) continue
|
||||||
|
input.setStore("question", sessionID, [])
|
||||||
|
}
|
||||||
|
for (const [sessionID, questions] of Object.entries(grouped)) {
|
||||||
|
input.setStore(
|
||||||
|
"question",
|
||||||
|
sessionID,
|
||||||
|
reconcile(
|
||||||
|
questions.filter((q) => !!q?.id).sort((a, b) => cmp(a.id, b.id)),
|
||||||
|
{ key: "id" },
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
() => Promise.resolve(input.loadSessions(input.directory)),
|
||||||
|
() =>
|
||||||
|
retry(() =>
|
||||||
|
input.sdk.mcp.status().then((x) => {
|
||||||
|
input.setStore("mcp", x.data!)
|
||||||
|
input.setStore("mcp_ready", true)
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
]
|
||||||
|
|
||||||
|
const errs = errors(await runAll(fast))
|
||||||
|
if (errs.length > 0) {
|
||||||
|
console.error("Failed to bootstrap instance", errs[0])
|
||||||
const project = getFilename(input.directory)
|
const project = getFilename(input.directory)
|
||||||
showToast({
|
showToast({
|
||||||
variant: "error",
|
variant: "error",
|
||||||
title: input.translate("toast.project.reloadFailed.title", { project }),
|
title: input.translate("toast.project.reloadFailed.title", { project }),
|
||||||
description: formatServerError(err, input.translate),
|
description: formatServerError(errs[0], input.translate),
|
||||||
})
|
})
|
||||||
input.setStore("status", "partial")
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (input.store.status !== "complete") input.setStore("status", "partial")
|
await waitForPaint()
|
||||||
|
const slowErrs = errors(await runAll(slow))
|
||||||
|
if (slowErrs.length > 0) {
|
||||||
|
console.error("Failed to finish bootstrap instance", slowErrs[0])
|
||||||
|
const project = getFilename(input.directory)
|
||||||
|
showToast({
|
||||||
|
variant: "error",
|
||||||
|
title: input.translate("toast.project.reloadFailed.title", { project }),
|
||||||
|
description: formatServerError(slowErrs[0], input.translate),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
Promise.all([
|
if (loading && errs.length === 0 && slowErrs.length === 0) input.setStore("status", "complete")
|
||||||
input.sdk.path.get().then((x) => input.setStore("path", x.data!)),
|
|
||||||
input.sdk.command.list().then((x) => input.setStore("command", x.data ?? [])),
|
const rev = (providerRev.get(input.directory) ?? 0) + 1
|
||||||
input.sdk.session.status().then((x) => input.setStore("session_status", x.data!)),
|
providerRev.set(input.directory, rev)
|
||||||
input.loadSessions(input.directory),
|
void retry(() => input.sdk.provider.list())
|
||||||
input.sdk.mcp.status().then((x) => input.setStore("mcp", x.data!)),
|
.then((x) => {
|
||||||
input.sdk.lsp.status().then((x) => input.setStore("lsp", x.data!)),
|
if (providerRev.get(input.directory) !== rev) return
|
||||||
input.sdk.vcs.get().then((x) => {
|
input.setStore("provider", normalizeProviderList(x.data!))
|
||||||
const next = x.data ?? input.store.vcs
|
input.setStore("provider_ready", true)
|
||||||
input.setStore("vcs", next)
|
})
|
||||||
if (next?.branch) input.vcsCache.setStore("value", next)
|
.catch((err) => {
|
||||||
}),
|
if (providerRev.get(input.directory) !== rev) return
|
||||||
input.sdk.permission.list().then((x) => {
|
console.error("Failed to refresh provider list", err)
|
||||||
const grouped = groupBySession(
|
const project = getFilename(input.directory)
|
||||||
(x.data ?? []).filter((perm): perm is PermissionRequest => !!perm?.id && !!perm.sessionID),
|
showToast({
|
||||||
)
|
variant: "error",
|
||||||
batch(() => {
|
title: input.translate("toast.project.reloadFailed.title", { project }),
|
||||||
for (const sessionID of Object.keys(input.store.permission)) {
|
description: formatServerError(err, input.translate),
|
||||||
if (grouped[sessionID]) continue
|
|
||||||
input.setStore("permission", sessionID, [])
|
|
||||||
}
|
|
||||||
for (const [sessionID, permissions] of Object.entries(grouped)) {
|
|
||||||
input.setStore(
|
|
||||||
"permission",
|
|
||||||
sessionID,
|
|
||||||
reconcile(
|
|
||||||
permissions.filter((p) => !!p?.id).sort((a, b) => cmp(a.id, b.id)),
|
|
||||||
{ key: "id" },
|
|
||||||
),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
}),
|
})
|
||||||
input.sdk.question.list().then((x) => {
|
|
||||||
const grouped = groupBySession((x.data ?? []).filter((q): q is QuestionRequest => !!q?.id && !!q.sessionID))
|
|
||||||
batch(() => {
|
|
||||||
for (const sessionID of Object.keys(input.store.question)) {
|
|
||||||
if (grouped[sessionID]) continue
|
|
||||||
input.setStore("question", sessionID, [])
|
|
||||||
}
|
|
||||||
for (const [sessionID, questions] of Object.entries(grouped)) {
|
|
||||||
input.setStore(
|
|
||||||
"question",
|
|
||||||
sessionID,
|
|
||||||
reconcile(
|
|
||||||
questions.filter((q) => !!q?.id).sort((a, b) => cmp(a.id, b.id)),
|
|
||||||
{ key: "id" },
|
|
||||||
),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}),
|
|
||||||
]).then(() => {
|
|
||||||
input.setStore("status", "complete")
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -160,6 +160,7 @@ export function createChildStoreManager(input: {
|
|||||||
project: "",
|
project: "",
|
||||||
projectMeta: initialMeta,
|
projectMeta: initialMeta,
|
||||||
icon: initialIcon,
|
icon: initialIcon,
|
||||||
|
provider_ready: false,
|
||||||
provider: { all: [], connected: [], default: {} },
|
provider: { all: [], connected: [], default: {} },
|
||||||
config: {},
|
config: {},
|
||||||
path: { state: "", config: "", worktree: "", directory: "", home: "" },
|
path: { state: "", config: "", worktree: "", directory: "", home: "" },
|
||||||
@@ -173,7 +174,9 @@ export function createChildStoreManager(input: {
|
|||||||
todo: {},
|
todo: {},
|
||||||
permission: {},
|
permission: {},
|
||||||
question: {},
|
question: {},
|
||||||
|
mcp_ready: false,
|
||||||
mcp: {},
|
mcp: {},
|
||||||
|
lsp_ready: false,
|
||||||
lsp: [],
|
lsp: [],
|
||||||
vcs: vcsStore.value,
|
vcs: vcsStore.value,
|
||||||
limit: 5,
|
limit: 5,
|
||||||
@@ -226,6 +229,15 @@ export function createChildStoreManager(input: {
|
|||||||
return childStore
|
return childStore
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function peek(directory: string, options: ChildOptions = {}) {
|
||||||
|
const childStore = ensureChild(directory)
|
||||||
|
const shouldBootstrap = options.bootstrap ?? true
|
||||||
|
if (shouldBootstrap && childStore[0].status === "loading") {
|
||||||
|
input.onBootstrap(directory)
|
||||||
|
}
|
||||||
|
return childStore
|
||||||
|
}
|
||||||
|
|
||||||
function projectMeta(directory: string, patch: ProjectMeta) {
|
function projectMeta(directory: string, patch: ProjectMeta) {
|
||||||
const [store, setStore] = ensureChild(directory)
|
const [store, setStore] = ensureChild(directory)
|
||||||
const cached = metaCache.get(directory)
|
const cached = metaCache.get(directory)
|
||||||
@@ -256,6 +268,7 @@ export function createChildStoreManager(input: {
|
|||||||
children,
|
children,
|
||||||
ensureChild,
|
ensureChild,
|
||||||
child,
|
child,
|
||||||
|
peek,
|
||||||
projectMeta,
|
projectMeta,
|
||||||
projectIcon,
|
projectIcon,
|
||||||
mark,
|
mark,
|
||||||
|
|||||||
@@ -15,6 +15,8 @@ import type { State, VcsCache } from "./types"
|
|||||||
import { trimSessions } from "./session-trim"
|
import { trimSessions } from "./session-trim"
|
||||||
import { dropSessionCaches } from "./session-cache"
|
import { dropSessionCaches } from "./session-cache"
|
||||||
|
|
||||||
|
const SKIP_PARTS = new Set(["patch", "step-start", "step-finish"])
|
||||||
|
|
||||||
export function applyGlobalEvent(input: {
|
export function applyGlobalEvent(input: {
|
||||||
event: { type: string; properties?: unknown }
|
event: { type: string; properties?: unknown }
|
||||||
project: Project[]
|
project: Project[]
|
||||||
@@ -211,6 +213,7 @@ export function applyDirectoryEvent(input: {
|
|||||||
}
|
}
|
||||||
case "message.part.updated": {
|
case "message.part.updated": {
|
||||||
const part = (event.properties as { part: Part }).part
|
const part = (event.properties as { part: Part }).part
|
||||||
|
if (SKIP_PARTS.has(part.type)) break
|
||||||
const parts = input.store.part[part.messageID]
|
const parts = input.store.part[part.messageID]
|
||||||
if (!parts) {
|
if (!parts) {
|
||||||
input.setStore("part", part.messageID, [part])
|
input.setStore("part", part.messageID, [part])
|
||||||
|
|||||||
@@ -38,6 +38,7 @@ export type State = {
|
|||||||
project: string
|
project: string
|
||||||
projectMeta: ProjectMeta | undefined
|
projectMeta: ProjectMeta | undefined
|
||||||
icon: string | undefined
|
icon: string | undefined
|
||||||
|
provider_ready: boolean
|
||||||
provider: ProviderListResponse
|
provider: ProviderListResponse
|
||||||
config: Config
|
config: Config
|
||||||
path: Path
|
path: Path
|
||||||
@@ -58,9 +59,11 @@ export type State = {
|
|||||||
question: {
|
question: {
|
||||||
[sessionID: string]: QuestionRequest[]
|
[sessionID: string]: QuestionRequest[]
|
||||||
}
|
}
|
||||||
|
mcp_ready: boolean
|
||||||
mcp: {
|
mcp: {
|
||||||
[name: string]: McpStatus
|
[name: string]: McpStatus
|
||||||
}
|
}
|
||||||
|
lsp_ready: boolean
|
||||||
lsp: LspStatus[]
|
lsp: LspStatus[]
|
||||||
vcs: VcsInfo | undefined
|
vcs: VcsInfo | undefined
|
||||||
limit: number
|
limit: number
|
||||||
|
|||||||
@@ -0,0 +1,35 @@
|
|||||||
|
import { describe, expect, test } from "bun:test"
|
||||||
|
import type { Agent } from "@opencode-ai/sdk/v2/client"
|
||||||
|
import { normalizeAgentList } from "./utils"
|
||||||
|
|
||||||
|
const agent = (name = "build") =>
|
||||||
|
({
|
||||||
|
name,
|
||||||
|
mode: "primary",
|
||||||
|
permission: {},
|
||||||
|
options: {},
|
||||||
|
}) as Agent
|
||||||
|
|
||||||
|
describe("normalizeAgentList", () => {
|
||||||
|
test("keeps array payloads", () => {
|
||||||
|
expect(normalizeAgentList([agent("build"), agent("docs")])).toEqual([agent("build"), agent("docs")])
|
||||||
|
})
|
||||||
|
|
||||||
|
test("wraps a single agent payload", () => {
|
||||||
|
expect(normalizeAgentList(agent("docs"))).toEqual([agent("docs")])
|
||||||
|
})
|
||||||
|
|
||||||
|
test("extracts agents from keyed objects", () => {
|
||||||
|
expect(
|
||||||
|
normalizeAgentList({
|
||||||
|
build: agent("build"),
|
||||||
|
docs: agent("docs"),
|
||||||
|
}),
|
||||||
|
).toEqual([agent("build"), agent("docs")])
|
||||||
|
})
|
||||||
|
|
||||||
|
test("drops invalid payloads", () => {
|
||||||
|
expect(normalizeAgentList({ name: "AbortError" })).toEqual([])
|
||||||
|
expect(normalizeAgentList([{ name: "build" }, agent("docs")])).toEqual([agent("docs")])
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -1,7 +1,21 @@
|
|||||||
import type { Project, ProviderListResponse } from "@opencode-ai/sdk/v2/client"
|
import type { Agent, Project, ProviderListResponse } from "@opencode-ai/sdk/v2/client"
|
||||||
|
|
||||||
export const cmp = (a: string, b: string) => (a < b ? -1 : a > b ? 1 : 0)
|
export const cmp = (a: string, b: string) => (a < b ? -1 : a > b ? 1 : 0)
|
||||||
|
|
||||||
|
function isAgent(input: unknown): input is Agent {
|
||||||
|
if (!input || typeof input !== "object") return false
|
||||||
|
const item = input as { name?: unknown; mode?: unknown }
|
||||||
|
if (typeof item.name !== "string") return false
|
||||||
|
return item.mode === "subagent" || item.mode === "primary" || item.mode === "all"
|
||||||
|
}
|
||||||
|
|
||||||
|
export function normalizeAgentList(input: unknown): Agent[] {
|
||||||
|
if (Array.isArray(input)) return input.filter(isAgent)
|
||||||
|
if (isAgent(input)) return [input]
|
||||||
|
if (!input || typeof input !== "object") return []
|
||||||
|
return Object.values(input).filter(isAgent)
|
||||||
|
}
|
||||||
|
|
||||||
export function normalizeProviderList(input: ProviderListResponse): ProviderListResponse {
|
export function normalizeProviderList(input: ProviderListResponse): ProviderListResponse {
|
||||||
return {
|
return {
|
||||||
...input,
|
...input,
|
||||||
|
|||||||
@@ -1,42 +1,10 @@
|
|||||||
import * as i18n from "@solid-primitives/i18n"
|
import * as i18n from "@solid-primitives/i18n"
|
||||||
import { createEffect, createMemo } from "solid-js"
|
import { createEffect, createMemo, createResource } from "solid-js"
|
||||||
import { createStore } from "solid-js/store"
|
import { createStore } from "solid-js/store"
|
||||||
import { createSimpleContext } from "@opencode-ai/ui/context"
|
import { createSimpleContext } from "@opencode-ai/ui/context"
|
||||||
import { Persist, persisted } from "@/utils/persist"
|
import { Persist, persisted } from "@/utils/persist"
|
||||||
import { dict as en } from "@/i18n/en"
|
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 ar } from "@/i18n/ar"
|
|
||||||
import { dict as no } from "@/i18n/no"
|
|
||||||
import { dict as br } from "@/i18n/br"
|
|
||||||
import { dict as th } from "@/i18n/th"
|
|
||||||
import { dict as bs } from "@/i18n/bs"
|
|
||||||
import { dict as tr } from "@/i18n/tr"
|
|
||||||
import { dict as uiEn } from "@opencode-ai/ui/i18n/en"
|
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"
|
|
||||||
import { dict as uiAr } from "@opencode-ai/ui/i18n/ar"
|
|
||||||
import { dict as uiNo } from "@opencode-ai/ui/i18n/no"
|
|
||||||
import { dict as uiBr } from "@opencode-ai/ui/i18n/br"
|
|
||||||
import { dict as uiTh } from "@opencode-ai/ui/i18n/th"
|
|
||||||
import { dict as uiBs } from "@opencode-ai/ui/i18n/bs"
|
|
||||||
import { dict as uiTr } from "@opencode-ai/ui/i18n/tr"
|
|
||||||
|
|
||||||
export type Locale =
|
export type Locale =
|
||||||
| "en"
|
| "en"
|
||||||
@@ -59,6 +27,7 @@ export type Locale =
|
|||||||
|
|
||||||
type RawDictionary = typeof en & typeof uiEn
|
type RawDictionary = typeof en & typeof uiEn
|
||||||
type Dictionary = i18n.Flatten<RawDictionary>
|
type Dictionary = i18n.Flatten<RawDictionary>
|
||||||
|
type Source = { dict: Record<string, string> }
|
||||||
|
|
||||||
function cookie(locale: Locale) {
|
function cookie(locale: Locale) {
|
||||||
return `oc_locale=${encodeURIComponent(locale)}; Path=/; Max-Age=31536000; SameSite=Lax`
|
return `oc_locale=${encodeURIComponent(locale)}; Path=/; Max-Age=31536000; SameSite=Lax`
|
||||||
@@ -125,24 +94,43 @@ const LABEL_KEY: Record<Locale, keyof Dictionary> = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const base = i18n.flatten({ ...en, ...uiEn })
|
const base = i18n.flatten({ ...en, ...uiEn })
|
||||||
const DICT: Record<Locale, Dictionary> = {
|
const dicts = new Map<Locale, Dictionary>([["en", base]])
|
||||||
en: base,
|
|
||||||
zh: { ...base, ...i18n.flatten({ ...zh, ...uiZh }) },
|
const merge = (app: Promise<Source>, ui: Promise<Source>) =>
|
||||||
zht: { ...base, ...i18n.flatten({ ...zht, ...uiZht }) },
|
Promise.all([app, ui]).then(([a, b]) => ({ ...base, ...i18n.flatten({ ...a.dict, ...b.dict }) }) as Dictionary)
|
||||||
ko: { ...base, ...i18n.flatten({ ...ko, ...uiKo }) },
|
|
||||||
de: { ...base, ...i18n.flatten({ ...de, ...uiDe }) },
|
const loaders: Record<Exclude<Locale, "en">, () => Promise<Dictionary>> = {
|
||||||
es: { ...base, ...i18n.flatten({ ...es, ...uiEs }) },
|
zh: () => merge(import("@/i18n/zh"), import("@opencode-ai/ui/i18n/zh")),
|
||||||
fr: { ...base, ...i18n.flatten({ ...fr, ...uiFr }) },
|
zht: () => merge(import("@/i18n/zht"), import("@opencode-ai/ui/i18n/zht")),
|
||||||
da: { ...base, ...i18n.flatten({ ...da, ...uiDa }) },
|
ko: () => merge(import("@/i18n/ko"), import("@opencode-ai/ui/i18n/ko")),
|
||||||
ja: { ...base, ...i18n.flatten({ ...ja, ...uiJa }) },
|
de: () => merge(import("@/i18n/de"), import("@opencode-ai/ui/i18n/de")),
|
||||||
pl: { ...base, ...i18n.flatten({ ...pl, ...uiPl }) },
|
es: () => merge(import("@/i18n/es"), import("@opencode-ai/ui/i18n/es")),
|
||||||
ru: { ...base, ...i18n.flatten({ ...ru, ...uiRu }) },
|
fr: () => merge(import("@/i18n/fr"), import("@opencode-ai/ui/i18n/fr")),
|
||||||
ar: { ...base, ...i18n.flatten({ ...ar, ...uiAr }) },
|
da: () => merge(import("@/i18n/da"), import("@opencode-ai/ui/i18n/da")),
|
||||||
no: { ...base, ...i18n.flatten({ ...no, ...uiNo }) },
|
ja: () => merge(import("@/i18n/ja"), import("@opencode-ai/ui/i18n/ja")),
|
||||||
br: { ...base, ...i18n.flatten({ ...br, ...uiBr }) },
|
pl: () => merge(import("@/i18n/pl"), import("@opencode-ai/ui/i18n/pl")),
|
||||||
th: { ...base, ...i18n.flatten({ ...th, ...uiTh }) },
|
ru: () => merge(import("@/i18n/ru"), import("@opencode-ai/ui/i18n/ru")),
|
||||||
bs: { ...base, ...i18n.flatten({ ...bs, ...uiBs }) },
|
ar: () => merge(import("@/i18n/ar"), import("@opencode-ai/ui/i18n/ar")),
|
||||||
tr: { ...base, ...i18n.flatten({ ...tr, ...uiTr }) },
|
no: () => merge(import("@/i18n/no"), import("@opencode-ai/ui/i18n/no")),
|
||||||
|
br: () => merge(import("@/i18n/br"), import("@opencode-ai/ui/i18n/br")),
|
||||||
|
th: () => merge(import("@/i18n/th"), import("@opencode-ai/ui/i18n/th")),
|
||||||
|
bs: () => merge(import("@/i18n/bs"), import("@opencode-ai/ui/i18n/bs")),
|
||||||
|
tr: () => merge(import("@/i18n/tr"), import("@opencode-ai/ui/i18n/tr")),
|
||||||
|
}
|
||||||
|
|
||||||
|
function loadDict(locale: Locale) {
|
||||||
|
const hit = dicts.get(locale)
|
||||||
|
if (hit) return Promise.resolve(hit)
|
||||||
|
if (locale === "en") return Promise.resolve(base)
|
||||||
|
const load = loaders[locale]
|
||||||
|
return load().then((next: Dictionary) => {
|
||||||
|
dicts.set(locale, next)
|
||||||
|
return next
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export function loadLocaleDict(locale: Locale) {
|
||||||
|
return loadDict(locale).then(() => undefined)
|
||||||
}
|
}
|
||||||
|
|
||||||
const localeMatchers: Array<{ locale: Locale; match: (language: string) => boolean }> = [
|
const localeMatchers: Array<{ locale: Locale; match: (language: string) => boolean }> = [
|
||||||
@@ -168,27 +156,6 @@ const localeMatchers: Array<{ locale: Locale; match: (language: string) => boole
|
|||||||
{ locale: "tr", match: (language) => language.startsWith("tr") },
|
{ locale: "tr", match: (language) => language.startsWith("tr") },
|
||||||
]
|
]
|
||||||
|
|
||||||
type ParityKey = "command.session.previous.unseen" | "command.session.next.unseen"
|
|
||||||
const PARITY_CHECK: Record<Exclude<Locale, "en">, Record<ParityKey, string>> = {
|
|
||||||
zh,
|
|
||||||
zht,
|
|
||||||
ko,
|
|
||||||
de,
|
|
||||||
es,
|
|
||||||
fr,
|
|
||||||
da,
|
|
||||||
ja,
|
|
||||||
pl,
|
|
||||||
ru,
|
|
||||||
ar,
|
|
||||||
no,
|
|
||||||
br,
|
|
||||||
th,
|
|
||||||
bs,
|
|
||||||
tr,
|
|
||||||
}
|
|
||||||
void PARITY_CHECK
|
|
||||||
|
|
||||||
function detectLocale(): Locale {
|
function detectLocale(): Locale {
|
||||||
if (typeof navigator !== "object") return "en"
|
if (typeof navigator !== "object") return "en"
|
||||||
|
|
||||||
@@ -203,27 +170,48 @@ function detectLocale(): Locale {
|
|||||||
return "en"
|
return "en"
|
||||||
}
|
}
|
||||||
|
|
||||||
function normalizeLocale(value: string): Locale {
|
export function normalizeLocale(value: string): Locale {
|
||||||
return LOCALES.includes(value as Locale) ? (value as Locale) : "en"
|
return LOCALES.includes(value as Locale) ? (value as Locale) : "en"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function readStoredLocale() {
|
||||||
|
if (typeof localStorage !== "object") return
|
||||||
|
try {
|
||||||
|
const raw = localStorage.getItem("opencode.global.dat:language")
|
||||||
|
if (!raw) return
|
||||||
|
const next = JSON.parse(raw) as { locale?: string }
|
||||||
|
if (typeof next?.locale !== "string") return
|
||||||
|
return normalizeLocale(next.locale)
|
||||||
|
} catch {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const warm = readStoredLocale() ?? detectLocale()
|
||||||
|
if (warm !== "en") void loadDict(warm)
|
||||||
|
|
||||||
export const { use: useLanguage, provider: LanguageProvider } = createSimpleContext({
|
export const { use: useLanguage, provider: LanguageProvider } = createSimpleContext({
|
||||||
name: "Language",
|
name: "Language",
|
||||||
init: () => {
|
init: (props: { locale?: Locale }) => {
|
||||||
|
const initial = props.locale ?? readStoredLocale() ?? detectLocale()
|
||||||
const [store, setStore, _, ready] = persisted(
|
const [store, setStore, _, ready] = persisted(
|
||||||
Persist.global("language", ["language.v1"]),
|
Persist.global("language", ["language.v1"]),
|
||||||
createStore({
|
createStore({
|
||||||
locale: detectLocale() as Locale,
|
locale: initial,
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
|
|
||||||
const locale = createMemo<Locale>(() => normalizeLocale(store.locale))
|
const locale = createMemo<Locale>(() => normalizeLocale(store.locale))
|
||||||
console.log("locale", locale())
|
|
||||||
const intl = createMemo(() => INTL[locale()])
|
const intl = createMemo(() => INTL[locale()])
|
||||||
|
|
||||||
const dict = createMemo<Dictionary>(() => DICT[locale()])
|
const [dict] = createResource(locale, loadDict, {
|
||||||
|
initialValue: dicts.get(initial) ?? base,
|
||||||
|
})
|
||||||
|
|
||||||
const t = i18n.translator(dict, i18n.resolveTemplate)
|
const t = i18n.translator(() => dict() ?? base, i18n.resolveTemplate) as (
|
||||||
|
key: keyof Dictionary,
|
||||||
|
params?: Record<string, string | number | boolean>,
|
||||||
|
) => string
|
||||||
|
|
||||||
const label = (value: Locale) => t(LABEL_KEY[value])
|
const label = (value: Locale) => t(LABEL_KEY[value])
|
||||||
|
|
||||||
|
|||||||
@@ -13,7 +13,8 @@ import { createScrollPersistence, type SessionScroll } from "./layout-scroll"
|
|||||||
import { createPathHelpers } from "./file/path"
|
import { createPathHelpers } from "./file/path"
|
||||||
|
|
||||||
const AVATAR_COLOR_KEYS = ["pink", "mint", "orange", "purple", "cyan", "lime"] as const
|
const AVATAR_COLOR_KEYS = ["pink", "mint", "orange", "purple", "cyan", "lime"] as const
|
||||||
const DEFAULT_PANEL_WIDTH = 344
|
const DEFAULT_SIDEBAR_WIDTH = 344
|
||||||
|
const DEFAULT_FILE_TREE_WIDTH = 200
|
||||||
const DEFAULT_SESSION_WIDTH = 600
|
const DEFAULT_SESSION_WIDTH = 600
|
||||||
const DEFAULT_TERMINAL_HEIGHT = 280
|
const DEFAULT_TERMINAL_HEIGHT = 280
|
||||||
export type AvatarColorKey = (typeof AVATAR_COLOR_KEYS)[number]
|
export type AvatarColorKey = (typeof AVATAR_COLOR_KEYS)[number]
|
||||||
@@ -161,11 +162,11 @@ export const { use: useLayout, provider: LayoutProvider } = createSimpleContext(
|
|||||||
if (!isRecord(fileTree)) return fileTree
|
if (!isRecord(fileTree)) return fileTree
|
||||||
if (fileTree.tab === "changes" || fileTree.tab === "all") return fileTree
|
if (fileTree.tab === "changes" || fileTree.tab === "all") return fileTree
|
||||||
|
|
||||||
const width = typeof fileTree.width === "number" ? fileTree.width : DEFAULT_PANEL_WIDTH
|
const width = typeof fileTree.width === "number" ? fileTree.width : DEFAULT_FILE_TREE_WIDTH
|
||||||
return {
|
return {
|
||||||
...fileTree,
|
...fileTree,
|
||||||
opened: true,
|
opened: true,
|
||||||
width: width === 260 ? DEFAULT_PANEL_WIDTH : width,
|
width: width === 260 ? DEFAULT_FILE_TREE_WIDTH : width,
|
||||||
tab: "changes",
|
tab: "changes",
|
||||||
}
|
}
|
||||||
})()
|
})()
|
||||||
@@ -230,7 +231,7 @@ export const { use: useLayout, provider: LayoutProvider } = createSimpleContext(
|
|||||||
createStore({
|
createStore({
|
||||||
sidebar: {
|
sidebar: {
|
||||||
opened: false,
|
opened: false,
|
||||||
width: DEFAULT_PANEL_WIDTH,
|
width: DEFAULT_SIDEBAR_WIDTH,
|
||||||
workspaces: {} as Record<string, boolean>,
|
workspaces: {} as Record<string, boolean>,
|
||||||
workspacesDefault: false,
|
workspacesDefault: false,
|
||||||
},
|
},
|
||||||
@@ -243,8 +244,8 @@ export const { use: useLayout, provider: LayoutProvider } = createSimpleContext(
|
|||||||
panelOpened: true,
|
panelOpened: true,
|
||||||
},
|
},
|
||||||
fileTree: {
|
fileTree: {
|
||||||
opened: true,
|
opened: false,
|
||||||
width: DEFAULT_PANEL_WIDTH,
|
width: DEFAULT_FILE_TREE_WIDTH,
|
||||||
tab: "changes" as "changes" | "all",
|
tab: "changes" as "changes" | "all",
|
||||||
},
|
},
|
||||||
session: {
|
session: {
|
||||||
@@ -543,12 +544,26 @@ export const { use: useLayout, provider: LayoutProvider } = createSimpleContext(
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
let sessionFrame: number | undefined
|
||||||
|
let sessionTimer: number | undefined
|
||||||
|
|
||||||
onMount(() => {
|
onMount(() => {
|
||||||
Promise.all(
|
sessionFrame = requestAnimationFrame(() => {
|
||||||
server.projects.list().map((project) => {
|
sessionFrame = undefined
|
||||||
return globalSync.project.loadSessions(project.worktree)
|
sessionTimer = window.setTimeout(() => {
|
||||||
}),
|
sessionTimer = undefined
|
||||||
)
|
void Promise.all(
|
||||||
|
server.projects.list().map((project) => {
|
||||||
|
return globalSync.project.loadSessions(project.worktree)
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
}, 0)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
onCleanup(() => {
|
||||||
|
if (sessionFrame !== undefined) cancelAnimationFrame(sessionFrame)
|
||||||
|
if (sessionTimer !== undefined) window.clearTimeout(sessionTimer)
|
||||||
})
|
})
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@@ -628,32 +643,32 @@ export const { use: useLayout, provider: LayoutProvider } = createSimpleContext(
|
|||||||
},
|
},
|
||||||
fileTree: {
|
fileTree: {
|
||||||
opened: createMemo(() => store.fileTree?.opened ?? true),
|
opened: createMemo(() => store.fileTree?.opened ?? true),
|
||||||
width: createMemo(() => store.fileTree?.width ?? DEFAULT_PANEL_WIDTH),
|
width: createMemo(() => store.fileTree?.width ?? DEFAULT_FILE_TREE_WIDTH),
|
||||||
tab: createMemo(() => store.fileTree?.tab ?? "changes"),
|
tab: createMemo(() => store.fileTree?.tab ?? "changes"),
|
||||||
setTab(tab: "changes" | "all") {
|
setTab(tab: "changes" | "all") {
|
||||||
if (!store.fileTree) {
|
if (!store.fileTree) {
|
||||||
setStore("fileTree", { opened: true, width: DEFAULT_PANEL_WIDTH, tab })
|
setStore("fileTree", { opened: true, width: DEFAULT_FILE_TREE_WIDTH, tab })
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
setStore("fileTree", "tab", tab)
|
setStore("fileTree", "tab", tab)
|
||||||
},
|
},
|
||||||
open() {
|
open() {
|
||||||
if (!store.fileTree) {
|
if (!store.fileTree) {
|
||||||
setStore("fileTree", { opened: true, width: DEFAULT_PANEL_WIDTH, tab: "changes" })
|
setStore("fileTree", { opened: true, width: DEFAULT_FILE_TREE_WIDTH, tab: "changes" })
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
setStore("fileTree", "opened", true)
|
setStore("fileTree", "opened", true)
|
||||||
},
|
},
|
||||||
close() {
|
close() {
|
||||||
if (!store.fileTree) {
|
if (!store.fileTree) {
|
||||||
setStore("fileTree", { opened: false, width: DEFAULT_PANEL_WIDTH, tab: "changes" })
|
setStore("fileTree", { opened: false, width: DEFAULT_FILE_TREE_WIDTH, tab: "changes" })
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
setStore("fileTree", "opened", false)
|
setStore("fileTree", "opened", false)
|
||||||
},
|
},
|
||||||
toggle() {
|
toggle() {
|
||||||
if (!store.fileTree) {
|
if (!store.fileTree) {
|
||||||
setStore("fileTree", { opened: true, width: DEFAULT_PANEL_WIDTH, tab: "changes" })
|
setStore("fileTree", { opened: true, width: DEFAULT_FILE_TREE_WIDTH, tab: "changes" })
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
setStore("fileTree", "opened", (x) => !x)
|
setStore("fileTree", "opened", (x) => !x)
|
||||||
|
|||||||
@@ -390,10 +390,18 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (modelEnabled()) {
|
if (modelEnabled()) {
|
||||||
|
const probe = Symbol("model-probe")
|
||||||
|
|
||||||
|
modelProbe.bind(probe, {
|
||||||
|
setAgent: agent.set,
|
||||||
|
setModel: model.set,
|
||||||
|
setVariant: model.variant.set,
|
||||||
|
})
|
||||||
|
|
||||||
createEffect(() => {
|
createEffect(() => {
|
||||||
const agent = result.agent.current()
|
const agent = result.agent.current()
|
||||||
const model = result.model.current()
|
const model = result.model.current()
|
||||||
modelProbe.set({
|
modelProbe.set(probe, {
|
||||||
dir: sdk.directory,
|
dir: sdk.directory,
|
||||||
sessionID: id(),
|
sessionID: id(),
|
||||||
last: store.last,
|
last: store.last,
|
||||||
@@ -411,10 +419,20 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({
|
|||||||
pick: scope(),
|
pick: scope(),
|
||||||
base: undefined,
|
base: undefined,
|
||||||
current: store.current,
|
current: store.current,
|
||||||
|
variants: result.model.variant.list(),
|
||||||
|
models: result.model
|
||||||
|
.list()
|
||||||
|
.filter((item) => result.model.visible({ providerID: item.provider.id, modelID: item.id }))
|
||||||
|
.map((item) => ({
|
||||||
|
providerID: item.provider.id,
|
||||||
|
modelID: item.id,
|
||||||
|
name: item.name,
|
||||||
|
})),
|
||||||
|
agents: result.agent.list().map((item) => ({ name: item.name })),
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
onCleanup(() => modelProbe.clear())
|
onCleanup(() => modelProbe.clear(probe))
|
||||||
}
|
}
|
||||||
|
|
||||||
return result
|
return result
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ import { base64Encode } from "@opencode-ai/util/encode"
|
|||||||
import { decode64 } from "@/utils/base64"
|
import { decode64 } from "@/utils/base64"
|
||||||
import { EventSessionError } from "@opencode-ai/sdk/v2"
|
import { EventSessionError } from "@opencode-ai/sdk/v2"
|
||||||
import { Persist, persisted } from "@/utils/persist"
|
import { Persist, persisted } from "@/utils/persist"
|
||||||
import { playSound, soundSrc } from "@/utils/sound"
|
import { playSoundById } from "@/utils/sound"
|
||||||
|
|
||||||
type NotificationBase = {
|
type NotificationBase = {
|
||||||
directory?: string
|
directory?: string
|
||||||
@@ -234,7 +234,7 @@ export const { use: useNotification, provider: NotificationProvider } = createSi
|
|||||||
if (session.parentID) return
|
if (session.parentID) return
|
||||||
|
|
||||||
if (settings.sounds.agentEnabled()) {
|
if (settings.sounds.agentEnabled()) {
|
||||||
playSound(soundSrc(settings.sounds.agent()))
|
void playSoundById(settings.sounds.agent())
|
||||||
}
|
}
|
||||||
|
|
||||||
append({
|
append({
|
||||||
@@ -263,7 +263,7 @@ export const { use: useNotification, provider: NotificationProvider } = createSi
|
|||||||
if (session?.parentID) return
|
if (session?.parentID) return
|
||||||
|
|
||||||
if (settings.sounds.errorsEnabled()) {
|
if (settings.sounds.errorsEnabled()) {
|
||||||
playSound(soundSrc(settings.sounds.errors()))
|
void playSoundById(settings.sounds.errors())
|
||||||
}
|
}
|
||||||
|
|
||||||
const error = "error" in event.properties ? event.properties.error : undefined
|
const error = "error" in event.properties ? event.properties.error : undefined
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import { ServerConnection } from "./server"
|
|||||||
|
|
||||||
type PickerPaths = string | string[] | null
|
type PickerPaths = string | string[] | null
|
||||||
type OpenDirectoryPickerOptions = { title?: string; multiple?: boolean }
|
type OpenDirectoryPickerOptions = { title?: string; multiple?: boolean }
|
||||||
type OpenFilePickerOptions = { title?: string; multiple?: boolean }
|
type OpenFilePickerOptions = { title?: string; multiple?: boolean; accept?: string[]; extensions?: string[] }
|
||||||
type SaveFilePickerOptions = { title?: string; defaultPath?: string }
|
type SaveFilePickerOptions = { title?: string; defaultPath?: string }
|
||||||
type UpdateInfo = { updateAvailable: boolean; version?: string }
|
type UpdateInfo = { updateAvailable: boolean; version?: string }
|
||||||
|
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
import { createStore, type SetStoreFunction } from "solid-js/store"
|
|
||||||
import { createSimpleContext } from "@opencode-ai/ui/context"
|
import { createSimpleContext } from "@opencode-ai/ui/context"
|
||||||
import { batch, createMemo, createRoot, onCleanup } from "solid-js"
|
import { checksum } from "@opencode-ai/util/encode"
|
||||||
import { useParams } from "@solidjs/router"
|
import { useParams } from "@solidjs/router"
|
||||||
|
import { batch, createMemo, createRoot, getOwner, onCleanup } from "solid-js"
|
||||||
|
import { createStore, type SetStoreFunction } from "solid-js/store"
|
||||||
import type { FileSelection } from "@/context/file"
|
import type { FileSelection } from "@/context/file"
|
||||||
import { Persist, persisted } from "@/utils/persist"
|
import { Persist, persisted } from "@/utils/persist"
|
||||||
import { checksum } from "@opencode-ai/util/encode"
|
|
||||||
|
|
||||||
interface PartBase {
|
interface PartBase {
|
||||||
content: string
|
content: string
|
||||||
@@ -250,6 +250,7 @@ export const { use: usePrompt, provider: PromptProvider } = createSimpleContext(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const owner = getOwner()
|
||||||
const load = (dir: string, id: string | undefined) => {
|
const load = (dir: string, id: string | undefined) => {
|
||||||
const key = `${dir}:${id ?? WORKSPACE_KEY}`
|
const key = `${dir}:${id ?? WORKSPACE_KEY}`
|
||||||
const existing = cache.get(key)
|
const existing = cache.get(key)
|
||||||
@@ -259,10 +260,13 @@ export const { use: usePrompt, provider: PromptProvider } = createSimpleContext(
|
|||||||
return existing.value
|
return existing.value
|
||||||
}
|
}
|
||||||
|
|
||||||
const entry = createRoot((dispose) => ({
|
const entry = createRoot(
|
||||||
value: createPromptSession(dir, id),
|
(dispose) => ({
|
||||||
dispose,
|
value: createPromptSession(dir, id),
|
||||||
}))
|
dispose,
|
||||||
|
}),
|
||||||
|
owner,
|
||||||
|
)
|
||||||
|
|
||||||
cache.set(key, entry)
|
cache.set(key, entry)
|
||||||
prune()
|
prune()
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user