mirror of
https://github.com/anomalyco/opencode.git
synced 2026-08-16 01:19:19 -04:00
Compare commits
3 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| ac384f3d27 | |||
| 92a50f2397 | |||
| ac7ee99a24 |
@@ -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,12 +19,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",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
"packages/app": {
|
"packages/app": {
|
||||||
@@ -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=="],
|
||||||
@@ -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=="],
|
||||||
|
|
||||||
@@ -5621,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=="],
|
||||||
@@ -5875,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=="],
|
||||||
@@ -5885,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=="],
|
||||||
@@ -5941,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=="],
|
||||||
@@ -6215,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=="],
|
||||||
@@ -6353,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=="],
|
||||||
|
|
||||||
@@ -6459,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=="],
|
||||||
@@ -6947,8 +6952,6 @@
|
|||||||
|
|
||||||
"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=="],
|
||||||
@@ -7079,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=="],
|
||||||
@@ -7091,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=="],
|
||||||
@@ -7359,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=="],
|
||||||
@@ -7381,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=="],
|
||||||
@@ -7491,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-ovrz0pALxRek5VEqSYkVpzb9YeiiVQWxfAHZQ3kX0/0=",
|
"x86_64-linux": "sha256-ZmAIcG3wPgssKejLe/EMJOjPuMHCyRiiItRvSqIjaME=",
|
||||||
"aarch64-linux": "sha256-+sqBt11Nl1fDrL1JvFAcN8JMWahspfbBPuMeM7YdagU=",
|
"aarch64-linux": "sha256-lbAHoVI/CvgdrfKZ/9dKZ/O7DxJvuWEOrDgcCuFGPBk=",
|
||||||
"aarch64-darwin": "sha256-PsZfcmMpz/Xzudjv4CQZu+wPfnyijrlVYeP2x9tRffk=",
|
"aarch64-darwin": "sha256-Nrs4ijvzQ3J8AYigJSlsr66feUvLZ5iiYCfBdaZNJEc=",
|
||||||
"x86_64-darwin": "sha256-4TpIc3Gh9nDaA5Y1zkCq0KraMdWu+PWtYVY3p7/CsJ0="
|
"x86_64-darwin": "sha256-ltCaJSO7kZWuMOU49269xvboNVX35OJDFj0gXCDf62Y="
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+2
-1
@@ -99,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",
|
||||||
|
|||||||
@@ -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),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -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"
|
||||||
@@ -27,9 +17,6 @@ import { MenuV2 } from "@opencode-ai/ui/v2/menu-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)
|
||||||
@@ -255,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))
|
||||||
@@ -341,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}>
|
||||||
|
|||||||
@@ -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,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()}
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import { createSignal, Show, type JSXElement } from "solid-js"
|
|||||||
import "./titlebar-tab-popover.css"
|
import "./titlebar-tab-popover.css"
|
||||||
|
|
||||||
// Initial hover delay before the preview appears, per design.
|
// Initial hover delay before the preview appears, per design.
|
||||||
const OPEN_DELAY = 400
|
const OPEN_DELAY = 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
|
||||||
|
|||||||
@@ -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,
|
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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"
|
||||||
@@ -494,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)
|
||||||
@@ -614,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
|
||||||
|
|||||||
@@ -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[]>,
|
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -604,7 +604,6 @@ export const dict = {
|
|||||||
"home.title": "Home",
|
"home.title": "Home",
|
||||||
"home.projects": "Projects",
|
"home.projects": "Projects",
|
||||||
"home.project.add": "Add project",
|
"home.project.add": "Add project",
|
||||||
"home.recentlyClosed": "Recently closed",
|
|
||||||
"home.server.collapse": "Collapse server projects",
|
"home.server.collapse": "Collapse server projects",
|
||||||
"home.server.expand": "Expand server projects",
|
"home.server.expand": "Expand server projects",
|
||||||
"home.sessions.search.placeholder": "Search sessions",
|
"home.sessions.search.placeholder": "Search sessions",
|
||||||
|
|||||||
@@ -270,10 +270,6 @@ export function NewHome() {
|
|||||||
})
|
})
|
||||||
const focusedSync = () => focusedServerCtx()?.sync ?? sync()
|
const focusedSync = () => focusedServerCtx()?.sync ?? sync()
|
||||||
const projects = createMemo(() => focusedServerCtx()?.projects.list() ?? layout.projects.list())
|
const projects = createMemo(() => focusedServerCtx()?.projects.list() ?? layout.projects.list())
|
||||||
const recentlyClosed = createMemo(
|
|
||||||
() => focusedServerCtx()?.projects.recentlyClosed() ?? layout.projects.recentlyClosed(),
|
|
||||||
)
|
|
||||||
const homedir = createMemo(() => focusedSync().data.path.home ?? "")
|
|
||||||
const selectedProject = createMemo(() => projects().find((project) => project.worktree === selection().directory))
|
const selectedProject = createMemo(() => projects().find((project) => project.worktree === selection().directory))
|
||||||
const newSessionProject = createMemo(
|
const newSessionProject = createMemo(
|
||||||
() =>
|
() =>
|
||||||
@@ -522,13 +518,10 @@ export function NewHome() {
|
|||||||
<div class="mx-auto grid h-full w-full max-w-[1080px] grid-rows-[auto_minmax(0,1fr)_auto] gap-4 px-3 lg:grid-cols-[280px_minmax(0,720px)] lg:grid-rows-1 lg:gap-8 lg:px-6">
|
<div class="mx-auto grid h-full w-full max-w-[1080px] grid-rows-[auto_minmax(0,1fr)_auto] gap-4 px-3 lg:grid-cols-[280px_minmax(0,720px)] lg:grid-rows-1 lg:gap-8 lg:px-6">
|
||||||
<HomeProjectColumn
|
<HomeProjectColumn
|
||||||
projects={projects()}
|
projects={projects()}
|
||||||
recentlyClosed={recentlyClosed()}
|
|
||||||
homedir={homedir()}
|
|
||||||
selected={selection()}
|
selected={selection()}
|
||||||
focusServer={focusServer}
|
focusServer={focusServer}
|
||||||
selectProject={selectProject}
|
selectProject={selectProject}
|
||||||
openNewSession={openProjectNewSession}
|
openNewSession={openProjectNewSession}
|
||||||
openRecentProject={(conn, directory) => addProjects(conn, [directory])}
|
|
||||||
chooseProject={(conn) => void chooseProject(conn)}
|
chooseProject={(conn) => void chooseProject(conn)}
|
||||||
editProject={editProject}
|
editProject={editProject}
|
||||||
closeProject={(conn, directory) => {
|
closeProject={(conn, directory) => {
|
||||||
@@ -645,13 +638,10 @@ export function NewHome() {
|
|||||||
|
|
||||||
function HomeProjectColumn(props: {
|
function HomeProjectColumn(props: {
|
||||||
projects: LocalProject[]
|
projects: LocalProject[]
|
||||||
recentlyClosed: LocalProject[]
|
|
||||||
homedir: string
|
|
||||||
selected: HomeProjectSelection
|
selected: HomeProjectSelection
|
||||||
focusServer: (server: ServerConnection.Any) => void
|
focusServer: (server: ServerConnection.Any) => void
|
||||||
selectProject: (server: ServerConnection.Any, directory: string) => void
|
selectProject: (server: ServerConnection.Any, directory: string) => void
|
||||||
openNewSession: (server: ServerConnection.Any, directory: string) => void
|
openNewSession: (server: ServerConnection.Any, directory: string) => void
|
||||||
openRecentProject: (server: ServerConnection.Any, directory: string) => void
|
|
||||||
chooseProject: (server: ServerConnection.Any) => void
|
chooseProject: (server: ServerConnection.Any) => void
|
||||||
editProject: (server: ServerConnection.Any, project: LocalProject) => void
|
editProject: (server: ServerConnection.Any, project: LocalProject) => void
|
||||||
closeProject: (server: ServerConnection.Any, directory: string) => void
|
closeProject: (server: ServerConnection.Any, directory: string) => void
|
||||||
@@ -680,10 +670,8 @@ function HomeProjectColumn(props: {
|
|||||||
aria-label={props.language.t("home.projects")}
|
aria-label={props.language.t("home.projects")}
|
||||||
>
|
>
|
||||||
<div class="flex h-7 min-w-0 shrink-0 items-center justify-between pl-1.5 pr-3">
|
<div class="flex h-7 min-w-0 shrink-0 items-center justify-between pl-1.5 pr-3">
|
||||||
<div class="text-v2-text-text-muted [font-weight:530]">{props.language.t("home.projects")}</div>
|
<div class={HOME_SECTION_LABEL}>{props.language.t("home.projects")}</div>
|
||||||
<Show
|
<Show when={global.servers.list().length === 1}>
|
||||||
when={global.servers.list().length === 1 && !(props.projects.length === 0 && props.recentlyClosed.length > 0)}
|
|
||||||
>
|
|
||||||
<TooltipV2 placement="bottom" value={props.language.t("home.project.add")}>
|
<TooltipV2 placement="bottom" value={props.language.t("home.project.add")}>
|
||||||
<IconButtonV2
|
<IconButtonV2
|
||||||
data-action="home-add-project"
|
data-action="home-add-project"
|
||||||
@@ -703,21 +691,7 @@ function HomeProjectColumn(props: {
|
|||||||
when={global.servers.list().length > 1}
|
when={global.servers.list().length > 1}
|
||||||
fallback={
|
fallback={
|
||||||
<div class="pr-3">
|
<div class="pr-3">
|
||||||
<Show
|
|
||||||
when={props.projects.length > 0}
|
|
||||||
fallback={
|
|
||||||
<HomeProjectEmpty
|
|
||||||
server={global.servers.list()[0]!}
|
|
||||||
recentlyClosed={props.recentlyClosed}
|
|
||||||
homedir={props.homedir}
|
|
||||||
chooseProject={props.chooseProject}
|
|
||||||
openRecentProject={props.openRecentProject}
|
|
||||||
language={props.language}
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<HomeProjectList {...props} server={global.servers.list()[0]!} />
|
<HomeProjectList {...props} server={global.servers.list()[0]!} />
|
||||||
</Show>
|
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
@@ -923,79 +897,6 @@ function HomeProjectList(props: {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
function HomeProjectEmpty(props: {
|
|
||||||
server: ServerConnection.Any
|
|
||||||
recentlyClosed: LocalProject[]
|
|
||||||
homedir: string
|
|
||||||
chooseProject: (server: ServerConnection.Any) => void
|
|
||||||
openRecentProject: (server: ServerConnection.Any, directory: string) => void
|
|
||||||
language: ReturnType<typeof useLanguage>
|
|
||||||
}) {
|
|
||||||
const global = useGlobal()
|
|
||||||
const unreachable = () => global.servers.health[ServerConnection.key(props.server)]?.healthy === false
|
|
||||||
return (
|
|
||||||
<div class="flex min-w-0 flex-col gap-1">
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
data-action="home-add-project-row"
|
|
||||||
class={`${HOME_PROJECT_NAV_ROW} disabled:opacity-60 [&>[data-slot=icon-svg]]:text-v2-icon-icon-muted`}
|
|
||||||
disabled={unreachable()}
|
|
||||||
onClick={() => props.chooseProject(props.server)}
|
|
||||||
>
|
|
||||||
<IconV2 name="folder-add-left" size="small" />
|
|
||||||
<span class={HOME_PROJECT_NAV_LABEL}>{props.language.t("home.project.add")}</span>
|
|
||||||
</button>
|
|
||||||
<Show when={props.recentlyClosed.length > 0}>
|
|
||||||
<div class="mt-3 flex h-7 min-w-0 shrink-0 items-center pl-1.5 pr-3">
|
|
||||||
<div class="text-v2-text-text-faint [font-weight:530]">{props.language.t("home.recentlyClosed")}</div>
|
|
||||||
</div>
|
|
||||||
<For each={props.recentlyClosed}>
|
|
||||||
{(project) => (
|
|
||||||
<HomeRecentlyClosedRow
|
|
||||||
project={project}
|
|
||||||
server={props.server}
|
|
||||||
homedir={props.homedir}
|
|
||||||
openRecentProject={props.openRecentProject}
|
|
||||||
language={props.language}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</For>
|
|
||||||
</Show>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function HomeRecentlyClosedRow(props: {
|
|
||||||
project: LocalProject
|
|
||||||
server: ServerConnection.Any
|
|
||||||
homedir: string
|
|
||||||
openRecentProject: (server: ServerConnection.Any, directory: string) => void
|
|
||||||
language: ReturnType<typeof useLanguage>
|
|
||||||
}) {
|
|
||||||
const global = useGlobal()
|
|
||||||
const unreachable = () => global.servers.health[ServerConnection.key(props.server)]?.healthy === false
|
|
||||||
const path = () => {
|
|
||||||
const home = props.homedir
|
|
||||||
const worktree = props.project.worktree
|
|
||||||
if (home && (worktree === home || worktree.startsWith(`${home}/`))) return `~${worktree.slice(home.length)}`
|
|
||||||
return worktree
|
|
||||||
}
|
|
||||||
return (
|
|
||||||
<TooltipV2 placement="right" value={path()}>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
data-component="home-recently-closed-row"
|
|
||||||
class={`${HOME_PROJECT_NAV_ROW} disabled:opacity-60`}
|
|
||||||
disabled={unreachable()}
|
|
||||||
onClick={() => props.openRecentProject(props.server, props.project.worktree)}
|
|
||||||
>
|
|
||||||
<HomeProjectAvatar project={props.project} outline />
|
|
||||||
<span class={HOME_PROJECT_NAV_LABEL}>{displayName(props.project)}</span>
|
|
||||||
</button>
|
|
||||||
</TooltipV2>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function HomeProjectRow(props: {
|
function HomeProjectRow(props: {
|
||||||
project: LocalProject
|
project: LocalProject
|
||||||
server: ServerConnection.Any
|
server: ServerConnection.Any
|
||||||
@@ -1078,13 +979,13 @@ function HomeProjectRow(props: {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
function HomeProjectAvatar(props: { project: LocalProject; outline?: boolean }) {
|
function HomeProjectAvatar(props: { project: LocalProject }) {
|
||||||
const name = createMemo(() => displayName(props.project))
|
const name = createMemo(() => displayName(props.project))
|
||||||
return (
|
return (
|
||||||
<ProjectAvatar
|
<ProjectAvatar
|
||||||
fallback={name()}
|
fallback={name()}
|
||||||
src={props.outline ? undefined : getProjectAvatarSource(props.project.id, props.project.icon)}
|
src={getProjectAvatarSource(props.project.id, props.project.icon)}
|
||||||
variant={props.outline ? "outline" : getProjectAvatarVariant(props.project.icon?.color)}
|
variant={getProjectAvatarVariant(props.project.icon?.color)}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1393,14 +1393,14 @@ export function MessageTimeline(props: {
|
|||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
data-slot="session-title-parent"
|
data-slot="session-title-parent"
|
||||||
class="min-w-0 max-w-[40%] truncate pl-2 text-[13px] font-[530] leading-4 tracking-[-0.04px] text-v2-text-text-faint transition-colors hover:text-v2-text-text-muted"
|
class="min-w-0 max-w-[40%] truncate px-2 text-[13px] font-[530] leading-4 tracking-[-0.04px] text-v2-text-text-faint transition-colors hover:text-v2-text-text-muted"
|
||||||
onClick={navigateParent}
|
onClick={navigateParent}
|
||||||
>
|
>
|
||||||
{parentTitle()}
|
{parentTitle()}
|
||||||
</button>
|
</button>
|
||||||
<span
|
<span
|
||||||
data-slot="session-title-separator"
|
data-slot="session-title-separator"
|
||||||
class="-translate-y-[0.5px] pl-2 pr-1 text-[11px] font-medium text-v2-text-text-faint"
|
class="-translate-y-[0.5px] px-1 text-[11px] font-medium text-v2-text-text-faint"
|
||||||
aria-hidden="true"
|
aria-hidden="true"
|
||||||
>
|
>
|
||||||
/
|
/
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ import { useLanguage } from "@/context/language"
|
|||||||
import { useLocal } from "@/context/local"
|
import { useLocal } from "@/context/local"
|
||||||
import { useSettings } from "@/context/settings"
|
import { useSettings } from "@/context/settings"
|
||||||
import { useDialog } from "@opencode-ai/ui/context/dialog"
|
import { useDialog } from "@opencode-ai/ui/context/dialog"
|
||||||
import { getCursorPosition, setCursorPosition } from "@/components/prompt-input/editor-dom"
|
|
||||||
import { useSessionLayout } from "./session-layout"
|
import { useSessionLayout } from "./session-layout"
|
||||||
import { createSessionOwnership } from "./session-ownership"
|
import { createSessionOwnership } from "./session-ownership"
|
||||||
|
|
||||||
@@ -27,23 +26,9 @@ export const useComposerCommands = () => {
|
|||||||
|
|
||||||
const chooseModel = async () => {
|
const chooseModel = async () => {
|
||||||
const owner = sessionOwnership.capture()
|
const owner = sessionOwnership.capture()
|
||||||
const editor = document.querySelector<HTMLElement>('[data-component="prompt-input"]')
|
|
||||||
const selection = window.getSelection()
|
|
||||||
const cursor =
|
|
||||||
editor && selection?.rangeCount && editor.contains(selection.anchorNode) ? getCursorPosition(editor) : null
|
|
||||||
const restoreComposer = () => {
|
|
||||||
// Kobalte restores focus during its teardown effect; defer past it so the
|
|
||||||
// composer keeps focus and the caret returns to where the user left it.
|
|
||||||
requestAnimationFrame(() => {
|
|
||||||
const editor = document.querySelector<HTMLElement>('[data-component="prompt-input"]')
|
|
||||||
if (!editor) return
|
|
||||||
editor.focus()
|
|
||||||
if (cursor !== null) setCursorPosition(editor, cursor)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
const { DialogSelectModel } = await import("@/components/dialog-select-model")
|
const { DialogSelectModel } = await import("@/components/dialog-select-model")
|
||||||
owner.run(() => {
|
owner.run(() => {
|
||||||
void dialog.show(() => <DialogSelectModel model={local.model} />, restoreComposer)
|
void dialog.show(() => <DialogSelectModel model={local.model} />)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,116 +0,0 @@
|
|||||||
const editableSelector = "input, textarea, select, [contenteditable=''], [contenteditable='true']"
|
|
||||||
|
|
||||||
export function handleDocumentSearchKeydown(
|
|
||||||
input: HTMLInputElement | undefined,
|
|
||||||
event: KeyboardEvent,
|
|
||||||
inputValue: string,
|
|
||||||
setInputValue: (value: string) => void,
|
|
||||||
) {
|
|
||||||
if (!input) return false
|
|
||||||
if (event.defaultPrevented || event.isComposing) return false
|
|
||||||
if (event.target === input) return false
|
|
||||||
if (event.target instanceof Element && event.target.closest(editableSelector)) return false
|
|
||||||
|
|
||||||
const action = searchKeyAction(event)
|
|
||||||
if (!action) return false
|
|
||||||
|
|
||||||
event.preventDefault()
|
|
||||||
event.stopPropagation()
|
|
||||||
input.focus()
|
|
||||||
|
|
||||||
const start = input.selectionStart ?? inputValue.length
|
|
||||||
const end = input.selectionEnd ?? inputValue.length
|
|
||||||
|
|
||||||
if (action.type === "selectAll") {
|
|
||||||
input.setSelectionRange(0, inputValue.length)
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
if (action.type === "move") {
|
|
||||||
moveSelection(input, inputValue, action.delta, event.shiftKey)
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
if (action.type === "home") {
|
|
||||||
setBoundarySelection(input, start, 0, event.shiftKey)
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
if (action.type === "end") {
|
|
||||||
setBoundarySelection(input, start, inputValue.length, event.shiftKey)
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
if (action.type === "deleteBackward") {
|
|
||||||
if (start !== end)
|
|
||||||
return updateValue(input, inputValue.slice(0, start) + inputValue.slice(end), start, setInputValue)
|
|
||||||
if (start === 0) return true
|
|
||||||
return updateValue(input, inputValue.slice(0, start - 1) + inputValue.slice(end), start - 1, setInputValue)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (action.type === "deleteForward") {
|
|
||||||
if (start !== end)
|
|
||||||
return updateValue(input, inputValue.slice(0, start) + inputValue.slice(end), start, setInputValue)
|
|
||||||
if (end === inputValue.length) return true
|
|
||||||
return updateValue(input, inputValue.slice(0, start) + inputValue.slice(end + 1), start, setInputValue)
|
|
||||||
}
|
|
||||||
|
|
||||||
return updateValue(
|
|
||||||
input,
|
|
||||||
inputValue.slice(0, start) + action.value + inputValue.slice(end),
|
|
||||||
start + action.value.length,
|
|
||||||
setInputValue,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function searchKeyAction(event: KeyboardEvent) {
|
|
||||||
if ((event.ctrlKey || event.metaKey) && !event.altKey && event.key.toLowerCase() === "a") {
|
|
||||||
return { type: "selectAll" } as const
|
|
||||||
}
|
|
||||||
if (event.ctrlKey || event.metaKey || event.altKey) return undefined
|
|
||||||
if (event.key.length === 1) return { type: "insert", value: event.key } as const
|
|
||||||
if (event.key === "Backspace") return { type: "deleteBackward" } as const
|
|
||||||
if (event.key === "Delete") return { type: "deleteForward" } as const
|
|
||||||
if (event.key === "ArrowLeft") return { type: "move", delta: -1 } as const
|
|
||||||
if (event.key === "ArrowRight") return { type: "move", delta: 1 } as const
|
|
||||||
if (event.key === "Home") return { type: "home" } as const
|
|
||||||
if (event.key === "End") return { type: "end" } as const
|
|
||||||
return undefined
|
|
||||||
}
|
|
||||||
|
|
||||||
function moveSelection(input: HTMLInputElement, inputValue: string, delta: -1 | 1, extend: boolean) {
|
|
||||||
const start = input.selectionStart ?? inputValue.length
|
|
||||||
const end = input.selectionEnd ?? inputValue.length
|
|
||||||
if (!extend && start !== end) {
|
|
||||||
const caret = delta < 0 ? start : end
|
|
||||||
input.setSelectionRange(caret, caret)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!extend) {
|
|
||||||
const caret = Math.max(0, Math.min(inputValue.length, start + delta))
|
|
||||||
input.setSelectionRange(caret, caret)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
const backward = input.selectionDirection === "backward"
|
|
||||||
const anchor = backward ? end : start
|
|
||||||
const focus = backward ? start : end
|
|
||||||
const next = Math.max(0, Math.min(inputValue.length, focus + delta))
|
|
||||||
input.setSelectionRange(Math.min(anchor, next), Math.max(anchor, next), next < anchor ? "backward" : "forward")
|
|
||||||
}
|
|
||||||
|
|
||||||
function setBoundarySelection(input: HTMLInputElement, anchor: number, focus: number, extend: boolean) {
|
|
||||||
if (!extend) {
|
|
||||||
input.setSelectionRange(focus, focus)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
input.setSelectionRange(Math.min(anchor, focus), Math.max(anchor, focus), focus < anchor ? "backward" : "forward")
|
|
||||||
}
|
|
||||||
|
|
||||||
function updateValue(input: HTMLInputElement, value: string, caret: number, setInputValue: (value: string) => void) {
|
|
||||||
input.value = value
|
|
||||||
setInputValue(value)
|
|
||||||
input.setSelectionRange(caret, caret)
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
@@ -1955,7 +1955,6 @@ export type ModelsListOutput = {
|
|||||||
| {
|
| {
|
||||||
readonly id: string
|
readonly id: string
|
||||||
readonly type: "native"
|
readonly type: "native"
|
||||||
readonly package?: string
|
|
||||||
readonly url?: string
|
readonly url?: string
|
||||||
readonly settings: { readonly [x: string]: JsonValue }
|
readonly settings: { readonly [x: string]: JsonValue }
|
||||||
}
|
}
|
||||||
@@ -2011,12 +2010,7 @@ export type ProvidersListOutput = {
|
|||||||
readonly url?: string
|
readonly url?: string
|
||||||
readonly settings?: { readonly [x: string]: JsonValue }
|
readonly settings?: { readonly [x: string]: JsonValue }
|
||||||
}
|
}
|
||||||
| {
|
| { readonly type: "native"; readonly url?: string; readonly settings: { readonly [x: string]: JsonValue } }
|
||||||
readonly type: "native"
|
|
||||||
readonly package?: string
|
|
||||||
readonly url?: string
|
|
||||||
readonly settings: { readonly [x: string]: JsonValue }
|
|
||||||
}
|
|
||||||
readonly request: {
|
readonly request: {
|
||||||
readonly headers: { readonly [x: string]: string }
|
readonly headers: { readonly [x: string]: string }
|
||||||
readonly body: { readonly [x: string]: JsonValue }
|
readonly body: { readonly [x: string]: JsonValue }
|
||||||
@@ -2049,12 +2043,7 @@ export type ProvidersGetOutput = {
|
|||||||
readonly url?: string
|
readonly url?: string
|
||||||
readonly settings?: { readonly [x: string]: JsonValue }
|
readonly settings?: { readonly [x: string]: JsonValue }
|
||||||
}
|
}
|
||||||
| {
|
| { readonly type: "native"; readonly url?: string; readonly settings: { readonly [x: string]: JsonValue } }
|
||||||
readonly type: "native"
|
|
||||||
readonly package?: string
|
|
||||||
readonly url?: string
|
|
||||||
readonly settings: { readonly [x: string]: JsonValue }
|
|
||||||
}
|
|
||||||
readonly request: {
|
readonly request: {
|
||||||
readonly headers: { readonly [x: string]: string }
|
readonly headers: { readonly [x: string]: string }
|
||||||
readonly body: { readonly [x: string]: JsonValue }
|
readonly body: { readonly [x: string]: JsonValue }
|
||||||
|
|||||||
@@ -55,9 +55,7 @@ const runtime = CodeMode.make({
|
|||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
const result =
|
const result = yield* runtime.execute(`
|
||||||
yield *
|
|
||||||
runtime.execute(`
|
|
||||||
const order = await tools.orders.lookup({ id: "order_42" })
|
const order = await tools.orders.lookup({ id: "order_42" })
|
||||||
return { id: order.id, needsAttention: order.status !== "complete" }
|
return { id: order.id, needsAttention: order.status !== "complete" }
|
||||||
`)
|
`)
|
||||||
@@ -91,9 +89,7 @@ The description and schemas are part of the model-visible tool contract. Keep de
|
|||||||
Use `CodeMode.execute` for a single execution:
|
Use `CodeMode.execute` for a single execution:
|
||||||
|
|
||||||
```ts
|
```ts
|
||||||
const result =
|
const result = yield* CodeMode.execute({
|
||||||
yield *
|
|
||||||
CodeMode.execute({
|
|
||||||
tools: { orders: { lookup: lookupOrder } },
|
tools: { orders: { lookup: lookupOrder } },
|
||||||
code: `return await tools.orders.lookup({ id: "order_42" })`,
|
code: `return await tools.orders.lookup({ id: "order_42" })`,
|
||||||
limits: { maxToolCalls: 10 },
|
limits: { maxToolCalls: 10 },
|
||||||
@@ -227,7 +223,7 @@ CodeMode is an orchestration language, not a general JavaScript runtime.
|
|||||||
The limits are exactly three knobs:
|
The limits are exactly three knobs:
|
||||||
|
|
||||||
| Limit | Default | Bounds |
|
| Limit | Default | Bounds |
|
||||||
| ---------------- | -------------------: | -------------------------------------------------------------------- |
|
| --- | ---: | --- |
|
||||||
| `timeoutMs` | none - no timeout | Wall-clock execution time. |
|
| `timeoutMs` | none - no timeout | Wall-clock execution time. |
|
||||||
| `maxToolCalls` | none - unlimited | Tool calls admitted during the execution. |
|
| `maxToolCalls` | none - unlimited | Tool calls admitted during the execution. |
|
||||||
| `maxOutputBytes` | none - no truncation | Model-facing output: the serialized result value plus captured logs. |
|
| `maxOutputBytes` | none - no truncation | Model-facing output: the serialized result value plus captured logs. |
|
||||||
@@ -259,7 +255,7 @@ Two interpreter internals are fixed constants rather than knobs: at most 8 tool
|
|||||||
Failures are data:
|
Failures are data:
|
||||||
|
|
||||||
| Kind | Meaning |
|
| Kind | Meaning |
|
||||||
| ----------------------- | -------------------------------------------------------------------------------------------------------- |
|
| --- | --- |
|
||||||
| `ParseError` | Source is empty or cannot be parsed. |
|
| `ParseError` | Source is empty or cannot be parsed. |
|
||||||
| `UnsupportedSyntax` | Parsed JavaScript is outside the supported subset. |
|
| `UnsupportedSyntax` | Parsed JavaScript is outside the supported subset. |
|
||||||
| `UnknownTool` | A program referenced a tool the host did not provide. |
|
| `UnknownTool` | A program referenced a tool the host did not provide. |
|
||||||
@@ -276,7 +272,10 @@ Unknown host failures, defects, invalid outputs, and copying failures are saniti
|
|||||||
```ts
|
```ts
|
||||||
import { toolError } from "@opencode-ai/codemode"
|
import { toolError } from "@opencode-ai/codemode"
|
||||||
|
|
||||||
run: ({ id }) => (authorized(id) ? loadOrder(id) : Effect.fail(toolError("Order is unavailable")))
|
run: ({ id }) =>
|
||||||
|
authorized(id)
|
||||||
|
? loadOrder(id)
|
||||||
|
: Effect.fail(toolError("Order is unavailable"))
|
||||||
```
|
```
|
||||||
|
|
||||||
Only the supplied message is model-visible. The optional cause is never returned in `ExecuteResult`; hosts should perform any required internal logging before crossing this boundary.
|
Only the supplied message is model-visible. The optional cause is never returned in `ExecuteResult`; hosts should perform any required internal logging before crossing this boundary.
|
||||||
|
|||||||
@@ -39,7 +39,6 @@ package and was **deleted** in Wave 3 (done, see below).
|
|||||||
From issue #34787 and design discussion. Do not relitigate these casually.
|
From issue #34787 and design discussion. Do not relitigate these casually.
|
||||||
|
|
||||||
### Core direction
|
### Core direction
|
||||||
|
|
||||||
- Generic CodeMode lives in its own package: `@opencode-ai/codemode` (repo scope convention;
|
- Generic CodeMode lives in its own package: `@opencode-ai/codemode` (repo scope convention;
|
||||||
the issue's `@opencode/codemode` name was normalized to the `@opencode-ai/*` convention).
|
the issue's `@opencode/codemode` name was normalized to the `@opencode-ai/*` convention).
|
||||||
- **Keep the hand-rolled interpreter.** No QuickJS/V8/sandbox-engine dependency. We own and
|
- **Keep the hand-rolled interpreter.** No QuickJS/V8/sandbox-engine dependency. We own and
|
||||||
@@ -53,7 +52,6 @@ From issue #34787 and design discussion. Do not relitigate these casually.
|
|||||||
products/blog posts) in code, comments, commit messages, or docs in this repo.
|
products/blog posts) in code, comments, commit messages, or docs in this repo.
|
||||||
|
|
||||||
### MCP / tools
|
### MCP / tools
|
||||||
|
|
||||||
- The MCP adapter lives in OpenCode, not here. It converts MCP definitions into ordinary
|
- The MCP adapter lives in OpenCode, not here. It converts MCP definitions into ordinary
|
||||||
`Tool.make(...)` definitions and hands CodeMode a plain tool tree.
|
`Tool.make(...)` definitions and hands CodeMode a plain tool tree.
|
||||||
- Permissions stay in the OpenCode adapter (each tool's `run` wraps the permission ask).
|
- Permissions stay in the OpenCode adapter (each tool's `run` wraps the permission ask).
|
||||||
@@ -63,7 +61,6 @@ From issue #34787 and design discussion. Do not relitigate these casually.
|
|||||||
`tools.<server>.<tool>` namespaces before handing them over.
|
`tools.<server>.<tool>` namespaces before handing them over.
|
||||||
|
|
||||||
### Discovery / search
|
### Discovery / search
|
||||||
|
|
||||||
- **Search only - no separate `describe`.** `tools.$codemode.search({ query?, namespace?,
|
- **Search only - no separate `describe`.** `tools.$codemode.search({ query?, namespace?,
|
||||||
limit? })` over the final tool tree, owned by this package.
|
limit? })` over the final tool tree, owned by this package.
|
||||||
- Search result item shape: `{ path, description, signature }` in an `{ items, total }`
|
- Search result item shape: `{ path, description, signature }` in an `{ items, total }`
|
||||||
@@ -85,7 +82,6 @@ limit? })` over the final tool tree, owned by this package.
|
|||||||
- Tools without an output schema render `unknown` as their return type.
|
- Tools without an output schema render `unknown` as their return type.
|
||||||
|
|
||||||
### Schemas / Tool.make
|
### Schemas / Tool.make
|
||||||
|
|
||||||
- `Tool.make` carries rich metadata so search can render real signatures.
|
- `Tool.make` carries rich metadata so search can render real signatures.
|
||||||
- Support **Effect Schema** (first-class, validating) and **JSON Schema** (initially
|
- Support **Effect Schema** (first-class, validating) and **JSON Schema** (initially
|
||||||
render-only - used for TypeScript rendering; the adapter may validate on its own). Leave
|
render-only - used for TypeScript rendering; the adapter may validate on its own). Leave
|
||||||
@@ -94,7 +90,6 @@ limit? })` over the final tool tree, owned by this package.
|
|||||||
normalization for plugin authors can come later.
|
normalization for plugin authors can come later.
|
||||||
|
|
||||||
### Attachments / output
|
### Attachments / output
|
||||||
|
|
||||||
- **No `output.text/file/image` API in v1.** (Deleted in Wave 2.)
|
- **No `output.text/file/image` API in v1.** (Deleted in Wave 2.)
|
||||||
- Tool calls return native structured payloads into the sandbox. Files/images emitted by
|
- Tool calls return native structured payloads into the sandbox. Files/images emitted by
|
||||||
child tools **never enter the sandbox** - the OpenCode adapter strips and accumulates them
|
child tools **never enter the sandbox** - the OpenCode adapter strips and accumulates them
|
||||||
@@ -105,7 +100,6 @@ limit? })` over the final tool tree, owned by this package.
|
|||||||
image bytes into context or drop attachments.
|
image bytes into context or drop attachments.
|
||||||
|
|
||||||
### Runtime behavior
|
### Runtime behavior
|
||||||
|
|
||||||
- Limits are EXACTLY the three public knobs: `{ timeoutMs, maxToolCalls, maxOutputBytes }` -
|
- Limits are EXACTLY the three public knobs: `{ timeoutMs, maxToolCalls, maxOutputBytes }` -
|
||||||
matching the original locked spec exactly. NO limit has a default (user direction, Fix 6
|
matching the original locked spec exactly. NO limit has a default (user direction, Fix 6
|
||||||
for the first two; extended to `maxOutputBytes` in the truncation-layering fix below):
|
for the first two; extended to `maxOutputBytes` in the truncation-layering fix below):
|
||||||
@@ -154,7 +148,6 @@ and `bun run typecheck`; from `packages/opencode`, `bun run typecheck` and
|
|||||||
`test/tool/registry.test.ts`).
|
`test/tool/registry.test.ts`).
|
||||||
|
|
||||||
### Wave 0 - scaffold (done)
|
### Wave 0 - scaffold (done)
|
||||||
|
|
||||||
- `packages/codemode` created from the experiments implementation: `src/{index,codemode,tool,
|
- `packages/codemode` created from the experiments implementation: `src/{index,codemode,tool,
|
||||||
tool-error,tool-runtime}.ts`, README, AGENTS.md, tests.
|
tool-error,tool-runtime}.ts`, README, AGENTS.md, tests.
|
||||||
- `package.json`: name `@opencode-ai/codemode`, deps `acorn@8.15.0`, `typescript: catalog:`,
|
- `package.json`: name `@opencode-ai/codemode`, deps `acorn@8.15.0`, `typescript: catalog:`,
|
||||||
@@ -164,7 +157,6 @@ tool-error,tool-runtime}.ts`, README, AGENTS.md, tests.
|
|||||||
Context.Service key string renamed to `@opencode-ai/codemode/CurrentToolCall`.
|
Context.Service key string renamed to `@opencode-ai/codemode/CurrentToolCall`.
|
||||||
|
|
||||||
### Wave 1a - forgiving JS semantics (done)
|
### Wave 1a - forgiving JS semantics (done)
|
||||||
|
|
||||||
Ported from the old opencode rune work; `test/parity.test.ts` (24 tests) is the acceptance
|
Ported from the old opencode rune work; `test/parity.test.ts` (24 tests) is the acceptance
|
||||||
spec. The seeded interpreter was deliberately strict; these behaviors replaced that:
|
spec. The seeded interpreter was deliberately strict; these behaviors replaced that:
|
||||||
|
|
||||||
@@ -181,7 +173,6 @@ spec. The seeded interpreter was deliberately strict; these behaviors replaced t
|
|||||||
null/undefined still throws (real JS throws too).
|
null/undefined still throws (real JS throws too).
|
||||||
|
|
||||||
### Wave 1b-i - stdlib value types: Date, RegExp, Map, Set (done)
|
### Wave 1b-i - stdlib value types: Date, RegExp, Map, Set (done)
|
||||||
|
|
||||||
`src/values.ts` holds `SandboxDate/SandboxRegExp/SandboxMap/SandboxSet` (own module so both
|
`src/values.ts` holds `SandboxDate/SandboxRegExp/SandboxMap/SandboxSet` (own module so both
|
||||||
`codemode.ts` and `tool-runtime.ts` import without a cycle). Design:
|
`codemode.ts` and `tool-runtime.ts` import without a cycle). Design:
|
||||||
|
|
||||||
@@ -211,14 +202,13 @@ spec. The seeded interpreter was deliberately strict; these behaviors replaced t
|
|||||||
interpolation renders `/regex/` and ISO dates directly.
|
interpolation renders `/regex/` and ISO dates directly.
|
||||||
|
|
||||||
### Wave 2 - API layer (done)
|
### Wave 2 - API layer (done)
|
||||||
|
|
||||||
The package's public contract, reshaped for the Wave 3 adapter. 101 tests / 0 fail after this
|
The package's public contract, reshaped for the Wave 3 adapter. 101 tests / 0 fail after this
|
||||||
wave; both packages typecheck clean.
|
wave; both packages typecheck clean.
|
||||||
|
|
||||||
- **`Tool.make` schema flexibility** (`src/tool.ts`): `input`/`output` each accept an Effect
|
- **`Tool.make` schema flexibility** (`src/tool.ts`): `input`/`output` each accept an Effect
|
||||||
Schema (validating, decoded both directions as before) OR a raw JSON Schema document
|
Schema (validating, decoded both directions as before) OR a raw JSON Schema document
|
||||||
(render-only - no validation, values pass through; rendering handles `$defs`/`definitions`
|
(render-only - no validation, values pass through; rendering handles `$defs`/`definitions`
|
||||||
- `$ref`). `output` is **optional** -> signature renders `Promise<unknown>` and the host
|
+ `$ref`). `output` is **optional** -> signature renders `Promise<unknown>` and the host
|
||||||
result is exposed as-is. Discrimination via `Schema.isSchema`. New helpers exported from
|
result is exposed as-is. Discrimination via `Schema.isSchema`. New helpers exported from
|
||||||
`tool.ts`: `inputTypeScript`/`outputTypeScript`/`decodeInput`/`decodeOutput`/
|
`tool.ts`: `inputTypeScript`/`outputTypeScript`/`decodeInput`/`decodeOutput`/
|
||||||
`jsonSchemaToTypeScript`; `tool-runtime.ts` consumes them (no direct `Schema.*` use there
|
`jsonSchemaToTypeScript`; `tool-runtime.ts` consumes them (no direct `Schema.*` use there
|
||||||
@@ -245,7 +235,7 @@ maxOutputBytes? }` (defaults 10_000 / 100 / 32_000). This wave kept the other kn
|
|||||||
a final `Effect.map` over every result path (success/timeout/normalized failure). Oversized
|
a final `Effect.map` over every result path (success/timeout/normalized failure). Oversized
|
||||||
serialized values become truncated text + ` [result truncated: N bytes exceeds the M-byte
|
serialized values become truncated text + ` [result truncated: N bytes exceeds the M-byte
|
||||||
output limit; return a smaller value]`; logs keep leading lines within the remaining budget
|
output limit; return a smaller value]`; logs keep leading lines within the remaining budget
|
||||||
- `[logs truncated: showing K of N lines]`; result gains `truncated: true` (also added to
|
+ `[logs truncated: showing K of N lines]`; result gains `truncated: true` (also added to
|
||||||
`ExecuteResultSchema`). UTF-8-safe truncation (no split code points). (The in-sandbox
|
`ExecuteResultSchema`). UTF-8-safe truncation (no split code points). (The in-sandbox
|
||||||
`maxDataBytes` check that used to throw first on oversized raw values died in Fix 5 -
|
`maxDataBytes` check that used to throw first on oversized raw values died in Fix 5 -
|
||||||
truncation is now the only result-size mechanism.)
|
truncation is now the only result-size mechanism.)
|
||||||
@@ -254,7 +244,6 @@ output limit; return a smaller value]`; logs keep leading lines within the remai
|
|||||||
(`total: 1`), bypassing ranking. Tokenization/ranking/shape unchanged.
|
(`total: 1`), bypassing ranking. Tokenization/ranking/shape unchanged.
|
||||||
|
|
||||||
### Wave 3 - OpenCode MCP adapter (done)
|
### Wave 3 - OpenCode MCP adapter (done)
|
||||||
|
|
||||||
`packages/opencode/src/session/code-mode.ts` rewritten as a thin adapter over this package;
|
`packages/opencode/src/session/code-mode.ts` rewritten as a thin adapter over this package;
|
||||||
the vendored rune interpreter is gone. Same `define(mcpTools, mcpDefs, servers)` signature, so
|
the vendored rune interpreter is gone. Same `define(mcpTools, mcpDefs, servers)` signature, so
|
||||||
`tools.ts` gating (flag on + MCP tools exist -> single `execute` tool, early-return suppresses
|
`tools.ts` gating (flag on + MCP tools exist -> single `execute` tool, early-return suppresses
|
||||||
@@ -307,7 +296,6 @@ error - logs are plain pre-formatted lines now), attachments: accumulated }` thr
|
|||||||
describe/`renderType`/`rankTools` tests died with the old design (58+17+24 -> 34+16).
|
describe/`renderType`/`rankTools` tests died with the old design (58+17+24 -> 34+16).
|
||||||
|
|
||||||
### Wave 4 - instructions/prompting + polish (done)
|
### Wave 4 - instructions/prompting + polish (done)
|
||||||
|
|
||||||
Instructions are now the budgeted-catalog + prompting-guidance form; verified e2e against a
|
Instructions are now the budgeted-catalog + prompting-guidance form; verified e2e against a
|
||||||
real MCP config. Package still 101 tests / 0 fail; opencode adapter suites still 34 + 16; both
|
real MCP config. Package still 101 tests / 0 fail; opencode adapter suites still 34 + 16; both
|
||||||
packages typecheck clean.
|
packages typecheck clean.
|
||||||
@@ -333,7 +321,7 @@ packages typecheck clean.
|
|||||||
exported); `CodeMode.execute` (one-shot) passes it too, preserving the
|
exported); `CodeMode.execute` (one-shot) passes it too, preserving the
|
||||||
`execute`==`make().execute` law. A speculative `tools.$codemode.search` call on a small
|
`execute`==`make().execute` law. A speculative `tools.$codemode.search` call on a small
|
||||||
catalog now succeeds instead of `UnknownTool`, and unknown-tool suggestions always point at
|
catalog now succeeds instead of `UnknownTool`, and unknown-tool suggestions always point at
|
||||||
search. Search is _advertised_ in the instructions only when the inlined list is PARTIAL,
|
search. Search is *advertised* in the instructions only when the inlined list is PARTIAL,
|
||||||
keeping small-catalog instructions tight.
|
keeping small-catalog instructions tight.
|
||||||
- **Prompting content** in `instructions()`, mapping 1:1 to the section 5 transcript failures:
|
- **Prompting content** in `instructions()`, mapping 1:1 to the section 5 transcript failures:
|
||||||
parse-string-results-as-JSON, return-small, console-for-intermediates, and
|
parse-string-results-as-JSON, return-small, console-for-intermediates, and
|
||||||
@@ -364,7 +352,6 @@ packages typecheck clean.
|
|||||||
images, output truncation.
|
images, output truncation.
|
||||||
|
|
||||||
### Wave 5 - Promise generalization (done)
|
### Wave 5 - Promise generalization (done)
|
||||||
|
|
||||||
First-class promise values in the interpreter; the direct-tool-call-only `Promise.all`
|
First-class promise values in the interpreter; the direct-tool-call-only `Promise.all`
|
||||||
restriction (and its bespoke AST checks) is gone. Package suite is 136 tests / 0 fail (35 new
|
restriction (and its bespoke AST checks) is gone. Package suite is 136 tests / 0 fail (35 new
|
||||||
in `test/promise.test.ts`); adapter suites and both typechecks unchanged/green; the opencode
|
in `test/promise.test.ts`); adapter suites and both typechecks unchanged/green; the opencode
|
||||||
@@ -539,13 +526,12 @@ adapter needed **no changes**.
|
|||||||
|
|
||||||
**Fix 4 - token-budgeted catalog (was bytes)** (user direction: signatures need a token
|
**Fix 4 - token-budgeted catalog (was bytes)** (user direction: signatures need a token
|
||||||
budget; namespaces must always be present):
|
budget; namespaces must always be present):
|
||||||
|
|
||||||
- `src/token.ts` added: copy of `@opencode-ai/core/util/token` (`round(chars / 4)`), so
|
- `src/token.ts` added: copy of `@opencode-ai/core/util/token` (`round(chars / 4)`), so
|
||||||
the package stays dependency-free; keep in sync if the core heuristic changes.
|
the package stays dependency-free; keep in sync if the core heuristic changes.
|
||||||
- `DiscoveryOptions.maxInlineCatalogBytes` -> `maxInlineCatalogTokens` (default 4,000
|
- `DiscoveryOptions.maxInlineCatalogBytes` -> `maxInlineCatalogTokens` (default 4,000
|
||||||
estimated tokens ~ the old 16,000 bytes at 4 chars/token - behavior parity, not a size
|
estimated tokens ~ the old 16,000 bytes at 4 chars/token - behavior parity, not a size
|
||||||
reduction). `discoveryPlan` charges `estimate(catalogLine(tool))` per line; cheapest-first
|
reduction). `discoveryPlan` charges `estimate(catalogLine(tool))` per line; cheapest-first
|
||||||
- stop-on-first-miss unchanged at the time (stop-on-first-miss replaced by round-robin in
|
+ stop-on-first-miss unchanged at the time (stop-on-first-miss replaced by round-robin in
|
||||||
Fix 8). Namespace stub lines were and remain unbudgeted - every
|
Fix 8). Namespace stub lines were and remain unbudgeted - every
|
||||||
namespace always appears with its tool count, even at budget 0 (asserted in package and
|
namespace always appears with its tool count, even at budget 0 (asserted in package and
|
||||||
adapter tests).
|
adapter tests).
|
||||||
@@ -557,7 +543,6 @@ budget; namespaces must always be present):
|
|||||||
|
|
||||||
**Fix 5 - internal limits removed** (user direction: only the three PUBLIC limits survive as
|
**Fix 5 - internal limits removed** (user direction: only the three PUBLIC limits survive as
|
||||||
configurable knobs; the internal limit system dies):
|
configurable knobs; the internal limit system dies):
|
||||||
|
|
||||||
- `ExecutionLimits` (`timeoutMs` 10_000 / `maxToolCalls` 100 / `maxOutputBytes` 32_000 at
|
- `ExecutionLimits` (`timeoutMs` 10_000 / `maxToolCalls` 100 / `maxOutputBytes` 32_000 at
|
||||||
the time; Fix 6 later removed the first two defaults. Same validation: safe integers,
|
the time; Fix 6 later removed the first two defaults. Same validation: safe integers,
|
||||||
timeoutMs >= 1, others >= 0, RangeError otherwise) is now
|
timeoutMs >= 1, others >= 0, RangeError otherwise) is now
|
||||||
@@ -654,7 +639,6 @@ adapter suites: 34 + 16.
|
|||||||
**Fix 8 - condensed instructions + round-robin catalog fairness + plural-aware search**
|
**Fix 8 - condensed instructions + round-robin catalog fairness + plural-aware search**
|
||||||
(user direction: the fixed instruction prose was too verbose; two discovery fixes ride
|
(user direction: the fixed instruction prose was too verbose; two discovery fixes ride
|
||||||
along). All in `tool-runtime.ts`; no interpreter changes.
|
along). All in `tool-runtime.ts`; no interpreter changes.
|
||||||
|
|
||||||
- **Syntax section inverted**: the three dense allowlist lines (~453 estimated tokens)
|
- **Syntax section inverted**: the three dense allowlist lines (~453 estimated tokens)
|
||||||
are replaced by four short lines (~188) built on "models already know JavaScript; name
|
are replaced by four short lines (~188) built on "models already know JavaScript; name
|
||||||
only what is unusual or missing": (1) standard modern JS works - functions/closures,
|
only what is unusual or missing": (1) standard modern JS works - functions/closures,
|
||||||
@@ -676,7 +660,7 @@ along). All in `tool-runtime.ts`; no interpreter changes.
|
|||||||
return-small content now lives ONLY in the numbered Workflow steps (with their
|
return-small content now lives ONLY in the numbered Workflow steps (with their
|
||||||
compliance-driving justifications inline: "most tools return JSON as a string", "raw
|
compliance-driving justifications inline: "most tools return JSON as a string", "raw
|
||||||
payloads get truncated and waste context"); Rules keeps only bullets adding new
|
payloads get truncated and waste context"); Rules keeps only bullets adding new
|
||||||
content - filter/aggregate collections in code, console.\* intermediates (logs ride
|
content - filter/aggregate collections in code, console.* intermediates (logs ride
|
||||||
back), Promise.all parallelism, Object.keys/for...in enumeration, browse-namespace
|
back), Promise.all parallelism, Object.keys/for...in enumeration, browse-namespace
|
||||||
(PARTIAL only), and the media rule compressed to one line. The no-.then/.catch
|
(PARTIAL only), and the media rule compressed to one line. The no-.then/.catch
|
||||||
guidance moved to the Syntax not-supported line. Content upgrades: the PARTIAL search
|
guidance moved to the Syntax not-supported line. Content upgrades: the PARTIAL search
|
||||||
@@ -723,7 +707,6 @@ along). All in `tool-runtime.ts`; no interpreter changes.
|
|||||||
|
|
||||||
**Fix 9 - prompting trims per user review of Fix 8** (user reviewed the condensed
|
**Fix 9 - prompting trims per user review of Fix 8** (user reviewed the condensed
|
||||||
instructions and directed further cuts):
|
instructions and directed further cuts):
|
||||||
|
|
||||||
- Default `maxInlineCatalogTokens` 4,000 -> **2,000** (user wants ~2k tokens of signatures
|
- Default `maxInlineCatalogTokens` 4,000 -> **2,000** (user wants ~2k tokens of signatures
|
||||||
auto-inlined; round-robin fairness from Fix 8 spreads it across all namespaces).
|
auto-inlined; round-robin fairness from Fix 8 spreads it across all namespaces).
|
||||||
- Console rule and files/images rule DROPPED from `## Rules`. Replaced by a single
|
- Console rule and files/images rule DROPPED from `## Rules`. Replaced by a single
|
||||||
@@ -742,7 +725,6 @@ instructions and directed further cuts):
|
|||||||
|
|
||||||
**DSL-expansion pass - interpreter-surface batch from section 4** (the deferred medium-tier JS
|
**DSL-expansion pass - interpreter-surface batch from section 4** (the deferred medium-tier JS
|
||||||
parity items, done as one focused pass; no public API or limit changes):
|
parity items, done as one focused pass; no public API or limit changes):
|
||||||
|
|
||||||
- **`instanceof` + real Error values**: the `errorConstructors` names (`Error`,
|
- **`instanceof` + real Error values**: the `errorConstructors` names (`Error`,
|
||||||
`TypeError`, `RangeError`, `SyntaxError`, `ReferenceError`, `EvalError`, `URIError`) are
|
`TypeError`, `RangeError`, `SyntaxError`, `ReferenceError`, `EvalError`, `URIError`) are
|
||||||
bound globals (`ErrorConstructorReference`, callable with or without `new`; `typeof` ->
|
bound globals (`ErrorConstructorReference`, callable with or without `new`; `typeof` ->
|
||||||
@@ -830,7 +812,6 @@ Error`/splice mentions (nothing else reworded); README updated (checkpoint
|
|||||||
|
|
||||||
**Truncation layering - CodeMode truncation off in OpenCode** (user direction; resolves the
|
**Truncation layering - CodeMode truncation off in OpenCode** (user direction; resolves the
|
||||||
section 4 outer-truncation item the OPPOSITE way from "kill the outer one"):
|
section 4 outer-truncation item the OPPOSITE way from "kill the outer one"):
|
||||||
|
|
||||||
- `maxOutputBytes` lost its 32,000 default and now behaves exactly like the other two
|
- `maxOutputBytes` lost its 32,000 default and now behaves exactly like the other two
|
||||||
limits: absent = no truncation. All three limits are uniformly no-default - budgets are
|
limits: absent = no truncation. All three limits are uniformly no-default - budgets are
|
||||||
host policy. `ResolvedExecutionLimits.maxOutputBytes` is `number | undefined`;
|
host policy. `ResolvedExecutionLimits.maxOutputBytes` is `number | undefined`;
|
||||||
@@ -856,7 +837,6 @@ regular dependency; hosts depend on it themselves because the API surface is Eff
|
|||||||
|
|
||||||
**Registry promotion + permission-aware catalog** (the "promote to a proper tool service"
|
**Registry promotion + permission-aware catalog** (the "promote to a proper tool service"
|
||||||
restructure; fixes the section 4 permission-advertising bug):
|
restructure; fixes the section 4 permission-advertising bug):
|
||||||
|
|
||||||
- **The adapter moved** `src/session/code-mode.ts` -> `src/tool/code-mode.ts` and is now a
|
- **The adapter moved** `src/session/code-mode.ts` -> `src/tool/code-mode.ts` and is now a
|
||||||
registry-resident tool service on the TaskTool precedent: `CodeModeTool =
|
registry-resident tool service on the TaskTool precedent: `CodeModeTool =
|
||||||
Tool.define(CODE_MODE_TOOL, ...)` whose init depends on `MCP.Service`, `Agent.Service`,
|
Tool.define(CODE_MODE_TOOL, ...)` whose init depends on `MCP.Service`, `Agent.Service`,
|
||||||
@@ -906,7 +886,6 @@ registry.test.ts` gained four registry-level tests: registered with flag+MCP too
|
|||||||
|
|
||||||
**Shared MCP invocation middle (`McpInvoke.invoke`)** (closes the section 4 "plugin hooks skip
|
**Shared MCP invocation middle (`McpInvoke.invoke`)** (closes the section 4 "plugin hooks skip
|
||||||
child calls" gap):
|
child calls" gap):
|
||||||
|
|
||||||
- `packages/opencode/src/mcp/invoke.ts` extracts the duplicated "invoke an MCP tool"
|
- `packages/opencode/src/mcp/invoke.ts` extracts the duplicated "invoke an MCP tool"
|
||||||
middle into one shared `McpInvoke.invoke(input)`: plugin `tool.execute.before` hook ->
|
middle into one shared `McpInvoke.invoke(input)`: plugin `tool.execute.before` hook ->
|
||||||
permission ask (`{ permission: key, patterns: ["*"], always: ["*"] }` via the caller's
|
permission ask (`{ permission: key, patterns: ["*"], always: ["*"] }` via the caller's
|
||||||
@@ -948,7 +927,6 @@ child calls" gap):
|
|||||||
|
|
||||||
**Signature rendering + compound-assignment parity fixes** (externally reported, both
|
**Signature rendering + compound-assignment parity fixes** (externally reported, both
|
||||||
verified real with failing tests before fixing):
|
verified real with failing tests before fixing):
|
||||||
|
|
||||||
- **Non-identifier property names in rendered signatures** (`src/tool.ts`): `renderSchema`
|
- **Non-identifier property names in rendered signatures** (`src/tool.ts`): `renderSchema`
|
||||||
emitted raw property names, so schema properties like `foo-bar`/`@type`/`x.y`/`123`
|
emitted raw property names, so schema properties like `foo-bar`/`@type`/`x.y`/`123`
|
||||||
rendered invalid TypeScript (`{ foo-bar?: string }`). Fixed with a `renderKey` helper -
|
rendered invalid TypeScript (`{ foo-bar?: string }`). Fixed with a `renderKey` helper -
|
||||||
@@ -983,10 +961,8 @@ verified real with failing tests before fixing):
|
|||||||
## 4. Remaining work (detailed TODO)
|
## 4. Remaining work (detailed TODO)
|
||||||
|
|
||||||
### Next DSL-expansion pass (done - see the DSL-expansion pass entry in section 3)
|
### Next DSL-expansion pass (done - see the DSL-expansion pass entry in section 3)
|
||||||
|
|
||||||
Batch these together - per user direction: important, but deliberately deferred to one
|
Batch these together - per user direction: important, but deliberately deferred to one
|
||||||
focused interpreter-surface pass rather than picked off piecemeal.
|
focused interpreter-surface pass rather than picked off piecemeal.
|
||||||
|
|
||||||
- [x] Medium-tier JS parity items deferred from the original audit: caught errors are plain
|
- [x] Medium-tier JS parity items deferred from the original audit: caught errors are plain
|
||||||
`{ name, message }` objects, not `instanceof Error` (and `Error` isn't a value -
|
`{ name, message }` objects, not `instanceof Error` (and `Error` isn't a value -
|
||||||
`x instanceof Error` is unsupported syntax); `splice` (still a
|
`x instanceof Error` is unsupported syntax); `splice` (still a
|
||||||
@@ -1005,7 +981,6 @@ focused interpreter-surface pass rather than picked off piecemeal.
|
|||||||
(`console.log({ m: map })`) - could deep-format instead.
|
(`console.log({ m: map })`) - could deep-format instead.
|
||||||
|
|
||||||
### Next iteration: text-result handling (deliberate follow-up, user-directed)
|
### Next iteration: text-result handling (deliberate follow-up, user-directed)
|
||||||
|
|
||||||
- [ ] Revisit how MCP text results reach the program. Today: `structuredContent` when the
|
- [ ] Revisit how MCP text results reach the program. Today: `structuredContent` when the
|
||||||
server sends it, else joined text as a plain string (the program JSON.parses it,
|
server sends it, else joined text as a plain string (the program JSON.parses it,
|
||||||
guided by a workflow step). Considered and deferred: (a) conservative boundary
|
guided by a workflow step). Considered and deferred: (a) conservative boundary
|
||||||
@@ -1017,7 +992,6 @@ focused interpreter-surface pass rather than picked off piecemeal.
|
|||||||
revisit once real usage shows which failure modes matter.
|
revisit once real usage shows which failure modes matter.
|
||||||
|
|
||||||
### Next iteration: stdlib surface (prioritized)
|
### Next iteration: stdlib surface (prioritized)
|
||||||
|
|
||||||
Current instructions say "usual Array/String/Object/Math/JSON methods," but the interpreter is
|
Current instructions say "usual Array/String/Object/Math/JSON methods," but the interpreter is
|
||||||
intentionally a subset. Keep CodeMode focused on orchestration and data shaping, not a full host
|
intentionally a subset. Keep CodeMode focused on orchestration and data shaping, not a full host
|
||||||
runtime, but close the high-friction gaps models are likely to reach for.
|
runtime, but close the high-friction gaps models are likely to reach for.
|
||||||
@@ -1054,9 +1028,7 @@ Explicit non-goals for now: `structuredClone`, `WeakMap`/`WeakSet`, and timers
|
|||||||
orchestration use case.
|
orchestration use case.
|
||||||
|
|
||||||
### Wiring-review findings (subagent code review of the OpenCode integration, triaged)
|
### Wiring-review findings (subagent code review of the OpenCode integration, triaged)
|
||||||
|
|
||||||
Pre-PR fixes (user-approved cut):
|
Pre-PR fixes (user-approved cut):
|
||||||
|
|
||||||
- [x] **Cancellation does not interrupt the interpreter** - the no-limits rationale claimed
|
- [x] **Cancellation does not interrupt the interpreter** - the no-limits rationale claimed
|
||||||
"user cancel interrupts the execution fiber," but `tools.ts` runs tools via
|
"user cancel interrupts the execution fiber," but `tools.ts` runs tools via
|
||||||
`run.promise` -> `Effect.runPromise` (`effect/bridge.ts:64-66`) with NO abort wiring;
|
`run.promise` -> `Effect.runPromise` (`effect/bridge.ts:64-66`) with NO abort wiring;
|
||||||
@@ -1101,7 +1073,6 @@ Pre-PR fixes (user-approved cut):
|
|||||||
`title: "execute"` sites in `code-mode.ts` now reference `CODE_MODE_TOOL`.
|
`title: "execute"` sites in `code-mode.ts` now reference `CODE_MODE_TOOL`.
|
||||||
|
|
||||||
Post-MVP (logged, not blocking an experimental flag):
|
Post-MVP (logged, not blocking an experimental flag):
|
||||||
|
|
||||||
- [x] **Plugin `tool.execute.before/after` hooks skip child calls** - legacy MCP
|
- [x] **Plugin `tool.execute.before/after` hooks skip child calls** - legacy MCP
|
||||||
registration fires them per tool (`tools.ts:419-441`); under code mode only the
|
registration fires them per tool (`tools.ts:419-441`); under code mode only the
|
||||||
outer `execute` fires them, so auditing/intercepting plugins silently lose MCP
|
outer `execute` fires them, so auditing/intercepting plugins silently lose MCP
|
||||||
@@ -1134,7 +1105,6 @@ Post-MVP (logged, not blocking an experimental flag):
|
|||||||
names that are no longer directly callable under code mode.
|
names that are no longer directly callable under code mode.
|
||||||
|
|
||||||
### Backlog / loose ends (non-blocking, any order)
|
### Backlog / loose ends (non-blocking, any order)
|
||||||
|
|
||||||
- [ ] `evaluateUpdateExpression` (`++`/`--`) still uses raw `Number(current)`, so `d++` on a
|
- [ ] `evaluateUpdateExpression` (`++`/`--`) still uses raw `Number(current)`, so `d++` on a
|
||||||
sandbox Date yields `NaN` where `d += 1` now uses epoch semantics (and real JS `d++`
|
sandbox Date yields `NaN` where `d += 1` now uses epoch semantics (and real JS `d++`
|
||||||
would give epoch+0 numeric). Pre-existing, out of scope of the compound-assignment
|
would give epoch+0 numeric). Pre-existing, out of scope of the compound-assignment
|
||||||
@@ -1171,7 +1141,7 @@ Post-MVP (logged, not blocking an experimental flag):
|
|||||||
## 5. Context and gotchas for whoever picks this up
|
## 5. Context and gotchas for whoever picks this up
|
||||||
|
|
||||||
- **Motivating failure (why forgiving semantics + prompting matter):** in a real transcript,
|
- **Motivating failure (why forgiving semantics + prompting matter):** in a real transcript,
|
||||||
the model wrote `me.result?.login ?? me.result` where the tool result was a JSON _string_ -
|
the model wrote `me.result?.login ?? me.result` where the tool result was a JSON *string* -
|
||||||
the old strict interpreter threw (`String property 'login' is not available`); then the
|
the old strict interpreter threw (`String property 'login' is not available`); then the
|
||||||
model returned a raw 105KB payload, which native truncation dumped to a file, costing a
|
model returned a raw 105KB payload, which native truncation dumped to a file, costing a
|
||||||
subagent round-trip to extract one number. Interpreter forgiveness stops the crashes;
|
subagent round-trip to extract one number. Interpreter forgiveness stops the crashes;
|
||||||
|
|||||||
+283
-824
File diff suppressed because it is too large
Load Diff
@@ -1,4 +1,10 @@
|
|||||||
export { ToolError, CodeMode, ExecuteInputSchema, ExecuteResultSchema, toolError } from "./codemode.js"
|
export {
|
||||||
|
ToolError,
|
||||||
|
CodeMode,
|
||||||
|
ExecuteInputSchema,
|
||||||
|
ExecuteResultSchema,
|
||||||
|
toolError,
|
||||||
|
} from "./codemode.js"
|
||||||
export { Tool } from "./tool.js"
|
export { Tool } from "./tool.js"
|
||||||
export type { Definition as ToolDefinition, JsonSchema, ToolSchema } from "./tool.js"
|
export type { Definition as ToolDefinition, JsonSchema, ToolSchema } from "./tool.js"
|
||||||
export type { ToolCallEnded, ToolCallHooks } from "./tool-runtime.js"
|
export type { ToolCallEnded, ToolCallHooks } from "./tool-runtime.js"
|
||||||
|
|||||||
@@ -21,15 +21,10 @@ export type HostTools<R = never> = {
|
|||||||
|
|
||||||
export type Services<Tools> = Tools extends (...args: Array<unknown>) => Effect.Effect<unknown, unknown, infer R>
|
export type Services<Tools> = Tools extends (...args: Array<unknown>) => Effect.Effect<unknown, unknown, infer R>
|
||||||
? R
|
? R
|
||||||
: Tools extends {
|
: Tools extends { readonly _tag: "CodeModeTool"; readonly run: (input: unknown) => Effect.Effect<unknown, unknown, infer R> }
|
||||||
readonly _tag: "CodeModeTool"
|
|
||||||
readonly run: (input: unknown) => Effect.Effect<unknown, unknown, infer R>
|
|
||||||
}
|
|
||||||
? R
|
? R
|
||||||
: Tools extends object
|
: Tools extends object
|
||||||
? string extends keyof Tools
|
? string extends keyof Tools ? never : Services<Tools[keyof Tools]>
|
||||||
? never
|
|
||||||
: Services<Tools[keyof Tools]>
|
|
||||||
: never
|
: never
|
||||||
|
|
||||||
/** Minimal audit record retained for each admitted tool call. */
|
/** Minimal audit record retained for each admitted tool call. */
|
||||||
@@ -73,13 +68,11 @@ export type SafeObject = Record<string, unknown>
|
|||||||
const reservedNamespace = "$codemode"
|
const reservedNamespace = "$codemode"
|
||||||
const defaultMaxInlineCatalogTokens = 2_000
|
const defaultMaxInlineCatalogTokens = 2_000
|
||||||
const defaultSearchLimit = 10
|
const defaultSearchLimit = 10
|
||||||
const searchSignature =
|
const searchSignature = "tools.$codemode.search({ query?: string, namespace?: string, limit?: number }): Promise<{ items: Array<{ path: string; description: string; signature: string }>; total: number }>"
|
||||||
"tools.$codemode.search({ query?: string, namespace?: string, limit?: number }): Promise<{ items: Array<{ path: string; description: string; signature: string }>; total: number }>"
|
|
||||||
const toolExpression = (path: string) =>
|
const toolExpression = (path: string) =>
|
||||||
"tools" +
|
"tools" + path
|
||||||
path
|
|
||||||
.split(".")
|
.split(".")
|
||||||
.map((segment) => (identifierSegment.test(segment) ? `.${segment}` : `[${JSON.stringify(segment)}]`))
|
.map((segment) => identifierSegment.test(segment) ? `.${segment}` : `[${JSON.stringify(segment)}]`)
|
||||||
.join("")
|
.join("")
|
||||||
|
|
||||||
export class ToolReference {
|
export class ToolReference {
|
||||||
@@ -95,12 +88,7 @@ const MAX_VALUE_DEPTH = 32
|
|||||||
|
|
||||||
export class ToolRuntimeError extends Error {
|
export class ToolRuntimeError extends Error {
|
||||||
constructor(
|
constructor(
|
||||||
readonly kind:
|
readonly kind: "UnknownTool" | "InvalidToolInput" | "InvalidToolOutput" | "InvalidDataValue" | "ToolCallLimitExceeded",
|
||||||
| "UnknownTool"
|
|
||||||
| "InvalidToolInput"
|
|
||||||
| "InvalidToolOutput"
|
|
||||||
| "InvalidDataValue"
|
|
||||||
| "ToolCallLimitExceeded",
|
|
||||||
message: string,
|
message: string,
|
||||||
readonly suggestions: ReadonlyArray<string> = [],
|
readonly suggestions: ReadonlyArray<string> = [],
|
||||||
) {
|
) {
|
||||||
@@ -143,13 +131,7 @@ export const isBlockedMember = (name: string): boolean => blockedMemberNames.has
|
|||||||
export const copyIn = (value: unknown, label: string, preserveSandboxValues = false): unknown =>
|
export const copyIn = (value: unknown, label: string, preserveSandboxValues = false): unknown =>
|
||||||
copyBounded(value, label, 0, new Set(), preserveSandboxValues)
|
copyBounded(value, label, 0, new Set(), preserveSandboxValues)
|
||||||
|
|
||||||
const copyBounded = (
|
const copyBounded = (value: unknown, label: string, depth: number, seen: Set<object>, preserveSandboxValues: boolean): unknown => {
|
||||||
value: unknown,
|
|
||||||
label: string,
|
|
||||||
depth: number,
|
|
||||||
seen: Set<object>,
|
|
||||||
preserveSandboxValues: boolean,
|
|
||||||
): unknown => {
|
|
||||||
if (depth > MAX_VALUE_DEPTH) {
|
if (depth > MAX_VALUE_DEPTH) {
|
||||||
throw new ToolRuntimeError("InvalidDataValue", `${label} exceeds the maximum value depth of ${MAX_VALUE_DEPTH}.`)
|
throw new ToolRuntimeError("InvalidDataValue", `${label} exceeds the maximum value depth of ${MAX_VALUE_DEPTH}.`)
|
||||||
}
|
}
|
||||||
@@ -184,12 +166,7 @@ const copyBounded = (
|
|||||||
// Intra-sandbox checkpoints keep sandbox value instances alive as leaves; their contents
|
// Intra-sandbox checkpoints keep sandbox value instances alive as leaves; their contents
|
||||||
// are never walked here (Map/Set members are validated where mutation happens, and the
|
// are never walked here (Map/Set members are validated where mutation happens, and the
|
||||||
// real boundary still serializes them below).
|
// real boundary still serializes them below).
|
||||||
if (
|
if (value instanceof SandboxDate || value instanceof SandboxRegExp || value instanceof SandboxMap || value instanceof SandboxSet) {
|
||||||
value instanceof SandboxDate ||
|
|
||||||
value instanceof SandboxRegExp ||
|
|
||||||
value instanceof SandboxMap ||
|
|
||||||
value instanceof SandboxSet
|
|
||||||
) {
|
|
||||||
return value
|
return value
|
||||||
}
|
}
|
||||||
// Host instances cannot normally reach an intra-sandbox checkpoint (tool results cross
|
// Host instances cannot normally reach an intra-sandbox checkpoint (tool results cross
|
||||||
@@ -220,12 +197,8 @@ const copyBounded = (
|
|||||||
return Number.isFinite(value.getTime()) ? value.toISOString() : null
|
return Number.isFinite(value.getTime()) ? value.toISOString() : null
|
||||||
}
|
}
|
||||||
if (
|
if (
|
||||||
value instanceof SandboxRegExp ||
|
value instanceof SandboxRegExp || value instanceof SandboxMap || value instanceof SandboxSet ||
|
||||||
value instanceof SandboxMap ||
|
value instanceof RegExp || value instanceof Map || value instanceof Set
|
||||||
value instanceof SandboxSet ||
|
|
||||||
value instanceof RegExp ||
|
|
||||||
value instanceof Map ||
|
|
||||||
value instanceof Set
|
|
||||||
) {
|
) {
|
||||||
return Object.create(null) as SafeObject
|
return Object.create(null) as SafeObject
|
||||||
}
|
}
|
||||||
@@ -277,10 +250,7 @@ export const copyOut = (value: unknown, undefinedAsNull = false): unknown => {
|
|||||||
return value
|
return value
|
||||||
}
|
}
|
||||||
|
|
||||||
const definitions = <R>(
|
const definitions = <R>(tools: HostTools<R>, path: ReadonlyArray<string> = []): Array<{ path: string; definition: Definition<R> }> => {
|
||||||
tools: HostTools<R>,
|
|
||||||
path: ReadonlyArray<string> = [],
|
|
||||||
): Array<{ path: string; definition: Definition<R> }> => {
|
|
||||||
const entries: Array<{ path: string; definition: Definition<R> }> = []
|
const entries: Array<{ path: string; definition: Definition<R> }> = []
|
||||||
for (const [name, value] of Object.entries(tools)) {
|
for (const [name, value] of Object.entries(tools)) {
|
||||||
const next = [...path, name]
|
const next = [...path, name]
|
||||||
@@ -372,11 +342,8 @@ const toSearchEntry = <R>(path: string, definition: Definition<R>, description:
|
|||||||
path,
|
path,
|
||||||
definition.description,
|
definition.description,
|
||||||
...inputProperties(definition).flatMap(({ name, description: property }) =>
|
...inputProperties(definition).flatMap(({ name, description: property }) =>
|
||||||
property === undefined ? [name] : [name, property],
|
property === undefined ? [name] : [name, property]),
|
||||||
),
|
].join("\n").toLowerCase(),
|
||||||
]
|
|
||||||
.join("\n")
|
|
||||||
.toLowerCase(),
|
|
||||||
})
|
})
|
||||||
|
|
||||||
/** The runtime search index over every described tool. Search is always registered. */
|
/** The runtime search index over every described tool. Search is always registered. */
|
||||||
@@ -429,8 +396,7 @@ export const discoveryPlan = <R>(
|
|||||||
namespace,
|
namespace,
|
||||||
picked: new Set<ToolDescription>(),
|
picked: new Set<ToolDescription>(),
|
||||||
queue: [...group].sort(
|
queue: [...group].sort(
|
||||||
(left, right) =>
|
(left, right) => estimate(catalogLine(left)) - estimate(catalogLine(right)) || left.path.localeCompare(right.path),
|
||||||
estimate(catalogLine(left)) - estimate(catalogLine(right)) || left.path.localeCompare(right.path),
|
|
||||||
),
|
),
|
||||||
}))
|
}))
|
||||||
let used = 0
|
let used = 0
|
||||||
@@ -506,9 +472,7 @@ export const discoveryPlan = <R>(
|
|||||||
"- A result typed `Promise<unknown>` has no guaranteed shape - verify what actually came back before relying on its fields.",
|
"- A result typed `Promise<unknown>` has no guaranteed shape - verify what actually came back before relying on its fields.",
|
||||||
"- Run independent calls in parallel: `await Promise.all(items.map((item) => tools.<namespace>.<tool>(item)))`.",
|
"- Run independent calls in parallel: `await Promise.all(items.map((item) => tools.<namespace>.<tool>(item)))`.",
|
||||||
"- `Object.keys(tools)` lists namespaces; `Object.keys(tools.<namespace>)` lists its tools; `for...in` works on both.",
|
"- `Object.keys(tools)` lists namespaces; `Object.keys(tools.<namespace>)` lists its tools; `for...in` works on both.",
|
||||||
...(complete
|
...(complete ? [] : ['- Browse one namespace: `await tools.$codemode.search({ query: "", namespace: "<name>" })`.']),
|
||||||
? []
|
|
||||||
: ['- Browse one namespace: `await tools.$codemode.search({ query: "", namespace: "<name>" })`.']),
|
|
||||||
]
|
]
|
||||||
|
|
||||||
const syntax = [
|
const syntax = [
|
||||||
@@ -536,21 +500,26 @@ export const discoveryPlan = <R>(
|
|||||||
const count = `${group.length} tool${group.length === 1 ? "" : "s"}`
|
const count = `${group.length} tool${group.length === 1 ? "" : "s"}`
|
||||||
// Annotate only when a namespace is not fully shown, so a comprehensive
|
// Annotate only when a namespace is not fully shown, so a comprehensive
|
||||||
// namespace reads cleanly and a truncated one is unambiguous.
|
// namespace reads cleanly and a truncated one is unambiguous.
|
||||||
const label =
|
const label = picked.size === group.length ? count : picked.size === 0 ? `${count}, none shown` : `${count}, ${picked.size} shown`
|
||||||
picked.size === group.length
|
|
||||||
? count
|
|
||||||
: picked.size === 0
|
|
||||||
? `${count}, none shown`
|
|
||||||
: `${count}, ${picked.size} shown`
|
|
||||||
toolSection.push(`- ${namespace} (${label})`)
|
toolSection.push(`- ${namespace} (${label})`)
|
||||||
for (const tool of group) if (picked.has(tool)) toolSection.push(catalogLine(tool))
|
for (const tool of group) if (picked.has(tool)) toolSection.push(catalogLine(tool))
|
||||||
}
|
}
|
||||||
if (!complete) {
|
if (!complete) {
|
||||||
toolSection.push("", "Search returns complete callable signatures:", `- ${searchSignature}`)
|
toolSection.push(
|
||||||
|
"",
|
||||||
|
"Search returns complete callable signatures:",
|
||||||
|
`- ${searchSignature}`,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const lines = [...intro, ...workflow, ...rules, ...syntax, ...toolSection]
|
const lines = [
|
||||||
|
...intro,
|
||||||
|
...workflow,
|
||||||
|
...rules,
|
||||||
|
...syntax,
|
||||||
|
...toolSection,
|
||||||
|
]
|
||||||
return {
|
return {
|
||||||
catalog: described,
|
catalog: described,
|
||||||
instructions: lines.join("\n"),
|
instructions: lines.join("\n"),
|
||||||
@@ -565,29 +534,18 @@ export const discoveryPlan = <R>(
|
|||||||
* function in JS). An unknown path is an `UnknownTool` error pointing at the working
|
* function in JS). An unknown path is an `UnknownTool` error pointing at the working
|
||||||
* discovery idioms, mirroring how calling an unknown tool fails.
|
* discovery idioms, mirroring how calling an unknown tool fails.
|
||||||
*/
|
*/
|
||||||
const namespaceKeys = <R>(
|
const namespaceKeys = <R>(tools: HostTools<R>, path: ReadonlyArray<string>, searchEnabled: boolean): ReadonlyArray<string> => {
|
||||||
tools: HostTools<R>,
|
|
||||||
path: ReadonlyArray<string>,
|
|
||||||
searchEnabled: boolean,
|
|
||||||
): ReadonlyArray<string> => {
|
|
||||||
// The reserved discovery namespace is virtual (never present in the host tree); enumerate
|
// The reserved discovery namespace is virtual (never present in the host tree); enumerate
|
||||||
// it explicitly so `Object.keys(tools.$codemode)` matches the callable surface.
|
// it explicitly so `Object.keys(tools.$codemode)` matches the callable surface.
|
||||||
if (searchEnabled && path.length === 1 && path[0] === reservedNamespace) return ["search"]
|
if (searchEnabled && path.length === 1 && path[0] === reservedNamespace) return ["search"]
|
||||||
let value: HostTool<R> | Definition<R> | HostTools<R> = tools
|
let value: HostTool<R> | Definition<R> | HostTools<R> = tools
|
||||||
for (const segment of path) {
|
for (const segment of path) {
|
||||||
if (
|
if (isBlockedMember(segment) || typeof value === "function" || isDefinition(value) || !Object.hasOwn(value, segment)) {
|
||||||
isBlockedMember(segment) ||
|
|
||||||
typeof value === "function" ||
|
|
||||||
isDefinition(value) ||
|
|
||||||
!Object.hasOwn(value, segment)
|
|
||||||
) {
|
|
||||||
throw new ToolRuntimeError(
|
throw new ToolRuntimeError(
|
||||||
"UnknownTool",
|
"UnknownTool",
|
||||||
`Unknown tool namespace '${path.join(".")}'.`,
|
`Unknown tool namespace '${path.join(".")}'.`,
|
||||||
searchEnabled
|
searchEnabled
|
||||||
? [
|
? ["Object.keys(tools) lists the available namespaces; tools.$codemode.search({ query }) finds described tools."]
|
||||||
"Object.keys(tools) lists the available namespaces; tools.$codemode.search({ query }) finds described tools.",
|
|
||||||
]
|
|
||||||
: ["Object.keys(tools) lists the available namespaces."],
|
: ["Object.keys(tools) lists the available namespaces."],
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -597,25 +555,12 @@ const namespaceKeys = <R>(
|
|||||||
return Object.keys(value)
|
return Object.keys(value)
|
||||||
}
|
}
|
||||||
|
|
||||||
const resolve = <R>(
|
const resolve = <R>(tools: HostTools<R>, path: ReadonlyArray<string>, searchEnabled: boolean): HostTool<R> | Definition<R> => {
|
||||||
tools: HostTools<R>,
|
|
||||||
path: ReadonlyArray<string>,
|
|
||||||
searchEnabled: boolean,
|
|
||||||
): HostTool<R> | Definition<R> => {
|
|
||||||
let value: HostTool<R> | Definition<R> | HostTools<R> = tools
|
let value: HostTool<R> | Definition<R> | HostTools<R> = tools
|
||||||
|
|
||||||
for (const segment of path) {
|
for (const segment of path) {
|
||||||
if (
|
if (isBlockedMember(segment) || typeof value === "function" || isDefinition(value) || !Object.hasOwn(value, segment)) {
|
||||||
isBlockedMember(segment) ||
|
throw new ToolRuntimeError("UnknownTool", `Unknown tool '${path.join(".")}'.`, searchEnabled ? ["Use tools.$codemode.search({ query }) to find available described tools."] : [])
|
||||||
typeof value === "function" ||
|
|
||||||
isDefinition(value) ||
|
|
||||||
!Object.hasOwn(value, segment)
|
|
||||||
) {
|
|
||||||
throw new ToolRuntimeError(
|
|
||||||
"UnknownTool",
|
|
||||||
`Unknown tool '${path.join(".")}'.`,
|
|
||||||
searchEnabled ? ["Use tools.$codemode.search({ query }) to find available described tools."] : [],
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
value = value[segment] as HostTool<R> | Definition<R> | HostTools<R>
|
value = value[segment] as HostTool<R> | Definition<R> | HostTools<R>
|
||||||
}
|
}
|
||||||
@@ -657,8 +602,7 @@ export const make = <R>(
|
|||||||
return effect.pipe(
|
return effect.pipe(
|
||||||
Effect.tap(() => onEnd({ ...call, durationMs: Date.now() - startedAt, outcome: "success" })),
|
Effect.tap(() => onEnd({ ...call, durationMs: Date.now() - startedAt, outcome: "success" })),
|
||||||
Effect.tapError((error) =>
|
Effect.tapError((error) =>
|
||||||
onEnd({ ...call, durationMs: Date.now() - startedAt, outcome: "failure", message: failureMessage(error) }),
|
onEnd({ ...call, durationMs: Date.now() - startedAt, outcome: "failure", message: failureMessage(error) })),
|
||||||
),
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -693,32 +637,17 @@ export const make = <R>(
|
|||||||
if (!searchEnabled) throw new ToolRuntimeError("UnknownTool", `Unknown tool '${name}'.`)
|
if (!searchEnabled) throw new ToolRuntimeError("UnknownTool", `Unknown tool '${name}'.`)
|
||||||
const input = externalArgs[0]
|
const input = externalArgs[0]
|
||||||
if (externalArgs.length !== 1 || input === null || typeof input !== "object" || Array.isArray(input)) {
|
if (externalArgs.length !== 1 || input === null || typeof input !== "object" || Array.isArray(input)) {
|
||||||
throw new ToolRuntimeError(
|
throw new ToolRuntimeError("InvalidToolInput", "tools.$codemode.search expects { query?: string; namespace?: string; limit?: number }.")
|
||||||
"InvalidToolInput",
|
|
||||||
"tools.$codemode.search expects { query?: string; namespace?: string; limit?: number }.",
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
const request = input as { query?: unknown; namespace?: unknown; limit?: unknown }
|
const request = input as { query?: unknown; namespace?: unknown; limit?: unknown }
|
||||||
if (request.query !== undefined && typeof request.query !== "string") {
|
if (request.query !== undefined && typeof request.query !== "string") {
|
||||||
throw new ToolRuntimeError(
|
throw new ToolRuntimeError("InvalidToolInput", "tools.$codemode.search query must be a string when provided.")
|
||||||
"InvalidToolInput",
|
|
||||||
"tools.$codemode.search query must be a string when provided.",
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
if (request.namespace !== undefined && typeof request.namespace !== "string") {
|
if (request.namespace !== undefined && typeof request.namespace !== "string") {
|
||||||
throw new ToolRuntimeError(
|
throw new ToolRuntimeError("InvalidToolInput", "tools.$codemode.search namespace must be a string when provided.")
|
||||||
"InvalidToolInput",
|
|
||||||
"tools.$codemode.search namespace must be a string when provided.",
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
if (
|
if (request.limit !== undefined && (typeof request.limit !== "number" || !Number.isSafeInteger(request.limit) || request.limit <= 0)) {
|
||||||
request.limit !== undefined &&
|
throw new ToolRuntimeError("InvalidToolInput", "tools.$codemode.search limit must be a positive safe integer when provided.")
|
||||||
(typeof request.limit !== "number" || !Number.isSafeInteger(request.limit) || request.limit <= 0)
|
|
||||||
) {
|
|
||||||
throw new ToolRuntimeError(
|
|
||||||
"InvalidToolInput",
|
|
||||||
"tools.$codemode.search limit must be a positive safe integer when provided.",
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
const query = typeof request.query === "string" ? request.query : ""
|
const query = typeof request.query === "string" ? request.query : ""
|
||||||
const namespace = typeof request.namespace === "string" ? request.namespace : undefined
|
const namespace = typeof request.namespace === "string" ? request.namespace : undefined
|
||||||
@@ -727,27 +656,20 @@ export const make = <R>(
|
|||||||
Effect.try({
|
Effect.try({
|
||||||
try: () => {
|
try: () => {
|
||||||
const limit = typeof request.limit === "number" ? request.limit : defaultSearchLimit
|
const limit = typeof request.limit === "number" ? request.limit : defaultSearchLimit
|
||||||
const scoped =
|
const scoped = namespace === undefined ? searchIndex : searchIndex.filter((entry) => entry.namespace === namespace)
|
||||||
namespace === undefined ? searchIndex : searchIndex.filter((entry) => entry.namespace === namespace)
|
|
||||||
// A query that names one tool path exactly (canonical path or rendered
|
// A query that names one tool path exactly (canonical path or rendered
|
||||||
// JavaScript expression) is a lookup, not a search: return that tool alone.
|
// JavaScript expression) is a lookup, not a search: return that tool alone.
|
||||||
const trimmed = query.trim()
|
const trimmed = query.trim()
|
||||||
const pathQuery = trimmed.startsWith("tools.") ? trimmed.slice("tools.".length) : trimmed
|
const pathQuery = trimmed.startsWith("tools.") ? trimmed.slice("tools.".length) : trimmed
|
||||||
const exact =
|
const exact = pathQuery === "" ? undefined : scoped.find((entry) =>
|
||||||
pathQuery === ""
|
entry.description.path === pathQuery || toolExpression(entry.description.path) === trimmed)
|
||||||
? undefined
|
|
||||||
: scoped.find(
|
|
||||||
(entry) =>
|
|
||||||
entry.description.path === pathQuery || toolExpression(entry.description.path) === trimmed,
|
|
||||||
)
|
|
||||||
const terms = tokenize(query).map(termForms)
|
const terms = tokenize(query).map(termForms)
|
||||||
// Additive field-weighted scoring, summed across terms: exact path or path
|
// Additive field-weighted scoring, summed across terms: exact path or path
|
||||||
// segment (20) > path substring (8) > description substring (4) > any
|
// segment (20) > path substring (8) > description substring (4) > any
|
||||||
// searchable text, incl. input parameter names/descriptions (2). Each term
|
// searchable text, incl. input parameter names/descriptions (2). Each term
|
||||||
// matches a field when any of its forms (the term or a singular variant)
|
// matches a field when any of its forms (the term or a singular variant)
|
||||||
// does. An empty query browses everything, alphabetical by path.
|
// does. An empty query browses everything, alphabetical by path.
|
||||||
const ranked =
|
const ranked = exact !== undefined
|
||||||
exact !== undefined
|
|
||||||
? [exact]
|
? [exact]
|
||||||
: scoped
|
: scoped
|
||||||
.map((entry) => {
|
.map((entry) => {
|
||||||
@@ -765,11 +687,8 @@ export const make = <R>(
|
|||||||
return { entry, score }
|
return { entry, score }
|
||||||
})
|
})
|
||||||
.filter(({ score }) => terms.length === 0 || score > 0)
|
.filter(({ score }) => terms.length === 0 || score > 0)
|
||||||
.sort(
|
.sort((left, right) =>
|
||||||
(left, right) =>
|
right.score - left.score || left.entry.description.path.localeCompare(right.entry.description.path))
|
||||||
right.score - left.score ||
|
|
||||||
left.entry.description.path.localeCompare(right.entry.description.path),
|
|
||||||
)
|
|
||||||
.map(({ entry }) => entry)
|
.map(({ entry }) => entry)
|
||||||
// Result paths are rendered as JavaScript expressions so each `path` is
|
// Result paths are rendered as JavaScript expressions so each `path` is
|
||||||
// directly usable as the call site (`await tools.github.list({ ... })` or
|
// directly usable as the call site (`await tools.github.list({ ... })` or
|
||||||
@@ -792,12 +711,10 @@ export const make = <R>(
|
|||||||
const tool = resolve(tools, path, searchEnabled)
|
const tool = resolve(tools, path, searchEnabled)
|
||||||
let describedInput: unknown
|
let describedInput: unknown
|
||||||
if (isDefinition(tool)) {
|
if (isDefinition(tool)) {
|
||||||
if (externalArgs.length !== 1)
|
if (externalArgs.length !== 1) throw new ToolRuntimeError("InvalidToolInput", `Tool '${name}' expects exactly one input object.`)
|
||||||
throw new ToolRuntimeError("InvalidToolInput", `Tool '${name}' expects exactly one input object.`)
|
|
||||||
describedInput = yield* Effect.try({
|
describedInput = yield* Effect.try({
|
||||||
try: () => decodeToolInput(tool, externalArgs[0]),
|
try: () => decodeToolInput(tool, externalArgs[0]),
|
||||||
catch: (cause) =>
|
catch: (cause) => new ToolRuntimeError("InvalidToolInput", `Invalid input for tool '${name}': ${String(cause)}`),
|
||||||
new ToolRuntimeError("InvalidToolInput", `Invalid input for tool '${name}': ${String(cause)}`),
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
const input = isDefinition(tool) ? describedInput : externalArgs
|
const input = isDefinition(tool) ? describedInput : externalArgs
|
||||||
|
|||||||
@@ -57,7 +57,8 @@ export type Options<I extends ToolSchema, O extends ToolSchema | undefined, R =
|
|||||||
export const isDefinition = <R = never>(value: unknown): value is Definition<R> =>
|
export const isDefinition = <R = never>(value: unknown): value is Definition<R> =>
|
||||||
typeof value === "object" && value !== null && "_tag" in value && value._tag === "CodeModeTool"
|
typeof value === "object" && value !== null && "_tag" in value && value._tag === "CodeModeTool"
|
||||||
|
|
||||||
const isEffectSchema = (schema: ToolSchema): schema is Schema.Decoder<unknown> & Schema.Top => Schema.isSchema(schema)
|
const isEffectSchema = (schema: ToolSchema): schema is Schema.Decoder<unknown> & Schema.Top =>
|
||||||
|
Schema.isSchema(schema)
|
||||||
|
|
||||||
const renderLiteral = (value: unknown): string => JSON.stringify(value) ?? "unknown"
|
const renderLiteral = (value: unknown): string => JSON.stringify(value) ?? "unknown"
|
||||||
|
|
||||||
@@ -68,12 +69,10 @@ const renderLiteral = (value: unknown): string => JSON.stringify(value) ?? "unkn
|
|||||||
export const identifierSegment = /^[A-Za-z_$][A-Za-z0-9_$]*$/
|
export const identifierSegment = /^[A-Za-z_$][A-Za-z0-9_$]*$/
|
||||||
|
|
||||||
/** Renders a property name as a valid TS object key: bare when an identifier, quoted otherwise. */
|
/** Renders a property name as a valid TS object key: bare when an identifier, quoted otherwise. */
|
||||||
const renderKey = (name: string): string => (identifierSegment.test(name) ? name : JSON.stringify(name))
|
const renderKey = (name: string): string => identifierSegment.test(name) ? name : JSON.stringify(name)
|
||||||
|
|
||||||
const effectNumberSentinel = (schema: JsonSchema) =>
|
const effectNumberSentinel = (schema: JsonSchema) =>
|
||||||
schema.type === "string" &&
|
schema.type === "string" && Array.isArray(schema.enum) && schema.enum.length === 1 &&
|
||||||
Array.isArray(schema.enum) &&
|
|
||||||
schema.enum.length === 1 &&
|
|
||||||
(schema.enum[0] === "NaN" || schema.enum[0] === "Infinity" || schema.enum[0] === "-Infinity")
|
(schema.enum[0] === "NaN" || schema.enum[0] === "Infinity" || schema.enum[0] === "-Infinity")
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -119,8 +118,7 @@ const docTags = (schema: JsonSchema): Array<string> => {
|
|||||||
*/
|
*/
|
||||||
const jsdoc = (description: string | undefined, tags: ReadonlyArray<string>, pad: string): string => {
|
const jsdoc = (description: string | undefined, tags: ReadonlyArray<string>, pad: string): string => {
|
||||||
const lines = [...(description === undefined ? [] : description.split("\n")), ...tags].map((line) =>
|
const lines = [...(description === undefined ? [] : description.split("\n")), ...tags].map((line) =>
|
||||||
line.replaceAll("*/", "* /").replace(/\s+$/, ""),
|
line.replaceAll("*/", "* /").replace(/\s+$/, ""))
|
||||||
)
|
|
||||||
while (lines.length > 0 && lines[0]!.trim() === "") lines.shift()
|
while (lines.length > 0 && lines[0]!.trim() === "") lines.shift()
|
||||||
while (lines.length > 0 && lines[lines.length - 1]!.trim() === "") lines.pop()
|
while (lines.length > 0 && lines[lines.length - 1]!.trim() === "") lines.pop()
|
||||||
if (lines.length === 0) return ""
|
if (lines.length === 0) return ""
|
||||||
@@ -129,12 +127,7 @@ const jsdoc = (description: string | undefined, tags: ReadonlyArray<string>, pad
|
|||||||
return `${pad}/**\n${body}\n${pad} */\n`
|
return `${pad}/**\n${body}\n${pad} */\n`
|
||||||
}
|
}
|
||||||
|
|
||||||
const renderSchema = (
|
const renderSchema = (schema: JsonSchema, ctx: RenderContext, depth = 0, seen: ReadonlySet<string> = new Set()): string => {
|
||||||
schema: JsonSchema,
|
|
||||||
ctx: RenderContext,
|
|
||||||
depth = 0,
|
|
||||||
seen: ReadonlySet<string> = new Set(),
|
|
||||||
): string => {
|
|
||||||
if (depth > MAX_RENDER_DEPTH) return "unknown"
|
if (depth > MAX_RENDER_DEPTH) return "unknown"
|
||||||
if (schema.$ref) {
|
if (schema.$ref) {
|
||||||
const name = schema.$ref.split("/").pop()
|
const name = schema.$ref.split("/").pop()
|
||||||
@@ -153,16 +146,13 @@ const renderSchema = (
|
|||||||
if (
|
if (
|
||||||
alternatives.some((item) => item.type === "number") &&
|
alternatives.some((item) => item.type === "number") &&
|
||||||
alternatives.every((item) => item.type === "number" || effectNumberSentinel(item))
|
alternatives.every((item) => item.type === "number" || effectNumberSentinel(item))
|
||||||
)
|
) return "number"
|
||||||
return "number"
|
|
||||||
// An empty Schema.Struct({}) emits `anyOf: [{ type: "object" }, { type: "array" }]`
|
// An empty Schema.Struct({}) emits `anyOf: [{ type: "object" }, { type: "array" }]`
|
||||||
// (no properties/items); render the bare shape as {} instead of `{} | Array<unknown>`.
|
// (no properties/items); render the bare shape as {} instead of `{} | Array<unknown>`.
|
||||||
if (
|
if (
|
||||||
alternatives.length === 2 &&
|
alternatives.length === 2 &&
|
||||||
alternatives[0]?.type === "object" &&
|
alternatives[0]?.type === "object" && alternatives[0].properties === undefined &&
|
||||||
alternatives[0].properties === undefined &&
|
alternatives[1]?.type === "array" && alternatives[1].items === undefined
|
||||||
alternatives[1]?.type === "array" &&
|
|
||||||
alternatives[1].items === undefined
|
|
||||||
) {
|
) {
|
||||||
return "{}"
|
return "{}"
|
||||||
}
|
}
|
||||||
@@ -180,8 +170,7 @@ const renderSchema = (
|
|||||||
const required = new Set(schema.required ?? [])
|
const required = new Set(schema.required ?? [])
|
||||||
const properties = Object.entries(schema.properties ?? {})
|
const properties = Object.entries(schema.properties ?? {})
|
||||||
const additional = schema.additionalProperties
|
const additional = schema.additionalProperties
|
||||||
const indexType =
|
const indexType = additional && typeof additional === "object" ? renderSchema(additional, ctx, depth + 1, seen) : undefined
|
||||||
additional && typeof additional === "object" ? renderSchema(additional, ctx, depth + 1, seen) : undefined
|
|
||||||
const field = ([name, value]: readonly [string, JsonSchema]) =>
|
const field = ([name, value]: readonly [string, JsonSchema]) =>
|
||||||
`${renderKey(name)}${required.has(name) ? "" : "?"}: ${renderSchema(value, ctx, depth + 1, seen)}`
|
`${renderKey(name)}${required.has(name) ? "" : "?"}: ${renderSchema(value, ctx, depth + 1, seen)}`
|
||||||
|
|
||||||
@@ -194,9 +183,7 @@ const renderSchema = (
|
|||||||
// Pretty: an indented block, each described field preceded by its JSDoc comment.
|
// Pretty: an indented block, each described field preceded by its JSDoc comment.
|
||||||
if (properties.length === 0 && indexType === undefined) return "{}"
|
if (properties.length === 0 && indexType === undefined) return "{}"
|
||||||
const pad = " ".repeat(depth + 1)
|
const pad = " ".repeat(depth + 1)
|
||||||
const lines = properties.map(
|
const lines = properties.map((entry) => `${jsdoc(entry[1].description, docTags(entry[1]), pad)}${pad}${field(entry)}`)
|
||||||
(entry) => `${jsdoc(entry[1].description, docTags(entry[1]), pad)}${pad}${field(entry)}`,
|
|
||||||
)
|
|
||||||
if (indexType !== undefined) lines.push(`${pad}[key: string]: ${indexType}`)
|
if (indexType !== undefined) lines.push(`${pad}[key: string]: ${indexType}`)
|
||||||
return `{\n${lines.join("\n")}\n${" ".repeat(depth)}}`
|
return `{\n${lines.join("\n")}\n${" ".repeat(depth)}}`
|
||||||
}
|
}
|
||||||
@@ -275,9 +262,7 @@ export const inputProperties = <R>(definition: Definition<R>): Array<InputProper
|
|||||||
* fields; the default stays the compact single-line form.
|
* fields; the default stays the compact single-line form.
|
||||||
*/
|
*/
|
||||||
export const inputTypeScript = <R>(definition: Definition<R>, pretty = false): string =>
|
export const inputTypeScript = <R>(definition: Definition<R>, pretty = false): string =>
|
||||||
isEffectSchema(definition.input)
|
isEffectSchema(definition.input) ? toTypeScript(definition.input, false, pretty) : jsonSchemaToTypeScript(definition.input, pretty)
|
||||||
? toTypeScript(definition.input, false, pretty)
|
|
||||||
: jsonSchemaToTypeScript(definition.input, pretty)
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The model-visible TypeScript type of a tool's result; tools without an output schema
|
* The model-visible TypeScript type of a tool's result; tools without an output schema
|
||||||
|
|||||||
@@ -28,7 +28,4 @@ export class SandboxSet {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const isSandboxValue = (value: unknown): value is SandboxDate | SandboxRegExp | SandboxMap | SandboxSet =>
|
export const isSandboxValue = (value: unknown): value is SandboxDate | SandboxRegExp | SandboxMap | SandboxSet =>
|
||||||
value instanceof SandboxDate ||
|
value instanceof SandboxDate || value instanceof SandboxRegExp || value instanceof SandboxMap || value instanceof SandboxSet
|
||||||
value instanceof SandboxRegExp ||
|
|
||||||
value instanceof SandboxMap ||
|
|
||||||
value instanceof SandboxSet
|
|
||||||
|
|||||||
@@ -1,13 +1,6 @@
|
|||||||
import { describe, expect, test } from "bun:test"
|
import { describe, expect, test } from "bun:test"
|
||||||
import { Cause, Effect, Schema } from "effect"
|
import { Cause, Effect, Schema } from "effect"
|
||||||
import {
|
import { CodeMode, ExecuteInputSchema, ExecuteResultSchema, Tool, toolError, type ExecutionLimits } from "../src/index.js"
|
||||||
CodeMode,
|
|
||||||
ExecuteInputSchema,
|
|
||||||
ExecuteResultSchema,
|
|
||||||
Tool,
|
|
||||||
toolError,
|
|
||||||
type ExecutionLimits,
|
|
||||||
} from "../src/index.js"
|
|
||||||
import type { Definition } from "../src/tool.js"
|
import type { Definition } from "../src/tool.js"
|
||||||
|
|
||||||
const run = (tool: Definition<never>) =>
|
const run = (tool: Definition<never>) =>
|
||||||
@@ -82,14 +75,11 @@ describe("CodeMode host failure boundary", () => {
|
|||||||
output: Schema.Unknown,
|
output: Schema.Unknown,
|
||||||
run: () =>
|
run: () =>
|
||||||
Effect.succeed(
|
Effect.succeed(
|
||||||
new Proxy(
|
new Proxy({}, {
|
||||||
{},
|
|
||||||
{
|
|
||||||
ownKeys: () => {
|
ownKeys: () => {
|
||||||
throw new Error("host-output-secret")
|
throw new Error("host-output-secret")
|
||||||
},
|
},
|
||||||
},
|
}),
|
||||||
),
|
|
||||||
),
|
),
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
@@ -172,7 +162,9 @@ describe("CodeMode tool-call observation", () => {
|
|||||||
)
|
)
|
||||||
|
|
||||||
expect(result.ok).toBe(true)
|
expect(result.ok).toBe(true)
|
||||||
expect(calls).toStrictEqual([{ index: 0, name: "context.lookup", input: { query: "deployment failure" } }])
|
expect(calls).toStrictEqual([
|
||||||
|
{ index: 0, name: "context.lookup", input: { query: "deployment failure" } },
|
||||||
|
])
|
||||||
})
|
})
|
||||||
|
|
||||||
test("observes settled calls with outcome and duration", async () => {
|
test("observes settled calls with outcome and duration", async () => {
|
||||||
@@ -181,17 +173,16 @@ describe("CodeMode tool-call observation", () => {
|
|||||||
description: "Look up a value",
|
description: "Look up a value",
|
||||||
input: Schema.Struct({ query: Schema.String }),
|
input: Schema.Struct({ query: Schema.String }),
|
||||||
output: Schema.String,
|
output: Schema.String,
|
||||||
run: ({ query }) => (query === "boom" ? Effect.fail(toolError("Lookup refused")) : Effect.succeed(query)),
|
run: ({ query }) =>
|
||||||
|
query === "boom" ? Effect.fail(toolError("Lookup refused")) : Effect.succeed(query),
|
||||||
})
|
})
|
||||||
|
|
||||||
const runtime = CodeMode.make({
|
const runtime = CodeMode.make({
|
||||||
tools: { context: { lookup } },
|
tools: { context: { lookup } },
|
||||||
onToolCallStart: (call) =>
|
onToolCallStart: (call) => Effect.sync(() => {
|
||||||
Effect.sync(() => {
|
|
||||||
events.push({ phase: "start", index: call.index, name: call.name })
|
events.push({ phase: "start", index: call.index, name: call.name })
|
||||||
}),
|
}),
|
||||||
onToolCallEnd: (call) =>
|
onToolCallEnd: (call) => Effect.sync(() => {
|
||||||
Effect.sync(() => {
|
|
||||||
expect(call.durationMs).toBeGreaterThanOrEqual(0)
|
expect(call.durationMs).toBeGreaterThanOrEqual(0)
|
||||||
events.push({
|
events.push({
|
||||||
phase: "end",
|
phase: "end",
|
||||||
@@ -219,15 +210,13 @@ describe("CodeMode tool-call observation", () => {
|
|||||||
|
|
||||||
describe("CodeMode console capture", () => {
|
describe("CodeMode console capture", () => {
|
||||||
test("captures console output as bounded result logs", async () => {
|
test("captures console output as bounded result logs", async () => {
|
||||||
const result = await Effect.runPromise(
|
const result = await Effect.runPromise(CodeMode.execute({
|
||||||
CodeMode.execute({
|
|
||||||
code: `
|
code: `
|
||||||
const returned = console.log("Thread info:", { name: "Demo", count: 2 })
|
const returned = console.log("Thread info:", { name: "Demo", count: 2 })
|
||||||
console.warn("careful")
|
console.warn("careful")
|
||||||
return returned
|
return returned
|
||||||
`,
|
`,
|
||||||
}),
|
}))
|
||||||
)
|
|
||||||
|
|
||||||
expect(result).toStrictEqual({
|
expect(result).toStrictEqual({
|
||||||
ok: true,
|
ok: true,
|
||||||
@@ -239,45 +228,39 @@ describe("CodeMode console capture", () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
test("keeps logs captured before failures", async () => {
|
test("keeps logs captured before failures", async () => {
|
||||||
const result = await Effect.runPromise(
|
const result = await Effect.runPromise(CodeMode.execute({
|
||||||
CodeMode.execute({
|
|
||||||
code: `
|
code: `
|
||||||
console.log("before failure")
|
console.log("before failure")
|
||||||
throw new Error("boom")
|
throw new Error("boom")
|
||||||
`,
|
`,
|
||||||
}),
|
}))
|
||||||
)
|
|
||||||
|
|
||||||
expect(result.ok ? undefined : result.logs).toStrictEqual(["before failure"])
|
expect(result.ok ? undefined : result.logs).toStrictEqual(["before failure"])
|
||||||
expect(result.ok ? undefined : result.error.message).toBe("Uncaught: boom")
|
expect(result.ok ? undefined : result.error.message).toBe("Uncaught: boom")
|
||||||
})
|
})
|
||||||
|
|
||||||
test("prints NaN and Infinity literally instead of the JSON null", async () => {
|
test("prints NaN and Infinity literally instead of the JSON null", async () => {
|
||||||
const result = await Effect.runPromise(
|
const result = await Effect.runPromise(CodeMode.execute({
|
||||||
CodeMode.execute({
|
|
||||||
code: `
|
code: `
|
||||||
console.log(NaN)
|
console.log(NaN)
|
||||||
console.log(Infinity, -Infinity)
|
console.log(Infinity, -Infinity)
|
||||||
console.log({ ratio: NaN, bounds: [Infinity] })
|
console.log({ ratio: NaN, bounds: [Infinity] })
|
||||||
return null
|
return null
|
||||||
`,
|
`,
|
||||||
}),
|
}))
|
||||||
)
|
|
||||||
|
|
||||||
expect(result.ok).toBe(true)
|
expect(result.ok).toBe(true)
|
||||||
expect(result.logs).toStrictEqual(["NaN", "Infinity -Infinity", '{"ratio":NaN,"bounds":[Infinity]}'])
|
expect(result.logs).toStrictEqual(["NaN", "Infinity -Infinity", '{"ratio":NaN,"bounds":[Infinity]}'])
|
||||||
})
|
})
|
||||||
|
|
||||||
test("renders sandbox values nested inside logged containers", async () => {
|
test("renders sandbox values nested inside logged containers", async () => {
|
||||||
const result = await Effect.runPromise(
|
const result = await Effect.runPromise(CodeMode.execute({
|
||||||
CodeMode.execute({
|
|
||||||
code: `
|
code: `
|
||||||
console.log({ m: new Map([["a", 1]]), when: new Date(0), r: /ab/g, s: new Set([1, 2]) })
|
console.log({ m: new Map([["a", 1]]), when: new Date(0), r: /ab/g, s: new Set([1, 2]) })
|
||||||
console.log([new Date(0)])
|
console.log([new Date(0)])
|
||||||
return null
|
return null
|
||||||
`,
|
`,
|
||||||
}),
|
}))
|
||||||
)
|
|
||||||
|
|
||||||
expect(result.ok).toBe(true)
|
expect(result.ok).toBe(true)
|
||||||
expect(result.logs).toStrictEqual([
|
expect(result.logs).toStrictEqual([
|
||||||
@@ -287,8 +270,7 @@ describe("CodeMode console capture", () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
test("console formatting is total: cycles and opaque references render as markers", async () => {
|
test("console formatting is total: cycles and opaque references render as markers", async () => {
|
||||||
const result = await Effect.runPromise(
|
const result = await Effect.runPromise(CodeMode.execute({
|
||||||
CodeMode.execute({
|
|
||||||
code: `
|
code: `
|
||||||
const m = new Map()
|
const m = new Map()
|
||||||
m.set("self", m)
|
m.set("self", m)
|
||||||
@@ -296,30 +278,29 @@ describe("CodeMode console capture", () => {
|
|||||||
console.log({ fn: (x) => x, ok: 1 })
|
console.log({ fn: (x) => x, ok: 1 })
|
||||||
return null
|
return null
|
||||||
`,
|
`,
|
||||||
}),
|
}))
|
||||||
)
|
|
||||||
|
|
||||||
expect(result.ok).toBe(true)
|
expect(result.ok).toBe(true)
|
||||||
expect(result.logs).toStrictEqual(['{"box":Map(1) [["self",[Circular]]]}', '{"fn":[CodeMode reference],"ok":1}'])
|
expect(result.logs).toStrictEqual([
|
||||||
|
'{"box":Map(1) [["self",[Circular]]]}',
|
||||||
|
'{"fn":[CodeMode reference],"ok":1}',
|
||||||
|
])
|
||||||
})
|
})
|
||||||
|
|
||||||
test("console.table renders sandbox value cells", async () => {
|
test("console.table renders sandbox value cells", async () => {
|
||||||
const result = await Effect.runPromise(
|
const result = await Effect.runPromise(CodeMode.execute({
|
||||||
CodeMode.execute({
|
|
||||||
code: `
|
code: `
|
||||||
console.table([{ when: new Date(0), n: NaN }])
|
console.table([{ when: new Date(0), n: NaN }])
|
||||||
return null
|
return null
|
||||||
`,
|
`,
|
||||||
}),
|
}))
|
||||||
)
|
|
||||||
|
|
||||||
expect(result.ok).toBe(true)
|
expect(result.ok).toBe(true)
|
||||||
expect(result.logs).toStrictEqual(["(index)\twhen\tn\n0\t1970-01-01T00:00:00.000Z\tNaN"])
|
expect(result.logs).toStrictEqual(["(index)\twhen\tn\n0\t1970-01-01T00:00:00.000Z\tNaN"])
|
||||||
})
|
})
|
||||||
|
|
||||||
test("captures console.dir and console.table output", async () => {
|
test("captures console.dir and console.table output", async () => {
|
||||||
const result = await Effect.runPromise(
|
const result = await Effect.runPromise(CodeMode.execute({
|
||||||
CodeMode.execute({
|
|
||||||
code: `
|
code: `
|
||||||
console.dir({ nested: { ok: true } })
|
console.dir({ nested: { ok: true } })
|
||||||
console.table([
|
console.table([
|
||||||
@@ -328,13 +309,15 @@ describe("CodeMode console capture", () => {
|
|||||||
], ["name", "count"])
|
], ["name", "count"])
|
||||||
return "done"
|
return "done"
|
||||||
`,
|
`,
|
||||||
}),
|
}))
|
||||||
)
|
|
||||||
|
|
||||||
expect(result).toStrictEqual({
|
expect(result).toStrictEqual({
|
||||||
ok: true,
|
ok: true,
|
||||||
value: "done",
|
value: "done",
|
||||||
logs: ['{"nested":{"ok":true}}', "(index)\tname\tcount\n0\tKit\t1\n1\tOlive\t2"],
|
logs: [
|
||||||
|
'{"nested":{"ok":true}}',
|
||||||
|
"(index)\tname\tcount\n0\tKit\t1\n1\tOlive\t2",
|
||||||
|
],
|
||||||
toolCalls: [],
|
toolCalls: [],
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
@@ -342,11 +325,9 @@ describe("CodeMode console capture", () => {
|
|||||||
|
|
||||||
describe("CodeMode output budget", () => {
|
describe("CodeMode output budget", () => {
|
||||||
test("absent maxOutputBytes means no truncation at all", async () => {
|
test("absent maxOutputBytes means no truncation at all", async () => {
|
||||||
const result = await Effect.runPromise(
|
const result = await Effect.runPromise(CodeMode.execute({
|
||||||
CodeMode.execute({
|
|
||||||
code: `console.log("z".repeat(50_000)); return "x".repeat(100_000)`,
|
code: `console.log("z".repeat(50_000)); return "x".repeat(100_000)`,
|
||||||
}),
|
}))
|
||||||
)
|
|
||||||
|
|
||||||
expect(result.ok).toBe(true)
|
expect(result.ok).toBe(true)
|
||||||
if (!result.ok) return
|
if (!result.ok) return
|
||||||
@@ -357,35 +338,29 @@ describe("CodeMode output budget", () => {
|
|||||||
|
|
||||||
test("truncates an oversized result value with a marker instead of failing", async () => {
|
test("truncates an oversized result value with a marker instead of failing", async () => {
|
||||||
const limits: ExecutionLimits = { maxOutputBytes: 40 }
|
const limits: ExecutionLimits = { maxOutputBytes: 40 }
|
||||||
const result = await Effect.runPromise(
|
const result = await Effect.runPromise(CodeMode.execute({
|
||||||
CodeMode.execute({
|
|
||||||
code: `return { data: "${"x".repeat(200)}" }`,
|
code: `return { data: "${"x".repeat(200)}" }`,
|
||||||
limits,
|
limits,
|
||||||
}),
|
}))
|
||||||
)
|
|
||||||
|
|
||||||
expect(result.ok).toBe(true)
|
expect(result.ok).toBe(true)
|
||||||
if (!result.ok) return
|
if (!result.ok) return
|
||||||
expect(result.truncated).toBe(true)
|
expect(result.truncated).toBe(true)
|
||||||
expect(typeof result.value).toBe("string")
|
expect(typeof result.value).toBe("string")
|
||||||
expect(result.value).toMatch(
|
expect(result.value).toMatch(/^\{"data":"x+ \[result truncated: \d+ bytes exceeds the 40-byte output limit; return a smaller value\]$/)
|
||||||
/^\{"data":"x+ \[result truncated: \d+ bytes exceeds the 40-byte output limit; return a smaller value\]$/,
|
|
||||||
)
|
|
||||||
expect(Schema.decodeUnknownSync(ExecuteResultSchema)(JSON.parse(JSON.stringify(result)))).toStrictEqual(result)
|
expect(Schema.decodeUnknownSync(ExecuteResultSchema)(JSON.parse(JSON.stringify(result)))).toStrictEqual(result)
|
||||||
})
|
})
|
||||||
|
|
||||||
test("keeps leading logs within the remaining budget and marks the cut", async () => {
|
test("keeps leading logs within the remaining budget and marks the cut", async () => {
|
||||||
const limits: ExecutionLimits = { maxOutputBytes: 40 }
|
const limits: ExecutionLimits = { maxOutputBytes: 40 }
|
||||||
const result = await Effect.runPromise(
|
const result = await Effect.runPromise(CodeMode.execute({
|
||||||
CodeMode.execute({
|
|
||||||
code: `
|
code: `
|
||||||
console.log("first line")
|
console.log("first line")
|
||||||
console.log("${"y".repeat(200)}")
|
console.log("${"y".repeat(200)}")
|
||||||
return "ok"
|
return "ok"
|
||||||
`,
|
`,
|
||||||
limits,
|
limits,
|
||||||
}),
|
}))
|
||||||
)
|
|
||||||
|
|
||||||
expect(result.ok).toBe(true)
|
expect(result.ok).toBe(true)
|
||||||
if (!result.ok) return
|
if (!result.ok) return
|
||||||
@@ -395,14 +370,12 @@ describe("CodeMode output budget", () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
test("does not mark results within the budget", async () => {
|
test("does not mark results within the budget", async () => {
|
||||||
const result = await Effect.runPromise(
|
const result = await Effect.runPromise(CodeMode.execute({
|
||||||
CodeMode.execute({
|
|
||||||
code: `
|
code: `
|
||||||
console.log("fits")
|
console.log("fits")
|
||||||
return { fits: true }
|
return { fits: true }
|
||||||
`,
|
`,
|
||||||
}),
|
}))
|
||||||
)
|
|
||||||
expect(result).toStrictEqual({
|
expect(result).toStrictEqual({
|
||||||
ok: true,
|
ok: true,
|
||||||
value: { fits: true },
|
value: { fits: true },
|
||||||
@@ -422,21 +395,18 @@ describe("CodeMode schema flexibility", () => {
|
|||||||
properties: { id: { type: "string" }, count: { type: "number" } },
|
properties: { id: { type: "string" }, count: { type: "number" } },
|
||||||
required: ["id"],
|
required: ["id"],
|
||||||
},
|
},
|
||||||
run: (input) =>
|
run: (input) => Effect.sync(() => {
|
||||||
Effect.sync(() => {
|
|
||||||
observed.push(input)
|
observed.push(input)
|
||||||
return { echoed: input }
|
return { echoed: input }
|
||||||
}),
|
}),
|
||||||
})
|
})
|
||||||
const runtime = CodeMode.make({ tools: { adapter: { call } } })
|
const runtime = CodeMode.make({ tools: { adapter: { call } } })
|
||||||
|
|
||||||
expect(runtime.catalog()).toStrictEqual([
|
expect(runtime.catalog()).toStrictEqual([{
|
||||||
{
|
|
||||||
path: "adapter.call",
|
path: "adapter.call",
|
||||||
description: "Call an adapter-described tool",
|
description: "Call an adapter-described tool",
|
||||||
signature: "tools.adapter.call(input: { id: string; count?: number }): Promise<unknown>",
|
signature: "tools.adapter.call(input: { id: string; count?: number }): Promise<unknown>",
|
||||||
},
|
}])
|
||||||
])
|
|
||||||
|
|
||||||
// JSON Schema is render-only: mistyped input passes through unvalidated.
|
// JSON Schema is render-only: mistyped input passes through unvalidated.
|
||||||
const result = await Effect.runPromise(runtime.execute(`return await tools.adapter.call({ id: 42 })`))
|
const result = await Effect.runPromise(runtime.execute(`return await tools.adapter.call({ id: 42 })`))
|
||||||
@@ -451,25 +421,17 @@ describe("CodeMode schema flexibility", () => {
|
|||||||
input: { type: "object", properties: { login: { type: "string" } }, required: ["login"] },
|
input: { type: "object", properties: { login: { type: "string" } }, required: ["login"] },
|
||||||
output: {
|
output: {
|
||||||
$ref: "#/$defs/User",
|
$ref: "#/$defs/User",
|
||||||
$defs: {
|
$defs: { User: { type: "object", properties: { login: { type: "string" }, id: { type: "number" } }, required: ["login", "id"] } },
|
||||||
User: {
|
|
||||||
type: "object",
|
|
||||||
properties: { login: { type: "string" }, id: { type: "number" } },
|
|
||||||
required: ["login", "id"],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
run: () => Effect.succeed({ login: "kit", id: 7 }),
|
run: () => Effect.succeed({ login: "kit", id: 7 }),
|
||||||
})
|
})
|
||||||
const runtime = CodeMode.make({ tools: { users: { lookup } } })
|
const runtime = CodeMode.make({ tools: { users: { lookup } } })
|
||||||
|
|
||||||
expect(runtime.catalog()).toStrictEqual([
|
expect(runtime.catalog()).toStrictEqual([{
|
||||||
{
|
|
||||||
path: "users.lookup",
|
path: "users.lookup",
|
||||||
description: "Look up a user",
|
description: "Look up a user",
|
||||||
signature: "tools.users.lookup(input: { login: string }): Promise<{ login: string; id: number }>",
|
signature: "tools.users.lookup(input: { login: string }): Promise<{ login: string; id: number }>",
|
||||||
},
|
}])
|
||||||
])
|
|
||||||
|
|
||||||
const result = await Effect.runPromise(runtime.execute(`return await tools.users.lookup({ login: "kit" })`))
|
const result = await Effect.runPromise(runtime.execute(`return await tools.users.lookup({ login: "kit" })`))
|
||||||
expect(result.ok).toBe(true)
|
expect(result.ok).toBe(true)
|
||||||
@@ -516,20 +478,16 @@ describe("CodeMode public contract", () => {
|
|||||||
expect(agentTool.input).toBe(ExecuteInputSchema)
|
expect(agentTool.input).toBe(ExecuteInputSchema)
|
||||||
expect(agentTool.output).toBe(ExecuteResultSchema)
|
expect(agentTool.output).toBe(ExecuteResultSchema)
|
||||||
expect(agentTool.description).toBe(runtime.instructions())
|
expect(agentTool.description).toBe(runtime.instructions())
|
||||||
expect(Schema.decodeUnknownSync(ExecuteResultSchema)(JSON.parse(JSON.stringify(projected)))).toStrictEqual(
|
expect(Schema.decodeUnknownSync(ExecuteResultSchema)(JSON.parse(JSON.stringify(projected)))).toStrictEqual(projected)
|
||||||
projected,
|
|
||||||
)
|
|
||||||
})
|
})
|
||||||
|
|
||||||
test("inlines a COMPLETE small catalog and keeps search registered but unadvertised", async () => {
|
test("inlines a COMPLETE small catalog and keeps search registered but unadvertised", async () => {
|
||||||
const runtime = CodeMode.make({ tools })
|
const runtime = CodeMode.make({ tools })
|
||||||
expect(runtime.catalog()).toStrictEqual([
|
expect(runtime.catalog()).toStrictEqual([{
|
||||||
{
|
|
||||||
path: "orders.lookup",
|
path: "orders.lookup",
|
||||||
description: "Look up an order by ID",
|
description: "Look up an order by ID",
|
||||||
signature: "tools.orders.lookup(input: { id: string }): Promise<{ id: string; status: string }>",
|
signature: "tools.orders.lookup(input: { id: string }): Promise<{ id: string; status: string }>",
|
||||||
},
|
}])
|
||||||
])
|
|
||||||
expect(runtime.instructions()).toContain("Available tools (COMPLETE list")
|
expect(runtime.instructions()).toContain("Available tools (COMPLETE list")
|
||||||
expect(runtime.instructions()).toContain("- orders (1 tool)")
|
expect(runtime.instructions()).toContain("- orders (1 tool)")
|
||||||
expect(runtime.instructions()).toContain(
|
expect(runtime.instructions()).toContain(
|
||||||
@@ -544,13 +502,11 @@ describe("CodeMode public contract", () => {
|
|||||||
expect(result.ok).toBe(true)
|
expect(result.ok).toBe(true)
|
||||||
if (result.ok) {
|
if (result.ok) {
|
||||||
expect(result.value).toStrictEqual({
|
expect(result.value).toStrictEqual({
|
||||||
items: [
|
items: [{
|
||||||
{
|
|
||||||
path: "tools.orders.lookup",
|
path: "tools.orders.lookup",
|
||||||
description: "Look up an order by ID",
|
description: "Look up an order by ID",
|
||||||
signature: "tools.orders.lookup(input: {\n id: string\n}): Promise<{\n id: string\n status: string\n}>",
|
signature: "tools.orders.lookup(input: {\n id: string\n}): Promise<{\n id: string\n status: string\n}>",
|
||||||
},
|
}],
|
||||||
],
|
|
||||||
total: 1,
|
total: 1,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -565,43 +521,31 @@ describe("CodeMode public contract", () => {
|
|||||||
})
|
})
|
||||||
const runtime = CodeMode.make({ tools: { context7: { "resolve-library-id": resolveLibrary } } })
|
const runtime = CodeMode.make({ tools: { context7: { "resolve-library-id": resolveLibrary } } })
|
||||||
|
|
||||||
expect(runtime.catalog()).toStrictEqual([
|
expect(runtime.catalog()).toStrictEqual([{
|
||||||
{
|
|
||||||
path: "context7.resolve-library-id",
|
path: "context7.resolve-library-id",
|
||||||
description: "Resolve a library ID",
|
description: "Resolve a library ID",
|
||||||
signature: 'tools.context7["resolve-library-id"](input: { libraryName: string }): Promise<string>',
|
signature: 'tools.context7["resolve-library-id"](input: { libraryName: string }): Promise<string>',
|
||||||
},
|
}])
|
||||||
])
|
expect(runtime.instructions()).toContain('tools.context7["resolve-library-id"](input: { libraryName: string }): Promise<string>')
|
||||||
expect(runtime.instructions()).toContain(
|
|
||||||
'tools.context7["resolve-library-id"](input: { libraryName: string }): Promise<string>',
|
|
||||||
)
|
|
||||||
|
|
||||||
const search = await Effect.runPromise(
|
const search = await Effect.runPromise(runtime.execute(`return await tools.$codemode.search({ query: "resolve library id" })`))
|
||||||
runtime.execute(`return await tools.$codemode.search({ query: "resolve library id" })`),
|
|
||||||
)
|
|
||||||
expect(search.ok).toBe(true)
|
expect(search.ok).toBe(true)
|
||||||
if (search.ok) {
|
if (search.ok) {
|
||||||
expect(search.value).toStrictEqual({
|
expect(search.value).toStrictEqual({
|
||||||
items: [
|
items: [{
|
||||||
{
|
|
||||||
path: 'tools.context7["resolve-library-id"]',
|
path: 'tools.context7["resolve-library-id"]',
|
||||||
description: "Resolve a library ID",
|
description: "Resolve a library ID",
|
||||||
signature: 'tools.context7["resolve-library-id"](input: {\n libraryName: string\n}): Promise<string>',
|
signature: 'tools.context7["resolve-library-id"](input: {\n libraryName: string\n}): Promise<string>',
|
||||||
},
|
}],
|
||||||
],
|
|
||||||
total: 1,
|
total: 1,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
const call = await Effect.runPromise(
|
const call = await Effect.runPromise(runtime.execute(`return await tools.context7["resolve-library-id"]({ libraryName: "TypeScript" })`))
|
||||||
runtime.execute(`return await tools.context7["resolve-library-id"]({ libraryName: "TypeScript" })`),
|
|
||||||
)
|
|
||||||
expect(call.ok).toBe(true)
|
expect(call.ok).toBe(true)
|
||||||
if (call.ok) expect(call.value).toBe("/resolved/TypeScript")
|
if (call.ok) expect(call.value).toBe("/resolved/TypeScript")
|
||||||
|
|
||||||
const exact = await Effect.runPromise(
|
const exact = await Effect.runPromise(runtime.execute(`return await tools.$codemode.search({ query: 'tools.context7["resolve-library-id"]' })`))
|
||||||
runtime.execute(`return await tools.$codemode.search({ query: 'tools.context7["resolve-library-id"]' })`),
|
|
||||||
)
|
|
||||||
expect(exact.ok).toBe(true)
|
expect(exact.ok).toBe(true)
|
||||||
if (exact.ok) expect((exact.value as { total: number }).total).toBe(1)
|
if (exact.ok) expect((exact.value as { total: number }).total).toBe(1)
|
||||||
})
|
})
|
||||||
@@ -617,9 +561,7 @@ describe("CodeMode public contract", () => {
|
|||||||
expect(instructions.indexOf("## Rules")).toBeLessThan(instructions.indexOf("## Syntax"))
|
expect(instructions.indexOf("## Rules")).toBeLessThan(instructions.indexOf("## Syntax"))
|
||||||
expect(instructions.indexOf("## Syntax")).toBeLessThan(instructions.indexOf("\n## Available tools (COMPLETE list"))
|
expect(instructions.indexOf("## Syntax")).toBeLessThan(instructions.indexOf("\n## Available tools (COMPLETE list"))
|
||||||
// The workflow carries the result-shape guidance; Rules only add content beyond it.
|
// The workflow carries the result-shape guidance; Rules only add content beyond it.
|
||||||
expect(instructions).toContain(
|
expect(instructions).toContain('`const data = typeof res === "string" ? JSON.parse(res) : res` - most tools return JSON as a string')
|
||||||
'`const data = typeof res === "string" ? JSON.parse(res) : res` - most tools return JSON as a string',
|
|
||||||
)
|
|
||||||
expect(instructions).toContain("Return only the fields you need")
|
expect(instructions).toContain("Return only the fields you need")
|
||||||
expect(instructions).toContain("raw payloads get truncated and waste context")
|
expect(instructions).toContain("raw payloads get truncated and waste context")
|
||||||
expect(instructions).toContain("`const res = await tools.<namespace>.<tool>(input)`")
|
expect(instructions).toContain("`const res = await tools.<namespace>.<tool>(input)`")
|
||||||
@@ -642,12 +584,8 @@ describe("CodeMode public contract", () => {
|
|||||||
expect(partial).toContain(
|
expect(partial).toContain(
|
||||||
'1. Find a tool (skip when it is already listed below): `const { items } = await tools.$codemode.search({ query: "<intent + key nouns>" })` - short phrases like "list issues" work best.',
|
'1. Find a tool (skip when it is already listed below): `const { items } = await tools.$codemode.search({ query: "<intent + key nouns>" })` - short phrases like "list issues" work best.',
|
||||||
)
|
)
|
||||||
expect(partial).toContain(
|
expect(partial).toContain("Only tools listed here or returned by `tools.$codemode.search` are available inside `tools`")
|
||||||
"Only tools listed here or returned by `tools.$codemode.search` are available inside `tools`",
|
expect(partial).toContain('- Browse one namespace: `await tools.$codemode.search({ query: "", namespace: "<name>" })`.')
|
||||||
)
|
|
||||||
expect(partial).toContain(
|
|
||||||
'- Browse one namespace: `await tools.$codemode.search({ query: "", namespace: "<name>" })`.',
|
|
||||||
)
|
|
||||||
expect(partial).not.toContain("total_count")
|
expect(partial).not.toContain("total_count")
|
||||||
expect(partial).not.toContain("tools.orders.lookup({")
|
expect(partial).not.toContain("tools.orders.lookup({")
|
||||||
})
|
})
|
||||||
@@ -666,9 +604,7 @@ describe("CodeMode public contract", () => {
|
|||||||
expect(instructions).not.toContain("instanceof Error")
|
expect(instructions).not.toContain("instanceof Error")
|
||||||
expect(instructions).not.toContain("splice")
|
expect(instructions).not.toContain("splice")
|
||||||
// The data-boundary note survives.
|
// The data-boundary note survives.
|
||||||
expect(instructions).toContain(
|
expect(instructions).toContain("Dates serialize to ISO strings at data boundaries; Map/Set/RegExp serialize to `{}`.")
|
||||||
"Dates serialize to ISO strings at data boundaries; Map/Set/RegExp serialize to `{}`.",
|
|
||||||
)
|
|
||||||
})
|
})
|
||||||
|
|
||||||
test("zero tools keep minimal sections and the no-tools notice", () => {
|
test("zero tools keep minimal sections and the no-tools notice", () => {
|
||||||
@@ -699,22 +635,18 @@ describe("CodeMode public contract", () => {
|
|||||||
tools: { thread: { uploadFile: upload, generateImage: generate }, orders: { lookup } },
|
tools: { thread: { uploadFile: upload, generateImage: generate }, orders: { lookup } },
|
||||||
discovery: { maxInlineCatalogTokens: 0 },
|
discovery: { maxInlineCatalogTokens: 0 },
|
||||||
})
|
})
|
||||||
expect(runtime.instructions()).toContain(
|
expect(runtime.instructions()).toContain("Available tools (PARTIAL - 0 of 3 shown; find the rest with tools.$codemode.search)")
|
||||||
"Available tools (PARTIAL - 0 of 3 shown; find the rest with tools.$codemode.search)",
|
|
||||||
)
|
|
||||||
expect(runtime.instructions()).toContain("- thread (2 tools, none shown)")
|
expect(runtime.instructions()).toContain("- thread (2 tools, none shown)")
|
||||||
expect(runtime.instructions()).toContain("- orders (1 tool, none shown)")
|
expect(runtime.instructions()).toContain("- orders (1 tool, none shown)")
|
||||||
expect(runtime.instructions()).toMatch(/\$codemode\.search/)
|
expect(runtime.instructions()).toMatch(/\$codemode\.search/)
|
||||||
expect(runtime.instructions()).not.toMatch(/tools\.thread\.uploadFile\(input/)
|
expect(runtime.instructions()).not.toMatch(/tools\.thread\.uploadFile\(input/)
|
||||||
|
|
||||||
const result = await Effect.runPromise(
|
const result = await Effect.runPromise(runtime.execute(`
|
||||||
runtime.execute(`
|
|
||||||
return await tools.$codemode.search({
|
return await tools.$codemode.search({
|
||||||
query: "send message attachment upload file to current Discord thread",
|
query: "send message attachment upload file to current Discord thread",
|
||||||
limit: 2
|
limit: 2
|
||||||
})
|
})
|
||||||
`),
|
`))
|
||||||
)
|
|
||||||
expect(result.ok).toBe(true)
|
expect(result.ok).toBe(true)
|
||||||
if (!result.ok) return
|
if (!result.ok) return
|
||||||
expect(result.value).toStrictEqual({
|
expect(result.value).toStrictEqual({
|
||||||
@@ -734,27 +666,19 @@ describe("CodeMode public contract", () => {
|
|||||||
})
|
})
|
||||||
expect(result.toolCalls).toStrictEqual([{ name: "$codemode.search" }])
|
expect(result.toolCalls).toStrictEqual([{ name: "$codemode.search" }])
|
||||||
|
|
||||||
const variants = await Effect.runPromise(
|
const variants = await Effect.runPromise(runtime.execute(`
|
||||||
runtime.execute(`
|
|
||||||
return await Promise.all([
|
return await Promise.all([
|
||||||
tools.$codemode.search({ query: "file" }),
|
tools.$codemode.search({ query: "file" }),
|
||||||
tools.$codemode.search({ query: "image" })
|
tools.$codemode.search({ query: "image" })
|
||||||
])
|
])
|
||||||
`),
|
`))
|
||||||
)
|
|
||||||
expect(variants.ok).toBe(true)
|
expect(variants.ok).toBe(true)
|
||||||
if (variants.ok) {
|
if (variants.ok) {
|
||||||
expect((variants.value as Array<{ items: Array<{ path: string }> }>)[0]?.items[0]?.path).toBe(
|
expect((variants.value as Array<{ items: Array<{ path: string }> }>)[0]?.items[0]?.path).toBe("tools.thread.uploadFile")
|
||||||
"tools.thread.uploadFile",
|
expect((variants.value as Array<{ items: Array<{ path: string }> }>)[1]?.items[0]?.path).toBe("tools.thread.generateImage")
|
||||||
)
|
|
||||||
expect((variants.value as Array<{ items: Array<{ path: string }> }>)[1]?.items[0]?.path).toBe(
|
|
||||||
"tools.thread.generateImage",
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const removed = await Effect.runPromise(
|
const removed = await Effect.runPromise(runtime.execute(`return await tools.$codemode.describe({ path: "thread.uploadFile" })`))
|
||||||
runtime.execute(`return await tools.$codemode.describe({ path: "thread.uploadFile" })`),
|
|
||||||
)
|
|
||||||
expect(removed.ok).toBe(false)
|
expect(removed.ok).toBe(false)
|
||||||
if (!removed.ok) expect(removed.error.kind).toBe("UnknownTool")
|
if (!removed.ok) expect(removed.error.kind).toBe("UnknownTool")
|
||||||
})
|
})
|
||||||
@@ -782,19 +706,15 @@ describe("CodeMode public contract", () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
for (const query of ["many.tool13", "tools.many.tool13"]) {
|
for (const query of ["many.tool13", "tools.many.tool13"]) {
|
||||||
const exact = await Effect.runPromise(
|
const exact = await Effect.runPromise(runtime.execute(`return await tools.$codemode.search({ query: ${JSON.stringify(query)} })`))
|
||||||
runtime.execute(`return await tools.$codemode.search({ query: ${JSON.stringify(query)} })`),
|
|
||||||
)
|
|
||||||
expect(exact.ok).toBe(true)
|
expect(exact.ok).toBe(true)
|
||||||
if (exact.ok) {
|
if (exact.ok) {
|
||||||
expect(exact.value).toStrictEqual({
|
expect(exact.value).toStrictEqual({
|
||||||
items: [
|
items: [{
|
||||||
{
|
|
||||||
path: "tools.many.tool13",
|
path: "tools.many.tool13",
|
||||||
description: "Numbered tool 13",
|
description: "Numbered tool 13",
|
||||||
signature: "tools.many.tool13(input: {\n id: string\n}): Promise<string>",
|
signature: "tools.many.tool13(input: {\n id: string\n}): Promise<string>",
|
||||||
},
|
}],
|
||||||
],
|
|
||||||
total: 1,
|
total: 1,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -817,23 +737,20 @@ describe("CodeMode public contract", () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
// Empty query + namespace browses just that namespace, alphabetical by path.
|
// Empty query + namespace browses just that namespace, alphabetical by path.
|
||||||
const browse = await Effect.runPromise(
|
const browse = await Effect.runPromise(runtime.execute(
|
||||||
runtime.execute(`return await tools.$codemode.search({ query: "", namespace: "github" })`),
|
`return await tools.$codemode.search({ query: "", namespace: "github" })`,
|
||||||
)
|
))
|
||||||
expect(browse.ok).toBe(true)
|
expect(browse.ok).toBe(true)
|
||||||
if (browse.ok) {
|
if (browse.ok) {
|
||||||
const value = browse.value as { items: Array<{ path: string }>; total: number }
|
const value = browse.value as { items: Array<{ path: string }>; total: number }
|
||||||
expect(value.total).toBe(2)
|
expect(value.total).toBe(2)
|
||||||
expect(value.items.map((item) => item.path)).toStrictEqual([
|
expect(value.items.map((item) => item.path)).toStrictEqual(["tools.github.create_issue", "tools.github.list_issues"])
|
||||||
"tools.github.create_issue",
|
|
||||||
"tools.github.list_issues",
|
|
||||||
])
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// A query + namespace ranks within that namespace only.
|
// A query + namespace ranks within that namespace only.
|
||||||
const scoped = await Effect.runPromise(
|
const scoped = await Effect.runPromise(runtime.execute(
|
||||||
runtime.execute(`return await tools.$codemode.search({ query: "issues", namespace: "linear" })`),
|
`return await tools.$codemode.search({ query: "issues", namespace: "linear" })`,
|
||||||
)
|
))
|
||||||
expect(scoped.ok).toBe(true)
|
expect(scoped.ok).toBe(true)
|
||||||
if (scoped.ok) {
|
if (scoped.ok) {
|
||||||
const value = scoped.value as { items: Array<{ path: string }>; total: number }
|
const value = scoped.value as { items: Array<{ path: string }>; total: number }
|
||||||
@@ -841,9 +758,9 @@ describe("CodeMode public contract", () => {
|
|||||||
expect(value.items[0]?.path).toBe("tools.linear.list_issues")
|
expect(value.items[0]?.path).toBe("tools.linear.list_issues")
|
||||||
}
|
}
|
||||||
|
|
||||||
const invalid = await Effect.runPromise(
|
const invalid = await Effect.runPromise(runtime.execute(
|
||||||
runtime.execute(`return await tools.$codemode.search({ query: "issues", namespace: 7 })`),
|
`return await tools.$codemode.search({ query: "issues", namespace: 7 })`,
|
||||||
)
|
))
|
||||||
expect(invalid.ok).toBe(false)
|
expect(invalid.ok).toBe(false)
|
||||||
if (!invalid.ok) expect(invalid.error.kind).toBe("InvalidToolInput")
|
if (!invalid.ok) expect(invalid.error.kind).toBe("InvalidToolInput")
|
||||||
})
|
})
|
||||||
@@ -868,9 +785,9 @@ describe("CodeMode public contract", () => {
|
|||||||
|
|
||||||
// "attachment" appears in neither path nor description - only in the input schema's
|
// "attachment" appears in neither path nor description - only in the input schema's
|
||||||
// property names, which the searchable text includes.
|
// property names, which the searchable text includes.
|
||||||
const byParameter = await Effect.runPromise(
|
const byParameter = await Effect.runPromise(runtime.execute(
|
||||||
runtime.execute(`return await tools.$codemode.search({ query: "attachment" })`),
|
`return await tools.$codemode.search({ query: "attachment" })`,
|
||||||
)
|
))
|
||||||
expect(byParameter.ok).toBe(true)
|
expect(byParameter.ok).toBe(true)
|
||||||
if (byParameter.ok) {
|
if (byParameter.ok) {
|
||||||
const value = byParameter.value as { items: Array<{ path: string }>; total: number }
|
const value = byParameter.value as { items: Array<{ path: string }>; total: number }
|
||||||
@@ -879,9 +796,9 @@ describe("CodeMode public contract", () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Substring matching: a partial word ("docum") still hits the description.
|
// Substring matching: a partial word ("docum") still hits the description.
|
||||||
const bySubstring = await Effect.runPromise(
|
const bySubstring = await Effect.runPromise(runtime.execute(
|
||||||
runtime.execute(`return await tools.$codemode.search({ query: "docum" })`),
|
`return await tools.$codemode.search({ query: "docum" })`,
|
||||||
)
|
))
|
||||||
expect(bySubstring.ok).toBe(true)
|
expect(bySubstring.ok).toBe(true)
|
||||||
if (bySubstring.ok) {
|
if (bySubstring.ok) {
|
||||||
const value = bySubstring.value as { items: Array<{ path: string }>; total: number }
|
const value = bySubstring.value as { items: Array<{ path: string }>; total: number }
|
||||||
@@ -908,9 +825,9 @@ describe("CodeMode public contract", () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
// "issues" still finds the singular-only tool (term OR singular(term) per field)...
|
// "issues" still finds the singular-only tool (term OR singular(term) per field)...
|
||||||
const plural = await Effect.runPromise(
|
const plural = await Effect.runPromise(runtime.execute(
|
||||||
runtime.execute(`return await tools.$codemode.search({ query: "issues", namespace: "tracker" })`),
|
`return await tools.$codemode.search({ query: "issues", namespace: "tracker" })`,
|
||||||
)
|
))
|
||||||
expect(plural.ok).toBe(true)
|
expect(plural.ok).toBe(true)
|
||||||
if (plural.ok) {
|
if (plural.ok) {
|
||||||
const value = plural.value as { items: Array<{ path: string }>; total: number }
|
const value = plural.value as { items: Array<{ path: string }>; total: number }
|
||||||
@@ -919,15 +836,14 @@ describe("CodeMode public contract", () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ...while a true "issues" path match still outranks the singular-only description match.
|
// ...while a true "issues" path match still outranks the singular-only description match.
|
||||||
const ranked = await Effect.runPromise(runtime.execute(`return await tools.$codemode.search({ query: "issues" })`))
|
const ranked = await Effect.runPromise(runtime.execute(
|
||||||
|
`return await tools.$codemode.search({ query: "issues" })`,
|
||||||
|
))
|
||||||
expect(ranked.ok).toBe(true)
|
expect(ranked.ok).toBe(true)
|
||||||
if (ranked.ok) {
|
if (ranked.ok) {
|
||||||
const value = ranked.value as { items: Array<{ path: string }>; total: number }
|
const value = ranked.value as { items: Array<{ path: string }>; total: number }
|
||||||
expect(value.total).toBe(2)
|
expect(value.total).toBe(2)
|
||||||
expect(value.items.map((item) => item.path)).toStrictEqual([
|
expect(value.items.map((item) => item.path)).toStrictEqual(["tools.github.list_issues", "tools.tracker.fetch_all"])
|
||||||
"tools.github.list_issues",
|
|
||||||
"tools.tracker.fetch_all",
|
|
||||||
])
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -966,12 +882,8 @@ describe("CodeMode public contract", () => {
|
|||||||
run: () => Effect.succeed("ok"),
|
run: () => Effect.succeed("ok"),
|
||||||
})
|
})
|
||||||
const expensive = Tool.make({
|
const expensive = Tool.make({
|
||||||
description:
|
description: "An expensive tool whose description alone consumes far more than the remaining inline catalog byte budget for this runtime",
|
||||||
"An expensive tool whose description alone consumes far more than the remaining inline catalog byte budget for this runtime",
|
input: Schema.Struct({ someRatherLongParameterName: Schema.String, anotherEvenLongerParameterName: Schema.Number }),
|
||||||
input: Schema.Struct({
|
|
||||||
someRatherLongParameterName: Schema.String,
|
|
||||||
anotherEvenLongerParameterName: Schema.Number,
|
|
||||||
}),
|
|
||||||
output: Schema.String,
|
output: Schema.String,
|
||||||
run: () => Effect.succeed("ok"),
|
run: () => Effect.succeed("ok"),
|
||||||
})
|
})
|
||||||
@@ -984,9 +896,7 @@ describe("CodeMode public contract", () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
const instructions = runtime.instructions()
|
const instructions = runtime.instructions()
|
||||||
expect(instructions).toContain(
|
expect(instructions).toContain("Available tools (PARTIAL - 2 of 3 shown; find the rest with tools.$codemode.search)")
|
||||||
"Available tools (PARTIAL - 2 of 3 shown; find the rest with tools.$codemode.search)",
|
|
||||||
)
|
|
||||||
expect(instructions).toContain("- alpha (2 tools, 1 shown)")
|
expect(instructions).toContain("- alpha (2 tools, 1 shown)")
|
||||||
expect(instructions).toContain(" - tools.alpha.cheap(input: { q: string }): Promise<string> // Cheap")
|
expect(instructions).toContain(" - tools.alpha.cheap(input: { q: string }): Promise<string> // Cheap")
|
||||||
expect(instructions).not.toContain("tools.alpha.expensive(")
|
expect(instructions).not.toContain("tools.alpha.expensive(")
|
||||||
@@ -1002,8 +912,7 @@ describe("CodeMode public contract", () => {
|
|||||||
description: "Double a number",
|
description: "Double a number",
|
||||||
input: Schema.Struct({ value: Schema.NumberFromString }),
|
input: Schema.Struct({ value: Schema.NumberFromString }),
|
||||||
output: Schema.NumberFromString,
|
output: Schema.NumberFromString,
|
||||||
run: ({ value }) =>
|
run: ({ value }) => Effect.sync(() => {
|
||||||
Effect.sync(() => {
|
|
||||||
observed.push(value)
|
observed.push(value)
|
||||||
return String(value * 2)
|
return String(value * 2)
|
||||||
}),
|
}),
|
||||||
@@ -1025,11 +934,9 @@ describe("CodeMode public contract", () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
test("returns JSON-safe data and normalizes undefined to null", async () => {
|
test("returns JSON-safe data and normalizes undefined to null", async () => {
|
||||||
const result = await Effect.runPromise(
|
const result = await Effect.runPromise(CodeMode.execute({
|
||||||
CodeMode.execute({
|
|
||||||
code: `return { top: undefined, nested: [1, undefined] }`,
|
code: `return { top: undefined, nested: [1, undefined] }`,
|
||||||
}),
|
}))
|
||||||
)
|
|
||||||
expect(result).toStrictEqual({
|
expect(result).toStrictEqual({
|
||||||
ok: true,
|
ok: true,
|
||||||
value: { top: null, nested: [1, null] },
|
value: { top: null, nested: [1, null] },
|
||||||
@@ -1040,20 +947,18 @@ describe("CodeMode public contract", () => {
|
|||||||
|
|
||||||
test("rejects invalid configuration and discovery limits", async () => {
|
test("rejects invalid configuration and discovery limits", async () => {
|
||||||
expect(() => CodeMode.execute({ code: "return 1", limits: { timeoutMs: 0 } })).toThrow(RangeError)
|
expect(() => CodeMode.execute({ code: "return 1", limits: { timeoutMs: 0 } })).toThrow(RangeError)
|
||||||
expect(() => CodeMode.execute({ code: "return 1", limits: { timeoutMs: Number.POSITIVE_INFINITY } })).toThrow(
|
expect(() => CodeMode.execute({ code: "return 1", limits: { timeoutMs: Number.POSITIVE_INFINITY } })).toThrow(RangeError)
|
||||||
RangeError,
|
|
||||||
)
|
|
||||||
expect(() => CodeMode.execute({ code: "return 1", limits: { maxToolCalls: -1 } })).toThrow(RangeError)
|
expect(() => CodeMode.execute({ code: "return 1", limits: { maxToolCalls: -1 } })).toThrow(RangeError)
|
||||||
expect(() => CodeMode.execute({ code: "return 1", limits: { maxOutputBytes: -1 } })).toThrow(RangeError)
|
expect(() => CodeMode.execute({ code: "return 1", limits: { maxOutputBytes: -1 } })).toThrow(RangeError)
|
||||||
|
|
||||||
expect(() => CodeMode.make({ tools, discovery: { maxInlineCatalogTokens: -1 } })).toThrow(RangeError)
|
expect(() => CodeMode.make({ tools, discovery: { maxInlineCatalogTokens: -1 } })).toThrow(RangeError)
|
||||||
|
|
||||||
const result = await Effect.runPromise(
|
const result = await Effect.runPromise(CodeMode.make({
|
||||||
CodeMode.make({
|
|
||||||
tools,
|
tools,
|
||||||
discovery: { maxInlineCatalogTokens: 0 },
|
discovery: { maxInlineCatalogTokens: 0 },
|
||||||
}).execute(`return await tools.$codemode.search({ query: "order", limit: 0.5 })`),
|
}).execute(
|
||||||
)
|
`return await tools.$codemode.search({ query: "order", limit: 0.5 })`,
|
||||||
|
))
|
||||||
expect(result.ok).toBe(false)
|
expect(result.ok).toBe(false)
|
||||||
if (result.ok) return
|
if (result.ok) return
|
||||||
expect(result.error.kind).toBe("InvalidToolInput")
|
expect(result.error.kind).toBe("InvalidToolInput")
|
||||||
@@ -1074,16 +979,14 @@ describe("CodeMode public contract", () => {
|
|||||||
output: Schema.Number,
|
output: Schema.Number,
|
||||||
run: () => Effect.succeed(1),
|
run: () => Effect.succeed(1),
|
||||||
})
|
})
|
||||||
const result = await Effect.runPromise(
|
const result = await Effect.runPromise(CodeMode.execute({
|
||||||
CodeMode.execute({
|
|
||||||
tools: { host: { count: counter } },
|
tools: { host: { count: counter } },
|
||||||
code: `
|
code: `
|
||||||
let total = 0
|
let total = 0
|
||||||
for (let i = 0; i < 150; i += 1) total += await tools.host.count({})
|
for (let i = 0; i < 150; i += 1) total += await tools.host.count({})
|
||||||
return total
|
return total
|
||||||
`,
|
`,
|
||||||
}),
|
}))
|
||||||
)
|
|
||||||
expect(result).toMatchObject({ ok: true, value: 150 })
|
expect(result).toMatchObject({ ok: true, value: 150 })
|
||||||
if (result.ok) expect(result.toolCalls.length).toBe(150)
|
if (result.ok) expect(result.toolCalls.length).toBe(150)
|
||||||
})
|
})
|
||||||
@@ -1105,6 +1008,8 @@ describe("CodeMode public contract", () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
test("reserves the discovery namespace", () => {
|
test("reserves the discovery namespace", () => {
|
||||||
expect(() => CodeMode.make({ tools: { $codemode: { lookup } } })).toThrow(/reserved for CodeMode discovery tools/)
|
expect(() => CodeMode.make({ tools: { $codemode: { lookup } } })).toThrow(
|
||||||
|
/reserved for CodeMode discovery tools/,
|
||||||
|
)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -36,12 +36,10 @@ const error = async (code: string) => {
|
|||||||
|
|
||||||
describe("Object.keys over tool references", () => {
|
describe("Object.keys over tool references", () => {
|
||||||
test("enumerates top-level namespaces (the transcript program)", async () => {
|
test("enumerates top-level namespaces (the transcript program)", async () => {
|
||||||
expect(
|
expect(await value(`
|
||||||
await value(`
|
|
||||||
const namespaces = Object.keys(tools)
|
const namespaces = Object.keys(tools)
|
||||||
return { namespaces, count: namespaces.length }
|
return { namespaces, count: namespaces.length }
|
||||||
`),
|
`)).toEqual({ namespaces: ["github", "memory", "playwright"], count: 3 })
|
||||||
).toEqual({ namespaces: ["github", "memory", "playwright"], count: 3 })
|
|
||||||
})
|
})
|
||||||
|
|
||||||
test("enumerates tool names at a nested namespace", async () => {
|
test("enumerates tool names at a nested namespace", async () => {
|
||||||
@@ -94,8 +92,7 @@ describe("Object.keys over arrays", () => {
|
|||||||
|
|
||||||
describe("for...in", () => {
|
describe("for...in", () => {
|
||||||
test("iterates own enumerable keys of a plain object with break/continue", async () => {
|
test("iterates own enumerable keys of a plain object with break/continue", async () => {
|
||||||
expect(
|
expect(await value(`
|
||||||
await value(`
|
|
||||||
const seen = []
|
const seen = []
|
||||||
for (const key in { a: 1, b: 2, c: 3, d: 4 }) {
|
for (const key in { a: 1, b: 2, c: 3, d: 4 }) {
|
||||||
if (key === "b") continue
|
if (key === "b") continue
|
||||||
@@ -103,50 +100,41 @@ describe("for...in", () => {
|
|||||||
seen.push(key)
|
seen.push(key)
|
||||||
}
|
}
|
||||||
return seen
|
return seen
|
||||||
`),
|
`)).toEqual(["a", "c"])
|
||||||
).toEqual(["a", "c"])
|
|
||||||
})
|
})
|
||||||
|
|
||||||
test("iterates index strings over arrays", async () => {
|
test("iterates index strings over arrays", async () => {
|
||||||
expect(
|
expect(await value(`
|
||||||
await value(`
|
|
||||||
const indexes = []
|
const indexes = []
|
||||||
for (const i in ["x", "y", "z"]) {
|
for (const i in ["x", "y", "z"]) {
|
||||||
if (i === "2") break
|
if (i === "2") break
|
||||||
indexes.push(i)
|
indexes.push(i)
|
||||||
}
|
}
|
||||||
return indexes
|
return indexes
|
||||||
`),
|
`)).toEqual(["0", "1"])
|
||||||
).toEqual(["0", "1"])
|
|
||||||
})
|
})
|
||||||
|
|
||||||
test("supports let declarations and bare identifiers", async () => {
|
test("supports let declarations and bare identifiers", async () => {
|
||||||
expect(
|
expect(await value(`
|
||||||
await value(`
|
|
||||||
let last = ""
|
let last = ""
|
||||||
for (let key in { a: 1, b: 2 }) last = key
|
for (let key in { a: 1, b: 2 }) last = key
|
||||||
return last
|
return last
|
||||||
`),
|
`)).toBe("b")
|
||||||
).toBe("b")
|
expect(await value(`
|
||||||
expect(
|
|
||||||
await value(`
|
|
||||||
let key = "before"
|
let key = "before"
|
||||||
for (key in { only: 1 }) {}
|
for (key in { only: 1 }) {}
|
||||||
return key
|
return key
|
||||||
`),
|
`)).toBe("only")
|
||||||
).toBe("only")
|
|
||||||
})
|
})
|
||||||
|
|
||||||
test("enumerates namespaces and tools from the host tool tree", async () => {
|
test("enumerates namespaces and tools from the host tool tree", async () => {
|
||||||
expect(
|
expect(await value(`
|
||||||
await value(`
|
|
||||||
const names = []
|
const names = []
|
||||||
for (const ns in tools) {
|
for (const ns in tools) {
|
||||||
for (const name in tools[ns]) names.push(ns + "." + name)
|
for (const name in tools[ns]) names.push(ns + "." + name)
|
||||||
}
|
}
|
||||||
return names
|
return names
|
||||||
`),
|
`)).toEqual(["github.list_issues", "github.get_issue", "memory.search", "playwright.navigate"])
|
||||||
).toEqual(["github.list_issues", "github.get_issue", "memory.search", "playwright.navigate"])
|
|
||||||
})
|
})
|
||||||
|
|
||||||
test("unsupported values fail with a hint at for...of and Object.keys", async () => {
|
test("unsupported values fail with a hint at for...of and Object.keys", async () => {
|
||||||
|
|||||||
@@ -143,40 +143,21 @@ describe("H1: NaN/Infinity flow as intermediates and normalize to null at the bo
|
|||||||
|
|
||||||
describe("Error values and instanceof", () => {
|
describe("Error values and instanceof", () => {
|
||||||
test("new Error carries name/message and is instanceof Error", async () => {
|
test("new Error carries name/message and is instanceof Error", async () => {
|
||||||
expect(await value(`const e = new Error("boom"); return [e instanceof Error, e.name, e.message]`)).toEqual([
|
expect(await value(`const e = new Error("boom"); return [e instanceof Error, e.name, e.message]`)).toEqual([true, "Error", "boom"])
|
||||||
true,
|
|
||||||
"Error",
|
|
||||||
"boom",
|
|
||||||
])
|
|
||||||
})
|
})
|
||||||
|
|
||||||
test("Error without new behaves like new Error", async () => {
|
test("Error without new behaves like new Error", async () => {
|
||||||
expect(await value(`const e = Error("plain"); return [e instanceof Error, e.name, e.message]`)).toEqual([
|
expect(await value(`const e = Error("plain"); return [e instanceof Error, e.name, e.message]`)).toEqual([true, "Error", "plain"])
|
||||||
true,
|
expect(await value(`const e = new Error(); return [e.name, e.message, e instanceof Error]`)).toEqual(["Error", "", true])
|
||||||
"Error",
|
|
||||||
"plain",
|
|
||||||
])
|
|
||||||
expect(await value(`const e = new Error(); return [e.name, e.message, e instanceof Error]`)).toEqual([
|
|
||||||
"Error",
|
|
||||||
"",
|
|
||||||
true,
|
|
||||||
])
|
|
||||||
})
|
})
|
||||||
|
|
||||||
test("specific error types are instanceof themselves and Error, not each other", async () => {
|
test("specific error types are instanceof themselves and Error, not each other", async () => {
|
||||||
expect(
|
expect(await value(`const e = new TypeError("t"); return [e instanceof TypeError, e instanceof Error, e instanceof RangeError]`)).toEqual([true, true, false])
|
||||||
await value(
|
|
||||||
`const e = new TypeError("t"); return [e instanceof TypeError, e instanceof Error, e instanceof RangeError]`,
|
|
||||||
),
|
|
||||||
).toEqual([true, true, false])
|
|
||||||
expect(await value(`return new Error("e") instanceof TypeError`)).toBe(false)
|
expect(await value(`return new Error("e") instanceof TypeError`)).toBe(false)
|
||||||
})
|
})
|
||||||
|
|
||||||
test("thrown errors keep instanceof through try/catch", async () => {
|
test("thrown errors keep instanceof through try/catch", async () => {
|
||||||
expect(await value(`try { throw new Error("x") } catch (e) { return [e instanceof Error, e.message] }`)).toEqual([
|
expect(await value(`try { throw new Error("x") } catch (e) { return [e instanceof Error, e.message] }`)).toEqual([true, "x"])
|
||||||
true,
|
|
||||||
"x",
|
|
||||||
])
|
|
||||||
})
|
})
|
||||||
|
|
||||||
test("interpreter runtime failures are caught as Error values", async () => {
|
test("interpreter runtime failures are caught as Error values", async () => {
|
||||||
@@ -187,48 +168,33 @@ describe("Error values and instanceof", () => {
|
|||||||
test("caught failures carry the constructor name the real-JS failure would have", async () => {
|
test("caught failures carry the constructor name the real-JS failure would have", async () => {
|
||||||
// JSON.parse throws SyntaxError: name and specific-instanceof both carry through, and the
|
// JSON.parse throws SyntaxError: name and specific-instanceof both carry through, and the
|
||||||
// message keeps the engine's position detail.
|
// message keeps the engine's position detail.
|
||||||
expect(
|
expect(await value(`
|
||||||
await value(`
|
|
||||||
try { JSON.parse("{oops") } catch (e) {
|
try { JSON.parse("{oops") } catch (e) {
|
||||||
return [e.name, e instanceof SyntaxError, e instanceof Error, e instanceof TypeError, e.message.includes("JSON")]
|
return [e.name, e instanceof SyntaxError, e instanceof Error, e instanceof TypeError, e.message.includes("JSON")]
|
||||||
}
|
}
|
||||||
`),
|
`)).toEqual(["SyntaxError", true, true, false, true])
|
||||||
).toEqual(["SyntaxError", true, true, false, true])
|
expect(await value(`try { undeclared() } catch (e) { return [e.name, e instanceof ReferenceError] }`))
|
||||||
expect(await value(`try { undeclared() } catch (e) { return [e.name, e instanceof ReferenceError] }`)).toEqual([
|
.toEqual(["ReferenceError", true])
|
||||||
"ReferenceError",
|
expect(await value(`try { const c = 1; c = 2 } catch (e) { return [e.name, e instanceof TypeError] }`))
|
||||||
true,
|
.toEqual(["TypeError", true])
|
||||||
])
|
expect(await value(`try { "a".normalize("NOPE") } catch (e) { return [e.name, e instanceof RangeError] }`))
|
||||||
expect(await value(`try { const c = 1; c = 2 } catch (e) { return [e.name, e instanceof TypeError] }`)).toEqual([
|
.toEqual(["RangeError", true])
|
||||||
"TypeError",
|
expect(await value(`try { "a".match("(") } catch (e) { return [e.name, e instanceof SyntaxError] }`))
|
||||||
true,
|
.toEqual(["SyntaxError", true])
|
||||||
])
|
expect(await value(`try { new RegExp("(") } catch (e) { return [e.name, e instanceof SyntaxError] }`))
|
||||||
expect(await value(`try { "a".normalize("NOPE") } catch (e) { return [e.name, e instanceof RangeError] }`)).toEqual(
|
.toEqual(["SyntaxError", true])
|
||||||
["RangeError", true],
|
|
||||||
)
|
|
||||||
expect(await value(`try { "a".match("(") } catch (e) { return [e.name, e instanceof SyntaxError] }`)).toEqual([
|
|
||||||
"SyntaxError",
|
|
||||||
true,
|
|
||||||
])
|
|
||||||
expect(await value(`try { new RegExp("(") } catch (e) { return [e.name, e instanceof SyntaxError] }`)).toEqual([
|
|
||||||
"SyntaxError",
|
|
||||||
true,
|
|
||||||
])
|
|
||||||
})
|
})
|
||||||
|
|
||||||
test("diagnostics without a specific real-JS analogue are named plain Error", async () => {
|
test("diagnostics without a specific real-JS analogue are named plain Error", async () => {
|
||||||
expect(await value(`try { JSON.parse(5) } catch (e) { return [e.name, e instanceof Error] }`)).toEqual([
|
expect(await value(`try { JSON.parse(5) } catch (e) { return [e.name, e instanceof Error] }`))
|
||||||
"Error",
|
.toEqual(["Error", true])
|
||||||
true,
|
|
||||||
])
|
|
||||||
})
|
})
|
||||||
|
|
||||||
test("Promise.allSettled rejection reasons are Error values", async () => {
|
test("Promise.allSettled rejection reasons are Error values", async () => {
|
||||||
expect(
|
expect(await value(`
|
||||||
await value(`
|
|
||||||
const settled = await Promise.allSettled([Promise.reject(new Error("b"))])
|
const settled = await Promise.allSettled([Promise.reject(new Error("b"))])
|
||||||
return [settled[0].reason instanceof Error, settled[0].reason.message]
|
return [settled[0].reason instanceof Error, settled[0].reason.message]
|
||||||
`),
|
`)).toEqual([true, "b"])
|
||||||
).toEqual([true, "b"])
|
|
||||||
})
|
})
|
||||||
|
|
||||||
test("non-error thrown values are not instanceof Error", async () => {
|
test("non-error thrown values are not instanceof Error", async () => {
|
||||||
@@ -237,11 +203,7 @@ describe("Error values and instanceof", () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
test("plain data is never instanceof Error", async () => {
|
test("plain data is never instanceof Error", async () => {
|
||||||
expect(await value(`return [({}) instanceof Error, "s" instanceof Error, null instanceof Error]`)).toEqual([
|
expect(await value(`return [({}) instanceof Error, "s" instanceof Error, null instanceof Error]`)).toEqual([false, false, false])
|
||||||
false,
|
|
||||||
false,
|
|
||||||
false,
|
|
||||||
])
|
|
||||||
})
|
})
|
||||||
|
|
||||||
test("error values still serialize as plain { name, message } data", async () => {
|
test("error values still serialize as plain { name, message } data", async () => {
|
||||||
@@ -265,29 +227,17 @@ describe("Error values and instanceof", () => {
|
|||||||
|
|
||||||
describe("array methods: splice, fill, copyWithin, keys/values/entries", () => {
|
describe("array methods: splice, fill, copyWithin, keys/values/entries", () => {
|
||||||
test("splice removes in place and returns the removed elements", async () => {
|
test("splice removes in place and returns the removed elements", async () => {
|
||||||
expect(await value(`const a = [1,2,3,4]; const removed = a.splice(1, 2); return { removed, a }`)).toEqual({
|
expect(await value(`const a = [1,2,3,4]; const removed = a.splice(1, 2); return { removed, a }`)).toEqual({ removed: [2, 3], a: [1, 4] })
|
||||||
removed: [2, 3],
|
|
||||||
a: [1, 4],
|
|
||||||
})
|
|
||||||
})
|
})
|
||||||
|
|
||||||
test("splice inserts new elements at the cut", async () => {
|
test("splice inserts new elements at the cut", async () => {
|
||||||
expect(await value(`const a = ["a","d"]; a.splice(1, 0, "b", "c"); return a`)).toEqual(["a", "b", "c", "d"])
|
expect(await value(`const a = ["a","d"]; a.splice(1, 0, "b", "c"); return a`)).toEqual(["a", "b", "c", "d"])
|
||||||
expect(await value(`const a = [1,2,3]; const removed = a.splice(1, 1, "x"); return { removed, a }`)).toEqual({
|
expect(await value(`const a = [1,2,3]; const removed = a.splice(1, 1, "x"); return { removed, a }`)).toEqual({ removed: [2], a: [1, "x", 3] })
|
||||||
removed: [2],
|
|
||||||
a: [1, "x", 3],
|
|
||||||
})
|
|
||||||
})
|
})
|
||||||
|
|
||||||
test("splice with one argument removes to the end; negative start counts back", async () => {
|
test("splice with one argument removes to the end; negative start counts back", async () => {
|
||||||
expect(await value(`const a = [1,2,3]; const removed = a.splice(1); return { removed, a }`)).toEqual({
|
expect(await value(`const a = [1,2,3]; const removed = a.splice(1); return { removed, a }`)).toEqual({ removed: [2, 3], a: [1] })
|
||||||
removed: [2, 3],
|
expect(await value(`const a = [1,2,3]; const removed = a.splice(-1); return { removed, a }`)).toEqual({ removed: [3], a: [1, 2] })
|
||||||
a: [1],
|
|
||||||
})
|
|
||||||
expect(await value(`const a = [1,2,3]; const removed = a.splice(-1); return { removed, a }`)).toEqual({
|
|
||||||
removed: [3],
|
|
||||||
a: [1, 2],
|
|
||||||
})
|
|
||||||
})
|
})
|
||||||
|
|
||||||
test("splice rejects inserting a container into itself", async () => {
|
test("splice rejects inserting a container into itself", async () => {
|
||||||
@@ -308,13 +258,11 @@ describe("array methods: splice, fill, copyWithin, keys/values/entries", () => {
|
|||||||
test("keys/values/entries return arrays usable with for...of and spread", async () => {
|
test("keys/values/entries return arrays usable with for...of and spread", async () => {
|
||||||
expect(await value(`return [...["x","y","z"].keys()]`)).toEqual([0, 1, 2])
|
expect(await value(`return [...["x","y","z"].keys()]`)).toEqual([0, 1, 2])
|
||||||
expect(await value(`return ["x","y"].values()`)).toEqual(["x", "y"])
|
expect(await value(`return ["x","y"].values()`)).toEqual(["x", "y"])
|
||||||
expect(
|
expect(await value(`
|
||||||
await value(`
|
|
||||||
const out = []
|
const out = []
|
||||||
for (const [index, item] of ["a","b"].entries()) out.push(index + ":" + item)
|
for (const [index, item] of ["a","b"].entries()) out.push(index + ":" + item)
|
||||||
return out
|
return out
|
||||||
`),
|
`)).toEqual(["0:a", "1:b"])
|
||||||
).toEqual(["0:a", "1:b"])
|
|
||||||
expect(await value(`return [...[7].entries()]`)).toEqual([[0, 7]])
|
expect(await value(`return [...[7].entries()]`)).toEqual([[0, 7]])
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
@@ -352,33 +300,40 @@ describe("compound assignment matches its binary operator", () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
test("sandbox Date += concatenates its string form, like d = d + 1", async () => {
|
test("sandbox Date += concatenates its string form, like d = d + 1", async () => {
|
||||||
const result = await pair(`let d = new Date(1000); d += 1; return d`, `let d = new Date(1000); d = d + 1; return d`)
|
const result = await pair(
|
||||||
|
`let d = new Date(1000); d += 1; return d`,
|
||||||
|
`let d = new Date(1000); d = d + 1; return d`,
|
||||||
|
)
|
||||||
expect(result).toBe("1970-01-01T00:00:01.000Z1")
|
expect(result).toBe("1970-01-01T00:00:01.000Z1")
|
||||||
})
|
})
|
||||||
|
|
||||||
test("sandbox Date numeric compound ops use its time value", async () => {
|
test("sandbox Date numeric compound ops use its time value", async () => {
|
||||||
expect(
|
expect(await pair(
|
||||||
await pair(`let d = new Date(1000); d -= 400; return d`, `let d = new Date(1000); d = d - 400; return d`),
|
`let d = new Date(1000); d -= 400; return d`,
|
||||||
).toBe(600)
|
`let d = new Date(1000); d = d - 400; return d`,
|
||||||
expect(await pair(`let d = new Date(1000); d /= 4; return d`, `let d = new Date(1000); d = d / 4; return d`)).toBe(
|
)).toBe(600)
|
||||||
250,
|
expect(await pair(
|
||||||
)
|
`let d = new Date(1000); d /= 4; return d`,
|
||||||
|
`let d = new Date(1000); d = d / 4; return d`,
|
||||||
|
)).toBe(250)
|
||||||
})
|
})
|
||||||
|
|
||||||
test("string += object/array matches x = x + obj", async () => {
|
test("string += object/array matches x = x + obj", async () => {
|
||||||
expect(await pair(`let x = "a"; x += { b: 1 }; return x`, `let x = "a"; x = x + { b: 1 }; return x`)).toBe(
|
expect(await pair(
|
||||||
"a[object Object]",
|
`let x = "a"; x += { b: 1 }; return x`,
|
||||||
)
|
`let x = "a"; x = x + { b: 1 }; return x`,
|
||||||
expect(await pair(`let x = "a"; x += [1, 2]; return x`, `let x = "a"; x = x + [1, 2]; return x`)).toBe("a1,2")
|
)).toBe("a[object Object]")
|
||||||
|
expect(await pair(
|
||||||
|
`let x = "a"; x += [1, 2]; return x`,
|
||||||
|
`let x = "a"; x = x + [1, 2]; return x`,
|
||||||
|
)).toBe("a1,2")
|
||||||
})
|
})
|
||||||
|
|
||||||
test("compound assignment through a member target coerces the same way", async () => {
|
test("compound assignment through a member target coerces the same way", async () => {
|
||||||
expect(
|
expect(await pair(
|
||||||
await pair(
|
|
||||||
`const o = { s: "t" }; o.s += new Date(0); return o.s`,
|
`const o = { s: "t" }; o.s += new Date(0); return o.s`,
|
||||||
`const o = { s: "t" }; o.s = o.s + new Date(0); return o.s`,
|
`const o = { s: "t" }; o.s = o.s + new Date(0); return o.s`,
|
||||||
),
|
)).toBe("t1970-01-01T00:00:00.000Z")
|
||||||
).toBe("t1970-01-01T00:00:00.000Z")
|
|
||||||
})
|
})
|
||||||
|
|
||||||
test("numeric and string compound operators sweep identically to their expansions", async () => {
|
test("numeric and string compound operators sweep identically to their expansions", async () => {
|
||||||
|
|||||||
@@ -31,14 +31,10 @@ const sleepyTool = (trace: Trace) =>
|
|||||||
trace.active -= 1
|
trace.active -= 1
|
||||||
trace.completed += 1
|
trace.completed += 1
|
||||||
return id
|
return id
|
||||||
}).pipe(
|
}).pipe(Effect.onInterrupt(() => Effect.sync(() => {
|
||||||
Effect.onInterrupt(() =>
|
|
||||||
Effect.sync(() => {
|
|
||||||
trace.active -= 1
|
trace.active -= 1
|
||||||
trace.interrupted += 1
|
trace.interrupted += 1
|
||||||
}),
|
}))),
|
||||||
),
|
|
||||||
),
|
|
||||||
})
|
})
|
||||||
|
|
||||||
const failingTool = Tool.make({
|
const failingTool = Tool.make({
|
||||||
@@ -50,13 +46,11 @@ const failingTool = Tool.make({
|
|||||||
|
|
||||||
const run = (code: string, options: { trace?: Trace; limits?: ExecutionLimits } = {}): Promise<ExecuteResult> => {
|
const run = (code: string, options: { trace?: Trace; limits?: ExecutionLimits } = {}): Promise<ExecuteResult> => {
|
||||||
const trace = options.trace ?? makeTrace()
|
const trace = options.trace ?? makeTrace()
|
||||||
return Effect.runPromise(
|
return Effect.runPromise(CodeMode.execute({
|
||||||
CodeMode.execute({
|
|
||||||
tools: { host: { sleepy: sleepyTool(trace), fail: failingTool } },
|
tools: { host: { sleepy: sleepyTool(trace), fail: failingTool } },
|
||||||
code,
|
code,
|
||||||
...(options.limits ? { limits: options.limits } : {}),
|
...(options.limits ? { limits: options.limits } : {}),
|
||||||
}),
|
}))
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const value = async (code: string, options: { trace?: Trace; limits?: ExecutionLimits } = {}) => {
|
const value = async (code: string, options: { trace?: Trace; limits?: ExecutionLimits } = {}) => {
|
||||||
@@ -127,8 +121,7 @@ describe("first-class promise values", () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
test("an awaited failure is catchable exactly like a synchronous throw", async () => {
|
test("an awaited failure is catchable exactly like a synchronous throw", async () => {
|
||||||
expect(
|
expect(await value(`
|
||||||
await value(`
|
|
||||||
const p = tools.host.fail({})
|
const p = tools.host.fail({})
|
||||||
try {
|
try {
|
||||||
await p
|
await p
|
||||||
@@ -136,8 +129,7 @@ describe("first-class promise values", () => {
|
|||||||
} catch (e) {
|
} catch (e) {
|
||||||
return e.message
|
return e.message
|
||||||
}
|
}
|
||||||
`),
|
`)).toBe("Lookup refused")
|
||||||
).toBe("Lookup refused")
|
|
||||||
})
|
})
|
||||||
|
|
||||||
test("a fire-and-forget call completes before the execution ends", async () => {
|
test("a fire-and-forget call completes before the execution ends", async () => {
|
||||||
@@ -194,24 +186,20 @@ describe("promises at data boundaries", () => {
|
|||||||
|
|
||||||
describe("Promise.all over arbitrary arrays", () => {
|
describe("Promise.all over arbitrary arrays", () => {
|
||||||
test("mixes promises and plain values, preserving order", async () => {
|
test("mixes promises and plain values, preserving order", async () => {
|
||||||
expect(
|
expect(await value(`
|
||||||
await value(`
|
|
||||||
return await Promise.all([tools.host.sleepy({ id: 1 }), "plain", tools.host.sleepy({ id: 2 }), 42])
|
return await Promise.all([tools.host.sleepy({ id: 1 }), "plain", tools.host.sleepy({ id: 2 }), 42])
|
||||||
`),
|
`)).toEqual([1, "plain", 2, 42])
|
||||||
).toEqual([1, "plain", 2, 42])
|
|
||||||
})
|
})
|
||||||
|
|
||||||
test("accepts arrays built beforehand, passed as identifiers, and spread elements", async () => {
|
test("accepts arrays built beforehand, passed as identifiers, and spread elements", async () => {
|
||||||
expect(
|
expect(await value(`
|
||||||
await value(`
|
|
||||||
const calls = []
|
const calls = []
|
||||||
calls.push(tools.host.sleepy({ id: 1 }))
|
calls.push(tools.host.sleepy({ id: 1 }))
|
||||||
calls.push(7)
|
calls.push(7)
|
||||||
const more = [tools.host.sleepy({ id: 2 })]
|
const more = [tools.host.sleepy({ id: 2 })]
|
||||||
const batch = [...calls, ...more, "x"]
|
const batch = [...calls, ...more, "x"]
|
||||||
return await Promise.all(batch)
|
return await Promise.all(batch)
|
||||||
`),
|
`)).toEqual([1, 7, 2, "x"])
|
||||||
).toEqual([1, 7, 2, "x"])
|
|
||||||
})
|
})
|
||||||
|
|
||||||
test("runs items.map tool calls in parallel", async () => {
|
test("runs items.map tool calls in parallel", async () => {
|
||||||
@@ -250,16 +238,14 @@ describe("Promise.all over arbitrary arrays", () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
test("rejects with the first failure, catchable in-program", async () => {
|
test("rejects with the first failure, catchable in-program", async () => {
|
||||||
expect(
|
expect(await value(`
|
||||||
await value(`
|
|
||||||
try {
|
try {
|
||||||
await Promise.all([tools.host.sleepy({ id: 1 }), tools.host.fail({})])
|
await Promise.all([tools.host.sleepy({ id: 1 }), tools.host.fail({})])
|
||||||
return "no"
|
return "no"
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
return e.message
|
return e.message
|
||||||
}
|
}
|
||||||
`),
|
`)).toBe("Lookup refused")
|
||||||
).toBe("Lookup refused")
|
|
||||||
})
|
})
|
||||||
|
|
||||||
test("a non-collection argument is a clear error", async () => {
|
test("a non-collection argument is a clear error", async () => {
|
||||||
@@ -278,16 +264,14 @@ describe("Promise.all over arbitrary arrays", () => {
|
|||||||
|
|
||||||
describe("Promise.allSettled", () => {
|
describe("Promise.allSettled", () => {
|
||||||
test("reports fulfilled and rejected outcomes with catch-normalized reasons", async () => {
|
test("reports fulfilled and rejected outcomes with catch-normalized reasons", async () => {
|
||||||
expect(
|
expect(await value(`
|
||||||
await value(`
|
|
||||||
return await Promise.allSettled([
|
return await Promise.allSettled([
|
||||||
tools.host.sleepy({ id: 5 }),
|
tools.host.sleepy({ id: 5 }),
|
||||||
tools.host.fail({}),
|
tools.host.fail({}),
|
||||||
"plain",
|
"plain",
|
||||||
Promise.reject(new Error("boom")),
|
Promise.reject(new Error("boom")),
|
||||||
])
|
])
|
||||||
`),
|
`)).toEqual([
|
||||||
).toEqual([
|
|
||||||
{ status: "fulfilled", value: 5 },
|
{ status: "fulfilled", value: 5 },
|
||||||
{ status: "rejected", reason: { name: "Error", message: "Lookup refused" } },
|
{ status: "rejected", reason: { name: "Error", message: "Lookup refused" } },
|
||||||
{ status: "fulfilled", value: "plain" },
|
{ status: "fulfilled", value: "plain" },
|
||||||
@@ -322,8 +306,7 @@ describe("Promise.race", () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
test("awaiting an interrupted loser afterwards is a catchable program failure", async () => {
|
test("awaiting an interrupted loser afterwards is a catchable program failure", async () => {
|
||||||
expect(
|
expect(await value(`
|
||||||
await value(`
|
|
||||||
const fast = tools.host.sleepy({ id: 1, ms: 10 })
|
const fast = tools.host.sleepy({ id: 1, ms: 10 })
|
||||||
const slow = tools.host.sleepy({ id: 2, ms: 5000 })
|
const slow = tools.host.sleepy({ id: 2, ms: 5000 })
|
||||||
const winner = await Promise.race([fast, slow])
|
const winner = await Promise.race([fast, slow])
|
||||||
@@ -333,31 +316,23 @@ describe("Promise.race", () => {
|
|||||||
} catch (e) {
|
} catch (e) {
|
||||||
return { winner, caught: e.message }
|
return { winner, caught: e.message }
|
||||||
}
|
}
|
||||||
`),
|
`)).toEqual({ winner: 1, caught: "This tool call was interrupted because another value settled a Promise.race first." })
|
||||||
).toEqual({
|
|
||||||
winner: 1,
|
|
||||||
caught: "This tool call was interrupted because another value settled a Promise.race first.",
|
|
||||||
})
|
|
||||||
})
|
})
|
||||||
|
|
||||||
test("a rejection can win the race", async () => {
|
test("a rejection can win the race", async () => {
|
||||||
expect(
|
expect(await value(`
|
||||||
await value(`
|
|
||||||
try {
|
try {
|
||||||
await Promise.race([tools.host.fail({}), tools.host.sleepy({ id: 1, ms: 5000 })])
|
await Promise.race([tools.host.fail({}), tools.host.sleepy({ id: 1, ms: 5000 })])
|
||||||
return "no"
|
return "no"
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
return e.message
|
return e.message
|
||||||
}
|
}
|
||||||
`),
|
`)).toBe("Lookup refused")
|
||||||
).toBe("Lookup refused")
|
|
||||||
})
|
})
|
||||||
|
|
||||||
test("a plain value wins over pending promises", async () => {
|
test("a plain value wins over pending promises", async () => {
|
||||||
const trace = makeTrace()
|
const trace = makeTrace()
|
||||||
expect(
|
expect(await value(`return await Promise.race([tools.host.sleepy({ id: 1, ms: 5000 }), "immediate"])`, { trace })).toBe("immediate")
|
||||||
await value(`return await Promise.race([tools.host.sleepy({ id: 1, ms: 5000 }), "immediate"])`, { trace }),
|
|
||||||
).toBe("immediate")
|
|
||||||
expect(trace.interrupted).toBe(1)
|
expect(trace.interrupted).toBe(1)
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -375,16 +350,14 @@ describe("Promise.resolve / Promise.reject", () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
test("reject produces a promise whose await throws the reason", async () => {
|
test("reject produces a promise whose await throws the reason", async () => {
|
||||||
expect(
|
expect(await value(`
|
||||||
await value(`
|
|
||||||
try {
|
try {
|
||||||
await Promise.reject("nope")
|
await Promise.reject("nope")
|
||||||
return "no"
|
return "no"
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
return e
|
return e
|
||||||
}
|
}
|
||||||
`),
|
`)).toBe("nope")
|
||||||
).toBe("nope")
|
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -83,9 +83,15 @@ describe("pretty signature rendering", () => {
|
|||||||
true,
|
true,
|
||||||
)
|
)
|
||||||
expect(pretty).toBe(
|
expect(pretty).toBe(
|
||||||
["{", " /** Search filter */", " filter?: {", " /** Issue state */", " state?: string", " }", "}"].join(
|
[
|
||||||
"\n",
|
"{",
|
||||||
),
|
" /** Search filter */",
|
||||||
|
" filter?: {",
|
||||||
|
" /** Issue state */",
|
||||||
|
" state?: string",
|
||||||
|
" }",
|
||||||
|
"}",
|
||||||
|
].join("\n"),
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -113,14 +119,7 @@ describe("pretty signature rendering", () => {
|
|||||||
expect(pretty).toContain(" /** @deprecated */\n legacy?: string")
|
expect(pretty).toContain(" /** @deprecated */\n legacy?: string")
|
||||||
expect(pretty).toContain(" /** @format uri */\n homepage?: string")
|
expect(pretty).toContain(" /** @format uri */\n homepage?: string")
|
||||||
expect(pretty).toContain(
|
expect(pretty).toContain(
|
||||||
[
|
[" /**", ' * @default ["a","b"]', " * @minItems 2", " * @maxItems 5", " */", " tags?: Array<string>"].join("\n"),
|
||||||
" /**",
|
|
||||||
' * @default ["a","b"]',
|
|
||||||
" * @minItems 2",
|
|
||||||
" * @maxItems 5",
|
|
||||||
" */",
|
|
||||||
" tags?: Array<string>",
|
|
||||||
].join("\n"),
|
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -213,11 +212,7 @@ describe("non-identifier property names render as quoted keys", () => {
|
|||||||
const tool = Tool.make({
|
const tool = Tool.make({
|
||||||
description: "Adapter tool with awkward field names",
|
description: "Adapter tool with awkward field names",
|
||||||
input: rawSchema,
|
input: rawSchema,
|
||||||
output: {
|
output: { type: "object", properties: { "content-type": { type: "string" } }, required: ["content-type"] } as const,
|
||||||
type: "object",
|
|
||||||
properties: { "content-type": { type: "string" } },
|
|
||||||
required: ["content-type"],
|
|
||||||
} as const,
|
|
||||||
run: () => Effect.succeed({ "content-type": "text/plain" }),
|
run: () => Effect.succeed({ "content-type": "text/plain" }),
|
||||||
})
|
})
|
||||||
expect(inputTypeScript(tool)).toContain('"foo-bar"?: string')
|
expect(inputTypeScript(tool)).toContain('"foo-bar"?: string')
|
||||||
@@ -274,9 +269,9 @@ describe("pretty signatures in search results", () => {
|
|||||||
const runtime = CodeMode.make({ tools: { github: { list_issues: listIssues }, orders: { lookup: lookupOrder } } })
|
const runtime = CodeMode.make({ tools: { github: { list_issues: listIssues }, orders: { lookup: lookupOrder } } })
|
||||||
|
|
||||||
const search = async (query: string) => {
|
const search = async (query: string) => {
|
||||||
const result = await Effect.runPromise(
|
const result = await Effect.runPromise(runtime.execute(
|
||||||
runtime.execute(`return await tools.$codemode.search({ query: ${JSON.stringify(query)} })`),
|
`return await tools.$codemode.search({ query: ${JSON.stringify(query)} })`,
|
||||||
)
|
))
|
||||||
expect(result.ok).toBe(true)
|
expect(result.ok).toBe(true)
|
||||||
if (!result.ok) throw new Error("search failed")
|
if (!result.ok) throw new Error("search failed")
|
||||||
return result.value as { items: Array<{ path: string; signature: string }>; total: number }
|
return result.value as { items: Array<{ path: string; signature: string }>; total: number }
|
||||||
|
|||||||
@@ -40,11 +40,7 @@ describe("Date", () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
test("UTC getters read calendar components", async () => {
|
test("UTC getters read calendar components", async () => {
|
||||||
expect(
|
expect(await value(`const d = new Date("2024-03-05T06:07:08.009Z"); return [d.getUTCFullYear(), d.getUTCMonth(), d.getUTCDate(), d.getUTCHours(), d.getUTCMinutes(), d.getUTCSeconds(), d.getUTCMilliseconds()]`)).toEqual([2024, 2, 5, 6, 7, 8, 9])
|
||||||
await value(
|
|
||||||
`const d = new Date("2024-03-05T06:07:08.009Z"); return [d.getUTCFullYear(), d.getUTCMonth(), d.getUTCDate(), d.getUTCHours(), d.getUTCMinutes(), d.getUTCSeconds(), d.getUTCMilliseconds()]`,
|
|
||||||
),
|
|
||||||
).toEqual([2024, 2, 5, 6, 7, 8, 9])
|
|
||||||
})
|
})
|
||||||
|
|
||||||
test("invalid dates yield NaN times, guardable in-sandbox", async () => {
|
test("invalid dates yield NaN times, guardable in-sandbox", async () => {
|
||||||
@@ -53,9 +49,7 @@ describe("Date", () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
test("toISOString on an invalid date is a catchable error", async () => {
|
test("toISOString on an invalid date is a catchable error", async () => {
|
||||||
expect(await value(`try { new Date("garbage").toISOString(); return "no" } catch { return "caught" }`)).toBe(
|
expect(await value(`try { new Date("garbage").toISOString(); return "no" } catch { return "caught" }`)).toBe("caught")
|
||||||
"caught",
|
|
||||||
)
|
|
||||||
})
|
})
|
||||||
|
|
||||||
test("template interpolation renders the ISO form", async () => {
|
test("template interpolation renders the ISO form", async () => {
|
||||||
@@ -78,18 +72,14 @@ describe("Date", () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
test("sorting dates with a numeric comparator", async () => {
|
test("sorting dates with a numeric comparator", async () => {
|
||||||
expect(
|
expect(await value(`
|
||||||
await value(`
|
|
||||||
const dates = [new Date(3000), new Date(1000), new Date(2000)]
|
const dates = [new Date(3000), new Date(1000), new Date(2000)]
|
||||||
return dates.sort((a, b) => a - b).map((d) => d.getTime())
|
return dates.sort((a, b) => a - b).map((d) => d.getTime())
|
||||||
`),
|
`)).toEqual([1000, 2000, 3000])
|
||||||
).toEqual([1000, 2000, 3000])
|
|
||||||
})
|
})
|
||||||
|
|
||||||
test("new Date(year, month, day) accepts component form", async () => {
|
test("new Date(year, month, day) accepts component form", async () => {
|
||||||
expect(await value(`const d = new Date(2024, 0, 2); return [d.getFullYear(), d.getMonth(), d.getDate()]`)).toEqual([
|
expect(await value(`const d = new Date(2024, 0, 2); return [d.getFullYear(), d.getMonth(), d.getDate()]`)).toEqual([2024, 0, 2])
|
||||||
2024, 0, 2,
|
|
||||||
])
|
|
||||||
})
|
})
|
||||||
|
|
||||||
test("typeof and unknown properties are forgiving", async () => {
|
test("typeof and unknown properties are forgiving", async () => {
|
||||||
@@ -105,31 +95,25 @@ describe("RegExp", () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
test("exec exposes captures and index", async () => {
|
test("exec exposes captures and index", async () => {
|
||||||
expect(await value(`const m = /a(b+)/.exec("xxabbc"); return { full: m[0], group: m[1], index: m.index }`)).toEqual(
|
expect(await value(`const m = /a(b+)/.exec("xxabbc"); return { full: m[0], group: m[1], index: m.index }`)).toEqual({
|
||||||
{
|
|
||||||
full: "abb",
|
full: "abb",
|
||||||
group: "bb",
|
group: "bb",
|
||||||
index: 2,
|
index: 2,
|
||||||
},
|
})
|
||||||
)
|
|
||||||
expect(await value(`return /a/.exec("zzz")`)).toBeNull()
|
expect(await value(`return /a/.exec("zzz")`)).toBeNull()
|
||||||
})
|
})
|
||||||
|
|
||||||
test("named groups read through", async () => {
|
test("named groups read through", async () => {
|
||||||
expect(
|
expect(await value(`const m = /(?<word>[a-z]+)-(?<num>\\d+)/.exec("id ab-42"); return m.groups.word + m.groups.num`)).toBe("ab42")
|
||||||
await value(`const m = /(?<word>[a-z]+)-(?<num>\\d+)/.exec("id ab-42"); return m.groups.word + m.groups.num`),
|
|
||||||
).toBe("ab42")
|
|
||||||
})
|
})
|
||||||
|
|
||||||
test("global exec advances lastIndex across calls", async () => {
|
test("global exec advances lastIndex across calls", async () => {
|
||||||
expect(
|
expect(await value(`
|
||||||
await value(`
|
|
||||||
const r = /\\d+/g
|
const r = /\\d+/g
|
||||||
const first = r.exec("a1b22c")
|
const first = r.exec("a1b22c")
|
||||||
const second = r.exec("a1b22c")
|
const second = r.exec("a1b22c")
|
||||||
return [first[0], second[0]]
|
return [first[0], second[0]]
|
||||||
`),
|
`)).toEqual(["1", "22"])
|
||||||
).toEqual(["1", "22"])
|
|
||||||
})
|
})
|
||||||
|
|
||||||
test("string match: non-global carries index, global lists all matches", async () => {
|
test("string match: non-global carries index, global lists all matches", async () => {
|
||||||
@@ -210,51 +194,34 @@ describe("RegExp", () => {
|
|||||||
|
|
||||||
describe("Map", () => {
|
describe("Map", () => {
|
||||||
test("get/set/has/size with chaining", async () => {
|
test("get/set/has/size with chaining", async () => {
|
||||||
expect(
|
expect(await value(`
|
||||||
await value(`
|
|
||||||
const m = new Map()
|
const m = new Map()
|
||||||
m.set("a", 1).set("b", 2)
|
m.set("a", 1).set("b", 2)
|
||||||
return { a: m.get("a"), b: m.get("b"), has: m.has("a"), miss: m.get("zz") === undefined, size: m.size }
|
return { a: m.get("a"), b: m.get("b"), has: m.has("a"), miss: m.get("zz") === undefined, size: m.size }
|
||||||
`),
|
`)).toEqual({ a: 1, b: 2, has: true, miss: true, size: 5 - 3 })
|
||||||
).toEqual({ a: 1, b: 2, has: true, miss: true, size: 5 - 3 })
|
|
||||||
})
|
})
|
||||||
|
|
||||||
test("object keys use identity", async () => {
|
test("object keys use identity", async () => {
|
||||||
expect(
|
expect(await value(`
|
||||||
await value(`
|
|
||||||
const key = { id: 1 }
|
const key = { id: 1 }
|
||||||
const m = new Map()
|
const m = new Map()
|
||||||
m.set(key, "hit")
|
m.set(key, "hit")
|
||||||
return [m.get(key), m.get({ id: 1 }) === undefined]
|
return [m.get(key), m.get({ id: 1 }) === undefined]
|
||||||
`),
|
`)).toEqual(["hit", true])
|
||||||
).toEqual(["hit", true])
|
|
||||||
})
|
})
|
||||||
|
|
||||||
test("construction from entry pairs and another Map", async () => {
|
test("construction from entry pairs and another Map", async () => {
|
||||||
expect(await value(`const m = new Map([["a", 1], ["b", 2]]); return m.get("b")`)).toBe(2)
|
expect(await value(`const m = new Map([["a", 1], ["b", 2]]); return m.get("b")`)).toBe(2)
|
||||||
expect(
|
expect(await value(`const m = new Map([["a", 1]]); const n = new Map(m); n.set("b", 2); return [n.get("a"), n.get("b"), m.has("b")]`)).toEqual([1, 2, false])
|
||||||
await value(
|
|
||||||
`const m = new Map([["a", 1]]); const n = new Map(m); n.set("b", 2); return [n.get("a"), n.get("b"), m.has("b")]`,
|
|
||||||
),
|
|
||||||
).toEqual([1, 2, false])
|
|
||||||
expect((await error(`return new Map("nope")`)).message).toMatch(/\[key, value\] pairs/)
|
expect((await error(`return new Map("nope")`)).message).toMatch(/\[key, value\] pairs/)
|
||||||
expect((await error(`return new Map(["flat"])`)).message).toMatch(/\[key, value\] pairs/)
|
expect((await error(`return new Map(["flat"])`)).message).toMatch(/\[key, value\] pairs/)
|
||||||
})
|
})
|
||||||
|
|
||||||
test("keys/values/entries return arrays", async () => {
|
test("keys/values/entries return arrays", async () => {
|
||||||
expect(
|
expect(await value(`
|
||||||
await value(`
|
|
||||||
const m = new Map([["a", 1], ["b", 2]])
|
const m = new Map([["a", 1], ["b", 2]])
|
||||||
return { keys: m.keys(), values: m.values(), entries: m.entries() }
|
return { keys: m.keys(), values: m.values(), entries: m.entries() }
|
||||||
`),
|
`)).toEqual({ keys: ["a", "b"], values: [1, 2], entries: [["a", 1], ["b", 2]] })
|
||||||
).toEqual({
|
|
||||||
keys: ["a", "b"],
|
|
||||||
values: [1, 2],
|
|
||||||
entries: [
|
|
||||||
["a", 1],
|
|
||||||
["b", 2],
|
|
||||||
],
|
|
||||||
})
|
|
||||||
})
|
})
|
||||||
|
|
||||||
test("Object.fromEntries(map) and Array.from(map)", async () => {
|
test("Object.fromEntries(map) and Array.from(map)", async () => {
|
||||||
@@ -263,15 +230,13 @@ describe("Map", () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
test("for...of iterates [key, value] pairs with destructuring", async () => {
|
test("for...of iterates [key, value] pairs with destructuring", async () => {
|
||||||
expect(
|
expect(await value(`
|
||||||
await value(`
|
|
||||||
const m = new Map([["a", 1], ["b", 2]])
|
const m = new Map([["a", 1], ["b", 2]])
|
||||||
let total = 0
|
let total = 0
|
||||||
let names = ""
|
let names = ""
|
||||||
for (const [key, count] of m) { names += key; total += count }
|
for (const [key, count] of m) { names += key; total += count }
|
||||||
return names + total
|
return names + total
|
||||||
`),
|
`)).toBe("ab3")
|
||||||
).toBe("ab3")
|
|
||||||
})
|
})
|
||||||
|
|
||||||
test("spread produces entry pairs", async () => {
|
test("spread produces entry pairs", async () => {
|
||||||
@@ -279,38 +244,32 @@ describe("Map", () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
test("forEach passes (value, key)", async () => {
|
test("forEach passes (value, key)", async () => {
|
||||||
expect(
|
expect(await value(`
|
||||||
await value(`
|
|
||||||
const m = new Map([["a", 1], ["b", 2]])
|
const m = new Map([["a", 1], ["b", 2]])
|
||||||
const seen = []
|
const seen = []
|
||||||
m.forEach((count, key) => seen.push(key + count))
|
m.forEach((count, key) => seen.push(key + count))
|
||||||
return seen
|
return seen
|
||||||
`),
|
`)).toEqual(["a1", "b2"])
|
||||||
).toEqual(["a1", "b2"])
|
|
||||||
})
|
})
|
||||||
|
|
||||||
test("delete and clear", async () => {
|
test("delete and clear", async () => {
|
||||||
expect(
|
expect(await value(`
|
||||||
await value(`
|
|
||||||
const m = new Map([["a", 1], ["b", 2]])
|
const m = new Map([["a", 1], ["b", 2]])
|
||||||
const removed = m.delete("a")
|
const removed = m.delete("a")
|
||||||
const missed = m.delete("zz")
|
const missed = m.delete("zz")
|
||||||
const sizeAfterDelete = m.size
|
const sizeAfterDelete = m.size
|
||||||
m.clear()
|
m.clear()
|
||||||
return [removed, missed, sizeAfterDelete, m.size]
|
return [removed, missed, sizeAfterDelete, m.size]
|
||||||
`),
|
`)).toEqual([true, false, 1, 0])
|
||||||
).toEqual([true, false, 1, 0])
|
|
||||||
})
|
})
|
||||||
|
|
||||||
test("counting idiom: grouped tallies", async () => {
|
test("counting idiom: grouped tallies", async () => {
|
||||||
expect(
|
expect(await value(`
|
||||||
await value(`
|
|
||||||
const words = ["a", "b", "a", "c", "a"]
|
const words = ["a", "b", "a", "c", "a"]
|
||||||
const counts = new Map()
|
const counts = new Map()
|
||||||
for (const word of words) counts.set(word, (counts.get(word) ?? 0) + 1)
|
for (const word of words) counts.set(word, (counts.get(word) ?? 0) + 1)
|
||||||
return Object.fromEntries(counts)
|
return Object.fromEntries(counts)
|
||||||
`),
|
`)).toEqual({ a: 3, b: 1, c: 1 })
|
||||||
).toEqual({ a: 3, b: 1, c: 1 })
|
|
||||||
})
|
})
|
||||||
|
|
||||||
test("maps serialize to {} at the boundary, like JSON", async () => {
|
test("maps serialize to {} at the boundary, like JSON", async () => {
|
||||||
@@ -327,14 +286,12 @@ describe("Map", () => {
|
|||||||
|
|
||||||
describe("Set", () => {
|
describe("Set", () => {
|
||||||
test("add/has/delete/size with chaining", async () => {
|
test("add/has/delete/size with chaining", async () => {
|
||||||
expect(
|
expect(await value(`
|
||||||
await value(`
|
|
||||||
const s = new Set()
|
const s = new Set()
|
||||||
s.add(1).add(2).add(1)
|
s.add(1).add(2).add(1)
|
||||||
const removed = s.delete(2)
|
const removed = s.delete(2)
|
||||||
return [s.size, s.has(1), s.has(2), removed]
|
return [s.size, s.has(1), s.has(2), removed]
|
||||||
`),
|
`)).toEqual([1, true, false, true])
|
||||||
).toEqual([1, true, false, true])
|
|
||||||
})
|
})
|
||||||
|
|
||||||
test("dedupe idiom: [...new Set(items)]", async () => {
|
test("dedupe idiom: [...new Set(items)]", async () => {
|
||||||
@@ -351,13 +308,11 @@ describe("Set", () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
test("for...of iterates values", async () => {
|
test("for...of iterates values", async () => {
|
||||||
expect(
|
expect(await value(`
|
||||||
await value(`
|
|
||||||
let total = 0
|
let total = 0
|
||||||
for (const n of new Set([1, 2, 3])) total += n
|
for (const n of new Set([1, 2, 3])) total += n
|
||||||
return total
|
return total
|
||||||
`),
|
`)).toBe(6)
|
||||||
).toBe(6)
|
|
||||||
})
|
})
|
||||||
|
|
||||||
test("sets serialize to {} at the boundary, like JSON", async () => {
|
test("sets serialize to {} at the boundary, like JSON", async () => {
|
||||||
@@ -383,32 +338,21 @@ describe("stdlib integration", () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
test("dates inside Map values survive in-sandbox reads", async () => {
|
test("dates inside Map values survive in-sandbox reads", async () => {
|
||||||
expect(
|
expect(await value(`
|
||||||
await value(`
|
|
||||||
const m = new Map([["start", new Date(1000)]])
|
const m = new Map([["start", new Date(1000)]])
|
||||||
return m.get("start").getTime()
|
return m.get("start").getTime()
|
||||||
`),
|
`)).toBe(1000)
|
||||||
).toBe(1000)
|
|
||||||
})
|
})
|
||||||
|
|
||||||
test("instanceof recognizes the stdlib value types", async () => {
|
test("instanceof recognizes the stdlib value types", async () => {
|
||||||
expect(
|
expect(await value(`return [new Date(0) instanceof Date, /a/ instanceof RegExp, new Map() instanceof Map, new Set() instanceof Set]`)).toEqual([true, true, true, true])
|
||||||
await value(
|
expect(await value(`return [[1] instanceof Array, [1] instanceof Object, ({}) instanceof Object, 5 instanceof Object]`)).toEqual([true, true, true, false])
|
||||||
`return [new Date(0) instanceof Date, /a/ instanceof RegExp, new Map() instanceof Map, new Set() instanceof Set]`,
|
|
||||||
),
|
|
||||||
).toEqual([true, true, true, true])
|
|
||||||
expect(
|
|
||||||
await value(`return [[1] instanceof Array, [1] instanceof Object, ({}) instanceof Object, 5 instanceof Object]`),
|
|
||||||
).toEqual([true, true, true, false])
|
|
||||||
expect(await value(`return [new Map() instanceof Set, "s" instanceof Date]`)).toEqual([false, false])
|
expect(await value(`return [new Map() instanceof Set, "s" instanceof Date]`)).toEqual([false, false])
|
||||||
expect(
|
expect(await value(`const p = Promise.resolve(1); const isPromise = p instanceof Promise; await p; return isPromise`)).toBe(true)
|
||||||
await value(`const p = Promise.resolve(1); const isPromise = p instanceof Promise; await p; return isPromise`),
|
|
||||||
).toBe(true)
|
|
||||||
})
|
})
|
||||||
|
|
||||||
test("realistic pipeline: parse, extract with regex, dedupe, count by day", async () => {
|
test("realistic pipeline: parse, extract with regex, dedupe, count by day", async () => {
|
||||||
expect(
|
expect(await value(`
|
||||||
await value(`
|
|
||||||
const raw = '[{"at":"2024-01-01T05:00:00Z","tag":"a b"},{"at":"2024-01-01T09:00:00Z","tag":"b c"},{"at":"2024-01-02T01:00:00Z","tag":"a"}]'
|
const raw = '[{"at":"2024-01-01T05:00:00Z","tag":"a b"},{"at":"2024-01-01T09:00:00Z","tag":"b c"},{"at":"2024-01-02T01:00:00Z","tag":"a"}]'
|
||||||
const rows = JSON.parse(raw)
|
const rows = JSON.parse(raw)
|
||||||
const tags = new Set()
|
const tags = new Set()
|
||||||
@@ -419,34 +363,27 @@ describe("stdlib integration", () => {
|
|||||||
byDay.set(day, (byDay.get(day) ?? 0) + 1)
|
byDay.set(day, (byDay.get(day) ?? 0) + 1)
|
||||||
}
|
}
|
||||||
return { tags: [...tags].sort((a, b) => (a < b ? -1 : 1)), byDay: Object.fromEntries(byDay) }
|
return { tags: [...tags].sort((a, b) => (a < b ? -1 : 1)), byDay: Object.fromEntries(byDay) }
|
||||||
`),
|
`)).toEqual({ tags: ["a", "b", "c"], byDay: { "2024-01-01": 2, "2024-01-02": 1 } })
|
||||||
).toEqual({ tags: ["a", "b", "c"], byDay: { "2024-01-01": 2, "2024-01-02": 1 } })
|
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
describe("sandbox values at intra-sandbox checkpoints", () => {
|
describe("sandbox values at intra-sandbox checkpoints", () => {
|
||||||
test("Object.values/entries keep Dates usable", async () => {
|
test("Object.values/entries keep Dates usable", async () => {
|
||||||
expect(await value(`return Object.values({ d: new Date(0) })[0].getTime()`)).toBe(0)
|
expect(await value(`return Object.values({ d: new Date(0) })[0].getTime()`)).toBe(0)
|
||||||
expect(await value(`const [key, d] = Object.entries({ d: new Date(0) })[0]; return key + ":" + d.getTime()`)).toBe(
|
expect(await value(`const [key, d] = Object.entries({ d: new Date(0) })[0]; return key + ":" + d.getTime()`)).toBe("d:0")
|
||||||
"d:0",
|
|
||||||
)
|
|
||||||
})
|
})
|
||||||
|
|
||||||
test("Object.assign keeps Maps usable", async () => {
|
test("Object.assign keeps Maps usable", async () => {
|
||||||
expect(await value(`const merged = Object.assign({}, { m: new Map([["a", 1]]) }); return merged.m.get("a")`)).toBe(
|
expect(await value(`const merged = Object.assign({}, { m: new Map([["a", 1]]) }); return merged.m.get("a")`)).toBe(1)
|
||||||
1,
|
|
||||||
)
|
|
||||||
})
|
})
|
||||||
|
|
||||||
test("object and array spread keep sandbox values usable", async () => {
|
test("object and array spread keep sandbox values usable", async () => {
|
||||||
expect(
|
expect(await value(`
|
||||||
await value(`
|
|
||||||
const src = { m: new Map([["a", 1]]) }
|
const src = { m: new Map([["a", 1]]) }
|
||||||
const copy = { ...src }
|
const copy = { ...src }
|
||||||
copy.m.set("b", 2)
|
copy.m.set("b", 2)
|
||||||
return [copy.m.get("a"), src.m.get("b")]
|
return [copy.m.get("a"), src.m.get("b")]
|
||||||
`),
|
`)).toEqual([1, 2])
|
||||||
).toEqual([1, 2])
|
|
||||||
expect(await value(`const list = [new Date(1000)]; const copy = [...list]; return copy[0].getTime()`)).toBe(1000)
|
expect(await value(`const list = [new Date(1000)]; const copy = [...list]; return copy[0].getTime()`)).toBe(1000)
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -467,10 +404,7 @@ describe("sandbox values at intra-sandbox checkpoints", () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
test("the host boundary still serializes JSON forms: results, JSON.stringify, and tool arguments", async () => {
|
test("the host boundary still serializes JSON forms: results, JSON.stringify, and tool arguments", async () => {
|
||||||
expect(await value(`return { d: new Date(0), m: new Map([["a", 1]]) }`)).toEqual({
|
expect(await value(`return { d: new Date(0), m: new Map([["a", 1]]) }`)).toEqual({ d: "1970-01-01T00:00:00.000Z", m: {} })
|
||||||
d: "1970-01-01T00:00:00.000Z",
|
|
||||||
m: {},
|
|
||||||
})
|
|
||||||
expect(await value(`return JSON.stringify({ d: new Date(0) })`)).toBe('{"d":"1970-01-01T00:00:00.000Z"}')
|
expect(await value(`return JSON.stringify({ d: new Date(0) })`)).toBe('{"d":"1970-01-01T00:00:00.000Z"}')
|
||||||
|
|
||||||
const observed: Array<unknown> = []
|
const observed: Array<unknown> = []
|
||||||
@@ -483,12 +417,10 @@ describe("sandbox values at intra-sandbox checkpoints", () => {
|
|||||||
return "ok"
|
return "ok"
|
||||||
}),
|
}),
|
||||||
})
|
})
|
||||||
const result = await Effect.runPromise(
|
const result = await Effect.runPromise(CodeMode.execute({
|
||||||
CodeMode.execute({
|
|
||||||
tools: { host: { capture } },
|
tools: { host: { capture } },
|
||||||
code: `return await tools.host.capture({ when: new Date(0), tags: new Map([["a", 1]]) })`,
|
code: `return await tools.host.capture({ when: new Date(0), tags: new Map([["a", 1]]) })`,
|
||||||
}),
|
}))
|
||||||
)
|
|
||||||
expect(result.ok).toBe(true)
|
expect(result.ok).toBe(true)
|
||||||
expect(observed).toStrictEqual([{ when: "1970-01-01T00:00:00.000Z", tags: {} }])
|
expect(observed).toStrictEqual([{ when: "1970-01-01T00:00:00.000Z", tags: {} }])
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -77,12 +77,7 @@ const layer = Layer.effect(
|
|||||||
|
|
||||||
const projectModel = (model: ModelV2.Info, provider: ProviderV2.Info) => {
|
const projectModel = (model: ModelV2.Info, provider: ProviderV2.Info) => {
|
||||||
const api =
|
const api =
|
||||||
// A native api with a package is explicitly targeted; only package-less,
|
model.api.type === "native" && !model.api.url && Object.keys(model.api.settings).length === 0
|
||||||
// settings-less native apis are placeholders that inherit the provider api.
|
|
||||||
model.api.type === "native" &&
|
|
||||||
model.api.package === undefined &&
|
|
||||||
!model.api.url &&
|
|
||||||
Object.keys(model.api.settings).length === 0
|
|
||||||
? { ...provider.api, id: model.api.id }
|
? { ...provider.api, id: model.api.id }
|
||||||
: model.api.type === "aisdk" && provider.api.type === "aisdk" && !model.api.url
|
: model.api.type === "aisdk" && provider.api.type === "aisdk" && !model.api.url
|
||||||
? { ...model.api, url: provider.api.url, settings: { ...provider.api.settings, ...model.api.settings } }
|
? { ...model.api, url: provider.api.url, settings: { ...provider.api.settings, ...model.api.settings } }
|
||||||
|
|||||||
@@ -230,7 +230,7 @@ export function hoist<A, E, T extends Tag, const Items extends Replacements = re
|
|||||||
if (existing && existing !== node) {
|
if (existing && existing !== node) {
|
||||||
throw new Error(`Tag ${tag} has conflicting implementations for ${node.name}`)
|
throw new Error(`Tag ${tag} has conflicting implementations for ${node.name}`)
|
||||||
}
|
}
|
||||||
hoisted.set(node.name, rewriteReplacementDependencies(node, replacementMap))
|
hoisted.set(node.name, node)
|
||||||
return group([])
|
return group([])
|
||||||
}
|
}
|
||||||
if (node.kind === "unbound") {
|
if (node.kind === "unbound") {
|
||||||
|
|||||||
@@ -89,10 +89,6 @@ export function buildLocationServiceMap(
|
|||||||
LayerMap.make(
|
LayerMap.make(
|
||||||
(ref: Location.Ref) => {
|
(ref: Location.Ref) => {
|
||||||
const allReplacements = replacements.concat([[Location.node, Location.boundNode(ref)]])
|
const allReplacements = replacements.concat([[Location.node, Location.boundNode(ref)]])
|
||||||
// Apply replacements during hoist, not afterward: replacements can
|
|
||||||
// introduce new tagged dependencies (Location.boundNode depends on
|
|
||||||
// Project), and the hoist walk is the only pass that can still slice
|
|
||||||
// those back out.
|
|
||||||
const location = LayerNode.hoist(locationServices, Node.tags.values.global, allReplacements)
|
const location = LayerNode.hoist(locationServices, Node.tags.values.global, allReplacements)
|
||||||
|
|
||||||
return LayerNode.compile(location.node).pipe(
|
return LayerNode.compile(location.node).pipe(
|
||||||
|
|||||||
@@ -1,10 +1,9 @@
|
|||||||
import { createServer } from "node:http"
|
import { createServer } from "node:http"
|
||||||
import type { IntegrationOAuthMethodRegistration } from "@opencode-ai/plugin/v2/effect/integration"
|
import type { IntegrationOAuthMethodRegistration } from "@opencode-ai/plugin/v2/effect/integration"
|
||||||
import { define } from "@opencode-ai/plugin/v2/effect/plugin"
|
import { define } from "@opencode-ai/plugin/v2/effect/plugin"
|
||||||
import { Deferred, Effect, Stream } from "effect"
|
import { Deferred, Effect } from "effect"
|
||||||
import type { Scope } from "effect"
|
import type { Scope } from "effect"
|
||||||
import { Credential } from "../../credential"
|
import { Credential } from "../../credential"
|
||||||
import { EventV2 } from "../../event"
|
|
||||||
import { InstallationVersion } from "../../installation/version"
|
import { InstallationVersion } from "../../installation/version"
|
||||||
import { Integration } from "../../integration"
|
import { Integration } from "../../integration"
|
||||||
import { ModelV2 } from "../../model"
|
import { ModelV2 } from "../../model"
|
||||||
@@ -18,7 +17,6 @@ const callbackPort = 1455
|
|||||||
const pollingSafetyMargin = 3000
|
const pollingSafetyMargin = 3000
|
||||||
const browserMethodID = Integration.MethodID.make("chatgpt-browser")
|
const browserMethodID = Integration.MethodID.make("chatgpt-browser")
|
||||||
const headlessMethodID = Integration.MethodID.make("chatgpt-headless")
|
const headlessMethodID = Integration.MethodID.make("chatgpt-headless")
|
||||||
const codexPackage = "@opencode-ai/llm/providers/openai/codex"
|
|
||||||
|
|
||||||
type Pkce = {
|
type Pkce = {
|
||||||
verifier: string
|
verifier: string
|
||||||
@@ -156,55 +154,24 @@ const headless = {
|
|||||||
export const OpenAIPlugin = define({
|
export const OpenAIPlugin = define({
|
||||||
id: "openai",
|
id: "openai",
|
||||||
effect: Effect.fn(function* (ctx) {
|
effect: Effect.fn(function* (ctx) {
|
||||||
const events = yield* EventV2.Service
|
|
||||||
yield* ctx.integration.transform((draft) => {
|
yield* ctx.integration.transform((draft) => {
|
||||||
draft.method.update(browser)
|
draft.method.update(browser)
|
||||||
draft.method.update(headless)
|
draft.method.update(headless)
|
||||||
})
|
})
|
||||||
yield* ctx.catalog.transform(
|
yield* ctx.catalog.transform(
|
||||||
Effect.fn(function* (evt) {
|
Effect.fn(function* (evt) {
|
||||||
const connection = yield* ctx.integration.connection.active("openai")
|
|
||||||
const credential = connection
|
|
||||||
? yield* ctx.integration.connection.resolve(connection).pipe(Effect.catch(() => Effect.succeed(undefined)))
|
|
||||||
: undefined
|
|
||||||
const chatgpt = isChatGPT(credential)
|
|
||||||
for (const item of evt.provider.list()) {
|
for (const item of evt.provider.list()) {
|
||||||
if (item.provider.api.type !== "aisdk") continue
|
if (item.provider.api.type !== "aisdk") continue
|
||||||
if (item.provider.api.package !== "@ai-sdk/openai") continue
|
if (item.provider.api.package !== "@ai-sdk/openai") continue
|
||||||
if (item.models.has(ModelV2.ID.make("gpt-5-chat-latest"))) {
|
if (!item.models.has(ModelV2.ID.make("gpt-5-chat-latest"))) continue
|
||||||
evt.model.update(item.provider.id, ModelV2.ID.make("gpt-5-chat-latest"), (model) => {
|
evt.model.update(item.provider.id, ModelV2.ID.make("gpt-5-chat-latest"), (model) => {
|
||||||
// OpenAIPlugin sends OpenAI models through Responses; this alias is a
|
// OpenAIPlugin sends OpenAI models through Responses; this alias is a
|
||||||
// chat-completions-only model, so hide it only from OpenAI's catalog.
|
// chat-completions-only model, so hide it only from OpenAI's catalog.
|
||||||
model.enabled = false
|
model.enabled = false
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
if (!chatgpt) continue
|
|
||||||
for (const model of item.models.values()) {
|
|
||||||
evt.model.update(item.provider.id, model.id, (draft) => {
|
|
||||||
if (!eligible(draft.api.id)) {
|
|
||||||
draft.enabled = false
|
|
||||||
return
|
|
||||||
}
|
|
||||||
draft.cost = draft.cost.map((cost) => ({ ...cost, input: 0, output: 0, cache: { read: 0, write: 0 } }))
|
|
||||||
draft.api = {
|
|
||||||
type: "native",
|
|
||||||
id: draft.api.id,
|
|
||||||
package: codexPackage,
|
|
||||||
settings:
|
|
||||||
draft.api.type === "aisdk"
|
|
||||||
? { ...item.provider.api.settings, ...draft.api.settings }
|
|
||||||
: draft.api.settings,
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
yield* events.subscribe(Integration.Event.ConnectionUpdated).pipe(
|
|
||||||
Stream.filter((event) => event.data.integrationID === Integration.ID.make("openai")),
|
|
||||||
Stream.runForEach(() => ctx.catalog.reload()),
|
|
||||||
Effect.forkScoped({ startImmediately: true }),
|
|
||||||
)
|
|
||||||
yield* ctx.aisdk.sdk(
|
yield* ctx.aisdk.sdk(
|
||||||
Effect.fn(function* (evt) {
|
Effect.fn(function* (evt) {
|
||||||
if (evt.package !== "@ai-sdk/openai") return
|
if (evt.package !== "@ai-sdk/openai") return
|
||||||
@@ -302,34 +269,16 @@ function authorizeURL(redirect: string, pkce: Pkce, state: string) {
|
|||||||
codex_cli_simplified_flow: "true",
|
codex_cli_simplified_flow: "true",
|
||||||
state,
|
state,
|
||||||
originator: "opencode",
|
originator: "opencode",
|
||||||
}).toString()}`
|
})}`
|
||||||
}
|
}
|
||||||
|
|
||||||
function extractAccountID(tokens: TokenResponse) {
|
function extractAccountID(tokens: TokenResponse) {
|
||||||
return claim(tokens.id_token) ?? claim(tokens.access_token)
|
return claim(tokens.id_token) ?? claim(tokens.access_token)
|
||||||
}
|
}
|
||||||
|
|
||||||
function isChatGPT(credential: { readonly type: string; readonly methodID?: string } | undefined) {
|
|
||||||
return (
|
|
||||||
credential?.type === "oauth" &&
|
|
||||||
(credential.methodID === browserMethodID || credential.methodID === headlessMethodID)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
const chatgptAllowed = new Set(["gpt-5.5", "gpt-5.3-codex-spark", "gpt-5.4", "gpt-5.4-mini"])
|
|
||||||
const chatgptDisallowed = new Set(["gpt-5.5-pro"])
|
|
||||||
|
|
||||||
/** Which API model ids a ChatGPT subscription may call through the codex backend. */
|
|
||||||
function eligible(apiID: string) {
|
|
||||||
if (chatgptAllowed.has(apiID)) return true
|
|
||||||
if (chatgptDisallowed.has(apiID)) return false
|
|
||||||
const match = apiID.match(/^gpt-(\d+\.\d+)/)
|
|
||||||
return match ? Number.parseFloat(match[1]) > 5.4 : false
|
|
||||||
}
|
|
||||||
|
|
||||||
function claim(token: string) {
|
function claim(token: string) {
|
||||||
const part = token.split(".")[1]
|
const part = token.split(".")[1]
|
||||||
if (!part) return undefined
|
if (!part) return
|
||||||
try {
|
try {
|
||||||
const claims = JSON.parse(Buffer.from(part, "base64url").toString()) as Claims
|
const claims = JSON.parse(Buffer.from(part, "base64url").toString()) as Claims
|
||||||
return (
|
return (
|
||||||
@@ -338,6 +287,6 @@ function claim(token: string) {
|
|||||||
claims.organizations?.[0]?.id
|
claims.organizations?.[0]?.id
|
||||||
)
|
)
|
||||||
} catch {
|
} catch {
|
||||||
return undefined
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,9 +1,7 @@
|
|||||||
export * as ProviderV2 from "./provider"
|
export * as ProviderV2 from "./provider"
|
||||||
|
|
||||||
import { Effect, Schema, Types } from "effect"
|
import { Types } from "effect"
|
||||||
import { Provider } from "@opencode-ai/schema/provider"
|
import { Provider } from "@opencode-ai/schema/provider"
|
||||||
import { ProviderPackage } from "@opencode-ai/llm/provider-package"
|
|
||||||
import { Anthropic, OpenAI, OpenAICodex, OpenAICompatible } from "@opencode-ai/llm/providers"
|
|
||||||
|
|
||||||
export const ID = Provider.ID
|
export const ID = Provider.ID
|
||||||
export type ID = typeof ID.Type
|
export type ID = typeof ID.Type
|
||||||
@@ -25,44 +23,3 @@ export const Info = Provider.Info
|
|||||||
export type Info = Provider.Info
|
export type Info = Provider.Info
|
||||||
|
|
||||||
export type MutableInfo = Omit<Types.DeepMutable<Info>, "api"> & { api: MutableApi }
|
export type MutableInfo = Omit<Types.DeepMutable<Info>, "api"> & { api: MutableApi }
|
||||||
|
|
||||||
export class PackageLoadError extends Schema.TaggedErrorClass<PackageLoadError>()("ProviderV2.PackageLoadError", {
|
|
||||||
specifier: Schema.String,
|
|
||||||
reason: Schema.String,
|
|
||||||
}) {
|
|
||||||
override get message() {
|
|
||||||
return `Failed to load provider package ${this.specifier}: ${this.reason}`
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
type PackageModule = { readonly model: ProviderPackage.Definition["model"] }
|
|
||||||
|
|
||||||
const builtins: Record<string, PackageModule> = {
|
|
||||||
"@opencode-ai/llm/providers/openai": OpenAI,
|
|
||||||
"@opencode-ai/llm/providers/anthropic": Anthropic,
|
|
||||||
"@opencode-ai/llm/providers/openai-compatible": OpenAICompatible,
|
|
||||||
"@opencode-ai/llm/providers/openai/codex": OpenAICodex,
|
|
||||||
}
|
|
||||||
|
|
||||||
export const loadPackage = (
|
|
||||||
specifier: string,
|
|
||||||
): Effect.Effect<ProviderPackage.Definition["model"], PackageLoadError> => {
|
|
||||||
const builtin = builtins[specifier]
|
|
||||||
if (builtin) return Effect.succeed(builtin.model)
|
|
||||||
return Effect.tryPromise({
|
|
||||||
try: () => import(specifier),
|
|
||||||
catch: (cause) =>
|
|
||||||
new PackageLoadError({ specifier, reason: cause instanceof Error ? cause.message : String(cause) }),
|
|
||||||
}).pipe(
|
|
||||||
Effect.flatMap((module) => {
|
|
||||||
if (hasModel(module)) return Effect.succeed(module.model)
|
|
||||||
return Effect.fail(new PackageLoadError({ specifier, reason: "missing model export" }))
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export const load = loadPackage
|
|
||||||
|
|
||||||
function hasModel(module: unknown): module is PackageModule {
|
|
||||||
return typeof module === "object" && module !== null && "model" in module && typeof module.model === "function"
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,8 +1,12 @@
|
|||||||
export * as SessionRunnerModel from "./model"
|
export * as SessionRunnerModel from "./model"
|
||||||
|
|
||||||
import { makeLocationNode } from "../../effect/app-node"
|
import { makeLocationNode } from "../../effect/app-node"
|
||||||
import { Model } from "@opencode-ai/llm"
|
import { type Model } from "@opencode-ai/llm"
|
||||||
import { Context, Effect, Layer, Predicate, Schema } from "effect"
|
import * as AnthropicMessages from "@opencode-ai/llm/protocols/anthropic-messages"
|
||||||
|
import * as OpenAICompatibleChat from "@opencode-ai/llm/protocols/openai-compatible-chat"
|
||||||
|
import * as OpenAIResponses from "@opencode-ai/llm/protocols/openai-responses"
|
||||||
|
import { Auth, type AnyRoute } from "@opencode-ai/llm/route"
|
||||||
|
import { Context, Effect, Layer, Schema } from "effect"
|
||||||
import { produce } from "immer"
|
import { produce } from "immer"
|
||||||
import { Catalog } from "../../catalog"
|
import { Catalog } from "../../catalog"
|
||||||
import { Credential } from "../../credential"
|
import { Credential } from "../../credential"
|
||||||
@@ -65,7 +69,6 @@ export type Error =
|
|||||||
| ModelUnavailableError
|
| ModelUnavailableError
|
||||||
| VariantUnavailableError
|
| VariantUnavailableError
|
||||||
| UnsupportedApiError
|
| UnsupportedApiError
|
||||||
| ProviderV2.PackageLoadError
|
|
||||||
| Integration.AuthorizationError
|
| Integration.AuthorizationError
|
||||||
|
|
||||||
export interface Interface {
|
export interface Interface {
|
||||||
@@ -77,6 +80,27 @@ export class Service extends Context.Service<Service, Interface>()("@opencode/v2
|
|||||||
/** Test or embedding seam for supplying a model resolver directly. */
|
/** Test or embedding seam for supplying a model resolver directly. */
|
||||||
export const layerWith = (resolve: Interface["resolve"]) => Layer.succeed(Service, Service.of({ resolve }))
|
export const layerWith = (resolve: Interface["resolve"]) => Layer.succeed(Service, Service.of({ resolve }))
|
||||||
|
|
||||||
|
const apiKey = (model: ModelV2.Info, credential?: Credential.Value) => {
|
||||||
|
if (credential?.type === "key") return Auth.value(credential.key)
|
||||||
|
if (credential?.type === "oauth") return Auth.value(credential.access)
|
||||||
|
const value = model.request.body.apiKey ?? model.api.settings?.apiKey
|
||||||
|
if (typeof value === "string") return Auth.value(value)
|
||||||
|
}
|
||||||
|
|
||||||
|
const withDefaults = (model: ModelV2.Info, route: AnyRoute) => {
|
||||||
|
const body = model.request.body
|
||||||
|
const httpBody = Object.hasOwn(body, "apiKey")
|
||||||
|
? Object.fromEntries(Object.entries(body).filter(([key]) => key !== "apiKey"))
|
||||||
|
: body
|
||||||
|
return route.with({
|
||||||
|
provider: model.providerID,
|
||||||
|
endpoint: model.api.url === undefined ? undefined : { baseURL: model.api.url },
|
||||||
|
headers: model.request.headers,
|
||||||
|
http: { body: httpBody },
|
||||||
|
limits: { context: model.limit.context, output: model.limit.output },
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
const withVariant = (
|
const withVariant = (
|
||||||
model: ModelV2.Info,
|
model: ModelV2.Info,
|
||||||
variantID: ModelV2.VariantID | undefined,
|
variantID: ModelV2.VariantID | undefined,
|
||||||
@@ -107,95 +131,52 @@ const apiName = (model: ModelV2.Info) =>
|
|||||||
export const fromCatalogModel = (
|
export const fromCatalogModel = (
|
||||||
model: ModelV2.Info,
|
model: ModelV2.Info,
|
||||||
credential?: Credential.Value,
|
credential?: Credential.Value,
|
||||||
): Effect.Effect<Model, UnsupportedApiError | ProviderV2.PackageLoadError> => {
|
): Effect.Effect<Model, UnsupportedApiError> => {
|
||||||
const resolved =
|
const resolved =
|
||||||
credential?.type !== "key" || credential.metadata === undefined
|
credential?.type !== "key" || credential.metadata === undefined
|
||||||
? model
|
? model
|
||||||
: produce(model, (draft) => {
|
: produce(model, (draft) => {
|
||||||
Object.assign(draft.request.body, credential.metadata)
|
Object.assign(draft.request.body, credential.metadata)
|
||||||
})
|
})
|
||||||
return packageSpecifier(resolved).pipe(
|
const key = apiKey(resolved, credential)
|
||||||
Effect.flatMap((specifier) =>
|
if (resolved.api.type === "aisdk" && resolved.api.package === "@ai-sdk/openai") {
|
||||||
ProviderV2.loadPackage(specifier).pipe(
|
return Effect.succeed(
|
||||||
Effect.map((load) => {
|
withDefaults(resolved, OpenAIResponses.route)
|
||||||
const selected = load(resolved.api.id ?? resolved.id, {
|
.with({ auth: key === undefined ? Auth.none : Auth.bearer(key) })
|
||||||
...resolved.api.settings,
|
.model({ id: resolved.api.id }),
|
||||||
...(credential?.type === "oauth" ? credential.metadata : undefined),
|
)
|
||||||
baseURL: resolved.api.url ?? settingsString(resolved.api.settings, "baseURL"),
|
}
|
||||||
apiKey: apiKey(resolved, credential),
|
if (resolved.api.type === "aisdk" && resolved.api.package === "@ai-sdk/anthropic") {
|
||||||
providerOptions: requestSettings(resolved.request),
|
return Effect.succeed(
|
||||||
headers: resolved.request.headers,
|
withDefaults(resolved, AnthropicMessages.route)
|
||||||
body: stripApiKey(resolved.request.body),
|
.with({ auth: key === undefined ? Auth.none : Auth.header("x-api-key", key) })
|
||||||
limits: { context: resolved.limit.context, output: resolved.limit.output },
|
.model({ id: resolved.api.id }),
|
||||||
})
|
)
|
||||||
return Model.update(selected, {
|
}
|
||||||
provider: resolved.providerID,
|
if (resolved.api.type === "aisdk" && resolved.api.package === "@ai-sdk/openai-compatible" && resolved.api.url) {
|
||||||
route: selected.route.with({ provider: resolved.providerID }),
|
return Effect.succeed(
|
||||||
})
|
withDefaults(resolved, OpenAICompatibleChat.route)
|
||||||
|
.with({ auth: key === undefined ? Auth.none : Auth.bearer(key) })
|
||||||
|
.model({ id: resolved.api.id }),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
return Effect.fail(
|
||||||
|
new UnsupportedApiError({
|
||||||
|
providerID: resolved.providerID,
|
||||||
|
modelID: resolved.id,
|
||||||
|
api: apiName(resolved),
|
||||||
}),
|
}),
|
||||||
),
|
|
||||||
),
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export const resolve = (session: SessionSchema.Info, model: ModelV2.Info, credential?: Credential.Value) =>
|
export const resolve = (session: SessionSchema.Info, model: ModelV2.Info, credential?: Credential.Value) =>
|
||||||
withVariant(model, session.model?.variant).pipe(Effect.flatMap((model) => fromCatalogModel(model, credential)))
|
withVariant(model, session.model?.variant).pipe(Effect.flatMap((model) => fromCatalogModel(model, credential)))
|
||||||
|
|
||||||
/** Legacy aisdk catalog entries dispatch to the equivalent native provider packages. */
|
|
||||||
const aisdkPackages: Record<string, string | undefined> = {
|
|
||||||
"@ai-sdk/openai": "@opencode-ai/llm/providers/openai",
|
|
||||||
"@ai-sdk/anthropic": "@opencode-ai/llm/providers/anthropic",
|
|
||||||
"@ai-sdk/openai-compatible": "@opencode-ai/llm/providers/openai-compatible",
|
|
||||||
}
|
|
||||||
|
|
||||||
export const supported = (model: ModelV2.Info) =>
|
export const supported = (model: ModelV2.Info) =>
|
||||||
(model.api.type === "native" && model.api.package !== undefined) ||
|
model.api.type === "aisdk" &&
|
||||||
(model.api.type === "aisdk" &&
|
(model.api.package === "@ai-sdk/openai" ||
|
||||||
aisdkPackages[model.api.package] !== undefined &&
|
model.api.package === "@ai-sdk/anthropic" ||
|
||||||
// The openai-compatible package has no default endpoint; a URL is required.
|
(model.api.package === "@ai-sdk/openai-compatible" && model.api.url !== undefined))
|
||||||
(model.api.package !== "@ai-sdk/openai-compatible" || model.api.url !== undefined))
|
|
||||||
|
|
||||||
const packageSpecifier = (model: ModelV2.Info): Effect.Effect<string, UnsupportedApiError> => {
|
|
||||||
if (supported(model)) {
|
|
||||||
if (model.api.type === "native" && model.api.package !== undefined) return Effect.succeed(model.api.package)
|
|
||||||
const specifier = model.api.type === "aisdk" ? aisdkPackages[model.api.package] : undefined
|
|
||||||
if (specifier !== undefined) return Effect.succeed(specifier)
|
|
||||||
}
|
|
||||||
return Effect.fail(
|
|
||||||
new UnsupportedApiError({
|
|
||||||
providerID: model.providerID,
|
|
||||||
modelID: model.id,
|
|
||||||
api: apiName(model),
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
const apiKey = (model: ModelV2.Info, credential?: Credential.Value) => {
|
|
||||||
if (credential?.type === "key") return credential.key
|
|
||||||
if (credential?.type === "oauth") return credential.access
|
|
||||||
const value = model.request.body.apiKey ?? model.api.settings?.apiKey
|
|
||||||
if (typeof value === "string") return value
|
|
||||||
return undefined
|
|
||||||
}
|
|
||||||
|
|
||||||
const stripApiKey = (body: ModelV2.Info["request"]["body"]) => {
|
|
||||||
if (!Object.hasOwn(body, "apiKey")) return body
|
|
||||||
return Object.fromEntries(Object.entries(body).filter(([key]) => key !== "apiKey"))
|
|
||||||
}
|
|
||||||
|
|
||||||
const requestSettings = (request: ModelV2.Info["request"]) => {
|
|
||||||
if (!("settings" in request)) return undefined
|
|
||||||
const settings = request.settings
|
|
||||||
if (!Predicate.isReadonlyObject(settings)) return undefined
|
|
||||||
if (Object.keys(settings).length === 0) return undefined
|
|
||||||
return settings
|
|
||||||
}
|
|
||||||
|
|
||||||
const settingsString = (settings: ModelV2.Info["api"]["settings"], key: string) => {
|
|
||||||
const value = settings?.[key]
|
|
||||||
if (typeof value === "string") return value
|
|
||||||
return undefined
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Resolves models from the catalog belonging to the current Location runtime. */
|
/** Resolves models from the catalog belonging to the current Location runtime. */
|
||||||
export const locationLayer = Layer.effect(
|
export const locationLayer = Layer.effect(
|
||||||
|
|||||||
@@ -213,40 +213,6 @@ describe("CatalogV2", () => {
|
|||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
|
|
||||||
it.effect("keeps an explicit native package api over the provider api", () =>
|
|
||||||
Effect.gen(function* () {
|
|
||||||
const catalog = yield* Catalog.Service
|
|
||||||
const providerID = ProviderV2.ID.make("test")
|
|
||||||
const modelID = ModelV2.ID.make("model")
|
|
||||||
yield* catalog.transform((catalog) => {
|
|
||||||
catalog.provider.update(providerID, (provider) => {
|
|
||||||
provider.api = {
|
|
||||||
type: "aisdk",
|
|
||||||
package: "@ai-sdk/openai",
|
|
||||||
}
|
|
||||||
})
|
|
||||||
catalog.model.update(providerID, modelID, (model) => {
|
|
||||||
// Plugins retarget models at explicit native packages (e.g. ChatGPT
|
|
||||||
// subscription models at the codex package); empty settings must not
|
|
||||||
// demote the api back to the provider placeholder rule.
|
|
||||||
model.api = {
|
|
||||||
type: "native",
|
|
||||||
id: model.api.id,
|
|
||||||
package: "@opencode-ai/llm/providers/openai/codex",
|
|
||||||
settings: {},
|
|
||||||
}
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
expect(required(yield* catalog.model.get(providerID, modelID)).api).toEqual({
|
|
||||||
id: modelID,
|
|
||||||
type: "native",
|
|
||||||
package: "@opencode-ai/llm/providers/openai/codex",
|
|
||||||
settings: {},
|
|
||||||
})
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
|
|
||||||
it.effect("resolves provider and model request merges", () =>
|
it.effect("resolves provider and model request merges", () =>
|
||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
const catalog = yield* Catalog.Service
|
const catalog = yield* Catalog.Service
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ import { describe, expect } from "bun:test"
|
|||||||
import type { LanguageModelV3 } from "@ai-sdk/provider"
|
import type { LanguageModelV3 } from "@ai-sdk/provider"
|
||||||
import { Effect } from "effect"
|
import { Effect } from "effect"
|
||||||
import { Catalog } from "@opencode-ai/core/catalog"
|
import { Catalog } from "@opencode-ai/core/catalog"
|
||||||
import { Credential } from "@opencode-ai/core/credential"
|
|
||||||
import { Integration } from "@opencode-ai/core/integration"
|
import { Integration } from "@opencode-ai/core/integration"
|
||||||
import { ModelV2 } from "@opencode-ai/core/model"
|
import { ModelV2 } from "@opencode-ai/core/model"
|
||||||
import { PluginV2 } from "@opencode-ai/core/plugin"
|
import { PluginV2 } from "@opencode-ai/core/plugin"
|
||||||
@@ -17,6 +16,7 @@ const it = testEffect(PluginTestLayer)
|
|||||||
|
|
||||||
const addPlugin = Effect.fn(function* () {
|
const addPlugin = Effect.fn(function* () {
|
||||||
const plugin = yield* PluginV2.Service
|
const plugin = yield* PluginV2.Service
|
||||||
|
const aisdk = yield* AISDK.Service
|
||||||
const host = yield* PluginHost.make(plugin)
|
const host = yield* PluginHost.make(plugin)
|
||||||
const integrations = yield* Integration.Service
|
const integrations = yield* Integration.Service
|
||||||
yield* OpenAIPlugin.effect(host).pipe(Effect.provideService(Integration.Service, integrations))
|
yield* OpenAIPlugin.effect(host).pipe(Effect.provideService(Integration.Service, integrations))
|
||||||
@@ -61,6 +61,7 @@ describe("OpenAIPlugin", () => {
|
|||||||
|
|
||||||
it.effect("creates an OpenAI SDK for @ai-sdk/openai using the provider ID as SDK name", () =>
|
it.effect("creates an OpenAI SDK for @ai-sdk/openai using the provider ID as SDK name", () =>
|
||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
|
const plugin = yield* PluginV2.Service
|
||||||
const aisdk = yield* AISDK.Service
|
const aisdk = yield* AISDK.Service
|
||||||
yield* addPlugin()
|
yield* addPlugin()
|
||||||
const result = yield* aisdk.runSDK({
|
const result = yield* aisdk.runSDK({
|
||||||
@@ -77,6 +78,7 @@ describe("OpenAIPlugin", () => {
|
|||||||
|
|
||||||
it.effect("ignores non-OpenAI SDK packages", () =>
|
it.effect("ignores non-OpenAI SDK packages", () =>
|
||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
|
const plugin = yield* PluginV2.Service
|
||||||
const aisdk = yield* AISDK.Service
|
const aisdk = yield* AISDK.Service
|
||||||
yield* addPlugin()
|
yield* addPlugin()
|
||||||
const result = yield* aisdk.runSDK({
|
const result = yield* aisdk.runSDK({
|
||||||
@@ -93,6 +95,7 @@ describe("OpenAIPlugin", () => {
|
|||||||
|
|
||||||
it.effect("uses the Responses API for language models", () =>
|
it.effect("uses the Responses API for language models", () =>
|
||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
|
const plugin = yield* PluginV2.Service
|
||||||
const aisdk = yield* AISDK.Service
|
const aisdk = yield* AISDK.Service
|
||||||
const calls: string[] = []
|
const calls: string[] = []
|
||||||
yield* addPlugin()
|
yield* addPlugin()
|
||||||
@@ -111,6 +114,7 @@ describe("OpenAIPlugin", () => {
|
|||||||
|
|
||||||
it.effect("ignores non-OpenAI providers", () =>
|
it.effect("ignores non-OpenAI providers", () =>
|
||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
|
const plugin = yield* PluginV2.Service
|
||||||
const aisdk = yield* AISDK.Service
|
const aisdk = yield* AISDK.Service
|
||||||
const calls: string[] = []
|
const calls: string[] = []
|
||||||
yield* addPlugin()
|
yield* addPlugin()
|
||||||
@@ -169,54 +173,4 @@ describe("OpenAIPlugin", () => {
|
|||||||
).toBe(true)
|
).toBe(true)
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
|
|
||||||
it.effect("routes eligible models to the codex package for ChatGPT connections", () =>
|
|
||||||
Effect.gen(function* () {
|
|
||||||
const catalog = yield* Catalog.Service
|
|
||||||
const credentials = yield* Credential.Service
|
|
||||||
yield* catalog.transform((catalog) => {
|
|
||||||
const item = ProviderV2.Info.make({
|
|
||||||
...ProviderV2.Info.empty(ProviderV2.ID.openai),
|
|
||||||
api: { type: "aisdk", package: "@ai-sdk/openai", settings: { store: false } },
|
|
||||||
})
|
|
||||||
catalog.provider.update(item.id, (draft) => {
|
|
||||||
draft.api = item.api
|
|
||||||
})
|
|
||||||
catalog.model.update(item.id, ModelV2.ID.make("gpt-5.3-codex-spark"), (draft) => {
|
|
||||||
draft.api = { id: ModelV2.ID.make("gpt-5.3-codex-spark"), type: "aisdk", package: "@ai-sdk/openai" }
|
|
||||||
draft.cost = [{ input: 1, output: 2, cache: { read: 3, write: 4 } }]
|
|
||||||
})
|
|
||||||
catalog.model.update(item.id, ModelV2.ID.make("gpt-5"), (draft) => {
|
|
||||||
draft.api = { id: ModelV2.ID.make("gpt-5"), type: "aisdk", package: "@ai-sdk/openai" }
|
|
||||||
})
|
|
||||||
})
|
|
||||||
yield* credentials.create({
|
|
||||||
integrationID: Integration.ID.make("openai"),
|
|
||||||
value: Credential.OAuth.make({
|
|
||||||
type: "oauth",
|
|
||||||
methodID: Integration.MethodID.make("chatgpt-browser"),
|
|
||||||
access: "access",
|
|
||||||
refresh: "refresh",
|
|
||||||
expires: Date.now() + 60_000,
|
|
||||||
metadata: { accountID: "account-123" },
|
|
||||||
}),
|
|
||||||
})
|
|
||||||
|
|
||||||
yield* addPlugin()
|
|
||||||
|
|
||||||
expect(
|
|
||||||
required(yield* catalog.model.get(ProviderV2.ID.openai, ModelV2.ID.make("gpt-5.3-codex-spark"))),
|
|
||||||
).toMatchObject({
|
|
||||||
enabled: true,
|
|
||||||
api: {
|
|
||||||
type: "native",
|
|
||||||
id: "gpt-5.3-codex-spark",
|
|
||||||
package: "@opencode-ai/llm/providers/openai/codex",
|
|
||||||
settings: { store: false },
|
|
||||||
},
|
|
||||||
cost: [{ input: 0, output: 0, cache: { read: 0, write: 0 } }],
|
|
||||||
})
|
|
||||||
expect(required(yield* catalog.model.get(ProviderV2.ID.openai, ModelV2.ID.make("gpt-5"))).enabled).toBe(false)
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -20,12 +20,7 @@ type Api =
|
|||||||
readonly url?: string
|
readonly url?: string
|
||||||
readonly settings?: Record<string, unknown>
|
readonly settings?: Record<string, unknown>
|
||||||
}
|
}
|
||||||
| {
|
| { readonly type: "native"; readonly url?: string; readonly settings: Record<string, unknown> }
|
||||||
readonly type: "native"
|
|
||||||
readonly package?: string
|
|
||||||
readonly url?: string
|
|
||||||
readonly settings: Record<string, unknown>
|
|
||||||
}
|
|
||||||
|
|
||||||
const model = (api: Api, variants: ModelV2.Info["variants"] = []) =>
|
const model = (api: Api, variants: ModelV2.Info["variants"] = []) =>
|
||||||
ModelV2.Info.make({
|
ModelV2.Info.make({
|
||||||
@@ -274,71 +269,6 @@ describe("SessionRunnerModel", () => {
|
|||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
|
|
||||||
it.effect("maps native provider package models into bearer-authenticated routes", () =>
|
|
||||||
Effect.gen(function* () {
|
|
||||||
const resolved = yield* SessionRunnerModel.fromCatalogModel(
|
|
||||||
ModelV2.Info.make({
|
|
||||||
...model({
|
|
||||||
type: "native",
|
|
||||||
package: "@opencode-ai/llm/providers/openai",
|
|
||||||
url: "https://openai.example/v1",
|
|
||||||
settings: {},
|
|
||||||
}),
|
|
||||||
request: { headers: {}, body: {} },
|
|
||||||
}),
|
|
||||||
Credential.Key.make({ type: "key", key: "secret" }),
|
|
||||||
)
|
|
||||||
const headers = yield* resolved.route.auth.apply({
|
|
||||||
request: LLM.request({ model: resolved, prompt: "Hello" }),
|
|
||||||
method: "POST",
|
|
||||||
url: "https://openai.example/v1/responses",
|
|
||||||
body: "{}",
|
|
||||||
headers: Headers.empty,
|
|
||||||
})
|
|
||||||
|
|
||||||
expect(resolved.route).toMatchObject({
|
|
||||||
id: "openai-responses",
|
|
||||||
endpoint: { baseURL: "https://openai.example/v1" },
|
|
||||||
})
|
|
||||||
expect(headers.authorization).toBe("Bearer secret")
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
|
|
||||||
it.effect("routes native ChatGPT OAuth credentials to the codex backend", () =>
|
|
||||||
Effect.gen(function* () {
|
|
||||||
const resolved = yield* SessionRunnerModel.fromCatalogModel(
|
|
||||||
ModelV2.Info.make({
|
|
||||||
...model({
|
|
||||||
type: "native",
|
|
||||||
package: "@opencode-ai/llm/providers/openai/codex",
|
|
||||||
settings: {},
|
|
||||||
}),
|
|
||||||
request: { headers: {}, body: {} },
|
|
||||||
}),
|
|
||||||
Credential.OAuth.make({
|
|
||||||
type: "oauth",
|
|
||||||
methodID: Integration.MethodID.make("chatgpt-browser"),
|
|
||||||
access: "oauth-token",
|
|
||||||
refresh: "refresh",
|
|
||||||
expires: Date.now() + 60_000,
|
|
||||||
metadata: { accountID: "account-123" },
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
const request = LLM.request({ model: resolved, prompt: "Hello" })
|
|
||||||
const headers = yield* resolved.route.auth.apply({
|
|
||||||
request,
|
|
||||||
method: "POST",
|
|
||||||
url: "https://chatgpt.com/backend-api/codex/responses",
|
|
||||||
body: "{}",
|
|
||||||
headers: Headers.empty,
|
|
||||||
})
|
|
||||||
|
|
||||||
expect(resolved.route.endpoint.baseURL).toBe("https://chatgpt.com/backend-api/codex")
|
|
||||||
expect(headers.authorization).toBe("Bearer oauth-token")
|
|
||||||
expect(headers["chatgpt-account-id"]).toBe("account-123")
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
|
|
||||||
it.effect("prefers stored credentials over configured auth", () =>
|
it.effect("prefers stored credentials over configured auth", () =>
|
||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
const credential = Credential.Key.make({ type: "key", key: "stored-secret", metadata: { tenant: "work" } })
|
const credential = Credential.Key.make({ type: "key", key: "stored-secret", metadata: { tenant: "work" } })
|
||||||
@@ -412,11 +342,6 @@ describe("SessionRunnerModel", () => {
|
|||||||
),
|
),
|
||||||
).toBe(false)
|
).toBe(false)
|
||||||
expect(SessionRunnerModel.supported(model({ type: "native", settings: {} }))).toBe(false)
|
expect(SessionRunnerModel.supported(model({ type: "native", settings: {} }))).toBe(false)
|
||||||
expect(
|
|
||||||
SessionRunnerModel.supported(
|
|
||||||
model({ type: "native", package: "@opencode-ai/llm/providers/openai", settings: {} }),
|
|
||||||
),
|
|
||||||
).toBe(true)
|
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -14,7 +14,6 @@
|
|||||||
".": "./src/index.ts",
|
".": "./src/index.ts",
|
||||||
"./route": "./src/route/index.ts",
|
"./route": "./src/route/index.ts",
|
||||||
"./provider": "./src/provider.ts",
|
"./provider": "./src/provider.ts",
|
||||||
"./provider-package": "./src/provider-package.ts",
|
|
||||||
"./providers": "./src/providers/index.ts",
|
"./providers": "./src/providers/index.ts",
|
||||||
"./providers/amazon-bedrock": "./src/providers/amazon-bedrock.ts",
|
"./providers/amazon-bedrock": "./src/providers/amazon-bedrock.ts",
|
||||||
"./providers/anthropic": "./src/providers/anthropic.ts",
|
"./providers/anthropic": "./src/providers/anthropic.ts",
|
||||||
@@ -23,7 +22,6 @@
|
|||||||
"./providers/github-copilot": "./src/providers/github-copilot.ts",
|
"./providers/github-copilot": "./src/providers/github-copilot.ts",
|
||||||
"./providers/google": "./src/providers/google.ts",
|
"./providers/google": "./src/providers/google.ts",
|
||||||
"./providers/openai": "./src/providers/openai.ts",
|
"./providers/openai": "./src/providers/openai.ts",
|
||||||
"./providers/openai/codex": "./src/providers/openai/codex.ts",
|
|
||||||
"./providers/openai-compatible": "./src/providers/openai-compatible.ts",
|
"./providers/openai-compatible": "./src/providers/openai-compatible.ts",
|
||||||
"./providers/openai-compatible-profile": "./src/providers/openai-compatible-profile.ts",
|
"./providers/openai-compatible-profile": "./src/providers/openai-compatible-profile.ts",
|
||||||
"./providers/openrouter": "./src/providers/openrouter.ts",
|
"./providers/openrouter": "./src/providers/openrouter.ts",
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
export { LLMClient } from "./route/client"
|
export { LLMClient } from "./route/client"
|
||||||
export { Auth } from "./route/auth"
|
export { Auth } from "./route/auth"
|
||||||
export { Provider } from "./provider"
|
export { Provider } from "./provider"
|
||||||
export { ProviderPackage } from "./provider-package"
|
|
||||||
export { isContextOverflow, isContextOverflowFailure } from "./provider-error"
|
export { isContextOverflow, isContextOverflowFailure } from "./provider-error"
|
||||||
export type {
|
export type {
|
||||||
RouteModelInput,
|
RouteModelInput,
|
||||||
|
|||||||
@@ -1,18 +0,0 @@
|
|||||||
export * as ProviderPackage from "./provider-package"
|
|
||||||
|
|
||||||
import type { Model } from "./schema"
|
|
||||||
|
|
||||||
export interface Settings extends Readonly<Record<string, unknown>> {
|
|
||||||
readonly apiKey?: string
|
|
||||||
readonly baseURL?: string
|
|
||||||
readonly providerOptions?: Readonly<Record<string, unknown>>
|
|
||||||
readonly headers?: Readonly<Record<string, string>>
|
|
||||||
readonly body?: Readonly<Record<string, unknown>>
|
|
||||||
readonly limits?: { readonly context: number; readonly output: number }
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface Definition<S extends Settings = Settings> {
|
|
||||||
readonly model: (modelID: string, settings: S) => Model
|
|
||||||
}
|
|
||||||
|
|
||||||
export const define = <S extends Settings = Settings>(model: (modelID: string, settings: S) => Model) => model
|
|
||||||
@@ -2,16 +2,13 @@ import type { RouteDefaultsInput } from "../route/client"
|
|||||||
import { Auth } from "../route/auth"
|
import { Auth } from "../route/auth"
|
||||||
import type { ProviderAuthOption } from "../route/auth-options"
|
import type { ProviderAuthOption } from "../route/auth-options"
|
||||||
import { ProviderID, type ModelID } from "../schema"
|
import { ProviderID, type ModelID } from "../schema"
|
||||||
import { ProviderPackage } from "../provider-package"
|
import * as AnthropicMessages from "../protocols/anthropic-messages"
|
||||||
import { AnthropicMessages } from "../protocols/anthropic-messages"
|
|
||||||
|
|
||||||
export const id = ProviderID.make("anthropic")
|
export const id = ProviderID.make("anthropic")
|
||||||
|
|
||||||
export const routes = [AnthropicMessages.route]
|
export const routes = [AnthropicMessages.route]
|
||||||
|
|
||||||
type Config = RouteDefaultsInput & ProviderAuthOption<"optional"> & { readonly baseURL?: string }
|
export type Config = RouteDefaultsInput & ProviderAuthOption<"optional"> & { readonly baseURL?: string }
|
||||||
|
|
||||||
export interface AnthropicSettings extends ProviderPackage.Settings {}
|
|
||||||
|
|
||||||
const auth = (options: ProviderAuthOption<"optional">) => {
|
const auth = (options: ProviderAuthOption<"optional">) => {
|
||||||
if ("auth" in options && options.auth) return options.auth
|
if ("auth" in options && options.auth) return options.auth
|
||||||
@@ -34,15 +31,5 @@ export const configure = (input: Config = {}) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const model = ProviderPackage.define((modelID, settings: AnthropicSettings) =>
|
export const provider = configure()
|
||||||
AnthropicMessages.route
|
export const model = provider.model
|
||||||
.with({
|
|
||||||
auth: settings.apiKey === undefined ? Auth.none : Auth.header("x-api-key", settings.apiKey),
|
|
||||||
endpoint: { baseURL: settings.baseURL },
|
|
||||||
headers: settings.headers,
|
|
||||||
providerOptions: settings.providerOptions && { anthropic: settings.providerOptions },
|
|
||||||
http: { body: settings.body },
|
|
||||||
limits: settings.limits,
|
|
||||||
})
|
|
||||||
.model({ id: modelID }),
|
|
||||||
)
|
|
||||||
|
|||||||
@@ -6,7 +6,6 @@ export { CloudflareAIGateway, CloudflareWorkersAI } from "./cloudflare"
|
|||||||
export * as GitHubCopilot from "./github-copilot"
|
export * as GitHubCopilot from "./github-copilot"
|
||||||
export * as Google from "./google"
|
export * as Google from "./google"
|
||||||
export * as OpenAI from "./openai"
|
export * as OpenAI from "./openai"
|
||||||
export * as OpenAICodex from "./openai/codex"
|
|
||||||
export * as OpenAICompatible from "./openai-compatible"
|
export * as OpenAICompatible from "./openai-compatible"
|
||||||
export * as OpenRouter from "./openrouter"
|
export * as OpenRouter from "./openrouter"
|
||||||
export * as XAI from "./xai"
|
export * as XAI from "./xai"
|
||||||
|
|||||||
@@ -1,9 +1,8 @@
|
|||||||
import { ProviderID, type ModelID } from "../schema"
|
import { ProviderID, type ModelID } from "../schema"
|
||||||
import { OpenAICompatibleChat } from "../protocols/openai-compatible-chat"
|
import * as OpenAICompatibleChat from "../protocols/openai-compatible-chat"
|
||||||
import type { RouteDefaultsInput } from "../route/client"
|
import type { RouteDefaultsInput } from "../route/client"
|
||||||
import { AuthOptions, type ProviderAuthOption } from "../route/auth-options"
|
import { AuthOptions, type ProviderAuthOption } from "../route/auth-options"
|
||||||
import { Auth } from "../route/auth"
|
import { profiles, type OpenAICompatibleProfile } from "./openai-compatible-profile"
|
||||||
import { ProviderPackage } from "../provider-package"
|
|
||||||
|
|
||||||
export const id = ProviderID.make("openai-compatible")
|
export const id = ProviderID.make("openai-compatible")
|
||||||
|
|
||||||
@@ -13,7 +12,10 @@ type GenericModelOptions = RouteDefaultsInput &
|
|||||||
readonly baseURL: string
|
readonly baseURL: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface OpenAICompatibleSettings extends ProviderPackage.Settings {}
|
export type FamilyModelOptions = RouteDefaultsInput &
|
||||||
|
ProviderAuthOption<"optional"> & {
|
||||||
|
readonly baseURL?: string
|
||||||
|
}
|
||||||
|
|
||||||
export const routes = [OpenAICompatibleChat.route]
|
export const routes = [OpenAICompatibleChat.route]
|
||||||
|
|
||||||
@@ -33,15 +35,31 @@ export const configure = (input: GenericModelOptions) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const model = ProviderPackage.define((modelID, settings: OpenAICompatibleSettings) =>
|
const define = (profile: OpenAICompatibleProfile) => {
|
||||||
OpenAICompatibleChat.route
|
const configureProfile = (input: FamilyModelOptions = {}) => {
|
||||||
.with({
|
const facade = configure({
|
||||||
auth: settings.apiKey === undefined ? Auth.none : Auth.bearer(settings.apiKey),
|
...input,
|
||||||
endpoint: { baseURL: settings.baseURL },
|
baseURL: input.baseURL ?? profile.baseURL,
|
||||||
headers: settings.headers,
|
provider: profile.provider,
|
||||||
providerOptions: settings.providerOptions && { openai: settings.providerOptions },
|
|
||||||
http: { body: settings.body },
|
|
||||||
limits: settings.limits,
|
|
||||||
})
|
})
|
||||||
.model({ id: modelID, provider: "openai-compatible" }),
|
return {
|
||||||
)
|
id: ProviderID.make(profile.provider),
|
||||||
|
model: facade.model,
|
||||||
|
configure: configureProfile,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return configureProfile()
|
||||||
|
}
|
||||||
|
|
||||||
|
export const provider = {
|
||||||
|
id,
|
||||||
|
configure,
|
||||||
|
}
|
||||||
|
|
||||||
|
export const baseten = define(profiles.baseten)
|
||||||
|
export const cerebras = define(profiles.cerebras)
|
||||||
|
export const deepinfra = define(profiles.deepinfra)
|
||||||
|
export const deepseek = define(profiles.deepseek)
|
||||||
|
export const fireworks = define(profiles.fireworks)
|
||||||
|
export const groq = define(profiles.groq)
|
||||||
|
export const togetherai = define(profiles.togetherai)
|
||||||
|
|||||||
@@ -1,10 +1,8 @@
|
|||||||
import { AuthOptions, type ProviderAuthOption } from "../route/auth-options"
|
import { AuthOptions, type ProviderAuthOption } from "../route/auth-options"
|
||||||
import { Auth } from "../route/auth"
|
|
||||||
import type { Route, RouteDefaultsInput } from "../route/client"
|
import type { Route, RouteDefaultsInput } from "../route/client"
|
||||||
import { ProviderID, type ModelID } from "../schema"
|
import { ProviderID, type ModelID } from "../schema"
|
||||||
import { ProviderPackage } from "../provider-package"
|
import * as OpenAIChat from "../protocols/openai-chat"
|
||||||
import { OpenAIChat } from "../protocols/openai-chat"
|
import * as OpenAIResponses from "../protocols/openai-responses"
|
||||||
import { OpenAIResponses } from "../protocols/openai-responses"
|
|
||||||
import { withOpenAIOptions, type OpenAIProviderOptionsInput } from "./openai-options"
|
import { withOpenAIOptions, type OpenAIProviderOptionsInput } from "./openai-options"
|
||||||
|
|
||||||
export type { OpenAIOptionsInput, OpenAIResponseIncludable } from "./openai-options"
|
export type { OpenAIOptionsInput, OpenAIResponseIncludable } from "./openai-options"
|
||||||
@@ -16,15 +14,13 @@ export const routes = [OpenAIResponses.route, OpenAIResponses.webSocketRoute, Op
|
|||||||
// This provider facade wraps the lower-level Responses and Chat model factories
|
// This provider facade wraps the lower-level Responses and Chat model factories
|
||||||
// with OpenAI-specific conveniences: typed options, API-key sugar, env fallback,
|
// with OpenAI-specific conveniences: typed options, API-key sugar, env fallback,
|
||||||
// and default option normalization.
|
// and default option normalization.
|
||||||
type Config = RouteDefaultsInput &
|
export type Config = RouteDefaultsInput &
|
||||||
ProviderAuthOption<"optional"> & {
|
ProviderAuthOption<"optional"> & {
|
||||||
readonly baseURL?: string
|
readonly baseURL?: string
|
||||||
readonly queryParams?: Record<string, string>
|
readonly queryParams?: Record<string, string>
|
||||||
readonly providerOptions?: OpenAIProviderOptionsInput
|
readonly providerOptions?: OpenAIProviderOptionsInput
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface OpenAISettings extends ProviderPackage.Settings {}
|
|
||||||
|
|
||||||
const auth = (options: ProviderAuthOption<"optional">) => AuthOptions.bearer(options, "OPENAI_API_KEY")
|
const auth = (options: ProviderAuthOption<"optional">) => AuthOptions.bearer(options, "OPENAI_API_KEY")
|
||||||
|
|
||||||
const defaults = (input: Config) => {
|
const defaults = (input: Config) => {
|
||||||
@@ -59,15 +55,9 @@ export const configure = (input: Config = {}) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const model = ProviderPackage.define((modelID, settings: OpenAISettings) =>
|
export const provider = configure()
|
||||||
OpenAIResponses.route
|
|
||||||
.with({
|
export const model = provider.model
|
||||||
auth: settings.apiKey === undefined ? Auth.none : Auth.bearer(settings.apiKey),
|
export const responses = provider.responses
|
||||||
endpoint: { baseURL: settings.baseURL },
|
export const responsesWebSocket = provider.responsesWebSocket
|
||||||
headers: settings.headers,
|
export const chat = provider.chat
|
||||||
providerOptions: settings.providerOptions && { openai: settings.providerOptions },
|
|
||||||
http: { body: settings.body },
|
|
||||||
limits: settings.limits,
|
|
||||||
})
|
|
||||||
.model({ id: modelID }),
|
|
||||||
)
|
|
||||||
|
|||||||
@@ -1,22 +0,0 @@
|
|||||||
import { Auth } from "../../route/auth"
|
|
||||||
import { ProviderPackage } from "../../provider-package"
|
|
||||||
import { OpenAIResponses } from "../../protocols/openai-responses"
|
|
||||||
|
|
||||||
export interface OpenAICodexSettings extends ProviderPackage.Settings {
|
|
||||||
readonly accountID?: string
|
|
||||||
}
|
|
||||||
|
|
||||||
export const model = ProviderPackage.define((modelID, settings: OpenAICodexSettings) =>
|
|
||||||
OpenAIResponses.route
|
|
||||||
.with({
|
|
||||||
auth: (settings.apiKey === undefined ? Auth.none : Auth.bearer(settings.apiKey)).andThen(
|
|
||||||
settings.accountID === undefined ? Auth.none : Auth.headers({ "chatgpt-account-id": settings.accountID }),
|
|
||||||
),
|
|
||||||
endpoint: { baseURL: "https://chatgpt.com/backend-api/codex" },
|
|
||||||
headers: settings.headers,
|
|
||||||
providerOptions: settings.providerOptions && { openai: settings.providerOptions },
|
|
||||||
http: { body: settings.body },
|
|
||||||
limits: settings.limits,
|
|
||||||
})
|
|
||||||
.model({ id: modelID }),
|
|
||||||
)
|
|
||||||
@@ -42,9 +42,13 @@ export type MediaPart = Schema.Schema.Type<typeof MediaPart>
|
|||||||
|
|
||||||
export { ToolContent, ToolFileContent, ToolTextContent }
|
export { ToolContent, ToolFileContent, ToolTextContent }
|
||||||
|
|
||||||
// Standalone schema const so the derived type does not participate in the
|
const isToolResultValue = (value: unknown): value is ToolResultValue =>
|
||||||
// Object.assign self-reference below; keeps checking order-independent.
|
isRecord(value) &&
|
||||||
const toolResultValueSchema = Schema.Union([
|
(value.type === "text" || value.type === "json" || value.type === "error" || value.type === "content") &&
|
||||||
|
"value" in value
|
||||||
|
|
||||||
|
export const ToolResultValue = Object.assign(
|
||||||
|
Schema.Union([
|
||||||
Schema.Struct({
|
Schema.Struct({
|
||||||
type: Schema.Literal("json"),
|
type: Schema.Literal("json"),
|
||||||
value: Schema.Unknown,
|
value: Schema.Unknown,
|
||||||
@@ -61,22 +65,17 @@ const toolResultValueSchema = Schema.Union([
|
|||||||
type: Schema.Literal("content"),
|
type: Schema.Literal("content"),
|
||||||
value: Schema.Array(ToolContent),
|
value: Schema.Array(ToolContent),
|
||||||
}),
|
}),
|
||||||
]).annotate({ identifier: "LLM.ToolResult" })
|
]).annotate({ identifier: "LLM.ToolResult" }),
|
||||||
|
{
|
||||||
const isToolResultValue = (value: unknown): value is ToolResultValue =>
|
|
||||||
isRecord(value) &&
|
|
||||||
(value.type === "text" || value.type === "json" || value.type === "error" || value.type === "content") &&
|
|
||||||
"value" in value
|
|
||||||
|
|
||||||
export const ToolResultValue = Object.assign(toolResultValueSchema, {
|
|
||||||
is: isToolResultValue,
|
is: isToolResultValue,
|
||||||
make: (value: unknown, type: ToolResultValue["type"] = "json"): ToolResultValue => {
|
make: (value: unknown, type: ToolResultValue["type"] = "json"): ToolResultValue => {
|
||||||
if (isToolResultValue(value)) return value
|
if (isToolResultValue(value)) return value
|
||||||
if (type === "content") return { type, value: Array.isArray(value) ? value : [] }
|
if (type === "content") return { type, value: Array.isArray(value) ? value : [] }
|
||||||
return { type, value }
|
return { type, value }
|
||||||
},
|
},
|
||||||
})
|
},
|
||||||
export type ToolResultValue = Schema.Schema.Type<typeof toolResultValueSchema>
|
)
|
||||||
|
export type ToolResultValue = Schema.Schema.Type<typeof ToolResultValue>
|
||||||
|
|
||||||
export interface ToolOutput {
|
export interface ToolOutput {
|
||||||
readonly structured: unknown
|
readonly structured: unknown
|
||||||
|
|||||||
@@ -67,6 +67,7 @@ requiredAuthModel("custom-model", {})
|
|||||||
// @ts-expect-error auth is an override, so apiKey cannot be supplied with it.
|
// @ts-expect-error auth is an override, so apiKey cannot be supplied with it.
|
||||||
requiredAuthModel("custom-model", { apiKey: "key", auth })
|
requiredAuthModel("custom-model", { apiKey: "key", auth })
|
||||||
|
|
||||||
|
OpenAI.responses("gpt-4.1-mini")
|
||||||
OpenAI.configure({}).responses("gpt-4.1-mini")
|
OpenAI.configure({}).responses("gpt-4.1-mini")
|
||||||
OpenAI.configure({ apiKey: "sk-test" }).responses("gpt-4.1-mini")
|
OpenAI.configure({ apiKey: "sk-test" }).responses("gpt-4.1-mini")
|
||||||
OpenAI.configure({ apiKey: configApiKey }).responses("gpt-4.1-mini")
|
OpenAI.configure({ apiKey: configApiKey }).responses("gpt-4.1-mini")
|
||||||
@@ -98,6 +99,7 @@ OpenAI.configure({ providerOptions: { openai: { store: "false" } } })
|
|||||||
// @ts-expect-error auth is an override, so OpenAI rejects apiKey with auth.
|
// @ts-expect-error auth is an override, so OpenAI rejects apiKey with auth.
|
||||||
OpenAI.configure({ apiKey: "sk-test", auth: RuntimeAuth.bearer("oauth-token") })
|
OpenAI.configure({ apiKey: "sk-test", auth: RuntimeAuth.bearer("oauth-token") })
|
||||||
|
|
||||||
|
OpenAI.chat("gpt-4.1-mini")
|
||||||
OpenAI.configure({ apiKey: "sk-test" }).chat("gpt-4.1-mini")
|
OpenAI.configure({ apiKey: "sk-test" }).chat("gpt-4.1-mini")
|
||||||
OpenAI.configure({ apiKey: configApiKey }).chat("gpt-4.1-mini")
|
OpenAI.configure({ apiKey: configApiKey }).chat("gpt-4.1-mini")
|
||||||
OpenAI.configure({ auth: RuntimeAuth.bearer("oauth-token") }).chat("gpt-4.1-mini")
|
OpenAI.configure({ auth: RuntimeAuth.bearer("oauth-token") }).chat("gpt-4.1-mini")
|
||||||
@@ -153,14 +155,9 @@ XAI.configure({ apiKey: "xai-key" }).responses("grok-4", {})
|
|||||||
// @ts-expect-error xAI Chat selectors only accept model ids.
|
// @ts-expect-error xAI Chat selectors only accept model ids.
|
||||||
XAI.configure({ apiKey: "xai-key" }).chat("grok-4", {})
|
XAI.configure({ apiKey: "xai-key" }).chat("grok-4", {})
|
||||||
|
|
||||||
OpenAICompatible.configure({
|
OpenAICompatible.deepseek.configure({ apiKey: "deepseek-key" }).model("deepseek-chat")
|
||||||
apiKey: "deepseek-key",
|
// @ts-expect-error OpenAI-compatible family selectors only accept model ids.
|
||||||
provider: "deepseek",
|
OpenAICompatible.deepseek.configure({ apiKey: "deepseek-key" }).model("deepseek-chat", {})
|
||||||
baseURL: "https://api.deepseek.com/v1",
|
|
||||||
}).model("deepseek-chat")
|
|
||||||
OpenAICompatible.configure({ apiKey: "deepseek-key", provider: "deepseek", baseURL: "https://api.deepseek.com/v1" })
|
|
||||||
// @ts-expect-error OpenAI-compatible model selectors only accept model ids.
|
|
||||||
.model("deepseek-chat", {})
|
|
||||||
|
|
||||||
Cloudflare.CloudflareWorkersAI.configure({ accountId: "account", apiKey: "cf-key" }).model("@cf/meta/llama")
|
Cloudflare.CloudflareWorkersAI.configure({ accountId: "account", apiKey: "cf-key" }).model("@cf/meta/llama")
|
||||||
// @ts-expect-error Cloudflare Workers AI model selectors only accept model ids.
|
// @ts-expect-error Cloudflare Workers AI model selectors only accept model ids.
|
||||||
|
|||||||
@@ -1,13 +1,11 @@
|
|||||||
import { describe, expect, test } from "bun:test"
|
import { describe, expect, test } from "bun:test"
|
||||||
import { LLM, LLMClient, Provider, ProviderPackage } from "@opencode-ai/llm"
|
import { LLM, LLMClient, Provider } from "@opencode-ai/llm"
|
||||||
import { Route, Protocol } from "@opencode-ai/llm/route"
|
import { Route, Protocol } from "@opencode-ai/llm/route"
|
||||||
import { Provider as ProviderSubpath } from "@opencode-ai/llm/provider"
|
import { Provider as ProviderSubpath } from "@opencode-ai/llm/provider"
|
||||||
import { ProviderPackage as ProviderPackageSubpath } from "@opencode-ai/llm/provider-package"
|
|
||||||
import {
|
import {
|
||||||
CloudflareAIGateway,
|
CloudflareAIGateway,
|
||||||
CloudflareWorkersAI,
|
CloudflareWorkersAI,
|
||||||
OpenAI,
|
OpenAI,
|
||||||
OpenAICodex,
|
|
||||||
OpenAICompatible,
|
OpenAICompatible,
|
||||||
OpenRouter,
|
OpenRouter,
|
||||||
XAI,
|
XAI,
|
||||||
@@ -23,8 +21,6 @@ describe("public exports", () => {
|
|||||||
expect(LLMClient.layer).toBeDefined()
|
expect(LLMClient.layer).toBeDefined()
|
||||||
expect(Provider.make).toBeFunction()
|
expect(Provider.make).toBeFunction()
|
||||||
expect(ProviderSubpath.make).toBe(Provider.make)
|
expect(ProviderSubpath.make).toBe(Provider.make)
|
||||||
expect(ProviderPackage.define).toBeFunction()
|
|
||||||
expect(ProviderPackageSubpath.define).toBe(ProviderPackage.define)
|
|
||||||
})
|
})
|
||||||
|
|
||||||
test("route barrel exposes route-authoring APIs", () => {
|
test("route barrel exposes route-authoring APIs", () => {
|
||||||
@@ -34,12 +30,11 @@ describe("public exports", () => {
|
|||||||
|
|
||||||
test("provider barrels expose user-facing facades", () => {
|
test("provider barrels expose user-facing facades", () => {
|
||||||
expect(OpenAI.model).toBeFunction()
|
expect(OpenAI.model).toBeFunction()
|
||||||
|
expect(OpenAI.provider.model).toBe(OpenAI.model)
|
||||||
|
expect(OpenAI.provider.responses).toBe(OpenAI.responses)
|
||||||
|
expect(OpenAI.provider.responsesWebSocket).toBe(OpenAI.responsesWebSocket)
|
||||||
expect(OpenAI.configure({ apiKey: "fixture" }).responses).toBeFunction()
|
expect(OpenAI.configure({ apiKey: "fixture" }).responses).toBeFunction()
|
||||||
expect(OpenAICodex.model).toBeFunction()
|
expect(OpenAICompatible.deepseek.model).toBeFunction()
|
||||||
expect(OpenAICompatible.model).toBeFunction()
|
|
||||||
expect(
|
|
||||||
OpenAICompatible.configure({ baseURL: "https://api.compatible.test/v1", apiKey: "fixture" }).model,
|
|
||||||
).toBeFunction()
|
|
||||||
expect(CloudflareAIGateway.configure).toBeFunction()
|
expect(CloudflareAIGateway.configure).toBeFunction()
|
||||||
expect(CloudflareAIGateway.configure({ accountId: "fixture", gatewayApiKey: "fixture" }).model).toBeFunction()
|
expect(CloudflareAIGateway.configure({ accountId: "fixture", gatewayApiKey: "fixture" }).model).toBeFunction()
|
||||||
expect(CloudflareWorkersAI.configure).toBeFunction()
|
expect(CloudflareWorkersAI.configure).toBeFunction()
|
||||||
|
|||||||
@@ -39,21 +39,17 @@ const cloudflareAIGatewayWorkers = cloudflareAIGateway.model("workers-ai/@cf/met
|
|||||||
const cloudflareAIGatewayWorkersTools = cloudflareAIGateway.model("workers-ai/@cf/openai/gpt-oss-20b")
|
const cloudflareAIGatewayWorkersTools = cloudflareAIGateway.model("workers-ai/@cf/openai/gpt-oss-20b")
|
||||||
const cloudflareWorkersAI = cloudflareWorkers.model("@cf/meta/llama-3.1-8b-instruct")
|
const cloudflareWorkersAI = cloudflareWorkers.model("@cf/meta/llama-3.1-8b-instruct")
|
||||||
const cloudflareWorkersAITools = cloudflareWorkers.model("@cf/openai/gpt-oss-20b")
|
const cloudflareWorkersAITools = cloudflareWorkers.model("@cf/openai/gpt-oss-20b")
|
||||||
const deepseek = OpenAICompatible.configure({
|
const deepseek = OpenAICompatible.deepseek
|
||||||
apiKey: process.env.DEEPSEEK_API_KEY ?? "fixture",
|
.configure({ apiKey: process.env.DEEPSEEK_API_KEY ?? "fixture" })
|
||||||
provider: "deepseek",
|
.model("deepseek-chat")
|
||||||
baseURL: "https://api.deepseek.com/v1",
|
const together = OpenAICompatible.togetherai
|
||||||
}).model("deepseek-chat")
|
.configure({
|
||||||
const together = OpenAICompatible.configure({
|
|
||||||
apiKey: process.env.TOGETHER_AI_API_KEY ?? "fixture",
|
apiKey: process.env.TOGETHER_AI_API_KEY ?? "fixture",
|
||||||
provider: "togetherai",
|
})
|
||||||
baseURL: "https://api.together.xyz/v1",
|
.model("meta-llama/Llama-3.3-70B-Instruct-Turbo")
|
||||||
}).model("meta-llama/Llama-3.3-70B-Instruct-Turbo")
|
const groq = OpenAICompatible.groq
|
||||||
const groq = OpenAICompatible.configure({
|
.configure({ apiKey: process.env.GROQ_API_KEY ?? "fixture" })
|
||||||
apiKey: process.env.GROQ_API_KEY ?? "fixture",
|
.model("llama-3.3-70b-versatile")
|
||||||
provider: "groq",
|
|
||||||
baseURL: "https://api.groq.com/openai/v1",
|
|
||||||
}).model("llama-3.3-70b-versatile")
|
|
||||||
const openRouter = OpenRouter.configure({ apiKey: process.env.OPENROUTER_API_KEY ?? "fixture" })
|
const openRouter = OpenRouter.configure({ apiKey: process.env.OPENROUTER_API_KEY ?? "fixture" })
|
||||||
const openrouter = openRouter.model("openai/gpt-4o-mini")
|
const openrouter = openRouter.model("openai/gpt-4o-mini")
|
||||||
const openrouterGpt55 = openRouter.model("openai/gpt-5.5")
|
const openrouterGpt55 = openRouter.model("openai/gpt-5.5")
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { Effect, Schema } from "effect"
|
|||||||
import { HttpClientRequest } from "effect/unstable/http"
|
import { HttpClientRequest } from "effect/unstable/http"
|
||||||
import { LLM, Message, ToolCallPart } from "../../src"
|
import { LLM, Message, ToolCallPart } from "../../src"
|
||||||
import { Auth, LLMClient } from "../../src/route"
|
import { Auth, LLMClient } from "../../src/route"
|
||||||
|
import * as OpenAICompatible from "../../src/providers/openai-compatible"
|
||||||
import * as OpenAICompatibleChat from "../../src/protocols/openai-compatible-chat"
|
import * as OpenAICompatibleChat from "../../src/protocols/openai-compatible-chat"
|
||||||
import { it } from "../lib/effect"
|
import { it } from "../lib/effect"
|
||||||
import { dynamicResponse } from "../lib/http"
|
import { dynamicResponse } from "../lib/http"
|
||||||
@@ -39,6 +40,15 @@ const usageChunk = (usage: object) => ({
|
|||||||
usage,
|
usage,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const providerFamilies = [
|
||||||
|
["baseten", OpenAICompatible.baseten, "https://inference.baseten.co/v1"],
|
||||||
|
["cerebras", OpenAICompatible.cerebras, "https://api.cerebras.ai/v1"],
|
||||||
|
["deepinfra", OpenAICompatible.deepinfra, "https://api.deepinfra.com/v1/openai"],
|
||||||
|
["deepseek", OpenAICompatible.deepseek, "https://api.deepseek.com/v1"],
|
||||||
|
["fireworks", OpenAICompatible.fireworks, "https://api.fireworks.ai/inference/v1"],
|
||||||
|
["togetherai", OpenAICompatible.togetherai, "https://api.together.xyz/v1"],
|
||||||
|
] as const
|
||||||
|
|
||||||
describe("OpenAI-compatible Chat route", () => {
|
describe("OpenAI-compatible Chat route", () => {
|
||||||
it.effect("prepares generic Chat target", () =>
|
it.effect("prepares generic Chat target", () =>
|
||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
@@ -80,6 +90,41 @@ describe("OpenAI-compatible Chat route", () => {
|
|||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
it.effect("provides model helpers for compatible provider families", () =>
|
||||||
|
Effect.gen(function* () {
|
||||||
|
expect(
|
||||||
|
providerFamilies.map(([provider, family]) => {
|
||||||
|
const model = family.configure({ apiKey: "test-key" }).model(`${provider}-model`)
|
||||||
|
return {
|
||||||
|
id: String(model.id),
|
||||||
|
provider: String(model.provider),
|
||||||
|
route: model.route.id,
|
||||||
|
baseURL: model.route.endpoint.baseURL,
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
).toEqual(
|
||||||
|
providerFamilies.map(([provider, _, baseURL]) => ({
|
||||||
|
id: `${provider}-model`,
|
||||||
|
provider,
|
||||||
|
route: "openai-compatible-chat",
|
||||||
|
baseURL,
|
||||||
|
})),
|
||||||
|
)
|
||||||
|
|
||||||
|
const custom = OpenAICompatible.deepseek
|
||||||
|
.configure({
|
||||||
|
apiKey: "test-key",
|
||||||
|
baseURL: "https://custom.deepseek.test/v1",
|
||||||
|
})
|
||||||
|
.model("deepseek-chat")
|
||||||
|
expect(custom).toMatchObject({
|
||||||
|
provider: "deepseek",
|
||||||
|
route: { id: "openai-compatible-chat" },
|
||||||
|
})
|
||||||
|
expect(custom.route.endpoint.baseURL).toBe("https://custom.deepseek.test/v1")
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
it.effect("matches AI SDK compatible basic request body fixture", () =>
|
it.effect("matches AI SDK compatible basic request body fixture", () =>
|
||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
const prepared = yield* LLMClient.prepare(request)
|
const prepared = yield* LLMClient.prepare(request)
|
||||||
|
|||||||
@@ -1,156 +0,0 @@
|
|||||||
import { describe, expect } from "bun:test"
|
|
||||||
import { Effect, Schema } from "effect"
|
|
||||||
import { HttpClientRequest } from "effect/unstable/http"
|
|
||||||
import { LLM, LLMClient } from "../../src"
|
|
||||||
import { Anthropic, OpenAI, OpenAICodex, OpenAICompatible } from "../../src/providers"
|
|
||||||
import { it } from "../lib/effect"
|
|
||||||
import { dynamicResponse } from "../lib/http"
|
|
||||||
import { sseEvents } from "../lib/sse"
|
|
||||||
|
|
||||||
const JsonRecord = Schema.fromJsonString(Schema.Record(Schema.String, Schema.Unknown))
|
|
||||||
const decodeJsonRecord = Schema.decodeUnknownSync(JsonRecord)
|
|
||||||
|
|
||||||
const requestFor = (model: ReturnType<typeof OpenAI.model>) =>
|
|
||||||
LLM.request({
|
|
||||||
model,
|
|
||||||
prompt: "Say hello.",
|
|
||||||
cache: "none",
|
|
||||||
})
|
|
||||||
|
|
||||||
describe("provider package contract", () => {
|
|
||||||
it.effect("builds OpenAI Responses models from flat settings", () =>
|
|
||||||
Effect.gen(function* () {
|
|
||||||
yield* LLMClient.generate(
|
|
||||||
requestFor(
|
|
||||||
OpenAI.model("gpt-x", {
|
|
||||||
apiKey: "sk-test",
|
|
||||||
headers: { "x-package": "openai" },
|
|
||||||
body: { metadata: { source: "package" } },
|
|
||||||
providerOptions: { store: true },
|
|
||||||
limits: { context: 100, output: 20 },
|
|
||||||
}),
|
|
||||||
),
|
|
||||||
).pipe(
|
|
||||||
Effect.provide(
|
|
||||||
dynamicResponse((input) =>
|
|
||||||
Effect.gen(function* () {
|
|
||||||
const web = yield* HttpClientRequest.toWeb(input.request).pipe(Effect.orDie)
|
|
||||||
const body = decodeJsonRecord(input.text)
|
|
||||||
|
|
||||||
expect(web.headers.get("authorization")).toBe("Bearer sk-test")
|
|
||||||
expect(web.headers.get("x-package")).toBe("openai")
|
|
||||||
expect(body).toMatchObject({
|
|
||||||
model: "gpt-x",
|
|
||||||
metadata: { source: "package" },
|
|
||||||
store: true,
|
|
||||||
stream: true,
|
|
||||||
})
|
|
||||||
expect(body).not.toHaveProperty("apiKey")
|
|
||||||
return input.respond(sseEvents({ type: "response.completed", response: {} }), {
|
|
||||||
headers: { "content-type": "text/event-stream" },
|
|
||||||
})
|
|
||||||
}),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
|
|
||||||
it.effect("builds Codex models against the ChatGPT Codex endpoint with optional account header", () =>
|
|
||||||
Effect.gen(function* () {
|
|
||||||
yield* LLMClient.generate(
|
|
||||||
requestFor(OpenAICodex.model("gpt-5-codex", { apiKey: "oauth-token", accountID: "account-123" })),
|
|
||||||
).pipe(
|
|
||||||
Effect.provide(
|
|
||||||
dynamicResponse((input) =>
|
|
||||||
Effect.gen(function* () {
|
|
||||||
const web = yield* HttpClientRequest.toWeb(input.request).pipe(Effect.orDie)
|
|
||||||
|
|
||||||
expect(web.url.startsWith("https://chatgpt.com/backend-api/codex")).toBe(true)
|
|
||||||
expect(web.headers.get("authorization")).toBe("Bearer oauth-token")
|
|
||||||
expect(web.headers.get("chatgpt-account-id")).toBe("account-123")
|
|
||||||
return input.respond(sseEvents({ type: "response.completed", response: {} }), {
|
|
||||||
headers: { "content-type": "text/event-stream" },
|
|
||||||
})
|
|
||||||
}),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
|
|
||||||
yield* LLMClient.generate(requestFor(OpenAICodex.model("gpt-5-codex", { apiKey: "oauth-token" }))).pipe(
|
|
||||||
Effect.provide(
|
|
||||||
dynamicResponse((input) =>
|
|
||||||
Effect.gen(function* () {
|
|
||||||
const web = yield* HttpClientRequest.toWeb(input.request).pipe(Effect.orDie)
|
|
||||||
|
|
||||||
expect(web.url.startsWith("https://chatgpt.com/backend-api/codex")).toBe(true)
|
|
||||||
expect(web.headers.get("authorization")).toBe("Bearer oauth-token")
|
|
||||||
expect(web.headers.get("chatgpt-account-id")).toBeNull()
|
|
||||||
return input.respond(sseEvents({ type: "response.completed", response: {} }), {
|
|
||||||
headers: { "content-type": "text/event-stream" },
|
|
||||||
})
|
|
||||||
}),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
|
|
||||||
it.effect("builds Anthropic Messages models with x-api-key auth", () =>
|
|
||||||
Effect.gen(function* () {
|
|
||||||
yield* LLMClient.generate(requestFor(Anthropic.model("claude-x", { apiKey: "anthropic-key" }))).pipe(
|
|
||||||
Effect.provide(
|
|
||||||
dynamicResponse((input) =>
|
|
||||||
Effect.gen(function* () {
|
|
||||||
const web = yield* HttpClientRequest.toWeb(input.request).pipe(Effect.orDie)
|
|
||||||
const body = decodeJsonRecord(input.text)
|
|
||||||
|
|
||||||
expect(web.headers.get("x-api-key")).toBe("anthropic-key")
|
|
||||||
expect(body).toMatchObject({ model: "claude-x", stream: true })
|
|
||||||
return input.respond(
|
|
||||||
sseEvents({ type: "message_delta", delta: { stop_reason: "end_turn" }, usage: { output_tokens: 1 } }),
|
|
||||||
{ headers: { "content-type": "text/event-stream" } },
|
|
||||||
)
|
|
||||||
}),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
|
|
||||||
it.effect("builds OpenAI-compatible Chat models from flat settings", () =>
|
|
||||||
Effect.gen(function* () {
|
|
||||||
yield* LLMClient.generate(
|
|
||||||
requestFor(
|
|
||||||
OpenAICompatible.model("compatible-x", {
|
|
||||||
apiKey: "compatible-key",
|
|
||||||
baseURL: "https://api.compatible.test/v1",
|
|
||||||
body: { user: "provider-package" },
|
|
||||||
providerOptions: { serviceTier: "priority" },
|
|
||||||
}),
|
|
||||||
),
|
|
||||||
).pipe(
|
|
||||||
Effect.provide(
|
|
||||||
dynamicResponse((input) =>
|
|
||||||
Effect.gen(function* () {
|
|
||||||
const web = yield* HttpClientRequest.toWeb(input.request).pipe(Effect.orDie)
|
|
||||||
const body = decodeJsonRecord(input.text)
|
|
||||||
|
|
||||||
expect(web.url).toBe("https://api.compatible.test/v1/chat/completions")
|
|
||||||
expect(web.headers.get("authorization")).toBe("Bearer compatible-key")
|
|
||||||
expect(body).toMatchObject({
|
|
||||||
model: "compatible-x",
|
|
||||||
user: "provider-package",
|
|
||||||
stream: true,
|
|
||||||
})
|
|
||||||
expect(body).not.toHaveProperty("apiKey")
|
|
||||||
return input.respond(sseEvents({ choices: [{ delta: {}, finish_reason: "stop" }], usage: null }), {
|
|
||||||
headers: { "content-type": "text/event-stream" },
|
|
||||||
})
|
|
||||||
}),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
})
|
|
||||||
@@ -43,6 +43,7 @@ export class Service extends ConfigService.Service<Service>()("@opencode/Runtime
|
|||||||
experimentalBackgroundSubagents: enabledByExperimental("OPENCODE_EXPERIMENTAL_BACKGROUND_SUBAGENTS"),
|
experimentalBackgroundSubagents: enabledByExperimental("OPENCODE_EXPERIMENTAL_BACKGROUND_SUBAGENTS"),
|
||||||
experimentalLspTy: bool("OPENCODE_EXPERIMENTAL_LSP_TY"),
|
experimentalLspTy: bool("OPENCODE_EXPERIMENTAL_LSP_TY"),
|
||||||
experimentalLspTool: enabledByExperimental("OPENCODE_EXPERIMENTAL_LSP_TOOL"),
|
experimentalLspTool: enabledByExperimental("OPENCODE_EXPERIMENTAL_LSP_TOOL"),
|
||||||
|
experimentalCodeMode: enabledByExperimental("OPENCODE_EXPERIMENTAL_CODE_MODE"),
|
||||||
experimentalOxfmt: enabledByExperimental("OPENCODE_EXPERIMENTAL_OXFMT"),
|
experimentalOxfmt: enabledByExperimental("OPENCODE_EXPERIMENTAL_OXFMT"),
|
||||||
experimentalPlanMode: enabledByExperimental("OPENCODE_EXPERIMENTAL_PLAN_MODE"),
|
experimentalPlanMode: enabledByExperimental("OPENCODE_EXPERIMENTAL_PLAN_MODE"),
|
||||||
experimentalEventSystem: enabledByExperimental("OPENCODE_EXPERIMENTAL_EVENT_SYSTEM"),
|
experimentalEventSystem: enabledByExperimental("OPENCODE_EXPERIMENTAL_EVENT_SYSTEM"),
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import path from "node:path"
|
import path from "node:path"
|
||||||
import { pathToFileURL } from "node:url"
|
import { pathToFileURL } from "node:url"
|
||||||
import { LayerNode } from "@opencode-ai/core/effect/layer-node"
|
import { LayerNode } from "@opencode-ai/core/effect/layer-node"
|
||||||
|
import { type Tool } from "ai"
|
||||||
import { ConfigV1 } from "@opencode-ai/core/v1/config/config"
|
import { ConfigV1 } from "@opencode-ai/core/v1/config/config"
|
||||||
import { serviceUse } from "@opencode-ai/core/effect/service-use"
|
import { serviceUse } from "@opencode-ai/core/effect/service-use"
|
||||||
import { Client, type ClientOptions } from "@modelcontextprotocol/sdk/client/index.js"
|
import { Client, type ClientOptions } from "@modelcontextprotocol/sdk/client/index.js"
|
||||||
@@ -153,19 +154,17 @@ export interface ServerInstructions {
|
|||||||
tools: string[]
|
tools: string[]
|
||||||
}
|
}
|
||||||
|
|
||||||
/** An MCP tool in its native shape; consumers adapt it to their own tool format. */
|
|
||||||
export interface McpTool {
|
|
||||||
/** Shared cached definition; consumers must copy rather than mutate it. */
|
|
||||||
readonly def: MCPToolDef
|
|
||||||
readonly client: MCPClient
|
|
||||||
readonly timeout?: number
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface Interface {
|
export interface Interface {
|
||||||
readonly status: () => Effect.Effect<Record<string, Status>>
|
readonly status: () => Effect.Effect<Record<string, Status>>
|
||||||
readonly clients: () => Effect.Effect<Record<string, MCPClient>>
|
readonly clients: () => Effect.Effect<Record<string, MCPClient>>
|
||||||
readonly instructions: () => Effect.Effect<ServerInstructions[]>
|
readonly instructions: () => Effect.Effect<ServerInstructions[]>
|
||||||
readonly tools: () => Effect.Effect<Record<string, McpTool>>
|
readonly tools: () => Effect.Effect<Record<string, Tool>>
|
||||||
|
/**
|
||||||
|
* Raw MCP tool definitions keyed identically to {@link tools} (`toolName(client, name)`).
|
||||||
|
* Unlike {@link tools}, these retain the original `inputSchema`/`outputSchema`, which code
|
||||||
|
* mode uses to render tool signatures (including return types) to the model.
|
||||||
|
*/
|
||||||
|
readonly defs: () => Effect.Effect<Record<string, MCPToolDef>>
|
||||||
readonly prompts: () => Effect.Effect<Record<string, PromptInfo & { client: string }>>
|
readonly prompts: () => Effect.Effect<Record<string, PromptInfo & { client: string }>>
|
||||||
readonly resources: (clientName?: string) => Effect.Effect<Record<string, ResourceInfo & { client: string }>>
|
readonly resources: (clientName?: string) => Effect.Effect<Record<string, ResourceInfo & { client: string }>>
|
||||||
readonly resourceTemplates: (
|
readonly resourceTemplates: (
|
||||||
@@ -663,7 +662,7 @@ const layer = Layer.effect(
|
|||||||
}
|
}
|
||||||
|
|
||||||
const tools = Effect.fn("MCP.tools")(function* () {
|
const tools = Effect.fn("MCP.tools")(function* () {
|
||||||
const result: Record<string, McpTool> = {}
|
const result: Record<string, Tool> = {}
|
||||||
const s = yield* InstanceState.get(state)
|
const s = yield* InstanceState.get(state)
|
||||||
|
|
||||||
const cfg = yield* cfgSvc.get()
|
const cfg = yield* cfgSvc.get()
|
||||||
@@ -679,8 +678,21 @@ const layer = Layer.effect(
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
const timeout = requestTimeout(s, clientName, mcpConfig, defaultTimeout)
|
const timeout = requestTimeout(s, clientName, mcpConfig, defaultTimeout)
|
||||||
for (const def of listed) {
|
for (const mcpTool of listed) {
|
||||||
result[McpCatalog.toolName(clientName, def.name)] = { def, client, timeout }
|
const key = McpCatalog.toolName(clientName, mcpTool.name)
|
||||||
|
result[key] = McpCatalog.convertTool(mcpTool, client, timeout)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
})
|
||||||
|
|
||||||
|
const defs = Effect.fn("MCP.defs")(function* () {
|
||||||
|
const result: Record<string, MCPToolDef> = {}
|
||||||
|
const s = yield* InstanceState.get(state)
|
||||||
|
for (const [clientName, listed] of Object.entries(s.defs)) {
|
||||||
|
if (s.status[clientName]?.status !== "connected") continue
|
||||||
|
for (const mcpTool of listed) {
|
||||||
|
result[McpCatalog.toolName(clientName, mcpTool.name)] = mcpTool
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return result
|
return result
|
||||||
@@ -988,6 +1000,7 @@ const layer = Layer.effect(
|
|||||||
clients,
|
clients,
|
||||||
instructions,
|
instructions,
|
||||||
tools,
|
tools,
|
||||||
|
defs,
|
||||||
prompts,
|
prompts,
|
||||||
resources,
|
resources,
|
||||||
resourceTemplates,
|
resourceTemplates,
|
||||||
|
|||||||
@@ -0,0 +1,65 @@
|
|||||||
|
import type { ToolExecutionOptions } from "ai"
|
||||||
|
import { Effect } from "effect"
|
||||||
|
import type { Plugin } from "@/plugin"
|
||||||
|
import type { Tool } from "@/tool/tool"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The shared middle of every raw MCP tool invocation: plugin `tool.execute.before`
|
||||||
|
* hook → permission ask → dispatch through the ai-sdk tool's execute inside the
|
||||||
|
* `Tool.execute` tracing span → plugin `tool.execute.after` hook. Used by both the
|
||||||
|
* legacy per-tool registration in `SessionTools.resolve` and code-mode child calls,
|
||||||
|
* so MCP tools execute identically on either path.
|
||||||
|
*
|
||||||
|
* Returns the RAW result the ai-sdk execute resolved with — callers own their
|
||||||
|
* shaping edge (model-facing text/attachment shaping + truncation on the legacy
|
||||||
|
* path, `toSandboxResult` for code-mode child calls). The after hook fires here
|
||||||
|
* with that same raw result, which is exactly what the legacy loop always passed
|
||||||
|
* (the raw MCP result, not the shaped `{title, output, metadata}`), so the hook
|
||||||
|
* payload cannot drift between callers.
|
||||||
|
*
|
||||||
|
* `callID` is the hook/span identity — an opaque string nothing parses. Legacy
|
||||||
|
* passes the ai-sdk `toolCallId`; code-mode child calls pass a synthetic
|
||||||
|
* `${parentCallID}/${n}`. `options.toolCallId` is what the ai-sdk execute sees and
|
||||||
|
* stays each caller's existing value. Failure semantics belong to the caller: hook
|
||||||
|
* failures, permission denials, and tool failures all propagate — the legacy path
|
||||||
|
* lets them fail the tool call as before; code mode converts them into catchable
|
||||||
|
* in-program tool errors at its edge.
|
||||||
|
*/
|
||||||
|
export const invoke = Effect.fn("McpInvoke.invoke")(function* <R>(input: {
|
||||||
|
plugin: Plugin.Interface
|
||||||
|
key: string
|
||||||
|
execute: (args: any, options: ToolExecutionOptions) => R | PromiseLike<R>
|
||||||
|
args: any
|
||||||
|
callID: string
|
||||||
|
options: ToolExecutionOptions
|
||||||
|
sessionID: string
|
||||||
|
messageID: string
|
||||||
|
ask: Tool.Context["ask"]
|
||||||
|
}) {
|
||||||
|
yield* input.plugin.trigger(
|
||||||
|
"tool.execute.before",
|
||||||
|
{ tool: input.key, sessionID: input.sessionID, callID: input.callID },
|
||||||
|
{ args: input.args },
|
||||||
|
)
|
||||||
|
const result: R = yield* Effect.gen(function* () {
|
||||||
|
yield* input.ask({ permission: input.key, metadata: {}, patterns: ["*"], always: ["*"] })
|
||||||
|
return yield* Effect.promise(() => Promise.resolve(input.execute(input.args, input.options)))
|
||||||
|
}).pipe(
|
||||||
|
Effect.withSpan("Tool.execute", {
|
||||||
|
attributes: {
|
||||||
|
"tool.name": input.key,
|
||||||
|
"tool.call_id": input.callID,
|
||||||
|
"session.id": input.sessionID,
|
||||||
|
"message.id": input.messageID,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
yield* input.plugin.trigger(
|
||||||
|
"tool.execute.after",
|
||||||
|
{ tool: input.key, sessionID: input.sessionID, callID: input.callID, args: input.args },
|
||||||
|
result,
|
||||||
|
)
|
||||||
|
return result
|
||||||
|
})
|
||||||
|
|
||||||
|
export * as McpInvoke from "./invoke"
|
||||||
@@ -213,6 +213,13 @@ export function disabled(tools: string[], ruleset: PermissionV1.Ruleset): Set<st
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The shared tool-visibility predicate: drop every tool a hard deny hides
|
||||||
|
* ({@link disabled} semantics — a matching `deny` rule with pattern `"*"`).
|
||||||
|
* Ask-level rules leave a tool fully visible and callable (it prompts at call
|
||||||
|
* time). Used both when preparing the LLM tool list (request prep) and when
|
||||||
|
* building/dispatching the code-mode MCP catalog, so the two cannot drift.
|
||||||
|
*/
|
||||||
export function visibleTools<T>(tools: Record<string, T>, ruleset: PermissionV1.Ruleset): Record<string, T> {
|
export function visibleTools<T>(tools: Record<string, T>, ruleset: PermissionV1.Ruleset): Record<string, T> {
|
||||||
const hidden = disabled(Object.keys(tools), ruleset)
|
const hidden = disabled(Object.keys(tools), ruleset)
|
||||||
return Object.fromEntries(Object.entries(tools).filter(([name]) => !hidden.has(name)))
|
return Object.fromEntries(Object.entries(tools).filter(([name]) => !hidden.has(name)))
|
||||||
|
|||||||
@@ -292,6 +292,8 @@ export function createRoutes(
|
|||||||
HttpServer.layerServices,
|
HttpServer.layerServices,
|
||||||
]),
|
]),
|
||||||
Layer.provide(Layer.succeed(CorsConfig)(corsOptions)),
|
Layer.provide(Layer.succeed(CorsConfig)(corsOptions)),
|
||||||
|
Layer.provideMerge(Observability.layer),
|
||||||
|
|
||||||
Layer.provide(sessionLocationLayer),
|
Layer.provide(sessionLocationLayer),
|
||||||
Layer.provide(locationLayer),
|
Layer.provide(locationLayer),
|
||||||
Layer.provide(PtyEnvironment.layer),
|
Layer.provide(PtyEnvironment.layer),
|
||||||
@@ -304,11 +306,6 @@ export function createRoutes(
|
|||||||
Layer.provide(locationServiceMapV2),
|
Layer.provide(locationServiceMapV2),
|
||||||
|
|
||||||
Layer.provide(AppNodeBuilderV1.build(app)),
|
Layer.provide(AppNodeBuilderV1.build(app)),
|
||||||
// Must stay last: layers provided later in this pipe build beneath earlier ones,
|
|
||||||
// so Observability must come after every service graph. Otherwise eagerly forked
|
|
||||||
// fibers (e.g. the ModelsDev background refresh) capture Effect's default stdout
|
|
||||||
// logger and corrupt the TUI (#34730).
|
|
||||||
Layer.provideMerge(Observability.layer),
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -206,11 +206,11 @@ export const prepare = Effect.fn("LLMRequestPrep.prepare")(function* (input: Pre
|
|||||||
})
|
})
|
||||||
|
|
||||||
function resolveTools(input: Pick<PrepareInput, "tools" | "agent" | "permission" | "user">) {
|
function resolveTools(input: Pick<PrepareInput, "tools" | "agent" | "permission" | "user">) {
|
||||||
const disabled = Permission.disabled(
|
const visible = Permission.visibleTools(
|
||||||
Object.keys(input.tools),
|
input.tools,
|
||||||
Permission.merge(input.agent.permission, input.permission ?? []),
|
Permission.merge(input.agent.permission, input.permission ?? []),
|
||||||
)
|
)
|
||||||
return Record.filter(input.tools, (_, k) => input.user.tools?.[k] !== false && !disabled.has(k))
|
return Record.filter(visible, (_, k) => input.user.tools?.[k] !== false)
|
||||||
}
|
}
|
||||||
|
|
||||||
export function hasToolCalls(messages: ModelMessage[]): boolean {
|
export function hasToolCalls(messages: ModelMessage[]): boolean {
|
||||||
|
|||||||
@@ -192,8 +192,6 @@ const layer = Layer.effect(
|
|||||||
status: "error",
|
status: "error",
|
||||||
input: match.part.state.input,
|
input: match.part.state.input,
|
||||||
error: errorMessage(error),
|
error: errorMessage(error),
|
||||||
// Keep metadata streamed while running so failures retain progress detail (e.g. execute's child calls).
|
|
||||||
metadata: match.part.state.metadata,
|
|
||||||
time: { start: match.part.state.time.start, end: Date.now() },
|
time: { start: match.part.state.time.start, end: Date.now() },
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1237,6 +1237,8 @@ const layer = Layer.effect(
|
|||||||
Effect.provideService(ToolRegistry.Service, registry),
|
Effect.provideService(ToolRegistry.Service, registry),
|
||||||
Effect.provideService(MCP.Service, mcp),
|
Effect.provideService(MCP.Service, mcp),
|
||||||
Effect.provideService(Truncate.Service, truncate),
|
Effect.provideService(Truncate.Service, truncate),
|
||||||
|
Effect.provideService(Agent.Service, agents),
|
||||||
|
Effect.provideService(RuntimeFlags.Service, flags),
|
||||||
)
|
)
|
||||||
|
|
||||||
if (lastUser.format?.type === "json_schema") {
|
if (lastUser.format?.type === "json_schema") {
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import { SessionV1 } from "@opencode-ai/core/v1/session"
|
|||||||
import { Provider } from "@/provider/provider"
|
import { Provider } from "@/provider/provider"
|
||||||
import { ProviderTransform } from "@/provider/transform"
|
import { ProviderTransform } from "@/provider/transform"
|
||||||
import { MCP } from "@/mcp"
|
import { MCP } from "@/mcp"
|
||||||
import { McpCatalog } from "@/mcp/catalog"
|
import { McpInvoke } from "@/mcp/invoke"
|
||||||
import { Permission } from "@/permission"
|
import { Permission } from "@/permission"
|
||||||
import { Tool } from "@/tool/tool"
|
import { Tool } from "@/tool/tool"
|
||||||
import { ToolJsonSchema } from "@/tool/json-schema"
|
import { ToolJsonSchema } from "@/tool/json-schema"
|
||||||
@@ -22,6 +22,7 @@ import { EffectBridge } from "@/effect/bridge"
|
|||||||
import { ProviderV2 } from "@opencode-ai/core/provider"
|
import { ProviderV2 } from "@opencode-ai/core/provider"
|
||||||
import { ModelV2 } from "@opencode-ai/core/model"
|
import { ModelV2 } from "@opencode-ai/core/model"
|
||||||
import { isRecord } from "@/util/record"
|
import { isRecord } from "@/util/record"
|
||||||
|
import { RuntimeFlags } from "@/effect/runtime-flags"
|
||||||
|
|
||||||
const MCP_RESOURCE_TOOLS = {
|
const MCP_RESOURCE_TOOLS = {
|
||||||
list: "list_mcp_resources",
|
list: "list_mcp_resources",
|
||||||
@@ -53,6 +54,7 @@ export const resolve = Effect.fn("SessionTools.resolve")(function* (input: {
|
|||||||
const registry = yield* ToolRegistry.Service
|
const registry = yield* ToolRegistry.Service
|
||||||
const mcp = yield* MCP.Service
|
const mcp = yield* MCP.Service
|
||||||
const truncate = yield* Truncate.Service
|
const truncate = yield* Truncate.Service
|
||||||
|
const flags = yield* RuntimeFlags.Service
|
||||||
|
|
||||||
const context = (args: Record<string, unknown>, options: ToolExecutionOptions): Tool.Context => ({
|
const context = (args: Record<string, unknown>, options: ToolExecutionOptions): Tool.Context => ({
|
||||||
sessionID: input.session.id,
|
sessionID: input.session.id,
|
||||||
@@ -87,11 +89,20 @@ export const resolve = Effect.fn("SessionTools.resolve")(function* (input: {
|
|||||||
.pipe(Effect.orDie),
|
.pipe(Effect.orDie),
|
||||||
})
|
})
|
||||||
|
|
||||||
for (const item of yield* registry.tools({
|
const mcpTools = yield* mcp.tools()
|
||||||
|
// When code mode is enabled and MCP tools are present, the registry exposes them
|
||||||
|
// through the single code-mode `execute` tool (ToolRegistry.tools), so raw per-MCP
|
||||||
|
// registration is suppressed via the early return below. Code mode is experimental
|
||||||
|
// and off by default.
|
||||||
|
const codeMode = flags.experimentalCodeMode && Object.keys(mcpTools).length > 0
|
||||||
|
const registryTools = yield* registry.tools({
|
||||||
modelID: ModelV2.ID.make(input.model.api.id),
|
modelID: ModelV2.ID.make(input.model.api.id),
|
||||||
providerID: input.model.providerID,
|
providerID: input.model.providerID,
|
||||||
agent: input.agent,
|
agent: input.agent,
|
||||||
})) {
|
permission: input.session.permission,
|
||||||
|
})
|
||||||
|
|
||||||
|
for (const item of registryTools) {
|
||||||
const schema = ProviderTransform.schema(input.model, ToolJsonSchema.fromTool(item))
|
const schema = ProviderTransform.schema(input.model, ToolJsonSchema.fromTool(item))
|
||||||
tools[item.id] = tool({
|
tools[item.id] = tool({
|
||||||
description: item.description,
|
description: item.description,
|
||||||
@@ -382,8 +393,9 @@ export const resolve = Effect.fn("SessionTools.resolve")(function* (input: {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const [key, entry] of Object.entries(yield* mcp.tools())) {
|
if (codeMode) return tools
|
||||||
const item = McpCatalog.convertTool(entry.def, entry.client, entry.timeout)
|
|
||||||
|
for (const [key, item] of Object.entries(mcpTools)) {
|
||||||
const execute = item.execute
|
const execute = item.execute
|
||||||
if (!execute) continue
|
if (!execute) continue
|
||||||
|
|
||||||
@@ -394,29 +406,19 @@ export const resolve = Effect.fn("SessionTools.resolve")(function* (input: {
|
|||||||
run.promise(
|
run.promise(
|
||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
const ctx = context(args, opts)
|
const ctx = context(args, opts)
|
||||||
yield* plugin.trigger(
|
// Shared MCP middle (before hook → permission ask → Tool.execute span →
|
||||||
"tool.execute.before",
|
// dispatch → after hook); this caller keeps the model-facing shaping edge below.
|
||||||
{ tool: key, sessionID: ctx.sessionID, callID: opts.toolCallId },
|
const result: Awaited<ReturnType<NonNullable<typeof execute>>> = yield* McpInvoke.invoke({
|
||||||
{ args },
|
plugin,
|
||||||
)
|
key,
|
||||||
const result: Awaited<ReturnType<NonNullable<typeof execute>>> = yield* Effect.gen(function* () {
|
execute,
|
||||||
yield* ctx.ask({ permission: key, metadata: {}, patterns: ["*"], always: ["*"] })
|
args,
|
||||||
return yield* Effect.promise(() => execute(args, opts))
|
callID: opts.toolCallId,
|
||||||
}).pipe(
|
options: opts,
|
||||||
Effect.withSpan("Tool.execute", {
|
sessionID: ctx.sessionID,
|
||||||
attributes: {
|
messageID: input.processor.message.id,
|
||||||
"tool.name": key,
|
ask: ctx.ask,
|
||||||
"tool.call_id": opts.toolCallId,
|
})
|
||||||
"session.id": ctx.sessionID,
|
|
||||||
"message.id": input.processor.message.id,
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
yield* plugin.trigger(
|
|
||||||
"tool.execute.after",
|
|
||||||
{ tool: key, sessionID: ctx.sessionID, callID: opts.toolCallId, args },
|
|
||||||
result,
|
|
||||||
)
|
|
||||||
|
|
||||||
const textParts: string[] = []
|
const textParts: string[] = []
|
||||||
const attachments: Omit<SessionV1.FilePart, "id" | "sessionID" | "messageID">[] = []
|
const attachments: Omit<SessionV1.FilePart, "id" | "sessionID" | "messageID">[] = []
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import * as Tool from "./tool"
|
import * as Tool from "./tool"
|
||||||
import { CallToolResultSchema, type CallToolResult } from "@modelcontextprotocol/sdk/types.js"
|
import type { Tool as AITool } from "ai"
|
||||||
|
import type { Tool as MCPToolDef } from "@modelcontextprotocol/sdk/types.js"
|
||||||
import { Cause, Effect, Schema } from "effect"
|
import { Cause, Effect, Schema } from "effect"
|
||||||
import {
|
import {
|
||||||
CodeMode,
|
CodeMode,
|
||||||
@@ -11,6 +12,7 @@ import {
|
|||||||
} from "@opencode-ai/codemode"
|
} from "@opencode-ai/codemode"
|
||||||
import { MCP } from "@/mcp"
|
import { MCP } from "@/mcp"
|
||||||
import { McpCatalog } from "@/mcp/catalog"
|
import { McpCatalog } from "@/mcp/catalog"
|
||||||
|
import { McpInvoke } from "@/mcp/invoke"
|
||||||
import { Agent } from "@/agent/agent"
|
import { Agent } from "@/agent/agent"
|
||||||
import { Session } from "@/session/session"
|
import { Session } from "@/session/session"
|
||||||
import { Permission } from "@/permission"
|
import { Permission } from "@/permission"
|
||||||
@@ -18,6 +20,18 @@ import { Plugin } from "@/plugin"
|
|||||||
|
|
||||||
export const CODE_MODE_TOOL = "execute"
|
export const CODE_MODE_TOOL = "execute"
|
||||||
|
|
||||||
|
// OpenCode sets NO execution limits: no timeout, no tool-call cap, and no CodeMode output
|
||||||
|
// truncation. Cancelling the tool call aborts `ctx.abort`, which wins the race below and
|
||||||
|
// interrupts the execution fiber — structured concurrency takes the program and its
|
||||||
|
// in-flight child calls down with it; every child call is permission-gated anyway. Output
|
||||||
|
// bounding is OpenCode's native tool-output truncation (Tool.define's shared wrapper),
|
||||||
|
// which applies to `execute` like any other tool and dumps the full output to a file when
|
||||||
|
// it triggers.
|
||||||
|
|
||||||
|
// The static base description. The full usage guide and the grouped, permission-filtered
|
||||||
|
// tool catalog are appended per agent by the registry (`describeCodeMode`, the same
|
||||||
|
// composition point `describeTask` uses), so `plugin.trigger("tool.definition")` sees this
|
||||||
|
// base first, exactly like the task tool.
|
||||||
const DESCRIPTION = [
|
const DESCRIPTION = [
|
||||||
"Execute a JavaScript/TypeScript program that orchestrates the connected MCP tools inside a confined runtime.",
|
"Execute a JavaScript/TypeScript program that orchestrates the connected MCP tools inside a confined runtime.",
|
||||||
"The full usage guide and the catalog of available tools follow below.",
|
"The full usage guide and the catalog of available tools follow below.",
|
||||||
@@ -33,51 +47,119 @@ export const Parameters = Schema.Struct({
|
|||||||
}),
|
}),
|
||||||
})
|
})
|
||||||
|
|
||||||
type CallEntry = { tool: string; status: "running" | "completed" | "error"; input?: Record<string, unknown> }
|
/** One child tool call, surfaced live so the UI can render a per-call line that
|
||||||
|
* updates as the program runs. `tool` is the dotted path (e.g. `github.create_issue`). */
|
||||||
|
export type CallEntry = { tool: string; status: "running" | "completed" | "error"; input?: Record<string, unknown> }
|
||||||
|
|
||||||
type Metadata = {
|
type Metadata = {
|
||||||
toolCalls: CallEntry[]
|
toolCalls: CallEntry[]
|
||||||
error?: boolean
|
error?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
type Attachment = NonNullable<Tool.ExecuteResult["attachments"]>[number]
|
/**
|
||||||
|
* A tool-result attachment: identical to a session `FilePart` (minus the ids) and
|
||||||
|
* carrying the actual bytes (`url`, often a base64 `data:` URL), so it lowers 1:1 into
|
||||||
|
* `Tool.ExecuteResult.attachments`. Attachments never enter the sandbox — media stripped
|
||||||
|
* from child tool results is accumulated host-side and returned on the outer `execute`
|
||||||
|
* result, where the existing attachment plumbing turns it into visible images/files.
|
||||||
|
*/
|
||||||
|
export type Attachment = NonNullable<Tool.ExecuteResult["attachments"]>[number]
|
||||||
|
|
||||||
type CatalogEntry = {
|
/** One MCP tool in the grouped catalog: the flat `server_tool` key split into its
|
||||||
|
* namespace (`server`) and local name, with the raw JSON Schemas used for rendering. */
|
||||||
|
export type CatalogEntry = {
|
||||||
path: string
|
path: string
|
||||||
key: string
|
key: string
|
||||||
server: string
|
server: string
|
||||||
local: string
|
local: string
|
||||||
tool: MCP.McpTool
|
description: string
|
||||||
|
tool: AITool
|
||||||
|
inputSchema: JsonSchema
|
||||||
|
outputSchema?: JsonSchema
|
||||||
}
|
}
|
||||||
|
|
||||||
function groupByServer(mcpTools: Record<string, MCP.McpTool>, servers: readonly string[]): Map<string, CatalogEntry[]> {
|
/** Render-only cast: MCP definitions carry JSON Schema documents already. */
|
||||||
|
const toJsonSchema = (schema: unknown): JsonSchema => schema as JsonSchema
|
||||||
|
|
||||||
|
/** The input schema for entries without a cached MCP definition, recovered from the
|
||||||
|
* ai-sdk tool when possible so signatures stay informative. */
|
||||||
|
function fallbackInputSchema(tool: AITool): JsonSchema {
|
||||||
|
const schema = (tool.inputSchema as { jsonSchema?: unknown } | undefined)?.jsonSchema
|
||||||
|
if (schema && typeof schema === "object") return toJsonSchema(schema)
|
||||||
|
return { type: "object", properties: {} }
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Group the flat `server_tool` catalog into per-server namespaces. `servers` are
|
||||||
|
* the sanitized MCP client names; the longest matching prefix wins so a server
|
||||||
|
* named `a_b` beats `a` for the key `a_b_tool`. `mcpDefs` carries the raw MCP
|
||||||
|
* definitions (keyed identically) so each entry retains its original
|
||||||
|
* `inputSchema`/`outputSchema` for signature rendering.
|
||||||
|
*/
|
||||||
|
export function groupByServer(
|
||||||
|
mcpTools: Record<string, AITool>,
|
||||||
|
servers: readonly string[],
|
||||||
|
mcpDefs: Record<string, MCPToolDef> = {},
|
||||||
|
): Map<string, CatalogEntry[]> {
|
||||||
const byLongest = [...servers].sort((a, b) => b.length - a.length)
|
const byLongest = [...servers].sort((a, b) => b.length - a.length)
|
||||||
const groups = new Map<string, CatalogEntry[]>()
|
const groups = new Map<string, CatalogEntry[]>()
|
||||||
for (const key of Object.keys(mcpTools).sort((a, b) => a.localeCompare(b))) {
|
for (const key of Object.keys(mcpTools).sort((a, b) => a.localeCompare(b))) {
|
||||||
const server =
|
const server = byLongest.find((name) => key.startsWith(name + "_")) ?? (key.includes("_") ? key.slice(0, key.indexOf("_")) : key)
|
||||||
byLongest.find((name) => key.startsWith(name + "_")) ?? (key.includes("_") ? key.slice(0, key.indexOf("_")) : key)
|
|
||||||
const local = server && key.startsWith(server + "_") ? key.slice(server.length + 1) : key
|
const local = server && key.startsWith(server + "_") ? key.slice(server.length + 1) : key
|
||||||
|
const def = mcpDefs[key]
|
||||||
const entry: CatalogEntry = {
|
const entry: CatalogEntry = {
|
||||||
path: `${server}.${local}`,
|
path: `${server}.${local}`,
|
||||||
key,
|
key,
|
||||||
server,
|
server,
|
||||||
local,
|
local,
|
||||||
|
description: mcpTools[key]!.description ?? def?.description ?? "",
|
||||||
tool: mcpTools[key]!,
|
tool: mcpTools[key]!,
|
||||||
|
inputSchema: def?.inputSchema ? toJsonSchema(def.inputSchema) : fallbackInputSchema(mcpTools[key]!),
|
||||||
|
...(def?.outputSchema ? { outputSchema: toJsonSchema(def.outputSchema) } : {}),
|
||||||
}
|
}
|
||||||
groups.set(server, [...(groups.get(server) ?? []), entry])
|
groups.set(server, [...(groups.get(server) ?? []), entry])
|
||||||
}
|
}
|
||||||
return groups
|
return groups
|
||||||
}
|
}
|
||||||
|
|
||||||
export function describeCatalog(mcpTools: Record<string, MCP.McpTool>, servers: readonly string[]): string {
|
/** The executable catalog for a (already permission-filtered) MCP tool set: grouped
|
||||||
|
* entries, minus any without an ai-sdk execute function. */
|
||||||
|
export function buildCatalog(
|
||||||
|
mcpTools: Record<string, AITool>,
|
||||||
|
mcpDefs: Record<string, MCPToolDef>,
|
||||||
|
servers: readonly string[],
|
||||||
|
): CatalogEntry[] {
|
||||||
|
return [...groupByServer(mcpTools, servers, mcpDefs).values()].flat().filter((entry) => entry.tool.execute !== undefined)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The model-facing usage guide plus grouped catalog for the given MCP tool set: the
|
||||||
|
* CodeMode instructions for this tool tree (syntax guide + tool signatures, or the
|
||||||
|
* namespace overview + search for large catalogs). Callers pass an already
|
||||||
|
* permission-filtered tool set — hard-denied tools never enter the catalog. The preview
|
||||||
|
* tree's runs are placeholders — rendering never invokes them.
|
||||||
|
*/
|
||||||
|
export function catalogInstructions(
|
||||||
|
mcpTools: Record<string, AITool>,
|
||||||
|
mcpDefs: Record<string, MCPToolDef>,
|
||||||
|
servers: readonly string[],
|
||||||
|
): string {
|
||||||
|
const catalog = buildCatalog(mcpTools, mcpDefs, servers)
|
||||||
return CodeMode.make({
|
return CodeMode.make({
|
||||||
tools: toolTree(
|
tools: toolTree(catalog, () => () => Effect.fail(toolError("Tool preview is not executable."))),
|
||||||
[...groupByServer(mcpTools, servers).values()].flat(),
|
|
||||||
() => () => Effect.fail(toolError("Tool preview is not executable.")),
|
|
||||||
),
|
|
||||||
}).instructions()
|
}).instructions()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function displayInput(input: unknown): Record<string, unknown> | undefined {
|
||||||
|
if (input === null || input === undefined) return
|
||||||
|
if (typeof input === "object" && !Array.isArray(input)) {
|
||||||
|
const value = input as Record<string, unknown>
|
||||||
|
if (Object.keys(value).length > 0) return value
|
||||||
|
return
|
||||||
|
}
|
||||||
|
return { input }
|
||||||
|
}
|
||||||
|
|
||||||
const lastSegment = (uri: string) => {
|
const lastSegment = (uri: string) => {
|
||||||
const trimmed = uri.split(/[?#]/, 1)[0]!.replace(/\/+$/, "")
|
const trimmed = uri.split(/[?#]/, 1)[0]!.replace(/\/+$/, "")
|
||||||
const segment = trimmed.slice(trimmed.lastIndexOf("/") + 1)
|
const segment = trimmed.slice(trimmed.lastIndexOf("/") + 1)
|
||||||
@@ -86,7 +168,24 @@ const lastSegment = (uri: string) => {
|
|||||||
|
|
||||||
const dataUrl = (mime: string, base64: string) => `data:${mime};base64,${base64}`
|
const dataUrl = (mime: string, base64: string) => `data:${mime};base64,${base64}`
|
||||||
|
|
||||||
function projectMcpResult(result: CallToolResult, collect: (attachment: Attachment) => void): unknown {
|
/** The stand-in payload for a media-only tool result, so the program knows the call
|
||||||
|
* succeeded even though the media itself never enters the sandbox. */
|
||||||
|
const mediaMarker = (files: number, images: number) => {
|
||||||
|
const noun = files === images ? "image" : "file"
|
||||||
|
return `[${files} ${noun}${files === 1 ? "" : "s"} attached to the result]`
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reduce a raw MCP tool result to the value the sandbox sees. Structured content is
|
||||||
|
* preferred; otherwise text blocks are joined. Media blocks (image/audio/resource
|
||||||
|
* blob/resource_link) NEVER enter the sandbox: they are stripped into `collect`, the
|
||||||
|
* per-execution attachment accumulator, and a tool that returned ONLY media yields a
|
||||||
|
* small text marker instead. Lenient — never throws on unexpected shapes.
|
||||||
|
*/
|
||||||
|
export function toSandboxResult(raw: unknown, collect: (attachment: Attachment) => void): unknown {
|
||||||
|
if (raw === null || typeof raw !== "object") return raw
|
||||||
|
const record = raw as { structuredContent?: unknown; content?: unknown }
|
||||||
|
const content = Array.isArray(record.content) ? record.content : []
|
||||||
const text: string[] = []
|
const text: string[] = []
|
||||||
let files = 0
|
let files = 0
|
||||||
let images = 0
|
let images = 0
|
||||||
@@ -95,109 +194,105 @@ function projectMcpResult(result: CallToolResult, collect: (attachment: Attachme
|
|||||||
if (attachment.mime.startsWith("image/")) images += 1
|
if (attachment.mime.startsWith("image/")) images += 1
|
||||||
collect(attachment)
|
collect(attachment)
|
||||||
}
|
}
|
||||||
for (const block of result.content) {
|
for (const item of content) {
|
||||||
|
if (!item || typeof item !== "object") continue
|
||||||
|
const block = item as Record<string, unknown>
|
||||||
switch (block.type) {
|
switch (block.type) {
|
||||||
case "text":
|
case "text":
|
||||||
text.push(block.text)
|
if (typeof block.text === "string") text.push(block.text)
|
||||||
break
|
break
|
||||||
case "image":
|
case "image":
|
||||||
case "audio":
|
case "audio":
|
||||||
|
if (typeof block.data === "string" && typeof block.mimeType === "string") {
|
||||||
push({ type: "file", mime: block.mimeType, url: dataUrl(block.mimeType, block.data) })
|
push({ type: "file", mime: block.mimeType, url: dataUrl(block.mimeType, block.data) })
|
||||||
|
}
|
||||||
break
|
break
|
||||||
case "resource": {
|
case "resource": {
|
||||||
if ("text" in block.resource) {
|
const res = block.resource as Record<string, unknown> | undefined
|
||||||
text.push(block.resource.text)
|
if (res && typeof res === "object") {
|
||||||
break
|
const mime = typeof res.mimeType === "string" ? res.mimeType : "application/octet-stream"
|
||||||
|
const uri = typeof res.uri === "string" ? res.uri : undefined
|
||||||
|
if (typeof res.blob === "string") {
|
||||||
|
push({ type: "file", mime, url: dataUrl(mime, res.blob), filename: uri ? lastSegment(uri) : undefined })
|
||||||
|
} else if (typeof res.text === "string") {
|
||||||
|
text.push(res.text)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
const mime = block.resource.mimeType ?? "application/octet-stream"
|
|
||||||
push({ type: "file", mime, url: dataUrl(mime, block.resource.blob), filename: lastSegment(block.resource.uri) })
|
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
case "resource_link":
|
case "resource_link":
|
||||||
// A link is a reference, not fetchable media; hand it to the program instead of the attachment channel.
|
if (typeof block.uri === "string") {
|
||||||
text.push(`${block.name}: ${block.uri}`)
|
push({
|
||||||
|
type: "file",
|
||||||
|
mime: typeof block.mimeType === "string" ? block.mimeType : "application/octet-stream",
|
||||||
|
url: block.uri,
|
||||||
|
filename: typeof block.name === "string" ? block.name : lastSegment(block.uri),
|
||||||
|
})
|
||||||
|
}
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (result.structuredContent !== undefined && result.structuredContent !== null) return result.structuredContent
|
if (record.structuredContent !== undefined && record.structuredContent !== null) return record.structuredContent
|
||||||
if (text.length > 0) return text.join("\n")
|
if (text.length > 0) return text.join("\n")
|
||||||
if (files > 0) {
|
if (files > 0) return mediaMarker(files, images)
|
||||||
const noun = files === images ? "image" : "file"
|
if (Array.isArray(record.content)) return null // MCP-shaped result with nothing extractable
|
||||||
return `[${files} ${noun}${files === 1 ? "" : "s"} attached to the result]`
|
return raw
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Append captured `console.*` output to the model-facing text as a trailing `Logs:` section,
|
||||||
|
* so a program's diagnostics ride back alongside its result — on success AND on error.
|
||||||
|
* Returns the text unchanged when nothing was logged. This is the sandbox's only
|
||||||
|
* stdout-like channel — it goes to the model, not the user.
|
||||||
|
*/
|
||||||
|
export function withLogs(output: string, logs: ReadonlyArray<string> = []): string {
|
||||||
|
if (logs.length === 0) return output
|
||||||
|
const section = "Logs:\n" + logs.join("\n")
|
||||||
|
return output.length > 0 ? `${output}\n\n${section}` : section
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Coerce the program's return value to model-facing text without ever failing on shape. */
|
||||||
|
export function formatValue(value: unknown): string {
|
||||||
|
if (typeof value === "string") return value
|
||||||
|
if (value === undefined) return "undefined"
|
||||||
|
try {
|
||||||
|
return JSON.stringify(value, null, 2) ?? String(value)
|
||||||
|
} catch {
|
||||||
|
return String(value)
|
||||||
}
|
}
|
||||||
return null
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type Run = (input: unknown) => Effect.Effect<unknown, unknown>
|
type Run = (input: unknown) => Effect.Effect<unknown, unknown>
|
||||||
|
|
||||||
|
/** Build the `tools.<server>.<tool>` tree CodeMode executes against, one
|
||||||
|
* `Tool.make` definition per MCP tool with its render-only JSON Schemas. */
|
||||||
function toolTree(catalog: readonly CatalogEntry[], run: (entry: CatalogEntry) => Run) {
|
function toolTree(catalog: readonly CatalogEntry[], run: (entry: CatalogEntry) => Run) {
|
||||||
const tree: Record<string, Record<string, ToolDefinition>> = {}
|
const tree: Record<string, Record<string, ToolDefinition>> = {}
|
||||||
for (const entry of catalog) {
|
for (const entry of catalog) {
|
||||||
const namespace = (tree[entry.server] ??= {})
|
const namespace = (tree[entry.server] ??= {})
|
||||||
namespace[entry.local] = SandboxTool.make({
|
namespace[entry.local] = SandboxTool.make({
|
||||||
description: entry.tool.def.description ?? "",
|
description: entry.description,
|
||||||
input: entry.tool.def.inputSchema as JsonSchema,
|
input: entry.inputSchema,
|
||||||
output: entry.tool.def.outputSchema as JsonSchema | undefined,
|
output: entry.outputSchema,
|
||||||
run: run(entry),
|
run: run(entry),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
return tree
|
return tree
|
||||||
}
|
}
|
||||||
|
|
||||||
const invokeChildTool = Effect.fn("CodeMode.invokeChildTool")(function* (input: {
|
/** Failures inside a child call — plugin hook failures, permission denials, and tool
|
||||||
plugin: Plugin.Interface
|
* failures alike — become safe, catchable in-program errors via toolError, so a
|
||||||
entry: CatalogEntry
|
* program can try/catch one call without the whole execution dying. Interruption
|
||||||
args: Record<string, unknown>
|
* (user cancel) keeps propagating as interruption. */
|
||||||
callID: string
|
const toCatchable = <A, E, R>(effect: Effect.Effect<A, E, R>) =>
|
||||||
ctx: Tool.Context
|
effect.pipe(
|
||||||
}) {
|
Effect.catchCause((cause) => {
|
||||||
yield* input.plugin.trigger(
|
if (Cause.hasInterruptsOnly(cause)) return Effect.interrupt
|
||||||
"tool.execute.before",
|
const error = Cause.squash(cause)
|
||||||
{ tool: input.entry.key, sessionID: input.ctx.sessionID, callID: input.callID },
|
return Effect.fail(toolError(error instanceof Error ? error.message : String(error), error))
|
||||||
{ args: input.args },
|
|
||||||
)
|
|
||||||
const result: CallToolResult = yield* Effect.gen(function* () {
|
|
||||||
yield* input.ctx.ask({ permission: input.entry.key, metadata: {}, patterns: ["*"], always: ["*"] })
|
|
||||||
// Deliberately mirrors McpCatalog.convertTool's transport call so the MCP service stays free of tool-loop concerns.
|
|
||||||
return yield* Effect.promise(async () => {
|
|
||||||
const raw = await input.entry.tool.client.callTool(
|
|
||||||
{ name: input.entry.tool.def.name, arguments: input.args },
|
|
||||||
CallToolResultSchema,
|
|
||||||
{
|
|
||||||
resetTimeoutOnProgress: true,
|
|
||||||
signal: input.ctx.abort,
|
|
||||||
timeout: input.entry.tool.timeout,
|
|
||||||
// The MCP SDK only sends a progress token when this hook is present, enabling timeout resets.
|
|
||||||
onprogress: () => {},
|
|
||||||
},
|
|
||||||
)
|
|
||||||
if (raw.isError)
|
|
||||||
throw new Error(
|
|
||||||
raw.content
|
|
||||||
.flatMap((item) => (item.type === "text" ? [item.text] : []))
|
|
||||||
.filter((text) => text.trim())
|
|
||||||
.join("\n\n") || "MCP tool returned an error",
|
|
||||||
)
|
|
||||||
return raw
|
|
||||||
})
|
|
||||||
}).pipe(
|
|
||||||
Effect.withSpan("Tool.execute", {
|
|
||||||
attributes: {
|
|
||||||
"tool.name": input.entry.key,
|
|
||||||
"tool.call_id": input.callID,
|
|
||||||
"session.id": input.ctx.sessionID,
|
|
||||||
"message.id": input.ctx.messageID,
|
|
||||||
},
|
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
yield* input.plugin.trigger(
|
|
||||||
"tool.execute.after",
|
|
||||||
{ tool: input.entry.key, sessionID: input.ctx.sessionID, callID: input.callID, args: input.args },
|
|
||||||
result,
|
|
||||||
)
|
|
||||||
return result
|
|
||||||
})
|
|
||||||
|
|
||||||
export const CodeModeTool = Tool.define(
|
export const CodeModeTool = Tool.define(
|
||||||
CODE_MODE_TOOL,
|
CODE_MODE_TOOL,
|
||||||
@@ -211,6 +306,8 @@ export const CodeModeTool = Tool.define(
|
|||||||
description: DESCRIPTION,
|
description: DESCRIPTION,
|
||||||
parameters: Parameters,
|
parameters: Parameters,
|
||||||
execute: Effect.fn("CodeMode.execute")(function* (params, ctx) {
|
execute: Effect.fn("CodeMode.execute")(function* (params, ctx) {
|
||||||
|
// Already cancelled: don't start the program at all. (The mid-flight case is the
|
||||||
|
// race below; racing alone would still let the program run its first steps.)
|
||||||
if (ctx.abort.aborted) {
|
if (ctx.abort.aborted) {
|
||||||
return {
|
return {
|
||||||
title: CODE_MODE_TOOL,
|
title: CODE_MODE_TOOL,
|
||||||
@@ -218,35 +315,53 @@ export const CodeModeTool = Tool.define(
|
|||||||
output: "Execution cancelled.",
|
output: "Execution cancelled.",
|
||||||
} satisfies Tool.ExecuteResult<Metadata>
|
} satisfies Tool.ExecuteResult<Metadata>
|
||||||
}
|
}
|
||||||
|
// A fresh MCP snapshot per execution, so the runtime tracks live tool-list
|
||||||
|
// changes, filtered with the same merged agent+session ruleset that gates
|
||||||
|
// `ctx.ask` (see SessionTools.context). A hard-denied tool never enters the
|
||||||
|
// tree, so it is not dispatchable even if the model guesses its name — the
|
||||||
|
// program gets the normal unknown-tool diagnostic, not a permission error.
|
||||||
const agent = yield* agents.get(ctx.agent)
|
const agent = yield* agents.get(ctx.agent)
|
||||||
const session = yield* sessions.get(ctx.sessionID).pipe(Effect.orDie)
|
const session = yield* sessions.get(ctx.sessionID).pipe(Effect.orDie)
|
||||||
const ruleset = Permission.merge(agent.permission, session.permission ?? [])
|
const ruleset = Permission.merge(agent.permission, session.permission ?? [])
|
||||||
const mcpTools = Permission.visibleTools(yield* mcp.tools(), ruleset)
|
const mcpTools = Permission.visibleTools(yield* mcp.tools(), ruleset)
|
||||||
const servers = Object.keys(yield* mcp.clients()).map(McpCatalog.sanitize)
|
const servers = Object.keys(yield* mcp.clients()).map(McpCatalog.sanitize)
|
||||||
const catalog = [...groupByServer(mcpTools, servers).values()].flat()
|
const catalog = buildCatalog(mcpTools, yield* mcp.defs(), servers)
|
||||||
|
|
||||||
const calls: CallEntry[] = []
|
const calls: CallEntry[] = []
|
||||||
|
// Media stripped from child tool results accumulates here for the life of the
|
||||||
|
// call; the bytes never enter the sandbox (see toSandboxResult).
|
||||||
const attachments: Attachment[] = []
|
const attachments: Attachment[] = []
|
||||||
const publish = () =>
|
const collect = (attachment: Attachment) => void attachments.push(attachment)
|
||||||
ctx.metadata({ title: CODE_MODE_TOOL, metadata: { toolCalls: calls.map((c) => ({ ...c })) } })
|
// Stream the current call list to the UI. Sent on every status change so the
|
||||||
|
// tool part shows each child call appearing and resolving while the program runs.
|
||||||
|
const publish = () => ctx.metadata({ title: CODE_MODE_TOOL, metadata: { toolCalls: calls.map((c) => ({ ...c })) } })
|
||||||
|
|
||||||
|
// One CodeMode tool per MCP tool, running the same shared middle as legacy
|
||||||
|
// per-tool registration (McpInvoke.invoke: plugin before hook → permission
|
||||||
|
// ask → Tool.execute span → dispatch through the ai-sdk wrapper, which owns
|
||||||
|
// callTool timeouts/progress and turns an MCP isError into a thrown Error →
|
||||||
|
// plugin after hook), so plugins observe child calls too. Each child gets a
|
||||||
|
// synthetic hook/span callID `${parentCallID}/${n}` (per-execution counter,
|
||||||
|
// opaque — nothing parses it); the ai-sdk toolCallId is unchanged. Failures —
|
||||||
|
// hook, denial, or tool — fail only that child call as a safe, catchable
|
||||||
|
// in-program error (toCatchable); the raw result is then shaped for the sandbox.
|
||||||
let childCalls = 0
|
let childCalls = 0
|
||||||
const callTool = (entry: CatalogEntry) => (input: unknown) =>
|
const callTool = (entry: CatalogEntry) => (input: unknown) =>
|
||||||
|
toCatchable(
|
||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
childCalls += 1
|
childCalls += 1
|
||||||
const result = yield* invokeChildTool({
|
const raw = yield* McpInvoke.invoke({
|
||||||
plugin,
|
plugin,
|
||||||
entry,
|
key: entry.key,
|
||||||
args: (input ?? {}) as Record<string, unknown>,
|
execute: entry.tool.execute!,
|
||||||
|
args: input ?? {},
|
||||||
callID: `${ctx.callID ?? entry.key}/${childCalls}`,
|
callID: `${ctx.callID ?? entry.key}/${childCalls}`,
|
||||||
ctx,
|
options: { toolCallId: ctx.callID ?? entry.key, abortSignal: ctx.abort, messages: [] },
|
||||||
|
sessionID: ctx.sessionID,
|
||||||
|
messageID: ctx.messageID,
|
||||||
|
ask: ctx.ask,
|
||||||
})
|
})
|
||||||
return projectMcpResult(result, (attachment: Attachment) => void attachments.push(attachment))
|
return toSandboxResult(raw, collect)
|
||||||
}).pipe(
|
|
||||||
Effect.catchCause((cause) => {
|
|
||||||
if (Cause.hasInterruptsOnly(cause)) return Effect.interrupt
|
|
||||||
const error = Cause.squash(cause)
|
|
||||||
return Effect.fail(toolError(error instanceof Error ? error.message : String(error), error))
|
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -254,14 +369,7 @@ export const CodeModeTool = Tool.define(
|
|||||||
tools: toolTree(catalog, callTool),
|
tools: toolTree(catalog, callTool),
|
||||||
onToolCallStart: ({ index, name, input }) =>
|
onToolCallStart: ({ index, name, input }) =>
|
||||||
Effect.suspend(() => {
|
Effect.suspend(() => {
|
||||||
const shown = (() => {
|
const shown = displayInput(input)
|
||||||
if (input === null || input === undefined) return
|
|
||||||
if (typeof input === "object" && !Array.isArray(input)) {
|
|
||||||
const value = input as Record<string, unknown>
|
|
||||||
return Object.keys(value).length > 0 ? value : undefined
|
|
||||||
}
|
|
||||||
return { input }
|
|
||||||
})()
|
|
||||||
calls[index] = { tool: name, status: "running", ...(shown ? { input: shown } : {}) }
|
calls[index] = { tool: name, status: "running", ...(shown ? { input: shown } : {}) }
|
||||||
return publish()
|
return publish()
|
||||||
}),
|
}),
|
||||||
@@ -273,51 +381,47 @@ export const CodeModeTool = Tool.define(
|
|||||||
}),
|
}),
|
||||||
})
|
})
|
||||||
|
|
||||||
const abort = Effect.callback<void>((resume) => {
|
// The shared tool runner does not wire ctx.abort to fiber interruption (it runs
|
||||||
if (ctx.abort.aborted) return resume(Effect.void)
|
// tools via Effect.runPromise with no abort handling), so without this race the
|
||||||
const handler = () => resume(Effect.void)
|
// program would keep running after the user cancels. The abort signal winning the
|
||||||
ctx.abort.addEventListener("abort", handler, { once: true })
|
// race interrupts the execution fiber; the cancelled result keeps the runner's
|
||||||
return Effect.sync(() => ctx.abort.removeEventListener("abort", handler))
|
// post-abort bookkeeping (completeToolCall) on its normal path.
|
||||||
})
|
const cancelled = Effect.callback<ExecuteResult>((resume) => {
|
||||||
const cancelled = (): ExecuteResult => ({
|
const onAbort = () =>
|
||||||
|
resume(
|
||||||
|
Effect.succeed<ExecuteResult>({
|
||||||
ok: false,
|
ok: false,
|
||||||
error: { kind: "ExecutionFailure", message: "Execution cancelled." },
|
error: { kind: "ExecutionFailure", message: "Execution cancelled." },
|
||||||
toolCalls: calls.map((call) => ({ name: call.tool })),
|
toolCalls: calls.map((call) => ({ name: call.tool })),
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
if (ctx.abort.aborted) return onAbort()
|
||||||
|
ctx.abort.addEventListener("abort", onAbort, { once: true })
|
||||||
|
return Effect.sync(() => ctx.abort.removeEventListener("abort", onAbort))
|
||||||
})
|
})
|
||||||
|
|
||||||
const result = yield* Effect.raceFirst(runtime.execute(params.code), abort.pipe(Effect.map(cancelled)))
|
const result = yield* Effect.raceFirst(runtime.execute(params.code), cancelled)
|
||||||
const logs = result.logs ?? []
|
const logs = result.logs ?? []
|
||||||
const withLogs = (text: string) => {
|
const attached = attachments.length > 0 ? { attachments } : {}
|
||||||
if (logs.length === 0) return text
|
|
||||||
return text.length > 0 ? `${text}\n\nLogs:\n${logs.join("\n")}` : `Logs:\n${logs.join("\n")}`
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!result.ok) {
|
|
||||||
if (ctx.abort.aborted) {
|
|
||||||
return {
|
|
||||||
title: CODE_MODE_TOOL,
|
|
||||||
metadata: { toolCalls: calls, error: true },
|
|
||||||
output: "Execution cancelled.",
|
|
||||||
} satisfies Tool.ExecuteResult<Metadata>
|
|
||||||
}
|
|
||||||
const hints = (result.error.suggestions ?? []).filter((hint) => !result.error.message.includes(hint))
|
|
||||||
return yield* Effect.fail(new Error(withLogs([result.error.message, ...hints].join("\n"))))
|
|
||||||
}
|
|
||||||
|
|
||||||
// The interpreter validates returned values as plain JSON, so stringify cannot throw;
|
|
||||||
// it yields undefined only for a program that returns undefined.
|
|
||||||
const output =
|
|
||||||
typeof result.value === "string"
|
|
||||||
? result.value
|
|
||||||
: (JSON.stringify(result.value, null, 2) ?? String(result.value))
|
|
||||||
|
|
||||||
|
if (result.ok) {
|
||||||
return {
|
return {
|
||||||
title: CODE_MODE_TOOL,
|
title: CODE_MODE_TOOL,
|
||||||
metadata: { toolCalls: calls },
|
metadata: { toolCalls: calls },
|
||||||
output: withLogs(output),
|
output: withLogs(formatValue(result.value), logs),
|
||||||
...(attachments.length > 0 ? { attachments } : {}),
|
...attached,
|
||||||
} satisfies Tool.ExecuteResult<Metadata>
|
} satisfies Tool.ExecuteResult<Metadata>
|
||||||
}, Effect.orDie),
|
}
|
||||||
|
// Diagnostics may carry suggestions (e.g. pointing an unknown tool at
|
||||||
|
// discovery); append the ones the message doesn't already contain.
|
||||||
|
const hints = (result.error.suggestions ?? []).filter((hint) => !result.error.message.includes(hint))
|
||||||
|
return {
|
||||||
|
title: CODE_MODE_TOOL,
|
||||||
|
metadata: { toolCalls: calls, error: true },
|
||||||
|
output: withLogs([result.error.message, ...hints].join("\n"), logs),
|
||||||
|
...attached,
|
||||||
|
} satisfies Tool.ExecuteResult<Metadata>
|
||||||
|
}),
|
||||||
}
|
}
|
||||||
return init
|
return init
|
||||||
}),
|
}),
|
||||||
|
|||||||
@@ -26,6 +26,9 @@ import { Plugin } from "../plugin"
|
|||||||
import { Provider } from "@/provider/provider"
|
import { Provider } from "@/provider/provider"
|
||||||
|
|
||||||
import { WebSearchTool } from "./websearch"
|
import { WebSearchTool } from "./websearch"
|
||||||
|
import { CodeModeTool, catalogInstructions } from "./code-mode"
|
||||||
|
import { MCP } from "@/mcp"
|
||||||
|
import { McpCatalog } from "@/mcp/catalog"
|
||||||
import { LspTool } from "./lsp"
|
import { LspTool } from "./lsp"
|
||||||
import * as Truncate from "./truncate"
|
import * as Truncate from "./truncate"
|
||||||
import { ApplyPatchTool } from "./apply_patch"
|
import { ApplyPatchTool } from "./apply_patch"
|
||||||
@@ -51,6 +54,7 @@ import { BackgroundJob } from "@/background/job"
|
|||||||
import { RuntimeFlags } from "@/effect/runtime-flags"
|
import { RuntimeFlags } from "@/effect/runtime-flags"
|
||||||
import { ProviderV2 } from "@opencode-ai/core/provider"
|
import { ProviderV2 } from "@opencode-ai/core/provider"
|
||||||
import { ModelV2 } from "@opencode-ai/core/model"
|
import { ModelV2 } from "@opencode-ai/core/model"
|
||||||
|
import type { PermissionV1 } from "@opencode-ai/core/v1/permission"
|
||||||
|
|
||||||
export function webSearchEnabled(providerID: ProviderV2.ID, flags = { exa: false, parallel: false }) {
|
export function webSearchEnabled(providerID: ProviderV2.ID, flags = { exa: false, parallel: false }) {
|
||||||
return providerID === ProviderV2.ID.opencode || flags.exa || flags.parallel
|
return providerID === ProviderV2.ID.opencode || flags.exa || flags.parallel
|
||||||
@@ -74,6 +78,7 @@ export interface Interface {
|
|||||||
providerID: ProviderV2.ID
|
providerID: ProviderV2.ID
|
||||||
modelID: ModelV2.ID
|
modelID: ModelV2.ID
|
||||||
agent: Agent.Info
|
agent: Agent.Info
|
||||||
|
permission?: PermissionV1.Ruleset
|
||||||
}) => Effect.Effect<Tool.Def[]>
|
}) => Effect.Effect<Tool.Def[]>
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -87,6 +92,7 @@ const layer = Layer.effect(
|
|||||||
const agents = yield* Agent.Service
|
const agents = yield* Agent.Service
|
||||||
const truncate = yield* Truncate.Service
|
const truncate = yield* Truncate.Service
|
||||||
const flags = yield* RuntimeFlags.Service
|
const flags = yield* RuntimeFlags.Service
|
||||||
|
const mcp = yield* MCP.Service
|
||||||
|
|
||||||
const invalid = yield* InvalidTool
|
const invalid = yield* InvalidTool
|
||||||
const task = yield* TaskTool
|
const task = yield* TaskTool
|
||||||
@@ -104,6 +110,7 @@ const layer = Layer.effect(
|
|||||||
const greptool = yield* GrepTool
|
const greptool = yield* GrepTool
|
||||||
const patchtool = yield* ApplyPatchTool
|
const patchtool = yield* ApplyPatchTool
|
||||||
const skilltool = yield* SkillTool
|
const skilltool = yield* SkillTool
|
||||||
|
const codemode = yield* CodeModeTool
|
||||||
const agent = yield* Agent.Service
|
const agent = yield* Agent.Service
|
||||||
|
|
||||||
const state = yield* InstanceState.make<State>(
|
const state = yield* InstanceState.make<State>(
|
||||||
@@ -211,6 +218,7 @@ const layer = Layer.effect(
|
|||||||
question: Tool.init(question),
|
question: Tool.init(question),
|
||||||
lsp: Tool.init(lsptool),
|
lsp: Tool.init(lsptool),
|
||||||
plan: Tool.init(plan),
|
plan: Tool.init(plan),
|
||||||
|
codemode: Tool.init(codemode),
|
||||||
})
|
})
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@@ -232,6 +240,7 @@ const layer = Layer.effect(
|
|||||||
tool.patch,
|
tool.patch,
|
||||||
...(flags.experimentalLspTool ? [tool.lsp] : []),
|
...(flags.experimentalLspTool ? [tool.lsp] : []),
|
||||||
...(flags.experimentalPlanMode && flags.client === "cli" ? [tool.plan] : []),
|
...(flags.experimentalPlanMode && flags.client === "cli" ? [tool.plan] : []),
|
||||||
|
...(flags.experimentalCodeMode ? [tool.codemode] : []),
|
||||||
],
|
],
|
||||||
task: tool.task,
|
task: tool.task,
|
||||||
read: tool.read,
|
read: tool.read,
|
||||||
@@ -263,11 +272,25 @@ const layer = Layer.effect(
|
|||||||
return ["Available agent types and the tools they have access to:", description].join("\n")
|
return ["Available agent types and the tools they have access to:", description].join("\n")
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// The grouped MCP-tool catalog appended to the code-mode base description, built
|
||||||
|
// fresh per turn so it tracks live tool-list changes. Hard-denied tools (the shared
|
||||||
|
// Permission.visibleTools predicate over the agent's ruleset) never enter the
|
||||||
|
// catalog, its inlined signatures, or the in-program search index.
|
||||||
|
const describeCodeMode = Effect.fn("ToolRegistry.describeCodeMode")(function* (agent: Agent.Info, permission?: PermissionV1.Ruleset) {
|
||||||
|
const visible = Permission.visibleTools(yield* mcp.tools(), Permission.merge(agent.permission, permission ?? []))
|
||||||
|
const servers = Object.keys(yield* mcp.clients()).map(McpCatalog.sanitize)
|
||||||
|
return catalogInstructions(visible, yield* mcp.defs(), servers)
|
||||||
|
})
|
||||||
|
|
||||||
const tools: Interface["tools"] = Effect.fn("ToolRegistry.tools")(function* (input) {
|
const tools: Interface["tools"] = Effect.fn("ToolRegistry.tools")(function* (input) {
|
||||||
|
// Consulted once before the synchronous filter: code mode registers only when the
|
||||||
|
// experimental flag is on AND at least one MCP tool is connected.
|
||||||
|
const mcpToolCount = flags.experimentalCodeMode ? Object.keys(yield* mcp.tools()).length : 0
|
||||||
const filtered = (yield* all()).filter((tool) => {
|
const filtered = (yield* all()).filter((tool) => {
|
||||||
if (tool.id === WebSearchTool.id) {
|
if (tool.id === WebSearchTool.id) {
|
||||||
return webSearchEnabled(input.providerID, { exa: flags.enableExa, parallel: flags.enableParallel })
|
return webSearchEnabled(input.providerID, { exa: flags.enableExa, parallel: flags.enableParallel })
|
||||||
}
|
}
|
||||||
|
if (tool.id === CodeModeTool.id) return flags.experimentalCodeMode && mcpToolCount > 0
|
||||||
|
|
||||||
const usePatch =
|
const usePatch =
|
||||||
input.modelID.includes("gpt-") && !input.modelID.includes("oss") && !input.modelID.includes("gpt-4")
|
input.modelID.includes("gpt-") && !input.modelID.includes("oss") && !input.modelID.includes("gpt-4")
|
||||||
@@ -292,7 +315,11 @@ const layer = Layer.effect(
|
|||||||
: undefined
|
: undefined
|
||||||
return {
|
return {
|
||||||
id: tool.id,
|
id: tool.id,
|
||||||
description: [output.description, tool.id === TaskTool.id ? yield* describeTask(input.agent) : undefined]
|
description: [
|
||||||
|
output.description,
|
||||||
|
tool.id === TaskTool.id ? yield* describeTask(input.agent) : undefined,
|
||||||
|
tool.id === CodeModeTool.id ? yield* describeCodeMode(input.agent, input.permission) : undefined,
|
||||||
|
]
|
||||||
.filter(Boolean)
|
.filter(Boolean)
|
||||||
.join("\n"),
|
.join("\n"),
|
||||||
parameters: output.parameters,
|
parameters: output.parameters,
|
||||||
@@ -314,6 +341,7 @@ const layer = Layer.effect(
|
|||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
function isZodType(value: unknown): value is z.ZodType {
|
function isZodType(value: unknown): value is z.ZodType {
|
||||||
return typeof value === "object" && value !== null && "_zod" in value
|
return typeof value === "object" && value !== null && "_zod" in value
|
||||||
}
|
}
|
||||||
@@ -406,6 +434,7 @@ export const node = LayerNode.make({
|
|||||||
LSP.node,
|
LSP.node,
|
||||||
Instruction.node,
|
Instruction.node,
|
||||||
FSUtil.node,
|
FSUtil.node,
|
||||||
|
MCP.node,
|
||||||
EventV2Bridge.node,
|
EventV2Bridge.node,
|
||||||
httpClient,
|
httpClient,
|
||||||
CrossSpawnSpawner.node,
|
CrossSpawnSpawner.node,
|
||||||
|
|||||||
@@ -117,6 +117,7 @@ function makeMcp(instructions: MCP.ServerInstructions[] = []) {
|
|||||||
clients: () => Effect.succeed({}),
|
clients: () => Effect.succeed({}),
|
||||||
instructions: () => Effect.succeed(instructions),
|
instructions: () => Effect.succeed(instructions),
|
||||||
tools: () => Effect.succeed({}),
|
tools: () => Effect.succeed({}),
|
||||||
|
defs: () => Effect.succeed({}),
|
||||||
prompts: () => Effect.succeed({}),
|
prompts: () => Effect.succeed({}),
|
||||||
resources: () => Effect.succeed({}),
|
resources: () => Effect.succeed({}),
|
||||||
resourceTemplates: () => Effect.succeed({}),
|
resourceTemplates: () => Effect.succeed({}),
|
||||||
|
|||||||
@@ -39,6 +39,7 @@ const mcp = Layer.succeed(
|
|||||||
clients: () => Effect.succeed({}),
|
clients: () => Effect.succeed({}),
|
||||||
instructions: () => Effect.succeed([]),
|
instructions: () => Effect.succeed([]),
|
||||||
tools: () => Effect.succeed({}),
|
tools: () => Effect.succeed({}),
|
||||||
|
defs: () => Effect.succeed({}),
|
||||||
prompts: () => Effect.succeed({}),
|
prompts: () => Effect.succeed({}),
|
||||||
resources: () => Effect.succeed({}),
|
resources: () => Effect.succeed({}),
|
||||||
resourceTemplates: () => Effect.succeed({}),
|
resourceTemplates: () => Effect.succeed({}),
|
||||||
|
|||||||
@@ -0,0 +1,164 @@
|
|||||||
|
import { afterEach, describe, expect } from "bun:test"
|
||||||
|
import path from "path"
|
||||||
|
import { Effect, Layer } from "effect"
|
||||||
|
import { LayerNode } from "@opencode-ai/core/effect/layer-node"
|
||||||
|
import { SessionTools } from "@/session/tools"
|
||||||
|
import { ToolRegistry } from "@/tool/registry"
|
||||||
|
import { Agent } from "@/agent/agent"
|
||||||
|
import { Config } from "@/config/config"
|
||||||
|
import { MCP } from "@/mcp"
|
||||||
|
import { McpCatalog } from "@/mcp/catalog"
|
||||||
|
import { Permission } from "@/permission"
|
||||||
|
import { Plugin } from "@/plugin"
|
||||||
|
import * as Truncate from "@/tool/truncate"
|
||||||
|
import { RuntimeFlags } from "@/effect/runtime-flags"
|
||||||
|
import { InstanceState } from "@/effect/instance-state"
|
||||||
|
import { MessageID, SessionID } from "@/session/schema"
|
||||||
|
import { disposeAllInstances } from "../fixture/fixture"
|
||||||
|
import { testEffect } from "../lib/effect"
|
||||||
|
import { TestConfig } from "../fixture/config"
|
||||||
|
import type { Tool as AITool } from "ai"
|
||||||
|
|
||||||
|
const configLayer = TestConfig.layer({
|
||||||
|
directories: () => InstanceState.directory.pipe(Effect.map((dir) => [path.join(dir, ".opencode")])),
|
||||||
|
})
|
||||||
|
|
||||||
|
// Fake MCP.Service built through the same `convertTool` path the real service uses,
|
||||||
|
// so the raw registration loop dispatches through a genuine ai-sdk execute. The fake
|
||||||
|
// client answers every call with a small text result.
|
||||||
|
function fakeMcpLayer(tools: Record<string, { description: string }>) {
|
||||||
|
const client = { callTool: async () => ({ content: [{ type: "text", text: "ok" }] }) }
|
||||||
|
const converted: Record<string, AITool> = Object.fromEntries(
|
||||||
|
Object.entries(tools).map(([key, def]) => [
|
||||||
|
key,
|
||||||
|
McpCatalog.convertTool(
|
||||||
|
{
|
||||||
|
name: key,
|
||||||
|
description: def.description,
|
||||||
|
inputSchema: { type: "object", properties: {} },
|
||||||
|
} as any,
|
||||||
|
client as any,
|
||||||
|
),
|
||||||
|
]),
|
||||||
|
)
|
||||||
|
return Layer.mock(MCP.Service, {
|
||||||
|
tools: () => Effect.succeed(converted),
|
||||||
|
defs: () => Effect.succeed({}),
|
||||||
|
clients: () => Effect.succeed({ github: { getServerCapabilities: () => undefined } } as any),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const mcpToolsLayer = fakeMcpLayer({
|
||||||
|
github_create_issue: { description: "Create an issue" },
|
||||||
|
github_list_issues: { description: "List issues" },
|
||||||
|
})
|
||||||
|
|
||||||
|
const root = LayerNode.group([ToolRegistry.node, Agent.node, MCP.node, RuntimeFlags.node])
|
||||||
|
const withCodeMode = testEffect(
|
||||||
|
LayerNode.compile(root, [
|
||||||
|
[Config.node, configLayer],
|
||||||
|
[RuntimeFlags.node, RuntimeFlags.layer({ experimentalCodeMode: true })],
|
||||||
|
[MCP.node, mcpToolsLayer],
|
||||||
|
]),
|
||||||
|
)
|
||||||
|
const withFlagOff = testEffect(
|
||||||
|
LayerNode.compile(root, [
|
||||||
|
[Config.node, configLayer],
|
||||||
|
[RuntimeFlags.node, RuntimeFlags.layer()],
|
||||||
|
[MCP.node, mcpToolsLayer],
|
||||||
|
]),
|
||||||
|
)
|
||||||
|
|
||||||
|
// Resolve the session tool record with the smallest honest stand-ins for the pieces
|
||||||
|
// resolve reads: a fabricated model (only providerID/api.id are consulted by the
|
||||||
|
// schema transform for these fixtures), a pass-through Truncate, an always-allow
|
||||||
|
// Permission, and an optionally-observing Plugin.trigger. Registry, MCP, and flags
|
||||||
|
// come from the compiled instance context.
|
||||||
|
function resolveTools(trigger?: Plugin.Interface["trigger"]) {
|
||||||
|
return Effect.gen(function* () {
|
||||||
|
const agents = yield* Agent.Service
|
||||||
|
const agent = yield* agents.defaultInfo()
|
||||||
|
return yield* SessionTools.resolve({
|
||||||
|
agent,
|
||||||
|
model: { providerID: "opencode", api: { id: "test" } } as any,
|
||||||
|
session: { id: SessionID.make("ses_session-tools"), permission: [] } as any,
|
||||||
|
processor: {
|
||||||
|
message: { id: MessageID.make("msg_session-tools") } as any,
|
||||||
|
updateToolCall: () => Effect.succeed(undefined),
|
||||||
|
completeToolCall: () => Effect.void,
|
||||||
|
},
|
||||||
|
bypassAgentCheck: false,
|
||||||
|
messages: [],
|
||||||
|
promptOps: {} as any,
|
||||||
|
})
|
||||||
|
}).pipe(
|
||||||
|
Effect.provide(
|
||||||
|
Layer.mergeAll(
|
||||||
|
Layer.mock(Permission.Service, { ask: () => Effect.void }),
|
||||||
|
Layer.mock(Plugin.Service, {
|
||||||
|
trigger:
|
||||||
|
trigger ?? (((_name, _input, output) => Effect.succeed(output)) as Plugin.Interface["trigger"]),
|
||||||
|
}),
|
||||||
|
Layer.mock(Truncate.Service, {
|
||||||
|
output: (text: string) => Effect.succeed({ content: text, truncated: false as const }),
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
afterEach(async () => {
|
||||||
|
await disposeAllInstances()
|
||||||
|
})
|
||||||
|
|
||||||
|
describe("session.tools resolve", () => {
|
||||||
|
withCodeMode.instance("code mode suppresses raw per-MCP registration behind the execute tool", () =>
|
||||||
|
Effect.gen(function* () {
|
||||||
|
const tools = yield* resolveTools()
|
||||||
|
const ids = Object.keys(tools)
|
||||||
|
expect(ids).toContain("execute")
|
||||||
|
expect(ids).not.toContain("github_create_issue")
|
||||||
|
expect(ids).not.toContain("github_list_issues")
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
|
withFlagOff.instance("without code mode the raw MCP tools register and execute is absent", () =>
|
||||||
|
Effect.gen(function* () {
|
||||||
|
const tools = yield* resolveTools()
|
||||||
|
const ids = Object.keys(tools)
|
||||||
|
expect(ids).not.toContain("execute")
|
||||||
|
expect(ids).toContain("github_create_issue")
|
||||||
|
expect(ids).toContain("github_list_issues")
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
|
withFlagOff.instance("legacy raw MCP execution fires plugin hooks keyed by the ai-sdk toolCallId", () =>
|
||||||
|
Effect.gen(function* () {
|
||||||
|
const events: { name: string; input: any; output: any }[] = []
|
||||||
|
const trigger = ((name: unknown, input: unknown, output: unknown) =>
|
||||||
|
Effect.sync(() => {
|
||||||
|
events.push({ name: name as string, input, output })
|
||||||
|
return output
|
||||||
|
})) as Plugin.Interface["trigger"]
|
||||||
|
const tools = yield* resolveTools(trigger)
|
||||||
|
const raw = tools["github_create_issue"]!
|
||||||
|
const result = yield* Effect.promise(() =>
|
||||||
|
Promise.resolve(
|
||||||
|
raw.execute!(
|
||||||
|
{ title: "x" },
|
||||||
|
{ toolCallId: "call_legacy", abortSignal: new AbortController().signal, messages: [] },
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
expect((result as any).output).toBe("ok")
|
||||||
|
expect(events.map((e) => [e.name, e.input.tool, e.input.callID])).toEqual([
|
||||||
|
["tool.execute.before", "github_create_issue", "call_legacy"],
|
||||||
|
["tool.execute.after", "github_create_issue", "call_legacy"],
|
||||||
|
])
|
||||||
|
expect(events[0]!.output).toEqual({ args: { title: "x" } })
|
||||||
|
// The after hook receives the raw MCP result, before model-facing shaping.
|
||||||
|
expect(events[1]!.output).toMatchObject({ content: [{ type: "text", text: "ok" }] })
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
})
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
import { beforeAll, describe, expect, test } from "bun:test"
|
import { beforeAll, describe, expect, test } from "bun:test"
|
||||||
import { CodeModeTool, describeCatalog } from "@/tool/code-mode"
|
import { CodeModeTool, catalogInstructions } from "@/tool/code-mode"
|
||||||
import { McpCatalog } from "@/mcp/catalog"
|
import { McpCatalog } from "@/mcp/catalog"
|
||||||
import { Agent } from "@/agent/agent"
|
import { Agent } from "@/agent/agent"
|
||||||
import { MCP } from "@/mcp"
|
import { MCP } from "@/mcp"
|
||||||
@@ -17,9 +17,12 @@ import {
|
|||||||
ListToolsRequestSchema,
|
ListToolsRequestSchema,
|
||||||
type Tool as MCPToolDef,
|
type Tool as MCPToolDef,
|
||||||
} from "@modelcontextprotocol/sdk/types.js"
|
} from "@modelcontextprotocol/sdk/types.js"
|
||||||
import { Cause, Effect, Exit, Layer } from "effect"
|
import type { Tool as AITool } from "ai"
|
||||||
|
import { Effect, Layer } from "effect"
|
||||||
|
|
||||||
const PNG = "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNkYPhfDwAChwGA60e6kgAAAABJRU5ErkJggg=="
|
// A 1x1 transparent PNG, base64-encoded, used to exercise image attachments.
|
||||||
|
const PNG =
|
||||||
|
"iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNkYPhfDwAChwGA60e6kgAAAABJRU5ErkJggg=="
|
||||||
|
|
||||||
const SERVER = "fixtures"
|
const SERVER = "fixtures"
|
||||||
|
|
||||||
@@ -34,7 +37,16 @@ const ctx: Tool.Context = {
|
|||||||
ask: () => Effect.void,
|
ask: () => Effect.void,
|
||||||
}
|
}
|
||||||
|
|
||||||
// Avoid the SDK Client here; other MCP tests mock it process-globally.
|
/**
|
||||||
|
* A minimal JSON-RPC MCP client speaking the real protocol over the in-memory transport.
|
||||||
|
* Deliberately NOT the SDK `Client`: `test/mcp/lifecycle.test.ts` (and friends) replace
|
||||||
|
* `@modelcontextprotocol/sdk/client/index.js` via bun's `mock.module`, which is
|
||||||
|
* process-global and irreversible — in a full `bun test` run (CI) any later import of the
|
||||||
|
* SDK Client gets the mock, whose `listTools` returns a canned `test_tool`. Speaking raw
|
||||||
|
* JSON-RPC keeps this suite's "real server, real transport, real protocol" property while
|
||||||
|
* being immune to that contamination. Only the surface `convertTool`/this file use is
|
||||||
|
* implemented: connect handshake, tools/list, tools/call.
|
||||||
|
*/
|
||||||
class RawJsonRpcClient {
|
class RawJsonRpcClient {
|
||||||
private nextId = 1
|
private nextId = 1
|
||||||
private pending = new Map<number, { resolve: (value: any) => void; reject: (error: Error) => void }>()
|
private pending = new Map<number, { resolve: (value: any) => void; reject: (error: Error) => void }>()
|
||||||
@@ -44,7 +56,7 @@ class RawJsonRpcClient {
|
|||||||
async connect() {
|
async connect() {
|
||||||
this.transport.onmessage = (message) => {
|
this.transport.onmessage = (message) => {
|
||||||
const msg = message as { id?: number; result?: unknown; error?: { message: string } }
|
const msg = message as { id?: number; result?: unknown; error?: { message: string } }
|
||||||
if (msg.id === undefined) return
|
if (msg.id === undefined) return // notifications/requests from the server are not needed here
|
||||||
const entry = this.pending.get(msg.id)
|
const entry = this.pending.get(msg.id)
|
||||||
if (!entry) return
|
if (!entry) return
|
||||||
this.pending.delete(msg.id)
|
this.pending.delete(msg.id)
|
||||||
@@ -71,11 +83,16 @@ class RawJsonRpcClient {
|
|||||||
return this.request("tools/list", {})
|
return this.request("tools/list", {})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** The `convertTool` surface: schema/options (timeouts, progress) are SDK-client
|
||||||
|
* concerns and are ignored here — the server never sees them. */
|
||||||
callTool(params: { name: string; arguments?: Record<string, unknown> }, _schema?: unknown, _options?: unknown) {
|
callTool(params: { name: string; arguments?: Record<string, unknown> }, _schema?: unknown, _options?: unknown) {
|
||||||
return this.request("tools/call", params)
|
return this.request("tools/call", params)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// A real MCP server, exposed over an in-memory transport, with a representative mix
|
||||||
|
// of tools: plain text, structured data (with an outputSchema), an image, and a
|
||||||
|
// failing tool. Tools are defined with raw JSON Schema so outputSchema is exact.
|
||||||
const TOOL_DEFS: MCPToolDef[] = [
|
const TOOL_DEFS: MCPToolDef[] = [
|
||||||
{
|
{
|
||||||
name: "get_text",
|
name: "get_text",
|
||||||
@@ -133,15 +150,21 @@ async function buildTool() {
|
|||||||
await client.connect()
|
await client.connect()
|
||||||
|
|
||||||
const listed = (await client.listTools()).tools as MCPToolDef[]
|
const listed = (await client.listTools()).tools as MCPToolDef[]
|
||||||
const mcpTools: Record<string, MCP.McpTool> = {}
|
const mcpTools: Record<string, AITool> = {}
|
||||||
|
const mcpDefs: Record<string, MCPToolDef> = {}
|
||||||
for (const def of listed) {
|
for (const def of listed) {
|
||||||
mcpTools[McpCatalog.toolName(SERVER, def.name)] = { def, client: client as unknown as Client }
|
const key = McpCatalog.toolName(SERVER, def.name)
|
||||||
|
mcpDefs[key] = def
|
||||||
|
mcpTools[key] = McpCatalog.convertTool(def, client as unknown as Client)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Truncate echoes its input so assertions read the exact program output; Agent/Session
|
||||||
|
// supply the (empty) permission rulesets the execute path merges; MCP serves the tools
|
||||||
|
// this real in-memory server listed — the same snapshot shape the live service returns.
|
||||||
const layer = Layer.mergeAll(
|
const layer = Layer.mergeAll(
|
||||||
Layer.mock(Plugin.Service, {
|
Layer.mock(Plugin.Service, {
|
||||||
trigger: ((_name: unknown, _input: unknown, output: unknown) =>
|
trigger: (((_name: unknown, _input: unknown, output: unknown) =>
|
||||||
Effect.succeed(output)) as Plugin.Interface["trigger"],
|
Effect.succeed(output)) as Plugin.Interface["trigger"]),
|
||||||
}),
|
}),
|
||||||
Layer.mock(Truncate.Service, {
|
Layer.mock(Truncate.Service, {
|
||||||
output: (text: string) => Effect.succeed({ content: text, truncated: false as const }),
|
output: (text: string) => Effect.succeed({ content: text, truncated: false as const }),
|
||||||
@@ -150,22 +173,18 @@ async function buildTool() {
|
|||||||
Layer.mock(Session.Service, { get: () => Effect.succeed({ permission: [] } as any) }),
|
Layer.mock(Session.Service, { get: () => Effect.succeed({ permission: [] } as any) }),
|
||||||
Layer.mock(MCP.Service, {
|
Layer.mock(MCP.Service, {
|
||||||
tools: () => Effect.succeed(mcpTools),
|
tools: () => Effect.succeed(mcpTools),
|
||||||
|
defs: () => Effect.succeed(mcpDefs),
|
||||||
clients: () => Effect.succeed({ [SERVER]: {} as any }),
|
clients: () => Effect.succeed({ [SERVER]: {} as any }),
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
return {
|
return {
|
||||||
tool: await Effect.runPromise(CodeModeTool.pipe(Effect.flatMap(Tool.init), Effect.provide(layer))),
|
tool: await Effect.runPromise(CodeModeTool.pipe(Effect.flatMap(Tool.init), Effect.provide(layer))),
|
||||||
description: describeCatalog(mcpTools, [SERVER]),
|
// The catalog section the registry appends to the base description (describeCodeMode).
|
||||||
|
description: catalogInstructions(mcpTools, mcpDefs, [SERVER]),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const run = (code: string) => Effect.runPromise(tool.execute({ code }, ctx))
|
const run = (code: string) => Effect.runPromise(tool.execute({ code }, ctx))
|
||||||
// Program failures die at the tool boundary; recover the defect for message assertions.
|
|
||||||
const runFailed = async (code: string) => {
|
|
||||||
const exit = await Effect.runPromise(tool.execute({ code }, ctx).pipe(Effect.exit))
|
|
||||||
if (Exit.isSuccess(exit)) throw new Error("expected the tool to fail")
|
|
||||||
return Cause.squash(exit.cause) as Error
|
|
||||||
}
|
|
||||||
|
|
||||||
beforeAll(async () => {
|
beforeAll(async () => {
|
||||||
const built = await buildTool()
|
const built = await buildTool()
|
||||||
@@ -177,10 +196,14 @@ describe("code mode integration (real MCP server)", () => {
|
|||||||
test("the appended catalog inlines full signatures with real MCP schemas", () => {
|
test("the appended catalog inlines full signatures with real MCP schemas", () => {
|
||||||
expect(description).toContain("Available tools (COMPLETE list")
|
expect(description).toContain("Available tools (COMPLETE list")
|
||||||
expect(description).toContain("- fixtures (4 tools)")
|
expect(description).toContain("- fixtures (4 tools)")
|
||||||
expect(description).toContain("tools.fixtures.add(input: { a: number; b: number }): Promise<{ sum: number }>")
|
expect(description).toContain(
|
||||||
|
"tools.fixtures.add(input: { a: number; b: number }): Promise<{ sum: number }>",
|
||||||
|
)
|
||||||
expect(description).toContain("tools.fixtures.get_text(input: { name: string }): Promise<unknown>")
|
expect(description).toContain("tools.fixtures.get_text(input: { name: string }): Promise<unknown>")
|
||||||
expect(description).toContain("// Add two numbers and return the structured sum")
|
expect(description).toContain("// Add two numbers and return the structured sum")
|
||||||
|
// Small catalog: everything is inline, so no discovery tool is advertised.
|
||||||
expect(description).not.toContain("$codemode")
|
expect(description).not.toContain("$codemode")
|
||||||
|
// The workflow section is present with placeholder-only call forms.
|
||||||
expect(description).toContain("## Workflow")
|
expect(description).toContain("## Workflow")
|
||||||
expect(description).toContain("`const res = await tools.<namespace>.<tool>(input)`")
|
expect(description).toContain("`const res = await tools.<namespace>.<tool>(input)`")
|
||||||
expect(description).not.toContain("total_count")
|
expect(description).not.toContain("total_count")
|
||||||
@@ -229,6 +252,7 @@ describe("code mode integration (real MCP server)", () => {
|
|||||||
value: "[1 image attached to the result]",
|
value: "[1 image attached to the result]",
|
||||||
})
|
})
|
||||||
expect(out.output).not.toContain(PNG)
|
expect(out.output).not.toContain(PNG)
|
||||||
|
// The stripped image still arrives as a real attachment.
|
||||||
expect(out.attachments).toHaveLength(1)
|
expect(out.attachments).toHaveLength(1)
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -254,8 +278,9 @@ describe("code mode integration (real MCP server)", () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
test("an uncaught MCP error surfaces as a failed execution", async () => {
|
test("an uncaught MCP error surfaces as a failed execution", async () => {
|
||||||
const error = await runFailed("await tools.fixtures.boom({}); return 'unreachable'")
|
const out = await run("await tools.fixtures.boom({}); return 'unreachable'")
|
||||||
expect(error.message).toContain("kaboom")
|
expect(out.metadata.error).toBe(true)
|
||||||
|
expect(out.output).toContain("kaboom")
|
||||||
})
|
})
|
||||||
|
|
||||||
test("console output is captured and appended as a Logs section after the result", async () => {
|
test("console output is captured and appended as a Logs section after the result", async () => {
|
||||||
@@ -270,13 +295,14 @@ describe("code mode integration (real MCP server)", () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
test("console output is preserved on the error path", async () => {
|
test("console output is preserved on the error path", async () => {
|
||||||
const error = await runFailed(`
|
const out = await run(`
|
||||||
console.log("before the throw")
|
console.log("before the throw")
|
||||||
await tools.fixtures.boom({})
|
await tools.fixtures.boom({})
|
||||||
return "unreachable"
|
return "unreachable"
|
||||||
`)
|
`)
|
||||||
expect(error.message).toContain("kaboom")
|
expect(out.metadata.error).toBe(true)
|
||||||
expect(error.message).toContain("Logs:\nbefore the throw")
|
expect(out.output).toContain("kaboom")
|
||||||
|
expect(out.output).toContain("Logs:\nbefore the throw")
|
||||||
})
|
})
|
||||||
|
|
||||||
test("a program that logs nothing gets no Logs section", async () => {
|
test("a program that logs nothing gets no Logs section", async () => {
|
||||||
|
|||||||
@@ -1,5 +1,15 @@
|
|||||||
import { describe, expect, test } from "bun:test"
|
import { describe, expect, test } from "bun:test"
|
||||||
import { CODE_MODE_TOOL, CodeModeTool, Parameters, describeCatalog } from "@/tool/code-mode"
|
import {
|
||||||
|
CODE_MODE_TOOL,
|
||||||
|
CodeModeTool,
|
||||||
|
Parameters,
|
||||||
|
catalogInstructions,
|
||||||
|
formatValue,
|
||||||
|
groupByServer,
|
||||||
|
toSandboxResult,
|
||||||
|
withLogs,
|
||||||
|
type Attachment,
|
||||||
|
} from "@/tool/code-mode"
|
||||||
import type { Tool as MCPToolDef } from "@modelcontextprotocol/sdk/types.js"
|
import type { Tool as MCPToolDef } from "@modelcontextprotocol/sdk/types.js"
|
||||||
import type { PermissionV1 } from "@opencode-ai/core/v1/permission"
|
import type { PermissionV1 } from "@opencode-ai/core/v1/permission"
|
||||||
import { Agent } from "@/agent/agent"
|
import { Agent } from "@/agent/agent"
|
||||||
@@ -9,8 +19,10 @@ import { Plugin } from "@/plugin"
|
|||||||
import { Session } from "@/session/session"
|
import { Session } from "@/session/session"
|
||||||
import { Tool } from "@/tool/tool"
|
import { Tool } from "@/tool/tool"
|
||||||
import * as Truncate from "@/tool/truncate"
|
import * as Truncate from "@/tool/truncate"
|
||||||
|
import { McpCatalog } from "@/mcp/catalog"
|
||||||
import { MessageID, SessionID } from "@/session/schema"
|
import { MessageID, SessionID } from "@/session/schema"
|
||||||
import { Cause, Effect, Exit, Layer, Schema } from "effect"
|
import type { Tool as AITool } from "ai"
|
||||||
|
import { Effect, Layer, Schema } from "effect"
|
||||||
|
|
||||||
const ctx: Tool.Context = {
|
const ctx: Tool.Context = {
|
||||||
sessionID: SessionID.make("ses_code-mode"),
|
sessionID: SessionID.make("ses_code-mode"),
|
||||||
@@ -23,22 +35,28 @@ const ctx: Tool.Context = {
|
|||||||
ask: () => Effect.void,
|
ask: () => Effect.void,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Build a real MCP-derived AI SDK tool over a fake transport, so the adapter exercises
|
||||||
|
// the same `convertTool` execution path that `mcp.tools()` produces at runtime.
|
||||||
function mcpTool(
|
function mcpTool(
|
||||||
name: string,
|
name: string,
|
||||||
handler: (args: Record<string, unknown>) => unknown,
|
handler: (args: Record<string, unknown>) => unknown,
|
||||||
inputSchema: Record<string, unknown> = { type: "object", properties: {} },
|
inputSchema: Record<string, unknown> = { type: "object", properties: {} },
|
||||||
outputSchema?: Record<string, unknown>,
|
): AITool {
|
||||||
): MCP.McpTool {
|
const client = {
|
||||||
return {
|
|
||||||
def: { name, description: name, inputSchema, ...(outputSchema ? { outputSchema } : {}) } as MCPToolDef,
|
|
||||||
client: {
|
|
||||||
callTool: async (params: { arguments?: Record<string, unknown> }) => handler(params.arguments ?? {}),
|
callTool: async (params: { arguments?: Record<string, unknown> }) => handler(params.arguments ?? {}),
|
||||||
} as unknown as MCP.McpTool["client"],
|
|
||||||
}
|
}
|
||||||
|
return McpCatalog.convertTool({ name, description: name, inputSchema } as any, client as any)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Truncate echoes its input so assertions read the exact program output. Agent.get is
|
||||||
|
// consulted by the shared wrapper during truncation AND at execute time for the
|
||||||
|
// permission ruleset that filters the dispatchable tool tree; Session.get supplies the
|
||||||
|
// (empty) session-level ruleset half of that merge. Plugin.trigger defaults to the
|
||||||
|
// pass-through the real service uses when no plugin implements a hook; tests observing
|
||||||
|
// or failing hooks override it.
|
||||||
function harness(input: {
|
function harness(input: {
|
||||||
mcpTools: Record<string, MCP.McpTool>
|
mcpTools: Record<string, AITool>
|
||||||
|
defs?: Record<string, MCPToolDef>
|
||||||
servers: string[]
|
servers: string[]
|
||||||
permission?: PermissionV1.Rule[]
|
permission?: PermissionV1.Rule[]
|
||||||
trigger?: Plugin.Interface["trigger"]
|
trigger?: Plugin.Interface["trigger"]
|
||||||
@@ -58,17 +76,21 @@ function harness(input: {
|
|||||||
}),
|
}),
|
||||||
Layer.mock(MCP.Service, {
|
Layer.mock(MCP.Service, {
|
||||||
tools: () => Effect.succeed(input.mcpTools),
|
tools: () => Effect.succeed(input.mcpTools),
|
||||||
|
defs: () => Effect.succeed(input.defs ?? {}),
|
||||||
clients: () => Effect.succeed(Object.fromEntries(input.servers.map((name) => [name, {} as any]))),
|
clients: () => Effect.succeed(Object.fromEntries(input.servers.map((name) => [name, {} as any]))),
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
function serverNames(mcpTools: Record<string, MCP.McpTool>, servers?: string[]) {
|
// Derive sanitized server namespaces from the catalog keys, mirroring how the registry
|
||||||
|
// passes `Object.keys(mcp.clients()).map(sanitize)`.
|
||||||
|
function serverNames(mcpTools: Record<string, AITool>, servers?: string[]) {
|
||||||
return servers ?? [...new Set(Object.keys(mcpTools).map((key) => key.split("_")[0]!))]
|
return servers ?? [...new Set(Object.keys(mcpTools).map((key) => key.split("_")[0]!))]
|
||||||
}
|
}
|
||||||
|
|
||||||
function build(
|
function build(
|
||||||
mcpTools: Record<string, MCP.McpTool>,
|
mcpTools: Record<string, AITool>,
|
||||||
|
defs: Record<string, MCPToolDef> = {},
|
||||||
servers?: string[],
|
servers?: string[],
|
||||||
permission?: PermissionV1.Rule[],
|
permission?: PermissionV1.Rule[],
|
||||||
trigger?: Plugin.Interface["trigger"],
|
trigger?: Plugin.Interface["trigger"],
|
||||||
@@ -77,20 +99,20 @@ function build(
|
|||||||
return Effect.runPromise(
|
return Effect.runPromise(
|
||||||
CodeModeTool.pipe(
|
CodeModeTool.pipe(
|
||||||
Effect.flatMap(Tool.init),
|
Effect.flatMap(Tool.init),
|
||||||
Effect.provide(harness({ mcpTools, servers: names, permission, trigger })),
|
Effect.provide(harness({ mcpTools, defs, servers: names, permission, trigger })),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
function describeFor(mcpTools: Record<string, MCP.McpTool>, servers?: string[], permission: PermissionV1.Rule[] = []) {
|
// The agent-facing description, as the registry composes it (`describeCodeMode`):
|
||||||
return describeCatalog(Permission.visibleTools(mcpTools, permission), serverNames(mcpTools, servers))
|
// permission-filtered tool set → grouped catalog → CodeMode instructions.
|
||||||
}
|
function describeFor(
|
||||||
|
mcpTools: Record<string, AITool>,
|
||||||
// Program failures die at the tool boundary; recover the defect for message assertions.
|
defs: Record<string, MCPToolDef> = {},
|
||||||
async function failure(effect: Effect.Effect<unknown>) {
|
servers?: string[],
|
||||||
const exit = await Effect.runPromise(effect.pipe(Effect.exit))
|
permission: PermissionV1.Rule[] = [],
|
||||||
if (Exit.isSuccess(exit)) throw new Error("expected the tool to fail")
|
) {
|
||||||
return Cause.squash(exit.cause) as Error
|
return catalogInstructions(Permission.visibleTools(mcpTools, permission), defs, serverNames(mcpTools, servers))
|
||||||
}
|
}
|
||||||
|
|
||||||
describe("code mode execute", () => {
|
describe("code mode execute", () => {
|
||||||
@@ -101,30 +123,38 @@ describe("code mode execute", () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
test("groups multi-underscore server names by longest matching prefix", () => {
|
test("groups multi-underscore server names by longest matching prefix", () => {
|
||||||
const description = describeFor({ my_server_do_thing: mcpTool("do_thing", () => "") }, ["my_server"])
|
const groups = groupByServer({ my_server_do_thing: mcpTool("do_thing", () => "") }, ["my_server"])
|
||||||
expect(description).toContain("- my_server (1 tool)")
|
expect([...groups.keys()]).toEqual(["my_server"])
|
||||||
expect(description).toContain("tools.my_server.do_thing(")
|
expect(groups.get("my_server")![0]).toMatchObject({
|
||||||
|
path: "my_server.do_thing",
|
||||||
|
local: "do_thing",
|
||||||
|
key: "my_server_do_thing",
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
test("groupByServer uses the whole key as the server name when it has no underscore", () => {
|
test("groupByServer uses the whole key as the server name when it has no underscore", () => {
|
||||||
const description = describeFor({ standalone: mcpTool("standalone", () => "") }, [])
|
const groups = groupByServer({ standalone: mcpTool("standalone", () => "") }, [])
|
||||||
expect(description).toContain("- standalone (1 tool)")
|
expect([...groups.keys()]).toEqual(["standalone"])
|
||||||
expect(description).toContain("tools.standalone.standalone(")
|
expect(groups.get("standalone")![0]).toMatchObject({
|
||||||
|
path: "standalone.standalone",
|
||||||
|
server: "standalone",
|
||||||
|
local: "standalone",
|
||||||
|
key: "standalone",
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
test("describeCatalog carries the raw MCP schemas for rendering", () => {
|
test("groupByServer carries the raw MCP schemas for rendering", () => {
|
||||||
const description = describeFor(
|
const defs: Record<string, MCPToolDef> = {
|
||||||
{
|
weather_current: {
|
||||||
weather_current: mcpTool(
|
name: "current",
|
||||||
"current",
|
inputSchema: { type: "object", properties: { city: { type: "string" } }, required: ["city"] },
|
||||||
() => "",
|
outputSchema: { type: "object", properties: { tempC: { type: "number" } }, required: ["tempC"] },
|
||||||
{ type: "object", properties: { city: { type: "string" } }, required: ["city"] },
|
} as any,
|
||||||
{ type: "object", properties: { tempC: { type: "number" } }, required: ["tempC"] },
|
}
|
||||||
),
|
const groups = groupByServer({ weather_current: mcpTool("current", () => "") }, ["weather"], defs)
|
||||||
},
|
const entry = groups.get("weather")![0]!
|
||||||
["weather"],
|
expect(entry.inputSchema).toEqual(defs.weather_current!.inputSchema as any)
|
||||||
)
|
expect(entry.outputSchema).toEqual(defs.weather_current!.outputSchema as any)
|
||||||
expect(description).toContain("tools.weather.current(input: { city: string }): Promise<{ tempC: number }>")
|
|
||||||
})
|
})
|
||||||
|
|
||||||
test("the static base description carries no catalog; the registry appends it", async () => {
|
test("the static base description carries no catalog; the registry appends it", async () => {
|
||||||
@@ -154,76 +184,90 @@ describe("code mode execute", () => {
|
|||||||
)
|
)
|
||||||
expect(description).toContain("tools.github.list_issues(")
|
expect(description).toContain("tools.github.list_issues(")
|
||||||
expect(description).toContain("tools.linear.search(")
|
expect(description).toContain("tools.linear.search(")
|
||||||
|
// A schema with no properties renders as an empty object, not `{ }`.
|
||||||
expect(description).toContain("tools.linear.search(input: {}): Promise<unknown>")
|
expect(description).toContain("tools.linear.search(input: {}): Promise<unknown>")
|
||||||
|
// Fully inlined catalog: no discovery round-trip is needed or advertised.
|
||||||
expect(description).not.toContain("$codemode")
|
expect(description).not.toContain("$codemode")
|
||||||
expect(description).not.toContain("Browse one namespace")
|
expect(description).not.toContain("Browse one namespace")
|
||||||
|
// The workflow/rules sections use placeholder call forms only — the example machinery
|
||||||
|
// never cherry-picks a catalog tool or fabricates result fields.
|
||||||
expect(description).toContain("## Workflow")
|
expect(description).toContain("## Workflow")
|
||||||
expect(description).toContain("1. Pick a tool from the list under `## Available tools`")
|
expect(description).toContain("1. Pick a tool from the list under `## Available tools`")
|
||||||
expect(description).toContain(
|
expect(description).toContain('`const data = typeof res === "string" ? JSON.parse(res) : res` - most tools return JSON as a string')
|
||||||
'`const data = typeof res === "string" ? JSON.parse(res) : res` - most tools return JSON as a string',
|
|
||||||
)
|
|
||||||
expect(description).toContain("Return only the fields you need")
|
expect(description).toContain("Return only the fields you need")
|
||||||
expect(description).not.toContain("total_count")
|
expect(description).not.toContain("total_count")
|
||||||
})
|
})
|
||||||
|
|
||||||
test("signatures render the declared outputSchema as the return type", () => {
|
test("signatures render the declared outputSchema as the return type", () => {
|
||||||
const description = describeFor({
|
const defs: Record<string, MCPToolDef> = {
|
||||||
weather_current: mcpTool(
|
weather_current: {
|
||||||
"current",
|
name: "current",
|
||||||
() => "",
|
inputSchema: { type: "object", properties: { city: { type: "string" } }, required: ["city"] },
|
||||||
{ type: "object", properties: { city: { type: "string" } }, required: ["city"] },
|
outputSchema: {
|
||||||
{
|
|
||||||
type: "object",
|
type: "object",
|
||||||
properties: { tempC: { type: "number" }, summary: { type: "string" } },
|
properties: { tempC: { type: "number" }, summary: { type: "string" } },
|
||||||
required: ["tempC"],
|
required: ["tempC"],
|
||||||
},
|
},
|
||||||
),
|
} as any,
|
||||||
})
|
}
|
||||||
|
const description = describeFor({ weather_current: mcpTool("current", () => "") }, defs)
|
||||||
expect(description).toContain(
|
expect(description).toContain(
|
||||||
"tools.weather.current(input: { city: string }): Promise<{ tempC: number; summary?: string }>",
|
"tools.weather.current(input: { city: string }): Promise<{ tempC: number; summary?: string }>",
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
test("large catalogs inline a budgeted PARTIAL list plus runtime search", async () => {
|
test("large catalogs inline a budgeted PARTIAL list plus runtime search", async () => {
|
||||||
const tools: Record<string, MCP.McpTool> = {}
|
const tools: Record<string, AITool> = {}
|
||||||
const filler = "a searchable description of this operation that consumes catalog budget ".repeat(3)
|
const filler = "a searchable description of this operation that consumes catalog budget ".repeat(3)
|
||||||
for (let i = 0; i < 150; i++) {
|
for (let i = 0; i < 150; i++) {
|
||||||
tools[`alpha_op_${i}`] = {
|
const client = { callTool: async () => ({ content: [] }) }
|
||||||
def: {
|
tools[`alpha_op_${i}`] = McpCatalog.convertTool(
|
||||||
|
{
|
||||||
name: `op_${i}`,
|
name: `op_${i}`,
|
||||||
description: `${filler}${i}`,
|
description: `${filler}${i}`,
|
||||||
inputSchema: { type: "object", properties: { value: { type: "string" }, count: { type: "number" } } },
|
inputSchema: { type: "object", properties: { value: { type: "string" }, count: { type: "number" } } },
|
||||||
} as MCPToolDef,
|
} as any,
|
||||||
client: { callTool: async () => ({ content: [] }) } as unknown as MCP.McpTool["client"],
|
client as any,
|
||||||
}
|
)
|
||||||
}
|
}
|
||||||
tools["zeta_only_tool"] = mcpTool("only_tool", () => "", {
|
tools["zeta_only_tool"] = mcpTool("only_tool", () => "", {
|
||||||
type: "object",
|
type: "object",
|
||||||
properties: { topic: { type: "string", description: "Subject to look up" } },
|
properties: { topic: { type: "string", description: "Subject to look up" } },
|
||||||
required: ["topic"],
|
required: ["topic"],
|
||||||
})
|
})
|
||||||
const description = describeFor(tools, ["alpha", "zeta"])
|
const description = describeFor(tools, {}, ["alpha", "zeta"])
|
||||||
|
|
||||||
|
// Every namespace is listed with counts; signatures inline round-robin across
|
||||||
|
// namespaces (cheapest-first within each) until the budget runs out, and the
|
||||||
|
// description states exactly how comprehensive the list is. Round-robin fairness:
|
||||||
|
// the small zeta namespace is fully shown even though alpha alone could exhaust
|
||||||
|
// the whole budget.
|
||||||
expect(description).toContain("Available tools (PARTIAL - ")
|
expect(description).toContain("Available tools (PARTIAL - ")
|
||||||
expect(description).toMatch(/- alpha \(150 tools, \d+ shown\)/)
|
expect(description).toMatch(/- alpha \(150 tools, \d+ shown\)/)
|
||||||
expect(description).toContain("- zeta (1 tool)\n")
|
expect(description).toContain("- zeta (1 tool)\n")
|
||||||
expect(description).toContain("tools.zeta.only_tool(input: { topic: string }): Promise<unknown>")
|
expect(description).toContain("tools.zeta.only_tool(input: { topic: string }): Promise<unknown>")
|
||||||
expect(description).toContain("tools.$codemode.search(")
|
expect(description).toContain("tools.$codemode.search(")
|
||||||
|
// PARTIAL catalogs put search first in the workflow and advertise namespace browsing.
|
||||||
expect(description).toContain("1. Find a tool (skip when it is already listed below)")
|
expect(description).toContain("1. Find a tool (skip when it is already listed below)")
|
||||||
expect(description).toContain(
|
expect(description).toContain('- Browse one namespace: `await tools.$codemode.search({ query: "", namespace: "<name>" })`.')
|
||||||
'- Browse one namespace: `await tools.$codemode.search({ query: "", namespace: "<name>" })`.',
|
|
||||||
)
|
|
||||||
expect(description).not.toContain("total_count")
|
expect(description).not.toContain("total_count")
|
||||||
|
// All op lines cost the same estimated tokens (chars/4 rounds away the 1- vs 3-digit
|
||||||
|
// name difference), so the path tiebreak decides: the lexicographically-first ops made
|
||||||
|
// the cut and the lexicographic tail (op_99 is maximal) did not.
|
||||||
expect(description).toContain("tools.alpha.op_0(")
|
expect(description).toContain("tools.alpha.op_0(")
|
||||||
expect(description).not.toContain("tools.alpha.op_99(")
|
expect(description).not.toContain("tools.alpha.op_99(")
|
||||||
|
|
||||||
const tool = await build(tools, ["alpha", "zeta"])
|
// The runtime search tool works in-program and returns complete signatures.
|
||||||
|
const tool = await build(tools, {}, ["alpha", "zeta"])
|
||||||
const out = await Effect.runPromise(
|
const out = await Effect.runPromise(
|
||||||
tool.execute({ code: "return await tools.$codemode.search({ query: 'only tool', limit: 3 })" }, ctx),
|
tool.execute({ code: "return await tools.$codemode.search({ query: 'only tool', limit: 3 })" }, ctx),
|
||||||
)
|
)
|
||||||
const result = JSON.parse(out.output)
|
const result = JSON.parse(out.output)
|
||||||
|
// Search-result paths carry the `tools.` prefix so each is directly usable as a call site.
|
||||||
expect(result.items.map((i: any) => i.path)).toContain("tools.zeta.only_tool")
|
expect(result.items.map((i: any) => i.path)).toContain("tools.zeta.only_tool")
|
||||||
expect(result.items[0].signature).toContain("tools.")
|
expect(result.items[0].signature).toContain("tools.")
|
||||||
|
// Search results render the pretty multiline signature: MCP input-property
|
||||||
|
// descriptions ride along as JSDoc field comments. The inline catalog stays compact.
|
||||||
const signature = result.items.find((i: any) => i.path === "tools.zeta.only_tool").signature
|
const signature = result.items.find((i: any) => i.path === "tools.zeta.only_tool").signature
|
||||||
expect(signature).toContain("tools.zeta.only_tool(input: {\n")
|
expect(signature).toContain("tools.zeta.only_tool(input: {\n")
|
||||||
expect(signature).toContain(" /** Subject to look up */\n topic: string")
|
expect(signature).toContain(" /** Subject to look up */\n topic: string")
|
||||||
@@ -246,10 +290,7 @@ describe("code mode execute", () => {
|
|||||||
linear_search: mcpTool("search", () => ""),
|
linear_search: mcpTool("search", () => ""),
|
||||||
})
|
})
|
||||||
const output = await Effect.runPromise(
|
const output = await Effect.runPromise(
|
||||||
tool.execute(
|
tool.execute({ code: "const namespaces = Object.keys(tools); return { namespaces, count: namespaces.length }" }, ctx),
|
||||||
{ code: "const namespaces = Object.keys(tools); return { namespaces, count: namespaces.length }" },
|
|
||||||
ctx,
|
|
||||||
),
|
|
||||||
)
|
)
|
||||||
expect(JSON.parse(output.output)).toEqual({ namespaces: ["github", "linear"], count: 2 })
|
expect(JSON.parse(output.output)).toEqual({ namespaces: ["github", "linear"], count: 2 })
|
||||||
})
|
})
|
||||||
@@ -320,16 +361,18 @@ describe("code mode execute", () => {
|
|||||||
expect(output.metadata.toolCalls.every((c) => c.status === "completed")).toBe(true)
|
expect(output.metadata.toolCalls.every((c) => c.status === "completed")).toBe(true)
|
||||||
})
|
})
|
||||||
|
|
||||||
test("a program failure fails the tool with a readable error", async () => {
|
test("returns a readable error when the program throws", async () => {
|
||||||
const tool = await build({})
|
const tool = await build({})
|
||||||
const error = await failure(tool.execute({ code: "throw new Error('boom')" }, ctx))
|
const output = await Effect.runPromise(tool.execute({ code: "throw new Error('boom')" }, ctx))
|
||||||
expect(error.message).toBe("Uncaught: boom")
|
expect(output.output).toBe("Uncaught: boom")
|
||||||
|
expect(output.metadata.error).toBe(true)
|
||||||
})
|
})
|
||||||
|
|
||||||
test("reports an unknown tool as a failed execution", async () => {
|
test("reports an unknown tool as a failed execution", async () => {
|
||||||
const tool = await build({ known_tool: mcpTool("tool", () => "ok") })
|
const tool = await build({ known_tool: mcpTool("tool", () => "ok") })
|
||||||
const error = await failure(tool.execute({ code: "return await tools.known.missing({})" }, ctx))
|
const output = await Effect.runPromise(tool.execute({ code: "return await tools.known.missing({})" }, ctx))
|
||||||
expect(error.message).toContain("Unknown tool 'known.missing'")
|
expect(output.metadata.error).toBe(true)
|
||||||
|
expect(output.output).toContain("Unknown tool 'known.missing'")
|
||||||
})
|
})
|
||||||
|
|
||||||
test("propagates an MCP tool error into the program as a catchable failure", async () => {
|
test("propagates an MCP tool error into the program as a catchable failure", async () => {
|
||||||
@@ -371,6 +414,7 @@ describe("code mode execute", () => {
|
|||||||
|
|
||||||
expect(output.output).toBe("denied: permission denied by user")
|
expect(output.output).toBe("denied: permission denied by user")
|
||||||
expect(output.metadata.error).toBeUndefined()
|
expect(output.metadata.error).toBeUndefined()
|
||||||
|
// The MCP tool itself never ran.
|
||||||
expect(called).toEqual([])
|
expect(called).toEqual([])
|
||||||
expect(output.metadata.toolCalls).toEqual([{ tool: "a.tool", status: "error" }])
|
expect(output.metadata.toolCalls).toEqual([{ tool: "a.tool", status: "error" }])
|
||||||
})
|
})
|
||||||
@@ -387,6 +431,7 @@ describe("code mode execute", () => {
|
|||||||
a_tool: mcpTool("a", () => ({ content: [{ type: "text", text: "one" }] })),
|
a_tool: mcpTool("a", () => ({ content: [{ type: "text", text: "one" }] })),
|
||||||
b_tool: mcpTool("b", () => ({ content: [{ type: "text", text: "two" }] })),
|
b_tool: mcpTool("b", () => ({ content: [{ type: "text", text: "two" }] })),
|
||||||
},
|
},
|
||||||
|
{},
|
||||||
undefined,
|
undefined,
|
||||||
undefined,
|
undefined,
|
||||||
trigger,
|
trigger,
|
||||||
@@ -397,6 +442,7 @@ describe("code mode execute", () => {
|
|||||||
)
|
)
|
||||||
|
|
||||||
expect(out.output).toBe("done")
|
expect(out.output).toBe("done")
|
||||||
|
// callID is synthetic and per-execution: `${parentCallID}/${n}`, n starting at 1.
|
||||||
expect(events.map((e) => [e.name, e.input.tool, e.input.callID])).toEqual([
|
expect(events.map((e) => [e.name, e.input.tool, e.input.callID])).toEqual([
|
||||||
["tool.execute.before", "a_tool", "call_code_mode/1"],
|
["tool.execute.before", "a_tool", "call_code_mode/1"],
|
||||||
["tool.execute.after", "a_tool", "call_code_mode/1"],
|
["tool.execute.after", "a_tool", "call_code_mode/1"],
|
||||||
@@ -407,6 +453,7 @@ describe("code mode execute", () => {
|
|||||||
expect(before!.input.sessionID).toBe(ctx.sessionID)
|
expect(before!.input.sessionID).toBe(ctx.sessionID)
|
||||||
expect(before!.output).toEqual({ args: { x: 1 } })
|
expect(before!.output).toEqual({ args: { x: 1 } })
|
||||||
expect(after!.input.args).toEqual({ x: 1 })
|
expect(after!.input.args).toEqual({ x: 1 })
|
||||||
|
// The after hook sees the raw MCP result — the same payload the legacy path passes.
|
||||||
expect(after!.output).toEqual({ content: [{ type: "text", text: "one" }] })
|
expect(after!.output).toEqual({ content: [{ type: "text", text: "one" }] })
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -422,6 +469,7 @@ describe("code mode execute", () => {
|
|||||||
}
|
}
|
||||||
const tool = await build(
|
const tool = await build(
|
||||||
{ a_tool: mcpTool("a", record("a")), b_tool: mcpTool("b", record("b")) },
|
{ a_tool: mcpTool("a", record("a")), b_tool: mcpTool("b", record("b")) },
|
||||||
|
{},
|
||||||
undefined,
|
undefined,
|
||||||
undefined,
|
undefined,
|
||||||
trigger,
|
trigger,
|
||||||
@@ -441,8 +489,10 @@ describe("code mode execute", () => {
|
|||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// The program handled the hook failure; the rest ran and the outer result is ok.
|
||||||
expect(out.metadata.error).toBeUndefined()
|
expect(out.metadata.error).toBeUndefined()
|
||||||
expect(out.output).toBe("hook exploded / ok")
|
expect(out.output).toBe("hook exploded / ok")
|
||||||
|
// The before hook gates dispatch: the failed child's tool never executed.
|
||||||
expect(called).toEqual(["b"])
|
expect(called).toEqual(["b"])
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -458,6 +508,7 @@ describe("code mode execute", () => {
|
|||||||
tool.execute({ code: "await tools.greeter.hello({ name: 'Ada' }); return 'done'" }, recordingCtx),
|
tool.execute({ code: "await tools.greeter.hello({ name: 'Ada' }); return 'done'" }, recordingCtx),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// The UI sees the call appear as running, then resolve to completed.
|
||||||
expect(snapshots).toContainEqual({
|
expect(snapshots).toContainEqual({
|
||||||
toolCalls: [{ tool: "greeter.hello", status: "running", input: { name: "Ada" } }],
|
toolCalls: [{ tool: "greeter.hello", status: "running", input: { name: "Ada" } }],
|
||||||
})
|
})
|
||||||
@@ -495,6 +546,7 @@ describe("code mode execute", () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
const out = await Effect.runPromise(tool.execute({ code: "return await tools.shot.take({})" }, ctx))
|
const out = await Effect.runPromise(tool.execute({ code: "return await tools.shot.take({})" }, ctx))
|
||||||
|
// The program received the structured content; the media rode along host-side.
|
||||||
expect(JSON.parse(out.output)).toEqual({ name: "shot.png" })
|
expect(JSON.parse(out.output)).toEqual({ name: "shot.png" })
|
||||||
expect(out.attachments).toEqual([{ type: "file", mime: "image/png", url: "data:image/png;base64,PNGDATA" }])
|
expect(out.attachments).toEqual([{ type: "file", mime: "image/png", url: "data:image/png;base64,PNGDATA" }])
|
||||||
expect(out.output).not.toContain("PNGDATA")
|
expect(out.output).not.toContain("PNGDATA")
|
||||||
@@ -509,83 +561,27 @@ describe("code mode execute", () => {
|
|||||||
expect(out.attachments).toEqual([{ type: "file", mime: "image/png", url: "data:image/png;base64,PNGDATA" }])
|
expect(out.attachments).toEqual([{ type: "file", mime: "image/png", url: "data:image/png;base64,PNGDATA" }])
|
||||||
})
|
})
|
||||||
|
|
||||||
test("media-only markers distinguish all-image from mixed attachments", async () => {
|
|
||||||
const tool = await build({
|
|
||||||
media_images: mcpTool("images", () => ({
|
|
||||||
content: [
|
|
||||||
{ type: "image", data: "PNG1", mimeType: "image/png" },
|
|
||||||
{ type: "image", data: "PNG2", mimeType: "image/png" },
|
|
||||||
],
|
|
||||||
})),
|
|
||||||
media_mixed: mcpTool("mixed", () => ({
|
|
||||||
content: [
|
|
||||||
{ type: "image", data: "PNG3", mimeType: "image/png" },
|
|
||||||
{ type: "resource", resource: { uri: "file:///tmp/report.pdf", mimeType: "application/pdf", blob: "PDF1" } },
|
|
||||||
],
|
|
||||||
})),
|
|
||||||
})
|
|
||||||
const out = await Effect.runPromise(
|
|
||||||
tool.execute(
|
|
||||||
{
|
|
||||||
code: `
|
|
||||||
const images = await tools.media.images({})
|
|
||||||
const mixed = await tools.media.mixed({})
|
|
||||||
return { images, mixed }
|
|
||||||
`,
|
|
||||||
},
|
|
||||||
ctx,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
|
|
||||||
expect(JSON.parse(out.output)).toEqual({
|
|
||||||
images: "[2 images attached to the result]",
|
|
||||||
mixed: "[2 files attached to the result]",
|
|
||||||
})
|
|
||||||
expect(out.output).not.toContain("PNG")
|
|
||||||
expect(out.attachments).toEqual([
|
|
||||||
{ type: "file", mime: "image/png", url: "data:image/png;base64,PNG1" },
|
|
||||||
{ type: "file", mime: "image/png", url: "data:image/png;base64,PNG2" },
|
|
||||||
{ type: "file", mime: "image/png", url: "data:image/png;base64,PNG3" },
|
|
||||||
{ type: "file", mime: "application/pdf", url: "data:application/pdf;base64,PDF1", filename: "report.pdf" },
|
|
||||||
])
|
|
||||||
})
|
|
||||||
|
|
||||||
test("resource links flow to the program as text, never as attachments", async () => {
|
|
||||||
const tool = await build({
|
|
||||||
docs_find: mcpTool("find", () => ({
|
|
||||||
content: [
|
|
||||||
{
|
|
||||||
type: "resource_link",
|
|
||||||
uri: "https://example.com/guide.pdf",
|
|
||||||
name: "guide.pdf",
|
|
||||||
mimeType: "application/pdf",
|
|
||||||
},
|
|
||||||
{ type: "resource_link", uri: "file:///tmp/notes.md", name: "notes.md" },
|
|
||||||
],
|
|
||||||
})),
|
|
||||||
})
|
|
||||||
const out = await Effect.runPromise(tool.execute({ code: "return await tools.docs.find({})" }, ctx))
|
|
||||||
|
|
||||||
expect(out.output).toBe("guide.pdf: https://example.com/guide.pdf\nnotes.md: file:///tmp/notes.md")
|
|
||||||
expect(out.attachments).toBeUndefined()
|
|
||||||
})
|
|
||||||
|
|
||||||
test("attachments still flow when the program returns something else entirely", async () => {
|
test("attachments still flow when the program returns something else entirely", async () => {
|
||||||
const tool = await build({
|
const tool = await build({
|
||||||
shot_take: mcpTool("take", () => ({ content: [{ type: "image", data: "PNGDATA", mimeType: "image/png" }] })),
|
shot_take: mcpTool("take", () => ({ content: [{ type: "image", data: "PNGDATA", mimeType: "image/png" }] })),
|
||||||
})
|
})
|
||||||
const out = await Effect.runPromise(tool.execute({ code: "await tools.shot.take({}); return 'captured'" }, ctx))
|
const out = await Effect.runPromise(
|
||||||
|
tool.execute({ code: "await tools.shot.take({}); return 'captured'" }, ctx),
|
||||||
|
)
|
||||||
expect(out.output).toBe("captured")
|
expect(out.output).toBe("captured")
|
||||||
expect(out.attachments).toHaveLength(1)
|
expect(out.attachments).toHaveLength(1)
|
||||||
})
|
})
|
||||||
|
|
||||||
test("isolates the sandbox from host globals", async () => {
|
test("isolates the sandbox from host globals", async () => {
|
||||||
const tool = await build({})
|
const tool = await build({})
|
||||||
const error = await failure(tool.execute({ code: "return process.env" }, ctx))
|
const output = await Effect.runPromise(tool.execute({ code: "return process.env" }, ctx))
|
||||||
expect(error.message).toContain("process")
|
expect(output.metadata.error).toBe(true)
|
||||||
})
|
})
|
||||||
|
|
||||||
test("cancelling via ctx.abort interrupts the running program", async () => {
|
test("cancelling via ctx.abort interrupts the running program", async () => {
|
||||||
|
// The child call triggers the abort itself, deterministically, while the program
|
||||||
|
// swallows the call's failure and heads into an infinite loop. If abort did not
|
||||||
|
// interrupt the execution fiber, this test would hang on the busy loop.
|
||||||
const controller = new AbortController()
|
const controller = new AbortController()
|
||||||
const tool = await build({
|
const tool = await build({
|
||||||
host_trigger: mcpTool("trigger", () => {
|
host_trigger: mcpTool("trigger", () => {
|
||||||
@@ -617,6 +613,8 @@ describe("code mode execute", () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
test("leaves oversized results to OpenCode's native tool-output truncation", async () => {
|
test("leaves oversized results to OpenCode's native tool-output truncation", async () => {
|
||||||
|
// No CodeMode output limit is set, so the full result reaches the shared Tool.define
|
||||||
|
// wrapper intact (the harness Truncate fake passes it through un-truncated).
|
||||||
const tool = await build({})
|
const tool = await build({})
|
||||||
const output = await Effect.runPromise(tool.execute({ code: "return 'x'.repeat(40000)" }, ctx))
|
const output = await Effect.runPromise(tool.execute({ code: "return 'x'.repeat(40000)" }, ctx))
|
||||||
expect(output.metadata.error).toBeUndefined()
|
expect(output.metadata.error).toBeUndefined()
|
||||||
@@ -632,9 +630,12 @@ describe("code mode execute", () => {
|
|||||||
)
|
)
|
||||||
expect(ok.output).toBe("done\n\nLogs:\nstep one\n[warn] careful")
|
expect(ok.output).toBe("done\n\nLogs:\nstep one\n[warn] careful")
|
||||||
|
|
||||||
const error = await failure(tool.execute({ code: "console.log('before the throw'); throw new Error('boom')" }, ctx))
|
const err = await Effect.runPromise(
|
||||||
expect(error.message).toContain("Uncaught: boom")
|
tool.execute({ code: "console.log('before the throw'); throw new Error('boom')" }, ctx),
|
||||||
expect(error.message).toContain("Logs:\nbefore the throw")
|
)
|
||||||
|
expect(err.metadata.error).toBe(true)
|
||||||
|
expect(err.output).toContain("Uncaught: boom")
|
||||||
|
expect(err.output).toContain("Logs:\nbefore the throw")
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -648,7 +649,7 @@ describe("code mode permission visibility", () => {
|
|||||||
github_create_issue: mcpTool("create_issue", ok),
|
github_create_issue: mcpTool("create_issue", ok),
|
||||||
github_list_issues: mcpTool("list_issues", ok),
|
github_list_issues: mcpTool("list_issues", ok),
|
||||||
}
|
}
|
||||||
const description = describeFor(mcpTools, ["github"], [deny("github_create_issue")])
|
const description = describeFor(mcpTools, {}, ["github"], [deny("github_create_issue")])
|
||||||
expect(description).toContain("tools.github.list_issues(")
|
expect(description).toContain("tools.github.list_issues(")
|
||||||
expect(description).not.toContain("create_issue")
|
expect(description).not.toContain("create_issue")
|
||||||
expect(description).toContain("- github (1 tool)")
|
expect(description).toContain("- github (1 tool)")
|
||||||
@@ -659,7 +660,7 @@ describe("code mode permission visibility", () => {
|
|||||||
github_create_issue: mcpTool("create_issue", ok),
|
github_create_issue: mcpTool("create_issue", ok),
|
||||||
github_list_issues: mcpTool("list_issues", ok),
|
github_list_issues: mcpTool("list_issues", ok),
|
||||||
}
|
}
|
||||||
const description = describeFor(mcpTools, ["github"], [askRule("github_create_issue")])
|
const description = describeFor(mcpTools, {}, ["github"], [askRule("github_create_issue")])
|
||||||
expect(description).toContain("tools.github.create_issue(")
|
expect(description).toContain("tools.github.create_issue(")
|
||||||
expect(description).toContain("tools.github.list_issues(")
|
expect(description).toContain("tools.github.list_issues(")
|
||||||
expect(description).toContain("- github (2 tools)")
|
expect(description).toContain("- github (2 tools)")
|
||||||
@@ -675,16 +676,23 @@ describe("code mode permission visibility", () => {
|
|||||||
}),
|
}),
|
||||||
github_list_issues: mcpTool("list_issues", ok),
|
github_list_issues: mcpTool("list_issues", ok),
|
||||||
},
|
},
|
||||||
|
{},
|
||||||
["github"],
|
["github"],
|
||||||
[deny("github_create_issue")],
|
[deny("github_create_issue")],
|
||||||
)
|
)
|
||||||
|
|
||||||
const denied = await failure(tool.execute({ code: "return await tools.github.create_issue({ title: 'x' })" }, ctx))
|
const denied = await Effect.runPromise(
|
||||||
expect(denied.message).toContain("Unknown tool 'github.create_issue'")
|
tool.execute({ code: "return await tools.github.create_issue({ title: 'x' })" }, ctx),
|
||||||
expect(denied.message).not.toContain("permission")
|
)
|
||||||
|
expect(denied.metadata.error).toBe(true)
|
||||||
|
expect(denied.output).toContain("Unknown tool 'github.create_issue'")
|
||||||
|
expect(denied.output).not.toContain("permission")
|
||||||
expect(called).toEqual([])
|
expect(called).toEqual([])
|
||||||
|
|
||||||
const allowed = await Effect.runPromise(tool.execute({ code: "return await tools.github.list_issues({})" }, ctx))
|
// The rest of the namespace still works.
|
||||||
|
const allowed = await Effect.runPromise(
|
||||||
|
tool.execute({ code: "return await tools.github.list_issues({})" }, ctx),
|
||||||
|
)
|
||||||
expect(allowed.metadata.error).toBeUndefined()
|
expect(allowed.metadata.error).toBeUndefined()
|
||||||
expect(allowed.output).toBe("ok")
|
expect(allowed.output).toBe("ok")
|
||||||
})
|
})
|
||||||
@@ -694,10 +702,13 @@ describe("code mode permission visibility", () => {
|
|||||||
const askCtx: Tool.Context = { ...ctx, ask: (req) => Effect.sync(() => void asked.push(req.permission)) }
|
const askCtx: Tool.Context = { ...ctx, ask: (req) => Effect.sync(() => void asked.push(req.permission)) }
|
||||||
const tool = await build(
|
const tool = await build(
|
||||||
{ github_list_issues: mcpTool("list_issues", ok) },
|
{ github_list_issues: mcpTool("list_issues", ok) },
|
||||||
|
{},
|
||||||
["github"],
|
["github"],
|
||||||
[askRule("github_list_issues")],
|
[askRule("github_list_issues")],
|
||||||
)
|
)
|
||||||
const out = await Effect.runPromise(tool.execute({ code: "return await tools.github.list_issues({})" }, askCtx))
|
const out = await Effect.runPromise(
|
||||||
|
tool.execute({ code: "return await tools.github.list_issues({})" }, askCtx),
|
||||||
|
)
|
||||||
expect(out.output).toBe("ok")
|
expect(out.output).toBe("ok")
|
||||||
expect(asked).toEqual(["github_list_issues"])
|
expect(asked).toEqual(["github_list_issues"])
|
||||||
})
|
})
|
||||||
@@ -707,8 +718,144 @@ describe("code mode permission visibility", () => {
|
|||||||
const visible = Permission.visibleTools(tools, [
|
const visible = Permission.visibleTools(tools, [
|
||||||
deny("a_tool"),
|
deny("a_tool"),
|
||||||
askRule("b_tool"),
|
askRule("b_tool"),
|
||||||
|
// A scoped (non-"*") deny does not hide the tool from the catalog.
|
||||||
{ permission: "c_tool", pattern: "something", action: "deny" },
|
{ permission: "c_tool", pattern: "something", action: "deny" },
|
||||||
])
|
])
|
||||||
expect(Object.keys(visible)).toEqual(["b_tool", "c_tool"])
|
expect(Object.keys(visible)).toEqual(["b_tool", "c_tool"])
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
describe("toSandboxResult", () => {
|
||||||
|
const collector = () => {
|
||||||
|
const attachments: Attachment[] = []
|
||||||
|
return { attachments, collect: (a: Attachment) => void attachments.push(a) }
|
||||||
|
}
|
||||||
|
|
||||||
|
test("prefers structuredContent over text", () => {
|
||||||
|
const { collect } = collector()
|
||||||
|
expect(toSandboxResult({ structuredContent: { x: 1 }, content: [{ type: "text", text: "hi" }] }, collect)).toEqual(
|
||||||
|
{ x: 1 },
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
test("joins text content when no structured content is present", () => {
|
||||||
|
const { collect } = collector()
|
||||||
|
expect(
|
||||||
|
toSandboxResult(
|
||||||
|
{ content: [{ type: "text", text: "one" }, { type: "text", text: "two" }] },
|
||||||
|
collect,
|
||||||
|
),
|
||||||
|
).toBe("one\ntwo")
|
||||||
|
})
|
||||||
|
|
||||||
|
test("passes non-MCP values through untouched", () => {
|
||||||
|
const { collect } = collector()
|
||||||
|
expect(toSandboxResult("raw", collect)).toBe("raw")
|
||||||
|
expect(toSandboxResult(42, collect)).toBe(42)
|
||||||
|
expect(toSandboxResult(null, collect)).toBeNull()
|
||||||
|
expect(toSandboxResult({ some: "object" }, collect)).toEqual({ some: "object" })
|
||||||
|
})
|
||||||
|
|
||||||
|
test("strips media into the accumulator; text stays the sandbox value", () => {
|
||||||
|
const { attachments, collect } = collector()
|
||||||
|
const value = toSandboxResult(
|
||||||
|
{
|
||||||
|
content: [
|
||||||
|
{ type: "text", text: "see image" },
|
||||||
|
{ type: "image", data: "AAAA", mimeType: "image/png" },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
collect,
|
||||||
|
)
|
||||||
|
expect(value).toBe("see image")
|
||||||
|
expect(attachments).toEqual([{ type: "file", mime: "image/png", url: "data:image/png;base64,AAAA" }])
|
||||||
|
})
|
||||||
|
|
||||||
|
test("a media-only result yields a marker; counts and nouns follow the content", () => {
|
||||||
|
const one = collector()
|
||||||
|
expect(toSandboxResult({ content: [{ type: "image", data: "A", mimeType: "image/png" }] }, one.collect)).toBe(
|
||||||
|
"[1 image attached to the result]",
|
||||||
|
)
|
||||||
|
|
||||||
|
const two = collector()
|
||||||
|
expect(
|
||||||
|
toSandboxResult(
|
||||||
|
{
|
||||||
|
content: [
|
||||||
|
{ type: "image", data: "A", mimeType: "image/png" },
|
||||||
|
{ type: "image", data: "B", mimeType: "image/jpeg" },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
two.collect,
|
||||||
|
),
|
||||||
|
).toBe("[2 images attached to the result]")
|
||||||
|
expect(two.attachments).toHaveLength(2)
|
||||||
|
|
||||||
|
const mixed = collector()
|
||||||
|
expect(
|
||||||
|
toSandboxResult(
|
||||||
|
{
|
||||||
|
content: [
|
||||||
|
{ type: "image", data: "A", mimeType: "image/png" },
|
||||||
|
{ type: "audio", data: "B", mimeType: "audio/wav" },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
mixed.collect,
|
||||||
|
),
|
||||||
|
).toBe("[2 files attached to the result]")
|
||||||
|
})
|
||||||
|
|
||||||
|
test("extracts embedded resources: text inline, blobs as attachments with filenames", () => {
|
||||||
|
const { attachments, collect } = collector()
|
||||||
|
const value = toSandboxResult(
|
||||||
|
{
|
||||||
|
content: [
|
||||||
|
{ type: "resource", resource: { uri: "file:///tmp/notes.txt", mimeType: "text/plain", text: "note text" } },
|
||||||
|
{ type: "resource", resource: { uri: "file:///tmp/doc.pdf", mimeType: "application/pdf", blob: "PDF" } },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
collect,
|
||||||
|
)
|
||||||
|
expect(value).toBe("note text")
|
||||||
|
expect(attachments).toEqual([
|
||||||
|
{ type: "file", mime: "application/pdf", url: "data:application/pdf;base64,PDF", filename: "doc.pdf" },
|
||||||
|
])
|
||||||
|
})
|
||||||
|
|
||||||
|
test("collects resource_link blocks as external-URL attachments", () => {
|
||||||
|
const { attachments, collect } = collector()
|
||||||
|
const value = toSandboxResult(
|
||||||
|
{ content: [{ type: "resource_link", uri: "https://example.com/report.csv", mimeType: "text/csv" }] },
|
||||||
|
collect,
|
||||||
|
)
|
||||||
|
expect(value).toBe("[1 file attached to the result]")
|
||||||
|
expect(attachments).toEqual([
|
||||||
|
{ type: "file", mime: "text/csv", url: "https://example.com/report.csv", filename: "report.csv" },
|
||||||
|
])
|
||||||
|
})
|
||||||
|
|
||||||
|
test("an MCP-shaped result with nothing extractable becomes null", () => {
|
||||||
|
const { collect } = collector()
|
||||||
|
expect(toSandboxResult({ content: [] }, collect)).toBeNull()
|
||||||
|
expect(toSandboxResult({ content: [{ type: "mystery" }] }, collect)).toBeNull()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe("formatting helpers", () => {
|
||||||
|
test("formatValue", () => {
|
||||||
|
expect(formatValue("text")).toBe("text")
|
||||||
|
expect(formatValue({ a: 1 })).toBe(JSON.stringify({ a: 1 }, null, 2))
|
||||||
|
expect(formatValue(null)).toBe("null")
|
||||||
|
expect(formatValue(undefined)).toBe("undefined")
|
||||||
|
})
|
||||||
|
|
||||||
|
test("withLogs", () => {
|
||||||
|
// No logs: output is returned untouched.
|
||||||
|
expect(withLogs("result", [])).toBe("result")
|
||||||
|
expect(withLogs("result")).toBe("result")
|
||||||
|
// Logs are appended as a trailing section, one line each.
|
||||||
|
expect(withLogs("result", ["a", "[warn] b"])).toBe("result\n\nLogs:\na\n[warn] b")
|
||||||
|
// Empty output still gets the section (no leading blank lines).
|
||||||
|
expect(withLogs("", ["[error] boom"])).toBe("Logs:\n[error] boom")
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|||||||
@@ -17,6 +17,8 @@ import { InstanceState } from "@/effect/instance-state"
|
|||||||
import { ToolJsonSchema } from "@/tool/json-schema"
|
import { ToolJsonSchema } from "@/tool/json-schema"
|
||||||
import { MessageID, SessionID } from "@/session/schema"
|
import { MessageID, SessionID } from "@/session/schema"
|
||||||
import { RuntimeFlags } from "@/effect/runtime-flags"
|
import { RuntimeFlags } from "@/effect/runtime-flags"
|
||||||
|
import { MCP } from "@/mcp"
|
||||||
|
import { McpCatalog } from "@/mcp/catalog"
|
||||||
import { ProviderV2 } from "@opencode-ai/core/provider"
|
import { ProviderV2 } from "@opencode-ai/core/provider"
|
||||||
import { ModelV2 } from "@opencode-ai/core/model"
|
import { ModelV2 } from "@opencode-ai/core/model"
|
||||||
|
|
||||||
@@ -57,6 +59,45 @@ const replacements = [
|
|||||||
const it = testEffect(LayerNode.compile(root, replacements))
|
const it = testEffect(LayerNode.compile(root, replacements))
|
||||||
const withBrokenPlugin = testEffect(LayerNode.compile(root, [...replacements, [Plugin.node, brokenPluginLayer]]))
|
const withBrokenPlugin = testEffect(LayerNode.compile(root, [...replacements, [Plugin.node, brokenPluginLayer]]))
|
||||||
|
|
||||||
|
// Fake MCP.Service serving two tools on one server, built through the same
|
||||||
|
// `convertTool` path the real service uses so the code-mode catalog renders
|
||||||
|
// genuine signatures.
|
||||||
|
function fakeMcpLayer(tools: Record<string, { description: string }>) {
|
||||||
|
const client = { callTool: async () => ({ content: [] }) }
|
||||||
|
const converted = Object.fromEntries(
|
||||||
|
Object.entries(tools).map(([key, def]) => [
|
||||||
|
key,
|
||||||
|
McpCatalog.convertTool(
|
||||||
|
{
|
||||||
|
name: key,
|
||||||
|
description: def.description,
|
||||||
|
inputSchema: { type: "object", properties: {} },
|
||||||
|
} as any,
|
||||||
|
client as any,
|
||||||
|
),
|
||||||
|
]),
|
||||||
|
)
|
||||||
|
return Layer.mock(MCP.Service, {
|
||||||
|
tools: () => Effect.succeed(converted),
|
||||||
|
defs: () => Effect.succeed({}),
|
||||||
|
clients: () => Effect.succeed(Object.keys(tools).length > 0 ? ({ github: {} } as any) : {}),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const mcpToolsLayer = fakeMcpLayer({
|
||||||
|
github_create_issue: { description: "Create an issue" },
|
||||||
|
github_list_issues: { description: "List issues" },
|
||||||
|
})
|
||||||
|
|
||||||
|
const codeModeFlags = [RuntimeFlags.node, RuntimeFlags.layer({ experimentalCodeMode: true })] as const
|
||||||
|
const withCodeMode = testEffect(
|
||||||
|
LayerNode.compile(root, [[Config.node, configLayer], codeModeFlags, [MCP.node, mcpToolsLayer]]),
|
||||||
|
)
|
||||||
|
const withCodeModeNoMcp = testEffect(
|
||||||
|
LayerNode.compile(root, [[Config.node, configLayer], codeModeFlags, [MCP.node, fakeMcpLayer({})]]),
|
||||||
|
)
|
||||||
|
const withFlagOff = testEffect(LayerNode.compile(root, [...replacements, [MCP.node, mcpToolsLayer]]))
|
||||||
|
|
||||||
afterEach(async () => {
|
afterEach(async () => {
|
||||||
await disposeAllInstances()
|
await disposeAllInstances()
|
||||||
})
|
})
|
||||||
@@ -491,3 +532,82 @@ describe("tool.registry", () => {
|
|||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
describe("tool.registry code mode", () => {
|
||||||
|
const model = { providerID: ProviderV2.ID.opencode, modelID: ModelV2.ID.make("test") }
|
||||||
|
|
||||||
|
withCodeMode.instance("registers the code-mode execute tool when the flag is on and MCP tools exist", () =>
|
||||||
|
Effect.gen(function* () {
|
||||||
|
const registry = yield* ToolRegistry.Service
|
||||||
|
const agents = yield* Agent.Service
|
||||||
|
const tools = yield* registry.tools({ ...model, agent: yield* agents.defaultInfo() })
|
||||||
|
const execute = tools.find((tool) => tool.id === "execute")
|
||||||
|
if (!execute) throw new Error("code-mode execute tool was not registered")
|
||||||
|
// The registry appends the grouped catalog to the static base description.
|
||||||
|
expect(execute.description).toContain("confined runtime")
|
||||||
|
expect(execute.description).toContain("## Available tools")
|
||||||
|
expect(execute.description).toContain("tools.github.create_issue(")
|
||||||
|
expect(execute.description).toContain("tools.github.list_issues(")
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
|
withCodeModeNoMcp.instance("does not register code mode when no MCP tools are connected", () =>
|
||||||
|
Effect.gen(function* () {
|
||||||
|
const registry = yield* ToolRegistry.Service
|
||||||
|
const agents = yield* Agent.Service
|
||||||
|
const tools = yield* registry.tools({ ...model, agent: yield* agents.defaultInfo() })
|
||||||
|
expect(tools.map((tool) => tool.id)).not.toContain("execute")
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
|
withFlagOff.instance("does not register code mode when the flag is off, even with MCP tools", () =>
|
||||||
|
Effect.gen(function* () {
|
||||||
|
const registry = yield* ToolRegistry.Service
|
||||||
|
const agents = yield* Agent.Service
|
||||||
|
const tools = yield* registry.tools({ ...model, agent: yield* agents.defaultInfo() })
|
||||||
|
expect(tools.map((tool) => tool.id)).not.toContain("execute")
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
|
withCodeMode.instance("hard-denied MCP tools are dropped from the catalog; ask-level ones stay", () =>
|
||||||
|
Effect.gen(function* () {
|
||||||
|
const registry = yield* ToolRegistry.Service
|
||||||
|
const agents = yield* Agent.Service
|
||||||
|
const base = yield* agents.defaultInfo()
|
||||||
|
const agent = {
|
||||||
|
...base,
|
||||||
|
permission: [
|
||||||
|
...base.permission,
|
||||||
|
{ permission: "github_create_issue", pattern: "*" as const, action: "deny" as const },
|
||||||
|
{ permission: "github_list_issues", pattern: "*" as const, action: "ask" as const },
|
||||||
|
],
|
||||||
|
}
|
||||||
|
const tools = yield* registry.tools({ ...model, agent })
|
||||||
|
const execute = tools.find((tool) => tool.id === "execute")
|
||||||
|
if (!execute) throw new Error("code-mode execute tool was not registered")
|
||||||
|
expect(execute.description).not.toContain("create_issue")
|
||||||
|
expect(execute.description).toContain("tools.github.list_issues(")
|
||||||
|
expect(execute.description).toContain("- github (1 tool)")
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
|
withCodeMode.instance("session-level hard-denied MCP tools are dropped from the code-mode catalog", () =>
|
||||||
|
Effect.gen(function* () {
|
||||||
|
const registry = yield* ToolRegistry.Service
|
||||||
|
const agents = yield* Agent.Service
|
||||||
|
const tools = yield* registry.tools({
|
||||||
|
...model,
|
||||||
|
agent: yield* agents.defaultInfo(),
|
||||||
|
permission: [
|
||||||
|
{ permission: "github_create_issue", pattern: "*" as const, action: "deny" as const },
|
||||||
|
{ permission: "github_list_issues", pattern: "*" as const, action: "ask" as const },
|
||||||
|
],
|
||||||
|
})
|
||||||
|
const execute = tools.find((tool) => tool.id === "execute")
|
||||||
|
if (!execute) throw new Error("code-mode execute tool was not registered")
|
||||||
|
expect(execute.description).not.toContain("create_issue")
|
||||||
|
expect(execute.description).toContain("tools.github.list_issues(")
|
||||||
|
expect(execute.description).toContain("- github (1 tool)")
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|||||||
@@ -34,7 +34,6 @@ export const AISDK = Schema.Struct({
|
|||||||
export interface Native extends Schema.Schema.Type<typeof Native> {}
|
export interface Native extends Schema.Schema.Type<typeof Native> {}
|
||||||
export const Native = Schema.Struct({
|
export const Native = Schema.Struct({
|
||||||
type: Schema.Literal("native"),
|
type: Schema.Literal("native"),
|
||||||
package: Schema.String.pipe(optional),
|
|
||||||
url: Schema.String.pipe(optional),
|
url: Schema.String.pipe(optional),
|
||||||
settings: Schema.Record(Schema.String, Schema.Unknown),
|
settings: Schema.Record(Schema.String, Schema.Unknown),
|
||||||
}).annotate({ identifier: "Provider.Native" })
|
}).annotate({ identifier: "Provider.Native" })
|
||||||
|
|||||||
@@ -4779,7 +4779,6 @@ export type ModelApi =
|
|||||||
| {
|
| {
|
||||||
id: string
|
id: string
|
||||||
type: "native"
|
type: "native"
|
||||||
package?: string
|
|
||||||
url?: string
|
url?: string
|
||||||
settings: {
|
settings: {
|
||||||
[key: string]: unknown
|
[key: string]: unknown
|
||||||
@@ -4854,7 +4853,6 @@ export type ProviderAisdk = {
|
|||||||
|
|
||||||
export type ProviderNative = {
|
export type ProviderNative = {
|
||||||
type: "native"
|
type: "native"
|
||||||
package?: string
|
|
||||||
url?: string
|
url?: string
|
||||||
settings: {
|
settings: {
|
||||||
[key: string]: unknown
|
[key: string]: unknown
|
||||||
|
|||||||
@@ -230,7 +230,6 @@
|
|||||||
line-height: var(--line-height-large);
|
line-height: var(--line-height-large);
|
||||||
letter-spacing: var(--letter-spacing-normal);
|
letter-spacing: var(--letter-spacing-normal);
|
||||||
text-transform: capitalize;
|
text-transform: capitalize;
|
||||||
color: var(--task-agent-legacy-color, var(--text-strong));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[data-slot="basic-tool-tool-subtitle"] {
|
[data-slot="basic-tool-tool-subtitle"] {
|
||||||
@@ -248,57 +247,6 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
body[data-new-layout] {
|
|
||||||
[data-component="task-tool-card"] {
|
|
||||||
padding: 8px 12px 8px 10px;
|
|
||||||
border-radius: 8px;
|
|
||||||
border: 0;
|
|
||||||
border-color: transparent;
|
|
||||||
background: var(--task-agent-background, light-dark(#fafafa, rgba(250, 250, 250, 0.15)));
|
|
||||||
box-shadow: inset 0 0 0 0.5px var(--task-agent-border, var(--v2-border-border-base));
|
|
||||||
|
|
||||||
[data-component="task-tool-spinner"],
|
|
||||||
[data-component="task-tool-title"] {
|
|
||||||
color: var(--task-agent-color, var(--text-strong));
|
|
||||||
}
|
|
||||||
|
|
||||||
[data-component="task-tool-title"] {
|
|
||||||
height: 20px;
|
|
||||||
font-family: var(--v2-font-family-sans, "Inter", sans-serif);
|
|
||||||
font-style: normal;
|
|
||||||
font-weight: 530;
|
|
||||||
font-size: 13px;
|
|
||||||
line-height: 20px;
|
|
||||||
letter-spacing: -0.04px;
|
|
||||||
flex: none;
|
|
||||||
flex-grow: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
[data-slot="basic-tool-tool-subtitle"] {
|
|
||||||
font-family: var(--v2-font-family-sans, "Inter", sans-serif);
|
|
||||||
font-style: normal;
|
|
||||||
font-weight: 440;
|
|
||||||
font-size: 13px;
|
|
||||||
line-height: 20px;
|
|
||||||
letter-spacing: -0.04px;
|
|
||||||
color: light-dark(var(--v2-text-text-base), #fafafa);
|
|
||||||
font-variation-settings: "slnt" 0;
|
|
||||||
flex: none;
|
|
||||||
flex-grow: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
[data-component="session-progress-indicator-v2"] {
|
|
||||||
color: var(--task-agent-color, light-dark(var(--v2-text-text-base), #ffffff));
|
|
||||||
}
|
|
||||||
|
|
||||||
&:hover,
|
|
||||||
&:focus-visible {
|
|
||||||
border-color: transparent;
|
|
||||||
background: var(--task-agent-background, light-dark(#fafafa, rgba(250, 250, 250, 0.15)));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
body:not([data-new-layout]) {
|
body:not([data-new-layout]) {
|
||||||
[data-component="tool-trigger"] {
|
[data-component="tool-trigger"] {
|
||||||
[data-slot="basic-tool-tool-spinner"] {
|
[data-slot="basic-tool-tool-spinner"] {
|
||||||
|
|||||||
@@ -62,7 +62,6 @@ import { animate } from "motion"
|
|||||||
import { useLocation } from "@solidjs/router"
|
import { useLocation } from "@solidjs/router"
|
||||||
import { attached, inline, kind } from "./message-file"
|
import { attached, inline, kind } from "./message-file"
|
||||||
import { readPartText } from "./message-part-text"
|
import { readPartText } from "./message-part-text"
|
||||||
import { SessionProgressIndicatorV2 } from "../v2/components/session-progress-indicator-v2"
|
|
||||||
|
|
||||||
async function writeClipboard(text: string): Promise<boolean> {
|
async function writeClipboard(text: string): Promise<boolean> {
|
||||||
const body = typeof document === "undefined" ? undefined : document.body
|
const body = typeof document === "undefined" ? undefined : document.body
|
||||||
@@ -371,44 +370,6 @@ const agentTones: Record<string, string> = {
|
|||||||
plan: "var(--icon-agent-plan-base)",
|
plan: "var(--icon-agent-plan-base)",
|
||||||
}
|
}
|
||||||
|
|
||||||
const v2AgentTones: Record<string, { color: string; border: string; background: string }> = {
|
|
||||||
build: {
|
|
||||||
color: "var(--v2-agent-build-solid)",
|
|
||||||
border: "var(--v2-agent-build-border)",
|
|
||||||
background: "var(--v2-agent-build-background)",
|
|
||||||
},
|
|
||||||
explore: {
|
|
||||||
color: "var(--v2-agent-explore-solid)",
|
|
||||||
border: "var(--v2-agent-explore-border)",
|
|
||||||
background: "var(--v2-agent-explore-background)",
|
|
||||||
},
|
|
||||||
plan: {
|
|
||||||
color: "var(--v2-agent-plan-solid)",
|
|
||||||
border: "var(--v2-agent-plan-border)",
|
|
||||||
background: "var(--v2-agent-plan-background)",
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
const agentThemeColors: Record<string, string> = {
|
|
||||||
primary: "var(--text-interactive-base)",
|
|
||||||
secondary: "var(--text-base)",
|
|
||||||
accent: "var(--icon-info-base)",
|
|
||||||
success: "var(--icon-success-base)",
|
|
||||||
warning: "var(--icon-warning-base)",
|
|
||||||
error: "var(--icon-critical-base)",
|
|
||||||
info: "var(--icon-info-base)",
|
|
||||||
}
|
|
||||||
|
|
||||||
const v2AgentThemeColors: Record<string, string> = {
|
|
||||||
primary: "var(--v2-text-text-accent)",
|
|
||||||
secondary: "var(--v2-text-text-muted)",
|
|
||||||
accent: "var(--v2-icon-icon-accent)",
|
|
||||||
success: "var(--v2-state-fg-success)",
|
|
||||||
warning: "var(--v2-state-fg-warning)",
|
|
||||||
error: "var(--v2-state-fg-danger)",
|
|
||||||
info: "var(--v2-state-fg-info)",
|
|
||||||
}
|
|
||||||
|
|
||||||
const agentPalette = [
|
const agentPalette = [
|
||||||
"var(--icon-agent-ask-base)",
|
"var(--icon-agent-ask-base)",
|
||||||
"var(--icon-agent-build-base)",
|
"var(--icon-agent-build-base)",
|
||||||
@@ -433,31 +394,16 @@ function tone(name: string) {
|
|||||||
function taskAgent(
|
function taskAgent(
|
||||||
raw: unknown,
|
raw: unknown,
|
||||||
list?: readonly { name: string; color?: string }[],
|
list?: readonly { name: string; color?: string }[],
|
||||||
): { name?: string; color?: string; v2Color?: string; border?: string; background?: string } {
|
): { name?: string; color?: string } {
|
||||||
if (typeof raw !== "string" || !raw) return {}
|
if (typeof raw !== "string" || !raw) return {}
|
||||||
const key = raw.toLowerCase()
|
const key = raw.toLowerCase()
|
||||||
const item = list?.find((entry) => entry.name === raw || entry.name.toLowerCase() === key)
|
const item = list?.find((entry) => entry.name === raw || entry.name.toLowerCase() === key)
|
||||||
const v2Tone = item?.color ? undefined : v2AgentTones[key]
|
|
||||||
const color = agentColor(item?.color, agentThemeColors) ?? agentTones[key] ?? tone(key)
|
|
||||||
const v2Color = agentColor(item?.color, v2AgentThemeColors) ?? v2Tone?.color ?? color
|
|
||||||
return {
|
return {
|
||||||
name: item?.name ?? `${raw[0]!.toUpperCase()}${raw.slice(1)}`,
|
name: item?.name ?? `${raw[0]!.toUpperCase()}${raw.slice(1)}`,
|
||||||
color,
|
color: item?.color ?? agentTones[key] ?? tone(key),
|
||||||
v2Color,
|
|
||||||
border: v2Tone?.border ?? `color-mix(in srgb, ${v2Color} 48%, transparent)`,
|
|
||||||
background: v2Tone?.background ?? `color-mix(in srgb, ${v2Color} 12%, transparent)`,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function agentColor(value: string | undefined, themeColors: Record<string, string>) {
|
|
||||||
if (!value) return
|
|
||||||
return themeColors[value] ?? value
|
|
||||||
}
|
|
||||||
|
|
||||||
function newLayout() {
|
|
||||||
return typeof document !== "undefined" && document.body.hasAttribute("data-new-layout")
|
|
||||||
}
|
|
||||||
|
|
||||||
function webSearchProviderLabel(provider: unknown) {
|
function webSearchProviderLabel(provider: unknown) {
|
||||||
if (provider === "parallel") return "Parallel Web Search"
|
if (provider === "parallel") return "Parallel Web Search"
|
||||||
if (provider === "exa") return "Exa Web Search"
|
if (provider === "exa") return "Exa Web Search"
|
||||||
@@ -1936,9 +1882,6 @@ ToolRegistry.register({
|
|||||||
const agent = createMemo(() => taskAgent(props.input.subagent_type, data.store.agent))
|
const agent = createMemo(() => taskAgent(props.input.subagent_type, data.store.agent))
|
||||||
const title = createMemo(() => agent().name ?? i18n.t("ui.tool.agent.default"))
|
const title = createMemo(() => agent().name ?? i18n.t("ui.tool.agent.default"))
|
||||||
const tone = createMemo(() => agent().color)
|
const tone = createMemo(() => agent().color)
|
||||||
const v2Tone = createMemo(() => agent().v2Color)
|
|
||||||
const border = createMemo(() => agent().border)
|
|
||||||
const background = createMemo(() => agent().background)
|
|
||||||
const subtitle = createMemo(() => {
|
const subtitle = createMemo(() => {
|
||||||
const value =
|
const value =
|
||||||
typeof props.input.description === "string" && props.input.description
|
typeof props.input.description === "string" && props.input.description
|
||||||
@@ -1978,27 +1921,17 @@ ToolRegistry.register({
|
|||||||
}
|
}
|
||||||
|
|
||||||
const trigger = () => (
|
const trigger = () => (
|
||||||
<div
|
<div data-component="task-tool-card">
|
||||||
data-component="task-tool-card"
|
|
||||||
style={{
|
|
||||||
"--task-agent-color": v2Tone(),
|
|
||||||
"--task-agent-legacy-color": tone(),
|
|
||||||
"--task-agent-border": border(),
|
|
||||||
"--task-agent-background": background(),
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<div data-slot="basic-tool-tool-info-structured">
|
<div data-slot="basic-tool-tool-info-structured">
|
||||||
<div data-slot="basic-tool-tool-info-main">
|
<div data-slot="basic-tool-tool-info-main">
|
||||||
<Show when={running()}>
|
<Show when={running()}>
|
||||||
<span data-component="task-tool-spinner" style={{ color: tone() ?? "var(--icon-interactive-base)" }}>
|
<span data-component="task-tool-spinner" style={{ color: tone() ?? "var(--icon-interactive-base)" }}>
|
||||||
<Show when={newLayout()} fallback={<Spinner />}>
|
<Spinner />
|
||||||
<SessionProgressIndicatorV2
|
|
||||||
style={{ color: v2Tone() ?? "light-dark(var(--v2-text-text-base), #ffffff)" }}
|
|
||||||
/>
|
|
||||||
</Show>
|
|
||||||
</span>
|
</span>
|
||||||
</Show>
|
</Show>
|
||||||
<span data-component="task-tool-title">{title()}</span>
|
<span data-component="task-tool-title" style={{ color: tone() ?? "var(--text-strong)" }}>
|
||||||
|
{title()}
|
||||||
|
</span>
|
||||||
<Show when={subtitle()}>
|
<Show when={subtitle()}>
|
||||||
<span data-slot="basic-tool-tool-subtitle">{subtitle()}</span>
|
<span data-slot="basic-tool-tool-subtitle">{subtitle()}</span>
|
||||||
</Show>
|
</Show>
|
||||||
|
|||||||
@@ -198,7 +198,7 @@ const en = {
|
|||||||
"model.knowledge": "Knowledge",
|
"model.knowledge": "Knowledge",
|
||||||
"model.release": "Release",
|
"model.release": "Release",
|
||||||
"model.inputs": "Inputs",
|
"model.inputs": "Inputs",
|
||||||
"model.overviewDescription": "Recent tokens, unique users, and market position.",
|
"model.overviewDescription": "Recent OpenCode Go tokens, unique users, and market position.",
|
||||||
"model.noSummaryTitle": "No usage summary",
|
"model.noSummaryTitle": "No usage summary",
|
||||||
"model.noSummaryDescription": "This model has no OpenCode Go usage rows yet.",
|
"model.noSummaryDescription": "This model has no OpenCode Go usage rows yet.",
|
||||||
"model.tokens": "Tokens",
|
"model.tokens": "Tokens",
|
||||||
|
|||||||
@@ -48,7 +48,6 @@ import {
|
|||||||
const statsUnfurlPath = "banner.png"
|
const statsUnfurlPath = "banner.png"
|
||||||
const geoMapWidth = 960
|
const geoMapWidth = 960
|
||||||
const geoMapHeight = 430
|
const geoMapHeight = 430
|
||||||
const shortMonths = ["JAN", "FEB", "MAR", "APR", "MAY", "JUN", "JUL", "AUG", "SEP", "OCT", "NOV", "DEC"] as const
|
|
||||||
|
|
||||||
type IsoCountryCode = readonly [string, string, string]
|
type IsoCountryCode = readonly [string, string, string]
|
||||||
type WorldCountryProperties = GeoJsonProperties & { name?: string }
|
type WorldCountryProperties = GeoJsonProperties & { name?: string }
|
||||||
@@ -120,7 +119,8 @@ export default function StatsModel() {
|
|||||||
const statsUnfurlUrl = new URL(statsUnfurlPath, localizedUrl("en", "/data/")).toString()
|
const statsUnfurlUrl = new URL(statsUnfurlPath, localizedUrl("en", "/data/")).toString()
|
||||||
const modelHeaderLinks = createMemo<readonly HeaderLink[]>(() => [
|
const modelHeaderLinks = createMemo<readonly HeaderLink[]>(() => [
|
||||||
{ href: "#overview", label: i18n.t("nav.overview") },
|
{ href: "#overview", label: i18n.t("nav.overview") },
|
||||||
{ href: "#momentum", label: i18n.t("model.momentum") },
|
{ href: "#usage", label: i18n.t("nav.usage") },
|
||||||
|
{ href: "#users", label: i18n.t("nav.users") },
|
||||||
{ href: "#efficiency", label: i18n.t("nav.efficiency") },
|
{ href: "#efficiency", label: i18n.t("nav.efficiency") },
|
||||||
{ href: "#geo-breakdown", label: i18n.t("nav.geoBreakdown") },
|
{ href: "#geo-breakdown", label: i18n.t("nav.geoBreakdown") },
|
||||||
{ href: "#peers", label: i18n.t("nav.peers") },
|
{ href: "#peers", label: i18n.t("nav.peers") },
|
||||||
@@ -181,8 +181,9 @@ export default function StatsModel() {
|
|||||||
catalogData={catalog() ?? null}
|
catalogData={catalog() ?? null}
|
||||||
labName={labName()}
|
labName={labName()}
|
||||||
/>
|
/>
|
||||||
<ModelOverview catalog={catalogEntry() ?? null} />
|
<ModelOverview data={stats() ?? null} />
|
||||||
<ModelMomentumSection data={stats() ?? null} />
|
<ModelUsageSection data={stats()?.usage ?? []} />
|
||||||
|
<ModelUsersSection data={stats()?.usage ?? []} />
|
||||||
<ModelEfficiencySection data={stats() ?? null} catalog={catalogEntry() ?? null} />
|
<ModelEfficiencySection data={stats() ?? null} catalog={catalogEntry() ?? null} />
|
||||||
<ModelGeoBreakdownSection data={stats()?.country ?? emptyCountryRecord()} />
|
<ModelGeoBreakdownSection data={stats()?.country ?? emptyCountryRecord()} />
|
||||||
<ModelPeersSection data={stats() ?? null} />
|
<ModelPeersSection data={stats() ?? null} />
|
||||||
@@ -422,59 +423,15 @@ function ChevronDownIcon() {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
function ModelOverview(props: { catalog: ModelCatalogEntry | null }) {
|
function ModelOverview(props: { data: StatsModelData | null }) {
|
||||||
const i18n = useI18n()
|
const i18n = useI18n()
|
||||||
const language = useLanguage()
|
|
||||||
const specs = createMemo(() => [
|
|
||||||
{
|
|
||||||
label: i18n.t("model.context"),
|
|
||||||
value: formatCatalogLimit(props.catalog?.limit?.context, i18n.t("home.unknown")),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: i18n.t("model.output"),
|
|
||||||
value: formatCatalogLimit(props.catalog?.limit?.output, i18n.t("home.unknown")),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: i18n.t("model.knowledge"),
|
|
||||||
value: formatCatalogMonth(props.catalog?.knowledge, language.tag(language.locale()), i18n.t("home.unknown")),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: i18n.t("model.release"),
|
|
||||||
value: formatCatalogMonth(props.catalog?.releaseDate, language.tag(language.locale()), i18n.t("home.unknown")),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: i18n.t("model.inputs"),
|
|
||||||
value: formatCatalogModalities(
|
|
||||||
props.catalog?.modalities.input ?? [],
|
|
||||||
language.tag(language.locale()),
|
|
||||||
i18n.t("home.unknown"),
|
|
||||||
),
|
|
||||||
},
|
|
||||||
])
|
|
||||||
return (
|
return (
|
||||||
<section id="model-overview" data-section="model-specs" aria-label={i18n.t("nav.overview")}>
|
<section id="model-overview" data-section="model-panel">
|
||||||
<For each={specs()}>
|
<SectionTitle
|
||||||
{(spec) => (
|
href="#model-overview"
|
||||||
<div data-component="model-spec">
|
title={i18n.t("nav.overview")}
|
||||||
<span>{spec.label}</span>
|
description={i18n.t("model.overviewDescription")}
|
||||||
<strong>{spec.value}</strong>
|
/>
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</For>
|
|
||||||
</section>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function ModelMomentumSection(props: { data: StatsModelData | null }) {
|
|
||||||
const i18n = useI18n()
|
|
||||||
const language = useLanguage()
|
|
||||||
return (
|
|
||||||
<section id="momentum" data-section="model-momentum">
|
|
||||||
<h2 data-slot="model-momentum-title">
|
|
||||||
<a href="#momentum">{i18n.t("model.momentum")}.</a>
|
|
||||||
<span>{i18n.t("model.overviewDescription")}</span>
|
|
||||||
</h2>
|
|
||||||
<div data-slot="model-momentum-pattern" aria-hidden="true" />
|
|
||||||
<Show
|
<Show
|
||||||
when={props.data}
|
when={props.data}
|
||||||
fallback={
|
fallback={
|
||||||
@@ -482,83 +439,189 @@ function ModelMomentumSection(props: { data: StatsModelData | null }) {
|
|||||||
}
|
}
|
||||||
>
|
>
|
||||||
{(data) => (
|
{(data) => (
|
||||||
<>
|
<div data-component="model-metric-grid">
|
||||||
<MomentumChart data={data()} locale={language.tag(language.locale())} />
|
<MetricCard
|
||||||
<div data-slot="model-momentum-metrics">
|
label={i18n.t("model.tokens")}
|
||||||
<MomentumMetric label={i18n.t("model.uniqueUsers")} value={formatUsers(data().totals.uniqueUsers)} />
|
value={formatTokens(data().totals.tokens)}
|
||||||
<MomentumMetric
|
detail={i18n.t("model.lastTwoMonths")}
|
||||||
label={capitalizeLabel(i18n.t("model.completedSessions"))}
|
|
||||||
value={formatInteger(data().totals.sessions)}
|
|
||||||
/>
|
/>
|
||||||
<MomentumMetric label={i18n.t("model.tokenShare")} value={formatPercent(data().tokenShare)} />
|
<MetricCard
|
||||||
<MomentumMetric
|
label={i18n.t("model.uniqueUsers")}
|
||||||
label="Rank"
|
value={formatUsers(data().totals.uniqueUsers)}
|
||||||
value={formatRankLabel(data().rank)}
|
detail={i18n.t("model.lastTwoMonths")}
|
||||||
watermark={formatRankLabel(data().rank)}
|
/>
|
||||||
|
<MetricCard
|
||||||
|
label={i18n.t("model.sessions")}
|
||||||
|
value={formatInteger(data().totals.sessions)}
|
||||||
|
detail={i18n.t("model.completedSessions")}
|
||||||
|
/>
|
||||||
|
<MetricCard
|
||||||
|
label={i18n.t("model.tokenShare")}
|
||||||
|
value={formatPercent(data().tokenShare)}
|
||||||
|
detail={i18n.t("model.totalModels", { count: data().totalModels })}
|
||||||
|
/>
|
||||||
|
<MetricCard
|
||||||
|
label={i18n.t("model.momentum")}
|
||||||
|
value={formatChange(data().tokenChange)}
|
||||||
|
detail={i18n.t("model.vsPreviousWindow")}
|
||||||
|
state={data().tokenChange < 0 ? "negative" : "positive"}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</>
|
|
||||||
)}
|
)}
|
||||||
</Show>
|
</Show>
|
||||||
</section>
|
</section>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
function MomentumChart(props: { data: StatsModelData; locale: string }) {
|
function ModelUsageSection(props: { data: ModelUsagePoint[] }) {
|
||||||
const chart = createMemo(() => momentumChart(props.data.usage, props.data.updatedAt))
|
const i18n = useI18n()
|
||||||
const changeState = createMemo(() => (props.data.tokenChange < 0 ? "negative" : "positive"))
|
|
||||||
return (
|
return (
|
||||||
<div data-component="model-momentum-chart" role="img" aria-label="Recent model token momentum">
|
<section id="usage" data-section="model-panel">
|
||||||
<div data-slot="model-momentum-summary">
|
<SectionTitle href="#usage" title={i18n.t("nav.usage")} description={i18n.t("model.usageDescription")} />
|
||||||
<div data-slot="model-momentum-total">
|
<Show
|
||||||
<strong>{formatTokens(props.data.totals.tokens)} tokens</strong>
|
when={props.data.some((item) => item.tokens > 0)}
|
||||||
<span data-state={changeState()}>{formatChange(props.data.tokenChange)}</span>
|
fallback={
|
||||||
</div>
|
<ModelEmptyState title={i18n.t("model.noUsageTitle")} description={i18n.t("model.noUsageDescription")} />
|
||||||
<p>
|
}
|
||||||
<span>{formatMomentumDate(chart().startDate, props.locale, props.data.updatedAt)}</span>
|
>
|
||||||
<span aria-hidden="true">→</span>
|
<ModelColumnChart data={props.data} metric="tokens" ariaLabel={i18n.t("model.dailyTokenChart")} />
|
||||||
<span>{formatMomentumDate(chart().endDate, props.locale, props.data.updatedAt)}</span>
|
</Show>
|
||||||
</p>
|
</section>
|
||||||
</div>
|
)
|
||||||
<div data-slot="model-momentum-plot">
|
}
|
||||||
<svg viewBox="0 0 1200 370" preserveAspectRatio="none" aria-hidden="true">
|
|
||||||
<path data-slot="model-momentum-line-muted" d={chart().previousPath} />
|
function ModelUsersSection(props: { data: ModelUsagePoint[] }) {
|
||||||
<path data-slot="model-momentum-line-active" d={chart().currentPath} />
|
const i18n = useI18n()
|
||||||
<For each={chart().markers}>
|
return (
|
||||||
{(marker) => (
|
<section id="users" data-section="model-panel">
|
||||||
<rect
|
<SectionTitle href="#users" title={i18n.t("model.uniqueUsers")} description={i18n.t("model.usersDescription")} />
|
||||||
data-slot="model-momentum-marker"
|
<Show
|
||||||
data-active={marker.active ? "true" : undefined}
|
when={props.data.some((item) => item.users > 0)}
|
||||||
x={marker.x - 3}
|
fallback={
|
||||||
y={marker.y - 3}
|
<ModelEmptyState title={i18n.t("model.noUsersTitle")} description={i18n.t("model.noUsersDescription")} />
|
||||||
width="6"
|
}
|
||||||
height="6"
|
>
|
||||||
/>
|
<ModelColumnChart data={props.data} metric="users" ariaLabel={i18n.t("model.dailyUserChart")} />
|
||||||
)}
|
</Show>
|
||||||
</For>
|
</section>
|
||||||
</svg>
|
)
|
||||||
<span data-slot="model-momentum-end" data-state={changeState()} style={chart().endStyle}>
|
}
|
||||||
<i />
|
|
||||||
{formatChange(props.data.tokenChange)}
|
function ModelColumnChart(props: { data: ModelUsagePoint[]; metric: "tokens" | "users"; ariaLabel: string }) {
|
||||||
|
const i18n = useI18n()
|
||||||
|
const [activeIndex, setActiveIndex] = createSignal<number>()
|
||||||
|
const max = createMemo(() => Math.max(0, ...props.data.map((item) => modelUsageMetricValue(item, props.metric))) || 1)
|
||||||
|
const activePoint = createMemo(() => {
|
||||||
|
const index = activeIndex()
|
||||||
|
if (index === undefined) return undefined
|
||||||
|
return props.data[index]
|
||||||
|
})
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
data-component="model-usage-chart"
|
||||||
|
data-metric={props.metric}
|
||||||
|
data-dense-labels={isModelUsageDense(props.data.length) ? "true" : undefined}
|
||||||
|
role="img"
|
||||||
|
aria-label={props.ariaLabel}
|
||||||
|
style={{ "--model-usage-count": props.data.length } as JSX.CSSProperties}
|
||||||
|
onPointerLeave={(event) => {
|
||||||
|
if (event.pointerType === "touch") return
|
||||||
|
setActiveIndex(undefined)
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div data-slot="model-usage-axis" aria-hidden="true">
|
||||||
|
<For each={props.data}>
|
||||||
|
{(point, index) => (
|
||||||
|
<div
|
||||||
|
data-active={activeIndex() === index() ? "true" : undefined}
|
||||||
|
data-label-hidden={isModelUsageLabelHidden(index(), props.data.length) ? "true" : undefined}
|
||||||
|
>
|
||||||
|
<span data-slot="model-usage-label">
|
||||||
|
<span data-slot="model-usage-total">{formatModelUsageValue(point, props.metric)}</span>
|
||||||
|
<span data-slot="model-usage-date">{point.date}</span>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div data-slot="model-momentum-months" aria-hidden="true">
|
)}
|
||||||
<For each={momentumMonthLabels(chart().startDate, props.locale, props.data.updatedAt)}>
|
</For>
|
||||||
{(month) => <span style={{ left: `${month.x}%` }}>{month.label}</span>}
|
</div>
|
||||||
|
<div data-slot="model-usage-bars">
|
||||||
|
<For each={props.data}>
|
||||||
|
{(point, index) => (
|
||||||
|
<div
|
||||||
|
data-slot="model-usage-column"
|
||||||
|
role="button"
|
||||||
|
tabIndex={0}
|
||||||
|
aria-label={`${point.date} ${formatModelUsageValue(point, props.metric)} ${modelUsageLabel(props.metric, i18n)}`}
|
||||||
|
data-active={activeIndex() === index() ? "true" : undefined}
|
||||||
|
data-muted={activeIndex() !== undefined && activeIndex() !== index() ? "true" : undefined}
|
||||||
|
onPointerDown={(event) => {
|
||||||
|
if (event.pointerType !== "touch") return
|
||||||
|
setActiveIndex(index())
|
||||||
|
}}
|
||||||
|
onPointerEnter={() => setActiveIndex(index())}
|
||||||
|
onPointerMove={(event) => {
|
||||||
|
if (event.pointerType === "touch") return
|
||||||
|
setActiveIndex(index())
|
||||||
|
}}
|
||||||
|
onClick={() => setActiveIndex(index())}
|
||||||
|
onFocus={() => setActiveIndex(index())}
|
||||||
|
onBlur={() => setActiveIndex(undefined)}
|
||||||
|
onKeyDown={(event) => {
|
||||||
|
if (event.key !== "Enter" && event.key !== " ") return
|
||||||
|
event.preventDefault()
|
||||||
|
setActiveIndex(index())
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
data-slot="model-usage-bar"
|
||||||
|
style={
|
||||||
|
{
|
||||||
|
"--model-usage-fill": `${modelUsageHeight(modelUsageMetricValue(point, props.metric), max())}%`,
|
||||||
|
} as JSX.CSSProperties
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<Show when={activeIndex() === index() && activePoint()}>
|
||||||
|
{(active) => (
|
||||||
|
<div
|
||||||
|
data-component="chart-tooltip"
|
||||||
|
data-placement={index() > props.data.length * 0.62 ? "left" : "right"}
|
||||||
|
>
|
||||||
|
<strong>{active().date}</strong>
|
||||||
|
<span>
|
||||||
|
{formatModelUsageValue(active(), props.metric)} {modelUsageLabel(props.metric, i18n)}
|
||||||
|
</span>
|
||||||
|
<div data-slot="tooltip-divider" />
|
||||||
|
<p>
|
||||||
|
<span data-slot="tooltip-label">
|
||||||
|
<i /> {i18n.t("chart.daily")} {modelUsageLabel(props.metric, i18n)}
|
||||||
|
</span>
|
||||||
|
<b>{formatModelUsageValue(active(), props.metric)}</b>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</Show>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</For>
|
</For>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
function MomentumMetric(props: { label: string; value: string; watermark?: string }) {
|
function modelUsageMetricValue(point: ModelUsagePoint, metric: "tokens" | "users") {
|
||||||
return (
|
if (metric === "users") return point.users
|
||||||
<div data-component="model-momentum-metric">
|
return point.tokens
|
||||||
<Show when={props.watermark}>{(watermark) => <em aria-hidden="true">{watermark()}</em>}</Show>
|
}
|
||||||
<span>{props.label}</span>
|
|
||||||
<strong>{props.value}</strong>
|
function formatModelUsageValue(point: ModelUsagePoint, metric: "tokens" | "users") {
|
||||||
</div>
|
if (metric === "users") return formatUsers(point.users)
|
||||||
)
|
return formatTokens(point.tokens)
|
||||||
|
}
|
||||||
|
|
||||||
|
function modelUsageLabel(metric: "tokens" | "users", i18n: ReturnType<typeof useI18n>) {
|
||||||
|
if (metric === "users") return i18n.t("format.users")
|
||||||
|
return i18n.t("format.tokens")
|
||||||
}
|
}
|
||||||
|
|
||||||
function ModelEfficiencySection(props: { data: StatsModelData | null; catalog: ModelCatalogEntry | null }) {
|
function ModelEfficiencySection(props: { data: StatsModelData | null; catalog: ModelCatalogEntry | null }) {
|
||||||
@@ -916,111 +979,19 @@ function formatGeoShare(value: number) {
|
|||||||
return `${value.toFixed(value > 0 && value < 1 ? 1 : 0)}%`
|
return `${value.toFixed(value > 0 && value < 1 ? 1 : 0)}%`
|
||||||
}
|
}
|
||||||
|
|
||||||
function momentumChart(data: ModelUsagePoint[], updatedAt: string | null) {
|
function modelUsageHeight(tokens: number, max: number) {
|
||||||
const fallbackDate = updatedAt ? formatMomentumDateLabel(updatedAt) : "JAN 1"
|
if (tokens <= 0) return 0
|
||||||
const points =
|
return Math.max(2, Math.min(100, (tokens / max) * 100))
|
||||||
data.length > 1 ? data : [data[0] ?? emptyUsagePoint(fallbackDate), data[0] ?? emptyUsagePoint(fallbackDate)]
|
|
||||||
const max = Math.max(1, ...points.map((point) => point.tokens))
|
|
||||||
const split = Math.max(1, Math.floor((points.length - 1) / 2))
|
|
||||||
const coordinates = points.map((point, index) => ({
|
|
||||||
date: point.date,
|
|
||||||
tokens: point.tokens,
|
|
||||||
x: (index / Math.max(1, points.length - 1)) * 1200,
|
|
||||||
y: 364 - (point.tokens / max) * 364,
|
|
||||||
}))
|
|
||||||
const end = coordinates[coordinates.length - 1]
|
|
||||||
return {
|
|
||||||
startDate: coordinates[0].date,
|
|
||||||
endDate: end.date,
|
|
||||||
previousPath: smoothLinePath(coordinates.slice(0, split + 1)),
|
|
||||||
currentPath: smoothLinePath(coordinates.slice(split)),
|
|
||||||
markers: [
|
|
||||||
{ ...coordinates[0], active: false },
|
|
||||||
{ ...coordinates[split], active: true },
|
|
||||||
{ ...end, active: true },
|
|
||||||
],
|
|
||||||
endStyle: {
|
|
||||||
"--momentum-end-x": `${Math.min(94, Math.max(0, ((end.x + 8) / 1200) * 100))}%`,
|
|
||||||
"--momentum-end-y": `${Math.min(96, Math.max(0, ((end.y - 7) / 370) * 100))}%`,
|
|
||||||
} as JSX.CSSProperties,
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function emptyUsagePoint(date: string): ModelUsagePoint {
|
function isModelUsageDense(count: number) {
|
||||||
return { date, tokens: 0, users: 0, sessions: 0, cost: 0 }
|
return count > 20
|
||||||
}
|
}
|
||||||
|
|
||||||
function smoothLinePath(points: { x: number; y: number }[]) {
|
function isModelUsageLabelHidden(index: number, count: number) {
|
||||||
if (points.length === 0) return ""
|
if (count <= 16) return false
|
||||||
if (points.length === 1) return `M${formatSparklinePoint(points[0].x)} ${formatSparklinePoint(points[0].y)}`
|
const interval = Math.ceil(count / 8)
|
||||||
return points
|
return index !== count - 1 && index % interval !== 0
|
||||||
.map((point, index) => {
|
|
||||||
if (index === 0) return `M${formatSparklinePoint(point.x)} ${formatSparklinePoint(point.y)}`
|
|
||||||
const previous = points[index - 1]
|
|
||||||
const next = points[index + 1] ?? point
|
|
||||||
const beforePrevious = points[index - 2] ?? previous
|
|
||||||
const controlStart = {
|
|
||||||
x: previous.x + (point.x - beforePrevious.x) / 6,
|
|
||||||
y: previous.y + (point.y - beforePrevious.y) / 6,
|
|
||||||
}
|
|
||||||
const controlEnd = {
|
|
||||||
x: point.x - (next.x - previous.x) / 6,
|
|
||||||
y: point.y - (next.y - previous.y) / 6,
|
|
||||||
}
|
|
||||||
return `C${formatSparklinePoint(controlStart.x)} ${formatSparklinePoint(controlStart.y)} ${formatSparklinePoint(controlEnd.x)} ${formatSparklinePoint(controlEnd.y)} ${formatSparklinePoint(point.x)} ${formatSparklinePoint(point.y)}`
|
|
||||||
})
|
|
||||||
.join(" ")
|
|
||||||
}
|
|
||||||
|
|
||||||
function momentumMonthLabels(startDate: string, locale: string, updatedAt: string | null) {
|
|
||||||
const start = parseMomentumDate(startDate, updatedAt)
|
|
||||||
const first = new Date(Date.UTC(start.getUTCFullYear(), start.getUTCMonth() - 1, 1))
|
|
||||||
return Array.from({ length: 5 }, (_, index) => {
|
|
||||||
const date = new Date(Date.UTC(first.getUTCFullYear(), first.getUTCMonth() + index, 1))
|
|
||||||
return {
|
|
||||||
label: new Intl.DateTimeFormat(locale, { month: "short", timeZone: "UTC" }).format(date).toUpperCase(),
|
|
||||||
x: index === 4 ? 98 : index * 24.5,
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
function formatMomentumDate(date: string, locale: string, updatedAt: string | null) {
|
|
||||||
return new Intl.DateTimeFormat(locale, {
|
|
||||||
month: "short",
|
|
||||||
day: "numeric",
|
|
||||||
year: "numeric",
|
|
||||||
timeZone: "UTC",
|
|
||||||
})
|
|
||||||
.format(parseMomentumDate(date, updatedAt))
|
|
||||||
.toUpperCase()
|
|
||||||
}
|
|
||||||
|
|
||||||
function parseMomentumDate(date: string, updatedAt: string | null): Date {
|
|
||||||
const iso = /^(\d{4})-(\d{2})-(\d{2})/.exec(date)
|
|
||||||
if (iso) return new Date(Date.UTC(Number(iso[1]), Number(iso[2]) - 1, Number(iso[3])))
|
|
||||||
|
|
||||||
const label = /^([A-Za-z]{3})\s+(\d{1,2})$/.exec(date.trim())
|
|
||||||
const month = label ? shortMonths.findIndex((item) => item.toLowerCase() === label[1].toLowerCase()) : -1
|
|
||||||
if (!label || month < 0) return new Date(Date.UTC(1970, 0, 1))
|
|
||||||
|
|
||||||
const anchor = updatedAt ? parseMomentumDate(updatedAt, null) : new Date(Date.UTC(1970, 0, 1))
|
|
||||||
const year = month > anchor.getUTCMonth() + 1 ? anchor.getUTCFullYear() - 1 : anchor.getUTCFullYear()
|
|
||||||
return new Date(Date.UTC(year, month, Number(label[2])))
|
|
||||||
}
|
|
||||||
|
|
||||||
function formatMomentumDateLabel(date: string) {
|
|
||||||
const parsed = parseMomentumDate(date, null)
|
|
||||||
if (parsed.getUTCFullYear() === 1970) return "JAN 1"
|
|
||||||
return `${shortMonths[parsed.getUTCMonth()]} ${parsed.getUTCDate()}`
|
|
||||||
}
|
|
||||||
|
|
||||||
function formatRankLabel(rank: number | null) {
|
|
||||||
if (rank === null) return "--"
|
|
||||||
return `#${String(rank).padStart(2, "0")}`
|
|
||||||
}
|
|
||||||
|
|
||||||
function capitalizeLabel(value: string) {
|
|
||||||
return value.charAt(0).toUpperCase() + value.slice(1)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function formatRankMove(change: number) {
|
function formatRankMove(change: number) {
|
||||||
@@ -1079,28 +1050,6 @@ function formatTokens(value: number) {
|
|||||||
return String(Math.round(value))
|
return String(Math.round(value))
|
||||||
}
|
}
|
||||||
|
|
||||||
function formatCatalogLimit(value: number | undefined, unknown: string) {
|
|
||||||
return value === undefined ? unknown : formatTokens(value)
|
|
||||||
}
|
|
||||||
|
|
||||||
function formatCatalogMonth(value: string | undefined, locale: string, unknown: string) {
|
|
||||||
if (!value) return unknown
|
|
||||||
const match = /^(\d{4})(?:-(\d{2}))?(?:-(\d{2}))?$/.exec(value)
|
|
||||||
if (!match) return value
|
|
||||||
return new Intl.DateTimeFormat(locale, {
|
|
||||||
month: match[2] ? "short" : undefined,
|
|
||||||
year: "numeric",
|
|
||||||
timeZone: "UTC",
|
|
||||||
}).format(new Date(Date.UTC(Number(match[1]), match[2] ? Number(match[2]) - 1 : 0, 1)))
|
|
||||||
}
|
|
||||||
|
|
||||||
function formatCatalogModalities(values: string[], locale: string, unknown: string) {
|
|
||||||
if (values.length === 0) return unknown
|
|
||||||
const labels = values.map((value) => value.replace(/[-_]/g, " ").replace(/\b\w/g, (letter) => letter.toUpperCase()))
|
|
||||||
if (labels.length === 1) return labels[0] ?? unknown
|
|
||||||
return new Intl.ListFormat(locale, { style: "long", type: "conjunction" }).format(labels)
|
|
||||||
}
|
|
||||||
|
|
||||||
function formatInteger(value: number) {
|
function formatInteger(value: number) {
|
||||||
return new Intl.NumberFormat("en").format(value)
|
return new Intl.NumberFormat("en").format(value)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2129,40 +2129,27 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
[data-page="stats"] [data-component="market-share-list"] li {
|
[data-page="stats"] [data-component="market-share-list"] li {
|
||||||
min-width: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
[data-page="stats"] [data-component="market-share-list"] li > a,
|
|
||||||
[data-page="stats"] [data-component="market-share-list"] li > button {
|
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 8px;
|
gap: 8px;
|
||||||
width: 100%;
|
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
height: 28px;
|
height: 28px;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
padding: 0 8px;
|
padding: 0 8px;
|
||||||
border: 1px solid var(--stats-line);
|
|
||||||
background: var(--stats-layer);
|
background: var(--stats-layer);
|
||||||
color: inherit;
|
border: 1px solid var(--stats-line);
|
||||||
font: inherit;
|
|
||||||
font-size: 11px;
|
font-size: 11px;
|
||||||
line-height: 1;
|
line-height: 1;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
outline: none;
|
outline: none;
|
||||||
text-decoration: none;
|
|
||||||
transition:
|
transition:
|
||||||
border-color 120ms ease,
|
border-color 120ms ease,
|
||||||
box-shadow 120ms ease,
|
box-shadow 120ms ease,
|
||||||
background 120ms ease;
|
background 120ms ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
[data-page="stats"] [data-component="market-share-list"] li > button {
|
[data-page="stats"] [data-component="market-share-list"] li[data-active="true"],
|
||||||
appearance: none;
|
[data-page="stats"] [data-component="market-share-list"] li:focus-visible {
|
||||||
}
|
|
||||||
|
|
||||||
[data-page="stats"] [data-component="market-share-list"] li[data-active="true"] > :is(a, button),
|
|
||||||
[data-page="stats"] [data-component="market-share-list"] li > :is(a, button):focus-visible {
|
|
||||||
border-color: var(--stats-text);
|
border-color: var(--stats-text);
|
||||||
background: var(--stats-layer);
|
background: var(--stats-layer);
|
||||||
box-shadow:
|
box-shadow:
|
||||||
@@ -3348,320 +3335,6 @@
|
|||||||
height: 24px;
|
height: 24px;
|
||||||
}
|
}
|
||||||
|
|
||||||
[data-page="stats"] [data-section="model-specs"] {
|
|
||||||
position: relative;
|
|
||||||
box-sizing: border-box;
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: repeat(5, minmax(0, 1fr));
|
|
||||||
min-height: 144px;
|
|
||||||
color: var(--stats-text);
|
|
||||||
box-shadow:
|
|
||||||
inset 0 1px var(--stats-line),
|
|
||||||
inset 0 -1px var(--stats-line),
|
|
||||||
inset 1px 0 var(--stats-line),
|
|
||||||
inset -1px 0 var(--stats-line);
|
|
||||||
}
|
|
||||||
|
|
||||||
[data-page="stats"] [data-component="model-spec"] {
|
|
||||||
display: grid;
|
|
||||||
align-content: start;
|
|
||||||
gap: 8px;
|
|
||||||
min-width: 0;
|
|
||||||
min-height: 144px;
|
|
||||||
box-sizing: border-box;
|
|
||||||
padding: 40px;
|
|
||||||
border-left: 1px solid var(--stats-line);
|
|
||||||
}
|
|
||||||
|
|
||||||
[data-page="stats"] [data-component="model-spec"]:first-child {
|
|
||||||
border-left: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
[data-page="stats"] [data-component="model-spec"] span {
|
|
||||||
color: var(--stats-muted);
|
|
||||||
font-size: 13px;
|
|
||||||
font-weight: 500;
|
|
||||||
line-height: 1.1;
|
|
||||||
}
|
|
||||||
|
|
||||||
[data-page="stats"] [data-component="model-spec"] strong {
|
|
||||||
min-width: 0;
|
|
||||||
overflow: hidden;
|
|
||||||
color: var(--stats-text);
|
|
||||||
font-size: 28px;
|
|
||||||
font-weight: 500;
|
|
||||||
line-height: 1.5;
|
|
||||||
letter-spacing: 0;
|
|
||||||
text-overflow: ellipsis;
|
|
||||||
white-space: nowrap;
|
|
||||||
}
|
|
||||||
|
|
||||||
[data-page="stats"] [data-section="model-momentum"] {
|
|
||||||
position: relative;
|
|
||||||
box-sizing: border-box;
|
|
||||||
display: grid;
|
|
||||||
gap: 40px;
|
|
||||||
min-height: 938px;
|
|
||||||
padding: 80px 40px;
|
|
||||||
color: var(--stats-text);
|
|
||||||
box-shadow:
|
|
||||||
inset 1px 0 var(--stats-line),
|
|
||||||
inset -1px 0 var(--stats-line);
|
|
||||||
}
|
|
||||||
|
|
||||||
[data-page="stats"] [data-slot="model-momentum-title"] {
|
|
||||||
min-width: 0;
|
|
||||||
height: 42px;
|
|
||||||
margin: 0;
|
|
||||||
overflow: hidden;
|
|
||||||
color: var(--stats-muted);
|
|
||||||
font-size: 28px;
|
|
||||||
font-weight: 400;
|
|
||||||
line-height: 42px;
|
|
||||||
letter-spacing: 0;
|
|
||||||
text-overflow: ellipsis;
|
|
||||||
white-space: nowrap;
|
|
||||||
}
|
|
||||||
|
|
||||||
[data-page="stats"] [data-slot="model-momentum-title"] a {
|
|
||||||
color: var(--stats-text);
|
|
||||||
font-weight: 700;
|
|
||||||
text-decoration: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
[data-page="stats"] [data-slot="model-momentum-title"] a:hover,
|
|
||||||
[data-page="stats"] [data-slot="model-momentum-title"] a:focus-visible {
|
|
||||||
color: var(--stats-text);
|
|
||||||
outline: none;
|
|
||||||
text-decoration: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
[data-page="stats"] [data-slot="model-momentum-title"] span {
|
|
||||||
margin-left: 12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
[data-page="stats"] [data-slot="model-momentum-pattern"] {
|
|
||||||
width: 100%;
|
|
||||||
height: 16px;
|
|
||||||
overflow: hidden;
|
|
||||||
background: var(--stats-line);
|
|
||||||
mask-image: 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");
|
|
||||||
mask-position: center top;
|
|
||||||
mask-repeat: repeat;
|
|
||||||
mask-size: 6px 6px;
|
|
||||||
-webkit-mask-image: 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");
|
|
||||||
-webkit-mask-position: center top;
|
|
||||||
-webkit-mask-repeat: repeat;
|
|
||||||
-webkit-mask-size: 6px 6px;
|
|
||||||
}
|
|
||||||
|
|
||||||
[data-page="stats"] [data-component="model-momentum-chart"] {
|
|
||||||
position: relative;
|
|
||||||
display: grid;
|
|
||||||
grid-template-rows: 64px 370px 14px;
|
|
||||||
gap: 16px;
|
|
||||||
height: 480px;
|
|
||||||
min-width: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
[data-page="stats"] [data-slot="model-momentum-summary"] {
|
|
||||||
display: grid;
|
|
||||||
align-content: start;
|
|
||||||
gap: 8px;
|
|
||||||
width: fit-content;
|
|
||||||
min-width: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
[data-page="stats"] [data-slot="model-momentum-total"] {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 16px;
|
|
||||||
min-width: 0;
|
|
||||||
height: 42px;
|
|
||||||
}
|
|
||||||
|
|
||||||
[data-page="stats"] [data-slot="model-momentum-total"] strong {
|
|
||||||
min-width: 0;
|
|
||||||
overflow: hidden;
|
|
||||||
color: var(--stats-text);
|
|
||||||
font-size: 28px;
|
|
||||||
font-weight: 700;
|
|
||||||
line-height: 42px;
|
|
||||||
letter-spacing: 0;
|
|
||||||
text-overflow: ellipsis;
|
|
||||||
white-space: nowrap;
|
|
||||||
}
|
|
||||||
|
|
||||||
[data-page="stats"] [data-slot="model-momentum-total"] span,
|
|
||||||
[data-page="stats"] [data-slot="model-momentum-end"] {
|
|
||||||
display: inline-flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
height: 24px;
|
|
||||||
box-sizing: border-box;
|
|
||||||
padding: 0 8px;
|
|
||||||
background: #198b4324;
|
|
||||||
color: #198b43;
|
|
||||||
font-size: 13px;
|
|
||||||
font-weight: 700;
|
|
||||||
line-height: 14px;
|
|
||||||
white-space: nowrap;
|
|
||||||
}
|
|
||||||
|
|
||||||
[data-page="stats"] [data-slot="model-momentum-total"] span[data-state="negative"],
|
|
||||||
[data-page="stats"] [data-slot="model-momentum-end"][data-state="negative"] {
|
|
||||||
background: #b82d3524;
|
|
||||||
color: #b82d35;
|
|
||||||
}
|
|
||||||
|
|
||||||
[data-page="stats"] [data-slot="model-momentum-summary"] p {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 8px;
|
|
||||||
min-width: 0;
|
|
||||||
margin: 0;
|
|
||||||
color: var(--stats-muted);
|
|
||||||
font-size: 13px;
|
|
||||||
font-weight: 500;
|
|
||||||
line-height: 14px;
|
|
||||||
white-space: nowrap;
|
|
||||||
}
|
|
||||||
|
|
||||||
[data-page="stats"] [data-slot="model-momentum-plot"] {
|
|
||||||
position: relative;
|
|
||||||
min-width: 0;
|
|
||||||
height: 370px;
|
|
||||||
}
|
|
||||||
|
|
||||||
[data-page="stats"] [data-slot="model-momentum-plot"] svg {
|
|
||||||
display: block;
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
overflow: visible;
|
|
||||||
}
|
|
||||||
|
|
||||||
[data-page="stats"] [data-slot="model-momentum-line-muted"],
|
|
||||||
[data-page="stats"] [data-slot="model-momentum-line-active"] {
|
|
||||||
fill: none;
|
|
||||||
stroke-width: 1.5;
|
|
||||||
vector-effect: non-scaling-stroke;
|
|
||||||
}
|
|
||||||
|
|
||||||
[data-page="stats"] [data-slot="model-momentum-line-muted"] {
|
|
||||||
stroke: var(--stats-bar-idle);
|
|
||||||
}
|
|
||||||
|
|
||||||
[data-page="stats"] [data-slot="model-momentum-line-active"] {
|
|
||||||
stroke: #198b43;
|
|
||||||
}
|
|
||||||
|
|
||||||
[data-page="stats"] [data-slot="model-momentum-marker"] {
|
|
||||||
fill: var(--stats-bar-idle);
|
|
||||||
}
|
|
||||||
|
|
||||||
[data-page="stats"] [data-slot="model-momentum-marker"][data-active="true"] {
|
|
||||||
fill: #198b43;
|
|
||||||
}
|
|
||||||
|
|
||||||
[data-page="stats"] [data-slot="model-momentum-end"] {
|
|
||||||
position: absolute;
|
|
||||||
top: var(--momentum-end-y);
|
|
||||||
left: var(--momentum-end-x);
|
|
||||||
gap: 8px;
|
|
||||||
height: 14px;
|
|
||||||
padding: 0;
|
|
||||||
background: transparent;
|
|
||||||
transform: translateY(-50%);
|
|
||||||
}
|
|
||||||
|
|
||||||
[data-page="stats"] [data-slot="model-momentum-end"] i {
|
|
||||||
display: block;
|
|
||||||
width: 6px;
|
|
||||||
height: 6px;
|
|
||||||
background: currentColor;
|
|
||||||
}
|
|
||||||
|
|
||||||
[data-page="stats"] [data-slot="model-momentum-months"] {
|
|
||||||
position: relative;
|
|
||||||
height: 14px;
|
|
||||||
color: var(--stats-muted);
|
|
||||||
font-size: 13px;
|
|
||||||
font-weight: 500;
|
|
||||||
line-height: 14px;
|
|
||||||
}
|
|
||||||
|
|
||||||
[data-page="stats"] [data-slot="model-momentum-months"] span {
|
|
||||||
position: absolute;
|
|
||||||
top: 0;
|
|
||||||
transform: translateX(-50%);
|
|
||||||
}
|
|
||||||
|
|
||||||
[data-page="stats"] [data-slot="model-momentum-months"] span:first-child {
|
|
||||||
transform: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
[data-page="stats"] [data-slot="model-momentum-months"] span:last-child {
|
|
||||||
transform: translateX(-100%);
|
|
||||||
}
|
|
||||||
|
|
||||||
[data-page="stats"] [data-slot="model-momentum-metrics"] {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: repeat(4, minmax(0, 1fr));
|
|
||||||
gap: 12px;
|
|
||||||
min-width: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
[data-page="stats"] [data-component="model-momentum-metric"] {
|
|
||||||
position: relative;
|
|
||||||
display: grid;
|
|
||||||
align-content: space-between;
|
|
||||||
min-width: 0;
|
|
||||||
min-height: 120px;
|
|
||||||
box-sizing: border-box;
|
|
||||||
padding: 16px;
|
|
||||||
overflow: hidden;
|
|
||||||
border: 1px solid var(--stats-line);
|
|
||||||
background:
|
|
||||||
linear-gradient(180deg, color-mix(in srgb, var(--stats-bg) 76%, transparent), transparent), var(--stats-layer);
|
|
||||||
}
|
|
||||||
|
|
||||||
[data-page="stats"] [data-component="model-momentum-metric"] span {
|
|
||||||
position: relative;
|
|
||||||
z-index: 1;
|
|
||||||
color: var(--stats-muted);
|
|
||||||
font-size: 13px;
|
|
||||||
font-weight: 500;
|
|
||||||
line-height: 14px;
|
|
||||||
}
|
|
||||||
|
|
||||||
[data-page="stats"] [data-component="model-momentum-metric"] strong {
|
|
||||||
position: relative;
|
|
||||||
z-index: 1;
|
|
||||||
min-width: 0;
|
|
||||||
overflow: hidden;
|
|
||||||
color: var(--stats-text);
|
|
||||||
font-size: 28px;
|
|
||||||
font-weight: 500;
|
|
||||||
line-height: 42px;
|
|
||||||
letter-spacing: 0;
|
|
||||||
text-overflow: ellipsis;
|
|
||||||
white-space: nowrap;
|
|
||||||
}
|
|
||||||
|
|
||||||
[data-page="stats"] [data-component="model-momentum-metric"] em {
|
|
||||||
position: absolute;
|
|
||||||
top: -90px;
|
|
||||||
left: -69px;
|
|
||||||
color: var(--stats-text);
|
|
||||||
font-size: 200px;
|
|
||||||
font-style: normal;
|
|
||||||
font-weight: 700;
|
|
||||||
line-height: 300px;
|
|
||||||
opacity: 0.04;
|
|
||||||
pointer-events: none;
|
|
||||||
white-space: nowrap;
|
|
||||||
}
|
|
||||||
|
|
||||||
[data-page="stats"] [data-section="lab-overview"] {
|
[data-page="stats"] [data-section="lab-overview"] {
|
||||||
position: relative;
|
position: relative;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
@@ -5293,7 +4966,6 @@
|
|||||||
[data-page="stats"] [data-section="session-cost"],
|
[data-page="stats"] [data-section="session-cost"],
|
||||||
[data-page="stats"] [data-section="model-hero"],
|
[data-page="stats"] [data-section="model-hero"],
|
||||||
[data-page="stats"] [data-section="lab-hero"],
|
[data-page="stats"] [data-section="lab-hero"],
|
||||||
[data-page="stats"] [data-section="model-momentum"],
|
|
||||||
[data-page="stats"] [data-section="model-panel"] {
|
[data-page="stats"] [data-section="model-panel"] {
|
||||||
padding: 64px 32px;
|
padding: 64px 32px;
|
||||||
}
|
}
|
||||||
@@ -5384,48 +5056,6 @@
|
|||||||
min-height: 0;
|
min-height: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
[data-page="stats"] [data-section="model-specs"] {
|
|
||||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
|
||||||
}
|
|
||||||
|
|
||||||
[data-page="stats"] [data-component="model-spec"] {
|
|
||||||
min-height: 132px;
|
|
||||||
padding: 32px;
|
|
||||||
}
|
|
||||||
|
|
||||||
[data-page="stats"] [data-component="model-spec"]:nth-child(3n + 1) {
|
|
||||||
border-left: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
[data-page="stats"] [data-component="model-spec"]:nth-child(n + 4) {
|
|
||||||
border-top: 1px solid var(--stats-line);
|
|
||||||
}
|
|
||||||
|
|
||||||
[data-page="stats"] [data-section="model-momentum"] {
|
|
||||||
min-height: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
[data-page="stats"] [data-slot="model-momentum-title"] {
|
|
||||||
height: auto;
|
|
||||||
overflow: visible;
|
|
||||||
line-height: 1.5;
|
|
||||||
text-overflow: clip;
|
|
||||||
white-space: normal;
|
|
||||||
}
|
|
||||||
|
|
||||||
[data-page="stats"] [data-component="model-momentum-chart"] {
|
|
||||||
grid-template-rows: auto 320px 14px;
|
|
||||||
height: 434px;
|
|
||||||
}
|
|
||||||
|
|
||||||
[data-page="stats"] [data-slot="model-momentum-plot"] {
|
|
||||||
height: 320px;
|
|
||||||
}
|
|
||||||
|
|
||||||
[data-page="stats"] [data-slot="model-momentum-metrics"] {
|
|
||||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
|
||||||
}
|
|
||||||
|
|
||||||
[data-page="stats"] [data-component="model-metric-grid"],
|
[data-page="stats"] [data-component="model-metric-grid"],
|
||||||
[data-page="stats"] [data-component="model-metric-grid"][data-variant="dense"],
|
[data-page="stats"] [data-component="model-metric-grid"][data-variant="dense"],
|
||||||
[data-page="stats"] [data-component="lab-model-grid"] {
|
[data-page="stats"] [data-component="lab-model-grid"] {
|
||||||
@@ -5554,7 +5184,6 @@
|
|||||||
[data-page="stats"] [data-section="session-cost"],
|
[data-page="stats"] [data-section="session-cost"],
|
||||||
[data-page="stats"] [data-section="model-hero"],
|
[data-page="stats"] [data-section="model-hero"],
|
||||||
[data-page="stats"] [data-section="lab-hero"],
|
[data-page="stats"] [data-section="lab-hero"],
|
||||||
[data-page="stats"] [data-section="model-momentum"],
|
|
||||||
[data-page="stats"] [data-section="model-panel"] {
|
[data-page="stats"] [data-section="model-panel"] {
|
||||||
padding: 48px 24px;
|
padding: 48px 24px;
|
||||||
}
|
}
|
||||||
@@ -5722,90 +5351,6 @@
|
|||||||
line-height: 22px;
|
line-height: 22px;
|
||||||
}
|
}
|
||||||
|
|
||||||
[data-page="stats"] [data-section="model-specs"] {
|
|
||||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
|
||||||
}
|
|
||||||
|
|
||||||
[data-page="stats"] [data-component="model-spec"] {
|
|
||||||
min-height: 116px;
|
|
||||||
padding: 24px;
|
|
||||||
}
|
|
||||||
|
|
||||||
[data-page="stats"] [data-component="model-spec"]:nth-child(n) {
|
|
||||||
border-left: 1px solid var(--stats-line);
|
|
||||||
border-top: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
[data-page="stats"] [data-component="model-spec"]:nth-child(2n + 1) {
|
|
||||||
border-left: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
[data-page="stats"] [data-component="model-spec"]:nth-child(n + 3) {
|
|
||||||
border-top: 1px solid var(--stats-line);
|
|
||||||
}
|
|
||||||
|
|
||||||
[data-page="stats"] [data-component="model-spec"]:last-child:nth-child(odd) {
|
|
||||||
grid-column: 1 / -1;
|
|
||||||
}
|
|
||||||
|
|
||||||
[data-page="stats"] [data-component="model-spec"] strong {
|
|
||||||
font-size: 24px;
|
|
||||||
line-height: 1.35;
|
|
||||||
}
|
|
||||||
|
|
||||||
[data-page="stats"] [data-section="model-momentum"] {
|
|
||||||
gap: 28px;
|
|
||||||
}
|
|
||||||
|
|
||||||
[data-page="stats"] [data-slot="model-momentum-title"] {
|
|
||||||
font-size: 20px;
|
|
||||||
line-height: 30px;
|
|
||||||
}
|
|
||||||
|
|
||||||
[data-page="stats"] [data-slot="model-momentum-title"] span {
|
|
||||||
margin-left: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
[data-page="stats"] [data-component="model-momentum-chart"] {
|
|
||||||
grid-template-rows: auto 260px 14px;
|
|
||||||
gap: 14px;
|
|
||||||
height: 380px;
|
|
||||||
}
|
|
||||||
|
|
||||||
[data-page="stats"] [data-slot="model-momentum-total"] {
|
|
||||||
align-items: flex-start;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 6px;
|
|
||||||
height: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
[data-page="stats"] [data-slot="model-momentum-total"] strong {
|
|
||||||
font-size: 24px;
|
|
||||||
line-height: 32px;
|
|
||||||
}
|
|
||||||
|
|
||||||
[data-page="stats"] [data-slot="model-momentum-summary"] p {
|
|
||||||
flex-wrap: wrap;
|
|
||||||
white-space: normal;
|
|
||||||
}
|
|
||||||
|
|
||||||
[data-page="stats"] [data-slot="model-momentum-plot"] {
|
|
||||||
height: 260px;
|
|
||||||
}
|
|
||||||
|
|
||||||
[data-page="stats"] [data-slot="model-momentum-end"] {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
[data-page="stats"] [data-component="model-momentum-metric"] {
|
|
||||||
min-height: 112px;
|
|
||||||
}
|
|
||||||
|
|
||||||
[data-page="stats"] [data-component="model-momentum-metric"] strong {
|
|
||||||
font-size: clamp(20px, 5.5vw, 24px);
|
|
||||||
line-height: 34px;
|
|
||||||
}
|
|
||||||
|
|
||||||
[data-page="stats"] [data-slot="model-catalog-grid"] {
|
[data-page="stats"] [data-slot="model-catalog-grid"] {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||||
|
|||||||
@@ -1262,56 +1262,30 @@ function MarketShareList(props: {
|
|||||||
onActiveAuthorChange: (author: string) => void
|
onActiveAuthorChange: (author: string) => void
|
||||||
}) {
|
}) {
|
||||||
const i18n = useI18n()
|
const i18n = useI18n()
|
||||||
const language = useLanguage()
|
|
||||||
return (
|
return (
|
||||||
<ol data-component="market-share-list">
|
<ol data-component="market-share-list">
|
||||||
<For each={props.data}>
|
<For each={props.data}>
|
||||||
{(item, index) => {
|
{(item, index) => (
|
||||||
const label = () =>
|
<li
|
||||||
`${item.author} ${formatTrillions(item.tokens)} ${item.share.toFixed(1)} ${i18n.t("chart.percent")}`
|
role="button"
|
||||||
const content = () => (
|
tabIndex={0}
|
||||||
<>
|
aria-label={`${item.author} ${formatTrillions(item.tokens)} ${item.share.toFixed(1)} ${i18n.t("chart.percent")}`}
|
||||||
|
data-active={props.activeAuthor === item.author ? "true" : undefined}
|
||||||
|
onPointerEnter={() => props.onActiveAuthorChange(item.author)}
|
||||||
|
onFocus={() => props.onActiveAuthorChange(item.author)}
|
||||||
|
onKeyDown={(event) => {
|
||||||
|
if (event.key !== "Enter" && event.key !== " ") return
|
||||||
|
event.preventDefault()
|
||||||
|
props.onActiveAuthorChange(item.author)
|
||||||
|
}}
|
||||||
|
>
|
||||||
<span>{String(index() + 1).padStart(2, "0")}</span>
|
<span>{String(index() + 1).padStart(2, "0")}</span>
|
||||||
<i style={{ background: getRankColor(item.author, index(), props.authorOrder, marketColors) }} />
|
<i style={{ background: getRankColor(item.author, index(), props.authorOrder, marketColors) }} />
|
||||||
<strong>{item.author}</strong>
|
<strong>{item.author}</strong>
|
||||||
<em>{formatTrillions(item.tokens)}</em>
|
<em>{formatTrillions(item.tokens)}</em>
|
||||||
<b>{item.share.toFixed(1)}%</b>
|
<b>{item.share.toFixed(1)}%</b>
|
||||||
</>
|
|
||||||
)
|
|
||||||
const href = () =>
|
|
||||||
item.author === "Other" ? undefined : language.route(`${import.meta.env.BASE_URL}${modelSlug(item.author)}`)
|
|
||||||
return (
|
|
||||||
<li
|
|
||||||
data-active={props.activeAuthor === item.author ? "true" : undefined}
|
|
||||||
onPointerEnter={() => props.onActiveAuthorChange(item.author)}
|
|
||||||
>
|
|
||||||
<Show
|
|
||||||
when={href()}
|
|
||||||
fallback={
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
aria-label={label()}
|
|
||||||
onClick={() => props.onActiveAuthorChange(item.author)}
|
|
||||||
onFocus={() => props.onActiveAuthorChange(item.author)}
|
|
||||||
>
|
|
||||||
{content()}
|
|
||||||
</button>
|
|
||||||
}
|
|
||||||
>
|
|
||||||
{(href) => (
|
|
||||||
<a
|
|
||||||
href={href()}
|
|
||||||
aria-label={label()}
|
|
||||||
onClick={() => props.onActiveAuthorChange(item.author)}
|
|
||||||
onFocus={() => props.onActiveAuthorChange(item.author)}
|
|
||||||
>
|
|
||||||
{content()}
|
|
||||||
</a>
|
|
||||||
)}
|
|
||||||
</Show>
|
|
||||||
</li>
|
</li>
|
||||||
)
|
)}
|
||||||
}}
|
|
||||||
</For>
|
</For>
|
||||||
</ol>
|
</ol>
|
||||||
)
|
)
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user