mirror of
https://github.com/anomalyco/opencode.git
synced 2026-08-15 17:08:21 -04:00
Compare commits
3 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| ac384f3d27 | |||
| 92a50f2397 | |||
| ac7ee99a24 |
@@ -1,5 +1,4 @@
|
|||||||
adamdotdevin
|
adamdotdevin
|
||||||
arvsrn
|
|
||||||
Brendonovich
|
Brendonovich
|
||||||
fwang
|
fwang
|
||||||
Hona
|
Hona
|
||||||
@@ -8,14 +7,11 @@ 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=52ao9352-5623-4fa0-b7dd-3407c392c1af&qr_code=true) | [X.com](https://x.com/opencode)
|
**加入我们的社区** [飞书](https://applink.feishu.cn/client/chat/chatter/add_by_link?link_token=738j8655-cd59-4633-a30a-1124e0096789&qr_code=true) | [X.com](https://x.com/opencode)
|
||||||
|
|||||||
+1
-1
@@ -125,4 +125,4 @@ OpenCode 內建了兩種 Agent,您可以使用 `Tab` 鍵快速切換。
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
**加入我們的社群** [飞书](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)
|
**加入我們的社群** [飞书](https://applink.feishu.cn/client/chat/chatter/add_by_link?link_token=738j8655-cd59-4633-a30a-1124e0096789&qr_code=true) | [X.com](https://x.com/opencode)
|
||||||
|
|||||||
@@ -1,3 +0,0 @@
|
|||||||
node_modules
|
|
||||||
.remotion
|
|
||||||
out/frame-*.png
|
|
||||||
@@ -1,483 +0,0 @@
|
|||||||
{
|
|
||||||
"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.
|
Before Width: | Height: | Size: 64 KiB |
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -1,24 +0,0 @@
|
|||||||
{
|
|
||||||
"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.
|
Before Width: | Height: | Size: 159 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 172 KiB |
@@ -1,156 +0,0 @@
|
|||||||
// 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
|
|
||||||
@@ -1,185 +0,0 @@
|
|||||||
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>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -1,36 +0,0 @@
|
|||||||
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)
|
|
||||||
@@ -1,144 +0,0 @@
|
|||||||
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>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -1,201 +0,0 @@
|
|||||||
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>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -1,135 +0,0 @@
|
|||||||
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>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -1,139 +0,0 @@
|
|||||||
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>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -1,254 +0,0 @@
|
|||||||
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>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -19,17 +19,18 @@
|
|||||||
"@typescript/native-preview": "catalog:",
|
"@typescript/native-preview": "catalog:",
|
||||||
"glob": "13.0.5",
|
"glob": "13.0.5",
|
||||||
"husky": "9.1.7",
|
"husky": "9.1.7",
|
||||||
|
"node-gyp": "12.4.0",
|
||||||
"oxlint": "1.60.0",
|
"oxlint": "1.60.0",
|
||||||
"oxlint-tsgolint": "0.21.0",
|
"oxlint-tsgolint": "0.21.0",
|
||||||
"prettier": "3.6.2",
|
"prettier": "3.6.2",
|
||||||
"semver": "^7.6.0",
|
"semver": "^7.6.0",
|
||||||
"sst": "catalog:",
|
"sst": "catalog:",
|
||||||
"turbo": "2.10.2",
|
"turbo": "2.8.13",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
"packages/app": {
|
"packages/app": {
|
||||||
"name": "@opencode-ai/app",
|
"name": "@opencode-ai/app",
|
||||||
"version": "1.17.15",
|
"version": "1.17.13",
|
||||||
"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 +94,7 @@
|
|||||||
},
|
},
|
||||||
"packages/cli": {
|
"packages/cli": {
|
||||||
"name": "@opencode-ai/cli",
|
"name": "@opencode-ai/cli",
|
||||||
"version": "1.17.15",
|
"version": "1.17.13",
|
||||||
"bin": {
|
"bin": {
|
||||||
"lildax": "./bin/lildax.cjs",
|
"lildax": "./bin/lildax.cjs",
|
||||||
},
|
},
|
||||||
@@ -141,7 +142,7 @@
|
|||||||
},
|
},
|
||||||
"packages/codemode": {
|
"packages/codemode": {
|
||||||
"name": "@opencode-ai/codemode",
|
"name": "@opencode-ai/codemode",
|
||||||
"version": "1.17.15",
|
"version": "0.0.1",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"acorn": "8.15.0",
|
"acorn": "8.15.0",
|
||||||
"effect": "catalog:",
|
"effect": "catalog:",
|
||||||
@@ -155,7 +156,7 @@
|
|||||||
},
|
},
|
||||||
"packages/console/app": {
|
"packages/console/app": {
|
||||||
"name": "@opencode-ai/console-app",
|
"name": "@opencode-ai/console-app",
|
||||||
"version": "1.17.15",
|
"version": "1.17.13",
|
||||||
"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 +192,7 @@
|
|||||||
},
|
},
|
||||||
"packages/console/core": {
|
"packages/console/core": {
|
||||||
"name": "@opencode-ai/console-core",
|
"name": "@opencode-ai/console-core",
|
||||||
"version": "1.17.15",
|
"version": "1.17.13",
|
||||||
"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 +219,7 @@
|
|||||||
},
|
},
|
||||||
"packages/console/function": {
|
"packages/console/function": {
|
||||||
"name": "@opencode-ai/console-function",
|
"name": "@opencode-ai/console-function",
|
||||||
"version": "1.17.15",
|
"version": "1.17.13",
|
||||||
"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 +241,7 @@
|
|||||||
},
|
},
|
||||||
"packages/console/mail": {
|
"packages/console/mail": {
|
||||||
"name": "@opencode-ai/console-mail",
|
"name": "@opencode-ai/console-mail",
|
||||||
"version": "1.17.15",
|
"version": "1.17.13",
|
||||||
"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 +265,7 @@
|
|||||||
},
|
},
|
||||||
"packages/console/support": {
|
"packages/console/support": {
|
||||||
"name": "@opencode-ai/console-support",
|
"name": "@opencode-ai/console-support",
|
||||||
"version": "1.17.15",
|
"version": "1.17.13",
|
||||||
"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 +285,7 @@
|
|||||||
},
|
},
|
||||||
"packages/core": {
|
"packages/core": {
|
||||||
"name": "@opencode-ai/core",
|
"name": "@opencode-ai/core",
|
||||||
"version": "1.17.15",
|
"version": "1.17.13",
|
||||||
"bin": {
|
"bin": {
|
||||||
"opencode": "./bin/opencode",
|
"opencode": "./bin/opencode",
|
||||||
},
|
},
|
||||||
@@ -308,7 +309,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.102",
|
"@ai-sdk/xai": "3.0.82",
|
||||||
"@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 +379,7 @@
|
|||||||
},
|
},
|
||||||
"packages/desktop": {
|
"packages/desktop": {
|
||||||
"name": "@opencode-ai/desktop",
|
"name": "@opencode-ai/desktop",
|
||||||
"version": "1.17.15",
|
"version": "1.17.13",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@zip.js/zip.js": "2.7.62",
|
"@zip.js/zip.js": "2.7.62",
|
||||||
"effect": "catalog:",
|
"effect": "catalog:",
|
||||||
@@ -432,7 +433,7 @@
|
|||||||
},
|
},
|
||||||
"packages/effect-drizzle-sqlite": {
|
"packages/effect-drizzle-sqlite": {
|
||||||
"name": "@opencode-ai/effect-drizzle-sqlite",
|
"name": "@opencode-ai/effect-drizzle-sqlite",
|
||||||
"version": "1.17.15",
|
"version": "1.17.13",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"drizzle-orm": "catalog:",
|
"drizzle-orm": "catalog:",
|
||||||
"effect": "catalog:",
|
"effect": "catalog:",
|
||||||
@@ -446,7 +447,7 @@
|
|||||||
},
|
},
|
||||||
"packages/effect-sqlite-node": {
|
"packages/effect-sqlite-node": {
|
||||||
"name": "@opencode-ai/effect-sqlite-node",
|
"name": "@opencode-ai/effect-sqlite-node",
|
||||||
"version": "1.17.15",
|
"version": "1.17.13",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"effect": "catalog:",
|
"effect": "catalog:",
|
||||||
},
|
},
|
||||||
@@ -458,7 +459,7 @@
|
|||||||
},
|
},
|
||||||
"packages/enterprise": {
|
"packages/enterprise": {
|
||||||
"name": "@opencode-ai/enterprise",
|
"name": "@opencode-ai/enterprise",
|
||||||
"version": "1.17.15",
|
"version": "1.17.13",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@hono/standard-validator": "catalog:",
|
"@hono/standard-validator": "catalog:",
|
||||||
"@opencode-ai/core": "workspace:*",
|
"@opencode-ai/core": "workspace:*",
|
||||||
@@ -490,7 +491,7 @@
|
|||||||
},
|
},
|
||||||
"packages/function": {
|
"packages/function": {
|
||||||
"name": "@opencode-ai/function",
|
"name": "@opencode-ai/function",
|
||||||
"version": "1.17.15",
|
"version": "1.17.13",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@octokit/auth-app": "8.0.1",
|
"@octokit/auth-app": "8.0.1",
|
||||||
"@octokit/rest": "catalog:",
|
"@octokit/rest": "catalog:",
|
||||||
@@ -506,7 +507,7 @@
|
|||||||
},
|
},
|
||||||
"packages/http-recorder": {
|
"packages/http-recorder": {
|
||||||
"name": "@opencode-ai/http-recorder",
|
"name": "@opencode-ai/http-recorder",
|
||||||
"version": "1.17.15",
|
"version": "1.17.13",
|
||||||
"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 +538,7 @@
|
|||||||
},
|
},
|
||||||
"packages/llm": {
|
"packages/llm": {
|
||||||
"name": "@opencode-ai/llm",
|
"name": "@opencode-ai/llm",
|
||||||
"version": "1.17.15",
|
"version": "1.17.13",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@opencode-ai/schema": "workspace:*",
|
"@opencode-ai/schema": "workspace:*",
|
||||||
"@smithy/eventstream-codec": "4.2.14",
|
"@smithy/eventstream-codec": "4.2.14",
|
||||||
@@ -556,7 +557,7 @@
|
|||||||
},
|
},
|
||||||
"packages/opencode": {
|
"packages/opencode": {
|
||||||
"name": "opencode",
|
"name": "opencode",
|
||||||
"version": "1.17.15",
|
"version": "1.17.13",
|
||||||
"bin": {
|
"bin": {
|
||||||
"opencode": "./bin/opencode",
|
"opencode": "./bin/opencode",
|
||||||
},
|
},
|
||||||
@@ -582,7 +583,7 @@
|
|||||||
"@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.102",
|
"@ai-sdk/xai": "3.0.82",
|
||||||
"@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:",
|
||||||
@@ -687,7 +688,7 @@
|
|||||||
},
|
},
|
||||||
"packages/plugin": {
|
"packages/plugin": {
|
||||||
"name": "@opencode-ai/plugin",
|
"name": "@opencode-ai/plugin",
|
||||||
"version": "1.17.15",
|
"version": "1.17.13",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@ai-sdk/provider": "3.0.8",
|
"@ai-sdk/provider": "3.0.8",
|
||||||
"@opencode-ai/sdk": "workspace:*",
|
"@opencode-ai/sdk": "workspace:*",
|
||||||
@@ -704,9 +705,9 @@
|
|||||||
"typescript": "catalog:",
|
"typescript": "catalog:",
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"@opentui/core": ">=0.4.3",
|
"@opentui/core": ">=0.3.4",
|
||||||
"@opentui/keymap": ">=0.4.3",
|
"@opentui/keymap": ">=0.3.4",
|
||||||
"@opentui/solid": ">=0.4.3",
|
"@opentui/solid": ">=0.3.4",
|
||||||
},
|
},
|
||||||
"optionalPeers": [
|
"optionalPeers": [
|
||||||
"@opentui/core",
|
"@opentui/core",
|
||||||
@@ -763,7 +764,7 @@
|
|||||||
},
|
},
|
||||||
"packages/sdk/js": {
|
"packages/sdk/js": {
|
||||||
"name": "@opencode-ai/sdk",
|
"name": "@opencode-ai/sdk",
|
||||||
"version": "1.17.15",
|
"version": "1.17.13",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"cross-spawn": "catalog:",
|
"cross-spawn": "catalog:",
|
||||||
},
|
},
|
||||||
@@ -778,7 +779,7 @@
|
|||||||
},
|
},
|
||||||
"packages/server": {
|
"packages/server": {
|
||||||
"name": "@opencode-ai/server",
|
"name": "@opencode-ai/server",
|
||||||
"version": "1.17.15",
|
"version": "1.17.13",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@opencode-ai/core": "workspace:*",
|
"@opencode-ai/core": "workspace:*",
|
||||||
"@opencode-ai/protocol": "workspace:*",
|
"@opencode-ai/protocol": "workspace:*",
|
||||||
@@ -793,7 +794,7 @@
|
|||||||
},
|
},
|
||||||
"packages/session-ui": {
|
"packages/session-ui": {
|
||||||
"name": "@opencode-ai/session-ui",
|
"name": "@opencode-ai/session-ui",
|
||||||
"version": "1.17.15",
|
"version": "1.17.13",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@kobalte/core": "catalog:",
|
"@kobalte/core": "catalog:",
|
||||||
"@opencode-ai/core": "workspace:*",
|
"@opencode-ai/core": "workspace:*",
|
||||||
@@ -837,7 +838,7 @@
|
|||||||
},
|
},
|
||||||
"packages/slack": {
|
"packages/slack": {
|
||||||
"name": "@opencode-ai/slack",
|
"name": "@opencode-ai/slack",
|
||||||
"version": "1.17.15",
|
"version": "1.17.13",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@opencode-ai/sdk": "workspace:*",
|
"@opencode-ai/sdk": "workspace:*",
|
||||||
"@slack/bolt": "^3.17.1",
|
"@slack/bolt": "^3.17.1",
|
||||||
@@ -850,7 +851,7 @@
|
|||||||
},
|
},
|
||||||
"packages/stats/app": {
|
"packages/stats/app": {
|
||||||
"name": "@opencode-ai/stats-app",
|
"name": "@opencode-ai/stats-app",
|
||||||
"version": "1.17.15",
|
"version": "1.17.13",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@ibm/plex": "6.4.1",
|
"@ibm/plex": "6.4.1",
|
||||||
"@opencode-ai/stats-core": "workspace:*",
|
"@opencode-ai/stats-core": "workspace:*",
|
||||||
@@ -883,7 +884,7 @@
|
|||||||
},
|
},
|
||||||
"packages/stats/core": {
|
"packages/stats/core": {
|
||||||
"name": "@opencode-ai/stats-core",
|
"name": "@opencode-ai/stats-core",
|
||||||
"version": "1.17.15",
|
"version": "1.17.13",
|
||||||
"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",
|
||||||
@@ -902,7 +903,7 @@
|
|||||||
},
|
},
|
||||||
"packages/stats/server": {
|
"packages/stats/server": {
|
||||||
"name": "@opencode-ai/stats-server",
|
"name": "@opencode-ai/stats-server",
|
||||||
"version": "1.17.15",
|
"version": "1.17.13",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@aws-sdk/client-firehose": "3.933.0",
|
"@aws-sdk/client-firehose": "3.933.0",
|
||||||
"@effect/platform-node": "catalog:",
|
"@effect/platform-node": "catalog:",
|
||||||
@@ -943,7 +944,7 @@
|
|||||||
},
|
},
|
||||||
"packages/tui": {
|
"packages/tui": {
|
||||||
"name": "@opencode-ai/tui",
|
"name": "@opencode-ai/tui",
|
||||||
"version": "1.17.15",
|
"version": "1.17.13",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@opencode-ai/core": "workspace:*",
|
"@opencode-ai/core": "workspace:*",
|
||||||
"@opencode-ai/plugin": "workspace:*",
|
"@opencode-ai/plugin": "workspace:*",
|
||||||
@@ -970,7 +971,7 @@
|
|||||||
},
|
},
|
||||||
"packages/ui": {
|
"packages/ui": {
|
||||||
"name": "@opencode-ai/ui",
|
"name": "@opencode-ai/ui",
|
||||||
"version": "1.17.15",
|
"version": "1.17.13",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@kobalte/core": "catalog:",
|
"@kobalte/core": "catalog:",
|
||||||
"@pierre/diffs": "catalog:",
|
"@pierre/diffs": "catalog:",
|
||||||
@@ -1021,7 +1022,7 @@
|
|||||||
},
|
},
|
||||||
"packages/web": {
|
"packages/web": {
|
||||||
"name": "@opencode-ai/web",
|
"name": "@opencode-ai/web",
|
||||||
"version": "1.17.15",
|
"version": "1.17.13",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@astrojs/cloudflare": "12.6.3",
|
"@astrojs/cloudflare": "12.6.3",
|
||||||
"@astrojs/markdown-remark": "6.3.1",
|
"@astrojs/markdown-remark": "6.3.1",
|
||||||
@@ -1064,7 +1065,6 @@
|
|||||||
],
|
],
|
||||||
"patchedDependencies": {
|
"patchedDependencies": {
|
||||||
"@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",
|
||||||
"@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",
|
||||||
@@ -1074,6 +1074,7 @@
|
|||||||
"@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/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",
|
||||||
},
|
},
|
||||||
@@ -1096,9 +1097,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.4.3",
|
"@opentui/core": "0.3.4",
|
||||||
"@opentui/keymap": "0.4.3",
|
"@opentui/keymap": "0.3.4",
|
||||||
"@opentui/solid": "0.4.3",
|
"@opentui/solid": "0.3.4",
|
||||||
"@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",
|
||||||
@@ -1211,7 +1212,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.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=="],
|
"@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=="],
|
||||||
|
|
||||||
"@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=="],
|
||||||
|
|
||||||
@@ -2031,27 +2032,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.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": ["@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-darwin-arm64": ["@opentui/core-darwin-arm64@0.4.3", "", { "os": "darwin", "cpu": "arm64" }, "sha512-p5+7AAxpxGuDGagyQfewKtmTFnN7THvTVY4FyKqUtJomNaHdQXPHztapNNzMx0DGWbwOUbVKzpL+yc3CZY3chQ=="],
|
"@opentui/core-darwin-arm64": ["@opentui/core-darwin-arm64@0.3.4", "", { "os": "darwin", "cpu": "arm64" }, "sha512-4A7JYXUsZqhu9PPCe07E30ourSJYkitkwMujUyNKjM5e/dHNDVnz+5r5cO3M5snofLafc1DN7+9jEPn4UQzchQ=="],
|
||||||
|
|
||||||
"@opentui/core-darwin-x64": ["@opentui/core-darwin-x64@0.4.3", "", { "os": "darwin", "cpu": "x64" }, "sha512-+fh0vEUE0lwVC7RW5ijYLRlTLp5NfvCRj8SzxDVd7IL2j2ssB6YXcfIbXq2EW7UGnrejwPRXf1tgUrIXW9KmOw=="],
|
"@opentui/core-darwin-x64": ["@opentui/core-darwin-x64@0.3.4", "", { "os": "darwin", "cpu": "x64" }, "sha512-Jvm9E8n2sPhKEyKSXn9GlmJcj8WoJXJTooXb3djwjVaiimjihIj0XxHzCWhdqbDtQp+VxDFyCKoQagOOz20qhA=="],
|
||||||
|
|
||||||
"@opentui/core-linux-arm64": ["@opentui/core-linux-arm64@0.4.3", "", { "os": "linux", "cpu": "arm64" }, "sha512-gl6qA5QJy6u8Cbt7gOtHbhhfMZ4qQDb0kEwFXHcMGmbnKzz4OHoq74D6tNjyvSQB9saoC7C6C0tvn2DcJOuNog=="],
|
"@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-musl": ["@opentui/core-linux-arm64-musl@0.4.3", "", { "os": "linux", "cpu": "arm64" }, "sha512-8p8g8/AEq/xFGpQ7XcIFKcAqjc0QwsZcv+Ll9RbCDpUA56FGH6jfLDir0KYTNTgYXJTIrBIENI9K46VuxMUMQA=="],
|
"@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-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": ["@opentui/core-linux-x64@0.3.4", "", { "os": "linux", "cpu": "x64" }, "sha512-btYIQeNdPbN4JCrCjVB/RwMGrnRY7qWB2piNEfALSByuULKNjPKQ33PYIj38Yd01zCvCV7FotIeXEGSHx3tgCA=="],
|
||||||
|
|
||||||
"@opentui/core-linux-x64-musl": ["@opentui/core-linux-x64-musl@0.4.3", "", { "os": "linux", "cpu": "x64" }, "sha512-/QiFpCrpU2O7vy8QYmLIQYbvAtKDgmqcVjR7dGtqSzkiQk3ktNJoo5RozG7ueXnjung1Wp0nKldKxo2Csg/OrA=="],
|
"@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-win32-arm64": ["@opentui/core-win32-arm64@0.4.3", "", { "os": "win32", "cpu": "arm64" }, "sha512-Mx2zuOjrhm/z2SDS6RExIyjP/SnN/8QhhagxURUw0jQi/NssGSeAllu1cBAFFnhobJL5QLTE4FU4CRhUK9svgg=="],
|
"@opentui/core-win32-arm64": ["@opentui/core-win32-arm64@0.3.4", "", { "os": "win32", "cpu": "arm64" }, "sha512-sh432vPU+eLp8eA4I0KWKKn7D0VHbk01YTg6mA9/ihCNYHntc6LZ8/sLvsPv8CvKscMotfIkh3M5YhdS36BuXw=="],
|
||||||
|
|
||||||
"@opentui/core-win32-x64": ["@opentui/core-win32-x64@0.4.3", "", { "os": "win32", "cpu": "x64" }, "sha512-NuoqvWKGXaYnmlqvu7Gg2lLI6yVMnS9OfWBvxp+7Q+McSgHFSTQmYBXaPpvQ8HikpQXE1nCeMPtuSG4PdZHe2w=="],
|
"@opentui/core-win32-x64": ["@opentui/core-win32-x64@0.3.4", "", { "os": "win32", "cpu": "x64" }, "sha512-dw8FcjUZaLAjw25P3/7BarobCh/QOHn3srYaWYQdysoqyvSlPkQumpI8kV/KgpJtdITU1GW02MQC4EeLIFFalA=="],
|
||||||
|
|
||||||
"@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/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/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=="],
|
"@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=="],
|
||||||
|
|
||||||
"@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=="],
|
||||||
|
|
||||||
@@ -2791,18 +2792,6 @@
|
|||||||
|
|
||||||
"@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=="],
|
||||||
@@ -3243,7 +3232,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.4", "", { "peerDependencies": { "typescript": "^5" } }, "sha512-AJzsqoVFs1KBbJbWHIYrVZLDC3NhTqqh25awRXqzoLzmBAKr5oqk6+CwuYHAekKx+VBCYVohBoKuRq40dV+TYg=="],
|
"bun-ffi-structs": ["bun-ffi-structs@0.2.2", "", { "peerDependencies": { "typescript": "^5" } }, "sha512-N/ZWtyN0piZlrXQT7TO0V+q952orYqkfhXRXM1Hcbb+R3QSiBH4vLnib187Mrs1H7pWIYECAmPeapGYDOMCl+w=="],
|
||||||
|
|
||||||
"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=="],
|
||||||
|
|
||||||
@@ -3611,7 +3600,7 @@
|
|||||||
|
|
||||||
"entities": ["entities@7.0.1", "", {}, "sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA=="],
|
"entities": ["entities@7.0.1", "", {}, "sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA=="],
|
||||||
|
|
||||||
"env-paths": ["env-paths@3.0.0", "", {}, "sha512-dtJUTepzMW3Lm/NPxRf3wP4642UWhjL2sQxc+ym2YMj1m/H2zDNQOlezafzkHwn6sMstjHTwG6iQQsctDW/b1A=="],
|
"env-paths": ["env-paths@2.2.1", "", {}, "sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A=="],
|
||||||
|
|
||||||
"err-code": ["err-code@2.0.3", "", {}, "sha512-2bmlRpNKBxT/CRmPOlyISQpNj+qSeYvcym/uT0Jx2bMOlKLtSy1ZmLuVxSEKKyor/N5yhvp/ZiG1oE3DEYMSFA=="],
|
"err-code": ["err-code@2.0.3", "", {}, "sha512-2bmlRpNKBxT/CRmPOlyISQpNj+qSeYvcym/uT0Jx2bMOlKLtSy1ZmLuVxSEKKyor/N5yhvp/ZiG1oE3DEYMSFA=="],
|
||||||
|
|
||||||
@@ -4533,7 +4522,7 @@
|
|||||||
|
|
||||||
"node-fetch-native": ["node-fetch-native@1.6.7", "", {}, "sha512-g9yhqoedzIUm0nTnTqAQvueMPVOuIY16bqgAJJC8XOOubYFNwz6IER9qs0Gq2Xd0+CecCKFjtdDTMA4u4xG06Q=="],
|
"node-fetch-native": ["node-fetch-native@1.6.7", "", {}, "sha512-g9yhqoedzIUm0nTnTqAQvueMPVOuIY16bqgAJJC8XOOubYFNwz6IER9qs0Gq2Xd0+CecCKFjtdDTMA4u4xG06Q=="],
|
||||||
|
|
||||||
"node-gyp": ["node-gyp@12.3.0", "", { "dependencies": { "env-paths": "^2.2.0", "exponential-backoff": "^3.1.1", "graceful-fs": "^4.2.6", "nopt": "^9.0.0", "proc-log": "^6.0.0", "semver": "^7.3.5", "tar": "^7.5.4", "tinyglobby": "^0.2.12", "undici": "^6.25.0", "which": "^6.0.0" }, "bin": { "node-gyp": "bin/node-gyp.js" } }, "sha512-QNcUWM+HgJplcPzBvFBZ9VXacyGZ4+VTOb80PwWR+TlVzoHbRKULNEzpRsnaoxG3Wzr7Qh7BYxGDU3CbKib2Yg=="],
|
"node-gyp": ["node-gyp@12.4.0", "", { "dependencies": { "env-paths": "^2.2.0", "exponential-backoff": "^3.1.1", "graceful-fs": "^4.2.6", "nopt": "^9.0.0", "proc-log": "^6.0.0", "semver": "^7.3.5", "tar": "^7.5.4", "tinyglobby": "^0.2.12", "undici": "^6.25.0", "which": "^6.0.0" }, "bin": { "node-gyp": "bin/node-gyp.js" } }, "sha512-OMcPNvqTCFUnNaBlmdgq+lfNqY7gTiSmNRDjY3uAXRyudeKZEZxu3CLtjMQrx4zZxCX2b/mpNqTtwuCJgXhHkw=="],
|
||||||
|
|
||||||
"node-gyp-build": ["node-gyp-build@4.8.4", "", { "bin": { "node-gyp-build": "bin.js", "node-gyp-build-optional": "optional.js", "node-gyp-build-test": "build-test.js" } }, "sha512-LA4ZjwlnUblHVgq0oBF3Jl/6h/Nvs5fzBLwdEF4nuxnFdsfajde4WfxtJr3CaiH+F6ewcIB/q4jQ4UzPyid+CQ=="],
|
"node-gyp-build": ["node-gyp-build@4.8.4", "", { "bin": { "node-gyp-build": "bin.js", "node-gyp-build-optional": "optional.js", "node-gyp-build-test": "build-test.js" } }, "sha512-LA4ZjwlnUblHVgq0oBF3Jl/6h/Nvs5fzBLwdEF4nuxnFdsfajde4WfxtJr3CaiH+F6ewcIB/q4jQ4UzPyid+CQ=="],
|
||||||
|
|
||||||
@@ -5333,7 +5322,19 @@
|
|||||||
|
|
||||||
"tunnel": ["tunnel@0.0.6", "", {}, "sha512-1h/Lnq9yajKY2PEbBadPXj3VxsDDu844OnaAo52UVmIzIvwwtBPIuNvkjuzBlTWpfJyUbG3ez0KSBibQkj4ojg=="],
|
"tunnel": ["tunnel@0.0.6", "", {}, "sha512-1h/Lnq9yajKY2PEbBadPXj3VxsDDu844OnaAo52UVmIzIvwwtBPIuNvkjuzBlTWpfJyUbG3ez0KSBibQkj4ojg=="],
|
||||||
|
|
||||||
"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": ["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-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=="],
|
||||||
|
|
||||||
@@ -5371,7 +5372,7 @@
|
|||||||
|
|
||||||
"uncrypto": ["uncrypto@0.1.3", "", {}, "sha512-Ql87qFHB3s/De2ClA9e0gsnS6zXG27SkTiSJwjCc9MebbfapQfuPzumMIUMi38ezPZVNFcHI9sUIepeQfw8J8Q=="],
|
"uncrypto": ["uncrypto@0.1.3", "", {}, "sha512-Ql87qFHB3s/De2ClA9e0gsnS6zXG27SkTiSJwjCc9MebbfapQfuPzumMIUMi38ezPZVNFcHI9sUIepeQfw8J8Q=="],
|
||||||
|
|
||||||
"undici": ["undici@8.3.0", "", {}, "sha512-TkUDgb6tl7KOGZ+7e8E3d2FYgUQgF6z5YypqjWmixVQSQERFcVrVg0ySADm2LVLRh5ljAaHTCR5Fmz3Q34rB7Q=="],
|
"undici": ["undici@6.26.0", "", {}, "sha512-4yqz8a3n5HmGTlsbADNtr/dJlhkh/55Rq798G6ibiULcXbDtaLpTl1pvdqcbFfeoj3iSi52lePFM7h9H21cw/A=="],
|
||||||
|
|
||||||
"undici-types": ["undici-types@7.16.0", "", {}, "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw=="],
|
"undici-types": ["undici-types@7.16.0", "", {}, "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw=="],
|
||||||
|
|
||||||
@@ -5573,7 +5574,7 @@
|
|||||||
|
|
||||||
"y18n": ["y18n@5.0.8", "", {}, "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA=="],
|
"y18n": ["y18n@5.0.8", "", {}, "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA=="],
|
||||||
|
|
||||||
"yallist": ["yallist@4.0.0", "", {}, "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A=="],
|
"yallist": ["yallist@5.0.0", "", {}, "sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw=="],
|
||||||
|
|
||||||
"yaml": ["yaml@2.9.0", "", { "bin": { "yaml": "bin.mjs" } }, "sha512-2AvhNX3mb8zd6Zy7INTtSpl1F15HW6Wnqj0srWlkKLcpYl/gMIMJiyuGq2KeI2YFxUPjdlB+3Lc10seMLtL4cA=="],
|
"yaml": ["yaml@2.9.0", "", { "bin": { "yaml": "bin.mjs" } }, "sha512-2AvhNX3mb8zd6Zy7INTtSpl1F15HW6Wnqj0srWlkKLcpYl/gMIMJiyuGq2KeI2YFxUPjdlB+3Lc10seMLtL4cA=="],
|
||||||
|
|
||||||
@@ -5591,6 +5592,8 @@
|
|||||||
|
|
||||||
"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=="],
|
||||||
@@ -5619,8 +5622,6 @@
|
|||||||
|
|
||||||
"@actions/github/undici": ["undici@5.29.0", "", { "dependencies": { "@fastify/busboy": "^2.0.0" } }, "sha512-raqeBD6NQK4SkWhQzeYKd1KmIG6dllBOTt55Rmkt4HtI9mwdWtJljnrXjAFUBLTSN67HWrOIZ3EPF4kjUw80Bg=="],
|
"@actions/github/undici": ["undici@5.29.0", "", { "dependencies": { "@fastify/busboy": "^2.0.0" } }, "sha512-raqeBD6NQK4SkWhQzeYKd1KmIG6dllBOTt55Rmkt4HtI9mwdWtJljnrXjAFUBLTSN67HWrOIZ3EPF4kjUw80Bg=="],
|
||||||
|
|
||||||
"@actions/http-client/undici": ["undici@6.26.0", "", {}, "sha512-4yqz8a3n5HmGTlsbADNtr/dJlhkh/55Rq798G6ibiULcXbDtaLpTl1pvdqcbFfeoj3iSi52lePFM7h9H21cw/A=="],
|
|
||||||
|
|
||||||
"@ai-sdk/alibaba/@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/alibaba/@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/amazon-bedrock/@ai-sdk/anthropic": ["@ai-sdk/anthropic@3.0.81", "", { "dependencies": { "@ai-sdk/provider": "3.0.10", "@ai-sdk/provider-utils": "4.0.27" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-B1JDd9Ugq9R5AgIaW3674lhGCMMYJcPUxnrZh8fzbGojgg4QvHFRv6eZahGQAUsmGHbcf74G9bdSBDLWQGY2GA=="],
|
"@ai-sdk/amazon-bedrock/@ai-sdk/anthropic": ["@ai-sdk/anthropic@3.0.81", "", { "dependencies": { "@ai-sdk/provider": "3.0.10", "@ai-sdk/provider-utils": "4.0.27" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-B1JDd9Ugq9R5AgIaW3674lhGCMMYJcPUxnrZh8fzbGojgg4QvHFRv6eZahGQAUsmGHbcf74G9bdSBDLWQGY2GA=="],
|
||||||
@@ -5693,11 +5694,7 @@
|
|||||||
|
|
||||||
"@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.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/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/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=="],
|
||||||
|
|
||||||
@@ -5877,6 +5874,8 @@
|
|||||||
|
|
||||||
"@dot/log/chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="],
|
"@dot/log/chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="],
|
||||||
|
|
||||||
|
"@effect/platform-node/undici": ["undici@8.3.0", "", {}, "sha512-TkUDgb6tl7KOGZ+7e8E3d2FYgUQgF6z5YypqjWmixVQSQERFcVrVg0ySADm2LVLRh5ljAaHTCR5Fmz3Q34rB7Q=="],
|
||||||
|
|
||||||
"@electron/asar/commander": ["commander@5.1.0", "", {}, "sha512-P0CysNDQ7rtVw4QIQtm+MRxV66vKFSvlsQvGYXZWR3qFU0jlMKHZZZgw8e+8DSah4UDKMqnknRDQz+xuQXQ/Zg=="],
|
"@electron/asar/commander": ["commander@5.1.0", "", {}, "sha512-P0CysNDQ7rtVw4QIQtm+MRxV66vKFSvlsQvGYXZWR3qFU0jlMKHZZZgw8e+8DSah4UDKMqnknRDQz+xuQXQ/Zg=="],
|
||||||
|
|
||||||
"@electron/asar/glob": ["glob@7.2.3", "", { "dependencies": { "fs.realpath": "^1.0.0", "inflight": "^1.0.4", "inherits": "2", "minimatch": "^3.1.1", "once": "^1.3.0", "path-is-absolute": "^1.0.0" } }, "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q=="],
|
"@electron/asar/glob": ["glob@7.2.3", "", { "dependencies": { "fs.realpath": "^1.0.0", "inflight": "^1.0.4", "inherits": "2", "minimatch": "^3.1.1", "once": "^1.3.0", "path-is-absolute": "^1.0.0" } }, "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q=="],
|
||||||
@@ -5887,12 +5886,16 @@
|
|||||||
|
|
||||||
"@electron/fuses/fs-extra": ["fs-extra@9.1.0", "", { "dependencies": { "at-least-node": "^1.0.0", "graceful-fs": "^4.2.0", "jsonfile": "^6.0.1", "universalify": "^2.0.0" } }, "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ=="],
|
"@electron/fuses/fs-extra": ["fs-extra@9.1.0", "", { "dependencies": { "at-least-node": "^1.0.0", "graceful-fs": "^4.2.0", "jsonfile": "^6.0.1", "universalify": "^2.0.0" } }, "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ=="],
|
||||||
|
|
||||||
|
"@electron/get/env-paths": ["env-paths@3.0.0", "", {}, "sha512-dtJUTepzMW3Lm/NPxRf3wP4642UWhjL2sQxc+ym2YMj1m/H2zDNQOlezafzkHwn6sMstjHTwG6iQQsctDW/b1A=="],
|
||||||
|
|
||||||
"@electron/get/undici": ["undici@7.26.0", "", {}, "sha512-3O9Tf67pGhgOv9jM35AbhkXAKi13f3oy3aE4CSgr+TckGeY+/iu97ZXN+J7DpHPzLbVApFd1IFhcnBjREYXYcg=="],
|
"@electron/get/undici": ["undici@7.26.0", "", {}, "sha512-3O9Tf67pGhgOv9jM35AbhkXAKi13f3oy3aE4CSgr+TckGeY+/iu97ZXN+J7DpHPzLbVApFd1IFhcnBjREYXYcg=="],
|
||||||
|
|
||||||
"@electron/notarize/fs-extra": ["fs-extra@9.1.0", "", { "dependencies": { "at-least-node": "^1.0.0", "graceful-fs": "^4.2.0", "jsonfile": "^6.0.1", "universalify": "^2.0.0" } }, "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ=="],
|
"@electron/notarize/fs-extra": ["fs-extra@9.1.0", "", { "dependencies": { "at-least-node": "^1.0.0", "graceful-fs": "^4.2.0", "jsonfile": "^6.0.1", "universalify": "^2.0.0" } }, "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ=="],
|
||||||
|
|
||||||
"@electron/osx-sign/isbinaryfile": ["isbinaryfile@4.0.10", "", {}, "sha512-iHrqe5shvBUcFbmZq9zOQHBoeOhZJu6RQGrDpBgenUm/Am+F3JM2MgQj+rK3Z601fzrL5gLZWtAPH2OBaSVcyw=="],
|
"@electron/osx-sign/isbinaryfile": ["isbinaryfile@4.0.10", "", {}, "sha512-iHrqe5shvBUcFbmZq9zOQHBoeOhZJu6RQGrDpBgenUm/Am+F3JM2MgQj+rK3Z601fzrL5gLZWtAPH2OBaSVcyw=="],
|
||||||
|
|
||||||
|
"@electron/rebuild/node-gyp": ["node-gyp@12.3.0", "", { "dependencies": { "env-paths": "^2.2.0", "exponential-backoff": "^3.1.1", "graceful-fs": "^4.2.6", "nopt": "^9.0.0", "proc-log": "^6.0.0", "semver": "^7.3.5", "tar": "^7.5.4", "tinyglobby": "^0.2.12", "undici": "^6.25.0", "which": "^6.0.0" }, "bin": { "node-gyp": "bin/node-gyp.js" } }, "sha512-QNcUWM+HgJplcPzBvFBZ9VXacyGZ4+VTOb80PwWR+TlVzoHbRKULNEzpRsnaoxG3Wzr7Qh7BYxGDU3CbKib2Yg=="],
|
||||||
|
|
||||||
"@electron/universal/fs-extra": ["fs-extra@11.3.5", "", { "dependencies": { "graceful-fs": "^4.2.0", "jsonfile": "^6.0.1", "universalify": "^2.0.0" } }, "sha512-eKpRKAovdpZtR1WopLHxlBWvAgPny3c4gX1G5Jhwmmw4XJj0ifSD5qB5TOo8hmA0wlRKDAOAhEE1yVPgs6Fgcg=="],
|
"@electron/universal/fs-extra": ["fs-extra@11.3.5", "", { "dependencies": { "graceful-fs": "^4.2.0", "jsonfile": "^6.0.1", "universalify": "^2.0.0" } }, "sha512-eKpRKAovdpZtR1WopLHxlBWvAgPny3c4gX1G5Jhwmmw4XJj0ifSD5qB5TOo8hmA0wlRKDAOAhEE1yVPgs6Fgcg=="],
|
||||||
|
|
||||||
"@electron/universal/minimatch": ["minimatch@9.0.9", "", { "dependencies": { "brace-expansion": "^2.0.2" } }, "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg=="],
|
"@electron/universal/minimatch": ["minimatch@9.0.9", "", { "dependencies": { "brace-expansion": "^2.0.2" } }, "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg=="],
|
||||||
@@ -5943,6 +5946,8 @@
|
|||||||
|
|
||||||
"@npmcli/query/postcss-selector-parser": ["postcss-selector-parser@7.1.1", "", { "dependencies": { "cssesc": "^3.0.0", "util-deprecate": "^1.0.2" } }, "sha512-orRsuYpJVw8LdAwqqLykBj9ecS5/cRHlI5+nvTo8LcCKmzDmqVORXtOIYEEQuL9D4BxtA1lm5isAqzQZCoQ6Eg=="],
|
"@npmcli/query/postcss-selector-parser": ["postcss-selector-parser@7.1.1", "", { "dependencies": { "cssesc": "^3.0.0", "util-deprecate": "^1.0.2" } }, "sha512-orRsuYpJVw8LdAwqqLykBj9ecS5/cRHlI5+nvTo8LcCKmzDmqVORXtOIYEEQuL9D4BxtA1lm5isAqzQZCoQ6Eg=="],
|
||||||
|
|
||||||
|
"@npmcli/run-script/node-gyp": ["node-gyp@12.3.0", "", { "dependencies": { "env-paths": "^2.2.0", "exponential-backoff": "^3.1.1", "graceful-fs": "^4.2.6", "nopt": "^9.0.0", "proc-log": "^6.0.0", "semver": "^7.3.5", "tar": "^7.5.4", "tinyglobby": "^0.2.12", "undici": "^6.25.0", "which": "^6.0.0" }, "bin": { "node-gyp": "bin/node-gyp.js" } }, "sha512-QNcUWM+HgJplcPzBvFBZ9VXacyGZ4+VTOb80PwWR+TlVzoHbRKULNEzpRsnaoxG3Wzr7Qh7BYxGDU3CbKib2Yg=="],
|
||||||
|
|
||||||
"@octokit/auth-app/@octokit/request": ["@octokit/request@10.0.10", "", { "dependencies": { "@octokit/endpoint": "^11.0.3", "@octokit/request-error": "^7.0.2", "@octokit/types": "^16.0.0", "content-type": "^2.0.0", "json-with-bigint": "^3.5.3", "universal-user-agent": "^7.0.2" } }, "sha512-KxNC2pTqqhszMNrf12ZRd4PonRgyJdsM4F/jySiddQK+DsRcfBtUvqn8t7UsyZhnRJHvX46OohDt5N3VqIWC2w=="],
|
"@octokit/auth-app/@octokit/request": ["@octokit/request@10.0.10", "", { "dependencies": { "@octokit/endpoint": "^11.0.3", "@octokit/request-error": "^7.0.2", "@octokit/types": "^16.0.0", "content-type": "^2.0.0", "json-with-bigint": "^3.5.3", "universal-user-agent": "^7.0.2" } }, "sha512-KxNC2pTqqhszMNrf12ZRd4PonRgyJdsM4F/jySiddQK+DsRcfBtUvqn8t7UsyZhnRJHvX46OohDt5N3VqIWC2w=="],
|
||||||
|
|
||||||
"@octokit/auth-app/@octokit/request-error": ["@octokit/request-error@7.1.0", "", { "dependencies": { "@octokit/types": "^16.0.0" } }, "sha512-KMQIfq5sOPpkQYajXHwnhjCC0slzCNScLHs9JafXc4RAJI+9f+jNDlBNaIMTvazOPLgb4BnlhGJOTbnN0wIjPw=="],
|
"@octokit/auth-app/@octokit/request-error": ["@octokit/request-error@7.1.0", "", { "dependencies": { "@octokit/types": "^16.0.0" } }, "sha512-KMQIfq5sOPpkQYajXHwnhjCC0slzCNScLHs9JafXc4RAJI+9f+jNDlBNaIMTvazOPLgb4BnlhGJOTbnN0wIjPw=="],
|
||||||
@@ -6147,8 +6152,6 @@
|
|||||||
|
|
||||||
"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=="],
|
||||||
@@ -6219,6 +6222,8 @@
|
|||||||
|
|
||||||
"conf/dot-prop": ["dot-prop@10.1.0", "", { "dependencies": { "type-fest": "^5.0.0" } }, "sha512-MVUtAugQMOff5RnBy2d9N31iG0lNwg1qAoAOn7pOK5wf94WIaE3My2p3uwTQuvS2AcqchkcR3bHByjaM0mmi7Q=="],
|
"conf/dot-prop": ["dot-prop@10.1.0", "", { "dependencies": { "type-fest": "^5.0.0" } }, "sha512-MVUtAugQMOff5RnBy2d9N31iG0lNwg1qAoAOn7pOK5wf94WIaE3My2p3uwTQuvS2AcqchkcR3bHByjaM0mmi7Q=="],
|
||||||
|
|
||||||
|
"conf/env-paths": ["env-paths@3.0.0", "", {}, "sha512-dtJUTepzMW3Lm/NPxRf3wP4642UWhjL2sQxc+ym2YMj1m/H2zDNQOlezafzkHwn6sMstjHTwG6iQQsctDW/b1A=="],
|
||||||
|
|
||||||
"config-chain/ini": ["ini@1.3.8", "", {}, "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew=="],
|
"config-chain/ini": ["ini@1.3.8", "", {}, "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew=="],
|
||||||
|
|
||||||
"crc/buffer": ["buffer@5.7.1", "", { "dependencies": { "base64-js": "^1.3.1", "ieee754": "^1.1.13" } }, "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ=="],
|
"crc/buffer": ["buffer@5.7.1", "", { "dependencies": { "base64-js": "^1.3.1", "ieee754": "^1.1.13" } }, "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ=="],
|
||||||
@@ -6357,9 +6362,7 @@
|
|||||||
|
|
||||||
"nitro/undici": ["undici@7.26.0", "", {}, "sha512-3O9Tf67pGhgOv9jM35AbhkXAKi13f3oy3aE4CSgr+TckGeY+/iu97ZXN+J7DpHPzLbVApFd1IFhcnBjREYXYcg=="],
|
"nitro/undici": ["undici@7.26.0", "", {}, "sha512-3O9Tf67pGhgOv9jM35AbhkXAKi13f3oy3aE4CSgr+TckGeY+/iu97ZXN+J7DpHPzLbVApFd1IFhcnBjREYXYcg=="],
|
||||||
|
|
||||||
"node-gyp/env-paths": ["env-paths@2.2.1", "", {}, "sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A=="],
|
"node-gyp/semver": ["semver@7.8.5", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA=="],
|
||||||
|
|
||||||
"node-gyp/undici": ["undici@6.26.0", "", {}, "sha512-4yqz8a3n5HmGTlsbADNtr/dJlhkh/55Rq798G6ibiULcXbDtaLpTl1pvdqcbFfeoj3iSi52lePFM7h9H21cw/A=="],
|
|
||||||
|
|
||||||
"node-gyp-build-optional-packages/detect-libc": ["detect-libc@2.1.2", "", {}, "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ=="],
|
"node-gyp-build-optional-packages/detect-libc": ["detect-libc@2.1.2", "", {}, "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ=="],
|
||||||
|
|
||||||
@@ -6463,8 +6466,6 @@
|
|||||||
|
|
||||||
"sucrase/commander": ["commander@4.1.1", "", {}, "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA=="],
|
"sucrase/commander": ["commander@4.1.1", "", {}, "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA=="],
|
||||||
|
|
||||||
"tar/yallist": ["yallist@5.0.0", "", {}, "sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw=="],
|
|
||||||
|
|
||||||
"terser/acorn": ["acorn@8.16.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw=="],
|
"terser/acorn": ["acorn@8.16.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw=="],
|
||||||
|
|
||||||
"terser/commander": ["commander@2.20.3", "", {}, "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ=="],
|
"terser/commander": ["commander@2.20.3", "", {}, "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ=="],
|
||||||
@@ -6583,8 +6584,6 @@
|
|||||||
|
|
||||||
"@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=="],
|
||||||
@@ -6947,16 +6946,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/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=="],
|
||||||
|
|
||||||
"ansi-align/string-width/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="],
|
"ansi-align/string-width/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="],
|
||||||
|
|
||||||
"app-builder-lib/@electron/get/env-paths": ["env-paths@2.2.1", "", {}, "sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A=="],
|
|
||||||
|
|
||||||
"app-builder-lib/@electron/get/fs-extra": ["fs-extra@8.1.0", "", { "dependencies": { "graceful-fs": "^4.2.0", "jsonfile": "^4.0.0", "universalify": "^0.1.0" } }, "sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g=="],
|
"app-builder-lib/@electron/get/fs-extra": ["fs-extra@8.1.0", "", { "dependencies": { "graceful-fs": "^4.2.0", "jsonfile": "^4.0.0", "universalify": "^0.1.0" } }, "sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g=="],
|
||||||
|
|
||||||
"app-builder-lib/@electron/get/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="],
|
"app-builder-lib/@electron/get/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="],
|
||||||
@@ -7087,6 +7082,10 @@
|
|||||||
|
|
||||||
"lazystream/readable-stream/string_decoder": ["string_decoder@1.1.1", "", { "dependencies": { "safe-buffer": "~5.1.0" } }, "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg=="],
|
"lazystream/readable-stream/string_decoder": ["string_decoder@1.1.1", "", { "dependencies": { "safe-buffer": "~5.1.0" } }, "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg=="],
|
||||||
|
|
||||||
|
"minipass-flush/minipass/yallist": ["yallist@4.0.0", "", {}, "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A=="],
|
||||||
|
|
||||||
|
"minipass-pipeline/minipass/yallist": ["yallist@4.0.0", "", {}, "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A=="],
|
||||||
|
|
||||||
"motion/framer-motion/motion-dom": ["motion-dom@12.40.0", "", { "dependencies": { "motion-utils": "^12.39.0" } }, "sha512-HxU3ZaBwNPVQUBQf1xxgq+7JrPNZvjLVxgbpEZL7RrWJnsxOf0/OM+yrHG9ogLQ31Do/r57Oz2gQWPK+6q62mg=="],
|
"motion/framer-motion/motion-dom": ["motion-dom@12.40.0", "", { "dependencies": { "motion-utils": "^12.39.0" } }, "sha512-HxU3ZaBwNPVQUBQf1xxgq+7JrPNZvjLVxgbpEZL7RrWJnsxOf0/OM+yrHG9ogLQ31Do/r57Oz2gQWPK+6q62mg=="],
|
||||||
|
|
||||||
"motion/framer-motion/motion-utils": ["motion-utils@12.39.0", "", {}, "sha512-8nadJAJjTtqRkmRF36FoJTrywK9nnFmnPwnSMyxaOCU7GDjN9RTMJIxx9De8ErM+vpPhMccr/6fo5WciyQLnMQ=="],
|
"motion/framer-motion/motion-utils": ["motion-utils@12.39.0", "", {}, "sha512-8nadJAJjTtqRkmRF36FoJTrywK9nnFmnPwnSMyxaOCU7GDjN9RTMJIxx9De8ErM+vpPhMccr/6fo5WciyQLnMQ=="],
|
||||||
@@ -7099,6 +7098,8 @@
|
|||||||
|
|
||||||
"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=="],
|
"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=="],
|
||||||
|
|
||||||
|
"openid-client/lru-cache/yallist": ["yallist@4.0.0", "", {}, "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A=="],
|
||||||
|
|
||||||
"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=="],
|
||||||
@@ -7367,6 +7368,8 @@
|
|||||||
|
|
||||||
"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=="],
|
||||||
|
|
||||||
|
"app-builder-lib/hosted-git-info/lru-cache/yallist": ["yallist@4.0.0", "", {}, "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A=="],
|
||||||
|
|
||||||
"archiver-utils/glob/jackspeak/@isaacs/cliui": ["@isaacs/cliui@8.0.2", "", { "dependencies": { "string-width": "^5.1.2", "string-width-cjs": "npm:string-width@^4.2.0", "strip-ansi": "^7.0.1", "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", "wrap-ansi": "^8.1.0", "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" } }, "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA=="],
|
"archiver-utils/glob/jackspeak/@isaacs/cliui": ["@isaacs/cliui@8.0.2", "", { "dependencies": { "string-width": "^5.1.2", "string-width-cjs": "npm:string-width@^4.2.0", "strip-ansi": "^7.0.1", "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", "wrap-ansi": "^8.1.0", "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" } }, "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA=="],
|
||||||
|
|
||||||
"archiver-utils/glob/minimatch/brace-expansion": ["brace-expansion@2.1.1", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-WR1cURNjuvBLMZBMbqM0UoE+WAfdUcEV1ccD8PVBVOI+Z3ND4+SZbN8RsfT2bMuG1qwz5RFvPukSZm5fF2D5eA=="],
|
"archiver-utils/glob/minimatch/brace-expansion": ["brace-expansion@2.1.1", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-WR1cURNjuvBLMZBMbqM0UoE+WAfdUcEV1ccD8PVBVOI+Z3ND4+SZbN8RsfT2bMuG1qwz5RFvPukSZm5fF2D5eA=="],
|
||||||
@@ -7389,8 +7392,6 @@
|
|||||||
|
|
||||||
"editorconfig/minimatch/brace-expansion/balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="],
|
"editorconfig/minimatch/brace-expansion/balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="],
|
||||||
|
|
||||||
"electron-builder-squirrel-windows/app-builder-lib/@electron/get/env-paths": ["env-paths@2.2.1", "", {}, "sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A=="],
|
|
||||||
|
|
||||||
"electron-builder-squirrel-windows/app-builder-lib/@electron/get/fs-extra": ["fs-extra@8.1.0", "", { "dependencies": { "graceful-fs": "^4.2.0", "jsonfile": "^4.0.0", "universalify": "^0.1.0" } }, "sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g=="],
|
"electron-builder-squirrel-windows/app-builder-lib/@electron/get/fs-extra": ["fs-extra@8.1.0", "", { "dependencies": { "graceful-fs": "^4.2.0", "jsonfile": "^4.0.0", "universalify": "^0.1.0" } }, "sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g=="],
|
||||||
|
|
||||||
"electron-builder-squirrel-windows/app-builder-lib/@electron/get/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="],
|
"electron-builder-squirrel-windows/app-builder-lib/@electron/get/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="],
|
||||||
@@ -7499,6 +7500,8 @@
|
|||||||
|
|
||||||
"electron-builder-squirrel-windows/app-builder-lib/@electron/get/fs-extra/universalify": ["universalify@0.1.2", "", {}, "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg=="],
|
"electron-builder-squirrel-windows/app-builder-lib/@electron/get/fs-extra/universalify": ["universalify@0.1.2", "", {}, "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg=="],
|
||||||
|
|
||||||
|
"electron-builder-squirrel-windows/app-builder-lib/hosted-git-info/lru-cache/yallist": ["yallist@4.0.0", "", {}, "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A=="],
|
||||||
|
|
||||||
"electron-builder/yargs/cliui/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="],
|
"electron-builder/yargs/cliui/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="],
|
||||||
|
|
||||||
"electron-builder/yargs/string-width/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="],
|
"electron-builder/yargs/string-width/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="],
|
||||||
|
|||||||
+4
-4
@@ -1,8 +1,8 @@
|
|||||||
{
|
{
|
||||||
"nodeModules": {
|
"nodeModules": {
|
||||||
"x86_64-linux": "sha256-K0V7FyvgujbeAf0Ip4SCJxcy0GpSt/H+iEwTVhvwBRQ=",
|
"x86_64-linux": "sha256-ZmAIcG3wPgssKejLe/EMJOjPuMHCyRiiItRvSqIjaME=",
|
||||||
"aarch64-linux": "sha256-lz2GhziKY9zL4vXb2/CmlXKrmVYPF5hPFD1ZpfDOqgM=",
|
"aarch64-linux": "sha256-lbAHoVI/CvgdrfKZ/9dKZ/O7DxJvuWEOrDgcCuFGPBk=",
|
||||||
"aarch64-darwin": "sha256-vfxsqozBq40tc1v6w8xvl1HIAif8gRn6oIYrLt6fZ1c=",
|
"aarch64-darwin": "sha256-Nrs4ijvzQ3J8AYigJSlsr66feUvLZ5iiYCfBdaZNJEc=",
|
||||||
"x86_64-darwin": "sha256-X7GAwO+8A6vwqObIHFtloDDJZO8ZUTqmplFDg1zfots="
|
"x86_64-darwin": "sha256-ltCaJSO7kZWuMOU49269xvboNVX35OJDFj0gXCDf62Y="
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+6
-6
@@ -19,7 +19,6 @@
|
|||||||
"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": {
|
||||||
@@ -40,9 +39,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.4.3",
|
"@opentui/core": "0.3.4",
|
||||||
"@opentui/keymap": "0.4.3",
|
"@opentui/keymap": "0.3.4",
|
||||||
"@opentui/solid": "0.4.3",
|
"@opentui/solid": "0.3.4",
|
||||||
"@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",
|
||||||
@@ -100,12 +99,13 @@
|
|||||||
"@typescript/native-preview": "catalog:",
|
"@typescript/native-preview": "catalog:",
|
||||||
"glob": "13.0.5",
|
"glob": "13.0.5",
|
||||||
"husky": "9.1.7",
|
"husky": "9.1.7",
|
||||||
|
"node-gyp": "12.4.0",
|
||||||
"oxlint": "1.60.0",
|
"oxlint": "1.60.0",
|
||||||
"oxlint-tsgolint": "0.21.0",
|
"oxlint-tsgolint": "0.21.0",
|
||||||
"prettier": "3.6.2",
|
"prettier": "3.6.2",
|
||||||
"semver": "^7.6.0",
|
"semver": "^7.6.0",
|
||||||
"sst": "catalog:",
|
"sst": "catalog:",
|
||||||
"turbo": "2.10.2"
|
"turbo": "2.8.13"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@aws-sdk/client-s3": "3.933.0",
|
"@aws-sdk/client-s3": "3.933.0",
|
||||||
@@ -147,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.102": "patches/@ai-sdk%2Fxai@3.0.102.patch",
|
"@ai-sdk/xai@3.0.82": "patches/@ai-sdk%2Fxai@3.0.82.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",
|
||||||
|
|||||||
@@ -48,40 +48,6 @@ 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"
|
||||||
|
|||||||
@@ -1,312 +0,0 @@
|
|||||||
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 },
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -1,151 +0,0 @@
|
|||||||
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,8 +4,6 @@ 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
|
||||||
@@ -43,8 +41,7 @@ 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 &&
|
||||||
sample.requiredPartVisible !== false &&
|
Math.abs(sample.bottomErrorPx ?? Infinity) <= 1
|
||||||
(sample.bottomAnchorRequired === false || Math.abs(sample.bottomErrorPx ?? Infinity) <= 1)
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -8,16 +8,9 @@ type SessionSwitchProbe = {
|
|||||||
|
|
||||||
async function installSessionSwitchProbe(
|
async function installSessionSwitchProbe(
|
||||||
page: Page,
|
page: Page,
|
||||||
input: {
|
input: { destinationIDs: string[]; sourceIDs: string[]; lastID: string; href: string },
|
||||||
destinationIDs: string[]
|
|
||||||
sourceIDs: string[]
|
|
||||||
lastID: string
|
|
||||||
requiredPartID?: string
|
|
||||||
requireBottomAnchor?: boolean
|
|
||||||
href: string
|
|
||||||
},
|
|
||||||
) {
|
) {
|
||||||
await page.evaluate(({ destinationIDs, sourceIDs, lastID, requiredPartID, requireBottomAnchor, href }) => {
|
await page.evaluate(({ destinationIDs, sourceIDs, lastID, 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[] = []
|
||||||
@@ -73,13 +66,6 @@ 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,
|
||||||
@@ -87,22 +73,11 @@ 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({
|
samples.push({ observedAtMs, destination: [], source: [], hasVisibleRows: false, last: false, review })
|
||||||
observedAtMs,
|
|
||||||
destination: [],
|
|
||||||
source: [],
|
|
||||||
hasVisibleRows: false,
|
|
||||||
last: false,
|
|
||||||
requiredPartVisible: requiredPartID ? false : undefined,
|
|
||||||
bottomAnchorRequired: requireBottomAnchor !== false,
|
|
||||||
review,
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
requestAnimationFrame(sample)
|
requestAnimationFrame(sample)
|
||||||
}, 0)
|
}, 0)
|
||||||
@@ -142,8 +117,7 @@ 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 &&
|
||||||
sample.requiredPartVisible !== false &&
|
Math.abs(sample.bottomErrorPx ?? Infinity) <= 1,
|
||||||
(sample.bottomAnchorRequired === false || Math.abs(sample.bottomErrorPx ?? Infinity) <= 1),
|
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
@@ -161,27 +135,13 @@ async function collectSessionSwitchResult(page: Page) {
|
|||||||
|
|
||||||
export async function measureSessionSwitch(
|
export async function measureSessionSwitch(
|
||||||
page: Page,
|
page: Page,
|
||||||
input: {
|
input: { destinationIDs: string[]; sourceIDs: string[]; lastID: string; href: string; switch: () => Promise<void> },
|
||||||
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)
|
||||||
try {
|
|
||||||
await run()
|
await run()
|
||||||
await waitForStableSessionSwitch(page)
|
await waitForStableSessionSwitch(page)
|
||||||
return await collectSessionSwitchResult(page)
|
return 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) {
|
||||||
|
|||||||
@@ -1,46 +0,0 @@
|
|||||||
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,35 +52,3 @@ 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)
|
|
||||||
})
|
|
||||||
|
|||||||
@@ -1,55 +0,0 @@
|
|||||||
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)
|
|
||||||
})
|
|
||||||
@@ -1,40 +0,0 @@
|
|||||||
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()
|
|
||||||
})
|
|
||||||
@@ -1,83 +0,0 @@
|
|||||||
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)
|
|
||||||
})
|
|
||||||
@@ -1,146 +0,0 @@
|
|||||||
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",
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,152 +0,0 @@
|
|||||||
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,9 +10,6 @@ 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"
|
||||||
|
|
||||||
@@ -28,34 +25,20 @@ 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: "generated-0000.ts" }))
|
await expectAppVisible(page.getByRole("button", { name: /example\.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 }
|
||||||
@@ -97,7 +80,16 @@ 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: diffs,
|
vcsDiff: [
|
||||||
|
{
|
||||||
|
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: [] }),
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -135,13 +127,3 @@ 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`,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,263 +0,0 @@
|
|||||||
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}`,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,220 +0,0 @@
|
|||||||
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,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -1,89 +0,0 @@
|
|||||||
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 panel while it is hidden", async ({ page }) => {
|
test("unmounts the terminal renderer while the pane 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,14 +64,13 @@ test("unmounts the terminal panel while it 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).toHaveCount(0)
|
await expect(panel).toHaveAttribute("aria-hidden", "true")
|
||||||
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,7 +10,6 @@
|
|||||||
"./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", "/experimental/resource"])
|
const emptyObject = new Set(["/global/config", "/config", "/provider/auth", "/mcp"])
|
||||||
|
|
||||||
export interface MockServerConfig {
|
export interface MockServerConfig {
|
||||||
provider: unknown
|
provider: unknown
|
||||||
@@ -11,10 +11,7 @@ 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[]
|
||||||
@@ -22,7 +19,6 @@ 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
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -56,7 +52,6 @@ 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")
|
||||||
@@ -67,23 +62,6 @@ 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])
|
||||||
@@ -94,18 +72,6 @@ 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, [])
|
||||||
@@ -116,8 +82,7 @@ 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" })
|
||||||
await config.beforeMessagesResponse?.({ sessionID: messagesMatch[1]!, before })
|
if (config.messageDelay) await new Promise((resolve) => setTimeout(resolve, config.messageDelay))
|
||||||
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.15",
|
"version": "1.17.13",
|
||||||
"description": "",
|
"description": "",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"exports": {
|
"exports": {
|
||||||
|
|||||||
+36
-109
@@ -9,10 +9,9 @@ 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, useNavigate, useParams, useSearchParams } from "@solidjs/router"
|
import { type BaseRouterProps, Navigate, Route, Router, 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,
|
||||||
@@ -33,7 +32,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, useServerSync } from "@/context/server-sync"
|
import { ServerSyncProvider } 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"
|
||||||
@@ -53,10 +52,9 @@ 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 { legacySessionHref, legacySessionServer, requireServerKey, sessionHref } from "./utils/session-route"
|
import { legacySessionServer, requireServerKey, sessionHref } from "./utils/session-route"
|
||||||
import { createSessionLineage } from "@/pages/session/session-lineage"
|
|
||||||
|
|
||||||
import { SessionPage, SessionRouteErrorBoundary, TargetSessionRouteContent } from "@/pages/session"
|
import { SessionPage, 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"))
|
||||||
@@ -90,14 +88,10 @@ const SessionRoute = () => {
|
|||||||
tabs.newDraft({ server: server.key, directory: sdk().directory }, search.prompt)
|
tabs.newDraft({ server: server.key, directory: sdk().directory }, search.prompt)
|
||||||
})
|
})
|
||||||
|
|
||||||
return (
|
return <SessionPage />
|
||||||
<SessionRouteErrorBoundary sessionID={params.id}>
|
|
||||||
<SessionPage />
|
|
||||||
</SessionRouteErrorBoundary>
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function TargetServerRoute(props: ParentProps) {
|
const TargetSessionRoute = () => {
|
||||||
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(() => {
|
||||||
@@ -111,47 +105,14 @@ function TargetServerRoute(props: ParentProps) {
|
|||||||
// 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}>{props.children}</ServerSyncProvider>
|
<ServerSyncProvider server={conn}>
|
||||||
|
<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.
|
||||||
@@ -165,17 +126,16 @@ function SelectedServerProviders(props: ParentProps) {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
function LegacyServerLayout(props: ParentProps<{ serverScoped?: JSX.Element }>) {
|
function LegacyServerLayout(props: ParentProps) {
|
||||||
return (
|
return (
|
||||||
<SelectedServerProviders>
|
<SelectedServerProviders>
|
||||||
<LegacyServerScopedShell serverScoped={props.serverScoped}>{props.children}</LegacyServerScopedShell>
|
<LegacyServerScopedShell>{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()}>
|
||||||
@@ -184,14 +144,7 @@ function DraftRoute() {
|
|||||||
keyed
|
keyed
|
||||||
fallback={<Navigate href="/" />}
|
fallback={<Navigate href="/" />}
|
||||||
>
|
>
|
||||||
{(draft) => (
|
{(draft) => <ResolvedDraftRoute draft={draft} />}
|
||||||
<Show
|
|
||||||
when={settings.general.newLayoutDesigns()}
|
|
||||||
fallback={<Navigate href={`/${base64Encode(draft.directory)}/session`} />}
|
|
||||||
>
|
|
||||||
<ResolvedDraftRoute draft={draft} />
|
|
||||||
</Show>
|
|
||||||
)}
|
|
||||||
</Show>
|
</Show>
|
||||||
</Show>
|
</Show>
|
||||||
)
|
)
|
||||||
@@ -233,7 +186,7 @@ declare global {
|
|||||||
deepLinks?: string[]
|
deepLinks?: string[]
|
||||||
}
|
}
|
||||||
api?: {
|
api?: {
|
||||||
setTitlebar?: (theme: { mode: "light" | "dark"; scheme?: "system" | "light" | "dark" }) => Promise<void>
|
setTitlebar?: (theme: { mode: "light" | "dark" }) => Promise<void>
|
||||||
exportDebugLogs?: () => Promise<string>
|
exportDebugLogs?: () => Promise<string>
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -310,14 +263,12 @@ 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>
|
||||||
@@ -326,16 +277,16 @@ function ServerScopedProviders(props: ServerScopedShellProps) {
|
|||||||
|
|
||||||
function LegacyServerScopedShell(props: ServerScopedShellProps) {
|
function LegacyServerScopedShell(props: ServerScopedShellProps) {
|
||||||
return (
|
return (
|
||||||
<ServerScopedProviders directory={props.directory} sessionID={props.sessionID} serverScoped={props.serverScoped}>
|
<ServerScopedProviders directory={props.directory} sessionID={props.sessionID}>
|
||||||
<LegacyLayout>{props.children}</LegacyLayout>
|
<LegacyLayout>{props.children}</LegacyLayout>
|
||||||
</ServerScopedProviders>
|
</ServerScopedProviders>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
function NewAppLayout(props: ParentProps<{ serverScoped?: JSX.Element }>) {
|
function NewAppLayout(props: ParentProps) {
|
||||||
return (
|
return (
|
||||||
<SelectedServerProviders>
|
<SelectedServerProviders>
|
||||||
<ServerScopedProviders serverScoped={props.serverScoped}>
|
<ServerScopedProviders>
|
||||||
<NewLayout>{props.children}</NewLayout>
|
<NewLayout>{props.children}</NewLayout>
|
||||||
</ServerScopedProviders>
|
</ServerScopedProviders>
|
||||||
</SelectedServerProviders>
|
</SelectedServerProviders>
|
||||||
@@ -367,8 +318,8 @@ export function AppBaseProviders(props: ParentProps<{ locale?: Locale }>) {
|
|||||||
<MetaProvider>
|
<MetaProvider>
|
||||||
<Font />
|
<Font />
|
||||||
<ThemeProvider
|
<ThemeProvider
|
||||||
onThemeApplied={(_, mode, scheme) => {
|
onThemeApplied={(_, mode) => {
|
||||||
void window.api?.setTitlebar?.({ mode, scheme })
|
void window.api?.setTitlebar?.({ mode })
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<LanguageProvider locale={props.locale}>
|
<LanguageProvider locale={props.locale}>
|
||||||
@@ -396,7 +347,7 @@ export function AppBaseProviders(props: ParentProps<{ locale?: Locale }>) {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
function ConnectionGate(props: ParentProps<{ disableHealthCheck?: boolean; startup?: Promise<void> }>) {
|
function ConnectionGate(props: ParentProps<{ disableHealthCheck?: boolean }>) {
|
||||||
const server = useServer()
|
const server = useServer()
|
||||||
const checkServerHealth = useCheckServerHealth()
|
const checkServerHealth = useCheckServerHealth()
|
||||||
|
|
||||||
@@ -425,21 +376,16 @@ function ConnectionGate(props: ParentProps<{ disableHealthCheck?: boolean; start
|
|||||||
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
|
||||||
<Show when={!checking()}>
|
when={!checking()}
|
||||||
|
fallback={
|
||||||
|
<div class="h-dvh w-screen flex flex-col items-center justify-center bg-background-base">
|
||||||
|
<Splash class="w-16 h-20 opacity-50 animate-pulse" />
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
>
|
||||||
<Show
|
<Show
|
||||||
when={startupHealthCheck.latest}
|
when={startupHealthCheck.latest}
|
||||||
fallback={
|
fallback={
|
||||||
@@ -458,12 +404,6 @@ function ConnectionGate(props: ParentProps<{ disableHealthCheck?: boolean; start
|
|||||||
{props.children}
|
{props.children}
|
||||||
</Show>
|
</Show>
|
||||||
</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" />
|
|
||||||
</div>
|
|
||||||
</Show>
|
|
||||||
</>
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -530,8 +470,6 @@ 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
|
||||||
@@ -553,7 +491,7 @@ export function AppInterface(props: {
|
|||||||
>
|
>
|
||||||
<GlobalProvider>
|
<GlobalProvider>
|
||||||
<SettingsProvider>
|
<SettingsProvider>
|
||||||
<ConnectionGate disableHealthCheck={props.disableHealthCheck} startup={props.startup}>
|
<ConnectionGate disableHealthCheck={props.disableHealthCheck}>
|
||||||
<Show when={useSettings().general.newLayoutDesigns().toString()} keyed>
|
<Show when={useSettings().general.newLayoutDesigns().toString()} keyed>
|
||||||
<Dynamic
|
<Dynamic
|
||||||
component={props.router ?? Router}
|
component={props.router ?? Router}
|
||||||
@@ -562,14 +500,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 serverScoped={props.serverScoped}>{routerProps.children}</NewAppLayout>
|
<NewAppLayout>{routerProps.children}</NewAppLayout>
|
||||||
</Show>
|
</Show>
|
||||||
</ServerShell>
|
</ServerShell>
|
||||||
</NotificationProvider>
|
</NotificationProvider>
|
||||||
</TabsProvider>
|
</TabsProvider>
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<Routes serverScoped={props.serverScoped} />
|
<Routes />
|
||||||
</Dynamic>
|
</Dynamic>
|
||||||
</Show>
|
</Show>
|
||||||
</ConnectionGate>
|
</ConnectionGate>
|
||||||
@@ -579,24 +517,13 @@ export function AppInterface(props: {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
function Routes(props: { serverScoped?: JSX.Element }) {
|
function Routes() {
|
||||||
const settings = useSettings()
|
const settings = useSettings()
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Route
|
<Route component={LegacyServerLayout}>
|
||||||
component={(routeProps) => (
|
<Show when={!settings.general.newLayoutDesigns()}>{<Route path="/" component={LegacyHome} />}</Show>
|
||||||
<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} />
|
||||||
@@ -604,15 +531,15 @@ function Routes(props: { serverScoped?: JSX.Element }) {
|
|||||||
</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={NewLayoutLegacySessionRedirect} />
|
<Route path="/:dir/session/:id" component={LegacyTargetSessionRoute} />
|
||||||
<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 NewLayoutLegacySessionRedirect() {
|
function LegacyTargetSessionRoute() {
|
||||||
const server = useServer()
|
const server = useServer()
|
||||||
const tabs = useTabs()
|
const tabs = useTabs()
|
||||||
const params = useParams<{ id: string }>()
|
const params = useParams<{ id: string }>()
|
||||||
|
|||||||
@@ -1,325 +0,0 @@
|
|||||||
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,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,220 +0,0 @@
|
|||||||
.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%;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,221 +0,0 @@
|
|||||||
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,174 +7,29 @@ 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 {
|
import { type Accessor, createEffect, createMemo, createResource, Match, onCleanup, onMount, Switch } from "solid-js"
|
||||||
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 { popularProviders, useProviders } from "@/hooks/use-providers"
|
import { useProviders } from "@/hooks/use-providers"
|
||||||
import { CustomProviderForm } from "./dialog-custom-provider"
|
|
||||||
|
|
||||||
const CUSTOM_ID = "_custom"
|
export function DialogConnectProvider(props: { provider: string; directory?: Accessor<string | undefined> }) {
|
||||||
|
|
||||||
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 }
|
||||||
|
|
||||||
@@ -509,14 +364,20 @@ function ProviderConnection(props: {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function goBack() {
|
function goBack() {
|
||||||
if (methods().length > 1 && store.methodIndex !== undefined) {
|
if (methods().length === 1) {
|
||||||
|
all()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (store.authorization) {
|
||||||
dispatch({ type: "method.reset" })
|
dispatch({ type: "method.reset" })
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
props.onBack()
|
if (store.methodIndex !== undefined) {
|
||||||
|
dispatch({ type: "method.reset" })
|
||||||
|
return
|
||||||
|
}
|
||||||
|
all()
|
||||||
}
|
}
|
||||||
|
|
||||||
props.setBack(goBack)
|
|
||||||
|
|
||||||
function MethodSelection() {
|
function MethodSelection() {
|
||||||
return (
|
return (
|
||||||
@@ -738,6 +599,17 @@ function ProviderConnection(props: {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
<Dialog
|
||||||
|
title={
|
||||||
|
<IconButton
|
||||||
|
tabIndex={-1}
|
||||||
|
icon="arrow-left"
|
||||||
|
variant="ghost"
|
||||||
|
onClick={goBack}
|
||||||
|
aria-label={language.t("common.goBack")}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
>
|
||||||
<div class="flex flex-col gap-6 px-2.5 pb-3">
|
<div class="flex flex-col gap-6 px-2.5 pb-3">
|
||||||
<div class="px-2.5 flex gap-4 items-center">
|
<div class="px-2.5 flex gap-4 items-center">
|
||||||
<ProviderIcon id={props.provider} class="size-5 shrink-0 icon-strong-base" />
|
<ProviderIcon id={props.provider} class="size-5 shrink-0 icon-strong-base" />
|
||||||
@@ -800,5 +672,6 @@ function ProviderConnection(props: {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</Dialog>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,41 +6,21 @@ 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 { batch, For } from "solid-js"
|
import { type Accessor, 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 = {
|
||||||
onBack: () => void
|
back?: "providers" | "close"
|
||||||
|
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()
|
||||||
@@ -56,6 +36,14 @@ export function CustomProviderForm() {
|
|||||||
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",
|
||||||
@@ -175,6 +163,18 @@ export function CustomProviderForm() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
<Dialog
|
||||||
|
title={
|
||||||
|
<IconButton
|
||||||
|
tabIndex={-1}
|
||||||
|
icon="arrow-left"
|
||||||
|
variant="ghost"
|
||||||
|
onClick={goBack}
|
||||||
|
aria-label={language.t("common.goBack")}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
transition
|
||||||
|
>
|
||||||
<div class="flex flex-col gap-6 px-2.5 pb-3 overflow-y-auto max-h-[60vh]">
|
<div class="flex flex-col gap-6 px-2.5 pb-3 overflow-y-auto max-h-[60vh]">
|
||||||
<div class="px-2.5 flex gap-4 items-center">
|
<div class="px-2.5 flex gap-4 items-center">
|
||||||
<ProviderIcon id="synthetic" class="size-5 shrink-0 icon-strong-base" />
|
<ProviderIcon id="synthetic" class="size-5 shrink-0 icon-strong-base" />
|
||||||
@@ -325,5 +325,6 @@ export function CustomProviderForm() {
|
|||||||
</Button>
|
</Button>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
</Dialog>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 { DialogConnectProvider } from "./dialog-connect-provider"
|
import { DialogSelectProvider } from "./dialog-select-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 = () => {
|
||||||
void dialog.show(() => <DialogConnectProvider directory={directory} />)
|
dialog.show(() => <DialogSelectProvider 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 = () => {
|
||||||
void dialog.show(() => <DialogConnectProvider directory={directory} />)
|
dialog.show(() => <DialogSelectProvider 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,55 +1,397 @@
|
|||||||
|
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 { createMemo, createSignal, lazy, Match, Show, Switch } from "solid-js"
|
import { useNavigate } from "@solidjs/router"
|
||||||
import { formatKeybind } from "@/context/command"
|
import { createMemo, createSignal, lazy, Match, onCleanup, Show, Switch } from "solid-js"
|
||||||
import { useServerSDK } from "@/context/server-sdk"
|
import { formatKeybind, useCommand, type CommandOption } from "@/context/command"
|
||||||
|
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 DialogSelectFileMode = "all" | "files"
|
|
||||||
|
|
||||||
export function DialogSelectFile(props: { mode?: DialogSelectFileMode; onOpenFile?: (path: string) => void }) {
|
type EntryType = "command" | "file" | "session"
|
||||||
const platform = usePlatform()
|
|
||||||
const settings = useSettings()
|
|
||||||
const filesOnly = () => props.mode === "files"
|
|
||||||
|
|
||||||
if (!filesOnly() && settings.general.newLayoutDesigns()) {
|
type Entry = {
|
||||||
return <DialogCommandPaletteV2 onOpenFile={props.onOpenFile} />
|
id: string
|
||||||
}
|
type: EntryType
|
||||||
|
title: string
|
||||||
if (filesOnly() && platform.platform === "desktop" && settings.general.newLayoutDesigns()) {
|
description?: string
|
||||||
return <DialogSelectFileDesktopV2 onOpenFile={props.onOpenFile} />
|
keybind?: string
|
||||||
}
|
category: string
|
||||||
|
option?: CommandOption
|
||||||
return <DialogSelectFileLegacy filesOnly={filesOnly} onOpenFile={props.onOpenFile} />
|
path?: string
|
||||||
|
directory?: string
|
||||||
|
sessionID?: string
|
||||||
|
archived?: number
|
||||||
|
updated?: number
|
||||||
}
|
}
|
||||||
|
|
||||||
function DialogSelectFileDesktopV2(props: { onOpenFile?: (path: string) => void }) {
|
type DialogSelectFileMode = "all" | "files"
|
||||||
const language = useLanguage()
|
|
||||||
const serverSDK = useServerSDK()
|
|
||||||
const { params } = useSessionLayout()
|
|
||||||
const projectDirectory = createMemo(() => decode64(params.dir) ?? "")
|
|
||||||
const openFile = createCommandPaletteFileOpener(props.onOpenFile)
|
|
||||||
|
|
||||||
|
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 }) {
|
||||||
|
const command = useCommand()
|
||||||
|
const language = useLanguage()
|
||||||
|
const platform = usePlatform()
|
||||||
|
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 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) ?? "")
|
||||||
|
const project = createMemo(() => {
|
||||||
|
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 })
|
||||||
|
|
||||||
|
const items = async (text: string) => {
|
||||||
|
const query = text.trim()
|
||||||
|
setGrouped(query.length > 0)
|
||||||
|
|
||||||
|
if (!query && filesOnly()) {
|
||||||
|
const loaded = file.tree.state("")?.loaded
|
||||||
|
const pending = loaded ? Promise.resolve() : file.tree.list("")
|
||||||
|
const next = uniqueEntries([...fileEntries.recent(), ...fileEntries.root()])
|
||||||
|
|
||||||
|
if (loaded || next.length > 0) {
|
||||||
|
void pending
|
||||||
|
return next
|
||||||
|
}
|
||||||
|
|
||||||
|
await pending
|
||||||
|
return uniqueEntries([...fileEntries.recent(), ...fileEntries.root()])
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!query) return [...commandEntries.picks(), ...fileEntries.recent()]
|
||||||
|
|
||||||
|
if (filesOnly()) {
|
||||||
|
const files = await file.searchFiles(query)
|
||||||
|
const category = language.t("palette.group.files")
|
||||||
|
return files.map((path) => createFileEntry(path, category))
|
||||||
|
}
|
||||||
|
|
||||||
|
const [files, nextSessions] = await Promise.all([file.searchFiles(query), Promise.resolve(sessions(query))])
|
||||||
|
const category = language.t("palette.group.files")
|
||||||
|
const entries = files.map((path) => createFileEntry(path, category))
|
||||||
|
return [...commandEntries.list(), ...nextSessions, ...entries]
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleMove = (item: Entry | undefined) => {
|
||||||
|
state.cleanup?.()
|
||||||
|
if (!item) return
|
||||||
|
if (item.type !== "command") return
|
||||||
|
state.cleanup = item.option?.onHighlight?.()
|
||||||
|
}
|
||||||
|
|
||||||
|
const open = (path: string) => {
|
||||||
|
const value = file.tab(path)
|
||||||
|
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 (
|
return (
|
||||||
<DialogSelectFileV2
|
<DialogSelectFileV2
|
||||||
server={serverSDK().server}
|
server={serverSDK().server}
|
||||||
@@ -58,49 +400,10 @@ function DialogSelectFileDesktopV2(props: { onOpenFile?: (path: string) => void
|
|||||||
title={language.t("session.header.searchFiles")}
|
title={language.t("session.header.searchFiles")}
|
||||||
onSelect={(result) => {
|
onSelect={(result) => {
|
||||||
if (typeof result !== "string") return
|
if (typeof result !== "string") return
|
||||||
openFile(result)
|
open(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 query = text.trim()
|
|
||||||
setGrouped(query.length > 0)
|
|
||||||
|
|
||||||
if (!query && props.filesOnly()) {
|
|
||||||
const loaded = palette.file.tree.state("")?.loaded
|
|
||||||
const pending = loaded ? Promise.resolve() : palette.file.tree.list("")
|
|
||||||
const next = uniqueCommandPaletteEntries([...palette.recentFileEntries(), ...palette.rootFileEntries()])
|
|
||||||
|
|
||||||
if (loaded || next.length > 0) {
|
|
||||||
void pending
|
|
||||||
return next
|
|
||||||
}
|
|
||||||
|
|
||||||
await pending
|
|
||||||
return uniqueCommandPaletteEntries([...palette.recentFileEntries(), ...palette.rootFileEntries()])
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!query) return [...palette.preferredCommandEntries(), ...palette.recentFileEntries()]
|
|
||||||
|
|
||||||
if (props.filesOnly()) {
|
|
||||||
const files = await palette.file.searchFiles(query)
|
|
||||||
const category = palette.language.t("palette.group.files")
|
|
||||||
return files.map((path) => createCommandPaletteFileEntry(path, category))
|
|
||||||
}
|
|
||||||
|
|
||||||
const [files, nextSessions] = await Promise.all([
|
|
||||||
palette.file.searchFiles(query),
|
|
||||||
Promise.resolve(palette.sessions(query)),
|
|
||||||
])
|
|
||||||
const category = palette.language.t("palette.group.files")
|
|
||||||
const entries = files.map((path) => createCommandPaletteFileEntry(path, category))
|
|
||||||
return [...palette.commandEntries(), ...nextSessions, ...entries]
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -108,21 +411,21 @@ function DialogSelectFileLegacy(props: { filesOnly: () => boolean; onOpenFile?:
|
|||||||
<List
|
<List
|
||||||
class="px-3"
|
class="px-3"
|
||||||
search={{
|
search={{
|
||||||
placeholder: props.filesOnly()
|
placeholder: filesOnly()
|
||||||
? palette.language.t("session.header.searchFiles")
|
? language.t("session.header.searchFiles")
|
||||||
: palette.language.t("palette.search.placeholder"),
|
: language.t("palette.search.placeholder"),
|
||||||
autofocus: true,
|
autofocus: true,
|
||||||
hideIcon: true,
|
hideIcon: true,
|
||||||
}}
|
}}
|
||||||
emptyMessage={palette.language.t("palette.empty")}
|
emptyMessage={language.t("palette.empty")}
|
||||||
loadingMessage={palette.language.t("common.loading")}
|
loadingMessage={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={(item: CommandPaletteEntry | undefined) => palette.highlight(item)}
|
onMove={handleMove}
|
||||||
onSelect={(item: CommandPaletteEntry | undefined) => palette.select(item)}
|
onSelect={handleSelect}
|
||||||
>
|
>
|
||||||
{(item) => (
|
{(item) => (
|
||||||
<Switch
|
<Switch
|
||||||
@@ -149,7 +452,7 @@ function DialogSelectFileLegacy(props: { filesOnly: () => boolean; onOpenFile?:
|
|||||||
</Show>
|
</Show>
|
||||||
</div>
|
</div>
|
||||||
<Show when={item.keybind}>
|
<Show when={item.keybind}>
|
||||||
<Keybind class="rounded-[4px]">{formatKeybind(item.keybind ?? "", palette.language.t)}</Keybind>
|
<Keybind class="rounded-[4px]">{formatKeybind(item.keybind ?? "", language.t)}</Keybind>
|
||||||
</Show>
|
</Show>
|
||||||
</div>
|
</div>
|
||||||
</Match>
|
</Match>
|
||||||
@@ -176,7 +479,7 @@ function DialogSelectFileLegacy(props: { filesOnly: () => boolean; onOpenFile?:
|
|||||||
</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(), palette.language.t)}
|
{getRelativeTime(new Date(item.updated!).toISOString(), language.t)}
|
||||||
</span>
|
</span>
|
||||||
</Show>
|
</Show>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,19 +0,0 @@
|
|||||||
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)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
@@ -1,18 +0,0 @@
|
|||||||
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,16 +22,17 @@ export const DialogSelectModelUnpaid: Component<{ model?: ModelState }> = (props
|
|||||||
const providers = useProviders(directory)
|
const providers = useProviders(directory)
|
||||||
const language = useLanguage()
|
const language = useLanguage()
|
||||||
|
|
||||||
const openProviders = (provider?: string) => {
|
const connect = (provider: string) => {
|
||||||
void import("./dialog-connect-provider").then((x) => {
|
void import("./dialog-connect-provider").then((x) => {
|
||||||
const controller = x.useProviderConnectController()
|
dialog.show(() => <x.DialogConnectProvider provider={provider} directory={directory} />)
|
||||||
controller.select(provider)
|
|
||||||
void dialog.show(() => <x.DialogConnectProvider controller={controller} directory={directory} />)
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
const connect = (provider: string) => openProviders(provider)
|
const all = () => {
|
||||||
const all = () => openProviders()
|
void import("./dialog-select-provider").then((x) => {
|
||||||
|
dialog.show(() => <x.DialogSelectProvider directory={directory} />)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
let listRef: ListRef | undefined
|
let listRef: ListRef | undefined
|
||||||
const handleKeyDown = (e: KeyboardEvent) => {
|
const handleKeyDown = (e: KeyboardEvent) => {
|
||||||
|
|||||||
@@ -1,15 +1,5 @@
|
|||||||
import { Popover as Kobalte } from "@kobalte/core/popover"
|
import { Popover as Kobalte } from "@kobalte/core/popover"
|
||||||
import {
|
import { Component, ComponentProps, createMemo, For, JSX, Show, ValidComponent } from "solid-js"
|
||||||
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"
|
||||||
@@ -24,13 +14,9 @@ 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)
|
||||||
@@ -93,7 +79,6 @@ 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}
|
||||||
@@ -157,8 +142,8 @@ export function ModelSelectorPopover(props: {
|
|||||||
|
|
||||||
const handleConnectProvider = () => {
|
const handleConnectProvider = () => {
|
||||||
close("provider")
|
close("provider")
|
||||||
void import("./dialog-connect-provider").then((x) => {
|
void import("./dialog-select-provider").then((x) => {
|
||||||
void dialog.show(() => <x.DialogConnectProvider directory={directory} />)
|
dialog.show(() => <x.DialogSelectProvider directory={directory} />)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
const language = useLanguage()
|
const language = useLanguage()
|
||||||
@@ -257,9 +242,14 @@ 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()
|
const search = store.search.trim().toLowerCase()
|
||||||
const filtered = search
|
const filtered = search
|
||||||
? allModels().filter((item) => matchesModelSearch(search, [item.name, item.id, item.provider.name]))
|
? allModels().filter(
|
||||||
|
(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))
|
||||||
@@ -343,23 +333,19 @@ 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()
|
const search = value.trim().toLowerCase()
|
||||||
const first = [...allModels()]
|
const first = [...allModels()]
|
||||||
.sort((a, b) => a.name.localeCompare(b.name))
|
.sort((a, b) => a.name.localeCompare(b.name))
|
||||||
.find((item) => matchesModelSearch(search, [item.name, item.id, item.provider.name]))
|
.find(
|
||||||
|
(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}>
|
||||||
@@ -448,25 +434,11 @@ export function ModelSelectorPopoverV2(props: {
|
|||||||
<MenuV2.RadioGroup value={current()}>
|
<MenuV2.RadioGroup value={current()}>
|
||||||
<For each={group.items}>
|
<For each={group.items}>
|
||||||
{(item) => (
|
{(item) => (
|
||||||
<TooltipV2
|
|
||||||
class="w-full"
|
|
||||||
placement="right-start"
|
|
||||||
gutter={6}
|
|
||||||
openDelay={0}
|
|
||||||
value={
|
|
||||||
<ModelTooltip
|
|
||||||
model={item}
|
|
||||||
latest={item.latest}
|
|
||||||
free={isFree(item.provider.id, item.cost)}
|
|
||||||
v2
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<MenuV2.RadioItem
|
<MenuV2.RadioItem
|
||||||
value={modelKey(item)}
|
value={modelKey(item)}
|
||||||
data-option-key={modelKey(item)}
|
data-option-key={modelKey(item)}
|
||||||
data-selected-model={current() === modelKey(item) ? true : undefined}
|
data-selected-model={current() === modelKey(item) ? true : undefined}
|
||||||
class="scroll-my-6 w-full"
|
class="scroll-my-6"
|
||||||
classList={{ "!bg-v2-overlay-simple-overlay-hover": store.active === modelKey(item) }}
|
classList={{ "!bg-v2-overlay-simple-overlay-hover": store.active === modelKey(item) }}
|
||||||
onMouseEnter={() => {
|
onMouseEnter={() => {
|
||||||
setStore("active", modelKey(item))
|
setStore("active", modelKey(item))
|
||||||
@@ -482,7 +454,6 @@ export function ModelSelectorPopoverV2(props: {
|
|||||||
<TagV2 class="shrink-0">{language.t("model.tag.latest")}</TagV2>
|
<TagV2 class="shrink-0">{language.t("model.tag.latest")}</TagV2>
|
||||||
</Show>
|
</Show>
|
||||||
</MenuV2.RadioItem>
|
</MenuV2.RadioItem>
|
||||||
</TooltipV2>
|
|
||||||
)}
|
)}
|
||||||
</For>
|
</For>
|
||||||
</MenuV2.RadioGroup>
|
</MenuV2.RadioGroup>
|
||||||
@@ -520,8 +491,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-connect-provider").then((x) => {
|
void import("./dialog-select-provider").then((x) => {
|
||||||
void dialog.show(() => <x.DialogConnectProvider directory={directory} />)
|
dialog.show(() => <x.DialogSelectProvider directory={directory} />)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,87 @@
|
|||||||
|
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,35 +1,22 @@
|
|||||||
import { Component, createSignal, startTransition } from "solid-js"
|
import { Component } 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<{ defaultValue?: string }> = (props) => {
|
export const DialogSettings: Component = () => {
|
||||||
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
|
<Tabs orientation="vertical" variant="settings" defaultValue="general" class="h-full settings-dialog">
|
||||||
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">
|
||||||
@@ -83,7 +70,7 @@ export const DialogSettings: Component<{ defaultValue?: string }> = (props) => {
|
|||||||
<SettingsServers />
|
<SettingsServers />
|
||||||
</Tabs.Content>
|
</Tabs.Content>
|
||||||
<Tabs.Content value="providers" class="no-scrollbar">
|
<Tabs.Content value="providers" class="no-scrollbar">
|
||||||
<SettingsProviders onBack={showProviders} />
|
<SettingsProviders />
|
||||||
</Tabs.Content>
|
</Tabs.Content>
|
||||||
<Tabs.Content value="models" class="no-scrollbar">
|
<Tabs.Content value="models" class="no-scrollbar">
|
||||||
<SettingsModels />
|
<SettingsModels />
|
||||||
|
|||||||
@@ -1,46 +0,0 @@
|
|||||||
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)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
@@ -1,77 +0,0 @@
|
|||||||
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,26 +1,95 @@
|
|||||||
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 { pathToFileUrl, withFileDragImage, type Kind } from "@/components/file-tree"
|
import {
|
||||||
import { createVirtualizer, defaultRangeExtractor } from "@tanstack/solid-virtual"
|
dirsToExpand,
|
||||||
import { buildFileTreeV2Model, flattenFileTreeV2, normalizeFileTreeV2Path } from "@/components/file-tree-v2-model"
|
pathToFileUrl,
|
||||||
import { virtualScrollElement } from "@/components/virtual-scroll-element"
|
shouldListRoot,
|
||||||
|
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"]) {
|
||||||
@@ -54,6 +123,7 @@ 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"
|
||||||
},
|
},
|
||||||
) => {
|
) => {
|
||||||
@@ -63,18 +133,18 @@ const FileTreeNodeV2 = (
|
|||||||
"active",
|
"active",
|
||||||
"draggable",
|
"draggable",
|
||||||
"kinds",
|
"kinds",
|
||||||
|
"marks",
|
||||||
"as",
|
"as",
|
||||||
"children",
|
"children",
|
||||||
"class",
|
"class",
|
||||||
"classList",
|
"classList",
|
||||||
])
|
])
|
||||||
const kind = () => local.kinds?.get(local.node.path)
|
const kind = () => visibleKind(local.node, local.kinds, local.marks)
|
||||||
|
|
||||||
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={{
|
||||||
@@ -107,161 +177,244 @@ const FileTreeNodeV2 = (
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
function GuideLines(props: { level: number }) {
|
// V2-styled fork of FileTree for the review sidebar. Unlike the v1 tree it never
|
||||||
return (
|
// lists unloaded subdirectories, so callers must pass `allowed` (nodes are
|
||||||
<For each={Array.from({ length: props.level })}>
|
// synthesized from that list) for nested content to appear.
|
||||||
{(_, 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 model = createMemo(() => buildFileTreeV2Model(props.allowed ?? []))
|
const key = (p: string) =>
|
||||||
const rows = createMemo(() => flattenFileTreeV2(model(), (path) => file.tree.state(path)?.expanded ?? true))
|
file
|
||||||
const [root, setRoot] = createSignal<HTMLDivElement>()
|
.normalize(p)
|
||||||
const [focused, setFocused] = createSignal<string>()
|
.replace(/[\\/]+$/, "")
|
||||||
const virtualizer = createVirtualizer<HTMLDivElement, HTMLDivElement>({
|
.replaceAll("\\", "/")
|
||||||
get count() {
|
const chain = props._chain ? [...props._chain, key(props.path)] : [key(props.path)]
|
||||||
return rows().length
|
|
||||||
},
|
const filter = createMemo(() => {
|
||||||
getScrollElement: () => virtualScrollElement(root()),
|
if (props._filter) return props._filter
|
||||||
initialRect: { width: 0, height: 600 },
|
|
||||||
estimateSize: () => 28,
|
const allowed = props.allowed
|
||||||
gap: 2,
|
if (!allowed) return
|
||||||
overscan: 10,
|
|
||||||
get getItemKey() {
|
const files = new Set(allowed)
|
||||||
const current = rows()
|
const dirs = new Set<string>()
|
||||||
return (index: number) => current[index]?.node.path ?? index
|
|
||||||
},
|
for (const item of allowed) {
|
||||||
rangeExtractor: (range) => {
|
const parts = item.split("/")
|
||||||
const indexes = defaultRangeExtractor(range)
|
const parents = parts.slice(0, -1)
|
||||||
const path = focused()
|
for (const [idx] of parents.entries()) {
|
||||||
const index = path ? rows().findIndex((row) => row.node.path === path) : -1
|
const dir = parents.slice(0, idx + 1).join("/")
|
||||||
if (index < 0 || indexes.includes(index)) return indexes
|
if (dir) dirs.add(dir)
|
||||||
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 path = active()
|
const current = filter()
|
||||||
if (!path) return
|
const dirs = dirsToExpand({
|
||||||
const index = rows().findIndex((row) => row.node.path === path)
|
level,
|
||||||
if (index < 0) return
|
filter: current,
|
||||||
queueMicrotask(() => {
|
expanded: (dir) => untrack(() => file.tree.state(dir)?.expanded) ?? false,
|
||||||
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)))
|
|
||||||
const virtualItemByKey = createMemo(
|
createEffect(
|
||||||
() => new Map(virtualizer.getVirtualItems().map((item) => [item.key, item] as const)),
|
on(
|
||||||
|
() => 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 (
|
||||||
<div
|
// group/file-tree-v2 scopes the group-hover guide lines below; hosts may add
|
||||||
ref={setRoot}
|
// an outer group with the same name to widen the hover area.
|
||||||
data-component="file-tree-v2"
|
<div data-component="file-tree-v2" class="group/file-tree-v2">
|
||||||
data-total-rows={model().total}
|
<For each={nodes()}>
|
||||||
class="group/file-tree-v2"
|
{(node) => {
|
||||||
style={{ position: "relative", height: `${virtualizer.getTotalSize()}px` }}
|
const expanded = () => file.tree.state(node.path)?.expanded ?? false
|
||||||
|
const deep = () => deeps().get(node.path) ?? -1
|
||||||
|
const hasChildren = () => visibleNodesForPath(node.path, file.tree.children, filter()).length > 0
|
||||||
|
return (
|
||||||
|
<Switch>
|
||||||
|
<Match when={node.type === "directory"}>
|
||||||
|
<Collapsible
|
||||||
|
variant="ghost"
|
||||||
|
class="w-full"
|
||||||
|
data-scope="file-tree-v2"
|
||||||
|
forceMount={false}
|
||||||
|
open={expanded()}
|
||||||
|
onOpenChange={(open) =>
|
||||||
|
open ? file.tree.expand(node.path, { list: false }) : file.tree.collapse(node.path)
|
||||||
|
}
|
||||||
>
|
>
|
||||||
<For each={virtualRowKeys()}>
|
<Collapsible.Trigger>
|
||||||
{(key) => (
|
|
||||||
<Show when={virtualItemByKey().get(key)}>
|
|
||||||
{(item) => (
|
|
||||||
<div
|
|
||||||
style={{
|
|
||||||
position: "absolute",
|
|
||||||
top: "0",
|
|
||||||
left: "0",
|
|
||||||
width: "100%",
|
|
||||||
height: `${item().size}px`,
|
|
||||||
transform: `translateY(${item().start}px)`,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Show when={rowByKey().get(key as string)}>
|
|
||||||
{(row) => (
|
|
||||||
<Show
|
|
||||||
when={row().node.type === "directory"}
|
|
||||||
fallback={
|
|
||||||
<FileTreeNodeV2
|
<FileTreeNodeV2
|
||||||
node={row().node}
|
node={node}
|
||||||
level={row().level}
|
level={level}
|
||||||
active={active()}
|
active={props.active}
|
||||||
draggable={draggable()}
|
draggable={draggable()}
|
||||||
kinds={props.kinds}
|
kinds={kinds()}
|
||||||
as="button"
|
marks={marks()}
|
||||||
type="button"
|
|
||||||
class="relative"
|
|
||||||
onFocus={() => setFocused(row().node.path)}
|
|
||||||
onBlur={() => setFocused(undefined)}
|
|
||||||
onClick={() =>
|
|
||||||
props.onFileClick?.({
|
|
||||||
...row().node,
|
|
||||||
path: row().node.originalPath,
|
|
||||||
absolute: row().node.originalPath,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
>
|
>
|
||||||
<GuideLines level={row().level} />
|
|
||||||
<Show when={row().level > 0}>
|
|
||||||
<div class="w-4 shrink-0" />
|
|
||||||
</Show>
|
|
||||||
<span class="filetree-iconpair size-4">
|
|
||||||
<FileIcon node={row().node} class="size-4 filetree-icon filetree-icon--color" />
|
|
||||||
<FileIcon node={row().node} class="size-4 filetree-icon filetree-icon--mono" mono />
|
|
||||||
</span>
|
|
||||||
</FileTreeNodeV2>
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<FileTreeNodeV2
|
|
||||||
node={row().node}
|
|
||||||
level={row().level}
|
|
||||||
active={active()}
|
|
||||||
draggable={draggable()}
|
|
||||||
kinds={props.kinds}
|
|
||||||
as="button"
|
|
||||||
type="button"
|
|
||||||
class="relative"
|
|
||||||
onFocus={() => setFocused(row().node.path)}
|
|
||||||
onBlur={() => setFocused(undefined)}
|
|
||||||
aria-expanded={file.tree.state(row().node.path)?.expanded ?? true}
|
|
||||||
onClick={() =>
|
|
||||||
file.tree.state(row().node.path)?.expanded === false
|
|
||||||
? file.tree.expand(row().node.path, { list: false })
|
|
||||||
: file.tree.collapse(row().node.path)
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<GuideLines level={row().level} />
|
|
||||||
<div
|
<div
|
||||||
data-slot="file-tree-v2-chevron"
|
data-slot="file-tree-v2-chevron"
|
||||||
data-expanded={file.tree.state(row().node.path)?.expanded === false ? undefined : ""}
|
data-expanded={expanded() ? "" : undefined}
|
||||||
class="size-4 flex items-center justify-center"
|
class="size-4 flex items-center justify-center"
|
||||||
>
|
>
|
||||||
<Icon name="chevron-down" />
|
<Icon name="chevron-down" />
|
||||||
</div>
|
</div>
|
||||||
</FileTreeNodeV2>
|
</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}
|
||||||
|
active={props.active}
|
||||||
|
draggable={props.draggable}
|
||||||
|
onFileClick={props.onFileClick}
|
||||||
|
_filter={filter()}
|
||||||
|
_marks={marks()}
|
||||||
|
_deeps={deeps()}
|
||||||
|
_kinds={kinds()}
|
||||||
|
_chain={chain}
|
||||||
|
/>
|
||||||
</Show>
|
</Show>
|
||||||
)}
|
</Collapsible.Content>
|
||||||
</Show>
|
</Show>
|
||||||
</div>
|
</Collapsible>
|
||||||
)}
|
</Match>
|
||||||
|
<Match when={node.type === "file"}>
|
||||||
|
<FileTreeNodeV2
|
||||||
|
node={node}
|
||||||
|
level={level}
|
||||||
|
active={props.active}
|
||||||
|
draggable={draggable()}
|
||||||
|
kinds={kinds()}
|
||||||
|
marks={marks()}
|
||||||
|
as="button"
|
||||||
|
type="button"
|
||||||
|
onClick={() => props.onFileClick?.(node)}
|
||||||
|
>
|
||||||
|
<Show when={level > 0}>
|
||||||
|
<div class="w-4 shrink-0" />
|
||||||
</Show>
|
</Show>
|
||||||
)}
|
<Show
|
||||||
|
when={!node.ignored}
|
||||||
|
fallback={<FileIcon node={node} class="size-4 filetree-icon filetree-icon--mono" mono />}
|
||||||
|
>
|
||||||
|
<span class="filetree-iconpair size-4">
|
||||||
|
<FileIcon node={node} class="size-4 filetree-icon filetree-icon--color" />
|
||||||
|
<FileIcon node={node} class="size-4 filetree-icon filetree-icon--mono" mono />
|
||||||
|
</span>
|
||||||
|
</Show>
|
||||||
|
</FileTreeNodeV2>
|
||||||
|
</Match>
|
||||||
|
</Switch>
|
||||||
|
)
|
||||||
|
}}
|
||||||
</For>
|
</For>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -201,7 +201,6 @@ 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>
|
||||||
@@ -441,7 +440,6 @@ 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()}
|
||||||
@@ -464,7 +462,6 @@ 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, type JSX } from "solid-js"
|
import { Show, type Component } 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,18 +23,7 @@ type ModelInfo = {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function ModelTooltipRow(props: { name: JSX.Element; value: JSX.Element }) {
|
export const ModelTooltip: Component<{ model: ModelInfo; latest?: boolean; free?: boolean }> = (props) => {
|
||||||
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()
|
||||||
@@ -62,13 +51,6 @@ 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
|
||||||
@@ -91,21 +73,6 @@ 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">
|
||||||
|
|||||||
@@ -218,7 +218,6 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
|
|||||||
let scrollRef!: HTMLDivElement
|
let scrollRef!: HTMLDivElement
|
||||||
let slashPopoverRef!: HTMLDivElement
|
let slashPopoverRef!: HTMLDivElement
|
||||||
let restoreEndOnFocus = true
|
let restoreEndOnFocus = true
|
||||||
let savedCursor: number | null = null
|
|
||||||
|
|
||||||
const mirror = { input: false }
|
const mirror = { input: false }
|
||||||
const inset = 56
|
const inset = 56
|
||||||
@@ -591,7 +590,7 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
|
|||||||
|
|
||||||
const restoreFocus = () => {
|
const restoreFocus = () => {
|
||||||
requestAnimationFrame(() => {
|
requestAnimationFrame(() => {
|
||||||
const cursor = savedCursor ?? prompt.cursor() ?? promptLength(prompt.current())
|
const cursor = prompt.cursor() ?? promptLength(prompt.current())
|
||||||
editorRef.focus()
|
editorRef.focus()
|
||||||
setCursorPosition(editorRef, cursor)
|
setCursorPosition(editorRef, cursor)
|
||||||
queueScroll()
|
queueScroll()
|
||||||
@@ -628,7 +627,6 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
|
|||||||
const isImeComposing = (event: KeyboardEvent) => event.isComposing || composing() || event.keyCode === 229
|
const isImeComposing = (event: KeyboardEvent) => event.isComposing || composing() || event.keyCode === 229
|
||||||
|
|
||||||
const handleBlur = () => {
|
const handleBlur = () => {
|
||||||
savedCursor = currentCursor()
|
|
||||||
closePopover()
|
closePopover()
|
||||||
setComposing(false)
|
setComposing(false)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 { TooltipV2 } from "@opencode-ai/ui/v2/tooltip-v2"
|
import { Tooltip } from "@opencode-ai/ui/tooltip"
|
||||||
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 (
|
||||||
<TooltipV2
|
<Tooltip
|
||||||
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>
|
||||||
</TooltipV2>
|
</Tooltip>
|
||||||
)
|
)
|
||||||
}}
|
}}
|
||||||
</For>
|
</For>
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { createEffect, For, onCleanup, Show, splitProps, type Accessor, type ComponentProps } from "solid-js"
|
import { For, 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,7 +8,6 @@ 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
|
||||||
@@ -102,16 +101,6 @@ 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,
|
||||||
@@ -138,7 +127,16 @@ export function createPromptProjectController(input: {
|
|||||||
}
|
}
|
||||||
setStore({ open: false, search: "", active: "" })
|
setStore({ open: false, search: "", active: "" })
|
||||||
},
|
},
|
||||||
setSearch,
|
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),
|
||||||
|
})
|
||||||
|
},
|
||||||
clearSearch() {
|
clearSearch() {
|
||||||
setStore({ search: "", active: initialActive() })
|
setStore({ search: "", active: initialActive() })
|
||||||
setTimeout(() => searchRef?.focus())
|
setTimeout(() => searchRef?.focus())
|
||||||
@@ -172,9 +170,6 @@ export function createPromptProjectController(input: {
|
|||||||
focusSearch() {
|
focusSearch() {
|
||||||
setTimeout(() => requestAnimationFrame(() => searchRef?.focus()))
|
setTimeout(() => requestAnimationFrame(() => searchRef?.focus()))
|
||||||
},
|
},
|
||||||
handleSearchKeydown(event: KeyboardEvent) {
|
|
||||||
return handleDocumentSearchKeydown(searchRef, event, store.search, setSearch)
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -248,13 +243,6 @@ 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,7 +4,6 @@ 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"
|
||||||
@@ -15,7 +14,6 @@ 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"
|
||||||
@@ -49,10 +47,8 @@ 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")
|
||||||
@@ -60,7 +56,6 @@ 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,7 +31,6 @@ 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",
|
||||||
@@ -285,9 +284,10 @@ export function SessionHeader() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const [centerMount, setCenterMount] = createSignal<HTMLElement | null>(null)
|
const [centerMount, setCenterMount] = createSignal<HTMLElement | null>(null)
|
||||||
const rightMount = useTitlebarRightMount()
|
const [rightMount, setRightMount] = createSignal<HTMLElement | null>(null)
|
||||||
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; temporary?: boolean }): JSX.Element {
|
export function FileVisual(props: { path: string; active?: 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,19 +22,12 @@ export function FileVisual(props: { path: string; active?: boolean; temporary?:
|
|||||||
<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" classList={{ italic: props.temporary }}>
|
<span class="text-14-medium truncate">{getFilename(props.path)}</span>
|
||||||
{getFilename(props.path)}
|
|
||||||
</span>
|
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export function SortableTab(props: {
|
export function SortableTab(props: { tab: string; onTabClose: (tab: string) => void }): JSX.Element {
|
||||||
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()
|
||||||
@@ -43,7 +36,7 @@ export function SortableTab(props: {
|
|||||||
const content = createMemo(() => {
|
const content = createMemo(() => {
|
||||||
const value = path()
|
const value = path()
|
||||||
if (!value) return
|
if (!value) return
|
||||||
return <FileVisual path={value} temporary={props.temporary} />
|
return <FileVisual path={value} />
|
||||||
})
|
})
|
||||||
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 }}>
|
||||||
@@ -68,7 +61,6 @@ export function SortableTab(props: {
|
|||||||
}
|
}
|
||||||
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>
|
||||||
|
|||||||
@@ -1,271 +0,0 @@
|
|||||||
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,6 +440,11 @@ 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"
|
||||||
@@ -466,6 +471,11 @@ 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,18 +5,24 @@ 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,7 +8,8 @@ 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, useProviderConnectController } from "./dialog-connect-provider"
|
import { DialogConnectProvider } 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"
|
||||||
@@ -27,26 +28,20 @@ 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<{ onBack?: () => void }> = (props) => {
|
export const SettingsProviders: Component = () => {
|
||||||
return (
|
return (
|
||||||
<SettingsServerScope>
|
<SettingsServerScope>
|
||||||
<SettingsProvidersContent onBack={props.onBack} />
|
<SettingsProvidersContent />
|
||||||
</SettingsServerScope>
|
</SettingsServerScope>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
const SettingsProvidersContent: Component<{ onBack?: () => void }> = (props) => {
|
const SettingsProvidersContent: Component = () => {
|
||||||
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
|
||||||
@@ -211,7 +206,14 @@ const SettingsProvidersContent: Component<{ onBack?: () => void }> = (props) =>
|
|||||||
{(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 size="large" variant="secondary" icon="plus-small" onClick={() => connect(item.id)}>
|
<Button
|
||||||
|
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>
|
||||||
@@ -237,7 +239,7 @@ const SettingsProvidersContent: Component<{ onBack?: () => void }> = (props) =>
|
|||||||
variant="secondary"
|
variant="secondary"
|
||||||
icon="plus-small"
|
icon="plus-small"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
dialog.show(() => <DialogCustomProvider onBack={dialog.close} />)
|
dialog.show(() => <DialogCustomProvider back="close" />)
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{language.t("common.connect")}
|
{language.t("common.connect")}
|
||||||
@@ -248,7 +250,9 @@ const SettingsProvidersContent: Component<{ onBack?: () => void }> = (props) =>
|
|||||||
<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={() => connect()}
|
onClick={() => {
|
||||||
|
dialog.show(() => <DialogSelectProvider />)
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
{language.t("dialog.provider.viewAll")}
|
{language.t("dialog.provider.viewAll")}
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { Component, createSignal, startTransition } from "solid-js"
|
import { Component } 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,30 +10,16 @@ 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
|
<TabsV2 orientation="vertical" variant="settings" defaultValue="general" class="settings-v2">
|
||||||
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">
|
||||||
@@ -87,7 +73,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 onBack={showProviders} />
|
<SettingsProvidersV2 />
|
||||||
</TabsV2.Content>
|
</TabsV2.Content>
|
||||||
<TabsV2.Content value="models" class="settings-v2-panel">
|
<TabsV2.Content value="models" class="settings-v2-panel">
|
||||||
<SettingsModelsV2 />
|
<SettingsModelsV2 />
|
||||||
|
|||||||
@@ -422,6 +422,11 @@ 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>
|
||||||
|
|
||||||
@@ -449,6 +454,11 @@ 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,7 +8,8 @@ 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, useProviderConnectController } from "../dialog-connect-provider"
|
import { DialogConnectProvider } 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"
|
||||||
@@ -29,18 +30,12 @@ const PROVIDER_NOTES = [
|
|||||||
|
|
||||||
const PROVIDER_ICON_SIZE = 16
|
const PROVIDER_ICON_SIZE = 16
|
||||||
|
|
||||||
export const SettingsProvidersV2: Component<{ onBack?: () => void }> = (props) => {
|
export const SettingsProvidersV2: Component = () => {
|
||||||
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
|
||||||
@@ -211,7 +206,14 @@ export const SettingsProvidersV2: Component<{ onBack?: () => void }> = (props) =
|
|||||||
</Show>
|
</Show>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<ButtonV2 size="normal" variant="neutral" icon="plus" onClick={() => connect(item.id)}>
|
<ButtonV2
|
||||||
|
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>
|
||||||
@@ -239,7 +241,7 @@ export const SettingsProvidersV2: Component<{ onBack?: () => void }> = (props) =
|
|||||||
variant="neutral"
|
variant="neutral"
|
||||||
icon="plus"
|
icon="plus"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
dialog.show(() => <DialogCustomProvider onBack={dialog.close} />)
|
dialog.show(() => <DialogCustomProvider back="close" />)
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{language.t("common.connect")}
|
{language.t("common.connect")}
|
||||||
@@ -247,7 +249,13 @@ export const SettingsProvidersV2: Component<{ onBack?: () => void }> = (props) =
|
|||||||
</div>
|
</div>
|
||||||
</SettingsListV2>
|
</SettingsListV2>
|
||||||
|
|
||||||
<button type="button" class="settings-v2-providers-view-all" onClick={() => connect()}>
|
<button
|
||||||
|
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>
|
||||||
|
|||||||
@@ -399,7 +399,7 @@
|
|||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
font-weight: 440;
|
font-weight: 440;
|
||||||
line-height: 16px;
|
line-height: 1;
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -246,23 +246,18 @@ 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]"
|
||||||
>
|
>
|
||||||
<span data-slot="project-avatar-slot" class="flex size-4 shrink-0 items-center justify-center">
|
<Show when={props.session()}>
|
||||||
<Show
|
|
||||||
when={props.session()}
|
|
||||||
fallback={
|
|
||||||
<span class="block size-4 rounded-[3px] border border-v2-border-border-muted" aria-hidden="true" />
|
|
||||||
}
|
|
||||||
>
|
|
||||||
{(session) => (
|
{(session) => (
|
||||||
|
<span data-slot="project-avatar-slot">
|
||||||
<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 = 2_000
|
const OPEN_DELAY = 200
|
||||||
// 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,6 +17,8 @@ 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
|
||||||
@@ -40,7 +42,6 @@ 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()
|
||||||
@@ -72,10 +73,8 @@ 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 (!current) return
|
if (!value || !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,
|
||||||
@@ -89,7 +88,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() && !persisted()?.title }}
|
classList={{ hidden: !session() && !missingSession() }}
|
||||||
>
|
>
|
||||||
<TabNavItem
|
<TabNavItem
|
||||||
ref={(el) => {
|
ref={(el) => {
|
||||||
@@ -98,7 +97,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={persisted()?.title ?? (missingSession() ? language.t("session.tab.unknown") : undefined)}
|
fallbackTitle={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, onMount, Show, Switch, untrack } from "solid-js"
|
import { createEffect, createMemo, createResource, createSignal, Match, 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,12 +60,6 @@ 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()
|
||||||
@@ -334,21 +328,6 @@ 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 }, "")
|
||||||
@@ -395,16 +374,9 @@ export function Titlebar(props: { update?: TitlebarUpdate }) {
|
|||||||
keybind: "mod+w",
|
keybind: "mod+w",
|
||||||
hidden: true,
|
hidden: true,
|
||||||
onSelect: () => {
|
onSelect: () => {
|
||||||
tabsStoreActions.closeTab(tabsStore.findIndex((tab) => current === tab))
|
tabsStoreActions.removeTab(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",
|
||||||
@@ -492,7 +464,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.closeTab(index)
|
if (index !== -1) tabsStoreActions.removeTab(index)
|
||||||
}}
|
}}
|
||||||
onReorder={(keys) => tabsStoreActions.reorder(keys)}
|
onReorder={(keys) => tabsStoreActions.reorder(keys)}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -1,18 +0,0 @@
|
|||||||
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()
|
|
||||||
})
|
|
||||||
@@ -1,4 +0,0 @@
|
|||||||
export function virtualScrollElement(root: HTMLElement | undefined) {
|
|
||||||
if (!root?.isConnected) return null
|
|
||||||
return root.closest<HTMLDivElement>(".scroll-view__viewport")
|
|
||||||
}
|
|
||||||
@@ -1,40 +0,0 @@
|
|||||||
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 { resolveKeybindOption, upsertCommandRegistration } from "./command"
|
import { upsertCommandRegistration } from "./command"
|
||||||
|
|
||||||
describe("upsertCommandRegistration", () => {
|
describe("upsertCommandRegistration", () => {
|
||||||
test("replaces keyed registrations", () => {
|
test("replaces keyed registrations", () => {
|
||||||
@@ -23,19 +23,3 @@ 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,15 +82,10 @@ 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 = {
|
||||||
@@ -339,7 +334,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
|
||||||
@@ -349,12 +344,8 @@ 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)
|
||||||
const existing = map.get(sig)
|
if (map.has(sig)) continue
|
||||||
if (existing) {
|
map.set(sig, option)
|
||||||
existing.push(option)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
map.set(sig, [option])
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return map
|
return map
|
||||||
@@ -383,7 +374,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 = resolveKeybindOption(keymap().get(sig), event)
|
const option = keymap().get(sig)
|
||||||
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"
|
||||||
|
|
||||||
@@ -392,19 +383,17 @@ 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, { capture: true })
|
makeEventListener(document, "keydown", handleKeyDown)
|
||||||
})
|
})
|
||||||
|
|
||||||
function register(cb: () => CommandOption[]): void
|
function register(cb: () => CommandOption[]): void
|
||||||
|
|||||||
@@ -203,15 +203,12 @@ export const { use: useFile, provider: FileProvider } = createSimpleContext({
|
|||||||
return promise
|
return promise
|
||||||
}
|
}
|
||||||
|
|
||||||
const search = (query: string, dirs: "true" | "false", options?: { limit?: number; signal?: AbortSignal }) =>
|
const search = (query: string, dirs: "true" | "false") =>
|
||||||
sdk()
|
sdk()
|
||||||
.client.find.files({ query, dirs, limit: options?.limit }, { signal: options?.signal })
|
.client.find.files({ query, dirs })
|
||||||
.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) => {
|
||||||
@@ -287,8 +284,7 @@ export const { use: useFile, provider: FileProvider } = createSimpleContext({
|
|||||||
setScrollLeft,
|
setScrollLeft,
|
||||||
selectedLines,
|
selectedLines,
|
||||||
setSelectedLines,
|
setSelectedLines,
|
||||||
searchFiles: (query: string, options?: { limit?: number; signal?: AbortSignal }) =>
|
searchFiles: (query: string) => search(query, "false"),
|
||||||
search(query, "false", options),
|
|
||||||
searchFilesAndDirectories: (query: string) => search(query, "true"),
|
searchFilesAndDirectories: (query: string) => search(query, "true"),
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,8 +1,7 @@
|
|||||||
import { createSimpleContext } from "@opencode-ai/ui/context"
|
import { createSimpleContext } from "@opencode-ai/ui/context"
|
||||||
import { createEffect, createMemo, createRoot } from "solid-js"
|
import { createEffect, createMemo, createRoot } from "solid-js"
|
||||||
import { createStore } from "solid-js/store"
|
import { createStore } from "solid-js/store"
|
||||||
import { createServerProjects, RECENTLY_CLOSED_DISPLAY_LIMIT, ServerConnection, useServer } from "./server"
|
import { createServerProjects, ServerConnection, useServer } from "./server"
|
||||||
import { pathKey } from "@/utils/path-key"
|
|
||||||
import { useServerHealth } from "@/utils/server-health"
|
import { useServerHealth } from "@/utils/server-health"
|
||||||
import { createServerSdkContext } from "./server-sdk"
|
import { createServerSdkContext } from "./server-sdk"
|
||||||
import { createServerSyncContext } from "./server-sync"
|
import { createServerSyncContext } from "./server-sync"
|
||||||
@@ -128,14 +127,6 @@ function createServerCtx(
|
|||||||
}
|
}
|
||||||
|
|
||||||
const projectsList = createMemo(() => projects.list().map(enrich))
|
const projectsList = createMemo(() => projects.list().map(enrich))
|
||||||
const recentlyClosedList = createMemo(() => {
|
|
||||||
const known = new Set(sync.data.project.map((project) => pathKey(project.worktree)))
|
|
||||||
return projects
|
|
||||||
.recentlyClosed()
|
|
||||||
.filter((worktree) => known.has(pathKey(worktree)))
|
|
||||||
.slice(0, RECENTLY_CLOSED_DISPLAY_LIMIT)
|
|
||||||
.map((worktree) => enrich({ worktree, expanded: false }))
|
|
||||||
})
|
|
||||||
|
|
||||||
const isLocal =
|
const isLocal =
|
||||||
(conn?.type === "sidecar" && conn.variant === "base") || (conn?.type === "http" && isLocalHost(conn.http.url))
|
(conn?.type === "sidecar" && conn.variant === "base") || (conn?.type === "http" && isLocalHost(conn.http.url))
|
||||||
@@ -148,7 +139,6 @@ function createServerCtx(
|
|||||||
projects: {
|
projects: {
|
||||||
...projects,
|
...projects,
|
||||||
list: projectsList,
|
list: projectsList,
|
||||||
recentlyClosed: recentlyClosedList,
|
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,82 +0,0 @@
|
|||||||
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"))
|
|
||||||
})
|
|
||||||
})
|
|
||||||
@@ -1,103 +0,0 @@
|
|||||||
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,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -5,11 +5,10 @@ import { createSimpleContext } from "@opencode-ai/ui/context"
|
|||||||
import { makeEventListener } from "@solid-primitives/event-listener"
|
import { makeEventListener } from "@solid-primitives/event-listener"
|
||||||
import { useServerSync } from "./server-sync"
|
import { useServerSync } from "./server-sync"
|
||||||
import { useServerSDK } from "./server-sdk"
|
import { useServerSDK } from "./server-sdk"
|
||||||
import { RECENTLY_CLOSED_DISPLAY_LIMIT, ServerConnection, useServer } from "./server"
|
import { ServerConnection, useServer } from "./server"
|
||||||
import { usePlatform } from "./platform"
|
import { usePlatform } from "./platform"
|
||||||
import { Project } from "@opencode-ai/sdk/v2"
|
import { Project } from "@opencode-ai/sdk/v2"
|
||||||
import { Persist, persisted, removePersisted } from "@/utils/persist"
|
import { Persist, persisted, removePersisted } from "@/utils/persist"
|
||||||
import { pathKey } from "@/utils/path-key"
|
|
||||||
import { decode64 } from "@/utils/base64"
|
import { decode64 } from "@/utils/base64"
|
||||||
import { same } from "@/utils/same"
|
import { same } from "@/utils/same"
|
||||||
import { createScrollPersistence, type SessionScroll } from "./layout-scroll"
|
import { createScrollPersistence, type SessionScroll } from "./layout-scroll"
|
||||||
@@ -19,7 +18,6 @@ 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 }
|
||||||
|
|
||||||
@@ -56,11 +54,14 @@ 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
|
||||||
@@ -77,7 +78,6 @@ 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 =
|
||||||
@@ -86,6 +86,14 @@ 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
|
||||||
@@ -299,7 +307,6 @@ 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
|
||||||
@@ -358,12 +365,6 @@ 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)
|
||||||
@@ -492,7 +493,7 @@ export const { use: useLayout, provider: LayoutProvider } = createSimpleContext(
|
|||||||
const root = rootFor(project.worktree)
|
const root = rootFor(project.worktree)
|
||||||
if (root === project.worktree) continue
|
if (root === project.worktree) continue
|
||||||
|
|
||||||
server.projects.remove(project.worktree)
|
server.projects.close(project.worktree)
|
||||||
|
|
||||||
if (!seen.has(root)) {
|
if (!seen.has(root)) {
|
||||||
server.projects.open(root)
|
server.projects.open(root)
|
||||||
@@ -612,14 +613,6 @@ export const { use: useLayout, provider: LayoutProvider } = createSimpleContext(
|
|||||||
},
|
},
|
||||||
projects: {
|
projects: {
|
||||||
list,
|
list,
|
||||||
recentlyClosed: createMemo(() => {
|
|
||||||
const known = new Set(serverSync().data.project.map((project) => pathKey(project.worktree)))
|
|
||||||
return server.projects
|
|
||||||
.recentlyClosed()
|
|
||||||
.filter((worktree) => known.has(pathKey(worktree)))
|
|
||||||
.slice(0, RECENTLY_CLOSED_DISPLAY_LIMIT)
|
|
||||||
.map((worktree) => enrich({ worktree, expanded: false }))
|
|
||||||
}),
|
|
||||||
open(directory: string) {
|
open(directory: string) {
|
||||||
const root = rootFor(directory)
|
const root = rootFor(directory)
|
||||||
if (server.projects.list().find((x) => x.worktree === root)) return
|
if (server.projects.list().find((x) => x.worktree === root)) return
|
||||||
@@ -789,14 +782,6 @@ 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"))
|
||||||
@@ -880,32 +865,6 @@ 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()
|
||||||
@@ -977,17 +936,10 @@ 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
|
||||||
@@ -1000,44 +952,40 @@ 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")
|
||||||
batch(() => {
|
|
||||||
if (!store.sessionTabs[session]) {
|
if (!store.sessionTabs[session]) {
|
||||||
setStore("sessionTabs", session, { all: next, active: undefined })
|
setStore("sessionTabs", session, { all: next, active: undefined })
|
||||||
} else {
|
} else {
|
||||||
setStore("sessionTabs", session, "all", next)
|
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()
|
||||||
apply(
|
const next = nextSessionTabsForOpen(store.sessionTabs[session], normalize(tab))
|
||||||
session,
|
setStore("sessionTabs", session, next)
|
||||||
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(
|
|
||||||
session,
|
if (tab === "review") {
|
||||||
closeSessionTab({ tabs: current, preview: ephemeral.sessionTabPreview[session] }, normalize(tab)),
|
if (current.active !== tab) return
|
||||||
)
|
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,13 +15,11 @@ 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,
|
||||||
@@ -33,22 +31,6 @@ 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",
|
||||||
@@ -63,8 +45,6 @@ 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>>) {
|
||||||
@@ -91,40 +71,6 @@ 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++) {
|
||||||
@@ -178,238 +124,6 @@ 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")
|
||||||
@@ -1191,26 +905,6 @@ 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,11 +54,6 @@ 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]
|
||||||
@@ -352,7 +347,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, baseline?: MessageLoadBaseline) => {
|
const resetMessageLoad = (sessionID: string, load: MessageLoadState) => {
|
||||||
load.touchedMessages.clear()
|
load.touchedMessages.clear()
|
||||||
load.retainedMessages.clear()
|
load.retainedMessages.clear()
|
||||||
load.touchedParts.clear()
|
load.touchedParts.clear()
|
||||||
@@ -385,27 +380,8 @@ 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)
|
||||||
@@ -484,18 +460,6 @@ 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))
|
||||||
@@ -614,68 +578,25 @@ 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
|
||||||
try {
|
await fetchMessages(sessionID, limit, before, () => resetMessageLoad(sessionID, load))
|
||||||
const page = await fetchMessages(sessionID, limit, before, () => resetMessageLoad(sessionID, load))
|
.then((page) => {
|
||||||
|
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 parents = [] as Awaited<ReturnType<typeof fetchMessage>>[]
|
|
||||||
if (mode !== "prepend") {
|
|
||||||
const users = new Set([
|
|
||||||
...page.session.filter((message) => message.role === "user").map((message) => message.id),
|
|
||||||
...(data.message[sessionID] ?? [])
|
|
||||||
.filter((message) => {
|
|
||||||
if (message.role !== "user") return false
|
|
||||||
const item = optimistic.get(sessionID)?.get(message.id)
|
|
||||||
return load.touchedMessages.has(message.id) && (!item || item.confirmedMessage === true)
|
|
||||||
})
|
|
||||||
.map((message) => message.id),
|
|
||||||
])
|
|
||||||
const parentIDs = [
|
|
||||||
...new Set(
|
|
||||||
page.session.flatMap((message) =>
|
|
||||||
message.role === "assistant" && !users.has(message.parentID) ? [message.parentID] : [],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
]
|
|
||||||
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 (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 =
|
const preserveUnfetched =
|
||||||
mode === "prepend" || (!result.complete && (!first || ((message: Message) => cmpMessage(message, first) < 0)))
|
mode === "prepend" || (!page.complete && (!first || ((message: Message) => cmpMessage(message, first) < 0)))
|
||||||
applyMessagePage(
|
applyMessagePage(
|
||||||
sessionID,
|
sessionID,
|
||||||
result,
|
page,
|
||||||
messageLoads.get(sessionID) === load ? load : undefined,
|
messageLoads.get(sessionID) === load ? load : undefined,
|
||||||
preserveUnfetched,
|
preserveUnfetched,
|
||||||
mode !== "prepend",
|
mode !== "prepend",
|
||||||
)
|
)
|
||||||
applied = true
|
applied = true
|
||||||
} finally {
|
})
|
||||||
|
.finally(() => {
|
||||||
if (!applied && generations.get(sessionID) === active && messageLoads.get(sessionID) === load) {
|
if (!applied && generations.get(sessionID) === active && messageLoads.get(sessionID) === load) {
|
||||||
for (const messageID of load.orphanParents) {
|
for (const messageID of load.orphanParents) {
|
||||||
if (!orphanParts.get(sessionID)?.has(messageID)) continue
|
if (!orphanParts.get(sessionID)?.has(messageID)) continue
|
||||||
@@ -686,7 +607,7 @@ export function createServerSession(client: OpencodeClient, options?: { retry?:
|
|||||||
}
|
}
|
||||||
if (messageLoads.get(sessionID) === load) messageLoads.delete(sessionID)
|
if (messageLoads.get(sessionID) === load) messageLoads.delete(sessionID)
|
||||||
if (generations.get(sessionID) === active) setMeta("loading", sessionID, false)
|
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 }) => {
|
||||||
|
|||||||
@@ -99,7 +99,7 @@ describe("createServerProjects", () => {
|
|||||||
test("keeps active and explicit server buckets in one reactive store", () => {
|
test("keeps active and explicit server buckets in one reactive store", () => {
|
||||||
createRoot((dispose) => {
|
createRoot((dispose) => {
|
||||||
const [scope] = createSignal(ServerScope.local)
|
const [scope] = createSignal(ServerScope.local)
|
||||||
const [store, setStore] = createStore({ projects: {}, lastProject: {}, recentlyClosed: {} })
|
const [store, setStore] = createStore({ projects: {}, lastProject: {} })
|
||||||
const active = createServerProjects({ scope, store, setStore })
|
const active = createServerProjects({ scope, store, setStore })
|
||||||
const remote = createServerProjects({ scope: () => "https://debian.example" as ServerScope, store, setStore })
|
const remote = createServerProjects({ scope: () => "https://debian.example" as ServerScope, store, setStore })
|
||||||
|
|
||||||
@@ -115,88 +115,6 @@ describe("createServerProjects", () => {
|
|||||||
dispose()
|
dispose()
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
test("tracks recently closed projects and drops them when reopened", () => {
|
|
||||||
createRoot((dispose) => {
|
|
||||||
const [scope] = createSignal(ServerScope.local)
|
|
||||||
const [store, setStore] = createStore({ projects: {}, lastProject: {}, recentlyClosed: {} })
|
|
||||||
const projects = createServerProjects({ scope, store, setStore })
|
|
||||||
|
|
||||||
projects.open("/a")
|
|
||||||
projects.open("/b")
|
|
||||||
projects.close("/a")
|
|
||||||
expect(projects.recentlyClosed()).toEqual(["/a"])
|
|
||||||
|
|
||||||
projects.close("/b")
|
|
||||||
expect(projects.recentlyClosed()).toEqual(["/b", "/a"])
|
|
||||||
|
|
||||||
projects.open("/a")
|
|
||||||
expect(projects.recentlyClosed()).toEqual(["/b"])
|
|
||||||
expect(projects.list()).toEqual([{ worktree: "/a", expanded: true }])
|
|
||||||
dispose()
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
test("remove drops a project without recording it as recently closed", () => {
|
|
||||||
createRoot((dispose) => {
|
|
||||||
const [scope] = createSignal(ServerScope.local)
|
|
||||||
const [store, setStore] = createStore({ projects: {}, lastProject: {}, recentlyClosed: {} })
|
|
||||||
const projects = createServerProjects({ scope, store, setStore })
|
|
||||||
|
|
||||||
projects.open("/repo/subdir")
|
|
||||||
projects.remove("/repo/subdir")
|
|
||||||
expect(projects.list()).toEqual([])
|
|
||||||
expect(projects.recentlyClosed()).toEqual([])
|
|
||||||
dispose()
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
test("retains recently closed history beyond the visible display limit", () => {
|
|
||||||
createRoot((dispose) => {
|
|
||||||
const [scope] = createSignal(ServerScope.local)
|
|
||||||
const [store, setStore] = createStore({ projects: {}, lastProject: {}, recentlyClosed: {} })
|
|
||||||
const projects = createServerProjects({ scope, store, setStore })
|
|
||||||
|
|
||||||
// Closing 6 projects keeps all 6 in the store even though only 5 are displayed;
|
|
||||||
// this prevents display-filtered entries from evicting still-visible ones.
|
|
||||||
for (const dir of ["/1", "/2", "/3", "/4", "/5", "/6"]) {
|
|
||||||
projects.open(dir)
|
|
||||||
projects.close(dir)
|
|
||||||
}
|
|
||||||
expect(projects.recentlyClosed()).toEqual(["/6", "/5", "/4", "/3", "/2", "/1"])
|
|
||||||
dispose()
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
test("caps recently closed history at the store limit", () => {
|
|
||||||
createRoot((dispose) => {
|
|
||||||
const [scope] = createSignal(ServerScope.local)
|
|
||||||
const [store, setStore] = createStore({ projects: {}, lastProject: {}, recentlyClosed: {} })
|
|
||||||
const projects = createServerProjects({ scope, store, setStore })
|
|
||||||
|
|
||||||
for (let i = 1; i <= 20; i++) {
|
|
||||||
projects.open(`/p${i}`)
|
|
||||||
projects.close(`/p${i}`)
|
|
||||||
}
|
|
||||||
expect(projects.recentlyClosed()).toHaveLength(16)
|
|
||||||
expect(projects.recentlyClosed()[0]).toBe("/p20")
|
|
||||||
expect(projects.recentlyClosed().at(-1)).toBe("/p5")
|
|
||||||
dispose()
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
test("dedupes recently closed entries by normalized path", () => {
|
|
||||||
createRoot((dispose) => {
|
|
||||||
const [scope] = createSignal(ServerScope.local)
|
|
||||||
const [store, setStore] = createStore({ projects: {}, lastProject: {}, recentlyClosed: {} })
|
|
||||||
const projects = createServerProjects({ scope, store, setStore })
|
|
||||||
|
|
||||||
projects.close("/repo")
|
|
||||||
projects.close("/repo/")
|
|
||||||
expect(projects.recentlyClosed()).toEqual(["/repo/"])
|
|
||||||
dispose()
|
|
||||||
})
|
|
||||||
})
|
|
||||||
})
|
})
|
||||||
|
|
||||||
describe("migrateCanonicalLocalServerState", () => {
|
describe("migrateCanonicalLocalServerState", () => {
|
||||||
|
|||||||
@@ -2,23 +2,12 @@ import { createSimpleContext } from "@opencode-ai/ui/context"
|
|||||||
import { type Accessor, batch, createMemo } from "solid-js"
|
import { type Accessor, batch, createMemo } from "solid-js"
|
||||||
import { createStore, type SetStoreFunction, type Store } from "solid-js/store"
|
import { createStore, type SetStoreFunction, type Store } from "solid-js/store"
|
||||||
import { Persist, persisted } from "@/utils/persist"
|
import { Persist, persisted } from "@/utils/persist"
|
||||||
import { pathKey } from "@/utils/path-key"
|
|
||||||
import { ServerScope } from "@/utils/server-scope"
|
import { ServerScope } from "@/utils/server-scope"
|
||||||
|
|
||||||
type StoredProject = { worktree: string; expanded: boolean }
|
type StoredProject = { worktree: string; expanded: boolean }
|
||||||
type StoredServer = string | ServerConnection.HttpBase | ServerConnection.Http
|
type StoredServer = string | ServerConnection.HttpBase | ServerConnection.Http
|
||||||
type ServerProjectState = {
|
type ServerProjectState = { projects: Record<string, StoredProject[]>; lastProject: Record<string, string> }
|
||||||
projects: Record<string, StoredProject[]>
|
|
||||||
lastProject: Record<string, string>
|
|
||||||
recentlyClosed: Record<string, string[]>
|
|
||||||
}
|
|
||||||
const HEALTH_POLL_INTERVAL_MS = 10_000
|
const HEALTH_POLL_INTERVAL_MS = 10_000
|
||||||
// The store retains more history than is displayed. Consumers filter recently closed entries
|
|
||||||
// against the live project list (dropping deleted projects) and then cap the visible count via
|
|
||||||
// RECENTLY_CLOSED_DISPLAY_LIMIT. Retaining extra history ensures entries that are temporarily
|
|
||||||
// filtered out do not evict still-visible ones from the persisted store.
|
|
||||||
const RECENTLY_CLOSED_HISTORY_LIMIT = 16
|
|
||||||
export const RECENTLY_CLOSED_DISPLAY_LIMIT = 5
|
|
||||||
|
|
||||||
export function normalizeServerUrl(input: string) {
|
export function normalizeServerUrl(input: string) {
|
||||||
const trimmed = input.trim()
|
const trimmed = input.trim()
|
||||||
@@ -83,42 +72,19 @@ export function createServerProjects<T extends ServerProjectState>(input: {
|
|||||||
}) {
|
}) {
|
||||||
const setStore = input.setStore as unknown as SetStoreFunction<ServerProjectState>
|
const setStore = input.setStore as unknown as SetStoreFunction<ServerProjectState>
|
||||||
const current = () => input.store.projects[input.scope()] ?? []
|
const current = () => input.store.projects[input.scope()] ?? []
|
||||||
const currentClosed = () => input.store.recentlyClosed?.[input.scope()] ?? []
|
return {
|
||||||
const remove = (directory: string) => {
|
list: current,
|
||||||
|
open(directory: string) {
|
||||||
|
const scope = input.scope()
|
||||||
|
if (current().some((project) => project.worktree === directory)) return
|
||||||
|
setStore("projects", scope, [{ worktree: directory, expanded: true }, ...current()])
|
||||||
|
},
|
||||||
|
close(directory: string) {
|
||||||
setStore(
|
setStore(
|
||||||
"projects",
|
"projects",
|
||||||
input.scope(),
|
input.scope(),
|
||||||
current().filter((project) => project.worktree !== directory),
|
current().filter((project) => project.worktree !== directory),
|
||||||
)
|
)
|
||||||
}
|
|
||||||
return {
|
|
||||||
list: current,
|
|
||||||
recentlyClosed: currentClosed,
|
|
||||||
remove,
|
|
||||||
open(directory: string) {
|
|
||||||
const scope = input.scope()
|
|
||||||
const key = pathKey(directory)
|
|
||||||
const closed = currentClosed()
|
|
||||||
if (closed.some((worktree) => pathKey(worktree) === key)) {
|
|
||||||
setStore(
|
|
||||||
"recentlyClosed",
|
|
||||||
scope,
|
|
||||||
closed.filter((worktree) => pathKey(worktree) !== key),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
if (current().some((project) => project.worktree === directory)) return
|
|
||||||
setStore("projects", scope, [{ worktree: directory, expanded: true }, ...current()])
|
|
||||||
},
|
|
||||||
// User-initiated close: removes the project and records it in recently closed.
|
|
||||||
// Internal, non-user removals (e.g. sandbox/worktree normalization) should use remove().
|
|
||||||
close(directory: string) {
|
|
||||||
remove(directory)
|
|
||||||
const key = pathKey(directory)
|
|
||||||
const closed = [directory, ...currentClosed().filter((worktree) => pathKey(worktree) !== key)].slice(
|
|
||||||
0,
|
|
||||||
RECENTLY_CLOSED_HISTORY_LIMIT,
|
|
||||||
)
|
|
||||||
setStore("recentlyClosed", input.scope(), closed)
|
|
||||||
},
|
},
|
||||||
expand(directory: string) {
|
expand(directory: string) {
|
||||||
const index = current().findIndex((project) => project.worktree === directory)
|
const index = current().findIndex((project) => project.worktree === directory)
|
||||||
@@ -269,7 +235,6 @@ export const { use: useServer, provider: ServerProvider } = createSimpleContext(
|
|||||||
list: [] as StoredServer[],
|
list: [] as StoredServer[],
|
||||||
projects: {} as Record<string, StoredProject[]>,
|
projects: {} as Record<string, StoredProject[]>,
|
||||||
lastProject: {} as Record<string, string>,
|
lastProject: {} as Record<string, string>,
|
||||||
recentlyClosed: {} as Record<string, string[]>,
|
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -312,10 +277,7 @@ export const { use: useServer, provider: ServerProvider } = createSimpleContext(
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
const isReady = Object.assign(
|
const isReady = createMemo(() => ready() && !!state.active)
|
||||||
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,15 +1,6 @@
|
|||||||
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", () => {
|
||||||
@@ -31,75 +22,3 @@ 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,7 +10,6 @@ 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"
|
||||||
@@ -28,11 +27,6 @@ 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
|
||||||
}
|
}
|
||||||
@@ -69,8 +63,6 @@ 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()
|
||||||
@@ -95,28 +87,10 @@ 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(() => {
|
||||||
@@ -125,26 +99,11 @@ 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)) {
|
if (!servers.has(tab.server)) memory.remove(tabKey(tab))
|
||||||
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) => {
|
||||||
@@ -158,7 +117,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 = nextTabAfterClose(store, index, recentKey() === key && location.pathname !== "/")
|
const nextTab = store[index + 1] ?? store[index - 1]
|
||||||
closing.add(key)
|
closing.add(key)
|
||||||
void startTransition(() => {
|
void startTransition(() => {
|
||||||
setStore(
|
setStore(
|
||||||
@@ -166,14 +125,11 @@ export const { use: useTabs, provider: TabsProvider } = createSimpleContext({
|
|||||||
tabs.splice(index, 1)
|
tabs.splice(index, 1)
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
if (nextTab === null) {
|
if (recent.key === key) setRecentKey(nextTab && tabKey(nextTab))
|
||||||
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)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -245,57 +201,23 @@ 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),
|
||||||
@@ -343,14 +265,6 @@ 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) {
|
||||||
@@ -375,6 +289,6 @@ export const { use: useTabs, provider: TabsProvider } = createSimpleContext({
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
return { ...actions, store, info, ready, recentReady }
|
return { ...actions, store, ready, recentReady }
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|||||||
+2
-148
@@ -21,9 +21,6 @@ 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": "فتح الإعدادات",
|
||||||
@@ -42,7 +39,6 @@ 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": "التركيز على حقل الإدخال",
|
||||||
@@ -197,11 +193,6 @@ 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}}",
|
||||||
@@ -211,7 +202,6 @@ 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": "إرسال",
|
||||||
@@ -228,8 +218,6 @@ 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": "إصلاح الاختبارات المعطلة",
|
||||||
@@ -326,74 +314,6 @@ 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": "أيقونة",
|
||||||
@@ -450,7 +370,6 @@ 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": "لغة",
|
||||||
@@ -487,12 +406,10 @@ 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}}",
|
||||||
@@ -533,38 +450,16 @@ 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": "اختر ملفًا لفتحه",
|
||||||
@@ -580,10 +475,6 @@ 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": "إرسال الآن",
|
||||||
@@ -596,16 +487,9 @@ 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": "بحث عن الملفات",
|
||||||
@@ -666,8 +550,6 @@ 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": "الخادم",
|
||||||
@@ -677,7 +559,6 @@ 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": "المؤثرات الصوتية",
|
||||||
@@ -685,11 +566,6 @@ 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": "مخطط الألوان",
|
||||||
@@ -698,29 +574,14 @@ 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": "خط الطرفية",
|
"settings.general.row.terminalFont.title": "Terminal Font",
|
||||||
"settings.general.row.terminalFont.description": "خصّص الخط المستخدم في الطرفية",
|
"settings.general.row.terminalFont.description": "Customise the font used in the terminal",
|
||||||
"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",
|
||||||
@@ -729,11 +590,6 @@ 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":
|
||||||
@@ -746,8 +602,6 @@ 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": "بلا",
|
||||||
|
|||||||
+3
-152
@@ -21,9 +21,6 @@ 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",
|
||||||
@@ -42,7 +39,6 @@ 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",
|
||||||
@@ -197,11 +193,6 @@ 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}}",
|
||||||
@@ -211,7 +202,6 @@ 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",
|
||||||
@@ -228,8 +218,6 @@ 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",
|
||||||
@@ -326,75 +314,6 @@ 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",
|
||||||
@@ -452,7 +371,6 @@ 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",
|
||||||
@@ -489,12 +407,10 @@ 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}}",
|
||||||
@@ -537,30 +453,10 @@ 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",
|
||||||
@@ -570,8 +466,6 @@ 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",
|
||||||
@@ -585,10 +479,6 @@ 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",
|
||||||
@@ -601,16 +491,9 @@ 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",
|
||||||
@@ -674,18 +557,15 @@ 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": "Integração com WSL",
|
"settings.desktop.wsl.title": "WSL integration",
|
||||||
"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",
|
||||||
@@ -693,11 +573,6 @@ 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",
|
||||||
@@ -706,8 +581,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": "Fonte do terminal",
|
"settings.general.row.terminalFont.title": "Terminal Font",
|
||||||
"settings.general.row.terminalFont.description": "Personalize a fonte usada no terminal",
|
"settings.general.row.terminalFont.description": "Customise the font used in the 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",
|
||||||
@@ -715,22 +590,6 @@ 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",
|
||||||
@@ -739,12 +598,6 @@ 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":
|
||||||
@@ -757,8 +610,6 @@ 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",
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user