Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 8e726761e2 |
@@ -4,8 +4,8 @@ import { tool } from "@opencode-ai/plugin"
|
||||
const TEAM = {
|
||||
tui: ["kommander", "simonklee"],
|
||||
desktop_web: ["Hona", "Brendonovich"],
|
||||
core: ["jlongster", "rekram1-node", "nexxeln", "kitlangton", "starptech"],
|
||||
inference: ["fwang", "MrMushrooooom", "starptech"],
|
||||
core: ["jlongster", "rekram1-node", "nexxeln", "kitlangton"],
|
||||
inference: ["fwang", "MrMushrooooom"],
|
||||
windows: ["Hona"],
|
||||
} as const
|
||||
|
||||
|
||||
@@ -4,14 +4,6 @@
|
||||
- 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
|
||||
|
||||
Use conventional commit-style messages and PR titles: `type(scope): summary`.
|
||||
|
||||
Valid types are `feat`, `fix`, `docs`, `chore`, `refactor`, and `test`. Scopes are optional; use the affected package or area when helpful, e.g. `core`, `opencode`, `tui`, `app`, `desktop`, `sdk`, or `plugin`.
|
||||
|
||||
Examples: `fix(tui): simplify thinking toggle styling`, `docs: update contributing guide`, `chore(sdk): regenerate types`.
|
||||
|
||||
## Style Guide
|
||||
|
||||
### General Principles
|
||||
|
||||
@@ -29,7 +29,7 @@
|
||||
},
|
||||
"packages/app": {
|
||||
"name": "@opencode-ai/app",
|
||||
"version": "1.15.6",
|
||||
"version": "1.15.5",
|
||||
"dependencies": {
|
||||
"@kobalte/core": "catalog:",
|
||||
"@opencode-ai/core": "workspace:*",
|
||||
@@ -84,7 +84,7 @@
|
||||
},
|
||||
"packages/console/app": {
|
||||
"name": "@opencode-ai/console-app",
|
||||
"version": "1.15.6",
|
||||
"version": "1.15.5",
|
||||
"dependencies": {
|
||||
"@cloudflare/vite-plugin": "1.15.2",
|
||||
"@ibm/plex": "6.4.1",
|
||||
@@ -119,7 +119,7 @@
|
||||
},
|
||||
"packages/console/core": {
|
||||
"name": "@opencode-ai/console-core",
|
||||
"version": "1.15.6",
|
||||
"version": "1.15.5",
|
||||
"dependencies": {
|
||||
"@aws-sdk/client-sts": "3.782.0",
|
||||
"@jsx-email/render": "1.1.1",
|
||||
@@ -146,7 +146,7 @@
|
||||
},
|
||||
"packages/console/function": {
|
||||
"name": "@opencode-ai/console-function",
|
||||
"version": "1.15.6",
|
||||
"version": "1.15.5",
|
||||
"dependencies": {
|
||||
"@ai-sdk/anthropic": "3.0.64",
|
||||
"@ai-sdk/openai": "3.0.48",
|
||||
@@ -168,7 +168,7 @@
|
||||
},
|
||||
"packages/console/mail": {
|
||||
"name": "@opencode-ai/console-mail",
|
||||
"version": "1.15.6",
|
||||
"version": "1.15.5",
|
||||
"dependencies": {
|
||||
"@jsx-email/all": "2.2.3",
|
||||
"@jsx-email/cli": "1.4.3",
|
||||
@@ -192,7 +192,7 @@
|
||||
},
|
||||
"packages/core": {
|
||||
"name": "@opencode-ai/core",
|
||||
"version": "1.15.6",
|
||||
"version": "1.15.5",
|
||||
"bin": {
|
||||
"opencode": "./bin/opencode",
|
||||
},
|
||||
@@ -253,7 +253,7 @@
|
||||
},
|
||||
"packages/desktop": {
|
||||
"name": "@opencode-ai/desktop",
|
||||
"version": "1.15.6",
|
||||
"version": "1.15.5",
|
||||
"dependencies": {
|
||||
"drizzle-orm": "catalog:",
|
||||
"effect": "catalog:",
|
||||
@@ -305,23 +305,9 @@
|
||||
"@parcel/watcher-win32-x64": "2.5.1",
|
||||
},
|
||||
},
|
||||
"packages/effect-drizzle-sqlite": {
|
||||
"name": "@opencode-ai/effect-drizzle-sqlite",
|
||||
"version": "1.15.5",
|
||||
"dependencies": {
|
||||
"drizzle-orm": "catalog:",
|
||||
"effect": "catalog:",
|
||||
},
|
||||
"devDependencies": {
|
||||
"@effect/sql-sqlite-bun": "catalog:",
|
||||
"@tsconfig/bun": "catalog:",
|
||||
"@types/bun": "catalog:",
|
||||
"@typescript/native-preview": "catalog:",
|
||||
},
|
||||
},
|
||||
"packages/enterprise": {
|
||||
"name": "@opencode-ai/enterprise",
|
||||
"version": "1.15.6",
|
||||
"version": "1.15.5",
|
||||
"dependencies": {
|
||||
"@opencode-ai/core": "workspace:*",
|
||||
"@opencode-ai/ui": "workspace:*",
|
||||
@@ -351,7 +337,7 @@
|
||||
},
|
||||
"packages/function": {
|
||||
"name": "@opencode-ai/function",
|
||||
"version": "1.15.6",
|
||||
"version": "1.15.5",
|
||||
"dependencies": {
|
||||
"@octokit/auth-app": "8.0.1",
|
||||
"@octokit/rest": "catalog:",
|
||||
@@ -367,7 +353,7 @@
|
||||
},
|
||||
"packages/http-recorder": {
|
||||
"name": "@opencode-ai/http-recorder",
|
||||
"version": "1.15.6",
|
||||
"version": "1.15.5",
|
||||
"dependencies": {
|
||||
"@effect/platform-node": "catalog:",
|
||||
"effect": "catalog:",
|
||||
@@ -380,7 +366,7 @@
|
||||
},
|
||||
"packages/llm": {
|
||||
"name": "@opencode-ai/llm",
|
||||
"version": "1.15.6",
|
||||
"version": "1.15.5",
|
||||
"dependencies": {
|
||||
"@smithy/eventstream-codec": "4.2.14",
|
||||
"@smithy/util-utf8": "4.2.2",
|
||||
@@ -398,7 +384,7 @@
|
||||
},
|
||||
"packages/opencode": {
|
||||
"name": "opencode",
|
||||
"version": "1.15.6",
|
||||
"version": "1.15.5",
|
||||
"bin": {
|
||||
"opencode": "./bin/opencode",
|
||||
},
|
||||
@@ -536,7 +522,7 @@
|
||||
},
|
||||
"packages/plugin": {
|
||||
"name": "@opencode-ai/plugin",
|
||||
"version": "1.15.6",
|
||||
"version": "1.15.5",
|
||||
"dependencies": {
|
||||
"@opencode-ai/sdk": "workspace:*",
|
||||
"effect": "catalog:",
|
||||
@@ -552,9 +538,9 @@
|
||||
"typescript": "catalog:",
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@opentui/core": ">=0.2.15",
|
||||
"@opentui/keymap": ">=0.2.15",
|
||||
"@opentui/solid": ">=0.2.15",
|
||||
"@opentui/core": ">=0.2.14",
|
||||
"@opentui/keymap": ">=0.2.14",
|
||||
"@opentui/solid": ">=0.2.14",
|
||||
},
|
||||
"optionalPeers": [
|
||||
"@opentui/core",
|
||||
@@ -574,7 +560,7 @@
|
||||
},
|
||||
"packages/sdk/js": {
|
||||
"name": "@opencode-ai/sdk",
|
||||
"version": "1.15.6",
|
||||
"version": "1.15.5",
|
||||
"dependencies": {
|
||||
"cross-spawn": "catalog:",
|
||||
},
|
||||
@@ -589,7 +575,7 @@
|
||||
},
|
||||
"packages/slack": {
|
||||
"name": "@opencode-ai/slack",
|
||||
"version": "1.15.6",
|
||||
"version": "1.15.5",
|
||||
"dependencies": {
|
||||
"@opencode-ai/sdk": "workspace:*",
|
||||
"@slack/bolt": "^3.17.1",
|
||||
@@ -624,7 +610,7 @@
|
||||
},
|
||||
"packages/ui": {
|
||||
"name": "@opencode-ai/ui",
|
||||
"version": "1.15.6",
|
||||
"version": "1.15.5",
|
||||
"dependencies": {
|
||||
"@kobalte/core": "catalog:",
|
||||
"@opencode-ai/core": "workspace:*",
|
||||
@@ -673,7 +659,7 @@
|
||||
},
|
||||
"packages/web": {
|
||||
"name": "@opencode-ai/web",
|
||||
"version": "1.15.6",
|
||||
"version": "1.15.5",
|
||||
"dependencies": {
|
||||
"@astrojs/cloudflare": "12.6.3",
|
||||
"@astrojs/markdown-remark": "6.3.1",
|
||||
@@ -729,18 +715,17 @@
|
||||
},
|
||||
"catalog": {
|
||||
"@cloudflare/workers-types": "4.20251008.0",
|
||||
"@effect/opentelemetry": "4.0.0-beta.66",
|
||||
"@effect/platform-node": "4.0.0-beta.66",
|
||||
"@effect/sql-sqlite-bun": "4.0.0-beta.66",
|
||||
"@effect/opentelemetry": "4.0.0-beta.65",
|
||||
"@effect/platform-node": "4.0.0-beta.65",
|
||||
"@hono/zod-validator": "0.4.2",
|
||||
"@kobalte/core": "0.13.11",
|
||||
"@lydell/node-pty": "1.2.0-beta.10",
|
||||
"@npmcli/arborist": "9.4.0",
|
||||
"@octokit/rest": "22.0.0",
|
||||
"@openauthjs/openauth": "0.0.0-20250322224806",
|
||||
"@opentui/core": "0.2.15",
|
||||
"@opentui/keymap": "0.2.15",
|
||||
"@opentui/solid": "0.2.15",
|
||||
"@opentui/core": "0.2.14",
|
||||
"@opentui/keymap": "0.2.14",
|
||||
"@opentui/solid": "0.2.14",
|
||||
"@pierre/diffs": "1.1.0-beta.18",
|
||||
"@playwright/test": "1.59.1",
|
||||
"@sentry/solid": "10.36.0",
|
||||
@@ -762,9 +747,9 @@
|
||||
"cross-spawn": "7.0.6",
|
||||
"diff": "8.0.2",
|
||||
"dompurify": "3.3.1",
|
||||
"drizzle-kit": "1.0.0-rc.2",
|
||||
"drizzle-orm": "1.0.0-rc.2",
|
||||
"effect": "4.0.0-beta.66",
|
||||
"drizzle-kit": "1.0.0-beta.19-d95b7a4",
|
||||
"drizzle-orm": "1.0.0-beta.19-d95b7a4",
|
||||
"effect": "4.0.0-beta.65",
|
||||
"fuzzysort": "3.1.0",
|
||||
"hono": "4.10.7",
|
||||
"hono-openapi": "1.1.2",
|
||||
@@ -1123,13 +1108,11 @@
|
||||
|
||||
"@drizzle-team/brocli": ["@drizzle-team/brocli@0.11.0", "", {}, "sha512-hD3pekGiPg0WPCCGAZmusBBJsDqGUR66Y452YgQsZOnkdQ7ViEPKuyP4huUGEZQefp8g34RRodXYmJ2TbCH+tg=="],
|
||||
|
||||
"@effect/opentelemetry": ["@effect/opentelemetry@4.0.0-beta.66", "", { "peerDependencies": { "@opentelemetry/api": "^1.9", "@opentelemetry/api-logs": ">=0.203.0 <0.300.0", "@opentelemetry/resources": "^2.0.0", "@opentelemetry/sdk-logs": ">=0.203.0 <0.300.0", "@opentelemetry/sdk-metrics": "^2.0.0", "@opentelemetry/sdk-trace-base": "^2.0.0", "@opentelemetry/sdk-trace-node": "^2.0.0", "@opentelemetry/sdk-trace-web": "^2.0.0", "@opentelemetry/semantic-conventions": "^1.33.0", "effect": "^4.0.0-beta.66" }, "optionalPeers": ["@opentelemetry/api", "@opentelemetry/api-logs", "@opentelemetry/resources", "@opentelemetry/sdk-logs", "@opentelemetry/sdk-metrics", "@opentelemetry/sdk-trace-base", "@opentelemetry/sdk-trace-node", "@opentelemetry/sdk-trace-web"] }, "sha512-LU3ejAzJS+4P+Qtfn9ULnsGcIPmx1tUUB2ZswFRL+EolD8US7zMljHTwGuQRUBJOjDwt7wFCMN5AR512vdY8FQ=="],
|
||||
"@effect/opentelemetry": ["@effect/opentelemetry@4.0.0-beta.65", "", { "peerDependencies": { "@opentelemetry/api": "^1.9", "@opentelemetry/api-logs": ">=0.203.0 <0.300.0", "@opentelemetry/resources": "^2.0.0", "@opentelemetry/sdk-logs": ">=0.203.0 <0.300.0", "@opentelemetry/sdk-metrics": "^2.0.0", "@opentelemetry/sdk-trace-base": "^2.0.0", "@opentelemetry/sdk-trace-node": "^2.0.0", "@opentelemetry/sdk-trace-web": "^2.0.0", "@opentelemetry/semantic-conventions": "^1.33.0", "effect": "^4.0.0-beta.65" }, "optionalPeers": ["@opentelemetry/api", "@opentelemetry/api-logs", "@opentelemetry/resources", "@opentelemetry/sdk-logs", "@opentelemetry/sdk-metrics", "@opentelemetry/sdk-trace-base", "@opentelemetry/sdk-trace-node", "@opentelemetry/sdk-trace-web"] }, "sha512-0CD2fSsXrDM7FP2WFkbGJO1DwMqWR3UKHh6oBDXPHAPA+RsJSKoh3pLQsbQfldLuKnhOy87Bv0v9r9IdrIHCQw=="],
|
||||
|
||||
"@effect/platform-node": ["@effect/platform-node@4.0.0-beta.66", "", { "dependencies": { "@effect/platform-node-shared": "^4.0.0-beta.66", "mime": "^4.1.0", "undici": "^8.0.2" }, "peerDependencies": { "effect": "^4.0.0-beta.66", "ioredis": "^5.7.0" } }, "sha512-s/0RgaQFuszzdorRnX1PwEQNnSOi+JgMJo3zEe9O2NR3sosMhTr0Uk+1AF6bUOI9uJ2CPT3KpTIIU7q5/TpOkg=="],
|
||||
"@effect/platform-node": ["@effect/platform-node@4.0.0-beta.65", "", { "dependencies": { "@effect/platform-node-shared": "^4.0.0-beta.65", "mime": "^4.1.0", "undici": "^8.0.2" }, "peerDependencies": { "effect": "^4.0.0-beta.65", "ioredis": "^5.7.0" } }, "sha512-QQy3KRcMwP0TngQdfQGl2u1zp03B7k7DuF5SNS8aZhD0dDBpKZpCwFad1ODY5qdY3ycPgMwBwKRRK7y/aw0C9w=="],
|
||||
|
||||
"@effect/platform-node-shared": ["@effect/platform-node-shared@4.0.0-beta.66", "", { "dependencies": { "@types/ws": "^8.18.1", "ws": "^8.20.0" }, "peerDependencies": { "effect": "^4.0.0-beta.66" } }, "sha512-+ymrhBnESv/hmn5SKTe2//IY9Ox/hGPeoogEWhW47ZGyhFI5eMYFxdEUBa+3IAV05rrBzrxON9lynu68n0DM7w=="],
|
||||
|
||||
"@effect/sql-sqlite-bun": ["@effect/sql-sqlite-bun@4.0.0-beta.66", "", { "peerDependencies": { "effect": "^4.0.0-beta.66" } }, "sha512-UYsrAb/5T0ZRypeN9Kmv3/ZInibGCjM6dtoiAWtfG+xKyuq8N05wmuVCXB0+XgVmUBxDWjw/S1fu4ivS0vZVuw=="],
|
||||
"@effect/platform-node-shared": ["@effect/platform-node-shared@4.0.0-beta.65", "", { "dependencies": { "@types/ws": "^8.18.1", "ws": "^8.20.0" }, "peerDependencies": { "effect": "^4.0.0-beta.65" } }, "sha512-3rY8F3WLEax6Hj08GI/OvDIH+KqjfxH7RM2bAMfgR75NgRmwDtny1P49PtPkoRjH5dcdtThThtsvE4X9OTZkpQ=="],
|
||||
|
||||
"@electron/asar": ["@electron/asar@3.4.1", "", { "dependencies": { "commander": "^5.0.0", "glob": "^7.1.6", "minimatch": "^3.0.4" }, "bin": { "asar": "bin/asar.js" } }, "sha512-i4/rNPRS84t0vSRa2HorerGRXWyF4vThfHesw0dmcWHp+cspK743UanA0suA5Q5y8kzY2y6YKrvbIUn69BCAiA=="],
|
||||
|
||||
@@ -1559,8 +1542,6 @@
|
||||
|
||||
"@opencode-ai/desktop": ["@opencode-ai/desktop@workspace:packages/desktop"],
|
||||
|
||||
"@opencode-ai/effect-drizzle-sqlite": ["@opencode-ai/effect-drizzle-sqlite@workspace:packages/effect-drizzle-sqlite"],
|
||||
|
||||
"@opencode-ai/enterprise": ["@opencode-ai/enterprise@workspace:packages/enterprise"],
|
||||
|
||||
"@opencode-ai/function": ["@opencode-ai/function@workspace:packages/function"],
|
||||
@@ -1611,23 +1592,23 @@
|
||||
|
||||
"@opentelemetry/semantic-conventions": ["@opentelemetry/semantic-conventions@1.40.0", "", {}, "sha512-cifvXDhcqMwwTlTK04GBNeIe7yyo28Mfby85QXFe1Yk8nmi36Ab/5UQwptOx84SsoGNRg+EVSjwzfSZMy6pmlw=="],
|
||||
|
||||
"@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": ["@opentui/core@0.2.14", "", { "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.14", "@opentui/core-darwin-x64": "0.2.14", "@opentui/core-linux-arm64": "0.2.14", "@opentui/core-linux-x64": "0.2.14", "@opentui/core-win32-arm64": "0.2.14", "@opentui/core-win32-x64": "0.2.14" }, "peerDependencies": { "web-tree-sitter": "0.25.10" } }, "sha512-17YCr3BqM9mhi/DdNVM+omgmrKQNIl0G5RzoaTFOHe4+OAhG+W3iooYi+WdsekJWSUOEwZqDRz0QBTZhOtgZsQ=="],
|
||||
|
||||
"@opentui/core-darwin-arm64": ["@opentui/core-darwin-arm64@0.2.15", "", { "os": "darwin", "cpu": "arm64" }, "sha512-s25f9GmZd6wxNM5ExRmwwnLT+NLCKxnTWuO9aObOlqsXfLMGHQZrb6YwgAn/PSTua98KmH7GJCVWdPgZ/P+0RQ=="],
|
||||
"@opentui/core-darwin-arm64": ["@opentui/core-darwin-arm64@0.2.14", "", { "os": "darwin", "cpu": "arm64" }, "sha512-iS4NZQkOKX2EP5rsNjDcU7inDLcKhPaSBn8ENjDXKx2smOh7p/rgM2qlEaiLI3njtL784QoF+nxTzSXbEI6+Jw=="],
|
||||
|
||||
"@opentui/core-darwin-x64": ["@opentui/core-darwin-x64@0.2.15", "", { "os": "darwin", "cpu": "x64" }, "sha512-GyaipN+nOcEr8rcTO2mqKTGmOBk0C300I69fLtubD3BadHcMI1DVNlQrcf/J1mkQEuMYbmBTi/1hT1ybWGr2Mw=="],
|
||||
"@opentui/core-darwin-x64": ["@opentui/core-darwin-x64@0.2.14", "", { "os": "darwin", "cpu": "x64" }, "sha512-ft4ZwYHCV0VtRMwQtHH5mAgwqRLHEXP26DWcwtCZWDEHDvghClBR0cj9UZLH5JAKn/j7ds5hZDCCZz+nUiEHYA=="],
|
||||
|
||||
"@opentui/core-linux-arm64": ["@opentui/core-linux-arm64@0.2.15", "", { "os": "linux", "cpu": "arm64" }, "sha512-h+uyufselGT4afKMP8Lg4yUl5Kp+DJBlhu3XpWXhphE5Pnq5+f0uGBr4P+34CNcWxMsDnvagSQLFRCS4rGrOWA=="],
|
||||
"@opentui/core-linux-arm64": ["@opentui/core-linux-arm64@0.2.14", "", { "os": "linux", "cpu": "arm64" }, "sha512-t/EKD4+rlzWuwYAa6NzGCmiBOHvF+hzjNwExj+dnSqX5wK7TU+VHl+N2iYUl4VhhJK94kPP6BnrF5GcHnZGFLg=="],
|
||||
|
||||
"@opentui/core-linux-x64": ["@opentui/core-linux-x64@0.2.15", "", { "os": "linux", "cpu": "x64" }, "sha512-jx+NImPq4wSp3Apfe7tlixiEJNnRyECTRJRWhGF6ZJz4PwFfgK2UHZKYR0DZHbV8nYawoDNQPJDXEWcoZShnMg=="],
|
||||
"@opentui/core-linux-x64": ["@opentui/core-linux-x64@0.2.14", "", { "os": "linux", "cpu": "x64" }, "sha512-Lvqmd92UZ+KZVnr0xU0jYj4XqnCSsBQJHS/FpYkJgZSAN7/4NmPlgMvQXIGW8a3BcFaGRKe8LuGpqM2E4oaX0Q=="],
|
||||
|
||||
"@opentui/core-win32-arm64": ["@opentui/core-win32-arm64@0.2.15", "", { "os": "win32", "cpu": "arm64" }, "sha512-2SQQLvf3sgmToxrNika9AdcccKrjPJEn5jW6sSv0oEixNBzUzW41vSZZG4LM/V3lL8eg0LoYDnRZeKLB4gwSqQ=="],
|
||||
"@opentui/core-win32-arm64": ["@opentui/core-win32-arm64@0.2.14", "", { "os": "win32", "cpu": "arm64" }, "sha512-Jnuud29daaEoZNEp80dxUDLyUcwLr+g6SruHPyyWerOe7J10JE1ihJNkDlXLT7T49xdBGYRQlRkuNGwRfZWx5A=="],
|
||||
|
||||
"@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/core-win32-x64": ["@opentui/core-win32-x64@0.2.14", "", { "os": "win32", "cpu": "x64" }, "sha512-2ZUNh7yaAMUwAOK8oFEO28qXqPFrWPGGD0KHK1Gp97Th9XuVZniMLtbkbrFtDRh15+PVj7MyrG0N967W7rLr1A=="],
|
||||
|
||||
"@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/keymap": ["@opentui/keymap@0.2.14", "", { "dependencies": { "@opentui/core": "0.2.14" }, "peerDependencies": { "@opentui/react": "0.2.14", "@opentui/solid": "0.2.14", "react": ">=19.2.0", "solid-js": "1.9.12" }, "optionalPeers": ["@opentui/react", "@opentui/solid", "react", "solid-js"] }, "sha512-Jd4F3S98D8bJcr41jk7KcsFwwQTA0GCNKb+LoDMkPwv00k+NV6XcrXPB3QlNeM/JrVbe55pyGaT/ynZklGYHRw=="],
|
||||
|
||||
"@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=="],
|
||||
"@opentui/solid": ["@opentui/solid@0.2.14", "", { "dependencies": { "@babel/core": "7.28.0", "@babel/preset-typescript": "7.27.1", "@opentui/core": "0.2.14", "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-tSWiiwdh/J+crkjwHgd26HXAlJHYocwkN/eUoqSH3+Y/uO3c3qRnbzQz7zzds+j4XDQU/8e9QcZshYLvQT9c7w=="],
|
||||
|
||||
"@oslojs/asn1": ["@oslojs/asn1@1.0.0", "", { "dependencies": { "@oslojs/binary": "1.0.0" } }, "sha512-zw/wn0sj0j0QKbIXfIlnEcTviaCzYOY3V5rAyjR6YtOByFtJiT574+8p9Wlach0lZH9fddD4yb9laEAIl4vXQA=="],
|
||||
|
||||
@@ -2993,9 +2974,9 @@
|
||||
|
||||
"dotenv-expand": ["dotenv-expand@11.0.7", "", { "dependencies": { "dotenv": "^16.4.5" } }, "sha512-zIHwmZPRshsCdpMDyVsqGmgyP0yT8GAgXUnkdAoJisxvf33k7yO6OuoKmcTGuXPWSsm8Oh88nZicRLA9Y0rUeA=="],
|
||||
|
||||
"drizzle-kit": ["drizzle-kit@1.0.0-rc.2", "", { "dependencies": { "@drizzle-team/brocli": "^0.11.0", "@js-temporal/polyfill": "^0.5.1", "esbuild": "^0.25.10", "get-tsconfig": "^4.13.6", "jiti": "^2.6.1" }, "bin": { "drizzle-kit": "bin.cjs" } }, "sha512-TRxUmj1wDA2QCt3GvuhfamvIa66wJ7+MzSxBMKkpRtYScjHTumT9BE+x6daSzuEacSrPEuUH5/cW1uo5RkoPIg=="],
|
||||
"drizzle-kit": ["drizzle-kit@1.0.0-beta.19-d95b7a4", "", { "dependencies": { "@drizzle-team/brocli": "^0.11.0", "@js-temporal/polyfill": "^0.5.1", "esbuild": "^0.25.10", "get-tsconfig": "^4.13.6", "jiti": "^2.6.1" }, "bin": { "drizzle-kit": "bin.cjs" } }, "sha512-M0sqc+42TYBod6kEZ3AsW6+JWe3+76gR1aDFbHH5DmuLKEwewmbzlhBG6qnvV6YA1cIIbkuam3dC7r6PREOCXw=="],
|
||||
|
||||
"drizzle-orm": ["drizzle-orm@1.0.0-rc.2", "", { "peerDependencies": { "@aws-sdk/client-rds-data": ">=3", "@cloudflare/workers-types": ">=4", "@effect/sql-pg": ">=4.0.0-beta.58 || >=4.0.0", "@electric-sql/pglite": ">=0.2.0", "@libsql/client": ">=0.10.0", "@libsql/client-wasm": ">=0.10.0", "@neondatabase/serverless": ">=0.10.0", "@op-engineering/op-sqlite": ">=2", "@opentelemetry/api": "^1.4.1", "@planetscale/database": ">=1.13", "@sinclair/typebox": ">=0.34.8", "@sqlitecloud/drivers": ">=1.0.653", "@tidbcloud/serverless": "*", "@tursodatabase/database": ">=0.2.1", "@tursodatabase/database-common": ">=0.2.1", "@tursodatabase/database-wasm": ">=0.2.1", "@types/better-sqlite3": "*", "@types/mssql": "^9.1.4", "@types/pg": "*", "@types/sql.js": "*", "@upstash/redis": ">=1.34.7", "@vercel/postgres": ">=0.8.0", "@xata.io/client": "*", "arktype": ">=2.0.0", "better-sqlite3": ">=9.3.0", "bun-types": "*", "effect": ">=4.0.0-beta.58 || >=4.0.0", "expo-sqlite": ">=14.0.0", "mssql": "^11.0.1", "mysql2": ">=2", "pg": ">=8", "postgres": ">=3", "sql.js": ">=1", "sqlite3": ">=5", "typebox": ">=1.0.0", "valibot": ">=1.0.0-beta.7", "zod": "^3.25.0 || ^4.0.0" }, "optionalPeers": ["@aws-sdk/client-rds-data", "@cloudflare/workers-types", "@effect/sql-pg", "@electric-sql/pglite", "@libsql/client", "@libsql/client-wasm", "@neondatabase/serverless", "@op-engineering/op-sqlite", "@opentelemetry/api", "@planetscale/database", "@sinclair/typebox", "@sqlitecloud/drivers", "@tidbcloud/serverless", "@tursodatabase/database", "@tursodatabase/database-common", "@tursodatabase/database-wasm", "@types/better-sqlite3", "@types/mssql", "@types/pg", "@types/sql.js", "@upstash/redis", "@vercel/postgres", "@xata.io/client", "arktype", "better-sqlite3", "bun-types", "effect", "expo-sqlite", "mssql", "mysql2", "pg", "postgres", "sql.js", "sqlite3", "typebox", "valibot", "zod"] }, "sha512-UXYDkbplF5wX0hwxll+80QhEwUvAJLBu+tAK/d4fna18kLE6VuliAzufF/ieDEIJeSnLRYgtmsXD6x1Xuy1kIg=="],
|
||||
"drizzle-orm": ["drizzle-orm@1.0.0-beta.19-d95b7a4", "", { "peerDependencies": { "@aws-sdk/client-rds-data": ">=3", "@cloudflare/workers-types": ">=4", "@effect/sql": "^0.48.5", "@effect/sql-pg": "^0.49.7", "@electric-sql/pglite": ">=0.2.0", "@libsql/client": ">=0.10.0", "@libsql/client-wasm": ">=0.10.0", "@neondatabase/serverless": ">=0.10.0", "@op-engineering/op-sqlite": ">=2", "@opentelemetry/api": "^1.4.1", "@planetscale/database": ">=1.13", "@sinclair/typebox": ">=0.34.8", "@sqlitecloud/drivers": ">=1.0.653", "@tidbcloud/serverless": "*", "@tursodatabase/database": ">=0.2.1", "@tursodatabase/database-common": ">=0.2.1", "@tursodatabase/database-wasm": ">=0.2.1", "@types/better-sqlite3": "*", "@types/mssql": "^9.1.4", "@types/pg": "*", "@types/sql.js": "*", "@upstash/redis": ">=1.34.7", "@vercel/postgres": ">=0.8.0", "@xata.io/client": "*", "arktype": ">=2.0.0", "better-sqlite3": ">=9.3.0", "bun-types": "*", "expo-sqlite": ">=14.0.0", "gel": ">=2", "mssql": "^11.0.1", "mysql2": ">=2", "pg": ">=8", "postgres": ">=3", "sql.js": ">=1", "sqlite3": ">=5", "typebox": ">=1.0.0", "valibot": ">=1.0.0-beta.7", "zod": "^3.25.0 || ^4.0.0" }, "optionalPeers": ["@aws-sdk/client-rds-data", "@cloudflare/workers-types", "@effect/sql", "@effect/sql-pg", "@electric-sql/pglite", "@libsql/client", "@libsql/client-wasm", "@neondatabase/serverless", "@op-engineering/op-sqlite", "@opentelemetry/api", "@planetscale/database", "@sinclair/typebox", "@sqlitecloud/drivers", "@tidbcloud/serverless", "@tursodatabase/database", "@tursodatabase/database-common", "@tursodatabase/database-wasm", "@types/better-sqlite3", "@types/pg", "@types/sql.js", "@upstash/redis", "@vercel/postgres", "@xata.io/client", "arktype", "better-sqlite3", "bun-types", "expo-sqlite", "gel", "mysql2", "pg", "postgres", "sql.js", "sqlite3", "typebox", "valibot", "zod"] }, "sha512-bZZKKeoRKrMVU6zKTscjrSH0+WNb1WEi3N0Jl4wEyQ7aQpTgHzdYY6IJQ1P0M74HuSJVeX4UpkFB/S6dtqLEJg=="],
|
||||
|
||||
"dset": ["dset@3.1.4", "", {}, "sha512-2QF/g9/zTaPDc3BjNcVTGoBbXBgYfMTTceLaYcFJ/W9kggFUkhxD/hMEeuLKbugyef9SqAx8cpgwlIP/jinUTA=="],
|
||||
|
||||
@@ -3009,7 +2990,7 @@
|
||||
|
||||
"ee-first": ["ee-first@1.1.1", "", {}, "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow=="],
|
||||
|
||||
"effect": ["effect@4.0.0-beta.66", "", { "dependencies": { "@standard-schema/spec": "^1.1.0", "fast-check": "^4.6.0", "find-my-way-ts": "^0.1.6", "ini": "^6.0.0", "kubernetes-types": "^1.30.0", "msgpackr": "^1.11.9", "multipasta": "^0.2.7", "toml": "^4.1.1", "uuid": "^13.0.0", "yaml": "^2.8.3" } }, "sha512-4arEr62cziFa8BBVDUwJCJJmaVepXf/kRg7KtC0h8+bufngscrHbwWFhr9c+HonwOF+31U3iD3xUJmw9KzX7Dw=="],
|
||||
"effect": ["effect@4.0.0-beta.65", "", { "dependencies": { "@standard-schema/spec": "^1.1.0", "fast-check": "^4.6.0", "find-my-way-ts": "^0.1.6", "ini": "^6.0.0", "kubernetes-types": "^1.30.0", "msgpackr": "^1.11.9", "multipasta": "^0.2.7", "toml": "^4.1.1", "uuid": "^13.0.0", "yaml": "^2.8.3" } }, "sha512-QYKvQPAj3CmtsvWkHQww15wX4KG2gNsszDWEcOO5sZCMknp66u6Si/Opmt3wwWCwsyvRmDAdIg+JIz5qzbbFIw=="],
|
||||
|
||||
"ejs": ["ejs@3.1.10", "", { "dependencies": { "jake": "^10.8.5" }, "bin": { "ejs": "bin/cli.js" } }, "sha512-UeJmFfOrAQS8OJWPZ4qtgHyWExa088/MtK5UEyoJGFH67cDEXkZSviOiKRCZ4Xij0zxI3JECgYs3oKx+AizQBA=="],
|
||||
|
||||
@@ -5697,8 +5678,6 @@
|
||||
|
||||
"cross-spawn/which": ["which@2.0.2", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "./bin/node-which" } }, "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA=="],
|
||||
|
||||
"db0/drizzle-orm": ["drizzle-orm@1.0.0-beta.19-d95b7a4", "", { "peerDependencies": { "@aws-sdk/client-rds-data": ">=3", "@cloudflare/workers-types": ">=4", "@effect/sql": "^0.48.5", "@effect/sql-pg": "^0.49.7", "@electric-sql/pglite": ">=0.2.0", "@libsql/client": ">=0.10.0", "@libsql/client-wasm": ">=0.10.0", "@neondatabase/serverless": ">=0.10.0", "@op-engineering/op-sqlite": ">=2", "@opentelemetry/api": "^1.4.1", "@planetscale/database": ">=1.13", "@sinclair/typebox": ">=0.34.8", "@sqlitecloud/drivers": ">=1.0.653", "@tidbcloud/serverless": "*", "@tursodatabase/database": ">=0.2.1", "@tursodatabase/database-common": ">=0.2.1", "@tursodatabase/database-wasm": ">=0.2.1", "@types/better-sqlite3": "*", "@types/mssql": "^9.1.4", "@types/pg": "*", "@types/sql.js": "*", "@upstash/redis": ">=1.34.7", "@vercel/postgres": ">=0.8.0", "@xata.io/client": "*", "arktype": ">=2.0.0", "better-sqlite3": ">=9.3.0", "bun-types": "*", "expo-sqlite": ">=14.0.0", "gel": ">=2", "mssql": "^11.0.1", "mysql2": ">=2", "pg": ">=8", "postgres": ">=3", "sql.js": ">=1", "sqlite3": ">=5", "typebox": ">=1.0.0", "valibot": ">=1.0.0-beta.7", "zod": "^3.25.0 || ^4.0.0" }, "optionalPeers": ["@aws-sdk/client-rds-data", "@cloudflare/workers-types", "@effect/sql", "@effect/sql-pg", "@electric-sql/pglite", "@libsql/client", "@libsql/client-wasm", "@neondatabase/serverless", "@op-engineering/op-sqlite", "@opentelemetry/api", "@planetscale/database", "@sinclair/typebox", "@sqlitecloud/drivers", "@tidbcloud/serverless", "@tursodatabase/database", "@tursodatabase/database-common", "@tursodatabase/database-wasm", "@types/better-sqlite3", "@types/pg", "@types/sql.js", "@upstash/redis", "@vercel/postgres", "@xata.io/client", "arktype", "better-sqlite3", "bun-types", "expo-sqlite", "gel", "mysql2", "pg", "postgres", "sql.js", "sqlite3", "typebox", "valibot", "zod"] }, "sha512-bZZKKeoRKrMVU6zKTscjrSH0+WNb1WEi3N0Jl4wEyQ7aQpTgHzdYY6IJQ1P0M74HuSJVeX4UpkFB/S6dtqLEJg=="],
|
||||
|
||||
"defaults/clone": ["clone@1.0.4", "", {}, "sha512-JQHZ2QMW6l3aH/j6xCqQThY/9OH4D/9ls34cgkUBiEeocRTU04tHfKPBsUK1PqZCUQM7GiA0IIXJSuXHI64Kbg=="],
|
||||
|
||||
"dir-compare/minimatch": ["minimatch@3.1.5", "", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w=="],
|
||||
|
||||
+2
-9
@@ -2,7 +2,6 @@ import { SECRET } from "./secret"
|
||||
import { domain } from "./stage"
|
||||
|
||||
const description = "Managed by SST (Don't edit in Honeycomb UI)"
|
||||
const alertsDisabled = $app.stage !== "production"
|
||||
|
||||
const webhookRecipient = new honeycomb.WebhookRecipient("DiscordAlerts", {
|
||||
name: $app.stage === "production" ? "Discord Alerts" : `Discord Alerts (${$app.stage})`,
|
||||
@@ -80,7 +79,7 @@ IF(
|
||||
filters,
|
||||
},
|
||||
],
|
||||
formulas: [{ name: "ERROR", expression: "IF(GTE($TOTAL, 150), DIV($FAILED, $TOTAL), 0)" }],
|
||||
formulas: [{ name: "ERROR", expression: "IF(GTE($TOTAL, 200), DIV($FAILED, $TOTAL), 0)" }],
|
||||
timeRange: 900,
|
||||
}).json
|
||||
}
|
||||
@@ -123,7 +122,7 @@ const providerHttpErrorsQuery = () => {
|
||||
},
|
||||
],
|
||||
formulas: [
|
||||
{ name: "ERROR", expression: "IF(GTE(SUM($SUCCESS, $FAILED), 150), DIV($FAILED, SUM($SUCCESS, $FAILED)), 0)" },
|
||||
{ name: "ERROR", expression: "IF(GTE(SUM($SUCCESS, $FAILED), 200), DIV($FAILED, SUM($SUCCESS, $FAILED)), 0)" },
|
||||
],
|
||||
timeRange: 900,
|
||||
}).json
|
||||
@@ -160,7 +159,6 @@ const modelLowTpsQuery = (product: "go" | "zen") => {
|
||||
new honeycomb.Trigger("IncreasedModelHttpErrorsGo", {
|
||||
name: "Increased Model HTTP Errors [Go]",
|
||||
description,
|
||||
disabled: alertsDisabled,
|
||||
queryJson: modelHttpErrorsQuery("go"),
|
||||
alertType: "on_change",
|
||||
frequency: 300,
|
||||
@@ -180,7 +178,6 @@ new honeycomb.Trigger("IncreasedModelHttpErrorsGo", {
|
||||
new honeycomb.Trigger("IncreasedModelHttpErrorsZen", {
|
||||
name: "Increased Model HTTP Errors [Zen]",
|
||||
description,
|
||||
disabled: alertsDisabled,
|
||||
queryJson: modelHttpErrorsQuery("zen"),
|
||||
alertType: "on_change",
|
||||
frequency: 300,
|
||||
@@ -200,7 +197,6 @@ new honeycomb.Trigger("IncreasedModelHttpErrorsZen", {
|
||||
new honeycomb.Trigger("LowModelTpsGo", {
|
||||
name: "Low Model TPS [Go]",
|
||||
description,
|
||||
disabled: alertsDisabled,
|
||||
queryJson: modelLowTpsQuery("go"),
|
||||
alertType: "on_change",
|
||||
frequency: 600,
|
||||
@@ -220,7 +216,6 @@ new honeycomb.Trigger("LowModelTpsGo", {
|
||||
new honeycomb.Trigger("LowModelTpsZen", {
|
||||
name: "Low Model TPS [Zen]",
|
||||
description,
|
||||
disabled: alertsDisabled,
|
||||
queryJson: modelLowTpsQuery("zen"),
|
||||
alertType: "on_change",
|
||||
frequency: 600,
|
||||
@@ -240,7 +235,6 @@ new honeycomb.Trigger("LowModelTpsZen", {
|
||||
new honeycomb.Trigger("IncreasedProviderHttpErrors", {
|
||||
name: "Increased Provider HTTP Errors",
|
||||
description,
|
||||
disabled: alertsDisabled,
|
||||
queryJson: providerHttpErrorsQuery(),
|
||||
alertType: "on_change",
|
||||
frequency: 300,
|
||||
@@ -260,7 +254,6 @@ new honeycomb.Trigger("IncreasedProviderHttpErrors", {
|
||||
new honeycomb.Trigger("IncreasedFreeTierRequests", {
|
||||
name: "Increased Free Tier Requests",
|
||||
description,
|
||||
disabled: alertsDisabled,
|
||||
queryJson: honeycomb.getQuerySpecificationOutput({
|
||||
calculations: [{ op: "COUNT" }],
|
||||
filters: [
|
||||
|
||||
+4
-4
@@ -1,8 +1,8 @@
|
||||
{
|
||||
"nodeModules": {
|
||||
"x86_64-linux": "sha256-kCSAVPQgJROcvnnwf0Cn6PuYL25hYgTasJeBJlmnFgQ=",
|
||||
"aarch64-linux": "sha256-prY27Ek2QhW+4OvBJ3bHHkUDoLTA4mD3KQmOQqSbAuo=",
|
||||
"aarch64-darwin": "sha256-0yIqnnjreVHTgGZLrKFpT9Cc2B2LNfmYcRByaCu7tiU=",
|
||||
"x86_64-darwin": "sha256-n+urvMRozB9nO5D3qyCweSa5HExFk1YGEzOt2445LEE="
|
||||
"x86_64-linux": "sha256-FI1mX42vJuYdUDdWevlfHz+OcYkDn/I/HUbHE/jdQvs=",
|
||||
"aarch64-linux": "sha256-3CQzzKnh/4Zf5vyn56yR5P3ULsW7K7Fr8/RQpekEJDk=",
|
||||
"aarch64-darwin": "sha256-XPDVHMxlPpXlf43BRqNnwF809unk6iE8tvd0o92d0/w=",
|
||||
"x86_64-darwin": "sha256-dFXTi13RSgL62lMsep1EoE/KSEPF7Oh31PVdxW1tkzg="
|
||||
}
|
||||
}
|
||||
|
||||
+8
-9
@@ -28,17 +28,16 @@
|
||||
"packages/slack"
|
||||
],
|
||||
"catalog": {
|
||||
"@effect/opentelemetry": "4.0.0-beta.66",
|
||||
"@effect/platform-node": "4.0.0-beta.66",
|
||||
"@effect/sql-sqlite-bun": "4.0.0-beta.66",
|
||||
"@effect/opentelemetry": "4.0.0-beta.65",
|
||||
"@effect/platform-node": "4.0.0-beta.65",
|
||||
"@npmcli/arborist": "9.4.0",
|
||||
"@types/bun": "1.3.13",
|
||||
"@types/cross-spawn": "6.0.6",
|
||||
"@octokit/rest": "22.0.0",
|
||||
"@hono/zod-validator": "0.4.2",
|
||||
"@opentui/core": "0.2.15",
|
||||
"@opentui/keymap": "0.2.15",
|
||||
"@opentui/solid": "0.2.15",
|
||||
"@opentui/core": "0.2.14",
|
||||
"@opentui/keymap": "0.2.14",
|
||||
"@opentui/solid": "0.2.14",
|
||||
"ulid": "3.0.1",
|
||||
"@kobalte/core": "0.13.11",
|
||||
"@types/luxon": "3.7.1",
|
||||
@@ -54,9 +53,9 @@
|
||||
"@tailwindcss/vite": "4.1.11",
|
||||
"diff": "8.0.2",
|
||||
"dompurify": "3.3.1",
|
||||
"drizzle-kit": "1.0.0-rc.2",
|
||||
"drizzle-orm": "1.0.0-rc.2",
|
||||
"effect": "4.0.0-beta.66",
|
||||
"drizzle-kit": "1.0.0-beta.19-d95b7a4",
|
||||
"drizzle-orm": "1.0.0-beta.19-d95b7a4",
|
||||
"effect": "4.0.0-beta.65",
|
||||
"ai": "6.0.168",
|
||||
"cross-spawn": "7.0.6",
|
||||
"hono": "4.10.7",
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
{
|
||||
"name": "@opencode-ai/app",
|
||||
"version": "1.15.6",
|
||||
"version": "1.15.5",
|
||||
"description": "",
|
||||
"type": "module",
|
||||
"exports": {
|
||||
".": "./src/index.ts",
|
||||
"./desktop-menu": "./src/desktop-menu.ts",
|
||||
"./vite": "./vite.js",
|
||||
"./index.css": "./src/index.css"
|
||||
},
|
||||
|
||||
@@ -1,23 +1,18 @@
|
||||
import { createEffect, createMemo, For, mapArray, Match, Show, startTransition, Switch, untrack } from "solid-js"
|
||||
import { createStore, produce } from "solid-js/store"
|
||||
import { useLocation, useMatch, useNavigate, useParams } from "@solidjs/router"
|
||||
import { createEffect, createMemo, Show, untrack } from "solid-js"
|
||||
import { createStore } from "solid-js/store"
|
||||
import { useLocation, useNavigate, useParams } from "@solidjs/router"
|
||||
import { IconButton } from "@opencode-ai/ui/icon-button"
|
||||
import { Icon } from "@opencode-ai/ui/icon"
|
||||
import { Button } from "@opencode-ai/ui/button"
|
||||
import { Tooltip, TooltipKeybind } from "@opencode-ai/ui/tooltip"
|
||||
import { useTheme } from "@opencode-ai/ui/theme/context"
|
||||
import { IconButtonV2 } from "@opencode-ai/ui/v2/components/icon-button-v2.jsx"
|
||||
|
||||
import { useLayout } from "@/context/layout"
|
||||
import { usePlatform } from "@/context/platform"
|
||||
import { useCommand } from "@/context/command"
|
||||
import { useLanguage } from "@/context/language"
|
||||
import { useSettings } from "@/context/settings"
|
||||
import { WindowsAppMenu } from "./windows-app-menu"
|
||||
import { applyPath, backPath, forwardPath } from "./titlebar-history"
|
||||
import { useGlobalSync } from "@/context/global-sync"
|
||||
import { decodeDirectory } from "@/pages/directory-layout"
|
||||
import { iife } from "@opencode-ai/core/util/iife"
|
||||
|
||||
type TauriDesktopWindow = {
|
||||
startDragging?: () => Promise<void>
|
||||
@@ -44,8 +39,6 @@ const titlebarHeight = 40
|
||||
const minTitlebarZoom = 0.25
|
||||
const windowsControlsBaseWidth = 138 // 3 native Windows caption buttons at 46px each.
|
||||
|
||||
const makeSessionHref = (b64Dir: string, sessionId: string) => `/${b64Dir}/session/${sessionId}`
|
||||
|
||||
export function Titlebar() {
|
||||
const layout = useLayout()
|
||||
const platform = usePlatform()
|
||||
@@ -59,7 +52,6 @@ export function Titlebar() {
|
||||
|
||||
const mac = createMemo(() => platform.platform === "desktop" && platform.os === "macos")
|
||||
const windows = createMemo(() => platform.platform === "desktop" && platform.os === "windows")
|
||||
const linux = createMemo(() => platform.platform === "desktop" && platform.os === "linux")
|
||||
const web = createMemo(() => platform.platform === "web")
|
||||
const zoom = () => platform.webviewZoom?.() ?? 1
|
||||
const titlebarZoom = () => (windows() ? Math.max(zoom(), minTitlebarZoom) : zoom())
|
||||
@@ -183,378 +175,162 @@ export function Titlebar() {
|
||||
|
||||
return (
|
||||
<header
|
||||
class="h-10 shrink-0 bg-background-base relative overflow-hidden flex flex-row"
|
||||
style={{ "min-height": minHeight(), "padding-left": mac() ? `${84 / zoom()}px` : 0 }}
|
||||
class="h-10 shrink-0 bg-background-base relative overflow-hidden"
|
||||
style={{ "min-height": minHeight() }}
|
||||
data-tauri-drag-region
|
||||
onMouseDown={drag}
|
||||
onDblClick={maximize}
|
||||
>
|
||||
<Switch>
|
||||
<Match when={import.meta.env.VITE_OPENCODE_CHANNEL !== "prod"}>
|
||||
{(_) => {
|
||||
const globalSync = useGlobalSync()
|
||||
const navigate = useNavigate()
|
||||
|
||||
type Tab = { dir: string; sessionId: string; params: any; href: string }
|
||||
|
||||
const [tabsStore, tabsStoreActions] = iife(() => {
|
||||
const [store, setStore] = createStore<Tab[]>(
|
||||
iife(() => {
|
||||
if (!params.dir || !params.id) return []
|
||||
return [
|
||||
{
|
||||
dir: decodeDirectory(params.dir) ?? "",
|
||||
sessionId: params.id,
|
||||
params: { id: params.id, dir: params.dir },
|
||||
href: makeSessionHref(params.dir, params.id),
|
||||
},
|
||||
]
|
||||
}),
|
||||
)
|
||||
|
||||
const actions = {
|
||||
addTab: (tab: Tab) => {
|
||||
setStore(
|
||||
produce((tabs) => {
|
||||
if (tabs.some((t) => t.href === tab.href)) return
|
||||
|
||||
tabs.push(tab)
|
||||
}),
|
||||
)
|
||||
},
|
||||
removeTab: (href: string) => {
|
||||
startTransition(() => {
|
||||
setStore(
|
||||
produce((tabs) => {
|
||||
const index = tabs.findIndex((t) => t.href === href)
|
||||
if (index === -1) return
|
||||
tabs.splice(index, 1)
|
||||
const nextTab = tabs[index] ?? tabs[tabs.length - 1]
|
||||
if (nextTab) navigate(nextTab.href)
|
||||
else navigate("/")
|
||||
}),
|
||||
)
|
||||
})
|
||||
},
|
||||
}
|
||||
|
||||
return [store, actions]
|
||||
})
|
||||
|
||||
createEffect(() => {
|
||||
const params = useParams()
|
||||
if (!(params.dir && params.id)) return
|
||||
|
||||
tabsStoreActions.addTab({
|
||||
dir: decodeDirectory(params.dir) ?? "",
|
||||
sessionId: params.id,
|
||||
params: { id: params.id, dir: params.dir },
|
||||
href: makeSessionHref(params.dir, params.id),
|
||||
})
|
||||
})
|
||||
|
||||
const tabsEnriched = iife(() => {
|
||||
const base = mapArray(
|
||||
() => tabsStore,
|
||||
(tab) => {
|
||||
const sync = globalSync.createDirSyncContext(tab.dir)
|
||||
const session = sync.session.get(tab.sessionId)
|
||||
return session ? { ...tab, info: session } : null
|
||||
},
|
||||
)
|
||||
|
||||
return () => base().flatMap((s) => (s ? [s] : []))
|
||||
})
|
||||
|
||||
return (
|
||||
<div class="h-full flex-1 flex flex-row items-center gap-1.5 pr-3">
|
||||
<ChannelIndicator />
|
||||
<Show when={windows() || linux()}>
|
||||
<WindowsAppMenu command={command} platform={platform} />
|
||||
</Show>
|
||||
<IconButtonV2
|
||||
as="a"
|
||||
href="/"
|
||||
variant="ghost-muted"
|
||||
size="large"
|
||||
class="!w-8"
|
||||
state={!!useMatch(() => "/")() ? "pressed" : undefined}
|
||||
>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16" fill="none">
|
||||
<path
|
||||
d="M13.9948 11.668H9.32812M11.6641 9.33203V13.9987M6.66667 9.33203V13.9987H2V9.33203H6.66667ZM6.66667 2V6.66667H2V2H6.66667ZM13.9948 2V6.66667H9.32812V2H13.9948Z"
|
||||
stroke="currentColor"
|
||||
stroke-miterlimit="10"
|
||||
stroke-linecap="square"
|
||||
/>
|
||||
</svg>
|
||||
</IconButtonV2>
|
||||
<div class="flex flex-row items-center gap-2">
|
||||
<For each={tabsEnriched()}>
|
||||
{(tab, i) => (
|
||||
<>
|
||||
{i() !== 0 && <div class="w-[1.5px] h-3 rounded-full bg-[var(--v2-background-bg-layer-02)]" />}
|
||||
<TabNavItem
|
||||
href={tab.href}
|
||||
title={tab.info.title}
|
||||
onClose={() => tabsStoreActions.removeTab(tab.href)}
|
||||
hideClose={tabsEnriched().length < 2}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</For>
|
||||
</div>
|
||||
<button>
|
||||
<div class="p-1.5">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="16"
|
||||
height="16"
|
||||
viewBox="0 0 16 16"
|
||||
fill="none"
|
||||
class="size-4"
|
||||
>
|
||||
<path
|
||||
d="M7.99978 2.88867V13.1109M2.88867 7.99978H13.1109"
|
||||
stroke="#808080"
|
||||
stroke-linejoin="round"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
</button>
|
||||
|
||||
<div class="flex-1" />
|
||||
{/*<button class="px-2.5 py-1.5 bg-[rgba(0,0,0,0.08)] rounded-[6px]">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="16"
|
||||
height="16"
|
||||
viewBox="0 0 16 16"
|
||||
fill="none"
|
||||
class="size-4"
|
||||
>
|
||||
<path
|
||||
d="M10.4443 2.44436V13.5555M1.55546 13.5554H14.4443V2.44434H1.55542L1.55546 13.5554Z"
|
||||
stroke="#3A3A3A"
|
||||
/>
|
||||
</svg>
|
||||
</button>*/}
|
||||
</div>
|
||||
)
|
||||
<div
|
||||
class="grid h-full min-h-full w-full grid-cols-[minmax(0,1fr)_auto_minmax(0,1fr)] items-center"
|
||||
style={{ zoom: counterZoom() }}
|
||||
>
|
||||
<div
|
||||
classList={{
|
||||
"flex items-center min-w-0": true,
|
||||
"pl-2": !mac(),
|
||||
}}
|
||||
</Match>
|
||||
<Match when>
|
||||
<div
|
||||
class="grid h-full min-h-full w-full grid-cols-[minmax(0,1fr)_auto_minmax(0,1fr)] items-center"
|
||||
style={{ zoom: counterZoom() }}
|
||||
>
|
||||
<div
|
||||
classList={{
|
||||
"flex items-center min-w-0": true,
|
||||
"pl-2": !mac(),
|
||||
}}
|
||||
>
|
||||
<Show when={windows() || linux()}>
|
||||
<WindowsAppMenu command={command} platform={platform} />
|
||||
</Show>
|
||||
<Show when={mac()}>
|
||||
<div class="h-full shrink-0" style={{ width: `${72 / zoom()}px` }} />
|
||||
<div class="xl:hidden w-10 shrink-0 flex items-center justify-center">
|
||||
<IconButton
|
||||
icon="menu"
|
||||
variant="ghost"
|
||||
class="titlebar-icon rounded-md"
|
||||
onClick={layout.mobileSidebar.toggle}
|
||||
aria-label={language.t("sidebar.menu.toggle")}
|
||||
aria-expanded={layout.mobileSidebar.opened()}
|
||||
/>
|
||||
</div>
|
||||
</Show>
|
||||
<Show when={!mac()}>
|
||||
<div class="xl:hidden w-[48px] shrink-0 flex items-center justify-center">
|
||||
<IconButton
|
||||
icon="menu"
|
||||
variant="ghost"
|
||||
class="titlebar-icon rounded-md"
|
||||
onClick={layout.mobileSidebar.toggle}
|
||||
aria-label={language.t("sidebar.menu.toggle")}
|
||||
aria-expanded={layout.mobileSidebar.opened()}
|
||||
/>
|
||||
</div>
|
||||
</Show>
|
||||
<div class="flex items-center gap-1 shrink-0">
|
||||
<TooltipKeybind
|
||||
class={web() ? "hidden xl:flex shrink-0 ml-14" : "hidden xl:flex shrink-0 ml-2"}
|
||||
placement="bottom"
|
||||
title={language.t("command.sidebar.toggle")}
|
||||
keybind={command.keybind("sidebar.toggle")}
|
||||
>
|
||||
<Button
|
||||
variant="ghost"
|
||||
class="group/sidebar-toggle titlebar-icon w-8 h-6 p-0 box-border"
|
||||
onClick={layout.sidebar.toggle}
|
||||
aria-label={language.t("command.sidebar.toggle")}
|
||||
aria-expanded={layout.sidebar.opened()}
|
||||
>
|
||||
<Icon size="small" name={layout.sidebar.opened() ? "sidebar-active" : "sidebar"} />
|
||||
</Button>
|
||||
</TooltipKeybind>
|
||||
<div class="hidden xl:flex items-center shrink-0">
|
||||
<Show when={params.dir}>
|
||||
<div
|
||||
class="flex items-center shrink-0 w-8 mr-1"
|
||||
aria-hidden={layout.sidebar.opened() ? "true" : undefined}
|
||||
>
|
||||
<div
|
||||
class="transition-opacity"
|
||||
classList={{
|
||||
"opacity-100 duration-120 ease-out": !layout.sidebar.opened(),
|
||||
"opacity-0 duration-120 ease-in delay-0 pointer-events-none": layout.sidebar.opened(),
|
||||
}}
|
||||
>
|
||||
<TooltipKeybind
|
||||
placement="bottom"
|
||||
title={language.t("command.session.new")}
|
||||
keybind={command.keybind("session.new")}
|
||||
openDelay={2000}
|
||||
>
|
||||
<Button
|
||||
variant="ghost"
|
||||
icon={creating() ? "new-session-active" : "new-session"}
|
||||
class="titlebar-icon w-8 h-6 p-0 box-border"
|
||||
disabled={layout.sidebar.opened()}
|
||||
tabIndex={layout.sidebar.opened() ? -1 : undefined}
|
||||
onClick={() => {
|
||||
if (!params.dir) return
|
||||
navigate(`/${params.dir}/session`)
|
||||
}}
|
||||
aria-label={language.t("command.session.new")}
|
||||
aria-current={creating() ? "page" : undefined}
|
||||
/>
|
||||
</TooltipKeybind>
|
||||
</div>
|
||||
</div>
|
||||
</Show>
|
||||
<div
|
||||
class="flex items-center shrink-0"
|
||||
classList={{
|
||||
"-translate-x-[36px]": layout.sidebar.opened() && !!params.dir,
|
||||
"duration-180 ease-out": !layout.sidebar.opened(),
|
||||
"duration-180 ease-in": layout.sidebar.opened(),
|
||||
}}
|
||||
>
|
||||
<Show when={hasProjects() && nav()}>
|
||||
<div class="flex items-center gap-0 transition-transform">
|
||||
<Tooltip placement="bottom" value={language.t("common.goBack")} openDelay={2000}>
|
||||
<Button
|
||||
variant="ghost"
|
||||
icon="chevron-left"
|
||||
class="titlebar-icon w-6 h-6 p-0 box-border"
|
||||
disabled={!canBack()}
|
||||
onClick={back}
|
||||
aria-label={language.t("common.goBack")}
|
||||
/>
|
||||
</Tooltip>
|
||||
<Tooltip placement="bottom" value={language.t("common.goForward")} openDelay={2000}>
|
||||
<Button
|
||||
variant="ghost"
|
||||
icon="chevron-right"
|
||||
class="titlebar-icon w-6 h-6 p-0 box-border"
|
||||
disabled={!canForward()}
|
||||
onClick={forward}
|
||||
aria-label={language.t("common.goForward")}
|
||||
/>
|
||||
</Tooltip>
|
||||
</div>
|
||||
</Show>
|
||||
<div id="opencode-titlebar-left" class="flex items-center gap-3 min-w-0 px-2" />
|
||||
<ChannelIndicator />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="min-w-0 flex items-center justify-center pointer-events-none">
|
||||
<div
|
||||
id="opencode-titlebar-center"
|
||||
class="pointer-events-auto min-w-0 flex justify-center w-fit max-w-full"
|
||||
>
|
||||
<Show when={mac()}>
|
||||
<div class="h-full shrink-0" style={{ width: `${72 / zoom()}px` }} />
|
||||
<div class="xl:hidden w-10 shrink-0 flex items-center justify-center">
|
||||
<IconButton
|
||||
icon="menu"
|
||||
variant="ghost"
|
||||
class="titlebar-icon rounded-md"
|
||||
onClick={layout.mobileSidebar.toggle}
|
||||
aria-label={language.t("sidebar.menu.toggle")}
|
||||
aria-expanded={layout.mobileSidebar.opened()}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div
|
||||
classList={{
|
||||
"flex items-center min-w-0 justify-end": true,
|
||||
"pr-2": !windows(),
|
||||
}}
|
||||
data-tauri-drag-region
|
||||
onMouseDown={drag}
|
||||
</Show>
|
||||
<Show when={!mac()}>
|
||||
<div class="xl:hidden w-[48px] shrink-0 flex items-center justify-center">
|
||||
<IconButton
|
||||
icon="menu"
|
||||
variant="ghost"
|
||||
class="titlebar-icon rounded-md"
|
||||
onClick={layout.mobileSidebar.toggle}
|
||||
aria-label={language.t("sidebar.menu.toggle")}
|
||||
aria-expanded={layout.mobileSidebar.opened()}
|
||||
/>
|
||||
</div>
|
||||
</Show>
|
||||
<div class="flex items-center gap-1 shrink-0">
|
||||
<TooltipKeybind
|
||||
class={web() ? "hidden xl:flex shrink-0 ml-14" : "hidden xl:flex shrink-0 ml-2"}
|
||||
placement="bottom"
|
||||
title={language.t("command.sidebar.toggle")}
|
||||
keybind={command.keybind("sidebar.toggle")}
|
||||
>
|
||||
<div id="opencode-titlebar-right" class="flex items-center gap-1 shrink-0 justify-end" />
|
||||
<Show when={windows()}>
|
||||
{!tauriApi() && <div class="shrink-0" style={{ width: windowsControlsWidth() }} />}
|
||||
<div data-tauri-decorum-tb class="flex flex-row" />
|
||||
<Button
|
||||
variant="ghost"
|
||||
class="group/sidebar-toggle titlebar-icon w-8 h-6 p-0 box-border"
|
||||
onClick={layout.sidebar.toggle}
|
||||
aria-label={language.t("command.sidebar.toggle")}
|
||||
aria-expanded={layout.sidebar.opened()}
|
||||
>
|
||||
<Icon size="small" name={layout.sidebar.opened() ? "sidebar-active" : "sidebar"} />
|
||||
</Button>
|
||||
</TooltipKeybind>
|
||||
<div class="hidden xl:flex items-center shrink-0">
|
||||
<Show when={params.dir}>
|
||||
<div
|
||||
class="flex items-center shrink-0 w-8 mr-1"
|
||||
aria-hidden={layout.sidebar.opened() ? "true" : undefined}
|
||||
>
|
||||
<div
|
||||
class="transition-opacity"
|
||||
classList={{
|
||||
"opacity-100 duration-120 ease-out": !layout.sidebar.opened(),
|
||||
"opacity-0 duration-120 ease-in delay-0 pointer-events-none": layout.sidebar.opened(),
|
||||
}}
|
||||
>
|
||||
<TooltipKeybind
|
||||
placement="bottom"
|
||||
title={language.t("command.session.new")}
|
||||
keybind={command.keybind("session.new")}
|
||||
openDelay={2000}
|
||||
>
|
||||
<Button
|
||||
variant="ghost"
|
||||
icon={creating() ? "new-session-active" : "new-session"}
|
||||
class="titlebar-icon w-8 h-6 p-0 box-border"
|
||||
disabled={layout.sidebar.opened()}
|
||||
tabIndex={layout.sidebar.opened() ? -1 : undefined}
|
||||
onClick={() => {
|
||||
if (!params.dir) return
|
||||
navigate(`/${params.dir}/session`)
|
||||
}}
|
||||
aria-label={language.t("command.session.new")}
|
||||
aria-current={creating() ? "page" : undefined}
|
||||
/>
|
||||
</TooltipKeybind>
|
||||
</div>
|
||||
</div>
|
||||
</Show>
|
||||
<div
|
||||
class="flex items-center shrink-0"
|
||||
classList={{
|
||||
"-translate-x-[36px]": layout.sidebar.opened() && !!params.dir,
|
||||
"duration-180 ease-out": !layout.sidebar.opened(),
|
||||
"duration-180 ease-in": layout.sidebar.opened(),
|
||||
}}
|
||||
>
|
||||
<Show when={hasProjects() && nav()}>
|
||||
<div class="flex items-center gap-0 transition-transform">
|
||||
<Tooltip placement="bottom" value={language.t("common.goBack")} openDelay={2000}>
|
||||
<Button
|
||||
variant="ghost"
|
||||
icon="chevron-left"
|
||||
class="titlebar-icon w-6 h-6 p-0 box-border"
|
||||
disabled={!canBack()}
|
||||
onClick={back}
|
||||
aria-label={language.t("common.goBack")}
|
||||
/>
|
||||
</Tooltip>
|
||||
<Tooltip placement="bottom" value={language.t("common.goForward")} openDelay={2000}>
|
||||
<Button
|
||||
variant="ghost"
|
||||
icon="chevron-right"
|
||||
class="titlebar-icon w-6 h-6 p-0 box-border"
|
||||
disabled={!canForward()}
|
||||
onClick={forward}
|
||||
aria-label={language.t("common.goForward")}
|
||||
/>
|
||||
</Tooltip>
|
||||
</div>
|
||||
</Show>
|
||||
<div id="opencode-titlebar-left" class="flex items-center gap-3 min-w-0 px-2" />
|
||||
{["beta", "dev"].includes(import.meta.env.VITE_OPENCODE_CHANNEL) && (
|
||||
<div class="bg-icon-interactive-base text-[#FFF] font-medium px-2 rounded-sm uppercase font-mono">
|
||||
{import.meta.env.VITE_OPENCODE_CHANNEL.toUpperCase()}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Match>
|
||||
</Switch>
|
||||
</div>
|
||||
|
||||
<div class="min-w-0 flex items-center justify-center pointer-events-none">
|
||||
<div id="opencode-titlebar-center" class="pointer-events-auto min-w-0 flex justify-center w-fit max-w-full" />
|
||||
</div>
|
||||
|
||||
<div
|
||||
classList={{
|
||||
"flex items-center min-w-0 justify-end": true,
|
||||
"pr-2": !windows(),
|
||||
}}
|
||||
data-tauri-drag-region
|
||||
onMouseDown={drag}
|
||||
>
|
||||
<div id="opencode-titlebar-right" class="flex items-center gap-1 shrink-0 justify-end" />
|
||||
<Show when={windows()}>
|
||||
{!tauriApi() && <div class="shrink-0" style={{ width: windowsControlsWidth() }} />}
|
||||
<div data-tauri-decorum-tb class="flex flex-row" />
|
||||
</Show>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
)
|
||||
}
|
||||
|
||||
function TabNavItem(props: { href: string; title: string; hideClose?: boolean; onClose: () => void }) {
|
||||
const match = useMatch(() => props.href)
|
||||
const isActive = () => !!match()
|
||||
return (
|
||||
<div
|
||||
class="group flex flex-row items-center max-w-60 whitespace-nowrap [--tab-bg:var(--v2-background-bg-deep)] data-[active='true']:[--tab-bg:var(--v2-background-bg-layer-02)] hover:[--tab-bg:var(--v2-background-bg-layer-02)] bg-[var(--tab-bg)] h-7 rounded-[6px] relative overflow-hidden"
|
||||
data-active={isActive()}
|
||||
>
|
||||
<a
|
||||
href={props.href}
|
||||
class="w-full h-full pl-1.5 flex-1 max-w-full flex flex-row items-center overflow-hidden font-medium"
|
||||
>
|
||||
{props.title}
|
||||
</a>
|
||||
|
||||
<div class="absolute right-0 inset-y-0 flex flex-row items-center pr-1 py-1 w-8 pl-2">
|
||||
<div
|
||||
class="absolute inset-0 bg-(image:--inactive-bg) group-hover:bg-(image:--active-bg) group-data-[active=true]:bg-(image:--active-bg)"
|
||||
style={{
|
||||
"--inactive-bg": "linear-gradient(to right, transparent 0%, var(--tab-bg) 80%)",
|
||||
"--active-bg": "linear-gradient(90deg, transparent 0%, var(--tab-bg) 25%)",
|
||||
}}
|
||||
/>
|
||||
<IconButtonV2
|
||||
size="small"
|
||||
variant="ghost-muted"
|
||||
class="opacity-0 group-hover:opacity-100 group-data-[active='true']:opacity-100"
|
||||
onClick={props.onClose}
|
||||
icon={
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="16"
|
||||
height="16"
|
||||
viewBox="0 0 16 16"
|
||||
fill="none"
|
||||
class="size-4"
|
||||
>
|
||||
<path d="M4.25 11.75L11.75 4.25M11.75 11.75L4.25 4.25" stroke="currentColor" />
|
||||
</svg>
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
function ChannelIndicator() {
|
||||
return (
|
||||
<>
|
||||
{["beta", "dev"].includes(import.meta.env.VITE_OPENCODE_CHANNEL) && (
|
||||
<div class="bg-icon-interactive-base text-[#FFF] font-medium px-2 rounded-sm uppercase font-mono">
|
||||
{import.meta.env.VITE_OPENCODE_CHANNEL.toUpperCase()}
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,111 +0,0 @@
|
||||
import { Show, type JSX } from "solid-js"
|
||||
import { DropdownMenu } from "@opencode-ai/ui/dropdown-menu"
|
||||
import { Icon } from "@opencode-ai/ui/icon"
|
||||
import { IconButton } from "@opencode-ai/ui/icon-button"
|
||||
|
||||
import { useCommand } from "@/context/command"
|
||||
import { DESKTOP_MENU, desktopMenuVisible, type DesktopMenuAction, type DesktopMenuEntry } from "@/desktop-menu"
|
||||
import { usePlatform } from "@/context/platform"
|
||||
|
||||
export function WindowsAppMenu(props: {
|
||||
command: ReturnType<typeof useCommand>
|
||||
platform: ReturnType<typeof usePlatform>
|
||||
}) {
|
||||
let lastFocused: HTMLElement | undefined
|
||||
|
||||
const rememberFocus = () => {
|
||||
const active = document.activeElement
|
||||
lastFocused = active instanceof HTMLElement ? active : undefined
|
||||
}
|
||||
const commandDisabled = (id: string) => {
|
||||
const option = props.command.options.find((option) => option.id === id)
|
||||
if (!option) return true
|
||||
return option.disabled ?? false
|
||||
}
|
||||
const runCommand = (id: string) => {
|
||||
if (commandDisabled(id)) return
|
||||
props.command.trigger(id)
|
||||
}
|
||||
const runAction = (action: DesktopMenuAction) => {
|
||||
if (action.startsWith("edit.") && lastFocused?.isConnected) lastFocused.focus({ preventScroll: true })
|
||||
void props.platform.runDesktopMenuAction?.(action)
|
||||
}
|
||||
const runEntry = (entry: DesktopMenuEntry) => {
|
||||
if (entry.type === "separator") return
|
||||
if (entry.command) {
|
||||
runCommand(entry.command)
|
||||
return
|
||||
}
|
||||
if (entry.action) {
|
||||
runAction(entry.action)
|
||||
return
|
||||
}
|
||||
if (entry.href) props.platform.openLink(entry.href)
|
||||
}
|
||||
|
||||
return (
|
||||
<DropdownMenu gutter={4} modal={false} placement="bottom-start">
|
||||
<DropdownMenu.Trigger
|
||||
as={IconButton}
|
||||
icon="menu"
|
||||
variant="ghost"
|
||||
class="titlebar-icon rounded-md shrink-0"
|
||||
aria-label="OpenCode menu"
|
||||
onPointerDown={rememberFocus}
|
||||
onKeyDown={rememberFocus}
|
||||
/>
|
||||
<DropdownMenu.Portal>
|
||||
<DropdownMenu.Content class="desktop-app-menu">
|
||||
<DropdownMenu.Group>
|
||||
<DropdownMenu.GroupLabel class="desktop-app-menu-heading">OpenCode</DropdownMenu.GroupLabel>
|
||||
{DESKTOP_MENU.filter((menu) => desktopMenuVisible(menu, "windows")).map((menu) => (
|
||||
<DesktopMenuSubmenu label={menu.label}>
|
||||
{menu.items
|
||||
?.filter((entry) => desktopMenuVisible(entry, "windows"))
|
||||
.map((entry) =>
|
||||
entry.type === "separator" ? (
|
||||
<DropdownMenu.Separator />
|
||||
) : (
|
||||
<DesktopMenuItem
|
||||
label={entry.label ?? ""}
|
||||
keybind={entry.command ? props.command.keybind(entry.command) : entry.accelerator?.windows}
|
||||
disabled={entry.command ? commandDisabled(entry.command) : false}
|
||||
onSelect={() => runEntry(entry)}
|
||||
/>
|
||||
),
|
||||
)}
|
||||
</DesktopMenuSubmenu>
|
||||
))}
|
||||
</DropdownMenu.Group>
|
||||
</DropdownMenu.Content>
|
||||
</DropdownMenu.Portal>
|
||||
</DropdownMenu>
|
||||
)
|
||||
}
|
||||
|
||||
function DesktopMenuSubmenu(props: { label: string; children: JSX.Element }) {
|
||||
return (
|
||||
<DropdownMenu.Sub>
|
||||
<DropdownMenu.SubTrigger>
|
||||
<span data-slot="dropdown-menu-item-label">{props.label}</span>
|
||||
<span data-slot="desktop-app-menu-chevron">
|
||||
<Icon name="chevron-right" size="small" />
|
||||
</span>
|
||||
</DropdownMenu.SubTrigger>
|
||||
<DropdownMenu.Portal>
|
||||
<DropdownMenu.SubContent class="desktop-app-menu">{props.children}</DropdownMenu.SubContent>
|
||||
</DropdownMenu.Portal>
|
||||
</DropdownMenu.Sub>
|
||||
)
|
||||
}
|
||||
|
||||
function DesktopMenuItem(props: { label: string; keybind?: string; disabled?: boolean; onSelect: () => void }) {
|
||||
return (
|
||||
<DropdownMenu.Item disabled={props.disabled} onSelect={props.onSelect}>
|
||||
<DropdownMenu.ItemLabel>{props.label}</DropdownMenu.ItemLabel>
|
||||
<Show when={props.keybind}>
|
||||
<span data-slot="desktop-app-menu-keybind">{props.keybind}</span>
|
||||
</Show>
|
||||
</DropdownMenu.Item>
|
||||
)
|
||||
}
|
||||
@@ -1,596 +0,0 @@
|
||||
import { batch, createMemo } from "solid-js"
|
||||
import { createStore, produce, reconcile } from "solid-js/store"
|
||||
import { Binary } from "@opencode-ai/core/util/binary"
|
||||
import { retry } from "@opencode-ai/core/util/retry"
|
||||
import {
|
||||
clearSessionPrefetch,
|
||||
getSessionPrefetch,
|
||||
getSessionPrefetchPromise,
|
||||
setSessionPrefetch,
|
||||
} from "./global-sync/session-prefetch"
|
||||
import { useGlobalSync } from "./global-sync"
|
||||
import type { Message, OpencodeClient, Part } from "@opencode-ai/sdk/v2/client"
|
||||
import { SESSION_CACHE_LIMIT, dropSessionCaches, pickSessionCacheEvictions } from "./global-sync/session-cache"
|
||||
import { diffs as list, message as clean } from "@/utils/diffs"
|
||||
|
||||
const SKIP_PARTS = new Set(["patch", "step-start", "step-finish"])
|
||||
|
||||
function sortParts(parts: Part[]) {
|
||||
return parts.filter((part) => !!part?.id).sort((a, b) => cmp(a.id, b.id))
|
||||
}
|
||||
|
||||
function runInflight(map: Map<string, Promise<void>>, key: string, task: () => Promise<void>) {
|
||||
const pending = map.get(key)
|
||||
if (pending) return pending
|
||||
const promise = task().finally(() => {
|
||||
map.delete(key)
|
||||
})
|
||||
map.set(key, promise)
|
||||
return promise
|
||||
}
|
||||
|
||||
const keyFor = (directory: string, id: string) => `${directory}\n${id}`
|
||||
|
||||
const cmp = (a: string, b: string) => (a < b ? -1 : a > b ? 1 : 0)
|
||||
|
||||
function merge<T extends { id: string }>(a: readonly T[], b: readonly T[]) {
|
||||
const map = new Map(a.map((item) => [item.id, item] as const))
|
||||
for (const item of b) map.set(item.id, item)
|
||||
return [...map.values()].sort((x, y) => cmp(x.id, y.id))
|
||||
}
|
||||
|
||||
type OptimisticStore = {
|
||||
message: Record<string, Message[] | undefined>
|
||||
part: Record<string, Part[] | undefined>
|
||||
}
|
||||
|
||||
type OptimisticAddInput = {
|
||||
sessionID: string
|
||||
message: Message
|
||||
parts: Part[]
|
||||
}
|
||||
|
||||
type OptimisticRemoveInput = {
|
||||
sessionID: string
|
||||
messageID: string
|
||||
}
|
||||
|
||||
type OptimisticItem = {
|
||||
message: Message
|
||||
parts: Part[]
|
||||
}
|
||||
|
||||
type MessagePage = {
|
||||
session: Message[]
|
||||
part: { id: string; part: Part[] }[]
|
||||
cursor?: string
|
||||
complete: boolean
|
||||
}
|
||||
|
||||
const hasParts = (parts: Part[] | undefined, want: Part[]) => {
|
||||
if (!parts) return want.length === 0
|
||||
return want.every((part) => Binary.search(parts, part.id, (item) => item.id).found)
|
||||
}
|
||||
|
||||
const mergeParts = (parts: Part[] | undefined, want: Part[]) => {
|
||||
if (!parts) return sortParts(want)
|
||||
const next = [...parts]
|
||||
let changed = false
|
||||
for (const part of want) {
|
||||
const result = Binary.search(next, part.id, (item) => item.id)
|
||||
if (result.found) continue
|
||||
next.splice(result.index, 0, part)
|
||||
changed = true
|
||||
}
|
||||
if (!changed) return parts
|
||||
return next
|
||||
}
|
||||
|
||||
export function mergeOptimisticPage(page: MessagePage, items: OptimisticItem[]) {
|
||||
if (items.length === 0) return { ...page, confirmed: [] as string[] }
|
||||
|
||||
const session = [...page.session]
|
||||
const part = new Map(page.part.map((item) => [item.id, sortParts(item.part)]))
|
||||
const confirmed: string[] = []
|
||||
|
||||
for (const item of items) {
|
||||
const result = Binary.search(session, item.message.id, (message) => message.id)
|
||||
const found = result.found
|
||||
if (!found) session.splice(result.index, 0, item.message)
|
||||
|
||||
const current = part.get(item.message.id)
|
||||
if (found && hasParts(current, item.parts)) {
|
||||
confirmed.push(item.message.id)
|
||||
continue
|
||||
}
|
||||
|
||||
part.set(item.message.id, mergeParts(current, item.parts))
|
||||
}
|
||||
|
||||
return {
|
||||
cursor: page.cursor,
|
||||
complete: page.complete,
|
||||
session,
|
||||
part: [...part.entries()].sort((a, b) => cmp(a[0], b[0])).map(([id, part]) => ({ id, part })),
|
||||
confirmed,
|
||||
}
|
||||
}
|
||||
|
||||
export function applyOptimisticAdd(draft: OptimisticStore, input: OptimisticAddInput) {
|
||||
const messages = draft.message[input.sessionID]
|
||||
if (messages) {
|
||||
const result = Binary.search(messages, input.message.id, (m) => m.id)
|
||||
messages.splice(result.index, 0, input.message)
|
||||
} else {
|
||||
draft.message[input.sessionID] = [input.message]
|
||||
}
|
||||
draft.part[input.message.id] = sortParts(input.parts)
|
||||
}
|
||||
|
||||
export function applyOptimisticRemove(draft: OptimisticStore, input: OptimisticRemoveInput) {
|
||||
const messages = draft.message[input.sessionID]
|
||||
if (messages) {
|
||||
const result = Binary.search(messages, input.messageID, (m) => m.id)
|
||||
if (result.found) messages.splice(result.index, 1)
|
||||
}
|
||||
delete draft.part[input.messageID]
|
||||
}
|
||||
|
||||
function setOptimisticAdd(setStore: (...args: unknown[]) => void, input: OptimisticAddInput) {
|
||||
setStore("message", input.sessionID, (messages: Message[] | undefined) => {
|
||||
if (!messages) return [input.message]
|
||||
const result = Binary.search(messages, input.message.id, (m) => m.id)
|
||||
const next = [...messages]
|
||||
next.splice(result.index, 0, input.message)
|
||||
return next
|
||||
})
|
||||
setStore("part", input.message.id, sortParts(input.parts))
|
||||
}
|
||||
|
||||
function setOptimisticRemove(setStore: (...args: unknown[]) => void, input: OptimisticRemoveInput) {
|
||||
setStore("message", input.sessionID, (messages: Message[] | undefined) => {
|
||||
if (!messages) return messages
|
||||
const result = Binary.search(messages, input.messageID, (m) => m.id)
|
||||
if (!result.found) return messages
|
||||
const next = [...messages]
|
||||
next.splice(result.index, 1)
|
||||
return next
|
||||
})
|
||||
setStore("part", (part: Record<string, Part[] | undefined>) => {
|
||||
if (!(input.messageID in part)) return part
|
||||
const next = { ...part }
|
||||
delete next[input.messageID]
|
||||
return next
|
||||
})
|
||||
}
|
||||
|
||||
export const createDirSyncContext = (client: OpencodeClient, directory: string) => {
|
||||
const globalSync = useGlobalSync()
|
||||
|
||||
type Child = ReturnType<(typeof globalSync)["child"]>
|
||||
type Setter = Child[1]
|
||||
|
||||
const current = createMemo(() => globalSync.child(directory))
|
||||
const target = (directory?: string) => {
|
||||
if (!directory || directory === directory) return current()
|
||||
return globalSync.child(directory)
|
||||
}
|
||||
const absolute = (path: string) => (current()[0].path.directory + "/" + path).replace("//", "/")
|
||||
const initialMessagePageSize = 80
|
||||
const historyMessagePageSize = 200
|
||||
const inflight = new Map<string, Promise<void>>()
|
||||
const inflightDiff = new Map<string, Promise<void>>()
|
||||
const inflightTodo = new Map<string, Promise<void>>()
|
||||
const optimistic = new Map<string, Map<string, OptimisticItem>>()
|
||||
const maxDirs = 30
|
||||
const seen = new Map<string, Set<string>>()
|
||||
const [meta, setMeta] = createStore({
|
||||
limit: {} as Record<string, number>,
|
||||
cursor: {} as Record<string, string | undefined>,
|
||||
complete: {} as Record<string, boolean>,
|
||||
loading: {} as Record<string, boolean>,
|
||||
})
|
||||
|
||||
const getSession = (sessionID: string) => {
|
||||
const store = current()[0]
|
||||
const match = Binary.search(store.session, sessionID, (s) => s.id)
|
||||
if (match.found) return store.session[match.index]
|
||||
return undefined
|
||||
}
|
||||
|
||||
const setOptimistic = (directory: string, sessionID: string, item: OptimisticItem) => {
|
||||
const key = keyFor(directory, sessionID)
|
||||
const list = optimistic.get(key)
|
||||
if (list) {
|
||||
list.set(item.message.id, { message: item.message, parts: sortParts(item.parts) })
|
||||
return
|
||||
}
|
||||
optimistic.set(key, new Map([[item.message.id, { message: item.message, parts: sortParts(item.parts) }]]))
|
||||
}
|
||||
|
||||
const clearOptimistic = (directory: string, sessionID: string, messageID?: string) => {
|
||||
const key = keyFor(directory, sessionID)
|
||||
if (!messageID) {
|
||||
optimistic.delete(key)
|
||||
return
|
||||
}
|
||||
|
||||
const list = optimistic.get(key)
|
||||
if (!list) return
|
||||
list.delete(messageID)
|
||||
if (list.size === 0) optimistic.delete(key)
|
||||
}
|
||||
|
||||
const getOptimistic = (directory: string, sessionID: string) => [
|
||||
...(optimistic.get(keyFor(directory, sessionID))?.values() ?? []),
|
||||
]
|
||||
|
||||
const seenFor = (directory: string) => {
|
||||
const existing = seen.get(directory)
|
||||
if (existing) {
|
||||
seen.delete(directory)
|
||||
seen.set(directory, existing)
|
||||
return existing
|
||||
}
|
||||
const created = new Set<string>()
|
||||
seen.set(directory, created)
|
||||
while (seen.size > maxDirs) {
|
||||
const first = seen.keys().next().value
|
||||
if (!first) break
|
||||
const stale = [...(seen.get(first) ?? [])]
|
||||
seen.delete(first)
|
||||
const [, setStore] = globalSync.child(first, { bootstrap: false })
|
||||
evict(first, setStore, stale)
|
||||
}
|
||||
return created
|
||||
}
|
||||
|
||||
const clearMeta = (directory: string, sessionIDs: string[]) => {
|
||||
if (sessionIDs.length === 0) return
|
||||
for (const sessionID of sessionIDs) {
|
||||
clearOptimistic(directory, sessionID)
|
||||
}
|
||||
setMeta(
|
||||
produce((draft) => {
|
||||
for (const sessionID of sessionIDs) {
|
||||
const key = keyFor(directory, sessionID)
|
||||
delete draft.limit[key]
|
||||
delete draft.cursor[key]
|
||||
delete draft.complete[key]
|
||||
delete draft.loading[key]
|
||||
}
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
const evict = (directory: string, setStore: Setter, sessionIDs: string[]) => {
|
||||
if (sessionIDs.length === 0) return
|
||||
clearSessionPrefetch(directory, sessionIDs)
|
||||
for (const sessionID of sessionIDs) {
|
||||
globalSync.todo.set(sessionID, undefined)
|
||||
}
|
||||
setStore(
|
||||
produce((draft) => {
|
||||
dropSessionCaches(draft, sessionIDs)
|
||||
}),
|
||||
)
|
||||
clearMeta(directory, sessionIDs)
|
||||
}
|
||||
|
||||
const touch = (directory: string, setStore: Setter, sessionID: string) => {
|
||||
const stale = pickSessionCacheEvictions({
|
||||
seen: seenFor(directory),
|
||||
keep: sessionID,
|
||||
limit: SESSION_CACHE_LIMIT,
|
||||
})
|
||||
evict(directory, setStore, stale)
|
||||
}
|
||||
|
||||
const fetchMessages = async (input: { client: typeof client; sessionID: string; limit: number; before?: string }) => {
|
||||
const messages = await retry(() =>
|
||||
input.client.session.messages({ sessionID: input.sessionID, limit: input.limit, before: input.before }),
|
||||
)
|
||||
const items = (messages.data ?? []).filter((x) => !!x?.info?.id)
|
||||
const session = items.map((x) => clean(x.info)).sort((a, b) => cmp(a.id, b.id))
|
||||
const part = items.map((message) => ({ id: message.info.id, part: sortParts(message.parts) }))
|
||||
const cursor = messages.response.headers.get("x-next-cursor") ?? undefined
|
||||
return {
|
||||
session,
|
||||
part,
|
||||
cursor,
|
||||
complete: !cursor,
|
||||
}
|
||||
}
|
||||
|
||||
const tracked = (directory: string, sessionID: string) => seen.get(directory)?.has(sessionID) ?? false
|
||||
|
||||
const loadMessages = async (input: {
|
||||
directory: string
|
||||
client: typeof client
|
||||
setStore: Setter
|
||||
sessionID: string
|
||||
limit: number
|
||||
before?: string
|
||||
mode?: "replace" | "prepend"
|
||||
}) => {
|
||||
const key = keyFor(input.directory, input.sessionID)
|
||||
if (meta.loading[key]) return
|
||||
|
||||
setMeta("loading", key, true)
|
||||
await fetchMessages(input)
|
||||
.then((page) => {
|
||||
if (!tracked(input.directory, input.sessionID)) return
|
||||
const next = mergeOptimisticPage(page, getOptimistic(input.directory, input.sessionID))
|
||||
for (const messageID of next.confirmed) {
|
||||
clearOptimistic(input.directory, input.sessionID, messageID)
|
||||
}
|
||||
const [store] = globalSync.child(input.directory, { bootstrap: false })
|
||||
const cached = input.mode === "prepend" ? (store.message[input.sessionID] ?? []) : []
|
||||
const message = input.mode === "prepend" ? merge(cached, next.session) : next.session
|
||||
batch(() => {
|
||||
input.setStore("message", input.sessionID, reconcile(message, { key: "id" }))
|
||||
for (const p of next.part) {
|
||||
const filtered = p.part.filter((x) => !SKIP_PARTS.has(x.type))
|
||||
if (filtered.length) input.setStore("part", p.id, filtered)
|
||||
}
|
||||
setMeta("limit", key, message.length)
|
||||
setMeta("cursor", key, next.cursor)
|
||||
setMeta("complete", key, next.complete)
|
||||
setSessionPrefetch({
|
||||
directory: input.directory,
|
||||
sessionID: input.sessionID,
|
||||
limit: message.length,
|
||||
cursor: next.cursor,
|
||||
complete: next.complete,
|
||||
})
|
||||
})
|
||||
})
|
||||
.finally(() => {
|
||||
setMeta(
|
||||
produce((draft) => {
|
||||
if (!tracked(input.directory, input.sessionID)) {
|
||||
delete draft.loading[key]
|
||||
return
|
||||
}
|
||||
draft.loading[key] = false
|
||||
}),
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
return {
|
||||
get data() {
|
||||
return current()[0]
|
||||
},
|
||||
get set(): Setter {
|
||||
return current()[1]
|
||||
},
|
||||
get status() {
|
||||
return current()[0].status
|
||||
},
|
||||
get ready() {
|
||||
return current()[0].status !== "loading"
|
||||
},
|
||||
get project() {
|
||||
const store = current()[0]
|
||||
const match = Binary.search(globalSync.data.project, store.project, (p) => p.id)
|
||||
if (match.found) return globalSync.data.project[match.index]
|
||||
return undefined
|
||||
},
|
||||
session: {
|
||||
get: getSession,
|
||||
optimistic: {
|
||||
add(input: { directory?: string; sessionID: string; message: Message; parts: Part[] }) {
|
||||
const _directory = input.directory ?? directory
|
||||
const [, setStore] = target(input.directory)
|
||||
setOptimistic(_directory, input.sessionID, { message: input.message, parts: input.parts })
|
||||
setOptimisticAdd(setStore as (...args: unknown[]) => void, input)
|
||||
},
|
||||
remove(input: { directory?: string; sessionID: string; messageID: string }) {
|
||||
const _directory = input.directory ?? directory
|
||||
const [, setStore] = target(input.directory)
|
||||
clearOptimistic(_directory, input.sessionID, input.messageID)
|
||||
setOptimisticRemove(setStore as (...args: unknown[]) => void, input)
|
||||
},
|
||||
},
|
||||
addOptimisticMessage(input: {
|
||||
sessionID: string
|
||||
messageID: string
|
||||
parts: Part[]
|
||||
agent: string
|
||||
model: { providerID: string; modelID: string }
|
||||
variant?: string
|
||||
}) {
|
||||
const message: Message = {
|
||||
id: input.messageID,
|
||||
sessionID: input.sessionID,
|
||||
role: "user",
|
||||
time: { created: Date.now() },
|
||||
agent: input.agent,
|
||||
model: { ...input.model, variant: input.variant },
|
||||
}
|
||||
const [, setStore] = target()
|
||||
setOptimistic(directory, input.sessionID, { message, parts: input.parts })
|
||||
setOptimisticAdd(setStore as (...args: unknown[]) => void, {
|
||||
sessionID: input.sessionID,
|
||||
message,
|
||||
parts: input.parts,
|
||||
})
|
||||
},
|
||||
async sync(sessionID: string, opts?: { force?: boolean }) {
|
||||
const [store, setStore] = globalSync.child(directory)
|
||||
const key = keyFor(directory, sessionID)
|
||||
|
||||
touch(directory, setStore, sessionID)
|
||||
|
||||
const seeded = getSessionPrefetch(directory, sessionID)
|
||||
if (seeded && store.message[sessionID] !== undefined && meta.limit[key] === undefined) {
|
||||
batch(() => {
|
||||
setMeta("limit", key, seeded.limit)
|
||||
setMeta("cursor", key, seeded.cursor)
|
||||
setMeta("complete", key, seeded.complete)
|
||||
setMeta("loading", key, false)
|
||||
})
|
||||
}
|
||||
|
||||
return runInflight(inflight, key, async () => {
|
||||
const pending = getSessionPrefetchPromise(directory, sessionID)
|
||||
if (pending) {
|
||||
await pending
|
||||
const seeded = getSessionPrefetch(directory, sessionID)
|
||||
if (seeded && store.message[sessionID] !== undefined && meta.limit[key] === undefined) {
|
||||
batch(() => {
|
||||
setMeta("limit", key, seeded.limit)
|
||||
setMeta("cursor", key, seeded.cursor)
|
||||
setMeta("complete", key, seeded.complete)
|
||||
setMeta("loading", key, false)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const hasSession = Binary.search(store.session, sessionID, (s) => s.id).found
|
||||
const cached = store.message[sessionID] !== undefined && meta.limit[key] !== undefined
|
||||
if (cached && hasSession && !opts?.force) return
|
||||
|
||||
const limit = meta.limit[key] ?? initialMessagePageSize
|
||||
const sessionReq =
|
||||
hasSession && !opts?.force
|
||||
? Promise.resolve()
|
||||
: retry(() => client.session.get({ sessionID })).then((session) => {
|
||||
if (!tracked(directory, sessionID)) return
|
||||
const data = session.data
|
||||
if (!data) return
|
||||
setStore(
|
||||
"session",
|
||||
produce((draft) => {
|
||||
const match = Binary.search(draft, sessionID, (s) => s.id)
|
||||
if (match.found) {
|
||||
draft[match.index] = data
|
||||
return
|
||||
}
|
||||
draft.splice(match.index, 0, data)
|
||||
}),
|
||||
)
|
||||
})
|
||||
|
||||
const messagesReq =
|
||||
cached && !opts?.force
|
||||
? Promise.resolve()
|
||||
: loadMessages({
|
||||
directory,
|
||||
client,
|
||||
setStore,
|
||||
sessionID,
|
||||
limit,
|
||||
})
|
||||
|
||||
await Promise.all([sessionReq, messagesReq])
|
||||
})
|
||||
},
|
||||
async diff(sessionID: string, opts?: { force?: boolean }) {
|
||||
const [store, setStore] = globalSync.child(directory)
|
||||
touch(directory, setStore, sessionID)
|
||||
if (store.session_diff[sessionID] !== undefined && !opts?.force) return
|
||||
|
||||
const key = keyFor(directory, sessionID)
|
||||
return runInflight(inflightDiff, key, () =>
|
||||
retry(() => client.session.diff({ sessionID })).then((diff) => {
|
||||
if (!tracked(directory, sessionID)) return
|
||||
setStore("session_diff", sessionID, reconcile(list(diff.data), { key: "file" }))
|
||||
}),
|
||||
)
|
||||
},
|
||||
async todo(sessionID: string, opts?: { force?: boolean }) {
|
||||
const [store, setStore] = globalSync.child(directory)
|
||||
touch(directory, setStore, sessionID)
|
||||
const existing = store.todo[sessionID]
|
||||
const cached = globalSync.data.session_todo[sessionID]
|
||||
if (existing !== undefined) {
|
||||
if (cached === undefined) {
|
||||
globalSync.todo.set(sessionID, existing)
|
||||
}
|
||||
if (!opts?.force) return
|
||||
}
|
||||
|
||||
if (cached !== undefined) {
|
||||
setStore("todo", sessionID, reconcile(cached, { key: "id" }))
|
||||
}
|
||||
|
||||
const key = keyFor(directory, sessionID)
|
||||
return runInflight(inflightTodo, key, () =>
|
||||
retry(() => client.session.todo({ sessionID })).then((todo) => {
|
||||
if (!tracked(directory, sessionID)) return
|
||||
const list = todo.data ?? []
|
||||
setStore("todo", sessionID, reconcile(list, { key: "id" }))
|
||||
globalSync.todo.set(sessionID, list)
|
||||
}),
|
||||
)
|
||||
},
|
||||
history: {
|
||||
more(sessionID: string) {
|
||||
const store = current()[0]
|
||||
const key = keyFor(directory, sessionID)
|
||||
if (store.message[sessionID] === undefined) return false
|
||||
if (meta.limit[key] === undefined) return false
|
||||
if (meta.complete[key]) return false
|
||||
return !!meta.cursor[key]
|
||||
},
|
||||
loading(sessionID: string) {
|
||||
const key = keyFor(directory, sessionID)
|
||||
return meta.loading[key] ?? false
|
||||
},
|
||||
async loadMore(sessionID: string, count?: number) {
|
||||
const [, setStore] = globalSync.child(directory)
|
||||
touch(directory, setStore, sessionID)
|
||||
const key = keyFor(directory, sessionID)
|
||||
const step = count ?? historyMessagePageSize
|
||||
if (meta.loading[key]) return
|
||||
if (meta.complete[key]) return
|
||||
const before = meta.cursor[key]
|
||||
if (!before) return
|
||||
|
||||
await loadMessages({
|
||||
directory,
|
||||
client,
|
||||
setStore,
|
||||
sessionID,
|
||||
limit: step,
|
||||
before,
|
||||
mode: "prepend",
|
||||
})
|
||||
},
|
||||
},
|
||||
evict(sessionID: string, _directory = directory) {
|
||||
const [, setStore] = globalSync.child(_directory)
|
||||
seenFor(_directory).delete(sessionID)
|
||||
evict(_directory, setStore, [sessionID])
|
||||
},
|
||||
fetch: async (count = 10) => {
|
||||
const [store, setStore] = globalSync.child(directory)
|
||||
setStore("limit", (x) => x + count)
|
||||
await client.session.list().then((x) => {
|
||||
const sessions = (x.data ?? [])
|
||||
.filter((s) => !!s?.id)
|
||||
.sort((a, b) => cmp(a.id, b.id))
|
||||
.slice(0, store.limit)
|
||||
setStore("session", reconcile(sessions, { key: "id" }))
|
||||
})
|
||||
},
|
||||
more: createMemo(() => current()[0].session.length >= current()[0].limit),
|
||||
archive: async (sessionID: string) => {
|
||||
const [, setStore] = globalSync.child(directory)
|
||||
await client.session.update({ sessionID, time: { archived: Date.now() } })
|
||||
setStore(
|
||||
produce((draft) => {
|
||||
const match = Binary.search(draft.session, sessionID, (s) => s.id)
|
||||
if (match.found) draft.session.splice(match.index, 1)
|
||||
}),
|
||||
)
|
||||
},
|
||||
},
|
||||
absolute,
|
||||
get directory() {
|
||||
return current()[0].path.directory
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -36,7 +36,6 @@ import { queryOptions, useMutation, useQueries, useQuery, useQueryClient } from
|
||||
import { createRefreshQueue } from "./global-sync/queue"
|
||||
import { directoryKey } from "./global-sync/utils"
|
||||
import { PathKey } from "@/utils/path-key"
|
||||
import { createDirSyncContext } from "./directory-sync"
|
||||
|
||||
type GlobalStore = {
|
||||
ready: boolean
|
||||
@@ -432,9 +431,6 @@ function createGlobalSync() {
|
||||
},
|
||||
}))
|
||||
|
||||
const dirSyncContexts = new Map<string, ReturnType<typeof createDirSyncContext>>()
|
||||
const dirSyncContextRefCounts = new Map<string, number>()
|
||||
|
||||
return {
|
||||
data: globalStore,
|
||||
set,
|
||||
@@ -453,26 +449,6 @@ function createGlobalSync() {
|
||||
todo: {
|
||||
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
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { createSimpleContext } from "@opencode-ai/ui/context"
|
||||
import type { AsyncStorage, SyncStorage } from "@solid-primitives/storage"
|
||||
import type { Accessor } from "solid-js"
|
||||
import type { DesktopMenuAction } from "../desktop-menu"
|
||||
import { ServerConnection } from "./server"
|
||||
|
||||
type PickerPaths = string | string[] | null
|
||||
@@ -83,9 +82,6 @@ export type Platform = {
|
||||
/** Webview zoom level (desktop only) */
|
||||
webviewZoom?: Accessor<number>
|
||||
|
||||
/** Run a desktop-only menu action from the app chrome */
|
||||
runDesktopMenuAction?(action: DesktopMenuAction): Promise<void> | void
|
||||
|
||||
/** Check if an editor app exists (desktop only) */
|
||||
checkAppExists?(appName: string): Promise<boolean>
|
||||
|
||||
|
||||
@@ -1,8 +1,19 @@
|
||||
import { batch, createMemo } from "solid-js"
|
||||
import { createStore, produce, reconcile } from "solid-js/store"
|
||||
import { Binary } from "@opencode-ai/core/util/binary"
|
||||
import { retry } from "@opencode-ai/core/util/retry"
|
||||
import { createSimpleContext } from "@opencode-ai/ui/context"
|
||||
import {
|
||||
clearSessionPrefetch,
|
||||
getSessionPrefetch,
|
||||
getSessionPrefetchPromise,
|
||||
setSessionPrefetch,
|
||||
} from "./global-sync/session-prefetch"
|
||||
import { useGlobalSync } from "./global-sync"
|
||||
import { useSDK } from "./sdk"
|
||||
import type { Message, Part } from "@opencode-ai/sdk/v2/client"
|
||||
import { SESSION_CACHE_LIMIT, dropSessionCaches, pickSessionCacheEvictions } from "./global-sync/session-cache"
|
||||
import { diffs as list, message as clean } from "@/utils/diffs"
|
||||
|
||||
const SKIP_PARTS = new Set(["patch", "step-start", "step-finish"])
|
||||
|
||||
@@ -161,6 +172,448 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({
|
||||
const globalSync = useGlobalSync()
|
||||
const sdk = useSDK()
|
||||
|
||||
return globalSync.createDirSyncContext(sdk.directory)
|
||||
type Child = ReturnType<(typeof globalSync)["child"]>
|
||||
type Setter = Child[1]
|
||||
|
||||
const current = createMemo(() => globalSync.child(sdk.directory))
|
||||
const target = (directory?: string) => {
|
||||
if (!directory || directory === sdk.directory) return current()
|
||||
return globalSync.child(directory)
|
||||
}
|
||||
const absolute = (path: string) => (current()[0].path.directory + "/" + path).replace("//", "/")
|
||||
const initialMessagePageSize = 80
|
||||
const historyMessagePageSize = 200
|
||||
const inflight = new Map<string, Promise<void>>()
|
||||
const inflightDiff = new Map<string, Promise<void>>()
|
||||
const inflightTodo = new Map<string, Promise<void>>()
|
||||
const optimistic = new Map<string, Map<string, OptimisticItem>>()
|
||||
const maxDirs = 30
|
||||
const seen = new Map<string, Set<string>>()
|
||||
const [meta, setMeta] = createStore({
|
||||
limit: {} as Record<string, number>,
|
||||
cursor: {} as Record<string, string | undefined>,
|
||||
complete: {} as Record<string, boolean>,
|
||||
loading: {} as Record<string, boolean>,
|
||||
})
|
||||
|
||||
const getSession = (sessionID: string) => {
|
||||
const store = current()[0]
|
||||
const match = Binary.search(store.session, sessionID, (s) => s.id)
|
||||
if (match.found) return store.session[match.index]
|
||||
return undefined
|
||||
}
|
||||
|
||||
const setOptimistic = (directory: string, sessionID: string, item: OptimisticItem) => {
|
||||
const key = keyFor(directory, sessionID)
|
||||
const list = optimistic.get(key)
|
||||
if (list) {
|
||||
list.set(item.message.id, { message: item.message, parts: sortParts(item.parts) })
|
||||
return
|
||||
}
|
||||
optimistic.set(key, new Map([[item.message.id, { message: item.message, parts: sortParts(item.parts) }]]))
|
||||
}
|
||||
|
||||
const clearOptimistic = (directory: string, sessionID: string, messageID?: string) => {
|
||||
const key = keyFor(directory, sessionID)
|
||||
if (!messageID) {
|
||||
optimistic.delete(key)
|
||||
return
|
||||
}
|
||||
|
||||
const list = optimistic.get(key)
|
||||
if (!list) return
|
||||
list.delete(messageID)
|
||||
if (list.size === 0) optimistic.delete(key)
|
||||
}
|
||||
|
||||
const getOptimistic = (directory: string, sessionID: string) => [
|
||||
...(optimistic.get(keyFor(directory, sessionID))?.values() ?? []),
|
||||
]
|
||||
|
||||
const seenFor = (directory: string) => {
|
||||
const existing = seen.get(directory)
|
||||
if (existing) {
|
||||
seen.delete(directory)
|
||||
seen.set(directory, existing)
|
||||
return existing
|
||||
}
|
||||
const created = new Set<string>()
|
||||
seen.set(directory, created)
|
||||
while (seen.size > maxDirs) {
|
||||
const first = seen.keys().next().value
|
||||
if (!first) break
|
||||
const stale = [...(seen.get(first) ?? [])]
|
||||
seen.delete(first)
|
||||
const [, setStore] = globalSync.child(first, { bootstrap: false })
|
||||
evict(first, setStore, stale)
|
||||
}
|
||||
return created
|
||||
}
|
||||
|
||||
const clearMeta = (directory: string, sessionIDs: string[]) => {
|
||||
if (sessionIDs.length === 0) return
|
||||
for (const sessionID of sessionIDs) {
|
||||
clearOptimistic(directory, sessionID)
|
||||
}
|
||||
setMeta(
|
||||
produce((draft) => {
|
||||
for (const sessionID of sessionIDs) {
|
||||
const key = keyFor(directory, sessionID)
|
||||
delete draft.limit[key]
|
||||
delete draft.cursor[key]
|
||||
delete draft.complete[key]
|
||||
delete draft.loading[key]
|
||||
}
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
const evict = (directory: string, setStore: Setter, sessionIDs: string[]) => {
|
||||
if (sessionIDs.length === 0) return
|
||||
clearSessionPrefetch(directory, sessionIDs)
|
||||
for (const sessionID of sessionIDs) {
|
||||
globalSync.todo.set(sessionID, undefined)
|
||||
}
|
||||
setStore(
|
||||
produce((draft) => {
|
||||
dropSessionCaches(draft, sessionIDs)
|
||||
}),
|
||||
)
|
||||
clearMeta(directory, sessionIDs)
|
||||
}
|
||||
|
||||
const touch = (directory: string, setStore: Setter, sessionID: string) => {
|
||||
const stale = pickSessionCacheEvictions({
|
||||
seen: seenFor(directory),
|
||||
keep: sessionID,
|
||||
limit: SESSION_CACHE_LIMIT,
|
||||
})
|
||||
evict(directory, setStore, stale)
|
||||
}
|
||||
|
||||
const fetchMessages = async (input: {
|
||||
client: typeof sdk.client
|
||||
sessionID: string
|
||||
limit: number
|
||||
before?: string
|
||||
}) => {
|
||||
const messages = await retry(() =>
|
||||
input.client.session.messages({ sessionID: input.sessionID, limit: input.limit, before: input.before }),
|
||||
)
|
||||
const items = (messages.data ?? []).filter((x) => !!x?.info?.id)
|
||||
const session = items.map((x) => clean(x.info)).sort((a, b) => cmp(a.id, b.id))
|
||||
const part = items.map((message) => ({ id: message.info.id, part: sortParts(message.parts) }))
|
||||
const cursor = messages.response.headers.get("x-next-cursor") ?? undefined
|
||||
return {
|
||||
session,
|
||||
part,
|
||||
cursor,
|
||||
complete: !cursor,
|
||||
}
|
||||
}
|
||||
|
||||
const tracked = (directory: string, sessionID: string) => seen.get(directory)?.has(sessionID) ?? false
|
||||
|
||||
const loadMessages = async (input: {
|
||||
directory: string
|
||||
client: typeof sdk.client
|
||||
setStore: Setter
|
||||
sessionID: string
|
||||
limit: number
|
||||
before?: string
|
||||
mode?: "replace" | "prepend"
|
||||
}) => {
|
||||
const key = keyFor(input.directory, input.sessionID)
|
||||
if (meta.loading[key]) return
|
||||
|
||||
setMeta("loading", key, true)
|
||||
await fetchMessages(input)
|
||||
.then((page) => {
|
||||
if (!tracked(input.directory, input.sessionID)) return
|
||||
const next = mergeOptimisticPage(page, getOptimistic(input.directory, input.sessionID))
|
||||
for (const messageID of next.confirmed) {
|
||||
clearOptimistic(input.directory, input.sessionID, messageID)
|
||||
}
|
||||
const [store] = globalSync.child(input.directory, { bootstrap: false })
|
||||
const cached = input.mode === "prepend" ? (store.message[input.sessionID] ?? []) : []
|
||||
const message = input.mode === "prepend" ? merge(cached, next.session) : next.session
|
||||
batch(() => {
|
||||
input.setStore("message", input.sessionID, reconcile(message, { key: "id" }))
|
||||
for (const p of next.part) {
|
||||
const filtered = p.part.filter((x) => !SKIP_PARTS.has(x.type))
|
||||
if (filtered.length) input.setStore("part", p.id, filtered)
|
||||
}
|
||||
setMeta("limit", key, message.length)
|
||||
setMeta("cursor", key, next.cursor)
|
||||
setMeta("complete", key, next.complete)
|
||||
setSessionPrefetch({
|
||||
directory: input.directory,
|
||||
sessionID: input.sessionID,
|
||||
limit: message.length,
|
||||
cursor: next.cursor,
|
||||
complete: next.complete,
|
||||
})
|
||||
})
|
||||
})
|
||||
.finally(() => {
|
||||
setMeta(
|
||||
produce((draft) => {
|
||||
if (!tracked(input.directory, input.sessionID)) {
|
||||
delete draft.loading[key]
|
||||
return
|
||||
}
|
||||
draft.loading[key] = false
|
||||
}),
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
return {
|
||||
get data() {
|
||||
return current()[0]
|
||||
},
|
||||
get set(): Setter {
|
||||
return current()[1]
|
||||
},
|
||||
get status() {
|
||||
return current()[0].status
|
||||
},
|
||||
get ready() {
|
||||
return current()[0].status !== "loading"
|
||||
},
|
||||
get project() {
|
||||
const store = current()[0]
|
||||
const match = Binary.search(globalSync.data.project, store.project, (p) => p.id)
|
||||
if (match.found) return globalSync.data.project[match.index]
|
||||
return undefined
|
||||
},
|
||||
session: {
|
||||
get: getSession,
|
||||
optimistic: {
|
||||
add(input: { directory?: string; sessionID: string; message: Message; parts: Part[] }) {
|
||||
const directory = input.directory ?? sdk.directory
|
||||
const [, setStore] = target(input.directory)
|
||||
setOptimistic(directory, input.sessionID, { message: input.message, parts: input.parts })
|
||||
setOptimisticAdd(setStore as (...args: unknown[]) => void, input)
|
||||
},
|
||||
remove(input: { directory?: string; sessionID: string; messageID: string }) {
|
||||
const directory = input.directory ?? sdk.directory
|
||||
const [, setStore] = target(input.directory)
|
||||
clearOptimistic(directory, input.sessionID, input.messageID)
|
||||
setOptimisticRemove(setStore as (...args: unknown[]) => void, input)
|
||||
},
|
||||
},
|
||||
addOptimisticMessage(input: {
|
||||
sessionID: string
|
||||
messageID: string
|
||||
parts: Part[]
|
||||
agent: string
|
||||
model: { providerID: string; modelID: string }
|
||||
variant?: string
|
||||
}) {
|
||||
const message: Message = {
|
||||
id: input.messageID,
|
||||
sessionID: input.sessionID,
|
||||
role: "user",
|
||||
time: { created: Date.now() },
|
||||
agent: input.agent,
|
||||
model: { ...input.model, variant: input.variant },
|
||||
}
|
||||
const [, setStore] = target()
|
||||
setOptimistic(sdk.directory, input.sessionID, { message, parts: input.parts })
|
||||
setOptimisticAdd(setStore as (...args: unknown[]) => void, {
|
||||
sessionID: input.sessionID,
|
||||
message,
|
||||
parts: input.parts,
|
||||
})
|
||||
},
|
||||
async sync(sessionID: string, opts?: { force?: boolean }) {
|
||||
const directory = sdk.directory
|
||||
const client = sdk.client
|
||||
const [store, setStore] = globalSync.child(directory)
|
||||
const key = keyFor(directory, sessionID)
|
||||
|
||||
touch(directory, setStore, sessionID)
|
||||
|
||||
const seeded = getSessionPrefetch(directory, sessionID)
|
||||
if (seeded && store.message[sessionID] !== undefined && meta.limit[key] === undefined) {
|
||||
batch(() => {
|
||||
setMeta("limit", key, seeded.limit)
|
||||
setMeta("cursor", key, seeded.cursor)
|
||||
setMeta("complete", key, seeded.complete)
|
||||
setMeta("loading", key, false)
|
||||
})
|
||||
}
|
||||
|
||||
return runInflight(inflight, key, async () => {
|
||||
const pending = getSessionPrefetchPromise(directory, sessionID)
|
||||
if (pending) {
|
||||
await pending
|
||||
const seeded = getSessionPrefetch(directory, sessionID)
|
||||
if (seeded && store.message[sessionID] !== undefined && meta.limit[key] === undefined) {
|
||||
batch(() => {
|
||||
setMeta("limit", key, seeded.limit)
|
||||
setMeta("cursor", key, seeded.cursor)
|
||||
setMeta("complete", key, seeded.complete)
|
||||
setMeta("loading", key, false)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const hasSession = Binary.search(store.session, sessionID, (s) => s.id).found
|
||||
const cached = store.message[sessionID] !== undefined && meta.limit[key] !== undefined
|
||||
if (cached && hasSession && !opts?.force) return
|
||||
|
||||
const limit = meta.limit[key] ?? initialMessagePageSize
|
||||
const sessionReq =
|
||||
hasSession && !opts?.force
|
||||
? Promise.resolve()
|
||||
: retry(() => client.session.get({ sessionID })).then((session) => {
|
||||
if (!tracked(directory, sessionID)) return
|
||||
const data = session.data
|
||||
if (!data) return
|
||||
setStore(
|
||||
"session",
|
||||
produce((draft) => {
|
||||
const match = Binary.search(draft, sessionID, (s) => s.id)
|
||||
if (match.found) {
|
||||
draft[match.index] = data
|
||||
return
|
||||
}
|
||||
draft.splice(match.index, 0, data)
|
||||
}),
|
||||
)
|
||||
})
|
||||
|
||||
const messagesReq =
|
||||
cached && !opts?.force
|
||||
? Promise.resolve()
|
||||
: loadMessages({
|
||||
directory,
|
||||
client,
|
||||
setStore,
|
||||
sessionID,
|
||||
limit,
|
||||
})
|
||||
|
||||
await Promise.all([sessionReq, messagesReq])
|
||||
})
|
||||
},
|
||||
async diff(sessionID: string, opts?: { force?: boolean }) {
|
||||
const directory = sdk.directory
|
||||
const client = sdk.client
|
||||
const [store, setStore] = globalSync.child(directory)
|
||||
touch(directory, setStore, sessionID)
|
||||
if (store.session_diff[sessionID] !== undefined && !opts?.force) return
|
||||
|
||||
const key = keyFor(directory, sessionID)
|
||||
return runInflight(inflightDiff, key, () =>
|
||||
retry(() => client.session.diff({ sessionID })).then((diff) => {
|
||||
if (!tracked(directory, sessionID)) return
|
||||
setStore("session_diff", sessionID, reconcile(list(diff.data), { key: "file" }))
|
||||
}),
|
||||
)
|
||||
},
|
||||
async todo(sessionID: string, opts?: { force?: boolean }) {
|
||||
const directory = sdk.directory
|
||||
const client = sdk.client
|
||||
const [store, setStore] = globalSync.child(directory)
|
||||
touch(directory, setStore, sessionID)
|
||||
const existing = store.todo[sessionID]
|
||||
const cached = globalSync.data.session_todo[sessionID]
|
||||
if (existing !== undefined) {
|
||||
if (cached === undefined) {
|
||||
globalSync.todo.set(sessionID, existing)
|
||||
}
|
||||
if (!opts?.force) return
|
||||
}
|
||||
|
||||
if (cached !== undefined) {
|
||||
setStore("todo", sessionID, reconcile(cached, { key: "id" }))
|
||||
}
|
||||
|
||||
const key = keyFor(directory, sessionID)
|
||||
return runInflight(inflightTodo, key, () =>
|
||||
retry(() => client.session.todo({ sessionID })).then((todo) => {
|
||||
if (!tracked(directory, sessionID)) return
|
||||
const list = todo.data ?? []
|
||||
setStore("todo", sessionID, reconcile(list, { key: "id" }))
|
||||
globalSync.todo.set(sessionID, list)
|
||||
}),
|
||||
)
|
||||
},
|
||||
history: {
|
||||
more(sessionID: string) {
|
||||
const store = current()[0]
|
||||
const key = keyFor(sdk.directory, sessionID)
|
||||
if (store.message[sessionID] === undefined) return false
|
||||
if (meta.limit[key] === undefined) return false
|
||||
if (meta.complete[key]) return false
|
||||
return !!meta.cursor[key]
|
||||
},
|
||||
loading(sessionID: string) {
|
||||
const key = keyFor(sdk.directory, sessionID)
|
||||
return meta.loading[key] ?? false
|
||||
},
|
||||
async loadMore(sessionID: string, count?: number) {
|
||||
const directory = sdk.directory
|
||||
const client = sdk.client
|
||||
const [, setStore] = globalSync.child(directory)
|
||||
touch(directory, setStore, sessionID)
|
||||
const key = keyFor(directory, sessionID)
|
||||
const step = count ?? historyMessagePageSize
|
||||
if (meta.loading[key]) return
|
||||
if (meta.complete[key]) return
|
||||
const before = meta.cursor[key]
|
||||
if (!before) return
|
||||
|
||||
await loadMessages({
|
||||
directory,
|
||||
client,
|
||||
setStore,
|
||||
sessionID,
|
||||
limit: step,
|
||||
before,
|
||||
mode: "prepend",
|
||||
})
|
||||
},
|
||||
},
|
||||
evict(sessionID: string, directory = sdk.directory) {
|
||||
const [, setStore] = globalSync.child(directory)
|
||||
seenFor(directory).delete(sessionID)
|
||||
evict(directory, setStore, [sessionID])
|
||||
},
|
||||
fetch: async (count = 10) => {
|
||||
const directory = sdk.directory
|
||||
const client = sdk.client
|
||||
const [store, setStore] = globalSync.child(directory)
|
||||
setStore("limit", (x) => x + count)
|
||||
await client.session.list().then((x) => {
|
||||
const sessions = (x.data ?? [])
|
||||
.filter((s) => !!s?.id)
|
||||
.sort((a, b) => cmp(a.id, b.id))
|
||||
.slice(0, store.limit)
|
||||
setStore("session", reconcile(sessions, { key: "id" }))
|
||||
})
|
||||
},
|
||||
more: createMemo(() => current()[0].session.length >= current()[0].limit),
|
||||
archive: async (sessionID: string) => {
|
||||
const directory = sdk.directory
|
||||
const client = sdk.client
|
||||
const [, setStore] = globalSync.child(directory)
|
||||
await client.session.update({ sessionID, time: { archived: Date.now() } })
|
||||
setStore(
|
||||
produce((draft) => {
|
||||
const match = Binary.search(draft.session, sessionID, (s) => s.id)
|
||||
if (match.found) draft.session.splice(match.index, 1)
|
||||
}),
|
||||
)
|
||||
},
|
||||
},
|
||||
absolute,
|
||||
get directory() {
|
||||
return current()[0].path.directory
|
||||
},
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
@@ -1,221 +0,0 @@
|
||||
export type DesktopMenuPlatform = "macos" | "windows"
|
||||
|
||||
export type DesktopMenuAction =
|
||||
| "app.checkForUpdates"
|
||||
| "app.relaunch"
|
||||
| "edit.undo"
|
||||
| "edit.redo"
|
||||
| "edit.cut"
|
||||
| "edit.copy"
|
||||
| "edit.paste"
|
||||
| "edit.delete"
|
||||
| "edit.selectAll"
|
||||
| "view.reload"
|
||||
| "view.toggleDevTools"
|
||||
| "view.resetZoom"
|
||||
| "view.zoomIn"
|
||||
| "view.zoomOut"
|
||||
| "view.toggleFullscreen"
|
||||
| "window.new"
|
||||
| "window.close"
|
||||
| "window.minimize"
|
||||
| "window.toggleMaximize"
|
||||
|
||||
export type DesktopMenuRole =
|
||||
| "about"
|
||||
| "close"
|
||||
| "copy"
|
||||
| "cut"
|
||||
| "hide"
|
||||
| "hideOthers"
|
||||
| "paste"
|
||||
| "quit"
|
||||
| "redo"
|
||||
| "reload"
|
||||
| "resetZoom"
|
||||
| "selectAll"
|
||||
| "toggleDevTools"
|
||||
| "togglefullscreen"
|
||||
| "undo"
|
||||
| "unhide"
|
||||
| "windowMenu"
|
||||
| "zoomIn"
|
||||
| "zoomOut"
|
||||
|
||||
export type DesktopMenuItem = {
|
||||
type: "item"
|
||||
label?: string
|
||||
command?: string
|
||||
action?: DesktopMenuAction
|
||||
role?: DesktopMenuRole
|
||||
href?: string
|
||||
accelerator?: Partial<Record<DesktopMenuPlatform, string>>
|
||||
enabled?: "updater"
|
||||
platforms?: DesktopMenuPlatform[]
|
||||
}
|
||||
|
||||
export type DesktopMenuSeparator = {
|
||||
type: "separator"
|
||||
platforms?: DesktopMenuPlatform[]
|
||||
}
|
||||
|
||||
export type DesktopMenuEntry = DesktopMenuItem | DesktopMenuSeparator
|
||||
|
||||
export type DesktopMenu = {
|
||||
id: string
|
||||
label: string
|
||||
role?: DesktopMenuRole
|
||||
items?: DesktopMenuEntry[]
|
||||
platforms?: DesktopMenuPlatform[]
|
||||
}
|
||||
|
||||
export const DESKTOP_MENU: DesktopMenu[] = [
|
||||
{
|
||||
id: "app",
|
||||
label: "OpenCode",
|
||||
platforms: ["macos"],
|
||||
items: [
|
||||
{ type: "item", role: "about" },
|
||||
{ type: "item", label: "Check for Updates...", action: "app.checkForUpdates", enabled: "updater" },
|
||||
{ type: "item", label: "Settings", command: "settings.open", accelerator: { macos: "Cmd+," } },
|
||||
{ type: "item", label: "Reload Webview", action: "view.reload" },
|
||||
{ type: "item", label: "Restart", action: "app.relaunch" },
|
||||
{ type: "separator" },
|
||||
{ type: "item", role: "hide" },
|
||||
{ type: "item", role: "hideOthers" },
|
||||
{ type: "item", role: "unhide" },
|
||||
{ type: "separator" },
|
||||
{ type: "item", role: "quit" },
|
||||
],
|
||||
},
|
||||
{
|
||||
id: "file",
|
||||
label: "File",
|
||||
items: [
|
||||
{
|
||||
type: "item",
|
||||
label: "New Session",
|
||||
command: "session.new",
|
||||
accelerator: { macos: "Shift+Cmd+S" },
|
||||
},
|
||||
{ type: "item", label: "Open Project...", command: "project.open", accelerator: { macos: "Cmd+O" } },
|
||||
{
|
||||
type: "item",
|
||||
label: "Settings",
|
||||
command: "settings.open",
|
||||
accelerator: { windows: "Ctrl+," },
|
||||
platforms: ["windows"],
|
||||
},
|
||||
{
|
||||
type: "item",
|
||||
label: "New Window",
|
||||
action: "window.new",
|
||||
accelerator: { macos: "Cmd+Shift+N", windows: "Ctrl+Shift+N" },
|
||||
},
|
||||
{ type: "separator" },
|
||||
{ type: "item", label: "Close Window", action: "window.close", role: "close" },
|
||||
],
|
||||
},
|
||||
{
|
||||
id: "edit",
|
||||
label: "Edit",
|
||||
items: [
|
||||
{ type: "item", label: "Undo", action: "edit.undo", role: "undo", accelerator: { windows: "Ctrl+Z" } },
|
||||
{ type: "item", label: "Redo", action: "edit.redo", role: "redo", accelerator: { windows: "Ctrl+Y" } },
|
||||
{ type: "separator" },
|
||||
{ type: "item", label: "Cut", action: "edit.cut", role: "cut", accelerator: { windows: "Ctrl+X" } },
|
||||
{ type: "item", label: "Copy", action: "edit.copy", role: "copy", accelerator: { windows: "Ctrl+C" } },
|
||||
{ type: "item", label: "Paste", action: "edit.paste", role: "paste", accelerator: { windows: "Ctrl+V" } },
|
||||
{ type: "item", label: "Delete", action: "edit.delete" },
|
||||
{
|
||||
type: "item",
|
||||
label: "Select All",
|
||||
action: "edit.selectAll",
|
||||
role: "selectAll",
|
||||
accelerator: { windows: "Ctrl+A" },
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: "view",
|
||||
label: "View",
|
||||
items: [
|
||||
{ type: "item", label: "Toggle Sidebar", command: "sidebar.toggle", accelerator: { macos: "Cmd+B" } },
|
||||
{ type: "item", label: "Toggle Terminal", command: "terminal.toggle", accelerator: { macos: "Ctrl+`" } },
|
||||
{ type: "item", label: "Toggle File Tree", command: "fileTree.toggle" },
|
||||
{ type: "separator" },
|
||||
{ type: "item", label: "Reload", action: "view.reload", role: "reload" },
|
||||
{ type: "item", label: "Toggle Developer Tools", action: "view.toggleDevTools", role: "toggleDevTools" },
|
||||
{ type: "separator" },
|
||||
{
|
||||
type: "item",
|
||||
label: "Actual Size",
|
||||
action: "view.resetZoom",
|
||||
role: "resetZoom",
|
||||
accelerator: { windows: "Ctrl+0" },
|
||||
},
|
||||
{ type: "item", label: "Zoom In", action: "view.zoomIn", role: "zoomIn", accelerator: { windows: "Ctrl++" } },
|
||||
{ type: "item", label: "Zoom Out", action: "view.zoomOut", role: "zoomOut", accelerator: { windows: "Ctrl+-" } },
|
||||
{ type: "separator" },
|
||||
{ type: "item", label: "Toggle Full Screen", action: "view.toggleFullscreen", role: "togglefullscreen" },
|
||||
],
|
||||
},
|
||||
{
|
||||
id: "go",
|
||||
label: "Go",
|
||||
items: [
|
||||
{ type: "item", label: "Back", command: "common.goBack", accelerator: { macos: "Cmd+[" } },
|
||||
{ type: "item", label: "Forward", command: "common.goForward", accelerator: { macos: "Cmd+]" } },
|
||||
{ type: "separator" },
|
||||
{ type: "item", label: "Previous Session", command: "session.previous", accelerator: { macos: "Option+Up" } },
|
||||
{ type: "item", label: "Next Session", command: "session.next", accelerator: { macos: "Option+Down" } },
|
||||
{ type: "separator" },
|
||||
{
|
||||
type: "item",
|
||||
label: "Previous Project",
|
||||
command: "project.previous",
|
||||
accelerator: { macos: "Cmd+Option+Up" },
|
||||
},
|
||||
{
|
||||
type: "item",
|
||||
label: "Next Project",
|
||||
command: "project.next",
|
||||
accelerator: { macos: "Cmd+Option+Down" },
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: "window",
|
||||
label: "Window",
|
||||
role: "windowMenu",
|
||||
items: [
|
||||
{ type: "item", label: "Minimize", action: "window.minimize" },
|
||||
{ type: "item", label: "Maximize", action: "window.toggleMaximize" },
|
||||
{ type: "separator" },
|
||||
{ type: "item", label: "Close Window", action: "window.close" },
|
||||
],
|
||||
},
|
||||
{
|
||||
id: "help",
|
||||
label: "Help",
|
||||
items: [
|
||||
{ type: "item", label: "OpenCode Documentation", href: "https://opencode.ai/docs" },
|
||||
{ type: "item", label: "Support Forum", href: "https://discord.com/invite/opencode" },
|
||||
{ type: "separator" },
|
||||
{
|
||||
type: "item",
|
||||
label: "Share Feedback",
|
||||
href: "https://github.com/anomalyco/opencode/issues/new?template=feature_request.yml",
|
||||
},
|
||||
{
|
||||
type: "item",
|
||||
label: "Report a Bug",
|
||||
href: "https://github.com/anomalyco/opencode/issues/new?template=bug_report.yml",
|
||||
},
|
||||
],
|
||||
},
|
||||
]
|
||||
|
||||
export function desktopMenuVisible(item: { platforms?: DesktopMenuPlatform[] }, platform: DesktopMenuPlatform) {
|
||||
return !item.platforms || item.platforms.includes(platform)
|
||||
}
|
||||
@@ -1,5 +1,4 @@
|
||||
@import "@opencode-ai/ui/styles/tailwind";
|
||||
@import "@opencode-ai/ui/v2/styles/tailwind.css";
|
||||
|
||||
@font-face {
|
||||
font-family: "JetBrainsMono Nerd Font Mono";
|
||||
@@ -54,63 +53,6 @@
|
||||
container-name: getting-started;
|
||||
}
|
||||
|
||||
[data-component="dropdown-menu-content"].desktop-app-menu,
|
||||
[data-component="dropdown-menu-sub-content"].desktop-app-menu {
|
||||
min-width: 160px;
|
||||
padding: 2px;
|
||||
}
|
||||
|
||||
[data-component="dropdown-menu-content"].desktop-app-menu {
|
||||
width: 160px;
|
||||
}
|
||||
|
||||
[data-component="dropdown-menu-sub-content"].desktop-app-menu {
|
||||
width: max-content;
|
||||
min-width: 240px;
|
||||
max-width: min(320px, calc(100vw - 24px));
|
||||
}
|
||||
|
||||
[data-component="dropdown-menu-content"].desktop-app-menu [data-slot="dropdown-menu-group-label"] {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
height: 28px;
|
||||
padding: 0 12px;
|
||||
font-size: var(--font-size-x-small);
|
||||
font-weight: var(--font-weight-medium);
|
||||
line-height: 1;
|
||||
color: var(--text-weak);
|
||||
}
|
||||
|
||||
[data-component="dropdown-menu-content"].desktop-app-menu [data-slot="dropdown-menu-item"],
|
||||
[data-component="dropdown-menu-content"].desktop-app-menu [data-slot="dropdown-menu-sub-trigger"],
|
||||
[data-component="dropdown-menu-sub-content"].desktop-app-menu [data-slot="dropdown-menu-item"],
|
||||
[data-component="dropdown-menu-sub-content"].desktop-app-menu [data-slot="dropdown-menu-sub-trigger"] {
|
||||
min-height: 28px;
|
||||
padding: 0 12px;
|
||||
gap: 8px;
|
||||
font-weight: var(--font-weight-regular);
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
[data-component="dropdown-menu-content"].desktop-app-menu [data-slot="dropdown-menu-item-label"],
|
||||
[data-component="dropdown-menu-sub-content"].desktop-app-menu [data-slot="dropdown-menu-item-label"] {
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
[data-slot="desktop-app-menu-keybind"] {
|
||||
margin-left: auto;
|
||||
color: var(--text-weak);
|
||||
font-size: var(--font-size-x-small);
|
||||
font-weight: var(--font-weight-regular);
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
[data-slot="desktop-app-menu-chevron"] {
|
||||
display: flex;
|
||||
margin-left: auto;
|
||||
color: var(--icon-base);
|
||||
}
|
||||
|
||||
[data-component="getting-started-actions"] {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
@@ -8,7 +8,6 @@ import { LocalProvider } from "@/context/local"
|
||||
import { SDKProvider } from "@/context/sdk"
|
||||
import { SyncProvider, useSync } from "@/context/sync"
|
||||
import { decode64 } from "@/utils/base64"
|
||||
import { Schema } from "effect"
|
||||
|
||||
function DirectoryDataProvider(props: ParentProps<{ directory: string }>) {
|
||||
const location = useLocation()
|
||||
@@ -41,15 +40,6 @@ function DirectoryDataProvider(props: ParentProps<{ directory: string }>) {
|
||||
)
|
||||
}
|
||||
|
||||
export const ProjectDirString = Schema.String.pipe(Schema.brand("ProjectDirString"))
|
||||
export type ProjectDirString = Schema.Schema.Type<typeof ProjectDirString>
|
||||
|
||||
export function decodeDirectory(dir: string): ProjectDirString | undefined {
|
||||
const decoded = decode64(dir)
|
||||
if (!decoded) return
|
||||
return ProjectDirString.make(decoded)
|
||||
}
|
||||
|
||||
export default function Layout(props: ParentProps) {
|
||||
const params = useParams()
|
||||
const language = useLanguage()
|
||||
@@ -58,7 +48,7 @@ export default function Layout(props: ParentProps) {
|
||||
|
||||
const resolved = createMemo(() => {
|
||||
if (!params.dir) return ""
|
||||
return decodeDirectory(params.dir) ?? ""
|
||||
return decode64(params.dir) ?? ""
|
||||
})
|
||||
|
||||
createEffect(() => {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@opencode-ai/console-app",
|
||||
"version": "1.15.6",
|
||||
"version": "1.15.5",
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
"scripts": {
|
||||
|
||||
@@ -1,289 +0,0 @@
|
||||
[data-component="go-credit-confirm"] {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--space-4);
|
||||
min-width: min(34rem, calc(100vw - var(--space-8)));
|
||||
|
||||
p {
|
||||
margin: 0;
|
||||
color: var(--color-text-secondary);
|
||||
font-size: var(--font-size-sm);
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
[data-slot="usage-preview"] {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--space-5);
|
||||
padding: var(--space-4);
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: var(--border-radius-sm);
|
||||
background-color: var(--color-bg-surface);
|
||||
}
|
||||
|
||||
[data-slot="usage-preview-item"] {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--space-2);
|
||||
}
|
||||
|
||||
[data-slot="usage-preview-header"] {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
justify-content: space-between;
|
||||
gap: var(--space-3);
|
||||
}
|
||||
|
||||
[data-slot="usage-preview-label"] {
|
||||
color: var(--color-text);
|
||||
font-size: var(--font-size-sm);
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
[data-slot="usage-preview-value"] {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: var(--space-1);
|
||||
color: var(--color-text-muted);
|
||||
font-family: var(--font-mono);
|
||||
font-size: var(--font-size-xs);
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
[data-slot="usage-preview-after-value"] {
|
||||
color: var(--color-accent);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
[data-slot="usage-preview-progress"] {
|
||||
position: relative;
|
||||
height: 8px;
|
||||
overflow: hidden;
|
||||
border-radius: var(--border-radius-sm);
|
||||
background-color: var(--color-bg);
|
||||
}
|
||||
|
||||
[data-slot="usage-preview-before"],
|
||||
[data-slot="usage-preview-after"] {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
border-radius: var(--border-radius-sm);
|
||||
}
|
||||
|
||||
[data-slot="usage-preview-before"] {
|
||||
background-color: var(--color-border);
|
||||
}
|
||||
|
||||
[data-slot="usage-preview-after"] {
|
||||
background-color: var(--color-accent);
|
||||
transition: width 0.35s ease;
|
||||
}
|
||||
|
||||
[data-slot="usage-preview-reset"] {
|
||||
color: var(--color-text-muted);
|
||||
font-size: var(--font-size-xs);
|
||||
}
|
||||
|
||||
[data-slot="modal-actions"] {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: var(--space-3);
|
||||
}
|
||||
}
|
||||
|
||||
[data-slot="invite-link-box"] {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--space-3);
|
||||
|
||||
> div {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--space-3);
|
||||
border-radius: var(--border-radius-sm);
|
||||
|
||||
@media (max-width: 40rem) {
|
||||
align-items: stretch;
|
||||
flex-direction: column;
|
||||
}
|
||||
}
|
||||
|
||||
code {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
padding: var(--space-3);
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: var(--border-radius-sm);
|
||||
background-color: var(--color-bg);
|
||||
color: var(--color-text);
|
||||
font-family: var(--font-mono);
|
||||
font-size: var(--font-size-sm);
|
||||
line-height: 1.4;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
|
||||
@media (max-width: 40rem) {
|
||||
padding: var(--space-2-5);
|
||||
font-size: var(--font-size-xs);
|
||||
}
|
||||
}
|
||||
|
||||
button {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: var(--space-2);
|
||||
min-width: 130px;
|
||||
white-space: nowrap;
|
||||
|
||||
@media (max-width: 40rem) {
|
||||
min-width: 96px;
|
||||
padding: var(--space-2-5) var(--space-3);
|
||||
font-size: var(--font-size-xs);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[data-slot="instructions"] {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--space-3);
|
||||
|
||||
ol {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--space-2);
|
||||
margin: 0;
|
||||
padding-left: 0;
|
||||
color: var(--color-text-secondary);
|
||||
font-size: var(--font-size-md);
|
||||
list-style-position: inside;
|
||||
line-height: 1.5;
|
||||
}
|
||||
}
|
||||
|
||||
[data-component="go-referral-section"] {
|
||||
[data-component="go-referral-overview"] {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--space-8);
|
||||
padding: var(--space-6);
|
||||
border: 1px dashed var(--color-border);
|
||||
border-radius: var(--border-radius-sm);
|
||||
background-color: var(--color-bg-surface);
|
||||
|
||||
@media (max-width: 30rem) {
|
||||
gap: var(--space-8);
|
||||
padding: var(--space-4);
|
||||
}
|
||||
}
|
||||
|
||||
[data-component="go-referral-overview"] + [data-slot="section-title"] {
|
||||
margin-top: var(--space-4);
|
||||
}
|
||||
|
||||
[data-slot="referrals-table"] {
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
[data-component="empty-state"] {
|
||||
padding: var(--space-4);
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: var(--border-radius-sm);
|
||||
background-color: var(--color-bg-surface);
|
||||
color: var(--color-text-muted);
|
||||
font-size: var(--font-size-sm);
|
||||
}
|
||||
|
||||
[data-slot="referrals-table-element"] {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
font-size: var(--font-size-sm);
|
||||
|
||||
thead {
|
||||
border-bottom: 1px solid var(--color-border);
|
||||
}
|
||||
|
||||
th {
|
||||
padding: var(--space-3) var(--space-4);
|
||||
text-align: left;
|
||||
font-weight: normal;
|
||||
color: var(--color-text-muted);
|
||||
text-transform: uppercase;
|
||||
|
||||
&:nth-child(1) {
|
||||
width: 120px;
|
||||
}
|
||||
|
||||
&:nth-child(3) {
|
||||
width: 180px;
|
||||
}
|
||||
|
||||
&:nth-child(4) {
|
||||
width: 140px;
|
||||
}
|
||||
}
|
||||
|
||||
td {
|
||||
padding: var(--space-3) var(--space-4);
|
||||
border-bottom: 1px solid var(--color-border-muted);
|
||||
color: var(--color-text-muted);
|
||||
font-family: var(--font-mono);
|
||||
|
||||
&[data-slot="referral-amount"] {
|
||||
color: var(--color-text);
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
&[data-slot="referral-source"] {
|
||||
color: var(--color-text-secondary);
|
||||
font-family: var(--font-sans);
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
&[data-slot="referral-action"] {
|
||||
text-align: right;
|
||||
font-family: var(--font-sans);
|
||||
white-space: nowrap;
|
||||
|
||||
button {
|
||||
min-width: 96px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
tbody tr {
|
||||
&[data-status="applied"] {
|
||||
td:not([data-slot="referral-action"]) {
|
||||
opacity: 0.68;
|
||||
}
|
||||
}
|
||||
|
||||
&[data-status="pending"] {
|
||||
td[data-slot="referral-amount"],
|
||||
td[data-slot="referral-date"] {
|
||||
color: var(--color-text-muted);
|
||||
}
|
||||
|
||||
td[data-slot="referral-source"] {
|
||||
color: var(--color-text);
|
||||
}
|
||||
}
|
||||
|
||||
&:last-child td {
|
||||
border-bottom: none;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 40rem) {
|
||||
th,
|
||||
td {
|
||||
padding: var(--space-2) var(--space-3);
|
||||
font-size: var(--font-size-xs);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,300 +0,0 @@
|
||||
import { action, json, query, useAction, useSubmission } from "@solidjs/router"
|
||||
import { createEffect, createMemo, createSignal, For, onCleanup, Show } from "solid-js"
|
||||
import { getRequestEvent } from "solid-js/web"
|
||||
import { Referral } from "@opencode-ai/console-core/referral.js"
|
||||
import { withActor } from "~/context/auth.withActor"
|
||||
import { Modal } from "~/component/modal"
|
||||
import { IconCheck, IconCopy } from "~/component/icon"
|
||||
import { useI18n } from "~/context/i18n"
|
||||
import { useLanguage } from "~/context/language"
|
||||
import { formatResetTime, liteResetTimeKeys } from "~/lib/format-reset-time"
|
||||
import { queryLiteSubscription } from "~/routes/workspace/[id]/go/lite-section"
|
||||
import "./go-referral.css"
|
||||
|
||||
type GoReferralSummary = Awaited<ReturnType<typeof Referral.summary>>
|
||||
type GoReferralReward = GoReferralSummary["rewards"][number]
|
||||
type GoLiteSubscription = Awaited<ReturnType<typeof queryLiteSubscription>>
|
||||
type GoReferralUsagePreview = NonNullable<Awaited<ReturnType<typeof Referral.usagePreview>>>
|
||||
type GoReferralUsagePreviewItem = GoReferralUsagePreview["rollingUsage"]
|
||||
|
||||
const emptyUsagePreview = {
|
||||
rollingUsage: { beforePercent: 0, afterPercent: 0, resetInSec: 0 },
|
||||
weeklyUsage: { beforePercent: 0, afterPercent: 0, resetInSec: 0 },
|
||||
monthlyUsage: { beforePercent: 0, afterPercent: 0, resetInSec: 0 },
|
||||
} satisfies GoReferralUsagePreview
|
||||
|
||||
export const queryGoReferral = query(async (workspaceID: string) => {
|
||||
"use server"
|
||||
return withActor(() => Referral.summary(), workspaceID)
|
||||
}, "go.referral.get")
|
||||
|
||||
export const queryGoReferralUsagePreview = query(async (workspaceID: string, referralID?: string) => {
|
||||
"use server"
|
||||
if (!referralID) return null
|
||||
return withActor(() => Referral.usagePreview({ referralID }), workspaceID)
|
||||
}, "go.referral.usagePreview")
|
||||
|
||||
export const applyGoReferralReward = action(async (workspaceID: string, referralID: string) => {
|
||||
"use server"
|
||||
return json(await withActor(() => Referral.applyReward({ referralID }), workspaceID), {
|
||||
revalidate: [queryGoReferral.key, queryGoReferralUsagePreview.key, queryLiteSubscription.key],
|
||||
})
|
||||
}, "go.referral.reward.apply")
|
||||
|
||||
function currentUsagePreview(usage: { resetInSec: number; usagePercent: number }) {
|
||||
return {
|
||||
beforePercent: usage.usagePercent,
|
||||
afterPercent: usage.usagePercent,
|
||||
resetInSec: usage.resetInSec,
|
||||
}
|
||||
}
|
||||
|
||||
function formatCurrency(amount: number) {
|
||||
if (amount % 100 === 0) return `$${amount / 100}`
|
||||
return `$${(amount / 100).toFixed(2)}`
|
||||
}
|
||||
|
||||
function formatDate(value: string | Date, locale: string) {
|
||||
return new Intl.DateTimeFormat(locale, { month: "short", day: "numeric", year: "numeric" }).format(new Date(value))
|
||||
}
|
||||
|
||||
function rewardDescriptionKey(source: GoReferralReward["source"]) {
|
||||
if (source === "invitee") return "workspace.referral.reward.description.invitee" as const
|
||||
return "workspace.referral.reward.description.inviter" as const
|
||||
}
|
||||
|
||||
function rewardActionKey(reward: GoReferralReward, hasActiveGo: boolean) {
|
||||
if (reward.status === "applied") return "workspace.referral.reward.action.applied" as const
|
||||
if (reward.status === "pending" || !hasActiveGo) return "workspace.referral.reward.action.subscribeUnlock" as const
|
||||
return "workspace.referral.reward.action.view" as const
|
||||
}
|
||||
|
||||
function CopyInviteLink(props: { summary: GoReferralSummary }) {
|
||||
const i18n = useI18n()
|
||||
const [copied, setCopied] = createSignal(false)
|
||||
const event = getRequestEvent()
|
||||
const origin = event
|
||||
? new URL(event.request.url).origin
|
||||
: typeof window === "object"
|
||||
? window.location.origin
|
||||
: undefined
|
||||
const inviteUrl = createMemo(() => {
|
||||
const path = `/go?ref=${props.summary.referralCode}`
|
||||
if (!origin) return path
|
||||
return new URL(path, origin).toString()
|
||||
})
|
||||
|
||||
async function copy() {
|
||||
if (typeof navigator !== "object") return
|
||||
await navigator.clipboard.writeText(inviteUrl())
|
||||
setCopied(true)
|
||||
window.setTimeout(() => setCopied(false), 1600)
|
||||
}
|
||||
|
||||
return (
|
||||
<div data-slot="invite-link-box">
|
||||
<div>
|
||||
<code title={inviteUrl()}>{inviteUrl()}</code>
|
||||
<button type="button" onClick={copy}>
|
||||
<Show
|
||||
when={copied()}
|
||||
fallback={
|
||||
<>
|
||||
<IconCopy style={{ width: "16px", height: "16px" }} /> {i18n.t("workspace.referral.copyLink")}
|
||||
</>
|
||||
}
|
||||
>
|
||||
<IconCheck style={{ width: "16px", height: "16px" }} /> {i18n.t("workspace.referral.copied")}
|
||||
</Show>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export function GoReferralSection(props: {
|
||||
workspaceID: string
|
||||
summary: GoReferralSummary
|
||||
lite: GoLiteSubscription | undefined
|
||||
}) {
|
||||
const i18n = useI18n()
|
||||
const language = useLanguage()
|
||||
const apply = useAction(applyGoReferralReward)
|
||||
const submission = useSubmission(applyGoReferralReward)
|
||||
const [selected, setSelected] = createSignal<GoReferralReward>()
|
||||
const [preview, setPreview] = createSignal<GoReferralUsagePreview | null>()
|
||||
const displayPreview = createMemo(() => {
|
||||
const loaded = preview()
|
||||
if (loaded) return loaded
|
||||
const current = props.lite
|
||||
if (!current) return emptyUsagePreview
|
||||
return {
|
||||
rollingUsage: currentUsagePreview(current.rollingUsage),
|
||||
weeklyUsage: currentUsagePreview(current.weeklyUsage),
|
||||
monthlyUsage: currentUsagePreview(current.monthlyUsage),
|
||||
} satisfies GoReferralUsagePreview
|
||||
})
|
||||
createEffect(() => {
|
||||
const reward = selected()
|
||||
if (!reward) {
|
||||
setPreview(undefined)
|
||||
return
|
||||
}
|
||||
|
||||
const request = { cancelled: false }
|
||||
setPreview(undefined)
|
||||
queryGoReferralUsagePreview(props.workspaceID, reward.id).then((result) => {
|
||||
if (request.cancelled) return
|
||||
setPreview(result)
|
||||
})
|
||||
onCleanup(() => {
|
||||
request.cancelled = true
|
||||
})
|
||||
})
|
||||
|
||||
async function onApply() {
|
||||
const reward = selected()
|
||||
if (!reward) return
|
||||
await apply(props.workspaceID, reward.id)
|
||||
setSelected(undefined)
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Show when={props.lite || props.summary.hasReferral}>
|
||||
<section data-component="go-referral-section">
|
||||
<Show when={props.lite}>
|
||||
<div data-slot="section-title">
|
||||
<h2>{i18n.t("workspace.referral.overview.title")}</h2>
|
||||
<p>{i18n.t("workspace.referral.overview.subtitle")}</p>
|
||||
</div>
|
||||
<div data-component="go-referral-overview">
|
||||
<CopyInviteLink summary={props.summary} />
|
||||
<div data-slot="instructions">
|
||||
<ol>
|
||||
<li>{i18n.t("workspace.referral.instructions.share")}</li>
|
||||
<li>{i18n.t("workspace.referral.instructions.subscribe")}</li>
|
||||
<li>{i18n.t("workspace.referral.instructions.claim")}</li>
|
||||
</ol>
|
||||
</div>
|
||||
</div>
|
||||
</Show>
|
||||
<Show when={props.summary.hasReferral}>
|
||||
<div data-slot="section-title">
|
||||
<h2>{i18n.t("workspace.referral.rewards.title")}</h2>
|
||||
<p>{i18n.t("workspace.referral.rewards.description")}</p>
|
||||
</div>
|
||||
<div data-slot="referrals-table">
|
||||
<table data-slot="referrals-table-element">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{i18n.t("workspace.referral.table.reward")}</th>
|
||||
<th>{i18n.t("workspace.referral.table.referral")}</th>
|
||||
<th>{i18n.t("workspace.referral.table.date")}</th>
|
||||
<th></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<For each={props.summary.rewards}>
|
||||
{(reward) => {
|
||||
const earnedAt = () => formatDate(reward.timeCreated, language.tag(language.locale()))
|
||||
return (
|
||||
<tr data-status={reward.status} data-source={reward.source}>
|
||||
<td data-slot="referral-amount">{formatCurrency(reward.amount)}</td>
|
||||
<td data-slot="referral-source">
|
||||
{i18n.t(rewardDescriptionKey(reward.source), { email: reward.email ?? "" })}
|
||||
</td>
|
||||
<td data-slot="referral-date" title={earnedAt()}>
|
||||
{earnedAt()}
|
||||
</td>
|
||||
<td data-slot="referral-action">
|
||||
<button
|
||||
type="button"
|
||||
disabled={reward.status !== "available" || !props.lite || submission.pending}
|
||||
onClick={() => setSelected(reward)}
|
||||
>
|
||||
{i18n.t(rewardActionKey(reward, !!props.lite))}
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
)
|
||||
}}
|
||||
</For>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</Show>
|
||||
</section>
|
||||
</Show>
|
||||
|
||||
<Modal
|
||||
open={!!selected()}
|
||||
onClose={() => setSelected(undefined)}
|
||||
title={i18n.t("workspace.referral.apply.confirmTitle")}
|
||||
>
|
||||
<div data-component="go-credit-confirm">
|
||||
<p>
|
||||
{i18n.t("workspace.referral.apply.confirmBody", {
|
||||
amount: formatCurrency(selected()?.amount ?? 0),
|
||||
})}
|
||||
</p>
|
||||
<GoReferralUsagePreview preview={displayPreview()} />
|
||||
<div data-slot="modal-actions">
|
||||
<button type="button" onClick={() => setSelected(undefined)}>
|
||||
{i18n.t("common.cancel")}
|
||||
</button>
|
||||
<button type="button" data-color="primary" disabled={submission.pending} onClick={onApply}>
|
||||
{submission.pending ? i18n.t("workspace.lite.loading") : i18n.t("workspace.referral.apply.confirmAction")}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
function GoReferralUsagePreview(props: { preview: GoReferralUsagePreview }) {
|
||||
const i18n = useI18n()
|
||||
|
||||
return (
|
||||
<div data-slot="usage-preview">
|
||||
<GoReferralUsagePreviewRow
|
||||
label={i18n.t("workspace.lite.subscription.rollingUsage")}
|
||||
usage={props.preview.rollingUsage}
|
||||
/>
|
||||
<GoReferralUsagePreviewRow
|
||||
label={i18n.t("workspace.lite.subscription.weeklyUsage")}
|
||||
usage={props.preview.weeklyUsage}
|
||||
/>
|
||||
<GoReferralUsagePreviewRow
|
||||
label={i18n.t("workspace.lite.subscription.monthlyUsage")}
|
||||
usage={props.preview.monthlyUsage}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function GoReferralUsagePreviewRow(props: { label: string; usage: GoReferralUsagePreviewItem }) {
|
||||
const i18n = useI18n()
|
||||
|
||||
return (
|
||||
<div data-slot="usage-preview-item">
|
||||
<div data-slot="usage-preview-header">
|
||||
<span data-slot="usage-preview-label">{props.label}</span>
|
||||
<span data-slot="usage-preview-value">
|
||||
<span>{props.usage.beforePercent}%</span>
|
||||
<span aria-hidden="true">-></span>
|
||||
<span data-slot="usage-preview-after-value">{props.usage.afterPercent}%</span>
|
||||
</span>
|
||||
</div>
|
||||
<div data-slot="usage-preview-progress">
|
||||
<div data-slot="usage-preview-before" style={{ width: `${props.usage.beforePercent}%` }} />
|
||||
<div data-slot="usage-preview-after" style={{ width: `${props.usage.afterPercent}%` }} />
|
||||
</div>
|
||||
<span data-slot="usage-preview-reset">
|
||||
{i18n.t("workspace.lite.subscription.resetsIn")}{" "}
|
||||
{formatResetTime(props.usage.resetInSec, i18n, liteResetTimeKeys)}
|
||||
</span>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -55,61 +55,6 @@
|
||||
@media (prefers-color-scheme: dark) {
|
||||
box-shadow: 0 10px 40px rgba(0, 0, 0, 0.5);
|
||||
}
|
||||
|
||||
button {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: var(--space-3) var(--space-4);
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: var(--border-radius-sm);
|
||||
background-color: var(--color-bg);
|
||||
color: var(--color-text);
|
||||
font-size: var(--font-size-sm);
|
||||
font-family: var(--font-sans);
|
||||
font-weight: 500;
|
||||
line-height: 1;
|
||||
cursor: pointer;
|
||||
transition: all 0.15s ease;
|
||||
|
||||
&:hover:not(:disabled) {
|
||||
background-color: var(--color-surface-hover);
|
||||
border-color: var(--color-accent);
|
||||
}
|
||||
|
||||
&:active:not(:disabled) {
|
||||
transform: translateY(1px);
|
||||
}
|
||||
|
||||
&:disabled {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
transform: none;
|
||||
}
|
||||
|
||||
&[data-color="primary"] {
|
||||
background-color: var(--color-primary);
|
||||
border-color: var(--color-primary);
|
||||
color: var(--color-primary-text);
|
||||
|
||||
&:hover:not(:disabled) {
|
||||
background-color: var(--color-primary-hover);
|
||||
border-color: var(--color-primary-hover);
|
||||
}
|
||||
}
|
||||
|
||||
&[data-color="ghost"] {
|
||||
background-color: transparent;
|
||||
border-color: transparent;
|
||||
color: var(--color-text-muted);
|
||||
|
||||
&:hover:not(:disabled) {
|
||||
background-color: var(--color-surface-hover);
|
||||
border-color: var(--color-border);
|
||||
color: var(--color-text);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[data-slot="title"] {
|
||||
@@ -119,16 +64,4 @@
|
||||
color: var(--color-text);
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
[data-slot="content"][data-variant="black"] {
|
||||
background-color: #000;
|
||||
border-color: rgba(255, 255, 255, 0.17);
|
||||
color: rgba(255, 255, 255, 0.92);
|
||||
font-family: var(--font-mono);
|
||||
|
||||
[data-slot="title"] {
|
||||
color: rgba(255, 255, 255, 0.92);
|
||||
font-family: var(--font-mono);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import { Dialog as Kobalte } from "@kobalte/core/dialog"
|
||||
import { JSX, Show } from "solid-js"
|
||||
import "./modal.css"
|
||||
|
||||
@@ -6,41 +5,20 @@ interface ModalProps {
|
||||
open: boolean
|
||||
onClose: () => void
|
||||
title?: string
|
||||
variant?: "black"
|
||||
children: JSX.Element
|
||||
}
|
||||
|
||||
export function Modal(props: ModalProps) {
|
||||
return (
|
||||
<Show when={props.open}>
|
||||
<Kobalte
|
||||
modal
|
||||
open={props.open}
|
||||
preventScroll={false}
|
||||
onOpenChange={(open) => {
|
||||
if (!open) props.onClose()
|
||||
}}
|
||||
>
|
||||
<Kobalte.Portal>
|
||||
<Kobalte.Overlay data-component="modal" data-slot="overlay" onClick={props.onClose}>
|
||||
<Kobalte.Content
|
||||
data-slot="content"
|
||||
data-variant={props.variant}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
onOpenAutoFocus={(e) => {
|
||||
e.preventDefault()
|
||||
const target = e.currentTarget as HTMLElement | null
|
||||
target?.focus({ preventScroll: true })
|
||||
}}
|
||||
>
|
||||
<Show when={props.title}>
|
||||
<Kobalte.Title data-slot="title">{props.title}</Kobalte.Title>
|
||||
</Show>
|
||||
{props.children}
|
||||
</Kobalte.Content>
|
||||
</Kobalte.Overlay>
|
||||
</Kobalte.Portal>
|
||||
</Kobalte>
|
||||
<div data-component="modal" data-slot="overlay" onClick={props.onClose}>
|
||||
<div data-slot="content" onClick={(e) => e.stopPropagation()}>
|
||||
<Show when={props.title}>
|
||||
<h2 data-slot="title">{props.title}</h2>
|
||||
</Show>
|
||||
{props.children}
|
||||
</div>
|
||||
</div>
|
||||
</Show>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -660,39 +660,6 @@ export const dict = {
|
||||
"workspace.lite.promo.otherMethods": "طرق دفع أخرى",
|
||||
"workspace.lite.promo.selectMethod": "اختر طريقة الدفع",
|
||||
|
||||
"workspace.referral.copyLink": "نسخ الرابط",
|
||||
"workspace.referral.copied": "تم النسخ",
|
||||
"workspace.referral.overview.title": "ادعُ أصدقاءك",
|
||||
"workspace.referral.overview.subtitle": "احصل على $5 عند اشتراك صديق. وسيحصل هو أيضًا على $5.",
|
||||
"workspace.referral.instructions.share": "شارك رابط الإحالة الخاص بك",
|
||||
"workspace.referral.instructions.subscribe": "ينضم صديقك ويشترك في Go",
|
||||
"workspace.referral.instructions.claim": "تحصلان كلاكما على رصيد استخدام بقيمة $5 لتطبيقه على حدود استخدام Go",
|
||||
"workspace.referral.rewards.title": "مكافآت الإحالة",
|
||||
"workspace.referral.rewards.description": "طبّق أرصدة الإحالة المتاحة على استخدامك لـ Go.",
|
||||
"workspace.referral.rewards.subtitle": "تم تطبيق {{applied}} / {{total}} من المكافآت.",
|
||||
"workspace.referral.rewards.empty": "لا توجد مكافآت إحالة بعد.",
|
||||
"workspace.referral.table.reward": "المكافأة",
|
||||
"workspace.referral.table.referral": "الوصف",
|
||||
"workspace.referral.table.date": "التاريخ",
|
||||
"workspace.referral.reward.description.inviter": "تمت دعوة {{email}}",
|
||||
"workspace.referral.reward.description.invitee": "تمت دعوتك بواسطة {{email}}",
|
||||
"workspace.referral.reward.action.subscribeUnlock": "اشترك لإلغاء القفل",
|
||||
"workspace.referral.reward.action.view": "عرض المكافأة",
|
||||
"workspace.referral.reward.action.applied": "تم تطبيق المكافأة",
|
||||
"workspace.referral.reward.source.pendingInviter": "بانتظار اشتراكه",
|
||||
"workspace.referral.reward.source.pendingInvitee": "اشترك لإلغاء قفل المكافأة",
|
||||
"workspace.referral.reward.source.available": "المكافأة جاهزة للتطبيق",
|
||||
"workspace.referral.reward.source.applied": "تم تطبيق المكافأة",
|
||||
"workspace.referral.reward.status.applied": "تم تطبيق المكافأة",
|
||||
"workspace.referral.reward.status.pendingInviter": "اشترك لإلغاء القفل",
|
||||
"workspace.referral.reward.status.pendingInvitee": "اشترك لإلغاء القفل",
|
||||
"workspace.referral.apply.noGo": "اشترك لإلغاء القفل",
|
||||
"workspace.referral.apply.preview": "عرض المكافأة",
|
||||
"workspace.referral.apply.action": "تطبيق",
|
||||
"workspace.referral.apply.confirmTitle": "تطبيق المكافأة",
|
||||
"workspace.referral.apply.confirmBody": "طبِّق {{amount}} لتقليل الاستخدام الحالي في مساحة العمل هذه.",
|
||||
"workspace.referral.apply.confirmAction": "تطبيق",
|
||||
|
||||
"download.title": "OpenCode | تنزيل",
|
||||
"download.meta.description": "نزّل OpenCode لـ macOS، Windows، وLinux",
|
||||
"download.hero.title": "تنزيل OpenCode",
|
||||
|
||||
@@ -670,40 +670,6 @@ export const dict = {
|
||||
"workspace.lite.promo.otherMethods": "Outros métodos de pagamento",
|
||||
"workspace.lite.promo.selectMethod": "Selecionar método de pagamento",
|
||||
|
||||
"workspace.referral.copyLink": "Copiar link",
|
||||
"workspace.referral.copied": "Copiado",
|
||||
"workspace.referral.overview.title": "Convide amigos",
|
||||
"workspace.referral.overview.subtitle": "Ganhe $5 quando um amigo assinar. Ele também ganha $5.",
|
||||
"workspace.referral.instructions.share": "Compartilhe seu link de indicação",
|
||||
"workspace.referral.instructions.subscribe": "Seu amigo entra e assina o Go",
|
||||
"workspace.referral.instructions.claim":
|
||||
"Vocês dois ganham um crédito de uso de $5 para aplicar aos seus limites de uso do Go",
|
||||
"workspace.referral.rewards.title": "Recompensas de indicação",
|
||||
"workspace.referral.rewards.description": "Aplique os créditos de indicação disponíveis no seu uso do Go.",
|
||||
"workspace.referral.rewards.subtitle": "{{applied}} / {{total}} recompensas aplicadas.",
|
||||
"workspace.referral.rewards.empty": "Ainda não há recompensas de indicação.",
|
||||
"workspace.referral.table.reward": "Recompensa",
|
||||
"workspace.referral.table.referral": "Descrição",
|
||||
"workspace.referral.table.date": "Data",
|
||||
"workspace.referral.reward.description.inviter": "Convidou {{email}}",
|
||||
"workspace.referral.reward.description.invitee": "Convidado por {{email}}",
|
||||
"workspace.referral.reward.action.subscribeUnlock": "Assine para desbloquear",
|
||||
"workspace.referral.reward.action.view": "Ver recompensa",
|
||||
"workspace.referral.reward.action.applied": "Recompensa aplicada",
|
||||
"workspace.referral.reward.source.pendingInviter": "Aguardando ele assinar",
|
||||
"workspace.referral.reward.source.pendingInvitee": "Assine para desbloquear a recompensa",
|
||||
"workspace.referral.reward.source.available": "Recompensa pronta para usar",
|
||||
"workspace.referral.reward.source.applied": "Recompensa aplicada",
|
||||
"workspace.referral.reward.status.applied": "Recompensa aplicada",
|
||||
"workspace.referral.reward.status.pendingInviter": "Assine para desbloquear",
|
||||
"workspace.referral.reward.status.pendingInvitee": "Assine para desbloquear",
|
||||
"workspace.referral.apply.noGo": "Assine para desbloquear",
|
||||
"workspace.referral.apply.preview": "Ver recompensa",
|
||||
"workspace.referral.apply.action": "Aplicar",
|
||||
"workspace.referral.apply.confirmTitle": "Aplicar recompensa",
|
||||
"workspace.referral.apply.confirmBody": "Aplique {{amount}} para reduzir o uso atual deste workspace.",
|
||||
"workspace.referral.apply.confirmAction": "Aplicar",
|
||||
|
||||
"download.title": "OpenCode | Baixar",
|
||||
"download.meta.description": "Baixe o OpenCode para macOS, Windows e Linux",
|
||||
"download.hero.title": "Baixar OpenCode",
|
||||
|
||||
@@ -666,39 +666,6 @@ export const dict = {
|
||||
"workspace.lite.promo.otherMethods": "Andre betalingsmetoder",
|
||||
"workspace.lite.promo.selectMethod": "Vælg betalingsmetode",
|
||||
|
||||
"workspace.referral.copyLink": "Kopiér link",
|
||||
"workspace.referral.copied": "Kopieret",
|
||||
"workspace.referral.overview.title": "Inviter venner",
|
||||
"workspace.referral.overview.subtitle": "Få $5, når en ven abonnerer. De får også $5.",
|
||||
"workspace.referral.instructions.share": "Del dit henvisningslink",
|
||||
"workspace.referral.instructions.subscribe": "Din ven tilmelder sig og abonnerer på Go",
|
||||
"workspace.referral.instructions.claim": "I får begge $5 i forbrugskredit til at bruge på jeres Go-forbrugsgrænser",
|
||||
"workspace.referral.rewards.title": "Henvisningsbelønninger",
|
||||
"workspace.referral.rewards.description": "Brug tilgængelige henvisningskreditter på dit Go-forbrug.",
|
||||
"workspace.referral.rewards.subtitle": "{{applied}} / {{total}} belønninger brugt.",
|
||||
"workspace.referral.rewards.empty": "Ingen henvisningsbelønninger endnu.",
|
||||
"workspace.referral.table.reward": "Belønning",
|
||||
"workspace.referral.table.referral": "Beskrivelse",
|
||||
"workspace.referral.table.date": "Dato",
|
||||
"workspace.referral.reward.description.inviter": "Inviterede {{email}}",
|
||||
"workspace.referral.reward.description.invitee": "Inviteret af {{email}}",
|
||||
"workspace.referral.reward.action.subscribeUnlock": "Abonner for at låse op",
|
||||
"workspace.referral.reward.action.view": "Vis belønning",
|
||||
"workspace.referral.reward.action.applied": "Belønning brugt",
|
||||
"workspace.referral.reward.source.pendingInviter": "Venter på, at de abonnerer",
|
||||
"workspace.referral.reward.source.pendingInvitee": "Abonner for at låse belønningen op",
|
||||
"workspace.referral.reward.source.available": "Belønning klar til brug",
|
||||
"workspace.referral.reward.source.applied": "Belønning brugt",
|
||||
"workspace.referral.reward.status.applied": "Belønning brugt",
|
||||
"workspace.referral.reward.status.pendingInviter": "Abonner for at låse op",
|
||||
"workspace.referral.reward.status.pendingInvitee": "Abonner for at låse op",
|
||||
"workspace.referral.apply.noGo": "Abonner for at låse op",
|
||||
"workspace.referral.apply.preview": "Vis belønning",
|
||||
"workspace.referral.apply.action": "Brug",
|
||||
"workspace.referral.apply.confirmTitle": "Brug belønning",
|
||||
"workspace.referral.apply.confirmBody": "Brug {{amount}} til at reducere dette workspaces nuværende forbrug.",
|
||||
"workspace.referral.apply.confirmAction": "Brug",
|
||||
|
||||
"download.title": "OpenCode | Download",
|
||||
"download.meta.description": "Download OpenCode til macOS, Windows og Linux",
|
||||
"download.hero.title": "Download OpenCode",
|
||||
|
||||
@@ -669,41 +669,6 @@ export const dict = {
|
||||
"workspace.lite.promo.otherMethods": "Andere Zahlungsmethoden",
|
||||
"workspace.lite.promo.selectMethod": "Zahlungsmethode auswählen",
|
||||
|
||||
"workspace.referral.copyLink": "Link kopieren",
|
||||
"workspace.referral.copied": "Kopiert",
|
||||
"workspace.referral.overview.title": "Freunde einladen",
|
||||
"workspace.referral.overview.subtitle": "Erhalte $5, wenn ein Freund abonniert. Er bekommt ebenfalls $5.",
|
||||
"workspace.referral.instructions.share": "Teile deinen Empfehlungslink",
|
||||
"workspace.referral.instructions.subscribe": "Dein Freund tritt bei und abonniert Go",
|
||||
"workspace.referral.instructions.claim":
|
||||
"Ihr erhaltet beide ein Nutzungsguthaben von $5, das ihr auf eure Go-Nutzungslimits anrechnen könnt",
|
||||
"workspace.referral.rewards.title": "Empfehlungsbelohnungen",
|
||||
"workspace.referral.rewards.description": "Verfügbare Empfehlungsguthaben auf deine Go-Nutzung anwenden.",
|
||||
"workspace.referral.rewards.subtitle": "{{applied}} / {{total}} Belohnungen eingelöst.",
|
||||
"workspace.referral.rewards.empty": "Noch keine Empfehlungsbelohnungen.",
|
||||
"workspace.referral.table.reward": "Belohnung",
|
||||
"workspace.referral.table.referral": "Beschreibung",
|
||||
"workspace.referral.table.date": "Datum",
|
||||
"workspace.referral.reward.description.inviter": "{{email}} eingeladen",
|
||||
"workspace.referral.reward.description.invitee": "Eingeladen von {{email}}",
|
||||
"workspace.referral.reward.action.subscribeUnlock": "Abonnieren zum Freischalten",
|
||||
"workspace.referral.reward.action.view": "Belohnung ansehen",
|
||||
"workspace.referral.reward.action.applied": "Belohnung eingelöst",
|
||||
"workspace.referral.reward.source.pendingInviter": "Warten auf das Abo des Freundes",
|
||||
"workspace.referral.reward.source.pendingInvitee": "Abonnieren, um Belohnung freizuschalten",
|
||||
"workspace.referral.reward.source.available": "Belohnung kann eingelöst werden",
|
||||
"workspace.referral.reward.source.applied": "Belohnung eingelöst",
|
||||
"workspace.referral.reward.status.applied": "Belohnung eingelöst",
|
||||
"workspace.referral.reward.status.pendingInviter": "Abonnieren zum Freischalten",
|
||||
"workspace.referral.reward.status.pendingInvitee": "Abonnieren zum Freischalten",
|
||||
"workspace.referral.apply.noGo": "Abonnieren zum Freischalten",
|
||||
"workspace.referral.apply.preview": "Belohnung ansehen",
|
||||
"workspace.referral.apply.action": "Einlösen",
|
||||
"workspace.referral.apply.confirmTitle": "Belohnung einlösen",
|
||||
"workspace.referral.apply.confirmBody":
|
||||
"Löse {{amount}} ein, um die aktuelle Nutzung dieses Workspace zu reduzieren.",
|
||||
"workspace.referral.apply.confirmAction": "Einlösen",
|
||||
|
||||
"download.title": "OpenCode | Download",
|
||||
"download.meta.description": "Lade OpenCode für macOS, Windows und Linux herunter",
|
||||
"download.hero.title": "OpenCode herunterladen",
|
||||
|
||||
@@ -662,39 +662,6 @@ export const dict = {
|
||||
"workspace.lite.promo.otherMethods": "Other payment methods",
|
||||
"workspace.lite.promo.selectMethod": "Select payment method",
|
||||
|
||||
"workspace.referral.copyLink": "Copy Link",
|
||||
"workspace.referral.copied": "Copied",
|
||||
"workspace.referral.overview.title": "Invite friends",
|
||||
"workspace.referral.overview.subtitle": "Earn $5 when a friend subscribes. They’ll get $5 too.",
|
||||
"workspace.referral.instructions.share": "Share your referral link",
|
||||
"workspace.referral.instructions.subscribe": "Your friend joins and subscribes to Go",
|
||||
"workspace.referral.instructions.claim": "You both get a $5 usage credit to apply toward your Go usage limits",
|
||||
"workspace.referral.rewards.title": "Referral rewards",
|
||||
"workspace.referral.rewards.description": "Apply available referral credits toward your Go usage.",
|
||||
"workspace.referral.rewards.subtitle": "{{applied}} / {{total}} rewards applied.",
|
||||
"workspace.referral.rewards.empty": "No referral rewards yet.",
|
||||
"workspace.referral.table.reward": "Reward",
|
||||
"workspace.referral.table.referral": "Description",
|
||||
"workspace.referral.table.date": "Date",
|
||||
"workspace.referral.reward.description.inviter": "Invited {{email}}",
|
||||
"workspace.referral.reward.description.invitee": "Invited by {{email}}",
|
||||
"workspace.referral.reward.action.subscribeUnlock": "Subscribe to unlock",
|
||||
"workspace.referral.reward.action.view": "View Reward",
|
||||
"workspace.referral.reward.action.applied": "Reward Applied",
|
||||
"workspace.referral.reward.source.pendingInviter": "Waiting for them to subscribe",
|
||||
"workspace.referral.reward.source.pendingInvitee": "Subscribe to unlock reward",
|
||||
"workspace.referral.reward.source.available": "Reward ready to apply",
|
||||
"workspace.referral.reward.source.applied": "Reward applied",
|
||||
"workspace.referral.reward.status.applied": "Reward Applied",
|
||||
"workspace.referral.reward.status.pendingInviter": "Subscribe to unlock",
|
||||
"workspace.referral.reward.status.pendingInvitee": "Subscribe to unlock",
|
||||
"workspace.referral.apply.noGo": "Subscribe to unlock",
|
||||
"workspace.referral.apply.preview": "View Reward",
|
||||
"workspace.referral.apply.action": "Apply",
|
||||
"workspace.referral.apply.confirmTitle": "Apply reward",
|
||||
"workspace.referral.apply.confirmBody": "Apply {{amount}} to reduce this workspace's current usage.",
|
||||
"workspace.referral.apply.confirmAction": "Apply",
|
||||
|
||||
"download.title": "OpenCode | Download",
|
||||
"download.meta.description": "Download OpenCode for macOS, Windows, and Linux",
|
||||
"download.hero.title": "Download OpenCode",
|
||||
|
||||
@@ -670,40 +670,6 @@ export const dict = {
|
||||
"workspace.lite.promo.otherMethods": "Otros métodos de pago",
|
||||
"workspace.lite.promo.selectMethod": "Seleccionar método de pago",
|
||||
|
||||
"workspace.referral.copyLink": "Copiar enlace",
|
||||
"workspace.referral.copied": "Copiado",
|
||||
"workspace.referral.overview.title": "Invita amigos",
|
||||
"workspace.referral.overview.subtitle": "Gana $5 cuando un amigo se suscriba. Él también recibirá $5.",
|
||||
"workspace.referral.instructions.share": "Comparte tu enlace de referido",
|
||||
"workspace.referral.instructions.subscribe": "Tu amigo se une y se suscribe a Go",
|
||||
"workspace.referral.instructions.claim":
|
||||
"Ambos reciben un crédito de uso de $5 para aplicar a sus límites de uso de Go",
|
||||
"workspace.referral.rewards.title": "Recompensas por referidos",
|
||||
"workspace.referral.rewards.description": "Aplica los créditos por referidos disponibles a tu uso de Go.",
|
||||
"workspace.referral.rewards.subtitle": "{{applied}} / {{total}} recompensas aplicadas.",
|
||||
"workspace.referral.rewards.empty": "Aún no hay recompensas por referidos.",
|
||||
"workspace.referral.table.reward": "Recompensa",
|
||||
"workspace.referral.table.referral": "Descripción",
|
||||
"workspace.referral.table.date": "Fecha",
|
||||
"workspace.referral.reward.description.inviter": "Invitaste a {{email}}",
|
||||
"workspace.referral.reward.description.invitee": "Invitado por {{email}}",
|
||||
"workspace.referral.reward.action.subscribeUnlock": "Suscríbete para desbloquear",
|
||||
"workspace.referral.reward.action.view": "Ver recompensa",
|
||||
"workspace.referral.reward.action.applied": "Recompensa aplicada",
|
||||
"workspace.referral.reward.source.pendingInviter": "Esperando a que se suscriba",
|
||||
"workspace.referral.reward.source.pendingInvitee": "Suscríbete para desbloquear la recompensa",
|
||||
"workspace.referral.reward.source.available": "Recompensa lista para aplicar",
|
||||
"workspace.referral.reward.source.applied": "Recompensa aplicada",
|
||||
"workspace.referral.reward.status.applied": "Recompensa aplicada",
|
||||
"workspace.referral.reward.status.pendingInviter": "Suscríbete para desbloquear",
|
||||
"workspace.referral.reward.status.pendingInvitee": "Suscríbete para desbloquear",
|
||||
"workspace.referral.apply.noGo": "Suscríbete para desbloquear",
|
||||
"workspace.referral.apply.preview": "Ver recompensa",
|
||||
"workspace.referral.apply.action": "Aplicar",
|
||||
"workspace.referral.apply.confirmTitle": "Aplicar recompensa",
|
||||
"workspace.referral.apply.confirmBody": "Aplica {{amount}} para reducir el uso actual de este workspace.",
|
||||
"workspace.referral.apply.confirmAction": "Aplicar",
|
||||
|
||||
"download.title": "OpenCode | Descargar",
|
||||
"download.meta.description": "Descarga OpenCode para macOS, Windows y Linux",
|
||||
"download.hero.title": "Descargar OpenCode",
|
||||
|
||||
@@ -676,41 +676,6 @@ export const dict = {
|
||||
"workspace.lite.promo.otherMethods": "Autres méthodes de paiement",
|
||||
"workspace.lite.promo.selectMethod": "Sélectionner la méthode de paiement",
|
||||
|
||||
"workspace.referral.copyLink": "Copier le lien",
|
||||
"workspace.referral.copied": "Copié",
|
||||
"workspace.referral.overview.title": "Inviter des amis",
|
||||
"workspace.referral.overview.subtitle": "Gagnez $5 lorsqu'un ami s'abonne. Il recevra également $5.",
|
||||
"workspace.referral.instructions.share": "Partagez votre lien de parrainage",
|
||||
"workspace.referral.instructions.subscribe": "Votre ami rejoint et s'abonne à Go",
|
||||
"workspace.referral.instructions.claim":
|
||||
"Vous recevez tous les deux un crédit d'utilisation de $5 à appliquer à vos limites d'utilisation Go",
|
||||
"workspace.referral.rewards.title": "Récompenses de parrainage",
|
||||
"workspace.referral.rewards.description":
|
||||
"Utilisez les crédits de parrainage disponibles pour votre utilisation de Go.",
|
||||
"workspace.referral.rewards.subtitle": "{{applied}} / {{total}} récompenses utilisées.",
|
||||
"workspace.referral.rewards.empty": "Aucune récompense de parrainage pour l'instant.",
|
||||
"workspace.referral.table.reward": "Récompense",
|
||||
"workspace.referral.table.referral": "Description",
|
||||
"workspace.referral.table.date": "Date",
|
||||
"workspace.referral.reward.description.inviter": "Vous avez invité {{email}}",
|
||||
"workspace.referral.reward.description.invitee": "Invité par {{email}}",
|
||||
"workspace.referral.reward.action.subscribeUnlock": "Abonnez-vous pour débloquer",
|
||||
"workspace.referral.reward.action.view": "Voir la récompense",
|
||||
"workspace.referral.reward.action.applied": "Récompense utilisée",
|
||||
"workspace.referral.reward.source.pendingInviter": "En attente de son abonnement",
|
||||
"workspace.referral.reward.source.pendingInvitee": "Abonnez-vous pour débloquer la récompense",
|
||||
"workspace.referral.reward.source.available": "Récompense prête à utiliser",
|
||||
"workspace.referral.reward.source.applied": "Récompense utilisée",
|
||||
"workspace.referral.reward.status.applied": "Récompense utilisée",
|
||||
"workspace.referral.reward.status.pendingInviter": "Abonnez-vous pour débloquer",
|
||||
"workspace.referral.reward.status.pendingInvitee": "Abonnez-vous pour débloquer",
|
||||
"workspace.referral.apply.noGo": "Abonnez-vous pour débloquer",
|
||||
"workspace.referral.apply.preview": "Voir la récompense",
|
||||
"workspace.referral.apply.action": "Utiliser",
|
||||
"workspace.referral.apply.confirmTitle": "Utiliser la récompense",
|
||||
"workspace.referral.apply.confirmBody": "Utilisez {{amount}} pour réduire l'utilisation actuelle de ce workspace.",
|
||||
"workspace.referral.apply.confirmAction": "Utiliser",
|
||||
|
||||
"download.title": "OpenCode | Téléchargement",
|
||||
"download.meta.description": "Téléchargez OpenCode pour macOS, Windows et Linux",
|
||||
"download.hero.title": "Télécharger OpenCode",
|
||||
|
||||
@@ -668,40 +668,6 @@ export const dict = {
|
||||
"workspace.lite.promo.otherMethods": "Altri metodi di pagamento",
|
||||
"workspace.lite.promo.selectMethod": "Seleziona metodo di pagamento",
|
||||
|
||||
"workspace.referral.copyLink": "Copia link",
|
||||
"workspace.referral.copied": "Copiato",
|
||||
"workspace.referral.overview.title": "Invita amici",
|
||||
"workspace.referral.overview.subtitle": "Guadagna $5 quando un amico si abbona. Anche lui riceverà $5.",
|
||||
"workspace.referral.instructions.share": "Condividi il tuo link di referral",
|
||||
"workspace.referral.instructions.subscribe": "Il tuo amico si iscrive e si abbona a Go",
|
||||
"workspace.referral.instructions.claim":
|
||||
"Entrambi ricevete un credito di utilizzo di $5 da applicare ai vostri limiti di utilizzo Go",
|
||||
"workspace.referral.rewards.title": "Premi referral",
|
||||
"workspace.referral.rewards.description": "Applica i crediti referral disponibili al tuo utilizzo di Go.",
|
||||
"workspace.referral.rewards.subtitle": "{{applied}} / {{total}} premi utilizzati.",
|
||||
"workspace.referral.rewards.empty": "Nessun premio referral ancora.",
|
||||
"workspace.referral.table.reward": "Premio",
|
||||
"workspace.referral.table.referral": "Descrizione",
|
||||
"workspace.referral.table.date": "Data",
|
||||
"workspace.referral.reward.description.inviter": "Hai invitato {{email}}",
|
||||
"workspace.referral.reward.description.invitee": "Invitato da {{email}}",
|
||||
"workspace.referral.reward.action.subscribeUnlock": "Abbonati per sbloccare",
|
||||
"workspace.referral.reward.action.view": "Vedi premio",
|
||||
"workspace.referral.reward.action.applied": "Premio utilizzato",
|
||||
"workspace.referral.reward.source.pendingInviter": "In attesa che si abboni",
|
||||
"workspace.referral.reward.source.pendingInvitee": "Abbonati per sbloccare il premio",
|
||||
"workspace.referral.reward.source.available": "Premio pronto da utilizzare",
|
||||
"workspace.referral.reward.source.applied": "Premio utilizzato",
|
||||
"workspace.referral.reward.status.applied": "Premio utilizzato",
|
||||
"workspace.referral.reward.status.pendingInviter": "Abbonati per sbloccare",
|
||||
"workspace.referral.reward.status.pendingInvitee": "Abbonati per sbloccare",
|
||||
"workspace.referral.apply.noGo": "Abbonati per sbloccare",
|
||||
"workspace.referral.apply.preview": "Vedi premio",
|
||||
"workspace.referral.apply.action": "Utilizza",
|
||||
"workspace.referral.apply.confirmTitle": "Utilizza premio",
|
||||
"workspace.referral.apply.confirmBody": "Utilizza {{amount}} per ridurre l'utilizzo attuale di questo workspace.",
|
||||
"workspace.referral.apply.confirmAction": "Utilizza",
|
||||
|
||||
"download.title": "OpenCode | Download",
|
||||
"download.meta.description": "Scarica OpenCode per macOS, Windows e Linux",
|
||||
"download.hero.title": "Scarica OpenCode",
|
||||
|
||||
@@ -668,39 +668,6 @@ export const dict = {
|
||||
"workspace.lite.promo.otherMethods": "その他の支払い方法",
|
||||
"workspace.lite.promo.selectMethod": "支払い方法を選択",
|
||||
|
||||
"workspace.referral.copyLink": "リンクをコピー",
|
||||
"workspace.referral.copied": "コピーしました",
|
||||
"workspace.referral.overview.title": "友達を招待",
|
||||
"workspace.referral.overview.subtitle": "友達がサブスクライブすると $5 を獲得。友達にも $5 が付与されます。",
|
||||
"workspace.referral.instructions.share": "リファラルリンクをシェア",
|
||||
"workspace.referral.instructions.subscribe": "友達が参加して Go にサブスクライブ",
|
||||
"workspace.referral.instructions.claim": "二人とも $5 の利用クレジットを獲得し、Go の利用上限に充当できます",
|
||||
"workspace.referral.rewards.title": "リファラル特典",
|
||||
"workspace.referral.rewards.description": "利用可能なリファラルクレジットを Go の利用に適用します。",
|
||||
"workspace.referral.rewards.subtitle": "{{applied}} / {{total}} 件の特典を適用済み。",
|
||||
"workspace.referral.rewards.empty": "リファラル特典はまだありません。",
|
||||
"workspace.referral.table.reward": "特典",
|
||||
"workspace.referral.table.referral": "説明",
|
||||
"workspace.referral.table.date": "日付",
|
||||
"workspace.referral.reward.description.inviter": "{{email}} を招待しました",
|
||||
"workspace.referral.reward.description.invitee": "{{email}} に招待されました",
|
||||
"workspace.referral.reward.action.subscribeUnlock": "サブスクライブしてアンロック",
|
||||
"workspace.referral.reward.action.view": "特典を表示",
|
||||
"workspace.referral.reward.action.applied": "特典を適用済み",
|
||||
"workspace.referral.reward.source.pendingInviter": "友達のサブスクライブ待ち",
|
||||
"workspace.referral.reward.source.pendingInvitee": "サブスクライブして特典をアンロック",
|
||||
"workspace.referral.reward.source.available": "特典は適用可能です",
|
||||
"workspace.referral.reward.source.applied": "特典を適用済み",
|
||||
"workspace.referral.reward.status.applied": "特典を適用済み",
|
||||
"workspace.referral.reward.status.pendingInviter": "サブスクライブしてアンロック",
|
||||
"workspace.referral.reward.status.pendingInvitee": "サブスクライブしてアンロック",
|
||||
"workspace.referral.apply.noGo": "サブスクライブしてアンロック",
|
||||
"workspace.referral.apply.preview": "特典を表示",
|
||||
"workspace.referral.apply.action": "適用",
|
||||
"workspace.referral.apply.confirmTitle": "特典を適用",
|
||||
"workspace.referral.apply.confirmBody": "{{amount}} を適用して、このワークスペースの現在の使用量を減らします。",
|
||||
"workspace.referral.apply.confirmAction": "適用",
|
||||
|
||||
"download.title": "OpenCode | ダウンロード",
|
||||
"download.meta.description": "OpenCode を macOS、Windows、Linux 向けにダウンロード",
|
||||
"download.hero.title": "OpenCode をダウンロード",
|
||||
|
||||
@@ -660,39 +660,6 @@ export const dict = {
|
||||
"workspace.lite.promo.otherMethods": "기타 결제 수단",
|
||||
"workspace.lite.promo.selectMethod": "결제 수단 선택",
|
||||
|
||||
"workspace.referral.copyLink": "링크 복사",
|
||||
"workspace.referral.copied": "복사됨",
|
||||
"workspace.referral.overview.title": "친구 초대",
|
||||
"workspace.referral.overview.subtitle": "친구가 구독하면 $5를 받으세요. 친구도 $5를 받습니다.",
|
||||
"workspace.referral.instructions.share": "추천 링크 공유",
|
||||
"workspace.referral.instructions.subscribe": "친구가 가입하고 Go를 구독",
|
||||
"workspace.referral.instructions.claim": "두 분 모두 $5 사용 크레딧을 받아 Go 사용 한도에 적용할 수 있습니다",
|
||||
"workspace.referral.rewards.title": "추천 보상",
|
||||
"workspace.referral.rewards.description": "사용 가능한 추천 크레딧을 Go 사용량에 적용합니다.",
|
||||
"workspace.referral.rewards.subtitle": "{{applied}} / {{total}}개 보상 사용됨.",
|
||||
"workspace.referral.rewards.empty": "아직 추천 보상이 없습니다.",
|
||||
"workspace.referral.table.reward": "보상",
|
||||
"workspace.referral.table.referral": "설명",
|
||||
"workspace.referral.table.date": "날짜",
|
||||
"workspace.referral.reward.description.inviter": "{{email}} 초대됨",
|
||||
"workspace.referral.reward.description.invitee": "{{email}}님이 초대",
|
||||
"workspace.referral.reward.action.subscribeUnlock": "구독하여 잠금 해제",
|
||||
"workspace.referral.reward.action.view": "보상 보기",
|
||||
"workspace.referral.reward.action.applied": "보상 사용됨",
|
||||
"workspace.referral.reward.source.pendingInviter": "친구의 구독을 기다리는 중",
|
||||
"workspace.referral.reward.source.pendingInvitee": "구독하여 보상 잠금 해제",
|
||||
"workspace.referral.reward.source.available": "보상 사용 가능",
|
||||
"workspace.referral.reward.source.applied": "보상 사용됨",
|
||||
"workspace.referral.reward.status.applied": "보상 사용됨",
|
||||
"workspace.referral.reward.status.pendingInviter": "구독하여 잠금 해제",
|
||||
"workspace.referral.reward.status.pendingInvitee": "구독하여 잠금 해제",
|
||||
"workspace.referral.apply.noGo": "구독하여 잠금 해제",
|
||||
"workspace.referral.apply.preview": "보상 보기",
|
||||
"workspace.referral.apply.action": "사용",
|
||||
"workspace.referral.apply.confirmTitle": "보상 사용",
|
||||
"workspace.referral.apply.confirmBody": "{{amount}}를 사용하여 이 워크스페이스의 현재 사용량을 줄입니다.",
|
||||
"workspace.referral.apply.confirmAction": "사용",
|
||||
|
||||
"download.title": "OpenCode | 다운로드",
|
||||
"download.meta.description": "macOS, Windows, Linux용 OpenCode 다운로드",
|
||||
"download.hero.title": "OpenCode 다운로드",
|
||||
|
||||
@@ -667,39 +667,6 @@ export const dict = {
|
||||
"workspace.lite.promo.otherMethods": "Andre betalingsmetoder",
|
||||
"workspace.lite.promo.selectMethod": "Velg betalingsmetode",
|
||||
|
||||
"workspace.referral.copyLink": "Kopier lenke",
|
||||
"workspace.referral.copied": "Kopiert",
|
||||
"workspace.referral.overview.title": "Inviter venner",
|
||||
"workspace.referral.overview.subtitle": "Få $5 når en venn abonnerer. De får også $5.",
|
||||
"workspace.referral.instructions.share": "Del henvisningslenken din",
|
||||
"workspace.referral.instructions.subscribe": "Vennen din blir med og abonnerer på Go",
|
||||
"workspace.referral.instructions.claim": "Dere får begge $5 i brukskreditt å bruke på Go-bruksgrensene deres",
|
||||
"workspace.referral.rewards.title": "Henvisningsbelønninger",
|
||||
"workspace.referral.rewards.description": "Bruk tilgjengelige henvisningskreditter på Go-bruken din.",
|
||||
"workspace.referral.rewards.subtitle": "{{applied}} / {{total}} belønninger brukt.",
|
||||
"workspace.referral.rewards.empty": "Ingen henvisningsbelønninger ennå.",
|
||||
"workspace.referral.table.reward": "Belønning",
|
||||
"workspace.referral.table.referral": "Beskrivelse",
|
||||
"workspace.referral.table.date": "Dato",
|
||||
"workspace.referral.reward.description.inviter": "Inviterte {{email}}",
|
||||
"workspace.referral.reward.description.invitee": "Invitert av {{email}}",
|
||||
"workspace.referral.reward.action.subscribeUnlock": "Abonner for å låse opp",
|
||||
"workspace.referral.reward.action.view": "Vis belønning",
|
||||
"workspace.referral.reward.action.applied": "Belønning brukt",
|
||||
"workspace.referral.reward.source.pendingInviter": "Venter på at de abonnerer",
|
||||
"workspace.referral.reward.source.pendingInvitee": "Abonner for å låse opp belønningen",
|
||||
"workspace.referral.reward.source.available": "Belønning klar til bruk",
|
||||
"workspace.referral.reward.source.applied": "Belønning brukt",
|
||||
"workspace.referral.reward.status.applied": "Belønning brukt",
|
||||
"workspace.referral.reward.status.pendingInviter": "Abonner for å låse opp",
|
||||
"workspace.referral.reward.status.pendingInvitee": "Abonner for å låse opp",
|
||||
"workspace.referral.apply.noGo": "Abonner for å låse opp",
|
||||
"workspace.referral.apply.preview": "Vis belønning",
|
||||
"workspace.referral.apply.action": "Bruk",
|
||||
"workspace.referral.apply.confirmTitle": "Bruk belønning",
|
||||
"workspace.referral.apply.confirmBody": "Bruk {{amount}} for å redusere dette workspacets nåværende forbruk.",
|
||||
"workspace.referral.apply.confirmAction": "Bruk",
|
||||
|
||||
"download.title": "OpenCode | Last ned",
|
||||
"download.meta.description": "Last ned OpenCode for macOS, Windows og Linux",
|
||||
"download.hero.title": "Last ned OpenCode",
|
||||
|
||||
@@ -668,39 +668,6 @@ export const dict = {
|
||||
"workspace.lite.promo.otherMethods": "Inne metody płatności",
|
||||
"workspace.lite.promo.selectMethod": "Wybierz metodę płatności",
|
||||
|
||||
"workspace.referral.copyLink": "Kopiuj link",
|
||||
"workspace.referral.copied": "Skopiowano",
|
||||
"workspace.referral.overview.title": "Zaproś znajomych",
|
||||
"workspace.referral.overview.subtitle": "Zdobądź $5, gdy znajomy się zasubskrybuje. On też dostanie $5.",
|
||||
"workspace.referral.instructions.share": "Udostępnij swój link polecający",
|
||||
"workspace.referral.instructions.subscribe": "Twój znajomy dołącza i subskrybuje Go",
|
||||
"workspace.referral.instructions.claim": "Oboje otrzymujecie kredyt $5 do wykorzystania na limity użycia Go",
|
||||
"workspace.referral.rewards.title": "Nagrody za polecenia",
|
||||
"workspace.referral.rewards.description": "Wykorzystaj dostępne środki za polecenia na swoje użycie Go.",
|
||||
"workspace.referral.rewards.subtitle": "Wykorzystano {{applied}} / {{total}} nagród.",
|
||||
"workspace.referral.rewards.empty": "Brak nagród za polecenia.",
|
||||
"workspace.referral.table.reward": "Nagroda",
|
||||
"workspace.referral.table.referral": "Opis",
|
||||
"workspace.referral.table.date": "Data",
|
||||
"workspace.referral.reward.description.inviter": "Zaproszono {{email}}",
|
||||
"workspace.referral.reward.description.invitee": "Zaproszony przez {{email}}",
|
||||
"workspace.referral.reward.action.subscribeUnlock": "Subskrybuj, aby odblokować",
|
||||
"workspace.referral.reward.action.view": "Zobacz nagrodę",
|
||||
"workspace.referral.reward.action.applied": "Nagroda wykorzystana",
|
||||
"workspace.referral.reward.source.pendingInviter": "Oczekiwanie na jego subskrypcję",
|
||||
"workspace.referral.reward.source.pendingInvitee": "Subskrybuj, aby odblokować nagrodę",
|
||||
"workspace.referral.reward.source.available": "Nagroda gotowa do wykorzystania",
|
||||
"workspace.referral.reward.source.applied": "Nagroda wykorzystana",
|
||||
"workspace.referral.reward.status.applied": "Nagroda wykorzystana",
|
||||
"workspace.referral.reward.status.pendingInviter": "Subskrybuj, aby odblokować",
|
||||
"workspace.referral.reward.status.pendingInvitee": "Subskrybuj, aby odblokować",
|
||||
"workspace.referral.apply.noGo": "Subskrybuj, aby odblokować",
|
||||
"workspace.referral.apply.preview": "Zobacz nagrodę",
|
||||
"workspace.referral.apply.action": "Wykorzystaj",
|
||||
"workspace.referral.apply.confirmTitle": "Wykorzystaj nagrodę",
|
||||
"workspace.referral.apply.confirmBody": "Wykorzystaj {{amount}}, aby zmniejszyć aktualne użycie w tym workspace.",
|
||||
"workspace.referral.apply.confirmAction": "Wykorzystaj",
|
||||
|
||||
"download.title": "OpenCode | Pobierz",
|
||||
"download.meta.description": "Pobierz OpenCode na macOS, Windows i Linux",
|
||||
"download.hero.title": "Pobierz OpenCode",
|
||||
|
||||
@@ -674,41 +674,6 @@ export const dict = {
|
||||
"workspace.lite.promo.otherMethods": "Другие способы оплаты",
|
||||
"workspace.lite.promo.selectMethod": "Выберите способ оплаты",
|
||||
|
||||
"workspace.referral.copyLink": "Копировать ссылку",
|
||||
"workspace.referral.copied": "Скопировано",
|
||||
"workspace.referral.overview.title": "Пригласите друзей",
|
||||
"workspace.referral.overview.subtitle": "Получите $5, когда друг оформит подписку. Он тоже получит $5.",
|
||||
"workspace.referral.instructions.share": "Поделитесь своей реферальной ссылкой",
|
||||
"workspace.referral.instructions.subscribe": "Ваш друг присоединяется и оформляет подписку на Go",
|
||||
"workspace.referral.instructions.claim":
|
||||
"Вы оба получаете кредит на использование $5, который можно применить к лимитам использования Go",
|
||||
"workspace.referral.rewards.title": "Реферальные награды",
|
||||
"workspace.referral.rewards.description": "Используйте доступные реферальные кредиты для оплаты использования Go.",
|
||||
"workspace.referral.rewards.subtitle": "Использовано {{applied}} / {{total}} наград.",
|
||||
"workspace.referral.rewards.empty": "Реферальных наград пока нет.",
|
||||
"workspace.referral.table.reward": "Награда",
|
||||
"workspace.referral.table.referral": "Описание",
|
||||
"workspace.referral.table.date": "Дата",
|
||||
"workspace.referral.reward.description.inviter": "Приглашён {{email}}",
|
||||
"workspace.referral.reward.description.invitee": "Приглашены пользователем {{email}}",
|
||||
"workspace.referral.reward.action.subscribeUnlock": "Оформите подписку для разблокировки",
|
||||
"workspace.referral.reward.action.view": "Посмотреть награду",
|
||||
"workspace.referral.reward.action.applied": "Награда использована",
|
||||
"workspace.referral.reward.source.pendingInviter": "Ожидание его подписки",
|
||||
"workspace.referral.reward.source.pendingInvitee": "Подпишитесь, чтобы разблокировать награду",
|
||||
"workspace.referral.reward.source.available": "Награда готова к применению",
|
||||
"workspace.referral.reward.source.applied": "Награда использована",
|
||||
"workspace.referral.reward.status.applied": "Награда использована",
|
||||
"workspace.referral.reward.status.pendingInviter": "Оформите подписку для разблокировки",
|
||||
"workspace.referral.reward.status.pendingInvitee": "Оформите подписку для разблокировки",
|
||||
"workspace.referral.apply.noGo": "Оформите подписку для разблокировки",
|
||||
"workspace.referral.apply.preview": "Посмотреть награду",
|
||||
"workspace.referral.apply.action": "Применить",
|
||||
"workspace.referral.apply.confirmTitle": "Применить награду",
|
||||
"workspace.referral.apply.confirmBody":
|
||||
"Используйте {{amount}}, чтобы уменьшить текущее использование этого workspace.",
|
||||
"workspace.referral.apply.confirmAction": "Применить",
|
||||
|
||||
"download.title": "OpenCode | Скачать",
|
||||
"download.meta.description": "Скачать OpenCode для macOS, Windows и Linux",
|
||||
"download.hero.title": "Скачать OpenCode",
|
||||
|
||||
@@ -663,39 +663,6 @@ export const dict = {
|
||||
"workspace.lite.promo.otherMethods": "วิธีการชำระเงินอื่นๆ",
|
||||
"workspace.lite.promo.selectMethod": "เลือกวิธีการชำระเงิน",
|
||||
|
||||
"workspace.referral.copyLink": "คัดลอกลิงก์",
|
||||
"workspace.referral.copied": "คัดลอกแล้ว",
|
||||
"workspace.referral.overview.title": "ชวนเพื่อน",
|
||||
"workspace.referral.overview.subtitle": "รับ $5 เมื่อเพื่อนสมัครสมาชิก เพื่อนก็จะได้รับ $5 เช่นกัน",
|
||||
"workspace.referral.instructions.share": "แชร์ลิงก์แนะนำของคุณ",
|
||||
"workspace.referral.instructions.subscribe": "เพื่อนของคุณเข้าร่วมและสมัครสมาชิก Go",
|
||||
"workspace.referral.instructions.claim": "คุณทั้งคู่จะได้รับเครดิตการใช้งาน $5 เพื่อใช้กับขีดจำกัดการใช้งาน Go",
|
||||
"workspace.referral.rewards.title": "รางวัลการแนะนำ",
|
||||
"workspace.referral.rewards.description": "ใช้เครดิตการแนะนำที่มีอยู่กับการใช้งาน Go ของคุณ",
|
||||
"workspace.referral.rewards.subtitle": "ใช้แล้ว {{applied}} / {{total}} รางวัล",
|
||||
"workspace.referral.rewards.empty": "ยังไม่มีรางวัลการแนะนำ",
|
||||
"workspace.referral.table.reward": "รางวัล",
|
||||
"workspace.referral.table.referral": "คำอธิบาย",
|
||||
"workspace.referral.table.date": "วันที่",
|
||||
"workspace.referral.reward.description.inviter": "เชิญ {{email}}",
|
||||
"workspace.referral.reward.description.invitee": "ได้รับเชิญจาก {{email}}",
|
||||
"workspace.referral.reward.action.subscribeUnlock": "สมัครสมาชิกเพื่อปลดล็อก",
|
||||
"workspace.referral.reward.action.view": "ดูรางวัล",
|
||||
"workspace.referral.reward.action.applied": "ใช้รางวัลแล้ว",
|
||||
"workspace.referral.reward.source.pendingInviter": "รอเพื่อนสมัครสมาชิก",
|
||||
"workspace.referral.reward.source.pendingInvitee": "สมัครสมาชิกเพื่อปลดล็อกรางวัล",
|
||||
"workspace.referral.reward.source.available": "รางวัลพร้อมใช้งาน",
|
||||
"workspace.referral.reward.source.applied": "ใช้รางวัลแล้ว",
|
||||
"workspace.referral.reward.status.applied": "ใช้รางวัลแล้ว",
|
||||
"workspace.referral.reward.status.pendingInviter": "สมัครสมาชิกเพื่อปลดล็อก",
|
||||
"workspace.referral.reward.status.pendingInvitee": "สมัครสมาชิกเพื่อปลดล็อก",
|
||||
"workspace.referral.apply.noGo": "สมัครสมาชิกเพื่อปลดล็อก",
|
||||
"workspace.referral.apply.preview": "ดูรางวัล",
|
||||
"workspace.referral.apply.action": "ใช้",
|
||||
"workspace.referral.apply.confirmTitle": "ใช้รางวัล",
|
||||
"workspace.referral.apply.confirmBody": "ใช้ {{amount}} เพื่อลดการใช้งานปัจจุบันของ workspace นี้",
|
||||
"workspace.referral.apply.confirmAction": "ใช้",
|
||||
|
||||
"download.title": "OpenCode | ดาวน์โหลด",
|
||||
"download.meta.description": "ดาวน์โหลด OpenCode สำหรับ macOS, Windows และ Linux",
|
||||
"download.hero.title": "ดาวน์โหลด OpenCode",
|
||||
|
||||
@@ -670,40 +670,6 @@ export const dict = {
|
||||
"workspace.lite.promo.otherMethods": "Diğer ödeme yöntemleri",
|
||||
"workspace.lite.promo.selectMethod": "Ödeme yöntemini seçin",
|
||||
|
||||
"workspace.referral.copyLink": "Bağlantıyı Kopyala",
|
||||
"workspace.referral.copied": "Kopyalandı",
|
||||
"workspace.referral.overview.title": "Arkadaşlarını davet et",
|
||||
"workspace.referral.overview.subtitle": "Bir arkadaşın abone olduğunda $5 kazan. O da $5 alacak.",
|
||||
"workspace.referral.instructions.share": "Referans bağlantını paylaş",
|
||||
"workspace.referral.instructions.subscribe": "Arkadaşın katılır ve Go'ya abone olur",
|
||||
"workspace.referral.instructions.claim":
|
||||
"İkiniz de Go kullanım limitlerinize uygulamak için $5 kullanım kredisi alırsınız",
|
||||
"workspace.referral.rewards.title": "Davet ödülleri",
|
||||
"workspace.referral.rewards.description": "Mevcut davet kredilerini Go kullanımınıza uygulayın.",
|
||||
"workspace.referral.rewards.subtitle": "{{applied}} / {{total}} ödül kullanıldı.",
|
||||
"workspace.referral.rewards.empty": "Henüz davet ödülü yok.",
|
||||
"workspace.referral.table.reward": "Ödül",
|
||||
"workspace.referral.table.referral": "Açıklama",
|
||||
"workspace.referral.table.date": "Tarih",
|
||||
"workspace.referral.reward.description.inviter": "{{email}} davet edildi",
|
||||
"workspace.referral.reward.description.invitee": "{{email}} tarafından davet edildi",
|
||||
"workspace.referral.reward.action.subscribeUnlock": "Kilidi açmak için abone ol",
|
||||
"workspace.referral.reward.action.view": "Ödülü Görüntüle",
|
||||
"workspace.referral.reward.action.applied": "Ödül Kullanıldı",
|
||||
"workspace.referral.reward.source.pendingInviter": "Abone olması bekleniyor",
|
||||
"workspace.referral.reward.source.pendingInvitee": "Ödülün kilidini açmak için abone ol",
|
||||
"workspace.referral.reward.source.available": "Ödül kullanıma hazır",
|
||||
"workspace.referral.reward.source.applied": "Ödül kullanıldı",
|
||||
"workspace.referral.reward.status.applied": "Ödül Kullanıldı",
|
||||
"workspace.referral.reward.status.pendingInviter": "Kilidi açmak için abone ol",
|
||||
"workspace.referral.reward.status.pendingInvitee": "Kilidi açmak için abone ol",
|
||||
"workspace.referral.apply.noGo": "Kilidi açmak için abone ol",
|
||||
"workspace.referral.apply.preview": "Ödülü Görüntüle",
|
||||
"workspace.referral.apply.action": "Kullan",
|
||||
"workspace.referral.apply.confirmTitle": "Ödülü kullan",
|
||||
"workspace.referral.apply.confirmBody": "Bu workspace'in mevcut kullanımını azaltmak için {{amount}} kullan.",
|
||||
"workspace.referral.apply.confirmAction": "Kullan",
|
||||
|
||||
"download.title": "OpenCode | İndir",
|
||||
"download.meta.description": "OpenCode'u macOS, Windows ve Linux için indirin",
|
||||
"download.hero.title": "OpenCode'u İndir",
|
||||
|
||||
@@ -643,39 +643,6 @@ export const dict = {
|
||||
"workspace.lite.promo.otherMethods": "其他付款方式",
|
||||
"workspace.lite.promo.selectMethod": "选择付款方式",
|
||||
|
||||
"workspace.referral.copyLink": "复制链接",
|
||||
"workspace.referral.copied": "已复制",
|
||||
"workspace.referral.overview.title": "邀请好友",
|
||||
"workspace.referral.overview.subtitle": "好友订阅后,您可获得 $5,对方也可获得 $5。",
|
||||
"workspace.referral.instructions.share": "分享您的推荐链接。",
|
||||
"workspace.referral.instructions.subscribe": "好友加入并订阅 Go。",
|
||||
"workspace.referral.instructions.claim": "你们都将获得 $5 使用额度,可用于您的 Go 使用限额。",
|
||||
"workspace.referral.rewards.title": "邀请奖励",
|
||||
"workspace.referral.rewards.description": "将可用的邀请积分应用到您的 Go 用量。",
|
||||
"workspace.referral.rewards.subtitle": "已使用 {{applied}} / {{total}} 个奖励。",
|
||||
"workspace.referral.rewards.empty": "暂无邀请奖励。",
|
||||
"workspace.referral.table.reward": "奖励",
|
||||
"workspace.referral.table.referral": "描述",
|
||||
"workspace.referral.table.date": "日期",
|
||||
"workspace.referral.reward.description.inviter": "已邀请 {{email}}",
|
||||
"workspace.referral.reward.description.invitee": "由 {{email}} 邀请",
|
||||
"workspace.referral.reward.action.subscribeUnlock": "订阅以解锁",
|
||||
"workspace.referral.reward.action.view": "查看奖励",
|
||||
"workspace.referral.reward.action.applied": "奖励已使用",
|
||||
"workspace.referral.reward.source.pendingInviter": "等待对方订阅",
|
||||
"workspace.referral.reward.source.pendingInvitee": "订阅即可解锁奖励",
|
||||
"workspace.referral.reward.source.available": "奖励可使用",
|
||||
"workspace.referral.reward.source.applied": "奖励已使用",
|
||||
"workspace.referral.reward.status.applied": "奖励已使用",
|
||||
"workspace.referral.reward.status.pendingInviter": "订阅以解锁",
|
||||
"workspace.referral.reward.status.pendingInvitee": "订阅以解锁",
|
||||
"workspace.referral.apply.noGo": "订阅以解锁",
|
||||
"workspace.referral.apply.preview": "查看奖励",
|
||||
"workspace.referral.apply.action": "使用",
|
||||
"workspace.referral.apply.confirmTitle": "使用奖励",
|
||||
"workspace.referral.apply.confirmBody": "使用 {{amount}} 抵扣当前工作区的用量。",
|
||||
"workspace.referral.apply.confirmAction": "使用",
|
||||
|
||||
"download.title": "OpenCode | 下载",
|
||||
"download.meta.description": "下载适用于 macOS, Windows, 和 Linux 的 OpenCode",
|
||||
"download.hero.title": "下载 OpenCode",
|
||||
|
||||
@@ -643,39 +643,6 @@ export const dict = {
|
||||
"workspace.lite.promo.otherMethods": "其他付款方式",
|
||||
"workspace.lite.promo.selectMethod": "選擇付款方式",
|
||||
|
||||
"workspace.referral.copyLink": "複製連結",
|
||||
"workspace.referral.copied": "已複製",
|
||||
"workspace.referral.overview.title": "邀請朋友",
|
||||
"workspace.referral.overview.subtitle": "朋友訂閱後,您可獲得 $5,對方也可獲得 $5。",
|
||||
"workspace.referral.instructions.share": "分享您的推薦連結。",
|
||||
"workspace.referral.instructions.subscribe": "朋友加入並訂閱 Go。",
|
||||
"workspace.referral.instructions.claim": "你們都將獲得 $5 使用額度,可用於您的 Go 使用限額。",
|
||||
"workspace.referral.rewards.title": "邀請獎勵",
|
||||
"workspace.referral.rewards.description": "將可用的邀請點數套用至您的 Go 使用量。",
|
||||
"workspace.referral.rewards.subtitle": "已使用 {{applied}} / {{total}} 個獎勵。",
|
||||
"workspace.referral.rewards.empty": "暫無邀請獎勵。",
|
||||
"workspace.referral.table.reward": "獎勵",
|
||||
"workspace.referral.table.referral": "描述",
|
||||
"workspace.referral.table.date": "日期",
|
||||
"workspace.referral.reward.description.inviter": "已邀請 {{email}}",
|
||||
"workspace.referral.reward.description.invitee": "由 {{email}} 邀請",
|
||||
"workspace.referral.reward.action.subscribeUnlock": "訂閱以解鎖",
|
||||
"workspace.referral.reward.action.view": "查看獎勵",
|
||||
"workspace.referral.reward.action.applied": "獎勵已使用",
|
||||
"workspace.referral.reward.source.pendingInviter": "等待對方訂閱",
|
||||
"workspace.referral.reward.source.pendingInvitee": "訂閱即可解鎖獎勵",
|
||||
"workspace.referral.reward.source.available": "獎勵可使用",
|
||||
"workspace.referral.reward.source.applied": "獎勵已使用",
|
||||
"workspace.referral.reward.status.applied": "獎勵已使用",
|
||||
"workspace.referral.reward.status.pendingInviter": "訂閱以解鎖",
|
||||
"workspace.referral.reward.status.pendingInvitee": "訂閱以解鎖",
|
||||
"workspace.referral.apply.noGo": "訂閱以解鎖",
|
||||
"workspace.referral.apply.preview": "查看獎勵",
|
||||
"workspace.referral.apply.action": "使用",
|
||||
"workspace.referral.apply.confirmTitle": "使用獎勵",
|
||||
"workspace.referral.apply.confirmBody": "使用 {{amount}} 抵扣目前工作區的用量。",
|
||||
"workspace.referral.apply.confirmAction": "使用",
|
||||
|
||||
"download.title": "OpenCode | 下載",
|
||||
"download.meta.description": "下載適用於 macOS、Windows 與 Linux 的 OpenCode",
|
||||
"download.hero.title": "下載 OpenCode",
|
||||
|
||||
@@ -1,47 +0,0 @@
|
||||
import type { Key } from "~/i18n"
|
||||
import type { useI18n } from "~/context/i18n"
|
||||
|
||||
type ResetTimeKeys = {
|
||||
day: Key
|
||||
days: Key
|
||||
hour: Key
|
||||
hours: Key
|
||||
minute: Key
|
||||
minutes: Key
|
||||
fewSeconds: Key
|
||||
}
|
||||
|
||||
export const liteResetTimeKeys = {
|
||||
day: "workspace.lite.time.day",
|
||||
days: "workspace.lite.time.days",
|
||||
hour: "workspace.lite.time.hour",
|
||||
hours: "workspace.lite.time.hours",
|
||||
minute: "workspace.lite.time.minute",
|
||||
minutes: "workspace.lite.time.minutes",
|
||||
fewSeconds: "workspace.lite.time.fewSeconds",
|
||||
} satisfies ResetTimeKeys
|
||||
|
||||
export const blackResetTimeKeys = {
|
||||
day: "workspace.black.time.day",
|
||||
days: "workspace.black.time.days",
|
||||
hour: "workspace.black.time.hour",
|
||||
hours: "workspace.black.time.hours",
|
||||
minute: "workspace.black.time.minute",
|
||||
minutes: "workspace.black.time.minutes",
|
||||
fewSeconds: "workspace.black.time.fewSeconds",
|
||||
} satisfies ResetTimeKeys
|
||||
|
||||
export function formatResetTime(seconds: number, i18n: ReturnType<typeof useI18n>, keys: ResetTimeKeys) {
|
||||
const days = Math.floor(seconds / 86400)
|
||||
if (days >= 1) {
|
||||
const hours = Math.floor((seconds % 86400) / 3600)
|
||||
return `${days} ${days === 1 ? i18n.t(keys.day) : i18n.t(keys.days)} ${hours} ${hours === 1 ? i18n.t(keys.hour) : i18n.t(keys.hours)}`
|
||||
}
|
||||
|
||||
const hours = Math.floor(seconds / 3600)
|
||||
const minutes = Math.floor((seconds % 3600) / 60)
|
||||
if (hours >= 1)
|
||||
return `${hours} ${hours === 1 ? i18n.t(keys.hour) : i18n.t(keys.hours)} ${minutes} ${minutes === 1 ? i18n.t(keys.minute) : i18n.t(keys.minutes)}`
|
||||
if (minutes === 0) return i18n.t(keys.fewSeconds)
|
||||
return `${minutes} ${minutes === 1 ? i18n.t(keys.minute) : i18n.t(keys.minutes)}`
|
||||
}
|
||||
@@ -1,28 +0,0 @@
|
||||
import { Referral } from "@opencode-ai/console-core/referral.js"
|
||||
|
||||
const REFERRAL_COOKIE = "oc_referral"
|
||||
const REFERRAL_MAX_AGE = 60 * 60 * 24 * 30
|
||||
|
||||
export function normalizeReferralCode(code?: string | null) {
|
||||
return Referral.normalizeCode(code)
|
||||
}
|
||||
|
||||
export function referralCookie(code: string) {
|
||||
return `${REFERRAL_COOKIE}=${encodeURIComponent(code)}; Path=/; Max-Age=${REFERRAL_MAX_AGE}; SameSite=Lax; HttpOnly`
|
||||
}
|
||||
|
||||
export function clearReferralCookie() {
|
||||
return `${REFERRAL_COOKIE}=; Path=/; Max-Age=0; SameSite=Lax; HttpOnly`
|
||||
}
|
||||
|
||||
export function referralCodeFromCookieHeader(header: string | null) {
|
||||
if (!header) return undefined
|
||||
|
||||
return normalizeReferralCode(
|
||||
header
|
||||
.split(";")
|
||||
.map((x) => x.trim())
|
||||
.find((x) => x.startsWith(`${REFERRAL_COOKIE}=`))
|
||||
?.slice(`${REFERRAL_COOKIE}=`.length),
|
||||
)
|
||||
}
|
||||
@@ -1,20 +1,16 @@
|
||||
import { createMiddleware } from "@solidjs/start/middleware"
|
||||
import { LOCALE_HEADER, cookie, fromPathname, strip } from "~/lib/language"
|
||||
import { normalizeReferralCode, referralCookie } from "~/lib/referral-invite"
|
||||
|
||||
export default createMiddleware({
|
||||
onRequest(event) {
|
||||
const url = new URL(event.request.url)
|
||||
const locale = fromPathname(url.pathname)
|
||||
if (locale) {
|
||||
url.pathname = strip(url.pathname)
|
||||
const request = new Request(url, event.request)
|
||||
request.headers.set(LOCALE_HEADER, locale)
|
||||
event.request = request
|
||||
event.response.headers.append("set-cookie", cookie(locale))
|
||||
}
|
||||
if (!locale) return
|
||||
|
||||
const referralCode = normalizeReferralCode(url.searchParams.get("ref"))
|
||||
if (referralCode) event.response.headers.append("set-cookie", referralCookie(referralCode))
|
||||
url.pathname = strip(url.pathname)
|
||||
const request = new Request(url, event.request)
|
||||
request.headers.set(LOCALE_HEADER, locale)
|
||||
event.request = request
|
||||
event.response.headers.append("set-cookie", cookie(locale))
|
||||
},
|
||||
})
|
||||
|
||||
@@ -1,11 +1,9 @@
|
||||
import { redirect } from "@solidjs/router"
|
||||
import type { APIEvent } from "@solidjs/start/server"
|
||||
import { Referral } from "@opencode-ai/console-core/referral.js"
|
||||
import { AuthClient } from "~/context/auth"
|
||||
import { useAuthSession } from "~/context/auth"
|
||||
import { i18n } from "~/i18n"
|
||||
import { localeFromRequest, route } from "~/lib/language"
|
||||
import { clearReferralCookie, referralCodeFromCookieHeader } from "~/lib/referral-invite"
|
||||
|
||||
export async function GET(input: APIEvent) {
|
||||
const url = new URL(input.request.url)
|
||||
@@ -19,7 +17,6 @@ export async function GET(input: APIEvent) {
|
||||
if (result.err) throw new Error(result.err.message)
|
||||
const decoded = AuthClient.decode(result.tokens.access, {} as any)
|
||||
if (decoded.err) throw new Error(decoded.err.message)
|
||||
const referralCode = referralCodeFromCookieHeader(input.request.headers.get("cookie"))
|
||||
const session = await useAuthSession()
|
||||
const id = decoded.subject.properties.accountID
|
||||
await session.update((value) => {
|
||||
@@ -35,15 +32,8 @@ export async function GET(input: APIEvent) {
|
||||
current: id,
|
||||
}
|
||||
})
|
||||
if (decoded.subject.properties.newAccount && referralCode) {
|
||||
await Referral.createFromAccount({ accountID: id, referralCode }).catch((error) => {
|
||||
console.error("Referral create failed", error)
|
||||
})
|
||||
}
|
||||
const next = url.pathname === "/auth/callback" ? "/auth" : url.pathname.replace("/auth/callback", "")
|
||||
const response = redirect(route(locale, next))
|
||||
if (referralCode) response.headers.append("set-cookie", clearReferralCookie())
|
||||
return response
|
||||
return redirect(route(locale, next))
|
||||
} catch (e: any) {
|
||||
return new Response(
|
||||
JSON.stringify({
|
||||
|
||||
@@ -700,6 +700,65 @@
|
||||
text-decoration: underline;
|
||||
}
|
||||
}
|
||||
|
||||
[data-slot="workspace-picker"] {
|
||||
[data-slot="workspace-list"] {
|
||||
width: 100%;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
list-style: none;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
gap: 8px;
|
||||
align-self: stretch;
|
||||
outline: none;
|
||||
overflow-y: auto;
|
||||
max-height: 240px;
|
||||
scrollbar-width: none;
|
||||
|
||||
&::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
|
||||
[data-slot="workspace-item"] {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
padding: 8px 12px;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
align-self: stretch;
|
||||
cursor: pointer;
|
||||
|
||||
[data-slot="selected-icon"] {
|
||||
visibility: hidden;
|
||||
color: rgba(255, 255, 255, 0.39);
|
||||
font-family: "IBM Plex Mono", monospace;
|
||||
font-size: 16px;
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
line-height: 160%;
|
||||
}
|
||||
|
||||
span:last-child {
|
||||
color: rgba(255, 255, 255, 0.92);
|
||||
font-size: 16px;
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
line-height: 160%;
|
||||
}
|
||||
|
||||
&:hover,
|
||||
&[data-active="true"] {
|
||||
background: #161616;
|
||||
|
||||
[data-slot="selected-icon"] {
|
||||
visibility: visible;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -780,64 +839,3 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[data-component="black-workspace-picker-modal"] {
|
||||
font-family: var(--font-mono);
|
||||
|
||||
[data-slot="workspace-list"] {
|
||||
width: 100%;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
list-style: none;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
gap: 8px;
|
||||
align-self: stretch;
|
||||
outline: none;
|
||||
overflow-y: auto;
|
||||
max-height: 240px;
|
||||
scrollbar-width: none;
|
||||
|
||||
&::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
[data-slot="workspace-item"] {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
padding: 8px 12px;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
align-self: stretch;
|
||||
cursor: pointer;
|
||||
|
||||
[data-slot="selected-icon"] {
|
||||
visibility: hidden;
|
||||
color: rgba(255, 255, 255, 0.39);
|
||||
font-family: "IBM Plex Mono", monospace;
|
||||
font-size: 16px;
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
line-height: 160%;
|
||||
}
|
||||
|
||||
span:last-child {
|
||||
color: rgba(255, 255, 255, 0.92);
|
||||
font-size: 16px;
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
line-height: 160%;
|
||||
}
|
||||
|
||||
&:hover,
|
||||
&[data-active="true"] {
|
||||
background: #161616;
|
||||
|
||||
[data-slot="selected-icon"] {
|
||||
visibility: visible;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -444,13 +444,8 @@ export default function BlackSubscribe() {
|
||||
</div>
|
||||
|
||||
{/* Workspace picker modal */}
|
||||
<Modal
|
||||
open={showWorkspacePicker() ?? false}
|
||||
onClose={() => {}}
|
||||
title={i18n.t("black.workspace.selectPlan")}
|
||||
variant="black"
|
||||
>
|
||||
<div data-component="black-workspace-picker-modal" data-slot="workspace-picker">
|
||||
<Modal open={showWorkspacePicker() ?? false} onClose={() => {}} title={i18n.t("black.workspace.selectPlan")}>
|
||||
<div data-slot="workspace-picker">
|
||||
<ul
|
||||
ref={listRef}
|
||||
data-slot="workspace-list"
|
||||
|
||||
@@ -9,7 +9,7 @@ import { Actor } from "@opencode-ai/console-core/actor.js"
|
||||
import { Resource } from "@opencode-ai/console-resource"
|
||||
import { LiteData } from "@opencode-ai/console-core/lite.js"
|
||||
import { BlackData } from "@opencode-ai/console-core/black.js"
|
||||
import { Referral } from "@opencode-ai/console-core/referral.js"
|
||||
import { User } from "@opencode-ai/console-core/user.js"
|
||||
|
||||
export async function POST(input: APIEvent) {
|
||||
const body = await Billing.stripe().webhooks.constructEventAsync(
|
||||
@@ -174,13 +174,6 @@ export async function POST(input: APIEvent) {
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
await Referral.completeFromLiteSubscription({
|
||||
workspaceID,
|
||||
userID,
|
||||
}).catch((error) => {
|
||||
console.error("Referral sync failed", error)
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -34,10 +34,6 @@
|
||||
background-color: var(--color-bg-surface);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[data-component="workspace-create-modal"] {
|
||||
width: 100%;
|
||||
|
||||
[data-slot="create-form"] {
|
||||
width: 100%;
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { query, useParams, action, createAsync, redirect, useSubmission } from "@solidjs/router"
|
||||
import { For, createEffect, createSignal } from "solid-js"
|
||||
import { For, createEffect } from "solid-js"
|
||||
import { createStore } from "solid-js/store"
|
||||
import { withActor } from "~/context/auth.withActor"
|
||||
import { Actor } from "@opencode-ai/console-core/actor.js"
|
||||
import { and, Database, eq, isNull } from "@opencode-ai/console-core/drizzle/index.js"
|
||||
@@ -50,7 +51,9 @@ export function WorkspacePicker() {
|
||||
const i18n = useI18n()
|
||||
const workspaces = createAsync(() => getWorkspaces())
|
||||
const submission = useSubmission(createWorkspace)
|
||||
const [showForm, setShowForm] = createSignal(false)
|
||||
const [store, setStore] = createStore({
|
||||
showForm: false,
|
||||
})
|
||||
let inputRef: HTMLInputElement | undefined
|
||||
|
||||
const currentWorkspace = () => {
|
||||
@@ -58,8 +61,12 @@ export function WorkspacePicker() {
|
||||
return ws ? ws.name : i18n.t("workspace.select")
|
||||
}
|
||||
|
||||
const handleWorkspaceNew = () => {
|
||||
setStore("showForm", true)
|
||||
}
|
||||
|
||||
createEffect(() => {
|
||||
if (showForm() && inputRef) {
|
||||
if (store.showForm && inputRef) {
|
||||
setTimeout(() => inputRef?.focus(), 0)
|
||||
}
|
||||
})
|
||||
@@ -72,7 +79,7 @@ export function WorkspacePicker() {
|
||||
// Reset signals when workspace ID changes
|
||||
createEffect(() => {
|
||||
params.id
|
||||
setShowForm(false)
|
||||
setStore("showForm", false)
|
||||
})
|
||||
|
||||
return (
|
||||
@@ -85,34 +92,32 @@ export function WorkspacePicker() {
|
||||
</DropdownItem>
|
||||
)}
|
||||
</For>
|
||||
<button data-slot="create-item" type="button" onClick={() => setShowForm(true)}>
|
||||
<button data-slot="create-item" type="button" onClick={() => handleWorkspaceNew()}>
|
||||
{i18n.t("workspace.createNew")}
|
||||
</button>
|
||||
</Dropdown>
|
||||
|
||||
<Modal open={showForm()} onClose={() => setShowForm(false)} title={i18n.t("workspace.modal.title")}>
|
||||
<div data-component="workspace-create-modal">
|
||||
<form data-slot="create-form" action={createWorkspace} method="post">
|
||||
<div data-slot="create-input-group">
|
||||
<input
|
||||
ref={inputRef}
|
||||
data-slot="create-input"
|
||||
type="text"
|
||||
name="workspaceName"
|
||||
placeholder={i18n.t("workspace.modal.placeholder")}
|
||||
required
|
||||
/>
|
||||
<div data-slot="button-group">
|
||||
<button type="button" data-color="ghost" onClick={() => setShowForm(false)}>
|
||||
{i18n.t("common.cancel")}
|
||||
</button>
|
||||
<button type="submit" data-color="primary" disabled={submission.pending}>
|
||||
{submission.pending ? i18n.t("common.creating") : i18n.t("common.create")}
|
||||
</button>
|
||||
</div>
|
||||
<Modal open={store.showForm} onClose={() => setStore("showForm", false)} title={i18n.t("workspace.modal.title")}>
|
||||
<form data-slot="create-form" action={createWorkspace} method="post">
|
||||
<div data-slot="create-input-group">
|
||||
<input
|
||||
ref={inputRef}
|
||||
data-slot="create-input"
|
||||
type="text"
|
||||
name="workspaceName"
|
||||
placeholder={i18n.t("workspace.modal.placeholder")}
|
||||
required
|
||||
/>
|
||||
<div data-slot="button-group">
|
||||
<button type="button" data-color="ghost" onClick={() => setStore("showForm", false)}>
|
||||
{i18n.t("common.cancel")}
|
||||
</button>
|
||||
<button type="submit" data-color="primary" disabled={submission.pending}>
|
||||
{submission.pending ? i18n.t("common.creating") : i18n.t("common.create")}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</Modal>
|
||||
</div>
|
||||
)
|
||||
|
||||
@@ -25,7 +25,6 @@
|
||||
|
||||
&:disabled {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
transform: none;
|
||||
}
|
||||
|
||||
|
||||
@@ -13,7 +13,6 @@ import styles from "./black-section.module.css"
|
||||
import waitlistStyles from "./black-waitlist-section.module.css"
|
||||
import { useI18n } from "~/context/i18n"
|
||||
import { formError } from "~/lib/form-error"
|
||||
import { blackResetTimeKeys, formatResetTime } from "~/lib/format-reset-time"
|
||||
|
||||
const querySubscription = query(async (workspaceID: string) => {
|
||||
"use server"
|
||||
@@ -53,6 +52,20 @@ const querySubscription = query(async (workspaceID: string) => {
|
||||
}, workspaceID)
|
||||
}, "subscription.get")
|
||||
|
||||
function formatResetTime(seconds: number, i18n: ReturnType<typeof useI18n>) {
|
||||
const days = Math.floor(seconds / 86400)
|
||||
if (days >= 1) {
|
||||
const hours = Math.floor((seconds % 86400) / 3600)
|
||||
return `${days} ${days === 1 ? i18n.t("workspace.black.time.day") : i18n.t("workspace.black.time.days")} ${hours} ${hours === 1 ? i18n.t("workspace.black.time.hour") : i18n.t("workspace.black.time.hours")}`
|
||||
}
|
||||
const hours = Math.floor(seconds / 3600)
|
||||
const minutes = Math.floor((seconds % 3600) / 60)
|
||||
if (hours >= 1)
|
||||
return `${hours} ${hours === 1 ? i18n.t("workspace.black.time.hour") : i18n.t("workspace.black.time.hours")} ${minutes} ${minutes === 1 ? i18n.t("workspace.black.time.minute") : i18n.t("workspace.black.time.minutes")}`
|
||||
if (minutes === 0) return i18n.t("workspace.black.time.fewSeconds")
|
||||
return `${minutes} ${minutes === 1 ? i18n.t("workspace.black.time.minute") : i18n.t("workspace.black.time.minutes")}`
|
||||
}
|
||||
|
||||
const cancelWaitlist = action(async (workspaceID: string) => {
|
||||
"use server"
|
||||
return json(
|
||||
@@ -196,7 +209,7 @@ export function BlackSection() {
|
||||
</div>
|
||||
<span data-slot="reset-time">
|
||||
{i18n.t("workspace.black.subscription.resetsIn")}{" "}
|
||||
{formatResetTime(sub().rollingUsage.resetInSec, i18n, blackResetTimeKeys)}
|
||||
{formatResetTime(sub().rollingUsage.resetInSec, i18n)}
|
||||
</span>
|
||||
</div>
|
||||
<div data-slot="usage-item">
|
||||
@@ -209,7 +222,7 @@ export function BlackSection() {
|
||||
</div>
|
||||
<span data-slot="reset-time">
|
||||
{i18n.t("workspace.black.subscription.resetsIn")}{" "}
|
||||
{formatResetTime(sub().weeklyUsage.resetInSec, i18n, blackResetTimeKeys)}
|
||||
{formatResetTime(sub().weeklyUsage.resetInSec, i18n)}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,17 +1,11 @@
|
||||
import { createAsync, useParams } from "@solidjs/router"
|
||||
import { Show } from "solid-js"
|
||||
import { IconGo } from "~/component/icon"
|
||||
import { GoReferralSection, queryGoReferral } from "~/component/go-referral"
|
||||
import { useI18n } from "~/context/i18n"
|
||||
import { useLanguage } from "~/context/language"
|
||||
import { LiteSection, queryLiteSubscription } from "./lite-section"
|
||||
import { LiteSection } from "./lite-section"
|
||||
|
||||
export default function () {
|
||||
const params = useParams()
|
||||
const i18n = useI18n()
|
||||
const language = useLanguage()
|
||||
const referral = createAsync(() => queryGoReferral(params.id!))
|
||||
const lite = createAsync(() => queryLiteSubscription(params.id!))
|
||||
|
||||
return (
|
||||
<div data-page="workspace-[id]">
|
||||
@@ -29,10 +23,7 @@ export default function () {
|
||||
</section>
|
||||
|
||||
<div data-slot="sections">
|
||||
<LiteSection lite={lite()} />
|
||||
<Show when={referral()} fallback={<section>{i18n.t("workspace.lite.loading")}</section>}>
|
||||
{(summary) => <GoReferralSection workspaceID={params.id!} summary={summary()} lite={lite()} />}
|
||||
</Show>
|
||||
<LiteSection />
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
||||
@@ -211,9 +211,7 @@
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
}
|
||||
}
|
||||
|
||||
.paymentMethodModal {
|
||||
[data-slot="modal-actions"] {
|
||||
display: flex;
|
||||
gap: var(--space-3);
|
||||
|
||||
@@ -14,11 +14,10 @@ import styles from "./lite-section.module.css"
|
||||
import { useI18n } from "~/context/i18n"
|
||||
import { useLanguage } from "~/context/language"
|
||||
import { formError } from "~/lib/form-error"
|
||||
import { formatResetTime, liteResetTimeKeys } from "~/lib/format-reset-time"
|
||||
|
||||
import { IconAlipay, IconUpi } from "~/component/icon"
|
||||
|
||||
export const queryLiteSubscription = query(async (workspaceID: string) => {
|
||||
const queryLiteSubscription = query(async (workspaceID: string) => {
|
||||
"use server"
|
||||
return withActor(async () => {
|
||||
const row = await Database.use((tx) =>
|
||||
@@ -68,7 +67,19 @@ export const queryLiteSubscription = query(async (workspaceID: string) => {
|
||||
}, workspaceID)
|
||||
}, "lite.subscription.get")
|
||||
|
||||
type LiteSubscription = Awaited<ReturnType<typeof queryLiteSubscription>>
|
||||
function formatResetTime(seconds: number, i18n: ReturnType<typeof useI18n>) {
|
||||
const days = Math.floor(seconds / 86400)
|
||||
if (days >= 1) {
|
||||
const hours = Math.floor((seconds % 86400) / 3600)
|
||||
return `${days} ${days === 1 ? i18n.t("workspace.lite.time.day") : i18n.t("workspace.lite.time.days")} ${hours} ${hours === 1 ? i18n.t("workspace.lite.time.hour") : i18n.t("workspace.lite.time.hours")}`
|
||||
}
|
||||
const hours = Math.floor(seconds / 3600)
|
||||
const minutes = Math.floor((seconds % 3600) / 60)
|
||||
if (hours >= 1)
|
||||
return `${hours} ${hours === 1 ? i18n.t("workspace.lite.time.hour") : i18n.t("workspace.lite.time.hours")} ${minutes} ${minutes === 1 ? i18n.t("workspace.lite.time.minute") : i18n.t("workspace.lite.time.minutes")}`
|
||||
if (minutes === 0) return i18n.t("workspace.lite.time.fewSeconds")
|
||||
return `${minutes} ${minutes === 1 ? i18n.t("workspace.lite.time.minute") : i18n.t("workspace.lite.time.minutes")}`
|
||||
}
|
||||
|
||||
const createLiteCheckoutUrl = action(
|
||||
async (workspaceID: string, successUrl: string, cancelUrl: string, method?: "alipay" | "upi") => {
|
||||
@@ -129,32 +140,13 @@ const setLiteUseBalance = action(async (form: FormData) => {
|
||||
)
|
||||
}, "setLiteUseBalance")
|
||||
|
||||
function LiteUsageItem(props: { label: string; usage: { usagePercent: number; resetInSec: number } }) {
|
||||
const i18n = useI18n()
|
||||
|
||||
return (
|
||||
<div data-slot="usage-item">
|
||||
<div data-slot="usage-header">
|
||||
<span data-slot="usage-label">{props.label}</span>
|
||||
<span data-slot="usage-value">{props.usage.usagePercent}%</span>
|
||||
</div>
|
||||
<div data-slot="progress">
|
||||
<div data-slot="progress-bar" style={{ width: `${props.usage.usagePercent}%` }} />
|
||||
</div>
|
||||
<span data-slot="reset-time">
|
||||
{i18n.t("workspace.lite.subscription.resetsIn")}{" "}
|
||||
{formatResetTime(props.usage.resetInSec, i18n, liteResetTimeKeys)}
|
||||
</span>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export function LiteSection(props: { lite: LiteSubscription | undefined }) {
|
||||
export function LiteSection() {
|
||||
const params = useParams()
|
||||
const i18n = useI18n()
|
||||
const language = useLanguage()
|
||||
const billingInfo = createAsync(() => queryBillingInfo(params.id!))
|
||||
const isBlack = createMemo(() => billingInfo()?.subscriptionID || billingInfo()?.timeSubscriptionBooked)
|
||||
const lite = createAsync(() => queryLiteSubscription(params.id!))
|
||||
const sessionAction = useAction(createSessionUrl)
|
||||
const sessionSubmission = useSubmission(createSessionUrl)
|
||||
const checkoutAction = useAction(createLiteCheckoutUrl)
|
||||
@@ -194,7 +186,7 @@ export function LiteSection(props: { lite: LiteSubscription | undefined }) {
|
||||
<p data-slot="other-message">{i18n.t("workspace.lite.black.message")}</p>
|
||||
</section>
|
||||
</Show>
|
||||
<Show when={!isBlack() && props.lite && props.lite.mine && props.lite}>
|
||||
<Show when={!isBlack() && lite() && lite()!.mine && lite()!}>
|
||||
{(sub) => (
|
||||
<section class={styles.root}>
|
||||
<div data-slot="section-title">
|
||||
@@ -215,9 +207,44 @@ export function LiteSection(props: { lite: LiteSubscription | undefined }) {
|
||||
.
|
||||
</div>
|
||||
<div data-slot="usage">
|
||||
<LiteUsageItem label={i18n.t("workspace.lite.subscription.rollingUsage")} usage={sub().rollingUsage} />
|
||||
<LiteUsageItem label={i18n.t("workspace.lite.subscription.weeklyUsage")} usage={sub().weeklyUsage} />
|
||||
<LiteUsageItem label={i18n.t("workspace.lite.subscription.monthlyUsage")} usage={sub().monthlyUsage} />
|
||||
<div data-slot="usage-item">
|
||||
<div data-slot="usage-header">
|
||||
<span data-slot="usage-label">{i18n.t("workspace.lite.subscription.rollingUsage")}</span>
|
||||
<span data-slot="usage-value">{sub().rollingUsage.usagePercent}%</span>
|
||||
</div>
|
||||
<div data-slot="progress">
|
||||
<div data-slot="progress-bar" style={{ width: `${sub().rollingUsage.usagePercent}%` }} />
|
||||
</div>
|
||||
<span data-slot="reset-time">
|
||||
{i18n.t("workspace.lite.subscription.resetsIn")}{" "}
|
||||
{formatResetTime(sub().rollingUsage.resetInSec, i18n)}
|
||||
</span>
|
||||
</div>
|
||||
<div data-slot="usage-item">
|
||||
<div data-slot="usage-header">
|
||||
<span data-slot="usage-label">{i18n.t("workspace.lite.subscription.weeklyUsage")}</span>
|
||||
<span data-slot="usage-value">{sub().weeklyUsage.usagePercent}%</span>
|
||||
</div>
|
||||
<div data-slot="progress">
|
||||
<div data-slot="progress-bar" style={{ width: `${sub().weeklyUsage.usagePercent}%` }} />
|
||||
</div>
|
||||
<span data-slot="reset-time">
|
||||
{i18n.t("workspace.lite.subscription.resetsIn")} {formatResetTime(sub().weeklyUsage.resetInSec, i18n)}
|
||||
</span>
|
||||
</div>
|
||||
<div data-slot="usage-item">
|
||||
<div data-slot="usage-header">
|
||||
<span data-slot="usage-label">{i18n.t("workspace.lite.subscription.monthlyUsage")}</span>
|
||||
<span data-slot="usage-value">{sub().monthlyUsage.usagePercent}%</span>
|
||||
</div>
|
||||
<div data-slot="progress">
|
||||
<div data-slot="progress-bar" style={{ width: `${sub().monthlyUsage.usagePercent}%` }} />
|
||||
</div>
|
||||
<span data-slot="reset-time">
|
||||
{i18n.t("workspace.lite.subscription.resetsIn")}{" "}
|
||||
{formatResetTime(sub().monthlyUsage.resetInSec, i18n)}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<form action={setLiteUseBalance} method="post" data-slot="setting-row">
|
||||
<p>{i18n.t("workspace.lite.subscription.useBalance")}</p>
|
||||
@@ -236,12 +263,12 @@ export function LiteSection(props: { lite: LiteSubscription | undefined }) {
|
||||
</section>
|
||||
)}
|
||||
</Show>
|
||||
<Show when={!isBlack() && props.lite && !props.lite.mine}>
|
||||
<Show when={!isBlack() && lite() && !lite()!.mine}>
|
||||
<section class={styles.root}>
|
||||
<p data-slot="other-message">{i18n.t("workspace.lite.other.message")}</p>
|
||||
</section>
|
||||
</Show>
|
||||
<Show when={!isBlack() && props.lite === null}>
|
||||
<Show when={!isBlack() && lite() === null}>
|
||||
<section class={styles.root}>
|
||||
<p data-slot="promo-description">
|
||||
<For
|
||||
@@ -303,33 +330,31 @@ export function LiteSection(props: { lite: LiteSubscription | undefined }) {
|
||||
onClose={() => setStore("showModal", false)}
|
||||
title={i18n.t("workspace.lite.promo.selectMethod")}
|
||||
>
|
||||
<div class={styles.paymentMethodModal}>
|
||||
<div data-slot="modal-actions">
|
||||
<button
|
||||
type="button"
|
||||
data-slot="method-button"
|
||||
data-color="ghost"
|
||||
disabled={checkoutSubmission.pending || busy()}
|
||||
onClick={() => onClickSubscribe("alipay")}
|
||||
>
|
||||
<Show when={store.loading !== "alipay"}>
|
||||
<IconAlipay style={{ width: "24px", height: "24px" }} />
|
||||
</Show>
|
||||
{store.loading === "alipay" ? i18n.t("workspace.lite.promo.subscribing") : "Alipay"}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
data-slot="method-button"
|
||||
data-color="ghost"
|
||||
disabled={checkoutSubmission.pending || busy()}
|
||||
onClick={() => onClickSubscribe("upi")}
|
||||
>
|
||||
<Show when={store.loading !== "upi"}>
|
||||
<IconUpi style={{ width: "auto", height: "16px" }} />
|
||||
</Show>
|
||||
{store.loading === "upi" ? i18n.t("workspace.lite.promo.subscribing") : "UPI"}
|
||||
</button>
|
||||
</div>
|
||||
<div data-slot="modal-actions">
|
||||
<button
|
||||
type="button"
|
||||
data-slot="method-button"
|
||||
data-color="ghost"
|
||||
disabled={checkoutSubmission.pending || busy()}
|
||||
onClick={() => onClickSubscribe("alipay")}
|
||||
>
|
||||
<Show when={store.loading !== "alipay"}>
|
||||
<IconAlipay style={{ width: "24px", height: "24px" }} />
|
||||
</Show>
|
||||
{store.loading === "alipay" ? i18n.t("workspace.lite.promo.subscribing") : "Alipay"}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
data-slot="method-button"
|
||||
data-color="ghost"
|
||||
disabled={checkoutSubmission.pending || busy()}
|
||||
onClick={() => onClickSubscribe("upi")}
|
||||
>
|
||||
<Show when={store.loading !== "upi"}>
|
||||
<IconUpi style={{ width: "auto", height: "16px" }} />
|
||||
</Show>
|
||||
{store.loading === "upi" ? i18n.t("workspace.lite.promo.subscribing") : "UPI"}
|
||||
</button>
|
||||
</div>
|
||||
</Modal>
|
||||
</section>
|
||||
|
||||
@@ -10,7 +10,6 @@ import {
|
||||
IconAnthropic,
|
||||
IconArcee,
|
||||
IconGemini,
|
||||
IconDeepSeek,
|
||||
IconMiniMax,
|
||||
IconMoonshotAI,
|
||||
IconNvidia,
|
||||
@@ -28,7 +27,6 @@ const getModelLab = (modelId: string) => {
|
||||
if (modelId.startsWith("claude")) return "Anthropic"
|
||||
if (modelId.startsWith("gpt")) return "OpenAI"
|
||||
if (modelId.startsWith("gemini")) return "Google"
|
||||
if (modelId.startsWith("deepseek")) return "DeepSeek"
|
||||
if (modelId.startsWith("kimi")) return "Moonshot AI"
|
||||
if (modelId.startsWith("glm")) return "Z.ai"
|
||||
if (modelId.startsWith("qwen")) return "Alibaba"
|
||||
@@ -49,19 +47,7 @@ const getModelsInfo = query(async (workspaceID: string) => {
|
||||
.filter(([id, _model]) => !id.startsWith("alpha-"))
|
||||
.filter(([id, _model]) => !id.endsWith(":global"))
|
||||
.sort(([idA, modelA], [idB, modelB]) => {
|
||||
const priority = [
|
||||
"big-pickle",
|
||||
"claude",
|
||||
"gpt",
|
||||
"gemini",
|
||||
"deepseek",
|
||||
"glm",
|
||||
"kimi",
|
||||
"qwen",
|
||||
"grok",
|
||||
"minimax",
|
||||
"mimo",
|
||||
]
|
||||
const priority = ["big-pickle", "minimax", "grok", "claude", "gpt", "gemini"]
|
||||
const getPriority = (id: string) => {
|
||||
const index = priority.findIndex((p) => id.startsWith(p))
|
||||
return index === -1 ? Infinity : index
|
||||
@@ -150,8 +136,6 @@ export function ModelSection() {
|
||||
return <IconAnthropic width={16} height={16} />
|
||||
case "Google":
|
||||
return <IconGemini width={16} height={16} />
|
||||
case "DeepSeek":
|
||||
return <IconDeepSeek width={16} height={16} />
|
||||
case "Moonshot AI":
|
||||
return <IconMoonshotAI width={16} height={16} />
|
||||
case "Z.ai":
|
||||
|
||||
@@ -170,10 +170,6 @@ export async function handler(
|
||||
if (v === "$ip") return [[k, ip]]
|
||||
if (v === "$workspace") return authInfo?.workspaceID ? [[k, authInfo?.workspaceID]] : []
|
||||
if (v === "$session") return sessionId ? [[k, sessionId]] : []
|
||||
if (v === "$user") {
|
||||
const user = sessionId ?? authInfo?.workspaceID ?? ip
|
||||
return user ? [[k, user]] : []
|
||||
}
|
||||
if (v.startsWith("$header.")) {
|
||||
const headerValue = input.request.headers.get(v.slice(8))
|
||||
return headerValue ? [[k, headerValue]] : []
|
||||
|
||||
@@ -13,7 +13,7 @@ export function createRateLimiter(
|
||||
if (!zenApiKey) return
|
||||
const dict = i18n(localeFromRequest(request))
|
||||
|
||||
const LIMIT = rateLimit ?? 1000
|
||||
const LIMIT = rateLimit ?? 500
|
||||
const yyyyMMddHHmm = new Date(Date.now())
|
||||
.toISOString()
|
||||
.replace(/[^0-9]/g, "")
|
||||
|
||||
@@ -1,47 +0,0 @@
|
||||
CREATE TABLE `referral_code` (
|
||||
`id` varchar(30) NOT NULL,
|
||||
`workspace_id` varchar(30) NOT NULL,
|
||||
`time_created` timestamp(3) NOT NULL DEFAULT (now()),
|
||||
`time_updated` timestamp(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3),
|
||||
`time_deleted` timestamp(3),
|
||||
`code` varchar(10) NOT NULL,
|
||||
CONSTRAINT PRIMARY KEY(`workspace_id`,`id`),
|
||||
CONSTRAINT `referral_code_workspace_id` UNIQUE INDEX(`workspace_id`),
|
||||
CONSTRAINT `referral_code_code` UNIQUE INDEX(`code`)
|
||||
);
|
||||
--> statement-breakpoint
|
||||
CREATE TABLE `referral_reward` (
|
||||
`id` varchar(30) NOT NULL,
|
||||
`workspace_id` varchar(30) NOT NULL,
|
||||
`time_created` timestamp(3) NOT NULL DEFAULT (now()),
|
||||
`time_updated` timestamp(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3),
|
||||
`time_deleted` timestamp(3),
|
||||
`referral_id` varchar(30) NOT NULL,
|
||||
`source` enum('inviter','invitee') NOT NULL,
|
||||
`amount` bigint NOT NULL,
|
||||
`applied_by_user_id` varchar(30),
|
||||
`time_applied` timestamp(3),
|
||||
CONSTRAINT PRIMARY KEY(`workspace_id`,`id`),
|
||||
CONSTRAINT `referral_reward_referral_source` UNIQUE INDEX(`referral_id`,`source`)
|
||||
);
|
||||
--> statement-breakpoint
|
||||
CREATE TABLE `referral` (
|
||||
`id` varchar(30) NOT NULL,
|
||||
`workspace_id` varchar(30) NOT NULL,
|
||||
`time_created` timestamp(3) NOT NULL DEFAULT (now()),
|
||||
`time_updated` timestamp(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3),
|
||||
`time_deleted` timestamp(3),
|
||||
`inviter_workspace_id` varchar(30) NOT NULL,
|
||||
`invitee_account_id` varchar(30) NOT NULL,
|
||||
`invitee_user_id` varchar(30) NOT NULL,
|
||||
`referral_code_id` varchar(30) NOT NULL,
|
||||
`stripe_customer_id` varchar(255) NOT NULL,
|
||||
`stripe_subscription_id` varchar(255) NOT NULL,
|
||||
CONSTRAINT PRIMARY KEY(`workspace_id`,`id`),
|
||||
CONSTRAINT `referral_invitee_account_id` UNIQUE INDEX(`invitee_account_id`),
|
||||
CONSTRAINT `referral_stripe_subscription_id` UNIQUE INDEX(`stripe_subscription_id`)
|
||||
);
|
||||
--> statement-breakpoint
|
||||
CREATE INDEX `referral_reward_workspace_time` ON `referral_reward` (`workspace_id`,`time_created`);--> statement-breakpoint
|
||||
CREATE INDEX `referral_inviter_workspace_id` ON `referral` (`inviter_workspace_id`);--> statement-breakpoint
|
||||
CREATE INDEX `referral_code_id` ON `referral` (`referral_code_id`);
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,20 +0,0 @@
|
||||
DROP TABLE `referral_code`;--> statement-breakpoint
|
||||
DROP INDEX `referral_reward_referral_source` ON `referral_reward`;--> statement-breakpoint
|
||||
DROP INDEX `referral_stripe_subscription_id` ON `referral`;--> statement-breakpoint
|
||||
DROP INDEX `referral_inviter_workspace_id` ON `referral`;--> statement-breakpoint
|
||||
DROP INDEX `referral_code_id` ON `referral`;--> statement-breakpoint
|
||||
ALTER TABLE `referral_reward` DROP PRIMARY KEY;--> statement-breakpoint
|
||||
ALTER TABLE `referral` DROP PRIMARY KEY;--> statement-breakpoint
|
||||
ALTER TABLE `referral_reward` MODIFY COLUMN `workspace_id` varchar(30);--> statement-breakpoint
|
||||
ALTER TABLE `workspace` ADD `referral_code` varchar(16);--> statement-breakpoint
|
||||
ALTER TABLE `referral_reward` ADD PRIMARY KEY (`id`);--> statement-breakpoint
|
||||
ALTER TABLE `referral` ADD PRIMARY KEY (`id`);--> statement-breakpoint
|
||||
CREATE INDEX `referral_workspace_id` ON `referral` (`workspace_id`);--> statement-breakpoint
|
||||
CREATE UNIQUE INDEX `workspace_referral_code` ON `workspace` (`referral_code`);--> statement-breakpoint
|
||||
ALTER TABLE `referral_reward` DROP COLUMN `source`;--> statement-breakpoint
|
||||
ALTER TABLE `referral_reward` DROP COLUMN `applied_by_user_id`;--> statement-breakpoint
|
||||
ALTER TABLE `referral` DROP COLUMN `inviter_workspace_id`;--> statement-breakpoint
|
||||
ALTER TABLE `referral` DROP COLUMN `invitee_user_id`;--> statement-breakpoint
|
||||
ALTER TABLE `referral` DROP COLUMN `referral_code_id`;--> statement-breakpoint
|
||||
ALTER TABLE `referral` DROP COLUMN `stripe_customer_id`;--> statement-breakpoint
|
||||
ALTER TABLE `referral` DROP COLUMN `stripe_subscription_id`;
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,4 +0,0 @@
|
||||
DROP INDEX `referral_reward_workspace_time` ON `referral_reward`;--> statement-breakpoint
|
||||
ALTER TABLE `referral_reward` DROP PRIMARY KEY;--> statement-breakpoint
|
||||
ALTER TABLE `referral_reward` ADD PRIMARY KEY (`workspace_id`,`referral_id`);--> statement-breakpoint
|
||||
ALTER TABLE `referral_reward` DROP COLUMN `id`;
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,3 +0,0 @@
|
||||
DROP INDEX `referral_workspace_id` ON `referral`;--> statement-breakpoint
|
||||
ALTER TABLE `referral` DROP PRIMARY KEY;--> statement-breakpoint
|
||||
ALTER TABLE `referral` ADD PRIMARY KEY (`workspace_id`,`id`);
|
||||
-3013
File diff suppressed because it is too large
Load Diff
@@ -1,2 +0,0 @@
|
||||
UPDATE `workspace` SET `referral_code` = NULL WHERE CHAR_LENGTH(`referral_code`) > 10;--> statement-breakpoint
|
||||
ALTER TABLE `workspace` MODIFY COLUMN `referral_code` varchar(10);
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,2 +0,0 @@
|
||||
DROP INDEX `workspace_referral_code` ON `workspace`;--> statement-breakpoint
|
||||
CREATE UNIQUE INDEX `referral_code` ON `workspace` (`referral_code`);
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,13 +0,0 @@
|
||||
CREATE TABLE `referral_code` (
|
||||
`workspace_id` varchar(30) PRIMARY KEY,
|
||||
`code` varchar(10) NOT NULL,
|
||||
`time_created` timestamp(3) NOT NULL DEFAULT (now()),
|
||||
`time_updated` timestamp(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3),
|
||||
`time_deleted` timestamp(3),
|
||||
CONSTRAINT `code` UNIQUE INDEX(`code`)
|
||||
);
|
||||
--> statement-breakpoint
|
||||
DROP INDEX `referral_invitee_account_id` ON `referral`;--> statement-breakpoint
|
||||
DROP INDEX `referral_code` ON `workspace`;--> statement-breakpoint
|
||||
CREATE UNIQUE INDEX `invitee_account_id` ON `referral` (`invitee_account_id`);--> statement-breakpoint
|
||||
ALTER TABLE `workspace` DROP COLUMN `referral_code`;
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/package.json",
|
||||
"name": "@opencode-ai/console-core",
|
||||
"version": "1.15.6",
|
||||
"version": "1.15.5",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
|
||||
@@ -155,26 +155,6 @@ export namespace Billing {
|
||||
return amountInMicroCents
|
||||
}
|
||||
|
||||
export const subtractLiteUsage = async (workspaceID: string, amountInMicroCents: number) => {
|
||||
await Database.transaction(async (tx) => {
|
||||
const lite = await tx
|
||||
.select({ id: LiteTable.id })
|
||||
.from(LiteTable)
|
||||
.where(and(eq(LiteTable.workspaceID, workspaceID), isNull(LiteTable.timeDeleted)))
|
||||
.then((rows) => rows[0])
|
||||
if (!lite) throw new Error("Subscribe to Go before applying referral rewards")
|
||||
|
||||
await tx
|
||||
.update(LiteTable)
|
||||
.set({
|
||||
monthlyUsage: sql`GREATEST(0, COALESCE(${LiteTable.monthlyUsage}, 0) - ${amountInMicroCents})`,
|
||||
weeklyUsage: sql`GREATEST(0, COALESCE(${LiteTable.weeklyUsage}, 0) - ${amountInMicroCents})`,
|
||||
rollingUsage: sql`GREATEST(0, COALESCE(${LiteTable.rollingUsage}, 0) - ${amountInMicroCents})`,
|
||||
})
|
||||
.where(and(eq(LiteTable.workspaceID, workspaceID), isNull(LiteTable.timeDeleted)))
|
||||
})
|
||||
}
|
||||
|
||||
export const redeemCoupon = async (email: string, type: (typeof CouponType)[number]) => {
|
||||
// validate coupon type
|
||||
await (async () => {
|
||||
|
||||
@@ -12,7 +12,6 @@ export namespace Identifier {
|
||||
model: "mod",
|
||||
payment: "pay",
|
||||
provider: "prv",
|
||||
referral: "ref",
|
||||
subscription: "sub",
|
||||
usage: "usg",
|
||||
user: "usr",
|
||||
|
||||
@@ -1,390 +0,0 @@
|
||||
import { z } from "zod"
|
||||
import { and, asc, eq, isNull, sql, Database } from "./drizzle"
|
||||
import { Actor } from "./actor"
|
||||
import { Identifier } from "./identifier"
|
||||
import { LiteTable } from "./schema/billing.sql"
|
||||
import { ReferralCodeTable, ReferralRewardTable, ReferralTable } from "./schema/referral.sql"
|
||||
import { AuthTable } from "./schema/auth.sql"
|
||||
import { UserTable } from "./schema/user.sql"
|
||||
import { WorkspaceTable } from "./schema/workspace.sql"
|
||||
import { centsToMicroCents, microCentsToCents } from "./util/price"
|
||||
import { fn } from "./util/fn"
|
||||
import { Billing } from "./billing"
|
||||
import { LiteData } from "./lite"
|
||||
import { Subscription } from "./subscription"
|
||||
import { ulid } from "ulid"
|
||||
|
||||
export namespace Referral {
|
||||
export const REWARD_AMOUNT = centsToMicroCents(500)
|
||||
export const CODE_LENGTH = 10
|
||||
|
||||
export function normalizeCode(code?: string | null) {
|
||||
return code
|
||||
?.toUpperCase()
|
||||
.replace(/[^A-Z0-9]/g, "")
|
||||
.slice(0, CODE_LENGTH)
|
||||
}
|
||||
|
||||
function generateCode() {
|
||||
return ulid().slice(-CODE_LENGTH).toUpperCase()
|
||||
}
|
||||
|
||||
async function ensureCode(workspaceID = Actor.workspace()) {
|
||||
return Database.use(async (db) => {
|
||||
const existing = await db
|
||||
.select({ code: ReferralCodeTable.code })
|
||||
.from(ReferralCodeTable)
|
||||
.where(eq(ReferralCodeTable.workspaceID, workspaceID))
|
||||
.then((rows) => rows[0])
|
||||
if (existing) return { code: existing.code }
|
||||
|
||||
await db.insert(ReferralCodeTable).ignore().values({
|
||||
workspaceID,
|
||||
code: generateCode(),
|
||||
})
|
||||
|
||||
const created = await db
|
||||
.select({ code: ReferralCodeTable.code })
|
||||
.from(ReferralCodeTable)
|
||||
.where(eq(ReferralCodeTable.workspaceID, workspaceID))
|
||||
.then((rows) => rows[0])
|
||||
if (created) return { code: created.code }
|
||||
|
||||
throw new Error("Failed to generate referral code")
|
||||
})
|
||||
}
|
||||
|
||||
export const summary = fn(z.void(), async () => {
|
||||
const workspaceID = Actor.workspace()
|
||||
const accountID = Actor.account()
|
||||
const code = await ensureCode(workspaceID)
|
||||
const rows = await Database.use(async (tx) => {
|
||||
const [rewards, invites, inviteeReferral, inviteeRewards] = await Promise.all([
|
||||
tx
|
||||
.select({
|
||||
referralID: ReferralRewardTable.referralID,
|
||||
workspaceID: ReferralRewardTable.workspaceID,
|
||||
referralWorkspaceID: ReferralTable.workspaceID,
|
||||
inviteeEmail: AuthTable.subject,
|
||||
amount: ReferralRewardTable.amount,
|
||||
timeCreated: ReferralRewardTable.timeCreated,
|
||||
timeApplied: ReferralRewardTable.timeApplied,
|
||||
})
|
||||
.from(ReferralRewardTable)
|
||||
.innerJoin(ReferralTable, eq(ReferralTable.id, ReferralRewardTable.referralID))
|
||||
.innerJoin(
|
||||
AuthTable,
|
||||
and(eq(AuthTable.accountID, ReferralTable.inviteeAccountID), eq(AuthTable.provider, "email")),
|
||||
)
|
||||
.where(
|
||||
and(
|
||||
eq(ReferralRewardTable.workspaceID, workspaceID),
|
||||
isNull(ReferralRewardTable.timeDeleted),
|
||||
isNull(ReferralTable.timeDeleted),
|
||||
),
|
||||
),
|
||||
tx
|
||||
.select({ id: ReferralTable.id, inviteeEmail: AuthTable.subject, timeCreated: ReferralTable.timeCreated })
|
||||
.from(ReferralTable)
|
||||
.innerJoin(
|
||||
AuthTable,
|
||||
and(eq(AuthTable.accountID, ReferralTable.inviteeAccountID), eq(AuthTable.provider, "email")),
|
||||
)
|
||||
.where(and(eq(ReferralTable.workspaceID, workspaceID), isNull(ReferralTable.timeDeleted))),
|
||||
tx
|
||||
.select({ id: ReferralTable.id, inviterEmail: AuthTable.subject, timeCreated: ReferralTable.timeCreated })
|
||||
.from(ReferralTable)
|
||||
.leftJoin(
|
||||
UserTable,
|
||||
and(
|
||||
eq(UserTable.workspaceID, ReferralTable.workspaceID),
|
||||
eq(UserTable.role, "admin"),
|
||||
isNull(UserTable.timeDeleted),
|
||||
),
|
||||
)
|
||||
.leftJoin(AuthTable, and(eq(AuthTable.accountID, UserTable.accountID), eq(AuthTable.provider, "email")))
|
||||
.where(and(eq(ReferralTable.inviteeAccountID, accountID), isNull(ReferralTable.timeDeleted)))
|
||||
.orderBy(asc(UserTable.timeCreated))
|
||||
.then((rows) => rows.find((row) => row.inviterEmail) ?? rows[0]),
|
||||
tx
|
||||
.select({ referralID: ReferralRewardTable.referralID })
|
||||
.from(ReferralRewardTable)
|
||||
.innerJoin(ReferralTable, eq(ReferralTable.id, ReferralRewardTable.referralID))
|
||||
.where(
|
||||
and(
|
||||
eq(ReferralTable.inviteeAccountID, accountID),
|
||||
isNull(ReferralRewardTable.timeDeleted),
|
||||
isNull(ReferralTable.timeDeleted),
|
||||
),
|
||||
),
|
||||
])
|
||||
|
||||
return { inviteeReferral, inviteeRewards, invites, rewards }
|
||||
})
|
||||
|
||||
const rewardReferralIDs = new Set(rows.rewards.map((reward) => reward.referralID))
|
||||
const inviteeRewardReferralIDs = new Set(rows.inviteeRewards.map((reward) => reward.referralID))
|
||||
const rewards = rows.rewards.map((reward) => {
|
||||
const source = reward.workspaceID === reward.referralWorkspaceID ? ("inviter" as const) : ("invitee" as const)
|
||||
return {
|
||||
id: reward.referralID,
|
||||
source,
|
||||
status: reward.timeApplied ? ("applied" as const) : ("available" as const),
|
||||
email: source === "invitee" ? (rows.inviteeReferral?.inviterEmail ?? null) : reward.inviteeEmail,
|
||||
amount: microCentsToCents(reward.amount),
|
||||
timeCreated: reward.timeCreated,
|
||||
timeApplied: reward.timeApplied,
|
||||
}
|
||||
})
|
||||
const pending = [
|
||||
...rows.invites
|
||||
.filter((referral) => !rewardReferralIDs.has(referral.id))
|
||||
.map((referral) => ({
|
||||
id: `${referral.id}:inviter`,
|
||||
source: "inviter" as const,
|
||||
status: "pending" as const,
|
||||
email: referral.inviteeEmail,
|
||||
amount: microCentsToCents(REWARD_AMOUNT),
|
||||
timeCreated: referral.timeCreated,
|
||||
timeApplied: null,
|
||||
})),
|
||||
...(rows.inviteeReferral && !inviteeRewardReferralIDs.has(rows.inviteeReferral.id)
|
||||
? [
|
||||
{
|
||||
id: `${rows.inviteeReferral.id}:invitee`,
|
||||
source: "invitee" as const,
|
||||
status: "pending" as const,
|
||||
email: rows.inviteeReferral.inviterEmail,
|
||||
amount: microCentsToCents(REWARD_AMOUNT),
|
||||
timeCreated: rows.inviteeReferral.timeCreated,
|
||||
timeApplied: null,
|
||||
},
|
||||
]
|
||||
: []),
|
||||
]
|
||||
const allRewards = [...pending, ...rewards].sort(
|
||||
(a, b) => new Date(b.timeCreated).getTime() - new Date(a.timeCreated).getTime(),
|
||||
)
|
||||
return {
|
||||
referralCode: code.code,
|
||||
hasReferral: allRewards.length > 0,
|
||||
rewardAmount: microCentsToCents(REWARD_AMOUNT),
|
||||
rewards: allRewards,
|
||||
}
|
||||
})
|
||||
|
||||
export const applyReward = fn(z.object({ referralID: z.string() }), async (input) => {
|
||||
const workspaceID = Actor.workspace()
|
||||
|
||||
return Database.transaction(async (tx) => {
|
||||
const reward = await tx
|
||||
.select({ amount: ReferralRewardTable.amount, timeApplied: ReferralRewardTable.timeApplied })
|
||||
.from(ReferralRewardTable)
|
||||
.where(
|
||||
and(
|
||||
eq(ReferralRewardTable.workspaceID, workspaceID),
|
||||
eq(ReferralRewardTable.referralID, input.referralID),
|
||||
isNull(ReferralRewardTable.timeDeleted),
|
||||
),
|
||||
)
|
||||
.then((rows) => rows[0])
|
||||
if (!reward) throw new Error("Referral reward not found")
|
||||
if (reward.timeApplied) throw new Error("Referral reward already applied")
|
||||
|
||||
const update = await tx
|
||||
.update(ReferralRewardTable)
|
||||
.set({
|
||||
timeApplied: sql`now()`,
|
||||
})
|
||||
.where(
|
||||
and(
|
||||
eq(ReferralRewardTable.workspaceID, workspaceID),
|
||||
eq(ReferralRewardTable.referralID, input.referralID),
|
||||
isNull(ReferralRewardTable.timeApplied),
|
||||
isNull(ReferralRewardTable.timeDeleted),
|
||||
),
|
||||
)
|
||||
if (update.rowsAffected === 0) throw new Error("Referral reward already applied")
|
||||
|
||||
await Billing.subtractLiteUsage(workspaceID, reward.amount)
|
||||
|
||||
return { amount: microCentsToCents(reward.amount) }
|
||||
})
|
||||
})
|
||||
|
||||
export const usagePreview = fn(z.object({ referralID: z.string() }), async (input) => {
|
||||
const row = await Database.use((tx) =>
|
||||
tx
|
||||
.select({
|
||||
rewardAmount: ReferralRewardTable.amount,
|
||||
rollingUsage: LiteTable.rollingUsage,
|
||||
weeklyUsage: LiteTable.weeklyUsage,
|
||||
monthlyUsage: LiteTable.monthlyUsage,
|
||||
timeRollingUpdated: LiteTable.timeRollingUpdated,
|
||||
timeWeeklyUpdated: LiteTable.timeWeeklyUpdated,
|
||||
timeMonthlyUpdated: LiteTable.timeMonthlyUpdated,
|
||||
timeCreated: LiteTable.timeCreated,
|
||||
})
|
||||
.from(ReferralRewardTable)
|
||||
.innerJoin(LiteTable, eq(LiteTable.workspaceID, ReferralRewardTable.workspaceID))
|
||||
.where(
|
||||
and(
|
||||
eq(ReferralRewardTable.workspaceID, Actor.workspace()),
|
||||
eq(ReferralRewardTable.referralID, input.referralID),
|
||||
isNull(ReferralRewardTable.timeApplied),
|
||||
isNull(ReferralRewardTable.timeDeleted),
|
||||
isNull(LiteTable.timeDeleted),
|
||||
),
|
||||
)
|
||||
.then((rows) => rows[0]),
|
||||
)
|
||||
if (!row) return null
|
||||
|
||||
const limits = LiteData.getLimits()
|
||||
return {
|
||||
rollingUsage: usagePreviewItem(
|
||||
Subscription.analyzeRollingUsage({
|
||||
limit: limits.rollingLimit,
|
||||
window: limits.rollingWindow,
|
||||
usage: row.rollingUsage ?? 0,
|
||||
timeUpdated: row.timeRollingUpdated ?? new Date(),
|
||||
}),
|
||||
Subscription.analyzeRollingUsage({
|
||||
limit: limits.rollingLimit,
|
||||
window: limits.rollingWindow,
|
||||
usage: Math.max(0, (row.rollingUsage ?? 0) - row.rewardAmount),
|
||||
timeUpdated: row.timeRollingUpdated ?? new Date(),
|
||||
}),
|
||||
),
|
||||
weeklyUsage: usagePreviewItem(
|
||||
Subscription.analyzeWeeklyUsage({
|
||||
limit: limits.weeklyLimit,
|
||||
usage: row.weeklyUsage ?? 0,
|
||||
timeUpdated: row.timeWeeklyUpdated ?? new Date(),
|
||||
}),
|
||||
Subscription.analyzeWeeklyUsage({
|
||||
limit: limits.weeklyLimit,
|
||||
usage: Math.max(0, (row.weeklyUsage ?? 0) - row.rewardAmount),
|
||||
timeUpdated: row.timeWeeklyUpdated ?? new Date(),
|
||||
}),
|
||||
),
|
||||
monthlyUsage: usagePreviewItem(
|
||||
Subscription.analyzeMonthlyUsage({
|
||||
limit: limits.monthlyLimit,
|
||||
usage: row.monthlyUsage ?? 0,
|
||||
timeUpdated: row.timeMonthlyUpdated ?? new Date(),
|
||||
timeSubscribed: row.timeCreated,
|
||||
}),
|
||||
Subscription.analyzeMonthlyUsage({
|
||||
limit: limits.monthlyLimit,
|
||||
usage: Math.max(0, (row.monthlyUsage ?? 0) - row.rewardAmount),
|
||||
timeUpdated: row.timeMonthlyUpdated ?? new Date(),
|
||||
timeSubscribed: row.timeCreated,
|
||||
}),
|
||||
),
|
||||
}
|
||||
})
|
||||
|
||||
export async function createFromAccount(input: { accountID: string; referralCode?: string }) {
|
||||
const referralCode = normalizeCode(input.referralCode)
|
||||
if (!referralCode) return
|
||||
|
||||
return Database.transaction(async (tx) => {
|
||||
const code = await tx
|
||||
.select({ workspaceID: ReferralCodeTable.workspaceID })
|
||||
.from(ReferralCodeTable)
|
||||
.innerJoin(WorkspaceTable, eq(WorkspaceTable.id, ReferralCodeTable.workspaceID))
|
||||
.where(and(eq(ReferralCodeTable.code, referralCode), isNull(WorkspaceTable.timeDeleted)))
|
||||
.then((rows) => rows[0])
|
||||
if (!code) throw new Error("Referral code invalid")
|
||||
|
||||
const existingReferral = await tx
|
||||
.select({ id: ReferralTable.id })
|
||||
.from(ReferralTable)
|
||||
.where(and(eq(ReferralTable.inviteeAccountID, input.accountID), isNull(ReferralTable.timeDeleted)))
|
||||
.then((rows) => rows[0])
|
||||
if (existingReferral) throw new Error("Referral already redeemed")
|
||||
|
||||
const selfReferral = await tx
|
||||
.select({ id: UserTable.id })
|
||||
.from(UserTable)
|
||||
.where(
|
||||
and(
|
||||
eq(UserTable.workspaceID, code.workspaceID),
|
||||
eq(UserTable.accountID, input.accountID),
|
||||
isNull(UserTable.timeDeleted),
|
||||
),
|
||||
)
|
||||
.then((rows) => rows[0])
|
||||
if (selfReferral) throw new Error("Self-referral is not allowed")
|
||||
|
||||
const referralID = Identifier.create("referral")
|
||||
await tx.insert(ReferralTable).ignore().values({
|
||||
workspaceID: code.workspaceID,
|
||||
id: referralID,
|
||||
inviteeAccountID: input.accountID,
|
||||
})
|
||||
|
||||
const referral = await tx
|
||||
.select({ id: ReferralTable.id, workspaceID: ReferralTable.workspaceID })
|
||||
.from(ReferralTable)
|
||||
.where(and(eq(ReferralTable.inviteeAccountID, input.accountID), isNull(ReferralTable.timeDeleted)))
|
||||
.then((rows) => rows[0])
|
||||
if (!referral) throw new Error("Referral not created")
|
||||
if (referral.id !== referralID) throw new Error("Referral already redeemed")
|
||||
})
|
||||
}
|
||||
|
||||
export async function completeFromLiteSubscription(input: { workspaceID: string; userID: string }) {
|
||||
return Database.transaction(async (tx) => {
|
||||
const invitee = await tx
|
||||
.select({ accountID: UserTable.accountID })
|
||||
.from(UserTable)
|
||||
.where(
|
||||
and(
|
||||
eq(UserTable.workspaceID, input.workspaceID),
|
||||
eq(UserTable.id, input.userID),
|
||||
isNull(UserTable.timeDeleted),
|
||||
),
|
||||
)
|
||||
.then((rows) => rows[0])
|
||||
if (!invitee?.accountID) throw new Error("Referral invitee account missing")
|
||||
|
||||
const referral = await tx
|
||||
.select({ id: ReferralTable.id, workspaceID: ReferralTable.workspaceID })
|
||||
.from(ReferralTable)
|
||||
.where(and(eq(ReferralTable.inviteeAccountID, invitee.accountID), isNull(ReferralTable.timeDeleted)))
|
||||
.then((rows) => rows[0])
|
||||
if (!referral) throw new Error("Referral not found")
|
||||
|
||||
const result = await tx
|
||||
.insert(ReferralRewardTable)
|
||||
.ignore()
|
||||
.values([
|
||||
{
|
||||
workspaceID: referral.workspaceID,
|
||||
referralID: referral.id,
|
||||
amount: REWARD_AMOUNT,
|
||||
},
|
||||
{
|
||||
workspaceID: input.workspaceID,
|
||||
referralID: referral.id,
|
||||
amount: REWARD_AMOUNT,
|
||||
},
|
||||
])
|
||||
|
||||
if (result.rowsAffected === 0) throw new Error("Referral already completed")
|
||||
})
|
||||
}
|
||||
|
||||
function usagePreviewItem(
|
||||
before: { usagePercent: number; resetInSec: number },
|
||||
after: { usagePercent: number; resetInSec: number },
|
||||
) {
|
||||
return {
|
||||
beforePercent: before.usagePercent,
|
||||
afterPercent: after.usagePercent,
|
||||
resetInSec: after.resetInSec,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,35 +0,0 @@
|
||||
import { bigint, mysqlTable, primaryKey, uniqueIndex, varchar } from "drizzle-orm/mysql-core"
|
||||
import { timestamps, ulid, utc, workspaceColumns } from "../drizzle/types"
|
||||
import { workspaceIndexes } from "./workspace.sql"
|
||||
|
||||
export const ReferralCodeTable = mysqlTable(
|
||||
"referral_code",
|
||||
{
|
||||
workspaceID: ulid("workspace_id").notNull(),
|
||||
code: varchar("code", { length: 10 }).notNull(),
|
||||
...timestamps,
|
||||
},
|
||||
(table) => [primaryKey({ columns: [table.workspaceID] }), uniqueIndex("code").on(table.code)],
|
||||
)
|
||||
|
||||
export const ReferralTable = mysqlTable(
|
||||
"referral",
|
||||
{
|
||||
...workspaceColumns,
|
||||
...timestamps,
|
||||
inviteeAccountID: ulid("invitee_account_id").notNull(),
|
||||
},
|
||||
(table) => [...workspaceIndexes(table), uniqueIndex("invitee_account_id").on(table.inviteeAccountID)],
|
||||
)
|
||||
|
||||
export const ReferralRewardTable = mysqlTable(
|
||||
"referral_reward",
|
||||
{
|
||||
workspaceID: ulid("workspace_id").notNull(),
|
||||
referralID: ulid("referral_id").notNull(),
|
||||
...timestamps,
|
||||
amount: bigint("amount", { mode: "number" }).notNull(),
|
||||
timeApplied: utc("time_applied"),
|
||||
},
|
||||
(table) => [primaryKey({ columns: [table.workspaceID, table.referralID] })],
|
||||
)
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@opencode-ai/console-function",
|
||||
"version": "1.15.6",
|
||||
"version": "1.15.5",
|
||||
"$schema": "https://json.schemastore.org/package.json",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
|
||||
@@ -26,7 +26,6 @@ export const subjects = createSubjects({
|
||||
account: z.object({
|
||||
accountID: z.string(),
|
||||
email: z.string(),
|
||||
newAccount: z.boolean().optional(),
|
||||
}),
|
||||
user: z.object({
|
||||
userID: z.string(),
|
||||
@@ -143,7 +142,6 @@ export default {
|
||||
}
|
||||
|
||||
// Get account
|
||||
let newAccount = false
|
||||
const accountID = await (async () => {
|
||||
const matches = await Database.use(async (tx) =>
|
||||
tx
|
||||
@@ -168,7 +166,6 @@ export default {
|
||||
if (!accountID) {
|
||||
console.log("creating account for", email)
|
||||
accountID = await Account.create({})
|
||||
newAccount = true
|
||||
}
|
||||
|
||||
await Database.use(async (tx) =>
|
||||
@@ -218,7 +215,7 @@ export default {
|
||||
await Workspace.create({ name: "Default" })
|
||||
}
|
||||
})
|
||||
return ctx.subject("account", accountID, { accountID, email, newAccount })
|
||||
return ctx.subject("account", accountID, { accountID, email })
|
||||
},
|
||||
}).fetch(request, env, ctx)
|
||||
return result
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@opencode-ai/console-mail",
|
||||
"version": "1.15.6",
|
||||
"version": "1.15.5",
|
||||
"dependencies": {
|
||||
"@jsx-email/all": "2.2.3",
|
||||
"@jsx-email/cli": "1.4.3",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/package.json",
|
||||
"version": "1.15.6",
|
||||
"version": "1.15.5",
|
||||
"name": "@opencode-ai/core",
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
|
||||
@@ -21,6 +21,7 @@ export const Flag = {
|
||||
OPENCODE_DISABLE_PRUNE: truthy("OPENCODE_DISABLE_PRUNE"),
|
||||
OPENCODE_DISABLE_TERMINAL_TITLE: truthy("OPENCODE_DISABLE_TERMINAL_TITLE"),
|
||||
OPENCODE_SHOW_TTFD: truthy("OPENCODE_SHOW_TTFD"),
|
||||
OPENCODE_PERMISSION: process.env["OPENCODE_PERMISSION"],
|
||||
OPENCODE_DISABLE_AUTOCOMPACT: truthy("OPENCODE_DISABLE_AUTOCOMPACT"),
|
||||
OPENCODE_DISABLE_MODELS_FETCH: truthy("OPENCODE_DISABLE_MODELS_FETCH"),
|
||||
OPENCODE_DISABLE_MOUSE: truthy("OPENCODE_DISABLE_MOUSE"),
|
||||
@@ -58,9 +59,6 @@ export const Flag = {
|
||||
get OPENCODE_PURE() {
|
||||
return truthy("OPENCODE_PURE")
|
||||
},
|
||||
get OPENCODE_PERMISSION() {
|
||||
return process.env["OPENCODE_PERMISSION"]
|
||||
},
|
||||
get OPENCODE_PLUGIN_META_FILE() {
|
||||
return process.env["OPENCODE_PLUGIN_META_FILE"]
|
||||
},
|
||||
|
||||
@@ -111,7 +111,7 @@ describe("cross-spawn spawner", () => {
|
||||
ChildProcess.make(process.execPath, ["-e", "process.stdout.write(process.cwd())"], { cwd: tmp.path }),
|
||||
),
|
||||
)
|
||||
expect(yield* Effect.promise(() => fs.realpath(out))).toBe(yield* Effect.promise(() => fs.realpath(tmp.path)))
|
||||
expect(out).toBe(tmp.path)
|
||||
}),
|
||||
)
|
||||
|
||||
@@ -119,9 +119,7 @@ describe("cross-spawn spawner", () => {
|
||||
"fails for invalid cwd",
|
||||
Effect.gen(function* () {
|
||||
const exit = yield* Effect.exit(
|
||||
ChildProcessSpawner.ChildProcessSpawner.use((svc) =>
|
||||
svc.spawn(ChildProcess.make("echo", ["test"], { cwd: "/nonexistent/directory/path" })),
|
||||
),
|
||||
ChildProcess.make("echo", ["test"], { cwd: "/nonexistent/directory/path" }).asEffect(),
|
||||
)
|
||||
expect(Exit.isFailure(exit)).toBe(true)
|
||||
}),
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@opencode-ai/desktop",
|
||||
"private": true,
|
||||
"version": "1.15.6",
|
||||
"version": "1.15.5",
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
"homepage": "https://opencode.ai",
|
||||
|
||||
@@ -1,84 +0,0 @@
|
||||
import { BrowserWindow } from "electron"
|
||||
import type { DesktopMenuAction } from "@opencode-ai/app/desktop-menu"
|
||||
import { createMainWindow, updateTitlebar } from "./windows"
|
||||
|
||||
export type DesktopMenuActionHandlers = Partial<{
|
||||
checkForUpdates: () => void
|
||||
relaunch: () => void
|
||||
}>
|
||||
|
||||
export function runDesktopMenuAction(
|
||||
win: BrowserWindow | null,
|
||||
action: DesktopMenuAction,
|
||||
handlers: DesktopMenuActionHandlers = {},
|
||||
) {
|
||||
switch (action) {
|
||||
case "app.checkForUpdates":
|
||||
handlers.checkForUpdates?.()
|
||||
return
|
||||
case "app.relaunch":
|
||||
handlers.relaunch?.()
|
||||
return
|
||||
case "window.new":
|
||||
createMainWindow()
|
||||
return
|
||||
case "window.close":
|
||||
win?.close()
|
||||
return
|
||||
case "window.minimize":
|
||||
win?.minimize()
|
||||
return
|
||||
case "window.toggleMaximize":
|
||||
if (win?.isMaximized()) {
|
||||
win.unmaximize()
|
||||
return
|
||||
}
|
||||
win?.maximize()
|
||||
return
|
||||
case "view.reload":
|
||||
win?.reload()
|
||||
return
|
||||
case "view.toggleDevTools":
|
||||
win?.webContents.toggleDevTools()
|
||||
return
|
||||
case "view.resetZoom":
|
||||
setZoom(win, 1)
|
||||
return
|
||||
case "view.zoomIn":
|
||||
setZoom(win, (win?.webContents.getZoomFactor() ?? 1) + 0.2)
|
||||
return
|
||||
case "view.zoomOut":
|
||||
setZoom(win, (win?.webContents.getZoomFactor() ?? 1) - 0.2)
|
||||
return
|
||||
case "view.toggleFullscreen":
|
||||
win?.setFullScreen(!win.isFullScreen())
|
||||
return
|
||||
case "edit.undo":
|
||||
win?.webContents.undo()
|
||||
return
|
||||
case "edit.redo":
|
||||
win?.webContents.redo()
|
||||
return
|
||||
case "edit.cut":
|
||||
win?.webContents.cut()
|
||||
return
|
||||
case "edit.copy":
|
||||
win?.webContents.copy()
|
||||
return
|
||||
case "edit.paste":
|
||||
win?.webContents.paste()
|
||||
return
|
||||
case "edit.delete":
|
||||
win?.webContents.delete()
|
||||
return
|
||||
case "edit.selectAll":
|
||||
win?.webContents.selectAll()
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
function setZoom(win: BrowserWindow | null, value: number) {
|
||||
if (!win) return
|
||||
win.webContents.setZoomFactor(Math.min(Math.max(value, 0.2), 10))
|
||||
updateTitlebar(win)
|
||||
}
|
||||
@@ -345,13 +345,11 @@ const main = Effect.gen(function* () {
|
||||
mainWindow = createMainWindow()
|
||||
if (mainWindow) {
|
||||
createMenu({
|
||||
trigger: (id) => {
|
||||
const win = BrowserWindow.getFocusedWindow() ?? mainWindow
|
||||
if (win) sendMenuCommand(win, id)
|
||||
},
|
||||
trigger: (id) => mainWindow && sendMenuCommand(mainWindow, id),
|
||||
checkForUpdates: () => {
|
||||
void checkForUpdates(true, killSidecar)
|
||||
},
|
||||
reload: () => mainWindow?.reload(),
|
||||
relaunch: () => {
|
||||
void killSidecar().finally(() => {
|
||||
app.relaunch()
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { execFile } from "node:child_process"
|
||||
import { BrowserWindow, Notification, app, clipboard, dialog, ipcMain, shell } from "electron"
|
||||
import type { IpcMainEvent, IpcMainInvokeEvent } from "electron"
|
||||
import type { DesktopMenuAction } from "@opencode-ai/app/desktop-menu"
|
||||
|
||||
import type {
|
||||
InitStep,
|
||||
@@ -11,7 +10,6 @@ import type {
|
||||
WindowConfig,
|
||||
WslConfig,
|
||||
} from "../preload/types"
|
||||
import { runDesktopMenuAction } from "./desktop-menu-actions"
|
||||
import { getStore } from "./store"
|
||||
import { setTitlebar, updateTitlebar } from "./windows"
|
||||
|
||||
@@ -200,9 +198,6 @@ export function registerIpcHandlers(deps: Deps) {
|
||||
if (!win) return
|
||||
setTitlebar(win, theme)
|
||||
})
|
||||
ipcMain.handle("run-desktop-menu-action", (event: IpcMainInvokeEvent, action: DesktopMenuAction) => {
|
||||
runDesktopMenuAction(BrowserWindow.fromWebContents(event.sender), action)
|
||||
})
|
||||
}
|
||||
|
||||
export function sendSqliteMigrationProgress(win: BrowserWindow, progress: SqliteMigrationProgress) {
|
||||
|
||||
@@ -1,67 +1,141 @@
|
||||
import { BrowserWindow, Menu, shell } from "electron"
|
||||
import type { MenuItemConstructorOptions } from "electron"
|
||||
import {
|
||||
DESKTOP_MENU,
|
||||
desktopMenuVisible,
|
||||
type DesktopMenuEntry,
|
||||
type DesktopMenuRole,
|
||||
} from "@opencode-ai/app/desktop-menu"
|
||||
import { Menu, shell } from "electron"
|
||||
|
||||
import { UPDATER_ENABLED } from "./constants"
|
||||
import { runDesktopMenuAction } from "./desktop-menu-actions"
|
||||
import { createMainWindow } from "./windows"
|
||||
|
||||
type Deps = {
|
||||
trigger: (id: string) => void
|
||||
checkForUpdates: () => void
|
||||
reload: () => void
|
||||
relaunch: () => void
|
||||
}
|
||||
|
||||
export function createMenu(deps: Deps) {
|
||||
if (process.platform !== "darwin") return
|
||||
|
||||
const template = DESKTOP_MENU.filter((menu) => desktopMenuVisible(menu, "macos")).map((menu) => {
|
||||
if (menu.role) return { role: nativeRole(menu.role) }
|
||||
return {
|
||||
label: menu.label,
|
||||
submenu: menu.items
|
||||
?.filter((entry) => desktopMenuVisible(entry, "macos"))
|
||||
.map((entry) => nativeItem(entry, deps)),
|
||||
}
|
||||
})
|
||||
const template: Electron.MenuItemConstructorOptions[] = [
|
||||
{
|
||||
label: "OpenCode",
|
||||
submenu: [
|
||||
{ role: "about" },
|
||||
{
|
||||
label: "Check for Updates...",
|
||||
enabled: UPDATER_ENABLED,
|
||||
click: () => deps.checkForUpdates(),
|
||||
},
|
||||
{
|
||||
label: "Settings",
|
||||
accelerator: "Cmd+,",
|
||||
click: () => deps.trigger("settings.open"),
|
||||
},
|
||||
{
|
||||
label: "Reload Webview",
|
||||
click: () => deps.reload(),
|
||||
},
|
||||
{
|
||||
label: "Restart",
|
||||
click: () => deps.relaunch(),
|
||||
},
|
||||
{ type: "separator" },
|
||||
{ role: "hide" },
|
||||
{ role: "hideOthers" },
|
||||
{ role: "unhide" },
|
||||
{ type: "separator" },
|
||||
{ role: "quit" },
|
||||
],
|
||||
},
|
||||
{
|
||||
label: "File",
|
||||
submenu: [
|
||||
{ label: "New Session", accelerator: "Shift+Cmd+S", click: () => deps.trigger("session.new") },
|
||||
{ label: "Open Project...", accelerator: "Cmd+O", click: () => deps.trigger("project.open") },
|
||||
{
|
||||
label: "New Window",
|
||||
accelerator: "Cmd+Shift+N",
|
||||
click: () => createMainWindow(),
|
||||
},
|
||||
{ type: "separator" },
|
||||
{ role: "close" },
|
||||
],
|
||||
},
|
||||
{
|
||||
label: "Edit",
|
||||
submenu: [
|
||||
{ role: "undo" },
|
||||
{ role: "redo" },
|
||||
{ type: "separator" },
|
||||
{ role: "cut" },
|
||||
{ role: "copy" },
|
||||
{ role: "paste" },
|
||||
{ role: "selectAll" },
|
||||
],
|
||||
},
|
||||
{
|
||||
label: "View",
|
||||
submenu: [
|
||||
{ label: "Toggle Sidebar", accelerator: "Cmd+B", click: () => deps.trigger("sidebar.toggle") },
|
||||
{ label: "Toggle Terminal", accelerator: "Ctrl+`", click: () => deps.trigger("terminal.toggle") },
|
||||
{ label: "Toggle File Tree", click: () => deps.trigger("fileTree.toggle") },
|
||||
{ type: "separator" },
|
||||
{ role: "reload" },
|
||||
{ role: "toggleDevTools" },
|
||||
{ type: "separator" },
|
||||
{ role: "resetZoom" },
|
||||
{ role: "zoomIn" },
|
||||
{ role: "zoomOut" },
|
||||
{ type: "separator" },
|
||||
{ role: "togglefullscreen" },
|
||||
],
|
||||
},
|
||||
{
|
||||
label: "Go",
|
||||
submenu: [
|
||||
{ label: "Back", accelerator: "Cmd+[", click: () => deps.trigger("common.goBack") },
|
||||
{ label: "Forward", accelerator: "Cmd+]", click: () => deps.trigger("common.goForward") },
|
||||
{ type: "separator" },
|
||||
{
|
||||
label: "Previous Session",
|
||||
accelerator: "Option+Up",
|
||||
click: () => deps.trigger("session.previous"),
|
||||
},
|
||||
{
|
||||
label: "Next Session",
|
||||
accelerator: "Option+Down",
|
||||
click: () => deps.trigger("session.next"),
|
||||
},
|
||||
{ type: "separator" },
|
||||
{
|
||||
label: "Previous Project",
|
||||
accelerator: "Cmd+Option+Up",
|
||||
click: () => deps.trigger("project.previous"),
|
||||
},
|
||||
{
|
||||
label: "Next Project",
|
||||
accelerator: "Cmd+Option+Down",
|
||||
click: () => deps.trigger("project.next"),
|
||||
},
|
||||
],
|
||||
},
|
||||
{ role: "windowMenu" },
|
||||
{
|
||||
label: "Help",
|
||||
submenu: [
|
||||
{ label: "OpenCode Documentation", click: () => shell.openExternal("https://opencode.ai/docs") },
|
||||
{ label: "Support Forum", click: () => shell.openExternal("https://discord.com/invite/opencode") },
|
||||
{ type: "separator" },
|
||||
{ type: "separator" },
|
||||
{
|
||||
label: "Share Feedback",
|
||||
click: () =>
|
||||
shell.openExternal("https://github.com/anomalyco/opencode/issues/new?template=feature_request.yml"),
|
||||
},
|
||||
{
|
||||
label: "Report a Bug",
|
||||
click: () => shell.openExternal("https://github.com/anomalyco/opencode/issues/new?template=bug_report.yml"),
|
||||
},
|
||||
],
|
||||
},
|
||||
]
|
||||
|
||||
Menu.setApplicationMenu(Menu.buildFromTemplate(template))
|
||||
}
|
||||
|
||||
function nativeItem(entry: DesktopMenuEntry, deps: Deps): MenuItemConstructorOptions {
|
||||
if (entry.type === "separator") return { type: "separator" }
|
||||
if (entry.role) return { role: nativeRole(entry.role) }
|
||||
|
||||
const item: MenuItemConstructorOptions = {
|
||||
label: entry.label,
|
||||
accelerator: entry.accelerator?.macos,
|
||||
enabled: entry.enabled === "updater" ? UPDATER_ENABLED : undefined,
|
||||
}
|
||||
|
||||
if (entry.command) {
|
||||
const command = entry.command
|
||||
item.click = () => deps.trigger(command)
|
||||
}
|
||||
if (entry.action) {
|
||||
const action = entry.action
|
||||
item.click = () =>
|
||||
runDesktopMenuAction(BrowserWindow.getFocusedWindow(), action, {
|
||||
checkForUpdates: deps.checkForUpdates,
|
||||
relaunch: deps.relaunch,
|
||||
})
|
||||
}
|
||||
if (entry.href) {
|
||||
const href = entry.href
|
||||
item.click = () => shell.openExternal(href)
|
||||
}
|
||||
|
||||
return item
|
||||
}
|
||||
|
||||
function nativeRole(role: DesktopMenuRole) {
|
||||
return role as NonNullable<MenuItemConstructorOptions["role"]>
|
||||
}
|
||||
|
||||
@@ -61,7 +61,6 @@ const api: ElectronAPI = {
|
||||
getZoomFactor: () => ipcRenderer.invoke("get-zoom-factor"),
|
||||
setZoomFactor: (factor) => ipcRenderer.invoke("set-zoom-factor", factor),
|
||||
setTitlebar: (theme) => ipcRenderer.invoke("set-titlebar", theme),
|
||||
runDesktopMenuAction: (action) => ipcRenderer.invoke("run-desktop-menu-action", action),
|
||||
loadingWindowComplete: () => ipcRenderer.send("loading-window-complete"),
|
||||
runUpdater: (alertOnFail) => ipcRenderer.invoke("run-updater", alertOnFail),
|
||||
checkUpdate: () => ipcRenderer.invoke("check-update"),
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
import type { DesktopMenuAction } from "@opencode-ai/app/desktop-menu"
|
||||
|
||||
export type InitStep = { phase: "server_waiting" } | { phase: "sqlite_waiting" } | { phase: "done" }
|
||||
|
||||
export type ServerReadyData = {
|
||||
@@ -16,6 +14,7 @@ export type LinuxDisplayBackend = "wayland" | "auto"
|
||||
export type TitlebarTheme = {
|
||||
mode: "light" | "dark"
|
||||
}
|
||||
|
||||
export type WindowConfig = {
|
||||
updaterEnabled: boolean
|
||||
}
|
||||
@@ -72,7 +71,6 @@ export type ElectronAPI = {
|
||||
getZoomFactor: () => Promise<number>
|
||||
setZoomFactor: (factor: number) => Promise<void>
|
||||
setTitlebar: (theme: TitlebarTheme) => Promise<void>
|
||||
runDesktopMenuAction: (action: DesktopMenuAction) => Promise<void>
|
||||
loadingWindowComplete: () => void
|
||||
runUpdater: (alertOnFail: boolean) => Promise<void>
|
||||
checkUpdate: () => Promise<{ updateAvailable: boolean; version?: string }>
|
||||
|
||||
@@ -21,7 +21,7 @@ import { createEffect, createResource, onCleanup, onMount, Show } from "solid-js
|
||||
import { render } from "solid-js/web"
|
||||
import pkg from "../../package.json"
|
||||
import { initI18n, t } from "./i18n"
|
||||
import { resetZoom, webviewZoom, zoomIn, zoomOut } from "./webview-zoom"
|
||||
import { webviewZoom } from "./webview-zoom"
|
||||
import "./styles.css"
|
||||
import { useTheme } from "@opencode-ai/ui/theme"
|
||||
|
||||
@@ -100,22 +100,6 @@ const createPlatform = (): Platform => {
|
||||
return window.api.wslPath(result, "linux").catch(() => result) as any
|
||||
}
|
||||
|
||||
const runDesktopMenuAction: Platform["runDesktopMenuAction"] = (action) => {
|
||||
switch (action) {
|
||||
case "view.resetZoom":
|
||||
resetZoom()
|
||||
return
|
||||
case "view.zoomIn":
|
||||
zoomIn()
|
||||
return
|
||||
case "view.zoomOut":
|
||||
zoomOut()
|
||||
return
|
||||
}
|
||||
|
||||
return window.api.runDesktopMenuAction(action)
|
||||
}
|
||||
|
||||
const storage = (() => {
|
||||
const cache = new Map<string, AsyncStorage>()
|
||||
|
||||
@@ -270,8 +254,6 @@ const createPlatform = (): Platform => {
|
||||
|
||||
webviewZoom,
|
||||
|
||||
runDesktopMenuAction,
|
||||
|
||||
checkAppExists: async (appName: string) => {
|
||||
return window.api.checkAppExists(appName)
|
||||
},
|
||||
|
||||
@@ -33,27 +33,23 @@ const applyZoom = (next: number) => {
|
||||
})
|
||||
}
|
||||
|
||||
const resetZoom = () => applyZoom(1)
|
||||
const zoomIn = () => applyZoom(clamp(requestedZoom + 0.2))
|
||||
const zoomOut = () => applyZoom(clamp(requestedZoom - 0.2))
|
||||
|
||||
window.addEventListener("keydown", (event) => {
|
||||
if (!(OS_NAME === "macos" ? event.metaKey : event.ctrlKey)) return
|
||||
|
||||
if (event.key === "-") {
|
||||
event.preventDefault()
|
||||
zoomOut()
|
||||
applyZoom(clamp(requestedZoom - 0.2))
|
||||
return
|
||||
}
|
||||
if (event.key === "=" || event.key === "+") {
|
||||
event.preventDefault()
|
||||
zoomIn()
|
||||
applyZoom(clamp(requestedZoom + 0.2))
|
||||
return
|
||||
}
|
||||
if (event.key === "0") {
|
||||
event.preventDefault()
|
||||
resetZoom()
|
||||
applyZoom(1)
|
||||
}
|
||||
})
|
||||
|
||||
export { webviewZoom, resetZoom, zoomIn, zoomOut }
|
||||
export { webviewZoom }
|
||||
|
||||
@@ -1,19 +0,0 @@
|
||||
# Effect Drizzle SQLite
|
||||
|
||||
This package vendors a Drizzle Effect SQLite adapter for this repo.
|
||||
|
||||
- Keep this package generic: Drizzle + Effect + SQLite only.
|
||||
- Do not add opencode-specific tables, paths, migrations, post-commit hooks, or domain storage APIs here.
|
||||
- Runtime code should depend on generic `effect/unstable/sql/SqlClient`, not a specific SQLite driver.
|
||||
- Concrete SQLite clients such as `@effect/sql-sqlite-bun` belong in tests or examples unless this package intentionally adds a driver-specific helper.
|
||||
- Preserve Drizzle adapter naming and behavior where possible so this can be replaced by upstream `drizzle-orm/effect-sqlite` later.
|
||||
- If touching copied Drizzle internals, compare with current `drizzle-orm@1.0.0-rc.2` declarations and runtime JS.
|
||||
- If touching Effect APIs, verify against `/Users/kit/code/open-source/effect-smol`.
|
||||
|
||||
Useful entry points:
|
||||
|
||||
- `src/effect-sqlite/driver.ts`: creates the Effect-backed Drizzle database with `make` and `makeWithDefaults`.
|
||||
- `src/effect-sqlite/session.ts`: adapts generic Effect `SqlClient` execution and transactions to Drizzle SQLite sessions.
|
||||
- `src/sqlite-core/effect/*`: Effect-yieldable SQLite query builders.
|
||||
- `src/internal/drizzle-utils.ts`: local typed shims for Drizzle runtime internals that RC2 does not expose in declarations.
|
||||
- `examples/basic.ts`: minimal usage example with Bun SQLite.
|
||||
@@ -1,92 +0,0 @@
|
||||
import { SqliteClient } from "@effect/sql-sqlite-bun"
|
||||
import { eq } from "drizzle-orm"
|
||||
import { integer, sqliteTable, text } from "drizzle-orm/sqlite-core"
|
||||
import * as Context from "effect/Context"
|
||||
import * as Effect from "effect/Effect"
|
||||
import * as Layer from "effect/Layer"
|
||||
import * as Schema from "effect/Schema"
|
||||
import { EffectDrizzleSqlite } from "../src"
|
||||
|
||||
const users = sqliteTable("users", {
|
||||
id: integer().primaryKey({ autoIncrement: true }),
|
||||
name: text().notNull(),
|
||||
})
|
||||
|
||||
type User = typeof users.$inferSelect
|
||||
|
||||
const makeDatabase = EffectDrizzleSqlite.makeWithDefaults()
|
||||
type DatabaseShape = Effect.Success<typeof makeDatabase>
|
||||
|
||||
const sqliteLayer = SqliteClient.layer({ filename: ":memory:", disableWAL: true })
|
||||
|
||||
class Database extends Context.Service<Database, DatabaseShape>()("@opencode/example/Database") {
|
||||
static layer = Layer.effect(Database, makeDatabase).pipe(Layer.provide(sqliteLayer))
|
||||
}
|
||||
|
||||
class UserStoreError extends Schema.TaggedErrorClass<UserStoreError>()("UserStoreError", {
|
||||
message: Schema.String,
|
||||
cause: Schema.optional(Schema.Defect),
|
||||
}) {}
|
||||
|
||||
const mapStoreError = (message: string) => (cause: unknown) => new UserStoreError({ message, cause })
|
||||
|
||||
interface UserStoreShape {
|
||||
migrate(): Effect.Effect<void, UserStoreError>
|
||||
create(name: string): Effect.Effect<void, UserStoreError>
|
||||
rename(from: string, to: string): Effect.Effect<void, UserStoreError>
|
||||
list(): Effect.Effect<User[], UserStoreError>
|
||||
}
|
||||
|
||||
class UserStore extends Context.Service<UserStore, UserStoreShape>()("@opencode/example/UserStore") {
|
||||
static layer = Layer.effect(
|
||||
UserStore,
|
||||
Effect.gen(function* () {
|
||||
const db = yield* Database
|
||||
|
||||
return UserStore.of({
|
||||
migrate: Effect.fn("UserStore.migrate")(function* () {
|
||||
yield* EffectDrizzleSqlite.migrate(db, { migrationsFolder: `${import.meta.dirname}/migrations` }).pipe(
|
||||
Effect.mapError((cause) => new UserStoreError({ message: "Failed to migrate users", cause })),
|
||||
)
|
||||
}),
|
||||
create: Effect.fn("UserStore.create")(function* (name: string) {
|
||||
yield* db
|
||||
.insert(users)
|
||||
.values({ name })
|
||||
.pipe(Effect.asVoid, Effect.mapError(mapStoreError("Failed to create user")))
|
||||
}),
|
||||
rename: Effect.fn("UserStore.rename")(function* (from: string, to: string) {
|
||||
yield* db
|
||||
.transaction(
|
||||
Effect.fnUntraced(function* (tx) {
|
||||
yield* tx.insert(users).values({ name: from })
|
||||
yield* tx.update(users).set({ name: to }).where(eq(users.name, from))
|
||||
}),
|
||||
{ behavior: "immediate" },
|
||||
)
|
||||
.pipe(Effect.asVoid, Effect.mapError(mapStoreError("Failed to rename user")))
|
||||
}),
|
||||
list: Effect.fn("UserStore.list")(function* () {
|
||||
return yield* db
|
||||
.select()
|
||||
.from(users)
|
||||
.pipe(Effect.mapError(mapStoreError("Failed to list users")))
|
||||
}),
|
||||
})
|
||||
}),
|
||||
).pipe(Layer.provide(Database.layer))
|
||||
}
|
||||
|
||||
const program = Effect.gen(function* () {
|
||||
const userStore = yield* UserStore
|
||||
|
||||
yield* userStore.migrate()
|
||||
yield* userStore.create("Ada")
|
||||
yield* userStore.rename("Grace", "Grace Hopper")
|
||||
|
||||
return yield* userStore.list()
|
||||
})
|
||||
|
||||
const rows = await Effect.runPromise(program.pipe(Effect.provide(UserStore.layer)))
|
||||
|
||||
console.log(rows)
|
||||
-4
@@ -1,4 +0,0 @@
|
||||
CREATE TABLE users (
|
||||
id integer PRIMARY KEY AUTOINCREMENT NOT NULL,
|
||||
name text NOT NULL
|
||||
);
|
||||
@@ -1,29 +0,0 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/package.json",
|
||||
"version": "1.15.5",
|
||||
"name": "@opencode-ai/effect-drizzle-sqlite",
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"test": "bun test --timeout 30000",
|
||||
"test:ci": "mkdir -p .artifacts/unit && bun test --timeout 30000 --reporter=junit --reporter-outfile=.artifacts/unit/junit.xml",
|
||||
"typecheck": "tsgo --noEmit"
|
||||
},
|
||||
"exports": {
|
||||
".": "./src/index.ts",
|
||||
"./effect-sqlite": "./src/effect-sqlite/index.ts",
|
||||
"./effect-sqlite/migrator": "./src/effect-sqlite/migrator.ts",
|
||||
"./sqlite-core/effect": "./src/sqlite-core/effect/index.ts"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@effect/sql-sqlite-bun": "catalog:",
|
||||
"@tsconfig/bun": "catalog:",
|
||||
"@types/bun": "catalog:",
|
||||
"@typescript/native-preview": "catalog:"
|
||||
},
|
||||
"dependencies": {
|
||||
"drizzle-orm": "catalog:",
|
||||
"effect": "catalog:"
|
||||
}
|
||||
}
|
||||
@@ -1,77 +0,0 @@
|
||||
/* oxlint-disable */
|
||||
import * as Effect from "effect/Effect"
|
||||
import * as Layer from "effect/Layer"
|
||||
import { SqlClient } from "effect/unstable/sql/SqlClient"
|
||||
import { EffectCache } from "drizzle-orm/cache/core/cache-effect"
|
||||
import { EffectLogger } from "drizzle-orm/effect-core"
|
||||
import { entityKind } from "drizzle-orm/entity"
|
||||
import type { AnyRelations, EmptyRelations } from "drizzle-orm/relations"
|
||||
import { SQLiteAsyncDialect } from "drizzle-orm/sqlite-core/dialect"
|
||||
import { SQLiteEffectDatabase } from "../sqlite-core/effect/db"
|
||||
import type { DrizzleConfig } from "drizzle-orm/utils"
|
||||
import { jitCompatCheck } from "../internal/drizzle-utils"
|
||||
import { type EffectSQLiteQueryEffectHKT, type EffectSQLiteRunResult, EffectSQLiteSession } from "./session"
|
||||
|
||||
export class EffectSQLiteDatabase<TRelations extends AnyRelations = EmptyRelations> extends SQLiteEffectDatabase<
|
||||
EffectSQLiteQueryEffectHKT,
|
||||
EffectSQLiteRunResult,
|
||||
TRelations
|
||||
> {
|
||||
static override readonly [entityKind]: string = "EffectSQLiteDatabase"
|
||||
}
|
||||
|
||||
export type EffectDrizzleSQLiteConfig<TRelations extends AnyRelations = EmptyRelations> = Omit<
|
||||
DrizzleConfig<Record<string, never>, TRelations>,
|
||||
"cache" | "logger" | "schema"
|
||||
>
|
||||
|
||||
export const DefaultServices = Layer.merge(EffectCache.Default, EffectLogger.Default)
|
||||
|
||||
/**
|
||||
* Creates an EffectSQLiteDatabase instance.
|
||||
*
|
||||
* Requires a generic Effect `SqlClient`, `EffectLogger`, and `EffectCache` services to be provided.
|
||||
* Drizzle only depends on the generic `SqlClient`; install and provide a compatible SQLite provider such as
|
||||
* `@effect/sql-sqlite-node`, `@effect/sql-sqlite-bun`, or another package that exposes `SqlClient`.
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* import { SqliteClient } from '@effect/sql-sqlite-node';
|
||||
* import * as SQLiteDrizzle from 'drizzle-orm/effect-sqlite';
|
||||
* import * as Effect from 'effect/Effect';
|
||||
*
|
||||
* const db = yield* SQLiteDrizzle.make({ relations }).pipe(
|
||||
* Effect.provide(SQLiteDrizzle.DefaultServices),
|
||||
* Effect.provide(SqliteClient.layer({ filename: 'sqlite.db' })),
|
||||
* );
|
||||
* ```
|
||||
*/
|
||||
export const make = Effect.fn("SQLiteDrizzle.make")(function* <TRelations extends AnyRelations = EmptyRelations>(
|
||||
config: EffectDrizzleSQLiteConfig<TRelations> = {},
|
||||
) {
|
||||
const client = yield* SqlClient
|
||||
const cache = yield* EffectCache
|
||||
const logger = yield* EffectLogger
|
||||
|
||||
const dialect = new SQLiteAsyncDialect()
|
||||
const relations = config.relations ?? ({} as TRelations)
|
||||
const session = new EffectSQLiteSession(client, dialect, relations, {
|
||||
logger,
|
||||
cache,
|
||||
useJitMappers: jitCompatCheck(config.jit),
|
||||
})
|
||||
const db = new EffectSQLiteDatabase(dialect, session, relations) as EffectSQLiteDatabase<TRelations> & {
|
||||
$client: SqlClient
|
||||
}
|
||||
db.$client = client
|
||||
db.$cache.invalidate = cache.onMutate
|
||||
|
||||
return db
|
||||
})
|
||||
|
||||
/**
|
||||
* Convenience function that creates an EffectSQLiteDatabase with `DefaultServices` already provided.
|
||||
*/
|
||||
export const makeWithDefaults = <TRelations extends AnyRelations = EmptyRelations>(
|
||||
config: EffectDrizzleSQLiteConfig<TRelations> = {},
|
||||
) => make(config).pipe(Effect.provide(DefaultServices))
|
||||
@@ -1,4 +0,0 @@
|
||||
/* oxlint-disable */
|
||||
export { EffectLogger } from "drizzle-orm/effect-core"
|
||||
export * from "./driver"
|
||||
export * from "./session"
|
||||
@@ -1,14 +0,0 @@
|
||||
/* oxlint-disable */
|
||||
import type { MigrationConfig } from "drizzle-orm/migrator"
|
||||
import { readMigrationFiles } from "drizzle-orm/migrator"
|
||||
import type { AnyRelations } from "drizzle-orm/relations"
|
||||
import { migrate as coreMigrate } from "../sqlite-core/effect/session"
|
||||
import type { EffectSQLiteDatabase } from "./driver"
|
||||
|
||||
export function migrate<TRelations extends AnyRelations>(
|
||||
db: EffectSQLiteDatabase<TRelations>,
|
||||
config: MigrationConfig,
|
||||
) {
|
||||
const migrations = readMigrationFiles(config)
|
||||
return coreMigrate(migrations, db.session, config)
|
||||
}
|
||||
@@ -1,214 +0,0 @@
|
||||
/* oxlint-disable */
|
||||
import * as Context from "effect/Context"
|
||||
import * as Effect from "effect/Effect"
|
||||
import * as Exit from "effect/Exit"
|
||||
import * as Scope from "effect/Scope"
|
||||
import type { SqlClient } from "effect/unstable/sql/SqlClient"
|
||||
import type { SqlError } from "effect/unstable/sql/SqlError"
|
||||
import type { EffectCacheShape } from "drizzle-orm/cache/core/cache-effect"
|
||||
import type { WithCacheConfig } from "drizzle-orm/cache/core/types"
|
||||
import type { EffectDrizzleQueryError } from "drizzle-orm/effect-core/errors"
|
||||
import type { EffectLoggerShape } from "drizzle-orm/effect-core/logger"
|
||||
import type { QueryEffectHKTBase } from "drizzle-orm/effect-core/query-effect"
|
||||
import { entityKind } from "drizzle-orm/entity"
|
||||
import type { AnyRelations } from "drizzle-orm/relations"
|
||||
import type { RelationalQueryMapperConfig } from "drizzle-orm/relations"
|
||||
import type { Query } from "drizzle-orm/sql/sql"
|
||||
import type { SQLiteAsyncDialect } from "drizzle-orm/sqlite-core/dialect"
|
||||
import { SQLiteEffectPreparedQuery, SQLiteEffectSession, SQLiteEffectTransaction } from "../sqlite-core/effect/session"
|
||||
import type { SelectedFieldsOrdered } from "drizzle-orm/sqlite-core/query-builders/select.types"
|
||||
import type { PreparedQueryConfig, SQLiteExecuteMethod, SQLiteTransactionConfig } from "drizzle-orm/sqlite-core/session"
|
||||
|
||||
export interface EffectSQLiteQueryEffectHKT extends QueryEffectHKTBase {
|
||||
readonly error: EffectDrizzleQueryError
|
||||
readonly context: never
|
||||
}
|
||||
|
||||
export type EffectSQLiteRunResult = readonly never[]
|
||||
|
||||
export interface EffectSQLiteSessionOptions {
|
||||
logger: EffectLoggerShape
|
||||
cache: EffectCacheShape
|
||||
useJitMappers?: boolean
|
||||
}
|
||||
|
||||
export class EffectSQLiteSession<TRelations extends AnyRelations> extends SQLiteEffectSession<
|
||||
EffectSQLiteQueryEffectHKT,
|
||||
EffectSQLiteRunResult,
|
||||
TRelations
|
||||
> {
|
||||
static override readonly [entityKind]: string = "EffectSQLiteSession"
|
||||
|
||||
constructor(
|
||||
private client: SqlClient,
|
||||
dialect: SQLiteAsyncDialect,
|
||||
protected relations: TRelations,
|
||||
private options: EffectSQLiteSessionOptions,
|
||||
) {
|
||||
super(dialect)
|
||||
}
|
||||
|
||||
override prepareQuery<T extends PreparedQueryConfig = PreparedQueryConfig>(
|
||||
query: Query,
|
||||
fields: SelectedFieldsOrdered | undefined,
|
||||
executeMethod: SQLiteExecuteMethod,
|
||||
customResultMapper?: (rows: unknown[][], mapColumnValue?: (value: unknown) => unknown) => unknown,
|
||||
queryMetadata?: {
|
||||
type: "select" | "update" | "delete" | "insert"
|
||||
tables: string[]
|
||||
},
|
||||
cacheConfig?: WithCacheConfig,
|
||||
): SQLiteEffectPreparedQuery<T, EffectSQLiteQueryEffectHKT> {
|
||||
return new SQLiteEffectPreparedQuery<T, EffectSQLiteQueryEffectHKT>(
|
||||
(params, method) => this.execute(query, params, method),
|
||||
query,
|
||||
this.options.logger,
|
||||
this.options.cache,
|
||||
queryMetadata,
|
||||
cacheConfig,
|
||||
fields,
|
||||
executeMethod,
|
||||
this.options.useJitMappers,
|
||||
customResultMapper,
|
||||
undefined,
|
||||
undefined,
|
||||
this.isInTransaction(),
|
||||
)
|
||||
}
|
||||
|
||||
override prepareRelationalQuery<T extends PreparedQueryConfig = PreparedQueryConfig>(
|
||||
query: Query,
|
||||
fields: SelectedFieldsOrdered | undefined,
|
||||
executeMethod: SQLiteExecuteMethod,
|
||||
customResultMapper: (rows: Record<string, unknown>[], mapColumnValue?: (value: unknown) => unknown) => unknown,
|
||||
config: RelationalQueryMapperConfig,
|
||||
): SQLiteEffectPreparedQuery<T, EffectSQLiteQueryEffectHKT, true> {
|
||||
return new SQLiteEffectPreparedQuery<T, EffectSQLiteQueryEffectHKT, true>(
|
||||
(params, method) => this.execute(query, params, method),
|
||||
query,
|
||||
this.options.logger,
|
||||
this.options.cache,
|
||||
undefined,
|
||||
undefined,
|
||||
fields,
|
||||
executeMethod,
|
||||
this.options.useJitMappers,
|
||||
customResultMapper,
|
||||
true,
|
||||
config,
|
||||
this.isInTransaction(),
|
||||
)
|
||||
}
|
||||
|
||||
private execute(query: Query, params: unknown[], method: SQLiteExecuteMethod | "values") {
|
||||
const statement = this.client.unsafe(query.sql, params)
|
||||
if (method === "values") return statement.values
|
||||
if (method === "get") return statement.withoutTransform.pipe(Effect.map((rows) => rows[0]))
|
||||
return statement.withoutTransform
|
||||
}
|
||||
|
||||
private isInTransaction() {
|
||||
return Effect.serviceOption(this.client.transactionService).pipe(Effect.map((option) => option._tag === "Some"))
|
||||
}
|
||||
|
||||
private executeTransactionStatement(connection: Effect.Success<SqlClient["reserve"]>, query: string) {
|
||||
return connection.executeUnprepared(query, [], undefined).pipe(Effect.asVoid)
|
||||
}
|
||||
|
||||
private withTransaction<A, E, R>(effect: Effect.Effect<A, E, R>, config: SQLiteTransactionConfig | undefined) {
|
||||
return Effect.uninterruptibleMask((restore) =>
|
||||
Effect.withFiber<A, E | SqlError, R>((fiber) => {
|
||||
const services = fiber.context
|
||||
const connectionOption = Context.getOption(services, this.client.transactionService)
|
||||
const connection: Effect.Effect<
|
||||
readonly [Scope.Closeable | undefined, Effect.Success<SqlClient["reserve"]>],
|
||||
SqlError
|
||||
> =
|
||||
connectionOption._tag === "Some"
|
||||
? Effect.succeed([undefined, connectionOption.value[0]] as const)
|
||||
: Scope.make().pipe(
|
||||
Effect.flatMap((scope) =>
|
||||
Scope.provide(this.client.reserve, scope).pipe(
|
||||
Effect.map((connection) => [scope, connection] as const),
|
||||
Effect.catch((error) =>
|
||||
Scope.close(scope, Exit.fail(error)).pipe(Effect.andThen(Effect.fail(error))),
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
const id = connectionOption._tag === "Some" ? connectionOption.value[1] + 1 : 0
|
||||
|
||||
return connection.pipe(
|
||||
Effect.flatMap(([scope, connection]) =>
|
||||
this.executeTransactionStatement(
|
||||
connection,
|
||||
id === 0 ? `begin ${config?.behavior ?? "deferred"}` : `savepoint effect_sql_${id}`,
|
||||
).pipe(
|
||||
Effect.flatMap(() =>
|
||||
Effect.provideContext(
|
||||
restore(effect),
|
||||
Context.add(services, this.client.transactionService, [connection, id]),
|
||||
),
|
||||
),
|
||||
Effect.exit,
|
||||
Effect.flatMap((exit) => {
|
||||
const finalize = Exit.isSuccess(exit)
|
||||
? id === 0
|
||||
? this.executeTransactionStatement(connection, "commit").pipe(
|
||||
// SQLite keeps the transaction open after deferred constraint commit failures.
|
||||
Effect.catch((error) =>
|
||||
this.executeTransactionStatement(connection, "rollback").pipe(
|
||||
Effect.catch(() => Effect.void),
|
||||
Effect.andThen(Effect.fail(error)),
|
||||
),
|
||||
),
|
||||
)
|
||||
: this.executeTransactionStatement(connection, `release savepoint effect_sql_${id}`)
|
||||
: id === 0
|
||||
? this.executeTransactionStatement(connection, "rollback")
|
||||
: this.executeTransactionStatement(connection, `rollback to savepoint effect_sql_${id}`).pipe(
|
||||
Effect.andThen(
|
||||
this.executeTransactionStatement(connection, `release savepoint effect_sql_${id}`),
|
||||
),
|
||||
)
|
||||
const scoped = scope === undefined ? finalize : Effect.ensuring(finalize, Scope.close(scope, exit))
|
||||
|
||||
return scoped.pipe(Effect.flatMap(() => exit))
|
||||
}),
|
||||
),
|
||||
),
|
||||
)
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
override transaction<A, E, R>(
|
||||
transaction: (tx: EffectSQLiteTransaction<TRelations>) => Effect.Effect<A, E, R>,
|
||||
config?: SQLiteTransactionConfig,
|
||||
): Effect.Effect<A, E | SqlError, R> {
|
||||
const { dialect, relations } = this
|
||||
|
||||
return this.withTransaction(
|
||||
Effect.gen({ self: this }, function* () {
|
||||
const tx = new EffectSQLiteTransaction<TRelations>(dialect, this, relations)
|
||||
|
||||
return yield* transaction(tx)
|
||||
}),
|
||||
config,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export class EffectSQLiteTransaction<TRelations extends AnyRelations> extends SQLiteEffectTransaction<
|
||||
EffectSQLiteQueryEffectHKT,
|
||||
EffectSQLiteRunResult,
|
||||
TRelations
|
||||
> {
|
||||
static override readonly [entityKind]: string = "EffectSQLiteTransaction"
|
||||
|
||||
override transaction: <A, E, R>(
|
||||
transaction: (
|
||||
tx: SQLiteEffectTransaction<EffectSQLiteQueryEffectHKT, EffectSQLiteRunResult, TRelations>,
|
||||
) => Effect.Effect<A, E, R>,
|
||||
) => Effect.Effect<A, SqlError | E, R> = (tx) => this.session.transaction(tx)
|
||||
}
|
||||
@@ -1,6 +0,0 @@
|
||||
export { EffectLogger } from "drizzle-orm/effect-core"
|
||||
export * from "./effect-sqlite/driver"
|
||||
export * from "./effect-sqlite/session"
|
||||
export { migrate } from "./effect-sqlite/migrator"
|
||||
|
||||
export * as EffectDrizzleSqlite from "."
|
||||
@@ -1,127 +0,0 @@
|
||||
/* oxlint-disable */
|
||||
import { Column, getColumnTable } from "drizzle-orm/column"
|
||||
import { is } from "drizzle-orm/entity"
|
||||
import type { JoinNullability } from "drizzle-orm/query-builders/select.types"
|
||||
import { Param, SQL } from "drizzle-orm/sql/sql"
|
||||
import type { SelectedFieldsOrdered } from "drizzle-orm/sqlite-core/query-builders/select.types"
|
||||
import type { SQLiteUpdateSetSource } from "drizzle-orm/sqlite-core/query-builders/update"
|
||||
import type { SQLiteTable } from "drizzle-orm/sqlite-core/table"
|
||||
import { SQLiteViewBase } from "drizzle-orm/sqlite-core/view-base"
|
||||
import { Subquery } from "drizzle-orm/subquery"
|
||||
import { Table, getTableName } from "drizzle-orm/table"
|
||||
import type { UpdateSet } from "drizzle-orm/utils"
|
||||
import { ViewBaseConfig } from "drizzle-orm/view-common"
|
||||
|
||||
const TableSymbol = (
|
||||
Table as unknown as {
|
||||
Symbol: { Columns: symbol; IsAlias: symbol; Name: symbol; BaseName: symbol }
|
||||
}
|
||||
).Symbol
|
||||
|
||||
export function getTableColumnsRuntime(table: SQLiteTable) {
|
||||
return (table as unknown as Record<symbol, Record<string, Column>>)[TableSymbol.Columns]
|
||||
}
|
||||
|
||||
export function getViewSelectedFieldsRuntime(view: SQLiteViewBase) {
|
||||
return (view as unknown as Record<symbol, { selectedFields: Record<string, unknown>; name: string }>)[ViewBaseConfig]
|
||||
}
|
||||
|
||||
export function jitCompatCheck(isEnabled: boolean | undefined) {
|
||||
if (!isEnabled) return false
|
||||
try {
|
||||
return new Function("input", '"use strict"; return input;')(true) === true
|
||||
} catch {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
export function orderSelectedFields<TColumn extends Column>(
|
||||
fields: Record<string, unknown>,
|
||||
pathPrefix?: string[],
|
||||
): SelectedFieldsOrdered {
|
||||
return Object.entries(fields).flatMap(([name, field]) => {
|
||||
const path = pathPrefix ? [...pathPrefix, name] : [name]
|
||||
if (is(field, Column) || is(field, SQL) || is(field, SQL.Aliased) || is(field, Subquery)) {
|
||||
return [{ path, field }] as SelectedFieldsOrdered
|
||||
}
|
||||
if (is(field, Table)) return orderSelectedFields(getTableColumnsRuntime(field as SQLiteTable), path)
|
||||
return orderSelectedFields(field as Record<string, unknown>, path)
|
||||
}) as SelectedFieldsOrdered
|
||||
}
|
||||
|
||||
export function mapUpdateSet<TTable extends SQLiteTable>(table: TTable, values: SQLiteUpdateSetSource<TTable>) {
|
||||
const entries = Object.entries(values).filter(([, value]) => value !== undefined)
|
||||
if (entries.length === 0) throw new Error("No values to set")
|
||||
|
||||
return Object.fromEntries(
|
||||
entries.map(([key, value]) => [
|
||||
key,
|
||||
is(value, SQL) || is(value, Column) ? value : new Param(value, getTableColumnsRuntime(table)[key]),
|
||||
]),
|
||||
) as UpdateSet
|
||||
}
|
||||
|
||||
export function mapResultRow(
|
||||
columns: SelectedFieldsOrdered,
|
||||
row: unknown[],
|
||||
joinsNotNullableMap: Record<string, boolean> | undefined,
|
||||
) {
|
||||
const nullifyMap: Record<string, string | false> = {}
|
||||
const result: Record<string, unknown> = {}
|
||||
|
||||
columns.forEach((column, columnIndex) => {
|
||||
const decoder = (
|
||||
is(column.field, Column)
|
||||
? column.field
|
||||
: is(column.field, SQL)
|
||||
? (column.field as unknown as { decoder: { mapFromDriverValue(value: unknown): unknown } }).decoder
|
||||
: is(column.field, Subquery)
|
||||
? (column.field._.sql as unknown as { decoder: { mapFromDriverValue(value: unknown): unknown } }).decoder
|
||||
: (column.field.sql as unknown as { decoder: { mapFromDriverValue(value: unknown): unknown } }).decoder
|
||||
) as {
|
||||
mapFromDriverValue(value: unknown): unknown
|
||||
}
|
||||
const rawValue = row[columnIndex]
|
||||
const value = rawValue === null ? null : decoder.mapFromDriverValue(rawValue)
|
||||
const objectName = column.path[0]
|
||||
let node = result
|
||||
|
||||
column.path.forEach((pathChunk, pathChunkIndex) => {
|
||||
if (pathChunkIndex === column.path.length - 1) {
|
||||
node[pathChunk] = value
|
||||
return
|
||||
}
|
||||
node[pathChunk] = (node[pathChunk] ?? {}) as Record<string, unknown>
|
||||
node = node[pathChunk] as Record<string, unknown>
|
||||
})
|
||||
|
||||
if (joinsNotNullableMap && is(column.field, Column) && column.path.length === 2 && objectName) {
|
||||
const tableName = getTableName(getColumnTable(column.field))
|
||||
nullifyMap[objectName] =
|
||||
!(objectName in nullifyMap) && value === null
|
||||
? tableName
|
||||
: typeof nullifyMap[objectName] === "string" && nullifyMap[objectName] !== tableName
|
||||
? false
|
||||
: nullifyMap[objectName]
|
||||
}
|
||||
})
|
||||
|
||||
Object.entries(nullifyMap).forEach(([objectName, tableName]) => {
|
||||
if (typeof tableName === "string" && !joinsNotNullableMap?.[tableName]) result[objectName] = null
|
||||
})
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
export function getTableLikeName(table: SQLiteTable | Subquery | SQLiteViewBase | SQL) {
|
||||
if (is(table, Subquery)) return table._.alias
|
||||
if (is(table, SQLiteViewBase)) return getViewSelectedFieldsRuntime(table).name
|
||||
if (is(table, SQL)) return undefined
|
||||
return (table as unknown as Record<symbol, string | boolean>)[
|
||||
(table as unknown as Record<symbol, string | boolean>)[TableSymbol.IsAlias]
|
||||
? TableSymbol.Name
|
||||
: TableSymbol.BaseName
|
||||
] as string
|
||||
}
|
||||
|
||||
export type { JoinNullability }
|
||||
@@ -1,58 +0,0 @@
|
||||
/* oxlint-disable */
|
||||
import type * as Effect from "effect/Effect"
|
||||
import { applyEffectWrapper, type QueryEffectHKTBase } from "drizzle-orm/effect-core/query-effect"
|
||||
import { entityKind } from "drizzle-orm/entity"
|
||||
import { SQL, sql, type SQLWrapper } from "drizzle-orm/sql/sql"
|
||||
import type { SQLiteTable } from "drizzle-orm/sqlite-core/table"
|
||||
import type { SQLiteView } from "drizzle-orm/sqlite-core/view"
|
||||
import type { SQLiteEffectSession } from "./session"
|
||||
|
||||
function buildSQLiteEmbeddedCount(source: SQLiteTable | SQLiteView | SQL | SQLWrapper, filters?: SQL<unknown>) {
|
||||
return sql<number>`(select count(*) from ${source}${sql.raw(" where ").if(filters)}${filters})`
|
||||
}
|
||||
|
||||
function buildSQLiteCount(source: SQLiteTable | SQLiteView | SQL | SQLWrapper, filters?: SQL<unknown>) {
|
||||
return sql<number>`select count(*) from ${source}${sql.raw(" where ").if(filters)}${filters}`
|
||||
}
|
||||
|
||||
export interface SQLiteEffectCountBuilder<TEffectHKT extends QueryEffectHKTBase = QueryEffectHKTBase>
|
||||
extends SQL<number>,
|
||||
SQLWrapper<number>,
|
||||
Effect.Effect<number, TEffectHKT["error"], TEffectHKT["context"]> {}
|
||||
|
||||
export class SQLiteEffectCountBuilder<TEffectHKT extends QueryEffectHKTBase = QueryEffectHKTBase> extends SQL<number> {
|
||||
static override readonly [entityKind]: string = "SQLiteEffectCountBuilder"
|
||||
|
||||
private sql: SQL<number>
|
||||
private session: SQLiteEffectSession<TEffectHKT, any, any>
|
||||
|
||||
constructor(params: {
|
||||
source: SQLiteTable | SQLiteView | SQL | SQLWrapper
|
||||
filters?: SQL<unknown>
|
||||
session: SQLiteEffectSession<TEffectHKT, any, any>
|
||||
}) {
|
||||
super(buildSQLiteEmbeddedCount(params.source, params.filters).queryChunks)
|
||||
|
||||
this.session = params.session
|
||||
this.sql = buildSQLiteCount(params.source, params.filters)
|
||||
}
|
||||
|
||||
execute(placeholderValues?: Record<string, unknown>) {
|
||||
return this.session
|
||||
.prepareQuery<{
|
||||
type: "async"
|
||||
execute: number
|
||||
run: unknown
|
||||
all: unknown
|
||||
get: unknown
|
||||
values: unknown
|
||||
}>(this.session.dialect.sqlToQuery(this.sql), undefined, "all", (rows) => {
|
||||
const v = rows[0]?.[0]
|
||||
if (typeof v === "number") return v
|
||||
return v ? Number(v) : 0
|
||||
})
|
||||
.execute(placeholderValues)
|
||||
}
|
||||
}
|
||||
|
||||
applyEffectWrapper(SQLiteEffectCountBuilder)
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user