mirror of
https://github.com/anomalyco/opencode.git
synced 2026-08-16 09:28:27 -04:00
Compare commits
109 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 439a5a5281 | |||
| 0abbcddac2 | |||
| dd25d143c5 | |||
| d8a57ee15c | |||
| aa52d30d7f | |||
| 83d633fd13 | |||
| 824455e2f1 | |||
| 62de2b3695 | |||
| 7550aa4b89 | |||
| c33a2011c4 | |||
| fff6764ed4 | |||
| 9644a008a4 | |||
| ccb6b7c3ea | |||
| f95f877e5f | |||
| fe96d4aebd | |||
| 3a95d56144 | |||
| ecd8be7593 | |||
| a72c3880bb | |||
| ccd8676bee | |||
| 5a533e366f | |||
| 77429f5982 | |||
| 6cfe4bc34f | |||
| 95013d2936 | |||
| 14a5529793 | |||
| 0b2e8707e3 | |||
| adf178a6b9 | |||
| 78f85b1cd6 | |||
| 888dc67cc3 | |||
| fd9ee435ec | |||
| 1c25b2f298 | |||
| ea9fffce2e | |||
| 516f0266b8 | |||
| 39397deb8d | |||
| 65fd2e5c91 | |||
| c5fe32fbb1 | |||
| c48b77d6f9 | |||
| 64fd7a2027 | |||
| ae259d87f0 | |||
| 50374abab8 | |||
| 0497e8badf | |||
| 2a08f8f5f4 | |||
| 9353559088 | |||
| 6aff2ee169 | |||
| 254a481e5d | |||
| 26885e7118 | |||
| dba08013d1 | |||
| fc09646973 | |||
| c3b9c9d888 | |||
| 203885d241 | |||
| 728e86b287 | |||
| d5aa79c73a | |||
| eb6ff0c1e0 | |||
| 0df44c4e91 | |||
| 561070fbc2 | |||
| b0e41ff2c4 | |||
| 977a40af68 | |||
| dffecb6478 | |||
| 7f57d2a9ac | |||
| 377d5d2287 | |||
| 38bb38ecb2 | |||
| 3a149ba71c | |||
| 14df88eab5 | |||
| e12cb7fb6b | |||
| d4f7039932 | |||
| e0ec9be238 | |||
| 2b34df94fa | |||
| 68f225a11d | |||
| e9f5d34096 | |||
| d3459eb740 | |||
| be73f465df | |||
| f14eafe9db | |||
| b7e4f1ef74 | |||
| efd5f0a80f | |||
| 7135bc4fb5 | |||
| 7cf8d1ca6d | |||
| 78bca5c707 | |||
| 82db86e2a1 | |||
| 476e3fc382 | |||
| a12d50e15a | |||
| 1b9b260458 | |||
| a8983bd2c7 | |||
| 709af58612 | |||
| bcbbf32569 | |||
| 7a8e7c88f4 | |||
| a22676720d | |||
| f63a451b6f | |||
| 8b6a2b173b | |||
| 933dbfdb36 | |||
| b44bc0ad07 | |||
| 436cd39d14 | |||
| ed6dc879be | |||
| 96d53c6716 | |||
| 911fb7063a | |||
| 27e8e2e22b | |||
| 39c6dd1c32 | |||
| a09447bc9b | |||
| 16815473a4 | |||
| 1f47bbfce2 | |||
| 33ed95f09c | |||
| 2152624f37 | |||
| 7d84cb05ef | |||
| eba0bd0397 | |||
| 9aab24e95e | |||
| a4fed69a82 | |||
| 41a3cfcdd9 | |||
| 72845e09fc | |||
| abaab29cb3 | |||
| 30936a9bca | |||
| a9144eccf8 |
@@ -1,4 +1,5 @@
|
|||||||
adamdotdevin
|
adamdotdevin
|
||||||
|
arvsrn
|
||||||
Brendonovich
|
Brendonovich
|
||||||
fwang
|
fwang
|
||||||
Hona
|
Hona
|
||||||
@@ -7,11 +8,14 @@ jayair
|
|||||||
jlongster
|
jlongster
|
||||||
kitlangton
|
kitlangton
|
||||||
kommander
|
kommander
|
||||||
|
ludvigrask
|
||||||
MrMushrooooom
|
MrMushrooooom
|
||||||
nexxeln
|
nexxeln
|
||||||
R44VC0RP
|
R44VC0RP
|
||||||
rekram1-node
|
rekram1-node
|
||||||
thdxr
|
thdxr
|
||||||
simonklee
|
simonklee
|
||||||
|
Slickstef11
|
||||||
|
usrnk1
|
||||||
vimtor
|
vimtor
|
||||||
starptech
|
starptech
|
||||||
|
|||||||
+1
-1
@@ -125,4 +125,4 @@ OpenCode 内置两种 Agent,可用 `Tab` 键快速切换:
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
**加入我们的社区** [飞书](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)
|
**加入我们的社区** [飞书](https://applink.feishu.cn/client/chat/chatter/add_by_link?link_token=52ao9352-5623-4fa0-b7dd-3407c392c1af&qr_code=true) | [X.com](https://x.com/opencode)
|
||||||
|
|||||||
+1
-1
@@ -125,4 +125,4 @@ OpenCode 內建了兩種 Agent,您可以使用 `Tab` 鍵快速切換。
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
**加入我們的社群** [飞书](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)
|
**加入我們的社群** [飞书](https://applink.feishu.cn/client/chat/chatter/add_by_link?link_token=52ao9352-5623-4fa0-b7dd-3407c392c1af&qr_code=true) | [X.com](https://x.com/opencode)
|
||||||
|
|||||||
@@ -0,0 +1,3 @@
|
|||||||
|
node_modules
|
||||||
|
.remotion
|
||||||
|
out/frame-*.png
|
||||||
@@ -0,0 +1,483 @@
|
|||||||
|
{
|
||||||
|
"lockfileVersion": 1,
|
||||||
|
"configVersion": 1,
|
||||||
|
"workspaces": {
|
||||||
|
"": {
|
||||||
|
"name": "glm52-rise-video",
|
||||||
|
"dependencies": {
|
||||||
|
"@remotion/cli": "^4.0.384",
|
||||||
|
"react": "^19.2.3",
|
||||||
|
"react-dom": "^19.2.3",
|
||||||
|
"remotion": "^4.0.384",
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/react": "^19.2.8",
|
||||||
|
"@types/react-dom": "^19.2.3",
|
||||||
|
"typescript": "^5.8.2",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"packages": {
|
||||||
|
"@babel/helper-string-parser": ["@babel/helper-string-parser@7.29.7", "", {}, "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw=="],
|
||||||
|
|
||||||
|
"@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.29.7", "", {}, "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg=="],
|
||||||
|
|
||||||
|
"@babel/parser": ["@babel/parser@7.24.1", "", { "bin": "./bin/babel-parser.js" }, "sha512-Zo9c7N3xdOIQrNip7Lc9wvRPzlRtovHVE4lkz8WEDr7uYh/GMQhSiIgFxGIArRHYdJE5kxtZjAf8rT0xhdLCzg=="],
|
||||||
|
|
||||||
|
"@babel/types": ["@babel/types@7.24.0", "", { "dependencies": { "@babel/helper-string-parser": "^7.23.4", "@babel/helper-validator-identifier": "^7.22.20", "to-fast-properties": "^2.0.0" } }, "sha512-+j7a5c253RfKh8iABBhywc8NSfP5LURe7Uh4qpsh6jc+aLJguvmIUBdjSdEMQv2bENrCR5MfRdjGo7vzS/ob7w=="],
|
||||||
|
|
||||||
|
"@emnapi/core": ["@emnapi/core@1.11.1", "", { "dependencies": { "@emnapi/wasi-threads": "1.2.2", "tslib": "^2.4.0" } }, "sha512-RSvbQmHzdKzNsLYa/wHrbc3KN4sYLKAdPZxqiM2HATqv/SBk2/ENSHpvXGaLOMcsAyz0poEGqkmmKYG3OWiJEQ=="],
|
||||||
|
|
||||||
|
"@emnapi/runtime": ["@emnapi/runtime@1.11.1", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-vgj7R3y3Wgx24IQaGPA/R6YFXLHVMOZ0uVEyIQPaWs+rd1AzfEMXlAC22FYwO1XkKR6NPsq7mUandH8oIRdZFw=="],
|
||||||
|
|
||||||
|
"@emnapi/wasi-threads": ["@emnapi/wasi-threads@1.2.2", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-c95qOXkHdydNKhscBTebqEC1CVAZpyqOfVfBzQ1qgzyl3gfeldUjIggDbIZgDKsHLgnsM+igH7TJ/eAasaVuMA=="],
|
||||||
|
|
||||||
|
"@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.28.1", "", { "os": "aix", "cpu": "ppc64" }, "sha512-Svl7tq8k/08+p6CXPpRjQ1fKX+1odH/BQbb48fV6fj3CWHhsoIOoY87w1oHXm0qEpkIK3ZfVgp0hed3XBXzXMQ=="],
|
||||||
|
|
||||||
|
"@esbuild/android-arm": ["@esbuild/android-arm@0.28.1", "", { "os": "android", "cpu": "arm" }, "sha512-0k2F129Xdio1TdJfzJ8sy1Q47vUD2NnwdhiAf7drUN1EBTfPf4hsFCtmMgu/6m8JSzsBrlmVjudMBQqOfG8usQ=="],
|
||||||
|
|
||||||
|
"@esbuild/android-arm64": ["@esbuild/android-arm64@0.28.1", "", { "os": "android", "cpu": "arm64" }, "sha512-34EGEbCIAgosYz6goLcopX6Mo7NyGv9tfwEM2/7Ce2VcVRk568iSvniGWcUXIy7wEDR1wzolcxcriFVrWYcwBg=="],
|
||||||
|
|
||||||
|
"@esbuild/android-x64": ["@esbuild/android-x64@0.28.1", "", { "os": "android", "cpu": "x64" }, "sha512-dbwY7ltSMDWsRatcRpCnES4F+im88OCUgGZjy52shC7GqHRE/cYlxNbB4Z4UpJswpcc4Qxd2oE/ufM0p61IKng=="],
|
||||||
|
|
||||||
|
"@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.28.1", "", { "os": "darwin", "cpu": "arm64" }, "sha512-TZbWkQY7kvTAXbXUT7uVACR5cMHsDiSz9z7ZKAX/RTq/WJEk3QyRr0wZpNhBDX+/0CtdqUIJlOiodQcta6tY3Q=="],
|
||||||
|
|
||||||
|
"@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.28.1", "", { "os": "darwin", "cpu": "x64" }, "sha512-zfdzgK9ACBNZLI/CyHTOx81SyNbM6YXn7rxSgX97VjyiPl9W1i4Ka4fgKECEoFCKGpvBj5qArWIGgQjOwkgskQ=="],
|
||||||
|
|
||||||
|
"@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.28.1", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-wG2EA8ENdEI0qhkSZMjfqrdY+ziCYCPMmtZjjIwOmXFjmyzEHn+UUxk5of+SYsjtfs3VpnlC7QLzSI5hY/rOAw=="],
|
||||||
|
|
||||||
|
"@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.28.1", "", { "os": "freebsd", "cpu": "x64" }, "sha512-i7dZ9vQgnvSCzi/rYCXNgtF/U+eKZNJBzu3eTQbRgHnM7tNSizLOkRFAl3qzVc/Op/u5YkHHa4pf/3DOYHthLQ=="],
|
||||||
|
|
||||||
|
"@esbuild/linux-arm": ["@esbuild/linux-arm@0.28.1", "", { "os": "linux", "cpu": "arm" }, "sha512-qVXBOHQS+d5Y722GwJzJUtOLlX7km3CraOaGormF1pDtPd2C/l1SHRPgjLunLGe51Sh5YYWKMFDyV4SxgMQYTQ=="],
|
||||||
|
|
||||||
|
"@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.28.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-yHs+0uc8+nvEAfAfxrWQKK5peSNzBc4PegcMO0EJ2hT71uA7vB8Ihg2e77R2P7SG5uYjPbHlLLmve4LLLRCf0g=="],
|
||||||
|
|
||||||
|
"@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.28.1", "", { "os": "linux", "cpu": "ia32" }, "sha512-d1z4ZuP0ajrfz/FhGT4vv278rX8KnPPJx8i5+AtK7TYbx9Le9F1hyzurZpkEyjkGa9dUGhQow4C1NmeGvqxN2w=="],
|
||||||
|
|
||||||
|
"@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.28.1", "", { "os": "linux", "cpu": "none" }, "sha512-M5sRjUVZrkm1OAPR3dlOYzNmN+loZKGVi1VUQGrwuqLcbR6qeAz+famMhjASeH3YVKvZz+zT1jlh/keC3Rj/lg=="],
|
||||||
|
|
||||||
|
"@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.28.1", "", { "os": "linux", "cpu": "none" }, "sha512-mRObBZeHh2OxcBFPWE/FjylkRgZdYuiTR3vaTozquCGOH14iP9oN4x4Ge81CoIDYQrXmIxpFumJBu5MtZpnQJQ=="],
|
||||||
|
|
||||||
|
"@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.28.1", "", { "os": "linux", "cpu": "ppc64" }, "sha512-slScBsMAb3GFDcdrCgLwZtPYRoH2H/youv10QiZyRjmsP48fznoveWytSgCI/R0ZcUgpc0ZhIUEx6LHts8yrfQ=="],
|
||||||
|
|
||||||
|
"@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.28.1", "", { "os": "linux", "cpu": "none" }, "sha512-kw0owk1o0GFETUJyW0jc0G4Yzs0BHZn0JDZ8JRT088vjJYX777BAs1fDGxAC+q831qOs2DTC96mNsG2opdfyyQ=="],
|
||||||
|
|
||||||
|
"@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.28.1", "", { "os": "linux", "cpu": "s390x" }, "sha512-/lAIjX8aYFRByhh6L5rYtPEDRqa9de/4V/juOXcta5frjvzXO4/sqEtyytse0g3zZFuWu5cDN0MkLz2qRDD2Ag=="],
|
||||||
|
|
||||||
|
"@esbuild/linux-x64": ["@esbuild/linux-x64@0.28.1", "", { "os": "linux", "cpu": "x64" }, "sha512-u/anNYF2mmVOEDwLtnQ1wOr3EZ9sTNGLWrsYGYwHWzGA3Si84IOkHXlbWTD1NB+9/1lcnweYKO54uhxZydNzfA=="],
|
||||||
|
|
||||||
|
"@esbuild/netbsd-arm64": ["@esbuild/netbsd-arm64@0.28.1", "", { "os": "none", "cpu": "arm64" }, "sha512-oks0DYbLwWMmaakTsCb+zL4E+aHRVLom9IJZOAthMQEPiQmydXHkziYEsGYRx0uNV/IjEKGAV941JzH02pflqw=="],
|
||||||
|
|
||||||
|
"@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.28.1", "", { "os": "none", "cpu": "x64" }, "sha512-aeL6lAnN89Hz43Mlh1G8ARasbuoYvSITDEx0tHh5b7jJnHcssqgjy9Yx430GDpmCa6OyrKoS0aNRjKundRizGg=="],
|
||||||
|
|
||||||
|
"@esbuild/openbsd-arm64": ["@esbuild/openbsd-arm64@0.28.1", "", { "os": "openbsd", "cpu": "arm64" }, "sha512-MEFJe5C3R8pwXdZ5Y21oo6m7ePiS0d9pWucn99O/wvyJZChoIQKrQDxKrGeW8F5+T0okTHesAmDeiHDTIq0V/Q=="],
|
||||||
|
|
||||||
|
"@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.28.1", "", { "os": "openbsd", "cpu": "x64" }, "sha512-i/ZLIOafE0Z8cI/XANJAixoJL/uRAoS2xOA3rb0xN+KK0K177cMAsQYkzHtBrtMXAKuAc7HGgcWiZ/sRC1Nxgw=="],
|
||||||
|
|
||||||
|
"@esbuild/openharmony-arm64": ["@esbuild/openharmony-arm64@0.28.1", "", { "os": "none", "cpu": "arm64" }, "sha512-ge+Z7EXFNt2BO1oAMsVpiQ8EwndV9i1xXerAeTIK7AtPs3bKFXQM7nlRxDSIUIMeueR1CNXxqztLzdNeReKBJg=="],
|
||||||
|
|
||||||
|
"@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.28.1", "", { "os": "sunos", "cpu": "x64" }, "sha512-BEjgtECkL3vY+SaSQ6nzVfiALUeFxpawyp8Jmf5PtYhf1Ug40N1h/hxlhts+f1FvSvarEigdxS3BlSMI2PJLcQ=="],
|
||||||
|
|
||||||
|
"@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.28.1", "", { "os": "win32", "cpu": "arm64" }, "sha512-lCv9eK/H6ZJWbE7bh2nw54CZ9M2nupBxJcTsdk/QQnWkdSjKGuxmmH8/GWrlT1eMmZfn4dGcCjRte397WqfQXA=="],
|
||||||
|
|
||||||
|
"@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.28.1", "", { "os": "win32", "cpu": "ia32" }, "sha512-zvb/mB2bSCoJOpoCBgYKKpX6YM6mJBlBUVUtVj41DlZJVEB6/0CKlRYxP5wWl1C1ILiCoAU5wZZ4q1P3qeS6Eg=="],
|
||||||
|
|
||||||
|
"@esbuild/win32-x64": ["@esbuild/win32-x64@0.28.1", "", { "os": "win32", "cpu": "x64" }, "sha512-bm4Mowrv+GXMlpWX++EcXw/iLyd1o3+bJkC2DkWXYVvgZCqD/bSj9ctZeAMC3cIxgjRVR2Dufaiu4YPxr5gW1A=="],
|
||||||
|
|
||||||
|
"@jridgewell/gen-mapping": ["@jridgewell/gen-mapping@0.3.13", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.0", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA=="],
|
||||||
|
|
||||||
|
"@jridgewell/resolve-uri": ["@jridgewell/resolve-uri@3.1.2", "", {}, "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw=="],
|
||||||
|
|
||||||
|
"@jridgewell/source-map": ["@jridgewell/source-map@0.3.11", "", { "dependencies": { "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.25" } }, "sha512-ZMp1V8ZFcPG5dIWnQLr3NSI1MiCU7UETdS/A0G8V/XWHvJv3ZsFqutJn1Y5RPmAPX6F3BiE397OqveU/9NCuIA=="],
|
||||||
|
|
||||||
|
"@jridgewell/sourcemap-codec": ["@jridgewell/sourcemap-codec@1.5.5", "", {}, "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og=="],
|
||||||
|
|
||||||
|
"@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.31", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw=="],
|
||||||
|
|
||||||
|
"@mediabunny/aac-encoder": ["@mediabunny/aac-encoder@1.47.0", "", { "peerDependencies": { "mediabunny": "^1.0.0" } }, "sha512-JNzgdJoHMFFnv5imi1+dmjZMudsJ1zNCUCEkKjBh90cqGhAFg8xu4V2gsyxE2i6oq/YpWx23P+OvoANLSMcDzA=="],
|
||||||
|
|
||||||
|
"@mediabunny/flac-encoder": ["@mediabunny/flac-encoder@1.47.0", "", { "peerDependencies": { "mediabunny": "^1.0.0" } }, "sha512-VpKmJO0xlYcFCRD6JvJlMbNQ6d/6YMHdO1gFIqWlZABHjSSL6BquNNEgWSCv5vdF8ELDBwIYBVZEslakIB/7GA=="],
|
||||||
|
|
||||||
|
"@mediabunny/mp3-encoder": ["@mediabunny/mp3-encoder@1.47.0", "", { "peerDependencies": { "mediabunny": "^1.0.0" } }, "sha512-JyzZyGeGRm2HVUQaGJ/VZT9OG+kG00mA8SLP/f3CO7+qWAmBncKc16WYXWHbZEo8Jn/ZGA6De32S5R2bTQbDqA=="],
|
||||||
|
|
||||||
|
"@module-federation/error-codes": ["@module-federation/error-codes@0.22.0", "", {}, "sha512-xF9SjnEy7vTdx+xekjPCV5cIHOGCkdn3pIxo9vU7gEZMIw0SvAEdsy6Uh17xaCpm8V0FWvR0SZoK9Ik6jGOaug=="],
|
||||||
|
|
||||||
|
"@module-federation/runtime": ["@module-federation/runtime@0.22.0", "", { "dependencies": { "@module-federation/error-codes": "0.22.0", "@module-federation/runtime-core": "0.22.0", "@module-federation/sdk": "0.22.0" } }, "sha512-38g5iPju2tPC3KHMPxRKmy4k4onNp6ypFPS1eKGsNLUkXgHsPMBFqAjDw96iEcjri91BrahG4XcdyKi97xZzlA=="],
|
||||||
|
|
||||||
|
"@module-federation/runtime-core": ["@module-federation/runtime-core@0.22.0", "", { "dependencies": { "@module-federation/error-codes": "0.22.0", "@module-federation/sdk": "0.22.0" } }, "sha512-GR1TcD6/s7zqItfhC87zAp30PqzvceoeDGYTgF3Vx2TXvsfDrhP6Qw9T4vudDQL3uJRne6t7CzdT29YyVxlgIA=="],
|
||||||
|
|
||||||
|
"@module-federation/runtime-tools": ["@module-federation/runtime-tools@0.22.0", "", { "dependencies": { "@module-federation/runtime": "0.22.0", "@module-federation/webpack-bundler-runtime": "0.22.0" } }, "sha512-4ScUJ/aUfEernb+4PbLdhM/c60VHl698Gn1gY21m9vyC1Ucn69fPCA1y2EwcCB7IItseRMoNhdcWQnzt/OPCNA=="],
|
||||||
|
|
||||||
|
"@module-federation/sdk": ["@module-federation/sdk@0.22.0", "", {}, "sha512-x4aFNBKn2KVQRuNVC5A7SnrSCSqyfIWmm1DvubjbO9iKFe7ith5niw8dqSFBekYBg2Fwy+eMg4sEFNVvCAdo6g=="],
|
||||||
|
|
||||||
|
"@module-federation/webpack-bundler-runtime": ["@module-federation/webpack-bundler-runtime@0.22.0", "", { "dependencies": { "@module-federation/runtime": "0.22.0", "@module-federation/sdk": "0.22.0" } }, "sha512-aM8gCqXu+/4wBmJtVeMeeMN5guw3chf+2i6HajKtQv7SJfxV/f4IyNQJUeUQu9HfiAZHjqtMV5Lvq/Lvh8LdyA=="],
|
||||||
|
|
||||||
|
"@napi-rs/wasm-runtime": ["@napi-rs/wasm-runtime@1.0.7", "", { "dependencies": { "@emnapi/core": "^1.5.0", "@emnapi/runtime": "^1.5.0", "@tybys/wasm-util": "^0.10.1" } }, "sha512-SeDnOO0Tk7Okiq6DbXmmBODgOAb9dp9gjlphokTUxmt8U3liIP1ZsozBahH69j/RJv+Rfs6IwUKHTgQYJ/HBAw=="],
|
||||||
|
|
||||||
|
"@remotion/bundler": ["@remotion/bundler@4.0.483", "", { "dependencies": { "@remotion/media-parser": "4.0.483", "@remotion/studio": "4.0.483", "@remotion/studio-shared": "4.0.483", "@remotion/timeline-utils": "4.0.483", "@rspack/core": "1.7.11", "@rspack/plugin-react-refresh": "1.6.1", "css-loader": "7.1.4", "esbuild": "0.28.1", "react-refresh": "0.18.0", "remotion": "4.0.483", "style-loader": "4.0.0", "webpack": "5.105.0" }, "peerDependencies": { "react": ">=16.8.0", "react-dom": ">=16.8.0" } }, "sha512-5fQRMYgr2sxZPBOThnIVuFwHBq/FhusJ+Ke6xopga8io8ecK/mj9GaRXC3tgfMa+YWuu8ZSXZ0U9EsMZjjP5YQ=="],
|
||||||
|
|
||||||
|
"@remotion/canvas-capture": ["@remotion/canvas-capture@4.0.483", "", { "dependencies": { "mediabunny": "1.47.0", "remotion": "4.0.483" }, "peerDependencies": { "react": ">=16.8.0", "react-dom": ">=16.8.0" } }, "sha512-JABgbfvXwjFp1E61tlDN9gVAH8OoeF4kUUELNwG9kcNOE4ex8+vCYEO6VKIaXZ4l8A1PEAQ5EK//NheTJ80gfw=="],
|
||||||
|
|
||||||
|
"@remotion/cli": ["@remotion/cli@4.0.483", "", { "dependencies": { "@remotion/bundler": "4.0.483", "@remotion/media-utils": "4.0.483", "@remotion/player": "4.0.483", "@remotion/renderer": "4.0.483", "@remotion/studio": "4.0.483", "@remotion/studio-server": "4.0.483", "@remotion/studio-shared": "4.0.483", "dotenv": "17.3.1", "minimist": "1.2.6", "prompts": "2.4.2", "remotion": "4.0.483" }, "peerDependencies": { "react": ">=16.8.0", "react-dom": ">=16.8.0" }, "bin": { "remotion": "remotion-cli.js", "remotionb": "remotionb-cli.js", "remotiond": "remotiond-cli.js" } }, "sha512-z4TQcgTfQbSIV4GQpCvIihDL77M+leQelbxv962PmfSKTT7z3XnNN732PcZ4sei2+Xg+d2SD95K4tI5XBYs5Gw=="],
|
||||||
|
|
||||||
|
"@remotion/compositor-darwin-arm64": ["@remotion/compositor-darwin-arm64@4.0.483", "", { "os": "darwin", "cpu": "arm64" }, "sha512-CkEJXouGEoCs5PX3KOufkXrlT/Kgn0ZF7SEQ4meCYGYMZD+QPVtsmS1MXH8pQ4RHlFO6Tk7g0CIHMT3Y4yzpuA=="],
|
||||||
|
|
||||||
|
"@remotion/compositor-darwin-x64": ["@remotion/compositor-darwin-x64@4.0.483", "", { "os": "darwin", "cpu": "x64" }, "sha512-vDZK5U8FYbHGMjFC2lUgJRBz8KXwbwFSvEQi8FATD0roNg75SoaEJWrqAOIVv8v5eBjPvo18znDc36NLRYn1Eg=="],
|
||||||
|
|
||||||
|
"@remotion/compositor-linux-arm64-gnu": ["@remotion/compositor-linux-arm64-gnu@4.0.483", "", { "os": "linux", "cpu": "arm64" }, "sha512-1LzfYDY/DQxy3MIc9pCbTn1ObcBDcyI6MzODOwe1bTZl8TZxdUUt1M3hDrQ60AB6iHwruBAnzT9dnogiMIdSew=="],
|
||||||
|
|
||||||
|
"@remotion/compositor-linux-arm64-musl": ["@remotion/compositor-linux-arm64-musl@4.0.483", "", { "os": "linux", "cpu": "arm64" }, "sha512-nG1btxg0HXuLzulIqAg9NnxWyewZNrA0iLe82HdniaJjeWLORreGo4cEEJz1SjbDpQuPEY562XGkTO2DLKK5mA=="],
|
||||||
|
|
||||||
|
"@remotion/compositor-linux-x64-gnu": ["@remotion/compositor-linux-x64-gnu@4.0.483", "", { "os": "linux", "cpu": "x64" }, "sha512-CV4h6r2rNYh4P8utUzP6RM2vW2vq43nA3IvVOwRSuh9LwJh/7rwG53Oco4HanY/sstaNuymGpT57wdXwONU5CQ=="],
|
||||||
|
|
||||||
|
"@remotion/compositor-linux-x64-musl": ["@remotion/compositor-linux-x64-musl@4.0.483", "", { "os": "linux", "cpu": "x64" }, "sha512-moKvavbkoz5nteJZA4K6LMgTTlewhwIbtO7DYARZ3LK3ClGUKnYfFcmaPS15hMun02JPTOMIWbrlaszdR75mxA=="],
|
||||||
|
|
||||||
|
"@remotion/compositor-win32-x64-msvc": ["@remotion/compositor-win32-x64-msvc@4.0.483", "", { "os": "win32", "cpu": "x64" }, "sha512-eDpWe6Vy7ySHugwxlgaeohqdBwR1etxiymXzT13cgbPw2+p6d55z1tJ7VPTXyfu0o+UdyRTauWqD4msoxN0wow=="],
|
||||||
|
|
||||||
|
"@remotion/licensing": ["@remotion/licensing@4.0.483", "", {}, "sha512-GRtjykrxMmB5Cjpq1oVqeYIRXI95vid6afX0LTLfp1shD6MR4lTZRoRuopxrrZp1E4XFGXoY64b6dlIudgTyBQ=="],
|
||||||
|
|
||||||
|
"@remotion/media-parser": ["@remotion/media-parser@4.0.483", "", {}, "sha512-iva9Eof7QQX+3hGxH/N8pGi80wgdGuzOr825Zn7IuEe7IWdtRYWlFpCEghmyfhrwgaWa7RY51wg77ycR3c3nRg=="],
|
||||||
|
|
||||||
|
"@remotion/media-utils": ["@remotion/media-utils@4.0.483", "", { "dependencies": { "mediabunny": "1.47.0", "remotion": "4.0.483" }, "peerDependencies": { "react": ">=16.8.0", "react-dom": ">=16.8.0" } }, "sha512-4T4WQh/U95kCBzgmz/EL/HL8Zeox/AKAL5ufsehrZFgwlcvH1EsKNJH/RWaAtNPRLF+pskEOgFzPYaczHeI8hg=="],
|
||||||
|
|
||||||
|
"@remotion/player": ["@remotion/player@4.0.483", "", { "dependencies": { "remotion": "4.0.483" }, "peerDependencies": { "react": ">=16.8.0", "react-dom": ">=16.8.0" } }, "sha512-yyIQ9vinUqqS0fQeL52bPCWQIr8qDXCqQ4iys8P7dSZncdu8V29p4W2a0/4eKV8bv5+mjnJQ/U6nl9NxBF1hdA=="],
|
||||||
|
|
||||||
|
"@remotion/renderer": ["@remotion/renderer@4.0.483", "", { "dependencies": { "@remotion/licensing": "4.0.483", "@remotion/streaming": "4.0.483", "execa": "5.1.1", "remotion": "4.0.483", "source-map": "0.8.0-beta.0", "ws": "8.21.0" }, "optionalDependencies": { "@remotion/compositor-darwin-arm64": "4.0.483", "@remotion/compositor-darwin-x64": "4.0.483", "@remotion/compositor-linux-arm64-gnu": "4.0.483", "@remotion/compositor-linux-arm64-musl": "4.0.483", "@remotion/compositor-linux-x64-gnu": "4.0.483", "@remotion/compositor-linux-x64-musl": "4.0.483", "@remotion/compositor-win32-x64-msvc": "4.0.483" }, "peerDependencies": { "react": ">=16.8.0", "react-dom": ">=16.8.0" } }, "sha512-eSKdQqm8rcl+GNyOhFsBgcPYWMcxTJWHL5xaNrd037piCaL3sdJ57OEO/AbfEvvOO14zwUvlsONKjlkLlEVyKg=="],
|
||||||
|
|
||||||
|
"@remotion/streaming": ["@remotion/streaming@4.0.483", "", {}, "sha512-96rqrk+l5AfriHOqmqxMn8rI6UJfFY7r5UMDZoSKqXWzTOwj+0VWHTRl9ilZQMXhYACaMSA/5yrL9jgQEK8Yuw=="],
|
||||||
|
|
||||||
|
"@remotion/studio": ["@remotion/studio@4.0.483", "", { "dependencies": { "@jridgewell/trace-mapping": "0.3.31", "@remotion/canvas-capture": "4.0.483", "@remotion/media-utils": "4.0.483", "@remotion/player": "4.0.483", "@remotion/renderer": "4.0.483", "@remotion/studio-shared": "4.0.483", "@remotion/timeline-utils": "4.0.483", "@remotion/web-renderer": "4.0.483", "@remotion/zod-types": "4.0.483", "mediabunny": "1.47.0", "memfs": "3.4.3", "open": "8.4.2", "remotion": "4.0.483", "semver": "7.5.3", "zod": "4.3.6" }, "peerDependencies": { "react": ">=16.8.0", "react-dom": ">=16.8.0" } }, "sha512-gNa1Lw8NmHl0jwFb5fk7rzA+WFTJQpGDT/49A38t9jpEwoF+h1mKTgY8gA202apzjBHrEECdFE450npnBOUpUw=="],
|
||||||
|
|
||||||
|
"@remotion/studio-server": ["@remotion/studio-server@4.0.483", "", { "dependencies": { "@babel/parser": "7.24.1", "@babel/types": "7.24.0", "@remotion/bundler": "4.0.483", "@remotion/renderer": "4.0.483", "@remotion/studio-shared": "4.0.483", "memfs": "3.4.3", "open": "8.4.2", "prettier": "3.8.1", "recast": "0.23.11", "remotion": "4.0.483", "semver": "7.5.3" } }, "sha512-2FSLbVN5N8bgtQPX+RQRbis557tJJnVTOZuEBZndfC9NOoPdARlSgEnwVer9tBNczUlc6B3KrPxyPSlS3UjL+Q=="],
|
||||||
|
|
||||||
|
"@remotion/studio-shared": ["@remotion/studio-shared@4.0.483", "", { "dependencies": { "remotion": "4.0.483" } }, "sha512-aXxBYUgsWgphHVq7T7bz+ICMGzfii7pcX1IxS5hbLMLJHqlDtk9g+qcxm4kJ2Fjj/Gb5gERKiNzYym8WZTIM2Q=="],
|
||||||
|
|
||||||
|
"@remotion/timeline-utils": ["@remotion/timeline-utils@4.0.483", "", { "dependencies": { "mediabunny": "1.47.0" } }, "sha512-KTTMdpNA5uRLX+iisAITO+V9cJBk17F8EWvuUnAEVmAFOQTbGIyPI9HNw5pZmL7SlzOJdFjAtksTjipeYGkEug=="],
|
||||||
|
|
||||||
|
"@remotion/web-renderer": ["@remotion/web-renderer@4.0.483", "", { "dependencies": { "@mediabunny/aac-encoder": "1.47.0", "@mediabunny/flac-encoder": "1.47.0", "@mediabunny/mp3-encoder": "1.47.0", "@remotion/licensing": "4.0.483", "mediabunny": "1.47.0", "remotion": "4.0.483" }, "peerDependencies": { "react": ">=18.0.0", "react-dom": ">=18.0.0" } }, "sha512-14iURfVWA/hcF/rJvkid1ZR5nmnVhkTIf071FabCFi6kdViOSVBugSglJ8tUPvWVbWmxlXGDGQD9CSU18IATww=="],
|
||||||
|
|
||||||
|
"@remotion/zod-types": ["@remotion/zod-types@4.0.483", "", { "dependencies": { "remotion": "4.0.483" }, "peerDependencies": { "zod": "4.3.6" } }, "sha512-KIVYIzFpZBDB0wxGGQu5tDBr4XOYRzvrPXjmo6O/FI5Xx7O8hgvJmBs9VDkEwonAOft86sg79hI19F1DTX1p4w=="],
|
||||||
|
|
||||||
|
"@rspack/binding": ["@rspack/binding@1.7.11", "", { "optionalDependencies": { "@rspack/binding-darwin-arm64": "1.7.11", "@rspack/binding-darwin-x64": "1.7.11", "@rspack/binding-linux-arm64-gnu": "1.7.11", "@rspack/binding-linux-arm64-musl": "1.7.11", "@rspack/binding-linux-x64-gnu": "1.7.11", "@rspack/binding-linux-x64-musl": "1.7.11", "@rspack/binding-wasm32-wasi": "1.7.11", "@rspack/binding-win32-arm64-msvc": "1.7.11", "@rspack/binding-win32-ia32-msvc": "1.7.11", "@rspack/binding-win32-x64-msvc": "1.7.11" } }, "sha512-2MGdy2s2HimsDT444Bp5XnALzNRxuBNc7y0JzyuqKbHBywd4x2NeXyhWXXoxufaCFu5PBc9Qq9jyfjW2Aeh06Q=="],
|
||||||
|
|
||||||
|
"@rspack/binding-darwin-arm64": ["@rspack/binding-darwin-arm64@1.7.11", "", { "os": "darwin", "cpu": "arm64" }, "sha512-oduECiZVqbO5zlVw+q7Vy65sJFth99fWPTyucwvLJJtJkPL5n17Uiql2cYP6Ijn0pkqtf1SXgK8WjiKLG5bIig=="],
|
||||||
|
|
||||||
|
"@rspack/binding-darwin-x64": ["@rspack/binding-darwin-x64@1.7.11", "", { "os": "darwin", "cpu": "x64" }, "sha512-a1+TtTE9ap6RalgFi7FGIgkJP6O4Vy6ctv+9WGJy53E4kuqHR0RygzaiVxCI/GMc/vBT9vY23hyrpWb3d1vtXA=="],
|
||||||
|
|
||||||
|
"@rspack/binding-linux-arm64-gnu": ["@rspack/binding-linux-arm64-gnu@1.7.11", "", { "os": "linux", "cpu": "arm64" }, "sha512-P0QrGRPbTWu6RKWfN0bDtbnEps3rXH0MWIMreZABoUrVmNQKtXR6e73J3ub6a+di5s2+K0M2LJ9Bh2/H4UsDUA=="],
|
||||||
|
|
||||||
|
"@rspack/binding-linux-arm64-musl": ["@rspack/binding-linux-arm64-musl@1.7.11", "", { "os": "linux", "cpu": "arm64" }, "sha512-6ky7R43VMjWwmx3Yx7Jl7faLBBMAgMDt+/bN35RgwjiPgsIByz65EwytUVuW9rikB43BGHvA/eqlnjLrUzNBqw=="],
|
||||||
|
|
||||||
|
"@rspack/binding-linux-x64-gnu": ["@rspack/binding-linux-x64-gnu@1.7.11", "", { "os": "linux", "cpu": "x64" }, "sha512-cuOJMfCOvb2Wgsry5enXJ3iT1FGUjdPqtGUBVupQlEG4ntSYsQ2PtF4wIDVasR3wdxC5nQbipOrDiN/u6fYsdQ=="],
|
||||||
|
|
||||||
|
"@rspack/binding-linux-x64-musl": ["@rspack/binding-linux-x64-musl@1.7.11", "", { "os": "linux", "cpu": "x64" }, "sha512-CoK37hva4AmHGh3VCsQXmGr40L36m1/AdnN5LEjUX6kx5rEH7/1nEBN6Ii72pejqDVvk9anEROmPDiPw10tpFg=="],
|
||||||
|
|
||||||
|
"@rspack/binding-wasm32-wasi": ["@rspack/binding-wasm32-wasi@1.7.11", "", { "dependencies": { "@napi-rs/wasm-runtime": "1.0.7" }, "cpu": "none" }, "sha512-OtrmnPUVJMxjNa3eDMfHyPdtlLRmmp/aIm0fQHlAOATbZvlGm12q7rhPW5BXTu1yh+1rQ1/uqvz+SzKEZXuJaQ=="],
|
||||||
|
|
||||||
|
"@rspack/binding-win32-arm64-msvc": ["@rspack/binding-win32-arm64-msvc@1.7.11", "", { "os": "win32", "cpu": "arm64" }, "sha512-lObFW6e5lCWNgTBNwT//yiEDbsxm9QG4BYUojqeXxothuzJ/L6ibXz6+gLMvbOvLGV3nKgkXmx8GvT9WDKR0mA=="],
|
||||||
|
|
||||||
|
"@rspack/binding-win32-ia32-msvc": ["@rspack/binding-win32-ia32-msvc@1.7.11", "", { "os": "win32", "cpu": "ia32" }, "sha512-0pYGnZd8PPqNR68zQ8skamqNAXEA1sUfXuAdYcknIIRq2wsbiwFzIc0Pov1cIfHYab37G7sSIPBiOUdOWF5Ivw=="],
|
||||||
|
|
||||||
|
"@rspack/binding-win32-x64-msvc": ["@rspack/binding-win32-x64-msvc@1.7.11", "", { "os": "win32", "cpu": "x64" }, "sha512-EeQXayoQk/uBkI3pdoXfQBXNIUrADq56L3s/DFyM2pJeUDrWmhfIw2UFIGkYPTMSCo8F2JcdcGM32FGJrSnU0Q=="],
|
||||||
|
|
||||||
|
"@rspack/core": ["@rspack/core@1.7.11", "", { "dependencies": { "@module-federation/runtime-tools": "0.22.0", "@rspack/binding": "1.7.11", "@rspack/lite-tapable": "1.1.0" }, "peerDependencies": { "@swc/helpers": ">=0.5.1" }, "optionalPeers": ["@swc/helpers"] }, "sha512-rsD9b+Khmot5DwCMiB3cqTQo53ioPG3M/A7BySu8+0+RS7GCxKm+Z+mtsjtG/vsu4Tn2tcqCdZtA3pgLoJB+ew=="],
|
||||||
|
|
||||||
|
"@rspack/lite-tapable": ["@rspack/lite-tapable@1.1.0", "", {}, "sha512-E2B0JhYFmVAwdDiG14+DW0Di4Ze4Jg10Pc4/lILUrd5DRCaklduz2OvJ5HYQ6G+hd+WTzqQb3QnDNfK4yvAFYw=="],
|
||||||
|
|
||||||
|
"@rspack/plugin-react-refresh": ["@rspack/plugin-react-refresh@1.6.1", "", { "dependencies": { "error-stack-parser": "^2.1.4", "html-entities": "^2.6.0" }, "peerDependencies": { "react-refresh": ">=0.10.0 <1.0.0", "webpack-hot-middleware": "2.x" }, "optionalPeers": ["webpack-hot-middleware"] }, "sha512-eqqW5645VG3CzGzFgNg5HqNdHVXY+567PGjtDhhrM8t67caxmsSzRmT5qfoEIfBcGgFkH9vEg7kzXwmCYQdQDw=="],
|
||||||
|
|
||||||
|
"@tybys/wasm-util": ["@tybys/wasm-util@0.10.3", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-F3fo1MYrRJYL3zER0OUOmkutjr1Vp23m7OsSgp7nq4SP6OqX6C/56XFIPAl5bt3zaBRjmW7SGz3u/6LwFpYcOg=="],
|
||||||
|
|
||||||
|
"@types/dom-mediacapture-transform": ["@types/dom-mediacapture-transform@0.1.11", "", { "dependencies": { "@types/dom-webcodecs": "*" } }, "sha512-Y2p+nGf1bF2XMttBnsVPHUWzRRZzqUoJAKmiP10b5umnO6DDrWI0BrGDJy1pOHoOULVmGSfFNkQrAlC5dcj6nQ=="],
|
||||||
|
|
||||||
|
"@types/dom-webcodecs": ["@types/dom-webcodecs@0.1.13", "", {}, "sha512-O5hkiFIcjjszPIYyUSyvScyvrBoV3NOEEZx/pMlsu44TKzWNkLVBBxnxJz42in5n3QIolYOcBYFCPZZ0h8SkwQ=="],
|
||||||
|
|
||||||
|
"@types/eslint": ["@types/eslint@9.6.1", "", { "dependencies": { "@types/estree": "*", "@types/json-schema": "*" } }, "sha512-FXx2pKgId/WyYo2jXw63kk7/+TY7u7AziEJxJAnSFzHlqTAS3Ync6SvgYAN/k4/PQpnnVuzoMuVnByKK2qp0ag=="],
|
||||||
|
|
||||||
|
"@types/eslint-scope": ["@types/eslint-scope@3.7.7", "", { "dependencies": { "@types/eslint": "*", "@types/estree": "*" } }, "sha512-MzMFlSLBqNF2gcHWO0G1vP/YQyfvrxZ0bF+u7mzUdZ1/xK4A4sru+nraZz5i3iEIk1l1uyicaDVTB4QbbEkAYg=="],
|
||||||
|
|
||||||
|
"@types/estree": ["@types/estree@1.0.9", "", {}, "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg=="],
|
||||||
|
|
||||||
|
"@types/json-schema": ["@types/json-schema@7.0.15", "", {}, "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA=="],
|
||||||
|
|
||||||
|
"@types/node": ["@types/node@26.0.1", "", { "dependencies": { "undici-types": "~8.3.0" } }, "sha512-fc3KiUoBt6kie0N9bIW3E47vZsuaMf0PM2AaUpLCLT0s/LvX1nxAim6Fc049cNxODPpGm6qRAuUOB86SkRuPQw=="],
|
||||||
|
|
||||||
|
"@types/react": ["@types/react@19.2.17", "", { "dependencies": { "csstype": "^3.2.2" } }, "sha512-MXfmqaVPEVgkBT/aY0aGCkRWWtByiYQXo3xdQ8r5RzuFrPiRn8Gar2tQdXSUQ2GKV3bkXckek89V8wQBY2Q/Aw=="],
|
||||||
|
|
||||||
|
"@types/react-dom": ["@types/react-dom@19.2.3", "", { "peerDependencies": { "@types/react": "^19.2.0" } }, "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ=="],
|
||||||
|
|
||||||
|
"@webassemblyjs/ast": ["@webassemblyjs/ast@1.14.1", "", { "dependencies": { "@webassemblyjs/helper-numbers": "1.13.2", "@webassemblyjs/helper-wasm-bytecode": "1.13.2" } }, "sha512-nuBEDgQfm1ccRp/8bCQrx1frohyufl4JlbMMZ4P1wpeOfDhF6FQkxZJ1b/e+PLwr6X1Nhw6OLme5usuBWYBvuQ=="],
|
||||||
|
|
||||||
|
"@webassemblyjs/floating-point-hex-parser": ["@webassemblyjs/floating-point-hex-parser@1.13.2", "", {}, "sha512-6oXyTOzbKxGH4steLbLNOu71Oj+C8Lg34n6CqRvqfS2O71BxY6ByfMDRhBytzknj9yGUPVJ1qIKhRlAwO1AovA=="],
|
||||||
|
|
||||||
|
"@webassemblyjs/helper-api-error": ["@webassemblyjs/helper-api-error@1.13.2", "", {}, "sha512-U56GMYxy4ZQCbDZd6JuvvNV/WFildOjsaWD3Tzzvmw/mas3cXzRJPMjP83JqEsgSbyrmaGjBfDtV7KDXV9UzFQ=="],
|
||||||
|
|
||||||
|
"@webassemblyjs/helper-buffer": ["@webassemblyjs/helper-buffer@1.14.1", "", {}, "sha512-jyH7wtcHiKssDtFPRB+iQdxlDf96m0E39yb0k5uJVhFGleZFoNw1c4aeIcVUPPbXUVJ94wwnMOAqUHyzoEPVMA=="],
|
||||||
|
|
||||||
|
"@webassemblyjs/helper-numbers": ["@webassemblyjs/helper-numbers@1.13.2", "", { "dependencies": { "@webassemblyjs/floating-point-hex-parser": "1.13.2", "@webassemblyjs/helper-api-error": "1.13.2", "@xtuc/long": "4.2.2" } }, "sha512-FE8aCmS5Q6eQYcV3gI35O4J789wlQA+7JrqTTpJqn5emA4U2hvwJmvFRC0HODS+3Ye6WioDklgd6scJ3+PLnEA=="],
|
||||||
|
|
||||||
|
"@webassemblyjs/helper-wasm-bytecode": ["@webassemblyjs/helper-wasm-bytecode@1.13.2", "", {}, "sha512-3QbLKy93F0EAIXLh0ogEVR6rOubA9AoZ+WRYhNbFyuB70j3dRdwH9g+qXhLAO0kiYGlg3TxDV+I4rQTr/YNXkA=="],
|
||||||
|
|
||||||
|
"@webassemblyjs/helper-wasm-section": ["@webassemblyjs/helper-wasm-section@1.14.1", "", { "dependencies": { "@webassemblyjs/ast": "1.14.1", "@webassemblyjs/helper-buffer": "1.14.1", "@webassemblyjs/helper-wasm-bytecode": "1.13.2", "@webassemblyjs/wasm-gen": "1.14.1" } }, "sha512-ds5mXEqTJ6oxRoqjhWDU83OgzAYjwsCV8Lo/N+oRsNDmx/ZDpqalmrtgOMkHwxsG0iI//3BwWAErYRHtgn0dZw=="],
|
||||||
|
|
||||||
|
"@webassemblyjs/ieee754": ["@webassemblyjs/ieee754@1.13.2", "", { "dependencies": { "@xtuc/ieee754": "^1.2.0" } }, "sha512-4LtOzh58S/5lX4ITKxnAK2USuNEvpdVV9AlgGQb8rJDHaLeHciwG4zlGr0j/SNWlr7x3vO1lDEsuePvtcDNCkw=="],
|
||||||
|
|
||||||
|
"@webassemblyjs/leb128": ["@webassemblyjs/leb128@1.13.2", "", { "dependencies": { "@xtuc/long": "4.2.2" } }, "sha512-Lde1oNoIdzVzdkNEAWZ1dZ5orIbff80YPdHx20mrHwHrVNNTjNr8E3xz9BdpcGqRQbAEa+fkrCb+fRFTl/6sQw=="],
|
||||||
|
|
||||||
|
"@webassemblyjs/utf8": ["@webassemblyjs/utf8@1.13.2", "", {}, "sha512-3NQWGjKTASY1xV5m7Hr0iPeXD9+RDobLll3T9d2AO+g3my8xy5peVyjSag4I50mR1bBSN/Ct12lo+R9tJk0NZQ=="],
|
||||||
|
|
||||||
|
"@webassemblyjs/wasm-edit": ["@webassemblyjs/wasm-edit@1.14.1", "", { "dependencies": { "@webassemblyjs/ast": "1.14.1", "@webassemblyjs/helper-buffer": "1.14.1", "@webassemblyjs/helper-wasm-bytecode": "1.13.2", "@webassemblyjs/helper-wasm-section": "1.14.1", "@webassemblyjs/wasm-gen": "1.14.1", "@webassemblyjs/wasm-opt": "1.14.1", "@webassemblyjs/wasm-parser": "1.14.1", "@webassemblyjs/wast-printer": "1.14.1" } }, "sha512-RNJUIQH/J8iA/1NzlE4N7KtyZNHi3w7at7hDjvRNm5rcUXa00z1vRz3glZoULfJ5mpvYhLybmVcwcjGrC1pRrQ=="],
|
||||||
|
|
||||||
|
"@webassemblyjs/wasm-gen": ["@webassemblyjs/wasm-gen@1.14.1", "", { "dependencies": { "@webassemblyjs/ast": "1.14.1", "@webassemblyjs/helper-wasm-bytecode": "1.13.2", "@webassemblyjs/ieee754": "1.13.2", "@webassemblyjs/leb128": "1.13.2", "@webassemblyjs/utf8": "1.13.2" } }, "sha512-AmomSIjP8ZbfGQhumkNvgC33AY7qtMCXnN6bL2u2Js4gVCg8fp735aEiMSBbDR7UQIj90n4wKAFUSEd0QN2Ukg=="],
|
||||||
|
|
||||||
|
"@webassemblyjs/wasm-opt": ["@webassemblyjs/wasm-opt@1.14.1", "", { "dependencies": { "@webassemblyjs/ast": "1.14.1", "@webassemblyjs/helper-buffer": "1.14.1", "@webassemblyjs/wasm-gen": "1.14.1", "@webassemblyjs/wasm-parser": "1.14.1" } }, "sha512-PTcKLUNvBqnY2U6E5bdOQcSM+oVP/PmrDY9NzowJjislEjwP/C4an2303MCVS2Mg9d3AJpIGdUFIQQWbPds0Sw=="],
|
||||||
|
|
||||||
|
"@webassemblyjs/wasm-parser": ["@webassemblyjs/wasm-parser@1.14.1", "", { "dependencies": { "@webassemblyjs/ast": "1.14.1", "@webassemblyjs/helper-api-error": "1.13.2", "@webassemblyjs/helper-wasm-bytecode": "1.13.2", "@webassemblyjs/ieee754": "1.13.2", "@webassemblyjs/leb128": "1.13.2", "@webassemblyjs/utf8": "1.13.2" } }, "sha512-JLBl+KZ0R5qB7mCnud/yyX08jWFw5MsoalJ1pQ4EdFlgj9VdXKGuENGsiCIjegI1W7p91rUlcB/LB5yRJKNTcQ=="],
|
||||||
|
|
||||||
|
"@webassemblyjs/wast-printer": ["@webassemblyjs/wast-printer@1.14.1", "", { "dependencies": { "@webassemblyjs/ast": "1.14.1", "@xtuc/long": "4.2.2" } }, "sha512-kPSSXE6De1XOR820C90RIo2ogvZG+c3KiHzqUoO/F34Y2shGzesfqv7o57xrxovZJH/MetF5UjroJ/R/3isoiw=="],
|
||||||
|
|
||||||
|
"@xtuc/ieee754": ["@xtuc/ieee754@1.2.0", "", {}, "sha512-DX8nKgqcGwsc0eJSqYt5lwP4DH5FlHnmuWWBRy7X0NcaGR0ZtuyeESgMwTYVEtxmsNGY+qit4QYT/MIYTOTPeA=="],
|
||||||
|
|
||||||
|
"@xtuc/long": ["@xtuc/long@4.2.2", "", {}, "sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ=="],
|
||||||
|
|
||||||
|
"acorn": ["acorn@8.17.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-xRQbDb9BnwDafYNn6Vwl839DYVjqXYb1XVGtWAZ1kcDc6iwAL4hg3B1dZlRiuENFeO2H53gFG3in621AdERVAg=="],
|
||||||
|
|
||||||
|
"acorn-import-phases": ["acorn-import-phases@1.0.4", "", { "peerDependencies": { "acorn": "^8.14.0" } }, "sha512-wKmbr/DDiIXzEOiWrTTUcDm24kQ2vGfZQvM2fwg2vXqR5uW6aapr7ObPtj1th32b9u90/Pf4AItvdTh42fBmVQ=="],
|
||||||
|
|
||||||
|
"ajv": ["ajv@8.20.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-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA=="],
|
||||||
|
|
||||||
|
"ajv-formats": ["ajv-formats@2.1.1", "", { "dependencies": { "ajv": "^8.0.0" } }, "sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA=="],
|
||||||
|
|
||||||
|
"ajv-keywords": ["ajv-keywords@5.1.0", "", { "dependencies": { "fast-deep-equal": "^3.1.3" }, "peerDependencies": { "ajv": "^8.8.2" } }, "sha512-YCS/JNFAUyr5vAuhk1DWm1CBxRHW9LbJ2ozWeemrIqpbsqKjHVxYPyi5GC0rjZIT5JxJ3virVTS8wk4i/Z+krw=="],
|
||||||
|
|
||||||
|
"ast-types": ["ast-types@0.16.1", "", { "dependencies": { "tslib": "^2.0.1" } }, "sha512-6t10qk83GOG8p0vKmaCr8eiilZwO171AvbROMtvvNiwrTly62t+7XkA8RdIIVbpMhCASAsxgAzdRSwh6nw/5Dg=="],
|
||||||
|
|
||||||
|
"baseline-browser-mapping": ["baseline-browser-mapping@2.10.40", "", { "bin": { "baseline-browser-mapping": "dist/cli.cjs" } }, "sha512-BSSLZ9/Cjjv7Gtj5B68ZzXcXUg8iOf3fme+FCuh8rC/Go+Kmh8cox7M3A8dolou16s64QjLPOSdngh7GxXvkSw=="],
|
||||||
|
|
||||||
|
"browserslist": ["browserslist@4.28.4", "", { "dependencies": { "baseline-browser-mapping": "^2.10.38", "caniuse-lite": "^1.0.30001799", "electron-to-chromium": "^1.5.376", "node-releases": "^2.0.48", "update-browserslist-db": "^1.2.3" }, "bin": { "browserslist": "cli.js" } }, "sha512-MTc8i/x9jBQd1iMw2CFGS+rwMa07eYjLR0CCTLDACl9xhxy+nIs3KeML/biicXtk9JrZ6dnnTatmc7ErPXIxqw=="],
|
||||||
|
|
||||||
|
"buffer-from": ["buffer-from@1.1.2", "", {}, "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ=="],
|
||||||
|
|
||||||
|
"caniuse-lite": ["caniuse-lite@1.0.30001799", "", {}, "sha512-hG1bReV+OUU+MOqK4t/ZWI0tZOyz3rqS9XuhOUz1cIcbwBKjOyJEJuw9ER5JuNyqxNk8u/JUVbGibBOL1yrjFw=="],
|
||||||
|
|
||||||
|
"chrome-trace-event": ["chrome-trace-event@1.0.4", "", {}, "sha512-rNjApaLzuwaOTjCiT8lSDdGN1APCiqkChLMJxJPWLunPAt5fy8xgU9/jNOchV84wfIxrA0lRQB7oCT8jrn/wrQ=="],
|
||||||
|
|
||||||
|
"commander": ["commander@2.20.3", "", {}, "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ=="],
|
||||||
|
|
||||||
|
"cross-spawn": ["cross-spawn@7.0.6", "", { "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", "which": "^2.0.1" } }, "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA=="],
|
||||||
|
|
||||||
|
"css-loader": ["css-loader@7.1.4", "", { "dependencies": { "icss-utils": "^5.1.0", "postcss": "^8.4.40", "postcss-modules-extract-imports": "^3.1.0", "postcss-modules-local-by-default": "^4.0.5", "postcss-modules-scope": "^3.2.0", "postcss-modules-values": "^4.0.0", "postcss-value-parser": "^4.2.0", "semver": "^7.6.3" }, "peerDependencies": { "@rspack/core": "0.x || ^1.0.0 || ^2.0.0-0", "webpack": "^5.27.0" }, "optionalPeers": ["@rspack/core", "webpack"] }, "sha512-vv3J9tlOl04WjiMvHQI/9tmIrCxVrj6PFbHemBB1iihpeRbi/I4h033eoFIhwxBBqLhI0KYFS7yvynBFhIZfTw=="],
|
||||||
|
|
||||||
|
"cssesc": ["cssesc@3.0.0", "", { "bin": { "cssesc": "bin/cssesc" } }, "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg=="],
|
||||||
|
|
||||||
|
"csstype": ["csstype@3.2.3", "", {}, "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ=="],
|
||||||
|
|
||||||
|
"define-lazy-prop": ["define-lazy-prop@2.0.0", "", {}, "sha512-Ds09qNh8yw3khSjiJjiUInaGX9xlqZDY7JVryGxdxV7NPeuqQfplOpQ66yJFZut3jLa5zOwkXw1g9EI2uKh4Og=="],
|
||||||
|
|
||||||
|
"dotenv": ["dotenv@17.3.1", "", {}, "sha512-IO8C/dzEb6O3F9/twg6ZLXz164a2fhTnEWb95H23Dm4OuN+92NmEAlTrupP9VW6Jm3sO26tQlqyvyi4CsnY9GA=="],
|
||||||
|
|
||||||
|
"electron-to-chromium": ["electron-to-chromium@1.5.379", "", {}, "sha512-v/qV5aV5EUA2pGilzUCq5/eyOloZAqDZBu9UMBIzgPpLlprjSR6zswsWBTv0KpqxLGUAZEwhO95ZCt7srymNVA=="],
|
||||||
|
|
||||||
|
"enhanced-resolve": ["enhanced-resolve@5.24.1", "", { "dependencies": { "graceful-fs": "^4.2.4", "tapable": "^2.3.3" } }, "sha512-7DdUaTjmNwMcH2gLr1qycesKII3BK4RLy/mdAb7x10Lq7bR4aNKHt1BR1ZALSv0rPM/hF5wYF0PhGop/rJm8vw=="],
|
||||||
|
|
||||||
|
"error-stack-parser": ["error-stack-parser@2.1.4", "", { "dependencies": { "stackframe": "^1.3.4" } }, "sha512-Sk5V6wVazPhq5MhpO+AUxJn5x7XSXGl1R93Vn7i+zS15KDVxQijejNCrz8340/2bgLBjR9GtEG8ZVKONDjcqGQ=="],
|
||||||
|
|
||||||
|
"es-module-lexer": ["es-module-lexer@2.1.0", "", {}, "sha512-n27zTYMjYu1aj4MjCWzSP7G9r75utsaoc8m61weK+W8JMBGGQybd43GstCXZ3WNmSFtGT9wi59qQTW6mhTR5LQ=="],
|
||||||
|
|
||||||
|
"esbuild": ["esbuild@0.28.1", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.28.1", "@esbuild/android-arm": "0.28.1", "@esbuild/android-arm64": "0.28.1", "@esbuild/android-x64": "0.28.1", "@esbuild/darwin-arm64": "0.28.1", "@esbuild/darwin-x64": "0.28.1", "@esbuild/freebsd-arm64": "0.28.1", "@esbuild/freebsd-x64": "0.28.1", "@esbuild/linux-arm": "0.28.1", "@esbuild/linux-arm64": "0.28.1", "@esbuild/linux-ia32": "0.28.1", "@esbuild/linux-loong64": "0.28.1", "@esbuild/linux-mips64el": "0.28.1", "@esbuild/linux-ppc64": "0.28.1", "@esbuild/linux-riscv64": "0.28.1", "@esbuild/linux-s390x": "0.28.1", "@esbuild/linux-x64": "0.28.1", "@esbuild/netbsd-arm64": "0.28.1", "@esbuild/netbsd-x64": "0.28.1", "@esbuild/openbsd-arm64": "0.28.1", "@esbuild/openbsd-x64": "0.28.1", "@esbuild/openharmony-arm64": "0.28.1", "@esbuild/sunos-x64": "0.28.1", "@esbuild/win32-arm64": "0.28.1", "@esbuild/win32-ia32": "0.28.1", "@esbuild/win32-x64": "0.28.1" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-HrJrvZv5ayxBzPfwphOoNzkzOIIlifzk0KJrGK2c8R4+LKpMtpYLQeUdjnwjWv/LZlkH2laZk+4w78pi99D4Vw=="],
|
||||||
|
|
||||||
|
"escalade": ["escalade@3.2.0", "", {}, "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA=="],
|
||||||
|
|
||||||
|
"eslint-scope": ["eslint-scope@5.1.1", "", { "dependencies": { "esrecurse": "^4.3.0", "estraverse": "^4.1.1" } }, "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw=="],
|
||||||
|
|
||||||
|
"esprima": ["esprima@4.0.1", "", { "bin": { "esparse": "./bin/esparse.js", "esvalidate": "./bin/esvalidate.js" } }, "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A=="],
|
||||||
|
|
||||||
|
"esrecurse": ["esrecurse@4.3.0", "", { "dependencies": { "estraverse": "^5.2.0" } }, "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag=="],
|
||||||
|
|
||||||
|
"estraverse": ["estraverse@4.3.0", "", {}, "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw=="],
|
||||||
|
|
||||||
|
"events": ["events@3.3.0", "", {}, "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q=="],
|
||||||
|
|
||||||
|
"execa": ["execa@5.1.1", "", { "dependencies": { "cross-spawn": "^7.0.3", "get-stream": "^6.0.0", "human-signals": "^2.1.0", "is-stream": "^2.0.0", "merge-stream": "^2.0.0", "npm-run-path": "^4.0.1", "onetime": "^5.1.2", "signal-exit": "^3.0.3", "strip-final-newline": "^2.0.0" } }, "sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg=="],
|
||||||
|
|
||||||
|
"fast-deep-equal": ["fast-deep-equal@3.1.3", "", {}, "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="],
|
||||||
|
|
||||||
|
"fast-uri": ["fast-uri@3.1.2", "", {}, "sha512-rVjf7ArG3LTk+FS6Yw81V1DLuZl1bRbNrev6Tmd/9RaroeeRRJhAt7jg/6YFxbvAQXUCavSoZhPPj6oOx+5KjQ=="],
|
||||||
|
|
||||||
|
"fs-monkey": ["fs-monkey@1.0.3", "", {}, "sha512-cybjIfiiE+pTWicSCLFHSrXZ6EilF30oh91FDP9S2B051prEa7QWfrVTQm10/dDpswBDXZugPa1Ogu8Yh+HV0Q=="],
|
||||||
|
|
||||||
|
"get-stream": ["get-stream@6.0.1", "", {}, "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg=="],
|
||||||
|
|
||||||
|
"glob-to-regexp": ["glob-to-regexp@0.4.1", "", {}, "sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw=="],
|
||||||
|
|
||||||
|
"graceful-fs": ["graceful-fs@4.2.11", "", {}, "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ=="],
|
||||||
|
|
||||||
|
"has-flag": ["has-flag@4.0.0", "", {}, "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ=="],
|
||||||
|
|
||||||
|
"html-entities": ["html-entities@2.6.0", "", {}, "sha512-kig+rMn/QOVRvr7c86gQ8lWXq+Hkv6CbAH1hLu+RG338StTpE8Z0b44SDVaqVu7HGKf27frdmUYEs9hTUX/cLQ=="],
|
||||||
|
|
||||||
|
"human-signals": ["human-signals@2.1.0", "", {}, "sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw=="],
|
||||||
|
|
||||||
|
"icss-utils": ["icss-utils@5.1.0", "", { "peerDependencies": { "postcss": "^8.1.0" } }, "sha512-soFhflCVWLfRNOPU3iv5Z9VUdT44xFRbzjLsEzSr5AQmgqPMTHdU3PMT1Cf1ssx8fLNJDA1juftYl+PUcv3MqA=="],
|
||||||
|
|
||||||
|
"is-docker": ["is-docker@2.2.1", "", { "bin": { "is-docker": "cli.js" } }, "sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ=="],
|
||||||
|
|
||||||
|
"is-stream": ["is-stream@2.0.1", "", {}, "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg=="],
|
||||||
|
|
||||||
|
"is-wsl": ["is-wsl@2.2.0", "", { "dependencies": { "is-docker": "^2.0.0" } }, "sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww=="],
|
||||||
|
|
||||||
|
"isexe": ["isexe@2.0.0", "", {}, "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="],
|
||||||
|
|
||||||
|
"jest-worker": ["jest-worker@27.5.1", "", { "dependencies": { "@types/node": "*", "merge-stream": "^2.0.0", "supports-color": "^8.0.0" } }, "sha512-7vuh85V5cdDofPyxn58nrPjBktZo0u9x1g8WtjQol+jZDaE+fhN+cIvTj11GndBnMnyfrUOG1sZQxCdjKh+DKg=="],
|
||||||
|
|
||||||
|
"json-parse-even-better-errors": ["json-parse-even-better-errors@2.3.1", "", {}, "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w=="],
|
||||||
|
|
||||||
|
"json-schema-traverse": ["json-schema-traverse@1.0.0", "", {}, "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug=="],
|
||||||
|
|
||||||
|
"kleur": ["kleur@3.0.3", "", {}, "sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w=="],
|
||||||
|
|
||||||
|
"loader-runner": ["loader-runner@4.3.2", "", {}, "sha512-DFEqQ3ihfS9blba08cLfYf1NRAIEm+dDjic073DRDc3/JspI/8wYmtDsHwd3+4hwvdxSK7PGaElfTmm0awWJ4w=="],
|
||||||
|
|
||||||
|
"lodash.sortby": ["lodash.sortby@4.7.0", "", {}, "sha512-HDWXG8isMntAyRF5vZ7xKuEvOhT4AhlRt/3czTSjvGUxjYCBVRQY48ViDHyfYz9VIoBkW4TMGQNapx+l3RUwdA=="],
|
||||||
|
|
||||||
|
"lru-cache": ["lru-cache@6.0.0", "", { "dependencies": { "yallist": "^4.0.0" } }, "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA=="],
|
||||||
|
|
||||||
|
"mediabunny": ["mediabunny@1.47.0", "", { "dependencies": { "@types/dom-mediacapture-transform": "^0.1.11", "@types/dom-webcodecs": "0.1.13" } }, "sha512-XQMZAcaKPkJ7hQ/Q2fvBdl3ZazQl2WVxDysUbJWh4PuAnLoerdsQBdPTDWdUdK6hh26LQ8Ue94MLLnmpWvlUYg=="],
|
||||||
|
|
||||||
|
"memfs": ["memfs@3.4.3", "", { "dependencies": { "fs-monkey": "1.0.3" } }, "sha512-eivjfi7Ahr6eQTn44nvTnR60e4a1Fs1Via2kCR5lHo/kyNoiMWaXCNJ/GpSd0ilXas2JSOl9B5FTIhflXu0hlg=="],
|
||||||
|
|
||||||
|
"merge-stream": ["merge-stream@2.0.0", "", {}, "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w=="],
|
||||||
|
|
||||||
|
"mime-db": ["mime-db@1.52.0", "", {}, "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg=="],
|
||||||
|
|
||||||
|
"mime-types": ["mime-types@2.1.35", "", { "dependencies": { "mime-db": "1.52.0" } }, "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw=="],
|
||||||
|
|
||||||
|
"mimic-fn": ["mimic-fn@2.1.0", "", {}, "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg=="],
|
||||||
|
|
||||||
|
"minimist": ["minimist@1.2.6", "", {}, "sha512-Jsjnk4bw3YJqYzbdyBiNsPWHPfO++UGG749Cxs6peCu5Xg4nrena6OVxOYxrQTqww0Jmwt+Ref8rggumkTLz9Q=="],
|
||||||
|
|
||||||
|
"nanoid": ["nanoid@3.3.15", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-y7Wygv/7mEOvxTuEQDB8StXdMRBWf1kR/tlhAzBRUFkB2jfcLOAxO/SHmOO2zgz1pVgK29/kyupn059/bCHdjA=="],
|
||||||
|
|
||||||
|
"neo-async": ["neo-async@2.6.2", "", {}, "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw=="],
|
||||||
|
|
||||||
|
"node-releases": ["node-releases@2.0.50", "", {}, "sha512-J6l92tKHX6w8Jy5nO1Vuc01NoIiRGi/d6qBKVxh+IQ8Cr3b6HbVNfKiF8ZpFKufTwpwxMmce2W3iQZ861ZRyTg=="],
|
||||||
|
|
||||||
|
"npm-run-path": ["npm-run-path@4.0.1", "", { "dependencies": { "path-key": "^3.0.0" } }, "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw=="],
|
||||||
|
|
||||||
|
"onetime": ["onetime@5.1.2", "", { "dependencies": { "mimic-fn": "^2.1.0" } }, "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg=="],
|
||||||
|
|
||||||
|
"open": ["open@8.4.2", "", { "dependencies": { "define-lazy-prop": "^2.0.0", "is-docker": "^2.1.1", "is-wsl": "^2.2.0" } }, "sha512-7x81NCL719oNbsq/3mh+hVrAWmFuEYUqrq/Iw3kUzH8ReypT9QQ0BLoJS7/G9k6N81XjW4qHWtjWwe/9eLy1EQ=="],
|
||||||
|
|
||||||
|
"path-key": ["path-key@3.1.1", "", {}, "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q=="],
|
||||||
|
|
||||||
|
"picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="],
|
||||||
|
|
||||||
|
"postcss": ["postcss@8.5.15", "", { "dependencies": { "nanoid": "^3.3.12", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-FfR8sjd4em2T6fb3I2MwAJU7HWVMr9zba+enmQeeWFfCbm+UOC/0X4DS8XtpUTMwWMGbjKYP7xjfNekzyGmB3A=="],
|
||||||
|
|
||||||
|
"postcss-modules-extract-imports": ["postcss-modules-extract-imports@3.1.0", "", { "peerDependencies": { "postcss": "^8.1.0" } }, "sha512-k3kNe0aNFQDAZGbin48pL2VNidTF0w4/eASDsxlyspobzU3wZQLOGj7L9gfRe0Jo9/4uud09DsjFNH7winGv8Q=="],
|
||||||
|
|
||||||
|
"postcss-modules-local-by-default": ["postcss-modules-local-by-default@4.2.0", "", { "dependencies": { "icss-utils": "^5.0.0", "postcss-selector-parser": "^7.0.0", "postcss-value-parser": "^4.1.0" }, "peerDependencies": { "postcss": "^8.1.0" } }, "sha512-5kcJm/zk+GJDSfw+V/42fJ5fhjL5YbFDl8nVdXkJPLLW+Vf9mTD5Xe0wqIaDnLuL2U6cDNpTr+UQ+v2HWIBhzw=="],
|
||||||
|
|
||||||
|
"postcss-modules-scope": ["postcss-modules-scope@3.2.1", "", { "dependencies": { "postcss-selector-parser": "^7.0.0" }, "peerDependencies": { "postcss": "^8.1.0" } }, "sha512-m9jZstCVaqGjTAuny8MdgE88scJnCiQSlSrOWcTQgM2t32UBe+MUmFSO5t7VMSfAf/FJKImAxBav8ooCHJXCJA=="],
|
||||||
|
|
||||||
|
"postcss-modules-values": ["postcss-modules-values@4.0.0", "", { "dependencies": { "icss-utils": "^5.0.0" }, "peerDependencies": { "postcss": "^8.1.0" } }, "sha512-RDxHkAiEGI78gS2ofyvCsu7iycRv7oqw5xMWn9iMoR0N/7mf9D50ecQqUo5BZ9Zh2vH4bCUR/ktCqbB9m8vJjQ=="],
|
||||||
|
|
||||||
|
"postcss-selector-parser": ["postcss-selector-parser@7.1.4", "", { "dependencies": { "cssesc": "^3.0.0", "util-deprecate": "^1.0.2" } }, "sha512-HeP7D2wyhkR+XaK6v4W8oRF62Dsz4flyuczALJp61GckGm42u1saSSJ/0auvcBqxs3jMRFEcPK34At/0JBKdOg=="],
|
||||||
|
|
||||||
|
"postcss-value-parser": ["postcss-value-parser@4.2.0", "", {}, "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ=="],
|
||||||
|
|
||||||
|
"prettier": ["prettier@3.8.1", "", { "bin": { "prettier": "bin/prettier.cjs" } }, "sha512-UOnG6LftzbdaHZcKoPFtOcCKztrQ57WkHDeRD9t/PTQtmT0NHSeWWepj6pS0z/N7+08BHFDQVUrfmfMRcZwbMg=="],
|
||||||
|
|
||||||
|
"prompts": ["prompts@2.4.2", "", { "dependencies": { "kleur": "^3.0.3", "sisteransi": "^1.0.5" } }, "sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q=="],
|
||||||
|
|
||||||
|
"punycode": ["punycode@2.3.1", "", {}, "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg=="],
|
||||||
|
|
||||||
|
"react": ["react@19.2.7", "", {}, "sha512-HNe9WslTbXmFK8o8cmwgAeJFSBvt1bPdHCVKtaaV+WlAN36mpT4hcRpwbf3fY56ar2oIXzsBpOAiIRHAdY0OlQ=="],
|
||||||
|
|
||||||
|
"react-dom": ["react-dom@19.2.7", "", { "dependencies": { "scheduler": "^0.27.0" }, "peerDependencies": { "react": "^19.2.7" } }, "sha512-t0BRVXvbiE/o20Hfw669rLbMCDWtYZLvmJigy2f0MxsXF+71pxhR3xOkspmsO8h3ZlNzyibAmtCa3l4lYKk6gQ=="],
|
||||||
|
|
||||||
|
"react-refresh": ["react-refresh@0.18.0", "", {}, "sha512-QgT5//D3jfjJb6Gsjxv0Slpj23ip+HtOpnNgnb2S5zU3CB26G/IDPGoy4RJB42wzFE46DRsstbW6tKHoKbhAxw=="],
|
||||||
|
|
||||||
|
"recast": ["recast@0.23.11", "", { "dependencies": { "ast-types": "^0.16.1", "esprima": "~4.0.0", "source-map": "~0.6.1", "tiny-invariant": "^1.3.3", "tslib": "^2.0.1" } }, "sha512-YTUo+Flmw4ZXiWfQKGcwwc11KnoRAYgzAE2E7mXKCjSviTKShtxBsN6YUUBB2gtaBzKzeKunxhUwNHQuRryhWA=="],
|
||||||
|
|
||||||
|
"remotion": ["remotion@4.0.483", "", { "peerDependencies": { "react": ">=16.8.0", "react-dom": ">=16.8.0" } }, "sha512-lf4xq4Twn75TQeTavFLrTE4zdck7EKBSx07ZrPv4om40Sist+3G0kq7NNzSjMeJ6G2Wx6vI+0oICD/vv4sGWtg=="],
|
||||||
|
|
||||||
|
"require-from-string": ["require-from-string@2.0.2", "", {}, "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw=="],
|
||||||
|
|
||||||
|
"scheduler": ["scheduler@0.27.0", "", {}, "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q=="],
|
||||||
|
|
||||||
|
"schema-utils": ["schema-utils@4.3.3", "", { "dependencies": { "@types/json-schema": "^7.0.9", "ajv": "^8.9.0", "ajv-formats": "^2.1.1", "ajv-keywords": "^5.1.0" } }, "sha512-eflK8wEtyOE6+hsaRVPxvUKYCpRgzLqDTb8krvAsRIwOGlHoSgYLgBXoubGgLd2fT41/OUYdb48v4k4WWHQurA=="],
|
||||||
|
|
||||||
|
"semver": ["semver@7.5.3", "", { "dependencies": { "lru-cache": "^6.0.0" }, "bin": { "semver": "bin/semver.js" } }, "sha512-QBlUtyVk/5EeHbi7X0fw6liDZc7BBmEaSYn01fMU1OUYbf6GPsbTtd8WmnqbI20SeycoHSeiybkE/q1Q+qlThQ=="],
|
||||||
|
|
||||||
|
"shebang-command": ["shebang-command@2.0.0", "", { "dependencies": { "shebang-regex": "^3.0.0" } }, "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA=="],
|
||||||
|
|
||||||
|
"shebang-regex": ["shebang-regex@3.0.0", "", {}, "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A=="],
|
||||||
|
|
||||||
|
"signal-exit": ["signal-exit@3.0.7", "", {}, "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ=="],
|
||||||
|
|
||||||
|
"sisteransi": ["sisteransi@1.0.5", "", {}, "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg=="],
|
||||||
|
|
||||||
|
"source-map": ["source-map@0.8.0-beta.0", "", { "dependencies": { "whatwg-url": "^7.0.0" } }, "sha512-2ymg6oRBpebeZi9UUNsgQ89bhx01TcTkmNTGnNO88imTmbSgy4nfujrgVEFKWpMTEGA11EDkTt7mqObTPdigIA=="],
|
||||||
|
|
||||||
|
"source-map-js": ["source-map-js@1.2.1", "", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="],
|
||||||
|
|
||||||
|
"source-map-support": ["source-map-support@0.5.21", "", { "dependencies": { "buffer-from": "^1.0.0", "source-map": "^0.6.0" } }, "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w=="],
|
||||||
|
|
||||||
|
"stackframe": ["stackframe@1.3.4", "", {}, "sha512-oeVtt7eWQS+Na6F//S4kJ2K2VbRlS9D43mAlMyVpVWovy9o+jfgH8O9agzANzaiLjclA0oYzUXEM4PurhSUChw=="],
|
||||||
|
|
||||||
|
"strip-final-newline": ["strip-final-newline@2.0.0", "", {}, "sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA=="],
|
||||||
|
|
||||||
|
"style-loader": ["style-loader@4.0.0", "", { "peerDependencies": { "webpack": "^5.27.0" } }, "sha512-1V4WqhhZZgjVAVJyt7TdDPZoPBPNHbekX4fWnCJL1yQukhCeZhJySUL+gL9y6sNdN95uEOS83Y55SqHcP7MzLA=="],
|
||||||
|
|
||||||
|
"supports-color": ["supports-color@8.1.1", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q=="],
|
||||||
|
|
||||||
|
"tapable": ["tapable@2.3.3", "", {}, "sha512-uxc/zpqFg6x7C8vOE7lh6Lbda8eEL9zmVm/PLeTPBRhh1xCgdWaQ+J1CUieGpIfm2HdtsUpRv+HshiasBMcc6A=="],
|
||||||
|
|
||||||
|
"terser": ["terser@5.48.0", "", { "dependencies": { "@jridgewell/source-map": "^0.3.3", "acorn": "^8.15.0", "commander": "^2.20.0", "source-map-support": "~0.5.20" }, "bin": { "terser": "bin/terser" } }, "sha512-J/9An6vs9Us6wKRriSFXBWdRZapREHqFzdNUKk0pmu804EMR6dr6winwo7e5JDxN4xahxQsuysyYFwlwj4XN/Q=="],
|
||||||
|
|
||||||
|
"terser-webpack-plugin": ["terser-webpack-plugin@5.6.1", "", { "dependencies": { "@jridgewell/trace-mapping": "^0.3.25", "jest-worker": "^27.4.5", "schema-utils": "^4.3.0", "terser": "^5.31.1" }, "peerDependencies": { "webpack": "^5.1.0" } }, "sha512-201R5j+sJpK8nFWwKVyNfZot8FaJbLZDq5evriVzbV1wDtSXDjRUDRfJzHpAaxFDMEhsZL1QkeqM61wgsS3KaQ=="],
|
||||||
|
|
||||||
|
"tiny-invariant": ["tiny-invariant@1.3.3", "", {}, "sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg=="],
|
||||||
|
|
||||||
|
"to-fast-properties": ["to-fast-properties@2.0.0", "", {}, "sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog=="],
|
||||||
|
|
||||||
|
"tr46": ["tr46@1.0.1", "", { "dependencies": { "punycode": "^2.1.0" } }, "sha512-dTpowEjclQ7Kgx5SdBkqRzVhERQXov8/l9Ft9dVM9fmg0W0KQSVaXX9T4i6twCPNtYiZM53lpSSUAwJbFPOHxA=="],
|
||||||
|
|
||||||
|
"tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
|
||||||
|
|
||||||
|
"typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="],
|
||||||
|
|
||||||
|
"undici-types": ["undici-types@8.3.0", "", {}, "sha512-j375ScV60dom+YkPFIfTLcOiPxkN/buHz5GobjLhixFuANaNs3C9l4GmrWqejgXWJ7BbJcFYpTEUkS1Ge8bpZQ=="],
|
||||||
|
|
||||||
|
"update-browserslist-db": ["update-browserslist-db@1.2.3", "", { "dependencies": { "escalade": "^3.2.0", "picocolors": "^1.1.1" }, "peerDependencies": { "browserslist": ">= 4.21.0" }, "bin": { "update-browserslist-db": "cli.js" } }, "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w=="],
|
||||||
|
|
||||||
|
"util-deprecate": ["util-deprecate@1.0.2", "", {}, "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw=="],
|
||||||
|
|
||||||
|
"watchpack": ["watchpack@2.5.2", "", { "dependencies": { "graceful-fs": "^4.1.2" } }, "sha512-6i/00NBjP4yGPs+caKSyRfpTF/8Torsu0MOW3mMzIbhgISFder8i7xbqgHlLMwJrdiN8ndBV3UA1/AfzPSr+jg=="],
|
||||||
|
|
||||||
|
"webidl-conversions": ["webidl-conversions@4.0.2", "", {}, "sha512-YQ+BmxuTgd6UXZW3+ICGfyqRyHXVlD5GtQr5+qjiNW7bF0cqrzX500HVXPBOvgXb5YnzDd+h0zqyv61KUD7+Sg=="],
|
||||||
|
|
||||||
|
"webpack": ["webpack@5.105.0", "", { "dependencies": { "@types/eslint-scope": "^3.7.7", "@types/estree": "^1.0.8", "@types/json-schema": "^7.0.15", "@webassemblyjs/ast": "^1.14.1", "@webassemblyjs/wasm-edit": "^1.14.1", "@webassemblyjs/wasm-parser": "^1.14.1", "acorn": "^8.15.0", "acorn-import-phases": "^1.0.3", "browserslist": "^4.28.1", "chrome-trace-event": "^1.0.2", "enhanced-resolve": "^5.19.0", "es-module-lexer": "^2.0.0", "eslint-scope": "5.1.1", "events": "^3.2.0", "glob-to-regexp": "^0.4.1", "graceful-fs": "^4.2.11", "json-parse-even-better-errors": "^2.3.1", "loader-runner": "^4.3.1", "mime-types": "^2.1.27", "neo-async": "^2.6.2", "schema-utils": "^4.3.3", "tapable": "^2.3.0", "terser-webpack-plugin": "^5.3.16", "watchpack": "^2.5.1", "webpack-sources": "^3.3.3" }, "bin": { "webpack": "bin/webpack.js" } }, "sha512-gX/dMkRQc7QOMzgTe6KsYFM7DxeIONQSui1s0n/0xht36HvrgbxtM1xBlgx596NbpHuQU8P7QpKwrZYwUX48nw=="],
|
||||||
|
|
||||||
|
"webpack-sources": ["webpack-sources@3.5.0", "", {}, "sha512-HPuy+uuoTCaaoEoI1LQ3JN9+vrPBvEesnnX1jADHy728cHSMlq4wUc4afYqahq2B1mhQVZxCXOkNTnXltr+2vQ=="],
|
||||||
|
|
||||||
|
"whatwg-url": ["whatwg-url@7.1.0", "", { "dependencies": { "lodash.sortby": "^4.7.0", "tr46": "^1.0.1", "webidl-conversions": "^4.0.2" } }, "sha512-WUu7Rg1DroM7oQvGWfOiAK21n74Gg+T4elXEQYkOhtyLeWiJFoOGLXPKI/9gzIie9CtwVLm8wtw6YJdKyxSjeg=="],
|
||||||
|
|
||||||
|
"which": ["which@2.0.2", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "./bin/node-which" } }, "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA=="],
|
||||||
|
|
||||||
|
"ws": ["ws@8.21.0", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-Vsp28b7DRcimFQvrqu2Wek3z1iYxDCWqHYB8Qsnk/S4RfaCQzPGPyBNuVjJV3cd6UiKtUtp6sNM77gWvzcCH+g=="],
|
||||||
|
|
||||||
|
"yallist": ["yallist@4.0.0", "", {}, "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A=="],
|
||||||
|
|
||||||
|
"zod": ["zod@4.3.6", "", {}, "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg=="],
|
||||||
|
|
||||||
|
"css-loader/semver": ["semver@7.8.5", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA=="],
|
||||||
|
|
||||||
|
"esrecurse/estraverse": ["estraverse@5.3.0", "", {}, "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA=="],
|
||||||
|
|
||||||
|
"recast/source-map": ["source-map@0.6.1", "", {}, "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g=="],
|
||||||
|
|
||||||
|
"source-map-support/source-map": ["source-map@0.6.1", "", {}, "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g=="],
|
||||||
|
}
|
||||||
|
}
|
||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
After Width: | Height: | Size: 64 KiB |
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,24 @@
|
|||||||
|
{
|
||||||
|
"name": "glm52-rise-video",
|
||||||
|
"private": true,
|
||||||
|
"type": "module",
|
||||||
|
"scripts": {
|
||||||
|
"render": "remotion render src/index.tsx GLM52Rise out/glm-52-broke-out.mp4 --codec h264 --pixel-format yuv420p",
|
||||||
|
"render:sheep": "remotion render src/index.tsx NZSheep out/nz-sheep.mp4 --codec h264 --pixel-format yuv420p",
|
||||||
|
"render:novel": "remotion render src/index.tsx NovelTokens out/novel-1984.mp4 --codec h264 --pixel-format yuv420p",
|
||||||
|
"render:flash": "remotion render src/index.tsx FlashShare out/flash-share.mp4 --codec h264 --pixel-format yuv420p",
|
||||||
|
"render:minimax": "remotion render src/index.tsx MiniMaxClimb out/minimax-climb.mp4 --codec h264 --pixel-format yuv420p",
|
||||||
|
"still:june": "remotion still src/index.tsx JuneTotals out/june-totals.png --frame=0"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@remotion/cli": "^4.0.384",
|
||||||
|
"react": "^19.2.3",
|
||||||
|
"react-dom": "^19.2.3",
|
||||||
|
"remotion": "^4.0.384"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/react": "^19.2.8",
|
||||||
|
"@types/react-dom": "^19.2.3",
|
||||||
|
"typescript": "^5.8.2"
|
||||||
|
}
|
||||||
|
}
|
||||||
Binary file not shown.
|
After Width: | Height: | Size: 159 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 172 KiB |
@@ -0,0 +1,156 @@
|
|||||||
|
// Verified against the production opencode-stats PlanetScale DB.
|
||||||
|
// Scope: tier='Go' (OpenCode Go), dataset='zen', client='all', source='all', grain='day'.
|
||||||
|
// metric = total_tokens. Daily token volume per model; GLM-5.2 (zhipu) launched Jun 17.
|
||||||
|
// Segments per day (tokens): glm-5.2, deepseek-v4-flash, deepseek-v4-pro, minimax-m3, all others.
|
||||||
|
|
||||||
|
export type Day = {
|
||||||
|
date: string
|
||||||
|
total: number
|
||||||
|
glm: number
|
||||||
|
dsf: number
|
||||||
|
dsp: number
|
||||||
|
mm: number
|
||||||
|
others: number
|
||||||
|
}
|
||||||
|
|
||||||
|
export const days: Day[] = [
|
||||||
|
{
|
||||||
|
date: "Jun 12",
|
||||||
|
total: 2_283_799_449_383,
|
||||||
|
glm: 0,
|
||||||
|
dsf: 1_176_701_653_509,
|
||||||
|
dsp: 569_527_034_307,
|
||||||
|
mm: 159_016_250_684,
|
||||||
|
others: 378_554_510_883,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
date: "Jun 13",
|
||||||
|
total: 2_008_462_388_420,
|
||||||
|
glm: 0,
|
||||||
|
dsf: 995_338_131_997,
|
||||||
|
dsp: 445_817_536_548,
|
||||||
|
mm: 211_743_241_967,
|
||||||
|
others: 355_563_477_908,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
date: "Jun 14",
|
||||||
|
total: 2_007_785_405_251,
|
||||||
|
glm: 0,
|
||||||
|
dsf: 983_954_176_228,
|
||||||
|
dsp: 428_151_999_341,
|
||||||
|
mm: 262_476_527_930,
|
||||||
|
others: 333_202_701_752,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
date: "Jun 15",
|
||||||
|
total: 2_694_736_103_062,
|
||||||
|
glm: 0,
|
||||||
|
dsf: 1_255_893_953_859,
|
||||||
|
dsp: 632_223_338_376,
|
||||||
|
mm: 352_507_442_991,
|
||||||
|
others: 454_111_367_836,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
date: "Jun 16",
|
||||||
|
total: 2_838_153_758_908,
|
||||||
|
glm: 0,
|
||||||
|
dsf: 1_336_625_283_800,
|
||||||
|
dsp: 676_480_415_730,
|
||||||
|
mm: 305_268_829_013,
|
||||||
|
others: 519_779_230_365,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
date: "Jun 17",
|
||||||
|
total: 2_778_964_711_109,
|
||||||
|
glm: 70_095_977_043,
|
||||||
|
dsf: 1_339_831_523_555,
|
||||||
|
dsp: 660_414_395_220,
|
||||||
|
mm: 251_302_096_157,
|
||||||
|
others: 457_320_719_134,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
date: "Jun 18",
|
||||||
|
total: 2_806_992_430_656,
|
||||||
|
glm: 201_130_231_172,
|
||||||
|
dsf: 1_295_599_996_869,
|
||||||
|
dsp: 595_665_008_776,
|
||||||
|
mm: 322_205_104_324,
|
||||||
|
others: 392_392_089_515,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
date: "Jun 19",
|
||||||
|
total: 2_419_611_630_232,
|
||||||
|
glm: 199_086_413_910,
|
||||||
|
dsf: 1_115_750_468_802,
|
||||||
|
dsp: 475_965_869_304,
|
||||||
|
mm: 303_586_698_735,
|
||||||
|
others: 325_222_179_481,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
date: "Jun 20",
|
||||||
|
total: 2_188_278_916_865,
|
||||||
|
glm: 193_931_516_396,
|
||||||
|
dsf: 1_050_194_681_012,
|
||||||
|
dsp: 395_303_435_278,
|
||||||
|
mm: 281_998_000_337,
|
||||||
|
others: 266_851_283_842,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
date: "Jun 21",
|
||||||
|
total: 2_042_309_961_344,
|
||||||
|
glm: 181_894_043_118,
|
||||||
|
dsf: 985_164_570_580,
|
||||||
|
dsp: 368_194_079_542,
|
||||||
|
mm: 259_812_551_324,
|
||||||
|
others: 247_244_716_780,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
date: "Jun 22",
|
||||||
|
total: 2_893_934_325_663,
|
||||||
|
glm: 301_759_048_475,
|
||||||
|
dsf: 1_298_124_282_989,
|
||||||
|
dsp: 581_012_596_194,
|
||||||
|
mm: 371_581_117_839,
|
||||||
|
others: 341_457_280_166,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
date: "Jun 23",
|
||||||
|
total: 3_109_009_321_480,
|
||||||
|
glm: 282_277_235_158,
|
||||||
|
dsf: 1_423_571_678_821,
|
||||||
|
dsp: 627_374_654_587,
|
||||||
|
mm: 429_416_300_508,
|
||||||
|
others: 346_369_452_406,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
date: "Jun 24",
|
||||||
|
total: 2_939_149_971_595,
|
||||||
|
glm: 256_497_442_533,
|
||||||
|
dsf: 1_373_583_023_234,
|
||||||
|
dsp: 601_270_997_775,
|
||||||
|
mm: 391_586_493_231,
|
||||||
|
others: 316_212_014_822,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
date: "Jun 25",
|
||||||
|
total: 3_029_641_552_948,
|
||||||
|
glm: 256_279_657_734,
|
||||||
|
dsf: 1_481_084_002_776,
|
||||||
|
dsp: 602_077_167_287,
|
||||||
|
mm: 375_985_302_874,
|
||||||
|
others: 314_215_422_277,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
export const launchIndex = 5 // Jun 17, first day of GLM-5.2 usage
|
||||||
|
// GLM-5.2 weekly token volume, Jun 19-25 (sum of glm): 1,671,725,357,324 = 1.67T
|
||||||
|
export const glmWeekTokensT = 1.672
|
||||||
|
|
||||||
|
// stacked segments, bottom -> top. GLM-5.2 is the hero (blue); the rest are the field it cut into.
|
||||||
|
export const segments = [
|
||||||
|
{ key: "glm", label: "GLM-5.2", color: "#3b5cf6", hero: true },
|
||||||
|
{ key: "dsf", label: "deepseek-v4-flash", color: "#9ca3ad" },
|
||||||
|
{ key: "dsp", label: "deepseek-v4-pro", color: "#b3b9c1" },
|
||||||
|
{ key: "mm", label: "minimax-m3", color: "#c8cdd3" },
|
||||||
|
{ key: "others", label: "other models", color: "#dde0e4" },
|
||||||
|
] as const
|
||||||
@@ -0,0 +1,185 @@
|
|||||||
|
import React from "react"
|
||||||
|
import { AbsoluteFill, Easing, interpolate, spring, useCurrentFrame, useVideoConfig } from "remotion"
|
||||||
|
|
||||||
|
// stats.opencode.ai design tokens (light theme)
|
||||||
|
const c = {
|
||||||
|
bg: "#ffffff",
|
||||||
|
ink: "#161616",
|
||||||
|
muted: "#5c5c5c",
|
||||||
|
faint: "#808080",
|
||||||
|
line: "#e6e6e6",
|
||||||
|
dot: "#e4e4e4",
|
||||||
|
gray: "#aab0b8",
|
||||||
|
accent: "#3b5cf6",
|
||||||
|
accentHi: "#5b78ff",
|
||||||
|
}
|
||||||
|
const MONO = '"IBM Plex Mono", ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace'
|
||||||
|
|
||||||
|
const DOT_MASK =
|
||||||
|
"url(\"data:image/svg+xml,%3Csvg viewBox='0 0 6 6' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M0 0H2V2H0V0Z' fill='black'/%3E%3C/svg%3E\")"
|
||||||
|
|
||||||
|
// verified: OpenCode Go, week of Jun 22-28, 2026 (2026-W26). share = % of 19.64T total Go tokens.
|
||||||
|
const bars = [
|
||||||
|
{ label: "deepseek-v4-flash", share: 48.3, hero: true },
|
||||||
|
{ label: "deepseek-v4-pro", share: 19.4 },
|
||||||
|
{ label: "minimax-m3", share: 13.0 },
|
||||||
|
{ label: "glm-5.2", share: 8.3 },
|
||||||
|
{ label: "mimo-v2.5", share: 4.3 },
|
||||||
|
{ label: "kimi-k2.7-code", share: 2.6 },
|
||||||
|
{ label: "other models", share: 4.1 },
|
||||||
|
]
|
||||||
|
|
||||||
|
function DataWordmark({ height = 30 }: { height?: number }) {
|
||||||
|
return (
|
||||||
|
<svg width={(height * 66) / 20} height={height} viewBox="0 0 66 20" fill="none" style={{ color: c.ink }}>
|
||||||
|
<path opacity="0.2" d="M12 16H4V8H12V16Z" fill="currentColor" />
|
||||||
|
<path d="M12 4H4V16H12V4ZM16 20H0V0H16V20Z" fill="currentColor" />
|
||||||
|
<path
|
||||||
|
d="M63.3543 16L62.5119 12.8711H58.6437L57.8013 16H55.7383L59.2454 4H61.9618L65.4689 16H63.3543ZM61.0678 7.851L60.6896 5.94269H60.4489L60.0707 7.851L59.1595 11.1347H61.9962L61.0678 7.851Z"
|
||||||
|
fill="currentColor"
|
||||||
|
/>
|
||||||
|
<path d="M52.5951 5.87392V16H50.4461V5.87392H47.4375V4H55.6209V5.87392H52.5951Z" fill="currentColor" />
|
||||||
|
<path
|
||||||
|
d="M45.2059 16L44.3635 12.8711H40.4953L39.6529 16H37.5898L41.097 4H43.8133L47.3205 16H45.2059ZM42.9194 7.851L42.5411 5.94269H42.3004L41.9222 7.851L41.011 11.1347H43.8477L42.9194 7.851Z"
|
||||||
|
fill="currentColor"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M28 4H32.0917C32.8138 4 33.4556 4.11461 34.0172 4.34384C34.5903 4.5616 35.0716 4.9169 35.4613 5.40974C35.8625 5.89112 36.1662 6.51003 36.3725 7.26648C36.5788 8.02292 36.6819 8.9341 36.6819 10C36.6819 11.0659 36.5788 11.9771 36.3725 12.7335C36.1662 13.49 35.8625 14.1146 35.4613 14.6075C35.0716 15.0888 34.5903 15.4441 34.0172 15.6734C33.4556 15.8911 32.8138 16 32.0917 16H28V4ZM32.0917 14.1261C32.8252 14.1261 33.3926 13.9026 33.7937 13.4556C34.1948 12.9971 34.3954 12.3152 34.3954 11.4097V8.59026C34.3954 7.68481 34.1948 7.0086 33.7937 6.5616C33.3926 6.10315 32.8252 5.87392 32.0917 5.87392H30.149V14.1261H32.0917Z"
|
||||||
|
fill="currentColor"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function FlashShare() {
|
||||||
|
const frame = useCurrentFrame()
|
||||||
|
const { fps } = useVideoConfig()
|
||||||
|
|
||||||
|
const grow = (i: number) =>
|
||||||
|
Math.min(
|
||||||
|
1,
|
||||||
|
Math.max(0, spring({ frame: frame - 18 - i * 7, fps, config: { damping: 18, stiffness: 120, mass: 0.6 } })),
|
||||||
|
)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<AbsoluteFill style={{ background: c.bg, color: c.ink, fontFamily: MONO, padding: 72, boxSizing: "border-box" }}>
|
||||||
|
<div style={{ height: "100%", display: "flex", flexDirection: "column" }}>
|
||||||
|
{/* header */}
|
||||||
|
<div style={{ display: "flex", justifyContent: "space-between", alignItems: "center" }}>
|
||||||
|
<DataWordmark height={30} />
|
||||||
|
<div style={{ fontSize: 20, fontWeight: 500, color: c.faint, letterSpacing: 1 }}>JUN 22–28, 2026</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* headline (static) */}
|
||||||
|
<div style={{ marginTop: 50 }}>
|
||||||
|
<div style={{ fontSize: 23, fontWeight: 600, color: c.muted, letterSpacing: 2 }}>
|
||||||
|
OPENCODE GO · SHARE OF TOKENS
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
style={{ display: "flex", justifyContent: "space-between", alignItems: "baseline", gap: 24, marginTop: 14 }}
|
||||||
|
>
|
||||||
|
<div style={{ fontSize: 62, fontWeight: 600, letterSpacing: -2, lineHeight: 1 }}>DeepSeek V4 Flash</div>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
fontSize: 88,
|
||||||
|
fontWeight: 600,
|
||||||
|
letterSpacing: -2,
|
||||||
|
lineHeight: 1,
|
||||||
|
color: c.accent,
|
||||||
|
fontVariantNumeric: "tabular-nums",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
48%
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* bar chart */}
|
||||||
|
<div style={{ flex: 1, display: "flex", flexDirection: "column", justifyContent: "center", gap: 16 }}>
|
||||||
|
{bars.map((b, i) => {
|
||||||
|
const g = grow(i)
|
||||||
|
const pct = b.share * g
|
||||||
|
return (
|
||||||
|
<div key={b.label} style={{ display: "flex", alignItems: "center", gap: 18 }}>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
width: 268,
|
||||||
|
fontSize: 23,
|
||||||
|
fontWeight: b.hero ? 600 : 500,
|
||||||
|
color: b.hero ? c.ink : c.muted,
|
||||||
|
textAlign: "right",
|
||||||
|
whiteSpace: "nowrap",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{b.label}
|
||||||
|
</div>
|
||||||
|
<div style={{ position: "relative", flex: 1, height: 48 }}>
|
||||||
|
{/* dotted 100% track — height is a multiple of the 12px tile, anchored bottom, so dots never clip */}
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
position: "absolute",
|
||||||
|
inset: 0,
|
||||||
|
background: c.dot,
|
||||||
|
WebkitMaskImage: DOT_MASK,
|
||||||
|
maskImage: DOT_MASK,
|
||||||
|
WebkitMaskSize: "12px 12px",
|
||||||
|
maskSize: "12px 12px",
|
||||||
|
WebkitMaskRepeat: "repeat",
|
||||||
|
maskRepeat: "repeat",
|
||||||
|
WebkitMaskPosition: "left bottom",
|
||||||
|
maskPosition: "left bottom",
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
{/* fill */}
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
position: "absolute",
|
||||||
|
left: 0,
|
||||||
|
top: 0,
|
||||||
|
bottom: 0,
|
||||||
|
width: `${pct}%`,
|
||||||
|
background: b.hero ? c.accent : c.gray,
|
||||||
|
borderRight: b.hero ? `2px solid ${c.accentHi}` : "none",
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
width: 84,
|
||||||
|
fontSize: 24,
|
||||||
|
fontWeight: b.hero ? 600 : 500,
|
||||||
|
color: b.hero ? c.accent : c.muted,
|
||||||
|
textAlign: "right",
|
||||||
|
fontVariantNumeric: "tabular-nums",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{Math.round(pct)}%
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* footer */}
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
display: "flex",
|
||||||
|
justifyContent: "space-between",
|
||||||
|
alignItems: "center",
|
||||||
|
marginTop: 24,
|
||||||
|
paddingTop: 22,
|
||||||
|
borderTop: `1px solid ${c.line}`,
|
||||||
|
fontSize: 20,
|
||||||
|
fontWeight: 500,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div style={{ display: "inline-flex", alignItems: "center", gap: 9, color: c.muted }}>
|
||||||
|
<span style={{ width: 13, height: 13, background: c.accent, display: "inline-block" }} />
|
||||||
|
DeepSeek V4 Flash · 9.48T tokens · 83.6M requests
|
||||||
|
</div>
|
||||||
|
<div style={{ color: c.ink }}>opencode.ai/data</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</AbsoluteFill>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
import { Composition, registerRoot } from "remotion"
|
||||||
|
import { GLM52Rise } from "./video"
|
||||||
|
import { NZSheep } from "./sheep"
|
||||||
|
import { NovelTokens } from "./novel"
|
||||||
|
import { FlashShare } from "./flash"
|
||||||
|
import { MiniMaxClimb } from "./minimax"
|
||||||
|
import { JuneTotals } from "./june"
|
||||||
|
|
||||||
|
function Root() {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Composition id="GLM52Rise" component={GLM52Rise} durationInFrames={240} fps={30} width={1080} height={1080} />
|
||||||
|
<Composition id="NZSheep" component={NZSheep} durationInFrames={150} fps={30} width={1080} height={1080} />
|
||||||
|
<Composition
|
||||||
|
id="NovelTokens"
|
||||||
|
component={NovelTokens}
|
||||||
|
durationInFrames={150}
|
||||||
|
fps={30}
|
||||||
|
width={1080}
|
||||||
|
height={1080}
|
||||||
|
/>
|
||||||
|
<Composition id="FlashShare" component={FlashShare} durationInFrames={165} fps={30} width={1080} height={1080} />
|
||||||
|
<Composition
|
||||||
|
id="MiniMaxClimb"
|
||||||
|
component={MiniMaxClimb}
|
||||||
|
durationInFrames={165}
|
||||||
|
fps={30}
|
||||||
|
width={1080}
|
||||||
|
height={1080}
|
||||||
|
/>
|
||||||
|
<Composition id="JuneTotals" component={JuneTotals} durationInFrames={1} fps={30} width={1080} height={1080} />
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
registerRoot(Root)
|
||||||
@@ -0,0 +1,144 @@
|
|||||||
|
import React from "react"
|
||||||
|
import { AbsoluteFill } from "remotion"
|
||||||
|
|
||||||
|
const c = {
|
||||||
|
bg: "#ffffff",
|
||||||
|
ink: "#161616",
|
||||||
|
muted: "#5c5c5c",
|
||||||
|
faint: "#808080",
|
||||||
|
line: "#e6e6e6",
|
||||||
|
dot: "#dcdcdc",
|
||||||
|
accent: "#3b5cf6",
|
||||||
|
}
|
||||||
|
const MONO = '"IBM Plex Mono", ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace'
|
||||||
|
|
||||||
|
const DOT_MASK =
|
||||||
|
"url(\"data:image/svg+xml,%3Csvg viewBox='0 0 6 6' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M0 0H2V2H0V0Z' fill='black'/%3E%3C/svg%3E\")"
|
||||||
|
|
||||||
|
// verified: OpenCode Go (tier=Go, dataset=zen), June 1-30, 2026.
|
||||||
|
// 72.78T tokens · 651.4M requests · 11.42M sessions -> rounded headline figures.
|
||||||
|
|
||||||
|
function DataWordmark({ height = 30 }: { height?: number }) {
|
||||||
|
return (
|
||||||
|
<svg width={(height * 66) / 20} height={height} viewBox="0 0 66 20" fill="none" style={{ color: c.ink }}>
|
||||||
|
<path opacity="0.2" d="M12 16H4V8H12V16Z" fill="currentColor" />
|
||||||
|
<path d="M12 4H4V16H12V4ZM16 20H0V0H16V20Z" fill="currentColor" />
|
||||||
|
<path
|
||||||
|
d="M63.3543 16L62.5119 12.8711H58.6437L57.8013 16H55.7383L59.2454 4H61.9618L65.4689 16H63.3543ZM61.0678 7.851L60.6896 5.94269H60.4489L60.0707 7.851L59.1595 11.1347H61.9962L61.0678 7.851Z"
|
||||||
|
fill="currentColor"
|
||||||
|
/>
|
||||||
|
<path d="M52.5951 5.87392V16H50.4461V5.87392H47.4375V4H55.6209V5.87392H52.5951Z" fill="currentColor" />
|
||||||
|
<path
|
||||||
|
d="M45.2059 16L44.3635 12.8711H40.4953L39.6529 16H37.5898L41.097 4H43.8133L47.3205 16H45.2059ZM42.9194 7.851L42.5411 5.94269H42.3004L41.9222 7.851L41.011 11.1347H43.8477L42.9194 7.851Z"
|
||||||
|
fill="currentColor"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M28 4H32.0917C32.8138 4 33.4556 4.11461 34.0172 4.34384C34.5903 4.5616 35.0716 4.9169 35.4613 5.40974C35.8625 5.89112 36.1662 6.51003 36.3725 7.26648C36.5788 8.02292 36.6819 8.9341 36.6819 10C36.6819 11.0659 36.5788 11.9771 36.3725 12.7335C36.1662 13.49 35.8625 14.1146 35.4613 14.6075C35.0716 15.0888 34.5903 15.4441 34.0172 15.6734C33.4556 15.8911 32.8138 16 32.0917 16H28V4ZM32.0917 14.1261C32.8252 14.1261 33.3926 13.9026 33.7937 13.4556C34.1948 12.9971 34.3954 12.3152 34.3954 11.4097V8.59026C34.3954 7.68481 34.1948 7.0086 33.7937 6.5616C33.3926 6.10315 32.8252 5.87392 32.0917 5.87392H30.149V14.1261H32.0917Z"
|
||||||
|
fill="currentColor"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function DotBand() {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
height: 12,
|
||||||
|
background: c.dot,
|
||||||
|
WebkitMaskImage: DOT_MASK,
|
||||||
|
maskImage: DOT_MASK,
|
||||||
|
WebkitMaskSize: "12px 12px",
|
||||||
|
maskSize: "12px 12px",
|
||||||
|
WebkitMaskRepeat: "repeat",
|
||||||
|
maskRepeat: "repeat",
|
||||||
|
WebkitMaskPosition: "left top",
|
||||||
|
maskPosition: "left top",
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function Metric({ value, label }: { value: string; label: string }) {
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
fontSize: 92,
|
||||||
|
fontWeight: 600,
|
||||||
|
letterSpacing: -3,
|
||||||
|
lineHeight: 1,
|
||||||
|
fontVariantNumeric: "tabular-nums",
|
||||||
|
marginLeft: -8, // align the glyph's visual left edge to the column
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{value}
|
||||||
|
</div>
|
||||||
|
<div style={{ marginTop: 16, fontSize: 24, fontWeight: 500, color: c.muted, letterSpacing: 1 }}>{label}</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function JuneTotals() {
|
||||||
|
return (
|
||||||
|
<AbsoluteFill style={{ background: c.bg, color: c.ink, fontFamily: MONO, padding: 72, boxSizing: "border-box" }}>
|
||||||
|
<div style={{ height: "100%", display: "flex", flexDirection: "column" }}>
|
||||||
|
{/* header */}
|
||||||
|
<div style={{ display: "flex", justifyContent: "space-between", alignItems: "center", marginBottom: 22 }}>
|
||||||
|
<DataWordmark height={30} />
|
||||||
|
<div style={{ fontSize: 20, fontWeight: 500, color: c.faint, letterSpacing: 1 }}>MONTHLY RECAP</div>
|
||||||
|
</div>
|
||||||
|
<DotBand />
|
||||||
|
|
||||||
|
{/* hero */}
|
||||||
|
<div style={{ flex: 1, display: "flex", flexDirection: "column", justifyContent: "center" }}>
|
||||||
|
<div style={{ fontSize: 25, fontWeight: 600, color: c.muted, letterSpacing: 3 }}>OPENCODE GO · JUNE 2026</div>
|
||||||
|
<div style={{ marginTop: 14 }}>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
fontSize: 300,
|
||||||
|
fontWeight: 600,
|
||||||
|
letterSpacing: -10,
|
||||||
|
lineHeight: 0.82,
|
||||||
|
color: c.accent,
|
||||||
|
marginLeft: -22,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
73T
|
||||||
|
</div>
|
||||||
|
<div style={{ fontSize: 50, fontWeight: 600, color: c.ink, marginTop: 10 }}>tokens processed</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* supporting metrics */}
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
display: "grid",
|
||||||
|
gridTemplateColumns: "1fr 1fr",
|
||||||
|
marginTop: 60,
|
||||||
|
paddingTop: 40,
|
||||||
|
borderTop: `1px solid ${c.line}`,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Metric value="650M" label="requests" />
|
||||||
|
<Metric value="11M" label="sessions" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* footer */}
|
||||||
|
<DotBand />
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
display: "flex",
|
||||||
|
justifyContent: "flex-end",
|
||||||
|
alignItems: "center",
|
||||||
|
marginTop: 22,
|
||||||
|
fontSize: 20,
|
||||||
|
fontWeight: 500,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div style={{ color: c.ink }}>opencode.ai/data</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</AbsoluteFill>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,201 @@
|
|||||||
|
import React from "react"
|
||||||
|
import { AbsoluteFill, interpolate, spring, useCurrentFrame, useVideoConfig } from "remotion"
|
||||||
|
|
||||||
|
const c = {
|
||||||
|
bg: "#ffffff",
|
||||||
|
ink: "#161616",
|
||||||
|
muted: "#5c5c5c",
|
||||||
|
faint: "#808080",
|
||||||
|
line: "#e6e6e6",
|
||||||
|
dot: "#e4e4e4",
|
||||||
|
accent: "#3b5cf6",
|
||||||
|
accentHi: "#5b78ff",
|
||||||
|
accentDim: "#aebcf3",
|
||||||
|
}
|
||||||
|
const MONO = '"IBM Plex Mono", ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace'
|
||||||
|
|
||||||
|
const DOT_MASK =
|
||||||
|
"url(\"data:image/svg+xml,%3Csvg viewBox='0 0 6 6' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M0 0H2V2H0V0Z' fill='black'/%3E%3C/svg%3E\")"
|
||||||
|
|
||||||
|
// verified: minimax-m3, OpenCode Go (tier=Go, dataset=zen), weekly total_tokens.
|
||||||
|
// W26 2.559T is +23.2% / +482.3B vs W25 2.077T.
|
||||||
|
const weeks = [
|
||||||
|
{ label: "May 25", t: 0.008 },
|
||||||
|
{ label: "Jun 1", t: 0.429 },
|
||||||
|
{ label: "Jun 8", t: 1.192 },
|
||||||
|
{ label: "Jun 15", t: 2.077 },
|
||||||
|
{ label: "Jun 22", t: 2.559, latest: true },
|
||||||
|
]
|
||||||
|
const AXIS_MAX = 3.0
|
||||||
|
|
||||||
|
function DataWordmark({ height = 30 }: { height?: number }) {
|
||||||
|
return (
|
||||||
|
<svg width={(height * 66) / 20} height={height} viewBox="0 0 66 20" fill="none" style={{ color: c.ink }}>
|
||||||
|
<path opacity="0.2" d="M12 16H4V8H12V16Z" fill="currentColor" />
|
||||||
|
<path d="M12 4H4V16H12V4ZM16 20H0V0H16V20Z" fill="currentColor" />
|
||||||
|
<path
|
||||||
|
d="M63.3543 16L62.5119 12.8711H58.6437L57.8013 16H55.7383L59.2454 4H61.9618L65.4689 16H63.3543ZM61.0678 7.851L60.6896 5.94269H60.4489L60.0707 7.851L59.1595 11.1347H61.9962L61.0678 7.851Z"
|
||||||
|
fill="currentColor"
|
||||||
|
/>
|
||||||
|
<path d="M52.5951 5.87392V16H50.4461V5.87392H47.4375V4H55.6209V5.87392H52.5951Z" fill="currentColor" />
|
||||||
|
<path
|
||||||
|
d="M45.2059 16L44.3635 12.8711H40.4953L39.6529 16H37.5898L41.097 4H43.8133L47.3205 16H45.2059ZM42.9194 7.851L42.5411 5.94269H42.3004L41.9222 7.851L41.011 11.1347H43.8477L42.9194 7.851Z"
|
||||||
|
fill="currentColor"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M28 4H32.0917C32.8138 4 33.4556 4.11461 34.0172 4.34384C34.5903 4.5616 35.0716 4.9169 35.4613 5.40974C35.8625 5.89112 36.1662 6.51003 36.3725 7.26648C36.5788 8.02292 36.6819 8.9341 36.6819 10C36.6819 11.0659 36.5788 11.9771 36.3725 12.7335C36.1662 13.49 35.8625 14.1146 35.4613 14.6075C35.0716 15.0888 34.5903 15.4441 34.0172 15.6734C33.4556 15.8911 32.8138 16 32.0917 16H28V4ZM32.0917 14.1261C32.8252 14.1261 33.3926 13.9026 33.7937 13.4556C34.1948 12.9971 34.3954 12.3152 34.3954 11.4097V8.59026C34.3954 7.68481 34.1948 7.0086 33.7937 6.5616C33.3926 6.10315 32.8252 5.87392 32.0917 5.87392H30.149V14.1261H32.0917Z"
|
||||||
|
fill="currentColor"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const CHART_H = 420 // multiple of 12 so the dotted tracks never clip
|
||||||
|
|
||||||
|
export function MiniMaxClimb() {
|
||||||
|
const frame = useCurrentFrame()
|
||||||
|
const { fps } = useVideoConfig()
|
||||||
|
|
||||||
|
const grow = (i: number) =>
|
||||||
|
Math.min(
|
||||||
|
1,
|
||||||
|
Math.max(0, spring({ frame: frame - 22 - i * 9, fps, config: { damping: 18, stiffness: 110, mass: 0.6 } })),
|
||||||
|
)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<AbsoluteFill style={{ background: c.bg, color: c.ink, fontFamily: MONO, padding: 72, boxSizing: "border-box" }}>
|
||||||
|
<div style={{ height: "100%", display: "flex", flexDirection: "column" }}>
|
||||||
|
{/* header */}
|
||||||
|
<div style={{ display: "flex", justifyContent: "space-between", alignItems: "center" }}>
|
||||||
|
<DataWordmark height={30} />
|
||||||
|
<div style={{ fontSize: 20, fontWeight: 500, color: c.faint, letterSpacing: 1 }}>JUN 22–28, 2026</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* headline (static) */}
|
||||||
|
<div style={{ marginTop: 50 }}>
|
||||||
|
<div style={{ fontSize: 23, fontWeight: 600, color: c.muted, letterSpacing: 2 }}>
|
||||||
|
OPENCODE GO · WEEKLY TOKENS
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
style={{ display: "flex", justifyContent: "space-between", alignItems: "baseline", gap: 24, marginTop: 14 }}
|
||||||
|
>
|
||||||
|
<div style={{ fontSize: 62, fontWeight: 600, letterSpacing: -2, lineHeight: 1 }}>MiniMax M3</div>
|
||||||
|
<div style={{ textAlign: "right", flexShrink: 0 }}>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
fontSize: 84,
|
||||||
|
fontWeight: 600,
|
||||||
|
letterSpacing: -2,
|
||||||
|
lineHeight: 1,
|
||||||
|
color: c.accent,
|
||||||
|
fontVariantNumeric: "tabular-nums",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
+23.2%
|
||||||
|
</div>
|
||||||
|
<div style={{ fontSize: 19, fontWeight: 500, color: c.muted, letterSpacing: 1, marginTop: 6 }}>
|
||||||
|
WEEK OVER WEEK
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* column chart */}
|
||||||
|
<div style={{ flex: 1, display: "flex", alignItems: "flex-end", marginTop: 30 }}>
|
||||||
|
<div style={{ width: "100%", display: "flex", alignItems: "flex-end", gap: 30 }}>
|
||||||
|
{weeks.map((w, i) => {
|
||||||
|
const g = grow(i)
|
||||||
|
const h = Math.round((w.t / AXIS_MAX) * CHART_H * g)
|
||||||
|
return (
|
||||||
|
<div key={w.label} style={{ flex: 1, display: "flex", flexDirection: "column", alignItems: "center" }}>
|
||||||
|
{/* value + bar */}
|
||||||
|
<div style={{ position: "relative", width: "100%", height: CHART_H }}>
|
||||||
|
{/* dotted track */}
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
position: "absolute",
|
||||||
|
inset: 0,
|
||||||
|
background: c.dot,
|
||||||
|
WebkitMaskImage: DOT_MASK,
|
||||||
|
maskImage: DOT_MASK,
|
||||||
|
WebkitMaskSize: "12px 12px",
|
||||||
|
maskSize: "12px 12px",
|
||||||
|
WebkitMaskRepeat: "repeat",
|
||||||
|
maskRepeat: "repeat",
|
||||||
|
WebkitMaskPosition: "left bottom",
|
||||||
|
maskPosition: "left bottom",
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
{/* fill */}
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
position: "absolute",
|
||||||
|
left: 0,
|
||||||
|
right: 0,
|
||||||
|
bottom: 0,
|
||||||
|
height: h,
|
||||||
|
background: w.latest ? c.accent : c.accentDim,
|
||||||
|
borderTop: w.latest ? `3px solid ${c.accentHi}` : "none",
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
{/* value label */}
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
position: "absolute",
|
||||||
|
left: 0,
|
||||||
|
right: 0,
|
||||||
|
bottom: h + 10,
|
||||||
|
textAlign: "center",
|
||||||
|
fontSize: 26,
|
||||||
|
fontWeight: w.latest ? 600 : 500,
|
||||||
|
color: w.latest ? c.accent : c.muted,
|
||||||
|
fontVariantNumeric: "tabular-nums",
|
||||||
|
opacity: interpolate(g, [0.5, 1], [0, 1], {
|
||||||
|
extrapolateLeft: "clamp",
|
||||||
|
extrapolateRight: "clamp",
|
||||||
|
}),
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{w.t.toFixed(2)}T
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/* week label */}
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
marginTop: 14,
|
||||||
|
fontSize: 18,
|
||||||
|
fontWeight: 500,
|
||||||
|
color: w.latest ? c.ink : c.faint,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{w.label}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* footer */}
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
display: "flex",
|
||||||
|
justifyContent: "space-between",
|
||||||
|
alignItems: "center",
|
||||||
|
marginTop: 22,
|
||||||
|
paddingTop: 22,
|
||||||
|
borderTop: `1px solid ${c.line}`,
|
||||||
|
fontSize: 20,
|
||||||
|
fontWeight: 500,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div style={{ display: "inline-flex", alignItems: "center", gap: 9, color: c.muted }}>
|
||||||
|
<span style={{ width: 13, height: 13, background: c.accent, display: "inline-block" }} />
|
||||||
|
2.56T tokens last week · +482.3B added
|
||||||
|
</div>
|
||||||
|
<div style={{ color: c.ink }}>opencode.ai/data</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</AbsoluteFill>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,135 @@
|
|||||||
|
import React from "react"
|
||||||
|
import { AbsoluteFill, Easing, Img, interpolate, staticFile, useCurrentFrame } from "remotion"
|
||||||
|
|
||||||
|
const c = {
|
||||||
|
white: "#ffffff",
|
||||||
|
dim: "rgba(255,255,255,0.74)",
|
||||||
|
}
|
||||||
|
const MONO = '"IBM Plex Mono", ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace'
|
||||||
|
|
||||||
|
// verified: OpenCode Go, week of Jun 22-28, 2026 (2026-W26)
|
||||||
|
// 19,642,742,937,105 tokens / 173,651,197 requests = 113,116 tokens/request
|
||||||
|
const AVG = 113116
|
||||||
|
const K = Math.round(AVG / 1000) // 113
|
||||||
|
|
||||||
|
const nf = new Intl.NumberFormat("en-US")
|
||||||
|
|
||||||
|
function DataWordmark({ height = 30 }: { height?: number }) {
|
||||||
|
return (
|
||||||
|
<svg width={(height * 66) / 20} height={height} viewBox="0 0 66 20" fill="none" style={{ color: c.white }}>
|
||||||
|
<path opacity="0.35" d="M12 16H4V8H12V16Z" fill="currentColor" />
|
||||||
|
<path d="M12 4H4V16H12V4ZM16 20H0V0H16V20Z" fill="currentColor" />
|
||||||
|
<path
|
||||||
|
d="M63.3543 16L62.5119 12.8711H58.6437L57.8013 16H55.7383L59.2454 4H61.9618L65.4689 16H63.3543ZM61.0678 7.851L60.6896 5.94269H60.4489L60.0707 7.851L59.1595 11.1347H61.9962L61.0678 7.851Z"
|
||||||
|
fill="currentColor"
|
||||||
|
/>
|
||||||
|
<path d="M52.5951 5.87392V16H50.4461V5.87392H47.4375V4H55.6209V5.87392H52.5951Z" fill="currentColor" />
|
||||||
|
<path
|
||||||
|
d="M45.2059 16L44.3635 12.8711H40.4953L39.6529 16H37.5898L41.097 4H43.8133L47.3205 16H45.2059ZM42.9194 7.851L42.5411 5.94269H42.3004L41.9222 7.851L41.011 11.1347H43.8477L42.9194 7.851Z"
|
||||||
|
fill="currentColor"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M28 4H32.0917C32.8138 4 33.4556 4.11461 34.0172 4.34384C34.5903 4.5616 35.0716 4.9169 35.4613 5.40974C35.8625 5.89112 36.1662 6.51003 36.3725 7.26648C36.5788 8.02292 36.6819 8.9341 36.6819 10C36.6819 11.0659 36.5788 11.9771 36.3725 12.7335C36.1662 13.49 35.8625 14.1146 35.4613 14.6075C35.0716 15.0888 34.5903 15.4441 34.0172 15.6734C33.4556 15.8911 32.8138 16 32.0917 16H28V4ZM32.0917 14.1261C32.8252 14.1261 33.3926 13.9026 33.7937 13.4556C34.1948 12.9971 34.3954 12.3152 34.3954 11.4097V8.59026C34.3954 7.68481 34.1948 7.0086 33.7937 6.5616C33.3926 6.10315 32.8252 5.87392 32.0917 5.87392H30.149V14.1261H32.0917Z"
|
||||||
|
fill="currentColor"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function NovelTokens() {
|
||||||
|
const frame = useCurrentFrame()
|
||||||
|
|
||||||
|
const k = Math.round(
|
||||||
|
K *
|
||||||
|
interpolate(frame, [18, 92], [0, 1], {
|
||||||
|
extrapolateLeft: "clamp",
|
||||||
|
extrapolateRight: "clamp",
|
||||||
|
easing: Easing.out(Easing.cubic),
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
|
const zoom = interpolate(frame, [0, 150], [1.06, 1.12], {
|
||||||
|
extrapolateRight: "clamp",
|
||||||
|
easing: Easing.inOut(Easing.quad),
|
||||||
|
})
|
||||||
|
|
||||||
|
return (
|
||||||
|
<AbsoluteFill style={{ background: "#0c0c0c", overflow: "hidden" }}>
|
||||||
|
<Img
|
||||||
|
src={staticFile("book.jpg")}
|
||||||
|
style={{
|
||||||
|
position: "absolute",
|
||||||
|
width: "100%",
|
||||||
|
height: "100%",
|
||||||
|
objectFit: "cover",
|
||||||
|
objectPosition: "center 34%",
|
||||||
|
transform: `scale(${zoom})`,
|
||||||
|
transformOrigin: "center 30%",
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* legibility scrims */}
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
position: "absolute",
|
||||||
|
inset: 0,
|
||||||
|
background:
|
||||||
|
"linear-gradient(to bottom, rgba(0,0,0,0.55) 0%, rgba(0,0,0,0) 20%, rgba(0,0,0,0.1) 44%, rgba(0,0,0,0.86) 100%)",
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
position: "absolute",
|
||||||
|
inset: 0,
|
||||||
|
boxSizing: "border-box",
|
||||||
|
padding: 64,
|
||||||
|
color: c.white,
|
||||||
|
fontFamily: MONO,
|
||||||
|
display: "flex",
|
||||||
|
flexDirection: "column",
|
||||||
|
justifyContent: "space-between",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{/* header */}
|
||||||
|
<div style={{ display: "flex", justifyContent: "space-between", alignItems: "center" }}>
|
||||||
|
<DataWordmark height={30} />
|
||||||
|
<div style={{ fontSize: 20, fontWeight: 500, color: c.dim, letterSpacing: 1 }}>JUN 22–28, 2026</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* bottom block */}
|
||||||
|
<div>
|
||||||
|
<div style={{ fontSize: 23, fontWeight: 600, color: c.dim, letterSpacing: 2, marginBottom: 8 }}>
|
||||||
|
OPENCODE GO · LAST WEEK
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
fontSize: 168,
|
||||||
|
fontWeight: 600,
|
||||||
|
lineHeight: 0.92,
|
||||||
|
letterSpacing: -5,
|
||||||
|
fontVariantNumeric: "tabular-nums",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{k}K
|
||||||
|
</div>
|
||||||
|
<div style={{ fontSize: 50, fontWeight: 600, letterSpacing: -1, marginTop: 4 }}>tokens per request</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
display: "flex",
|
||||||
|
justifyContent: "space-between",
|
||||||
|
alignItems: "center",
|
||||||
|
marginTop: 30,
|
||||||
|
fontSize: 20,
|
||||||
|
fontWeight: 500,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div style={{ color: c.dim }}>{nf.format(AVG)} tokens / request · last week</div>
|
||||||
|
<div style={{ color: c.white }}>opencode.ai/data</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</AbsoluteFill>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,139 @@
|
|||||||
|
import React from "react"
|
||||||
|
import { AbsoluteFill, Easing, Img, interpolate, staticFile, useCurrentFrame } from "remotion"
|
||||||
|
|
||||||
|
const c = {
|
||||||
|
white: "#ffffff",
|
||||||
|
dim: "rgba(255,255,255,0.72)",
|
||||||
|
faint: "rgba(255,255,255,0.55)",
|
||||||
|
}
|
||||||
|
const MONO = '"IBM Plex Mono", ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace'
|
||||||
|
|
||||||
|
// verified: NZ OpenCode Go, week of Jun 22-28, 2026 (2026-W26)
|
||||||
|
const TOKENS = 40_915_594_381 // 40.9B
|
||||||
|
const SHEEP = 23_600_000 // 23.6M
|
||||||
|
const PER_SHEEP = Math.round(TOKENS / SHEEP) // 1,734
|
||||||
|
const nf = new Intl.NumberFormat("en-US")
|
||||||
|
|
||||||
|
// the correct opencode "DATA" wordmark (white, over photo)
|
||||||
|
function DataWordmark({ height = 30 }: { height?: number }) {
|
||||||
|
return (
|
||||||
|
<svg width={(height * 66) / 20} height={height} viewBox="0 0 66 20" fill="none" style={{ color: c.white }}>
|
||||||
|
<path opacity="0.35" d="M12 16H4V8H12V16Z" fill="currentColor" />
|
||||||
|
<path d="M12 4H4V16H12V4ZM16 20H0V0H16V20Z" fill="currentColor" />
|
||||||
|
<path
|
||||||
|
d="M63.3543 16L62.5119 12.8711H58.6437L57.8013 16H55.7383L59.2454 4H61.9618L65.4689 16H63.3543ZM61.0678 7.851L60.6896 5.94269H60.4489L60.0707 7.851L59.1595 11.1347H61.9962L61.0678 7.851Z"
|
||||||
|
fill="currentColor"
|
||||||
|
/>
|
||||||
|
<path d="M52.5951 5.87392V16H50.4461V5.87392H47.4375V4H55.6209V5.87392H52.5951Z" fill="currentColor" />
|
||||||
|
<path
|
||||||
|
d="M45.2059 16L44.3635 12.8711H40.4953L39.6529 16H37.5898L41.097 4H43.8133L47.3205 16H45.2059ZM42.9194 7.851L42.5411 5.94269H42.3004L41.9222 7.851L41.011 11.1347H43.8477L42.9194 7.851Z"
|
||||||
|
fill="currentColor"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M28 4H32.0917C32.8138 4 33.4556 4.11461 34.0172 4.34384C34.5903 4.5616 35.0716 4.9169 35.4613 5.40974C35.8625 5.89112 36.1662 6.51003 36.3725 7.26648C36.5788 8.02292 36.6819 8.9341 36.6819 10C36.6819 11.0659 36.5788 11.9771 36.3725 12.7335C36.1662 13.49 35.8625 14.1146 35.4613 14.6075C35.0716 15.0888 34.5903 15.4441 34.0172 15.6734C33.4556 15.8911 32.8138 16 32.0917 16H28V4ZM32.0917 14.1261C32.8252 14.1261 33.3926 13.9026 33.7937 13.4556C34.1948 12.9971 34.3954 12.3152 34.3954 11.4097V8.59026C34.3954 7.68481 34.1948 7.0086 33.7937 6.5616C33.3926 6.10315 32.8252 5.87392 32.0917 5.87392H30.149V14.1261H32.0917Z"
|
||||||
|
fill="currentColor"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function NZSheep() {
|
||||||
|
const frame = useCurrentFrame()
|
||||||
|
|
||||||
|
const count = Math.round(
|
||||||
|
PER_SHEEP *
|
||||||
|
interpolate(frame, [18, 90], [0, 1], {
|
||||||
|
extrapolateLeft: "clamp",
|
||||||
|
extrapolateRight: "clamp",
|
||||||
|
easing: Easing.out(Easing.cubic),
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
|
// slow Ken Burns push-in (scale up only — never reveals an edge)
|
||||||
|
const zoom = interpolate(frame, [0, 150], [1.06, 1.12], {
|
||||||
|
extrapolateRight: "clamp",
|
||||||
|
easing: Easing.inOut(Easing.quad),
|
||||||
|
})
|
||||||
|
|
||||||
|
return (
|
||||||
|
<AbsoluteFill style={{ background: "#0c0c0c", overflow: "hidden" }}>
|
||||||
|
{/* the sheep, staring */}
|
||||||
|
<Img
|
||||||
|
src={staticFile("sheep.jpg")}
|
||||||
|
style={{
|
||||||
|
position: "absolute",
|
||||||
|
width: "100%",
|
||||||
|
height: "100%",
|
||||||
|
objectFit: "cover",
|
||||||
|
objectPosition: "center 38%",
|
||||||
|
transform: `scale(${zoom})`,
|
||||||
|
transformOrigin: "center 35%",
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* legibility scrims */}
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
position: "absolute",
|
||||||
|
inset: 0,
|
||||||
|
background:
|
||||||
|
"linear-gradient(to bottom, rgba(0,0,0,0.5) 0%, rgba(0,0,0,0) 22%, rgba(0,0,0,0) 48%, rgba(0,0,0,0.78) 100%)",
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* content */}
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
position: "absolute",
|
||||||
|
inset: 0,
|
||||||
|
boxSizing: "border-box",
|
||||||
|
padding: 64,
|
||||||
|
color: c.white,
|
||||||
|
fontFamily: MONO,
|
||||||
|
display: "flex",
|
||||||
|
flexDirection: "column",
|
||||||
|
justifyContent: "space-between",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{/* header */}
|
||||||
|
<div style={{ display: "flex", justifyContent: "space-between", alignItems: "center" }}>
|
||||||
|
<DataWordmark height={30} />
|
||||||
|
<div style={{ fontSize: 20, fontWeight: 500, color: c.dim, letterSpacing: 1 }}>JUN 22–28, 2026</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* bottom block */}
|
||||||
|
<div>
|
||||||
|
<div style={{ fontSize: 23, fontWeight: 600, color: c.dim, letterSpacing: 2, marginBottom: 8 }}>
|
||||||
|
OPENCODE GO · NEW ZEALAND
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
fontSize: 168,
|
||||||
|
fontWeight: 600,
|
||||||
|
lineHeight: 0.92,
|
||||||
|
letterSpacing: -5,
|
||||||
|
fontVariantNumeric: "tabular-nums",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{nf.format(count)}
|
||||||
|
</div>
|
||||||
|
<div style={{ fontSize: 50, fontWeight: 600, letterSpacing: -1, marginTop: 4 }}>tokens per sheep</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
display: "flex",
|
||||||
|
justifyContent: "space-between",
|
||||||
|
alignItems: "center",
|
||||||
|
marginTop: 34,
|
||||||
|
fontSize: 20,
|
||||||
|
fontWeight: 500,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div style={{ color: c.dim }}>40.9B tokens ÷ 23.6M sheep · last week</div>
|
||||||
|
<div style={{ color: c.white }}>opencode.ai/data</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</AbsoluteFill>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,254 @@
|
|||||||
|
import React from "react"
|
||||||
|
import { AbsoluteFill, Easing, interpolate, useCurrentFrame, useVideoConfig } from "remotion"
|
||||||
|
import { days, launchIndex, glmWeekTokensT, segments } from "./data"
|
||||||
|
|
||||||
|
// stats.opencode.ai design tokens (light theme)
|
||||||
|
const c = {
|
||||||
|
bg: "#ffffff",
|
||||||
|
ink: "#161616",
|
||||||
|
muted: "#5c5c5c",
|
||||||
|
faint: "#808080",
|
||||||
|
line: "#e6e6e6",
|
||||||
|
dot: "#ededed",
|
||||||
|
accent: "#3b5cf6",
|
||||||
|
accentHi: "#5b78ff",
|
||||||
|
}
|
||||||
|
|
||||||
|
const MONO = '"IBM Plex Mono", ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace'
|
||||||
|
const W = 1080
|
||||||
|
|
||||||
|
const DOT_MASK =
|
||||||
|
"url(\"data:image/svg+xml,%3Csvg viewBox='0 0 6 6' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M0 0H2V2H0V0Z' fill='black'/%3E%3C/svg%3E\")"
|
||||||
|
|
||||||
|
const field = segments.filter((s) => !s.hero)
|
||||||
|
const glmColor = segments.find((s) => s.hero)!.color
|
||||||
|
|
||||||
|
const clamp = (v: number, lo: number, hi: number) => Math.min(hi, Math.max(lo, v))
|
||||||
|
|
||||||
|
// the correct opencode "DATA" wordmark (from stats.opencode.ai header)
|
||||||
|
function DataWordmark({ height = 30, color = c.ink }: { height?: number; color?: string }) {
|
||||||
|
return (
|
||||||
|
<svg width={(height * 66) / 20} height={height} viewBox="0 0 66 20" fill="none" style={{ color }}>
|
||||||
|
<path opacity="0.2" d="M12 16H4V8H12V16Z" fill="currentColor" />
|
||||||
|
<path d="M12 4H4V16H12V4ZM16 20H0V0H16V20Z" fill="currentColor" />
|
||||||
|
<path
|
||||||
|
d="M63.3543 16L62.5119 12.8711H58.6437L57.8013 16H55.7383L59.2454 4H61.9618L65.4689 16H63.3543ZM61.0678 7.851L60.6896 5.94269H60.4489L60.0707 7.851L59.1595 11.1347H61.9962L61.0678 7.851Z"
|
||||||
|
fill="currentColor"
|
||||||
|
/>
|
||||||
|
<path d="M52.5951 5.87392V16H50.4461V5.87392H47.4375V4H55.6209V5.87392H52.5951Z" fill="currentColor" />
|
||||||
|
<path
|
||||||
|
d="M45.2059 16L44.3635 12.8711H40.4953L39.6529 16H37.5898L41.097 4H43.8133L47.3205 16H45.2059ZM42.9194 7.851L42.5411 5.94269H42.3004L41.9222 7.851L41.011 11.1347H43.8477L42.9194 7.851Z"
|
||||||
|
fill="currentColor"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M28 4H32.0917C32.8138 4 33.4556 4.11461 34.0172 4.34384C34.5903 4.5616 35.0716 4.9169 35.4613 5.40974C35.8625 5.89112 36.1662 6.51003 36.3725 7.26648C36.5788 8.02292 36.6819 8.9341 36.6819 10C36.6819 11.0659 36.5788 11.9771 36.3725 12.7335C36.1662 13.49 35.8625 14.1146 35.4613 14.6075C35.0716 15.0888 34.5903 15.4441 34.0172 15.6734C33.4556 15.8911 32.8138 16 32.0917 16H28V4ZM32.0917 14.1261C32.8252 14.1261 33.3926 13.9026 33.7937 13.4556C34.1948 12.9971 34.3954 12.3152 34.3954 11.4097V8.59026C34.3954 7.68481 34.1948 7.0086 33.7937 6.5616C33.3926 6.10315 32.8252 5.87392 32.0917 5.87392H30.149V14.1261H32.0917Z"
|
||||||
|
fill="currentColor"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function GLM52Rise() {
|
||||||
|
const frame = useCurrentFrame()
|
||||||
|
const { fps } = useVideoConfig()
|
||||||
|
|
||||||
|
// ---------- virtual camera ----------
|
||||||
|
// open zoomed-in on the field, pan right while the blue fills, then pull back to reveal.
|
||||||
|
const K = [0, 32, 150, 206, 240]
|
||||||
|
const ease = Easing.inOut(Easing.cubic)
|
||||||
|
const opt = { extrapolateLeft: "clamp" as const, extrapolateRight: "clamp" as const, easing: ease }
|
||||||
|
const s = interpolate(frame, K, [1.82, 1.72, 1.72, 1.0, 1.0], opt)
|
||||||
|
let fx = interpolate(frame, K, [420, 438, 760, 540, 540], opt)
|
||||||
|
let fy = interpolate(frame, K, [664, 664, 664, 540, 540], opt)
|
||||||
|
// keep the framing inside the 1080 canvas so edges never reveal black
|
||||||
|
fx = clamp(fx, 540 / s, W - 540 / s)
|
||||||
|
fy = clamp(fy, 540 / s, W - 540 / s)
|
||||||
|
const camera = `translate(${540 - fx * s}px, ${540 - fy * s}px) scale(${s})`
|
||||||
|
|
||||||
|
// ---------- blue sweep (synced to the pan) ----------
|
||||||
|
const p = interpolate(frame, [32, 150], [0, 1], { extrapolateLeft: "clamp", extrapolateRight: "clamp", easing: ease })
|
||||||
|
const revealAmount = p * (days.length - launchIndex) + 0.35
|
||||||
|
const fillOf = (i: number) => clamp(revealAmount - (i - launchIndex), 0, 1)
|
||||||
|
|
||||||
|
// token number climbs as the camera pulls back and the headline re-enters frame
|
||||||
|
const tokensT =
|
||||||
|
glmWeekTokensT *
|
||||||
|
interpolate(frame, [150, 202], [0, 1], {
|
||||||
|
extrapolateLeft: "clamp",
|
||||||
|
extrapolateRight: "clamp",
|
||||||
|
easing: Easing.out(Easing.cubic),
|
||||||
|
})
|
||||||
|
|
||||||
|
// chart geometry
|
||||||
|
const chartH = 440
|
||||||
|
const chartW = 936
|
||||||
|
const gap = 14
|
||||||
|
const EXAGGERATE = 2.876 // broken y-axis: GLM-5.2 magnified ~2.9x for emphasis
|
||||||
|
|
||||||
|
return (
|
||||||
|
<AbsoluteFill style={{ background: c.bg, overflow: "hidden" }}>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
position: "absolute",
|
||||||
|
width: W,
|
||||||
|
height: W,
|
||||||
|
background: c.bg,
|
||||||
|
transformOrigin: "0 0",
|
||||||
|
transform: camera,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
width: W,
|
||||||
|
height: W,
|
||||||
|
boxSizing: "border-box",
|
||||||
|
padding: 72,
|
||||||
|
color: c.ink,
|
||||||
|
fontFamily: MONO,
|
||||||
|
display: "flex",
|
||||||
|
flexDirection: "column",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{/* header */}
|
||||||
|
<div style={{ display: "flex", justifyContent: "space-between", alignItems: "center" }}>
|
||||||
|
<DataWordmark height={30} />
|
||||||
|
<div style={{ fontSize: 20, fontWeight: 500, color: c.faint, letterSpacing: 1 }}>JUN 12–25, 2026</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* headline (static) */}
|
||||||
|
<div style={{ marginTop: 52 }}>
|
||||||
|
<div style={{ fontSize: 92, fontWeight: 600, lineHeight: 0.98, letterSpacing: -2 }}>
|
||||||
|
GLM-5.2 <span style={{ color: c.accent }}>broke out</span>
|
||||||
|
</div>
|
||||||
|
<div style={{ marginTop: 22, fontSize: 30, fontWeight: 500, color: c.muted }}>
|
||||||
|
From 0 to <span style={{ color: c.ink }}>{tokensT.toFixed(2)}T tokens</span> in a week.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* stacked chart */}
|
||||||
|
<div style={{ flex: 1, display: "flex", alignItems: "flex-end", marginTop: 40 }}>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
position: "relative",
|
||||||
|
width: chartW,
|
||||||
|
height: chartH,
|
||||||
|
margin: "0 auto",
|
||||||
|
borderBottom: `2px solid ${c.ink}`,
|
||||||
|
boxSizing: "border-box",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{/* faint dotted backdrop */}
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
position: "absolute",
|
||||||
|
left: -8,
|
||||||
|
right: -8,
|
||||||
|
top: -8,
|
||||||
|
bottom: 0,
|
||||||
|
background: c.dot,
|
||||||
|
WebkitMaskImage: DOT_MASK,
|
||||||
|
maskImage: DOT_MASK,
|
||||||
|
WebkitMaskSize: "12px 12px",
|
||||||
|
maskSize: "12px 12px",
|
||||||
|
WebkitMaskRepeat: "repeat",
|
||||||
|
maskRepeat: "repeat",
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* columns */}
|
||||||
|
<div style={{ position: "absolute", inset: 0, display: "flex", gap, alignItems: "flex-end" }}>
|
||||||
|
{days.map((d, i) => {
|
||||||
|
const glmShare = d.glm / d.total
|
||||||
|
const blueH = Math.round(glmShare * EXAGGERATE * chartH)
|
||||||
|
const filled = Math.round(blueH * fillOf(i))
|
||||||
|
const slotGap = filled > 2 ? 5 : 0
|
||||||
|
const fieldH = chartH - filled - slotGap
|
||||||
|
const fieldTotal = d.dsf + d.dsp + d.mm + d.others
|
||||||
|
return (
|
||||||
|
<div key={i} style={{ position: "relative", flex: 1, height: chartH }}>
|
||||||
|
{/* gray field of other models (already in place) */}
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
position: "absolute",
|
||||||
|
top: 0,
|
||||||
|
left: 0,
|
||||||
|
right: 0,
|
||||||
|
height: fieldH,
|
||||||
|
display: "flex",
|
||||||
|
flexDirection: "column-reverse",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{field.map((seg) => (
|
||||||
|
<div
|
||||||
|
key={seg.key}
|
||||||
|
style={{
|
||||||
|
height: Math.round(((d[seg.key as keyof typeof d] as number) / fieldTotal) * fieldH),
|
||||||
|
background: seg.color,
|
||||||
|
borderTop: `2px solid ${c.bg}`,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
{/* GLM-5.2, animates in */}
|
||||||
|
{filled > 2 && (
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
position: "absolute",
|
||||||
|
left: 0,
|
||||||
|
right: 0,
|
||||||
|
bottom: 0,
|
||||||
|
height: filled,
|
||||||
|
background: glmColor,
|
||||||
|
borderTop: `2px solid ${c.accentHi}`,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* day axis */}
|
||||||
|
<div style={{ display: "flex", gap, width: chartW, margin: "14px auto 0" }}>
|
||||||
|
{days.map((d, i) => (
|
||||||
|
<div
|
||||||
|
key={i}
|
||||||
|
style={{
|
||||||
|
flex: 1,
|
||||||
|
textAlign: "center",
|
||||||
|
fontSize: 15,
|
||||||
|
fontWeight: 500,
|
||||||
|
color: i === days.length - 1 ? c.ink : c.faint,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{i === 0 || i === launchIndex || i === days.length - 1 ? d.date : ""}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* footer */}
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
display: "flex",
|
||||||
|
justifyContent: "space-between",
|
||||||
|
alignItems: "center",
|
||||||
|
marginTop: 28,
|
||||||
|
paddingTop: 22,
|
||||||
|
borderTop: `1px solid ${c.line}`,
|
||||||
|
fontSize: 20,
|
||||||
|
fontWeight: 500,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div style={{ display: "inline-flex", alignItems: "center", gap: 9, color: c.muted }}>
|
||||||
|
<span style={{ width: 13, height: 13, background: c.accent, display: "inline-block" }} />
|
||||||
|
GLM-5.2
|
||||||
|
</div>
|
||||||
|
<div style={{ color: c.ink }}>opencode.ai/data</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</AbsoluteFill>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -24,12 +24,12 @@
|
|||||||
"prettier": "3.6.2",
|
"prettier": "3.6.2",
|
||||||
"semver": "^7.6.0",
|
"semver": "^7.6.0",
|
||||||
"sst": "catalog:",
|
"sst": "catalog:",
|
||||||
"turbo": "2.8.13",
|
"turbo": "2.10.2",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
"packages/app": {
|
"packages/app": {
|
||||||
"name": "@opencode-ai/app",
|
"name": "@opencode-ai/app",
|
||||||
"version": "1.17.13",
|
"version": "1.17.15",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@dnd-kit/abstract": "0.5.0",
|
"@dnd-kit/abstract": "0.5.0",
|
||||||
"@dnd-kit/dom": "0.5.0",
|
"@dnd-kit/dom": "0.5.0",
|
||||||
@@ -93,7 +93,7 @@
|
|||||||
},
|
},
|
||||||
"packages/cli": {
|
"packages/cli": {
|
||||||
"name": "@opencode-ai/cli",
|
"name": "@opencode-ai/cli",
|
||||||
"version": "1.17.13",
|
"version": "1.17.15",
|
||||||
"bin": {
|
"bin": {
|
||||||
"lildax": "./bin/lildax.cjs",
|
"lildax": "./bin/lildax.cjs",
|
||||||
},
|
},
|
||||||
@@ -141,7 +141,7 @@
|
|||||||
},
|
},
|
||||||
"packages/codemode": {
|
"packages/codemode": {
|
||||||
"name": "@opencode-ai/codemode",
|
"name": "@opencode-ai/codemode",
|
||||||
"version": "0.0.1",
|
"version": "1.17.15",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"acorn": "8.15.0",
|
"acorn": "8.15.0",
|
||||||
"effect": "catalog:",
|
"effect": "catalog:",
|
||||||
@@ -155,7 +155,7 @@
|
|||||||
},
|
},
|
||||||
"packages/console/app": {
|
"packages/console/app": {
|
||||||
"name": "@opencode-ai/console-app",
|
"name": "@opencode-ai/console-app",
|
||||||
"version": "1.17.13",
|
"version": "1.17.15",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@cloudflare/vite-plugin": "1.15.2",
|
"@cloudflare/vite-plugin": "1.15.2",
|
||||||
"@ibm/plex": "6.4.1",
|
"@ibm/plex": "6.4.1",
|
||||||
@@ -191,7 +191,7 @@
|
|||||||
},
|
},
|
||||||
"packages/console/core": {
|
"packages/console/core": {
|
||||||
"name": "@opencode-ai/console-core",
|
"name": "@opencode-ai/console-core",
|
||||||
"version": "1.17.13",
|
"version": "1.17.15",
|
||||||
"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",
|
||||||
@@ -218,7 +218,7 @@
|
|||||||
},
|
},
|
||||||
"packages/console/function": {
|
"packages/console/function": {
|
||||||
"name": "@opencode-ai/console-function",
|
"name": "@opencode-ai/console-function",
|
||||||
"version": "1.17.13",
|
"version": "1.17.15",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@ai-sdk/anthropic": "3.0.82",
|
"@ai-sdk/anthropic": "3.0.82",
|
||||||
"@ai-sdk/openai": "3.0.48",
|
"@ai-sdk/openai": "3.0.48",
|
||||||
@@ -240,7 +240,7 @@
|
|||||||
},
|
},
|
||||||
"packages/console/mail": {
|
"packages/console/mail": {
|
||||||
"name": "@opencode-ai/console-mail",
|
"name": "@opencode-ai/console-mail",
|
||||||
"version": "1.17.13",
|
"version": "1.17.15",
|
||||||
"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",
|
||||||
@@ -264,7 +264,7 @@
|
|||||||
},
|
},
|
||||||
"packages/console/support": {
|
"packages/console/support": {
|
||||||
"name": "@opencode-ai/console-support",
|
"name": "@opencode-ai/console-support",
|
||||||
"version": "1.17.13",
|
"version": "1.17.15",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@cloudflare/vite-plugin": "1.15.2",
|
"@cloudflare/vite-plugin": "1.15.2",
|
||||||
"@opencode-ai/console-core": "workspace:*",
|
"@opencode-ai/console-core": "workspace:*",
|
||||||
@@ -284,7 +284,7 @@
|
|||||||
},
|
},
|
||||||
"packages/core": {
|
"packages/core": {
|
||||||
"name": "@opencode-ai/core",
|
"name": "@opencode-ai/core",
|
||||||
"version": "1.17.13",
|
"version": "1.17.15",
|
||||||
"bin": {
|
"bin": {
|
||||||
"opencode": "./bin/opencode",
|
"opencode": "./bin/opencode",
|
||||||
},
|
},
|
||||||
@@ -293,14 +293,14 @@
|
|||||||
"@ai-sdk/amazon-bedrock": "4.0.112",
|
"@ai-sdk/amazon-bedrock": "4.0.112",
|
||||||
"@ai-sdk/anthropic": "3.0.82",
|
"@ai-sdk/anthropic": "3.0.82",
|
||||||
"@ai-sdk/azure": "3.0.49",
|
"@ai-sdk/azure": "3.0.49",
|
||||||
"@ai-sdk/cerebras": "2.0.41",
|
"@ai-sdk/cerebras": "2.0.60",
|
||||||
"@ai-sdk/cohere": "3.0.27",
|
"@ai-sdk/cohere": "3.0.27",
|
||||||
"@ai-sdk/deepinfra": "2.0.41",
|
"@ai-sdk/deepinfra": "2.0.41",
|
||||||
"@ai-sdk/gateway": "3.0.104",
|
"@ai-sdk/gateway": "3.0.104",
|
||||||
"@ai-sdk/google": "3.0.73",
|
"@ai-sdk/google": "3.0.73",
|
||||||
"@ai-sdk/google-vertex": "4.0.128",
|
"@ai-sdk/google-vertex": "4.0.128",
|
||||||
"@ai-sdk/groq": "3.0.31",
|
"@ai-sdk/groq": "3.0.31",
|
||||||
"@ai-sdk/mistral": "3.0.27",
|
"@ai-sdk/mistral": "3.0.34",
|
||||||
"@ai-sdk/openai": "3.0.53",
|
"@ai-sdk/openai": "3.0.53",
|
||||||
"@ai-sdk/openai-compatible": "2.0.41",
|
"@ai-sdk/openai-compatible": "2.0.41",
|
||||||
"@ai-sdk/perplexity": "3.0.26",
|
"@ai-sdk/perplexity": "3.0.26",
|
||||||
@@ -308,7 +308,7 @@
|
|||||||
"@ai-sdk/provider-utils": "4.0.23",
|
"@ai-sdk/provider-utils": "4.0.23",
|
||||||
"@ai-sdk/togetherai": "2.0.41",
|
"@ai-sdk/togetherai": "2.0.41",
|
||||||
"@ai-sdk/vercel": "2.0.39",
|
"@ai-sdk/vercel": "2.0.39",
|
||||||
"@ai-sdk/xai": "3.0.82",
|
"@ai-sdk/xai": "3.0.102",
|
||||||
"@aws-sdk/credential-providers": "3.1057.0",
|
"@aws-sdk/credential-providers": "3.1057.0",
|
||||||
"@effect/opentelemetry": "catalog:",
|
"@effect/opentelemetry": "catalog:",
|
||||||
"@effect/platform-node": "catalog:",
|
"@effect/platform-node": "catalog:",
|
||||||
@@ -378,7 +378,7 @@
|
|||||||
},
|
},
|
||||||
"packages/desktop": {
|
"packages/desktop": {
|
||||||
"name": "@opencode-ai/desktop",
|
"name": "@opencode-ai/desktop",
|
||||||
"version": "1.17.13",
|
"version": "1.17.15",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@zip.js/zip.js": "2.7.62",
|
"@zip.js/zip.js": "2.7.62",
|
||||||
"effect": "catalog:",
|
"effect": "catalog:",
|
||||||
@@ -432,7 +432,7 @@
|
|||||||
},
|
},
|
||||||
"packages/effect-drizzle-sqlite": {
|
"packages/effect-drizzle-sqlite": {
|
||||||
"name": "@opencode-ai/effect-drizzle-sqlite",
|
"name": "@opencode-ai/effect-drizzle-sqlite",
|
||||||
"version": "1.17.13",
|
"version": "1.17.15",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"drizzle-orm": "catalog:",
|
"drizzle-orm": "catalog:",
|
||||||
"effect": "catalog:",
|
"effect": "catalog:",
|
||||||
@@ -446,7 +446,7 @@
|
|||||||
},
|
},
|
||||||
"packages/effect-sqlite-node": {
|
"packages/effect-sqlite-node": {
|
||||||
"name": "@opencode-ai/effect-sqlite-node",
|
"name": "@opencode-ai/effect-sqlite-node",
|
||||||
"version": "1.17.13",
|
"version": "1.17.15",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"effect": "catalog:",
|
"effect": "catalog:",
|
||||||
},
|
},
|
||||||
@@ -458,7 +458,7 @@
|
|||||||
},
|
},
|
||||||
"packages/enterprise": {
|
"packages/enterprise": {
|
||||||
"name": "@opencode-ai/enterprise",
|
"name": "@opencode-ai/enterprise",
|
||||||
"version": "1.17.13",
|
"version": "1.17.15",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@hono/standard-validator": "catalog:",
|
"@hono/standard-validator": "catalog:",
|
||||||
"@opencode-ai/core": "workspace:*",
|
"@opencode-ai/core": "workspace:*",
|
||||||
@@ -490,7 +490,7 @@
|
|||||||
},
|
},
|
||||||
"packages/function": {
|
"packages/function": {
|
||||||
"name": "@opencode-ai/function",
|
"name": "@opencode-ai/function",
|
||||||
"version": "1.17.13",
|
"version": "1.17.15",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@octokit/auth-app": "8.0.1",
|
"@octokit/auth-app": "8.0.1",
|
||||||
"@octokit/rest": "catalog:",
|
"@octokit/rest": "catalog:",
|
||||||
@@ -506,7 +506,7 @@
|
|||||||
},
|
},
|
||||||
"packages/http-recorder": {
|
"packages/http-recorder": {
|
||||||
"name": "@opencode-ai/http-recorder",
|
"name": "@opencode-ai/http-recorder",
|
||||||
"version": "1.17.13",
|
"version": "1.17.15",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@effect/platform-node": "4.0.0-beta.83",
|
"@effect/platform-node": "4.0.0-beta.83",
|
||||||
"@effect/platform-node-shared": "4.0.0-beta.83",
|
"@effect/platform-node-shared": "4.0.0-beta.83",
|
||||||
@@ -537,7 +537,7 @@
|
|||||||
},
|
},
|
||||||
"packages/llm": {
|
"packages/llm": {
|
||||||
"name": "@opencode-ai/llm",
|
"name": "@opencode-ai/llm",
|
||||||
"version": "1.17.13",
|
"version": "1.17.15",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@opencode-ai/schema": "workspace:*",
|
"@opencode-ai/schema": "workspace:*",
|
||||||
"@smithy/eventstream-codec": "4.2.14",
|
"@smithy/eventstream-codec": "4.2.14",
|
||||||
@@ -556,7 +556,7 @@
|
|||||||
},
|
},
|
||||||
"packages/opencode": {
|
"packages/opencode": {
|
||||||
"name": "opencode",
|
"name": "opencode",
|
||||||
"version": "1.17.13",
|
"version": "1.17.15",
|
||||||
"bin": {
|
"bin": {
|
||||||
"opencode": "./bin/opencode",
|
"opencode": "./bin/opencode",
|
||||||
},
|
},
|
||||||
@@ -575,14 +575,14 @@
|
|||||||
"@ai-sdk/google": "3.0.73",
|
"@ai-sdk/google": "3.0.73",
|
||||||
"@ai-sdk/google-vertex": "4.0.128",
|
"@ai-sdk/google-vertex": "4.0.128",
|
||||||
"@ai-sdk/groq": "3.0.31",
|
"@ai-sdk/groq": "3.0.31",
|
||||||
"@ai-sdk/mistral": "3.0.27",
|
"@ai-sdk/mistral": "3.0.34",
|
||||||
"@ai-sdk/openai": "3.0.53",
|
"@ai-sdk/openai": "3.0.53",
|
||||||
"@ai-sdk/openai-compatible": "2.0.41",
|
"@ai-sdk/openai-compatible": "2.0.41",
|
||||||
"@ai-sdk/perplexity": "3.0.26",
|
"@ai-sdk/perplexity": "3.0.26",
|
||||||
"@ai-sdk/provider": "3.0.8",
|
"@ai-sdk/provider": "3.0.8",
|
||||||
"@ai-sdk/togetherai": "2.0.41",
|
"@ai-sdk/togetherai": "2.0.41",
|
||||||
"@ai-sdk/vercel": "2.0.39",
|
"@ai-sdk/vercel": "2.0.39",
|
||||||
"@ai-sdk/xai": "3.0.82",
|
"@ai-sdk/xai": "3.0.102",
|
||||||
"@aws-sdk/credential-providers": "3.1057.0",
|
"@aws-sdk/credential-providers": "3.1057.0",
|
||||||
"@clack/prompts": "1.0.0-alpha.1",
|
"@clack/prompts": "1.0.0-alpha.1",
|
||||||
"@effect/opentelemetry": "catalog:",
|
"@effect/opentelemetry": "catalog:",
|
||||||
@@ -593,6 +593,7 @@
|
|||||||
"@octokit/graphql": "9.0.2",
|
"@octokit/graphql": "9.0.2",
|
||||||
"@octokit/rest": "catalog:",
|
"@octokit/rest": "catalog:",
|
||||||
"@openauthjs/openauth": "catalog:",
|
"@openauthjs/openauth": "catalog:",
|
||||||
|
"@opencode-ai/codemode": "workspace:*",
|
||||||
"@opencode-ai/llm": "workspace:*",
|
"@opencode-ai/llm": "workspace:*",
|
||||||
"@opencode-ai/plugin": "workspace:*",
|
"@opencode-ai/plugin": "workspace:*",
|
||||||
"@opencode-ai/protocol": "workspace:*",
|
"@opencode-ai/protocol": "workspace:*",
|
||||||
@@ -686,7 +687,7 @@
|
|||||||
},
|
},
|
||||||
"packages/plugin": {
|
"packages/plugin": {
|
||||||
"name": "@opencode-ai/plugin",
|
"name": "@opencode-ai/plugin",
|
||||||
"version": "1.17.13",
|
"version": "1.17.15",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@ai-sdk/provider": "3.0.8",
|
"@ai-sdk/provider": "3.0.8",
|
||||||
"@opencode-ai/sdk": "workspace:*",
|
"@opencode-ai/sdk": "workspace:*",
|
||||||
@@ -703,9 +704,9 @@
|
|||||||
"typescript": "catalog:",
|
"typescript": "catalog:",
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"@opentui/core": ">=0.3.4",
|
"@opentui/core": ">=0.4.3",
|
||||||
"@opentui/keymap": ">=0.3.4",
|
"@opentui/keymap": ">=0.4.3",
|
||||||
"@opentui/solid": ">=0.3.4",
|
"@opentui/solid": ">=0.4.3",
|
||||||
},
|
},
|
||||||
"optionalPeers": [
|
"optionalPeers": [
|
||||||
"@opentui/core",
|
"@opentui/core",
|
||||||
@@ -762,7 +763,7 @@
|
|||||||
},
|
},
|
||||||
"packages/sdk/js": {
|
"packages/sdk/js": {
|
||||||
"name": "@opencode-ai/sdk",
|
"name": "@opencode-ai/sdk",
|
||||||
"version": "1.17.13",
|
"version": "1.17.15",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"cross-spawn": "catalog:",
|
"cross-spawn": "catalog:",
|
||||||
},
|
},
|
||||||
@@ -777,7 +778,7 @@
|
|||||||
},
|
},
|
||||||
"packages/server": {
|
"packages/server": {
|
||||||
"name": "@opencode-ai/server",
|
"name": "@opencode-ai/server",
|
||||||
"version": "1.17.13",
|
"version": "1.17.15",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@opencode-ai/core": "workspace:*",
|
"@opencode-ai/core": "workspace:*",
|
||||||
"@opencode-ai/protocol": "workspace:*",
|
"@opencode-ai/protocol": "workspace:*",
|
||||||
@@ -792,7 +793,7 @@
|
|||||||
},
|
},
|
||||||
"packages/session-ui": {
|
"packages/session-ui": {
|
||||||
"name": "@opencode-ai/session-ui",
|
"name": "@opencode-ai/session-ui",
|
||||||
"version": "1.17.13",
|
"version": "1.17.15",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@kobalte/core": "catalog:",
|
"@kobalte/core": "catalog:",
|
||||||
"@opencode-ai/core": "workspace:*",
|
"@opencode-ai/core": "workspace:*",
|
||||||
@@ -836,7 +837,7 @@
|
|||||||
},
|
},
|
||||||
"packages/slack": {
|
"packages/slack": {
|
||||||
"name": "@opencode-ai/slack",
|
"name": "@opencode-ai/slack",
|
||||||
"version": "1.17.13",
|
"version": "1.17.15",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@opencode-ai/sdk": "workspace:*",
|
"@opencode-ai/sdk": "workspace:*",
|
||||||
"@slack/bolt": "^3.17.1",
|
"@slack/bolt": "^3.17.1",
|
||||||
@@ -849,7 +850,7 @@
|
|||||||
},
|
},
|
||||||
"packages/stats/app": {
|
"packages/stats/app": {
|
||||||
"name": "@opencode-ai/stats-app",
|
"name": "@opencode-ai/stats-app",
|
||||||
"version": "1.17.13",
|
"version": "1.17.15",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@ibm/plex": "6.4.1",
|
"@ibm/plex": "6.4.1",
|
||||||
"@opencode-ai/stats-core": "workspace:*",
|
"@opencode-ai/stats-core": "workspace:*",
|
||||||
@@ -882,7 +883,7 @@
|
|||||||
},
|
},
|
||||||
"packages/stats/core": {
|
"packages/stats/core": {
|
||||||
"name": "@opencode-ai/stats-core",
|
"name": "@opencode-ai/stats-core",
|
||||||
"version": "1.17.13",
|
"version": "1.17.15",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@aws-sdk/client-athena": "3.933.0",
|
"@aws-sdk/client-athena": "3.933.0",
|
||||||
"@planetscale/database": "1.19.0",
|
"@planetscale/database": "1.19.0",
|
||||||
@@ -901,7 +902,7 @@
|
|||||||
},
|
},
|
||||||
"packages/stats/server": {
|
"packages/stats/server": {
|
||||||
"name": "@opencode-ai/stats-server",
|
"name": "@opencode-ai/stats-server",
|
||||||
"version": "1.17.13",
|
"version": "1.17.15",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@aws-sdk/client-firehose": "3.933.0",
|
"@aws-sdk/client-firehose": "3.933.0",
|
||||||
"@effect/platform-node": "catalog:",
|
"@effect/platform-node": "catalog:",
|
||||||
@@ -942,7 +943,7 @@
|
|||||||
},
|
},
|
||||||
"packages/tui": {
|
"packages/tui": {
|
||||||
"name": "@opencode-ai/tui",
|
"name": "@opencode-ai/tui",
|
||||||
"version": "1.17.13",
|
"version": "1.17.15",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@opencode-ai/core": "workspace:*",
|
"@opencode-ai/core": "workspace:*",
|
||||||
"@opencode-ai/plugin": "workspace:*",
|
"@opencode-ai/plugin": "workspace:*",
|
||||||
@@ -969,7 +970,7 @@
|
|||||||
},
|
},
|
||||||
"packages/ui": {
|
"packages/ui": {
|
||||||
"name": "@opencode-ai/ui",
|
"name": "@opencode-ai/ui",
|
||||||
"version": "1.17.13",
|
"version": "1.17.15",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@kobalte/core": "catalog:",
|
"@kobalte/core": "catalog:",
|
||||||
"@pierre/diffs": "catalog:",
|
"@pierre/diffs": "catalog:",
|
||||||
@@ -1020,7 +1021,7 @@
|
|||||||
},
|
},
|
||||||
"packages/web": {
|
"packages/web": {
|
||||||
"name": "@opencode-ai/web",
|
"name": "@opencode-ai/web",
|
||||||
"version": "1.17.13",
|
"version": "1.17.15",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@astrojs/cloudflare": "12.6.3",
|
"@astrojs/cloudflare": "12.6.3",
|
||||||
"@astrojs/markdown-remark": "6.3.1",
|
"@astrojs/markdown-remark": "6.3.1",
|
||||||
@@ -1062,19 +1063,24 @@
|
|||||||
"tree-sitter-bash",
|
"tree-sitter-bash",
|
||||||
],
|
],
|
||||||
"patchedDependencies": {
|
"patchedDependencies": {
|
||||||
|
"ai-gateway-provider@3.1.2": "patches/ai-gateway-provider@3.1.2.patch",
|
||||||
|
"@ai-sdk/openai-compatible@2.0.37": "patches/@ai-sdk%2Fopenai-compatible@2.0.37.patch",
|
||||||
"@pierre/trees@1.0.0-beta.4": "patches/@pierre%2Ftrees@1.0.0-beta.4.patch",
|
"@pierre/trees@1.0.0-beta.4": "patches/@pierre%2Ftrees@1.0.0-beta.4.patch",
|
||||||
|
"@ai-sdk/xai@3.0.102": "patches/@ai-sdk%2Fxai@3.0.102.patch",
|
||||||
"@modelcontextprotocol/sdk@1.29.0": "patches/@modelcontextprotocol%2Fsdk@1.29.0.patch",
|
"@modelcontextprotocol/sdk@1.29.0": "patches/@modelcontextprotocol%2Fsdk@1.29.0.patch",
|
||||||
"gcp-metadata@8.1.2": "patches/gcp-metadata@8.1.2.patch",
|
"gcp-metadata@8.1.2": "patches/gcp-metadata@8.1.2.patch",
|
||||||
|
"@ai-sdk/cohere@3.0.27": "patches/@ai-sdk%2Fcohere@3.0.27.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",
|
||||||
"effect@4.0.0-beta.83": "patches/effect@4.0.0-beta.83.patch",
|
"effect@4.0.0-beta.83": "patches/effect@4.0.0-beta.83.patch",
|
||||||
"@npmcli/agent@4.0.2": "patches/@npmcli%2Fagent@4.0.2.patch",
|
"@npmcli/agent@4.0.2": "patches/@npmcli%2Fagent@4.0.2.patch",
|
||||||
"@silvia-odwyer/photon-node@0.3.4": "patches/@silvia-odwyer%2Fphoton-node@0.3.4.patch",
|
"@silvia-odwyer/photon-node@0.3.4": "patches/@silvia-odwyer%2Fphoton-node@0.3.4.patch",
|
||||||
"@tanstack/solid-virtual@3.13.28": "patches/@tanstack%2Fsolid-virtual@3.13.28.patch",
|
"@tanstack/solid-virtual@3.13.28": "patches/@tanstack%2Fsolid-virtual@3.13.28.patch",
|
||||||
"solid-js@1.9.10": "patches/solid-js@1.9.10.patch",
|
"solid-js@1.9.10": "patches/solid-js@1.9.10.patch",
|
||||||
|
"@ai-sdk/groq@3.0.31": "patches/@ai-sdk%2Fgroq@3.0.31.patch",
|
||||||
"@ai-sdk/google@3.0.73": "patches/@ai-sdk%2Fgoogle@3.0.73.patch",
|
"@ai-sdk/google@3.0.73": "patches/@ai-sdk%2Fgoogle@3.0.73.patch",
|
||||||
"@ai-sdk/xai@3.0.82": "patches/@ai-sdk%2Fxai@3.0.82.patch",
|
|
||||||
"@tanstack/virtual-core@3.17.0": "patches/@tanstack%2Fvirtual-core@3.17.0.patch",
|
"@tanstack/virtual-core@3.17.0": "patches/@tanstack%2Fvirtual-core@3.17.0.patch",
|
||||||
"pacote@21.5.0": "patches/pacote@21.5.0.patch",
|
"pacote@21.5.0": "patches/pacote@21.5.0.patch",
|
||||||
|
"@ai-sdk/mistral@3.0.34": "patches/@ai-sdk%2Fmistral@3.0.34.patch",
|
||||||
},
|
},
|
||||||
"overrides": {
|
"overrides": {
|
||||||
"@opentui/core": "catalog:",
|
"@opentui/core": "catalog:",
|
||||||
@@ -1095,9 +1101,9 @@
|
|||||||
"@npmcli/arborist": "9.4.0",
|
"@npmcli/arborist": "9.4.0",
|
||||||
"@octokit/rest": "22.0.0",
|
"@octokit/rest": "22.0.0",
|
||||||
"@openauthjs/openauth": "0.0.0-20250322224806",
|
"@openauthjs/openauth": "0.0.0-20250322224806",
|
||||||
"@opentui/core": "0.3.4",
|
"@opentui/core": "0.4.3",
|
||||||
"@opentui/keymap": "0.3.4",
|
"@opentui/keymap": "0.4.3",
|
||||||
"@opentui/solid": "0.3.4",
|
"@opentui/solid": "0.4.3",
|
||||||
"@pierre/diffs": "1.2.10",
|
"@pierre/diffs": "1.2.10",
|
||||||
"@playwright/test": "1.59.1",
|
"@playwright/test": "1.59.1",
|
||||||
"@sentry/solid": "10.36.0",
|
"@sentry/solid": "10.36.0",
|
||||||
@@ -1172,7 +1178,7 @@
|
|||||||
|
|
||||||
"@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/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@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/cerebras": ["@ai-sdk/cerebras@2.0.60", "", { "dependencies": { "@ai-sdk/openai-compatible": "2.0.54", "@ai-sdk/provider": "3.0.12", "@ai-sdk/provider-utils": "4.0.33" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-Rnok3cThg6awBwaDSyiZpgRpbV7pqxGYrA89LODCo5cuEHeP2h0AM0lLHP7zIkclAdXfOm4wldKi/S2T/DGCOw=="],
|
||||||
|
|
||||||
"@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/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=="],
|
||||||
|
|
||||||
@@ -1194,7 +1200,7 @@
|
|||||||
|
|
||||||
"@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/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@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/mistral": ["@ai-sdk/mistral@3.0.34", "", { "dependencies": { "@ai-sdk/provider": "3.0.10", "@ai-sdk/provider-utils": "4.0.26" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-HpK28sWGdIfg1vTSScJNtzVdvNRfA4mfCmPmPR+j/MGJ0oAuEJMqxWkL96ZnGPdhZt5KdW09aKovdIe+q2zQ7A=="],
|
||||||
|
|
||||||
"@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": ["@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=="],
|
||||||
|
|
||||||
@@ -1210,7 +1216,7 @@
|
|||||||
|
|
||||||
"@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/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@3.0.82", "", { "dependencies": { "@ai-sdk/openai-compatible": "2.0.41", "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.23" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-A0VFMufnVf4wODcT3SPQUUzvYXiIO1VhFuXj9r6z/vP4rlo+QRDPw3WSTchcz93ROQWSfBE3I6Szqz342OHi5w=="],
|
"@ai-sdk/xai": ["@ai-sdk/xai@3.0.102", "", { "dependencies": { "@ai-sdk/openai-compatible": "2.0.56", "@ai-sdk/provider": "3.0.13", "@ai-sdk/provider-utils": "4.0.35" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-NeQyOR7OCqDMgaLS4uNX/ep/HrwUzzFYLzXQSRoqLy2jsnqxAJhsgltRwAwf+ADjyPBIAKEOestWnIQA+LrLrQ=="],
|
||||||
|
|
||||||
"@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=="],
|
||||||
|
|
||||||
@@ -2030,27 +2036,27 @@
|
|||||||
|
|
||||||
"@opentelemetry/semantic-conventions": ["@opentelemetry/semantic-conventions@1.41.1", "", {}, "sha512-/UhIkaZgPutTFmQ7RnIJGgDXZmtEJ7Dvi86xNTFWcnRxVRNk/aotsqDJYeEvDP+FSMB2SdW+pQzNMcWP0rwuNA=="],
|
"@opentelemetry/semantic-conventions": ["@opentelemetry/semantic-conventions@1.41.1", "", {}, "sha512-/UhIkaZgPutTFmQ7RnIJGgDXZmtEJ7Dvi86xNTFWcnRxVRNk/aotsqDJYeEvDP+FSMB2SdW+pQzNMcWP0rwuNA=="],
|
||||||
|
|
||||||
"@opentui/core": ["@opentui/core@0.3.4", "", { "dependencies": { "bun-ffi-structs": "0.2.2", "diff": "9.0.0", "marked": "17.0.1", "string-width": "7.2.0", "strip-ansi": "7.1.2", "yoga-layout": "3.2.1" }, "optionalDependencies": { "@opentui/core-darwin-arm64": "0.3.4", "@opentui/core-darwin-x64": "0.3.4", "@opentui/core-linux-arm64": "0.3.4", "@opentui/core-linux-arm64-musl": "0.3.4", "@opentui/core-linux-x64": "0.3.4", "@opentui/core-linux-x64-musl": "0.3.4", "@opentui/core-win32-arm64": "0.3.4", "@opentui/core-win32-x64": "0.3.4" }, "peerDependencies": { "web-tree-sitter": "0.25.10" } }, "sha512-y0DlrChP9lcJ4jC5z/1wMS34+ygfSTW7gD5OJHwJaAScfmlFvuJOZbwmCGrJURZ+5wFBxuOi9LatZsmeAUIKAA=="],
|
"@opentui/core": ["@opentui/core@0.4.3", "", { "dependencies": { "bun-ffi-structs": "0.2.4", "diff": "9.0.0", "marked": "17.0.1", "string-width": "7.2.0", "strip-ansi": "7.1.2" }, "optionalDependencies": { "@opentui/core-darwin-arm64": "0.4.3", "@opentui/core-darwin-x64": "0.4.3", "@opentui/core-linux-arm64": "0.4.3", "@opentui/core-linux-arm64-musl": "0.4.3", "@opentui/core-linux-x64": "0.4.3", "@opentui/core-linux-x64-musl": "0.4.3", "@opentui/core-win32-arm64": "0.4.3", "@opentui/core-win32-x64": "0.4.3" }, "peerDependencies": { "web-tree-sitter": "0.25.10" } }, "sha512-rrJfAk13tALDqldYjhc78eWQ+aKq1iknJgffIOg3OwyZoqQo+p6gtuqyhmWvXIfQzlNUbpgpCPcxbXlhMnlaHQ=="],
|
||||||
|
|
||||||
"@opentui/core-darwin-arm64": ["@opentui/core-darwin-arm64@0.3.4", "", { "os": "darwin", "cpu": "arm64" }, "sha512-4A7JYXUsZqhu9PPCe07E30ourSJYkitkwMujUyNKjM5e/dHNDVnz+5r5cO3M5snofLafc1DN7+9jEPn4UQzchQ=="],
|
"@opentui/core-darwin-arm64": ["@opentui/core-darwin-arm64@0.4.3", "", { "os": "darwin", "cpu": "arm64" }, "sha512-p5+7AAxpxGuDGagyQfewKtmTFnN7THvTVY4FyKqUtJomNaHdQXPHztapNNzMx0DGWbwOUbVKzpL+yc3CZY3chQ=="],
|
||||||
|
|
||||||
"@opentui/core-darwin-x64": ["@opentui/core-darwin-x64@0.3.4", "", { "os": "darwin", "cpu": "x64" }, "sha512-Jvm9E8n2sPhKEyKSXn9GlmJcj8WoJXJTooXb3djwjVaiimjihIj0XxHzCWhdqbDtQp+VxDFyCKoQagOOz20qhA=="],
|
"@opentui/core-darwin-x64": ["@opentui/core-darwin-x64@0.4.3", "", { "os": "darwin", "cpu": "x64" }, "sha512-+fh0vEUE0lwVC7RW5ijYLRlTLp5NfvCRj8SzxDVd7IL2j2ssB6YXcfIbXq2EW7UGnrejwPRXf1tgUrIXW9KmOw=="],
|
||||||
|
|
||||||
"@opentui/core-linux-arm64": ["@opentui/core-linux-arm64@0.3.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-0uPuHCeZxm/O7+L+iNQl8zRAfehiwYstKkT9J0uTZO64/byBCLvy5lvn1DiE/72s/nTJ5nwpLN+pQs2/WYVKLQ=="],
|
"@opentui/core-linux-arm64": ["@opentui/core-linux-arm64@0.4.3", "", { "os": "linux", "cpu": "arm64" }, "sha512-gl6qA5QJy6u8Cbt7gOtHbhhfMZ4qQDb0kEwFXHcMGmbnKzz4OHoq74D6tNjyvSQB9saoC7C6C0tvn2DcJOuNog=="],
|
||||||
|
|
||||||
"@opentui/core-linux-arm64-musl": ["@opentui/core-linux-arm64-musl@0.3.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-sJYUzYcSOb5PCXRlhwsse/fdsMiVomNvIwq/2TDhAANef+YPO3Br+OH9kQRbuj0bjVDmUS36SGYWSTFu2lUO+A=="],
|
"@opentui/core-linux-arm64-musl": ["@opentui/core-linux-arm64-musl@0.4.3", "", { "os": "linux", "cpu": "arm64" }, "sha512-8p8g8/AEq/xFGpQ7XcIFKcAqjc0QwsZcv+Ll9RbCDpUA56FGH6jfLDir0KYTNTgYXJTIrBIENI9K46VuxMUMQA=="],
|
||||||
|
|
||||||
"@opentui/core-linux-x64": ["@opentui/core-linux-x64@0.3.4", "", { "os": "linux", "cpu": "x64" }, "sha512-btYIQeNdPbN4JCrCjVB/RwMGrnRY7qWB2piNEfALSByuULKNjPKQ33PYIj38Yd01zCvCV7FotIeXEGSHx3tgCA=="],
|
"@opentui/core-linux-x64": ["@opentui/core-linux-x64@0.4.3", "", { "os": "linux", "cpu": "x64" }, "sha512-dXpJitiZdYE3hq2Pvx6e9I0uPQSOcnaLLp1pDgWAHv+3kvKSHEX//9Yr/pV/Ua6qqT7p+2D/K4vXNap/NKVo2w=="],
|
||||||
|
|
||||||
"@opentui/core-linux-x64-musl": ["@opentui/core-linux-x64-musl@0.3.4", "", { "os": "linux", "cpu": "x64" }, "sha512-fhmUey4oJJ2+N62xlIgAPxAl36Fa7wYffqDOT4QLpm0jfyD5xzo+wL/hr2zUqaEI439R8Iq6jHNxf/Nsx1WuuQ=="],
|
"@opentui/core-linux-x64-musl": ["@opentui/core-linux-x64-musl@0.4.3", "", { "os": "linux", "cpu": "x64" }, "sha512-/QiFpCrpU2O7vy8QYmLIQYbvAtKDgmqcVjR7dGtqSzkiQk3ktNJoo5RozG7ueXnjung1Wp0nKldKxo2Csg/OrA=="],
|
||||||
|
|
||||||
"@opentui/core-win32-arm64": ["@opentui/core-win32-arm64@0.3.4", "", { "os": "win32", "cpu": "arm64" }, "sha512-sh432vPU+eLp8eA4I0KWKKn7D0VHbk01YTg6mA9/ihCNYHntc6LZ8/sLvsPv8CvKscMotfIkh3M5YhdS36BuXw=="],
|
"@opentui/core-win32-arm64": ["@opentui/core-win32-arm64@0.4.3", "", { "os": "win32", "cpu": "arm64" }, "sha512-Mx2zuOjrhm/z2SDS6RExIyjP/SnN/8QhhagxURUw0jQi/NssGSeAllu1cBAFFnhobJL5QLTE4FU4CRhUK9svgg=="],
|
||||||
|
|
||||||
"@opentui/core-win32-x64": ["@opentui/core-win32-x64@0.3.4", "", { "os": "win32", "cpu": "x64" }, "sha512-dw8FcjUZaLAjw25P3/7BarobCh/QOHn3srYaWYQdysoqyvSlPkQumpI8kV/KgpJtdITU1GW02MQC4EeLIFFalA=="],
|
"@opentui/core-win32-x64": ["@opentui/core-win32-x64@0.4.3", "", { "os": "win32", "cpu": "x64" }, "sha512-NuoqvWKGXaYnmlqvu7Gg2lLI6yVMnS9OfWBvxp+7Q+McSgHFSTQmYBXaPpvQ8HikpQXE1nCeMPtuSG4PdZHe2w=="],
|
||||||
|
|
||||||
"@opentui/keymap": ["@opentui/keymap@0.3.4", "", { "dependencies": { "@opentui/core": "0.3.4" }, "peerDependencies": { "@opentui/react": "0.3.4", "@opentui/solid": "0.3.4", "react": ">=19.2.0", "solid-js": "1.9.12" }, "optionalPeers": ["@opentui/react", "@opentui/solid", "react", "solid-js"] }, "sha512-8fo6BZWQgCjANfbKkzPo0ghAzS1E7TlHjDDS+SUhrX01qEUO1clFTRssKluHbXd2UJY1Ehle01TV5bFmY78f8w=="],
|
"@opentui/keymap": ["@opentui/keymap@0.4.3", "", { "dependencies": { "@opentui/core": "0.4.3" }, "peerDependencies": { "@opentui/react": "0.4.3", "@opentui/solid": "0.4.3", "react": ">=19.2.0", "solid-js": "1.9.12" }, "optionalPeers": ["@opentui/react", "@opentui/solid", "react", "solid-js"] }, "sha512-sinX0pyQBRrEvo89PSSUbSUDIYpL3xWo81VEfec58VFoVRB5FG48/deAtvRTQfJ8w1kgbzN8hzdOXdSm61zBmw=="],
|
||||||
|
|
||||||
"@opentui/solid": ["@opentui/solid@0.3.4", "", { "dependencies": { "@babel/core": "7.28.0", "@babel/preset-typescript": "7.27.1", "@opentui/core": "0.3.4", "babel-plugin-module-resolver": "5.0.2", "babel-preset-solid": "1.9.12", "entities": "7.0.1", "s-js": "^0.4.9" }, "peerDependencies": { "solid-js": "1.9.12" } }, "sha512-gin1VnsVBahX0nrU3mpgh5U1qvyJBIZu4NE5mc0YnObWOEf9HVNxKY4/BpUvQPh91kT6zeOzTBvAvYK4R7g9MQ=="],
|
"@opentui/solid": ["@opentui/solid@0.4.3", "", { "dependencies": { "@babel/core": "7.28.0", "@babel/preset-typescript": "7.27.1", "@opentui/core": "0.4.3", "babel-plugin-module-resolver": "5.0.2", "babel-preset-solid": "1.9.12", "entities": "7.0.1", "s-js": "^0.4.9" }, "peerDependencies": { "solid-js": "1.9.12" } }, "sha512-RcV0+S8HMdXOASyr7HmJUBuTUIaFPzAxMDa44VftS5C2JUgrmAuWo0Njv1q3TWRB1owjHnyKhEfWGKq7A82wxw=="],
|
||||||
|
|
||||||
"@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=="],
|
||||||
|
|
||||||
@@ -2790,6 +2796,18 @@
|
|||||||
|
|
||||||
"@tufjs/models": ["@tufjs/models@4.1.0", "", { "dependencies": { "@tufjs/canonical-json": "2.0.0", "minimatch": "^10.1.1" } }, "sha512-Y8cK9aggNRsqJVaKUlEYs4s7CvQ1b1ta2DVPyAimb0I2qhzjNk+A+mxvll/klL0RlfuIUei8BF7YWiua4kQqww=="],
|
"@tufjs/models": ["@tufjs/models@4.1.0", "", { "dependencies": { "@tufjs/canonical-json": "2.0.0", "minimatch": "^10.1.1" } }, "sha512-Y8cK9aggNRsqJVaKUlEYs4s7CvQ1b1ta2DVPyAimb0I2qhzjNk+A+mxvll/klL0RlfuIUei8BF7YWiua4kQqww=="],
|
||||||
|
|
||||||
|
"@turbo/darwin-64": ["@turbo/darwin-64@2.10.2", "", { "os": "darwin", "cpu": "x64" }, "sha512-wBM3ObqOWnKUDmg7QfUFDkDHPFUAJmrYlYqmEM8jMPAPA/I6wRJIbWimeQUqhOiQ8xPKhzyWM+xaiUP0wz8FEQ=="],
|
||||||
|
|
||||||
|
"@turbo/darwin-arm64": ["@turbo/darwin-arm64@2.10.2", "", { "os": "darwin", "cpu": "arm64" }, "sha512-/Cq0joWnuMjDPfhjbFP4sv+C/7gkQ415zlaO4XUzD5EZxbtrKgXKvuuydMvogG8GeUnN1aDltW71RlmEfpjbyw=="],
|
||||||
|
|
||||||
|
"@turbo/linux-64": ["@turbo/linux-64@2.10.2", "", { "os": "linux", "cpu": "x64" }, "sha512-mMsf5IIhiKuceEXNstd25IbadjBXZ0amxzFOqliEzJX6HyeeHdBQPVSY583PWqYDyqM/FB8d5ZjkthfBSeuH3Q=="],
|
||||||
|
|
||||||
|
"@turbo/linux-arm64": ["@turbo/linux-arm64@2.10.2", "", { "os": "linux", "cpu": "arm64" }, "sha512-Wcng1i2kaKmXutmwxT9MUoYZvdaIekXAdlGr4+0TpgbhGLw7nDuEcRBFrxb5BbRoX1d1q8SpdRxLc45TvDZIdQ=="],
|
||||||
|
|
||||||
|
"@turbo/windows-64": ["@turbo/windows-64@2.10.2", "", { "os": "win32", "cpu": "x64" }, "sha512-SsNhM7Ho7EpAdwtrJKBOic9Hso23vu6Dp0gAfLOvUFjPzurr/sGQlXZEvr6z89ne4RDOypTwz5CBDrixpMKtXw=="],
|
||||||
|
|
||||||
|
"@turbo/windows-arm64": ["@turbo/windows-arm64@2.10.2", "", { "os": "win32", "cpu": "arm64" }, "sha512-Gf+S7ICAdimT/n02bOuVWKvhHnct/HYjZg3oBNIz5hZ9ZyWHbQim9J3P5Qip8WpX0ksxF7eaBVziJCuLnjhqDg=="],
|
||||||
|
|
||||||
"@tybys/wasm-util": ["@tybys/wasm-util@0.10.2", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-RoBvJ2X0wuKlWFIjrwffGw1IqZHKQqzIchKaadZZfnNpsAYp2mM0h36JtPCjNDAHGgYez/15uMBpfGwchhiMgg=="],
|
"@tybys/wasm-util": ["@tybys/wasm-util@0.10.2", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-RoBvJ2X0wuKlWFIjrwffGw1IqZHKQqzIchKaadZZfnNpsAYp2mM0h36JtPCjNDAHGgYez/15uMBpfGwchhiMgg=="],
|
||||||
|
|
||||||
"@types/aria-query": ["@types/aria-query@5.0.4", "", {}, "sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw=="],
|
"@types/aria-query": ["@types/aria-query@5.0.4", "", {}, "sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw=="],
|
||||||
@@ -3230,7 +3248,7 @@
|
|||||||
|
|
||||||
"builder-util-runtime": ["builder-util-runtime@9.7.0", "", { "dependencies": { "debug": "^4.3.4", "sax": "^1.2.4" } }, "sha512-g/kR520giAFYkSXTzcmF3kqQq7wi8F6N6SzeDgZrqTBN+VHdmgWOyTdD1yD7AATDId/yXLvuP34CxW46/BwCdw=="],
|
"builder-util-runtime": ["builder-util-runtime@9.7.0", "", { "dependencies": { "debug": "^4.3.4", "sax": "^1.2.4" } }, "sha512-g/kR520giAFYkSXTzcmF3kqQq7wi8F6N6SzeDgZrqTBN+VHdmgWOyTdD1yD7AATDId/yXLvuP34CxW46/BwCdw=="],
|
||||||
|
|
||||||
"bun-ffi-structs": ["bun-ffi-structs@0.2.2", "", { "peerDependencies": { "typescript": "^5" } }, "sha512-N/ZWtyN0piZlrXQT7TO0V+q952orYqkfhXRXM1Hcbb+R3QSiBH4vLnib187Mrs1H7pWIYECAmPeapGYDOMCl+w=="],
|
"bun-ffi-structs": ["bun-ffi-structs@0.2.4", "", { "peerDependencies": { "typescript": "^5" } }, "sha512-AJzsqoVFs1KBbJbWHIYrVZLDC3NhTqqh25awRXqzoLzmBAKr5oqk6+CwuYHAekKx+VBCYVohBoKuRq40dV+TYg=="],
|
||||||
|
|
||||||
"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=="],
|
||||||
|
|
||||||
@@ -5320,19 +5338,7 @@
|
|||||||
|
|
||||||
"tunnel": ["tunnel@0.0.6", "", {}, "sha512-1h/Lnq9yajKY2PEbBadPXj3VxsDDu844OnaAo52UVmIzIvwwtBPIuNvkjuzBlTWpfJyUbG3ez0KSBibQkj4ojg=="],
|
"tunnel": ["tunnel@0.0.6", "", {}, "sha512-1h/Lnq9yajKY2PEbBadPXj3VxsDDu844OnaAo52UVmIzIvwwtBPIuNvkjuzBlTWpfJyUbG3ez0KSBibQkj4ojg=="],
|
||||||
|
|
||||||
"turbo": ["turbo@2.8.13", "", { "optionalDependencies": { "turbo-darwin-64": "2.8.13", "turbo-darwin-arm64": "2.8.13", "turbo-linux-64": "2.8.13", "turbo-linux-arm64": "2.8.13", "turbo-windows-64": "2.8.13", "turbo-windows-arm64": "2.8.13" }, "bin": { "turbo": "bin/turbo" } }, "sha512-nyM99hwFB9/DHaFyKEqatdayGjsMNYsQ/XBNO6MITc7roncZetKb97MpHxWf3uiU+LB9c9HUlU3Jp2Ixei2k1A=="],
|
"turbo": ["turbo@2.10.2", "", { "optionalDependencies": { "@turbo/darwin-64": "2.10.2", "@turbo/darwin-arm64": "2.10.2", "@turbo/linux-64": "2.10.2", "@turbo/linux-arm64": "2.10.2", "@turbo/windows-64": "2.10.2", "@turbo/windows-arm64": "2.10.2" }, "bin": { "turbo": "bin/turbo" } }, "sha512-wTExrNrRjB8qzIcg+ZLm0A3GFNLDsWNwdS/RBXB0FPrBDyzk3i96Yx+TxWZC7a0k1SIreFB8ciUbxjmEqTH8IQ=="],
|
||||||
|
|
||||||
"turbo-darwin-64": ["turbo-darwin-64@2.8.13", "", { "os": "darwin", "cpu": "x64" }, "sha512-PmOvodQNiOj77+Zwoqku70vwVjKzL34RTNxxoARjp5RU5FOj/CGiC6vcDQhNtFPUOWSAaogHF5qIka9TBhX4XA=="],
|
|
||||||
|
|
||||||
"turbo-darwin-arm64": ["turbo-darwin-arm64@2.8.13", "", { "os": "darwin", "cpu": "arm64" }, "sha512-kI+anKcLIM4L8h+NsM7mtAUpElkCOxv5LgiQVQR8BASyDFfc8Efj5kCk3cqxuxOvIqx0sLfCX7atrHQ2kwuNJQ=="],
|
|
||||||
|
|
||||||
"turbo-linux-64": ["turbo-linux-64@2.8.13", "", { "os": "linux", "cpu": "x64" }, "sha512-j29KnQhHyzdzgCykBFeBqUPS4Wj7lWMnZ8CHqytlYDap4Jy70l4RNG46pOL9+lGu6DepK2s1rE86zQfo0IOdPw=="],
|
|
||||||
|
|
||||||
"turbo-linux-arm64": ["turbo-linux-arm64@2.8.13", "", { "os": "linux", "cpu": "arm64" }, "sha512-OEl1YocXGZDRDh28doOUn49QwNe82kXljO1HXApjU0LapkDiGpfl3jkAlPKxEkGDSYWc8MH5Ll8S16Rf5tEBYg=="],
|
|
||||||
|
|
||||||
"turbo-windows-64": ["turbo-windows-64@2.8.13", "", { "os": "win32", "cpu": "x64" }, "sha512-717bVk1+Pn2Jody7OmWludhEirEe0okoj1NpRbSm5kVZz/yNN/jfjbxWC6ilimXMz7xoMT3IDfQFJsFR3PMANA=="],
|
|
||||||
|
|
||||||
"turbo-windows-arm64": ["turbo-windows-arm64@2.8.13", "", { "os": "win32", "cpu": "arm64" }, "sha512-R819HShLIT0Wj6zWVnIsYvSNtRNj1q9VIyaUz0P24SMcLCbQZIm1sV09F4SDbg+KCCumqD2lcaR2UViQ8SnUJA=="],
|
|
||||||
|
|
||||||
"turndown": ["turndown@7.2.0", "", { "dependencies": { "@mixmark-io/domino": "^2.2.0" } }, "sha512-eCZGBN4nNNqM9Owkv9HAtWRYfLA4h909E/WGAWWBpmB275ehNhZyk87/Tpvjbp0jjNl9XwCsbe6bm6CqFsgD+A=="],
|
"turndown": ["turndown@7.2.0", "", { "dependencies": { "@mixmark-io/domino": "^2.2.0" } }, "sha512-eCZGBN4nNNqM9Owkv9HAtWRYfLA4h909E/WGAWWBpmB275ehNhZyk87/Tpvjbp0jjNl9XwCsbe6bm6CqFsgD+A=="],
|
||||||
|
|
||||||
@@ -5590,8 +5596,6 @@
|
|||||||
|
|
||||||
"yoctocolors": ["yoctocolors@2.1.2", "", {}, "sha512-CzhO+pFNo8ajLM2d2IW/R93ipy99LWjtwblvC1RsoSUMZgyLbYFr221TnSNT7GjGdYui6P459mw9JH/g/zW2ug=="],
|
"yoctocolors": ["yoctocolors@2.1.2", "", {}, "sha512-CzhO+pFNo8ajLM2d2IW/R93ipy99LWjtwblvC1RsoSUMZgyLbYFr221TnSNT7GjGdYui6P459mw9JH/g/zW2ug=="],
|
||||||
|
|
||||||
"yoga-layout": ["yoga-layout@3.2.1", "", {}, "sha512-0LPOt3AxKqMdFBZA3HBAt/t/8vIKq7VaQYbuA8WxCgung+p9TVyKRYdpvCb80HcdTN2NkbIKbhNwKUfm3tQywQ=="],
|
|
||||||
|
|
||||||
"youch": ["youch@4.1.0-beta.10", "", { "dependencies": { "@poppinss/colors": "^4.1.5", "@poppinss/dumper": "^0.6.4", "@speed-highlight/core": "^1.2.7", "cookie": "^1.0.2", "youch-core": "^0.3.3" } }, "sha512-rLfVLB4FgQneDr0dv1oddCVZmKjcJ6yX6mS4pU82Mq/Dt9a3cLZQ62pDBL4AUO+uVrCvtWz3ZFUL2HFAFJ/BXQ=="],
|
"youch": ["youch@4.1.0-beta.10", "", { "dependencies": { "@poppinss/colors": "^4.1.5", "@poppinss/dumper": "^0.6.4", "@speed-highlight/core": "^1.2.7", "cookie": "^1.0.2", "youch-core": "^0.3.3" } }, "sha512-rLfVLB4FgQneDr0dv1oddCVZmKjcJ6yX6mS4pU82Mq/Dt9a3cLZQ62pDBL4AUO+uVrCvtWz3ZFUL2HFAFJ/BXQ=="],
|
||||||
|
|
||||||
"youch-core": ["youch-core@0.3.3", "", { "dependencies": { "@poppinss/exception": "^1.2.2", "error-stack-parser-es": "^1.0.5" } }, "sha512-ho7XuGjLaJ2hWHoK8yFnsUGy2Y5uDpqSTq1FkHLK4/oqKtyUU1AFbOOxY4IpC9f0fTLjwYbslUz0Po5BpD1wrA=="],
|
"youch-core": ["youch-core@0.3.3", "", { "dependencies": { "@poppinss/exception": "^1.2.2", "error-stack-parser-es": "^1.0.5" } }, "sha512-ho7XuGjLaJ2hWHoK8yFnsUGy2Y5uDpqSTq1FkHLK4/oqKtyUU1AFbOOxY4IpC9f0fTLjwYbslUz0Po5BpD1wrA=="],
|
||||||
@@ -5642,7 +5646,11 @@
|
|||||||
|
|
||||||
"@ai-sdk/azure/@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/azure/@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/cerebras/@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/cerebras/@ai-sdk/openai-compatible": ["@ai-sdk/openai-compatible@2.0.54", "", { "dependencies": { "@ai-sdk/provider": "3.0.12", "@ai-sdk/provider-utils": "4.0.33" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-OyXt0zK8y2/ZIyWlbxTv2r1M7AK227S+Gl4BYOEF42q0wz1n5m4fwR8L4Fy/MQ4Ho6xje47MPsFcRdIqIyP6Rw=="],
|
||||||
|
|
||||||
|
"@ai-sdk/cerebras/@ai-sdk/provider": ["@ai-sdk/provider@3.0.12", "", { "dependencies": { "json-schema": "^0.4.0" } }, "sha512-sj9DWTJ2Ze0WR9qsiOPqoqzNx3OxL6iMxHImbhvoe9qOspekbzxNDMiJ4TIGfYHYh9w4OmBjz3prvqhzTi96+Q=="],
|
||||||
|
|
||||||
|
"@ai-sdk/cerebras/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@4.0.33", "", { "dependencies": { "@ai-sdk/provider": "3.0.12", "@standard-schema/spec": "^1.1.0", "eventsource-parser": "^3.0.8" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-nJ0bAfegMAIJtrzMJtbzer1cS3nb7c7DsyU1S4nrPm7ZU0Mn6SBBZv5IGZZGTbpWTJwqKTSPeZJTXalbAxt1BA=="],
|
||||||
|
|
||||||
"@ai-sdk/cohere/@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/cohere/@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=="],
|
||||||
|
|
||||||
@@ -5680,7 +5688,9 @@
|
|||||||
|
|
||||||
"@ai-sdk/groq/@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/groq/@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/mistral/@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/mistral/@ai-sdk/provider": ["@ai-sdk/provider@3.0.10", "", { "dependencies": { "json-schema": "^0.4.0" } }, "sha512-Q3BZ27qfpYqnCYGvE3vt+Qi6LGOF9R5Nmzn+9JoM1lCRsD9mYaIhfJLkSunN48nfGXJ6n+XNV0J/XVpqGQl7Dw=="],
|
||||||
|
|
||||||
|
"@ai-sdk/mistral/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@4.0.26", "", { "dependencies": { "@ai-sdk/provider": "3.0.10", "@standard-schema/spec": "^1.1.0", "eventsource-parser": "^3.0.8" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-CsKNLKsOpvPujRlIYvoz+Ybw+kGn7J4/fIZa/58+R7iWLLfwn6ifE2G6Yq8K9XvH/I/3bzaDAJ3NhRwEMsLBKQ=="],
|
||||||
|
|
||||||
"@ai-sdk/openai/@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/openai/@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=="],
|
||||||
|
|
||||||
@@ -5694,7 +5704,11 @@
|
|||||||
|
|
||||||
"@ai-sdk/vercel/@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/vercel/@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/xai/@ai-sdk/openai-compatible": ["@ai-sdk/openai-compatible@2.0.41", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.23" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-kNAGINk71AlOXx10Dq/PXw4t/9XjdK8uxfpVElRwtSFMdeSiLVt58p9TPx4/FJD+hxZuVhvxYj9r42osxWq79g=="],
|
"@ai-sdk/xai/@ai-sdk/openai-compatible": ["@ai-sdk/openai-compatible@2.0.56", "", { "dependencies": { "@ai-sdk/provider": "3.0.13", "@ai-sdk/provider-utils": "4.0.35" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-cQrN6OUn/jvsY3OdsU6Wn+ss7vp1iwIcakZKSlSRMnYqShBfyT7Qht+eqmgxs7w9ttrw6FAG6o11AiBs+iEsTA=="],
|
||||||
|
|
||||||
|
"@ai-sdk/xai/@ai-sdk/provider": ["@ai-sdk/provider@3.0.13", "", { "dependencies": { "json-schema": "^0.4.0" } }, "sha512-ZPtVYt5QIJzOta1kdUiDuCx4HhFkvNPv/rvmZ2b1iXwybYjJsCnNYR4PAw4kW7rgVfDARvHXcU64efWuqNp6bw=="],
|
||||||
|
|
||||||
|
"@ai-sdk/xai/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@4.0.35", "", { "dependencies": { "@ai-sdk/provider": "3.0.13", "@standard-schema/spec": "^1.1.0", "eventsource-parser": "^3.0.8" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-bjYld/2KGPLt78kpqbya+fD4LYS7BqVQJyUjE3qAHrYB0FR2Q90BaWEVIBZaguTWXf/A8L6uG1zO1v9TxVlGWg=="],
|
||||||
|
|
||||||
"@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=="],
|
||||||
|
|
||||||
@@ -6142,8 +6156,14 @@
|
|||||||
|
|
||||||
"ai-gateway-provider/@ai-sdk/anthropic": ["@ai-sdk/anthropic@3.0.78", "", { "dependencies": { "@ai-sdk/provider": "3.0.10", "@ai-sdk/provider-utils": "4.0.27" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-0OY12G20cUt6iU6htpEA1491Oz++NVxZxlmWGX4B7rSbeZ5pnDmOu6YtW9BKzdZlNx5Gn23i6WMxyZFoMKNcgA=="],
|
"ai-gateway-provider/@ai-sdk/anthropic": ["@ai-sdk/anthropic@3.0.78", "", { "dependencies": { "@ai-sdk/provider": "3.0.10", "@ai-sdk/provider-utils": "4.0.27" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-0OY12G20cUt6iU6htpEA1491Oz++NVxZxlmWGX4B7rSbeZ5pnDmOu6YtW9BKzdZlNx5Gn23i6WMxyZFoMKNcgA=="],
|
||||||
|
|
||||||
|
"ai-gateway-provider/@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-gateway-provider/@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-gateway-provider/@ai-sdk/openai": ["@ai-sdk/openai@3.0.53", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.23" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-Wld+Rbc05KaUn08uBt06eEuwcgalcIFtIl32Yp+GxuZXUQwOb6YeAuq+C6da4ch6BurFoqEaLemJVwjBb7x+PQ=="],
|
"ai-gateway-provider/@ai-sdk/openai": ["@ai-sdk/openai@3.0.53", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.23" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-Wld+Rbc05KaUn08uBt06eEuwcgalcIFtIl32Yp+GxuZXUQwOb6YeAuq+C6da4ch6BurFoqEaLemJVwjBb7x+PQ=="],
|
||||||
|
|
||||||
|
"ai-gateway-provider/@ai-sdk/xai": ["@ai-sdk/xai@3.0.82", "", { "dependencies": { "@ai-sdk/openai-compatible": "2.0.41", "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.23" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-A0VFMufnVf4wODcT3SPQUUzvYXiIO1VhFuXj9r6z/vP4rlo+QRDPw3WSTchcz93ROQWSfBE3I6Szqz342OHi5w=="],
|
||||||
|
|
||||||
"ai-gateway-provider/@openrouter/ai-sdk-provider": ["@openrouter/ai-sdk-provider@2.8.1", "", { "peerDependencies": { "ai": "^6.0.0", "zod": "^3.25.0 || ^4.0.0" } }, "sha512-Y6j3yivgoEUf/kutD/k5GX/mzZfioRFoSx0gbQ+mIOzMaH/vJv1rCkztiuvlLw5xRYQil7oxHUZvmSfXqOx1NQ=="],
|
"ai-gateway-provider/@openrouter/ai-sdk-provider": ["@openrouter/ai-sdk-provider@2.8.1", "", { "peerDependencies": { "ai": "^6.0.0", "zod": "^3.25.0 || ^4.0.0" } }, "sha512-Y6j3yivgoEUf/kutD/k5GX/mzZfioRFoSx0gbQ+mIOzMaH/vJv1rCkztiuvlLw5xRYQil7oxHUZvmSfXqOx1NQ=="],
|
||||||
|
|
||||||
"ajv-keywords/ajv": ["ajv@6.15.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-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw=="],
|
"ajv-keywords/ajv": ["ajv@6.15.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-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw=="],
|
||||||
@@ -6364,8 +6384,6 @@
|
|||||||
|
|
||||||
"nypm/tinyexec": ["tinyexec@1.2.4", "", {}, "sha512-SHf/r48b7vOrjve9PxJo3MN5v5yuyjHvdUcrQffT3WXMUfnGmHDVbC4k3sHJaJTgZCwpUplIaAo5ANtMyp3YHg=="],
|
"nypm/tinyexec": ["tinyexec@1.2.4", "", {}, "sha512-SHf/r48b7vOrjve9PxJo3MN5v5yuyjHvdUcrQffT3WXMUfnGmHDVbC4k3sHJaJTgZCwpUplIaAo5ANtMyp3YHg=="],
|
||||||
|
|
||||||
"opencode/@ai-sdk/cerebras": ["@ai-sdk/cerebras@2.0.60", "", { "dependencies": { "@ai-sdk/openai-compatible": "2.0.54", "@ai-sdk/provider": "3.0.12", "@ai-sdk/provider-utils": "4.0.33" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-Rnok3cThg6awBwaDSyiZpgRpbV7pqxGYrA89LODCo5cuEHeP2h0AM0lLHP7zIkclAdXfOm4wldKi/S2T/DGCOw=="],
|
|
||||||
|
|
||||||
"opencode/@ai-sdk/openai": ["@ai-sdk/openai@3.0.53", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.23" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-Wld+Rbc05KaUn08uBt06eEuwcgalcIFtIl32Yp+GxuZXUQwOb6YeAuq+C6da4ch6BurFoqEaLemJVwjBb7x+PQ=="],
|
"opencode/@ai-sdk/openai": ["@ai-sdk/openai@3.0.53", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.23" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-Wld+Rbc05KaUn08uBt06eEuwcgalcIFtIl32Yp+GxuZXUQwOb6YeAuq+C6da4ch6BurFoqEaLemJVwjBb7x+PQ=="],
|
||||||
|
|
||||||
"opencode/@ai-sdk/openai-compatible": ["@ai-sdk/openai-compatible@2.0.41", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.23" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-kNAGINk71AlOXx10Dq/PXw4t/9XjdK8uxfpVElRwtSFMdeSiLVt58p9TPx4/FJD+hxZuVhvxYj9r42osxWq79g=="],
|
"opencode/@ai-sdk/openai-compatible": ["@ai-sdk/openai-compatible@2.0.41", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.23" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-kNAGINk71AlOXx10Dq/PXw4t/9XjdK8uxfpVElRwtSFMdeSiLVt58p9TPx4/FJD+hxZuVhvxYj9r42osxWq79g=="],
|
||||||
@@ -6578,6 +6596,8 @@
|
|||||||
|
|
||||||
"@ai-sdk/vercel/@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=="],
|
||||||
@@ -6940,6 +6960,12 @@
|
|||||||
|
|
||||||
"ai-gateway-provider/@ai-sdk/anthropic/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@4.0.27", "", { "dependencies": { "@ai-sdk/provider": "3.0.10", "@standard-schema/spec": "^1.1.0", "eventsource-parser": "^3.0.8" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-ubkAJ+xODouwtmN1tYlvTPphH1hPOBfZaEQe8U7skGvFAnIRs9PPpsq57bC2+Ky/MB4yzhd6YOsxTAx9sGpazw=="],
|
"ai-gateway-provider/@ai-sdk/anthropic/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@4.0.27", "", { "dependencies": { "@ai-sdk/provider": "3.0.10", "@standard-schema/spec": "^1.1.0", "eventsource-parser": "^3.0.8" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-ubkAJ+xODouwtmN1tYlvTPphH1hPOBfZaEQe8U7skGvFAnIRs9PPpsq57bC2+Ky/MB4yzhd6YOsxTAx9sGpazw=="],
|
||||||
|
|
||||||
|
"ai-gateway-provider/@ai-sdk/cerebras/@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-gateway-provider/@ai-sdk/mistral/@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-gateway-provider/@ai-sdk/xai/@ai-sdk/openai-compatible": ["@ai-sdk/openai-compatible@2.0.41", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.23" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-kNAGINk71AlOXx10Dq/PXw4t/9XjdK8uxfpVElRwtSFMdeSiLVt58p9TPx4/FJD+hxZuVhvxYj9r42osxWq79g=="],
|
||||||
|
|
||||||
"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=="],
|
||||||
@@ -7084,12 +7110,6 @@
|
|||||||
|
|
||||||
"opencode-gitlab-auth/open/wsl-utils": ["wsl-utils@0.1.0", "", { "dependencies": { "is-wsl": "^3.1.0" } }, "sha512-h3Fbisa2nKGPxCpm89Hk33lBLsnaGBvctQopaBSOW/uIs6FTe1ATyAnKFJrzVs9vpGdsTe73WF3V4lIsk4Gacw=="],
|
"opencode-gitlab-auth/open/wsl-utils": ["wsl-utils@0.1.0", "", { "dependencies": { "is-wsl": "^3.1.0" } }, "sha512-h3Fbisa2nKGPxCpm89Hk33lBLsnaGBvctQopaBSOW/uIs6FTe1ATyAnKFJrzVs9vpGdsTe73WF3V4lIsk4Gacw=="],
|
||||||
|
|
||||||
"opencode/@ai-sdk/cerebras/@ai-sdk/openai-compatible": ["@ai-sdk/openai-compatible@2.0.54", "", { "dependencies": { "@ai-sdk/provider": "3.0.12", "@ai-sdk/provider-utils": "4.0.33" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-OyXt0zK8y2/ZIyWlbxTv2r1M7AK227S+Gl4BYOEF42q0wz1n5m4fwR8L4Fy/MQ4Ho6xje47MPsFcRdIqIyP6Rw=="],
|
|
||||||
|
|
||||||
"opencode/@ai-sdk/cerebras/@ai-sdk/provider": ["@ai-sdk/provider@3.0.12", "", { "dependencies": { "json-schema": "^0.4.0" } }, "sha512-sj9DWTJ2Ze0WR9qsiOPqoqzNx3OxL6iMxHImbhvoe9qOspekbzxNDMiJ4TIGfYHYh9w4OmBjz3prvqhzTi96+Q=="],
|
|
||||||
|
|
||||||
"opencode/@ai-sdk/cerebras/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@4.0.33", "", { "dependencies": { "@ai-sdk/provider": "3.0.12", "@standard-schema/spec": "^1.1.0", "eventsource-parser": "^3.0.8" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-nJ0bAfegMAIJtrzMJtbzer1cS3nb7c7DsyU1S4nrPm7ZU0Mn6SBBZv5IGZZGTbpWTJwqKTSPeZJTXalbAxt1BA=="],
|
|
||||||
|
|
||||||
"p-locate/p-limit/yocto-queue": ["yocto-queue@0.1.0", "", {}, "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q=="],
|
"p-locate/p-limit/yocto-queue": ["yocto-queue@0.1.0", "", {}, "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q=="],
|
||||||
|
|
||||||
"pkg-dir/find-up/locate-path": ["locate-path@5.0.0", "", { "dependencies": { "p-locate": "^4.1.0" } }, "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g=="],
|
"pkg-dir/find-up/locate-path": ["locate-path@5.0.0", "", { "dependencies": { "p-locate": "^4.1.0" } }, "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g=="],
|
||||||
@@ -7354,6 +7374,10 @@
|
|||||||
|
|
||||||
"ai-gateway-provider/@ai-sdk/anthropic/@ai-sdk/provider-utils/@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="],
|
"ai-gateway-provider/@ai-sdk/anthropic/@ai-sdk/provider-utils/@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="],
|
||||||
|
|
||||||
|
"ai-gateway-provider/@ai-sdk/cerebras/@ai-sdk/provider-utils/@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="],
|
||||||
|
|
||||||
|
"ai-gateway-provider/@ai-sdk/mistral/@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=="],
|
||||||
@@ -7422,8 +7446,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/cerebras/@ai-sdk/provider-utils/@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="],
|
|
||||||
|
|
||||||
"pkg-dir/find-up/locate-path/p-locate": ["p-locate@4.1.0", "", { "dependencies": { "p-limit": "^2.2.0" } }, "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A=="],
|
"pkg-dir/find-up/locate-path/p-locate": ["p-locate@4.1.0", "", { "dependencies": { "p-limit": "^2.2.0" } }, "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A=="],
|
||||||
|
|
||||||
"pkg-up/find-up/locate-path/p-locate": ["p-locate@3.0.0", "", { "dependencies": { "p-limit": "^2.0.0" } }, "sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ=="],
|
"pkg-up/find-up/locate-path/p-locate": ["p-locate@3.0.0", "", { "dependencies": { "p-limit": "^2.0.0" } }, "sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ=="],
|
||||||
|
|||||||
+4
-4
@@ -1,8 +1,8 @@
|
|||||||
{
|
{
|
||||||
"nodeModules": {
|
"nodeModules": {
|
||||||
"x86_64-linux": "sha256-/Tyscbq9D9pDm18icK0CBsuZPduQmMKuauPnYbae+X8=",
|
"x86_64-linux": "sha256-K0V7FyvgujbeAf0Ip4SCJxcy0GpSt/H+iEwTVhvwBRQ=",
|
||||||
"aarch64-linux": "sha256-foak7M2pny+8U5rkYpIl8tjYk5vgYkVtcZsnJN3bkV4=",
|
"aarch64-linux": "sha256-lz2GhziKY9zL4vXb2/CmlXKrmVYPF5hPFD1ZpfDOqgM=",
|
||||||
"aarch64-darwin": "sha256-JxiN/6nF+jvLIfX/B8HEZZmp0WujTEjY8pvMva1pQy4=",
|
"aarch64-darwin": "sha256-vfxsqozBq40tc1v6w8xvl1HIAif8gRn6oIYrLt6fZ1c=",
|
||||||
"x86_64-darwin": "sha256-6Lt7RgGSqt0z3Lc9XbfHX/pPPlLjMnrGJUD8eqyexnk="
|
"x86_64-darwin": "sha256-X7GAwO+8A6vwqObIHFtloDDJZO8ZUTqmplFDg1zfots="
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+12
-6
@@ -19,6 +19,7 @@
|
|||||||
"prepare": "husky",
|
"prepare": "husky",
|
||||||
"random": "echo 'Random script'",
|
"random": "echo 'Random script'",
|
||||||
"sso": "aws sso login --sso-session=opencode --no-browser",
|
"sso": "aws sso login --sso-session=opencode --no-browser",
|
||||||
|
"translate:app": "bun run script/translate-app.ts",
|
||||||
"test": "echo 'do not run tests from root' && exit 1"
|
"test": "echo 'do not run tests from root' && exit 1"
|
||||||
},
|
},
|
||||||
"workspaces": {
|
"workspaces": {
|
||||||
@@ -39,9 +40,9 @@
|
|||||||
"@octokit/rest": "22.0.0",
|
"@octokit/rest": "22.0.0",
|
||||||
"@hono/standard-validator": "0.2.0",
|
"@hono/standard-validator": "0.2.0",
|
||||||
"@hono/zod-validator": "0.4.2",
|
"@hono/zod-validator": "0.4.2",
|
||||||
"@opentui/core": "0.3.4",
|
"@opentui/core": "0.4.3",
|
||||||
"@opentui/keymap": "0.3.4",
|
"@opentui/keymap": "0.4.3",
|
||||||
"@opentui/solid": "0.3.4",
|
"@opentui/solid": "0.4.3",
|
||||||
"@tanstack/solid-virtual": "3.13.28",
|
"@tanstack/solid-virtual": "3.13.28",
|
||||||
"@shikijs/stream": "4.2.0",
|
"@shikijs/stream": "4.2.0",
|
||||||
"ulid": "3.0.1",
|
"ulid": "3.0.1",
|
||||||
@@ -104,7 +105,7 @@
|
|||||||
"prettier": "3.6.2",
|
"prettier": "3.6.2",
|
||||||
"semver": "^7.6.0",
|
"semver": "^7.6.0",
|
||||||
"sst": "catalog:",
|
"sst": "catalog:",
|
||||||
"turbo": "2.8.13"
|
"turbo": "2.10.2"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@aws-sdk/client-s3": "3.933.0",
|
"@aws-sdk/client-s3": "3.933.0",
|
||||||
@@ -146,7 +147,7 @@
|
|||||||
"@silvia-odwyer/photon-node@0.3.4": "patches/@silvia-odwyer%2Fphoton-node@0.3.4.patch",
|
"@silvia-odwyer/photon-node@0.3.4": "patches/@silvia-odwyer%2Fphoton-node@0.3.4.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",
|
||||||
"solid-js@1.9.10": "patches/solid-js@1.9.10.patch",
|
"solid-js@1.9.10": "patches/solid-js@1.9.10.patch",
|
||||||
"@ai-sdk/xai@3.0.82": "patches/@ai-sdk%2Fxai@3.0.82.patch",
|
"@ai-sdk/xai@3.0.102": "patches/@ai-sdk%2Fxai@3.0.102.patch",
|
||||||
"gcp-metadata@8.1.2": "patches/gcp-metadata@8.1.2.patch",
|
"gcp-metadata@8.1.2": "patches/gcp-metadata@8.1.2.patch",
|
||||||
"pacote@21.5.0": "patches/pacote@21.5.0.patch",
|
"pacote@21.5.0": "patches/pacote@21.5.0.patch",
|
||||||
"@ai-sdk/google@3.0.73": "patches/@ai-sdk%2Fgoogle@3.0.73.patch",
|
"@ai-sdk/google@3.0.73": "patches/@ai-sdk%2Fgoogle@3.0.73.patch",
|
||||||
@@ -154,6 +155,11 @@
|
|||||||
"@pierre/trees@1.0.0-beta.4": "patches/@pierre%2Ftrees@1.0.0-beta.4.patch",
|
"@pierre/trees@1.0.0-beta.4": "patches/@pierre%2Ftrees@1.0.0-beta.4.patch",
|
||||||
"@modelcontextprotocol/sdk@1.29.0": "patches/@modelcontextprotocol%2Fsdk@1.29.0.patch",
|
"@modelcontextprotocol/sdk@1.29.0": "patches/@modelcontextprotocol%2Fsdk@1.29.0.patch",
|
||||||
"@tanstack/virtual-core@3.17.0": "patches/@tanstack%2Fvirtual-core@3.17.0.patch",
|
"@tanstack/virtual-core@3.17.0": "patches/@tanstack%2Fvirtual-core@3.17.0.patch",
|
||||||
"effect@4.0.0-beta.83": "patches/effect@4.0.0-beta.83.patch"
|
"effect@4.0.0-beta.83": "patches/effect@4.0.0-beta.83.patch",
|
||||||
|
"@ai-sdk/mistral@3.0.34": "patches/@ai-sdk%2Fmistral@3.0.34.patch",
|
||||||
|
"ai-gateway-provider@3.1.2": "patches/ai-gateway-provider@3.1.2.patch",
|
||||||
|
"@ai-sdk/cohere@3.0.27": "patches/@ai-sdk%2Fcohere@3.0.27.patch",
|
||||||
|
"@ai-sdk/groq@3.0.31": "patches/@ai-sdk%2Fgroq@3.0.31.patch",
|
||||||
|
"@ai-sdk/openai-compatible@2.0.37": "patches/@ai-sdk%2Fopenai-compatible@2.0.37.patch"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -48,6 +48,40 @@ test("does not reverse visible rows when the user wheels during shell remeasurem
|
|||||||
await reportVisualStability(testInfo, "wheel-during-resize", trace, rowPairPlan(regions, 1))
|
await reportVisualStability(testInfo, "wheel-during-resize", trace, rowPairPlan(regions, 1))
|
||||||
})
|
})
|
||||||
|
|
||||||
|
test("keeps moving upward while drag-selecting above the timeline", async ({ page }) => {
|
||||||
|
await setupTimeline(page, {
|
||||||
|
messages: history(80),
|
||||||
|
viewport: { width: 1400, height: 700 },
|
||||||
|
reducedMotion: true,
|
||||||
|
})
|
||||||
|
const scroller = page.locator(".scroll-view__viewport", { has: page.locator("[data-timeline-row]") })
|
||||||
|
const text = page.getByText("History 79.", { exact: false })
|
||||||
|
await expect(text).toBeVisible()
|
||||||
|
await scroller.evaluate((element) => {
|
||||||
|
element.dataset.selectionLength = "0"
|
||||||
|
document.addEventListener("selectionchange", () => {
|
||||||
|
element.dataset.selectionLength = String(
|
||||||
|
Math.max(Number(element.dataset.selectionLength), window.getSelection()?.toString().length ?? 0),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
const textBox = await text.boundingBox()
|
||||||
|
const scrollBox = await scroller.boundingBox()
|
||||||
|
expect(textBox).not.toBeNull()
|
||||||
|
expect(scrollBox).not.toBeNull()
|
||||||
|
if (!textBox || !scrollBox) return
|
||||||
|
|
||||||
|
await page.mouse.move(textBox.x + textBox.width - 10, textBox.y + textBox.height / 2)
|
||||||
|
await page.mouse.down()
|
||||||
|
await page.mouse.move(textBox.x + 20, scrollBox.y - 120, { steps: 30 })
|
||||||
|
|
||||||
|
await expect.poll(() => scroller.evaluate((element) => Number(element.dataset.selectionLength))).toBeGreaterThan(0)
|
||||||
|
await expect
|
||||||
|
.poll(() => scroller.evaluate((element) => element.scrollHeight - element.clientHeight - element.scrollTop))
|
||||||
|
.toBeGreaterThan(500)
|
||||||
|
await page.mouse.up()
|
||||||
|
})
|
||||||
|
|
||||||
test("does not pull a keyboard-scrolled user during shell remeasurement", async ({ page }, testInfo) => {
|
test("does not pull a keyboard-scrolled user during shell remeasurement", async ({ page }, testInfo) => {
|
||||||
const shellID = "prt_keyboard_01_shell"
|
const shellID = "prt_keyboard_01_shell"
|
||||||
const followingID = "prt_keyboard_02_following"
|
const followingID = "prt_keyboard_02_following"
|
||||||
|
|||||||
@@ -0,0 +1,312 @@
|
|||||||
|
import type { Page } from "@playwright/test"
|
||||||
|
import { benchmark, expect } from "../benchmark"
|
||||||
|
import { setupTimelineBenchmark } from "./session-timeline-benchmark.fixture"
|
||||||
|
|
||||||
|
const changedLinesPerFile = 100
|
||||||
|
const linesPerSide = changedLinesPerFile / 2
|
||||||
|
const fileCounts = [1, 10, 100, 1_000, 10_000]
|
||||||
|
const filesPerDirectory = 100
|
||||||
|
const readyFrames = 3
|
||||||
|
const completionTimeoutMs = Number(process.env.REVIEW_PANE_COMPLETION_TIMEOUT_MS ?? 900_000)
|
||||||
|
|
||||||
|
type ReviewPaneScalingSample = {
|
||||||
|
observedAtMs: number
|
||||||
|
logicalRows: number
|
||||||
|
treeRows: number
|
||||||
|
fileRows: number
|
||||||
|
diffLines: number
|
||||||
|
header: string
|
||||||
|
ready: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
type ReviewPaneScalingProbe = {
|
||||||
|
startedAt?: number
|
||||||
|
firstTreeRowMs?: number
|
||||||
|
logicalTreeReadyMs?: number
|
||||||
|
firstDiffRenderMs?: number
|
||||||
|
stableReadyMs?: number
|
||||||
|
samples: ReviewPaneScalingSample[]
|
||||||
|
frameTimesMs: number[]
|
||||||
|
longTasks: { startTime: number; duration: number }[]
|
||||||
|
stop: () => void
|
||||||
|
}
|
||||||
|
|
||||||
|
benchmark.describe("performance: review pane scaling", () => {
|
||||||
|
for (const fileCount of fileCounts) {
|
||||||
|
const changedLines = fileCount * changedLinesPerFile
|
||||||
|
|
||||||
|
benchmark(
|
||||||
|
`${changedLines} changed lines across ${fileCount} ${fileCount === 1 ? "file" : "files"}`,
|
||||||
|
async ({ page, report }) => {
|
||||||
|
benchmark.setTimeout(1_200_000)
|
||||||
|
await page.emulateMedia({ reducedMotion: "reduce" })
|
||||||
|
|
||||||
|
const patchByteLimit = Number(process.env.REVIEW_PANE_PATCH_BYTE_LIMIT ?? Number.POSITIVE_INFINITY)
|
||||||
|
if (Number.isNaN(patchByteLimit) || patchByteLimit < 0)
|
||||||
|
throw new Error(`Invalid REVIEW_PANE_PATCH_BYTE_LIMIT: ${process.env.REVIEW_PANE_PATCH_BYTE_LIMIT}`)
|
||||||
|
const responseBody = JSON.stringify(createScalingDiffs(fileCount, patchByteLimit))
|
||||||
|
await setupTimelineBenchmark(page, {
|
||||||
|
historyTurns: 0,
|
||||||
|
eventBatch: 1,
|
||||||
|
newLayoutDesigns: true,
|
||||||
|
})
|
||||||
|
await page.route("**/vcs/diff**", (route) =>
|
||||||
|
route.fulfill({
|
||||||
|
status: 200,
|
||||||
|
contentType: "application/json",
|
||||||
|
headers: { "access-control-allow-origin": "*" },
|
||||||
|
body: responseBody,
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
|
const expectedRows = fileCount + 2 + Math.ceil(fileCount / filesPerDirectory)
|
||||||
|
const metrics = await measureReviewPaneLoad(page, {
|
||||||
|
expectedFile: reviewFile(0),
|
||||||
|
expectedRows,
|
||||||
|
})
|
||||||
|
const search = await measureBroadReviewSearch(page, fileCount)
|
||||||
|
|
||||||
|
expect(metrics.logicalRows).toBe(expectedRows)
|
||||||
|
expect(metrics.fileRows).toBeGreaterThan(0)
|
||||||
|
expect(metrics.treeRows).toBeGreaterThan(0)
|
||||||
|
expect(metrics.diffLines).toBeGreaterThan(0)
|
||||||
|
expect(search.logicalRows).toBe(fileCount)
|
||||||
|
expect(search.renderedRows).toBeGreaterThan(0)
|
||||||
|
report(
|
||||||
|
{ ...metrics, search },
|
||||||
|
{
|
||||||
|
fileCount,
|
||||||
|
changedLinesPerFile,
|
||||||
|
changedLines,
|
||||||
|
additions: changedLines / 2,
|
||||||
|
deletions: changedLines / 2,
|
||||||
|
patchLines: changedLines,
|
||||||
|
patchByteLimit: Number.isFinite(patchByteLimit) ? patchByteLimit : null,
|
||||||
|
payloadBytes: new TextEncoder().encode(responseBody).byteLength,
|
||||||
|
expectedRows,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
async function measureBroadReviewSearch(page: Page, expectedRows: number) {
|
||||||
|
const filter = page.getByRole("searchbox", { name: "Filter files" })
|
||||||
|
await filter.evaluate((element) => {
|
||||||
|
element.addEventListener(
|
||||||
|
"input",
|
||||||
|
() => {
|
||||||
|
;(window as Window & { __reviewSearchStartedAt?: number }).__reviewSearchStartedAt = performance.now()
|
||||||
|
},
|
||||||
|
{ once: true, capture: true },
|
||||||
|
)
|
||||||
|
})
|
||||||
|
await filter.fill("file-")
|
||||||
|
|
||||||
|
return page.evaluate((expectedRows) => {
|
||||||
|
const startedAt = (window as Window & { __reviewSearchStartedAt?: number }).__reviewSearchStartedAt!
|
||||||
|
return new Promise<{ stableMs: number; logicalRows: number; renderedRows: number }>((resolve) => {
|
||||||
|
let previous = -1
|
||||||
|
let streak = 0
|
||||||
|
const sample = () => {
|
||||||
|
const tree = document.querySelector<HTMLElement>('#review-panel [data-component="file-tree-v2"]')
|
||||||
|
const rows = [...document.querySelectorAll<HTMLElement>('#review-panel [data-slot="file-tree-v2-row"]')]
|
||||||
|
const logicalRows = Number(tree?.dataset.totalRows ?? rows.length)
|
||||||
|
const ready =
|
||||||
|
logicalRows === expectedRows && rows.length > 0 && rows.every((row) => row.textContent?.includes("file-"))
|
||||||
|
streak = ready && rows.length === previous ? streak + 1 : ready ? 1 : 0
|
||||||
|
previous = rows.length
|
||||||
|
if (streak >= 3) {
|
||||||
|
resolve({ stableMs: performance.now() - startedAt, logicalRows, renderedRows: rows.length })
|
||||||
|
return
|
||||||
|
}
|
||||||
|
requestAnimationFrame(sample)
|
||||||
|
}
|
||||||
|
requestAnimationFrame(sample)
|
||||||
|
})
|
||||||
|
}, expectedRows)
|
||||||
|
}
|
||||||
|
|
||||||
|
function createScalingDiffs(fileCount: number, patchByteLimit: number) {
|
||||||
|
const changes = Array.from({ length: linesPerSide }, (_, index) => {
|
||||||
|
const line = String(index).padStart(3, "0")
|
||||||
|
return `-export const value_${line} = "before"\n+export const value_${line} = "after"`
|
||||||
|
}).join("\n")
|
||||||
|
let patchBytes = 0
|
||||||
|
let capped = false
|
||||||
|
|
||||||
|
return Array.from({ length: fileCount }, (_, index) => {
|
||||||
|
const file = reviewFile(index)
|
||||||
|
const fullPatch = [
|
||||||
|
`diff --git a/${file} b/${file}`,
|
||||||
|
`--- a/${file}`,
|
||||||
|
`+++ b/${file}`,
|
||||||
|
`@@ -1,${linesPerSide} +1,${linesPerSide} @@`,
|
||||||
|
changes,
|
||||||
|
].join("\n")
|
||||||
|
if (index === 0 && fullPatch.length > patchByteLimit)
|
||||||
|
throw new Error(`REVIEW_PANE_PATCH_BYTE_LIMIT must include the active patch (${fullPatch.length} bytes)`)
|
||||||
|
const patch = !capped && patchBytes + fullPatch.length <= patchByteLimit ? fullPatch : emptyReviewPatch(file)
|
||||||
|
if (patch === fullPatch) patchBytes += fullPatch.length
|
||||||
|
else capped = true
|
||||||
|
return {
|
||||||
|
file,
|
||||||
|
patch,
|
||||||
|
additions: linesPerSide,
|
||||||
|
deletions: linesPerSide,
|
||||||
|
status: "modified" as const,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function emptyReviewPatch(file: string) {
|
||||||
|
return [`diff --git a/${file} b/${file}`, `--- a/${file}`, `+++ b/${file}`].join("\n")
|
||||||
|
}
|
||||||
|
|
||||||
|
function reviewFile(index: number) {
|
||||||
|
return `src/review/d${String(Math.floor(index / filesPerDirectory)).padStart(5, "0")}/file-${String(index).padStart(5, "0")}.ts`
|
||||||
|
}
|
||||||
|
|
||||||
|
async function measureReviewPaneLoad(page: Page, input: { expectedFile: string; expectedRows: number }) {
|
||||||
|
const toggle = page.getByRole("button", { name: "Toggle review" })
|
||||||
|
await expect(toggle).toBeVisible()
|
||||||
|
await toggle.evaluate((element) => element.setAttribute("data-review-pane-scaling-toggle", ""))
|
||||||
|
await installReviewPaneScalingProbe(page, input)
|
||||||
|
await toggle.click()
|
||||||
|
await page.waitForFunction(
|
||||||
|
() =>
|
||||||
|
(window as Window & { __reviewPaneScalingProbe?: ReviewPaneScalingProbe }).__reviewPaneScalingProbe
|
||||||
|
?.stableReadyMs !== undefined,
|
||||||
|
undefined,
|
||||||
|
{ timeout: completionTimeoutMs },
|
||||||
|
)
|
||||||
|
|
||||||
|
return page.evaluate(() => {
|
||||||
|
const probe = (window as Window & { __reviewPaneScalingProbe?: ReviewPaneScalingProbe }).__reviewPaneScalingProbe!
|
||||||
|
probe.stop()
|
||||||
|
const startedAt = probe.startedAt!
|
||||||
|
const final = probe.samples.at(-1)!
|
||||||
|
const resources = performance
|
||||||
|
.getEntriesByType("resource")
|
||||||
|
.filter((entry) => entry.name.includes("/vcs/diff")) as PerformanceResourceTiming[]
|
||||||
|
const resource = resources.at(-1)
|
||||||
|
const longTasks = probe.longTasks.filter(
|
||||||
|
(entry) => entry.startTime >= startedAt && entry.startTime <= startedAt + probe.stableReadyMs!,
|
||||||
|
)
|
||||||
|
const frameGaps = probe.frameTimesMs.map((time, index) => time - (probe.frameTimesMs[index - 1] ?? 0))
|
||||||
|
|
||||||
|
return {
|
||||||
|
firstTreeRowMs: probe.firstTreeRowMs ?? null,
|
||||||
|
logicalTreeReadyMs: probe.logicalTreeReadyMs ?? null,
|
||||||
|
firstDiffRenderMs: probe.firstDiffRenderMs ?? null,
|
||||||
|
stableReadyMs: probe.stableReadyMs ?? null,
|
||||||
|
responseStartMs: resource ? resource.responseStart - startedAt : null,
|
||||||
|
responseEndMs: resource ? resource.responseEnd - startedAt : null,
|
||||||
|
responseToStableMs: resource ? probe.stableReadyMs! - (resource.responseEnd - startedAt) : null,
|
||||||
|
treeRows: final.treeRows,
|
||||||
|
logicalRows: final.logicalRows,
|
||||||
|
fileRows: final.fileRows,
|
||||||
|
diffLines: final.diffLines,
|
||||||
|
samples: probe.samples.length,
|
||||||
|
maxFrameGapMs: Math.max(0, ...frameGaps),
|
||||||
|
longTaskCount: longTasks.length,
|
||||||
|
longTaskTotalMs: longTasks.reduce((sum, entry) => sum + entry.duration, 0),
|
||||||
|
maxLongTaskMs: Math.max(0, ...longTasks.map((entry) => entry.duration)),
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
async function installReviewPaneScalingProbe(page: Page, input: { expectedFile: string; expectedRows: number }) {
|
||||||
|
await page.evaluate(
|
||||||
|
({ expectedFile, expectedRows, stableFrames }) => {
|
||||||
|
let running = true
|
||||||
|
let readyStreak = 0
|
||||||
|
const basename = expectedFile.split("/").at(-1)!
|
||||||
|
const longTaskObserver = PerformanceObserver.supportedEntryTypes.includes("longtask")
|
||||||
|
? new PerformanceObserver((list) => {
|
||||||
|
probe.longTasks.push(
|
||||||
|
...list.getEntries().map((entry) => ({ startTime: entry.startTime, duration: entry.duration })),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
: undefined
|
||||||
|
const probe: ReviewPaneScalingProbe = {
|
||||||
|
samples: [],
|
||||||
|
frameTimesMs: [],
|
||||||
|
longTasks: [],
|
||||||
|
stop: () => {
|
||||||
|
running = false
|
||||||
|
longTaskObserver?.disconnect()
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
const sample = (time: number) => {
|
||||||
|
if (!running || probe.startedAt === undefined) return
|
||||||
|
const panel = document.querySelector<HTMLElement>("#review-panel")
|
||||||
|
const tree = panel?.querySelector<HTMLElement>('[data-component="file-tree-v2"]')
|
||||||
|
const rows = panel?.querySelectorAll('[data-slot="file-tree-v2-row"]') ?? []
|
||||||
|
const fileRows = panel?.querySelectorAll('button[data-slot="file-tree-v2-row"]') ?? []
|
||||||
|
const header =
|
||||||
|
panel?.querySelector<HTMLElement>('[data-slot="session-review-v2-file-header"]')?.textContent?.trim() ?? ""
|
||||||
|
const viewers = panel
|
||||||
|
? [...panel.querySelectorAll<HTMLElement>('[data-component="file"][data-mode="diff"]')]
|
||||||
|
: []
|
||||||
|
const diffLines = viewers.reduce(
|
||||||
|
(sum, viewer) =>
|
||||||
|
sum + (viewer.querySelector("diffs-container")?.shadowRoot?.querySelectorAll("[data-line]").length ?? 0),
|
||||||
|
0,
|
||||||
|
)
|
||||||
|
const observedAtMs = time - probe.startedAt
|
||||||
|
const logicalRows = Number(tree?.dataset.totalRows ?? rows.length)
|
||||||
|
const ready =
|
||||||
|
logicalRows === expectedRows &&
|
||||||
|
fileRows.length > 0 &&
|
||||||
|
header.includes(basename) &&
|
||||||
|
viewers.length === 1 &&
|
||||||
|
diffLines > 0
|
||||||
|
const previous = probe.samples.at(-1)
|
||||||
|
const stable =
|
||||||
|
ready &&
|
||||||
|
previous?.ready === true &&
|
||||||
|
previous.logicalRows === logicalRows &&
|
||||||
|
previous.treeRows === rows.length &&
|
||||||
|
previous.fileRows === fileRows.length &&
|
||||||
|
previous.diffLines === diffLines &&
|
||||||
|
previous.header === header
|
||||||
|
|
||||||
|
probe.frameTimesMs.push(observedAtMs)
|
||||||
|
probe.samples.push({
|
||||||
|
observedAtMs,
|
||||||
|
logicalRows,
|
||||||
|
treeRows: rows.length,
|
||||||
|
fileRows: fileRows.length,
|
||||||
|
diffLines,
|
||||||
|
header,
|
||||||
|
ready,
|
||||||
|
})
|
||||||
|
if (probe.firstTreeRowMs === undefined && rows.length > 0) probe.firstTreeRowMs = observedAtMs
|
||||||
|
if (probe.logicalTreeReadyMs === undefined && logicalRows === expectedRows)
|
||||||
|
probe.logicalTreeReadyMs = observedAtMs
|
||||||
|
if (probe.firstDiffRenderMs === undefined && diffLines > 0) probe.firstDiffRenderMs = observedAtMs
|
||||||
|
readyStreak = !ready ? 0 : stable ? readyStreak + 1 : 1
|
||||||
|
if (readyStreak === stableFrames) probe.stableReadyMs = observedAtMs
|
||||||
|
if (probe.stableReadyMs === undefined) requestAnimationFrame(sample)
|
||||||
|
}
|
||||||
|
|
||||||
|
longTaskObserver?.observe({ type: "longtask", buffered: true })
|
||||||
|
document.addEventListener(
|
||||||
|
"click",
|
||||||
|
(event) => {
|
||||||
|
const toggle = event.target instanceof Element ? event.target.closest("button") : undefined
|
||||||
|
if (!toggle?.hasAttribute("data-review-pane-scaling-toggle")) return
|
||||||
|
probe.startedAt = performance.now()
|
||||||
|
performance.mark("opencode.review-pane-scaling.click")
|
||||||
|
requestAnimationFrame(sample)
|
||||||
|
},
|
||||||
|
{ capture: true, once: true },
|
||||||
|
)
|
||||||
|
;(window as Window & { __reviewPaneScalingProbe?: ReviewPaneScalingProbe }).__reviewPaneScalingProbe = probe
|
||||||
|
},
|
||||||
|
{ ...input, stableFrames: readyFrames },
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,151 @@
|
|||||||
|
import type { Page } from "@playwright/test"
|
||||||
|
import { expectSessionTitle } from "../../utils/waits"
|
||||||
|
import { mockOpenCodeServer } from "../../utils/mock-server"
|
||||||
|
import { benchmark, expect, withBenchmarkPage } from "../benchmark"
|
||||||
|
import { fixture } from "./session-timeline-stress.fixture"
|
||||||
|
import { installStressSessionTabs, stressSessionHref } from "./timeline-test-helpers"
|
||||||
|
import { measureSessionSwitch, waitForStableTimeline } from "./session-tab-switch-probe"
|
||||||
|
|
||||||
|
type ParentHydrationBenchmarkMode = "natural" | "candidate"
|
||||||
|
|
||||||
|
const mode = process.env.SESSION_PARENT_HYDRATION_BENCHMARK_MODE ?? "natural"
|
||||||
|
if (mode !== "natural" && mode !== "candidate") throw new Error(`Unknown parent hydration benchmark mode: ${mode}`)
|
||||||
|
const userID = "msg_parent_hydration_user"
|
||||||
|
const user = {
|
||||||
|
...fixture.messages[fixture.targetID][0]!,
|
||||||
|
info: { ...fixture.messages[fixture.targetID][0]!.info, id: userID, time: { created: 1700001000000 } },
|
||||||
|
parts: fixture.messages[fixture.targetID][0]!.parts.map((part, index) => ({
|
||||||
|
...part,
|
||||||
|
id: `prt_parent_hydration_user_${index}`,
|
||||||
|
messageID: userID,
|
||||||
|
})),
|
||||||
|
}
|
||||||
|
const assistantSeed = fixture.messages[fixture.targetID][3]!
|
||||||
|
const assistants = Array.from({ length: 14 }, (_, index) => {
|
||||||
|
const messageID = `msg_parent_hydration_${String(index).padStart(2, "0")}`
|
||||||
|
return {
|
||||||
|
...assistantSeed,
|
||||||
|
info: {
|
||||||
|
...assistantSeed.info,
|
||||||
|
id: messageID,
|
||||||
|
parentID: userID,
|
||||||
|
time: { created: 1700001001000 + index * 1_000, completed: 1700001001500 + index * 1_000 },
|
||||||
|
},
|
||||||
|
parts: assistantSeed.parts.map((part, partIndex) => ({
|
||||||
|
...part,
|
||||||
|
id: `prt_parent_hydration_${String(index).padStart(2, "0")}_${partIndex}`,
|
||||||
|
messageID,
|
||||||
|
})),
|
||||||
|
}
|
||||||
|
})
|
||||||
|
const messages = [user, ...assistants]
|
||||||
|
const target = fixture.sessions.find((session) => session.id === fixture.targetID)!
|
||||||
|
const lastID = userID
|
||||||
|
const lastPartID = assistants.at(-1)!.parts.at(-1)!.id
|
||||||
|
|
||||||
|
benchmark("hydrates an orphaned latest turn after a cold session click", async ({ browser, report }, testInfo) => {
|
||||||
|
benchmark.setTimeout(180_000)
|
||||||
|
const results = [] as Awaited<ReturnType<typeof trial>>[]
|
||||||
|
for (let run = 0; run < 5; run++) {
|
||||||
|
results.push(
|
||||||
|
await withBenchmarkPage(
|
||||||
|
browser,
|
||||||
|
`session-parent-hydration-${mode}-${run}`,
|
||||||
|
(page) => trial(page, mode),
|
||||||
|
testInfo,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
const timing = results.map((result) => result.metrics.firstCorrectObservedMs!).sort((a, b) => a - b)
|
||||||
|
report(
|
||||||
|
{
|
||||||
|
results: results.map((result) => ({ ...result.metrics, historyGateCount: result.historyGateCount })),
|
||||||
|
summary: {
|
||||||
|
firstCorrectObservedMs: { min: timing[0], median: timing[2], max: timing.at(-1) },
|
||||||
|
blankSamples: results.map((result) => result.metrics.blankSamples),
|
||||||
|
requestCounts: {
|
||||||
|
list: results.map((result) => result.requestCounts.list),
|
||||||
|
parent: results.map((result) => result.requestCounts.parent),
|
||||||
|
},
|
||||||
|
historyGateCount: results.map((result) => result.historyGateCount),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{ mode },
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
async function trial(page: Page, mode: ParentHydrationBenchmarkMode) {
|
||||||
|
const requests: { type: "list" | "parent"; before?: string }[] = []
|
||||||
|
const history = mode === "candidate" ? Promise.withResolvers<void>() : undefined
|
||||||
|
let historyGates = 0
|
||||||
|
await mockOpenCodeServer(page, {
|
||||||
|
sessions: fixture.sessions.filter((session) => session.id === fixture.sourceID),
|
||||||
|
provider: fixture.provider,
|
||||||
|
directory: fixture.directory,
|
||||||
|
project: fixture.project,
|
||||||
|
messageDelay: 50,
|
||||||
|
onMessages: (request) => {
|
||||||
|
if (request.sessionID === fixture.targetID && request.phase === "start")
|
||||||
|
requests.push({ type: "list", before: request.before })
|
||||||
|
},
|
||||||
|
beforeMessagesResponse: (request) => {
|
||||||
|
if (mode !== "candidate" || request.sessionID !== fixture.targetID || !request.before) return Promise.resolve()
|
||||||
|
historyGates++
|
||||||
|
return history!.promise
|
||||||
|
},
|
||||||
|
onMessage: (request) => {
|
||||||
|
if (request.sessionID === fixture.targetID && request.messageID === userID) requests.push({ type: "parent" })
|
||||||
|
},
|
||||||
|
message: (sessionID, messageID) => {
|
||||||
|
if (sessionID !== fixture.targetID || messageID !== userID) return
|
||||||
|
return user
|
||||||
|
},
|
||||||
|
pageMessages: (sessionID, limit, before) => {
|
||||||
|
const items = sessionID === fixture.targetID ? messages : fixture.messages[fixture.sourceID]
|
||||||
|
const end = before ? items.findIndex((message) => message.info.id === before) : items.length
|
||||||
|
const start = Math.max(0, end - limit)
|
||||||
|
return { items: items.slice(start, end), cursor: start > 0 ? items[start]!.info.id : undefined }
|
||||||
|
},
|
||||||
|
})
|
||||||
|
await page.route(`**/session/${fixture.targetID}`, (route) =>
|
||||||
|
route.fulfill({ status: 200, contentType: "application/json", body: JSON.stringify(target) }),
|
||||||
|
)
|
||||||
|
await installStressSessionTabs(page, { sessionIDs: [fixture.sourceID] })
|
||||||
|
await page.goto(stressSessionHref(fixture.sourceID))
|
||||||
|
await expectSessionTitle(page, fixture.expected.sourceTitle)
|
||||||
|
await waitForStableTimeline(page, fixture.expected.sourceMessageIDs.at(-1)!)
|
||||||
|
|
||||||
|
const href = stressSessionHref(fixture.targetID)
|
||||||
|
await page.evaluate(
|
||||||
|
({ href, title }) => {
|
||||||
|
const link = document.createElement("a")
|
||||||
|
link.id = "parent-hydration-target"
|
||||||
|
link.href = href
|
||||||
|
link.textContent = title
|
||||||
|
document.body.append(link)
|
||||||
|
},
|
||||||
|
{ href, title: target.title },
|
||||||
|
)
|
||||||
|
const metrics = await measureSessionSwitch(page, {
|
||||||
|
destinationIDs: messages.map((message) => message.info.id),
|
||||||
|
sourceIDs: fixture.messages[fixture.sourceID].map((message) => message.info.id),
|
||||||
|
lastID,
|
||||||
|
requiredPartID: lastPartID,
|
||||||
|
requireBottomAnchor: false,
|
||||||
|
href,
|
||||||
|
switch: async () => {
|
||||||
|
await page.locator("#parent-hydration-target").click()
|
||||||
|
await expectSessionTitle(page, target.title)
|
||||||
|
},
|
||||||
|
}).finally(() => history?.resolve())
|
||||||
|
expect(metrics.firstCorrectObservedMs).not.toBeNull()
|
||||||
|
const requestCounts = {
|
||||||
|
list: requests.filter((request) => request.type === "list").length,
|
||||||
|
parent: requests.filter((request) => request.type === "parent").length,
|
||||||
|
}
|
||||||
|
if (mode === "candidate") {
|
||||||
|
expect(requestCounts.parent).toBe(1)
|
||||||
|
expect(historyGates).toBe(1)
|
||||||
|
}
|
||||||
|
return { metrics, requestCounts, historyGateCount: historyGates }
|
||||||
|
}
|
||||||
@@ -4,6 +4,8 @@ export type SessionSwitchSample = {
|
|||||||
source: string[]
|
source: string[]
|
||||||
hasVisibleRows: boolean
|
hasVisibleRows: boolean
|
||||||
last: boolean
|
last: boolean
|
||||||
|
requiredPartVisible?: boolean
|
||||||
|
bottomAnchorRequired?: boolean
|
||||||
bottomErrorPx?: number
|
bottomErrorPx?: number
|
||||||
review?: {
|
review?: {
|
||||||
fileHost: boolean
|
fileHost: boolean
|
||||||
@@ -41,7 +43,8 @@ export function isCorrectDestination(sample: SessionSwitchSample) {
|
|||||||
sample.destination.length > 0 &&
|
sample.destination.length > 0 &&
|
||||||
sample.source.length === 0 &&
|
sample.source.length === 0 &&
|
||||||
sample.last &&
|
sample.last &&
|
||||||
Math.abs(sample.bottomErrorPx ?? Infinity) <= 1
|
sample.requiredPartVisible !== false &&
|
||||||
|
(sample.bottomAnchorRequired === false || Math.abs(sample.bottomErrorPx ?? Infinity) <= 1)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -8,9 +8,16 @@ type SessionSwitchProbe = {
|
|||||||
|
|
||||||
async function installSessionSwitchProbe(
|
async function installSessionSwitchProbe(
|
||||||
page: Page,
|
page: Page,
|
||||||
input: { destinationIDs: string[]; sourceIDs: string[]; lastID: string; href: string },
|
input: {
|
||||||
|
destinationIDs: string[]
|
||||||
|
sourceIDs: string[]
|
||||||
|
lastID: string
|
||||||
|
requiredPartID?: string
|
||||||
|
requireBottomAnchor?: boolean
|
||||||
|
href: string
|
||||||
|
},
|
||||||
) {
|
) {
|
||||||
await page.evaluate(({ destinationIDs, sourceIDs, lastID, href }) => {
|
await page.evaluate(({ destinationIDs, sourceIDs, lastID, requiredPartID, requireBottomAnchor, href }) => {
|
||||||
const destination = new Set(destinationIDs)
|
const destination = new Set(destinationIDs)
|
||||||
const source = new Set(sourceIDs)
|
const source = new Set(sourceIDs)
|
||||||
const samples: SessionSwitchSample[] = []
|
const samples: SessionSwitchSample[] = []
|
||||||
@@ -66,6 +73,13 @@ async function installSessionSwitchProbe(
|
|||||||
const rect = element.getBoundingClientRect()
|
const rect = element.getBoundingClientRect()
|
||||||
return rect.bottom > view.top && rect.top < view.bottom
|
return rect.bottom > view.top && rect.top < view.bottom
|
||||||
})
|
})
|
||||||
|
const requiredPartVisible = requiredPartID
|
||||||
|
? [...root.querySelectorAll<HTMLElement>("[data-timeline-part-id]")].some((element) => {
|
||||||
|
if (element.dataset.timelinePartId !== requiredPartID) return false
|
||||||
|
const rect = element.getBoundingClientRect()
|
||||||
|
return rect.width > 0 && rect.height > 0 && rect.bottom > view.top && rect.top < view.bottom
|
||||||
|
})
|
||||||
|
: undefined
|
||||||
const spacer = root.querySelector<HTMLElement>('[data-timeline-row="bottom-spacer"]')?.getBoundingClientRect()
|
const spacer = root.querySelector<HTMLElement>('[data-timeline-row="bottom-spacer"]')?.getBoundingClientRect()
|
||||||
samples.push({
|
samples.push({
|
||||||
observedAtMs,
|
observedAtMs,
|
||||||
@@ -73,11 +87,22 @@ async function installSessionSwitchProbe(
|
|||||||
source: visible.filter((id) => source.has(id)),
|
source: visible.filter((id) => source.has(id)),
|
||||||
hasVisibleRows,
|
hasVisibleRows,
|
||||||
last: visible.includes(lastID),
|
last: visible.includes(lastID),
|
||||||
|
requiredPartVisible,
|
||||||
|
bottomAnchorRequired: requireBottomAnchor !== false,
|
||||||
bottomErrorPx: spacer ? spacer.bottom - view.bottom : undefined,
|
bottomErrorPx: spacer ? spacer.bottom - view.bottom : undefined,
|
||||||
review,
|
review,
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
samples.push({ observedAtMs, destination: [], source: [], hasVisibleRows: false, last: false, review })
|
samples.push({
|
||||||
|
observedAtMs,
|
||||||
|
destination: [],
|
||||||
|
source: [],
|
||||||
|
hasVisibleRows: false,
|
||||||
|
last: false,
|
||||||
|
requiredPartVisible: requiredPartID ? false : undefined,
|
||||||
|
bottomAnchorRequired: requireBottomAnchor !== false,
|
||||||
|
review,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
requestAnimationFrame(sample)
|
requestAnimationFrame(sample)
|
||||||
}, 0)
|
}, 0)
|
||||||
@@ -117,7 +142,8 @@ async function waitForStableSessionSwitch(page: Page) {
|
|||||||
sample.destination.length > 0 &&
|
sample.destination.length > 0 &&
|
||||||
sample.source.length === 0 &&
|
sample.source.length === 0 &&
|
||||||
sample.last &&
|
sample.last &&
|
||||||
Math.abs(sample.bottomErrorPx ?? Infinity) <= 1,
|
sample.requiredPartVisible !== false &&
|
||||||
|
(sample.bottomAnchorRequired === false || Math.abs(sample.bottomErrorPx ?? Infinity) <= 1),
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
@@ -135,13 +161,27 @@ async function collectSessionSwitchResult(page: Page) {
|
|||||||
|
|
||||||
export async function measureSessionSwitch(
|
export async function measureSessionSwitch(
|
||||||
page: Page,
|
page: Page,
|
||||||
input: { destinationIDs: string[]; sourceIDs: string[]; lastID: string; href: string; switch: () => Promise<void> },
|
input: {
|
||||||
|
destinationIDs: string[]
|
||||||
|
sourceIDs: string[]
|
||||||
|
lastID: string
|
||||||
|
requiredPartID?: string
|
||||||
|
requireBottomAnchor?: boolean
|
||||||
|
href: string
|
||||||
|
switch: () => Promise<void>
|
||||||
|
},
|
||||||
) {
|
) {
|
||||||
const { switch: run, ...probe } = input
|
const { switch: run, ...probe } = input
|
||||||
await installSessionSwitchProbe(page, probe)
|
await installSessionSwitchProbe(page, probe)
|
||||||
await run()
|
try {
|
||||||
await waitForStableSessionSwitch(page)
|
await run()
|
||||||
return collectSessionSwitchResult(page)
|
await waitForStableSessionSwitch(page)
|
||||||
|
return await collectSessionSwitchResult(page)
|
||||||
|
} finally {
|
||||||
|
await page.evaluate(() => {
|
||||||
|
;(window as Window & { __sessionSwitchProbe?: SessionSwitchProbe }).__sessionSwitchProbe?.stop()
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function waitForStableTimeline(page: Page, lastID: string) {
|
export async function waitForStableTimeline(page: Page, lastID: string) {
|
||||||
|
|||||||
@@ -0,0 +1,46 @@
|
|||||||
|
import { expect, test } from "bun:test"
|
||||||
|
import type { Page, Route } from "@playwright/test"
|
||||||
|
import { mockOpenCodeServer } from "../../utils/mock-server"
|
||||||
|
|
||||||
|
test("applies message latency after a list response gate is released", async () => {
|
||||||
|
const events: string[] = []
|
||||||
|
const gate = Promise.withResolvers<void>()
|
||||||
|
let handler: ((route: Route) => Promise<void>) | undefined
|
||||||
|
const page = {
|
||||||
|
route: (_url: string, callback: (route: Route) => Promise<void>) => {
|
||||||
|
handler = callback
|
||||||
|
return Promise.resolve()
|
||||||
|
},
|
||||||
|
} as unknown as Page
|
||||||
|
await mockOpenCodeServer(page, {
|
||||||
|
provider: {},
|
||||||
|
directory: "C:/OpenCode",
|
||||||
|
project: {},
|
||||||
|
sessions: [{ id: "session" }],
|
||||||
|
messageDelay: 25,
|
||||||
|
beforeMessagesResponse: () => {
|
||||||
|
events.push("before")
|
||||||
|
return gate.promise
|
||||||
|
},
|
||||||
|
onMessages: (request) => events.push(request.phase),
|
||||||
|
pageMessages: () => {
|
||||||
|
events.push("page")
|
||||||
|
return { items: [] }
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
const response = handler!({
|
||||||
|
request: () => ({ url: () => "http://127.0.0.1:4096/session/session/message" }),
|
||||||
|
fulfill: () => {
|
||||||
|
events.push("fulfill")
|
||||||
|
return Promise.resolve()
|
||||||
|
},
|
||||||
|
} as unknown as Route)
|
||||||
|
expect(events).toEqual(["start", "before"])
|
||||||
|
|
||||||
|
const released = performance.now()
|
||||||
|
gate.resolve()
|
||||||
|
await response
|
||||||
|
expect(performance.now() - released).toBeGreaterThanOrEqual(20)
|
||||||
|
expect(events).toEqual(["start", "before", "page", "end", "fulfill"])
|
||||||
|
})
|
||||||
@@ -52,3 +52,35 @@ test("reports missing correctness without throwing", () => {
|
|||||||
expect(result.firstCorrectObservedMs).toBeNull()
|
expect(result.firstCorrectObservedMs).toBeNull()
|
||||||
expect(result.stableObservedMs).toBeNull()
|
expect(result.stableObservedMs).toBeNull()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
test("requires an explicitly tracked part to be visible", () => {
|
||||||
|
const result = classifySessionSwitch([
|
||||||
|
{
|
||||||
|
observedAtMs: 16,
|
||||||
|
destination: ["destination"],
|
||||||
|
source: [],
|
||||||
|
hasVisibleRows: true,
|
||||||
|
last: true,
|
||||||
|
requiredPartVisible: false,
|
||||||
|
bottomErrorPx: 0,
|
||||||
|
},
|
||||||
|
])
|
||||||
|
|
||||||
|
expect(result.firstCorrectObservedMs).toBeNull()
|
||||||
|
})
|
||||||
|
|
||||||
|
test("can measure content correctness without requiring a bottom anchor", () => {
|
||||||
|
const result = classifySessionSwitch([
|
||||||
|
{
|
||||||
|
observedAtMs: 16,
|
||||||
|
destination: ["destination"],
|
||||||
|
source: [],
|
||||||
|
hasVisibleRows: true,
|
||||||
|
last: true,
|
||||||
|
requiredPartVisible: true,
|
||||||
|
bottomAnchorRequired: false,
|
||||||
|
},
|
||||||
|
])
|
||||||
|
|
||||||
|
expect(result.firstCorrectObservedMs).toBe(16)
|
||||||
|
})
|
||||||
|
|||||||
@@ -0,0 +1,55 @@
|
|||||||
|
import { expect, test } from "bun:test"
|
||||||
|
import type { Page } from "@playwright/test"
|
||||||
|
import { measureSessionSwitch } from "../timeline/session-tab-switch-probe"
|
||||||
|
|
||||||
|
function testPage(waitFailure?: Error) {
|
||||||
|
const stops: unknown[] = []
|
||||||
|
const page = {
|
||||||
|
evaluate: async (_callback: unknown, input?: unknown) => {
|
||||||
|
if (input) return
|
||||||
|
stops.push(undefined)
|
||||||
|
},
|
||||||
|
waitForFunction: async () => {
|
||||||
|
if (waitFailure) throw waitFailure
|
||||||
|
},
|
||||||
|
} as unknown as Page
|
||||||
|
return { page, stops }
|
||||||
|
}
|
||||||
|
|
||||||
|
function input(run: () => Promise<void>) {
|
||||||
|
return {
|
||||||
|
destinationIDs: ["destination"],
|
||||||
|
sourceIDs: ["source"],
|
||||||
|
lastID: "destination",
|
||||||
|
href: "/session/destination",
|
||||||
|
switch: run,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
test("stops sampling when the session switch fails", async () => {
|
||||||
|
const failure = new Error("switch failed")
|
||||||
|
const context = testPage()
|
||||||
|
|
||||||
|
await expect(
|
||||||
|
measureSessionSwitch(
|
||||||
|
context.page,
|
||||||
|
input(async () => Promise.reject(failure)),
|
||||||
|
),
|
||||||
|
).rejects.toBe(failure)
|
||||||
|
|
||||||
|
expect(context.stops).toHaveLength(1)
|
||||||
|
})
|
||||||
|
|
||||||
|
test("stops sampling when the stable wait fails", async () => {
|
||||||
|
const failure = new Error("stable wait failed")
|
||||||
|
const context = testPage(failure)
|
||||||
|
|
||||||
|
await expect(
|
||||||
|
measureSessionSwitch(
|
||||||
|
context.page,
|
||||||
|
input(async () => {}),
|
||||||
|
),
|
||||||
|
).rejects.toBe(failure)
|
||||||
|
|
||||||
|
expect(context.stops).toHaveLength(1)
|
||||||
|
})
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
import { expect, test } from "@playwright/test"
|
||||||
|
import { base64Encode } from "@opencode-ai/core/util/encode"
|
||||||
|
import { mockOpenCodeServer } from "../utils/mock-server"
|
||||||
|
|
||||||
|
const draftID = "draft_legacy_new_session"
|
||||||
|
const directory = "C:/OpenCode/LegacyNewSession"
|
||||||
|
const server = `http://${process.env.PLAYWRIGHT_SERVER_HOST ?? "127.0.0.1"}:${process.env.PLAYWRIGHT_SERVER_PORT ?? "4096"}`
|
||||||
|
|
||||||
|
test("redirects a draft to the legacy new-session route", async ({ page }) => {
|
||||||
|
await mockOpenCodeServer(page, {
|
||||||
|
directory,
|
||||||
|
project: {
|
||||||
|
id: "proj_legacy_new_session",
|
||||||
|
worktree: directory,
|
||||||
|
vcs: "git",
|
||||||
|
name: "legacy-new-session",
|
||||||
|
time: { created: 1700000000000, updated: 1700000000000 },
|
||||||
|
sandboxes: [],
|
||||||
|
},
|
||||||
|
provider: { all: [], connected: [], default: {} },
|
||||||
|
sessions: [],
|
||||||
|
pageMessages: () => ({ items: [] }),
|
||||||
|
})
|
||||||
|
await page.addInitScript(
|
||||||
|
({ directory, draftID, server }) => {
|
||||||
|
localStorage.setItem("settings.v3", JSON.stringify({ general: { newLayoutDesigns: false } }))
|
||||||
|
localStorage.setItem(
|
||||||
|
"opencode.window.browser.dat:tabs",
|
||||||
|
JSON.stringify([{ type: "draft", draftID, server, directory }]),
|
||||||
|
)
|
||||||
|
},
|
||||||
|
{ directory, draftID, server },
|
||||||
|
)
|
||||||
|
|
||||||
|
await page.goto(`/new-session?draftId=${draftID}`)
|
||||||
|
|
||||||
|
await expect(page).toHaveURL(`/${base64Encode(directory)}/session`)
|
||||||
|
await expect(page.locator("header[data-tauri-drag-region]")).toBeVisible()
|
||||||
|
await expect(page.locator('[data-component="prompt-input"]')).toBeVisible()
|
||||||
|
})
|
||||||
@@ -0,0 +1,83 @@
|
|||||||
|
import { expect, test } from "@playwright/test"
|
||||||
|
import { mockOpenCodeServer } from "../utils/mock-server"
|
||||||
|
import { expectAppVisible } from "../utils/waits"
|
||||||
|
|
||||||
|
const draftID = "draft_new_session_panel_corner"
|
||||||
|
const directory = "C:/OpenCode/NewSessionPanelCorner"
|
||||||
|
const server = `http://${process.env.PLAYWRIGHT_SERVER_HOST ?? "127.0.0.1"}:${process.env.PLAYWRIGHT_SERVER_PORT ?? "4096"}`
|
||||||
|
|
||||||
|
test.use({
|
||||||
|
viewport: { width: 935, height: 522 },
|
||||||
|
deviceScaleFactor: 1,
|
||||||
|
})
|
||||||
|
|
||||||
|
test("matches the rounded panel corners to the dark new-session background", async ({ page }, testInfo) => {
|
||||||
|
await mockOpenCodeServer(page, {
|
||||||
|
directory,
|
||||||
|
project: {
|
||||||
|
id: "proj_new_session_panel_corner",
|
||||||
|
worktree: directory,
|
||||||
|
vcs: "git",
|
||||||
|
name: "new-session-panel-corner",
|
||||||
|
time: { created: 1700000000000, updated: 1700000000000 },
|
||||||
|
sandboxes: [],
|
||||||
|
},
|
||||||
|
provider: { all: [], connected: [], default: {} },
|
||||||
|
sessions: [],
|
||||||
|
pageMessages: () => ({ items: [] }),
|
||||||
|
})
|
||||||
|
await page.addInitScript(
|
||||||
|
({ directory, draftID, server }) => {
|
||||||
|
localStorage.setItem("settings.v3", JSON.stringify({ general: { newLayoutDesigns: true } }))
|
||||||
|
localStorage.setItem("opencode-theme-id", "oc-2")
|
||||||
|
localStorage.setItem("opencode-color-scheme", "dark")
|
||||||
|
localStorage.setItem(
|
||||||
|
"opencode.global.dat:server",
|
||||||
|
JSON.stringify({
|
||||||
|
projects: { local: [{ worktree: directory, expanded: true }] },
|
||||||
|
lastProject: { local: directory },
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
localStorage.setItem(
|
||||||
|
"opencode.window.browser.dat:tabs",
|
||||||
|
JSON.stringify([{ type: "draft", draftID, server, directory }]),
|
||||||
|
)
|
||||||
|
},
|
||||||
|
{ directory, draftID, server },
|
||||||
|
)
|
||||||
|
|
||||||
|
await page.goto(`/new-session?draftId=${draftID}`)
|
||||||
|
await expectAppVisible(page.locator('[data-component="prompt-input"]'))
|
||||||
|
await expect(page.locator("html")).toHaveAttribute("data-color-scheme", "dark")
|
||||||
|
const panel = page.locator('main div[class*="rounded-[10px]"][class*="overflow-hidden"]')
|
||||||
|
await expect(panel).toHaveCount(1)
|
||||||
|
const box = await panel.boundingBox()
|
||||||
|
if (!box) throw new Error("New-session panel bounds are unavailable")
|
||||||
|
|
||||||
|
const screenshot = await page.screenshot({ path: testInfo.outputPath("new-session-dark.png") })
|
||||||
|
const corners = await page.evaluate(
|
||||||
|
async ({ source, points }) => {
|
||||||
|
const image = new Image()
|
||||||
|
image.src = source
|
||||||
|
await image.decode()
|
||||||
|
const canvas = document.createElement("canvas")
|
||||||
|
canvas.width = image.naturalWidth
|
||||||
|
canvas.height = image.naturalHeight
|
||||||
|
const context = canvas.getContext("2d")
|
||||||
|
if (!context) throw new Error("2D canvas is unavailable")
|
||||||
|
context.drawImage(image, 0, 0)
|
||||||
|
return points.map((point) => Array.from(context.getImageData(point.x, point.y, 1, 1).data))
|
||||||
|
},
|
||||||
|
{
|
||||||
|
source: `data:image/png;base64,${screenshot.toString("base64")}`,
|
||||||
|
points: [
|
||||||
|
{ x: Math.floor(box.x), y: Math.floor(box.y) },
|
||||||
|
{ x: Math.ceil(box.x + box.width) - 1, y: Math.floor(box.y) },
|
||||||
|
{ x: Math.floor(box.x), y: Math.ceil(box.y + box.height) - 1 },
|
||||||
|
{ x: Math.ceil(box.x + box.width) - 1, y: Math.ceil(box.y + box.height) - 1 },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(corners.every(([red, green, blue, alpha]) => red <= 8 && green <= 8 && blue <= 8 && alpha === 255)).toBe(true)
|
||||||
|
})
|
||||||
@@ -0,0 +1,146 @@
|
|||||||
|
import { base64Encode } from "@opencode-ai/core/util/encode"
|
||||||
|
import { expect, test } from "@playwright/test"
|
||||||
|
import { mockOpenCodeServer } from "../utils/mock-server"
|
||||||
|
import { expectSessionTitle } from "../utils/waits"
|
||||||
|
|
||||||
|
const directory = "C:/OpenCode/ReviewOpenFile"
|
||||||
|
const projectID = "proj_review_open_file"
|
||||||
|
const sessionID = "ses_review_open_file"
|
||||||
|
const title = "Review open file"
|
||||||
|
const server = `http://${process.env.PLAYWRIGHT_SERVER_HOST ?? "127.0.0.1"}:${process.env.PLAYWRIGHT_SERVER_PORT ?? "4096"}`
|
||||||
|
|
||||||
|
test.use({ viewport: { width: 1440, height: 900 } })
|
||||||
|
|
||||||
|
test("opens and searches project files inline", async ({ page }) => {
|
||||||
|
const searches: { query: string; dirs?: string; limit?: number }[] = []
|
||||||
|
await mockOpenCodeServer(page, {
|
||||||
|
directory,
|
||||||
|
project: {
|
||||||
|
id: projectID,
|
||||||
|
worktree: directory,
|
||||||
|
vcs: "git",
|
||||||
|
name: "open-file-project",
|
||||||
|
time: { created: 1700000000000, updated: 1700000000000 },
|
||||||
|
sandboxes: [],
|
||||||
|
},
|
||||||
|
provider: {
|
||||||
|
all: [
|
||||||
|
{
|
||||||
|
id: "opencode",
|
||||||
|
name: "OpenCode",
|
||||||
|
models: { test: { id: "test", name: "Test", limit: { context: 200_000 } } },
|
||||||
|
},
|
||||||
|
],
|
||||||
|
connected: ["opencode"],
|
||||||
|
default: { providerID: "opencode", modelID: "test" },
|
||||||
|
},
|
||||||
|
sessions: [
|
||||||
|
{
|
||||||
|
id: sessionID,
|
||||||
|
slug: sessionID,
|
||||||
|
projectID,
|
||||||
|
directory,
|
||||||
|
title,
|
||||||
|
version: "dev",
|
||||||
|
time: { created: 1700000000000, updated: 1700000000000 },
|
||||||
|
},
|
||||||
|
],
|
||||||
|
vcsDiff: [fileDiff("src/changed.ts")],
|
||||||
|
fileList: (path) => {
|
||||||
|
if (path) return []
|
||||||
|
return [
|
||||||
|
fileNode("README.md"),
|
||||||
|
{ name: "src", path: "src", absolute: `${directory}/src`, type: "directory", ignored: false },
|
||||||
|
]
|
||||||
|
},
|
||||||
|
fileContent: (path) => ({ type: "text", content: `contents:${path}` }),
|
||||||
|
findFiles: (input) => {
|
||||||
|
searches.push(input)
|
||||||
|
return input.query === "nested" ? ["src/nested.ts"] : []
|
||||||
|
},
|
||||||
|
pageMessages: () => ({ items: [] }),
|
||||||
|
})
|
||||||
|
await page.addInitScript(
|
||||||
|
({ directory, server, sessionID }) => {
|
||||||
|
localStorage.setItem("settings.v3", JSON.stringify({ general: { newLayoutDesigns: true } }))
|
||||||
|
localStorage.setItem(
|
||||||
|
"opencode.global.dat:server",
|
||||||
|
JSON.stringify({
|
||||||
|
projects: { local: [{ worktree: directory, expanded: true }] },
|
||||||
|
lastProject: { local: directory },
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
localStorage.setItem(
|
||||||
|
"opencode.global.dat:layout",
|
||||||
|
JSON.stringify({ review: { diffStyle: "split", panelOpened: true } }),
|
||||||
|
)
|
||||||
|
localStorage.setItem(
|
||||||
|
"opencode.window.browser.dat:tabs",
|
||||||
|
JSON.stringify([{ type: "session", server, sessionId: sessionID }]),
|
||||||
|
)
|
||||||
|
},
|
||||||
|
{ directory, server, sessionID },
|
||||||
|
)
|
||||||
|
|
||||||
|
await page.goto(`/server/${base64Encode(server)}/session/${sessionID}`)
|
||||||
|
await expectSessionTitle(page, title)
|
||||||
|
|
||||||
|
const panel = page.locator("#review-panel")
|
||||||
|
const contextButton = page.getByRole("button", { name: "View context usage" })
|
||||||
|
await contextButton.click()
|
||||||
|
await expect(panel.getByRole("tab", { name: "Context" })).toHaveAttribute("data-selected", "")
|
||||||
|
await panel.getByRole("button", { name: "Open file" }).click()
|
||||||
|
await expect(panel.getByRole("tab", { name: "Open file" })).toHaveAttribute("data-selected", "")
|
||||||
|
await contextButton.click()
|
||||||
|
await expect(panel.getByRole("tab", { name: "Context" })).toHaveAttribute("data-selected", "")
|
||||||
|
await panel.getByRole("button", { name: "Open file" }).click()
|
||||||
|
const filter = panel.getByRole("combobox", { name: "Filter files" })
|
||||||
|
await expect(filter).toBeFocused()
|
||||||
|
await expect(panel.getByRole("tab", { name: "Open file" })).toHaveAttribute("data-selected", "")
|
||||||
|
await expect(panel.getByText("open-file-project", { exact: true })).toBeVisible()
|
||||||
|
|
||||||
|
await panel.getByRole("button", { name: "README.md" }).click()
|
||||||
|
await expect(panel.getByRole("tab", { name: "README.md" })).toHaveAttribute("data-selected", "")
|
||||||
|
await expect(panel.getByText("contents:README.md", { exact: true })).toBeVisible()
|
||||||
|
|
||||||
|
await panel.getByRole("button", { name: "Open file" }).click()
|
||||||
|
await expect(panel.getByRole("tab", { name: "README.md" })).toHaveCount(0)
|
||||||
|
await filter.fill("nested")
|
||||||
|
const result = panel.getByRole("option", { name: /nested\.ts/ })
|
||||||
|
await expect(result).toBeVisible()
|
||||||
|
const resultID = await result.getAttribute("id")
|
||||||
|
expect(resultID).toBeTruthy()
|
||||||
|
await expect(filter).toHaveAttribute("aria-activedescendant", resultID!)
|
||||||
|
await filter.press("Enter")
|
||||||
|
await expect(panel.getByRole("tab", { name: "nested.ts" })).toHaveAttribute("data-selected", "")
|
||||||
|
await expect(panel.getByText("contents:src/nested.ts", { exact: true })).toBeVisible()
|
||||||
|
expect(searches).toContainEqual({ query: "nested", dirs: "false", limit: 200 })
|
||||||
|
|
||||||
|
await panel.getByRole("button", { name: "Open file" }).click()
|
||||||
|
await expect(panel.getByRole("tab", { name: "nested.ts" })).toHaveCount(1)
|
||||||
|
await expect(panel.getByRole("tab", { name: "Open file" })).toHaveAttribute("data-selected", "")
|
||||||
|
await page.keyboard.press("Control+w")
|
||||||
|
await expect(panel.getByRole("tab", { name: "Open file" })).toHaveCount(0)
|
||||||
|
await expect(panel.getByRole("tab", { name: "nested.ts" })).toHaveAttribute("data-selected", "")
|
||||||
|
})
|
||||||
|
|
||||||
|
function fileNode(path: string) {
|
||||||
|
return {
|
||||||
|
name: path,
|
||||||
|
path,
|
||||||
|
absolute: `${directory}/${path}`,
|
||||||
|
type: "file",
|
||||||
|
ignored: false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function fileDiff(file: string) {
|
||||||
|
return {
|
||||||
|
file,
|
||||||
|
before: "before\n",
|
||||||
|
after: "after\n",
|
||||||
|
additions: 1,
|
||||||
|
deletions: 1,
|
||||||
|
status: "modified",
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,152 @@
|
|||||||
|
import { base64Encode } from "@opencode-ai/core/util/encode"
|
||||||
|
import { expect, test, type Page } from "@playwright/test"
|
||||||
|
import { mockOpenCodeServer } from "../utils/mock-server"
|
||||||
|
import { expectSessionTitle } from "../utils/waits"
|
||||||
|
|
||||||
|
const directory = "C:/OpenCode/ReviewStatePersistence"
|
||||||
|
const projectID = "proj_review_state_persistence"
|
||||||
|
const sessionA = "ses_review_state_a"
|
||||||
|
const sessionB = "ses_review_state_b"
|
||||||
|
const titleA = "Alpha review state"
|
||||||
|
const titleB = "Beta review state"
|
||||||
|
const server = `http://${process.env.PLAYWRIGHT_SERVER_HOST ?? "127.0.0.1"}:${process.env.PLAYWRIGHT_SERVER_PORT ?? "4096"}`
|
||||||
|
|
||||||
|
test.use({ viewport: { width: 1440, height: 900 } })
|
||||||
|
|
||||||
|
test("restores review mode and selected file per session", async ({ page }) => {
|
||||||
|
await setup(page)
|
||||||
|
await page.goto(sessionHref(sessionA))
|
||||||
|
await expectSessionTitle(page, titleA)
|
||||||
|
await page.getByRole("button", { name: "Toggle review" }).click()
|
||||||
|
|
||||||
|
await selectMode(page, "Git changes", "Branch changes")
|
||||||
|
await selectFile(page, "beta.ts")
|
||||||
|
|
||||||
|
await switchSession(page, titleB)
|
||||||
|
await expect(page.getByRole("button", { name: "Git changes" })).toBeVisible()
|
||||||
|
await selectFile(page, "gamma.ts")
|
||||||
|
|
||||||
|
await switchSession(page, titleA)
|
||||||
|
await expect(page.getByRole("button", { name: "Branch changes" })).toBeVisible()
|
||||||
|
await expectSelectedFile(page, "beta.ts")
|
||||||
|
await selectMode(page, "Branch changes", "Git changes")
|
||||||
|
await expectSelectedFile(page, "alpha.ts")
|
||||||
|
await selectMode(page, "Git changes", "Branch changes")
|
||||||
|
await expectSelectedFile(page, "beta.ts")
|
||||||
|
|
||||||
|
await page.reload()
|
||||||
|
await expectSessionTitle(page, titleA)
|
||||||
|
await expect(page.getByRole("button", { name: "Branch changes" })).toBeVisible()
|
||||||
|
await expectSelectedFile(page, "beta.ts")
|
||||||
|
|
||||||
|
await switchSession(page, titleB)
|
||||||
|
await expect(page.getByRole("button", { name: "Git changes" })).toBeVisible()
|
||||||
|
await expectSelectedFile(page, "gamma.ts")
|
||||||
|
})
|
||||||
|
|
||||||
|
async function selectMode(page: Page, current: string, next: string) {
|
||||||
|
await page.getByRole("button", { name: current }).click()
|
||||||
|
await page.getByRole("option", { name: next }).click()
|
||||||
|
}
|
||||||
|
|
||||||
|
async function selectFile(page: Page, file: string) {
|
||||||
|
await page.getByRole("button", { name: file }).click()
|
||||||
|
await expectSelectedFile(page, file)
|
||||||
|
}
|
||||||
|
|
||||||
|
async function expectSelectedFile(page: Page, file: string) {
|
||||||
|
await expect(page.locator('[data-slot="session-review-v2-file-name"]')).toHaveText(file)
|
||||||
|
}
|
||||||
|
|
||||||
|
async function switchSession(page: Page, title: string) {
|
||||||
|
await page.locator("[data-titlebar-tab-slot]", { hasText: title }).click()
|
||||||
|
await expectSessionTitle(page, title)
|
||||||
|
}
|
||||||
|
|
||||||
|
async function setup(page: Page) {
|
||||||
|
await mockOpenCodeServer(page, {
|
||||||
|
directory,
|
||||||
|
project: {
|
||||||
|
id: projectID,
|
||||||
|
worktree: directory,
|
||||||
|
vcs: "git",
|
||||||
|
name: "review-state-persistence",
|
||||||
|
time: { created: 1700000000000, updated: 1700000000000 },
|
||||||
|
sandboxes: [],
|
||||||
|
},
|
||||||
|
provider: {
|
||||||
|
all: [
|
||||||
|
{
|
||||||
|
id: "opencode",
|
||||||
|
name: "OpenCode",
|
||||||
|
models: { test: { id: "test", name: "Test", limit: { context: 200_000 } } },
|
||||||
|
},
|
||||||
|
],
|
||||||
|
connected: ["opencode"],
|
||||||
|
default: { providerID: "opencode", modelID: "test" },
|
||||||
|
},
|
||||||
|
sessions: [session(sessionA, titleA, 1700000000000), session(sessionB, titleB, 1700000001000)],
|
||||||
|
pageMessages: () => ({ items: [] }),
|
||||||
|
})
|
||||||
|
await page.route(/\/vcs(?:\?.*)?$/, (route) =>
|
||||||
|
route.fulfill({
|
||||||
|
status: 200,
|
||||||
|
contentType: "application/json",
|
||||||
|
body: JSON.stringify({ branch: "feature", default_branch: "dev" }),
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
await page.route("**/vcs/diff**", (route) =>
|
||||||
|
route.fulfill({
|
||||||
|
status: 200,
|
||||||
|
contentType: "application/json",
|
||||||
|
body: JSON.stringify(
|
||||||
|
new URL(route.request().url()).searchParams.get("mode") === "branch"
|
||||||
|
? [diff("src/alpha.ts"), diff("src/beta.ts")]
|
||||||
|
: [diff("src/alpha.ts"), diff("src/gamma.ts")],
|
||||||
|
),
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
await page.addInitScript(
|
||||||
|
({ directory, server, sessions }) => {
|
||||||
|
localStorage.setItem("settings.v3", JSON.stringify({ general: { newLayoutDesigns: true } }))
|
||||||
|
localStorage.setItem(
|
||||||
|
"opencode.global.dat:server",
|
||||||
|
JSON.stringify({
|
||||||
|
projects: { local: [{ worktree: directory, expanded: true }] },
|
||||||
|
lastProject: { local: directory },
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
localStorage.setItem(
|
||||||
|
"opencode.window.browser.dat:tabs",
|
||||||
|
JSON.stringify(sessions.map((sessionId: string) => ({ type: "session", server, sessionId }))),
|
||||||
|
)
|
||||||
|
},
|
||||||
|
{ directory, server, sessions: [sessionA, sessionB] },
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function session(id: string, title: string, created: number) {
|
||||||
|
return {
|
||||||
|
id,
|
||||||
|
slug: id,
|
||||||
|
projectID,
|
||||||
|
directory,
|
||||||
|
title,
|
||||||
|
version: "dev",
|
||||||
|
time: { created, updated: created },
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function diff(file: string) {
|
||||||
|
return {
|
||||||
|
file,
|
||||||
|
additions: 1,
|
||||||
|
deletions: 1,
|
||||||
|
status: "modified",
|
||||||
|
patch: `diff --git a/${file} b/${file}\n--- a/${file}\n+++ b/${file}\n@@ -1 +1 @@\n-export const value = 'before'\n+export const value = 'after'\n`,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function sessionHref(sessionID: string) {
|
||||||
|
return `/server/${base64Encode(server)}/session/${sessionID}`
|
||||||
|
}
|
||||||
@@ -10,6 +10,9 @@ const sessionB = "ses_review_tab_b"
|
|||||||
const titleA = "Alpha session"
|
const titleA = "Alpha session"
|
||||||
const titleB = "Beta session"
|
const titleB = "Beta session"
|
||||||
const server = `http://${process.env.PLAYWRIGHT_SERVER_HOST ?? "127.0.0.1"}:${process.env.PLAYWRIGHT_SERVER_PORT ?? "4096"}`
|
const server = `http://${process.env.PLAYWRIGHT_SERVER_HOST ?? "127.0.0.1"}:${process.env.PLAYWRIGHT_SERVER_PORT ?? "4096"}`
|
||||||
|
const diffs = Array.from({ length: 2_740 }, (_, index) =>
|
||||||
|
fileDiff(`src/generated-${String(index).padStart(4, "0")}.ts`),
|
||||||
|
)
|
||||||
// Marks the review pane DOM node so a remount (fresh node) is detectable.
|
// Marks the review pane DOM node so a remount (fresh node) is detectable.
|
||||||
const PROBE = "original"
|
const PROBE = "original"
|
||||||
|
|
||||||
@@ -25,20 +28,34 @@ test("keeps the v2 review pane mounted when switching session tabs in a workspac
|
|||||||
await expectSessionTitle(page, titleA)
|
await expectSessionTitle(page, titleA)
|
||||||
|
|
||||||
await page.getByRole("button", { name: "Toggle review" }).click()
|
await page.getByRole("button", { name: "Toggle review" }).click()
|
||||||
|
const reviewTab = page.getByRole("tab", { name: /Review/ })
|
||||||
|
const reviewTabPanel = page.getByRole("tabpanel", { name: /Review/ })
|
||||||
|
await expect(reviewTab).toHaveAttribute("aria-controls", "session-side-panel-review-tabpanel")
|
||||||
|
await expect(reviewTabPanel).toHaveAttribute("id", "session-side-panel-review-tabpanel")
|
||||||
const review = page.locator('#review-panel [data-component="session-review-v2"]')
|
const review = page.locator('#review-panel [data-component="session-review-v2"]')
|
||||||
await expectAppVisible(review)
|
await expectAppVisible(review)
|
||||||
await expectAppVisible(page.getByRole("button", { name: /example\.ts/ }))
|
await expectAppVisible(page.getByRole("button", { name: "generated-0000.ts" }))
|
||||||
await writeProbe(page)
|
await writeProbe(page)
|
||||||
|
|
||||||
await switchTab(page, titleB)
|
await switchTab(page, titleB)
|
||||||
await expectSessionTitle(page, titleB)
|
await expectSessionTitle(page, titleB)
|
||||||
await expectAppVisible(review)
|
await expectAppVisible(review)
|
||||||
|
await expectAppVisible(page.getByRole("button", { name: "generated-0000.ts" }))
|
||||||
expect(await readProbe(page)).toBe(PROBE)
|
expect(await readProbe(page)).toBe(PROBE)
|
||||||
|
|
||||||
await switchTab(page, titleA)
|
await switchTab(page, titleA)
|
||||||
await expectSessionTitle(page, titleA)
|
await expectSessionTitle(page, titleA)
|
||||||
await expectAppVisible(review)
|
await expectAppVisible(review)
|
||||||
|
await expectAppVisible(page.getByRole("button", { name: "generated-0000.ts" }))
|
||||||
expect(await readProbe(page)).toBe(PROBE)
|
expect(await readProbe(page)).toBe(PROBE)
|
||||||
|
|
||||||
|
const viewport = page.locator('#review-panel [data-slot="session-review-v2-sidebar-tree"] .scroll-view__viewport')
|
||||||
|
await viewport.hover()
|
||||||
|
await page.mouse.wheel(0, 100_000)
|
||||||
|
await expect
|
||||||
|
.poll(() => viewport.evaluate((element) => element.scrollHeight - element.clientHeight - element.scrollTop))
|
||||||
|
.toBeLessThanOrEqual(1)
|
||||||
|
await expect(page.getByRole("button", { name: "generated-2739.ts" })).toBeVisible()
|
||||||
})
|
})
|
||||||
|
|
||||||
type Probed = HTMLElement & { __e2eProbe?: string }
|
type Probed = HTMLElement & { __e2eProbe?: string }
|
||||||
@@ -80,16 +97,7 @@ async function setup(page: Page) {
|
|||||||
default: { providerID: "opencode", modelID: "test" },
|
default: { providerID: "opencode", modelID: "test" },
|
||||||
},
|
},
|
||||||
sessions: [session(sessionA, titleA, 1700000000000), session(sessionB, titleB, 1700000001000)],
|
sessions: [session(sessionA, titleA, 1700000000000), session(sessionB, titleB, 1700000001000)],
|
||||||
vcsDiff: [
|
vcsDiff: diffs,
|
||||||
{
|
|
||||||
file: "src/example.ts",
|
|
||||||
additions: 1,
|
|
||||||
deletions: 1,
|
|
||||||
status: "modified",
|
|
||||||
patch:
|
|
||||||
"diff --git a/src/example.ts b/src/example.ts\n--- a/src/example.ts\n+++ b/src/example.ts\n@@ -1 +1 @@\n-export const value = 'before'\n+export const value = 'after'\n",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
pageMessages: () => ({ items: [] }),
|
pageMessages: () => ({ items: [] }),
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -127,3 +135,13 @@ function session(id: string, title: string, created: number) {
|
|||||||
function sessionHref(sessionID: string) {
|
function sessionHref(sessionID: string) {
|
||||||
return `/server/${base64Encode(server)}/session/${sessionID}`
|
return `/server/${base64Encode(server)}/session/${sessionID}`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function fileDiff(file: string) {
|
||||||
|
return {
|
||||||
|
file,
|
||||||
|
additions: 1,
|
||||||
|
deletions: 1,
|
||||||
|
status: "modified",
|
||||||
|
patch: `diff --git a/${file} b/${file}\n--- a/${file}\n+++ b/${file}\n@@ -1 +1 @@\n-export const value = 'before'\n+export const value = 'after'\n`,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,263 @@
|
|||||||
|
import { expect, test, type Page } from "@playwright/test"
|
||||||
|
import { mockOpenCodeServer } from "../utils/mock-server"
|
||||||
|
import { expectSessionTitle } from "../utils/waits"
|
||||||
|
|
||||||
|
const directory = "C:/OpenCode/ReviewTerminalStacked"
|
||||||
|
const projectID = "proj_review_terminal_stacked"
|
||||||
|
const sessionID = "ses_review_terminal_stacked"
|
||||||
|
const title = "Review terminal stacked"
|
||||||
|
const branchDiffs = [
|
||||||
|
fileDiff(".github/actions/setup-bun/action.yml", 7),
|
||||||
|
...Array.from({ length: 2_739 }, (_, index) =>
|
||||||
|
fileDiff(
|
||||||
|
`src/branch/d${String(Math.floor(index / 100)).padStart(5, "0")}/generated-${String(index).padStart(4, "0")}.ts`,
|
||||||
|
100,
|
||||||
|
false,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
]
|
||||||
|
|
||||||
|
test("keeps the review tree and terminal sized when both panels are open", async ({ page }) => {
|
||||||
|
test.setTimeout(120_000)
|
||||||
|
const events: Array<{ directory: string; payload: Record<string, unknown> }> = []
|
||||||
|
let detailVersion = 1
|
||||||
|
let detailFailures = 1
|
||||||
|
await page.setViewportSize({ width: 1400, height: 900 })
|
||||||
|
await mockOpenCodeServer(page, {
|
||||||
|
directory,
|
||||||
|
project: {
|
||||||
|
id: projectID,
|
||||||
|
worktree: directory,
|
||||||
|
vcs: "git",
|
||||||
|
name: "review-terminal-stacked",
|
||||||
|
time: { created: 1700000000000, updated: 1700000000000 },
|
||||||
|
sandboxes: [],
|
||||||
|
},
|
||||||
|
provider: {
|
||||||
|
all: [
|
||||||
|
{
|
||||||
|
id: "opencode",
|
||||||
|
name: "OpenCode",
|
||||||
|
models: { test: { id: "test", name: "Test", limit: { context: 200_000 } } },
|
||||||
|
},
|
||||||
|
],
|
||||||
|
connected: ["opencode"],
|
||||||
|
default: { providerID: "opencode", modelID: "test" },
|
||||||
|
},
|
||||||
|
sessions: [
|
||||||
|
{
|
||||||
|
id: sessionID,
|
||||||
|
slug: "review-terminal-stacked",
|
||||||
|
projectID,
|
||||||
|
directory,
|
||||||
|
title,
|
||||||
|
version: "dev",
|
||||||
|
time: { created: 1700000000000, updated: 1700000000000 },
|
||||||
|
},
|
||||||
|
],
|
||||||
|
sessionStatus: { [sessionID]: { type: "idle" } },
|
||||||
|
pageMessages: () => ({ items: [] }),
|
||||||
|
events: () => events.splice(0, 1),
|
||||||
|
eventRetry: 16,
|
||||||
|
})
|
||||||
|
await page.route(/\/vcs(?:\?.*)?$/, (route) =>
|
||||||
|
route.fulfill({
|
||||||
|
status: 200,
|
||||||
|
contentType: "application/json",
|
||||||
|
body: JSON.stringify({ branch: "review-pane-performance", default_branch: "dev" }),
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
await page.route("**/vcs/diff**", (route) => {
|
||||||
|
const url = new URL(route.request().url())
|
||||||
|
const scope = url.searchParams.get("directory")?.replaceAll("\\", "/")
|
||||||
|
const detail = scope?.endsWith("/src/branch/d00027")
|
||||||
|
if (detail && detailFailures-- > 0) return route.fulfill({ status: 500, body: "retry detail" })
|
||||||
|
return route.fulfill({
|
||||||
|
status: 200,
|
||||||
|
contentType: "application/json",
|
||||||
|
body: JSON.stringify(
|
||||||
|
url.searchParams.get("mode") === "branch"
|
||||||
|
? detail
|
||||||
|
? branchDiffs
|
||||||
|
.filter((diff) => diff.file.startsWith("src/branch/d00027/"))
|
||||||
|
.map((diff) => fileDiff(diff.file, diff.additions, true, detailVersion))
|
||||||
|
: branchDiffs
|
||||||
|
: Array.from({ length: 7 }, (_, index) => fileDiff(`src/git-${index}.ts`, 1)),
|
||||||
|
),
|
||||||
|
})
|
||||||
|
})
|
||||||
|
await page.route("**/pty", (route) =>
|
||||||
|
route.fulfill({
|
||||||
|
status: 200,
|
||||||
|
contentType: "application/json",
|
||||||
|
body: JSON.stringify({ id: "pty_review_terminal", title: "Terminal 1" }),
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
await page.route("**/pty/pty_review_terminal", (route) =>
|
||||||
|
route.fulfill({ status: 200, contentType: "application/json", body: "{}" }),
|
||||||
|
)
|
||||||
|
await page.routeWebSocket("**/pty/pty_review_terminal/connect", () => undefined)
|
||||||
|
await page.addInitScript(() => {
|
||||||
|
localStorage.setItem("settings.v3", JSON.stringify({ general: { newLayoutDesigns: true } }))
|
||||||
|
localStorage.setItem(
|
||||||
|
"opencode.global.dat:layout",
|
||||||
|
JSON.stringify({ review: { diffStyle: "split", panelOpened: true } }),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
await page.goto(`/${base64Encode(directory)}/session/${sessionID}`)
|
||||||
|
await expectSessionTitle(page, title)
|
||||||
|
await expect(page.locator("#review-panel")).toBeVisible()
|
||||||
|
await expectTree(page, 8, "git-0.ts")
|
||||||
|
|
||||||
|
await selectMode(page, "Git changes", "Branch changes")
|
||||||
|
await expect(page.getByRole("tab", { name: "Review 2740" })).toBeVisible()
|
||||||
|
await page.keyboard.press("Control+Backquote")
|
||||||
|
await expect(page.locator("#terminal-panel")).toBeVisible()
|
||||||
|
await expectTree(page, 2_773, "action.yml")
|
||||||
|
await expectStackGeometry(page)
|
||||||
|
|
||||||
|
const treeViewport = page.locator('#review-panel [data-slot="session-review-v2-sidebar-tree"] .scroll-view__viewport')
|
||||||
|
await treeViewport.hover()
|
||||||
|
await page.mouse.wheel(0, 100_000)
|
||||||
|
await expect
|
||||||
|
.poll(() => treeViewport.evaluate((element) => element.scrollHeight - element.clientHeight - element.scrollTop))
|
||||||
|
.toBeLessThanOrEqual(1)
|
||||||
|
const lastFile = page.getByRole("button", { name: "generated-2738.ts" })
|
||||||
|
await expect(lastFile).toBeVisible()
|
||||||
|
const bottomGap = await lastFile.evaluate((element) => {
|
||||||
|
const viewport = element.closest<HTMLElement>(".scroll-view__viewport")!.getBoundingClientRect()
|
||||||
|
return viewport.bottom - element.getBoundingClientRect().bottom
|
||||||
|
})
|
||||||
|
expect(bottomGap).toBeGreaterThanOrEqual(0)
|
||||||
|
expect(bottomGap).toBeLessThanOrEqual(16)
|
||||||
|
const lazyDiff = page.waitForRequest((request) => {
|
||||||
|
const url = new URL(request.url())
|
||||||
|
return (
|
||||||
|
url.pathname === "/vcs/diff" &&
|
||||||
|
url.searchParams.get("directory")?.replaceAll("\\", "/").endsWith("/src/branch/d00027") === true
|
||||||
|
)
|
||||||
|
})
|
||||||
|
await lastFile.click()
|
||||||
|
await lazyDiff
|
||||||
|
const preview = page.locator('[data-slot="session-review-v2-diff-scroll"]')
|
||||||
|
await expect(preview).toContainText("after-1")
|
||||||
|
detailVersion = 2
|
||||||
|
events.push(statusEvent("busy"))
|
||||||
|
await expect(page.getByRole("button", { name: "Stop" })).toBeVisible()
|
||||||
|
const refreshedDiff = page.waitForRequest((request) => {
|
||||||
|
const url = new URL(request.url())
|
||||||
|
return (
|
||||||
|
url.pathname === "/vcs/diff" &&
|
||||||
|
url.searchParams.get("directory")?.replaceAll("\\", "/").endsWith("/src/branch/d00027") === true
|
||||||
|
)
|
||||||
|
})
|
||||||
|
events.push(statusEvent("idle"))
|
||||||
|
await refreshedDiff
|
||||||
|
await expect(preview).toContainText("after-2")
|
||||||
|
await selectMode(page, "Branch changes", "Git changes")
|
||||||
|
await expectTree(page, 8, "git-0.ts")
|
||||||
|
await page.getByRole("button", { name: "git-0.ts" }).click()
|
||||||
|
await selectMode(page, "Git changes", "Branch changes")
|
||||||
|
await expectTree(page, 2_773, "action.yml")
|
||||||
|
|
||||||
|
const filter = page.getByRole("searchbox", { name: "Filter files" })
|
||||||
|
await filter.fill("generated-2738")
|
||||||
|
await expectTree(page, 1, "generated-2738.ts")
|
||||||
|
await filter.fill("")
|
||||||
|
await expectTree(page, 2_773, "action.yml")
|
||||||
|
|
||||||
|
await page.getByRole("button", { name: "Toggle file tree" }).click()
|
||||||
|
await expect(page.locator('[data-slot="session-review-v2-sidebar"]')).toHaveCount(0)
|
||||||
|
await expect(page.locator('#review-panel [data-component="file-tree-v2"]')).toHaveCount(0)
|
||||||
|
await page.getByRole("button", { name: "Toggle file tree" }).click()
|
||||||
|
await expectTree(page, 2_773, "action.yml")
|
||||||
|
|
||||||
|
await page.keyboard.press("Control+Backquote")
|
||||||
|
await expect(page.locator("#terminal-panel")).toHaveCount(0)
|
||||||
|
await expectTree(page, 2_773, "action.yml")
|
||||||
|
await page.keyboard.press("Control+Backquote")
|
||||||
|
await expect(page.locator("#terminal-panel")).toBeVisible()
|
||||||
|
await expectTree(page, 2_773, "action.yml")
|
||||||
|
|
||||||
|
await page.getByRole("button", { name: "Toggle review" }).click()
|
||||||
|
await expect(page.locator("#review-panel")).toHaveCount(0)
|
||||||
|
await page.getByRole("button", { name: "Toggle review" }).click()
|
||||||
|
await expectTree(page, 2_773, "action.yml")
|
||||||
|
await page.setViewportSize({ width: 1_000, height: 700 })
|
||||||
|
await expectTree(page, 2_773, "action.yml")
|
||||||
|
await expectStackGeometry(page)
|
||||||
|
await page.setViewportSize({ width: 1_000, height: 120 })
|
||||||
|
await page.setViewportSize({ width: 1_400, height: 900 })
|
||||||
|
await expectTree(page, 2_773, "action.yml")
|
||||||
|
await expectStackGeometry(page)
|
||||||
|
})
|
||||||
|
|
||||||
|
async function selectMode(page: Page, current: string, next: string) {
|
||||||
|
await page.getByRole("button", { name: current }).click()
|
||||||
|
const option = page.getByRole("option", { name: next })
|
||||||
|
await expect(option).toBeVisible()
|
||||||
|
await option.click()
|
||||||
|
}
|
||||||
|
|
||||||
|
async function expectTree(page: Page, total: number, file: string) {
|
||||||
|
await expectMountedTree(page, total)
|
||||||
|
await expect(page.getByRole("button", { name: file })).toBeVisible()
|
||||||
|
}
|
||||||
|
|
||||||
|
async function expectMountedTree(page: Page, total: number) {
|
||||||
|
const tree = page.locator('#review-panel [data-component="file-tree-v2"]')
|
||||||
|
await expect(tree).toHaveAttribute("data-total-rows", String(total))
|
||||||
|
await expect
|
||||||
|
.poll(() => tree.evaluate((element) => element.querySelectorAll('[data-slot="file-tree-v2-row"]').length))
|
||||||
|
.toBeGreaterThan(0)
|
||||||
|
const state = await tree.evaluate((element) => ({
|
||||||
|
root: element.getBoundingClientRect().height,
|
||||||
|
viewport: element.closest<HTMLElement>(".scroll-view__viewport")!.getBoundingClientRect().height,
|
||||||
|
rows: element.querySelectorAll('[data-slot="file-tree-v2-row"]').length,
|
||||||
|
}))
|
||||||
|
expect(state.viewport).toBeGreaterThan(0)
|
||||||
|
expect(state.root).toBeGreaterThan(0)
|
||||||
|
expect(state.rows).toBeGreaterThan(0)
|
||||||
|
expect(state.rows).toBeLessThanOrEqual(60)
|
||||||
|
}
|
||||||
|
|
||||||
|
async function expectStackGeometry(page: Page) {
|
||||||
|
const geometry = await page.evaluate(() => {
|
||||||
|
const review = document.querySelector<HTMLElement>("#review-panel")!
|
||||||
|
const terminal = document.querySelector<HTMLElement>("#terminal-panel")!
|
||||||
|
const reviewParent = review.parentElement!.getBoundingClientRect()
|
||||||
|
const terminalParent = terminal.parentElement!.getBoundingClientRect()
|
||||||
|
return {
|
||||||
|
review: review.getBoundingClientRect().height,
|
||||||
|
reviewParent: reviewParent.height,
|
||||||
|
terminal: terminal.getBoundingClientRect().height,
|
||||||
|
terminalParent: terminalParent.height,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
expect(Math.abs(geometry.review - geometry.reviewParent)).toBeLessThanOrEqual(1)
|
||||||
|
expect(Math.abs(geometry.terminal - geometry.terminalParent)).toBeLessThanOrEqual(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
function base64Encode(value: string) {
|
||||||
|
return Buffer.from(value, "utf8").toString("base64").replace(/\+/g, "-").replace(/\//g, "_").replace(/=/g, "")
|
||||||
|
}
|
||||||
|
|
||||||
|
function statusEvent(type: "busy" | "idle") {
|
||||||
|
return {
|
||||||
|
directory,
|
||||||
|
payload: { type: "session.status", properties: { sessionID, status: { type } } },
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function fileDiff(file: string, additions: number, loaded = true, version = 1) {
|
||||||
|
return {
|
||||||
|
file,
|
||||||
|
additions,
|
||||||
|
deletions: 0,
|
||||||
|
status: "modified",
|
||||||
|
patch: loaded
|
||||||
|
? `diff --git a/${file} b/${file}\n--- a/${file}\n+++ b/${file}\n@@ -1 +1 @@\n-export const value = 'before'\n+export const value = 'after-${version}'\n`
|
||||||
|
: `diff --git a/${file} b/${file}\n--- a/${file}\n+++ b/${file}`,
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,220 @@
|
|||||||
|
import { base64Encode } from "@opencode-ai/core/util/encode"
|
||||||
|
import { expect, test, type Page } from "@playwright/test"
|
||||||
|
import {
|
||||||
|
assistantMessage,
|
||||||
|
directory,
|
||||||
|
messageUpdated,
|
||||||
|
project,
|
||||||
|
session,
|
||||||
|
sessionID,
|
||||||
|
status,
|
||||||
|
textPart,
|
||||||
|
title,
|
||||||
|
userID,
|
||||||
|
userMessage,
|
||||||
|
} from "../performance/timeline-stability/fixture"
|
||||||
|
import { mockOpenCodeServer } from "../utils/mock-server"
|
||||||
|
import { installSseTransport } from "../utils/sse-transport"
|
||||||
|
import { expectSessionTitle } from "../utils/waits"
|
||||||
|
|
||||||
|
const assistants = Array.from({ length: 14 }, (_, index) =>
|
||||||
|
assistantMessage([textPart(`prt_history_root_${index}`, `Assistant response ${index}`)], {
|
||||||
|
id: `msg_${String(index + 1001).padStart(4, "0")}_history_root_assistant`,
|
||||||
|
parentID: userID,
|
||||||
|
created: 1700000001000 + index * 1_000,
|
||||||
|
completed: index < 13,
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
const messages = [userMessage(), ...assistants]
|
||||||
|
const lastAssistant = assistants.at(-1)!
|
||||||
|
const lastPartID = assistants.at(-1)!.parts[0]!.id
|
||||||
|
const userPartID = `prt_${userID}_text`
|
||||||
|
const completed = {
|
||||||
|
...lastAssistant.info,
|
||||||
|
time: { ...lastAssistant.info.time, completed: lastAssistant.info.time.created + 15_000 },
|
||||||
|
}
|
||||||
|
const scenarios = [
|
||||||
|
{ name: "completion", info: completed, idleFirst: false, interrupted: false },
|
||||||
|
{
|
||||||
|
name: "interruption",
|
||||||
|
info: { ...completed, error: { name: "MessageAbortedError", data: { message: "Stopped" } } },
|
||||||
|
idleFirst: true,
|
||||||
|
interrupted: true,
|
||||||
|
},
|
||||||
|
] as const
|
||||||
|
|
||||||
|
test.use({ viewport: { width: 646, height: 1385 } })
|
||||||
|
|
||||||
|
for (const scenario of scenarios) {
|
||||||
|
test(`keeps the latest user turn visible through ${scenario.name}`, async ({ page }) => {
|
||||||
|
const requests: { before?: string; phase: "start" | "end" }[] = []
|
||||||
|
const pages: { before?: string; limit: number }[] = []
|
||||||
|
const roots: { sessionID: string; messageID: string }[] = []
|
||||||
|
const sequence: string[] = []
|
||||||
|
const history = Promise.withResolvers<void>()
|
||||||
|
const transport = await installSseTransport<{ directory: string; payload: Record<string, unknown> }>(page, {
|
||||||
|
server: `http://${process.env.PLAYWRIGHT_SERVER_HOST ?? "127.0.0.1"}:${process.env.PLAYWRIGHT_SERVER_PORT ?? "4096"}`,
|
||||||
|
retry: 20,
|
||||||
|
})
|
||||||
|
await mockOpenCodeServer(page, {
|
||||||
|
directory,
|
||||||
|
project: project(),
|
||||||
|
provider: {
|
||||||
|
all: [
|
||||||
|
{
|
||||||
|
id: "opencode",
|
||||||
|
name: "OpenCode",
|
||||||
|
models: {
|
||||||
|
"claude-opus-4-6": {
|
||||||
|
id: "claude-opus-4-6",
|
||||||
|
name: "Claude Opus 4.6",
|
||||||
|
limit: { context: 200_000 },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
connected: ["opencode"],
|
||||||
|
default: { providerID: "opencode", modelID: "claude-opus-4-6" },
|
||||||
|
},
|
||||||
|
sessions: [session()],
|
||||||
|
sessionStatus: { [sessionID]: { type: "busy" } },
|
||||||
|
beforeMessagesResponse: (request) => (request.before ? history.promise : Promise.resolve()),
|
||||||
|
onMessages: (request) => {
|
||||||
|
requests.push(request)
|
||||||
|
sequence.push(`messages:${request.phase}:${request.before ?? "latest"}`)
|
||||||
|
},
|
||||||
|
onMessage: (request) => {
|
||||||
|
roots.push(request)
|
||||||
|
sequence.push(`message:${request.messageID}`)
|
||||||
|
},
|
||||||
|
message: (requestedSessionID, messageID) => {
|
||||||
|
if (requestedSessionID !== sessionID) return
|
||||||
|
return messages.find((item) => item.info.id === messageID)
|
||||||
|
},
|
||||||
|
pageMessages: (_, limit, before) => {
|
||||||
|
pages.push({ before, limit })
|
||||||
|
const end = before ? messages.findIndex((message) => message.info.id === before) : messages.length
|
||||||
|
const start = Math.max(0, end - limit)
|
||||||
|
return {
|
||||||
|
items: messages.slice(start, end),
|
||||||
|
cursor: start > 0 ? messages[start]!.info.id : undefined,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
})
|
||||||
|
await page.addInitScript(
|
||||||
|
({ userPartID, lastPartID }) => {
|
||||||
|
const state = { armed: false, hidden: false, samples: 0, stop: false }
|
||||||
|
;(window as Window & { __historyRootProbe?: typeof state }).__historyRootProbe = state
|
||||||
|
const sample = () => {
|
||||||
|
if (state.armed) {
|
||||||
|
const virtual = document.querySelector<HTMLElement>("[data-timeline-virtual-content]")
|
||||||
|
const viewport = virtual?.closest<HTMLElement>(".scroll-view__viewport")
|
||||||
|
const view = viewport?.getBoundingClientRect()
|
||||||
|
const visible = (partID: string) => {
|
||||||
|
const part = viewport?.querySelector<HTMLElement>(`[data-timeline-part-id="${partID}"]`)
|
||||||
|
const rect = part?.getBoundingClientRect()
|
||||||
|
return (
|
||||||
|
!!rect &&
|
||||||
|
!!view &&
|
||||||
|
rect.width > 0 &&
|
||||||
|
rect.height > 0 &&
|
||||||
|
rect.bottom > view.top &&
|
||||||
|
rect.top < view.bottom
|
||||||
|
)
|
||||||
|
}
|
||||||
|
if (!virtual || !visible(userPartID) || !visible(lastPartID)) state.hidden = true
|
||||||
|
state.samples++
|
||||||
|
}
|
||||||
|
if (!state.stop) requestAnimationFrame(() => setTimeout(sample, 0))
|
||||||
|
}
|
||||||
|
requestAnimationFrame(() => setTimeout(sample, 0))
|
||||||
|
},
|
||||||
|
{ userPartID, lastPartID },
|
||||||
|
)
|
||||||
|
|
||||||
|
await page.goto(`/${base64Encode(directory)}/session/${sessionID}`)
|
||||||
|
await transport.waitForConnection()
|
||||||
|
await expectSessionTitle(page, title)
|
||||||
|
await expect(page.locator(`[data-timeline-part-id="${lastPartID}"]`)).toBeVisible()
|
||||||
|
await expect(page.locator(`[data-timeline-part-id="${userPartID}"]`)).toBeVisible()
|
||||||
|
await expect.poll(() => requests.filter((request) => request.phase === "start").length).toBe(2)
|
||||||
|
expect(requests.filter((request) => request.phase === "end")).toHaveLength(1)
|
||||||
|
expect(sequence.slice(0, 4)).toEqual([
|
||||||
|
"messages:start:latest",
|
||||||
|
"messages:end:latest",
|
||||||
|
`message:${userID}`,
|
||||||
|
`messages:start:${messages.at(-2)!.info.id}`,
|
||||||
|
])
|
||||||
|
await page.evaluate(() => {
|
||||||
|
;(
|
||||||
|
window as Window & {
|
||||||
|
__historyRootProbe?: { armed: boolean }
|
||||||
|
}
|
||||||
|
).__historyRootProbe!.armed = true
|
||||||
|
})
|
||||||
|
await waitForProbeSamples(page, 0)
|
||||||
|
expect(await historyRootHidden(page)).toBe(false)
|
||||||
|
const beforeHistory = await probeSamples(page)
|
||||||
|
history.resolve()
|
||||||
|
await expect(page.locator('[data-timeline-part-id^="prt_history_root_"]')).toHaveCount(14)
|
||||||
|
await expect(page.getByRole("button", { name: "Stop" })).toBeVisible()
|
||||||
|
await waitForProbeSamples(page, beforeHistory)
|
||||||
|
expect(pages[0]).toEqual({ before: undefined, limit: 2 })
|
||||||
|
expect(roots).toEqual([{ sessionID, messageID: userID }])
|
||||||
|
|
||||||
|
const message = messageUpdated(scenario.info)
|
||||||
|
const idle = status("idle")
|
||||||
|
for (const event of scenario.idleFirst ? [idle, message] : [message, idle]) {
|
||||||
|
const beforeEvent = await probeSamples(page)
|
||||||
|
await transport.send(event)
|
||||||
|
if (event === idle) await expect(page.getByRole("button", { name: "Stop" })).toHaveCount(0)
|
||||||
|
if (event === message && scenario.interrupted)
|
||||||
|
await expect(page.getByText("Interrupted", { exact: true })).toBeVisible()
|
||||||
|
await waitForProbeSamples(page, beforeEvent)
|
||||||
|
const current = await timelineState(page)
|
||||||
|
expect(current, JSON.stringify(current)).toMatchObject({ virtual: true })
|
||||||
|
expect(current.rows, JSON.stringify(current)).toBeGreaterThan(0)
|
||||||
|
}
|
||||||
|
|
||||||
|
expect(requests[0]).toEqual({ before: undefined, phase: "start", sessionID })
|
||||||
|
expect(requests[1]).toEqual({ before: undefined, phase: "end", sessionID })
|
||||||
|
await expect(page.getByRole("button", { name: "Stop" })).toHaveCount(0)
|
||||||
|
await expect(page.locator('[data-timeline-row="bottom-spacer"]')).toBeVisible()
|
||||||
|
if (scenario.interrupted) await expect(page.getByText("Interrupted", { exact: true })).toBeVisible()
|
||||||
|
expect(
|
||||||
|
await page.evaluate(() => {
|
||||||
|
const state = (window as Window & { __historyRootProbe?: { hidden: boolean; stop: boolean } })
|
||||||
|
.__historyRootProbe!
|
||||||
|
state.stop = true
|
||||||
|
return state.hidden
|
||||||
|
}),
|
||||||
|
).toBe(false)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function timelineState(page: Page) {
|
||||||
|
return page.evaluate(() => ({
|
||||||
|
virtual: !!document.querySelector("[data-timeline-virtual-content]"),
|
||||||
|
rows: document.querySelectorAll("[data-timeline-key]").length,
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
function probeSamples(page: Page) {
|
||||||
|
return page.evaluate(
|
||||||
|
() => (window as Window & { __historyRootProbe?: { samples: number } }).__historyRootProbe!.samples,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
async function waitForProbeSamples(page: Page, after: number) {
|
||||||
|
await page.waitForFunction(
|
||||||
|
(after) =>
|
||||||
|
(window as Window & { __historyRootProbe?: { samples: number } }).__historyRootProbe!.samples >= after + 3,
|
||||||
|
after,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function historyRootHidden(page: Page) {
|
||||||
|
return page.evaluate(
|
||||||
|
() => (window as Window & { __historyRootProbe?: { hidden: boolean } }).__historyRootProbe!.hidden,
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,89 @@
|
|||||||
|
import { base64Encode } from "@opencode-ai/core/util/encode"
|
||||||
|
import { expect, test } from "@playwright/test"
|
||||||
|
import { mockOpenCodeServer } from "../utils/mock-server"
|
||||||
|
import { expectSessionTitle } from "../utils/waits"
|
||||||
|
|
||||||
|
const directory = "C:/OpenCode/TerminalComposerFocus"
|
||||||
|
const projectID = "proj_terminal_composer_focus"
|
||||||
|
const sessionID = "ses_terminal_composer_focus"
|
||||||
|
const ptyID = "pty_terminal_composer_focus"
|
||||||
|
|
||||||
|
test.use({ viewport: { width: 1440, height: 900 } })
|
||||||
|
|
||||||
|
test("routes typing to the composer unless the open terminal is focused", async ({ page }) => {
|
||||||
|
await mockOpenCodeServer(page, {
|
||||||
|
directory,
|
||||||
|
project: {
|
||||||
|
id: projectID,
|
||||||
|
worktree: directory,
|
||||||
|
vcs: "git",
|
||||||
|
name: "terminal-composer-focus",
|
||||||
|
time: { created: 1700000000000, updated: 1700000000000 },
|
||||||
|
sandboxes: [],
|
||||||
|
},
|
||||||
|
provider: {
|
||||||
|
all: [
|
||||||
|
{
|
||||||
|
id: "opencode",
|
||||||
|
name: "OpenCode",
|
||||||
|
models: { test: { id: "test", name: "Test", limit: { context: 200_000 } } },
|
||||||
|
},
|
||||||
|
],
|
||||||
|
connected: ["opencode"],
|
||||||
|
default: { providerID: "opencode", modelID: "test" },
|
||||||
|
},
|
||||||
|
sessions: [
|
||||||
|
{
|
||||||
|
id: sessionID,
|
||||||
|
slug: "terminal-composer-focus",
|
||||||
|
projectID,
|
||||||
|
directory,
|
||||||
|
title: "Terminal composer focus",
|
||||||
|
version: "dev",
|
||||||
|
time: { created: 1700000000000, updated: 1700000000000 },
|
||||||
|
},
|
||||||
|
],
|
||||||
|
pageMessages: () => ({ items: [] }),
|
||||||
|
})
|
||||||
|
await page.route("**/pty", (route) =>
|
||||||
|
route.fulfill({
|
||||||
|
status: 200,
|
||||||
|
contentType: "application/json",
|
||||||
|
body: JSON.stringify({ id: ptyID, title: "Terminal 1" }),
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
await page.route(`**/pty/${ptyID}`, (route) =>
|
||||||
|
route.fulfill({ status: 200, contentType: "application/json", body: "{}" }),
|
||||||
|
)
|
||||||
|
await page.route(`**/pty/${ptyID}/connect-token*`, (route) =>
|
||||||
|
route.fulfill({
|
||||||
|
status: 200,
|
||||||
|
contentType: "application/json",
|
||||||
|
headers: { "access-control-allow-origin": "*" },
|
||||||
|
body: JSON.stringify({ ticket: "e2e-ticket" }),
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
await page.routeWebSocket(new RegExp(`/pty/${ptyID}/connect`), () => undefined)
|
||||||
|
await page.addInitScript(() => {
|
||||||
|
localStorage.setItem("settings.v3", JSON.stringify({ general: { newLayoutDesigns: true } }))
|
||||||
|
})
|
||||||
|
|
||||||
|
await page.goto(`/${base64Encode(directory)}/session/${sessionID}`)
|
||||||
|
await expectSessionTitle(page, "Terminal composer focus")
|
||||||
|
|
||||||
|
const composer = page.locator('[data-component="prompt-input"]')
|
||||||
|
const terminal = page.locator('[data-component="terminal"]')
|
||||||
|
await page.keyboard.press("Control+Backquote")
|
||||||
|
await expect(terminal).toBeVisible()
|
||||||
|
await expect.poll(() => terminal.evaluate((element) => element.contains(document.activeElement))).toBe(true)
|
||||||
|
|
||||||
|
await page.keyboard.type("x")
|
||||||
|
await expect(composer).toHaveText("")
|
||||||
|
|
||||||
|
await page.waitForTimeout(300)
|
||||||
|
await page.evaluate(() => (document.activeElement as HTMLElement | null)?.blur())
|
||||||
|
await page.keyboard.type("a")
|
||||||
|
|
||||||
|
await expect(composer).toBeFocused()
|
||||||
|
await expect(composer).toHaveText("a")
|
||||||
|
})
|
||||||
@@ -7,7 +7,7 @@ const projectID = "proj_hidden_terminal_regression"
|
|||||||
const sessionID = "ses_hidden_terminal_regression"
|
const sessionID = "ses_hidden_terminal_regression"
|
||||||
const title = "Hidden terminal regression"
|
const title = "Hidden terminal regression"
|
||||||
|
|
||||||
test("unmounts the terminal renderer while the pane is hidden", async ({ page }) => {
|
test("unmounts the terminal panel while it is hidden", async ({ page }) => {
|
||||||
await page.setViewportSize({ width: 1400, height: 900 })
|
await page.setViewportSize({ width: 1400, height: 900 })
|
||||||
await mockOpenCodeServer(page, {
|
await mockOpenCodeServer(page, {
|
||||||
directory,
|
directory,
|
||||||
@@ -64,13 +64,14 @@ test("unmounts the terminal renderer while the pane is hidden", async ({ page })
|
|||||||
await expect(page.locator('[data-component="terminal"]')).toBeVisible()
|
await expect(page.locator('[data-component="terminal"]')).toBeVisible()
|
||||||
|
|
||||||
await page.keyboard.press("Control+Backquote")
|
await page.keyboard.press("Control+Backquote")
|
||||||
await expect(panel).toHaveAttribute("aria-hidden", "true")
|
await expect(panel).toHaveCount(0)
|
||||||
await expect(page.locator('[data-component="terminal"]')).toHaveCount(0)
|
await expect(page.locator('[data-component="terminal"]')).toHaveCount(0)
|
||||||
|
|
||||||
await page.setViewportSize({ width: 1200, height: 700 })
|
await page.setViewportSize({ width: 1200, height: 700 })
|
||||||
await expect(page.locator('[data-component="terminal"]')).toHaveCount(0)
|
await expect(page.locator('[data-component="terminal"]')).toHaveCount(0)
|
||||||
|
|
||||||
await page.keyboard.press("Control+Backquote")
|
await page.keyboard.press("Control+Backquote")
|
||||||
|
await expect(panel).toBeVisible()
|
||||||
await expect(page.locator('[data-component="terminal"]')).toBeVisible()
|
await expect(page.locator('[data-component="terminal"]')).toBeVisible()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -10,6 +10,7 @@
|
|||||||
"./performance/timeline-stability/fixture.test.ts",
|
"./performance/timeline-stability/fixture.test.ts",
|
||||||
"./performance/timeline-stability/fixture.ts",
|
"./performance/timeline-stability/fixture.ts",
|
||||||
"./performance/unit/visual-stability.test.ts",
|
"./performance/unit/visual-stability.test.ts",
|
||||||
|
"./regression/new-session-panel-corner.spec.ts",
|
||||||
"./regression/session-timeline-context-resize.spec.ts",
|
"./regression/session-timeline-context-resize.spec.ts",
|
||||||
"./utils/**/*.ts"
|
"./utils/**/*.ts"
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import type { Page, Route } from "@playwright/test"
|
import type { Page, Route } from "@playwright/test"
|
||||||
|
|
||||||
const emptyList = new Set(["/skill", "/command", "/lsp", "/formatter", "/vcs/status", "/vcs/diff"])
|
const emptyList = new Set(["/skill", "/command", "/lsp", "/formatter", "/vcs/status", "/vcs/diff"])
|
||||||
const emptyObject = new Set(["/global/config", "/config", "/provider/auth", "/mcp"])
|
const emptyObject = new Set(["/global/config", "/config", "/provider/auth", "/mcp", "/experimental/resource"])
|
||||||
|
|
||||||
export interface MockServerConfig {
|
export interface MockServerConfig {
|
||||||
provider: unknown
|
provider: unknown
|
||||||
@@ -11,7 +11,10 @@ export interface MockServerConfig {
|
|||||||
pageMessages: (sessionId: string, limit: number, before?: string) => { items: unknown[]; cursor?: string }
|
pageMessages: (sessionId: string, limit: number, before?: string) => { items: unknown[]; cursor?: string }
|
||||||
vcsDiff?: unknown[]
|
vcsDiff?: unknown[]
|
||||||
messageDelay?: number
|
messageDelay?: number
|
||||||
|
beforeMessagesResponse?: (input: { sessionID: string; before?: string }) => Promise<void>
|
||||||
onMessages?: (input: { sessionID: string; before?: string; phase: "start" | "end" }) => void
|
onMessages?: (input: { sessionID: string; before?: string; phase: "start" | "end" }) => void
|
||||||
|
message?: (sessionID: string, messageID: string) => unknown
|
||||||
|
onMessage?: (input: { sessionID: string; messageID: string }) => void
|
||||||
events?: () => unknown[]
|
events?: () => unknown[]
|
||||||
eventRetry?: number
|
eventRetry?: number
|
||||||
todos?: (sessionID: string) => unknown[]
|
todos?: (sessionID: string) => unknown[]
|
||||||
@@ -19,6 +22,7 @@ export interface MockServerConfig {
|
|||||||
questions?: unknown[] | (() => unknown[])
|
questions?: unknown[] | (() => unknown[])
|
||||||
fileList?: (path: string) => unknown | Promise<unknown>
|
fileList?: (path: string) => unknown | Promise<unknown>
|
||||||
fileContent?: (path: string) => unknown | Promise<unknown>
|
fileContent?: (path: string) => unknown | Promise<unknown>
|
||||||
|
findFiles?: (input: { query: string; dirs?: string; limit?: number }) => unknown
|
||||||
sessionStatus?: unknown
|
sessionStatus?: unknown
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -52,6 +56,7 @@ export async function mockOpenCodeServer(page: Page, config: MockServerConfig) {
|
|||||||
const path = url.pathname
|
const path = url.pathname
|
||||||
if (path === "/global/event" || path === "/event") return sse(route, config.events?.(), config.eventRetry)
|
if (path === "/global/event" || path === "/event") return sse(route, config.events?.(), config.eventRetry)
|
||||||
if (path === "/global/health") return json(route, { healthy: true })
|
if (path === "/global/health") return json(route, { healthy: true })
|
||||||
|
if (path === "/experimental/capabilities") return json(route, { backgroundSubagents: false })
|
||||||
if (path === "/permission")
|
if (path === "/permission")
|
||||||
return json(route, typeof config.permissions === "function" ? config.permissions() : (config.permissions ?? []))
|
return json(route, typeof config.permissions === "function" ? config.permissions() : (config.permissions ?? []))
|
||||||
if (path === "/question")
|
if (path === "/question")
|
||||||
@@ -62,6 +67,23 @@ export async function mockOpenCodeServer(page: Page, config: MockServerConfig) {
|
|||||||
return json(route, await config.fileList(url.searchParams.get("path") ?? ""))
|
return json(route, await config.fileList(url.searchParams.get("path") ?? ""))
|
||||||
if (path === "/file/content" && config.fileContent)
|
if (path === "/file/content" && config.fileContent)
|
||||||
return json(route, await config.fileContent(url.searchParams.get("path") ?? ""))
|
return json(route, await config.fileContent(url.searchParams.get("path") ?? ""))
|
||||||
|
if (path === "/find/file" && config.findFiles)
|
||||||
|
return json(
|
||||||
|
route,
|
||||||
|
await config.findFiles({
|
||||||
|
query: url.searchParams.get("query") ?? "",
|
||||||
|
dirs: url.searchParams.get("dirs") ?? undefined,
|
||||||
|
limit: url.searchParams.has("limit") ? Number(url.searchParams.get("limit")) : undefined,
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
if (path === "/api/reference")
|
||||||
|
return json(route, {
|
||||||
|
location: {
|
||||||
|
directory: config.directory,
|
||||||
|
project: { id: (config.project as { id?: string }).id, directory: config.directory },
|
||||||
|
},
|
||||||
|
data: [],
|
||||||
|
})
|
||||||
if (emptyObject.has(path)) return json(route, {})
|
if (emptyObject.has(path)) return json(route, {})
|
||||||
if (emptyList.has(path)) return json(route, [])
|
if (emptyList.has(path)) return json(route, [])
|
||||||
if (path in staticRoutes) return json(route, staticRoutes[path])
|
if (path in staticRoutes) return json(route, staticRoutes[path])
|
||||||
@@ -72,6 +94,18 @@ export async function mockOpenCodeServer(page: Page, config: MockServerConfig) {
|
|||||||
return json(route, session ?? {})
|
return json(route, session ?? {})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const projectMatch = path.match(/^\/project\/([^/]+)$/)
|
||||||
|
if (projectMatch) return json(route, config.project)
|
||||||
|
|
||||||
|
const messageMatch = path.match(/^\/session\/([^/]+)\/message\/([^/]+)$/)
|
||||||
|
if (messageMatch) {
|
||||||
|
config.onMessage?.({ sessionID: messageMatch[1]!, messageID: messageMatch[2]! })
|
||||||
|
if (config.messageDelay !== undefined) await new Promise((resolve) => setTimeout(resolve, config.messageDelay))
|
||||||
|
const message = config.message?.(messageMatch[1]!, messageMatch[2]!)
|
||||||
|
if (message === undefined) return json(route, { error: "Message not found" }, undefined, 404)
|
||||||
|
return json(route, message)
|
||||||
|
}
|
||||||
|
|
||||||
const todoMatch = path.match(/^\/session\/([^/]+)\/todo$/)
|
const todoMatch = path.match(/^\/session\/([^/]+)\/todo$/)
|
||||||
if (todoMatch) return json(route, config.todos?.(todoMatch[1]!) ?? [])
|
if (todoMatch) return json(route, config.todos?.(todoMatch[1]!) ?? [])
|
||||||
if (/^\/session\/[^/]+\/(children|diff)$/.test(path)) return json(route, [])
|
if (/^\/session\/[^/]+\/(children|diff)$/.test(path)) return json(route, [])
|
||||||
@@ -82,7 +116,8 @@ export async function mockOpenCodeServer(page: Page, config: MockServerConfig) {
|
|||||||
const before = token ? cursors.get(token) : undefined
|
const before = token ? cursors.get(token) : undefined
|
||||||
if (token && !before) return json(route, { error: "Invalid cursor" }, undefined, 400)
|
if (token && !before) return json(route, { error: "Invalid cursor" }, undefined, 400)
|
||||||
config.onMessages?.({ sessionID: messagesMatch[1], before, phase: "start" })
|
config.onMessages?.({ sessionID: messagesMatch[1], before, phase: "start" })
|
||||||
if (config.messageDelay) await new Promise((resolve) => setTimeout(resolve, config.messageDelay))
|
await config.beforeMessagesResponse?.({ sessionID: messagesMatch[1]!, before })
|
||||||
|
if (config.messageDelay !== undefined) await new Promise((resolve) => setTimeout(resolve, config.messageDelay))
|
||||||
const limit = Number(url.searchParams.get("limit") ?? 80)
|
const limit = Number(url.searchParams.get("limit") ?? 80)
|
||||||
const pageData = config.pageMessages(messagesMatch[1], limit, before)
|
const pageData = config.pageMessages(messagesMatch[1], limit, before)
|
||||||
config.onMessages?.({ sessionID: messagesMatch[1], before, phase: "end" })
|
config.onMessages?.({ sessionID: messagesMatch[1], before, phase: "end" })
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@opencode-ai/app",
|
"name": "@opencode-ai/app",
|
||||||
"version": "1.17.13",
|
"version": "1.17.15",
|
||||||
"description": "",
|
"description": "",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"exports": {
|
"exports": {
|
||||||
|
|||||||
+124
-51
@@ -9,9 +9,10 @@ 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/context"
|
import { ThemeProvider } from "@opencode-ai/ui/theme/context"
|
||||||
import { MetaProvider } from "@solidjs/meta"
|
import { MetaProvider } from "@solidjs/meta"
|
||||||
import { type BaseRouterProps, Navigate, Route, Router, useParams, useSearchParams } from "@solidjs/router"
|
import { type BaseRouterProps, Navigate, Route, Router, useNavigate, useParams, useSearchParams } from "@solidjs/router"
|
||||||
import { QueryClient, QueryClientProvider } from "@tanstack/solid-query"
|
import { QueryClient, QueryClientProvider } from "@tanstack/solid-query"
|
||||||
import { Effect } from "effect"
|
import { Effect } from "effect"
|
||||||
|
import { base64Encode } from "@opencode-ai/core/util/encode"
|
||||||
import {
|
import {
|
||||||
type Component,
|
type Component,
|
||||||
createEffect,
|
createEffect,
|
||||||
@@ -32,7 +33,7 @@ import { CommandProvider, useCommand, type CommandOption } from "@/context/comma
|
|||||||
import { CommentsProvider } from "@/context/comments"
|
import { CommentsProvider } from "@/context/comments"
|
||||||
import { FileProvider } from "@/context/file"
|
import { FileProvider } from "@/context/file"
|
||||||
import { ServerSDKProvider } from "@/context/server-sdk"
|
import { ServerSDKProvider } from "@/context/server-sdk"
|
||||||
import { ServerSyncProvider } from "@/context/server-sync"
|
import { ServerSyncProvider, useServerSync } from "@/context/server-sync"
|
||||||
import { GlobalProvider, useGlobal } from "@/context/global"
|
import { GlobalProvider, useGlobal } from "@/context/global"
|
||||||
import { HighlightsProvider } from "@/context/highlights"
|
import { HighlightsProvider } from "@/context/highlights"
|
||||||
import { LanguageProvider, type Locale, useLanguage } from "@/context/language"
|
import { LanguageProvider, type Locale, useLanguage } from "@/context/language"
|
||||||
@@ -52,9 +53,10 @@ import LegacyLayout from "@/pages/layout"
|
|||||||
import NewLayout from "@/pages/layout-new"
|
import NewLayout from "@/pages/layout-new"
|
||||||
import { ErrorPage } from "./pages/error"
|
import { ErrorPage } from "./pages/error"
|
||||||
import { useCheckServerHealth } from "./utils/server-health"
|
import { useCheckServerHealth } from "./utils/server-health"
|
||||||
import { legacySessionServer, requireServerKey, sessionHref } from "./utils/session-route"
|
import { legacySessionHref, legacySessionServer, requireServerKey, sessionHref } from "./utils/session-route"
|
||||||
|
import { createSessionLineage } from "@/pages/session/session-lineage"
|
||||||
|
|
||||||
import { SessionPage, TargetSessionRouteContent } from "@/pages/session"
|
import { SessionPage, SessionRouteErrorBoundary, TargetSessionRouteContent } from "@/pages/session"
|
||||||
import { NewHome, LegacyHome } from "@/pages/home"
|
import { NewHome, LegacyHome } from "@/pages/home"
|
||||||
|
|
||||||
const NewSession = lazy(() => import("@/pages/new-session"))
|
const NewSession = lazy(() => import("@/pages/new-session"))
|
||||||
@@ -88,10 +90,14 @@ const SessionRoute = () => {
|
|||||||
tabs.newDraft({ server: server.key, directory: sdk().directory }, search.prompt)
|
tabs.newDraft({ server: server.key, directory: sdk().directory }, search.prompt)
|
||||||
})
|
})
|
||||||
|
|
||||||
return <SessionPage />
|
return (
|
||||||
|
<SessionRouteErrorBoundary sessionID={params.id}>
|
||||||
|
<SessionPage />
|
||||||
|
</SessionRouteErrorBoundary>
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
const TargetSessionRoute = () => {
|
function TargetServerRoute(props: ParentProps) {
|
||||||
const params = useParams<{ serverKey: string; id: string }>()
|
const params = useParams<{ serverKey: string; id: string }>()
|
||||||
const global = useGlobal()
|
const global = useGlobal()
|
||||||
const conn = createMemo(() => {
|
const conn = createMemo(() => {
|
||||||
@@ -105,14 +111,47 @@ const TargetSessionRoute = () => {
|
|||||||
// re-resolves reactively instead); both rely on this key for server changes.
|
// re-resolves reactively instead); both rely on this key for server changes.
|
||||||
<Show when={requireServerKey(params.serverKey)} keyed>
|
<Show when={requireServerKey(params.serverKey)} keyed>
|
||||||
<ServerSDKProvider server={conn}>
|
<ServerSDKProvider server={conn}>
|
||||||
<ServerSyncProvider server={conn}>
|
<ServerSyncProvider server={conn}>{props.children}</ServerSyncProvider>
|
||||||
<TargetSessionRouteContent />
|
|
||||||
</ServerSyncProvider>
|
|
||||||
</ServerSDKProvider>
|
</ServerSDKProvider>
|
||||||
</Show>
|
</Show>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const TargetSessionRoute = () => (
|
||||||
|
<TargetServerRoute>
|
||||||
|
<TargetSessionRouteContent />
|
||||||
|
</TargetServerRoute>
|
||||||
|
)
|
||||||
|
|
||||||
|
function LegacyTargetSessionRoute() {
|
||||||
|
const params = useParams<{ serverKey: string; id: string }>()
|
||||||
|
return (
|
||||||
|
<TargetServerRoute>
|
||||||
|
<SessionRouteErrorBoundary sessionID={params.id} serverKey={requireServerKey(params.serverKey)}>
|
||||||
|
<LegacyTargetSessionRedirect />
|
||||||
|
</SessionRouteErrorBoundary>
|
||||||
|
</TargetServerRoute>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function LegacyTargetSessionRedirect() {
|
||||||
|
const params = useParams<{ id: string }>()
|
||||||
|
const navigate = useNavigate()
|
||||||
|
const sync = useServerSync()
|
||||||
|
const current = createSessionLineage(
|
||||||
|
() => params.id,
|
||||||
|
() => sync().session.lineage,
|
||||||
|
)
|
||||||
|
|
||||||
|
createEffect(() => {
|
||||||
|
const directory = current()?.session.directory
|
||||||
|
if (!directory) return
|
||||||
|
navigate(legacySessionHref(directory, params.id), { replace: true })
|
||||||
|
})
|
||||||
|
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
// Wraps the non-draft routes. They are gated on (and keyed to) the globally selected
|
// Wraps the non-draft routes. They are gated on (and keyed to) the globally selected
|
||||||
// server via ServerKey, then provide the server-scoped shell (Permission/Layout/
|
// server via ServerKey, then provide the server-scoped shell (Permission/Layout/
|
||||||
// Notification/Models + the visual Layout) for that server.
|
// Notification/Models + the visual Layout) for that server.
|
||||||
@@ -126,16 +165,17 @@ function SelectedServerProviders(props: ParentProps) {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
function LegacyServerLayout(props: ParentProps) {
|
function LegacyServerLayout(props: ParentProps<{ serverScoped?: JSX.Element }>) {
|
||||||
return (
|
return (
|
||||||
<SelectedServerProviders>
|
<SelectedServerProviders>
|
||||||
<LegacyServerScopedShell>{props.children}</LegacyServerScopedShell>
|
<LegacyServerScopedShell serverScoped={props.serverScoped}>{props.children}</LegacyServerScopedShell>
|
||||||
</SelectedServerProviders>
|
</SelectedServerProviders>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
function DraftRoute() {
|
function DraftRoute() {
|
||||||
const [search] = useSearchParams<{ draftId?: string }>()
|
const [search] = useSearchParams<{ draftId?: string }>()
|
||||||
|
const settings = useSettings()
|
||||||
const tabs = useTabs()
|
const tabs = useTabs()
|
||||||
return (
|
return (
|
||||||
<Show when={tabs.ready()}>
|
<Show when={tabs.ready()}>
|
||||||
@@ -144,7 +184,14 @@ function DraftRoute() {
|
|||||||
keyed
|
keyed
|
||||||
fallback={<Navigate href="/" />}
|
fallback={<Navigate href="/" />}
|
||||||
>
|
>
|
||||||
{(draft) => <ResolvedDraftRoute draft={draft} />}
|
{(draft) => (
|
||||||
|
<Show
|
||||||
|
when={settings.general.newLayoutDesigns()}
|
||||||
|
fallback={<Navigate href={`/${base64Encode(draft.directory)}/session`} />}
|
||||||
|
>
|
||||||
|
<ResolvedDraftRoute draft={draft} />
|
||||||
|
</Show>
|
||||||
|
)}
|
||||||
</Show>
|
</Show>
|
||||||
</Show>
|
</Show>
|
||||||
)
|
)
|
||||||
@@ -186,7 +233,7 @@ declare global {
|
|||||||
deepLinks?: string[]
|
deepLinks?: string[]
|
||||||
}
|
}
|
||||||
api?: {
|
api?: {
|
||||||
setTitlebar?: (theme: { mode: "light" | "dark" }) => Promise<void>
|
setTitlebar?: (theme: { mode: "light" | "dark"; scheme?: "system" | "light" | "dark" }) => Promise<void>
|
||||||
exportDebugLogs?: () => Promise<string>
|
exportDebugLogs?: () => Promise<string>
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -263,12 +310,14 @@ function DesktopCommands() {
|
|||||||
type ServerScopedShellProps = ParentProps<{
|
type ServerScopedShellProps = ParentProps<{
|
||||||
directory?: () => string | undefined
|
directory?: () => string | undefined
|
||||||
sessionID?: () => string | undefined
|
sessionID?: () => string | undefined
|
||||||
|
serverScoped?: JSX.Element
|
||||||
}>
|
}>
|
||||||
|
|
||||||
function ServerScopedProviders(props: ServerScopedShellProps) {
|
function ServerScopedProviders(props: ServerScopedShellProps) {
|
||||||
return (
|
return (
|
||||||
<PermissionProvider directory={props.directory}>
|
<PermissionProvider directory={props.directory}>
|
||||||
<LayoutProvider>
|
<LayoutProvider>
|
||||||
|
{props.serverScoped}
|
||||||
<ModelsProvider directory={props.directory}>{props.children}</ModelsProvider>
|
<ModelsProvider directory={props.directory}>{props.children}</ModelsProvider>
|
||||||
</LayoutProvider>
|
</LayoutProvider>
|
||||||
</PermissionProvider>
|
</PermissionProvider>
|
||||||
@@ -277,16 +326,16 @@ function ServerScopedProviders(props: ServerScopedShellProps) {
|
|||||||
|
|
||||||
function LegacyServerScopedShell(props: ServerScopedShellProps) {
|
function LegacyServerScopedShell(props: ServerScopedShellProps) {
|
||||||
return (
|
return (
|
||||||
<ServerScopedProviders directory={props.directory} sessionID={props.sessionID}>
|
<ServerScopedProviders directory={props.directory} sessionID={props.sessionID} serverScoped={props.serverScoped}>
|
||||||
<LegacyLayout>{props.children}</LegacyLayout>
|
<LegacyLayout>{props.children}</LegacyLayout>
|
||||||
</ServerScopedProviders>
|
</ServerScopedProviders>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
function NewAppLayout(props: ParentProps) {
|
function NewAppLayout(props: ParentProps<{ serverScoped?: JSX.Element }>) {
|
||||||
return (
|
return (
|
||||||
<SelectedServerProviders>
|
<SelectedServerProviders>
|
||||||
<ServerScopedProviders>
|
<ServerScopedProviders serverScoped={props.serverScoped}>
|
||||||
<NewLayout>{props.children}</NewLayout>
|
<NewLayout>{props.children}</NewLayout>
|
||||||
</ServerScopedProviders>
|
</ServerScopedProviders>
|
||||||
</SelectedServerProviders>
|
</SelectedServerProviders>
|
||||||
@@ -318,8 +367,8 @@ export function AppBaseProviders(props: ParentProps<{ locale?: Locale }>) {
|
|||||||
<MetaProvider>
|
<MetaProvider>
|
||||||
<Font />
|
<Font />
|
||||||
<ThemeProvider
|
<ThemeProvider
|
||||||
onThemeApplied={(_, mode) => {
|
onThemeApplied={(_, mode, scheme) => {
|
||||||
void window.api?.setTitlebar?.({ mode })
|
void window.api?.setTitlebar?.({ mode, scheme })
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<LanguageProvider locale={props.locale}>
|
<LanguageProvider locale={props.locale}>
|
||||||
@@ -347,7 +396,7 @@ export function AppBaseProviders(props: ParentProps<{ locale?: Locale }>) {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
function ConnectionGate(props: ParentProps<{ disableHealthCheck?: boolean }>) {
|
function ConnectionGate(props: ParentProps<{ disableHealthCheck?: boolean; startup?: Promise<void> }>) {
|
||||||
const server = useServer()
|
const server = useServer()
|
||||||
const checkServerHealth = useCheckServerHealth()
|
const checkServerHealth = useCheckServerHealth()
|
||||||
|
|
||||||
@@ -376,34 +425,45 @@ function ConnectionGate(props: ParentProps<{ disableHealthCheck?: boolean }>) {
|
|||||||
const checking = createMemo(
|
const checking = createMemo(
|
||||||
() => checkMode() === "blocking" && ["unresolved", "pending"].includes(startupHealthCheck.state),
|
() => checkMode() === "blocking" && ["unresolved", "pending"].includes(startupHealthCheck.state),
|
||||||
)
|
)
|
||||||
|
const [startup] = createResource(async () => {
|
||||||
|
if (!props.startup) return true
|
||||||
|
await props.startup.catch((error) => {
|
||||||
|
console.error("[startup] startup gate failed", error)
|
||||||
|
})
|
||||||
|
return true
|
||||||
|
})
|
||||||
|
const startupChecking = createMemo(
|
||||||
|
() => startupHealthCheck.latest === true && ["unresolved", "pending"].includes(startup.state),
|
||||||
|
)
|
||||||
|
const loading = createMemo(() => checking() || startupChecking())
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Show
|
<>
|
||||||
when={!checking()}
|
<Show when={!checking()}>
|
||||||
fallback={
|
<Show
|
||||||
<div class="h-dvh w-screen flex flex-col items-center justify-center bg-background-base">
|
when={startupHealthCheck.latest}
|
||||||
|
fallback={
|
||||||
|
<ConnectionError
|
||||||
|
onRetry={() => {
|
||||||
|
if (checkMode() === "background") void healthCheckActions.refetch()
|
||||||
|
}}
|
||||||
|
onServerSelected={(key) => {
|
||||||
|
setCheckMode("blocking")
|
||||||
|
server.setActive(key)
|
||||||
|
void healthCheckActions.refetch()
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{props.children}
|
||||||
|
</Show>
|
||||||
|
</Show>
|
||||||
|
<Show when={loading()}>
|
||||||
|
<div class="fixed inset-0 z-[9999] flex flex-col items-center justify-center bg-background-base">
|
||||||
<Splash class="w-16 h-20 opacity-50 animate-pulse" />
|
<Splash class="w-16 h-20 opacity-50 animate-pulse" />
|
||||||
</div>
|
</div>
|
||||||
}
|
|
||||||
>
|
|
||||||
<Show
|
|
||||||
when={startupHealthCheck.latest}
|
|
||||||
fallback={
|
|
||||||
<ConnectionError
|
|
||||||
onRetry={() => {
|
|
||||||
if (checkMode() === "background") void healthCheckActions.refetch()
|
|
||||||
}}
|
|
||||||
onServerSelected={(key) => {
|
|
||||||
setCheckMode("blocking")
|
|
||||||
server.setActive(key)
|
|
||||||
void healthCheckActions.refetch()
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
>
|
|
||||||
{props.children}
|
|
||||||
</Show>
|
</Show>
|
||||||
</Show>
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -470,6 +530,8 @@ export function AppInterface(props: {
|
|||||||
servers?: Array<ServerConnection.Any>
|
servers?: Array<ServerConnection.Any>
|
||||||
router?: Component<BaseRouterProps>
|
router?: Component<BaseRouterProps>
|
||||||
disableHealthCheck?: boolean
|
disableHealthCheck?: boolean
|
||||||
|
startup?: Promise<void>
|
||||||
|
serverScoped?: JSX.Element
|
||||||
}) {
|
}) {
|
||||||
// The visual new layout lives in the router root so it remains mounted across
|
// The visual new layout lives in the router root so it remains mounted across
|
||||||
// route changes. Draft and session routes override only their server-bound data
|
// route changes. Draft and session routes override only their server-bound data
|
||||||
@@ -491,7 +553,7 @@ export function AppInterface(props: {
|
|||||||
>
|
>
|
||||||
<GlobalProvider>
|
<GlobalProvider>
|
||||||
<SettingsProvider>
|
<SettingsProvider>
|
||||||
<ConnectionGate disableHealthCheck={props.disableHealthCheck}>
|
<ConnectionGate disableHealthCheck={props.disableHealthCheck} startup={props.startup}>
|
||||||
<Show when={useSettings().general.newLayoutDesigns().toString()} keyed>
|
<Show when={useSettings().general.newLayoutDesigns().toString()} keyed>
|
||||||
<Dynamic
|
<Dynamic
|
||||||
component={props.router ?? Router}
|
component={props.router ?? Router}
|
||||||
@@ -500,14 +562,14 @@ export function AppInterface(props: {
|
|||||||
<NotificationProvider>
|
<NotificationProvider>
|
||||||
<ServerShell>
|
<ServerShell>
|
||||||
<Show when={useSettings().general.newLayoutDesigns()} fallback={routerProps.children}>
|
<Show when={useSettings().general.newLayoutDesigns()} fallback={routerProps.children}>
|
||||||
<NewAppLayout>{routerProps.children}</NewAppLayout>
|
<NewAppLayout serverScoped={props.serverScoped}>{routerProps.children}</NewAppLayout>
|
||||||
</Show>
|
</Show>
|
||||||
</ServerShell>
|
</ServerShell>
|
||||||
</NotificationProvider>
|
</NotificationProvider>
|
||||||
</TabsProvider>
|
</TabsProvider>
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<Routes />
|
<Routes serverScoped={props.serverScoped} />
|
||||||
</Dynamic>
|
</Dynamic>
|
||||||
</Show>
|
</Show>
|
||||||
</ConnectionGate>
|
</ConnectionGate>
|
||||||
@@ -517,13 +579,24 @@ export function AppInterface(props: {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
function Routes() {
|
function Routes(props: { serverScoped?: JSX.Element }) {
|
||||||
const settings = useSettings()
|
const settings = useSettings()
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Route component={LegacyServerLayout}>
|
<Route
|
||||||
<Show when={!settings.general.newLayoutDesigns()}>{<Route path="/" component={LegacyHome} />}</Show>
|
component={(routeProps) => (
|
||||||
|
<LegacyServerLayout serverScoped={props.serverScoped}>{routeProps.children}</LegacyServerLayout>
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<Show when={!settings.general.newLayoutDesigns()}>
|
||||||
|
{
|
||||||
|
<>
|
||||||
|
<Route path="/" component={LegacyHome} />
|
||||||
|
<Route path="/server/:serverKey/session/:id" component={LegacyTargetSessionRoute} />
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
</Show>
|
||||||
<Route path="/:dir" component={DirectoryLayout}>
|
<Route path="/:dir" component={DirectoryLayout}>
|
||||||
<Route path="/" component={() => <Navigate href="session" />} />
|
<Route path="/" component={() => <Navigate href="session" />} />
|
||||||
<Route path="/session/:id?" component={SessionRoute} />
|
<Route path="/session/:id?" component={SessionRoute} />
|
||||||
@@ -531,15 +604,15 @@ function Routes() {
|
|||||||
</Route>
|
</Route>
|
||||||
<Show when={settings.general.newLayoutDesigns()}>
|
<Show when={settings.general.newLayoutDesigns()}>
|
||||||
<Route path="/" component={NewHome} />
|
<Route path="/" component={NewHome} />
|
||||||
<Route path="/:dir/session/:id" component={LegacyTargetSessionRoute} />
|
<Route path="/:dir/session/:id" component={NewLayoutLegacySessionRedirect} />
|
||||||
|
<Route path="/server/:serverKey/session/:id" component={TargetSessionRoute} />
|
||||||
</Show>
|
</Show>
|
||||||
<Route path="/new-session" component={DraftRoute} />
|
<Route path="/new-session" component={DraftRoute} />
|
||||||
<Route path="/server/:serverKey/session/:id" component={TargetSessionRoute} />
|
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
function LegacyTargetSessionRoute() {
|
function NewLayoutLegacySessionRedirect() {
|
||||||
const server = useServer()
|
const server = useServer()
|
||||||
const tabs = useTabs()
|
const tabs = useTabs()
|
||||||
const params = useParams<{ id: string }>()
|
const params = useParams<{ id: string }>()
|
||||||
|
|||||||
@@ -0,0 +1,325 @@
|
|||||||
|
import { base64Encode } from "@opencode-ai/core/util/encode"
|
||||||
|
import { getFilename } from "@opencode-ai/core/util/path"
|
||||||
|
import { useDialog } from "@opencode-ai/ui/context/dialog"
|
||||||
|
import { useNavigate } from "@solidjs/router"
|
||||||
|
import { createMemo, onCleanup } from "solid-js"
|
||||||
|
import { useCommand, type CommandOption } from "@/context/command"
|
||||||
|
import { useFile } from "@/context/file"
|
||||||
|
import { useLanguage } from "@/context/language"
|
||||||
|
import { useLayout } from "@/context/layout"
|
||||||
|
import { useServerSDK, type ServerSDK } from "@/context/server-sdk"
|
||||||
|
import { useServerSync } from "@/context/server-sync"
|
||||||
|
import { createSessionTabs } from "@/pages/session/helpers"
|
||||||
|
import { useSessionLayout } from "@/pages/session/session-layout"
|
||||||
|
import { decode64 } from "@/utils/base64"
|
||||||
|
|
||||||
|
export type CommandPaletteEntry = {
|
||||||
|
id: string
|
||||||
|
type: "command" | "file" | "session"
|
||||||
|
title: string
|
||||||
|
description?: string
|
||||||
|
keybind?: string
|
||||||
|
category: string
|
||||||
|
option?: CommandOption
|
||||||
|
path?: string
|
||||||
|
directory?: string
|
||||||
|
sessionID?: string
|
||||||
|
archived?: number
|
||||||
|
updated?: number
|
||||||
|
}
|
||||||
|
|
||||||
|
const ENTRY_LIMIT = 5
|
||||||
|
const COMMON_COMMAND_IDS = [
|
||||||
|
"session.new",
|
||||||
|
"workspace.new",
|
||||||
|
"session.previous",
|
||||||
|
"session.next",
|
||||||
|
"terminal.toggle",
|
||||||
|
"review.toggle",
|
||||||
|
] as const
|
||||||
|
|
||||||
|
export function uniqueCommandPaletteEntries(items: CommandPaletteEntry[]) {
|
||||||
|
const seen = new Set<string>()
|
||||||
|
return items.filter((item) => {
|
||||||
|
if (seen.has(item.id)) return false
|
||||||
|
seen.add(item.id)
|
||||||
|
return true
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export function createCommandPaletteFileEntry(path: string, category: string): CommandPaletteEntry {
|
||||||
|
return {
|
||||||
|
id: "file:" + path,
|
||||||
|
type: "file",
|
||||||
|
title: path,
|
||||||
|
category,
|
||||||
|
path,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function createCommandPaletteFileOpener(onOpenFile?: (path: string) => void) {
|
||||||
|
const file = useFile()
|
||||||
|
const layout = useLayout()
|
||||||
|
const { tabs, view } = useSessionLayout()
|
||||||
|
|
||||||
|
return (path: string) => {
|
||||||
|
const value = file.tab(path)
|
||||||
|
void tabs().open(value)
|
||||||
|
void file.load(path)
|
||||||
|
if (!view().reviewPanel.opened()) view().reviewPanel.open()
|
||||||
|
layout.fileTree.setTab("all")
|
||||||
|
onOpenFile?.(path)
|
||||||
|
tabs().setActive(value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function createCommandPaletteModel(props: { filesOnly?: () => boolean; onOpenFile?: (path: string) => void }) {
|
||||||
|
const command = useCommand()
|
||||||
|
const language = useLanguage()
|
||||||
|
const layout = useLayout()
|
||||||
|
const file = useFile()
|
||||||
|
const dialog = useDialog()
|
||||||
|
const navigate = useNavigate()
|
||||||
|
const serverSDK = useServerSDK()()
|
||||||
|
const serverSync = useServerSync()
|
||||||
|
const { params, tabs } = useSessionLayout()
|
||||||
|
const openFile = createCommandPaletteFileOpener(props.onOpenFile)
|
||||||
|
const state = { cleanup: undefined as (() => void) | void, committed: false }
|
||||||
|
const filesOnly = () => props.filesOnly?.() ?? false
|
||||||
|
|
||||||
|
const allowedCommands = createMemo(() => {
|
||||||
|
if (filesOnly()) return []
|
||||||
|
return command.options.filter(
|
||||||
|
(option) =>
|
||||||
|
!option.disabled && !option.hidden && !option.id.startsWith("suggested.") && option.id !== "file.open",
|
||||||
|
)
|
||||||
|
})
|
||||||
|
const commandEntries = createMemo(() => {
|
||||||
|
const category = language.t("palette.group.commands")
|
||||||
|
return allowedCommands().map((option) => createCommandEntry(option, category))
|
||||||
|
})
|
||||||
|
const preferredCommandEntries = createMemo(() => {
|
||||||
|
const all = allowedCommands()
|
||||||
|
const order = new Map<string, number>(COMMON_COMMAND_IDS.map((id, index) => [id, index]))
|
||||||
|
const picked = all.filter((option) => order.has(option.id))
|
||||||
|
const base = picked.length ? picked : all.slice(0, ENTRY_LIMIT)
|
||||||
|
const sorted = picked.length ? [...base].sort((a, b) => (order.get(a.id) ?? 0) - (order.get(b.id) ?? 0)) : base
|
||||||
|
const category = language.t("palette.group.commands")
|
||||||
|
return sorted.map((option) => createCommandEntry(option, category))
|
||||||
|
})
|
||||||
|
|
||||||
|
const tabState = createSessionTabs({
|
||||||
|
tabs,
|
||||||
|
pathFromTab: file.pathFromTab,
|
||||||
|
normalizeTab: (tab) => (tab.startsWith("file://") ? file.tab(tab) : tab),
|
||||||
|
})
|
||||||
|
const recentFileEntries = createMemo(() => {
|
||||||
|
const all = tabState.openedTabs()
|
||||||
|
const active = tabState.activeFileTab()
|
||||||
|
const order = active ? [active, ...all.filter((item) => item !== active)] : all
|
||||||
|
const seen = new Set<string>()
|
||||||
|
const category = language.t("palette.group.files")
|
||||||
|
return order
|
||||||
|
.map((item) => file.pathFromTab(item))
|
||||||
|
.filter((path): path is string => {
|
||||||
|
if (!path || seen.has(path)) return false
|
||||||
|
seen.add(path)
|
||||||
|
return true
|
||||||
|
})
|
||||||
|
.slice(0, ENTRY_LIMIT)
|
||||||
|
.map((path) => createCommandPaletteFileEntry(path, category))
|
||||||
|
})
|
||||||
|
const rootFileEntries = createMemo(() => {
|
||||||
|
const category = language.t("palette.group.files")
|
||||||
|
return file.tree
|
||||||
|
.children("")
|
||||||
|
.filter((node) => node.type === "file")
|
||||||
|
.map((node) => node.path)
|
||||||
|
.sort((a, b) => a.localeCompare(b))
|
||||||
|
.slice(0, ENTRY_LIMIT)
|
||||||
|
.map((path) => createCommandPaletteFileEntry(path, category))
|
||||||
|
})
|
||||||
|
|
||||||
|
const projectDirectory = createMemo(() => decode64(params.dir) ?? "")
|
||||||
|
const project = createMemo(() => {
|
||||||
|
const directory = projectDirectory()
|
||||||
|
if (!directory) return undefined
|
||||||
|
return layout.projects.list().find((item) => item.worktree === directory || item.sandboxes?.includes(directory))
|
||||||
|
})
|
||||||
|
const workspaces = createMemo(() => {
|
||||||
|
const directory = projectDirectory()
|
||||||
|
const current = project()
|
||||||
|
if (!current) return directory ? [directory] : []
|
||||||
|
const dirs = [current.worktree, ...(current.sandboxes ?? [])]
|
||||||
|
if (directory && !dirs.includes(directory)) return [...dirs, directory]
|
||||||
|
return dirs
|
||||||
|
})
|
||||||
|
const homedir = createMemo(() => serverSync().data.path.home)
|
||||||
|
const sessions = createSessionEntries({
|
||||||
|
workspaces,
|
||||||
|
label: (directory) => {
|
||||||
|
const current = project()
|
||||||
|
const kind =
|
||||||
|
current && directory === current.worktree
|
||||||
|
? language.t("workspace.type.local")
|
||||||
|
: language.t("workspace.type.sandbox")
|
||||||
|
const [store] = serverSync().child(directory, { bootstrap: false })
|
||||||
|
const home = homedir()
|
||||||
|
const path = home ? directory.replace(home, "~") : directory
|
||||||
|
const name = store.vcs?.branch ?? getFilename(directory)
|
||||||
|
return `${kind} : ${name || path}`
|
||||||
|
},
|
||||||
|
load: (directory) => serverSDK.client.session.list({ directory, roots: true }),
|
||||||
|
untitled: () => language.t("command.session.new"),
|
||||||
|
category: () => language.t("command.category.session"),
|
||||||
|
})
|
||||||
|
|
||||||
|
const highlight = (item: CommandPaletteEntry | undefined) => {
|
||||||
|
state.cleanup?.()
|
||||||
|
state.cleanup = undefined
|
||||||
|
if (item?.type !== "command") return
|
||||||
|
state.cleanup = item.option?.onHighlight?.()
|
||||||
|
}
|
||||||
|
|
||||||
|
const select = (item: CommandPaletteEntry | undefined) => {
|
||||||
|
if (!item) return
|
||||||
|
state.committed = true
|
||||||
|
state.cleanup = undefined
|
||||||
|
dialog.close()
|
||||||
|
if (item.type === "command") {
|
||||||
|
item.option?.onSelect?.("palette")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (item.type === "session") {
|
||||||
|
if (!item.directory || !item.sessionID) return
|
||||||
|
navigate(`/${base64Encode(item.directory)}/session/${item.sessionID}`)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (!item.path) return
|
||||||
|
openFile(item.path)
|
||||||
|
}
|
||||||
|
|
||||||
|
onCleanup(() => {
|
||||||
|
if (state.committed) return
|
||||||
|
state.cleanup?.()
|
||||||
|
})
|
||||||
|
|
||||||
|
return {
|
||||||
|
language,
|
||||||
|
file,
|
||||||
|
commandEntries,
|
||||||
|
preferredCommandEntries,
|
||||||
|
recentFileEntries,
|
||||||
|
rootFileEntries,
|
||||||
|
sessions,
|
||||||
|
highlight,
|
||||||
|
select,
|
||||||
|
close: () => dialog.close(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function createCommandEntry(option: CommandOption, category: string): CommandPaletteEntry {
|
||||||
|
return {
|
||||||
|
id: "command:" + option.id,
|
||||||
|
type: "command",
|
||||||
|
title: option.title,
|
||||||
|
description: option.description,
|
||||||
|
keybind: option.keybind,
|
||||||
|
category,
|
||||||
|
option,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function createSessionEntries(props: {
|
||||||
|
workspaces: () => string[]
|
||||||
|
label: (directory: string) => string
|
||||||
|
load: (directory: string) => ReturnType<ServerSDK["client"]["session"]["list"]>
|
||||||
|
untitled: () => string
|
||||||
|
category: () => string
|
||||||
|
}) {
|
||||||
|
const state: {
|
||||||
|
token: number
|
||||||
|
inflight: Promise<CommandPaletteEntry[]> | undefined
|
||||||
|
cached: CommandPaletteEntry[] | undefined
|
||||||
|
} = { token: 0, inflight: undefined, cached: undefined }
|
||||||
|
|
||||||
|
return (text: string) => {
|
||||||
|
if (!text.trim()) {
|
||||||
|
state.token += 1
|
||||||
|
state.inflight = undefined
|
||||||
|
state.cached = undefined
|
||||||
|
return [] as CommandPaletteEntry[]
|
||||||
|
}
|
||||||
|
if (state.cached) return state.cached
|
||||||
|
if (state.inflight) return state.inflight
|
||||||
|
|
||||||
|
const current = state.token
|
||||||
|
const dirs = props.workspaces()
|
||||||
|
if (dirs.length === 0) return [] as CommandPaletteEntry[]
|
||||||
|
|
||||||
|
state.inflight = Promise.all(
|
||||||
|
dirs.map((directory) => {
|
||||||
|
const description = props.label(directory)
|
||||||
|
return props
|
||||||
|
.load(directory)
|
||||||
|
.then((result) =>
|
||||||
|
(result.data ?? [])
|
||||||
|
.filter((session) => !!session?.id)
|
||||||
|
.map((session) => ({
|
||||||
|
id: session.id,
|
||||||
|
title: session.title ?? props.untitled(),
|
||||||
|
description,
|
||||||
|
directory,
|
||||||
|
archived: session.time?.archived,
|
||||||
|
updated: session.time?.updated,
|
||||||
|
})),
|
||||||
|
)
|
||||||
|
.catch(() => [] as SessionEntryInput[])
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
.then((results) => {
|
||||||
|
if (state.token !== current) return [] as CommandPaletteEntry[]
|
||||||
|
const seen = new Set<string>()
|
||||||
|
const next = results
|
||||||
|
.flat()
|
||||||
|
.filter((item) => {
|
||||||
|
const key = `${item.directory}:${item.id}`
|
||||||
|
if (seen.has(key)) return false
|
||||||
|
seen.add(key)
|
||||||
|
return true
|
||||||
|
})
|
||||||
|
.map((item) => createSessionEntry(item, props.category()))
|
||||||
|
state.cached = next
|
||||||
|
return next
|
||||||
|
})
|
||||||
|
.catch(() => [] as CommandPaletteEntry[])
|
||||||
|
.finally(() => {
|
||||||
|
state.inflight = undefined
|
||||||
|
})
|
||||||
|
|
||||||
|
return state.inflight
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type SessionEntryInput = {
|
||||||
|
directory: string
|
||||||
|
id: string
|
||||||
|
title: string
|
||||||
|
description: string
|
||||||
|
archived?: number
|
||||||
|
updated?: number
|
||||||
|
}
|
||||||
|
|
||||||
|
function createSessionEntry(input: SessionEntryInput, category: string): CommandPaletteEntry {
|
||||||
|
return {
|
||||||
|
id: `session:${input.directory}:${input.id}`,
|
||||||
|
type: "session",
|
||||||
|
title: input.title,
|
||||||
|
description: input.description,
|
||||||
|
category,
|
||||||
|
directory: input.directory,
|
||||||
|
sessionID: input.id,
|
||||||
|
archived: input.archived,
|
||||||
|
updated: input.updated,
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,220 @@
|
|||||||
|
.command-palette-v2 {
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Anchor to the top edge of where a centered 480px-tall dialog would sit, so the
|
||||||
|
top stays put while the content-driven height grows and shrinks. */
|
||||||
|
[data-component="dialog-v2"]:has(.command-palette-v2) {
|
||||||
|
align-items: flex-start;
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-component="dialog-v2"]:has(.command-palette-v2) [data-slot="dialog-container"] {
|
||||||
|
width: min(calc(100vw - 24px), 640px);
|
||||||
|
height: auto;
|
||||||
|
min-height: 280px;
|
||||||
|
max-height: min(calc(100vh - 96px), 480px);
|
||||||
|
margin-top: max(48px, calc((100vh - 480px) / 2));
|
||||||
|
border-radius: 12px;
|
||||||
|
background: var(--v2-background-bg-base);
|
||||||
|
box-shadow: var(--v2-elevation-floating);
|
||||||
|
}
|
||||||
|
|
||||||
|
.command-palette-v2-body {
|
||||||
|
display: flex;
|
||||||
|
min-height: 0;
|
||||||
|
flex: 1;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 0;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.command-palette-v2-search {
|
||||||
|
flex-shrink: 0;
|
||||||
|
padding: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.command-palette-v2-search [data-component="text-input-v2"] {
|
||||||
|
width: 100%;
|
||||||
|
height: 36px;
|
||||||
|
border-radius: 6px;
|
||||||
|
outline: 0;
|
||||||
|
background: color-mix(in srgb, var(--v2-background-bg-layer-02) 60%, transparent);
|
||||||
|
box-shadow: none;
|
||||||
|
transition:
|
||||||
|
background-color 120ms ease-in-out,
|
||||||
|
box-shadow 120ms ease-in-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
.command-palette-v2-search [data-component="text-input-v2"]:where(:hover):not([data-disabled], [data-invalid]),
|
||||||
|
.command-palette-v2-search [data-component="text-input-v2"]:where(:focus-within):not([data-disabled], [data-invalid]) {
|
||||||
|
background: var(--v2-background-bg-layer-02);
|
||||||
|
box-shadow: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.command-palette-v2-search [data-component="text-input-v2"] [data-slot="text-input-v2-value"] {
|
||||||
|
border-radius: 6px;
|
||||||
|
background: transparent;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.command-palette-v2-search [data-component="text-input-v2"] [data-slot="text-input-v2-leading-icon"] {
|
||||||
|
padding-left: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.command-palette-v2-search [data-component="text-input-v2"] [data-slot="text-input-v2-input"] {
|
||||||
|
border-radius: 6px;
|
||||||
|
background: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
.command-palette-v2-scroll {
|
||||||
|
min-height: 0;
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.command-palette-v2-results {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 16px;
|
||||||
|
padding: 6px 6px 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.command-palette-v2-group {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 1px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.command-palette-v2-group-title {
|
||||||
|
margin: 6px 0;
|
||||||
|
padding: 0 12px;
|
||||||
|
color: var(--v2-text-text-muted);
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 440;
|
||||||
|
line-height: 16px;
|
||||||
|
letter-spacing: -0.04px;
|
||||||
|
user-select: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.command-palette-v2-row {
|
||||||
|
display: flex;
|
||||||
|
width: 100%;
|
||||||
|
height: 36px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 8px;
|
||||||
|
padding: 0 12px;
|
||||||
|
border: 0;
|
||||||
|
border-radius: 6px;
|
||||||
|
background: transparent;
|
||||||
|
color: var(--v2-text-text-base);
|
||||||
|
text-align: left;
|
||||||
|
cursor: default;
|
||||||
|
scroll-margin: 6px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.command-palette-v2-row[data-active] {
|
||||||
|
background: var(--v2-overlay-simple-overlay-hover);
|
||||||
|
}
|
||||||
|
|
||||||
|
.command-palette-v2-row:focus-visible {
|
||||||
|
background: var(--v2-overlay-simple-overlay-hover);
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.command-palette-v2-row-main {
|
||||||
|
display: flex;
|
||||||
|
min-width: 0;
|
||||||
|
flex: 1;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.command-palette-v2-row-icon {
|
||||||
|
flex-shrink: 0;
|
||||||
|
color: var(--v2-icon-icon-muted);
|
||||||
|
}
|
||||||
|
|
||||||
|
.command-palette-v2-row-text {
|
||||||
|
display: flex;
|
||||||
|
min-width: 0;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.command-palette-v2-title {
|
||||||
|
min-width: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
color: var(--v2-text-text-base);
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 530;
|
||||||
|
line-height: 16px;
|
||||||
|
letter-spacing: -0.04px;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.command-palette-v2-description,
|
||||||
|
.command-palette-v2-meta {
|
||||||
|
min-width: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
color: var(--v2-text-text-muted);
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 440;
|
||||||
|
line-height: 16px;
|
||||||
|
letter-spacing: -0.04px;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.command-palette-v2-meta {
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.command-palette-v2-file-path {
|
||||||
|
display: flex;
|
||||||
|
min-width: 0;
|
||||||
|
align-items: baseline;
|
||||||
|
font-size: 13px;
|
||||||
|
line-height: 16px;
|
||||||
|
letter-spacing: -0.04px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.command-palette-v2-file-dir {
|
||||||
|
min-width: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
color: var(--v2-text-text-muted);
|
||||||
|
font-weight: 440;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.command-palette-v2-file-name {
|
||||||
|
flex-shrink: 0;
|
||||||
|
color: var(--v2-text-text-base);
|
||||||
|
font-weight: 530;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.command-palette-v2-state {
|
||||||
|
display: grid;
|
||||||
|
min-height: 120px;
|
||||||
|
place-items: center;
|
||||||
|
color: var(--v2-text-text-muted);
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 440;
|
||||||
|
line-height: 16px;
|
||||||
|
letter-spacing: -0.04px;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 640px) {
|
||||||
|
.command-palette-v2-row-text {
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: flex-start;
|
||||||
|
gap: 1px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.command-palette-v2-description {
|
||||||
|
max-width: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,221 @@
|
|||||||
|
import { getDirectory, getFilename } from "@opencode-ai/core/util/path"
|
||||||
|
import { FileIcon } from "@opencode-ai/ui/file-icon"
|
||||||
|
import { ScrollView } from "@opencode-ai/ui/scroll-view"
|
||||||
|
import { Dialog, DialogBody } from "@opencode-ai/ui/v2/dialog-v2"
|
||||||
|
import { Icon } from "@opencode-ai/ui/v2/icon"
|
||||||
|
import { KeybindV2 } from "@opencode-ai/ui/v2/keybind-v2"
|
||||||
|
import { TextInputV2 } from "@opencode-ai/ui/v2/text-input-v2"
|
||||||
|
import { createEffect, createMemo, createResource, createSignal, For, Match, Show, Switch } from "solid-js"
|
||||||
|
import { formatKeybindParts } from "@/context/command"
|
||||||
|
import { useLanguage } from "@/context/language"
|
||||||
|
import { getRelativeTime } from "@/utils/time"
|
||||||
|
import {
|
||||||
|
createCommandPaletteFileEntry,
|
||||||
|
createCommandPaletteModel,
|
||||||
|
uniqueCommandPaletteEntries,
|
||||||
|
type CommandPaletteEntry,
|
||||||
|
} from "./command-palette"
|
||||||
|
import "./dialog-command-palette-v2.css"
|
||||||
|
|
||||||
|
function groups(entries: CommandPaletteEntry[]) {
|
||||||
|
const map = new Map<string, CommandPaletteEntry[]>()
|
||||||
|
for (const entry of entries) map.set(entry.category, [...(map.get(entry.category) ?? []), entry])
|
||||||
|
return Array.from(map.entries()).map(([category, entries]) => ({ category, entries }))
|
||||||
|
}
|
||||||
|
|
||||||
|
function matchesEntry(entry: CommandPaletteEntry, query: string) {
|
||||||
|
const value = query.toLowerCase()
|
||||||
|
return [entry.title, entry.description, entry.category].some((text) => text?.toLowerCase().includes(value))
|
||||||
|
}
|
||||||
|
|
||||||
|
export function DialogCommandPaletteV2(props: { onOpenFile?: (path: string) => void }) {
|
||||||
|
const palette = createCommandPaletteModel(props)
|
||||||
|
const [query, setQuery] = createSignal("")
|
||||||
|
const [active, setActive] = createSignal(0)
|
||||||
|
|
||||||
|
const loadItems = async (text: string) => {
|
||||||
|
const q = text.trim()
|
||||||
|
if (!q) return [...palette.preferredCommandEntries(), ...palette.recentFileEntries()]
|
||||||
|
|
||||||
|
const [files, nextSessions] = await Promise.all([palette.file.searchFiles(q), Promise.resolve(palette.sessions(q))])
|
||||||
|
const category = palette.language.t("palette.group.files")
|
||||||
|
return [
|
||||||
|
...palette.commandEntries().filter((entry) => matchesEntry(entry, q)),
|
||||||
|
...nextSessions.filter((entry) => matchesEntry(entry, q)),
|
||||||
|
...files.map((path) => createCommandPaletteFileEntry(path, category)),
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
const [entries] = createResource(query, loadItems, { initialValue: [] as CommandPaletteEntry[] })
|
||||||
|
// Render stale results while a new query loads to avoid flashing "Loading" per keystroke.
|
||||||
|
const visibleEntries = createMemo(() => uniqueCommandPaletteEntries(entries.latest ?? []))
|
||||||
|
const groupedEntries = createMemo(() => groups(visibleEntries()))
|
||||||
|
const activeEntry = createMemo(() => visibleEntries()[active()])
|
||||||
|
|
||||||
|
createEffect(() => {
|
||||||
|
query()
|
||||||
|
visibleEntries()
|
||||||
|
setActive(0)
|
||||||
|
})
|
||||||
|
|
||||||
|
createEffect(() => {
|
||||||
|
palette.highlight(activeEntry())
|
||||||
|
})
|
||||||
|
|
||||||
|
let resultsRef: HTMLDivElement | undefined
|
||||||
|
|
||||||
|
const move = (delta: -1 | 1) => {
|
||||||
|
const count = visibleEntries().length
|
||||||
|
if (count === 0) return
|
||||||
|
setActive((index) => (index + delta + count) % count)
|
||||||
|
requestAnimationFrame(() => {
|
||||||
|
resultsRef?.querySelector("[data-active]")?.scrollIntoView({ block: "nearest" })
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleKeyDown = (event: KeyboardEvent) => {
|
||||||
|
if (event.key === "ArrowDown") {
|
||||||
|
event.preventDefault()
|
||||||
|
move(1)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (event.key === "ArrowUp") {
|
||||||
|
event.preventDefault()
|
||||||
|
move(-1)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (event.key === "Enter") {
|
||||||
|
event.preventDefault()
|
||||||
|
palette.select(activeEntry())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (event.key === "Escape") {
|
||||||
|
event.preventDefault()
|
||||||
|
palette.close()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Dialog class="command-palette-v2" size="large">
|
||||||
|
<DialogBody class="command-palette-v2-body">
|
||||||
|
<div class="command-palette-v2-search">
|
||||||
|
<TextInputV2
|
||||||
|
value={query()}
|
||||||
|
autofocus
|
||||||
|
autocomplete="off"
|
||||||
|
spellcheck={false}
|
||||||
|
appearance="large"
|
||||||
|
placeholder={palette.language.t("palette.search.placeholder")}
|
||||||
|
leadingIcon={<Icon name="magnifying-glass" />}
|
||||||
|
onInput={(event) => setQuery(event.currentTarget.value)}
|
||||||
|
onKeyDown={handleKeyDown}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<ScrollView class="command-palette-v2-scroll" viewportRef={(el) => (resultsRef = el)}>
|
||||||
|
<div class="command-palette-v2-results" role="listbox">
|
||||||
|
<Show
|
||||||
|
when={visibleEntries().length > 0}
|
||||||
|
fallback={
|
||||||
|
<div class="command-palette-v2-state">
|
||||||
|
{entries.loading ? palette.language.t("common.loading") : palette.language.t("palette.empty")}
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<For each={groupedEntries()}>
|
||||||
|
{(group) => (
|
||||||
|
<div class="command-palette-v2-group">
|
||||||
|
<Show when={group.category}>
|
||||||
|
<div class="command-palette-v2-group-title">{group.category}</div>
|
||||||
|
</Show>
|
||||||
|
<For each={group.entries}>
|
||||||
|
{(item) => (
|
||||||
|
<PaletteRow
|
||||||
|
item={item}
|
||||||
|
active={activeEntry()?.id === item.id}
|
||||||
|
language={palette.language}
|
||||||
|
onActive={() => setActive(visibleEntries().findIndex((entry) => entry.id === item.id))}
|
||||||
|
onSelect={() => palette.select(item)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</For>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</For>
|
||||||
|
</Show>
|
||||||
|
</div>
|
||||||
|
</ScrollView>
|
||||||
|
</DialogBody>
|
||||||
|
</Dialog>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function PaletteRow(props: {
|
||||||
|
item: CommandPaletteEntry
|
||||||
|
active: boolean
|
||||||
|
language: ReturnType<typeof useLanguage>
|
||||||
|
onActive: () => void
|
||||||
|
onSelect: () => void
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="command-palette-v2-row"
|
||||||
|
role="option"
|
||||||
|
aria-selected={props.active}
|
||||||
|
data-active={props.active ? "" : undefined}
|
||||||
|
onMouseMove={(event) => {
|
||||||
|
// Ignore hover from a static cursor when keyboard scrolling moves rows underneath it.
|
||||||
|
if (event.movementX === 0 && event.movementY === 0) return
|
||||||
|
props.onActive()
|
||||||
|
}}
|
||||||
|
onMouseDown={(event) => event.preventDefault()}
|
||||||
|
onClick={props.onSelect}
|
||||||
|
>
|
||||||
|
<Switch
|
||||||
|
fallback={
|
||||||
|
<div class="command-palette-v2-row-main">
|
||||||
|
<FileIcon node={{ path: props.item.path ?? "", type: "file" }} class="command-palette-v2-row-icon size-4" />
|
||||||
|
<div class="command-palette-v2-file-path">
|
||||||
|
<span class="command-palette-v2-file-dir">{getDirectory(props.item.path ?? "")}</span>
|
||||||
|
<span class="command-palette-v2-file-name">{getFilename(props.item.path ?? "")}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<Match when={props.item.type === "command"}>
|
||||||
|
<div class="command-palette-v2-row-main">
|
||||||
|
<div class="command-palette-v2-row-text">
|
||||||
|
<span class="command-palette-v2-title">{props.item.title}</span>
|
||||||
|
<Show when={props.item.description}>
|
||||||
|
<span class="command-palette-v2-description">{props.item.description}</span>
|
||||||
|
</Show>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<Show when={props.item.keybind}>
|
||||||
|
<KeybindV2 keys={formatKeybindParts(props.item.keybind ?? "", props.language.t)} variant="neutral" />
|
||||||
|
</Show>
|
||||||
|
</Match>
|
||||||
|
<Match when={props.item.type === "session"}>
|
||||||
|
<div class="command-palette-v2-row-main">
|
||||||
|
<Icon name="status" class="command-palette-v2-row-icon" />
|
||||||
|
<div class="command-palette-v2-row-text">
|
||||||
|
<span class="command-palette-v2-title" classList={{ "opacity-70": !!props.item.archived }}>
|
||||||
|
{props.item.title}
|
||||||
|
</span>
|
||||||
|
<Show when={props.item.description}>
|
||||||
|
<span class="command-palette-v2-description" classList={{ "opacity-70": !!props.item.archived }}>
|
||||||
|
{props.item.description}
|
||||||
|
</span>
|
||||||
|
</Show>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<Show when={props.item.updated}>
|
||||||
|
<span class="command-palette-v2-meta">
|
||||||
|
{getRelativeTime(new Date(props.item.updated!).toISOString(), props.language.t)}
|
||||||
|
</span>
|
||||||
|
</Show>
|
||||||
|
</Match>
|
||||||
|
</Switch>
|
||||||
|
</button>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -7,29 +7,174 @@ import { IconButton } from "@opencode-ai/ui/icon-button"
|
|||||||
import { List, type ListRef } from "@opencode-ai/ui/list"
|
import { List, type ListRef } from "@opencode-ai/ui/list"
|
||||||
import { ProviderIcon } from "@opencode-ai/ui/provider-icon"
|
import { ProviderIcon } from "@opencode-ai/ui/provider-icon"
|
||||||
import { Spinner } from "@opencode-ai/ui/spinner"
|
import { Spinner } from "@opencode-ai/ui/spinner"
|
||||||
|
import { Tag } from "@opencode-ai/ui/tag"
|
||||||
import { TextField } from "@opencode-ai/ui/text-field"
|
import { TextField } from "@opencode-ai/ui/text-field"
|
||||||
import { showToast } from "@/utils/toast"
|
import { showToast } from "@/utils/toast"
|
||||||
import { type Accessor, createEffect, createMemo, createResource, Match, onCleanup, onMount, Switch } from "solid-js"
|
import {
|
||||||
|
type Accessor,
|
||||||
|
type Component,
|
||||||
|
createEffect,
|
||||||
|
createMemo,
|
||||||
|
createResource,
|
||||||
|
Match,
|
||||||
|
onCleanup,
|
||||||
|
onMount,
|
||||||
|
Show,
|
||||||
|
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 { useServerSDK } from "@/context/server-sdk"
|
import { useServerSDK } from "@/context/server-sdk"
|
||||||
import { useServerSync } from "@/context/server-sync"
|
import { useServerSync } from "@/context/server-sync"
|
||||||
import { useLanguage } from "@/context/language"
|
import { useLanguage } from "@/context/language"
|
||||||
import { useProviders } from "@/hooks/use-providers"
|
import { popularProviders, useProviders } from "@/hooks/use-providers"
|
||||||
|
import { CustomProviderForm } from "./dialog-custom-provider"
|
||||||
|
|
||||||
export function DialogConnectProvider(props: { provider: string; directory?: Accessor<string | undefined> }) {
|
const CUSTOM_ID = "_custom"
|
||||||
|
|
||||||
|
export function useProviderConnectController(options: { onBack?: () => void } = {}) {
|
||||||
|
const [store, setStore] = createStore({ selected: undefined as string | undefined })
|
||||||
|
const reset = () => setStore("selected", undefined)
|
||||||
|
|
||||||
|
return {
|
||||||
|
selected: () => store.selected,
|
||||||
|
select: (provider?: string) => setStore("selected", provider),
|
||||||
|
back: options.onBack ?? reset,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const DialogConnectProvider: Component<{
|
||||||
|
directory?: Accessor<string | undefined>
|
||||||
|
controller?: ReturnType<typeof useProviderConnectController>
|
||||||
|
}> = (props) => {
|
||||||
|
const fallback = useProviderConnectController()
|
||||||
|
const controller = props.controller ?? fallback
|
||||||
|
const language = useLanguage()
|
||||||
|
const reset = controller.back
|
||||||
|
const back = { current: reset }
|
||||||
|
const select = (provider?: string) => {
|
||||||
|
back.current = reset
|
||||||
|
controller.select(provider)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Dialog
|
||||||
|
class="h-full"
|
||||||
|
transition
|
||||||
|
title={
|
||||||
|
<Show when={controller.selected()} fallback={language.t("command.provider.connect")}>
|
||||||
|
<IconButton
|
||||||
|
tabIndex={-1}
|
||||||
|
icon="arrow-left"
|
||||||
|
variant="ghost"
|
||||||
|
onClick={() => back.current()}
|
||||||
|
aria-label={language.t("common.goBack")}
|
||||||
|
/>
|
||||||
|
</Show>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<Switch>
|
||||||
|
<Match when={controller.selected() === CUSTOM_ID}>
|
||||||
|
<CustomProviderForm />
|
||||||
|
</Match>
|
||||||
|
<Match when={controller.selected() && controller.selected() !== CUSTOM_ID ? controller.selected() : undefined}>
|
||||||
|
{(provider) => (
|
||||||
|
<ProviderConnection
|
||||||
|
provider={provider()}
|
||||||
|
directory={props.directory}
|
||||||
|
onBack={reset}
|
||||||
|
setBack={(handler) => (back.current = handler)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</Match>
|
||||||
|
<Match when={true}>
|
||||||
|
<ProviderPicker directory={props.directory} onSelect={select} />
|
||||||
|
</Match>
|
||||||
|
</Switch>
|
||||||
|
</Dialog>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function ProviderPicker(props: { directory?: Accessor<string | undefined>; onSelect: (provider: string) => void }) {
|
||||||
|
const providers = useProviders(props.directory)
|
||||||
|
const language = useLanguage()
|
||||||
|
const popularGroup = () => language.t("dialog.provider.group.popular")
|
||||||
|
const otherGroup = () => language.t("dialog.provider.group.other")
|
||||||
|
const customLabel = () => language.t("settings.providers.tag.custom")
|
||||||
|
const note = (id: string) => {
|
||||||
|
if (id === "anthropic") return language.t("dialog.provider.anthropic.note")
|
||||||
|
if (id === "openai") return language.t("dialog.provider.openai.note")
|
||||||
|
if (id.startsWith("github-copilot")) return language.t("dialog.provider.copilot.note")
|
||||||
|
if (id === "opencode-go") return language.t("dialog.provider.opencodeGo.tagline")
|
||||||
|
return undefined
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<List
|
||||||
|
class="px-3"
|
||||||
|
search={{ placeholder: language.t("dialog.provider.search.placeholder"), autofocus: true }}
|
||||||
|
emptyMessage={language.t("dialog.provider.empty")}
|
||||||
|
activeIcon="plus-small"
|
||||||
|
key={(x) => x?.id}
|
||||||
|
items={() => {
|
||||||
|
language.locale()
|
||||||
|
return [{ id: CUSTOM_ID, name: customLabel() }, ...providers.all().values()]
|
||||||
|
}}
|
||||||
|
filterKeys={["id", "name"]}
|
||||||
|
groupBy={(x) => (popularProviders.includes(x.id) ? popularGroup() : otherGroup())}
|
||||||
|
sortBy={(a, b) => {
|
||||||
|
if (a.id === CUSTOM_ID) return -1
|
||||||
|
if (b.id === CUSTOM_ID) return 1
|
||||||
|
if (popularProviders.includes(a.id) && popularProviders.includes(b.id))
|
||||||
|
return popularProviders.indexOf(a.id) - popularProviders.indexOf(b.id)
|
||||||
|
return a.name.localeCompare(b.name)
|
||||||
|
}}
|
||||||
|
sortGroupsBy={(a, b) => {
|
||||||
|
const popular = popularGroup()
|
||||||
|
if (a.category === popular && b.category !== popular) return -1
|
||||||
|
if (b.category === popular && a.category !== popular) return 1
|
||||||
|
return 0
|
||||||
|
}}
|
||||||
|
onSelect={(x) => {
|
||||||
|
if (!x) return
|
||||||
|
props.onSelect(x.id)
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{(i) => (
|
||||||
|
<div class="px-1.25 w-full flex items-center gap-x-3">
|
||||||
|
<ProviderIcon data-slot="list-item-extra-icon" id={i.id} />
|
||||||
|
<span>{i.name}</span>
|
||||||
|
<Show when={i.id === "opencode"}>
|
||||||
|
<div class="text-14-regular text-text-weak">{language.t("dialog.provider.opencode.tagline")}</div>
|
||||||
|
</Show>
|
||||||
|
<Show when={i.id === CUSTOM_ID}>
|
||||||
|
<Tag>{language.t("settings.providers.tag.custom")}</Tag>
|
||||||
|
</Show>
|
||||||
|
<Show when={i.id === "opencode"}>
|
||||||
|
<Tag>{language.t("dialog.provider.tag.recommended")}</Tag>
|
||||||
|
</Show>
|
||||||
|
<Show when={note(i.id)}>{(value) => <div class="text-14-regular text-text-weak">{value()}</div>}</Show>
|
||||||
|
<Show when={i.id === "opencode-go"}>
|
||||||
|
<Tag>{language.t("dialog.provider.tag.recommended")}</Tag>
|
||||||
|
</Show>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</List>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function ProviderConnection(props: {
|
||||||
|
provider: string
|
||||||
|
directory?: Accessor<string | undefined>
|
||||||
|
onBack: () => void
|
||||||
|
setBack: (handler: () => void) => void
|
||||||
|
}) {
|
||||||
const dialog = useDialog()
|
const dialog = useDialog()
|
||||||
const serverSync = useServerSync()
|
const serverSync = useServerSync()
|
||||||
const serverSDK = useServerSDK()
|
const serverSDK = useServerSDK()
|
||||||
const language = useLanguage()
|
const language = useLanguage()
|
||||||
const providers = useProviders(props.directory)
|
const providers = useProviders(props.directory)
|
||||||
|
|
||||||
const all = () => {
|
|
||||||
void import("./dialog-select-provider").then((x) => {
|
|
||||||
dialog.show(() => <x.DialogSelectProvider directory={props.directory} />)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
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 }
|
||||||
|
|
||||||
@@ -364,21 +509,15 @@ export function DialogConnectProvider(props: { provider: string; directory?: Acc
|
|||||||
}
|
}
|
||||||
|
|
||||||
function goBack() {
|
function goBack() {
|
||||||
if (methods().length === 1) {
|
if (methods().length > 1 && store.methodIndex !== undefined) {
|
||||||
all()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if (store.authorization) {
|
|
||||||
dispatch({ type: "method.reset" })
|
dispatch({ type: "method.reset" })
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if (store.methodIndex !== undefined) {
|
props.onBack()
|
||||||
dispatch({ type: "method.reset" })
|
|
||||||
return
|
|
||||||
}
|
|
||||||
all()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
props.setBack(goBack)
|
||||||
|
|
||||||
function MethodSelection() {
|
function MethodSelection() {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
@@ -599,79 +738,67 @@ export function DialogConnectProvider(props: { provider: string; directory?: Acc
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog
|
<div class="flex flex-col gap-6 px-2.5 pb-3">
|
||||||
title={
|
<div class="px-2.5 flex gap-4 items-center">
|
||||||
<IconButton
|
<ProviderIcon id={props.provider} class="size-5 shrink-0 icon-strong-base" />
|
||||||
tabIndex={-1}
|
<div class="text-16-medium text-text-strong">
|
||||||
icon="arrow-left"
|
<Switch>
|
||||||
variant="ghost"
|
<Match when={props.provider === "anthropic" && method()?.label?.toLowerCase().includes("max")}>
|
||||||
onClick={goBack}
|
{language.t("provider.connect.title.anthropicProMax")}
|
||||||
aria-label={language.t("common.goBack")}
|
</Match>
|
||||||
/>
|
<Match when={true}>{language.t("provider.connect.title", { provider: provider().name })}</Match>
|
||||||
}
|
</Switch>
|
||||||
>
|
|
||||||
<div class="flex flex-col gap-6 px-2.5 pb-3">
|
|
||||||
<div class="px-2.5 flex gap-4 items-center">
|
|
||||||
<ProviderIcon id={props.provider} class="size-5 shrink-0 icon-strong-base" />
|
|
||||||
<div class="text-16-medium text-text-strong">
|
|
||||||
<Switch>
|
|
||||||
<Match when={props.provider === "anthropic" && method()?.label?.toLowerCase().includes("max")}>
|
|
||||||
{language.t("provider.connect.title.anthropicProMax")}
|
|
||||||
</Match>
|
|
||||||
<Match when={true}>{language.t("provider.connect.title", { provider: provider().name })}</Match>
|
|
||||||
</Switch>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="px-2.5 pb-10 flex flex-col gap-6">
|
|
||||||
<div onKeyDown={handleKey} tabIndex={0} autofocus={store.methodIndex === undefined ? true : undefined}>
|
|
||||||
<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}>
|
|
||||||
<MethodSelection />
|
|
||||||
</Match>
|
|
||||||
<Match when={store.state === "pending"}>
|
|
||||||
<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.state === "prompt"}>
|
|
||||||
<AuthPromptsView />
|
|
||||||
</Match>
|
|
||||||
<Match when={store.state === "error"}>
|
|
||||||
<div class="text-14-regular text-text-base">
|
|
||||||
<div class="flex items-center gap-x-2">
|
|
||||||
<Icon name="circle-ban-sign" class="text-icon-critical-base" />
|
|
||||||
<span>{language.t("provider.connect.status.failed", { error: store.error ?? "" })}</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Match>
|
|
||||||
<Match when={method()?.type === "api"}>
|
|
||||||
<ApiAuthView />
|
|
||||||
</Match>
|
|
||||||
<Match when={method()?.type === "oauth"}>
|
|
||||||
<Switch>
|
|
||||||
<Match when={store.authorization?.method === "code"}>
|
|
||||||
<OAuthCodeView />
|
|
||||||
</Match>
|
|
||||||
<Match when={store.authorization?.method === "auto"}>
|
|
||||||
<OAuthAutoView />
|
|
||||||
</Match>
|
|
||||||
</Switch>
|
|
||||||
</Match>
|
|
||||||
</Switch>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Dialog>
|
<div class="px-2.5 pb-10 flex flex-col gap-6">
|
||||||
|
<div onKeyDown={handleKey} tabIndex={0} autofocus={store.methodIndex === undefined ? true : undefined}>
|
||||||
|
<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}>
|
||||||
|
<MethodSelection />
|
||||||
|
</Match>
|
||||||
|
<Match when={store.state === "pending"}>
|
||||||
|
<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.state === "prompt"}>
|
||||||
|
<AuthPromptsView />
|
||||||
|
</Match>
|
||||||
|
<Match when={store.state === "error"}>
|
||||||
|
<div class="text-14-regular text-text-base">
|
||||||
|
<div class="flex items-center gap-x-2">
|
||||||
|
<Icon name="circle-ban-sign" class="text-icon-critical-base" />
|
||||||
|
<span>{language.t("provider.connect.status.failed", { error: store.error ?? "" })}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Match>
|
||||||
|
<Match when={method()?.type === "api"}>
|
||||||
|
<ApiAuthView />
|
||||||
|
</Match>
|
||||||
|
<Match when={method()?.type === "oauth"}>
|
||||||
|
<Switch>
|
||||||
|
<Match when={store.authorization?.method === "code"}>
|
||||||
|
<OAuthCodeView />
|
||||||
|
</Match>
|
||||||
|
<Match when={store.authorization?.method === "auto"}>
|
||||||
|
<OAuthAutoView />
|
||||||
|
</Match>
|
||||||
|
</Switch>
|
||||||
|
</Match>
|
||||||
|
</Switch>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,21 +6,41 @@ import { ProviderIcon } from "@opencode-ai/ui/provider-icon"
|
|||||||
import { useMutation } from "@tanstack/solid-query"
|
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 "@/utils/toast"
|
import { showToast } from "@/utils/toast"
|
||||||
import { type Accessor, batch, For } from "solid-js"
|
import { batch, For } 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 { useServerSDK } from "@/context/server-sdk"
|
import { useServerSDK } from "@/context/server-sdk"
|
||||||
import { useServerSync } from "@/context/server-sync"
|
import { useServerSync } from "@/context/server-sync"
|
||||||
import { useLanguage } from "@/context/language"
|
import { useLanguage } from "@/context/language"
|
||||||
import { type FormState, headerRow, modelRow, validateCustomProvider } from "./dialog-custom-provider-form"
|
import { type FormState, headerRow, modelRow, validateCustomProvider } from "./dialog-custom-provider-form"
|
||||||
import { DialogSelectProvider } from "./dialog-select-provider"
|
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
back?: "providers" | "close"
|
onBack: () => void
|
||||||
directory?: Accessor<string | undefined>
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function DialogCustomProvider(props: Props) {
|
export function DialogCustomProvider(props: Props) {
|
||||||
|
const language = useLanguage()
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Dialog
|
||||||
|
class="h-full"
|
||||||
|
title={
|
||||||
|
<IconButton
|
||||||
|
tabIndex={-1}
|
||||||
|
icon="arrow-left"
|
||||||
|
variant="ghost"
|
||||||
|
onClick={props.onBack}
|
||||||
|
aria-label={language.t("common.goBack")}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
transition
|
||||||
|
>
|
||||||
|
<CustomProviderForm />
|
||||||
|
</Dialog>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function CustomProviderForm() {
|
||||||
const dialog = useDialog()
|
const dialog = useDialog()
|
||||||
const serverSync = useServerSync()
|
const serverSync = useServerSync()
|
||||||
const serverSDK = useServerSDK()
|
const serverSDK = useServerSDK()
|
||||||
@@ -36,14 +56,6 @@ export function DialogCustomProvider(props: Props) {
|
|||||||
err: {},
|
err: {},
|
||||||
})
|
})
|
||||||
|
|
||||||
const goBack = () => {
|
|
||||||
if (props.back === "close") {
|
|
||||||
dialog.close()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
dialog.show(() => <DialogSelectProvider directory={props.directory} />)
|
|
||||||
}
|
|
||||||
|
|
||||||
const addModel = () => {
|
const addModel = () => {
|
||||||
setForm(
|
setForm(
|
||||||
"models",
|
"models",
|
||||||
@@ -163,168 +175,155 @@ export function DialogCustomProvider(props: Props) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog
|
<div class="flex flex-col gap-6 px-2.5 pb-3 overflow-y-auto max-h-[60vh]">
|
||||||
title={
|
<div class="px-2.5 flex gap-4 items-center">
|
||||||
<IconButton
|
<ProviderIcon id="synthetic" class="size-5 shrink-0 icon-strong-base" />
|
||||||
tabIndex={-1}
|
<div class="text-16-medium text-text-strong">{language.t("provider.custom.title")}</div>
|
||||||
icon="arrow-left"
|
</div>
|
||||||
variant="ghost"
|
|
||||||
onClick={goBack}
|
<form onSubmit={save} class="px-2.5 pb-6 flex flex-col gap-6">
|
||||||
aria-label={language.t("common.goBack")}
|
<p class="text-14-regular text-text-base">
|
||||||
/>
|
{language.t("provider.custom.description.prefix")}
|
||||||
}
|
<Link href="https://opencode.ai/docs/providers/#custom-provider" tabIndex={-1}>
|
||||||
transition
|
{language.t("provider.custom.description.link")}
|
||||||
>
|
</Link>
|
||||||
<div class="flex flex-col gap-6 px-2.5 pb-3 overflow-y-auto max-h-[60vh]">
|
{language.t("provider.custom.description.suffix")}
|
||||||
<div class="px-2.5 flex gap-4 items-center">
|
</p>
|
||||||
<ProviderIcon id="synthetic" class="size-5 shrink-0 icon-strong-base" />
|
|
||||||
<div class="text-16-medium text-text-strong">{language.t("provider.custom.title")}</div>
|
<div class="flex flex-col gap-4">
|
||||||
|
<TextField
|
||||||
|
autofocus
|
||||||
|
label={language.t("provider.custom.field.providerID.label")}
|
||||||
|
placeholder={language.t("provider.custom.field.providerID.placeholder")}
|
||||||
|
description={language.t("provider.custom.field.providerID.description")}
|
||||||
|
value={form.providerID}
|
||||||
|
onChange={(v) => setField("providerID", v)}
|
||||||
|
validationState={form.err.providerID ? "invalid" : undefined}
|
||||||
|
error={form.err.providerID}
|
||||||
|
/>
|
||||||
|
<TextField
|
||||||
|
label={language.t("provider.custom.field.name.label")}
|
||||||
|
placeholder={language.t("provider.custom.field.name.placeholder")}
|
||||||
|
value={form.name}
|
||||||
|
onChange={(v) => setField("name", v)}
|
||||||
|
validationState={form.err.name ? "invalid" : undefined}
|
||||||
|
error={form.err.name}
|
||||||
|
/>
|
||||||
|
<TextField
|
||||||
|
label={language.t("provider.custom.field.baseURL.label")}
|
||||||
|
placeholder={language.t("provider.custom.field.baseURL.placeholder")}
|
||||||
|
value={form.baseURL}
|
||||||
|
onChange={(v) => setField("baseURL", v)}
|
||||||
|
validationState={form.err.baseURL ? "invalid" : undefined}
|
||||||
|
error={form.err.baseURL}
|
||||||
|
/>
|
||||||
|
<TextField
|
||||||
|
label={language.t("provider.custom.field.apiKey.label")}
|
||||||
|
placeholder={language.t("provider.custom.field.apiKey.placeholder")}
|
||||||
|
description={language.t("provider.custom.field.apiKey.description")}
|
||||||
|
value={form.apiKey}
|
||||||
|
onChange={(v) => setField("apiKey", v)}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<form onSubmit={save} class="px-2.5 pb-6 flex flex-col gap-6">
|
<div class="flex flex-col gap-3">
|
||||||
<p class="text-14-regular text-text-base">
|
<label class="text-12-medium text-text-weak">{language.t("provider.custom.models.label")}</label>
|
||||||
{language.t("provider.custom.description.prefix")}
|
<For each={form.models}>
|
||||||
<Link href="https://opencode.ai/docs/providers/#custom-provider" tabIndex={-1}>
|
{(m, i) => (
|
||||||
{language.t("provider.custom.description.link")}
|
<div class="flex gap-2 items-start" data-row={m.row}>
|
||||||
</Link>
|
<div class="flex-1">
|
||||||
{language.t("provider.custom.description.suffix")}
|
<TextField
|
||||||
</p>
|
label={language.t("provider.custom.models.id.label")}
|
||||||
|
hideLabel
|
||||||
<div class="flex flex-col gap-4">
|
placeholder={language.t("provider.custom.models.id.placeholder")}
|
||||||
<TextField
|
value={m.id}
|
||||||
autofocus
|
onChange={(v) => setModel(i(), "id", v)}
|
||||||
label={language.t("provider.custom.field.providerID.label")}
|
validationState={m.err.id ? "invalid" : undefined}
|
||||||
placeholder={language.t("provider.custom.field.providerID.placeholder")}
|
error={m.err.id}
|
||||||
description={language.t("provider.custom.field.providerID.description")}
|
|
||||||
value={form.providerID}
|
|
||||||
onChange={(v) => setField("providerID", v)}
|
|
||||||
validationState={form.err.providerID ? "invalid" : undefined}
|
|
||||||
error={form.err.providerID}
|
|
||||||
/>
|
|
||||||
<TextField
|
|
||||||
label={language.t("provider.custom.field.name.label")}
|
|
||||||
placeholder={language.t("provider.custom.field.name.placeholder")}
|
|
||||||
value={form.name}
|
|
||||||
onChange={(v) => setField("name", v)}
|
|
||||||
validationState={form.err.name ? "invalid" : undefined}
|
|
||||||
error={form.err.name}
|
|
||||||
/>
|
|
||||||
<TextField
|
|
||||||
label={language.t("provider.custom.field.baseURL.label")}
|
|
||||||
placeholder={language.t("provider.custom.field.baseURL.placeholder")}
|
|
||||||
value={form.baseURL}
|
|
||||||
onChange={(v) => setField("baseURL", v)}
|
|
||||||
validationState={form.err.baseURL ? "invalid" : undefined}
|
|
||||||
error={form.err.baseURL}
|
|
||||||
/>
|
|
||||||
<TextField
|
|
||||||
label={language.t("provider.custom.field.apiKey.label")}
|
|
||||||
placeholder={language.t("provider.custom.field.apiKey.placeholder")}
|
|
||||||
description={language.t("provider.custom.field.apiKey.description")}
|
|
||||||
value={form.apiKey}
|
|
||||||
onChange={(v) => setField("apiKey", v)}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="flex flex-col gap-3">
|
|
||||||
<label class="text-12-medium text-text-weak">{language.t("provider.custom.models.label")}</label>
|
|
||||||
<For each={form.models}>
|
|
||||||
{(m, i) => (
|
|
||||||
<div class="flex gap-2 items-start" data-row={m.row}>
|
|
||||||
<div class="flex-1">
|
|
||||||
<TextField
|
|
||||||
label={language.t("provider.custom.models.id.label")}
|
|
||||||
hideLabel
|
|
||||||
placeholder={language.t("provider.custom.models.id.placeholder")}
|
|
||||||
value={m.id}
|
|
||||||
onChange={(v) => setModel(i(), "id", v)}
|
|
||||||
validationState={m.err.id ? "invalid" : undefined}
|
|
||||||
error={m.err.id}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div class="flex-1">
|
|
||||||
<TextField
|
|
||||||
label={language.t("provider.custom.models.name.label")}
|
|
||||||
hideLabel
|
|
||||||
placeholder={language.t("provider.custom.models.name.placeholder")}
|
|
||||||
value={m.name}
|
|
||||||
onChange={(v) => setModel(i(), "name", v)}
|
|
||||||
validationState={m.err.name ? "invalid" : undefined}
|
|
||||||
error={m.err.name}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<IconButton
|
|
||||||
type="button"
|
|
||||||
icon="trash"
|
|
||||||
variant="ghost"
|
|
||||||
class="mt-1.5"
|
|
||||||
onClick={() => removeModel(i())}
|
|
||||||
disabled={form.models.length <= 1}
|
|
||||||
aria-label={language.t("provider.custom.models.remove")}
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
)}
|
<div class="flex-1">
|
||||||
</For>
|
<TextField
|
||||||
<Button type="button" size="small" variant="ghost" icon="plus-small" onClick={addModel} class="self-start">
|
label={language.t("provider.custom.models.name.label")}
|
||||||
{language.t("provider.custom.models.add")}
|
hideLabel
|
||||||
</Button>
|
placeholder={language.t("provider.custom.models.name.placeholder")}
|
||||||
</div>
|
value={m.name}
|
||||||
|
onChange={(v) => setModel(i(), "name", v)}
|
||||||
<div class="flex flex-col gap-3">
|
validationState={m.err.name ? "invalid" : undefined}
|
||||||
<label class="text-12-medium text-text-weak">{language.t("provider.custom.headers.label")}</label>
|
error={m.err.name}
|
||||||
<For each={form.headers}>
|
|
||||||
{(h, i) => (
|
|
||||||
<div class="flex gap-2 items-start" data-row={h.row}>
|
|
||||||
<div class="flex-1">
|
|
||||||
<TextField
|
|
||||||
label={language.t("provider.custom.headers.key.label")}
|
|
||||||
hideLabel
|
|
||||||
placeholder={language.t("provider.custom.headers.key.placeholder")}
|
|
||||||
value={h.key}
|
|
||||||
onChange={(v) => setHeader(i(), "key", v)}
|
|
||||||
validationState={h.err.key ? "invalid" : undefined}
|
|
||||||
error={h.err.key}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div class="flex-1">
|
|
||||||
<TextField
|
|
||||||
label={language.t("provider.custom.headers.value.label")}
|
|
||||||
hideLabel
|
|
||||||
placeholder={language.t("provider.custom.headers.value.placeholder")}
|
|
||||||
value={h.value}
|
|
||||||
onChange={(v) => setHeader(i(), "value", v)}
|
|
||||||
validationState={h.err.value ? "invalid" : undefined}
|
|
||||||
error={h.err.value}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<IconButton
|
|
||||||
type="button"
|
|
||||||
icon="trash"
|
|
||||||
variant="ghost"
|
|
||||||
class="mt-1.5"
|
|
||||||
onClick={() => removeHeader(i())}
|
|
||||||
disabled={form.headers.length <= 1}
|
|
||||||
aria-label={language.t("provider.custom.headers.remove")}
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
)}
|
<IconButton
|
||||||
</For>
|
type="button"
|
||||||
<Button type="button" size="small" variant="ghost" icon="plus-small" onClick={addHeader} class="self-start">
|
icon="trash"
|
||||||
{language.t("provider.custom.headers.add")}
|
variant="ghost"
|
||||||
</Button>
|
class="mt-1.5"
|
||||||
</div>
|
onClick={() => removeModel(i())}
|
||||||
|
disabled={form.models.length <= 1}
|
||||||
<Button
|
aria-label={language.t("provider.custom.models.remove")}
|
||||||
class="w-auto self-start"
|
/>
|
||||||
type="submit"
|
</div>
|
||||||
size="large"
|
)}
|
||||||
variant="primary"
|
</For>
|
||||||
disabled={saveMutation.isPending}
|
<Button type="button" size="small" variant="ghost" icon="plus-small" onClick={addModel} class="self-start">
|
||||||
>
|
{language.t("provider.custom.models.add")}
|
||||||
{saveMutation.isPending ? language.t("common.saving") : language.t("common.submit")}
|
|
||||||
</Button>
|
</Button>
|
||||||
</form>
|
</div>
|
||||||
</div>
|
|
||||||
</Dialog>
|
<div class="flex flex-col gap-3">
|
||||||
|
<label class="text-12-medium text-text-weak">{language.t("provider.custom.headers.label")}</label>
|
||||||
|
<For each={form.headers}>
|
||||||
|
{(h, i) => (
|
||||||
|
<div class="flex gap-2 items-start" data-row={h.row}>
|
||||||
|
<div class="flex-1">
|
||||||
|
<TextField
|
||||||
|
label={language.t("provider.custom.headers.key.label")}
|
||||||
|
hideLabel
|
||||||
|
placeholder={language.t("provider.custom.headers.key.placeholder")}
|
||||||
|
value={h.key}
|
||||||
|
onChange={(v) => setHeader(i(), "key", v)}
|
||||||
|
validationState={h.err.key ? "invalid" : undefined}
|
||||||
|
error={h.err.key}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="flex-1">
|
||||||
|
<TextField
|
||||||
|
label={language.t("provider.custom.headers.value.label")}
|
||||||
|
hideLabel
|
||||||
|
placeholder={language.t("provider.custom.headers.value.placeholder")}
|
||||||
|
value={h.value}
|
||||||
|
onChange={(v) => setHeader(i(), "value", v)}
|
||||||
|
validationState={h.err.value ? "invalid" : undefined}
|
||||||
|
error={h.err.value}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<IconButton
|
||||||
|
type="button"
|
||||||
|
icon="trash"
|
||||||
|
variant="ghost"
|
||||||
|
class="mt-1.5"
|
||||||
|
onClick={() => removeHeader(i())}
|
||||||
|
disabled={form.headers.length <= 1}
|
||||||
|
aria-label={language.t("provider.custom.headers.remove")}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</For>
|
||||||
|
<Button type="button" size="small" variant="ghost" icon="plus-small" onClick={addHeader} class="self-start">
|
||||||
|
{language.t("provider.custom.headers.add")}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
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>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ import { useLocal } from "@/context/local"
|
|||||||
import { popularProviders } from "@/hooks/use-providers"
|
import { popularProviders } from "@/hooks/use-providers"
|
||||||
import { useLanguage } from "@/context/language"
|
import { useLanguage } from "@/context/language"
|
||||||
import { useDialog } from "@opencode-ai/ui/context/dialog"
|
import { useDialog } from "@opencode-ai/ui/context/dialog"
|
||||||
import { DialogSelectProvider } from "./dialog-select-provider"
|
import { DialogConnectProvider } from "./dialog-connect-provider"
|
||||||
import { decode64 } from "@/utils/base64"
|
import { decode64 } from "@/utils/base64"
|
||||||
import { SettingsListV2 } from "./settings-v2/parts/list"
|
import { SettingsListV2 } from "./settings-v2/parts/list"
|
||||||
import { SettingsRowV2 } from "./settings-v2/parts/row"
|
import { SettingsRowV2 } from "./settings-v2/parts/row"
|
||||||
@@ -31,7 +31,7 @@ export const DialogManageModels: Component = () => {
|
|||||||
const directory = () => decode64(local.slug())
|
const directory = () => decode64(local.slug())
|
||||||
|
|
||||||
const handleConnectProvider = () => {
|
const handleConnectProvider = () => {
|
||||||
dialog.show(() => <DialogSelectProvider directory={directory} />)
|
void dialog.show(() => <DialogConnectProvider directory={directory} />)
|
||||||
}
|
}
|
||||||
const providerRank = (id: string) => popularProviders.indexOf(id)
|
const providerRank = (id: string) => popularProviders.indexOf(id)
|
||||||
const providerList = (providerID: string) => local.model.list().filter((x) => x.provider.id === providerID)
|
const providerList = (providerID: string) => local.model.list().filter((x) => x.provider.id === providerID)
|
||||||
@@ -123,7 +123,7 @@ export const DialogManageModelsV2: Component = () => {
|
|||||||
const directory = () => decode64(local.slug())
|
const directory = () => decode64(local.slug())
|
||||||
|
|
||||||
const handleConnectProvider = () => {
|
const handleConnectProvider = () => {
|
||||||
dialog.show(() => <DialogSelectProvider directory={directory} />)
|
void dialog.show(() => <DialogConnectProvider directory={directory} />)
|
||||||
}
|
}
|
||||||
const providerList = (providerID: string) => local.model.list().filter((x) => x.provider.id === providerID)
|
const providerList = (providerID: string) => local.model.list().filter((x) => x.provider.id === providerID)
|
||||||
const providerVisible = (providerID: string) =>
|
const providerVisible = (providerID: string) =>
|
||||||
|
|||||||
@@ -1,329 +1,81 @@
|
|||||||
import { useDialog } from "@opencode-ai/ui/context/dialog"
|
|
||||||
import { Dialog } from "@opencode-ai/ui/dialog"
|
import { Dialog } from "@opencode-ai/ui/dialog"
|
||||||
import { FileIcon } from "@opencode-ai/ui/file-icon"
|
import { FileIcon } from "@opencode-ai/ui/file-icon"
|
||||||
import { Icon } from "@opencode-ai/ui/icon"
|
import { Icon } from "@opencode-ai/ui/icon"
|
||||||
import { Keybind } from "@opencode-ai/ui/keybind"
|
import { Keybind } from "@opencode-ai/ui/keybind"
|
||||||
import { List } from "@opencode-ai/ui/list"
|
import { List } from "@opencode-ai/ui/list"
|
||||||
import { base64Encode } from "@opencode-ai/core/util/encode"
|
|
||||||
import { getDirectory, getFilename } from "@opencode-ai/core/util/path"
|
import { getDirectory, getFilename } from "@opencode-ai/core/util/path"
|
||||||
import { useNavigate } from "@solidjs/router"
|
import { createMemo, createSignal, lazy, Match, Show, Switch } from "solid-js"
|
||||||
import { createMemo, createSignal, lazy, Match, onCleanup, Show, Switch } from "solid-js"
|
import { formatKeybind } from "@/context/command"
|
||||||
import { formatKeybind, useCommand, type CommandOption } from "@/context/command"
|
import { useServerSDK } from "@/context/server-sdk"
|
||||||
import { useServerSDK, type ServerSDK } from "@/context/server-sdk"
|
|
||||||
import { useServerSync } from "@/context/server-sync"
|
|
||||||
import { useLayout } from "@/context/layout"
|
|
||||||
import { useFile } from "@/context/file"
|
|
||||||
import { useLanguage } from "@/context/language"
|
import { useLanguage } from "@/context/language"
|
||||||
import { usePlatform } from "@/context/platform"
|
import { usePlatform } from "@/context/platform"
|
||||||
import { useSettings } from "@/context/settings"
|
import { useSettings } from "@/context/settings"
|
||||||
import { useSessionLayout } from "@/pages/session/session-layout"
|
import { useSessionLayout } from "@/pages/session/session-layout"
|
||||||
import { createSessionTabs } from "@/pages/session/helpers"
|
|
||||||
import { decode64 } from "@/utils/base64"
|
import { decode64 } from "@/utils/base64"
|
||||||
import { getRelativeTime } from "@/utils/time"
|
import { getRelativeTime } from "@/utils/time"
|
||||||
|
import {
|
||||||
|
createCommandPaletteFileEntry,
|
||||||
|
createCommandPaletteFileOpener,
|
||||||
|
createCommandPaletteModel,
|
||||||
|
uniqueCommandPaletteEntries,
|
||||||
|
type CommandPaletteEntry,
|
||||||
|
} from "./command-palette"
|
||||||
|
import { DialogCommandPaletteV2 } from "./dialog-command-palette-v2"
|
||||||
|
|
||||||
const DialogSelectFileV2 = lazy(() =>
|
const DialogSelectFileV2 = lazy(() =>
|
||||||
import("./dialog-select-directory-v2").then((module) => ({ default: module.DialogSelectDirectoryV2 })),
|
import("./dialog-select-directory-v2").then((module) => ({ default: module.DialogSelectDirectoryV2 })),
|
||||||
)
|
)
|
||||||
|
|
||||||
type EntryType = "command" | "file" | "session"
|
|
||||||
|
|
||||||
type Entry = {
|
|
||||||
id: string
|
|
||||||
type: EntryType
|
|
||||||
title: string
|
|
||||||
description?: string
|
|
||||||
keybind?: string
|
|
||||||
category: string
|
|
||||||
option?: CommandOption
|
|
||||||
path?: string
|
|
||||||
directory?: string
|
|
||||||
sessionID?: string
|
|
||||||
archived?: number
|
|
||||||
updated?: number
|
|
||||||
}
|
|
||||||
|
|
||||||
type DialogSelectFileMode = "all" | "files"
|
type DialogSelectFileMode = "all" | "files"
|
||||||
|
|
||||||
const ENTRY_LIMIT = 5
|
|
||||||
const COMMON_COMMAND_IDS = [
|
|
||||||
"session.new",
|
|
||||||
"workspace.new",
|
|
||||||
"session.previous",
|
|
||||||
"session.next",
|
|
||||||
"terminal.toggle",
|
|
||||||
"review.toggle",
|
|
||||||
] as const
|
|
||||||
|
|
||||||
const uniqueEntries = (items: Entry[]) => {
|
|
||||||
const seen = new Set<string>()
|
|
||||||
const out: Entry[] = []
|
|
||||||
for (const item of items) {
|
|
||||||
if (seen.has(item.id)) continue
|
|
||||||
seen.add(item.id)
|
|
||||||
out.push(item)
|
|
||||||
}
|
|
||||||
return out
|
|
||||||
}
|
|
||||||
|
|
||||||
const createCommandEntry = (option: CommandOption, category: string): Entry => ({
|
|
||||||
id: "command:" + option.id,
|
|
||||||
type: "command",
|
|
||||||
title: option.title,
|
|
||||||
description: option.description,
|
|
||||||
keybind: option.keybind,
|
|
||||||
category,
|
|
||||||
option,
|
|
||||||
})
|
|
||||||
|
|
||||||
const createFileEntry = (path: string, category: string): Entry => ({
|
|
||||||
id: "file:" + path,
|
|
||||||
type: "file",
|
|
||||||
title: path,
|
|
||||||
category,
|
|
||||||
path,
|
|
||||||
})
|
|
||||||
|
|
||||||
const createSessionEntry = (
|
|
||||||
input: {
|
|
||||||
directory: string
|
|
||||||
id: string
|
|
||||||
title: string
|
|
||||||
description: string
|
|
||||||
archived?: number
|
|
||||||
updated?: number
|
|
||||||
},
|
|
||||||
category: string,
|
|
||||||
): Entry => ({
|
|
||||||
id: `session:${input.directory}:${input.id}`,
|
|
||||||
type: "session",
|
|
||||||
title: input.title,
|
|
||||||
description: input.description,
|
|
||||||
category,
|
|
||||||
directory: input.directory,
|
|
||||||
sessionID: input.id,
|
|
||||||
archived: input.archived,
|
|
||||||
updated: input.updated,
|
|
||||||
})
|
|
||||||
|
|
||||||
function createCommandEntries(props: {
|
|
||||||
filesOnly: () => boolean
|
|
||||||
command: ReturnType<typeof useCommand>
|
|
||||||
language: ReturnType<typeof useLanguage>
|
|
||||||
}) {
|
|
||||||
const allowed = createMemo(() => {
|
|
||||||
if (props.filesOnly()) return []
|
|
||||||
return props.command.options.filter(
|
|
||||||
(option) =>
|
|
||||||
!option.disabled && !option.hidden && !option.id.startsWith("suggested.") && option.id !== "file.open",
|
|
||||||
)
|
|
||||||
})
|
|
||||||
|
|
||||||
const list = createMemo(() => {
|
|
||||||
const category = props.language.t("palette.group.commands")
|
|
||||||
return allowed().map((option) => createCommandEntry(option, category))
|
|
||||||
})
|
|
||||||
|
|
||||||
const picks = createMemo(() => {
|
|
||||||
const all = allowed()
|
|
||||||
const order = new Map<string, number>(COMMON_COMMAND_IDS.map((id, index) => [id, index]))
|
|
||||||
const picked = all.filter((option) => order.has(option.id))
|
|
||||||
const base = picked.length ? picked : all.slice(0, ENTRY_LIMIT)
|
|
||||||
const sorted = picked.length ? [...base].sort((a, b) => (order.get(a.id) ?? 0) - (order.get(b.id) ?? 0)) : base
|
|
||||||
const category = props.language.t("palette.group.commands")
|
|
||||||
return sorted.map((option) => createCommandEntry(option, category))
|
|
||||||
})
|
|
||||||
|
|
||||||
return { allowed, list, picks }
|
|
||||||
}
|
|
||||||
|
|
||||||
function createFileEntries(props: {
|
|
||||||
file: ReturnType<typeof useFile>
|
|
||||||
tabs: () => ReturnType<ReturnType<typeof useLayout>["tabs"]>
|
|
||||||
language: ReturnType<typeof useLanguage>
|
|
||||||
}) {
|
|
||||||
const tabState = createSessionTabs({
|
|
||||||
tabs: props.tabs,
|
|
||||||
pathFromTab: props.file.pathFromTab,
|
|
||||||
normalizeTab: (tab) => (tab.startsWith("file://") ? props.file.tab(tab) : tab),
|
|
||||||
})
|
|
||||||
const recent = createMemo(() => {
|
|
||||||
const all = tabState.openedTabs()
|
|
||||||
const active = tabState.activeFileTab()
|
|
||||||
const order = active ? [active, ...all.filter((item) => item !== active)] : all
|
|
||||||
const seen = new Set<string>()
|
|
||||||
const category = props.language.t("palette.group.files")
|
|
||||||
const items: Entry[] = []
|
|
||||||
|
|
||||||
for (const item of order) {
|
|
||||||
const path = props.file.pathFromTab(item)
|
|
||||||
if (!path) continue
|
|
||||||
if (seen.has(path)) continue
|
|
||||||
seen.add(path)
|
|
||||||
items.push(createFileEntry(path, category))
|
|
||||||
}
|
|
||||||
|
|
||||||
return items.slice(0, ENTRY_LIMIT)
|
|
||||||
})
|
|
||||||
|
|
||||||
const root = createMemo(() => {
|
|
||||||
const category = props.language.t("palette.group.files")
|
|
||||||
const nodes = props.file.tree.children("")
|
|
||||||
const paths = nodes
|
|
||||||
.filter((node) => node.type === "file")
|
|
||||||
.map((node) => node.path)
|
|
||||||
.sort((a, b) => a.localeCompare(b))
|
|
||||||
return paths.slice(0, ENTRY_LIMIT).map((path) => createFileEntry(path, category))
|
|
||||||
})
|
|
||||||
|
|
||||||
return { recent, root }
|
|
||||||
}
|
|
||||||
|
|
||||||
function createSessionEntries(props: {
|
|
||||||
workspaces: () => string[]
|
|
||||||
label: (directory: string) => string
|
|
||||||
serverSDK: ServerSDK
|
|
||||||
language: ReturnType<typeof useLanguage>
|
|
||||||
}) {
|
|
||||||
const state: {
|
|
||||||
token: number
|
|
||||||
inflight: Promise<Entry[]> | undefined
|
|
||||||
cached: Entry[] | undefined
|
|
||||||
} = {
|
|
||||||
token: 0,
|
|
||||||
inflight: undefined,
|
|
||||||
cached: undefined,
|
|
||||||
}
|
|
||||||
|
|
||||||
const sessions = (text: string) => {
|
|
||||||
const query = text.trim()
|
|
||||||
if (!query) {
|
|
||||||
state.token += 1
|
|
||||||
state.inflight = undefined
|
|
||||||
state.cached = undefined
|
|
||||||
return [] as Entry[]
|
|
||||||
}
|
|
||||||
|
|
||||||
if (state.cached) return state.cached
|
|
||||||
if (state.inflight) return state.inflight
|
|
||||||
|
|
||||||
const current = state.token
|
|
||||||
const dirs = props.workspaces()
|
|
||||||
if (dirs.length === 0) return [] as Entry[]
|
|
||||||
|
|
||||||
state.inflight = Promise.all(
|
|
||||||
dirs.map((directory) => {
|
|
||||||
const description = props.label(directory)
|
|
||||||
return props.serverSDK.client.session
|
|
||||||
.list({ directory, roots: true })
|
|
||||||
.then((x) =>
|
|
||||||
(x.data ?? [])
|
|
||||||
.filter((s) => !!s?.id)
|
|
||||||
.map((s) => ({
|
|
||||||
id: s.id,
|
|
||||||
title: s.title ?? props.language.t("command.session.new"),
|
|
||||||
description,
|
|
||||||
directory,
|
|
||||||
archived: s.time?.archived,
|
|
||||||
updated: s.time?.updated,
|
|
||||||
})),
|
|
||||||
)
|
|
||||||
.catch(
|
|
||||||
() =>
|
|
||||||
[] as {
|
|
||||||
id: string
|
|
||||||
title: string
|
|
||||||
description: string
|
|
||||||
directory: string
|
|
||||||
archived?: number
|
|
||||||
updated?: number
|
|
||||||
}[],
|
|
||||||
)
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
.then((results) => {
|
|
||||||
if (state.token !== current) return [] as Entry[]
|
|
||||||
const seen = new Set<string>()
|
|
||||||
const category = props.language.t("command.category.session")
|
|
||||||
const next = results
|
|
||||||
.flat()
|
|
||||||
.filter((item) => {
|
|
||||||
const key = `${item.directory}:${item.id}`
|
|
||||||
if (seen.has(key)) return false
|
|
||||||
seen.add(key)
|
|
||||||
return true
|
|
||||||
})
|
|
||||||
.map((item) => createSessionEntry(item, category))
|
|
||||||
state.cached = next
|
|
||||||
return next
|
|
||||||
})
|
|
||||||
.catch(() => [] as Entry[])
|
|
||||||
.finally(() => {
|
|
||||||
state.inflight = undefined
|
|
||||||
})
|
|
||||||
|
|
||||||
return state.inflight
|
|
||||||
}
|
|
||||||
|
|
||||||
return { sessions }
|
|
||||||
}
|
|
||||||
|
|
||||||
export function DialogSelectFile(props: { mode?: DialogSelectFileMode; onOpenFile?: (path: string) => void }) {
|
export function DialogSelectFile(props: { mode?: DialogSelectFileMode; onOpenFile?: (path: string) => void }) {
|
||||||
const command = useCommand()
|
|
||||||
const language = useLanguage()
|
|
||||||
const platform = usePlatform()
|
const platform = usePlatform()
|
||||||
const settings = useSettings()
|
const settings = useSettings()
|
||||||
const layout = useLayout()
|
|
||||||
const file = useFile()
|
|
||||||
const dialog = useDialog()
|
|
||||||
const navigate = useNavigate()
|
|
||||||
const serverSDK = useServerSDK()
|
|
||||||
const serverSync = useServerSync()
|
|
||||||
const { params, tabs, view } = useSessionLayout()
|
|
||||||
const filesOnly = () => props.mode === "files"
|
const filesOnly = () => props.mode === "files"
|
||||||
const state = { cleanup: undefined as (() => void) | void, committed: false }
|
|
||||||
const [grouped, setGrouped] = createSignal(false)
|
|
||||||
const commandEntries = createCommandEntries({ filesOnly, command, language })
|
|
||||||
const fileEntries = createFileEntries({ file, tabs, language })
|
|
||||||
|
|
||||||
const projectDirectory = createMemo(() => decode64(params.dir) ?? "")
|
if (!filesOnly() && settings.general.newLayoutDesigns()) {
|
||||||
const project = createMemo(() => {
|
return <DialogCommandPaletteV2 onOpenFile={props.onOpenFile} />
|
||||||
const directory = projectDirectory()
|
|
||||||
if (!directory) return
|
|
||||||
return layout.projects.list().find((p) => p.worktree === directory || p.sandboxes?.includes(directory))
|
|
||||||
})
|
|
||||||
const workspaces = createMemo(() => {
|
|
||||||
const directory = projectDirectory()
|
|
||||||
const current = project()
|
|
||||||
if (!current) return directory ? [directory] : []
|
|
||||||
|
|
||||||
const dirs = [current.worktree, ...(current.sandboxes ?? [])]
|
|
||||||
if (directory && !dirs.includes(directory)) return [...dirs, directory]
|
|
||||||
return dirs
|
|
||||||
})
|
|
||||||
const homedir = createMemo(() => serverSync().data.path.home)
|
|
||||||
const label = (directory: string) => {
|
|
||||||
const current = project()
|
|
||||||
const kind =
|
|
||||||
current && directory === current.worktree
|
|
||||||
? language.t("workspace.type.local")
|
|
||||||
: language.t("workspace.type.sandbox")
|
|
||||||
const [store] = serverSync().child(directory, { bootstrap: false })
|
|
||||||
const home = homedir()
|
|
||||||
const path = home ? directory.replace(home, "~") : directory
|
|
||||||
const name = store.vcs?.branch ?? getFilename(directory)
|
|
||||||
return `${kind} : ${name || path}`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const { sessions } = createSessionEntries({ workspaces, label, serverSDK: serverSDK(), language })
|
if (filesOnly() && platform.platform === "desktop" && settings.general.newLayoutDesigns()) {
|
||||||
|
return <DialogSelectFileDesktopV2 onOpenFile={props.onOpenFile} />
|
||||||
|
}
|
||||||
|
|
||||||
|
return <DialogSelectFileLegacy filesOnly={filesOnly} onOpenFile={props.onOpenFile} />
|
||||||
|
}
|
||||||
|
|
||||||
|
function DialogSelectFileDesktopV2(props: { onOpenFile?: (path: string) => void }) {
|
||||||
|
const language = useLanguage()
|
||||||
|
const serverSDK = useServerSDK()
|
||||||
|
const { params } = useSessionLayout()
|
||||||
|
const projectDirectory = createMemo(() => decode64(params.dir) ?? "")
|
||||||
|
const openFile = createCommandPaletteFileOpener(props.onOpenFile)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<DialogSelectFileV2
|
||||||
|
server={serverSDK().server}
|
||||||
|
mode="file"
|
||||||
|
start={projectDirectory()}
|
||||||
|
title={language.t("session.header.searchFiles")}
|
||||||
|
onSelect={(result) => {
|
||||||
|
if (typeof result !== "string") return
|
||||||
|
openFile(result)
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function DialogSelectFileLegacy(props: { filesOnly: () => boolean; onOpenFile?: (path: string) => void }) {
|
||||||
|
const palette = createCommandPaletteModel(props)
|
||||||
|
const [grouped, setGrouped] = createSignal(false)
|
||||||
|
|
||||||
const items = async (text: string) => {
|
const items = async (text: string) => {
|
||||||
const query = text.trim()
|
const query = text.trim()
|
||||||
setGrouped(query.length > 0)
|
setGrouped(query.length > 0)
|
||||||
|
|
||||||
if (!query && filesOnly()) {
|
if (!query && props.filesOnly()) {
|
||||||
const loaded = file.tree.state("")?.loaded
|
const loaded = palette.file.tree.state("")?.loaded
|
||||||
const pending = loaded ? Promise.resolve() : file.tree.list("")
|
const pending = loaded ? Promise.resolve() : palette.file.tree.list("")
|
||||||
const next = uniqueEntries([...fileEntries.recent(), ...fileEntries.root()])
|
const next = uniqueCommandPaletteEntries([...palette.recentFileEntries(), ...palette.rootFileEntries()])
|
||||||
|
|
||||||
if (loaded || next.length > 0) {
|
if (loaded || next.length > 0) {
|
||||||
void pending
|
void pending
|
||||||
@@ -331,79 +83,24 @@ export function DialogSelectFile(props: { mode?: DialogSelectFileMode; onOpenFil
|
|||||||
}
|
}
|
||||||
|
|
||||||
await pending
|
await pending
|
||||||
return uniqueEntries([...fileEntries.recent(), ...fileEntries.root()])
|
return uniqueCommandPaletteEntries([...palette.recentFileEntries(), ...palette.rootFileEntries()])
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!query) return [...commandEntries.picks(), ...fileEntries.recent()]
|
if (!query) return [...palette.preferredCommandEntries(), ...palette.recentFileEntries()]
|
||||||
|
|
||||||
if (filesOnly()) {
|
if (props.filesOnly()) {
|
||||||
const files = await file.searchFiles(query)
|
const files = await palette.file.searchFiles(query)
|
||||||
const category = language.t("palette.group.files")
|
const category = palette.language.t("palette.group.files")
|
||||||
return files.map((path) => createFileEntry(path, category))
|
return files.map((path) => createCommandPaletteFileEntry(path, category))
|
||||||
}
|
}
|
||||||
|
|
||||||
const [files, nextSessions] = await Promise.all([file.searchFiles(query), Promise.resolve(sessions(query))])
|
const [files, nextSessions] = await Promise.all([
|
||||||
const category = language.t("palette.group.files")
|
palette.file.searchFiles(query),
|
||||||
const entries = files.map((path) => createFileEntry(path, category))
|
Promise.resolve(palette.sessions(query)),
|
||||||
return [...commandEntries.list(), ...nextSessions, ...entries]
|
])
|
||||||
}
|
const category = palette.language.t("palette.group.files")
|
||||||
|
const entries = files.map((path) => createCommandPaletteFileEntry(path, category))
|
||||||
const handleMove = (item: Entry | undefined) => {
|
return [...palette.commandEntries(), ...nextSessions, ...entries]
|
||||||
state.cleanup?.()
|
|
||||||
if (!item) return
|
|
||||||
if (item.type !== "command") return
|
|
||||||
state.cleanup = item.option?.onHighlight?.()
|
|
||||||
}
|
|
||||||
|
|
||||||
const open = (path: string) => {
|
|
||||||
const value = file.tab(path)
|
|
||||||
void tabs().open(value)
|
|
||||||
void file.load(path)
|
|
||||||
if (!view().reviewPanel.opened()) view().reviewPanel.open()
|
|
||||||
layout.fileTree.setTab("all")
|
|
||||||
props.onOpenFile?.(path)
|
|
||||||
tabs().setActive(value)
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleSelect = (item: Entry | undefined) => {
|
|
||||||
if (!item) return
|
|
||||||
state.committed = true
|
|
||||||
state.cleanup = undefined
|
|
||||||
dialog.close()
|
|
||||||
|
|
||||||
if (item.type === "command") {
|
|
||||||
item.option?.onSelect?.("palette")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if (item.type === "session") {
|
|
||||||
if (!item.directory || !item.sessionID) return
|
|
||||||
navigate(`/${base64Encode(item.directory)}/session/${item.sessionID}`)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!item.path) return
|
|
||||||
open(item.path)
|
|
||||||
}
|
|
||||||
|
|
||||||
onCleanup(() => {
|
|
||||||
if (state.committed) return
|
|
||||||
state.cleanup?.()
|
|
||||||
})
|
|
||||||
|
|
||||||
if (filesOnly() && platform.platform === "desktop" && settings.general.newLayoutDesigns()) {
|
|
||||||
return (
|
|
||||||
<DialogSelectFileV2
|
|
||||||
server={serverSDK().server}
|
|
||||||
mode="file"
|
|
||||||
start={projectDirectory()}
|
|
||||||
title={language.t("session.header.searchFiles")}
|
|
||||||
onSelect={(result) => {
|
|
||||||
if (typeof result !== "string") return
|
|
||||||
open(result)
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -411,21 +108,21 @@ export function DialogSelectFile(props: { mode?: DialogSelectFileMode; onOpenFil
|
|||||||
<List
|
<List
|
||||||
class="px-3"
|
class="px-3"
|
||||||
search={{
|
search={{
|
||||||
placeholder: filesOnly()
|
placeholder: props.filesOnly()
|
||||||
? language.t("session.header.searchFiles")
|
? palette.language.t("session.header.searchFiles")
|
||||||
: language.t("palette.search.placeholder"),
|
: palette.language.t("palette.search.placeholder"),
|
||||||
autofocus: true,
|
autofocus: true,
|
||||||
hideIcon: true,
|
hideIcon: true,
|
||||||
}}
|
}}
|
||||||
emptyMessage={language.t("palette.empty")}
|
emptyMessage={palette.language.t("palette.empty")}
|
||||||
loadingMessage={language.t("common.loading")}
|
loadingMessage={palette.language.t("common.loading")}
|
||||||
items={items}
|
items={items}
|
||||||
key={(item) => item.id}
|
key={(item) => item.id}
|
||||||
filterKeys={["title", "description", "category"]}
|
filterKeys={["title", "description", "category"]}
|
||||||
skipFilter={(item) => item.type === "file"}
|
skipFilter={(item) => item.type === "file"}
|
||||||
groupBy={grouped() ? (item) => item.category : () => ""}
|
groupBy={grouped() ? (item) => item.category : () => ""}
|
||||||
onMove={handleMove}
|
onMove={(item: CommandPaletteEntry | undefined) => palette.highlight(item)}
|
||||||
onSelect={handleSelect}
|
onSelect={(item: CommandPaletteEntry | undefined) => palette.select(item)}
|
||||||
>
|
>
|
||||||
{(item) => (
|
{(item) => (
|
||||||
<Switch
|
<Switch
|
||||||
@@ -452,7 +149,7 @@ export function DialogSelectFile(props: { mode?: DialogSelectFileMode; onOpenFil
|
|||||||
</Show>
|
</Show>
|
||||||
</div>
|
</div>
|
||||||
<Show when={item.keybind}>
|
<Show when={item.keybind}>
|
||||||
<Keybind class="rounded-[4px]">{formatKeybind(item.keybind ?? "", language.t)}</Keybind>
|
<Keybind class="rounded-[4px]">{formatKeybind(item.keybind ?? "", palette.language.t)}</Keybind>
|
||||||
</Show>
|
</Show>
|
||||||
</div>
|
</div>
|
||||||
</Match>
|
</Match>
|
||||||
@@ -479,7 +176,7 @@ export function DialogSelectFile(props: { mode?: DialogSelectFileMode; onOpenFil
|
|||||||
</div>
|
</div>
|
||||||
<Show when={item.updated}>
|
<Show when={item.updated}>
|
||||||
<span class="text-12-regular text-text-weak whitespace-nowrap ml-2">
|
<span class="text-12-regular text-text-weak whitespace-nowrap ml-2">
|
||||||
{getRelativeTime(new Date(item.updated!).toISOString(), language.t)}
|
{getRelativeTime(new Date(item.updated!).toISOString(), palette.language.t)}
|
||||||
</span>
|
</span>
|
||||||
</Show>
|
</Show>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -0,0 +1,19 @@
|
|||||||
|
import { describe, expect, test } from "bun:test"
|
||||||
|
import { matchesModelSearch } from "./dialog-select-model-search"
|
||||||
|
|
||||||
|
describe("matchesModelSearch", () => {
|
||||||
|
test("matches model names across separators", () => {
|
||||||
|
expect(matchesModelSearch("gpt 5", ["GPT-5.5"])).toBe(true)
|
||||||
|
expect(matchesModelSearch("gpt-5", ["GPT-5.5"])).toBe(true)
|
||||||
|
expect(matchesModelSearch("gpt5", ["GPT-5.5"])).toBe(true)
|
||||||
|
})
|
||||||
|
|
||||||
|
test("matches any searchable model field", () => {
|
||||||
|
expect(matchesModelSearch("open ai", ["GPT-5.5", "gpt-5.5", "OpenAI"])).toBe(true)
|
||||||
|
expect(matchesModelSearch("gpt 5", ["GPT-5.5", "gpt-5.5", "OpenAI"])).toBe(true)
|
||||||
|
})
|
||||||
|
|
||||||
|
test("does not match unrelated searches", () => {
|
||||||
|
expect(matchesModelSearch("claude", ["GPT-5.5", "gpt-5.5", "OpenAI"])).toBe(false)
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
export const normalizeModelSearch = (value: string) =>
|
||||||
|
value
|
||||||
|
.toLowerCase()
|
||||||
|
.replace(/[^\p{Letter}\p{Number}]+/gu, " ")
|
||||||
|
.trim()
|
||||||
|
.replace(/\s+/g, " ")
|
||||||
|
|
||||||
|
export const compactModelSearch = (value: string) => normalizeModelSearch(value).replaceAll(" ", "")
|
||||||
|
|
||||||
|
export const matchesModelSearch = (query: string, values: string[]) => {
|
||||||
|
const search = normalizeModelSearch(query)
|
||||||
|
if (!search) return true
|
||||||
|
|
||||||
|
const compactSearch = compactModelSearch(query)
|
||||||
|
return values.some(
|
||||||
|
(value) => normalizeModelSearch(value).includes(search) || compactModelSearch(value).includes(compactSearch),
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -22,17 +22,16 @@ export const DialogSelectModelUnpaid: Component<{ model?: ModelState }> = (props
|
|||||||
const providers = useProviders(directory)
|
const providers = useProviders(directory)
|
||||||
const language = useLanguage()
|
const language = useLanguage()
|
||||||
|
|
||||||
const connect = (provider: string) => {
|
const openProviders = (provider?: string) => {
|
||||||
void import("./dialog-connect-provider").then((x) => {
|
void import("./dialog-connect-provider").then((x) => {
|
||||||
dialog.show(() => <x.DialogConnectProvider provider={provider} directory={directory} />)
|
const controller = x.useProviderConnectController()
|
||||||
|
controller.select(provider)
|
||||||
|
void dialog.show(() => <x.DialogConnectProvider controller={controller} directory={directory} />)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
const all = () => {
|
const connect = (provider: string) => openProviders(provider)
|
||||||
void import("./dialog-select-provider").then((x) => {
|
const all = () => openProviders()
|
||||||
dialog.show(() => <x.DialogSelectProvider directory={directory} />)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
let listRef: ListRef | undefined
|
let listRef: ListRef | undefined
|
||||||
const handleKeyDown = (e: KeyboardEvent) => {
|
const handleKeyDown = (e: KeyboardEvent) => {
|
||||||
|
|||||||
@@ -1,5 +1,15 @@
|
|||||||
import { Popover as Kobalte } from "@kobalte/core/popover"
|
import { Popover as Kobalte } from "@kobalte/core/popover"
|
||||||
import { Component, ComponentProps, createMemo, For, JSX, Show, ValidComponent } from "solid-js"
|
import {
|
||||||
|
Component,
|
||||||
|
ComponentProps,
|
||||||
|
createEffect,
|
||||||
|
createMemo,
|
||||||
|
For,
|
||||||
|
JSX,
|
||||||
|
onCleanup,
|
||||||
|
Show,
|
||||||
|
ValidComponent,
|
||||||
|
} 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 { useDialog } from "@opencode-ai/ui/context/dialog"
|
import { useDialog } from "@opencode-ai/ui/context/dialog"
|
||||||
@@ -14,9 +24,13 @@ import { Tooltip } from "@opencode-ai/ui/tooltip"
|
|||||||
import { Icon } from "@opencode-ai/ui/v2/icon"
|
import { Icon } from "@opencode-ai/ui/v2/icon"
|
||||||
import { Tag as TagV2 } from "@opencode-ai/ui/v2/badge-v2"
|
import { Tag as TagV2 } from "@opencode-ai/ui/v2/badge-v2"
|
||||||
import { MenuV2 } from "@opencode-ai/ui/v2/menu-v2"
|
import { MenuV2 } from "@opencode-ai/ui/v2/menu-v2"
|
||||||
|
import { TooltipV2 } from "@opencode-ai/ui/v2/tooltip-v2"
|
||||||
import { ModelTooltip } from "./model-tooltip"
|
import { ModelTooltip } from "./model-tooltip"
|
||||||
import { useLanguage } from "@/context/language"
|
import { useLanguage } from "@/context/language"
|
||||||
import { decode64 } from "@/utils/base64"
|
import { decode64 } from "@/utils/base64"
|
||||||
|
import { handleDocumentSearchKeydown } from "@/utils/search-keydown"
|
||||||
|
import { createEventListener } from "@solid-primitives/event-listener"
|
||||||
|
import { matchesModelSearch } from "./dialog-select-model-search"
|
||||||
|
|
||||||
const isFree = (provider: string, cost: { input: number } | undefined) =>
|
const isFree = (provider: string, cost: { input: number } | undefined) =>
|
||||||
provider === "opencode" && (!cost || cost.input === 0)
|
provider === "opencode" && (!cost || cost.input === 0)
|
||||||
@@ -79,6 +93,7 @@ const ModelList: Component<{
|
|||||||
class="w-full"
|
class="w-full"
|
||||||
placement="right-start"
|
placement="right-start"
|
||||||
gutter={12}
|
gutter={12}
|
||||||
|
openDelay={0}
|
||||||
value={<ModelTooltip model={item} latest={item.latest} free={isFree(item.provider.id, item.cost)} />}
|
value={<ModelTooltip model={item} latest={item.latest} free={isFree(item.provider.id, item.cost)} />}
|
||||||
>
|
>
|
||||||
{node}
|
{node}
|
||||||
@@ -142,8 +157,8 @@ export function ModelSelectorPopover(props: {
|
|||||||
|
|
||||||
const handleConnectProvider = () => {
|
const handleConnectProvider = () => {
|
||||||
close("provider")
|
close("provider")
|
||||||
void import("./dialog-select-provider").then((x) => {
|
void import("./dialog-connect-provider").then((x) => {
|
||||||
dialog.show(() => <x.DialogSelectProvider directory={directory} />)
|
void dialog.show(() => <x.DialogConnectProvider directory={directory} />)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
const language = useLanguage()
|
const language = useLanguage()
|
||||||
@@ -242,14 +257,9 @@ export function ModelSelectorPopoverV2(props: {
|
|||||||
.filter((item) => (props.provider ? item.provider.id === props.provider : true)),
|
.filter((item) => (props.provider ? item.provider.id === props.provider : true)),
|
||||||
)
|
)
|
||||||
const models = createMemo(() => {
|
const models = createMemo(() => {
|
||||||
const search = store.search.trim().toLowerCase()
|
const search = store.search.trim()
|
||||||
const filtered = search
|
const filtered = search
|
||||||
? allModels().filter(
|
? allModels().filter((item) => matchesModelSearch(search, [item.name, item.id, item.provider.name]))
|
||||||
(item) =>
|
|
||||||
item.name.toLowerCase().includes(search) ||
|
|
||||||
item.id.toLowerCase().includes(search) ||
|
|
||||||
item.provider.name.toLowerCase().includes(search),
|
|
||||||
)
|
|
||||||
: allModels()
|
: allModels()
|
||||||
|
|
||||||
return [...filtered].sort((a, b) => a.name.localeCompare(b.name))
|
return [...filtered].sort((a, b) => a.name.localeCompare(b.name))
|
||||||
@@ -333,19 +343,23 @@ export function ModelSelectorPopoverV2(props: {
|
|||||||
queueMicrotask(() => activeItem()?.scrollIntoView({ block: "nearest" }))
|
queueMicrotask(() => activeItem()?.scrollIntoView({ block: "nearest" }))
|
||||||
}
|
}
|
||||||
const setSearch = (value: string) => {
|
const setSearch = (value: string) => {
|
||||||
const search = value.trim().toLowerCase()
|
const search = value.trim()
|
||||||
const first = [...allModels()]
|
const first = [...allModels()]
|
||||||
.sort((a, b) => a.name.localeCompare(b.name))
|
.sort((a, b) => a.name.localeCompare(b.name))
|
||||||
.find(
|
.find((item) => matchesModelSearch(search, [item.name, item.id, item.provider.name]))
|
||||||
(item) =>
|
|
||||||
!search ||
|
|
||||||
item.name.toLowerCase().includes(search) ||
|
|
||||||
item.id.toLowerCase().includes(search) ||
|
|
||||||
item.provider.name.toLowerCase().includes(search),
|
|
||||||
)
|
|
||||||
setStore({ search: value, active: first ? modelKey(first) : manageKey })
|
setStore({ search: value, active: first ? modelKey(first) : manageKey })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
createEffect(() => {
|
||||||
|
if (!store.open) return
|
||||||
|
createEventListener(
|
||||||
|
document,
|
||||||
|
"keydown",
|
||||||
|
(event: KeyboardEvent) => handleDocumentSearchKeydown(searchRef, event, store.search, setSearch),
|
||||||
|
true,
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<MenuV2 open={store.open} modal={false} placement="top-start" gutter={6} onOpenChange={setOpen}>
|
<MenuV2 open={store.open} modal={false} placement="top-start" gutter={6} onOpenChange={setOpen}>
|
||||||
<MenuV2.Trigger as={props.triggerAs ?? "div"} {...props.triggerProps}>
|
<MenuV2.Trigger as={props.triggerAs ?? "div"} {...props.triggerProps}>
|
||||||
@@ -434,26 +448,41 @@ export function ModelSelectorPopoverV2(props: {
|
|||||||
<MenuV2.RadioGroup value={current()}>
|
<MenuV2.RadioGroup value={current()}>
|
||||||
<For each={group.items}>
|
<For each={group.items}>
|
||||||
{(item) => (
|
{(item) => (
|
||||||
<MenuV2.RadioItem
|
<TooltipV2
|
||||||
value={modelKey(item)}
|
class="w-full"
|
||||||
data-option-key={modelKey(item)}
|
placement="right-start"
|
||||||
data-selected-model={current() === modelKey(item) ? true : undefined}
|
gutter={6}
|
||||||
class="scroll-my-6"
|
openDelay={0}
|
||||||
classList={{ "!bg-v2-overlay-simple-overlay-hover": store.active === modelKey(item) }}
|
value={
|
||||||
onMouseEnter={() => {
|
<ModelTooltip
|
||||||
setStore("active", modelKey(item))
|
model={item}
|
||||||
setTimeout(() => searchRef?.focus())
|
latest={item.latest}
|
||||||
}}
|
free={isFree(item.provider.id, item.cost)}
|
||||||
onSelect={() => selectModel(item)}
|
v2
|
||||||
|
/>
|
||||||
|
}
|
||||||
>
|
>
|
||||||
<span class="min-w-0 truncate">{item.name}</span>
|
<MenuV2.RadioItem
|
||||||
<Show when={isFree(item.provider.id, item.cost)}>
|
value={modelKey(item)}
|
||||||
<TagV2 class="shrink-0">{language.t("model.tag.free")}</TagV2>
|
data-option-key={modelKey(item)}
|
||||||
</Show>
|
data-selected-model={current() === modelKey(item) ? true : undefined}
|
||||||
<Show when={item.latest}>
|
class="scroll-my-6 w-full"
|
||||||
<TagV2 class="shrink-0">{language.t("model.tag.latest")}</TagV2>
|
classList={{ "!bg-v2-overlay-simple-overlay-hover": store.active === modelKey(item) }}
|
||||||
</Show>
|
onMouseEnter={() => {
|
||||||
</MenuV2.RadioItem>
|
setStore("active", modelKey(item))
|
||||||
|
setTimeout(() => searchRef?.focus())
|
||||||
|
}}
|
||||||
|
onSelect={() => selectModel(item)}
|
||||||
|
>
|
||||||
|
<span class="min-w-0 truncate">{item.name}</span>
|
||||||
|
<Show when={isFree(item.provider.id, item.cost)}>
|
||||||
|
<TagV2 class="shrink-0">{language.t("model.tag.free")}</TagV2>
|
||||||
|
</Show>
|
||||||
|
<Show when={item.latest}>
|
||||||
|
<TagV2 class="shrink-0">{language.t("model.tag.latest")}</TagV2>
|
||||||
|
</Show>
|
||||||
|
</MenuV2.RadioItem>
|
||||||
|
</TooltipV2>
|
||||||
)}
|
)}
|
||||||
</For>
|
</For>
|
||||||
</MenuV2.RadioGroup>
|
</MenuV2.RadioGroup>
|
||||||
@@ -491,8 +520,8 @@ export const DialogSelectModel: Component<{ provider?: string; model?: ModelStat
|
|||||||
const directory = () => decode64(local.slug())
|
const directory = () => decode64(local.slug())
|
||||||
|
|
||||||
const provider = () => {
|
const provider = () => {
|
||||||
void import("./dialog-select-provider").then((x) => {
|
void import("./dialog-connect-provider").then((x) => {
|
||||||
dialog.show(() => <x.DialogSelectProvider directory={directory} />)
|
void dialog.show(() => <x.DialogConnectProvider directory={directory} />)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,87 +0,0 @@
|
|||||||
import { type Accessor, Component, Show } from "solid-js"
|
|
||||||
import { useDialog } from "@opencode-ai/ui/context/dialog"
|
|
||||||
import { popularProviders, useProviders } from "@/hooks/use-providers"
|
|
||||||
import { Dialog } from "@opencode-ai/ui/dialog"
|
|
||||||
import { List } from "@opencode-ai/ui/list"
|
|
||||||
import { Tag } from "@opencode-ai/ui/tag"
|
|
||||||
import { ProviderIcon } from "@opencode-ai/ui/provider-icon"
|
|
||||||
import { DialogConnectProvider } from "./dialog-connect-provider"
|
|
||||||
import { useLanguage } from "@/context/language"
|
|
||||||
import { DialogCustomProvider } from "./dialog-custom-provider"
|
|
||||||
|
|
||||||
const CUSTOM_ID = "_custom"
|
|
||||||
|
|
||||||
export const DialogSelectProvider: Component<{ directory?: Accessor<string | undefined> }> = (props) => {
|
|
||||||
const dialog = useDialog()
|
|
||||||
const providers = useProviders(props.directory)
|
|
||||||
const language = useLanguage()
|
|
||||||
|
|
||||||
const popularGroup = () => language.t("dialog.provider.group.popular")
|
|
||||||
const otherGroup = () => language.t("dialog.provider.group.other")
|
|
||||||
const customLabel = () => language.t("settings.providers.tag.custom")
|
|
||||||
const note = (id: string) => {
|
|
||||||
if (id === "anthropic") return language.t("dialog.provider.anthropic.note")
|
|
||||||
if (id === "openai") return language.t("dialog.provider.openai.note")
|
|
||||||
if (id.startsWith("github-copilot")) return language.t("dialog.provider.copilot.note")
|
|
||||||
if (id === "opencode-go") return language.t("dialog.provider.opencodeGo.tagline")
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Dialog title={language.t("command.provider.connect")} transition>
|
|
||||||
<List
|
|
||||||
class="px-3"
|
|
||||||
search={{ placeholder: language.t("dialog.provider.search.placeholder"), autofocus: true }}
|
|
||||||
emptyMessage={language.t("dialog.provider.empty")}
|
|
||||||
activeIcon="plus-small"
|
|
||||||
key={(x) => x?.id}
|
|
||||||
items={() => {
|
|
||||||
language.locale()
|
|
||||||
return [{ id: CUSTOM_ID, name: customLabel() }, ...providers.all().values()]
|
|
||||||
}}
|
|
||||||
filterKeys={["id", "name"]}
|
|
||||||
groupBy={(x) => (popularProviders.includes(x.id) ? popularGroup() : otherGroup())}
|
|
||||||
sortBy={(a, b) => {
|
|
||||||
if (a.id === CUSTOM_ID) return -1
|
|
||||||
if (b.id === CUSTOM_ID) return 1
|
|
||||||
if (popularProviders.includes(a.id) && popularProviders.includes(b.id))
|
|
||||||
return popularProviders.indexOf(a.id) - popularProviders.indexOf(b.id)
|
|
||||||
return a.name.localeCompare(b.name)
|
|
||||||
}}
|
|
||||||
sortGroupsBy={(a, b) => {
|
|
||||||
const popular = popularGroup()
|
|
||||||
if (a.category === popular && b.category !== popular) return -1
|
|
||||||
if (b.category === popular && a.category !== popular) return 1
|
|
||||||
return 0
|
|
||||||
}}
|
|
||||||
onSelect={(x) => {
|
|
||||||
if (!x) return
|
|
||||||
if (x.id === CUSTOM_ID) {
|
|
||||||
dialog.show(() => <DialogCustomProvider back="providers" directory={props.directory} />)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
dialog.show(() => <DialogConnectProvider provider={x.id} directory={props.directory} />)
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{(i) => (
|
|
||||||
<div class="px-1.25 w-full flex items-center gap-x-3">
|
|
||||||
<ProviderIcon data-slot="list-item-extra-icon" id={i.id} />
|
|
||||||
<span>{i.name}</span>
|
|
||||||
<Show when={i.id === "opencode"}>
|
|
||||||
<div class="text-14-regular text-text-weak">{language.t("dialog.provider.opencode.tagline")}</div>
|
|
||||||
</Show>
|
|
||||||
<Show when={i.id === CUSTOM_ID}>
|
|
||||||
<Tag>{language.t("settings.providers.tag.custom")}</Tag>
|
|
||||||
</Show>
|
|
||||||
<Show when={i.id === "opencode"}>
|
|
||||||
<Tag>{language.t("dialog.provider.tag.recommended")}</Tag>
|
|
||||||
</Show>
|
|
||||||
<Show when={note(i.id)}>{(value) => <div class="text-14-regular text-text-weak">{value()}</div>}</Show>
|
|
||||||
<Show when={i.id === "opencode-go"}>
|
|
||||||
<Tag>{language.t("dialog.provider.tag.recommended")}</Tag>
|
|
||||||
</Show>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</List>
|
|
||||||
</Dialog>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -1,22 +1,35 @@
|
|||||||
import { Component } from "solid-js"
|
import { Component, createSignal, startTransition } from "solid-js"
|
||||||
import { Dialog } from "@opencode-ai/ui/dialog"
|
import { Dialog } from "@opencode-ai/ui/dialog"
|
||||||
import { Tabs } from "@opencode-ai/ui/tabs"
|
import { Tabs } from "@opencode-ai/ui/tabs"
|
||||||
import { Icon } from "@opencode-ai/ui/icon"
|
import { Icon } from "@opencode-ai/ui/icon"
|
||||||
import { useLanguage } from "@/context/language"
|
import { useLanguage } from "@/context/language"
|
||||||
import { usePlatform } from "@/context/platform"
|
import { usePlatform } from "@/context/platform"
|
||||||
|
import { useDialog } from "@opencode-ai/ui/context/dialog"
|
||||||
import { SettingsGeneral } from "./settings-general"
|
import { SettingsGeneral } from "./settings-general"
|
||||||
import { SettingsKeybinds } from "./settings-keybinds"
|
import { SettingsKeybinds } from "./settings-keybinds"
|
||||||
import { SettingsProviders } from "./settings-providers"
|
import { SettingsProviders } from "./settings-providers"
|
||||||
import { SettingsModels } from "./settings-models"
|
import { SettingsModels } from "./settings-models"
|
||||||
import { SettingsServers } from "./settings-servers"
|
import { SettingsServers } from "./settings-servers"
|
||||||
|
|
||||||
export const DialogSettings: Component = () => {
|
export const DialogSettings: Component<{ defaultValue?: string }> = (props) => {
|
||||||
const language = useLanguage()
|
const language = useLanguage()
|
||||||
const platform = usePlatform()
|
const platform = usePlatform()
|
||||||
|
const dialog = useDialog()
|
||||||
|
const [tab, setTab] = createSignal(props.defaultValue ?? "general")
|
||||||
|
|
||||||
|
const showProviders = () => {
|
||||||
|
void dialog.show(() => <DialogSettings defaultValue="providers" />)
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog size="x-large" transition>
|
<Dialog size="x-large" transition>
|
||||||
<Tabs orientation="vertical" variant="settings" defaultValue="general" class="h-full settings-dialog">
|
<Tabs
|
||||||
|
orientation="vertical"
|
||||||
|
variant="settings"
|
||||||
|
value={tab()}
|
||||||
|
onChange={(value) => void startTransition(() => setTab(value))}
|
||||||
|
class="h-full settings-dialog"
|
||||||
|
>
|
||||||
<Tabs.List>
|
<Tabs.List>
|
||||||
<div class="flex flex-col justify-between h-full w-full gap-4">
|
<div class="flex flex-col justify-between h-full w-full gap-4">
|
||||||
<div class="flex flex-col gap-3 w-full pt-3">
|
<div class="flex flex-col gap-3 w-full pt-3">
|
||||||
@@ -70,7 +83,7 @@ export const DialogSettings: Component = () => {
|
|||||||
<SettingsServers />
|
<SettingsServers />
|
||||||
</Tabs.Content>
|
</Tabs.Content>
|
||||||
<Tabs.Content value="providers" class="no-scrollbar">
|
<Tabs.Content value="providers" class="no-scrollbar">
|
||||||
<SettingsProviders />
|
<SettingsProviders onBack={showProviders} />
|
||||||
</Tabs.Content>
|
</Tabs.Content>
|
||||||
<Tabs.Content value="models" class="no-scrollbar">
|
<Tabs.Content value="models" class="no-scrollbar">
|
||||||
<SettingsModels />
|
<SettingsModels />
|
||||||
|
|||||||
@@ -0,0 +1,46 @@
|
|||||||
|
import { describe, expect, test } from "bun:test"
|
||||||
|
import { buildFileTreeV2Model, flattenFileTreeV2 } from "./file-tree-v2-model"
|
||||||
|
|
||||||
|
describe("file tree v2 model", () => {
|
||||||
|
test("builds sorted depth-first rows", () => {
|
||||||
|
const model = buildFileTreeV2Model(["src/z.ts", "src/lib/b.ts", "src/lib/a.ts", "README.md", "docs/guide.md"])
|
||||||
|
|
||||||
|
expect(model.total).toBe(8)
|
||||||
|
expect(flattenFileTreeV2(model, () => true).map((row) => [row.node.path, row.node.type, row.level])).toEqual([
|
||||||
|
["docs", "directory", 0],
|
||||||
|
["docs/guide.md", "file", 1],
|
||||||
|
["src", "directory", 0],
|
||||||
|
["src/lib", "directory", 1],
|
||||||
|
["src/lib/a.ts", "file", 2],
|
||||||
|
["src/lib/b.ts", "file", 2],
|
||||||
|
["src/z.ts", "file", 1],
|
||||||
|
["README.md", "file", 0],
|
||||||
|
])
|
||||||
|
})
|
||||||
|
|
||||||
|
test("omits descendants of collapsed directories", () => {
|
||||||
|
const model = buildFileTreeV2Model(["src/lib/a.ts", "src/z.ts"])
|
||||||
|
|
||||||
|
expect(flattenFileTreeV2(model, (path) => path !== "src/lib").map((row) => row.node.path)).toEqual([
|
||||||
|
"src",
|
||||||
|
"src/lib",
|
||||||
|
"src/z.ts",
|
||||||
|
])
|
||||||
|
})
|
||||||
|
|
||||||
|
test("normalizes separators and duplicate paths", () => {
|
||||||
|
const model = buildFileTreeV2Model(["src\\lib\\a.ts", "src/lib/a.ts", "/src//lib/b.ts/"])
|
||||||
|
const rows = flattenFileTreeV2(model, () => true)
|
||||||
|
|
||||||
|
expect(model.total).toBe(4)
|
||||||
|
expect(rows.map((row) => row.node.path)).toEqual(["src", "src/lib", "src/lib/a.ts", "src/lib/b.ts"])
|
||||||
|
expect(rows.find((row) => row.node.path === "src/lib/a.ts")?.node.originalPath).toBe("src\\lib\\a.ts")
|
||||||
|
})
|
||||||
|
|
||||||
|
test("supports paths deeper than the legacy recursion limit", () => {
|
||||||
|
const file = `${Array.from({ length: 130 }, (_, index) => `dir-${index}`).join("/")}/file.ts`
|
||||||
|
const model = buildFileTreeV2Model([file])
|
||||||
|
|
||||||
|
expect(flattenFileTreeV2(model, () => true)).toHaveLength(131)
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -0,0 +1,77 @@
|
|||||||
|
import type { FileNode } from "@opencode-ai/sdk/v2"
|
||||||
|
|
||||||
|
export type FileTreeV2Model = {
|
||||||
|
children: ReadonlyMap<string, readonly FileTreeV2Node[]>
|
||||||
|
total: number
|
||||||
|
}
|
||||||
|
|
||||||
|
export type FileTreeV2Node = FileNode & { originalPath: string }
|
||||||
|
|
||||||
|
export type FileTreeV2Row = {
|
||||||
|
node: FileTreeV2Node
|
||||||
|
level: number
|
||||||
|
}
|
||||||
|
|
||||||
|
export function normalizeFileTreeV2Path(value: string) {
|
||||||
|
return value
|
||||||
|
.replaceAll("\\", "/")
|
||||||
|
.replace(/^\/+|\/+$/g, "")
|
||||||
|
.replace(/\/{2,}/g, "/")
|
||||||
|
}
|
||||||
|
|
||||||
|
export function buildFileTreeV2Model(paths: readonly string[]): FileTreeV2Model {
|
||||||
|
const nodes = new Map<string, FileTreeV2Node>()
|
||||||
|
|
||||||
|
paths.forEach((value) => {
|
||||||
|
const file = normalizeFileTreeV2Path(value)
|
||||||
|
if (!file) return
|
||||||
|
|
||||||
|
const parts = file.split("/")
|
||||||
|
parts.forEach((name, index) => {
|
||||||
|
const path = parts.slice(0, index + 1).join("/")
|
||||||
|
if (nodes.has(path)) return
|
||||||
|
nodes.set(path, {
|
||||||
|
name,
|
||||||
|
path,
|
||||||
|
absolute: path,
|
||||||
|
type: index === parts.length - 1 ? "file" : "directory",
|
||||||
|
ignored: false,
|
||||||
|
originalPath: index === parts.length - 1 ? value : path,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
const children = new Map<string, FileTreeV2Node[]>()
|
||||||
|
nodes.forEach((node) => {
|
||||||
|
const index = node.path.lastIndexOf("/")
|
||||||
|
const parent = index === -1 ? "" : node.path.slice(0, index)
|
||||||
|
const list = children.get(parent)
|
||||||
|
if (list) list.push(node)
|
||||||
|
else children.set(parent, [node])
|
||||||
|
})
|
||||||
|
children.forEach((nodes) =>
|
||||||
|
nodes.sort((a, b) => {
|
||||||
|
if (a.type !== b.type) return a.type === "directory" ? -1 : 1
|
||||||
|
return a.name.localeCompare(b.name)
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
|
return { children, total: nodes.size }
|
||||||
|
}
|
||||||
|
|
||||||
|
export function flattenFileTreeV2(model: FileTreeV2Model, expanded: (path: string) => boolean) {
|
||||||
|
const rows: FileTreeV2Row[] = []
|
||||||
|
const stack = (model.children.get("") ?? []).toReversed().map((node) => ({ node, level: 0 }))
|
||||||
|
|
||||||
|
while (stack.length > 0) {
|
||||||
|
const row = stack.pop()!
|
||||||
|
rows.push(row)
|
||||||
|
if (row.node.type !== "directory" || !expanded(row.node.path)) continue
|
||||||
|
const children = model.children.get(row.node.path) ?? []
|
||||||
|
for (let index = children.length - 1; index >= 0; index--) {
|
||||||
|
stack.push({ node: children[index]!, level: row.level + 1 })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return rows
|
||||||
|
}
|
||||||
@@ -1,95 +1,26 @@
|
|||||||
import { useFile } from "@/context/file"
|
import { useFile } from "@/context/file"
|
||||||
import { Collapsible } from "@opencode-ai/ui/collapsible"
|
|
||||||
import { FileIcon } from "@opencode-ai/ui/file-icon"
|
import { FileIcon } from "@opencode-ai/ui/file-icon"
|
||||||
import "@opencode-ai/ui/v2/file-tree-v2.css"
|
import "@opencode-ai/ui/v2/file-tree-v2.css"
|
||||||
import {
|
import {
|
||||||
createEffect,
|
createEffect,
|
||||||
createMemo,
|
createMemo,
|
||||||
|
createSignal,
|
||||||
For,
|
For,
|
||||||
Match,
|
|
||||||
on,
|
|
||||||
Show,
|
Show,
|
||||||
splitProps,
|
splitProps,
|
||||||
Switch,
|
|
||||||
untrack,
|
|
||||||
type ComponentProps,
|
type ComponentProps,
|
||||||
type ParentProps,
|
type ParentProps,
|
||||||
} from "solid-js"
|
} from "solid-js"
|
||||||
import { Dynamic } from "solid-js/web"
|
import { Dynamic } from "solid-js/web"
|
||||||
import type { FileNode } from "@opencode-ai/sdk/v2"
|
import type { FileNode } from "@opencode-ai/sdk/v2"
|
||||||
import { Icon } from "@opencode-ai/ui/v2/icon"
|
import { Icon } from "@opencode-ai/ui/v2/icon"
|
||||||
import {
|
import { pathToFileUrl, withFileDragImage, type Kind } from "@/components/file-tree"
|
||||||
dirsToExpand,
|
import { createVirtualizer, defaultRangeExtractor } from "@tanstack/solid-virtual"
|
||||||
pathToFileUrl,
|
import { buildFileTreeV2Model, flattenFileTreeV2, normalizeFileTreeV2Path } from "@/components/file-tree-v2-model"
|
||||||
shouldListRoot,
|
import { virtualScrollElement } from "@/components/virtual-scroll-element"
|
||||||
visibleKind,
|
|
||||||
withFileDragImage,
|
|
||||||
type Filter,
|
|
||||||
type Kind,
|
|
||||||
} from "@/components/file-tree"
|
|
||||||
|
|
||||||
export type { Kind } from "@/components/file-tree"
|
export type { Kind } from "@/components/file-tree"
|
||||||
|
|
||||||
const MAX_DEPTH = 128
|
|
||||||
|
|
||||||
function visibleNodesForPath(path: string, children: (dir: string) => FileNode[], current: Filter | undefined) {
|
|
||||||
const nodes = children(path)
|
|
||||||
if (!current) return nodes
|
|
||||||
|
|
||||||
const parent = (item: string) => {
|
|
||||||
const idx = item.lastIndexOf("/")
|
|
||||||
if (idx === -1) return ""
|
|
||||||
return item.slice(0, idx)
|
|
||||||
}
|
|
||||||
|
|
||||||
const leaf = (item: string) => {
|
|
||||||
const idx = item.lastIndexOf("/")
|
|
||||||
return idx === -1 ? item : item.slice(idx + 1)
|
|
||||||
}
|
|
||||||
|
|
||||||
const out = nodes.filter((node) => {
|
|
||||||
if (node.type === "file") return current.files.has(node.path)
|
|
||||||
return current.dirs.has(node.path)
|
|
||||||
})
|
|
||||||
|
|
||||||
const seen = new Set(out.map((node) => node.path))
|
|
||||||
|
|
||||||
for (const dir of current.dirs) {
|
|
||||||
if (parent(dir) !== path) continue
|
|
||||||
if (seen.has(dir)) continue
|
|
||||||
out.push({
|
|
||||||
name: leaf(dir),
|
|
||||||
path: dir,
|
|
||||||
absolute: dir,
|
|
||||||
type: "directory",
|
|
||||||
ignored: false,
|
|
||||||
})
|
|
||||||
seen.add(dir)
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const item of current.files) {
|
|
||||||
if (parent(item) !== path) continue
|
|
||||||
if (seen.has(item)) continue
|
|
||||||
out.push({
|
|
||||||
name: leaf(item),
|
|
||||||
path: item,
|
|
||||||
absolute: item,
|
|
||||||
type: "file",
|
|
||||||
ignored: false,
|
|
||||||
})
|
|
||||||
seen.add(item)
|
|
||||||
}
|
|
||||||
|
|
||||||
out.sort((a, b) => {
|
|
||||||
if (a.type !== b.type) {
|
|
||||||
return a.type === "directory" ? -1 : 1
|
|
||||||
}
|
|
||||||
return a.name.localeCompare(b.name)
|
|
||||||
})
|
|
||||||
|
|
||||||
return out
|
|
||||||
}
|
|
||||||
|
|
||||||
const INDENT_STEP = 16
|
const INDENT_STEP = 16
|
||||||
|
|
||||||
function rowPaddingLeft(level: number, type: FileNode["type"]) {
|
function rowPaddingLeft(level: number, type: FileNode["type"]) {
|
||||||
@@ -123,7 +54,6 @@ const FileTreeNodeV2 = (
|
|||||||
active?: string
|
active?: string
|
||||||
draggable: boolean
|
draggable: boolean
|
||||||
kinds?: ReadonlyMap<string, Kind>
|
kinds?: ReadonlyMap<string, Kind>
|
||||||
marks?: Set<string>
|
|
||||||
as?: "div" | "button"
|
as?: "div" | "button"
|
||||||
},
|
},
|
||||||
) => {
|
) => {
|
||||||
@@ -133,18 +63,18 @@ const FileTreeNodeV2 = (
|
|||||||
"active",
|
"active",
|
||||||
"draggable",
|
"draggable",
|
||||||
"kinds",
|
"kinds",
|
||||||
"marks",
|
|
||||||
"as",
|
"as",
|
||||||
"children",
|
"children",
|
||||||
"class",
|
"class",
|
||||||
"classList",
|
"classList",
|
||||||
])
|
])
|
||||||
const kind = () => visibleKind(local.node, local.kinds, local.marks)
|
const kind = () => local.kinds?.get(local.node.path)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Dynamic
|
<Dynamic
|
||||||
component={local.as ?? "div"}
|
component={local.as ?? "div"}
|
||||||
data-slot="file-tree-v2-row"
|
data-slot="file-tree-v2-row"
|
||||||
|
data-path={local.node.path}
|
||||||
data-selected={local.node.path === local.active ? "" : undefined}
|
data-selected={local.node.path === local.active ? "" : undefined}
|
||||||
data-ignored={local.node.ignored ? "" : undefined}
|
data-ignored={local.node.ignored ? "" : undefined}
|
||||||
classList={{
|
classList={{
|
||||||
@@ -177,244 +107,161 @@ const FileTreeNodeV2 = (
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
// V2-styled fork of FileTree for the review sidebar. Unlike the v1 tree it never
|
function GuideLines(props: { level: number }) {
|
||||||
// lists unloaded subdirectories, so callers must pass `allowed` (nodes are
|
return (
|
||||||
// synthesized from that list) for nested content to appear.
|
<For each={Array.from({ length: props.level })}>
|
||||||
|
{(_, index) => (
|
||||||
|
<div
|
||||||
|
class="absolute top-0 bottom-0 w-px pointer-events-none bg-border-weak-base opacity-0 group-hover/file-tree-v2:opacity-50"
|
||||||
|
style={`left: ${guideLineLeft(index())}px`}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</For>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
export default function FileTreeV2(props: {
|
export default function FileTreeV2(props: {
|
||||||
path: string
|
|
||||||
active?: string
|
active?: string
|
||||||
level?: number
|
|
||||||
allowed?: readonly string[]
|
allowed?: readonly string[]
|
||||||
kinds?: ReadonlyMap<string, Kind>
|
kinds?: ReadonlyMap<string, Kind>
|
||||||
draggable?: boolean
|
draggable?: boolean
|
||||||
onFileClick?: (file: FileNode) => void
|
onFileClick?: (file: FileNode) => void
|
||||||
|
|
||||||
_filter?: Filter
|
|
||||||
_marks?: Set<string>
|
|
||||||
_deeps?: Map<string, number>
|
|
||||||
_kinds?: ReadonlyMap<string, Kind>
|
|
||||||
_chain?: readonly string[]
|
|
||||||
}) {
|
}) {
|
||||||
const file = useFile()
|
const file = useFile()
|
||||||
const level = props.level ?? 0
|
|
||||||
const draggable = () => props.draggable ?? true
|
const draggable = () => props.draggable ?? true
|
||||||
|
const active = () => normalizeFileTreeV2Path(props.active ?? "")
|
||||||
const key = (p: string) =>
|
const model = createMemo(() => buildFileTreeV2Model(props.allowed ?? []))
|
||||||
file
|
const rows = createMemo(() => flattenFileTreeV2(model(), (path) => file.tree.state(path)?.expanded ?? true))
|
||||||
.normalize(p)
|
const [root, setRoot] = createSignal<HTMLDivElement>()
|
||||||
.replace(/[\\/]+$/, "")
|
const [focused, setFocused] = createSignal<string>()
|
||||||
.replaceAll("\\", "/")
|
const virtualizer = createVirtualizer<HTMLDivElement, HTMLDivElement>({
|
||||||
const chain = props._chain ? [...props._chain, key(props.path)] : [key(props.path)]
|
get count() {
|
||||||
|
return rows().length
|
||||||
const filter = createMemo(() => {
|
},
|
||||||
if (props._filter) return props._filter
|
getScrollElement: () => virtualScrollElement(root()),
|
||||||
|
initialRect: { width: 0, height: 600 },
|
||||||
const allowed = props.allowed
|
estimateSize: () => 28,
|
||||||
if (!allowed) return
|
gap: 2,
|
||||||
|
overscan: 10,
|
||||||
const files = new Set(allowed)
|
get getItemKey() {
|
||||||
const dirs = new Set<string>()
|
const current = rows()
|
||||||
|
return (index: number) => current[index]?.node.path ?? index
|
||||||
for (const item of allowed) {
|
},
|
||||||
const parts = item.split("/")
|
rangeExtractor: (range) => {
|
||||||
const parents = parts.slice(0, -1)
|
const indexes = defaultRangeExtractor(range)
|
||||||
for (const [idx] of parents.entries()) {
|
const path = focused()
|
||||||
const dir = parents.slice(0, idx + 1).join("/")
|
const index = path ? rows().findIndex((row) => row.node.path === path) : -1
|
||||||
if (dir) dirs.add(dir)
|
if (index < 0 || indexes.includes(index)) return indexes
|
||||||
}
|
return [...indexes, index].sort((a, b) => a - b)
|
||||||
}
|
},
|
||||||
|
|
||||||
return { files, dirs }
|
|
||||||
})
|
})
|
||||||
|
|
||||||
const marks = createMemo(() => {
|
|
||||||
if (props._marks) return props._marks
|
|
||||||
|
|
||||||
const out = new Set<string>(props.kinds?.keys() ?? [])
|
|
||||||
if (out.size === 0) return
|
|
||||||
return out
|
|
||||||
})
|
|
||||||
|
|
||||||
const kinds = createMemo(() => {
|
|
||||||
if (props._kinds) return props._kinds
|
|
||||||
return props.kinds
|
|
||||||
})
|
|
||||||
|
|
||||||
const deeps = createMemo(() => {
|
|
||||||
if (props._deeps) return props._deeps
|
|
||||||
|
|
||||||
const out = new Map<string, number>()
|
|
||||||
|
|
||||||
const root = props.path
|
|
||||||
if (!(file.tree.state(root)?.expanded ?? false)) return out
|
|
||||||
|
|
||||||
const seen = new Set<string>()
|
|
||||||
const stack: { dir: string; lvl: number; i: number; kids: string[]; max: number }[] = []
|
|
||||||
|
|
||||||
const push = (dir: string, lvl: number) => {
|
|
||||||
const id = key(dir)
|
|
||||||
if (seen.has(id)) return
|
|
||||||
seen.add(id)
|
|
||||||
|
|
||||||
const kids = file.tree
|
|
||||||
.children(dir)
|
|
||||||
.filter((node) => node.type === "directory" && (file.tree.state(node.path)?.expanded ?? false))
|
|
||||||
.map((node) => node.path)
|
|
||||||
|
|
||||||
stack.push({ dir, lvl, i: 0, kids, max: lvl })
|
|
||||||
}
|
|
||||||
|
|
||||||
push(root, level - 1)
|
|
||||||
|
|
||||||
while (stack.length > 0) {
|
|
||||||
const top = stack[stack.length - 1]!
|
|
||||||
|
|
||||||
if (top.i < top.kids.length) {
|
|
||||||
const next = top.kids[top.i]!
|
|
||||||
top.i++
|
|
||||||
push(next, top.lvl + 1)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
out.set(top.dir, top.max)
|
|
||||||
stack.pop()
|
|
||||||
|
|
||||||
const parent = stack[stack.length - 1]
|
|
||||||
if (!parent) continue
|
|
||||||
parent.max = Math.max(parent.max, top.max)
|
|
||||||
}
|
|
||||||
|
|
||||||
return out
|
|
||||||
})
|
|
||||||
|
|
||||||
createEffect(() => {
|
createEffect(() => {
|
||||||
const current = filter()
|
const path = active()
|
||||||
const dirs = dirsToExpand({
|
if (!path) return
|
||||||
level,
|
const index = rows().findIndex((row) => row.node.path === path)
|
||||||
filter: current,
|
if (index < 0) return
|
||||||
expanded: (dir) => untrack(() => file.tree.state(dir)?.expanded) ?? false,
|
queueMicrotask(() => {
|
||||||
|
if (virtualizer.range && index >= virtualizer.range.startIndex && index <= virtualizer.range.endIndex) return
|
||||||
|
virtualizer.scrollToIndex(index, { align: "auto" })
|
||||||
})
|
})
|
||||||
// Nodes come from the `allowed` filter; skip listing so directories that only
|
|
||||||
// exist on the diff's base branch do not each fail with an error toast.
|
|
||||||
for (const dir of dirs) file.tree.expand(dir, { list: false })
|
|
||||||
})
|
})
|
||||||
|
const rowByKey = createMemo(() => new Map(rows().map((row) => [row.node.path, row] as const)))
|
||||||
createEffect(
|
const virtualItemByKey = createMemo(
|
||||||
on(
|
() => new Map(virtualizer.getVirtualItems().map((item) => [item.key, item] as const)),
|
||||||
() => props.path,
|
|
||||||
(path) => {
|
|
||||||
const dir = untrack(() => file.tree.state(path))
|
|
||||||
if (!shouldListRoot({ level, dir })) return
|
|
||||||
void file.tree.list(path)
|
|
||||||
},
|
|
||||||
{ defer: false },
|
|
||||||
),
|
|
||||||
)
|
)
|
||||||
|
const virtualRowKeys = createMemo(() => virtualizer.getVirtualItems().map((item) => item.key))
|
||||||
const nodes = createMemo(() => visibleNodesForPath(props.path, file.tree.children, filter()))
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
// group/file-tree-v2 scopes the group-hover guide lines below; hosts may add
|
<div
|
||||||
// an outer group with the same name to widen the hover area.
|
ref={setRoot}
|
||||||
<div data-component="file-tree-v2" class="group/file-tree-v2">
|
data-component="file-tree-v2"
|
||||||
<For each={nodes()}>
|
data-total-rows={model().total}
|
||||||
{(node) => {
|
class="group/file-tree-v2"
|
||||||
const expanded = () => file.tree.state(node.path)?.expanded ?? false
|
style={{ position: "relative", height: `${virtualizer.getTotalSize()}px` }}
|
||||||
const deep = () => deeps().get(node.path) ?? -1
|
>
|
||||||
const hasChildren = () => visibleNodesForPath(node.path, file.tree.children, filter()).length > 0
|
<For each={virtualRowKeys()}>
|
||||||
return (
|
{(key) => (
|
||||||
<Switch>
|
<Show when={virtualItemByKey().get(key)}>
|
||||||
<Match when={node.type === "directory"}>
|
{(item) => (
|
||||||
<Collapsible
|
<div
|
||||||
variant="ghost"
|
style={{
|
||||||
class="w-full"
|
position: "absolute",
|
||||||
data-scope="file-tree-v2"
|
top: "0",
|
||||||
forceMount={false}
|
left: "0",
|
||||||
open={expanded()}
|
width: "100%",
|
||||||
onOpenChange={(open) =>
|
height: `${item().size}px`,
|
||||||
open ? file.tree.expand(node.path, { list: false }) : file.tree.collapse(node.path)
|
transform: `translateY(${item().start}px)`,
|
||||||
}
|
}}
|
||||||
>
|
>
|
||||||
<Collapsible.Trigger>
|
<Show when={rowByKey().get(key as string)}>
|
||||||
<FileTreeNodeV2
|
{(row) => (
|
||||||
node={node}
|
<Show
|
||||||
level={level}
|
when={row().node.type === "directory"}
|
||||||
active={props.active}
|
fallback={
|
||||||
draggable={draggable()}
|
<FileTreeNodeV2
|
||||||
kinds={kinds()}
|
node={row().node}
|
||||||
marks={marks()}
|
level={row().level}
|
||||||
>
|
active={active()}
|
||||||
<div
|
draggable={draggable()}
|
||||||
data-slot="file-tree-v2-chevron"
|
|
||||||
data-expanded={expanded() ? "" : undefined}
|
|
||||||
class="size-4 flex items-center justify-center"
|
|
||||||
>
|
|
||||||
<Icon name="chevron-down" />
|
|
||||||
</div>
|
|
||||||
</FileTreeNodeV2>
|
|
||||||
</Collapsible.Trigger>
|
|
||||||
<Show when={hasChildren()}>
|
|
||||||
<Collapsible.Content class="relative">
|
|
||||||
<div
|
|
||||||
classList={{
|
|
||||||
"absolute top-0 bottom-0 w-px pointer-events-none bg-border-weak-base opacity-0 transition-opacity duration-150 ease-out motion-reduce:transition-none": true,
|
|
||||||
"group-hover/file-tree-v2:opacity-100": expanded() && deep() === level,
|
|
||||||
"group-hover/file-tree-v2:opacity-50": !(expanded() && deep() === level),
|
|
||||||
}}
|
|
||||||
style={`left: ${guideLineLeft(level)}px`}
|
|
||||||
/>
|
|
||||||
<Show
|
|
||||||
when={level < MAX_DEPTH && !chain.includes(key(node.path))}
|
|
||||||
fallback={<div class="px-2 py-1 text-12-regular text-text-weak">...</div>}
|
|
||||||
>
|
|
||||||
<FileTreeV2
|
|
||||||
path={node.path}
|
|
||||||
level={level + 1}
|
|
||||||
allowed={props.allowed}
|
|
||||||
kinds={props.kinds}
|
kinds={props.kinds}
|
||||||
active={props.active}
|
as="button"
|
||||||
draggable={props.draggable}
|
type="button"
|
||||||
onFileClick={props.onFileClick}
|
class="relative"
|
||||||
_filter={filter()}
|
onFocus={() => setFocused(row().node.path)}
|
||||||
_marks={marks()}
|
onBlur={() => setFocused(undefined)}
|
||||||
_deeps={deeps()}
|
onClick={() =>
|
||||||
_kinds={kinds()}
|
props.onFileClick?.({
|
||||||
_chain={chain}
|
...row().node,
|
||||||
/>
|
path: row().node.originalPath,
|
||||||
</Show>
|
absolute: row().node.originalPath,
|
||||||
</Collapsible.Content>
|
})
|
||||||
</Show>
|
}
|
||||||
</Collapsible>
|
>
|
||||||
</Match>
|
<GuideLines level={row().level} />
|
||||||
<Match when={node.type === "file"}>
|
<Show when={row().level > 0}>
|
||||||
<FileTreeNodeV2
|
<div class="w-4 shrink-0" />
|
||||||
node={node}
|
</Show>
|
||||||
level={level}
|
<span class="filetree-iconpair size-4">
|
||||||
active={props.active}
|
<FileIcon node={row().node} class="size-4 filetree-icon filetree-icon--color" />
|
||||||
draggable={draggable()}
|
<FileIcon node={row().node} class="size-4 filetree-icon filetree-icon--mono" mono />
|
||||||
kinds={kinds()}
|
</span>
|
||||||
marks={marks()}
|
</FileTreeNodeV2>
|
||||||
as="button"
|
}
|
||||||
type="button"
|
>
|
||||||
onClick={() => props.onFileClick?.(node)}
|
<FileTreeNodeV2
|
||||||
>
|
node={row().node}
|
||||||
<Show when={level > 0}>
|
level={row().level}
|
||||||
<div class="w-4 shrink-0" />
|
active={active()}
|
||||||
</Show>
|
draggable={draggable()}
|
||||||
<Show
|
kinds={props.kinds}
|
||||||
when={!node.ignored}
|
as="button"
|
||||||
fallback={<FileIcon node={node} class="size-4 filetree-icon filetree-icon--mono" mono />}
|
type="button"
|
||||||
>
|
class="relative"
|
||||||
<span class="filetree-iconpair size-4">
|
onFocus={() => setFocused(row().node.path)}
|
||||||
<FileIcon node={node} class="size-4 filetree-icon filetree-icon--color" />
|
onBlur={() => setFocused(undefined)}
|
||||||
<FileIcon node={node} class="size-4 filetree-icon filetree-icon--mono" mono />
|
aria-expanded={file.tree.state(row().node.path)?.expanded ?? true}
|
||||||
</span>
|
onClick={() =>
|
||||||
</Show>
|
file.tree.state(row().node.path)?.expanded === false
|
||||||
</FileTreeNodeV2>
|
? file.tree.expand(row().node.path, { list: false })
|
||||||
</Match>
|
: file.tree.collapse(row().node.path)
|
||||||
</Switch>
|
}
|
||||||
)
|
>
|
||||||
}}
|
<GuideLines level={row().level} />
|
||||||
|
<div
|
||||||
|
data-slot="file-tree-v2-chevron"
|
||||||
|
data-expanded={file.tree.state(row().node.path)?.expanded === false ? undefined : ""}
|
||||||
|
class="size-4 flex items-center justify-center"
|
||||||
|
>
|
||||||
|
<Icon name="chevron-down" />
|
||||||
|
</div>
|
||||||
|
</FileTreeNodeV2>
|
||||||
|
</Show>
|
||||||
|
)}
|
||||||
|
</Show>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</Show>
|
||||||
|
)}
|
||||||
</For>
|
</For>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -201,6 +201,7 @@ export default function FileTree(props: {
|
|||||||
kinds?: ReadonlyMap<string, Kind>
|
kinds?: ReadonlyMap<string, Kind>
|
||||||
draggable?: boolean
|
draggable?: boolean
|
||||||
onFileClick?: (file: FileNode) => void
|
onFileClick?: (file: FileNode) => void
|
||||||
|
onFileDoubleClick?: (file: FileNode) => void
|
||||||
|
|
||||||
_filter?: Filter
|
_filter?: Filter
|
||||||
_marks?: Set<string>
|
_marks?: Set<string>
|
||||||
@@ -440,6 +441,7 @@ export default function FileTree(props: {
|
|||||||
active={props.active}
|
active={props.active}
|
||||||
draggable={props.draggable}
|
draggable={props.draggable}
|
||||||
onFileClick={props.onFileClick}
|
onFileClick={props.onFileClick}
|
||||||
|
onFileDoubleClick={props.onFileDoubleClick}
|
||||||
_filter={filter()}
|
_filter={filter()}
|
||||||
_marks={marks()}
|
_marks={marks()}
|
||||||
_deeps={deeps()}
|
_deeps={deeps()}
|
||||||
@@ -462,6 +464,7 @@ export default function FileTree(props: {
|
|||||||
as="button"
|
as="button"
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => props.onFileClick?.(node)}
|
onClick={() => props.onFileClick?.(node)}
|
||||||
|
onDblClick={() => props.onFileDoubleClick?.(node)}
|
||||||
>
|
>
|
||||||
<div class="w-4 shrink-0" />
|
<div class="w-4 shrink-0" />
|
||||||
<Switch>
|
<Switch>
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { Show, type Component } from "solid-js"
|
import { Show, type Component, type JSX } from "solid-js"
|
||||||
import { useLanguage } from "@/context/language"
|
import { useLanguage } from "@/context/language"
|
||||||
|
|
||||||
type InputKey = "text" | "image" | "audio" | "video" | "pdf"
|
type InputKey = "text" | "image" | "audio" | "video" | "pdf"
|
||||||
@@ -23,7 +23,18 @@ type ModelInfo = {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const ModelTooltip: Component<{ model: ModelInfo; latest?: boolean; free?: boolean }> = (props) => {
|
function ModelTooltipRow(props: { name: JSX.Element; value: JSX.Element }) {
|
||||||
|
return (
|
||||||
|
<div class="flex min-w-0 items-center gap-4">
|
||||||
|
<span class="shrink-0 text-v2-text-text-muted">{props.name}</span>
|
||||||
|
<span class="ml-auto min-w-0 truncate text-right text-v2-text-text-base">{props.value}</span>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ModelTooltip: Component<{ model: ModelInfo; latest?: boolean; free?: boolean; v2?: boolean }> = (
|
||||||
|
props,
|
||||||
|
) => {
|
||||||
const language = useLanguage()
|
const language = useLanguage()
|
||||||
const sourceName = (model: ModelInfo) => {
|
const sourceName = (model: ModelInfo) => {
|
||||||
const value = `${model.id} ${model.name}`.toLowerCase()
|
const value = `${model.id} ${model.name}`.toLowerCase()
|
||||||
@@ -51,6 +62,13 @@ export const ModelTooltip: Component<{ model: ModelInfo; latest?: boolean; free?
|
|||||||
const suffix = tags.length ? ` (${tags.join(", ")})` : ""
|
const suffix = tags.length ? ` (${tags.join(", ")})` : ""
|
||||||
return `${sourceName(props.model)} ${props.model.name}${suffix}`
|
return `${sourceName(props.model)} ${props.model.name}${suffix}`
|
||||||
}
|
}
|
||||||
|
const name = () => {
|
||||||
|
const tags: Array<string> = []
|
||||||
|
if (props.latest) tags.push(language.t("model.tag.latest"))
|
||||||
|
if (props.free) tags.push(language.t("model.tag.free"))
|
||||||
|
const suffix = tags.length ? ` (${tags.join(", ")})` : ""
|
||||||
|
return `${props.model.name}${suffix}`
|
||||||
|
}
|
||||||
const inputs = () => {
|
const inputs = () => {
|
||||||
if (props.model.capabilities) {
|
if (props.model.capabilities) {
|
||||||
const input = props.model.capabilities.input
|
const input = props.model.capabilities.input
|
||||||
@@ -73,6 +91,21 @@ export const ModelTooltip: Component<{ model: ModelInfo; latest?: boolean; free?
|
|||||||
: language.t("model.tooltip.reasoning.none")
|
: language.t("model.tooltip.reasoning.none")
|
||||||
}
|
}
|
||||||
const context = () => language.t("model.tooltip.context", { limit: props.model.limit.context.toLocaleString() })
|
const context = () => language.t("model.tooltip.context", { limit: props.model.limit.context.toLocaleString() })
|
||||||
|
const contextLimit = () => props.model.limit.context.toLocaleString(language.intl())
|
||||||
|
|
||||||
|
if (props.v2) {
|
||||||
|
return (
|
||||||
|
<div class="flex w-[180px] flex-col gap-2">
|
||||||
|
<ModelTooltipRow name={language.t("model.tooltip.model")} value={name()} />
|
||||||
|
<ModelTooltipRow name={language.t("model.tooltip.provider")} value={props.model.provider.name} />
|
||||||
|
<Show when={inputs()}>
|
||||||
|
{(value) => <ModelTooltipRow name={language.t("model.tooltip.inputs")} value={value()} />}
|
||||||
|
</Show>
|
||||||
|
<ModelTooltipRow name={language.t("model.tooltip.reasoning")} value={reasoning()} />
|
||||||
|
<ModelTooltipRow name={language.t("model.tooltip.context.label")} value={contextLimit()} />
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div class="flex flex-col gap-1 py-1">
|
<div class="flex flex-col gap-1 py-1">
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { Component, For, Show } from "solid-js"
|
import { Component, For, Show } from "solid-js"
|
||||||
import { FileIcon } from "@opencode-ai/ui/file-icon"
|
import { FileIcon } from "@opencode-ai/ui/file-icon"
|
||||||
import { IconButton } from "@opencode-ai/ui/icon-button"
|
import { IconButton } from "@opencode-ai/ui/icon-button"
|
||||||
import { Tooltip } from "@opencode-ai/ui/tooltip"
|
import { TooltipV2 } from "@opencode-ai/ui/v2/tooltip-v2"
|
||||||
import { getDirectory, getFilename, getFilenameTruncated } from "@opencode-ai/core/util/path"
|
import { getDirectory, getFilename, getFilenameTruncated } from "@opencode-ai/core/util/path"
|
||||||
import type { ContextItem } from "@/context/prompt"
|
import type { ContextItem } from "@/context/prompt"
|
||||||
|
|
||||||
@@ -27,7 +27,7 @@ export const PromptContextItems: Component<ContextItemsProps> = (props) => {
|
|||||||
const selected = props.active(item)
|
const selected = props.active(item)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Tooltip
|
<TooltipV2
|
||||||
value={
|
value={
|
||||||
<span class="flex max-w-[300px]">
|
<span class="flex max-w-[300px]">
|
||||||
<span class="text-text-invert-base truncate-start [unicode-bidi:plaintext] min-w-0">
|
<span class="text-text-invert-base truncate-start [unicode-bidi:plaintext] min-w-0">
|
||||||
@@ -78,7 +78,7 @@ export const PromptContextItems: Component<ContextItemsProps> = (props) => {
|
|||||||
{(comment) => <div class="text-12-regular text-text-strong ml-5 pr-1 truncate">{comment()}</div>}
|
{(comment) => <div class="text-12-regular text-text-strong ml-5 pr-1 truncate">{comment()}</div>}
|
||||||
</Show>
|
</Show>
|
||||||
</div>
|
</div>
|
||||||
</Tooltip>
|
</TooltipV2>
|
||||||
)
|
)
|
||||||
}}
|
}}
|
||||||
</For>
|
</For>
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { For, Show, splitProps, type Accessor, type ComponentProps } from "solid-js"
|
import { createEffect, For, onCleanup, Show, splitProps, type Accessor, type ComponentProps } from "solid-js"
|
||||||
import { createStore } from "solid-js/store"
|
import { createStore } from "solid-js/store"
|
||||||
import { DropdownMenu } from "@opencode-ai/ui/dropdown-menu"
|
import { DropdownMenu } from "@opencode-ai/ui/dropdown-menu"
|
||||||
import { Icon } from "@opencode-ai/ui/icon"
|
import { Icon } from "@opencode-ai/ui/icon"
|
||||||
@@ -8,6 +8,7 @@ import { getProjectAvatarVariant } from "@/context/layout"
|
|||||||
import { useLanguage } from "@/context/language"
|
import { useLanguage } from "@/context/language"
|
||||||
import { displayName, getProjectAvatarSource } from "@/pages/layout/helpers"
|
import { displayName, getProjectAvatarSource } from "@/pages/layout/helpers"
|
||||||
import { pathKey } from "@/utils/path-key"
|
import { pathKey } from "@/utils/path-key"
|
||||||
|
import { handleDocumentSearchKeydown } from "@/utils/search-keydown"
|
||||||
|
|
||||||
export type PromptProject = {
|
export type PromptProject = {
|
||||||
name?: string
|
name?: string
|
||||||
@@ -101,6 +102,16 @@ export function createPromptProjectController(input: {
|
|||||||
setStore({ open: false, search: "", active: "" })
|
setStore({ open: false, search: "", active: "" })
|
||||||
input.controls().add(language.t("command.project.open"), server)
|
input.controls().add(language.t("command.project.open"), server)
|
||||||
}
|
}
|
||||||
|
const setSearch = (value: string) => {
|
||||||
|
const search = value.trim().toLowerCase()
|
||||||
|
const first = input
|
||||||
|
.controls()
|
||||||
|
.available.find((project) => !search || displayName(project).toLowerCase().includes(search))
|
||||||
|
setStore({
|
||||||
|
search: value,
|
||||||
|
active: first ? projectKey(first) : actionKey(servers().length > 1 ? undefined : servers()[0]?.key),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
selected,
|
selected,
|
||||||
@@ -127,16 +138,7 @@ export function createPromptProjectController(input: {
|
|||||||
}
|
}
|
||||||
setStore({ open: false, search: "", active: "" })
|
setStore({ open: false, search: "", active: "" })
|
||||||
},
|
},
|
||||||
setSearch(value: string) {
|
setSearch,
|
||||||
const search = value.trim().toLowerCase()
|
|
||||||
const first = input
|
|
||||||
.controls()
|
|
||||||
.available.find((project) => !search || displayName(project).toLowerCase().includes(search))
|
|
||||||
setStore({
|
|
||||||
search: value,
|
|
||||||
active: first ? projectKey(first) : actionKey(servers().length > 1 ? undefined : servers()[0]?.key),
|
|
||||||
})
|
|
||||||
},
|
|
||||||
clearSearch() {
|
clearSearch() {
|
||||||
setStore({ search: "", active: initialActive() })
|
setStore({ search: "", active: initialActive() })
|
||||||
setTimeout(() => searchRef?.focus())
|
setTimeout(() => searchRef?.focus())
|
||||||
@@ -170,6 +172,9 @@ export function createPromptProjectController(input: {
|
|||||||
focusSearch() {
|
focusSearch() {
|
||||||
setTimeout(() => requestAnimationFrame(() => searchRef?.focus()))
|
setTimeout(() => requestAnimationFrame(() => searchRef?.focus()))
|
||||||
},
|
},
|
||||||
|
handleSearchKeydown(event: KeyboardEvent) {
|
||||||
|
return handleDocumentSearchKeydown(searchRef, event, store.search, setSearch)
|
||||||
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -243,6 +248,13 @@ export function PromptProjectSelector(props: {
|
|||||||
return project ? props.controller.projectKey(project) : undefined
|
return project ? props.controller.projectKey(project) : undefined
|
||||||
}
|
}
|
||||||
|
|
||||||
|
createEffect(() => {
|
||||||
|
if (!props.controller.open()) return
|
||||||
|
const handler = (event: KeyboardEvent) => props.controller.handleSearchKeydown(event)
|
||||||
|
document.addEventListener("keydown", handler, true)
|
||||||
|
onCleanup(() => document.removeEventListener("keydown", handler, true))
|
||||||
|
})
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<DropdownMenu
|
<DropdownMenu
|
||||||
open={props.controller.open()}
|
open={props.controller.open()}
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import { ProgressCircleV2 } from "@opencode-ai/ui/v2/progress-circle-v2"
|
|||||||
import { Button } from "@opencode-ai/ui/button"
|
import { Button } from "@opencode-ai/ui/button"
|
||||||
import { IconButtonV2 } from "@opencode-ai/ui/v2/icon-button-v2"
|
import { IconButtonV2 } from "@opencode-ai/ui/v2/icon-button-v2"
|
||||||
import { TooltipV2 } from "@opencode-ai/ui/v2/tooltip-v2"
|
import { TooltipV2 } from "@opencode-ai/ui/v2/tooltip-v2"
|
||||||
|
import { createMediaQuery } from "@solid-primitives/media"
|
||||||
|
|
||||||
import { useFile } from "@/context/file"
|
import { useFile } from "@/context/file"
|
||||||
import { useLayout } from "@/context/layout"
|
import { useLayout } from "@/context/layout"
|
||||||
@@ -14,6 +15,7 @@ import { useSDK } from "@/context/sdk"
|
|||||||
import { getSessionContext, getSessionTokenTotal } from "@/components/session/session-context-metrics"
|
import { getSessionContext, getSessionTokenTotal } 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"
|
||||||
|
import { useSettings } from "@/context/settings"
|
||||||
|
|
||||||
interface SessionContextUsageProps {
|
interface SessionContextUsageProps {
|
||||||
variant?: "button" | "indicator"
|
variant?: "button" | "indicator"
|
||||||
@@ -47,8 +49,10 @@ export function SessionContextUsage(props: SessionContextUsageProps) {
|
|||||||
const layout = useLayout()
|
const layout = useLayout()
|
||||||
const language = useLanguage()
|
const language = useLanguage()
|
||||||
const sdk = useSDK()
|
const sdk = useSDK()
|
||||||
|
const settings = useSettings()
|
||||||
const providers = useProviders(() => sdk().directory)
|
const providers = useProviders(() => sdk().directory)
|
||||||
const { params, tabs, view } = useSessionLayout()
|
const { params, tabs, view } = useSessionLayout()
|
||||||
|
const isDesktop = createMediaQuery("(min-width: 768px)")
|
||||||
|
|
||||||
const variant = createMemo(() => props.variant ?? "button")
|
const variant = createMemo(() => props.variant ?? "button")
|
||||||
const buttonAppearance = createMemo(() => props.buttonAppearance ?? "default")
|
const buttonAppearance = createMemo(() => props.buttonAppearance ?? "default")
|
||||||
@@ -56,6 +60,7 @@ export function SessionContextUsage(props: SessionContextUsageProps) {
|
|||||||
tabs,
|
tabs,
|
||||||
pathFromTab: file.pathFromTab,
|
pathFromTab: file.pathFromTab,
|
||||||
normalizeTab: (tab) => (tab.startsWith("file://") ? file.tab(tab) : tab),
|
normalizeTab: (tab) => (tab.startsWith("file://") ? file.tab(tab) : tab),
|
||||||
|
fileBrowser: () => settings.general.newLayoutDesigns() && isDesktop() && !!params.id,
|
||||||
})
|
})
|
||||||
const messages = createMemo(() => (params.id ? (sync().data.message[params.id] ?? []) : []))
|
const messages = createMemo(() => (params.id ? (sync().data.message[params.id] ?? []) : []))
|
||||||
const info = createMemo(() => (params.id ? sync().session.get(params.id) : undefined))
|
const info = createMemo(() => (params.id ? sync().session.get(params.id) : undefined))
|
||||||
|
|||||||
@@ -31,6 +31,7 @@ import { Icon as IconV2 } from "@opencode-ai/ui/v2/icon"
|
|||||||
import { KeybindV2 } from "@opencode-ai/ui/v2/keybind-v2"
|
import { KeybindV2 } from "@opencode-ai/ui/v2/keybind-v2"
|
||||||
import { TooltipV2 } from "@opencode-ai/ui/v2/tooltip-v2"
|
import { TooltipV2 } from "@opencode-ai/ui/v2/tooltip-v2"
|
||||||
import { reviewTooltipKeybind } from "../command-tooltip-keybind"
|
import { reviewTooltipKeybind } from "../command-tooltip-keybind"
|
||||||
|
import { useTitlebarRightMount } from "../titlebar"
|
||||||
|
|
||||||
const OPEN_APPS = [
|
const OPEN_APPS = [
|
||||||
"vscode",
|
"vscode",
|
||||||
@@ -284,10 +285,9 @@ export function SessionHeader() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const [centerMount, setCenterMount] = createSignal<HTMLElement | null>(null)
|
const [centerMount, setCenterMount] = createSignal<HTMLElement | null>(null)
|
||||||
const [rightMount, setRightMount] = createSignal<HTMLElement | null>(null)
|
const rightMount = useTitlebarRightMount()
|
||||||
onMount(() => {
|
onMount(() => {
|
||||||
setCenterMount(document.getElementById("opencode-titlebar-center"))
|
setCenterMount(document.getElementById("opencode-titlebar-center"))
|
||||||
setRightMount(document.getElementById("opencode-titlebar-right"))
|
|
||||||
})
|
})
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ import { useFile } from "@/context/file"
|
|||||||
import { useLanguage } from "@/context/language"
|
import { useLanguage } from "@/context/language"
|
||||||
import { useCommand } from "@/context/command"
|
import { useCommand } from "@/context/command"
|
||||||
|
|
||||||
export function FileVisual(props: { path: string; active?: boolean }): JSX.Element {
|
export function FileVisual(props: { path: string; active?: boolean; temporary?: boolean }): JSX.Element {
|
||||||
return (
|
return (
|
||||||
<div class="flex items-center gap-x-1.5 min-w-0">
|
<div class="flex items-center gap-x-1.5 min-w-0">
|
||||||
<Show
|
<Show
|
||||||
@@ -22,12 +22,19 @@ export function FileVisual(props: { path: string; active?: boolean }): JSX.Eleme
|
|||||||
<FileIcon node={{ path: props.path, type: "file" }} mono class="absolute inset-0 size-4 tab-fileicon-mono" />
|
<FileIcon node={{ path: props.path, type: "file" }} mono class="absolute inset-0 size-4 tab-fileicon-mono" />
|
||||||
</span>
|
</span>
|
||||||
</Show>
|
</Show>
|
||||||
<span class="text-14-medium truncate">{getFilename(props.path)}</span>
|
<span class="text-14-medium truncate" classList={{ italic: props.temporary }}>
|
||||||
|
{getFilename(props.path)}
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export function SortableTab(props: { tab: string; onTabClose: (tab: string) => void }): JSX.Element {
|
export function SortableTab(props: {
|
||||||
|
tab: string
|
||||||
|
temporary?: boolean
|
||||||
|
onTabClose: (tab: string) => void
|
||||||
|
onTabDoubleClick?: (tab: string) => void
|
||||||
|
}): JSX.Element {
|
||||||
const file = useFile()
|
const file = useFile()
|
||||||
const language = useLanguage()
|
const language = useLanguage()
|
||||||
const command = useCommand()
|
const command = useCommand()
|
||||||
@@ -36,7 +43,7 @@ export function SortableTab(props: { tab: string; onTabClose: (tab: string) => v
|
|||||||
const content = createMemo(() => {
|
const content = createMemo(() => {
|
||||||
const value = path()
|
const value = path()
|
||||||
if (!value) return
|
if (!value) return
|
||||||
return <FileVisual path={value} />
|
return <FileVisual path={value} temporary={props.temporary} />
|
||||||
})
|
})
|
||||||
return (
|
return (
|
||||||
<div use:sortable class="h-full flex items-center" classList={{ "opacity-0": sortable.isActiveDraggable }}>
|
<div use:sortable class="h-full flex items-center" classList={{ "opacity-0": sortable.isActiveDraggable }}>
|
||||||
@@ -61,6 +68,7 @@ export function SortableTab(props: { tab: string; onTabClose: (tab: string) => v
|
|||||||
}
|
}
|
||||||
hideCloseButton
|
hideCloseButton
|
||||||
onMiddleClick={() => props.onTabClose(props.tab)}
|
onMiddleClick={() => props.onTabClose(props.tab)}
|
||||||
|
onDblClick={() => props.onTabDoubleClick?.(props.tab)}
|
||||||
>
|
>
|
||||||
<Show when={content()}>{(value) => value()}</Show>
|
<Show when={content()}>{(value) => value()}</Show>
|
||||||
</Tabs.Trigger>
|
</Tabs.Trigger>
|
||||||
|
|||||||
@@ -0,0 +1,271 @@
|
|||||||
|
import type { JSX } from "solid-js"
|
||||||
|
import { Show, createEffect, onCleanup } from "solid-js"
|
||||||
|
import { createStore } from "solid-js/store"
|
||||||
|
import { useSortable } from "@dnd-kit/solid/sortable"
|
||||||
|
import { IconButton } from "@opencode-ai/ui/icon-button"
|
||||||
|
import { Tabs } from "@opencode-ai/ui/tabs"
|
||||||
|
import { DropdownMenu } from "@opencode-ai/ui/dropdown-menu"
|
||||||
|
import { Icon } from "@opencode-ai/ui/icon"
|
||||||
|
import { MenuV2 } from "@opencode-ai/ui/v2/menu-v2"
|
||||||
|
import { isDefaultTitle as isDefaultTerminalTitle } from "@/context/terminal-title"
|
||||||
|
import { useTerminal, type LocalPTY } from "@/context/terminal"
|
||||||
|
import { useLanguage } from "@/context/language"
|
||||||
|
import { focusTerminalById } from "@/pages/session/helpers"
|
||||||
|
|
||||||
|
export function SortableTerminalTabV2(props: {
|
||||||
|
terminal: LocalPTY
|
||||||
|
index: () => number
|
||||||
|
newLayout: boolean
|
||||||
|
onClose?: () => void
|
||||||
|
}): JSX.Element {
|
||||||
|
const terminal = useTerminal()
|
||||||
|
const language = useLanguage()
|
||||||
|
const sortable = useSortable({
|
||||||
|
get id() {
|
||||||
|
return props.terminal.id
|
||||||
|
},
|
||||||
|
get index() {
|
||||||
|
return props.index()
|
||||||
|
},
|
||||||
|
})
|
||||||
|
const [store, setStore] = createStore({
|
||||||
|
editing: false,
|
||||||
|
title: props.terminal.title,
|
||||||
|
menuOpen: false,
|
||||||
|
menuPosition: { x: 0, y: 0 },
|
||||||
|
blurEnabled: false,
|
||||||
|
})
|
||||||
|
let input: HTMLInputElement | undefined
|
||||||
|
let blurFrame: number | undefined
|
||||||
|
let editRequested = false
|
||||||
|
|
||||||
|
const isDefaultTitle = () => {
|
||||||
|
const number = props.terminal.titleNumber
|
||||||
|
if (!Number.isFinite(number) || number <= 0) return false
|
||||||
|
return isDefaultTerminalTitle(props.terminal.title, number)
|
||||||
|
}
|
||||||
|
|
||||||
|
const label = () => {
|
||||||
|
language.locale()
|
||||||
|
if (props.terminal.title && !isDefaultTitle()) return props.terminal.title
|
||||||
|
|
||||||
|
const number = props.terminal.titleNumber
|
||||||
|
if (Number.isFinite(number) && number > 0) return language.t("terminal.title.numbered", { number })
|
||||||
|
if (props.terminal.title) return props.terminal.title
|
||||||
|
return language.t("terminal.title")
|
||||||
|
}
|
||||||
|
|
||||||
|
const close = () => {
|
||||||
|
const count = terminal.all().length
|
||||||
|
void terminal.close(props.terminal.id)
|
||||||
|
if (count === 1) {
|
||||||
|
props.onClose?.()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const focus = () => {
|
||||||
|
if (store.editing) return
|
||||||
|
terminal.open(props.terminal.id)
|
||||||
|
if (document.activeElement instanceof HTMLElement) document.activeElement.blur()
|
||||||
|
focusTerminalById(props.terminal.id)
|
||||||
|
}
|
||||||
|
|
||||||
|
const edit = (e?: Event) => {
|
||||||
|
if (e) {
|
||||||
|
e.stopPropagation()
|
||||||
|
e.preventDefault()
|
||||||
|
}
|
||||||
|
|
||||||
|
setStore("blurEnabled", false)
|
||||||
|
setStore("title", props.terminal.title)
|
||||||
|
setStore("editing", true)
|
||||||
|
}
|
||||||
|
|
||||||
|
const save = () => {
|
||||||
|
if (!store.blurEnabled) return
|
||||||
|
|
||||||
|
const value = store.title.trim()
|
||||||
|
if (value && value !== props.terminal.title) {
|
||||||
|
terminal.update({ id: props.terminal.id, title: value })
|
||||||
|
}
|
||||||
|
setStore("editing", false)
|
||||||
|
}
|
||||||
|
|
||||||
|
const keydown = (e: KeyboardEvent) => {
|
||||||
|
if (e.key === "Enter") {
|
||||||
|
e.preventDefault()
|
||||||
|
save()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (e.key === "Escape") {
|
||||||
|
e.preventDefault()
|
||||||
|
setStore("editing", false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const menu = (e: MouseEvent) => {
|
||||||
|
e.preventDefault()
|
||||||
|
setStore("menuPosition", { x: e.clientX, y: e.clientY })
|
||||||
|
setStore("menuOpen", true)
|
||||||
|
}
|
||||||
|
|
||||||
|
createEffect(() => {
|
||||||
|
if (!store.editing) return
|
||||||
|
if (!input) return
|
||||||
|
input.focus()
|
||||||
|
input.select()
|
||||||
|
if (blurFrame !== undefined) cancelAnimationFrame(blurFrame)
|
||||||
|
blurFrame = requestAnimationFrame(() => {
|
||||||
|
blurFrame = undefined
|
||||||
|
setStore("blurEnabled", true)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
onCleanup(() => {
|
||||||
|
if (blurFrame === undefined) return
|
||||||
|
cancelAnimationFrame(blurFrame)
|
||||||
|
})
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
ref={sortable.ref}
|
||||||
|
class="outline-none focus:outline-none focus-visible:outline-none"
|
||||||
|
classList={{
|
||||||
|
"h-full flex items-center": props.newLayout,
|
||||||
|
"h-full": !props.newLayout,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Show
|
||||||
|
when={props.newLayout}
|
||||||
|
fallback={
|
||||||
|
<div class="relative h-full">
|
||||||
|
<Tabs.Trigger
|
||||||
|
value={props.terminal.id}
|
||||||
|
onClick={focus}
|
||||||
|
onMouseDown={(e) => e.preventDefault()}
|
||||||
|
onContextMenu={menu}
|
||||||
|
class="!shadow-none"
|
||||||
|
classes={{
|
||||||
|
button: "border-0 outline-none focus:outline-none focus-visible:outline-none !shadow-none !ring-0",
|
||||||
|
}}
|
||||||
|
closeButton={
|
||||||
|
<IconButton
|
||||||
|
icon="close"
|
||||||
|
variant="ghost"
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation()
|
||||||
|
close()
|
||||||
|
}}
|
||||||
|
aria-label={language.t("terminal.close")}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<span onDblClick={edit} classList={{ invisible: store.editing }}>
|
||||||
|
{label()}
|
||||||
|
</span>
|
||||||
|
</Tabs.Trigger>
|
||||||
|
<Show when={store.editing}>
|
||||||
|
<div class="absolute inset-0 flex items-center px-3 bg-muted z-10 pointer-events-auto">
|
||||||
|
<input
|
||||||
|
ref={input}
|
||||||
|
type="text"
|
||||||
|
value={store.title}
|
||||||
|
onInput={(e) => setStore("title", e.currentTarget.value)}
|
||||||
|
onBlur={save}
|
||||||
|
onKeyDown={keydown}
|
||||||
|
onMouseDown={(e) => e.stopPropagation()}
|
||||||
|
class="bg-transparent border-none outline-none text-sm min-w-0 flex-1"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</Show>
|
||||||
|
<DropdownMenu open={store.menuOpen} onOpenChange={(open) => setStore("menuOpen", open)}>
|
||||||
|
<DropdownMenu.Portal>
|
||||||
|
<DropdownMenu.Content
|
||||||
|
class="fixed"
|
||||||
|
style={{
|
||||||
|
left: `${store.menuPosition.x}px`,
|
||||||
|
top: `${store.menuPosition.y}px`,
|
||||||
|
}}
|
||||||
|
onCloseAutoFocus={(e) => {
|
||||||
|
if (!editRequested) return
|
||||||
|
e.preventDefault()
|
||||||
|
editRequested = false
|
||||||
|
requestAnimationFrame(() => edit())
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<DropdownMenu.Item onSelect={() => (editRequested = true)}>
|
||||||
|
<Icon name="edit" class="w-4 h-4 mr-2" />
|
||||||
|
{language.t("common.rename")}
|
||||||
|
</DropdownMenu.Item>
|
||||||
|
<DropdownMenu.Item onSelect={close}>
|
||||||
|
<Icon name="close" class="w-4 h-4 mr-2" />
|
||||||
|
{language.t("common.close")}
|
||||||
|
</DropdownMenu.Item>
|
||||||
|
</DropdownMenu.Content>
|
||||||
|
</DropdownMenu.Portal>
|
||||||
|
</DropdownMenu>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<MenuV2.Context>
|
||||||
|
<MenuV2.Context.Trigger class="relative" as="div">
|
||||||
|
<Tabs.Trigger
|
||||||
|
value={props.terminal.id}
|
||||||
|
onClick={focus}
|
||||||
|
closeButton={
|
||||||
|
<IconButton
|
||||||
|
icon="close-small"
|
||||||
|
variant="ghost"
|
||||||
|
class="h-5 w-5"
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation()
|
||||||
|
close()
|
||||||
|
}}
|
||||||
|
aria-label={language.t("terminal.close")}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
hideCloseButton
|
||||||
|
onMiddleClick={close}
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
class="truncate"
|
||||||
|
data-slot="terminal-tab-title"
|
||||||
|
onDblClick={edit}
|
||||||
|
classList={{ invisible: store.editing }}
|
||||||
|
>
|
||||||
|
{label()}
|
||||||
|
</span>
|
||||||
|
</Tabs.Trigger>
|
||||||
|
<Show when={store.editing}>
|
||||||
|
<div class="absolute inset-0 flex items-center bg-v2-background-bg-layer-01 z-10 pointer-events-auto rounded-[6px] shadow-[inset_0_0_0_0.5px_var(--v2-border-border-muted)] px-2">
|
||||||
|
<input
|
||||||
|
ref={input}
|
||||||
|
type="text"
|
||||||
|
value={store.title}
|
||||||
|
onInput={(e) => setStore("title", e.currentTarget.value)}
|
||||||
|
onBlur={save}
|
||||||
|
onKeyDown={keydown}
|
||||||
|
onMouseDown={(e) => e.stopPropagation()}
|
||||||
|
class="bg-transparent border-none outline-none min-w-0 flex-1 p-0 text-[13px] leading-4 tracking-[-0.04px] text-v2-text-text-base [font-weight:440] [font-variation-settings:'slnt'_0] [font-variant-numeric:tabular-nums]"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</Show>
|
||||||
|
</MenuV2.Context.Trigger>
|
||||||
|
<MenuV2.Context.Portal>
|
||||||
|
<MenuV2.Context.Content
|
||||||
|
onCloseAutoFocus={(e) => {
|
||||||
|
if (!editRequested) return
|
||||||
|
e.preventDefault()
|
||||||
|
editRequested = false
|
||||||
|
requestAnimationFrame(() => edit())
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<MenuV2.Item onSelect={() => (editRequested = true)}>{language.t("common.rename")}</MenuV2.Item>
|
||||||
|
<MenuV2.Item onSelect={close}>{language.t("common.close")}</MenuV2.Item>
|
||||||
|
</MenuV2.Context.Content>
|
||||||
|
</MenuV2.Context.Portal>
|
||||||
|
</MenuV2.Context>
|
||||||
|
</Show>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -440,11 +440,6 @@ export const SettingsGeneral: Component = () => {
|
|||||||
value={(o) => o.value}
|
value={(o) => o.value}
|
||||||
label={(o) => o.label}
|
label={(o) => o.label}
|
||||||
onSelect={(option) => option && theme.setColorScheme(option.value)}
|
onSelect={(option) => option && theme.setColorScheme(option.value)}
|
||||||
onHighlight={(option) => {
|
|
||||||
if (!option) return
|
|
||||||
theme.previewColorScheme(option.value)
|
|
||||||
return () => theme.cancelPreview()
|
|
||||||
}}
|
|
||||||
variant="secondary"
|
variant="secondary"
|
||||||
size="small"
|
size="small"
|
||||||
triggerVariant="settings"
|
triggerVariant="settings"
|
||||||
@@ -471,11 +466,6 @@ export const SettingsGeneral: Component = () => {
|
|||||||
if (!option) return
|
if (!option) return
|
||||||
theme.setTheme(option.id)
|
theme.setTheme(option.id)
|
||||||
}}
|
}}
|
||||||
onHighlight={(option) => {
|
|
||||||
if (!option) return
|
|
||||||
theme.previewTheme(option.id)
|
|
||||||
return () => theme.cancelPreview()
|
|
||||||
}}
|
|
||||||
variant="secondary"
|
variant="secondary"
|
||||||
size="small"
|
size="small"
|
||||||
triggerVariant="settings"
|
triggerVariant="settings"
|
||||||
|
|||||||
@@ -5,24 +5,18 @@ import { Button } from "@opencode-ai/ui/button"
|
|||||||
import { Icon } from "@opencode-ai/ui/icon"
|
import { Icon } from "@opencode-ai/ui/icon"
|
||||||
import { IconButton } from "@opencode-ai/ui/icon-button"
|
import { IconButton } from "@opencode-ai/ui/icon-button"
|
||||||
import { TextField } from "@opencode-ai/ui/text-field"
|
import { TextField } from "@opencode-ai/ui/text-field"
|
||||||
|
import { ButtonV2 } from "@opencode-ai/ui/v2/button-v2"
|
||||||
|
import { IconButtonV2 } from "@opencode-ai/ui/v2/icon-button-v2"
|
||||||
|
import { TextInputV2 } from "@opencode-ai/ui/v2/text-input-v2"
|
||||||
import { showToast } from "@/utils/toast"
|
import { showToast } from "@/utils/toast"
|
||||||
import fuzzysort from "fuzzysort"
|
import fuzzysort from "fuzzysort"
|
||||||
import { formatKeybind, parseKeybind, useCommand } from "@/context/command"
|
import { formatKeybind, parseKeybind, useCommand } from "@/context/command"
|
||||||
import { useLanguage } from "@/context/language"
|
import { useLanguage } from "@/context/language"
|
||||||
import { useSettings } from "@/context/settings"
|
import { useSettings } from "@/context/settings"
|
||||||
import { SettingsList } from "./settings-list"
|
import { SettingsList } from "./settings-list"
|
||||||
|
import { SettingsListV2 } from "./settings-v2/parts/list"
|
||||||
|
|
||||||
const ButtonV2 = lazy(() => import("@opencode-ai/ui/v2/button-v2").then((module) => ({ default: module.ButtonV2 })))
|
|
||||||
const IconV2 = lazy(() => import("@opencode-ai/ui/v2/icon").then((module) => ({ default: module.Icon })))
|
const IconV2 = lazy(() => import("@opencode-ai/ui/v2/icon").then((module) => ({ default: module.Icon })))
|
||||||
const IconButtonV2 = lazy(() =>
|
|
||||||
import("@opencode-ai/ui/v2/icon-button-v2").then((module) => ({ default: module.IconButtonV2 })),
|
|
||||||
)
|
|
||||||
const TextInputV2 = lazy(() =>
|
|
||||||
import("@opencode-ai/ui/v2/text-input-v2").then((module) => ({ default: module.TextInputV2 })),
|
|
||||||
)
|
|
||||||
const SettingsListV2 = lazy(() =>
|
|
||||||
import("./settings-v2/parts/list").then((module) => ({ default: module.SettingsListV2 })),
|
|
||||||
)
|
|
||||||
|
|
||||||
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)
|
||||||
const PALETTE_ID = "command.palette"
|
const PALETTE_ID = "command.palette"
|
||||||
|
|||||||
@@ -8,8 +8,7 @@ import { createMemo, type Component, For, Show } from "solid-js"
|
|||||||
import { useLanguage } from "@/context/language"
|
import { useLanguage } from "@/context/language"
|
||||||
import { useServerSDK } from "@/context/server-sdk"
|
import { useServerSDK } from "@/context/server-sdk"
|
||||||
import { useServerSync } from "@/context/server-sync"
|
import { useServerSync } from "@/context/server-sync"
|
||||||
import { DialogConnectProvider } from "./dialog-connect-provider"
|
import { DialogConnectProvider, useProviderConnectController } from "./dialog-connect-provider"
|
||||||
import { DialogSelectProvider } from "./dialog-select-provider"
|
|
||||||
import { DialogCustomProvider } from "./dialog-custom-provider"
|
import { DialogCustomProvider } from "./dialog-custom-provider"
|
||||||
import { SettingsList } from "./settings-list"
|
import { SettingsList } from "./settings-list"
|
||||||
import { SettingsServerPicker, SettingsServerScope } from "./settings-server-picker"
|
import { SettingsServerPicker, SettingsServerScope } from "./settings-server-picker"
|
||||||
@@ -28,20 +27,26 @@ const PROVIDER_NOTES = [
|
|||||||
{ match: (id: string) => id === "vercel", key: "dialog.provider.vercel.note" },
|
{ match: (id: string) => id === "vercel", key: "dialog.provider.vercel.note" },
|
||||||
] as const
|
] as const
|
||||||
|
|
||||||
export const SettingsProviders: Component = () => {
|
export const SettingsProviders: Component<{ onBack?: () => void }> = (props) => {
|
||||||
return (
|
return (
|
||||||
<SettingsServerScope>
|
<SettingsServerScope>
|
||||||
<SettingsProvidersContent />
|
<SettingsProvidersContent onBack={props.onBack} />
|
||||||
</SettingsServerScope>
|
</SettingsServerScope>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
const SettingsProvidersContent: Component = () => {
|
const SettingsProvidersContent: Component<{ onBack?: () => void }> = (props) => {
|
||||||
const dialog = useDialog()
|
const dialog = useDialog()
|
||||||
const language = useLanguage()
|
const language = useLanguage()
|
||||||
const serverSDK = useServerSDK()
|
const serverSDK = useServerSDK()
|
||||||
const serverSync = useServerSync()
|
const serverSync = useServerSync()
|
||||||
const providers = useProviders()
|
const providers = useProviders()
|
||||||
|
const providerConnect = useProviderConnectController({ onBack: props.onBack })
|
||||||
|
|
||||||
|
const connect = (provider?: string) => {
|
||||||
|
providerConnect.select(provider)
|
||||||
|
void dialog.show(() => <DialogConnectProvider controller={providerConnect} />)
|
||||||
|
}
|
||||||
|
|
||||||
const connected = createMemo(() => {
|
const connected = createMemo(() => {
|
||||||
return providers
|
return providers
|
||||||
@@ -206,14 +211,7 @@ const SettingsProvidersContent: Component = () => {
|
|||||||
{(key) => <span class="text-12-regular text-text-weak pl-8">{language.t(key())}</span>}
|
{(key) => <span class="text-12-regular text-text-weak pl-8">{language.t(key())}</span>}
|
||||||
</Show>
|
</Show>
|
||||||
</div>
|
</div>
|
||||||
<Button
|
<Button size="large" variant="secondary" icon="plus-small" onClick={() => connect(item.id)}>
|
||||||
size="large"
|
|
||||||
variant="secondary"
|
|
||||||
icon="plus-small"
|
|
||||||
onClick={() => {
|
|
||||||
dialog.show(() => <DialogConnectProvider provider={item.id} />)
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{language.t("common.connect")}
|
{language.t("common.connect")}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
@@ -239,7 +237,7 @@ const SettingsProvidersContent: Component = () => {
|
|||||||
variant="secondary"
|
variant="secondary"
|
||||||
icon="plus-small"
|
icon="plus-small"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
dialog.show(() => <DialogCustomProvider back="close" />)
|
dialog.show(() => <DialogCustomProvider onBack={dialog.close} />)
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{language.t("common.connect")}
|
{language.t("common.connect")}
|
||||||
@@ -250,9 +248,7 @@ const SettingsProvidersContent: Component = () => {
|
|||||||
<Button
|
<Button
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
class="px-0 py-0 mt-5 text-14-medium text-text-interactive-base text-left justify-start hover:bg-transparent active:bg-transparent"
|
class="px-0 py-0 mt-5 text-14-medium text-text-interactive-base text-left justify-start hover:bg-transparent active:bg-transparent"
|
||||||
onClick={() => {
|
onClick={() => connect()}
|
||||||
dialog.show(() => <DialogSelectProvider />)
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
{language.t("dialog.provider.viewAll")}
|
{language.t("dialog.provider.viewAll")}
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { Component } from "solid-js"
|
import { Component, createSignal, startTransition } from "solid-js"
|
||||||
import { Dialog } from "@opencode-ai/ui/v2/dialog-v2"
|
import { Dialog } from "@opencode-ai/ui/v2/dialog-v2"
|
||||||
import { TabsV2 } from "@opencode-ai/ui/v2/tabs-v2"
|
import { TabsV2 } from "@opencode-ai/ui/v2/tabs-v2"
|
||||||
import { Icon } from "@opencode-ai/ui/icon"
|
import { Icon } from "@opencode-ai/ui/icon"
|
||||||
@@ -10,16 +10,30 @@ import { SettingsProvidersV2 } from "./providers"
|
|||||||
import { SettingsModelsV2 } from "./models"
|
import { SettingsModelsV2 } from "./models"
|
||||||
import "./settings-v2.css"
|
import "./settings-v2.css"
|
||||||
import { SettingsServersV2 } from "./servers"
|
import { SettingsServersV2 } from "./servers"
|
||||||
|
import { useDialog } from "@opencode-ai/ui/context/dialog"
|
||||||
|
|
||||||
export const DialogSettings: Component<{
|
export const DialogSettings: Component<{
|
||||||
sessionID?: string
|
sessionID?: string
|
||||||
|
defaultValue?: string
|
||||||
}> = (props) => {
|
}> = (props) => {
|
||||||
const language = useLanguage()
|
const language = useLanguage()
|
||||||
const platform = usePlatform()
|
const platform = usePlatform()
|
||||||
|
const dialog = useDialog()
|
||||||
|
const [tab, setTab] = createSignal(props.defaultValue ?? "general")
|
||||||
|
|
||||||
|
const showProviders = () => {
|
||||||
|
void dialog.show(() => <DialogSettings sessionID={props.sessionID} defaultValue="providers" />)
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog size="x-large" variant="settings" class="settings-v2-dialog">
|
<Dialog size="x-large" variant="settings" class="settings-v2-dialog">
|
||||||
<TabsV2 orientation="vertical" variant="settings" defaultValue="general" class="settings-v2">
|
<TabsV2
|
||||||
|
orientation="vertical"
|
||||||
|
variant="settings"
|
||||||
|
value={tab()}
|
||||||
|
onChange={(value) => void startTransition(() => setTab(value))}
|
||||||
|
class="settings-v2"
|
||||||
|
>
|
||||||
<TabsV2.List>
|
<TabsV2.List>
|
||||||
<div class="flex flex-col justify-between h-full w-full">
|
<div class="flex flex-col justify-between h-full w-full">
|
||||||
<div class="flex flex-col gap-3 w-full">
|
<div class="flex flex-col gap-3 w-full">
|
||||||
@@ -73,7 +87,7 @@ export const DialogSettings: Component<{
|
|||||||
<SettingsServersV2 />
|
<SettingsServersV2 />
|
||||||
</TabsV2.Content>
|
</TabsV2.Content>
|
||||||
<TabsV2.Content value="providers" class="settings-v2-panel">
|
<TabsV2.Content value="providers" class="settings-v2-panel">
|
||||||
<SettingsProvidersV2 />
|
<SettingsProvidersV2 onBack={showProviders} />
|
||||||
</TabsV2.Content>
|
</TabsV2.Content>
|
||||||
<TabsV2.Content value="models" class="settings-v2-panel">
|
<TabsV2.Content value="models" class="settings-v2-panel">
|
||||||
<SettingsModelsV2 />
|
<SettingsModelsV2 />
|
||||||
|
|||||||
@@ -422,11 +422,6 @@ export const SettingsGeneralV2: Component<{
|
|||||||
value={(o) => o.value}
|
value={(o) => o.value}
|
||||||
label={(o) => o.label}
|
label={(o) => o.label}
|
||||||
onSelect={(option) => option && theme.setColorScheme(option.value)}
|
onSelect={(option) => option && theme.setColorScheme(option.value)}
|
||||||
onHighlight={(option) => {
|
|
||||||
if (!option) return
|
|
||||||
theme.previewColorScheme(option.value)
|
|
||||||
return () => theme.cancelPreview()
|
|
||||||
}}
|
|
||||||
/>
|
/>
|
||||||
</SettingsRowV2>
|
</SettingsRowV2>
|
||||||
|
|
||||||
@@ -454,11 +449,6 @@ export const SettingsGeneralV2: Component<{
|
|||||||
if (!option) return
|
if (!option) return
|
||||||
theme.setTheme(option.id)
|
theme.setTheme(option.id)
|
||||||
}}
|
}}
|
||||||
onHighlight={(option) => {
|
|
||||||
if (!option) return
|
|
||||||
theme.previewTheme(option.id)
|
|
||||||
return () => theme.cancelPreview()
|
|
||||||
}}
|
|
||||||
/>
|
/>
|
||||||
</SettingsRowV2>
|
</SettingsRowV2>
|
||||||
|
|
||||||
|
|||||||
@@ -8,8 +8,7 @@ import { createMemo, type Component, For, Show } from "solid-js"
|
|||||||
import { useLanguage } from "@/context/language"
|
import { useLanguage } from "@/context/language"
|
||||||
import { useServerSDK } from "@/context/server-sdk"
|
import { useServerSDK } from "@/context/server-sdk"
|
||||||
import { useServerSync } from "@/context/server-sync"
|
import { useServerSync } from "@/context/server-sync"
|
||||||
import { DialogConnectProvider } from "../dialog-connect-provider"
|
import { DialogConnectProvider, useProviderConnectController } from "../dialog-connect-provider"
|
||||||
import { DialogSelectProvider } from "../dialog-select-provider"
|
|
||||||
import { DialogCustomProvider } from "../dialog-custom-provider"
|
import { DialogCustomProvider } from "../dialog-custom-provider"
|
||||||
import { SettingsListV2 } from "./parts/list"
|
import { SettingsListV2 } from "./parts/list"
|
||||||
import "./settings-v2.css"
|
import "./settings-v2.css"
|
||||||
@@ -30,12 +29,18 @@ const PROVIDER_NOTES = [
|
|||||||
|
|
||||||
const PROVIDER_ICON_SIZE = 16
|
const PROVIDER_ICON_SIZE = 16
|
||||||
|
|
||||||
export const SettingsProvidersV2: Component = () => {
|
export const SettingsProvidersV2: Component<{ onBack?: () => void }> = (props) => {
|
||||||
const dialog = useDialog()
|
const dialog = useDialog()
|
||||||
const language = useLanguage()
|
const language = useLanguage()
|
||||||
const serverSdk = useServerSDK()
|
const serverSdk = useServerSDK()
|
||||||
const serverSync = useServerSync()
|
const serverSync = useServerSync()
|
||||||
const providers = useProviders()
|
const providers = useProviders()
|
||||||
|
const providerConnect = useProviderConnectController({ onBack: props.onBack })
|
||||||
|
|
||||||
|
const connect = (provider?: string) => {
|
||||||
|
providerConnect.select(provider)
|
||||||
|
void dialog.show(() => <DialogConnectProvider controller={providerConnect} />)
|
||||||
|
}
|
||||||
|
|
||||||
const connected = createMemo(() => {
|
const connected = createMemo(() => {
|
||||||
return providers
|
return providers
|
||||||
@@ -206,14 +211,7 @@ export const SettingsProvidersV2: Component = () => {
|
|||||||
</Show>
|
</Show>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<ButtonV2
|
<ButtonV2 size="normal" variant="neutral" icon="plus" onClick={() => connect(item.id)}>
|
||||||
size="normal"
|
|
||||||
variant="neutral"
|
|
||||||
icon="plus"
|
|
||||||
onClick={() => {
|
|
||||||
dialog.show(() => <DialogConnectProvider provider={item.id} />)
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{language.t("common.connect")}
|
{language.t("common.connect")}
|
||||||
</ButtonV2>
|
</ButtonV2>
|
||||||
</div>
|
</div>
|
||||||
@@ -241,7 +239,7 @@ export const SettingsProvidersV2: Component = () => {
|
|||||||
variant="neutral"
|
variant="neutral"
|
||||||
icon="plus"
|
icon="plus"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
dialog.show(() => <DialogCustomProvider back="close" />)
|
dialog.show(() => <DialogCustomProvider onBack={dialog.close} />)
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{language.t("common.connect")}
|
{language.t("common.connect")}
|
||||||
@@ -249,13 +247,7 @@ export const SettingsProvidersV2: Component = () => {
|
|||||||
</div>
|
</div>
|
||||||
</SettingsListV2>
|
</SettingsListV2>
|
||||||
|
|
||||||
<button
|
<button type="button" class="settings-v2-providers-view-all" onClick={() => connect()}>
|
||||||
type="button"
|
|
||||||
class="settings-v2-providers-view-all"
|
|
||||||
onClick={() => {
|
|
||||||
dialog.show(() => <DialogSelectProvider />)
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{language.t("dialog.provider.viewAll")}
|
{language.t("dialog.provider.viewAll")}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -246,18 +246,23 @@ export function TabNavItem(props: {
|
|||||||
}}
|
}}
|
||||||
class="flex h-full min-w-0 flex-1 flex-row items-center gap-1.5 text-[13px] font-medium text-v2-text-text-faint group-data-[active='true']:text-v2-text-text-base group-data-[editing='true']:text-v2-text-text-base [-webkit-user-drag:none]"
|
class="flex h-full min-w-0 flex-1 flex-row items-center gap-1.5 text-[13px] font-medium text-v2-text-text-faint group-data-[active='true']:text-v2-text-text-base group-data-[editing='true']:text-v2-text-text-base [-webkit-user-drag:none]"
|
||||||
>
|
>
|
||||||
<Show when={props.session()}>
|
<span data-slot="project-avatar-slot" class="flex size-4 shrink-0 items-center justify-center">
|
||||||
{(session) => (
|
<Show
|
||||||
<span data-slot="project-avatar-slot">
|
when={props.session()}
|
||||||
|
fallback={
|
||||||
|
<span class="block size-4 rounded-[3px] border border-v2-border-border-muted" aria-hidden="true" />
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{(session) => (
|
||||||
<SessionTabAvatar
|
<SessionTabAvatar
|
||||||
project={project()}
|
project={project()}
|
||||||
directory={session().directory}
|
directory={session().directory}
|
||||||
sessionId={session().id}
|
sessionId={session().id}
|
||||||
server={props.server}
|
server={props.server}
|
||||||
/>
|
/>
|
||||||
</span>
|
)}
|
||||||
)}
|
</Show>
|
||||||
</Show>
|
</span>
|
||||||
<span
|
<span
|
||||||
ref={(el) => {
|
ref={(el) => {
|
||||||
titleEl = el
|
titleEl = el
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import { createSignal, Show, type JSXElement } from "solid-js"
|
|||||||
import "./titlebar-tab-popover.css"
|
import "./titlebar-tab-popover.css"
|
||||||
|
|
||||||
// Initial hover delay before the preview appears, per design.
|
// Initial hover delay before the preview appears, per design.
|
||||||
const OPEN_DELAY = 400
|
const OPEN_DELAY = 2_000
|
||||||
// Mouse-out delay: begin closing immediately (a brief exit animation plays).
|
// Mouse-out delay: begin closing immediately (a brief exit animation plays).
|
||||||
const CLOSE_DELAY = 0
|
const CLOSE_DELAY = 0
|
||||||
// After a preview closes, hovering a neighbouring tab within this window skips
|
// After a preview closes, hovering a neighbouring tab within this window skips
|
||||||
|
|||||||
@@ -17,8 +17,6 @@ import { createTabPromptState } from "@/context/prompt"
|
|||||||
import { base64Encode } from "@opencode-ai/core/util/encode"
|
import { base64Encode } from "@opencode-ai/core/util/encode"
|
||||||
import { canStartTabDrag, isTabCloseTarget } from "./titlebar-tab-gesture"
|
import { canStartTabDrag, isTabCloseTarget } from "./titlebar-tab-gesture"
|
||||||
|
|
||||||
const sortableTransition = { duration: 0 }
|
|
||||||
|
|
||||||
function SessionTabSlot(props: {
|
function SessionTabSlot(props: {
|
||||||
tab: SessionTab
|
tab: SessionTab
|
||||||
id: string
|
id: string
|
||||||
@@ -42,6 +40,7 @@ function SessionTabSlot(props: {
|
|||||||
let ref!: HTMLDivElement
|
let ref!: HTMLDivElement
|
||||||
const sdk = createMemo(() => props.serverCtx()?.sdk ?? null)
|
const sdk = createMemo(() => props.serverCtx()?.sdk ?? null)
|
||||||
const cachedSession = createMemo(() => props.serverCtx()?.sync.session.peek(props.tab.sessionId))
|
const cachedSession = createMemo(() => props.serverCtx()?.sync.session.peek(props.tab.sessionId))
|
||||||
|
const persisted = createMemo(() => tabs.info[props.id])
|
||||||
const [loadedSession] = createResource(
|
const [loadedSession] = createResource(
|
||||||
() => {
|
() => {
|
||||||
const ctx = props.serverCtx()
|
const ctx = props.serverCtx()
|
||||||
@@ -73,8 +72,10 @@ function SessionTabSlot(props: {
|
|||||||
|
|
||||||
createEffect(() => {
|
createEffect(() => {
|
||||||
const value = session()
|
const value = session()
|
||||||
|
if (!value) return
|
||||||
|
tabs.rememberSessionInfo(props.tab, value)
|
||||||
const current = sdk()
|
const current = sdk()
|
||||||
if (!value || !current) return
|
if (!current) return
|
||||||
createTabPromptState(tabs, props.tab, current.scope, {
|
createTabPromptState(tabs, props.tab, current.scope, {
|
||||||
dir: base64Encode(value.directory),
|
dir: base64Encode(value.directory),
|
||||||
id: value.id,
|
id: value.id,
|
||||||
@@ -88,7 +89,7 @@ function SessionTabSlot(props: {
|
|||||||
data-tab-key={props.id}
|
data-tab-key={props.id}
|
||||||
data-active={props.active()}
|
data-active={props.active()}
|
||||||
class="relative flex w-56 min-w-7 max-w-56 flex-shrink"
|
class="relative flex w-56 min-w-7 max-w-56 flex-shrink"
|
||||||
classList={{ hidden: !session() && !missingSession() }}
|
classList={{ hidden: !session() && !missingSession() && !persisted()?.title }}
|
||||||
>
|
>
|
||||||
<TabNavItem
|
<TabNavItem
|
||||||
ref={(el) => {
|
ref={(el) => {
|
||||||
@@ -97,7 +98,7 @@ function SessionTabSlot(props: {
|
|||||||
href={tabHref(props.tab)}
|
href={tabHref(props.tab)}
|
||||||
server={props.tab.server}
|
server={props.tab.server}
|
||||||
session={session}
|
session={session}
|
||||||
fallbackTitle={missingSession() ? language.t("session.tab.unknown") : undefined}
|
fallbackTitle={persisted()?.title ?? (missingSession() ? language.t("session.tab.unknown") : undefined)}
|
||||||
onTitleChange={(title) => {
|
onTitleChange={(title) => {
|
||||||
const value = session()
|
const value = session()
|
||||||
const ctx = props.serverCtx()
|
const ctx = props.serverCtx()
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { createEffect, createMemo, createResource, createSignal, Match, Show, Switch, untrack } from "solid-js"
|
import { createEffect, createMemo, createResource, createSignal, Match, onMount, Show, Switch, untrack } from "solid-js"
|
||||||
import { createStore } from "solid-js/store"
|
import { createStore } from "solid-js/store"
|
||||||
import { useLocation, useNavigate, useParams } from "@solidjs/router"
|
import { useLocation, useNavigate, useParams } from "@solidjs/router"
|
||||||
import { IconButton } from "@opencode-ai/ui/icon-button"
|
import { IconButton } from "@opencode-ai/ui/icon-button"
|
||||||
@@ -60,6 +60,12 @@ export type TitlebarUpdate = {
|
|||||||
install: () => void
|
install: () => void
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function useTitlebarRightMount() {
|
||||||
|
const [mount, setMount] = createSignal<HTMLElement | null>(null)
|
||||||
|
onMount(() => setMount(document.getElementById("opencode-titlebar-right")))
|
||||||
|
return mount
|
||||||
|
}
|
||||||
|
|
||||||
export function Titlebar(props: { update?: TitlebarUpdate }) {
|
export function Titlebar(props: { update?: TitlebarUpdate }) {
|
||||||
const layout = useLayout()
|
const layout = useLayout()
|
||||||
const platform = usePlatform()
|
const platform = usePlatform()
|
||||||
@@ -328,6 +334,21 @@ export function Titlebar(props: { update?: TitlebarUpdate }) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (route.type === "home") {
|
||||||
|
const selection = layout.home.selection()
|
||||||
|
const conn = global.servers.list().find((item) => ServerConnection.key(item) === selection.server)
|
||||||
|
const project = conn
|
||||||
|
? global
|
||||||
|
.ensureServerCtx(conn)
|
||||||
|
.projects.list()
|
||||||
|
.find((item) => item.worktree === selection.directory)
|
||||||
|
: undefined
|
||||||
|
if (conn && project) {
|
||||||
|
tabs.newDraft({ server: ServerConnection.key(conn), directory: project.worktree }, "")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const current = layout.projects.list()[0]
|
const current = layout.projects.list()[0]
|
||||||
if (current) {
|
if (current) {
|
||||||
tabs.newDraft({ server: server.key, directory: current.worktree }, "")
|
tabs.newDraft({ server: server.key, directory: current.worktree }, "")
|
||||||
@@ -374,9 +395,16 @@ export function Titlebar(props: { update?: TitlebarUpdate }) {
|
|||||||
keybind: "mod+w",
|
keybind: "mod+w",
|
||||||
hidden: true,
|
hidden: true,
|
||||||
onSelect: () => {
|
onSelect: () => {
|
||||||
tabsStoreActions.removeTab(tabsStore.findIndex((tab) => current === tab))
|
tabsStoreActions.closeTab(tabsStore.findIndex((tab) => current === tab))
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
id: "tab.reopenClosed",
|
||||||
|
category: language.t("command.category.file"),
|
||||||
|
title: language.t("command.tab.reopenClosed"),
|
||||||
|
keybind: "mod+shift+t",
|
||||||
|
onSelect: () => tabsStoreActions.reopenClosedTab(),
|
||||||
|
},
|
||||||
{
|
{
|
||||||
id: `tab.prev`,
|
id: `tab.prev`,
|
||||||
category: "tab",
|
category: "tab",
|
||||||
@@ -464,7 +492,7 @@ export function Titlebar(props: { update?: TitlebarUpdate }) {
|
|||||||
}}
|
}}
|
||||||
onClose={(tab) => {
|
onClose={(tab) => {
|
||||||
const index = tabsStore.findIndex((item) => tabKey(item) === tabKey(tab))
|
const index = tabsStore.findIndex((item) => tabKey(item) === tabKey(tab))
|
||||||
if (index !== -1) tabsStoreActions.removeTab(index)
|
if (index !== -1) tabsStoreActions.closeTab(index)
|
||||||
}}
|
}}
|
||||||
onReorder={(keys) => tabsStoreActions.reorder(keys)}
|
onReorder={(keys) => tabsStoreActions.reorder(keys)}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -0,0 +1,18 @@
|
|||||||
|
import { expect, test } from "bun:test"
|
||||||
|
import { virtualScrollElement } from "./virtual-scroll-element"
|
||||||
|
|
||||||
|
test("resolves the connected viewport that owns the virtual root", () => {
|
||||||
|
const stale = document.createElement("div")
|
||||||
|
stale.className = "scroll-view__viewport"
|
||||||
|
const viewport = document.createElement("div")
|
||||||
|
viewport.className = "scroll-view__viewport"
|
||||||
|
const root = document.createElement("div")
|
||||||
|
viewport.append(root)
|
||||||
|
document.body.append(viewport)
|
||||||
|
|
||||||
|
expect(virtualScrollElement(root)).toBe(viewport)
|
||||||
|
expect(virtualScrollElement(root)).not.toBe(stale)
|
||||||
|
|
||||||
|
viewport.remove()
|
||||||
|
expect(virtualScrollElement(root)).toBeNull()
|
||||||
|
})
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
export function virtualScrollElement(root: HTMLElement | undefined) {
|
||||||
|
if (!root?.isConnected) return null
|
||||||
|
return root.closest<HTMLDivElement>(".scroll-view__viewport")
|
||||||
|
}
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
import type { SessionTab, Tab } from "./tabs"
|
||||||
|
|
||||||
|
export type ClosedTab = {
|
||||||
|
tab: SessionTab
|
||||||
|
index: number
|
||||||
|
}
|
||||||
|
|
||||||
|
const CLOSED_TAB_LIMIT = 25
|
||||||
|
|
||||||
|
// Only session tabs are recorded; closing a draft tab deletes its persisted
|
||||||
|
// state, so a reopened draft would come back empty anyway.
|
||||||
|
export function pushClosedTab(stack: ClosedTab[], tab: Tab, index: number): ClosedTab[] {
|
||||||
|
if (tab.type !== "session") return stack
|
||||||
|
return [...stack, { tab: { ...tab }, index }].slice(-CLOSED_TAB_LIMIT)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Pops the most recently closed tab that is not open again,
|
||||||
|
// discarding stale entries along the way.
|
||||||
|
export function takeClosedTab(stack: ClosedTab[], tabs: Tab[]): { entry?: ClosedTab; stack: ClosedTab[] } {
|
||||||
|
const remaining = [...stack]
|
||||||
|
while (remaining.length) {
|
||||||
|
const entry = remaining.pop()
|
||||||
|
if (entry && !isOpen(tabs, entry.tab)) return { entry, stack: remaining }
|
||||||
|
}
|
||||||
|
return { stack: remaining }
|
||||||
|
}
|
||||||
|
|
||||||
|
export function removeClosedTabs(stack: ClosedTab[], server: SessionTab["server"], sessionIDs: string[]) {
|
||||||
|
const removed = new Set(sessionIDs)
|
||||||
|
return stack.filter((entry) => entry.tab.server !== server || !removed.has(entry.tab.sessionId))
|
||||||
|
}
|
||||||
|
|
||||||
|
export function nextTabAfterClose(tabs: Tab[], index: number, active: boolean) {
|
||||||
|
if (!active) return undefined
|
||||||
|
return tabs[index + 1] ?? tabs[index - 1] ?? null
|
||||||
|
}
|
||||||
|
|
||||||
|
function isOpen(tabs: Tab[], tab: SessionTab) {
|
||||||
|
return tabs.some((item) => item.type === "session" && item.server === tab.server && item.sessionId === tab.sessionId)
|
||||||
|
}
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
import { describe, expect, test } from "bun:test"
|
import { describe, expect, test } from "bun:test"
|
||||||
import { upsertCommandRegistration } from "./command"
|
import { resolveKeybindOption, upsertCommandRegistration } from "./command"
|
||||||
|
|
||||||
describe("upsertCommandRegistration", () => {
|
describe("upsertCommandRegistration", () => {
|
||||||
test("replaces keyed registrations", () => {
|
test("replaces keyed registrations", () => {
|
||||||
@@ -23,3 +23,19 @@ describe("upsertCommandRegistration", () => {
|
|||||||
expect(next[1]?.options).toBe(one)
|
expect(next[1]?.options).toBe(one)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
describe("resolveKeybindOption", () => {
|
||||||
|
test("prefers a matching contextual command over the global fallback", () => {
|
||||||
|
const fallback = { id: "tab.close", title: "Close tab" }
|
||||||
|
const contextual = { id: "terminal.close", title: "Close terminal", when: () => true }
|
||||||
|
|
||||||
|
expect(resolveKeybindOption([fallback, contextual], new KeyboardEvent("keydown"))).toBe(contextual)
|
||||||
|
})
|
||||||
|
|
||||||
|
test("uses the global fallback outside the command context", () => {
|
||||||
|
const fallback = { id: "tab.close", title: "Close tab" }
|
||||||
|
const contextual = { id: "terminal.close", title: "Close terminal", when: () => false }
|
||||||
|
|
||||||
|
expect(resolveKeybindOption([fallback, contextual], new KeyboardEvent("keydown"))).toBe(fallback)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|||||||
@@ -82,10 +82,15 @@ export interface CommandOption {
|
|||||||
suggested?: boolean
|
suggested?: boolean
|
||||||
disabled?: boolean
|
disabled?: boolean
|
||||||
hidden?: boolean
|
hidden?: boolean
|
||||||
|
when?: (event: KeyboardEvent) => boolean
|
||||||
onSelect?: (source?: "palette" | "keybind" | "slash") => void
|
onSelect?: (source?: "palette" | "keybind" | "slash") => void
|
||||||
onHighlight?: () => (() => void) | void
|
onHighlight?: () => (() => void) | void
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function resolveKeybindOption(candidates: CommandOption[] | undefined, event: KeyboardEvent) {
|
||||||
|
return candidates?.find((option) => option.when?.(event)) ?? candidates?.find((option) => !option.when)
|
||||||
|
}
|
||||||
|
|
||||||
type CommandSource = "palette" | "keybind" | "slash"
|
type CommandSource = "palette" | "keybind" | "slash"
|
||||||
|
|
||||||
export type CommandCatalogItem = {
|
export type CommandCatalogItem = {
|
||||||
@@ -334,7 +339,7 @@ export const { use: useCommand, provider: CommandProvider } = createSimpleContex
|
|||||||
})
|
})
|
||||||
|
|
||||||
const keymap = createMemo(() => {
|
const keymap = createMemo(() => {
|
||||||
const map = new Map<string, CommandOption>()
|
const map = new Map<string, CommandOption[]>()
|
||||||
for (const option of options()) {
|
for (const option of options()) {
|
||||||
if (option.id.startsWith(SUGGESTED_PREFIX)) continue
|
if (option.id.startsWith(SUGGESTED_PREFIX)) continue
|
||||||
if (option.disabled) continue
|
if (option.disabled) continue
|
||||||
@@ -344,8 +349,12 @@ export const { use: useCommand, provider: CommandProvider } = createSimpleContex
|
|||||||
for (const kb of keybinds) {
|
for (const kb of keybinds) {
|
||||||
if (!kb.key) continue
|
if (!kb.key) continue
|
||||||
const sig = signature(kb.key, kb.ctrl, kb.meta, kb.shift, kb.alt)
|
const sig = signature(kb.key, kb.ctrl, kb.meta, kb.shift, kb.alt)
|
||||||
if (map.has(sig)) continue
|
const existing = map.get(sig)
|
||||||
map.set(sig, option)
|
if (existing) {
|
||||||
|
existing.push(option)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
map.set(sig, [option])
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return map
|
return map
|
||||||
@@ -374,7 +383,7 @@ export const { use: useCommand, provider: CommandProvider } = createSimpleContex
|
|||||||
|
|
||||||
const sig = signatureFromEvent(event)
|
const sig = signatureFromEvent(event)
|
||||||
const isPalette = palette().has(sig)
|
const isPalette = palette().has(sig)
|
||||||
const option = keymap().get(sig)
|
const option = resolveKeybindOption(keymap().get(sig), event)
|
||||||
const modified = event.ctrlKey || event.metaKey || event.altKey
|
const modified = event.ctrlKey || event.metaKey || event.altKey
|
||||||
const isTab = event.key === "Tab"
|
const isTab = event.key === "Tab"
|
||||||
|
|
||||||
@@ -383,17 +392,19 @@ export const { use: useCommand, provider: CommandProvider } = createSimpleContex
|
|||||||
|
|
||||||
if (isPalette) {
|
if (isPalette) {
|
||||||
event.preventDefault()
|
event.preventDefault()
|
||||||
|
event.stopPropagation()
|
||||||
showPalette()
|
showPalette()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!option) return
|
if (!option) return
|
||||||
event.preventDefault()
|
event.preventDefault()
|
||||||
|
event.stopPropagation()
|
||||||
option.onSelect?.("keybind")
|
option.onSelect?.("keybind")
|
||||||
}
|
}
|
||||||
|
|
||||||
onMount(() => {
|
onMount(() => {
|
||||||
makeEventListener(document, "keydown", handleKeyDown)
|
makeEventListener(document, "keydown", handleKeyDown, { capture: true })
|
||||||
})
|
})
|
||||||
|
|
||||||
function register(cb: () => CommandOption[]): void
|
function register(cb: () => CommandOption[]): void
|
||||||
|
|||||||
@@ -203,12 +203,15 @@ export const { use: useFile, provider: FileProvider } = createSimpleContext({
|
|||||||
return promise
|
return promise
|
||||||
}
|
}
|
||||||
|
|
||||||
const search = (query: string, dirs: "true" | "false") =>
|
const search = (query: string, dirs: "true" | "false", options?: { limit?: number; signal?: AbortSignal }) =>
|
||||||
sdk()
|
sdk()
|
||||||
.client.find.files({ query, dirs })
|
.client.find.files({ query, dirs, limit: options?.limit }, { signal: options?.signal })
|
||||||
.then(
|
.then(
|
||||||
(x) => (x.data ?? []).map(path.normalize),
|
(x) => (x.data ?? []).map(path.normalize),
|
||||||
() => [],
|
(error) => {
|
||||||
|
if (options?.signal?.aborted) throw error
|
||||||
|
return []
|
||||||
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
const stop = sdk().event.listen((e) => {
|
const stop = sdk().event.listen((e) => {
|
||||||
@@ -284,7 +287,8 @@ export const { use: useFile, provider: FileProvider } = createSimpleContext({
|
|||||||
setScrollLeft,
|
setScrollLeft,
|
||||||
selectedLines,
|
selectedLines,
|
||||||
setSelectedLines,
|
setSelectedLines,
|
||||||
searchFiles: (query: string) => search(query, "false"),
|
searchFiles: (query: string, options?: { limit?: number; signal?: AbortSignal }) =>
|
||||||
|
search(query, "false", options),
|
||||||
searchFilesAndDirectories: (query: string) => search(query, "true"),
|
searchFilesAndDirectories: (query: string) => search(query, "true"),
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -0,0 +1,82 @@
|
|||||||
|
import { describe, expect, test } from "bun:test"
|
||||||
|
import {
|
||||||
|
SESSION_OPEN_FILE_TAB,
|
||||||
|
closeSessionTab,
|
||||||
|
openSessionTab,
|
||||||
|
previewSessionTab,
|
||||||
|
type SessionTabState,
|
||||||
|
} from "./layout-tabs"
|
||||||
|
|
||||||
|
const state = (all: string[], active?: string, preview?: string): SessionTabState => ({
|
||||||
|
tabs: { all, active },
|
||||||
|
preview,
|
||||||
|
})
|
||||||
|
|
||||||
|
describe("previewSessionTab", () => {
|
||||||
|
test("appends the Open File placeholder", () => {
|
||||||
|
expect(previewSessionTab(state(["file://a.ts"], "file://a.ts"), SESSION_OPEN_FILE_TAB)).toEqual(
|
||||||
|
state(["file://a.ts", SESSION_OPEN_FILE_TAB], SESSION_OPEN_FILE_TAB, SESSION_OPEN_FILE_TAB),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
test("replaces the current preview in place", () => {
|
||||||
|
expect(
|
||||||
|
previewSessionTab(
|
||||||
|
state(["context", SESSION_OPEN_FILE_TAB, "file://b.ts"], SESSION_OPEN_FILE_TAB, SESSION_OPEN_FILE_TAB),
|
||||||
|
"file://a.ts",
|
||||||
|
),
|
||||||
|
).toEqual(state(["context", "file://a.ts", "file://b.ts"], "file://a.ts", "file://a.ts"))
|
||||||
|
})
|
||||||
|
|
||||||
|
test("activates a durable tab without duplicating it", () => {
|
||||||
|
expect(
|
||||||
|
previewSessionTab(
|
||||||
|
state(["file://a.ts", SESSION_OPEN_FILE_TAB, "file://b.ts"], SESSION_OPEN_FILE_TAB, SESSION_OPEN_FILE_TAB),
|
||||||
|
"file://b.ts",
|
||||||
|
),
|
||||||
|
).toEqual(state(["file://a.ts", "file://b.ts"], "file://b.ts"))
|
||||||
|
})
|
||||||
|
|
||||||
|
test("replaces a restored Open File placeholder", () => {
|
||||||
|
expect(
|
||||||
|
previewSessionTab(state(["file://a.ts", SESSION_OPEN_FILE_TAB], SESSION_OPEN_FILE_TAB), "file://b.ts"),
|
||||||
|
).toEqual(state(["file://a.ts", "file://b.ts"], "file://b.ts", "file://b.ts"))
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe("openSessionTab", () => {
|
||||||
|
test("pins the current preview", () => {
|
||||||
|
expect(openSessionTab(state(["file://a.ts"], "file://a.ts", "file://a.ts"), "file://a.ts")).toEqual(
|
||||||
|
state(["file://a.ts"], "file://a.ts"),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
test("replaces a preview with a directly opened file", () => {
|
||||||
|
expect(openSessionTab(state(["file://a.ts"], "file://a.ts", "file://a.ts"), "file://b.ts")).toEqual(
|
||||||
|
state(["file://b.ts"], "file://b.ts"),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
test("keeps the preview when switching to Review", () => {
|
||||||
|
expect(openSessionTab(state(["file://a.ts"], "file://a.ts", "file://a.ts"), "review")).toEqual(
|
||||||
|
state(["file://a.ts"], "review", "file://a.ts"),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
test("replaces a restored Open File placeholder with a direct open", () => {
|
||||||
|
expect(openSessionTab(state(["file://a.ts", SESSION_OPEN_FILE_TAB], SESSION_OPEN_FILE_TAB), "file://b.ts")).toEqual(
|
||||||
|
state(["file://a.ts", "file://b.ts"], "file://b.ts"),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe("closeSessionTab", () => {
|
||||||
|
test("clears preview metadata and selects the left neighbor", () => {
|
||||||
|
expect(
|
||||||
|
closeSessionTab(
|
||||||
|
state(["file://a.ts", "file://b.ts", "file://c.ts"], "file://b.ts", "file://b.ts"),
|
||||||
|
"file://b.ts",
|
||||||
|
),
|
||||||
|
).toEqual(state(["file://a.ts", "file://c.ts"], "file://a.ts"))
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -0,0 +1,103 @@
|
|||||||
|
export const SESSION_OPEN_FILE_TAB = "open-file"
|
||||||
|
|
||||||
|
export type SessionTabs = {
|
||||||
|
active?: string
|
||||||
|
all: string[]
|
||||||
|
}
|
||||||
|
|
||||||
|
export type SessionTabState = {
|
||||||
|
tabs: SessionTabs
|
||||||
|
preview?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
const sessionTabPreview = (current: SessionTabState) =>
|
||||||
|
current.preview ?? (current.tabs.all.includes(SESSION_OPEN_FILE_TAB) ? SESSION_OPEN_FILE_TAB : undefined)
|
||||||
|
|
||||||
|
export function previewSessionTab(current: SessionTabState, tab: string): SessionTabState {
|
||||||
|
const preview = sessionTabPreview(current)
|
||||||
|
const previewIndex = preview ? current.tabs.all.indexOf(preview) : -1
|
||||||
|
const existingIndex = current.tabs.all.indexOf(tab)
|
||||||
|
|
||||||
|
if (existingIndex !== -1) {
|
||||||
|
if (previewIndex === -1 || preview === tab) {
|
||||||
|
return { tabs: { all: current.tabs.all, active: tab }, preview: preview === tab ? tab : undefined }
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
tabs: { all: current.tabs.all.filter((item) => item !== preview), active: tab },
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (previewIndex === -1) {
|
||||||
|
return { tabs: { all: [...current.tabs.all, tab], active: tab }, preview: tab }
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
tabs: {
|
||||||
|
all: current.tabs.all.map((item, index) => (index === previewIndex ? tab : item)),
|
||||||
|
active: tab,
|
||||||
|
},
|
||||||
|
preview: tab,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function openSessionTab(current: SessionTabState, tab: string): SessionTabState {
|
||||||
|
const preview = sessionTabPreview(current)
|
||||||
|
if (tab === "review") {
|
||||||
|
return {
|
||||||
|
tabs: { all: current.tabs.all.filter((item) => item !== tab), active: tab },
|
||||||
|
preview,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (tab === "context") {
|
||||||
|
return {
|
||||||
|
tabs: { all: [tab, ...current.tabs.all.filter((item) => item !== tab)], active: tab },
|
||||||
|
preview,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const previewIndex = preview ? current.tabs.all.indexOf(preview) : -1
|
||||||
|
const existingIndex = current.tabs.all.indexOf(tab)
|
||||||
|
if (existingIndex !== -1) {
|
||||||
|
if (previewIndex === -1 || preview === tab) {
|
||||||
|
return { tabs: { all: current.tabs.all, active: tab } }
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
tabs: { all: current.tabs.all.filter((item) => item !== preview), active: tab },
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (previewIndex === -1) {
|
||||||
|
return { tabs: { all: [...current.tabs.all, tab], active: tab } }
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
tabs: {
|
||||||
|
all: current.tabs.all.map((item, index) => (index === previewIndex ? tab : item)),
|
||||||
|
active: tab,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function closeSessionTab(current: SessionTabState, tab: string): SessionTabState {
|
||||||
|
if (tab === "review") {
|
||||||
|
if (current.tabs.active !== tab) return current
|
||||||
|
return {
|
||||||
|
tabs: { all: current.tabs.all, active: current.tabs.all[0] },
|
||||||
|
preview: current.preview,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const all = current.tabs.all.filter((item) => item !== tab)
|
||||||
|
const preview = current.preview === tab ? undefined : current.preview
|
||||||
|
if (current.tabs.active !== tab) return { tabs: { ...current.tabs, all }, preview }
|
||||||
|
|
||||||
|
const index = current.tabs.all.indexOf(tab)
|
||||||
|
return {
|
||||||
|
tabs: {
|
||||||
|
all,
|
||||||
|
active: current.tabs.all[index - 1] ?? current.tabs.all[index + 1] ?? all[0],
|
||||||
|
},
|
||||||
|
preview,
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -19,6 +19,7 @@ import { migrateLegacySessionStateKeys, ServerScope, SessionStateKey } from "@/u
|
|||||||
import { createSessionKeyReader, ensureSessionKey, pruneSessionKeys } from "./layout-helpers"
|
import { createSessionKeyReader, ensureSessionKey, pruneSessionKeys } from "./layout-helpers"
|
||||||
import { requireServerKey } from "@/utils/session-route"
|
import { requireServerKey } from "@/utils/session-route"
|
||||||
import { type DraftTab, useTabs } from "./tabs"
|
import { type DraftTab, useTabs } from "./tabs"
|
||||||
|
import { closeSessionTab, openSessionTab, previewSessionTab, type SessionTabs } from "./layout-tabs"
|
||||||
|
|
||||||
export { createSessionKeyReader, ensureSessionKey, pruneSessionKeys }
|
export { createSessionKeyReader, ensureSessionKey, pruneSessionKeys }
|
||||||
|
|
||||||
@@ -55,14 +56,11 @@ export function getProjectAvatarVariant(key?: string): ProjectAvatarVariant {
|
|||||||
return "gray"
|
return "gray"
|
||||||
}
|
}
|
||||||
|
|
||||||
type SessionTabs = {
|
|
||||||
active?: string
|
|
||||||
all: string[]
|
|
||||||
}
|
|
||||||
|
|
||||||
type SessionView = {
|
type SessionView = {
|
||||||
scroll: Record<string, SessionScroll>
|
scroll: Record<string, SessionScroll>
|
||||||
reviewOpen?: string[]
|
reviewOpen?: string[]
|
||||||
|
reviewMode?: ReviewChangeMode
|
||||||
|
reviewFile?: string
|
||||||
pendingMessage?: string
|
pendingMessage?: string
|
||||||
pendingMessageAt?: number
|
pendingMessageAt?: number
|
||||||
todoCollapsed?: boolean
|
todoCollapsed?: boolean
|
||||||
@@ -79,6 +77,7 @@ export type LocalProject = Partial<Project> & { worktree: string; expanded: bool
|
|||||||
export type HomeProjectSelection = { server: ServerConnection.Key; directory?: string }
|
export type HomeProjectSelection = { server: ServerConnection.Key; directory?: string }
|
||||||
|
|
||||||
export type ReviewDiffStyle = "unified" | "split"
|
export type ReviewDiffStyle = "unified" | "split"
|
||||||
|
export type ReviewChangeMode = "git" | "branch" | "turn"
|
||||||
export type ReviewPanelSource = "context-button" | "other"
|
export type ReviewPanelSource = "context-button" | "other"
|
||||||
|
|
||||||
export type LayoutRoute =
|
export type LayoutRoute =
|
||||||
@@ -87,14 +86,6 @@ export type LayoutRoute =
|
|||||||
| { type: "dir-new-sesssion"; dir: string; dirBase64: string; server?: ServerConnection.Key }
|
| { type: "dir-new-sesssion"; dir: string; dirBase64: string; server?: ServerConnection.Key }
|
||||||
| { type: "session"; sessionId: string; server?: ServerConnection.Key }
|
| { type: "session"; sessionId: string; server?: ServerConnection.Key }
|
||||||
|
|
||||||
function nextSessionTabsForOpen(current: SessionTabs | undefined, tab: string): SessionTabs {
|
|
||||||
const all = current?.all ?? []
|
|
||||||
if (tab === "review") return { all: all.filter((x) => x !== "review"), active: tab }
|
|
||||||
if (tab === "context") return { all: [tab, ...all.filter((x) => x !== tab)], active: tab }
|
|
||||||
if (!all.includes(tab)) return { all: [...all, tab], active: tab }
|
|
||||||
return { all, active: tab }
|
|
||||||
}
|
|
||||||
|
|
||||||
const sessionPath = (key: string) => {
|
const sessionPath = (key: string) => {
|
||||||
const dir = SessionStateKey.route(key).split("/")[0]
|
const dir = SessionStateKey.route(key).split("/")[0]
|
||||||
if (!dir) return
|
if (!dir) return
|
||||||
@@ -308,6 +299,7 @@ export const { use: useLayout, provider: LayoutProvider } = createSimpleContext(
|
|||||||
)
|
)
|
||||||
const [ephemeral, setEphemeral] = createStore({
|
const [ephemeral, setEphemeral] = createStore({
|
||||||
reviewPanelSource: "other" as ReviewPanelSource,
|
reviewPanelSource: "other" as ReviewPanelSource,
|
||||||
|
sessionTabPreview: {} as Record<string, string | undefined>,
|
||||||
})
|
})
|
||||||
|
|
||||||
const MAX_SESSION_KEYS = 50
|
const MAX_SESSION_KEYS = 50
|
||||||
@@ -366,6 +358,12 @@ export const { use: useLayout, provider: LayoutProvider } = createSimpleContext(
|
|||||||
|
|
||||||
scroll.drop(drop)
|
scroll.drop(drop)
|
||||||
dropSessionState(drop)
|
dropSessionState(drop)
|
||||||
|
setEphemeral(
|
||||||
|
"sessionTabPreview",
|
||||||
|
produce((draft) => {
|
||||||
|
for (const key of drop) delete draft[key]
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
for (const key of drop) {
|
for (const key of drop) {
|
||||||
usage.used.delete(key)
|
usage.used.delete(key)
|
||||||
@@ -791,6 +789,14 @@ export const { use: useLayout, provider: LayoutProvider } = createSimpleContext(
|
|||||||
view(sessionKey: string | Accessor<string>) {
|
view(sessionKey: string | Accessor<string>) {
|
||||||
const key = createSessionKeyReader(sessionKey, ensureKey)
|
const key = createSessionKeyReader(sessionKey, ensureKey)
|
||||||
const s = createMemo(() => store.sessionView[key()] ?? { scroll: {} })
|
const s = createMemo(() => store.sessionView[key()] ?? { scroll: {} })
|
||||||
|
const reviewMode = createMemo(() => {
|
||||||
|
const mode = s().reviewMode
|
||||||
|
if (mode === "git" || mode === "branch" || mode === "turn") return mode
|
||||||
|
})
|
||||||
|
const reviewFile = createMemo(() => {
|
||||||
|
const file = s().reviewFile
|
||||||
|
if (typeof file === "string") return file
|
||||||
|
})
|
||||||
const terminalOpened = createMemo(() => store.terminal?.opened ?? false)
|
const terminalOpened = createMemo(() => store.terminal?.opened ?? false)
|
||||||
const reviewPanelOpened = createMemo(() => store.review?.panelOpened ?? DEFAULT_REVIEW_PANEL_OPENED)
|
const reviewPanelOpened = createMemo(() => store.review?.panelOpened ?? DEFAULT_REVIEW_PANEL_OPENED)
|
||||||
const reviewPanelSource = createMemo(() => (reviewPanelOpened() ? ephemeral.reviewPanelSource : "other"))
|
const reviewPanelSource = createMemo(() => (reviewPanelOpened() ? ephemeral.reviewPanelSource : "other"))
|
||||||
@@ -874,6 +880,32 @@ export const { use: useLayout, provider: LayoutProvider } = createSimpleContext(
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
review: {
|
review: {
|
||||||
|
mode: reviewMode,
|
||||||
|
setMode(mode: ReviewChangeMode) {
|
||||||
|
const session = key()
|
||||||
|
const current = store.sessionView[session]
|
||||||
|
if (!current) {
|
||||||
|
setStore("sessionView", session, { scroll: {}, reviewMode: mode })
|
||||||
|
prune(session)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (current.reviewMode === mode) return
|
||||||
|
setStore("sessionView", session, "reviewMode", mode)
|
||||||
|
prune(session)
|
||||||
|
},
|
||||||
|
file: reviewFile,
|
||||||
|
setFile(file: string) {
|
||||||
|
const session = key()
|
||||||
|
const current = store.sessionView[session]
|
||||||
|
if (!current) {
|
||||||
|
setStore("sessionView", session, { scroll: {}, reviewFile: file })
|
||||||
|
prune(session)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (current.reviewFile === file) return
|
||||||
|
setStore("sessionView", session, "reviewFile", file)
|
||||||
|
prune(session)
|
||||||
|
},
|
||||||
open: createMemo(() => s().reviewOpen ?? []),
|
open: createMemo(() => s().reviewOpen ?? []),
|
||||||
setOpen(open: string[]) {
|
setOpen(open: string[]) {
|
||||||
const session = key()
|
const session = key()
|
||||||
@@ -945,10 +977,17 @@ export const { use: useLayout, provider: LayoutProvider } = createSimpleContext(
|
|||||||
const tabs = createMemo(() => store.sessionTabs[key()] ?? { all: [] })
|
const tabs = createMemo(() => store.sessionTabs[key()] ?? { all: [] })
|
||||||
const normalize = (tab: string) => normalizeSessionTab(path(), tab)
|
const normalize = (tab: string) => normalizeSessionTab(path(), tab)
|
||||||
const normalizeAll = (all: string[]) => normalizeSessionTabList(path(), all)
|
const normalizeAll = (all: string[]) => normalizeSessionTabList(path(), all)
|
||||||
|
const apply = (session: string, next: ReturnType<typeof openSessionTab>) => {
|
||||||
|
batch(() => {
|
||||||
|
setStore("sessionTabs", session, next.tabs)
|
||||||
|
setEphemeral("sessionTabPreview", session, next.preview)
|
||||||
|
})
|
||||||
|
}
|
||||||
return {
|
return {
|
||||||
tabs,
|
tabs,
|
||||||
active: createMemo(() => tabs().active),
|
active: createMemo(() => tabs().active),
|
||||||
all: createMemo(() => tabs().all.filter((tab) => tab !== "review")),
|
all: createMemo(() => tabs().all.filter((tab) => tab !== "review")),
|
||||||
|
preview: createMemo(() => ephemeral.sessionTabPreview[key()]),
|
||||||
setActive(tab: string | undefined) {
|
setActive(tab: string | undefined) {
|
||||||
const session = key()
|
const session = key()
|
||||||
const next = tab ? normalize(tab) : tab
|
const next = tab ? normalize(tab) : tab
|
||||||
@@ -961,40 +1000,44 @@ export const { use: useLayout, provider: LayoutProvider } = createSimpleContext(
|
|||||||
setAll(all: string[]) {
|
setAll(all: string[]) {
|
||||||
const session = key()
|
const session = key()
|
||||||
const next = normalizeAll(all).filter((tab) => tab !== "review")
|
const next = normalizeAll(all).filter((tab) => tab !== "review")
|
||||||
if (!store.sessionTabs[session]) {
|
batch(() => {
|
||||||
setStore("sessionTabs", session, { all: next, active: undefined })
|
if (!store.sessionTabs[session]) {
|
||||||
} else {
|
setStore("sessionTabs", session, { all: next, active: undefined })
|
||||||
setStore("sessionTabs", session, "all", next)
|
} else {
|
||||||
}
|
setStore("sessionTabs", session, "all", next)
|
||||||
|
}
|
||||||
|
const preview = ephemeral.sessionTabPreview[session]
|
||||||
|
if (preview && !next.includes(preview)) setEphemeral("sessionTabPreview", session, undefined)
|
||||||
|
})
|
||||||
},
|
},
|
||||||
async open(tab: string) {
|
async open(tab: string) {
|
||||||
const session = key()
|
const session = key()
|
||||||
const next = nextSessionTabsForOpen(store.sessionTabs[session], normalize(tab))
|
apply(
|
||||||
setStore("sessionTabs", session, next)
|
session,
|
||||||
|
openSessionTab(
|
||||||
|
{ tabs: store.sessionTabs[session] ?? { all: [] }, preview: ephemeral.sessionTabPreview[session] },
|
||||||
|
normalize(tab),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
},
|
||||||
|
previewTab(tab: string) {
|
||||||
|
const session = key()
|
||||||
|
apply(
|
||||||
|
session,
|
||||||
|
previewSessionTab(
|
||||||
|
{ tabs: store.sessionTabs[session] ?? { all: [] }, preview: ephemeral.sessionTabPreview[session] },
|
||||||
|
normalize(tab),
|
||||||
|
),
|
||||||
|
)
|
||||||
},
|
},
|
||||||
close(tab: string) {
|
close(tab: string) {
|
||||||
const session = key()
|
const session = key()
|
||||||
const current = store.sessionTabs[session]
|
const current = store.sessionTabs[session]
|
||||||
if (!current) return
|
if (!current) return
|
||||||
|
apply(
|
||||||
if (tab === "review") {
|
session,
|
||||||
if (current.active !== tab) return
|
closeSessionTab({ tabs: current, preview: ephemeral.sessionTabPreview[session] }, normalize(tab)),
|
||||||
setStore("sessionTabs", session, "active", current.all[0])
|
)
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
const all = current.all.filter((x) => x !== tab)
|
|
||||||
if (current.active !== tab) {
|
|
||||||
setStore("sessionTabs", session, "all", all)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
const index = current.all.findIndex((f) => f === tab)
|
|
||||||
const next = current.all[index - 1] ?? current.all[index + 1] ?? all[0]
|
|
||||||
batch(() => {
|
|
||||||
setStore("sessionTabs", session, "all", all)
|
|
||||||
setStore("sessionTabs", session, "active", next)
|
|
||||||
})
|
|
||||||
},
|
},
|
||||||
move(tab: string, to: number) {
|
move(tab: string, to: number) {
|
||||||
const session = key()
|
const session = key()
|
||||||
|
|||||||
@@ -15,11 +15,13 @@ const session = (id: string, parentID?: string): Session => ({
|
|||||||
})
|
})
|
||||||
|
|
||||||
type UserMessage = Extract<Message, { role: "user" }>
|
type UserMessage = Extract<Message, { role: "user" }>
|
||||||
|
type AssistantMessage = Extract<Message, { role: "assistant" }>
|
||||||
type TextPart = Extract<Part, { type: "text" }>
|
type TextPart = Extract<Part, { type: "text" }>
|
||||||
type MessageResponse = {
|
type MessageResponse = {
|
||||||
data: { info: Message; parts: Part[] }[]
|
data: { info: Message; parts: Part[] }[]
|
||||||
response: { headers: Headers }
|
response: { headers: Headers }
|
||||||
}
|
}
|
||||||
|
type SingleMessageResponse = { data: MessageResponse["data"][number] }
|
||||||
|
|
||||||
const userMessage = (id: string, input: Partial<UserMessage> = {}): UserMessage => ({
|
const userMessage = (id: string, input: Partial<UserMessage> = {}): UserMessage => ({
|
||||||
id,
|
id,
|
||||||
@@ -31,6 +33,22 @@ const userMessage = (id: string, input: Partial<UserMessage> = {}): UserMessage
|
|||||||
...input,
|
...input,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const assistantMessage = (id: string, parentID: string, input: Partial<AssistantMessage> = {}): AssistantMessage => ({
|
||||||
|
id,
|
||||||
|
sessionID: "child",
|
||||||
|
role: "assistant",
|
||||||
|
time: { created: Number(id.at(-1)), completed: Number(id.at(-1)) },
|
||||||
|
parentID,
|
||||||
|
modelID: "model",
|
||||||
|
providerID: "provider",
|
||||||
|
mode: "build",
|
||||||
|
agent: "build",
|
||||||
|
path: { cwd: "/repo", root: "/repo" },
|
||||||
|
cost: 0,
|
||||||
|
tokens: { input: 0, output: 0, reasoning: 0, cache: { read: 0, write: 0 } },
|
||||||
|
...input,
|
||||||
|
})
|
||||||
|
|
||||||
const textPart = (messageID: string, input: Partial<TextPart> = {}): TextPart => ({
|
const textPart = (messageID: string, input: Partial<TextPart> = {}): TextPart => ({
|
||||||
id: "part",
|
id: "part",
|
||||||
sessionID: "child",
|
sessionID: "child",
|
||||||
@@ -45,6 +63,8 @@ const response = (data: MessageResponse["data"] = [], cursor?: string): MessageR
|
|||||||
response: { headers: new Headers(cursor ? { "x-next-cursor": cursor } : undefined) },
|
response: { headers: new Headers(cursor ? { "x-next-cursor": cursor } : undefined) },
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const singleResponse = (info: Message, parts: Part[] = []): SingleMessageResponse => ({ data: { info, parts } })
|
||||||
|
|
||||||
const deferredResponse = () => Promise.withResolvers<MessageResponse>()
|
const deferredResponse = () => Promise.withResolvers<MessageResponse>()
|
||||||
|
|
||||||
function messageClient(...responses: Array<MessageResponse | Promise<MessageResponse>>) {
|
function messageClient(...responses: Array<MessageResponse | Promise<MessageResponse>>) {
|
||||||
@@ -71,6 +91,40 @@ function messageClient(...responses: Array<MessageResponse | Promise<MessageResp
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function rootMessageClient(
|
||||||
|
pages: Array<MessageResponse | Promise<MessageResponse>>,
|
||||||
|
roots: Array<SingleMessageResponse | Promise<SingleMessageResponse>>,
|
||||||
|
) {
|
||||||
|
let pageIndex = 0
|
||||||
|
let rootIndex = 0
|
||||||
|
const requests: unknown[] = []
|
||||||
|
const rootRequests: unknown[] = []
|
||||||
|
const rootWaiting = new Map<number, () => void>()
|
||||||
|
const client = {
|
||||||
|
session: {
|
||||||
|
get: async () => ({ data: session("child", "root") }),
|
||||||
|
messages: (input: unknown) => {
|
||||||
|
requests.push(input)
|
||||||
|
return pages[pageIndex++]
|
||||||
|
},
|
||||||
|
message: (input: unknown) => {
|
||||||
|
rootRequests.push(input)
|
||||||
|
rootWaiting.get(rootRequests.length)?.()
|
||||||
|
rootWaiting.delete(rootRequests.length)
|
||||||
|
return roots[rootIndex++]
|
||||||
|
},
|
||||||
|
},
|
||||||
|
} as unknown as OpencodeClient
|
||||||
|
return Object.assign(client, {
|
||||||
|
requests,
|
||||||
|
rootRequests,
|
||||||
|
rootRequested(count: number) {
|
||||||
|
if (rootRequests.length >= count) return Promise.resolve()
|
||||||
|
return new Promise<void>((resolve) => rootWaiting.set(count, resolve))
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
const retryImmediately: typeof retry = async (task, options = {}) => {
|
const retryImmediately: typeof retry = async (task, options = {}) => {
|
||||||
const attempts = options.attempts ?? 3
|
const attempts = options.attempts ?? 3
|
||||||
for (let attempt = 0; ; attempt++) {
|
for (let attempt = 0; ; attempt++) {
|
||||||
@@ -124,6 +178,238 @@ describe("server session", () => {
|
|||||||
expect(ctx.store.data.message.root).toEqual([])
|
expect(ctx.store.data.message.root).toEqual([])
|
||||||
})
|
})
|
||||||
|
|
||||||
|
test("backfills an assistant-only initial page through its user root", async () => {
|
||||||
|
const user = userMessage("message-1")
|
||||||
|
const assistants = [assistantMessage("message-2", user.id), assistantMessage("message-3", user.id)]
|
||||||
|
const client = rootMessageClient(
|
||||||
|
[
|
||||||
|
response(
|
||||||
|
assistants.map((info) => ({ info, parts: [] })),
|
||||||
|
"older",
|
||||||
|
),
|
||||||
|
],
|
||||||
|
[singleResponse(user)],
|
||||||
|
)
|
||||||
|
const store = createServerSession(client)
|
||||||
|
|
||||||
|
await store.sync("child")
|
||||||
|
|
||||||
|
expect(client.requests).toEqual([{ sessionID: "child", limit: 2, before: undefined }])
|
||||||
|
expect(client.rootRequests).toEqual([{ sessionID: "child", messageID: user.id }])
|
||||||
|
expect(store.data.message.child).toEqual([user, ...assistants])
|
||||||
|
expect(store.history.more("child")).toBe(true)
|
||||||
|
})
|
||||||
|
|
||||||
|
test("does not let an optimistic user suppress initial root backfill", async () => {
|
||||||
|
const user = userMessage("message-1")
|
||||||
|
const part = textPart(user.id)
|
||||||
|
const assistants = [assistantMessage("message-2", user.id), assistantMessage("message-3", user.id)]
|
||||||
|
const client = rootMessageClient(
|
||||||
|
[
|
||||||
|
response(
|
||||||
|
assistants.map((info) => ({ info, parts: [] })),
|
||||||
|
"older",
|
||||||
|
),
|
||||||
|
],
|
||||||
|
[singleResponse(user)],
|
||||||
|
)
|
||||||
|
const store = createServerSession(client)
|
||||||
|
store.optimistic.add({ sessionID: "child", message: user, parts: [part] })
|
||||||
|
|
||||||
|
await store.sync("child")
|
||||||
|
store.optimistic.remove({ sessionID: "child", messageID: user.id })
|
||||||
|
|
||||||
|
expect(client.requests).toHaveLength(1)
|
||||||
|
expect(client.rootRequests).toHaveLength(1)
|
||||||
|
expect(store.data.message.child).toEqual([user, ...assistants])
|
||||||
|
})
|
||||||
|
|
||||||
|
test("backfills the parent of fetched assistants when another user is cached", async () => {
|
||||||
|
const unrelated = userMessage("message-0", { time: { created: 0 } })
|
||||||
|
const user = userMessage("message-1")
|
||||||
|
const assistants = [assistantMessage("message-2", user.id), assistantMessage("message-3", user.id)]
|
||||||
|
const client = rootMessageClient(
|
||||||
|
[
|
||||||
|
response([{ info: unrelated, parts: [] }]),
|
||||||
|
response(
|
||||||
|
assistants.map((info) => ({ info, parts: [] })),
|
||||||
|
"older",
|
||||||
|
),
|
||||||
|
],
|
||||||
|
[singleResponse(user)],
|
||||||
|
)
|
||||||
|
const store = createServerSession(client)
|
||||||
|
await store.sync("child")
|
||||||
|
|
||||||
|
await store.sync("child", { force: true })
|
||||||
|
|
||||||
|
expect(client.requests).toHaveLength(2)
|
||||||
|
expect(client.rootRequests).toHaveLength(1)
|
||||||
|
expect(store.data.message.child).toEqual([unrelated, user, ...assistants])
|
||||||
|
})
|
||||||
|
|
||||||
|
test("preserves cached history between an injected parent and the page boundary", async () => {
|
||||||
|
const user = userMessage("message-1")
|
||||||
|
const cached = userMessage("message-3", { time: { created: 3 } })
|
||||||
|
const assistant = assistantMessage("message-4", user.id)
|
||||||
|
const client = rootMessageClient(
|
||||||
|
[response([{ info: cached, parts: [] }]), response([{ info: assistant, parts: [] }], "older")],
|
||||||
|
[singleResponse(user)],
|
||||||
|
)
|
||||||
|
const store = createServerSession(client)
|
||||||
|
await store.sync("child")
|
||||||
|
|
||||||
|
await store.sync("child", { force: true })
|
||||||
|
|
||||||
|
expect(store.data.message.child).toEqual([user, cached, assistant])
|
||||||
|
})
|
||||||
|
|
||||||
|
test("refreshes a cached parent omitted by an assistant-only replacement page", async () => {
|
||||||
|
const stale = userMessage("message-1", { summary: { title: "stale", diffs: [] } })
|
||||||
|
const fresh = { ...stale, summary: { title: "fresh", diffs: [] } }
|
||||||
|
const stalePart = textPart(stale.id, { text: "stale" })
|
||||||
|
const freshPart = { ...stalePart, text: "fresh" }
|
||||||
|
const assistant = assistantMessage("message-2", stale.id)
|
||||||
|
const client = rootMessageClient(
|
||||||
|
[response([{ info: stale, parts: [stalePart] }]), response([{ info: assistant, parts: [] }], "older")],
|
||||||
|
[singleResponse(fresh, [freshPart])],
|
||||||
|
)
|
||||||
|
const store = createServerSession(client)
|
||||||
|
await store.sync("child")
|
||||||
|
|
||||||
|
await store.sync("child", { force: true })
|
||||||
|
|
||||||
|
expect(client.rootRequests).toEqual([{ sessionID: "child", messageID: stale.id }])
|
||||||
|
expect(store.data.message.child).toEqual([fresh, assistant])
|
||||||
|
expect(store.data.part[stale.id]).toEqual([freshPart])
|
||||||
|
})
|
||||||
|
|
||||||
|
test("refreshes a confirmed optimistic parent while preserving pending parts", async () => {
|
||||||
|
const stale = userMessage("message-1", { summary: { title: "stale", diffs: [] } })
|
||||||
|
const fresh = { ...stale, summary: { title: "fresh", diffs: [] } }
|
||||||
|
const confirmed = textPart(stale.id, { id: "confirmed", text: "stale" })
|
||||||
|
const refreshed = { ...confirmed, text: "fresh" }
|
||||||
|
const pending = textPart(stale.id, { id: "pending", text: "pending" })
|
||||||
|
const assistant = assistantMessage("message-2", stale.id)
|
||||||
|
const client = rootMessageClient(
|
||||||
|
[response([{ info: stale, parts: [confirmed] }]), response([{ info: assistant, parts: [] }], "older")],
|
||||||
|
[singleResponse(fresh, [refreshed])],
|
||||||
|
)
|
||||||
|
const store = createServerSession(client)
|
||||||
|
store.optimistic.add({ sessionID: "child", message: stale, parts: [confirmed, pending] })
|
||||||
|
await store.sync("child")
|
||||||
|
|
||||||
|
await store.sync("child", { force: true })
|
||||||
|
|
||||||
|
expect(client.rootRequests).toEqual([{ sessionID: "child", messageID: stale.id }])
|
||||||
|
expect(store.data.message.child).toEqual([fresh, assistant])
|
||||||
|
expect(store.data.part[stale.id]).toEqual([refreshed, pending])
|
||||||
|
})
|
||||||
|
|
||||||
|
test("uses a parent received by SSE during the replacement load", async () => {
|
||||||
|
const pending = deferredResponse()
|
||||||
|
const user = userMessage("message-1")
|
||||||
|
const assistant = assistantMessage("message-2", user.id)
|
||||||
|
const client = rootMessageClient([pending.promise], [])
|
||||||
|
const store = createServerSession(client)
|
||||||
|
const loading = store.sync("child")
|
||||||
|
|
||||||
|
store.apply({ type: "message.updated", properties: { info: user } })
|
||||||
|
pending.resolve(response([{ info: assistant, parts: [] }], "older"))
|
||||||
|
await loading
|
||||||
|
|
||||||
|
expect(client.rootRequests).toEqual([])
|
||||||
|
expect(store.data.message.child).toEqual([user, assistant])
|
||||||
|
})
|
||||||
|
|
||||||
|
test("uses a successful retry over events received by a failed backfill attempt", async () => {
|
||||||
|
const failed = deferredResponse()
|
||||||
|
const user = userMessage("message-1")
|
||||||
|
const live = { ...user, agent: "stale" }
|
||||||
|
const assistants = [assistantMessage("message-2", user.id), assistantMessage("message-3", user.id)]
|
||||||
|
const client = rootMessageClient(
|
||||||
|
[
|
||||||
|
response(
|
||||||
|
assistants.map((info) => ({ info, parts: [] })),
|
||||||
|
"older",
|
||||||
|
),
|
||||||
|
],
|
||||||
|
[failed.promise.then((result) => ({ data: result.data[0]! })), singleResponse(user)],
|
||||||
|
)
|
||||||
|
const store = createServerSession(client, { retry: retryImmediately })
|
||||||
|
const loading = store.sync("child")
|
||||||
|
await client.rootRequested(1)
|
||||||
|
|
||||||
|
store.apply({ type: "message.updated", properties: { info: live } })
|
||||||
|
failed.reject(new Error("retry"))
|
||||||
|
await loading
|
||||||
|
|
||||||
|
expect(client.requests).toHaveLength(1)
|
||||||
|
expect(client.rootRequests).toHaveLength(2)
|
||||||
|
expect(store.data.message.child).toEqual([user, ...assistants])
|
||||||
|
})
|
||||||
|
|
||||||
|
test("preserves newer-page events across a failed parent retry", async () => {
|
||||||
|
const failed = deferredResponse()
|
||||||
|
const user = userMessage("message-1")
|
||||||
|
const assistant = assistantMessage("message-2", user.id)
|
||||||
|
const live = { ...assistant, cost: 1 }
|
||||||
|
const client = rootMessageClient(
|
||||||
|
[response([{ info: assistant, parts: [] }], "older")],
|
||||||
|
[failed.promise.then((result) => ({ data: result.data[0]! })), singleResponse(user)],
|
||||||
|
)
|
||||||
|
const store = createServerSession(client, { retry: retryImmediately })
|
||||||
|
const loading = store.sync("child")
|
||||||
|
await client.rootRequested(1)
|
||||||
|
|
||||||
|
store.apply({ type: "message.updated", properties: { info: live } })
|
||||||
|
failed.reject(new Error("retry"))
|
||||||
|
await loading
|
||||||
|
|
||||||
|
expect(store.data.message.child).toEqual([user, live])
|
||||||
|
})
|
||||||
|
|
||||||
|
test("preserves unrelated message events across a failed parent retry", async () => {
|
||||||
|
const failed = deferredResponse()
|
||||||
|
const user = userMessage("message-1")
|
||||||
|
const assistant = assistantMessage("message-2", user.id)
|
||||||
|
const live = userMessage("message-4", { time: { created: 4 } })
|
||||||
|
const client = rootMessageClient(
|
||||||
|
[response([{ info: assistant, parts: [] }], "older")],
|
||||||
|
[failed.promise.then((result) => ({ data: result.data[0]! })), singleResponse(user)],
|
||||||
|
)
|
||||||
|
const store = createServerSession(client, { retry: retryImmediately })
|
||||||
|
const loading = store.sync("child")
|
||||||
|
await client.rootRequested(1)
|
||||||
|
|
||||||
|
store.apply({ type: "message.updated", properties: { info: live } })
|
||||||
|
failed.reject(new Error("retry"))
|
||||||
|
await loading
|
||||||
|
|
||||||
|
expect(store.data.message.child).toEqual([user, assistant, live])
|
||||||
|
})
|
||||||
|
|
||||||
|
test("preserves newer-page part events across a failed parent retry", async () => {
|
||||||
|
const failed = deferredResponse()
|
||||||
|
const user = userMessage("message-1")
|
||||||
|
const assistant = assistantMessage("message-2", user.id)
|
||||||
|
const stale = textPart(assistant.id, { text: "stale" })
|
||||||
|
const live = { ...stale, text: "live" }
|
||||||
|
const client = rootMessageClient(
|
||||||
|
[response([{ info: assistant, parts: [stale] }], "older")],
|
||||||
|
[failed.promise.then((result) => ({ data: result.data[0]! })), singleResponse(user)],
|
||||||
|
)
|
||||||
|
const store = createServerSession(client, { retry: retryImmediately })
|
||||||
|
const loading = store.sync("child")
|
||||||
|
await client.rootRequested(1)
|
||||||
|
|
||||||
|
store.apply({ type: "message.part.updated", properties: { sessionID: "child", part: live, time: 2 } })
|
||||||
|
failed.reject(new Error("retry"))
|
||||||
|
await loading
|
||||||
|
|
||||||
|
expect(store.data.part[assistant.id]).toEqual([live])
|
||||||
|
})
|
||||||
|
|
||||||
test("merges live events into the initial page", async () => {
|
test("merges live events into the initial page", async () => {
|
||||||
const pending = deferredResponse()
|
const pending = deferredResponse()
|
||||||
const user = userMessage("message-1")
|
const user = userMessage("message-1")
|
||||||
@@ -905,6 +1191,26 @@ describe("server session", () => {
|
|||||||
expect(store.data.message.child).toEqual([latest])
|
expect(store.data.message.child).toEqual([latest])
|
||||||
})
|
})
|
||||||
|
|
||||||
|
test("does not scan cached messages for user roots during history prepend", async () => {
|
||||||
|
const guard = { active: false }
|
||||||
|
const latest = new Proxy(userMessage("message-2", { time: { created: 2 } }), {
|
||||||
|
get(target, property, receiver) {
|
||||||
|
if (guard.active && property === "role") throw new Error("cached role accessed")
|
||||||
|
return Reflect.get(target, property, receiver)
|
||||||
|
},
|
||||||
|
})
|
||||||
|
const older = userMessage("message-1")
|
||||||
|
const store = createServerSession(
|
||||||
|
messageClient(response([{ info: latest, parts: [] }], "older"), response([{ info: older, parts: [] }])),
|
||||||
|
)
|
||||||
|
await store.sync("child")
|
||||||
|
guard.active = true
|
||||||
|
|
||||||
|
await store.history.loadMore("child")
|
||||||
|
|
||||||
|
expect(store.data.message.child).toEqual([older, latest])
|
||||||
|
})
|
||||||
|
|
||||||
test("preserves loaded history during an incomplete refresh", async () => {
|
test("preserves loaded history during an incomplete refresh", async () => {
|
||||||
const older = userMessage("message-1")
|
const older = userMessage("message-1")
|
||||||
const latest = userMessage("message-2", { time: { created: 2 } })
|
const latest = userMessage("message-2", { time: { created: 2 } })
|
||||||
|
|||||||
@@ -54,6 +54,11 @@ type MessageLoadState = {
|
|||||||
clearedMessageParts: Set<string>
|
clearedMessageParts: Set<string>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type MessageLoadBaseline = Pick<
|
||||||
|
MessageLoadState,
|
||||||
|
"touchedMessages" | "retainedMessages" | "touchedParts" | "clearedMessageParts"
|
||||||
|
>
|
||||||
|
|
||||||
function mergeOptimisticPage(page: MessagePage, items: OptimisticItem[]) {
|
function mergeOptimisticPage(page: MessagePage, items: OptimisticItem[]) {
|
||||||
if (items.length === 0) return { ...page, observed: [] as { messageID: string; parts: Part[] }[] }
|
if (items.length === 0) return { ...page, observed: [] as { messageID: string; parts: Part[] }[] }
|
||||||
const session = [...page.session]
|
const session = [...page.session]
|
||||||
@@ -347,7 +352,7 @@ export function createServerSession(client: OpencodeClient, options?: { retry?:
|
|||||||
load.touchedParts.set(messageID, new Set([partID]))
|
load.touchedParts.set(messageID, new Set([partID]))
|
||||||
}
|
}
|
||||||
|
|
||||||
const resetMessageLoad = (sessionID: string, load: MessageLoadState) => {
|
const resetMessageLoad = (sessionID: string, load: MessageLoadState, baseline?: MessageLoadBaseline) => {
|
||||||
load.touchedMessages.clear()
|
load.touchedMessages.clear()
|
||||||
load.retainedMessages.clear()
|
load.retainedMessages.clear()
|
||||||
load.touchedParts.clear()
|
load.touchedParts.clear()
|
||||||
@@ -380,8 +385,27 @@ export function createServerSession(client: OpencodeClient, options?: { retry?:
|
|||||||
parts.forEach((partID) => touched.add(partID))
|
parts.forEach((partID) => touched.add(partID))
|
||||||
load.touchedParts.set(messageID, touched)
|
load.touchedParts.set(messageID, touched)
|
||||||
}
|
}
|
||||||
|
baseline?.touchedMessages.forEach((messageID) => load.touchedMessages.add(messageID))
|
||||||
|
baseline?.retainedMessages.forEach((messageID) => load.retainedMessages.add(messageID))
|
||||||
|
baseline?.clearedMessageParts.forEach((messageID) => load.clearedMessageParts.add(messageID))
|
||||||
|
baseline?.touchedParts.forEach((parts, messageID) => {
|
||||||
|
const touched = load.touchedParts.get(messageID) ?? new Set<string>()
|
||||||
|
parts.forEach((partID) => touched.add(partID))
|
||||||
|
load.touchedParts.set(messageID, touched)
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const messageLoadBaseline = (load: MessageLoadState, exclude: string): MessageLoadBaseline => ({
|
||||||
|
touchedMessages: new Set([...load.touchedMessages].filter((messageID) => messageID !== exclude)),
|
||||||
|
retainedMessages: new Set([...load.retainedMessages].filter((messageID) => messageID !== exclude)),
|
||||||
|
touchedParts: new Map(
|
||||||
|
[...load.touchedParts]
|
||||||
|
.filter(([messageID]) => messageID !== exclude)
|
||||||
|
.map(([messageID, parts]) => [messageID, new Set(parts)]),
|
||||||
|
),
|
||||||
|
clearedMessageParts: new Set([...load.clearedMessageParts].filter((messageID) => messageID !== exclude)),
|
||||||
|
})
|
||||||
|
|
||||||
const evict = (sessionIDs: string[]) => {
|
const evict = (sessionIDs: string[]) => {
|
||||||
if (sessionIDs.length === 0) return
|
if (sessionIDs.length === 0) return
|
||||||
const evicted = new Set(sessionIDs)
|
const evicted = new Set(sessionIDs)
|
||||||
@@ -460,6 +484,18 @@ export function createServerSession(client: OpencodeClient, options?: { retry?:
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const fetchMessage = async (sessionID: string, messageID: string, onAttempt?: () => void) => {
|
||||||
|
const response = await (options?.retry ?? retry)(() => {
|
||||||
|
onAttempt?.()
|
||||||
|
return client.session.message({ sessionID, messageID })
|
||||||
|
})
|
||||||
|
if (!response.data?.info?.id) throw new Error(`Message not found: ${messageID}`)
|
||||||
|
return {
|
||||||
|
message: cleanMessage(response.data.info),
|
||||||
|
parts: response.data.parts.filter((part) => !!part?.id).sort((a, b) => cmp(a.id, b.id)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const replaceMessages = (sessionID: string, messages: Message[]) => {
|
const replaceMessages = (sessionID: string, messages: Message[]) => {
|
||||||
const messageIDs = new Set(messages.map((message) => message.id))
|
const messageIDs = new Set(messages.map((message) => message.id))
|
||||||
const dropped = (data.message[sessionID] ?? []).filter((message) => !messageIDs.has(message.id))
|
const dropped = (data.message[sessionID] ?? []).filter((message) => !messageIDs.has(message.id))
|
||||||
@@ -578,36 +614,79 @@ export function createServerSession(client: OpencodeClient, options?: { retry?:
|
|||||||
messageLoads.set(sessionID, load)
|
messageLoads.set(sessionID, load)
|
||||||
setMeta("loading", sessionID, true)
|
setMeta("loading", sessionID, true)
|
||||||
let applied = false
|
let applied = false
|
||||||
await fetchMessages(sessionID, limit, before, () => resetMessageLoad(sessionID, load))
|
try {
|
||||||
.then((page) => {
|
const page = await fetchMessages(sessionID, limit, before, () => resetMessageLoad(sessionID, load))
|
||||||
if (generations.get(sessionID) !== active) return
|
const first = page.session.reduce<Message | undefined>(
|
||||||
const first = page.session.reduce<Message | undefined>(
|
(oldest, message) => (!oldest || cmpMessage(message, oldest) < 0 ? message : oldest),
|
||||||
(oldest, message) => (!oldest || cmpMessage(message, oldest) < 0 ? message : oldest),
|
undefined,
|
||||||
undefined,
|
)
|
||||||
)
|
if (generations.get(sessionID) !== active) return
|
||||||
const preserveUnfetched =
|
|
||||||
mode === "prepend" || (!page.complete && (!first || ((message: Message) => cmpMessage(message, first) < 0)))
|
const parents = [] as Awaited<ReturnType<typeof fetchMessage>>[]
|
||||||
applyMessagePage(
|
if (mode !== "prepend") {
|
||||||
sessionID,
|
const users = new Set([
|
||||||
page,
|
...page.session.filter((message) => message.role === "user").map((message) => message.id),
|
||||||
messageLoads.get(sessionID) === load ? load : undefined,
|
...(data.message[sessionID] ?? [])
|
||||||
preserveUnfetched,
|
.filter((message) => {
|
||||||
mode !== "prepend",
|
if (message.role !== "user") return false
|
||||||
)
|
const item = optimistic.get(sessionID)?.get(message.id)
|
||||||
applied = true
|
return load.touchedMessages.has(message.id) && (!item || item.confirmedMessage === true)
|
||||||
})
|
})
|
||||||
.finally(() => {
|
.map((message) => message.id),
|
||||||
if (!applied && generations.get(sessionID) === active && messageLoads.get(sessionID) === load) {
|
])
|
||||||
for (const messageID of load.orphanParents) {
|
const parentIDs = [
|
||||||
if (!orphanParts.get(sessionID)?.has(messageID)) continue
|
...new Set(
|
||||||
setData(produce((draft) => deleteMessageParts(draft, messageID)))
|
page.session.flatMap((message) =>
|
||||||
orphanParts.get(sessionID)?.delete(messageID)
|
message.role === "assistant" && !users.has(message.parentID) ? [message.parentID] : [],
|
||||||
}
|
),
|
||||||
if (orphanParts.get(sessionID)?.size === 0) orphanParts.delete(sessionID)
|
),
|
||||||
|
]
|
||||||
|
for (const parentID of parentIDs) {
|
||||||
|
if (generations.get(sessionID) !== active) break
|
||||||
|
const parent = await fetchMessage(sessionID, parentID, () =>
|
||||||
|
resetMessageLoad(sessionID, load, messageLoadBaseline(load, parentID)),
|
||||||
|
)
|
||||||
|
if (parent.message.role !== "user") throw new Error(`Assistant parent is not a user message: ${parentID}`)
|
||||||
|
parents.push(parent)
|
||||||
}
|
}
|
||||||
if (messageLoads.get(sessionID) === load) messageLoads.delete(sessionID)
|
}
|
||||||
if (generations.get(sessionID) === active) setMeta("loading", sessionID, false)
|
if (generations.get(sessionID) !== active) return
|
||||||
})
|
const result =
|
||||||
|
mode === "prepend"
|
||||||
|
? page
|
||||||
|
: {
|
||||||
|
...page,
|
||||||
|
session: merge(
|
||||||
|
page.session,
|
||||||
|
parents.map((parent) => parent.message),
|
||||||
|
),
|
||||||
|
part: merge(
|
||||||
|
page.part,
|
||||||
|
parents.map((parent) => ({ id: parent.message.id, part: parent.parts })),
|
||||||
|
),
|
||||||
|
}
|
||||||
|
const preserveUnfetched =
|
||||||
|
mode === "prepend" || (!result.complete && (!first || ((message: Message) => cmpMessage(message, first) < 0)))
|
||||||
|
applyMessagePage(
|
||||||
|
sessionID,
|
||||||
|
result,
|
||||||
|
messageLoads.get(sessionID) === load ? load : undefined,
|
||||||
|
preserveUnfetched,
|
||||||
|
mode !== "prepend",
|
||||||
|
)
|
||||||
|
applied = true
|
||||||
|
} finally {
|
||||||
|
if (!applied && generations.get(sessionID) === active && messageLoads.get(sessionID) === load) {
|
||||||
|
for (const messageID of load.orphanParents) {
|
||||||
|
if (!orphanParts.get(sessionID)?.has(messageID)) continue
|
||||||
|
setData(produce((draft) => deleteMessageParts(draft, messageID)))
|
||||||
|
orphanParts.get(sessionID)?.delete(messageID)
|
||||||
|
}
|
||||||
|
if (orphanParts.get(sessionID)?.size === 0) orphanParts.delete(sessionID)
|
||||||
|
}
|
||||||
|
if (messageLoads.get(sessionID) === load) messageLoads.delete(sessionID)
|
||||||
|
if (generations.get(sessionID) === active) setMeta("loading", sessionID, false)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const sync = (sessionID: string, options?: { force?: boolean; messageLimit?: number }) => {
|
const sync = (sessionID: string, options?: { force?: boolean; messageLimit?: number }) => {
|
||||||
|
|||||||
@@ -312,7 +312,10 @@ export const { use: useServer, provider: ServerProvider } = createSimpleContext(
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
const isReady = createMemo(() => ready() && !!state.active)
|
const isReady = Object.assign(
|
||||||
|
createMemo(() => ready() && !!state.active),
|
||||||
|
{ promise: ready.promise },
|
||||||
|
)
|
||||||
|
|
||||||
const scope = (key = state.active) => ServerScope.fromServerKey(key, props.canonicalLocalServer)
|
const scope = (key = state.active) => ServerScope.fromServerKey(key, props.canonicalLocalServer)
|
||||||
const projects = createServerProjects({ scope, store, setStore })
|
const projects = createServerProjects({ scope, store, setStore })
|
||||||
|
|||||||
@@ -1,6 +1,15 @@
|
|||||||
import { describe, expect, test } from "bun:test"
|
import { describe, expect, test } from "bun:test"
|
||||||
import { createRoot, getOwner, onCleanup } from "solid-js"
|
import { createRoot, getOwner, onCleanup } from "solid-js"
|
||||||
import { createTabMemory } from "./tab-memory"
|
import { createTabMemory } from "./tab-memory"
|
||||||
|
import { nextTabAfterClose, pushClosedTab, removeClosedTabs, takeClosedTab, type ClosedTab } from "./closed-tabs"
|
||||||
|
import type { SessionTab, Tab } from "./tabs"
|
||||||
|
import type { ServerConnection } from "./server"
|
||||||
|
|
||||||
|
const server = "local\nhttp://localhost:4096" as ServerConnection.Key
|
||||||
|
|
||||||
|
function sessionTab(sessionId: string): SessionTab {
|
||||||
|
return { type: "session", server, sessionId }
|
||||||
|
}
|
||||||
|
|
||||||
describe("tab memory", () => {
|
describe("tab memory", () => {
|
||||||
test("keeps state until its tab is removed", () => {
|
test("keeps state until its tab is removed", () => {
|
||||||
@@ -22,3 +31,75 @@ describe("tab memory", () => {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
describe("closed tab stack", () => {
|
||||||
|
test("records session tabs with their index", () => {
|
||||||
|
const stack = pushClosedTab([], sessionTab("a"), 2)
|
||||||
|
|
||||||
|
expect(stack).toEqual([{ tab: sessionTab("a"), index: 2 }])
|
||||||
|
})
|
||||||
|
|
||||||
|
test("ignores draft tabs", () => {
|
||||||
|
const draft: Tab = { type: "draft", draftID: "d1", server, directory: "/tmp" }
|
||||||
|
|
||||||
|
expect(pushClosedTab([], draft, 0)).toEqual([])
|
||||||
|
})
|
||||||
|
|
||||||
|
test("caps the stack size", () => {
|
||||||
|
const stack = Array.from({ length: 30 }, (_, i) => i).reduce<ClosedTab[]>(
|
||||||
|
(acc, i) => pushClosedTab(acc, sessionTab(`s${i}`), i),
|
||||||
|
[],
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(stack).toHaveLength(25)
|
||||||
|
expect(stack[0]?.tab.sessionId).toBe("s5")
|
||||||
|
expect(stack.at(-1)?.tab.sessionId).toBe("s29")
|
||||||
|
})
|
||||||
|
|
||||||
|
test("pops the most recently closed tab", () => {
|
||||||
|
const stack = [
|
||||||
|
{ tab: sessionTab("a"), index: 0 },
|
||||||
|
{ tab: sessionTab("b"), index: 1 },
|
||||||
|
]
|
||||||
|
const result = takeClosedTab(stack, [])
|
||||||
|
|
||||||
|
expect(result.entry?.tab.sessionId).toBe("b")
|
||||||
|
expect(result.stack).toEqual([{ tab: sessionTab("a"), index: 0 }])
|
||||||
|
})
|
||||||
|
|
||||||
|
test("skips entries whose tab is already open", () => {
|
||||||
|
const stack = [
|
||||||
|
{ tab: sessionTab("a"), index: 0 },
|
||||||
|
{ tab: sessionTab("b"), index: 1 },
|
||||||
|
]
|
||||||
|
const result = takeClosedTab(stack, [sessionTab("b")])
|
||||||
|
|
||||||
|
expect(result.entry?.tab.sessionId).toBe("a")
|
||||||
|
expect(result.stack).toEqual([])
|
||||||
|
})
|
||||||
|
|
||||||
|
test("returns no entry when everything is open or empty", () => {
|
||||||
|
expect(takeClosedTab([], []).entry).toBeUndefined()
|
||||||
|
|
||||||
|
const result = takeClosedTab([{ tab: sessionTab("a"), index: 0 }], [sessionTab("a")])
|
||||||
|
expect(result.entry).toBeUndefined()
|
||||||
|
expect(result.stack).toEqual([])
|
||||||
|
})
|
||||||
|
|
||||||
|
test("purges removed sessions", () => {
|
||||||
|
const stack = [
|
||||||
|
{ tab: sessionTab("a"), index: 0 },
|
||||||
|
{ tab: sessionTab("b"), index: 1 },
|
||||||
|
]
|
||||||
|
|
||||||
|
expect(removeClosedTabs(stack, server, ["a"])).toEqual([{ tab: sessionTab("b"), index: 1 }])
|
||||||
|
})
|
||||||
|
|
||||||
|
test("does not navigate when a background tab closes", () => {
|
||||||
|
const tabs = [sessionTab("a"), sessionTab("b"), sessionTab("c")]
|
||||||
|
|
||||||
|
expect(nextTabAfterClose(tabs, 1, false)).toBeUndefined()
|
||||||
|
expect(nextTabAfterClose(tabs, 1, true)).toEqual(sessionTab("c"))
|
||||||
|
expect(nextTabAfterClose([sessionTab("a")], 0, true)).toBeNull()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import { uuid } from "@/utils/uuid"
|
|||||||
import { SessionTabsRemovedDetail } from "@/components/titlebar-session-events"
|
import { SessionTabsRemovedDetail } from "@/components/titlebar-session-events"
|
||||||
import { sessionHref } from "@/utils/session-route"
|
import { sessionHref } from "@/utils/session-route"
|
||||||
import { createTabMemory } from "./tab-memory"
|
import { createTabMemory } from "./tab-memory"
|
||||||
|
import { nextTabAfterClose, pushClosedTab, removeClosedTabs, takeClosedTab, type ClosedTab } from "./closed-tabs"
|
||||||
|
|
||||||
export type SessionTab = {
|
export type SessionTab = {
|
||||||
type: "session"
|
type: "session"
|
||||||
@@ -27,6 +28,11 @@ export type DraftTab = {
|
|||||||
|
|
||||||
export type Tab = SessionTab | DraftTab
|
export type Tab = SessionTab | DraftTab
|
||||||
|
|
||||||
|
export type TabInfo = {
|
||||||
|
title?: string
|
||||||
|
directory?: string
|
||||||
|
}
|
||||||
|
|
||||||
type RecentTab = {
|
type RecentTab = {
|
||||||
key?: string
|
key?: string
|
||||||
}
|
}
|
||||||
@@ -63,6 +69,8 @@ export const { use: useTabs, provider: TabsProvider } = createSimpleContext({
|
|||||||
createStore<Tab[]>([]),
|
createStore<Tab[]>([]),
|
||||||
)
|
)
|
||||||
const [recent, setRecent, , recentReady] = persisted(Persist.window("tabs.recent"), createStore<RecentTab>({}))
|
const [recent, setRecent, , recentReady] = persisted(Persist.window("tabs.recent"), createStore<RecentTab>({}))
|
||||||
|
const [info, setInfo] = persisted(Persist.window("tabs.info"), createStore<Record<string, TabInfo>>({}))
|
||||||
|
const [closed, setClosed, , closedReady] = persisted(Persist.window("tabs.closed"), createStore<ClosedTab[]>([]))
|
||||||
|
|
||||||
const params = useParams()
|
const params = useParams()
|
||||||
const navigate = useNavigate()
|
const navigate = useNavigate()
|
||||||
@@ -87,10 +95,28 @@ export const { use: useTabs, provider: TabsProvider } = createSimpleContext({
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const updateClosed = (update: (stack: ClosedTab[]) => ClosedTab[]) => {
|
||||||
|
const apply = () => setClosed((stack) => update(stack))
|
||||||
|
if (closedReady()) {
|
||||||
|
apply()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
void closedReady.promise?.then(apply)
|
||||||
|
}
|
||||||
|
|
||||||
const removeDraftPersisted = (draftID: string) => {
|
const removeDraftPersisted = (draftID: string) => {
|
||||||
for (const key of draftPersistedKeys()) removePersisted(Persist.draft(draftID, key), platform)
|
for (const key of draftPersistedKeys()) removePersisted(Persist.draft(draftID, key), platform)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const removeInfo = (key: string) => {
|
||||||
|
if (!info[key]) return
|
||||||
|
setInfo(
|
||||||
|
produce((draft) => {
|
||||||
|
delete draft[key]
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
onCleanup(memory.dispose)
|
onCleanup(memory.dispose)
|
||||||
|
|
||||||
createEffect(() => {
|
createEffect(() => {
|
||||||
@@ -99,11 +125,26 @@ export const { use: useTabs, provider: TabsProvider } = createSimpleContext({
|
|||||||
const next = store.filter((tab) => servers.has(tab.server))
|
const next = store.filter((tab) => servers.has(tab.server))
|
||||||
if (next.length !== store.length) {
|
if (next.length !== store.length) {
|
||||||
for (const tab of store) {
|
for (const tab of store) {
|
||||||
if (!servers.has(tab.server)) memory.remove(tabKey(tab))
|
if (!servers.has(tab.server)) {
|
||||||
|
const key = tabKey(tab)
|
||||||
|
memory.remove(key)
|
||||||
|
removeInfo(key)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
setStore(() => next)
|
setStore(() => next)
|
||||||
}
|
}
|
||||||
if (recent.key && !next.some((tab) => tabKey(tab) === recent.key)) setRecentKey(undefined)
|
if (recent.key && !next.some((tab) => tabKey(tab) === recent.key)) setRecentKey(undefined)
|
||||||
|
const keys = new Set(next.map(tabKey))
|
||||||
|
for (const key of Object.keys(info)) {
|
||||||
|
if (!keys.has(key)) removeInfo(key)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
createEffect(() => {
|
||||||
|
if (!closedReady()) return
|
||||||
|
const servers = new Set(server.list.map(ServerConnection.key))
|
||||||
|
const next = closed.filter((entry) => servers.has(entry.tab.server))
|
||||||
|
if (next.length !== closed.length) setClosed(() => next)
|
||||||
})
|
})
|
||||||
|
|
||||||
const navigateTab = (tab: Tab) => {
|
const navigateTab = (tab: Tab) => {
|
||||||
@@ -117,7 +158,7 @@ export const { use: useTabs, provider: TabsProvider } = createSimpleContext({
|
|||||||
if (!tab) return
|
if (!tab) return
|
||||||
const key = tabKey(tab)
|
const key = tabKey(tab)
|
||||||
const draftID = tab.type === "draft" ? tab.draftID : undefined
|
const draftID = tab.type === "draft" ? tab.draftID : undefined
|
||||||
const nextTab = store[index + 1] ?? store[index - 1]
|
const nextTab = nextTabAfterClose(store, index, recentKey() === key && location.pathname !== "/")
|
||||||
closing.add(key)
|
closing.add(key)
|
||||||
void startTransition(() => {
|
void startTransition(() => {
|
||||||
setStore(
|
setStore(
|
||||||
@@ -125,11 +166,14 @@ export const { use: useTabs, provider: TabsProvider } = createSimpleContext({
|
|||||||
tabs.splice(index, 1)
|
tabs.splice(index, 1)
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
if (recent.key === key) setRecentKey(nextTab && tabKey(nextTab))
|
if (nextTab === null) {
|
||||||
|
setRecentKey(undefined)
|
||||||
|
navigate("/")
|
||||||
|
}
|
||||||
if (nextTab) navigateTab(nextTab)
|
if (nextTab) navigateTab(nextTab)
|
||||||
else navigate("/")
|
|
||||||
}).finally(() => closing.delete(key))
|
}).finally(() => closing.delete(key))
|
||||||
memory.remove(key)
|
memory.remove(key)
|
||||||
|
removeInfo(key)
|
||||||
if (draftID) removeDraftPersisted(draftID)
|
if (draftID) removeDraftPersisted(draftID)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -201,23 +245,57 @@ export const { use: useTabs, provider: TabsProvider } = createSimpleContext({
|
|||||||
removeDraftPersisted(draftID)
|
removeDraftPersisted(draftID)
|
||||||
},
|
},
|
||||||
removeTab,
|
removeTab,
|
||||||
|
// User-initiated close: records the tab so it can be reopened.
|
||||||
|
// Cleanup paths (missing sessions, archive, server removal) go through
|
||||||
|
// removeTab and friends directly and are not recorded.
|
||||||
|
closeTab(index: number) {
|
||||||
|
const tab = store[index]
|
||||||
|
if (!tab) return
|
||||||
|
if (tab.type === "session") updateClosed((stack) => pushClosedTab(stack, tab, index))
|
||||||
|
removeTab(index)
|
||||||
|
},
|
||||||
|
reopenClosedTab() {
|
||||||
|
if (!closedReady()) {
|
||||||
|
void closedReady.promise?.then(() => actions.reopenClosedTab())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const result = takeClosedTab(closed, store)
|
||||||
|
if (result.stack.length === closed.length) return
|
||||||
|
setClosed(() => result.stack)
|
||||||
|
const entry = result.entry
|
||||||
|
if (!entry) return
|
||||||
|
const index = Math.min(entry.index, store.length)
|
||||||
|
void startTransition(() => {
|
||||||
|
setStore(
|
||||||
|
produce((tabs) => {
|
||||||
|
if (tabs.some((item) => tabKey(item) === tabKey(entry.tab))) return
|
||||||
|
tabs.splice(index, 0, entry.tab)
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
navigateTab(entry.tab)
|
||||||
|
})
|
||||||
|
},
|
||||||
removeSessionTab(input: Omit<SessionTab, "type">) {
|
removeSessionTab(input: Omit<SessionTab, "type">) {
|
||||||
|
updateClosed((stack) => removeClosedTabs(stack, input.server, [input.sessionId]))
|
||||||
const index = store.findIndex(
|
const index = store.findIndex(
|
||||||
(tab) => tab.type === "session" && tab.server === input.server && tab.sessionId === input.sessionId,
|
(tab) => tab.type === "session" && tab.server === input.server && tab.sessionId === input.sessionId,
|
||||||
)
|
)
|
||||||
if (index !== -1) removeTab(index)
|
if (index !== -1) removeTab(index)
|
||||||
},
|
},
|
||||||
removeServer(key: ServerConnection.Key) {
|
removeServer(key: ServerConnection.Key) {
|
||||||
|
updateClosed((stack) => stack.filter((entry) => entry.tab.server !== key))
|
||||||
const drafts = store.flatMap((tab) => (tab.type === "draft" && tab.server === key ? [tab.draftID] : []))
|
const drafts = store.flatMap((tab) => (tab.type === "draft" && tab.server === key ? [tab.draftID] : []))
|
||||||
const removed = store.filter((tab) => tab.server === key).map(tabKey)
|
const removed = store.filter((tab) => tab.server === key).map(tabKey)
|
||||||
setStore((tabs) => tabs.filter((tab) => tab.server !== key))
|
setStore((tabs) => tabs.filter((tab) => tab.server !== key))
|
||||||
for (const key of removed) memory.remove(key)
|
for (const key of removed) memory.remove(key)
|
||||||
|
for (const key of removed) removeInfo(key)
|
||||||
if (recent.key && removed.includes(recent.key)) setRecentKey(undefined)
|
if (recent.key && removed.includes(recent.key)) setRecentKey(undefined)
|
||||||
for (const draftID of drafts) removeDraftPersisted(draftID)
|
for (const draftID of drafts) removeDraftPersisted(draftID)
|
||||||
if (server.key === key) navigate("/")
|
if (server.key === key) navigate("/")
|
||||||
},
|
},
|
||||||
removeSessions: (input: SessionTabsRemovedDetail) => {
|
removeSessions: (input: SessionTabsRemovedDetail) => {
|
||||||
const targetServer = input.server ?? server.key
|
const targetServer = input.server ?? server.key
|
||||||
|
updateClosed((stack) => removeClosedTabs(stack, targetServer, input.sessionIDs))
|
||||||
const removed = store
|
const removed = store
|
||||||
.filter(
|
.filter(
|
||||||
(tab) => tab.type === "session" && tab.server === targetServer && input.sessionIDs.includes(tab.sessionId),
|
(tab) => tab.type === "session" && tab.server === targetServer && input.sessionIDs.includes(tab.sessionId),
|
||||||
@@ -265,6 +343,14 @@ export const { use: useTabs, provider: TabsProvider } = createSimpleContext({
|
|||||||
if (recent.key && removed.includes(recent.key)) setRecentKey(undefined)
|
if (recent.key && removed.includes(recent.key)) setRecentKey(undefined)
|
||||||
})
|
})
|
||||||
for (const key of removed) memory.remove(key)
|
for (const key of removed) memory.remove(key)
|
||||||
|
for (const key of removed) removeInfo(key)
|
||||||
|
},
|
||||||
|
rememberSessionInfo(tab: SessionTab, session: Session) {
|
||||||
|
const key = tabKey(tab)
|
||||||
|
const next = { title: session.title, directory: session.directory }
|
||||||
|
const current = info[key]
|
||||||
|
if (current?.title === next.title && current.directory === next.directory) return
|
||||||
|
setInfo(key, next)
|
||||||
},
|
},
|
||||||
select: navigateTab,
|
select: navigateTab,
|
||||||
remember(tab: Tab) {
|
remember(tab: Tab) {
|
||||||
@@ -289,6 +375,6 @@ export const { use: useTabs, provider: TabsProvider } = createSimpleContext({
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
return { ...actions, store, ready, recentReady }
|
return { ...actions, store, info, ready, recentReady }
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|||||||
+148
-2
@@ -21,6 +21,9 @@ export const dict = {
|
|||||||
"theme.scheme.dark": "داكن",
|
"theme.scheme.dark": "داكن",
|
||||||
"command.sidebar.toggle": "تبديل الشريط الجانبي",
|
"command.sidebar.toggle": "تبديل الشريط الجانبي",
|
||||||
"command.project.open": "فتح مشروع",
|
"command.project.open": "فتح مشروع",
|
||||||
|
"command.project.previous": "المشروع السابق",
|
||||||
|
"command.project.next": "المشروع التالي",
|
||||||
|
"command.project.index": "التبديل إلى المشروع {{index}}",
|
||||||
"command.provider.connect": "اتصال بموفر",
|
"command.provider.connect": "اتصال بموفر",
|
||||||
"command.server.switch": "تبديل الخادم",
|
"command.server.switch": "تبديل الخادم",
|
||||||
"command.settings.open": "فتح الإعدادات",
|
"command.settings.open": "فتح الإعدادات",
|
||||||
@@ -39,6 +42,7 @@ export const dict = {
|
|||||||
"command.session.new": "جلسة جديدة",
|
"command.session.new": "جلسة جديدة",
|
||||||
"command.file.open": "فتح ملف",
|
"command.file.open": "فتح ملف",
|
||||||
"command.tab.close": "إغلاق علامة التبويب",
|
"command.tab.close": "إغلاق علامة التبويب",
|
||||||
|
"command.tab.reopenClosed": "إعادة فتح علامة التبويب المغلقة",
|
||||||
"command.context.addSelection": "إضافة التحديد إلى السياق",
|
"command.context.addSelection": "إضافة التحديد إلى السياق",
|
||||||
"command.context.addSelection.description": "إضافة الأسطر المحددة من الملف الحالي",
|
"command.context.addSelection.description": "إضافة الأسطر المحددة من الملف الحالي",
|
||||||
"command.input.focus": "التركيز على حقل الإدخال",
|
"command.input.focus": "التركيز على حقل الإدخال",
|
||||||
@@ -193,6 +197,11 @@ export const dict = {
|
|||||||
"model.input.video": "فيديو",
|
"model.input.video": "فيديو",
|
||||||
"model.input.pdf": "pdf",
|
"model.input.pdf": "pdf",
|
||||||
"model.tooltip.allows": "يسمح: {{inputs}}",
|
"model.tooltip.allows": "يسمح: {{inputs}}",
|
||||||
|
"model.tooltip.context.label": "السياق",
|
||||||
|
"model.tooltip.inputs": "المدخلات",
|
||||||
|
"model.tooltip.model": "النموذج",
|
||||||
|
"model.tooltip.provider": "الموفر",
|
||||||
|
"model.tooltip.reasoning": "الاستنتاج",
|
||||||
"model.tooltip.reasoning.allowed": "يسمح بالاستنتاج",
|
"model.tooltip.reasoning.allowed": "يسمح بالاستنتاج",
|
||||||
"model.tooltip.reasoning.none": "بدون استنتاج",
|
"model.tooltip.reasoning.none": "بدون استنتاج",
|
||||||
"model.tooltip.context": "حد السياق {{limit}}",
|
"model.tooltip.context": "حد السياق {{limit}}",
|
||||||
@@ -202,6 +211,7 @@ export const dict = {
|
|||||||
"common.loading": "جارٍ التحميل",
|
"common.loading": "جارٍ التحميل",
|
||||||
"common.loading.ellipsis": "...",
|
"common.loading.ellipsis": "...",
|
||||||
"common.cancel": "إلغاء",
|
"common.cancel": "إلغاء",
|
||||||
|
"common.clear": "مسح",
|
||||||
"common.connect": "اتصال",
|
"common.connect": "اتصال",
|
||||||
"common.disconnect": "قطع الاتصال",
|
"common.disconnect": "قطع الاتصال",
|
||||||
"common.continue": "إرسال",
|
"common.continue": "إرسال",
|
||||||
@@ -218,6 +228,8 @@ export const dict = {
|
|||||||
"prompt.mode.shell": "Shell",
|
"prompt.mode.shell": "Shell",
|
||||||
"prompt.mode.normal": "Prompt",
|
"prompt.mode.normal": "Prompt",
|
||||||
"prompt.mode.shell.exit": "esc للخروج",
|
"prompt.mode.shell.exit": "esc للخروج",
|
||||||
|
"session.child.promptDisabled": "لا يمكن إرسال موجّهات إلى جلسات الوكلاء الفرعيين.",
|
||||||
|
"session.child.backToParent": "العودة إلى الجلسة الرئيسية.",
|
||||||
"prompt.example.1": "إصلاح TODO في قاعدة التعليمات البرمجية",
|
"prompt.example.1": "إصلاح TODO في قاعدة التعليمات البرمجية",
|
||||||
"prompt.example.2": "ما هو المكدس التقني لهذا المشروع؟",
|
"prompt.example.2": "ما هو المكدس التقني لهذا المشروع؟",
|
||||||
"prompt.example.3": "إصلاح الاختبارات المعطلة",
|
"prompt.example.3": "إصلاح الاختبارات المعطلة",
|
||||||
@@ -314,6 +326,74 @@ export const dict = {
|
|||||||
"dialog.server.menu.delete": "حذف",
|
"dialog.server.menu.delete": "حذف",
|
||||||
"dialog.server.current": "الخادم الحالي",
|
"dialog.server.current": "الخادم الحالي",
|
||||||
"dialog.server.status.default": "افتراضي",
|
"dialog.server.status.default": "افتراضي",
|
||||||
|
"wsl.server.add": "إضافة خادم WSL",
|
||||||
|
"wsl.server.addShort": "إضافة WSL",
|
||||||
|
"wsl.server.label": "WSL",
|
||||||
|
"wsl.server.menu.label": "خادم WSL",
|
||||||
|
"wsl.server.retryStart": "إعادة محاولة التشغيل",
|
||||||
|
"wsl.server.updating": "جارٍ التحديث...",
|
||||||
|
"wsl.onboarding.step.distro": "اختر توزيعة",
|
||||||
|
"wsl.onboarding.step.opencode": "OpenCode",
|
||||||
|
"wsl.onboarding.checkingRuntime": "جارٍ التحقق من WSL...",
|
||||||
|
"wsl.onboarding.restartRequired": "يحتاج Windows إلى إعادة التشغيل لإكمال تثبيت WSL.",
|
||||||
|
"wsl.onboarding.ready": "WSL جاهز.",
|
||||||
|
"wsl.onboarding.required": "يلزم WSL للمتابعة.",
|
||||||
|
"wsl.onboarding.checkingDistros": "جارٍ التحقق من التوزيعات...",
|
||||||
|
"wsl.onboarding.installingDistro": "جارٍ تثبيت {{distro}}...",
|
||||||
|
"wsl.onboarding.checkingDistro": "جارٍ التحقق من {{distro}}...",
|
||||||
|
"wsl.onboarding.listingDistros": "جارٍ سرد التوزيعات...",
|
||||||
|
"wsl.onboarding.distroReady": "{{distro}} جاهزة.",
|
||||||
|
"wsl.onboarding.distroNotInstalled": "لم يتم تثبيت {{distro}} بعد.",
|
||||||
|
"wsl.onboarding.openDistroOnce": "افتح {{distro}} مرة واحدة لإكمال الإعداد.",
|
||||||
|
"wsl.onboarding.finishingDistro": "جارٍ إكمال إعداد {{distro}}.",
|
||||||
|
"wsl.onboarding.pickDistro": "اختر توزيعة أو ثبّت واحدة أدناه.",
|
||||||
|
"wsl.onboarding.checkingOpencode": "جارٍ التحقق من OpenCode...",
|
||||||
|
"wsl.onboarding.checkingOpencodeIn": "جارٍ التحقق من OpenCode في {{distro}}...",
|
||||||
|
"wsl.onboarding.updatingOpencode": "جارٍ تحديث OpenCode...",
|
||||||
|
"wsl.onboarding.updatingOpencodeIn": "جارٍ تحديث OpenCode في {{distro}}...",
|
||||||
|
"wsl.onboarding.updateOpencodeIn": "حدّث OpenCode في {{distro}}.",
|
||||||
|
"wsl.onboarding.updateOpencode": "تحديث OpenCode",
|
||||||
|
"wsl.onboarding.opencodeReadyIn": "OpenCode جاهز في {{distro}}.",
|
||||||
|
"wsl.onboarding.opencodeReady": "OpenCode جاهز.",
|
||||||
|
"wsl.onboarding.installOpencodeIn": "ثبّت OpenCode في {{distro}}.",
|
||||||
|
"wsl.onboarding.installOpencode": "تثبيت OpenCode",
|
||||||
|
"wsl.onboarding.chooseDistroFirst": "اختر توزيعة أولاً.",
|
||||||
|
"wsl.onboarding.loadFailed": "فشل تحميل حالة WSL.",
|
||||||
|
"wsl.onboarding.loading": "جارٍ التحميل...",
|
||||||
|
"wsl.onboarding.installedDistros": "التوزيعات المثبتة",
|
||||||
|
"wsl.onboarding.checkAgain": "تحقق مجددًا",
|
||||||
|
"wsl.onboarding.distroStatus.ready": "جاهزة",
|
||||||
|
"wsl.onboarding.distroStatus.checking": "جارٍ التحقق...",
|
||||||
|
"wsl.onboarding.distroStatus.opencodeMissing": "OpenCode غير مثبت",
|
||||||
|
"wsl.onboarding.distroStatus.missingTools": "bash و curl مفقودان",
|
||||||
|
"wsl.onboarding.distroStatus.unsupported": "غير مدعومة · استخدم WSL 2",
|
||||||
|
"wsl.onboarding.needAnotherDistro": "هل تحتاج إلى توزيعة أخرى؟",
|
||||||
|
"wsl.onboarding.needAnotherDistroHint": "ثبّت توزيعة Linux من كتالوج WSL",
|
||||||
|
"wsl.onboarding.wslNotInstalled.title": "WSL غير مثبت",
|
||||||
|
"wsl.onboarding.wslNotInstalled.description":
|
||||||
|
"يلزم WSL (نظام Windows الفرعي لـ Linux) قبل أن يتمكن OpenCode من إضافة خادم WSL",
|
||||||
|
"wsl.onboarding.wslUnavailable.title": "WSL غير متاح",
|
||||||
|
"wsl.onboarding.wslUnavailable.description": "تعذر على OpenCode التحقق من WSL على هذا الجهاز.",
|
||||||
|
"wsl.onboarding.installWsl": "تثبيت WSL",
|
||||||
|
"wsl.onboarding.windowsRestartRequired": "أعد تشغيل Windows لإكمال تثبيت WSL، ثم أعد فتح OpenCode.",
|
||||||
|
"wsl.onboarding.next": "التالي",
|
||||||
|
"wsl.onboarding.refresh": "تحديث",
|
||||||
|
"wsl.onboarding.allDistrosAdded": "تمت إضافة جميع التوزيعات المثبتة بالفعل.",
|
||||||
|
"wsl.onboarding.noDistros": "لم يتم اكتشاف أي توزيعات بعد.",
|
||||||
|
"wsl.onboarding.install": "تثبيت",
|
||||||
|
"wsl.onboarding.installing": "جارٍ التثبيت...",
|
||||||
|
"wsl.onboarding.installDistro": "تثبيت توزيعة",
|
||||||
|
"wsl.onboarding.searchDistros": "البحث عن توزيعات",
|
||||||
|
"wsl.onboarding.wsl2Required": "يلزم WSL 2.",
|
||||||
|
"wsl.onboarding.toolsRequired": "تحتاج هذه التوزيعة إلى bash و curl.",
|
||||||
|
"wsl.onboarding.openTerminal": "فتح المحطة الطرفية",
|
||||||
|
"wsl.onboarding.path": "المسار: {{path}}",
|
||||||
|
"wsl.onboarding.notFound": "غير موجود",
|
||||||
|
"wsl.onboarding.version": "الإصدار: {{version}}",
|
||||||
|
"wsl.onboarding.unknown": "غير معروف",
|
||||||
|
"wsl.onboarding.desktopVersion": "سطح المكتب {{version}}",
|
||||||
|
"wsl.onboarding.versionMismatch": "لا يتطابق الإصدار المثبت مع إصدار تطبيق سطح المكتب.",
|
||||||
|
"wsl.onboarding.adding": "جارٍ الإضافة...",
|
||||||
"dialog.project.edit.title": "تحرير المشروع",
|
"dialog.project.edit.title": "تحرير المشروع",
|
||||||
"dialog.project.edit.name": "الاسم",
|
"dialog.project.edit.name": "الاسم",
|
||||||
"dialog.project.edit.icon": "أيقونة",
|
"dialog.project.edit.icon": "أيقونة",
|
||||||
@@ -370,6 +450,7 @@ export const dict = {
|
|||||||
"language.no": "Norsk",
|
"language.no": "Norsk",
|
||||||
"language.br": "Português (Brasil)",
|
"language.br": "Português (Brasil)",
|
||||||
"language.bs": "Bosanski",
|
"language.bs": "Bosanski",
|
||||||
|
"language.uk": "Українська",
|
||||||
"language.th": "ไทย",
|
"language.th": "ไทย",
|
||||||
"language.tr": "Türkçe",
|
"language.tr": "Türkçe",
|
||||||
"toast.language.title": "لغة",
|
"toast.language.title": "لغة",
|
||||||
@@ -406,10 +487,12 @@ export const dict = {
|
|||||||
"toast.update.action.notYet": "ليس الآن",
|
"toast.update.action.notYet": "ليس الآن",
|
||||||
"error.page.title": "حدث خطأ ما",
|
"error.page.title": "حدث خطأ ما",
|
||||||
"error.page.description": "حدث خطأ أثناء تحميل التطبيق.",
|
"error.page.description": "حدث خطأ أثناء تحميل التطبيق.",
|
||||||
|
"error.page.description.localServerStartup": "حدث خطأ أثناء بدء تشغيل الخادم المحلي.",
|
||||||
"error.page.details.label": "تفاصيل الخطأ",
|
"error.page.details.label": "تفاصيل الخطأ",
|
||||||
"error.page.action.restart": "إعادة تشغيل",
|
"error.page.action.restart": "إعادة تشغيل",
|
||||||
"error.page.action.report": "الإبلاغ عن الخطأ",
|
"error.page.action.report": "الإبلاغ عن الخطأ",
|
||||||
"error.page.action.reported": "تم الإبلاغ عن الخطأ",
|
"error.page.action.reported": "تم الإبلاغ عن الخطأ",
|
||||||
|
"error.page.action.exportLogs": "تصدير السجلات",
|
||||||
"error.page.action.checking": "جارٍ التحقق...",
|
"error.page.action.checking": "جارٍ التحقق...",
|
||||||
"error.page.action.checkUpdates": "التحقق من وجود تحديثات",
|
"error.page.action.checkUpdates": "التحقق من وجود تحديثات",
|
||||||
"error.page.action.updateTo": "تحديث إلى {{version}}",
|
"error.page.action.updateTo": "تحديث إلى {{version}}",
|
||||||
@@ -450,16 +533,38 @@ export const dict = {
|
|||||||
"home.recentProjects": "المشاريع الحديثة",
|
"home.recentProjects": "المشاريع الحديثة",
|
||||||
"home.empty.title": "لا توجد مشاريع حديثة",
|
"home.empty.title": "لا توجد مشاريع حديثة",
|
||||||
"home.empty.description": "ابدأ بفتح مشروع محلي",
|
"home.empty.description": "ابدأ بفتح مشروع محلي",
|
||||||
|
"home.title": "الرئيسية",
|
||||||
|
"home.projects": "المشاريع",
|
||||||
|
"home.project.add": "إضافة مشروع",
|
||||||
|
"home.recentlyClosed": "المغلقة مؤخرًا",
|
||||||
|
"home.server.collapse": "طي مشاريع الخادم",
|
||||||
|
"home.server.expand": "توسيع مشاريع الخادم",
|
||||||
|
"home.sessions.search.placeholder": "البحث في الجلسات",
|
||||||
|
"home.sessions.search.placeholder.scoped": "البحث في جلسات {{scope}}",
|
||||||
|
"home.sessions.search.sessions": "الجلسات",
|
||||||
|
"home.sessions.search.noResults": "لم يتم العثور على جلسات لـ {{query}}",
|
||||||
|
"home.sessions.empty": "لا يوجد شيء هنا بعد",
|
||||||
|
"home.sessions.empty.description": "أنشئ جلسة للبدء",
|
||||||
|
"home.sessions.group.today": "اليوم",
|
||||||
|
"home.sessions.group.yesterday": "أمس",
|
||||||
|
"home.sessions.group.older": "الأقدم",
|
||||||
"session.tab.session": "جلسة",
|
"session.tab.session": "جلسة",
|
||||||
"session.tab.review": "مراجعة",
|
"session.tab.review": "مراجعة",
|
||||||
"session.tab.context": "سياق",
|
"session.tab.context": "سياق",
|
||||||
|
"session.tab.unknown": "جلسة غير معروفة",
|
||||||
"session.panel.reviewAndFiles": "المراجعة والملفات",
|
"session.panel.reviewAndFiles": "المراجعة والملفات",
|
||||||
|
"session.error.notFound": "لا يمكن العثور على هذه الجلسة",
|
||||||
|
"session.error.notFound.description": "يشير هذا التبويب إلى جلسة لم تعد موجودة على هذا الخادم.",
|
||||||
|
"session.error.notFound.closeTab": "إغلاق علامة التبويب",
|
||||||
|
"session.error.serverConnection": "تعذر الاتصال بهذا الخادم",
|
||||||
"session.review.filesChanged": "تم تغيير {{count}} ملفات",
|
"session.review.filesChanged": "تم تغيير {{count}} ملفات",
|
||||||
"session.review.change.one": "تغيير",
|
"session.review.change.one": "تغيير",
|
||||||
"session.review.change.other": "تغييرات",
|
"session.review.change.other": "تغييرات",
|
||||||
"session.review.loadingChanges": "جارٍ تحميل التغييرات...",
|
"session.review.loadingChanges": "جارٍ تحميل التغييرات...",
|
||||||
"session.review.empty": "لا توجد تغييرات في هذه الجلسة بعد",
|
"session.review.empty": "لا توجد تغييرات في هذه الجلسة بعد",
|
||||||
"session.review.noChanges": "لا توجد تغييرات",
|
"session.review.noChanges": "لا توجد تغييرات",
|
||||||
|
"session.review.noUncommittedChanges": "لا توجد تغييرات غير مُودَعة بعد",
|
||||||
|
"session.review.noBranchChanges": "لا توجد تغييرات في الفرع بعد",
|
||||||
"session.review.noVcs": "لم يتم اكتشاف نظام التحكم في الإصدار Git، لن يتم عرض التغييرات",
|
"session.review.noVcs": "لم يتم اكتشاف نظام التحكم في الإصدار Git، لن يتم عرض التغييرات",
|
||||||
"session.review.noSnapshot": "تم تعطيل تتبع اللقطات في التكوين، لذا فإن تغييرات الجلسة غير متوفرة",
|
"session.review.noSnapshot": "تم تعطيل تتبع اللقطات في التكوين، لذا فإن تغييرات الجلسة غير متوفرة",
|
||||||
"session.files.selectToOpen": "اختر ملفًا لفتحه",
|
"session.files.selectToOpen": "اختر ملفًا لفتحه",
|
||||||
@@ -475,6 +580,10 @@ export const dict = {
|
|||||||
"session.todo.title": "المهام",
|
"session.todo.title": "المهام",
|
||||||
"session.todo.collapse": "طي",
|
"session.todo.collapse": "طي",
|
||||||
"session.todo.expand": "توسيع",
|
"session.todo.expand": "توسيع",
|
||||||
|
"session.question.minimize": "تصغير السؤال",
|
||||||
|
"session.question.restore": "استعادة السؤال",
|
||||||
|
"session.question.pending.one": "{{count}} سؤال معلق",
|
||||||
|
"session.question.pending.other": "الأسئلة المعلقة: {{count}}",
|
||||||
"session.followupDock.summary.one": "{{count}} رسالة في الانتظار",
|
"session.followupDock.summary.one": "{{count}} رسالة في الانتظار",
|
||||||
"session.followupDock.summary.other": "{{count}} رسائل في الانتظار",
|
"session.followupDock.summary.other": "{{count}} رسائل في الانتظار",
|
||||||
"session.followupDock.sendNow": "إرسال الآن",
|
"session.followupDock.sendNow": "إرسال الآن",
|
||||||
@@ -487,9 +596,16 @@ export const dict = {
|
|||||||
"session.revertDock.expand": "توسيع الرسائل التي تم التراجع عنها",
|
"session.revertDock.expand": "توسيع الرسائل التي تم التراجع عنها",
|
||||||
"session.revertDock.restore": "استعادة الرسالة",
|
"session.revertDock.restore": "استعادة الرسالة",
|
||||||
"session.new.title": "ابنِ أي شيء",
|
"session.new.title": "ابنِ أي شيء",
|
||||||
|
"session.new.project.new": "مشروع جديد",
|
||||||
|
"session.new.project.search": "البحث عن المشاريع",
|
||||||
|
"session.new.project.add": "إضافة مشروع",
|
||||||
"session.new.worktree.main": "الفرع الرئيسي",
|
"session.new.worktree.main": "الفرع الرئيسي",
|
||||||
"session.new.worktree.mainWithBranch": "الفرع الرئيسي ({{branch}})",
|
"session.new.worktree.mainWithBranch": "الفرع الرئيسي ({{branch}})",
|
||||||
"session.new.worktree.create": "إنشاء شجرة عمل جديدة",
|
"session.new.worktree.create": "إنشاء شجرة عمل جديدة",
|
||||||
|
"session.new.workspace.runIn": "تشغيل الجلسة في",
|
||||||
|
"session.new.workspace.triggerLocal": "محلي",
|
||||||
|
"session.new.workspace.local": "المستودع المحلي",
|
||||||
|
"session.new.workspace.existing": "مساحة عمل…",
|
||||||
"session.new.lastModified": "آخر تعديل",
|
"session.new.lastModified": "آخر تعديل",
|
||||||
"session.header.search.placeholder": "بحث {{project}}",
|
"session.header.search.placeholder": "بحث {{project}}",
|
||||||
"session.header.searchFiles": "بحث عن الملفات",
|
"session.header.searchFiles": "بحث عن الملفات",
|
||||||
@@ -550,6 +666,8 @@ export const dict = {
|
|||||||
"sidebar.project.recentSessions": "الجلسات الحديثة",
|
"sidebar.project.recentSessions": "الجلسات الحديثة",
|
||||||
"sidebar.project.viewAllSessions": "عرض جميع الجلسات",
|
"sidebar.project.viewAllSessions": "عرض جميع الجلسات",
|
||||||
"sidebar.project.clearNotifications": "مسح الإشعارات",
|
"sidebar.project.clearNotifications": "مسح الإشعارات",
|
||||||
|
"sidebar.empty.title": "لا توجد مشاريع مفتوحة",
|
||||||
|
"sidebar.empty.description": "افتح مشروعًا للبدء",
|
||||||
"app.name.desktop": "OpenCode Desktop",
|
"app.name.desktop": "OpenCode Desktop",
|
||||||
"settings.section.desktop": "سطح المكتب",
|
"settings.section.desktop": "سطح المكتب",
|
||||||
"settings.section.server": "الخادم",
|
"settings.section.server": "الخادم",
|
||||||
@@ -559,6 +677,7 @@ export const dict = {
|
|||||||
"settings.desktop.wsl.title": "تكامل WSL",
|
"settings.desktop.wsl.title": "تكامل WSL",
|
||||||
"settings.desktop.wsl.description": "تشغيل خادم OpenCode داخل WSL على Windows.",
|
"settings.desktop.wsl.description": "تشغيل خادم OpenCode داخل WSL على Windows.",
|
||||||
"settings.general.section.appearance": "المظهر",
|
"settings.general.section.appearance": "المظهر",
|
||||||
|
"settings.general.section.advanced": "متقدم",
|
||||||
"settings.general.section.notifications": "إشعارات النظام",
|
"settings.general.section.notifications": "إشعارات النظام",
|
||||||
"settings.general.section.updates": "التحديثات",
|
"settings.general.section.updates": "التحديثات",
|
||||||
"settings.general.section.sounds": "المؤثرات الصوتية",
|
"settings.general.section.sounds": "المؤثرات الصوتية",
|
||||||
@@ -566,6 +685,11 @@ export const dict = {
|
|||||||
"settings.general.section.display": "شاشة العرض",
|
"settings.general.section.display": "شاشة العرض",
|
||||||
"settings.general.row.language.title": "اللغة",
|
"settings.general.row.language.title": "اللغة",
|
||||||
"settings.general.row.language.description": "تغيير لغة العرض لـ OpenCode",
|
"settings.general.row.language.description": "تغيير لغة العرض لـ OpenCode",
|
||||||
|
"settings.general.row.shell.title": "Shell المحطة الطرفية",
|
||||||
|
"settings.general.row.shell.description":
|
||||||
|
"اختر shell المستخدم في المحطة الطرفية. تُستخدم واجهات shell المتوافقة أيضًا في استدعاءات أدوات الوكيل.",
|
||||||
|
"settings.general.row.shell.autoDefault": "تلقائي (افتراضي)",
|
||||||
|
"settings.general.row.shell.terminalOnly": "المحطة الطرفية فقط",
|
||||||
"settings.general.row.appearance.title": "المظهر",
|
"settings.general.row.appearance.title": "المظهر",
|
||||||
"settings.general.row.appearance.description": "تخصيص كيفية ظهور OpenCode على جهازك",
|
"settings.general.row.appearance.description": "تخصيص كيفية ظهور OpenCode على جهازك",
|
||||||
"settings.general.row.colorScheme.title": "مخطط الألوان",
|
"settings.general.row.colorScheme.title": "مخطط الألوان",
|
||||||
@@ -574,14 +698,29 @@ export const dict = {
|
|||||||
"settings.general.row.theme.description": "تخصيص سمة OpenCode.",
|
"settings.general.row.theme.description": "تخصيص سمة OpenCode.",
|
||||||
"settings.general.row.font.title": "خط الكود",
|
"settings.general.row.font.title": "خط الكود",
|
||||||
"settings.general.row.font.description": "خصّص الخط المستخدم في كتل التعليمات البرمجية",
|
"settings.general.row.font.description": "خصّص الخط المستخدم في كتل التعليمات البرمجية",
|
||||||
"settings.general.row.terminalFont.title": "Terminal Font",
|
"settings.general.row.terminalFont.title": "خط الطرفية",
|
||||||
"settings.general.row.terminalFont.description": "Customise the font used in the terminal",
|
"settings.general.row.terminalFont.description": "خصّص الخط المستخدم في الطرفية",
|
||||||
"settings.general.row.uiFont.title": "خط الواجهة",
|
"settings.general.row.uiFont.title": "خط الواجهة",
|
||||||
"settings.general.row.uiFont.description": "خصّص الخط المستخدم في الواجهة بأكملها",
|
"settings.general.row.uiFont.description": "خصّص الخط المستخدم في الواجهة بأكملها",
|
||||||
"settings.general.row.followup.title": "سلوك المتابعة",
|
"settings.general.row.followup.title": "سلوك المتابعة",
|
||||||
"settings.general.row.followup.description": "اختر ما إذا كانت طلبات المتابعة توجه فورًا أو تنتظر في قائمة انتظار",
|
"settings.general.row.followup.description": "اختر ما إذا كانت طلبات المتابعة توجه فورًا أو تنتظر في قائمة انتظار",
|
||||||
"settings.general.row.followup.option.queue": "قائمة انتظار",
|
"settings.general.row.followup.option.queue": "قائمة انتظار",
|
||||||
"settings.general.row.followup.option.steer": "توجيه",
|
"settings.general.row.followup.option.steer": "توجيه",
|
||||||
|
"settings.general.row.showFileTree.title": "شجرة الملفات",
|
||||||
|
"settings.general.row.showFileTree.description": "إظهار لوحة شجرة الملفات في الجلسات",
|
||||||
|
"settings.general.row.showNavigation.title": "عناصر التحكم في التنقل",
|
||||||
|
"settings.general.row.showNavigation.description": "إظهار زري الرجوع والتقدم في شريط عنوان سطح المكتب",
|
||||||
|
"settings.general.row.showSearch.title": "لوحة الأوامر",
|
||||||
|
"settings.general.row.showSearch.description": "إظهار زر البحث ولوحة الأوامر في شريط العنوان",
|
||||||
|
"settings.general.row.showTerminal.title": "المحطة الطرفية",
|
||||||
|
"settings.general.row.showTerminal.description": "إظهار زر المحطة الطرفية في شريط عنوان سطح المكتب",
|
||||||
|
"settings.general.row.showStatus.title": "حالة الخادم",
|
||||||
|
"settings.general.row.showStatus.description": "إظهار زر حالة الخادم في شريط العنوان",
|
||||||
|
"settings.general.row.mobileTitlebarBottom.title": "التنقل السفلي",
|
||||||
|
"settings.general.row.mobileTitlebarBottom.description":
|
||||||
|
"وضع شريط العنوان وعلامات تبويب الجلسات أسفل الشاشة على الأجهزة المحمولة",
|
||||||
|
"settings.general.row.showCustomAgents.title": "الوكلاء المخصصون",
|
||||||
|
"settings.general.row.showCustomAgents.description": "إظهار محدد الوكلاء في محرر الرسائل",
|
||||||
"settings.general.row.reasoningSummaries.title": "إظهار ملخصات الاستنتاج",
|
"settings.general.row.reasoningSummaries.title": "إظهار ملخصات الاستنتاج",
|
||||||
"settings.general.row.reasoningSummaries.description": "عرض ملخصات استنتاج النموذج في الشريط الزمني",
|
"settings.general.row.reasoningSummaries.description": "عرض ملخصات استنتاج النموذج في الشريط الزمني",
|
||||||
"settings.general.row.shellToolPartsExpanded.title": "توسيع أجزاء أداة shell",
|
"settings.general.row.shellToolPartsExpanded.title": "توسيع أجزاء أداة shell",
|
||||||
@@ -590,6 +729,11 @@ export const dict = {
|
|||||||
"settings.general.row.editToolPartsExpanded.title": "توسيع أجزاء أداة edit",
|
"settings.general.row.editToolPartsExpanded.title": "توسيع أجزاء أداة edit",
|
||||||
"settings.general.row.editToolPartsExpanded.description":
|
"settings.general.row.editToolPartsExpanded.description":
|
||||||
"إظهار أجزاء أدوات edit و write و patch موسعة بشكل افتراضي في الشريط الزمني",
|
"إظهار أجزاء أدوات edit و write و patch موسعة بشكل افتراضي في الشريط الزمني",
|
||||||
|
"settings.general.row.newLayoutDesigns.title": "التخطيط والتصاميم الجديدة",
|
||||||
|
"settings.general.row.newLayoutDesigns.description":
|
||||||
|
"تمكين التخطيط والصفحة الرئيسية ومحرر الرسائل وواجهة الجلسة المعاد تصميمها",
|
||||||
|
"settings.general.row.pinchZoom.title": "التكبير بإيماءة القرص",
|
||||||
|
"settings.general.row.pinchZoom.description": "السماح بإيماءة القرص على لوحة اللمس وإيماءة Ctrl-scroll للتكبير",
|
||||||
"settings.general.row.wayland.title": "استخدام Wayland الأصلي",
|
"settings.general.row.wayland.title": "استخدام Wayland الأصلي",
|
||||||
"settings.general.row.wayland.description": "تعطيل التراجع إلى X11 على Wayland. يتطلب إعادة التشغيل.",
|
"settings.general.row.wayland.description": "تعطيل التراجع إلى X11 على Wayland. يتطلب إعادة التشغيل.",
|
||||||
"settings.general.row.wayland.tooltip":
|
"settings.general.row.wayland.tooltip":
|
||||||
@@ -602,6 +746,8 @@ export const dict = {
|
|||||||
"settings.updates.row.check.description": "التحقق يدويًا من التحديثات وتثبيتها إذا كانت متاحة",
|
"settings.updates.row.check.description": "التحقق يدويًا من التحديثات وتثبيتها إذا كانت متاحة",
|
||||||
"settings.updates.action.checkNow": "تحقق الآن",
|
"settings.updates.action.checkNow": "تحقق الآن",
|
||||||
"settings.updates.action.checking": "جارٍ التحقق...",
|
"settings.updates.action.checking": "جارٍ التحقق...",
|
||||||
|
"settings.updates.action.downloading": "جارٍ التنزيل...",
|
||||||
|
"settings.updates.action.installing": "جارٍ التثبيت...",
|
||||||
"settings.updates.toast.latest.title": "أنت على آخر إصدار",
|
"settings.updates.toast.latest.title": "أنت على آخر إصدار",
|
||||||
"settings.updates.toast.latest.description": "أنت تستخدم أحدث إصدار من OpenCode.",
|
"settings.updates.toast.latest.description": "أنت تستخدم أحدث إصدار من OpenCode.",
|
||||||
"sound.option.none": "بلا",
|
"sound.option.none": "بلا",
|
||||||
|
|||||||
+152
-3
@@ -21,6 +21,9 @@ export const dict = {
|
|||||||
"theme.scheme.dark": "Escuro",
|
"theme.scheme.dark": "Escuro",
|
||||||
"command.sidebar.toggle": "Alternar barra lateral",
|
"command.sidebar.toggle": "Alternar barra lateral",
|
||||||
"command.project.open": "Abrir projeto",
|
"command.project.open": "Abrir projeto",
|
||||||
|
"command.project.previous": "Projeto anterior",
|
||||||
|
"command.project.next": "Próximo projeto",
|
||||||
|
"command.project.index": "Alternar para o projeto {{index}}",
|
||||||
"command.provider.connect": "Conectar provedor",
|
"command.provider.connect": "Conectar provedor",
|
||||||
"command.server.switch": "Trocar servidor",
|
"command.server.switch": "Trocar servidor",
|
||||||
"command.settings.open": "Abrir configurações",
|
"command.settings.open": "Abrir configurações",
|
||||||
@@ -39,6 +42,7 @@ export const dict = {
|
|||||||
"command.session.new": "Nova sessão",
|
"command.session.new": "Nova sessão",
|
||||||
"command.file.open": "Abrir arquivo",
|
"command.file.open": "Abrir arquivo",
|
||||||
"command.tab.close": "Fechar aba",
|
"command.tab.close": "Fechar aba",
|
||||||
|
"command.tab.reopenClosed": "Reabrir aba fechada",
|
||||||
"command.context.addSelection": "Adicionar seleção ao contexto",
|
"command.context.addSelection": "Adicionar seleção ao contexto",
|
||||||
"command.context.addSelection.description": "Adicionar as linhas selecionadas do arquivo atual",
|
"command.context.addSelection.description": "Adicionar as linhas selecionadas do arquivo atual",
|
||||||
"command.input.focus": "Focar entrada",
|
"command.input.focus": "Focar entrada",
|
||||||
@@ -193,6 +197,11 @@ export const dict = {
|
|||||||
"model.input.video": "vídeo",
|
"model.input.video": "vídeo",
|
||||||
"model.input.pdf": "pdf",
|
"model.input.pdf": "pdf",
|
||||||
"model.tooltip.allows": "Permite: {{inputs}}",
|
"model.tooltip.allows": "Permite: {{inputs}}",
|
||||||
|
"model.tooltip.context.label": "Contexto",
|
||||||
|
"model.tooltip.inputs": "Entradas",
|
||||||
|
"model.tooltip.model": "Modelo",
|
||||||
|
"model.tooltip.provider": "Provedor",
|
||||||
|
"model.tooltip.reasoning": "Raciocínio",
|
||||||
"model.tooltip.reasoning.allowed": "Permite raciocínio",
|
"model.tooltip.reasoning.allowed": "Permite raciocínio",
|
||||||
"model.tooltip.reasoning.none": "Sem raciocínio",
|
"model.tooltip.reasoning.none": "Sem raciocínio",
|
||||||
"model.tooltip.context": "Limite de contexto {{limit}}",
|
"model.tooltip.context": "Limite de contexto {{limit}}",
|
||||||
@@ -202,6 +211,7 @@ export const dict = {
|
|||||||
"common.loading": "Carregando",
|
"common.loading": "Carregando",
|
||||||
"common.loading.ellipsis": "...",
|
"common.loading.ellipsis": "...",
|
||||||
"common.cancel": "Cancelar",
|
"common.cancel": "Cancelar",
|
||||||
|
"common.clear": "Limpar",
|
||||||
"common.connect": "Conectar",
|
"common.connect": "Conectar",
|
||||||
"common.disconnect": "Desconectar",
|
"common.disconnect": "Desconectar",
|
||||||
"common.continue": "Enviar",
|
"common.continue": "Enviar",
|
||||||
@@ -218,6 +228,8 @@ export const dict = {
|
|||||||
"prompt.mode.shell": "Shell",
|
"prompt.mode.shell": "Shell",
|
||||||
"prompt.mode.normal": "Prompt",
|
"prompt.mode.normal": "Prompt",
|
||||||
"prompt.mode.shell.exit": "esc para sair",
|
"prompt.mode.shell.exit": "esc para sair",
|
||||||
|
"session.child.promptDisabled": "Não é possível enviar prompts em sessões de subagentes.",
|
||||||
|
"session.child.backToParent": "Voltar à sessão principal.",
|
||||||
"prompt.example.1": "Corrigir um TODO no código",
|
"prompt.example.1": "Corrigir um TODO no código",
|
||||||
"prompt.example.2": "Qual é a stack tecnológica deste projeto?",
|
"prompt.example.2": "Qual é a stack tecnológica deste projeto?",
|
||||||
"prompt.example.3": "Corrigir testes quebrados",
|
"prompt.example.3": "Corrigir testes quebrados",
|
||||||
@@ -314,6 +326,75 @@ export const dict = {
|
|||||||
"dialog.server.menu.delete": "Excluir",
|
"dialog.server.menu.delete": "Excluir",
|
||||||
"dialog.server.current": "Servidor atual",
|
"dialog.server.current": "Servidor atual",
|
||||||
"dialog.server.status.default": "Padrão",
|
"dialog.server.status.default": "Padrão",
|
||||||
|
"wsl.server.add": "Adicionar servidor WSL",
|
||||||
|
"wsl.server.addShort": "Adicionar WSL",
|
||||||
|
"wsl.server.label": "WSL",
|
||||||
|
"wsl.server.menu.label": "Servidor WSL",
|
||||||
|
"wsl.server.retryStart": "Tentar iniciar novamente",
|
||||||
|
"wsl.server.updating": "Atualizando...",
|
||||||
|
"wsl.onboarding.step.distro": "Escolher distribuição",
|
||||||
|
"wsl.onboarding.step.opencode": "OpenCode",
|
||||||
|
"wsl.onboarding.checkingRuntime": "Verificando o WSL...",
|
||||||
|
"wsl.onboarding.restartRequired": "O Windows precisa ser reiniciado para concluir a instalação do WSL.",
|
||||||
|
"wsl.onboarding.ready": "O WSL está pronto.",
|
||||||
|
"wsl.onboarding.required": "O WSL é necessário para continuar.",
|
||||||
|
"wsl.onboarding.checkingDistros": "Verificando distribuições...",
|
||||||
|
"wsl.onboarding.installingDistro": "Instalando {{distro}}...",
|
||||||
|
"wsl.onboarding.checkingDistro": "Verificando {{distro}}...",
|
||||||
|
"wsl.onboarding.listingDistros": "Listando distribuições...",
|
||||||
|
"wsl.onboarding.distroReady": "{{distro}} está pronta.",
|
||||||
|
"wsl.onboarding.distroNotInstalled": "{{distro}} ainda não está instalada.",
|
||||||
|
"wsl.onboarding.openDistroOnce": "Abra {{distro}} uma vez para concluir a configuração.",
|
||||||
|
"wsl.onboarding.finishingDistro": "Concluindo a configuração de {{distro}}.",
|
||||||
|
"wsl.onboarding.pickDistro": "Escolha uma distribuição ou instale uma abaixo.",
|
||||||
|
"wsl.onboarding.checkingOpencode": "Verificando o OpenCode...",
|
||||||
|
"wsl.onboarding.checkingOpencodeIn": "Verificando o OpenCode em {{distro}}...",
|
||||||
|
"wsl.onboarding.updatingOpencode": "Atualizando o OpenCode...",
|
||||||
|
"wsl.onboarding.updatingOpencodeIn": "Atualizando o OpenCode em {{distro}}...",
|
||||||
|
"wsl.onboarding.updateOpencodeIn": "Atualize o OpenCode em {{distro}}.",
|
||||||
|
"wsl.onboarding.updateOpencode": "Atualizar o OpenCode",
|
||||||
|
"wsl.onboarding.opencodeReadyIn": "O OpenCode está pronto em {{distro}}.",
|
||||||
|
"wsl.onboarding.opencodeReady": "O OpenCode está pronto.",
|
||||||
|
"wsl.onboarding.installOpencodeIn": "Instale o OpenCode em {{distro}}.",
|
||||||
|
"wsl.onboarding.installOpencode": "Instalar o OpenCode",
|
||||||
|
"wsl.onboarding.chooseDistroFirst": "Escolha primeiro uma distribuição.",
|
||||||
|
"wsl.onboarding.loadFailed": "Falha ao carregar o estado do WSL.",
|
||||||
|
"wsl.onboarding.loading": "Carregando...",
|
||||||
|
"wsl.onboarding.installedDistros": "Distribuições instaladas",
|
||||||
|
"wsl.onboarding.checkAgain": "Verificar novamente",
|
||||||
|
"wsl.onboarding.distroStatus.ready": "Pronta",
|
||||||
|
"wsl.onboarding.distroStatus.checking": "Verificando...",
|
||||||
|
"wsl.onboarding.distroStatus.opencodeMissing": "OpenCode não instalado",
|
||||||
|
"wsl.onboarding.distroStatus.missingTools": "bash e curl ausentes",
|
||||||
|
"wsl.onboarding.distroStatus.unsupported": "Sem suporte · Use o WSL 2",
|
||||||
|
"wsl.onboarding.needAnotherDistro": "Precisa de outra distribuição?",
|
||||||
|
"wsl.onboarding.needAnotherDistroHint": "Instale uma distribuição Linux pelo catálogo do WSL",
|
||||||
|
"wsl.onboarding.wslNotInstalled.title": "WSL não instalado",
|
||||||
|
"wsl.onboarding.wslNotInstalled.description":
|
||||||
|
"O WSL (Windows Subsystem for Linux) é necessário para que o OpenCode possa adicionar um servidor WSL",
|
||||||
|
"wsl.onboarding.wslUnavailable.title": "WSL indisponível",
|
||||||
|
"wsl.onboarding.wslUnavailable.description": "O OpenCode não conseguiu verificar o WSL neste computador.",
|
||||||
|
"wsl.onboarding.installWsl": "Instalar o WSL",
|
||||||
|
"wsl.onboarding.windowsRestartRequired":
|
||||||
|
"Reinicie o Windows para concluir a instalação do WSL e abra o OpenCode novamente.",
|
||||||
|
"wsl.onboarding.next": "Próximo",
|
||||||
|
"wsl.onboarding.refresh": "Atualizar",
|
||||||
|
"wsl.onboarding.allDistrosAdded": "Todas as distribuições instaladas já foram adicionadas.",
|
||||||
|
"wsl.onboarding.noDistros": "Nenhuma distribuição detectada ainda.",
|
||||||
|
"wsl.onboarding.install": "Instalar",
|
||||||
|
"wsl.onboarding.installing": "Instalando...",
|
||||||
|
"wsl.onboarding.installDistro": "Instalar distribuição",
|
||||||
|
"wsl.onboarding.searchDistros": "Buscar distribuições",
|
||||||
|
"wsl.onboarding.wsl2Required": "O WSL 2 é necessário.",
|
||||||
|
"wsl.onboarding.toolsRequired": "Esta distribuição precisa do bash e do curl.",
|
||||||
|
"wsl.onboarding.openTerminal": "Abrir terminal",
|
||||||
|
"wsl.onboarding.path": "Caminho: {{path}}",
|
||||||
|
"wsl.onboarding.notFound": "não encontrado",
|
||||||
|
"wsl.onboarding.version": "Versão: {{version}}",
|
||||||
|
"wsl.onboarding.unknown": "desconhecido",
|
||||||
|
"wsl.onboarding.desktopVersion": "desktop {{version}}",
|
||||||
|
"wsl.onboarding.versionMismatch": "A versão instalada não corresponde à versão do aplicativo para desktop.",
|
||||||
|
"wsl.onboarding.adding": "Adicionando...",
|
||||||
"dialog.project.edit.title": "Editar projeto",
|
"dialog.project.edit.title": "Editar projeto",
|
||||||
"dialog.project.edit.name": "Nome",
|
"dialog.project.edit.name": "Nome",
|
||||||
"dialog.project.edit.icon": "Ícone",
|
"dialog.project.edit.icon": "Ícone",
|
||||||
@@ -371,6 +452,7 @@ export const dict = {
|
|||||||
"language.no": "Norsk",
|
"language.no": "Norsk",
|
||||||
"language.br": "Português (Brasil)",
|
"language.br": "Português (Brasil)",
|
||||||
"language.bs": "Bosanski",
|
"language.bs": "Bosanski",
|
||||||
|
"language.uk": "Українська",
|
||||||
"language.th": "ไทย",
|
"language.th": "ไทย",
|
||||||
"language.tr": "Türkçe",
|
"language.tr": "Türkçe",
|
||||||
"toast.language.title": "Idioma",
|
"toast.language.title": "Idioma",
|
||||||
@@ -407,10 +489,12 @@ export const dict = {
|
|||||||
"toast.update.action.notYet": "Agora não",
|
"toast.update.action.notYet": "Agora não",
|
||||||
"error.page.title": "Algo deu errado",
|
"error.page.title": "Algo deu errado",
|
||||||
"error.page.description": "Ocorreu um erro ao carregar a aplicação.",
|
"error.page.description": "Ocorreu um erro ao carregar a aplicação.",
|
||||||
|
"error.page.description.localServerStartup": "Ocorreu um erro ao iniciar o servidor local.",
|
||||||
"error.page.details.label": "Detalhes do Erro",
|
"error.page.details.label": "Detalhes do Erro",
|
||||||
"error.page.action.restart": "Reiniciar",
|
"error.page.action.restart": "Reiniciar",
|
||||||
"error.page.action.report": "Reportar erro",
|
"error.page.action.report": "Reportar erro",
|
||||||
"error.page.action.reported": "Erro reportado",
|
"error.page.action.reported": "Erro reportado",
|
||||||
|
"error.page.action.exportLogs": "Exportar logs",
|
||||||
"error.page.action.checking": "Verificando...",
|
"error.page.action.checking": "Verificando...",
|
||||||
"error.page.action.checkUpdates": "Verificar atualizações",
|
"error.page.action.checkUpdates": "Verificar atualizações",
|
||||||
"error.page.action.updateTo": "Atualizar para {{version}}",
|
"error.page.action.updateTo": "Atualizar para {{version}}",
|
||||||
@@ -453,10 +537,30 @@ export const dict = {
|
|||||||
"home.recentProjects": "Projetos recentes",
|
"home.recentProjects": "Projetos recentes",
|
||||||
"home.empty.title": "Nenhum projeto recente",
|
"home.empty.title": "Nenhum projeto recente",
|
||||||
"home.empty.description": "Comece abrindo um projeto local",
|
"home.empty.description": "Comece abrindo um projeto local",
|
||||||
|
"home.title": "Início",
|
||||||
|
"home.projects": "Projetos",
|
||||||
|
"home.project.add": "Adicionar projeto",
|
||||||
|
"home.recentlyClosed": "Projetos fechados recentemente",
|
||||||
|
"home.server.collapse": "Recolher projetos do servidor",
|
||||||
|
"home.server.expand": "Expandir projetos do servidor",
|
||||||
|
"home.sessions.search.placeholder": "Buscar sessões",
|
||||||
|
"home.sessions.search.placeholder.scoped": "Buscar sessões em {{scope}}",
|
||||||
|
"home.sessions.search.sessions": "Sessões",
|
||||||
|
"home.sessions.search.noResults": "Nenhuma sessão encontrada para {{query}}",
|
||||||
|
"home.sessions.empty": "Nada por aqui ainda",
|
||||||
|
"home.sessions.empty.description": "Crie uma sessão para começar",
|
||||||
|
"home.sessions.group.today": "Hoje",
|
||||||
|
"home.sessions.group.yesterday": "Ontem",
|
||||||
|
"home.sessions.group.older": "Mais antigas",
|
||||||
"session.tab.session": "Sessão",
|
"session.tab.session": "Sessão",
|
||||||
"session.tab.review": "Revisão",
|
"session.tab.review": "Revisão",
|
||||||
"session.tab.context": "Contexto",
|
"session.tab.context": "Contexto",
|
||||||
|
"session.tab.unknown": "Sessão desconhecida",
|
||||||
"session.panel.reviewAndFiles": "Revisão e arquivos",
|
"session.panel.reviewAndFiles": "Revisão e arquivos",
|
||||||
|
"session.error.notFound": "Esta sessão não foi encontrada",
|
||||||
|
"session.error.notFound.description": "Esta aba aponta para uma sessão que não existe mais neste servidor.",
|
||||||
|
"session.error.notFound.closeTab": "Fechar aba",
|
||||||
|
"session.error.serverConnection": "Não é possível conectar a este servidor",
|
||||||
"session.review.filesChanged": "{{count}} Arquivos Alterados",
|
"session.review.filesChanged": "{{count}} Arquivos Alterados",
|
||||||
"session.review.change.one": "Alteração",
|
"session.review.change.one": "Alteração",
|
||||||
"session.review.change.other": "Alterações",
|
"session.review.change.other": "Alterações",
|
||||||
@@ -466,6 +570,8 @@ export const dict = {
|
|||||||
"session.review.noSnapshot":
|
"session.review.noSnapshot":
|
||||||
"O rastreamento de snapshot está desabilitado na configuração, então as alterações da sessão estão indisponíveis",
|
"O rastreamento de snapshot está desabilitado na configuração, então as alterações da sessão estão indisponíveis",
|
||||||
"session.review.noChanges": "Sem alterações",
|
"session.review.noChanges": "Sem alterações",
|
||||||
|
"session.review.noUncommittedChanges": "Ainda não há alterações sem commit",
|
||||||
|
"session.review.noBranchChanges": "Ainda não há alterações na branch",
|
||||||
"session.files.selectToOpen": "Selecione um arquivo para abrir",
|
"session.files.selectToOpen": "Selecione um arquivo para abrir",
|
||||||
"session.files.all": "Todos os arquivos",
|
"session.files.all": "Todos os arquivos",
|
||||||
"session.files.empty": "Nenhum arquivo",
|
"session.files.empty": "Nenhum arquivo",
|
||||||
@@ -479,6 +585,10 @@ export const dict = {
|
|||||||
"session.todo.title": "Tarefas",
|
"session.todo.title": "Tarefas",
|
||||||
"session.todo.collapse": "Recolher",
|
"session.todo.collapse": "Recolher",
|
||||||
"session.todo.expand": "Expandir",
|
"session.todo.expand": "Expandir",
|
||||||
|
"session.question.minimize": "Minimizar pergunta",
|
||||||
|
"session.question.restore": "Restaurar pergunta",
|
||||||
|
"session.question.pending.one": "{{count}} pergunta pendente",
|
||||||
|
"session.question.pending.other": "{{count}} perguntas pendentes",
|
||||||
"session.followupDock.summary.one": "{{count}} mensagem na fila",
|
"session.followupDock.summary.one": "{{count}} mensagem na fila",
|
||||||
"session.followupDock.summary.other": "{{count}} mensagens na fila",
|
"session.followupDock.summary.other": "{{count}} mensagens na fila",
|
||||||
"session.followupDock.sendNow": "Enviar agora",
|
"session.followupDock.sendNow": "Enviar agora",
|
||||||
@@ -491,9 +601,16 @@ export const dict = {
|
|||||||
"session.revertDock.expand": "Expandir mensagens revertidas",
|
"session.revertDock.expand": "Expandir mensagens revertidas",
|
||||||
"session.revertDock.restore": "Restaurar mensagem",
|
"session.revertDock.restore": "Restaurar mensagem",
|
||||||
"session.new.title": "Crie qualquer coisa",
|
"session.new.title": "Crie qualquer coisa",
|
||||||
|
"session.new.project.new": "Novo projeto",
|
||||||
|
"session.new.project.search": "Buscar projetos",
|
||||||
|
"session.new.project.add": "Adicionar projeto",
|
||||||
"session.new.worktree.main": "Branch principal",
|
"session.new.worktree.main": "Branch principal",
|
||||||
"session.new.worktree.mainWithBranch": "Branch principal ({{branch}})",
|
"session.new.worktree.mainWithBranch": "Branch principal ({{branch}})",
|
||||||
"session.new.worktree.create": "Criar novo worktree",
|
"session.new.worktree.create": "Criar novo worktree",
|
||||||
|
"session.new.workspace.runIn": "Executar sessão em",
|
||||||
|
"session.new.workspace.triggerLocal": "Local",
|
||||||
|
"session.new.workspace.local": "Repositório local",
|
||||||
|
"session.new.workspace.existing": "Espaço de trabalho…",
|
||||||
"session.new.lastModified": "Última modificação",
|
"session.new.lastModified": "Última modificação",
|
||||||
"session.header.search.placeholder": "Buscar {{project}}",
|
"session.header.search.placeholder": "Buscar {{project}}",
|
||||||
"session.header.searchFiles": "Buscar arquivos",
|
"session.header.searchFiles": "Buscar arquivos",
|
||||||
@@ -557,15 +674,18 @@ export const dict = {
|
|||||||
"sidebar.project.recentSessions": "Sessões recentes",
|
"sidebar.project.recentSessions": "Sessões recentes",
|
||||||
"sidebar.project.viewAllSessions": "Ver todas as sessões",
|
"sidebar.project.viewAllSessions": "Ver todas as sessões",
|
||||||
"sidebar.project.clearNotifications": "Limpar notificações",
|
"sidebar.project.clearNotifications": "Limpar notificações",
|
||||||
|
"sidebar.empty.title": "Nenhum projeto aberto",
|
||||||
|
"sidebar.empty.description": "Abra um projeto para começar",
|
||||||
"app.name.desktop": "OpenCode Desktop",
|
"app.name.desktop": "OpenCode Desktop",
|
||||||
"settings.section.desktop": "Desktop",
|
"settings.section.desktop": "Desktop",
|
||||||
"settings.section.server": "Servidor",
|
"settings.section.server": "Servidor",
|
||||||
"settings.tab.general": "Geral",
|
"settings.tab.general": "Geral",
|
||||||
"settings.tab.shortcuts": "Atalhos",
|
"settings.tab.shortcuts": "Atalhos",
|
||||||
"settings.desktop.section.wsl": "WSL",
|
"settings.desktop.section.wsl": "WSL",
|
||||||
"settings.desktop.wsl.title": "WSL integration",
|
"settings.desktop.wsl.title": "Integração com WSL",
|
||||||
"settings.desktop.wsl.description": "Executar o servidor OpenCode dentro do WSL no Windows.",
|
"settings.desktop.wsl.description": "Executar o servidor OpenCode dentro do WSL no Windows.",
|
||||||
"settings.general.section.appearance": "Aparência",
|
"settings.general.section.appearance": "Aparência",
|
||||||
|
"settings.general.section.advanced": "Avançado",
|
||||||
"settings.general.section.notifications": "Notificações do sistema",
|
"settings.general.section.notifications": "Notificações do sistema",
|
||||||
"settings.general.section.updates": "Atualizações",
|
"settings.general.section.updates": "Atualizações",
|
||||||
"settings.general.section.sounds": "Efeitos sonoros",
|
"settings.general.section.sounds": "Efeitos sonoros",
|
||||||
@@ -573,6 +693,11 @@ export const dict = {
|
|||||||
"settings.general.section.display": "Tela",
|
"settings.general.section.display": "Tela",
|
||||||
"settings.general.row.language.title": "Idioma",
|
"settings.general.row.language.title": "Idioma",
|
||||||
"settings.general.row.language.description": "Alterar o idioma de exibição do OpenCode",
|
"settings.general.row.language.description": "Alterar o idioma de exibição do OpenCode",
|
||||||
|
"settings.general.row.shell.title": "Shell do terminal",
|
||||||
|
"settings.general.row.shell.description":
|
||||||
|
"Escolha o shell usado no terminal. Os shells compatíveis também são usados nas chamadas de ferramentas do agente.",
|
||||||
|
"settings.general.row.shell.autoDefault": "Automático (Padrão)",
|
||||||
|
"settings.general.row.shell.terminalOnly": "somente no terminal",
|
||||||
"settings.general.row.appearance.title": "Aparência",
|
"settings.general.row.appearance.title": "Aparência",
|
||||||
"settings.general.row.appearance.description": "Personalize como o OpenCode aparece no seu dispositivo",
|
"settings.general.row.appearance.description": "Personalize como o OpenCode aparece no seu dispositivo",
|
||||||
"settings.general.row.colorScheme.title": "Esquema de cores",
|
"settings.general.row.colorScheme.title": "Esquema de cores",
|
||||||
@@ -581,8 +706,8 @@ export const dict = {
|
|||||||
"settings.general.row.theme.description": "Personalize como o OpenCode é tematizado.",
|
"settings.general.row.theme.description": "Personalize como o OpenCode é tematizado.",
|
||||||
"settings.general.row.font.title": "Fonte de código",
|
"settings.general.row.font.title": "Fonte de código",
|
||||||
"settings.general.row.font.description": "Personalize a fonte usada em blocos de código",
|
"settings.general.row.font.description": "Personalize a fonte usada em blocos de código",
|
||||||
"settings.general.row.terminalFont.title": "Terminal Font",
|
"settings.general.row.terminalFont.title": "Fonte do terminal",
|
||||||
"settings.general.row.terminalFont.description": "Customise the font used in the terminal",
|
"settings.general.row.terminalFont.description": "Personalize a fonte usada no terminal",
|
||||||
"settings.general.row.uiFont.title": "Fonte da interface",
|
"settings.general.row.uiFont.title": "Fonte da interface",
|
||||||
"settings.general.row.uiFont.description": "Personalize a fonte usada em toda a interface",
|
"settings.general.row.uiFont.description": "Personalize a fonte usada em toda a interface",
|
||||||
"settings.general.row.followup.title": "Comportamento de acompanhamento",
|
"settings.general.row.followup.title": "Comportamento de acompanhamento",
|
||||||
@@ -590,6 +715,22 @@ export const dict = {
|
|||||||
"Escolha se os prompts de acompanhamento orientam imediatamente ou esperam na fila",
|
"Escolha se os prompts de acompanhamento orientam imediatamente ou esperam na fila",
|
||||||
"settings.general.row.followup.option.queue": "Fila",
|
"settings.general.row.followup.option.queue": "Fila",
|
||||||
"settings.general.row.followup.option.steer": "Orientar",
|
"settings.general.row.followup.option.steer": "Orientar",
|
||||||
|
"settings.general.row.showFileTree.title": "Árvore de arquivos",
|
||||||
|
"settings.general.row.showFileTree.description": "Mostrar o painel da árvore de arquivos nas sessões",
|
||||||
|
"settings.general.row.showNavigation.title": "Controles de navegação",
|
||||||
|
"settings.general.row.showNavigation.description":
|
||||||
|
"Mostrar os botões de voltar e avançar na barra de título do desktop",
|
||||||
|
"settings.general.row.showSearch.title": "Paleta de comandos",
|
||||||
|
"settings.general.row.showSearch.description": "Mostrar o botão de busca e paleta de comandos na barra de título",
|
||||||
|
"settings.general.row.showTerminal.title": "Terminal",
|
||||||
|
"settings.general.row.showTerminal.description": "Mostrar o botão do terminal na barra de título do desktop",
|
||||||
|
"settings.general.row.showStatus.title": "Status do servidor",
|
||||||
|
"settings.general.row.showStatus.description": "Mostrar o botão de status do servidor na barra de título",
|
||||||
|
"settings.general.row.mobileTitlebarBottom.title": "Navegação inferior",
|
||||||
|
"settings.general.row.mobileTitlebarBottom.description":
|
||||||
|
"Posicionar a barra de título e as abas da sessão na parte inferior da tela em dispositivos móveis",
|
||||||
|
"settings.general.row.showCustomAgents.title": "Agentes personalizados",
|
||||||
|
"settings.general.row.showCustomAgents.description": "Mostrar o seletor de agentes na área de composição",
|
||||||
"settings.general.row.reasoningSummaries.title": "Mostrar resumos de raciocínio",
|
"settings.general.row.reasoningSummaries.title": "Mostrar resumos de raciocínio",
|
||||||
"settings.general.row.reasoningSummaries.description": "Exibir resumos de raciocínio do modelo na linha do tempo",
|
"settings.general.row.reasoningSummaries.description": "Exibir resumos de raciocínio do modelo na linha do tempo",
|
||||||
"settings.general.row.shellToolPartsExpanded.title": "Expandir partes da ferramenta shell",
|
"settings.general.row.shellToolPartsExpanded.title": "Expandir partes da ferramenta shell",
|
||||||
@@ -598,6 +739,12 @@ export const dict = {
|
|||||||
"settings.general.row.editToolPartsExpanded.title": "Expandir partes da ferramenta de edição",
|
"settings.general.row.editToolPartsExpanded.title": "Expandir partes da ferramenta de edição",
|
||||||
"settings.general.row.editToolPartsExpanded.description":
|
"settings.general.row.editToolPartsExpanded.description":
|
||||||
"Mostrar partes das ferramentas de edição, escrita e patch expandidas por padrão na linha do tempo",
|
"Mostrar partes das ferramentas de edição, escrita e patch expandidas por padrão na linha do tempo",
|
||||||
|
"settings.general.row.newLayoutDesigns.title": "Novo layout e design",
|
||||||
|
"settings.general.row.newLayoutDesigns.description":
|
||||||
|
"Ativar o layout, a página inicial, a área de composição e a interface de sessão reformulados",
|
||||||
|
"settings.general.row.pinchZoom.title": "Zoom com gesto de pinça",
|
||||||
|
"settings.general.row.pinchZoom.description":
|
||||||
|
"Permitir gestos de pinça no trackpad e de Ctrl+rolagem para aplicar zoom",
|
||||||
"settings.general.row.wayland.title": "Usar Wayland nativo",
|
"settings.general.row.wayland.title": "Usar Wayland nativo",
|
||||||
"settings.general.row.wayland.description": "Desabilitar fallback X11 no Wayland. Requer reinicialização.",
|
"settings.general.row.wayland.description": "Desabilitar fallback X11 no Wayland. Requer reinicialização.",
|
||||||
"settings.general.row.wayland.tooltip":
|
"settings.general.row.wayland.tooltip":
|
||||||
@@ -610,6 +757,8 @@ export const dict = {
|
|||||||
"settings.updates.row.check.description": "Verificar atualizações manualmente e instalar se houver",
|
"settings.updates.row.check.description": "Verificar atualizações manualmente e instalar se houver",
|
||||||
"settings.updates.action.checkNow": "Verificar agora",
|
"settings.updates.action.checkNow": "Verificar agora",
|
||||||
"settings.updates.action.checking": "Verificando...",
|
"settings.updates.action.checking": "Verificando...",
|
||||||
|
"settings.updates.action.downloading": "Baixando...",
|
||||||
|
"settings.updates.action.installing": "Instalando...",
|
||||||
"settings.updates.toast.latest.title": "Você está atualizado",
|
"settings.updates.toast.latest.title": "Você está atualizado",
|
||||||
"settings.updates.toast.latest.description": "Você está usando a versão mais recente do OpenCode.",
|
"settings.updates.toast.latest.description": "Você está usando a versão mais recente do OpenCode.",
|
||||||
"sound.option.none": "Nenhum",
|
"sound.option.none": "Nenhum",
|
||||||
|
|||||||
+151
-2
@@ -23,6 +23,9 @@ export const dict = {
|
|||||||
|
|
||||||
"command.sidebar.toggle": "Prikaži/sakrij bočnu traku",
|
"command.sidebar.toggle": "Prikaži/sakrij bočnu traku",
|
||||||
"command.project.open": "Otvori projekat",
|
"command.project.open": "Otvori projekat",
|
||||||
|
"command.project.previous": "Prethodni projekat",
|
||||||
|
"command.project.next": "Sljedeći projekat",
|
||||||
|
"command.project.index": "Prebaci na projekat {{index}}",
|
||||||
"command.provider.connect": "Poveži provajdera",
|
"command.provider.connect": "Poveži provajdera",
|
||||||
"command.server.switch": "Promijeni server",
|
"command.server.switch": "Promijeni server",
|
||||||
"command.settings.open": "Otvori postavke",
|
"command.settings.open": "Otvori postavke",
|
||||||
@@ -45,6 +48,7 @@ export const dict = {
|
|||||||
"command.session.new": "Nova sesija",
|
"command.session.new": "Nova sesija",
|
||||||
"command.file.open": "Otvori datoteku",
|
"command.file.open": "Otvori datoteku",
|
||||||
"command.tab.close": "Zatvori karticu",
|
"command.tab.close": "Zatvori karticu",
|
||||||
|
"command.tab.reopenClosed": "Ponovo otvori zatvorenu karticu",
|
||||||
"command.context.addSelection": "Dodaj odabir u kontekst",
|
"command.context.addSelection": "Dodaj odabir u kontekst",
|
||||||
"command.context.addSelection.description": "Dodaj odabrane linije iz trenutne datoteke",
|
"command.context.addSelection.description": "Dodaj odabrane linije iz trenutne datoteke",
|
||||||
"command.input.focus": "Fokusiraj polje za unos",
|
"command.input.focus": "Fokusiraj polje za unos",
|
||||||
@@ -209,6 +213,11 @@ export const dict = {
|
|||||||
"model.input.video": "video",
|
"model.input.video": "video",
|
||||||
"model.input.pdf": "pdf",
|
"model.input.pdf": "pdf",
|
||||||
"model.tooltip.allows": "Podržava: {{inputs}}",
|
"model.tooltip.allows": "Podržava: {{inputs}}",
|
||||||
|
"model.tooltip.context.label": "Kontekst",
|
||||||
|
"model.tooltip.inputs": "Ulazi",
|
||||||
|
"model.tooltip.model": "Model",
|
||||||
|
"model.tooltip.provider": "Provajder",
|
||||||
|
"model.tooltip.reasoning": "Rasuđivanje",
|
||||||
"model.tooltip.reasoning.allowed": "Podržava rasuđivanje",
|
"model.tooltip.reasoning.allowed": "Podržava rasuđivanje",
|
||||||
"model.tooltip.reasoning.none": "Bez rasuđivanja",
|
"model.tooltip.reasoning.none": "Bez rasuđivanja",
|
||||||
"model.tooltip.context": "Limit konteksta {{limit}}",
|
"model.tooltip.context": "Limit konteksta {{limit}}",
|
||||||
@@ -219,6 +228,7 @@ export const dict = {
|
|||||||
"common.loading": "Učitavanje",
|
"common.loading": "Učitavanje",
|
||||||
"common.loading.ellipsis": "...",
|
"common.loading.ellipsis": "...",
|
||||||
"common.cancel": "Otkaži",
|
"common.cancel": "Otkaži",
|
||||||
|
"common.clear": "Očisti",
|
||||||
"common.connect": "Poveži",
|
"common.connect": "Poveži",
|
||||||
"common.disconnect": "Prekini vezu",
|
"common.disconnect": "Prekini vezu",
|
||||||
"common.continue": "Pošalji",
|
"common.continue": "Pošalji",
|
||||||
@@ -236,6 +246,8 @@ export const dict = {
|
|||||||
"prompt.mode.shell": "Shell",
|
"prompt.mode.shell": "Shell",
|
||||||
"prompt.mode.normal": "Prompt",
|
"prompt.mode.normal": "Prompt",
|
||||||
"prompt.mode.shell.exit": "esc za izlaz",
|
"prompt.mode.shell.exit": "esc za izlaz",
|
||||||
|
"session.child.promptDisabled": "Sesijama podagenta nije moguće slati upite.",
|
||||||
|
"session.child.backToParent": "Nazad na glavnu sesiju.",
|
||||||
|
|
||||||
"prompt.example.1": "Popravi TODO u bazi koda",
|
"prompt.example.1": "Popravi TODO u bazi koda",
|
||||||
"prompt.example.2": "Koji je tehnološki stack ovog projekta?",
|
"prompt.example.2": "Koji je tehnološki stack ovog projekta?",
|
||||||
@@ -342,6 +354,75 @@ export const dict = {
|
|||||||
"dialog.server.menu.delete": "Izbriši",
|
"dialog.server.menu.delete": "Izbriši",
|
||||||
"dialog.server.current": "Trenutni server",
|
"dialog.server.current": "Trenutni server",
|
||||||
"dialog.server.status.default": "Podrazumijevano",
|
"dialog.server.status.default": "Podrazumijevano",
|
||||||
|
"wsl.server.add": "Dodaj WSL server",
|
||||||
|
"wsl.server.addShort": "Dodaj WSL",
|
||||||
|
"wsl.server.label": "WSL",
|
||||||
|
"wsl.server.menu.label": "WSL server",
|
||||||
|
"wsl.server.retryStart": "Ponovi pokretanje",
|
||||||
|
"wsl.server.updating": "Ažuriranje...",
|
||||||
|
"wsl.onboarding.step.distro": "Odaberi distribuciju",
|
||||||
|
"wsl.onboarding.step.opencode": "OpenCode",
|
||||||
|
"wsl.onboarding.checkingRuntime": "Provjera WSL-a...",
|
||||||
|
"wsl.onboarding.restartRequired": "Potrebno je ponovo pokrenuti Windows da bi se završila instalacija WSL-a.",
|
||||||
|
"wsl.onboarding.ready": "WSL je spreman.",
|
||||||
|
"wsl.onboarding.required": "WSL je potreban za nastavak.",
|
||||||
|
"wsl.onboarding.checkingDistros": "Provjera distribucija...",
|
||||||
|
"wsl.onboarding.installingDistro": "Instaliranje {{distro}}...",
|
||||||
|
"wsl.onboarding.checkingDistro": "Provjera {{distro}}...",
|
||||||
|
"wsl.onboarding.listingDistros": "Učitavanje liste distribucija...",
|
||||||
|
"wsl.onboarding.distroReady": "Distribucija {{distro}} je spremna.",
|
||||||
|
"wsl.onboarding.distroNotInstalled": "Distribucija {{distro}} još nije instalirana.",
|
||||||
|
"wsl.onboarding.openDistroOnce": "Otvori {{distro}} jednom da završiš postavljanje.",
|
||||||
|
"wsl.onboarding.finishingDistro": "Završavanje postavljanja za {{distro}}.",
|
||||||
|
"wsl.onboarding.pickDistro": "Odaberi distribuciju ili instaliraj jednu ispod.",
|
||||||
|
"wsl.onboarding.checkingOpencode": "Provjera OpenCode...",
|
||||||
|
"wsl.onboarding.checkingOpencodeIn": "Provjera OpenCode u distribuciji {{distro}}...",
|
||||||
|
"wsl.onboarding.updatingOpencode": "Ažuriranje OpenCode...",
|
||||||
|
"wsl.onboarding.updatingOpencodeIn": "Ažuriranje OpenCode u distribuciji {{distro}}...",
|
||||||
|
"wsl.onboarding.updateOpencodeIn": "Ažuriraj OpenCode u distribuciji {{distro}}.",
|
||||||
|
"wsl.onboarding.updateOpencode": "Ažuriraj OpenCode",
|
||||||
|
"wsl.onboarding.opencodeReadyIn": "OpenCode je spreman u distribuciji {{distro}}.",
|
||||||
|
"wsl.onboarding.opencodeReady": "OpenCode je spreman.",
|
||||||
|
"wsl.onboarding.installOpencodeIn": "Instaliraj OpenCode u distribuciji {{distro}}.",
|
||||||
|
"wsl.onboarding.installOpencode": "Instaliraj OpenCode",
|
||||||
|
"wsl.onboarding.chooseDistroFirst": "Prvo odaberi distribuciju.",
|
||||||
|
"wsl.onboarding.loadFailed": "Nije uspjelo učitavanje WSL stanja.",
|
||||||
|
"wsl.onboarding.loading": "Učitavanje...",
|
||||||
|
"wsl.onboarding.installedDistros": "Instalirane distribucije",
|
||||||
|
"wsl.onboarding.checkAgain": "Provjeri ponovo",
|
||||||
|
"wsl.onboarding.distroStatus.ready": "Spremno",
|
||||||
|
"wsl.onboarding.distroStatus.checking": "Provjera...",
|
||||||
|
"wsl.onboarding.distroStatus.opencodeMissing": "OpenCode nije instaliran",
|
||||||
|
"wsl.onboarding.distroStatus.missingTools": "Nedostaju bash i curl",
|
||||||
|
"wsl.onboarding.distroStatus.unsupported": "Nije podržano · Koristi WSL 2",
|
||||||
|
"wsl.onboarding.needAnotherDistro": "Treba ti druga distribucija?",
|
||||||
|
"wsl.onboarding.needAnotherDistroHint": "Instaliraj Linux distribuciju iz WSL kataloga",
|
||||||
|
"wsl.onboarding.wslNotInstalled.title": "WSL nije instaliran",
|
||||||
|
"wsl.onboarding.wslNotInstalled.description":
|
||||||
|
"WSL (Windows Subsystem for Linux) je potreban da bi OpenCode mogao dodati WSL server",
|
||||||
|
"wsl.onboarding.wslUnavailable.title": "WSL nije dostupan",
|
||||||
|
"wsl.onboarding.wslUnavailable.description": "OpenCode nije mogao provjeriti WSL na ovom uređaju.",
|
||||||
|
"wsl.onboarding.installWsl": "Instaliraj WSL",
|
||||||
|
"wsl.onboarding.windowsRestartRequired":
|
||||||
|
"Ponovo pokreni Windows da završiš instalaciju WSL-a, zatim ponovo otvori OpenCode.",
|
||||||
|
"wsl.onboarding.next": "Dalje",
|
||||||
|
"wsl.onboarding.refresh": "Osvježi",
|
||||||
|
"wsl.onboarding.allDistrosAdded": "Sve instalirane distribucije su već dodane.",
|
||||||
|
"wsl.onboarding.noDistros": "Još nema otkrivenih distribucija.",
|
||||||
|
"wsl.onboarding.install": "Instaliraj",
|
||||||
|
"wsl.onboarding.installing": "Instaliranje...",
|
||||||
|
"wsl.onboarding.installDistro": "Instaliraj distribuciju",
|
||||||
|
"wsl.onboarding.searchDistros": "Pretraži distribucije",
|
||||||
|
"wsl.onboarding.wsl2Required": "Potreban je WSL 2.",
|
||||||
|
"wsl.onboarding.toolsRequired": "Ovoj distribuciji su potrebni bash i curl.",
|
||||||
|
"wsl.onboarding.openTerminal": "Otvori terminal",
|
||||||
|
"wsl.onboarding.path": "Putanja: {{path}}",
|
||||||
|
"wsl.onboarding.notFound": "nije pronađeno",
|
||||||
|
"wsl.onboarding.version": "Verzija: {{version}}",
|
||||||
|
"wsl.onboarding.unknown": "nepoznato",
|
||||||
|
"wsl.onboarding.desktopVersion": "desktop {{version}}",
|
||||||
|
"wsl.onboarding.versionMismatch": "Instalirana verzija ne odgovara verziji desktop aplikacije.",
|
||||||
|
"wsl.onboarding.adding": "Dodavanje...",
|
||||||
|
|
||||||
"dialog.project.edit.title": "Uredi projekat",
|
"dialog.project.edit.title": "Uredi projekat",
|
||||||
"dialog.project.edit.name": "Naziv",
|
"dialog.project.edit.name": "Naziv",
|
||||||
@@ -405,6 +486,7 @@ export const dict = {
|
|||||||
"language.no": "Norsk",
|
"language.no": "Norsk",
|
||||||
"language.br": "Português (Brasil)",
|
"language.br": "Português (Brasil)",
|
||||||
"language.bs": "Bosanski",
|
"language.bs": "Bosanski",
|
||||||
|
"language.uk": "Українська",
|
||||||
"language.th": "ไทย",
|
"language.th": "ไทย",
|
||||||
"language.tr": "Türkçe",
|
"language.tr": "Türkçe",
|
||||||
|
|
||||||
@@ -453,10 +535,12 @@ export const dict = {
|
|||||||
|
|
||||||
"error.page.title": "Nešto je pošlo po zlu",
|
"error.page.title": "Nešto je pošlo po zlu",
|
||||||
"error.page.description": "Došlo je do greške prilikom učitavanja aplikacije.",
|
"error.page.description": "Došlo je do greške prilikom učitavanja aplikacije.",
|
||||||
|
"error.page.description.localServerStartup": "Došlo je do greške pri pokretanju lokalnog servera.",
|
||||||
"error.page.details.label": "Detalji greške",
|
"error.page.details.label": "Detalji greške",
|
||||||
"error.page.action.restart": "Restartuj",
|
"error.page.action.restart": "Restartuj",
|
||||||
"error.page.action.report": "Prijavi grešku",
|
"error.page.action.report": "Prijavi grešku",
|
||||||
"error.page.action.reported": "Greška prijavljena",
|
"error.page.action.reported": "Greška prijavljena",
|
||||||
|
"error.page.action.exportLogs": "Izvezi logove",
|
||||||
"error.page.action.checking": "Provjera...",
|
"error.page.action.checking": "Provjera...",
|
||||||
"error.page.action.checkUpdates": "Provjeri ažuriranja",
|
"error.page.action.checkUpdates": "Provjeri ažuriranja",
|
||||||
"error.page.action.updateTo": "Ažuriraj na {{version}}",
|
"error.page.action.updateTo": "Ažuriraj na {{version}}",
|
||||||
@@ -504,11 +588,31 @@ export const dict = {
|
|||||||
"home.recentProjects": "Nedavni projekti",
|
"home.recentProjects": "Nedavni projekti",
|
||||||
"home.empty.title": "Nema nedavnih projekata",
|
"home.empty.title": "Nema nedavnih projekata",
|
||||||
"home.empty.description": "Kreni tako što ćeš otvoriti lokalni projekat",
|
"home.empty.description": "Kreni tako što ćeš otvoriti lokalni projekat",
|
||||||
|
"home.title": "Početna",
|
||||||
|
"home.projects": "Projekti",
|
||||||
|
"home.project.add": "Dodaj projekat",
|
||||||
|
"home.recentlyClosed": "Nedavno zatvoreni projekti",
|
||||||
|
"home.server.collapse": "Sažmi projekte servera",
|
||||||
|
"home.server.expand": "Proširi projekte servera",
|
||||||
|
"home.sessions.search.placeholder": "Pretraži sesije",
|
||||||
|
"home.sessions.search.placeholder.scoped": "Pretraži sesije u {{scope}}",
|
||||||
|
"home.sessions.search.sessions": "Sesije",
|
||||||
|
"home.sessions.search.noResults": "Nema pronađenih sesija za {{query}}",
|
||||||
|
"home.sessions.empty": "Ovdje još nema ničega",
|
||||||
|
"home.sessions.empty.description": "Kreiraj sesiju za početak",
|
||||||
|
"home.sessions.group.today": "Danas",
|
||||||
|
"home.sessions.group.yesterday": "Jučer",
|
||||||
|
"home.sessions.group.older": "Starije",
|
||||||
|
|
||||||
"session.tab.session": "Sesija",
|
"session.tab.session": "Sesija",
|
||||||
"session.tab.review": "Pregled",
|
"session.tab.review": "Pregled",
|
||||||
"session.tab.context": "Kontekst",
|
"session.tab.context": "Kontekst",
|
||||||
|
"session.tab.unknown": "Nepoznata sesija",
|
||||||
"session.panel.reviewAndFiles": "Pregled i datoteke",
|
"session.panel.reviewAndFiles": "Pregled i datoteke",
|
||||||
|
"session.error.notFound": "Ova sesija nije pronađena",
|
||||||
|
"session.error.notFound.description": "Ova kartica upućuje na sesiju koja više ne postoji na ovom serveru.",
|
||||||
|
"session.error.notFound.closeTab": "Zatvori karticu",
|
||||||
|
"session.error.serverConnection": "Nije moguće povezati se s ovim serverom",
|
||||||
"session.review.filesChanged": "Izmijenjeno {{count}} datoteka",
|
"session.review.filesChanged": "Izmijenjeno {{count}} datoteka",
|
||||||
"session.review.change.one": "Izmjena",
|
"session.review.change.one": "Izmjena",
|
||||||
"session.review.change.other": "Izmjene",
|
"session.review.change.other": "Izmjene",
|
||||||
@@ -518,6 +622,8 @@ export const dict = {
|
|||||||
"session.review.noSnapshot":
|
"session.review.noSnapshot":
|
||||||
"Praćenje snimaka (snapshot) je onemogućeno u konfiguraciji, pa promjene sesije nisu dostupne",
|
"Praćenje snimaka (snapshot) je onemogućeno u konfiguraciji, pa promjene sesije nisu dostupne",
|
||||||
"session.review.noChanges": "Nema izmjena",
|
"session.review.noChanges": "Nema izmjena",
|
||||||
|
"session.review.noUncommittedChanges": "Još nema nepotvrđenih promjena",
|
||||||
|
"session.review.noBranchChanges": "Još nema promjena na grani",
|
||||||
|
|
||||||
"session.files.selectToOpen": "Odaberi datoteku za otvaranje",
|
"session.files.selectToOpen": "Odaberi datoteku za otvaranje",
|
||||||
"session.files.all": "Sve datoteke",
|
"session.files.all": "Sve datoteke",
|
||||||
@@ -534,6 +640,10 @@ export const dict = {
|
|||||||
"session.todo.title": "Zadaci",
|
"session.todo.title": "Zadaci",
|
||||||
"session.todo.collapse": "Sažmi",
|
"session.todo.collapse": "Sažmi",
|
||||||
"session.todo.expand": "Proširi",
|
"session.todo.expand": "Proširi",
|
||||||
|
"session.question.minimize": "Minimiziraj pitanje",
|
||||||
|
"session.question.restore": "Vrati pitanje",
|
||||||
|
"session.question.pending.one": "{{count}} pitanje na čekanju",
|
||||||
|
"session.question.pending.other": "{{count}} pitanja na čekanju",
|
||||||
"session.followupDock.summary.one": "{{count}} poruka na čekanju",
|
"session.followupDock.summary.one": "{{count}} poruka na čekanju",
|
||||||
"session.followupDock.summary.other": "{{count}} poruka na čekanju",
|
"session.followupDock.summary.other": "{{count}} poruka na čekanju",
|
||||||
"session.followupDock.sendNow": "Pošalji sada",
|
"session.followupDock.sendNow": "Pošalji sada",
|
||||||
@@ -547,9 +657,16 @@ export const dict = {
|
|||||||
"session.revertDock.restore": "Vrati poruku",
|
"session.revertDock.restore": "Vrati poruku",
|
||||||
|
|
||||||
"session.new.title": "Napravi bilo šta",
|
"session.new.title": "Napravi bilo šta",
|
||||||
|
"session.new.project.new": "Novi projekat",
|
||||||
|
"session.new.project.search": "Pretraži projekte",
|
||||||
|
"session.new.project.add": "Dodaj projekat",
|
||||||
"session.new.worktree.main": "Glavna grana",
|
"session.new.worktree.main": "Glavna grana",
|
||||||
"session.new.worktree.mainWithBranch": "Glavna grana ({{branch}})",
|
"session.new.worktree.mainWithBranch": "Glavna grana ({{branch}})",
|
||||||
"session.new.worktree.create": "Kreiraj novi worktree",
|
"session.new.worktree.create": "Kreiraj novi worktree",
|
||||||
|
"session.new.workspace.runIn": "Pokreni sesiju u",
|
||||||
|
"session.new.workspace.triggerLocal": "Lokalno",
|
||||||
|
"session.new.workspace.local": "Lokalni repozitorij",
|
||||||
|
"session.new.workspace.existing": "Radni prostor…",
|
||||||
"session.new.lastModified": "Posljednja izmjena",
|
"session.new.lastModified": "Posljednja izmjena",
|
||||||
|
|
||||||
"session.header.search.placeholder": "Pretraži {{project}}",
|
"session.header.search.placeholder": "Pretraži {{project}}",
|
||||||
@@ -618,6 +735,8 @@ export const dict = {
|
|||||||
"sidebar.project.recentSessions": "Nedavne sesije",
|
"sidebar.project.recentSessions": "Nedavne sesije",
|
||||||
"sidebar.project.viewAllSessions": "Prikaži sve sesije",
|
"sidebar.project.viewAllSessions": "Prikaži sve sesije",
|
||||||
"sidebar.project.clearNotifications": "Očisti obavijesti",
|
"sidebar.project.clearNotifications": "Očisti obavijesti",
|
||||||
|
"sidebar.empty.title": "Nema otvorenih projekata",
|
||||||
|
"sidebar.empty.description": "Otvori projekat za početak",
|
||||||
|
|
||||||
"app.name.desktop": "OpenCode Desktop",
|
"app.name.desktop": "OpenCode Desktop",
|
||||||
|
|
||||||
@@ -630,6 +749,7 @@ export const dict = {
|
|||||||
"settings.desktop.wsl.description": "Pokreni OpenCode server unutar WSL-a na Windowsu.",
|
"settings.desktop.wsl.description": "Pokreni OpenCode server unutar WSL-a na Windowsu.",
|
||||||
|
|
||||||
"settings.general.section.appearance": "Izgled",
|
"settings.general.section.appearance": "Izgled",
|
||||||
|
"settings.general.section.advanced": "Napredno",
|
||||||
"settings.general.section.notifications": "Sistemske obavijesti",
|
"settings.general.section.notifications": "Sistemske obavijesti",
|
||||||
"settings.general.section.updates": "Ažuriranja",
|
"settings.general.section.updates": "Ažuriranja",
|
||||||
"settings.general.section.sounds": "Zvučni efekti",
|
"settings.general.section.sounds": "Zvučni efekti",
|
||||||
@@ -638,6 +758,11 @@ export const dict = {
|
|||||||
|
|
||||||
"settings.general.row.language.title": "Jezik",
|
"settings.general.row.language.title": "Jezik",
|
||||||
"settings.general.row.language.description": "Promijeni jezik prikaza u OpenCode-u",
|
"settings.general.row.language.description": "Promijeni jezik prikaza u OpenCode-u",
|
||||||
|
"settings.general.row.shell.title": "Shell terminala",
|
||||||
|
"settings.general.row.shell.description":
|
||||||
|
"Odaberi shell koji se koristi za terminal. Kompatibilni shellovi koriste se i za pozive alata agenta.",
|
||||||
|
"settings.general.row.shell.autoDefault": "Automatski (podrazumijevano)",
|
||||||
|
"settings.general.row.shell.terminalOnly": "samo terminal",
|
||||||
"settings.general.row.appearance.title": "Izgled",
|
"settings.general.row.appearance.title": "Izgled",
|
||||||
"settings.general.row.appearance.description": "Prilagodi kako OpenCode izgleda na tvom uređaju",
|
"settings.general.row.appearance.description": "Prilagodi kako OpenCode izgleda na tvom uređaju",
|
||||||
"settings.general.row.colorScheme.title": "Šema boja",
|
"settings.general.row.colorScheme.title": "Šema boja",
|
||||||
@@ -646,14 +771,30 @@ export const dict = {
|
|||||||
"settings.general.row.theme.description": "Prilagodi temu OpenCode-a.",
|
"settings.general.row.theme.description": "Prilagodi temu OpenCode-a.",
|
||||||
"settings.general.row.font.title": "Font za kod",
|
"settings.general.row.font.title": "Font za kod",
|
||||||
"settings.general.row.font.description": "Prilagodi font koji se koristi u blokovima koda",
|
"settings.general.row.font.description": "Prilagodi font koji se koristi u blokovima koda",
|
||||||
"settings.general.row.terminalFont.title": "Terminal Font",
|
"settings.general.row.terminalFont.title": "Font terminala",
|
||||||
"settings.general.row.terminalFont.description": "Customise the font used in the terminal",
|
"settings.general.row.terminalFont.description": "Prilagodite font koji se koristi u terminalu",
|
||||||
"settings.general.row.uiFont.title": "UI font",
|
"settings.general.row.uiFont.title": "UI font",
|
||||||
"settings.general.row.uiFont.description": "Prilagodi font koji se koristi u cijelom interfejsu",
|
"settings.general.row.uiFont.description": "Prilagodi font koji se koristi u cijelom interfejsu",
|
||||||
"settings.general.row.followup.title": "Ponašanje nadovezivanja",
|
"settings.general.row.followup.title": "Ponašanje nadovezivanja",
|
||||||
"settings.general.row.followup.description": "Odaberi da li upiti nadovezivanja usmjeravaju odmah ili čekaju u redu",
|
"settings.general.row.followup.description": "Odaberi da li upiti nadovezivanja usmjeravaju odmah ili čekaju u redu",
|
||||||
"settings.general.row.followup.option.queue": "Red čekanja",
|
"settings.general.row.followup.option.queue": "Red čekanja",
|
||||||
"settings.general.row.followup.option.steer": "Usmjeri",
|
"settings.general.row.followup.option.steer": "Usmjeri",
|
||||||
|
"settings.general.row.showFileTree.title": "Stablo datoteka",
|
||||||
|
"settings.general.row.showFileTree.description": "Prikaži stablo datoteka u sesijama",
|
||||||
|
"settings.general.row.showNavigation.title": "Kontrole navigacije",
|
||||||
|
"settings.general.row.showNavigation.description":
|
||||||
|
"Prikaži dugmad za nazad i naprijed u naslovnoj traci desktop aplikacije",
|
||||||
|
"settings.general.row.showSearch.title": "Paleta komandi",
|
||||||
|
"settings.general.row.showSearch.description": "Prikaži dugme za pretragu i paletu komandi u naslovnoj traci",
|
||||||
|
"settings.general.row.showTerminal.title": "Terminal",
|
||||||
|
"settings.general.row.showTerminal.description": "Prikaži dugme terminala u naslovnoj traci desktop aplikacije",
|
||||||
|
"settings.general.row.showStatus.title": "Status servera",
|
||||||
|
"settings.general.row.showStatus.description": "Prikaži dugme statusa servera u naslovnoj traci",
|
||||||
|
"settings.general.row.mobileTitlebarBottom.title": "Donja navigacija",
|
||||||
|
"settings.general.row.mobileTitlebarBottom.description":
|
||||||
|
"Postavi naslovnu traku i kartice sesije na dno ekrana na mobilnim uređajima",
|
||||||
|
"settings.general.row.showCustomAgents.title": "Prilagođeni agenti",
|
||||||
|
"settings.general.row.showCustomAgents.description": "Prikaži izbor agenta u uređivaču poruke",
|
||||||
"settings.general.row.reasoningSummaries.title": "Prikaži sažetke rasuđivanja",
|
"settings.general.row.reasoningSummaries.title": "Prikaži sažetke rasuđivanja",
|
||||||
"settings.general.row.reasoningSummaries.description": "Prikaži sažetke rasuđivanja modela na vremenskoj traci",
|
"settings.general.row.reasoningSummaries.description": "Prikaži sažetke rasuđivanja modela na vremenskoj traci",
|
||||||
|
|
||||||
@@ -663,6 +804,12 @@ export const dict = {
|
|||||||
"settings.general.row.editToolPartsExpanded.title": "Proširi dijelove alata za uređivanje",
|
"settings.general.row.editToolPartsExpanded.title": "Proširi dijelove alata za uređivanje",
|
||||||
"settings.general.row.editToolPartsExpanded.description":
|
"settings.general.row.editToolPartsExpanded.description":
|
||||||
"Prikaži dijelove alata za uređivanje, pisanje i patch podrazumijevano proširene na vremenskoj traci",
|
"Prikaži dijelove alata za uređivanje, pisanje i patch podrazumijevano proširene na vremenskoj traci",
|
||||||
|
"settings.general.row.newLayoutDesigns.title": "Novi raspored i dizajn",
|
||||||
|
"settings.general.row.newLayoutDesigns.description":
|
||||||
|
"Omogući redizajnirani raspored, početnu stranicu, uređivač poruke i interfejs sesije",
|
||||||
|
"settings.general.row.pinchZoom.title": "Zumiranje štipanjem",
|
||||||
|
"settings.general.row.pinchZoom.description":
|
||||||
|
"Dozvoli zumiranje gestom štipanja na dodirnoj ploči i pomoću Ctrl-pomjeranja",
|
||||||
"settings.general.row.wayland.title": "Koristi nativni Wayland",
|
"settings.general.row.wayland.title": "Koristi nativni Wayland",
|
||||||
"settings.general.row.wayland.description": "Onemogući X11 fallback na Waylandu. Zahtijeva restart.",
|
"settings.general.row.wayland.description": "Onemogući X11 fallback na Waylandu. Zahtijeva restart.",
|
||||||
"settings.general.row.wayland.tooltip":
|
"settings.general.row.wayland.tooltip":
|
||||||
@@ -677,6 +824,8 @@ export const dict = {
|
|||||||
"settings.updates.row.check.description": "Ručno provjeri ažuriranja i instaliraj ako su dostupna",
|
"settings.updates.row.check.description": "Ručno provjeri ažuriranja i instaliraj ako su dostupna",
|
||||||
"settings.updates.action.checkNow": "Provjeri sada",
|
"settings.updates.action.checkNow": "Provjeri sada",
|
||||||
"settings.updates.action.checking": "Provjera...",
|
"settings.updates.action.checking": "Provjera...",
|
||||||
|
"settings.updates.action.downloading": "Preuzimanje...",
|
||||||
|
"settings.updates.action.installing": "Instaliranje...",
|
||||||
"settings.updates.toast.latest.title": "Sve je ažurno",
|
"settings.updates.toast.latest.title": "Sve je ažurno",
|
||||||
"settings.updates.toast.latest.description": "Koristiš najnoviju verziju OpenCode-a.",
|
"settings.updates.toast.latest.description": "Koristiš najnoviju verziju OpenCode-a.",
|
||||||
"sound.option.none": "Nijedan",
|
"sound.option.none": "Nijedan",
|
||||||
|
|||||||
+149
-2
@@ -23,6 +23,9 @@ export const dict = {
|
|||||||
|
|
||||||
"command.sidebar.toggle": "Skift sidebjælke",
|
"command.sidebar.toggle": "Skift sidebjælke",
|
||||||
"command.project.open": "Åbn projekt",
|
"command.project.open": "Åbn projekt",
|
||||||
|
"command.project.previous": "Forrige projekt",
|
||||||
|
"command.project.next": "Næste projekt",
|
||||||
|
"command.project.index": "Skift til projekt {{index}}",
|
||||||
"command.provider.connect": "Tilslut udbyder",
|
"command.provider.connect": "Tilslut udbyder",
|
||||||
"command.server.switch": "Skift server",
|
"command.server.switch": "Skift server",
|
||||||
"command.settings.open": "Åbn indstillinger",
|
"command.settings.open": "Åbn indstillinger",
|
||||||
@@ -45,6 +48,7 @@ export const dict = {
|
|||||||
"command.session.new": "Ny session",
|
"command.session.new": "Ny session",
|
||||||
"command.file.open": "Åbn fil",
|
"command.file.open": "Åbn fil",
|
||||||
"command.tab.close": "Luk fane",
|
"command.tab.close": "Luk fane",
|
||||||
|
"command.tab.reopenClosed": "Åbn lukket fane igen",
|
||||||
"command.context.addSelection": "Tilføj markering til kontekst",
|
"command.context.addSelection": "Tilføj markering til kontekst",
|
||||||
"command.context.addSelection.description": "Tilføj markerede linjer fra den aktuelle fil",
|
"command.context.addSelection.description": "Tilføj markerede linjer fra den aktuelle fil",
|
||||||
"command.input.focus": "Fokuser inputfelt",
|
"command.input.focus": "Fokuser inputfelt",
|
||||||
@@ -208,6 +212,11 @@ export const dict = {
|
|||||||
"model.input.video": "video",
|
"model.input.video": "video",
|
||||||
"model.input.pdf": "pdf",
|
"model.input.pdf": "pdf",
|
||||||
"model.tooltip.allows": "Tillader: {{inputs}}",
|
"model.tooltip.allows": "Tillader: {{inputs}}",
|
||||||
|
"model.tooltip.context.label": "Kontekst",
|
||||||
|
"model.tooltip.inputs": "Input",
|
||||||
|
"model.tooltip.model": "Model",
|
||||||
|
"model.tooltip.provider": "Udbyder",
|
||||||
|
"model.tooltip.reasoning": "Tænkning",
|
||||||
"model.tooltip.reasoning.allowed": "Tillader tænkning",
|
"model.tooltip.reasoning.allowed": "Tillader tænkning",
|
||||||
"model.tooltip.reasoning.none": "Ingen tænkning",
|
"model.tooltip.reasoning.none": "Ingen tænkning",
|
||||||
"model.tooltip.context": "Kontekstgrænse {{limit}}",
|
"model.tooltip.context": "Kontekstgrænse {{limit}}",
|
||||||
@@ -217,6 +226,7 @@ export const dict = {
|
|||||||
"common.loading": "Indlæser",
|
"common.loading": "Indlæser",
|
||||||
"common.loading.ellipsis": "...",
|
"common.loading.ellipsis": "...",
|
||||||
"common.cancel": "Annuller",
|
"common.cancel": "Annuller",
|
||||||
|
"common.clear": "Ryd",
|
||||||
"common.connect": "Forbind",
|
"common.connect": "Forbind",
|
||||||
"common.disconnect": "Frakobl",
|
"common.disconnect": "Frakobl",
|
||||||
"common.continue": "Indsend",
|
"common.continue": "Indsend",
|
||||||
@@ -234,6 +244,8 @@ export const dict = {
|
|||||||
"prompt.mode.shell": "Shell",
|
"prompt.mode.shell": "Shell",
|
||||||
"prompt.mode.normal": "Prompt",
|
"prompt.mode.normal": "Prompt",
|
||||||
"prompt.mode.shell.exit": "esc for at afslutte",
|
"prompt.mode.shell.exit": "esc for at afslutte",
|
||||||
|
"session.child.promptDisabled": "Der kan ikke sendes prompts til underagentsessioner.",
|
||||||
|
"session.child.backToParent": "Tilbage til hovedsessionen.",
|
||||||
|
|
||||||
"prompt.example.1": "Ret en TODO i koden",
|
"prompt.example.1": "Ret en TODO i koden",
|
||||||
"prompt.example.2": "Hvad er teknologistakken for dette projekt?",
|
"prompt.example.2": "Hvad er teknologistakken for dette projekt?",
|
||||||
@@ -340,6 +352,75 @@ export const dict = {
|
|||||||
"dialog.server.menu.delete": "Slet",
|
"dialog.server.menu.delete": "Slet",
|
||||||
"dialog.server.current": "Nuværende server",
|
"dialog.server.current": "Nuværende server",
|
||||||
"dialog.server.status.default": "Standard",
|
"dialog.server.status.default": "Standard",
|
||||||
|
"wsl.server.add": "Tilføj WSL-server",
|
||||||
|
"wsl.server.addShort": "Tilføj WSL",
|
||||||
|
"wsl.server.label": "WSL",
|
||||||
|
"wsl.server.menu.label": "WSL-server",
|
||||||
|
"wsl.server.retryStart": "Prøv at starte igen",
|
||||||
|
"wsl.server.updating": "Opdaterer...",
|
||||||
|
"wsl.onboarding.step.distro": "Vælg distribution",
|
||||||
|
"wsl.onboarding.step.opencode": "OpenCode",
|
||||||
|
"wsl.onboarding.checkingRuntime": "Tjekker WSL...",
|
||||||
|
"wsl.onboarding.restartRequired": "Windows skal genstartes for at fuldføre installationen af WSL.",
|
||||||
|
"wsl.onboarding.ready": "WSL er klar.",
|
||||||
|
"wsl.onboarding.required": "WSL er påkrævet for at fortsætte.",
|
||||||
|
"wsl.onboarding.checkingDistros": "Tjekker distributioner...",
|
||||||
|
"wsl.onboarding.installingDistro": "Installerer {{distro}}...",
|
||||||
|
"wsl.onboarding.checkingDistro": "Tjekker {{distro}}...",
|
||||||
|
"wsl.onboarding.listingDistros": "Henter liste over distributioner...",
|
||||||
|
"wsl.onboarding.distroReady": "{{distro}} er klar.",
|
||||||
|
"wsl.onboarding.distroNotInstalled": "{{distro}} er ikke installeret endnu.",
|
||||||
|
"wsl.onboarding.openDistroOnce": "Åbn {{distro}} én gang for at fuldføre opsætningen.",
|
||||||
|
"wsl.onboarding.finishingDistro": "Fuldfører opsætningen af {{distro}}.",
|
||||||
|
"wsl.onboarding.pickDistro": "Vælg en distribution, eller installer en nedenfor.",
|
||||||
|
"wsl.onboarding.checkingOpencode": "Tjekker OpenCode...",
|
||||||
|
"wsl.onboarding.checkingOpencodeIn": "Tjekker OpenCode i {{distro}}...",
|
||||||
|
"wsl.onboarding.updatingOpencode": "Opdaterer OpenCode...",
|
||||||
|
"wsl.onboarding.updatingOpencodeIn": "Opdaterer OpenCode i {{distro}}...",
|
||||||
|
"wsl.onboarding.updateOpencodeIn": "Opdater OpenCode i {{distro}}.",
|
||||||
|
"wsl.onboarding.updateOpencode": "Opdater OpenCode",
|
||||||
|
"wsl.onboarding.opencodeReadyIn": "OpenCode er klar i {{distro}}.",
|
||||||
|
"wsl.onboarding.opencodeReady": "OpenCode er klar.",
|
||||||
|
"wsl.onboarding.installOpencodeIn": "Installer OpenCode i {{distro}}.",
|
||||||
|
"wsl.onboarding.installOpencode": "Installer OpenCode",
|
||||||
|
"wsl.onboarding.chooseDistroFirst": "Vælg først en distribution.",
|
||||||
|
"wsl.onboarding.loadFailed": "Kunne ikke indlæse WSL-status.",
|
||||||
|
"wsl.onboarding.loading": "Indlæser...",
|
||||||
|
"wsl.onboarding.installedDistros": "Installerede distributioner",
|
||||||
|
"wsl.onboarding.checkAgain": "Tjek igen",
|
||||||
|
"wsl.onboarding.distroStatus.ready": "Klar",
|
||||||
|
"wsl.onboarding.distroStatus.checking": "Tjekker...",
|
||||||
|
"wsl.onboarding.distroStatus.opencodeMissing": "OpenCode er ikke installeret",
|
||||||
|
"wsl.onboarding.distroStatus.missingTools": "Mangler bash, curl",
|
||||||
|
"wsl.onboarding.distroStatus.unsupported": "Ikke understøttet · Brug WSL 2",
|
||||||
|
"wsl.onboarding.needAnotherDistro": "Har du brug for en anden distribution?",
|
||||||
|
"wsl.onboarding.needAnotherDistroHint": "Installer en Linux-distribution fra WSL-kataloget",
|
||||||
|
"wsl.onboarding.wslNotInstalled.title": "WSL er ikke installeret",
|
||||||
|
"wsl.onboarding.wslNotInstalled.description":
|
||||||
|
"WSL (Windows Subsystem for Linux) er påkrævet, før OpenCode kan tilføje en WSL-server",
|
||||||
|
"wsl.onboarding.wslUnavailable.title": "WSL er ikke tilgængelig",
|
||||||
|
"wsl.onboarding.wslUnavailable.description": "OpenCode kunne ikke bekræfte WSL på denne maskine.",
|
||||||
|
"wsl.onboarding.installWsl": "Installer WSL",
|
||||||
|
"wsl.onboarding.windowsRestartRequired":
|
||||||
|
"Genstart Windows for at fuldføre installationen af WSL, og åbn derefter OpenCode igen.",
|
||||||
|
"wsl.onboarding.next": "Næste",
|
||||||
|
"wsl.onboarding.refresh": "Opdater",
|
||||||
|
"wsl.onboarding.allDistrosAdded": "Alle installerede distributioner er allerede tilføjet.",
|
||||||
|
"wsl.onboarding.noDistros": "Ingen distributioner fundet endnu.",
|
||||||
|
"wsl.onboarding.install": "Installer",
|
||||||
|
"wsl.onboarding.installing": "Installerer...",
|
||||||
|
"wsl.onboarding.installDistro": "Installer distribution",
|
||||||
|
"wsl.onboarding.searchDistros": "Søg efter distributioner",
|
||||||
|
"wsl.onboarding.wsl2Required": "WSL 2 er påkrævet.",
|
||||||
|
"wsl.onboarding.toolsRequired": "Denne distribution kræver bash og curl.",
|
||||||
|
"wsl.onboarding.openTerminal": "Åbn terminal",
|
||||||
|
"wsl.onboarding.path": "Sti: {{path}}",
|
||||||
|
"wsl.onboarding.notFound": "ikke fundet",
|
||||||
|
"wsl.onboarding.version": "Version: {{version}}",
|
||||||
|
"wsl.onboarding.unknown": "ukendt",
|
||||||
|
"wsl.onboarding.desktopVersion": "desktop {{version}}",
|
||||||
|
"wsl.onboarding.versionMismatch": "Den installerede version matcher ikke desktopappens version.",
|
||||||
|
"wsl.onboarding.adding": "Tilføjer...",
|
||||||
|
|
||||||
"dialog.project.edit.title": "Rediger projekt",
|
"dialog.project.edit.title": "Rediger projekt",
|
||||||
"dialog.project.edit.name": "Navn",
|
"dialog.project.edit.name": "Navn",
|
||||||
@@ -403,6 +484,7 @@ export const dict = {
|
|||||||
"language.no": "Norsk",
|
"language.no": "Norsk",
|
||||||
"language.br": "Português (Brasil)",
|
"language.br": "Português (Brasil)",
|
||||||
"language.bs": "Bosanski",
|
"language.bs": "Bosanski",
|
||||||
|
"language.uk": "Українська",
|
||||||
"language.th": "ไทย",
|
"language.th": "ไทย",
|
||||||
"language.tr": "Türkçe",
|
"language.tr": "Türkçe",
|
||||||
|
|
||||||
@@ -450,10 +532,12 @@ export const dict = {
|
|||||||
|
|
||||||
"error.page.title": "Noget gik galt",
|
"error.page.title": "Noget gik galt",
|
||||||
"error.page.description": "Der opstod en fejl under indlæsning af applikationen.",
|
"error.page.description": "Der opstod en fejl under indlæsning af applikationen.",
|
||||||
|
"error.page.description.localServerStartup": "Der opstod en fejl under start af den lokale server.",
|
||||||
"error.page.details.label": "Fejldetaljer",
|
"error.page.details.label": "Fejldetaljer",
|
||||||
"error.page.action.restart": "Genstart",
|
"error.page.action.restart": "Genstart",
|
||||||
"error.page.action.report": "Rapportér fejl",
|
"error.page.action.report": "Rapportér fejl",
|
||||||
"error.page.action.reported": "Fejl rapporteret",
|
"error.page.action.reported": "Fejl rapporteret",
|
||||||
|
"error.page.action.exportLogs": "Eksportér logfiler",
|
||||||
"error.page.action.checking": "Tjekker...",
|
"error.page.action.checking": "Tjekker...",
|
||||||
"error.page.action.checkUpdates": "Tjek for opdateringer",
|
"error.page.action.checkUpdates": "Tjek for opdateringer",
|
||||||
"error.page.action.updateTo": "Opdater til {{version}}",
|
"error.page.action.updateTo": "Opdater til {{version}}",
|
||||||
@@ -501,11 +585,31 @@ export const dict = {
|
|||||||
"home.recentProjects": "Seneste projekter",
|
"home.recentProjects": "Seneste projekter",
|
||||||
"home.empty.title": "Ingen seneste projekter",
|
"home.empty.title": "Ingen seneste projekter",
|
||||||
"home.empty.description": "Kom i gang ved at åbne et lokalt projekt",
|
"home.empty.description": "Kom i gang ved at åbne et lokalt projekt",
|
||||||
|
"home.title": "Hjem",
|
||||||
|
"home.projects": "Projekter",
|
||||||
|
"home.project.add": "Tilføj projekt",
|
||||||
|
"home.recentlyClosed": "Lukket for nylig",
|
||||||
|
"home.server.collapse": "Skjul serverprojekter",
|
||||||
|
"home.server.expand": "Udvid serverprojekter",
|
||||||
|
"home.sessions.search.placeholder": "Søg i sessioner",
|
||||||
|
"home.sessions.search.placeholder.scoped": "Søg i sessioner i {{scope}}",
|
||||||
|
"home.sessions.search.sessions": "Sessioner",
|
||||||
|
"home.sessions.search.noResults": "Ingen sessioner fundet for {{query}}",
|
||||||
|
"home.sessions.empty": "Intet her endnu",
|
||||||
|
"home.sessions.empty.description": "Opret en session for at komme i gang",
|
||||||
|
"home.sessions.group.today": "I dag",
|
||||||
|
"home.sessions.group.yesterday": "I går",
|
||||||
|
"home.sessions.group.older": "Ældre",
|
||||||
|
|
||||||
"session.tab.session": "Session",
|
"session.tab.session": "Session",
|
||||||
"session.tab.review": "Gennemgang",
|
"session.tab.review": "Gennemgang",
|
||||||
"session.tab.context": "Kontekst",
|
"session.tab.context": "Kontekst",
|
||||||
|
"session.tab.unknown": "Ukendt session",
|
||||||
"session.panel.reviewAndFiles": "Gennemgang og filer",
|
"session.panel.reviewAndFiles": "Gennemgang og filer",
|
||||||
|
"session.error.notFound": "Denne session kan ikke findes",
|
||||||
|
"session.error.notFound.description": "Denne fane henviser til en session, der ikke længere findes på denne server.",
|
||||||
|
"session.error.notFound.closeTab": "Luk fane",
|
||||||
|
"session.error.serverConnection": "Kan ikke oprette forbindelse til denne server",
|
||||||
"session.review.filesChanged": "{{count}} Filer ændret",
|
"session.review.filesChanged": "{{count}} Filer ændret",
|
||||||
"session.review.change.one": "Ændring",
|
"session.review.change.one": "Ændring",
|
||||||
"session.review.change.other": "Ændringer",
|
"session.review.change.other": "Ændringer",
|
||||||
@@ -515,6 +619,8 @@ export const dict = {
|
|||||||
"session.review.noSnapshot":
|
"session.review.noSnapshot":
|
||||||
"Snapshot-sporing er deaktiveret i konfigurationen, så sessionsændringer er ikke tilgængelige",
|
"Snapshot-sporing er deaktiveret i konfigurationen, så sessionsændringer er ikke tilgængelige",
|
||||||
"session.review.noChanges": "Ingen ændringer",
|
"session.review.noChanges": "Ingen ændringer",
|
||||||
|
"session.review.noUncommittedChanges": "Ingen ændringer uden commit endnu",
|
||||||
|
"session.review.noBranchChanges": "Ingen grenændringer endnu",
|
||||||
"session.files.selectToOpen": "Vælg en fil at åbne",
|
"session.files.selectToOpen": "Vælg en fil at åbne",
|
||||||
"session.files.all": "Alle filer",
|
"session.files.all": "Alle filer",
|
||||||
"session.files.empty": "Ingen filer",
|
"session.files.empty": "Ingen filer",
|
||||||
@@ -529,6 +635,10 @@ export const dict = {
|
|||||||
"session.todo.title": "Opgaver",
|
"session.todo.title": "Opgaver",
|
||||||
"session.todo.collapse": "Skjul",
|
"session.todo.collapse": "Skjul",
|
||||||
"session.todo.expand": "Udvid",
|
"session.todo.expand": "Udvid",
|
||||||
|
"session.question.minimize": "Minimer spørgsmål",
|
||||||
|
"session.question.restore": "Gendan spørgsmål",
|
||||||
|
"session.question.pending.one": "{{count}} afventende spørgsmål",
|
||||||
|
"session.question.pending.other": "{{count}} afventende spørgsmål",
|
||||||
"session.followupDock.summary.one": "{{count}} besked i kø",
|
"session.followupDock.summary.one": "{{count}} besked i kø",
|
||||||
"session.followupDock.summary.other": "{{count}} beskeder i kø",
|
"session.followupDock.summary.other": "{{count}} beskeder i kø",
|
||||||
"session.followupDock.sendNow": "Send nu",
|
"session.followupDock.sendNow": "Send nu",
|
||||||
@@ -542,9 +652,16 @@ export const dict = {
|
|||||||
"session.revertDock.restore": "Gendan besked",
|
"session.revertDock.restore": "Gendan besked",
|
||||||
|
|
||||||
"session.new.title": "Byg hvad som helst",
|
"session.new.title": "Byg hvad som helst",
|
||||||
|
"session.new.project.new": "Nyt projekt",
|
||||||
|
"session.new.project.search": "Søg efter projekter",
|
||||||
|
"session.new.project.add": "Tilføj projekt",
|
||||||
"session.new.worktree.main": "Hovedgren",
|
"session.new.worktree.main": "Hovedgren",
|
||||||
"session.new.worktree.mainWithBranch": "Hovedgren ({{branch}})",
|
"session.new.worktree.mainWithBranch": "Hovedgren ({{branch}})",
|
||||||
"session.new.worktree.create": "Opret nyt worktree",
|
"session.new.worktree.create": "Opret nyt worktree",
|
||||||
|
"session.new.workspace.runIn": "Kør session i",
|
||||||
|
"session.new.workspace.triggerLocal": "Lokal",
|
||||||
|
"session.new.workspace.local": "Lokalt repository",
|
||||||
|
"session.new.workspace.existing": "Arbejdsområde…",
|
||||||
"session.new.lastModified": "Sidst ændret",
|
"session.new.lastModified": "Sidst ændret",
|
||||||
|
|
||||||
"session.header.search.placeholder": "Søg {{project}}",
|
"session.header.search.placeholder": "Søg {{project}}",
|
||||||
@@ -614,6 +731,8 @@ export const dict = {
|
|||||||
"sidebar.project.recentSessions": "Seneste sessioner",
|
"sidebar.project.recentSessions": "Seneste sessioner",
|
||||||
"sidebar.project.viewAllSessions": "Vis alle sessioner",
|
"sidebar.project.viewAllSessions": "Vis alle sessioner",
|
||||||
"sidebar.project.clearNotifications": "Ryd notifikationer",
|
"sidebar.project.clearNotifications": "Ryd notifikationer",
|
||||||
|
"sidebar.empty.title": "Ingen åbne projekter",
|
||||||
|
"sidebar.empty.description": "Åbn et projekt for at komme i gang",
|
||||||
|
|
||||||
"app.name.desktop": "OpenCode Desktop",
|
"app.name.desktop": "OpenCode Desktop",
|
||||||
"settings.section.desktop": "Desktop",
|
"settings.section.desktop": "Desktop",
|
||||||
@@ -625,6 +744,7 @@ export const dict = {
|
|||||||
"settings.desktop.wsl.description": "Kør OpenCode-serveren inde i WSL på Windows.",
|
"settings.desktop.wsl.description": "Kør OpenCode-serveren inde i WSL på Windows.",
|
||||||
|
|
||||||
"settings.general.section.appearance": "Udseende",
|
"settings.general.section.appearance": "Udseende",
|
||||||
|
"settings.general.section.advanced": "Avanceret",
|
||||||
"settings.general.section.notifications": "Systemmeddelelser",
|
"settings.general.section.notifications": "Systemmeddelelser",
|
||||||
"settings.general.section.updates": "Opdateringer",
|
"settings.general.section.updates": "Opdateringer",
|
||||||
"settings.general.section.sounds": "Lydeffekter",
|
"settings.general.section.sounds": "Lydeffekter",
|
||||||
@@ -633,6 +753,11 @@ export const dict = {
|
|||||||
|
|
||||||
"settings.general.row.language.title": "Sprog",
|
"settings.general.row.language.title": "Sprog",
|
||||||
"settings.general.row.language.description": "Ændr visningssproget for OpenCode",
|
"settings.general.row.language.description": "Ændr visningssproget for OpenCode",
|
||||||
|
"settings.general.row.shell.title": "Terminalshell",
|
||||||
|
"settings.general.row.shell.description":
|
||||||
|
"Vælg den shell, der bruges i din terminal. Kompatible shells bruges også til agentens værktøjskald.",
|
||||||
|
"settings.general.row.shell.autoDefault": "Automatisk (standard)",
|
||||||
|
"settings.general.row.shell.terminalOnly": "kun terminal",
|
||||||
"settings.general.row.appearance.title": "Udseende",
|
"settings.general.row.appearance.title": "Udseende",
|
||||||
"settings.general.row.appearance.description": "Tilpas hvordan OpenCode ser ud på din enhed",
|
"settings.general.row.appearance.description": "Tilpas hvordan OpenCode ser ud på din enhed",
|
||||||
"settings.general.row.colorScheme.title": "Farveskema",
|
"settings.general.row.colorScheme.title": "Farveskema",
|
||||||
@@ -641,14 +766,29 @@ export const dict = {
|
|||||||
"settings.general.row.theme.description": "Tilpas hvordan OpenCode er temabestemt.",
|
"settings.general.row.theme.description": "Tilpas hvordan OpenCode er temabestemt.",
|
||||||
"settings.general.row.font.title": "Kode-skrifttype",
|
"settings.general.row.font.title": "Kode-skrifttype",
|
||||||
"settings.general.row.font.description": "Tilpas skrifttypen, der bruges i kodeblokke",
|
"settings.general.row.font.description": "Tilpas skrifttypen, der bruges i kodeblokke",
|
||||||
"settings.general.row.terminalFont.title": "Terminal Font",
|
"settings.general.row.terminalFont.title": "Terminalskrifttype",
|
||||||
"settings.general.row.terminalFont.description": "Customise the font used in the terminal",
|
"settings.general.row.terminalFont.description": "Tilpas den skrifttype, der bruges i terminalen",
|
||||||
"settings.general.row.uiFont.title": "UI-skrifttype",
|
"settings.general.row.uiFont.title": "UI-skrifttype",
|
||||||
"settings.general.row.uiFont.description": "Tilpas skrifttypen, der bruges i hele brugerfladen",
|
"settings.general.row.uiFont.description": "Tilpas skrifttypen, der bruges i hele brugerfladen",
|
||||||
"settings.general.row.followup.title": "Opfølgningsadfærd",
|
"settings.general.row.followup.title": "Opfølgningsadfærd",
|
||||||
"settings.general.row.followup.description": "Vælg om opfølgende forespørgsler skal styre straks eller vente i kø",
|
"settings.general.row.followup.description": "Vælg om opfølgende forespørgsler skal styre straks eller vente i kø",
|
||||||
"settings.general.row.followup.option.queue": "Kø",
|
"settings.general.row.followup.option.queue": "Kø",
|
||||||
"settings.general.row.followup.option.steer": "Styr",
|
"settings.general.row.followup.option.steer": "Styr",
|
||||||
|
"settings.general.row.showFileTree.title": "Filtræ",
|
||||||
|
"settings.general.row.showFileTree.description": "Vis filtræspanelet i sessioner",
|
||||||
|
"settings.general.row.showNavigation.title": "Navigationsknapper",
|
||||||
|
"settings.general.row.showNavigation.description": "Vis tilbage- og frem-knapperne i desktopappens titellinje",
|
||||||
|
"settings.general.row.showSearch.title": "Kommandopalette",
|
||||||
|
"settings.general.row.showSearch.description": "Vis knappen til søgning og kommandopaletten i titellinjen",
|
||||||
|
"settings.general.row.showTerminal.title": "Terminal",
|
||||||
|
"settings.general.row.showTerminal.description": "Vis terminalknappen i desktopappens titellinje",
|
||||||
|
"settings.general.row.showStatus.title": "Serverstatus",
|
||||||
|
"settings.general.row.showStatus.description": "Vis serverstatusknappen i titellinjen",
|
||||||
|
"settings.general.row.mobileTitlebarBottom.title": "Navigation nederst",
|
||||||
|
"settings.general.row.mobileTitlebarBottom.description":
|
||||||
|
"Placer titellinjen og sessionsfanerne nederst på skærmen på mobilenheder",
|
||||||
|
"settings.general.row.showCustomAgents.title": "Brugerdefinerede agenter",
|
||||||
|
"settings.general.row.showCustomAgents.description": "Vis agentvælgeren i promptfeltet",
|
||||||
"settings.general.row.reasoningSummaries.title": "Vis tænkeoversigter",
|
"settings.general.row.reasoningSummaries.title": "Vis tænkeoversigter",
|
||||||
"settings.general.row.reasoningSummaries.description": "Vis model tænkeoversigter i tidslinjen",
|
"settings.general.row.reasoningSummaries.description": "Vis model tænkeoversigter i tidslinjen",
|
||||||
|
|
||||||
@@ -657,6 +797,11 @@ export const dict = {
|
|||||||
"settings.general.row.editToolPartsExpanded.title": "Udvid edit-værktøjsdele",
|
"settings.general.row.editToolPartsExpanded.title": "Udvid edit-værktøjsdele",
|
||||||
"settings.general.row.editToolPartsExpanded.description":
|
"settings.general.row.editToolPartsExpanded.description":
|
||||||
"Vis edit-, write- og patch-værktøjsdele udvidet som standard i tidslinjen",
|
"Vis edit-, write- og patch-værktøjsdele udvidet som standard i tidslinjen",
|
||||||
|
"settings.general.row.newLayoutDesigns.title": "Nyt layout og design",
|
||||||
|
"settings.general.row.newLayoutDesigns.description":
|
||||||
|
"Aktivér det nydesignede layout, startsiden, promptfeltet og sessionsbrugerfladen",
|
||||||
|
"settings.general.row.pinchZoom.title": "Knib for at zoome",
|
||||||
|
"settings.general.row.pinchZoom.description": "Tillad knibebevægelser på pegefeltet og Ctrl-rulning for at zoome",
|
||||||
"settings.general.row.wayland.title": "Brug native Wayland",
|
"settings.general.row.wayland.title": "Brug native Wayland",
|
||||||
"settings.general.row.wayland.description": "Deaktiver X11-fallback på Wayland. Kræver genstart.",
|
"settings.general.row.wayland.description": "Deaktiver X11-fallback på Wayland. Kræver genstart.",
|
||||||
"settings.general.row.wayland.tooltip":
|
"settings.general.row.wayland.tooltip":
|
||||||
@@ -671,6 +816,8 @@ export const dict = {
|
|||||||
"settings.updates.row.check.description": "Tjek manuelt for opdateringer og installer, hvis tilgængelig",
|
"settings.updates.row.check.description": "Tjek manuelt for opdateringer og installer, hvis tilgængelig",
|
||||||
"settings.updates.action.checkNow": "Tjek nu",
|
"settings.updates.action.checkNow": "Tjek nu",
|
||||||
"settings.updates.action.checking": "Tjekker...",
|
"settings.updates.action.checking": "Tjekker...",
|
||||||
|
"settings.updates.action.downloading": "Downloader...",
|
||||||
|
"settings.updates.action.installing": "Installerer...",
|
||||||
"settings.updates.toast.latest.title": "Du er opdateret",
|
"settings.updates.toast.latest.title": "Du er opdateret",
|
||||||
"settings.updates.toast.latest.description": "Du kører den nyeste version af OpenCode.",
|
"settings.updates.toast.latest.description": "Du kører den nyeste version af OpenCode.",
|
||||||
|
|
||||||
|
|||||||
+152
-2
@@ -25,6 +25,9 @@ export const dict = {
|
|||||||
"theme.scheme.dark": "Dunkel",
|
"theme.scheme.dark": "Dunkel",
|
||||||
"command.sidebar.toggle": "Seitenleiste umschalten",
|
"command.sidebar.toggle": "Seitenleiste umschalten",
|
||||||
"command.project.open": "Projekt öffnen",
|
"command.project.open": "Projekt öffnen",
|
||||||
|
"command.project.previous": "Vorheriges Projekt",
|
||||||
|
"command.project.next": "Nächstes Projekt",
|
||||||
|
"command.project.index": "Zu Projekt {{index}} wechseln",
|
||||||
"command.provider.connect": "Anbieter verbinden",
|
"command.provider.connect": "Anbieter verbinden",
|
||||||
"command.server.switch": "Server wechseln",
|
"command.server.switch": "Server wechseln",
|
||||||
"command.settings.open": "Einstellungen öffnen",
|
"command.settings.open": "Einstellungen öffnen",
|
||||||
@@ -43,6 +46,7 @@ export const dict = {
|
|||||||
"command.session.new": "Neue Sitzung",
|
"command.session.new": "Neue Sitzung",
|
||||||
"command.file.open": "Datei öffnen",
|
"command.file.open": "Datei öffnen",
|
||||||
"command.tab.close": "Tab schließen",
|
"command.tab.close": "Tab schließen",
|
||||||
|
"command.tab.reopenClosed": "Geschlossenen Tab wieder öffnen",
|
||||||
"command.context.addSelection": "Auswahl zum Kontext hinzufügen",
|
"command.context.addSelection": "Auswahl zum Kontext hinzufügen",
|
||||||
"command.context.addSelection.description": "Ausgewählte Zeilen aus der aktuellen Datei hinzufügen",
|
"command.context.addSelection.description": "Ausgewählte Zeilen aus der aktuellen Datei hinzufügen",
|
||||||
"command.input.focus": "Eingabefeld fokussieren",
|
"command.input.focus": "Eingabefeld fokussieren",
|
||||||
@@ -198,6 +202,11 @@ export const dict = {
|
|||||||
"model.input.video": "Video",
|
"model.input.video": "Video",
|
||||||
"model.input.pdf": "PDF",
|
"model.input.pdf": "PDF",
|
||||||
"model.tooltip.allows": "Erlaubt: {{inputs}}",
|
"model.tooltip.allows": "Erlaubt: {{inputs}}",
|
||||||
|
"model.tooltip.context.label": "Kontext",
|
||||||
|
"model.tooltip.inputs": "Eingaben",
|
||||||
|
"model.tooltip.model": "Modell",
|
||||||
|
"model.tooltip.provider": "Anbieter",
|
||||||
|
"model.tooltip.reasoning": "Reasoning",
|
||||||
"model.tooltip.reasoning.allowed": "Erlaubt Reasoning",
|
"model.tooltip.reasoning.allowed": "Erlaubt Reasoning",
|
||||||
"model.tooltip.reasoning.none": "Kein Reasoning",
|
"model.tooltip.reasoning.none": "Kein Reasoning",
|
||||||
"model.tooltip.context": "Kontextlimit {{limit}}",
|
"model.tooltip.context": "Kontextlimit {{limit}}",
|
||||||
@@ -207,6 +216,7 @@ export const dict = {
|
|||||||
"common.loading": "Laden",
|
"common.loading": "Laden",
|
||||||
"common.loading.ellipsis": "...",
|
"common.loading.ellipsis": "...",
|
||||||
"common.cancel": "Abbrechen",
|
"common.cancel": "Abbrechen",
|
||||||
|
"common.clear": "Leeren",
|
||||||
"common.connect": "Verbinden",
|
"common.connect": "Verbinden",
|
||||||
"common.disconnect": "Trennen",
|
"common.disconnect": "Trennen",
|
||||||
"common.continue": "Absenden",
|
"common.continue": "Absenden",
|
||||||
@@ -223,6 +233,8 @@ export const dict = {
|
|||||||
"prompt.mode.shell": "Shell",
|
"prompt.mode.shell": "Shell",
|
||||||
"prompt.mode.normal": "Prompt",
|
"prompt.mode.normal": "Prompt",
|
||||||
"prompt.mode.shell.exit": "esc zum Verlassen",
|
"prompt.mode.shell.exit": "esc zum Verlassen",
|
||||||
|
"session.child.promptDisabled": "Unteragenten-Sitzungen können keine Prompts erhalten.",
|
||||||
|
"session.child.backToParent": "Zurück zur Hauptsitzung.",
|
||||||
"prompt.example.1": "Ein TODO in der Codebasis beheben",
|
"prompt.example.1": "Ein TODO in der Codebasis beheben",
|
||||||
"prompt.example.2": "Was ist der Tech-Stack dieses Projekts?",
|
"prompt.example.2": "Was ist der Tech-Stack dieses Projekts?",
|
||||||
"prompt.example.3": "Fehlerhafte Tests beheben",
|
"prompt.example.3": "Fehlerhafte Tests beheben",
|
||||||
@@ -320,6 +332,75 @@ export const dict = {
|
|||||||
"dialog.server.menu.delete": "Löschen",
|
"dialog.server.menu.delete": "Löschen",
|
||||||
"dialog.server.current": "Aktueller Server",
|
"dialog.server.current": "Aktueller Server",
|
||||||
"dialog.server.status.default": "Standard",
|
"dialog.server.status.default": "Standard",
|
||||||
|
"wsl.server.add": "WSL-Server hinzufügen",
|
||||||
|
"wsl.server.addShort": "WSL hinzufügen",
|
||||||
|
"wsl.server.label": "WSL",
|
||||||
|
"wsl.server.menu.label": "WSL-Server",
|
||||||
|
"wsl.server.retryStart": "Start erneut versuchen",
|
||||||
|
"wsl.server.updating": "Wird aktualisiert...",
|
||||||
|
"wsl.onboarding.step.distro": "Distribution auswählen",
|
||||||
|
"wsl.onboarding.step.opencode": "OpenCode",
|
||||||
|
"wsl.onboarding.checkingRuntime": "WSL wird geprüft...",
|
||||||
|
"wsl.onboarding.restartRequired": "Windows muss neu gestartet werden, um die WSL-Installation abzuschließen.",
|
||||||
|
"wsl.onboarding.ready": "WSL ist bereit.",
|
||||||
|
"wsl.onboarding.required": "WSL ist erforderlich, um fortzufahren.",
|
||||||
|
"wsl.onboarding.checkingDistros": "Distributionen werden geprüft...",
|
||||||
|
"wsl.onboarding.installingDistro": "{{distro}} wird installiert...",
|
||||||
|
"wsl.onboarding.checkingDistro": "{{distro}} wird geprüft...",
|
||||||
|
"wsl.onboarding.listingDistros": "Distributionen werden aufgelistet...",
|
||||||
|
"wsl.onboarding.distroReady": "{{distro}} ist bereit.",
|
||||||
|
"wsl.onboarding.distroNotInstalled": "{{distro}} ist noch nicht installiert.",
|
||||||
|
"wsl.onboarding.openDistroOnce": "Öffnen Sie {{distro}} einmal, um die Einrichtung abzuschließen.",
|
||||||
|
"wsl.onboarding.finishingDistro": "Einrichtung von {{distro}} wird abgeschlossen.",
|
||||||
|
"wsl.onboarding.pickDistro": "Wählen Sie eine Distribution aus oder installieren Sie unten eine.",
|
||||||
|
"wsl.onboarding.checkingOpencode": "OpenCode wird geprüft...",
|
||||||
|
"wsl.onboarding.checkingOpencodeIn": "OpenCode in {{distro}} wird geprüft...",
|
||||||
|
"wsl.onboarding.updatingOpencode": "OpenCode wird aktualisiert...",
|
||||||
|
"wsl.onboarding.updatingOpencodeIn": "OpenCode in {{distro}} wird aktualisiert...",
|
||||||
|
"wsl.onboarding.updateOpencodeIn": "Aktualisieren Sie OpenCode in {{distro}}.",
|
||||||
|
"wsl.onboarding.updateOpencode": "OpenCode aktualisieren",
|
||||||
|
"wsl.onboarding.opencodeReadyIn": "OpenCode ist in {{distro}} bereit.",
|
||||||
|
"wsl.onboarding.opencodeReady": "OpenCode ist bereit.",
|
||||||
|
"wsl.onboarding.installOpencodeIn": "Installieren Sie OpenCode in {{distro}}.",
|
||||||
|
"wsl.onboarding.installOpencode": "OpenCode installieren",
|
||||||
|
"wsl.onboarding.chooseDistroFirst": "Wählen Sie zuerst eine Distribution aus.",
|
||||||
|
"wsl.onboarding.loadFailed": "WSL-Status konnte nicht geladen werden.",
|
||||||
|
"wsl.onboarding.loading": "Wird geladen...",
|
||||||
|
"wsl.onboarding.installedDistros": "Installierte Distributionen",
|
||||||
|
"wsl.onboarding.checkAgain": "Erneut prüfen",
|
||||||
|
"wsl.onboarding.distroStatus.ready": "Bereit",
|
||||||
|
"wsl.onboarding.distroStatus.checking": "Wird geprüft...",
|
||||||
|
"wsl.onboarding.distroStatus.opencodeMissing": "OpenCode nicht installiert",
|
||||||
|
"wsl.onboarding.distroStatus.missingTools": "Fehlend: bash, curl",
|
||||||
|
"wsl.onboarding.distroStatus.unsupported": "Nicht unterstützt · WSL 2 verwenden",
|
||||||
|
"wsl.onboarding.needAnotherDistro": "Andere Distribution benötigt?",
|
||||||
|
"wsl.onboarding.needAnotherDistroHint": "Installieren Sie eine Linux-Distribution aus dem WSL-Katalog",
|
||||||
|
"wsl.onboarding.wslNotInstalled.title": "WSL nicht installiert",
|
||||||
|
"wsl.onboarding.wslNotInstalled.description":
|
||||||
|
"WSL (Windows Subsystem for Linux) ist erforderlich, bevor OpenCode einen WSL-Server hinzufügen kann",
|
||||||
|
"wsl.onboarding.wslUnavailable.title": "WSL nicht verfügbar",
|
||||||
|
"wsl.onboarding.wslUnavailable.description": "OpenCode konnte WSL auf diesem Computer nicht überprüfen.",
|
||||||
|
"wsl.onboarding.installWsl": "WSL installieren",
|
||||||
|
"wsl.onboarding.windowsRestartRequired":
|
||||||
|
"Starten Sie Windows neu, um die WSL-Installation abzuschließen, und öffnen Sie dann OpenCode erneut.",
|
||||||
|
"wsl.onboarding.next": "Weiter",
|
||||||
|
"wsl.onboarding.refresh": "Aktualisieren",
|
||||||
|
"wsl.onboarding.allDistrosAdded": "Alle installierten Distributionen wurden bereits hinzugefügt.",
|
||||||
|
"wsl.onboarding.noDistros": "Noch keine Distributionen erkannt.",
|
||||||
|
"wsl.onboarding.install": "Installieren",
|
||||||
|
"wsl.onboarding.installing": "Wird installiert...",
|
||||||
|
"wsl.onboarding.installDistro": "Distribution installieren",
|
||||||
|
"wsl.onboarding.searchDistros": "Distributionen durchsuchen",
|
||||||
|
"wsl.onboarding.wsl2Required": "WSL 2 ist erforderlich.",
|
||||||
|
"wsl.onboarding.toolsRequired": "Diese Distribution benötigt bash und curl.",
|
||||||
|
"wsl.onboarding.openTerminal": "Terminal öffnen",
|
||||||
|
"wsl.onboarding.path": "Pfad: {{path}}",
|
||||||
|
"wsl.onboarding.notFound": "nicht gefunden",
|
||||||
|
"wsl.onboarding.version": "Version: {{version}}",
|
||||||
|
"wsl.onboarding.unknown": "unbekannt",
|
||||||
|
"wsl.onboarding.desktopVersion": "Desktop {{version}}",
|
||||||
|
"wsl.onboarding.versionMismatch": "Die installierte Version entspricht nicht der Version der Desktop-App.",
|
||||||
|
"wsl.onboarding.adding": "Wird hinzugefügt...",
|
||||||
"dialog.project.edit.title": "Projekt bearbeiten",
|
"dialog.project.edit.title": "Projekt bearbeiten",
|
||||||
"dialog.project.edit.name": "Name",
|
"dialog.project.edit.name": "Name",
|
||||||
"dialog.project.edit.icon": "Icon",
|
"dialog.project.edit.icon": "Icon",
|
||||||
@@ -378,6 +459,7 @@ export const dict = {
|
|||||||
"language.no": "Norsk",
|
"language.no": "Norsk",
|
||||||
"language.br": "Português (Brasil)",
|
"language.br": "Português (Brasil)",
|
||||||
"language.bs": "Bosanski",
|
"language.bs": "Bosanski",
|
||||||
|
"language.uk": "Українська",
|
||||||
"language.th": "ไทย",
|
"language.th": "ไทย",
|
||||||
"language.tr": "Türkçe",
|
"language.tr": "Türkçe",
|
||||||
"toast.language.title": "Sprache",
|
"toast.language.title": "Sprache",
|
||||||
@@ -414,10 +496,12 @@ export const dict = {
|
|||||||
"toast.update.action.notYet": "Noch nicht",
|
"toast.update.action.notYet": "Noch nicht",
|
||||||
"error.page.title": "Etwas ist schiefgelaufen",
|
"error.page.title": "Etwas ist schiefgelaufen",
|
||||||
"error.page.description": "Beim Laden der Anwendung ist ein Fehler aufgetreten.",
|
"error.page.description": "Beim Laden der Anwendung ist ein Fehler aufgetreten.",
|
||||||
|
"error.page.description.localServerStartup": "Beim Starten des lokalen Servers ist ein Fehler aufgetreten.",
|
||||||
"error.page.details.label": "Fehlerdetails",
|
"error.page.details.label": "Fehlerdetails",
|
||||||
"error.page.action.restart": "Neustart",
|
"error.page.action.restart": "Neustart",
|
||||||
"error.page.action.report": "Fehler melden",
|
"error.page.action.report": "Fehler melden",
|
||||||
"error.page.action.reported": "Fehler gemeldet",
|
"error.page.action.reported": "Fehler gemeldet",
|
||||||
|
"error.page.action.exportLogs": "Protokolle exportieren",
|
||||||
"error.page.action.checking": "Prüfen...",
|
"error.page.action.checking": "Prüfen...",
|
||||||
"error.page.action.checkUpdates": "Nach Updates suchen",
|
"error.page.action.checkUpdates": "Nach Updates suchen",
|
||||||
"error.page.action.updateTo": "Auf {{version}} aktualisieren",
|
"error.page.action.updateTo": "Auf {{version}} aktualisieren",
|
||||||
@@ -461,10 +545,31 @@ export const dict = {
|
|||||||
"home.recentProjects": "Letzte Projekte",
|
"home.recentProjects": "Letzte Projekte",
|
||||||
"home.empty.title": "Keine letzten Projekte",
|
"home.empty.title": "Keine letzten Projekte",
|
||||||
"home.empty.description": "Starten Sie, indem Sie ein lokales Projekt öffnen",
|
"home.empty.description": "Starten Sie, indem Sie ein lokales Projekt öffnen",
|
||||||
|
"home.title": "Startseite",
|
||||||
|
"home.projects": "Projekte",
|
||||||
|
"home.project.add": "Projekt hinzufügen",
|
||||||
|
"home.recentlyClosed": "Kürzlich geschlossen",
|
||||||
|
"home.server.collapse": "Serverprojekte einklappen",
|
||||||
|
"home.server.expand": "Serverprojekte ausklappen",
|
||||||
|
"home.sessions.search.placeholder": "Sitzungen durchsuchen",
|
||||||
|
"home.sessions.search.placeholder.scoped": "Sitzungen in {{scope}} durchsuchen",
|
||||||
|
"home.sessions.search.sessions": "Sitzungen",
|
||||||
|
"home.sessions.search.noResults": "Keine Sitzungen für {{query}} gefunden",
|
||||||
|
"home.sessions.empty": "Hier ist noch nichts",
|
||||||
|
"home.sessions.empty.description": "Erstellen Sie eine Sitzung, um loszulegen",
|
||||||
|
"home.sessions.group.today": "Heute",
|
||||||
|
"home.sessions.group.yesterday": "Gestern",
|
||||||
|
"home.sessions.group.older": "Älter",
|
||||||
"session.tab.session": "Sitzung",
|
"session.tab.session": "Sitzung",
|
||||||
"session.tab.review": "Überprüfung",
|
"session.tab.review": "Überprüfung",
|
||||||
"session.tab.context": "Kontext",
|
"session.tab.context": "Kontext",
|
||||||
|
"session.tab.unknown": "Unbekannte Sitzung",
|
||||||
"session.panel.reviewAndFiles": "Überprüfung und Dateien",
|
"session.panel.reviewAndFiles": "Überprüfung und Dateien",
|
||||||
|
"session.error.notFound": "Diese Sitzung wurde nicht gefunden",
|
||||||
|
"session.error.notFound.description":
|
||||||
|
"Dieser Tab verweist auf eine Sitzung, die auf diesem Server nicht mehr existiert.",
|
||||||
|
"session.error.notFound.closeTab": "Tab schließen",
|
||||||
|
"session.error.serverConnection": "Verbindung zu diesem Server nicht möglich",
|
||||||
"session.review.filesChanged": "{{count}} Dateien geändert",
|
"session.review.filesChanged": "{{count}} Dateien geändert",
|
||||||
"session.review.change.one": "Änderung",
|
"session.review.change.one": "Änderung",
|
||||||
"session.review.change.other": "Änderungen",
|
"session.review.change.other": "Änderungen",
|
||||||
@@ -474,6 +579,8 @@ export const dict = {
|
|||||||
"session.review.noSnapshot":
|
"session.review.noSnapshot":
|
||||||
"Snapshot-Tracking ist in der Konfiguration deaktiviert, daher sind Sitzungsänderungen nicht verfügbar",
|
"Snapshot-Tracking ist in der Konfiguration deaktiviert, daher sind Sitzungsänderungen nicht verfügbar",
|
||||||
"session.review.noChanges": "Keine Änderungen",
|
"session.review.noChanges": "Keine Änderungen",
|
||||||
|
"session.review.noUncommittedChanges": "Noch keine nicht committeten Änderungen",
|
||||||
|
"session.review.noBranchChanges": "Noch keine Branch-Änderungen",
|
||||||
"session.files.selectToOpen": "Datei zum Öffnen auswählen",
|
"session.files.selectToOpen": "Datei zum Öffnen auswählen",
|
||||||
"session.files.all": "Alle Dateien",
|
"session.files.all": "Alle Dateien",
|
||||||
"session.files.empty": "Keine Dateien",
|
"session.files.empty": "Keine Dateien",
|
||||||
@@ -487,6 +594,10 @@ export const dict = {
|
|||||||
"session.todo.title": "Aufgaben",
|
"session.todo.title": "Aufgaben",
|
||||||
"session.todo.collapse": "Einklappen",
|
"session.todo.collapse": "Einklappen",
|
||||||
"session.todo.expand": "Ausklappen",
|
"session.todo.expand": "Ausklappen",
|
||||||
|
"session.question.minimize": "Frage minimieren",
|
||||||
|
"session.question.restore": "Frage wiederherstellen",
|
||||||
|
"session.question.pending.one": "{{count}} ausstehende Frage",
|
||||||
|
"session.question.pending.other": "{{count}} ausstehende Fragen",
|
||||||
"session.followupDock.summary.one": "{{count}} Nachricht in der Warteschlange",
|
"session.followupDock.summary.one": "{{count}} Nachricht in der Warteschlange",
|
||||||
"session.followupDock.summary.other": "{{count}} Nachrichten in der Warteschlange",
|
"session.followupDock.summary.other": "{{count}} Nachrichten in der Warteschlange",
|
||||||
"session.followupDock.sendNow": "Jetzt senden",
|
"session.followupDock.sendNow": "Jetzt senden",
|
||||||
@@ -499,9 +610,16 @@ export const dict = {
|
|||||||
"session.revertDock.expand": "Zurückgesetzte Nachrichten ausklappen",
|
"session.revertDock.expand": "Zurückgesetzte Nachrichten ausklappen",
|
||||||
"session.revertDock.restore": "Nachricht wiederherstellen",
|
"session.revertDock.restore": "Nachricht wiederherstellen",
|
||||||
"session.new.title": "Baue, was du willst",
|
"session.new.title": "Baue, was du willst",
|
||||||
|
"session.new.project.new": "Neues Projekt",
|
||||||
|
"session.new.project.search": "Projekte durchsuchen",
|
||||||
|
"session.new.project.add": "Projekt hinzufügen",
|
||||||
"session.new.worktree.main": "Haupt-Branch",
|
"session.new.worktree.main": "Haupt-Branch",
|
||||||
"session.new.worktree.mainWithBranch": "Haupt-Branch ({{branch}})",
|
"session.new.worktree.mainWithBranch": "Haupt-Branch ({{branch}})",
|
||||||
"session.new.worktree.create": "Neuen Worktree erstellen",
|
"session.new.worktree.create": "Neuen Worktree erstellen",
|
||||||
|
"session.new.workspace.runIn": "Sitzung ausführen in",
|
||||||
|
"session.new.workspace.triggerLocal": "Lokal",
|
||||||
|
"session.new.workspace.local": "Lokales Repository",
|
||||||
|
"session.new.workspace.existing": "Arbeitsbereich…",
|
||||||
"session.new.lastModified": "Zuletzt geändert",
|
"session.new.lastModified": "Zuletzt geändert",
|
||||||
"session.header.search.placeholder": "{{project}} durchsuchen",
|
"session.header.search.placeholder": "{{project}} durchsuchen",
|
||||||
"session.header.searchFiles": "Dateien suchen",
|
"session.header.searchFiles": "Dateien suchen",
|
||||||
@@ -566,6 +684,8 @@ export const dict = {
|
|||||||
"sidebar.project.recentSessions": "Letzte Sitzungen",
|
"sidebar.project.recentSessions": "Letzte Sitzungen",
|
||||||
"sidebar.project.viewAllSessions": "Alle Sitzungen anzeigen",
|
"sidebar.project.viewAllSessions": "Alle Sitzungen anzeigen",
|
||||||
"sidebar.project.clearNotifications": "Benachrichtigungen löschen",
|
"sidebar.project.clearNotifications": "Benachrichtigungen löschen",
|
||||||
|
"sidebar.empty.title": "Keine Projekte geöffnet",
|
||||||
|
"sidebar.empty.description": "Öffnen Sie ein Projekt, um loszulegen",
|
||||||
"app.name.desktop": "OpenCode Desktop",
|
"app.name.desktop": "OpenCode Desktop",
|
||||||
"settings.section.desktop": "Desktop",
|
"settings.section.desktop": "Desktop",
|
||||||
"settings.section.server": "Server",
|
"settings.section.server": "Server",
|
||||||
@@ -575,6 +695,7 @@ export const dict = {
|
|||||||
"settings.desktop.wsl.title": "WSL-Integration",
|
"settings.desktop.wsl.title": "WSL-Integration",
|
||||||
"settings.desktop.wsl.description": "OpenCode-Server innerhalb von WSL unter Windows ausführen.",
|
"settings.desktop.wsl.description": "OpenCode-Server innerhalb von WSL unter Windows ausführen.",
|
||||||
"settings.general.section.appearance": "Erscheinungsbild",
|
"settings.general.section.appearance": "Erscheinungsbild",
|
||||||
|
"settings.general.section.advanced": "Erweitert",
|
||||||
"settings.general.section.notifications": "Systembenachrichtigungen",
|
"settings.general.section.notifications": "Systembenachrichtigungen",
|
||||||
"settings.general.section.updates": "Updates",
|
"settings.general.section.updates": "Updates",
|
||||||
"settings.general.section.sounds": "Soundeffekte",
|
"settings.general.section.sounds": "Soundeffekte",
|
||||||
@@ -582,6 +703,11 @@ export const dict = {
|
|||||||
"settings.general.section.display": "Anzeige",
|
"settings.general.section.display": "Anzeige",
|
||||||
"settings.general.row.language.title": "Sprache",
|
"settings.general.row.language.title": "Sprache",
|
||||||
"settings.general.row.language.description": "Die Anzeigesprache für OpenCode ändern",
|
"settings.general.row.language.description": "Die Anzeigesprache für OpenCode ändern",
|
||||||
|
"settings.general.row.shell.title": "Terminal-Shell",
|
||||||
|
"settings.general.row.shell.description":
|
||||||
|
"Wählen Sie die Shell für Ihr Terminal aus. Kompatible Shells werden auch für Tool-Aufrufe von Agenten verwendet.",
|
||||||
|
"settings.general.row.shell.autoDefault": "Automatisch (Standard)",
|
||||||
|
"settings.general.row.shell.terminalOnly": "nur Terminal",
|
||||||
"settings.general.row.appearance.title": "Erscheinungsbild",
|
"settings.general.row.appearance.title": "Erscheinungsbild",
|
||||||
"settings.general.row.appearance.description": "Anpassen, wie OpenCode auf Ihrem Gerät aussieht",
|
"settings.general.row.appearance.description": "Anpassen, wie OpenCode auf Ihrem Gerät aussieht",
|
||||||
"settings.general.row.colorScheme.title": "Farbschema",
|
"settings.general.row.colorScheme.title": "Farbschema",
|
||||||
@@ -591,8 +717,8 @@ export const dict = {
|
|||||||
"settings.general.row.theme.description": "Das Thema von OpenCode anpassen.",
|
"settings.general.row.theme.description": "Das Thema von OpenCode anpassen.",
|
||||||
"settings.general.row.font.title": "Code-Schriftart",
|
"settings.general.row.font.title": "Code-Schriftart",
|
||||||
"settings.general.row.font.description": "Die in Codeblöcken verwendete Schriftart anpassen",
|
"settings.general.row.font.description": "Die in Codeblöcken verwendete Schriftart anpassen",
|
||||||
"settings.general.row.terminalFont.title": "Terminal Font",
|
"settings.general.row.terminalFont.title": "Terminalschriftart",
|
||||||
"settings.general.row.terminalFont.description": "Customise the font used in the terminal",
|
"settings.general.row.terminalFont.description": "Passe die im Terminal verwendete Schriftart an",
|
||||||
"settings.general.row.uiFont.title": "UI-Schriftart",
|
"settings.general.row.uiFont.title": "UI-Schriftart",
|
||||||
"settings.general.row.uiFont.description": "Die im gesamten Interface verwendete Schriftart anpassen",
|
"settings.general.row.uiFont.description": "Die im gesamten Interface verwendete Schriftart anpassen",
|
||||||
"settings.general.row.followup.title": "Verhalten bei Folgefragen",
|
"settings.general.row.followup.title": "Verhalten bei Folgefragen",
|
||||||
@@ -600,6 +726,23 @@ export const dict = {
|
|||||||
"Wählen Sie, ob Folgefragen sofort steuern oder in einer Warteschlange warten",
|
"Wählen Sie, ob Folgefragen sofort steuern oder in einer Warteschlange warten",
|
||||||
"settings.general.row.followup.option.queue": "Warteschlange",
|
"settings.general.row.followup.option.queue": "Warteschlange",
|
||||||
"settings.general.row.followup.option.steer": "Steuern",
|
"settings.general.row.followup.option.steer": "Steuern",
|
||||||
|
"settings.general.row.showFileTree.title": "Dateibaum",
|
||||||
|
"settings.general.row.showFileTree.description": "Dateibaum in Sitzungen anzeigen",
|
||||||
|
"settings.general.row.showNavigation.title": "Navigationssteuerung",
|
||||||
|
"settings.general.row.showNavigation.description":
|
||||||
|
"Schaltflächen zum Vor- und Zurücknavigieren in der Desktop-Titelleiste anzeigen",
|
||||||
|
"settings.general.row.showSearch.title": "Befehlspalette",
|
||||||
|
"settings.general.row.showSearch.description":
|
||||||
|
"Schaltfläche für Suche und Befehlspalette in der Titelleiste anzeigen",
|
||||||
|
"settings.general.row.showTerminal.title": "Terminal",
|
||||||
|
"settings.general.row.showTerminal.description": "Terminal-Schaltfläche in der Desktop-Titelleiste anzeigen",
|
||||||
|
"settings.general.row.showStatus.title": "Serverstatus",
|
||||||
|
"settings.general.row.showStatus.description": "Serverstatus-Schaltfläche in der Titelleiste anzeigen",
|
||||||
|
"settings.general.row.mobileTitlebarBottom.title": "Navigation unten",
|
||||||
|
"settings.general.row.mobileTitlebarBottom.description":
|
||||||
|
"Titelleiste und Sitzungs-Tabs auf Mobilgeräten am unteren Bildschirmrand platzieren",
|
||||||
|
"settings.general.row.showCustomAgents.title": "Benutzerdefinierte Agenten",
|
||||||
|
"settings.general.row.showCustomAgents.description": "Agentenauswahl im Eingabebereich anzeigen",
|
||||||
"settings.general.row.reasoningSummaries.title": "Reasoning-Zusammenfassungen anzeigen",
|
"settings.general.row.reasoningSummaries.title": "Reasoning-Zusammenfassungen anzeigen",
|
||||||
"settings.general.row.reasoningSummaries.description":
|
"settings.general.row.reasoningSummaries.description":
|
||||||
"Zusammenfassungen des Modell-Reasonings in der Timeline anzeigen",
|
"Zusammenfassungen des Modell-Reasonings in der Timeline anzeigen",
|
||||||
@@ -609,6 +752,11 @@ export const dict = {
|
|||||||
"settings.general.row.editToolPartsExpanded.title": "Edit-Tool-Abschnitte ausklappen",
|
"settings.general.row.editToolPartsExpanded.title": "Edit-Tool-Abschnitte ausklappen",
|
||||||
"settings.general.row.editToolPartsExpanded.description":
|
"settings.general.row.editToolPartsExpanded.description":
|
||||||
"Edit-, Write- und Patch-Tool-Abschnitte standardmäßig in der Timeline ausgeklappt anzeigen",
|
"Edit-, Write- und Patch-Tool-Abschnitte standardmäßig in der Timeline ausgeklappt anzeigen",
|
||||||
|
"settings.general.row.newLayoutDesigns.title": "Neues Layout und Design",
|
||||||
|
"settings.general.row.newLayoutDesigns.description":
|
||||||
|
"Neu gestaltetes Layout, Startseite, Eingabebereich und Sitzungsoberfläche aktivieren",
|
||||||
|
"settings.general.row.pinchZoom.title": "Zoom per Fingergeste",
|
||||||
|
"settings.general.row.pinchZoom.description": "Zoomen per Zwei-Finger- und Ctrl-Scroll-Geste erlauben",
|
||||||
"settings.general.row.wayland.title": "Natives Wayland verwenden",
|
"settings.general.row.wayland.title": "Natives Wayland verwenden",
|
||||||
"settings.general.row.wayland.description": "X11-Fallback unter Wayland deaktivieren. Erfordert Neustart.",
|
"settings.general.row.wayland.description": "X11-Fallback unter Wayland deaktivieren. Erfordert Neustart.",
|
||||||
"settings.general.row.wayland.tooltip":
|
"settings.general.row.wayland.tooltip":
|
||||||
@@ -621,6 +769,8 @@ export const dict = {
|
|||||||
"settings.updates.row.check.description": "Manuell nach Updates suchen und installieren, wenn verfügbar",
|
"settings.updates.row.check.description": "Manuell nach Updates suchen und installieren, wenn verfügbar",
|
||||||
"settings.updates.action.checkNow": "Jetzt prüfen",
|
"settings.updates.action.checkNow": "Jetzt prüfen",
|
||||||
"settings.updates.action.checking": "Wird geprüft...",
|
"settings.updates.action.checking": "Wird geprüft...",
|
||||||
|
"settings.updates.action.downloading": "Wird heruntergeladen...",
|
||||||
|
"settings.updates.action.installing": "Wird installiert...",
|
||||||
"settings.updates.toast.latest.title": "Du bist auf dem neuesten Stand",
|
"settings.updates.toast.latest.title": "Du bist auf dem neuesten Stand",
|
||||||
"settings.updates.toast.latest.description": "Du verwendest die aktuelle Version von OpenCode.",
|
"settings.updates.toast.latest.description": "Du verwendest die aktuelle Version von OpenCode.",
|
||||||
"sound.option.none": "Keine",
|
"sound.option.none": "Keine",
|
||||||
|
|||||||
@@ -48,6 +48,7 @@ export const dict = {
|
|||||||
"command.session.new": "New session",
|
"command.session.new": "New session",
|
||||||
"command.file.open": "Open file",
|
"command.file.open": "Open file",
|
||||||
"command.tab.close": "Close tab",
|
"command.tab.close": "Close tab",
|
||||||
|
"command.tab.reopenClosed": "Reopen closed tab",
|
||||||
"command.context.addSelection": "Add selection to context",
|
"command.context.addSelection": "Add selection to context",
|
||||||
"command.context.addSelection.description": "Add selected lines from the current file",
|
"command.context.addSelection.description": "Add selected lines from the current file",
|
||||||
"command.input.focus": "Focus input",
|
"command.input.focus": "Focus input",
|
||||||
@@ -211,6 +212,11 @@ export const dict = {
|
|||||||
"model.input.video": "video",
|
"model.input.video": "video",
|
||||||
"model.input.pdf": "pdf",
|
"model.input.pdf": "pdf",
|
||||||
"model.tooltip.allows": "Allows: {{inputs}}",
|
"model.tooltip.allows": "Allows: {{inputs}}",
|
||||||
|
"model.tooltip.context.label": "Context",
|
||||||
|
"model.tooltip.inputs": "Inputs",
|
||||||
|
"model.tooltip.model": "Model",
|
||||||
|
"model.tooltip.provider": "Provider",
|
||||||
|
"model.tooltip.reasoning": "Reasoning",
|
||||||
"model.tooltip.reasoning.allowed": "Allows reasoning",
|
"model.tooltip.reasoning.allowed": "Allows reasoning",
|
||||||
"model.tooltip.reasoning.none": "No reasoning",
|
"model.tooltip.reasoning.none": "No reasoning",
|
||||||
"model.tooltip.context": "Context limit {{limit}}",
|
"model.tooltip.context": "Context limit {{limit}}",
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user