Compare commits
4
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f89f648d11 | ||
|
|
c28ea79662 | ||
|
|
08bef1696d | ||
|
|
6186ae5e43 |
@@ -1,12 +0,0 @@
|
|||||||
.git
|
|
||||||
.opencode
|
|
||||||
.sst
|
|
||||||
.turbo
|
|
||||||
.wrangler
|
|
||||||
node_modules
|
|
||||||
**/node_modules
|
|
||||||
**/.output
|
|
||||||
**/dist
|
|
||||||
**/.turbo
|
|
||||||
**/.vite
|
|
||||||
**/coverage
|
|
||||||
@@ -9,15 +9,9 @@ on:
|
|||||||
|
|
||||||
concurrency: ${{ github.workflow }}-${{ github.ref }}
|
concurrency: ${{ github.workflow }}-${{ github.ref }}
|
||||||
|
|
||||||
permissions:
|
|
||||||
contents: read
|
|
||||||
id-token: write
|
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
deploy:
|
deploy:
|
||||||
if: github.repository == 'anomalyco/opencode' && (github.ref_name == 'dev' || github.ref_name == 'production')
|
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
environment: ${{ github.ref_name }}
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@f43a0e5ff2bd294095638e18286ca9a3d1956744 # v3.6.0
|
- uses: actions/checkout@f43a0e5ff2bd294095638e18286ca9a3d1956744 # v3.6.0
|
||||||
|
|
||||||
@@ -27,12 +21,6 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
node-version: "24"
|
node-version: "24"
|
||||||
|
|
||||||
- uses: aws-actions/configure-aws-credentials@7474bc4690e29a8392af63c5b98e7449536d5c3a # v4.3.1
|
|
||||||
with:
|
|
||||||
role-to-assume: ${{ vars.AWS_DEPLOY_ROLE_ARN }}
|
|
||||||
role-session-name: opencode-${{ github.run_id }}
|
|
||||||
aws-region: us-east-1
|
|
||||||
|
|
||||||
- run: bun sst deploy --stage=${{ github.ref_name }}
|
- run: bun sst deploy --stage=${{ github.ref_name }}
|
||||||
env:
|
env:
|
||||||
CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
|
CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
- To regenerate the JavaScript SDK, run `./packages/sdk/js/script/build.ts`.
|
- To regenerate the JavaScript SDK, run `./packages/sdk/js/script/build.ts`.
|
||||||
|
- ALWAYS USE PARALLEL TOOLS WHEN APPLICABLE.
|
||||||
- The default branch in this repo is `dev`.
|
- The default branch in this repo is `dev`.
|
||||||
- Local `main` ref may not exist; use `dev` or `origin/dev` for diffs.
|
- Local `main` ref may not exist; use `dev` or `origin/dev` for diffs.
|
||||||
|
- Prefer automation: execute requested actions without confirmation unless blocked by missing info or safety/irreversibility.
|
||||||
|
|
||||||
## Commits and PR Titles
|
## Commits and PR Titles
|
||||||
|
|
||||||
|
|||||||
@@ -23,13 +23,13 @@
|
|||||||
"oxlint-tsgolint": "0.21.0",
|
"oxlint-tsgolint": "0.21.0",
|
||||||
"prettier": "3.6.2",
|
"prettier": "3.6.2",
|
||||||
"semver": "^7.6.0",
|
"semver": "^7.6.0",
|
||||||
"sst": "catalog:",
|
"sst": "4.13.1",
|
||||||
"turbo": "2.8.13",
|
"turbo": "2.8.13",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
"packages/app": {
|
"packages/app": {
|
||||||
"name": "@opencode-ai/app",
|
"name": "@opencode-ai/app",
|
||||||
"version": "1.15.11",
|
"version": "1.15.10",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@kobalte/core": "catalog:",
|
"@kobalte/core": "catalog:",
|
||||||
"@opencode-ai/core": "workspace:*",
|
"@opencode-ai/core": "workspace:*",
|
||||||
@@ -85,7 +85,7 @@
|
|||||||
},
|
},
|
||||||
"packages/console/app": {
|
"packages/console/app": {
|
||||||
"name": "@opencode-ai/console-app",
|
"name": "@opencode-ai/console-app",
|
||||||
"version": "1.15.11",
|
"version": "1.15.10",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@cloudflare/vite-plugin": "1.15.2",
|
"@cloudflare/vite-plugin": "1.15.2",
|
||||||
"@ibm/plex": "6.4.1",
|
"@ibm/plex": "6.4.1",
|
||||||
@@ -102,7 +102,6 @@
|
|||||||
"@solidjs/router": "catalog:",
|
"@solidjs/router": "catalog:",
|
||||||
"@solidjs/start": "catalog:",
|
"@solidjs/start": "catalog:",
|
||||||
"@stripe/stripe-js": "8.6.1",
|
"@stripe/stripe-js": "8.6.1",
|
||||||
"@upstash/redis": "1.38.0",
|
|
||||||
"chart.js": "4.5.1",
|
"chart.js": "4.5.1",
|
||||||
"nitro": "3.0.1-alpha.1",
|
"nitro": "3.0.1-alpha.1",
|
||||||
"solid-js": "catalog:",
|
"solid-js": "catalog:",
|
||||||
@@ -121,7 +120,7 @@
|
|||||||
},
|
},
|
||||||
"packages/console/core": {
|
"packages/console/core": {
|
||||||
"name": "@opencode-ai/console-core",
|
"name": "@opencode-ai/console-core",
|
||||||
"version": "1.15.11",
|
"version": "1.15.10",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@aws-sdk/client-sts": "3.782.0",
|
"@aws-sdk/client-sts": "3.782.0",
|
||||||
"@jsx-email/render": "1.1.1",
|
"@jsx-email/render": "1.1.1",
|
||||||
@@ -148,7 +147,7 @@
|
|||||||
},
|
},
|
||||||
"packages/console/function": {
|
"packages/console/function": {
|
||||||
"name": "@opencode-ai/console-function",
|
"name": "@opencode-ai/console-function",
|
||||||
"version": "1.15.11",
|
"version": "1.15.10",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@ai-sdk/anthropic": "3.0.64",
|
"@ai-sdk/anthropic": "3.0.64",
|
||||||
"@ai-sdk/openai": "3.0.48",
|
"@ai-sdk/openai": "3.0.48",
|
||||||
@@ -170,7 +169,7 @@
|
|||||||
},
|
},
|
||||||
"packages/console/mail": {
|
"packages/console/mail": {
|
||||||
"name": "@opencode-ai/console-mail",
|
"name": "@opencode-ai/console-mail",
|
||||||
"version": "1.15.11",
|
"version": "1.15.10",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@jsx-email/all": "2.2.3",
|
"@jsx-email/all": "2.2.3",
|
||||||
"@jsx-email/cli": "1.4.3",
|
"@jsx-email/cli": "1.4.3",
|
||||||
@@ -192,29 +191,9 @@
|
|||||||
"cloudflare": "5.2.0",
|
"cloudflare": "5.2.0",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
"packages/console/support": {
|
|
||||||
"name": "@opencode-ai/console-support",
|
|
||||||
"version": "1.15.11",
|
|
||||||
"dependencies": {
|
|
||||||
"@cloudflare/vite-plugin": "1.15.2",
|
|
||||||
"@opencode-ai/console-core": "workspace:*",
|
|
||||||
"@solidjs/meta": "catalog:",
|
|
||||||
"@solidjs/router": "catalog:",
|
|
||||||
"@solidjs/start": "catalog:",
|
|
||||||
"nitro": "3.0.1-alpha.1",
|
|
||||||
"solid-js": "catalog:",
|
|
||||||
"vite": "catalog:",
|
|
||||||
},
|
|
||||||
"devDependencies": {
|
|
||||||
"@types/bun": "catalog:",
|
|
||||||
"@typescript/native-preview": "catalog:",
|
|
||||||
"typescript": "catalog:",
|
|
||||||
"wrangler": "4.50.0",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
"packages/core": {
|
"packages/core": {
|
||||||
"name": "@opencode-ai/core",
|
"name": "@opencode-ai/core",
|
||||||
"version": "1.15.11",
|
"version": "1.15.10",
|
||||||
"bin": {
|
"bin": {
|
||||||
"opencode": "./bin/opencode",
|
"opencode": "./bin/opencode",
|
||||||
},
|
},
|
||||||
@@ -227,8 +206,8 @@
|
|||||||
"@ai-sdk/cohere": "3.0.27",
|
"@ai-sdk/cohere": "3.0.27",
|
||||||
"@ai-sdk/deepinfra": "2.0.41",
|
"@ai-sdk/deepinfra": "2.0.41",
|
||||||
"@ai-sdk/gateway": "3.0.104",
|
"@ai-sdk/gateway": "3.0.104",
|
||||||
"@ai-sdk/google": "3.0.73",
|
"@ai-sdk/google": "3.0.75",
|
||||||
"@ai-sdk/google-vertex": "4.0.128",
|
"@ai-sdk/google-vertex": "4.0.131",
|
||||||
"@ai-sdk/groq": "3.0.31",
|
"@ai-sdk/groq": "3.0.31",
|
||||||
"@ai-sdk/mistral": "3.0.27",
|
"@ai-sdk/mistral": "3.0.27",
|
||||||
"@ai-sdk/openai": "3.0.53",
|
"@ai-sdk/openai": "3.0.53",
|
||||||
@@ -275,7 +254,7 @@
|
|||||||
},
|
},
|
||||||
"packages/desktop": {
|
"packages/desktop": {
|
||||||
"name": "@opencode-ai/desktop",
|
"name": "@opencode-ai/desktop",
|
||||||
"version": "1.15.11",
|
"version": "1.15.10",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@zip.js/zip.js": "2.7.62",
|
"@zip.js/zip.js": "2.7.62",
|
||||||
"drizzle-orm": "catalog:",
|
"drizzle-orm": "catalog:",
|
||||||
@@ -330,7 +309,7 @@
|
|||||||
},
|
},
|
||||||
"packages/effect-drizzle-sqlite": {
|
"packages/effect-drizzle-sqlite": {
|
||||||
"name": "@opencode-ai/effect-drizzle-sqlite",
|
"name": "@opencode-ai/effect-drizzle-sqlite",
|
||||||
"version": "1.15.11",
|
"version": "1.15.10",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"drizzle-orm": "catalog:",
|
"drizzle-orm": "catalog:",
|
||||||
"effect": "catalog:",
|
"effect": "catalog:",
|
||||||
@@ -344,7 +323,7 @@
|
|||||||
},
|
},
|
||||||
"packages/enterprise": {
|
"packages/enterprise": {
|
||||||
"name": "@opencode-ai/enterprise",
|
"name": "@opencode-ai/enterprise",
|
||||||
"version": "1.15.11",
|
"version": "1.15.10",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@opencode-ai/core": "workspace:*",
|
"@opencode-ai/core": "workspace:*",
|
||||||
"@opencode-ai/ui": "workspace:*",
|
"@opencode-ai/ui": "workspace:*",
|
||||||
@@ -374,7 +353,7 @@
|
|||||||
},
|
},
|
||||||
"packages/function": {
|
"packages/function": {
|
||||||
"name": "@opencode-ai/function",
|
"name": "@opencode-ai/function",
|
||||||
"version": "1.15.11",
|
"version": "1.15.10",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@octokit/auth-app": "8.0.1",
|
"@octokit/auth-app": "8.0.1",
|
||||||
"@octokit/rest": "catalog:",
|
"@octokit/rest": "catalog:",
|
||||||
@@ -390,7 +369,7 @@
|
|||||||
},
|
},
|
||||||
"packages/http-recorder": {
|
"packages/http-recorder": {
|
||||||
"name": "@opencode-ai/http-recorder",
|
"name": "@opencode-ai/http-recorder",
|
||||||
"version": "1.15.11",
|
"version": "1.15.10",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@effect/platform-node": "catalog:",
|
"@effect/platform-node": "catalog:",
|
||||||
"effect": "catalog:",
|
"effect": "catalog:",
|
||||||
@@ -403,7 +382,7 @@
|
|||||||
},
|
},
|
||||||
"packages/llm": {
|
"packages/llm": {
|
||||||
"name": "@opencode-ai/llm",
|
"name": "@opencode-ai/llm",
|
||||||
"version": "1.15.11",
|
"version": "1.15.10",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@smithy/eventstream-codec": "4.2.14",
|
"@smithy/eventstream-codec": "4.2.14",
|
||||||
"@smithy/util-utf8": "4.2.2",
|
"@smithy/util-utf8": "4.2.2",
|
||||||
@@ -421,7 +400,7 @@
|
|||||||
},
|
},
|
||||||
"packages/opencode": {
|
"packages/opencode": {
|
||||||
"name": "opencode",
|
"name": "opencode",
|
||||||
"version": "1.15.11",
|
"version": "1.15.10",
|
||||||
"bin": {
|
"bin": {
|
||||||
"opencode": "./bin/opencode",
|
"opencode": "./bin/opencode",
|
||||||
},
|
},
|
||||||
@@ -437,8 +416,8 @@
|
|||||||
"@ai-sdk/cohere": "3.0.27",
|
"@ai-sdk/cohere": "3.0.27",
|
||||||
"@ai-sdk/deepinfra": "2.0.41",
|
"@ai-sdk/deepinfra": "2.0.41",
|
||||||
"@ai-sdk/gateway": "3.0.104",
|
"@ai-sdk/gateway": "3.0.104",
|
||||||
"@ai-sdk/google": "3.0.73",
|
"@ai-sdk/google": "3.0.75",
|
||||||
"@ai-sdk/google-vertex": "4.0.128",
|
"@ai-sdk/google-vertex": "4.0.131",
|
||||||
"@ai-sdk/groq": "3.0.31",
|
"@ai-sdk/groq": "3.0.31",
|
||||||
"@ai-sdk/mistral": "3.0.27",
|
"@ai-sdk/mistral": "3.0.27",
|
||||||
"@ai-sdk/openai": "3.0.53",
|
"@ai-sdk/openai": "3.0.53",
|
||||||
@@ -478,7 +457,6 @@
|
|||||||
"@solid-primitives/event-bus": "1.1.2",
|
"@solid-primitives/event-bus": "1.1.2",
|
||||||
"@solid-primitives/scheduled": "1.5.2",
|
"@solid-primitives/scheduled": "1.5.2",
|
||||||
"@standard-schema/spec": "1.0.0",
|
"@standard-schema/spec": "1.0.0",
|
||||||
"@types/ws": "8.18.1",
|
|
||||||
"@zip.js/zip.js": "2.7.62",
|
"@zip.js/zip.js": "2.7.62",
|
||||||
"ai": "catalog:",
|
"ai": "catalog:",
|
||||||
"ai-gateway-provider": "3.1.2",
|
"ai-gateway-provider": "3.1.2",
|
||||||
@@ -520,7 +498,6 @@
|
|||||||
"vscode-jsonrpc": "8.2.1",
|
"vscode-jsonrpc": "8.2.1",
|
||||||
"web-tree-sitter": "0.25.10",
|
"web-tree-sitter": "0.25.10",
|
||||||
"which": "6.0.1",
|
"which": "6.0.1",
|
||||||
"ws": "8.21.0",
|
|
||||||
"xdg-basedir": "5.1.0",
|
"xdg-basedir": "5.1.0",
|
||||||
"yargs": "18.0.0",
|
"yargs": "18.0.0",
|
||||||
"zod": "catalog:",
|
"zod": "catalog:",
|
||||||
@@ -561,7 +538,7 @@
|
|||||||
},
|
},
|
||||||
"packages/plugin": {
|
"packages/plugin": {
|
||||||
"name": "@opencode-ai/plugin",
|
"name": "@opencode-ai/plugin",
|
||||||
"version": "1.15.11",
|
"version": "1.15.10",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@opencode-ai/sdk": "workspace:*",
|
"@opencode-ai/sdk": "workspace:*",
|
||||||
"effect": "catalog:",
|
"effect": "catalog:",
|
||||||
@@ -577,9 +554,9 @@
|
|||||||
"typescript": "catalog:",
|
"typescript": "catalog:",
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"@opentui/core": ">=0.2.16",
|
"@opentui/core": ">=0.2.15",
|
||||||
"@opentui/keymap": ">=0.2.16",
|
"@opentui/keymap": ">=0.2.15",
|
||||||
"@opentui/solid": ">=0.2.16",
|
"@opentui/solid": ">=0.2.15",
|
||||||
},
|
},
|
||||||
"optionalPeers": [
|
"optionalPeers": [
|
||||||
"@opentui/core",
|
"@opentui/core",
|
||||||
@@ -599,7 +576,7 @@
|
|||||||
},
|
},
|
||||||
"packages/sdk/js": {
|
"packages/sdk/js": {
|
||||||
"name": "@opencode-ai/sdk",
|
"name": "@opencode-ai/sdk",
|
||||||
"version": "1.15.11",
|
"version": "1.15.10",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"cross-spawn": "catalog:",
|
"cross-spawn": "catalog:",
|
||||||
},
|
},
|
||||||
@@ -614,7 +591,7 @@
|
|||||||
},
|
},
|
||||||
"packages/slack": {
|
"packages/slack": {
|
||||||
"name": "@opencode-ai/slack",
|
"name": "@opencode-ai/slack",
|
||||||
"version": "1.15.11",
|
"version": "1.15.10",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@opencode-ai/sdk": "workspace:*",
|
"@opencode-ai/sdk": "workspace:*",
|
||||||
"@slack/bolt": "^3.17.1",
|
"@slack/bolt": "^3.17.1",
|
||||||
@@ -625,67 +602,6 @@
|
|||||||
"typescript": "catalog:",
|
"typescript": "catalog:",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
"packages/stats/app": {
|
|
||||||
"name": "@opencode-ai/stats-app",
|
|
||||||
"version": "1.15.11",
|
|
||||||
"dependencies": {
|
|
||||||
"@ibm/plex": "6.4.1",
|
|
||||||
"@opencode-ai/stats-core": "workspace:*",
|
|
||||||
"@opencode-ai/ui": "workspace:*",
|
|
||||||
"@solidjs/meta": "catalog:",
|
|
||||||
"@solidjs/router": "catalog:",
|
|
||||||
"@solidjs/start": "catalog:",
|
|
||||||
"d3-scale": "4.0.2",
|
|
||||||
"effect": "catalog:",
|
|
||||||
"nitro": "3.0.1-alpha.1",
|
|
||||||
"solid-js": "catalog:",
|
|
||||||
"vite": "catalog:",
|
|
||||||
},
|
|
||||||
"devDependencies": {
|
|
||||||
"@cloudflare/workers-types": "catalog:",
|
|
||||||
"@types/bun": "catalog:",
|
|
||||||
"@types/d3-scale": "4.0.9",
|
|
||||||
"@typescript/native-preview": "catalog:",
|
|
||||||
"typescript": "catalog:",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
"packages/stats/core": {
|
|
||||||
"name": "@opencode-ai/stats-core",
|
|
||||||
"version": "1.15.11",
|
|
||||||
"dependencies": {
|
|
||||||
"@aws-sdk/client-athena": "3.933.0",
|
|
||||||
"@planetscale/database": "1.19.0",
|
|
||||||
"drizzle-orm": "catalog:",
|
|
||||||
"effect": "catalog:",
|
|
||||||
"sst": "catalog:",
|
|
||||||
},
|
|
||||||
"devDependencies": {
|
|
||||||
"@tsconfig/node22": "catalog:",
|
|
||||||
"@types/bun": "catalog:",
|
|
||||||
"@types/node": "catalog:",
|
|
||||||
"@typescript/native-preview": "catalog:",
|
|
||||||
"drizzle-kit": "catalog:",
|
|
||||||
"typescript": "catalog:",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
"packages/stats/server": {
|
|
||||||
"name": "@opencode-ai/stats-server",
|
|
||||||
"version": "1.15.11",
|
|
||||||
"dependencies": {
|
|
||||||
"@aws-sdk/client-firehose": "3.933.0",
|
|
||||||
"@effect/platform-node": "catalog:",
|
|
||||||
"@opencode-ai/stats-core": "workspace:*",
|
|
||||||
"effect": "catalog:",
|
|
||||||
"sst": "catalog:",
|
|
||||||
},
|
|
||||||
"devDependencies": {
|
|
||||||
"@tsconfig/node22": "catalog:",
|
|
||||||
"@types/bun": "catalog:",
|
|
||||||
"@types/node": "catalog:",
|
|
||||||
"@typescript/native-preview": "catalog:",
|
|
||||||
"typescript": "catalog:",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
"packages/storybook": {
|
"packages/storybook": {
|
||||||
"name": "@opencode-ai/storybook",
|
"name": "@opencode-ai/storybook",
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
@@ -710,7 +626,7 @@
|
|||||||
},
|
},
|
||||||
"packages/ui": {
|
"packages/ui": {
|
||||||
"name": "@opencode-ai/ui",
|
"name": "@opencode-ai/ui",
|
||||||
"version": "1.15.11",
|
"version": "1.15.10",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@kobalte/core": "catalog:",
|
"@kobalte/core": "catalog:",
|
||||||
"@opencode-ai/core": "workspace:*",
|
"@opencode-ai/core": "workspace:*",
|
||||||
@@ -759,7 +675,7 @@
|
|||||||
},
|
},
|
||||||
"packages/web": {
|
"packages/web": {
|
||||||
"name": "@opencode-ai/web",
|
"name": "@opencode-ai/web",
|
||||||
"version": "1.15.11",
|
"version": "1.15.10",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@astrojs/cloudflare": "12.6.3",
|
"@astrojs/cloudflare": "12.6.3",
|
||||||
"@astrojs/markdown-remark": "6.3.1",
|
"@astrojs/markdown-remark": "6.3.1",
|
||||||
@@ -802,9 +718,8 @@
|
|||||||
],
|
],
|
||||||
"patchedDependencies": {
|
"patchedDependencies": {
|
||||||
"solid-js@1.9.10": "patches/solid-js@1.9.10.patch",
|
"solid-js@1.9.10": "patches/solid-js@1.9.10.patch",
|
||||||
"virtua@0.49.1": "patches/virtua@0.49.1.patch",
|
|
||||||
"gcp-metadata@8.1.2": "patches/gcp-metadata@8.1.2.patch",
|
|
||||||
"@ai-sdk/xai@3.0.82": "patches/@ai-sdk%2Fxai@3.0.82.patch",
|
"@ai-sdk/xai@3.0.82": "patches/@ai-sdk%2Fxai@3.0.82.patch",
|
||||||
|
"virtua@0.49.1": "patches/virtua@0.49.1.patch",
|
||||||
"@standard-community/standard-openapi@0.2.9": "patches/@standard-community%2Fstandard-openapi@0.2.9.patch",
|
"@standard-community/standard-openapi@0.2.9": "patches/@standard-community%2Fstandard-openapi@0.2.9.patch",
|
||||||
"@npmcli/agent@4.0.0": "patches/@npmcli%2Fagent@4.0.0.patch",
|
"@npmcli/agent@4.0.0": "patches/@npmcli%2Fagent@4.0.0.patch",
|
||||||
"@silvia-odwyer/photon-node@0.3.4": "patches/@silvia-odwyer%2Fphoton-node@0.3.4.patch",
|
"@silvia-odwyer/photon-node@0.3.4": "patches/@silvia-odwyer%2Fphoton-node@0.3.4.patch",
|
||||||
@@ -827,9 +742,9 @@
|
|||||||
"@npmcli/arborist": "9.4.0",
|
"@npmcli/arborist": "9.4.0",
|
||||||
"@octokit/rest": "22.0.0",
|
"@octokit/rest": "22.0.0",
|
||||||
"@openauthjs/openauth": "0.0.0-20250322224806",
|
"@openauthjs/openauth": "0.0.0-20250322224806",
|
||||||
"@opentui/core": "0.2.16",
|
"@opentui/core": "0.2.15",
|
||||||
"@opentui/keymap": "0.2.16",
|
"@opentui/keymap": "0.2.15",
|
||||||
"@opentui/solid": "0.2.16",
|
"@opentui/solid": "0.2.15",
|
||||||
"@pierre/diffs": "1.1.0-beta.18",
|
"@pierre/diffs": "1.1.0-beta.18",
|
||||||
"@playwright/test": "1.59.1",
|
"@playwright/test": "1.59.1",
|
||||||
"@sentry/solid": "10.36.0",
|
"@sentry/solid": "10.36.0",
|
||||||
@@ -867,7 +782,6 @@
|
|||||||
"shiki": "3.20.0",
|
"shiki": "3.20.0",
|
||||||
"solid-js": "1.9.10",
|
"solid-js": "1.9.10",
|
||||||
"solid-list": "0.3.0",
|
"solid-list": "0.3.0",
|
||||||
"sst": "4.13.1",
|
|
||||||
"tailwindcss": "4.1.11",
|
"tailwindcss": "4.1.11",
|
||||||
"typescript": "5.8.2",
|
"typescript": "5.8.2",
|
||||||
"ulid": "3.0.1",
|
"ulid": "3.0.1",
|
||||||
@@ -919,9 +833,9 @@
|
|||||||
|
|
||||||
"@ai-sdk/gateway": ["@ai-sdk/gateway@3.0.104", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.23", "@vercel/oidc": "3.2.0" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-ZKX5n74io8VIRlhIMSLWVlvT3sXC8Z7cZ9GHuWBWZDVi96+62AIsWuLGvMfcBA1STYuSoDrp6rIziZmvrTq0TA=="],
|
"@ai-sdk/gateway": ["@ai-sdk/gateway@3.0.104", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.23", "@vercel/oidc": "3.2.0" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-ZKX5n74io8VIRlhIMSLWVlvT3sXC8Z7cZ9GHuWBWZDVi96+62AIsWuLGvMfcBA1STYuSoDrp6rIziZmvrTq0TA=="],
|
||||||
|
|
||||||
"@ai-sdk/google": ["@ai-sdk/google@3.0.73", "", { "dependencies": { "@ai-sdk/provider": "3.0.10", "@ai-sdk/provider-utils": "4.0.27" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-o2MuIeyvZrFIeIbnbA8Thrr63irdyUBh0uWBZ2lY6yFeXuE/tcwyXF74bDKS4KvTu84uFpQfpbS/LXHGKKXz+g=="],
|
"@ai-sdk/google": ["@ai-sdk/google@3.0.75", "", { "dependencies": { "@ai-sdk/provider": "3.0.10", "@ai-sdk/provider-utils": "4.0.27" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-XAm31ftiOrzlb8NjDzT7kw0xw+4lmgFdGFn1QKM73nXFFKyN1kWLESBV75UGNfjXP8X1YJ0YydnMVqO0jaPghw=="],
|
||||||
|
|
||||||
"@ai-sdk/google-vertex": ["@ai-sdk/google-vertex@4.0.128", "", { "dependencies": { "@ai-sdk/anthropic": "3.0.77", "@ai-sdk/google": "3.0.73", "@ai-sdk/openai-compatible": "2.0.47", "@ai-sdk/provider": "3.0.10", "@ai-sdk/provider-utils": "4.0.27", "google-auth-library": "^10.5.0" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-jK8fixb4km2yfgvb9DUFQRpV/jiDB0v9gyxHoHfPydaQvz+CpAz8DTt1quyaM+Wg9G2R8Zo68CYmHbIkUqW2AA=="],
|
"@ai-sdk/google-vertex": ["@ai-sdk/google-vertex@4.0.131", "", { "dependencies": { "@ai-sdk/anthropic": "3.0.78", "@ai-sdk/google": "3.0.75", "@ai-sdk/openai-compatible": "2.0.47", "@ai-sdk/provider": "3.0.10", "@ai-sdk/provider-utils": "4.0.27", "google-auth-library": "^10.5.0" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-Oj1X8p0rVgvEoR5OOSxWi6XgzJ3QDlE/n30MZVtpKkCiToYYDyvlvVDGXz3IqhMyUev2JhlcuUk1brScKT01kA=="],
|
||||||
|
|
||||||
"@ai-sdk/groq": ["@ai-sdk/groq@3.0.31", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.21" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-XbbugpnFmXGu2TlXiq8KUJskP6/VVbuFcnFIGDzDIB/Chg6XHsNnqrTF80Zxkh0Pd3+NvbM+2Uqrtsndk6bDAg=="],
|
"@ai-sdk/groq": ["@ai-sdk/groq@3.0.31", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.21" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-XbbugpnFmXGu2TlXiq8KUJskP6/VVbuFcnFIGDzDIB/Chg6XHsNnqrTF80Zxkh0Pd3+NvbM+2Uqrtsndk6bDAg=="],
|
||||||
|
|
||||||
@@ -993,12 +907,8 @@
|
|||||||
|
|
||||||
"@aws-crypto/util": ["@aws-crypto/util@5.2.0", "", { "dependencies": { "@aws-sdk/types": "^3.222.0", "@smithy/util-utf8": "^2.0.0", "tslib": "^2.6.2" } }, "sha512-4RkU9EsI6ZpBve5fseQlGNUWKMa1RLPQ1dnjnQoe07ldfIzcsGb5hC5W0Dm7u423KWzawlrpbjXBrXCEv9zazQ=="],
|
"@aws-crypto/util": ["@aws-crypto/util@5.2.0", "", { "dependencies": { "@aws-sdk/types": "^3.222.0", "@smithy/util-utf8": "^2.0.0", "tslib": "^2.6.2" } }, "sha512-4RkU9EsI6ZpBve5fseQlGNUWKMa1RLPQ1dnjnQoe07ldfIzcsGb5hC5W0Dm7u423KWzawlrpbjXBrXCEv9zazQ=="],
|
||||||
|
|
||||||
"@aws-sdk/client-athena": ["@aws-sdk/client-athena@3.933.0", "", { "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", "@aws-sdk/core": "3.932.0", "@aws-sdk/credential-provider-node": "3.933.0", "@aws-sdk/middleware-host-header": "3.930.0", "@aws-sdk/middleware-logger": "3.930.0", "@aws-sdk/middleware-recursion-detection": "3.933.0", "@aws-sdk/middleware-user-agent": "3.932.0", "@aws-sdk/region-config-resolver": "3.930.0", "@aws-sdk/types": "3.930.0", "@aws-sdk/util-endpoints": "3.930.0", "@aws-sdk/util-user-agent-browser": "3.930.0", "@aws-sdk/util-user-agent-node": "3.932.0", "@smithy/config-resolver": "^4.4.3", "@smithy/core": "^3.18.2", "@smithy/fetch-http-handler": "^5.3.6", "@smithy/hash-node": "^4.2.5", "@smithy/invalid-dependency": "^4.2.5", "@smithy/middleware-content-length": "^4.2.5", "@smithy/middleware-endpoint": "^4.3.9", "@smithy/middleware-retry": "^4.4.9", "@smithy/middleware-serde": "^4.2.5", "@smithy/middleware-stack": "^4.2.5", "@smithy/node-config-provider": "^4.3.5", "@smithy/node-http-handler": "^4.4.5", "@smithy/protocol-http": "^5.3.5", "@smithy/smithy-client": "^4.9.5", "@smithy/types": "^4.9.0", "@smithy/url-parser": "^4.2.5", "@smithy/util-base64": "^4.3.0", "@smithy/util-body-length-browser": "^4.2.0", "@smithy/util-body-length-node": "^4.2.1", "@smithy/util-defaults-mode-browser": "^4.3.8", "@smithy/util-defaults-mode-node": "^4.2.11", "@smithy/util-endpoints": "^3.2.5", "@smithy/util-middleware": "^4.2.5", "@smithy/util-retry": "^4.2.5", "@smithy/util-utf8": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-9eMUCu1Ay3C9ojo+dJcynSdpbxuwDVtZUt/Xhce+c2+mgDsmvRzjww+wfLpZwRNWxBWmeauQQAZk52tCwQgXsQ=="],
|
|
||||||
|
|
||||||
"@aws-sdk/client-cognito-identity": ["@aws-sdk/client-cognito-identity@3.993.0", "", { "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", "@aws-sdk/core": "^3.973.11", "@aws-sdk/credential-provider-node": "^3.972.10", "@aws-sdk/middleware-host-header": "^3.972.3", "@aws-sdk/middleware-logger": "^3.972.3", "@aws-sdk/middleware-recursion-detection": "^3.972.3", "@aws-sdk/middleware-user-agent": "^3.972.11", "@aws-sdk/region-config-resolver": "^3.972.3", "@aws-sdk/types": "^3.973.1", "@aws-sdk/util-endpoints": "3.993.0", "@aws-sdk/util-user-agent-browser": "^3.972.3", "@aws-sdk/util-user-agent-node": "^3.972.9", "@smithy/config-resolver": "^4.4.6", "@smithy/core": "^3.23.2", "@smithy/fetch-http-handler": "^5.3.9", "@smithy/hash-node": "^4.2.8", "@smithy/invalid-dependency": "^4.2.8", "@smithy/middleware-content-length": "^4.2.8", "@smithy/middleware-endpoint": "^4.4.16", "@smithy/middleware-retry": "^4.4.33", "@smithy/middleware-serde": "^4.2.9", "@smithy/middleware-stack": "^4.2.8", "@smithy/node-config-provider": "^4.3.8", "@smithy/node-http-handler": "^4.4.10", "@smithy/protocol-http": "^5.3.8", "@smithy/smithy-client": "^4.11.5", "@smithy/types": "^4.12.0", "@smithy/url-parser": "^4.2.8", "@smithy/util-base64": "^4.3.0", "@smithy/util-body-length-browser": "^4.2.0", "@smithy/util-body-length-node": "^4.2.1", "@smithy/util-defaults-mode-browser": "^4.3.32", "@smithy/util-defaults-mode-node": "^4.2.35", "@smithy/util-endpoints": "^3.2.8", "@smithy/util-middleware": "^4.2.8", "@smithy/util-retry": "^4.2.8", "@smithy/util-utf8": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-7Ne3Yk/bgQPVebAkv7W+RfhiwTRSbfER9BtbhOa2w/+dIr902LrJf6vrZlxiqaJbGj2ALx8M+ZK1YIHVxSwu9A=="],
|
"@aws-sdk/client-cognito-identity": ["@aws-sdk/client-cognito-identity@3.993.0", "", { "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", "@aws-sdk/core": "^3.973.11", "@aws-sdk/credential-provider-node": "^3.972.10", "@aws-sdk/middleware-host-header": "^3.972.3", "@aws-sdk/middleware-logger": "^3.972.3", "@aws-sdk/middleware-recursion-detection": "^3.972.3", "@aws-sdk/middleware-user-agent": "^3.972.11", "@aws-sdk/region-config-resolver": "^3.972.3", "@aws-sdk/types": "^3.973.1", "@aws-sdk/util-endpoints": "3.993.0", "@aws-sdk/util-user-agent-browser": "^3.972.3", "@aws-sdk/util-user-agent-node": "^3.972.9", "@smithy/config-resolver": "^4.4.6", "@smithy/core": "^3.23.2", "@smithy/fetch-http-handler": "^5.3.9", "@smithy/hash-node": "^4.2.8", "@smithy/invalid-dependency": "^4.2.8", "@smithy/middleware-content-length": "^4.2.8", "@smithy/middleware-endpoint": "^4.4.16", "@smithy/middleware-retry": "^4.4.33", "@smithy/middleware-serde": "^4.2.9", "@smithy/middleware-stack": "^4.2.8", "@smithy/node-config-provider": "^4.3.8", "@smithy/node-http-handler": "^4.4.10", "@smithy/protocol-http": "^5.3.8", "@smithy/smithy-client": "^4.11.5", "@smithy/types": "^4.12.0", "@smithy/url-parser": "^4.2.8", "@smithy/util-base64": "^4.3.0", "@smithy/util-body-length-browser": "^4.2.0", "@smithy/util-body-length-node": "^4.2.1", "@smithy/util-defaults-mode-browser": "^4.3.32", "@smithy/util-defaults-mode-node": "^4.2.35", "@smithy/util-endpoints": "^3.2.8", "@smithy/util-middleware": "^4.2.8", "@smithy/util-retry": "^4.2.8", "@smithy/util-utf8": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-7Ne3Yk/bgQPVebAkv7W+RfhiwTRSbfER9BtbhOa2w/+dIr902LrJf6vrZlxiqaJbGj2ALx8M+ZK1YIHVxSwu9A=="],
|
||||||
|
|
||||||
"@aws-sdk/client-firehose": ["@aws-sdk/client-firehose@3.933.0", "", { "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", "@aws-sdk/core": "3.932.0", "@aws-sdk/credential-provider-node": "3.933.0", "@aws-sdk/middleware-host-header": "3.930.0", "@aws-sdk/middleware-logger": "3.930.0", "@aws-sdk/middleware-recursion-detection": "3.933.0", "@aws-sdk/middleware-user-agent": "3.932.0", "@aws-sdk/region-config-resolver": "3.930.0", "@aws-sdk/types": "3.930.0", "@aws-sdk/util-endpoints": "3.930.0", "@aws-sdk/util-user-agent-browser": "3.930.0", "@aws-sdk/util-user-agent-node": "3.932.0", "@smithy/config-resolver": "^4.4.3", "@smithy/core": "^3.18.2", "@smithy/fetch-http-handler": "^5.3.6", "@smithy/hash-node": "^4.2.5", "@smithy/invalid-dependency": "^4.2.5", "@smithy/middleware-content-length": "^4.2.5", "@smithy/middleware-endpoint": "^4.3.9", "@smithy/middleware-retry": "^4.4.9", "@smithy/middleware-serde": "^4.2.5", "@smithy/middleware-stack": "^4.2.5", "@smithy/node-config-provider": "^4.3.5", "@smithy/node-http-handler": "^4.4.5", "@smithy/protocol-http": "^5.3.5", "@smithy/smithy-client": "^4.9.5", "@smithy/types": "^4.9.0", "@smithy/url-parser": "^4.2.5", "@smithy/util-base64": "^4.3.0", "@smithy/util-body-length-browser": "^4.2.0", "@smithy/util-body-length-node": "^4.2.1", "@smithy/util-defaults-mode-browser": "^4.3.8", "@smithy/util-defaults-mode-node": "^4.2.11", "@smithy/util-endpoints": "^3.2.5", "@smithy/util-middleware": "^4.2.5", "@smithy/util-retry": "^4.2.5", "@smithy/util-utf8": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-tDrtgczN2lQsflLDPYu/wdOoyCZLVYtgzmWnYzSEOBWd/cp2AbuQ7D+FemSwUTzyoMTuhhIevyEJKzqsF+QYxA=="],
|
|
||||||
|
|
||||||
"@aws-sdk/client-lambda": ["@aws-sdk/client-lambda@3.1048.0", "", { "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", "@aws-sdk/core": "^3.974.11", "@aws-sdk/credential-provider-node": "^3.972.42", "@aws-sdk/types": "^3.973.8", "@smithy/core": "^3.24.2", "@smithy/fetch-http-handler": "^5.4.2", "@smithy/node-http-handler": "^4.7.2", "@smithy/types": "^4.14.1", "tslib": "^2.6.2" } }, "sha512-ryEYNVdilyWkKsOs/7Xy/l7+qjtSz4sll8NpcWD6AtONxjG/5OMaAhxxDkQb4iBoNMKnISxsARzQAp/Wa8pXIg=="],
|
"@aws-sdk/client-lambda": ["@aws-sdk/client-lambda@3.1048.0", "", { "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", "@aws-sdk/core": "^3.974.11", "@aws-sdk/credential-provider-node": "^3.972.42", "@aws-sdk/types": "^3.973.8", "@smithy/core": "^3.24.2", "@smithy/fetch-http-handler": "^5.4.2", "@smithy/node-http-handler": "^4.7.2", "@smithy/types": "^4.14.1", "tslib": "^2.6.2" } }, "sha512-ryEYNVdilyWkKsOs/7Xy/l7+qjtSz4sll8NpcWD6AtONxjG/5OMaAhxxDkQb4iBoNMKnISxsARzQAp/Wa8pXIg=="],
|
||||||
|
|
||||||
"@aws-sdk/client-s3": ["@aws-sdk/client-s3@3.933.0", "", { "dependencies": { "@aws-crypto/sha1-browser": "5.2.0", "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", "@aws-sdk/core": "3.932.0", "@aws-sdk/credential-provider-node": "3.933.0", "@aws-sdk/middleware-bucket-endpoint": "3.930.0", "@aws-sdk/middleware-expect-continue": "3.930.0", "@aws-sdk/middleware-flexible-checksums": "3.932.0", "@aws-sdk/middleware-host-header": "3.930.0", "@aws-sdk/middleware-location-constraint": "3.930.0", "@aws-sdk/middleware-logger": "3.930.0", "@aws-sdk/middleware-recursion-detection": "3.933.0", "@aws-sdk/middleware-sdk-s3": "3.932.0", "@aws-sdk/middleware-ssec": "3.930.0", "@aws-sdk/middleware-user-agent": "3.932.0", "@aws-sdk/region-config-resolver": "3.930.0", "@aws-sdk/signature-v4-multi-region": "3.932.0", "@aws-sdk/types": "3.930.0", "@aws-sdk/util-endpoints": "3.930.0", "@aws-sdk/util-user-agent-browser": "3.930.0", "@aws-sdk/util-user-agent-node": "3.932.0", "@smithy/config-resolver": "^4.4.3", "@smithy/core": "^3.18.2", "@smithy/eventstream-serde-browser": "^4.2.5", "@smithy/eventstream-serde-config-resolver": "^4.3.5", "@smithy/eventstream-serde-node": "^4.2.5", "@smithy/fetch-http-handler": "^5.3.6", "@smithy/hash-blob-browser": "^4.2.6", "@smithy/hash-node": "^4.2.5", "@smithy/hash-stream-node": "^4.2.5", "@smithy/invalid-dependency": "^4.2.5", "@smithy/md5-js": "^4.2.5", "@smithy/middleware-content-length": "^4.2.5", "@smithy/middleware-endpoint": "^4.3.9", "@smithy/middleware-retry": "^4.4.9", "@smithy/middleware-serde": "^4.2.5", "@smithy/middleware-stack": "^4.2.5", "@smithy/node-config-provider": "^4.3.5", "@smithy/node-http-handler": "^4.4.5", "@smithy/protocol-http": "^5.3.5", "@smithy/smithy-client": "^4.9.5", "@smithy/types": "^4.9.0", "@smithy/url-parser": "^4.2.5", "@smithy/util-base64": "^4.3.0", "@smithy/util-body-length-browser": "^4.2.0", "@smithy/util-body-length-node": "^4.2.1", "@smithy/util-defaults-mode-browser": "^4.3.8", "@smithy/util-defaults-mode-node": "^4.2.11", "@smithy/util-endpoints": "^3.2.5", "@smithy/util-middleware": "^4.2.5", "@smithy/util-retry": "^4.2.5", "@smithy/util-stream": "^4.5.6", "@smithy/util-utf8": "^4.2.0", "@smithy/util-waiter": "^4.2.5", "tslib": "^2.6.2" } }, "sha512-KxwZvdxdCeWK6o8mpnb+kk7Kgb8V+8AjTwSXUWH1UAD85B0tjdo1cSfE5zoR5fWGol4Ml5RLez12a6LPhsoTqA=="],
|
"@aws-sdk/client-s3": ["@aws-sdk/client-s3@3.933.0", "", { "dependencies": { "@aws-crypto/sha1-browser": "5.2.0", "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", "@aws-sdk/core": "3.932.0", "@aws-sdk/credential-provider-node": "3.933.0", "@aws-sdk/middleware-bucket-endpoint": "3.930.0", "@aws-sdk/middleware-expect-continue": "3.930.0", "@aws-sdk/middleware-flexible-checksums": "3.932.0", "@aws-sdk/middleware-host-header": "3.930.0", "@aws-sdk/middleware-location-constraint": "3.930.0", "@aws-sdk/middleware-logger": "3.930.0", "@aws-sdk/middleware-recursion-detection": "3.933.0", "@aws-sdk/middleware-sdk-s3": "3.932.0", "@aws-sdk/middleware-ssec": "3.930.0", "@aws-sdk/middleware-user-agent": "3.932.0", "@aws-sdk/region-config-resolver": "3.930.0", "@aws-sdk/signature-v4-multi-region": "3.932.0", "@aws-sdk/types": "3.930.0", "@aws-sdk/util-endpoints": "3.930.0", "@aws-sdk/util-user-agent-browser": "3.930.0", "@aws-sdk/util-user-agent-node": "3.932.0", "@smithy/config-resolver": "^4.4.3", "@smithy/core": "^3.18.2", "@smithy/eventstream-serde-browser": "^4.2.5", "@smithy/eventstream-serde-config-resolver": "^4.3.5", "@smithy/eventstream-serde-node": "^4.2.5", "@smithy/fetch-http-handler": "^5.3.6", "@smithy/hash-blob-browser": "^4.2.6", "@smithy/hash-node": "^4.2.5", "@smithy/hash-stream-node": "^4.2.5", "@smithy/invalid-dependency": "^4.2.5", "@smithy/md5-js": "^4.2.5", "@smithy/middleware-content-length": "^4.2.5", "@smithy/middleware-endpoint": "^4.3.9", "@smithy/middleware-retry": "^4.4.9", "@smithy/middleware-serde": "^4.2.5", "@smithy/middleware-stack": "^4.2.5", "@smithy/node-config-provider": "^4.3.5", "@smithy/node-http-handler": "^4.4.5", "@smithy/protocol-http": "^5.3.5", "@smithy/smithy-client": "^4.9.5", "@smithy/types": "^4.9.0", "@smithy/url-parser": "^4.2.5", "@smithy/util-base64": "^4.3.0", "@smithy/util-body-length-browser": "^4.2.0", "@smithy/util-body-length-node": "^4.2.1", "@smithy/util-defaults-mode-browser": "^4.3.8", "@smithy/util-defaults-mode-node": "^4.2.11", "@smithy/util-endpoints": "^3.2.5", "@smithy/util-middleware": "^4.2.5", "@smithy/util-retry": "^4.2.5", "@smithy/util-stream": "^4.5.6", "@smithy/util-utf8": "^4.2.0", "@smithy/util-waiter": "^4.2.5", "tslib": "^2.6.2" } }, "sha512-KxwZvdxdCeWK6o8mpnb+kk7Kgb8V+8AjTwSXUWH1UAD85B0tjdo1cSfE5zoR5fWGol4Ml5RLez12a6LPhsoTqA=="],
|
||||||
@@ -1655,8 +1565,6 @@
|
|||||||
|
|
||||||
"@opencode-ai/console-resource": ["@opencode-ai/console-resource@workspace:packages/console/resource"],
|
"@opencode-ai/console-resource": ["@opencode-ai/console-resource@workspace:packages/console/resource"],
|
||||||
|
|
||||||
"@opencode-ai/console-support": ["@opencode-ai/console-support@workspace:packages/console/support"],
|
|
||||||
|
|
||||||
"@opencode-ai/core": ["@opencode-ai/core@workspace:packages/core"],
|
"@opencode-ai/core": ["@opencode-ai/core@workspace:packages/core"],
|
||||||
|
|
||||||
"@opencode-ai/desktop": ["@opencode-ai/desktop@workspace:packages/desktop"],
|
"@opencode-ai/desktop": ["@opencode-ai/desktop@workspace:packages/desktop"],
|
||||||
@@ -1679,12 +1587,6 @@
|
|||||||
|
|
||||||
"@opencode-ai/slack": ["@opencode-ai/slack@workspace:packages/slack"],
|
"@opencode-ai/slack": ["@opencode-ai/slack@workspace:packages/slack"],
|
||||||
|
|
||||||
"@opencode-ai/stats-app": ["@opencode-ai/stats-app@workspace:packages/stats/app"],
|
|
||||||
|
|
||||||
"@opencode-ai/stats-core": ["@opencode-ai/stats-core@workspace:packages/stats/core"],
|
|
||||||
|
|
||||||
"@opencode-ai/stats-server": ["@opencode-ai/stats-server@workspace:packages/stats/server"],
|
|
||||||
|
|
||||||
"@opencode-ai/storybook": ["@opencode-ai/storybook@workspace:packages/storybook"],
|
"@opencode-ai/storybook": ["@opencode-ai/storybook@workspace:packages/storybook"],
|
||||||
|
|
||||||
"@opencode-ai/ui": ["@opencode-ai/ui@workspace:packages/ui"],
|
"@opencode-ai/ui": ["@opencode-ai/ui@workspace:packages/ui"],
|
||||||
@@ -1719,23 +1621,23 @@
|
|||||||
|
|
||||||
"@opentelemetry/semantic-conventions": ["@opentelemetry/semantic-conventions@1.40.0", "", {}, "sha512-cifvXDhcqMwwTlTK04GBNeIe7yyo28Mfby85QXFe1Yk8nmi36Ab/5UQwptOx84SsoGNRg+EVSjwzfSZMy6pmlw=="],
|
"@opentelemetry/semantic-conventions": ["@opentelemetry/semantic-conventions@1.40.0", "", {}, "sha512-cifvXDhcqMwwTlTK04GBNeIe7yyo28Mfby85QXFe1Yk8nmi36Ab/5UQwptOx84SsoGNRg+EVSjwzfSZMy6pmlw=="],
|
||||||
|
|
||||||
"@opentui/core": ["@opentui/core@0.2.16", "", { "dependencies": { "bun-ffi-structs": "0.2.2", "diff": "9.0.0", "marked": "17.0.1", "string-width": "7.2.0", "strip-ansi": "7.1.2", "yoga-layout": "3.2.1" }, "optionalDependencies": { "@opentui/core-darwin-arm64": "0.2.16", "@opentui/core-darwin-x64": "0.2.16", "@opentui/core-linux-arm64": "0.2.16", "@opentui/core-linux-x64": "0.2.16", "@opentui/core-win32-arm64": "0.2.16", "@opentui/core-win32-x64": "0.2.16" }, "peerDependencies": { "web-tree-sitter": "0.25.10" } }, "sha512-4vWN15Zc3nsXJlOiHhhpqkBXD+wrNFKxCPtiTiillZYDRre+XsZogVTOOGUDwaBIC23OSxq7imezLmmtShVBEA=="],
|
"@opentui/core": ["@opentui/core@0.2.15", "", { "dependencies": { "bun-ffi-structs": "0.2.2", "diff": "9.0.0", "marked": "17.0.1", "string-width": "7.2.0", "strip-ansi": "7.1.2", "yoga-layout": "3.2.1" }, "optionalDependencies": { "@opentui/core-darwin-arm64": "0.2.15", "@opentui/core-darwin-x64": "0.2.15", "@opentui/core-linux-arm64": "0.2.15", "@opentui/core-linux-x64": "0.2.15", "@opentui/core-win32-arm64": "0.2.15", "@opentui/core-win32-x64": "0.2.15" }, "peerDependencies": { "web-tree-sitter": "0.25.10" } }, "sha512-YGHttdZWScMcSvtYgZkLR6VhUO1OoUiQzwYjZgIusf5eCkPLD8PapH+PTMVqAiX16CHO6JxfMlkHv5qDiHAccQ=="],
|
||||||
|
|
||||||
"@opentui/core-darwin-arm64": ["@opentui/core-darwin-arm64@0.2.16", "", { "os": "darwin", "cpu": "arm64" }, "sha512-aFb2Yp+oqDu3h6VCWi7xpQ9yjpKSQcROzGGfHgqC6Nd3U+uiLfPJBkmiI87iK0opCggCFj5TkKI004050DmGjg=="],
|
"@opentui/core-darwin-arm64": ["@opentui/core-darwin-arm64@0.2.15", "", { "os": "darwin", "cpu": "arm64" }, "sha512-s25f9GmZd6wxNM5ExRmwwnLT+NLCKxnTWuO9aObOlqsXfLMGHQZrb6YwgAn/PSTua98KmH7GJCVWdPgZ/P+0RQ=="],
|
||||||
|
|
||||||
"@opentui/core-darwin-x64": ["@opentui/core-darwin-x64@0.2.16", "", { "os": "darwin", "cpu": "x64" }, "sha512-KimiHE0j7EsTB5P8doW0lr1eH5iZKLPKWQO+tmy1VcdYr/TzqhdHSvGuJXrZvfTFi9/rV57Eq0d7964Ri9O0vQ=="],
|
"@opentui/core-darwin-x64": ["@opentui/core-darwin-x64@0.2.15", "", { "os": "darwin", "cpu": "x64" }, "sha512-GyaipN+nOcEr8rcTO2mqKTGmOBk0C300I69fLtubD3BadHcMI1DVNlQrcf/J1mkQEuMYbmBTi/1hT1ybWGr2Mw=="],
|
||||||
|
|
||||||
"@opentui/core-linux-arm64": ["@opentui/core-linux-arm64@0.2.16", "", { "os": "linux", "cpu": "arm64" }, "sha512-4fCwRCfTtUgS/5QcSEkSuBjgQymSOUWXgrXG2ycrf3Swi0QhKDA/pVjwLrUJ6eF+/8mQyQSEV72T8MxMO3M2qg=="],
|
"@opentui/core-linux-arm64": ["@opentui/core-linux-arm64@0.2.15", "", { "os": "linux", "cpu": "arm64" }, "sha512-h+uyufselGT4afKMP8Lg4yUl5Kp+DJBlhu3XpWXhphE5Pnq5+f0uGBr4P+34CNcWxMsDnvagSQLFRCS4rGrOWA=="],
|
||||||
|
|
||||||
"@opentui/core-linux-x64": ["@opentui/core-linux-x64@0.2.16", "", { "os": "linux", "cpu": "x64" }, "sha512-KgQBGjiucw4e7gM+R8qOzHWBFhjCY1IfCrGjW3Wzxv2hKUlL+mPhelaeJwnEqtNxMUdVTYjlwlu3IHxslXMJWQ=="],
|
"@opentui/core-linux-x64": ["@opentui/core-linux-x64@0.2.15", "", { "os": "linux", "cpu": "x64" }, "sha512-jx+NImPq4wSp3Apfe7tlixiEJNnRyECTRJRWhGF6ZJz4PwFfgK2UHZKYR0DZHbV8nYawoDNQPJDXEWcoZShnMg=="],
|
||||||
|
|
||||||
"@opentui/core-win32-arm64": ["@opentui/core-win32-arm64@0.2.16", "", { "os": "win32", "cpu": "arm64" }, "sha512-C6WqEI3VkXatXraMgSFXZjEXq0pzURGjRpFAJZYmuVDmpqE57o7E80Np2UkdZ6m5kpJDt4mRyu3krc/P825iNQ=="],
|
"@opentui/core-win32-arm64": ["@opentui/core-win32-arm64@0.2.15", "", { "os": "win32", "cpu": "arm64" }, "sha512-2SQQLvf3sgmToxrNika9AdcccKrjPJEn5jW6sSv0oEixNBzUzW41vSZZG4LM/V3lL8eg0LoYDnRZeKLB4gwSqQ=="],
|
||||||
|
|
||||||
"@opentui/core-win32-x64": ["@opentui/core-win32-x64@0.2.16", "", { "os": "win32", "cpu": "x64" }, "sha512-kCX3CMTns6DMCFDNTDV4sjmBKyA/iEvzaVhl/jYi4JRIVT2zcy1lo+lhXT5mPgYHmJZu8Uye6j3Zi3c7Z2Me5A=="],
|
"@opentui/core-win32-x64": ["@opentui/core-win32-x64@0.2.15", "", { "os": "win32", "cpu": "x64" }, "sha512-SVMVgnC7LVEm+yVZKdmmhRBj/xAT94PanT+UCcHxaCWK+OLmv/AX+ohHq2m0odup6iXcEqj+7mAltO9fgJLFIg=="],
|
||||||
|
|
||||||
"@opentui/keymap": ["@opentui/keymap@0.2.16", "", { "dependencies": { "@opentui/core": "0.2.16" }, "peerDependencies": { "@opentui/react": "0.2.16", "@opentui/solid": "0.2.16", "react": ">=19.2.0", "solid-js": "1.9.12" }, "optionalPeers": ["@opentui/react", "@opentui/solid", "react", "solid-js"] }, "sha512-YBLQfNLbU2kx49bjEY9rrFoNlvIoi5qNJfRcOt6frvnR3C6MLl0/8hZY+vMQ2PEQWeEiNejFnl1lQw+z4Nk2FQ=="],
|
"@opentui/keymap": ["@opentui/keymap@0.2.15", "", { "dependencies": { "@opentui/core": "0.2.15" }, "peerDependencies": { "@opentui/react": "0.2.15", "@opentui/solid": "0.2.15", "react": ">=19.2.0", "solid-js": "1.9.12" }, "optionalPeers": ["@opentui/react", "@opentui/solid", "react", "solid-js"] }, "sha512-wqQp6y7P2jZZJiOMwupxjGryuSWCs+njjglwW/xny9J17gomBmUvTIcIIWNG0Jv+EGO9ScBzCScGlwBHFhHyYw=="],
|
||||||
|
|
||||||
"@opentui/solid": ["@opentui/solid@0.2.16", "", { "dependencies": { "@babel/core": "7.28.0", "@babel/preset-typescript": "7.27.1", "@opentui/core": "0.2.16", "babel-plugin-module-resolver": "5.0.2", "babel-preset-solid": "1.9.12", "entities": "7.0.1", "s-js": "^0.4.9" }, "peerDependencies": { "solid-js": "1.9.12" } }, "sha512-2Q+v1PPpXXr+sALi9Aj6I5Jvo7xDfbmstYjRLL7lW3Hghh9i7ONQKpt/gyDDRbhSsYrhxKYTNenF9OxgoXkTHg=="],
|
"@opentui/solid": ["@opentui/solid@0.2.15", "", { "dependencies": { "@babel/core": "7.28.0", "@babel/preset-typescript": "7.27.1", "@opentui/core": "0.2.15", "babel-plugin-module-resolver": "5.0.2", "babel-preset-solid": "1.9.12", "entities": "7.0.1", "s-js": "^0.4.9" }, "peerDependencies": { "solid-js": "1.9.12" } }, "sha512-CViepAjsCWXwrLndMt+qlLo7cooVX7DXwSJHNizw7mfrRJtOPzSYJZCIk1vF4IJTWffCHygoYMe3uSeKvzAcbw=="],
|
||||||
|
|
||||||
"@oslojs/asn1": ["@oslojs/asn1@1.0.0", "", { "dependencies": { "@oslojs/binary": "1.0.0" } }, "sha512-zw/wn0sj0j0QKbIXfIlnEcTviaCzYOY3V5rAyjR6YtOByFtJiT574+8p9Wlach0lZH9fddD4yb9laEAIl4vXQA=="],
|
"@oslojs/asn1": ["@oslojs/asn1@1.0.0", "", { "dependencies": { "@oslojs/binary": "1.0.0" } }, "sha512-zw/wn0sj0j0QKbIXfIlnEcTviaCzYOY3V5rAyjR6YtOByFtJiT574+8p9Wlach0lZH9fddD4yb9laEAIl4vXQA=="],
|
||||||
|
|
||||||
@@ -2423,10 +2325,6 @@
|
|||||||
|
|
||||||
"@types/cross-spawn": ["@types/cross-spawn@6.0.6", "", { "dependencies": { "@types/node": "*" } }, "sha512-fXRhhUkG4H3TQk5dBhQ7m/JDdSNHKwR2BBia62lhwEIq9xGiQKLxd6LymNhn47SjXhsUEPmxi+PKw2OkW4LLjA=="],
|
"@types/cross-spawn": ["@types/cross-spawn@6.0.6", "", { "dependencies": { "@types/node": "*" } }, "sha512-fXRhhUkG4H3TQk5dBhQ7m/JDdSNHKwR2BBia62lhwEIq9xGiQKLxd6LymNhn47SjXhsUEPmxi+PKw2OkW4LLjA=="],
|
||||||
|
|
||||||
"@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=="],
|
|
||||||
|
|
||||||
"@types/debug": ["@types/debug@4.1.13", "", { "dependencies": { "@types/ms": "*" } }, "sha512-KSVgmQmzMwPlmtljOomayoR89W4FynCAi3E8PPs7vmDVPe84hT+vGPKkJfThkmXs0x0jAaa9U8uW8bbfyS2fWw=="],
|
"@types/debug": ["@types/debug@4.1.13", "", { "dependencies": { "@types/ms": "*" } }, "sha512-KSVgmQmzMwPlmtljOomayoR89W4FynCAi3E8PPs7vmDVPe84hT+vGPKkJfThkmXs0x0jAaa9U8uW8bbfyS2fWw=="],
|
||||||
|
|
||||||
"@types/deep-eql": ["@types/deep-eql@4.0.2", "", {}, "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw=="],
|
"@types/deep-eql": ["@types/deep-eql@4.0.2", "", {}, "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw=="],
|
||||||
@@ -2571,8 +2469,6 @@
|
|||||||
|
|
||||||
"@ungap/structured-clone": ["@ungap/structured-clone@1.3.0", "", {}, "sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g=="],
|
"@ungap/structured-clone": ["@ungap/structured-clone@1.3.0", "", {}, "sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g=="],
|
||||||
|
|
||||||
"@upstash/redis": ["@upstash/redis@1.38.0", "", { "dependencies": { "uncrypto": "^0.1.3" } }, "sha512-wu+dZBptlLy0+MCUEoHmzrY/TnmgDey3+c7EbIGwrLqAvkP8yi5MWZHYGIFtAygmL4Bkz2TdFu+eU0vFPncIcg=="],
|
|
||||||
|
|
||||||
"@valibot/to-json-schema": ["@valibot/to-json-schema@1.6.0", "", { "peerDependencies": { "valibot": "^1.3.0" } }, "sha512-d6rYyK5KVa2XdqamWgZ4/Nr+cXhxjy7lmpe6Iajw15J/jmU+gyxl2IEd1Otg1d7Rl3gOQL5reulnSypzBtYy1A=="],
|
"@valibot/to-json-schema": ["@valibot/to-json-schema@1.6.0", "", { "peerDependencies": { "valibot": "^1.3.0" } }, "sha512-d6rYyK5KVa2XdqamWgZ4/Nr+cXhxjy7lmpe6Iajw15J/jmU+gyxl2IEd1Otg1d7Rl3gOQL5reulnSypzBtYy1A=="],
|
||||||
|
|
||||||
"@vercel/oidc": ["@vercel/oidc@3.2.0", "", {}, "sha512-UycprH3T6n3jH0k44NHMa7pnFHGu/N05MjojYr+Mc6I7obkoLIJujSWwin1pCvdy/eOxrI/l3uDLQsmcrOb4ug=="],
|
"@vercel/oidc": ["@vercel/oidc@3.2.0", "", {}, "sha512-UycprH3T6n3jH0k44NHMa7pnFHGu/N05MjojYr+Mc6I7obkoLIJujSWwin1pCvdy/eOxrI/l3uDLQsmcrOb4ug=="],
|
||||||
@@ -2999,20 +2895,6 @@
|
|||||||
|
|
||||||
"csstype": ["csstype@3.2.3", "", {}, "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ=="],
|
"csstype": ["csstype@3.2.3", "", {}, "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ=="],
|
||||||
|
|
||||||
"d3-array": ["d3-array@3.2.4", "", { "dependencies": { "internmap": "1 - 2" } }, "sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg=="],
|
|
||||||
|
|
||||||
"d3-color": ["d3-color@3.1.0", "", {}, "sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA=="],
|
|
||||||
|
|
||||||
"d3-format": ["d3-format@3.1.2", "", {}, "sha512-AJDdYOdnyRDV5b6ArilzCPPwc1ejkHcoyFarqlPqT7zRYjhavcT3uSrqcMvsgh2CgoPbK3RCwyHaVyxYcP2Arg=="],
|
|
||||||
|
|
||||||
"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=="],
|
|
||||||
|
|
||||||
"d3-time": ["d3-time@3.1.0", "", { "dependencies": { "d3-array": "2 - 3" } }, "sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q=="],
|
|
||||||
|
|
||||||
"d3-time-format": ["d3-time-format@4.1.0", "", { "dependencies": { "d3-time": "1 - 3" } }, "sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg=="],
|
|
||||||
|
|
||||||
"data-uri-to-buffer": ["data-uri-to-buffer@4.0.1", "", {}, "sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A=="],
|
"data-uri-to-buffer": ["data-uri-to-buffer@4.0.1", "", {}, "sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A=="],
|
||||||
|
|
||||||
"data-view-buffer": ["data-view-buffer@1.0.2", "", { "dependencies": { "call-bound": "^1.0.3", "es-errors": "^1.3.0", "is-data-view": "^1.0.2" } }, "sha512-EmKO5V3OLXh1rtK2wgXRansaK1/mtVdTUEiEI0W8RkvgT05kfxaH29PliLnpLP73yYO6142Q72QNa8Wx/A5CqQ=="],
|
"data-view-buffer": ["data-view-buffer@1.0.2", "", { "dependencies": { "call-bound": "^1.0.3", "es-errors": "^1.3.0", "is-data-view": "^1.0.2" } }, "sha512-EmKO5V3OLXh1rtK2wgXRansaK1/mtVdTUEiEI0W8RkvgT05kfxaH29PliLnpLP73yYO6142Q72QNa8Wx/A5CqQ=="],
|
||||||
@@ -3587,8 +3469,6 @@
|
|||||||
|
|
||||||
"internal-slot": ["internal-slot@1.1.0", "", { "dependencies": { "es-errors": "^1.3.0", "hasown": "^2.0.2", "side-channel": "^1.1.0" } }, "sha512-4gd7VpWNQNB4UKKCFFVcp1AVv+FMOgs9NKzjHKusc8jTMhd5eL1NqQqOpE0KzMds804/yHlglp3uxgluOqAPLw=="],
|
"internal-slot": ["internal-slot@1.1.0", "", { "dependencies": { "es-errors": "^1.3.0", "hasown": "^2.0.2", "side-channel": "^1.1.0" } }, "sha512-4gd7VpWNQNB4UKKCFFVcp1AVv+FMOgs9NKzjHKusc8jTMhd5eL1NqQqOpE0KzMds804/yHlglp3uxgluOqAPLw=="],
|
||||||
|
|
||||||
"internmap": ["internmap@2.0.3", "", {}, "sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg=="],
|
|
||||||
|
|
||||||
"ioredis": ["ioredis@5.10.1", "", { "dependencies": { "@ioredis/commands": "1.5.1", "cluster-key-slot": "^1.1.0", "debug": "^4.3.4", "denque": "^2.1.0", "lodash.defaults": "^4.2.0", "lodash.isarguments": "^3.1.0", "redis-errors": "^1.2.0", "redis-parser": "^3.0.0", "standard-as-callback": "^2.1.0" } }, "sha512-HuEDBTI70aYdx1v6U97SbNx9F1+svQKBDo30o0b9fw055LMepzpOOd0Ccg9Q6tbqmBSJaMuY0fB7yw9/vjBYCA=="],
|
"ioredis": ["ioredis@5.10.1", "", { "dependencies": { "@ioredis/commands": "1.5.1", "cluster-key-slot": "^1.1.0", "debug": "^4.3.4", "denque": "^2.1.0", "lodash.defaults": "^4.2.0", "lodash.isarguments": "^3.1.0", "redis-errors": "^1.2.0", "redis-parser": "^3.0.0", "standard-as-callback": "^2.1.0" } }, "sha512-HuEDBTI70aYdx1v6U97SbNx9F1+svQKBDo30o0b9fw055LMepzpOOd0Ccg9Q6tbqmBSJaMuY0fB7yw9/vjBYCA=="],
|
||||||
|
|
||||||
"ip-address": ["ip-address@10.1.0", "", {}, "sha512-XXADHxXmvT9+CRxhXg56LJovE+bmWnEWB78LB83VZTprKTmaC5QfruXocxzTZ2Kl0DNwKuBdlIhjL8LeY8Sf8Q=="],
|
"ip-address": ["ip-address@10.1.0", "", {}, "sha512-XXADHxXmvT9+CRxhXg56LJovE+bmWnEWB78LB83VZTprKTmaC5QfruXocxzTZ2Kl0DNwKuBdlIhjL8LeY8Sf8Q=="],
|
||||||
@@ -5141,7 +5021,7 @@
|
|||||||
|
|
||||||
"write-file-atomic": ["write-file-atomic@7.0.1", "", { "dependencies": { "signal-exit": "^4.0.1" } }, "sha512-OTIk8iR8/aCRWBqvxrzxR0hgxWpnYBblY1S5hDWBQfk/VFmJwzmJgQFN3WsoUKHISv2eAwe+PpbUzyL1CKTLXg=="],
|
"write-file-atomic": ["write-file-atomic@7.0.1", "", { "dependencies": { "signal-exit": "^4.0.1" } }, "sha512-OTIk8iR8/aCRWBqvxrzxR0hgxWpnYBblY1S5hDWBQfk/VFmJwzmJgQFN3WsoUKHISv2eAwe+PpbUzyL1CKTLXg=="],
|
||||||
|
|
||||||
"ws": ["ws@8.21.0", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-Vsp28b7DRcimFQvrqu2Wek3z1iYxDCWqHYB8Qsnk/S4RfaCQzPGPyBNuVjJV3cd6UiKtUtp6sNM77gWvzcCH+g=="],
|
"ws": ["ws@8.18.0", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw=="],
|
||||||
|
|
||||||
"wsl-utils": ["wsl-utils@0.3.1", "", { "dependencies": { "is-wsl": "^3.1.0", "powershell-utils": "^0.1.0" } }, "sha512-g/eziiSUNBSsdDJtCLB8bdYEUMj4jR7AGeUo96p/3dTafgjHhpF4RiCFPiRILwjQoDXx5MqkBr4fwWtR3Ky4Wg=="],
|
"wsl-utils": ["wsl-utils@0.3.1", "", { "dependencies": { "is-wsl": "^3.1.0", "powershell-utils": "^0.1.0" } }, "sha512-g/eziiSUNBSsdDJtCLB8bdYEUMj4jR7AGeUo96p/3dTafgjHhpF4RiCFPiRILwjQoDXx5MqkBr4fwWtR3Ky4Wg=="],
|
||||||
|
|
||||||
@@ -5237,7 +5117,7 @@
|
|||||||
|
|
||||||
"@ai-sdk/google/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@4.0.27", "", { "dependencies": { "@ai-sdk/provider": "3.0.10", "@standard-schema/spec": "^1.1.0", "eventsource-parser": "^3.0.8" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-ubkAJ+xODouwtmN1tYlvTPphH1hPOBfZaEQe8U7skGvFAnIRs9PPpsq57bC2+Ky/MB4yzhd6YOsxTAx9sGpazw=="],
|
"@ai-sdk/google/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@4.0.27", "", { "dependencies": { "@ai-sdk/provider": "3.0.10", "@standard-schema/spec": "^1.1.0", "eventsource-parser": "^3.0.8" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-ubkAJ+xODouwtmN1tYlvTPphH1hPOBfZaEQe8U7skGvFAnIRs9PPpsq57bC2+Ky/MB4yzhd6YOsxTAx9sGpazw=="],
|
||||||
|
|
||||||
"@ai-sdk/google-vertex/@ai-sdk/anthropic": ["@ai-sdk/anthropic@3.0.77", "", { "dependencies": { "@ai-sdk/provider": "3.0.10", "@ai-sdk/provider-utils": "4.0.27" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-ML8C2M1YvPA1ulEx4TiyF0k1xvC2ikEiPBIC1PPQ0a5xELUGrO2lAaEzsTEoJ+eCeDd8PSBuFJjs+r+9yIwQXA=="],
|
"@ai-sdk/google-vertex/@ai-sdk/anthropic": ["@ai-sdk/anthropic@3.0.78", "", { "dependencies": { "@ai-sdk/provider": "3.0.10", "@ai-sdk/provider-utils": "4.0.27" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-0OY12G20cUt6iU6htpEA1491Oz++NVxZxlmWGX4B7rSbeZ5pnDmOu6YtW9BKzdZlNx5Gn23i6WMxyZFoMKNcgA=="],
|
||||||
|
|
||||||
"@ai-sdk/google-vertex/@ai-sdk/openai-compatible": ["@ai-sdk/openai-compatible@2.0.47", "", { "dependencies": { "@ai-sdk/provider": "3.0.10", "@ai-sdk/provider-utils": "4.0.27" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-Enm5UlL0zUCrW3792opk5h7hRWxZOZzDe6eQYVFqX9LUOGGCe1h8MZWAGim765nwzgnjlpeYOsuzZmLtRsTPlg=="],
|
"@ai-sdk/google-vertex/@ai-sdk/openai-compatible": ["@ai-sdk/openai-compatible@2.0.47", "", { "dependencies": { "@ai-sdk/provider": "3.0.10", "@ai-sdk/provider-utils": "4.0.27" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-Enm5UlL0zUCrW3792opk5h7hRWxZOZzDe6eQYVFqX9LUOGGCe1h8MZWAGim765nwzgnjlpeYOsuzZmLtRsTPlg=="],
|
||||||
|
|
||||||
@@ -5297,16 +5177,6 @@
|
|||||||
|
|
||||||
"@aws-crypto/util/@smithy/util-utf8": ["@smithy/util-utf8@2.3.0", "", { "dependencies": { "@smithy/util-buffer-from": "^2.2.0", "tslib": "^2.6.2" } }, "sha512-R8Rdn8Hy72KKcebgLiv8jQcQkXoLMOGGv5uI1/k0l+snqkOzQ1R0ChUBCxWMlBsFMekWjq0wRudIweFs7sKT5A=="],
|
"@aws-crypto/util/@smithy/util-utf8": ["@smithy/util-utf8@2.3.0", "", { "dependencies": { "@smithy/util-buffer-from": "^2.2.0", "tslib": "^2.6.2" } }, "sha512-R8Rdn8Hy72KKcebgLiv8jQcQkXoLMOGGv5uI1/k0l+snqkOzQ1R0ChUBCxWMlBsFMekWjq0wRudIweFs7sKT5A=="],
|
||||||
|
|
||||||
"@aws-sdk/client-athena/@smithy/core": ["@smithy/core@3.24.3", "", { "dependencies": { "@aws-crypto/crc32": "5.2.0", "@smithy/types": "^4.14.2", "tslib": "^2.6.2" } }, "sha512-Ep/7tPamGY8mgESE3LyLKtxJyy6U52WWAqr/3wial47Sj4u3PiIF73AOGI27UyLy9duTkhZbgzodOfLV4TduZg=="],
|
|
||||||
|
|
||||||
"@aws-sdk/client-athena/@smithy/fetch-http-handler": ["@smithy/fetch-http-handler@5.4.3", "", { "dependencies": { "@smithy/core": "^3.24.3", "@smithy/types": "^4.14.2", "tslib": "^2.6.2" } }, "sha512-F+DRf8IJazRJgYog2A/yJK7eYVc0rqTlRzO+5ZxjJd4WkZoKz0IJRncf7G6t1pdVT3kryJcwuTFhN1c5m6N47A=="],
|
|
||||||
|
|
||||||
"@aws-sdk/client-athena/@smithy/node-http-handler": ["@smithy/node-http-handler@4.7.3", "", { "dependencies": { "@smithy/core": "^3.24.3", "@smithy/types": "^4.14.2", "tslib": "^2.6.2" } }, "sha512-/jPhevcTFPMVl6KNjbaI47iOg1zxC7IsnX4PQDGVZKMFceOXtB8IEYaB7a9VvkP/3oC60WzTeKocvSI7vLT0vA=="],
|
|
||||||
|
|
||||||
"@aws-sdk/client-athena/@smithy/types": ["@smithy/types@4.14.2", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-P+otAxbV4CqBybp7EkcJCrig63yE2E7PuNVOmilVMRcx/O+QDzGULTrKsq4DV13gSfak9ObPrWaHl/9bL5YcWw=="],
|
|
||||||
|
|
||||||
"@aws-sdk/client-athena/@smithy/util-utf8": ["@smithy/util-utf8@4.2.2", "", { "dependencies": { "@smithy/util-buffer-from": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-75MeYpjdWRe8M5E3AW0O4Cx3UadweS+cwdXjwYGBW5h/gxxnbeZ877sLPX/ZJA9GVTlL/qG0dXP29JWFCD1Ayw=="],
|
|
||||||
|
|
||||||
"@aws-sdk/client-cognito-identity/@aws-sdk/core": ["@aws-sdk/core@3.973.27", "", { "dependencies": { "@aws-sdk/types": "^3.973.7", "@aws-sdk/xml-builder": "^3.972.17", "@smithy/core": "^3.23.14", "@smithy/node-config-provider": "^4.3.13", "@smithy/property-provider": "^4.2.13", "@smithy/protocol-http": "^5.3.13", "@smithy/signature-v4": "^5.3.13", "@smithy/smithy-client": "^4.12.9", "@smithy/types": "^4.14.0", "@smithy/util-base64": "^4.3.2", "@smithy/util-middleware": "^4.2.13", "@smithy/util-utf8": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-CUZ5m8hwMCH6OYI4Li/WgMfIEx10Q2PLI9Y3XOUTPGZJ53aZ0007jCv+X/ywsaERyKPdw5MRZWk877roQksQ4A=="],
|
"@aws-sdk/client-cognito-identity/@aws-sdk/core": ["@aws-sdk/core@3.973.27", "", { "dependencies": { "@aws-sdk/types": "^3.973.7", "@aws-sdk/xml-builder": "^3.972.17", "@smithy/core": "^3.23.14", "@smithy/node-config-provider": "^4.3.13", "@smithy/property-provider": "^4.2.13", "@smithy/protocol-http": "^5.3.13", "@smithy/signature-v4": "^5.3.13", "@smithy/smithy-client": "^4.12.9", "@smithy/types": "^4.14.0", "@smithy/util-base64": "^4.3.2", "@smithy/util-middleware": "^4.2.13", "@smithy/util-utf8": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-CUZ5m8hwMCH6OYI4Li/WgMfIEx10Q2PLI9Y3XOUTPGZJ53aZ0007jCv+X/ywsaERyKPdw5MRZWk877roQksQ4A=="],
|
||||||
|
|
||||||
"@aws-sdk/client-cognito-identity/@aws-sdk/credential-provider-node": ["@aws-sdk/credential-provider-node@3.972.30", "", { "dependencies": { "@aws-sdk/credential-provider-env": "^3.972.25", "@aws-sdk/credential-provider-http": "^3.972.27", "@aws-sdk/credential-provider-ini": "^3.972.29", "@aws-sdk/credential-provider-process": "^3.972.25", "@aws-sdk/credential-provider-sso": "^3.972.29", "@aws-sdk/credential-provider-web-identity": "^3.972.29", "@aws-sdk/types": "^3.973.7", "@smithy/credential-provider-imds": "^4.2.13", "@smithy/property-provider": "^4.2.13", "@smithy/shared-ini-file-loader": "^4.4.8", "@smithy/types": "^4.14.0", "tslib": "^2.6.2" } }, "sha512-FMnAnWxc8PG+ZrZ2OBKzY4luCUJhe9CG0B9YwYr4pzrYGLXBS2rl+UoUvjGbAwiptxRL6hyA3lFn03Bv1TLqTw=="],
|
"@aws-sdk/client-cognito-identity/@aws-sdk/credential-provider-node": ["@aws-sdk/credential-provider-node@3.972.30", "", { "dependencies": { "@aws-sdk/credential-provider-env": "^3.972.25", "@aws-sdk/credential-provider-http": "^3.972.27", "@aws-sdk/credential-provider-ini": "^3.972.29", "@aws-sdk/credential-provider-process": "^3.972.25", "@aws-sdk/credential-provider-sso": "^3.972.29", "@aws-sdk/credential-provider-web-identity": "^3.972.29", "@aws-sdk/types": "^3.973.7", "@smithy/credential-provider-imds": "^4.2.13", "@smithy/property-provider": "^4.2.13", "@smithy/shared-ini-file-loader": "^4.4.8", "@smithy/types": "^4.14.0", "tslib": "^2.6.2" } }, "sha512-FMnAnWxc8PG+ZrZ2OBKzY4luCUJhe9CG0B9YwYr4pzrYGLXBS2rl+UoUvjGbAwiptxRL6hyA3lFn03Bv1TLqTw=="],
|
||||||
@@ -5331,16 +5201,6 @@
|
|||||||
|
|
||||||
"@aws-sdk/client-cognito-identity/@smithy/util-utf8": ["@smithy/util-utf8@4.2.2", "", { "dependencies": { "@smithy/util-buffer-from": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-75MeYpjdWRe8M5E3AW0O4Cx3UadweS+cwdXjwYGBW5h/gxxnbeZ877sLPX/ZJA9GVTlL/qG0dXP29JWFCD1Ayw=="],
|
"@aws-sdk/client-cognito-identity/@smithy/util-utf8": ["@smithy/util-utf8@4.2.2", "", { "dependencies": { "@smithy/util-buffer-from": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-75MeYpjdWRe8M5E3AW0O4Cx3UadweS+cwdXjwYGBW5h/gxxnbeZ877sLPX/ZJA9GVTlL/qG0dXP29JWFCD1Ayw=="],
|
||||||
|
|
||||||
"@aws-sdk/client-firehose/@smithy/core": ["@smithy/core@3.24.3", "", { "dependencies": { "@aws-crypto/crc32": "5.2.0", "@smithy/types": "^4.14.2", "tslib": "^2.6.2" } }, "sha512-Ep/7tPamGY8mgESE3LyLKtxJyy6U52WWAqr/3wial47Sj4u3PiIF73AOGI27UyLy9duTkhZbgzodOfLV4TduZg=="],
|
|
||||||
|
|
||||||
"@aws-sdk/client-firehose/@smithy/fetch-http-handler": ["@smithy/fetch-http-handler@5.4.3", "", { "dependencies": { "@smithy/core": "^3.24.3", "@smithy/types": "^4.14.2", "tslib": "^2.6.2" } }, "sha512-F+DRf8IJazRJgYog2A/yJK7eYVc0rqTlRzO+5ZxjJd4WkZoKz0IJRncf7G6t1pdVT3kryJcwuTFhN1c5m6N47A=="],
|
|
||||||
|
|
||||||
"@aws-sdk/client-firehose/@smithy/node-http-handler": ["@smithy/node-http-handler@4.7.3", "", { "dependencies": { "@smithy/core": "^3.24.3", "@smithy/types": "^4.14.2", "tslib": "^2.6.2" } }, "sha512-/jPhevcTFPMVl6KNjbaI47iOg1zxC7IsnX4PQDGVZKMFceOXtB8IEYaB7a9VvkP/3oC60WzTeKocvSI7vLT0vA=="],
|
|
||||||
|
|
||||||
"@aws-sdk/client-firehose/@smithy/types": ["@smithy/types@4.14.2", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-P+otAxbV4CqBybp7EkcJCrig63yE2E7PuNVOmilVMRcx/O+QDzGULTrKsq4DV13gSfak9ObPrWaHl/9bL5YcWw=="],
|
|
||||||
|
|
||||||
"@aws-sdk/client-firehose/@smithy/util-utf8": ["@smithy/util-utf8@4.2.2", "", { "dependencies": { "@smithy/util-buffer-from": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-75MeYpjdWRe8M5E3AW0O4Cx3UadweS+cwdXjwYGBW5h/gxxnbeZ877sLPX/ZJA9GVTlL/qG0dXP29JWFCD1Ayw=="],
|
|
||||||
|
|
||||||
"@aws-sdk/client-lambda/@aws-sdk/core": ["@aws-sdk/core@3.974.11", "", { "dependencies": { "@aws-sdk/types": "^3.973.8", "@aws-sdk/xml-builder": "^3.972.24", "@aws/lambda-invoke-store": "^0.2.2", "@smithy/core": "^3.24.2", "@smithy/signature-v4": "^5.4.2", "@smithy/types": "^4.14.1", "bowser": "^2.11.0", "tslib": "^2.6.2" } }, "sha512-QpnINq5FZH6EOaDEkmHdT7eUunbvD27pDNQypaWjFyYz7Zl1q3UCMQErBZxpmfGfI7MvI2TlK8KTkgNpv8b1ug=="],
|
"@aws-sdk/client-lambda/@aws-sdk/core": ["@aws-sdk/core@3.974.11", "", { "dependencies": { "@aws-sdk/types": "^3.973.8", "@aws-sdk/xml-builder": "^3.972.24", "@aws/lambda-invoke-store": "^0.2.2", "@smithy/core": "^3.24.2", "@smithy/signature-v4": "^5.4.2", "@smithy/types": "^4.14.1", "bowser": "^2.11.0", "tslib": "^2.6.2" } }, "sha512-QpnINq5FZH6EOaDEkmHdT7eUunbvD27pDNQypaWjFyYz7Zl1q3UCMQErBZxpmfGfI7MvI2TlK8KTkgNpv8b1ug=="],
|
||||||
|
|
||||||
"@aws-sdk/client-lambda/@aws-sdk/credential-provider-node": ["@aws-sdk/credential-provider-node@3.972.42", "", { "dependencies": { "@aws-sdk/credential-provider-env": "^3.972.37", "@aws-sdk/credential-provider-http": "^3.972.39", "@aws-sdk/credential-provider-ini": "^3.972.41", "@aws-sdk/credential-provider-process": "^3.972.37", "@aws-sdk/credential-provider-sso": "^3.972.41", "@aws-sdk/credential-provider-web-identity": "^3.972.41", "@aws-sdk/types": "^3.973.8", "@smithy/core": "^3.24.2", "@smithy/credential-provider-imds": "^4.3.2", "@smithy/types": "^4.14.1", "tslib": "^2.6.2" } }, "sha512-D4oon2zbqqsWOJUM99Gm3/ZyJ0IJvTXVN3PyloGb3kQEyI36fjCZheZj422lAgTWWd6TSHgiImLt3RIaLdv3dQ=="],
|
"@aws-sdk/client-lambda/@aws-sdk/credential-provider-node": ["@aws-sdk/credential-provider-node@3.972.42", "", { "dependencies": { "@aws-sdk/credential-provider-env": "^3.972.37", "@aws-sdk/credential-provider-http": "^3.972.39", "@aws-sdk/credential-provider-ini": "^3.972.41", "@aws-sdk/credential-provider-process": "^3.972.37", "@aws-sdk/credential-provider-sso": "^3.972.41", "@aws-sdk/credential-provider-web-identity": "^3.972.41", "@aws-sdk/types": "^3.973.8", "@smithy/core": "^3.24.2", "@smithy/credential-provider-imds": "^4.3.2", "@smithy/types": "^4.14.1", "tslib": "^2.6.2" } }, "sha512-D4oon2zbqqsWOJUM99Gm3/ZyJ0IJvTXVN3PyloGb3kQEyI36fjCZheZj422lAgTWWd6TSHgiImLt3RIaLdv3dQ=="],
|
||||||
@@ -5499,8 +5359,6 @@
|
|||||||
|
|
||||||
"@cloudflare/kv-asset-handler/mime": ["mime@3.0.0", "", { "bin": { "mime": "cli.js" } }, "sha512-jSCU7/VB1loIWBZe14aEYHU/+1UMEHoaO7qxCOVJOw9GgH72VAWppxNcjU+x9a2k3GSIBXNKxXQFqRvvZ7vr3A=="],
|
"@cloudflare/kv-asset-handler/mime": ["mime@3.0.0", "", { "bin": { "mime": "cli.js" } }, "sha512-jSCU7/VB1loIWBZe14aEYHU/+1UMEHoaO7qxCOVJOw9GgH72VAWppxNcjU+x9a2k3GSIBXNKxXQFqRvvZ7vr3A=="],
|
||||||
|
|
||||||
"@cloudflare/vite-plugin/ws": ["ws@8.18.0", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw=="],
|
|
||||||
|
|
||||||
"@cspotcode/source-map-support/@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.9", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.0.3", "@jridgewell/sourcemap-codec": "^1.4.10" } }, "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ=="],
|
"@cspotcode/source-map-support/@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.9", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.0.3", "@jridgewell/sourcemap-codec": "^1.4.10" } }, "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ=="],
|
||||||
|
|
||||||
"@develar/schema-utils/ajv": ["ajv@6.14.0", "", { "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", "json-schema-traverse": "^0.4.1", "uri-js": "^4.2.2" } }, "sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw=="],
|
"@develar/schema-utils/ajv": ["ajv@6.14.0", "", { "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", "json-schema-traverse": "^0.4.1", "uri-js": "^4.2.2" } }, "sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw=="],
|
||||||
@@ -5963,8 +5821,6 @@
|
|||||||
|
|
||||||
"miniflare/undici": ["undici@7.14.0", "", {}, "sha512-Vqs8HTzjpQXZeXdpsfChQTlafcMQaaIwnGwLam1wudSSjlJeQ3bw1j+TLPePgrCnCpUXx7Ba5Pdpf5OBih62NQ=="],
|
"miniflare/undici": ["undici@7.14.0", "", {}, "sha512-Vqs8HTzjpQXZeXdpsfChQTlafcMQaaIwnGwLam1wudSSjlJeQ3bw1j+TLPePgrCnCpUXx7Ba5Pdpf5OBih62NQ=="],
|
||||||
|
|
||||||
"miniflare/ws": ["ws@8.18.0", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw=="],
|
|
||||||
|
|
||||||
"miniflare/zod": ["zod@3.22.3", "", {}, "sha512-EjIevzuJRiRPbVH4mGc8nApb/lVLKVpmUhAaR5R5doKGfAnGJ6Gr3CViAVjP+4FWSxCsybeWQdcgCtbX+7oZug=="],
|
"miniflare/zod": ["zod@3.22.3", "", {}, "sha512-EjIevzuJRiRPbVH4mGc8nApb/lVLKVpmUhAaR5R5doKGfAnGJ6Gr3CViAVjP+4FWSxCsybeWQdcgCtbX+7oZug=="],
|
||||||
|
|
||||||
"minipass-flush/minipass": ["minipass@3.3.6", "", { "dependencies": { "yallist": "^4.0.0" } }, "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw=="],
|
"minipass-flush/minipass": ["minipass@3.3.6", "", { "dependencies": { "yallist": "^4.0.0" } }, "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw=="],
|
||||||
@@ -6081,8 +5937,6 @@
|
|||||||
|
|
||||||
"storybook/open": ["open@10.2.0", "", { "dependencies": { "default-browser": "^5.2.1", "define-lazy-prop": "^3.0.0", "is-inside-container": "^1.0.0", "wsl-utils": "^0.1.0" } }, "sha512-YgBpdJHPyQ2UE5x+hlSXcnejzAvD0b22U2OuAP+8OnlJT+PjWPxtgmGqKKc+RgTM63U9gN0YzrYc71R2WT/hTA=="],
|
"storybook/open": ["open@10.2.0", "", { "dependencies": { "default-browser": "^5.2.1", "define-lazy-prop": "^3.0.0", "is-inside-container": "^1.0.0", "wsl-utils": "^0.1.0" } }, "sha512-YgBpdJHPyQ2UE5x+hlSXcnejzAvD0b22U2OuAP+8OnlJT+PjWPxtgmGqKKc+RgTM63U9gN0YzrYc71R2WT/hTA=="],
|
||||||
|
|
||||||
"storybook/ws": ["ws@8.18.0", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw=="],
|
|
||||||
|
|
||||||
"storybook-solidjs-vite/vite-plugin-solid": ["vite-plugin-solid@2.11.12", "", { "dependencies": { "@babel/core": "^7.23.3", "@types/babel__core": "^7.20.4", "babel-preset-solid": "^1.8.4", "merge-anything": "^5.1.7", "solid-refresh": "^0.6.3", "vitefu": "^1.0.4" }, "peerDependencies": { "@testing-library/jest-dom": "^5.16.6 || ^5.17.0 || ^6.*", "solid-js": "^1.7.2", "vite": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0" }, "optionalPeers": ["@testing-library/jest-dom"] }, "sha512-FgjPcx2OwX9h6f28jli7A4bG7PP3te8uyakE5iqsmpq3Jqi1TWLgSroC9N6cMfGRU2zXsl4Q6ISvTr2VL0QHpA=="],
|
"storybook-solidjs-vite/vite-plugin-solid": ["vite-plugin-solid@2.11.12", "", { "dependencies": { "@babel/core": "^7.23.3", "@types/babel__core": "^7.20.4", "babel-preset-solid": "^1.8.4", "merge-anything": "^5.1.7", "solid-refresh": "^0.6.3", "vitefu": "^1.0.4" }, "peerDependencies": { "@testing-library/jest-dom": "^5.16.6 || ^5.17.0 || ^6.*", "solid-js": "^1.7.2", "vite": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0" }, "optionalPeers": ["@testing-library/jest-dom"] }, "sha512-FgjPcx2OwX9h6f28jli7A4bG7PP3te8uyakE5iqsmpq3Jqi1TWLgSroC9N6cMfGRU2zXsl4Q6ISvTr2VL0QHpA=="],
|
||||||
|
|
||||||
"string-width-cjs/emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="],
|
"string-width-cjs/emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="],
|
||||||
|
|||||||
+1
-1
@@ -30,7 +30,7 @@ export const api = new sst.cloudflare.Worker("Api", {
|
|||||||
transform: {
|
transform: {
|
||||||
worker: (args) => {
|
worker: (args) => {
|
||||||
args.logpush = true
|
args.logpush = true
|
||||||
if ($app.stage === "vimtor" || $app.stage === "adam") return
|
if ($app.stage === "vimtor") return
|
||||||
args.bindings = $resolve(args.bindings).apply((bindings) => [
|
args.bindings = $resolve(args.bindings).apply((bindings) => [
|
||||||
...bindings,
|
...bindings,
|
||||||
{
|
{
|
||||||
|
|||||||
+3
-7
@@ -1,9 +1,7 @@
|
|||||||
import { deployAws, domain } from "./stage"
|
import { domain } from "./stage"
|
||||||
import { EMAILOCTOPUS_API_KEY } from "./app"
|
import { EMAILOCTOPUS_API_KEY } from "./app"
|
||||||
import { SECRET } from "./secret"
|
import { SECRET } from "./secret"
|
||||||
|
|
||||||
const lake = deployAws ? await import("./lake") : undefined
|
|
||||||
|
|
||||||
////////////////
|
////////////////
|
||||||
// DATABASE
|
// DATABASE
|
||||||
////////////////
|
////////////////
|
||||||
@@ -242,7 +240,7 @@ const SALESFORCE_INSTANCE_URL = new sst.Secret("SALESFORCE_INSTANCE_URL")
|
|||||||
|
|
||||||
const logProcessor = new sst.cloudflare.Worker("LogProcessor", {
|
const logProcessor = new sst.cloudflare.Worker("LogProcessor", {
|
||||||
handler: "packages/console/function/src/log-processor.ts",
|
handler: "packages/console/function/src/log-processor.ts",
|
||||||
link: [SECRET.HoneycombApiKey, ...(lake?.lakeIngest ? [lake.lakeIngest] : [])],
|
link: [new sst.Secret("HONEYCOMB_API_KEY")],
|
||||||
})
|
})
|
||||||
|
|
||||||
new sst.cloudflare.x.SolidStart("Console", {
|
new sst.cloudflare.x.SolidStart("Console", {
|
||||||
@@ -252,8 +250,6 @@ new sst.cloudflare.x.SolidStart("Console", {
|
|||||||
bucket,
|
bucket,
|
||||||
bucketNew,
|
bucketNew,
|
||||||
database,
|
database,
|
||||||
SECRET.UpstashRedisRestUrl,
|
|
||||||
SECRET.UpstashRedisRestToken,
|
|
||||||
AUTH_API_URL,
|
AUTH_API_URL,
|
||||||
STRIPE_WEBHOOK_SECRET,
|
STRIPE_WEBHOOK_SECRET,
|
||||||
DISCORD_INCIDENT_WEBHOOK_URL,
|
DISCORD_INCIDENT_WEBHOOK_URL,
|
||||||
@@ -285,7 +281,7 @@ new sst.cloudflare.x.SolidStart("Console", {
|
|||||||
},
|
},
|
||||||
transform: {
|
transform: {
|
||||||
server: {
|
server: {
|
||||||
placement: { region: "aws:us-east-2" },
|
placement: { region: "aws:us-east-1" },
|
||||||
transform: {
|
transform: {
|
||||||
worker: {
|
worker: {
|
||||||
tailConsumers: [{ service: logProcessor.nodes.worker.scriptName }],
|
tailConsumers: [{ service: logProcessor.nodes.worker.scriptName }],
|
||||||
|
|||||||
-322
@@ -1,322 +0,0 @@
|
|||||||
import { domain } from "./stage"
|
|
||||||
|
|
||||||
const current = aws.getCallerIdentityOutput({})
|
|
||||||
const partition = aws.getPartitionOutput({})
|
|
||||||
const region = aws.getRegionOutput({})
|
|
||||||
|
|
||||||
const tableBucketName = `opencode-${$app.stage}-lake`
|
|
||||||
const glueCatalogName = "s3tablescatalog"
|
|
||||||
const glueCatalogArn = $interpolate`arn:${partition.partition}:glue:${region.region}:${current.accountId}:catalog`
|
|
||||||
const glueS3TablesCatalogArn = $interpolate`${glueCatalogArn}/${glueCatalogName}`
|
|
||||||
const glueS3TablesChildCatalogArn = $interpolate`${glueS3TablesCatalogArn}/${tableBucketName}`
|
|
||||||
const glueS3TablesDatabaseWildcardArn = $interpolate`arn:${partition.partition}:glue:${region.region}:${current.accountId}:database/${glueCatalogName}/${tableBucketName}/*`
|
|
||||||
const glueS3TablesTableWildcardArn = $interpolate`arn:${partition.partition}:glue:${region.region}:${current.accountId}:table/${glueCatalogName}/${tableBucketName}/*/*`
|
|
||||||
const s3TablesBucketWildcardArn = $interpolate`arn:${partition.partition}:s3tables:${region.region}:${current.accountId}:bucket/*`
|
|
||||||
|
|
||||||
export const tableBucket = new aws.s3tables.TableBucket("LakeTableBucket", {
|
|
||||||
name: tableBucketName,
|
|
||||||
forceDestroy: $app.stage !== "production",
|
|
||||||
})
|
|
||||||
|
|
||||||
const s3TablesCatalog = new aws.cloudcontrol.Resource(
|
|
||||||
"LakeS3TablesCatalog",
|
|
||||||
{
|
|
||||||
typeName: "AWS::Glue::Catalog",
|
|
||||||
desiredState: $jsonStringify({
|
|
||||||
Name: glueCatalogName,
|
|
||||||
Description: "Federated catalog for S3 Tables",
|
|
||||||
FederatedCatalog: {
|
|
||||||
Identifier: s3TablesBucketWildcardArn,
|
|
||||||
ConnectionName: "aws:s3tables",
|
|
||||||
},
|
|
||||||
CreateDatabaseDefaultPermissions: [
|
|
||||||
{
|
|
||||||
Principal: {
|
|
||||||
DataLakePrincipalIdentifier: "IAM_ALLOWED_PRINCIPALS",
|
|
||||||
},
|
|
||||||
Permissions: ["ALL"],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
CreateTableDefaultPermissions: [
|
|
||||||
{
|
|
||||||
Principal: {
|
|
||||||
DataLakePrincipalIdentifier: "IAM_ALLOWED_PRINCIPALS",
|
|
||||||
},
|
|
||||||
Permissions: ["ALL"],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
AllowFullTableExternalDataAccess: "True",
|
|
||||||
}),
|
|
||||||
},
|
|
||||||
{ dependsOn: [tableBucket] },
|
|
||||||
)
|
|
||||||
|
|
||||||
const athenaResultsBucket = new aws.s3.Bucket("LakeAthenaResults", {
|
|
||||||
bucket: `opencode-${$app.stage}-lake-athena-results`,
|
|
||||||
forceDestroy: $app.stage !== "production",
|
|
||||||
})
|
|
||||||
|
|
||||||
const firehoseErrorBucket = new aws.s3.Bucket("LakeFirehoseErrors", {
|
|
||||||
bucket: `opencode-${$app.stage}-lake-firehose-errors`,
|
|
||||||
forceDestroy: $app.stage !== "production",
|
|
||||||
})
|
|
||||||
|
|
||||||
const athenaWorkgroup = new aws.athena.Workgroup("LakeAthenaWorkgroup", {
|
|
||||||
name: `opencode-${$app.stage}-lake-workgroup`,
|
|
||||||
forceDestroy: $app.stage !== "production",
|
|
||||||
configuration: {
|
|
||||||
enforceWorkgroupConfiguration: true,
|
|
||||||
publishCloudwatchMetricsEnabled: true,
|
|
||||||
resultConfiguration: {
|
|
||||||
outputLocation: $interpolate`s3://${athenaResultsBucket.bucket}/`,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
const firehoseRole = new aws.iam.Role("LakeFirehoseRole", {
|
|
||||||
assumeRolePolicy: aws.iam.getPolicyDocumentOutput({
|
|
||||||
statements: [
|
|
||||||
{
|
|
||||||
effect: "Allow",
|
|
||||||
actions: ["sts:AssumeRole"],
|
|
||||||
principals: [
|
|
||||||
{
|
|
||||||
type: "Service",
|
|
||||||
identifiers: ["firehose.amazonaws.com"],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
}).json,
|
|
||||||
})
|
|
||||||
|
|
||||||
const firehosePolicy = new aws.iam.RolePolicy("LakeFirehosePolicy", {
|
|
||||||
role: firehoseRole.id,
|
|
||||||
policy: aws.iam.getPolicyDocumentOutput({
|
|
||||||
statements: [
|
|
||||||
{
|
|
||||||
effect: "Allow",
|
|
||||||
actions: [
|
|
||||||
"s3tables:ListTableBuckets",
|
|
||||||
"s3tables:GetTableBucket",
|
|
||||||
"s3tables:GetNamespace",
|
|
||||||
"s3tables:GetTable",
|
|
||||||
"s3tables:GetTableData",
|
|
||||||
"s3tables:GetTableMetadataLocation",
|
|
||||||
"s3tables:ListNamespaces",
|
|
||||||
"s3tables:ListTables",
|
|
||||||
"s3tables:PutTableData",
|
|
||||||
"s3tables:UpdateTableMetadataLocation",
|
|
||||||
],
|
|
||||||
resources: ["*"],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
effect: "Allow",
|
|
||||||
actions: [
|
|
||||||
"glue:GetCatalog",
|
|
||||||
"glue:GetCatalogs",
|
|
||||||
"glue:GetDatabase",
|
|
||||||
"glue:GetDatabases",
|
|
||||||
"glue:GetTable",
|
|
||||||
"glue:GetTables",
|
|
||||||
"glue:UpdateTable",
|
|
||||||
],
|
|
||||||
resources: [
|
|
||||||
glueCatalogArn,
|
|
||||||
glueS3TablesCatalogArn,
|
|
||||||
$interpolate`${glueS3TablesCatalogArn}/*`,
|
|
||||||
glueS3TablesDatabaseWildcardArn,
|
|
||||||
glueS3TablesTableWildcardArn,
|
|
||||||
$interpolate`arn:${partition.partition}:glue:${region.region}:${current.accountId}:database/*`,
|
|
||||||
$interpolate`arn:${partition.partition}:glue:${region.region}:${current.accountId}:table/*/*`,
|
|
||||||
$interpolate`arn:${partition.partition}:glue:${region.region}:${current.accountId}:table/${glueCatalogName}/*`,
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
effect: "Allow",
|
|
||||||
actions: [
|
|
||||||
"s3:AbortMultipartUpload",
|
|
||||||
"s3:GetBucketLocation",
|
|
||||||
"s3:GetObject",
|
|
||||||
"s3:ListBucket",
|
|
||||||
"s3:ListBucketMultipartUploads",
|
|
||||||
"s3:PutObject",
|
|
||||||
],
|
|
||||||
resources: [firehoseErrorBucket.arn, $interpolate`${firehoseErrorBucket.arn}/*`],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
effect: "Allow",
|
|
||||||
actions: ["lakeformation:GetDataAccess"],
|
|
||||||
resources: ["*"],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
}).json,
|
|
||||||
})
|
|
||||||
|
|
||||||
const firehose = new aws.kinesis.FirehoseDeliveryStream(
|
|
||||||
"LakeFirehose",
|
|
||||||
{
|
|
||||||
name: `opencode-${$app.stage}-lake-ingest`,
|
|
||||||
destination: "iceberg",
|
|
||||||
icebergConfiguration: {
|
|
||||||
appendOnly: true,
|
|
||||||
bufferingInterval: 60,
|
|
||||||
bufferingSize: 1,
|
|
||||||
catalogArn: glueS3TablesChildCatalogArn,
|
|
||||||
processingConfiguration: {
|
|
||||||
enabled: true,
|
|
||||||
processors: [
|
|
||||||
{
|
|
||||||
type: "MetadataExtraction",
|
|
||||||
parameters: [
|
|
||||||
{ parameterName: "JsonParsingEngine", parameterValue: "JQ-1.6" },
|
|
||||||
{
|
|
||||||
parameterName: "MetadataExtractionQuery",
|
|
||||||
parameterValue:
|
|
||||||
'{destinationDatabaseName:._lake_database,destinationTableName:._lake_table,operation:(._lake_operation // "insert")}',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
roleArn: firehoseRole.arn,
|
|
||||||
s3BackupMode: "FailedDataOnly",
|
|
||||||
s3Configuration: {
|
|
||||||
roleArn: firehoseRole.arn,
|
|
||||||
bucketArn: firehoseErrorBucket.arn,
|
|
||||||
errorOutputPrefix: "errors/!{firehose:error-output-type}/",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{ dependsOn: [s3TablesCatalog, firehosePolicy] },
|
|
||||||
)
|
|
||||||
|
|
||||||
export const lakeVpc = new sst.aws.Vpc("LakeVpc")
|
|
||||||
export const lakeCluster = new sst.aws.Cluster("LakeCluster", { vpc: lakeVpc })
|
|
||||||
export const lakeRegion = region.region
|
|
||||||
export const lakeCatalog = $interpolate`${glueCatalogName}/${tableBucket.name}`
|
|
||||||
export const lakeAthenaWorkgroup = athenaWorkgroup
|
|
||||||
|
|
||||||
const ingestSecret = new random.RandomPassword("LakeIngestSecret", { length: 32 })
|
|
||||||
|
|
||||||
const ingestConfig = new sst.Linkable("LakeIngestConfig", {
|
|
||||||
properties: {
|
|
||||||
streamName: firehose.name,
|
|
||||||
secret: ingestSecret.result,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
const ingestService = new sst.aws.Service("LakeIngestService", {
|
|
||||||
cluster: lakeCluster,
|
|
||||||
architecture: "arm64",
|
|
||||||
cpu: "1 vCPU",
|
|
||||||
memory: "4 GB",
|
|
||||||
image: {
|
|
||||||
context: ".",
|
|
||||||
dockerfile: "packages/stats/server/Dockerfile",
|
|
||||||
},
|
|
||||||
link: [ingestConfig],
|
|
||||||
permissions: [
|
|
||||||
{
|
|
||||||
actions: ["firehose:PutRecord", "firehose:PutRecordBatch"],
|
|
||||||
resources: [firehose.arn],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
scaling: {
|
|
||||||
min: $app.stage === "production" ? 2 : 1,
|
|
||||||
max: $app.stage === "production" ? 32 : 4,
|
|
||||||
cpuUtilization: 60,
|
|
||||||
memoryUtilization: 70,
|
|
||||||
},
|
|
||||||
loadBalancer: {
|
|
||||||
domain: {
|
|
||||||
name: `lake.${domain}`,
|
|
||||||
dns: sst.cloudflare.dns(),
|
|
||||||
},
|
|
||||||
rules: [
|
|
||||||
{ listen: "80/http", redirect: "443/https" },
|
|
||||||
{ listen: "443/https", forward: "3000/http" },
|
|
||||||
],
|
|
||||||
health: {
|
|
||||||
"3000/http": {
|
|
||||||
path: "/ready",
|
|
||||||
successCodes: "200-299",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
health: {
|
|
||||||
command: [
|
|
||||||
"CMD-SHELL",
|
|
||||||
"bun --eval \"fetch('http://localhost:3000/health').then((r) => process.exit(r.ok ? 0 : 1)).catch(() => process.exit(1))\"",
|
|
||||||
],
|
|
||||||
interval: "30 seconds",
|
|
||||||
retries: 3,
|
|
||||||
startPeriod: "30 seconds",
|
|
||||||
timeout: "5 seconds",
|
|
||||||
},
|
|
||||||
dev: {
|
|
||||||
command: "bun run start",
|
|
||||||
directory: "packages/stats/server",
|
|
||||||
url: "http://localhost:3000",
|
|
||||||
},
|
|
||||||
wait: $app.stage === "production",
|
|
||||||
})
|
|
||||||
|
|
||||||
export const lakeIngest = new sst.Linkable("LakeIngest", {
|
|
||||||
properties: {
|
|
||||||
url: ingestService.url,
|
|
||||||
secret: ingestSecret.result,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
export const lakeQueryPermissions = [
|
|
||||||
{
|
|
||||||
actions: ["athena:StartQueryExecution", "athena:GetQueryExecution", "athena:GetQueryResults"],
|
|
||||||
resources: [athenaWorkgroup.arn],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
actions: [
|
|
||||||
"glue:GetCatalog",
|
|
||||||
"glue:GetCatalogs",
|
|
||||||
"glue:GetDatabase",
|
|
||||||
"glue:GetDatabases",
|
|
||||||
"glue:GetTable",
|
|
||||||
"glue:GetTables",
|
|
||||||
"glue:GetPartitions",
|
|
||||||
],
|
|
||||||
resources: [
|
|
||||||
glueCatalogArn,
|
|
||||||
glueS3TablesCatalogArn,
|
|
||||||
$interpolate`${glueS3TablesCatalogArn}/*`,
|
|
||||||
glueS3TablesDatabaseWildcardArn,
|
|
||||||
glueS3TablesTableWildcardArn,
|
|
||||||
$interpolate`arn:${partition.partition}:glue:${region.region}:${current.accountId}:database/*`,
|
|
||||||
$interpolate`arn:${partition.partition}:glue:${region.region}:${current.accountId}:table/*/*`,
|
|
||||||
$interpolate`arn:${partition.partition}:glue:${region.region}:${current.accountId}:table/${glueCatalogName}/*`,
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
actions: ["s3:GetBucketLocation", "s3:ListBucket"],
|
|
||||||
resources: [athenaResultsBucket.arn],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
actions: ["s3:GetObject", "s3:PutObject", "s3:AbortMultipartUpload", "s3:ListBucketMultipartUploads"],
|
|
||||||
resources: [$interpolate`${athenaResultsBucket.arn}/*`],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
actions: [
|
|
||||||
"s3tables:GetTableBucket",
|
|
||||||
"s3tables:GetNamespace",
|
|
||||||
"s3tables:GetTable",
|
|
||||||
"s3tables:GetTableData",
|
|
||||||
"s3tables:GetTableMetadataLocation",
|
|
||||||
"s3tables:ListNamespaces",
|
|
||||||
"s3tables:ListTables",
|
|
||||||
],
|
|
||||||
resources: ["*"],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
actions: ["lakeformation:GetDataAccess"],
|
|
||||||
resources: ["*"],
|
|
||||||
},
|
|
||||||
]
|
|
||||||
@@ -7,8 +7,5 @@ sst.Linkable.wrap(random.RandomPassword, (resource) => ({
|
|||||||
export const SECRET = {
|
export const SECRET = {
|
||||||
R2AccessKey: new sst.Secret("R2AccessKey", "unknown"),
|
R2AccessKey: new sst.Secret("R2AccessKey", "unknown"),
|
||||||
R2SecretKey: new sst.Secret("R2SecretKey", "unknown"),
|
R2SecretKey: new sst.Secret("R2SecretKey", "unknown"),
|
||||||
HoneycombApiKey: new sst.Secret("HONEYCOMB_API_KEY"),
|
|
||||||
HoneycombWebhookSecret: new random.RandomPassword("HoneycombWebhookSecret", { length: 24 }),
|
HoneycombWebhookSecret: new random.RandomPassword("HoneycombWebhookSecret", { length: 24 }),
|
||||||
UpstashRedisRestUrl: new sst.Secret("UpstashRedisRestUrl"),
|
|
||||||
UpstashRedisRestToken: new sst.Secret("UpstashRedisRestToken"),
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,8 +5,6 @@ export const domain = (() => {
|
|||||||
})()
|
})()
|
||||||
|
|
||||||
export const zoneID = "430ba34c138cfb5360826c4909f99be8"
|
export const zoneID = "430ba34c138cfb5360826c4909f99be8"
|
||||||
export const awsStage = $app.stage === "production" ? "production" : "dev"
|
|
||||||
export const deployAws = $app.stage === awsStage
|
|
||||||
|
|
||||||
new cloudflare.RegionalHostname("RegionalHostname", {
|
new cloudflare.RegionalHostname("RegionalHostname", {
|
||||||
hostname: domain,
|
hostname: domain,
|
||||||
|
|||||||
-207
@@ -1,207 +0,0 @@
|
|||||||
import { lakeAthenaWorkgroup, lakeCatalog, lakeCluster, lakeQueryPermissions, lakeRegion, tableBucket } from "./lake"
|
|
||||||
|
|
||||||
const domain = (() => {
|
|
||||||
if ($app.stage === "production") return "stats.opencode.ai"
|
|
||||||
if ($app.stage === "dev") return "stats.dev.opencode.ai"
|
|
||||||
return `stats.${$app.stage}.dev.opencode.ai`
|
|
||||||
})()
|
|
||||||
|
|
||||||
////////////////
|
|
||||||
// LAKE
|
|
||||||
////////////////
|
|
||||||
|
|
||||||
const inferenceNamespace = new aws.s3tables.Namespace("LakeInferenceNamespace", {
|
|
||||||
namespace: "inference",
|
|
||||||
tableBucketArn: tableBucket.arn,
|
|
||||||
})
|
|
||||||
|
|
||||||
const inferenceEventTable = new aws.s3tables.Table(
|
|
||||||
"LakeInferenceEventTable",
|
|
||||||
{
|
|
||||||
name: "event",
|
|
||||||
namespace: inferenceNamespace.namespace,
|
|
||||||
tableBucketArn: inferenceNamespace.tableBucketArn,
|
|
||||||
format: "ICEBERG",
|
|
||||||
metadata: {
|
|
||||||
iceberg: {
|
|
||||||
schema: {
|
|
||||||
fields: [
|
|
||||||
{ name: "event_timestamp", type: "string", required: false },
|
|
||||||
{ name: "event_date", type: "string", required: false },
|
|
||||||
{ name: "event_type", type: "string", required: false },
|
|
||||||
{ name: "dataset", type: "string", required: false },
|
|
||||||
{ name: "cf_continent", type: "string", required: false },
|
|
||||||
{ name: "cf_country", type: "string", required: false },
|
|
||||||
{ name: "cf_city", type: "string", required: false },
|
|
||||||
{ name: "cf_region", type: "string", required: false },
|
|
||||||
{ name: "cf_latitude", type: "double", required: false },
|
|
||||||
{ name: "cf_longitude", type: "double", required: false },
|
|
||||||
{ name: "cf_timezone", type: "string", required: false },
|
|
||||||
{ name: "duration", type: "double", required: false },
|
|
||||||
{ name: "request_length", type: "long", required: false },
|
|
||||||
{ name: "status", type: "int", required: false },
|
|
||||||
{ name: "ip", type: "string", required: false },
|
|
||||||
{ name: "is_stream", type: "boolean", required: false },
|
|
||||||
{ name: "session", type: "string", required: false },
|
|
||||||
{ name: "request", type: "string", required: false },
|
|
||||||
{ name: "client", type: "string", required: false },
|
|
||||||
{ name: "user_agent", type: "string", required: false },
|
|
||||||
{ name: "model_variant", type: "string", required: false },
|
|
||||||
{ name: "source", type: "string", required: false },
|
|
||||||
{ name: "provider", type: "string", required: false },
|
|
||||||
{ name: "provider_model", type: "string", required: false },
|
|
||||||
{ name: "model", type: "string", required: false },
|
|
||||||
{ name: "llm_error_code", type: "int", required: false },
|
|
||||||
{ name: "llm_error_message", type: "string", required: false },
|
|
||||||
{ name: "error_response", type: "string", required: false },
|
|
||||||
{ name: "error_type", type: "string", required: false },
|
|
||||||
{ name: "error_message", type: "string", required: false },
|
|
||||||
{ name: "error_cause", type: "string", required: false },
|
|
||||||
{ name: "error_cause2", type: "string", required: false },
|
|
||||||
{ name: "api_key", type: "string", required: false },
|
|
||||||
{ name: "workspace", type: "string", required: false },
|
|
||||||
{ name: "is_subscription", type: "boolean", required: false },
|
|
||||||
{ name: "subscription", type: "string", required: false },
|
|
||||||
{ name: "response_length", type: "long", required: false },
|
|
||||||
{ name: "time_to_first_byte", type: "long", required: false },
|
|
||||||
{ name: "timestamp_first_byte", type: "long", required: false },
|
|
||||||
{ name: "timestamp_last_byte", type: "long", required: false },
|
|
||||||
{ name: "tokens_input", type: "long", required: false },
|
|
||||||
{ name: "tokens_output", type: "long", required: false },
|
|
||||||
{ name: "tokens_reasoning", type: "long", required: false },
|
|
||||||
{ name: "tokens_cache_read", type: "long", required: false },
|
|
||||||
{ name: "tokens_cache_write_5m", type: "long", required: false },
|
|
||||||
{ name: "tokens_cache_write_1h", type: "long", required: false },
|
|
||||||
{ name: "cost_input_microcents", type: "long", required: false },
|
|
||||||
{ name: "cost_output_microcents", type: "long", required: false },
|
|
||||||
{ name: "cost_cache_read_microcents", type: "long", required: false },
|
|
||||||
{ name: "cost_cache_write_microcents", type: "long", required: false },
|
|
||||||
{ name: "cost_total_microcents", type: "long", required: false },
|
|
||||||
{ name: "cost_input", type: "long", required: false },
|
|
||||||
{ name: "cost_output", type: "long", required: false },
|
|
||||||
{ name: "cost_cache_read", type: "long", required: false },
|
|
||||||
{ name: "cost_cache_write_5m", type: "long", required: false },
|
|
||||||
{ name: "cost_cache_write_1h", type: "long", required: false },
|
|
||||||
{ name: "cost_total", type: "long", required: false },
|
|
||||||
],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{ deleteBeforeReplace: $app.stage !== "production" },
|
|
||||||
)
|
|
||||||
|
|
||||||
export const inferenceEvent = new sst.Linkable("InferenceEvent", {
|
|
||||||
properties: {
|
|
||||||
region: lakeRegion,
|
|
||||||
catalog: lakeCatalog,
|
|
||||||
database: inferenceNamespace.namespace,
|
|
||||||
table: inferenceEventTable.name,
|
|
||||||
tableBucket: tableBucket.name,
|
|
||||||
workgroup: lakeAthenaWorkgroup.name,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
////////////////
|
|
||||||
// DATABASE
|
|
||||||
////////////////
|
|
||||||
|
|
||||||
const cluster = planetscale.getDatabaseOutput({
|
|
||||||
name: "opencode-stats",
|
|
||||||
organization: "anomalyco",
|
|
||||||
})
|
|
||||||
|
|
||||||
const branch =
|
|
||||||
$app.stage === "production"
|
|
||||||
? planetscale.getBranchOutput({
|
|
||||||
name: "production",
|
|
||||||
organization: cluster.organization,
|
|
||||||
database: cluster.name,
|
|
||||||
})
|
|
||||||
: new planetscale.Branch("StatsDatabaseBranch", {
|
|
||||||
database: cluster.name,
|
|
||||||
organization: cluster.organization,
|
|
||||||
name: $app.stage,
|
|
||||||
parentBranch: "production",
|
|
||||||
})
|
|
||||||
|
|
||||||
const password = new planetscale.Password("StatsDatabasePassword", {
|
|
||||||
name: $app.stage,
|
|
||||||
database: cluster.name,
|
|
||||||
organization: cluster.organization,
|
|
||||||
branch: branch.name,
|
|
||||||
})
|
|
||||||
|
|
||||||
const databaseUrl = $interpolate`mysql://${password.username.apply(encodeURIComponent)}:${password.plaintext.apply(
|
|
||||||
encodeURIComponent,
|
|
||||||
)}@${password.accessHostUrl}/${cluster.name}`
|
|
||||||
|
|
||||||
export const database = new sst.Linkable("StatsDatabase", {
|
|
||||||
properties: {
|
|
||||||
host: password.accessHostUrl,
|
|
||||||
database: cluster.name,
|
|
||||||
username: password.username,
|
|
||||||
password: password.plaintext,
|
|
||||||
port: 3306,
|
|
||||||
url: databaseUrl,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
new sst.x.DevCommand("StatsStudio", {
|
|
||||||
link: [database],
|
|
||||||
environment: {
|
|
||||||
DATABASE_URL: databaseUrl,
|
|
||||||
},
|
|
||||||
dev: {
|
|
||||||
command: "bun db:studio",
|
|
||||||
directory: "packages/stats/core",
|
|
||||||
autostart: false,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
////////////////
|
|
||||||
// APP
|
|
||||||
////////////////
|
|
||||||
|
|
||||||
// export const app = new sst.cloudflare.x.SolidStart("Stats", {
|
|
||||||
// path: "packages/stats/app",
|
|
||||||
// buildCommand: "bun run build",
|
|
||||||
// domain,
|
|
||||||
// link: [database],
|
|
||||||
// environment: {
|
|
||||||
// PUBLIC_URL: `https://${domain}`,
|
|
||||||
// },
|
|
||||||
// })
|
|
||||||
|
|
||||||
////////////////
|
|
||||||
// SERVICES
|
|
||||||
////////////////
|
|
||||||
|
|
||||||
const statsSyncConfig = new sst.Linkable("StatsSyncConfig", {
|
|
||||||
properties: {
|
|
||||||
dataset: "zen",
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
export const statSync = new sst.aws.Service("StatsSyncService", {
|
|
||||||
cluster: lakeCluster,
|
|
||||||
architecture: "arm64",
|
|
||||||
cpu: "0.25 vCPU",
|
|
||||||
memory: "0.5 GB",
|
|
||||||
image: {
|
|
||||||
context: ".",
|
|
||||||
dockerfile: "packages/stats/server/Dockerfile",
|
|
||||||
},
|
|
||||||
command: ["bun", "src/stat-sync.ts"],
|
|
||||||
link: [database, inferenceEvent, statsSyncConfig],
|
|
||||||
permissions: lakeQueryPermissions,
|
|
||||||
scaling: {
|
|
||||||
min: 1,
|
|
||||||
max: 1,
|
|
||||||
},
|
|
||||||
dev: {
|
|
||||||
command: "bun src/stat-sync.ts",
|
|
||||||
directory: "packages/stats/server",
|
|
||||||
autostart: false,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
+4
-4
@@ -1,8 +1,8 @@
|
|||||||
{
|
{
|
||||||
"nodeModules": {
|
"nodeModules": {
|
||||||
"x86_64-linux": "sha256-70om2TEJYJJOcf9lqex+Sxs27c1SVVgN7FNkLbTXBR0=",
|
"x86_64-linux": "sha256-Nol27sqG6za6oR32MqZrmRxch0/XFeQ61C9Yqtxo7Hk=",
|
||||||
"aarch64-linux": "sha256-/xTsjAFl6TmydJFXETctBoRePIFAkMyxv0HLWHFymJg=",
|
"aarch64-linux": "sha256-+jfkGr4tuv/s/v890XaFTRqUxwA8H8KVMiys6sEXi/4=",
|
||||||
"aarch64-darwin": "sha256-/xbFzrnScm1x4xuz2rEJyyjockDMIIyqJtxm3Axghbg=",
|
"aarch64-darwin": "sha256-FYtKluNazzuQcrr2iHm7B2pJfYrAHkbcj4kWs2FtBgc=",
|
||||||
"x86_64-darwin": "sha256-ZbJDPhmcvBlY80CPmzygw4S8mjdR+d/UyL+3R3nhZtM="
|
"x86_64-darwin": "sha256-PpyWDkzb0p50US7Bs3m3A4PZ0JM3dccGgTKYN9JFYR0="
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+6
-10
@@ -10,7 +10,6 @@
|
|||||||
"dev:desktop": "bun --cwd packages/desktop dev",
|
"dev:desktop": "bun --cwd packages/desktop dev",
|
||||||
"dev:web": "bun --cwd packages/app dev",
|
"dev:web": "bun --cwd packages/app dev",
|
||||||
"dev:console": "ulimit -n 10240 2>/dev/null; bun run --cwd packages/console/app dev",
|
"dev:console": "ulimit -n 10240 2>/dev/null; bun run --cwd packages/console/app dev",
|
||||||
"dev:stats": "bun sst shell --stage=production -- bun run --cwd packages/stats/app dev",
|
|
||||||
"dev:storybook": "bun --cwd packages/storybook storybook",
|
"dev:storybook": "bun --cwd packages/storybook storybook",
|
||||||
"lint": "oxlint",
|
"lint": "oxlint",
|
||||||
"typecheck": "bun turbo typecheck",
|
"typecheck": "bun turbo typecheck",
|
||||||
@@ -18,14 +17,13 @@
|
|||||||
"postinstall": "bun run --cwd packages/opencode fix-node-pty",
|
"postinstall": "bun run --cwd packages/opencode fix-node-pty",
|
||||||
"prepare": "husky",
|
"prepare": "husky",
|
||||||
"random": "echo 'Random script'",
|
"random": "echo 'Random script'",
|
||||||
"sso": "aws sso login --sso-session=opencode --no-browser",
|
"hello": "echo 'Hello World!'",
|
||||||
"test": "echo 'do not run tests from root' && exit 1"
|
"test": "echo 'do not run tests from root' && exit 1"
|
||||||
},
|
},
|
||||||
"workspaces": {
|
"workspaces": {
|
||||||
"packages": [
|
"packages": [
|
||||||
"packages/*",
|
"packages/*",
|
||||||
"packages/console/*",
|
"packages/console/*",
|
||||||
"packages/stats/*",
|
|
||||||
"packages/sdk/js",
|
"packages/sdk/js",
|
||||||
"packages/slack"
|
"packages/slack"
|
||||||
],
|
],
|
||||||
@@ -38,9 +36,9 @@
|
|||||||
"@types/cross-spawn": "6.0.6",
|
"@types/cross-spawn": "6.0.6",
|
||||||
"@octokit/rest": "22.0.0",
|
"@octokit/rest": "22.0.0",
|
||||||
"@hono/zod-validator": "0.4.2",
|
"@hono/zod-validator": "0.4.2",
|
||||||
"@opentui/core": "0.2.16",
|
"@opentui/core": "0.2.15",
|
||||||
"@opentui/keymap": "0.2.16",
|
"@opentui/keymap": "0.2.15",
|
||||||
"@opentui/solid": "0.2.16",
|
"@opentui/solid": "0.2.15",
|
||||||
"ulid": "3.0.1",
|
"ulid": "3.0.1",
|
||||||
"@kobalte/core": "0.13.11",
|
"@kobalte/core": "0.13.11",
|
||||||
"@types/luxon": "3.7.1",
|
"@types/luxon": "3.7.1",
|
||||||
@@ -74,7 +72,6 @@
|
|||||||
"@typescript/native-preview": "7.0.0-dev.20251207.1",
|
"@typescript/native-preview": "7.0.0-dev.20251207.1",
|
||||||
"zod": "4.1.8",
|
"zod": "4.1.8",
|
||||||
"remeda": "2.26.0",
|
"remeda": "2.26.0",
|
||||||
"sst": "4.13.1",
|
|
||||||
"shiki": "3.20.0",
|
"shiki": "3.20.0",
|
||||||
"solid-list": "0.3.0",
|
"solid-list": "0.3.0",
|
||||||
"tailwindcss": "4.1.11",
|
"tailwindcss": "4.1.11",
|
||||||
@@ -101,7 +98,7 @@
|
|||||||
"oxlint-tsgolint": "0.21.0",
|
"oxlint-tsgolint": "0.21.0",
|
||||||
"prettier": "3.6.2",
|
"prettier": "3.6.2",
|
||||||
"semver": "^7.6.0",
|
"semver": "^7.6.0",
|
||||||
"sst": "catalog:",
|
"sst": "4.13.1",
|
||||||
"turbo": "2.8.13"
|
"turbo": "2.8.13"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
@@ -144,7 +141,6 @@
|
|||||||
"@standard-community/standard-openapi@0.2.9": "patches/@standard-community%2Fstandard-openapi@0.2.9.patch",
|
"@standard-community/standard-openapi@0.2.9": "patches/@standard-community%2Fstandard-openapi@0.2.9.patch",
|
||||||
"solid-js@1.9.10": "patches/solid-js@1.9.10.patch",
|
"solid-js@1.9.10": "patches/solid-js@1.9.10.patch",
|
||||||
"virtua@0.49.1": "patches/virtua@0.49.1.patch",
|
"virtua@0.49.1": "patches/virtua@0.49.1.patch",
|
||||||
"@ai-sdk/xai@3.0.82": "patches/@ai-sdk%2Fxai@3.0.82.patch",
|
"@ai-sdk/xai@3.0.82": "patches/@ai-sdk%2Fxai@3.0.82.patch"
|
||||||
"gcp-metadata@8.1.2": "patches/gcp-metadata@8.1.2.patch"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
import { expect, test, type Locator, type Page } from "@playwright/test"
|
import { expect, test, type Locator, type Page, type Route } from "@playwright/test"
|
||||||
import { mockOpenCodeServer } from "../utils/mock-server"
|
|
||||||
|
|
||||||
const directory = "C:/OpenCode/TimelineStateRegression"
|
const directory = "C:/OpenCode/TimelineStateRegression"
|
||||||
const projectID = "proj_timeline_state_regression"
|
const projectID = "proj_timeline_state_regression"
|
||||||
@@ -300,13 +299,39 @@ function readExpanded(element: Element) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function mockServer(page: Page, events: EventPayload[]) {
|
async function mockServer(page: Page, events: EventPayload[]) {
|
||||||
await mockOpenCodeServer(page, {
|
await page.route("**/*", async (route) => {
|
||||||
directory,
|
const url = new URL(route.request().url())
|
||||||
project: project(),
|
const targetPort = process.env.PLAYWRIGHT_SERVER_PORT ?? "4096"
|
||||||
provider: provider(),
|
if (url.port !== targetPort) return route.fallback()
|
||||||
sessions: [session()],
|
|
||||||
pageMessages: () => ({ items: [userMessage, assistantMessage] }),
|
const path = url.pathname
|
||||||
events: () => events.splice(0),
|
if (path === "/global/event") return sse(route, events.splice(0))
|
||||||
|
if (
|
||||||
|
path === "/global/config" ||
|
||||||
|
path === "/config" ||
|
||||||
|
path === "/provider/auth" ||
|
||||||
|
path === "/mcp" ||
|
||||||
|
path === "/session/status"
|
||||||
|
)
|
||||||
|
return json(route, {})
|
||||||
|
if (
|
||||||
|
["/skill", "/command", "/lsp", "/formatter", "/permission", "/question", "/vcs/status", "/vcs/diff"].includes(
|
||||||
|
path,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
return json(route, [])
|
||||||
|
if (path === "/provider") return json(route, provider())
|
||||||
|
if (path === "/path")
|
||||||
|
return json(route, { state: directory, config: directory, worktree: directory, directory, home: "C:/OpenCode" })
|
||||||
|
if (path === "/project") return json(route, [project()])
|
||||||
|
if (path === "/project/current") return json(route, project())
|
||||||
|
if (path === "/agent") return json(route, [{ name: "build", mode: "primary" }])
|
||||||
|
if (path === "/vcs") return json(route, { branch: "main", default_branch: "main" })
|
||||||
|
if (path === "/session") return json(route, [session()])
|
||||||
|
if (path === `/session/${sessionID}`) return json(route, session())
|
||||||
|
if (/^\/session\/[^/]+\/(children|todo|diff)$/.test(path)) return json(route, [])
|
||||||
|
if (path === `/session/${sessionID}/message`) return json(route, [userMessage, assistantMessage])
|
||||||
|
return json(route, {})
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -347,6 +372,24 @@ function provider() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function json(route: Route, body: unknown, headers?: Record<string, string>) {
|
||||||
|
return route.fulfill({
|
||||||
|
status: 200,
|
||||||
|
contentType: "application/json",
|
||||||
|
headers: { "access-control-allow-origin": "*", "access-control-expose-headers": "x-next-cursor", ...headers },
|
||||||
|
body: JSON.stringify(body ?? null),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function sse(route: Route, events: EventPayload[]) {
|
||||||
|
return route.fulfill({
|
||||||
|
status: 200,
|
||||||
|
contentType: "text/event-stream",
|
||||||
|
headers: { "access-control-allow-origin": "*" },
|
||||||
|
body: events.map((event) => `data: ${JSON.stringify(event)}\n\n`).join(""),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
function base64Encode(value: string) {
|
function base64Encode(value: string) {
|
||||||
return Buffer.from(value, "utf8").toString("base64").replace(/\+/g, "-").replace(/\//g, "_").replace(/=/g, "")
|
return Buffer.from(value, "utf8").toString("base64").replace(/\+/g, "-").replace(/\//g, "_").replace(/=/g, "")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
import { expect, test, type Page } from "@playwright/test"
|
import { expect, test, type Page, type Route } from "@playwright/test"
|
||||||
import { mockOpenCodeServer } from "../utils/mock-server"
|
|
||||||
|
|
||||||
const directory = "C:/OpenCode/ContextResizeRegression"
|
const directory = "C:/OpenCode/ContextResizeRegression"
|
||||||
const projectID = "proj_context_resize_regression"
|
const projectID = "proj_context_resize_regression"
|
||||||
@@ -208,12 +207,33 @@ function contextTool(partID: string, messageID: string, tool: string, input: Rec
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function mockServer(page: Page) {
|
async function mockServer(page: Page) {
|
||||||
await mockOpenCodeServer(page, {
|
await page.route("**/*", async (route) => {
|
||||||
directory,
|
const url = new URL(route.request().url())
|
||||||
project: project(),
|
const targetPort = process.env.PLAYWRIGHT_SERVER_PORT ?? "4096"
|
||||||
provider: provider(),
|
if (url.port !== targetPort) return route.fallback()
|
||||||
sessions: [session()],
|
|
||||||
pageMessages: () => ({ items: messages }),
|
const path = url.pathname
|
||||||
|
if (path === "/global/event" || path === "/event") return sse(route)
|
||||||
|
if (["/global/config", "/config", "/provider/auth", "/mcp", "/session/status"].includes(path))
|
||||||
|
return json(route, {})
|
||||||
|
if (
|
||||||
|
["/skill", "/command", "/lsp", "/formatter", "/permission", "/question", "/vcs/status", "/vcs/diff"].includes(
|
||||||
|
path,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
return json(route, [])
|
||||||
|
if (path === "/provider") return json(route, provider())
|
||||||
|
if (path === "/path")
|
||||||
|
return json(route, { state: directory, config: directory, worktree: directory, directory, home: "C:/OpenCode" })
|
||||||
|
if (path === "/project") return json(route, [project()])
|
||||||
|
if (path === "/project/current") return json(route, project())
|
||||||
|
if (path === "/agent") return json(route, [{ name: "build", mode: "primary" }])
|
||||||
|
if (path === "/vcs") return json(route, { branch: "main", default_branch: "main" })
|
||||||
|
if (path === "/session") return json(route, [session()])
|
||||||
|
if (path === `/session/${sessionID}`) return json(route, session())
|
||||||
|
if (/^\/session\/[^/]+\/(children|todo|diff)$/.test(path)) return json(route, [])
|
||||||
|
if (path === `/session/${sessionID}/message`) return json(route, messages)
|
||||||
|
return json(route, {})
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -262,6 +282,19 @@ function provider() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function json(route: Route, body: unknown, headers?: Record<string, string>) {
|
||||||
|
return route.fulfill({
|
||||||
|
status: 200,
|
||||||
|
contentType: "application/json",
|
||||||
|
headers: { "access-control-allow-origin": "*", "access-control-expose-headers": "x-next-cursor", ...headers },
|
||||||
|
body: JSON.stringify(body ?? null),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function sse(route: Route) {
|
||||||
|
return route.fulfill({ status: 200, contentType: "text/event-stream", body: ": ok\n\n" })
|
||||||
|
}
|
||||||
|
|
||||||
function base64Encode(value: string) {
|
function base64Encode(value: string) {
|
||||||
return Buffer.from(value, "utf8").toString("base64").replace(/\+/g, "-").replace(/\//g, "_").replace(/=/g, "")
|
return Buffer.from(value, "utf8").toString("base64").replace(/\+/g, "-").replace(/\//g, "_").replace(/=/g, "")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,7 +18,6 @@ export interface MockServerConfig {
|
|||||||
project: unknown
|
project: unknown
|
||||||
sessions: ({ id: string } & Record<string, unknown>)[]
|
sessions: ({ id: string } & Record<string, unknown>)[]
|
||||||
pageMessages: (sessionId: string, limit: number, before?: string) => { items: unknown[]; cursor?: string }
|
pageMessages: (sessionId: string, limit: number, before?: string) => { items: unknown[]; cursor?: string }
|
||||||
events?: () => unknown[]
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function mockOpenCodeServer(page: Page, config: MockServerConfig) {
|
export async function mockOpenCodeServer(page: Page, config: MockServerConfig) {
|
||||||
@@ -44,8 +43,7 @@ export async function mockOpenCodeServer(page: Page, config: MockServerConfig) {
|
|||||||
if (url.port !== targetPort) return route.fallback()
|
if (url.port !== targetPort) return route.fallback()
|
||||||
|
|
||||||
const path = url.pathname
|
const path = url.pathname
|
||||||
if (path === "/global/event" || path === "/event") return sse(route, config.events?.())
|
if (path === "/global/event" || path === "/event") return sse(route)
|
||||||
if (path === "/global/health") return json(route, { healthy: true })
|
|
||||||
if (emptyObject.has(path)) return json(route, {})
|
if (emptyObject.has(path)) return json(route, {})
|
||||||
if (emptyList.has(path)) return json(route, [])
|
if (emptyList.has(path)) return json(route, [])
|
||||||
if (path in staticRoutes) return json(route, staticRoutes[path])
|
if (path in staticRoutes) return json(route, staticRoutes[path])
|
||||||
@@ -83,10 +81,6 @@ function json(route: Route, body: unknown, headers?: Record<string, string>) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
function sse(route: Route, events?: unknown[]) {
|
function sse(route: Route) {
|
||||||
return route.fulfill({
|
return route.fulfill({ status: 200, contentType: "text/event-stream", body: ": ok\n\n" })
|
||||||
status: 200,
|
|
||||||
contentType: "text/event-stream",
|
|
||||||
body: events?.map((event) => `data: ${JSON.stringify(event)}\n\n`).join("") || ": ok\n\n",
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@opencode-ai/app",
|
"name": "@opencode-ai/app",
|
||||||
"version": "1.15.11",
|
"version": "1.15.10",
|
||||||
"description": "",
|
"description": "",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"exports": {
|
"exports": {
|
||||||
|
|||||||
Binary file not shown.
@@ -14,7 +14,6 @@ import { QueryClient, QueryClientProvider } from "@tanstack/solid-query"
|
|||||||
import { Effect } from "effect"
|
import { Effect } from "effect"
|
||||||
import {
|
import {
|
||||||
type Component,
|
type Component,
|
||||||
createEffect,
|
|
||||||
createMemo,
|
createMemo,
|
||||||
createResource,
|
createResource,
|
||||||
createSignal,
|
createSignal,
|
||||||
@@ -41,13 +40,12 @@ import { NotificationProvider } from "@/context/notification"
|
|||||||
import { PermissionProvider } from "@/context/permission"
|
import { PermissionProvider } from "@/context/permission"
|
||||||
import { PromptProvider } from "@/context/prompt"
|
import { PromptProvider } from "@/context/prompt"
|
||||||
import { ServerConnection, ServerProvider, serverName, useServer } from "@/context/server"
|
import { ServerConnection, ServerProvider, serverName, useServer } from "@/context/server"
|
||||||
import { SettingsProvider, useSettings } from "@/context/settings"
|
import { SettingsProvider } from "@/context/settings"
|
||||||
import { TerminalProvider } from "@/context/terminal"
|
import { TerminalProvider } from "@/context/terminal"
|
||||||
import DirectoryLayout from "@/pages/directory-layout"
|
import DirectoryLayout from "@/pages/directory-layout"
|
||||||
import Layout from "@/pages/layout"
|
import Layout from "@/pages/layout"
|
||||||
import { ErrorPage } from "./pages/error"
|
import { ErrorPage } from "./pages/error"
|
||||||
import { useCheckServerHealth } from "./utils/server-health"
|
import { useCheckServerHealth } from "./utils/server-health"
|
||||||
import { ServersProvider } from "./context/servers"
|
|
||||||
|
|
||||||
const HomeRoute = lazy(() => import("@/pages/home"))
|
const HomeRoute = lazy(() => import("@/pages/home"))
|
||||||
const Session = lazy(() => import("@/pages/session"))
|
const Session = lazy(() => import("@/pages/session"))
|
||||||
@@ -93,26 +91,9 @@ function QueryProvider(props: ParentProps) {
|
|||||||
return <QueryClientProvider client={client}>{props.children}</QueryClientProvider>
|
return <QueryClientProvider client={client}>{props.children}</QueryClientProvider>
|
||||||
}
|
}
|
||||||
|
|
||||||
function BodyDesignClass() {
|
|
||||||
const settings = useSettings()
|
|
||||||
|
|
||||||
createEffect(() => {
|
|
||||||
if (typeof document === "undefined") return
|
|
||||||
|
|
||||||
const enabled = settings.general.newLayoutDesigns()
|
|
||||||
document.body.classList.toggle("text-12-regular", !enabled)
|
|
||||||
document.body.classList.toggle("font-(family-name:--font-family-text)", enabled)
|
|
||||||
document.body.classList.toggle("text-[13px]", enabled)
|
|
||||||
document.body.classList.toggle("font-[440]", enabled)
|
|
||||||
})
|
|
||||||
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
function AppShellProviders(props: ParentProps) {
|
function AppShellProviders(props: ParentProps) {
|
||||||
return (
|
return (
|
||||||
<SettingsProvider>
|
<SettingsProvider>
|
||||||
<BodyDesignClass />
|
|
||||||
<PermissionProvider>
|
<PermissionProvider>
|
||||||
<LayoutProvider>
|
<LayoutProvider>
|
||||||
<NotificationProvider>
|
<NotificationProvider>
|
||||||
@@ -315,8 +296,11 @@ export function AppInterface(props: {
|
|||||||
disableHealthCheck?: boolean
|
disableHealthCheck?: boolean
|
||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
<ServerProvider defaultServer={props.defaultServer} servers={props.servers}>
|
<ServerProvider
|
||||||
<ServersProvider>
|
defaultServer={props.defaultServer}
|
||||||
|
disableHealthCheck={props.disableHealthCheck}
|
||||||
|
servers={props.servers}
|
||||||
|
>
|
||||||
<ConnectionGate disableHealthCheck={props.disableHealthCheck}>
|
<ConnectionGate disableHealthCheck={props.disableHealthCheck}>
|
||||||
<ServerKey>
|
<ServerKey>
|
||||||
<QueryProvider>
|
<QueryProvider>
|
||||||
@@ -337,7 +321,6 @@ export function AppInterface(props: {
|
|||||||
</QueryProvider>
|
</QueryProvider>
|
||||||
</ServerKey>
|
</ServerKey>
|
||||||
</ConnectionGate>
|
</ConnectionGate>
|
||||||
</ServersProvider>
|
|
||||||
</ServerProvider>
|
</ServerProvider>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,8 +4,6 @@ import {
|
|||||||
createEffect,
|
createEffect,
|
||||||
on,
|
on,
|
||||||
Component,
|
Component,
|
||||||
splitProps,
|
|
||||||
For,
|
|
||||||
Show,
|
Show,
|
||||||
onCleanup,
|
onCleanup,
|
||||||
createMemo,
|
createMemo,
|
||||||
@@ -13,10 +11,7 @@ import {
|
|||||||
createResource,
|
createResource,
|
||||||
Switch,
|
Switch,
|
||||||
Match,
|
Match,
|
||||||
type ComponentProps,
|
|
||||||
type JSX,
|
|
||||||
} from "solid-js"
|
} from "solid-js"
|
||||||
import { Popover as KobaltePopover } from "@kobalte/core/popover"
|
|
||||||
import { createStore } from "solid-js/store"
|
import { createStore } from "solid-js/store"
|
||||||
import { useLocal } from "@/context/local"
|
import { useLocal } from "@/context/local"
|
||||||
import { selectionFromLines, type SelectedLineRange, useFile } from "@/context/file"
|
import { selectionFromLines, type SelectedLineRange, useFile } from "@/context/file"
|
||||||
@@ -31,14 +26,12 @@ import {
|
|||||||
FileAttachmentPart,
|
FileAttachmentPart,
|
||||||
} from "@/context/prompt"
|
} from "@/context/prompt"
|
||||||
import { useLayout } from "@/context/layout"
|
import { useLayout } from "@/context/layout"
|
||||||
import { useNavigate } from "@solidjs/router"
|
|
||||||
import { useSDK } from "@/context/sdk"
|
import { useSDK } from "@/context/sdk"
|
||||||
import { useServer } from "@/context/server"
|
|
||||||
import { useSync } from "@/context/sync"
|
import { useSync } from "@/context/sync"
|
||||||
import { useComments } from "@/context/comments"
|
import { useComments } from "@/context/comments"
|
||||||
import { Button } from "@opencode-ai/ui/button"
|
import { Button } from "@opencode-ai/ui/button"
|
||||||
import { DockShellForm, DockTray } from "@opencode-ai/ui/dock-surface"
|
import { DockShellForm, DockTray } from "@opencode-ai/ui/dock-surface"
|
||||||
import { Icon, type IconProps } from "@opencode-ai/ui/icon"
|
import { Icon } from "@opencode-ai/ui/icon"
|
||||||
import { ProviderIcon } from "@opencode-ai/ui/provider-icon"
|
import { ProviderIcon } from "@opencode-ai/ui/provider-icon"
|
||||||
import { Tooltip, TooltipKeybind } from "@opencode-ai/ui/tooltip"
|
import { Tooltip, TooltipKeybind } from "@opencode-ai/ui/tooltip"
|
||||||
import { IconButton } from "@opencode-ai/ui/icon-button"
|
import { IconButton } from "@opencode-ai/ui/icon-button"
|
||||||
@@ -51,7 +44,6 @@ import { Persist, persisted } from "@/utils/persist"
|
|||||||
import { usePermission } from "@/context/permission"
|
import { usePermission } from "@/context/permission"
|
||||||
import { useLanguage } from "@/context/language"
|
import { useLanguage } from "@/context/language"
|
||||||
import { usePlatform } from "@/context/platform"
|
import { usePlatform } from "@/context/platform"
|
||||||
import { useSettings } from "@/context/settings"
|
|
||||||
import { useSessionLayout } from "@/pages/session/session-layout"
|
import { useSessionLayout } from "@/pages/session/session-layout"
|
||||||
import { createSessionTabs } from "@/pages/session/helpers"
|
import { createSessionTabs } from "@/pages/session/helpers"
|
||||||
import { createTextFragment, getCursorPosition, setCursorPosition, setRangeEdge } from "./prompt-input/editor-dom"
|
import { createTextFragment, getCursorPosition, setCursorPosition, setRangeEdge } from "./prompt-input/editor-dom"
|
||||||
@@ -76,14 +68,14 @@ import { ImagePreview } from "@opencode-ai/ui/image-preview"
|
|||||||
import { useQueries } from "@tanstack/solid-query"
|
import { useQueries } from "@tanstack/solid-query"
|
||||||
import { useQueryOptions } from "@/context/server-sync"
|
import { useQueryOptions } from "@/context/server-sync"
|
||||||
import { pathKey } from "@/utils/path-key"
|
import { pathKey } from "@/utils/path-key"
|
||||||
import { base64Encode } from "@opencode-ai/core/util/encode"
|
import { getFilename } from "@opencode-ai/core/util/path"
|
||||||
import { displayName } from "@/pages/layout/helpers"
|
|
||||||
|
|
||||||
interface PromptInputProps {
|
interface PromptInputProps {
|
||||||
class?: string
|
class?: string
|
||||||
variant?: "dock" | "new-session"
|
variant?: "dock" | "new-session"
|
||||||
ref?: (el: HTMLDivElement) => void
|
ref?: (el: HTMLDivElement) => void
|
||||||
newSessionWorktree?: string
|
newSessionWorktree?: string
|
||||||
|
onNewSessionWorktreeChange?: (worktree: string) => void
|
||||||
onNewSessionWorktreeReset?: () => void
|
onNewSessionWorktreeReset?: () => void
|
||||||
edit?: { id: string; prompt: Prompt; context: FollowupDraft["context"] }
|
edit?: { id: string; prompt: Prompt; context: FollowupDraft["context"] }
|
||||||
onEditLoaded?: () => void
|
onEditLoaded?: () => void
|
||||||
@@ -121,9 +113,11 @@ const EXAMPLES = [
|
|||||||
"prompt.example.25",
|
"prompt.example.25",
|
||||||
] as const
|
] as const
|
||||||
|
|
||||||
|
const MAIN_WORKTREE = "main"
|
||||||
|
const CREATE_WORKTREE = "create"
|
||||||
|
|
||||||
export const PromptInput: Component<PromptInputProps> = (props) => {
|
export const PromptInput: Component<PromptInputProps> = (props) => {
|
||||||
const sdk = useSDK()
|
const sdk = useSDK()
|
||||||
const navigate = useNavigate()
|
|
||||||
const queryOptions = useQueryOptions()
|
const queryOptions = useQueryOptions()
|
||||||
|
|
||||||
const sync = useSync()
|
const sync = useSync()
|
||||||
@@ -131,7 +125,6 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
|
|||||||
const files = useFile()
|
const files = useFile()
|
||||||
const prompt = usePrompt()
|
const prompt = usePrompt()
|
||||||
const layout = useLayout()
|
const layout = useLayout()
|
||||||
const server = useServer()
|
|
||||||
const comments = useComments()
|
const comments = useComments()
|
||||||
const dialog = useDialog()
|
const dialog = useDialog()
|
||||||
const providers = useProviders()
|
const providers = useProviders()
|
||||||
@@ -139,13 +132,11 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
|
|||||||
const permission = usePermission()
|
const permission = usePermission()
|
||||||
const language = useLanguage()
|
const language = useLanguage()
|
||||||
const platform = usePlatform()
|
const platform = usePlatform()
|
||||||
const settings = useSettings()
|
|
||||||
const { params, tabs, view } = useSessionLayout()
|
const { params, tabs, view } = useSessionLayout()
|
||||||
let editorRef!: HTMLDivElement
|
let editorRef!: HTMLDivElement
|
||||||
let fileInputRef: HTMLInputElement | undefined
|
let fileInputRef: HTMLInputElement | undefined
|
||||||
let scrollRef!: HTMLDivElement
|
let scrollRef!: HTMLDivElement
|
||||||
let slashPopoverRef!: HTMLDivElement
|
let slashPopoverRef!: HTMLDivElement
|
||||||
let projectSearchRef: HTMLInputElement | undefined
|
|
||||||
|
|
||||||
const mirror = { input: false }
|
const mirror = { input: false }
|
||||||
const inset = 56
|
const inset = 56
|
||||||
@@ -286,10 +277,6 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
|
|||||||
mode: "normal",
|
mode: "normal",
|
||||||
applyingHistory: false,
|
applyingHistory: false,
|
||||||
})
|
})
|
||||||
const [picker, setPicker] = createStore({
|
|
||||||
projectOpen: false,
|
|
||||||
projectSearch: "",
|
|
||||||
})
|
|
||||||
|
|
||||||
const buttonsSpring = useSpring(() => (store.mode === "normal" ? 1 : 0), { visualDuration: 0.2, bounce: 0 })
|
const buttonsSpring = useSpring(() => (store.mode === "normal" ? 1 : 0), { visualDuration: 0.2, bounce: 0 })
|
||||||
const motion = (value: number) => ({
|
const motion = (value: number) => ({
|
||||||
@@ -1316,124 +1303,91 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
|
|||||||
return "Ask anything, / for commands, @ for context..."
|
return "Ask anything, / for commands, @ for context..."
|
||||||
}
|
}
|
||||||
|
|
||||||
const modelControlState = createMemo<ComposerModelControlState>(() => ({
|
const modelControl = () => (
|
||||||
loading: providersLoading(),
|
<Show when={!providersLoading()}>
|
||||||
paid: providers.paid().length > 0,
|
<Show
|
||||||
title: language.t("command.model.choose"),
|
when={providers.paid().length > 0}
|
||||||
keybind: command.keybind("model.choose"),
|
fallback={
|
||||||
model: local.model,
|
<TooltipKeybind
|
||||||
providerID: local.model.current()?.provider?.id,
|
placement="top"
|
||||||
modelName: local.model.current()?.name ?? language.t("dialog.model.select.title"),
|
gutter={4}
|
||||||
style: control(),
|
title={language.t("command.model.choose")}
|
||||||
onClose: restoreFocus,
|
keybind={command.keybind("model.choose")}
|
||||||
onUnpaidClick: () => {
|
>
|
||||||
|
<Button
|
||||||
|
data-action="prompt-model"
|
||||||
|
as="div"
|
||||||
|
variant="ghost"
|
||||||
|
size="normal"
|
||||||
|
class="min-w-0 max-w-[220px] justify-start text-[13px] font-[440] leading-4 text-v2-text-text-faint group"
|
||||||
|
style={control()}
|
||||||
|
onClick={() => {
|
||||||
void import("@/components/dialog-select-model-unpaid").then((x) => {
|
void import("@/components/dialog-select-model-unpaid").then((x) => {
|
||||||
dialog.show(() => <x.DialogSelectModelUnpaid model={local.model} />)
|
dialog.show(() => <x.DialogSelectModelUnpaid model={local.model} />)
|
||||||
})
|
})
|
||||||
},
|
}}
|
||||||
}))
|
>
|
||||||
|
<Show when={local.model.current()?.provider?.id}>
|
||||||
|
<ProviderIcon
|
||||||
|
id={local.model.current()?.provider?.id ?? ""}
|
||||||
|
class="size-4 shrink-0 opacity-40 group-hover:opacity-100 transition-opacity duration-150"
|
||||||
|
style={{ "will-change": "opacity", transform: "translateZ(0)" }}
|
||||||
|
/>
|
||||||
|
</Show>
|
||||||
|
<span class="truncate">{local.model.current()?.name ?? language.t("dialog.model.select.title")}</span>
|
||||||
|
<Icon name="chevron-down" size="small" class="shrink-0 text-v2-icon-icon-muted" />
|
||||||
|
</Button>
|
||||||
|
</TooltipKeybind>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<TooltipKeybind
|
||||||
|
placement="top"
|
||||||
|
gutter={4}
|
||||||
|
title={language.t("command.model.choose")}
|
||||||
|
keybind={command.keybind("model.choose")}
|
||||||
|
>
|
||||||
|
<ModelSelectorPopover
|
||||||
|
model={local.model}
|
||||||
|
triggerAs={Button}
|
||||||
|
triggerProps={{
|
||||||
|
variant: "ghost",
|
||||||
|
size: "normal",
|
||||||
|
style: control(),
|
||||||
|
class:
|
||||||
|
"min-w-0 max-w-[220px] justify-start text-[13px] font-[440] leading-4 text-v2-text-text-faint group",
|
||||||
|
"data-action": "prompt-model",
|
||||||
|
}}
|
||||||
|
onClose={restoreFocus}
|
||||||
|
>
|
||||||
|
<Show when={local.model.current()?.provider?.id}>
|
||||||
|
<ProviderIcon
|
||||||
|
id={local.model.current()?.provider?.id ?? ""}
|
||||||
|
class="size-4 shrink-0 opacity-40 group-hover:opacity-100 transition-opacity duration-150"
|
||||||
|
style={{ "will-change": "opacity", transform: "translateZ(0)" }}
|
||||||
|
/>
|
||||||
|
</Show>
|
||||||
|
<span class="truncate">{local.model.current()?.name ?? language.t("dialog.model.select.title")}</span>
|
||||||
|
<Icon name="chevron-down" size="small" class="shrink-0 text-v2-icon-icon-muted" />
|
||||||
|
</ModelSelectorPopover>
|
||||||
|
</TooltipKeybind>
|
||||||
|
</Show>
|
||||||
|
</Show>
|
||||||
|
)
|
||||||
|
|
||||||
const newSession = () => props.variant === "new-session"
|
const newSession = () => props.variant === "new-session"
|
||||||
const projects = createMemo(() => layout.projects.list())
|
const worktrees = createMemo(() => [MAIN_WORKTREE, ...(sync.project?.sandboxes ?? []), CREATE_WORKTREE])
|
||||||
const projectForDirectory = (directory: string | undefined) => {
|
const currentWorktree = createMemo(() => {
|
||||||
if (!directory) return
|
if (worktrees().includes(props.newSessionWorktree ?? MAIN_WORKTREE))
|
||||||
const key = pathKey(directory)
|
return props.newSessionWorktree ?? MAIN_WORKTREE
|
||||||
return projects().find(
|
return MAIN_WORKTREE
|
||||||
(project) => pathKey(project.worktree) === key || project.sandboxes?.some((sandbox) => pathKey(sandbox) === key),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
const selectedProject = createMemo(() => projectForDirectory(sdk.directory))
|
|
||||||
const projectResults = createMemo(() => {
|
|
||||||
const search = picker.projectSearch.trim().toLowerCase()
|
|
||||||
if (!search) return projects()
|
|
||||||
return projects().filter((project) => displayName(project).toLowerCase().includes(search))
|
|
||||||
})
|
|
||||||
const showAgentControl = createMemo(() => settings.general.showCustomAgents() && agentNames().length > 0)
|
|
||||||
const selectProject = (worktree: string) => {
|
|
||||||
setPicker({
|
|
||||||
projectOpen: false,
|
|
||||||
projectSearch: "",
|
|
||||||
})
|
|
||||||
if (pathKey(worktree) === pathKey(selectedProject()?.worktree ?? "")) {
|
|
||||||
restoreFocus()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
layout.projects.open(worktree)
|
|
||||||
server.projects.touch(worktree)
|
|
||||||
navigate(`/${base64Encode(worktree)}/session`)
|
|
||||||
}
|
|
||||||
const addProject = async () => {
|
|
||||||
const select = (result: string | string[] | null) => {
|
|
||||||
const directory = Array.isArray(result) ? result[0] : result
|
|
||||||
if (!directory) return
|
|
||||||
selectProject(directory)
|
|
||||||
}
|
|
||||||
if (platform.openDirectoryPickerDialog && server.isLocal()) {
|
|
||||||
select(await platform.openDirectoryPickerDialog({ title: language.t("command.project.open") }))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
void import("@/components/dialog-select-directory").then((x) => {
|
|
||||||
dialog.show(
|
|
||||||
() => <x.DialogSelectDirectory onSelect={select} />,
|
|
||||||
() => select(null),
|
|
||||||
)
|
|
||||||
})
|
})
|
||||||
|
const worktreeLabel = (value: string) => {
|
||||||
|
if (value === MAIN_WORKTREE) return MAIN_WORKTREE
|
||||||
|
if (value === CREATE_WORKTREE) return language.t("session.new.worktree.create")
|
||||||
|
return getFilename(value)
|
||||||
}
|
}
|
||||||
|
|
||||||
const projectPickerState = createMemo<ComposerPickerState>(() => ({
|
const USE_V2_INPUT = import.meta.env.VITE_OPENCODE_CHANNEL !== "prod"
|
||||||
open: picker.projectOpen,
|
|
||||||
trigger: {
|
|
||||||
action: "prompt-project",
|
|
||||||
icon: "folder",
|
|
||||||
label: selectedProject() ? displayName(selectedProject()!) : language.t("session.new.project.new"),
|
|
||||||
class: "max-w-[203px]",
|
|
||||||
style: control(),
|
|
||||||
onPress: () => setPicker("projectOpen", true),
|
|
||||||
},
|
|
||||||
search: picker.projectSearch,
|
|
||||||
searchPlaceholder: language.t("session.new.project.search"),
|
|
||||||
clearLabel: language.t("common.clear"),
|
|
||||||
items: projectResults().map((project) => ({
|
|
||||||
icon: "folder",
|
|
||||||
label: displayName(project),
|
|
||||||
selected: selectedProject()?.worktree === project.worktree,
|
|
||||||
onSelect: () => selectProject(project.worktree),
|
|
||||||
})),
|
|
||||||
action: {
|
|
||||||
icon: "plus",
|
|
||||||
label: language.t("session.new.project.add"),
|
|
||||||
onSelect: () => {
|
|
||||||
setPicker("projectOpen", false)
|
|
||||||
void addProject()
|
|
||||||
},
|
|
||||||
},
|
|
||||||
onOpenChange: (open) => {
|
|
||||||
setPicker("projectOpen", open)
|
|
||||||
if (open) requestAnimationFrame(() => projectSearchRef?.focus())
|
|
||||||
},
|
|
||||||
onSearchInput: (value) => setPicker("projectSearch", value),
|
|
||||||
onSearchClear: () => setPicker("projectSearch", ""),
|
|
||||||
searchRef: (el) => (projectSearchRef = el),
|
|
||||||
}))
|
|
||||||
const agentControlState = createMemo<ComposerAgentControlState>(() => ({
|
|
||||||
title: language.t("command.agent.cycle"),
|
|
||||||
keybind: command.keybind("agent.cycle"),
|
|
||||||
options: agentNames(),
|
|
||||||
current: local.agent.current()?.name ?? "",
|
|
||||||
style: control(),
|
|
||||||
onSelect: (value) => {
|
|
||||||
local.agent.set(value)
|
|
||||||
restoreFocus()
|
|
||||||
},
|
|
||||||
}))
|
|
||||||
const newProjectTriggerState = createMemo<ComposerPickerTriggerState>(() => ({
|
|
||||||
action: "prompt-project",
|
|
||||||
icon: "folder-add-left",
|
|
||||||
label: language.t("session.new.project.new"),
|
|
||||||
class: "max-w-[160px]",
|
|
||||||
style: control(),
|
|
||||||
onPress: () => void addProject(),
|
|
||||||
}))
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div class="relative size-full flex flex-col gap-0">
|
<div class="relative size-full flex flex-col gap-0">
|
||||||
@@ -1454,8 +1408,7 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
|
|||||||
t={(key) => language.t(key as Parameters<typeof language.t>[0])}
|
t={(key) => language.t(key as Parameters<typeof language.t>[0])}
|
||||||
/>
|
/>
|
||||||
<Switch>
|
<Switch>
|
||||||
<Match when={settings.general.newLayoutDesigns()}>
|
<Match when={USE_V2_INPUT}>
|
||||||
<div class="flex flex-col gap-3">
|
|
||||||
<DockShellForm
|
<DockShellForm
|
||||||
data-component={newSession() ? "session-new-composer" : "session-composer"}
|
data-component={newSession() ? "session-new-composer" : "session-composer"}
|
||||||
onSubmit={handleSubmit}
|
onSubmit={handleSubmit}
|
||||||
@@ -1497,7 +1450,7 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
|
|||||||
onMouseDown={(e) => {
|
onMouseDown={(e) => {
|
||||||
const target = e.target
|
const target = e.target
|
||||||
if (!(target instanceof HTMLElement)) return
|
if (!(target instanceof HTMLElement)) return
|
||||||
if (target.closest('[data-action^="prompt-"]')) return
|
if (target.closest('[data-action="prompt-attach"], [data-action="prompt-submit"]')) return
|
||||||
editorRef?.focus()
|
editorRef?.focus()
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
@@ -1526,7 +1479,7 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
|
|||||||
onKeyDown={handleKeyDown}
|
onKeyDown={handleKeyDown}
|
||||||
classList={{
|
classList={{
|
||||||
"select-text": true,
|
"select-text": true,
|
||||||
"min-h-[52px] w-full px-4 pt-4 pb-2 focus:outline-none whitespace-pre-wrap leading-5 text-[13px] font-[440] text-v2-text-text-base": true,
|
"min-h-[52px] w-full px-4 pt-4 pb-2 focus:outline-none whitespace-pre-wrap leading-5 text-[13px] font-[440] text-v2-text-text-faint [font-family:Inter,var(--font-family-sans)]": true,
|
||||||
"[&_[data-type=file]]:text-syntax-property": true,
|
"[&_[data-type=file]]:text-syntax-property": true,
|
||||||
"[&_[data-type=agent]]:text-syntax-type": true,
|
"[&_[data-type=agent]]:text-syntax-type": true,
|
||||||
"font-mono!": store.mode === "shell",
|
"font-mono!": store.mode === "shell",
|
||||||
@@ -1562,13 +1515,29 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
|
|||||||
aria-label={language.t("prompt.action.attachFile")}
|
aria-label={language.t("prompt.action.attachFile")}
|
||||||
/>
|
/>
|
||||||
</TooltipKeybind>
|
</TooltipKeybind>
|
||||||
<Show when={showAgentControl()}>
|
<Show when={newSession()}>
|
||||||
<ComposerAgentControl state={agentControlState()} />
|
<div class="relative">
|
||||||
|
<div class="pointer-events-none absolute left-2 top-1/2 z-10 flex size-4 -translate-y-1/2 items-center justify-center">
|
||||||
|
<Icon name="sliders" size="small" />
|
||||||
|
</div>
|
||||||
|
<Select
|
||||||
|
size="normal"
|
||||||
|
options={worktrees()}
|
||||||
|
current={currentWorktree()}
|
||||||
|
label={worktreeLabel}
|
||||||
|
onSelect={(value) => {
|
||||||
|
if (value) props.onNewSessionWorktreeChange?.(value)
|
||||||
|
restoreFocus()
|
||||||
|
}}
|
||||||
|
class="max-w-[175px] justify-start text-text-base [&_[data-component=icon]]:text-v2-icon-icon-muted"
|
||||||
|
valueClass="truncate pl-5 text-[13px] font-[440] leading-4 text-v2-text-text-faint"
|
||||||
|
triggerStyle={control()}
|
||||||
|
triggerProps={{ "data-action": "prompt-workspace" }}
|
||||||
|
variant="ghost"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</Show>
|
</Show>
|
||||||
<Show when={newSession() && !selectedProject()}>
|
{modelControl()}
|
||||||
<ComposerPickerTrigger state={newProjectTriggerState()} />
|
|
||||||
</Show>
|
|
||||||
<ComposerModelControl state={modelControlState()} />
|
|
||||||
</div>
|
</div>
|
||||||
<Tooltip placement="top" inactive={!working() && blank()} value={tip()}>
|
<Tooltip placement="top" inactive={!working() && blank()} value={tip()}>
|
||||||
<IconButton
|
<IconButton
|
||||||
@@ -1588,12 +1557,6 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
|
|||||||
</Tooltip>
|
</Tooltip>
|
||||||
</div>
|
</div>
|
||||||
</DockShellForm>
|
</DockShellForm>
|
||||||
<Show when={newSession() && selectedProject()}>
|
|
||||||
<div class="flex h-7 min-w-0 items-center gap-0 px-2">
|
|
||||||
<ComposerPicker state={projectPickerState()} />
|
|
||||||
</div>
|
|
||||||
</Show>
|
|
||||||
</div>
|
|
||||||
</Match>
|
</Match>
|
||||||
<Match when>
|
<Match when>
|
||||||
<DockShellForm
|
<DockShellForm
|
||||||
@@ -1929,226 +1892,3 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
|
|||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
type ComposerPickerItemState = {
|
|
||||||
icon: IconProps["name"]
|
|
||||||
label: string
|
|
||||||
selected?: boolean
|
|
||||||
onSelect: () => void
|
|
||||||
}
|
|
||||||
|
|
||||||
type ComposerPickerTriggerState = {
|
|
||||||
action: string
|
|
||||||
icon?: IconProps["name"]
|
|
||||||
label: string
|
|
||||||
class?: string
|
|
||||||
style: JSX.CSSProperties | undefined
|
|
||||||
onPress: () => void
|
|
||||||
}
|
|
||||||
|
|
||||||
type ComposerPickerState = {
|
|
||||||
open: boolean
|
|
||||||
trigger: ComposerPickerTriggerState
|
|
||||||
search: string
|
|
||||||
searchPlaceholder: string
|
|
||||||
clearLabel: string
|
|
||||||
items: ComposerPickerItemState[]
|
|
||||||
action: ComposerPickerItemState
|
|
||||||
listClass?: string
|
|
||||||
searchRef: (el: HTMLInputElement) => void
|
|
||||||
onOpenChange: (open: boolean) => void
|
|
||||||
onSearchInput: (value: string) => void
|
|
||||||
onSearchClear: () => void
|
|
||||||
}
|
|
||||||
|
|
||||||
type ComposerAgentControlState = {
|
|
||||||
title: string
|
|
||||||
keybind: string
|
|
||||||
options: string[]
|
|
||||||
current: string
|
|
||||||
style: JSX.CSSProperties | undefined
|
|
||||||
onSelect: (value: string | undefined) => void
|
|
||||||
}
|
|
||||||
|
|
||||||
type ComposerModelControlState = {
|
|
||||||
loading: boolean
|
|
||||||
paid: boolean
|
|
||||||
title: string
|
|
||||||
keybind: string
|
|
||||||
model: ReturnType<typeof useLocal>["model"]
|
|
||||||
providerID?: string
|
|
||||||
modelName: string
|
|
||||||
style: JSX.CSSProperties | undefined
|
|
||||||
onClose: () => void
|
|
||||||
onUnpaidClick: () => void
|
|
||||||
}
|
|
||||||
|
|
||||||
function ComposerPickerTrigger(props: ComponentProps<"button"> & { state: ComposerPickerTriggerState }) {
|
|
||||||
const [local, rest] = splitProps(props, ["state", "class", "style", "onClick"])
|
|
||||||
return (
|
|
||||||
<button
|
|
||||||
{...rest}
|
|
||||||
data-action={local.state.action}
|
|
||||||
type="button"
|
|
||||||
class={`flex h-7 min-w-0 items-center gap-1.5 rounded px-2 text-[13px] font-[440] leading-5 tracking-[-0.04px] text-v2-text-text-faint transition-colors hover:bg-v2-overlay-simple-overlay-hover focus-visible:bg-v2-overlay-simple-overlay-hover focus-visible:outline-none ${local.state.class ?? ""}`}
|
|
||||||
style={local.state.style}
|
|
||||||
onClick={() => local.state.onPress()}
|
|
||||||
>
|
|
||||||
<Show when={local.state.icon}>
|
|
||||||
{(icon) => <Icon name={icon()} size="small" class="shrink-0 text-v2-icon-icon-muted" />}
|
|
||||||
</Show>
|
|
||||||
<span class="min-w-0 truncate leading-5">{local.state.label}</span>
|
|
||||||
<Icon name="chevron-down" size="small" class="shrink-0 text-v2-icon-icon-muted" />
|
|
||||||
</button>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function ComposerPickerMenuItem(props: { state: ComposerPickerItemState }) {
|
|
||||||
return (
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
class="flex h-7 w-full items-center gap-2 rounded px-3 text-left text-[13px] font-[440] leading-5 tracking-[-0.04px] text-v2-text-text-base hover:bg-v2-overlay-simple-overlay-hover focus-visible:bg-v2-overlay-simple-overlay-hover focus-visible:outline-none"
|
|
||||||
onClick={props.state.onSelect}
|
|
||||||
>
|
|
||||||
<Icon name={props.state.icon} size="small" class="shrink-0 text-v2-icon-icon-base" />
|
|
||||||
<span class="min-w-0 flex-1 truncate leading-5">{props.state.label}</span>
|
|
||||||
<Show when={props.state.selected}>
|
|
||||||
<Icon name="check-small" size="small" class="shrink-0 text-v2-icon-icon-base" />
|
|
||||||
</Show>
|
|
||||||
</button>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function ComposerPicker(props: { state: ComposerPickerState }) {
|
|
||||||
return (
|
|
||||||
<KobaltePopover
|
|
||||||
open={props.state.open}
|
|
||||||
placement="bottom-start"
|
|
||||||
gutter={4}
|
|
||||||
modal={false}
|
|
||||||
onOpenChange={props.state.onOpenChange}
|
|
||||||
>
|
|
||||||
<KobaltePopover.Trigger as={ComposerPickerTrigger} state={props.state.trigger} />
|
|
||||||
<KobaltePopover.Portal>
|
|
||||||
<KobaltePopover.Content
|
|
||||||
class="w-[243px] overflow-hidden rounded-md bg-v2-background-bg-layer-01 shadow-[var(--v2-elevation-floating)] focus:outline-none"
|
|
||||||
onOpenAutoFocus={(event) => event.preventDefault()}
|
|
||||||
>
|
|
||||||
<div class={`flex flex-col p-0.5 ${props.state.listClass ?? ""}`}>
|
|
||||||
<div class="flex h-7 items-center gap-2 rounded px-3 text-v2-icon-icon-muted">
|
|
||||||
<Icon name="magnifying-glass" size="small" class="shrink-0" />
|
|
||||||
<input
|
|
||||||
ref={props.state.searchRef}
|
|
||||||
value={props.state.search}
|
|
||||||
placeholder={props.state.searchPlaceholder}
|
|
||||||
class="h-7 min-w-0 flex-1 border-0 bg-transparent text-[13px] font-[440] leading-5 tracking-[-0.04px] text-v2-text-text-base outline-none placeholder:text-v2-text-text-faint"
|
|
||||||
onInput={(event) => props.state.onSearchInput(event.currentTarget.value)}
|
|
||||||
/>
|
|
||||||
<Show when={props.state.search.trim()}>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
class="flex size-5 items-center justify-center rounded text-v2-icon-icon-muted hover:bg-v2-overlay-simple-overlay-hover"
|
|
||||||
onClick={props.state.onSearchClear}
|
|
||||||
aria-label={props.state.clearLabel}
|
|
||||||
>
|
|
||||||
<Icon name="close-small" size="small" />
|
|
||||||
</button>
|
|
||||||
</Show>
|
|
||||||
</div>
|
|
||||||
<For each={props.state.items}>{(item) => <ComposerPickerMenuItem state={item} />}</For>
|
|
||||||
</div>
|
|
||||||
<div class="h-px bg-v2-border-border-muted" />
|
|
||||||
<div class="flex flex-col p-0.5">
|
|
||||||
<ComposerPickerMenuItem state={props.state.action} />
|
|
||||||
</div>
|
|
||||||
</KobaltePopover.Content>
|
|
||||||
</KobaltePopover.Portal>
|
|
||||||
</KobaltePopover>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function ComposerAgentControl(props: { state: ComposerAgentControlState }) {
|
|
||||||
return (
|
|
||||||
<div class="relative">
|
|
||||||
<div class="pointer-events-none absolute left-2 top-1/2 z-10 flex size-4 -translate-y-1/2 items-center justify-center text-v2-icon-icon-muted">
|
|
||||||
<Icon name="sliders" size="small" />
|
|
||||||
</div>
|
|
||||||
<TooltipKeybind placement="top" gutter={4} title={props.state.title} keybind={props.state.keybind}>
|
|
||||||
<Select
|
|
||||||
size="normal"
|
|
||||||
options={props.state.options}
|
|
||||||
current={props.state.current}
|
|
||||||
onSelect={props.state.onSelect}
|
|
||||||
class="max-w-[175px] justify-start text-v2-text-text-faint [&_[data-component=icon]]:text-v2-icon-icon-muted"
|
|
||||||
valueClass="truncate pl-5 text-[13px] font-[440] leading-5 text-v2-text-text-faint"
|
|
||||||
triggerStyle={props.state.style}
|
|
||||||
triggerProps={{ "data-action": "prompt-agent" }}
|
|
||||||
variant="ghost"
|
|
||||||
/>
|
|
||||||
</TooltipKeybind>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function ComposerModelControl(props: { state: ComposerModelControlState }) {
|
|
||||||
return (
|
|
||||||
<Show when={!props.state.loading}>
|
|
||||||
<Show
|
|
||||||
when={props.state.paid}
|
|
||||||
fallback={
|
|
||||||
<TooltipKeybind placement="top" gutter={4} title={props.state.title} keybind={props.state.keybind}>
|
|
||||||
<Button
|
|
||||||
data-action="prompt-model"
|
|
||||||
as="div"
|
|
||||||
variant="ghost"
|
|
||||||
size="normal"
|
|
||||||
class="min-w-0 max-w-[220px] justify-start text-[13px] font-[440] leading-5 text-v2-text-text-faint group"
|
|
||||||
style={props.state.style}
|
|
||||||
onClick={props.state.onUnpaidClick}
|
|
||||||
>
|
|
||||||
<Show when={props.state.providerID}>
|
|
||||||
{(providerID) => (
|
|
||||||
<ProviderIcon
|
|
||||||
id={providerID()}
|
|
||||||
class="size-4 shrink-0 opacity-40 group-hover:opacity-100 transition-opacity duration-150"
|
|
||||||
style={{ "will-change": "opacity", transform: "translateZ(0)" }}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</Show>
|
|
||||||
<span class="truncate">{props.state.modelName}</span>
|
|
||||||
<Icon name="chevron-down" size="small" class="shrink-0 text-v2-icon-icon-muted" />
|
|
||||||
</Button>
|
|
||||||
</TooltipKeybind>
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<TooltipKeybind placement="top" gutter={4} title={props.state.title} keybind={props.state.keybind}>
|
|
||||||
<ModelSelectorPopover
|
|
||||||
model={props.state.model}
|
|
||||||
triggerAs={Button}
|
|
||||||
triggerProps={{
|
|
||||||
variant: "ghost",
|
|
||||||
size: "normal",
|
|
||||||
style: props.state.style,
|
|
||||||
class:
|
|
||||||
"min-w-0 max-w-[220px] justify-start text-[13px] font-[440] leading-5 text-v2-text-text-faint group",
|
|
||||||
"data-action": "prompt-model",
|
|
||||||
}}
|
|
||||||
onClose={props.state.onClose}
|
|
||||||
>
|
|
||||||
<Show when={props.state.providerID}>
|
|
||||||
{(providerID) => (
|
|
||||||
<ProviderIcon
|
|
||||||
id={providerID()}
|
|
||||||
class="size-4 shrink-0 opacity-40 group-hover:opacity-100 transition-opacity duration-150"
|
|
||||||
style={{ "will-change": "opacity", transform: "translateZ(0)" }}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</Show>
|
|
||||||
<span class="truncate">{props.state.modelName}</span>
|
|
||||||
<Icon name="chevron-down" size="small" class="shrink-0 text-v2-icon-icon-muted" />
|
|
||||||
</ModelSelectorPopover>
|
|
||||||
</TooltipKeybind>
|
|
||||||
</Show>
|
|
||||||
</Show>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -24,9 +24,7 @@ import { useSessionLayout } from "@/pages/session/session-layout"
|
|||||||
import { messageAgentColor } from "@/utils/agent"
|
import { messageAgentColor } from "@/utils/agent"
|
||||||
import { decode64 } from "@/utils/base64"
|
import { decode64 } from "@/utils/base64"
|
||||||
import { Persist, persisted } from "@/utils/persist"
|
import { Persist, persisted } from "@/utils/persist"
|
||||||
import { StatusPopover, StatusPopoverV2 } from "../status-popover"
|
import { StatusPopover } from "../status-popover"
|
||||||
import { IconButtonV2 } from "@opencode-ai/ui/v2/components/icon-button-v2.jsx"
|
|
||||||
import { Icon as IconV2 } from "@opencode-ai/ui/v2/components/icon.jsx"
|
|
||||||
|
|
||||||
const OPEN_APPS = [
|
const OPEN_APPS = [
|
||||||
"vscode",
|
"vscode",
|
||||||
@@ -155,11 +153,11 @@ export function SessionHeader() {
|
|||||||
})
|
})
|
||||||
const hotkey = createMemo(() => command.keybind("file.open"))
|
const hotkey = createMemo(() => command.keybind("file.open"))
|
||||||
const os = createMemo(() => detectOS(platform))
|
const os = createMemo(() => detectOS(platform))
|
||||||
const isDesktopV2 = createMemo(() => platform.platform === "desktop" && settings.general.newLayoutDesigns())
|
const isDesktopBeta = platform.platform === "desktop" && import.meta.env.VITE_OPENCODE_CHANNEL === "beta"
|
||||||
const search = createMemo(() => (isDesktopV2() ? settings.general.showSearch() : true))
|
const search = createMemo(() => !isDesktopBeta || settings.general.showSearch())
|
||||||
const tree = createMemo(() => (isDesktopV2() ? settings.general.showFileTree() : true))
|
const tree = createMemo(() => !isDesktopBeta || settings.general.showFileTree())
|
||||||
const term = createMemo(() => (isDesktopV2() ? settings.general.showTerminal() : true))
|
const term = createMemo(() => !isDesktopBeta || settings.general.showTerminal())
|
||||||
const status = createMemo(() => (isDesktopV2() ? settings.general.showStatus() : true))
|
const status = createMemo(() => !isDesktopBeta || settings.general.showStatus())
|
||||||
|
|
||||||
const [exists, setExists] = createStore<Partial<Record<OpenApp, boolean>>>({
|
const [exists, setExists] = createStore<Partial<Record<OpenApp, boolean>>>({
|
||||||
finder: true,
|
finder: true,
|
||||||
@@ -233,14 +231,6 @@ export function SessionHeader() {
|
|||||||
const tint = createMemo(() =>
|
const tint = createMemo(() =>
|
||||||
messageAgentColor(params.id ? sync.data.message[params.id] : undefined, sync.data.agent),
|
messageAgentColor(params.id ? sync.data.message[params.id] : undefined, sync.data.agent),
|
||||||
)
|
)
|
||||||
const v2ActionsState = createMemo<SessionHeaderV2ActionsState>(() => ({
|
|
||||||
statusVisible: status(),
|
|
||||||
statusLabel: language.t("status.popover.trigger"),
|
|
||||||
reviewLabel: language.t("command.review.toggle"),
|
|
||||||
reviewKeybind: command.keybind("review.toggle"),
|
|
||||||
reviewOpened: view().reviewPanel.opened(),
|
|
||||||
onReviewToggle: () => view().reviewPanel.toggle(),
|
|
||||||
}))
|
|
||||||
|
|
||||||
const selectApp = (app: OpenApp) => {
|
const selectApp = (app: OpenApp) => {
|
||||||
if (!options().some((item) => item.id === app)) return
|
if (!options().some((item) => item.id === app)) return
|
||||||
@@ -321,9 +311,6 @@ export function SessionHeader() {
|
|||||||
<Show when={rightMount()}>
|
<Show when={rightMount()}>
|
||||||
{(mount) => (
|
{(mount) => (
|
||||||
<Portal mount={mount()}>
|
<Portal mount={mount()}>
|
||||||
<Show
|
|
||||||
when={isDesktopV2}
|
|
||||||
fallback={
|
|
||||||
<div class="flex items-center gap-2">
|
<div class="flex items-center gap-2">
|
||||||
<Show when={projectDirectory()}>
|
<Show when={projectDirectory()}>
|
||||||
<div class="hidden xl:flex items-center">
|
<div class="hidden xl:flex items-center">
|
||||||
@@ -508,48 +495,9 @@ export function SessionHeader() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
}
|
|
||||||
>
|
|
||||||
<SessionHeaderV2Actions state={v2ActionsState()} />
|
|
||||||
</Show>
|
|
||||||
</Portal>
|
</Portal>
|
||||||
)}
|
)}
|
||||||
</Show>
|
</Show>
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
type SessionHeaderV2ActionsState = {
|
|
||||||
statusVisible: boolean
|
|
||||||
statusLabel: string
|
|
||||||
reviewLabel: string
|
|
||||||
reviewKeybind: string
|
|
||||||
reviewOpened: boolean
|
|
||||||
onReviewToggle: () => void
|
|
||||||
}
|
|
||||||
|
|
||||||
function SessionHeaderV2Actions(props: { state: SessionHeaderV2ActionsState }) {
|
|
||||||
return (
|
|
||||||
<div class="flex items-center gap-0">
|
|
||||||
<Show when={props.state.statusVisible}>
|
|
||||||
<Tooltip placement="bottom" value={props.state.statusLabel}>
|
|
||||||
<StatusPopoverV2 />
|
|
||||||
</Tooltip>
|
|
||||||
</Show>
|
|
||||||
<TooltipKeybind title={props.state.reviewLabel} keybind={props.state.reviewKeybind}>
|
|
||||||
<IconButtonV2
|
|
||||||
type="button"
|
|
||||||
variant="ghost-muted"
|
|
||||||
size="large"
|
|
||||||
class="!w-9 shrink-0"
|
|
||||||
state={props.state.reviewOpened ? "pressed" : undefined}
|
|
||||||
onClick={props.state.onReviewToggle}
|
|
||||||
aria-label={props.state.reviewLabel}
|
|
||||||
aria-expanded={props.state.reviewOpened}
|
|
||||||
aria-controls="review-panel"
|
|
||||||
icon={<IconV2 name="sidebar-right" />}
|
|
||||||
/>
|
|
||||||
</TooltipKeybind>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,13 +1,76 @@
|
|||||||
import type { JSX } from "solid-js"
|
import type { JSX } from "solid-js"
|
||||||
|
import { createMemo } from "solid-js"
|
||||||
|
import { useNavigate } from "@solidjs/router"
|
||||||
|
import { useServerSync } from "@/context/server-sync"
|
||||||
|
import { useLayout } from "@/context/layout"
|
||||||
|
import { useSDK } from "@/context/sdk"
|
||||||
|
import { useServer } from "@/context/server"
|
||||||
|
import { useSync } from "@/context/sync"
|
||||||
|
import { base64Encode } from "@opencode-ai/core/util/encode"
|
||||||
|
import { getFilename } from "@opencode-ai/core/util/path"
|
||||||
|
import { Icon } from "@opencode-ai/ui/icon"
|
||||||
|
import { Select } from "@opencode-ai/ui/select"
|
||||||
import { WordmarkV2 } from "@opencode-ai/ui/v2/components/wordmark-v2.jsx"
|
import { WordmarkV2 } from "@opencode-ai/ui/v2/components/wordmark-v2.jsx"
|
||||||
|
|
||||||
export function NewSessionDesignView(props: { children: JSX.Element }) {
|
const MAIN_WORKTREE = "main"
|
||||||
|
|
||||||
|
export function NewSessionDesignView(props: { worktree: string; children: JSX.Element }) {
|
||||||
|
const serverSync = useServerSync()
|
||||||
|
const layout = useLayout()
|
||||||
|
const navigate = useNavigate()
|
||||||
|
const sdk = useSDK()
|
||||||
|
const server = useServer()
|
||||||
|
const sync = useSync()
|
||||||
|
|
||||||
|
const projectRoot = createMemo(() => sync.project?.worktree ?? sdk.directory)
|
||||||
|
const projects = createMemo(() => {
|
||||||
|
const roots = serverSync.data.project.map((project) => project.worktree)
|
||||||
|
if (roots.includes(projectRoot())) return roots
|
||||||
|
return [projectRoot(), ...roots]
|
||||||
|
})
|
||||||
|
const branch = createMemo(() => sync.data.vcs?.branch ?? MAIN_WORKTREE)
|
||||||
|
|
||||||
|
const openProject = (directory: string | undefined) => {
|
||||||
|
if (!directory) return
|
||||||
|
if (directory === projectRoot()) return
|
||||||
|
layout.projects.open(directory)
|
||||||
|
server.projects.touch(directory)
|
||||||
|
navigate(`/${base64Encode(directory)}/session`)
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div data-component="session-new-design" class="relative size-full overflow-hidden bg-v2-background-bg-deep">
|
<div data-component="session-new-design" class="relative size-full overflow-hidden bg-v2-background-bg-deep">
|
||||||
<div class="absolute inset-x-0 top-[25.375%] flex justify-center px-6">
|
<div class="absolute inset-x-0 top-[25.375%] flex justify-center px-6">
|
||||||
<div class="w-full max-w-[720px]">
|
<div class="w-full max-w-[720px]">
|
||||||
<WordmarkV2 class="h-auto w-full text-v2-icon-icon-base" />
|
<WordmarkV2 class="h-auto w-full text-v2-icon-icon-base" />
|
||||||
<div class="mt-8">{props.children}</div>
|
<div class="mt-8">
|
||||||
|
{props.children}
|
||||||
|
<div class="mt-3 flex h-7 items-center gap-0 pl-2">
|
||||||
|
<Select
|
||||||
|
size="normal"
|
||||||
|
variant="ghost"
|
||||||
|
options={projects()}
|
||||||
|
current={projectRoot()}
|
||||||
|
label={getFilename}
|
||||||
|
onSelect={openProject}
|
||||||
|
class="max-w-[203px] justify-start text-text-base [&_[data-component=icon]]:text-v2-icon-icon-muted"
|
||||||
|
valueClass="truncate text-[length:13px] font-[440] text-v2-text-text-faint"
|
||||||
|
/>
|
||||||
|
<div class="relative">
|
||||||
|
<div class="pointer-events-none absolute left-2 top-1/2 z-10 flex size-4 -translate-y-1/2 items-center justify-center">
|
||||||
|
<Icon name="branch" size="small" />
|
||||||
|
</div>
|
||||||
|
<Select
|
||||||
|
size="normal"
|
||||||
|
variant="ghost"
|
||||||
|
options={[branch()]}
|
||||||
|
current={branch()}
|
||||||
|
class="max-w-[240px] justify-start text-text-base [&_[data-component=icon]]:text-v2-icon-icon-muted"
|
||||||
|
valueClass="truncate pl-5 font-[440] text-v2-text-text-faint"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -399,18 +399,6 @@ export const SettingsGeneral: Component = () => {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</SettingsRow>
|
</SettingsRow>
|
||||||
|
|
||||||
<SettingsRow
|
|
||||||
title={language.t("settings.general.row.newLayoutDesigns.title")}
|
|
||||||
description={language.t("settings.general.row.newLayoutDesigns.description")}
|
|
||||||
>
|
|
||||||
<div data-action="settings-new-layout-designs">
|
|
||||||
<Switch
|
|
||||||
checked={settings.general.newLayoutDesigns()}
|
|
||||||
onChange={(checked) => settings.general.setNewLayoutDesigns(checked)}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</SettingsRow>
|
|
||||||
</SettingsList>
|
</SettingsList>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
@@ -456,6 +444,18 @@ export const SettingsGeneral: Component = () => {
|
|||||||
</div>
|
</div>
|
||||||
</SettingsRow>
|
</SettingsRow>
|
||||||
|
|
||||||
|
<SettingsRow
|
||||||
|
title={language.t("settings.general.row.showTerminal.title")}
|
||||||
|
description={language.t("settings.general.row.showTerminal.description")}
|
||||||
|
>
|
||||||
|
<div data-action="settings-show-terminal">
|
||||||
|
<Switch
|
||||||
|
checked={settings.general.showTerminal()}
|
||||||
|
onChange={(checked) => settings.general.setShowTerminal(checked)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</SettingsRow>
|
||||||
|
|
||||||
<SettingsRow
|
<SettingsRow
|
||||||
title={language.t("settings.general.row.showStatus.title")}
|
title={language.t("settings.general.row.showStatus.title")}
|
||||||
description={language.t("settings.general.row.showStatus.description")}
|
description={language.t("settings.general.row.showStatus.description")}
|
||||||
@@ -467,18 +467,6 @@ export const SettingsGeneral: Component = () => {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</SettingsRow>
|
</SettingsRow>
|
||||||
|
|
||||||
<SettingsRow
|
|
||||||
title={language.t("settings.general.row.showCustomAgents.title")}
|
|
||||||
description={language.t("settings.general.row.showCustomAgents.description")}
|
|
||||||
>
|
|
||||||
<div data-action="settings-show-custom-agents">
|
|
||||||
<Switch
|
|
||||||
checked={settings.general.showCustomAgents()}
|
|
||||||
onChange={(checked) => settings.general.setShowCustomAgents(checked)}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</SettingsRow>
|
|
||||||
</SettingsList>
|
</SettingsList>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
@@ -793,6 +781,7 @@ export const SettingsGeneral: Component = () => {
|
|||||||
</Show>
|
</Show>
|
||||||
)
|
)
|
||||||
|
|
||||||
|
console.log(import.meta.env)
|
||||||
return (
|
return (
|
||||||
<div class="flex flex-col h-full overflow-y-auto no-scrollbar px-4 pb-10 sm:px-10 sm:pb-10">
|
<div class="flex flex-col h-full overflow-y-auto no-scrollbar px-4 pb-10 sm:px-10 sm:pb-10">
|
||||||
<div class="sticky top-0 z-10 bg-[linear-gradient(to_bottom,var(--surface-stronger-non-alpha)_calc(100%_-_24px),transparent)]">
|
<div class="sticky top-0 z-10 bg-[linear-gradient(to_bottom,var(--surface-stronger-non-alpha)_calc(100%_-_24px),transparent)]">
|
||||||
@@ -814,7 +803,7 @@ export const SettingsGeneral: Component = () => {
|
|||||||
|
|
||||||
<DisplaySection />
|
<DisplaySection />
|
||||||
|
|
||||||
<Show when={desktop()}>
|
<Show when={desktop() && import.meta.env.VITE_OPENCODE_CHANNEL === "beta"}>
|
||||||
<AdvancedSection />
|
<AdvancedSection />
|
||||||
</Show>
|
</Show>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -7,17 +7,16 @@ import { useMutation, useQueryClient } from "@tanstack/solid-query"
|
|||||||
import { showToast } from "@opencode-ai/ui/toast"
|
import { showToast } from "@opencode-ai/ui/toast"
|
||||||
import { useNavigate } from "@solidjs/router"
|
import { useNavigate } from "@solidjs/router"
|
||||||
import { type Accessor, createEffect, createMemo, For, type JSXElement, onCleanup, Show } from "solid-js"
|
import { type Accessor, createEffect, createMemo, For, type JSXElement, onCleanup, Show } from "solid-js"
|
||||||
import { createStore } from "solid-js/store"
|
import { createStore, reconcile } from "solid-js/store"
|
||||||
import { ServerHealthIndicator, ServerRow } from "@/components/server/server-row"
|
import { ServerHealthIndicator, ServerRow } from "@/components/server/server-row"
|
||||||
import { useLanguage } from "@/context/language"
|
import { useLanguage } from "@/context/language"
|
||||||
import { usePlatform } from "@/context/platform"
|
import { usePlatform } from "@/context/platform"
|
||||||
import { useSDK } from "@/context/sdk"
|
import { useSDK } from "@/context/sdk"
|
||||||
import { normalizeServerUrl, ServerConnection, useServer } from "@/context/server"
|
import { normalizeServerUrl, ServerConnection, useServer } from "@/context/server"
|
||||||
import { useSync } from "@/context/sync"
|
import { useSync } from "@/context/sync"
|
||||||
import { type ServerHealth } from "@/utils/server-health"
|
import { useCheckServerHealth, type ServerHealth } from "@/utils/server-health"
|
||||||
import { useQueryOptions } from "@/context/server-sync"
|
import { useQueryOptions } from "@/context/server-sync"
|
||||||
import { pathKey } from "@/utils/path-key"
|
import { pathKey } from "@/utils/path-key"
|
||||||
import { useServers } from "@/context/servers"
|
|
||||||
|
|
||||||
const pollMs = 10_000
|
const pollMs = 10_000
|
||||||
|
|
||||||
@@ -55,6 +54,40 @@ const listServersByHealth = (
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const useServerHealth = (servers: Accessor<ServerConnection.Any[]>, enabled: Accessor<boolean>) => {
|
||||||
|
const checkServerHealth = useCheckServerHealth()
|
||||||
|
const [status, setStatus] = createStore({} as Record<ServerConnection.Key, ServerHealth | undefined>)
|
||||||
|
|
||||||
|
createEffect(() => {
|
||||||
|
if (!enabled()) {
|
||||||
|
setStatus(reconcile({}))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const list = servers()
|
||||||
|
let dead = false
|
||||||
|
|
||||||
|
const refresh = async () => {
|
||||||
|
const results: Record<string, ServerHealth> = {}
|
||||||
|
await Promise.all(
|
||||||
|
list.map(async (conn) => {
|
||||||
|
results[ServerConnection.key(conn)] = await checkServerHealth(conn.http)
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
if (dead) return
|
||||||
|
setStatus(reconcile(results))
|
||||||
|
}
|
||||||
|
|
||||||
|
void refresh()
|
||||||
|
const id = setInterval(() => void refresh(), pollMs)
|
||||||
|
onCleanup(() => {
|
||||||
|
dead = true
|
||||||
|
clearInterval(id)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
return status
|
||||||
|
}
|
||||||
|
|
||||||
const useDefaultServerKey = (
|
const useDefaultServerKey = (
|
||||||
get: (() => string | Promise<string | null | undefined> | null | undefined) | undefined,
|
get: (() => string | Promise<string | null | undefined> | null | undefined) | undefined,
|
||||||
) => {
|
) => {
|
||||||
@@ -133,162 +166,8 @@ const useMcpToggleMutation = () => {
|
|||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
type ServerStatusState = {
|
|
||||||
servers: () => ServerStatusItem[]
|
|
||||||
defaultKey: () => ServerConnection.Key | undefined
|
|
||||||
ariaLabel: string
|
|
||||||
serversLabel: string
|
|
||||||
defaultLabel: string
|
|
||||||
manageLabel: string
|
|
||||||
onManage: () => void
|
|
||||||
}
|
|
||||||
|
|
||||||
type ServerStatusItem = {
|
|
||||||
key: ServerConnection.Key
|
|
||||||
conn: ServerConnection.Any
|
|
||||||
health?: ServerHealth
|
|
||||||
blocked: boolean
|
|
||||||
active: boolean
|
|
||||||
onSelect: () => void
|
|
||||||
}
|
|
||||||
|
|
||||||
export function StatusPopoverServerBody() {
|
|
||||||
const servers = useServers()
|
|
||||||
const server = useServer()
|
|
||||||
const platform = usePlatform()
|
|
||||||
const dialog = useDialog()
|
|
||||||
const language = useLanguage()
|
|
||||||
const navigate = useNavigate()
|
|
||||||
|
|
||||||
let dialogRun = 0
|
|
||||||
let dialogDead = false
|
|
||||||
onCleanup(() => {
|
|
||||||
dialogDead = true
|
|
||||||
dialogRun += 1
|
|
||||||
})
|
|
||||||
|
|
||||||
const sortedServers = createMemo(() => listServersByHealth(servers.list(), server.key, servers.health))
|
|
||||||
const defaultServer = useDefaultServerKey(platform.getDefaultServer)
|
|
||||||
const serverItems = createMemo(() =>
|
|
||||||
sortedServers().map((conn) => {
|
|
||||||
const key = ServerConnection.key(conn)
|
|
||||||
return {
|
|
||||||
key,
|
|
||||||
conn,
|
|
||||||
health: servers.health[key],
|
|
||||||
blocked: servers.health[key]?.healthy === false,
|
|
||||||
active: !!server.current && key === ServerConnection.key(server.current),
|
|
||||||
onSelect: () => {
|
|
||||||
navigate("/")
|
|
||||||
queueMicrotask(() => server.setActive(key))
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
|
|
||||||
return (
|
|
||||||
<ServerStatusPopoverView
|
|
||||||
state={{
|
|
||||||
servers: serverItems,
|
|
||||||
defaultKey: defaultServer.key,
|
|
||||||
ariaLabel: language.t("status.popover.ariaLabel"),
|
|
||||||
serversLabel: language.t("status.popover.tab.servers"),
|
|
||||||
defaultLabel: language.t("common.default"),
|
|
||||||
manageLabel: language.t("status.popover.action.manageServers"),
|
|
||||||
onManage: () => {
|
|
||||||
const run = ++dialogRun
|
|
||||||
void import("./dialog-select-server").then((x) => {
|
|
||||||
if (dialogDead || dialogRun !== run) return
|
|
||||||
dialog.show(() => <x.DialogSelectServer />, defaultServer.refresh)
|
|
||||||
})
|
|
||||||
},
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function ServerStatusPopoverView(props: { state: ServerStatusState }) {
|
|
||||||
return (
|
|
||||||
<div class="flex items-center gap-1 w-[360px] rounded-xl shadow-[var(--shadow-lg-border-base)]">
|
|
||||||
<Tabs
|
|
||||||
aria-label={props.state.ariaLabel}
|
|
||||||
class="tabs bg-background-strong rounded-xl overflow-hidden"
|
|
||||||
data-component="tabs"
|
|
||||||
data-active="servers"
|
|
||||||
defaultValue="servers"
|
|
||||||
variant="alt"
|
|
||||||
>
|
|
||||||
<Tabs.List data-slot="tablist" class="bg-transparent border-b-0 px-4 pt-2 pb-0 gap-4 h-10">
|
|
||||||
<Tabs.Trigger value="servers" data-slot="tab" class="text-12-regular">
|
|
||||||
{props.state.servers().length > 0 ? `${props.state.servers().length} ` : ""}
|
|
||||||
{props.state.serversLabel}
|
|
||||||
</Tabs.Trigger>
|
|
||||||
</Tabs.List>
|
|
||||||
<Tabs.Content value="servers">
|
|
||||||
<ServerStatusList state={props.state} />
|
|
||||||
</Tabs.Content>
|
|
||||||
</Tabs>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function ServerStatusList(props: { state: ServerStatusState }) {
|
|
||||||
return (
|
|
||||||
<div class="flex flex-col px-2 pb-2">
|
|
||||||
<div class="flex flex-col p-3 bg-background-base rounded-sm min-h-14">
|
|
||||||
<For each={props.state.servers()}>
|
|
||||||
{(item) => {
|
|
||||||
return (
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
class="flex items-center gap-2 w-full h-8 pl-3 pr-1.5 py-1.5 rounded-md transition-colors text-left"
|
|
||||||
classList={{
|
|
||||||
"hover:bg-surface-raised-base-hover": !item.blocked,
|
|
||||||
"cursor-not-allowed": item.blocked,
|
|
||||||
}}
|
|
||||||
aria-disabled={item.blocked}
|
|
||||||
onClick={() => {
|
|
||||||
if (item.blocked) return
|
|
||||||
item.onSelect()
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<ServerHealthIndicator health={item.health} />
|
|
||||||
<ServerRow
|
|
||||||
conn={item.conn}
|
|
||||||
dimmed={item.blocked}
|
|
||||||
status={item.health}
|
|
||||||
class="flex items-center gap-2 w-full min-w-0"
|
|
||||||
nameClass="text-14-regular text-text-base truncate"
|
|
||||||
versionClass="text-12-regular text-text-weak truncate"
|
|
||||||
badge={
|
|
||||||
<Show when={item.key === props.state.defaultKey()}>
|
|
||||||
<span class="text-11-regular text-text-base bg-surface-base px-1.5 py-0.5 rounded-md">
|
|
||||||
{props.state.defaultLabel}
|
|
||||||
</span>
|
|
||||||
</Show>
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<div class="flex-1" />
|
|
||||||
<Show when={item.active}>
|
|
||||||
<Icon name="check" size="small" class="text-icon-weak shrink-0" />
|
|
||||||
</Show>
|
|
||||||
</ServerRow>
|
|
||||||
</button>
|
|
||||||
)
|
|
||||||
}}
|
|
||||||
</For>
|
|
||||||
|
|
||||||
<Button variant="secondary" class="mt-3 self-start h-8 px-3 py-1.5" onClick={props.state.onManage}>
|
|
||||||
{props.state.manageLabel}
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export function StatusPopoverBody(props: { shown: Accessor<boolean> }) {
|
export function StatusPopoverBody(props: { shown: Accessor<boolean> }) {
|
||||||
const sync = useSync()
|
const sync = useSync()
|
||||||
const servers = useServers()
|
|
||||||
const server = useServer()
|
const server = useServer()
|
||||||
const platform = usePlatform()
|
const platform = usePlatform()
|
||||||
const dialog = useDialog()
|
const dialog = useDialog()
|
||||||
@@ -313,7 +192,15 @@ export function StatusPopoverBody(props: { shown: Accessor<boolean> }) {
|
|||||||
dialogDead = true
|
dialogDead = true
|
||||||
dialogRun += 1
|
dialogRun += 1
|
||||||
})
|
})
|
||||||
const sortedServers = createMemo(() => listServersByHealth(servers.list(), server.key, servers.health))
|
const servers = createMemo(() => {
|
||||||
|
const current = server.current
|
||||||
|
const list = server.list
|
||||||
|
if (!current) return list
|
||||||
|
if (list.every((item) => ServerConnection.key(item) !== ServerConnection.key(current))) return [current, ...list]
|
||||||
|
return [current, ...list.filter((item) => ServerConnection.key(item) !== ServerConnection.key(current))]
|
||||||
|
})
|
||||||
|
const health = useServerHealth(servers, props.shown)
|
||||||
|
const sortedServers = createMemo(() => listServersByHealth(servers(), server.key, health))
|
||||||
const toggleMcp = useMcpToggleMutation()
|
const toggleMcp = useMcpToggleMutation()
|
||||||
const defaultServer = useDefaultServerKey(platform.getDefaultServer)
|
const defaultServer = useDefaultServerKey(platform.getDefaultServer)
|
||||||
const mcpNames = createMemo(() => Object.keys(sync.data.mcp ?? {}).sort((a, b) => a.localeCompare(b)))
|
const mcpNames = createMemo(() => Object.keys(sync.data.mcp ?? {}).sort((a, b) => a.localeCompare(b)))
|
||||||
@@ -339,7 +226,7 @@ export function StatusPopoverBody(props: { shown: Accessor<boolean> }) {
|
|||||||
>
|
>
|
||||||
<Tabs.List data-slot="tablist" class="bg-transparent border-b-0 px-4 pt-2 pb-0 gap-4 h-10">
|
<Tabs.List data-slot="tablist" class="bg-transparent border-b-0 px-4 pt-2 pb-0 gap-4 h-10">
|
||||||
<Tabs.Trigger value="servers" data-slot="tab" class="text-12-regular">
|
<Tabs.Trigger value="servers" data-slot="tab" class="text-12-regular">
|
||||||
{servers.list().length > 0 ? `${servers.list().length} ` : ""}
|
{sortedServers().length > 0 ? `${sortedServers().length} ` : ""}
|
||||||
{language.t("status.popover.tab.servers")}
|
{language.t("status.popover.tab.servers")}
|
||||||
</Tabs.Trigger>
|
</Tabs.Trigger>
|
||||||
<Tabs.Trigger value="mcp" data-slot="tab" class="text-12-regular">
|
<Tabs.Trigger value="mcp" data-slot="tab" class="text-12-regular">
|
||||||
@@ -362,7 +249,7 @@ export function StatusPopoverBody(props: { shown: Accessor<boolean> }) {
|
|||||||
<For each={sortedServers()}>
|
<For each={sortedServers()}>
|
||||||
{(s) => {
|
{(s) => {
|
||||||
const key = ServerConnection.key(s)
|
const key = ServerConnection.key(s)
|
||||||
const blocked = () => servers.health[key]?.healthy === false
|
const blocked = () => health[key]?.healthy === false
|
||||||
return (
|
return (
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
@@ -378,11 +265,11 @@ export function StatusPopoverBody(props: { shown: Accessor<boolean> }) {
|
|||||||
queueMicrotask(() => server.setActive(key))
|
queueMicrotask(() => server.setActive(key))
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<ServerHealthIndicator health={servers.health[key]} />
|
<ServerHealthIndicator health={health[key]} />
|
||||||
<ServerRow
|
<ServerRow
|
||||||
conn={s}
|
conn={s}
|
||||||
dimmed={blocked()}
|
dimmed={blocked()}
|
||||||
status={servers.health[key]}
|
status={health[key]}
|
||||||
class="flex items-center gap-2 w-full min-w-0"
|
class="flex items-center gap-2 w-full min-w-0"
|
||||||
nameClass="text-14-regular text-text-base truncate"
|
nameClass="text-14-regular text-text-base truncate"
|
||||||
versionClass="text-12-regular text-text-weak truncate"
|
versionClass="text-12-regular text-text-weak truncate"
|
||||||
|
|||||||
@@ -1,24 +1,19 @@
|
|||||||
import { Button } from "@opencode-ai/ui/button"
|
import { Button } from "@opencode-ai/ui/button"
|
||||||
import { Icon } from "@opencode-ai/ui/icon"
|
import { Icon } from "@opencode-ai/ui/icon"
|
||||||
import { IconButtonV2 } from "@opencode-ai/ui/v2/components/icon-button-v2.jsx"
|
|
||||||
import { Icon as IconV2 } from "@opencode-ai/ui/v2/components/icon.jsx"
|
|
||||||
import { Popover } from "@opencode-ai/ui/popover"
|
import { Popover } from "@opencode-ai/ui/popover"
|
||||||
import { Suspense, createMemo, createSignal, lazy, Show, type JSX } from "solid-js"
|
import { Suspense, createMemo, createSignal, lazy, Show } from "solid-js"
|
||||||
import { useLanguage } from "@/context/language"
|
import { useLanguage } from "@/context/language"
|
||||||
import { useServer } from "@/context/server"
|
import { useServer } from "@/context/server"
|
||||||
import { useSync } from "@/context/sync"
|
import { useSync } from "@/context/sync"
|
||||||
import { useServers } from "@/context/servers"
|
|
||||||
|
|
||||||
const Body = lazy(() => import("./status-popover-body").then((x) => ({ default: x.StatusPopoverBody })))
|
const Body = lazy(() => import("./status-popover-body").then((x) => ({ default: x.StatusPopoverBody })))
|
||||||
const ServerBody = lazy(() => import("./status-popover-body").then((x) => ({ default: x.StatusPopoverServerBody })))
|
|
||||||
|
|
||||||
export function StatusPopover() {
|
export function StatusPopover() {
|
||||||
const language = useLanguage()
|
const language = useLanguage()
|
||||||
const server = useServer()
|
const server = useServer()
|
||||||
const servers = useServers()
|
|
||||||
const sync = useSync()
|
const sync = useSync()
|
||||||
const [shown, setShown] = createSignal(false)
|
const [shown, setShown] = createSignal(false)
|
||||||
const ready = createMemo(() => servers.health[server.key]?.healthy === false || sync.data.mcp_ready)
|
const ready = createMemo(() => server.healthy() === false || sync.data.mcp_ready)
|
||||||
const mcpIssue = createMemo(() => {
|
const mcpIssue = createMemo(() => {
|
||||||
const mcp = Object.values(sync.data.mcp ?? {})
|
const mcp = Object.values(sync.data.mcp ?? {})
|
||||||
const failed = mcp.some((item) => item.status === "failed" || item.status === "needs_client_registration")
|
const failed = mcp.some((item) => item.status === "failed" || item.status === "needs_client_registration")
|
||||||
@@ -26,8 +21,7 @@ export function StatusPopover() {
|
|||||||
if (failed) return "critical" as const
|
if (failed) return "critical" as const
|
||||||
if (warn) return "warning" as const
|
if (warn) return "warning" as const
|
||||||
})
|
})
|
||||||
const serverHealthy = () => servers.health[server.key]?.healthy === true
|
const healthy = createMemo(() => server.healthy() === true && !mcpIssue())
|
||||||
const healthy = createMemo(() => servers.health[server.key]?.healthy === true && !mcpIssue())
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Popover
|
<Popover
|
||||||
@@ -49,9 +43,10 @@ export function StatusPopover() {
|
|||||||
classList={{
|
classList={{
|
||||||
"absolute -top-px -right-px size-1.5 rounded-full": true,
|
"absolute -top-px -right-px size-1.5 rounded-full": true,
|
||||||
"bg-icon-success-base": ready() && healthy(),
|
"bg-icon-success-base": ready() && healthy(),
|
||||||
"bg-icon-warning-base": ready() && serverHealthy() && mcpIssue() === "warning",
|
"bg-icon-warning-base": ready() && server.healthy() === true && mcpIssue() === "warning",
|
||||||
"bg-icon-critical-base": serverHealthy() || (ready() && serverHealthy() && mcpIssue() === "critical"),
|
"bg-icon-critical-base":
|
||||||
"bg-border-weak-base": serverHealthy() || !ready(),
|
server.healthy() === false || (ready() && server.healthy() === true && mcpIssue() === "critical"),
|
||||||
|
"bg-border-weak-base": server.healthy() === undefined || !ready(),
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@@ -73,135 +68,3 @@ export function StatusPopover() {
|
|||||||
</Popover>
|
</Popover>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export function StatusPopoverV2(props: { scope?: "server" }) {
|
|
||||||
if (props.scope === "server") return <ServerStatusPopover />
|
|
||||||
return <DirectoryStatusPopover />
|
|
||||||
}
|
|
||||||
|
|
||||||
function DirectoryStatusPopover() {
|
|
||||||
const language = useLanguage()
|
|
||||||
const server = useServer()
|
|
||||||
const servers = useServers()
|
|
||||||
const sync = useSync()
|
|
||||||
const [shown, setShown] = createSignal(false)
|
|
||||||
const serverHealth = () => servers.health[server.key]?.healthy
|
|
||||||
const ready = createMemo(() => serverHealth() === false || sync.data.mcp_ready)
|
|
||||||
const mcpIssue = createMemo(() => {
|
|
||||||
const mcp = Object.values(sync.data.mcp ?? {})
|
|
||||||
const failed = mcp.some((item) => item.status === "failed" || item.status === "needs_client_registration")
|
|
||||||
const warn = mcp.some((item) => item.status === "needs_auth")
|
|
||||||
if (failed) return "critical" as const
|
|
||||||
if (warn) return "warning" as const
|
|
||||||
})
|
|
||||||
const healthy = createMemo(() => serverHealth() === true && !mcpIssue())
|
|
||||||
const state = createMemo<StatusPopoverState>(() => ({
|
|
||||||
shown: shown(),
|
|
||||||
ready: ready(),
|
|
||||||
healthy: healthy(),
|
|
||||||
serverHealth: serverHealth(),
|
|
||||||
issue: mcpIssue(),
|
|
||||||
label: language.t("status.popover.trigger"),
|
|
||||||
onOpenChange: setShown,
|
|
||||||
body: () => (
|
|
||||||
<StatusPopoverBody shown={shown()}>
|
|
||||||
<Body shown={shown} />
|
|
||||||
</StatusPopoverBody>
|
|
||||||
),
|
|
||||||
}))
|
|
||||||
|
|
||||||
return <StatusPopoverView state={state()} />
|
|
||||||
}
|
|
||||||
|
|
||||||
function ServerStatusPopover() {
|
|
||||||
const language = useLanguage()
|
|
||||||
const server = useServer()
|
|
||||||
const servers = useServers()
|
|
||||||
const [shown, setShown] = createSignal(false)
|
|
||||||
const serverHealth = () => servers.health[server.key]?.healthy
|
|
||||||
const state = createMemo<StatusPopoverState>(() => ({
|
|
||||||
shown: shown(),
|
|
||||||
ready: serverHealth() !== undefined,
|
|
||||||
healthy: serverHealth() === true,
|
|
||||||
serverHealth: serverHealth(),
|
|
||||||
label: language.t("status.popover.trigger"),
|
|
||||||
onOpenChange: setShown,
|
|
||||||
body: () => (
|
|
||||||
<StatusPopoverBody shown={shown()}>
|
|
||||||
<ServerBody />
|
|
||||||
</StatusPopoverBody>
|
|
||||||
),
|
|
||||||
}))
|
|
||||||
|
|
||||||
return <StatusPopoverView state={state()} />
|
|
||||||
}
|
|
||||||
|
|
||||||
type StatusPopoverState = {
|
|
||||||
shown: boolean
|
|
||||||
ready: boolean
|
|
||||||
healthy: boolean
|
|
||||||
serverHealth: boolean | undefined
|
|
||||||
issue?: "critical" | "warning"
|
|
||||||
label: string
|
|
||||||
onOpenChange: (value: boolean) => void
|
|
||||||
body: () => JSX.Element
|
|
||||||
}
|
|
||||||
|
|
||||||
function StatusPopoverBody(props: { shown: boolean; children: JSX.Element }) {
|
|
||||||
return (
|
|
||||||
<Show when={props.shown}>
|
|
||||||
<Suspense
|
|
||||||
fallback={<div class="w-[360px] h-14 rounded-xl bg-background-strong shadow-[var(--shadow-lg-border-base)]" />}
|
|
||||||
>
|
|
||||||
{props.children}
|
|
||||||
</Suspense>
|
|
||||||
</Show>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function StatusPopoverView(props: { state: StatusPopoverState }) {
|
|
||||||
const statusDotClass = () => ({
|
|
||||||
"absolute rounded-full": true,
|
|
||||||
"bg-icon-success-base": props.state.ready && props.state.healthy,
|
|
||||||
"bg-icon-warning-base": props.state.ready && props.state.serverHealth === true && props.state.issue === "warning",
|
|
||||||
"bg-icon-critical-base":
|
|
||||||
props.state.serverHealth === false ||
|
|
||||||
(props.state.ready && props.state.serverHealth === true && props.state.issue === "critical"),
|
|
||||||
"bg-border-weak-base": props.state.serverHealth === undefined || !props.state.ready,
|
|
||||||
})
|
|
||||||
|
|
||||||
const popoverProps = {
|
|
||||||
class:
|
|
||||||
"[&_[data-slot=popover-body]]:p-0 w-[360px] max-w-[calc(100vw-40px)] bg-transparent border-0 shadow-none rounded-xl",
|
|
||||||
gutter: 4,
|
|
||||||
placement: "bottom-end" as const,
|
|
||||||
shift: -168,
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Popover
|
|
||||||
open={props.state.shown}
|
|
||||||
onOpenChange={props.state.onOpenChange}
|
|
||||||
triggerAs={IconButtonV2}
|
|
||||||
triggerProps={{
|
|
||||||
variant: "ghost-muted",
|
|
||||||
size: "large",
|
|
||||||
class: "!w-9 shrink-0",
|
|
||||||
state: props.state.shown ? "pressed" : undefined,
|
|
||||||
"aria-label": props.state.label,
|
|
||||||
}}
|
|
||||||
trigger={
|
|
||||||
<div class="relative size-4">
|
|
||||||
<IconV2 name={props.state.shown ? "status-active" : "status"} />
|
|
||||||
<div
|
|
||||||
classList={statusDotClass()}
|
|
||||||
class="-top-1 -right-1 size-2 border border-[var(--v2-background-bg-deep)]"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
{...popoverProps}
|
|
||||||
>
|
|
||||||
{props.state.body()}
|
|
||||||
</Popover>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,28 +0,0 @@
|
|||||||
import { describe, expect, test } from "bun:test"
|
|
||||||
import { readSessionTabsRemovedDetail, SESSION_TABS_REMOVED_EVENT } from "./titlebar-session-events"
|
|
||||||
|
|
||||||
describe("titlebar session events", () => {
|
|
||||||
test("reads valid removed session tab details", () => {
|
|
||||||
expect(
|
|
||||||
readSessionTabsRemovedDetail(
|
|
||||||
new CustomEvent(SESSION_TABS_REMOVED_EVENT, {
|
|
||||||
detail: { directory: "/tmp/project", sessionIDs: ["ses_1", "ses_2", 1] },
|
|
||||||
}),
|
|
||||||
),
|
|
||||||
).toEqual({
|
|
||||||
directory: "/tmp/project",
|
|
||||||
sessionIDs: ["ses_1", "ses_2"],
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
test("ignores invalid removed session tab details", () => {
|
|
||||||
expect(readSessionTabsRemovedDetail(new Event(SESSION_TABS_REMOVED_EVENT))).toBeUndefined()
|
|
||||||
expect(
|
|
||||||
readSessionTabsRemovedDetail(
|
|
||||||
new CustomEvent(SESSION_TABS_REMOVED_EVENT, {
|
|
||||||
detail: { directory: "/tmp/project", sessionIDs: [] },
|
|
||||||
}),
|
|
||||||
),
|
|
||||||
).toBeUndefined()
|
|
||||||
})
|
|
||||||
})
|
|
||||||
@@ -1,29 +0,0 @@
|
|||||||
export const SESSION_TABS_REMOVED_EVENT = "opencode:session-tabs-removed"
|
|
||||||
|
|
||||||
export type SessionTabsRemovedDetail = {
|
|
||||||
directory: string
|
|
||||||
sessionIDs: string[]
|
|
||||||
}
|
|
||||||
|
|
||||||
export function notifySessionTabsRemoved(input: SessionTabsRemovedDetail) {
|
|
||||||
window.dispatchEvent(new CustomEvent(SESSION_TABS_REMOVED_EVENT, { detail: input }))
|
|
||||||
}
|
|
||||||
|
|
||||||
export function readSessionTabsRemovedDetail(event: Event): SessionTabsRemovedDetail | undefined {
|
|
||||||
if (!(event instanceof CustomEvent)) return undefined
|
|
||||||
|
|
||||||
const detail: unknown = event.detail
|
|
||||||
if (!detail || typeof detail !== "object") return undefined
|
|
||||||
if (!("directory" in detail)) return undefined
|
|
||||||
if (!("sessionIDs" in detail)) return undefined
|
|
||||||
if (typeof detail.directory !== "string") return undefined
|
|
||||||
if (!Array.isArray(detail.sessionIDs)) return undefined
|
|
||||||
|
|
||||||
const sessionIDs = detail.sessionIDs.filter((id): id is string => typeof id === "string")
|
|
||||||
if (sessionIDs.length === 0) return undefined
|
|
||||||
|
|
||||||
return {
|
|
||||||
directory: detail.directory,
|
|
||||||
sessionIDs,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -23,12 +23,8 @@ import { base64Encode } from "@opencode-ai/core/util/encode"
|
|||||||
import { Avatar as AvatarV2 } from "@opencode-ai/ui/v2/components/avatar-v2.jsx"
|
import { Avatar as AvatarV2 } from "@opencode-ai/ui/v2/components/avatar-v2.jsx"
|
||||||
import { displayName, getProjectAvatarSource, projectForSession } from "@/pages/layout/helpers"
|
import { displayName, getProjectAvatarSource, projectForSession } from "@/pages/layout/helpers"
|
||||||
import { makeEventListener } from "@solid-primitives/event-listener"
|
import { makeEventListener } from "@solid-primitives/event-listener"
|
||||||
import { StatusPopoverV2 } from "@/components/status-popover"
|
import { StatusPopover } from "./status-popover"
|
||||||
import {
|
import { SDKProvider } from "@/context/sdk"
|
||||||
readSessionTabsRemovedDetail,
|
|
||||||
SESSION_TABS_REMOVED_EVENT,
|
|
||||||
type SessionTabsRemovedDetail,
|
|
||||||
} from "@/components/titlebar-session-events"
|
|
||||||
|
|
||||||
type TauriDesktopWindow = {
|
type TauriDesktopWindow = {
|
||||||
startDragging?: () => Promise<void>
|
startDragging?: () => Promise<void>
|
||||||
@@ -55,6 +51,7 @@ const legacyTitlebarHeight = 40
|
|||||||
const v2TitlebarHeight = 44
|
const v2TitlebarHeight = 44
|
||||||
const minTitlebarZoom = 0.25
|
const minTitlebarZoom = 0.25
|
||||||
const windowsControlsBaseWidth = 138 // 3 native Windows caption buttons at 46px each.
|
const windowsControlsBaseWidth = 138 // 3 native Windows caption buttons at 46px each.
|
||||||
|
const USE_V2_TITLEBAR = import.meta.env.VITE_OPENCODE_CHANNEL !== "prod"
|
||||||
|
|
||||||
const makeSessionHref = (b64Dir: string, sessionId: string) => `/${b64Dir}/session/${sessionId}`
|
const makeSessionHref = (b64Dir: string, sessionId: string) => `/${b64Dir}/session/${sessionId}`
|
||||||
|
|
||||||
@@ -74,7 +71,6 @@ export function Titlebar(props: { update?: TitlebarUpdate }) {
|
|||||||
const navigate = useNavigate()
|
const navigate = useNavigate()
|
||||||
const location = useLocation()
|
const location = useLocation()
|
||||||
const params = useParams()
|
const params = useParams()
|
||||||
const useV2Titlebar = createMemo(() => settings.general.newLayoutDesigns())
|
|
||||||
|
|
||||||
const mac = createMemo(() => platform.platform === "desktop" && platform.os === "macos")
|
const mac = createMemo(() => platform.platform === "desktop" && platform.os === "macos")
|
||||||
const windows = createMemo(() => platform.platform === "desktop" && platform.os === "windows")
|
const windows = createMemo(() => platform.platform === "desktop" && platform.os === "windows")
|
||||||
@@ -85,7 +81,7 @@ export function Titlebar(props: { update?: TitlebarUpdate }) {
|
|||||||
const titlebarZoom = () => (windows() ? Math.max(zoom(), minTitlebarZoom) : zoom())
|
const titlebarZoom = () => (windows() ? Math.max(zoom(), minTitlebarZoom) : zoom())
|
||||||
const counterZoom = () => (windows() && titlebarZoom() < 1 ? 1 / titlebarZoom() : 1)
|
const counterZoom = () => (windows() && titlebarZoom() < 1 ? 1 / titlebarZoom() : 1)
|
||||||
const minHeight = () => {
|
const minHeight = () => {
|
||||||
const height = useV2Titlebar() ? v2TitlebarHeight : legacyTitlebarHeight
|
const height = USE_V2_TITLEBAR ? v2TitlebarHeight : legacyTitlebarHeight
|
||||||
if (mac()) return `${height / zoom()}px`
|
if (mac()) return `${height / zoom()}px`
|
||||||
if (windows()) return `${height / Math.min(titlebarZoom(), 1)}px`
|
if (windows()) return `${height / Math.min(titlebarZoom(), 1)}px`
|
||||||
return undefined
|
return undefined
|
||||||
@@ -119,23 +115,7 @@ export function Titlebar(props: { update?: TitlebarUpdate }) {
|
|||||||
const canBack = createMemo(() => history.index > 0)
|
const canBack = createMemo(() => history.index > 0)
|
||||||
const canForward = createMemo(() => history.index < history.stack.length - 1)
|
const canForward = createMemo(() => history.index < history.stack.length - 1)
|
||||||
const hasProjects = createMemo(() => layout.projects.list().length > 0)
|
const hasProjects = createMemo(() => layout.projects.list().length > 0)
|
||||||
const nav = createMemo(() => (useV2Titlebar() ? settings.general.showNavigation() : true))
|
const nav = createMemo(() => import.meta.env.VITE_OPENCODE_CHANNEL !== "beta" || settings.general.showNavigation())
|
||||||
const updateState = createMemo<TitlebarUpdatePillState>(() => {
|
|
||||||
const version = props.update?.version()
|
|
||||||
return {
|
|
||||||
visible: version !== undefined,
|
|
||||||
installing: props.update?.installing() ?? false,
|
|
||||||
label: "Update",
|
|
||||||
ariaLabel: language.t("toast.update.action.installRestart"),
|
|
||||||
title: version ? `Update ${version}` : undefined,
|
|
||||||
onInstall: () => props.update?.install(),
|
|
||||||
}
|
|
||||||
})
|
|
||||||
const v2RightState = createMemo<TitlebarV2RightState>(() => ({
|
|
||||||
update: updateState(),
|
|
||||||
statusVisible: !params.dir && settings.general.showStatus(),
|
|
||||||
statusLabel: language.t("status.popover.trigger"),
|
|
||||||
}))
|
|
||||||
|
|
||||||
const back = () => {
|
const back = () => {
|
||||||
const next = backPath(history)
|
const next = backPath(history)
|
||||||
@@ -222,8 +202,8 @@ export function Titlebar(props: { update?: TitlebarUpdate }) {
|
|||||||
<header
|
<header
|
||||||
classList={{
|
classList={{
|
||||||
"shrink-0 relative overflow-hidden flex flex-row": true,
|
"shrink-0 relative overflow-hidden flex flex-row": true,
|
||||||
"h-11 bg-v2-background-bg-deep": useV2Titlebar(),
|
"h-11 bg-v2-background-bg-deep": USE_V2_TITLEBAR,
|
||||||
"h-10 bg-background-base": !useV2Titlebar(),
|
"h-10 bg-background-base": !USE_V2_TITLEBAR,
|
||||||
}}
|
}}
|
||||||
style={{
|
style={{
|
||||||
"min-height": minHeight(),
|
"min-height": minHeight(),
|
||||||
@@ -239,7 +219,7 @@ export function Titlebar(props: { update?: TitlebarUpdate }) {
|
|||||||
onDblClick={maximize}
|
onDblClick={maximize}
|
||||||
>
|
>
|
||||||
<Switch>
|
<Switch>
|
||||||
<Match when={useV2Titlebar()}>
|
<Match when={USE_V2_TITLEBAR}>
|
||||||
{(_) => {
|
{(_) => {
|
||||||
const serverSync = useServerSync()
|
const serverSync = useServerSync()
|
||||||
const navigate = useNavigate()
|
const navigate = useNavigate()
|
||||||
@@ -281,7 +261,7 @@ export function Titlebar(props: { update?: TitlebarUpdate }) {
|
|||||||
)
|
)
|
||||||
},
|
},
|
||||||
removeTab: (href: string) => {
|
removeTab: (href: string) => {
|
||||||
void startTransition(() => {
|
startTransition(() => {
|
||||||
setStore(
|
setStore(
|
||||||
produce((tabs) => {
|
produce((tabs) => {
|
||||||
const index = tabs.findIndex((t) => t.href === href)
|
const index = tabs.findIndex((t) => t.href === href)
|
||||||
@@ -294,45 +274,11 @@ export function Titlebar(props: { update?: TitlebarUpdate }) {
|
|||||||
)
|
)
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
removeSessions: (input: SessionTabsRemovedDetail) => {
|
|
||||||
void startTransition(() => {
|
|
||||||
setStore(
|
|
||||||
produce((tabs) => {
|
|
||||||
const sessionIDs = new Set(input.sessionIDs)
|
|
||||||
const currentHref = params.dir && params.id ? makeSessionHref(params.dir, params.id) : undefined
|
|
||||||
const currentIndex = currentHref ? tabs.findIndex((tab) => tab.href === currentHref) : -1
|
|
||||||
const removedCurrent =
|
|
||||||
currentIndex !== -1 &&
|
|
||||||
tabs[currentIndex]?.dir === input.directory &&
|
|
||||||
sessionIDs.has(tabs[currentIndex]?.sessionId ?? "")
|
|
||||||
|
|
||||||
for (let i = tabs.length - 1; i >= 0; i--) {
|
|
||||||
const tab = tabs[i]
|
|
||||||
if (!tab) continue
|
|
||||||
if (tab.dir !== input.directory) continue
|
|
||||||
if (!sessionIDs.has(tab.sessionId)) continue
|
|
||||||
tabs.splice(i, 1)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!removedCurrent) return
|
|
||||||
const nextTab = tabs[currentIndex] ?? tabs[tabs.length - 1]
|
|
||||||
if (nextTab) navigate(nextTab.href)
|
|
||||||
else navigate("/")
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
})
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return [store, actions]
|
return [store, actions]
|
||||||
})
|
})
|
||||||
|
|
||||||
makeEventListener(window, SESSION_TABS_REMOVED_EVENT, (event) => {
|
|
||||||
const detail = readSessionTabsRemovedDetail(event)
|
|
||||||
if (!detail) return
|
|
||||||
tabsStoreActions.removeSessions(detail)
|
|
||||||
})
|
|
||||||
|
|
||||||
createEffect(() => {
|
createEffect(() => {
|
||||||
const params = useParams()
|
const params = useParams()
|
||||||
if (!(params.dir && params.id)) return
|
if (!(params.dir && params.id)) return
|
||||||
@@ -490,6 +436,7 @@ export function Titlebar(props: { update?: TitlebarUpdate }) {
|
|||||||
project={projectForSession(tab.info, projects(), projectByID())}
|
project={projectForSession(tab.info, projects(), projectByID())}
|
||||||
directory={tab.dir}
|
directory={tab.dir}
|
||||||
onClose={() => tabsStoreActions.removeTab(tab.href)}
|
onClose={() => tabsStoreActions.removeTab(tab.href)}
|
||||||
|
hideClose={tabsEnriched().length < 2}
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
@@ -518,7 +465,16 @@ export function Titlebar(props: { update?: TitlebarUpdate }) {
|
|||||||
</Show>
|
</Show>
|
||||||
<div class="min-w-0 flex-1" />
|
<div class="min-w-0 flex-1" />
|
||||||
</div>
|
</div>
|
||||||
<TitlebarV2Right state={v2RightState()} />
|
<Show when={currentSessionTab()?.dir} keyed>
|
||||||
|
{(dir) => (
|
||||||
|
<SDKProvider directory={dir}>
|
||||||
|
<Tooltip placement="bottom" value={language.t("status.popover.trigger")}>
|
||||||
|
<StatusPopover />
|
||||||
|
</Tooltip>
|
||||||
|
</SDKProvider>
|
||||||
|
)}
|
||||||
|
</Show>
|
||||||
|
<TitlebarUpdatePill update={props.update} />
|
||||||
<Show when={windows() && !electronWindows()}>
|
<Show when={windows() && !electronWindows()}>
|
||||||
<div data-tauri-decorum-tb class="flex flex-row" />
|
<div data-tauri-decorum-tb class="flex flex-row" />
|
||||||
</Show>
|
</Show>
|
||||||
@@ -685,75 +641,54 @@ export function Titlebar(props: { update?: TitlebarUpdate }) {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
type TitlebarUpdatePillState = {
|
function TitlebarUpdatePill(props: { update?: TitlebarUpdate }) {
|
||||||
visible: boolean
|
const language = useLanguage()
|
||||||
installing: boolean
|
const version = () => props.update?.version()
|
||||||
label: string
|
|
||||||
ariaLabel: string
|
|
||||||
title?: string
|
|
||||||
onInstall: () => void
|
|
||||||
}
|
|
||||||
|
|
||||||
type TitlebarV2RightState = {
|
|
||||||
update: TitlebarUpdatePillState
|
|
||||||
statusVisible: boolean
|
|
||||||
statusLabel: string
|
|
||||||
}
|
|
||||||
|
|
||||||
function TitlebarV2Right(props: { state: TitlebarV2RightState }) {
|
|
||||||
return (
|
return (
|
||||||
<div class="flex shrink-0 items-center justify-end gap-0">
|
<Show when={version() !== undefined}>
|
||||||
<TitlebarUpdatePill state={props.state.update} />
|
|
||||||
<Show when={props.state.statusVisible}>
|
|
||||||
<Tooltip placement="bottom" value={props.state.statusLabel}>
|
|
||||||
<StatusPopoverV2 scope="server" />
|
|
||||||
</Tooltip>
|
|
||||||
</Show>
|
|
||||||
<div id="opencode-titlebar-right" class="flex shrink-0 items-center justify-end gap-0" />
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function TitlebarUpdatePill(props: { state: TitlebarUpdatePillState }) {
|
|
||||||
return (
|
|
||||||
<Show when={props.state.visible}>
|
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
class="h-5 shrink-0 rounded-[27px] bg-[var(--v2-background-bg-layer-03)] px-2.5 text-[11px] font-[530] leading-4 tracking-[0.05px] text-[var(--v2-text-text-base)] disabled:opacity-60"
|
class="h-5 shrink-0 rounded-[27px] bg-[var(--v2-background-bg-accent)] px-2.5 text-[11px] font-[530] leading-[1.1] tracking-[-0.04px] text-[var(--v2-text-text-contrast)] disabled:opacity-60"
|
||||||
onClick={props.state.onInstall}
|
onClick={() => props.update?.install()}
|
||||||
disabled={props.state.installing}
|
disabled={props.update?.installing()}
|
||||||
aria-label={props.state.ariaLabel}
|
aria-label={language.t("toast.update.action.installRestart")}
|
||||||
title={props.state.title}
|
title={version() ? `Update ${version()}` : undefined}
|
||||||
>
|
>
|
||||||
{props.state.label}
|
Update
|
||||||
</button>
|
</button>
|
||||||
</Show>
|
</Show>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function DesktopTitlebarIconButton(props: Parameters<typeof IconButtonV2>[0]) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
function TabNavItem(props: {
|
function TabNavItem(props: {
|
||||||
href: string
|
href: string
|
||||||
title: string
|
title: string
|
||||||
project?: LocalProject
|
project?: LocalProject
|
||||||
directory: string
|
directory: string
|
||||||
|
hideClose?: boolean
|
||||||
onClose: () => void
|
onClose: () => void
|
||||||
}) {
|
}) {
|
||||||
const match = useMatch(() => props.href)
|
const match = useMatch(() => props.href)
|
||||||
const isActive = () => !!match()
|
const isActive = () => !!match()
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
class="group relative flex h-7 min-w-24 max-w-60 flex-row items-center gap-1.5 overflow-hidden whitespace-nowrap rounded-[6px] bg-[var(--tab-bg)] px-1.5 [--tab-bg:var(--v2-background-bg-deep)] hover:[--tab-bg:var(--v2-background-bg-layer-02)] data-[active='true']:[--tab-bg:var(--v2-background-bg-layer-02)]"
|
class="group relative flex h-7 min-w-24 max-w-60 flex-row items-center gap-1.5 overflow-hidden whitespace-nowrap rounded-[6px] bg-[var(--tab-bg)] pl-1.5 [--tab-bg:var(--v2-background-bg-deep)] hover:[--tab-bg:var(--v2-background-bg-layer-02)] data-[active='true']:[--tab-bg:var(--v2-background-bg-layer-02)]"
|
||||||
data-active={isActive()}
|
data-active={isActive()}
|
||||||
>
|
>
|
||||||
<a
|
<a
|
||||||
href={props.href}
|
href={props.href}
|
||||||
class="flex h-full min-w-0 flex-1 flex-row items-center gap-1.5 overflow-hidden text-[13px] font-medium leading-5 text-v2-text-text-faint group-data-[active='true']:text-v2-text-text-base"
|
class="flex h-full min-w-0 flex-1 flex-row items-center gap-1.5 overflow-hidden text-[13px] font-medium text-v2-text-text-faint group-data-[active='true']:text-v2-text-text-base"
|
||||||
>
|
>
|
||||||
<ProjectTabAvatar project={props.project} directory={props.directory} />
|
<ProjectTabAvatar project={props.project} directory={props.directory} />
|
||||||
<span class="text-clip leading-5">{props.title}</span>
|
<span class="text-clip">{props.title}</span>
|
||||||
</a>
|
</a>
|
||||||
|
|
||||||
<div class="absolute not-group-hover:not-group-data-[active=true]:left-52 group-hover:right-0 group-data-[active=true]:right-0 inset-y-0 flex flex-row items-center pr-1 py-1 w-8 pl-2">
|
<div class="absolute right-0 inset-y-0 flex flex-row items-center pr-1 py-1 w-8 pl-2">
|
||||||
<div
|
<div
|
||||||
class="absolute inset-0 bg-(image:--inactive-bg) group-hover:bg-(image:--active-bg) group-data-[active=true]:bg-(image:--active-bg)"
|
class="absolute inset-0 bg-(image:--inactive-bg) group-hover:bg-(image:--active-bg) group-data-[active=true]:bg-(image:--active-bg)"
|
||||||
style={{
|
style={{
|
||||||
@@ -764,7 +699,7 @@ function TabNavItem(props: {
|
|||||||
<IconButtonV2
|
<IconButtonV2
|
||||||
size="small"
|
size="small"
|
||||||
variant="ghost-muted"
|
variant="ghost-muted"
|
||||||
class="opacity-0 group-hover:opacity-100 group-data-[active='true']:opacity-100 z-10"
|
class="opacity-0 group-hover:opacity-100 group-data-[active='true']:opacity-100"
|
||||||
onClick={props.onClose}
|
onClick={props.onClose}
|
||||||
icon={<IconV2 name="xmark-small" />}
|
icon={<IconV2 name="xmark-small" />}
|
||||||
/>
|
/>
|
||||||
@@ -792,12 +727,12 @@ function NewSessionTabItem(props: { href: string; title: string; onClose: () =>
|
|||||||
<a
|
<a
|
||||||
href={props.href}
|
href={props.href}
|
||||||
aria-current="page"
|
aria-current="page"
|
||||||
class="flex h-full min-w-0 flex-1 flex-row items-center gap-1.5 overflow-hidden text-[13px] font-medium leading-5 text-[var(--v2-text-text-base)]"
|
class="flex h-full min-w-0 flex-1 flex-row items-center gap-1.5 overflow-hidden text-[13px] font-medium leading-none text-[var(--v2-text-text-base)]"
|
||||||
>
|
>
|
||||||
<span class="flex size-4 shrink-0 rotate-90 items-center justify-center">
|
<span class="flex size-4 shrink-0 rotate-90 items-center justify-center">
|
||||||
<IconV2 name="edit" />
|
<IconV2 name="edit" />
|
||||||
</span>
|
</span>
|
||||||
<span class="truncate leading-5">{props.title}</span>
|
<span class="truncate">{props.title}</span>
|
||||||
</a>
|
</a>
|
||||||
<div class="absolute right-0 inset-y-0 flex w-7 items-center justify-center">
|
<div class="absolute right-0 inset-y-0 flex w-7 items-center justify-center">
|
||||||
<IconButtonV2
|
<IconButtonV2
|
||||||
|
|||||||
@@ -8,8 +8,8 @@ import {
|
|||||||
getSessionPrefetchPromise,
|
getSessionPrefetchPromise,
|
||||||
setSessionPrefetch,
|
setSessionPrefetch,
|
||||||
} from "./global-sync/session-prefetch"
|
} from "./global-sync/session-prefetch"
|
||||||
import { createServerSyncContext } from "./server-sync"
|
import { createServerSyncContext, useServerSync } from "./server-sync"
|
||||||
import type { Message, Part } from "@opencode-ai/sdk/v2/client"
|
import type { Message, OpencodeClient, Part } from "@opencode-ai/sdk/v2/client"
|
||||||
import { SESSION_CACHE_LIMIT, dropSessionCaches, pickSessionCacheEvictions } from "./global-sync/session-cache"
|
import { SESSION_CACHE_LIMIT, dropSessionCaches, pickSessionCacheEvictions } from "./global-sync/session-cache"
|
||||||
import { diffs as list, message as clean } from "@/utils/diffs"
|
import { diffs as list, message as clean } from "@/utils/diffs"
|
||||||
import { useServerSDK } from "./server-sdk"
|
import { useServerSDK } from "./server-sdk"
|
||||||
@@ -34,12 +34,6 @@ const keyFor = (directory: string, id: string) => `${directory}\n${id}`
|
|||||||
|
|
||||||
const cmp = (a: string, b: string) => (a < b ? -1 : a > b ? 1 : 0)
|
const cmp = (a: string, b: string) => (a < b ? -1 : a > b ? 1 : 0)
|
||||||
|
|
||||||
const isNotFound = (error: unknown) =>
|
|
||||||
error instanceof Error &&
|
|
||||||
typeof error.cause === "object" &&
|
|
||||||
error.cause !== null &&
|
|
||||||
(error.cause as { status?: unknown }).status === 404
|
|
||||||
|
|
||||||
function merge<T extends { id: string }>(a: readonly T[], b: readonly T[]) {
|
function merge<T extends { id: string }>(a: readonly T[], b: readonly T[]) {
|
||||||
const map = new Map(a.map((item) => [item.id, item] as const))
|
const map = new Map(a.map((item) => [item.id, item] as const))
|
||||||
for (const item of b) map.set(item.id, item)
|
for (const item of b) map.set(item.id, item)
|
||||||
@@ -353,10 +347,6 @@ export const createDirSyncContext = (directory: string, serverSync: ReturnType<t
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
.catch((error) => {
|
|
||||||
if (isNotFound(error) && !tracked(input.directory, input.sessionID)) return
|
|
||||||
throw error
|
|
||||||
})
|
|
||||||
.finally(() => {
|
.finally(() => {
|
||||||
setMeta(
|
setMeta(
|
||||||
produce((draft) => {
|
produce((draft) => {
|
||||||
@@ -468,8 +458,7 @@ export const createDirSyncContext = (directory: string, serverSync: ReturnType<t
|
|||||||
const sessionReq =
|
const sessionReq =
|
||||||
hasSession && !opts?.force
|
hasSession && !opts?.force
|
||||||
? Promise.resolve()
|
? Promise.resolve()
|
||||||
: retry(() => client.session.get({ sessionID }))
|
: retry(() => client.session.get({ sessionID })).then((session) => {
|
||||||
.then((session) => {
|
|
||||||
if (!tracked(directory, sessionID)) return
|
if (!tracked(directory, sessionID)) return
|
||||||
const data = session.data
|
const data = session.data
|
||||||
if (!data) return
|
if (!data) return
|
||||||
@@ -485,10 +474,6 @@ export const createDirSyncContext = (directory: string, serverSync: ReturnType<t
|
|||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
.catch((error) => {
|
|
||||||
if (isNotFound(error) && !tracked(directory, sessionID)) return
|
|
||||||
throw error
|
|
||||||
})
|
|
||||||
|
|
||||||
const messagesReq =
|
const messagesReq =
|
||||||
cached && !opts?.force
|
cached && !opts?.force
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
import { createSimpleContext } from "@opencode-ai/ui/context"
|
import { createSimpleContext } from "@opencode-ai/ui/context"
|
||||||
import { type Accessor, batch, createMemo } from "solid-js"
|
import { type Accessor, batch, createEffect, createMemo, onCleanup } from "solid-js"
|
||||||
import { createStore } from "solid-js/store"
|
import { createStore } from "solid-js/store"
|
||||||
import { Persist, persisted } from "@/utils/persist"
|
import { Persist, persisted } from "@/utils/persist"
|
||||||
|
import { useCheckServerHealth } from "@/utils/server-health"
|
||||||
|
|
||||||
type StoredProject = { worktree: string; expanded: boolean }
|
type StoredProject = { worktree: string; expanded: boolean }
|
||||||
type StoredServer = string | ServerConnection.HttpBase | ServerConnection.Http
|
type StoredServer = string | ServerConnection.HttpBase | ServerConnection.Http
|
||||||
@@ -36,30 +37,24 @@ export function resolveServerList(input: {
|
|||||||
props?: Array<ServerConnection.Any>
|
props?: Array<ServerConnection.Any>
|
||||||
stored: StoredServer[]
|
stored: StoredServer[]
|
||||||
}): Array<ServerConnection.Any> {
|
}): Array<ServerConnection.Any> {
|
||||||
const deduped = new Map<ServerConnection.Key, ServerConnection.Any>(
|
const servers = [
|
||||||
input.props?.map((v) => [ServerConnection.key(v), v]) ?? [],
|
...input.stored.map((value) =>
|
||||||
)
|
|
||||||
|
|
||||||
for (const value of input.stored) {
|
|
||||||
const conn: ServerConnection.Http =
|
|
||||||
typeof value === "string"
|
typeof value === "string"
|
||||||
? {
|
? {
|
||||||
type: "http" as const,
|
type: "http" as const,
|
||||||
http: { url: value },
|
http: { url: value },
|
||||||
}
|
}
|
||||||
: "http" in value
|
: value,
|
||||||
? value
|
),
|
||||||
: { type: "http", http: value }
|
...(input.props ?? []),
|
||||||
const key = ServerConnection.key(conn)
|
]
|
||||||
|
|
||||||
const existing = deduped.get(key)
|
const deduped = new Map<ServerConnection.Key, ServerConnection.Any>()
|
||||||
if (existing)
|
for (const value of servers) {
|
||||||
deduped.set(key, {
|
const conn: ServerConnection.Any = "type" in value ? value : { type: "http", http: value }
|
||||||
...existing,
|
const key = ServerConnection.key(conn)
|
||||||
...conn,
|
if (deduped.has(key) && conn.type === "http" && !conn.authToken) continue
|
||||||
http: { ...existing.http, ...conn.http },
|
deduped.set(key, conn)
|
||||||
})
|
|
||||||
else deduped.set(key, conn)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return [...deduped.values()]
|
return [...deduped.values()]
|
||||||
@@ -127,7 +122,13 @@ export namespace ServerConnection {
|
|||||||
|
|
||||||
export const { use: useServer, provider: ServerProvider } = createSimpleContext({
|
export const { use: useServer, provider: ServerProvider } = createSimpleContext({
|
||||||
name: "Server",
|
name: "Server",
|
||||||
init: (props: { defaultServer: ServerConnection.Key; servers?: Array<ServerConnection.Any> }) => {
|
init: (props: {
|
||||||
|
defaultServer: ServerConnection.Key
|
||||||
|
disableHealthCheck?: boolean
|
||||||
|
servers?: Array<ServerConnection.Any>
|
||||||
|
}) => {
|
||||||
|
const checkServerHealth = useCheckServerHealth()
|
||||||
|
|
||||||
const [store, setStore, _, ready] = persisted(
|
const [store, setStore, _, ready] = persisted(
|
||||||
Persist.global("server", ["server.v3"]),
|
Persist.global("server", ["server.v3"]),
|
||||||
createStore({
|
createStore({
|
||||||
@@ -145,8 +146,36 @@ export const { use: useServer, provider: ServerProvider } = createSimpleContext(
|
|||||||
|
|
||||||
const [state, setState] = createStore({
|
const [state, setState] = createStore({
|
||||||
active: props.defaultServer,
|
active: props.defaultServer,
|
||||||
|
healthy: undefined as boolean | undefined,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const healthy = () => state.healthy
|
||||||
|
|
||||||
|
function startHealthPolling(conn: ServerConnection.Any) {
|
||||||
|
let alive = true
|
||||||
|
let busy = false
|
||||||
|
|
||||||
|
const run = () => {
|
||||||
|
if (busy) return
|
||||||
|
busy = true
|
||||||
|
void check(conn)
|
||||||
|
.then((next) => {
|
||||||
|
if (!alive) return
|
||||||
|
setState("healthy", next)
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
busy = false
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
run()
|
||||||
|
const interval = setInterval(run, HEALTH_POLL_INTERVAL_MS)
|
||||||
|
return () => {
|
||||||
|
alive = false
|
||||||
|
clearInterval(interval)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function setActive(input: ServerConnection.Key) {
|
function setActive(input: ServerConnection.Key) {
|
||||||
if (state.active !== input) setState("active", input)
|
if (state.active !== input) setState("active", input)
|
||||||
}
|
}
|
||||||
@@ -180,6 +209,20 @@ export const { use: useServer, provider: ServerProvider } = createSimpleContext(
|
|||||||
|
|
||||||
const isReady = createMemo(() => ready() && !!state.active)
|
const isReady = createMemo(() => ready() && !!state.active)
|
||||||
|
|
||||||
|
const check = (conn: ServerConnection.Any) => checkServerHealth(conn.http).then((x) => x.healthy)
|
||||||
|
|
||||||
|
createEffect(() => {
|
||||||
|
const current_ = current()
|
||||||
|
if (!current_) return
|
||||||
|
|
||||||
|
if (props.disableHealthCheck) {
|
||||||
|
setState("healthy", true)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
setState("healthy", undefined)
|
||||||
|
onCleanup(startHealthPolling(current_))
|
||||||
|
})
|
||||||
|
|
||||||
const origin = createMemo(() => projectsKey(state.active))
|
const origin = createMemo(() => projectsKey(state.active))
|
||||||
const projectsList = createMemo(() => store.projects[origin()] ?? [])
|
const projectsList = createMemo(() => store.projects[origin()] ?? [])
|
||||||
const current: Accessor<ServerConnection.Any | undefined> = createMemo(
|
const current: Accessor<ServerConnection.Any | undefined> = createMemo(
|
||||||
@@ -192,6 +235,7 @@ export const { use: useServer, provider: ServerProvider } = createSimpleContext(
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
ready: isReady,
|
ready: isReady,
|
||||||
|
healthy,
|
||||||
isLocal,
|
isLocal,
|
||||||
get key() {
|
get key() {
|
||||||
return state.active
|
return state.active
|
||||||
|
|||||||
@@ -1,20 +0,0 @@
|
|||||||
import { createSimpleContext } from "@opencode-ai/ui/context"
|
|
||||||
import { useServer } from "./server"
|
|
||||||
import { useServerHealth } from "@/utils/server-health"
|
|
||||||
|
|
||||||
export const { use: useServers, provider: ServersProvider } = createSimpleContext({
|
|
||||||
name: "Servers",
|
|
||||||
init: () => {
|
|
||||||
const server = useServer()
|
|
||||||
|
|
||||||
const health = useServerHealth(
|
|
||||||
() => server.list,
|
|
||||||
() => true,
|
|
||||||
)
|
|
||||||
|
|
||||||
return {
|
|
||||||
list: () => server.list,
|
|
||||||
health,
|
|
||||||
}
|
|
||||||
},
|
|
||||||
})
|
|
||||||
@@ -32,8 +32,6 @@ export interface Settings {
|
|||||||
shellToolPartsExpanded: boolean
|
shellToolPartsExpanded: boolean
|
||||||
editToolPartsExpanded: boolean
|
editToolPartsExpanded: boolean
|
||||||
showSessionProgressBar: boolean
|
showSessionProgressBar: boolean
|
||||||
showCustomAgents: boolean
|
|
||||||
newLayoutDesigns?: boolean
|
|
||||||
}
|
}
|
||||||
updates: {
|
updates: {
|
||||||
startup: boolean
|
startup: boolean
|
||||||
@@ -55,7 +53,6 @@ export interface Settings {
|
|||||||
export const monoDefault = "System Mono"
|
export const monoDefault = "System Mono"
|
||||||
export const sansDefault = "System Sans"
|
export const sansDefault = "System Sans"
|
||||||
export const terminalDefault = "JetBrainsMono Nerd Font Mono"
|
export const terminalDefault = "JetBrainsMono Nerd Font Mono"
|
||||||
export const newLayoutDesignsDefault = import.meta.env.VITE_OPENCODE_CHANNEL !== "prod"
|
|
||||||
|
|
||||||
const monoFallback =
|
const monoFallback =
|
||||||
'ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace'
|
'ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace'
|
||||||
@@ -120,7 +117,6 @@ const defaultSettings: Settings = {
|
|||||||
shellToolPartsExpanded: false,
|
shellToolPartsExpanded: false,
|
||||||
editToolPartsExpanded: false,
|
editToolPartsExpanded: false,
|
||||||
showSessionProgressBar: true,
|
showSessionProgressBar: true,
|
||||||
showCustomAgents: false,
|
|
||||||
},
|
},
|
||||||
updates: {
|
updates: {
|
||||||
startup: true,
|
startup: true,
|
||||||
@@ -240,14 +236,6 @@ export const { use: useSettings, provider: SettingsProvider } = createSimpleCont
|
|||||||
setShowSessionProgressBar(value: boolean) {
|
setShowSessionProgressBar(value: boolean) {
|
||||||
setStore("general", "showSessionProgressBar", value)
|
setStore("general", "showSessionProgressBar", value)
|
||||||
},
|
},
|
||||||
showCustomAgents: withFallback(() => store.general?.showCustomAgents, defaultSettings.general.showCustomAgents),
|
|
||||||
setShowCustomAgents(value: boolean) {
|
|
||||||
setStore("general", "showCustomAgents", value)
|
|
||||||
},
|
|
||||||
newLayoutDesigns: withFallback(() => store.general?.newLayoutDesigns, newLayoutDesignsDefault),
|
|
||||||
setNewLayoutDesigns(value: boolean) {
|
|
||||||
setStore("general", "newLayoutDesigns", value)
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
updates: {
|
updates: {
|
||||||
startup: withFallback(() => store.updates?.startup, defaultSettings.updates.startup),
|
startup: withFallback(() => store.updates?.startup, defaultSettings.updates.startup),
|
||||||
|
|||||||
@@ -221,7 +221,6 @@ export const dict = {
|
|||||||
"common.loading": "Loading",
|
"common.loading": "Loading",
|
||||||
"common.loading.ellipsis": "...",
|
"common.loading.ellipsis": "...",
|
||||||
"common.cancel": "Cancel",
|
"common.cancel": "Cancel",
|
||||||
"common.clear": "Clear",
|
|
||||||
"common.open": "Open",
|
"common.open": "Open",
|
||||||
"common.connect": "Connect",
|
"common.connect": "Connect",
|
||||||
"common.disconnect": "Disconnect",
|
"common.disconnect": "Disconnect",
|
||||||
@@ -532,7 +531,6 @@ export const dict = {
|
|||||||
"home.project.add": "Add project",
|
"home.project.add": "Add project",
|
||||||
"home.sessions.search.placeholder": "Search sessions",
|
"home.sessions.search.placeholder": "Search sessions",
|
||||||
"home.sessions.empty": "No sessions found",
|
"home.sessions.empty": "No sessions found",
|
||||||
"home.sessions.empty.description": "Start a new session for this project",
|
|
||||||
"home.sessions.group.today": "Today",
|
"home.sessions.group.today": "Today",
|
||||||
"home.sessions.group.yesterday": "Yesterday",
|
"home.sessions.group.yesterday": "Yesterday",
|
||||||
"home.sessions.group.older": "Older",
|
"home.sessions.group.older": "Older",
|
||||||
@@ -586,9 +584,6 @@ export const dict = {
|
|||||||
"session.revertDock.restore": "Restore message",
|
"session.revertDock.restore": "Restore message",
|
||||||
|
|
||||||
"session.new.title": "Build anything",
|
"session.new.title": "Build anything",
|
||||||
"session.new.project.new": "New project",
|
|
||||||
"session.new.project.search": "Search projects",
|
|
||||||
"session.new.project.add": "Add project",
|
|
||||||
"session.new.worktree.main": "Main branch",
|
"session.new.worktree.main": "Main branch",
|
||||||
"session.new.worktree.mainWithBranch": "Main branch ({{branch}})",
|
"session.new.worktree.mainWithBranch": "Main branch ({{branch}})",
|
||||||
"session.new.worktree.create": "Create new worktree",
|
"session.new.worktree.create": "Create new worktree",
|
||||||
@@ -769,7 +764,7 @@ export const dict = {
|
|||||||
"settings.general.row.followup.option.queue": "Queue",
|
"settings.general.row.followup.option.queue": "Queue",
|
||||||
"settings.general.row.followup.option.steer": "Steer",
|
"settings.general.row.followup.option.steer": "Steer",
|
||||||
"settings.general.row.showFileTree.title": "File tree",
|
"settings.general.row.showFileTree.title": "File tree",
|
||||||
"settings.general.row.showFileTree.description": "Show the file tree panel in desktop sessions",
|
"settings.general.row.showFileTree.description": "Show the file tree toggle and panel in desktop sessions",
|
||||||
"settings.general.row.showNavigation.title": "Navigation controls",
|
"settings.general.row.showNavigation.title": "Navigation controls",
|
||||||
"settings.general.row.showNavigation.description": "Show the back and forward buttons in the desktop title bar",
|
"settings.general.row.showNavigation.description": "Show the back and forward buttons in the desktop title bar",
|
||||||
"settings.general.row.showSearch.title": "Command palette",
|
"settings.general.row.showSearch.title": "Command palette",
|
||||||
@@ -778,8 +773,6 @@ export const dict = {
|
|||||||
"settings.general.row.showTerminal.description": "Show the terminal button in the desktop title bar",
|
"settings.general.row.showTerminal.description": "Show the terminal button in the desktop title bar",
|
||||||
"settings.general.row.showStatus.title": "Server status",
|
"settings.general.row.showStatus.title": "Server status",
|
||||||
"settings.general.row.showStatus.description": "Show the server status button in the desktop title bar",
|
"settings.general.row.showStatus.description": "Show the server status button in the desktop title bar",
|
||||||
"settings.general.row.showCustomAgents.title": "Custom agents",
|
|
||||||
"settings.general.row.showCustomAgents.description": "Show the agent picker in the v2 desktop composer",
|
|
||||||
"settings.general.row.reasoningSummaries.title": "Show reasoning summaries",
|
"settings.general.row.reasoningSummaries.title": "Show reasoning summaries",
|
||||||
"settings.general.row.reasoningSummaries.description": "Display model reasoning summaries in the timeline",
|
"settings.general.row.reasoningSummaries.description": "Display model reasoning summaries in the timeline",
|
||||||
"settings.general.row.shellToolPartsExpanded.title": "Expand shell tool parts",
|
"settings.general.row.shellToolPartsExpanded.title": "Expand shell tool parts",
|
||||||
@@ -791,8 +784,6 @@ export const dict = {
|
|||||||
"settings.general.row.showSessionProgressBar.title": "Show session progress bar",
|
"settings.general.row.showSessionProgressBar.title": "Show session progress bar",
|
||||||
"settings.general.row.showSessionProgressBar.description":
|
"settings.general.row.showSessionProgressBar.description":
|
||||||
"Display the animated progress bar at the top of the session when the agent is working",
|
"Display the animated progress bar at the top of the session when the agent is working",
|
||||||
"settings.general.row.newLayoutDesigns.title": "New layout and designs",
|
|
||||||
"settings.general.row.newLayoutDesigns.description": "Enable the redesigned layout, home, composer, and session UI",
|
|
||||||
"settings.general.row.pinchZoom.title": "Pinch to zoom",
|
"settings.general.row.pinchZoom.title": "Pinch to zoom",
|
||||||
"settings.general.row.pinchZoom.description": "Allow trackpad pinch and Ctrl-scroll gestures to zoom",
|
"settings.general.row.pinchZoom.description": "Allow trackpad pinch and Ctrl-scroll gestures to zoom",
|
||||||
|
|
||||||
|
|||||||
@@ -759,7 +759,8 @@ export const dict = {
|
|||||||
"settings.general.row.followup.option.queue": "Черга",
|
"settings.general.row.followup.option.queue": "Черга",
|
||||||
"settings.general.row.followup.option.steer": "Керування",
|
"settings.general.row.followup.option.steer": "Керування",
|
||||||
"settings.general.row.showFileTree.title": "Дерево файлів",
|
"settings.general.row.showFileTree.title": "Дерево файлів",
|
||||||
"settings.general.row.showFileTree.description": "Показувати панель дерева файлів у сесіях на робочому столі",
|
"settings.general.row.showFileTree.description":
|
||||||
|
"Показувати перемикач і панель дерева файлів у сесіях на робочому столі",
|
||||||
"settings.general.row.showNavigation.title": "Елементи навігації",
|
"settings.general.row.showNavigation.title": "Елементи навігації",
|
||||||
"settings.general.row.showNavigation.description": "Показувати кнопки назад і вперед у заголовку робочого столу",
|
"settings.general.row.showNavigation.description": "Показувати кнопки назад і вперед у заголовку робочого столу",
|
||||||
"settings.general.row.showSearch.title": "Палітра команд",
|
"settings.general.row.showSearch.title": "Палітра команд",
|
||||||
|
|||||||
@@ -8,13 +8,6 @@
|
|||||||
font-style: normal;
|
font-style: normal;
|
||||||
}
|
}
|
||||||
|
|
||||||
@font-face {
|
|
||||||
font-family: "Inter";
|
|
||||||
src: url("/assets/Inter.ttf") format("truetype");
|
|
||||||
font-weight: normal;
|
|
||||||
font-style: normal;
|
|
||||||
}
|
|
||||||
|
|
||||||
@layer components {
|
@layer components {
|
||||||
@keyframes session-progress-whip {
|
@keyframes session-progress-whip {
|
||||||
0% {
|
0% {
|
||||||
|
|||||||
+54
-322
@@ -1,5 +1,5 @@
|
|||||||
import type { Session } from "@opencode-ai/sdk/v2/client"
|
import type { Session } from "@opencode-ai/sdk/v2/client"
|
||||||
import { createMemo, createSignal, For, Match, Show, Switch } from "solid-js"
|
import { createMemo, For, Match, Show, Switch } from "solid-js"
|
||||||
import { createStore } from "solid-js/store"
|
import { createStore } from "solid-js/store"
|
||||||
import { useQuery } from "@tanstack/solid-query"
|
import { useQuery } from "@tanstack/solid-query"
|
||||||
import { Button } from "@opencode-ai/ui/button"
|
import { Button } from "@opencode-ai/ui/button"
|
||||||
@@ -9,7 +9,6 @@ import { Avatar as AvatarV2 } from "@opencode-ai/ui/v2/components/avatar-v2.jsx"
|
|||||||
import { ButtonV2 } from "@opencode-ai/ui/v2/components/button-v2.jsx"
|
import { ButtonV2 } from "@opencode-ai/ui/v2/components/button-v2.jsx"
|
||||||
import { Icon as IconV2 } from "@opencode-ai/ui/v2/components/icon.jsx"
|
import { Icon as IconV2 } from "@opencode-ai/ui/v2/components/icon.jsx"
|
||||||
import { IconButtonV2 } from "@opencode-ai/ui/v2/components/icon-button-v2.jsx"
|
import { IconButtonV2 } from "@opencode-ai/ui/v2/components/icon-button-v2.jsx"
|
||||||
import { MenuV2 } from "@opencode-ai/ui/v2/components/menu-v2.jsx"
|
|
||||||
import { getAvatarColors, useLayout, type LocalProject } from "@/context/layout"
|
import { getAvatarColors, useLayout, type LocalProject } from "@/context/layout"
|
||||||
import { useNavigate } from "@solidjs/router"
|
import { useNavigate } from "@solidjs/router"
|
||||||
import { base64Encode } from "@opencode-ai/core/util/encode"
|
import { base64Encode } from "@opencode-ai/core/util/encode"
|
||||||
@@ -19,24 +18,23 @@ import { DateTime } from "luxon"
|
|||||||
import { useDialog } from "@opencode-ai/ui/context/dialog"
|
import { useDialog } from "@opencode-ai/ui/context/dialog"
|
||||||
import { DialogSelectDirectory } from "@/components/dialog-select-directory"
|
import { DialogSelectDirectory } from "@/components/dialog-select-directory"
|
||||||
import { DialogSelectServer } from "@/components/dialog-select-server"
|
import { DialogSelectServer } from "@/components/dialog-select-server"
|
||||||
import { ServerConnection, useServer } from "@/context/server"
|
import { useServer } from "@/context/server"
|
||||||
import { useServerSync } from "@/context/server-sync"
|
import { useServerSync } from "@/context/server-sync"
|
||||||
import { useLanguage } from "@/context/language"
|
import { useLanguage } from "@/context/language"
|
||||||
import { useNotification } from "@/context/notification"
|
import { useNotification } from "@/context/notification"
|
||||||
import { usePermission } from "@/context/permission"
|
import { usePermission } from "@/context/permission"
|
||||||
import { displayName, getProjectAvatarSource, projectForSession, sortedRootSessions } from "@/pages/layout/helpers"
|
import { displayName, getProjectAvatarSource, projectForSession, sortedRootSessions } from "@/pages/layout/helpers"
|
||||||
|
import { getFilename } from "@opencode-ai/core/util/path"
|
||||||
import { sessionTitle } from "@/utils/session-title"
|
import { sessionTitle } from "@/utils/session-title"
|
||||||
import { pathKey } from "@/utils/path-key"
|
import { pathKey } from "@/utils/path-key"
|
||||||
import { messageAgentColor } from "@/utils/agent"
|
import { messageAgentColor } from "@/utils/agent"
|
||||||
import { sessionPermissionRequest } from "@/pages/session/composer/session-request-tree"
|
import { sessionPermissionRequest } from "@/pages/session/composer/session-request-tree"
|
||||||
import { ServerHealthIndicator } from "@/components/server/server-row"
|
|
||||||
import { useServers } from "@/context/servers"
|
|
||||||
import { useSettings } from "@/context/settings"
|
|
||||||
|
|
||||||
|
const USE_HOME_DESIGN = import.meta.env.VITE_OPENCODE_CHANNEL !== "prod"
|
||||||
const HOME_SESSION_LIMIT = 15
|
const HOME_SESSION_LIMIT = 15
|
||||||
const HOME_ROW =
|
const HOME_ROW =
|
||||||
"flex min-w-0 w-full shrink-0 cursor-default items-center rounded-[6px] border-0 bg-transparent text-left text-v2-text-text-muted transition-colors duration-[120ms] ease-in-out hover:bg-v2-overlay-simple-overlay-hover focus-visible:bg-v2-overlay-simple-overlay-hover focus-visible:outline-none"
|
"flex min-w-0 w-full shrink-0 cursor-default items-center rounded-[6px] border-0 bg-transparent text-left [font-weight:530] text-v2-text-text-muted transition-colors duration-[120ms] ease-in-out hover:bg-v2-overlay-simple-overlay-hover focus-visible:bg-v2-overlay-simple-overlay-hover focus-visible:outline-none"
|
||||||
const HOME_PROJECT_NAV_ROW = `${HOME_ROW} h-7 gap-2 px-1.5 [&>span]:min-w-0 [&>span]:overflow-hidden [&>span]:text-ellipsis [&>span]:whitespace-nowrap`
|
const HOME_PROJECT_NAV_ROW = `${HOME_ROW} h-8 gap-1.5 px-3 [&>span]:min-w-0 [&>span]:overflow-hidden [&>span]:text-ellipsis [&>span]:whitespace-nowrap`
|
||||||
const HOME_SECTION_LABEL = "text-v2-text-text-muted [font-weight:440]"
|
const HOME_SECTION_LABEL = "text-v2-text-text-muted [font-weight:440]"
|
||||||
|
|
||||||
type HomeSessionRecord = {
|
type HomeSessionRecord = {
|
||||||
@@ -52,12 +50,8 @@ type HomeSessionGroup = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export default function Home() {
|
export default function Home() {
|
||||||
const settings = useSettings()
|
if (USE_HOME_DESIGN) return <HomeDesign />
|
||||||
return (
|
return <LegacyHome />
|
||||||
<Show when={settings.general.newLayoutDesigns()} fallback={<LegacyHome />}>
|
|
||||||
<HomeDesign />
|
|
||||||
</Show>
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function HomeDesign() {
|
function HomeDesign() {
|
||||||
@@ -68,16 +62,16 @@ function HomeDesign() {
|
|||||||
const navigate = useNavigate()
|
const navigate = useNavigate()
|
||||||
const server = useServer()
|
const server = useServer()
|
||||||
const language = useLanguage()
|
const language = useLanguage()
|
||||||
const notification = useNotification()
|
|
||||||
const [state, setState] = createStore({ search: "", project: undefined as string | undefined })
|
const [state, setState] = createStore({ search: "", project: undefined as string | undefined })
|
||||||
|
|
||||||
const projects = createMemo(() => layout.projects.list())
|
const projects = createMemo(() => layout.projects.list())
|
||||||
const selectedProject = createMemo(() => projects().find((project) => project.worktree === state.project))
|
const selectedProject = createMemo(
|
||||||
const directories = (project: LocalProject) => [project.worktree, ...(project.sandboxes ?? [])]
|
() => projects().find((project) => project.worktree === state.project) ?? projects()[0],
|
||||||
|
)
|
||||||
const projectDirectories = createMemo(() => {
|
const projectDirectories = createMemo(() => {
|
||||||
const project = selectedProject()
|
const project = selectedProject()
|
||||||
if (!project) return [...projects().flatMap((project) => directories(project))]
|
if (!project) return []
|
||||||
return directories(project)
|
return [project.worktree, ...(project.sandboxes ?? [])]
|
||||||
})
|
})
|
||||||
const search = createMemo(() => state.search.trim())
|
const search = createMemo(() => state.search.trim())
|
||||||
const sessionLoad = useQuery(() => ({
|
const sessionLoad = useQuery(() => ({
|
||||||
@@ -91,8 +85,8 @@ function HomeDesign() {
|
|||||||
const projectByID = createMemo(
|
const projectByID = createMemo(
|
||||||
() => new Map(projects().flatMap((project) => (project.id ? [[project.id, project] as const] : []))),
|
() => new Map(projects().flatMap((project) => (project.id ? [[project.id, project] as const] : []))),
|
||||||
)
|
)
|
||||||
const records = createMemo(() => {
|
const records = createMemo(() =>
|
||||||
return [
|
[
|
||||||
...new Map(
|
...new Map(
|
||||||
projectDirectories()
|
projectDirectories()
|
||||||
.flatMap((directory) => sortedRootSessions(sync.child(directory, { bootstrap: false })[0], Date.now()))
|
.flatMap((directory) => sortedRootSessions(sync.child(directory, { bootstrap: false })[0], Date.now()))
|
||||||
@@ -114,8 +108,8 @@ function HomeDesign() {
|
|||||||
if (!value) return true
|
if (!value) return true
|
||||||
return `${record.session.title} ${record.projectName}`.toLowerCase().includes(value)
|
return `${record.session.title} ${record.projectName}`.toLowerCase().includes(value)
|
||||||
})
|
})
|
||||||
.slice(0, HOME_SESSION_LIMIT)
|
.slice(0, HOME_SESSION_LIMIT),
|
||||||
})
|
)
|
||||||
const groups = createMemo(() => groupSessions(records(), language))
|
const groups = createMemo(() => groupSessions(records(), language))
|
||||||
|
|
||||||
function selectProject(directory: string) {
|
function selectProject(directory: string) {
|
||||||
@@ -140,26 +134,6 @@ function HomeDesign() {
|
|||||||
navigate(`/${base64Encode(project.worktree)}/session`)
|
navigate(`/${base64Encode(project.worktree)}/session`)
|
||||||
}
|
}
|
||||||
|
|
||||||
function openProjectNewSession(directory: string) {
|
|
||||||
layout.projects.open(directory)
|
|
||||||
server.projects.touch(directory)
|
|
||||||
navigate(`/${base64Encode(directory)}/session`)
|
|
||||||
}
|
|
||||||
|
|
||||||
const showEditProjectDialog = (project: LocalProject) => {
|
|
||||||
void import("@/components/dialog-edit-project").then((x) => {
|
|
||||||
dialog.show(() => <x.DialogEditProject project={project} />)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
const unseenCount = (project: LocalProject) =>
|
|
||||||
directories(project).reduce((total, directory) => total + notification.project.unseenCount(directory), 0)
|
|
||||||
|
|
||||||
const clearNotifications = (project: LocalProject) =>
|
|
||||||
directories(project)
|
|
||||||
.filter((directory) => notification.project.unseenCount(directory) > 0)
|
|
||||||
.forEach((directory) => notification.project.markViewed(directory))
|
|
||||||
|
|
||||||
function openSession(session: Session) {
|
function openSession(session: Session) {
|
||||||
const project = projectForSession(session, projects(), projectByID())
|
const project = projectForSession(session, projects(), projectByID())
|
||||||
layout.projects.open(project?.worktree ?? session.directory)
|
layout.projects.open(project?.worktree ?? session.directory)
|
||||||
@@ -201,17 +175,10 @@ function HomeDesign() {
|
|||||||
return (
|
return (
|
||||||
<div class="mx-auto grid w-full h-full max-w-[1080px] gap-8 px-6 pb-16 lg:grid-cols-[280px_minmax(0,720px)]">
|
<div class="mx-auto grid w-full h-full max-w-[1080px] gap-8 px-6 pb-16 lg:grid-cols-[280px_minmax(0,720px)]">
|
||||||
<HomeProjectColumn
|
<HomeProjectColumn
|
||||||
selectedProject={state.project}
|
projects={projects()}
|
||||||
|
selected={selectedProject()?.worktree}
|
||||||
selectProject={selectProject}
|
selectProject={selectProject}
|
||||||
openNewSession={openProjectNewSession}
|
|
||||||
chooseProject={() => void chooseProject()}
|
chooseProject={() => void chooseProject()}
|
||||||
editProject={showEditProjectDialog}
|
|
||||||
closeProject={(directory) => {
|
|
||||||
layout.projects.close(directory)
|
|
||||||
if (state.project === directory) setState("project", undefined)
|
|
||||||
}}
|
|
||||||
clearNotifications={clearNotifications}
|
|
||||||
unseenCount={unseenCount}
|
|
||||||
openSettings={openSettings}
|
openSettings={openSettings}
|
||||||
openHelp={() => platform.openLink("https://opencode.ai/desktop-feedback")}
|
openHelp={() => platform.openLink("https://opencode.ai/desktop-feedback")}
|
||||||
language={language}
|
language={language}
|
||||||
@@ -220,42 +187,21 @@ function HomeDesign() {
|
|||||||
<section
|
<section
|
||||||
class="min-w-0 flex-1 flex flex-col overflow-y-hidden pt-12"
|
class="min-w-0 flex-1 flex flex-col overflow-y-hidden pt-12"
|
||||||
aria-label={language.t("sidebar.project.recentSessions")}
|
aria-label={language.t("sidebar.project.recentSessions")}
|
||||||
>
|
|
||||||
<Show
|
|
||||||
when={projectDirectories().length > 0}
|
|
||||||
fallback={
|
|
||||||
<HomeEmptyState
|
|
||||||
icon="folder-add-left"
|
|
||||||
title={language.t("home.empty.title")}
|
|
||||||
description={language.t("home.empty.description")}
|
|
||||||
action={language.t("home.project.add")}
|
|
||||||
onAction={() => void chooseProject()}
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
>
|
>
|
||||||
<HomeSessionSearch
|
<HomeSessionSearch
|
||||||
value={state.search}
|
value={state.search}
|
||||||
placeholder={language.t("home.sessions.search.placeholder")}
|
placeholder={language.t("home.sessions.search.placeholder")}
|
||||||
onInput={(value) => setState("search", value)}
|
onInput={(value) => setState("search", value)}
|
||||||
clearLabel={language.t("common.clear")}
|
|
||||||
onClear={() => setState("search", "")}
|
|
||||||
/>
|
/>
|
||||||
<div class="mt-3 overflow-auto flex-1">
|
<div class="mt-3 overflow-auto flex-1">
|
||||||
<div class="pt-3 flex flex-col gap-6">
|
<div class="pt-3 flex flex-col gap-6">
|
||||||
<Show
|
<Show when={!sessionLoad.isLoading} fallback={<HomeSessionSkeleton label={language.t("common.loading")} />}>
|
||||||
when={!sessionLoad.isLoading}
|
|
||||||
fallback={<HomeSessionSkeleton label={language.t("common.loading")} />}
|
|
||||||
>
|
|
||||||
<Show
|
<Show
|
||||||
when={groups().length > 0}
|
when={groups().length > 0}
|
||||||
fallback={
|
fallback={
|
||||||
<HomeEmptyState
|
<div class="flex min-w-0 flex-col gap-4">
|
||||||
icon="edit"
|
<HomeSessionGroupHeader title={language.t("home.sessions.empty")} onNewSession={openNewSession} />
|
||||||
title={language.t("home.sessions.empty")}
|
</div>
|
||||||
description={language.t("home.sessions.empty.description")}
|
|
||||||
action={language.t("command.session.new")}
|
|
||||||
onAction={openNewSession}
|
|
||||||
/>
|
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<For each={groups()}>
|
<For each={groups()}>
|
||||||
@@ -277,31 +223,23 @@ function HomeDesign() {
|
|||||||
</Show>
|
</Show>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Show>
|
|
||||||
</section>
|
</section>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
function HomeProjectColumn(props: {
|
function HomeProjectColumn(props: {
|
||||||
selectedProject?: string
|
projects: LocalProject[]
|
||||||
|
selected?: string
|
||||||
selectProject: (directory: string) => void
|
selectProject: (directory: string) => void
|
||||||
openNewSession: (directory: string) => void
|
|
||||||
chooseProject: () => void
|
chooseProject: () => void
|
||||||
editProject: (project: LocalProject) => void
|
|
||||||
closeProject: (directory: string) => void
|
|
||||||
clearNotifications: (project: LocalProject) => void
|
|
||||||
unseenCount: (project: LocalProject) => number
|
|
||||||
openSettings: () => void
|
openSettings: () => void
|
||||||
openHelp: () => void
|
openHelp: () => void
|
||||||
language: ReturnType<typeof useLanguage>
|
language: ReturnType<typeof useLanguage>
|
||||||
}) {
|
}) {
|
||||||
const servers = useServers()
|
|
||||||
const layout = useLayout()
|
|
||||||
const projects = createMemo(() => layout.projects.list())
|
|
||||||
return (
|
return (
|
||||||
<aside class="flex min-w-0 flex-col lg:pt-[52px] gap-4" aria-label={props.language.t("home.projects")}>
|
<aside class="flex min-w-0 flex-col lg:pt-[52px]" aria-label={props.language.t("home.projects")}>
|
||||||
<div class="flex h-7 min-w-0 items-center justify-between pl-1.5">
|
<div class="flex h-7 min-w-0 items-center justify-between pl-3">
|
||||||
<div class={HOME_SECTION_LABEL}>{props.language.t("home.projects")}</div>
|
<div class={HOME_SECTION_LABEL}>{props.language.t("home.projects")}</div>
|
||||||
<IconButtonV2
|
<IconButtonV2
|
||||||
data-action="home-add-project"
|
data-action="home-add-project"
|
||||||
@@ -313,80 +251,39 @@ function HomeProjectColumn(props: {
|
|||||||
aria-label={props.language.t("home.project.add")}
|
aria-label={props.language.t("home.project.add")}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="mt-4 flex max-h-[min(572px,calc(100vh_-_300px))] min-w-0 flex-col gap-1 overflow-y-auto [scrollbar-width:none] [&::-webkit-scrollbar]:hidden">
|
||||||
<Show
|
<Show
|
||||||
when={servers.list().length > 1}
|
when={props.projects.length > 0}
|
||||||
fallback={
|
fallback={
|
||||||
<ProjectList
|
<button
|
||||||
projects={projects()}
|
type="button"
|
||||||
selectedProject={props.selectedProject}
|
class={`${HOME_PROJECT_NAV_ROW} text-v2-text-text-faint [&>[data-slot=icon-svg]]:text-v2-icon-icon-muted`}
|
||||||
onSelectedProjectChange={props.selectProject}
|
onClick={props.chooseProject}
|
||||||
onChooseProject={props.chooseProject}
|
>
|
||||||
openNewSession={props.openNewSession}
|
<IconV2 name="folder-add-left" size="small" />
|
||||||
editProject={props.editProject}
|
<span>{props.language.t("home.project.add")}</span>
|
||||||
closeProject={props.closeProject}
|
</button>
|
||||||
clearNotifications={props.clearNotifications}
|
|
||||||
unseenCount={props.unseenCount}
|
|
||||||
language={props.language}
|
|
||||||
/>
|
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<For each={servers.list()}>
|
<For each={props.projects}>
|
||||||
{(server) => {
|
{(project) => (
|
||||||
const key = ServerConnection.key(server)
|
|
||||||
const healthy = () => !!servers.health[key]?.healthy
|
|
||||||
const [open, setOpen] = createSignal(true)
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div class="max-h-[min(572px,calc(100vh_-_300px))] min-w-0 [scrollbar-width:none] [&::-webkit-scrollbar]:hidden">
|
|
||||||
<div class="relative h-7 group">
|
|
||||||
<button
|
<button
|
||||||
class="w-full h-full px-1.5 gap-2 flex flex-row items-center hover:not-disabled:bg-v2-overlay-simple-overlay-hover rounded-[4px]"
|
type="button"
|
||||||
disabled={!healthy()}
|
data-component="home-project-row"
|
||||||
onClick={() => setOpen((o) => !o)}
|
class={HOME_PROJECT_NAV_ROW}
|
||||||
|
classList={{ "bg-v2-overlay-simple-overlay-hover": props.selected === project.worktree }}
|
||||||
|
data-selected={props.selected === project.worktree ? "" : undefined}
|
||||||
|
aria-current={props.selected === project.worktree ? "page" : undefined}
|
||||||
|
onClick={() => props.selectProject(project.worktree)}
|
||||||
>
|
>
|
||||||
<div class="size-4 flex items-center justify-center">
|
<HomeProjectAvatar project={project} />
|
||||||
<ServerHealthIndicator health={servers.health[key]} />
|
<span>{displayName(project)}</span>
|
||||||
</div>
|
|
||||||
<div class="flex flex-row items-center gap-1">
|
|
||||||
<span>{server.displayName ?? new URL(server.http.url).host}</span>
|
|
||||||
<Show when={healthy()}>
|
|
||||||
<IconV2
|
|
||||||
name="outline-chevron-down"
|
|
||||||
class="text-v2-icon-icon-muted data-[open=false]:-rotate-90"
|
|
||||||
data-open={open()}
|
|
||||||
/>
|
|
||||||
</Show>
|
|
||||||
</div>
|
|
||||||
</button>
|
</button>
|
||||||
<IconButtonV2
|
)}
|
||||||
class="absolute right-1 inset-y-1 opacity-0 group-hover:opacity-100"
|
|
||||||
name="out"
|
|
||||||
variant="ghost-muted"
|
|
||||||
size="small"
|
|
||||||
icon={<IconV2 name="outline-dots" class="text-v2-icon-icon-muted" />}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<Show when={healthy() && open()}>
|
|
||||||
<div class="h-px bg-v2-border-border-base mx-3 my-1" />
|
|
||||||
<ProjectList
|
|
||||||
projects={projects()}
|
|
||||||
selectedProject={props.selectedProject}
|
|
||||||
onSelectedProjectChange={props.selectProject}
|
|
||||||
onChooseProject={props.chooseProject}
|
|
||||||
openNewSession={props.openNewSession}
|
|
||||||
editProject={props.editProject}
|
|
||||||
closeProject={props.closeProject}
|
|
||||||
clearNotifications={props.clearNotifications}
|
|
||||||
unseenCount={props.unseenCount}
|
|
||||||
language={props.language}
|
|
||||||
/>
|
|
||||||
</Show>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}}
|
|
||||||
</For>
|
</For>
|
||||||
</Show>
|
</Show>
|
||||||
<div class="flex min-w-0 flex-col gap-1">
|
</div>
|
||||||
|
<div class="mt-4 flex min-w-0 flex-col gap-1">
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
class={`${HOME_PROJECT_NAV_ROW} text-v2-text-text-faint [&>[data-slot=icon-svg]]:text-v2-icon-icon-muted`}
|
class={`${HOME_PROJECT_NAV_ROW} text-v2-text-text-faint [&>[data-slot=icon-svg]]:text-v2-icon-icon-muted`}
|
||||||
@@ -408,81 +305,6 @@ function HomeProjectColumn(props: {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
function HomeProjectRow(props: {
|
|
||||||
project: LocalProject
|
|
||||||
selected: boolean
|
|
||||||
unseenCount: number
|
|
||||||
selectProject: (directory: string) => void
|
|
||||||
openNewSession: (directory: string) => void
|
|
||||||
editProject: (project: LocalProject) => void
|
|
||||||
closeProject: (directory: string) => void
|
|
||||||
clearNotifications: (project: LocalProject) => void
|
|
||||||
language: ReturnType<typeof useLanguage>
|
|
||||||
}) {
|
|
||||||
const name = createMemo(() => displayName(props.project))
|
|
||||||
const [menuOpen, setMenuOpen] = createSignal(false)
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div class="group/project relative flex h-8 min-w-0 items-center rounded-[6px]">
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
data-component="home-project-row"
|
|
||||||
class={`${HOME_PROJECT_NAV_ROW} pr-16 peer`}
|
|
||||||
classList={{ "bg-v2-overlay-simple-overlay-hover": props.selected }}
|
|
||||||
data-selected={props.selected ? "" : undefined}
|
|
||||||
aria-current={props.selected ? "page" : undefined}
|
|
||||||
onClick={() => props.selectProject(props.project.worktree)}
|
|
||||||
>
|
|
||||||
<HomeProjectAvatar project={props.project} />
|
|
||||||
<span>{name()}</span>
|
|
||||||
</button>
|
|
||||||
<div
|
|
||||||
class="absolute right-1 top-1/2 flex -translate-y-1/2 items-center gap-0.5 opacity-0 transition-opacity group-hover/project:opacity-100 peer-focus-visible:opacity-100 focus-within:opacity-100 data-[menu=true]:opacity-100"
|
|
||||||
data-menu={menuOpen()}
|
|
||||||
>
|
|
||||||
<MenuV2 gutter={4} modal={false} placement="bottom-end" open={menuOpen()} onOpenChange={setMenuOpen}>
|
|
||||||
<MenuV2.Trigger
|
|
||||||
as={IconButtonV2}
|
|
||||||
data-action="home-project-menu"
|
|
||||||
variant="ghost-muted"
|
|
||||||
size="small"
|
|
||||||
icon={<IconV2 name="outline-dots" />}
|
|
||||||
aria-label={props.language.t("common.moreOptions")}
|
|
||||||
/>
|
|
||||||
<MenuV2.Portal>
|
|
||||||
<MenuV2.Content>
|
|
||||||
<MenuV2.Item onSelect={() => props.openNewSession(props.project.worktree)}>
|
|
||||||
{props.language.t("command.session.new")}
|
|
||||||
</MenuV2.Item>
|
|
||||||
<MenuV2.Item onSelect={() => props.editProject(props.project)}>
|
|
||||||
{props.language.t("common.edit")}
|
|
||||||
</MenuV2.Item>
|
|
||||||
<MenuV2.Item disabled={props.unseenCount === 0} onSelect={() => props.clearNotifications(props.project)}>
|
|
||||||
{props.language.t("sidebar.project.clearNotifications")}
|
|
||||||
</MenuV2.Item>
|
|
||||||
<MenuV2.Separator />
|
|
||||||
<MenuV2.Item onSelect={() => props.closeProject(props.project.worktree)}>
|
|
||||||
{props.language.t("common.close")}
|
|
||||||
</MenuV2.Item>
|
|
||||||
</MenuV2.Content>
|
|
||||||
</MenuV2.Portal>
|
|
||||||
</MenuV2>
|
|
||||||
<IconButtonV2
|
|
||||||
data-action="home-project-new-session"
|
|
||||||
variant="ghost-muted"
|
|
||||||
size="small"
|
|
||||||
icon={<IconV2 name="edit" />}
|
|
||||||
aria-label={props.language.t("command.session.new")}
|
|
||||||
onClick={(event) => {
|
|
||||||
event.stopPropagation()
|
|
||||||
props.openNewSession(props.project.worktree)
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function HomeProjectAvatar(props: { project: LocalProject }) {
|
function HomeProjectAvatar(props: { project: LocalProject }) {
|
||||||
const name = createMemo(() => displayName(props.project))
|
const name = createMemo(() => displayName(props.project))
|
||||||
return (
|
return (
|
||||||
@@ -497,13 +319,7 @@ function HomeProjectAvatar(props: { project: LocalProject }) {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
function HomeSessionSearch(props: {
|
function HomeSessionSearch(props: { value: string; placeholder: string; onInput: (value: string) => void }) {
|
||||||
value: string
|
|
||||||
placeholder: string
|
|
||||||
clearLabel: string
|
|
||||||
onInput: (value: string) => void
|
|
||||||
onClear: () => void
|
|
||||||
}) {
|
|
||||||
return (
|
return (
|
||||||
<label class="ml-4 flex h-9 w-[calc(100%_-_48px)] sticky top-0 inset-x-0 items-center gap-2 rounded-[6px] bg-v2-background-bg-deep px-3 py-1 text-v2-icon-icon-muted transition-[background-color,box-shadow] duration-[120ms] ease-in-out focus-within:bg-v2-background-bg-base focus-within:shadow-[0_0_0_0.5px_var(--v2-border-border-focus),var(--v2-elevation-raised)]">
|
<label class="ml-4 flex h-9 w-[calc(100%_-_48px)] sticky top-0 inset-x-0 items-center gap-2 rounded-[6px] bg-v2-background-bg-deep px-3 py-1 text-v2-icon-icon-muted transition-[background-color,box-shadow] duration-[120ms] ease-in-out focus-within:bg-v2-background-bg-base focus-within:shadow-[0_0_0_0.5px_var(--v2-border-border-focus),var(--v2-elevation-raised)]">
|
||||||
<IconV2 name="magnifying-glass" size="small" />
|
<IconV2 name="magnifying-glass" size="small" />
|
||||||
@@ -514,46 +330,10 @@ function HomeSessionSearch(props: {
|
|||||||
aria-label={props.placeholder}
|
aria-label={props.placeholder}
|
||||||
onInput={(event) => props.onInput(event.currentTarget.value)}
|
onInput={(event) => props.onInput(event.currentTarget.value)}
|
||||||
/>
|
/>
|
||||||
<Show when={props.value.trim()}>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
class="flex size-5 shrink-0 items-center justify-center rounded text-v2-icon-icon-muted hover:bg-v2-overlay-simple-overlay-hover focus-visible:bg-v2-overlay-simple-overlay-hover focus-visible:outline-none"
|
|
||||||
aria-label={props.clearLabel}
|
|
||||||
onClick={(event) => {
|
|
||||||
event.preventDefault()
|
|
||||||
props.onClear()
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Icon name="close-small" size="small" />
|
|
||||||
</button>
|
|
||||||
</Show>
|
|
||||||
</label>
|
</label>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
function HomeEmptyState(props: {
|
|
||||||
icon: Parameters<typeof IconV2>[0]["name"]
|
|
||||||
title: string
|
|
||||||
description: string
|
|
||||||
action: string
|
|
||||||
onAction: () => void
|
|
||||||
}) {
|
|
||||||
return (
|
|
||||||
<div class="flex min-h-[320px] flex-1 flex-col items-center justify-center gap-4 px-6 text-center">
|
|
||||||
<div class="flex size-10 items-center justify-center rounded-[10px] bg-v2-background-bg-deep text-v2-icon-icon-muted shadow-[var(--v2-elevation-raised)]">
|
|
||||||
<IconV2 name={props.icon} />
|
|
||||||
</div>
|
|
||||||
<div class="flex max-w-[320px] flex-col gap-1">
|
|
||||||
<div class="text-v2-text-text-base [font-weight:530]">{props.title}</div>
|
|
||||||
<div class="text-v2-text-text-muted [font-weight:440]">{props.description}</div>
|
|
||||||
</div>
|
|
||||||
<ButtonV2 variant="neutral" size="normal" icon={props.icon} onClick={props.onAction}>
|
|
||||||
{props.action}
|
|
||||||
</ButtonV2>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function HomeSessionGroupHeader(props: { title: string; onNewSession?: () => void }) {
|
function HomeSessionGroupHeader(props: { title: string; onNewSession?: () => void }) {
|
||||||
const language = useLanguage()
|
const language = useLanguage()
|
||||||
return (
|
return (
|
||||||
@@ -566,7 +346,7 @@ function HomeSessionGroupHeader(props: { title: string; onNewSession?: () => voi
|
|||||||
variant="ghost"
|
variant="ghost"
|
||||||
size="normal"
|
size="normal"
|
||||||
icon="edit"
|
icon="edit"
|
||||||
class="h-7 px-2 text-v2-text-text-muted"
|
class="h-7 px-2 text-v2-text-text-muted [font-weight:530]"
|
||||||
onClick={onNewSession()}
|
onClick={onNewSession()}
|
||||||
>
|
>
|
||||||
{language.t("command.session.new")}
|
{language.t("command.session.new")}
|
||||||
@@ -684,7 +464,6 @@ function LegacyHome() {
|
|||||||
const platform = usePlatform()
|
const platform = usePlatform()
|
||||||
const dialog = useDialog()
|
const dialog = useDialog()
|
||||||
const navigate = useNavigate()
|
const navigate = useNavigate()
|
||||||
const servers = useServers()
|
|
||||||
const server = useServer()
|
const server = useServer()
|
||||||
const language = useLanguage()
|
const language = useLanguage()
|
||||||
const homedir = createMemo(() => sync.data.path.home)
|
const homedir = createMemo(() => sync.data.path.home)
|
||||||
@@ -696,7 +475,7 @@ function LegacyHome() {
|
|||||||
})
|
})
|
||||||
|
|
||||||
const serverDotClass = createMemo(() => {
|
const serverDotClass = createMemo(() => {
|
||||||
const healthy = servers.health[server.key]?.healthy
|
const healthy = server.healthy()
|
||||||
if (healthy === true) return "bg-icon-success-base"
|
if (healthy === true) return "bg-icon-success-base"
|
||||||
if (healthy === false) return "bg-icon-critical-base"
|
if (healthy === false) return "bg-icon-critical-base"
|
||||||
return "bg-border-weak-base"
|
return "bg-border-weak-base"
|
||||||
@@ -802,50 +581,3 @@ function LegacyHome() {
|
|||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
function ProjectList(props: {
|
|
||||||
projects: LocalProject[]
|
|
||||||
selectedProject?: string
|
|
||||||
onSelectedProjectChange?(project: string): void
|
|
||||||
onChooseProject?(): void
|
|
||||||
openNewSession: (directory: string) => void
|
|
||||||
editProject: (project: LocalProject) => void
|
|
||||||
closeProject: (directory: string) => void
|
|
||||||
clearNotifications: (project: LocalProject) => void
|
|
||||||
unseenCount: (project: LocalProject) => number
|
|
||||||
language: ReturnType<typeof useLanguage>
|
|
||||||
}) {
|
|
||||||
return (
|
|
||||||
<Show
|
|
||||||
when={props.projects.length > 0}
|
|
||||||
fallback={
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
class={`${HOME_PROJECT_NAV_ROW} text-v2-text-text-faint [&>[data-slot=icon-svg]]:text-v2-icon-icon-muted`}
|
|
||||||
onClick={() => props.onChooseProject?.()}
|
|
||||||
>
|
|
||||||
<IconV2 name="folder-add-left" size="small" />
|
|
||||||
<span>{props.language.t("home.project.add")}</span>
|
|
||||||
</button>
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<div class="flex flex-col gap-1">
|
|
||||||
<For each={props.projects}>
|
|
||||||
{(project) => (
|
|
||||||
<HomeProjectRow
|
|
||||||
project={project}
|
|
||||||
selected={props.selectedProject === project.worktree}
|
|
||||||
unseenCount={props.unseenCount(project)}
|
|
||||||
selectProject={(directory) => props.onSelectedProjectChange?.(directory)}
|
|
||||||
openNewSession={props.openNewSession}
|
|
||||||
editProject={props.editProject}
|
|
||||||
closeProject={props.closeProject}
|
|
||||||
clearNotifications={props.clearNotifications}
|
|
||||||
language={props.language}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</For>
|
|
||||||
</div>
|
|
||||||
</Show>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -89,6 +89,8 @@ import {
|
|||||||
import { ProjectDragOverlay, SortableProject, type ProjectSidebarContext } from "./layout/sidebar-project"
|
import { ProjectDragOverlay, SortableProject, type ProjectSidebarContext } from "./layout/sidebar-project"
|
||||||
import { SidebarContent } from "./layout/sidebar-shell"
|
import { SidebarContent } from "./layout/sidebar-shell"
|
||||||
|
|
||||||
|
const USE_NEW_DESIGN = import.meta.env.VITE_OPENCODE_CHANNEL !== "prod"
|
||||||
|
|
||||||
export default function Layout(props: ParentProps) {
|
export default function Layout(props: ParentProps) {
|
||||||
const [store, setStore, , ready] = persisted(
|
const [store, setStore, , ready] = persisted(
|
||||||
Persist.global("layout.page", ["layout.page.v1"]),
|
Persist.global("layout.page", ["layout.page.v1"]),
|
||||||
@@ -127,7 +129,6 @@ export default function Layout(props: ParentProps) {
|
|||||||
const command = useCommand()
|
const command = useCommand()
|
||||||
const theme = useTheme()
|
const theme = useTheme()
|
||||||
const language = useLanguage()
|
const language = useLanguage()
|
||||||
const newDesign = createMemo(() => settings.general.newLayoutDesigns())
|
|
||||||
const initialDirectory = decode64(params.dir)
|
const initialDirectory = decode64(params.dir)
|
||||||
const location = useLocation()
|
const location = useLocation()
|
||||||
const route = createMemo(() => {
|
const route = createMemo(() => {
|
||||||
@@ -153,7 +154,7 @@ export default function Layout(props: ParentProps) {
|
|||||||
const currentDir = createMemo(() => route().dir)
|
const currentDir = createMemo(() => route().dir)
|
||||||
|
|
||||||
const [state, setState] = createStore({
|
const [state, setState] = createStore({
|
||||||
autoselect: !initialDirectory && !newDesign(),
|
autoselect: !initialDirectory && !USE_NEW_DESIGN,
|
||||||
busyWorkspaces: {} as Record<string, boolean>,
|
busyWorkspaces: {} as Record<string, boolean>,
|
||||||
hoverProject: undefined as string | undefined,
|
hoverProject: undefined as string | undefined,
|
||||||
scrollSessionKey: undefined as string | undefined,
|
scrollSessionKey: undefined as string | undefined,
|
||||||
@@ -1141,7 +1142,7 @@ export default function Layout(props: ParentProps) {
|
|||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
if (!newDesign())
|
if (!USE_NEW_DESIGN)
|
||||||
Array.from({ length: 9 }, (_, i) => {
|
Array.from({ length: 9 }, (_, i) => {
|
||||||
const index = i
|
const index = i
|
||||||
const number = index + 1
|
const number = index + 1
|
||||||
@@ -1820,7 +1821,7 @@ export default function Layout(props: ParentProps) {
|
|||||||
createEffect(() => {
|
createEffect(() => {
|
||||||
document.documentElement.style.setProperty(
|
document.documentElement.style.setProperty(
|
||||||
"--dialog-left-margin",
|
"--dialog-left-margin",
|
||||||
newDesign() ? "0px" : `${layout.sidebar.opened() ? layout.sidebar.width() : 48}px`,
|
USE_NEW_DESIGN ? "0px" : `${layout.sidebar.opened() ? layout.sidebar.width() : 48}px`,
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -2362,10 +2363,8 @@ export default function Layout(props: ParentProps) {
|
|||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if (USE_NEW_DESIGN) {
|
||||||
return (
|
return (
|
||||||
<Show
|
|
||||||
when={!newDesign()}
|
|
||||||
fallback={
|
|
||||||
<div class="relative bg-v2-background-bg-deep flex-1 min-h-0 min-w-0 flex flex-col select-none [&_input]:select-text [&_textarea]:select-text [&_[contenteditable]]:select-text">
|
<div class="relative bg-v2-background-bg-deep flex-1 min-h-0 min-w-0 flex flex-col select-none [&_input]:select-text [&_textarea]:select-text [&_[contenteditable]]:select-text">
|
||||||
{autoselecting() ?? ""}
|
{autoselecting() ?? ""}
|
||||||
<Titlebar update={titlebarUpdate} />
|
<Titlebar update={titlebarUpdate} />
|
||||||
@@ -2382,8 +2381,10 @@ export default function Layout(props: ParentProps) {
|
|||||||
{import.meta.env.DEV && <DebugBar />}
|
{import.meta.env.DEV && <DebugBar />}
|
||||||
<Toast.Region />
|
<Toast.Region />
|
||||||
</div>
|
</div>
|
||||||
|
)
|
||||||
}
|
}
|
||||||
>
|
|
||||||
|
return (
|
||||||
<div class="relative bg-background-base flex-1 min-h-0 min-w-0 flex flex-col select-none [&_input]:select-text [&_textarea]:select-text [&_[contenteditable]]:select-text">
|
<div class="relative bg-background-base flex-1 min-h-0 min-w-0 flex flex-col select-none [&_input]:select-text [&_textarea]:select-text [&_[contenteditable]]:select-text">
|
||||||
{autoselecting() ?? ""}
|
{autoselecting() ?? ""}
|
||||||
<Titlebar update={titlebarUpdate} />
|
<Titlebar update={titlebarUpdate} />
|
||||||
@@ -2535,7 +2536,6 @@ export default function Layout(props: ParentProps) {
|
|||||||
</div>
|
</div>
|
||||||
<Toast.Region />
|
<Toast.Region />
|
||||||
</div>
|
</div>
|
||||||
</Show>
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -75,6 +75,7 @@ const emptyFollowups: FollowupItem[] = []
|
|||||||
|
|
||||||
type ChangeMode = "git" | "branch" | "turn"
|
type ChangeMode = "git" | "branch" | "turn"
|
||||||
type VcsMode = "git" | "branch"
|
type VcsMode = "git" | "branch"
|
||||||
|
const USE_NEW_SESSION_DESIGN = import.meta.env.VITE_OPENCODE_CHANNEL !== "prod"
|
||||||
|
|
||||||
type SessionHistoryWindowInput = {
|
type SessionHistoryWindowInput = {
|
||||||
sessionID: () => string | undefined
|
sessionID: () => string | undefined
|
||||||
@@ -197,7 +198,6 @@ export default function Page() {
|
|||||||
const [searchParams, setSearchParams] = useSearchParams<{ prompt?: string }>()
|
const [searchParams, setSearchParams] = useSearchParams<{ prompt?: string }>()
|
||||||
const location = useLocation()
|
const location = useLocation()
|
||||||
const { params, sessionKey, tabs, view } = useSessionLayout()
|
const { params, sessionKey, tabs, view } = useSessionLayout()
|
||||||
const newSessionDesign = createMemo(() => settings.general.newLayoutDesigns())
|
|
||||||
|
|
||||||
createEffect(() => {
|
createEffect(() => {
|
||||||
if (!prompt.ready()) return
|
if (!prompt.ready()) return
|
||||||
@@ -265,7 +265,7 @@ export default function Page() {
|
|||||||
const isDesktop = createMediaQuery("(min-width: 768px)")
|
const isDesktop = createMediaQuery("(min-width: 768px)")
|
||||||
const size = createSizing()
|
const size = createSizing()
|
||||||
const isV2NewSessionPage = () =>
|
const isV2NewSessionPage = () =>
|
||||||
shouldUseV2NewSessionPage({ newLayoutDesigns: newSessionDesign(), sessionID: params.id })
|
shouldUseV2NewSessionPage({ channel: import.meta.env.VITE_OPENCODE_CHANNEL, sessionID: params.id })
|
||||||
const desktopReviewOpen = createMemo(() => isDesktop() && view().reviewPanel.opened() && !isV2NewSessionPage())
|
const desktopReviewOpen = createMemo(() => isDesktop() && view().reviewPanel.opened() && !isV2NewSessionPage())
|
||||||
const desktopFileTreeOpen = createMemo(() => isDesktop() && layout.fileTree.opened() && !isV2NewSessionPage())
|
const desktopFileTreeOpen = createMemo(() => isDesktop() && layout.fileTree.opened() && !isV2NewSessionPage())
|
||||||
const desktopSidePanelOpen = createMemo(() => desktopReviewOpen() || desktopFileTreeOpen())
|
const desktopSidePanelOpen = createMemo(() => desktopReviewOpen() || desktopFileTreeOpen())
|
||||||
@@ -1663,6 +1663,7 @@ export default function Page() {
|
|||||||
inputRef = el
|
inputRef = el
|
||||||
}}
|
}}
|
||||||
newSessionWorktree={newSessionWorktree()}
|
newSessionWorktree={newSessionWorktree()}
|
||||||
|
onNewSessionWorktreeChange={(value) => setStore("newSessionWorktree", value)}
|
||||||
onNewSessionWorktreeReset={() => setStore("newSessionWorktree", "main")}
|
onNewSessionWorktreeReset={() => setStore("newSessionWorktree", "main")}
|
||||||
onSubmit={() => {
|
onSubmit={() => {
|
||||||
comments.clear()
|
comments.clear()
|
||||||
@@ -1798,14 +1799,16 @@ export default function Page() {
|
|||||||
</Show>
|
</Show>
|
||||||
</Match>
|
</Match>
|
||||||
<Match when={true}>
|
<Match when={true}>
|
||||||
<Show when={newSessionDesign()} fallback={<NewSessionView worktree={newSessionWorktree()} />}>
|
<Show when={USE_NEW_SESSION_DESIGN} fallback={<NewSessionView worktree={newSessionWorktree()} />}>
|
||||||
<NewSessionDesignView>{composerRegion("inline")}</NewSessionDesignView>
|
<NewSessionDesignView worktree={newSessionWorktree()}>
|
||||||
|
{composerRegion("inline")}
|
||||||
|
</NewSessionDesignView>
|
||||||
</Show>
|
</Show>
|
||||||
</Match>
|
</Match>
|
||||||
</Switch>
|
</Switch>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Show when={params.id || !newSessionDesign()}>{composerRegion("dock")}</Show>
|
<Show when={params.id || !USE_NEW_SESSION_DESIGN}>{composerRegion("dock")}</Show>
|
||||||
|
|
||||||
<Show when={desktopReviewOpen()}>
|
<Show when={desktopReviewOpen()}>
|
||||||
<div onPointerDown={() => size.start()}>
|
<div onPointerDown={() => size.start()}>
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ export function SessionComposerRegion(props: {
|
|||||||
placement?: "dock" | "inline"
|
placement?: "dock" | "inline"
|
||||||
inputRef: (el: HTMLDivElement) => void
|
inputRef: (el: HTMLDivElement) => void
|
||||||
newSessionWorktree: string
|
newSessionWorktree: string
|
||||||
|
onNewSessionWorktreeChange?: (worktree: string) => void
|
||||||
onNewSessionWorktreeReset: () => void
|
onNewSessionWorktreeReset: () => void
|
||||||
onSubmit: () => void
|
onSubmit: () => void
|
||||||
onResponseSubmit: () => void
|
onResponseSubmit: () => void
|
||||||
@@ -264,6 +265,7 @@ export function SessionComposerRegion(props: {
|
|||||||
variant={props.placement === "inline" ? "new-session" : undefined}
|
variant={props.placement === "inline" ? "new-session" : undefined}
|
||||||
ref={props.inputRef}
|
ref={props.inputRef}
|
||||||
newSessionWorktree={props.newSessionWorktree}
|
newSessionWorktree={props.newSessionWorktree}
|
||||||
|
onNewSessionWorktreeChange={props.onNewSessionWorktreeChange}
|
||||||
onNewSessionWorktreeReset={props.onNewSessionWorktreeReset}
|
onNewSessionWorktreeReset={props.onNewSessionWorktreeReset}
|
||||||
edit={props.followup?.edit}
|
edit={props.followup?.edit}
|
||||||
onEditLoaded={props.followup?.onEditLoaded}
|
onEditLoaded={props.followup?.onEditLoaded}
|
||||||
|
|||||||
@@ -65,7 +65,6 @@ import { usePlatform } from "@/context/platform"
|
|||||||
import { useSettings } from "@/context/settings"
|
import { useSettings } from "@/context/settings"
|
||||||
import { useSDK } from "@/context/sdk"
|
import { useSDK } from "@/context/sdk"
|
||||||
import { useSync } from "@/context/sync"
|
import { useSync } from "@/context/sync"
|
||||||
import { notifySessionTabsRemoved } from "@/components/titlebar-session-events"
|
|
||||||
import { messageAgentColor } from "@/utils/agent"
|
import { messageAgentColor } from "@/utils/agent"
|
||||||
import { sessionTitle } from "@/utils/session-title"
|
import { sessionTitle } from "@/utils/session-title"
|
||||||
import { makeTimer } from "@solid-primitives/timer"
|
import { makeTimer } from "@solid-primitives/timer"
|
||||||
@@ -862,9 +861,7 @@ export function MessageTimeline(props: {
|
|||||||
if (index !== -1) draft.session.splice(index, 1)
|
if (index !== -1) draft.session.splice(index, 1)
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
sync.session.evict(sessionID)
|
|
||||||
navigateAfterSessionRemoval(sessionID, session.parentID, nextSession?.id)
|
navigateAfterSessionRemoval(sessionID, session.parentID, nextSession?.id)
|
||||||
notifySessionTabsRemoved({ directory: sdk.directory, sessionIDs: [sessionID] })
|
|
||||||
})
|
})
|
||||||
.catch((err) => {
|
.catch((err) => {
|
||||||
showToast({
|
showToast({
|
||||||
@@ -895,9 +892,12 @@ export function MessageTimeline(props: {
|
|||||||
|
|
||||||
if (!result) return false
|
if (!result) return false
|
||||||
|
|
||||||
|
sync.set(
|
||||||
|
produce((draft) => {
|
||||||
const removed = new Set<string>([sessionID])
|
const removed = new Set<string>([sessionID])
|
||||||
|
|
||||||
const byParent = new Map<string, string[]>()
|
const byParent = new Map<string, string[]>()
|
||||||
for (const item of sync.data.session) {
|
for (const item of draft.session) {
|
||||||
const parentID = item.parentID
|
const parentID = item.parentID
|
||||||
if (!parentID) continue
|
if (!parentID) continue
|
||||||
const existing = byParent.get(parentID)
|
const existing = byParent.get(parentID)
|
||||||
@@ -923,18 +923,11 @@ export function MessageTimeline(props: {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
navigateAfterSessionRemoval(sessionID, session.parentID, nextSession?.id)
|
|
||||||
|
|
||||||
sync.set(
|
|
||||||
produce((draft) => {
|
|
||||||
draft.session = draft.session.filter((s) => !removed.has(s.id))
|
draft.session = draft.session.filter((s) => !removed.has(s.id))
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
|
|
||||||
for (const id of removed) {
|
navigateAfterSessionRemoval(sessionID, session.parentID, nextSession?.id)
|
||||||
sync.session.evict(id)
|
|
||||||
}
|
|
||||||
notifySessionTabsRemoved({ directory: sdk.directory, sessionIDs: [...removed] })
|
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -2,13 +2,13 @@ import { describe, expect, test } from "bun:test"
|
|||||||
import { shouldUseV2NewSessionPage } from "./new-session-layout"
|
import { shouldUseV2NewSessionPage } from "./new-session-layout"
|
||||||
|
|
||||||
describe("shouldUseV2NewSessionPage", () => {
|
describe("shouldUseV2NewSessionPage", () => {
|
||||||
test("keeps disabled pages on the legacy layout", () => {
|
test("keeps prod session pages on the legacy layout", () => {
|
||||||
expect(shouldUseV2NewSessionPage({ newLayoutDesigns: false, sessionID: "ses_123" })).toBe(false)
|
expect(shouldUseV2NewSessionPage({ channel: "prod", sessionID: "ses_123" })).toBe(false)
|
||||||
expect(shouldUseV2NewSessionPage({ newLayoutDesigns: false })).toBe(false)
|
expect(shouldUseV2NewSessionPage({ channel: "prod" })).toBe(false)
|
||||||
})
|
})
|
||||||
|
|
||||||
test("uses the v2 layout only for enabled new-session pages", () => {
|
test("uses the v2 layout only for non-prod new-session pages", () => {
|
||||||
expect(shouldUseV2NewSessionPage({ newLayoutDesigns: true })).toBe(true)
|
expect(shouldUseV2NewSessionPage({ channel: "dev" })).toBe(true)
|
||||||
expect(shouldUseV2NewSessionPage({ newLayoutDesigns: true, sessionID: "ses_123" })).toBe(false)
|
expect(shouldUseV2NewSessionPage({ channel: "dev", sessionID: "ses_123" })).toBe(false)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,3 +1,3 @@
|
|||||||
export function shouldUseV2NewSessionPage(input: { newLayoutDesigns: boolean; sessionID?: string }) {
|
export function shouldUseV2NewSessionPage(input: { channel?: "dev" | "beta" | "prod"; sessionID?: string }) {
|
||||||
return input.newLayoutDesigns && !input.sessionID
|
return input.channel !== "prod" && !input.sessionID
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -58,8 +58,12 @@ export function SessionSidePanel(props: {
|
|||||||
const { sessionKey, tabs, view, params } = useSessionLayout()
|
const { sessionKey, tabs, view, params } = useSessionLayout()
|
||||||
|
|
||||||
const isDesktop = createMediaQuery("(min-width: 768px)")
|
const isDesktop = createMediaQuery("(min-width: 768px)")
|
||||||
const desktopV2 = () => platform.platform === "desktop" && settings.general.newLayoutDesigns()
|
const shown = createMemo(
|
||||||
const shown = createMemo(() => (desktopV2() ? settings.general.showFileTree() : true))
|
() =>
|
||||||
|
platform.platform !== "desktop" ||
|
||||||
|
import.meta.env.VITE_OPENCODE_CHANNEL !== "beta" ||
|
||||||
|
settings.general.showFileTree(),
|
||||||
|
)
|
||||||
|
|
||||||
const reviewOpen = createMemo(() => isDesktop() && view().reviewPanel.opened())
|
const reviewOpen = createMemo(() => isDesktop() && view().reviewPanel.opened())
|
||||||
const fileOpen = createMemo(() => isDesktop() && shown() && layout.fileTree.opened())
|
const fileOpen = createMemo(() => isDesktop() && shown() && layout.fileTree.opened())
|
||||||
@@ -203,7 +207,7 @@ export function SessionSidePanel(props: {
|
|||||||
})
|
})
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Show when={isDesktop() && !(settings.general.newLayoutDesigns() && !params.id)}>
|
<Show when={isDesktop() && !(import.meta.env.VITE_OPENCODE_CHANNEL !== "prod" && !params.id)}>
|
||||||
<aside
|
<aside
|
||||||
id="review-panel"
|
id="review-panel"
|
||||||
aria-label={language.t("session.panel.reviewAndFiles")}
|
aria-label={language.t("session.panel.reviewAndFiles")}
|
||||||
|
|||||||
@@ -70,8 +70,10 @@ export const useSessionCommands = (actions: SessionCommandContext) => {
|
|||||||
})
|
})
|
||||||
const activeFileTab = tabState.activeFileTab
|
const activeFileTab = tabState.activeFileTab
|
||||||
const closableTab = tabState.closableTab
|
const closableTab = tabState.closableTab
|
||||||
const desktopV2 = () => platform.platform === "desktop" && settings.general.newLayoutDesigns()
|
const shown = () =>
|
||||||
const shown = () => (desktopV2() ? settings.general.showFileTree() : true)
|
platform.platform !== "desktop" ||
|
||||||
|
import.meta.env.VITE_OPENCODE_CHANNEL !== "beta" ||
|
||||||
|
settings.general.showFileTree()
|
||||||
|
|
||||||
const messages = () => {
|
const messages = () => {
|
||||||
const id = params.id
|
const id = params.id
|
||||||
|
|||||||
@@ -1,8 +1,6 @@
|
|||||||
import { usePlatform } from "@/context/platform"
|
import { usePlatform } from "@/context/platform"
|
||||||
import { ServerConnection } from "@/context/server"
|
import type { ServerConnection } from "@/context/server"
|
||||||
import { createSdkForServer } from "./server"
|
import { createSdkForServer } from "./server"
|
||||||
import { Accessor, createEffect, onCleanup } from "solid-js"
|
|
||||||
import { createStore, reconcile } from "solid-js/store"
|
|
||||||
|
|
||||||
export type ServerHealth = { healthy: boolean; version?: string }
|
export type ServerHealth = { healthy: boolean; version?: string }
|
||||||
|
|
||||||
@@ -94,8 +92,6 @@ export async function checkServerHealth(
|
|||||||
return attempt(0).finally(() => timeout?.clear?.())
|
return attempt(0).finally(() => timeout?.clear?.())
|
||||||
}
|
}
|
||||||
|
|
||||||
const pollMs = 10_000
|
|
||||||
|
|
||||||
export function useCheckServerHealth() {
|
export function useCheckServerHealth() {
|
||||||
const platform = usePlatform()
|
const platform = usePlatform()
|
||||||
const fetcher = platform.fetch ?? globalThis.fetch
|
const fetcher = platform.fetch ?? globalThis.fetch
|
||||||
@@ -115,37 +111,3 @@ export function useCheckServerHealth() {
|
|||||||
return promise
|
return promise
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const useServerHealth = (servers: Accessor<ServerConnection.Any[]>, enabled: Accessor<boolean>) => {
|
|
||||||
const checkServerHealth = useCheckServerHealth()
|
|
||||||
const [status, setStatus] = createStore({} as Record<ServerConnection.Key, ServerHealth | undefined>)
|
|
||||||
|
|
||||||
createEffect(() => {
|
|
||||||
if (!enabled()) {
|
|
||||||
setStatus(reconcile({}))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
const list = servers()
|
|
||||||
let dead = false
|
|
||||||
|
|
||||||
const refresh = async () => {
|
|
||||||
const results: Record<string, ServerHealth> = {}
|
|
||||||
await Promise.all(
|
|
||||||
list.map(async (conn) => {
|
|
||||||
results[ServerConnection.key(conn)] = await checkServerHealth(conn.http)
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
if (dead) return
|
|
||||||
setStatus(reconcile(results))
|
|
||||||
}
|
|
||||||
|
|
||||||
void refresh()
|
|
||||||
const id = setInterval(() => void refresh(), pollMs)
|
|
||||||
onCleanup(() => {
|
|
||||||
dead = true
|
|
||||||
clearInterval(id)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
return status
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@opencode-ai/console-app",
|
"name": "@opencode-ai/console-app",
|
||||||
"version": "1.15.11",
|
"version": "1.15.10",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
@@ -26,7 +26,6 @@
|
|||||||
"@solidjs/router": "catalog:",
|
"@solidjs/router": "catalog:",
|
||||||
"@solidjs/start": "catalog:",
|
"@solidjs/start": "catalog:",
|
||||||
"@stripe/stripe-js": "8.6.1",
|
"@stripe/stripe-js": "8.6.1",
|
||||||
"@upstash/redis": "1.38.0",
|
|
||||||
"chart.js": "4.5.1",
|
"chart.js": "4.5.1",
|
||||||
"nitro": "3.0.1-alpha.1",
|
"nitro": "3.0.1-alpha.1",
|
||||||
"solid-js": "catalog:",
|
"solid-js": "catalog:",
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ import { action, json, query, useAction, useSubmission } from "@solidjs/router"
|
|||||||
import { createEffect, createMemo, createSignal, For, onCleanup, Show } from "solid-js"
|
import { createEffect, createMemo, createSignal, For, onCleanup, Show } from "solid-js"
|
||||||
import { getRequestEvent } from "solid-js/web"
|
import { getRequestEvent } from "solid-js/web"
|
||||||
import { Referral } from "@opencode-ai/console-core/referral.js"
|
import { Referral } from "@opencode-ai/console-core/referral.js"
|
||||||
import { Actor } from "@opencode-ai/console-core/actor.js"
|
|
||||||
import { withActor } from "~/context/auth.withActor"
|
import { withActor } from "~/context/auth.withActor"
|
||||||
import { Modal } from "~/component/modal"
|
import { Modal } from "~/component/modal"
|
||||||
import { IconCheck, IconCopy } from "~/component/icon"
|
import { IconCheck, IconCopy } from "~/component/icon"
|
||||||
@@ -10,7 +9,6 @@ import { useI18n } from "~/context/i18n"
|
|||||||
import { useLanguage } from "~/context/language"
|
import { useLanguage } from "~/context/language"
|
||||||
import { formatResetTime, liteResetTimeKeys } from "~/lib/format-reset-time"
|
import { formatResetTime, liteResetTimeKeys } from "~/lib/format-reset-time"
|
||||||
import { queryLiteSubscription } from "~/routes/workspace/[id]/go/lite-section"
|
import { queryLiteSubscription } from "~/routes/workspace/[id]/go/lite-section"
|
||||||
import { clearReferralCookie, referralCodeFromCookieHeader } from "~/lib/referral-invite"
|
|
||||||
import "./go-referral.css"
|
import "./go-referral.css"
|
||||||
|
|
||||||
type GoReferralSummary = Awaited<ReturnType<typeof Referral.summary>>
|
type GoReferralSummary = Awaited<ReturnType<typeof Referral.summary>>
|
||||||
@@ -27,21 +25,7 @@ const emptyUsagePreview = {
|
|||||||
|
|
||||||
export const queryGoReferral = query(async (workspaceID: string) => {
|
export const queryGoReferral = query(async (workspaceID: string) => {
|
||||||
"use server"
|
"use server"
|
||||||
return withActor(async () => {
|
return withActor(() => Referral.summary(), workspaceID)
|
||||||
const event = getRequestEvent()
|
|
||||||
const referralCode = referralCodeFromCookieHeader(event?.request.headers.get("cookie") ?? null)
|
|
||||||
if (referralCode) {
|
|
||||||
await Referral.createFromAccount({
|
|
||||||
accountID: Actor.account(),
|
|
||||||
referralCode,
|
|
||||||
}).catch((error) => {
|
|
||||||
console.error("Referral create failed", error)
|
|
||||||
})
|
|
||||||
event?.response.headers.append("set-cookie", clearReferralCookie())
|
|
||||||
}
|
|
||||||
|
|
||||||
return Referral.summary()
|
|
||||||
}, workspaceID)
|
|
||||||
}, "go.referral.get")
|
}, "go.referral.get")
|
||||||
|
|
||||||
export const queryGoReferralUsagePreview = query(async (workspaceID: string, referralID?: string) => {
|
export const queryGoReferralUsagePreview = query(async (workspaceID: string, referralID?: string) => {
|
||||||
@@ -81,8 +65,6 @@ function rewardDescriptionKey(source: GoReferralReward["source"]) {
|
|||||||
|
|
||||||
function rewardActionKey(reward: GoReferralReward, hasActiveGo: boolean) {
|
function rewardActionKey(reward: GoReferralReward, hasActiveGo: boolean) {
|
||||||
if (reward.status === "applied") return "workspace.referral.reward.action.applied" as const
|
if (reward.status === "applied") return "workspace.referral.reward.action.applied" as const
|
||||||
if (reward.status === "pending" && reward.source === "inviter")
|
|
||||||
return "workspace.referral.reward.source.pendingInviter" as const
|
|
||||||
if (reward.status === "pending" || !hasActiveGo) return "workspace.referral.reward.action.subscribeUnlock" as const
|
if (reward.status === "pending" || !hasActiveGo) return "workspace.referral.reward.action.subscribeUnlock" as const
|
||||||
return "workspace.referral.reward.action.view" as const
|
return "workspace.referral.reward.action.view" as const
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -249,7 +249,7 @@ export const dict = {
|
|||||||
|
|
||||||
"go.title": "OpenCode Go | نماذج برمجة منخفضة التكلفة للجميع",
|
"go.title": "OpenCode Go | نماذج برمجة منخفضة التكلفة للجميع",
|
||||||
"go.meta.description":
|
"go.meta.description":
|
||||||
"يبدأ Go من $5 للشهر الأول، ثم $10/شهر، مع حدود طلب سخية لمدة 5 ساعات لـ GLM-5.1 وGLM-5 وKimi K2.5 وKimi K2.6 وMiMo-V2.5-Pro وMiMo-V2.5 وQwen3.7 Max وQwen3.6 Plus وQwen3.5 Plus وMiniMax M2.5 وMiniMax M2.7 وDeepSeek V4 Pro وDeepSeek V4 Flash.",
|
"يبدأ Go من $5 للشهر الأول، ثم $10/شهر، مع حدود طلب سخية لمدة 5 ساعات لـ GLM-5.1 وGLM-5 وKimi K2.5 وKimi K2.6 وMiMo-V2.5-Pro وMiMo-V2.5 وQwen3.5 Plus وQwen3.6 Plus وMiniMax M2.5 وMiniMax M2.7 وDeepSeek V4 Pro وDeepSeek V4 Flash.",
|
||||||
"go.hero.title": "نماذج برمجة منخفضة التكلفة للجميع",
|
"go.hero.title": "نماذج برمجة منخفضة التكلفة للجميع",
|
||||||
"go.hero.body":
|
"go.hero.body":
|
||||||
"يجلب Go البرمجة الوكيلة للمبرمجين حول العالم. يوفر حدودًا سخية ووصولًا موثوقًا إلى أقوى النماذج مفتوحة المصدر، حتى تتمكن من البناء باستخدام وكلاء أقوياء دون القلق بشأن التكلفة أو التوفر.",
|
"يجلب Go البرمجة الوكيلة للمبرمجين حول العالم. يوفر حدودًا سخية ووصولًا موثوقًا إلى أقوى النماذج مفتوحة المصدر، حتى تتمكن من البناء باستخدام وكلاء أقوياء دون القلق بشأن التكلفة أو التوفر.",
|
||||||
@@ -298,7 +298,7 @@ export const dict = {
|
|||||||
"go.problem.item2": "حدود سخية ووصول موثوق",
|
"go.problem.item2": "حدود سخية ووصول موثوق",
|
||||||
"go.problem.item3": "مصمم لأكبر عدد ممكن من المبرمجين",
|
"go.problem.item3": "مصمم لأكبر عدد ممكن من المبرمجين",
|
||||||
"go.problem.item4":
|
"go.problem.item4":
|
||||||
"يتضمن GLM-5.1 وGLM-5 وKimi K2.5 وKimi K2.6 وMiMo-V2.5-Pro وMiMo-V2.5 وQwen3.7 Max وQwen3.6 Plus وQwen3.5 Plus وMiniMax M2.5 وMiniMax M2.7 وDeepSeek V4 Pro وDeepSeek V4 Flash",
|
"يتضمن GLM-5.1 وGLM-5 وKimi K2.5 وKimi K2.6 وMiMo-V2.5-Pro وMiMo-V2.5 وQwen3.5 Plus وQwen3.6 Plus وMiniMax M2.5 وMiniMax M2.7 وDeepSeek V4 Pro وDeepSeek V4 Flash",
|
||||||
"go.how.title": "كيف يعمل Go",
|
"go.how.title": "كيف يعمل Go",
|
||||||
"go.how.body": "يبدأ Go من $5 للشهر الأول، ثم $10/شهر. يمكنك استخدامه مع OpenCode أو أي وكيل.",
|
"go.how.body": "يبدأ Go من $5 للشهر الأول، ثم $10/شهر. يمكنك استخدامه مع OpenCode أو أي وكيل.",
|
||||||
"go.how.step1.title": "أنشئ حسابًا",
|
"go.how.step1.title": "أنشئ حسابًا",
|
||||||
@@ -322,7 +322,7 @@ export const dict = {
|
|||||||
"go.faq.a2": "يتضمن Go النماذج المدرجة أدناه، مع حدود سخية وإتاحة موثوقة.",
|
"go.faq.a2": "يتضمن Go النماذج المدرجة أدناه، مع حدود سخية وإتاحة موثوقة.",
|
||||||
"go.faq.q3": "هل Go هو نفسه Zen؟",
|
"go.faq.q3": "هل Go هو نفسه Zen؟",
|
||||||
"go.faq.a3":
|
"go.faq.a3":
|
||||||
"لا. Zen هو الدفع حسب الاستخدام، بينما يبدأ Go من $5 للشهر الأول، ثم $10/شهر، مع حدود سخية ووصول موثوق إلى نماذج المصدر المفتوح GLM-5.1 وGLM-5 وKimi K2.5 وKimi K2.6 وMiMo-V2.5-Pro وMiMo-V2.5 وQwen3.7 Max وQwen3.6 Plus وQwen3.5 Plus وMiniMax M2.5 وMiniMax M2.7 وDeepSeek V4 Pro وDeepSeek V4 Flash.",
|
"لا. Zen هو الدفع حسب الاستخدام، بينما يبدأ Go من $5 للشهر الأول، ثم $10/شهر، مع حدود سخية ووصول موثوق إلى نماذج المصدر المفتوح GLM-5.1 وGLM-5 وKimi K2.5 وKimi K2.6 وMiMo-V2.5-Pro وMiMo-V2.5 وQwen3.5 Plus وQwen3.6 Plus وMiniMax M2.5 وMiniMax M2.7 وDeepSeek V4 Pro وDeepSeek V4 Flash.",
|
||||||
"go.faq.q4": "كم تكلفة Go؟",
|
"go.faq.q4": "كم تكلفة Go؟",
|
||||||
"go.faq.a4.p1.beforePricing": "تكلفة Go",
|
"go.faq.a4.p1.beforePricing": "تكلفة Go",
|
||||||
"go.faq.a4.p1.pricingLink": "$5 للشهر الأول",
|
"go.faq.a4.p1.pricingLink": "$5 للشهر الأول",
|
||||||
@@ -345,7 +345,7 @@ export const dict = {
|
|||||||
|
|
||||||
"go.faq.q9": "ما الفرق بين النماذج المجانية وGo؟",
|
"go.faq.q9": "ما الفرق بين النماذج المجانية وGo؟",
|
||||||
"go.faq.a9":
|
"go.faq.a9":
|
||||||
"تشمل النماذج المجانية Big Pickle بالإضافة إلى النماذج الترويجية المتاحة في ذلك الوقت، مع حصة 200 طلب/يوم. يتضمن Go نماذج GLM-5.1 وGLM-5 وKimi K2.5 وKimi K2.6 وMiMo-V2.5-Pro وMiMo-V2.5 وQwen3.7 Max وQwen3.6 Plus وQwen3.5 Plus وMiniMax M2.5 وMiniMax M2.7 وDeepSeek V4 Pro وDeepSeek V4 Flash مع حصص طلبات أعلى مطبقة عبر نوافذ متجددة (5 ساعات، أسبوعيًا، وشهريًا)، تعادل تقريبًا 12 دولارًا كل 5 ساعات، و30 دولارًا في الأسبوع، و60 دولارًا في الشهر (تختلف أعداد الطلبات الفعلية حسب النموذج والاستخدام).",
|
"تشمل النماذج المجانية Big Pickle بالإضافة إلى النماذج الترويجية المتاحة في ذلك الوقت، مع حصة 200 طلب/يوم. يتضمن Go نماذج GLM-5.1 وGLM-5 وKimi K2.5 وKimi K2.6 وMiMo-V2.5-Pro وMiMo-V2.5 وQwen3.5 Plus وQwen3.6 Plus وMiniMax M2.5 وMiniMax M2.7 وDeepSeek V4 Pro وDeepSeek V4 Flash مع حصص طلبات أعلى مطبقة عبر نوافذ متجددة (5 ساعات، أسبوعيًا، وشهريًا)، تعادل تقريبًا 12 دولارًا كل 5 ساعات، و30 دولارًا في الأسبوع، و60 دولارًا في الشهر (تختلف أعداد الطلبات الفعلية حسب النموذج والاستخدام).",
|
||||||
|
|
||||||
"zen.api.error.rateLimitExceeded": "تم تجاوز حد الطلبات. يرجى المحاولة مرة أخرى لاحقًا.",
|
"zen.api.error.rateLimitExceeded": "تم تجاوز حد الطلبات. يرجى المحاولة مرة أخرى لاحقًا.",
|
||||||
"zen.api.error.modelNotSupported": "النموذج {{model}} غير مدعوم",
|
"zen.api.error.modelNotSupported": "النموذج {{model}} غير مدعوم",
|
||||||
|
|||||||
@@ -253,7 +253,7 @@ export const dict = {
|
|||||||
|
|
||||||
"go.title": "OpenCode Go | Modelos de codificação de baixo custo para todos",
|
"go.title": "OpenCode Go | Modelos de codificação de baixo custo para todos",
|
||||||
"go.meta.description":
|
"go.meta.description":
|
||||||
"O Go começa em $5 no primeiro mês, depois $10/mês, com limites generosos de solicitação de 5 horas para GLM-5.1, GLM-5, Kimi K2.5, Kimi K2.6, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.7 Max, Qwen3.6 Plus, Qwen3.5 Plus, MiniMax M2.5, MiniMax M2.7, DeepSeek V4 Pro e DeepSeek V4 Flash.",
|
"O Go começa em $5 no primeiro mês, depois $10/mês, com limites generosos de solicitação de 5 horas para GLM-5.1, GLM-5, Kimi K2.5, Kimi K2.6, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.5 Plus, Qwen3.6 Plus, MiniMax M2.5, MiniMax M2.7, DeepSeek V4 Pro e DeepSeek V4 Flash.",
|
||||||
"go.hero.title": "Modelos de codificação de baixo custo para todos",
|
"go.hero.title": "Modelos de codificação de baixo custo para todos",
|
||||||
"go.hero.body":
|
"go.hero.body":
|
||||||
"O Go traz a codificação com agentes para programadores em todo o mundo. Oferecendo limites generosos e acesso confiável aos modelos de código aberto mais capazes, para que você possa construir com agentes poderosos sem se preocupar com custos ou disponibilidade.",
|
"O Go traz a codificação com agentes para programadores em todo o mundo. Oferecendo limites generosos e acesso confiável aos modelos de código aberto mais capazes, para que você possa construir com agentes poderosos sem se preocupar com custos ou disponibilidade.",
|
||||||
@@ -303,7 +303,7 @@ export const dict = {
|
|||||||
"go.problem.item2": "Limites generosos e acesso confiável",
|
"go.problem.item2": "Limites generosos e acesso confiável",
|
||||||
"go.problem.item3": "Feito para o maior número possível de programadores",
|
"go.problem.item3": "Feito para o maior número possível de programadores",
|
||||||
"go.problem.item4":
|
"go.problem.item4":
|
||||||
"Inclui GLM-5.1, GLM-5, Kimi K2.5, Kimi K2.6, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.7 Max, Qwen3.6 Plus, Qwen3.5 Plus, MiniMax M2.5, MiniMax M2.7, DeepSeek V4 Pro e DeepSeek V4 Flash",
|
"Inclui GLM-5.1, GLM-5, Kimi K2.5, Kimi K2.6, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.5 Plus, Qwen3.6 Plus, MiniMax M2.5, MiniMax M2.7, DeepSeek V4 Pro e DeepSeek V4 Flash",
|
||||||
"go.how.title": "Como o Go funciona",
|
"go.how.title": "Como o Go funciona",
|
||||||
"go.how.body":
|
"go.how.body":
|
||||||
"O Go começa em $5 no primeiro mês, depois $10/mês. Você pode usá-lo com o OpenCode ou qualquer agente.",
|
"O Go começa em $5 no primeiro mês, depois $10/mês. Você pode usá-lo com o OpenCode ou qualquer agente.",
|
||||||
@@ -329,7 +329,7 @@ export const dict = {
|
|||||||
"go.faq.a2": "O Go inclui os modelos listados abaixo, com limites generosos e acesso confiável.",
|
"go.faq.a2": "O Go inclui os modelos listados abaixo, com limites generosos e acesso confiável.",
|
||||||
"go.faq.q3": "O Go é o mesmo que o Zen?",
|
"go.faq.q3": "O Go é o mesmo que o Zen?",
|
||||||
"go.faq.a3":
|
"go.faq.a3":
|
||||||
"Não. Zen é pay-as-you-go, enquanto o Go começa em $5 no primeiro mês, depois $10/mês, com limites generosos e acesso confiável aos modelos open source GLM-5.1, GLM-5, Kimi K2.5, Kimi K2.6, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.7 Max, Qwen3.6 Plus, Qwen3.5 Plus, MiniMax M2.5, MiniMax M2.7, DeepSeek V4 Pro e DeepSeek V4 Flash.",
|
"Não. Zen é pay-as-you-go, enquanto o Go começa em $5 no primeiro mês, depois $10/mês, com limites generosos e acesso confiável aos modelos open source GLM-5.1, GLM-5, Kimi K2.5, Kimi K2.6, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.5 Plus, Qwen3.6 Plus, MiniMax M2.5, MiniMax M2.7, DeepSeek V4 Pro e DeepSeek V4 Flash.",
|
||||||
"go.faq.q4": "Quanto custa o Go?",
|
"go.faq.q4": "Quanto custa o Go?",
|
||||||
"go.faq.a4.p1.beforePricing": "O Go custa",
|
"go.faq.a4.p1.beforePricing": "O Go custa",
|
||||||
"go.faq.a4.p1.pricingLink": "$5 no primeiro mês",
|
"go.faq.a4.p1.pricingLink": "$5 no primeiro mês",
|
||||||
@@ -353,7 +353,7 @@ export const dict = {
|
|||||||
|
|
||||||
"go.faq.q9": "Qual a diferença entre os modelos gratuitos e o Go?",
|
"go.faq.q9": "Qual a diferença entre os modelos gratuitos e o Go?",
|
||||||
"go.faq.a9":
|
"go.faq.a9":
|
||||||
"Os modelos gratuitos incluem Big Pickle e modelos promocionais disponíveis no momento, com uma cota de 200 requisições/dia. O Go inclui GLM-5.1, GLM-5, Kimi K2.5, Kimi K2.6, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.7 Max, Qwen3.6 Plus, Qwen3.5 Plus, MiniMax M2.5, MiniMax M2.7, DeepSeek V4 Pro e DeepSeek V4 Flash com cotas de requisição mais altas aplicadas em janelas móveis (5 horas, semanal e mensal), aproximadamente equivalentes a $12 por 5 horas, $30 por semana e $60 por mês (as contagens reais de requisições variam de acordo com o modelo e o uso).",
|
"Os modelos gratuitos incluem Big Pickle e modelos promocionais disponíveis no momento, com uma cota de 200 requisições/dia. O Go inclui GLM-5.1, GLM-5, Kimi K2.5, Kimi K2.6, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.5 Plus, Qwen3.6 Plus, MiniMax M2.5, MiniMax M2.7, DeepSeek V4 Pro e DeepSeek V4 Flash com cotas de requisição mais altas aplicadas em janelas móveis (5 horas, semanal e mensal), aproximadamente equivalentes a $12 por 5 horas, $30 por semana e $60 por mês (as contagens reais de requisições variam de acordo com o modelo e o uso).",
|
||||||
|
|
||||||
"zen.api.error.rateLimitExceeded": "Limite de taxa excedido. Por favor, tente novamente mais tarde.",
|
"zen.api.error.rateLimitExceeded": "Limite de taxa excedido. Por favor, tente novamente mais tarde.",
|
||||||
"zen.api.error.modelNotSupported": "Modelo {{model}} não suportado",
|
"zen.api.error.modelNotSupported": "Modelo {{model}} não suportado",
|
||||||
|
|||||||
@@ -251,7 +251,7 @@ export const dict = {
|
|||||||
|
|
||||||
"go.title": "OpenCode Go | Kodningsmodeller til lav pris for alle",
|
"go.title": "OpenCode Go | Kodningsmodeller til lav pris for alle",
|
||||||
"go.meta.description":
|
"go.meta.description":
|
||||||
"Go starter ved $5 for den første måned, derefter $10/måned, med generøse 5-timers anmodningsgrænser for GLM-5.1, GLM-5, Kimi K2.5, Kimi K2.6, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.7 Max, Qwen3.6 Plus, Qwen3.5 Plus, MiniMax M2.5, MiniMax M2.7, DeepSeek V4 Pro og DeepSeek V4 Flash.",
|
"Go starter ved $5 for den første måned, derefter $10/måned, med generøse 5-timers anmodningsgrænser for GLM-5.1, GLM-5, Kimi K2.5, Kimi K2.6, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.5 Plus, Qwen3.6 Plus, MiniMax M2.5, MiniMax M2.7, DeepSeek V4 Pro og DeepSeek V4 Flash.",
|
||||||
"go.hero.title": "Kodningsmodeller til lav pris for alle",
|
"go.hero.title": "Kodningsmodeller til lav pris for alle",
|
||||||
"go.hero.body":
|
"go.hero.body":
|
||||||
"Go bringer agentisk kodning til programmører over hele verden. Med generøse grænser og pålidelig adgang til de mest kapable open source-modeller, så du kan bygge med kraftfulde agenter uden at bekymre dig om omkostninger eller tilgængelighed.",
|
"Go bringer agentisk kodning til programmører over hele verden. Med generøse grænser og pålidelig adgang til de mest kapable open source-modeller, så du kan bygge med kraftfulde agenter uden at bekymre dig om omkostninger eller tilgængelighed.",
|
||||||
@@ -300,7 +300,7 @@ export const dict = {
|
|||||||
"go.problem.item2": "Generøse grænser og pålidelig adgang",
|
"go.problem.item2": "Generøse grænser og pålidelig adgang",
|
||||||
"go.problem.item3": "Bygget til så mange programmører som muligt",
|
"go.problem.item3": "Bygget til så mange programmører som muligt",
|
||||||
"go.problem.item4":
|
"go.problem.item4":
|
||||||
"Inkluderer GLM-5.1, GLM-5, Kimi K2.5, Kimi K2.6, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.7 Max, Qwen3.6 Plus, Qwen3.5 Plus, MiniMax M2.5, MiniMax M2.7, DeepSeek V4 Pro og DeepSeek V4 Flash",
|
"Inkluderer GLM-5.1, GLM-5, Kimi K2.5, Kimi K2.6, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.5 Plus, Qwen3.6 Plus, MiniMax M2.5, MiniMax M2.7, DeepSeek V4 Pro og DeepSeek V4 Flash",
|
||||||
"go.how.title": "Hvordan Go virker",
|
"go.how.title": "Hvordan Go virker",
|
||||||
"go.how.body":
|
"go.how.body":
|
||||||
"Go starter ved $5 for den første måned, derefter $10/måned. Du kan bruge det med OpenCode eller enhver agent.",
|
"Go starter ved $5 for den første måned, derefter $10/måned. Du kan bruge det med OpenCode eller enhver agent.",
|
||||||
@@ -326,7 +326,7 @@ export const dict = {
|
|||||||
"go.faq.a2": "Go inkluderer modellerne nedenfor med generøse grænser og pålidelig adgang.",
|
"go.faq.a2": "Go inkluderer modellerne nedenfor med generøse grænser og pålidelig adgang.",
|
||||||
"go.faq.q3": "Er Go det samme som Zen?",
|
"go.faq.q3": "Er Go det samme som Zen?",
|
||||||
"go.faq.a3":
|
"go.faq.a3":
|
||||||
"Nej. Zen er pay-as-you-go, mens Go starter ved $5 for den første måned, derefter $10/måned, med generøse grænser og pålidelig adgang til open source-modellerne GLM-5.1, GLM-5, Kimi K2.5, Kimi K2.6, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.7 Max, Qwen3.6 Plus, Qwen3.5 Plus, MiniMax M2.5, MiniMax M2.7, DeepSeek V4 Pro og DeepSeek V4 Flash.",
|
"Nej. Zen er pay-as-you-go, mens Go starter ved $5 for den første måned, derefter $10/måned, med generøse grænser og pålidelig adgang til open source-modellerne GLM-5.1, GLM-5, Kimi K2.5, Kimi K2.6, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.5 Plus, Qwen3.6 Plus, MiniMax M2.5, MiniMax M2.7, DeepSeek V4 Pro og DeepSeek V4 Flash.",
|
||||||
"go.faq.q4": "Hvad koster Go?",
|
"go.faq.q4": "Hvad koster Go?",
|
||||||
"go.faq.a4.p1.beforePricing": "Go koster",
|
"go.faq.a4.p1.beforePricing": "Go koster",
|
||||||
"go.faq.a4.p1.pricingLink": "$5 første måned",
|
"go.faq.a4.p1.pricingLink": "$5 første måned",
|
||||||
@@ -349,7 +349,7 @@ export const dict = {
|
|||||||
|
|
||||||
"go.faq.q9": "Hvad er forskellen på gratis modeller og Go?",
|
"go.faq.q9": "Hvad er forskellen på gratis modeller og Go?",
|
||||||
"go.faq.a9":
|
"go.faq.a9":
|
||||||
"Gratis modeller inkluderer Big Pickle plus salgsfremmende modeller tilgængelige på det tidspunkt, med en kvote på 200 forespørgsler/dag. Go inkluderer GLM-5.1, GLM-5, Kimi K2.5, Kimi K2.6, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.7 Max, Qwen3.6 Plus, Qwen3.5 Plus, MiniMax M2.5, MiniMax M2.7, DeepSeek V4 Pro og DeepSeek V4 Flash med højere anmodningskvoter håndhævet over rullende vinduer (5-timers, ugentlig og månedlig), nogenlunde svarende til $12 pr. 5 timer, $30 pr. uge og $60 pr. måned (faktiske anmodningstal varierer efter model og brug).",
|
"Gratis modeller inkluderer Big Pickle plus salgsfremmende modeller tilgængelige på det tidspunkt, med en kvote på 200 forespørgsler/dag. Go inkluderer GLM-5.1, GLM-5, Kimi K2.5, Kimi K2.6, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.5 Plus, Qwen3.6 Plus, MiniMax M2.5, MiniMax M2.7, DeepSeek V4 Pro og DeepSeek V4 Flash med højere anmodningskvoter håndhævet over rullende vinduer (5-timers, ugentlig og månedlig), nogenlunde svarende til $12 pr. 5 timer, $30 pr. uge og $60 pr. måned (faktiske anmodningstal varierer efter model og brug).",
|
||||||
|
|
||||||
"zen.api.error.rateLimitExceeded": "Hastighedsgrænse overskredet. Prøv venligst igen senere.",
|
"zen.api.error.rateLimitExceeded": "Hastighedsgrænse overskredet. Prøv venligst igen senere.",
|
||||||
"zen.api.error.modelNotSupported": "Model {{model}} understøttes ikke",
|
"zen.api.error.modelNotSupported": "Model {{model}} understøttes ikke",
|
||||||
|
|||||||
@@ -253,7 +253,7 @@ export const dict = {
|
|||||||
|
|
||||||
"go.title": "OpenCode Go | Kostengünstige Coding-Modelle für alle",
|
"go.title": "OpenCode Go | Kostengünstige Coding-Modelle für alle",
|
||||||
"go.meta.description":
|
"go.meta.description":
|
||||||
"Go beginnt bei $5 für den ersten Monat, danach $10/Monat, mit großzügigen 5-Stunden-Anfragelimits für GLM-5.1, GLM-5, Kimi K2.5, Kimi K2.6, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.7 Max, Qwen3.6 Plus, Qwen3.5 Plus, MiniMax M2.5, MiniMax M2.7, DeepSeek V4 Pro und DeepSeek V4 Flash.",
|
"Go beginnt bei $5 für den ersten Monat, danach $10/Monat, mit großzügigen 5-Stunden-Anfragelimits für GLM-5.1, GLM-5, Kimi K2.5, Kimi K2.6, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.5 Plus, Qwen3.6 Plus, MiniMax M2.5, MiniMax M2.7, DeepSeek V4 Pro und DeepSeek V4 Flash.",
|
||||||
"go.hero.title": "Kostengünstige Coding-Modelle für alle",
|
"go.hero.title": "Kostengünstige Coding-Modelle für alle",
|
||||||
"go.hero.body":
|
"go.hero.body":
|
||||||
"Go bringt Agentic Coding zu Programmierern auf der ganzen Welt. Mit großzügigen Limits und zuverlässigem Zugang zu den leistungsfähigsten Open-Source-Modellen, damit du mit leistungsstarken Agenten entwickeln kannst, ohne dir Gedanken über Kosten oder Verfügbarkeit zu machen.",
|
"Go bringt Agentic Coding zu Programmierern auf der ganzen Welt. Mit großzügigen Limits und zuverlässigem Zugang zu den leistungsfähigsten Open-Source-Modellen, damit du mit leistungsstarken Agenten entwickeln kannst, ohne dir Gedanken über Kosten oder Verfügbarkeit zu machen.",
|
||||||
@@ -302,7 +302,7 @@ export const dict = {
|
|||||||
"go.problem.item2": "Großzügige Limits und zuverlässiger Zugang",
|
"go.problem.item2": "Großzügige Limits und zuverlässiger Zugang",
|
||||||
"go.problem.item3": "Für so viele Programmierer wie möglich gebaut",
|
"go.problem.item3": "Für so viele Programmierer wie möglich gebaut",
|
||||||
"go.problem.item4":
|
"go.problem.item4":
|
||||||
"Beinhaltet GLM-5.1, GLM-5, Kimi K2.5, Kimi K2.6, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.7 Max, Qwen3.6 Plus, Qwen3.5 Plus, MiniMax M2.5, MiniMax M2.7, DeepSeek V4 Pro und DeepSeek V4 Flash",
|
"Beinhaltet GLM-5.1, GLM-5, Kimi K2.5, Kimi K2.6, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.5 Plus, Qwen3.6 Plus, MiniMax M2.5, MiniMax M2.7, DeepSeek V4 Pro und DeepSeek V4 Flash",
|
||||||
"go.how.title": "Wie Go funktioniert",
|
"go.how.title": "Wie Go funktioniert",
|
||||||
"go.how.body":
|
"go.how.body":
|
||||||
"Go beginnt bei $5 für den ersten Monat, danach $10/Monat. Du kannst es mit OpenCode oder jedem Agenten nutzen.",
|
"Go beginnt bei $5 für den ersten Monat, danach $10/Monat. Du kannst es mit OpenCode oder jedem Agenten nutzen.",
|
||||||
@@ -328,7 +328,7 @@ export const dict = {
|
|||||||
"go.faq.a2": "Go umfasst die unten aufgeführten Modelle mit großzügigen Limits und zuverlässigem Zugriff.",
|
"go.faq.a2": "Go umfasst die unten aufgeführten Modelle mit großzügigen Limits und zuverlässigem Zugriff.",
|
||||||
"go.faq.q3": "Ist Go dasselbe wie Zen?",
|
"go.faq.q3": "Ist Go dasselbe wie Zen?",
|
||||||
"go.faq.a3":
|
"go.faq.a3":
|
||||||
"Nein. Zen ist Pay-as-you-go, während Go bei $5 für den ersten Monat beginnt, danach $10/Monat, mit großzügigen Limits und zuverlässigem Zugang zu den Open-Source-Modellen GLM-5.1, GLM-5, Kimi K2.5, Kimi K2.6, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.7 Max, Qwen3.6 Plus, Qwen3.5 Plus, MiniMax M2.5, MiniMax M2.7, DeepSeek V4 Pro und DeepSeek V4 Flash.",
|
"Nein. Zen ist Pay-as-you-go, während Go bei $5 für den ersten Monat beginnt, danach $10/Monat, mit großzügigen Limits und zuverlässigem Zugang zu den Open-Source-Modellen GLM-5.1, GLM-5, Kimi K2.5, Kimi K2.6, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.5 Plus, Qwen3.6 Plus, MiniMax M2.5, MiniMax M2.7, DeepSeek V4 Pro und DeepSeek V4 Flash.",
|
||||||
"go.faq.q4": "Wie viel kostet Go?",
|
"go.faq.q4": "Wie viel kostet Go?",
|
||||||
"go.faq.a4.p1.beforePricing": "Go kostet",
|
"go.faq.a4.p1.beforePricing": "Go kostet",
|
||||||
"go.faq.a4.p1.pricingLink": "$5 im ersten Monat",
|
"go.faq.a4.p1.pricingLink": "$5 im ersten Monat",
|
||||||
@@ -352,7 +352,7 @@ export const dict = {
|
|||||||
|
|
||||||
"go.faq.q9": "Was ist der Unterschied zwischen kostenlosen Modellen und Go?",
|
"go.faq.q9": "Was ist der Unterschied zwischen kostenlosen Modellen und Go?",
|
||||||
"go.faq.a9":
|
"go.faq.a9":
|
||||||
"Kostenlose Modelle beinhalten Big Pickle sowie Werbemodelle, die zum jeweiligen Zeitpunkt verfügbar sind, mit einem Kontingent von 200 Anfragen/Tag. Go beinhaltet GLM-5.1, GLM-5, Kimi K2.5, Kimi K2.6, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.7 Max, Qwen3.6 Plus, Qwen3.5 Plus, MiniMax M2.5, MiniMax M2.7, DeepSeek V4 Pro und DeepSeek V4 Flash mit höheren Anfragekontingenten, die über rollierende Zeitfenster (5 Stunden, wöchentlich und monatlich) durchgesetzt werden, grob äquivalent zu $12 pro 5 Stunden, $30 pro Woche und $60 pro Monat (tatsächliche Anfragezahlen variieren je nach Modell und Nutzung).",
|
"Kostenlose Modelle beinhalten Big Pickle sowie Werbemodelle, die zum jeweiligen Zeitpunkt verfügbar sind, mit einem Kontingent von 200 Anfragen/Tag. Go beinhaltet GLM-5.1, GLM-5, Kimi K2.5, Kimi K2.6, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.5 Plus, Qwen3.6 Plus, MiniMax M2.5, MiniMax M2.7, DeepSeek V4 Pro und DeepSeek V4 Flash mit höheren Anfragekontingenten, die über rollierende Zeitfenster (5 Stunden, wöchentlich und monatlich) durchgesetzt werden, grob äquivalent zu $12 pro 5 Stunden, $30 pro Woche und $60 pro Monat (tatsächliche Anfragezahlen variieren je nach Modell und Nutzung).",
|
||||||
|
|
||||||
"zen.api.error.rateLimitExceeded": "Ratenlimit überschritten. Bitte versuche es später erneut.",
|
"zen.api.error.rateLimitExceeded": "Ratenlimit überschritten. Bitte versuche es später erneut.",
|
||||||
"zen.api.error.modelNotSupported": "Modell {{model}} wird nicht unterstützt",
|
"zen.api.error.modelNotSupported": "Modell {{model}} wird nicht unterstützt",
|
||||||
|
|||||||
@@ -248,7 +248,7 @@ export const dict = {
|
|||||||
|
|
||||||
"go.title": "OpenCode Go | Low cost coding models for everyone",
|
"go.title": "OpenCode Go | Low cost coding models for everyone",
|
||||||
"go.meta.description":
|
"go.meta.description":
|
||||||
"Go starts at $5 for your first month, then $10/month, with generous 5-hour request limits for GLM-5.1, GLM-5, Kimi K2.5, Kimi K2.6, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.7 Max, Qwen3.6 Plus, Qwen3.5 Plus, MiniMax M2.5, MiniMax M2.7, DeepSeek V4 Pro, and DeepSeek V4 Flash.",
|
"Go starts at $5 for your first month, then $10/month, with generous 5-hour request limits for GLM-5.1, GLM-5, Kimi K2.5, Kimi K2.6, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.5 Plus, Qwen3.6 Plus, MiniMax M2.5, MiniMax M2.7, DeepSeek V4 Pro, and DeepSeek V4 Flash.",
|
||||||
"go.hero.title": "Low cost coding models for everyone",
|
"go.hero.title": "Low cost coding models for everyone",
|
||||||
"go.hero.body":
|
"go.hero.body":
|
||||||
"Go brings agentic coding to programmers around the world. Offering generous limits and reliable access to the most capable open-source models, so you can build with powerful agents without worrying about cost or availability.",
|
"Go brings agentic coding to programmers around the world. Offering generous limits and reliable access to the most capable open-source models, so you can build with powerful agents without worrying about cost or availability.",
|
||||||
@@ -296,7 +296,7 @@ export const dict = {
|
|||||||
"go.problem.item2": "Generous limits and reliable access",
|
"go.problem.item2": "Generous limits and reliable access",
|
||||||
"go.problem.item3": "Built for as many programmers as possible",
|
"go.problem.item3": "Built for as many programmers as possible",
|
||||||
"go.problem.item4":
|
"go.problem.item4":
|
||||||
"Includes GLM-5.1, GLM-5, Kimi K2.5, Kimi K2.6, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.7 Max, Qwen3.6 Plus, Qwen3.5 Plus, MiniMax M2.5, MiniMax M2.7, DeepSeek V4 Pro, and DeepSeek V4 Flash",
|
"Includes GLM-5.1, GLM-5, Kimi K2.5, Kimi K2.6, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.5 Plus, Qwen3.6 Plus, MiniMax M2.5, MiniMax M2.7, DeepSeek V4 Pro, and DeepSeek V4 Flash",
|
||||||
"go.how.title": "How Go works",
|
"go.how.title": "How Go works",
|
||||||
"go.how.body": "Go starts at $5 for your first month, then $10/month. You can use it with OpenCode or any agent.",
|
"go.how.body": "Go starts at $5 for your first month, then $10/month. You can use it with OpenCode or any agent.",
|
||||||
"go.how.step1.title": "Create an account",
|
"go.how.step1.title": "Create an account",
|
||||||
@@ -321,7 +321,7 @@ export const dict = {
|
|||||||
"go.faq.a2": "Go includes the models listed below, with generous limits and reliable access.",
|
"go.faq.a2": "Go includes the models listed below, with generous limits and reliable access.",
|
||||||
"go.faq.q3": "Is Go the same as Zen?",
|
"go.faq.q3": "Is Go the same as Zen?",
|
||||||
"go.faq.a3":
|
"go.faq.a3":
|
||||||
"No. Zen is pay-as-you-go, while Go starts at $5 for your first month, then $10/month, with generous limits and reliable access to open-source models GLM-5.1, GLM-5, Kimi K2.5, Kimi K2.6, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.7 Max, Qwen3.6 Plus, Qwen3.5 Plus, MiniMax M2.5, MiniMax M2.7, DeepSeek V4 Pro, and DeepSeek V4 Flash.",
|
"No. Zen is pay-as-you-go, while Go starts at $5 for your first month, then $10/month, with generous limits and reliable access to open-source models GLM-5.1, GLM-5, Kimi K2.5, Kimi K2.6, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.5 Plus, Qwen3.6 Plus, MiniMax M2.5, MiniMax M2.7, DeepSeek V4 Pro, and DeepSeek V4 Flash.",
|
||||||
"go.faq.q4": "How much does Go cost?",
|
"go.faq.q4": "How much does Go cost?",
|
||||||
"go.faq.a4.p1.beforePricing": "Go costs",
|
"go.faq.a4.p1.beforePricing": "Go costs",
|
||||||
"go.faq.a4.p1.pricingLink": "$5 first month",
|
"go.faq.a4.p1.pricingLink": "$5 first month",
|
||||||
@@ -345,7 +345,7 @@ export const dict = {
|
|||||||
|
|
||||||
"go.faq.q9": "What is the difference between free models and Go?",
|
"go.faq.q9": "What is the difference between free models and Go?",
|
||||||
"go.faq.a9":
|
"go.faq.a9":
|
||||||
"Free models include Big Pickle plus promotional models available at the time, with a quota of 200 requests/day. Go includes GLM-5.1, GLM-5, Kimi K2.5, Kimi K2.6, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.7 Max, Qwen3.6 Plus, Qwen3.5 Plus, MiniMax M2.5, MiniMax M2.7, DeepSeek V4 Pro, and DeepSeek V4 Flash with higher request quotas enforced across rolling windows (5-hour, weekly, and monthly), roughly equivalent to $12 per 5 hours, $30 per week, and $60 per month (actual request counts vary by model and usage).",
|
"Free models include Big Pickle plus promotional models available at the time, with a quota of 200 requests/day. Go includes GLM-5.1, GLM-5, Kimi K2.5, Kimi K2.6, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.5 Plus, Qwen3.6 Plus, MiniMax M2.5, MiniMax M2.7, DeepSeek V4 Pro, and DeepSeek V4 Flash with higher request quotas enforced across rolling windows (5-hour, weekly, and monthly), roughly equivalent to $12 per 5 hours, $30 per week, and $60 per month (actual request counts vary by model and usage).",
|
||||||
|
|
||||||
"zen.api.error.rateLimitExceeded": "Rate limit exceeded. Please try again later.",
|
"zen.api.error.rateLimitExceeded": "Rate limit exceeded. Please try again later.",
|
||||||
"zen.api.error.modelNotSupported": "Model {{model}} is not supported",
|
"zen.api.error.modelNotSupported": "Model {{model}} is not supported",
|
||||||
|
|||||||
@@ -254,7 +254,7 @@ export const dict = {
|
|||||||
|
|
||||||
"go.title": "OpenCode Go | Modelos de programación de bajo coste para todos",
|
"go.title": "OpenCode Go | Modelos de programación de bajo coste para todos",
|
||||||
"go.meta.description":
|
"go.meta.description":
|
||||||
"Go comienza en $5 el primer mes, luego 10 $/mes, con generosos límites de solicitudes de 5 horas para GLM-5.1, GLM-5, Kimi K2.5, Kimi K2.6, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.7 Max, Qwen3.6 Plus, Qwen3.5 Plus, MiniMax M2.5, MiniMax M2.7, DeepSeek V4 Pro y DeepSeek V4 Flash.",
|
"Go comienza en $5 el primer mes, luego 10 $/mes, con generosos límites de solicitudes de 5 horas para GLM-5.1, GLM-5, Kimi K2.5, Kimi K2.6, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.5 Plus, Qwen3.6 Plus, MiniMax M2.5, MiniMax M2.7, DeepSeek V4 Pro y DeepSeek V4 Flash.",
|
||||||
"go.hero.title": "Modelos de programación de bajo coste para todos",
|
"go.hero.title": "Modelos de programación de bajo coste para todos",
|
||||||
"go.hero.body":
|
"go.hero.body":
|
||||||
"Go lleva la programación agéntica a programadores de todo el mundo. Ofrece límites generosos y acceso fiable a los modelos de código abierto más capaces, para que puedas crear con agentes potentes sin preocuparte por el coste o la disponibilidad.",
|
"Go lleva la programación agéntica a programadores de todo el mundo. Ofrece límites generosos y acceso fiable a los modelos de código abierto más capaces, para que puedas crear con agentes potentes sin preocuparte por el coste o la disponibilidad.",
|
||||||
@@ -304,7 +304,7 @@ export const dict = {
|
|||||||
"go.problem.item2": "Límites generosos y acceso fiable",
|
"go.problem.item2": "Límites generosos y acceso fiable",
|
||||||
"go.problem.item3": "Creado para tantos programadores como sea posible",
|
"go.problem.item3": "Creado para tantos programadores como sea posible",
|
||||||
"go.problem.item4":
|
"go.problem.item4":
|
||||||
"Incluye GLM-5.1, GLM-5, Kimi K2.5, Kimi K2.6, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.7 Max, Qwen3.6 Plus, Qwen3.5 Plus, MiniMax M2.5, MiniMax M2.7, DeepSeek V4 Pro y DeepSeek V4 Flash",
|
"Incluye GLM-5.1, GLM-5, Kimi K2.5, Kimi K2.6, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.5 Plus, Qwen3.6 Plus, MiniMax M2.5, MiniMax M2.7, DeepSeek V4 Pro y DeepSeek V4 Flash",
|
||||||
"go.how.title": "Cómo funciona Go",
|
"go.how.title": "Cómo funciona Go",
|
||||||
"go.how.body": "Go comienza en $5 el primer mes, luego 10 $/mes. Puedes usarlo con OpenCode o cualquier agente.",
|
"go.how.body": "Go comienza en $5 el primer mes, luego 10 $/mes. Puedes usarlo con OpenCode o cualquier agente.",
|
||||||
"go.how.step1.title": "Crear una cuenta",
|
"go.how.step1.title": "Crear una cuenta",
|
||||||
@@ -329,7 +329,7 @@ export const dict = {
|
|||||||
"go.faq.a2": "Go incluye los modelos que se indican abajo, con límites generosos y acceso confiable.",
|
"go.faq.a2": "Go incluye los modelos que se indican abajo, con límites generosos y acceso confiable.",
|
||||||
"go.faq.q3": "¿Es Go lo mismo que Zen?",
|
"go.faq.q3": "¿Es Go lo mismo que Zen?",
|
||||||
"go.faq.a3":
|
"go.faq.a3":
|
||||||
"No. Zen es pago por uso, mientras que Go comienza en $5 el primer mes, luego 10 $/mes, con límites generosos y acceso fiable a los modelos de código abierto GLM-5.1, GLM-5, Kimi K2.5, Kimi K2.6, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.7 Max, Qwen3.6 Plus, Qwen3.5 Plus, MiniMax M2.5, MiniMax M2.7, DeepSeek V4 Pro y DeepSeek V4 Flash.",
|
"No. Zen es pago por uso, mientras que Go comienza en $5 el primer mes, luego 10 $/mes, con límites generosos y acceso fiable a los modelos de código abierto GLM-5.1, GLM-5, Kimi K2.5, Kimi K2.6, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.5 Plus, Qwen3.6 Plus, MiniMax M2.5, MiniMax M2.7, DeepSeek V4 Pro y DeepSeek V4 Flash.",
|
||||||
"go.faq.q4": "¿Cuánto cuesta Go?",
|
"go.faq.q4": "¿Cuánto cuesta Go?",
|
||||||
"go.faq.a4.p1.beforePricing": "Go cuesta",
|
"go.faq.a4.p1.beforePricing": "Go cuesta",
|
||||||
"go.faq.a4.p1.pricingLink": "$5 el primer mes",
|
"go.faq.a4.p1.pricingLink": "$5 el primer mes",
|
||||||
@@ -353,7 +353,7 @@ export const dict = {
|
|||||||
|
|
||||||
"go.faq.q9": "¿Cuál es la diferencia entre los modelos gratuitos y Go?",
|
"go.faq.q9": "¿Cuál es la diferencia entre los modelos gratuitos y Go?",
|
||||||
"go.faq.a9":
|
"go.faq.a9":
|
||||||
"Los modelos gratuitos incluyen Big Pickle más modelos promocionales disponibles en el momento, con una cuota de 200 solicitudes/día. Go incluye GLM-5.1, GLM-5, Kimi K2.5, Kimi K2.6, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.7 Max, Qwen3.6 Plus, Qwen3.5 Plus, MiniMax M2.5, MiniMax M2.7, DeepSeek V4 Pro y DeepSeek V4 Flash con cuotas de solicitud más altas aplicadas a través de ventanas móviles (5 horas, semanal y mensual), aproximadamente equivalente a 12 $ por 5 horas, 30 $ por semana y 60 $ por mes (los recuentos reales de solicitudes varían según el modelo y el uso).",
|
"Los modelos gratuitos incluyen Big Pickle más modelos promocionales disponibles en el momento, con una cuota de 200 solicitudes/día. Go incluye GLM-5.1, GLM-5, Kimi K2.5, Kimi K2.6, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.5 Plus, Qwen3.6 Plus, MiniMax M2.5, MiniMax M2.7, DeepSeek V4 Pro y DeepSeek V4 Flash con cuotas de solicitud más altas aplicadas a través de ventanas móviles (5 horas, semanal y mensual), aproximadamente equivalente a 12 $ por 5 horas, 30 $ por semana y 60 $ por mes (los recuentos reales de solicitudes varían según el modelo y el uso).",
|
||||||
|
|
||||||
"zen.api.error.rateLimitExceeded": "Límite de tasa excedido. Por favor, inténtalo de nuevo más tarde.",
|
"zen.api.error.rateLimitExceeded": "Límite de tasa excedido. Por favor, inténtalo de nuevo más tarde.",
|
||||||
"zen.api.error.modelNotSupported": "Modelo {{model}} no soportado",
|
"zen.api.error.modelNotSupported": "Modelo {{model}} no soportado",
|
||||||
|
|||||||
@@ -255,7 +255,7 @@ export const dict = {
|
|||||||
|
|
||||||
"go.title": "OpenCode Go | Modèles de code à faible coût pour tous",
|
"go.title": "OpenCode Go | Modèles de code à faible coût pour tous",
|
||||||
"go.meta.description":
|
"go.meta.description":
|
||||||
"Go commence à $5 pour le premier mois, puis 10 $/mois, avec des limites de requêtes généreuses sur 5 heures pour GLM-5.1, GLM-5, Kimi K2.5, Kimi K2.6, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.7 Max, Qwen3.6 Plus, Qwen3.5 Plus, MiniMax M2.5, MiniMax M2.7, DeepSeek V4 Pro et DeepSeek V4 Flash.",
|
"Go commence à $5 pour le premier mois, puis 10 $/mois, avec des limites de requêtes généreuses sur 5 heures pour GLM-5.1, GLM-5, Kimi K2.5, Kimi K2.6, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.5 Plus, Qwen3.6 Plus, MiniMax M2.5, MiniMax M2.7, DeepSeek V4 Pro et DeepSeek V4 Flash.",
|
||||||
"go.hero.title": "Modèles de code à faible coût pour tous",
|
"go.hero.title": "Modèles de code à faible coût pour tous",
|
||||||
"go.hero.body":
|
"go.hero.body":
|
||||||
"Go apporte le codage agentique aux programmeurs du monde entier. Offrant des limites généreuses et un accès fiable aux modèles open source les plus capables, pour que vous puissiez construire avec des agents puissants sans vous soucier du coût ou de la disponibilité.",
|
"Go apporte le codage agentique aux programmeurs du monde entier. Offrant des limites généreuses et un accès fiable aux modèles open source les plus capables, pour que vous puissiez construire avec des agents puissants sans vous soucier du coût ou de la disponibilité.",
|
||||||
@@ -304,7 +304,7 @@ export const dict = {
|
|||||||
"go.problem.item2": "Limites généreuses et accès fiable",
|
"go.problem.item2": "Limites généreuses et accès fiable",
|
||||||
"go.problem.item3": "Conçu pour autant de programmeurs que possible",
|
"go.problem.item3": "Conçu pour autant de programmeurs que possible",
|
||||||
"go.problem.item4":
|
"go.problem.item4":
|
||||||
"Inclut GLM-5.1, GLM-5, Kimi K2.5, Kimi K2.6, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.7 Max, Qwen3.6 Plus, Qwen3.5 Plus, MiniMax M2.5, MiniMax M2.7, DeepSeek V4 Pro et DeepSeek V4 Flash",
|
"Inclut GLM-5.1, GLM-5, Kimi K2.5, Kimi K2.6, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.5 Plus, Qwen3.6 Plus, MiniMax M2.5, MiniMax M2.7, DeepSeek V4 Pro et DeepSeek V4 Flash",
|
||||||
"go.how.title": "Comment fonctionne Go",
|
"go.how.title": "Comment fonctionne Go",
|
||||||
"go.how.body":
|
"go.how.body":
|
||||||
"Go commence à $5 pour le premier mois, puis 10 $/mois. Vous pouvez l'utiliser avec OpenCode ou n'importe quel agent.",
|
"Go commence à $5 pour le premier mois, puis 10 $/mois. Vous pouvez l'utiliser avec OpenCode ou n'importe quel agent.",
|
||||||
@@ -330,7 +330,7 @@ export const dict = {
|
|||||||
"go.faq.a2": "Go inclut les modèles ci-dessous, avec des limites généreuses et un accès fiable.",
|
"go.faq.a2": "Go inclut les modèles ci-dessous, avec des limites généreuses et un accès fiable.",
|
||||||
"go.faq.q3": "Est-ce que Go est la même chose que Zen ?",
|
"go.faq.q3": "Est-ce que Go est la même chose que Zen ?",
|
||||||
"go.faq.a3":
|
"go.faq.a3":
|
||||||
"Non. Zen est un paiement à l'utilisation, tandis que Go commence à $5 pour le premier mois, puis 10 $/mois, avec des limites généreuses et un accès fiable aux modèles open source GLM-5.1, GLM-5, Kimi K2.5, Kimi K2.6, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.7 Max, Qwen3.6 Plus, Qwen3.5 Plus, MiniMax M2.5, MiniMax M2.7, DeepSeek V4 Pro et DeepSeek V4 Flash.",
|
"Non. Zen est un paiement à l'utilisation, tandis que Go commence à $5 pour le premier mois, puis 10 $/mois, avec des limites généreuses et un accès fiable aux modèles open source GLM-5.1, GLM-5, Kimi K2.5, Kimi K2.6, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.5 Plus, Qwen3.6 Plus, MiniMax M2.5, MiniMax M2.7, DeepSeek V4 Pro et DeepSeek V4 Flash.",
|
||||||
"go.faq.q4": "Combien coûte Go ?",
|
"go.faq.q4": "Combien coûte Go ?",
|
||||||
"go.faq.a4.p1.beforePricing": "Go coûte",
|
"go.faq.a4.p1.beforePricing": "Go coûte",
|
||||||
"go.faq.a4.p1.pricingLink": "$5 le premier mois",
|
"go.faq.a4.p1.pricingLink": "$5 le premier mois",
|
||||||
@@ -353,7 +353,7 @@ export const dict = {
|
|||||||
"Oui, vous pouvez utiliser Go avec n'importe quel agent. Suivez les instructions de configuration dans votre agent de code préféré.",
|
"Oui, vous pouvez utiliser Go avec n'importe quel agent. Suivez les instructions de configuration dans votre agent de code préféré.",
|
||||||
"go.faq.q9": "Quelle est la différence entre les modèles gratuits et Go ?",
|
"go.faq.q9": "Quelle est la différence entre les modèles gratuits et Go ?",
|
||||||
"go.faq.a9":
|
"go.faq.a9":
|
||||||
"Les modèles gratuits incluent Big Pickle ainsi que des modèles promotionnels disponibles à ce moment-là, avec un quota de 200 requêtes/jour. Go inclut GLM-5.1, GLM-5, Kimi K2.5, Kimi K2.6, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.7 Max, Qwen3.6 Plus, Qwen3.5 Plus, MiniMax M2.5, MiniMax M2.7, DeepSeek V4 Pro et DeepSeek V4 Flash avec des quotas de requêtes plus élevés appliqués sur des fenêtres glissantes (5 heures, hebdomadaire et mensuelle), à peu près équivalent à 12 $ par 5 heures, 30 $ par semaine et 60 $ par mois (le nombre réel de requêtes varie selon le modèle et l'utilisation).",
|
"Les modèles gratuits incluent Big Pickle ainsi que des modèles promotionnels disponibles à ce moment-là, avec un quota de 200 requêtes/jour. Go inclut GLM-5.1, GLM-5, Kimi K2.5, Kimi K2.6, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.5 Plus, Qwen3.6 Plus, MiniMax M2.5, MiniMax M2.7, DeepSeek V4 Pro et DeepSeek V4 Flash avec des quotas de requêtes plus élevés appliqués sur des fenêtres glissantes (5 heures, hebdomadaire et mensuelle), à peu près équivalent à 12 $ par 5 heures, 30 $ par semaine et 60 $ par mois (le nombre réel de requêtes varie selon le modèle et l'utilisation).",
|
||||||
|
|
||||||
"zen.api.error.rateLimitExceeded": "Limite de débit dépassée. Veuillez réessayer plus tard.",
|
"zen.api.error.rateLimitExceeded": "Limite de débit dépassée. Veuillez réessayer plus tard.",
|
||||||
"zen.api.error.modelNotSupported": "Modèle {{model}} non pris en charge",
|
"zen.api.error.modelNotSupported": "Modèle {{model}} non pris en charge",
|
||||||
|
|||||||
@@ -251,7 +251,7 @@ export const dict = {
|
|||||||
|
|
||||||
"go.title": "OpenCode Go | Modelli di coding a basso costo per tutti",
|
"go.title": "OpenCode Go | Modelli di coding a basso costo per tutti",
|
||||||
"go.meta.description":
|
"go.meta.description":
|
||||||
"Go inizia a $5 per il primo mese, poi $10/mese, con generosi limiti di richiesta di 5 ore per GLM-5.1, GLM-5, Kimi K2.5, Kimi K2.6, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.7 Max, Qwen3.6 Plus, Qwen3.5 Plus, MiniMax M2.5, MiniMax M2.7, DeepSeek V4 Pro e DeepSeek V4 Flash.",
|
"Go inizia a $5 per il primo mese, poi $10/mese, con generosi limiti di richiesta di 5 ore per GLM-5.1, GLM-5, Kimi K2.5, Kimi K2.6, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.5 Plus, Qwen3.6 Plus, MiniMax M2.5, MiniMax M2.7, DeepSeek V4 Pro e DeepSeek V4 Flash.",
|
||||||
"go.hero.title": "Modelli di coding a basso costo per tutti",
|
"go.hero.title": "Modelli di coding a basso costo per tutti",
|
||||||
"go.hero.body":
|
"go.hero.body":
|
||||||
"Go porta il coding agentico ai programmatori di tutto il mondo. Offrendo limiti generosi e un accesso affidabile ai modelli open source più capaci, in modo da poter costruire con agenti potenti senza preoccuparsi dei costi o della disponibilità.",
|
"Go porta il coding agentico ai programmatori di tutto il mondo. Offrendo limiti generosi e un accesso affidabile ai modelli open source più capaci, in modo da poter costruire con agenti potenti senza preoccuparsi dei costi o della disponibilità.",
|
||||||
@@ -300,7 +300,7 @@ export const dict = {
|
|||||||
"go.problem.item2": "Limiti generosi e accesso affidabile",
|
"go.problem.item2": "Limiti generosi e accesso affidabile",
|
||||||
"go.problem.item3": "Costruito per il maggior numero possibile di programmatori",
|
"go.problem.item3": "Costruito per il maggior numero possibile di programmatori",
|
||||||
"go.problem.item4":
|
"go.problem.item4":
|
||||||
"Include GLM-5.1, GLM-5, Kimi K2.5, Kimi K2.6, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.7 Max, Qwen3.6 Plus, Qwen3.5 Plus, MiniMax M2.5, MiniMax M2.7, DeepSeek V4 Pro e DeepSeek V4 Flash",
|
"Include GLM-5.1, GLM-5, Kimi K2.5, Kimi K2.6, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.5 Plus, Qwen3.6 Plus, MiniMax M2.5, MiniMax M2.7, DeepSeek V4 Pro e DeepSeek V4 Flash",
|
||||||
"go.how.title": "Come funziona Go",
|
"go.how.title": "Come funziona Go",
|
||||||
"go.how.body": "Go inizia a $5 per il primo mese, poi $10/mese. Puoi usarlo con OpenCode o qualsiasi agente.",
|
"go.how.body": "Go inizia a $5 per il primo mese, poi $10/mese. Puoi usarlo con OpenCode o qualsiasi agente.",
|
||||||
"go.how.step1.title": "Crea un account",
|
"go.how.step1.title": "Crea un account",
|
||||||
@@ -325,7 +325,7 @@ export const dict = {
|
|||||||
"go.faq.a2": "Go include i modelli elencati di seguito, con limiti generosi e accesso affidabile.",
|
"go.faq.a2": "Go include i modelli elencati di seguito, con limiti generosi e accesso affidabile.",
|
||||||
"go.faq.q3": "Go è lo stesso di Zen?",
|
"go.faq.q3": "Go è lo stesso di Zen?",
|
||||||
"go.faq.a3":
|
"go.faq.a3":
|
||||||
"No. Zen è a consumo, mentre Go inizia a $5 per il primo mese, poi $10/mese, con limiti generosi e accesso affidabile ai modelli open source GLM-5.1, GLM-5, Kimi K2.5, Kimi K2.6, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.7 Max, Qwen3.6 Plus, Qwen3.5 Plus, MiniMax M2.5, MiniMax M2.7, DeepSeek V4 Pro e DeepSeek V4 Flash.",
|
"No. Zen è a consumo, mentre Go inizia a $5 per il primo mese, poi $10/mese, con limiti generosi e accesso affidabile ai modelli open source GLM-5.1, GLM-5, Kimi K2.5, Kimi K2.6, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.5 Plus, Qwen3.6 Plus, MiniMax M2.5, MiniMax M2.7, DeepSeek V4 Pro e DeepSeek V4 Flash.",
|
||||||
"go.faq.q4": "Quanto costa Go?",
|
"go.faq.q4": "Quanto costa Go?",
|
||||||
"go.faq.a4.p1.beforePricing": "Go costa",
|
"go.faq.a4.p1.beforePricing": "Go costa",
|
||||||
"go.faq.a4.p1.pricingLink": "$5 il primo mese",
|
"go.faq.a4.p1.pricingLink": "$5 il primo mese",
|
||||||
@@ -349,7 +349,7 @@ export const dict = {
|
|||||||
|
|
||||||
"go.faq.q9": "Qual è la differenza tra i modelli gratuiti e Go?",
|
"go.faq.q9": "Qual è la differenza tra i modelli gratuiti e Go?",
|
||||||
"go.faq.a9":
|
"go.faq.a9":
|
||||||
"I modelli gratuiti includono Big Pickle più modelli promozionali disponibili al momento, con una quota di 200 richieste/giorno. Go include GLM-5.1, GLM-5, Kimi K2.5, Kimi K2.6, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.7 Max, Qwen3.6 Plus, Qwen3.5 Plus, MiniMax M2.5, MiniMax M2.7, DeepSeek V4 Pro e DeepSeek V4 Flash con quote di richiesta più elevate applicate su finestre mobili (5 ore, settimanale e mensile), approssimativamente equivalenti a $12 ogni 5 ore, $30 a settimana e $60 al mese (il conteggio effettivo delle richieste varia in base al modello e all'utilizzo).",
|
"I modelli gratuiti includono Big Pickle più modelli promozionali disponibili al momento, con una quota di 200 richieste/giorno. Go include GLM-5.1, GLM-5, Kimi K2.5, Kimi K2.6, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.5 Plus, Qwen3.6 Plus, MiniMax M2.5, MiniMax M2.7, DeepSeek V4 Pro e DeepSeek V4 Flash con quote di richiesta più elevate applicate su finestre mobili (5 ore, settimanale e mensile), approssimativamente equivalenti a $12 ogni 5 ore, $30 a settimana e $60 al mese (il conteggio effettivo delle richieste varia in base al modello e all'utilizzo).",
|
||||||
|
|
||||||
"zen.api.error.rateLimitExceeded": "Limite di richieste superato. Riprova più tardi.",
|
"zen.api.error.rateLimitExceeded": "Limite di richieste superato. Riprova più tardi.",
|
||||||
"zen.api.error.modelNotSupported": "Modello {{model}} non supportato",
|
"zen.api.error.modelNotSupported": "Modello {{model}} non supportato",
|
||||||
|
|||||||
@@ -250,7 +250,7 @@ export const dict = {
|
|||||||
|
|
||||||
"go.title": "OpenCode Go | すべての人のための低価格なコーディングモデル",
|
"go.title": "OpenCode Go | すべての人のための低価格なコーディングモデル",
|
||||||
"go.meta.description":
|
"go.meta.description":
|
||||||
"Goは最初の月$5、その後$10/月で、GLM-5.1、GLM-5、Kimi K2.5、Kimi K2.6、MiMo-V2.5-Pro、MiMo-V2.5、Qwen3.7 Max、Qwen3.6 Plus、Qwen3.5 Plus、MiniMax M2.5、MiniMax M2.7、DeepSeek V4 Pro、DeepSeek V4 Flashに対して5時間のゆとりあるリクエスト上限があります。",
|
"Goは最初の月$5、その後$10/月で、GLM-5.1、GLM-5、Kimi K2.5、Kimi K2.6、MiMo-V2.5-Pro、MiMo-V2.5、Qwen3.5 Plus、Qwen3.6 Plus、MiniMax M2.5、MiniMax M2.7、DeepSeek V4 Pro、DeepSeek V4 Flashに対して5時間のゆとりあるリクエスト上限があります。",
|
||||||
"go.hero.title": "すべての人のための低価格なコーディングモデル",
|
"go.hero.title": "すべての人のための低価格なコーディングモデル",
|
||||||
"go.hero.body":
|
"go.hero.body":
|
||||||
"Goは、世界中のプログラマーにエージェント型コーディングをもたらします。最も高性能なオープンソースモデルへの十分な制限と安定したアクセスを提供し、コストや可用性を気にすることなく強力なエージェントで構築できます。",
|
"Goは、世界中のプログラマーにエージェント型コーディングをもたらします。最も高性能なオープンソースモデルへの十分な制限と安定したアクセスを提供し、コストや可用性を気にすることなく強力なエージェントで構築できます。",
|
||||||
@@ -300,7 +300,7 @@ export const dict = {
|
|||||||
"go.problem.item2": "十分な制限と安定したアクセス",
|
"go.problem.item2": "十分な制限と安定したアクセス",
|
||||||
"go.problem.item3": "できるだけ多くのプログラマーのために構築",
|
"go.problem.item3": "できるだけ多くのプログラマーのために構築",
|
||||||
"go.problem.item4":
|
"go.problem.item4":
|
||||||
"GLM-5.1、GLM-5、Kimi K2.5、Kimi K2.6、MiMo-V2.5-Pro、MiMo-V2.5、Qwen3.7 Max、Qwen3.6 Plus、Qwen3.5 Plus、MiniMax M2.5、MiniMax M2.7、DeepSeek V4 Pro、DeepSeek V4 Flashを含む",
|
"GLM-5.1、GLM-5、Kimi K2.5、Kimi K2.6、MiMo-V2.5-Pro、MiMo-V2.5、Qwen3.5 Plus、Qwen3.6 Plus、MiniMax M2.5、MiniMax M2.7、DeepSeek V4 Pro、DeepSeek V4 Flashを含む",
|
||||||
"go.how.title": "Goの仕組み",
|
"go.how.title": "Goの仕組み",
|
||||||
"go.how.body": "Goは最初の月$5、その後$10/月で始まります。OpenCodeまたは任意のエージェントで使えます。",
|
"go.how.body": "Goは最初の月$5、その後$10/月で始まります。OpenCodeまたは任意のエージェントで使えます。",
|
||||||
"go.how.step1.title": "アカウントを作成",
|
"go.how.step1.title": "アカウントを作成",
|
||||||
@@ -325,7 +325,7 @@ export const dict = {
|
|||||||
"go.faq.a2": "Go には、十分な利用上限と安定したアクセスを備えた、以下のモデルが含まれます。",
|
"go.faq.a2": "Go には、十分な利用上限と安定したアクセスを備えた、以下のモデルが含まれます。",
|
||||||
"go.faq.q3": "GoはZenと同じですか?",
|
"go.faq.q3": "GoはZenと同じですか?",
|
||||||
"go.faq.a3":
|
"go.faq.a3":
|
||||||
"いいえ。Zenは従量課金制ですが、Goは最初の月$5、その後$10/月で始まり、GLM-5.1、GLM-5、Kimi K2.5、Kimi K2.6、MiMo-V2.5-Pro、MiMo-V2.5、Qwen3.7 Max、Qwen3.6 Plus、Qwen3.5 Plus、MiniMax M2.5、MiniMax M2.7、DeepSeek V4 Pro、DeepSeek V4 Flashのオープンソースモデルに対して、ゆとりある上限と信頼できるアクセスを提供します。",
|
"いいえ。Zenは従量課金制ですが、Goは最初の月$5、その後$10/月で始まり、GLM-5.1、GLM-5、Kimi K2.5、Kimi K2.6、MiMo-V2.5-Pro、MiMo-V2.5、Qwen3.5 Plus、Qwen3.6 Plus、MiniMax M2.5、MiniMax M2.7、DeepSeek V4 Pro、DeepSeek V4 Flashのオープンソースモデルに対して、ゆとりある上限と信頼できるアクセスを提供します。",
|
||||||
"go.faq.q4": "Goの料金は?",
|
"go.faq.q4": "Goの料金は?",
|
||||||
"go.faq.a4.p1.beforePricing": "Goは",
|
"go.faq.a4.p1.beforePricing": "Goは",
|
||||||
"go.faq.a4.p1.pricingLink": "最初の月$5",
|
"go.faq.a4.p1.pricingLink": "最初の月$5",
|
||||||
@@ -349,7 +349,7 @@ export const dict = {
|
|||||||
|
|
||||||
"go.faq.q9": "無料モデルとGoの違いは何ですか?",
|
"go.faq.q9": "無料モデルとGoの違いは何ですか?",
|
||||||
"go.faq.a9":
|
"go.faq.a9":
|
||||||
"無料モデルにはBig Pickleと、その時点で利用可能なプロモーションモデルが含まれ、1日200リクエストの制限があります。GoにはGLM-5.1、GLM-5、Kimi K2.5、Kimi K2.6、MiMo-V2.5-Pro、MiMo-V2.5、Qwen3.7 Max、Qwen3.6 Plus、Qwen3.5 Plus、MiniMax M2.5、MiniMax M2.7、DeepSeek V4 Pro、DeepSeek V4 Flashが含まれ、ローリングウィンドウ(5時間、週間、月間)全体でより高いリクエスト制限が適用されます。これは概算で5時間あたり$12、週間$30、月間$60相当です(実際のリクエスト数はモデルと使用状況により異なります)。",
|
"無料モデルにはBig Pickleと、その時点で利用可能なプロモーションモデルが含まれ、1日200リクエストの制限があります。GoにはGLM-5.1、GLM-5、Kimi K2.5、Kimi K2.6、MiMo-V2.5-Pro、MiMo-V2.5、Qwen3.5 Plus、Qwen3.6 Plus、MiniMax M2.5、MiniMax M2.7、DeepSeek V4 Pro、DeepSeek V4 Flashが含まれ、ローリングウィンドウ(5時間、週間、月間)全体でより高いリクエスト制限が適用されます。これは概算で5時間あたり$12、週間$30、月間$60相当です(実際のリクエスト数はモデルと使用状況により異なります)。",
|
||||||
|
|
||||||
"zen.api.error.rateLimitExceeded": "レート制限を超えました。後でもう一度お試しください。",
|
"zen.api.error.rateLimitExceeded": "レート制限を超えました。後でもう一度お試しください。",
|
||||||
"zen.api.error.modelNotSupported": "モデル {{model}} はサポートされていません",
|
"zen.api.error.modelNotSupported": "モデル {{model}} はサポートされていません",
|
||||||
|
|||||||
@@ -247,7 +247,7 @@ export const dict = {
|
|||||||
|
|
||||||
"go.title": "OpenCode Go | 모두를 위한 저비용 코딩 모델",
|
"go.title": "OpenCode Go | 모두를 위한 저비용 코딩 모델",
|
||||||
"go.meta.description":
|
"go.meta.description":
|
||||||
"Go는 첫 달 $5, 이후 $10/월로 시작하며, GLM-5.1, GLM-5, Kimi K2.5, Kimi K2.6, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.7 Max, Qwen3.6 Plus, Qwen3.5 Plus, MiniMax M2.5, MiniMax M2.7, DeepSeek V4 Pro, DeepSeek V4 Flash에 대해 넉넉한 5시간 요청 한도를 제공합니다.",
|
"Go는 첫 달 $5, 이후 $10/월로 시작하며, GLM-5.1, GLM-5, Kimi K2.5, Kimi K2.6, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.5 Plus, Qwen3.6 Plus, MiniMax M2.5, MiniMax M2.7, DeepSeek V4 Pro, DeepSeek V4 Flash에 대해 넉넉한 5시간 요청 한도를 제공합니다.",
|
||||||
"go.hero.title": "모두를 위한 저비용 코딩 모델",
|
"go.hero.title": "모두를 위한 저비용 코딩 모델",
|
||||||
"go.hero.body":
|
"go.hero.body":
|
||||||
"Go는 전 세계 프로그래머들에게 에이전트 코딩을 제공합니다. 가장 유능한 오픈 소스 모델에 대한 넉넉한 한도와 안정적인 액세스를 제공하므로, 비용이나 가용성 걱정 없이 강력한 에이전트로 빌드할 수 있습니다.",
|
"Go는 전 세계 프로그래머들에게 에이전트 코딩을 제공합니다. 가장 유능한 오픈 소스 모델에 대한 넉넉한 한도와 안정적인 액세스를 제공하므로, 비용이나 가용성 걱정 없이 강력한 에이전트로 빌드할 수 있습니다.",
|
||||||
@@ -297,7 +297,7 @@ export const dict = {
|
|||||||
"go.problem.item2": "넉넉한 한도와 안정적인 액세스",
|
"go.problem.item2": "넉넉한 한도와 안정적인 액세스",
|
||||||
"go.problem.item3": "가능한 한 많은 프로그래머를 위해 제작됨",
|
"go.problem.item3": "가능한 한 많은 프로그래머를 위해 제작됨",
|
||||||
"go.problem.item4":
|
"go.problem.item4":
|
||||||
"GLM-5.1, GLM-5, Kimi K2.5, Kimi K2.6, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.7 Max, Qwen3.6 Plus, Qwen3.5 Plus, MiniMax M2.5, MiniMax M2.7, DeepSeek V4 Pro, DeepSeek V4 Flash 포함",
|
"GLM-5.1, GLM-5, Kimi K2.5, Kimi K2.6, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.5 Plus, Qwen3.6 Plus, MiniMax M2.5, MiniMax M2.7, DeepSeek V4 Pro, DeepSeek V4 Flash 포함",
|
||||||
"go.how.title": "Go 작동 방식",
|
"go.how.title": "Go 작동 방식",
|
||||||
"go.how.body": "Go는 첫 달 $5, 이후 $10/월로 시작합니다. OpenCode 또는 어떤 에이전트와도 함께 사용할 수 있습니다.",
|
"go.how.body": "Go는 첫 달 $5, 이후 $10/월로 시작합니다. OpenCode 또는 어떤 에이전트와도 함께 사용할 수 있습니다.",
|
||||||
"go.how.step1.title": "계정 생성",
|
"go.how.step1.title": "계정 생성",
|
||||||
@@ -321,7 +321,7 @@ export const dict = {
|
|||||||
"go.faq.a2": "Go에는 넉넉한 한도와 안정적인 액세스를 제공하는 아래 모델이 포함됩니다.",
|
"go.faq.a2": "Go에는 넉넉한 한도와 안정적인 액세스를 제공하는 아래 모델이 포함됩니다.",
|
||||||
"go.faq.q3": "Go는 Zen과 같은가요?",
|
"go.faq.q3": "Go는 Zen과 같은가요?",
|
||||||
"go.faq.a3":
|
"go.faq.a3":
|
||||||
"아니요. Zen은 종량제인 반면, Go는 첫 달 $5, 이후 $10/월로 시작하며, GLM-5.1, GLM-5, Kimi K2.5, Kimi K2.6, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.7 Max, Qwen3.6 Plus, Qwen3.5 Plus, MiniMax M2.5, MiniMax M2.7, DeepSeek V4 Pro, DeepSeek V4 Flash 오픈 소스 모델에 대한 넉넉한 한도와 안정적인 액세스를 제공합니다.",
|
"아니요. Zen은 종량제인 반면, Go는 첫 달 $5, 이후 $10/월로 시작하며, GLM-5.1, GLM-5, Kimi K2.5, Kimi K2.6, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.5 Plus, Qwen3.6 Plus, MiniMax M2.5, MiniMax M2.7, DeepSeek V4 Pro, DeepSeek V4 Flash 오픈 소스 모델에 대한 넉넉한 한도와 안정적인 액세스를 제공합니다.",
|
||||||
"go.faq.q4": "Go 비용은 얼마인가요?",
|
"go.faq.q4": "Go 비용은 얼마인가요?",
|
||||||
"go.faq.a4.p1.beforePricing": "Go 비용은",
|
"go.faq.a4.p1.beforePricing": "Go 비용은",
|
||||||
"go.faq.a4.p1.pricingLink": "첫 달 $5",
|
"go.faq.a4.p1.pricingLink": "첫 달 $5",
|
||||||
@@ -344,7 +344,7 @@ export const dict = {
|
|||||||
|
|
||||||
"go.faq.q9": "무료 모델과 Go의 차이점은 무엇인가요?",
|
"go.faq.q9": "무료 모델과 Go의 차이점은 무엇인가요?",
|
||||||
"go.faq.a9":
|
"go.faq.a9":
|
||||||
"무료 모델에는 Big Pickle과 당시 사용 가능한 프로모션 모델이 포함되며, 하루 200회 요청 할당량이 적용됩니다. Go는 GLM-5.1, GLM-5, Kimi K2.5, Kimi K2.6, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.7 Max, Qwen3.6 Plus, Qwen3.5 Plus, MiniMax M2.5, MiniMax M2.7, DeepSeek V4 Pro, DeepSeek V4 Flash를 포함하며, 롤링 윈도우(5시간, 주간, 월간)에 걸쳐 더 높은 요청 할당량을 적용합니다. 이는 대략 5시간당 $12, 주당 $30, 월 $60에 해당합니다(실제 요청 수는 모델 및 사용량에 따라 다름).",
|
"무료 모델에는 Big Pickle과 당시 사용 가능한 프로모션 모델이 포함되며, 하루 200회 요청 할당량이 적용됩니다. Go는 GLM-5.1, GLM-5, Kimi K2.5, Kimi K2.6, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.5 Plus, Qwen3.6 Plus, MiniMax M2.5, MiniMax M2.7, DeepSeek V4 Pro, DeepSeek V4 Flash를 포함하며, 롤링 윈도우(5시간, 주간, 월간)에 걸쳐 더 높은 요청 할당량을 적용합니다. 이는 대략 5시간당 $12, 주당 $30, 월 $60에 해당합니다(실제 요청 수는 모델 및 사용량에 따라 다름).",
|
||||||
|
|
||||||
"zen.api.error.rateLimitExceeded": "속도 제한을 초과했습니다. 나중에 다시 시도해 주세요.",
|
"zen.api.error.rateLimitExceeded": "속도 제한을 초과했습니다. 나중에 다시 시도해 주세요.",
|
||||||
"zen.api.error.modelNotSupported": "{{model}} 모델은 지원되지 않습니다",
|
"zen.api.error.modelNotSupported": "{{model}} 모델은 지원되지 않습니다",
|
||||||
|
|||||||
@@ -251,7 +251,7 @@ export const dict = {
|
|||||||
|
|
||||||
"go.title": "OpenCode Go | Rimelige kodemodeller for alle",
|
"go.title": "OpenCode Go | Rimelige kodemodeller for alle",
|
||||||
"go.meta.description":
|
"go.meta.description":
|
||||||
"Go starter på $5 for den første måneden, deretter $10/måned, med sjenerøse 5-timers forespørselsgrenser for GLM-5.1, GLM-5, Kimi K2.5, Kimi K2.6, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.7 Max, Qwen3.6 Plus, Qwen3.5 Plus, MiniMax M2.5, MiniMax M2.7, DeepSeek V4 Pro og DeepSeek V4 Flash.",
|
"Go starter på $5 for den første måneden, deretter $10/måned, med sjenerøse 5-timers forespørselsgrenser for GLM-5.1, GLM-5, Kimi K2.5, Kimi K2.6, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.5 Plus, Qwen3.6 Plus, MiniMax M2.5, MiniMax M2.7, DeepSeek V4 Pro og DeepSeek V4 Flash.",
|
||||||
"go.hero.title": "Rimelige kodemodeller for alle",
|
"go.hero.title": "Rimelige kodemodeller for alle",
|
||||||
"go.hero.body":
|
"go.hero.body":
|
||||||
"Go bringer agent-koding til programmerere over hele verden. Med rause grenser og pålitelig tilgang til de mest kapable åpen kildekode-modellene, kan du bygge med kraftige agenter uten å bekymre deg for kostnader eller tilgjengelighet.",
|
"Go bringer agent-koding til programmerere over hele verden. Med rause grenser og pålitelig tilgang til de mest kapable åpen kildekode-modellene, kan du bygge med kraftige agenter uten å bekymre deg for kostnader eller tilgjengelighet.",
|
||||||
@@ -300,7 +300,7 @@ export const dict = {
|
|||||||
"go.problem.item2": "Rause grenser og pålitelig tilgang",
|
"go.problem.item2": "Rause grenser og pålitelig tilgang",
|
||||||
"go.problem.item3": "Bygget for så mange programmerere som mulig",
|
"go.problem.item3": "Bygget for så mange programmerere som mulig",
|
||||||
"go.problem.item4":
|
"go.problem.item4":
|
||||||
"Inkluderer GLM-5.1, GLM-5, Kimi K2.5, Kimi K2.6, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.7 Max, Qwen3.6 Plus, Qwen3.5 Plus, MiniMax M2.5, MiniMax M2.7, DeepSeek V4 Pro og DeepSeek V4 Flash",
|
"Inkluderer GLM-5.1, GLM-5, Kimi K2.5, Kimi K2.6, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.5 Plus, Qwen3.6 Plus, MiniMax M2.5, MiniMax M2.7, DeepSeek V4 Pro og DeepSeek V4 Flash",
|
||||||
"go.how.title": "Hvordan Go fungerer",
|
"go.how.title": "Hvordan Go fungerer",
|
||||||
"go.how.body":
|
"go.how.body":
|
||||||
"Go starter på $5 for den første måneden, deretter $10/måned. Du kan bruke det med OpenCode eller hvilken som helst agent.",
|
"Go starter på $5 for den første måneden, deretter $10/måned. Du kan bruke det med OpenCode eller hvilken som helst agent.",
|
||||||
@@ -326,7 +326,7 @@ export const dict = {
|
|||||||
"go.faq.a2": "Go inkluderer modellene nedenfor, med høye grenser og pålitelig tilgang.",
|
"go.faq.a2": "Go inkluderer modellene nedenfor, med høye grenser og pålitelig tilgang.",
|
||||||
"go.faq.q3": "Er Go det samme som Zen?",
|
"go.faq.q3": "Er Go det samme som Zen?",
|
||||||
"go.faq.a3":
|
"go.faq.a3":
|
||||||
"Nei. Zen er betaling etter bruk, mens Go starter på $5 for den første måneden, deretter $10/måned, med sjenerøse grenser og pålitelig tilgang til åpen kildekode-modellene GLM-5.1, GLM-5, Kimi K2.5, Kimi K2.6, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.7 Max, Qwen3.6 Plus, Qwen3.5 Plus, MiniMax M2.5, MiniMax M2.7, DeepSeek V4 Pro og DeepSeek V4 Flash.",
|
"Nei. Zen er betaling etter bruk, mens Go starter på $5 for den første måneden, deretter $10/måned, med sjenerøse grenser og pålitelig tilgang til åpen kildekode-modellene GLM-5.1, GLM-5, Kimi K2.5, Kimi K2.6, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.5 Plus, Qwen3.6 Plus, MiniMax M2.5, MiniMax M2.7, DeepSeek V4 Pro og DeepSeek V4 Flash.",
|
||||||
"go.faq.q4": "Hva koster Go?",
|
"go.faq.q4": "Hva koster Go?",
|
||||||
"go.faq.a4.p1.beforePricing": "Go koster",
|
"go.faq.a4.p1.beforePricing": "Go koster",
|
||||||
"go.faq.a4.p1.pricingLink": "$5 første måned",
|
"go.faq.a4.p1.pricingLink": "$5 første måned",
|
||||||
@@ -350,7 +350,7 @@ export const dict = {
|
|||||||
|
|
||||||
"go.faq.q9": "Hva er forskjellen mellom gratis modeller og Go?",
|
"go.faq.q9": "Hva er forskjellen mellom gratis modeller og Go?",
|
||||||
"go.faq.a9":
|
"go.faq.a9":
|
||||||
"Gratis modeller inkluderer Big Pickle pluss kampanjemodeller tilgjengelig på det tidspunktet, med en kvote på 200 forespørsler/dag. Go inkluderer GLM-5.1, GLM-5, Kimi K2.5, Kimi K2.6, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.7 Max, Qwen3.6 Plus, Qwen3.5 Plus, MiniMax M2.5, MiniMax M2.7, DeepSeek V4 Pro og DeepSeek V4 Flash med høyere kvoter håndhevet over rullerende vinduer (5 timer, ukentlig og månedlig), omtrent tilsvarende $12 per 5 timer, $30 per uke og $60 per måned (faktiske forespørselsantall varierer etter modell og bruk).",
|
"Gratis modeller inkluderer Big Pickle pluss kampanjemodeller tilgjengelig på det tidspunktet, med en kvote på 200 forespørsler/dag. Go inkluderer GLM-5.1, GLM-5, Kimi K2.5, Kimi K2.6, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.5 Plus, Qwen3.6 Plus, MiniMax M2.5, MiniMax M2.7, DeepSeek V4 Pro og DeepSeek V4 Flash med høyere kvoter håndhevet over rullerende vinduer (5 timer, ukentlig og månedlig), omtrent tilsvarende $12 per 5 timer, $30 per uke og $60 per måned (faktiske forespørselsantall varierer etter modell og bruk).",
|
||||||
|
|
||||||
"zen.api.error.rateLimitExceeded": "Rate limit overskredet. Vennligst prøv igjen senere.",
|
"zen.api.error.rateLimitExceeded": "Rate limit overskredet. Vennligst prøv igjen senere.",
|
||||||
"zen.api.error.modelNotSupported": "Modell {{model}} støttes ikke",
|
"zen.api.error.modelNotSupported": "Modell {{model}} støttes ikke",
|
||||||
|
|||||||
@@ -252,7 +252,7 @@ export const dict = {
|
|||||||
|
|
||||||
"go.title": "OpenCode Go | Niskokosztowe modele do kodowania dla każdego",
|
"go.title": "OpenCode Go | Niskokosztowe modele do kodowania dla każdego",
|
||||||
"go.meta.description":
|
"go.meta.description":
|
||||||
"Go zaczyna się od $5 za pierwszy miesiąc, potem $10/miesiąc, z hojnymi 5-godzinnymi limitami zapytań dla GLM-5.1, GLM-5, Kimi K2.5, Kimi K2.6, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.7 Max, Qwen3.6 Plus, Qwen3.5 Plus, MiniMax M2.5, MiniMax M2.7, DeepSeek V4 Pro i DeepSeek V4 Flash.",
|
"Go zaczyna się od $5 za pierwszy miesiąc, potem $10/miesiąc, z hojnymi 5-godzinnymi limitami zapytań dla GLM-5.1, GLM-5, Kimi K2.5, Kimi K2.6, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.5 Plus, Qwen3.6 Plus, MiniMax M2.5, MiniMax M2.7, DeepSeek V4 Pro i DeepSeek V4 Flash.",
|
||||||
"go.hero.title": "Niskokosztowe modele do kodowania dla każdego",
|
"go.hero.title": "Niskokosztowe modele do kodowania dla każdego",
|
||||||
"go.hero.body":
|
"go.hero.body":
|
||||||
"Go udostępnia programowanie z agentami programistom na całym świecie. Oferuje hojne limity i niezawodny dostęp do najzdolniejszych modeli open source, dzięki czemu możesz budować za pomocą potężnych agentów, nie martwiąc się o koszty czy dostępność.",
|
"Go udostępnia programowanie z agentami programistom na całym świecie. Oferuje hojne limity i niezawodny dostęp do najzdolniejszych modeli open source, dzięki czemu możesz budować za pomocą potężnych agentów, nie martwiąc się o koszty czy dostępność.",
|
||||||
@@ -301,7 +301,7 @@ export const dict = {
|
|||||||
"go.problem.item2": "Hojne limity i niezawodny dostęp",
|
"go.problem.item2": "Hojne limity i niezawodny dostęp",
|
||||||
"go.problem.item3": "Stworzony dla jak największej liczby programistów",
|
"go.problem.item3": "Stworzony dla jak największej liczby programistów",
|
||||||
"go.problem.item4":
|
"go.problem.item4":
|
||||||
"Zawiera GLM-5.1, GLM-5, Kimi K2.5, Kimi K2.6, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.7 Max, Qwen3.6 Plus, Qwen3.5 Plus, MiniMax M2.5, MiniMax M2.7, DeepSeek V4 Pro i DeepSeek V4 Flash",
|
"Zawiera GLM-5.1, GLM-5, Kimi K2.5, Kimi K2.6, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.5 Plus, Qwen3.6 Plus, MiniMax M2.5, MiniMax M2.7, DeepSeek V4 Pro i DeepSeek V4 Flash",
|
||||||
"go.how.title": "Jak działa Go",
|
"go.how.title": "Jak działa Go",
|
||||||
"go.how.body":
|
"go.how.body":
|
||||||
"Go zaczyna się od $5 za pierwszy miesiąc, potem $10/miesiąc. Możesz go używać z OpenCode lub dowolnym agentem.",
|
"Go zaczyna się od $5 za pierwszy miesiąc, potem $10/miesiąc. Możesz go używać z OpenCode lub dowolnym agentem.",
|
||||||
@@ -327,7 +327,7 @@ export const dict = {
|
|||||||
"go.faq.a2": "Go obejmuje poniższe modele z wysokimi limitami i niezawodnym dostępem.",
|
"go.faq.a2": "Go obejmuje poniższe modele z wysokimi limitami i niezawodnym dostępem.",
|
||||||
"go.faq.q3": "Czy Go to to samo co Zen?",
|
"go.faq.q3": "Czy Go to to samo co Zen?",
|
||||||
"go.faq.a3":
|
"go.faq.a3":
|
||||||
"Nie. Zen to model płatności za użycie, podczas gdy Go zaczyna się od $5 za pierwszy miesiąc, potem $10/miesiąc, z hojnymi limitami i niezawodnym dostępem do modeli open source GLM-5.1, GLM-5, Kimi K2.5, Kimi K2.6, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.7 Max, Qwen3.6 Plus, Qwen3.5 Plus, MiniMax M2.5, MiniMax M2.7, DeepSeek V4 Pro i DeepSeek V4 Flash.",
|
"Nie. Zen to model płatności za użycie, podczas gdy Go zaczyna się od $5 za pierwszy miesiąc, potem $10/miesiąc, z hojnymi limitami i niezawodnym dostępem do modeli open source GLM-5.1, GLM-5, Kimi K2.5, Kimi K2.6, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.5 Plus, Qwen3.6 Plus, MiniMax M2.5, MiniMax M2.7, DeepSeek V4 Pro i DeepSeek V4 Flash.",
|
||||||
"go.faq.q4": "Ile kosztuje Go?",
|
"go.faq.q4": "Ile kosztuje Go?",
|
||||||
"go.faq.a4.p1.beforePricing": "Go kosztuje",
|
"go.faq.a4.p1.beforePricing": "Go kosztuje",
|
||||||
"go.faq.a4.p1.pricingLink": "$5 za pierwszy miesiąc",
|
"go.faq.a4.p1.pricingLink": "$5 za pierwszy miesiąc",
|
||||||
@@ -351,7 +351,7 @@ export const dict = {
|
|||||||
|
|
||||||
"go.faq.q9": "Jaka jest różnica między darmowymi modelami a Go?",
|
"go.faq.q9": "Jaka jest różnica między darmowymi modelami a Go?",
|
||||||
"go.faq.a9":
|
"go.faq.a9":
|
||||||
"Darmowe modele obejmują Big Pickle oraz modele promocyjne dostępne w danym momencie, z limitem 200 zapytań/dzień. Go zawiera GLM-5.1, GLM-5, Kimi K2.5, Kimi K2.6, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.7 Max, Qwen3.6 Plus, Qwen3.5 Plus, MiniMax M2.5, MiniMax M2.7, DeepSeek V4 Pro i DeepSeek V4 Flash z wyższymi limitami zapytań egzekwowanymi w oknach kroczących (5-godzinnych, tygodniowych i miesięcznych), w przybliżeniu równoważnymi $12 na 5 godzin, $30 tygodniowo i $60 miesięcznie (rzeczywista liczba zapytań zależy od modelu i użycia).",
|
"Darmowe modele obejmują Big Pickle oraz modele promocyjne dostępne w danym momencie, z limitem 200 zapytań/dzień. Go zawiera GLM-5.1, GLM-5, Kimi K2.5, Kimi K2.6, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.5 Plus, Qwen3.6 Plus, MiniMax M2.5, MiniMax M2.7, DeepSeek V4 Pro i DeepSeek V4 Flash z wyższymi limitami zapytań egzekwowanymi w oknach kroczących (5-godzinnych, tygodniowych i miesięcznych), w przybliżeniu równoważnymi $12 na 5 godzin, $30 tygodniowo i $60 miesięcznie (rzeczywista liczba zapytań zależy od modelu i użycia).",
|
||||||
|
|
||||||
"zen.api.error.rateLimitExceeded": "Przekroczono limit zapytań. Spróbuj ponownie później.",
|
"zen.api.error.rateLimitExceeded": "Przekroczono limit zapytań. Spróbuj ponownie później.",
|
||||||
"zen.api.error.modelNotSupported": "Model {{model}} nie jest obsługiwany",
|
"zen.api.error.modelNotSupported": "Model {{model}} nie jest obsługiwany",
|
||||||
|
|||||||
@@ -255,7 +255,7 @@ export const dict = {
|
|||||||
|
|
||||||
"go.title": "OpenCode Go | Недорогие модели для кодинга для всех",
|
"go.title": "OpenCode Go | Недорогие модели для кодинга для всех",
|
||||||
"go.meta.description":
|
"go.meta.description":
|
||||||
"Go начинается с $5 за первый месяц, затем $10/месяц, с щедрыми лимитами запросов за 5 часов для GLM-5.1, GLM-5, Kimi K2.5, Kimi K2.6, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.7 Max, Qwen3.6 Plus, Qwen3.5 Plus, MiniMax M2.5, MiniMax M2.7, DeepSeek V4 Pro и DeepSeek V4 Flash.",
|
"Go начинается с $5 за первый месяц, затем $10/месяц, с щедрыми лимитами запросов за 5 часов для GLM-5.1, GLM-5, Kimi K2.5, Kimi K2.6, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.5 Plus, Qwen3.6 Plus, MiniMax M2.5, MiniMax M2.7, DeepSeek V4 Pro и DeepSeek V4 Flash.",
|
||||||
"go.hero.title": "Недорогие модели для кодинга для всех",
|
"go.hero.title": "Недорогие модели для кодинга для всех",
|
||||||
"go.hero.body":
|
"go.hero.body":
|
||||||
"Go открывает доступ к агентам-программистам разработчикам по всему миру. Предлагая щедрые лимиты и надежный доступ к наиболее способным моделям с открытым исходным кодом, вы можете создавать проекты с мощными агентами, не беспокоясь о затратах или доступности.",
|
"Go открывает доступ к агентам-программистам разработчикам по всему миру. Предлагая щедрые лимиты и надежный доступ к наиболее способным моделям с открытым исходным кодом, вы можете создавать проекты с мощными агентами, не беспокоясь о затратах или доступности.",
|
||||||
@@ -305,7 +305,7 @@ export const dict = {
|
|||||||
"go.problem.item2": "Щедрые лимиты и надежный доступ",
|
"go.problem.item2": "Щедрые лимиты и надежный доступ",
|
||||||
"go.problem.item3": "Создан для максимального числа программистов",
|
"go.problem.item3": "Создан для максимального числа программистов",
|
||||||
"go.problem.item4":
|
"go.problem.item4":
|
||||||
"Включает GLM-5.1, GLM-5, Kimi K2.5, Kimi K2.6, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.7 Max, Qwen3.6 Plus, Qwen3.5 Plus, MiniMax M2.5, MiniMax M2.7, DeepSeek V4 Pro и DeepSeek V4 Flash",
|
"Включает GLM-5.1, GLM-5, Kimi K2.5, Kimi K2.6, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.5 Plus, Qwen3.6 Plus, MiniMax M2.5, MiniMax M2.7, DeepSeek V4 Pro и DeepSeek V4 Flash",
|
||||||
"go.how.title": "Как работает Go",
|
"go.how.title": "Как работает Go",
|
||||||
"go.how.body":
|
"go.how.body":
|
||||||
"Go начинается с $5 за первый месяц, затем $10/месяц. Вы можете использовать его с OpenCode или любым агентом.",
|
"Go начинается с $5 за первый месяц, затем $10/месяц. Вы можете использовать его с OpenCode или любым агентом.",
|
||||||
@@ -331,7 +331,7 @@ export const dict = {
|
|||||||
"go.faq.a2": "Go включает перечисленные ниже модели с щедрыми лимитами и надежным доступом.",
|
"go.faq.a2": "Go включает перечисленные ниже модели с щедрыми лимитами и надежным доступом.",
|
||||||
"go.faq.q3": "Go — это то же самое, что и Zen?",
|
"go.faq.q3": "Go — это то же самое, что и Zen?",
|
||||||
"go.faq.a3":
|
"go.faq.a3":
|
||||||
"Нет. Zen - это оплата по мере использования, в то время как Go начинается с $5 за первый месяц, затем $10/месяц, с щедрыми лимитами и надежным доступом к моделям с открытым исходным кодом GLM-5.1, GLM-5, Kimi K2.5, Kimi K2.6, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.7 Max, Qwen3.6 Plus, Qwen3.5 Plus, MiniMax M2.5, MiniMax M2.7, DeepSeek V4 Pro и DeepSeek V4 Flash.",
|
"Нет. Zen - это оплата по мере использования, в то время как Go начинается с $5 за первый месяц, затем $10/месяц, с щедрыми лимитами и надежным доступом к моделям с открытым исходным кодом GLM-5.1, GLM-5, Kimi K2.5, Kimi K2.6, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.5 Plus, Qwen3.6 Plus, MiniMax M2.5, MiniMax M2.7, DeepSeek V4 Pro и DeepSeek V4 Flash.",
|
||||||
"go.faq.q4": "Сколько стоит Go?",
|
"go.faq.q4": "Сколько стоит Go?",
|
||||||
"go.faq.a4.p1.beforePricing": "Go стоит",
|
"go.faq.a4.p1.beforePricing": "Go стоит",
|
||||||
"go.faq.a4.p1.pricingLink": "$5 за первый месяц",
|
"go.faq.a4.p1.pricingLink": "$5 за первый месяц",
|
||||||
@@ -355,7 +355,7 @@ export const dict = {
|
|||||||
|
|
||||||
"go.faq.q9": "В чем разница между бесплатными моделями и Go?",
|
"go.faq.q9": "В чем разница между бесплатными моделями и Go?",
|
||||||
"go.faq.a9":
|
"go.faq.a9":
|
||||||
"Бесплатные модели включают Big Pickle плюс промо-модели, доступные на данный момент, с квотой 200 запросов/день. Go включает GLM-5.1, GLM-5, Kimi K2.5, Kimi K2.6, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.7 Max, Qwen3.6 Plus, Qwen3.5 Plus, MiniMax M2.5, MiniMax M2.7, DeepSeek V4 Pro и DeepSeek V4 Flash с более высокими квотами запросов, применяемыми в скользящих окнах (5 часов, неделя и месяц), что примерно эквивалентно $12 за 5 часов, $30 в неделю и $60 в месяц (фактическое количество запросов зависит от модели и использования).",
|
"Бесплатные модели включают Big Pickle плюс промо-модели, доступные на данный момент, с квотой 200 запросов/день. Go включает GLM-5.1, GLM-5, Kimi K2.5, Kimi K2.6, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.5 Plus, Qwen3.6 Plus, MiniMax M2.5, MiniMax M2.7, DeepSeek V4 Pro и DeepSeek V4 Flash с более высокими квотами запросов, применяемыми в скользящих окнах (5 часов, неделя и месяц), что примерно эквивалентно $12 за 5 часов, $30 в неделю и $60 в месяц (фактическое количество запросов зависит от модели и использования).",
|
||||||
|
|
||||||
"zen.api.error.rateLimitExceeded": "Превышен лимит запросов. Пожалуйста, попробуйте позже.",
|
"zen.api.error.rateLimitExceeded": "Превышен лимит запросов. Пожалуйста, попробуйте позже.",
|
||||||
"zen.api.error.modelNotSupported": "Модель {{model}} не поддерживается",
|
"zen.api.error.modelNotSupported": "Модель {{model}} не поддерживается",
|
||||||
|
|||||||
@@ -250,7 +250,7 @@ export const dict = {
|
|||||||
|
|
||||||
"go.title": "OpenCode Go | โมเดลเขียนโค้ดราคาประหยัดสำหรับทุกคน",
|
"go.title": "OpenCode Go | โมเดลเขียนโค้ดราคาประหยัดสำหรับทุกคน",
|
||||||
"go.meta.description":
|
"go.meta.description":
|
||||||
"Go เริ่มต้นที่ $5 สำหรับเดือนแรก จากนั้น $10/เดือน พร้อมขีดจำกัดคำขอ 5 ชั่วโมงที่เอื้อเฟื้อสำหรับ GLM-5.1, GLM-5, Kimi K2.5, Kimi K2.6, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.7 Max, Qwen3.6 Plus, Qwen3.5 Plus, MiniMax M2.5, MiniMax M2.7, DeepSeek V4 Pro และ DeepSeek V4 Flash",
|
"Go เริ่มต้นที่ $5 สำหรับเดือนแรก จากนั้น $10/เดือน พร้อมขีดจำกัดคำขอ 5 ชั่วโมงที่เอื้อเฟื้อสำหรับ GLM-5.1, GLM-5, Kimi K2.5, Kimi K2.6, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.5 Plus, Qwen3.6 Plus, MiniMax M2.5, MiniMax M2.7, DeepSeek V4 Pro และ DeepSeek V4 Flash",
|
||||||
"go.hero.title": "โมเดลเขียนโค้ดราคาประหยัดสำหรับทุกคน",
|
"go.hero.title": "โมเดลเขียนโค้ดราคาประหยัดสำหรับทุกคน",
|
||||||
"go.hero.body":
|
"go.hero.body":
|
||||||
"Go นำการเขียนโค้ดแบบเอเจนต์มาสู่นักเขียนโปรแกรมทั่วโลก เสนอขีดจำกัดที่กว้างขวางและการเข้าถึงโมเดลโอเพนซอร์สที่มีความสามารถสูงสุดได้อย่างน่าเชื่อถือ เพื่อให้คุณสามารถสร้างสรรค์ด้วยเอเจนต์ที่ทรงพลังโดยไม่ต้องกังวลเรื่องค่าใช้จ่ายหรือความพร้อมใช้งาน",
|
"Go นำการเขียนโค้ดแบบเอเจนต์มาสู่นักเขียนโปรแกรมทั่วโลก เสนอขีดจำกัดที่กว้างขวางและการเข้าถึงโมเดลโอเพนซอร์สที่มีความสามารถสูงสุดได้อย่างน่าเชื่อถือ เพื่อให้คุณสามารถสร้างสรรค์ด้วยเอเจนต์ที่ทรงพลังโดยไม่ต้องกังวลเรื่องค่าใช้จ่ายหรือความพร้อมใช้งาน",
|
||||||
@@ -298,7 +298,7 @@ export const dict = {
|
|||||||
"go.problem.item2": "ขีดจำกัดที่กว้างขวางและการเข้าถึงที่เชื่อถือได้",
|
"go.problem.item2": "ขีดจำกัดที่กว้างขวางและการเข้าถึงที่เชื่อถือได้",
|
||||||
"go.problem.item3": "สร้างขึ้นเพื่อโปรแกรมเมอร์จำนวนมากที่สุดเท่าที่จะเป็นไปได้",
|
"go.problem.item3": "สร้างขึ้นเพื่อโปรแกรมเมอร์จำนวนมากที่สุดเท่าที่จะเป็นไปได้",
|
||||||
"go.problem.item4":
|
"go.problem.item4":
|
||||||
"รวมถึง GLM-5.1, GLM-5, Kimi K2.5, Kimi K2.6, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.7 Max, Qwen3.6 Plus, Qwen3.5 Plus, MiniMax M2.5, MiniMax M2.7, DeepSeek V4 Pro และ DeepSeek V4 Flash",
|
"รวมถึง GLM-5.1, GLM-5, Kimi K2.5, Kimi K2.6, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.5 Plus, Qwen3.6 Plus, MiniMax M2.5, MiniMax M2.7, DeepSeek V4 Pro และ DeepSeek V4 Flash",
|
||||||
"go.how.title": "Go ทำงานอย่างไร",
|
"go.how.title": "Go ทำงานอย่างไร",
|
||||||
"go.how.body": "Go เริ่มต้นที่ $5 สำหรับเดือนแรก จากนั้น $10/เดือน คุณสามารถใช้กับ OpenCode หรือเอเจนต์ใดก็ได้",
|
"go.how.body": "Go เริ่มต้นที่ $5 สำหรับเดือนแรก จากนั้น $10/เดือน คุณสามารถใช้กับ OpenCode หรือเอเจนต์ใดก็ได้",
|
||||||
"go.how.step1.title": "สร้างบัญชี",
|
"go.how.step1.title": "สร้างบัญชี",
|
||||||
@@ -323,7 +323,7 @@ export const dict = {
|
|||||||
"go.faq.a2": "Go รวมโมเดลด้านล่างนี้ พร้อมขีดจำกัดที่มากและการเข้าถึงที่เชื่อถือได้",
|
"go.faq.a2": "Go รวมโมเดลด้านล่างนี้ พร้อมขีดจำกัดที่มากและการเข้าถึงที่เชื่อถือได้",
|
||||||
"go.faq.q3": "Go เหมือนกับ Zen หรือไม่?",
|
"go.faq.q3": "Go เหมือนกับ Zen หรือไม่?",
|
||||||
"go.faq.a3":
|
"go.faq.a3":
|
||||||
"ไม่ Zen เป็นแบบจ่ายตามการใช้งาน ในขณะที่ Go เริ่มต้นที่ $5 สำหรับเดือนแรก จากนั้น $10/เดือน พร้อมขีดจำกัดที่เอื้อเฟื้อและการเข้าถึงโมเดลโอเพนซอร์ส GLM-5.1, GLM-5, Kimi K2.5, Kimi K2.6, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.7 Max, Qwen3.6 Plus, Qwen3.5 Plus, MiniMax M2.5, MiniMax M2.7, DeepSeek V4 Pro และ DeepSeek V4 Flash อย่างเชื่อถือได้",
|
"ไม่ Zen เป็นแบบจ่ายตามการใช้งาน ในขณะที่ Go เริ่มต้นที่ $5 สำหรับเดือนแรก จากนั้น $10/เดือน พร้อมขีดจำกัดที่เอื้อเฟื้อและการเข้าถึงโมเดลโอเพนซอร์ส GLM-5.1, GLM-5, Kimi K2.5, Kimi K2.6, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.5 Plus, Qwen3.6 Plus, MiniMax M2.5, MiniMax M2.7, DeepSeek V4 Pro และ DeepSeek V4 Flash อย่างเชื่อถือได้",
|
||||||
"go.faq.q4": "Go ราคาเท่าไหร่?",
|
"go.faq.q4": "Go ราคาเท่าไหร่?",
|
||||||
"go.faq.a4.p1.beforePricing": "Go ราคา",
|
"go.faq.a4.p1.beforePricing": "Go ราคา",
|
||||||
"go.faq.a4.p1.pricingLink": "$5 เดือนแรก",
|
"go.faq.a4.p1.pricingLink": "$5 เดือนแรก",
|
||||||
@@ -346,7 +346,7 @@ export const dict = {
|
|||||||
|
|
||||||
"go.faq.q9": "ความแตกต่างระหว่างโมเดลฟรีและ Go คืออะไร?",
|
"go.faq.q9": "ความแตกต่างระหว่างโมเดลฟรีและ Go คืออะไร?",
|
||||||
"go.faq.a9":
|
"go.faq.a9":
|
||||||
"โมเดลฟรีรวมถึง Big Pickle บวกกับโมเดลโปรโมชั่นที่มีให้ในขณะนั้น ด้วยโควต้า 200 คำขอ/วัน Go รวมถึง GLM-5.1, GLM-5, Kimi K2.5, Kimi K2.6, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.7 Max, Qwen3.6 Plus, Qwen3.5 Plus, MiniMax M2.5, MiniMax M2.7, DeepSeek V4 Pro และ DeepSeek V4 Flash ที่มีโควต้าคำขอสูงกว่า ซึ่งบังคับใช้ผ่านช่วงเวลาหมุนเวียน (5 ชั่วโมง, รายสัปดาห์ และรายเดือน) เทียบเท่าประมาณ $12 ต่อ 5 ชั่วโมง, $30 ต่อสัปดาห์ และ $60 ต่อเดือน (จำนวนคำขอจริงจะแตกต่างกันไปตามโมเดลและการใช้งาน)",
|
"โมเดลฟรีรวมถึง Big Pickle บวกกับโมเดลโปรโมชั่นที่มีให้ในขณะนั้น ด้วยโควต้า 200 คำขอ/วัน Go รวมถึง GLM-5.1, GLM-5, Kimi K2.5, Kimi K2.6, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.5 Plus, Qwen3.6 Plus, MiniMax M2.5, MiniMax M2.7, DeepSeek V4 Pro และ DeepSeek V4 Flash ที่มีโควต้าคำขอสูงกว่า ซึ่งบังคับใช้ผ่านช่วงเวลาหมุนเวียน (5 ชั่วโมง, รายสัปดาห์ และรายเดือน) เทียบเท่าประมาณ $12 ต่อ 5 ชั่วโมง, $30 ต่อสัปดาห์ และ $60 ต่อเดือน (จำนวนคำขอจริงจะแตกต่างกันไปตามโมเดลและการใช้งาน)",
|
||||||
|
|
||||||
"zen.api.error.rateLimitExceeded": "เกินขีดจำกัดอัตราการใช้งาน กรุณาลองใหม่ในภายหลัง",
|
"zen.api.error.rateLimitExceeded": "เกินขีดจำกัดอัตราการใช้งาน กรุณาลองใหม่ในภายหลัง",
|
||||||
"zen.api.error.modelNotSupported": "ไม่รองรับโมเดล {{model}}",
|
"zen.api.error.modelNotSupported": "ไม่รองรับโมเดล {{model}}",
|
||||||
|
|||||||
@@ -253,7 +253,7 @@ export const dict = {
|
|||||||
|
|
||||||
"go.title": "OpenCode Go | Herkes için düşük maliyetli kodlama modelleri",
|
"go.title": "OpenCode Go | Herkes için düşük maliyetli kodlama modelleri",
|
||||||
"go.meta.description":
|
"go.meta.description":
|
||||||
"Go ilk ay $5, sonrasında ayda 10$ fiyatıyla başlar; GLM-5.1, GLM-5, Kimi K2.5, Kimi K2.6, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.7 Max, Qwen3.6 Plus, Qwen3.5 Plus, MiniMax M2.5, MiniMax M2.7, DeepSeek V4 Pro ve DeepSeek V4 Flash için cömert 5 saatlik istek limitleri sunar.",
|
"Go ilk ay $5, sonrasında ayda 10$ fiyatıyla başlar; GLM-5.1, GLM-5, Kimi K2.5, Kimi K2.6, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.5 Plus, Qwen3.6 Plus, MiniMax M2.5, MiniMax M2.7, DeepSeek V4 Pro ve DeepSeek V4 Flash için cömert 5 saatlik istek limitleri sunar.",
|
||||||
"go.hero.title": "Herkes için düşük maliyetli kodlama modelleri",
|
"go.hero.title": "Herkes için düşük maliyetli kodlama modelleri",
|
||||||
"go.hero.body":
|
"go.hero.body":
|
||||||
"Go, dünya çapındaki programcılara ajan tabanlı kodlama getiriyor. En yetenekli açık kaynaklı modellere cömert limitler ve güvenilir erişim sunarak, maliyet veya erişilebilirlik konusunda endişelenmeden güçlü ajanlarla geliştirme yapmanızı sağlar.",
|
"Go, dünya çapındaki programcılara ajan tabanlı kodlama getiriyor. En yetenekli açık kaynaklı modellere cömert limitler ve güvenilir erişim sunarak, maliyet veya erişilebilirlik konusunda endişelenmeden güçlü ajanlarla geliştirme yapmanızı sağlar.",
|
||||||
@@ -303,7 +303,7 @@ export const dict = {
|
|||||||
"go.problem.item2": "Cömert limitler ve güvenilir erişim",
|
"go.problem.item2": "Cömert limitler ve güvenilir erişim",
|
||||||
"go.problem.item3": "Mümkün olduğunca çok programcı için geliştirildi",
|
"go.problem.item3": "Mümkün olduğunca çok programcı için geliştirildi",
|
||||||
"go.problem.item4":
|
"go.problem.item4":
|
||||||
"GLM-5.1, GLM-5, Kimi K2.5, Kimi K2.6, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.7 Max, Qwen3.6 Plus, Qwen3.5 Plus, MiniMax M2.5, MiniMax M2.7, DeepSeek V4 Pro ve DeepSeek V4 Flash içerir",
|
"GLM-5.1, GLM-5, Kimi K2.5, Kimi K2.6, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.5 Plus, Qwen3.6 Plus, MiniMax M2.5, MiniMax M2.7, DeepSeek V4 Pro ve DeepSeek V4 Flash içerir",
|
||||||
"go.how.title": "Go nasıl çalışır?",
|
"go.how.title": "Go nasıl çalışır?",
|
||||||
"go.how.body":
|
"go.how.body":
|
||||||
"Go ilk ay $5, sonrasında ayda 10$ fiyatıyla başlar. OpenCode veya herhangi bir ajanla kullanabilirsiniz.",
|
"Go ilk ay $5, sonrasında ayda 10$ fiyatıyla başlar. OpenCode veya herhangi bir ajanla kullanabilirsiniz.",
|
||||||
@@ -329,7 +329,7 @@ export const dict = {
|
|||||||
"go.faq.a2": "Go, aşağıda listelenen modelleri cömert limitler ve güvenilir erişimle sunar.",
|
"go.faq.a2": "Go, aşağıda listelenen modelleri cömert limitler ve güvenilir erişimle sunar.",
|
||||||
"go.faq.q3": "Go, Zen ile aynı mı?",
|
"go.faq.q3": "Go, Zen ile aynı mı?",
|
||||||
"go.faq.a3":
|
"go.faq.a3":
|
||||||
"Hayır. Zen kullandıkça öde modelidir, Go ise ilk ay $5, sonrasında ayda 10$ fiyatıyla başlar; GLM-5.1, GLM-5, Kimi K2.5, Kimi K2.6, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.7 Max, Qwen3.6 Plus, Qwen3.5 Plus, MiniMax M2.5, MiniMax M2.7, DeepSeek V4 Pro ve DeepSeek V4 Flash açık kaynak modellerine cömert limitler ve güvenilir erişim sunar.",
|
"Hayır. Zen kullandıkça öde modelidir, Go ise ilk ay $5, sonrasında ayda 10$ fiyatıyla başlar; GLM-5.1, GLM-5, Kimi K2.5, Kimi K2.6, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.5 Plus, Qwen3.6 Plus, MiniMax M2.5, MiniMax M2.7, DeepSeek V4 Pro ve DeepSeek V4 Flash açık kaynak modellerine cömert limitler ve güvenilir erişim sunar.",
|
||||||
"go.faq.q4": "Go ne kadar?",
|
"go.faq.q4": "Go ne kadar?",
|
||||||
"go.faq.a4.p1.beforePricing": "Go'nun maliyeti",
|
"go.faq.a4.p1.beforePricing": "Go'nun maliyeti",
|
||||||
"go.faq.a4.p1.pricingLink": "İlk ay $5",
|
"go.faq.a4.p1.pricingLink": "İlk ay $5",
|
||||||
@@ -353,7 +353,7 @@ export const dict = {
|
|||||||
|
|
||||||
"go.faq.q9": "Ücretsiz modeller ve Go arasındaki fark nedir?",
|
"go.faq.q9": "Ücretsiz modeller ve Go arasındaki fark nedir?",
|
||||||
"go.faq.a9":
|
"go.faq.a9":
|
||||||
"Ücretsiz modeller, günlük 200 istek kotası ile Big Pickle ve o sırada mevcut olan promosyonel modelleri içerir. Go ise GLM-5.1, GLM-5, Kimi K2.5, Kimi K2.6, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.7 Max, Qwen3.6 Plus, Qwen3.5 Plus, MiniMax M2.5, MiniMax M2.7, DeepSeek V4 Pro ve DeepSeek V4 Flash modellerini; yuvarlanan pencereler (5 saatlik, haftalık ve aylık) üzerinden uygulanan daha yüksek istek kotalarıyla içerir. Bu kotalar kabaca her 5 saatte 12$, haftada 30$ ve ayda 60$ değerine eşdeğerdir (gerçek istek sayıları modele ve kullanıma göre değişir).",
|
"Ücretsiz modeller, günlük 200 istek kotası ile Big Pickle ve o sırada mevcut olan promosyonel modelleri içerir. Go ise GLM-5.1, GLM-5, Kimi K2.5, Kimi K2.6, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.5 Plus, Qwen3.6 Plus, MiniMax M2.5, MiniMax M2.7, DeepSeek V4 Pro ve DeepSeek V4 Flash modellerini; yuvarlanan pencereler (5 saatlik, haftalık ve aylık) üzerinden uygulanan daha yüksek istek kotalarıyla içerir. Bu kotalar kabaca her 5 saatte 12$, haftada 30$ ve ayda 60$ değerine eşdeğerdir (gerçek istek sayıları modele ve kullanıma göre değişir).",
|
||||||
|
|
||||||
"zen.api.error.rateLimitExceeded": "İstek limiti aşıldı. Lütfen daha sonra tekrar deneyin.",
|
"zen.api.error.rateLimitExceeded": "İstek limiti aşıldı. Lütfen daha sonra tekrar deneyin.",
|
||||||
"zen.api.error.modelNotSupported": "{{model}} modeli desteklenmiyor",
|
"zen.api.error.modelNotSupported": "{{model}} modeli desteklenmiyor",
|
||||||
|
|||||||
@@ -252,7 +252,7 @@ export const dict = {
|
|||||||
|
|
||||||
"go.title": "OpenCode Go | Недорогі моделі кодування для всіх",
|
"go.title": "OpenCode Go | Недорогі моделі кодування для всіх",
|
||||||
"go.meta.description":
|
"go.meta.description":
|
||||||
"Go починається від $5 за перший місяць, потім $10/місяць, з generous 5-годинними лімітами запитів для GLM-5.1, GLM-5, Kimi K2.5, Kimi K2.6, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.7 Max, Qwen3.6 Plus, Qwen3.5 Plus, MiniMax M2.5, MiniMax M2.7, DeepSeek V4 Pro та DeepSeek V4 Flash.",
|
"Go починається від $5 за перший місяць, потім $10/місяць, з generous 5-годинними лімітами запитів для GLM-5.1, GLM-5, Kimi K2.5, Kimi K2.6, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.5 Plus, Qwen3.6 Plus, MiniMax M2.5, MiniMax M2.7, DeepSeek V4 Pro та DeepSeek V4 Flash.",
|
||||||
"go.hero.title": "Недорогі моделі кодування для всіх",
|
"go.hero.title": "Недорогі моделі кодування для всіх",
|
||||||
"go.hero.body":
|
"go.hero.body":
|
||||||
"Go надає агентне програмування програмістам у всьому світі, пропонуючи щедрі ліміти та надійний доступ до найкращих моделей з відкритим кодом.",
|
"Go надає агентне програмування програмістам у всьому світі, пропонуючи щедрі ліміти та надійний доступ до найкращих моделей з відкритим кодом.",
|
||||||
@@ -301,7 +301,7 @@ export const dict = {
|
|||||||
"go.problem.item2": "Щедрі ліміти та надійний доступ",
|
"go.problem.item2": "Щедрі ліміти та надійний доступ",
|
||||||
"go.problem.item3": "Створено для якомога більшої кількості програмістів",
|
"go.problem.item3": "Створено для якомога більшої кількості програмістів",
|
||||||
"go.problem.item4":
|
"go.problem.item4":
|
||||||
"Включає GLM-5.1, GLM-5, Kimi K2.5, Kimi K2.6, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.7 Max, Qwen3.6 Plus, Qwen3.5 Plus, MiniMax M2.5, MiniMax M2.7, DeepSeek V4 Pro та DeepSeek V4 Flash",
|
"Включає GLM-5.1, GLM-5, Kimi K2.5, Kimi K2.6, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.5 Plus, Qwen3.6 Plus, MiniMax M2.5, MiniMax M2.7, DeepSeek V4 Pro та DeepSeek V4 Flash",
|
||||||
"go.how.title": "Як працює Go",
|
"go.how.title": "Як працює Go",
|
||||||
"go.how.body":
|
"go.how.body":
|
||||||
"Go починається від $5 за перший місяць, потім $10/місяць. Використовуйте з OpenCode або будь-яким агентом.",
|
"Go починається від $5 за перший місяць, потім $10/місяць. Використовуйте з OpenCode або будь-яким агентом.",
|
||||||
@@ -350,7 +350,7 @@ export const dict = {
|
|||||||
|
|
||||||
"go.faq.q9": "Яка різниця між безкоштовними моделями та Go?",
|
"go.faq.q9": "Яка різниця між безкоштовними моделями та Go?",
|
||||||
"go.faq.a9":
|
"go.faq.a9":
|
||||||
"Безкоштовні моделі включають Big Pickle та акційні моделі з лімітом 200 запитів/день. Go включає GLM-5.1, GLM-5, Kimi K2.5, Kimi K2.6, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.7 Max, Qwen3.6 Plus, Qwen3.5 Plus, MiniMax M2.5, MiniMax M2.7, DeepSeek V4 Pro та DeepSeek V4 Flash із вищими лімітами.",
|
"Безкоштовні моделі включають Big Pickle та акційні моделі з лімітом 200 запитів/день. Go включає GLM-5.1, GLM-5, Kimi K2.5, Kimi K2.6, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.5 Plus, Qwen3.6 Plus, MiniMax M2.5, MiniMax M2.7, DeepSeek V4 Pro та DeepSeek V4 Flash із вищими лімітами.",
|
||||||
|
|
||||||
"zen.api.error.rateLimitExceeded": "Перевищено ліміт запитів. Спробуйте пізніше.",
|
"zen.api.error.rateLimitExceeded": "Перевищено ліміт запитів. Спробуйте пізніше.",
|
||||||
"zen.api.error.modelNotSupported": "Модель {{model}} не підтримується",
|
"zen.api.error.modelNotSupported": "Модель {{model}} не підтримується",
|
||||||
|
|||||||
@@ -241,7 +241,7 @@ export const dict = {
|
|||||||
|
|
||||||
"go.title": "OpenCode Go | 人人可用的低成本编程模型",
|
"go.title": "OpenCode Go | 人人可用的低成本编程模型",
|
||||||
"go.meta.description":
|
"go.meta.description":
|
||||||
"Go 首月 $5,之后 $10/月,提供对 GLM-5.1、GLM-5、Kimi K2.5、Kimi K2.6、MiMo-V2.5-Pro、MiMo-V2.5、Qwen3.7 Max、Qwen3.6 Plus、Qwen3.5 Plus、MiniMax M2.5、MiniMax M2.7、DeepSeek V4 Pro 和 DeepSeek V4 Flash 的 5 小时充裕请求额度。",
|
"Go 首月 $5,之后 $10/月,提供对 GLM-5.1、GLM-5、Kimi K2.5、Kimi K2.6、MiMo-V2.5-Pro、MiMo-V2.5、Qwen3.5 Plus、Qwen3.6 Plus、MiniMax M2.5、MiniMax M2.7、DeepSeek V4 Pro 和 DeepSeek V4 Flash 的 5 小时充裕请求额度。",
|
||||||
"go.hero.title": "人人可用的低成本编程模型",
|
"go.hero.title": "人人可用的低成本编程模型",
|
||||||
"go.hero.body":
|
"go.hero.body":
|
||||||
"Go 将代理编程带给全世界的程序员。提供充裕的限额和对最强大的开源模型的可靠访问,让您可以利用强大的代理进行构建,而无需担心成本或可用性。",
|
"Go 将代理编程带给全世界的程序员。提供充裕的限额和对最强大的开源模型的可靠访问,让您可以利用强大的代理进行构建,而无需担心成本或可用性。",
|
||||||
@@ -289,7 +289,7 @@ export const dict = {
|
|||||||
"go.problem.item2": "充裕的限额和可靠的访问",
|
"go.problem.item2": "充裕的限额和可靠的访问",
|
||||||
"go.problem.item3": "为尽可能多的程序员打造",
|
"go.problem.item3": "为尽可能多的程序员打造",
|
||||||
"go.problem.item4":
|
"go.problem.item4":
|
||||||
"包含 GLM-5.1, GLM-5, Kimi K2.5、Kimi K2.6, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.7 Max, Qwen3.6 Plus, Qwen3.5 Plus, MiniMax M2.5、MiniMax M2.7、DeepSeek V4 Pro 和 DeepSeek V4 Flash",
|
"包含 GLM-5.1, GLM-5, Kimi K2.5、Kimi K2.6, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.5 Plus, Qwen3.6 Plus, MiniMax M2.5、MiniMax M2.7、DeepSeek V4 Pro 和 DeepSeek V4 Flash",
|
||||||
"go.how.title": "Go 如何工作",
|
"go.how.title": "Go 如何工作",
|
||||||
"go.how.body": "Go 起价为首月 $5,之后 $10/月。您可以将其与 OpenCode 或任何代理搭配使用。",
|
"go.how.body": "Go 起价为首月 $5,之后 $10/月。您可以将其与 OpenCode 或任何代理搭配使用。",
|
||||||
"go.how.step1.title": "创建账户",
|
"go.how.step1.title": "创建账户",
|
||||||
@@ -311,7 +311,7 @@ export const dict = {
|
|||||||
"go.faq.a2": "Go 包含下方列出的模型,提供充足的限额和可靠的访问。",
|
"go.faq.a2": "Go 包含下方列出的模型,提供充足的限额和可靠的访问。",
|
||||||
"go.faq.q3": "Go 和 Zen 一样吗?",
|
"go.faq.q3": "Go 和 Zen 一样吗?",
|
||||||
"go.faq.a3":
|
"go.faq.a3":
|
||||||
"不。Zen 是按量付费,而 Go 首月 $5,之后 $10/月,提供充裕的额度,并可可靠地访问 GLM-5.1、GLM-5、Kimi K2.5、Kimi K2.6、MiMo-V2.5-Pro、MiMo-V2.5、Qwen3.7 Max、Qwen3.6 Plus、Qwen3.5 Plus、MiniMax M2.5、MiniMax M2.7、DeepSeek V4 Pro 和 DeepSeek V4 Flash 等开源模型。",
|
"不。Zen 是按量付费,而 Go 首月 $5,之后 $10/月,提供充裕的额度,并可可靠地访问 GLM-5.1、GLM-5、Kimi K2.5、Kimi K2.6、MiMo-V2.5-Pro、MiMo-V2.5、Qwen3.5 Plus、Qwen3.6 Plus、MiniMax M2.5、MiniMax M2.7、DeepSeek V4 Pro 和 DeepSeek V4 Flash 等开源模型。",
|
||||||
"go.faq.q4": "Go 多少钱?",
|
"go.faq.q4": "Go 多少钱?",
|
||||||
"go.faq.a4.p1.beforePricing": "Go 费用为",
|
"go.faq.a4.p1.beforePricing": "Go 费用为",
|
||||||
"go.faq.a4.p1.pricingLink": "首月 $5",
|
"go.faq.a4.p1.pricingLink": "首月 $5",
|
||||||
@@ -333,7 +333,7 @@ export const dict = {
|
|||||||
|
|
||||||
"go.faq.q9": "免费模型和 Go 之间的区别是什么?",
|
"go.faq.q9": "免费模型和 Go 之间的区别是什么?",
|
||||||
"go.faq.a9":
|
"go.faq.a9":
|
||||||
"免费模型包含 Big Pickle 加上当时可用的促销模型,每天有 200 次请求的配额。Go 包含 GLM-5.1, GLM-5, Kimi K2.5、Kimi K2.6, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.7 Max, Qwen3.6 Plus, Qwen3.5 Plus, MiniMax M2.5、MiniMax M2.7、DeepSeek V4 Pro 和 DeepSeek V4 Flash,并在滚动窗口(5 小时、每周和每月)内执行更高的请求配额,大致相当于每 5 小时 $12、每周 $30 和每月 $60(实际请求计数因模型和使用情况而异)。",
|
"免费模型包含 Big Pickle 加上当时可用的促销模型,每天有 200 次请求的配额。Go 包含 GLM-5.1, GLM-5, Kimi K2.5、Kimi K2.6, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.5 Plus, Qwen3.6 Plus, MiniMax M2.5、MiniMax M2.7、DeepSeek V4 Pro 和 DeepSeek V4 Flash,并在滚动窗口(5 小时、每周和每月)内执行更高的请求配额,大致相当于每 5 小时 $12、每周 $30 和每月 $60(实际请求计数因模型和使用情况而异)。",
|
||||||
|
|
||||||
"zen.api.error.rateLimitExceeded": "超出速率限制。请稍后重试。",
|
"zen.api.error.rateLimitExceeded": "超出速率限制。请稍后重试。",
|
||||||
"zen.api.error.modelNotSupported": "不支持模型 {{model}}",
|
"zen.api.error.modelNotSupported": "不支持模型 {{model}}",
|
||||||
|
|||||||
@@ -241,7 +241,7 @@ export const dict = {
|
|||||||
|
|
||||||
"go.title": "OpenCode Go | 低成本全民編碼模型",
|
"go.title": "OpenCode Go | 低成本全民編碼模型",
|
||||||
"go.meta.description":
|
"go.meta.description":
|
||||||
"Go 首月 $5,之後 $10/月,提供對 GLM-5.1、GLM-5、Kimi K2.5、Kimi K2.6、MiMo-V2.5-Pro、MiMo-V2.5、Qwen3.7 Max、Qwen3.6 Plus、Qwen3.5 Plus、MiniMax M2.5、MiniMax M2.7、DeepSeek V4 Pro 和 DeepSeek V4 Flash 的 5 小時充裕請求額度。",
|
"Go 首月 $5,之後 $10/月,提供對 GLM-5.1、GLM-5、Kimi K2.5、Kimi K2.6、MiMo-V2.5-Pro、MiMo-V2.5、Qwen3.5 Plus、Qwen3.6 Plus、MiniMax M2.5、MiniMax M2.7、DeepSeek V4 Pro 和 DeepSeek V4 Flash 的 5 小時充裕請求額度。",
|
||||||
"go.hero.title": "低成本全民編碼模型",
|
"go.hero.title": "低成本全民編碼模型",
|
||||||
"go.hero.body":
|
"go.hero.body":
|
||||||
"Go 將代理編碼帶給全世界的程式設計師。提供寬裕的限額以及對最強大開源模型的穩定存取,讓你可以使用強大的代理進行構建,而無需擔心成本或可用性。",
|
"Go 將代理編碼帶給全世界的程式設計師。提供寬裕的限額以及對最強大開源模型的穩定存取,讓你可以使用強大的代理進行構建,而無需擔心成本或可用性。",
|
||||||
@@ -289,7 +289,7 @@ export const dict = {
|
|||||||
"go.problem.item2": "寬裕的限額與穩定存取",
|
"go.problem.item2": "寬裕的限額與穩定存取",
|
||||||
"go.problem.item3": "專為盡可能多的程式設計師打造",
|
"go.problem.item3": "專為盡可能多的程式設計師打造",
|
||||||
"go.problem.item4":
|
"go.problem.item4":
|
||||||
"包含 GLM-5.1、GLM-5、Kimi K2.5、Kimi K2.6、MiMo-V2.5-Pro、MiMo-V2.5、Qwen3.7 Max、Qwen3.6 Plus、Qwen3.5 Plus、MiniMax M2.5、MiniMax M2.7、DeepSeek V4 Pro 與 DeepSeek V4 Flash",
|
"包含 GLM-5.1、GLM-5、Kimi K2.5、Kimi K2.6、MiMo-V2.5-Pro、MiMo-V2.5、Qwen3.5 Plus、Qwen3.6 Plus、MiniMax M2.5、MiniMax M2.7、DeepSeek V4 Pro 與 DeepSeek V4 Flash",
|
||||||
"go.how.title": "Go 如何運作",
|
"go.how.title": "Go 如何運作",
|
||||||
"go.how.body": "Go 起價為首月 $5,之後 $10/月。您可以將其與 OpenCode 或任何代理搭配使用。",
|
"go.how.body": "Go 起價為首月 $5,之後 $10/月。您可以將其與 OpenCode 或任何代理搭配使用。",
|
||||||
"go.how.step1.title": "建立帳號",
|
"go.how.step1.title": "建立帳號",
|
||||||
@@ -311,7 +311,7 @@ export const dict = {
|
|||||||
"go.faq.a2": "Go 包含下方列出的模型,提供充足的額度與穩定的存取。",
|
"go.faq.a2": "Go 包含下方列出的模型,提供充足的額度與穩定的存取。",
|
||||||
"go.faq.q3": "Go 與 Zen 一樣嗎?",
|
"go.faq.q3": "Go 與 Zen 一樣嗎?",
|
||||||
"go.faq.a3":
|
"go.faq.a3":
|
||||||
"不。Zen 是按量付費,而 Go 首月 $5,之後 $10/月,提供充裕的額度,並可可靠地存取 GLM-5.1、GLM-5、Kimi K2.5、Kimi K2.6、MiMo-V2.5-Pro、MiMo-V2.5、Qwen3.7 Max、Qwen3.6 Plus、Qwen3.5 Plus、MiniMax M2.5、MiniMax M2.7、DeepSeek V4 Pro 和 DeepSeek V4 Flash 等開源模型。",
|
"不。Zen 是按量付費,而 Go 首月 $5,之後 $10/月,提供充裕的額度,並可可靠地存取 GLM-5.1、GLM-5、Kimi K2.5、Kimi K2.6、MiMo-V2.5-Pro、MiMo-V2.5、Qwen3.5 Plus、Qwen3.6 Plus、MiniMax M2.5、MiniMax M2.7、DeepSeek V4 Pro 和 DeepSeek V4 Flash 等開源模型。",
|
||||||
"go.faq.q4": "Go 費用是多少?",
|
"go.faq.q4": "Go 費用是多少?",
|
||||||
"go.faq.a4.p1.beforePricing": "Go 費用為",
|
"go.faq.a4.p1.beforePricing": "Go 費用為",
|
||||||
"go.faq.a4.p1.pricingLink": "首月 $5",
|
"go.faq.a4.p1.pricingLink": "首月 $5",
|
||||||
@@ -333,7 +333,7 @@ export const dict = {
|
|||||||
|
|
||||||
"go.faq.q9": "免費模型與 Go 有什麼區別?",
|
"go.faq.q9": "免費模型與 Go 有什麼區別?",
|
||||||
"go.faq.a9":
|
"go.faq.a9":
|
||||||
"免費模型包括 Big Pickle 以及當時可用的促銷模型,配額為 200 次請求/天。Go 包括 GLM-5.1、GLM-5、Kimi K2.5、Kimi K2.6、MiMo-V2.5-Pro、MiMo-V2.5、Qwen3.7 Max、Qwen3.6 Plus、Qwen3.5 Plus、MiniMax M2.5、MiniMax M2.7、DeepSeek V4 Pro 與 DeepSeek V4 Flash,並在滾動視窗(5 小時、每週和每月)內執行更高的請求配額,大約相當於每 5 小時 $12、每週 $30 和每月 $60(實際請求數因模型和使用情況而異)。",
|
"免費模型包括 Big Pickle 以及當時可用的促銷模型,配額為 200 次請求/天。Go 包括 GLM-5.1、GLM-5、Kimi K2.5、Kimi K2.6、MiMo-V2.5-Pro、MiMo-V2.5、Qwen3.5 Plus、Qwen3.6 Plus、MiniMax M2.5、MiniMax M2.7、DeepSeek V4 Pro 與 DeepSeek V4 Flash,並在滾動視窗(5 小時、每週和每月)內執行更高的請求配額,大約相當於每 5 小時 $12、每週 $30 和每月 $60(實際請求數因模型和使用情況而異)。",
|
||||||
|
|
||||||
"zen.api.error.rateLimitExceeded": "超出頻率限制。請稍後再試。",
|
"zen.api.error.rateLimitExceeded": "超出頻率限制。請稍後再試。",
|
||||||
"zen.api.error.modelNotSupported": "不支援模型 {{model}}",
|
"zen.api.error.modelNotSupported": "不支援模型 {{model}}",
|
||||||
|
|||||||
@@ -1,9 +1,11 @@
|
|||||||
import { redirect } from "@solidjs/router"
|
import { redirect } from "@solidjs/router"
|
||||||
import type { APIEvent } from "@solidjs/start/server"
|
import type { APIEvent } from "@solidjs/start/server"
|
||||||
|
import { Referral } from "@opencode-ai/console-core/referral.js"
|
||||||
import { AuthClient } from "~/context/auth"
|
import { AuthClient } from "~/context/auth"
|
||||||
import { useAuthSession } from "~/context/auth"
|
import { useAuthSession } from "~/context/auth"
|
||||||
import { i18n } from "~/i18n"
|
import { i18n } from "~/i18n"
|
||||||
import { localeFromRequest, route } from "~/lib/language"
|
import { localeFromRequest, route } from "~/lib/language"
|
||||||
|
import { clearReferralCookie, referralCodeFromCookieHeader } from "~/lib/referral-invite"
|
||||||
|
|
||||||
export async function GET(input: APIEvent) {
|
export async function GET(input: APIEvent) {
|
||||||
const url = new URL(input.request.url)
|
const url = new URL(input.request.url)
|
||||||
@@ -17,6 +19,7 @@ export async function GET(input: APIEvent) {
|
|||||||
if (result.err) throw new Error(result.err.message)
|
if (result.err) throw new Error(result.err.message)
|
||||||
const decoded = AuthClient.decode(result.tokens.access, {} as any)
|
const decoded = AuthClient.decode(result.tokens.access, {} as any)
|
||||||
if (decoded.err) throw new Error(decoded.err.message)
|
if (decoded.err) throw new Error(decoded.err.message)
|
||||||
|
const referralCode = referralCodeFromCookieHeader(input.request.headers.get("cookie"))
|
||||||
const session = await useAuthSession()
|
const session = await useAuthSession()
|
||||||
const id = decoded.subject.properties.accountID
|
const id = decoded.subject.properties.accountID
|
||||||
await session.update((value) => {
|
await session.update((value) => {
|
||||||
@@ -32,8 +35,15 @@ export async function GET(input: APIEvent) {
|
|||||||
current: id,
|
current: id,
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
if (decoded.subject.properties.newAccount && referralCode) {
|
||||||
|
await Referral.createFromAccount({ accountID: id, referralCode }).catch((error) => {
|
||||||
|
console.error("Referral create failed", error)
|
||||||
|
})
|
||||||
|
}
|
||||||
const next = url.pathname === "/auth/callback" ? "/auth" : url.pathname.replace("/auth/callback", "")
|
const next = url.pathname === "/auth/callback" ? "/auth" : url.pathname.replace("/auth/callback", "")
|
||||||
return redirect(route(locale, next))
|
const response = redirect(route(locale, next))
|
||||||
|
if (referralCode) response.headers.append("set-cookie", clearReferralCookie())
|
||||||
|
return response
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
return new Response(
|
return new Response(
|
||||||
JSON.stringify({
|
JSON.stringify({
|
||||||
|
|||||||
@@ -29,9 +29,8 @@ const models = [
|
|||||||
{ name: "Kimi K2.6", provider: "Moonshot AI" },
|
{ name: "Kimi K2.6", provider: "Moonshot AI" },
|
||||||
{ name: "MiMo-V2.5-Pro", provider: "Xiaomi MiMo" },
|
{ name: "MiMo-V2.5-Pro", provider: "Xiaomi MiMo" },
|
||||||
{ name: "MiMo-V2.5", provider: "Xiaomi MiMo" },
|
{ name: "MiMo-V2.5", provider: "Xiaomi MiMo" },
|
||||||
{ name: "Qwen3.7 Max", provider: "Alibaba Cloud Model Studio" },
|
|
||||||
{ name: "Qwen3.6 Plus", provider: "Alibaba Cloud Model Studio" },
|
|
||||||
{ name: "Qwen3.5 Plus", provider: "Alibaba Cloud Model Studio" },
|
{ name: "Qwen3.5 Plus", provider: "Alibaba Cloud Model Studio" },
|
||||||
|
{ name: "Qwen3.6 Plus", provider: "Alibaba Cloud Model Studio" },
|
||||||
{ name: "MiniMax M2.7", provider: "MiniMax" },
|
{ name: "MiniMax M2.7", provider: "MiniMax" },
|
||||||
{ name: "MiniMax M2.5", provider: "MiniMax" },
|
{ name: "MiniMax M2.5", provider: "MiniMax" },
|
||||||
{ name: "DeepSeek V4 Pro", provider: "DeepSeek" },
|
{ name: "DeepSeek V4 Pro", provider: "DeepSeek" },
|
||||||
@@ -62,18 +61,17 @@ function LimitsGraph(props: { href: string }) {
|
|||||||
const free = 200
|
const free = 200
|
||||||
const graph = [
|
const graph = [
|
||||||
{ id: "glm-5.1", name: "GLM-5.1", req: 880, d: "100ms" },
|
{ id: "glm-5.1", name: "GLM-5.1", req: 880, d: "100ms" },
|
||||||
{ id: "qwen3.7-max", name: "Qwen3.7 Max", req: 950, d: "280ms" },
|
|
||||||
{ id: "kimi-k2.6", name: "Kimi K2.6", req: 1150, d: "150ms" },
|
{ id: "kimi-k2.6", name: "Kimi K2.6", req: 1150, d: "150ms" },
|
||||||
{ id: "mimo-v2.5-pro", name: "MiMo-V2.5-Pro", req: 3250, d: "150ms" },
|
{ id: "mimo-v2.5-pro", name: "MiMo-V2.5-Pro", req: 1290, d: "150ms" },
|
||||||
{ id: "qwen3.6-plus", name: "Qwen3.6 Plus", req: 3300, d: "280ms" },
|
{ id: "qwen3.6-plus", name: "Qwen3.6 Plus", req: 3300, d: "280ms" },
|
||||||
{ id: "minimax-m2.7", name: "MiniMax M2.7", req: 3400, d: "300ms" },
|
{ id: "minimax-m2.7", name: "MiniMax M2.7", req: 3400, d: "300ms" },
|
||||||
{ id: "deepseek-v4-pro", name: "DeepSeek V4 Pro", req: 3450, d: "200ms" },
|
{ id: "deepseek-v4-pro", name: "DeepSeek V4 Pro", req: 3450, d: "200ms" },
|
||||||
{ id: "qwen3.5-plus", name: "Qwen3.5 Plus", req: 10200, d: "360ms" },
|
{ id: "qwen3.5-plus", name: "Qwen3.5 Plus", req: 10200, d: "360ms" },
|
||||||
{ id: "mimo-v2.5", name: "MiMo-V2.5", req: 30100, d: "340ms" },
|
|
||||||
{ id: "deepseek-v4-flash", name: "DeepSeek V4 Flash", req: 31650, d: "340ms" },
|
{ id: "deepseek-v4-flash", name: "DeepSeek V4 Flash", req: 31650, d: "340ms" },
|
||||||
]
|
]
|
||||||
|
|
||||||
const w = 720
|
const w = 720
|
||||||
|
const h = 330
|
||||||
const left = 40
|
const left = 40
|
||||||
const right = 60
|
const right = 60
|
||||||
const top = 18
|
const top = 18
|
||||||
@@ -109,7 +107,6 @@ function LimitsGraph(props: { href: string }) {
|
|||||||
const bh = 8
|
const bh = 8
|
||||||
const gap = 20
|
const gap = 20
|
||||||
const step = bh + gap
|
const step = bh + gap
|
||||||
const h = 330 + Math.max(0, graph.length - 8) * step
|
|
||||||
const sep = bh + 40
|
const sep = bh + 40
|
||||||
const fy = top + 22
|
const fy = top + 22
|
||||||
const gy = (i: number) => fy + sep + step * i
|
const gy = (i: number) => fy + sep + step * i
|
||||||
|
|||||||
@@ -268,7 +268,6 @@ export function LiteSection(props: { lite: LiteSubscription | undefined }) {
|
|||||||
<li>MiniMax M2.7</li>
|
<li>MiniMax M2.7</li>
|
||||||
<li>Qwen3.5 Plus</li>
|
<li>Qwen3.5 Plus</li>
|
||||||
<li>Qwen3.6 Plus</li>
|
<li>Qwen3.6 Plus</li>
|
||||||
<li>Qwen3.7 Max</li>
|
|
||||||
<li>DeepSeek V4 Pro</li>
|
<li>DeepSeek V4 Pro</li>
|
||||||
<li>DeepSeek V4 Flash</li>
|
<li>DeepSeek V4 Flash</li>
|
||||||
</ul>
|
</ul>
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import { IconCopy, IconCheck } from "~/component/icon"
|
|||||||
import { Key } from "@opencode-ai/console-core/key.js"
|
import { Key } from "@opencode-ai/console-core/key.js"
|
||||||
import { withActor } from "~/context/auth.withActor"
|
import { withActor } from "~/context/auth.withActor"
|
||||||
import { createStore } from "solid-js/store"
|
import { createStore } from "solid-js/store"
|
||||||
|
import { formatDateUTC, formatDateForTable } from "../../common"
|
||||||
import styles from "./key-section.module.css"
|
import styles from "./key-section.module.css"
|
||||||
import { Actor } from "@opencode-ai/console-core/actor.js"
|
import { Actor } from "@opencode-ai/console-core/actor.js"
|
||||||
import { useI18n } from "~/context/i18n"
|
import { useI18n } from "~/context/i18n"
|
||||||
@@ -123,6 +124,7 @@ export function KeySection() {
|
|||||||
<th>{i18n.t("workspace.keys.table.name")}</th>
|
<th>{i18n.t("workspace.keys.table.name")}</th>
|
||||||
<th>{i18n.t("workspace.keys.table.key")}</th>
|
<th>{i18n.t("workspace.keys.table.key")}</th>
|
||||||
<th>{i18n.t("workspace.keys.table.createdBy")}</th>
|
<th>{i18n.t("workspace.keys.table.createdBy")}</th>
|
||||||
|
<th>{i18n.t("workspace.keys.table.lastUsed")}</th>
|
||||||
<th></th>
|
<th></th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
@@ -154,6 +156,9 @@ export function KeySection() {
|
|||||||
</Show>
|
</Show>
|
||||||
</td>
|
</td>
|
||||||
<td data-slot="key-user-email">{key.email}</td>
|
<td data-slot="key-user-email">{key.email}</td>
|
||||||
|
<td data-slot="key-last-used" title={key.timeUsed ? formatDateUTC(key.timeUsed) : undefined}>
|
||||||
|
{key.timeUsed ? formatDateForTable(key.timeUsed) : "-"}
|
||||||
|
</td>
|
||||||
<td data-slot="key-actions">
|
<td data-slot="key-actions">
|
||||||
<form action={removeKey} method="post">
|
<form action={removeKey} method="post">
|
||||||
<input type="hidden" name="id" value={key.id} />
|
<input type="hidden" name="id" value={key.id} />
|
||||||
|
|||||||
@@ -249,9 +249,8 @@ export async function handler(
|
|||||||
if (!isStream || [400, 404, 429].includes(res.status)) {
|
if (!isStream || [400, 404, 429].includes(res.status)) {
|
||||||
const json = await res.json()
|
const json = await res.json()
|
||||||
await rateLimiter?.track()
|
await rateLimiter?.track()
|
||||||
const usage = providerInfo.extractUsage(json)
|
if (json.usage) {
|
||||||
if (usage) {
|
const usageInfo = providerInfo.normalizeUsage(json.usage)
|
||||||
const usageInfo = providerInfo.normalizeUsage(usage)
|
|
||||||
const costInfo = calculateCost(modelInfo, usageInfo)
|
const costInfo = calculateCost(modelInfo, usageInfo)
|
||||||
await trialLimiter?.track(usageInfo)
|
await trialLimiter?.track(usageInfo)
|
||||||
await modelTpmLimiter?.track(providerInfo.id, providerInfo.model, usageInfo)
|
await modelTpmLimiter?.track(providerInfo.id, providerInfo.model, usageInfo)
|
||||||
@@ -483,29 +482,28 @@ export async function handler(
|
|||||||
modelTpsLimits: Record<string, boolean> | undefined,
|
modelTpsLimits: Record<string, boolean> | undefined,
|
||||||
) {
|
) {
|
||||||
const modelProvider = (() => {
|
const modelProvider = (() => {
|
||||||
const allProviders = modelInfo.providers.filter((provider) => !provider.disabled)
|
|
||||||
|
|
||||||
// Byok is top priority b/c if user set their own API key, we should use it
|
// Byok is top priority b/c if user set their own API key, we should use it
|
||||||
// instead of using the sticky provider for the same session
|
// instead of using the sticky provider for the same session
|
||||||
if (authInfo?.provider?.credentials) {
|
if (authInfo?.provider?.credentials) {
|
||||||
return allProviders.find((provider) => provider.id === modelInfo.byokProvider)
|
return modelInfo.providers.find((provider) => provider.id === modelInfo.byokProvider)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Always use the same provider for the same session
|
// Always use the same provider for the same session
|
||||||
if (stickyProvider) {
|
if (stickyProvider) {
|
||||||
const provider = allProviders.find((provider) => provider.id === stickyProvider)
|
const provider = modelInfo.providers.find((provider) => provider.id === stickyProvider)
|
||||||
if (provider) return provider
|
if (provider) return provider
|
||||||
}
|
}
|
||||||
|
|
||||||
if (trialProviders) {
|
if (trialProviders) {
|
||||||
const trialProvider = trialProviders[Math.floor(Math.random() * trialProviders.length)]
|
const trialProvider = trialProviders[Math.floor(Math.random() * trialProviders.length)]
|
||||||
const provider = allProviders.find((provider) => provider.id === trialProvider)
|
const provider = modelInfo.providers.find((provider) => provider.id === trialProvider)
|
||||||
if (provider) return provider
|
if (provider) return provider
|
||||||
}
|
}
|
||||||
|
|
||||||
if (retry.retryCount !== MAX_FAILOVER_RETRIES) {
|
if (retry.retryCount !== MAX_FAILOVER_RETRIES) {
|
||||||
let topPriority = Infinity
|
let topPriority = Infinity
|
||||||
const providers = allProviders
|
const providers = modelInfo.providers
|
||||||
|
.filter((provider) => !provider.disabled)
|
||||||
.filter((provider) => provider.weight !== 0)
|
.filter((provider) => provider.weight !== 0)
|
||||||
.filter((provider) => !retry.excludeProviders.includes(provider.id))
|
.filter((provider) => !retry.excludeProviders.includes(provider.id))
|
||||||
.filter((provider) => {
|
.filter((provider) => {
|
||||||
@@ -537,7 +535,7 @@ export async function handler(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// fallback provider
|
// fallback provider
|
||||||
return allProviders.find((provider) => provider.id === modelInfo.fallbackProvider)
|
return modelInfo.providers.find((provider) => provider.id === modelInfo.fallbackProvider)
|
||||||
})()
|
})()
|
||||||
|
|
||||||
if (!modelProvider) throw new ModelError(t("zen.api.error.noProviderAvailable"))
|
if (!modelProvider) throw new ModelError(t("zen.api.error.noProviderAvailable"))
|
||||||
@@ -989,6 +987,10 @@ export async function handler(
|
|||||||
return undefined
|
return undefined
|
||||||
})(),
|
})(),
|
||||||
}),
|
}),
|
||||||
|
db
|
||||||
|
.update(KeyTable)
|
||||||
|
.set({ timeUsed: sql`now()` })
|
||||||
|
.where(and(eq(KeyTable.workspaceID, authInfo.workspaceID), eq(KeyTable.id, authInfo.apiKeyId))),
|
||||||
...(() => {
|
...(() => {
|
||||||
if (billingSource === "subscription") {
|
if (billingSource === "subscription") {
|
||||||
const plan = authInfo.billing.subscription!.plan
|
const plan = authInfo.billing.subscription!.plan
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
|
import { Database, eq, and, sql, inArray } from "@opencode-ai/console-core/drizzle/index.js"
|
||||||
|
import { IpRateLimitTable } from "@opencode-ai/console-core/schema/ip.sql.js"
|
||||||
import { FreeUsageLimitError } from "./error"
|
import { FreeUsageLimitError } from "./error"
|
||||||
import { logger } from "./logger"
|
import { logger } from "./logger"
|
||||||
import { buildRateLimitKey, getRedis } from "./redis"
|
|
||||||
import { i18n } from "~/i18n"
|
import { i18n } from "~/i18n"
|
||||||
import { localeFromRequest } from "~/lib/language"
|
import { localeFromRequest } from "~/lib/language"
|
||||||
import { Subscription } from "@opencode-ai/console-core/subscription.js"
|
import { Subscription } from "@opencode-ai/console-core/subscription.js"
|
||||||
@@ -20,31 +21,45 @@ export function createRateLimiter(modelId: string, rateLimit: number | undefined
|
|||||||
|
|
||||||
const ip = !rawIp.length ? "unknown" : rawIp
|
const ip = !rawIp.length ? "unknown" : rawIp
|
||||||
const now = Date.now()
|
const now = Date.now()
|
||||||
|
const lifetimeInterval = ""
|
||||||
const dailyInterval = rateLimit ? `${buildYYYYMMDD(now)}${modelId.substring(0, 2)}` : buildYYYYMMDD(now)
|
const dailyInterval = rateLimit ? `${buildYYYYMMDD(now)}${modelId.substring(0, 2)}` : buildYYYYMMDD(now)
|
||||||
const retryAfter = getRetryAfterDay(now)
|
|
||||||
const redis = getRedis()
|
let _isNew: boolean
|
||||||
const lifetimeKey = buildRateLimitKey("ip", ip)
|
|
||||||
const dailyKey = buildRateLimitKey("ip", ip, dailyInterval)
|
|
||||||
let isNew = false
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
check: async () => {
|
check: async () => {
|
||||||
const counts = await redis.mget<(string | number | null)[]>(isDefaultModel ? [lifetimeKey, dailyKey] : [dailyKey])
|
const rows = await Database.use((tx) =>
|
||||||
const lifetimeCount = isDefaultModel ? Number(counts[0] ?? 0) : 0
|
tx
|
||||||
const dailyCount = Number(counts[isDefaultModel ? 1 : 0] ?? 0)
|
.select({ interval: IpRateLimitTable.interval, count: IpRateLimitTable.count })
|
||||||
|
.from(IpRateLimitTable)
|
||||||
|
.where(
|
||||||
|
and(
|
||||||
|
eq(IpRateLimitTable.ip, ip),
|
||||||
|
isDefaultModel
|
||||||
|
? inArray(IpRateLimitTable.interval, [lifetimeInterval, dailyInterval])
|
||||||
|
: inArray(IpRateLimitTable.interval, [dailyInterval]),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
const lifetimeCount = rows.find((r) => r.interval === lifetimeInterval)?.count ?? 0
|
||||||
|
const dailyCount = rows.find((r) => r.interval === dailyInterval)?.count ?? 0
|
||||||
logger.debug(`rate limit lifetime: ${lifetimeCount}, daily: ${dailyCount}`)
|
logger.debug(`rate limit lifetime: ${lifetimeCount}, daily: ${dailyCount}`)
|
||||||
|
|
||||||
isNew = isDefaultModel && lifetimeCount < dailyLimit * 7
|
_isNew = isDefaultModel && lifetimeCount < dailyLimit * 7
|
||||||
|
|
||||||
if ((isNew && dailyCount >= dailyLimit * 2) || (!isNew && dailyCount >= dailyLimit))
|
if ((_isNew && dailyCount >= dailyLimit * 2) || (!_isNew && dailyCount >= dailyLimit))
|
||||||
throw new FreeUsageLimitError(dict["zen.api.error.rateLimitExceeded"], retryAfter)
|
throw new FreeUsageLimitError(dict["zen.api.error.rateLimitExceeded"], getRetryAfterDay(now))
|
||||||
},
|
},
|
||||||
track: async () => {
|
track: async () => {
|
||||||
const pipeline = redis.pipeline()
|
await Database.use((tx) =>
|
||||||
pipeline.incr(dailyKey)
|
tx
|
||||||
pipeline.expire(dailyKey, retryAfter)
|
.insert(IpRateLimitTable)
|
||||||
if (isNew) pipeline.incr(lifetimeKey)
|
.values([
|
||||||
await pipeline.exec()
|
{ ip, interval: dailyInterval, count: 1 },
|
||||||
|
...(_isNew ? [{ ip, interval: lifetimeInterval, count: 1 }] : []),
|
||||||
|
])
|
||||||
|
.onDuplicateKeyUpdate({ set: { count: sql`${IpRateLimitTable.count} + 1` } }),
|
||||||
|
)
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
|
import { Database, eq, and, sql } from "@opencode-ai/console-core/drizzle/index.js"
|
||||||
|
import { KeyRateLimitTable } from "@opencode-ai/console-core/schema/ip.sql.js"
|
||||||
import { RateLimitError } from "./error"
|
import { RateLimitError } from "./error"
|
||||||
import { buildRateLimitKey, getRedis } from "./redis"
|
|
||||||
import { i18n } from "~/i18n"
|
import { i18n } from "~/i18n"
|
||||||
import { localeFromRequest } from "~/lib/language"
|
import { localeFromRequest } from "~/lib/language"
|
||||||
|
|
||||||
@@ -18,20 +19,26 @@ export function createRateLimiter(
|
|||||||
.replace(/[^0-9]/g, "")
|
.replace(/[^0-9]/g, "")
|
||||||
.substring(0, 12)
|
.substring(0, 12)
|
||||||
const interval = `${modelId.substring(0, 27)}-${yyyyMMddHHmm}`
|
const interval = `${modelId.substring(0, 27)}-${yyyyMMddHHmm}`
|
||||||
const redis = getRedis()
|
|
||||||
const key = buildRateLimitKey("key", zenApiKey, interval)
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
check: async () => {
|
check: async () => {
|
||||||
const count = Number((await redis.mget<(string | number | null)[]>([key]))[0] ?? 0)
|
const rows = await Database.use((tx) =>
|
||||||
|
tx
|
||||||
|
.select({ interval: KeyRateLimitTable.interval, count: KeyRateLimitTable.count })
|
||||||
|
.from(KeyRateLimitTable)
|
||||||
|
.where(and(eq(KeyRateLimitTable.key, zenApiKey), eq(KeyRateLimitTable.interval, interval))),
|
||||||
|
).then((rows) => rows[0])
|
||||||
|
const count = rows?.count ?? 0
|
||||||
|
|
||||||
if (count >= LIMIT) throw new RateLimitError(dict["zen.api.error.rateLimitExceeded"], 60)
|
if (count >= LIMIT) throw new RateLimitError(dict["zen.api.error.rateLimitExceeded"], 60)
|
||||||
},
|
},
|
||||||
track: async () => {
|
track: async () => {
|
||||||
const pipeline = redis.pipeline()
|
await Database.use((tx) =>
|
||||||
pipeline.incr(key)
|
tx
|
||||||
pipeline.expire(key, 60)
|
.insert(KeyRateLimitTable)
|
||||||
await pipeline.exec()
|
.values({ key: zenApiKey, interval, count: 1 })
|
||||||
|
.onDuplicateKeyUpdate({ set: { count: sql`${KeyRateLimitTable.count} + 1` } }),
|
||||||
|
)
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -175,7 +175,6 @@ export const anthropicHelper: ProviderHelper = ({ reqModel, providerModel }) =>
|
|||||||
retrieve: () => usage,
|
retrieve: () => usage,
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
extractUsage: (response: any) => response.usage,
|
|
||||||
normalizeUsage: (usage: Usage) => ({
|
normalizeUsage: (usage: Usage) => ({
|
||||||
inputTokens: usage.input_tokens ?? 0,
|
inputTokens: usage.input_tokens ?? 0,
|
||||||
outputTokens: usage.output_tokens ?? 0,
|
outputTokens: usage.output_tokens ?? 0,
|
||||||
|
|||||||
@@ -58,7 +58,6 @@ export const googleHelper: ProviderHelper = ({ providerModel }) => ({
|
|||||||
retrieve: () => usage,
|
retrieve: () => usage,
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
extractUsage: (response: any) => response.usageMetadata,
|
|
||||||
normalizeUsage: (usage: Usage) => {
|
normalizeUsage: (usage: Usage) => {
|
||||||
const inputTokens = usage.promptTokenCount ?? 0
|
const inputTokens = usage.promptTokenCount ?? 0
|
||||||
const outputTokens = usage.candidatesTokenCount ?? 0
|
const outputTokens = usage.candidatesTokenCount ?? 0
|
||||||
|
|||||||
@@ -58,7 +58,6 @@ export const oaCompatHelper: ProviderHelper = ({ adjustCacheUsage }) => ({
|
|||||||
retrieve: () => usage,
|
retrieve: () => usage,
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
extractUsage: (response: any) => response.usage,
|
|
||||||
normalizeUsage: (usage: Usage) => {
|
normalizeUsage: (usage: Usage) => {
|
||||||
let inputTokens = usage.prompt_tokens ?? 0
|
let inputTokens = usage.prompt_tokens ?? 0
|
||||||
const outputTokens = usage.completion_tokens ?? 0
|
const outputTokens = usage.completion_tokens ?? 0
|
||||||
|
|||||||
@@ -43,7 +43,6 @@ export const openaiHelper: ProviderHelper = ({ workspaceID }) => ({
|
|||||||
retrieve: () => usage,
|
retrieve: () => usage,
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
extractUsage: (response: any) => response.usage ?? response.response?.usage,
|
|
||||||
normalizeUsage: (usage: Usage) => {
|
normalizeUsage: (usage: Usage) => {
|
||||||
const inputTokens = usage.input_tokens ?? 0
|
const inputTokens = usage.input_tokens ?? 0
|
||||||
const outputTokens = usage.output_tokens ?? 0
|
const outputTokens = usage.output_tokens ?? 0
|
||||||
|
|||||||
@@ -49,7 +49,6 @@ export type ProviderHelper = (input: {
|
|||||||
parse: (chunk: string) => void
|
parse: (chunk: string) => void
|
||||||
retrieve: () => any
|
retrieve: () => any
|
||||||
}
|
}
|
||||||
extractUsage: (response: any) => any
|
|
||||||
normalizeUsage: (usage: any) => UsageInfo
|
normalizeUsage: (usage: any) => UsageInfo
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,18 +0,0 @@
|
|||||||
import { Resource } from "@opencode-ai/console-resource"
|
|
||||||
import { Redis } from "@upstash/redis/cloudflare"
|
|
||||||
|
|
||||||
let redis: Redis | undefined
|
|
||||||
|
|
||||||
export function getRedis() {
|
|
||||||
if (redis) return redis
|
|
||||||
redis = new Redis({
|
|
||||||
url: Resource.UpstashRedisRestUrl.value,
|
|
||||||
token: Resource.UpstashRedisRestToken.value,
|
|
||||||
enableTelemetry: false,
|
|
||||||
})
|
|
||||||
return redis
|
|
||||||
}
|
|
||||||
|
|
||||||
export function buildRateLimitKey(kind: string, identifier: string, interval?: string) {
|
|
||||||
return `${Resource.App.stage}:ratelimit:${kind}:${identifier}${interval ? `:${interval}` : ""}`
|
|
||||||
}
|
|
||||||
@@ -1,68 +0,0 @@
|
|||||||
import { describe, expect, test } from "bun:test"
|
|
||||||
import type { ZenData } from "@opencode-ai/console-core/model.js"
|
|
||||||
import type { ProviderHelper } from "../src/routes/zen/util/provider/provider"
|
|
||||||
import { anthropicHelper } from "../src/routes/zen/util/provider/anthropic"
|
|
||||||
import { googleHelper } from "../src/routes/zen/util/provider/google"
|
|
||||||
import { oaCompatHelper } from "../src/routes/zen/util/provider/openai-compatible"
|
|
||||||
import { openaiHelper } from "../src/routes/zen/util/provider/openai"
|
|
||||||
|
|
||||||
const providers = {
|
|
||||||
anthropic: anthropicHelper({ reqModel: "claude-haiku-4-5", providerModel: "claude-haiku-4-5" }),
|
|
||||||
google: googleHelper({ reqModel: "gemini-3-flash", providerModel: "gemini-3-flash" }),
|
|
||||||
openai: openaiHelper({ reqModel: "gpt-5", providerModel: "gpt-5" }),
|
|
||||||
"oa-compat": oaCompatHelper({ reqModel: "gpt-5-nano", providerModel: "gpt-5-nano" }),
|
|
||||||
} satisfies Record<ZenData.Format, ReturnType<ProviderHelper>>
|
|
||||||
|
|
||||||
describe("provider usage extraction", () => {
|
|
||||||
test("extracts Google non-stream usage metadata", () => {
|
|
||||||
const usage = providers.google.extractUsage({
|
|
||||||
usageMetadata: {
|
|
||||||
promptTokenCount: 10,
|
|
||||||
candidatesTokenCount: 3,
|
|
||||||
thoughtsTokenCount: 2,
|
|
||||||
cachedContentTokenCount: 4,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
expect(providers.google.normalizeUsage(usage)).toEqual({
|
|
||||||
inputTokens: 6,
|
|
||||||
outputTokens: 3,
|
|
||||||
reasoningTokens: 2,
|
|
||||||
cacheReadTokens: 4,
|
|
||||||
cacheWrite5mTokens: undefined,
|
|
||||||
cacheWrite1hTokens: undefined,
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
test("parses Google stream usage metadata", () => {
|
|
||||||
const usageParser = providers.google.createUsageParser()
|
|
||||||
usageParser.parse(
|
|
||||||
'data: {"usageMetadata":{"promptTokenCount":10,"candidatesTokenCount":3,"thoughtsTokenCount":2,"cachedContentTokenCount":4}}',
|
|
||||||
)
|
|
||||||
|
|
||||||
expect(providers.google.normalizeUsage(usageParser.retrieve())).toEqual({
|
|
||||||
inputTokens: 6,
|
|
||||||
outputTokens: 3,
|
|
||||||
reasoningTokens: 2,
|
|
||||||
cacheReadTokens: 4,
|
|
||||||
cacheWrite5mTokens: undefined,
|
|
||||||
cacheWrite1hTokens: undefined,
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
test("extracts nested OpenAI Responses usage", () => {
|
|
||||||
expect(
|
|
||||||
providers.openai.extractUsage({
|
|
||||||
response: {
|
|
||||||
usage: {
|
|
||||||
input_tokens: 5,
|
|
||||||
output_tokens: 7,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
).toEqual({
|
|
||||||
input_tokens: 5,
|
|
||||||
output_tokens: 7,
|
|
||||||
})
|
|
||||||
})
|
|
||||||
})
|
|
||||||
@@ -9,7 +9,7 @@ export default defineConfig({
|
|||||||
}) as PluginOption,
|
}) as PluginOption,
|
||||||
nitro({
|
nitro({
|
||||||
compatibilityDate: "2024-09-19",
|
compatibilityDate: "2024-09-19",
|
||||||
preset: "cloudflare-module",
|
preset: "cloudflare_module",
|
||||||
cloudflare: {
|
cloudflare: {
|
||||||
nodeCompat: true,
|
nodeCompat: true,
|
||||||
},
|
},
|
||||||
@@ -17,7 +17,6 @@ export default defineConfig({
|
|||||||
],
|
],
|
||||||
server: {
|
server: {
|
||||||
allowedHosts: true,
|
allowedHosts: true,
|
||||||
port: 3001,
|
|
||||||
},
|
},
|
||||||
build: {
|
build: {
|
||||||
rollupOptions: {
|
rollupOptions: {
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"$schema": "https://json.schemastore.org/package.json",
|
"$schema": "https://json.schemastore.org/package.json",
|
||||||
"name": "@opencode-ai/console-core",
|
"name": "@opencode-ai/console-core",
|
||||||
"version": "1.15.11",
|
"version": "1.15.10",
|
||||||
"private": true,
|
"private": true,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
|||||||
@@ -1,146 +0,0 @@
|
|||||||
import { Resource } from "@opencode-ai/console-resource"
|
|
||||||
import { and, Database, eq, isNull } from "../src/drizzle/index.js"
|
|
||||||
import { Identifier } from "../src/identifier.js"
|
|
||||||
import { AccountTable } from "../src/schema/account.sql.js"
|
|
||||||
import { AuthTable } from "../src/schema/auth.sql.js"
|
|
||||||
import { BillingTable } from "../src/schema/billing.sql.js"
|
|
||||||
import { KeyTable } from "../src/schema/key.sql.js"
|
|
||||||
import { UserTable } from "../src/schema/user.sql.js"
|
|
||||||
import { WorkspaceTable } from "../src/schema/workspace.sql.js"
|
|
||||||
import { centsToMicroCents } from "../src/util/price.js"
|
|
||||||
|
|
||||||
const args = parseArgs(process.argv.slice(2))
|
|
||||||
if (!args.email) {
|
|
||||||
console.error(
|
|
||||||
"Usage: bun script/create-api-key.ts --email <email> [--workspace-id <wrk_...>] [--workspace-name <name>] [--key-name <name>] [--balance-dollars <amount>] [--allow-production]",
|
|
||||||
)
|
|
||||||
process.exit(1)
|
|
||||||
}
|
|
||||||
if (Resource.App.stage === "production" && !args.allowProduction) {
|
|
||||||
throw new Error("Refusing to create a production API key without --allow-production")
|
|
||||||
}
|
|
||||||
|
|
||||||
const result = await Database.transaction(async (tx) => {
|
|
||||||
const auth = await tx
|
|
||||||
.select()
|
|
||||||
.from(AuthTable)
|
|
||||||
.where(and(eq(AuthTable.provider, "email"), eq(AuthTable.subject, args.email)))
|
|
||||||
.then((rows) => rows[0])
|
|
||||||
const accountID = auth?.accountID ?? Identifier.create("account")
|
|
||||||
if (!auth) {
|
|
||||||
await tx.insert(AccountTable).values({ id: accountID })
|
|
||||||
await tx.insert(AuthTable).values({
|
|
||||||
id: Identifier.create("auth"),
|
|
||||||
provider: "email",
|
|
||||||
subject: args.email,
|
|
||||||
accountID,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
const workspace = args.workspaceID
|
|
||||||
? await tx
|
|
||||||
.select()
|
|
||||||
.from(WorkspaceTable)
|
|
||||||
.where(eq(WorkspaceTable.id, args.workspaceID))
|
|
||||||
.then((rows) => rows[0])
|
|
||||||
: await tx
|
|
||||||
.select({ workspace: WorkspaceTable })
|
|
||||||
.from(UserTable)
|
|
||||||
.innerJoin(WorkspaceTable, eq(WorkspaceTable.id, UserTable.workspaceID))
|
|
||||||
.where(and(eq(UserTable.accountID, accountID), isNull(UserTable.timeDeleted)))
|
|
||||||
.then((rows) => rows[0]?.workspace)
|
|
||||||
if (args.workspaceID && !workspace) throw new Error(`Workspace not found: ${args.workspaceID}`)
|
|
||||||
const workspaceID = workspace?.id ?? Identifier.create("workspace")
|
|
||||||
if (!workspace) {
|
|
||||||
await tx.insert(WorkspaceTable).values({
|
|
||||||
id: workspaceID,
|
|
||||||
slug: null,
|
|
||||||
name: args.workspaceName ?? `${args.email} manual`,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
const user = await tx
|
|
||||||
.select()
|
|
||||||
.from(UserTable)
|
|
||||||
.where(
|
|
||||||
and(eq(UserTable.workspaceID, workspaceID), eq(UserTable.accountID, accountID), isNull(UserTable.timeDeleted)),
|
|
||||||
)
|
|
||||||
.then((rows) => rows[0])
|
|
||||||
const userID = user?.id ?? Identifier.create("user")
|
|
||||||
if (!user) {
|
|
||||||
await tx.insert(UserTable).values({
|
|
||||||
id: userID,
|
|
||||||
workspaceID,
|
|
||||||
accountID,
|
|
||||||
email: args.email,
|
|
||||||
name: args.email,
|
|
||||||
role: "admin",
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
const balance = centsToMicroCents(args.balanceDollars * 100)
|
|
||||||
const billing = await tx
|
|
||||||
.select()
|
|
||||||
.from(BillingTable)
|
|
||||||
.where(eq(BillingTable.workspaceID, workspaceID))
|
|
||||||
.then((rows) => rows[0])
|
|
||||||
if (!billing) {
|
|
||||||
await tx.insert(BillingTable).values({
|
|
||||||
id: Identifier.create("billing"),
|
|
||||||
workspaceID,
|
|
||||||
balance,
|
|
||||||
})
|
|
||||||
} else if (billing.balance < balance) {
|
|
||||||
await tx.update(BillingTable).set({ balance }).where(eq(BillingTable.workspaceID, workspaceID))
|
|
||||||
}
|
|
||||||
|
|
||||||
const secretKey = createSecretKey()
|
|
||||||
const keyID = Identifier.create("key")
|
|
||||||
await tx.insert(KeyTable).values({
|
|
||||||
id: keyID,
|
|
||||||
workspaceID,
|
|
||||||
userID,
|
|
||||||
name: args.keyName ?? "Manual API Key",
|
|
||||||
key: secretKey,
|
|
||||||
timeUsed: null,
|
|
||||||
})
|
|
||||||
|
|
||||||
return { accountID, workspaceID, userID, keyID, secretKey }
|
|
||||||
})
|
|
||||||
|
|
||||||
console.log(JSON.stringify({ stage: Resource.App.stage, ...result }, null, 2))
|
|
||||||
|
|
||||||
function createSecretKey() {
|
|
||||||
const chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"
|
|
||||||
const values = new Uint32Array(64)
|
|
||||||
crypto.getRandomValues(values)
|
|
||||||
return `sk-${Array.from(values, (value) => chars[value % chars.length]).join("")}`
|
|
||||||
}
|
|
||||||
|
|
||||||
function parseArgs(argv: string[]) {
|
|
||||||
const parsed = {
|
|
||||||
email: "",
|
|
||||||
workspaceID: "",
|
|
||||||
workspaceName: "",
|
|
||||||
keyName: "",
|
|
||||||
balanceDollars: 100,
|
|
||||||
allowProduction: false,
|
|
||||||
}
|
|
||||||
for (let index = 0; index < argv.length; index++) {
|
|
||||||
const arg = argv[index]
|
|
||||||
if (arg === "--email") parsed.email = requiredValue(argv, ++index, arg)
|
|
||||||
if (arg === "--workspace-id") parsed.workspaceID = requiredValue(argv, ++index, arg)
|
|
||||||
if (arg === "--workspace-name") parsed.workspaceName = requiredValue(argv, ++index, arg)
|
|
||||||
if (arg === "--key-name") parsed.keyName = requiredValue(argv, ++index, arg)
|
|
||||||
if (arg === "--balance-dollars") parsed.balanceDollars = Number(requiredValue(argv, ++index, arg))
|
|
||||||
if (arg === "--allow-production") parsed.allowProduction = true
|
|
||||||
}
|
|
||||||
if (!Number.isFinite(parsed.balanceDollars) || parsed.balanceDollars < 0) throw new Error("Invalid --balance-dollars")
|
|
||||||
return parsed
|
|
||||||
}
|
|
||||||
|
|
||||||
function requiredValue(argv: string[], index: number, arg: string) {
|
|
||||||
const value = argv[index]
|
|
||||||
if (!value || value.startsWith("--")) throw new Error(`Missing value for ${arg}`)
|
|
||||||
return value
|
|
||||||
}
|
|
||||||
@@ -1,8 +1,8 @@
|
|||||||
import { z } from "zod"
|
import { z } from "zod"
|
||||||
import { and, asc, eq, inArray, isNull, sql, Database } from "./drizzle"
|
import { and, asc, eq, isNull, sql, Database } from "./drizzle"
|
||||||
import { Actor } from "./actor"
|
import { Actor } from "./actor"
|
||||||
import { Identifier } from "./identifier"
|
import { Identifier } from "./identifier"
|
||||||
import { LiteTable, PaymentTable } from "./schema/billing.sql"
|
import { LiteTable } from "./schema/billing.sql"
|
||||||
import { ReferralCodeTable, ReferralRewardTable, ReferralTable } from "./schema/referral.sql"
|
import { ReferralCodeTable, ReferralRewardTable, ReferralTable } from "./schema/referral.sql"
|
||||||
import { AuthTable } from "./schema/auth.sql"
|
import { AuthTable } from "./schema/auth.sql"
|
||||||
import { UserTable } from "./schema/user.sql"
|
import { UserTable } from "./schema/user.sql"
|
||||||
@@ -318,26 +318,6 @@ export namespace Referral {
|
|||||||
.then((rows) => rows[0])
|
.then((rows) => rows[0])
|
||||||
if (selfReferral) throw new Error("Self-referral is not allowed")
|
if (selfReferral) throw new Error("Self-referral is not allowed")
|
||||||
|
|
||||||
const workspaceIDs = await tx
|
|
||||||
.select({ workspaceID: UserTable.workspaceID })
|
|
||||||
.from(UserTable)
|
|
||||||
.where(and(eq(UserTable.accountID, input.accountID), isNull(UserTable.timeDeleted)))
|
|
||||||
.then((rows) => rows.map((row) => row.workspaceID))
|
|
||||||
if (workspaceIDs.length === 0) return
|
|
||||||
|
|
||||||
const litePayment = await tx
|
|
||||||
.select({ id: PaymentTable.id })
|
|
||||||
.from(PaymentTable)
|
|
||||||
.where(
|
|
||||||
and(
|
|
||||||
inArray(PaymentTable.workspaceID, workspaceIDs),
|
|
||||||
isNull(PaymentTable.timeDeleted),
|
|
||||||
sql`JSON_UNQUOTE(JSON_EXTRACT(${PaymentTable.enrichment}, '$.type')) = 'lite'`,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
.then((rows) => rows[0])
|
|
||||||
if (litePayment) return
|
|
||||||
|
|
||||||
const referralID = Identifier.create("referral")
|
const referralID = Identifier.create("referral")
|
||||||
await tx.insert(ReferralTable).ignore().values({
|
await tx.insert(ReferralTable).ignore().values({
|
||||||
workspaceID: code.workspaceID,
|
workspaceID: code.workspaceID,
|
||||||
@@ -375,7 +355,7 @@ export namespace Referral {
|
|||||||
.from(ReferralTable)
|
.from(ReferralTable)
|
||||||
.where(and(eq(ReferralTable.inviteeAccountID, invitee.accountID), isNull(ReferralTable.timeDeleted)))
|
.where(and(eq(ReferralTable.inviteeAccountID, invitee.accountID), isNull(ReferralTable.timeDeleted)))
|
||||||
.then((rows) => rows[0])
|
.then((rows) => rows[0])
|
||||||
if (!referral) return
|
if (!referral) throw new Error("Referral not found")
|
||||||
|
|
||||||
const result = await tx
|
const result = await tx
|
||||||
.insert(ReferralRewardTable)
|
.insert(ReferralRewardTable)
|
||||||
@@ -393,7 +373,7 @@ export namespace Referral {
|
|||||||
},
|
},
|
||||||
])
|
])
|
||||||
|
|
||||||
if (result.rowsAffected === 0) return
|
if (result.rowsAffected === 0) throw new Error("Referral already completed")
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@opencode-ai/console-function",
|
"name": "@opencode-ai/console-function",
|
||||||
"version": "1.15.11",
|
"version": "1.15.10",
|
||||||
"$schema": "https://json.schemastore.org/package.json",
|
"$schema": "https://json.schemastore.org/package.json",
|
||||||
"private": true,
|
"private": true,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ export default {
|
|||||||
)
|
)
|
||||||
continue
|
continue
|
||||||
|
|
||||||
let data: Record<string, unknown> = {
|
let data = {
|
||||||
"cf.continent": event.event.request.cf?.continent,
|
"cf.continent": event.event.request.cf?.continent,
|
||||||
"cf.country": event.event.request.cf?.country,
|
"cf.country": event.event.request.cf?.country,
|
||||||
"cf.city": event.event.request.cf?.city,
|
"cf.city": event.event.request.cf?.city,
|
||||||
@@ -35,152 +35,30 @@ export default {
|
|||||||
ip: event.event.request.headers["x-real-ip"],
|
ip: event.event.request.headers["x-real-ip"],
|
||||||
}
|
}
|
||||||
const time = new Date(event.eventTimestamp ?? Date.now()).toISOString()
|
const time = new Date(event.eventTimestamp ?? Date.now()).toISOString()
|
||||||
const events = [
|
const events = []
|
||||||
...event.logs.flatMap((log) =>
|
for (const log of event.logs) {
|
||||||
log.message.flatMap((message: string) => {
|
for (const message of log.message) {
|
||||||
if (!message.startsWith("_metric:")) return []
|
if (!message.startsWith("_metric:")) continue
|
||||||
const json = JSON.parse(message.slice(8)) as Record<string, unknown>
|
const json = JSON.parse(message.slice(8))
|
||||||
data = { ...data, ...json }
|
data = { ...data, ...json }
|
||||||
if ("llm.error.code" in json) {
|
if ("llm.error.code" in json) {
|
||||||
return [{ time, data: { ...data, event_type: "llm.error" } }]
|
events.push({ time, data: { ...data, event_type: "llm.error" } })
|
||||||
}
|
}
|
||||||
return []
|
}
|
||||||
}),
|
}
|
||||||
),
|
events.push({ time, data: { ...data, event_type: "completions" } })
|
||||||
{ time, data: { ...data, event_type: "completions" } },
|
|
||||||
]
|
|
||||||
console.log(JSON.stringify(data, null, 2))
|
console.log(JSON.stringify(data, null, 2))
|
||||||
|
|
||||||
const lakeIngest = getLakeIngest()
|
const ret = await fetch("https://api.honeycomb.io/1/batch/zen", {
|
||||||
const [honeycomb, lake] = await Promise.all([
|
|
||||||
fetch("https://api.honeycomb.io/1/batch/zen", {
|
|
||||||
method: "POST",
|
method: "POST",
|
||||||
headers: {
|
headers: {
|
||||||
"Content-Type": "application/json",
|
"Content-Type": "application/json",
|
||||||
"X-Honeycomb-Team": Resource.HONEYCOMB_API_KEY.value,
|
"X-Honeycomb-Team": Resource.HONEYCOMB_API_KEY.value,
|
||||||
},
|
},
|
||||||
body: JSON.stringify(events),
|
body: JSON.stringify(events),
|
||||||
}),
|
})
|
||||||
...(lakeIngest
|
console.log(ret.status)
|
||||||
? [
|
console.log(await ret.text())
|
||||||
fetch(lakeIngest.url, {
|
|
||||||
method: "POST",
|
|
||||||
headers: {
|
|
||||||
"Content-Type": "application/json",
|
|
||||||
Authorization: `Bearer ${lakeIngest.secret}`,
|
|
||||||
},
|
|
||||||
body: JSON.stringify({ events: events.map((event) => toLakeEvent(event.time, event.data)) }),
|
|
||||||
}),
|
|
||||||
]
|
|
||||||
: []),
|
|
||||||
])
|
|
||||||
console.log(honeycomb.status)
|
|
||||||
console.log(await honeycomb.text())
|
|
||||||
if (lake) {
|
|
||||||
console.log(lake.status)
|
|
||||||
console.log(await lake.text())
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
function getLakeIngest(): { url: string; secret: string } | undefined {
|
|
||||||
try {
|
|
||||||
return Resource.LakeIngest
|
|
||||||
} catch {
|
|
||||||
return undefined
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function toLakeEvent(time: string, data: Record<string, unknown>) {
|
|
||||||
return {
|
|
||||||
_datalake_key: "inference.event",
|
|
||||||
event_timestamp: time,
|
|
||||||
event_date: time.slice(0, 10),
|
|
||||||
event_type: string(data, "event_type"),
|
|
||||||
dataset: "zen",
|
|
||||||
cf_continent: string(data, "cf.continent"),
|
|
||||||
cf_country: string(data, "cf.country"),
|
|
||||||
cf_city: string(data, "cf.city"),
|
|
||||||
cf_region: string(data, "cf.region"),
|
|
||||||
cf_latitude: number(data, "cf.latitude"),
|
|
||||||
cf_longitude: number(data, "cf.longitude"),
|
|
||||||
cf_timezone: string(data, "cf.timezone"),
|
|
||||||
duration: number(data, "duration"),
|
|
||||||
request_length: integer(data, "request_length"),
|
|
||||||
status: integer(data, "status"),
|
|
||||||
ip: string(data, "ip"),
|
|
||||||
is_stream: boolean(data, "is_stream"),
|
|
||||||
session: string(data, "session"),
|
|
||||||
request: string(data, "request"),
|
|
||||||
client: string(data, "client"),
|
|
||||||
user_agent: string(data, "user_agent"),
|
|
||||||
model_variant: string(data, "model.variant"),
|
|
||||||
source: string(data, "source"),
|
|
||||||
provider: string(data, "provider"),
|
|
||||||
provider_model: string(data, "provider.model"),
|
|
||||||
model: string(data, "model"),
|
|
||||||
llm_error_code: integer(data, "llm.error.code"),
|
|
||||||
llm_error_message: string(data, "llm.error.message"),
|
|
||||||
error_response: string(data, "error.response"),
|
|
||||||
error_type: string(data, "error.type"),
|
|
||||||
error_message: string(data, "error.message"),
|
|
||||||
error_cause: string(data, "error.cause"),
|
|
||||||
error_cause2: string(data, "error.cause2"),
|
|
||||||
api_key: string(data, "api_key"),
|
|
||||||
workspace: string(data, "workspace"),
|
|
||||||
is_subscription: boolean(data, "isSubscription"),
|
|
||||||
subscription: string(data, "subscription"),
|
|
||||||
response_length: integer(data, "response_length"),
|
|
||||||
time_to_first_byte: integer(data, "time_to_first_byte"),
|
|
||||||
timestamp_first_byte: integer(data, "timestamp.first_byte"),
|
|
||||||
timestamp_last_byte: integer(data, "timestamp.last_byte"),
|
|
||||||
tokens_input: integer(data, "tokens.input"),
|
|
||||||
tokens_output: integer(data, "tokens.output"),
|
|
||||||
tokens_reasoning: integer(data, "tokens.reasoning"),
|
|
||||||
tokens_cache_read: integer(data, "tokens.cache_read"),
|
|
||||||
tokens_cache_write_5m: integer(data, "tokens.cache_write_5m"),
|
|
||||||
tokens_cache_write_1h: integer(data, "tokens.cache_write_1h"),
|
|
||||||
cost_input_microcents: integer(data, "cost.input.microcents"),
|
|
||||||
cost_output_microcents: integer(data, "cost.output.microcents"),
|
|
||||||
cost_cache_read_microcents: integer(data, "cost.cache_read.microcents"),
|
|
||||||
cost_cache_write_microcents: integer(data, "cost.cache_write.microcents"),
|
|
||||||
cost_total_microcents: integer(data, "cost.total.microcents"),
|
|
||||||
cost_input: integer(data, "cost.input"),
|
|
||||||
cost_output: integer(data, "cost.output"),
|
|
||||||
cost_cache_read: integer(data, "cost.cache_read"),
|
|
||||||
cost_cache_write_5m: integer(data, "cost.cache_write_5m"),
|
|
||||||
cost_cache_write_1h: integer(data, "cost.cache_write_1h"),
|
|
||||||
cost_total: integer(data, "cost.total"),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function string(data: Record<string, unknown>, key: string) {
|
|
||||||
const value = data[key]
|
|
||||||
if (typeof value === "string") return value
|
|
||||||
if (typeof value === "number" || typeof value === "boolean") return String(value)
|
|
||||||
return undefined
|
|
||||||
}
|
|
||||||
|
|
||||||
function boolean(data: Record<string, unknown>, key: string) {
|
|
||||||
const value = data[key]
|
|
||||||
if (typeof value === "boolean") return value
|
|
||||||
if (typeof value === "string") return value === "true" ? true : value === "false" ? false : undefined
|
|
||||||
return undefined
|
|
||||||
}
|
|
||||||
|
|
||||||
function integer(data: Record<string, unknown>, key: string) {
|
|
||||||
const value = number(data, key)
|
|
||||||
if (value === undefined) return undefined
|
|
||||||
return Math.round(value)
|
|
||||||
}
|
|
||||||
|
|
||||||
function number(data: Record<string, unknown>, key: string) {
|
|
||||||
const value = data[key]
|
|
||||||
if (typeof value === "number") return Number.isFinite(value) ? value : undefined
|
|
||||||
if (typeof value === "string") {
|
|
||||||
const parsed = Number(value)
|
|
||||||
return Number.isFinite(parsed) ? parsed : undefined
|
|
||||||
}
|
|
||||||
return undefined
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@opencode-ai/console-mail",
|
"name": "@opencode-ai/console-mail",
|
||||||
"version": "1.15.11",
|
"version": "1.15.10",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@jsx-email/all": "2.2.3",
|
"@jsx-email/all": "2.2.3",
|
||||||
"@jsx-email/cli": "1.4.3",
|
"@jsx-email/cli": "1.4.3",
|
||||||
|
|||||||
@@ -11,7 +11,6 @@ export const Resource = new Proxy(
|
|||||||
{
|
{
|
||||||
get(_target, prop: keyof typeof ResourceBase) {
|
get(_target, prop: keyof typeof ResourceBase) {
|
||||||
const value = ResourceBase[prop]
|
const value = ResourceBase[prop]
|
||||||
const secrets = ResourceBase as unknown as Record<string, { value: string }>
|
|
||||||
if ("type" in value) {
|
if ("type" in value) {
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
if (value.type === "sst.cloudflare.Bucket") {
|
if (value.type === "sst.cloudflare.Bucket") {
|
||||||
@@ -22,11 +21,11 @@ export const Resource = new Proxy(
|
|||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
if (value.type === "sst.cloudflare.Kv") {
|
if (value.type === "sst.cloudflare.Kv") {
|
||||||
const client = new Cloudflare({
|
const client = new Cloudflare({
|
||||||
apiToken: secrets.CLOUDFLARE_API_TOKEN.value,
|
apiToken: ResourceBase.CLOUDFLARE_API_TOKEN.value,
|
||||||
})
|
})
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
const namespaceId = value.namespaceId
|
const namespaceId = value.namespaceId
|
||||||
const accountId = secrets.CLOUDFLARE_DEFAULT_ACCOUNT_ID.value
|
const accountId = ResourceBase.CLOUDFLARE_DEFAULT_ACCOUNT_ID.value
|
||||||
return {
|
return {
|
||||||
get: (k: string | string[]) => {
|
get: (k: string | string[]) => {
|
||||||
const isMulti = Array.isArray(k)
|
const isMulti = Array.isArray(k)
|
||||||
|
|||||||
@@ -1,29 +0,0 @@
|
|||||||
{
|
|
||||||
"name": "@opencode-ai/console-support",
|
|
||||||
"version": "1.15.11",
|
|
||||||
"type": "module",
|
|
||||||
"license": "MIT",
|
|
||||||
"scripts": {
|
|
||||||
"typecheck": "tsgo --noEmit",
|
|
||||||
"dev": "sst shell --stage production -- vite dev"
|
|
||||||
},
|
|
||||||
"dependencies": {
|
|
||||||
"@cloudflare/vite-plugin": "1.15.2",
|
|
||||||
"@opencode-ai/console-core": "workspace:*",
|
|
||||||
"@solidjs/meta": "catalog:",
|
|
||||||
"@solidjs/router": "catalog:",
|
|
||||||
"@solidjs/start": "catalog:",
|
|
||||||
"nitro": "3.0.1-alpha.1",
|
|
||||||
"solid-js": "catalog:",
|
|
||||||
"vite": "catalog:"
|
|
||||||
},
|
|
||||||
"devDependencies": {
|
|
||||||
"@types/bun": "catalog:",
|
|
||||||
"@typescript/native-preview": "catalog:",
|
|
||||||
"typescript": "catalog:",
|
|
||||||
"wrangler": "4.50.0"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=22"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,133 +0,0 @@
|
|||||||
* {
|
|
||||||
box-sizing: border-box;
|
|
||||||
}
|
|
||||||
|
|
||||||
html,
|
|
||||||
body {
|
|
||||||
margin: 0;
|
|
||||||
padding: 0;
|
|
||||||
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;
|
|
||||||
background: #0d0d0d;
|
|
||||||
color: #e6e6e6;
|
|
||||||
}
|
|
||||||
|
|
||||||
main[data-page="support"] {
|
|
||||||
max-width: 1400px;
|
|
||||||
margin: 0 auto;
|
|
||||||
padding: 2rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
main[data-page="support"] h1 {
|
|
||||||
margin: 0 0 1rem;
|
|
||||||
font-size: 1.25rem;
|
|
||||||
font-weight: 500;
|
|
||||||
}
|
|
||||||
|
|
||||||
form[data-component="lookup"] {
|
|
||||||
display: flex;
|
|
||||||
gap: 0.5rem;
|
|
||||||
margin-bottom: 2rem;
|
|
||||||
align-items: center;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
}
|
|
||||||
|
|
||||||
form[data-component="lookup"] input[type="text"] {
|
|
||||||
flex: 1;
|
|
||||||
min-width: 280px;
|
|
||||||
padding: 0.6rem 0.75rem;
|
|
||||||
border: 1px solid #2a2a2a;
|
|
||||||
background: #161616;
|
|
||||||
color: #e6e6e6;
|
|
||||||
border-radius: 4px;
|
|
||||||
font: inherit;
|
|
||||||
}
|
|
||||||
|
|
||||||
form[data-component="lookup"] label {
|
|
||||||
display: inline-flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 0.4rem;
|
|
||||||
color: #b0b0b0;
|
|
||||||
}
|
|
||||||
|
|
||||||
form[data-component="lookup"] button {
|
|
||||||
padding: 0.6rem 1rem;
|
|
||||||
border: 1px solid #3a3a3a;
|
|
||||||
background: #1f1f1f;
|
|
||||||
color: #e6e6e6;
|
|
||||||
border-radius: 4px;
|
|
||||||
font: inherit;
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
|
|
||||||
form[data-component="lookup"] button:hover {
|
|
||||||
background: #2a2a2a;
|
|
||||||
}
|
|
||||||
|
|
||||||
form[data-component="lookup"] button:disabled {
|
|
||||||
opacity: 0.5;
|
|
||||||
cursor: not-allowed;
|
|
||||||
}
|
|
||||||
|
|
||||||
[data-component="error"] {
|
|
||||||
color: #ff6b6b;
|
|
||||||
background: #2a1414;
|
|
||||||
border: 1px solid #4a1f1f;
|
|
||||||
padding: 0.75rem 1rem;
|
|
||||||
border-radius: 4px;
|
|
||||||
margin-bottom: 1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
[data-component="section"] {
|
|
||||||
margin-bottom: 2rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
[data-component="section"] h2 {
|
|
||||||
margin: 0 0 0.5rem;
|
|
||||||
font-size: 1rem;
|
|
||||||
font-weight: 500;
|
|
||||||
color: #b0b0b0;
|
|
||||||
border-bottom: 1px solid #2a2a2a;
|
|
||||||
padding-bottom: 0.4rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
[data-component="section"] h3 {
|
|
||||||
margin: 1.75rem 0 0.5rem;
|
|
||||||
font-size: 0.875rem;
|
|
||||||
font-weight: 500;
|
|
||||||
color: #888;
|
|
||||||
}
|
|
||||||
|
|
||||||
[data-component="section"] h3:first-of-type {
|
|
||||||
margin-top: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
[data-component="section"] table {
|
|
||||||
width: 100%;
|
|
||||||
border-collapse: collapse;
|
|
||||||
font-size: 0.8125rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
[data-component="section"] th,
|
|
||||||
[data-component="section"] td {
|
|
||||||
text-align: left;
|
|
||||||
padding: 0.4rem 0.6rem;
|
|
||||||
border-bottom: 1px solid #1f1f1f;
|
|
||||||
vertical-align: top;
|
|
||||||
white-space: nowrap;
|
|
||||||
}
|
|
||||||
|
|
||||||
[data-component="section"] th {
|
|
||||||
color: #888;
|
|
||||||
font-weight: 500;
|
|
||||||
background: #141414;
|
|
||||||
}
|
|
||||||
|
|
||||||
[data-component="section"] td a {
|
|
||||||
color: #6ea8fe;
|
|
||||||
}
|
|
||||||
|
|
||||||
[data-component="section"] [data-empty] {
|
|
||||||
color: #666;
|
|
||||||
font-style: italic;
|
|
||||||
padding: 0.4rem 0;
|
|
||||||
}
|
|
||||||
@@ -1,21 +0,0 @@
|
|||||||
import { MetaProvider, Title } from "@solidjs/meta"
|
|
||||||
import { Router } from "@solidjs/router"
|
|
||||||
import { FileRoutes } from "@solidjs/start/router"
|
|
||||||
import { Suspense } from "solid-js"
|
|
||||||
import "./app.css"
|
|
||||||
|
|
||||||
export default function App() {
|
|
||||||
return (
|
|
||||||
<Router
|
|
||||||
explicitLinks={true}
|
|
||||||
root={(props) => (
|
|
||||||
<MetaProvider>
|
|
||||||
<Title>opencode support</Title>
|
|
||||||
<Suspense>{props.children}</Suspense>
|
|
||||||
</MetaProvider>
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
<FileRoutes />
|
|
||||||
</Router>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -1,119 +0,0 @@
|
|||||||
import { For, Show } from "solid-js"
|
|
||||||
import type { LookupResult, WorkspaceSection } from "~/lib/lookup"
|
|
||||||
|
|
||||||
export function Result(props: { data: LookupResult }) {
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<Show when={props.data.auth}>
|
|
||||||
{(auth) => (
|
|
||||||
<section data-component="section">
|
|
||||||
<h2>Auth</h2>
|
|
||||||
<DataTable rows={auth()} />
|
|
||||||
</section>
|
|
||||||
)}
|
|
||||||
</Show>
|
|
||||||
|
|
||||||
<Show when={props.data.accountWorkspaces}>
|
|
||||||
{(workspaces) => (
|
|
||||||
<section data-component="section">
|
|
||||||
<h2>Workspaces</h2>
|
|
||||||
<DataTable rows={workspaces()} />
|
|
||||||
</section>
|
|
||||||
)}
|
|
||||||
</Show>
|
|
||||||
|
|
||||||
<For each={props.data.workspaces}>{(ws) => <WorkspaceView section={ws} />}</For>
|
|
||||||
</>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function WorkspaceView(props: { section: WorkspaceSection }) {
|
|
||||||
return (
|
|
||||||
<section data-component="section" id={`workspace-${props.section.workspaceID}`}>
|
|
||||||
<h2>{props.section.title}</h2>
|
|
||||||
|
|
||||||
<h3>Users</h3>
|
|
||||||
<DataTable rows={props.section.users} />
|
|
||||||
|
|
||||||
<h3>Billing</h3>
|
|
||||||
<DataTable rows={props.section.billing ? [props.section.billing] : []} />
|
|
||||||
|
|
||||||
<h3>GO</h3>
|
|
||||||
<DataTable rows={props.section.go} />
|
|
||||||
|
|
||||||
<h3>Payments</h3>
|
|
||||||
<DataTable rows={props.section.payments} />
|
|
||||||
|
|
||||||
<h3>28-Day Usage</h3>
|
|
||||||
<DataTable rows={props.section.usage} />
|
|
||||||
|
|
||||||
<h3>Disabled Models</h3>
|
|
||||||
<DataTable rows={props.section.disabledModels} />
|
|
||||||
</section>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function DataTable(props: { rows: Record<string, unknown>[] }) {
|
|
||||||
const columns = () => {
|
|
||||||
const cols = new Set<string>()
|
|
||||||
for (const row of props.rows) {
|
|
||||||
for (const key of Object.keys(row)) cols.add(key)
|
|
||||||
}
|
|
||||||
return [...cols]
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Show when={props.rows.length > 0} fallback={<div data-empty>(no data)</div>}>
|
|
||||||
<table>
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<For each={columns()}>{(col) => <th>{col}</th>}</For>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
<For each={props.rows}>
|
|
||||||
{(row) => (
|
|
||||||
<tr>
|
|
||||||
<For each={columns()}>{(col) => <td>{renderCell(row[col])}</td>}</For>
|
|
||||||
</tr>
|
|
||||||
)}
|
|
||||||
</For>
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</Show>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function renderCell(value: unknown) {
|
|
||||||
if (value === null || value === undefined) return ""
|
|
||||||
if (typeof value === "string" && value.startsWith("https://")) {
|
|
||||||
return (
|
|
||||||
<a href={value} target="_blank" rel="noopener noreferrer">
|
|
||||||
{value}
|
|
||||||
</a>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
if (isLinkCell(value)) {
|
|
||||||
const external = value.__link.startsWith("http")
|
|
||||||
return (
|
|
||||||
<a
|
|
||||||
href={value.__link}
|
|
||||||
target={external ? "_blank" : undefined}
|
|
||||||
rel={external ? "noopener noreferrer" : undefined}
|
|
||||||
>
|
|
||||||
{value.label}
|
|
||||||
</a>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
if (typeof value === "object") return JSON.stringify(value)
|
|
||||||
return String(value)
|
|
||||||
}
|
|
||||||
|
|
||||||
function isLinkCell(value: unknown): value is { __link: string; label: string } {
|
|
||||||
return (
|
|
||||||
typeof value === "object" &&
|
|
||||||
value !== null &&
|
|
||||||
"__link" in value &&
|
|
||||||
typeof (value as { __link: unknown }).__link === "string"
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -1,4 +0,0 @@
|
|||||||
// @refresh reload
|
|
||||||
import { mount, StartClient } from "@solidjs/start/client"
|
|
||||||
|
|
||||||
mount(() => <StartClient />, document.getElementById("app")!)
|
|
||||||
@@ -1,26 +0,0 @@
|
|||||||
// @refresh reload
|
|
||||||
import { createHandler, StartServer } from "@solidjs/start/server"
|
|
||||||
|
|
||||||
export default createHandler(
|
|
||||||
() => (
|
|
||||||
<StartServer
|
|
||||||
document={({ assets, children, scripts }) => (
|
|
||||||
<html lang="en">
|
|
||||||
<head>
|
|
||||||
<meta charset="utf-8" />
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
||||||
<meta name="robots" content="noindex,nofollow" />
|
|
||||||
{assets}
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<div id="app">{children}</div>
|
|
||||||
{scripts}
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
),
|
|
||||||
{
|
|
||||||
mode: "async",
|
|
||||||
},
|
|
||||||
)
|
|
||||||
-1
@@ -1 +0,0 @@
|
|||||||
/// <reference types="@solidjs/start/env" />
|
|
||||||
@@ -1,479 +0,0 @@
|
|||||||
"use server"
|
|
||||||
|
|
||||||
import { Database, and, eq, isNull, sql } from "@opencode-ai/console-core/drizzle/index.js"
|
|
||||||
import { AuthTable } from "@opencode-ai/console-core/schema/auth.sql.js"
|
|
||||||
import { UserTable } from "@opencode-ai/console-core/schema/user.sql.js"
|
|
||||||
import {
|
|
||||||
BillingTable,
|
|
||||||
PaymentTable,
|
|
||||||
SubscriptionTable,
|
|
||||||
BlackPlans,
|
|
||||||
UsageTable,
|
|
||||||
LiteTable,
|
|
||||||
} from "@opencode-ai/console-core/schema/billing.sql.js"
|
|
||||||
import { WorkspaceTable } from "@opencode-ai/console-core/schema/workspace.sql.js"
|
|
||||||
import { KeyTable } from "@opencode-ai/console-core/schema/key.sql.js"
|
|
||||||
import { ModelTable } from "@opencode-ai/console-core/schema/model.sql.js"
|
|
||||||
import { BlackData } from "@opencode-ai/console-core/black.js"
|
|
||||||
import { LiteData } from "@opencode-ai/console-core/lite.js"
|
|
||||||
import { Subscription } from "@opencode-ai/console-core/subscription.js"
|
|
||||||
import { centsToMicroCents } from "@opencode-ai/console-core/util/price.js"
|
|
||||||
import { getWeekBounds } from "@opencode-ai/console-core/util/date.js"
|
|
||||||
|
|
||||||
export type LookupResult = {
|
|
||||||
identifier: string
|
|
||||||
auth?: Record<string, unknown>[]
|
|
||||||
accountWorkspaces?: Record<string, unknown>[]
|
|
||||||
workspaces: WorkspaceSection[]
|
|
||||||
}
|
|
||||||
|
|
||||||
export type WorkspaceSection = {
|
|
||||||
workspaceID: string
|
|
||||||
title: string
|
|
||||||
users: Record<string, unknown>[]
|
|
||||||
billing: Record<string, unknown> | null
|
|
||||||
go: Record<string, unknown>[]
|
|
||||||
payments: Record<string, unknown>[]
|
|
||||||
usage: Record<string, unknown>[]
|
|
||||||
disabledModels: Record<string, unknown>[]
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function lookup(identifier: string): Promise<LookupResult> {
|
|
||||||
if (!identifier) throw new Error("Identifier is required")
|
|
||||||
|
|
||||||
if (identifier.startsWith("wrk_")) {
|
|
||||||
const workspace = await loadWorkspace(identifier)
|
|
||||||
return { identifier, workspaces: [workspace] }
|
|
||||||
}
|
|
||||||
|
|
||||||
if (identifier.startsWith("key_")) {
|
|
||||||
const key = await Database.use((tx) =>
|
|
||||||
tx
|
|
||||||
.select()
|
|
||||||
.from(KeyTable)
|
|
||||||
.where(eq(KeyTable.id, identifier))
|
|
||||||
.then((rows) => rows[0]),
|
|
||||||
)
|
|
||||||
if (!key) throw new Error("API key not found")
|
|
||||||
const workspace = await loadWorkspace(key.workspaceID)
|
|
||||||
return { identifier, workspaces: [workspace] }
|
|
||||||
}
|
|
||||||
|
|
||||||
if (identifier.startsWith("sk-")) {
|
|
||||||
const key = await Database.use((tx) =>
|
|
||||||
tx
|
|
||||||
.select()
|
|
||||||
.from(KeyTable)
|
|
||||||
.where(eq(KeyTable.key, identifier))
|
|
||||||
.then((rows) => rows[0]),
|
|
||||||
)
|
|
||||||
if (!key) throw new Error("API key not found")
|
|
||||||
const workspace = await loadWorkspace(key.workspaceID)
|
|
||||||
return { identifier, workspaces: [workspace] }
|
|
||||||
}
|
|
||||||
|
|
||||||
// Treat as email
|
|
||||||
const authData = await Database.use((tx) => tx.select().from(AuthTable).where(eq(AuthTable.subject, identifier)))
|
|
||||||
if (authData.length === 0) throw new Error("Email not found")
|
|
||||||
|
|
||||||
const accountID = authData[0].accountID
|
|
||||||
const auth = await Database.use((tx) => tx.select().from(AuthTable).where(eq(AuthTable.accountID, accountID)))
|
|
||||||
|
|
||||||
const accountWorkspaces = await Database.use((tx) =>
|
|
||||||
tx
|
|
||||||
.select({
|
|
||||||
userID: UserTable.id,
|
|
||||||
workspaceID: UserTable.workspaceID,
|
|
||||||
workspaceName: WorkspaceTable.name,
|
|
||||||
balance: BillingTable.balance,
|
|
||||||
role: UserTable.role,
|
|
||||||
black: SubscriptionTable.timeCreated,
|
|
||||||
lite: LiteTable.timeCreated,
|
|
||||||
})
|
|
||||||
.from(UserTable)
|
|
||||||
.rightJoin(WorkspaceTable, eq(WorkspaceTable.id, UserTable.workspaceID))
|
|
||||||
.leftJoin(BillingTable, eq(BillingTable.workspaceID, WorkspaceTable.id))
|
|
||||||
.leftJoin(SubscriptionTable, eq(SubscriptionTable.userID, UserTable.id))
|
|
||||||
.leftJoin(LiteTable, eq(LiteTable.userID, UserTable.id))
|
|
||||||
.where(eq(UserTable.accountID, accountID))
|
|
||||||
.then((rows) =>
|
|
||||||
rows.map((row) => ({
|
|
||||||
workspaceName: row.workspaceID
|
|
||||||
? { __link: `#workspace-${row.workspaceID}`, label: row.workspaceName }
|
|
||||||
: row.workspaceName,
|
|
||||||
userID: row.userID,
|
|
||||||
workspaceID: row.workspaceID,
|
|
||||||
balance: formatMicroCents(row.balance) ?? "$0.00",
|
|
||||||
role: row.role,
|
|
||||||
black: formatDate(row.black),
|
|
||||||
lite: formatDate(row.lite),
|
|
||||||
})),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
|
|
||||||
const workspaces: WorkspaceSection[] = []
|
|
||||||
for (const w of accountWorkspaces) {
|
|
||||||
if (!w.workspaceID) continue
|
|
||||||
workspaces.push(await loadWorkspace(w.workspaceID))
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
identifier,
|
|
||||||
auth: auth.map((row) => ({
|
|
||||||
provider: row.provider,
|
|
||||||
subject: row.subject,
|
|
||||||
accountID: row.accountID,
|
|
||||||
})),
|
|
||||||
accountWorkspaces,
|
|
||||||
workspaces,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function loadWorkspace(workspaceID: string): Promise<WorkspaceSection> {
|
|
||||||
const workspace = await Database.use((tx) =>
|
|
||||||
tx
|
|
||||||
.select()
|
|
||||||
.from(WorkspaceTable)
|
|
||||||
.where(eq(WorkspaceTable.id, workspaceID))
|
|
||||||
.then((rows) => rows[0]),
|
|
||||||
)
|
|
||||||
if (!workspace) throw new Error(`Workspace ${workspaceID} not found`)
|
|
||||||
|
|
||||||
const users = await Database.use((tx) =>
|
|
||||||
tx
|
|
||||||
.select({
|
|
||||||
authEmail: AuthTable.subject,
|
|
||||||
inviteEmail: UserTable.email,
|
|
||||||
role: UserTable.role,
|
|
||||||
timeSeen: UserTable.timeSeen,
|
|
||||||
monthlyLimit: UserTable.monthlyLimit,
|
|
||||||
monthlyUsage: UserTable.monthlyUsage,
|
|
||||||
timeDeleted: UserTable.timeDeleted,
|
|
||||||
fixedUsage: SubscriptionTable.fixedUsage,
|
|
||||||
rollingUsage: SubscriptionTable.rollingUsage,
|
|
||||||
timeFixedUpdated: SubscriptionTable.timeFixedUpdated,
|
|
||||||
timeRollingUpdated: SubscriptionTable.timeRollingUpdated,
|
|
||||||
timeSubscriptionCreated: SubscriptionTable.timeCreated,
|
|
||||||
subscription: BillingTable.subscription,
|
|
||||||
})
|
|
||||||
.from(UserTable)
|
|
||||||
.innerJoin(BillingTable, eq(BillingTable.workspaceID, workspace.id))
|
|
||||||
.leftJoin(AuthTable, and(eq(UserTable.accountID, AuthTable.accountID), eq(AuthTable.provider, "email")))
|
|
||||||
.leftJoin(SubscriptionTable, eq(SubscriptionTable.userID, UserTable.id))
|
|
||||||
.where(eq(UserTable.workspaceID, workspace.id))
|
|
||||||
.then((rows) =>
|
|
||||||
rows.map((row) => {
|
|
||||||
const subStatus = getSubscriptionStatus(row)
|
|
||||||
return {
|
|
||||||
email: (row.timeDeleted ? "[deleted] " : "") + (row.authEmail ?? row.inviteEmail),
|
|
||||||
role: row.role,
|
|
||||||
timeSeen: formatDate(row.timeSeen),
|
|
||||||
monthly: formatMonthlyUsage(row.monthlyUsage, row.monthlyLimit),
|
|
||||||
subscribed: formatDate(row.timeSubscriptionCreated),
|
|
||||||
subWeekly: subStatus.weekly,
|
|
||||||
subRolling: subStatus.rolling,
|
|
||||||
rateLimited: subStatus.rateLimited,
|
|
||||||
retryIn: subStatus.retryIn,
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
|
|
||||||
const billing = await Database.use((tx) =>
|
|
||||||
tx
|
|
||||||
.select({
|
|
||||||
balance: BillingTable.balance,
|
|
||||||
customerID: BillingTable.customerID,
|
|
||||||
reload: BillingTable.reload,
|
|
||||||
blackSubscriptionID: BillingTable.subscriptionID,
|
|
||||||
blackSubscription: {
|
|
||||||
plan: BillingTable.subscriptionPlan,
|
|
||||||
booked: BillingTable.timeSubscriptionBooked,
|
|
||||||
enrichment: BillingTable.subscription,
|
|
||||||
},
|
|
||||||
timeBlackSubscriptionSelected: BillingTable.timeSubscriptionSelected,
|
|
||||||
liteSubscriptionID: BillingTable.liteSubscriptionID,
|
|
||||||
})
|
|
||||||
.from(BillingTable)
|
|
||||||
.where(eq(BillingTable.workspaceID, workspace.id))
|
|
||||||
.then(
|
|
||||||
(rows) =>
|
|
||||||
rows.map((row) => ({
|
|
||||||
balance: `$${(row.balance / 100000000).toFixed(2)}`,
|
|
||||||
reload: row.reload ? "yes" : "no",
|
|
||||||
customerID: row.customerID,
|
|
||||||
GO: row.liteSubscriptionID,
|
|
||||||
Black: row.blackSubscriptionID
|
|
||||||
? [
|
|
||||||
`Black ${row.blackSubscription.enrichment!.plan}`,
|
|
||||||
row.blackSubscription.enrichment!.seats > 1
|
|
||||||
? `X ${row.blackSubscription.enrichment!.seats} seats`
|
|
||||||
: "",
|
|
||||||
row.blackSubscription.enrichment!.coupon
|
|
||||||
? `(coupon: ${row.blackSubscription.enrichment!.coupon})`
|
|
||||||
: "",
|
|
||||||
`(ref: ${row.blackSubscriptionID})`,
|
|
||||||
].join(" ")
|
|
||||||
: row.blackSubscription.booked
|
|
||||||
? `Waitlist ${row.blackSubscription.plan} plan${row.timeBlackSubscriptionSelected ? " (selected)" : ""}`
|
|
||||||
: undefined,
|
|
||||||
}))[0] ?? null,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
|
|
||||||
const liteLimits = LiteData.getLimits()
|
|
||||||
const go = await Database.use((tx) =>
|
|
||||||
tx
|
|
||||||
.select({
|
|
||||||
userID: LiteTable.userID,
|
|
||||||
userEmail: UserTable.email,
|
|
||||||
authEmail: AuthTable.subject,
|
|
||||||
rollingUsage: LiteTable.rollingUsage,
|
|
||||||
weeklyUsage: LiteTable.weeklyUsage,
|
|
||||||
monthlyUsage: LiteTable.monthlyUsage,
|
|
||||||
timeRollingUpdated: LiteTable.timeRollingUpdated,
|
|
||||||
timeWeeklyUpdated: LiteTable.timeWeeklyUpdated,
|
|
||||||
timeMonthlyUpdated: LiteTable.timeMonthlyUpdated,
|
|
||||||
timeCreated: LiteTable.timeCreated,
|
|
||||||
useBalance: BillingTable.lite,
|
|
||||||
})
|
|
||||||
.from(LiteTable)
|
|
||||||
.innerJoin(BillingTable, eq(BillingTable.workspaceID, LiteTable.workspaceID))
|
|
||||||
.leftJoin(UserTable, eq(UserTable.id, LiteTable.userID))
|
|
||||||
.leftJoin(AuthTable, and(eq(UserTable.accountID, AuthTable.accountID), eq(AuthTable.provider, "email")))
|
|
||||||
.where(and(eq(LiteTable.workspaceID, workspace.id), isNull(LiteTable.timeDeleted)))
|
|
||||||
.then((rows) =>
|
|
||||||
rows.map((row) => {
|
|
||||||
const rolling = Subscription.analyzeRollingUsage({
|
|
||||||
limit: liteLimits.rollingLimit,
|
|
||||||
window: liteLimits.rollingWindow,
|
|
||||||
usage: row.rollingUsage ?? 0,
|
|
||||||
timeUpdated: row.timeRollingUpdated ?? new Date(),
|
|
||||||
})
|
|
||||||
const weekly = Subscription.analyzeWeeklyUsage({
|
|
||||||
limit: liteLimits.weeklyLimit,
|
|
||||||
usage: row.weeklyUsage ?? 0,
|
|
||||||
timeUpdated: row.timeWeeklyUpdated ?? new Date(),
|
|
||||||
})
|
|
||||||
const monthly = Subscription.analyzeMonthlyUsage({
|
|
||||||
limit: liteLimits.monthlyLimit,
|
|
||||||
usage: row.monthlyUsage ?? 0,
|
|
||||||
timeUpdated: row.timeMonthlyUpdated ?? new Date(),
|
|
||||||
timeSubscribed: row.timeCreated,
|
|
||||||
})
|
|
||||||
return {
|
|
||||||
email: row.authEmail ?? row.userEmail ?? row.userID,
|
|
||||||
subscribed: formatDate(row.timeCreated),
|
|
||||||
useBalance: row.useBalance?.useBalance ? "yes" : "no",
|
|
||||||
rolling: formatLiteUsage(rolling),
|
|
||||||
weekly: formatLiteUsage(weekly),
|
|
||||||
monthly: formatLiteUsage(monthly),
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
|
|
||||||
const payments = await Database.use((tx) =>
|
|
||||||
tx
|
|
||||||
.select({
|
|
||||||
amount: PaymentTable.amount,
|
|
||||||
paymentID: PaymentTable.paymentID,
|
|
||||||
invoiceID: PaymentTable.invoiceID,
|
|
||||||
customerID: PaymentTable.customerID,
|
|
||||||
timeCreated: PaymentTable.timeCreated,
|
|
||||||
timeRefunded: PaymentTable.timeRefunded,
|
|
||||||
})
|
|
||||||
.from(PaymentTable)
|
|
||||||
.where(eq(PaymentTable.workspaceID, workspace.id))
|
|
||||||
.orderBy(sql`${PaymentTable.timeCreated} DESC`)
|
|
||||||
.limit(100)
|
|
||||||
.then((rows) =>
|
|
||||||
rows.map((row) => ({
|
|
||||||
amount: `$${(row.amount / 100000000).toFixed(2)}`,
|
|
||||||
paymentID: row.paymentID
|
|
||||||
? `https://dashboard.stripe.com/acct_1RszBH2StuRr0lbX/payments/${row.paymentID}`
|
|
||||||
: null,
|
|
||||||
invoiceID: row.invoiceID,
|
|
||||||
customerID: row.customerID,
|
|
||||||
timeCreated: formatDate(row.timeCreated),
|
|
||||||
timeRefunded: formatDate(row.timeRefunded),
|
|
||||||
})),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
|
|
||||||
const planExpr = sql`JSON_UNQUOTE(JSON_EXTRACT(${UsageTable.enrichment}, '$.plan'))`
|
|
||||||
const usage = await Database.use((tx) =>
|
|
||||||
tx
|
|
||||||
.select({
|
|
||||||
date: sql<string>`DATE(${UsageTable.timeCreated})`.as("date"),
|
|
||||||
freeRequests: sql<number>`SUM(CASE WHEN ${UsageTable.cost} = 0 THEN 1 ELSE 0 END)`.as("free_requests"),
|
|
||||||
goRequests: sql<number>`SUM(CASE WHEN ${planExpr} = 'lite' THEN 1 ELSE 0 END)`.as("go_requests"),
|
|
||||||
goCost: sql<number>`SUM(CASE WHEN ${planExpr} = 'lite' THEN ${UsageTable.cost} ELSE 0 END)`.as("go_cost"),
|
|
||||||
apiRequests: sql<number>`SUM(CASE WHEN ${planExpr} IS NULL AND ${UsageTable.cost} > 0 THEN 1 ELSE 0 END)`.as(
|
|
||||||
"api_requests",
|
|
||||||
),
|
|
||||||
apiCost:
|
|
||||||
sql<number>`SUM(CASE WHEN ${planExpr} IS NULL AND ${UsageTable.cost} > 0 THEN ${UsageTable.cost} ELSE 0 END)`.as(
|
|
||||||
"api_cost",
|
|
||||||
),
|
|
||||||
})
|
|
||||||
.from(UsageTable)
|
|
||||||
.where(
|
|
||||||
and(
|
|
||||||
eq(UsageTable.workspaceID, workspace.id),
|
|
||||||
sql`${UsageTable.timeCreated} >= DATE_SUB(NOW(), INTERVAL 28 DAY)`,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
.groupBy(sql`DATE(${UsageTable.timeCreated})`)
|
|
||||||
.orderBy(sql`DATE(${UsageTable.timeCreated}) DESC`)
|
|
||||||
.then((rows) => {
|
|
||||||
const totals = rows.reduce(
|
|
||||||
(acc, r) => ({
|
|
||||||
freeRequests: acc.freeRequests + Number(r.freeRequests),
|
|
||||||
goRequests: acc.goRequests + Number(r.goRequests),
|
|
||||||
goCost: acc.goCost + Number(r.goCost),
|
|
||||||
apiRequests: acc.apiRequests + Number(r.apiRequests),
|
|
||||||
apiCost: acc.apiCost + Number(r.apiCost),
|
|
||||||
}),
|
|
||||||
{ freeRequests: 0, goRequests: 0, goCost: 0, apiRequests: 0, apiCost: 0 },
|
|
||||||
)
|
|
||||||
const mapped: Record<string, unknown>[] = rows.map((row) => ({
|
|
||||||
date: row.date,
|
|
||||||
freeRequests: Number(row.freeRequests),
|
|
||||||
goRequests: Number(row.goRequests),
|
|
||||||
goCost: formatMicroCents(Number(row.goCost)) ?? "$0.00",
|
|
||||||
apiRequests: Number(row.apiRequests),
|
|
||||||
apiCost: formatMicroCents(Number(row.apiCost)) ?? "$0.00",
|
|
||||||
}))
|
|
||||||
if (mapped.length > 0) {
|
|
||||||
mapped.push({
|
|
||||||
date: "TOTAL",
|
|
||||||
freeRequests: totals.freeRequests,
|
|
||||||
goRequests: totals.goRequests,
|
|
||||||
goCost: formatMicroCents(totals.goCost) ?? "$0.00",
|
|
||||||
apiRequests: totals.apiRequests,
|
|
||||||
apiCost: formatMicroCents(totals.apiCost) ?? "$0.00",
|
|
||||||
})
|
|
||||||
}
|
|
||||||
return mapped
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
|
|
||||||
const disabledModels = await Database.use((tx) =>
|
|
||||||
tx
|
|
||||||
.select({
|
|
||||||
model: ModelTable.model,
|
|
||||||
timeCreated: ModelTable.timeCreated,
|
|
||||||
})
|
|
||||||
.from(ModelTable)
|
|
||||||
.where(eq(ModelTable.workspaceID, workspace.id))
|
|
||||||
.orderBy(sql`${ModelTable.timeCreated} DESC`)
|
|
||||||
.then((rows) =>
|
|
||||||
rows.map((row) => ({
|
|
||||||
model: row.model,
|
|
||||||
timeCreated: formatDate(row.timeCreated),
|
|
||||||
})),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
|
|
||||||
return {
|
|
||||||
workspaceID: workspace.id,
|
|
||||||
title: `Workspace "${workspace.name}" (${workspace.id})`,
|
|
||||||
users,
|
|
||||||
billing,
|
|
||||||
go,
|
|
||||||
payments,
|
|
||||||
usage,
|
|
||||||
disabledModels,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function formatLiteUsage(usage: { status: "ok" | "rate-limited"; usagePercent: number; resetInSec: number }) {
|
|
||||||
const reset = formatResetTime(usage.resetInSec)
|
|
||||||
const status = usage.status === "rate-limited" ? " [limited]" : ""
|
|
||||||
return `${usage.usagePercent}% (resets in ${reset})${status}`
|
|
||||||
}
|
|
||||||
|
|
||||||
function formatResetTime(seconds: number) {
|
|
||||||
if (seconds <= 0) return "now"
|
|
||||||
const days = Math.floor(seconds / 86400)
|
|
||||||
if (days >= 1) return `${days}d`
|
|
||||||
const hours = Math.floor(seconds / 3600)
|
|
||||||
if (hours >= 1) {
|
|
||||||
const minutes = Math.floor((seconds % 3600) / 60)
|
|
||||||
return minutes > 0 ? `${hours}h ${minutes}m` : `${hours}h`
|
|
||||||
}
|
|
||||||
const minutes = Math.max(1, Math.ceil(seconds / 60))
|
|
||||||
return `${minutes}m`
|
|
||||||
}
|
|
||||||
|
|
||||||
function formatMicroCents(value: number | null | undefined) {
|
|
||||||
if (value === null || value === undefined) return null
|
|
||||||
return `$${(value / 100000000).toFixed(2)}`
|
|
||||||
}
|
|
||||||
|
|
||||||
function formatDate(value: Date | null | undefined) {
|
|
||||||
if (!value) return null
|
|
||||||
return value.toISOString().split("T")[0]
|
|
||||||
}
|
|
||||||
|
|
||||||
function formatMonthlyUsage(usage: number | null | undefined, limit: number | null | undefined) {
|
|
||||||
const usageText = formatMicroCents(usage) ?? "$0.00"
|
|
||||||
if (limit === null || limit === undefined) return `${usageText} / no limit`
|
|
||||||
return `${usageText} / $${limit.toFixed(2)}`
|
|
||||||
}
|
|
||||||
|
|
||||||
function formatRetryTime(seconds: number) {
|
|
||||||
const days = Math.floor(seconds / 86400)
|
|
||||||
if (days >= 1) return `${days} day${days > 1 ? "s" : ""}`
|
|
||||||
const hours = Math.floor(seconds / 3600)
|
|
||||||
const minutes = Math.ceil((seconds % 3600) / 60)
|
|
||||||
if (hours >= 1) return `${hours}hr ${minutes}min`
|
|
||||||
return `${minutes}min`
|
|
||||||
}
|
|
||||||
|
|
||||||
function getSubscriptionStatus(row: {
|
|
||||||
subscription: {
|
|
||||||
plan: (typeof BlackPlans)[number]
|
|
||||||
} | null
|
|
||||||
timeSubscriptionCreated: Date | null
|
|
||||||
fixedUsage: number | null
|
|
||||||
rollingUsage: number | null
|
|
||||||
timeFixedUpdated: Date | null
|
|
||||||
timeRollingUpdated: Date | null
|
|
||||||
}) {
|
|
||||||
if (!row.timeSubscriptionCreated || !row.subscription) {
|
|
||||||
return { weekly: null, rolling: null, rateLimited: null, retryIn: null }
|
|
||||||
}
|
|
||||||
|
|
||||||
const black = BlackData.getLimits({ plan: row.subscription.plan })
|
|
||||||
const now = new Date()
|
|
||||||
const week = getWeekBounds(now)
|
|
||||||
|
|
||||||
const fixedLimit = black.fixedLimit ? centsToMicroCents(black.fixedLimit * 100) : null
|
|
||||||
const rollingLimit = black.rollingLimit ? centsToMicroCents(black.rollingLimit * 100) : null
|
|
||||||
const rollingWindowMs = (black.rollingWindow ?? 5) * 3600 * 1000
|
|
||||||
|
|
||||||
const currentWeekly =
|
|
||||||
row.fixedUsage && row.timeFixedUpdated && row.timeFixedUpdated >= week.start ? row.fixedUsage : 0
|
|
||||||
|
|
||||||
const windowStart = new Date(now.getTime() - rollingWindowMs)
|
|
||||||
const currentRolling =
|
|
||||||
row.rollingUsage && row.timeRollingUpdated && row.timeRollingUpdated >= windowStart ? row.rollingUsage : 0
|
|
||||||
|
|
||||||
const isWeeklyLimited = fixedLimit !== null && currentWeekly >= fixedLimit
|
|
||||||
const isRollingLimited = rollingLimit !== null && currentRolling >= rollingLimit
|
|
||||||
|
|
||||||
const retryIn = isWeeklyLimited
|
|
||||||
? formatRetryTime(Math.ceil((week.end.getTime() - now.getTime()) / 1000))
|
|
||||||
: isRollingLimited && row.timeRollingUpdated
|
|
||||||
? formatRetryTime(Math.ceil((row.timeRollingUpdated.getTime() + rollingWindowMs - now.getTime()) / 1000))
|
|
||||||
: null
|
|
||||||
|
|
||||||
return {
|
|
||||||
weekly: fixedLimit !== null ? `${formatMicroCents(currentWeekly)} / $${black.fixedLimit}` : null,
|
|
||||||
rolling: rollingLimit !== null ? `${formatMicroCents(currentRolling)} / $${black.rollingLimit}` : null,
|
|
||||||
rateLimited: isWeeklyLimited || isRollingLimited ? "yes" : "no",
|
|
||||||
retryIn,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,22 +0,0 @@
|
|||||||
import { Title } from "@solidjs/meta"
|
|
||||||
|
|
||||||
export default function SupportPage() {
|
|
||||||
return (
|
|
||||||
<main data-page="support">
|
|
||||||
<Title>opencode support — lookup user</Title>
|
|
||||||
<h1>Lookup user</h1>
|
|
||||||
|
|
||||||
<form data-component="lookup" action="/lookup" method="get" target="_blank">
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
name="identifier"
|
|
||||||
placeholder="email, wrk_..., key_..., or sk-..."
|
|
||||||
autocomplete="off"
|
|
||||||
autofocus
|
|
||||||
required
|
|
||||||
/>
|
|
||||||
<button type="submit">Lookup</button>
|
|
||||||
</form>
|
|
||||||
</main>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -1,39 +0,0 @@
|
|||||||
import { Title } from "@solidjs/meta"
|
|
||||||
import { createAsync, query, useSearchParams, type RouteDefinition } from "@solidjs/router"
|
|
||||||
import { Show } from "solid-js"
|
|
||||||
import { ErrorBoundary } from "solid-js"
|
|
||||||
import { Result } from "~/component/result"
|
|
||||||
import { lookup } from "~/lib/lookup"
|
|
||||||
|
|
||||||
const getLookup = query(async (identifier: string) => {
|
|
||||||
"use server"
|
|
||||||
return lookup(identifier)
|
|
||||||
}, "support.lookup")
|
|
||||||
|
|
||||||
export const route: RouteDefinition = {
|
|
||||||
preload: ({ location }) => {
|
|
||||||
const identifier = new URLSearchParams(location.search).get("identifier")?.trim()
|
|
||||||
if (identifier) void getLookup(identifier)
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function LookupPage() {
|
|
||||||
const [params] = useSearchParams()
|
|
||||||
const identifier = () => String(params.identifier ?? "").trim()
|
|
||||||
const data = createAsync(() => (identifier() ? getLookup(identifier()) : Promise.resolve(undefined)))
|
|
||||||
|
|
||||||
return (
|
|
||||||
<main data-page="support">
|
|
||||||
<Title>opencode support — {identifier() || "lookup"}</Title>
|
|
||||||
<h1>Lookup: {identifier() || "(no identifier)"}</h1>
|
|
||||||
|
|
||||||
<Show when={identifier()} fallback={<div data-empty>Provide an `identifier` query parameter.</div>}>
|
|
||||||
<ErrorBoundary fallback={(err) => <div data-component="error">{(err as Error).message}</div>}>
|
|
||||||
<Show when={data()} fallback={<div data-empty>Loading...</div>}>
|
|
||||||
{(result) => <Result data={result()} />}
|
|
||||||
</Show>
|
|
||||||
</ErrorBoundary>
|
|
||||||
</Show>
|
|
||||||
</main>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
-10
@@ -1,10 +0,0 @@
|
|||||||
/* This file is auto-generated by SST. Do not edit. */
|
|
||||||
/* tslint:disable */
|
|
||||||
/* eslint-disable */
|
|
||||||
/* deno-fmt-ignore-file */
|
|
||||||
/* biome-ignore-all lint: auto-generated */
|
|
||||||
|
|
||||||
/// <reference path="../../../sst-env.d.ts" />
|
|
||||||
|
|
||||||
import "sst"
|
|
||||||
export {}
|
|
||||||
@@ -1,21 +0,0 @@
|
|||||||
{
|
|
||||||
"$schema": "https://json.schemastore.org/tsconfig",
|
|
||||||
"compilerOptions": {
|
|
||||||
"target": "ESNext",
|
|
||||||
"module": "ESNext",
|
|
||||||
"skipLibCheck": true,
|
|
||||||
"moduleResolution": "bundler",
|
|
||||||
"allowSyntheticDefaultImports": true,
|
|
||||||
"esModuleInterop": true,
|
|
||||||
"jsx": "preserve",
|
|
||||||
"jsxImportSource": "solid-js",
|
|
||||||
"allowJs": true,
|
|
||||||
"strict": true,
|
|
||||||
"noEmit": true,
|
|
||||||
"types": ["vite/client", "bun"],
|
|
||||||
"isolatedModules": true,
|
|
||||||
"paths": {
|
|
||||||
"~/*": ["./src/*"]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,25 +0,0 @@
|
|||||||
import { defineConfig, PluginOption } from "vite"
|
|
||||||
import { solidStart } from "@solidjs/start/config"
|
|
||||||
import { nitro } from "nitro/vite"
|
|
||||||
|
|
||||||
export default defineConfig({
|
|
||||||
plugins: [
|
|
||||||
solidStart() as PluginOption,
|
|
||||||
nitro({
|
|
||||||
compatibilityDate: "2024-09-19",
|
|
||||||
preset: "cloudflare_module",
|
|
||||||
cloudflare: {
|
|
||||||
nodeCompat: true,
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
],
|
|
||||||
server: {
|
|
||||||
allowedHosts: true,
|
|
||||||
},
|
|
||||||
build: {
|
|
||||||
rollupOptions: {
|
|
||||||
external: ["cloudflare:workers"],
|
|
||||||
},
|
|
||||||
minify: false,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"$schema": "https://json.schemastore.org/package.json",
|
"$schema": "https://json.schemastore.org/package.json",
|
||||||
"version": "1.15.11",
|
"version": "1.15.10",
|
||||||
"name": "@opencode-ai/core",
|
"name": "@opencode-ai/core",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
@@ -34,8 +34,8 @@
|
|||||||
"@ai-sdk/cohere": "3.0.27",
|
"@ai-sdk/cohere": "3.0.27",
|
||||||
"@ai-sdk/deepinfra": "2.0.41",
|
"@ai-sdk/deepinfra": "2.0.41",
|
||||||
"@ai-sdk/gateway": "3.0.104",
|
"@ai-sdk/gateway": "3.0.104",
|
||||||
"@ai-sdk/google": "3.0.73",
|
"@ai-sdk/google": "3.0.75",
|
||||||
"@ai-sdk/google-vertex": "4.0.128",
|
"@ai-sdk/google-vertex": "4.0.131",
|
||||||
"@ai-sdk/groq": "3.0.31",
|
"@ai-sdk/groq": "3.0.31",
|
||||||
"@ai-sdk/mistral": "3.0.27",
|
"@ai-sdk/mistral": "3.0.27",
|
||||||
"@ai-sdk/openai": "3.0.53",
|
"@ai-sdk/openai": "3.0.53",
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user