mirror of
https://github.com/anomalyco/opencode.git
synced 2026-08-26 11:36:14 +00:00
Compare commits
23
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1216c55094 | ||
|
|
ec25388937 | ||
|
|
c7134cbb01 | ||
|
|
ba4d0ea8bf | ||
|
|
1cc53890dc | ||
|
|
ae2ea3c723 | ||
|
|
3f31551fad | ||
|
|
13c27598d3 | ||
|
|
2564a4f172 | ||
|
|
fd9bd448a2 | ||
|
|
b72b50006b | ||
|
|
ac1c048e64 | ||
|
|
8615731d46 | ||
|
|
a7444bf944 | ||
|
|
69aaa22793 | ||
|
|
322e2b9dd5 | ||
|
|
afd6f3b064 | ||
|
|
1e86be2bc5 | ||
|
|
a57230b80b | ||
|
|
6bb1a76e58 | ||
|
|
bdcb6be649 | ||
|
|
2f36ffe35d | ||
|
|
31c409a865 |
@@ -29,7 +29,7 @@
|
||||
},
|
||||
"packages/app": {
|
||||
"name": "@opencode-ai/app",
|
||||
"version": "1.18.22",
|
||||
"version": "1.18.23",
|
||||
"dependencies": {
|
||||
"@corvu/drawer": "catalog:",
|
||||
"@dnd-kit/abstract": "0.5.0",
|
||||
@@ -96,7 +96,7 @@
|
||||
},
|
||||
"packages/cli": {
|
||||
"name": "@opencode-ai/cli",
|
||||
"version": "1.18.22",
|
||||
"version": "1.18.23",
|
||||
"bin": {
|
||||
"lildax": "./bin/lildax.cjs",
|
||||
},
|
||||
@@ -144,7 +144,7 @@
|
||||
},
|
||||
"packages/codemode": {
|
||||
"name": "@opencode-ai/codemode",
|
||||
"version": "1.18.22",
|
||||
"version": "1.18.23",
|
||||
"dependencies": {
|
||||
"acorn": "8.15.0",
|
||||
"effect": "catalog:",
|
||||
@@ -158,7 +158,7 @@
|
||||
},
|
||||
"packages/console/app": {
|
||||
"name": "@opencode-ai/console-app",
|
||||
"version": "1.18.22",
|
||||
"version": "1.18.23",
|
||||
"dependencies": {
|
||||
"@cloudflare/vite-plugin": "1.15.2",
|
||||
"@ibm/plex": "6.4.1",
|
||||
@@ -194,7 +194,7 @@
|
||||
},
|
||||
"packages/console/core": {
|
||||
"name": "@opencode-ai/console-core",
|
||||
"version": "1.18.22",
|
||||
"version": "1.18.23",
|
||||
"dependencies": {
|
||||
"@aws-sdk/client-sts": "3.782.0",
|
||||
"@jsx-email/render": "1.1.1",
|
||||
@@ -221,7 +221,7 @@
|
||||
},
|
||||
"packages/console/function": {
|
||||
"name": "@opencode-ai/console-function",
|
||||
"version": "1.18.22",
|
||||
"version": "1.18.23",
|
||||
"dependencies": {
|
||||
"@ai-sdk/anthropic": "3.0.82",
|
||||
"@ai-sdk/openai": "3.0.48",
|
||||
@@ -235,6 +235,7 @@
|
||||
"devDependencies": {
|
||||
"@cloudflare/workers-types": "catalog:",
|
||||
"@tsconfig/node22": "22.0.2",
|
||||
"@types/bun": "catalog:",
|
||||
"@types/node": "catalog:",
|
||||
"@typescript/native-preview": "catalog:",
|
||||
"openai": "5.11.0",
|
||||
@@ -243,7 +244,7 @@
|
||||
},
|
||||
"packages/console/mail": {
|
||||
"name": "@opencode-ai/console-mail",
|
||||
"version": "1.18.22",
|
||||
"version": "1.18.23",
|
||||
"dependencies": {
|
||||
"@jsx-email/all": "2.2.3",
|
||||
"@jsx-email/cli": "1.4.3",
|
||||
@@ -267,7 +268,7 @@
|
||||
},
|
||||
"packages/console/support": {
|
||||
"name": "@opencode-ai/console-support",
|
||||
"version": "1.18.22",
|
||||
"version": "1.18.23",
|
||||
"dependencies": {
|
||||
"@cloudflare/vite-plugin": "1.15.2",
|
||||
"@opencode-ai/console-core": "workspace:*",
|
||||
@@ -287,7 +288,7 @@
|
||||
},
|
||||
"packages/core": {
|
||||
"name": "@opencode-ai/core",
|
||||
"version": "1.18.22",
|
||||
"version": "1.18.23",
|
||||
"bin": {
|
||||
"opencode": "./bin/opencode",
|
||||
},
|
||||
@@ -381,7 +382,7 @@
|
||||
},
|
||||
"packages/desktop": {
|
||||
"name": "@opencode-ai/desktop",
|
||||
"version": "1.18.22",
|
||||
"version": "1.18.23",
|
||||
"dependencies": {
|
||||
"@zip.js/zip.js": "2.7.62",
|
||||
"drizzle-orm": "catalog:",
|
||||
@@ -435,7 +436,7 @@
|
||||
},
|
||||
"packages/effect-drizzle-sqlite": {
|
||||
"name": "@opencode-ai/effect-drizzle-sqlite",
|
||||
"version": "1.18.22",
|
||||
"version": "1.18.23",
|
||||
"dependencies": {
|
||||
"drizzle-orm": "catalog:",
|
||||
"effect": "catalog:",
|
||||
@@ -449,7 +450,7 @@
|
||||
},
|
||||
"packages/effect-sqlite-node": {
|
||||
"name": "@opencode-ai/effect-sqlite-node",
|
||||
"version": "1.18.22",
|
||||
"version": "1.18.23",
|
||||
"dependencies": {
|
||||
"effect": "catalog:",
|
||||
},
|
||||
@@ -461,7 +462,7 @@
|
||||
},
|
||||
"packages/enterprise": {
|
||||
"name": "@opencode-ai/enterprise",
|
||||
"version": "1.18.22",
|
||||
"version": "1.18.23",
|
||||
"dependencies": {
|
||||
"@hono/standard-validator": "catalog:",
|
||||
"@opencode-ai/core": "workspace:*",
|
||||
@@ -493,7 +494,7 @@
|
||||
},
|
||||
"packages/function": {
|
||||
"name": "@opencode-ai/function",
|
||||
"version": "1.18.22",
|
||||
"version": "1.18.23",
|
||||
"dependencies": {
|
||||
"@octokit/auth-app": "8.0.1",
|
||||
"@octokit/rest": "catalog:",
|
||||
@@ -509,7 +510,7 @@
|
||||
},
|
||||
"packages/http-recorder": {
|
||||
"name": "@opencode-ai/http-recorder",
|
||||
"version": "1.18.22",
|
||||
"version": "1.18.23",
|
||||
"dependencies": {
|
||||
"@effect/platform-node": "4.0.0-beta.83",
|
||||
"@effect/platform-node-shared": "4.0.0-beta.83",
|
||||
@@ -540,7 +541,7 @@
|
||||
},
|
||||
"packages/llm": {
|
||||
"name": "@opencode-ai/llm",
|
||||
"version": "1.18.22",
|
||||
"version": "1.18.23",
|
||||
"dependencies": {
|
||||
"@opencode-ai/schema": "workspace:*",
|
||||
"@smithy/eventstream-codec": "4.2.14",
|
||||
@@ -559,7 +560,7 @@
|
||||
},
|
||||
"packages/opencode": {
|
||||
"name": "opencode",
|
||||
"version": "1.18.22",
|
||||
"version": "1.18.23",
|
||||
"bin": {
|
||||
"opencode": "./bin/opencode",
|
||||
},
|
||||
@@ -690,7 +691,7 @@
|
||||
},
|
||||
"packages/plugin": {
|
||||
"name": "@opencode-ai/plugin",
|
||||
"version": "1.18.22",
|
||||
"version": "1.18.23",
|
||||
"dependencies": {
|
||||
"@ai-sdk/provider": "3.0.8",
|
||||
"@opencode-ai/sdk": "workspace:*",
|
||||
@@ -766,7 +767,7 @@
|
||||
},
|
||||
"packages/sdk/js": {
|
||||
"name": "@opencode-ai/sdk",
|
||||
"version": "1.18.22",
|
||||
"version": "1.18.23",
|
||||
"dependencies": {
|
||||
"cross-spawn": "catalog:",
|
||||
},
|
||||
@@ -781,7 +782,7 @@
|
||||
},
|
||||
"packages/server": {
|
||||
"name": "@opencode-ai/server",
|
||||
"version": "1.18.22",
|
||||
"version": "1.18.23",
|
||||
"dependencies": {
|
||||
"@opencode-ai/core": "workspace:*",
|
||||
"@opencode-ai/protocol": "workspace:*",
|
||||
@@ -796,7 +797,7 @@
|
||||
},
|
||||
"packages/session-ui": {
|
||||
"name": "@opencode-ai/session-ui",
|
||||
"version": "1.18.22",
|
||||
"version": "1.18.23",
|
||||
"dependencies": {
|
||||
"@kobalte/core": "catalog:",
|
||||
"@opencode-ai/client": "file:../app/vendor/opencode-ai-client-1.17.13-v2.tgz",
|
||||
@@ -836,7 +837,7 @@
|
||||
},
|
||||
"packages/slack": {
|
||||
"name": "@opencode-ai/slack",
|
||||
"version": "1.18.22",
|
||||
"version": "1.18.23",
|
||||
"dependencies": {
|
||||
"@opencode-ai/sdk": "workspace:*",
|
||||
"@slack/bolt": "^3.17.1",
|
||||
@@ -849,7 +850,7 @@
|
||||
},
|
||||
"packages/stats/app": {
|
||||
"name": "@opencode-ai/stats-app",
|
||||
"version": "1.18.22",
|
||||
"version": "1.18.23",
|
||||
"dependencies": {
|
||||
"@ibm/plex": "6.4.1",
|
||||
"@kobalte/core": "catalog:",
|
||||
@@ -858,32 +859,25 @@
|
||||
"@solidjs/meta": "catalog:",
|
||||
"@solidjs/router": "catalog:",
|
||||
"@solidjs/start": "catalog:",
|
||||
"d3-geo": "3.1.1",
|
||||
"d3-scale": "4.0.2",
|
||||
"effect": "catalog:",
|
||||
"i18n-iso-countries": "7.14.0",
|
||||
"nitro": "3.0.1-alpha.1",
|
||||
"solid-js": "catalog:",
|
||||
"sst": "catalog:",
|
||||
"topojson-client": "3.1.0",
|
||||
"vite": "catalog:",
|
||||
"world-atlas": "2.0.2",
|
||||
},
|
||||
"devDependencies": {
|
||||
"@cloudflare/workers-types": "catalog:",
|
||||
"@types/bun": "catalog:",
|
||||
"@types/d3-geo": "3.1.0",
|
||||
"@types/d3-scale": "4.0.9",
|
||||
"@types/geojson": "7946.0.16",
|
||||
"@types/topojson-client": "3.1.5",
|
||||
"@types/topojson-specification": "1.0.5",
|
||||
"@typescript/native-preview": "catalog:",
|
||||
"typescript": "catalog:",
|
||||
},
|
||||
},
|
||||
"packages/stats/core": {
|
||||
"name": "@opencode-ai/stats-core",
|
||||
"version": "1.18.22",
|
||||
"version": "1.18.23",
|
||||
"dependencies": {
|
||||
"@aws-sdk/client-athena": "3.933.0",
|
||||
"@planetscale/database": "1.19.0",
|
||||
@@ -902,7 +896,7 @@
|
||||
},
|
||||
"packages/stats/server": {
|
||||
"name": "@opencode-ai/stats-server",
|
||||
"version": "1.18.22",
|
||||
"version": "1.18.23",
|
||||
"dependencies": {
|
||||
"@aws-sdk/client-firehose": "3.933.0",
|
||||
"@effect/platform-node": "catalog:",
|
||||
@@ -944,7 +938,7 @@
|
||||
},
|
||||
"packages/tui": {
|
||||
"name": "@opencode-ai/tui",
|
||||
"version": "1.18.22",
|
||||
"version": "1.18.23",
|
||||
"dependencies": {
|
||||
"@opencode-ai/core": "workspace:*",
|
||||
"@opencode-ai/plugin": "workspace:*",
|
||||
@@ -971,7 +965,7 @@
|
||||
},
|
||||
"packages/ui": {
|
||||
"name": "@opencode-ai/ui",
|
||||
"version": "1.18.22",
|
||||
"version": "1.18.23",
|
||||
"dependencies": {
|
||||
"@kobalte/core": "catalog:",
|
||||
"@pierre/diffs": "catalog:",
|
||||
@@ -1022,7 +1016,7 @@
|
||||
},
|
||||
"packages/web": {
|
||||
"name": "@opencode-ai/web",
|
||||
"version": "1.18.22",
|
||||
"version": "1.18.23",
|
||||
"dependencies": {
|
||||
"@astrojs/cloudflare": "12.6.3",
|
||||
"@astrojs/markdown-remark": "6.3.1",
|
||||
@@ -2843,8 +2837,6 @@
|
||||
|
||||
"@types/cross-spawn": ["@types/cross-spawn@6.0.6", "", { "dependencies": { "@types/node": "*" } }, "sha512-fXRhhUkG4H3TQk5dBhQ7m/JDdSNHKwR2BBia62lhwEIq9xGiQKLxd6LymNhn47SjXhsUEPmxi+PKw2OkW4LLjA=="],
|
||||
|
||||
"@types/d3-geo": ["@types/d3-geo@3.1.0", "", { "dependencies": { "@types/geojson": "*" } }, "sha512-856sckF0oP/diXtS4jNsiQw/UuK5fQG8l/a9VVLeSouf1/PPbBE1i1W852zVwKwYCBkFJJB7nCFTbk6UMEXBOQ=="],
|
||||
|
||||
"@types/d3-scale": ["@types/d3-scale@4.0.9", "", { "dependencies": { "@types/d3-time": "*" } }, "sha512-dLmtwB8zkAeO/juAMfnV+sItKjlsw2lKdZVVy6LRr0cBmegxSABiLEpGVmSJJ8O08i4+sGR6qQtb6WtuwJdvVw=="],
|
||||
|
||||
"@types/d3-time": ["@types/d3-time@3.0.4", "", {}, "sha512-yuzZug1nkAAaBlBBikKZTgzCeA+k1uy4ZFwWANOfKw5z5LRhV0gNA7gNkKm7HoK+HRN0wX3EkxGk0fpbWhmB7g=="],
|
||||
@@ -2865,8 +2857,6 @@
|
||||
|
||||
"@types/fs-extra": ["@types/fs-extra@9.0.13", "", { "dependencies": { "@types/node": "*" } }, "sha512-nEnwB++1u5lVDM2UI4c1+5R+FYaKfaAzS4OococimjVm3nQw3TuzH5UNsocrcTBbhnerblyHj4A49qXbIiZdpA=="],
|
||||
|
||||
"@types/geojson": ["@types/geojson@7946.0.16", "", {}, "sha512-6C8nqWur3j98U6+lXDfTUWIfgvZU+EumvpHKcYjujKH7woYyLj2sUmff0tRhrqM7BohUw7Pz3ZB1jj2gW9Fvmg=="],
|
||||
|
||||
"@types/hast": ["@types/hast@3.0.4", "", { "dependencies": { "@types/unist": "*" } }, "sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ=="],
|
||||
|
||||
"@types/http-cache-semantics": ["@types/http-cache-semantics@4.2.0", "", {}, "sha512-L3LgimLHXtGkWikKnsPg0/VFx9OGZaC+eN1u4r+OB1XRqH3meBIAVC2zr1WdMH+RHmnRkqliQAOHNJ/E0j/e0Q=="],
|
||||
@@ -2945,10 +2935,6 @@
|
||||
|
||||
"@types/ssri": ["@types/ssri@7.1.5", "", { "dependencies": { "@types/node": "*" } }, "sha512-odD/56S3B51liILSk5aXJlnYt99S6Rt9EFDDqGtJM26rKHApHcwyU/UoYHrzKkdkHMAIquGWCuHtQTbes+FRQw=="],
|
||||
|
||||
"@types/topojson-client": ["@types/topojson-client@3.1.5", "", { "dependencies": { "@types/geojson": "*", "@types/topojson-specification": "*" } }, "sha512-C79rySTyPxnQNNguTZNI1Ct4D7IXgvyAs3p9HPecnl6mNrJ5+UhvGNYcZfpROYV2lMHI48kJPxwR+F9C6c7nmw=="],
|
||||
|
||||
"@types/topojson-specification": ["@types/topojson-specification@1.0.5", "", { "dependencies": { "@types/geojson": "*" } }, "sha512-C7KvcQh+C2nr6Y2Ub4YfgvWvWCgP2nOQMtfhlnwsRL4pYmmwzBS7HclGiS87eQfDOU/DLQpX6GEscviaz4yLIQ=="],
|
||||
|
||||
"@types/trusted-types": ["@types/trusted-types@2.0.7", "", {}, "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw=="],
|
||||
|
||||
"@types/tsscmp": ["@types/tsscmp@1.0.2", "", {}, "sha512-cy7BRSU8GYYgxjcx0Py+8lo5MthuDhlyu076KUcYzVNXL23luYgRHkMG2fIFEc6neckeh/ntP82mw+U4QjZq+g=="],
|
||||
@@ -3437,8 +3423,6 @@
|
||||
|
||||
"d3-format": ["d3-format@3.1.2", "", {}, "sha512-AJDdYOdnyRDV5b6ArilzCPPwc1ejkHcoyFarqlPqT7zRYjhavcT3uSrqcMvsgh2CgoPbK3RCwyHaVyxYcP2Arg=="],
|
||||
|
||||
"d3-geo": ["d3-geo@3.1.1", "", { "dependencies": { "d3-array": "2.5.0 - 3" } }, "sha512-637ln3gXKXOwhalDzinUgY83KzNWZRKbYubaG+fGVuc/dxO64RRljtCTnf5ecMyE1RIdtqpkVcq0IbtU2S8j2Q=="],
|
||||
|
||||
"d3-interpolate": ["d3-interpolate@3.0.1", "", { "dependencies": { "d3-color": "1 - 3" } }, "sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g=="],
|
||||
|
||||
"d3-scale": ["d3-scale@4.0.2", "", { "dependencies": { "d3-array": "2.10.0 - 3", "d3-format": "1 - 3", "d3-interpolate": "1.2.0 - 3", "d3-time": "2.1.1 - 3", "d3-time-format": "2 - 4" } }, "sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ=="],
|
||||
@@ -5317,8 +5301,6 @@
|
||||
|
||||
"toolbeam-docs-theme": ["toolbeam-docs-theme@0.4.8", "", { "peerDependencies": { "@astrojs/starlight": "^0.34.3", "astro": "^5.7.13" } }, "sha512-b+5ynEFp4Woe5a22hzNQm42lD23t13ZMihVxHbzjA50zdcM9aOSJTIjdJ0PDSd4/50HbBXcpHiQsz6rM4N88ww=="],
|
||||
|
||||
"topojson-client": ["topojson-client@3.1.0", "", { "dependencies": { "commander": "2" }, "bin": { "topo2geo": "bin/topo2geo", "topomerge": "bin/topomerge", "topoquantize": "bin/topoquantize" } }, "sha512-605uxS6bcYxGXw9qi62XyrV6Q3xwbndjachmNxu8HWTtVPxZfEJN9fd/SZS1Q54Sn2y0TMyMxFj/cJINqGHrKw=="],
|
||||
|
||||
"tr46": ["tr46@0.0.3", "", {}, "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw=="],
|
||||
|
||||
"traverse": ["traverse@0.3.9", "", {}, "sha512-iawgk0hLP3SxGKDfnDJf8wTz4p2qImnyihM5Hh/sGvQ3K37dPi/w8sRhdNIxYA1TwFwc5mDhIJq+O0RsvXBKdQ=="],
|
||||
@@ -5561,8 +5543,6 @@
|
||||
|
||||
"workerd": ["workerd@1.20251118.0", "", { "optionalDependencies": { "@cloudflare/workerd-darwin-64": "1.20251118.0", "@cloudflare/workerd-darwin-arm64": "1.20251118.0", "@cloudflare/workerd-linux-64": "1.20251118.0", "@cloudflare/workerd-linux-arm64": "1.20251118.0", "@cloudflare/workerd-windows-64": "1.20251118.0" }, "bin": { "workerd": "bin/workerd" } }, "sha512-Om5ns0Lyx/LKtYI04IV0bjIrkBgoFNg0p6urzr2asekJlfP18RqFzyqMFZKf0i9Gnjtz/JfAS/Ol6tjCe5JJsQ=="],
|
||||
|
||||
"world-atlas": ["world-atlas@2.0.2", "", {}, "sha512-IXfV0qwlKXpckz1FhwXVwKRjiIhOnWttOskm5CtxMsjgE/MXAYRHWJqgXOpM8IkcPBoXnyTU5lFHcYa5ChG0LQ=="],
|
||||
|
||||
"wrangler": ["wrangler@4.50.0", "", { "dependencies": { "@cloudflare/kv-asset-handler": "0.4.0", "@cloudflare/unenv-preset": "2.7.11", "blake3-wasm": "2.1.5", "esbuild": "0.25.4", "miniflare": "4.20251118.1", "path-to-regexp": "6.3.0", "unenv": "2.0.0-rc.24", "workerd": "1.20251118.0" }, "optionalDependencies": { "fsevents": "~2.3.2" }, "peerDependencies": { "@cloudflare/workers-types": "^4.20251118.0" }, "optionalPeers": ["@cloudflare/workers-types"], "bin": { "wrangler": "bin/wrangler.js", "wrangler2": "bin/wrangler.js" } }, "sha512-+nuZuHZxDdKmAyXOSrHlciGshCoAPiy5dM+t6mEohWm7HpXvTHmWQGUf/na9jjWlWJHCJYOWzkA1P5HBJqrIEA=="],
|
||||
|
||||
"wrap-ansi": ["wrap-ansi@9.0.2", "", { "dependencies": { "ansi-styles": "^6.2.1", "string-width": "^7.0.0", "strip-ansi": "^7.1.0" } }, "sha512-42AtmgqjV+X1VpdOfyTGOYRi0/zsoLqtXQckTmqTeybT+BDIbM/Guxo7x3pE2vtpr1ok6xRqM9OpBe+Jyoqyww=="],
|
||||
@@ -6515,8 +6495,6 @@
|
||||
|
||||
"tiny-async-pool/semver": ["semver@5.7.2", "", { "bin": { "semver": "bin/semver" } }, "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g=="],
|
||||
|
||||
"topojson-client/commander": ["commander@2.20.3", "", {}, "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ=="],
|
||||
|
||||
"tree-sitter-bash/node-addon-api": ["node-addon-api@8.8.0", "", {}, "sha512-c5Ko1fZJIJmzhFIkhRN76WTq+fC6tWnGy9CXA0fA+XygsWZmEwG8vmbkNqxMyoaa0Tin4djul49NzdVcJJcjeA=="],
|
||||
|
||||
"tw-to-css/postcss": ["postcss@8.4.31", "", { "dependencies": { "nanoid": "^3.3.6", "picocolors": "^1.0.0", "source-map-js": "^1.0.2" } }, "sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ=="],
|
||||
|
||||
+4
-4
@@ -1,8 +1,8 @@
|
||||
{
|
||||
"nodeModules": {
|
||||
"x86_64-linux": "sha256-Be1I6OG6UitofhcGu2BeNzevmoQXc4Or5r/NPzwtft4=",
|
||||
"aarch64-linux": "sha256-O+d+26CQIjZ08Rn8Qm3IytdDqIMbPdhzaGOZeUKAvIU=",
|
||||
"aarch64-darwin": "sha256-ObS50y/oy6fM9wSGUL/wx6O0+fTWHC04mXJNd7w/2Z0=",
|
||||
"x86_64-darwin": "sha256-eoR7ZSyH62Fq2ZaW2b2QqU2FC97rYxTMeEe+djT0nto="
|
||||
"x86_64-linux": "sha256-fJ72uEK9rSoFL6eJk0Lwkc2TIMLZyQ7Iz83WrZE2duA=",
|
||||
"aarch64-linux": "sha256-ElEwz5spFa8XFYSBiGjKlTKRFQCju/ZYDlb6h1FaKoI=",
|
||||
"aarch64-darwin": "sha256-RmbrAlggOqxNFdhW+qj2tjRCpRf2NDLe68TikbGtCeA=",
|
||||
"x86_64-darwin": "sha256-ZgYE0J+Dkz/kALK3kZ1jdFIZ5/BEkEaw0mXCqPon0iY="
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@opencode-ai/app",
|
||||
"version": "1.18.22",
|
||||
"version": "1.18.23",
|
||||
"description": "",
|
||||
"type": "module",
|
||||
"exports": {
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
import { describe, expect, test } from "bun:test"
|
||||
import type { SessionV2Info } from "@opencode-ai/sdk/v2/client"
|
||||
import { QueryClient } from "@tanstack/solid-query"
|
||||
import type { Session, SessionV2Info } from "@opencode-ai/sdk/v2/client"
|
||||
import {
|
||||
applyHomeSessionEvent,
|
||||
appendHomeSessionEvent,
|
||||
createHomeSessionIndexCache,
|
||||
HOME_V2_SESSION_PAGE_LIMIT,
|
||||
loadHomeSessionIndex,
|
||||
homeSessionIndexSessions,
|
||||
@@ -151,4 +153,33 @@ describe("Home V2 session index", () => {
|
||||
expect(homeSessionIndexRefresh("global.disposed", true).refetch).toBe(true)
|
||||
expect(homeSessionIndexRefresh("session.next.moved", true).refetch).toBe(true)
|
||||
})
|
||||
|
||||
test("removes a session from the loaded Home index", () => {
|
||||
const queryClient = new QueryClient()
|
||||
const cache = createHomeSessionIndexCache(queryClient, "server")
|
||||
const sessions = [
|
||||
{ id: "a", time: { created: 1, updated: 1 } },
|
||||
{ id: "b", time: { created: 1, updated: 1 } },
|
||||
] as Session[]
|
||||
queryClient.setQueryData(cache.indexKey, { sessions, eventSequence: 0 })
|
||||
|
||||
cache.remove("a")
|
||||
|
||||
const index = queryClient.getQueryData<{ sessions: Session[] }>(cache.indexKey)
|
||||
expect(index?.sessions.map((item) => item.id)).toEqual(["b"])
|
||||
})
|
||||
|
||||
test("keeps the session out of the Home list when the index is not mounted", () => {
|
||||
const queryClient = new QueryClient()
|
||||
const cache = createHomeSessionIndexCache(queryClient, "server")
|
||||
const sessions = [
|
||||
{ id: "a", time: { created: 1, updated: 1 } },
|
||||
{ id: "b", time: { created: 1, updated: 1 } },
|
||||
] as Session[]
|
||||
|
||||
cache.remove("a")
|
||||
|
||||
expect(queryClient.getQueryData(cache.indexKey)).toBeUndefined()
|
||||
expect(cache.sessions({ sessions, eventSequence: 0 }, undefined).map((item) => item.id)).toEqual(["b"])
|
||||
})
|
||||
})
|
||||
|
||||
@@ -85,6 +85,7 @@ export function createHomeSessionIndexCache(queryClient: QueryClient, server: st
|
||||
const indexKey = homeSessionIndexKey(server)
|
||||
const eventsKey = homeSessionEventsKey(server)
|
||||
let connected = false
|
||||
const removed = new Set<string>()
|
||||
|
||||
return {
|
||||
indexKey,
|
||||
@@ -97,7 +98,8 @@ export function createHomeSessionIndexCache(queryClient: QueryClient, server: st
|
||||
queryClient.setQueryData<HomeSessionEvents>(eventsKey, (current) => trimHomeSessionEvents(current, sequence))
|
||||
},
|
||||
sessions(index: HomeSessionIndex | undefined, events: HomeSessionEvents | undefined) {
|
||||
return homeSessionIndexSessions(index, events)
|
||||
const sessions = homeSessionIndexSessions(index, events)
|
||||
return removed.size === 0 ? sessions : sessions.filter((session) => !removed.has(session.id))
|
||||
},
|
||||
apply(event: HomeSessionEvent) {
|
||||
if (!queryClient.getQueryState(indexKey)) return
|
||||
@@ -116,6 +118,16 @@ export function createHomeSessionIndexCache(queryClient: QueryClient, server: st
|
||||
}
|
||||
queryClient.setQueryData<HomeSessionEvents>(eventsKey, { sequence: next.sequence, entries: [] })
|
||||
},
|
||||
remove(sessionID: string) {
|
||||
removed.add(sessionID)
|
||||
if (!queryClient.getQueryState(indexKey)) return
|
||||
queryClient.setQueryData<HomeSessionIndex>(indexKey, (index) => {
|
||||
if (!index) return index
|
||||
const at = index.sessions.findIndex((session) => session.id === sessionID)
|
||||
if (at === -1) return index
|
||||
return { ...index, sessions: index.sessions.toSpliced(at, 1) }
|
||||
})
|
||||
},
|
||||
refresh(event: Event["type"]) {
|
||||
const result = homeSessionIndexRefresh(event, connected)
|
||||
connected = result.connected
|
||||
|
||||
@@ -219,13 +219,15 @@ export function createHomeSessionsController(home: HomeController) {
|
||||
directory: session.directory,
|
||||
time: { archived: Date.now() },
|
||||
}),
|
||||
remove: () =>
|
||||
remove: () => {
|
||||
setStore(
|
||||
produce((draft) => {
|
||||
const match = Binary.search(draft.session, session.id, (item) => item.id)
|
||||
if (match.found) draft.session.splice(match.index, 1)
|
||||
}),
|
||||
),
|
||||
)
|
||||
homeSessions().remove(session.id)
|
||||
},
|
||||
onError: (cause) =>
|
||||
showToast({
|
||||
title: language.t("common.requestFailed"),
|
||||
|
||||
@@ -3,6 +3,7 @@ import { produce } from "solid-js/store"
|
||||
import { notifySessionTabsRemoved } from "@/components/titlebar-session-events"
|
||||
import { useLanguage } from "@/context/language"
|
||||
import { useSDK } from "@/context/sdk"
|
||||
import { useServerSync } from "@/context/server-sync"
|
||||
import { useSync } from "@/context/sync"
|
||||
import { useTabs } from "@/context/tabs"
|
||||
import { errorMessage } from "@/pages/layout/helpers"
|
||||
@@ -15,6 +16,7 @@ export function useSessionArchive() {
|
||||
const navigate = useNavigate()
|
||||
const sdk = useSDK()
|
||||
const sync = useSync()
|
||||
const serverSync = useServerSync()
|
||||
const tabs = useTabs()
|
||||
const { params } = useSessionKey()
|
||||
|
||||
@@ -56,6 +58,7 @@ export function useSessionArchive() {
|
||||
}),
|
||||
)
|
||||
sync().session.evict(sessionID)
|
||||
serverSync().homeSessions.remove(sessionID)
|
||||
navigateAfterRemoval(sessionID, session.parentID, nextSession?.id)
|
||||
notifySessionTabsRemoved({ directory: sdk().directory, sessionIDs: [sessionID] })
|
||||
})
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/package.json",
|
||||
"name": "@opencode-ai/cli",
|
||||
"version": "1.18.22",
|
||||
"version": "1.18.23",
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/package.json",
|
||||
"name": "@opencode-ai/codemode",
|
||||
"version": "1.18.22",
|
||||
"version": "1.18.23",
|
||||
"description": "Effect-native confined code execution over schema-described tools",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@opencode-ai/console-app",
|
||||
"version": "1.18.22",
|
||||
"version": "1.18.23",
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
"scripts": {
|
||||
|
||||
@@ -252,7 +252,6 @@ export const dict = {
|
||||
"zen.privacy.exceptionsLink": "الاستثناءات التالية",
|
||||
|
||||
"go.title": "OpenCode Go | نماذج برمجة منخفضة التكلفة للجميع",
|
||||
"go.banner.text": "Ox Alpha Free متاح على Go لفترة محدودة",
|
||||
"go.meta.description": "يبلغ سعر Go $10/شهر، مع حدود استخدام سخية ووصول موثوق إلى نماذج البرمجة الرائدة.",
|
||||
"go.hero.title": "نماذج برمجة منخفضة التكلفة للجميع",
|
||||
"go.hero.body":
|
||||
@@ -661,10 +660,18 @@ export const dict = {
|
||||
"workspace.lite.time.fewSeconds": "بضع ثوان",
|
||||
"workspace.lite.subscription.message": "أنت مشترك في OpenCode Go.",
|
||||
"workspace.lite.subscription.manage": "إدارة الاشتراك",
|
||||
"workspace.lite.subscription.rollingUsage": "الاستخدام المتجدد",
|
||||
"workspace.lite.subscription.rollingUsage": "الاستخدام خلال 5 ساعات",
|
||||
"workspace.lite.subscription.rollingQuota": "الحصة خلال 5 ساعات",
|
||||
"workspace.lite.subscription.weeklyUsage": "الاستخدام الأسبوعي",
|
||||
"workspace.lite.subscription.weeklyQuota": "الحصة الأسبوعية",
|
||||
"workspace.lite.subscription.monthlyUsage": "الاستخدام الشهري",
|
||||
"workspace.lite.subscription.monthlyQuota": "الحصة الشهرية",
|
||||
"workspace.lite.subscription.resetsIn": "إعادة تعيين في",
|
||||
"workspace.lite.subscription.showDetails": "إظهار التفاصيل",
|
||||
"workspace.lite.subscription.hideDetails": "إخفاء التفاصيل",
|
||||
"workspace.lite.subscription.model": "النموذج",
|
||||
"workspace.lite.subscription.contribution": "%",
|
||||
"workspace.lite.subscription.total": "الإجمالي",
|
||||
"workspace.lite.subscription.useBalance": "استخدم رصيدك المتوفر بعد الوصول إلى حدود الاستخدام",
|
||||
"workspace.lite.subscription.selectProvider":
|
||||
'اختر "OpenCode Go" كمزود في إعدادات opencode الخاصة بك لاستخدام نماذج Go.',
|
||||
|
||||
@@ -256,7 +256,6 @@ export const dict = {
|
||||
"zen.privacy.exceptionsLink": "seguintes exceções",
|
||||
|
||||
"go.title": "OpenCode Go | Modelos de codificação de baixo custo para todos",
|
||||
"go.banner.text": "Ox Alpha Free está disponível no Go por tempo limitado",
|
||||
"go.meta.description":
|
||||
"O Go custa $10/mês, com limites generosos de uso e acesso confiável aos principais modelos de codificação.",
|
||||
"go.hero.title": "Modelos de codificação de baixo custo para todos",
|
||||
@@ -673,10 +672,18 @@ export const dict = {
|
||||
"workspace.lite.time.fewSeconds": "alguns segundos",
|
||||
"workspace.lite.subscription.message": "Você assina o OpenCode Go.",
|
||||
"workspace.lite.subscription.manage": "Gerenciar Assinatura",
|
||||
"workspace.lite.subscription.rollingUsage": "Uso Contínuo",
|
||||
"workspace.lite.subscription.rollingUsage": "Uso de 5 horas",
|
||||
"workspace.lite.subscription.rollingQuota": "Cota de 5 horas",
|
||||
"workspace.lite.subscription.weeklyUsage": "Uso Semanal",
|
||||
"workspace.lite.subscription.weeklyQuota": "Cota Semanal",
|
||||
"workspace.lite.subscription.monthlyUsage": "Uso Mensal",
|
||||
"workspace.lite.subscription.monthlyQuota": "Cota Mensal",
|
||||
"workspace.lite.subscription.resetsIn": "Reinicia em",
|
||||
"workspace.lite.subscription.showDetails": "Mostrar detalhes",
|
||||
"workspace.lite.subscription.hideDetails": "Ocultar detalhes",
|
||||
"workspace.lite.subscription.model": "Modelo",
|
||||
"workspace.lite.subscription.contribution": "%",
|
||||
"workspace.lite.subscription.total": "Total",
|
||||
"workspace.lite.subscription.useBalance": "Use seu saldo disponível após atingir os limites de uso",
|
||||
"workspace.lite.subscription.selectProvider":
|
||||
'Selecione "OpenCode Go" como provedor na sua configuração do opencode para usar os modelos Go.',
|
||||
|
||||
@@ -254,7 +254,6 @@ export const dict = {
|
||||
"zen.privacy.exceptionsLink": "følgende undtagelser",
|
||||
|
||||
"go.title": "OpenCode Go | Kodningsmodeller til lav pris for alle",
|
||||
"go.banner.text": "Ox Alpha Free er tilgængelig på Go i en begrænset periode",
|
||||
"go.meta.description":
|
||||
"Go koster $10/måned, med generøse brugsgrænser og pålidelig adgang til førende kodningsmodeller.",
|
||||
"go.hero.title": "Kodningsmodeller til lav pris for alle",
|
||||
@@ -669,10 +668,18 @@ export const dict = {
|
||||
"workspace.lite.time.fewSeconds": "et par sekunder",
|
||||
"workspace.lite.subscription.message": "Du abonnerer på OpenCode Go.",
|
||||
"workspace.lite.subscription.manage": "Administrer abonnement",
|
||||
"workspace.lite.subscription.rollingUsage": "Løbende forbrug",
|
||||
"workspace.lite.subscription.rollingUsage": "5-timers forbrug",
|
||||
"workspace.lite.subscription.rollingQuota": "5-timers kvote",
|
||||
"workspace.lite.subscription.weeklyUsage": "Ugentligt forbrug",
|
||||
"workspace.lite.subscription.weeklyQuota": "Ugentlig kvote",
|
||||
"workspace.lite.subscription.monthlyUsage": "Månedligt forbrug",
|
||||
"workspace.lite.subscription.monthlyQuota": "Månedlig kvote",
|
||||
"workspace.lite.subscription.resetsIn": "Nulstiller i",
|
||||
"workspace.lite.subscription.showDetails": "Vis detaljer",
|
||||
"workspace.lite.subscription.hideDetails": "Skjul detaljer",
|
||||
"workspace.lite.subscription.model": "Model",
|
||||
"workspace.lite.subscription.contribution": "%",
|
||||
"workspace.lite.subscription.total": "I alt",
|
||||
"workspace.lite.subscription.useBalance": "Brug din tilgængelige saldo, når du har nået forbrugsgrænserne",
|
||||
"workspace.lite.subscription.selectProvider":
|
||||
'Vælg "OpenCode Go" som udbyder i din opencode-konfiguration for at bruge Go-modeller.',
|
||||
|
||||
@@ -256,7 +256,6 @@ export const dict = {
|
||||
"zen.privacy.exceptionsLink": "folgenden Ausnahmen",
|
||||
|
||||
"go.title": "OpenCode Go | Kostengünstige Coding-Modelle für alle",
|
||||
"go.banner.text": "Ox Alpha Free ist für begrenzte Zeit auf Go verfügbar",
|
||||
"go.meta.description":
|
||||
"Go kostet $10/Monat, mit großzügigen Nutzungslimits und zuverlässigem Zugang zu führenden Coding-Modellen.",
|
||||
"go.hero.title": "Kostengünstige Coding-Modelle für alle",
|
||||
@@ -671,10 +670,18 @@ export const dict = {
|
||||
"workspace.lite.time.fewSeconds": "einige Sekunden",
|
||||
"workspace.lite.subscription.message": "Du hast OpenCode Go abonniert.",
|
||||
"workspace.lite.subscription.manage": "Abo verwalten",
|
||||
"workspace.lite.subscription.rollingUsage": "Fortlaufende Nutzung",
|
||||
"workspace.lite.subscription.rollingUsage": "5-Stunden-Nutzung",
|
||||
"workspace.lite.subscription.rollingQuota": "5-Stunden-Kontingent",
|
||||
"workspace.lite.subscription.weeklyUsage": "Wöchentliche Nutzung",
|
||||
"workspace.lite.subscription.weeklyQuota": "Wöchentliches Kontingent",
|
||||
"workspace.lite.subscription.monthlyUsage": "Monatliche Nutzung",
|
||||
"workspace.lite.subscription.monthlyQuota": "Monatliches Kontingent",
|
||||
"workspace.lite.subscription.resetsIn": "Setzt zurück in",
|
||||
"workspace.lite.subscription.showDetails": "Details anzeigen",
|
||||
"workspace.lite.subscription.hideDetails": "Details ausblenden",
|
||||
"workspace.lite.subscription.model": "Modell",
|
||||
"workspace.lite.subscription.contribution": "%",
|
||||
"workspace.lite.subscription.total": "Gesamt",
|
||||
"workspace.lite.subscription.useBalance": "Nutze dein verfügbares Guthaben, nachdem die Nutzungslimits erreicht sind",
|
||||
"workspace.lite.subscription.selectProvider":
|
||||
'Wähle "OpenCode Go" als Anbieter in deiner opencode-Konfiguration, um Go-Modelle zu verwenden.',
|
||||
|
||||
@@ -253,7 +253,6 @@ export const dict = {
|
||||
"zen.privacy.exceptionsLink": "following exceptions",
|
||||
|
||||
"go.title": "OpenCode Go | Low cost coding models for everyone",
|
||||
"go.banner.text": "Ox Alpha Free is available on Go for a limited time",
|
||||
"go.meta.description": "Go costs $10/month, with generous usage limits and reliable access to leading coding models.",
|
||||
"go.hero.title": "Low cost coding models for everyone",
|
||||
"go.hero.body":
|
||||
@@ -669,10 +668,18 @@ export const dict = {
|
||||
"workspace.lite.time.fewSeconds": "a few seconds",
|
||||
"workspace.lite.subscription.message": "You are subscribed to OpenCode Go.",
|
||||
"workspace.lite.subscription.manage": "Manage Subscription",
|
||||
"workspace.lite.subscription.rollingUsage": "Rolling Usage",
|
||||
"workspace.lite.subscription.rollingUsage": "5-hour Usage",
|
||||
"workspace.lite.subscription.rollingQuota": "5-hour Quota",
|
||||
"workspace.lite.subscription.weeklyUsage": "Weekly Usage",
|
||||
"workspace.lite.subscription.weeklyQuota": "Weekly Quota",
|
||||
"workspace.lite.subscription.monthlyUsage": "Monthly Usage",
|
||||
"workspace.lite.subscription.monthlyQuota": "Monthly Quota",
|
||||
"workspace.lite.subscription.resetsIn": "Resets in",
|
||||
"workspace.lite.subscription.showDetails": "Show details",
|
||||
"workspace.lite.subscription.hideDetails": "Hide details",
|
||||
"workspace.lite.subscription.model": "Model",
|
||||
"workspace.lite.subscription.contribution": "%",
|
||||
"workspace.lite.subscription.total": "Total",
|
||||
"workspace.lite.subscription.useBalance": "Use your available balance after reaching the usage limits",
|
||||
"workspace.lite.subscription.selectProvider":
|
||||
'Select "OpenCode Go" as the provider in your opencode configuration to use Go models.',
|
||||
|
||||
@@ -257,7 +257,6 @@ export const dict = {
|
||||
"zen.privacy.exceptionsLink": "siguientes excepciones",
|
||||
|
||||
"go.title": "OpenCode Go | Modelos de programación de bajo coste para todos",
|
||||
"go.banner.text": "Ox Alpha Free está disponible en Go por tiempo limitado",
|
||||
"go.meta.description":
|
||||
"Go cuesta 10 $/mes, con límites de uso generosos y acceso fiable a modelos de programación líderes.",
|
||||
"go.hero.title": "Modelos de programación de bajo coste para todos",
|
||||
@@ -674,10 +673,18 @@ export const dict = {
|
||||
"workspace.lite.time.fewSeconds": "unos pocos segundos",
|
||||
"workspace.lite.subscription.message": "Estás suscrito a OpenCode Go.",
|
||||
"workspace.lite.subscription.manage": "Gestionar Suscripción",
|
||||
"workspace.lite.subscription.rollingUsage": "Uso Continuo",
|
||||
"workspace.lite.subscription.rollingUsage": "Uso de 5 horas",
|
||||
"workspace.lite.subscription.rollingQuota": "Cuota de 5 horas",
|
||||
"workspace.lite.subscription.weeklyUsage": "Uso Semanal",
|
||||
"workspace.lite.subscription.weeklyQuota": "Cuota Semanal",
|
||||
"workspace.lite.subscription.monthlyUsage": "Uso Mensual",
|
||||
"workspace.lite.subscription.monthlyQuota": "Cuota Mensual",
|
||||
"workspace.lite.subscription.resetsIn": "Se reinicia en",
|
||||
"workspace.lite.subscription.showDetails": "Mostrar detalles",
|
||||
"workspace.lite.subscription.hideDetails": "Ocultar detalles",
|
||||
"workspace.lite.subscription.model": "Modelo",
|
||||
"workspace.lite.subscription.contribution": "%",
|
||||
"workspace.lite.subscription.total": "Total",
|
||||
"workspace.lite.subscription.useBalance": "Usa tu saldo disponible después de alcanzar los límites de uso",
|
||||
"workspace.lite.subscription.selectProvider":
|
||||
'Selecciona "OpenCode Go" como proveedor en tu configuración de opencode para usar los modelos Go.',
|
||||
|
||||
@@ -258,7 +258,6 @@ export const dict = {
|
||||
"zen.privacy.exceptionsLink": "exceptions suivantes",
|
||||
|
||||
"go.title": "OpenCode Go | Modèles de code à faible coût pour tous",
|
||||
"go.banner.text": "Ox Alpha Free est disponible sur Go pour une durée limitée",
|
||||
"go.meta.description":
|
||||
"Go coûte 10 $/mois, avec des limites d'utilisation généreuses et un accès fiable aux principaux modèles de codage.",
|
||||
"go.hero.title": "Modèles de code à faible coût pour tous",
|
||||
@@ -679,10 +678,18 @@ export const dict = {
|
||||
"workspace.lite.time.fewSeconds": "quelques secondes",
|
||||
"workspace.lite.subscription.message": "Vous êtes abonné à OpenCode Go.",
|
||||
"workspace.lite.subscription.manage": "Gérer l'abonnement",
|
||||
"workspace.lite.subscription.rollingUsage": "Utilisation glissante",
|
||||
"workspace.lite.subscription.rollingUsage": "Utilisation sur 5 heures",
|
||||
"workspace.lite.subscription.rollingQuota": "Quota sur 5 heures",
|
||||
"workspace.lite.subscription.weeklyUsage": "Utilisation hebdomadaire",
|
||||
"workspace.lite.subscription.weeklyQuota": "Quota hebdomadaire",
|
||||
"workspace.lite.subscription.monthlyUsage": "Utilisation mensuelle",
|
||||
"workspace.lite.subscription.monthlyQuota": "Quota mensuel",
|
||||
"workspace.lite.subscription.resetsIn": "Réinitialisation dans",
|
||||
"workspace.lite.subscription.showDetails": "Afficher les détails",
|
||||
"workspace.lite.subscription.hideDetails": "Masquer les détails",
|
||||
"workspace.lite.subscription.model": "Modèle",
|
||||
"workspace.lite.subscription.contribution": "%",
|
||||
"workspace.lite.subscription.total": "Total",
|
||||
"workspace.lite.subscription.useBalance":
|
||||
"Utilisez votre solde disponible après avoir atteint les limites d'utilisation",
|
||||
"workspace.lite.subscription.selectProvider":
|
||||
|
||||
@@ -254,7 +254,6 @@ export const dict = {
|
||||
"zen.privacy.exceptionsLink": "seguenti eccezioni",
|
||||
|
||||
"go.title": "OpenCode Go | Modelli di coding a basso costo per tutti",
|
||||
"go.banner.text": "Ox Alpha Free è disponibile su Go per un periodo limitato",
|
||||
"go.meta.description":
|
||||
"Go costa $10/mese, con limiti di utilizzo generosi e un accesso affidabile ai principali modelli di coding.",
|
||||
"go.hero.title": "Modelli di coding a basso costo per tutti",
|
||||
@@ -672,10 +671,18 @@ export const dict = {
|
||||
"workspace.lite.time.fewSeconds": "pochi secondi",
|
||||
"workspace.lite.subscription.message": "Sei abbonato a OpenCode Go.",
|
||||
"workspace.lite.subscription.manage": "Gestisci Abbonamento",
|
||||
"workspace.lite.subscription.rollingUsage": "Utilizzo Continuativo",
|
||||
"workspace.lite.subscription.rollingUsage": "Utilizzo su 5 ore",
|
||||
"workspace.lite.subscription.rollingQuota": "Quota su 5 ore",
|
||||
"workspace.lite.subscription.weeklyUsage": "Utilizzo Settimanale",
|
||||
"workspace.lite.subscription.weeklyQuota": "Quota Settimanale",
|
||||
"workspace.lite.subscription.monthlyUsage": "Utilizzo Mensile",
|
||||
"workspace.lite.subscription.monthlyQuota": "Quota Mensile",
|
||||
"workspace.lite.subscription.resetsIn": "Si resetta tra",
|
||||
"workspace.lite.subscription.showDetails": "Mostra dettagli",
|
||||
"workspace.lite.subscription.hideDetails": "Nascondi dettagli",
|
||||
"workspace.lite.subscription.model": "Modello",
|
||||
"workspace.lite.subscription.contribution": "%",
|
||||
"workspace.lite.subscription.total": "Totale",
|
||||
"workspace.lite.subscription.useBalance": "Usa il tuo saldo disponibile dopo aver raggiunto i limiti di utilizzo",
|
||||
"workspace.lite.subscription.selectProvider":
|
||||
'Seleziona "OpenCode Go" come provider nella tua configurazione opencode per utilizzare i modelli Go.',
|
||||
|
||||
@@ -253,7 +253,6 @@ export const dict = {
|
||||
"zen.privacy.exceptionsLink": "以下の例外",
|
||||
|
||||
"go.title": "OpenCode Go | すべての人のための低価格なコーディングモデル",
|
||||
"go.banner.text": "Ox Alpha Freeは期間限定でGoで利用できます",
|
||||
"go.meta.description":
|
||||
"Goは月額$10で、主要なコーディングモデルへのゆとりある利用上限と安定したアクセスを提供します。",
|
||||
"go.hero.title": "すべての人のための低価格なコーディングモデル",
|
||||
@@ -670,10 +669,18 @@ export const dict = {
|
||||
"workspace.lite.time.fewSeconds": "数秒",
|
||||
"workspace.lite.subscription.message": "あなたは OpenCode Go を購読しています。",
|
||||
"workspace.lite.subscription.manage": "サブスクリプションの管理",
|
||||
"workspace.lite.subscription.rollingUsage": "ローリング利用量",
|
||||
"workspace.lite.subscription.rollingUsage": "5時間利用量",
|
||||
"workspace.lite.subscription.rollingQuota": "5時間上限",
|
||||
"workspace.lite.subscription.weeklyUsage": "週間利用量",
|
||||
"workspace.lite.subscription.weeklyQuota": "週間上限",
|
||||
"workspace.lite.subscription.monthlyUsage": "月間利用量",
|
||||
"workspace.lite.subscription.monthlyQuota": "月間上限",
|
||||
"workspace.lite.subscription.resetsIn": "リセットまで",
|
||||
"workspace.lite.subscription.showDetails": "詳細を表示",
|
||||
"workspace.lite.subscription.hideDetails": "詳細を非表示",
|
||||
"workspace.lite.subscription.model": "モデル",
|
||||
"workspace.lite.subscription.contribution": "%",
|
||||
"workspace.lite.subscription.total": "合計",
|
||||
"workspace.lite.subscription.useBalance": "利用限度額に達したら利用可能な残高を使用する",
|
||||
"workspace.lite.subscription.selectProvider":
|
||||
"Go モデルを使用するには、opencode の設定で「OpenCode Go」をプロバイダーとして選択してください。",
|
||||
|
||||
@@ -250,7 +250,6 @@ export const dict = {
|
||||
"zen.privacy.exceptionsLink": "다음 예외",
|
||||
|
||||
"go.title": "OpenCode Go | 모두를 위한 저비용 코딩 모델",
|
||||
"go.banner.text": "Ox Alpha Free가 한정된 기간 동안 Go에서 제공됩니다",
|
||||
"go.meta.description": "Go는 월 $10이며, 넉넉한 사용 한도와 주요 코딩 모델에 대한 안정적인 액세스를 제공합니다.",
|
||||
"go.hero.title": "모두를 위한 저비용 코딩 모델",
|
||||
"go.hero.body":
|
||||
@@ -661,10 +660,18 @@ export const dict = {
|
||||
"workspace.lite.time.fewSeconds": "몇 초",
|
||||
"workspace.lite.subscription.message": "현재 OpenCode Go를 구독 중입니다.",
|
||||
"workspace.lite.subscription.manage": "구독 관리",
|
||||
"workspace.lite.subscription.rollingUsage": "롤링 사용량",
|
||||
"workspace.lite.subscription.rollingUsage": "5시간 사용량",
|
||||
"workspace.lite.subscription.rollingQuota": "5시간 할당량",
|
||||
"workspace.lite.subscription.weeklyUsage": "주간 사용량",
|
||||
"workspace.lite.subscription.weeklyQuota": "주간 할당량",
|
||||
"workspace.lite.subscription.monthlyUsage": "월간 사용량",
|
||||
"workspace.lite.subscription.monthlyQuota": "월간 할당량",
|
||||
"workspace.lite.subscription.resetsIn": "초기화까지 남은 시간:",
|
||||
"workspace.lite.subscription.showDetails": "상세 정보 보기",
|
||||
"workspace.lite.subscription.hideDetails": "상세 정보 숨기기",
|
||||
"workspace.lite.subscription.model": "모델",
|
||||
"workspace.lite.subscription.contribution": "%",
|
||||
"workspace.lite.subscription.total": "합계",
|
||||
"workspace.lite.subscription.useBalance": "사용 한도 도달 후에는 보유 잔액 사용",
|
||||
"workspace.lite.subscription.selectProvider":
|
||||
'Go 모델을 사용하려면 opencode 설정에서 "OpenCode Go"를 공급자로 선택하세요.',
|
||||
|
||||
@@ -254,7 +254,6 @@ export const dict = {
|
||||
"zen.privacy.exceptionsLink": "følgende unntak",
|
||||
|
||||
"go.title": "OpenCode Go | Rimelige kodemodeller for alle",
|
||||
"go.banner.text": "Ox Alpha Free er tilgjengelig på Go i en begrenset periode",
|
||||
"go.meta.description":
|
||||
"Go koster $10/måned, med sjenerøse bruksgrenser og pålitelig tilgang til ledende kodemodeller.",
|
||||
"go.hero.title": "Rimelige kodemodeller for alle",
|
||||
@@ -670,10 +669,18 @@ export const dict = {
|
||||
"workspace.lite.time.fewSeconds": "noen få sekunder",
|
||||
"workspace.lite.subscription.message": "Du abonnerer på OpenCode Go.",
|
||||
"workspace.lite.subscription.manage": "Administrer abonnement",
|
||||
"workspace.lite.subscription.rollingUsage": "Løpende bruk",
|
||||
"workspace.lite.subscription.rollingUsage": "5-timers bruk",
|
||||
"workspace.lite.subscription.rollingQuota": "5-timers kvote",
|
||||
"workspace.lite.subscription.weeklyUsage": "Ukentlig bruk",
|
||||
"workspace.lite.subscription.weeklyQuota": "Ukentlig kvote",
|
||||
"workspace.lite.subscription.monthlyUsage": "Månedlig bruk",
|
||||
"workspace.lite.subscription.monthlyQuota": "Månedlig kvote",
|
||||
"workspace.lite.subscription.resetsIn": "Nullstilles om",
|
||||
"workspace.lite.subscription.showDetails": "Vis detaljer",
|
||||
"workspace.lite.subscription.hideDetails": "Skjul detaljer",
|
||||
"workspace.lite.subscription.model": "Modell",
|
||||
"workspace.lite.subscription.contribution": "%",
|
||||
"workspace.lite.subscription.total": "Totalt",
|
||||
"workspace.lite.subscription.useBalance": "Bruk din tilgjengelige saldo etter å ha nådd bruksgrensene",
|
||||
"workspace.lite.subscription.selectProvider":
|
||||
'Velg "OpenCode Go" som leverandør i opencode-konfigurasjonen din for å bruke Go-modeller.',
|
||||
|
||||
@@ -255,7 +255,6 @@ export const dict = {
|
||||
"zen.privacy.exceptionsLink": "następującymi wyjątkami",
|
||||
|
||||
"go.title": "OpenCode Go | Niskokosztowe modele do kodowania dla każdego",
|
||||
"go.banner.text": "Ox Alpha Free jest dostępny w Go przez ograniczony czas",
|
||||
"go.meta.description":
|
||||
"Go kosztuje $10/miesiąc, oferując hojne limity użycia i niezawodny dostęp do wiodących modeli do kodowania.",
|
||||
"go.hero.title": "Niskokosztowe modele do kodowania dla każdego",
|
||||
@@ -671,10 +670,18 @@ export const dict = {
|
||||
"workspace.lite.time.fewSeconds": "kilka sekund",
|
||||
"workspace.lite.subscription.message": "Subskrybujesz OpenCode Go.",
|
||||
"workspace.lite.subscription.manage": "Zarządzaj subskrypcją",
|
||||
"workspace.lite.subscription.rollingUsage": "Użycie kroczące",
|
||||
"workspace.lite.subscription.rollingUsage": "Użycie w ciągu 5 godzin",
|
||||
"workspace.lite.subscription.rollingQuota": "Limit 5-godzinny",
|
||||
"workspace.lite.subscription.weeklyUsage": "Użycie tygodniowe",
|
||||
"workspace.lite.subscription.weeklyQuota": "Limit tygodniowy",
|
||||
"workspace.lite.subscription.monthlyUsage": "Użycie miesięczne",
|
||||
"workspace.lite.subscription.monthlyQuota": "Limit miesięczny",
|
||||
"workspace.lite.subscription.resetsIn": "Resetuje się za",
|
||||
"workspace.lite.subscription.showDetails": "Pokaż szczegóły",
|
||||
"workspace.lite.subscription.hideDetails": "Ukryj szczegóły",
|
||||
"workspace.lite.subscription.model": "Model",
|
||||
"workspace.lite.subscription.contribution": "%",
|
||||
"workspace.lite.subscription.total": "Łącznie",
|
||||
"workspace.lite.subscription.useBalance": "Użyj dostępnego salda po osiągnięciu limitów użycia",
|
||||
"workspace.lite.subscription.selectProvider":
|
||||
'Wybierz "OpenCode Go" jako dostawcę w konfiguracji opencode, aby używać modeli Go.',
|
||||
|
||||
@@ -258,7 +258,6 @@ export const dict = {
|
||||
"zen.privacy.exceptionsLink": "следующими исключениями",
|
||||
|
||||
"go.title": "OpenCode Go | Недорогие модели для кодинга для всех",
|
||||
"go.banner.text": "Ox Alpha Free доступна в Go в течение ограниченного времени",
|
||||
"go.meta.description":
|
||||
"Go стоит $10/месяц и предлагает щедрые лимиты использования и надежный доступ к ведущим моделям для кодинга.",
|
||||
"go.hero.title": "Недорогие модели для кодинга для всех",
|
||||
@@ -678,10 +677,18 @@ export const dict = {
|
||||
"workspace.lite.time.fewSeconds": "несколько секунд",
|
||||
"workspace.lite.subscription.message": "Вы подписаны на OpenCode Go.",
|
||||
"workspace.lite.subscription.manage": "Управление подпиской",
|
||||
"workspace.lite.subscription.rollingUsage": "Скользящее использование",
|
||||
"workspace.lite.subscription.rollingUsage": "Использование за 5 часов",
|
||||
"workspace.lite.subscription.rollingQuota": "Квота на 5 часов",
|
||||
"workspace.lite.subscription.weeklyUsage": "Недельное использование",
|
||||
"workspace.lite.subscription.weeklyQuota": "Недельная квота",
|
||||
"workspace.lite.subscription.monthlyUsage": "Ежемесячное использование",
|
||||
"workspace.lite.subscription.monthlyQuota": "Ежемесячная квота",
|
||||
"workspace.lite.subscription.resetsIn": "Сброс через",
|
||||
"workspace.lite.subscription.showDetails": "Показать подробности",
|
||||
"workspace.lite.subscription.hideDetails": "Скрыть подробности",
|
||||
"workspace.lite.subscription.model": "Модель",
|
||||
"workspace.lite.subscription.contribution": "%",
|
||||
"workspace.lite.subscription.total": "Итого",
|
||||
"workspace.lite.subscription.useBalance": "Использовать доступный баланс после достижения лимитов",
|
||||
"workspace.lite.subscription.selectProvider":
|
||||
'Выберите "OpenCode Go" в качестве провайдера в настройках opencode для использования моделей Go.',
|
||||
|
||||
@@ -253,7 +253,6 @@ export const dict = {
|
||||
"zen.privacy.exceptionsLink": "ข้อยกเว้นดังนี้",
|
||||
|
||||
"go.title": "OpenCode Go | โมเดลเขียนโค้ดราคาประหยัดสำหรับทุกคน",
|
||||
"go.banner.text": "Ox Alpha Free พร้อมใช้งานบน Go ในช่วงเวลาจำกัด",
|
||||
"go.meta.description":
|
||||
"Go มีราคา $10/เดือน พร้อมขีดจำกัดการใช้งานที่เอื้อเฟื้อและการเข้าถึงโมเดลเขียนโค้ดชั้นนำอย่างเชื่อถือได้",
|
||||
"go.hero.title": "โมเดลเขียนโค้ดราคาประหยัดสำหรับทุกคน",
|
||||
@@ -667,10 +666,18 @@ export const dict = {
|
||||
"workspace.lite.time.fewSeconds": "ไม่กี่วินาที",
|
||||
"workspace.lite.subscription.message": "คุณได้สมัครสมาชิก OpenCode Go แล้ว",
|
||||
"workspace.lite.subscription.manage": "จัดการการสมัครสมาชิก",
|
||||
"workspace.lite.subscription.rollingUsage": "การใช้งานแบบหมุนเวียน",
|
||||
"workspace.lite.subscription.rollingUsage": "การใช้งานใน 5 ชั่วโมง",
|
||||
"workspace.lite.subscription.rollingQuota": "โควตา 5 ชั่วโมง",
|
||||
"workspace.lite.subscription.weeklyUsage": "การใช้งานรายสัปดาห์",
|
||||
"workspace.lite.subscription.weeklyQuota": "โควตารายสัปดาห์",
|
||||
"workspace.lite.subscription.monthlyUsage": "การใช้งานรายเดือน",
|
||||
"workspace.lite.subscription.monthlyQuota": "โควตารายเดือน",
|
||||
"workspace.lite.subscription.resetsIn": "รีเซ็ตใน",
|
||||
"workspace.lite.subscription.showDetails": "แสดงรายละเอียด",
|
||||
"workspace.lite.subscription.hideDetails": "ซ่อนรายละเอียด",
|
||||
"workspace.lite.subscription.model": "โมเดล",
|
||||
"workspace.lite.subscription.contribution": "%",
|
||||
"workspace.lite.subscription.total": "รวม",
|
||||
"workspace.lite.subscription.useBalance": "ใช้ยอดคงเหลือของคุณหลังจากถึงขีดจำกัดการใช้งาน",
|
||||
"workspace.lite.subscription.selectProvider":
|
||||
'เลือก "OpenCode Go" เป็นผู้ให้บริการในการตั้งค่า opencode ของคุณเพื่อใช้โมเดล Go',
|
||||
|
||||
@@ -256,7 +256,6 @@ export const dict = {
|
||||
"zen.privacy.exceptionsLink": "aşağıdaki istisnalar",
|
||||
|
||||
"go.title": "OpenCode Go | Herkes için düşük maliyetli kodlama modelleri",
|
||||
"go.banner.text": "Ox Alpha Free sınırlı bir süre için Go'da kullanılabilir",
|
||||
"go.meta.description":
|
||||
"Go ayda 10$'dır; cömert kullanım limitleri ve önde gelen kodlama modellerine güvenilir erişim sunar.",
|
||||
"go.hero.title": "Herkes için düşük maliyetli kodlama modelleri",
|
||||
@@ -673,10 +672,18 @@ export const dict = {
|
||||
"workspace.lite.time.fewSeconds": "birkaç saniye",
|
||||
"workspace.lite.subscription.message": "OpenCode Go abonesisiniz.",
|
||||
"workspace.lite.subscription.manage": "Aboneliği Yönet",
|
||||
"workspace.lite.subscription.rollingUsage": "Devam Eden Kullanım",
|
||||
"workspace.lite.subscription.rollingUsage": "5 Saatlik Kullanım",
|
||||
"workspace.lite.subscription.rollingQuota": "5 Saatlik Kota",
|
||||
"workspace.lite.subscription.weeklyUsage": "Haftalık Kullanım",
|
||||
"workspace.lite.subscription.weeklyQuota": "Haftalık Kota",
|
||||
"workspace.lite.subscription.monthlyUsage": "Aylık Kullanım",
|
||||
"workspace.lite.subscription.monthlyQuota": "Aylık Kota",
|
||||
"workspace.lite.subscription.resetsIn": "Sıfırlama süresi",
|
||||
"workspace.lite.subscription.showDetails": "Ayrıntıları göster",
|
||||
"workspace.lite.subscription.hideDetails": "Ayrıntıları gizle",
|
||||
"workspace.lite.subscription.model": "Model",
|
||||
"workspace.lite.subscription.contribution": "%",
|
||||
"workspace.lite.subscription.total": "Toplam",
|
||||
"workspace.lite.subscription.useBalance": "Kullanım limitlerine ulaştıktan sonra mevcut bakiyenizi kullanın",
|
||||
"workspace.lite.subscription.selectProvider":
|
||||
'Go modellerini kullanmak için opencode yapılandırmanızda "OpenCode Go"\'yu sağlayıcı olarak seçin.',
|
||||
|
||||
@@ -255,7 +255,6 @@ export const dict = {
|
||||
"zen.privacy.exceptionsLink": "такими винятками",
|
||||
|
||||
"go.title": "OpenCode Go | Недорогі моделі кодування для всіх",
|
||||
"go.banner.text": "Ox Alpha Free доступна в Go протягом обмеженого часу",
|
||||
"go.meta.description":
|
||||
"Go коштує $10/місяць, зі щедрими лімітами використання та надійним доступом до провідних моделей для кодування.",
|
||||
"go.hero.title": "Недорогі моделі кодування для всіх",
|
||||
@@ -669,10 +668,18 @@ export const dict = {
|
||||
"workspace.lite.time.fewSeconds": "кілька секунд",
|
||||
"workspace.lite.subscription.message": "Ви підписані на OpenCode Go.",
|
||||
"workspace.lite.subscription.manage": "Керувати підпискою",
|
||||
"workspace.lite.subscription.rollingUsage": "Ковзне використання",
|
||||
"workspace.lite.subscription.rollingUsage": "Використання за 5 годин",
|
||||
"workspace.lite.subscription.rollingQuota": "Квота на 5 годин",
|
||||
"workspace.lite.subscription.weeklyUsage": "Тижневе використання",
|
||||
"workspace.lite.subscription.weeklyQuota": "Тижнева квота",
|
||||
"workspace.lite.subscription.monthlyUsage": "Місячне використання",
|
||||
"workspace.lite.subscription.monthlyQuota": "Місячна квота",
|
||||
"workspace.lite.subscription.resetsIn": "Скидається через",
|
||||
"workspace.lite.subscription.showDetails": "Показати подробиці",
|
||||
"workspace.lite.subscription.hideDetails": "Приховати подробиці",
|
||||
"workspace.lite.subscription.model": "Модель",
|
||||
"workspace.lite.subscription.contribution": "%",
|
||||
"workspace.lite.subscription.total": "Усього",
|
||||
"workspace.lite.subscription.useBalance": "Використовуйте доступний баланс після досягнення лімітів",
|
||||
"workspace.lite.subscription.selectProvider": 'Виберіть "OpenCode Go" як провайдера в конфігурації opencode.',
|
||||
"workspace.lite.providers.title": "Провайдери",
|
||||
|
||||
@@ -244,7 +244,6 @@ export const dict = {
|
||||
"zen.privacy.exceptionsLink": "以下例外情况除外",
|
||||
|
||||
"go.title": "OpenCode Go | 人人可用的低成本编程模型",
|
||||
"go.banner.text": "Ox Alpha Free 限时加入 Go",
|
||||
"go.meta.description": "Go 每月 $10,提供充裕的使用限额,并可可靠访问领先的编程模型。",
|
||||
"go.hero.title": "人人可用的低成本编程模型",
|
||||
"go.hero.body":
|
||||
@@ -642,10 +641,18 @@ export const dict = {
|
||||
"workspace.lite.time.fewSeconds": "几秒钟",
|
||||
"workspace.lite.subscription.message": "您已订阅 OpenCode Go。",
|
||||
"workspace.lite.subscription.manage": "管理订阅",
|
||||
"workspace.lite.subscription.rollingUsage": "滚动用量",
|
||||
"workspace.lite.subscription.rollingUsage": "5 小时用量",
|
||||
"workspace.lite.subscription.rollingQuota": "5 小时配额",
|
||||
"workspace.lite.subscription.weeklyUsage": "每周用量",
|
||||
"workspace.lite.subscription.weeklyQuota": "每周配额",
|
||||
"workspace.lite.subscription.monthlyUsage": "每月用量",
|
||||
"workspace.lite.subscription.monthlyQuota": "每月配额",
|
||||
"workspace.lite.subscription.resetsIn": "重置于",
|
||||
"workspace.lite.subscription.showDetails": "显示详情",
|
||||
"workspace.lite.subscription.hideDetails": "隐藏详情",
|
||||
"workspace.lite.subscription.model": "模型",
|
||||
"workspace.lite.subscription.contribution": "%",
|
||||
"workspace.lite.subscription.total": "总计",
|
||||
"workspace.lite.subscription.useBalance": "达到使用限额后使用您的可用余额",
|
||||
"workspace.lite.subscription.selectProvider":
|
||||
"在你的 opencode 配置中选择「OpenCode Go」作为提供商,即可使用 Go 模型。",
|
||||
|
||||
@@ -244,7 +244,6 @@ export const dict = {
|
||||
"zen.privacy.exceptionsLink": "以下例外情況",
|
||||
|
||||
"go.title": "OpenCode Go | 低成本全民編碼模型",
|
||||
"go.banner.text": "Ox Alpha Free 限時加入 Go",
|
||||
"go.meta.description": "Go 每月 $10,提供充裕的使用限額,並可穩定存取領先的編碼模型。",
|
||||
"go.hero.title": "低成本全民編碼模型",
|
||||
"go.hero.body":
|
||||
@@ -642,10 +641,18 @@ export const dict = {
|
||||
"workspace.lite.time.fewSeconds": "幾秒",
|
||||
"workspace.lite.subscription.message": "您已訂閱 OpenCode Go。",
|
||||
"workspace.lite.subscription.manage": "管理訂閱",
|
||||
"workspace.lite.subscription.rollingUsage": "滾動使用量",
|
||||
"workspace.lite.subscription.rollingUsage": "5 小時使用量",
|
||||
"workspace.lite.subscription.rollingQuota": "5 小時配額",
|
||||
"workspace.lite.subscription.weeklyUsage": "每週使用量",
|
||||
"workspace.lite.subscription.weeklyQuota": "每週配額",
|
||||
"workspace.lite.subscription.monthlyUsage": "每月使用量",
|
||||
"workspace.lite.subscription.monthlyQuota": "每月配額",
|
||||
"workspace.lite.subscription.resetsIn": "重置時間:",
|
||||
"workspace.lite.subscription.showDetails": "顯示詳情",
|
||||
"workspace.lite.subscription.hideDetails": "隱藏詳情",
|
||||
"workspace.lite.subscription.model": "模型",
|
||||
"workspace.lite.subscription.contribution": "%",
|
||||
"workspace.lite.subscription.total": "總計",
|
||||
"workspace.lite.subscription.useBalance": "達到使用限制後使用您的可用餘額",
|
||||
"workspace.lite.subscription.selectProvider":
|
||||
"在您的 opencode 設定中選擇「OpenCode Go」作為提供商,即可使用 Go 模型。",
|
||||
|
||||
@@ -0,0 +1,60 @@
|
||||
export type LiteUsageBreakdownSource = {
|
||||
model: string
|
||||
name: string
|
||||
cost: number
|
||||
quotaCost: number
|
||||
multiplier?: number
|
||||
estimated: boolean
|
||||
}
|
||||
|
||||
export type LiteUsageBreakdownItem = {
|
||||
model: string
|
||||
name: string
|
||||
cost?: number
|
||||
multiplier?: number
|
||||
quotaCost: number
|
||||
contributionPercent: number
|
||||
estimated: boolean
|
||||
}
|
||||
|
||||
export function buildLiteUsageBreakdown(input: { usage: number; limit: number; sources: LiteUsageBreakdownSource[] }) {
|
||||
const rows: LiteUsageBreakdownItem[] = input.sources
|
||||
.filter((item) => item.cost !== 0 || item.quotaCost !== 0)
|
||||
.sort((a, b) => b.quotaCost - a.quotaCost)
|
||||
.map((item) => ({
|
||||
...item,
|
||||
contributionPercent: 0,
|
||||
}))
|
||||
|
||||
const usagePercent = getUsagePercent(input.usage, input.limit)
|
||||
const target = Math.max(0, Math.round(usagePercent * 10))
|
||||
const totalQuota = rows.reduce((total, item) => total + Math.max(0, item.quotaCost), 0)
|
||||
const units = rows.map((item) => {
|
||||
const exact = totalQuota === 0 ? 0 : (Math.max(0, item.quotaCost) / totalQuota) * target
|
||||
const value = Math.floor(exact)
|
||||
return { item, exact, value }
|
||||
})
|
||||
const remaining = target - units.reduce((total, item) => total + item.value, 0)
|
||||
const ranked = units.toSorted((a, b) => b.exact - b.value - (a.exact - a.value))
|
||||
Array.from({ length: ranked.length === 0 ? 0 : remaining }).forEach((_, index) => {
|
||||
ranked[index % ranked.length].value += 1
|
||||
})
|
||||
units.forEach((unit) => (unit.item.contributionPercent = unit.value / 10))
|
||||
|
||||
return {
|
||||
usage: input.usage,
|
||||
limit: input.limit,
|
||||
usagePercent,
|
||||
rows,
|
||||
}
|
||||
}
|
||||
|
||||
export function getModelQuotaLimit(limit: number, multiplier?: number) {
|
||||
if (multiplier === undefined || multiplier <= 0) return
|
||||
return limit / multiplier
|
||||
}
|
||||
|
||||
export function getUsagePercent(amount: number, limit: number) {
|
||||
if (limit === 0) return 0
|
||||
return Math.round((amount / limit) * 1000) / 10
|
||||
}
|
||||
@@ -103,9 +103,6 @@ export default function Black() {
|
||||
<button type="button" onClick={() => cancel()} data-slot="cancel">
|
||||
{i18n.t("common.cancel")}
|
||||
</button>
|
||||
<a href={`/black/subscribe/${plan().id}`} data-slot="continue">
|
||||
{i18n.t("black.action.continue")}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,489 +0,0 @@
|
||||
import { A, createAsync, query, redirect, useParams } from "@solidjs/router"
|
||||
import { Title } from "@solidjs/meta"
|
||||
import { createEffect, createSignal, For, Match, Show, Switch } from "solid-js"
|
||||
import { type Stripe, type PaymentMethod, loadStripe } from "@stripe/stripe-js"
|
||||
import { Elements, PaymentElement, useStripe, useElements, AddressElement } from "solid-stripe"
|
||||
import { PlanID, plans } from "../common"
|
||||
import { getActor, useAuthSession } from "~/context/auth"
|
||||
import { withActor } from "~/context/auth.withActor"
|
||||
import { Actor } from "@opencode-ai/console-core/actor.js"
|
||||
import { and, Database, eq, isNull } from "@opencode-ai/console-core/drizzle/index.js"
|
||||
import { WorkspaceTable } from "@opencode-ai/console-core/schema/workspace.sql.js"
|
||||
import { UserTable } from "@opencode-ai/console-core/schema/user.sql.js"
|
||||
import { createList } from "solid-list"
|
||||
import { Modal } from "~/component/modal"
|
||||
import { BillingTable } from "@opencode-ai/console-core/schema/billing.sql.js"
|
||||
import { Billing } from "@opencode-ai/console-core/billing.js"
|
||||
import { useI18n } from "~/context/i18n"
|
||||
import { useLanguage } from "~/context/language"
|
||||
import { formError } from "~/lib/form-error"
|
||||
import { Resource } from "@opencode-ai/console-resource"
|
||||
|
||||
const getEnabled = query(async () => {
|
||||
"use server"
|
||||
return Resource.App.stage !== "production"
|
||||
}, "black.subscribe.enabled")
|
||||
|
||||
const plansMap = Object.fromEntries(plans.map((p) => [p.id, p])) as Record<PlanID, (typeof plans)[number]>
|
||||
const stripePromise = loadStripe(import.meta.env.VITE_STRIPE_PUBLISHABLE_KEY!)
|
||||
|
||||
const getWorkspaces = query(async (plan: string) => {
|
||||
"use server"
|
||||
const actor = await getActor()
|
||||
if (actor.type === "public") throw redirect("/auth/authorize?continue=/black/subscribe/" + plan)
|
||||
return withActor(async () => {
|
||||
return Database.use((tx) =>
|
||||
tx
|
||||
.select({
|
||||
id: WorkspaceTable.id,
|
||||
name: WorkspaceTable.name,
|
||||
slug: WorkspaceTable.slug,
|
||||
billing: {
|
||||
customerID: BillingTable.customerID,
|
||||
paymentMethodID: BillingTable.paymentMethodID,
|
||||
paymentMethodType: BillingTable.paymentMethodType,
|
||||
paymentMethodLast4: BillingTable.paymentMethodLast4,
|
||||
subscriptionID: BillingTable.subscriptionID,
|
||||
timeSubscriptionBooked: BillingTable.timeSubscriptionBooked,
|
||||
},
|
||||
})
|
||||
.from(UserTable)
|
||||
.innerJoin(WorkspaceTable, eq(UserTable.workspaceID, WorkspaceTable.id))
|
||||
.innerJoin(BillingTable, eq(WorkspaceTable.id, BillingTable.workspaceID))
|
||||
.where(
|
||||
and(
|
||||
eq(UserTable.accountID, Actor.account()),
|
||||
isNull(WorkspaceTable.timeDeleted),
|
||||
isNull(UserTable.timeDeleted),
|
||||
),
|
||||
),
|
||||
)
|
||||
})
|
||||
}, "black.subscribe.workspaces")
|
||||
|
||||
const createSetupIntent = async (input: { plan: string; workspaceID: string }) => {
|
||||
"use server"
|
||||
const { plan, workspaceID } = input
|
||||
|
||||
if (!plan || !["20", "100", "200"].includes(plan)) return { error: formError.invalidPlan }
|
||||
if (!workspaceID) return { error: formError.workspaceRequired }
|
||||
|
||||
return withActor(async () => {
|
||||
const session = await useAuthSession()
|
||||
const account = session.data.account?.[session.data.current ?? ""]
|
||||
const email = account?.email
|
||||
|
||||
const customer = await Database.use((tx) =>
|
||||
tx
|
||||
.select({
|
||||
customerID: BillingTable.customerID,
|
||||
subscriptionID: BillingTable.subscriptionID,
|
||||
})
|
||||
.from(BillingTable)
|
||||
.where(eq(BillingTable.workspaceID, workspaceID))
|
||||
.then((rows) => rows[0]),
|
||||
)
|
||||
if (customer?.subscriptionID) {
|
||||
return { error: formError.alreadySubscribed }
|
||||
}
|
||||
|
||||
let customerID = customer?.customerID
|
||||
if (!customerID) {
|
||||
const customer = await Billing.stripe().customers.create({
|
||||
email,
|
||||
metadata: {
|
||||
workspaceID,
|
||||
},
|
||||
})
|
||||
customerID = customer.id
|
||||
await Database.use((tx) =>
|
||||
tx
|
||||
.update(BillingTable)
|
||||
.set({
|
||||
customerID,
|
||||
})
|
||||
.where(eq(BillingTable.workspaceID, workspaceID)),
|
||||
)
|
||||
}
|
||||
|
||||
const intent = await Billing.stripe().setupIntents.create({
|
||||
customer: customerID,
|
||||
payment_method_types: ["card"],
|
||||
metadata: {
|
||||
workspaceID,
|
||||
},
|
||||
})
|
||||
|
||||
return { clientSecret: intent.client_secret ?? undefined }
|
||||
}, workspaceID)
|
||||
}
|
||||
|
||||
const bookSubscription = async (input: {
|
||||
workspaceID: string
|
||||
plan: PlanID
|
||||
paymentMethodID: string
|
||||
paymentMethodType: string
|
||||
paymentMethodLast4?: string
|
||||
}) => {
|
||||
"use server"
|
||||
return withActor(
|
||||
() =>
|
||||
Database.use((tx) =>
|
||||
tx
|
||||
.update(BillingTable)
|
||||
.set({
|
||||
paymentMethodID: input.paymentMethodID,
|
||||
paymentMethodType: input.paymentMethodType,
|
||||
paymentMethodLast4: input.paymentMethodLast4,
|
||||
subscriptionPlan: input.plan,
|
||||
timeSubscriptionBooked: new Date(),
|
||||
})
|
||||
.where(eq(BillingTable.workspaceID, input.workspaceID)),
|
||||
),
|
||||
input.workspaceID,
|
||||
)
|
||||
}
|
||||
|
||||
interface SuccessData {
|
||||
plan: string
|
||||
paymentMethodType: string
|
||||
paymentMethodLast4?: string
|
||||
}
|
||||
|
||||
function Failure(props: { message: string }) {
|
||||
const i18n = useI18n()
|
||||
|
||||
return (
|
||||
<div data-slot="failure">
|
||||
<p data-slot="message">
|
||||
{i18n.t("black.subscribe.failurePrefix")} {props.message}
|
||||
</p>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function Success(props: SuccessData) {
|
||||
const i18n = useI18n()
|
||||
|
||||
return (
|
||||
<div data-slot="success">
|
||||
<p data-slot="title">{i18n.t("black.subscribe.success.title")}</p>
|
||||
<dl data-slot="details">
|
||||
<div>
|
||||
<dt>{i18n.t("black.subscribe.success.subscriptionPlan")}</dt>
|
||||
<dd>{i18n.t("black.subscribe.success.planName", { plan: props.plan })}</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt>{i18n.t("black.subscribe.success.amount")}</dt>
|
||||
<dd>{i18n.t("black.subscribe.success.amountValue", { plan: props.plan })}</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt>{i18n.t("black.subscribe.success.paymentMethod")}</dt>
|
||||
<dd>
|
||||
<Show when={props.paymentMethodLast4} fallback={<span>{props.paymentMethodType}</span>}>
|
||||
<span>
|
||||
{props.paymentMethodType} - {props.paymentMethodLast4}
|
||||
</span>
|
||||
</Show>
|
||||
</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt>{i18n.t("black.subscribe.success.dateJoined")}</dt>
|
||||
<dd>{new Date().toLocaleDateString(undefined, { month: "short", day: "numeric", year: "numeric" })}</dd>
|
||||
</div>
|
||||
</dl>
|
||||
<p data-slot="charge-notice">{i18n.t("black.subscribe.success.chargeNotice")}</p>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function IntentForm(props: { plan: PlanID; workspaceID: string; onSuccess: (data: SuccessData) => void }) {
|
||||
const i18n = useI18n()
|
||||
const stripe = useStripe()
|
||||
const elements = useElements()
|
||||
const [error, setError] = createSignal<string | undefined>(undefined)
|
||||
const [loading, setLoading] = createSignal(false)
|
||||
|
||||
const handleSubmit = async (e: Event) => {
|
||||
e.preventDefault()
|
||||
if (!stripe() || !elements()) return
|
||||
|
||||
setLoading(true)
|
||||
setError(undefined)
|
||||
|
||||
const result = await elements()!.submit()
|
||||
if (result.error) {
|
||||
setError(result.error.message ?? i18n.t("black.subscribe.error.generic"))
|
||||
setLoading(false)
|
||||
return
|
||||
}
|
||||
|
||||
const { error: confirmError, setupIntent } = await stripe()!.confirmSetup({
|
||||
elements: elements()!,
|
||||
confirmParams: {
|
||||
expand: ["payment_method"],
|
||||
payment_method_data: {
|
||||
allow_redisplay: "always",
|
||||
},
|
||||
},
|
||||
redirect: "if_required",
|
||||
})
|
||||
|
||||
if (confirmError) {
|
||||
setError(confirmError.message ?? i18n.t("black.subscribe.error.generic"))
|
||||
setLoading(false)
|
||||
return
|
||||
}
|
||||
|
||||
if (setupIntent?.status === "succeeded") {
|
||||
const pm = setupIntent.payment_method as PaymentMethod
|
||||
|
||||
await bookSubscription({
|
||||
workspaceID: props.workspaceID,
|
||||
plan: props.plan,
|
||||
paymentMethodID: pm.id,
|
||||
paymentMethodType: pm.type,
|
||||
paymentMethodLast4: pm.card?.last4,
|
||||
})
|
||||
|
||||
props.onSuccess({
|
||||
plan: props.plan,
|
||||
paymentMethodType: pm.type,
|
||||
paymentMethodLast4: pm.card?.last4,
|
||||
})
|
||||
}
|
||||
|
||||
setLoading(false)
|
||||
}
|
||||
|
||||
return (
|
||||
<form onSubmit={handleSubmit} data-slot="checkout-form">
|
||||
<PaymentElement />
|
||||
<AddressElement options={{ mode: "billing" }} />
|
||||
<Show when={error()}>
|
||||
<p data-slot="error">{error()}</p>
|
||||
</Show>
|
||||
<button type="submit" disabled={loading() || !stripe() || !elements()} data-slot="submit-button">
|
||||
{loading() ? i18n.t("black.subscribe.processing") : i18n.t("black.subscribe.submit", { plan: props.plan })}
|
||||
</button>
|
||||
<p data-slot="charge-notice">{i18n.t("black.subscribe.form.chargeNotice")}</p>
|
||||
</form>
|
||||
)
|
||||
}
|
||||
|
||||
export default function BlackSubscribe() {
|
||||
const params = useParams()
|
||||
const i18n = useI18n()
|
||||
const language = useLanguage()
|
||||
const enabled = createAsync(() => getEnabled())
|
||||
const planData = plansMap[(params.plan as PlanID) ?? "20"] ?? plansMap["20"]
|
||||
const plan = planData.id
|
||||
|
||||
const workspaces = createAsync(() => getWorkspaces(plan))
|
||||
const [selectedWorkspace, setSelectedWorkspace] = createSignal<string | undefined>(undefined)
|
||||
const [success, setSuccess] = createSignal<SuccessData | undefined>(undefined)
|
||||
const [failure, setFailure] = createSignal<string | undefined>(undefined)
|
||||
const [clientSecret, setClientSecret] = createSignal<string | undefined>(undefined)
|
||||
const [stripe, setStripe] = createSignal<Stripe | undefined>(undefined)
|
||||
|
||||
const formatError = (error: string) => {
|
||||
if (error === formError.invalidPlan) return i18n.t("black.subscribe.error.invalidPlan")
|
||||
if (error === formError.workspaceRequired) return i18n.t("black.subscribe.error.workspaceRequired")
|
||||
if (error === formError.alreadySubscribed) return i18n.t("black.subscribe.error.alreadySubscribed")
|
||||
if (error === "Invalid plan") return i18n.t("black.subscribe.error.invalidPlan")
|
||||
if (error === "Workspace ID is required") return i18n.t("black.subscribe.error.workspaceRequired")
|
||||
if (error === "This workspace already has a subscription") return i18n.t("black.subscribe.error.alreadySubscribed")
|
||||
return error
|
||||
}
|
||||
|
||||
// Resolve stripe promise once
|
||||
createEffect(() => {
|
||||
void stripePromise.then((s) => {
|
||||
if (s) setStripe(s)
|
||||
})
|
||||
})
|
||||
|
||||
// Auto-select if only one workspace
|
||||
createEffect(() => {
|
||||
const ws = workspaces()
|
||||
if (ws?.length === 1 && !selectedWorkspace()) {
|
||||
setSelectedWorkspace(ws[0].id)
|
||||
}
|
||||
})
|
||||
|
||||
// Fetch setup intent when workspace is selected (unless workspace already has payment method)
|
||||
createEffect(async () => {
|
||||
const id = selectedWorkspace()
|
||||
if (!id) return
|
||||
|
||||
const ws = workspaces()?.find((w) => w.id === id)
|
||||
if (ws?.billing?.subscriptionID) {
|
||||
setFailure(i18n.t("black.subscribe.error.alreadySubscribed"))
|
||||
return
|
||||
}
|
||||
if (ws?.billing?.paymentMethodID) {
|
||||
if (!ws?.billing?.timeSubscriptionBooked) {
|
||||
await bookSubscription({
|
||||
workspaceID: id,
|
||||
plan: planData.id,
|
||||
paymentMethodID: ws.billing.paymentMethodID!,
|
||||
paymentMethodType: ws.billing.paymentMethodType!,
|
||||
paymentMethodLast4: ws.billing.paymentMethodLast4 ?? undefined,
|
||||
})
|
||||
}
|
||||
setSuccess({
|
||||
plan: planData.id,
|
||||
paymentMethodType: ws.billing.paymentMethodType!,
|
||||
paymentMethodLast4: ws.billing.paymentMethodLast4 ?? undefined,
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
const result = await createSetupIntent({ plan, workspaceID: id })
|
||||
if (result.error) {
|
||||
setFailure(formatError(result.error))
|
||||
} else if ("clientSecret" in result) {
|
||||
setClientSecret(result.clientSecret)
|
||||
}
|
||||
})
|
||||
|
||||
// Keyboard navigation for workspace picker
|
||||
const { active, setActive, onKeyDown } = createList({
|
||||
items: () => workspaces()?.map((w) => w.id) ?? [],
|
||||
initialActive: null,
|
||||
})
|
||||
|
||||
const handleSelectWorkspace = (id: string) => {
|
||||
setSelectedWorkspace(id)
|
||||
}
|
||||
|
||||
let listRef: HTMLUListElement | undefined
|
||||
|
||||
// Show workspace picker if multiple workspaces and none selected
|
||||
const showWorkspacePicker = () => {
|
||||
const ws = workspaces()
|
||||
return ws && ws.length > 1 && !selectedWorkspace()
|
||||
}
|
||||
|
||||
return (
|
||||
<Show when={enabled()}>
|
||||
<Title>{i18n.t("black.subscribe.title")}</Title>
|
||||
<section data-slot="subscribe-form">
|
||||
<div data-slot="form-card">
|
||||
<Switch>
|
||||
<Match when={success()}>{(data) => <Success {...data()} />}</Match>
|
||||
<Match when={failure()}>{(data) => <Failure message={data()} />}</Match>
|
||||
<Match when={true}>
|
||||
<>
|
||||
<div data-slot="plan-header">
|
||||
<p data-slot="title">{i18n.t("black.subscribe.title")}</p>
|
||||
<p data-slot="price">
|
||||
<span data-slot="amount">${planData.id}</span>{" "}
|
||||
<span data-slot="period">{i18n.t("black.price.perMonth")}</span>
|
||||
<Show when={planData.multiplier}>
|
||||
{(multiplier) => <span data-slot="multiplier">{i18n.t(multiplier())}</span>}
|
||||
</Show>
|
||||
</p>
|
||||
</div>
|
||||
<div data-slot="divider" />
|
||||
<p data-slot="section-title">{i18n.t("black.subscribe.paymentMethod")}</p>
|
||||
|
||||
<Show
|
||||
when={clientSecret() && selectedWorkspace() && stripe()}
|
||||
fallback={
|
||||
<div data-slot="loading">
|
||||
<p>
|
||||
{selectedWorkspace()
|
||||
? i18n.t("black.subscribe.loadingPaymentForm")
|
||||
: i18n.t("black.subscribe.selectWorkspaceToContinue")}
|
||||
</p>
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<Elements
|
||||
stripe={stripe()!}
|
||||
options={{
|
||||
clientSecret: clientSecret()!,
|
||||
appearance: {
|
||||
theme: "night",
|
||||
variables: {
|
||||
colorPrimary: "#ffffff",
|
||||
colorBackground: "#1a1a1a",
|
||||
colorText: "#ffffff",
|
||||
colorTextSecondary: "#999999",
|
||||
colorDanger: "#ff6b6b",
|
||||
fontFamily: "JetBrains Mono, monospace",
|
||||
borderRadius: "4px",
|
||||
spacingUnit: "4px",
|
||||
},
|
||||
rules: {
|
||||
".Input": {
|
||||
backgroundColor: "#1a1a1a",
|
||||
border: "1px solid rgba(255, 255, 255, 0.17)",
|
||||
color: "#ffffff",
|
||||
},
|
||||
".Input:focus": {
|
||||
borderColor: "rgba(255, 255, 255, 0.35)",
|
||||
boxShadow: "none",
|
||||
},
|
||||
".Label": {
|
||||
color: "rgba(255, 255, 255, 0.59)",
|
||||
fontSize: "14px",
|
||||
marginBottom: "8px",
|
||||
},
|
||||
},
|
||||
},
|
||||
}}
|
||||
>
|
||||
<IntentForm plan={plan} workspaceID={selectedWorkspace()!} onSuccess={setSuccess} />
|
||||
</Elements>
|
||||
</Show>
|
||||
</>
|
||||
</Match>
|
||||
</Switch>
|
||||
</div>
|
||||
|
||||
{/* Workspace picker modal */}
|
||||
<Modal
|
||||
open={showWorkspacePicker() ?? false}
|
||||
onClose={() => {}}
|
||||
title={i18n.t("black.workspace.selectPlan")}
|
||||
variant="black"
|
||||
>
|
||||
<div data-component="black-workspace-picker-modal" data-slot="workspace-picker">
|
||||
<ul
|
||||
ref={listRef}
|
||||
data-slot="workspace-list"
|
||||
tabIndex={0}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === "Enter" && active()) {
|
||||
handleSelectWorkspace(active()!)
|
||||
} else {
|
||||
onKeyDown(e)
|
||||
}
|
||||
}}
|
||||
>
|
||||
<For each={workspaces()}>
|
||||
{(workspace) => (
|
||||
<li
|
||||
data-slot="workspace-item"
|
||||
data-active={active() === workspace.id}
|
||||
onMouseEnter={() => setActive(workspace.id)}
|
||||
onClick={() => handleSelectWorkspace(workspace.id)}
|
||||
>
|
||||
<span data-slot="selected-icon">[*]</span>
|
||||
<span>{workspace.name || workspace.slug}</span>
|
||||
</li>
|
||||
)}
|
||||
</For>
|
||||
</ul>
|
||||
</div>
|
||||
</Modal>
|
||||
<p data-slot="fine-print">
|
||||
{i18n.t("black.finePrint.beforeTerms")} ·{" "}
|
||||
<A href={language.route("/legal/terms-of-service")}>{i18n.t("black.finePrint.terms")}</A>
|
||||
</p>
|
||||
</section>
|
||||
</Show>
|
||||
)
|
||||
}
|
||||
@@ -327,37 +327,6 @@ body {
|
||||
}
|
||||
}
|
||||
|
||||
[data-component="desktop-app-banner"] {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
margin-bottom: 32px;
|
||||
|
||||
[data-slot="badge"] {
|
||||
background: var(--color-background-strong);
|
||||
color: var(--color-text-inverted);
|
||||
font-weight: 500;
|
||||
padding: 4px 8px;
|
||||
line-height: 1;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
[data-slot="content"] {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 1ch;
|
||||
}
|
||||
|
||||
[data-slot="text"] {
|
||||
color: var(--color-text-strong);
|
||||
line-height: 1.4;
|
||||
|
||||
@media (max-width: 30.625rem) {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[data-slot="hero-copy"] {
|
||||
img {
|
||||
margin-bottom: 24px;
|
||||
|
||||
@@ -23,7 +23,7 @@ const checkLoggedIn = query(async () => {
|
||||
}, "checkLoggedIn.get")
|
||||
|
||||
const models = [
|
||||
{ name: "Grok 4.5", training: "go.faq.a5.notUsed", retention: "go.faq.a5.retention30" },
|
||||
{ name: "Grok 4.6", training: "go.faq.a5.notUsed", retention: "go.faq.a5.retention30" },
|
||||
{ name: "GPT 5.6 Luna", training: "go.faq.a5.notUsed", retention: "go.faq.a5.retention30" },
|
||||
{ name: "GLM-5.3", training: "go.faq.a5.notUsed", retention: "go.faq.a5.retention0" },
|
||||
{ name: "GLM-5.2", training: "go.faq.a5.notUsed", retention: "go.faq.a5.retention0" },
|
||||
@@ -70,8 +70,8 @@ function LimitsGraph(props: { href: string }) {
|
||||
const baseline = 100
|
||||
const graph = [
|
||||
{ id: "kimi-k3", name: "Kimi K3", req: 110, d: "50ms" },
|
||||
{ id: "grok-4.5", name: "Grok 4.5", req: 120, d: "75ms" },
|
||||
{ id: "qwen3.8-max", name: "Qwen3.8 Max", req: 160, d: "90ms" },
|
||||
{ id: "grok-4.6", name: "Grok 4.6", req: 169, d: "75ms" },
|
||||
{ id: "glm-5.2", name: "GLM-5.2", req: 880, d: "100ms" },
|
||||
{ id: "gpt-5.6-luna", name: "GPT 5.6 Luna", req: 2050, d: "290ms" },
|
||||
{ id: "minimax-m3", name: "MiniMax M3", req: 3200, d: "210ms" },
|
||||
@@ -81,7 +81,6 @@ function LimitsGraph(props: { href: string }) {
|
||||
{ id: "mimo-v2.5", name: "MiMo-V2.5", req: 30100, d: "340ms" },
|
||||
{ id: "hy3", name: "Hy3", req: 34400, baseReq: 4300, d: "320ms" },
|
||||
{ id: "muse-spark-1.2-contributor", name: "Muse Spark 1.2 Contributor", req: 45300, edge: true, d: "360ms" },
|
||||
{ id: "ox-alpha-free", name: "Ox Alpha Free", req: Infinity, infinite: true, edge: true, d: "400ms" },
|
||||
]
|
||||
|
||||
const w = 1040
|
||||
@@ -270,12 +269,6 @@ export default function Home() {
|
||||
|
||||
<div data-component="content">
|
||||
<section data-component="hero">
|
||||
<div data-component="desktop-app-banner">
|
||||
<span data-slot="badge">{i18n.t("home.banner.badge")}</span>
|
||||
<div data-slot="content">
|
||||
<span data-slot="text">{i18n.t("go.banner.text")}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div data-slot="hero-copy">
|
||||
<img data-slot="zen logo light" src={goLogoLight} alt="" />
|
||||
<img data-slot="zen logo dark" src={goLogoDark} alt="" />
|
||||
@@ -511,7 +504,7 @@ export default function Home() {
|
||||
</div>
|
||||
<div data-slot="faq-retention-notes">
|
||||
<p>
|
||||
<strong>Grok 4.5:</strong> {i18n.t("go.faq.a5.grokRetention")}{" "}
|
||||
<strong>Grok 4.6:</strong> {i18n.t("go.faq.a5.grokRetention")}{" "}
|
||||
<a href="https://docs.x.ai/developers/faq/security#what-is-zero-data-retention-zdr">
|
||||
{i18n.t("go.faq.a5.learnMore")}
|
||||
</a>
|
||||
|
||||
@@ -205,6 +205,13 @@ export async function POST(input: APIEvent) {
|
||||
} else if (productID === BlackData.productID()) {
|
||||
await Billing.unsubscribeBlack({ subscriptionID })
|
||||
}
|
||||
|
||||
const latestInvoice = body.data.object.latest_invoice
|
||||
const invoiceID = typeof latestInvoice === "string" ? latestInvoice : latestInvoice?.id
|
||||
if (invoiceID) {
|
||||
const invoice = await Billing.stripe().invoices.retrieve(invoiceID)
|
||||
if (invoice.status === "open") await Billing.stripe().invoices.voidInvoice(invoiceID)
|
||||
}
|
||||
}
|
||||
if (body.type === "invoice.payment_succeeded") {
|
||||
if (
|
||||
|
||||
@@ -19,6 +19,7 @@
|
||||
|
||||
[data-slot="usage-item"] {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--space-2);
|
||||
@@ -60,6 +61,119 @@
|
||||
color: var(--color-text-muted);
|
||||
}
|
||||
|
||||
[data-slot="usage-details"] {
|
||||
margin-top: var(--space-1);
|
||||
}
|
||||
|
||||
[data-slot="usage-details-trigger"] {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: var(--space-2);
|
||||
width: fit-content;
|
||||
padding: 0;
|
||||
border: 0;
|
||||
background-color: transparent;
|
||||
color: var(--color-text-secondary);
|
||||
font-size: var(--font-size-sm);
|
||||
cursor: pointer;
|
||||
|
||||
&:hover:not(:disabled) {
|
||||
background-color: transparent;
|
||||
border-color: transparent;
|
||||
}
|
||||
|
||||
svg {
|
||||
transition: transform 0.2s ease;
|
||||
}
|
||||
|
||||
&[aria-expanded="true"] svg {
|
||||
transform: rotate(180deg);
|
||||
}
|
||||
|
||||
[data-slot="hide-details"] {
|
||||
display: none;
|
||||
}
|
||||
|
||||
&[aria-expanded="true"] [data-slot="show-details"] {
|
||||
display: none;
|
||||
}
|
||||
|
||||
&[aria-expanded="true"] [data-slot="hide-details"] {
|
||||
display: inline;
|
||||
}
|
||||
}
|
||||
|
||||
[data-slot="usage-details-content"] {
|
||||
width: 100%;
|
||||
margin-top: var(--space-3);
|
||||
}
|
||||
|
||||
[data-slot="usage-details-loading"] {
|
||||
width: 100%;
|
||||
margin-top: var(--space-3);
|
||||
color: var(--color-text-muted);
|
||||
font-size: var(--font-size-sm);
|
||||
}
|
||||
|
||||
[data-slot="usage-details-empty"] {
|
||||
margin: 0;
|
||||
color: var(--color-text-muted);
|
||||
font-size: var(--font-size-sm);
|
||||
}
|
||||
|
||||
[data-slot="usage-details-table"] {
|
||||
overflow-x: auto;
|
||||
border: 1px solid var(--color-border-muted);
|
||||
border-radius: var(--border-radius-sm);
|
||||
|
||||
table {
|
||||
width: 100%;
|
||||
min-width: 28rem;
|
||||
table-layout: fixed;
|
||||
border-collapse: collapse;
|
||||
font-size: var(--font-size-sm);
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
th,
|
||||
td {
|
||||
width: 25%;
|
||||
padding: var(--space-2) var(--space-3);
|
||||
border-bottom: 1px solid var(--color-border-muted);
|
||||
text-align: end;
|
||||
font-variant-numeric: tabular-nums;
|
||||
}
|
||||
|
||||
th {
|
||||
color: var(--color-text-muted);
|
||||
font-size: var(--font-size-xs);
|
||||
font-weight: 500;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
th:first-child,
|
||||
td:first-child {
|
||||
text-align: start;
|
||||
}
|
||||
|
||||
td:first-child {
|
||||
max-width: 13rem;
|
||||
overflow: hidden;
|
||||
color: var(--color-text);
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
tbody tr:last-child td {
|
||||
border-bottom: 0;
|
||||
}
|
||||
|
||||
[data-slot="usage-total"] td {
|
||||
border-top: 1px solid var(--color-border);
|
||||
color: var(--color-text);
|
||||
font-weight: 600;
|
||||
}
|
||||
}
|
||||
|
||||
[data-slot="setting-row"] {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
@@ -1,15 +1,19 @@
|
||||
import { action, useParams, useAction, useSubmission, json, query, createAsync } from "@solidjs/router"
|
||||
import { createStore } from "solid-js/store"
|
||||
import { createMemo, For, Show } from "solid-js"
|
||||
import { createMemo, createSignal, For, Show } from "solid-js"
|
||||
import { Modal } from "~/component/modal"
|
||||
import { Billing } from "@opencode-ai/console-core/billing.js"
|
||||
import { Database, eq, and, isNull } from "@opencode-ai/console-core/drizzle/index.js"
|
||||
import { BillingTable, LiteTable } from "@opencode-ai/console-core/schema/billing.sql.js"
|
||||
import { Database, eq, and, gte, isNull, sql } from "@opencode-ai/console-core/drizzle/index.js"
|
||||
import { BillingTable, LiteTable, UsageTable } from "@opencode-ai/console-core/schema/billing.sql.js"
|
||||
import { KeyTable } from "@opencode-ai/console-core/schema/key.sql.js"
|
||||
import { WorkspaceTable } from "@opencode-ai/console-core/schema/workspace.sql.js"
|
||||
import { Actor } from "@opencode-ai/console-core/actor.js"
|
||||
import { Workspace } from "@opencode-ai/console-core/workspace.js"
|
||||
import { Subscription } from "@opencode-ai/console-core/subscription.js"
|
||||
import { LiteData } from "@opencode-ai/console-core/lite.js"
|
||||
import { ZenData } from "@opencode-ai/console-core/model.js"
|
||||
import { getMonthlyBounds, getWeekBounds } from "@opencode-ai/console-core/util/date.js"
|
||||
import { centsToMicroCents } from "@opencode-ai/console-core/util/price.js"
|
||||
import { withActor } from "~/context/auth.withActor"
|
||||
import { queryBillingInfo } from "../../common"
|
||||
import styles from "./lite-section.module.css"
|
||||
@@ -20,8 +24,12 @@ import { formatResetTime, liteResetTimeKeys } from "~/lib/format-reset-time"
|
||||
import { createReferralFromCookie } from "~/lib/referral-invite"
|
||||
import { getRequestEvent } from "solid-js/web"
|
||||
import { countryFromRequest } from "~/lib/request-country"
|
||||
import { checkCheckoutRateLimit } from "~/routes/zen/util/redis"
|
||||
|
||||
import { IconAlipay, IconUpi } from "~/component/icon"
|
||||
import { IconAlipay, IconChevron, IconUpi } from "~/component/icon"
|
||||
import { buildLiteUsageBreakdown, getModelQuotaLimit, getUsagePercent } from "~/lib/lite-usage"
|
||||
|
||||
type LiteUsageWindow = "rolling" | "weekly" | "monthly"
|
||||
|
||||
export const queryLiteSubscription = query(async (workspaceID: string) => {
|
||||
"use server"
|
||||
@@ -51,6 +59,33 @@ export const queryLiteSubscription = query(async (workspaceID: string) => {
|
||||
|
||||
const limits = LiteData.getLimits()
|
||||
const mine = row.userID === Actor.userID()
|
||||
const now = new Date()
|
||||
const rollingCutoff = new Date(now.getTime() - limits.rollingWindow * 3600 * 1000)
|
||||
const week = getWeekBounds(now)
|
||||
const month = getMonthlyBounds(now, row.timeCreated)
|
||||
const rollingActive = !!row.timeRollingUpdated && row.timeRollingUpdated >= rollingCutoff
|
||||
const weeklyActive = !!row.timeWeeklyUpdated && row.timeWeeklyUpdated >= week.start
|
||||
const monthlyActive = !!row.timeMonthlyUpdated && row.timeMonthlyUpdated >= month.start
|
||||
const rollingLimit = centsToMicroCents(limits.rollingLimit * 100)
|
||||
const weeklyLimit = centsToMicroCents(limits.weeklyLimit * 100)
|
||||
const monthlyLimit = centsToMicroCents(limits.monthlyLimit * 100)
|
||||
const rollingUsage = Subscription.analyzeRollingUsage({
|
||||
limit: limits.rollingLimit,
|
||||
window: limits.rollingWindow,
|
||||
usage: row.rollingUsage ?? 0,
|
||||
timeUpdated: row.timeRollingUpdated ?? now,
|
||||
})
|
||||
const weeklyUsage = Subscription.analyzeWeeklyUsage({
|
||||
limit: limits.weeklyLimit,
|
||||
usage: row.weeklyUsage ?? 0,
|
||||
timeUpdated: row.timeWeeklyUpdated ?? now,
|
||||
})
|
||||
const monthlyUsage = Subscription.analyzeMonthlyUsage({
|
||||
limit: limits.monthlyLimit,
|
||||
usage: row.monthlyUsage ?? 0,
|
||||
timeUpdated: row.timeMonthlyUpdated ?? now,
|
||||
timeSubscribed: row.timeCreated,
|
||||
})
|
||||
|
||||
return {
|
||||
mine,
|
||||
@@ -58,27 +93,126 @@ export const queryLiteSubscription = query(async (workspaceID: string) => {
|
||||
allowTraining: row.allowTraining ?? false,
|
||||
region:
|
||||
row.region ?? (await Workspace.setDefaultRegion({ country: countryFromRequest(getRequestEvent()?.request) })),
|
||||
rollingUsage: Subscription.analyzeRollingUsage({
|
||||
limit: limits.rollingLimit,
|
||||
window: limits.rollingWindow,
|
||||
usage: row.rollingUsage ?? 0,
|
||||
timeUpdated: row.timeRollingUpdated ?? new Date(),
|
||||
}),
|
||||
weeklyUsage: Subscription.analyzeWeeklyUsage({
|
||||
limit: limits.weeklyLimit,
|
||||
usage: row.weeklyUsage ?? 0,
|
||||
timeUpdated: row.timeWeeklyUpdated ?? new Date(),
|
||||
}),
|
||||
monthlyUsage: Subscription.analyzeMonthlyUsage({
|
||||
limit: limits.monthlyLimit,
|
||||
usage: row.monthlyUsage ?? 0,
|
||||
timeUpdated: row.timeMonthlyUpdated ?? new Date(),
|
||||
timeSubscribed: row.timeCreated,
|
||||
}),
|
||||
rollingUsage: {
|
||||
...rollingUsage,
|
||||
usage: rollingActive ? (row.rollingUsage ?? 0) : 0,
|
||||
limit: rollingLimit,
|
||||
usagePercent: getUsagePercent(rollingActive ? (row.rollingUsage ?? 0) : 0, rollingLimit),
|
||||
},
|
||||
weeklyUsage: {
|
||||
...weeklyUsage,
|
||||
usage: weeklyActive ? (row.weeklyUsage ?? 0) : 0,
|
||||
limit: weeklyLimit,
|
||||
usagePercent: getUsagePercent(weeklyActive ? (row.weeklyUsage ?? 0) : 0, weeklyLimit),
|
||||
},
|
||||
monthlyUsage: {
|
||||
...monthlyUsage,
|
||||
usage: monthlyActive ? (row.monthlyUsage ?? 0) : 0,
|
||||
limit: monthlyLimit,
|
||||
usagePercent: getUsagePercent(monthlyActive ? (row.monthlyUsage ?? 0) : 0, monthlyLimit),
|
||||
},
|
||||
}
|
||||
}, workspaceID)
|
||||
}, "lite.subscription.get")
|
||||
|
||||
export const queryLiteUsageDetails = query(async (workspaceID: string, window: LiteUsageWindow) => {
|
||||
"use server"
|
||||
return withActor(async () => {
|
||||
if (window !== "rolling" && window !== "weekly" && window !== "monthly") return null
|
||||
const row = await Database.use((tx) =>
|
||||
tx
|
||||
.select({
|
||||
userID: LiteTable.userID,
|
||||
rollingUsage: LiteTable.rollingUsage,
|
||||
weeklyUsage: LiteTable.weeklyUsage,
|
||||
monthlyUsage: LiteTable.monthlyUsage,
|
||||
timeRollingUpdated: LiteTable.timeRollingUpdated,
|
||||
timeWeeklyUpdated: LiteTable.timeWeeklyUpdated,
|
||||
timeMonthlyUpdated: LiteTable.timeMonthlyUpdated,
|
||||
timeCreated: LiteTable.timeCreated,
|
||||
})
|
||||
.from(LiteTable)
|
||||
.where(and(eq(LiteTable.workspaceID, Actor.workspace()), isNull(LiteTable.timeDeleted)))
|
||||
.then((result) => result[0]),
|
||||
)
|
||||
if (!row || row.userID !== Actor.userID()) return null
|
||||
|
||||
const limits = LiteData.getLimits()
|
||||
const now = new Date()
|
||||
const detail = (() => {
|
||||
if (window === "rolling") {
|
||||
const active =
|
||||
!!row.timeRollingUpdated &&
|
||||
row.timeRollingUpdated >= new Date(now.getTime() - limits.rollingWindow * 3600 * 1000)
|
||||
return {
|
||||
start: active ? row.timeRollingUpdated! : now,
|
||||
usage: active ? (row.rollingUsage ?? 0) : 0,
|
||||
limit: centsToMicroCents(limits.rollingLimit * 100),
|
||||
}
|
||||
}
|
||||
if (window === "weekly") {
|
||||
const start = getWeekBounds(now).start
|
||||
return {
|
||||
start,
|
||||
usage: row.timeWeeklyUpdated && row.timeWeeklyUpdated >= start ? (row.weeklyUsage ?? 0) : 0,
|
||||
limit: centsToMicroCents(limits.weeklyLimit * 100),
|
||||
}
|
||||
}
|
||||
const start = getMonthlyBounds(now, row.timeCreated).start
|
||||
return {
|
||||
start,
|
||||
usage: row.timeMonthlyUpdated && row.timeMonthlyUpdated >= start ? (row.monthlyUsage ?? 0) : 0,
|
||||
limit: centsToMicroCents(limits.monthlyLimit * 100),
|
||||
}
|
||||
})()
|
||||
const modelData = Object.fromEntries(
|
||||
Object.entries(ZenData.list("lite").models).map(([id, value]) => {
|
||||
const models = Array.isArray(value) ? value : [value]
|
||||
const multipliers = new Set(models.map((model) => model.costMultiplier))
|
||||
return [id, { name: models[0].name, multiplier: multipliers.size === 1 ? models[0].costMultiplier : undefined }]
|
||||
}),
|
||||
)
|
||||
const usageRows = await Database.use((tx) =>
|
||||
tx
|
||||
.select({
|
||||
model: UsageTable.model,
|
||||
multiplier: sql<string | null>`JSON_UNQUOTE(JSON_EXTRACT(${UsageTable.enrichment}, '$.costMultiplier'))`,
|
||||
cost: sql<string>`SUM(${UsageTable.cost})`,
|
||||
quotaCost: sql<string>`SUM(CASE WHEN JSON_EXTRACT(${UsageTable.enrichment}, '$.costMultiplier') IS NOT NULL THEN ROUND(${UsageTable.cost} * CAST(JSON_UNQUOTE(JSON_EXTRACT(${UsageTable.enrichment}, '$.costMultiplier')) AS DECIMAL(20, 8))) ELSE 0 END)`,
|
||||
})
|
||||
.from(UsageTable)
|
||||
.innerJoin(KeyTable, and(eq(KeyTable.id, UsageTable.keyID), eq(KeyTable.workspaceID, UsageTable.workspaceID)))
|
||||
.where(
|
||||
and(
|
||||
eq(UsageTable.workspaceID, Actor.workspace()),
|
||||
eq(KeyTable.userID, row.userID),
|
||||
gte(UsageTable.timeCreated, detail.start),
|
||||
sql`JSON_UNQUOTE(JSON_EXTRACT(${UsageTable.enrichment}, '$.plan')) = 'lite'`,
|
||||
),
|
||||
)
|
||||
.groupBy(UsageTable.model, sql`JSON_UNQUOTE(JSON_EXTRACT(${UsageTable.enrichment}, '$.costMultiplier'))`),
|
||||
)
|
||||
|
||||
return buildLiteUsageBreakdown({
|
||||
usage: detail.usage,
|
||||
limit: detail.limit,
|
||||
sources: usageRows.map((usage) => {
|
||||
const cost = Number(usage.cost)
|
||||
const info = modelData[usage.model]
|
||||
const multiplier = usage.multiplier === null ? info?.multiplier : Number(usage.multiplier)
|
||||
return {
|
||||
model: usage.model,
|
||||
name: info?.name ?? usage.model,
|
||||
cost,
|
||||
quotaCost: usage.multiplier === null ? Math.round(cost * (multiplier ?? 1)) : Number(usage.quotaCost),
|
||||
multiplier,
|
||||
estimated: usage.multiplier === null,
|
||||
}
|
||||
}),
|
||||
})
|
||||
}, workspaceID)
|
||||
}, "lite.subscription.usage")
|
||||
|
||||
type LiteSubscription = Awaited<ReturnType<typeof queryLiteSubscription>>
|
||||
|
||||
const createLiteCheckoutUrl = action(
|
||||
@@ -86,6 +220,7 @@ const createLiteCheckoutUrl = action(
|
||||
"use server"
|
||||
return json(
|
||||
await withActor(async () => {
|
||||
await checkCheckoutRateLimit(Actor.account())
|
||||
const data = await Billing.generateLiteCheckoutUrl({ successUrl, cancelUrl, method })
|
||||
await createReferralFromCookie()
|
||||
return { error: undefined, data }
|
||||
@@ -174,7 +309,16 @@ const setGoAllowTraining = action(async (form: FormData) => {
|
||||
)
|
||||
}, "go.allowTraining.set")
|
||||
|
||||
function LiteUsageItem(props: { label: string; usage: { usagePercent: number; resetInSec: number } }) {
|
||||
type LiteUsage = NonNullable<LiteSubscription>["rollingUsage"]
|
||||
type LiteUsageDetailsData = NonNullable<Awaited<ReturnType<typeof queryLiteUsageDetails>>>
|
||||
|
||||
function LiteUsageItem(props: {
|
||||
id: LiteUsageWindow
|
||||
label: string
|
||||
usage: LiteUsage
|
||||
open: boolean
|
||||
onToggle: () => void
|
||||
}) {
|
||||
const i18n = useI18n()
|
||||
|
||||
return (
|
||||
@@ -183,17 +327,178 @@ function LiteUsageItem(props: { label: string; usage: { usagePercent: number; re
|
||||
<span data-slot="usage-label">{props.label}</span>
|
||||
<span data-slot="usage-value">{props.usage.usagePercent}%</span>
|
||||
</div>
|
||||
<div data-slot="progress">
|
||||
<div data-slot="progress-bar" style={{ width: `${props.usage.usagePercent}%` }} />
|
||||
<div
|
||||
data-slot="progress"
|
||||
role="progressbar"
|
||||
aria-label={props.label}
|
||||
aria-valuemin="0"
|
||||
aria-valuemax="100"
|
||||
aria-valuenow={Math.min(100, props.usage.usagePercent)}
|
||||
>
|
||||
<div data-slot="progress-bar" style={{ width: `${Math.min(100, props.usage.usagePercent)}%` }} />
|
||||
</div>
|
||||
<span data-slot="reset-time">
|
||||
{i18n.t("workspace.lite.subscription.resetsIn")}{" "}
|
||||
{formatResetTime(props.usage.resetInSec, i18n, liteResetTimeKeys)}
|
||||
</span>
|
||||
<Show when={props.usage.usage > 0}>
|
||||
<div data-slot="usage-details">
|
||||
<button
|
||||
type="button"
|
||||
data-slot="usage-details-trigger"
|
||||
aria-expanded={props.open}
|
||||
aria-controls={`usage-details-${props.id}`}
|
||||
onClick={props.onToggle}
|
||||
>
|
||||
<span data-slot="show-details">{i18n.t("workspace.lite.subscription.showDetails")}</span>
|
||||
<span data-slot="hide-details">{i18n.t("workspace.lite.subscription.hideDetails")}</span>
|
||||
<IconChevron />
|
||||
</button>
|
||||
</div>
|
||||
</Show>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function LiteUsageDetails(props: {
|
||||
id: LiteUsageWindow
|
||||
label: string
|
||||
quotaLabel: string
|
||||
usage: LiteUsageDetailsData
|
||||
}) {
|
||||
const i18n = useI18n()
|
||||
const language = useLanguage()
|
||||
const money = (amount: number) =>
|
||||
new Intl.NumberFormat(language.tag(language.locale()), {
|
||||
style: "currency",
|
||||
currency: "USD",
|
||||
minimumFractionDigits: 2,
|
||||
maximumFractionDigits: 4,
|
||||
}).format(amount / 100_000_000)
|
||||
const totalPercentage = () =>
|
||||
Number(props.usage.rows.reduce((total, row) => total + row.contributionPercent, 0).toFixed(1))
|
||||
|
||||
return (
|
||||
<div id={`usage-details-${props.id}`} data-slot="usage-details-content" role="region" aria-label={props.label}>
|
||||
<div data-slot="usage-details-table">
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{i18n.t("workspace.lite.subscription.model")}</th>
|
||||
<th>{props.label}</th>
|
||||
<th>{props.quotaLabel}</th>
|
||||
<th>{i18n.t("workspace.lite.subscription.contribution")}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<For each={props.usage.rows}>
|
||||
{(row) => {
|
||||
const quota = getModelQuotaLimit(props.usage.limit, row.multiplier)
|
||||
return (
|
||||
<tr>
|
||||
<td>
|
||||
<bdi dir="auto">{row.name}</bdi>
|
||||
</td>
|
||||
<td>{row.cost === undefined ? "-" : money(row.cost)}</td>
|
||||
<td>{quota === undefined ? "-" : money(quota)}</td>
|
||||
<td>{row.contributionPercent}%</td>
|
||||
</tr>
|
||||
)
|
||||
}}
|
||||
</For>
|
||||
<tr data-slot="usage-total">
|
||||
<td colSpan={3}>{i18n.t("workspace.lite.subscription.total")}</td>
|
||||
<td>{totalPercentage()}%</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function LiteUsageGroup(props: { lite: NonNullable<LiteSubscription> }) {
|
||||
const params = useParams()
|
||||
const i18n = useI18n()
|
||||
const [open, setOpen] = createSignal<LiteUsageWindow>()
|
||||
const [store, setStore] = createStore({
|
||||
details: {} as Partial<Record<LiteUsageWindow, LiteUsageDetailsData | null>>,
|
||||
loading: undefined as LiteUsageWindow | undefined,
|
||||
})
|
||||
const items = () =>
|
||||
[
|
||||
{
|
||||
id: "rolling",
|
||||
label: i18n.t("workspace.lite.subscription.rollingUsage"),
|
||||
quotaLabel: i18n.t("workspace.lite.subscription.rollingQuota"),
|
||||
usage: props.lite.rollingUsage,
|
||||
},
|
||||
{
|
||||
id: "weekly",
|
||||
label: i18n.t("workspace.lite.subscription.weeklyUsage"),
|
||||
quotaLabel: i18n.t("workspace.lite.subscription.weeklyQuota"),
|
||||
usage: props.lite.weeklyUsage,
|
||||
},
|
||||
{
|
||||
id: "monthly",
|
||||
label: i18n.t("workspace.lite.subscription.monthlyUsage"),
|
||||
quotaLabel: i18n.t("workspace.lite.subscription.monthlyQuota"),
|
||||
usage: props.lite.monthlyUsage,
|
||||
},
|
||||
] as const
|
||||
const selected = createMemo(() => items().find((item) => item.id === open()))
|
||||
|
||||
async function toggle(id: LiteUsageWindow) {
|
||||
if (open() === id) {
|
||||
setOpen()
|
||||
return
|
||||
}
|
||||
setOpen(id)
|
||||
if (store.details[id] !== undefined) return
|
||||
setStore("loading", id)
|
||||
const details = await queryLiteUsageDetails(params.id!, id).catch(() => null)
|
||||
setStore("details", id, details)
|
||||
setStore("loading", (current) => (current === id ? undefined : current))
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<div data-slot="usage">
|
||||
<For each={items()}>
|
||||
{(item) => (
|
||||
<LiteUsageItem
|
||||
id={item.id}
|
||||
label={item.label}
|
||||
usage={item.usage}
|
||||
open={open() === item.id}
|
||||
onToggle={() => toggle(item.id)}
|
||||
/>
|
||||
)}
|
||||
</For>
|
||||
</div>
|
||||
<Show when={selected()}>
|
||||
{(item) => {
|
||||
const details = () => store.details[item().id]
|
||||
return (
|
||||
<Show
|
||||
when={details()}
|
||||
fallback={
|
||||
<Show when={store.loading === item().id}>
|
||||
<div data-slot="usage-details-loading">{i18n.t("workspace.lite.loading")}</div>
|
||||
</Show>
|
||||
}
|
||||
>
|
||||
{(usage) => (
|
||||
<LiteUsageDetails id={item().id} label={item().label} quotaLabel={item().quotaLabel} usage={usage()} />
|
||||
)}
|
||||
</Show>
|
||||
)
|
||||
}}
|
||||
</Show>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export function LiteSection(props: { lite: LiteSubscription | undefined }) {
|
||||
const params = useParams()
|
||||
const i18n = useI18n()
|
||||
@@ -261,11 +566,7 @@ export function LiteSection(props: { lite: LiteSubscription | undefined }) {
|
||||
</a>
|
||||
.
|
||||
</div>
|
||||
<div data-slot="usage">
|
||||
<LiteUsageItem label={i18n.t("workspace.lite.subscription.rollingUsage")} usage={sub().rollingUsage} />
|
||||
<LiteUsageItem label={i18n.t("workspace.lite.subscription.weeklyUsage")} usage={sub().weeklyUsage} />
|
||||
<LiteUsageItem label={i18n.t("workspace.lite.subscription.monthlyUsage")} usage={sub().monthlyUsage} />
|
||||
</div>
|
||||
<LiteUsageGroup lite={sub()} />
|
||||
<form action={setLiteUseBalance} method="post" data-slot="setting-row">
|
||||
<p>{i18n.t("workspace.lite.subscription.useBalance")}</p>
|
||||
<input type="hidden" name="workspaceID" value={params.id} />
|
||||
@@ -339,7 +640,7 @@ export function LiteSection(props: { lite: LiteSubscription | undefined }) {
|
||||
</p>
|
||||
<h3 data-slot="promo-models-title">{i18n.t("workspace.lite.promo.modelsTitle")}</h3>
|
||||
<ul data-slot="promo-models">
|
||||
<li>Grok 4.5</li>
|
||||
<li>Grok 4.6</li>
|
||||
<li>GPT 5.6 Luna</li>
|
||||
<li>GLM-5.3</li>
|
||||
<li>GLM-5.2</li>
|
||||
@@ -361,7 +662,6 @@ export function LiteSection(props: { lite: LiteSubscription | undefined }) {
|
||||
<li>MiMo-V2.5</li>
|
||||
<li>MiMo-V2.5-Pro</li>
|
||||
<li>Hy3</li>
|
||||
<li>Ox Alpha Free</li>
|
||||
</ul>
|
||||
<p data-slot="promo-description">{i18n.t("workspace.lite.promo.footer")}</p>
|
||||
<div data-slot="subscribe-actions">
|
||||
|
||||
@@ -6,6 +6,7 @@ import { Billing } from "@opencode-ai/console-core/billing.js"
|
||||
import { and, Database, desc, eq, isNull } from "@opencode-ai/console-core/drizzle/index.js"
|
||||
import { WorkspaceTable } from "@opencode-ai/console-core/schema/workspace.sql.js"
|
||||
import { UserTable } from "@opencode-ai/console-core/schema/user.sql.js"
|
||||
import { checkCheckoutRateLimit } from "~/routes/zen/util/redis"
|
||||
|
||||
export function formatDateForTable(date: Date) {
|
||||
const options: Intl.DateTimeFormatOptions = {
|
||||
@@ -77,7 +78,8 @@ export const createCheckoutUrl = action(
|
||||
return json(
|
||||
await withActor(
|
||||
() =>
|
||||
Billing.generateCheckoutUrl({ amount, successUrl, cancelUrl })
|
||||
checkCheckoutRateLimit(Actor.account())
|
||||
.then(() => Billing.generateCheckoutUrl({ amount, successUrl, cancelUrl }))
|
||||
.then((data) => ({ error: undefined, data }))
|
||||
.catch((e) => ({
|
||||
error: e.message as string,
|
||||
|
||||
@@ -234,12 +234,18 @@ export async function handler(
|
||||
}
|
||||
headers.set(k, v)
|
||||
})
|
||||
if (isNewInference) {
|
||||
headers.set("x-zen-model", model)
|
||||
}
|
||||
headers.delete("host")
|
||||
headers.delete("content-length")
|
||||
headers.delete("x-opencode-request")
|
||||
if (!isNewInference) headers.delete("x-opencode-session")
|
||||
headers.delete("x-opencode-project")
|
||||
headers.delete("x-opencode-client")
|
||||
if (!isNewInference) {
|
||||
headers.delete("x-opencode-session")
|
||||
headers.delete("x-opencode-project")
|
||||
headers.delete("x-opencode-client")
|
||||
headers.delete("x-opencode-request")
|
||||
headers.delete("x-zen-model")
|
||||
}
|
||||
return headers
|
||||
})(),
|
||||
body: reqBody,
|
||||
@@ -1108,7 +1114,7 @@ export async function handler(
|
||||
enrichment: (() => {
|
||||
if (billingSource === "subscription") return { plan: "sub" }
|
||||
if (billingSource === "byok") return { plan: "byok" }
|
||||
if (billingSource === "lite") return { plan: "lite" }
|
||||
if (billingSource === "lite") return { plan: "lite", costMultiplier: modelInfo.costMultiplier }
|
||||
return undefined
|
||||
})(),
|
||||
}),
|
||||
|
||||
@@ -16,3 +16,11 @@ export function getRedis() {
|
||||
export function buildRateLimitKey(kind: string, identifier: string, interval?: string) {
|
||||
return `${Resource.App.stage}:ratelimit:${kind}:${identifier}${interval ? `:${interval}` : ""}`
|
||||
}
|
||||
|
||||
export async function checkCheckoutRateLimit(accountID: string) {
|
||||
const redis = getRedis()
|
||||
const key = buildRateLimitKey("checkout", accountID)
|
||||
const count = await redis.incr(key)
|
||||
if (count === 1) await redis.expire(key, 60 * 60)
|
||||
if (count > 5) throw new Error("Too many payment attempts. Please try again later.")
|
||||
}
|
||||
|
||||
@@ -0,0 +1,75 @@
|
||||
import { describe, expect, test } from "bun:test"
|
||||
import { buildLiteUsageBreakdown, getModelQuotaLimit } from "../src/lib/lite-usage"
|
||||
|
||||
describe("Go usage breakdown", () => {
|
||||
test("derives the model quota from the window limit and multiplier", () => {
|
||||
expect(getModelQuotaLimit(30, 1)).toBe(30)
|
||||
expect(getModelQuotaLimit(30, 2)).toBe(15)
|
||||
expect(getModelQuotaLimit(30, 4)).toBe(7.5)
|
||||
})
|
||||
|
||||
test("groups model quota usage into the percentage of the limit", () => {
|
||||
const result = buildLiteUsageBreakdown({
|
||||
usage: 416,
|
||||
limit: 1_200,
|
||||
sources: [
|
||||
{ model: "glm", name: "GLM", cost: 200, quotaCost: 300, multiplier: 1.5, estimated: false },
|
||||
{ model: "kimi", name: "Kimi", cost: 116, quotaCost: 116, multiplier: 1, estimated: false },
|
||||
],
|
||||
})
|
||||
|
||||
expect(result.usagePercent).toBe(34.7)
|
||||
expect(result.rows[0]).toMatchObject({ name: "GLM", multiplier: 1.5, contributionPercent: 25 })
|
||||
expect(result.rows.reduce((total, row) => total + row.contributionPercent, 0)).toBeCloseTo(result.usagePercent)
|
||||
})
|
||||
|
||||
test("distributes credits across the model contributions", () => {
|
||||
const result = buildLiteUsageBreakdown({
|
||||
usage: 366,
|
||||
limit: 1_200,
|
||||
sources: [
|
||||
{ model: "glm", name: "GLM", cost: 200, quotaCost: 300, multiplier: 1.5, estimated: false },
|
||||
{ model: "kimi", name: "Kimi", cost: 116, quotaCost: 116, multiplier: 1, estimated: true },
|
||||
],
|
||||
})
|
||||
|
||||
expect(result.rows).toHaveLength(2)
|
||||
expect(result.rows.every((row) => row.contributionPercent >= 0)).toBe(true)
|
||||
expect(result.rows.reduce((total, row) => total + row.contributionPercent, 0)).toBeCloseTo(result.usagePercent)
|
||||
})
|
||||
|
||||
test("does not synthesize a row when request history is unavailable", () => {
|
||||
const result = buildLiteUsageBreakdown({ usage: 120, limit: 1_200, sources: [] })
|
||||
|
||||
expect(result.rows).toEqual([])
|
||||
})
|
||||
|
||||
test("allocates rounded percentages without making positive rows negative", () => {
|
||||
const sources = Array.from({ length: 20 }, (_, index) => ({
|
||||
model: `model-${index}`,
|
||||
name: `Model ${index}`,
|
||||
cost: 4,
|
||||
quotaCost: 4,
|
||||
multiplier: 1,
|
||||
estimated: false,
|
||||
}))
|
||||
const result = buildLiteUsageBreakdown({ usage: 80, limit: 10_000, sources })
|
||||
|
||||
expect(result.rows.every((row) => row.contributionPercent >= 0)).toBe(true)
|
||||
expect(result.rows.reduce((total, row) => total + row.contributionPercent, 0)).toBeCloseTo(result.usagePercent)
|
||||
})
|
||||
|
||||
test("keeps multiplier changes for the same model as separate rows", () => {
|
||||
const result = buildLiteUsageBreakdown({
|
||||
usage: 500,
|
||||
limit: 1_000,
|
||||
sources: [
|
||||
{ model: "glm", name: "GLM", cost: 100, quotaCost: 100, multiplier: 1, estimated: false },
|
||||
{ model: "glm", name: "GLM", cost: 200, quotaCost: 400, multiplier: 2, estimated: false },
|
||||
],
|
||||
})
|
||||
|
||||
expect(result.rows.map((row) => row.multiplier)).toEqual([2, 1])
|
||||
expect(result.rows.map((row) => row.contributionPercent)).toEqual([40, 10])
|
||||
})
|
||||
})
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/package.json",
|
||||
"name": "@opencode-ai/console-core",
|
||||
"version": "1.18.22",
|
||||
"version": "1.18.23",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
|
||||
@@ -129,6 +129,7 @@ export const UsageTable = mysqlTable(
|
||||
sessionID: varchar("session_id", { length: 30 }),
|
||||
enrichment: json("enrichment").$type<{
|
||||
plan: "sub" | "byok" | "lite"
|
||||
costMultiplier?: number
|
||||
}>(),
|
||||
},
|
||||
(table) => [...workspaceIndexes(table), index("usage_time_created").on(table.workspaceID, table.timeCreated)],
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@opencode-ai/console-function",
|
||||
"version": "1.18.22",
|
||||
"version": "1.18.23",
|
||||
"$schema": "https://json.schemastore.org/package.json",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
@@ -11,6 +11,7 @@
|
||||
"devDependencies": {
|
||||
"@cloudflare/workers-types": "catalog:",
|
||||
"@tsconfig/node22": "22.0.2",
|
||||
"@types/bun": "catalog:",
|
||||
"@types/node": "catalog:",
|
||||
"openai": "5.11.0",
|
||||
"typescript": "catalog:",
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
import { describe, expect, test } from "bun:test"
|
||||
import { isAllowedAuthorizationRedirect } from "./auth-redirect"
|
||||
|
||||
describe("authorization redirect validation", () => {
|
||||
test("allows registered OpenCode callbacks", () => {
|
||||
expect(isAllowedAuthorizationRedirect("app", "https://opencode.ai/auth/callback")).toBe(true)
|
||||
expect(isAllowedAuthorizationRedirect("app", "https://dev.opencode.ai/auth/callback")).toBe(true)
|
||||
expect(isAllowedAuthorizationRedirect("app", "http://localhost:3000/auth/callback")).toBe(true)
|
||||
expect(isAllowedAuthorizationRedirect("app", "http://127.0.0.1:3000/auth/callback")).toBe(true)
|
||||
})
|
||||
|
||||
test("rejects unregistered clients and external redirects", () => {
|
||||
expect(isAllowedAuthorizationRedirect("other", "https://opencode.ai/auth/callback")).toBe(false)
|
||||
expect(isAllowedAuthorizationRedirect("app", "https://evil.example/callback")).toBe(false)
|
||||
expect(isAllowedAuthorizationRedirect("app", "https://opencode.ai.evil.example/callback")).toBe(false)
|
||||
expect(isAllowedAuthorizationRedirect("app", "javascript:alert(1)")).toBe(false)
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,18 @@
|
||||
export const isAllowedAuthorizationRedirect = (clientID: string, redirectURI: string) => {
|
||||
if (clientID !== "app") return false
|
||||
const redirect = (() => {
|
||||
try {
|
||||
return new URL(redirectURI)
|
||||
} catch {
|
||||
return undefined
|
||||
}
|
||||
})()
|
||||
if (redirect === undefined) return false
|
||||
if (redirect.hostname === "localhost" || redirect.hostname === "127.0.0.1") {
|
||||
return redirect.protocol === "http:" || redirect.protocol === "https:"
|
||||
}
|
||||
return (
|
||||
redirect.protocol === "https:" &&
|
||||
(redirect.hostname === "opencode.ai" || redirect.hostname.endsWith(".opencode.ai"))
|
||||
)
|
||||
}
|
||||
@@ -17,6 +17,7 @@ import { WorkspaceTable } from "@opencode-ai/console-core/schema/workspace.sql.j
|
||||
import { UserTable } from "@opencode-ai/console-core/schema/user.sql.js"
|
||||
import { AuthTable } from "@opencode-ai/console-core/schema/auth.sql.js"
|
||||
import { Identifier } from "@opencode-ai/console-core/identifier.js"
|
||||
import { isAllowedAuthorizationRedirect } from "./auth-redirect.js"
|
||||
|
||||
type Env = {
|
||||
AuthStorage: KVNamespace
|
||||
@@ -41,6 +42,17 @@ const MY_THEME: Theme = {
|
||||
|
||||
export default {
|
||||
async fetch(request: Request, env: Env, ctx: ExecutionContext) {
|
||||
const requestURL = new URL(request.url)
|
||||
if (requestURL.pathname === "/authorize") {
|
||||
const redirectURI = requestURL.searchParams.get("redirect_uri")
|
||||
if (
|
||||
redirectURI !== null &&
|
||||
!isAllowedAuthorizationRedirect(requestURL.searchParams.get("client_id") ?? "", redirectURI)
|
||||
) {
|
||||
return new Response("Unauthorized client", { status: 400 })
|
||||
}
|
||||
}
|
||||
|
||||
const result = await issuer({
|
||||
theme: MY_THEME,
|
||||
providers: {
|
||||
@@ -102,6 +114,7 @@ export default {
|
||||
namespace: env.AuthStorage,
|
||||
}),
|
||||
subjects,
|
||||
allow: ({ clientID, redirectURI }) => Promise.resolve(isAllowedAuthorizationRedirect(clientID, redirectURI)),
|
||||
async success(ctx, response) {
|
||||
console.log(response)
|
||||
|
||||
|
||||
@@ -6,6 +6,6 @@
|
||||
"moduleResolution": "bundler",
|
||||
"jsx": "preserve",
|
||||
"jsxImportSource": "react",
|
||||
"types": ["@cloudflare/workers-types", "node"]
|
||||
"types": ["@cloudflare/workers-types", "bun", "node"]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@opencode-ai/console-mail",
|
||||
"version": "1.18.22",
|
||||
"version": "1.18.23",
|
||||
"dependencies": {
|
||||
"@jsx-email/all": "2.2.3",
|
||||
"@jsx-email/cli": "1.4.3",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@opencode-ai/console-support",
|
||||
"version": "1.18.22",
|
||||
"version": "1.18.23",
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
"scripts": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/package.json",
|
||||
"version": "1.18.22",
|
||||
"version": "1.18.23",
|
||||
"name": "@opencode-ai/core",
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
|
||||
@@ -54,12 +54,39 @@ export function applyOnly(db: Database, input: Migration[]) {
|
||||
if (
|
||||
yield* db.get(sql`SELECT name FROM sqlite_master WHERE type = 'table' AND name = ${"__drizzle_migrations"}`)
|
||||
) {
|
||||
yield* db.run(sql`
|
||||
INSERT OR IGNORE INTO ${sql.identifier("migration")} (id, time_completed)
|
||||
SELECT name, ${Date.now()}
|
||||
FROM ${sql.identifier("__drizzle_migrations")}
|
||||
WHERE name IS NOT NULL
|
||||
`)
|
||||
const named = (yield* db.all<{ name: string }>(
|
||||
sql`SELECT name FROM pragma_table_info('__drizzle_migrations')`,
|
||||
)).some((column) => column.name === "name")
|
||||
|
||||
if (named) {
|
||||
yield* db.run(sql`
|
||||
INSERT OR IGNORE INTO ${sql.identifier("migration")} (id, time_completed)
|
||||
SELECT name, ${Date.now()}
|
||||
FROM ${sql.identifier("__drizzle_migrations")}
|
||||
WHERE name IS NOT NULL
|
||||
`)
|
||||
}
|
||||
|
||||
if (!named) {
|
||||
const entries = yield* db.all<{ created_at: number; prefix: string | null }>(sql`
|
||||
SELECT created_at, strftime('%Y%m%d%H%M%S', created_at / 1000, 'unixepoch') AS prefix
|
||||
FROM ${sql.identifier("__drizzle_migrations")}
|
||||
WHERE created_at IS NOT NULL
|
||||
`)
|
||||
|
||||
for (const entry of entries) {
|
||||
const migration = input.find((item) => item.id.startsWith(`${entry.prefix}_`))
|
||||
if (!migration) {
|
||||
return yield* Effect.die(
|
||||
new Error(`Legacy migration timestamp ${entry.created_at} does not match any known migration`),
|
||||
)
|
||||
}
|
||||
yield* db.run(sql`
|
||||
INSERT OR IGNORE INTO ${sql.identifier("migration")} (id, time_completed)
|
||||
VALUES (${migration.id}, ${Date.now()})
|
||||
`)
|
||||
}
|
||||
}
|
||||
completed = new Set(
|
||||
(yield* db.all<{ id: string }>(sql`SELECT id FROM ${sql.identifier("migration")}`)).map((row) => row.id),
|
||||
)
|
||||
|
||||
@@ -5,6 +5,9 @@ export default {
|
||||
id: "20260410174513_workspace-name",
|
||||
up(tx) {
|
||||
return Effect.gen(function* () {
|
||||
const columns = yield* tx.all<{ name: string }>(`PRAGMA table_info(\`workspace\`)`)
|
||||
const name = columns.some((column) => column.name === "name") ? "`name`" : "''"
|
||||
|
||||
yield* tx.run(`PRAGMA foreign_keys=OFF;`)
|
||||
yield* tx.run(`
|
||||
CREATE TABLE \`__new_workspace\` (
|
||||
@@ -19,7 +22,7 @@ export default {
|
||||
);
|
||||
`)
|
||||
yield* tx.run(
|
||||
`INSERT INTO \`__new_workspace\`(\`id\`, \`type\`, \`branch\`, \`name\`, \`directory\`, \`extra\`, \`project_id\`) SELECT \`id\`, \`type\`, \`branch\`, \`name\`, \`directory\`, \`extra\`, \`project_id\` FROM \`workspace\`;`,
|
||||
`INSERT INTO \`__new_workspace\`(\`id\`, \`type\`, \`branch\`, \`name\`, \`directory\`, \`extra\`, \`project_id\`) SELECT \`id\`, \`type\`, \`branch\`, ${name}, \`directory\`, \`extra\`, \`project_id\` FROM \`workspace\`;`,
|
||||
)
|
||||
yield* tx.run(`DROP TABLE \`workspace\`;`)
|
||||
yield* tx.run(`ALTER TABLE \`__new_workspace\` RENAME TO \`workspace\`;`)
|
||||
|
||||
@@ -8,6 +8,7 @@ import { Effect, Layer } from "effect"
|
||||
import { eq, inArray, sql } from "drizzle-orm"
|
||||
import { DatabaseMigration } from "@opencode-ai/core/database/migration"
|
||||
import { migrations } from "@opencode-ai/core/database/migration.gen"
|
||||
import workspaceNameMigration from "@opencode-ai/core/database/migration/20260410174513_workspace-name"
|
||||
import sessionUsageMigration from "@opencode-ai/core/database/migration/20260510033149_session_usage"
|
||||
import normalizeStoragePathsMigration from "@opencode-ai/core/database/migration/20260601010001_normalize_storage_paths"
|
||||
import sessionMessageProjectionOrderMigration from "@opencode-ai/core/database/migration/20260603040000_session_message_projection_order"
|
||||
@@ -38,6 +39,68 @@ const run = <A, E>(effect: Effect.Effect<A, E, SqlClientService>) =>
|
||||
const makeDb = EffectDrizzleSqlite.makeWithDefaults()
|
||||
|
||||
describe("DatabaseMigration", () => {
|
||||
test("defaults missing workspace names while preserving legacy workspace data", async () => {
|
||||
await run(
|
||||
Effect.gen(function* () {
|
||||
const db = yield* makeDb
|
||||
yield* db.run(sql`
|
||||
CREATE TABLE workspace (
|
||||
id text PRIMARY KEY,
|
||||
type text NOT NULL,
|
||||
branch text,
|
||||
directory text,
|
||||
extra text,
|
||||
project_id text NOT NULL
|
||||
)
|
||||
`)
|
||||
yield* db.run(sql`
|
||||
INSERT INTO workspace (id, type, branch, directory, extra, project_id)
|
||||
VALUES ('wrk_legacy', 'remote', 'main', '/repo', '{}', 'proj_legacy')
|
||||
`)
|
||||
|
||||
yield* DatabaseMigration.applyOnly(db, [workspaceNameMigration])
|
||||
|
||||
expect(yield* db.get(sql`SELECT id, name, branch, directory, extra FROM workspace`)).toEqual({
|
||||
id: "wrk_legacy",
|
||||
name: "",
|
||||
branch: "main",
|
||||
directory: "/repo",
|
||||
extra: "{}",
|
||||
})
|
||||
}),
|
||||
)
|
||||
})
|
||||
|
||||
test("imports unnamed legacy Drizzle journal entries by their actual migration timestamps", async () => {
|
||||
await run(
|
||||
Effect.gen(function* () {
|
||||
const db = yield* makeDb
|
||||
yield* db.run(sql`CREATE TABLE __drizzle_migrations (id integer PRIMARY KEY, hash text, created_at integer)`)
|
||||
yield* db.run(sql`
|
||||
INSERT INTO __drizzle_migrations (hash, created_at)
|
||||
VALUES ('', ${Date.UTC(2026, 3, 10, 17, 45, 13)})
|
||||
`)
|
||||
|
||||
yield* DatabaseMigration.applyOnly(db, [workspaceNameMigration])
|
||||
|
||||
expect(yield* db.all(sql`SELECT id FROM migration`)).toEqual([{ id: "20260410174513_workspace-name" }])
|
||||
}),
|
||||
)
|
||||
})
|
||||
|
||||
test("rejects unknown legacy Drizzle journal timestamps instead of guessing completed migrations", async () => {
|
||||
await expect(
|
||||
run(
|
||||
Effect.gen(function* () {
|
||||
const db = yield* makeDb
|
||||
yield* db.run(sql`CREATE TABLE __drizzle_migrations (id integer PRIMARY KEY, hash text, created_at integer)`)
|
||||
yield* db.run(sql`INSERT INTO __drizzle_migrations (hash, created_at) VALUES ('', 1234567890000)`)
|
||||
yield* DatabaseMigration.applyOnly(db, [workspaceNameMigration])
|
||||
}),
|
||||
),
|
||||
).rejects.toThrow("does not match any known migration")
|
||||
})
|
||||
|
||||
test("serializes concurrent embedded initialization for one database path", async () => {
|
||||
await using tmp = await tmpdir()
|
||||
const filename = path.join(tmp.path, "embedded.sqlite")
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@opencode-ai/desktop",
|
||||
"private": true,
|
||||
"version": "1.18.22",
|
||||
"version": "1.18.23",
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
"homepage": "https://opencode.ai",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/package.json",
|
||||
"version": "1.18.22",
|
||||
"version": "1.18.23",
|
||||
"name": "@opencode-ai/effect-drizzle-sqlite",
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/package.json",
|
||||
"version": "1.18.22",
|
||||
"version": "1.18.23",
|
||||
"name": "@opencode-ai/effect-sqlite-node",
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@opencode-ai/enterprise",
|
||||
"version": "1.18.22",
|
||||
"version": "1.18.23",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@opencode-ai/function",
|
||||
"version": "1.18.22",
|
||||
"version": "1.18.23",
|
||||
"$schema": "https://json.schemastore.org/package.json",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/package.json",
|
||||
"version": "1.18.22",
|
||||
"version": "1.18.23",
|
||||
"name": "@opencode-ai/http-recorder",
|
||||
"description": "Record and replay Effect HTTP client traffic with deterministic cassettes",
|
||||
"type": "module",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/package.json",
|
||||
"version": "1.18.22",
|
||||
"version": "1.18.23",
|
||||
"name": "@opencode-ai/llm",
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/package.json",
|
||||
"version": "1.18.22",
|
||||
"version": "1.18.23",
|
||||
"name": "opencode",
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
|
||||
@@ -18,7 +18,6 @@ type OpenApiResponse = {
|
||||
readonly content?: Record<string, { readonly schema?: OpenApiSchema }>
|
||||
}
|
||||
type OpenApiOperation = {
|
||||
readonly operationId?: string
|
||||
readonly parameters?: ReadonlyArray<{
|
||||
readonly name: string
|
||||
readonly in: string
|
||||
@@ -71,14 +70,6 @@ function isBuiltInEndpointError(name: string) {
|
||||
}
|
||||
|
||||
describe("PublicApi OpenAPI v2 errors", () => {
|
||||
test("omits version prefixes from operation IDs", () => {
|
||||
const spec = OpenApi.fromApi(PublicApi) as OpenApiSpec
|
||||
|
||||
expect(v2Operations(spec).map(({ operation }) => operation.operationId)).not.toContainEqual(
|
||||
expect.stringMatching(/v2/i),
|
||||
)
|
||||
})
|
||||
|
||||
test("includes plugin-facing core schemas", () => {
|
||||
const spec = OpenApi.fromApi(PublicApi) as OpenApiSpec
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/package.json",
|
||||
"name": "@opencode-ai/plugin",
|
||||
"version": "1.18.22",
|
||||
"version": "1.18.23",
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
"scripts": {
|
||||
|
||||
@@ -12,7 +12,7 @@ export const AgentGroup = HttpApiGroup.make("server.agent").add(
|
||||
.annotateMerge(locationQueryOpenApi)
|
||||
.annotateMerge(
|
||||
OpenApi.annotations({
|
||||
identifier: "agent.list",
|
||||
identifier: "v2.agent.list",
|
||||
summary: "List agents",
|
||||
description: "Retrieve currently registered agents.",
|
||||
}),
|
||||
|
||||
@@ -13,7 +13,7 @@ export const CommandGroup = HttpApiGroup.make("server.command")
|
||||
.annotateMerge(locationQueryOpenApi)
|
||||
.annotateMerge(
|
||||
OpenApi.annotations({
|
||||
identifier: "command.list",
|
||||
identifier: "v2.command.list",
|
||||
summary: "List commands",
|
||||
description: "Retrieve currently registered commands.",
|
||||
}),
|
||||
|
||||
@@ -14,7 +14,7 @@ export const CredentialGroup = HttpApiGroup.make("server.credential")
|
||||
.annotateMerge(locationQueryOpenApi)
|
||||
.annotateMerge(
|
||||
OpenApi.annotations({
|
||||
identifier: "credential.update",
|
||||
identifier: "v2.credential.update",
|
||||
summary: "Update credential",
|
||||
description: "Update a stored credential label.",
|
||||
}),
|
||||
@@ -29,7 +29,7 @@ export const CredentialGroup = HttpApiGroup.make("server.credential")
|
||||
.annotateMerge(locationQueryOpenApi)
|
||||
.annotateMerge(
|
||||
OpenApi.annotations({
|
||||
identifier: "credential.remove",
|
||||
identifier: "v2.credential.remove",
|
||||
summary: "Remove credential",
|
||||
description: "Remove a stored integration credential.",
|
||||
}),
|
||||
|
||||
@@ -36,7 +36,7 @@ const make = <const Definitions extends ReadonlyArray<Definition>>(definitions:
|
||||
success: HttpApiSchema.StreamSse({ data: EventSchema }),
|
||||
}).annotateMerge(
|
||||
OpenApi.annotations({
|
||||
identifier: "event.subscribe",
|
||||
identifier: "v2.event.subscribe",
|
||||
summary: "Subscribe to events",
|
||||
description: "Subscribe to native event payloads for the server.",
|
||||
}),
|
||||
|
||||
@@ -26,7 +26,7 @@ export const FileSystemGroup = HttpApiGroup.make("server.fs")
|
||||
.annotateMerge(locationQueryOpenApi)
|
||||
.annotateMerge(
|
||||
OpenApi.annotations({
|
||||
identifier: "fs.read",
|
||||
identifier: "v2.fs.read",
|
||||
summary: "Read file",
|
||||
description: "Serve one file relative to the requested location.",
|
||||
}),
|
||||
@@ -40,7 +40,7 @@ export const FileSystemGroup = HttpApiGroup.make("server.fs")
|
||||
.annotateMerge(locationQueryOpenApi)
|
||||
.annotateMerge(
|
||||
OpenApi.annotations({
|
||||
identifier: "fs.list",
|
||||
identifier: "v2.fs.list",
|
||||
summary: "List directory",
|
||||
description: "List direct children of one directory relative to the requested location.",
|
||||
}),
|
||||
@@ -54,7 +54,7 @@ export const FileSystemGroup = HttpApiGroup.make("server.fs")
|
||||
.annotateMerge(locationQueryOpenApi)
|
||||
.annotateMerge(
|
||||
OpenApi.annotations({
|
||||
identifier: "fs.find",
|
||||
identifier: "v2.fs.find",
|
||||
summary: "Find files",
|
||||
description: "Find recursively ranked filesystem entries relative to the requested location.",
|
||||
}),
|
||||
|
||||
@@ -6,7 +6,7 @@ export const HealthGroup = HttpApiGroup.make("server.health").add(
|
||||
success: Schema.Struct({ healthy: Schema.Literal(true) }),
|
||||
}).annotateMerge(
|
||||
OpenApi.annotations({
|
||||
identifier: "health.get",
|
||||
identifier: "v2.health.get",
|
||||
summary: "Check server health",
|
||||
description: "Check whether the API server is ready to accept requests.",
|
||||
}),
|
||||
|
||||
@@ -16,7 +16,7 @@ export const IntegrationGroup = HttpApiGroup.make("server.integration")
|
||||
.annotateMerge(locationQueryOpenApi)
|
||||
.annotateMerge(
|
||||
OpenApi.annotations({
|
||||
identifier: "integration.list",
|
||||
identifier: "v2.integration.list",
|
||||
summary: "List integrations",
|
||||
description: "Retrieve available integrations and their authentication methods.",
|
||||
}),
|
||||
@@ -31,7 +31,7 @@ export const IntegrationGroup = HttpApiGroup.make("server.integration")
|
||||
.annotateMerge(locationQueryOpenApi)
|
||||
.annotateMerge(
|
||||
OpenApi.annotations({
|
||||
identifier: "integration.get",
|
||||
identifier: "v2.integration.get",
|
||||
summary: "Get integration",
|
||||
description: "Retrieve one integration and its authentication methods.",
|
||||
}),
|
||||
@@ -51,7 +51,7 @@ export const IntegrationGroup = HttpApiGroup.make("server.integration")
|
||||
.annotateMerge(locationQueryOpenApi)
|
||||
.annotateMerge(
|
||||
OpenApi.annotations({
|
||||
identifier: "integration.connect.key",
|
||||
identifier: "v2.integration.connect.key",
|
||||
summary: "Connect with key",
|
||||
description: "Run a key authentication method and store the resulting credential.",
|
||||
}),
|
||||
@@ -72,7 +72,7 @@ export const IntegrationGroup = HttpApiGroup.make("server.integration")
|
||||
.annotateMerge(locationQueryOpenApi)
|
||||
.annotateMerge(
|
||||
OpenApi.annotations({
|
||||
identifier: "integration.connect.oauth",
|
||||
identifier: "v2.integration.connect.oauth",
|
||||
summary: "Begin OAuth connection",
|
||||
description: "Start an OAuth attempt and return the authorization details.",
|
||||
}),
|
||||
@@ -87,7 +87,7 @@ export const IntegrationGroup = HttpApiGroup.make("server.integration")
|
||||
.annotateMerge(locationQueryOpenApi)
|
||||
.annotateMerge(
|
||||
OpenApi.annotations({
|
||||
identifier: "integration.attempt.status",
|
||||
identifier: "v2.integration.attempt.status",
|
||||
summary: "Get OAuth attempt status",
|
||||
description: "Poll the current status of an OAuth attempt.",
|
||||
}),
|
||||
@@ -104,7 +104,7 @@ export const IntegrationGroup = HttpApiGroup.make("server.integration")
|
||||
.annotateMerge(locationQueryOpenApi)
|
||||
.annotateMerge(
|
||||
OpenApi.annotations({
|
||||
identifier: "integration.attempt.complete",
|
||||
identifier: "v2.integration.attempt.complete",
|
||||
summary: "Complete OAuth connection",
|
||||
description: "Complete a code-based OAuth attempt and store the resulting credential.",
|
||||
}),
|
||||
@@ -119,7 +119,7 @@ export const IntegrationGroup = HttpApiGroup.make("server.integration")
|
||||
.annotateMerge(locationQueryOpenApi)
|
||||
.annotateMerge(
|
||||
OpenApi.annotations({
|
||||
identifier: "integration.attempt.cancel",
|
||||
identifier: "v2.integration.attempt.cancel",
|
||||
summary: "Cancel OAuth connection",
|
||||
description: "Cancel an OAuth attempt and release its resources.",
|
||||
}),
|
||||
|
||||
@@ -34,7 +34,7 @@ export const LocationGroup = HttpApiGroup.make("server.location").add(
|
||||
.annotateMerge(locationQueryOpenApi)
|
||||
.annotateMerge(
|
||||
OpenApi.annotations({
|
||||
identifier: "location.get",
|
||||
identifier: "v2.location.get",
|
||||
summary: "Get location",
|
||||
description: "Resolve the requested location or the server default location.",
|
||||
}),
|
||||
|
||||
@@ -36,7 +36,7 @@ export const MessageGroup = HttpApiGroup.make("server.message")
|
||||
error: [InvalidCursorError, SessionNotFoundError, UnknownError],
|
||||
}).annotateMerge(
|
||||
OpenApi.annotations({
|
||||
identifier: "session.messages",
|
||||
identifier: "v2.session.messages",
|
||||
summary: "Get session messages",
|
||||
description:
|
||||
"Retrieve projected messages for a session. Items keep the requested order across pages; use cursor.next or cursor.previous to move through the ordered timeline.",
|
||||
|
||||
@@ -15,7 +15,7 @@ export const ModelGroup = HttpApiGroup.make("server.model")
|
||||
.annotateMerge(locationQueryOpenApi)
|
||||
.annotateMerge(
|
||||
OpenApi.annotations({
|
||||
identifier: "model.list",
|
||||
identifier: "v2.model.list",
|
||||
summary: "List models",
|
||||
description: "Retrieve available models ordered by release date.",
|
||||
}),
|
||||
|
||||
@@ -27,7 +27,7 @@ export const makePermissionGroup = <
|
||||
.annotateMerge(locationQueryOpenApi)
|
||||
.annotateMerge(
|
||||
OpenApi.annotations({
|
||||
identifier: "permission.request.list",
|
||||
identifier: "v2.permission.request.list",
|
||||
summary: "List pending permission requests",
|
||||
description: "Retrieve pending permission requests for a location.",
|
||||
}),
|
||||
@@ -39,7 +39,7 @@ export const makePermissionGroup = <
|
||||
success: Schema.Struct({ data: Schema.Array(PermissionSaved.Info) }),
|
||||
}).annotateMerge(
|
||||
OpenApi.annotations({
|
||||
identifier: "permission.saved.list",
|
||||
identifier: "v2.permission.saved.list",
|
||||
summary: "List saved permissions",
|
||||
description: "Retrieve saved permissions, optionally filtered by project.",
|
||||
}),
|
||||
@@ -51,7 +51,7 @@ export const makePermissionGroup = <
|
||||
success: HttpApiSchema.NoContent,
|
||||
}).annotateMerge(
|
||||
OpenApi.annotations({
|
||||
identifier: "permission.saved.remove",
|
||||
identifier: "v2.permission.saved.remove",
|
||||
summary: "Remove saved permission",
|
||||
description: "Remove a saved permission by ID.",
|
||||
}),
|
||||
@@ -79,7 +79,7 @@ export const makePermissionGroup = <
|
||||
.middleware(sessionLocationMiddleware)
|
||||
.annotateMerge(
|
||||
OpenApi.annotations({
|
||||
identifier: "session.permission.create",
|
||||
identifier: "v2.session.permission.create",
|
||||
summary: "Create permission request",
|
||||
description: "Evaluate and, when approval is required, create a permission request for a session.",
|
||||
}),
|
||||
@@ -94,7 +94,7 @@ export const makePermissionGroup = <
|
||||
.middleware(sessionLocationMiddleware)
|
||||
.annotateMerge(
|
||||
OpenApi.annotations({
|
||||
identifier: "session.permission.list",
|
||||
identifier: "v2.session.permission.list",
|
||||
summary: "List session permission requests",
|
||||
description: "Retrieve pending permission requests owned by a session.",
|
||||
}),
|
||||
@@ -109,7 +109,7 @@ export const makePermissionGroup = <
|
||||
.middleware(sessionLocationMiddleware)
|
||||
.annotateMerge(
|
||||
OpenApi.annotations({
|
||||
identifier: "session.permission.get",
|
||||
identifier: "v2.session.permission.get",
|
||||
summary: "Get permission request",
|
||||
description: "Retrieve a pending permission request owned by a session.",
|
||||
}),
|
||||
@@ -128,7 +128,7 @@ export const makePermissionGroup = <
|
||||
.middleware(sessionLocationMiddleware)
|
||||
.annotateMerge(
|
||||
OpenApi.annotations({
|
||||
identifier: "session.permission.reply",
|
||||
identifier: "v2.session.permission.reply",
|
||||
summary: "Reply to pending permission request",
|
||||
description: "Respond to a pending permission request owned by a session.",
|
||||
}),
|
||||
|
||||
@@ -30,7 +30,7 @@ export const ProjectCopyGroup = HttpApiGroup.make("server.projectCopy")
|
||||
error: ProjectCopyError,
|
||||
})
|
||||
.annotateMerge(locationQueryOpenApi)
|
||||
.annotateMerge(OpenApi.annotations({ identifier: "projectCopy.create" })),
|
||||
.annotateMerge(OpenApi.annotations({ identifier: "v2.projectCopy.create" })),
|
||||
)
|
||||
.add(
|
||||
HttpApiEndpoint.delete("projectCopy.remove", root, {
|
||||
@@ -41,7 +41,7 @@ export const ProjectCopyGroup = HttpApiGroup.make("server.projectCopy")
|
||||
error: ProjectCopyError,
|
||||
})
|
||||
.annotateMerge(locationQueryOpenApi)
|
||||
.annotateMerge(OpenApi.annotations({ identifier: "projectCopy.remove" })),
|
||||
.annotateMerge(OpenApi.annotations({ identifier: "v2.projectCopy.remove" })),
|
||||
)
|
||||
.add(
|
||||
HttpApiEndpoint.post("projectCopy.refresh", `${root}/refresh`, {
|
||||
@@ -51,6 +51,6 @@ export const ProjectCopyGroup = HttpApiGroup.make("server.projectCopy")
|
||||
error: ProjectCopyError,
|
||||
})
|
||||
.annotateMerge(locationQueryOpenApi)
|
||||
.annotateMerge(OpenApi.annotations({ identifier: "projectCopy.refresh" })),
|
||||
.annotateMerge(OpenApi.annotations({ identifier: "v2.projectCopy.refresh" })),
|
||||
)
|
||||
.annotateMerge(OpenApi.annotations({ title: "projectCopy", description: "Project copy management routes." }))
|
||||
|
||||
@@ -15,7 +15,7 @@ export const ProviderGroup = HttpApiGroup.make("server.provider")
|
||||
.annotateMerge(locationQueryOpenApi)
|
||||
.annotateMerge(
|
||||
OpenApi.annotations({
|
||||
identifier: "provider.list",
|
||||
identifier: "v2.provider.list",
|
||||
summary: "List providers",
|
||||
description: "Retrieve active AI providers so clients can show provider availability and configuration.",
|
||||
}),
|
||||
@@ -31,7 +31,7 @@ export const ProviderGroup = HttpApiGroup.make("server.provider")
|
||||
.annotateMerge(locationQueryOpenApi)
|
||||
.annotateMerge(
|
||||
OpenApi.annotations({
|
||||
identifier: "provider.get",
|
||||
identifier: "v2.provider.get",
|
||||
summary: "Get provider",
|
||||
description: "Retrieve a single AI provider so clients can inspect its availability and endpoint settings.",
|
||||
}),
|
||||
|
||||
@@ -27,7 +27,7 @@ export const PtyGroup = HttpApiGroup.make("server.pty")
|
||||
.annotateMerge(locationQueryOpenApi)
|
||||
.annotateMerge(
|
||||
OpenApi.annotations({
|
||||
identifier: "pty.list",
|
||||
identifier: "v2.pty.list",
|
||||
summary: "List PTY sessions",
|
||||
description: "List PTY sessions for a location, including exited sessions retained until removal.",
|
||||
}),
|
||||
@@ -42,7 +42,7 @@ export const PtyGroup = HttpApiGroup.make("server.pty")
|
||||
.annotateMerge(locationQueryOpenApi)
|
||||
.annotateMerge(
|
||||
OpenApi.annotations({
|
||||
identifier: "pty.create",
|
||||
identifier: "v2.pty.create",
|
||||
summary: "Create PTY session",
|
||||
description: "Create a pseudo-terminal session for a location.",
|
||||
}),
|
||||
@@ -58,7 +58,7 @@ export const PtyGroup = HttpApiGroup.make("server.pty")
|
||||
.annotateMerge(locationQueryOpenApi)
|
||||
.annotateMerge(
|
||||
OpenApi.annotations({
|
||||
identifier: "pty.get",
|
||||
identifier: "v2.pty.get",
|
||||
summary: "Get PTY session",
|
||||
description: "Get one PTY session, including its exit code once exited.",
|
||||
}),
|
||||
@@ -75,7 +75,7 @@ export const PtyGroup = HttpApiGroup.make("server.pty")
|
||||
.annotateMerge(locationQueryOpenApi)
|
||||
.annotateMerge(
|
||||
OpenApi.annotations({
|
||||
identifier: "pty.update",
|
||||
identifier: "v2.pty.update",
|
||||
summary: "Update PTY session",
|
||||
description: "Update the title or viewport size of one PTY session.",
|
||||
}),
|
||||
@@ -91,7 +91,7 @@ export const PtyGroup = HttpApiGroup.make("server.pty")
|
||||
.annotateMerge(locationQueryOpenApi)
|
||||
.annotateMerge(
|
||||
OpenApi.annotations({
|
||||
identifier: "pty.remove",
|
||||
identifier: "v2.pty.remove",
|
||||
summary: "Remove PTY session",
|
||||
description: "Terminate and remove one PTY session.",
|
||||
}),
|
||||
@@ -107,7 +107,7 @@ export const PtyGroup = HttpApiGroup.make("server.pty")
|
||||
.annotateMerge(locationQueryOpenApi)
|
||||
.annotateMerge(
|
||||
OpenApi.annotations({
|
||||
identifier: "pty.connectToken",
|
||||
identifier: "v2.pty.connectToken",
|
||||
summary: "Create PTY WebSocket token",
|
||||
description: "Create a short-lived single-use ticket for opening a PTY WebSocket connection.",
|
||||
}),
|
||||
@@ -122,7 +122,7 @@ export const PtyGroup = HttpApiGroup.make("server.pty")
|
||||
error: [ForbiddenError, PtyNotFoundError],
|
||||
}).annotateMerge(
|
||||
OpenApi.annotations({
|
||||
identifier: "pty.connect",
|
||||
identifier: "v2.pty.connect",
|
||||
summary: "Connect to PTY session",
|
||||
description: "Establish a WebSocket connection streaming PTY output and accepting terminal input.",
|
||||
transform: (operation) => ({
|
||||
|
||||
@@ -24,7 +24,7 @@ export const makeQuestionGroup = <
|
||||
.annotateMerge(locationQueryOpenApi)
|
||||
.annotateMerge(
|
||||
OpenApi.annotations({
|
||||
identifier: "question.request.list",
|
||||
identifier: "v2.question.request.list",
|
||||
summary: "List pending question requests",
|
||||
description: "Retrieve pending question requests for a location.",
|
||||
}),
|
||||
@@ -42,7 +42,7 @@ export const makeQuestionGroup = <
|
||||
.middleware(sessionLocationMiddleware)
|
||||
.annotateMerge(
|
||||
OpenApi.annotations({
|
||||
identifier: "session.question.list",
|
||||
identifier: "v2.session.question.list",
|
||||
summary: "List session question requests",
|
||||
description: "Retrieve pending question requests owned by a session.",
|
||||
}),
|
||||
@@ -58,7 +58,7 @@ export const makeQuestionGroup = <
|
||||
.middleware(sessionLocationMiddleware)
|
||||
.annotateMerge(
|
||||
OpenApi.annotations({
|
||||
identifier: "session.question.reply",
|
||||
identifier: "v2.session.question.reply",
|
||||
summary: "Reply to pending question request",
|
||||
description: "Answer a pending question request owned by a session.",
|
||||
}),
|
||||
@@ -73,7 +73,7 @@ export const makeQuestionGroup = <
|
||||
.middleware(sessionLocationMiddleware)
|
||||
.annotateMerge(
|
||||
OpenApi.annotations({
|
||||
identifier: "session.question.reject",
|
||||
identifier: "v2.session.question.reject",
|
||||
summary: "Reject pending question request",
|
||||
description: "Reject a pending question request owned by a session.",
|
||||
}),
|
||||
|
||||
@@ -13,7 +13,7 @@ export const ReferenceGroup = HttpApiGroup.make("server.reference")
|
||||
.annotateMerge(locationQueryOpenApi)
|
||||
.annotateMerge(
|
||||
OpenApi.annotations({
|
||||
identifier: "reference.list",
|
||||
identifier: "v2.reference.list",
|
||||
summary: "List references",
|
||||
description: "List references available in the requested location.",
|
||||
}),
|
||||
|
||||
@@ -118,7 +118,7 @@ export const makeSessionGroup = <I extends HttpApiMiddleware.AnyId, S>(sessionLo
|
||||
error: [InvalidCursorError, InvalidRequestError],
|
||||
}).annotateMerge(
|
||||
OpenApi.annotations({
|
||||
identifier: "session.list",
|
||||
identifier: "v2.session.list",
|
||||
summary: "List sessions",
|
||||
description:
|
||||
"Retrieve sessions in the requested order. Items keep that order across pages; use cursor.next or cursor.previous to move through the ordered list.",
|
||||
@@ -136,7 +136,7 @@ export const makeSessionGroup = <I extends HttpApiMiddleware.AnyId, S>(sessionLo
|
||||
success: Schema.Struct({ data: Session.Info }),
|
||||
}).annotateMerge(
|
||||
OpenApi.annotations({
|
||||
identifier: "session.create",
|
||||
identifier: "v2.session.create",
|
||||
summary: "Create session",
|
||||
description: "Create a session at the requested location.",
|
||||
}),
|
||||
@@ -147,7 +147,7 @@ export const makeSessionGroup = <I extends HttpApiMiddleware.AnyId, S>(sessionLo
|
||||
success: Schema.Struct({ data: Schema.Record(Session.ID, SessionActive) }),
|
||||
}).annotateMerge(
|
||||
OpenApi.annotations({
|
||||
identifier: "session.active",
|
||||
identifier: "v2.session.active",
|
||||
summary: "List active sessions",
|
||||
description:
|
||||
"Retrieve foreground Session drains currently owned by this OpenCode process. Sessions absent from the result are inactive.",
|
||||
@@ -163,7 +163,7 @@ export const makeSessionGroup = <I extends HttpApiMiddleware.AnyId, S>(sessionLo
|
||||
.middleware(sessionLocationMiddleware)
|
||||
.annotateMerge(
|
||||
OpenApi.annotations({
|
||||
identifier: "session.get",
|
||||
identifier: "v2.session.get",
|
||||
summary: "Get session",
|
||||
description: "Retrieve a session by ID.",
|
||||
}),
|
||||
@@ -179,7 +179,7 @@ export const makeSessionGroup = <I extends HttpApiMiddleware.AnyId, S>(sessionLo
|
||||
.middleware(sessionLocationMiddleware)
|
||||
.annotateMerge(
|
||||
OpenApi.annotations({
|
||||
identifier: "session.switchAgent",
|
||||
identifier: "v2.session.switchAgent",
|
||||
summary: "Switch session agent",
|
||||
description: "Switch the agent used by subsequent provider turns.",
|
||||
}),
|
||||
@@ -195,7 +195,7 @@ export const makeSessionGroup = <I extends HttpApiMiddleware.AnyId, S>(sessionLo
|
||||
.middleware(sessionLocationMiddleware)
|
||||
.annotateMerge(
|
||||
OpenApi.annotations({
|
||||
identifier: "session.switchModel",
|
||||
identifier: "v2.session.switchModel",
|
||||
summary: "Switch session model",
|
||||
description: "Switch the model used by subsequent provider turns.",
|
||||
}),
|
||||
@@ -216,7 +216,7 @@ export const makeSessionGroup = <I extends HttpApiMiddleware.AnyId, S>(sessionLo
|
||||
.middleware(sessionLocationMiddleware)
|
||||
.annotateMerge(
|
||||
OpenApi.annotations({
|
||||
identifier: "session.prompt",
|
||||
identifier: "v2.session.prompt",
|
||||
summary: "Send message",
|
||||
description: "Durably admit one session input and schedule agent-loop execution unless resume is false.",
|
||||
}),
|
||||
@@ -231,7 +231,7 @@ export const makeSessionGroup = <I extends HttpApiMiddleware.AnyId, S>(sessionLo
|
||||
.middleware(sessionLocationMiddleware)
|
||||
.annotateMerge(
|
||||
OpenApi.annotations({
|
||||
identifier: "session.compact",
|
||||
identifier: "v2.session.compact",
|
||||
summary: "Compact session",
|
||||
description: "Compact a session conversation.",
|
||||
}),
|
||||
@@ -246,7 +246,7 @@ export const makeSessionGroup = <I extends HttpApiMiddleware.AnyId, S>(sessionLo
|
||||
.middleware(sessionLocationMiddleware)
|
||||
.annotateMerge(
|
||||
OpenApi.annotations({
|
||||
identifier: "session.wait",
|
||||
identifier: "v2.session.wait",
|
||||
summary: "Wait for session",
|
||||
description: "Wait for a session agent loop to become idle.",
|
||||
}),
|
||||
@@ -262,7 +262,7 @@ export const makeSessionGroup = <I extends HttpApiMiddleware.AnyId, S>(sessionLo
|
||||
.middleware(sessionLocationMiddleware)
|
||||
.annotateMerge(
|
||||
OpenApi.annotations({
|
||||
identifier: "session.revert.stage",
|
||||
identifier: "v2.session.revert.stage",
|
||||
summary: "Stage session revert",
|
||||
description: "Stage or move a reversible session boundary and optionally apply its file changes.",
|
||||
}),
|
||||
@@ -275,7 +275,7 @@ export const makeSessionGroup = <I extends HttpApiMiddleware.AnyId, S>(sessionLo
|
||||
error: [SessionNotFoundError, UnknownError],
|
||||
})
|
||||
.middleware(sessionLocationMiddleware)
|
||||
.annotateMerge(OpenApi.annotations({ identifier: "session.revert.clear", summary: "Clear staged revert" })),
|
||||
.annotateMerge(OpenApi.annotations({ identifier: "v2.session.revert.clear", summary: "Clear staged revert" })),
|
||||
)
|
||||
.add(
|
||||
HttpApiEndpoint.post("session.revert.commit", "/api/session/:sessionID/revert/commit", {
|
||||
@@ -285,7 +285,7 @@ export const makeSessionGroup = <I extends HttpApiMiddleware.AnyId, S>(sessionLo
|
||||
})
|
||||
.middleware(sessionLocationMiddleware)
|
||||
.annotateMerge(
|
||||
OpenApi.annotations({ identifier: "session.revert.commit", summary: "Commit staged revert" }),
|
||||
OpenApi.annotations({ identifier: "v2.session.revert.commit", summary: "Commit staged revert" }),
|
||||
),
|
||||
)
|
||||
.add(
|
||||
@@ -297,7 +297,7 @@ export const makeSessionGroup = <I extends HttpApiMiddleware.AnyId, S>(sessionLo
|
||||
.middleware(sessionLocationMiddleware)
|
||||
.annotateMerge(
|
||||
OpenApi.annotations({
|
||||
identifier: "session.context",
|
||||
identifier: "v2.session.context",
|
||||
summary: "Get session context",
|
||||
description: "Retrieve the active context messages for a session (all messages after the last compaction).",
|
||||
}),
|
||||
@@ -316,7 +316,7 @@ export const makeSessionGroup = <I extends HttpApiMiddleware.AnyId, S>(sessionLo
|
||||
.middleware(sessionLocationMiddleware)
|
||||
.annotateMerge(
|
||||
OpenApi.annotations({
|
||||
identifier: "session.history",
|
||||
identifier: "v2.session.history",
|
||||
summary: "Get session history",
|
||||
description:
|
||||
"Read one finite page of public durable Session events after an exclusive aggregate sequence. Newly committed events may appear on later pages.",
|
||||
@@ -335,7 +335,7 @@ export const makeSessionGroup = <I extends HttpApiMiddleware.AnyId, S>(sessionLo
|
||||
.middleware(sessionLocationMiddleware)
|
||||
.annotateMerge(
|
||||
OpenApi.annotations({
|
||||
identifier: "session.events",
|
||||
identifier: "v2.session.events",
|
||||
summary: "Subscribe to session events",
|
||||
description: "Replay durable events after an aggregate sequence, then continue with new durable events.",
|
||||
}),
|
||||
@@ -350,7 +350,7 @@ export const makeSessionGroup = <I extends HttpApiMiddleware.AnyId, S>(sessionLo
|
||||
.middleware(sessionLocationMiddleware)
|
||||
.annotateMerge(
|
||||
OpenApi.annotations({
|
||||
identifier: "session.interrupt",
|
||||
identifier: "v2.session.interrupt",
|
||||
summary: "Interrupt session execution",
|
||||
description: "Interrupt active execution owned by this OpenCode process. Idle interruption is a no-op.",
|
||||
}),
|
||||
@@ -365,7 +365,7 @@ export const makeSessionGroup = <I extends HttpApiMiddleware.AnyId, S>(sessionLo
|
||||
.middleware(sessionLocationMiddleware)
|
||||
.annotateMerge(
|
||||
OpenApi.annotations({
|
||||
identifier: "session.message",
|
||||
identifier: "v2.session.message",
|
||||
summary: "Get session message",
|
||||
description: "Retrieve one projected message owned by the Session.",
|
||||
}),
|
||||
|
||||
@@ -13,7 +13,7 @@ export const SkillGroup = HttpApiGroup.make("server.skill")
|
||||
.annotateMerge(locationQueryOpenApi)
|
||||
.annotateMerge(
|
||||
OpenApi.annotations({
|
||||
identifier: "skill.list",
|
||||
identifier: "v2.skill.list",
|
||||
summary: "List skills",
|
||||
description: "Retrieve currently registered skills.",
|
||||
}),
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/package.json",
|
||||
"name": "@opencode-ai/sdk",
|
||||
"version": "1.18.22",
|
||||
"version": "1.18.23",
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
"scripts": {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/package.json",
|
||||
"name": "@opencode-ai/server",
|
||||
"version": "1.18.22",
|
||||
"version": "1.18.23",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@opencode-ai/session-ui",
|
||||
"version": "1.18.22",
|
||||
"version": "1.18.23",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@opencode-ai/slack",
|
||||
"version": "1.18.22",
|
||||
"version": "1.18.23",
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
"scripts": {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/package.json",
|
||||
"name": "@opencode-ai/stats-app",
|
||||
"version": "1.18.22",
|
||||
"version": "1.18.23",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
@@ -19,25 +19,18 @@
|
||||
"@solidjs/meta": "catalog:",
|
||||
"@solidjs/router": "catalog:",
|
||||
"@solidjs/start": "catalog:",
|
||||
"d3-geo": "3.1.1",
|
||||
"d3-scale": "4.0.2",
|
||||
"effect": "catalog:",
|
||||
"i18n-iso-countries": "7.14.0",
|
||||
"nitro": "3.0.1-alpha.1",
|
||||
"solid-js": "catalog:",
|
||||
"sst": "catalog:",
|
||||
"topojson-client": "3.1.0",
|
||||
"vite": "catalog:",
|
||||
"world-atlas": "2.0.2"
|
||||
"vite": "catalog:"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@cloudflare/workers-types": "catalog:",
|
||||
"@types/bun": "catalog:",
|
||||
"@types/d3-geo": "3.1.0",
|
||||
"@types/d3-scale": "4.0.9",
|
||||
"@types/geojson": "7946.0.16",
|
||||
"@types/topojson-client": "3.1.5",
|
||||
"@types/topojson-specification": "1.0.5",
|
||||
"@typescript/native-preview": "catalog:",
|
||||
"typescript": "catalog:"
|
||||
},
|
||||
|
||||
@@ -126,12 +126,9 @@ const en = {
|
||||
"home.noMarketDescription": "No model rows matched this range.",
|
||||
"home.marketChart": "Market share by model author",
|
||||
"home.noData": "No data",
|
||||
"home.geoTitle": "Geo Breakdown",
|
||||
"home.geoDescription": "Tokens used by country.",
|
||||
"home.geoTitle": "Geographic Breakdown",
|
||||
"home.noGeoTitle": "No geo data",
|
||||
"home.noGeoDescription": "No geo rows matched this range.",
|
||||
"home.worldMap": "World map of token usage by country",
|
||||
"home.geoMapTitle": "Geo Breakdown map",
|
||||
"home.unknown": "Unknown",
|
||||
"home.tokenCostTitle": "Token Cost",
|
||||
"home.tokenCostDescription": "Price per 1M tokens.",
|
||||
@@ -236,10 +233,8 @@ const en = {
|
||||
"model.averageTokensSession": "Average tokens / session",
|
||||
"model.cacheRatio": "Cache Ratio",
|
||||
"model.inputTokens": "input tokens",
|
||||
"model.geoDescription": "OpenCode model tokens used by country.",
|
||||
"model.noGeoTitle": "No geo data",
|
||||
"model.noGeoDescription": "No OpenCode geo rows matched this model.",
|
||||
"model.worldMap": "World map of model token usage by country",
|
||||
"model.peersDescription": "Nearby models by recent OpenCode token volume.",
|
||||
"model.noPeersTitle": "No peers",
|
||||
"model.noPeersDescription": "Peer rankings appear after usage lands.",
|
||||
|
||||
@@ -108,11 +108,8 @@ export const dict = {
|
||||
"home.marketChart": "حصة السوق حسب مؤلف النموذج",
|
||||
"home.noData": "لا توجد بيانات",
|
||||
"home.geoTitle": "التوزيع الجغرافي",
|
||||
"home.geoDescription": "الرموز المستخدمة حسب البلد.",
|
||||
"home.noGeoTitle": "لا توجد بيانات جغرافية",
|
||||
"home.noGeoDescription": "لم تطابق أي صفوف جغرافية هذا النطاق.",
|
||||
"home.worldMap": "خريطة عالمية لاستخدام الرموز حسب البلد",
|
||||
"home.geoMapTitle": "خريطة التوزيع الجغرافي",
|
||||
"home.unknown": "غير معروف",
|
||||
"home.tokenCostTitle": "تكلفة الرموز",
|
||||
"home.tokenCostDescription": "السعر لكل مليون رمز.",
|
||||
@@ -216,10 +213,8 @@ export const dict = {
|
||||
"model.tokensSession": "الرموز / الجلسة",
|
||||
"model.cacheRatio": "نسبة التخزين المؤقت",
|
||||
"model.inputTokens": "رموز الإدخال",
|
||||
"model.geoDescription": "رموز نموذج OpenCode المستخدمة حسب البلد.",
|
||||
"model.noGeoTitle": "لا توجد بيانات جغرافية",
|
||||
"model.noGeoDescription": "لم تطابق أي صفوف جغرافية في OpenCode هذا النموذج.",
|
||||
"model.worldMap": "خريطة عالمية لاستخدام رموز النموذج حسب البلد",
|
||||
"model.peersDescription": "نماذج قريبة حسب حجم رموز OpenCode الأخير.",
|
||||
"model.noPeersTitle": "لا توجد نماذج مشابهة",
|
||||
"model.noPeersDescription": "تظهر ترتيبات النماذج المشابهة بعد وصول الاستخدام.",
|
||||
|
||||
@@ -109,11 +109,8 @@ export const dict = {
|
||||
"home.marketChart": "Participação de mercado por autor do modelo",
|
||||
"home.noData": "Sem dados",
|
||||
"home.geoTitle": "Distribuição geográfica",
|
||||
"home.geoDescription": "Tokens usados por país.",
|
||||
"home.noGeoTitle": "Sem dados geográficos",
|
||||
"home.noGeoDescription": "Nenhuma linha geográfica correspondeu a este intervalo.",
|
||||
"home.worldMap": "Mapa-múndi do uso de tokens por país",
|
||||
"home.geoMapTitle": "Mapa da distribuição geográfica",
|
||||
"home.unknown": "Desconhecido",
|
||||
"home.tokenCostTitle": "Custo de tokens",
|
||||
"home.tokenCostDescription": "Preço por 1 milhão de tokens.",
|
||||
@@ -219,10 +216,8 @@ export const dict = {
|
||||
"model.tokensSession": "Tokens / sessão",
|
||||
"model.cacheRatio": "Taxa de cache",
|
||||
"model.inputTokens": "tokens de entrada",
|
||||
"model.geoDescription": "Tokens do modelo OpenCode usados por país.",
|
||||
"model.noGeoTitle": "Sem dados geográficos",
|
||||
"model.noGeoDescription": "Nenhuma linha geográfica do OpenCode correspondeu a este modelo.",
|
||||
"model.worldMap": "Mapa-múndi do uso de tokens do modelo por país",
|
||||
"model.peersDescription": "Modelos próximos por volume recente de tokens do OpenCode.",
|
||||
"model.noPeersTitle": "Sem pares",
|
||||
"model.noPeersDescription": "Os rankings de pares aparecem depois que o uso chega.",
|
||||
|
||||
@@ -109,11 +109,8 @@ export const dict = {
|
||||
"home.marketChart": "Markedsandel efter modelforfatter",
|
||||
"home.noData": "Ingen data",
|
||||
"home.geoTitle": "Geografisk opdeling",
|
||||
"home.geoDescription": "Tokens brugt efter land.",
|
||||
"home.noGeoTitle": "Ingen geodata",
|
||||
"home.noGeoDescription": "Ingen georækker matchede dette interval.",
|
||||
"home.worldMap": "Verdenskort over tokenbrug efter land",
|
||||
"home.geoMapTitle": "Kort over geografisk opdeling",
|
||||
"home.unknown": "Ukendt",
|
||||
"home.tokenCostTitle": "Tokenomkostning",
|
||||
"home.tokenCostDescription": "Pris pr. 1 mio. tokens.",
|
||||
@@ -217,10 +214,8 @@ export const dict = {
|
||||
"model.tokensSession": "Tokens / session",
|
||||
"model.cacheRatio": "Cacheandel",
|
||||
"model.inputTokens": "inputtokens",
|
||||
"model.geoDescription": "OpenCode-modeltokens brugt efter land.",
|
||||
"model.noGeoTitle": "Ingen geodata",
|
||||
"model.noGeoDescription": "Ingen OpenCode-georækker matchede denne model.",
|
||||
"model.worldMap": "Verdenskort over modeltokenbrug efter land",
|
||||
"model.peersDescription": "Nærliggende modeller efter seneste OpenCode-tokenvolumen.",
|
||||
"model.noPeersTitle": "Ingen lignende modeller",
|
||||
"model.noPeersDescription": "Ranglister over lignende modeller vises, når brug lander.",
|
||||
|
||||
@@ -109,11 +109,8 @@ export const dict = {
|
||||
"home.marketChart": "Marktanteil nach Modellautor",
|
||||
"home.noData": "Keine Daten",
|
||||
"home.geoTitle": "Geografische Aufschlüsselung",
|
||||
"home.geoDescription": "Nach Land verwendete Tokens.",
|
||||
"home.noGeoTitle": "Keine Geodaten",
|
||||
"home.noGeoDescription": "Keine Geozeilen passten zu diesem Zeitraum.",
|
||||
"home.worldMap": "Weltkarte der Tokennutzung nach Land",
|
||||
"home.geoMapTitle": "Karte der geografischen Aufschlüsselung",
|
||||
"home.unknown": "Unbekannt",
|
||||
"home.tokenCostTitle": "Tokenkosten",
|
||||
"home.tokenCostDescription": "Preis pro 1 Mio. Tokens.",
|
||||
@@ -219,10 +216,8 @@ export const dict = {
|
||||
"model.tokensSession": "Tokens / Sitzung",
|
||||
"model.cacheRatio": "Cache-Anteil",
|
||||
"model.inputTokens": "Eingabetokens",
|
||||
"model.geoDescription": "OpenCode-Modelltokens nach Land.",
|
||||
"model.noGeoTitle": "Keine Geodaten",
|
||||
"model.noGeoDescription": "Keine OpenCode-Geozeilen passten zu diesem Modell.",
|
||||
"model.worldMap": "Weltkarte der Modelltokennutzung nach Land",
|
||||
"model.peersDescription": "Nahe Modelle nach aktuellem OpenCode-Tokenvolumen.",
|
||||
"model.noPeersTitle": "Keine Vergleichsmodelle",
|
||||
"model.noPeersDescription": "Vergleichsrankings erscheinen, nachdem Nutzung eingegangen ist.",
|
||||
|
||||
@@ -108,11 +108,8 @@ export const dict = {
|
||||
"home.marketChart": "Cuota de mercado por autor del modelo",
|
||||
"home.noData": "Sin datos",
|
||||
"home.geoTitle": "Desglose geográfico",
|
||||
"home.geoDescription": "Tokens usados por país.",
|
||||
"home.noGeoTitle": "Sin datos geográficos",
|
||||
"home.noGeoDescription": "Ninguna fila geográfica coincidió con este rango.",
|
||||
"home.worldMap": "Mapa mundial del uso de tokens por país",
|
||||
"home.geoMapTitle": "Mapa de desglose geográfico",
|
||||
"home.unknown": "Desconocido",
|
||||
"home.tokenCostTitle": "Coste de tokens",
|
||||
"home.tokenCostDescription": "Precio por 1 M de tokens.",
|
||||
@@ -218,10 +215,8 @@ export const dict = {
|
||||
"model.tokensSession": "Tokens / sesión",
|
||||
"model.cacheRatio": "Ratio de caché",
|
||||
"model.inputTokens": "tokens de entrada",
|
||||
"model.geoDescription": "Tokens del modelo de OpenCode usados por país.",
|
||||
"model.noGeoTitle": "Sin datos geográficos",
|
||||
"model.noGeoDescription": "Ninguna fila geográfica de OpenCode coincidió con este modelo.",
|
||||
"model.worldMap": "Mapa mundial del uso de tokens del modelo por país",
|
||||
"model.peersDescription": "Modelos cercanos por volumen reciente de tokens de OpenCode.",
|
||||
"model.noPeersTitle": "Sin modelos similares",
|
||||
"model.noPeersDescription": "Las clasificaciones de modelos similares aparecen después de que llegue uso.",
|
||||
|
||||
@@ -109,11 +109,8 @@ export const dict = {
|
||||
"home.marketChart": "Part de marché par auteur de modèle",
|
||||
"home.noData": "Aucune donnée",
|
||||
"home.geoTitle": "Répartition géographique",
|
||||
"home.geoDescription": "Tokens utilisés par pays.",
|
||||
"home.noGeoTitle": "Aucune donnée géographique",
|
||||
"home.noGeoDescription": "Aucune ligne géographique ne correspondait à cette période.",
|
||||
"home.worldMap": "Carte mondiale de l'utilisation des tokens par pays",
|
||||
"home.geoMapTitle": "Carte de répartition géographique",
|
||||
"home.unknown": "Inconnu",
|
||||
"home.tokenCostTitle": "Coût des tokens",
|
||||
"home.tokenCostDescription": "Prix par million de tokens.",
|
||||
@@ -220,10 +217,8 @@ export const dict = {
|
||||
"model.tokensSession": "Tokens / session",
|
||||
"model.cacheRatio": "Taux de cache",
|
||||
"model.inputTokens": "tokens d'entrée",
|
||||
"model.geoDescription": "Tokens du modèle OpenCode utilisés par pays.",
|
||||
"model.noGeoTitle": "Aucune donnée géographique",
|
||||
"model.noGeoDescription": "Aucune ligne géographique OpenCode ne correspondait à ce modèle.",
|
||||
"model.worldMap": "Carte mondiale de l'utilisation des tokens du modèle par pays",
|
||||
"model.peersDescription": "Modèles proches par volume récent de tokens OpenCode.",
|
||||
"model.noPeersTitle": "Aucun modèle proche",
|
||||
"model.noPeersDescription": "Les classements de modèles proches apparaissent après l'arrivée de l'utilisation.",
|
||||
|
||||
@@ -109,11 +109,8 @@ export const dict = {
|
||||
"home.marketChart": "Quota di mercato per autore del modello",
|
||||
"home.noData": "Nessun dato",
|
||||
"home.geoTitle": "Ripartizione geografica",
|
||||
"home.geoDescription": "Token usati per paese.",
|
||||
"home.noGeoTitle": "Nessun dato geografico",
|
||||
"home.noGeoDescription": "Nessuna riga geografica corrispondeva a questo intervallo.",
|
||||
"home.worldMap": "Mappa mondiale dell'utilizzo dei token per paese",
|
||||
"home.geoMapTitle": "Mappa della ripartizione geografica",
|
||||
"home.unknown": "Sconosciuto",
|
||||
"home.tokenCostTitle": "Costo token",
|
||||
"home.tokenCostDescription": "Prezzo per 1 M di token.",
|
||||
@@ -219,10 +216,8 @@ export const dict = {
|
||||
"model.tokensSession": "Token / sessione",
|
||||
"model.cacheRatio": "Rapporto cache",
|
||||
"model.inputTokens": "token di input",
|
||||
"model.geoDescription": "Token del modello OpenCode usati per paese.",
|
||||
"model.noGeoTitle": "Nessun dato geografico",
|
||||
"model.noGeoDescription": "Nessuna riga geografica OpenCode corrispondeva a questo modello.",
|
||||
"model.worldMap": "Mappa mondiale dell'utilizzo dei token del modello per paese",
|
||||
"model.peersDescription": "Modelli vicini per volume recente di token OpenCode.",
|
||||
"model.noPeersTitle": "Nessun modello simile",
|
||||
"model.noPeersDescription": "Le classifiche dei modelli simili appaiono dopo l'arrivo dell'utilizzo.",
|
||||
|
||||
@@ -111,11 +111,8 @@ export const dict = {
|
||||
"home.marketChart": "モデル作者別マーケットシェア",
|
||||
"home.noData": "データなし",
|
||||
"home.geoTitle": "地域別内訳",
|
||||
"home.geoDescription": "国別のトークン使用量。",
|
||||
"home.noGeoTitle": "地域データがありません",
|
||||
"home.noGeoDescription": "この期間に一致する地域行はありません。",
|
||||
"home.worldMap": "国別トークン使用量の世界地図",
|
||||
"home.geoMapTitle": "地域別内訳マップ",
|
||||
"home.unknown": "不明",
|
||||
"home.tokenCostTitle": "トークンコスト",
|
||||
"home.tokenCostDescription": "100万トークンあたりの価格。",
|
||||
@@ -220,10 +217,8 @@ export const dict = {
|
||||
"model.tokensSession": "トークン / セッション",
|
||||
"model.cacheRatio": "キャッシュ比率",
|
||||
"model.inputTokens": "入力トークン",
|
||||
"model.geoDescription": "国別のOpenCodeモデルのトークン使用量。",
|
||||
"model.noGeoTitle": "地域データがありません",
|
||||
"model.noGeoDescription": "このモデルに一致するOpenCode地域行はありません。",
|
||||
"model.worldMap": "国別モデル別トークン使用量の世界地図",
|
||||
"model.peersDescription": "最近のOpenCodeトークン量が近いモデル。",
|
||||
"model.noPeersTitle": "類似モデルがありません",
|
||||
"model.noPeersDescription": "使用量が届くと類似モデルのランキングが表示されます。",
|
||||
|
||||
@@ -111,11 +111,8 @@ export const dict = {
|
||||
"home.marketChart": "모델 작성자별 시장 점유율",
|
||||
"home.noData": "데이터 없음",
|
||||
"home.geoTitle": "지역별 분포",
|
||||
"home.geoDescription": "국가별 사용 토큰입니다.",
|
||||
"home.noGeoTitle": "지역 데이터 없음",
|
||||
"home.noGeoDescription": "이 범위에 맞는 지역 행이 없습니다.",
|
||||
"home.worldMap": "국가별 토큰 사용량 세계 지도",
|
||||
"home.geoMapTitle": "지역별 분포 지도",
|
||||
"home.unknown": "알 수 없음",
|
||||
"home.tokenCostTitle": "토큰 비용",
|
||||
"home.tokenCostDescription": "100만 토큰당 가격입니다.",
|
||||
@@ -219,10 +216,8 @@ export const dict = {
|
||||
"model.tokensSession": "토큰 / 세션",
|
||||
"model.cacheRatio": "캐시 비율",
|
||||
"model.inputTokens": "입력 토큰",
|
||||
"model.geoDescription": "국가별 OpenCode 모델 토큰 사용량입니다.",
|
||||
"model.noGeoTitle": "지역 데이터 없음",
|
||||
"model.noGeoDescription": "이 모델과 일치하는 OpenCode 지역 행이 없습니다.",
|
||||
"model.worldMap": "국가별 모델 토큰 사용량 세계 지도",
|
||||
"model.peersDescription": "최근 OpenCode 토큰 볼륨이 가까운 모델입니다.",
|
||||
"model.noPeersTitle": "비슷한 모델 없음",
|
||||
"model.noPeersDescription": "사용량이 들어오면 비슷한 모델 순위가 표시됩니다.",
|
||||
|
||||
@@ -109,11 +109,8 @@ export const dict = {
|
||||
"home.marketChart": "Markedsandel etter modellforfatter",
|
||||
"home.noData": "Ingen data",
|
||||
"home.geoTitle": "Geografisk fordeling",
|
||||
"home.geoDescription": "Tokens brukt etter land.",
|
||||
"home.noGeoTitle": "Ingen geodata",
|
||||
"home.noGeoDescription": "Ingen georader matchet dette intervallet.",
|
||||
"home.worldMap": "Verdenskart over tokenbruk etter land",
|
||||
"home.geoMapTitle": "Kart over geografisk fordeling",
|
||||
"home.unknown": "Ukjent",
|
||||
"home.tokenCostTitle": "Tokenkostnad",
|
||||
"home.tokenCostDescription": "Pris per 1 mill. tokens.",
|
||||
@@ -218,10 +215,8 @@ export const dict = {
|
||||
"model.tokensSession": "Tokens / økt",
|
||||
"model.cacheRatio": "Cacheandel",
|
||||
"model.inputTokens": "inndata-tokens",
|
||||
"model.geoDescription": "OpenCode-modelltokens brukt etter land.",
|
||||
"model.noGeoTitle": "Ingen geodata",
|
||||
"model.noGeoDescription": "Ingen OpenCode-georader matchet denne modellen.",
|
||||
"model.worldMap": "Verdenskart over modelltokenbruk etter land",
|
||||
"model.peersDescription": "Nærliggende modeller etter nylig OpenCode-tokenvolum.",
|
||||
"model.noPeersTitle": "Ingen lignende modeller",
|
||||
"model.noPeersDescription": "Rangeringer for lignende modeller vises etter at bruk lander.",
|
||||
|
||||
@@ -108,11 +108,8 @@ export const dict = {
|
||||
"home.marketChart": "Udział w rynku według autora modelu",
|
||||
"home.noData": "Brak danych",
|
||||
"home.geoTitle": "Podział geograficzny",
|
||||
"home.geoDescription": "Tokeny użyte według kraju.",
|
||||
"home.noGeoTitle": "Brak danych geograficznych",
|
||||
"home.noGeoDescription": "Żadne wiersze geograficzne nie pasowały do tego zakresu.",
|
||||
"home.worldMap": "Mapa świata użycia tokenów według kraju",
|
||||
"home.geoMapTitle": "Mapa podziału geograficznego",
|
||||
"home.unknown": "Nieznane",
|
||||
"home.tokenCostTitle": "Koszt tokenów",
|
||||
"home.tokenCostDescription": "Cena za 1 mln tokenów.",
|
||||
@@ -217,10 +214,8 @@ export const dict = {
|
||||
"model.tokensSession": "Tokeny / sesja",
|
||||
"model.cacheRatio": "Współczynnik cache",
|
||||
"model.inputTokens": "tokeny wejściowe",
|
||||
"model.geoDescription": "Tokeny modelu OpenCode użyte według kraju.",
|
||||
"model.noGeoTitle": "Brak danych geograficznych",
|
||||
"model.noGeoDescription": "Żadne wiersze geograficzne OpenCode nie pasowały do tego modelu.",
|
||||
"model.worldMap": "Mapa świata użycia tokenów modelu według kraju",
|
||||
"model.peersDescription": "Pobliskie modele według ostatniego wolumenu tokenów OpenCode.",
|
||||
"model.noPeersTitle": "Brak podobnych modeli",
|
||||
"model.noPeersDescription": "Rankingi podobnych modeli pojawią się po nadejściu użycia.",
|
||||
|
||||
@@ -109,11 +109,8 @@ export const dict = {
|
||||
"home.marketChart": "Доля рынка по автору модели",
|
||||
"home.noData": "Нет данных",
|
||||
"home.geoTitle": "Географический разрез",
|
||||
"home.geoDescription": "Токены, использованные по странам.",
|
||||
"home.noGeoTitle": "Нет геоданных",
|
||||
"home.noGeoDescription": "Нет географических строк для этого диапазона.",
|
||||
"home.worldMap": "Карта мира использования токенов по странам",
|
||||
"home.geoMapTitle": "Карта географического разреза",
|
||||
"home.unknown": "Неизвестно",
|
||||
"home.tokenCostTitle": "Стоимость токенов",
|
||||
"home.tokenCostDescription": "Цена за 1 млн токенов.",
|
||||
@@ -219,10 +216,8 @@ export const dict = {
|
||||
"model.tokensSession": "Токены / сеанс",
|
||||
"model.cacheRatio": "Доля кэша",
|
||||
"model.inputTokens": "входные токены",
|
||||
"model.geoDescription": "Токены модели OpenCode, использованные по странам.",
|
||||
"model.noGeoTitle": "Нет геоданных",
|
||||
"model.noGeoDescription": "Нет географических строк OpenCode для этой модели.",
|
||||
"model.worldMap": "Карта мира использования токенов модели по странам",
|
||||
"model.peersDescription": "Близкие модели по недавнему объему токенов OpenCode.",
|
||||
"model.noPeersTitle": "Нет похожих моделей",
|
||||
"model.noPeersDescription": "Рейтинги похожих моделей появятся после использования.",
|
||||
|
||||
@@ -110,11 +110,8 @@ export const dict = {
|
||||
"home.marketChart": "ส่วนแบ่งตลาดตามผู้สร้างโมเดล",
|
||||
"home.noData": "ไม่มีข้อมูล",
|
||||
"home.geoTitle": "แยกตามภูมิศาสตร์",
|
||||
"home.geoDescription": "token ที่ใช้แยกตามประเทศ",
|
||||
"home.noGeoTitle": "ไม่มีข้อมูลภูมิศาสตร์",
|
||||
"home.noGeoDescription": "ไม่มีแถวภูมิศาสตร์ที่ตรงกับช่วงเวลานี้",
|
||||
"home.worldMap": "แผนที่โลกของการใช้ token แยกตามประเทศ",
|
||||
"home.geoMapTitle": "แผนที่แยกตามภูมิศาสตร์",
|
||||
"home.unknown": "ไม่ทราบ",
|
||||
"home.tokenCostTitle": "ต้นทุน Token",
|
||||
"home.tokenCostDescription": "ราคาต่อ 1 ล้าน token",
|
||||
@@ -219,10 +216,8 @@ export const dict = {
|
||||
"model.tokensSession": "Token / เซสชัน",
|
||||
"model.cacheRatio": "อัตราแคช",
|
||||
"model.inputTokens": "input token",
|
||||
"model.geoDescription": "token ของโมเดล OpenCode ที่ใช้แยกตามประเทศ",
|
||||
"model.noGeoTitle": "ไม่มีข้อมูลภูมิศาสตร์",
|
||||
"model.noGeoDescription": "ไม่มีแถวภูมิศาสตร์ของ OpenCode ที่ตรงกับโมเดลนี้",
|
||||
"model.worldMap": "แผนที่โลกของการใช้ token ของโมเดลแยกตามประเทศ",
|
||||
"model.peersDescription": "โมเดลใกล้เคียงตามปริมาณ token ล่าสุดของ OpenCode",
|
||||
"model.noPeersTitle": "ไม่มีโมเดลใกล้เคียง",
|
||||
"model.noPeersDescription": "อันดับโมเดลใกล้เคียงจะแสดงหลังจากมีการใช้งานเข้ามา",
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user