Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ce6c114d99 |
@@ -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.9",
|
||||||
"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.9",
|
||||||
"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.9",
|
||||||
"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.9",
|
||||||
"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.9",
|
||||||
"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,43 +191,23 @@
|
|||||||
"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.9",
|
||||||
"bin": {
|
"bin": {
|
||||||
"opencode": "./bin/opencode",
|
"opencode": "./bin/opencode",
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@ai-sdk/alibaba": "1.0.17",
|
"@ai-sdk/alibaba": "1.0.17",
|
||||||
"@ai-sdk/amazon-bedrock": "4.0.107",
|
"@ai-sdk/amazon-bedrock": "4.0.96",
|
||||||
"@ai-sdk/anthropic": "3.0.71",
|
"@ai-sdk/anthropic": "3.0.71",
|
||||||
"@ai-sdk/azure": "3.0.49",
|
"@ai-sdk/azure": "3.0.49",
|
||||||
"@ai-sdk/cerebras": "2.0.41",
|
"@ai-sdk/cerebras": "2.0.41",
|
||||||
"@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.63",
|
||||||
"@ai-sdk/google-vertex": "4.0.128",
|
"@ai-sdk/google-vertex": "4.0.112",
|
||||||
"@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",
|
||||||
@@ -260,7 +239,7 @@
|
|||||||
"minimatch": "10.2.5",
|
"minimatch": "10.2.5",
|
||||||
"npm-package-arg": "13.0.2",
|
"npm-package-arg": "13.0.2",
|
||||||
"semver": "^7.6.3",
|
"semver": "^7.6.3",
|
||||||
"venice-ai-sdk-provider": "2.0.2",
|
"venice-ai-sdk-provider": "2.0.1",
|
||||||
"xdg-basedir": "5.1.0",
|
"xdg-basedir": "5.1.0",
|
||||||
"zod": "catalog:",
|
"zod": "catalog:",
|
||||||
},
|
},
|
||||||
@@ -275,7 +254,7 @@
|
|||||||
},
|
},
|
||||||
"packages/desktop": {
|
"packages/desktop": {
|
||||||
"name": "@opencode-ai/desktop",
|
"name": "@opencode-ai/desktop",
|
||||||
"version": "1.15.11",
|
"version": "1.15.9",
|
||||||
"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.9",
|
||||||
"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.9",
|
||||||
"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.9",
|
||||||
"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.9",
|
||||||
"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.9",
|
||||||
"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.9",
|
||||||
"bin": {
|
"bin": {
|
||||||
"opencode": "./bin/opencode",
|
"opencode": "./bin/opencode",
|
||||||
},
|
},
|
||||||
@@ -430,15 +409,15 @@
|
|||||||
"@actions/github": "6.0.1",
|
"@actions/github": "6.0.1",
|
||||||
"@agentclientprotocol/sdk": "0.21.0",
|
"@agentclientprotocol/sdk": "0.21.0",
|
||||||
"@ai-sdk/alibaba": "1.0.17",
|
"@ai-sdk/alibaba": "1.0.17",
|
||||||
"@ai-sdk/amazon-bedrock": "4.0.107",
|
"@ai-sdk/amazon-bedrock": "4.0.96",
|
||||||
"@ai-sdk/anthropic": "3.0.71",
|
"@ai-sdk/anthropic": "3.0.71",
|
||||||
"@ai-sdk/azure": "3.0.49",
|
"@ai-sdk/azure": "3.0.49",
|
||||||
"@ai-sdk/cerebras": "2.0.41",
|
"@ai-sdk/cerebras": "2.0.41",
|
||||||
"@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.63",
|
||||||
"@ai-sdk/google-vertex": "4.0.128",
|
"@ai-sdk/google-vertex": "4.0.112",
|
||||||
"@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",
|
||||||
@@ -516,11 +494,10 @@
|
|||||||
"tree-sitter-powershell": "0.25.10",
|
"tree-sitter-powershell": "0.25.10",
|
||||||
"turndown": "7.2.0",
|
"turndown": "7.2.0",
|
||||||
"ulid": "catalog:",
|
"ulid": "catalog:",
|
||||||
"venice-ai-sdk-provider": "2.0.2",
|
"venice-ai-sdk-provider": "2.0.1",
|
||||||
"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.9",
|
||||||
"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.9",
|
||||||
"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.9",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@opencode-ai/sdk": "workspace:*",
|
"@opencode-ai/sdk": "workspace:*",
|
||||||
"@slack/bolt": "^3.17.1",
|
"@slack/bolt": "^3.17.1",
|
||||||
@@ -625,66 +602,6 @@
|
|||||||
"typescript": "catalog:",
|
"typescript": "catalog:",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
"packages/stats/app": {
|
|
||||||
"name": "@opencode-ai/stats-app",
|
|
||||||
"version": "1.15.11",
|
|
||||||
"dependencies": {
|
|
||||||
"@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": {
|
||||||
@@ -709,7 +626,7 @@
|
|||||||
},
|
},
|
||||||
"packages/ui": {
|
"packages/ui": {
|
||||||
"name": "@opencode-ai/ui",
|
"name": "@opencode-ai/ui",
|
||||||
"version": "1.15.11",
|
"version": "1.15.9",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@kobalte/core": "catalog:",
|
"@kobalte/core": "catalog:",
|
||||||
"@opencode-ai/core": "workspace:*",
|
"@opencode-ai/core": "workspace:*",
|
||||||
@@ -758,7 +675,7 @@
|
|||||||
},
|
},
|
||||||
"packages/web": {
|
"packages/web": {
|
||||||
"name": "@opencode-ai/web",
|
"name": "@opencode-ai/web",
|
||||||
"version": "1.15.11",
|
"version": "1.15.9",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@astrojs/cloudflare": "12.6.3",
|
"@astrojs/cloudflare": "12.6.3",
|
||||||
"@astrojs/markdown-remark": "6.3.1",
|
"@astrojs/markdown-remark": "6.3.1",
|
||||||
@@ -801,8 +718,6 @@
|
|||||||
],
|
],
|
||||||
"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",
|
||||||
"@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",
|
||||||
@@ -826,9 +741,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",
|
||||||
@@ -866,7 +781,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",
|
||||||
@@ -896,7 +810,7 @@
|
|||||||
|
|
||||||
"@ai-sdk/alibaba": ["@ai-sdk/alibaba@1.0.17", "", { "dependencies": { "@ai-sdk/openai-compatible": "2.0.41", "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.23" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-ZbE+U5bWz2JBc5DERLowx5+TKbjGBE93LqKZAWvuEn7HOSQMraxFMZuc0ST335QZJAyfBOzh7m1mPQ+y7EaaoA=="],
|
"@ai-sdk/alibaba": ["@ai-sdk/alibaba@1.0.17", "", { "dependencies": { "@ai-sdk/openai-compatible": "2.0.41", "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.23" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-ZbE+U5bWz2JBc5DERLowx5+TKbjGBE93LqKZAWvuEn7HOSQMraxFMZuc0ST335QZJAyfBOzh7m1mPQ+y7EaaoA=="],
|
||||||
|
|
||||||
"@ai-sdk/amazon-bedrock": ["@ai-sdk/amazon-bedrock@4.0.107", "", { "dependencies": { "@ai-sdk/anthropic": "3.0.78", "@ai-sdk/provider": "3.0.10", "@ai-sdk/provider-utils": "4.0.27", "@smithy/eventstream-codec": "^4.0.1", "@smithy/util-utf8": "^4.0.0", "aws4fetch": "^1.0.20" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-8nT08pGPy25rleJNk56ep00UHK6kCtCmu+ZNqVVSSPDieADlIZqcaN1iRXAFBoCH0Fb9F6C2EjFDaySdsargfQ=="],
|
"@ai-sdk/amazon-bedrock": ["@ai-sdk/amazon-bedrock@4.0.96", "", { "dependencies": { "@ai-sdk/anthropic": "3.0.71", "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.23", "@smithy/eventstream-codec": "^4.0.1", "@smithy/util-utf8": "^4.0.0", "aws4fetch": "^1.0.20" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-Mc4Ias2jRMD1jOB6xWtKNPdhECeuCZyIlbr9EAGfBnyBt++sS13ziZh9qv9TdyMCAZJ7xoQcpbchoRJcKwPdpA=="],
|
||||||
|
|
||||||
"@ai-sdk/anthropic": ["@ai-sdk/anthropic@3.0.64", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.21" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-rwLi/Rsuj2pYniQXIrvClHvXDzgM4UQHHnvHTWEF14efnlKclG/1ghpNC+adsRujAbCTr6gRsSbDE2vEqriV7g=="],
|
"@ai-sdk/anthropic": ["@ai-sdk/anthropic@3.0.64", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.21" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-rwLi/Rsuj2pYniQXIrvClHvXDzgM4UQHHnvHTWEF14efnlKclG/1ghpNC+adsRujAbCTr6gRsSbDE2vEqriV7g=="],
|
||||||
|
|
||||||
@@ -918,9 +832,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.63", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.23" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-RfOZWVMYSPu2sPRfGajrauWAZ9BSaRopSn+AszkKWQ1MFj8nhaXvCqRHB5pBQUaHTfZKagvOmMpNfa/s3gPLgQ=="],
|
||||||
|
|
||||||
"@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.112", "", { "dependencies": { "@ai-sdk/anthropic": "3.0.71", "@ai-sdk/google": "3.0.64", "@ai-sdk/openai-compatible": "2.0.41", "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.23", "google-auth-library": "^10.5.0" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-cSfHCkM+9ZrFtQWIN1WlV93JPD+isGSdFxKj7u1L9m2aLVZajlXdcE41GL9hMt7ld7bZYE4NnZ+4VLxBAHE+Eg=="],
|
||||||
|
|
||||||
"@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=="],
|
||||||
|
|
||||||
@@ -992,12 +906,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=="],
|
||||||
@@ -1654,8 +1564,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"],
|
||||||
@@ -1678,12 +1586,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"],
|
||||||
@@ -1718,23 +1620,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=="],
|
||||||
|
|
||||||
@@ -2422,10 +2324,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=="],
|
||||||
@@ -2570,8 +2468,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=="],
|
||||||
@@ -2998,20 +2894,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=="],
|
||||||
@@ -3586,8 +3468,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=="],
|
||||||
@@ -5032,7 +4912,7 @@
|
|||||||
|
|
||||||
"vary": ["vary@1.1.2", "", {}, "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg=="],
|
"vary": ["vary@1.1.2", "", {}, "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg=="],
|
||||||
|
|
||||||
"venice-ai-sdk-provider": ["venice-ai-sdk-provider@2.0.2", "", { "dependencies": { "@ai-sdk/openai-compatible": "^2.0.47", "@ai-sdk/provider": "^3.0.10", "@ai-sdk/provider-utils": "^4.0.27" }, "peerDependencies": { "ai": "^6.0.90" } }, "sha512-aoa05nI3BTK5aGbjBflq+Gfln2AHAkwNbWuGGvCzUIsOfp5Y3iPD4O4PUGDAEiWVJWbjpPn0KfDa0H/HebwsaA=="],
|
"venice-ai-sdk-provider": ["venice-ai-sdk-provider@2.0.1", "", { "dependencies": { "@ai-sdk/openai-compatible": "^2.0.37", "@ai-sdk/provider": "^3.0.8", "@ai-sdk/provider-utils": "^4.0.21" }, "peerDependencies": { "ai": "^6.0.90" } }, "sha512-6SxA8a4MoA6Q/c+D3q7My0Hfog76enN3n0MXhwosM+tso66rXBEGeBRD/0lravRDVzL2Q1w5QJPc86rAVJtfXg=="],
|
||||||
|
|
||||||
"verror": ["verror@1.10.1", "", { "dependencies": { "assert-plus": "^1.0.0", "core-util-is": "1.0.2", "extsprintf": "^1.2.0" } }, "sha512-veufcmxri4e3XSrT0xwfUR7kguIkaxBeosDg00yDWhk49wdwkSUrvvsm7nc75e1PUyvIeZj6nS8VQRYz2/S4Xg=="],
|
"verror": ["verror@1.10.1", "", { "dependencies": { "assert-plus": "^1.0.0", "core-util-is": "1.0.2", "extsprintf": "^1.2.0" } }, "sha512-veufcmxri4e3XSrT0xwfUR7kguIkaxBeosDg00yDWhk49wdwkSUrvvsm7nc75e1PUyvIeZj6nS8VQRYz2/S4Xg=="],
|
||||||
|
|
||||||
@@ -5140,7 +5020,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=="],
|
||||||
|
|
||||||
@@ -5210,13 +5090,9 @@
|
|||||||
|
|
||||||
"@ai-sdk/alibaba/@ai-sdk/openai-compatible": ["@ai-sdk/openai-compatible@2.0.41", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.23" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-kNAGINk71AlOXx10Dq/PXw4t/9XjdK8uxfpVElRwtSFMdeSiLVt58p9TPx4/FJD+hxZuVhvxYj9r42osxWq79g=="],
|
"@ai-sdk/alibaba/@ai-sdk/openai-compatible": ["@ai-sdk/openai-compatible@2.0.41", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.23" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-kNAGINk71AlOXx10Dq/PXw4t/9XjdK8uxfpVElRwtSFMdeSiLVt58p9TPx4/FJD+hxZuVhvxYj9r42osxWq79g=="],
|
||||||
|
|
||||||
"@ai-sdk/amazon-bedrock/@ai-sdk/anthropic": ["@ai-sdk/anthropic@3.0.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/amazon-bedrock/@ai-sdk/anthropic": ["@ai-sdk/anthropic@3.0.71", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.23" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-bUWOzrzR0gJKJO/PLGMR4uH2dqEgqGhrsCV+sSpk4KtOEnUQlfjZI/F7BFlqSvVpFbjdgYRRLysAeEZpJ6S1lg=="],
|
||||||
|
|
||||||
"@ai-sdk/amazon-bedrock/@ai-sdk/provider": ["@ai-sdk/provider@3.0.10", "", { "dependencies": { "json-schema": "^0.4.0" } }, "sha512-Q3BZ27qfpYqnCYGvE3vt+Qi6LGOF9R5Nmzn+9JoM1lCRsD9mYaIhfJLkSunN48nfGXJ6n+XNV0J/XVpqGQl7Dw=="],
|
"@ai-sdk/amazon-bedrock/@smithy/eventstream-codec": ["@smithy/eventstream-codec@4.2.13", "", { "dependencies": { "@aws-crypto/crc32": "5.2.0", "@smithy/types": "^4.14.0", "@smithy/util-hex-encoding": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-vYahwBAtRaAcFbOmE9aLr12z7RiHYDSLcnogSdxfm7kKfsNa3wH+NU5r7vTeB5rKvLsWyPjVX8iH94brP7umiQ=="],
|
||||||
|
|
||||||
"@ai-sdk/amazon-bedrock/@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/amazon-bedrock/@smithy/eventstream-codec": ["@smithy/eventstream-codec@4.2.14", "", { "dependencies": { "@aws-crypto/crc32": "5.2.0", "@smithy/types": "^4.14.1", "@smithy/util-hex-encoding": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-erZq0nOIpzfeZdCyzZjdJb4nVSKLUmSkaQUVkRGQTXs30gyUGeKnrYEg+Xe1W5gE3aReS7IgsvANwVPxSzY6Pw=="],
|
|
||||||
|
|
||||||
"@ai-sdk/amazon-bedrock/@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=="],
|
"@ai-sdk/amazon-bedrock/@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=="],
|
||||||
|
|
||||||
@@ -5232,17 +5108,11 @@
|
|||||||
|
|
||||||
"@ai-sdk/fireworks/@ai-sdk/openai-compatible": ["@ai-sdk/openai-compatible@2.0.41", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.23" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-kNAGINk71AlOXx10Dq/PXw4t/9XjdK8uxfpVElRwtSFMdeSiLVt58p9TPx4/FJD+hxZuVhvxYj9r42osxWq79g=="],
|
"@ai-sdk/fireworks/@ai-sdk/openai-compatible": ["@ai-sdk/openai-compatible@2.0.41", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.23" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-kNAGINk71AlOXx10Dq/PXw4t/9XjdK8uxfpVElRwtSFMdeSiLVt58p9TPx4/FJD+hxZuVhvxYj9r42osxWq79g=="],
|
||||||
|
|
||||||
"@ai-sdk/google/@ai-sdk/provider": ["@ai-sdk/provider@3.0.10", "", { "dependencies": { "json-schema": "^0.4.0" } }, "sha512-Q3BZ27qfpYqnCYGvE3vt+Qi6LGOF9R5Nmzn+9JoM1lCRsD9mYaIhfJLkSunN48nfGXJ6n+XNV0J/XVpqGQl7Dw=="],
|
"@ai-sdk/google-vertex/@ai-sdk/anthropic": ["@ai-sdk/anthropic@3.0.71", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.23" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-bUWOzrzR0gJKJO/PLGMR4uH2dqEgqGhrsCV+sSpk4KtOEnUQlfjZI/F7BFlqSvVpFbjdgYRRLysAeEZpJ6S1lg=="],
|
||||||
|
|
||||||
"@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/google": ["@ai-sdk/google@3.0.64", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.23" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-CbR82EgGPNrj/6q0HtclwuCqe0/pDShyv3nWDP/A9DroujzWXnLMlUJVrgPOsg4b40zQCwwVs2XSKCxvt/4QaA=="],
|
||||||
|
|
||||||
"@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/openai-compatible": ["@ai-sdk/openai-compatible@2.0.41", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.23" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-kNAGINk71AlOXx10Dq/PXw4t/9XjdK8uxfpVElRwtSFMdeSiLVt58p9TPx4/FJD+hxZuVhvxYj9r42osxWq79g=="],
|
||||||
|
|
||||||
"@ai-sdk/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/provider": ["@ai-sdk/provider@3.0.10", "", { "dependencies": { "json-schema": "^0.4.0" } }, "sha512-Q3BZ27qfpYqnCYGvE3vt+Qi6LGOF9R5Nmzn+9JoM1lCRsD9mYaIhfJLkSunN48nfGXJ6n+XNV0J/XVpqGQl7Dw=="],
|
|
||||||
|
|
||||||
"@ai-sdk/google-vertex/@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/groq/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@4.0.21", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@standard-schema/spec": "^1.1.0", "eventsource-parser": "^3.0.6" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-MtFUYI1/8mgDvRmaBDjbLJPFFrMG777AvSgyIFQtZHIMzm88R/12vYBBpnk7pfiWLFE1DSZzY4WDYzGbKAcmiw=="],
|
"@ai-sdk/groq/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@4.0.21", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@standard-schema/spec": "^1.1.0", "eventsource-parser": "^3.0.6" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-MtFUYI1/8mgDvRmaBDjbLJPFFrMG777AvSgyIFQtZHIMzm88R/12vYBBpnk7pfiWLFE1DSZzY4WDYzGbKAcmiw=="],
|
||||||
|
|
||||||
@@ -5296,16 +5166,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=="],
|
||||||
@@ -5330,16 +5190,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=="],
|
||||||
@@ -5498,8 +5348,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=="],
|
||||||
@@ -5962,8 +5810,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=="],
|
||||||
@@ -6080,8 +5926,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=="],
|
||||||
@@ -6112,11 +5956,7 @@
|
|||||||
|
|
||||||
"unused-filename/path-exists": ["path-exists@5.0.0", "", {}, "sha512-RjhtfwJOxzcFmNOi6ltcbcu4Iu+FL3zEj83dk4kAS+fVpTxXLO1b38RvJgT/0QwvV/L3aY9TAnyv0EOqW4GoMQ=="],
|
"unused-filename/path-exists": ["path-exists@5.0.0", "", {}, "sha512-RjhtfwJOxzcFmNOi6ltcbcu4Iu+FL3zEj83dk4kAS+fVpTxXLO1b38RvJgT/0QwvV/L3aY9TAnyv0EOqW4GoMQ=="],
|
||||||
|
|
||||||
"venice-ai-sdk-provider/@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=="],
|
"venice-ai-sdk-provider/@ai-sdk/openai-compatible": ["@ai-sdk/openai-compatible@2.0.41", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.23" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-kNAGINk71AlOXx10Dq/PXw4t/9XjdK8uxfpVElRwtSFMdeSiLVt58p9TPx4/FJD+hxZuVhvxYj9r42osxWq79g=="],
|
||||||
|
|
||||||
"venice-ai-sdk-provider/@ai-sdk/provider": ["@ai-sdk/provider@3.0.10", "", { "dependencies": { "json-schema": "^0.4.0" } }, "sha512-Q3BZ27qfpYqnCYGvE3vt+Qi6LGOF9R5Nmzn+9JoM1lCRsD9mYaIhfJLkSunN48nfGXJ6n+XNV0J/XVpqGQl7Dw=="],
|
|
||||||
|
|
||||||
"venice-ai-sdk-provider/@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=="],
|
|
||||||
|
|
||||||
"vite-plugin-icons-spritesheet/glob": ["glob@11.1.0", "", { "dependencies": { "foreground-child": "^3.3.1", "jackspeak": "^4.1.1", "minimatch": "^10.1.1", "minipass": "^7.1.2", "package-json-from-dist": "^1.0.0", "path-scurry": "^2.0.0" }, "bin": { "glob": "dist/esm/bin.mjs" } }, "sha512-vuNwKSaKiqm7g0THUBu2x7ckSs3XJLXE+2ssL7/MfTGPLLcrJQ/4Uq1CjPTtO5cCIiRxqvN6Twy1qOwhL0Xjcw=="],
|
"vite-plugin-icons-spritesheet/glob": ["glob@11.1.0", "", { "dependencies": { "foreground-child": "^3.3.1", "jackspeak": "^4.1.1", "minimatch": "^10.1.1", "minipass": "^7.1.2", "package-json-from-dist": "^1.0.0", "path-scurry": "^2.0.0" }, "bin": { "glob": "dist/esm/bin.mjs" } }, "sha512-vuNwKSaKiqm7g0THUBu2x7ckSs3XJLXE+2ssL7/MfTGPLLcrJQ/4Uq1CjPTtO5cCIiRxqvN6Twy1qOwhL0Xjcw=="],
|
||||||
|
|
||||||
@@ -6162,12 +6002,6 @@
|
|||||||
|
|
||||||
"@actions/github/@octokit/plugin-rest-endpoint-methods/@octokit/types": ["@octokit/types@12.6.0", "", { "dependencies": { "@octokit/openapi-types": "^20.0.0" } }, "sha512-1rhSOfRa6H9w4YwK0yrf5faDaDTb+yLyBUKOCV4xtCDB5VmIPqd/v9yr9o6SAzOAlRxMiRiCic6JVM1/kunVkw=="],
|
"@actions/github/@octokit/plugin-rest-endpoint-methods/@octokit/types": ["@octokit/types@12.6.0", "", { "dependencies": { "@octokit/openapi-types": "^20.0.0" } }, "sha512-1rhSOfRa6H9w4YwK0yrf5faDaDTb+yLyBUKOCV4xtCDB5VmIPqd/v9yr9o6SAzOAlRxMiRiCic6JVM1/kunVkw=="],
|
||||||
|
|
||||||
"@ai-sdk/amazon-bedrock/@ai-sdk/provider-utils/@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="],
|
|
||||||
|
|
||||||
"@ai-sdk/amazon-bedrock/@ai-sdk/provider-utils/eventsource-parser": ["eventsource-parser@3.0.8", "", {}, "sha512-70QWGkr4snxr0OXLRWsFLeRBIRPuQOvt4s8QYjmUlmlkyTZkRqS7EDVRZtzU3TiyDbXSzaOeF0XUKy8PchzukQ=="],
|
|
||||||
|
|
||||||
"@ai-sdk/amazon-bedrock/@smithy/eventstream-codec/@smithy/types": ["@smithy/types@4.14.1", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-59b5HtSVrVR/eYNei3BUj3DCPKD/G7EtDDe7OEJE7i7FtQFugYo6MxbotS8mVJkLNVf8gYaAlEBwwtJ9HzhWSg=="],
|
|
||||||
|
|
||||||
"@ai-sdk/anthropic/@ai-sdk/provider-utils/@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="],
|
"@ai-sdk/anthropic/@ai-sdk/provider-utils/@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="],
|
||||||
|
|
||||||
"@ai-sdk/azure/@ai-sdk/provider-utils/@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="],
|
"@ai-sdk/azure/@ai-sdk/provider-utils/@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="],
|
||||||
@@ -6178,14 +6012,6 @@
|
|||||||
|
|
||||||
"@ai-sdk/deepinfra/@ai-sdk/provider-utils/@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="],
|
"@ai-sdk/deepinfra/@ai-sdk/provider-utils/@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="],
|
||||||
|
|
||||||
"@ai-sdk/google-vertex/@ai-sdk/provider-utils/@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="],
|
|
||||||
|
|
||||||
"@ai-sdk/google-vertex/@ai-sdk/provider-utils/eventsource-parser": ["eventsource-parser@3.0.8", "", {}, "sha512-70QWGkr4snxr0OXLRWsFLeRBIRPuQOvt4s8QYjmUlmlkyTZkRqS7EDVRZtzU3TiyDbXSzaOeF0XUKy8PchzukQ=="],
|
|
||||||
|
|
||||||
"@ai-sdk/google/@ai-sdk/provider-utils/@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="],
|
|
||||||
|
|
||||||
"@ai-sdk/google/@ai-sdk/provider-utils/eventsource-parser": ["eventsource-parser@3.0.8", "", {}, "sha512-70QWGkr4snxr0OXLRWsFLeRBIRPuQOvt4s8QYjmUlmlkyTZkRqS7EDVRZtzU3TiyDbXSzaOeF0XUKy8PchzukQ=="],
|
|
||||||
|
|
||||||
"@ai-sdk/groq/@ai-sdk/provider-utils/@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="],
|
"@ai-sdk/groq/@ai-sdk/provider-utils/@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="],
|
||||||
|
|
||||||
"@ai-sdk/mistral/@ai-sdk/provider-utils/@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="],
|
"@ai-sdk/mistral/@ai-sdk/provider-utils/@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="],
|
||||||
@@ -6832,10 +6658,6 @@
|
|||||||
|
|
||||||
"unplugin/chokidar/readdirp": ["readdirp@3.6.0", "", { "dependencies": { "picomatch": "^2.2.1" } }, "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA=="],
|
"unplugin/chokidar/readdirp": ["readdirp@3.6.0", "", { "dependencies": { "picomatch": "^2.2.1" } }, "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA=="],
|
||||||
|
|
||||||
"venice-ai-sdk-provider/@ai-sdk/provider-utils/@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="],
|
|
||||||
|
|
||||||
"venice-ai-sdk-provider/@ai-sdk/provider-utils/eventsource-parser": ["eventsource-parser@3.0.8", "", {}, "sha512-70QWGkr4snxr0OXLRWsFLeRBIRPuQOvt4s8QYjmUlmlkyTZkRqS7EDVRZtzU3TiyDbXSzaOeF0XUKy8PchzukQ=="],
|
|
||||||
|
|
||||||
"vitest/@vitest/expect/@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="],
|
"vitest/@vitest/expect/@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="],
|
||||||
|
|
||||||
"vitest/@vitest/expect/chai": ["chai@6.2.2", "", {}, "sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg=="],
|
"vitest/@vitest/expect/chai": ["chai@6.2.2", "", {}, "sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg=="],
|
||||||
|
|||||||
+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,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
+1
-10
@@ -3,7 +3,6 @@
|
|||||||
stdenv,
|
stdenv,
|
||||||
bun,
|
bun,
|
||||||
nodejs,
|
nodejs,
|
||||||
darwin,
|
|
||||||
electron_41,
|
electron_41,
|
||||||
makeWrapper,
|
makeWrapper,
|
||||||
writableTmpDirAsHomeHook,
|
writableTmpDirAsHomeHook,
|
||||||
@@ -15,12 +14,7 @@ let
|
|||||||
in
|
in
|
||||||
stdenv.mkDerivation (finalAttrs: {
|
stdenv.mkDerivation (finalAttrs: {
|
||||||
pname = "opencode-desktop";
|
pname = "opencode-desktop";
|
||||||
inherit (opencode)
|
inherit (opencode) version src node_modules;
|
||||||
version
|
|
||||||
src
|
|
||||||
node_modules
|
|
||||||
patches
|
|
||||||
;
|
|
||||||
|
|
||||||
nativeBuildInputs = [
|
nativeBuildInputs = [
|
||||||
bun
|
bun
|
||||||
@@ -29,9 +23,6 @@ stdenv.mkDerivation (finalAttrs: {
|
|||||||
writableTmpDirAsHomeHook
|
writableTmpDirAsHomeHook
|
||||||
] ++ lib.optionals stdenv.hostPlatform.isLinux [
|
] ++ lib.optionals stdenv.hostPlatform.isLinux [
|
||||||
autoPatchelfHook
|
autoPatchelfHook
|
||||||
] ++ lib.optionals stdenv.hostPlatform.isDarwin [
|
|
||||||
# Ad-hoc sign the .app: --config.mac.identity=null below skips signing.
|
|
||||||
darwin.autoSignDarwinBinariesHook
|
|
||||||
];
|
];
|
||||||
|
|
||||||
buildInputs = lib.optionals stdenv.hostPlatform.isLinux [
|
buildInputs = lib.optionals stdenv.hostPlatform.isLinux [
|
||||||
|
|||||||
+4
-4
@@ -1,8 +1,8 @@
|
|||||||
{
|
{
|
||||||
"nodeModules": {
|
"nodeModules": {
|
||||||
"x86_64-linux": "sha256-KFVkxs0jtN1EXvwuxFMOlWg6BDr8L6K/wJZ/N73p8cI=",
|
"x86_64-linux": "sha256-1RiaZQHzIhdtcOJUMsLagpP+nBBL/Qu6zQgrAXMHDCI=",
|
||||||
"aarch64-linux": "sha256-7OLglRBBt5prwb9e0I0iMTXi3MrDfDI+8xcDrxtq/JI=",
|
"aarch64-linux": "sha256-5QZhtkWuNpY/qUxlKRHcGbILOAVnuyzu+h8VDIuMQcU=",
|
||||||
"aarch64-darwin": "sha256-Q7iE6xJyomDnvqUgsHh+FlCnJFI6vK0zJSfI9BBYitQ=",
|
"aarch64-darwin": "sha256-9w3QA22XNc1itGLyhikYU90xGH/iLUUM+SSGXla7lhw=",
|
||||||
"x86_64-darwin": "sha256-9lVsq2Bw74Vgphg6GSo/HxCtDXUyabxcS/vE1Um/Hfw="
|
"x86_64-darwin": "sha256-cSYiyhhSqIYiTeK1uWHDkHbYstQYD5jEB7JaYjWjgi4="
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+6
-11
@@ -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=dev -- 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": {
|
||||||
@@ -143,8 +140,6 @@
|
|||||||
"@silvia-odwyer/photon-node@0.3.4": "patches/@silvia-odwyer%2Fphoton-node@0.3.4.patch",
|
"@silvia-odwyer/photon-node@0.3.4": "patches/@silvia-odwyer%2Fphoton-node@0.3.4.patch",
|
||||||
"@standard-community/standard-openapi@0.2.9": "patches/@standard-community%2Fstandard-openapi@0.2.9.patch",
|
"@standard-community/standard-openapi@0.2.9": "patches/@standard-community%2Fstandard-openapi@0.2.9.patch",
|
||||||
"solid-js@1.9.10": "patches/solid-js@1.9.10.patch",
|
"solid-js@1.9.10": "patches/solid-js@1.9.10.patch",
|
||||||
"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,352 +0,0 @@
|
|||||||
import { expect, test, type Locator, type Page } from "@playwright/test"
|
|
||||||
import { mockOpenCodeServer } from "../utils/mock-server"
|
|
||||||
|
|
||||||
const directory = "C:/OpenCode/TimelineStateRegression"
|
|
||||||
const projectID = "proj_timeline_state_regression"
|
|
||||||
const sessionID = "ses_timeline_state_regression"
|
|
||||||
const userMessageID = "msg_user_regression"
|
|
||||||
const assistantMessageID = "msg_assistant_regression"
|
|
||||||
const editPartID = "prt_0001_edit"
|
|
||||||
const textPartID = "prt_9999_text"
|
|
||||||
const title = "Timeline collapse state regression"
|
|
||||||
const model = { providerID: "opencode", modelID: "claude-opus-4-6", variant: "max" }
|
|
||||||
|
|
||||||
type EventPayload = {
|
|
||||||
directory: string
|
|
||||||
payload: Record<string, unknown>
|
|
||||||
}
|
|
||||||
|
|
||||||
declare global {
|
|
||||||
interface Window {
|
|
||||||
__timelineDiffProbe: {
|
|
||||||
reset: () => void
|
|
||||||
shadowRoots: () => number
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const userMessage = {
|
|
||||||
info: {
|
|
||||||
id: userMessageID,
|
|
||||||
sessionID,
|
|
||||||
role: "user",
|
|
||||||
time: { created: 1700000000000 },
|
|
||||||
summary: { diffs: [] },
|
|
||||||
agent: "build",
|
|
||||||
model,
|
|
||||||
},
|
|
||||||
parts: [
|
|
||||||
{
|
|
||||||
id: "prt_user_text",
|
|
||||||
sessionID,
|
|
||||||
messageID: userMessageID,
|
|
||||||
type: "text",
|
|
||||||
text: "Please edit the file.",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
}
|
|
||||||
|
|
||||||
const editPart = {
|
|
||||||
id: editPartID,
|
|
||||||
sessionID,
|
|
||||||
messageID: assistantMessageID,
|
|
||||||
type: "tool",
|
|
||||||
callID: "call_edit_regression",
|
|
||||||
tool: "edit",
|
|
||||||
state: {
|
|
||||||
status: "completed",
|
|
||||||
input: { filePath: "src/regression.ts" },
|
|
||||||
output: "Edited src/regression.ts",
|
|
||||||
title: "src/regression.ts",
|
|
||||||
metadata: {
|
|
||||||
filediff: {
|
|
||||||
file: "src/regression.ts",
|
|
||||||
additions: 1,
|
|
||||||
deletions: 1,
|
|
||||||
before: "export const value = 'before'\n",
|
|
||||||
after: "export const value = 'after'\n",
|
|
||||||
},
|
|
||||||
diff: "diff --git a/src/regression.ts b/src/regression.ts\n-export const value = 'before'\n+export const value = 'after'\n",
|
|
||||||
},
|
|
||||||
time: { start: 1700000001000, end: 1700000002000 },
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
const streamedTextPart = {
|
|
||||||
id: textPartID,
|
|
||||||
sessionID,
|
|
||||||
messageID: assistantMessageID,
|
|
||||||
type: "text",
|
|
||||||
text: "Streaming added a later assistant text part.",
|
|
||||||
}
|
|
||||||
|
|
||||||
const assistantMessage = {
|
|
||||||
info: {
|
|
||||||
id: assistantMessageID,
|
|
||||||
sessionID,
|
|
||||||
role: "assistant",
|
|
||||||
time: { created: 1700000001000 },
|
|
||||||
parentID: userMessageID,
|
|
||||||
modelID: model.modelID,
|
|
||||||
providerID: model.providerID,
|
|
||||||
mode: "build",
|
|
||||||
agent: "build",
|
|
||||||
path: { cwd: directory, root: directory },
|
|
||||||
cost: 0.01,
|
|
||||||
tokens: { input: 100, output: 200, reasoning: 0, cache: { read: 0, write: 0 } },
|
|
||||||
variant: "max",
|
|
||||||
},
|
|
||||||
parts: [editPart],
|
|
||||||
}
|
|
||||||
|
|
||||||
test.describe("regression: session timeline local row state", () => {
|
|
||||||
test("keeps a manually collapsed tool collapsed when later assistant content streams", async ({ page }) => {
|
|
||||||
const events: EventPayload[] = []
|
|
||||||
await mockServer(page, events)
|
|
||||||
await configurePage(page)
|
|
||||||
|
|
||||||
await page.goto(`/${base64Encode(directory)}/session/${sessionID}`)
|
|
||||||
await expect(page.getByRole("heading", { name: title })).toBeVisible()
|
|
||||||
|
|
||||||
const wrapper = page.locator(`[data-timeline-part-id="${editPartID}"]`).first()
|
|
||||||
await expect(wrapper).toBeVisible()
|
|
||||||
await expectExpanded(wrapper, true)
|
|
||||||
|
|
||||||
await wrapper.evaluate((element) => {
|
|
||||||
;(element as HTMLElement).dataset.regressionMarker = "before-stream"
|
|
||||||
})
|
|
||||||
await wrapper.locator('[data-slot="collapsible-trigger"]').first().click()
|
|
||||||
await expectExpanded(wrapper, false)
|
|
||||||
|
|
||||||
events.push({
|
|
||||||
directory,
|
|
||||||
payload: {
|
|
||||||
type: "message.part.updated",
|
|
||||||
properties: { part: streamedTextPart },
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
await expect(page.locator(`[data-timeline-part-id="${textPartID}"]`).first()).toBeVisible({ timeout: 10_000 })
|
|
||||||
|
|
||||||
expect(await readToolState(page)).toEqual({
|
|
||||||
expanded: false,
|
|
||||||
row: "AssistantPart",
|
|
||||||
streamedTextVisible: true,
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
test("does not remount an edit diff when sibling parts or diff counts update", async ({ page }) => {
|
|
||||||
const events: EventPayload[] = []
|
|
||||||
await installDiffProbe(page)
|
|
||||||
await mockServer(page, events)
|
|
||||||
await configurePage(page)
|
|
||||||
|
|
||||||
await page.goto(`/${base64Encode(directory)}/session/${sessionID}`)
|
|
||||||
await expect(page.getByRole("heading", { name: title })).toBeVisible()
|
|
||||||
|
|
||||||
const wrapper = page.locator(`[data-timeline-part-id="${editPartID}"]`).first()
|
|
||||||
await expect(wrapper).toBeVisible()
|
|
||||||
await expect(wrapper.locator('[data-component="file"][data-mode="diff"]').first()).toBeVisible()
|
|
||||||
await markDiffProbe(page)
|
|
||||||
|
|
||||||
events.push({
|
|
||||||
directory,
|
|
||||||
payload: {
|
|
||||||
type: "message.part.updated",
|
|
||||||
properties: { part: streamedTextPart },
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
await expect(page.locator(`[data-timeline-part-id="${textPartID}"]`).first()).toBeVisible({ timeout: 10_000 })
|
|
||||||
expect(await readDiffProbe(page)).toEqual({ fileMarker: "before", shadowRoots: 0, toolMarker: "before" })
|
|
||||||
|
|
||||||
await markDiffProbe(page)
|
|
||||||
events.push({
|
|
||||||
directory,
|
|
||||||
payload: {
|
|
||||||
type: "message.part.updated",
|
|
||||||
properties: { part: editPartWithAdditions(2) },
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
await expect(wrapper.locator('[data-slot="diff-changes-additions"]').filter({ hasText: "+2" }).first()).toBeVisible(
|
|
||||||
{ timeout: 10_000 },
|
|
||||||
)
|
|
||||||
expect(await readDiffProbe(page)).toEqual({ fileMarker: "before", shadowRoots: 0, toolMarker: "before" })
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
async function configurePage(page: Page) {
|
|
||||||
await page.addInitScript(() => {
|
|
||||||
localStorage.setItem(
|
|
||||||
"settings.v3",
|
|
||||||
JSON.stringify({
|
|
||||||
general: {
|
|
||||||
editToolPartsExpanded: true,
|
|
||||||
shellToolPartsExpanded: true,
|
|
||||||
showReasoningSummaries: true,
|
|
||||||
showSessionProgressBar: true,
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
async function expectExpanded(locator: Locator, expected: boolean) {
|
|
||||||
await expect.poll(() => locator.evaluate(readExpanded)).toBe(expected)
|
|
||||||
}
|
|
||||||
|
|
||||||
async function readToolState(page: Page) {
|
|
||||||
return page
|
|
||||||
.locator(`[data-timeline-part-id="${editPartID}"]`)
|
|
||||||
.first()
|
|
||||||
.evaluate(
|
|
||||||
(element, textPartID) => ({
|
|
||||||
expanded: (() => {
|
|
||||||
const trigger = element.querySelector('[data-slot="collapsible-trigger"]')
|
|
||||||
const aria = trigger?.getAttribute("aria-expanded")
|
|
||||||
if (aria === "true") return true
|
|
||||||
if (aria === "false") return false
|
|
||||||
|
|
||||||
const root = element.querySelector('[data-component="collapsible"]')
|
|
||||||
if (root?.hasAttribute("data-expanded")) return true
|
|
||||||
if (root?.hasAttribute("data-closed")) return false
|
|
||||||
|
|
||||||
const content = element.querySelector<HTMLElement>('[data-slot="collapsible-content"]')
|
|
||||||
return !!content && content.getBoundingClientRect().height > 0
|
|
||||||
})(),
|
|
||||||
row: element.closest("[data-timeline-row]")?.getAttribute("data-timeline-row"),
|
|
||||||
streamedTextVisible: !!document.querySelector(`[data-timeline-part-id="${textPartID}"]`),
|
|
||||||
}),
|
|
||||||
textPartID,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
async function installDiffProbe(page: Page) {
|
|
||||||
await page.addInitScript(() => {
|
|
||||||
let shadowRootCount = 0
|
|
||||||
const attachShadow = Element.prototype.attachShadow
|
|
||||||
Element.prototype.attachShadow = function (init) {
|
|
||||||
shadowRootCount += 1
|
|
||||||
return attachShadow.call(this, init)
|
|
||||||
}
|
|
||||||
window.__timelineDiffProbe = {
|
|
||||||
reset: () => {
|
|
||||||
shadowRootCount = 0
|
|
||||||
},
|
|
||||||
shadowRoots: () => shadowRootCount,
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
async function markDiffProbe(page: Page) {
|
|
||||||
await page
|
|
||||||
.locator(`[data-timeline-part-id="${editPartID}"]`)
|
|
||||||
.first()
|
|
||||||
.evaluate((element) => {
|
|
||||||
const tool = element as HTMLElement
|
|
||||||
const file = tool.querySelector<HTMLElement>('[data-component="file"][data-mode="diff"]')
|
|
||||||
if (!file) throw new Error("missing edit diff file")
|
|
||||||
|
|
||||||
tool.dataset.timelineProbe = "before"
|
|
||||||
file.dataset.timelineProbe = "before"
|
|
||||||
window.__timelineDiffProbe.reset()
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
async function readDiffProbe(page: Page) {
|
|
||||||
return page
|
|
||||||
.locator(`[data-timeline-part-id="${editPartID}"]`)
|
|
||||||
.first()
|
|
||||||
.evaluate((element) => {
|
|
||||||
const tool = element as HTMLElement
|
|
||||||
const file = tool.querySelector<HTMLElement>('[data-component="file"][data-mode="diff"]')
|
|
||||||
return {
|
|
||||||
fileMarker: file?.dataset.timelineProbe,
|
|
||||||
shadowRoots: window.__timelineDiffProbe.shadowRoots(),
|
|
||||||
toolMarker: tool.dataset.timelineProbe,
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
function editPartWithAdditions(additions: number) {
|
|
||||||
return {
|
|
||||||
...editPart,
|
|
||||||
state: {
|
|
||||||
...editPart.state,
|
|
||||||
metadata: {
|
|
||||||
...editPart.state.metadata,
|
|
||||||
filediff: {
|
|
||||||
...editPart.state.metadata.filediff,
|
|
||||||
additions,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function readExpanded(element: Element) {
|
|
||||||
const trigger = element.querySelector('[data-slot="collapsible-trigger"]')
|
|
||||||
const aria = trigger?.getAttribute("aria-expanded")
|
|
||||||
if (aria === "true") return true
|
|
||||||
if (aria === "false") return false
|
|
||||||
|
|
||||||
const root = element.querySelector('[data-component="collapsible"]')
|
|
||||||
if (root?.hasAttribute("data-expanded")) return true
|
|
||||||
if (root?.hasAttribute("data-closed")) return false
|
|
||||||
|
|
||||||
const content = element.querySelector<HTMLElement>('[data-slot="collapsible-content"]')
|
|
||||||
return !!content && content.getBoundingClientRect().height > 0
|
|
||||||
}
|
|
||||||
|
|
||||||
async function mockServer(page: Page, events: EventPayload[]) {
|
|
||||||
await mockOpenCodeServer(page, {
|
|
||||||
directory,
|
|
||||||
project: project(),
|
|
||||||
provider: provider(),
|
|
||||||
sessions: [session()],
|
|
||||||
pageMessages: () => ({ items: [userMessage, assistantMessage] }),
|
|
||||||
events: () => events.splice(0),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
function project() {
|
|
||||||
return {
|
|
||||||
id: projectID,
|
|
||||||
worktree: directory,
|
|
||||||
vcs: "git",
|
|
||||||
name: "timeline-state-regression",
|
|
||||||
time: { created: 1700000000000, updated: 1700000000000 },
|
|
||||||
sandboxes: [],
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function session() {
|
|
||||||
return {
|
|
||||||
id: sessionID,
|
|
||||||
slug: "timeline-state-regression",
|
|
||||||
projectID,
|
|
||||||
directory,
|
|
||||||
title,
|
|
||||||
version: "dev",
|
|
||||||
time: { created: 1700000000000, updated: 1700000000000 },
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function provider() {
|
|
||||||
return {
|
|
||||||
all: [
|
|
||||||
{
|
|
||||||
id: "opencode",
|
|
||||||
name: "OpenCode",
|
|
||||||
models: { "claude-opus-4-6": { id: "claude-opus-4-6", name: "Claude Opus 4.6", limit: { context: 200_000 } } },
|
|
||||||
},
|
|
||||||
],
|
|
||||||
connected: ["opencode"],
|
|
||||||
default: { providerID: "opencode", modelID: "claude-opus-4-6" },
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function base64Encode(value: string) {
|
|
||||||
return Buffer.from(value, "utf8").toString("base64").replace(/\+/g, "-").replace(/\//g, "_").replace(/=/g, "")
|
|
||||||
}
|
|
||||||
@@ -1,267 +0,0 @@
|
|||||||
import { expect, test, type Page } from "@playwright/test"
|
|
||||||
import { mockOpenCodeServer } from "../utils/mock-server"
|
|
||||||
|
|
||||||
const directory = "C:/OpenCode/ContextResizeRegression"
|
|
||||||
const projectID = "proj_context_resize_regression"
|
|
||||||
const sessionID = "ses_context_resize_regression"
|
|
||||||
const title = "Context resize regression"
|
|
||||||
const model = { providerID: "opencode", modelID: "claude-opus-4-6", variant: "max" }
|
|
||||||
const contextIDs = ["prt_0100_read", "prt_0101_glob", "prt_0102_grep", "prt_0103_list"]
|
|
||||||
const followingTextID = "prt_0104_text"
|
|
||||||
|
|
||||||
type Message = {
|
|
||||||
info: Record<string, unknown> & { id: string; role: "user" | "assistant" }
|
|
||||||
parts: Record<string, unknown>[]
|
|
||||||
}
|
|
||||||
|
|
||||||
const messages = [...Array.from({ length: 8 }, (_, index) => turn(index, false)).flat(), ...turn(10, true)]
|
|
||||||
|
|
||||||
test.describe("regression: session timeline context group resize", () => {
|
|
||||||
test("remeasures a recent explored context group before the next paint", async ({ page }) => {
|
|
||||||
await page.setViewportSize({ width: 1400, height: 900 })
|
|
||||||
await mockServer(page)
|
|
||||||
await configurePage(page)
|
|
||||||
|
|
||||||
await page.goto(`/${base64Encode(directory)}/session/${sessionID}`)
|
|
||||||
await expect(page.getByRole("heading", { name: title })).toBeVisible()
|
|
||||||
await expect(page.locator(`[data-timeline-part-ids="${contextIDs.join(",")}"]`).first()).toBeVisible()
|
|
||||||
await expect(page.locator(`[data-timeline-part-id="${followingTextID}"]`).first()).toBeVisible()
|
|
||||||
await settle(page)
|
|
||||||
|
|
||||||
const samples = await sampleExpansion(page)
|
|
||||||
const visibleOverlap = samples.filter((sample) => sample.frame >= 1 && sample.overlap > 0.5)
|
|
||||||
|
|
||||||
console.log("context resize samples", JSON.stringify(samples, null, 2))
|
|
||||||
|
|
||||||
expect(samples[0]?.overlap).toBe(0)
|
|
||||||
expect(visibleOverlap).toEqual([])
|
|
||||||
expect(samples.at(-1)?.expanded).toBe("true")
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
async function configurePage(page: Page) {
|
|
||||||
await page.addInitScript(() => {
|
|
||||||
localStorage.setItem(
|
|
||||||
"settings.v3",
|
|
||||||
JSON.stringify({
|
|
||||||
general: {
|
|
||||||
editToolPartsExpanded: true,
|
|
||||||
shellToolPartsExpanded: true,
|
|
||||||
showReasoningSummaries: true,
|
|
||||||
showSessionProgressBar: true,
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
async function sampleExpansion(page: Page) {
|
|
||||||
return page.evaluate(
|
|
||||||
({ contextIDs, followingTextID }) =>
|
|
||||||
new Promise<
|
|
||||||
{
|
|
||||||
frame: number
|
|
||||||
label: string
|
|
||||||
scrollTop: number
|
|
||||||
scrollHeight: number
|
|
||||||
contextBottom: number
|
|
||||||
textTop: number
|
|
||||||
overlap: number
|
|
||||||
gap: number
|
|
||||||
expanded: string | null
|
|
||||||
}[]
|
|
||||||
>((resolve) => {
|
|
||||||
const context = document.querySelector<HTMLElement>(`[data-timeline-part-ids="${contextIDs.join(",")}"]`)
|
|
||||||
const text = document.querySelector<HTMLElement>(`[data-timeline-part-id="${followingTextID}"]`)
|
|
||||||
const scroller = context?.closest<HTMLElement>(".scroll-view__viewport")
|
|
||||||
const trigger = context?.querySelector<HTMLElement>('[data-slot="collapsible-trigger"]')
|
|
||||||
const contextRow = context?.closest<HTMLElement>('[data-timeline-row="AssistantPart"]')
|
|
||||||
const textRow = text?.closest<HTMLElement>('[data-timeline-row="AssistantPart"]')
|
|
||||||
if (!context || !text || !scroller || !trigger || !contextRow || !textRow)
|
|
||||||
throw new Error("missing regression nodes")
|
|
||||||
|
|
||||||
scroller.scrollTop = scroller.scrollHeight
|
|
||||||
const samples: {
|
|
||||||
frame: number
|
|
||||||
label: string
|
|
||||||
scrollTop: number
|
|
||||||
scrollHeight: number
|
|
||||||
contextBottom: number
|
|
||||||
textTop: number
|
|
||||||
overlap: number
|
|
||||||
gap: number
|
|
||||||
expanded: string | null
|
|
||||||
}[] = []
|
|
||||||
const capture = (frame: number, label: string) => {
|
|
||||||
const contextRect = contextRow.getBoundingClientRect()
|
|
||||||
const textRect = textRow.getBoundingClientRect()
|
|
||||||
samples.push({
|
|
||||||
frame,
|
|
||||||
label,
|
|
||||||
scrollTop: Math.round(scroller.scrollTop * 10) / 10,
|
|
||||||
scrollHeight: Math.round(scroller.scrollHeight * 10) / 10,
|
|
||||||
contextBottom: Math.round(contextRect.bottom * 10) / 10,
|
|
||||||
textTop: Math.round(textRect.top * 10) / 10,
|
|
||||||
overlap: Math.max(0, Math.round((contextRect.bottom - textRect.top) * 10) / 10),
|
|
||||||
gap: Math.max(0, Math.round((textRect.top - contextRect.bottom) * 10) / 10),
|
|
||||||
expanded: trigger.getAttribute("aria-expanded"),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
capture(-1, "before")
|
|
||||||
trigger.click()
|
|
||||||
capture(0, "sync-after-click")
|
|
||||||
|
|
||||||
let frame = 1
|
|
||||||
const tick = () => {
|
|
||||||
capture(frame, "raf")
|
|
||||||
frame += 1
|
|
||||||
if (frame > 8) {
|
|
||||||
resolve(samples)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
requestAnimationFrame(tick)
|
|
||||||
}
|
|
||||||
requestAnimationFrame(tick)
|
|
||||||
}),
|
|
||||||
{ contextIDs, followingTextID },
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function turn(index: number, target: boolean): Message[] {
|
|
||||||
const userID = id("msg_user", index)
|
|
||||||
const assistantID = id("msg_assistant", index)
|
|
||||||
return [
|
|
||||||
{
|
|
||||||
info: {
|
|
||||||
id: userID,
|
|
||||||
sessionID,
|
|
||||||
role: "user",
|
|
||||||
time: { created: 1700000000000 + index * 10_000 },
|
|
||||||
summary: { diffs: [] },
|
|
||||||
agent: "build",
|
|
||||||
model,
|
|
||||||
},
|
|
||||||
parts: [{ id: id("prt_user", index), sessionID, messageID: userID, type: "text", text: `User message ${index}` }],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
info: {
|
|
||||||
id: assistantID,
|
|
||||||
sessionID,
|
|
||||||
role: "assistant",
|
|
||||||
time: { created: 1700000000000 + index * 10_000 + 1_000, completed: 1700000000000 + index * 10_000 + 2_000 },
|
|
||||||
parentID: userID,
|
|
||||||
modelID: model.modelID,
|
|
||||||
providerID: model.providerID,
|
|
||||||
mode: "build",
|
|
||||||
agent: "build",
|
|
||||||
path: { cwd: directory, root: directory },
|
|
||||||
cost: 0.01,
|
|
||||||
tokens: { input: 100, output: 200, reasoning: 0, cache: { read: 0, write: 0 } },
|
|
||||||
variant: "max",
|
|
||||||
finish: "stop",
|
|
||||||
},
|
|
||||||
parts: target
|
|
||||||
? [
|
|
||||||
contextTool(contextIDs[0]!, assistantID, "read", { filePath: "src/recent-a.ts", offset: 0, limit: 120 }),
|
|
||||||
contextTool(contextIDs[1]!, assistantID, "glob", { path: directory, pattern: "**/*.ts" }),
|
|
||||||
contextTool(contextIDs[2]!, assistantID, "grep", { path: directory, pattern: "Explored", include: "*.ts" }),
|
|
||||||
contextTool(contextIDs[3]!, assistantID, "list", { path: "src" }),
|
|
||||||
{
|
|
||||||
id: followingTextID,
|
|
||||||
sessionID,
|
|
||||||
messageID: assistantID,
|
|
||||||
type: "text",
|
|
||||||
text: "This assistant text is immediately after the explored context group.",
|
|
||||||
},
|
|
||||||
]
|
|
||||||
: [
|
|
||||||
{
|
|
||||||
id: id("prt_text", index),
|
|
||||||
sessionID,
|
|
||||||
messageID: assistantID,
|
|
||||||
type: "text",
|
|
||||||
text: `Assistant filler ${index}. ${"filler ".repeat(60)}`,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
]
|
|
||||||
}
|
|
||||||
|
|
||||||
function contextTool(partID: string, messageID: string, tool: string, input: Record<string, unknown>) {
|
|
||||||
return {
|
|
||||||
id: partID,
|
|
||||||
sessionID,
|
|
||||||
messageID,
|
|
||||||
type: "tool",
|
|
||||||
callID: `call_${partID}`,
|
|
||||||
tool,
|
|
||||||
state: {
|
|
||||||
status: "completed",
|
|
||||||
input,
|
|
||||||
output: `Completed ${tool}.\n${"detail line\n".repeat(8)}`,
|
|
||||||
title: input.filePath || input.path || input.pattern || "completed",
|
|
||||||
metadata: {},
|
|
||||||
time: { start: 1700000000000, end: 1700000000100 },
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function mockServer(page: Page) {
|
|
||||||
await mockOpenCodeServer(page, {
|
|
||||||
directory,
|
|
||||||
project: project(),
|
|
||||||
provider: provider(),
|
|
||||||
sessions: [session()],
|
|
||||||
pageMessages: () => ({ items: messages }),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
async function settle(page: Page) {
|
|
||||||
await page.evaluate(() => new Promise((resolve) => requestAnimationFrame(() => requestAnimationFrame(resolve))))
|
|
||||||
}
|
|
||||||
|
|
||||||
function id(prefix: string, index: number) {
|
|
||||||
return `${prefix}_${String(index).padStart(4, "0")}`
|
|
||||||
}
|
|
||||||
|
|
||||||
function project() {
|
|
||||||
return {
|
|
||||||
id: projectID,
|
|
||||||
worktree: directory,
|
|
||||||
vcs: "git",
|
|
||||||
name: "context-resize-regression",
|
|
||||||
time: { created: 1700000000000, updated: 1700000000000 },
|
|
||||||
sandboxes: [],
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function session() {
|
|
||||||
return {
|
|
||||||
id: sessionID,
|
|
||||||
slug: "context-resize-regression",
|
|
||||||
projectID,
|
|
||||||
directory,
|
|
||||||
title,
|
|
||||||
version: "dev",
|
|
||||||
time: { created: 1700000000000, updated: 1700000000000 },
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function provider() {
|
|
||||||
return {
|
|
||||||
all: [
|
|
||||||
{
|
|
||||||
id: "opencode",
|
|
||||||
name: "OpenCode",
|
|
||||||
models: { "claude-opus-4-6": { id: "claude-opus-4-6", name: "Claude Opus 4.6", limit: { context: 200_000 } } },
|
|
||||||
},
|
|
||||||
],
|
|
||||||
connected: ["opencode"],
|
|
||||||
default: { providerID: "opencode", modelID: "claude-opus-4-6" },
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function base64Encode(value: string) {
|
|
||||||
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.9",
|
||||||
"description": "",
|
"description": "",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"exports": {
|
"exports": {
|
||||||
|
|||||||
Binary file not shown.
+28
-45
@@ -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,
|
||||||
@@ -31,8 +30,8 @@ import { Dynamic } from "solid-js/web"
|
|||||||
import { CommandProvider } from "@/context/command"
|
import { CommandProvider } from "@/context/command"
|
||||||
import { CommentsProvider } from "@/context/comments"
|
import { CommentsProvider } from "@/context/comments"
|
||||||
import { FileProvider } from "@/context/file"
|
import { FileProvider } from "@/context/file"
|
||||||
import { ServerSDKProvider } from "@/context/server-sdk"
|
import { GlobalSDKProvider } from "@/context/global-sdk"
|
||||||
import { ServerSyncProvider } from "@/context/server-sync"
|
import { GlobalSyncProvider } from "@/context/global-sync"
|
||||||
import { HighlightsProvider } from "@/context/highlights"
|
import { HighlightsProvider } from "@/context/highlights"
|
||||||
import { LanguageProvider, type Locale, useLanguage } from "@/context/language"
|
import { LanguageProvider, type Locale, useLanguage } from "@/context/language"
|
||||||
import { LayoutProvider } from "@/context/layout"
|
import { LayoutProvider } from "@/context/layout"
|
||||||
@@ -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,29 +296,31 @@ export function AppInterface(props: {
|
|||||||
disableHealthCheck?: boolean
|
disableHealthCheck?: boolean
|
||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
<ServerProvider defaultServer={props.defaultServer} servers={props.servers}>
|
<ServerProvider
|
||||||
<ServersProvider>
|
defaultServer={props.defaultServer}
|
||||||
<ConnectionGate disableHealthCheck={props.disableHealthCheck}>
|
disableHealthCheck={props.disableHealthCheck}
|
||||||
<ServerKey>
|
servers={props.servers}
|
||||||
<QueryProvider>
|
>
|
||||||
<ServerSDKProvider>
|
<ConnectionGate disableHealthCheck={props.disableHealthCheck}>
|
||||||
<ServerSyncProvider>
|
<ServerKey>
|
||||||
<Dynamic
|
<QueryProvider>
|
||||||
component={props.router ?? Router}
|
<GlobalSDKProvider>
|
||||||
root={(routerProps) => <RouterRoot appChildren={props.children}>{routerProps.children}</RouterRoot>}
|
<GlobalSyncProvider>
|
||||||
>
|
<Dynamic
|
||||||
<Route path="/" component={HomeRoute} />
|
component={props.router ?? Router}
|
||||||
<Route path="/:dir" component={DirectoryLayout}>
|
root={(routerProps) => <RouterRoot appChildren={props.children}>{routerProps.children}</RouterRoot>}
|
||||||
<Route path="/" component={() => <Navigate href="session" />} />
|
>
|
||||||
<Route path="/session/:id?" component={SessionRoute} />
|
<Route path="/" component={HomeRoute} />
|
||||||
</Route>
|
<Route path="/:dir" component={DirectoryLayout}>
|
||||||
</Dynamic>
|
<Route path="/" component={() => <Navigate href="session" />} />
|
||||||
</ServerSyncProvider>
|
<Route path="/session/:id?" component={SessionRoute} />
|
||||||
</ServerSDKProvider>
|
</Route>
|
||||||
</QueryProvider>
|
</Dynamic>
|
||||||
</ServerKey>
|
</GlobalSyncProvider>
|
||||||
</ConnectionGate>
|
</GlobalSDKProvider>
|
||||||
</ServersProvider>
|
</QueryProvider>
|
||||||
|
</ServerKey>
|
||||||
|
</ConnectionGate>
|
||||||
</ServerProvider>
|
</ServerProvider>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,15 +12,15 @@ import { showToast } from "@opencode-ai/ui/toast"
|
|||||||
import { createEffect, createMemo, createResource, Match, onCleanup, onMount, Switch } from "solid-js"
|
import { createEffect, createMemo, createResource, Match, onCleanup, onMount, Switch } from "solid-js"
|
||||||
import { createStore, produce } from "solid-js/store"
|
import { createStore, produce } from "solid-js/store"
|
||||||
import { Link } from "@/components/link"
|
import { Link } from "@/components/link"
|
||||||
import { useServerSDK } from "@/context/server-sdk"
|
import { useGlobalSDK } from "@/context/global-sdk"
|
||||||
import { useServerSync } from "@/context/server-sync"
|
import { useGlobalSync } from "@/context/global-sync"
|
||||||
import { useLanguage } from "@/context/language"
|
import { useLanguage } from "@/context/language"
|
||||||
import { useProviders } from "@/hooks/use-providers"
|
import { useProviders } from "@/hooks/use-providers"
|
||||||
|
|
||||||
export function DialogConnectProvider(props: { provider: string }) {
|
export function DialogConnectProvider(props: { provider: string }) {
|
||||||
const dialog = useDialog()
|
const dialog = useDialog()
|
||||||
const serverSync = useServerSync()
|
const globalSync = useGlobalSync()
|
||||||
const serverSDK = useServerSDK()
|
const globalSDK = useGlobalSDK()
|
||||||
const language = useLanguage()
|
const language = useLanguage()
|
||||||
const providers = useProviders()
|
const providers = useProviders()
|
||||||
|
|
||||||
@@ -41,7 +41,7 @@ export function DialogConnectProvider(props: { provider: string }) {
|
|||||||
})
|
})
|
||||||
|
|
||||||
const provider = createMemo(
|
const provider = createMemo(
|
||||||
() => providers.all().get(props.provider) ?? serverSync.data.provider.all.get(props.provider)!,
|
() => providers.all().get(props.provider) ?? globalSync.data.provider.all.get(props.provider)!,
|
||||||
)
|
)
|
||||||
const fallback = createMemo<ProviderAuthMethod[]>(() => [
|
const fallback = createMemo<ProviderAuthMethod[]>(() => [
|
||||||
{
|
{
|
||||||
@@ -52,16 +52,16 @@ export function DialogConnectProvider(props: { provider: string }) {
|
|||||||
const [auth] = createResource(
|
const [auth] = createResource(
|
||||||
() => props.provider,
|
() => props.provider,
|
||||||
async () => {
|
async () => {
|
||||||
const cached = serverSync.data.provider_auth[props.provider]
|
const cached = globalSync.data.provider_auth[props.provider]
|
||||||
if (cached) return cached
|
if (cached) return cached
|
||||||
const res = await serverSDK.client.provider.auth()
|
const res = await globalSDK.client.provider.auth()
|
||||||
if (!alive.value) return fallback()
|
if (!alive.value) return fallback()
|
||||||
serverSync.set("provider_auth", res.data ?? {})
|
globalSync.set("provider_auth", res.data ?? {})
|
||||||
return res.data?.[props.provider] ?? fallback()
|
return res.data?.[props.provider] ?? fallback()
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
const loading = createMemo(() => auth.loading && !serverSync.data.provider_auth[props.provider])
|
const loading = createMemo(() => auth.loading && !globalSync.data.provider_auth[props.provider])
|
||||||
const methods = createMemo(() => auth.latest ?? serverSync.data.provider_auth[props.provider] ?? fallback())
|
const methods = createMemo(() => auth.latest ?? globalSync.data.provider_auth[props.provider] ?? fallback())
|
||||||
const [store, setStore] = createStore({
|
const [store, setStore] = createStore({
|
||||||
methodIndex: undefined as undefined | number,
|
methodIndex: undefined as undefined | number,
|
||||||
authorization: undefined as undefined | ProviderAuthAuthorization,
|
authorization: undefined as undefined | ProviderAuthAuthorization,
|
||||||
@@ -158,7 +158,7 @@ export function DialogConnectProvider(props: { provider: string }) {
|
|||||||
}
|
}
|
||||||
dispatch({ type: "auth.pending" })
|
dispatch({ type: "auth.pending" })
|
||||||
const start = Date.now()
|
const start = Date.now()
|
||||||
await serverSDK.client.provider.oauth
|
await globalSDK.client.provider.oauth
|
||||||
.authorize(
|
.authorize(
|
||||||
{
|
{
|
||||||
providerID: props.provider,
|
providerID: props.provider,
|
||||||
@@ -330,7 +330,7 @@ export function DialogConnectProvider(props: { provider: string }) {
|
|||||||
})
|
})
|
||||||
|
|
||||||
async function complete() {
|
async function complete() {
|
||||||
await serverSDK.client.global.dispose()
|
await globalSDK.client.global.dispose()
|
||||||
dialog.close()
|
dialog.close()
|
||||||
showToast({
|
showToast({
|
||||||
variant: "success",
|
variant: "success",
|
||||||
@@ -407,7 +407,7 @@ export function DialogConnectProvider(props: { provider: string }) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
setFormStore("error", undefined)
|
setFormStore("error", undefined)
|
||||||
await serverSDK.client.auth.set({
|
await globalSDK.client.auth.set({
|
||||||
providerID: props.provider,
|
providerID: props.provider,
|
||||||
auth: {
|
auth: {
|
||||||
type: "api",
|
type: "api",
|
||||||
@@ -478,7 +478,7 @@ export function DialogConnectProvider(props: { provider: string }) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
setFormStore("error", undefined)
|
setFormStore("error", undefined)
|
||||||
const result = await serverSDK.client.provider.oauth
|
const result = await globalSDK.client.provider.oauth
|
||||||
.callback({
|
.callback({
|
||||||
providerID: props.provider,
|
providerID: props.provider,
|
||||||
method: store.methodIndex,
|
method: store.methodIndex,
|
||||||
@@ -531,7 +531,7 @@ export function DialogConnectProvider(props: { provider: string }) {
|
|||||||
|
|
||||||
onMount(() => {
|
onMount(() => {
|
||||||
void (async () => {
|
void (async () => {
|
||||||
const result = await serverSDK.client.provider.oauth
|
const result = await globalSDK.client.provider.oauth
|
||||||
.callback({
|
.callback({
|
||||||
providerID: props.provider,
|
providerID: props.provider,
|
||||||
method: store.methodIndex,
|
method: store.methodIndex,
|
||||||
|
|||||||
@@ -9,8 +9,8 @@ import { showToast } from "@opencode-ai/ui/toast"
|
|||||||
import { batch, For } from "solid-js"
|
import { batch, For } from "solid-js"
|
||||||
import { createStore, produce } from "solid-js/store"
|
import { createStore, produce } from "solid-js/store"
|
||||||
import { Link } from "@/components/link"
|
import { Link } from "@/components/link"
|
||||||
import { useServerSDK } from "@/context/server-sdk"
|
import { useGlobalSDK } from "@/context/global-sdk"
|
||||||
import { useServerSync } from "@/context/server-sync"
|
import { useGlobalSync } from "@/context/global-sync"
|
||||||
import { useLanguage } from "@/context/language"
|
import { useLanguage } from "@/context/language"
|
||||||
import { type FormState, headerRow, modelRow, validateCustomProvider } from "./dialog-custom-provider-form"
|
import { type FormState, headerRow, modelRow, validateCustomProvider } from "./dialog-custom-provider-form"
|
||||||
import { DialogSelectProvider } from "./dialog-select-provider"
|
import { DialogSelectProvider } from "./dialog-select-provider"
|
||||||
@@ -21,8 +21,8 @@ type Props = {
|
|||||||
|
|
||||||
export function DialogCustomProvider(props: Props) {
|
export function DialogCustomProvider(props: Props) {
|
||||||
const dialog = useDialog()
|
const dialog = useDialog()
|
||||||
const serverSync = useServerSync()
|
const globalSync = useGlobalSync()
|
||||||
const serverSDK = useServerSDK()
|
const globalSDK = useGlobalSDK()
|
||||||
const language = useLanguage()
|
const language = useLanguage()
|
||||||
|
|
||||||
const [form, setForm] = createStore<FormState>({
|
const [form, setForm] = createStore<FormState>({
|
||||||
@@ -105,8 +105,8 @@ export function DialogCustomProvider(props: Props) {
|
|||||||
const output = validateCustomProvider({
|
const output = validateCustomProvider({
|
||||||
form,
|
form,
|
||||||
t: language.t,
|
t: language.t,
|
||||||
disabledProviders: serverSync.data.config.disabled_providers ?? [],
|
disabledProviders: globalSync.data.config.disabled_providers ?? [],
|
||||||
existingProviderIDs: new Set(serverSync.data.provider.all.keys()),
|
existingProviderIDs: new Set(globalSync.data.provider.all.keys()),
|
||||||
})
|
})
|
||||||
batch(() => {
|
batch(() => {
|
||||||
setForm("err", output.err)
|
setForm("err", output.err)
|
||||||
@@ -118,11 +118,11 @@ export function DialogCustomProvider(props: Props) {
|
|||||||
|
|
||||||
const saveMutation = useMutation(() => ({
|
const saveMutation = useMutation(() => ({
|
||||||
mutationFn: async (result: NonNullable<ReturnType<typeof validate>>) => {
|
mutationFn: async (result: NonNullable<ReturnType<typeof validate>>) => {
|
||||||
const disabledProviders = serverSync.data.config.disabled_providers ?? []
|
const disabledProviders = globalSync.data.config.disabled_providers ?? []
|
||||||
const nextDisabled = disabledProviders.filter((id) => id !== result.providerID)
|
const nextDisabled = disabledProviders.filter((id) => id !== result.providerID)
|
||||||
|
|
||||||
if (result.key) {
|
if (result.key) {
|
||||||
await serverSDK.client.auth.set({
|
await globalSDK.client.auth.set({
|
||||||
providerID: result.providerID,
|
providerID: result.providerID,
|
||||||
auth: {
|
auth: {
|
||||||
type: "api",
|
type: "api",
|
||||||
@@ -131,7 +131,7 @@ export function DialogCustomProvider(props: Props) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
await serverSync.updateConfig({
|
await globalSync.updateConfig({
|
||||||
provider: { [result.providerID]: result.config },
|
provider: { [result.providerID]: result.config },
|
||||||
disabled_providers: nextDisabled,
|
disabled_providers: nextDisabled,
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -6,8 +6,8 @@ import { useMutation } from "@tanstack/solid-query"
|
|||||||
import { Icon } from "@opencode-ai/ui/icon"
|
import { Icon } from "@opencode-ai/ui/icon"
|
||||||
import { createMemo, For, Show } from "solid-js"
|
import { createMemo, For, Show } from "solid-js"
|
||||||
import { createStore } from "solid-js/store"
|
import { createStore } from "solid-js/store"
|
||||||
import { useServerSDK } from "@/context/server-sdk"
|
import { useGlobalSDK } from "@/context/global-sdk"
|
||||||
import { useServerSync } from "@/context/server-sync"
|
import { useGlobalSync } from "@/context/global-sync"
|
||||||
import { type LocalProject, getAvatarColors } from "@/context/layout"
|
import { type LocalProject, getAvatarColors } from "@/context/layout"
|
||||||
import { getFilename } from "@opencode-ai/core/util/path"
|
import { getFilename } from "@opencode-ai/core/util/path"
|
||||||
import { Avatar } from "@opencode-ai/ui/avatar"
|
import { Avatar } from "@opencode-ai/ui/avatar"
|
||||||
@@ -18,8 +18,8 @@ const AVATAR_COLOR_KEYS = ["pink", "mint", "orange", "purple", "cyan", "lime"] a
|
|||||||
|
|
||||||
export function DialogEditProject(props: { project: LocalProject }) {
|
export function DialogEditProject(props: { project: LocalProject }) {
|
||||||
const dialog = useDialog()
|
const dialog = useDialog()
|
||||||
const serverSDK = useServerSDK()
|
const globalSDK = useGlobalSDK()
|
||||||
const serverSync = useServerSync()
|
const globalSync = useGlobalSync()
|
||||||
const language = useLanguage()
|
const language = useLanguage()
|
||||||
|
|
||||||
const folderName = createMemo(() => getFilename(props.project.worktree))
|
const folderName = createMemo(() => getFilename(props.project.worktree))
|
||||||
@@ -78,19 +78,19 @@ export function DialogEditProject(props: { project: LocalProject }) {
|
|||||||
const start = store.startup.trim()
|
const start = store.startup.trim()
|
||||||
|
|
||||||
if (props.project.id && props.project.id !== "global") {
|
if (props.project.id && props.project.id !== "global") {
|
||||||
await serverSDK.client.project.update({
|
await globalSDK.client.project.update({
|
||||||
projectID: props.project.id,
|
projectID: props.project.id,
|
||||||
directory: props.project.worktree,
|
directory: props.project.worktree,
|
||||||
name,
|
name,
|
||||||
icon: { color: store.color || "", override: store.iconOverride || "" },
|
icon: { color: store.color || "", override: store.iconOverride || "" },
|
||||||
commands: { start },
|
commands: { start },
|
||||||
})
|
})
|
||||||
serverSync.project.icon(props.project.worktree, store.iconOverride || undefined)
|
globalSync.project.icon(props.project.worktree, store.iconOverride || undefined)
|
||||||
dialog.close()
|
dialog.close()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
serverSync.project.meta(props.project.worktree, {
|
globalSync.project.meta(props.project.worktree, {
|
||||||
name,
|
name,
|
||||||
icon: { color: store.color || undefined, override: store.iconOverride || undefined },
|
icon: { color: store.color || undefined, override: store.iconOverride || undefined },
|
||||||
commands: { start: start || undefined },
|
commands: { start: start || undefined },
|
||||||
|
|||||||
@@ -6,8 +6,8 @@ import type { ListRef } from "@opencode-ai/ui/list"
|
|||||||
import { getDirectory, getFilename } from "@opencode-ai/core/util/path"
|
import { getDirectory, getFilename } from "@opencode-ai/core/util/path"
|
||||||
import fuzzysort from "fuzzysort"
|
import fuzzysort from "fuzzysort"
|
||||||
import { createMemo, createResource, createSignal } from "solid-js"
|
import { createMemo, createResource, createSignal } from "solid-js"
|
||||||
import { useServerSDK } from "@/context/server-sdk"
|
import { useGlobalSDK } from "@/context/global-sdk"
|
||||||
import { useServerSync } from "@/context/server-sync"
|
import { useGlobalSync } from "@/context/global-sync"
|
||||||
import { useLayout } from "@/context/layout"
|
import { useLayout } from "@/context/layout"
|
||||||
import { useLanguage } from "@/context/language"
|
import { useLanguage } from "@/context/language"
|
||||||
|
|
||||||
@@ -128,7 +128,7 @@ function uniqueRows(rows: Row[]) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function useDirectorySearch(args: {
|
function useDirectorySearch(args: {
|
||||||
sdk: ReturnType<typeof useServerSDK>
|
sdk: ReturnType<typeof useGlobalSDK>
|
||||||
start: () => string | undefined
|
start: () => string | undefined
|
||||||
home: () => string
|
home: () => string
|
||||||
}) {
|
}) {
|
||||||
@@ -246,8 +246,8 @@ function useDirectorySearch(args: {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function DialogSelectDirectory(props: DialogSelectDirectoryProps) {
|
export function DialogSelectDirectory(props: DialogSelectDirectoryProps) {
|
||||||
const sync = useServerSync()
|
const sync = useGlobalSync()
|
||||||
const sdk = useServerSDK()
|
const sdk = useGlobalSDK()
|
||||||
const layout = useLayout()
|
const layout = useLayout()
|
||||||
const dialog = useDialog()
|
const dialog = useDialog()
|
||||||
const language = useLanguage()
|
const language = useLanguage()
|
||||||
|
|||||||
@@ -9,8 +9,8 @@ import { getDirectory, getFilename } from "@opencode-ai/core/util/path"
|
|||||||
import { useNavigate } from "@solidjs/router"
|
import { useNavigate } from "@solidjs/router"
|
||||||
import { createMemo, createSignal, Match, onCleanup, Show, Switch } from "solid-js"
|
import { createMemo, createSignal, Match, onCleanup, Show, Switch } from "solid-js"
|
||||||
import { formatKeybind, useCommand, type CommandOption } from "@/context/command"
|
import { formatKeybind, useCommand, type CommandOption } from "@/context/command"
|
||||||
import { useServerSDK } from "@/context/server-sdk"
|
import { useGlobalSDK } from "@/context/global-sdk"
|
||||||
import { useServerSync } from "@/context/server-sync"
|
import { useGlobalSync } from "@/context/global-sync"
|
||||||
import { useLayout } from "@/context/layout"
|
import { useLayout } from "@/context/layout"
|
||||||
import { useFile } from "@/context/file"
|
import { useFile } from "@/context/file"
|
||||||
import { useLanguage } from "@/context/language"
|
import { useLanguage } from "@/context/language"
|
||||||
@@ -175,7 +175,7 @@ function createFileEntries(props: {
|
|||||||
function createSessionEntries(props: {
|
function createSessionEntries(props: {
|
||||||
workspaces: () => string[]
|
workspaces: () => string[]
|
||||||
label: (directory: string) => string
|
label: (directory: string) => string
|
||||||
serverSDK: ReturnType<typeof useServerSDK>
|
globalSDK: ReturnType<typeof useGlobalSDK>
|
||||||
language: ReturnType<typeof useLanguage>
|
language: ReturnType<typeof useLanguage>
|
||||||
}) {
|
}) {
|
||||||
const state: {
|
const state: {
|
||||||
@@ -207,7 +207,7 @@ function createSessionEntries(props: {
|
|||||||
state.inflight = Promise.all(
|
state.inflight = Promise.all(
|
||||||
dirs.map((directory) => {
|
dirs.map((directory) => {
|
||||||
const description = props.label(directory)
|
const description = props.label(directory)
|
||||||
return props.serverSDK.client.session
|
return props.globalSDK.client.session
|
||||||
.list({ directory, roots: true })
|
.list({ directory, roots: true })
|
||||||
.then((x) =>
|
.then((x) =>
|
||||||
(x.data ?? [])
|
(x.data ?? [])
|
||||||
@@ -268,8 +268,8 @@ export function DialogSelectFile(props: { mode?: DialogSelectFileMode; onOpenFil
|
|||||||
const file = useFile()
|
const file = useFile()
|
||||||
const dialog = useDialog()
|
const dialog = useDialog()
|
||||||
const navigate = useNavigate()
|
const navigate = useNavigate()
|
||||||
const serverSDK = useServerSDK()
|
const globalSDK = useGlobalSDK()
|
||||||
const serverSync = useServerSync()
|
const globalSync = useGlobalSync()
|
||||||
const { params, tabs, view } = useSessionLayout()
|
const { params, tabs, view } = useSessionLayout()
|
||||||
const filesOnly = () => props.mode === "files"
|
const filesOnly = () => props.mode === "files"
|
||||||
const state = { cleanup: undefined as (() => void) | void, committed: false }
|
const state = { cleanup: undefined as (() => void) | void, committed: false }
|
||||||
@@ -292,21 +292,21 @@ export function DialogSelectFile(props: { mode?: DialogSelectFileMode; onOpenFil
|
|||||||
if (directory && !dirs.includes(directory)) return [...dirs, directory]
|
if (directory && !dirs.includes(directory)) return [...dirs, directory]
|
||||||
return dirs
|
return dirs
|
||||||
})
|
})
|
||||||
const homedir = createMemo(() => serverSync.data.path.home)
|
const homedir = createMemo(() => globalSync.data.path.home)
|
||||||
const label = (directory: string) => {
|
const label = (directory: string) => {
|
||||||
const current = project()
|
const current = project()
|
||||||
const kind =
|
const kind =
|
||||||
current && directory === current.worktree
|
current && directory === current.worktree
|
||||||
? language.t("workspace.type.local")
|
? language.t("workspace.type.local")
|
||||||
: language.t("workspace.type.sandbox")
|
: language.t("workspace.type.sandbox")
|
||||||
const [store] = serverSync.child(directory, { bootstrap: false })
|
const [store] = globalSync.child(directory, { bootstrap: false })
|
||||||
const home = homedir()
|
const home = homedir()
|
||||||
const path = home ? directory.replace(home, "~") : directory
|
const path = home ? directory.replace(home, "~") : directory
|
||||||
const name = store.vcs?.branch ?? getFilename(directory)
|
const name = store.vcs?.branch ?? getFilename(directory)
|
||||||
return `${kind} : ${name || path}`
|
return `${kind} : ${name || path}`
|
||||||
}
|
}
|
||||||
|
|
||||||
const { sessions } = createSessionEntries({ workspaces, label, serverSDK, language })
|
const { sessions } = createSessionEntries({ workspaces, label, globalSDK, language })
|
||||||
|
|
||||||
const items = async (text: string) => {
|
const items = async (text: string) => {
|
||||||
const query = text.trim()
|
const query = text.trim()
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import { Dialog } from "@opencode-ai/ui/dialog"
|
|||||||
import { List } from "@opencode-ai/ui/list"
|
import { List } from "@opencode-ai/ui/list"
|
||||||
import { Switch } from "@opencode-ai/ui/switch"
|
import { Switch } from "@opencode-ai/ui/switch"
|
||||||
import { useLanguage } from "@/context/language"
|
import { useLanguage } from "@/context/language"
|
||||||
import { useQueryOptions } from "@/context/server-sync"
|
import { useQueryOptions } from "@/context/global-sync"
|
||||||
import { pathKey } from "@/utils/path-key"
|
import { pathKey } from "@/utils/path-key"
|
||||||
|
|
||||||
const statusLabels = {
|
const statusLabels = {
|
||||||
|
|||||||
@@ -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"
|
||||||
@@ -74,16 +66,16 @@ import { PromptDragOverlay } from "./prompt-input/drag-overlay"
|
|||||||
import { promptPlaceholder } from "./prompt-input/placeholder"
|
import { promptPlaceholder } from "./prompt-input/placeholder"
|
||||||
import { ImagePreview } from "@opencode-ai/ui/image-preview"
|
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/global-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: () => {
|
>
|
||||||
void import("@/components/dialog-select-model-unpaid").then((x) => {
|
<Button
|
||||||
dialog.show(() => <x.DialogSelectModelUnpaid model={local.model} />)
|
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) => {
|
||||||
|
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 worktreeLabel = (value: string) => {
|
||||||
const selectProject = (worktree: string) => {
|
if (value === MAIN_WORKTREE) return MAIN_WORKTREE
|
||||||
setPicker({
|
if (value === CREATE_WORKTREE) return language.t("session.new.worktree.create")
|
||||||
projectOpen: false,
|
return getFilename(value)
|
||||||
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 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,146 +1408,155 @@ 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}
|
classList={{
|
||||||
classList={{
|
"group/prompt-input min-h-[96px] w-full rounded-xl bg-v2-background-bg-base shadow-[var(--v2-elevation-raised)]": true,
|
||||||
"group/prompt-input min-h-[96px] w-full rounded-xl bg-v2-background-bg-base shadow-[var(--v2-elevation-raised)]": true,
|
"border-icon-info-active border-dashed": store.draggingType !== null,
|
||||||
"border-icon-info-active border-dashed": store.draggingType !== null,
|
[props.class ?? ""]: !!props.class,
|
||||||
[props.class ?? ""]: !!props.class,
|
}}
|
||||||
|
>
|
||||||
|
<PromptDragOverlay
|
||||||
|
type={store.draggingType}
|
||||||
|
label={language.t(
|
||||||
|
store.draggingType === "@mention" ? "prompt.dropzone.file.label" : "prompt.dropzone.label",
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<PromptContextItems
|
||||||
|
items={contextItems()}
|
||||||
|
active={(item) => {
|
||||||
|
const active = comments.active()
|
||||||
|
return !!item.commentID && item.commentID === active?.id && item.path === active?.file
|
||||||
|
}}
|
||||||
|
openComment={openComment}
|
||||||
|
remove={(item) => {
|
||||||
|
if (item.commentID) comments.remove(item.path, item.commentID)
|
||||||
|
prompt.context.remove(item.key)
|
||||||
|
}}
|
||||||
|
t={(key) => language.t(key as Parameters<typeof language.t>[0])}
|
||||||
|
/>
|
||||||
|
<PromptImageAttachments
|
||||||
|
attachments={imageAttachments()}
|
||||||
|
onOpen={(attachment) =>
|
||||||
|
dialog.show(() => <ImagePreview src={attachment.dataUrl} alt={attachment.filename} />)
|
||||||
|
}
|
||||||
|
onRemove={removeAttachment}
|
||||||
|
removeLabel={language.t("prompt.attachment.remove")}
|
||||||
|
/>
|
||||||
|
<div
|
||||||
|
class="relative min-h-[52px]"
|
||||||
|
onMouseDown={(e) => {
|
||||||
|
const target = e.target
|
||||||
|
if (!(target instanceof HTMLElement)) return
|
||||||
|
if (target.closest('[data-action="prompt-attach"], [data-action="prompt-submit"]')) return
|
||||||
|
editorRef?.focus()
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<PromptDragOverlay
|
<div class="relative max-h-[180px] overflow-y-auto no-scrollbar" ref={(el) => (scrollRef = el)}>
|
||||||
type={store.draggingType}
|
<div
|
||||||
label={language.t(
|
data-component="prompt-input"
|
||||||
store.draggingType === "@mention" ? "prompt.dropzone.file.label" : "prompt.dropzone.label",
|
ref={(el) => {
|
||||||
)}
|
editorRef = el
|
||||||
/>
|
props.ref?.(el)
|
||||||
<PromptContextItems
|
}}
|
||||||
items={contextItems()}
|
role="textbox"
|
||||||
active={(item) => {
|
aria-multiline="true"
|
||||||
const active = comments.active()
|
aria-label={designPlaceholder()}
|
||||||
return !!item.commentID && item.commentID === active?.id && item.path === active?.file
|
contenteditable="true"
|
||||||
}}
|
autocapitalize={store.mode === "normal" ? "sentences" : "off"}
|
||||||
openComment={openComment}
|
autocorrect={store.mode === "normal" ? "on" : "off"}
|
||||||
remove={(item) => {
|
spellcheck={store.mode === "normal"}
|
||||||
if (item.commentID) comments.remove(item.path, item.commentID)
|
inputMode="text"
|
||||||
prompt.context.remove(item.key)
|
// @ts-expect-error
|
||||||
}}
|
autocomplete="off"
|
||||||
t={(key) => language.t(key as Parameters<typeof language.t>[0])}
|
onInput={handleInput}
|
||||||
/>
|
onPaste={handlePaste}
|
||||||
<PromptImageAttachments
|
onCompositionStart={handleCompositionStart}
|
||||||
attachments={imageAttachments()}
|
onCompositionEnd={handleCompositionEnd}
|
||||||
onOpen={(attachment) =>
|
onBlur={handleBlur}
|
||||||
dialog.show(() => <ImagePreview src={attachment.dataUrl} alt={attachment.filename} />)
|
onKeyDown={handleKeyDown}
|
||||||
}
|
classList={{
|
||||||
onRemove={removeAttachment}
|
"select-text": true,
|
||||||
removeLabel={language.t("prompt.attachment.remove")}
|
"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,
|
||||||
<div
|
"[&_[data-type=agent]]:text-syntax-type": true,
|
||||||
class="relative min-h-[52px]"
|
"font-mono!": store.mode === "shell",
|
||||||
onMouseDown={(e) => {
|
}}
|
||||||
const target = e.target
|
/>
|
||||||
if (!(target instanceof HTMLElement)) return
|
<div
|
||||||
if (target.closest('[data-action^="prompt-"]')) return
|
data-component={newSession() ? "session-new-design-text" : "session-composer-text"}
|
||||||
editorRef?.focus()
|
class="absolute top-0 inset-x-0 px-4 pt-4 pointer-events-none whitespace-nowrap truncate leading-5 text-[13px] font-[440] text-v2-text-text-faint [font-family:Inter,var(--font-family-sans)]"
|
||||||
}}
|
classList={{ "font-mono!": store.mode === "shell", hidden: prompt.dirty() }}
|
||||||
>
|
>
|
||||||
<div class="relative max-h-[180px] overflow-y-auto no-scrollbar" ref={(el) => (scrollRef = el)}>
|
{designPlaceholder()}
|
||||||
<div
|
|
||||||
data-component="prompt-input"
|
|
||||||
ref={(el) => {
|
|
||||||
editorRef = el
|
|
||||||
props.ref?.(el)
|
|
||||||
}}
|
|
||||||
role="textbox"
|
|
||||||
aria-multiline="true"
|
|
||||||
aria-label={designPlaceholder()}
|
|
||||||
contenteditable="true"
|
|
||||||
autocapitalize={store.mode === "normal" ? "sentences" : "off"}
|
|
||||||
autocorrect={store.mode === "normal" ? "on" : "off"}
|
|
||||||
spellcheck={store.mode === "normal"}
|
|
||||||
inputMode="text"
|
|
||||||
// @ts-expect-error
|
|
||||||
autocomplete="off"
|
|
||||||
onInput={handleInput}
|
|
||||||
onPaste={handlePaste}
|
|
||||||
onCompositionStart={handleCompositionStart}
|
|
||||||
onCompositionEnd={handleCompositionEnd}
|
|
||||||
onBlur={handleBlur}
|
|
||||||
onKeyDown={handleKeyDown}
|
|
||||||
classList={{
|
|
||||||
"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,
|
|
||||||
"[&_[data-type=file]]:text-syntax-property": true,
|
|
||||||
"[&_[data-type=agent]]:text-syntax-type": true,
|
|
||||||
"font-mono!": store.mode === "shell",
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<div
|
|
||||||
data-component={newSession() ? "session-new-design-text" : "session-composer-text"}
|
|
||||||
class="absolute top-0 inset-x-0 px-4 pt-4 pointer-events-none whitespace-nowrap truncate leading-5 text-[13px] font-[440] text-v2-text-text-faint [font-family:Inter,var(--font-family-sans)]"
|
|
||||||
classList={{ "font-mono!": store.mode === "shell", hidden: prompt.dirty() }}
|
|
||||||
>
|
|
||||||
{designPlaceholder()}
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex h-11 items-center px-2">
|
</div>
|
||||||
<div class="flex min-w-0 flex-1 items-center gap-0">
|
<div class="flex h-11 items-center px-2">
|
||||||
{fileAttachmentInput()}
|
<div class="flex min-w-0 flex-1 items-center gap-0">
|
||||||
<TooltipKeybind
|
{fileAttachmentInput()}
|
||||||
placement="top"
|
<TooltipKeybind
|
||||||
title={language.t("prompt.action.attachFile")}
|
placement="top"
|
||||||
keybind={command.keybind("file.attach")}
|
title={language.t("prompt.action.attachFile")}
|
||||||
>
|
keybind={command.keybind("file.attach")}
|
||||||
<IconButton
|
>
|
||||||
data-action="prompt-attach"
|
|
||||||
type="button"
|
|
||||||
icon="plus"
|
|
||||||
variant="ghost"
|
|
||||||
class="size-7 rounded-md p-[6px] text-v2-icon-icon-muted"
|
|
||||||
style={buttons()}
|
|
||||||
onClick={pick}
|
|
||||||
disabled={store.mode !== "normal"}
|
|
||||||
tabIndex={store.mode === "normal" ? undefined : -1}
|
|
||||||
aria-label={language.t("prompt.action.attachFile")}
|
|
||||||
/>
|
|
||||||
</TooltipKeybind>
|
|
||||||
<Show when={showAgentControl()}>
|
|
||||||
<ComposerAgentControl state={agentControlState()} />
|
|
||||||
</Show>
|
|
||||||
<Show when={newSession() && !selectedProject()}>
|
|
||||||
<ComposerPickerTrigger state={newProjectTriggerState()} />
|
|
||||||
</Show>
|
|
||||||
<ComposerModelControl state={modelControlState()} />
|
|
||||||
</div>
|
|
||||||
<Tooltip placement="top" inactive={!working() && blank()} value={tip()}>
|
|
||||||
<IconButton
|
<IconButton
|
||||||
data-action="prompt-submit"
|
data-action="prompt-attach"
|
||||||
type="submit"
|
type="button"
|
||||||
disabled={!working() && blank()}
|
icon="plus"
|
||||||
|
variant="ghost"
|
||||||
|
class="size-7 rounded-md p-[6px] text-v2-icon-icon-muted"
|
||||||
|
style={buttons()}
|
||||||
|
onClick={pick}
|
||||||
|
disabled={store.mode !== "normal"}
|
||||||
tabIndex={store.mode === "normal" ? undefined : -1}
|
tabIndex={store.mode === "normal" ? undefined : -1}
|
||||||
icon={stopping() ? "stop" : store.mode === "shell" ? "arrow-undo-down" : "arrow-up"}
|
aria-label={language.t("prompt.action.attachFile")}
|
||||||
variant="primary"
|
|
||||||
class="size-7 rounded-md p-[6px] text-v2-icon-icon-muted shadow-[var(--v2-elevation-button-contrast)] disabled:opacity-50"
|
|
||||||
style={{
|
|
||||||
"background-image":
|
|
||||||
"linear-gradient(180deg,var(--v2-alpha-light-20) 0%,var(--v2-alpha-light-0) 100%),linear-gradient(90deg,var(--v2-background-bg-contrast) 0%,var(--v2-background-bg-contrast) 100%)",
|
|
||||||
}}
|
|
||||||
aria-label={stopping() ? language.t("prompt.action.stop") : language.t("prompt.action.send")}
|
|
||||||
/>
|
/>
|
||||||
</Tooltip>
|
</TooltipKeybind>
|
||||||
|
<Show when={newSession()}>
|
||||||
|
<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>
|
||||||
|
{modelControl()}
|
||||||
</div>
|
</div>
|
||||||
</DockShellForm>
|
<Tooltip placement="top" inactive={!working() && blank()} value={tip()}>
|
||||||
<Show when={newSession() && selectedProject()}>
|
<IconButton
|
||||||
<div class="flex h-7 min-w-0 items-center gap-0 px-2">
|
data-action="prompt-submit"
|
||||||
<ComposerPicker state={projectPickerState()} />
|
type="submit"
|
||||||
</div>
|
disabled={!working() && blank()}
|
||||||
</Show>
|
tabIndex={store.mode === "normal" ? undefined : -1}
|
||||||
</div>
|
icon={stopping() ? "stop" : store.mode === "shell" ? "arrow-undo-down" : "arrow-up"}
|
||||||
|
variant="primary"
|
||||||
|
class="size-7 rounded-md p-[6px] text-v2-icon-icon-muted shadow-[var(--v2-elevation-button-contrast)] disabled:opacity-50"
|
||||||
|
style={{
|
||||||
|
"background-image":
|
||||||
|
"linear-gradient(180deg,var(--v2-alpha-light-20) 0%,var(--v2-alpha-light-0) 100%),linear-gradient(90deg,var(--v2-background-bg-contrast) 0%,var(--v2-background-bg-contrast) 100%)",
|
||||||
|
}}
|
||||||
|
aria-label={stopping() ? language.t("prompt.action.stop") : language.t("prompt.action.send")}
|
||||||
|
/>
|
||||||
|
</Tooltip>
|
||||||
|
</div>
|
||||||
|
</DockShellForm>
|
||||||
</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>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -162,8 +162,8 @@ beforeAll(async () => {
|
|||||||
}),
|
}),
|
||||||
}))
|
}))
|
||||||
|
|
||||||
mock.module("@/context/server-sync", () => ({
|
mock.module("@/context/global-sync", () => ({
|
||||||
useServerSync: () => ({
|
useGlobalSync: () => ({
|
||||||
child: (directory: string) => {
|
child: (directory: string) => {
|
||||||
syncedDirectories.push(directory)
|
syncedDirectories.push(directory)
|
||||||
storedSessions[directory] ??= []
|
storedSessions[directory] ??= []
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import { Binary } from "@opencode-ai/core/util/binary"
|
|||||||
import { useNavigate, useParams } from "@solidjs/router"
|
import { useNavigate, useParams } from "@solidjs/router"
|
||||||
import { batch, type Accessor } from "solid-js"
|
import { batch, type Accessor } from "solid-js"
|
||||||
import type { FileSelection } from "@/context/file"
|
import type { FileSelection } from "@/context/file"
|
||||||
import { useServerSync } from "@/context/server-sync"
|
import { useGlobalSync } from "@/context/global-sync"
|
||||||
import { useLanguage } from "@/context/language"
|
import { useLanguage } from "@/context/language"
|
||||||
import { useLayout } from "@/context/layout"
|
import { useLayout } from "@/context/layout"
|
||||||
import { useLocal } from "@/context/local"
|
import { useLocal } from "@/context/local"
|
||||||
@@ -38,7 +38,7 @@ export type FollowupDraft = {
|
|||||||
|
|
||||||
type FollowupSendInput = {
|
type FollowupSendInput = {
|
||||||
client: ReturnType<typeof useSDK>["client"]
|
client: ReturnType<typeof useSDK>["client"]
|
||||||
serverSync: ReturnType<typeof useServerSync>
|
globalSync: ReturnType<typeof useGlobalSync>
|
||||||
sync: ReturnType<typeof useSync>
|
sync: ReturnType<typeof useSync>
|
||||||
draft: FollowupDraft
|
draft: FollowupDraft
|
||||||
messageID?: string
|
messageID?: string
|
||||||
@@ -53,7 +53,7 @@ const draftImages = (prompt: Prompt) => prompt.filter((part): part is ImageAttac
|
|||||||
export async function sendFollowupDraft(input: FollowupSendInput) {
|
export async function sendFollowupDraft(input: FollowupSendInput) {
|
||||||
const text = draftText(input.draft.prompt)
|
const text = draftText(input.draft.prompt)
|
||||||
const images = draftImages(input.draft.prompt)
|
const images = draftImages(input.draft.prompt)
|
||||||
const [, setStore] = input.serverSync.child(input.draft.sessionDirectory)
|
const [, setStore] = input.globalSync.child(input.draft.sessionDirectory)
|
||||||
|
|
||||||
const setBusy = () => {
|
const setBusy = () => {
|
||||||
if (!input.optimisticBusy) return
|
if (!input.optimisticBusy) return
|
||||||
@@ -205,7 +205,7 @@ export function createPromptSubmit(input: PromptSubmitInput) {
|
|||||||
const navigate = useNavigate()
|
const navigate = useNavigate()
|
||||||
const sdk = useSDK()
|
const sdk = useSDK()
|
||||||
const sync = useSync()
|
const sync = useSync()
|
||||||
const serverSync = useServerSync()
|
const globalSync = useGlobalSync()
|
||||||
const local = useLocal()
|
const local = useLocal()
|
||||||
const permission = usePermission()
|
const permission = usePermission()
|
||||||
const prompt = usePrompt()
|
const prompt = usePrompt()
|
||||||
@@ -226,8 +226,8 @@ export function createPromptSubmit(input: PromptSubmitInput) {
|
|||||||
const sessionID = params.id
|
const sessionID = params.id
|
||||||
if (!sessionID) return Promise.resolve()
|
if (!sessionID) return Promise.resolve()
|
||||||
|
|
||||||
serverSync.todo.set(sessionID, [])
|
globalSync.todo.set(sessionID, [])
|
||||||
const [, setStore] = serverSync.child(sdk.directory)
|
const [, setStore] = globalSync.child(sdk.directory)
|
||||||
setStore("todo", sessionID, [])
|
setStore("todo", sessionID, [])
|
||||||
|
|
||||||
input.onAbort?.()
|
input.onAbort?.()
|
||||||
@@ -273,7 +273,7 @@ export function createPromptSubmit(input: PromptSubmitInput) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const seed = (dir: string, info: Session) => {
|
const seed = (dir: string, info: Session) => {
|
||||||
const [, setStore] = serverSync.child(dir)
|
const [, setStore] = globalSync.child(dir)
|
||||||
setStore("session", (list: Session[]) => {
|
setStore("session", (list: Session[]) => {
|
||||||
const result = Binary.search(list, info.id, (item) => item.id)
|
const result = Binary.search(list, info.id, (item) => item.id)
|
||||||
const next = [...list]
|
const next = [...list]
|
||||||
@@ -354,7 +354,7 @@ export function createPromptSubmit(input: PromptSubmitInput) {
|
|||||||
directory: sessionDirectory,
|
directory: sessionDirectory,
|
||||||
throwOnError: true,
|
throwOnError: true,
|
||||||
})
|
})
|
||||||
serverSync.child(sessionDirectory)
|
globalSync.child(sessionDirectory)
|
||||||
}
|
}
|
||||||
|
|
||||||
input.onNewSessionWorktreeReset?.()
|
input.onNewSessionWorktreeReset?.()
|
||||||
@@ -557,7 +557,7 @@ export function createPromptSubmit(input: PromptSubmitInput) {
|
|||||||
void sendFollowupDraft({
|
void sendFollowupDraft({
|
||||||
client,
|
client,
|
||||||
sync,
|
sync,
|
||||||
serverSync,
|
globalSync,
|
||||||
draft,
|
draft,
|
||||||
messageID,
|
messageID,
|
||||||
optimisticBusy: sessionDirectory === projectDirectory,
|
optimisticBusy: sessionDirectory === projectDirectory,
|
||||||
|
|||||||
@@ -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,235 +311,193 @@ export function SessionHeader() {
|
|||||||
<Show when={rightMount()}>
|
<Show when={rightMount()}>
|
||||||
{(mount) => (
|
{(mount) => (
|
||||||
<Portal mount={mount()}>
|
<Portal mount={mount()}>
|
||||||
<Show
|
<div class="flex items-center gap-2">
|
||||||
when={isDesktopV2}
|
<Show when={projectDirectory()}>
|
||||||
fallback={
|
<div class="hidden xl:flex items-center">
|
||||||
<div class="flex items-center gap-2">
|
<Show
|
||||||
<Show when={projectDirectory()}>
|
when={canOpen()}
|
||||||
<div class="hidden xl:flex items-center">
|
fallback={
|
||||||
<Show
|
<div class="flex h-[24px] box-border items-center rounded-md border border-border-weak-base bg-surface-panel overflow-hidden">
|
||||||
when={canOpen()}
|
<Button
|
||||||
fallback={
|
variant="ghost"
|
||||||
<div class="flex h-[24px] box-border items-center rounded-md border border-border-weak-base bg-surface-panel overflow-hidden">
|
class="rounded-none h-full py-0 pr-3 pl-0.5 gap-1.5 border-none shadow-none"
|
||||||
<Button
|
onClick={copyPath}
|
||||||
variant="ghost"
|
aria-label={language.t("session.header.open.copyPath")}
|
||||||
class="rounded-none h-full py-0 pr-3 pl-0.5 gap-1.5 border-none shadow-none"
|
>
|
||||||
onClick={copyPath}
|
<Icon name="copy" size="small" class="text-icon-base" />
|
||||||
aria-label={language.t("session.header.open.copyPath")}
|
<span class="text-12-regular text-text-strong">
|
||||||
>
|
{language.t("session.header.open.copyPath")}
|
||||||
<Icon name="copy" size="small" class="text-icon-base" />
|
</span>
|
||||||
<span class="text-12-regular text-text-strong">
|
</Button>
|
||||||
{language.t("session.header.open.copyPath")}
|
</div>
|
||||||
</span>
|
}
|
||||||
</Button>
|
>
|
||||||
|
<div class="flex items-center">
|
||||||
|
<div class="flex h-[24px] box-border items-center rounded-md border border-border-weak-base bg-surface-panel overflow-hidden">
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
class="rounded-none h-full px-0.5 border-none shadow-none disabled:!cursor-default"
|
||||||
|
classList={{
|
||||||
|
"bg-surface-raised-base-active": opening(),
|
||||||
|
}}
|
||||||
|
onClick={() => openDir(current().id)}
|
||||||
|
disabled={opening()}
|
||||||
|
aria-label={language.t("session.header.open.ariaLabel", { app: current().label })}
|
||||||
|
>
|
||||||
|
<div class="flex size-5 shrink-0 items-center justify-center [&_[data-component=app-icon]]:size-5">
|
||||||
|
<Show when={opening()} fallback={<AppIcon id={current().icon} />}>
|
||||||
|
<Spinner class="size-3.5" style={{ color: tint() ?? "var(--icon-base)" }} />
|
||||||
|
</Show>
|
||||||
</div>
|
</div>
|
||||||
}
|
</Button>
|
||||||
>
|
<DropdownMenu
|
||||||
<div class="flex items-center">
|
gutter={4}
|
||||||
<div class="flex h-[24px] box-border items-center rounded-md border border-border-weak-base bg-surface-panel overflow-hidden">
|
placement="bottom-end"
|
||||||
<Button
|
open={menu.open}
|
||||||
variant="ghost"
|
onOpenChange={(open) => setMenu("open", open)}
|
||||||
class="rounded-none h-full px-0.5 border-none shadow-none disabled:!cursor-default"
|
>
|
||||||
classList={{
|
<DropdownMenu.Trigger
|
||||||
"bg-surface-raised-base-active": opening(),
|
as={IconButton}
|
||||||
}}
|
icon="chevron-down"
|
||||||
onClick={() => openDir(current().id)}
|
variant="ghost"
|
||||||
disabled={opening()}
|
disabled={opening()}
|
||||||
aria-label={language.t("session.header.open.ariaLabel", { app: current().label })}
|
class="rounded-none h-full w-[20px] p-0 border-none shadow-none data-[expanded]:bg-surface-raised-base-active disabled:!cursor-default"
|
||||||
>
|
classList={{
|
||||||
<div class="flex size-5 shrink-0 items-center justify-center [&_[data-component=app-icon]]:size-5">
|
"bg-surface-raised-base-active": opening(),
|
||||||
<Show when={opening()} fallback={<AppIcon id={current().icon} />}>
|
}}
|
||||||
<Spinner class="size-3.5" style={{ color: tint() ?? "var(--icon-base)" }} />
|
aria-label={language.t("session.header.open.menu")}
|
||||||
</Show>
|
/>
|
||||||
</div>
|
<DropdownMenu.Portal>
|
||||||
</Button>
|
<DropdownMenu.Content class="[&_[data-slot=dropdown-menu-item]]:pl-1 [&_[data-slot=dropdown-menu-radio-item]]:pl-1 [&_[data-slot=dropdown-menu-radio-item]+[data-slot=dropdown-menu-radio-item]]:mt-1">
|
||||||
<DropdownMenu
|
<DropdownMenu.Group>
|
||||||
gutter={4}
|
<DropdownMenu.GroupLabel class="!px-1 !py-1">
|
||||||
placement="bottom-end"
|
{language.t("session.header.openIn")}
|
||||||
open={menu.open}
|
</DropdownMenu.GroupLabel>
|
||||||
onOpenChange={(open) => setMenu("open", open)}
|
<DropdownMenu.RadioGroup
|
||||||
>
|
class="mt-1"
|
||||||
<DropdownMenu.Trigger
|
value={current().id}
|
||||||
as={IconButton}
|
onChange={(value) => {
|
||||||
icon="chevron-down"
|
if (!OPEN_APPS.includes(value as OpenApp)) return
|
||||||
variant="ghost"
|
selectApp(value as OpenApp)
|
||||||
disabled={opening()}
|
}}
|
||||||
class="rounded-none h-full w-[20px] p-0 border-none shadow-none data-[expanded]:bg-surface-raised-base-active disabled:!cursor-default"
|
>
|
||||||
classList={{
|
<For each={options()}>
|
||||||
"bg-surface-raised-base-active": opening(),
|
{(o) => (
|
||||||
|
<DropdownMenu.RadioItem
|
||||||
|
value={o.id}
|
||||||
|
disabled={opening()}
|
||||||
|
onSelect={() => {
|
||||||
|
setMenu("open", false)
|
||||||
|
openDir(o.id)
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div class="flex size-5 shrink-0 items-center justify-center [&_[data-component=app-icon]]:size-5">
|
||||||
|
<AppIcon id={o.icon} />
|
||||||
|
</div>
|
||||||
|
<DropdownMenu.ItemLabel>{o.label}</DropdownMenu.ItemLabel>
|
||||||
|
<DropdownMenu.ItemIndicator>
|
||||||
|
<Icon name="check-small" size="small" class="text-icon-weak" />
|
||||||
|
</DropdownMenu.ItemIndicator>
|
||||||
|
</DropdownMenu.RadioItem>
|
||||||
|
)}
|
||||||
|
</For>
|
||||||
|
</DropdownMenu.RadioGroup>
|
||||||
|
</DropdownMenu.Group>
|
||||||
|
<DropdownMenu.Separator />
|
||||||
|
<DropdownMenu.Item
|
||||||
|
onSelect={() => {
|
||||||
|
setMenu("open", false)
|
||||||
|
copyPath()
|
||||||
}}
|
}}
|
||||||
aria-label={language.t("session.header.open.menu")}
|
>
|
||||||
/>
|
<div class="flex size-5 shrink-0 items-center justify-center">
|
||||||
<DropdownMenu.Portal>
|
<Icon name="copy" size="small" class="text-icon-weak" />
|
||||||
<DropdownMenu.Content class="[&_[data-slot=dropdown-menu-item]]:pl-1 [&_[data-slot=dropdown-menu-radio-item]]:pl-1 [&_[data-slot=dropdown-menu-radio-item]+[data-slot=dropdown-menu-radio-item]]:mt-1">
|
</div>
|
||||||
<DropdownMenu.Group>
|
<DropdownMenu.ItemLabel>
|
||||||
<DropdownMenu.GroupLabel class="!px-1 !py-1">
|
{language.t("session.header.open.copyPath")}
|
||||||
{language.t("session.header.openIn")}
|
</DropdownMenu.ItemLabel>
|
||||||
</DropdownMenu.GroupLabel>
|
</DropdownMenu.Item>
|
||||||
<DropdownMenu.RadioGroup
|
</DropdownMenu.Content>
|
||||||
class="mt-1"
|
</DropdownMenu.Portal>
|
||||||
value={current().id}
|
</DropdownMenu>
|
||||||
onChange={(value) => {
|
</div>
|
||||||
if (!OPEN_APPS.includes(value as OpenApp)) return
|
|
||||||
selectApp(value as OpenApp)
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<For each={options()}>
|
|
||||||
{(o) => (
|
|
||||||
<DropdownMenu.RadioItem
|
|
||||||
value={o.id}
|
|
||||||
disabled={opening()}
|
|
||||||
onSelect={() => {
|
|
||||||
setMenu("open", false)
|
|
||||||
openDir(o.id)
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<div class="flex size-5 shrink-0 items-center justify-center [&_[data-component=app-icon]]:size-5">
|
|
||||||
<AppIcon id={o.icon} />
|
|
||||||
</div>
|
|
||||||
<DropdownMenu.ItemLabel>{o.label}</DropdownMenu.ItemLabel>
|
|
||||||
<DropdownMenu.ItemIndicator>
|
|
||||||
<Icon name="check-small" size="small" class="text-icon-weak" />
|
|
||||||
</DropdownMenu.ItemIndicator>
|
|
||||||
</DropdownMenu.RadioItem>
|
|
||||||
)}
|
|
||||||
</For>
|
|
||||||
</DropdownMenu.RadioGroup>
|
|
||||||
</DropdownMenu.Group>
|
|
||||||
<DropdownMenu.Separator />
|
|
||||||
<DropdownMenu.Item
|
|
||||||
onSelect={() => {
|
|
||||||
setMenu("open", false)
|
|
||||||
copyPath()
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<div class="flex size-5 shrink-0 items-center justify-center">
|
|
||||||
<Icon name="copy" size="small" class="text-icon-weak" />
|
|
||||||
</div>
|
|
||||||
<DropdownMenu.ItemLabel>
|
|
||||||
{language.t("session.header.open.copyPath")}
|
|
||||||
</DropdownMenu.ItemLabel>
|
|
||||||
</DropdownMenu.Item>
|
|
||||||
</DropdownMenu.Content>
|
|
||||||
</DropdownMenu.Portal>
|
|
||||||
</DropdownMenu>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Show>
|
|
||||||
</div>
|
</div>
|
||||||
</Show>
|
</Show>
|
||||||
<div class="flex items-center gap-1">
|
|
||||||
<Show when={status()}>
|
|
||||||
<Tooltip placement="bottom" value={language.t("status.popover.trigger")}>
|
|
||||||
<StatusPopover />
|
|
||||||
</Tooltip>
|
|
||||||
</Show>
|
|
||||||
<Show when={term()}>
|
|
||||||
<TooltipKeybind
|
|
||||||
title={language.t("command.terminal.toggle")}
|
|
||||||
keybind={command.keybind("terminal.toggle")}
|
|
||||||
>
|
|
||||||
<Button
|
|
||||||
variant="ghost"
|
|
||||||
class="group/terminal-toggle titlebar-icon w-8 h-6 p-0 box-border shrink-0"
|
|
||||||
onClick={toggleTerminal}
|
|
||||||
aria-label={language.t("command.terminal.toggle")}
|
|
||||||
aria-expanded={view().terminal.opened()}
|
|
||||||
aria-controls="terminal-panel"
|
|
||||||
>
|
|
||||||
<Icon size="small" name={view().terminal.opened() ? "terminal-active" : "terminal"} />
|
|
||||||
</Button>
|
|
||||||
</TooltipKeybind>
|
|
||||||
</Show>
|
|
||||||
|
|
||||||
<div class="hidden md:flex items-center gap-1 shrink-0">
|
|
||||||
<TooltipKeybind
|
|
||||||
title={language.t("command.review.toggle")}
|
|
||||||
keybind={command.keybind("review.toggle")}
|
|
||||||
>
|
|
||||||
<Button
|
|
||||||
variant="ghost"
|
|
||||||
class="group/review-toggle titlebar-icon w-8 h-6 p-0 box-border"
|
|
||||||
onClick={() => view().reviewPanel.toggle()}
|
|
||||||
aria-label={language.t("command.review.toggle")}
|
|
||||||
aria-expanded={view().reviewPanel.opened()}
|
|
||||||
aria-controls="review-panel"
|
|
||||||
>
|
|
||||||
<Icon size="small" name={view().reviewPanel.opened() ? "review-active" : "review"} />
|
|
||||||
</Button>
|
|
||||||
</TooltipKeybind>
|
|
||||||
|
|
||||||
<Show when={tree()}>
|
|
||||||
<TooltipKeybind
|
|
||||||
title={language.t("command.fileTree.toggle")}
|
|
||||||
keybind={command.keybind("fileTree.toggle")}
|
|
||||||
>
|
|
||||||
<Button
|
|
||||||
variant="ghost"
|
|
||||||
class="titlebar-icon w-8 h-6 p-0 box-border"
|
|
||||||
onClick={() => layout.fileTree.toggle()}
|
|
||||||
aria-label={language.t("command.fileTree.toggle")}
|
|
||||||
aria-expanded={layout.fileTree.opened()}
|
|
||||||
aria-controls="file-tree-panel"
|
|
||||||
>
|
|
||||||
<div class="relative flex items-center justify-center size-4">
|
|
||||||
<Icon
|
|
||||||
size="small"
|
|
||||||
name={layout.fileTree.opened() ? "file-tree-active" : "file-tree"}
|
|
||||||
classList={{
|
|
||||||
"text-icon-strong": layout.fileTree.opened(),
|
|
||||||
"text-icon-weak": !layout.fileTree.opened(),
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</Button>
|
|
||||||
</TooltipKeybind>
|
|
||||||
</Show>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
}
|
</Show>
|
||||||
>
|
<div class="flex items-center gap-1">
|
||||||
<SessionHeaderV2Actions state={v2ActionsState()} />
|
<Show when={status()}>
|
||||||
</Show>
|
<Tooltip placement="bottom" value={language.t("status.popover.trigger")}>
|
||||||
|
<StatusPopover />
|
||||||
|
</Tooltip>
|
||||||
|
</Show>
|
||||||
|
<Show when={term()}>
|
||||||
|
<TooltipKeybind
|
||||||
|
title={language.t("command.terminal.toggle")}
|
||||||
|
keybind={command.keybind("terminal.toggle")}
|
||||||
|
>
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
class="group/terminal-toggle titlebar-icon w-8 h-6 p-0 box-border shrink-0"
|
||||||
|
onClick={toggleTerminal}
|
||||||
|
aria-label={language.t("command.terminal.toggle")}
|
||||||
|
aria-expanded={view().terminal.opened()}
|
||||||
|
aria-controls="terminal-panel"
|
||||||
|
>
|
||||||
|
<Icon size="small" name={view().terminal.opened() ? "terminal-active" : "terminal"} />
|
||||||
|
</Button>
|
||||||
|
</TooltipKeybind>
|
||||||
|
</Show>
|
||||||
|
|
||||||
|
<div class="hidden md:flex items-center gap-1 shrink-0">
|
||||||
|
<TooltipKeybind
|
||||||
|
title={language.t("command.review.toggle")}
|
||||||
|
keybind={command.keybind("review.toggle")}
|
||||||
|
>
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
class="group/review-toggle titlebar-icon w-8 h-6 p-0 box-border"
|
||||||
|
onClick={() => view().reviewPanel.toggle()}
|
||||||
|
aria-label={language.t("command.review.toggle")}
|
||||||
|
aria-expanded={view().reviewPanel.opened()}
|
||||||
|
aria-controls="review-panel"
|
||||||
|
>
|
||||||
|
<Icon size="small" name={view().reviewPanel.opened() ? "review-active" : "review"} />
|
||||||
|
</Button>
|
||||||
|
</TooltipKeybind>
|
||||||
|
|
||||||
|
<Show when={tree()}>
|
||||||
|
<TooltipKeybind
|
||||||
|
title={language.t("command.fileTree.toggle")}
|
||||||
|
keybind={command.keybind("fileTree.toggle")}
|
||||||
|
>
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
class="titlebar-icon w-8 h-6 p-0 box-border"
|
||||||
|
onClick={() => layout.fileTree.toggle()}
|
||||||
|
aria-label={language.t("command.fileTree.toggle")}
|
||||||
|
aria-expanded={layout.fileTree.opened()}
|
||||||
|
aria-controls="file-tree-panel"
|
||||||
|
>
|
||||||
|
<div class="relative flex items-center justify-center size-4">
|
||||||
|
<Icon
|
||||||
|
size="small"
|
||||||
|
name={layout.fileTree.opened() ? "file-tree-active" : "file-tree"}
|
||||||
|
classList={{
|
||||||
|
"text-icon-strong": layout.fileTree.opened(),
|
||||||
|
"text-icon-weak": !layout.fileTree.opened(),
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</Button>
|
||||||
|
</TooltipKeybind>
|
||||||
|
</Show>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</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 { useGlobalSync } from "@/context/global-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 globalSync = useGlobalSync()
|
||||||
|
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 = globalSync.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>
|
||||||
|
|||||||
@@ -12,8 +12,8 @@ import { useParams } from "@solidjs/router"
|
|||||||
import { useLanguage } from "@/context/language"
|
import { useLanguage } from "@/context/language"
|
||||||
import { usePermission } from "@/context/permission"
|
import { usePermission } from "@/context/permission"
|
||||||
import { usePlatform, type DisplayBackend } from "@/context/platform"
|
import { usePlatform, type DisplayBackend } from "@/context/platform"
|
||||||
import { useServerSync } from "@/context/server-sync"
|
import { useGlobalSync } from "@/context/global-sync"
|
||||||
import { useServerSDK } from "@/context/server-sdk"
|
import { useGlobalSDK } from "@/context/global-sdk"
|
||||||
import {
|
import {
|
||||||
monoDefault,
|
monoDefault,
|
||||||
monoFontFamily,
|
monoFontFamily,
|
||||||
@@ -175,8 +175,8 @@ export const SettingsGeneral: Component = () => {
|
|||||||
|
|
||||||
const themeOptions = createMemo<ThemeOption[]>(() => theme.ids().map((id) => ({ id, name: theme.name(id) })))
|
const themeOptions = createMemo<ThemeOption[]>(() => theme.ids().map((id) => ({ id, name: theme.name(id) })))
|
||||||
|
|
||||||
const serverSync = useServerSync()
|
const globalSync = useGlobalSync()
|
||||||
const globalSdk = useServerSDK()
|
const globalSdk = useGlobalSDK()
|
||||||
|
|
||||||
const [shells] = createResource(
|
const [shells] = createResource(
|
||||||
() =>
|
() =>
|
||||||
@@ -204,11 +204,11 @@ export const SettingsGeneral: Component = () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
const autoOption = { id: "auto", value: "", label: language.t("settings.general.row.shell.autoDefault") }
|
const autoOption = { id: "auto", value: "", label: language.t("settings.general.row.shell.autoDefault") }
|
||||||
const currentShell = createMemo(() => serverSync.data.config.shell ?? "")
|
const currentShell = createMemo(() => globalSync.data.config.shell ?? "")
|
||||||
|
|
||||||
const shellOptions = createMemo<ShellSelectOption[]>(() => {
|
const shellOptions = createMemo<ShellSelectOption[]>(() => {
|
||||||
const list = shells.latest
|
const list = shells.latest
|
||||||
const current = serverSync.data.config.shell
|
const current = globalSync.data.config.shell
|
||||||
|
|
||||||
const nameCounts = new Map<string, number>()
|
const nameCounts = new Map<string, number>()
|
||||||
for (const s of list) {
|
for (const s of list) {
|
||||||
@@ -343,7 +343,7 @@ export const SettingsGeneral: Component = () => {
|
|||||||
onSelect={(option) => {
|
onSelect={(option) => {
|
||||||
if (!option) return
|
if (!option) return
|
||||||
if (option.value === currentShell()) return
|
if (option.value === currentShell()) return
|
||||||
serverSync.updateConfig({ shell: option.value })
|
globalSync.updateConfig({ shell: option.value })
|
||||||
}}
|
}}
|
||||||
variant="secondary"
|
variant="secondary"
|
||||||
size="small"
|
size="small"
|
||||||
@@ -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>
|
||||||
|
|||||||
@@ -6,8 +6,8 @@ import { showToast } from "@opencode-ai/ui/toast"
|
|||||||
import { popularProviders, useProviders } from "@/hooks/use-providers"
|
import { popularProviders, useProviders } from "@/hooks/use-providers"
|
||||||
import { createMemo, type Component, For, Show } from "solid-js"
|
import { createMemo, type Component, For, Show } from "solid-js"
|
||||||
import { useLanguage } from "@/context/language"
|
import { useLanguage } from "@/context/language"
|
||||||
import { useServerSDK } from "@/context/server-sdk"
|
import { useGlobalSDK } from "@/context/global-sdk"
|
||||||
import { useServerSync } from "@/context/server-sync"
|
import { useGlobalSync } from "@/context/global-sync"
|
||||||
import { DialogConnectProvider } from "./dialog-connect-provider"
|
import { DialogConnectProvider } from "./dialog-connect-provider"
|
||||||
import { DialogSelectProvider } from "./dialog-select-provider"
|
import { DialogSelectProvider } from "./dialog-select-provider"
|
||||||
import { DialogCustomProvider } from "./dialog-custom-provider"
|
import { DialogCustomProvider } from "./dialog-custom-provider"
|
||||||
@@ -30,8 +30,8 @@ const PROVIDER_NOTES = [
|
|||||||
export const SettingsProviders: Component = () => {
|
export const SettingsProviders: Component = () => {
|
||||||
const dialog = useDialog()
|
const dialog = useDialog()
|
||||||
const language = useLanguage()
|
const language = useLanguage()
|
||||||
const serverSDK = useServerSDK()
|
const globalSDK = useGlobalSDK()
|
||||||
const serverSync = useServerSync()
|
const globalSync = useGlobalSync()
|
||||||
const providers = useProviders()
|
const providers = useProviders()
|
||||||
|
|
||||||
const connected = createMemo(() => {
|
const connected = createMemo(() => {
|
||||||
@@ -74,7 +74,7 @@ export const SettingsProviders: Component = () => {
|
|||||||
const note = (id: string) => PROVIDER_NOTES.find((item) => item.match(id))?.key
|
const note = (id: string) => PROVIDER_NOTES.find((item) => item.match(id))?.key
|
||||||
|
|
||||||
const isConfigCustom = (providerID: string) => {
|
const isConfigCustom = (providerID: string) => {
|
||||||
const provider = serverSync.data.config.provider?.[providerID]
|
const provider = globalSync.data.config.provider?.[providerID]
|
||||||
if (!provider) return false
|
if (!provider) return false
|
||||||
if (provider.npm !== "@ai-sdk/openai-compatible") return false
|
if (provider.npm !== "@ai-sdk/openai-compatible") return false
|
||||||
if (!provider.models || Object.keys(provider.models).length === 0) return false
|
if (!provider.models || Object.keys(provider.models).length === 0) return false
|
||||||
@@ -82,11 +82,11 @@ export const SettingsProviders: Component = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const disableProvider = async (providerID: string, name: string) => {
|
const disableProvider = async (providerID: string, name: string) => {
|
||||||
const before = serverSync.data.config.disabled_providers ?? []
|
const before = globalSync.data.config.disabled_providers ?? []
|
||||||
const next = before.includes(providerID) ? before : [...before, providerID]
|
const next = before.includes(providerID) ? before : [...before, providerID]
|
||||||
serverSync.set("config", "disabled_providers", next)
|
globalSync.set("config", "disabled_providers", next)
|
||||||
|
|
||||||
await serverSync
|
await globalSync
|
||||||
.updateConfig({ disabled_providers: next })
|
.updateConfig({ disabled_providers: next })
|
||||||
.then(() => {
|
.then(() => {
|
||||||
showToast({
|
showToast({
|
||||||
@@ -97,7 +97,7 @@ export const SettingsProviders: Component = () => {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
.catch((err: unknown) => {
|
.catch((err: unknown) => {
|
||||||
serverSync.set("config", "disabled_providers", before)
|
globalSync.set("config", "disabled_providers", before)
|
||||||
const message = err instanceof Error ? err.message : String(err)
|
const message = err instanceof Error ? err.message : String(err)
|
||||||
showToast({ title: language.t("common.requestFailed"), description: message })
|
showToast({ title: language.t("common.requestFailed"), description: message })
|
||||||
})
|
})
|
||||||
@@ -105,14 +105,14 @@ export const SettingsProviders: Component = () => {
|
|||||||
|
|
||||||
const disconnect = async (providerID: string, name: string) => {
|
const disconnect = async (providerID: string, name: string) => {
|
||||||
if (isConfigCustom(providerID)) {
|
if (isConfigCustom(providerID)) {
|
||||||
await serverSDK.client.auth.remove({ providerID }).catch(() => undefined)
|
await globalSDK.client.auth.remove({ providerID }).catch(() => undefined)
|
||||||
await disableProvider(providerID, name)
|
await disableProvider(providerID, name)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
await serverSDK.client.auth
|
await globalSDK.client.auth
|
||||||
.remove({ providerID })
|
.remove({ providerID })
|
||||||
.then(async () => {
|
.then(async () => {
|
||||||
await serverSDK.client.global.dispose()
|
await globalSDK.client.global.dispose()
|
||||||
showToast({
|
showToast({
|
||||||
variant: "success",
|
variant: "success",
|
||||||
icon: "circle-check",
|
icon: "circle-check",
|
||||||
|
|||||||
@@ -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/global-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,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -16,19 +16,15 @@ import { useLanguage } from "@/context/language"
|
|||||||
import { useSettings } from "@/context/settings"
|
import { useSettings } from "@/context/settings"
|
||||||
import { WindowsAppMenu } from "./windows-app-menu"
|
import { WindowsAppMenu } from "./windows-app-menu"
|
||||||
import { applyPath, backPath, forwardPath } from "./titlebar-history"
|
import { applyPath, backPath, forwardPath } from "./titlebar-history"
|
||||||
import { useServerSync } from "@/context/server-sync"
|
import { useGlobalSync } from "@/context/global-sync"
|
||||||
import { decodeDirectory } from "@/pages/directory-layout"
|
import { decodeDirectory } from "@/pages/directory-layout"
|
||||||
import { iife } from "@opencode-ai/core/util/iife"
|
import { iife } from "@opencode-ai/core/util/iife"
|
||||||
import { base64Encode } from "@opencode-ai/core/util/encode"
|
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,9 +219,9 @@ export function Titlebar(props: { update?: TitlebarUpdate }) {
|
|||||||
onDblClick={maximize}
|
onDblClick={maximize}
|
||||||
>
|
>
|
||||||
<Switch>
|
<Switch>
|
||||||
<Match when={useV2Titlebar()}>
|
<Match when={USE_V2_TITLEBAR}>
|
||||||
{(_) => {
|
{(_) => {
|
||||||
const serverSync = useServerSync()
|
const globalSync = useGlobalSync()
|
||||||
const navigate = useNavigate()
|
const navigate = useNavigate()
|
||||||
const homeMatch = useMatch(() => "/")
|
const homeMatch = useMatch(() => "/")
|
||||||
|
|
||||||
@@ -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
|
||||||
@@ -445,7 +391,7 @@ export function Titlebar(props: { update?: TitlebarUpdate }) {
|
|||||||
const base = mapArray(
|
const base = mapArray(
|
||||||
() => tabsStore,
|
() => tabsStore,
|
||||||
(tab) => {
|
(tab) => {
|
||||||
const sync = serverSync.createDirSyncContext(tab.dir)
|
const sync = globalSync.createDirSyncContext(tab.dir)
|
||||||
const session = sync.session.get(tab.sessionId)
|
const session = sync.session.get(tab.sessionId)
|
||||||
return session ? { ...tab, info: session } : null
|
return session ? { ...tab, info: session } : null
|
||||||
},
|
},
|
||||||
@@ -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,11 +8,10 @@ import {
|
|||||||
getSessionPrefetchPromise,
|
getSessionPrefetchPromise,
|
||||||
setSessionPrefetch,
|
setSessionPrefetch,
|
||||||
} from "./global-sync/session-prefetch"
|
} from "./global-sync/session-prefetch"
|
||||||
import { createServerSyncContext } from "./server-sync"
|
import { useGlobalSync } from "./global-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"
|
|
||||||
|
|
||||||
const SKIP_PARTS = new Set(["patch", "step-start", "step-finish"])
|
const SKIP_PARTS = new Set(["patch", "step-start", "step-finish"])
|
||||||
|
|
||||||
@@ -34,12 +33,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)
|
||||||
@@ -171,17 +164,16 @@ function setOptimisticRemove(setStore: (...args: unknown[]) => void, input: Opti
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
export const createDirSyncContext = (directory: string, serverSync: ReturnType<typeof createServerSyncContext>) => {
|
export const createDirSyncContext = (client: OpencodeClient, directory: string) => {
|
||||||
const serverSDK = useServerSDK()
|
const globalSync = useGlobalSync()
|
||||||
const client = serverSDK.createClient({ directory, throwOnError: true })
|
|
||||||
|
|
||||||
type Child = ReturnType<(typeof serverSync)["child"]>
|
type Child = ReturnType<(typeof globalSync)["child"]>
|
||||||
type Setter = Child[1]
|
type Setter = Child[1]
|
||||||
|
|
||||||
const current = createMemo(() => serverSync.child(directory))
|
const current = createMemo(() => globalSync.child(directory))
|
||||||
const target = (directory?: string) => {
|
const target = (directory?: string) => {
|
||||||
if (!directory || directory === directory) return current()
|
if (!directory || directory === directory) return current()
|
||||||
return serverSync.child(directory)
|
return globalSync.child(directory)
|
||||||
}
|
}
|
||||||
const absolute = (path: string) => (current()[0].path.directory + "/" + path).replace("//", "/")
|
const absolute = (path: string) => (current()[0].path.directory + "/" + path).replace("//", "/")
|
||||||
const initialMessagePageSize = 80
|
const initialMessagePageSize = 80
|
||||||
@@ -247,7 +239,7 @@ export const createDirSyncContext = (directory: string, serverSync: ReturnType<t
|
|||||||
if (!first) break
|
if (!first) break
|
||||||
const stale = [...(seen.get(first) ?? [])]
|
const stale = [...(seen.get(first) ?? [])]
|
||||||
seen.delete(first)
|
seen.delete(first)
|
||||||
const [, setStore] = serverSync.child(first, { bootstrap: false })
|
const [, setStore] = globalSync.child(first, { bootstrap: false })
|
||||||
evict(first, setStore, stale)
|
evict(first, setStore, stale)
|
||||||
}
|
}
|
||||||
return created
|
return created
|
||||||
@@ -275,7 +267,7 @@ export const createDirSyncContext = (directory: string, serverSync: ReturnType<t
|
|||||||
if (sessionIDs.length === 0) return
|
if (sessionIDs.length === 0) return
|
||||||
clearSessionPrefetch(directory, sessionIDs)
|
clearSessionPrefetch(directory, sessionIDs)
|
||||||
for (const sessionID of sessionIDs) {
|
for (const sessionID of sessionIDs) {
|
||||||
serverSync.todo.set(sessionID, undefined)
|
globalSync.todo.set(sessionID, undefined)
|
||||||
}
|
}
|
||||||
setStore(
|
setStore(
|
||||||
produce((draft) => {
|
produce((draft) => {
|
||||||
@@ -332,7 +324,7 @@ export const createDirSyncContext = (directory: string, serverSync: ReturnType<t
|
|||||||
for (const messageID of next.confirmed) {
|
for (const messageID of next.confirmed) {
|
||||||
clearOptimistic(input.directory, input.sessionID, messageID)
|
clearOptimistic(input.directory, input.sessionID, messageID)
|
||||||
}
|
}
|
||||||
const [store] = serverSync.child(input.directory, { bootstrap: false })
|
const [store] = globalSync.child(input.directory, { bootstrap: false })
|
||||||
const cached = input.mode === "prepend" ? (store.message[input.sessionID] ?? []) : []
|
const cached = input.mode === "prepend" ? (store.message[input.sessionID] ?? []) : []
|
||||||
const message = input.mode === "prepend" ? merge(cached, next.session) : next.session
|
const message = input.mode === "prepend" ? merge(cached, next.session) : next.session
|
||||||
batch(() => {
|
batch(() => {
|
||||||
@@ -353,10 +345,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) => {
|
||||||
@@ -385,8 +373,8 @@ export const createDirSyncContext = (directory: string, serverSync: ReturnType<t
|
|||||||
},
|
},
|
||||||
get project() {
|
get project() {
|
||||||
const store = current()[0]
|
const store = current()[0]
|
||||||
const match = Binary.search(serverSync.data.project, store.project, (p) => p.id)
|
const match = Binary.search(globalSync.data.project, store.project, (p) => p.id)
|
||||||
if (match.found) return serverSync.data.project[match.index]
|
if (match.found) return globalSync.data.project[match.index]
|
||||||
return undefined
|
return undefined
|
||||||
},
|
},
|
||||||
session: {
|
session: {
|
||||||
@@ -430,7 +418,7 @@ export const createDirSyncContext = (directory: string, serverSync: ReturnType<t
|
|||||||
})
|
})
|
||||||
},
|
},
|
||||||
async sync(sessionID: string, opts?: { force?: boolean }) {
|
async sync(sessionID: string, opts?: { force?: boolean }) {
|
||||||
const [store, setStore] = serverSync.child(directory)
|
const [store, setStore] = globalSync.child(directory)
|
||||||
const key = keyFor(directory, sessionID)
|
const key = keyFor(directory, sessionID)
|
||||||
|
|
||||||
touch(directory, setStore, sessionID)
|
touch(directory, setStore, sessionID)
|
||||||
@@ -468,27 +456,22 @@ 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
|
setStore(
|
||||||
setStore(
|
"session",
|
||||||
"session",
|
produce((draft) => {
|
||||||
produce((draft) => {
|
const match = Binary.search(draft, sessionID, (s) => s.id)
|
||||||
const match = Binary.search(draft, sessionID, (s) => s.id)
|
if (match.found) {
|
||||||
if (match.found) {
|
draft[match.index] = data
|
||||||
draft[match.index] = data
|
return
|
||||||
return
|
}
|
||||||
}
|
draft.splice(match.index, 0, data)
|
||||||
draft.splice(match.index, 0, data)
|
}),
|
||||||
}),
|
)
|
||||||
)
|
})
|
||||||
})
|
|
||||||
.catch((error) => {
|
|
||||||
if (isNotFound(error) && !tracked(directory, sessionID)) return
|
|
||||||
throw error
|
|
||||||
})
|
|
||||||
|
|
||||||
const messagesReq =
|
const messagesReq =
|
||||||
cached && !opts?.force
|
cached && !opts?.force
|
||||||
@@ -505,7 +488,7 @@ export const createDirSyncContext = (directory: string, serverSync: ReturnType<t
|
|||||||
})
|
})
|
||||||
},
|
},
|
||||||
async diff(sessionID: string, opts?: { force?: boolean }) {
|
async diff(sessionID: string, opts?: { force?: boolean }) {
|
||||||
const [store, setStore] = serverSync.child(directory)
|
const [store, setStore] = globalSync.child(directory)
|
||||||
touch(directory, setStore, sessionID)
|
touch(directory, setStore, sessionID)
|
||||||
if (store.session_diff[sessionID] !== undefined && !opts?.force) return
|
if (store.session_diff[sessionID] !== undefined && !opts?.force) return
|
||||||
|
|
||||||
@@ -518,13 +501,13 @@ export const createDirSyncContext = (directory: string, serverSync: ReturnType<t
|
|||||||
)
|
)
|
||||||
},
|
},
|
||||||
async todo(sessionID: string, opts?: { force?: boolean }) {
|
async todo(sessionID: string, opts?: { force?: boolean }) {
|
||||||
const [store, setStore] = serverSync.child(directory)
|
const [store, setStore] = globalSync.child(directory)
|
||||||
touch(directory, setStore, sessionID)
|
touch(directory, setStore, sessionID)
|
||||||
const existing = store.todo[sessionID]
|
const existing = store.todo[sessionID]
|
||||||
const cached = serverSync.data.session_todo[sessionID]
|
const cached = globalSync.data.session_todo[sessionID]
|
||||||
if (existing !== undefined) {
|
if (existing !== undefined) {
|
||||||
if (cached === undefined) {
|
if (cached === undefined) {
|
||||||
serverSync.todo.set(sessionID, existing)
|
globalSync.todo.set(sessionID, existing)
|
||||||
}
|
}
|
||||||
if (!opts?.force) return
|
if (!opts?.force) return
|
||||||
}
|
}
|
||||||
@@ -539,7 +522,7 @@ export const createDirSyncContext = (directory: string, serverSync: ReturnType<t
|
|||||||
if (!tracked(directory, sessionID)) return
|
if (!tracked(directory, sessionID)) return
|
||||||
const list = todo.data ?? []
|
const list = todo.data ?? []
|
||||||
setStore("todo", sessionID, reconcile(list, { key: "id" }))
|
setStore("todo", sessionID, reconcile(list, { key: "id" }))
|
||||||
serverSync.todo.set(sessionID, list)
|
globalSync.todo.set(sessionID, list)
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
@@ -557,7 +540,7 @@ export const createDirSyncContext = (directory: string, serverSync: ReturnType<t
|
|||||||
return meta.loading[key] ?? false
|
return meta.loading[key] ?? false
|
||||||
},
|
},
|
||||||
async loadMore(sessionID: string, count?: number) {
|
async loadMore(sessionID: string, count?: number) {
|
||||||
const [, setStore] = serverSync.child(directory)
|
const [, setStore] = globalSync.child(directory)
|
||||||
touch(directory, setStore, sessionID)
|
touch(directory, setStore, sessionID)
|
||||||
const key = keyFor(directory, sessionID)
|
const key = keyFor(directory, sessionID)
|
||||||
const step = count ?? historyMessagePageSize
|
const step = count ?? historyMessagePageSize
|
||||||
@@ -578,12 +561,12 @@ export const createDirSyncContext = (directory: string, serverSync: ReturnType<t
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
evict(sessionID: string, _directory = directory) {
|
evict(sessionID: string, _directory = directory) {
|
||||||
const [, setStore] = serverSync.child(_directory)
|
const [, setStore] = globalSync.child(_directory)
|
||||||
seenFor(_directory).delete(sessionID)
|
seenFor(_directory).delete(sessionID)
|
||||||
evict(_directory, setStore, [sessionID])
|
evict(_directory, setStore, [sessionID])
|
||||||
},
|
},
|
||||||
fetch: async (count = 10) => {
|
fetch: async (count = 10) => {
|
||||||
const [store, setStore] = serverSync.child(directory)
|
const [store, setStore] = globalSync.child(directory)
|
||||||
setStore("limit", (x) => x + count)
|
setStore("limit", (x) => x + count)
|
||||||
await client.session.list().then((x) => {
|
await client.session.list().then((x) => {
|
||||||
const sessions = (x.data ?? [])
|
const sessions = (x.data ?? [])
|
||||||
@@ -595,7 +578,7 @@ export const createDirSyncContext = (directory: string, serverSync: ReturnType<t
|
|||||||
},
|
},
|
||||||
more: createMemo(() => current()[0].session.length >= current()[0].limit),
|
more: createMemo(() => current()[0].session.length >= current()[0].limit),
|
||||||
archive: async (sessionID: string) => {
|
archive: async (sessionID: string) => {
|
||||||
const [, setStore] = serverSync.child(directory)
|
const [, setStore] = globalSync.child(directory)
|
||||||
await client.session.update({ sessionID, time: { archived: Date.now() } })
|
await client.session.update({ sessionID, time: { archived: Date.now() } })
|
||||||
setStore(
|
setStore(
|
||||||
produce((draft) => {
|
produce((draft) => {
|
||||||
|
|||||||
@@ -0,0 +1,309 @@
|
|||||||
|
import type { Event } from "@opencode-ai/sdk/v2/client"
|
||||||
|
import { createSimpleContext } from "@opencode-ai/ui/context"
|
||||||
|
import { createGlobalEmitter } from "@solid-primitives/event-bus"
|
||||||
|
import { makeEventListener } from "@solid-primitives/event-listener"
|
||||||
|
import { batch, onCleanup, onMount } from "solid-js"
|
||||||
|
import { createSdkForServer } from "@/utils/server"
|
||||||
|
import { useLanguage } from "./language"
|
||||||
|
import { usePlatform } from "./platform"
|
||||||
|
import { useServer } from "./server"
|
||||||
|
|
||||||
|
const isAbortError = (error: unknown) =>
|
||||||
|
error !== null && typeof error === "object" && "name" in error && error.name === "AbortError"
|
||||||
|
|
||||||
|
export const { use: useGlobalSDK, provider: GlobalSDKProvider } = createSimpleContext({
|
||||||
|
name: "GlobalSDK",
|
||||||
|
init: () => {
|
||||||
|
const language = useLanguage()
|
||||||
|
const server = useServer()
|
||||||
|
const platform = usePlatform()
|
||||||
|
const abort = new AbortController()
|
||||||
|
|
||||||
|
const eventFetch = (() => {
|
||||||
|
if (!platform.fetch || !server.current) return
|
||||||
|
try {
|
||||||
|
const url = new URL(server.current.http.url)
|
||||||
|
const loopback = url.hostname === "localhost" || url.hostname === "127.0.0.1" || url.hostname === "::1"
|
||||||
|
if (url.protocol === "http:" && !loopback) return platform.fetch
|
||||||
|
} catch {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
})()
|
||||||
|
|
||||||
|
const currentServer = server.current
|
||||||
|
if (!currentServer) throw new Error(language.t("error.globalSDK.noServerAvailable"))
|
||||||
|
|
||||||
|
const eventSdk = createSdkForServer({
|
||||||
|
signal: abort.signal,
|
||||||
|
fetch: eventFetch,
|
||||||
|
server: currentServer.http,
|
||||||
|
})
|
||||||
|
const emitter = createGlobalEmitter<{
|
||||||
|
[key: string]: Event
|
||||||
|
}>()
|
||||||
|
|
||||||
|
type Queued = { directory: string; payload: Event }
|
||||||
|
const FLUSH_FRAME_MS = 16
|
||||||
|
const STREAM_YIELD_MS = 8
|
||||||
|
const RECONNECT_DELAY_MS = 250
|
||||||
|
|
||||||
|
let queue: Queued[] = []
|
||||||
|
let buffer: Queued[] = []
|
||||||
|
const coalesced = new Map<string, number>()
|
||||||
|
const staleDeltas = new Set<string>()
|
||||||
|
let timer: ReturnType<typeof setTimeout> | undefined
|
||||||
|
let last = 0
|
||||||
|
|
||||||
|
const deltaKey = (directory: string, messageID: string, partID: string) => `${directory}:${messageID}:${partID}`
|
||||||
|
|
||||||
|
const key = (directory: string, payload: Event) => {
|
||||||
|
if (payload.type === "session.status") return `session.status:${directory}:${payload.properties.sessionID}`
|
||||||
|
if (payload.type === "lsp.updated") return `lsp.updated:${directory}`
|
||||||
|
if (payload.type === "message.part.updated") {
|
||||||
|
const part = payload.properties.part
|
||||||
|
return `message.part.updated:${directory}:${part.messageID}:${part.id}`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const flush = () => {
|
||||||
|
if (timer) clearTimeout(timer)
|
||||||
|
timer = undefined
|
||||||
|
|
||||||
|
if (queue.length === 0) return
|
||||||
|
|
||||||
|
const events = queue
|
||||||
|
const skip = staleDeltas.size > 0 ? new Set(staleDeltas) : undefined
|
||||||
|
queue = buffer
|
||||||
|
buffer = events
|
||||||
|
queue.length = 0
|
||||||
|
coalesced.clear()
|
||||||
|
staleDeltas.clear()
|
||||||
|
|
||||||
|
last = Date.now()
|
||||||
|
batch(() => {
|
||||||
|
for (const event of events) {
|
||||||
|
if (skip && event.payload.type === "message.part.delta") {
|
||||||
|
const props = event.payload.properties
|
||||||
|
if (skip.has(deltaKey(event.directory, props.messageID, props.partID))) continue
|
||||||
|
}
|
||||||
|
emitter.emit(event.directory, event.payload)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
buffer.length = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
const schedule = () => {
|
||||||
|
if (timer) return
|
||||||
|
const elapsed = Date.now() - last
|
||||||
|
timer = setTimeout(flush, Math.max(0, FLUSH_FRAME_MS - elapsed))
|
||||||
|
}
|
||||||
|
|
||||||
|
let streamErrorLogged = false
|
||||||
|
const wait = (ms: number) => new Promise<void>((resolve) => setTimeout(resolve, ms))
|
||||||
|
const aborted = isAbortError
|
||||||
|
|
||||||
|
let attempt: AbortController | undefined
|
||||||
|
let run: Promise<void> | undefined
|
||||||
|
let started = false
|
||||||
|
const HEARTBEAT_TIMEOUT_MS = 15_000
|
||||||
|
let lastEventAt = Date.now()
|
||||||
|
let heartbeat: ReturnType<typeof setTimeout> | undefined
|
||||||
|
const resetHeartbeat = () => {
|
||||||
|
lastEventAt = Date.now()
|
||||||
|
if (heartbeat) clearTimeout(heartbeat)
|
||||||
|
heartbeat = setTimeout(() => {
|
||||||
|
attempt?.abort()
|
||||||
|
}, HEARTBEAT_TIMEOUT_MS)
|
||||||
|
}
|
||||||
|
const clearHeartbeat = () => {
|
||||||
|
if (!heartbeat) return
|
||||||
|
clearTimeout(heartbeat)
|
||||||
|
heartbeat = undefined
|
||||||
|
}
|
||||||
|
|
||||||
|
const start = () => {
|
||||||
|
if (started) return run
|
||||||
|
started = true
|
||||||
|
run = (async () => {
|
||||||
|
// oxlint-disable-next-line no-unmodified-loop-condition -- `started` is set to false by stop() which also aborts; both flags are checked to allow graceful exit
|
||||||
|
while (!abort.signal.aborted && started) {
|
||||||
|
attempt = new AbortController()
|
||||||
|
lastEventAt = Date.now()
|
||||||
|
const onAbort = () => {
|
||||||
|
attempt?.abort()
|
||||||
|
}
|
||||||
|
abort.signal.addEventListener("abort", onAbort)
|
||||||
|
try {
|
||||||
|
const events = await eventSdk.global.event({
|
||||||
|
signal: attempt.signal,
|
||||||
|
onSseError: (error) => {
|
||||||
|
if (aborted(error)) return
|
||||||
|
if (streamErrorLogged) return
|
||||||
|
streamErrorLogged = true
|
||||||
|
console.error("[global-sdk] event stream error", {
|
||||||
|
url: currentServer.http.url,
|
||||||
|
fetch: eventFetch ? "platform" : "webview",
|
||||||
|
error,
|
||||||
|
})
|
||||||
|
},
|
||||||
|
})
|
||||||
|
let yielded = Date.now()
|
||||||
|
resetHeartbeat()
|
||||||
|
for await (const event of events.stream) {
|
||||||
|
resetHeartbeat()
|
||||||
|
streamErrorLogged = false
|
||||||
|
const directory = event.directory ?? "global"
|
||||||
|
if (event.payload.type === "sync") {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
const payload = event.payload as Event
|
||||||
|
|
||||||
|
const k = key(directory, payload)
|
||||||
|
if (k) {
|
||||||
|
const i = coalesced.get(k)
|
||||||
|
if (i !== undefined) {
|
||||||
|
queue[i] = { directory, payload }
|
||||||
|
if (payload.type === "message.part.updated") {
|
||||||
|
const part = payload.properties.part
|
||||||
|
staleDeltas.add(deltaKey(directory, part.messageID, part.id))
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
coalesced.set(k, queue.length)
|
||||||
|
}
|
||||||
|
queue.push({ directory, payload })
|
||||||
|
schedule()
|
||||||
|
|
||||||
|
if (Date.now() - yielded < STREAM_YIELD_MS) continue
|
||||||
|
yielded = Date.now()
|
||||||
|
await wait(0)
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
if (!aborted(error) && !streamErrorLogged) {
|
||||||
|
streamErrorLogged = true
|
||||||
|
console.error("[global-sdk] event stream failed", {
|
||||||
|
url: currentServer.http.url,
|
||||||
|
fetch: eventFetch ? "platform" : "webview",
|
||||||
|
error,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
abort.signal.removeEventListener("abort", onAbort)
|
||||||
|
attempt = undefined
|
||||||
|
clearHeartbeat()
|
||||||
|
}
|
||||||
|
|
||||||
|
if (abort.signal.aborted || !started) return
|
||||||
|
await wait(RECONNECT_DELAY_MS)
|
||||||
|
}
|
||||||
|
})().finally(() => {
|
||||||
|
run = undefined
|
||||||
|
flush()
|
||||||
|
})
|
||||||
|
return run
|
||||||
|
}
|
||||||
|
|
||||||
|
const stop = () => {
|
||||||
|
started = false
|
||||||
|
attempt?.abort()
|
||||||
|
clearHeartbeat()
|
||||||
|
}
|
||||||
|
|
||||||
|
onMount(() => {
|
||||||
|
makeEventListener(document, "visibilitychange", () => {
|
||||||
|
if (document.visibilityState !== "visible") return
|
||||||
|
if (!started) return
|
||||||
|
if (Date.now() - lastEventAt < HEARTBEAT_TIMEOUT_MS) return
|
||||||
|
attempt?.abort()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
onCleanup(() => {
|
||||||
|
stop()
|
||||||
|
abort.abort()
|
||||||
|
flush()
|
||||||
|
})
|
||||||
|
|
||||||
|
const sdk = createSdkForServer({
|
||||||
|
server: server.current.http,
|
||||||
|
fetch: platform.fetch,
|
||||||
|
throwOnError: true,
|
||||||
|
})
|
||||||
|
|
||||||
|
const dirSyncContexts = new Map<string, ReturnType<typeof createDirSdkContext>>()
|
||||||
|
const dirSdkContextRefCounts = new Map<string, number>()
|
||||||
|
|
||||||
|
return {
|
||||||
|
url: currentServer.http.url,
|
||||||
|
client: sdk,
|
||||||
|
event: {
|
||||||
|
on: emitter.on.bind(emitter),
|
||||||
|
listen: emitter.listen.bind(emitter),
|
||||||
|
start,
|
||||||
|
},
|
||||||
|
createClient(opts: Omit<Parameters<typeof createSdkForServer>[0], "server" | "fetch">) {
|
||||||
|
const s = server.current
|
||||||
|
if (!s) throw new Error(language.t("error.globalSDK.serverNotAvailable"))
|
||||||
|
return createSdkForServer({
|
||||||
|
server: s.http,
|
||||||
|
fetch: platform.fetch,
|
||||||
|
...opts,
|
||||||
|
})
|
||||||
|
},
|
||||||
|
createDirSyncContext: (directory: string) => {
|
||||||
|
onCleanup(() => {
|
||||||
|
dirSdkContextRefCounts.set(directory, (dirSdkContextRefCounts.get(directory) ?? 0) - 1)
|
||||||
|
if (dirSdkContextRefCounts.get(directory) === 0) {
|
||||||
|
dirSyncContexts.delete(directory)
|
||||||
|
dirSdkContextRefCounts.delete(directory)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const cached = dirSyncContexts.get(directory)
|
||||||
|
if (cached) {
|
||||||
|
dirSdkContextRefCounts.set(directory, (dirSdkContextRefCounts.get(directory) ?? 0) + 1)
|
||||||
|
return cached
|
||||||
|
}
|
||||||
|
const ctx = createDirSdkContext(directory)
|
||||||
|
dirSyncContexts.set(directory, ctx)
|
||||||
|
dirSdkContextRefCounts.set(directory, 1)
|
||||||
|
|
||||||
|
return ctx
|
||||||
|
},
|
||||||
|
}
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
type SDKEventMap = {
|
||||||
|
[key in Event["type"]]: Extract<Event, { type: key }>
|
||||||
|
}
|
||||||
|
|
||||||
|
function createDirSdkContext(directory: string) {
|
||||||
|
const globalSDK = useGlobalSDK()
|
||||||
|
|
||||||
|
const client = globalSDK.createClient({
|
||||||
|
directory,
|
||||||
|
throwOnError: true,
|
||||||
|
})
|
||||||
|
|
||||||
|
const emitter = createGlobalEmitter<SDKEventMap>()
|
||||||
|
|
||||||
|
const unsub = globalSDK.event.on(directory, (event) => {
|
||||||
|
emitter.emit(event.type, event)
|
||||||
|
})
|
||||||
|
onCleanup(unsub)
|
||||||
|
|
||||||
|
return {
|
||||||
|
directory,
|
||||||
|
client,
|
||||||
|
event: emitter,
|
||||||
|
get url() {
|
||||||
|
return globalSDK.url
|
||||||
|
},
|
||||||
|
createClient(opts: Parameters<typeof globalSDK.createClient>[0]) {
|
||||||
|
return globalSDK.createClient(opts)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
+53
-30
@@ -5,7 +5,7 @@ import { batch, createContext, getOwner, onCleanup, onMount, type ParentProps, u
|
|||||||
import { createStore, produce, reconcile } from "solid-js/store"
|
import { createStore, produce, reconcile } from "solid-js/store"
|
||||||
import { useLanguage } from "@/context/language"
|
import { useLanguage } from "@/context/language"
|
||||||
import type { InitError } from "../pages/error"
|
import type { InitError } from "../pages/error"
|
||||||
import { useServerSDK } from "./server-sdk"
|
import { useGlobalSDK } from "./global-sdk"
|
||||||
import {
|
import {
|
||||||
bootstrapDirectory,
|
bootstrapDirectory,
|
||||||
bootstrapGlobal,
|
bootstrapGlobal,
|
||||||
@@ -29,8 +29,7 @@ import { createRefreshQueue } from "./global-sync/queue"
|
|||||||
import { directoryKey } from "./global-sync/utils"
|
import { directoryKey } from "./global-sync/utils"
|
||||||
import { PathKey } from "@/utils/path-key"
|
import { PathKey } from "@/utils/path-key"
|
||||||
import { createDirSyncContext } from "./directory-sync"
|
import { createDirSyncContext } from "./directory-sync"
|
||||||
import { createSimpleContext, NormalizedProviderListResponse } from "@opencode-ai/ui/context"
|
import { NormalizedProviderListResponse } from "@opencode-ai/ui/context"
|
||||||
import { createRefCountMap } from "@/utils/refcount"
|
|
||||||
|
|
||||||
type GlobalStore = {
|
type GlobalStore = {
|
||||||
ready: boolean
|
ready: boolean
|
||||||
@@ -58,13 +57,13 @@ export const loadLspQuery = (directory: string, sdk: OpencodeClient) =>
|
|||||||
queryFn: () => sdk.lsp.status().then((r) => r.data ?? []),
|
queryFn: () => sdk.lsp.status().then((r) => r.data ?? []),
|
||||||
})
|
})
|
||||||
|
|
||||||
function makeQueryOptionsApi(serverSDK: () => OpencodeClient, sdkFor: (dir: PathKey) => OpencodeClient) {
|
function makeQueryOptionsApi(globalSDK: () => OpencodeClient, sdkFor: (dir: PathKey) => OpencodeClient) {
|
||||||
return {
|
return {
|
||||||
globalConfig: () => loadGlobalConfigQuery(serverSDK()),
|
globalConfig: () => loadGlobalConfigQuery(globalSDK()),
|
||||||
projects: () => loadProjectsQuery(serverSDK()),
|
projects: () => loadProjectsQuery(globalSDK()),
|
||||||
providers: (directory: PathKey | null) =>
|
providers: (directory: PathKey | null) =>
|
||||||
loadProvidersQuery(directory, directory === null ? serverSDK() : sdkFor(directory)),
|
loadProvidersQuery(directory, directory === null ? globalSDK() : sdkFor(directory)),
|
||||||
path: (directory: PathKey | null) => loadPathQuery(directory, directory === null ? serverSDK() : sdkFor(directory)),
|
path: (directory: PathKey | null) => loadPathQuery(directory, directory === null ? globalSDK() : sdkFor(directory)),
|
||||||
agents: (directory: PathKey) => loadAgentsQuery(directory, sdkFor(directory)),
|
agents: (directory: PathKey) => loadAgentsQuery(directory, sdkFor(directory)),
|
||||||
mcp: (directory: PathKey) => loadMcpQuery(directory, sdkFor(directory)),
|
mcp: (directory: PathKey) => loadMcpQuery(directory, sdkFor(directory)),
|
||||||
lsp: (directory: PathKey) => loadLspQuery(directory, sdkFor(directory)),
|
lsp: (directory: PathKey) => loadLspQuery(directory, sdkFor(directory)),
|
||||||
@@ -73,11 +72,11 @@ function makeQueryOptionsApi(serverSDK: () => OpencodeClient, sdkFor: (dir: Path
|
|||||||
}
|
}
|
||||||
export type QueryOptionsApi = ReturnType<typeof makeQueryOptionsApi>
|
export type QueryOptionsApi = ReturnType<typeof makeQueryOptionsApi>
|
||||||
|
|
||||||
export function createServerSyncContext() {
|
function createGlobalSync() {
|
||||||
const serverSDK = useServerSDK()
|
const globalSDK = useGlobalSDK()
|
||||||
const language = useLanguage()
|
const language = useLanguage()
|
||||||
const owner = getOwner()
|
const owner = getOwner()
|
||||||
if (!owner) throw new Error("ServerSync must be created within owner")
|
if (!owner) throw new Error("GlobalSync must be created within owner")
|
||||||
|
|
||||||
const sdkCache = new Map<string, OpencodeClient>()
|
const sdkCache = new Map<string, OpencodeClient>()
|
||||||
const booting = new Map<string, Promise<void>>()
|
const booting = new Map<string, Promise<void>>()
|
||||||
@@ -88,7 +87,7 @@ export function createServerSyncContext() {
|
|||||||
const key = directoryKey(directory)
|
const key = directoryKey(directory)
|
||||||
const cached = sdkCache.get(key)
|
const cached = sdkCache.get(key)
|
||||||
if (cached) return cached
|
if (cached) return cached
|
||||||
const sdk = serverSDK.createClient({
|
const sdk = globalSDK.createClient({
|
||||||
directory,
|
directory,
|
||||||
throwOnError: true,
|
throwOnError: true,
|
||||||
})
|
})
|
||||||
@@ -96,7 +95,7 @@ export function createServerSyncContext() {
|
|||||||
return sdk
|
return sdk
|
||||||
}
|
}
|
||||||
|
|
||||||
const queryOptionsApi = makeQueryOptionsApi(() => serverSDK.client, sdkFor)
|
const queryOptionsApi = makeQueryOptionsApi(() => globalSDK.client, sdkFor)
|
||||||
|
|
||||||
const [configQuery, providerQuery, pathQuery] = useQueries(() => ({
|
const [configQuery, providerQuery, pathQuery] = useQueries(() => ({
|
||||||
queries: [queryOptionsApi.globalConfig(), queryOptionsApi.providers(null), queryOptionsApi.path(null)],
|
queries: [queryOptionsApi.globalConfig(), queryOptionsApi.providers(null), queryOptionsApi.path(null)],
|
||||||
@@ -155,7 +154,7 @@ export function createServerSyncContext() {
|
|||||||
queryKey: ["bootstrap"],
|
queryKey: ["bootstrap"],
|
||||||
queryFn: async () => {
|
queryFn: async () => {
|
||||||
await bootstrapGlobal({
|
await bootstrapGlobal({
|
||||||
serverSDK: serverSDK.client,
|
globalSDK: globalSDK.client,
|
||||||
requestFailedTitle: language.t("common.requestFailed"),
|
requestFailedTitle: language.t("common.requestFailed"),
|
||||||
translate: language.t,
|
translate: language.t,
|
||||||
formatMoreCount: (count) => language.t("common.moreCountSuffix", { count }),
|
formatMoreCount: (count) => language.t("common.moreCountSuffix", { count }),
|
||||||
@@ -249,7 +248,7 @@ export function createServerSyncContext() {
|
|||||||
loadRootSessionsWithFallback({
|
loadRootSessionsWithFallback({
|
||||||
directory,
|
directory,
|
||||||
limit,
|
limit,
|
||||||
list: (query) => serverSDK.client.session.list(query),
|
list: (query) => globalSDK.client.session.list(query),
|
||||||
})
|
})
|
||||||
.then((x) => {
|
.then((x) => {
|
||||||
const nonArchived = (x.data ?? [])
|
const nonArchived = (x.data ?? [])
|
||||||
@@ -335,7 +334,7 @@ export function createServerSyncContext() {
|
|||||||
return promise
|
return promise
|
||||||
}
|
}
|
||||||
|
|
||||||
const unsub = serverSDK.event.listen((e) => {
|
const unsub = globalSDK.event.listen((e) => {
|
||||||
const directory = e.name
|
const directory = e.name
|
||||||
const key = directoryKey(directory)
|
const key = directoryKey(directory)
|
||||||
const event = e.details
|
const event = e.details
|
||||||
@@ -394,13 +393,13 @@ export function createServerSyncContext() {
|
|||||||
eventFrame = undefined
|
eventFrame = undefined
|
||||||
eventTimer = setTimeout(() => {
|
eventTimer = setTimeout(() => {
|
||||||
eventTimer = undefined
|
eventTimer = undefined
|
||||||
void serverSDK.event.start()
|
void globalSDK.event.start()
|
||||||
}, 0)
|
}, 0)
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
eventTimer = setTimeout(() => {
|
eventTimer = setTimeout(() => {
|
||||||
eventTimer = undefined
|
eventTimer = undefined
|
||||||
void serverSDK.event.start()
|
void globalSDK.event.start()
|
||||||
}, 0)
|
}, 0)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@@ -416,7 +415,7 @@ export function createServerSyncContext() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const updateConfigMutation = useMutation(() => ({
|
const updateConfigMutation = useMutation(() => ({
|
||||||
mutationFn: (config: Config) => serverSDK.client.global.config.update({ config }),
|
mutationFn: (config: Config) => globalSDK.client.global.config.update({ config }),
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
bootstrap.refetch()
|
bootstrap.refetch()
|
||||||
// Invalidate all provider queries so newly configured custom providers
|
// Invalidate all provider queries so newly configured custom providers
|
||||||
@@ -426,6 +425,9 @@ export function createServerSyncContext() {
|
|||||||
},
|
},
|
||||||
}))
|
}))
|
||||||
|
|
||||||
|
const dirSyncContexts = new Map<string, ReturnType<typeof createDirSyncContext>>()
|
||||||
|
const dirSyncContextRefCounts = new Map<string, number>()
|
||||||
|
|
||||||
return {
|
return {
|
||||||
data: globalStore,
|
data: globalStore,
|
||||||
set,
|
set,
|
||||||
@@ -444,21 +446,42 @@ export function createServerSyncContext() {
|
|||||||
todo: {
|
todo: {
|
||||||
set: setSessionTodo,
|
set: setSessionTodo,
|
||||||
},
|
},
|
||||||
|
createDirSyncContext: (directory: string) => {
|
||||||
|
onCleanup(() => {
|
||||||
|
dirSyncContextRefCounts.set(directory, (dirSyncContextRefCounts.get(directory) ?? 0) - 1)
|
||||||
|
if (dirSyncContextRefCounts.get(directory) === 0) {
|
||||||
|
dirSyncContexts.delete(directory)
|
||||||
|
dirSyncContextRefCounts.delete(directory)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const cached = dirSyncContexts.get(directory)
|
||||||
|
if (cached) {
|
||||||
|
dirSyncContextRefCounts.set(directory, (dirSyncContextRefCounts.get(directory) ?? 0) + 1)
|
||||||
|
return cached
|
||||||
|
}
|
||||||
|
const ctx = createDirSyncContext(globalSDK.createClient({ directory, throwOnError: true }), directory)
|
||||||
|
dirSyncContexts.set(directory, ctx)
|
||||||
|
dirSyncContextRefCounts.set(directory, 1)
|
||||||
|
|
||||||
|
return ctx
|
||||||
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const { use: useServerSync, provider: ServerSyncProvider } = createSimpleContext({
|
const GlobalSyncContext = createContext<ReturnType<typeof createGlobalSync>>()
|
||||||
name: "ServerSync",
|
|
||||||
init: () => {
|
|
||||||
const sync = createServerSyncContext()
|
|
||||||
|
|
||||||
return {
|
export function GlobalSyncProvider(props: ParentProps) {
|
||||||
...sync,
|
const value = createGlobalSync()
|
||||||
createDirSyncContext: createRefCountMap((dir) => createDirSyncContext(dir, sync)),
|
return <GlobalSyncContext.Provider value={value}>{props.children}</GlobalSyncContext.Provider>
|
||||||
}
|
}
|
||||||
},
|
|
||||||
})
|
export function useGlobalSync() {
|
||||||
|
const context = useContext(GlobalSyncContext)
|
||||||
|
if (!context) throw new Error("useGlobalSync must be used within GlobalSyncProvider")
|
||||||
|
return context
|
||||||
|
}
|
||||||
|
|
||||||
export function useQueryOptions() {
|
export function useQueryOptions() {
|
||||||
return useServerSync().queryOptions
|
return useGlobalSync().queryOptions
|
||||||
}
|
}
|
||||||
@@ -1,78 +0,0 @@
|
|||||||
import { describe, expect, test } from "bun:test"
|
|
||||||
import { createStore } from "solid-js/store"
|
|
||||||
import { QueryClient } from "@tanstack/solid-query"
|
|
||||||
import type { Config, OpencodeClient, Project } from "@opencode-ai/sdk/v2/client"
|
|
||||||
import type { NormalizedProviderListResponse } from "@opencode-ai/ui/context"
|
|
||||||
import { bootstrapDirectory } from "./bootstrap"
|
|
||||||
import type { State, VcsCache } from "./types"
|
|
||||||
|
|
||||||
const provider = { all: new Map(), connected: [], default: {} } satisfies NormalizedProviderListResponse
|
|
||||||
|
|
||||||
describe("bootstrapDirectory", () => {
|
|
||||||
test("marks a loading directory partial during bootstrap and complete after success", async () => {
|
|
||||||
const [store, setStore] = createStore<State>({
|
|
||||||
status: "loading",
|
|
||||||
agent: [],
|
|
||||||
command: [],
|
|
||||||
project: "",
|
|
||||||
projectMeta: undefined,
|
|
||||||
icon: undefined,
|
|
||||||
provider_ready: true,
|
|
||||||
provider,
|
|
||||||
config: {},
|
|
||||||
path: { state: "", config: "", worktree: "/project", directory: "/project", home: "/home" },
|
|
||||||
session: [],
|
|
||||||
sessionTotal: 0,
|
|
||||||
session_status: {},
|
|
||||||
session_working(id: string) {
|
|
||||||
return this.session_status[id]?.type !== "idle"
|
|
||||||
},
|
|
||||||
session_diff: {},
|
|
||||||
todo: {},
|
|
||||||
permission: {},
|
|
||||||
question: {},
|
|
||||||
mcp_ready: true,
|
|
||||||
mcp: {},
|
|
||||||
lsp_ready: true,
|
|
||||||
lsp: [],
|
|
||||||
vcs: undefined,
|
|
||||||
limit: 5,
|
|
||||||
message: {},
|
|
||||||
part: {},
|
|
||||||
part_text_accum_delta: {},
|
|
||||||
})
|
|
||||||
|
|
||||||
await bootstrapDirectory({
|
|
||||||
directory: "/project",
|
|
||||||
global: {
|
|
||||||
config: {} satisfies Config,
|
|
||||||
path: { state: "", config: "", worktree: "/project", directory: "/project", home: "/home" },
|
|
||||||
project: [{ id: "project", worktree: "/project" } as Project],
|
|
||||||
provider,
|
|
||||||
},
|
|
||||||
sdk: {
|
|
||||||
app: { agents: async () => ({ data: [{ name: "build", mode: "primary" }] }) },
|
|
||||||
config: { get: async () => ({ data: {} }) },
|
|
||||||
session: { status: async () => ({ data: {} }) },
|
|
||||||
vcs: { get: async () => ({ data: undefined }) },
|
|
||||||
command: { list: async () => ({ data: [] }) },
|
|
||||||
permission: { list: async () => ({ data: [] }) },
|
|
||||||
question: { list: async () => ({ data: [] }) },
|
|
||||||
mcp: { status: async () => ({ data: {} }) },
|
|
||||||
provider: { list: async () => ({ data: { all: [], connected: [], default: {} } }) },
|
|
||||||
} as unknown as OpencodeClient,
|
|
||||||
store,
|
|
||||||
setStore,
|
|
||||||
vcsCache: { setStore() {} } as unknown as VcsCache,
|
|
||||||
loadSessions() {},
|
|
||||||
translate: (key) => key,
|
|
||||||
queryClient: new QueryClient(),
|
|
||||||
})
|
|
||||||
|
|
||||||
expect(store.status).toBe("partial")
|
|
||||||
|
|
||||||
await new Promise((resolve) => setTimeout(resolve, 80))
|
|
||||||
|
|
||||||
expect(store.status).toBe("complete")
|
|
||||||
})
|
|
||||||
})
|
|
||||||
@@ -18,7 +18,7 @@ import type { State, VcsCache } from "./types"
|
|||||||
import { cmp, normalizeAgentList, normalizeProviderList } from "./utils"
|
import { cmp, normalizeAgentList, normalizeProviderList } from "./utils"
|
||||||
import { formatServerError } from "@/utils/server-errors"
|
import { formatServerError } from "@/utils/server-errors"
|
||||||
import { QueryClient, queryOptions } from "@tanstack/solid-query"
|
import { QueryClient, queryOptions } from "@tanstack/solid-query"
|
||||||
import { loadMcpQuery } from "../server-sync"
|
import { loadMcpQuery } from "../global-sync"
|
||||||
import { NormalizedProviderListResponse } from "@opencode-ai/ui/context"
|
import { NormalizedProviderListResponse } from "@opencode-ai/ui/context"
|
||||||
|
|
||||||
type GlobalStore = {
|
type GlobalStore = {
|
||||||
@@ -105,7 +105,7 @@ export const loadProjectsQuery = (sdk: OpencodeClient) =>
|
|||||||
})
|
})
|
||||||
|
|
||||||
export async function bootstrapGlobal(input: {
|
export async function bootstrapGlobal(input: {
|
||||||
serverSDK: OpencodeClient
|
globalSDK: OpencodeClient
|
||||||
requestFailedTitle: string
|
requestFailedTitle: string
|
||||||
translate: (key: string, vars?: Record<string, string | number>) => string
|
translate: (key: string, vars?: Record<string, string | number>) => string
|
||||||
formatMoreCount: (count: number) => string
|
formatMoreCount: (count: number) => string
|
||||||
@@ -113,12 +113,12 @@ export async function bootstrapGlobal(input: {
|
|||||||
queryClient: QueryClient
|
queryClient: QueryClient
|
||||||
}) {
|
}) {
|
||||||
const slow = [
|
const slow = [
|
||||||
() => input.queryClient.fetchQuery(loadGlobalConfigQuery(input.serverSDK)),
|
() => input.queryClient.fetchQuery(loadGlobalConfigQuery(input.globalSDK)),
|
||||||
() => input.queryClient.fetchQuery(loadProvidersQuery(null, input.serverSDK)),
|
() => input.queryClient.fetchQuery(loadProvidersQuery(null, input.globalSDK)),
|
||||||
() => input.queryClient.fetchQuery(loadPathQuery(null, input.serverSDK)),
|
() => input.queryClient.fetchQuery(loadPathQuery(null, input.globalSDK)),
|
||||||
() =>
|
() =>
|
||||||
input.queryClient
|
input.queryClient
|
||||||
.fetchQuery(loadProjectsQuery(input.serverSDK))
|
.fetchQuery(loadProjectsQuery(input.globalSDK))
|
||||||
.then((data) => input.setGlobalStore("project", data)),
|
.then((data) => input.setGlobalStore("project", data)),
|
||||||
]
|
]
|
||||||
await runAll(slow)
|
await runAll(slow)
|
||||||
@@ -220,7 +220,6 @@ export async function bootstrapDirectory(input: {
|
|||||||
if (Object.keys(input.store.config).length === 0 && Object.keys(input.global.config).length > 0) {
|
if (Object.keys(input.store.config).length === 0 && Object.keys(input.global.config).length > 0) {
|
||||||
input.setStore("config", reconcile(input.global.config, { merge: false }))
|
input.setStore("config", reconcile(input.global.config, { merge: false }))
|
||||||
}
|
}
|
||||||
if (loading) input.setStore("status", "partial")
|
|
||||||
|
|
||||||
const rev = (providerRev.get(input.directory) ?? 0) + 1
|
const rev = (providerRev.get(input.directory) ?? 0) + 1
|
||||||
providerRev.set(input.directory, rev)
|
providerRev.set(input.directory, rev)
|
||||||
@@ -327,7 +326,5 @@ export async function bootstrapDirectory(input: {
|
|||||||
description: formatServerError(slowErrs[0], input.translate),
|
description: formatServerError(slowErrs[0], input.translate),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
if (loading && slowErrs.length === 0) input.setStore("status", "complete")
|
|
||||||
})()
|
})()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,63 +1,10 @@
|
|||||||
import { beforeAll, describe, expect, mock, test } from "bun:test"
|
import { describe, expect, test } from "bun:test"
|
||||||
import { createRoot, getOwner, type Owner } from "solid-js"
|
import { createRoot, getOwner } from "solid-js"
|
||||||
import { createStore } from "solid-js/store"
|
import { createStore } from "solid-js/store"
|
||||||
import type { NormalizedProviderListResponse } from "@opencode-ai/ui/context"
|
|
||||||
import type { State } from "./types"
|
import type { State } from "./types"
|
||||||
import type { QueryOptionsApi } from "../server-sync"
|
import { createChildStoreManager } from "./child-store"
|
||||||
|
|
||||||
let createChildStoreManager: typeof import("./child-store").createChildStoreManager
|
|
||||||
|
|
||||||
const child = () => createStore({} as State)
|
const child = () => createStore({} as State)
|
||||||
const provider = { all: new Map(), connected: [], default: {} } satisfies NormalizedProviderListResponse
|
|
||||||
|
|
||||||
const queryOptionsApi = {
|
|
||||||
globalConfig: () => ({ queryKey: ["globalConfig"], queryFn: async () => ({}) }),
|
|
||||||
projects: () => ({ queryKey: ["projects"], queryFn: async () => [] }),
|
|
||||||
providers: (directory: string | null) => ({ queryKey: [directory, "providers"], queryFn: async () => provider }),
|
|
||||||
path: (directory: string | null) => ({
|
|
||||||
queryKey: [directory, "path"],
|
|
||||||
queryFn: async () => ({
|
|
||||||
state: "",
|
|
||||||
config: "",
|
|
||||||
worktree: "",
|
|
||||||
directory: directory ?? "",
|
|
||||||
home: "",
|
|
||||||
}),
|
|
||||||
}),
|
|
||||||
agents: (directory: string) => ({ queryKey: [directory, "agents"], queryFn: async () => [] }),
|
|
||||||
mcp: (directory: string) => ({ queryKey: [directory, "mcp"], queryFn: async () => ({}) }),
|
|
||||||
lsp: (directory: string) => ({ queryKey: [directory, "lsp"], queryFn: async () => [] }),
|
|
||||||
sessions: (directory: string) => ({ queryKey: [directory, "loadSessions"] as const }),
|
|
||||||
} as unknown as QueryOptionsApi
|
|
||||||
|
|
||||||
function createOwner(callback: (owner: Owner) => void) {
|
|
||||||
return createRoot((dispose) => {
|
|
||||||
const owner = getOwner()
|
|
||||||
if (!owner) throw new Error("owner required")
|
|
||||||
callback(owner)
|
|
||||||
|
|
||||||
return dispose
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
beforeAll(async () => {
|
|
||||||
mock.module("@/utils/persist", () => ({
|
|
||||||
Persist: {
|
|
||||||
workspace: (...parts: string[]) => parts.join(":"),
|
|
||||||
},
|
|
||||||
persisted: (_target: string, store: unknown[]) => [store[0], store[1], null, () => true],
|
|
||||||
}))
|
|
||||||
mock.module("@tanstack/solid-query", () => ({
|
|
||||||
useQueries: () => [
|
|
||||||
{ isLoading: false, data: { state: "", config: "", worktree: "", directory: "", home: "" } },
|
|
||||||
{ isLoading: false, data: {} },
|
|
||||||
{ isLoading: false, data: [] },
|
|
||||||
{ isLoading: false, data: provider },
|
|
||||||
],
|
|
||||||
}))
|
|
||||||
|
|
||||||
createChildStoreManager = (await import("./child-store")).createChildStoreManager
|
|
||||||
})
|
|
||||||
|
|
||||||
describe("createChildStoreManager", () => {
|
describe("createChildStoreManager", () => {
|
||||||
test("does not evict the active directory during mark", () => {
|
test("does not evict the active directory during mark", () => {
|
||||||
@@ -75,8 +22,8 @@ describe("createChildStoreManager", () => {
|
|||||||
onBootstrap() {},
|
onBootstrap() {},
|
||||||
onDispose() {},
|
onDispose() {},
|
||||||
translate: (key) => key,
|
translate: (key) => key,
|
||||||
queryOptions: queryOptionsApi,
|
queryOptions: {} as any,
|
||||||
global: { provider },
|
global: { provider: null! },
|
||||||
})
|
})
|
||||||
|
|
||||||
Array.from({ length: 30 }, (_, index) => `/pinned-${index}`).forEach((directory) => {
|
Array.from({ length: 30 }, (_, index) => `/pinned-${index}`).forEach((directory) => {
|
||||||
@@ -90,35 +37,4 @@ describe("createChildStoreManager", () => {
|
|||||||
|
|
||||||
expect(manager.children[directory]).toBeDefined()
|
expect(manager.children[directory]).toBeDefined()
|
||||||
})
|
})
|
||||||
|
|
||||||
test("starts new child stores as loading and bootstraps them on first access", () => {
|
|
||||||
const bootstraps: string[] = []
|
|
||||||
let manager: ReturnType<typeof createChildStoreManager> | undefined
|
|
||||||
|
|
||||||
const dispose = createOwner((owner) => {
|
|
||||||
manager = createChildStoreManager({
|
|
||||||
owner,
|
|
||||||
isBooting: () => false,
|
|
||||||
isLoadingSessions: () => false,
|
|
||||||
onBootstrap(directory) {
|
|
||||||
bootstraps.push(directory)
|
|
||||||
},
|
|
||||||
onDispose() {},
|
|
||||||
translate: (key) => key,
|
|
||||||
queryOptions: queryOptionsApi,
|
|
||||||
global: { provider },
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
try {
|
|
||||||
if (!manager) throw new Error("manager required")
|
|
||||||
|
|
||||||
const [store] = manager.child("/project")
|
|
||||||
|
|
||||||
expect(store.status).toBe("loading")
|
|
||||||
expect(bootstraps).toEqual(["/project"])
|
|
||||||
} finally {
|
|
||||||
dispose()
|
|
||||||
}
|
|
||||||
})
|
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ import {
|
|||||||
} from "./types"
|
} from "./types"
|
||||||
import { canDisposeDirectory, pickDirectoriesToEvict } from "./eviction"
|
import { canDisposeDirectory, pickDirectoriesToEvict } from "./eviction"
|
||||||
import { useQueries } from "@tanstack/solid-query"
|
import { useQueries } from "@tanstack/solid-query"
|
||||||
import { QueryOptionsApi } from "../server-sync"
|
import { QueryOptionsApi } from "../global-sync"
|
||||||
import { directoryKey, type DirectoryKey } from "./utils"
|
import { directoryKey, type DirectoryKey } from "./utils"
|
||||||
import { NormalizedProviderListResponse } from "@opencode-ai/ui/context"
|
import { NormalizedProviderListResponse } from "@opencode-ai/ui/context"
|
||||||
|
|
||||||
@@ -202,7 +202,7 @@ export function createChildStoreManager(input: {
|
|||||||
return { state: "", config: "", worktree: "", directory: "", home: "" }
|
return { state: "", config: "", worktree: "", directory: "", home: "" }
|
||||||
return pathQuery.data
|
return pathQuery.data
|
||||||
},
|
},
|
||||||
status: "loading" as const,
|
status: "complete" as const,
|
||||||
agent: [],
|
agent: [],
|
||||||
command: [],
|
command: [],
|
||||||
session: [],
|
session: [],
|
||||||
|
|||||||
@@ -2,8 +2,8 @@ import { createStore, produce } from "solid-js/store"
|
|||||||
import { batch, createEffect, createMemo, onCleanup, onMount, type Accessor } from "solid-js"
|
import { batch, createEffect, createMemo, onCleanup, onMount, type Accessor } from "solid-js"
|
||||||
import { createSimpleContext } from "@opencode-ai/ui/context"
|
import { createSimpleContext } from "@opencode-ai/ui/context"
|
||||||
import { makeEventListener } from "@solid-primitives/event-listener"
|
import { makeEventListener } from "@solid-primitives/event-listener"
|
||||||
import { useServerSync } from "./server-sync"
|
import { useGlobalSync } from "./global-sync"
|
||||||
import { useServerSDK } from "./server-sdk"
|
import { useGlobalSDK } from "./global-sdk"
|
||||||
import { useServer } from "./server"
|
import { useServer } from "./server"
|
||||||
import { usePlatform } from "./platform"
|
import { usePlatform } from "./platform"
|
||||||
import { Project } from "@opencode-ai/sdk/v2"
|
import { Project } from "@opencode-ai/sdk/v2"
|
||||||
@@ -136,8 +136,8 @@ const normalizeStoredSessionTabs = (key: string, tabs: SessionTabs) => {
|
|||||||
export const { use: useLayout, provider: LayoutProvider } = createSimpleContext({
|
export const { use: useLayout, provider: LayoutProvider } = createSimpleContext({
|
||||||
name: "Layout",
|
name: "Layout",
|
||||||
init: () => {
|
init: () => {
|
||||||
const globalSdk = useServerSDK()
|
const globalSdk = useGlobalSDK()
|
||||||
const serverSync = useServerSync()
|
const globalSync = useGlobalSync()
|
||||||
const server = useServer()
|
const server = useServer()
|
||||||
const platform = usePlatform()
|
const platform = usePlatform()
|
||||||
|
|
||||||
@@ -386,11 +386,11 @@ export const { use: useLayout, provider: LayoutProvider } = createSimpleContext(
|
|||||||
}
|
}
|
||||||
|
|
||||||
function enrich(project: { worktree: string; expanded: boolean }) {
|
function enrich(project: { worktree: string; expanded: boolean }) {
|
||||||
const [childStore] = serverSync.child(project.worktree, { bootstrap: false })
|
const [childStore] = globalSync.child(project.worktree, { bootstrap: false })
|
||||||
const projectID = childStore.project
|
const projectID = childStore.project
|
||||||
const metadata = projectID
|
const metadata = projectID
|
||||||
? serverSync.data.project.find((x) => x.id === projectID)
|
? globalSync.data.project.find((x) => x.id === projectID)
|
||||||
: serverSync.data.project.find((x) => x.worktree === project.worktree)
|
: globalSync.data.project.find((x) => x.worktree === project.worktree)
|
||||||
|
|
||||||
// Preserve local icon override from per-workspace localStorage cache (childStore.icon).
|
// Preserve local icon override from per-workspace localStorage cache (childStore.icon).
|
||||||
// Without this, different subdirectories of the same git repo would share the same
|
// Without this, different subdirectories of the same git repo would share the same
|
||||||
@@ -404,7 +404,7 @@ export const { use: useLayout, provider: LayoutProvider } = createSimpleContext(
|
|||||||
|
|
||||||
const roots = createMemo(() => {
|
const roots = createMemo(() => {
|
||||||
const map = new Map<string, string>()
|
const map = new Map<string, string>()
|
||||||
for (const project of serverSync.data.project) {
|
for (const project of globalSync.data.project) {
|
||||||
const sandboxes = project.sandboxes ?? []
|
const sandboxes = project.sandboxes ?? []
|
||||||
for (const sandbox of sandboxes) {
|
for (const sandbox of sandboxes) {
|
||||||
map.set(sandbox, project.worktree)
|
map.set(sandbox, project.worktree)
|
||||||
@@ -470,12 +470,12 @@ export const { use: useLayout, provider: LayoutProvider } = createSimpleContext(
|
|||||||
createEffect(() => {
|
createEffect(() => {
|
||||||
const projects = enriched()
|
const projects = enriched()
|
||||||
if (projects.length === 0) return
|
if (projects.length === 0) return
|
||||||
if (!serverSync.ready) return
|
if (!globalSync.ready) return
|
||||||
|
|
||||||
for (const project of projects) {
|
for (const project of projects) {
|
||||||
if (!project.id) continue
|
if (!project.id) continue
|
||||||
if (project.id === "global") continue
|
if (project.id === "global") continue
|
||||||
serverSync.project.icon(project.worktree, project.icon?.override)
|
globalSync.project.icon(project.worktree, project.icon?.override)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -509,7 +509,7 @@ export const { use: useLayout, provider: LayoutProvider } = createSimpleContext(
|
|||||||
colorRequested.set(worktree, color)
|
colorRequested.set(worktree, color)
|
||||||
|
|
||||||
if (project.id === "global") {
|
if (project.id === "global") {
|
||||||
serverSync.project.meta(worktree, { icon: { color } })
|
globalSync.project.meta(worktree, { icon: { color } })
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -531,7 +531,7 @@ export const { use: useLayout, provider: LayoutProvider } = createSimpleContext(
|
|||||||
sessionTimer = undefined
|
sessionTimer = undefined
|
||||||
void Promise.all(
|
void Promise.all(
|
||||||
server.projects.list().map((project) => {
|
server.projects.list().map((project) => {
|
||||||
return serverSync.project.loadSessions(project.worktree)
|
return globalSync.project.loadSessions(project.worktree)
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
}, 0)
|
}, 0)
|
||||||
@@ -560,7 +560,7 @@ export const { use: useLayout, provider: LayoutProvider } = createSimpleContext(
|
|||||||
open(directory: string) {
|
open(directory: string) {
|
||||||
const root = rootFor(directory)
|
const root = rootFor(directory)
|
||||||
if (server.projects.list().find((x) => x.worktree === root)) return
|
if (server.projects.list().find((x) => x.worktree === root)) return
|
||||||
void serverSync.project.loadSessions(root)
|
void globalSync.project.loadSessions(root)
|
||||||
server.projects.open(root)
|
server.projects.open(root)
|
||||||
},
|
},
|
||||||
close(directory: string) {
|
close(directory: string) {
|
||||||
|
|||||||
@@ -2,8 +2,8 @@ import { createStore, reconcile } from "solid-js/store"
|
|||||||
import { batch, createEffect, createMemo, onCleanup } from "solid-js"
|
import { batch, createEffect, createMemo, onCleanup } from "solid-js"
|
||||||
import { useParams } from "@solidjs/router"
|
import { useParams } from "@solidjs/router"
|
||||||
import { createSimpleContext } from "@opencode-ai/ui/context"
|
import { createSimpleContext } from "@opencode-ai/ui/context"
|
||||||
import { useServerSDK } from "./server-sdk"
|
import { useGlobalSDK } from "./global-sdk"
|
||||||
import { useServerSync } from "./server-sync"
|
import { useGlobalSync } from "./global-sync"
|
||||||
import { usePlatform } from "@/context/platform"
|
import { usePlatform } from "@/context/platform"
|
||||||
import { useLanguage } from "@/context/language"
|
import { useLanguage } from "@/context/language"
|
||||||
import { useSettings } from "@/context/settings"
|
import { useSettings } from "@/context/settings"
|
||||||
@@ -109,8 +109,8 @@ export const { use: useNotification, provider: NotificationProvider } = createSi
|
|||||||
name: "Notification",
|
name: "Notification",
|
||||||
init: () => {
|
init: () => {
|
||||||
const params = useParams()
|
const params = useParams()
|
||||||
const serverSDK = useServerSDK()
|
const globalSDK = useGlobalSDK()
|
||||||
const serverSync = useServerSync()
|
const globalSync = useGlobalSync()
|
||||||
const platform = usePlatform()
|
const platform = usePlatform()
|
||||||
const settings = useSettings()
|
const settings = useSettings()
|
||||||
const language = useLanguage()
|
const language = useLanguage()
|
||||||
@@ -207,10 +207,10 @@ export const { use: useNotification, provider: NotificationProvider } = createSi
|
|||||||
|
|
||||||
const lookup = async (directory: string, sessionID?: string) => {
|
const lookup = async (directory: string, sessionID?: string) => {
|
||||||
if (!sessionID) return undefined
|
if (!sessionID) return undefined
|
||||||
const [syncStore] = serverSync.child(directory, { bootstrap: false })
|
const [syncStore] = globalSync.child(directory, { bootstrap: false })
|
||||||
const match = Binary.search(syncStore.session, sessionID, (s) => s.id)
|
const match = Binary.search(syncStore.session, sessionID, (s) => s.id)
|
||||||
if (match.found) return syncStore.session[match.index]
|
if (match.found) return syncStore.session[match.index]
|
||||||
return serverSDK.client.session
|
return globalSDK.client.session
|
||||||
.get({ directory, sessionID })
|
.get({ directory, sessionID })
|
||||||
.then((x) => x.data)
|
.then((x) => x.data)
|
||||||
.catch(() => undefined)
|
.catch(() => undefined)
|
||||||
@@ -285,7 +285,7 @@ export const { use: useNotification, provider: NotificationProvider } = createSi
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
const unsub = serverSDK.event.listen((e) => {
|
const unsub = globalSDK.event.listen((e) => {
|
||||||
const event = e.details
|
const event = e.details
|
||||||
if (event.type !== "session.idle" && event.type !== "session.error") return
|
if (event.type !== "session.idle" && event.type !== "session.error") return
|
||||||
|
|
||||||
|
|||||||
@@ -3,8 +3,8 @@ import { createStore, produce } from "solid-js/store"
|
|||||||
import { createSimpleContext } from "@opencode-ai/ui/context"
|
import { createSimpleContext } from "@opencode-ai/ui/context"
|
||||||
import type { PermissionRequest } from "@opencode-ai/sdk/v2/client"
|
import type { PermissionRequest } from "@opencode-ai/sdk/v2/client"
|
||||||
import { Persist, persisted } from "@/utils/persist"
|
import { Persist, persisted } from "@/utils/persist"
|
||||||
import { useServerSDK } from "@/context/server-sdk"
|
import { useGlobalSDK } from "@/context/global-sdk"
|
||||||
import { useServerSync } from "./server-sync"
|
import { useGlobalSync } from "./global-sync"
|
||||||
import { useParams } from "@solidjs/router"
|
import { useParams } from "@solidjs/router"
|
||||||
import { decode64 } from "@/utils/base64"
|
import { decode64 } from "@/utils/base64"
|
||||||
import {
|
import {
|
||||||
@@ -48,13 +48,13 @@ export const { use: usePermission, provider: PermissionProvider } = createSimple
|
|||||||
name: "Permission",
|
name: "Permission",
|
||||||
init: () => {
|
init: () => {
|
||||||
const params = useParams()
|
const params = useParams()
|
||||||
const serverSDK = useServerSDK()
|
const globalSDK = useGlobalSDK()
|
||||||
const serverSync = useServerSync()
|
const globalSync = useGlobalSync()
|
||||||
|
|
||||||
const permissionsEnabled = createMemo(() => {
|
const permissionsEnabled = createMemo(() => {
|
||||||
const directory = decode64(params.dir)
|
const directory = decode64(params.dir)
|
||||||
if (!directory) return false
|
if (!directory) return false
|
||||||
const [store] = serverSync.child(directory)
|
const [store] = globalSync.child(directory)
|
||||||
return hasPermissionPromptRules(store.config.permission)
|
return hasPermissionPromptRules(store.config.permission)
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -86,7 +86,7 @@ export const { use: usePermission, provider: PermissionProvider } = createSimple
|
|||||||
if (!ready()) return
|
if (!ready()) return
|
||||||
const directory = decode64(params.dir)
|
const directory = decode64(params.dir)
|
||||||
if (!directory) return
|
if (!directory) return
|
||||||
const [childStore] = serverSync.child(directory)
|
const [childStore] = globalSync.child(directory)
|
||||||
const perm = childStore.config.permission
|
const perm = childStore.config.permission
|
||||||
if (typeof perm === "string" && perm === "allow") {
|
if (typeof perm === "string" && perm === "allow") {
|
||||||
const key = directoryAcceptKey(directory)
|
const key = directoryAcceptKey(directory)
|
||||||
@@ -118,7 +118,7 @@ export const { use: usePermission, provider: PermissionProvider } = createSimple
|
|||||||
}
|
}
|
||||||
|
|
||||||
const respond: PermissionRespondFn = (input) => {
|
const respond: PermissionRespondFn = (input) => {
|
||||||
serverSDK.client.permission.respond(input).catch(() => {
|
globalSDK.client.permission.respond(input).catch(() => {
|
||||||
responded.delete(input.permissionID)
|
responded.delete(input.permissionID)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -139,7 +139,7 @@ export const { use: usePermission, provider: PermissionProvider } = createSimple
|
|||||||
}
|
}
|
||||||
|
|
||||||
function isAutoAccepting(sessionID: string, directory?: string) {
|
function isAutoAccepting(sessionID: string, directory?: string) {
|
||||||
const session = directory ? serverSync.child(directory, { bootstrap: false })[0].session : []
|
const session = directory ? globalSync.child(directory, { bootstrap: false })[0].session : []
|
||||||
return autoRespondsPermission(store.autoAccept, session, { sessionID }, directory)
|
return autoRespondsPermission(store.autoAccept, session, { sessionID }, directory)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -148,7 +148,7 @@ export const { use: usePermission, provider: PermissionProvider } = createSimple
|
|||||||
}
|
}
|
||||||
|
|
||||||
function shouldAutoRespond(permission: PermissionRequest, directory?: string) {
|
function shouldAutoRespond(permission: PermissionRequest, directory?: string) {
|
||||||
const session = directory ? serverSync.child(directory, { bootstrap: false })[0].session : []
|
const session = directory ? globalSync.child(directory, { bootstrap: false })[0].session : []
|
||||||
return autoRespondsPermission(store.autoAccept, session, permission, directory)
|
return autoRespondsPermission(store.autoAccept, session, permission, directory)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -159,7 +159,7 @@ export const { use: usePermission, provider: PermissionProvider } = createSimple
|
|||||||
return next
|
return next
|
||||||
}
|
}
|
||||||
|
|
||||||
const unsubscribe = serverSDK.event.listen((e) => {
|
const unsubscribe = globalSDK.event.listen((e) => {
|
||||||
const event = e.details
|
const event = e.details
|
||||||
if (event?.type !== "permission.asked") return
|
if (event?.type !== "permission.asked") return
|
||||||
|
|
||||||
@@ -178,7 +178,7 @@ export const { use: usePermission, provider: PermissionProvider } = createSimple
|
|||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
|
|
||||||
serverSDK.client.permission
|
globalSDK.client.permission
|
||||||
.list({ directory })
|
.list({ directory })
|
||||||
.then((x) => {
|
.then((x) => {
|
||||||
if (!isAutoAcceptingDirectory(directory)) return
|
if (!isAutoAcceptingDirectory(directory)) return
|
||||||
@@ -210,7 +210,7 @@ export const { use: usePermission, provider: PermissionProvider } = createSimple
|
|||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
|
|
||||||
serverSDK.client.permission
|
globalSDK.client.permission
|
||||||
.list({ directory })
|
.list({ directory })
|
||||||
.then((x) => {
|
.then((x) => {
|
||||||
if (enableVersion.get(key) !== version) return
|
if (enableVersion.get(key) !== version) return
|
||||||
@@ -268,7 +268,7 @@ export const { use: usePermission, provider: PermissionProvider } = createSimple
|
|||||||
},
|
},
|
||||||
permissionsEnabled,
|
permissionsEnabled,
|
||||||
isPermissionAllowAll(directory: string) {
|
isPermissionAllowAll(directory: string) {
|
||||||
const [childStore] = serverSync.child(directory)
|
const [childStore] = globalSync.child(directory)
|
||||||
const perm = childStore.config.permission
|
const perm = childStore.config.permission
|
||||||
return typeof perm === "string" && perm === "allow"
|
return typeof perm === "string" && perm === "allow"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
import { createSimpleContext } from "@opencode-ai/ui/context"
|
import { createSimpleContext } from "@opencode-ai/ui/context"
|
||||||
import { useServerSDK } from "./server-sdk"
|
import { useGlobalSDK } from "./global-sdk"
|
||||||
|
|
||||||
export const { use: useSDK, provider: SDKProvider } = createSimpleContext({
|
export const { use: useSDK, provider: SDKProvider } = createSimpleContext({
|
||||||
name: "SDK",
|
name: "SDK",
|
||||||
init: (props: { directory: string }) => {
|
init: (props: { directory: string }) => {
|
||||||
const serverSDK = useServerSDK()
|
const globalSDK = useGlobalSDK()
|
||||||
|
|
||||||
return serverSDK.createDirSdkContext(props.directory)
|
return globalSDK.createDirSyncContext(props.directory)
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,290 +0,0 @@
|
|||||||
import type { Event } from "@opencode-ai/sdk/v2/client"
|
|
||||||
import { createSimpleContext } from "@opencode-ai/ui/context"
|
|
||||||
import { createGlobalEmitter } from "@solid-primitives/event-bus"
|
|
||||||
import { makeEventListener } from "@solid-primitives/event-listener"
|
|
||||||
import { batch, onCleanup, onMount } from "solid-js"
|
|
||||||
import { createSdkForServer } from "@/utils/server"
|
|
||||||
import { useLanguage } from "./language"
|
|
||||||
import { usePlatform } from "./platform"
|
|
||||||
import { ServerConnection, useServer } from "./server"
|
|
||||||
import { createRefCountMap } from "@/utils/refcount"
|
|
||||||
|
|
||||||
const isAbortError = (error: unknown) =>
|
|
||||||
error !== null && typeof error === "object" && "name" in error && error.name === "AbortError"
|
|
||||||
|
|
||||||
function createServerSdkContext(server: ServerConnection.Any) {
|
|
||||||
const platform = usePlatform()
|
|
||||||
const abort = new AbortController()
|
|
||||||
|
|
||||||
const eventFetch = (() => {
|
|
||||||
if (!platform.fetch || !server) return
|
|
||||||
try {
|
|
||||||
const url = new URL(server.http.url)
|
|
||||||
const loopback = url.hostname === "localhost" || url.hostname === "127.0.0.1" || url.hostname === "::1"
|
|
||||||
if (url.protocol === "http:" && !loopback) return platform.fetch
|
|
||||||
} catch {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
})()
|
|
||||||
|
|
||||||
const eventSdk = createSdkForServer({
|
|
||||||
signal: abort.signal,
|
|
||||||
fetch: eventFetch,
|
|
||||||
server: server.http,
|
|
||||||
})
|
|
||||||
const emitter = createGlobalEmitter<{
|
|
||||||
[key: string]: Event
|
|
||||||
}>()
|
|
||||||
|
|
||||||
type Queued = { directory: string; payload: Event }
|
|
||||||
const FLUSH_FRAME_MS = 16
|
|
||||||
const STREAM_YIELD_MS = 8
|
|
||||||
const RECONNECT_DELAY_MS = 250
|
|
||||||
|
|
||||||
let queue: Queued[] = []
|
|
||||||
let buffer: Queued[] = []
|
|
||||||
const coalesced = new Map<string, number>()
|
|
||||||
const staleDeltas = new Set<string>()
|
|
||||||
let timer: ReturnType<typeof setTimeout> | undefined
|
|
||||||
let last = 0
|
|
||||||
|
|
||||||
const deltaKey = (directory: string, messageID: string, partID: string) => `${directory}:${messageID}:${partID}`
|
|
||||||
|
|
||||||
const key = (directory: string, payload: Event) => {
|
|
||||||
if (payload.type === "session.status") return `session.status:${directory}:${payload.properties.sessionID}`
|
|
||||||
if (payload.type === "lsp.updated") return `lsp.updated:${directory}`
|
|
||||||
if (payload.type === "message.part.updated") {
|
|
||||||
const part = payload.properties.part
|
|
||||||
return `message.part.updated:${directory}:${part.messageID}:${part.id}`
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const flush = () => {
|
|
||||||
if (timer) clearTimeout(timer)
|
|
||||||
timer = undefined
|
|
||||||
|
|
||||||
if (queue.length === 0) return
|
|
||||||
|
|
||||||
const events = queue
|
|
||||||
const skip = staleDeltas.size > 0 ? new Set(staleDeltas) : undefined
|
|
||||||
queue = buffer
|
|
||||||
buffer = events
|
|
||||||
queue.length = 0
|
|
||||||
coalesced.clear()
|
|
||||||
staleDeltas.clear()
|
|
||||||
|
|
||||||
last = Date.now()
|
|
||||||
batch(() => {
|
|
||||||
for (const event of events) {
|
|
||||||
if (skip && event.payload.type === "message.part.delta") {
|
|
||||||
const props = event.payload.properties
|
|
||||||
if (skip.has(deltaKey(event.directory, props.messageID, props.partID))) continue
|
|
||||||
}
|
|
||||||
emitter.emit(event.directory, event.payload)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
buffer.length = 0
|
|
||||||
}
|
|
||||||
|
|
||||||
const schedule = () => {
|
|
||||||
if (timer) return
|
|
||||||
const elapsed = Date.now() - last
|
|
||||||
timer = setTimeout(flush, Math.max(0, FLUSH_FRAME_MS - elapsed))
|
|
||||||
}
|
|
||||||
|
|
||||||
let streamErrorLogged = false
|
|
||||||
const wait = (ms: number) => new Promise<void>((resolve) => setTimeout(resolve, ms))
|
|
||||||
const aborted = isAbortError
|
|
||||||
|
|
||||||
let attempt: AbortController | undefined
|
|
||||||
let run: Promise<void> | undefined
|
|
||||||
let started = false
|
|
||||||
const HEARTBEAT_TIMEOUT_MS = 15_000
|
|
||||||
let lastEventAt = Date.now()
|
|
||||||
let heartbeat: ReturnType<typeof setTimeout> | undefined
|
|
||||||
const resetHeartbeat = () => {
|
|
||||||
lastEventAt = Date.now()
|
|
||||||
if (heartbeat) clearTimeout(heartbeat)
|
|
||||||
heartbeat = setTimeout(() => {
|
|
||||||
attempt?.abort()
|
|
||||||
}, HEARTBEAT_TIMEOUT_MS)
|
|
||||||
}
|
|
||||||
const clearHeartbeat = () => {
|
|
||||||
if (!heartbeat) return
|
|
||||||
clearTimeout(heartbeat)
|
|
||||||
heartbeat = undefined
|
|
||||||
}
|
|
||||||
|
|
||||||
const start = () => {
|
|
||||||
if (started) return run
|
|
||||||
started = true
|
|
||||||
run = (async () => {
|
|
||||||
// oxlint-disable-next-line no-unmodified-loop-condition -- `started` is set to false by stop() which also aborts; both flags are checked to allow graceful exit
|
|
||||||
while (!abort.signal.aborted && started) {
|
|
||||||
attempt = new AbortController()
|
|
||||||
lastEventAt = Date.now()
|
|
||||||
const onAbort = () => {
|
|
||||||
attempt?.abort()
|
|
||||||
}
|
|
||||||
abort.signal.addEventListener("abort", onAbort)
|
|
||||||
try {
|
|
||||||
const events = await eventSdk.global.event({
|
|
||||||
signal: attempt.signal,
|
|
||||||
onSseError: (error) => {
|
|
||||||
if (aborted(error)) return
|
|
||||||
if (streamErrorLogged) return
|
|
||||||
streamErrorLogged = true
|
|
||||||
console.error("[global-sdk] event stream error", {
|
|
||||||
url: server.http.url,
|
|
||||||
fetch: eventFetch ? "platform" : "webview",
|
|
||||||
error,
|
|
||||||
})
|
|
||||||
},
|
|
||||||
})
|
|
||||||
let yielded = Date.now()
|
|
||||||
resetHeartbeat()
|
|
||||||
for await (const event of events.stream) {
|
|
||||||
resetHeartbeat()
|
|
||||||
streamErrorLogged = false
|
|
||||||
const directory = event.directory ?? "global"
|
|
||||||
if (event.payload.type === "sync") {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
const payload = event.payload as Event
|
|
||||||
|
|
||||||
const k = key(directory, payload)
|
|
||||||
if (k) {
|
|
||||||
const i = coalesced.get(k)
|
|
||||||
if (i !== undefined) {
|
|
||||||
queue[i] = { directory, payload }
|
|
||||||
if (payload.type === "message.part.updated") {
|
|
||||||
const part = payload.properties.part
|
|
||||||
staleDeltas.add(deltaKey(directory, part.messageID, part.id))
|
|
||||||
}
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
coalesced.set(k, queue.length)
|
|
||||||
}
|
|
||||||
queue.push({ directory, payload })
|
|
||||||
schedule()
|
|
||||||
|
|
||||||
if (Date.now() - yielded < STREAM_YIELD_MS) continue
|
|
||||||
yielded = Date.now()
|
|
||||||
await wait(0)
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
if (!aborted(error) && !streamErrorLogged) {
|
|
||||||
streamErrorLogged = true
|
|
||||||
console.error("[global-sdk] event stream failed", {
|
|
||||||
url: server.http.url,
|
|
||||||
fetch: eventFetch ? "platform" : "webview",
|
|
||||||
error,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
} finally {
|
|
||||||
abort.signal.removeEventListener("abort", onAbort)
|
|
||||||
attempt = undefined
|
|
||||||
clearHeartbeat()
|
|
||||||
}
|
|
||||||
|
|
||||||
if (abort.signal.aborted || !started) return
|
|
||||||
await wait(RECONNECT_DELAY_MS)
|
|
||||||
}
|
|
||||||
})().finally(() => {
|
|
||||||
run = undefined
|
|
||||||
flush()
|
|
||||||
})
|
|
||||||
return run
|
|
||||||
}
|
|
||||||
|
|
||||||
const stop = () => {
|
|
||||||
started = false
|
|
||||||
attempt?.abort()
|
|
||||||
clearHeartbeat()
|
|
||||||
}
|
|
||||||
|
|
||||||
onMount(() => {
|
|
||||||
makeEventListener(document, "visibilitychange", () => {
|
|
||||||
if (document.visibilityState !== "visible") return
|
|
||||||
if (!started) return
|
|
||||||
if (Date.now() - lastEventAt < HEARTBEAT_TIMEOUT_MS) return
|
|
||||||
attempt?.abort()
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
onCleanup(() => {
|
|
||||||
stop()
|
|
||||||
abort.abort()
|
|
||||||
flush()
|
|
||||||
})
|
|
||||||
|
|
||||||
const sdk = createSdkForServer({
|
|
||||||
server: server.http,
|
|
||||||
fetch: platform.fetch,
|
|
||||||
throwOnError: true,
|
|
||||||
})
|
|
||||||
|
|
||||||
return {
|
|
||||||
url: server.http.url,
|
|
||||||
client: sdk,
|
|
||||||
event: {
|
|
||||||
on: emitter.on.bind(emitter),
|
|
||||||
listen: emitter.listen.bind(emitter),
|
|
||||||
start,
|
|
||||||
},
|
|
||||||
createClient(opts: Omit<Parameters<typeof createSdkForServer>[0], "server" | "fetch">) {
|
|
||||||
return createSdkForServer({
|
|
||||||
server: server.http,
|
|
||||||
fetch: platform.fetch,
|
|
||||||
...opts,
|
|
||||||
})
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export const { use: useServerSDK, provider: ServerSDKProvider } = createSimpleContext({
|
|
||||||
name: "ServerSDK",
|
|
||||||
init: () => {
|
|
||||||
const language = useLanguage()
|
|
||||||
const server = useServer()
|
|
||||||
|
|
||||||
if (!server.current) throw new Error(language.t("error.serverSDK.noServerAvailable"))
|
|
||||||
const sdk = createServerSdkContext(server.current)
|
|
||||||
return {
|
|
||||||
...sdk,
|
|
||||||
createDirSdkContext: createRefCountMap((dir) => createDirSdkContext(dir, sdk)),
|
|
||||||
}
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
type SDKEventMap = {
|
|
||||||
[key in Event["type"]]: Extract<Event, { type: key }>
|
|
||||||
}
|
|
||||||
|
|
||||||
function createDirSdkContext(directory: string, serverSDK: ReturnType<typeof createServerSdkContext>) {
|
|
||||||
const client = serverSDK.createClient({
|
|
||||||
directory,
|
|
||||||
throwOnError: true,
|
|
||||||
})
|
|
||||||
|
|
||||||
const emitter = createGlobalEmitter<SDKEventMap>()
|
|
||||||
|
|
||||||
const unsub = serverSDK.event.on(directory, (event) => {
|
|
||||||
emitter.emit(event.type, event)
|
|
||||||
})
|
|
||||||
onCleanup(unsub)
|
|
||||||
|
|
||||||
return {
|
|
||||||
directory,
|
|
||||||
client,
|
|
||||||
event: emitter,
|
|
||||||
get url() {
|
|
||||||
return serverSDK.url
|
|
||||||
},
|
|
||||||
createClient(opts: Parameters<typeof serverSDK.createClient>[0]) {
|
|
||||||
return serverSDK.createClient(opts)
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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),
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { Binary } from "@opencode-ai/core/util/binary"
|
import { Binary } from "@opencode-ai/core/util/binary"
|
||||||
import { useServerSync } from "./server-sync"
|
import { useGlobalSync } from "./global-sync"
|
||||||
import { useSDK } from "./sdk"
|
import { useSDK } from "./sdk"
|
||||||
import type { Message, Part } from "@opencode-ai/sdk/v2/client"
|
import type { Message, Part } from "@opencode-ai/sdk/v2/client"
|
||||||
|
|
||||||
@@ -109,8 +109,8 @@ export function applyOptimisticRemove(draft: OptimisticStore, input: OptimisticR
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const useSync = () => {
|
export const useSync = () => {
|
||||||
const serverSync = useServerSync()
|
const globalSync = useGlobalSync()
|
||||||
const sdk = useSDK()
|
const sdk = useSDK()
|
||||||
|
|
||||||
return serverSync.createDirSyncContext(sdk.directory)
|
return globalSync.createDirSyncContext(sdk.directory)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { useServerSync } from "@/context/server-sync"
|
import { useGlobalSync } from "@/context/global-sync"
|
||||||
import { decode64 } from "@/utils/base64"
|
import { decode64 } from "@/utils/base64"
|
||||||
import { useParams } from "@solidjs/router"
|
import { useParams } from "@solidjs/router"
|
||||||
import { Iterable, pipe } from "effect"
|
import { Iterable, pipe } from "effect"
|
||||||
@@ -17,15 +17,15 @@ export const popularProviders = [
|
|||||||
const popularProviderSet = new Set(popularProviders)
|
const popularProviderSet = new Set(popularProviders)
|
||||||
|
|
||||||
export function useProviders() {
|
export function useProviders() {
|
||||||
const serverSync = useServerSync()
|
const globalSync = useGlobalSync()
|
||||||
const params = useParams()
|
const params = useParams()
|
||||||
const dir = createMemo(() => decode64(params.dir) ?? "")
|
const dir = createMemo(() => decode64(params.dir) ?? "")
|
||||||
const providers = () => {
|
const providers = () => {
|
||||||
if (dir()) {
|
if (dir()) {
|
||||||
const [projectStore] = serverSync.child(dir())
|
const [projectStore] = globalSync.child(dir())
|
||||||
if (projectStore.provider_ready) return projectStore.provider
|
if (projectStore.provider_ready) return projectStore.provider
|
||||||
}
|
}
|
||||||
return serverSync.data.provider
|
return globalSync.data.provider
|
||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
all: () => providers().all,
|
all: () => providers().all,
|
||||||
|
|||||||
@@ -413,7 +413,7 @@ export const dict = {
|
|||||||
"error.page.version": "الإصدار: {{version}}",
|
"error.page.version": "الإصدار: {{version}}",
|
||||||
"error.dev.rootNotFound":
|
"error.dev.rootNotFound":
|
||||||
"لم يتم العثور على العنصر الجذري. هل نسيت إضافته إلى index.html؟ أو ربما تمت كتابة سمة id بشكل خاطئ؟",
|
"لم يتم العثور على العنصر الجذري. هل نسيت إضافته إلى index.html؟ أو ربما تمت كتابة سمة id بشكل خاطئ؟",
|
||||||
"error.serverSync.connectFailed": "تعذر الاتصال بالخادم. هل هناك خادم يعمل في `{{url}}`؟",
|
"error.globalSync.connectFailed": "تعذر الاتصال بالخادم. هل هناك خادم يعمل في `{{url}}`؟",
|
||||||
"directory.error.invalidUrl": "دليل غير صالح في عنوان URL.",
|
"directory.error.invalidUrl": "دليل غير صالح في عنوان URL.",
|
||||||
"error.chain.unknown": "خطأ غير معروف",
|
"error.chain.unknown": "خطأ غير معروف",
|
||||||
"error.chain.causedBy": "بسبب:",
|
"error.chain.causedBy": "بسبب:",
|
||||||
@@ -837,8 +837,8 @@ export const dict = {
|
|||||||
"common.key.insert": "Insert",
|
"common.key.insert": "Insert",
|
||||||
"common.unknown": "غير معروف",
|
"common.unknown": "غير معروف",
|
||||||
"error.page.circular": "[دائري]",
|
"error.page.circular": "[دائري]",
|
||||||
"error.serverSDK.noServerAvailable": "لا يوجد خادم متاح",
|
"error.globalSDK.noServerAvailable": "لا يوجد خادم متاح",
|
||||||
"error.serverSDK.serverNotAvailable": "الخادم غير متاح",
|
"error.globalSDK.serverNotAvailable": "الخادم غير متاح",
|
||||||
"error.childStore.persistedCacheCreateFailed": "فشل إنشاء ذاكرة التخزين المؤقت الدائمة",
|
"error.childStore.persistedCacheCreateFailed": "فشل إنشاء ذاكرة التخزين المؤقت الدائمة",
|
||||||
"error.childStore.persistedProjectMetadataCreateFailed": "فشل إنشاء بيانات تعريف المشروع الدائمة",
|
"error.childStore.persistedProjectMetadataCreateFailed": "فشل إنشاء بيانات تعريف المشروع الدائمة",
|
||||||
"error.childStore.persistedProjectIconCreateFailed": "فشل إنشاء أيقونة المشروع الدائمة",
|
"error.childStore.persistedProjectIconCreateFailed": "فشل إنشاء أيقونة المشروع الدائمة",
|
||||||
|
|||||||
@@ -414,7 +414,7 @@ export const dict = {
|
|||||||
"error.page.version": "Versão: {{version}}",
|
"error.page.version": "Versão: {{version}}",
|
||||||
"error.dev.rootNotFound":
|
"error.dev.rootNotFound":
|
||||||
"Elemento raiz não encontrado. Você esqueceu de adicioná-lo ao seu index.html? Ou talvez o atributo id foi escrito incorretamente?",
|
"Elemento raiz não encontrado. Você esqueceu de adicioná-lo ao seu index.html? Ou talvez o atributo id foi escrito incorretamente?",
|
||||||
"error.serverSync.connectFailed": "Não foi possível conectar ao servidor. Há um servidor executando em `{{url}}`?",
|
"error.globalSync.connectFailed": "Não foi possível conectar ao servidor. Há um servidor executando em `{{url}}`?",
|
||||||
"directory.error.invalidUrl": "Diretório inválido na URL.",
|
"directory.error.invalidUrl": "Diretório inválido na URL.",
|
||||||
"error.chain.unknown": "Erro desconhecido",
|
"error.chain.unknown": "Erro desconhecido",
|
||||||
"error.chain.causedBy": "Causado por:",
|
"error.chain.causedBy": "Causado por:",
|
||||||
@@ -850,8 +850,8 @@ export const dict = {
|
|||||||
"common.key.insert": "Insert",
|
"common.key.insert": "Insert",
|
||||||
"common.unknown": "desconhecido",
|
"common.unknown": "desconhecido",
|
||||||
"error.page.circular": "[Circular]",
|
"error.page.circular": "[Circular]",
|
||||||
"error.serverSDK.noServerAvailable": "Nenhum servidor disponível",
|
"error.globalSDK.noServerAvailable": "Nenhum servidor disponível",
|
||||||
"error.serverSDK.serverNotAvailable": "Servidor indisponível",
|
"error.globalSDK.serverNotAvailable": "Servidor indisponível",
|
||||||
"error.childStore.persistedCacheCreateFailed": "Falha ao criar cache persistente",
|
"error.childStore.persistedCacheCreateFailed": "Falha ao criar cache persistente",
|
||||||
"error.childStore.persistedProjectMetadataCreateFailed": "Falha ao criar metadados de projeto persistentes",
|
"error.childStore.persistedProjectMetadataCreateFailed": "Falha ao criar metadados de projeto persistentes",
|
||||||
"error.childStore.persistedProjectIconCreateFailed": "Falha ao criar ícone de projeto persistente",
|
"error.childStore.persistedProjectIconCreateFailed": "Falha ao criar ícone de projeto persistente",
|
||||||
|
|||||||
@@ -462,7 +462,7 @@ export const dict = {
|
|||||||
"error.dev.rootNotFound":
|
"error.dev.rootNotFound":
|
||||||
"Korijenski element nije pronađen. Da li si zaboravio da ga dodaš u index.html? Ili je možda id atribut pogrešno napisan?",
|
"Korijenski element nije pronađen. Da li si zaboravio da ga dodaš u index.html? Ili je možda id atribut pogrešno napisan?",
|
||||||
|
|
||||||
"error.serverSync.connectFailed": "Nije moguće povezati se na server. Da li server radi na `{{url}}`?",
|
"error.globalSync.connectFailed": "Nije moguće povezati se na server. Da li server radi na `{{url}}`?",
|
||||||
"directory.error.invalidUrl": "Nevažeći direktorij u URL-u.",
|
"directory.error.invalidUrl": "Nevažeći direktorij u URL-u.",
|
||||||
|
|
||||||
"error.chain.unknown": "Nepoznata greška",
|
"error.chain.unknown": "Nepoznata greška",
|
||||||
@@ -926,8 +926,8 @@ export const dict = {
|
|||||||
"common.key.insert": "Insert",
|
"common.key.insert": "Insert",
|
||||||
"common.unknown": "nepoznato",
|
"common.unknown": "nepoznato",
|
||||||
"error.page.circular": "[Kružno]",
|
"error.page.circular": "[Kružno]",
|
||||||
"error.serverSDK.noServerAvailable": "Nema dostupnog servera",
|
"error.globalSDK.noServerAvailable": "Nema dostupnog servera",
|
||||||
"error.serverSDK.serverNotAvailable": "Server nije dostupan",
|
"error.globalSDK.serverNotAvailable": "Server nije dostupan",
|
||||||
"error.childStore.persistedCacheCreateFailed": "Nije uspjelo kreiranje trajnog keša",
|
"error.childStore.persistedCacheCreateFailed": "Nije uspjelo kreiranje trajnog keša",
|
||||||
"error.childStore.persistedProjectMetadataCreateFailed": "Nije uspjelo kreiranje trajnih metapodataka projekta",
|
"error.childStore.persistedProjectMetadataCreateFailed": "Nije uspjelo kreiranje trajnih metapodataka projekta",
|
||||||
"error.childStore.persistedProjectIconCreateFailed": "Nije uspjelo kreiranje trajne ikone projekta",
|
"error.childStore.persistedProjectIconCreateFailed": "Nije uspjelo kreiranje trajne ikone projekta",
|
||||||
|
|||||||
@@ -459,7 +459,7 @@ export const dict = {
|
|||||||
"error.dev.rootNotFound":
|
"error.dev.rootNotFound":
|
||||||
"Rodelement ikke fundet. Har du glemt at tilføje det til din index.html? Eller måske er id-attributten stavet forkert?",
|
"Rodelement ikke fundet. Har du glemt at tilføje det til din index.html? Eller måske er id-attributten stavet forkert?",
|
||||||
|
|
||||||
"error.serverSync.connectFailed": "Kunne ikke forbinde til server. Kører der en server på `{{url}}`?",
|
"error.globalSync.connectFailed": "Kunne ikke forbinde til server. Kører der en server på `{{url}}`?",
|
||||||
"directory.error.invalidUrl": "Ugyldig mappe i URL.",
|
"directory.error.invalidUrl": "Ugyldig mappe i URL.",
|
||||||
|
|
||||||
"error.chain.unknown": "Ukendt fejl",
|
"error.chain.unknown": "Ukendt fejl",
|
||||||
@@ -920,8 +920,8 @@ export const dict = {
|
|||||||
"common.key.insert": "Insert",
|
"common.key.insert": "Insert",
|
||||||
"common.unknown": "ukendt",
|
"common.unknown": "ukendt",
|
||||||
"error.page.circular": "[Cirkulær]",
|
"error.page.circular": "[Cirkulær]",
|
||||||
"error.serverSDK.noServerAvailable": "Ingen server tilgængelig",
|
"error.globalSDK.noServerAvailable": "Ingen server tilgængelig",
|
||||||
"error.serverSDK.serverNotAvailable": "Server ikke tilgængelig",
|
"error.globalSDK.serverNotAvailable": "Server ikke tilgængelig",
|
||||||
"error.childStore.persistedCacheCreateFailed": "Kunne ikke oprette vedvarende cache",
|
"error.childStore.persistedCacheCreateFailed": "Kunne ikke oprette vedvarende cache",
|
||||||
"error.childStore.persistedProjectMetadataCreateFailed": "Kunne ikke oprette vedvarende projektmetadata",
|
"error.childStore.persistedProjectMetadataCreateFailed": "Kunne ikke oprette vedvarende projektmetadata",
|
||||||
"error.childStore.persistedProjectIconCreateFailed": "Kunne ikke oprette vedvarende projektikon",
|
"error.childStore.persistedProjectIconCreateFailed": "Kunne ikke oprette vedvarende projektikon",
|
||||||
|
|||||||
@@ -421,7 +421,7 @@ export const dict = {
|
|||||||
"error.page.version": "Version: {{version}}",
|
"error.page.version": "Version: {{version}}",
|
||||||
"error.dev.rootNotFound":
|
"error.dev.rootNotFound":
|
||||||
"Wurzelelement nicht gefunden. Haben Sie vergessen, es in Ihre index.html aufzunehmen? Oder wurde das id-Attribut falsch geschrieben?",
|
"Wurzelelement nicht gefunden. Haben Sie vergessen, es in Ihre index.html aufzunehmen? Oder wurde das id-Attribut falsch geschrieben?",
|
||||||
"error.serverSync.connectFailed": "Verbindung zum Server fehlgeschlagen. Läuft ein Server unter `{{url}}`?",
|
"error.globalSync.connectFailed": "Verbindung zum Server fehlgeschlagen. Läuft ein Server unter `{{url}}`?",
|
||||||
"directory.error.invalidUrl": "Ungültiges Verzeichnis in der URL.",
|
"directory.error.invalidUrl": "Ungültiges Verzeichnis in der URL.",
|
||||||
"error.chain.unknown": "Unbekannter Fehler",
|
"error.chain.unknown": "Unbekannter Fehler",
|
||||||
"error.chain.causedBy": "Verursacht durch:",
|
"error.chain.causedBy": "Verursacht durch:",
|
||||||
@@ -862,8 +862,8 @@ export const dict = {
|
|||||||
"common.key.insert": "Einfg",
|
"common.key.insert": "Einfg",
|
||||||
"common.unknown": "unbekannt",
|
"common.unknown": "unbekannt",
|
||||||
"error.page.circular": "[Zirkulär]",
|
"error.page.circular": "[Zirkulär]",
|
||||||
"error.serverSDK.noServerAvailable": "Kein Server verfügbar",
|
"error.globalSDK.noServerAvailable": "Kein Server verfügbar",
|
||||||
"error.serverSDK.serverNotAvailable": "Server nicht verfügbar",
|
"error.globalSDK.serverNotAvailable": "Server nicht verfügbar",
|
||||||
"error.childStore.persistedCacheCreateFailed": "Dauerhafter Cache konnte nicht erstellt werden",
|
"error.childStore.persistedCacheCreateFailed": "Dauerhafter Cache konnte nicht erstellt werden",
|
||||||
"error.childStore.persistedProjectMetadataCreateFailed": "Dauerhafte Projektmetadaten konnten nicht erstellt werden",
|
"error.childStore.persistedProjectMetadataCreateFailed": "Dauerhafte Projektmetadaten konnten nicht erstellt werden",
|
||||||
"error.childStore.persistedProjectIconCreateFailed": "Dauerhaftes Projekticon konnte nicht erstellt werden",
|
"error.childStore.persistedProjectIconCreateFailed": "Dauerhaftes Projekticon konnte nicht erstellt werden",
|
||||||
|
|||||||
@@ -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",
|
||||||
@@ -483,9 +482,9 @@ export const dict = {
|
|||||||
"error.dev.rootNotFound":
|
"error.dev.rootNotFound":
|
||||||
"Root element not found. Did you forget to add it to your index.html? Or maybe the id attribute got misspelled?",
|
"Root element not found. Did you forget to add it to your index.html? Or maybe the id attribute got misspelled?",
|
||||||
|
|
||||||
"error.serverSync.connectFailed": "Could not connect to server. Is there a server running at `{{url}}`?",
|
"error.globalSync.connectFailed": "Could not connect to server. Is there a server running at `{{url}}`?",
|
||||||
"error.serverSDK.noServerAvailable": "No server available",
|
"error.globalSDK.noServerAvailable": "No server available",
|
||||||
"error.serverSDK.serverNotAvailable": "Server not available",
|
"error.globalSDK.serverNotAvailable": "Server not available",
|
||||||
"error.childStore.persistedCacheCreateFailed": "Failed to create persisted cache",
|
"error.childStore.persistedCacheCreateFailed": "Failed to create persisted cache",
|
||||||
"error.childStore.persistedProjectMetadataCreateFailed": "Failed to create persisted project metadata",
|
"error.childStore.persistedProjectMetadataCreateFailed": "Failed to create persisted project metadata",
|
||||||
"error.childStore.persistedProjectIconCreateFailed": "Failed to create persisted project icon",
|
"error.childStore.persistedProjectIconCreateFailed": "Failed to create persisted project icon",
|
||||||
@@ -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",
|
||||||
|
|
||||||
|
|||||||
@@ -462,7 +462,7 @@ export const dict = {
|
|||||||
"error.dev.rootNotFound":
|
"error.dev.rootNotFound":
|
||||||
"Elemento raíz no encontrado. ¿Olvidaste añadirlo a tu index.html? ¿O tal vez el atributo id está mal escrito?",
|
"Elemento raíz no encontrado. ¿Olvidaste añadirlo a tu index.html? ¿O tal vez el atributo id está mal escrito?",
|
||||||
|
|
||||||
"error.serverSync.connectFailed": "No se pudo conectar al servidor. ¿Hay un servidor ejecutándose en `{{url}}`?",
|
"error.globalSync.connectFailed": "No se pudo conectar al servidor. ¿Hay un servidor ejecutándose en `{{url}}`?",
|
||||||
"directory.error.invalidUrl": "URL de directorio inválida.",
|
"directory.error.invalidUrl": "URL de directorio inválida.",
|
||||||
|
|
||||||
"error.chain.unknown": "Error desconocido",
|
"error.chain.unknown": "Error desconocido",
|
||||||
@@ -933,8 +933,8 @@ export const dict = {
|
|||||||
"common.key.insert": "Insert",
|
"common.key.insert": "Insert",
|
||||||
"common.unknown": "desconocido",
|
"common.unknown": "desconocido",
|
||||||
"error.page.circular": "[Circular]",
|
"error.page.circular": "[Circular]",
|
||||||
"error.serverSDK.noServerAvailable": "Ningún servidor disponible",
|
"error.globalSDK.noServerAvailable": "Ningún servidor disponible",
|
||||||
"error.serverSDK.serverNotAvailable": "Servidor no disponible",
|
"error.globalSDK.serverNotAvailable": "Servidor no disponible",
|
||||||
"error.childStore.persistedCacheCreateFailed": "Error al crear caché persistente",
|
"error.childStore.persistedCacheCreateFailed": "Error al crear caché persistente",
|
||||||
"error.childStore.persistedProjectMetadataCreateFailed": "Error al crear metadatos de proyecto persistentes",
|
"error.childStore.persistedProjectMetadataCreateFailed": "Error al crear metadatos de proyecto persistentes",
|
||||||
"error.childStore.persistedProjectIconCreateFailed": "Error al crear icono de proyecto persistente",
|
"error.childStore.persistedProjectIconCreateFailed": "Error al crear icono de proyecto persistente",
|
||||||
|
|||||||
@@ -417,7 +417,7 @@ export const dict = {
|
|||||||
"error.page.version": "Version : {{version}}",
|
"error.page.version": "Version : {{version}}",
|
||||||
"error.dev.rootNotFound":
|
"error.dev.rootNotFound":
|
||||||
"Élément racine introuvable. Avez-vous oublié de l'ajouter à votre index.html ? Ou peut-être que l'attribut id est mal orthographié ?",
|
"Élément racine introuvable. Avez-vous oublié de l'ajouter à votre index.html ? Ou peut-être que l'attribut id est mal orthographié ?",
|
||||||
"error.serverSync.connectFailed":
|
"error.globalSync.connectFailed":
|
||||||
"Impossible de se connecter au serveur. Y a-t-il un serveur en cours d'exécution à `{{url}}` ?",
|
"Impossible de se connecter au serveur. Y a-t-il un serveur en cours d'exécution à `{{url}}` ?",
|
||||||
"directory.error.invalidUrl": "Répertoire invalide dans l'URL.",
|
"directory.error.invalidUrl": "Répertoire invalide dans l'URL.",
|
||||||
"error.chain.unknown": "Erreur inconnue",
|
"error.chain.unknown": "Erreur inconnue",
|
||||||
@@ -860,8 +860,8 @@ export const dict = {
|
|||||||
"common.key.insert": "Inser",
|
"common.key.insert": "Inser",
|
||||||
"common.unknown": "inconnu",
|
"common.unknown": "inconnu",
|
||||||
"error.page.circular": "[Circulaire]",
|
"error.page.circular": "[Circulaire]",
|
||||||
"error.serverSDK.noServerAvailable": "Aucun serveur disponible",
|
"error.globalSDK.noServerAvailable": "Aucun serveur disponible",
|
||||||
"error.serverSDK.serverNotAvailable": "Serveur non disponible",
|
"error.globalSDK.serverNotAvailable": "Serveur non disponible",
|
||||||
"error.childStore.persistedCacheCreateFailed": "Échec de la création du cache persistant",
|
"error.childStore.persistedCacheCreateFailed": "Échec de la création du cache persistant",
|
||||||
"error.childStore.persistedProjectMetadataCreateFailed":
|
"error.childStore.persistedProjectMetadataCreateFailed":
|
||||||
"Échec de la création des métadonnées de projet persistantes",
|
"Échec de la création des métadonnées de projet persistantes",
|
||||||
|
|||||||
@@ -413,7 +413,7 @@ export const dict = {
|
|||||||
"error.page.version": "バージョン: {{version}}",
|
"error.page.version": "バージョン: {{version}}",
|
||||||
"error.dev.rootNotFound":
|
"error.dev.rootNotFound":
|
||||||
"ルート要素が見つかりません。index.htmlに追加するのを忘れていませんか?またはid属性のスペルが間違っていませんか?",
|
"ルート要素が見つかりません。index.htmlに追加するのを忘れていませんか?またはid属性のスペルが間違っていませんか?",
|
||||||
"error.serverSync.connectFailed": "サーバーに接続できませんでした。`{{url}}`でサーバーが実行されていますか?",
|
"error.globalSync.connectFailed": "サーバーに接続できませんでした。`{{url}}`でサーバーが実行されていますか?",
|
||||||
"directory.error.invalidUrl": "URL内のディレクトリが無効です。",
|
"directory.error.invalidUrl": "URL内のディレクトリが無効です。",
|
||||||
"error.chain.unknown": "不明なエラー",
|
"error.chain.unknown": "不明なエラー",
|
||||||
"error.chain.causedBy": "原因:",
|
"error.chain.causedBy": "原因:",
|
||||||
@@ -844,8 +844,8 @@ export const dict = {
|
|||||||
"common.key.insert": "Insert",
|
"common.key.insert": "Insert",
|
||||||
"common.unknown": "不明",
|
"common.unknown": "不明",
|
||||||
"error.page.circular": "[循環]",
|
"error.page.circular": "[循環]",
|
||||||
"error.serverSDK.noServerAvailable": "利用可能なサーバーがありません",
|
"error.globalSDK.noServerAvailable": "利用可能なサーバーがありません",
|
||||||
"error.serverSDK.serverNotAvailable": "サーバーが利用できません",
|
"error.globalSDK.serverNotAvailable": "サーバーが利用できません",
|
||||||
"error.childStore.persistedCacheCreateFailed": "永続キャッシュの作成に失敗しました",
|
"error.childStore.persistedCacheCreateFailed": "永続キャッシュの作成に失敗しました",
|
||||||
"error.childStore.persistedProjectMetadataCreateFailed": "永続プロジェクトメタデータの作成に失敗しました",
|
"error.childStore.persistedProjectMetadataCreateFailed": "永続プロジェクトメタデータの作成に失敗しました",
|
||||||
"error.childStore.persistedProjectIconCreateFailed": "永続プロジェクトアイコンの作成に失敗しました",
|
"error.childStore.persistedProjectIconCreateFailed": "永続プロジェクトアイコンの作成に失敗しました",
|
||||||
|
|||||||
@@ -412,7 +412,7 @@ export const dict = {
|
|||||||
"error.page.version": "버전: {{version}}",
|
"error.page.version": "버전: {{version}}",
|
||||||
"error.dev.rootNotFound":
|
"error.dev.rootNotFound":
|
||||||
"루트 요소를 찾을 수 없습니다. index.html에 추가하는 것을 잊으셨나요? 또는 id 속성의 철자가 틀렸을 수 있습니다.",
|
"루트 요소를 찾을 수 없습니다. index.html에 추가하는 것을 잊으셨나요? 또는 id 속성의 철자가 틀렸을 수 있습니다.",
|
||||||
"error.serverSync.connectFailed": "서버에 연결할 수 없습니다. `{{url}}`에서 서버가 실행 중인가요?",
|
"error.globalSync.connectFailed": "서버에 연결할 수 없습니다. `{{url}}`에서 서버가 실행 중인가요?",
|
||||||
"directory.error.invalidUrl": "URL에 유효하지 않은 디렉터리가 있습니다.",
|
"directory.error.invalidUrl": "URL에 유효하지 않은 디렉터리가 있습니다.",
|
||||||
"error.chain.unknown": "알 수 없는 오류",
|
"error.chain.unknown": "알 수 없는 오류",
|
||||||
"error.chain.causedBy": "원인:",
|
"error.chain.causedBy": "원인:",
|
||||||
@@ -839,8 +839,8 @@ export const dict = {
|
|||||||
"common.key.insert": "Insert",
|
"common.key.insert": "Insert",
|
||||||
"common.unknown": "알 수 없음",
|
"common.unknown": "알 수 없음",
|
||||||
"error.page.circular": "[순환]",
|
"error.page.circular": "[순환]",
|
||||||
"error.serverSDK.noServerAvailable": "사용 가능한 서버 없음",
|
"error.globalSDK.noServerAvailable": "사용 가능한 서버 없음",
|
||||||
"error.serverSDK.serverNotAvailable": "서버를 사용할 수 없음",
|
"error.globalSDK.serverNotAvailable": "서버를 사용할 수 없음",
|
||||||
"error.childStore.persistedCacheCreateFailed": "영구 캐시 생성 실패",
|
"error.childStore.persistedCacheCreateFailed": "영구 캐시 생성 실패",
|
||||||
"error.childStore.persistedProjectMetadataCreateFailed": "영구 프로젝트 메타데이터 생성 실패",
|
"error.childStore.persistedProjectMetadataCreateFailed": "영구 프로젝트 메타데이터 생성 실패",
|
||||||
"error.childStore.persistedProjectIconCreateFailed": "영구 프로젝트 아이콘 생성 실패",
|
"error.childStore.persistedProjectIconCreateFailed": "영구 프로젝트 아이콘 생성 실패",
|
||||||
|
|||||||
@@ -463,7 +463,7 @@ export const dict = {
|
|||||||
"error.dev.rootNotFound":
|
"error.dev.rootNotFound":
|
||||||
"Rotelement ikke funnet. Glemte du å legge det til i index.html? Eller kanskje id-attributten er feilstavet?",
|
"Rotelement ikke funnet. Glemte du å legge det til i index.html? Eller kanskje id-attributten er feilstavet?",
|
||||||
|
|
||||||
"error.serverSync.connectFailed": "Kunne ikke koble til server. Kjører det en server på `{{url}}`?",
|
"error.globalSync.connectFailed": "Kunne ikke koble til server. Kjører det en server på `{{url}}`?",
|
||||||
"directory.error.invalidUrl": "Invalid directory in URL.",
|
"directory.error.invalidUrl": "Invalid directory in URL.",
|
||||||
|
|
||||||
"error.chain.unknown": "Ukjent feil",
|
"error.chain.unknown": "Ukjent feil",
|
||||||
@@ -927,8 +927,8 @@ export const dict = {
|
|||||||
"common.key.insert": "Insert",
|
"common.key.insert": "Insert",
|
||||||
"common.unknown": "ukjent",
|
"common.unknown": "ukjent",
|
||||||
"error.page.circular": "[Sirkulær]",
|
"error.page.circular": "[Sirkulær]",
|
||||||
"error.serverSDK.noServerAvailable": "Ingen server tilgjengelig",
|
"error.globalSDK.noServerAvailable": "Ingen server tilgjengelig",
|
||||||
"error.serverSDK.serverNotAvailable": "Server ikke tilgjengelig",
|
"error.globalSDK.serverNotAvailable": "Server ikke tilgjengelig",
|
||||||
"error.childStore.persistedCacheCreateFailed": "Kunne ikke opprette vedvarende hurtigbuffer",
|
"error.childStore.persistedCacheCreateFailed": "Kunne ikke opprette vedvarende hurtigbuffer",
|
||||||
"error.childStore.persistedProjectMetadataCreateFailed": "Kunne ikke opprette vedvarende prosjektmetadata",
|
"error.childStore.persistedProjectMetadataCreateFailed": "Kunne ikke opprette vedvarende prosjektmetadata",
|
||||||
"error.childStore.persistedProjectIconCreateFailed": "Kunne ikke opprette vedvarende prosjektikon",
|
"error.childStore.persistedProjectIconCreateFailed": "Kunne ikke opprette vedvarende prosjektikon",
|
||||||
|
|||||||
@@ -414,7 +414,7 @@ export const dict = {
|
|||||||
"error.page.version": "Wersja: {{version}}",
|
"error.page.version": "Wersja: {{version}}",
|
||||||
"error.dev.rootNotFound":
|
"error.dev.rootNotFound":
|
||||||
"Nie znaleziono elementu głównego. Czy zapomniałeś dodać go do swojego index.html? A może atrybut id został błędnie wpisany?",
|
"Nie znaleziono elementu głównego. Czy zapomniałeś dodać go do swojego index.html? A może atrybut id został błędnie wpisany?",
|
||||||
"error.serverSync.connectFailed": "Nie można połączyć się z serwerem. Czy serwer działa pod adresem `{{url}}`?",
|
"error.globalSync.connectFailed": "Nie można połączyć się z serwerem. Czy serwer działa pod adresem `{{url}}`?",
|
||||||
"directory.error.invalidUrl": "Nieprawidłowy katalog w URL.",
|
"directory.error.invalidUrl": "Nieprawidłowy katalog w URL.",
|
||||||
"error.chain.unknown": "Nieznany błąd",
|
"error.chain.unknown": "Nieznany błąd",
|
||||||
"error.chain.causedBy": "Spowodowany przez:",
|
"error.chain.causedBy": "Spowodowany przez:",
|
||||||
@@ -848,8 +848,8 @@ export const dict = {
|
|||||||
"common.key.insert": "Insert",
|
"common.key.insert": "Insert",
|
||||||
"common.unknown": "nieznany",
|
"common.unknown": "nieznany",
|
||||||
"error.page.circular": "[Cykliczne]",
|
"error.page.circular": "[Cykliczne]",
|
||||||
"error.serverSDK.noServerAvailable": "Brak dostępnego serwera",
|
"error.globalSDK.noServerAvailable": "Brak dostępnego serwera",
|
||||||
"error.serverSDK.serverNotAvailable": "Serwer niedostępny",
|
"error.globalSDK.serverNotAvailable": "Serwer niedostępny",
|
||||||
"error.childStore.persistedCacheCreateFailed": "Nie udało się utworzyć trwałej pamięci podręcznej",
|
"error.childStore.persistedCacheCreateFailed": "Nie udało się utworzyć trwałej pamięci podręcznej",
|
||||||
"error.childStore.persistedProjectMetadataCreateFailed": "Nie udało się utworzyć trwałych metadanych projektu",
|
"error.childStore.persistedProjectMetadataCreateFailed": "Nie udało się utworzyć trwałych metadanych projektu",
|
||||||
"error.childStore.persistedProjectIconCreateFailed": "Nie udało się utworzyć trwałej ikony projektu",
|
"error.childStore.persistedProjectIconCreateFailed": "Nie udało się utworzyć trwałej ikony projektu",
|
||||||
|
|||||||
@@ -461,7 +461,7 @@ export const dict = {
|
|||||||
"error.dev.rootNotFound":
|
"error.dev.rootNotFound":
|
||||||
"Корневой элемент не найден. Вы забыли добавить его в index.html? Или, может быть, атрибут id был написан неправильно?",
|
"Корневой элемент не найден. Вы забыли добавить его в index.html? Или, может быть, атрибут id был написан неправильно?",
|
||||||
|
|
||||||
"error.serverSync.connectFailed": "Не удалось подключиться к серверу. Запущен ли сервер по адресу `{{url}}`?",
|
"error.globalSync.connectFailed": "Не удалось подключиться к серверу. Запущен ли сервер по адресу `{{url}}`?",
|
||||||
"directory.error.invalidUrl": "Недопустимая директория в URL.",
|
"directory.error.invalidUrl": "Недопустимая директория в URL.",
|
||||||
|
|
||||||
"error.chain.unknown": "Неизвестная ошибка",
|
"error.chain.unknown": "Неизвестная ошибка",
|
||||||
@@ -929,8 +929,8 @@ export const dict = {
|
|||||||
"common.key.insert": "Insert",
|
"common.key.insert": "Insert",
|
||||||
"common.unknown": "неизвестно",
|
"common.unknown": "неизвестно",
|
||||||
"error.page.circular": "[Циклично]",
|
"error.page.circular": "[Циклично]",
|
||||||
"error.serverSDK.noServerAvailable": "Нет доступного сервера",
|
"error.globalSDK.noServerAvailable": "Нет доступного сервера",
|
||||||
"error.serverSDK.serverNotAvailable": "Сервер недоступен",
|
"error.globalSDK.serverNotAvailable": "Сервер недоступен",
|
||||||
"error.childStore.persistedCacheCreateFailed": "Не удалось создать постоянный кэш",
|
"error.childStore.persistedCacheCreateFailed": "Не удалось создать постоянный кэш",
|
||||||
"error.childStore.persistedProjectMetadataCreateFailed": "Не удалось создать постоянные метаданные проекта",
|
"error.childStore.persistedProjectMetadataCreateFailed": "Не удалось создать постоянные метаданные проекта",
|
||||||
"error.childStore.persistedProjectIconCreateFailed": "Не удалось создать постоянный значок проекта",
|
"error.childStore.persistedProjectIconCreateFailed": "Не удалось создать постоянный значок проекта",
|
||||||
|
|||||||
@@ -459,7 +459,7 @@ export const dict = {
|
|||||||
|
|
||||||
"error.dev.rootNotFound": "ไม่พบองค์ประกอบรูท คุณลืมเพิ่มใน index.html หรือบางทีแอตทริบิวต์ id อาจสะกดผิด?",
|
"error.dev.rootNotFound": "ไม่พบองค์ประกอบรูท คุณลืมเพิ่มใน index.html หรือบางทีแอตทริบิวต์ id อาจสะกดผิด?",
|
||||||
|
|
||||||
"error.serverSync.connectFailed": "ไม่สามารถเชื่อมต่อกับเซิร์ฟเวอร์ มีเซิร์ฟเวอร์ทำงานอยู่ที่ `{{url}}` หรือไม่?",
|
"error.globalSync.connectFailed": "ไม่สามารถเชื่อมต่อกับเซิร์ฟเวอร์ มีเซิร์ฟเวอร์ทำงานอยู่ที่ `{{url}}` หรือไม่?",
|
||||||
"directory.error.invalidUrl": "ไดเรกทอรีใน URL ไม่ถูกต้อง",
|
"directory.error.invalidUrl": "ไดเรกทอรีใน URL ไม่ถูกต้อง",
|
||||||
|
|
||||||
"error.chain.unknown": "ข้อผิดพลาดที่ไม่รู้จัก",
|
"error.chain.unknown": "ข้อผิดพลาดที่ไม่รู้จัก",
|
||||||
@@ -916,8 +916,8 @@ export const dict = {
|
|||||||
"common.key.insert": "Insert",
|
"common.key.insert": "Insert",
|
||||||
"common.unknown": "ไม่ทราบ",
|
"common.unknown": "ไม่ทราบ",
|
||||||
"error.page.circular": "[วงกลม]",
|
"error.page.circular": "[วงกลม]",
|
||||||
"error.serverSDK.noServerAvailable": "ไม่มีเซิร์ฟเวอร์",
|
"error.globalSDK.noServerAvailable": "ไม่มีเซิร์ฟเวอร์",
|
||||||
"error.serverSDK.serverNotAvailable": "เซิร์ฟเวอร์ไม่พร้อมใช้งาน",
|
"error.globalSDK.serverNotAvailable": "เซิร์ฟเวอร์ไม่พร้อมใช้งาน",
|
||||||
"error.childStore.persistedCacheCreateFailed": "ไม่สามารถสร้างแคชถาวร",
|
"error.childStore.persistedCacheCreateFailed": "ไม่สามารถสร้างแคชถาวร",
|
||||||
"error.childStore.persistedProjectMetadataCreateFailed": "ไม่สามารถสร้างเมตาดาต้าโปรเจกต์ถาวร",
|
"error.childStore.persistedProjectMetadataCreateFailed": "ไม่สามารถสร้างเมตาดาต้าโปรเจกต์ถาวร",
|
||||||
"error.childStore.persistedProjectIconCreateFailed": "ไม่สามารถสร้างไอคอนโปรเจกต์ถาวร",
|
"error.childStore.persistedProjectIconCreateFailed": "ไม่สามารถสร้างไอคอนโปรเจกต์ถาวร",
|
||||||
|
|||||||
@@ -465,7 +465,7 @@ export const dict = {
|
|||||||
"error.dev.rootNotFound":
|
"error.dev.rootNotFound":
|
||||||
"Kök eleman bulunamadı. index.html dosyanıza eklemeyi unuttunuz mu? Ya da id özelliği yanlış mı yazıldı?",
|
"Kök eleman bulunamadı. index.html dosyanıza eklemeyi unuttunuz mu? Ya da id özelliği yanlış mı yazıldı?",
|
||||||
|
|
||||||
"error.serverSync.connectFailed": "Sunucuya bağlanılamadı. `{{url}}` adresinde çalışan bir sunucu var mı?",
|
"error.globalSync.connectFailed": "Sunucuya bağlanılamadı. `{{url}}` adresinde çalışan bir sunucu var mı?",
|
||||||
"directory.error.invalidUrl": "URL'de geçersiz dizin.",
|
"directory.error.invalidUrl": "URL'de geçersiz dizin.",
|
||||||
|
|
||||||
"error.chain.unknown": "Bilinmeyen hata",
|
"error.chain.unknown": "Bilinmeyen hata",
|
||||||
@@ -935,8 +935,8 @@ export const dict = {
|
|||||||
"common.key.insert": "Insert",
|
"common.key.insert": "Insert",
|
||||||
"common.unknown": "bilinmiyor",
|
"common.unknown": "bilinmiyor",
|
||||||
"error.page.circular": "[Döngüsel]",
|
"error.page.circular": "[Döngüsel]",
|
||||||
"error.serverSDK.noServerAvailable": "Sunucu yok",
|
"error.globalSDK.noServerAvailable": "Sunucu yok",
|
||||||
"error.serverSDK.serverNotAvailable": "Sunucu mevcut değil",
|
"error.globalSDK.serverNotAvailable": "Sunucu mevcut değil",
|
||||||
"error.childStore.persistedCacheCreateFailed": "Kalıcı önbellek oluşturulamadı",
|
"error.childStore.persistedCacheCreateFailed": "Kalıcı önbellek oluşturulamadı",
|
||||||
"error.childStore.persistedProjectMetadataCreateFailed": "Kalıcı proje meta verileri oluşturulamadı",
|
"error.childStore.persistedProjectMetadataCreateFailed": "Kalıcı proje meta verileri oluşturulamadı",
|
||||||
"error.childStore.persistedProjectIconCreateFailed": "Kalıcı proje simgesi oluşturulamadı",
|
"error.childStore.persistedProjectIconCreateFailed": "Kalıcı proje simgesi oluşturulamadı",
|
||||||
|
|||||||
@@ -483,9 +483,9 @@ export const dict = {
|
|||||||
"error.dev.rootNotFound":
|
"error.dev.rootNotFound":
|
||||||
"Кореневий елемент не знайдено. Ви забули додати його до index.html? Або, можливо, атрибут id було написано з помилкою?",
|
"Кореневий елемент не знайдено. Ви забули додати його до index.html? Або, можливо, атрибут id було написано з помилкою?",
|
||||||
|
|
||||||
"error.serverSync.connectFailed": "Не вдалося підключитися до сервера. Чи працює сервер за адресою `{{url}}`?",
|
"error.globalSync.connectFailed": "Не вдалося підключитися до сервера. Чи працює сервер за адресою `{{url}}`?",
|
||||||
"error.serverSDK.noServerAvailable": "Сервер недоступний",
|
"error.globalSDK.noServerAvailable": "Сервер недоступний",
|
||||||
"error.serverSDK.serverNotAvailable": "Сервер недоступний",
|
"error.globalSDK.serverNotAvailable": "Сервер недоступний",
|
||||||
"error.childStore.persistedCacheCreateFailed": "Не вдалося створити постійний кеш",
|
"error.childStore.persistedCacheCreateFailed": "Не вдалося створити постійний кеш",
|
||||||
"error.childStore.persistedProjectMetadataCreateFailed": "Не вдалося створити постійні метадані проєкту",
|
"error.childStore.persistedProjectMetadataCreateFailed": "Не вдалося створити постійні метадані проєкту",
|
||||||
"error.childStore.persistedProjectIconCreateFailed": "Не вдалося створити постійну іконку проєкту",
|
"error.childStore.persistedProjectIconCreateFailed": "Не вдалося створити постійну іконку проєкту",
|
||||||
@@ -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": "Палітра команд",
|
||||||
|
|||||||
@@ -462,7 +462,7 @@ export const dict = {
|
|||||||
"error.page.report.discord": "在 Discord 上",
|
"error.page.report.discord": "在 Discord 上",
|
||||||
"error.page.version": "版本:{{version}}",
|
"error.page.version": "版本:{{version}}",
|
||||||
"error.dev.rootNotFound": "未找到根元素。你是不是忘了把它添加到 index.html?或者 id 属性拼写错了?",
|
"error.dev.rootNotFound": "未找到根元素。你是不是忘了把它添加到 index.html?或者 id 属性拼写错了?",
|
||||||
"error.serverSync.connectFailed": "无法连接到服务器。是否有服务器正在 `{{url}}` 运行?",
|
"error.globalSync.connectFailed": "无法连接到服务器。是否有服务器正在 `{{url}}` 运行?",
|
||||||
|
|
||||||
"directory.error.invalidUrl": "URL 中的目录无效。",
|
"directory.error.invalidUrl": "URL 中的目录无效。",
|
||||||
|
|
||||||
@@ -919,8 +919,8 @@ export const dict = {
|
|||||||
"common.key.insert": "Insert",
|
"common.key.insert": "Insert",
|
||||||
"common.unknown": "未知",
|
"common.unknown": "未知",
|
||||||
"error.page.circular": "[循环]",
|
"error.page.circular": "[循环]",
|
||||||
"error.serverSDK.noServerAvailable": "无可用服务器",
|
"error.globalSDK.noServerAvailable": "无可用服务器",
|
||||||
"error.serverSDK.serverNotAvailable": "服务器不可用",
|
"error.globalSDK.serverNotAvailable": "服务器不可用",
|
||||||
"error.childStore.persistedCacheCreateFailed": "创建持久化缓存失败",
|
"error.childStore.persistedCacheCreateFailed": "创建持久化缓存失败",
|
||||||
"error.childStore.persistedProjectMetadataCreateFailed": "创建持久化项目元数据失败",
|
"error.childStore.persistedProjectMetadataCreateFailed": "创建持久化项目元数据失败",
|
||||||
"error.childStore.persistedProjectIconCreateFailed": "创建持久化项目图标失败",
|
"error.childStore.persistedProjectIconCreateFailed": "创建持久化项目图标失败",
|
||||||
|
|||||||
@@ -457,7 +457,7 @@ export const dict = {
|
|||||||
|
|
||||||
"error.dev.rootNotFound": "找不到根元素。你是不是忘了把它新增到 index.html? 或者 id 屬性拼錯了?",
|
"error.dev.rootNotFound": "找不到根元素。你是不是忘了把它新增到 index.html? 或者 id 屬性拼錯了?",
|
||||||
|
|
||||||
"error.serverSync.connectFailed": "無法連線到伺服器。是否有伺服器正在 `{{url}}` 執行?",
|
"error.globalSync.connectFailed": "無法連線到伺服器。是否有伺服器正在 `{{url}}` 執行?",
|
||||||
"directory.error.invalidUrl": "URL 中的目錄無效。",
|
"directory.error.invalidUrl": "URL 中的目錄無效。",
|
||||||
|
|
||||||
"error.chain.unknown": "未知錯誤",
|
"error.chain.unknown": "未知錯誤",
|
||||||
@@ -907,8 +907,8 @@ export const dict = {
|
|||||||
"common.key.insert": "Insert",
|
"common.key.insert": "Insert",
|
||||||
"common.unknown": "未知",
|
"common.unknown": "未知",
|
||||||
"error.page.circular": "[循環]",
|
"error.page.circular": "[循環]",
|
||||||
"error.serverSDK.noServerAvailable": "無可用的伺服器",
|
"error.globalSDK.noServerAvailable": "無可用的伺服器",
|
||||||
"error.serverSDK.serverNotAvailable": "伺服器無法使用",
|
"error.globalSDK.serverNotAvailable": "伺服器無法使用",
|
||||||
"error.childStore.persistedCacheCreateFailed": "建立持續性快取失敗",
|
"error.childStore.persistedCacheCreateFailed": "建立持續性快取失敗",
|
||||||
"error.childStore.persistedProjectMetadataCreateFailed": "建立持續性專案中繼資料失敗",
|
"error.childStore.persistedProjectMetadataCreateFailed": "建立持續性專案中繼資料失敗",
|
||||||
"error.childStore.persistedProjectIconCreateFailed": "建立持續性專案圖示失敗",
|
"error.childStore.persistedProjectIconCreateFailed": "建立持續性專案圖示失敗",
|
||||||
|
|||||||
@@ -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% {
|
||||||
|
|||||||
+270
-414
@@ -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,24 @@ 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 { DialogSelectModel } from "@/components/dialog-select-model"
|
||||||
import { useServerSync } from "@/context/server-sync"
|
import { useServer } from "@/context/server"
|
||||||
|
import { useGlobalSync } from "@/context/global-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,32 +51,28 @@ 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() {
|
||||||
const sync = useServerSync()
|
const sync = useGlobalSync()
|
||||||
const layout = useLayout()
|
const layout = useLayout()
|
||||||
const platform = usePlatform()
|
const platform = usePlatform()
|
||||||
const dialog = useDialog()
|
const dialog = useDialog()
|
||||||
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 +86,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 +109,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 +135,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 +176,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}
|
||||||
@@ -221,87 +189,58 @@ function HomeDesign() {
|
|||||||
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
|
<HomeSessionSearch
|
||||||
when={projectDirectories().length > 0}
|
value={state.search}
|
||||||
fallback={
|
placeholder={language.t("home.sessions.search.placeholder")}
|
||||||
<HomeEmptyState
|
onInput={(value) => setState("search", value)}
|
||||||
icon="folder-add-left"
|
/>
|
||||||
title={language.t("home.empty.title")}
|
<div class="mt-3 overflow-auto flex-1">
|
||||||
description={language.t("home.empty.description")}
|
<div class="pt-3 flex flex-col gap-6">
|
||||||
action={language.t("home.project.add")}
|
<Show when={!sessionLoad.isLoading} fallback={<HomeSessionSkeleton label={language.t("common.loading")} />}>
|
||||||
onAction={() => void chooseProject()}
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<HomeSessionSearch
|
|
||||||
value={state.search}
|
|
||||||
placeholder={language.t("home.sessions.search.placeholder")}
|
|
||||||
onInput={(value) => setState("search", value)}
|
|
||||||
clearLabel={language.t("common.clear")}
|
|
||||||
onClear={() => setState("search", "")}
|
|
||||||
/>
|
|
||||||
<div class="mt-3 overflow-auto flex-1">
|
|
||||||
<div class="pt-3 flex flex-col gap-6">
|
|
||||||
<Show
|
<Show
|
||||||
when={!sessionLoad.isLoading}
|
when={groups().length > 0}
|
||||||
fallback={<HomeSessionSkeleton label={language.t("common.loading")} />}
|
fallback={
|
||||||
|
<div class="flex min-w-0 flex-col gap-4">
|
||||||
|
<HomeSessionGroupHeader title={language.t("home.sessions.empty")} onNewSession={openNewSession} />
|
||||||
|
</div>
|
||||||
|
}
|
||||||
>
|
>
|
||||||
<Show
|
<For each={groups()}>
|
||||||
when={groups().length > 0}
|
{(group, index) => (
|
||||||
fallback={
|
<div class="flex min-w-0 flex-col gap-4">
|
||||||
<HomeEmptyState
|
<HomeSessionGroupHeader
|
||||||
icon="edit"
|
title={group.title}
|
||||||
title={language.t("home.sessions.empty")}
|
onNewSession={index() === 0 ? openNewSession : undefined}
|
||||||
description={language.t("home.sessions.empty.description")}
|
/>
|
||||||
action={language.t("command.session.new")}
|
<div class="flex min-w-0 flex-col gap-px">
|
||||||
onAction={openNewSession}
|
<For each={group.sessions}>
|
||||||
/>
|
{(record) => <HomeSessionRow record={record} openSession={openSession} />}
|
||||||
}
|
</For>
|
||||||
>
|
|
||||||
<For each={groups()}>
|
|
||||||
{(group, index) => (
|
|
||||||
<div class="flex min-w-0 flex-col gap-4">
|
|
||||||
<HomeSessionGroupHeader
|
|
||||||
title={group.title}
|
|
||||||
onNewSession={index() === 0 ? openNewSession : undefined}
|
|
||||||
/>
|
|
||||||
<div class="flex min-w-0 flex-col gap-px">
|
|
||||||
<For each={group.sessions}>
|
|
||||||
{(record) => <HomeSessionRow record={record} openSession={openSession} />}
|
|
||||||
</For>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
</div>
|
||||||
</For>
|
)}
|
||||||
</Show>
|
</For>
|
||||||
</Show>
|
</Show>
|
||||||
</div>
|
</Show>
|
||||||
</div>
|
</div>
|
||||||
</Show>
|
</div>
|
||||||
</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 +252,39 @@ function HomeProjectColumn(props: {
|
|||||||
aria-label={props.language.t("home.project.add")}
|
aria-label={props.language.t("home.project.add")}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<Show
|
<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">
|
||||||
when={servers.list().length > 1}
|
<Show
|
||||||
fallback={
|
when={props.projects.length > 0}
|
||||||
<ProjectList
|
fallback={
|
||||||
projects={projects()}
|
<button
|
||||||
selectedProject={props.selectedProject}
|
type="button"
|
||||||
onSelectedProjectChange={props.selectProject}
|
class={`${HOME_PROJECT_NAV_ROW} text-v2-text-text-faint [&>[data-slot=icon-svg]]:text-v2-icon-icon-muted`}
|
||||||
onChooseProject={props.chooseProject}
|
onClick={props.chooseProject}
|
||||||
openNewSession={props.openNewSession}
|
>
|
||||||
editProject={props.editProject}
|
<IconV2 name="folder-add-left" size="small" />
|
||||||
closeProject={props.closeProject}
|
<span>{props.language.t("home.project.add")}</span>
|
||||||
clearNotifications={props.clearNotifications}
|
</button>
|
||||||
unseenCount={props.unseenCount}
|
}
|
||||||
language={props.language}
|
>
|
||||||
/>
|
<For each={props.projects}>
|
||||||
}
|
{(project) => (
|
||||||
>
|
<button
|
||||||
<For each={servers.list()}>
|
type="button"
|
||||||
{(server) => {
|
data-component="home-project-row"
|
||||||
const key = ServerConnection.key(server)
|
class={HOME_PROJECT_NAV_ROW}
|
||||||
const healthy = () => !!servers.health[key]?.healthy
|
classList={{ "bg-v2-overlay-simple-overlay-hover": props.selected === project.worktree }}
|
||||||
const [open, setOpen] = createSignal(true)
|
data-selected={props.selected === project.worktree ? "" : undefined}
|
||||||
|
aria-current={props.selected === project.worktree ? "page" : undefined}
|
||||||
return (
|
onClick={() => props.selectProject(project.worktree)}
|
||||||
<div class="max-h-[min(572px,calc(100vh_-_300px))] min-w-0 [scrollbar-width:none] [&::-webkit-scrollbar]:hidden">
|
>
|
||||||
<div class="relative h-7 group">
|
<HomeProjectAvatar project={project} />
|
||||||
<button
|
<span>{displayName(project)}</span>
|
||||||
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]"
|
</button>
|
||||||
disabled={!healthy()}
|
)}
|
||||||
onClick={() => setOpen((o) => !o)}
|
</For>
|
||||||
>
|
</Show>
|
||||||
<div class="size-4 flex items-center justify-center">
|
</div>
|
||||||
<ServerHealthIndicator health={servers.health[key]} />
|
<div class="mt-4 flex min-w-0 flex-col gap-1">
|
||||||
</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>
|
|
||||||
<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>
|
|
||||||
</Show>
|
|
||||||
<div class="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 +306,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 +320,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 +331,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 +347,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")}
|
||||||
@@ -578,10 +359,10 @@ function HomeSessionGroupHeader(props: { title: string; onNewSession?: () => voi
|
|||||||
}
|
}
|
||||||
|
|
||||||
function HomeSessionRow(props: { record: HomeSessionRecord; openSession: (session: Session) => void }) {
|
function HomeSessionRow(props: { record: HomeSessionRecord; openSession: (session: Session) => void }) {
|
||||||
const serverSync = useServerSync()
|
const globalSync = useGlobalSync()
|
||||||
const notification = useNotification()
|
const notification = useNotification()
|
||||||
const permission = usePermission()
|
const permission = usePermission()
|
||||||
const [sessionStore] = serverSync.child(props.record.session.directory, { bootstrap: false })
|
const [sessionStore] = globalSync.child(props.record.session.directory, { bootstrap: false })
|
||||||
const title = createMemo(() => sessionTitle(props.record.session.title) || props.record.session.id)
|
const title = createMemo(() => sessionTitle(props.record.session.title) || props.record.session.id)
|
||||||
const unseenCount = createMemo(() => notification.session.unseenCount(props.record.session.id))
|
const unseenCount = createMemo(() => notification.session.unseenCount(props.record.session.id))
|
||||||
const hasError = createMemo(() => notification.session.unseenHasError(props.record.session.id))
|
const hasError = createMemo(() => notification.session.unseenHasError(props.record.session.id))
|
||||||
@@ -679,14 +460,18 @@ function groupSessions(records: HomeSessionRecord[], language: ReturnType<typeof
|
|||||||
}
|
}
|
||||||
|
|
||||||
function LegacyHome() {
|
function LegacyHome() {
|
||||||
const sync = useServerSync()
|
const sync = useGlobalSync()
|
||||||
const layout = useLayout()
|
const layout = useLayout()
|
||||||
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 [promptText, setPromptText] = createSignal("")
|
||||||
|
const [selectedAgent, setSelectedAgent] = createSignal("frontend-specialist")
|
||||||
|
const [showProjectsDropdown, setShowProjectsDropdown] = createSignal(false)
|
||||||
|
|
||||||
const homedir = createMemo(() => sync.data.path.home)
|
const homedir = createMemo(() => sync.data.path.home)
|
||||||
const recent = createMemo(() => {
|
const recent = createMemo(() => {
|
||||||
return sync.data.project
|
return sync.data.project
|
||||||
@@ -695,8 +480,10 @@ function LegacyHome() {
|
|||||||
.slice(0, 5)
|
.slice(0, 5)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const currentProject = createMemo(() => recent()[0]?.worktree)
|
||||||
|
|
||||||
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"
|
||||||
@@ -733,119 +520,188 @@ function LegacyHome() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function handleModelSelect() {
|
||||||
|
dialog.show(() => <DialogSelectModel />)
|
||||||
|
}
|
||||||
|
|
||||||
|
function toggleAgent() {
|
||||||
|
const agents = ["frontend-specialist", "build", "general"]
|
||||||
|
const nextIndex = (agents.indexOf(selectedAgent()) + 1) % agents.length
|
||||||
|
setSelectedAgent(agents[nextIndex])
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleSubmit() {
|
||||||
|
const projectToOpen = currentProject()
|
||||||
|
if (projectToOpen) {
|
||||||
|
openProject(projectToOpen)
|
||||||
|
} else {
|
||||||
|
chooseProject()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const activeModelName = createMemo(() => {
|
||||||
|
const model = sync.data.config.model
|
||||||
|
if (!model) return "GPT-5.7 Pro"
|
||||||
|
const parts = model.split("/")
|
||||||
|
return parts[parts.length - 1]
|
||||||
|
})
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div class="mx-auto mt-55 w-full md:w-auto px-4">
|
<div class="mx-auto mt-24 w-full max-w-2xl px-6 flex flex-col items-center">
|
||||||
<Logo class="md:w-xl opacity-12" />
|
<div class="flex flex-col items-center gap-3 mb-10">
|
||||||
<Button
|
<div onClick={chooseProject} class="cursor-pointer hover:opacity-25 transition-opacity duration-200">
|
||||||
size="large"
|
<Logo class="w-48 opacity-15" />
|
||||||
variant="ghost"
|
</div>
|
||||||
class="mt-4 mx-auto text-14-regular text-text-weak"
|
<Button
|
||||||
onClick={() => dialog.show(() => <DialogSelectServer />)}
|
size="normal"
|
||||||
>
|
variant="ghost"
|
||||||
<div
|
class="text-12-regular text-text-weak px-3"
|
||||||
classList={{
|
onClick={() => dialog.show(() => <DialogSelectServer />)}
|
||||||
"size-2 rounded-full": true,
|
>
|
||||||
[serverDotClass()]: true,
|
<div
|
||||||
}}
|
classList={{
|
||||||
/>
|
"size-1.5 rounded-full mr-2": true,
|
||||||
{server.name}
|
[serverDotClass()]: true,
|
||||||
</Button>
|
}}
|
||||||
|
/>
|
||||||
|
{server.name}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
<Switch>
|
<Switch>
|
||||||
<Match when={sync.data.project.length > 0}>
|
<Match when={recent().length > 0}>
|
||||||
<div class="mt-20 w-full flex flex-col gap-4">
|
<div class="w-full flex flex-col items-center gap-6">
|
||||||
<div class="flex gap-2 items-center justify-between pl-3">
|
<div class="text-20-medium text-text-strong text-center">{language.t("session.new.title")}</div>
|
||||||
<div class="text-14-medium text-text-strong">{language.t("home.recentProjects")}</div>
|
|
||||||
<Button icon="folder-add-left" size="normal" class="pl-2 pr-3" onClick={chooseProject}>
|
<div class="w-full bg-surface-base border border-border-base rounded-xl p-4 flex flex-col gap-3 shadow-md relative">
|
||||||
{language.t("command.project.open")}
|
<textarea
|
||||||
</Button>
|
class="bg-transparent border-none outline-none text-14-regular text-text-base placeholder-text-weak w-full resize-none h-20 focus:outline-none"
|
||||||
</div>
|
placeholder="Ask anything, / for commands, @ for context..."
|
||||||
<ul class="flex flex-col gap-2">
|
value={promptText()}
|
||||||
<For each={recent()}>
|
onInput={(e) => setPromptText(e.currentTarget.value)}
|
||||||
{(project) => (
|
onKeyDown={(e) => {
|
||||||
|
if (e.key === "Enter" && !e.shiftKey) {
|
||||||
|
e.preventDefault()
|
||||||
|
handleSubmit()
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div class="flex flex-wrap items-center gap-2 pt-3 border-t border-border-weak-base">
|
||||||
|
<Button
|
||||||
|
size="small"
|
||||||
|
variant="ghost"
|
||||||
|
class="text-12-medium text-text-weak hover:text-text-strong flex items-center gap-1.5 px-2.5 py-1 bg-surface-raised-base hover:bg-surface-raised-base-hover border border-border-weak-base rounded-md"
|
||||||
|
onClick={toggleAgent}
|
||||||
|
>
|
||||||
|
<Icon name="sliders" size="small" class="shrink-0" />
|
||||||
|
<span>Agent: {selectedAgent()}</span>
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
size="small"
|
||||||
|
variant="ghost"
|
||||||
|
class="text-12-medium text-text-weak hover:text-text-strong flex items-center gap-1.5 px-2.5 py-1 bg-surface-raised-base hover:bg-surface-raised-base-hover border border-border-weak-base rounded-md"
|
||||||
|
onClick={handleModelSelect}
|
||||||
|
>
|
||||||
|
<Icon name="brain" size="small" class="shrink-0" />
|
||||||
|
<span>Model: {activeModelName()}</span>
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<div class="relative">
|
||||||
<Button
|
<Button
|
||||||
size="large"
|
size="small"
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
class="text-14-mono text-left justify-between px-3"
|
class="text-12-medium text-text-weak hover:text-text-strong flex items-center gap-1.5 px-2.5 py-1 bg-surface-raised-base hover:bg-surface-raised-base-hover border border-border-weak-base rounded-md"
|
||||||
onClick={() => openProject(project.worktree)}
|
onClick={() => setShowProjectsDropdown(!showProjectsDropdown())}
|
||||||
>
|
>
|
||||||
{project.worktree.replace(homedir(), "~")}
|
<Icon name="folder" size="small" class="shrink-0" />
|
||||||
<div class="text-14-regular text-text-weak">
|
<span>Project: {currentProject() ? getFilename(currentProject()) : "Select Project"}</span>
|
||||||
{DateTime.fromMillis(project.time.updated ?? project.time.created).toRelative()}
|
|
||||||
</div>
|
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
|
||||||
</For>
|
<Show when={showProjectsDropdown()}>
|
||||||
</ul>
|
<div class="absolute left-0 mt-1 w-64 bg-surface-raised-base border border-border-base rounded-lg p-2 shadow-lg z-50 flex flex-col gap-1">
|
||||||
|
<div class="text-10-semibold text-text-weak px-2 py-1 uppercase tracking-wider">
|
||||||
|
{language.t("home.recentProjects")}
|
||||||
|
</div>
|
||||||
|
<For each={recent()}>
|
||||||
|
{(project) => (
|
||||||
|
<button
|
||||||
|
class="text-12-mono text-left px-2 py-1.5 hover:bg-surface-raised-base-hover rounded flex items-center justify-between w-full"
|
||||||
|
onClick={() => {
|
||||||
|
openProject(project.worktree)
|
||||||
|
setShowProjectsDropdown(false)
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<span class="truncate">{getFilename(project.worktree)}</span>
|
||||||
|
<span class="text-10-regular text-text-weak shrink-0 pl-2">
|
||||||
|
{DateTime.fromMillis(project.time.updated ?? project.time.created).toRelative()}
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</For>
|
||||||
|
<div class="border-t border-border-weak-base my-1" />
|
||||||
|
<button
|
||||||
|
class="text-12-medium text-text-strong text-left px-2 py-1.5 hover:bg-surface-raised-base-hover rounded flex items-center gap-2 w-full"
|
||||||
|
onClick={() => {
|
||||||
|
setShowProjectsDropdown(false)
|
||||||
|
chooseProject()
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Icon name="folder-add-left" size="small" />
|
||||||
|
{language.t("command.project.open")}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</Show>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
size="small"
|
||||||
|
variant="ghost"
|
||||||
|
class="text-12-medium text-text-weak flex items-center gap-1.5 px-2.5 py-1 bg-surface-raised-base border border-border-weak-base rounded-md cursor-default pointer-events-none"
|
||||||
|
>
|
||||||
|
<Icon name="branch" size="small" class="shrink-0" />
|
||||||
|
<span>Branch: dev</span>
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Match>
|
</Match>
|
||||||
<Match when={!sync.ready}>
|
|
||||||
<div class="mt-30 mx-auto flex flex-col items-center gap-3">
|
|
||||||
<div class="text-12-regular text-text-weak">{language.t("common.loading")}</div>
|
|
||||||
<Button class="px-3" onClick={chooseProject}>
|
|
||||||
{language.t("command.project.open")}
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</Match>
|
|
||||||
<Match when={true}>
|
<Match when={true}>
|
||||||
<div class="mt-30 mx-auto flex flex-col items-center gap-3">
|
<div class="w-full flex flex-col items-center gap-6">
|
||||||
<Icon name="folder-add-left" size="large" />
|
<div class="text-20-medium text-text-strong text-center">{language.t("home.empty.title")}</div>
|
||||||
<div class="flex flex-col gap-1 items-center justify-center">
|
|
||||||
<div class="text-14-medium text-text-strong">{language.t("home.empty.title")}</div>
|
<div class="w-full bg-surface-base border border-border-base rounded-xl p-4 flex flex-col gap-3 shadow-md">
|
||||||
<div class="text-12-regular text-text-weak">{language.t("home.empty.description")}</div>
|
<div class="text-14-regular text-text-weak w-full min-h-[4rem] cursor-pointer" onClick={chooseProject}>
|
||||||
|
Ask anything, / for commands, @ for context...
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex flex-wrap items-center gap-2 pt-3 border-t border-border-weak-base">
|
||||||
|
<Button
|
||||||
|
size="small"
|
||||||
|
variant="ghost"
|
||||||
|
class="text-12-medium text-text-weak hover:text-text-strong flex items-center gap-1.5 px-2.5 py-1 bg-surface-raised-base hover:bg-surface-raised-base-hover border border-border-weak-base rounded-md"
|
||||||
|
onClick={chooseProject}
|
||||||
|
>
|
||||||
|
<Icon name="folder" size="small" class="shrink-0" />
|
||||||
|
<span>Open project</span>
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
size="small"
|
||||||
|
variant="ghost"
|
||||||
|
class="text-12-medium text-text-weak hover:text-text-strong flex items-center gap-1.5 px-2.5 py-1 bg-surface-raised-base hover:bg-surface-raised-base-hover border border-border-weak-base rounded-md"
|
||||||
|
onClick={handleModelSelect}
|
||||||
|
>
|
||||||
|
<Icon name="brain" size="small" class="shrink-0" />
|
||||||
|
<span>Model: {activeModelName()}</span>
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<Button class="px-3 mt-1" onClick={chooseProject}>
|
|
||||||
{language.t("command.project.open")}
|
|
||||||
</Button>
|
|
||||||
</div>
|
</div>
|
||||||
</Match>
|
</Match>
|
||||||
</Switch>
|
</Switch>
|
||||||
</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>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|||||||
+217
-217
@@ -16,7 +16,7 @@ import { makeEventListener } from "@solid-primitives/event-listener"
|
|||||||
import { useLocation, useNavigate, useParams } from "@solidjs/router"
|
import { useLocation, useNavigate, useParams } from "@solidjs/router"
|
||||||
import { useQuery } from "@tanstack/solid-query"
|
import { useQuery } from "@tanstack/solid-query"
|
||||||
import { useLayout, LocalProject } from "@/context/layout"
|
import { useLayout, LocalProject } from "@/context/layout"
|
||||||
import { useServerSync } from "@/context/server-sync"
|
import { useGlobalSync } from "@/context/global-sync"
|
||||||
import { Persist, persisted } from "@/utils/persist"
|
import { Persist, persisted } from "@/utils/persist"
|
||||||
import { base64Encode } from "@opencode-ai/core/util/encode"
|
import { base64Encode } from "@opencode-ai/core/util/encode"
|
||||||
import { decode64 } from "@/utils/base64"
|
import { decode64 } from "@/utils/base64"
|
||||||
@@ -35,7 +35,7 @@ import { DragDropProvider, DragDropSensors, DragOverlay, SortableProvider, close
|
|||||||
import type { DragEvent } from "@thisbeyond/solid-dnd"
|
import type { DragEvent } from "@thisbeyond/solid-dnd"
|
||||||
import { useProviders } from "@/hooks/use-providers"
|
import { useProviders } from "@/hooks/use-providers"
|
||||||
import { showToast, Toast, toaster } from "@opencode-ai/ui/toast"
|
import { showToast, Toast, toaster } from "@opencode-ai/ui/toast"
|
||||||
import { useServerSDK } from "@/context/server-sdk"
|
import { useGlobalSDK } from "@/context/global-sdk"
|
||||||
import { clearWorkspaceTerminals, getTerminalServerScope } from "@/context/terminal"
|
import { clearWorkspaceTerminals, getTerminalServerScope } from "@/context/terminal"
|
||||||
import { dropSessionCaches, pickSessionCacheEvictions } from "@/context/global-sync/session-cache"
|
import { dropSessionCaches, pickSessionCacheEvictions } from "@/context/global-sync/session-cache"
|
||||||
import {
|
import {
|
||||||
@@ -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"]),
|
||||||
@@ -111,8 +113,8 @@ export default function Layout(props: ParentProps) {
|
|||||||
let dialogDead = false
|
let dialogDead = false
|
||||||
|
|
||||||
const params = useParams()
|
const params = useParams()
|
||||||
const serverSDK = useServerSDK()
|
const globalSDK = useGlobalSDK()
|
||||||
const serverSync = useServerSync()
|
const globalSync = useGlobalSync()
|
||||||
const layout = useLayout()
|
const layout = useLayout()
|
||||||
const layoutReady = createMemo(() => layout.ready())
|
const layoutReady = createMemo(() => layout.ready())
|
||||||
const platform = usePlatform()
|
const platform = usePlatform()
|
||||||
@@ -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(() => {
|
||||||
@@ -135,7 +136,7 @@ export default function Layout(props: ParentProps) {
|
|||||||
if (!slug) return { slug, dir: "" }
|
if (!slug) return { slug, dir: "" }
|
||||||
const dir = decode64(slug)
|
const dir = decode64(slug)
|
||||||
if (!dir) return { slug, dir: "" }
|
if (!dir) return { slug, dir: "" }
|
||||||
const store = serverSync.peek(dir, { bootstrap: false })
|
const store = globalSync.peek(dir, { bootstrap: false })
|
||||||
return {
|
return {
|
||||||
slug,
|
slug,
|
||||||
store,
|
store,
|
||||||
@@ -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,
|
||||||
@@ -225,7 +226,7 @@ export default function Layout(props: ParentProps) {
|
|||||||
active: () => state.hoverProject,
|
active: () => state.hoverProject,
|
||||||
el: () => state.nav?.querySelector<HTMLElement>("[data-component='sidebar-rail']") ?? state.nav,
|
el: () => state.nav?.querySelector<HTMLElement>("[data-component='sidebar-rail']") ?? state.nav,
|
||||||
onActivate: (directory) => {
|
onActivate: (directory) => {
|
||||||
serverSync.child(directory)
|
globalSync.child(directory)
|
||||||
setState("hoverProject", directory)
|
setState("hoverProject", directory)
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
@@ -409,7 +410,7 @@ export default function Layout(props: ParentProps) {
|
|||||||
alertedAtBySession.delete(sessionKey)
|
alertedAtBySession.delete(sessionKey)
|
||||||
}
|
}
|
||||||
|
|
||||||
const unsub = serverSDK.event.listen((e) => {
|
const unsub = globalSDK.event.listen((e) => {
|
||||||
if (e.details?.type === "worktree.ready") {
|
if (e.details?.type === "worktree.ready") {
|
||||||
setBusy(e.name, false)
|
setBusy(e.name, false)
|
||||||
WorktreeState.ready(e.name)
|
WorktreeState.ready(e.name)
|
||||||
@@ -443,7 +444,7 @@ export default function Layout(props: ParentProps) {
|
|||||||
const props = e.details.properties
|
const props = e.details.properties
|
||||||
if (e.details.type === "permission.asked" && permission.autoResponds(e.details.properties, directory)) return
|
if (e.details.type === "permission.asked" && permission.autoResponds(e.details.properties, directory)) return
|
||||||
|
|
||||||
const [store] = serverSync.child(directory, { bootstrap: false })
|
const [store] = globalSync.child(directory, { bootstrap: false })
|
||||||
const session = store.session.find((s) => s.id === props.sessionID)
|
const session = store.session.find((s) => s.id === props.sessionID)
|
||||||
const sessionKey = `${directory}:${props.sessionID}`
|
const sessionKey = `${directory}:${props.sessionID}`
|
||||||
|
|
||||||
@@ -506,7 +507,7 @@ export default function Layout(props: ParentProps) {
|
|||||||
if (!currentDir() || !currentSession) return
|
if (!currentDir() || !currentSession) return
|
||||||
const sessionKey = `${currentDir()}:${currentSession}`
|
const sessionKey = `${currentDir()}:${currentSession}`
|
||||||
dismissSessionAlert(sessionKey)
|
dismissSessionAlert(sessionKey)
|
||||||
const [store] = serverSync.child(currentDir(), { bootstrap: false })
|
const [store] = globalSync.child(currentDir(), { bootstrap: false })
|
||||||
const childSessions = store.session.filter((s) => s.parentID === currentSession)
|
const childSessions = store.session.filter((s) => s.parentID === currentSession)
|
||||||
for (const child of childSessions) {
|
for (const child of childSessions) {
|
||||||
dismissSessionAlert(`${currentDir()}:${child.id}`)
|
dismissSessionAlert(`${currentDir()}:${child.id}`)
|
||||||
@@ -544,11 +545,11 @@ export default function Layout(props: ParentProps) {
|
|||||||
const direct = projects.find((p) => pathKey(p.worktree) === key)
|
const direct = projects.find((p) => pathKey(p.worktree) === key)
|
||||||
if (direct) return direct
|
if (direct) return direct
|
||||||
|
|
||||||
const [child] = serverSync.child(directory, { bootstrap: false })
|
const [child] = globalSync.child(directory, { bootstrap: false })
|
||||||
const id = child.project
|
const id = child.project
|
||||||
if (!id) return
|
if (!id) return
|
||||||
|
|
||||||
const meta = serverSync.data.project.find((p) => p.id === id)
|
const meta = globalSync.data.project.find((p) => p.id === id)
|
||||||
const root = meta?.worktree
|
const root = meta?.worktree
|
||||||
if (!root) return
|
if (!root) return
|
||||||
|
|
||||||
@@ -639,7 +640,7 @@ export default function Layout(props: ParentProps) {
|
|||||||
|
|
||||||
const result: Session[] = []
|
const result: Session[] = []
|
||||||
for (const dir of dirs) {
|
for (const dir of dirs) {
|
||||||
const [dirStore] = serverSync.child(dir, { bootstrap: true })
|
const [dirStore] = globalSync.child(dir, { bootstrap: true })
|
||||||
const dirSessions = sortedRootSessions(dirStore, now)
|
const dirSessions = sortedRootSessions(dirStore, now)
|
||||||
result.push(...dirSessions)
|
result.push(...dirSessions)
|
||||||
}
|
}
|
||||||
@@ -691,7 +692,7 @@ export default function Layout(props: ParentProps) {
|
|||||||
|
|
||||||
createEffect(() => {
|
createEffect(() => {
|
||||||
route()
|
route()
|
||||||
serverSDK.url
|
globalSDK.url
|
||||||
|
|
||||||
prefetchToken.value += 1
|
prefetchToken.value += 1
|
||||||
clearSessionPrefetchInflight()
|
clearSessionPrefetchInflight()
|
||||||
@@ -738,13 +739,13 @@ export default function Layout(props: ParentProps) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function prefetchMessages(directory: string, sessionID: string, token: number) {
|
async function prefetchMessages(directory: string, sessionID: string, token: number) {
|
||||||
const [store, setStore] = serverSync.child(directory, { bootstrap: false })
|
const [store, setStore] = globalSync.child(directory, { bootstrap: false })
|
||||||
|
|
||||||
return runSessionPrefetch({
|
return runSessionPrefetch({
|
||||||
directory,
|
directory,
|
||||||
sessionID,
|
sessionID,
|
||||||
task: (rev) =>
|
task: (rev) =>
|
||||||
retry(() => serverSDK.client.session.messages({ directory, sessionID, limit: prefetchChunk }))
|
retry(() => globalSDK.client.session.messages({ directory, sessionID, limit: prefetchChunk }))
|
||||||
.then((messages) => {
|
.then((messages) => {
|
||||||
if (prefetchToken.value !== token) return
|
if (prefetchToken.value !== token) return
|
||||||
if (!isSessionPrefetchCurrent(directory, sessionID, rev)) return
|
if (!isSessionPrefetchCurrent(directory, sessionID, rev)) return
|
||||||
@@ -764,7 +765,7 @@ export default function Layout(props: ParentProps) {
|
|||||||
if (stale.length > 0) {
|
if (stale.length > 0) {
|
||||||
clearSessionPrefetch(directory, stale)
|
clearSessionPrefetch(directory, stale)
|
||||||
for (const id of stale) {
|
for (const id of stale) {
|
||||||
serverSync.todo.set(id, undefined)
|
globalSync.todo.set(id, undefined)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -829,7 +830,7 @@ export default function Layout(props: ParentProps) {
|
|||||||
const directory = session.directory
|
const directory = session.directory
|
||||||
if (!directory) return
|
if (!directory) return
|
||||||
|
|
||||||
const [store] = serverSync.child(directory, { bootstrap: false })
|
const [store] = globalSync.child(directory, { bootstrap: false })
|
||||||
const cached = untrack(() => {
|
const cached = untrack(() => {
|
||||||
const info = getSessionPrefetch(directory, session.id)
|
const info = getSessionPrefetch(directory, session.id)
|
||||||
return shouldSkipSessionPrefetch({
|
return shouldSkipSessionPrefetch({
|
||||||
@@ -934,7 +935,7 @@ export default function Layout(props: ParentProps) {
|
|||||||
if (!target) return
|
if (!target) return
|
||||||
|
|
||||||
// warm up child store to prevent flicker
|
// warm up child store to prevent flicker
|
||||||
serverSync.child(target.worktree)
|
globalSync.child(target.worktree)
|
||||||
void openProject(target.worktree)
|
void openProject(target.worktree)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -943,7 +944,7 @@ export default function Layout(props: ParentProps) {
|
|||||||
const target = projects[index]
|
const target = projects[index]
|
||||||
if (!target) return
|
if (!target) return
|
||||||
|
|
||||||
serverSync.child(target.worktree)
|
globalSync.child(target.worktree)
|
||||||
void openProject(target.worktree)
|
void openProject(target.worktree)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -972,12 +973,12 @@ export default function Layout(props: ParentProps) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function archiveSession(session: Session) {
|
async function archiveSession(session: Session) {
|
||||||
const [store, setStore] = serverSync.child(session.directory)
|
const [store, setStore] = globalSync.child(session.directory)
|
||||||
const sessions = store.session ?? []
|
const sessions = store.session ?? []
|
||||||
const index = sessions.findIndex((s) => s.id === session.id)
|
const index = sessions.findIndex((s) => s.id === session.id)
|
||||||
const nextSession = sessions[index + 1] ?? sessions[index - 1]
|
const nextSession = sessions[index + 1] ?? sessions[index - 1]
|
||||||
|
|
||||||
await serverSDK.client.session.update({
|
await globalSDK.client.session.update({
|
||||||
directory: session.directory,
|
directory: session.directory,
|
||||||
sessionID: session.id,
|
sessionID: session.id,
|
||||||
time: { archived: Date.now() },
|
time: { archived: Date.now() },
|
||||||
@@ -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
|
||||||
@@ -1245,11 +1246,11 @@ export default function Layout(props: ParentProps) {
|
|||||||
)
|
)
|
||||||
if (known) return known[0]
|
if (known) return known[0]
|
||||||
|
|
||||||
const [child] = serverSync.child(directory, { bootstrap: false })
|
const [child] = globalSync.child(directory, { bootstrap: false })
|
||||||
const id = child.project
|
const id = child.project
|
||||||
if (!id) return directory
|
if (!id) return directory
|
||||||
|
|
||||||
const meta = serverSync.data.project.find((item) => item.id === id)
|
const meta = globalSync.data.project.find((item) => item.id === id)
|
||||||
return meta?.worktree ?? directory
|
return meta?.worktree ?? directory
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1297,7 +1298,7 @@ export default function Layout(props: ParentProps) {
|
|||||||
}
|
}
|
||||||
const refreshDirs = async (target?: string) => {
|
const refreshDirs = async (target?: string) => {
|
||||||
if (!target || target === root || canOpen(target)) return canOpen(target)
|
if (!target || target === root || canOpen(target)) return canOpen(target)
|
||||||
const listed = await serverSDK.client.worktree
|
const listed = await globalSDK.client.worktree
|
||||||
.list({ directory: root })
|
.list({ directory: root })
|
||||||
.then((x) => x.data ?? [])
|
.then((x) => x.data ?? [])
|
||||||
.catch(() => [] as string[])
|
.catch(() => [] as string[])
|
||||||
@@ -1306,13 +1307,13 @@ export default function Layout(props: ParentProps) {
|
|||||||
}
|
}
|
||||||
const openSession = async (target: { directory: string; id: string }) => {
|
const openSession = async (target: { directory: string; id: string }) => {
|
||||||
if (!canOpen(target.directory)) return false
|
if (!canOpen(target.directory)) return false
|
||||||
const [data] = serverSync.child(target.directory, { bootstrap: false })
|
const [data] = globalSync.child(target.directory, { bootstrap: false })
|
||||||
if (data.session.some((item) => item.id === target.id)) {
|
if (data.session.some((item) => item.id === target.id)) {
|
||||||
setStore("lastProjectSession", root, { directory: target.directory, id: target.id, at: Date.now() })
|
setStore("lastProjectSession", root, { directory: target.directory, id: target.id, at: Date.now() })
|
||||||
navigateWithSidebarReset(`/${base64Encode(target.directory)}/session/${target.id}`)
|
navigateWithSidebarReset(`/${base64Encode(target.directory)}/session/${target.id}`)
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
const resolved = await serverSDK.client.session
|
const resolved = await globalSDK.client.session
|
||||||
.get({ sessionID: target.id })
|
.get({ sessionID: target.id })
|
||||||
.then((x) => x.data)
|
.then((x) => x.data)
|
||||||
.catch(() => undefined)
|
.catch(() => undefined)
|
||||||
@@ -1332,7 +1333,7 @@ export default function Layout(props: ParentProps) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const latest = latestRootSession(
|
const latest = latestRootSession(
|
||||||
dirs.map((item) => serverSync.child(item, { bootstrap: false })[0]),
|
dirs.map((item) => globalSync.child(item, { bootstrap: false })[0]),
|
||||||
Date.now(),
|
Date.now(),
|
||||||
)
|
)
|
||||||
if (latest && (await openSession(latest))) {
|
if (latest && (await openSession(latest))) {
|
||||||
@@ -1343,7 +1344,7 @@ export default function Layout(props: ParentProps) {
|
|||||||
await Promise.all(
|
await Promise.all(
|
||||||
dirs.map(async (item) => ({
|
dirs.map(async (item) => ({
|
||||||
path: { directory: item },
|
path: { directory: item },
|
||||||
session: await serverSDK.client.session
|
session: await globalSDK.client.session
|
||||||
.list({ directory: item })
|
.list({ directory: item })
|
||||||
.then((x) => x.data ?? [])
|
.then((x) => x.data ?? [])
|
||||||
.catch(() => []),
|
.catch(() => []),
|
||||||
@@ -1404,11 +1405,11 @@ export default function Layout(props: ParentProps) {
|
|||||||
const name = next === getFilename(project.worktree) ? "" : next
|
const name = next === getFilename(project.worktree) ? "" : next
|
||||||
|
|
||||||
if (project.id && project.id !== "global") {
|
if (project.id && project.id !== "global") {
|
||||||
await serverSDK.client.project.update({ projectID: project.id, directory: project.worktree, name })
|
await globalSDK.client.project.update({ projectID: project.id, directory: project.worktree, name })
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
serverSync.project.meta(project.worktree, { name })
|
globalSync.project.meta(project.worktree, { name })
|
||||||
}
|
}
|
||||||
|
|
||||||
const renameWorkspace = (directory: string, next: string, projectId?: string, branch?: string) => {
|
const renameWorkspace = (directory: string, next: string, projectId?: string, branch?: string) => {
|
||||||
@@ -1505,7 +1506,7 @@ export default function Layout(props: ParentProps) {
|
|||||||
|
|
||||||
setBusy(directory, true)
|
setBusy(directory, true)
|
||||||
|
|
||||||
const result = await serverSDK.client.worktree
|
const result = await globalSDK.client.worktree
|
||||||
.remove({ directory: root, worktreeRemoveInput: { directory } })
|
.remove({ directory: root, worktreeRemoveInput: { directory } })
|
||||||
.then((x) => x.data)
|
.then((x) => x.data)
|
||||||
.catch((err) => {
|
.catch((err) => {
|
||||||
@@ -1524,7 +1525,7 @@ export default function Layout(props: ParentProps) {
|
|||||||
clearLastProjectSession(root)
|
clearLastProjectSession(root)
|
||||||
}
|
}
|
||||||
|
|
||||||
serverSync.set(
|
globalSync.set(
|
||||||
"project",
|
"project",
|
||||||
produce((draft) => {
|
produce((draft) => {
|
||||||
const project = draft.find((item) => item.worktree === root)
|
const project = draft.find((item) => item.worktree === root)
|
||||||
@@ -1563,7 +1564,7 @@ export default function Layout(props: ParentProps) {
|
|||||||
})
|
})
|
||||||
const dismiss = () => toaster.dismiss(progress)
|
const dismiss = () => toaster.dismiss(progress)
|
||||||
|
|
||||||
const sessions: Session[] = await serverSDK.client.session
|
const sessions: Session[] = await globalSDK.client.session
|
||||||
.list({ directory })
|
.list({ directory })
|
||||||
.then((x) => x.data ?? [])
|
.then((x) => x.data ?? [])
|
||||||
.catch(() => [])
|
.catch(() => [])
|
||||||
@@ -1574,9 +1575,9 @@ export default function Layout(props: ParentProps) {
|
|||||||
platform,
|
platform,
|
||||||
getTerminalServerScope(server.current, server.key),
|
getTerminalServerScope(server.current, server.key),
|
||||||
)
|
)
|
||||||
await serverSDK.client.instance.dispose({ directory }).catch(() => undefined)
|
await globalSDK.client.instance.dispose({ directory }).catch(() => undefined)
|
||||||
|
|
||||||
const result = await serverSDK.client.worktree
|
const result = await globalSDK.client.worktree
|
||||||
.reset({ directory: root, worktreeResetInput: { directory } })
|
.reset({ directory: root, worktreeResetInput: { directory } })
|
||||||
.then((x) => x.data)
|
.then((x) => x.data)
|
||||||
.catch((err) => {
|
.catch((err) => {
|
||||||
@@ -1598,7 +1599,7 @@ export default function Layout(props: ParentProps) {
|
|||||||
sessions
|
sessions
|
||||||
.filter((session) => session.time.archived === undefined)
|
.filter((session) => session.time.archived === undefined)
|
||||||
.map((session) =>
|
.map((session) =>
|
||||||
serverSDK.client.session
|
globalSDK.client.session
|
||||||
.update({
|
.update({
|
||||||
sessionID: session.id,
|
sessionID: session.id,
|
||||||
directory: session.directory,
|
directory: session.directory,
|
||||||
@@ -1639,7 +1640,7 @@ export default function Layout(props: ParentProps) {
|
|||||||
})
|
})
|
||||||
|
|
||||||
onMount(() => {
|
onMount(() => {
|
||||||
serverSDK.client.file
|
globalSDK.client.file
|
||||||
.status({ directory: props.directory })
|
.status({ directory: props.directory })
|
||||||
.then((x) => {
|
.then((x) => {
|
||||||
const files = x.data ?? []
|
const files = x.data ?? []
|
||||||
@@ -1698,7 +1699,7 @@ export default function Layout(props: ParentProps) {
|
|||||||
})
|
})
|
||||||
|
|
||||||
const refresh = async () => {
|
const refresh = async () => {
|
||||||
const sessions = await serverSDK.client.session
|
const sessions = await globalSDK.client.session
|
||||||
.list({ directory: props.directory })
|
.list({ directory: props.directory })
|
||||||
.then((x) => x.data ?? [])
|
.then((x) => x.data ?? [])
|
||||||
.catch(() => [])
|
.catch(() => [])
|
||||||
@@ -1707,7 +1708,7 @@ export default function Layout(props: ParentProps) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
onMount(() => {
|
onMount(() => {
|
||||||
serverSDK.client.file
|
globalSDK.client.file
|
||||||
.status({ directory: props.directory })
|
.status({ directory: props.directory })
|
||||||
.then((x) => {
|
.then((x) => {
|
||||||
const files = x.data ?? []
|
const files = x.data ?? []
|
||||||
@@ -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`,
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -1841,7 +1842,7 @@ export default function Layout(props: ParentProps) {
|
|||||||
const next = new Set(dirs)
|
const next = new Set(dirs)
|
||||||
for (const directory of next) {
|
for (const directory of next) {
|
||||||
if (loadedSessionDirs.has(directory)) continue
|
if (loadedSessionDirs.has(directory)) continue
|
||||||
void serverSync.project.loadSessions(directory)
|
void globalSync.project.loadSessions(directory)
|
||||||
}
|
}
|
||||||
|
|
||||||
loadedSessionDirs.clear()
|
loadedSessionDirs.clear()
|
||||||
@@ -1938,7 +1939,7 @@ export default function Layout(props: ParentProps) {
|
|||||||
|
|
||||||
const createWorkspace = async (project: LocalProject) => {
|
const createWorkspace = async (project: LocalProject) => {
|
||||||
clearSidebarHoverState()
|
clearSidebarHoverState()
|
||||||
const created = await serverSDK.client.worktree
|
const created = await globalSDK.client.worktree
|
||||||
.create({ directory: project.worktree })
|
.create({ directory: project.worktree })
|
||||||
.then((x) => x.data)
|
.then((x) => x.data)
|
||||||
.catch((err) => {
|
.catch((err) => {
|
||||||
@@ -1972,7 +1973,7 @@ export default function Layout(props: ParentProps) {
|
|||||||
return [created.directory, ...next]
|
return [created.directory, ...next]
|
||||||
})
|
})
|
||||||
|
|
||||||
serverSync.child(created.directory)
|
globalSync.child(created.directory)
|
||||||
navigateWithSidebarReset(`/${base64Encode(created.directory)}/session`)
|
navigateWithSidebarReset(`/${base64Encode(created.directory)}/session`)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2077,7 +2078,7 @@ export default function Layout(props: ParentProps) {
|
|||||||
if (!item) return false
|
if (!item) return false
|
||||||
return item.vcs === "git" || layout.sidebar.workspaces(item.worktree)()
|
return item.vcs === "git" || layout.sidebar.workspaces(item.worktree)()
|
||||||
})
|
})
|
||||||
const homedir = createMemo(() => serverSync.data.path.home)
|
const homedir = createMemo(() => globalSync.data.path.home)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
@@ -2362,180 +2363,179 @@ export default function Layout(props: ParentProps) {
|
|||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
|
|
||||||
return (
|
if (USE_NEW_DESIGN) {
|
||||||
<Show
|
return (
|
||||||
when={!newDesign()}
|
<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">
|
||||||
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">
|
|
||||||
{autoselecting() ?? ""}
|
|
||||||
<Titlebar update={titlebarUpdate} />
|
|
||||||
<main
|
|
||||||
class="flex-1 min-h-0 min-w-0 overflow-x-hidden flex flex-col items-start contain-strict bg-v2-background-bg-base"
|
|
||||||
classList={{
|
|
||||||
"m-2 mt-0 rounded-[10px] shadow-[var(--v2-elevation-raised)] overflow-hidden": !!params.id || !params.dir,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Show when={!autoselecting.loading} fallback={<div class="size-full" />}>
|
|
||||||
{props.children}
|
|
||||||
</Show>
|
|
||||||
</main>
|
|
||||||
{import.meta.env.DEV && <DebugBar />}
|
|
||||||
<Toast.Region />
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<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} />
|
||||||
<Show when={updateVersion() !== undefined}>
|
<main
|
||||||
<UpdateAvailableToast version={updateVersion() ?? ""} install={installUpdate} language={language} />
|
class="flex-1 min-h-0 min-w-0 overflow-x-hidden flex flex-col items-start contain-strict bg-v2-background-bg-base"
|
||||||
</Show>
|
classList={{
|
||||||
<div class="flex-1 min-h-0 min-w-0 flex">
|
"m-2 mt-0 rounded-[10px] shadow-[var(--v2-elevation-raised)] overflow-hidden": !!params.id || !params.dir,
|
||||||
<div class="flex-1 min-h-0 relative">
|
}}
|
||||||
<div class="size-full relative overflow-x-hidden">
|
>
|
||||||
<nav
|
<Show when={!autoselecting.loading} fallback={<div class="size-full" />}>
|
||||||
aria-label={language.t("sidebar.nav.projectsAndSessions")}
|
{props.children}
|
||||||
data-component="sidebar-nav-desktop"
|
</Show>
|
||||||
classList={{
|
</main>
|
||||||
"hidden xl:block": true,
|
{import.meta.env.DEV && <DebugBar />}
|
||||||
"absolute inset-y-0 left-0": true,
|
|
||||||
"z-10": true,
|
|
||||||
}}
|
|
||||||
style={{ width: `${side()}px` }}
|
|
||||||
ref={(el) => {
|
|
||||||
setState("nav", el)
|
|
||||||
}}
|
|
||||||
onMouseEnter={() => {
|
|
||||||
disarm()
|
|
||||||
}}
|
|
||||||
onMouseLeave={() => {
|
|
||||||
aim.reset()
|
|
||||||
if (!sidebarHovering()) return
|
|
||||||
|
|
||||||
arm()
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<div class="@container w-full h-full contain-strict">{sidebarContent()}</div>
|
|
||||||
</nav>
|
|
||||||
|
|
||||||
<Show when={layout.sidebar.opened()}>
|
|
||||||
<div
|
|
||||||
class="hidden xl:block absolute inset-y-0 z-30 w-0 overflow-visible"
|
|
||||||
style={{ left: `${side()}px` }}
|
|
||||||
onPointerDown={() => setState("sizing", true)}
|
|
||||||
>
|
|
||||||
<ResizeHandle
|
|
||||||
direction="horizontal"
|
|
||||||
size={layout.sidebar.width()}
|
|
||||||
min={244}
|
|
||||||
max={typeof window === "undefined" ? 1000 : window.innerWidth * 0.3 + 64}
|
|
||||||
onResize={(w) => {
|
|
||||||
setState("sizing", true)
|
|
||||||
if (sizet !== undefined) clearTimeout(sizet)
|
|
||||||
sizet = window.setTimeout(() => setState("sizing", false), 120)
|
|
||||||
layout.sidebar.resize(w)
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</Show>
|
|
||||||
|
|
||||||
<div
|
|
||||||
class="hidden xl:block pointer-events-none absolute top-0 right-0 z-0 border-t border-border-weaker-base"
|
|
||||||
style={{ left: "calc(4rem + 12px)" }}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<div class="xl:hidden">
|
|
||||||
<div
|
|
||||||
classList={{
|
|
||||||
"fixed inset-x-0 top-10 bottom-0 z-40 transition-opacity duration-200": true,
|
|
||||||
"opacity-100 pointer-events-auto": layout.mobileSidebar.opened(),
|
|
||||||
"opacity-0 pointer-events-none": !layout.mobileSidebar.opened(),
|
|
||||||
}}
|
|
||||||
onClick={(e) => {
|
|
||||||
if (e.target === e.currentTarget) layout.mobileSidebar.hide()
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<nav
|
|
||||||
aria-label={language.t("sidebar.nav.projectsAndSessions")}
|
|
||||||
data-component="sidebar-nav-mobile"
|
|
||||||
classList={{
|
|
||||||
"@container fixed top-10 bottom-0 left-0 z-50 w-full max-w-[400px] overflow-hidden border-r border-border-weaker-base bg-background-base transition-transform duration-200 ease-out": true,
|
|
||||||
"translate-x-0": layout.mobileSidebar.opened(),
|
|
||||||
"-translate-x-full": !layout.mobileSidebar.opened(),
|
|
||||||
}}
|
|
||||||
onClick={(e) => e.stopPropagation()}
|
|
||||||
>
|
|
||||||
{sidebarContent(true)}
|
|
||||||
</nav>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div
|
|
||||||
classList={{
|
|
||||||
"absolute inset-0": true,
|
|
||||||
"xl:inset-y-0 xl:right-0 xl:left-[var(--main-left)]": true,
|
|
||||||
"z-20": true,
|
|
||||||
"transition-[left] duration-200 ease-[cubic-bezier(0.22,1,0.36,1)] will-change-[left] motion-reduce:transition-none":
|
|
||||||
!state.sizing,
|
|
||||||
}}
|
|
||||||
style={{
|
|
||||||
"--main-left": layout.sidebar.opened() ? `${side()}px` : "4rem",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<main
|
|
||||||
classList={{
|
|
||||||
"size-full overflow-x-hidden flex flex-col items-start contain-strict border-t border-border-weak-base bg-background-base xl:border-l xl:rounded-tl-[12px]": true,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Show when={!autoselecting.loading} fallback={<div class="size-full" />}>
|
|
||||||
{props.children}
|
|
||||||
</Show>
|
|
||||||
</main>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div
|
|
||||||
classList={{
|
|
||||||
"hidden xl:flex absolute inset-y-0 left-16 z-30": true,
|
|
||||||
"opacity-100 translate-x-0 pointer-events-auto": state.peeked && !layout.sidebar.opened(),
|
|
||||||
"opacity-0 -translate-x-2 pointer-events-none": !state.peeked || layout.sidebar.opened(),
|
|
||||||
"transition-[opacity,transform] motion-reduce:transition-none": true,
|
|
||||||
"duration-180 ease-out": state.peeked && !layout.sidebar.opened(),
|
|
||||||
"duration-120 ease-in": !state.peeked || layout.sidebar.opened(),
|
|
||||||
}}
|
|
||||||
onMouseMove={disarm}
|
|
||||||
onMouseEnter={() => {
|
|
||||||
disarm()
|
|
||||||
aim.reset()
|
|
||||||
}}
|
|
||||||
onPointerDown={disarm}
|
|
||||||
onMouseLeave={() => {
|
|
||||||
arm()
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Show when={peekProject()}>
|
|
||||||
<SidebarPanel project={peekProject} merged={false} />
|
|
||||||
</Show>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div
|
|
||||||
classList={{
|
|
||||||
"hidden xl:block pointer-events-none absolute inset-y-0 right-0 z-25 overflow-hidden": true,
|
|
||||||
"opacity-100 translate-x-0": state.peeked && !layout.sidebar.opened(),
|
|
||||||
"opacity-0 -translate-x-2": !state.peeked || layout.sidebar.opened(),
|
|
||||||
"transition-[opacity,transform] motion-reduce:transition-none": true,
|
|
||||||
"duration-180 ease-out": state.peeked && !layout.sidebar.opened(),
|
|
||||||
"duration-120 ease-in": !state.peeked || layout.sidebar.opened(),
|
|
||||||
}}
|
|
||||||
style={{ left: `calc(4rem + ${panel()}px)` }}
|
|
||||||
>
|
|
||||||
<div class="h-full w-px" style={{ "box-shadow": "var(--shadow-sidebar-overlay)" }} />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{import.meta.env.DEV && <DebugBar />}
|
|
||||||
</div>
|
|
||||||
<Toast.Region />
|
<Toast.Region />
|
||||||
</div>
|
</div>
|
||||||
</Show>
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
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">
|
||||||
|
{autoselecting() ?? ""}
|
||||||
|
<Titlebar update={titlebarUpdate} />
|
||||||
|
<Show when={updateVersion() !== undefined}>
|
||||||
|
<UpdateAvailableToast version={updateVersion() ?? ""} install={installUpdate} language={language} />
|
||||||
|
</Show>
|
||||||
|
<div class="flex-1 min-h-0 min-w-0 flex">
|
||||||
|
<div class="flex-1 min-h-0 relative">
|
||||||
|
<div class="size-full relative overflow-x-hidden">
|
||||||
|
<nav
|
||||||
|
aria-label={language.t("sidebar.nav.projectsAndSessions")}
|
||||||
|
data-component="sidebar-nav-desktop"
|
||||||
|
classList={{
|
||||||
|
"hidden xl:block": true,
|
||||||
|
"absolute inset-y-0 left-0": true,
|
||||||
|
"z-10": true,
|
||||||
|
}}
|
||||||
|
style={{ width: `${side()}px` }}
|
||||||
|
ref={(el) => {
|
||||||
|
setState("nav", el)
|
||||||
|
}}
|
||||||
|
onMouseEnter={() => {
|
||||||
|
disarm()
|
||||||
|
}}
|
||||||
|
onMouseLeave={() => {
|
||||||
|
aim.reset()
|
||||||
|
if (!sidebarHovering()) return
|
||||||
|
|
||||||
|
arm()
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div class="@container w-full h-full contain-strict">{sidebarContent()}</div>
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
<Show when={layout.sidebar.opened()}>
|
||||||
|
<div
|
||||||
|
class="hidden xl:block absolute inset-y-0 z-30 w-0 overflow-visible"
|
||||||
|
style={{ left: `${side()}px` }}
|
||||||
|
onPointerDown={() => setState("sizing", true)}
|
||||||
|
>
|
||||||
|
<ResizeHandle
|
||||||
|
direction="horizontal"
|
||||||
|
size={layout.sidebar.width()}
|
||||||
|
min={244}
|
||||||
|
max={typeof window === "undefined" ? 1000 : window.innerWidth * 0.3 + 64}
|
||||||
|
onResize={(w) => {
|
||||||
|
setState("sizing", true)
|
||||||
|
if (sizet !== undefined) clearTimeout(sizet)
|
||||||
|
sizet = window.setTimeout(() => setState("sizing", false), 120)
|
||||||
|
layout.sidebar.resize(w)
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</Show>
|
||||||
|
|
||||||
|
<div
|
||||||
|
class="hidden xl:block pointer-events-none absolute top-0 right-0 z-0 border-t border-border-weaker-base"
|
||||||
|
style={{ left: "calc(4rem + 12px)" }}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div class="xl:hidden">
|
||||||
|
<div
|
||||||
|
classList={{
|
||||||
|
"fixed inset-x-0 top-10 bottom-0 z-40 transition-opacity duration-200": true,
|
||||||
|
"opacity-100 pointer-events-auto": layout.mobileSidebar.opened(),
|
||||||
|
"opacity-0 pointer-events-none": !layout.mobileSidebar.opened(),
|
||||||
|
}}
|
||||||
|
onClick={(e) => {
|
||||||
|
if (e.target === e.currentTarget) layout.mobileSidebar.hide()
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<nav
|
||||||
|
aria-label={language.t("sidebar.nav.projectsAndSessions")}
|
||||||
|
data-component="sidebar-nav-mobile"
|
||||||
|
classList={{
|
||||||
|
"@container fixed top-10 bottom-0 left-0 z-50 w-full max-w-[400px] overflow-hidden border-r border-border-weaker-base bg-background-base transition-transform duration-200 ease-out": true,
|
||||||
|
"translate-x-0": layout.mobileSidebar.opened(),
|
||||||
|
"-translate-x-full": !layout.mobileSidebar.opened(),
|
||||||
|
}}
|
||||||
|
onClick={(e) => e.stopPropagation()}
|
||||||
|
>
|
||||||
|
{sidebarContent(true)}
|
||||||
|
</nav>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
classList={{
|
||||||
|
"absolute inset-0": true,
|
||||||
|
"xl:inset-y-0 xl:right-0 xl:left-[var(--main-left)]": true,
|
||||||
|
"z-20": true,
|
||||||
|
"transition-[left] duration-200 ease-[cubic-bezier(0.22,1,0.36,1)] will-change-[left] motion-reduce:transition-none":
|
||||||
|
!state.sizing,
|
||||||
|
}}
|
||||||
|
style={{
|
||||||
|
"--main-left": layout.sidebar.opened() ? `${side()}px` : "4rem",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<main
|
||||||
|
classList={{
|
||||||
|
"size-full overflow-x-hidden flex flex-col items-start contain-strict border-t border-border-weak-base bg-background-base xl:border-l xl:rounded-tl-[12px]": true,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Show when={!autoselecting.loading} fallback={<div class="size-full" />}>
|
||||||
|
{props.children}
|
||||||
|
</Show>
|
||||||
|
</main>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
classList={{
|
||||||
|
"hidden xl:flex absolute inset-y-0 left-16 z-30": true,
|
||||||
|
"opacity-100 translate-x-0 pointer-events-auto": state.peeked && !layout.sidebar.opened(),
|
||||||
|
"opacity-0 -translate-x-2 pointer-events-none": !state.peeked || layout.sidebar.opened(),
|
||||||
|
"transition-[opacity,transform] motion-reduce:transition-none": true,
|
||||||
|
"duration-180 ease-out": state.peeked && !layout.sidebar.opened(),
|
||||||
|
"duration-120 ease-in": !state.peeked || layout.sidebar.opened(),
|
||||||
|
}}
|
||||||
|
onMouseMove={disarm}
|
||||||
|
onMouseEnter={() => {
|
||||||
|
disarm()
|
||||||
|
aim.reset()
|
||||||
|
}}
|
||||||
|
onPointerDown={disarm}
|
||||||
|
onMouseLeave={() => {
|
||||||
|
arm()
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Show when={peekProject()}>
|
||||||
|
<SidebarPanel project={peekProject} merged={false} />
|
||||||
|
</Show>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
classList={{
|
||||||
|
"hidden xl:block pointer-events-none absolute inset-y-0 right-0 z-25 overflow-hidden": true,
|
||||||
|
"opacity-100 translate-x-0": state.peeked && !layout.sidebar.opened(),
|
||||||
|
"opacity-0 -translate-x-2": !state.peeked || layout.sidebar.opened(),
|
||||||
|
"transition-[opacity,transform] motion-reduce:transition-none": true,
|
||||||
|
"duration-180 ease-out": state.peeked && !layout.sidebar.opened(),
|
||||||
|
"duration-120 ease-in": !state.peeked || layout.sidebar.opened(),
|
||||||
|
}}
|
||||||
|
style={{ left: `calc(4rem + ${panel()}px)` }}
|
||||||
|
>
|
||||||
|
<div class="h-full w-px" style={{ "box-shadow": "var(--shadow-sidebar-overlay)" }} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{import.meta.env.DEV && <DebugBar />}
|
||||||
|
</div>
|
||||||
|
<Toast.Region />
|
||||||
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import { Tooltip } from "@opencode-ai/ui/tooltip"
|
|||||||
import { getFilename } from "@opencode-ai/core/util/path"
|
import { getFilename } from "@opencode-ai/core/util/path"
|
||||||
import { A, useParams } from "@solidjs/router"
|
import { A, useParams } from "@solidjs/router"
|
||||||
import { type Accessor, createMemo, For, type JSX, Match, Show, Switch } from "solid-js"
|
import { type Accessor, createMemo, For, type JSX, Match, Show, Switch } from "solid-js"
|
||||||
import { useServerSync } from "@/context/server-sync"
|
import { useGlobalSync } from "@/context/global-sync"
|
||||||
import { useLanguage } from "@/context/language"
|
import { useLanguage } from "@/context/language"
|
||||||
import { getAvatarColors, type LocalProject, useLayout } from "@/context/layout"
|
import { getAvatarColors, type LocalProject, useLayout } from "@/context/layout"
|
||||||
import { useNotification } from "@/context/notification"
|
import { useNotification } from "@/context/notification"
|
||||||
@@ -23,7 +23,7 @@ export const ProjectIcon = (props: {
|
|||||||
notify?: boolean
|
notify?: boolean
|
||||||
working?: boolean
|
working?: boolean
|
||||||
}): JSX.Element => {
|
}): JSX.Element => {
|
||||||
const serverSync = useServerSync()
|
const globalSync = useGlobalSync()
|
||||||
const notification = useNotification()
|
const notification = useNotification()
|
||||||
const permission = usePermission()
|
const permission = usePermission()
|
||||||
const dirs = createMemo(() => [props.project.worktree, ...(props.project.sandboxes ?? [])])
|
const dirs = createMemo(() => [props.project.worktree, ...(props.project.sandboxes ?? [])])
|
||||||
@@ -33,7 +33,7 @@ export const ProjectIcon = (props: {
|
|||||||
const hasError = createMemo(() => dirs().some((directory) => notification.project.unseenHasError(directory)))
|
const hasError = createMemo(() => dirs().some((directory) => notification.project.unseenHasError(directory)))
|
||||||
const hasPermissions = createMemo(() =>
|
const hasPermissions = createMemo(() =>
|
||||||
dirs().some((directory) => {
|
dirs().some((directory) => {
|
||||||
const [store] = serverSync.child(directory, { bootstrap: false })
|
const [store] = globalSync.child(directory, { bootstrap: false })
|
||||||
return hasProjectPermissions(store.permission, (item) => !permission.autoResponds(item, directory))
|
return hasProjectPermissions(store.permission, (item) => !permission.autoResponds(item, directory))
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
@@ -146,10 +146,10 @@ export const SessionItem = (props: SessionItemProps): JSX.Element => {
|
|||||||
const language = useLanguage()
|
const language = useLanguage()
|
||||||
const notification = useNotification()
|
const notification = useNotification()
|
||||||
const permission = usePermission()
|
const permission = usePermission()
|
||||||
const serverSync = useServerSync()
|
const globalSync = useGlobalSync()
|
||||||
const unseenCount = createMemo(() => notification.session.unseenCount(props.session.id))
|
const unseenCount = createMemo(() => notification.session.unseenCount(props.session.id))
|
||||||
const hasError = createMemo(() => notification.session.unseenHasError(props.session.id))
|
const hasError = createMemo(() => notification.session.unseenHasError(props.session.id))
|
||||||
const [sessionStore] = serverSync.child(props.session.directory)
|
const [sessionStore] = globalSync.child(props.session.directory)
|
||||||
const hasPermissions = createMemo(() => {
|
const hasPermissions = createMemo(() => {
|
||||||
return !!sessionPermissionRequest(sessionStore.session, sessionStore.permission, props.session.id, (item) => {
|
return !!sessionPermissionRequest(sessionStore.session, sessionStore.permission, props.session.id, (item) => {
|
||||||
return !permission.autoResponds(item, props.session.directory)
|
return !permission.autoResponds(item, props.session.directory)
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import { HoverCard } from "@opencode-ai/ui/hover-card"
|
|||||||
import { Icon } from "@opencode-ai/ui/icon"
|
import { Icon } from "@opencode-ai/ui/icon"
|
||||||
import { createSortable } from "@thisbeyond/solid-dnd"
|
import { createSortable } from "@thisbeyond/solid-dnd"
|
||||||
import { useLayout, type LocalProject } from "@/context/layout"
|
import { useLayout, type LocalProject } from "@/context/layout"
|
||||||
import { useServerSync } from "@/context/server-sync"
|
import { useGlobalSync } from "@/context/global-sync"
|
||||||
import { useLanguage } from "@/context/language"
|
import { useLanguage } from "@/context/language"
|
||||||
import { useNotification } from "@/context/notification"
|
import { useNotification } from "@/context/notification"
|
||||||
import { ProjectIcon, SessionItem, type SessionItemProps } from "./sidebar-items"
|
import { ProjectIcon, SessionItem, type SessionItemProps } from "./sidebar-items"
|
||||||
@@ -274,7 +274,7 @@ export const SortableProject = (props: {
|
|||||||
ctx: ProjectSidebarContext
|
ctx: ProjectSidebarContext
|
||||||
sortNow: Accessor<number>
|
sortNow: Accessor<number>
|
||||||
}): JSX.Element => {
|
}): JSX.Element => {
|
||||||
const serverSync = useServerSync()
|
const globalSync = useGlobalSync()
|
||||||
const language = useLanguage()
|
const language = useLanguage()
|
||||||
const sortable = createSortable(props.project.worktree)
|
const sortable = createSortable(props.project.worktree)
|
||||||
const selected = createMemo(() => props.ctx.currentProject()?.worktree === props.project.worktree)
|
const selected = createMemo(() => props.ctx.currentProject()?.worktree === props.project.worktree)
|
||||||
@@ -294,23 +294,23 @@ export const SortableProject = (props: {
|
|||||||
const hoverOpen = () => isHoverProject() && preview() && !selected() && !state.menu
|
const hoverOpen = () => isHoverProject() && preview() && !selected() && !state.menu
|
||||||
|
|
||||||
const label = (directory: string) => {
|
const label = (directory: string) => {
|
||||||
const [data] = serverSync.child(directory, { bootstrap: false })
|
const [data] = globalSync.child(directory, { bootstrap: false })
|
||||||
const kind =
|
const kind =
|
||||||
directory === props.project.worktree ? language.t("workspace.type.local") : language.t("workspace.type.sandbox")
|
directory === props.project.worktree ? language.t("workspace.type.local") : language.t("workspace.type.sandbox")
|
||||||
const name = props.ctx.workspaceLabel(directory, data.vcs?.branch, props.project.id)
|
const name = props.ctx.workspaceLabel(directory, data.vcs?.branch, props.project.id)
|
||||||
return `${kind} : ${name}`
|
return `${kind} : ${name}`
|
||||||
}
|
}
|
||||||
|
|
||||||
const projectStore = createMemo(() => serverSync.child(props.project.worktree, { bootstrap: false })[0])
|
const projectStore = createMemo(() => globalSync.child(props.project.worktree, { bootstrap: false })[0])
|
||||||
const isWorking = createMemo(() =>
|
const isWorking = createMemo(() =>
|
||||||
dirs().some((directory) => {
|
dirs().some((directory) => {
|
||||||
const [store] = serverSync.child(directory, { bootstrap: false })
|
const [store] = globalSync.child(directory, { bootstrap: false })
|
||||||
return Object.keys(store.session_status).some((id) => store.session_working(id))
|
return Object.keys(store.session_status).some((id) => store.session_working(id))
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
const projectSessions = createMemo(() => sortedRootSessions(projectStore(), props.sortNow()))
|
const projectSessions = createMemo(() => sortedRootSessions(projectStore(), props.sortNow()))
|
||||||
const workspaceSessions = (directory: string) => {
|
const workspaceSessions = (directory: string) => {
|
||||||
const [data] = serverSync.child(directory, { bootstrap: false })
|
const [data] = globalSync.child(directory, { bootstrap: false })
|
||||||
return sortedRootSessions(data, props.sortNow())
|
return sortedRootSessions(data, props.sortNow())
|
||||||
}
|
}
|
||||||
const tile = () => (
|
const tile = () => (
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ import { Spinner } from "@opencode-ai/ui/spinner"
|
|||||||
import { Tooltip } from "@opencode-ai/ui/tooltip"
|
import { Tooltip } from "@opencode-ai/ui/tooltip"
|
||||||
import { type Session } from "@opencode-ai/sdk/v2/client"
|
import { type Session } from "@opencode-ai/sdk/v2/client"
|
||||||
import { type LocalProject } from "@/context/layout"
|
import { type LocalProject } from "@/context/layout"
|
||||||
import { useServerSync, useQueryOptions } from "@/context/server-sync"
|
import { useGlobalSync, useQueryOptions } from "@/context/global-sync"
|
||||||
import { useLanguage } from "@/context/language"
|
import { useLanguage } from "@/context/language"
|
||||||
import { pathKey } from "@/utils/path-key"
|
import { pathKey } from "@/utils/path-key"
|
||||||
import { NewSessionItem, SessionItem, SessionSkeleton } from "./sidebar-items"
|
import { NewSessionItem, SessionItem, SessionSkeleton } from "./sidebar-items"
|
||||||
@@ -60,7 +60,7 @@ export const WorkspaceDragOverlay = (props: {
|
|||||||
activeWorkspace: Accessor<string | undefined>
|
activeWorkspace: Accessor<string | undefined>
|
||||||
workspaceLabel: (directory: string, branch?: string, projectId?: string) => string
|
workspaceLabel: (directory: string, branch?: string, projectId?: string) => string
|
||||||
}): JSX.Element => {
|
}): JSX.Element => {
|
||||||
const serverSync = useServerSync()
|
const globalSync = useGlobalSync()
|
||||||
const language = useLanguage()
|
const language = useLanguage()
|
||||||
const label = createMemo(() => {
|
const label = createMemo(() => {
|
||||||
const project = props.sidebarProject()
|
const project = props.sidebarProject()
|
||||||
@@ -68,7 +68,7 @@ export const WorkspaceDragOverlay = (props: {
|
|||||||
const directory = props.activeWorkspace()
|
const directory = props.activeWorkspace()
|
||||||
if (!directory) return
|
if (!directory) return
|
||||||
|
|
||||||
const [workspaceStore] = serverSync.child(directory, { bootstrap: false })
|
const [workspaceStore] = globalSync.child(directory, { bootstrap: false })
|
||||||
const kind =
|
const kind =
|
||||||
directory === project.worktree ? language.t("workspace.type.local") : language.t("workspace.type.sandbox")
|
directory === project.worktree ? language.t("workspace.type.local") : language.t("workspace.type.sandbox")
|
||||||
const name = props.workspaceLabel(directory, workspaceStore.vcs?.branch, project.id)
|
const name = props.workspaceLabel(directory, workspaceStore.vcs?.branch, project.id)
|
||||||
@@ -299,11 +299,11 @@ export const SortableWorkspace = (props: {
|
|||||||
}): JSX.Element => {
|
}): JSX.Element => {
|
||||||
const navigate = useNavigate()
|
const navigate = useNavigate()
|
||||||
const params = useParams()
|
const params = useParams()
|
||||||
const serverSync = useServerSync()
|
const globalSync = useGlobalSync()
|
||||||
const queryOptions = useQueryOptions()
|
const queryOptions = useQueryOptions()
|
||||||
const language = useLanguage()
|
const language = useLanguage()
|
||||||
const sortable = createSortable(props.directory)
|
const sortable = createSortable(props.directory)
|
||||||
const [workspaceStore, setWorkspaceStore] = serverSync.child(props.directory, { bootstrap: false })
|
const [workspaceStore, setWorkspaceStore] = globalSync.child(props.directory, { bootstrap: false })
|
||||||
const [menu, setMenu] = createStore({
|
const [menu, setMenu] = createStore({
|
||||||
open: false,
|
open: false,
|
||||||
pendingRename: false,
|
pendingRename: false,
|
||||||
@@ -328,7 +328,7 @@ export const SortableWorkspace = (props: {
|
|||||||
const showNew = createMemo(() => !loading() && (touch() || count() === 0 || (active() && !params.id)))
|
const showNew = createMemo(() => !loading() && (touch() || count() === 0 || (active() && !params.id)))
|
||||||
const loadMore = async () => {
|
const loadMore = async () => {
|
||||||
setWorkspaceStore("limit", (limit) => (limit ?? 0) + 5)
|
setWorkspaceStore("limit", (limit) => (limit ?? 0) + 5)
|
||||||
await serverSync.project.loadSessions(props.directory)
|
await globalSync.project.loadSessions(props.directory)
|
||||||
}
|
}
|
||||||
|
|
||||||
const workspaceEditActive = createMemo(() => props.ctx.editorOpen(`workspace:${props.directory}`))
|
const workspaceEditActive = createMemo(() => props.ctx.editorOpen(`workspace:${props.directory}`))
|
||||||
@@ -357,7 +357,7 @@ export const SortableWorkspace = (props: {
|
|||||||
|
|
||||||
createEffect(() => {
|
createEffect(() => {
|
||||||
if (!boot()) return
|
if (!boot()) return
|
||||||
serverSync.child(props.directory, { bootstrap: true })
|
globalSync.child(props.directory, { bootstrap: true })
|
||||||
})
|
})
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -446,11 +446,11 @@ export const LocalWorkspace = (props: {
|
|||||||
sortNow: Accessor<number>
|
sortNow: Accessor<number>
|
||||||
mobile?: boolean
|
mobile?: boolean
|
||||||
}): JSX.Element => {
|
}): JSX.Element => {
|
||||||
const serverSync = useServerSync()
|
const globalSync = useGlobalSync()
|
||||||
const queryOptions = useQueryOptions()
|
const queryOptions = useQueryOptions()
|
||||||
const language = useLanguage()
|
const language = useLanguage()
|
||||||
const workspace = createMemo(() => {
|
const workspace = createMemo(() => {
|
||||||
const [store, setStore] = serverSync.child(props.project.worktree)
|
const [store, setStore] = globalSync.child(props.project.worktree)
|
||||||
return { store, setStore }
|
return { store, setStore }
|
||||||
})
|
})
|
||||||
const slug = createMemo(() => base64Encode(props.project.worktree))
|
const slug = createMemo(() => base64Encode(props.project.worktree))
|
||||||
@@ -461,7 +461,7 @@ export const LocalWorkspace = (props: {
|
|||||||
const loading = () => fetching() > 0 && count() === 0
|
const loading = () => fetching() > 0 && count() === 0
|
||||||
const loadMore = async () => {
|
const loadMore = async () => {
|
||||||
workspace().setStore("limit", (limit) => (limit ?? 0) + 5)
|
workspace().setStore("limit", (limit) => (limit ?? 0) + 5)
|
||||||
await serverSync.project.loadSessions(props.project.worktree)
|
await globalSync.project.loadSessions(props.project.worktree)
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -34,7 +34,7 @@ import { useLocation, useSearchParams } from "@solidjs/router"
|
|||||||
import { NewSessionDesignView, NewSessionView, SessionHeader } from "@/components/session"
|
import { NewSessionDesignView, NewSessionView, SessionHeader } from "@/components/session"
|
||||||
import { useComments } from "@/context/comments"
|
import { useComments } from "@/context/comments"
|
||||||
import { getSessionPrefetch, SESSION_PREFETCH_TTL } from "@/context/global-sync/session-prefetch"
|
import { getSessionPrefetch, SESSION_PREFETCH_TTL } from "@/context/global-sync/session-prefetch"
|
||||||
import { useServerSync } from "@/context/server-sync"
|
import { useGlobalSync } from "@/context/global-sync"
|
||||||
import { useLanguage } from "@/context/language"
|
import { useLanguage } from "@/context/language"
|
||||||
import { useLayout } from "@/context/layout"
|
import { useLayout } from "@/context/layout"
|
||||||
import { usePrompt } from "@/context/prompt"
|
import { usePrompt } from "@/context/prompt"
|
||||||
@@ -59,7 +59,6 @@ import { SessionSidePanel } from "@/pages/session/session-side-panel"
|
|||||||
import { TerminalPanel } from "@/pages/session/terminal-panel"
|
import { TerminalPanel } from "@/pages/session/terminal-panel"
|
||||||
import { useSessionCommands } from "@/pages/session/use-session-commands"
|
import { useSessionCommands } from "@/pages/session/use-session-commands"
|
||||||
import { useSessionHashScroll } from "@/pages/session/use-session-hash-scroll"
|
import { useSessionHashScroll } from "@/pages/session/use-session-hash-scroll"
|
||||||
import { shouldUseV2NewSessionPage } from "@/pages/session/new-session-layout"
|
|
||||||
import { Identifier } from "@/utils/id"
|
import { Identifier } from "@/utils/id"
|
||||||
import { diffs as list } from "@/utils/diffs"
|
import { diffs as list } from "@/utils/diffs"
|
||||||
import { Persist, persisted } from "@/utils/persist"
|
import { Persist, persisted } from "@/utils/persist"
|
||||||
@@ -75,6 +74,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
|
||||||
@@ -181,7 +181,7 @@ function createSessionHistoryLoader(input: SessionHistoryWindowInput) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export default function Page() {
|
export default function Page() {
|
||||||
const serverSync = useServerSync()
|
const globalSync = useGlobalSync()
|
||||||
const layout = useLayout()
|
const layout = useLayout()
|
||||||
const local = useLocal()
|
const local = useLocal()
|
||||||
const file = useFile()
|
const file = useFile()
|
||||||
@@ -197,7 +197,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
|
||||||
@@ -264,8 +263,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 = () => import.meta.env.VITE_OPENCODE_CHANNEL === "prod" || !params.id
|
||||||
shouldUseV2NewSessionPage({ newLayoutDesigns: newSessionDesign(), 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())
|
||||||
@@ -560,11 +558,11 @@ export default function Page() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function upsert(next: Project) {
|
function upsert(next: Project) {
|
||||||
const list = serverSync.data.project
|
const list = globalSync.data.project
|
||||||
sync.set("project", next.id)
|
sync.set("project", next.id)
|
||||||
const idx = list.findIndex((item) => item.id === next.id)
|
const idx = list.findIndex((item) => item.id === next.id)
|
||||||
if (idx >= 0) {
|
if (idx >= 0) {
|
||||||
serverSync.set(
|
globalSync.set(
|
||||||
"project",
|
"project",
|
||||||
list.map((item, i) => (i === idx ? { ...item, ...next } : item)),
|
list.map((item, i) => (i === idx ? { ...item, ...next } : item)),
|
||||||
)
|
)
|
||||||
@@ -572,10 +570,10 @@ export default function Page() {
|
|||||||
}
|
}
|
||||||
const at = list.findIndex((item) => item.id > next.id)
|
const at = list.findIndex((item) => item.id > next.id)
|
||||||
if (at >= 0) {
|
if (at >= 0) {
|
||||||
serverSync.set("project", [...list.slice(0, at), next, ...list.slice(at)])
|
globalSync.set("project", [...list.slice(0, at), next, ...list.slice(at)])
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
serverSync.set("project", [...list, next])
|
globalSync.set("project", [...list, next])
|
||||||
}
|
}
|
||||||
|
|
||||||
const gitMutation = useMutation(() => ({
|
const gitMutation = useMutation(() => ({
|
||||||
@@ -673,7 +671,7 @@ export default function Page() {
|
|||||||
todoTimer = undefined
|
todoTimer = undefined
|
||||||
if (!id) return
|
if (!id) return
|
||||||
if (status === "idle" && !blocked) return
|
if (status === "idle" && !blocked) return
|
||||||
const cached = untrack(() => sync.data.todo[id] !== undefined || serverSync.data.session_todo[id] !== undefined)
|
const cached = untrack(() => sync.data.todo[id] !== undefined || globalSync.data.session_todo[id] !== undefined)
|
||||||
|
|
||||||
todoFrame = requestAnimationFrame(() => {
|
todoFrame = requestAnimationFrame(() => {
|
||||||
todoFrame = undefined
|
todoFrame = undefined
|
||||||
@@ -1386,7 +1384,7 @@ export default function Page() {
|
|||||||
const ok = await sendFollowupDraft({
|
const ok = await sendFollowupDraft({
|
||||||
client: sdk.client,
|
client: sdk.client,
|
||||||
sync,
|
sync,
|
||||||
serverSync,
|
globalSync,
|
||||||
draft: item,
|
draft: item,
|
||||||
optimisticBusy: item.sessionDirectory === sdk.directory,
|
optimisticBusy: item.sessionDirectory === sdk.directory,
|
||||||
}).catch((err) => {
|
}).catch((err) => {
|
||||||
@@ -1663,6 +1661,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 +1797,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}
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import { createStore } from "solid-js/store"
|
|||||||
import type { PermissionRequest, QuestionRequest, Todo } from "@opencode-ai/sdk/v2"
|
import type { PermissionRequest, QuestionRequest, Todo } from "@opencode-ai/sdk/v2"
|
||||||
import { useParams } from "@solidjs/router"
|
import { useParams } from "@solidjs/router"
|
||||||
import { showToast } from "@opencode-ai/ui/toast"
|
import { showToast } from "@opencode-ai/ui/toast"
|
||||||
import { useServerSync } from "@/context/server-sync"
|
import { useGlobalSync } from "@/context/global-sync"
|
||||||
import { useLanguage } from "@/context/language"
|
import { useLanguage } from "@/context/language"
|
||||||
import { usePermission } from "@/context/permission"
|
import { usePermission } from "@/context/permission"
|
||||||
import { useSDK } from "@/context/sdk"
|
import { useSDK } from "@/context/sdk"
|
||||||
@@ -27,7 +27,7 @@ export function createSessionComposerState(options?: { closeMs?: number | (() =>
|
|||||||
const params = useParams()
|
const params = useParams()
|
||||||
const sdk = useSDK()
|
const sdk = useSDK()
|
||||||
const sync = useSync()
|
const sync = useSync()
|
||||||
const serverSync = useServerSync()
|
const globalSync = useGlobalSync()
|
||||||
const language = useLanguage()
|
const language = useLanguage()
|
||||||
const permission = usePermission()
|
const permission = usePermission()
|
||||||
|
|
||||||
@@ -50,7 +50,7 @@ export function createSessionComposerState(options?: { closeMs?: number | (() =>
|
|||||||
const todos = createMemo((): Todo[] => {
|
const todos = createMemo((): Todo[] => {
|
||||||
const id = params.id
|
const id = params.id
|
||||||
if (!id) return []
|
if (!id) return []
|
||||||
return serverSync.data.session_todo[id] ?? []
|
return globalSync.data.session_todo[id] ?? []
|
||||||
})
|
})
|
||||||
|
|
||||||
const done = createMemo(
|
const done = createMemo(
|
||||||
@@ -111,7 +111,7 @@ export function createSessionComposerState(options?: { closeMs?: number | (() =>
|
|||||||
const clear = () => {
|
const clear = () => {
|
||||||
const id = params.id
|
const id = params.id
|
||||||
if (!id) return
|
if (!id) return
|
||||||
serverSync.todo.set(id, [])
|
globalSync.todo.set(id, [])
|
||||||
sync.set("todo", id, [])
|
sync.set("todo", id, [])
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ export type TimelineRowMap = {
|
|||||||
userMessageID: string
|
userMessageID: string
|
||||||
group: PartGroup
|
group: PartGroup
|
||||||
previousAssistantPart: boolean
|
previousAssistantPart: boolean
|
||||||
|
lastAssistantPart: boolean
|
||||||
}
|
}
|
||||||
Thinking: { userMessageID: string; reasoningHeading?: string }
|
Thinking: { userMessageID: string; reasoningHeading?: string }
|
||||||
Retry: { userMessageID: string }
|
Retry: { userMessageID: string }
|
||||||
@@ -49,6 +50,7 @@ export namespace TimelineRow {
|
|||||||
userMessageID: string
|
userMessageID: string
|
||||||
group: PartGroup
|
group: PartGroup
|
||||||
previousAssistantPart: boolean
|
previousAssistantPart: boolean
|
||||||
|
lastAssistantPart: boolean
|
||||||
}> {}
|
}> {}
|
||||||
export class Thinking extends Data.TaggedClass("Thinking")<{
|
export class Thinking extends Data.TaggedClass("Thinking")<{
|
||||||
userMessageID: string
|
userMessageID: string
|
||||||
@@ -149,6 +151,8 @@ export namespace Timeline {
|
|||||||
),
|
),
|
||||||
]
|
]
|
||||||
: groupParts(assistantPartRefs).map((group) => ({ type: "part" as const, group }))
|
: groupParts(assistantPartRefs).map((group) => ({ type: "part" as const, group }))
|
||||||
|
const assistantGroupCount = assistantItems.filter((item) => item.type === "part").length
|
||||||
|
|
||||||
if (comments.length > 0)
|
if (comments.length > 0)
|
||||||
rows.push(
|
rows.push(
|
||||||
new TimelineRow.CommentStrip({
|
new TimelineRow.CommentStrip({
|
||||||
@@ -191,6 +195,7 @@ export namespace Timeline {
|
|||||||
userMessageID: userMessage.id,
|
userMessageID: userMessage.id,
|
||||||
group: item.group,
|
group: item.group,
|
||||||
previousAssistantPart: assistantGroupIndex > 0,
|
previousAssistantPart: assistantGroupIndex > 0,
|
||||||
|
lastAssistantPart: assistantGroupIndex === assistantGroupCount - 1,
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
assistantGroupIndex += 1
|
assistantGroupIndex += 1
|
||||||
|
|||||||
@@ -1,16 +1,4 @@
|
|||||||
import {
|
import { createEffect, createMemo, createSignal, For, Index, on, onCleanup, Show, mapArray, type JSX } from "solid-js"
|
||||||
createEffect,
|
|
||||||
createMemo,
|
|
||||||
createSignal,
|
|
||||||
For,
|
|
||||||
Index,
|
|
||||||
on,
|
|
||||||
onCleanup,
|
|
||||||
Show,
|
|
||||||
mapArray,
|
|
||||||
type Accessor,
|
|
||||||
type JSX,
|
|
||||||
} from "solid-js"
|
|
||||||
import { createStore, produce } from "solid-js/store"
|
import { createStore, produce } from "solid-js/store"
|
||||||
import { Dynamic } from "solid-js/web"
|
import { Dynamic } from "solid-js/web"
|
||||||
import { useNavigate } from "@solidjs/router"
|
import { useNavigate } from "@solidjs/router"
|
||||||
@@ -60,12 +48,11 @@ import { useDialog } from "@opencode-ai/ui/context/dialog"
|
|||||||
import { createResizeObserver } from "@solid-primitives/resize-observer"
|
import { createResizeObserver } from "@solid-primitives/resize-observer"
|
||||||
import { useLanguage } from "@/context/language"
|
import { useLanguage } from "@/context/language"
|
||||||
import { useSessionKey } from "@/pages/session/session-layout"
|
import { useSessionKey } from "@/pages/session/session-layout"
|
||||||
import { useServerSDK } from "@/context/server-sdk"
|
import { useGlobalSDK } from "@/context/global-sdk"
|
||||||
import { usePlatform } from "@/context/platform"
|
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"
|
||||||
@@ -258,7 +245,7 @@ function TimelineDiffView(props: { diff: SummaryDiff }) {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div data-slot="session-turn-diff-view" data-scrollable>
|
<div data-slot="session-turn-diff-view" data-scrollable>
|
||||||
<Dynamic component={fileComponent} mode="diff" virtualize={false} fileDiff={view.fileDiff} />
|
<Dynamic component={fileComponent} mode="diff" fileDiff={view.fileDiff} />
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -286,7 +273,7 @@ export function MessageTimeline(props: {
|
|||||||
let touchGesture: number | undefined
|
let touchGesture: number | undefined
|
||||||
|
|
||||||
const navigate = useNavigate()
|
const navigate = useNavigate()
|
||||||
const serverSDK = useServerSDK()
|
const globalSDK = useGlobalSDK()
|
||||||
const sdk = useSDK()
|
const sdk = useSDK()
|
||||||
const sync = useSync()
|
const sync = useSync()
|
||||||
const settings = useSettings()
|
const settings = useSettings()
|
||||||
@@ -428,7 +415,8 @@ export function MessageTimeline(props: {
|
|||||||
if (rows.length === 0) return rows
|
if (rows.length === 0) return rows
|
||||||
return reuseTimelineRows(previous, [...rows, new TimelineRow.BottomSpacer()])
|
return reuseTimelineRows(previous, [...rows, new TimelineRow.BottomSpacer()])
|
||||||
})
|
})
|
||||||
const timelineRowKeys = createMemo(() => timelineRows().map(TimelineRow.key), [] as string[], { equals: sameKeys })
|
const timelineRowByKey = createMemo(() => new Map(timelineRows().map((row) => [TimelineRow.key(row), row] as const)))
|
||||||
|
const timelineRowKeys = createMemo(() => [...timelineRowByKey().keys()], [] as string[], { equals: sameKeys })
|
||||||
const virtualCache = createMemo(() => readTimelineCache(sessionKey(), timelineRowKeys()))
|
const virtualCache = createMemo(() => readTimelineCache(sessionKey(), timelineRowKeys()))
|
||||||
const messageRowIndex = createMemo(() => {
|
const messageRowIndex = createMemo(() => {
|
||||||
const result = new Map<string, number>()
|
const result = new Map<string, number>()
|
||||||
@@ -439,14 +427,6 @@ export function MessageTimeline(props: {
|
|||||||
})
|
})
|
||||||
return result
|
return result
|
||||||
})
|
})
|
||||||
const lastAssistantGroupKey = createMemo(() => {
|
|
||||||
const result = new Map<string, string>()
|
|
||||||
timelineRows().forEach((row) => {
|
|
||||||
if (row._tag !== "AssistantPart") return
|
|
||||||
result.set(row.userMessageID, row.group.key)
|
|
||||||
})
|
|
||||||
return result
|
|
||||||
})
|
|
||||||
const keepMounted = createMemo(() => {
|
const keepMounted = createMemo(() => {
|
||||||
const id = activeMessageID()
|
const id = activeMessageID()
|
||||||
if (!id) return
|
if (!id) return
|
||||||
@@ -560,7 +540,6 @@ export function MessageTimeline(props: {
|
|||||||
const [bar, setBar] = createStore({
|
const [bar, setBar] = createStore({
|
||||||
ms: pace(640),
|
ms: pace(640),
|
||||||
})
|
})
|
||||||
const [toolOpen, setToolOpen] = createStore<Record<string, boolean | undefined>>({})
|
|
||||||
|
|
||||||
let more: HTMLButtonElement | undefined
|
let more: HTMLButtonElement | undefined
|
||||||
let head: HTMLDivElement | undefined
|
let head: HTMLDivElement | undefined
|
||||||
@@ -581,11 +560,6 @@ export function MessageTimeline(props: {
|
|||||||
|
|
||||||
const isMeasuredBottom = (root: HTMLDivElement) => root.scrollHeight - root.clientHeight - root.scrollTop <= 4
|
const isMeasuredBottom = (root: HTMLDivElement) => root.scrollHeight - root.clientHeight - root.scrollTop <= 4
|
||||||
|
|
||||||
const measureTimeline = () => {
|
|
||||||
virtualizer?.measure()
|
|
||||||
anchorMeasuredBottom()
|
|
||||||
}
|
|
||||||
|
|
||||||
function anchorMeasuredBottom() {
|
function anchorMeasuredBottom() {
|
||||||
if (!listRoot) return false
|
if (!listRoot) return false
|
||||||
if (!measuredBottomAnchored) return false
|
if (!measuredBottomAnchored) return false
|
||||||
@@ -730,14 +704,14 @@ export function MessageTimeline(props: {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const shareMutation = useMutation(() => ({
|
const shareMutation = useMutation(() => ({
|
||||||
mutationFn: (id: string) => serverSDK.client.session.share({ sessionID: id, directory: sdk.directory }),
|
mutationFn: (id: string) => globalSDK.client.session.share({ sessionID: id, directory: sdk.directory }),
|
||||||
onError: (err) => {
|
onError: (err) => {
|
||||||
console.error("Failed to share session", err)
|
console.error("Failed to share session", err)
|
||||||
},
|
},
|
||||||
}))
|
}))
|
||||||
|
|
||||||
const unshareMutation = useMutation(() => ({
|
const unshareMutation = useMutation(() => ({
|
||||||
mutationFn: (id: string) => serverSDK.client.session.unshare({ sessionID: id, directory: sdk.directory }),
|
mutationFn: (id: string) => globalSDK.client.session.unshare({ sessionID: id, directory: sdk.directory }),
|
||||||
onError: (err) => {
|
onError: (err) => {
|
||||||
console.error("Failed to unshare session", err)
|
console.error("Failed to unshare session", err)
|
||||||
},
|
},
|
||||||
@@ -862,9 +836,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,46 +867,42 @@ export function MessageTimeline(props: {
|
|||||||
|
|
||||||
if (!result) return false
|
if (!result) return false
|
||||||
|
|
||||||
const removed = new Set<string>([sessionID])
|
|
||||||
const byParent = new Map<string, string[]>()
|
|
||||||
for (const item of sync.data.session) {
|
|
||||||
const parentID = item.parentID
|
|
||||||
if (!parentID) continue
|
|
||||||
const existing = byParent.get(parentID)
|
|
||||||
if (existing) {
|
|
||||||
existing.push(item.id)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
byParent.set(parentID, [item.id])
|
|
||||||
}
|
|
||||||
|
|
||||||
const stack = [sessionID]
|
|
||||||
while (stack.length) {
|
|
||||||
const parentID = stack.pop()
|
|
||||||
if (!parentID) continue
|
|
||||||
|
|
||||||
const children = byParent.get(parentID)
|
|
||||||
if (!children) continue
|
|
||||||
|
|
||||||
for (const child of children) {
|
|
||||||
if (removed.has(child)) continue
|
|
||||||
removed.add(child)
|
|
||||||
stack.push(child)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
navigateAfterSessionRemoval(sessionID, session.parentID, nextSession?.id)
|
|
||||||
|
|
||||||
sync.set(
|
sync.set(
|
||||||
produce((draft) => {
|
produce((draft) => {
|
||||||
|
const removed = new Set<string>([sessionID])
|
||||||
|
|
||||||
|
const byParent = new Map<string, string[]>()
|
||||||
|
for (const item of draft.session) {
|
||||||
|
const parentID = item.parentID
|
||||||
|
if (!parentID) continue
|
||||||
|
const existing = byParent.get(parentID)
|
||||||
|
if (existing) {
|
||||||
|
existing.push(item.id)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
byParent.set(parentID, [item.id])
|
||||||
|
}
|
||||||
|
|
||||||
|
const stack = [sessionID]
|
||||||
|
while (stack.length) {
|
||||||
|
const parentID = stack.pop()
|
||||||
|
if (!parentID) continue
|
||||||
|
|
||||||
|
const children = byParent.get(parentID)
|
||||||
|
if (!children) continue
|
||||||
|
|
||||||
|
for (const child of children) {
|
||||||
|
if (removed.has(child)) continue
|
||||||
|
removed.add(child)
|
||||||
|
stack.push(child)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
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
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1012,42 +980,29 @@ export function MessageTimeline(props: {
|
|||||||
|
|
||||||
const getMsgPart = (messageID: string, partID: string) => getMsgParts(messageID).find((part) => part.id === partID)
|
const getMsgPart = (messageID: string, partID: string) => getMsgParts(messageID).find((part) => part.id === partID)
|
||||||
|
|
||||||
const renderAssistantPartGroup = (row: Accessor<TimelineRowMap["AssistantPart"]>) => {
|
const renderAssistantPartGroup = (row: TimelineRowMap["AssistantPart"]) => {
|
||||||
if (row().group.type === "context") {
|
if (row.group.type === "context") {
|
||||||
const parts = createMemo(() => {
|
const parts = createMemo(() => {
|
||||||
const group = row().group
|
const group = row.group
|
||||||
if (group.type !== "context") return emptyTools
|
if (group.type !== "context") return emptyTools
|
||||||
return group.refs
|
return group.refs
|
||||||
.map((ref) => getMsgPart(ref.messageID, ref.partID))
|
.map((ref) => getMsgPart(ref.messageID, ref.partID))
|
||||||
.filter((part): part is ToolPart => part?.type === "tool")
|
.filter((part): part is ToolPart => part?.type === "tool")
|
||||||
})
|
})
|
||||||
|
|
||||||
return (
|
return <ContextToolGroup parts={parts()} busy={workingTurn(row.userMessageID) && row.lastAssistantPart} />
|
||||||
<ContextToolGroup
|
|
||||||
parts={parts()}
|
|
||||||
busy={
|
|
||||||
workingTurn(row().userMessageID) && lastAssistantGroupKey().get(row().userMessageID) === row().group.key
|
|
||||||
}
|
|
||||||
onSizeChange={measureTimeline}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const message = createMemo(() => {
|
const message = createMemo(() => {
|
||||||
const group = row().group
|
const group = row.group
|
||||||
if (group.type !== "part") return
|
if (group.type !== "part") return
|
||||||
return messageByID().get(group.ref.messageID)
|
return messageByID().get(group.ref.messageID)
|
||||||
})
|
})
|
||||||
const part = createMemo(() => {
|
const part = createMemo(() => {
|
||||||
const group = row().group
|
const group = row.group
|
||||||
if (group.type !== "part") return
|
if (group.type !== "part") return
|
||||||
return getMsgPart(group.ref.messageID, group.ref.partID)
|
return getMsgPart(group.ref.messageID, group.ref.partID)
|
||||||
})
|
})
|
||||||
const defaultOpen = createMemo(() => {
|
|
||||||
const item = part()
|
|
||||||
if (!item) return
|
|
||||||
return partDefaultOpen(item, settings.general.shellToolPartsExpanded(), settings.general.editToolPartsExpanded())
|
|
||||||
})
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Show when={message()}>
|
<Show when={message()}>
|
||||||
@@ -1057,13 +1012,14 @@ export function MessageTimeline(props: {
|
|||||||
<MessagePart
|
<MessagePart
|
||||||
part={part()}
|
part={part()}
|
||||||
message={message()}
|
message={message()}
|
||||||
showAssistantCopyPartID={assistantCopyPartID(row().userMessageID)}
|
showAssistantCopyPartID={assistantCopyPartID(row.userMessageID)}
|
||||||
turnDurationMs={turnDurationMs(row().userMessageID)}
|
turnDurationMs={turnDurationMs(row.userMessageID)}
|
||||||
defaultOpen={defaultOpen()}
|
defaultOpen={partDefaultOpen(
|
||||||
toolOpen={toolOpen[part().id] ?? defaultOpen()}
|
part(),
|
||||||
onToolOpenChange={(open) => setToolOpen(part().id, open)}
|
settings.general.shellToolPartsExpanded(),
|
||||||
|
settings.general.editToolPartsExpanded(),
|
||||||
|
)}
|
||||||
deferToolContent={false}
|
deferToolContent={false}
|
||||||
virtualizeDiff={false}
|
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</Show>
|
</Show>
|
||||||
@@ -1072,25 +1028,25 @@ export function MessageTimeline(props: {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
function TimelineRowFrame(input: { row: Accessor<FramedTimelineRow>; children: JSX.Element }) {
|
function TimelineRowFrame(input: { row: FramedTimelineRow; children: JSX.Element }) {
|
||||||
const anchor = () => {
|
const anchor = () => {
|
||||||
const row = input.row()
|
const row = input.row
|
||||||
return row._tag === "CommentStrip" || (row._tag === "UserMessage" && row.anchor)
|
return row._tag === "CommentStrip" || (row._tag === "UserMessage" && row.anchor)
|
||||||
}
|
}
|
||||||
const previousUserMessage = () => {
|
const previousUserMessage = () => {
|
||||||
const row = input.row()
|
const row = input.row
|
||||||
return (row._tag === "CommentStrip" || row._tag === "UserMessage") && row.previousUserMessage
|
return (row._tag === "CommentStrip" || row._tag === "UserMessage") && row.previousUserMessage
|
||||||
}
|
}
|
||||||
const previousAssistantPart = () => {
|
const previousAssistantPart = () => {
|
||||||
const row = input.row()
|
const row = input.row
|
||||||
return row._tag === "AssistantPart" && row.previousAssistantPart
|
return row._tag === "AssistantPart" && row.previousAssistantPart
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
id={anchor() ? props.anchor(input.row().userMessageID) : undefined}
|
id={anchor() ? props.anchor(input.row.userMessageID) : undefined}
|
||||||
data-message-id={input.row().userMessageID}
|
data-message-id={input.row.userMessageID}
|
||||||
data-timeline-row={input.row()._tag}
|
data-timeline-row={input.row._tag}
|
||||||
classList={{
|
classList={{
|
||||||
"min-w-0 w-full max-w-full": true,
|
"min-w-0 w-full max-w-full": true,
|
||||||
"md:max-w-200 2xl:max-w-[1000px]": props.centered,
|
"md:max-w-200 2xl:max-w-[1000px]": props.centered,
|
||||||
@@ -1106,15 +1062,14 @@ export function MessageTimeline(props: {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
const renderTimelineRow = (row: Accessor<TimelineRow.TimelineRow>) => {
|
const renderTimelineRow = (row: TimelineRow.TimelineRow) => {
|
||||||
switch (row()._tag) {
|
switch (row._tag) {
|
||||||
case "CommentStrip": {
|
case "CommentStrip": {
|
||||||
const commentStripRow = row as Accessor<TimelineRowByTag<"CommentStrip">>
|
|
||||||
const comments = createMemo(() =>
|
const comments = createMemo(() =>
|
||||||
getMsgParts(commentStripRow().userMessageID).flatMap((part) => MessageComment.fromPart(part) ?? []),
|
getMsgParts(row.userMessageID).flatMap((part) => MessageComment.fromPart(part) ?? []),
|
||||||
)
|
)
|
||||||
return (
|
return (
|
||||||
<TimelineRowFrame row={commentStripRow}>
|
<TimelineRowFrame row={row}>
|
||||||
<div class="w-full px-4 md:px-5 pb-2">
|
<div class="w-full px-4 md:px-5 pb-2">
|
||||||
<div class="ml-auto max-w-[82%] overflow-x-auto no-scrollbar">
|
<div class="ml-auto max-w-[82%] overflow-x-auto no-scrollbar">
|
||||||
<div class="flex w-max min-w-full justify-end gap-2">
|
<div class="flex w-max min-w-full justify-end gap-2">
|
||||||
@@ -1147,22 +1102,17 @@ export function MessageTimeline(props: {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
case "UserMessage": {
|
case "UserMessage": {
|
||||||
const userMessageRow = row as Accessor<TimelineRowByTag<"UserMessage">>
|
|
||||||
const message = createMemo(() => {
|
const message = createMemo(() => {
|
||||||
const m = messageByID().get(userMessageRow().userMessageID)
|
const m = messageByID().get(row.userMessageID)
|
||||||
if (m?.role === "user") return m
|
if (m?.role === "user") return m
|
||||||
})
|
})
|
||||||
return (
|
return (
|
||||||
<TimelineRowFrame row={userMessageRow}>
|
<TimelineRowFrame row={row}>
|
||||||
<Show when={message()}>
|
<Show when={message()}>
|
||||||
{(message) => (
|
{(message) => (
|
||||||
<div data-slot="session-turn-message-container" class="w-full px-4 md:px-5">
|
<div data-slot="session-turn-message-container" class="w-full px-4 md:px-5">
|
||||||
<div data-slot="session-turn-message-content" aria-live="off">
|
<div data-slot="session-turn-message-content" aria-live="off">
|
||||||
<Message
|
<Message message={message()} parts={getMsgParts(row.userMessageID)} actions={props.actions} />
|
||||||
message={message()}
|
|
||||||
parts={getMsgParts(userMessageRow().userMessageID)}
|
|
||||||
actions={props.actions}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@@ -1171,14 +1121,13 @@ export function MessageTimeline(props: {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
case "TurnDivider": {
|
case "TurnDivider": {
|
||||||
const turnDividerRow = row as Accessor<TimelineRowByTag<"TurnDivider">>
|
|
||||||
return (
|
return (
|
||||||
<TimelineRowFrame row={turnDividerRow}>
|
<TimelineRowFrame row={row}>
|
||||||
<div data-slot="session-turn-message-container" class="w-full px-4 md:px-5">
|
<div data-slot="session-turn-message-container" class="w-full px-4 md:px-5">
|
||||||
<div data-slot="session-turn-compaction">
|
<div data-slot="session-turn-compaction">
|
||||||
<MessageDivider
|
<MessageDivider
|
||||||
label={language.t(
|
label={language.t(
|
||||||
turnDividerRow().label === "compaction" ? "ui.messagePart.compaction" : "ui.message.interrupted",
|
row.label === "compaction" ? "ui.messagePart.compaction" : "ui.message.interrupted",
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@@ -1187,27 +1136,22 @@ export function MessageTimeline(props: {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
case "AssistantPart": {
|
case "AssistantPart": {
|
||||||
const assistantPartRow = row as Accessor<TimelineRowByTag<"AssistantPart">>
|
|
||||||
return (
|
return (
|
||||||
<TimelineRowFrame row={assistantPartRow}>
|
<TimelineRowFrame row={row}>
|
||||||
<div data-slot="session-turn-message-container" class="w-full px-4 md:px-5">
|
<div data-slot="session-turn-message-container" class="w-full px-4 md:px-5">
|
||||||
<div
|
<div data-slot="session-turn-assistant-content" aria-hidden={workingTurn(row.userMessageID)}>
|
||||||
data-slot="session-turn-assistant-content"
|
{renderAssistantPartGroup(row)}
|
||||||
aria-hidden={workingTurn(assistantPartRow().userMessageID)}
|
|
||||||
>
|
|
||||||
{renderAssistantPartGroup(assistantPartRow)}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</TimelineRowFrame>
|
</TimelineRowFrame>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
case "Thinking": {
|
case "Thinking": {
|
||||||
const thinkingRow = row as Accessor<TimelineRowByTag<"Thinking">>
|
|
||||||
return (
|
return (
|
||||||
<TimelineRowFrame row={thinkingRow}>
|
<TimelineRowFrame row={row}>
|
||||||
<div data-slot="session-turn-message-container" class="w-full px-4 md:px-5">
|
<div data-slot="session-turn-message-container" class="w-full px-4 md:px-5">
|
||||||
<TimelineThinkingRow
|
<TimelineThinkingRow
|
||||||
reasoningHeading={thinkingRow().reasoningHeading}
|
reasoningHeading={row.reasoningHeading}
|
||||||
showReasoningSummaries={settings.general.showReasoningSummaries()}
|
showReasoningSummaries={settings.general.showReasoningSummaries()}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@@ -1215,32 +1159,29 @@ export function MessageTimeline(props: {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
case "Retry": {
|
case "Retry": {
|
||||||
const retryRow = row as Accessor<TimelineRowByTag<"Retry">>
|
|
||||||
return (
|
return (
|
||||||
<TimelineRowFrame row={retryRow}>
|
<TimelineRowFrame row={row}>
|
||||||
<div data-slot="session-turn-message-container" class="w-full px-4 md:px-5">
|
<div data-slot="session-turn-message-container" class="w-full px-4 md:px-5">
|
||||||
<SessionRetry status={sessionStatus()} show={activeMessageID() === retryRow().userMessageID} />
|
<SessionRetry status={sessionStatus()} show={activeMessageID() === row.userMessageID} />
|
||||||
</div>
|
</div>
|
||||||
</TimelineRowFrame>
|
</TimelineRowFrame>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
case "DiffSummary": {
|
case "DiffSummary": {
|
||||||
const diffSummaryRow = row as Accessor<TimelineRowByTag<"DiffSummary">>
|
|
||||||
return (
|
return (
|
||||||
<TimelineRowFrame row={diffSummaryRow}>
|
<TimelineRowFrame row={row}>
|
||||||
<div data-slot="session-turn-message-container" class="w-full px-4 md:px-5">
|
<div data-slot="session-turn-message-container" class="w-full px-4 md:px-5">
|
||||||
<TimelineDiffSummaryRow diffs={diffSummaryRow().diffs} />
|
<TimelineDiffSummaryRow diffs={row.diffs} />
|
||||||
</div>
|
</div>
|
||||||
</TimelineRowFrame>
|
</TimelineRowFrame>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
case "Error": {
|
case "Error": {
|
||||||
const errorRow = row as Accessor<TimelineRowByTag<"Error">>
|
|
||||||
return (
|
return (
|
||||||
<TimelineRowFrame row={errorRow}>
|
<TimelineRowFrame row={row}>
|
||||||
<div data-slot="session-turn-message-container" class="w-full px-4 md:px-5">
|
<div data-slot="session-turn-message-container" class="w-full px-4 md:px-5">
|
||||||
<Card variant="error" class="error-card">
|
<Card variant="error" class="error-card">
|
||||||
{errorRow().text}
|
{row.text}
|
||||||
</Card>
|
</Card>
|
||||||
</div>
|
</div>
|
||||||
</TimelineRowFrame>
|
</TimelineRowFrame>
|
||||||
@@ -1251,8 +1192,12 @@ export function MessageTimeline(props: {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function TimelineRowView(props: { row: TimelineRow.TimelineRow }) {
|
function TimelineRowView(props: { rowKey: string }) {
|
||||||
return renderTimelineRow(() => props.row)
|
return (
|
||||||
|
<Show when={timelineRowByKey().get(props.rowKey)} keyed>
|
||||||
|
{(item) => renderTimelineRow(item)}
|
||||||
|
</Show>
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -1584,7 +1529,7 @@ export function MessageTimeline(props: {
|
|||||||
<Show when={scrollRoot()}>
|
<Show when={scrollRoot()}>
|
||||||
{(root) => (
|
{(root) => (
|
||||||
<Virtualizer
|
<Virtualizer
|
||||||
data={timelineRows()}
|
data={timelineRowKeys()}
|
||||||
cache={virtualCache()}
|
cache={virtualCache()}
|
||||||
itemSize={virtualCache() ? undefined : timelineFallbackItemSize}
|
itemSize={virtualCache() ? undefined : timelineFallbackItemSize}
|
||||||
scrollRef={root()}
|
scrollRef={root()}
|
||||||
@@ -1604,7 +1549,7 @@ export function MessageTimeline(props: {
|
|||||||
scheduleContentRoot(root())
|
scheduleContentRoot(root())
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{(row) => <TimelineRowView row={row} />}
|
{(key) => <TimelineRowView rowKey={key} />}
|
||||||
</Virtualizer>
|
</Virtualizer>
|
||||||
)}
|
)}
|
||||||
</Show>
|
</Show>
|
||||||
|
|||||||
@@ -1,14 +0,0 @@
|
|||||||
import { describe, expect, test } from "bun:test"
|
|
||||||
import { shouldUseV2NewSessionPage } from "./new-session-layout"
|
|
||||||
|
|
||||||
describe("shouldUseV2NewSessionPage", () => {
|
|
||||||
test("keeps disabled pages on the legacy layout", () => {
|
|
||||||
expect(shouldUseV2NewSessionPage({ newLayoutDesigns: false, sessionID: "ses_123" })).toBe(false)
|
|
||||||
expect(shouldUseV2NewSessionPage({ newLayoutDesigns: false })).toBe(false)
|
|
||||||
})
|
|
||||||
|
|
||||||
test("uses the v2 layout only for enabled new-session pages", () => {
|
|
||||||
expect(shouldUseV2NewSessionPage({ newLayoutDesigns: true })).toBe(true)
|
|
||||||
expect(shouldUseV2NewSessionPage({ newLayoutDesigns: true, sessionID: "ses_123" })).toBe(false)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
export function shouldUseV2NewSessionPage(input: { newLayoutDesigns: boolean; sessionID?: string }) {
|
|
||||||
return input.newLayoutDesigns && !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,26 +0,0 @@
|
|||||||
import { onCleanup } from "solid-js"
|
|
||||||
|
|
||||||
export function createRefCountMap<T>(create: (key: string) => T) {
|
|
||||||
const items = new Map<string, T>()
|
|
||||||
const refCounts = new Map<string, number>()
|
|
||||||
|
|
||||||
return (key: string) => {
|
|
||||||
onCleanup(() => {
|
|
||||||
refCounts.set(key, (refCounts.get(key) ?? 0) - 1)
|
|
||||||
if (refCounts.get(key) === 0) {
|
|
||||||
items.delete(key)
|
|
||||||
refCounts.delete(key)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
const cached = items.get(key)
|
|
||||||
if (cached) {
|
|
||||||
refCounts.set(key, (refCounts.get(key) ?? 0) + 1)
|
|
||||||
return cached
|
|
||||||
}
|
|
||||||
const item = create(key)
|
|
||||||
items.set(key, item)
|
|
||||||
refCounts.set(key, 1)
|
|
||||||
return item
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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.9",
|
||||||
"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",
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user