Compare commits

..

13 Commits

Author SHA1 Message Date
Aiden Cline 802534891c fix: ensure copilot model list filters out disabled models 2026-04-17 15:50:43 -05:00
Kit Langton 3431dfb8b8 refactor: unwrap ServerProxy namespace + self-reexport (#22954) 2026-04-16 21:40:31 +00:00
Kit Langton fefe8b500a refactor: unwrap FileWatcher namespace + self-reexport (#22941) 2026-04-16 17:39:56 -04:00
Kit Langton 5b8573e804 refactor: unwrap FileTime namespace + self-reexport (#22940) 2026-04-16 17:39:53 -04:00
Kit Langton 40123cbe2d refactor: unwrap Server namespace + self-reexport (#22955) 2026-04-16 21:39:52 +00:00
Kit Langton 016c641860 refactor: unwrap Ripgrep namespace + self-reexport (#22939) 2026-04-16 17:39:50 -04:00
Kit Langton b41b0e3d2d refactor: unwrap MDNS namespace + self-reexport (#22953) 2026-04-16 21:38:47 +00:00
Kit Langton 3d3e50ebf0 refactor: unwrap Identifier namespace + self-reexport (#22932) 2026-04-16 17:37:51 -04:00
Kit Langton 0abc0d541a refactor: unwrap BusEvent namespace + self-reexport (#22930) 2026-04-16 17:37:47 -04:00
Dax Raad 3f3989e694 fix type error 2026-04-16 17:35:56 -04:00
opencode-agent[bot] b4667d9b0d chore: generate 2026-04-16 21:33:01 +00:00
Dax Raad 3c68d75776 import performance improvements 2026-04-16 17:31:43 -04:00
Kit Langton 23ed876835 fix: narrow several from any type assertions in opencode core (#22926) 2026-04-16 17:15:18 -04:00
400 changed files with 18961 additions and 30597 deletions
+1
View File
@@ -594,6 +594,7 @@ OPENCODE_DISABLE_CLAUDE_CODE
OPENCODE_DISABLE_CLAUDE_CODE_PROMPT OPENCODE_DISABLE_CLAUDE_CODE_PROMPT
OPENCODE_DISABLE_CLAUDE_CODE_SKILLS OPENCODE_DISABLE_CLAUDE_CODE_SKILLS
OPENCODE_DISABLE_DEFAULT_PLUGINS OPENCODE_DISABLE_DEFAULT_PLUGINS
OPENCODE_DISABLE_FILETIME_CHECK
OPENCODE_DISABLE_LSP_DOWNLOAD OPENCODE_DISABLE_LSP_DOWNLOAD
OPENCODE_DISABLE_MODELS_FETCH OPENCODE_DISABLE_MODELS_FETCH
OPENCODE_DISABLE_PRUNE OPENCODE_DISABLE_PRUNE
+5 -1
View File
@@ -1,6 +1,10 @@
{ {
"$schema": "https://opencode.ai/config.json", "$schema": "https://opencode.ai/config.json",
"provider": {}, "provider": {
"opencode": {
"options": {},
},
},
"permission": { "permission": {
"edit": { "edit": {
"packages/opencode/migration/*": "deny", "packages/opencode/migration/*": "deny",
-1
View File
@@ -14,7 +14,6 @@
- Use Bun APIs when possible, like `Bun.file()` - Use Bun APIs when possible, like `Bun.file()`
- Rely on type inference when possible; avoid explicit type annotations or interfaces unless necessary for exports or clarity - Rely on type inference when possible; avoid explicit type annotations or interfaces unless necessary for exports or clarity
- Prefer functional array methods (flatMap, filter, map) over for loops; use type guards on filter to maintain type inference downstream - Prefer functional array methods (flatMap, filter, map) over for loops; use type guards on filter to maintain type inference downstream
- In `src/config`, follow the existing self-export pattern at the top of the file (for example `export * as ConfigAgent from "./agent"`) when adding a new config module.
Reduce total variable count by inlining when a value is only used once. Reduce total variable count by inlining when a value is only used once.
+54 -92
View File
@@ -29,7 +29,7 @@
}, },
"packages/app": { "packages/app": {
"name": "@opencode-ai/app", "name": "@opencode-ai/app",
"version": "1.14.18", "version": "1.4.6",
"dependencies": { "dependencies": {
"@kobalte/core": "catalog:", "@kobalte/core": "catalog:",
"@opencode-ai/sdk": "workspace:*", "@opencode-ai/sdk": "workspace:*",
@@ -83,7 +83,7 @@
}, },
"packages/console/app": { "packages/console/app": {
"name": "@opencode-ai/console-app", "name": "@opencode-ai/console-app",
"version": "1.14.18", "version": "1.4.6",
"dependencies": { "dependencies": {
"@cloudflare/vite-plugin": "1.15.2", "@cloudflare/vite-plugin": "1.15.2",
"@ibm/plex": "6.4.1", "@ibm/plex": "6.4.1",
@@ -117,7 +117,7 @@
}, },
"packages/console/core": { "packages/console/core": {
"name": "@opencode-ai/console-core", "name": "@opencode-ai/console-core",
"version": "1.14.18", "version": "1.4.6",
"dependencies": { "dependencies": {
"@aws-sdk/client-sts": "3.782.0", "@aws-sdk/client-sts": "3.782.0",
"@jsx-email/render": "1.1.1", "@jsx-email/render": "1.1.1",
@@ -144,7 +144,7 @@
}, },
"packages/console/function": { "packages/console/function": {
"name": "@opencode-ai/console-function", "name": "@opencode-ai/console-function",
"version": "1.14.18", "version": "1.4.6",
"dependencies": { "dependencies": {
"@ai-sdk/anthropic": "3.0.64", "@ai-sdk/anthropic": "3.0.64",
"@ai-sdk/openai": "3.0.48", "@ai-sdk/openai": "3.0.48",
@@ -168,7 +168,7 @@
}, },
"packages/console/mail": { "packages/console/mail": {
"name": "@opencode-ai/console-mail", "name": "@opencode-ai/console-mail",
"version": "1.14.18", "version": "1.4.6",
"dependencies": { "dependencies": {
"@jsx-email/all": "2.2.3", "@jsx-email/all": "2.2.3",
"@jsx-email/cli": "1.4.3", "@jsx-email/cli": "1.4.3",
@@ -190,35 +190,9 @@
"cloudflare": "5.2.0", "cloudflare": "5.2.0",
}, },
}, },
"packages/core": {
"name": "@opencode-ai/core",
"version": "1.4.11",
"bin": {
"opencode": "./bin/opencode",
},
"dependencies": {
"@effect/platform-node": "catalog:",
"@npmcli/arborist": "catalog:",
"effect": "catalog:",
"glob": "13.0.5",
"mime-types": "3.0.2",
"minimatch": "10.2.5",
"npm-package-arg": "13.0.2",
"semver": "catalog:",
"xdg-basedir": "5.1.0",
"zod": "catalog:",
},
"devDependencies": {
"@tsconfig/bun": "catalog:",
"@types/bun": "catalog:",
"@types/npm-package-arg": "6.1.4",
"@types/npmcli__arborist": "6.3.3",
"@types/semver": "catalog:",
},
},
"packages/desktop": { "packages/desktop": {
"name": "@opencode-ai/desktop", "name": "@opencode-ai/desktop",
"version": "1.14.18", "version": "1.4.6",
"dependencies": { "dependencies": {
"@opencode-ai/app": "workspace:*", "@opencode-ai/app": "workspace:*",
"@opencode-ai/ui": "workspace:*", "@opencode-ai/ui": "workspace:*",
@@ -251,9 +225,8 @@
}, },
"packages/desktop-electron": { "packages/desktop-electron": {
"name": "@opencode-ai/desktop-electron", "name": "@opencode-ai/desktop-electron",
"version": "1.14.18", "version": "1.4.6",
"dependencies": { "dependencies": {
"drizzle-orm": "catalog:",
"effect": "catalog:", "effect": "catalog:",
"electron-context-menu": "4.1.2", "electron-context-menu": "4.1.2",
"electron-log": "^5", "electron-log": "^5",
@@ -275,7 +248,7 @@
"@types/node": "catalog:", "@types/node": "catalog:",
"@typescript/native-preview": "catalog:", "@typescript/native-preview": "catalog:",
"@valibot/to-json-schema": "1.6.0", "@valibot/to-json-schema": "1.6.0",
"electron": "41.2.1", "electron": "40.4.1",
"electron-builder": "^26", "electron-builder": "^26",
"electron-vite": "^5", "electron-vite": "^5",
"solid-js": "catalog:", "solid-js": "catalog:",
@@ -295,7 +268,7 @@
}, },
"packages/enterprise": { "packages/enterprise": {
"name": "@opencode-ai/enterprise", "name": "@opencode-ai/enterprise",
"version": "1.14.18", "version": "1.4.6",
"dependencies": { "dependencies": {
"@opencode-ai/shared": "workspace:*", "@opencode-ai/shared": "workspace:*",
"@opencode-ai/ui": "workspace:*", "@opencode-ai/ui": "workspace:*",
@@ -324,7 +297,7 @@
}, },
"packages/function": { "packages/function": {
"name": "@opencode-ai/function", "name": "@opencode-ai/function",
"version": "1.14.18", "version": "1.4.6",
"dependencies": { "dependencies": {
"@octokit/auth-app": "8.0.1", "@octokit/auth-app": "8.0.1",
"@octokit/rest": "catalog:", "@octokit/rest": "catalog:",
@@ -340,7 +313,7 @@
}, },
"packages/opencode": { "packages/opencode": {
"name": "opencode", "name": "opencode",
"version": "1.14.18", "version": "1.4.6",
"bin": { "bin": {
"opencode": "./bin/opencode", "opencode": "./bin/opencode",
}, },
@@ -349,15 +322,15 @@
"@actions/github": "6.0.1", "@actions/github": "6.0.1",
"@agentclientprotocol/sdk": "0.16.1", "@agentclientprotocol/sdk": "0.16.1",
"@ai-sdk/alibaba": "1.0.17", "@ai-sdk/alibaba": "1.0.17",
"@ai-sdk/amazon-bedrock": "4.0.96", "@ai-sdk/amazon-bedrock": "4.0.94",
"@ai-sdk/anthropic": "3.0.71", "@ai-sdk/anthropic": "3.0.70",
"@ai-sdk/azure": "3.0.49", "@ai-sdk/azure": "3.0.49",
"@ai-sdk/cerebras": "2.0.41", "@ai-sdk/cerebras": "2.0.41",
"@ai-sdk/cohere": "3.0.27", "@ai-sdk/cohere": "3.0.27",
"@ai-sdk/deepinfra": "2.0.41", "@ai-sdk/deepinfra": "2.0.41",
"@ai-sdk/gateway": "3.0.104", "@ai-sdk/gateway": "3.0.102",
"@ai-sdk/google": "3.0.63", "@ai-sdk/google": "3.0.63",
"@ai-sdk/google-vertex": "4.0.112", "@ai-sdk/google-vertex": "4.0.111",
"@ai-sdk/groq": "3.0.31", "@ai-sdk/groq": "3.0.31",
"@ai-sdk/mistral": "3.0.27", "@ai-sdk/mistral": "3.0.27",
"@ai-sdk/openai": "3.0.53", "@ai-sdk/openai": "3.0.53",
@@ -392,8 +365,8 @@
"@opentelemetry/exporter-trace-otlp-http": "0.214.0", "@opentelemetry/exporter-trace-otlp-http": "0.214.0",
"@opentelemetry/sdk-trace-base": "2.6.1", "@opentelemetry/sdk-trace-base": "2.6.1",
"@opentelemetry/sdk-trace-node": "2.6.1", "@opentelemetry/sdk-trace-node": "2.6.1",
"@opentui/core": "0.1.101", "@opentui/core": "0.1.99",
"@opentui/solid": "0.1.101", "@opentui/solid": "0.1.99",
"@parcel/watcher": "2.5.1", "@parcel/watcher": "2.5.1",
"@pierre/diffs": "catalog:", "@pierre/diffs": "catalog:",
"@solid-primitives/event-bus": "1.1.2", "@solid-primitives/event-bus": "1.1.2",
@@ -413,7 +386,7 @@
"drizzle-orm": "catalog:", "drizzle-orm": "catalog:",
"effect": "catalog:", "effect": "catalog:",
"fuzzysort": "3.1.0", "fuzzysort": "3.1.0",
"gitlab-ai-provider": "6.6.0", "gitlab-ai-provider": "6.4.2",
"glob": "13.0.5", "glob": "13.0.5",
"google-auth-library": "10.5.0", "google-auth-library": "10.5.0",
"gray-matter": "4.0.3", "gray-matter": "4.0.3",
@@ -431,6 +404,7 @@
"opentui-spinner": "0.0.6", "opentui-spinner": "0.0.6",
"partial-json": "0.1.7", "partial-json": "0.1.7",
"remeda": "catalog:", "remeda": "catalog:",
"ripgrep": "0.3.1",
"semver": "^7.6.3", "semver": "^7.6.3",
"solid-js": "catalog:", "solid-js": "catalog:",
"strip-ansi": "7.1.2", "strip-ansi": "7.1.2",
@@ -484,23 +458,23 @@
}, },
"packages/plugin": { "packages/plugin": {
"name": "@opencode-ai/plugin", "name": "@opencode-ai/plugin",
"version": "1.14.18", "version": "1.4.6",
"dependencies": { "dependencies": {
"@opencode-ai/sdk": "workspace:*", "@opencode-ai/sdk": "workspace:*",
"effect": "catalog:", "effect": "catalog:",
"zod": "catalog:", "zod": "catalog:",
}, },
"devDependencies": { "devDependencies": {
"@opentui/core": "0.1.101", "@opentui/core": "0.1.99",
"@opentui/solid": "0.1.101", "@opentui/solid": "0.1.99",
"@tsconfig/node22": "catalog:", "@tsconfig/node22": "catalog:",
"@types/node": "catalog:", "@types/node": "catalog:",
"@typescript/native-preview": "catalog:", "@typescript/native-preview": "catalog:",
"typescript": "catalog:", "typescript": "catalog:",
}, },
"peerDependencies": { "peerDependencies": {
"@opentui/core": ">=0.1.101", "@opentui/core": ">=0.1.99",
"@opentui/solid": ">=0.1.101", "@opentui/solid": ">=0.1.99",
}, },
"optionalPeers": [ "optionalPeers": [
"@opentui/core", "@opentui/core",
@@ -519,7 +493,7 @@
}, },
"packages/sdk/js": { "packages/sdk/js": {
"name": "@opencode-ai/sdk", "name": "@opencode-ai/sdk",
"version": "1.14.18", "version": "1.4.6",
"dependencies": { "dependencies": {
"cross-spawn": "catalog:", "cross-spawn": "catalog:",
}, },
@@ -534,7 +508,7 @@
}, },
"packages/shared": { "packages/shared": {
"name": "@opencode-ai/shared", "name": "@opencode-ai/shared",
"version": "1.14.18", "version": "1.4.6",
"bin": { "bin": {
"opencode": "./bin/opencode", "opencode": "./bin/opencode",
}, },
@@ -558,7 +532,7 @@
}, },
"packages/slack": { "packages/slack": {
"name": "@opencode-ai/slack", "name": "@opencode-ai/slack",
"version": "1.14.18", "version": "1.4.6",
"dependencies": { "dependencies": {
"@opencode-ai/sdk": "workspace:*", "@opencode-ai/sdk": "workspace:*",
"@slack/bolt": "^3.17.1", "@slack/bolt": "^3.17.1",
@@ -593,7 +567,7 @@
}, },
"packages/ui": { "packages/ui": {
"name": "@opencode-ai/ui", "name": "@opencode-ai/ui",
"version": "1.14.18", "version": "1.4.6",
"dependencies": { "dependencies": {
"@kobalte/core": "catalog:", "@kobalte/core": "catalog:",
"@opencode-ai/sdk": "workspace:*", "@opencode-ai/sdk": "workspace:*",
@@ -642,7 +616,7 @@
}, },
"packages/web": { "packages/web": {
"name": "@opencode-ai/web", "name": "@opencode-ai/web",
"version": "1.14.18", "version": "1.4.6",
"dependencies": { "dependencies": {
"@astrojs/cloudflare": "12.6.3", "@astrojs/cloudflare": "12.6.3",
"@astrojs/markdown-remark": "6.3.1", "@astrojs/markdown-remark": "6.3.1",
@@ -701,8 +675,6 @@
"@npmcli/arborist": "9.4.0", "@npmcli/arborist": "9.4.0",
"@octokit/rest": "22.0.0", "@octokit/rest": "22.0.0",
"@openauthjs/openauth": "0.0.0-20250322224806", "@openauthjs/openauth": "0.0.0-20250322224806",
"@opentui/core": "0.1.99",
"@opentui/solid": "0.1.99",
"@pierre/diffs": "1.1.0-beta.18", "@pierre/diffs": "1.1.0-beta.18",
"@playwright/test": "1.59.1", "@playwright/test": "1.59.1",
"@solid-primitives/storage": "4.3.3", "@solid-primitives/storage": "4.3.3",
@@ -718,7 +690,7 @@
"@types/node": "22.13.9", "@types/node": "22.13.9",
"@types/semver": "7.7.1", "@types/semver": "7.7.1",
"@typescript/native-preview": "7.0.0-dev.20251207.1", "@typescript/native-preview": "7.0.0-dev.20251207.1",
"ai": "6.0.168", "ai": "6.0.158",
"cross-spawn": "7.0.6", "cross-spawn": "7.0.6",
"diff": "8.0.2", "diff": "8.0.2",
"dompurify": "3.3.1", "dompurify": "3.3.1",
@@ -766,7 +738,7 @@
"@ai-sdk/alibaba": ["@ai-sdk/alibaba@1.0.17", "", { "dependencies": { "@ai-sdk/openai-compatible": "2.0.41", "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.23" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-ZbE+U5bWz2JBc5DERLowx5+TKbjGBE93LqKZAWvuEn7HOSQMraxFMZuc0ST335QZJAyfBOzh7m1mPQ+y7EaaoA=="], "@ai-sdk/alibaba": ["@ai-sdk/alibaba@1.0.17", "", { "dependencies": { "@ai-sdk/openai-compatible": "2.0.41", "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.23" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-ZbE+U5bWz2JBc5DERLowx5+TKbjGBE93LqKZAWvuEn7HOSQMraxFMZuc0ST335QZJAyfBOzh7m1mPQ+y7EaaoA=="],
"@ai-sdk/amazon-bedrock": ["@ai-sdk/amazon-bedrock@4.0.96", "", { "dependencies": { "@ai-sdk/anthropic": "3.0.71", "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.23", "@smithy/eventstream-codec": "^4.0.1", "@smithy/util-utf8": "^4.0.0", "aws4fetch": "^1.0.20" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-Mc4Ias2jRMD1jOB6xWtKNPdhECeuCZyIlbr9EAGfBnyBt++sS13ziZh9qv9TdyMCAZJ7xoQcpbchoRJcKwPdpA=="], "@ai-sdk/amazon-bedrock": ["@ai-sdk/amazon-bedrock@4.0.94", "", { "dependencies": { "@ai-sdk/anthropic": "3.0.70", "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.23", "@smithy/eventstream-codec": "^4.0.1", "@smithy/util-utf8": "^4.0.0", "aws4fetch": "^1.0.20" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-XKE7wAjXejsIfNQvn3onvGUByhGHVM6W+xlL+1DAQLmjEb+ue4sOJIRehJ96rEvTXVVHRVyA6bSXx7ayxXfn5A=="],
"@ai-sdk/anthropic": ["@ai-sdk/anthropic@3.0.64", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.21" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-rwLi/Rsuj2pYniQXIrvClHvXDzgM4UQHHnvHTWEF14efnlKclG/1ghpNC+adsRujAbCTr6gRsSbDE2vEqriV7g=="], "@ai-sdk/anthropic": ["@ai-sdk/anthropic@3.0.64", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.21" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-rwLi/Rsuj2pYniQXIrvClHvXDzgM4UQHHnvHTWEF14efnlKclG/1ghpNC+adsRujAbCTr6gRsSbDE2vEqriV7g=="],
@@ -786,11 +758,11 @@
"@ai-sdk/fireworks": ["@ai-sdk/fireworks@2.0.46", "", { "dependencies": { "@ai-sdk/openai-compatible": "2.0.41", "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.23" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-XRKR0zgRyegdmtK5CDUEjlyRp0Fo+XVCdoG+301U1SGtgRIAYG3ObVtgzVJBVpJdHFSLHuYeLTnNiQoUxD7+FQ=="], "@ai-sdk/fireworks": ["@ai-sdk/fireworks@2.0.46", "", { "dependencies": { "@ai-sdk/openai-compatible": "2.0.41", "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.23" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-XRKR0zgRyegdmtK5CDUEjlyRp0Fo+XVCdoG+301U1SGtgRIAYG3ObVtgzVJBVpJdHFSLHuYeLTnNiQoUxD7+FQ=="],
"@ai-sdk/gateway": ["@ai-sdk/gateway@3.0.104", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.23", "@vercel/oidc": "3.2.0" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-ZKX5n74io8VIRlhIMSLWVlvT3sXC8Z7cZ9GHuWBWZDVi96+62AIsWuLGvMfcBA1STYuSoDrp6rIziZmvrTq0TA=="], "@ai-sdk/gateway": ["@ai-sdk/gateway@3.0.102", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.23", "@vercel/oidc": "3.1.0" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-GrwDpaYJiVafrsA1MTbZtXPcQUI67g5AXiJo7Y1F8b+w+SiYHLk3ZIn1YmpQVoVAh2bjvxjj+Vo0AvfskuGH4g=="],
"@ai-sdk/google": ["@ai-sdk/google@3.0.63", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.23" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-RfOZWVMYSPu2sPRfGajrauWAZ9BSaRopSn+AszkKWQ1MFj8nhaXvCqRHB5pBQUaHTfZKagvOmMpNfa/s3gPLgQ=="], "@ai-sdk/google": ["@ai-sdk/google@3.0.63", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.23" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-RfOZWVMYSPu2sPRfGajrauWAZ9BSaRopSn+AszkKWQ1MFj8nhaXvCqRHB5pBQUaHTfZKagvOmMpNfa/s3gPLgQ=="],
"@ai-sdk/google-vertex": ["@ai-sdk/google-vertex@4.0.112", "", { "dependencies": { "@ai-sdk/anthropic": "3.0.71", "@ai-sdk/google": "3.0.64", "@ai-sdk/openai-compatible": "2.0.41", "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.23", "google-auth-library": "^10.5.0" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-cSfHCkM+9ZrFtQWIN1WlV93JPD+isGSdFxKj7u1L9m2aLVZajlXdcE41GL9hMt7ld7bZYE4NnZ+4VLxBAHE+Eg=="], "@ai-sdk/google-vertex": ["@ai-sdk/google-vertex@4.0.111", "", { "dependencies": { "@ai-sdk/anthropic": "3.0.70", "@ai-sdk/google": "3.0.64", "@ai-sdk/openai-compatible": "2.0.41", "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.23", "google-auth-library": "^10.5.0" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-5gILpAWWI5idfal/MfoH3tlQeSnOJ9jfL8JB8m2fdc3ue/9xoXkYDpXpDL/nyJImFjMCi6eR0Fpvlo/IKEWDIg=="],
"@ai-sdk/groq": ["@ai-sdk/groq@3.0.31", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.21" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-XbbugpnFmXGu2TlXiq8KUJskP6/VVbuFcnFIGDzDIB/Chg6XHsNnqrTF80Zxkh0Pd3+NvbM+2Uqrtsndk6bDAg=="], "@ai-sdk/groq": ["@ai-sdk/groq@3.0.31", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.21" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-XbbugpnFmXGu2TlXiq8KUJskP6/VVbuFcnFIGDzDIB/Chg6XHsNnqrTF80Zxkh0Pd3+NvbM+2Uqrtsndk6bDAg=="],
@@ -1574,8 +1546,6 @@
"@opencode-ai/console-resource": ["@opencode-ai/console-resource@workspace:packages/console/resource"], "@opencode-ai/console-resource": ["@opencode-ai/console-resource@workspace:packages/console/resource"],
"@opencode-ai/core": ["@opencode-ai/core@workspace:packages/core"],
"@opencode-ai/desktop": ["@opencode-ai/desktop@workspace:packages/desktop"], "@opencode-ai/desktop": ["@opencode-ai/desktop@workspace:packages/desktop"],
"@opencode-ai/desktop-electron": ["@opencode-ai/desktop-electron@workspace:packages/desktop-electron"], "@opencode-ai/desktop-electron": ["@opencode-ai/desktop-electron@workspace:packages/desktop-electron"],
@@ -1616,7 +1586,7 @@
"@opentelemetry/otlp-transformer": ["@opentelemetry/otlp-transformer@0.214.0", "", { "dependencies": { "@opentelemetry/api-logs": "0.214.0", "@opentelemetry/core": "2.6.1", "@opentelemetry/resources": "2.6.1", "@opentelemetry/sdk-logs": "0.214.0", "@opentelemetry/sdk-metrics": "2.6.1", "@opentelemetry/sdk-trace-base": "2.6.1", "protobufjs": "^7.0.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-DSaYcuBRh6uozfsWN3R8HsN0yDhCuWP7tOFdkUOVaWD1KVJg8m4qiLUsg/tNhTLS9HUYUcwNpwL2eroLtsZZ/w=="], "@opentelemetry/otlp-transformer": ["@opentelemetry/otlp-transformer@0.214.0", "", { "dependencies": { "@opentelemetry/api-logs": "0.214.0", "@opentelemetry/core": "2.6.1", "@opentelemetry/resources": "2.6.1", "@opentelemetry/sdk-logs": "0.214.0", "@opentelemetry/sdk-metrics": "2.6.1", "@opentelemetry/sdk-trace-base": "2.6.1", "protobufjs": "^7.0.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-DSaYcuBRh6uozfsWN3R8HsN0yDhCuWP7tOFdkUOVaWD1KVJg8m4qiLUsg/tNhTLS9HUYUcwNpwL2eroLtsZZ/w=="],
"@opentelemetry/resources": ["@opentelemetry/resources@2.2.0", "", { "dependencies": { "@opentelemetry/core": "2.2.0", "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.3.0 <1.10.0" } }, "sha512-1pNQf/JazQTMA0BiO5NINUzH0cbLbbl7mntLa4aJNmCCXSj0q03T5ZXXL0zw4G55TjdL9Tz32cznGClf+8zr5A=="], "@opentelemetry/resources": ["@opentelemetry/resources@2.6.1", "", { "dependencies": { "@opentelemetry/core": "2.6.1", "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.3.0 <1.10.0" } }, "sha512-lID/vxSuKWXM55XhAKNoYXu9Cutoq5hFdkbTdI/zDKQktXzcWBVhNsOkiZFTMU9UtEWuGRNe0HUgmsFldIdxVA=="],
"@opentelemetry/sdk-logs": ["@opentelemetry/sdk-logs@0.214.0", "", { "dependencies": { "@opentelemetry/api-logs": "0.214.0", "@opentelemetry/core": "2.6.1", "@opentelemetry/resources": "2.6.1", "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.4.0 <1.10.0" } }, "sha512-zf6acnScjhsaBUU22zXZ/sLWim1dfhUAbGXdMmHmNG3LfBnQ3DKsOCITb2IZwoUsNNMTogqFKBnlIPPftUgGwA=="], "@opentelemetry/sdk-logs": ["@opentelemetry/sdk-logs@0.214.0", "", { "dependencies": { "@opentelemetry/api-logs": "0.214.0", "@opentelemetry/core": "2.6.1", "@opentelemetry/resources": "2.6.1", "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.4.0 <1.10.0" } }, "sha512-zf6acnScjhsaBUU22zXZ/sLWim1dfhUAbGXdMmHmNG3LfBnQ3DKsOCITb2IZwoUsNNMTogqFKBnlIPPftUgGwA=="],
@@ -1628,21 +1598,21 @@
"@opentelemetry/semantic-conventions": ["@opentelemetry/semantic-conventions@1.40.0", "", {}, "sha512-cifvXDhcqMwwTlTK04GBNeIe7yyo28Mfby85QXFe1Yk8nmi36Ab/5UQwptOx84SsoGNRg+EVSjwzfSZMy6pmlw=="], "@opentelemetry/semantic-conventions": ["@opentelemetry/semantic-conventions@1.40.0", "", {}, "sha512-cifvXDhcqMwwTlTK04GBNeIe7yyo28Mfby85QXFe1Yk8nmi36Ab/5UQwptOx84SsoGNRg+EVSjwzfSZMy6pmlw=="],
"@opentui/core": ["@opentui/core@0.1.101", "", { "dependencies": { "bun-ffi-structs": "0.1.2", "diff": "8.0.2", "jimp": "1.6.0", "marked": "17.0.1", "yoga-layout": "3.2.1" }, "optionalDependencies": { "@dimforge/rapier2d-simd-compat": "^0.17.3", "@opentui/core-darwin-arm64": "0.1.101", "@opentui/core-darwin-x64": "0.1.101", "@opentui/core-linux-arm64": "0.1.101", "@opentui/core-linux-x64": "0.1.101", "@opentui/core-win32-arm64": "0.1.101", "@opentui/core-win32-x64": "0.1.101", "bun-webgpu": "0.1.5", "planck": "^1.4.2", "three": "0.177.0" }, "peerDependencies": { "web-tree-sitter": "0.25.10" } }, "sha512-8jUhNKnwCDO3Y2iiEmagoQLjgX5l1WbddQiwky8B5JU4FW0/WRHairBmU1kRAQBmhdeg57dVinSG4iu2PAtKEA=="], "@opentui/core": ["@opentui/core@0.1.99", "", { "dependencies": { "bun-ffi-structs": "0.1.2", "diff": "8.0.2", "jimp": "1.6.0", "marked": "17.0.1", "yoga-layout": "3.2.1" }, "optionalDependencies": { "@dimforge/rapier2d-simd-compat": "^0.17.3", "@opentui/core-darwin-arm64": "0.1.99", "@opentui/core-darwin-x64": "0.1.99", "@opentui/core-linux-arm64": "0.1.99", "@opentui/core-linux-x64": "0.1.99", "@opentui/core-win32-arm64": "0.1.99", "@opentui/core-win32-x64": "0.1.99", "bun-webgpu": "0.1.5", "planck": "^1.4.2", "three": "0.177.0" }, "peerDependencies": { "web-tree-sitter": "0.25.10" } }, "sha512-I3+AEgGzqNWIpWX9g2WOscSPwtQDNOm4KlBjxBWCZjLxkF07u77heWXF7OiAdhKLtNUW6TFiyt6yznqAZPdG3A=="],
"@opentui/core-darwin-arm64": ["@opentui/core-darwin-arm64@0.1.101", "", { "os": "darwin", "cpu": "arm64" }, "sha512-HtqZh8TIKCH1Nge5J0etBCpzYfPY4fVcq110uJm2As6D/dTTPv8r4J+KkrqoSphkpj/Y2b4t7KpqNHthXA0EVw=="], "@opentui/core-darwin-arm64": ["@opentui/core-darwin-arm64@0.1.99", "", { "os": "darwin", "cpu": "arm64" }, "sha512-bzVrqeX2vb5iWrc/ftOUOqeUY8XO+qSgoTwj5TXHuwagavgwD3Hpeyjx8+icnTTeM4pao0som1WR9xfye6/X5Q=="],
"@opentui/core-darwin-x64": ["@opentui/core-darwin-x64@0.1.101", "", { "os": "darwin", "cpu": "x64" }, "sha512-o5ClQWnGG1inRE2YZAatPw1jPEAJni00amcoIfKBj8e1WS+fQA+iQTq1xFunNcyNPObLDCVuW1X+NrbK9xmPvQ=="], "@opentui/core-darwin-x64": ["@opentui/core-darwin-x64@0.1.99", "", { "os": "darwin", "cpu": "x64" }, "sha512-VE4FrXBYpkxnvkqcCV1a8aN9jyyMJMihVW+V2NLCtp+4yQsj0AapG5TiUSN76XnmSZRptxDy5rBmEempeoIZbg=="],
"@opentui/core-linux-arm64": ["@opentui/core-linux-arm64@0.1.101", "", { "os": "linux", "cpu": "arm64" }, "sha512-E/weY7DQpaPWGYDPD0CROHowUotqnVlk7Kb6l9+iZCrxm9s7HPRHkcMDVmcWDqHEqa/J879EJcqaUDzDArqC+w=="], "@opentui/core-linux-arm64": ["@opentui/core-linux-arm64@0.1.99", "", { "os": "linux", "cpu": "arm64" }, "sha512-viXQsbpS7yHjYkl7+am32JdvG96QU9lvHh1UiZtpOxcNUUqiYmA2ZwZFPD2Bi54jNyj5l2hjH6YkD3DzE2FEWA=="],
"@opentui/core-linux-x64": ["@opentui/core-linux-x64@0.1.101", "", { "os": "linux", "cpu": "x64" }, "sha512-+Bfr8jLbbR1WREUMCCvSZ44G1+WU2lPqJx7x1StTa9iFNEdicxCdd0QQsO6cnKn5yW+2Pr/FdrqHbxSQw3ejbA=="], "@opentui/core-linux-x64": ["@opentui/core-linux-x64@0.1.99", "", { "os": "linux", "cpu": "x64" }, "sha512-WLoEFINOSp0tZSR9y4LUuGc7n4Y7H1wcpjUPzQ9vChkYDXrfZltEanzoDWbDcQ4kZQW5tHVC7LrZHpAsRLwFZg=="],
"@opentui/core-win32-arm64": ["@opentui/core-win32-arm64@0.1.101", "", { "os": "win32", "cpu": "arm64" }, "sha512-LTMIHJzJrVqS8mgpp+tuyVHuqYlicQTvFi/sTsJ6Xswf1asatsvZYsbQByhBLpFT80j10G7uvDa361S5gjCUDA=="], "@opentui/core-win32-arm64": ["@opentui/core-win32-arm64@0.1.99", "", { "os": "win32", "cpu": "arm64" }, "sha512-yWMOLWCEO8HdrctU1dMkgZC8qGkiO4Dwr4/e11tTvVpRmYhDsP/IR89ZjEEtOwnKwFOFuB/MxvflqaEWVQ2g5Q=="],
"@opentui/core-win32-x64": ["@opentui/core-win32-x64@0.1.101", "", { "os": "win32", "cpu": "x64" }, "sha512-VaMs5bg6y0tYKptaEK8Hy5wTp4m//wJRKUdW8uvrS9cFgxyovZGuw0+TfK3NgbdeX+8jWm8LEAiak4jle5BABg=="], "@opentui/core-win32-x64": ["@opentui/core-win32-x64@0.1.99", "", { "os": "win32", "cpu": "x64" }, "sha512-aYRlsL2w8YRL6vPd7/hrqlNVkXU3QowWb01TOvAcHS8UAsXaGFUr47kSDyjxDi1wg1MzmVduCfsC7T3NoThV1w=="],
"@opentui/solid": ["@opentui/solid@0.1.101", "", { "dependencies": { "@babel/core": "7.28.0", "@babel/preset-typescript": "7.27.1", "@opentui/core": "0.1.101", "babel-plugin-module-resolver": "5.0.2", "babel-preset-solid": "1.9.10", "entities": "7.0.1", "s-js": "^0.4.9" }, "peerDependencies": { "solid-js": "1.9.11" } }, "sha512-STY2FQYtVS2rhUgpslG6mM0EAkgobBDF91+B+SNmvXIkJwP+ydP6UVgcuIo5McIbb9GIbAODx5X2Q48PSR7hgw=="], "@opentui/solid": ["@opentui/solid@0.1.99", "", { "dependencies": { "@babel/core": "7.28.0", "@babel/preset-typescript": "7.27.1", "@opentui/core": "0.1.99", "babel-plugin-module-resolver": "5.0.2", "babel-preset-solid": "1.9.10", "entities": "7.0.1", "s-js": "^0.4.9" }, "peerDependencies": { "solid-js": "1.9.11" } }, "sha512-DrqqO4h2V88FmeIP2cErYkMU0ZK5MrUsZw3w6IzZpoXyyiL4/9qpWzUq+CXx+r16VP2iGxDJwGKUmtFAzUch2Q=="],
"@oslojs/asn1": ["@oslojs/asn1@1.0.0", "", { "dependencies": { "@oslojs/binary": "1.0.0" } }, "sha512-zw/wn0sj0j0QKbIXfIlnEcTviaCzYOY3V5rAyjR6YtOByFtJiT574+8p9Wlach0lZH9fddD4yb9laEAIl4vXQA=="], "@oslojs/asn1": ["@oslojs/asn1@1.0.0", "", { "dependencies": { "@oslojs/binary": "1.0.0" } }, "sha512-zw/wn0sj0j0QKbIXfIlnEcTviaCzYOY3V5rAyjR6YtOByFtJiT574+8p9Wlach0lZH9fddD4yb9laEAIl4vXQA=="],
@@ -2484,7 +2454,7 @@
"@valibot/to-json-schema": ["@valibot/to-json-schema@1.6.0", "", { "peerDependencies": { "valibot": "^1.3.0" } }, "sha512-d6rYyK5KVa2XdqamWgZ4/Nr+cXhxjy7lmpe6Iajw15J/jmU+gyxl2IEd1Otg1d7Rl3gOQL5reulnSypzBtYy1A=="], "@valibot/to-json-schema": ["@valibot/to-json-schema@1.6.0", "", { "peerDependencies": { "valibot": "^1.3.0" } }, "sha512-d6rYyK5KVa2XdqamWgZ4/Nr+cXhxjy7lmpe6Iajw15J/jmU+gyxl2IEd1Otg1d7Rl3gOQL5reulnSypzBtYy1A=="],
"@vercel/oidc": ["@vercel/oidc@3.2.0", "", {}, "sha512-UycprH3T6n3jH0k44NHMa7pnFHGu/N05MjojYr+Mc6I7obkoLIJujSWwin1pCvdy/eOxrI/l3uDLQsmcrOb4ug=="], "@vercel/oidc": ["@vercel/oidc@3.1.0", "", {}, "sha512-Fw28YZpRnA3cAHHDlkt7xQHiJ0fcL+NRcIqsocZQUSmbzeIKRpwttJjik5ZGanXP+vlA4SbTg+AbA3bP363l+w=="],
"@vitejs/plugin-react": ["@vitejs/plugin-react@4.7.0", "", { "dependencies": { "@babel/core": "^7.28.0", "@babel/plugin-transform-react-jsx-self": "^7.27.1", "@babel/plugin-transform-react-jsx-source": "^7.27.1", "@rolldown/pluginutils": "1.0.0-beta.27", "@types/babel__core": "^7.20.5", "react-refresh": "^0.17.0" }, "peerDependencies": { "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0" } }, "sha512-gUu9hwfWvvEDBBmgtAowQCojwZmJ5mcLn3aufeCsitijs3+f2NsrPtlAWIR6OPiqljl96GVCUbLe0HyqIpVaoA=="], "@vitejs/plugin-react": ["@vitejs/plugin-react@4.7.0", "", { "dependencies": { "@babel/core": "^7.28.0", "@babel/plugin-transform-react-jsx-self": "^7.27.1", "@babel/plugin-transform-react-jsx-source": "^7.27.1", "@rolldown/pluginutils": "1.0.0-beta.27", "@types/babel__core": "^7.20.5", "react-refresh": "^0.17.0" }, "peerDependencies": { "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0" } }, "sha512-gUu9hwfWvvEDBBmgtAowQCojwZmJ5mcLn3aufeCsitijs3+f2NsrPtlAWIR6OPiqljl96GVCUbLe0HyqIpVaoA=="],
@@ -2544,7 +2514,7 @@
"agentkeepalive": ["agentkeepalive@4.6.0", "", { "dependencies": { "humanize-ms": "^1.2.1" } }, "sha512-kja8j7PjmncONqaTsB8fQ+wE2mSU2DJ9D4XKoJ5PFWIdRMa6SLSN1ff4mOr4jCbfRSsxR4keIiySJU0N9T5hIQ=="], "agentkeepalive": ["agentkeepalive@4.6.0", "", { "dependencies": { "humanize-ms": "^1.2.1" } }, "sha512-kja8j7PjmncONqaTsB8fQ+wE2mSU2DJ9D4XKoJ5PFWIdRMa6SLSN1ff4mOr4jCbfRSsxR4keIiySJU0N9T5hIQ=="],
"ai": ["ai@6.0.168", "", { "dependencies": { "@ai-sdk/gateway": "3.0.104", "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.23", "@opentelemetry/api": "1.9.0" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-2HqCJuO+1V2aV7vfYs5LFEUfxbkGX+5oa54q/gCCTL7KLTdbxcCu5D7TdLA5kwsrs3Szgjah9q6D9tpjHM3hUQ=="], "ai": ["ai@6.0.158", "", { "dependencies": { "@ai-sdk/gateway": "3.0.95", "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.23", "@opentelemetry/api": "1.9.0" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-gLTp1UXFtMqKUi3XHs33K7UFglbvojkxF/aq337TxnLGOhHIW9+GyP2jwW4hYX87f1es+wId3VQoPRRu9zEStQ=="],
"ai-gateway-provider": ["ai-gateway-provider@3.1.2", "", { "optionalDependencies": { "@ai-sdk/amazon-bedrock": "^4.0.62", "@ai-sdk/anthropic": "^3.0.46", "@ai-sdk/azure": "^3.0.31", "@ai-sdk/cerebras": "^2.0.34", "@ai-sdk/cohere": "^3.0.21", "@ai-sdk/deepgram": "^2.0.20", "@ai-sdk/deepseek": "^2.0.20", "@ai-sdk/elevenlabs": "^2.0.20", "@ai-sdk/fireworks": "^2.0.34", "@ai-sdk/google": "^3.0.30", "@ai-sdk/google-vertex": "^4.0.61", "@ai-sdk/groq": "^3.0.24", "@ai-sdk/mistral": "^3.0.20", "@ai-sdk/openai": "^3.0.30", "@ai-sdk/perplexity": "^3.0.19", "@ai-sdk/xai": "^3.0.57", "@openrouter/ai-sdk-provider": "^2.2.3" }, "peerDependencies": { "@ai-sdk/openai-compatible": "^2.0.0", "@ai-sdk/provider": "^3.0.0", "@ai-sdk/provider-utils": "^4.0.0", "ai": "^6.0.0" } }, "sha512-krGNnJSoO/gJ7Hbe5nQDlsBpDUGIBGtMQTRUaW7s1MylsfvLduba0TLWzQaGtOmNRkP0pGhtGlwsnS6FNQMlyw=="], "ai-gateway-provider": ["ai-gateway-provider@3.1.2", "", { "optionalDependencies": { "@ai-sdk/amazon-bedrock": "^4.0.62", "@ai-sdk/anthropic": "^3.0.46", "@ai-sdk/azure": "^3.0.31", "@ai-sdk/cerebras": "^2.0.34", "@ai-sdk/cohere": "^3.0.21", "@ai-sdk/deepgram": "^2.0.20", "@ai-sdk/deepseek": "^2.0.20", "@ai-sdk/elevenlabs": "^2.0.20", "@ai-sdk/fireworks": "^2.0.34", "@ai-sdk/google": "^3.0.30", "@ai-sdk/google-vertex": "^4.0.61", "@ai-sdk/groq": "^3.0.24", "@ai-sdk/mistral": "^3.0.20", "@ai-sdk/openai": "^3.0.30", "@ai-sdk/perplexity": "^3.0.19", "@ai-sdk/xai": "^3.0.57", "@openrouter/ai-sdk-provider": "^2.2.3" }, "peerDependencies": { "@ai-sdk/openai-compatible": "^2.0.0", "@ai-sdk/provider": "^3.0.0", "@ai-sdk/provider-utils": "^4.0.0", "ai": "^6.0.0" } }, "sha512-krGNnJSoO/gJ7Hbe5nQDlsBpDUGIBGtMQTRUaW7s1MylsfvLduba0TLWzQaGtOmNRkP0pGhtGlwsnS6FNQMlyw=="],
@@ -3054,7 +3024,7 @@
"ejs": ["ejs@3.1.10", "", { "dependencies": { "jake": "^10.8.5" }, "bin": { "ejs": "bin/cli.js" } }, "sha512-UeJmFfOrAQS8OJWPZ4qtgHyWExa088/MtK5UEyoJGFH67cDEXkZSviOiKRCZ4Xij0zxI3JECgYs3oKx+AizQBA=="], "ejs": ["ejs@3.1.10", "", { "dependencies": { "jake": "^10.8.5" }, "bin": { "ejs": "bin/cli.js" } }, "sha512-UeJmFfOrAQS8OJWPZ4qtgHyWExa088/MtK5UEyoJGFH67cDEXkZSviOiKRCZ4Xij0zxI3JECgYs3oKx+AizQBA=="],
"electron": ["electron@41.2.1", "", { "dependencies": { "@electron/get": "^2.0.0", "@types/node": "^24.9.0", "extract-zip": "^2.0.1" }, "bin": { "electron": "cli.js" } }, "sha512-teeRThiYGTPKf/2yOW7zZA1bhb91KEQ4yLBPOg7GxpmnkLFLugKgQaAKOrCgdzwsXh/5mFIfmkm+4+wACJKwaA=="], "electron": ["electron@40.4.1", "", { "dependencies": { "@electron/get": "^2.0.0", "@types/node": "^24.9.0", "extract-zip": "^2.0.1" }, "bin": { "electron": "cli.js" } }, "sha512-N1ZXybQZL8kYemO8vAeh9nrk4mSvqlAO8xs0QCHkXIvRnuB/7VGwEehjvQbsU5/f4bmTKpG+2GQERe/zmKpudQ=="],
"electron-builder": ["electron-builder@26.8.1", "", { "dependencies": { "app-builder-lib": "26.8.1", "builder-util": "26.8.1", "builder-util-runtime": "9.5.1", "chalk": "^4.1.2", "ci-info": "^4.2.0", "dmg-builder": "26.8.1", "fs-extra": "^10.1.0", "lazy-val": "^1.0.5", "simple-update-notifier": "2.0.0", "yargs": "^17.6.2" }, "bin": { "electron-builder": "cli.js", "install-app-deps": "install-app-deps.js" } }, "sha512-uWhx1r74NGpCagG0ULs/P9Nqv2nsoo+7eo4fLUOB8L8MdWltq9odW/uuLXMFCDGnPafknYLZgjNX0ZIFRzOQAw=="], "electron-builder": ["electron-builder@26.8.1", "", { "dependencies": { "app-builder-lib": "26.8.1", "builder-util": "26.8.1", "builder-util-runtime": "9.5.1", "chalk": "^4.1.2", "ci-info": "^4.2.0", "dmg-builder": "26.8.1", "fs-extra": "^10.1.0", "lazy-val": "^1.0.5", "simple-update-notifier": "2.0.0", "yargs": "^17.6.2" }, "bin": { "electron-builder": "cli.js", "install-app-deps": "install-app-deps.js" } }, "sha512-uWhx1r74NGpCagG0ULs/P9Nqv2nsoo+7eo4fLUOB8L8MdWltq9odW/uuLXMFCDGnPafknYLZgjNX0ZIFRzOQAw=="],
@@ -3334,7 +3304,7 @@
"get-tsconfig": ["get-tsconfig@4.13.8", "", { "dependencies": { "resolve-pkg-maps": "^1.0.0" } }, "sha512-J87BxkLXykmisLQ+KA4x2+O6rVf+PJrtFUO8lGyiRg4lyxJLJ8/v0sRAKdVZQOy6tR6lMRAF1NqzCf9BQijm0w=="], "get-tsconfig": ["get-tsconfig@4.13.8", "", { "dependencies": { "resolve-pkg-maps": "^1.0.0" } }, "sha512-J87BxkLXykmisLQ+KA4x2+O6rVf+PJrtFUO8lGyiRg4lyxJLJ8/v0sRAKdVZQOy6tR6lMRAF1NqzCf9BQijm0w=="],
"ghostty-web": ["ghostty-web@github:anomalyco/ghostty-web#20bd361", {}, "anomalyco-ghostty-web-20bd361", "sha512-dW0nwaiBBcun9y5WJSvm3HxDLe5o9V0xLCndQvWonRVubU8CS1PHxZpLffyPt1YujPWC13ez03aWxcuKBPYYGQ=="], "ghostty-web": ["ghostty-web@github:anomalyco/ghostty-web#4af877d", {}, "anomalyco-ghostty-web-4af877d", "sha512-fbEK8mtr7ar4ySsF+JUGjhaZrane7dKphanN+SxHt5XXI6yLMAh/Hpf6sNCOyyVa2UlGCd7YpXG/T2v2RUAX+A=="],
"gifwrap": ["gifwrap@0.10.1", "", { "dependencies": { "image-q": "^4.0.0", "omggif": "^1.0.10" } }, "sha512-2760b1vpJHNmLzZ/ubTtNnEx5WApN/PYWJvXvgS+tL1egTTthayFYIQQNi136FLEDcN/IyEY2EcGpIITD6eYUw=="], "gifwrap": ["gifwrap@0.10.1", "", { "dependencies": { "image-q": "^4.0.0", "omggif": "^1.0.10" } }, "sha512-2760b1vpJHNmLzZ/ubTtNnEx5WApN/PYWJvXvgS+tL1egTTthayFYIQQNi136FLEDcN/IyEY2EcGpIITD6eYUw=="],
@@ -3342,7 +3312,7 @@
"github-slugger": ["github-slugger@2.0.0", "", {}, "sha512-IaOQ9puYtjrkq7Y0Ygl9KDZnrf/aiUJYUpVf89y8kyaxbRG7Y1SrX/jaumrv81vc61+kiMempujsM3Yw7w5qcw=="], "github-slugger": ["github-slugger@2.0.0", "", {}, "sha512-IaOQ9puYtjrkq7Y0Ygl9KDZnrf/aiUJYUpVf89y8kyaxbRG7Y1SrX/jaumrv81vc61+kiMempujsM3Yw7w5qcw=="],
"gitlab-ai-provider": ["gitlab-ai-provider@6.6.0", "", { "dependencies": { "@anthropic-ai/sdk": "^0.71.0", "@anycable/core": "^0.9.2", "graphql-request": "^6.1.0", "isomorphic-ws": "^5.0.0", "openai": "^6.16.0", "socket.io-client": "^4.8.1", "vscode-jsonrpc": "^8.2.1", "zod": "^3.25.76" }, "peerDependencies": { "@ai-sdk/provider": ">=3.0.0", "@ai-sdk/provider-utils": ">=4.0.0" } }, "sha512-jUxYnKA4XQaPc3wxACDZ8bPDXO0Mzx7cZaBDxbT2uGgLqtGZmSi+9tVNIg7louSS+s/ioVra3SoUz3iOFVhKPA=="], "gitlab-ai-provider": ["gitlab-ai-provider@6.4.2", "", { "dependencies": { "@anthropic-ai/sdk": "^0.71.0", "@anycable/core": "^0.9.2", "graphql-request": "^6.1.0", "isomorphic-ws": "^5.0.0", "openai": "^6.16.0", "socket.io-client": "^4.8.1", "vscode-jsonrpc": "^8.2.1", "zod": "^3.25.76" }, "peerDependencies": { "@ai-sdk/provider": ">=3.0.0", "@ai-sdk/provider-utils": ">=4.0.0" } }, "sha512-Wyw6uslCuipBOr/NYwAtpgXEUJj68iJY5aekad2DjePN99JetKVQBqkLgAy9PZp2EA4OuscfRQu9qKIBN/evNw=="],
"glob": ["glob@13.0.5", "", { "dependencies": { "minimatch": "^10.2.1", "minipass": "^7.1.2", "path-scurry": "^2.0.0" } }, "sha512-BzXxZg24Ibra1pbQ/zE7Kys4Ua1ks7Bn6pKLkVPZ9FZe4JQS6/Q7ef3LG1H+k7lUf5l4T3PLSyYyYJVYUvfgTw=="], "glob": ["glob@13.0.5", "", { "dependencies": { "minimatch": "^10.2.1", "minipass": "^7.1.2", "path-scurry": "^2.0.0" } }, "sha512-BzXxZg24Ibra1pbQ/zE7Kys4Ua1ks7Bn6pKLkVPZ9FZe4JQS6/Q7ef3LG1H+k7lUf5l4T3PLSyYyYJVYUvfgTw=="],
@@ -4510,6 +4480,8 @@
"rimraf": ["rimraf@2.6.3", "", { "dependencies": { "glob": "^7.1.3" }, "bin": { "rimraf": "./bin.js" } }, "sha512-mwqeW5XsA2qAejG46gYdENaxXjx9onRNCfn7L0duuP4hCuTIi/QO7PDK07KJfp1d+izWPrzEJDcSqBa0OZQriA=="], "rimraf": ["rimraf@2.6.3", "", { "dependencies": { "glob": "^7.1.3" }, "bin": { "rimraf": "./bin.js" } }, "sha512-mwqeW5XsA2qAejG46gYdENaxXjx9onRNCfn7L0duuP4hCuTIi/QO7PDK07KJfp1d+izWPrzEJDcSqBa0OZQriA=="],
"ripgrep": ["ripgrep@0.3.1", "", { "bin": { "rg": "lib/rg.mjs", "ripgrep": "lib/rg.mjs" } }, "sha512-6bDtNIBh1qPviVIU685/4uv0Ap5t8eS4wiJhy/tR2LdIeIey9CVasENlGS+ul3HnTmGANIp7AjnfsztsRmALfQ=="],
"roarr": ["roarr@2.15.4", "", { "dependencies": { "boolean": "^3.0.1", "detect-node": "^2.0.4", "globalthis": "^1.0.1", "json-stringify-safe": "^5.0.1", "semver-compare": "^1.0.0", "sprintf-js": "^1.1.2" } }, "sha512-CHhPh+UNHD2GTXNYhPWLnU8ONHdI+5DI+4EYIAOaiD63rHeYlZvyh8P+in5999TTSFgUYuKUAjzRI4mdh/p+2A=="], "roarr": ["roarr@2.15.4", "", { "dependencies": { "boolean": "^3.0.1", "detect-node": "^2.0.4", "globalthis": "^1.0.1", "json-stringify-safe": "^5.0.1", "semver-compare": "^1.0.0", "sprintf-js": "^1.1.2" } }, "sha512-CHhPh+UNHD2GTXNYhPWLnU8ONHdI+5DI+4EYIAOaiD63rHeYlZvyh8P+in5999TTSFgUYuKUAjzRI4mdh/p+2A=="],
"rollup": ["rollup@4.60.1", "", { "dependencies": { "@types/estree": "1.0.8" }, "optionalDependencies": { "@rollup/rollup-android-arm-eabi": "4.60.1", "@rollup/rollup-android-arm64": "4.60.1", "@rollup/rollup-darwin-arm64": "4.60.1", "@rollup/rollup-darwin-x64": "4.60.1", "@rollup/rollup-freebsd-arm64": "4.60.1", "@rollup/rollup-freebsd-x64": "4.60.1", "@rollup/rollup-linux-arm-gnueabihf": "4.60.1", "@rollup/rollup-linux-arm-musleabihf": "4.60.1", "@rollup/rollup-linux-arm64-gnu": "4.60.1", "@rollup/rollup-linux-arm64-musl": "4.60.1", "@rollup/rollup-linux-loong64-gnu": "4.60.1", "@rollup/rollup-linux-loong64-musl": "4.60.1", "@rollup/rollup-linux-ppc64-gnu": "4.60.1", "@rollup/rollup-linux-ppc64-musl": "4.60.1", "@rollup/rollup-linux-riscv64-gnu": "4.60.1", "@rollup/rollup-linux-riscv64-musl": "4.60.1", "@rollup/rollup-linux-s390x-gnu": "4.60.1", "@rollup/rollup-linux-x64-gnu": "4.60.1", "@rollup/rollup-linux-x64-musl": "4.60.1", "@rollup/rollup-openbsd-x64": "4.60.1", "@rollup/rollup-openharmony-arm64": "4.60.1", "@rollup/rollup-win32-arm64-msvc": "4.60.1", "@rollup/rollup-win32-ia32-msvc": "4.60.1", "@rollup/rollup-win32-x64-gnu": "4.60.1", "@rollup/rollup-win32-x64-msvc": "4.60.1", "fsevents": "~2.3.2" }, "bin": { "rollup": "dist/bin/rollup" } }, "sha512-VmtB2rFU/GroZ4oL8+ZqXgSA38O6GR8KSIvWmEFv63pQ0G6KaBH9s07PO8XTXP4vI+3UJUEypOfjkGfmSBBR0w=="], "rollup": ["rollup@4.60.1", "", { "dependencies": { "@types/estree": "1.0.8" }, "optionalDependencies": { "@rollup/rollup-android-arm-eabi": "4.60.1", "@rollup/rollup-android-arm64": "4.60.1", "@rollup/rollup-darwin-arm64": "4.60.1", "@rollup/rollup-darwin-x64": "4.60.1", "@rollup/rollup-freebsd-arm64": "4.60.1", "@rollup/rollup-freebsd-x64": "4.60.1", "@rollup/rollup-linux-arm-gnueabihf": "4.60.1", "@rollup/rollup-linux-arm-musleabihf": "4.60.1", "@rollup/rollup-linux-arm64-gnu": "4.60.1", "@rollup/rollup-linux-arm64-musl": "4.60.1", "@rollup/rollup-linux-loong64-gnu": "4.60.1", "@rollup/rollup-linux-loong64-musl": "4.60.1", "@rollup/rollup-linux-ppc64-gnu": "4.60.1", "@rollup/rollup-linux-ppc64-musl": "4.60.1", "@rollup/rollup-linux-riscv64-gnu": "4.60.1", "@rollup/rollup-linux-riscv64-musl": "4.60.1", "@rollup/rollup-linux-s390x-gnu": "4.60.1", "@rollup/rollup-linux-x64-gnu": "4.60.1", "@rollup/rollup-linux-x64-musl": "4.60.1", "@rollup/rollup-openbsd-x64": "4.60.1", "@rollup/rollup-openharmony-arm64": "4.60.1", "@rollup/rollup-win32-arm64-msvc": "4.60.1", "@rollup/rollup-win32-ia32-msvc": "4.60.1", "@rollup/rollup-win32-x64-gnu": "4.60.1", "@rollup/rollup-win32-x64-msvc": "4.60.1", "fsevents": "~2.3.2" }, "bin": { "rollup": "dist/bin/rollup" } }, "sha512-VmtB2rFU/GroZ4oL8+ZqXgSA38O6GR8KSIvWmEFv63pQ0G6KaBH9s07PO8XTXP4vI+3UJUEypOfjkGfmSBBR0w=="],
@@ -5180,7 +5152,7 @@
"@ai-sdk/alibaba/@ai-sdk/openai-compatible": ["@ai-sdk/openai-compatible@2.0.41", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.23" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-kNAGINk71AlOXx10Dq/PXw4t/9XjdK8uxfpVElRwtSFMdeSiLVt58p9TPx4/FJD+hxZuVhvxYj9r42osxWq79g=="], "@ai-sdk/alibaba/@ai-sdk/openai-compatible": ["@ai-sdk/openai-compatible@2.0.41", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.23" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-kNAGINk71AlOXx10Dq/PXw4t/9XjdK8uxfpVElRwtSFMdeSiLVt58p9TPx4/FJD+hxZuVhvxYj9r42osxWq79g=="],
"@ai-sdk/amazon-bedrock/@ai-sdk/anthropic": ["@ai-sdk/anthropic@3.0.71", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.23" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-bUWOzrzR0gJKJO/PLGMR4uH2dqEgqGhrsCV+sSpk4KtOEnUQlfjZI/F7BFlqSvVpFbjdgYRRLysAeEZpJ6S1lg=="], "@ai-sdk/amazon-bedrock/@ai-sdk/anthropic": ["@ai-sdk/anthropic@3.0.70", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.23" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-hubTFcfnG3NbrlcDW0tU2fsZhRy/7dF5GCymu4DzBQUYliy2lb7tCeeMhDtFBaYa01qSBHRjkwGnsAdUtDPCwA=="],
"@ai-sdk/amazon-bedrock/@smithy/eventstream-codec": ["@smithy/eventstream-codec@4.2.13", "", { "dependencies": { "@aws-crypto/crc32": "5.2.0", "@smithy/types": "^4.14.0", "@smithy/util-hex-encoding": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-vYahwBAtRaAcFbOmE9aLr12z7RiHYDSLcnogSdxfm7kKfsNa3wH+NU5r7vTeB5rKvLsWyPjVX8iH94brP7umiQ=="], "@ai-sdk/amazon-bedrock/@smithy/eventstream-codec": ["@smithy/eventstream-codec@4.2.13", "", { "dependencies": { "@aws-crypto/crc32": "5.2.0", "@smithy/types": "^4.14.0", "@smithy/util-hex-encoding": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-vYahwBAtRaAcFbOmE9aLr12z7RiHYDSLcnogSdxfm7kKfsNa3wH+NU5r7vTeB5rKvLsWyPjVX8iH94brP7umiQ=="],
@@ -5198,7 +5170,7 @@
"@ai-sdk/fireworks/@ai-sdk/openai-compatible": ["@ai-sdk/openai-compatible@2.0.41", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.23" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-kNAGINk71AlOXx10Dq/PXw4t/9XjdK8uxfpVElRwtSFMdeSiLVt58p9TPx4/FJD+hxZuVhvxYj9r42osxWq79g=="], "@ai-sdk/fireworks/@ai-sdk/openai-compatible": ["@ai-sdk/openai-compatible@2.0.41", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.23" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-kNAGINk71AlOXx10Dq/PXw4t/9XjdK8uxfpVElRwtSFMdeSiLVt58p9TPx4/FJD+hxZuVhvxYj9r42osxWq79g=="],
"@ai-sdk/google-vertex/@ai-sdk/anthropic": ["@ai-sdk/anthropic@3.0.71", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.23" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-bUWOzrzR0gJKJO/PLGMR4uH2dqEgqGhrsCV+sSpk4KtOEnUQlfjZI/F7BFlqSvVpFbjdgYRRLysAeEZpJ6S1lg=="], "@ai-sdk/google-vertex/@ai-sdk/anthropic": ["@ai-sdk/anthropic@3.0.70", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.23" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-hubTFcfnG3NbrlcDW0tU2fsZhRy/7dF5GCymu4DzBQUYliy2lb7tCeeMhDtFBaYa01qSBHRjkwGnsAdUtDPCwA=="],
"@ai-sdk/google-vertex/@ai-sdk/google": ["@ai-sdk/google@3.0.64", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.23" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-CbR82EgGPNrj/6q0HtclwuCqe0/pDShyv3nWDP/A9DroujzWXnLMlUJVrgPOsg4b40zQCwwVs2XSKCxvt/4QaA=="], "@ai-sdk/google-vertex/@ai-sdk/google": ["@ai-sdk/google@3.0.64", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.23" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-CbR82EgGPNrj/6q0HtclwuCqe0/pDShyv3nWDP/A9DroujzWXnLMlUJVrgPOsg4b40zQCwwVs2XSKCxvt/4QaA=="],
@@ -5610,18 +5582,6 @@
"@opencode-ai/web/@shikijs/transformers": ["@shikijs/transformers@3.20.0", "", { "dependencies": { "@shikijs/core": "3.20.0", "@shikijs/types": "3.20.0" } }, "sha512-PrHHMRr3Q5W1qB/42kJW6laqFyWdhrPF2hNR9qjOm1xcSiAO3hAHo7HaVyHE6pMyevmy3i51O8kuGGXC78uK3g=="], "@opencode-ai/web/@shikijs/transformers": ["@shikijs/transformers@3.20.0", "", { "dependencies": { "@shikijs/core": "3.20.0", "@shikijs/types": "3.20.0" } }, "sha512-PrHHMRr3Q5W1qB/42kJW6laqFyWdhrPF2hNR9qjOm1xcSiAO3hAHo7HaVyHE6pMyevmy3i51O8kuGGXC78uK3g=="],
"@opentelemetry/exporter-trace-otlp-http/@opentelemetry/resources": ["@opentelemetry/resources@2.6.1", "", { "dependencies": { "@opentelemetry/core": "2.6.1", "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.3.0 <1.10.0" } }, "sha512-lID/vxSuKWXM55XhAKNoYXu9Cutoq5hFdkbTdI/zDKQktXzcWBVhNsOkiZFTMU9UtEWuGRNe0HUgmsFldIdxVA=="],
"@opentelemetry/otlp-transformer/@opentelemetry/resources": ["@opentelemetry/resources@2.6.1", "", { "dependencies": { "@opentelemetry/core": "2.6.1", "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.3.0 <1.10.0" } }, "sha512-lID/vxSuKWXM55XhAKNoYXu9Cutoq5hFdkbTdI/zDKQktXzcWBVhNsOkiZFTMU9UtEWuGRNe0HUgmsFldIdxVA=="],
"@opentelemetry/resources/@opentelemetry/core": ["@opentelemetry/core@2.2.0", "", { "dependencies": { "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, "sha512-FuabnnUm8LflnieVxs6eP7Z383hgQU4W1e3KJS6aOG3RxWxcHyBxH8fDMHNgu/gFx/M2jvTOW/4/PHhLz6bjWw=="],
"@opentelemetry/sdk-logs/@opentelemetry/resources": ["@opentelemetry/resources@2.6.1", "", { "dependencies": { "@opentelemetry/core": "2.6.1", "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.3.0 <1.10.0" } }, "sha512-lID/vxSuKWXM55XhAKNoYXu9Cutoq5hFdkbTdI/zDKQktXzcWBVhNsOkiZFTMU9UtEWuGRNe0HUgmsFldIdxVA=="],
"@opentelemetry/sdk-metrics/@opentelemetry/resources": ["@opentelemetry/resources@2.6.1", "", { "dependencies": { "@opentelemetry/core": "2.6.1", "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.3.0 <1.10.0" } }, "sha512-lID/vxSuKWXM55XhAKNoYXu9Cutoq5hFdkbTdI/zDKQktXzcWBVhNsOkiZFTMU9UtEWuGRNe0HUgmsFldIdxVA=="],
"@opentelemetry/sdk-trace-base/@opentelemetry/resources": ["@opentelemetry/resources@2.6.1", "", { "dependencies": { "@opentelemetry/core": "2.6.1", "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.3.0 <1.10.0" } }, "sha512-lID/vxSuKWXM55XhAKNoYXu9Cutoq5hFdkbTdI/zDKQktXzcWBVhNsOkiZFTMU9UtEWuGRNe0HUgmsFldIdxVA=="],
"@opentui/solid/@babel/core": ["@babel/core@7.28.0", "", { "dependencies": { "@ampproject/remapping": "^2.2.0", "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.28.0", "@babel/helper-compilation-targets": "^7.27.2", "@babel/helper-module-transforms": "^7.27.3", "@babel/helpers": "^7.27.6", "@babel/parser": "^7.28.0", "@babel/template": "^7.27.2", "@babel/traverse": "^7.28.0", "@babel/types": "^7.28.0", "convert-source-map": "^2.0.0", "debug": "^4.1.0", "gensync": "^1.0.0-beta.2", "json5": "^2.2.3", "semver": "^6.3.1" } }, "sha512-UlLAnTPrFdNGoFtbSXwcGFQBtQZJCNjaN6hQNP3UPvuNXT1i82N26KL3dZeIpNalWywr9IuQuncaAfUaS1g6sQ=="], "@opentui/solid/@babel/core": ["@babel/core@7.28.0", "", { "dependencies": { "@ampproject/remapping": "^2.2.0", "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.28.0", "@babel/helper-compilation-targets": "^7.27.2", "@babel/helper-module-transforms": "^7.27.3", "@babel/helpers": "^7.27.6", "@babel/parser": "^7.28.0", "@babel/template": "^7.27.2", "@babel/traverse": "^7.28.0", "@babel/types": "^7.28.0", "convert-source-map": "^2.0.0", "debug": "^4.1.0", "gensync": "^1.0.0-beta.2", "json5": "^2.2.3", "semver": "^6.3.1" } }, "sha512-UlLAnTPrFdNGoFtbSXwcGFQBtQZJCNjaN6hQNP3UPvuNXT1i82N26KL3dZeIpNalWywr9IuQuncaAfUaS1g6sQ=="],
"@opentui/solid/babel-preset-solid": ["babel-preset-solid@1.9.10", "", { "dependencies": { "babel-plugin-jsx-dom-expressions": "^0.40.3" }, "peerDependencies": { "@babel/core": "^7.0.0", "solid-js": "^1.9.10" }, "optionalPeers": ["solid-js"] }, "sha512-HCelrgua/Y+kqO8RyL04JBWS/cVdrtUv/h45GntgQY+cJl4eBcKkCDV3TdMjtKx1nXwRaR9QXslM/Npm1dxdZQ=="], "@opentui/solid/babel-preset-solid": ["babel-preset-solid@1.9.10", "", { "dependencies": { "babel-plugin-jsx-dom-expressions": "^0.40.3" }, "peerDependencies": { "@babel/core": "^7.0.0", "solid-js": "^1.9.10" }, "optionalPeers": ["solid-js"] }, "sha512-HCelrgua/Y+kqO8RyL04JBWS/cVdrtUv/h45GntgQY+cJl4eBcKkCDV3TdMjtKx1nXwRaR9QXslM/Npm1dxdZQ=="],
@@ -5728,6 +5688,8 @@
"accepts/negotiator": ["negotiator@0.6.3", "", {}, "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg=="], "accepts/negotiator": ["negotiator@0.6.3", "", {}, "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg=="],
"ai/@ai-sdk/gateway": ["@ai-sdk/gateway@3.0.95", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.23", "@vercel/oidc": "3.1.0" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-ZmUNNbZl3V42xwQzPaNUi+s8eqR2lnrxf0bvB6YbLXpLjHYv0k2Y78t12cNOfY0bxGeuVVTLyk856uLuQIuXEQ=="],
"ai-gateway-provider/@ai-sdk/amazon-bedrock": ["@ai-sdk/amazon-bedrock@4.0.93", "", { "dependencies": { "@ai-sdk/anthropic": "3.0.69", "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.23", "@smithy/eventstream-codec": "^4.0.1", "@smithy/util-utf8": "^4.0.0", "aws4fetch": "^1.0.20" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-hcXDU8QDwpAzLVTuY932TQVlIij9+iaVTxc5mPGY6yb//JMAAC5hMVhg93IrxlrxWLvMgjezNgoZGwquR+SGnw=="], "ai-gateway-provider/@ai-sdk/amazon-bedrock": ["@ai-sdk/amazon-bedrock@4.0.93", "", { "dependencies": { "@ai-sdk/anthropic": "3.0.69", "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.23", "@smithy/eventstream-codec": "^4.0.1", "@smithy/util-utf8": "^4.0.0", "aws4fetch": "^1.0.20" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-hcXDU8QDwpAzLVTuY932TQVlIij9+iaVTxc5mPGY6yb//JMAAC5hMVhg93IrxlrxWLvMgjezNgoZGwquR+SGnw=="],
"ai-gateway-provider/@ai-sdk/anthropic": ["@ai-sdk/anthropic@3.0.69", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.23" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-LshR7X3pFugY0o41G2VKTmg1XoGpSl7uoYWfzk6zjVZLhCfeFiwgpOga+eTV4XY1VVpZwKVqRnkDbIL7K2eH5g=="], "ai-gateway-provider/@ai-sdk/anthropic": ["@ai-sdk/anthropic@3.0.69", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.23" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-LshR7X3pFugY0o41G2VKTmg1XoGpSl7uoYWfzk6zjVZLhCfeFiwgpOga+eTV4XY1VVpZwKVqRnkDbIL7K2eH5g=="],
@@ -5946,7 +5908,7 @@
"nypm/tinyexec": ["tinyexec@1.1.1", "", {}, "sha512-VKS/ZaQhhkKFMANmAOhhXVoIfBXblQxGX1myCQ2faQrfmobMftXeJPcZGp0gS07ocvGJWDLZGyOZDadDBqYIJg=="], "nypm/tinyexec": ["tinyexec@1.1.1", "", {}, "sha512-VKS/ZaQhhkKFMANmAOhhXVoIfBXblQxGX1myCQ2faQrfmobMftXeJPcZGp0gS07ocvGJWDLZGyOZDadDBqYIJg=="],
"opencode/@ai-sdk/anthropic": ["@ai-sdk/anthropic@3.0.71", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.23" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-bUWOzrzR0gJKJO/PLGMR4uH2dqEgqGhrsCV+sSpk4KtOEnUQlfjZI/F7BFlqSvVpFbjdgYRRLysAeEZpJ6S1lg=="], "opencode/@ai-sdk/anthropic": ["@ai-sdk/anthropic@3.0.70", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.23" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-hubTFcfnG3NbrlcDW0tU2fsZhRy/7dF5GCymu4DzBQUYliy2lb7tCeeMhDtFBaYa01qSBHRjkwGnsAdUtDPCwA=="],
"opencode/@ai-sdk/openai": ["@ai-sdk/openai@3.0.53", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.23" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-Wld+Rbc05KaUn08uBt06eEuwcgalcIFtIl32Yp+GxuZXUQwOb6YeAuq+C6da4ch6BurFoqEaLemJVwjBb7x+PQ=="], "opencode/@ai-sdk/openai": ["@ai-sdk/openai@3.0.53", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.23" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-Wld+Rbc05KaUn08uBt06eEuwcgalcIFtIl32Yp+GxuZXUQwOb6YeAuq+C6da4ch6BurFoqEaLemJVwjBb7x+PQ=="],
+1
View File
@@ -236,6 +236,7 @@ new sst.cloudflare.x.SolidStart("Console", {
SALESFORCE_INSTANCE_URL, SALESFORCE_INSTANCE_URL,
ZEN_BLACK_PRICE, ZEN_BLACK_PRICE,
ZEN_LITE_PRICE, ZEN_LITE_PRICE,
new sst.Secret("ZEN_LITE_COUPON_FIRST_MONTH_100_INVITEES"),
new sst.Secret("ZEN_LIMITS"), new sst.Secret("ZEN_LIMITS"),
new sst.Secret("ZEN_SESSION_SECRET"), new sst.Secret("ZEN_SESSION_SECRET"),
...ZEN_MODELS, ...ZEN_MODELS,
+4 -4
View File
@@ -1,8 +1,8 @@
{ {
"nodeModules": { "nodeModules": {
"x86_64-linux": "sha256-YcVW8AGN3TP34CoBoCw+Fx30RL1aveNvxr5eoeOgYeg=", "x86_64-linux": "sha256-tYAb5Mo39UW1VEejYuo0jW0jzH2OyY/HrqgiZL3rmjY=",
"aarch64-linux": "sha256-G/J3YFfrpEwXSHa25kNyUhYpwPhzNIZf/4v+RCfuslk=", "aarch64-linux": "sha256-3zGKV5UwokXpmY0nT1mry3IhNf2EQYLKT7ac+/trmQA=",
"aarch64-darwin": "sha256-dNPYrGWKoafk4rHqc34U34TtiJGk87yUv5tKnliQcWs=", "aarch64-darwin": "sha256-oKXAut7eu/eW5a43OT8+aFuH1F1tuIldTs+7PUXSCv4=",
"x86_64-darwin": "sha256-1LStvefCajGkbdXobMpk0IQyw9SQcQgGKE+U3Fc0Osw=" "x86_64-darwin": "sha256-Az+9X1scOEhw3aOO8laKJoZjiuz3qlLTIk1bx25P/z4="
} }
} }
+17 -18
View File
@@ -7,7 +7,6 @@
sysctl, sysctl,
makeBinaryWrapper, makeBinaryWrapper,
models-dev, models-dev,
ripgrep,
installShellFiles, installShellFiles,
versionCheckHook, versionCheckHook,
writableTmpDirAsHomeHook, writableTmpDirAsHomeHook,
@@ -52,25 +51,25 @@ stdenvNoCC.mkDerivation (finalAttrs: {
runHook postBuild runHook postBuild
''; '';
installPhase = '' installPhase =
runHook preInstall ''
runHook preInstall
install -Dm755 dist/opencode-*/bin/opencode $out/bin/opencode install -Dm755 dist/opencode-*/bin/opencode $out/bin/opencode
install -Dm644 schema.json $out/share/opencode/schema.json install -Dm644 schema.json $out/share/opencode/schema.json
''
wrapProgram $out/bin/opencode \ # bun runs sysctl to detect if dunning on rosetta2
--prefix PATH : ${ + lib.optionalString stdenvNoCC.hostPlatform.isDarwin ''
lib.makeBinPath ( wrapProgram $out/bin/opencode \
[ --prefix PATH : ${
ripgrep lib.makeBinPath [
sysctl
] ]
# bun runs sysctl to detect if dunning on rosetta2 }
++ lib.optional stdenvNoCC.hostPlatform.isDarwin sysctl ''
) + ''
} runHook postInstall
'';
runHook postInstall
'';
postInstall = lib.optionalString (stdenvNoCC.buildPlatform.canExecute stdenvNoCC.hostPlatform) '' postInstall = lib.optionalString (stdenvNoCC.buildPlatform.canExecute stdenvNoCC.hostPlatform) ''
# trick yargs into also generating zsh completions # trick yargs into also generating zsh completions
+2 -4
View File
@@ -7,7 +7,7 @@
"packageManager": "bun@1.3.11", "packageManager": "bun@1.3.11",
"scripts": { "scripts": {
"dev": "bun run --cwd packages/opencode --conditions=browser src/index.ts", "dev": "bun run --cwd packages/opencode --conditions=browser src/index.ts",
"dev:desktop": "bun --cwd packages/desktop-electron dev", "dev:desktop": "bun --cwd packages/desktop tauri dev",
"dev:web": "bun --cwd packages/app dev", "dev:web": "bun --cwd packages/app dev",
"dev:console": "ulimit -n 10240 2>/dev/null; bun run --cwd packages/console/app dev", "dev:console": "ulimit -n 10240 2>/dev/null; bun run --cwd packages/console/app dev",
"dev:storybook": "bun --cwd packages/storybook storybook", "dev:storybook": "bun --cwd packages/storybook storybook",
@@ -34,8 +34,6 @@
"@types/cross-spawn": "6.0.6", "@types/cross-spawn": "6.0.6",
"@octokit/rest": "22.0.0", "@octokit/rest": "22.0.0",
"@hono/zod-validator": "0.4.2", "@hono/zod-validator": "0.4.2",
"@opentui/core": "0.1.99",
"@opentui/solid": "0.1.99",
"ulid": "3.0.1", "ulid": "3.0.1",
"@kobalte/core": "0.13.11", "@kobalte/core": "0.13.11",
"@types/luxon": "3.7.1", "@types/luxon": "3.7.1",
@@ -53,7 +51,7 @@
"drizzle-kit": "1.0.0-beta.19-d95b7a4", "drizzle-kit": "1.0.0-beta.19-d95b7a4",
"drizzle-orm": "1.0.0-beta.19-d95b7a4", "drizzle-orm": "1.0.0-beta.19-d95b7a4",
"effect": "4.0.0-beta.48", "effect": "4.0.0-beta.48",
"ai": "6.0.168", "ai": "6.0.158",
"cross-spawn": "7.0.6", "cross-spawn": "7.0.6",
"hono": "4.10.7", "hono": "4.10.7",
"hono-openapi": "1.1.2", "hono-openapi": "1.1.2",
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "@opencode-ai/app", "name": "@opencode-ai/app",
"version": "1.14.18", "version": "1.4.6",
"description": "", "description": "",
"type": "module", "type": "module",
"exports": { "exports": {
@@ -8,7 +8,7 @@ import { Spinner } from "@opencode-ai/ui/spinner"
import { showToast } from "@opencode-ai/ui/toast" import { showToast } from "@opencode-ai/ui/toast"
import { Tooltip, TooltipKeybind } from "@opencode-ai/ui/tooltip" import { Tooltip, TooltipKeybind } from "@opencode-ai/ui/tooltip"
import { getFilename } from "@opencode-ai/shared/util/path" import { getFilename } from "@opencode-ai/shared/util/path"
import { createEffect, createMemo, createSignal, For, onMount, Show } from "solid-js" import { createEffect, createMemo, For, Show } from "solid-js"
import { createStore } from "solid-js/store" import { createStore } from "solid-js/store"
import { Portal } from "solid-js/web" import { Portal } from "solid-js/web"
import { useCommand } from "@/context/command" import { useCommand } from "@/context/command"
@@ -16,7 +16,6 @@ import { useLanguage } from "@/context/language"
import { useLayout } from "@/context/layout" import { useLayout } from "@/context/layout"
import { usePlatform } from "@/context/platform" import { usePlatform } from "@/context/platform"
import { useServer } from "@/context/server" import { useServer } from "@/context/server"
import { useSettings } from "@/context/settings"
import { useSync } from "@/context/sync" import { useSync } from "@/context/sync"
import { useTerminal } from "@/context/terminal" import { useTerminal } from "@/context/terminal"
import { focusTerminalById } from "@/pages/session/helpers" import { focusTerminalById } from "@/pages/session/helpers"
@@ -135,7 +134,6 @@ export function SessionHeader() {
const server = useServer() const server = useServer()
const platform = usePlatform() const platform = usePlatform()
const language = useLanguage() const language = useLanguage()
const settings = useSettings()
const sync = useSync() const sync = useSync()
const terminal = useTerminal() const terminal = useTerminal()
const { params, view } = useSessionLayout() const { params, view } = useSessionLayout()
@@ -153,11 +151,6 @@ export function SessionHeader() {
}) })
const hotkey = createMemo(() => command.keybind("file.open")) const hotkey = createMemo(() => command.keybind("file.open"))
const os = createMemo(() => detectOS(platform)) const os = createMemo(() => detectOS(platform))
const isDesktopBeta = platform.platform === "desktop" && import.meta.env.VITE_OPENCODE_CHANNEL === "beta"
const search = createMemo(() => !isDesktopBeta || settings.general.showSearch())
const tree = createMemo(() => !isDesktopBeta || settings.general.showFileTree())
const term = createMemo(() => !isDesktopBeta || settings.general.showTerminal())
const status = createMemo(() => !isDesktopBeta || settings.general.showStatus())
const [exists, setExists] = createStore<Partial<Record<OpenApp, boolean>>>({ const [exists, setExists] = createStore<Partial<Record<OpenApp, boolean>>>({
finder: true, finder: true,
@@ -269,16 +262,12 @@ export function SessionHeader() {
.catch((err: unknown) => showRequestError(language, err)) .catch((err: unknown) => showRequestError(language, err))
} }
const [centerMount, setCenterMount] = createSignal<HTMLElement | null>(null) const centerMount = createMemo(() => document.getElementById("opencode-titlebar-center"))
const [rightMount, setRightMount] = createSignal<HTMLElement | null>(null) const rightMount = createMemo(() => document.getElementById("opencode-titlebar-right"))
onMount(() => {
setCenterMount(document.getElementById("opencode-titlebar-center"))
setRightMount(document.getElementById("opencode-titlebar-right"))
})
return ( return (
<> <>
<Show when={search() && centerMount()}> <Show when={centerMount()}>
{(mount) => ( {(mount) => (
<Portal mount={mount()}> <Portal mount={mount()}>
<Button <Button
@@ -426,28 +415,24 @@ export function SessionHeader() {
</div> </div>
</Show> </Show>
<div class="flex items-center gap-1"> <div class="flex items-center gap-1">
<Show when={status()}> <Tooltip placement="bottom" value={language.t("status.popover.trigger")}>
<Tooltip placement="bottom" value={language.t("status.popover.trigger")}> <StatusPopover />
<StatusPopover /> </Tooltip>
</Tooltip> <TooltipKeybind
</Show> title={language.t("command.terminal.toggle")}
<Show when={term()}> keybind={command.keybind("terminal.toggle")}
<TooltipKeybind >
title={language.t("command.terminal.toggle")} <Button
keybind={command.keybind("terminal.toggle")} variant="ghost"
class="group/terminal-toggle titlebar-icon w-8 h-6 p-0 box-border shrink-0"
onClick={toggleTerminal}
aria-label={language.t("command.terminal.toggle")}
aria-expanded={view().terminal.opened()}
aria-controls="terminal-panel"
> >
<Button <Icon size="small" name={view().terminal.opened() ? "terminal-active" : "terminal"} />
variant="ghost" </Button>
class="group/terminal-toggle titlebar-icon w-8 h-6 p-0 box-border shrink-0" </TooltipKeybind>
onClick={toggleTerminal}
aria-label={language.t("command.terminal.toggle")}
aria-expanded={view().terminal.opened()}
aria-controls="terminal-panel"
>
<Icon size="small" name={view().terminal.opened() ? "terminal-active" : "terminal"} />
</Button>
</TooltipKeybind>
</Show>
<div class="hidden md:flex items-center gap-1 shrink-0"> <div class="hidden md:flex items-center gap-1 shrink-0">
<TooltipKeybind <TooltipKeybind
@@ -466,32 +451,30 @@ export function SessionHeader() {
</Button> </Button>
</TooltipKeybind> </TooltipKeybind>
<Show when={tree()}> <TooltipKeybind
<TooltipKeybind title={language.t("command.fileTree.toggle")}
title={language.t("command.fileTree.toggle")} keybind={command.keybind("fileTree.toggle")}
keybind={command.keybind("fileTree.toggle")} >
<Button
variant="ghost"
class="titlebar-icon w-8 h-6 p-0 box-border"
onClick={() => layout.fileTree.toggle()}
aria-label={language.t("command.fileTree.toggle")}
aria-expanded={layout.fileTree.opened()}
aria-controls="file-tree-panel"
> >
<Button <div class="relative flex items-center justify-center size-4">
variant="ghost" <Icon
class="titlebar-icon w-8 h-6 p-0 box-border" size="small"
onClick={() => layout.fileTree.toggle()} name={layout.fileTree.opened() ? "file-tree-active" : "file-tree"}
aria-label={language.t("command.fileTree.toggle")} classList={{
aria-expanded={layout.fileTree.opened()} "text-icon-strong": layout.fileTree.opened(),
aria-controls="file-tree-panel" "text-icon-weak": !layout.fileTree.opened(),
> }}
<div class="relative flex items-center justify-center size-4"> />
<Icon </div>
size="small" </Button>
name={layout.fileTree.opened() ? "file-tree-active" : "file-tree"} </TooltipKeybind>
classList={{
"text-icon-strong": layout.fileTree.opened(),
"text-icon-weak": !layout.fileTree.opened(),
}}
/>
</div>
</Button>
</TooltipKeybind>
</Show>
</div> </div>
</div> </div>
</div> </div>
@@ -19,9 +19,6 @@ import {
sansDefault, sansDefault,
sansFontFamily, sansFontFamily,
sansInput, sansInput,
terminalDefault,
terminalFontFamily,
terminalInput,
useSettings, useSettings,
} from "@/context/settings" } from "@/context/settings"
import { decode64 } from "@/utils/base64" import { decode64 } from "@/utils/base64"
@@ -109,7 +106,6 @@ export const SettingsGeneral: Component = () => {
permission.disableAutoAccept(params.id, value) permission.disableAutoAccept(params.id, value)
} }
const desktop = createMemo(() => platform.platform === "desktop")
const check = () => { const check = () => {
if (!platform.checkUpdate) return if (!platform.checkUpdate) return
@@ -184,7 +180,6 @@ export const SettingsGeneral: Component = () => {
const soundOptions = [noneSound, ...SOUND_OPTIONS] const soundOptions = [noneSound, ...SOUND_OPTIONS]
const mono = () => monoInput(settings.appearance.font()) const mono = () => monoInput(settings.appearance.font())
const sans = () => sansInput(settings.appearance.uiFont()) const sans = () => sansInput(settings.appearance.uiFont())
const terminal = () => terminalInput(settings.appearance.terminalFont())
const soundSelectProps = ( const soundSelectProps = (
enabled: () => boolean, enabled: () => boolean,
@@ -284,74 +279,6 @@ export const SettingsGeneral: Component = () => {
</div> </div>
) )
const AdvancedSection = () => (
<div class="flex flex-col gap-1">
<h3 class="text-14-medium text-text-strong pb-2">{language.t("settings.general.section.advanced")}</h3>
<SettingsList>
<SettingsRow
title={language.t("settings.general.row.showFileTree.title")}
description={language.t("settings.general.row.showFileTree.description")}
>
<div data-action="settings-show-file-tree">
<Switch
checked={settings.general.showFileTree()}
onChange={(checked) => settings.general.setShowFileTree(checked)}
/>
</div>
</SettingsRow>
<SettingsRow
title={language.t("settings.general.row.showNavigation.title")}
description={language.t("settings.general.row.showNavigation.description")}
>
<div data-action="settings-show-navigation">
<Switch
checked={settings.general.showNavigation()}
onChange={(checked) => settings.general.setShowNavigation(checked)}
/>
</div>
</SettingsRow>
<SettingsRow
title={language.t("settings.general.row.showSearch.title")}
description={language.t("settings.general.row.showSearch.description")}
>
<div data-action="settings-show-search">
<Switch
checked={settings.general.showSearch()}
onChange={(checked) => settings.general.setShowSearch(checked)}
/>
</div>
</SettingsRow>
<SettingsRow
title={language.t("settings.general.row.showTerminal.title")}
description={language.t("settings.general.row.showTerminal.description")}
>
<div data-action="settings-show-terminal">
<Switch
checked={settings.general.showTerminal()}
onChange={(checked) => settings.general.setShowTerminal(checked)}
/>
</div>
</SettingsRow>
<SettingsRow
title={language.t("settings.general.row.showStatus.title")}
description={language.t("settings.general.row.showStatus.description")}
>
<div data-action="settings-show-status">
<Switch
checked={settings.general.showStatus()}
onChange={(checked) => settings.general.setShowStatus(checked)}
/>
</div>
</SettingsRow>
</SettingsList>
</div>
)
const AppearanceSection = () => ( const AppearanceSection = () => (
<div class="flex flex-col gap-1"> <div class="flex flex-col gap-1">
<h3 class="text-14-medium text-text-strong pb-2">{language.t("settings.general.section.appearance")}</h3> <h3 class="text-14-medium text-text-strong pb-2">{language.t("settings.general.section.appearance")}</h3>
@@ -455,29 +382,6 @@ export const SettingsGeneral: Component = () => {
/> />
</div> </div>
</SettingsRow> </SettingsRow>
<SettingsRow
title={language.t("settings.general.row.terminalFont.title")}
description={language.t("settings.general.row.terminalFont.description")}
>
<div class="w-full sm:w-[220px]">
<TextField
data-action="settings-terminal-font"
label={language.t("settings.general.row.terminalFont.title")}
hideLabel
type="text"
value={terminal()}
onChange={(value) => settings.appearance.setTerminalFont(value)}
placeholder={terminalDefault}
spellcheck={false}
autocorrect="off"
autocomplete="off"
autocapitalize="off"
class="text-12-regular"
style={{ "font-family": terminalFontFamily(settings.appearance.terminalFont()) }}
/>
</div>
</SettingsRow>
</SettingsList> </SettingsList>
</div> </div>
) )
@@ -623,7 +527,6 @@ export const SettingsGeneral: Component = () => {
</div> </div>
) )
console.log(import.meta.env)
return ( return (
<div class="flex flex-col h-full overflow-y-auto no-scrollbar px-4 pb-10 sm:px-10 sm:pb-10"> <div class="flex flex-col h-full overflow-y-auto no-scrollbar px-4 pb-10 sm:px-10 sm:pb-10">
<div class="sticky top-0 z-10 bg-[linear-gradient(to_bottom,var(--surface-stronger-non-alpha)_calc(100%_-_24px),transparent)]"> <div class="sticky top-0 z-10 bg-[linear-gradient(to_bottom,var(--surface-stronger-non-alpha)_calc(100%_-_24px),transparent)]">
@@ -706,10 +609,6 @@ export const SettingsGeneral: Component = () => {
) )
}} }}
</Show> </Show>
<Show when={desktop() && import.meta.env.VITE_OPENCODE_CHANNEL === "beta"}>
<AdvancedSection />
</Show>
</div> </div>
</div> </div>
) )
+3 -3
View File
@@ -11,7 +11,7 @@ import { useLanguage } from "@/context/language"
import { usePlatform } from "@/context/platform" import { usePlatform } from "@/context/platform"
import { useSDK } from "@/context/sdk" import { useSDK } from "@/context/sdk"
import { useServer } from "@/context/server" import { useServer } from "@/context/server"
import { terminalFontFamily, useSettings } from "@/context/settings" import { monoFontFamily, useSettings } from "@/context/settings"
import type { LocalPTY } from "@/context/terminal" import type { LocalPTY } from "@/context/terminal"
import { disposeIfDisposable, getHoveredLinkText, setOptionIfSupported } from "@/utils/runtime-adapters" import { disposeIfDisposable, getHoveredLinkText, setOptionIfSupported } from "@/utils/runtime-adapters"
import { terminalWriter } from "@/utils/terminal-writer" import { terminalWriter } from "@/utils/terminal-writer"
@@ -300,7 +300,7 @@ export const Terminal = (props: TerminalProps) => {
}) })
createEffect(() => { createEffect(() => {
const font = terminalFontFamily(settings.appearance.terminalFont()) const font = monoFontFamily(settings.appearance.font())
if (!term) return if (!term) return
setOptionIfSupported(term, "fontFamily", font) setOptionIfSupported(term, "fontFamily", font)
scheduleFit() scheduleFit()
@@ -360,7 +360,7 @@ export const Terminal = (props: TerminalProps) => {
cols: restoreSize?.cols, cols: restoreSize?.cols,
rows: restoreSize?.rows, rows: restoreSize?.rows,
fontSize: 14, fontSize: 14,
fontFamily: terminalFontFamily(settings.appearance.terminalFont()), fontFamily: monoFontFamily(settings.appearance.font()),
allowTransparency: false, allowTransparency: false,
convertEol: false, convertEol: false,
theme: terminalColors(), theme: terminalColors(),
+3 -5
View File
@@ -11,7 +11,6 @@ import { useLayout } from "@/context/layout"
import { usePlatform } from "@/context/platform" import { usePlatform } from "@/context/platform"
import { useCommand } from "@/context/command" import { useCommand } from "@/context/command"
import { useLanguage } from "@/context/language" import { useLanguage } from "@/context/language"
import { useSettings } from "@/context/settings"
import { applyPath, backPath, forwardPath } from "./titlebar-history" import { applyPath, backPath, forwardPath } from "./titlebar-history"
type TauriDesktopWindow = { type TauriDesktopWindow = {
@@ -41,7 +40,6 @@ export function Titlebar() {
const platform = usePlatform() const platform = usePlatform()
const command = useCommand() const command = useCommand()
const language = useLanguage() const language = useLanguage()
const settings = useSettings()
const theme = useTheme() const theme = useTheme()
const navigate = useNavigate() const navigate = useNavigate()
const location = useLocation() const location = useLocation()
@@ -80,7 +78,6 @@ export function Titlebar() {
const canBack = createMemo(() => history.index > 0) const canBack = createMemo(() => history.index > 0)
const canForward = createMemo(() => history.index < history.stack.length - 1) const canForward = createMemo(() => history.index < history.stack.length - 1)
const hasProjects = createMemo(() => layout.projects.list().length > 0) const hasProjects = createMemo(() => layout.projects.list().length > 0)
const nav = createMemo(() => import.meta.env.VITE_OPENCODE_CHANNEL !== "beta" || settings.general.showNavigation())
const back = () => { const back = () => {
const next = backPath(history) const next = backPath(history)
@@ -258,12 +255,13 @@ export function Titlebar() {
<div <div
class="flex items-center shrink-0" class="flex items-center shrink-0"
classList={{ classList={{
"-translate-x-[36px]": layout.sidebar.opened() && !!params.dir, "translate-x-0": !layout.sidebar.opened(),
"-translate-x-[36px]": layout.sidebar.opened(),
"duration-180 ease-out": !layout.sidebar.opened(), "duration-180 ease-out": !layout.sidebar.opened(),
"duration-180 ease-in": layout.sidebar.opened(), "duration-180 ease-in": layout.sidebar.opened(),
}} }}
> >
<Show when={hasProjects() && nav()}> <Show when={hasProjects()}>
<div class="flex items-center gap-0 transition-transform"> <div class="flex items-center gap-0 transition-transform">
<Tooltip placement="bottom" value={language.t("common.goBack")} openDelay={2000}> <Tooltip placement="bottom" value={language.t("common.goBack")} openDelay={2000}>
<Button <Button
+2 -1
View File
@@ -204,7 +204,7 @@ function createGlobalSync() {
const limit = Math.max(store.limit + SESSION_RECENT_LIMIT, SESSION_RECENT_LIMIT) const limit = Math.max(store.limit + SESSION_RECENT_LIMIT, SESSION_RECENT_LIMIT)
const promise = queryClient const promise = queryClient
.fetchQuery({ .ensureQueryData({
...loadSessionsQuery(directory), ...loadSessionsQuery(directory),
queryFn: () => queryFn: () =>
loadRootSessionsWithFallback({ loadRootSessionsWithFallback({
@@ -264,6 +264,7 @@ function createGlobalSync() {
children.pin(directory) children.pin(directory)
const promise = Promise.resolve().then(async () => { const promise = Promise.resolve().then(async () => {
const child = children.ensureChild(directory) const child = children.ensureChild(directory)
child[1]("bootstrapPromise", promise!)
const cache = children.vcsCache.get(directory) const cache = children.vcsCache.get(directory)
if (!cache) return if (!cache) return
const sdk = sdkFor(directory) const sdk = sdkFor(directory)
@@ -182,6 +182,7 @@ export function createChildStoreManager(input: {
limit: 5, limit: 5,
message: {}, message: {},
part: {}, part: {},
bootstrapPromise: Promise.resolve(),
}) })
children[directory] = child children[directory] = child
disposers.set(directory, dispose) disposers.set(directory, dispose)
@@ -72,6 +72,7 @@ export type State = {
part: { part: {
[messageID: string]: Part[] [messageID: string]: Part[]
} }
bootstrapPromise: Promise<void>
} }
export type VcsCache = { export type VcsCache = {
-48
View File
@@ -23,11 +23,6 @@ export interface Settings {
autoSave: boolean autoSave: boolean
releaseNotes: boolean releaseNotes: boolean
followup: "queue" | "steer" followup: "queue" | "steer"
showFileTree: boolean
showNavigation: boolean
showSearch: boolean
showStatus: boolean
showTerminal: boolean
showReasoningSummaries: boolean showReasoningSummaries: boolean
shellToolPartsExpanded: boolean shellToolPartsExpanded: boolean
editToolPartsExpanded: boolean editToolPartsExpanded: boolean
@@ -39,7 +34,6 @@ export interface Settings {
fontSize: number fontSize: number
mono: string mono: string
sans: string sans: string
terminal: string
} }
keybinds: Record<string, string> keybinds: Record<string, string>
permissions: { permissions: {
@@ -51,17 +45,13 @@ export interface Settings {
export const monoDefault = "System Mono" export const monoDefault = "System Mono"
export const sansDefault = "System Sans" export const sansDefault = "System Sans"
export const terminalDefault = "JetBrainsMono Nerd Font Mono"
const monoFallback = const monoFallback =
'ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace' 'ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace'
const sansFallback = 'ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif' const sansFallback = 'ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif'
const terminalFallback =
'"JetBrainsMono Nerd Font Mono", ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace'
const monoBase = monoFallback const monoBase = monoFallback
const sansBase = sansFallback const sansBase = sansFallback
const terminalBase = terminalFallback
function input(font: string | undefined) { function input(font: string | undefined) {
return font ?? "" return font ?? ""
@@ -94,24 +84,11 @@ export function sansFontFamily(font: string | undefined) {
return stack(font, sansBase) return stack(font, sansBase)
} }
export function terminalInput(font: string | undefined) {
return input(font)
}
export function terminalFontFamily(font: string | undefined) {
return stack(font, terminalBase)
}
const defaultSettings: Settings = { const defaultSettings: Settings = {
general: { general: {
autoSave: true, autoSave: true,
releaseNotes: true, releaseNotes: true,
followup: "steer", followup: "steer",
showFileTree: false,
showNavigation: false,
showSearch: false,
showStatus: false,
showTerminal: false,
showReasoningSummaries: false, showReasoningSummaries: false,
shellToolPartsExpanded: false, shellToolPartsExpanded: false,
editToolPartsExpanded: false, editToolPartsExpanded: false,
@@ -123,7 +100,6 @@ const defaultSettings: Settings = {
fontSize: 14, fontSize: 14,
mono: "", mono: "",
sans: "", sans: "",
terminal: "",
}, },
keybinds: {}, keybinds: {},
permissions: { permissions: {
@@ -186,26 +162,6 @@ export const { use: useSettings, provider: SettingsProvider } = createSimpleCont
setFollowup(value: "queue" | "steer") { setFollowup(value: "queue" | "steer") {
setStore("general", "followup", value === "queue" ? "steer" : value) setStore("general", "followup", value === "queue" ? "steer" : value)
}, },
showFileTree: withFallback(() => store.general?.showFileTree, defaultSettings.general.showFileTree),
setShowFileTree(value: boolean) {
setStore("general", "showFileTree", value)
},
showNavigation: withFallback(() => store.general?.showNavigation, defaultSettings.general.showNavigation),
setShowNavigation(value: boolean) {
setStore("general", "showNavigation", value)
},
showSearch: withFallback(() => store.general?.showSearch, defaultSettings.general.showSearch),
setShowSearch(value: boolean) {
setStore("general", "showSearch", value)
},
showStatus: withFallback(() => store.general?.showStatus, defaultSettings.general.showStatus),
setShowStatus(value: boolean) {
setStore("general", "showStatus", value)
},
showTerminal: withFallback(() => store.general?.showTerminal, defaultSettings.general.showTerminal),
setShowTerminal(value: boolean) {
setStore("general", "showTerminal", value)
},
showReasoningSummaries: withFallback( showReasoningSummaries: withFallback(
() => store.general?.showReasoningSummaries, () => store.general?.showReasoningSummaries,
defaultSettings.general.showReasoningSummaries, defaultSettings.general.showReasoningSummaries,
@@ -247,10 +203,6 @@ export const { use: useSettings, provider: SettingsProvider } = createSimpleCont
setUIFont(value: string) { setUIFont(value: string) {
setStore("appearance", "sans", value.trim() ? value : "") setStore("appearance", "sans", value.trim() ? value : "")
}, },
terminalFont: withFallback(() => store.appearance?.terminal, defaultSettings.appearance.terminal),
setTerminalFont(value: string) {
setStore("appearance", "terminal", value.trim() ? value : "")
},
}, },
keybinds: { keybinds: {
get: (action: string) => store.keybinds?.[action], get: (action: string) => store.keybinds?.[action],
+4 -2
View File
@@ -1,14 +1,16 @@
import "solid-js"
interface ImportMetaEnv { interface ImportMetaEnv {
readonly VITE_OPENCODE_SERVER_HOST: string readonly VITE_OPENCODE_SERVER_HOST: string
readonly VITE_OPENCODE_SERVER_PORT: string readonly VITE_OPENCODE_SERVER_PORT: string
readonly VITE_OPENCODE_CHANNEL?: "dev" | "beta" | "prod" readonly OPENCODE_CHANNEL?: "dev" | "beta" | "prod"
} }
interface ImportMeta { interface ImportMeta {
readonly env: ImportMetaEnv readonly env: ImportMetaEnv
} }
export declare module "solid-js" { declare module "solid-js" {
namespace JSX { namespace JSX {
interface Directives { interface Directives {
sortable: true sortable: true
+1 -3
View File
@@ -565,9 +565,7 @@ export const dict = {
"settings.general.row.theme.title": "السمة", "settings.general.row.theme.title": "السمة",
"settings.general.row.theme.description": "تخصيص سمة OpenCode.", "settings.general.row.theme.description": "تخصيص سمة OpenCode.",
"settings.general.row.font.title": "خط الكود", "settings.general.row.font.title": "خط الكود",
"settings.general.row.font.description": "خصّص الخط المستخدم في كتل التعليمات البرمجية", "settings.general.row.font.description": "خصّص الخط المستخدم في كتل التعليمات البرمجية والطرفيات",
"settings.general.row.terminalFont.title": "Terminal Font",
"settings.general.row.terminalFont.description": "Customise the font used in the terminal",
"settings.general.row.uiFont.title": "خط الواجهة", "settings.general.row.uiFont.title": "خط الواجهة",
"settings.general.row.uiFont.description": "خصّص الخط المستخدم في الواجهة بأكملها", "settings.general.row.uiFont.description": "خصّص الخط المستخدم في الواجهة بأكملها",
"settings.general.row.followup.title": "سلوك المتابعة", "settings.general.row.followup.title": "سلوك المتابعة",
+1 -3
View File
@@ -572,9 +572,7 @@ export const dict = {
"settings.general.row.theme.title": "Tema", "settings.general.row.theme.title": "Tema",
"settings.general.row.theme.description": "Personalize como o OpenCode é tematizado.", "settings.general.row.theme.description": "Personalize como o OpenCode é tematizado.",
"settings.general.row.font.title": "Fonte de código", "settings.general.row.font.title": "Fonte de código",
"settings.general.row.font.description": "Personalize a fonte usada em blocos de código", "settings.general.row.font.description": "Personalize a fonte usada em blocos de código e terminais",
"settings.general.row.terminalFont.title": "Terminal Font",
"settings.general.row.terminalFont.description": "Customise the font used in the terminal",
"settings.general.row.uiFont.title": "Fonte da interface", "settings.general.row.uiFont.title": "Fonte da interface",
"settings.general.row.uiFont.description": "Personalize a fonte usada em toda a interface", "settings.general.row.uiFont.description": "Personalize a fonte usada em toda a interface",
"settings.general.row.followup.title": "Comportamento de acompanhamento", "settings.general.row.followup.title": "Comportamento de acompanhamento",
+1 -3
View File
@@ -637,9 +637,7 @@ export const dict = {
"settings.general.row.theme.title": "Tema", "settings.general.row.theme.title": "Tema",
"settings.general.row.theme.description": "Prilagodi temu OpenCode-a.", "settings.general.row.theme.description": "Prilagodi temu OpenCode-a.",
"settings.general.row.font.title": "Font za kod", "settings.general.row.font.title": "Font za kod",
"settings.general.row.font.description": "Prilagodi font koji se koristi u blokovima koda", "settings.general.row.font.description": "Prilagodi font koji se koristi u blokovima koda i terminalima",
"settings.general.row.terminalFont.title": "Terminal Font",
"settings.general.row.terminalFont.description": "Customise the font used in the terminal",
"settings.general.row.uiFont.title": "UI font", "settings.general.row.uiFont.title": "UI font",
"settings.general.row.uiFont.description": "Prilagodi font koji se koristi u cijelom interfejsu", "settings.general.row.uiFont.description": "Prilagodi font koji se koristi u cijelom interfejsu",
"settings.general.row.followup.title": "Ponašanje nadovezivanja", "settings.general.row.followup.title": "Ponašanje nadovezivanja",
+1 -3
View File
@@ -632,9 +632,7 @@ export const dict = {
"settings.general.row.theme.title": "Tema", "settings.general.row.theme.title": "Tema",
"settings.general.row.theme.description": "Tilpas hvordan OpenCode er temabestemt.", "settings.general.row.theme.description": "Tilpas hvordan OpenCode er temabestemt.",
"settings.general.row.font.title": "Kode-skrifttype", "settings.general.row.font.title": "Kode-skrifttype",
"settings.general.row.font.description": "Tilpas skrifttypen, der bruges i kodeblokke", "settings.general.row.font.description": "Tilpas skrifttypen, der bruges i kodeblokke og terminaler",
"settings.general.row.terminalFont.title": "Terminal Font",
"settings.general.row.terminalFont.description": "Customise the font used in the terminal",
"settings.general.row.uiFont.title": "UI-skrifttype", "settings.general.row.uiFont.title": "UI-skrifttype",
"settings.general.row.uiFont.description": "Tilpas skrifttypen, der bruges i hele brugerfladen", "settings.general.row.uiFont.description": "Tilpas skrifttypen, der bruges i hele brugerfladen",
"settings.general.row.followup.title": "Opfølgningsadfærd", "settings.general.row.followup.title": "Opfølgningsadfærd",
+1 -3
View File
@@ -582,9 +582,7 @@ export const dict = {
"settings.general.row.theme.title": "Thema", "settings.general.row.theme.title": "Thema",
"settings.general.row.theme.description": "Das Thema von OpenCode anpassen.", "settings.general.row.theme.description": "Das Thema von OpenCode anpassen.",
"settings.general.row.font.title": "Code-Schriftart", "settings.general.row.font.title": "Code-Schriftart",
"settings.general.row.font.description": "Die in Codeblöcken verwendete Schriftart anpassen", "settings.general.row.font.description": "Die in Codeblöcken und Terminals verwendete Schriftart anpassen",
"settings.general.row.terminalFont.title": "Terminal Font",
"settings.general.row.terminalFont.description": "Customise the font used in the terminal",
"settings.general.row.uiFont.title": "UI-Schriftart", "settings.general.row.uiFont.title": "UI-Schriftart",
"settings.general.row.uiFont.description": "Die im gesamten Interface verwendete Schriftart anpassen", "settings.general.row.uiFont.description": "Die im gesamten Interface verwendete Schriftart anpassen",
"settings.general.row.followup.title": "Verhalten bei Folgefragen", "settings.general.row.followup.title": "Verhalten bei Folgefragen",
+1 -14
View File
@@ -719,7 +719,6 @@ export const dict = {
"settings.desktop.wsl.description": "Run the OpenCode server inside WSL on Windows.", "settings.desktop.wsl.description": "Run the OpenCode server inside WSL on Windows.",
"settings.general.section.appearance": "Appearance", "settings.general.section.appearance": "Appearance",
"settings.general.section.advanced": "Advanced",
"settings.general.section.notifications": "System notifications", "settings.general.section.notifications": "System notifications",
"settings.general.section.updates": "Updates", "settings.general.section.updates": "Updates",
"settings.general.section.sounds": "Sound effects", "settings.general.section.sounds": "Sound effects",
@@ -735,25 +734,13 @@ export const dict = {
"settings.general.row.theme.title": "Theme", "settings.general.row.theme.title": "Theme",
"settings.general.row.theme.description": "Customise how OpenCode is themed.", "settings.general.row.theme.description": "Customise how OpenCode is themed.",
"settings.general.row.font.title": "Code Font", "settings.general.row.font.title": "Code Font",
"settings.general.row.font.description": "Customise the font used in code blocks", "settings.general.row.font.description": "Customise the font used in code blocks and terminals",
"settings.general.row.terminalFont.title": "Terminal Font",
"settings.general.row.terminalFont.description": "Customise the font used in the terminal",
"settings.general.row.uiFont.title": "UI Font", "settings.general.row.uiFont.title": "UI Font",
"settings.general.row.uiFont.description": "Customise the font used throughout the interface", "settings.general.row.uiFont.description": "Customise the font used throughout the interface",
"settings.general.row.followup.title": "Follow-up behavior", "settings.general.row.followup.title": "Follow-up behavior",
"settings.general.row.followup.description": "Choose whether follow-up prompts steer immediately or wait in a queue", "settings.general.row.followup.description": "Choose whether follow-up prompts steer immediately or wait in a queue",
"settings.general.row.followup.option.queue": "Queue", "settings.general.row.followup.option.queue": "Queue",
"settings.general.row.followup.option.steer": "Steer", "settings.general.row.followup.option.steer": "Steer",
"settings.general.row.showFileTree.title": "File tree",
"settings.general.row.showFileTree.description": "Show the file tree toggle and panel in desktop sessions",
"settings.general.row.showNavigation.title": "Navigation controls",
"settings.general.row.showNavigation.description": "Show the back and forward buttons in the desktop title bar",
"settings.general.row.showSearch.title": "Command palette",
"settings.general.row.showSearch.description": "Show the search and command palette button in the desktop title bar",
"settings.general.row.showTerminal.title": "Terminal",
"settings.general.row.showTerminal.description": "Show the terminal button in the desktop title bar",
"settings.general.row.showStatus.title": "Server status",
"settings.general.row.showStatus.description": "Show the server status button in the desktop title bar",
"settings.general.row.reasoningSummaries.title": "Show reasoning summaries", "settings.general.row.reasoningSummaries.title": "Show reasoning summaries",
"settings.general.row.reasoningSummaries.description": "Display model reasoning summaries in the timeline", "settings.general.row.reasoningSummaries.description": "Display model reasoning summaries in the timeline",
"settings.general.row.shellToolPartsExpanded.title": "Expand shell tool parts", "settings.general.row.shellToolPartsExpanded.title": "Expand shell tool parts",
+1 -3
View File
@@ -640,9 +640,7 @@ export const dict = {
"settings.general.row.theme.title": "Tema", "settings.general.row.theme.title": "Tema",
"settings.general.row.theme.description": "Personaliza el tema de OpenCode.", "settings.general.row.theme.description": "Personaliza el tema de OpenCode.",
"settings.general.row.font.title": "Fuente de código", "settings.general.row.font.title": "Fuente de código",
"settings.general.row.font.description": "Personaliza la fuente usada en bloques de código", "settings.general.row.font.description": "Personaliza la fuente usada en bloques de código y terminales",
"settings.general.row.terminalFont.title": "Terminal Font",
"settings.general.row.terminalFont.description": "Customise the font used in the terminal",
"settings.general.row.uiFont.title": "Fuente de la interfaz", "settings.general.row.uiFont.title": "Fuente de la interfaz",
"settings.general.row.uiFont.description": "Personaliza la fuente usada en toda la interfaz", "settings.general.row.uiFont.description": "Personaliza la fuente usada en toda la interfaz",
"settings.general.row.followup.title": "Comportamiento de seguimiento", "settings.general.row.followup.title": "Comportamiento de seguimiento",
+1 -3
View File
@@ -579,9 +579,7 @@ export const dict = {
"settings.general.row.theme.title": "Thème", "settings.general.row.theme.title": "Thème",
"settings.general.row.theme.description": "Personnaliser le thème d'OpenCode.", "settings.general.row.theme.description": "Personnaliser le thème d'OpenCode.",
"settings.general.row.font.title": "Police de code", "settings.general.row.font.title": "Police de code",
"settings.general.row.font.description": "Personnaliser la police utilisée dans les blocs de code", "settings.general.row.font.description": "Personnaliser la police utilisée dans les blocs de code et les terminaux",
"settings.general.row.terminalFont.title": "Terminal Font",
"settings.general.row.terminalFont.description": "Customise the font used in the terminal",
"settings.general.row.uiFont.title": "Police de l'interface", "settings.general.row.uiFont.title": "Police de l'interface",
"settings.general.row.uiFont.description": "Personnaliser la police utilisée dans toute l'interface", "settings.general.row.uiFont.description": "Personnaliser la police utilisée dans toute l'interface",
"settings.general.row.followup.title": "Comportement de suivi", "settings.general.row.followup.title": "Comportement de suivi",
+1 -3
View File
@@ -569,9 +569,7 @@ export const dict = {
"settings.general.row.theme.title": "テーマ", "settings.general.row.theme.title": "テーマ",
"settings.general.row.theme.description": "OpenCodeのテーマをカスタマイズします。", "settings.general.row.theme.description": "OpenCodeのテーマをカスタマイズします。",
"settings.general.row.font.title": "コードフォント", "settings.general.row.font.title": "コードフォント",
"settings.general.row.font.description": "コードブロックで使用するフォントをカスタマイズします", "settings.general.row.font.description": "コードブロックとターミナルで使用するフォントをカスタマイズします",
"settings.general.row.terminalFont.title": "Terminal Font",
"settings.general.row.terminalFont.description": "Customise the font used in the terminal",
"settings.general.row.uiFont.title": "UIフォント", "settings.general.row.uiFont.title": "UIフォント",
"settings.general.row.uiFont.description": "インターフェース全体で使用するフォントをカスタマイズします", "settings.general.row.uiFont.description": "インターフェース全体で使用するフォントをカスタマイズします",
"settings.general.row.followup.title": "フォローアップの動作", "settings.general.row.followup.title": "フォローアップの動作",
+1 -3
View File
@@ -566,9 +566,7 @@ export const dict = {
"settings.general.row.theme.title": "테마", "settings.general.row.theme.title": "테마",
"settings.general.row.theme.description": "OpenCode 테마 사용자 지정", "settings.general.row.theme.description": "OpenCode 테마 사용자 지정",
"settings.general.row.font.title": "코드 글꼴", "settings.general.row.font.title": "코드 글꼴",
"settings.general.row.font.description": "코드 블록에 사용되는 글꼴을 사용자 지정", "settings.general.row.font.description": "코드 블록과 터미널에 사용되는 글꼴을 사용자 지정",
"settings.general.row.terminalFont.title": "Terminal Font",
"settings.general.row.terminalFont.description": "Customise the font used in the terminal",
"settings.general.row.uiFont.title": "UI 글꼴", "settings.general.row.uiFont.title": "UI 글꼴",
"settings.general.row.uiFont.description": "인터페이스 전반에 사용되는 글꼴을 사용자 지정", "settings.general.row.uiFont.description": "인터페이스 전반에 사용되는 글꼴을 사용자 지정",
"settings.general.row.followup.title": "후속 조치 동작", "settings.general.row.followup.title": "후속 조치 동작",
+1 -3
View File
@@ -640,9 +640,7 @@ export const dict = {
"settings.general.row.theme.title": "Tema", "settings.general.row.theme.title": "Tema",
"settings.general.row.theme.description": "Tilpass hvordan OpenCode er tematisert.", "settings.general.row.theme.description": "Tilpass hvordan OpenCode er tematisert.",
"settings.general.row.font.title": "Kodefont", "settings.general.row.font.title": "Kodefont",
"settings.general.row.font.description": "Tilpass skrifttypen som brukes i kodeblokker", "settings.general.row.font.description": "Tilpass skrifttypen som brukes i kodeblokker og terminaler",
"settings.general.row.terminalFont.title": "Terminal Font",
"settings.general.row.terminalFont.description": "Customise the font used in the terminal",
"settings.general.row.uiFont.title": "UI-skrift", "settings.general.row.uiFont.title": "UI-skrift",
"settings.general.row.uiFont.description": "Tilpass skrifttypen som brukes i hele grensesnittet", "settings.general.row.uiFont.description": "Tilpass skrifttypen som brukes i hele grensesnittet",
"settings.general.row.followup.title": "Oppfølgingsadferd", "settings.general.row.followup.title": "Oppfølgingsadferd",
+1 -3
View File
@@ -571,9 +571,7 @@ export const dict = {
"settings.general.row.theme.title": "Motyw", "settings.general.row.theme.title": "Motyw",
"settings.general.row.theme.description": "Dostosuj motyw OpenCode.", "settings.general.row.theme.description": "Dostosuj motyw OpenCode.",
"settings.general.row.font.title": "Czcionka kodu", "settings.general.row.font.title": "Czcionka kodu",
"settings.general.row.font.description": "Dostosuj czcionkę używaną w blokach kodu", "settings.general.row.font.description": "Dostosuj czcionkę używaną w blokach kodu i terminalach",
"settings.general.row.terminalFont.title": "Terminal Font",
"settings.general.row.terminalFont.description": "Customise the font used in the terminal",
"settings.general.row.uiFont.title": "Czcionka interfejsu", "settings.general.row.uiFont.title": "Czcionka interfejsu",
"settings.general.row.uiFont.description": "Dostosuj czcionkę używaną w całym interfejsie", "settings.general.row.uiFont.description": "Dostosuj czcionkę używaną w całym interfejsie",
"settings.general.row.followup.title": "Zachowanie kontynuacji", "settings.general.row.followup.title": "Zachowanie kontynuacji",
+1 -3
View File
@@ -637,9 +637,7 @@ export const dict = {
"settings.general.row.theme.title": "Тема", "settings.general.row.theme.title": "Тема",
"settings.general.row.theme.description": "Настройте оформление OpenCode.", "settings.general.row.theme.description": "Настройте оформление OpenCode.",
"settings.general.row.font.title": "Шрифт кода", "settings.general.row.font.title": "Шрифт кода",
"settings.general.row.font.description": "Настройте шрифт, используемый в блоках кода", "settings.general.row.font.description": "Настройте шрифт, используемый в блоках кода и терминалах",
"settings.general.row.terminalFont.title": "Terminal Font",
"settings.general.row.terminalFont.description": "Customise the font used in the terminal",
"settings.general.row.uiFont.title": "Шрифт интерфейса", "settings.general.row.uiFont.title": "Шрифт интерфейса",
"settings.general.row.uiFont.description": "Настройте шрифт, используемый во всем интерфейсе", "settings.general.row.uiFont.description": "Настройте шрифт, используемый во всем интерфейсе",
"settings.general.row.followup.title": "Поведение уточняющих вопросов", "settings.general.row.followup.title": "Поведение уточняющих вопросов",
+1 -3
View File
@@ -631,9 +631,7 @@ export const dict = {
"settings.general.row.theme.title": "ธีม", "settings.general.row.theme.title": "ธีม",
"settings.general.row.theme.description": "ปรับแต่งวิธีการที่ OpenCode มีธีม", "settings.general.row.theme.description": "ปรับแต่งวิธีการที่ OpenCode มีธีม",
"settings.general.row.font.title": "ฟอนต์โค้ด", "settings.general.row.font.title": "ฟอนต์โค้ด",
"settings.general.row.font.description": "ปรับแต่งฟอนต์ที่ใช้ในบล็อกโค้ด", "settings.general.row.font.description": "ปรับแต่งฟอนต์ที่ใช้ในบล็อกโค้ดและเทอร์มินัล",
"settings.general.row.terminalFont.title": "Terminal Font",
"settings.general.row.terminalFont.description": "Customise the font used in the terminal",
"settings.general.row.uiFont.title": "ฟอนต์ UI", "settings.general.row.uiFont.title": "ฟอนต์ UI",
"settings.general.row.uiFont.description": "ปรับแต่งฟอนต์ที่ใช้ทั่วทั้งอินเทอร์เฟซ", "settings.general.row.uiFont.description": "ปรับแต่งฟอนต์ที่ใช้ทั่วทั้งอินเทอร์เฟซ",
"settings.general.row.followup.title": "พฤติกรรมการติดตามผล", "settings.general.row.followup.title": "พฤติกรรมการติดตามผล",
+1 -3
View File
@@ -644,9 +644,7 @@ export const dict = {
"settings.general.row.theme.title": "Tema", "settings.general.row.theme.title": "Tema",
"settings.general.row.theme.description": "OpenCode'un temasını özelleştirin.", "settings.general.row.theme.description": "OpenCode'un temasını özelleştirin.",
"settings.general.row.font.title": "Kod Yazı Tipi", "settings.general.row.font.title": "Kod Yazı Tipi",
"settings.general.row.font.description": "Kod bloklarında kullanılan yazı tipini özelleştirin", "settings.general.row.font.description": "Kod bloklarında ve terminallerde kullanılan yazı tipini özelleştirin",
"settings.general.row.terminalFont.title": "Terminal Font",
"settings.general.row.terminalFont.description": "Customise the font used in the terminal",
"settings.general.row.uiFont.title": "Arayüz Yazı Tipi", "settings.general.row.uiFont.title": "Arayüz Yazı Tipi",
"settings.general.row.uiFont.description": "Arayüz genelinde kullanılan yazı tipini özelleştirin", "settings.general.row.uiFont.description": "Arayüz genelinde kullanılan yazı tipini özelleştirin",
"settings.general.row.followup.title": "Takip davranışı", "settings.general.row.followup.title": "Takip davranışı",
+1 -3
View File
@@ -631,9 +631,7 @@ export const dict = {
"settings.general.row.theme.title": "主题", "settings.general.row.theme.title": "主题",
"settings.general.row.theme.description": "自定义 OpenCode 的主题。", "settings.general.row.theme.description": "自定义 OpenCode 的主题。",
"settings.general.row.font.title": "代码字体", "settings.general.row.font.title": "代码字体",
"settings.general.row.font.description": "自定义代码块使用的字体", "settings.general.row.font.description": "自定义代码块和终端使用的字体",
"settings.general.row.terminalFont.title": "Terminal Font",
"settings.general.row.terminalFont.description": "Customise the font used in the terminal",
"settings.general.row.uiFont.title": "界面字体", "settings.general.row.uiFont.title": "界面字体",
"settings.general.row.uiFont.description": "自定义整个界面使用的字体", "settings.general.row.uiFont.description": "自定义整个界面使用的字体",
"settings.general.row.followup.title": "跟进消息行为", "settings.general.row.followup.title": "跟进消息行为",
+1 -3
View File
@@ -626,9 +626,7 @@ export const dict = {
"settings.general.row.theme.title": "主題", "settings.general.row.theme.title": "主題",
"settings.general.row.theme.description": "自訂 OpenCode 的主題。", "settings.general.row.theme.description": "自訂 OpenCode 的主題。",
"settings.general.row.font.title": "程式碼字型", "settings.general.row.font.title": "程式碼字型",
"settings.general.row.font.description": "自訂程式碼區塊使用的字型", "settings.general.row.font.description": "自訂程式碼區塊和終端機使用的字型",
"settings.general.row.terminalFont.title": "Terminal Font",
"settings.general.row.terminalFont.description": "Customise the font used in the terminal",
"settings.general.row.uiFont.title": "介面字型", "settings.general.row.uiFont.title": "介面字型",
"settings.general.row.uiFont.description": "自訂整個介面使用的字型", "settings.general.row.uiFont.description": "自訂整個介面使用的字型",
"settings.general.row.followup.title": "後續追問行為", "settings.general.row.followup.title": "後續追問行為",
-7
View File
@@ -1,12 +1,5 @@
@import "@opencode-ai/ui/styles/tailwind"; @import "@opencode-ai/ui/styles/tailwind";
@font-face {
font-family: "JetBrainsMono Nerd Font Mono";
src: url("/assets/JetBrainsMonoNerdFontMono-Regular.woff2") format("woff2");
font-weight: normal;
font-style: normal;
}
@layer components { @layer components {
@keyframes session-progress-whip { @keyframes session-progress-whip {
0% { 0% {
+182 -180
View File
@@ -13,7 +13,7 @@ import {
type Accessor, type Accessor,
} from "solid-js" } from "solid-js"
import { makeEventListener } from "@solid-primitives/event-listener" import { makeEventListener } from "@solid-primitives/event-listener"
import { useLocation, useNavigate, useParams } from "@solidjs/router" import { useNavigate, useParams } from "@solidjs/router"
import { useLayout, LocalProject } from "@/context/layout" import { useLayout, LocalProject } from "@/context/layout"
import { useGlobalSync } from "@/context/global-sync" import { useGlobalSync } from "@/context/global-sync"
import { Persist, persisted } from "@/utils/persist" import { Persist, persisted } from "@/utils/persist"
@@ -127,7 +127,6 @@ export default function Layout(props: ParentProps) {
const theme = useTheme() const theme = useTheme()
const language = useLanguage() const language = useLanguage()
const initialDirectory = decode64(params.dir) const initialDirectory = decode64(params.dir)
const location = useLocation()
const route = createMemo(() => { const route = createMemo(() => {
const slug = params.dir const slug = params.dir
if (!slug) return { slug, dir: "" } if (!slug) return { slug, dir: "" }
@@ -2103,198 +2102,196 @@ export default function Layout(props: ParentProps) {
</Show> </Show>
} }
> >
{(project) => ( <>
<> <div class="shrink-0 pl-1 py-1">
<div class="shrink-0 pl-1 py-1"> <div class="group/project flex items-start justify-between gap-2 py-2 pl-2 pr-0">
<div class="group/project flex items-start justify-between gap-2 py-2 pl-2 pr-0"> <div class="flex flex-col min-w-0">
<div class="flex flex-col min-w-0"> <InlineEditor
<InlineEditor id={`project:${projectId()}`}
id={`project:${projectId()}`} value={projectName}
value={projectName} onSave={(next) => {
onSave={(next) => { const item = project()
const item = project() if (!item) return
if (!item) return void renameProject(item, next)
void renameProject(item, next) }}
}} class="text-14-medium text-text-strong truncate"
class="text-14-medium text-text-strong truncate" displayClass="text-14-medium text-text-strong truncate"
displayClass="text-14-medium text-text-strong truncate" stopPropagation
stopPropagation />
/>
<Tooltip <Tooltip
placement="bottom" placement="bottom"
gutter={2} gutter={2}
value={worktree()} value={worktree()}
class="shrink-0" class="shrink-0"
contentStyle={{ contentStyle={{
"max-width": "640px", "max-width": "640px",
transform: "translate3d(52px, 0, 0)", transform: "translate3d(52px, 0, 0)",
}} }}
> >
<span class="text-12-regular text-text-base truncate select-text"> <span class="text-12-regular text-text-base truncate select-text">
{worktree().replace(homedir(), "~")} {worktree().replace(homedir(), "~")}
</span> </span>
</Tooltip> </Tooltip>
</div>
<DropdownMenu modal={!sidebarHovering()}>
<DropdownMenu.Trigger
as={IconButton}
icon="dot-grid"
variant="ghost"
data-action="project-menu"
data-project={slug()}
class="shrink-0 size-6 rounded-md transition-opacity data-[expanded]:bg-surface-base-active"
classList={{
"opacity-100": panelProps.mobile || merged(),
"opacity-0 group-hover/project:opacity-100 group-focus-within/project:opacity-100 data-[expanded]:opacity-100":
!panelProps.mobile && !merged(),
}}
aria-label={language.t("common.moreOptions")}
/>
<DropdownMenu.Portal>
<DropdownMenu.Content class="mt-1">
<DropdownMenu.Item
onSelect={() => {
const item = project()
if (!item) return
showEditProjectDialog(item)
}}
>
<DropdownMenu.ItemLabel>{language.t("common.edit")}</DropdownMenu.ItemLabel>
</DropdownMenu.Item>
<DropdownMenu.Item
data-action="project-workspaces-toggle"
data-project={slug()}
disabled={!canToggle()}
onSelect={() => {
const item = project()
if (!item) return
toggleProjectWorkspaces(item)
}}
>
<DropdownMenu.ItemLabel>
{workspacesEnabled()
? language.t("sidebar.workspaces.disable")
: language.t("sidebar.workspaces.enable")}
</DropdownMenu.ItemLabel>
</DropdownMenu.Item>
<DropdownMenu.Item
data-action="project-clear-notifications"
data-project={slug()}
disabled={unseenCount() === 0}
onSelect={clearNotifications}
>
<DropdownMenu.ItemLabel>
{language.t("sidebar.project.clearNotifications")}
</DropdownMenu.ItemLabel>
</DropdownMenu.Item>
<DropdownMenu.Separator />
<DropdownMenu.Item
data-action="project-close-menu"
data-project={slug()}
onSelect={() => {
const dir = worktree()
if (!dir) return
closeProject(dir)
}}
>
<DropdownMenu.ItemLabel>{language.t("common.close")}</DropdownMenu.ItemLabel>
</DropdownMenu.Item>
</DropdownMenu.Content>
</DropdownMenu.Portal>
</DropdownMenu>
</div> </div>
</div>
<div class="flex-1 min-h-0 flex flex-col"> <DropdownMenu modal={!sidebarHovering()}>
<Show <DropdownMenu.Trigger
when={workspacesEnabled()} as={IconButton}
fallback={ icon="dot-grid"
<> variant="ghost"
<div class="shrink-0 py-4"> data-action="project-menu"
<Button data-project={slug()}
size="large" class="shrink-0 size-6 rounded-md transition-opacity data-[expanded]:bg-surface-base-active"
icon="new-session" classList={{
class="w-full" "opacity-100": panelProps.mobile || merged(),
onClick={() => { "opacity-0 group-hover/project:opacity-100 group-focus-within/project:opacity-100 data-[expanded]:opacity-100":
const dir = worktree() !panelProps.mobile && !merged(),
if (!dir) return }}
navigateWithSidebarReset(`/${base64Encode(dir)}/session`) aria-label={language.t("common.moreOptions")}
}} />
> <DropdownMenu.Portal>
{language.t("command.session.new")} <DropdownMenu.Content class="mt-1">
</Button> <DropdownMenu.Item
</div> onSelect={() => {
<div class="flex-1 min-h-0"> const item = project()
<LocalWorkspace if (!item) return
ctx={workspaceSidebarCtx} showEditProjectDialog(item)
project={project()} }}
sortNow={sortNow} >
mobile={panelProps.mobile} <DropdownMenu.ItemLabel>{language.t("common.edit")}</DropdownMenu.ItemLabel>
/> </DropdownMenu.Item>
</div> <DropdownMenu.Item
</> data-action="project-workspaces-toggle"
} data-project={slug()}
> disabled={!canToggle()}
onSelect={() => {
const item = project()
if (!item) return
toggleProjectWorkspaces(item)
}}
>
<DropdownMenu.ItemLabel>
{workspacesEnabled()
? language.t("sidebar.workspaces.disable")
: language.t("sidebar.workspaces.enable")}
</DropdownMenu.ItemLabel>
</DropdownMenu.Item>
<DropdownMenu.Item
data-action="project-clear-notifications"
data-project={slug()}
disabled={unseenCount() === 0}
onSelect={clearNotifications}
>
<DropdownMenu.ItemLabel>
{language.t("sidebar.project.clearNotifications")}
</DropdownMenu.ItemLabel>
</DropdownMenu.Item>
<DropdownMenu.Separator />
<DropdownMenu.Item
data-action="project-close-menu"
data-project={slug()}
onSelect={() => {
const dir = worktree()
if (!dir) return
closeProject(dir)
}}
>
<DropdownMenu.ItemLabel>{language.t("common.close")}</DropdownMenu.ItemLabel>
</DropdownMenu.Item>
</DropdownMenu.Content>
</DropdownMenu.Portal>
</DropdownMenu>
</div>
</div>
<div class="flex-1 min-h-0 flex flex-col">
<Show
when={workspacesEnabled()}
fallback={
<> <>
<div class="shrink-0 py-4"> <div class="shrink-0 py-4">
<Button <Button
size="large" size="large"
icon="plus-small" icon="new-session"
class="w-full" class="w-full"
onClick={() => { onClick={() => {
const item = project() const dir = worktree()
if (!item) return if (!dir) return
void createWorkspace(item) navigateWithSidebarReset(`/${base64Encode(dir)}/session`)
}} }}
> >
{language.t("workspace.new")} {language.t("command.session.new")}
</Button> </Button>
</div> </div>
<div class="relative flex-1 min-h-0"> <div class="flex-1 min-h-0">
<DragDropProvider <LocalWorkspace
onDragStart={handleWorkspaceDragStart} ctx={workspaceSidebarCtx}
onDragEnd={handleWorkspaceDragEnd} project={project()!}
onDragOver={handleWorkspaceDragOver} sortNow={sortNow}
collisionDetector={closestCenter} mobile={panelProps.mobile}
> />
<DragDropSensors />
<ConstrainDragXAxis />
<div
ref={(el) => {
if (!panelProps.mobile) scrollContainerRef = el
}}
class="size-full flex flex-col py-2 gap-4 overflow-y-auto no-scrollbar [overflow-anchor:none]"
>
<SortableProvider ids={workspaces()}>
<For each={workspaces()}>
{(directory) => (
<SortableWorkspace
ctx={workspaceSidebarCtx}
directory={directory}
project={project()}
sortNow={sortNow}
mobile={panelProps.mobile}
/>
)}
</For>
</SortableProvider>
</div>
<DragOverlay>
<WorkspaceDragOverlay
sidebarProject={sidebarProject}
activeWorkspace={() => store.activeWorkspace}
workspaceLabel={workspaceLabel}
/>
</DragOverlay>
</DragDropProvider>
</div> </div>
</> </>
</Show> }
</div> >
</> <>
)} <div class="shrink-0 py-4">
<Button
size="large"
icon="plus-small"
class="w-full"
onClick={() => {
const item = project()
if (!item) return
void createWorkspace(item)
}}
>
{language.t("workspace.new")}
</Button>
</div>
<div class="relative flex-1 min-h-0">
<DragDropProvider
onDragStart={handleWorkspaceDragStart}
onDragEnd={handleWorkspaceDragEnd}
onDragOver={handleWorkspaceDragOver}
collisionDetector={closestCenter}
>
<DragDropSensors />
<ConstrainDragXAxis />
<div
ref={(el) => {
if (!panelProps.mobile) scrollContainerRef = el
}}
class="size-full flex flex-col py-2 gap-4 overflow-y-auto no-scrollbar [overflow-anchor:none]"
>
<SortableProvider ids={workspaces()}>
<For each={workspaces()}>
{(directory) => (
<SortableWorkspace
ctx={workspaceSidebarCtx}
directory={directory}
project={project()!}
sortNow={sortNow}
mobile={panelProps.mobile}
/>
)}
</For>
</SortableProvider>
</div>
<DragOverlay>
<WorkspaceDragOverlay
sidebarProject={sidebarProject}
activeWorkspace={() => store.activeWorkspace}
workspaceLabel={workspaceLabel}
/>
</DragOverlay>
</DragDropProvider>
</div>
</>
</Show>
</div>
</>
</Show> </Show>
<div <div
@@ -2358,9 +2355,14 @@ export default function Layout(props: ParentProps) {
/> />
) )
const [loading] = createResource(
() => route()?.store?.[0]?.bootstrapPromise,
(p) => p,
)
return ( return (
<div class="relative bg-background-base flex-1 min-h-0 min-w-0 flex flex-col select-none [&_input]:select-text [&_textarea]:select-text [&_[contenteditable]]:select-text"> <div class="relative bg-background-base flex-1 min-h-0 min-w-0 flex flex-col select-none [&_input]:select-text [&_textarea]:select-text [&_[contenteditable]]:select-text">
{autoselecting() ?? ""} {(autoselecting(), loading()) ?? ""}
<Titlebar /> <Titlebar />
<div class="flex-1 min-h-0 min-w-0 flex"> <div class="flex-1 min-h-0 min-w-0 flex">
<div class="flex-1 min-h-0 relative"> <div class="flex-1 min-h-0 relative">
@@ -317,11 +317,12 @@ export const SortableWorkspace = (props: {
}) })
const open = createMemo(() => props.ctx.workspaceExpanded(props.directory, local())) const open = createMemo(() => props.ctx.workspaceExpanded(props.directory, local()))
const boot = createMemo(() => open() || active()) const boot = createMemo(() => open() || active())
const booted = createMemo((prev) => prev || workspaceStore.status === "complete", false)
const count = createMemo(() => sessions()?.length ?? 0) const count = createMemo(() => sessions()?.length ?? 0)
const hasMore = createMemo(() => workspaceStore.sessionTotal > count()) const hasMore = createMemo(() => workspaceStore.sessionTotal > count())
const query = useQuery(() => ({ ...loadSessionsQuery(props.project.worktree) }))
const busy = createMemo(() => props.ctx.isBusy(props.directory)) const busy = createMemo(() => props.ctx.isBusy(props.directory))
const loading = () => query.isLoading const wasBusy = createMemo((prev) => prev || busy(), false)
const loading = createMemo(() => open() && !booted() && count() === 0 && !wasBusy())
const touch = createMediaQuery("(hover: none)") const touch = createMediaQuery("(hover: none)")
const showNew = createMemo(() => !loading() && (touch() || count() === 0 || (active() && !params.id))) const showNew = createMemo(() => !loading() && (touch() || count() === 0 || (active() && !params.id)))
const loadMore = async () => { const loadMore = async () => {
@@ -426,7 +427,7 @@ export const SortableWorkspace = (props: {
mobile={props.mobile} mobile={props.mobile}
ctx={props.ctx} ctx={props.ctx}
showNew={showNew} showNew={showNew}
loading={() => query.isLoading && count() === 0} loading={loading}
sessions={sessions} sessions={sessions}
hasMore={hasMore} hasMore={hasMore}
loadMore={loadMore} loadMore={loadMore}
@@ -452,10 +453,11 @@ export const LocalWorkspace = (props: {
}) })
const slug = createMemo(() => base64Encode(props.project.worktree)) const slug = createMemo(() => base64Encode(props.project.worktree))
const sessions = createMemo(() => sortedRootSessions(workspace().store, props.sortNow())) const sessions = createMemo(() => sortedRootSessions(workspace().store, props.sortNow()))
const booted = createMemo((prev) => prev || workspace().store.status === "complete", false)
const count = createMemo(() => sessions()?.length ?? 0) const count = createMemo(() => sessions()?.length ?? 0)
const query = useQuery(() => ({ ...loadSessionsQuery(props.project.worktree) })) const query = useQuery(() => ({ ...loadSessionsQuery(props.project.worktree) }))
const loading = createMemo(() => query.isPending && count() === 0)
const hasMore = createMemo(() => workspace().store.sessionTotal > count()) const hasMore = createMemo(() => workspace().store.sessionTotal > count())
const loading = () => query.isLoading && count() === 0
const loadMore = async () => { const loadMore = async () => {
workspace().setStore("limit", (limit) => (limit ?? 0) + 5) workspace().setStore("limit", (limit) => (limit ?? 0) + 5)
await globalSync.project.loadSessions(props.project.worktree) await globalSync.project.loadSessions(props.project.worktree)
@@ -471,7 +473,7 @@ export const LocalWorkspace = (props: {
mobile={props.mobile} mobile={props.mobile}
ctx={props.ctx} ctx={props.ctx}
showNew={() => false} showNew={() => false}
loading={loading} loading={() => query.isLoading}
sessions={sessions} sessions={sessions}
hasMore={hasMore} hasMore={hasMore}
loadMore={loadMore} loadMore={loadMore}
+133 -45
View File
@@ -1,6 +1,6 @@
import type { Project, UserMessage } from "@opencode-ai/sdk/v2" import type { Project, UserMessage, VcsFileDiff } from "@opencode-ai/sdk/v2"
import { useDialog } from "@opencode-ai/ui/context/dialog" import { useDialog } from "@opencode-ai/ui/context/dialog"
import { createQuery, skipToken, useMutation, useQueryClient } from "@tanstack/solid-query" import { useMutation } from "@tanstack/solid-query"
import { import {
batch, batch,
onCleanup, onCleanup,
@@ -324,7 +324,6 @@ export default function Page() {
const local = useLocal() const local = useLocal()
const file = useFile() const file = useFile()
const sync = useSync() const sync = useSync()
const queryClient = useQueryClient()
const dialog = useDialog() const dialog = useDialog()
const language = useLanguage() const language = useLanguage()
const sdk = useSDK() const sdk = useSDK()
@@ -519,6 +518,26 @@ export default function Page() {
deferRender: false, deferRender: false,
}) })
const [vcs, setVcs] = createStore<{
diff: {
git: VcsFileDiff[]
branch: VcsFileDiff[]
}
ready: {
git: boolean
branch: boolean
}
}>({
diff: {
git: [] as VcsFileDiff[],
branch: [] as VcsFileDiff[],
},
ready: {
git: false,
branch: false,
},
})
const [followup, setFollowup] = persisted( const [followup, setFollowup] = persisted(
Persist.workspace(sdk.directory, "followup", ["followup.v1"]), Persist.workspace(sdk.directory, "followup", ["followup.v1"]),
createStore<{ createStore<{
@@ -552,6 +571,68 @@ export default function Page() {
let todoTimer: number | undefined let todoTimer: number | undefined
let diffFrame: number | undefined let diffFrame: number | undefined
let diffTimer: number | undefined let diffTimer: number | undefined
const vcsTask = new Map<VcsMode, Promise<void>>()
const vcsRun = new Map<VcsMode, number>()
const bumpVcs = (mode: VcsMode) => {
const next = (vcsRun.get(mode) ?? 0) + 1
vcsRun.set(mode, next)
return next
}
const resetVcs = (mode?: VcsMode) => {
const list = mode ? [mode] : (["git", "branch"] as const)
list.forEach((item) => {
bumpVcs(item)
vcsTask.delete(item)
setVcs("diff", item, [])
setVcs("ready", item, false)
})
}
const loadVcs = (mode: VcsMode, force = false) => {
if (sync.project?.vcs !== "git") return Promise.resolve()
if (!force && vcs.ready[mode]) return Promise.resolve()
if (force) {
if (vcsTask.has(mode)) bumpVcs(mode)
vcsTask.delete(mode)
setVcs("ready", mode, false)
}
const current = vcsTask.get(mode)
if (current) return current
const run = bumpVcs(mode)
const task = sdk.client.vcs
.diff({ mode })
.then((result) => {
if (vcsRun.get(mode) !== run) return
setVcs("diff", mode, list(result.data))
setVcs("ready", mode, true)
})
.catch((error) => {
if (vcsRun.get(mode) !== run) return
console.debug("[session-review] failed to load vcs diff", { mode, error })
setVcs("diff", mode, [])
setVcs("ready", mode, true)
})
.finally(() => {
if (vcsTask.get(mode) === task) vcsTask.delete(mode)
})
vcsTask.set(mode, task)
return task
}
const refreshVcs = () => {
resetVcs()
const mode = untrack(vcsMode)
if (!mode) return
if (!untrack(wantsReview)) return
void loadVcs(mode, true)
}
createComputed((prev) => { createComputed((prev) => {
const open = desktopReviewOpen() const open = desktopReviewOpen()
@@ -582,52 +663,21 @@ export default function Page() {
list.push("turn") list.push("turn")
return list return list
}) })
const mobileChanges = createMemo(() => !isDesktop() && store.mobileTab === "changes")
const wantsReview = createMemo(() =>
isDesktop()
? desktopFileTreeOpen() || (desktopReviewOpen() && activeTab() === "review")
: store.mobileTab === "changes",
)
const vcsMode = createMemo<VcsMode | undefined>(() => { const vcsMode = createMemo<VcsMode | undefined>(() => {
if (store.changes === "git" || store.changes === "branch") return store.changes if (store.changes === "git" || store.changes === "branch") return store.changes
}) })
const vcsKey = createMemo( const reviewDiffs = createMemo(() => {
() => ["session-vcs", sdk.directory, sync.data.vcs?.branch ?? "", sync.data.vcs?.default_branch ?? ""] as const, if (store.changes === "git") return list(vcs.diff.git)
) if (store.changes === "branch") return list(vcs.diff.branch)
const vcsQuery = createQuery(() => {
const mode = vcsMode()
const enabled = wantsReview() && sync.project?.vcs === "git"
return {
queryKey: [...vcsKey(), mode] as const,
enabled,
staleTime: Number.POSITIVE_INFINITY,
gcTime: 60 * 1000,
queryFn: mode
? () =>
sdk.client.vcs
.diff({ mode })
.then((result) => list(result.data))
.catch((error) => {
console.debug("[session-review] failed to load vcs diff", { mode, error })
return []
})
: skipToken,
}
})
const refreshVcs = () => void queryClient.invalidateQueries({ queryKey: vcsKey() })
const reviewDiffs = () => {
if (store.changes === "git" || store.changes === "branch")
// avoids suspense
return vcsQuery.isFetched ? (vcsQuery.data ?? []) : []
return turnDiffs() return turnDiffs()
} })
const reviewCount = () => reviewDiffs().length const reviewCount = createMemo(() => reviewDiffs().length)
const hasReview = () => reviewCount() > 0 const hasReview = createMemo(() => reviewCount() > 0)
const reviewReady = () => { const reviewReady = createMemo(() => {
if (store.changes === "git" || store.changes === "branch") return !vcsQuery.isPending if (store.changes === "git") return vcs.ready.git
if (store.changes === "branch") return vcs.ready.branch
return true return true
} })
const newSessionWorktree = createMemo(() => { const newSessionWorktree = createMemo(() => {
if (store.newSessionWorktree === "create") return "create" if (store.newSessionWorktree === "create") return "create"
@@ -847,6 +897,27 @@ export default function Page() {
), ),
) )
createEffect(
on(
() => sdk.directory,
() => {
resetVcs()
},
{ defer: true },
),
)
createEffect(
on(
() => [sync.data.vcs?.branch, sync.data.vcs?.default_branch] as const,
(next, prev) => {
if (prev === undefined || same(next, prev)) return
refreshVcs()
},
{ defer: true },
),
)
const stopVcs = sdk.event.listen((evt) => { const stopVcs = sdk.event.listen((evt) => {
if (evt.details.type !== "file.watcher.updated") return if (evt.details.type !== "file.watcher.updated") return
const props = const props =
@@ -980,6 +1051,13 @@ export default function Page() {
} }
} }
const mobileChanges = createMemo(() => !isDesktop() && store.mobileTab === "changes")
const wantsReview = createMemo(() =>
isDesktop()
? desktopFileTreeOpen() || (desktopReviewOpen() && activeTab() === "review")
: store.mobileTab === "changes",
)
createEffect(() => { createEffect(() => {
const list = changesOptions() const list = changesOptions()
if (list.includes(store.changes)) return if (list.includes(store.changes)) return
@@ -988,12 +1066,22 @@ export default function Page() {
setStore("changes", next) setStore("changes", next)
}) })
createEffect(() => {
const mode = vcsMode()
if (!mode) return
if (!wantsReview()) return
void loadVcs(mode)
})
createEffect( createEffect(
on( on(
() => sync.data.session_status[params.id ?? ""]?.type, () => sync.data.session_status[params.id ?? ""]?.type,
(next, prev) => { (next, prev) => {
const mode = vcsMode()
if (!mode) return
if (!wantsReview()) return
if (next !== "idle" || prev === undefined || prev === "idle") return if (next !== "idle" || prev === undefined || prev === "idle") return
refreshVcs() void loadVcs(mode, true)
}, },
{ defer: true }, { defer: true },
), ),
@@ -19,9 +19,6 @@ import { useCommand } from "@/context/command"
import { useFile, type SelectedLineRange } from "@/context/file" import { useFile, type SelectedLineRange } from "@/context/file"
import { useLanguage } from "@/context/language" import { useLanguage } from "@/context/language"
import { useLayout } from "@/context/layout" import { useLayout } from "@/context/layout"
import { usePlatform } from "@/context/platform"
import { useSettings } from "@/context/settings"
import { useSync } from "@/context/sync"
import { createFileTabListSync } from "@/pages/session/file-tab-scroll" import { createFileTabListSync } from "@/pages/session/file-tab-scroll"
import { FileTabContent } from "@/pages/session/file-tabs" import { FileTabContent } from "@/pages/session/file-tabs"
import { createOpenSessionFileTab, createSessionTabs, getTabReorderIndex, type Sizing } from "@/pages/session/helpers" import { createOpenSessionFileTab, createSessionTabs, getTabReorderIndex, type Sizing } from "@/pages/session/helpers"
@@ -42,9 +39,6 @@ export function SessionSidePanel(props: {
size: Sizing size: Sizing
}) { }) {
const layout = useLayout() const layout = useLayout()
const platform = usePlatform()
const settings = useSettings()
const sync = useSync()
const file = useFile() const file = useFile()
const language = useLanguage() const language = useLanguage()
const command = useCommand() const command = useCommand()
@@ -52,15 +46,9 @@ export function SessionSidePanel(props: {
const { sessionKey, tabs, view } = useSessionLayout() const { sessionKey, tabs, view } = useSessionLayout()
const isDesktop = createMediaQuery("(min-width: 768px)") const isDesktop = createMediaQuery("(min-width: 768px)")
const shown = createMemo(
() =>
platform.platform !== "desktop" ||
import.meta.env.VITE_OPENCODE_CHANNEL !== "beta" ||
settings.general.showFileTree(),
)
const reviewOpen = createMemo(() => isDesktop() && view().reviewPanel.opened()) const reviewOpen = createMemo(() => isDesktop() && view().reviewPanel.opened())
const fileOpen = createMemo(() => isDesktop() && shown() && layout.fileTree.opened()) const fileOpen = createMemo(() => isDesktop() && layout.fileTree.opened())
const open = createMemo(() => reviewOpen() || fileOpen()) const open = createMemo(() => reviewOpen() || fileOpen())
const reviewTab = createMemo(() => isDesktop()) const reviewTab = createMemo(() => isDesktop())
const panelWidth = createMemo(() => { const panelWidth = createMemo(() => {
@@ -353,99 +341,98 @@ export function SessionSidePanel(props: {
</div> </div>
</div> </div>
<Show when={shown()}> <div
id="file-tree-panel"
aria-hidden={!fileOpen()}
inert={!fileOpen()}
class="relative min-w-0 h-full shrink-0 overflow-hidden"
classList={{
"pointer-events-none": !fileOpen(),
"transition-[width] duration-200 ease-[cubic-bezier(0.22,1,0.36,1)] will-change-[width] motion-reduce:transition-none":
!props.size.active(),
}}
style={{ width: treeWidth() }}
>
<div <div
id="file-tree-panel" class="h-full flex flex-col overflow-hidden group/filetree"
aria-hidden={!fileOpen()} classList={{ "border-l border-border-weaker-base": reviewOpen() }}
inert={!fileOpen()}
class="relative min-w-0 h-full shrink-0 overflow-hidden"
classList={{
"pointer-events-none": !fileOpen(),
"transition-[width] duration-200 ease-[cubic-bezier(0.22,1,0.36,1)] will-change-[width] motion-reduce:transition-none":
!props.size.active(),
}}
style={{ width: treeWidth() }}
> >
<div <Tabs
class="h-full flex flex-col overflow-hidden group/filetree" variant="pill"
classList={{ "border-l border-border-weaker-base": reviewOpen() }} value={fileTreeTab()}
onChange={setFileTreeTabValue}
class="h-full"
data-scope="filetree"
> >
<Tabs <Tabs.List>
variant="pill" <Tabs.Trigger value="changes" class="flex-1" classes={{ button: "w-full" }}>
value={fileTreeTab()} {props.reviewCount()}{" "}
onChange={setFileTreeTabValue} {language.t(
class="h-full" props.reviewCount() === 1 ? "session.review.change.one" : "session.review.change.other",
data-scope="filetree" )}
> </Tabs.Trigger>
<Tabs.List> <Tabs.Trigger value="all" class="flex-1" classes={{ button: "w-full" }}>
<Tabs.Trigger value="changes" class="flex-1" classes={{ button: "w-full" }}> {language.t("session.files.all")}
{props.reviewCount()}{" "} </Tabs.Trigger>
{language.t( </Tabs.List>
props.reviewCount() === 1 ? "session.review.change.one" : "session.review.change.other", <Tabs.Content value="changes" class="bg-background-stronger px-3 py-0">
)} <Switch>
</Tabs.Trigger> <Match when={props.hasReview() || !props.diffsReady()}>
<Tabs.Trigger value="all" class="flex-1" classes={{ button: "w-full" }}> <Show
{language.t("session.files.all")} when={props.diffsReady()}
</Tabs.Trigger> fallback={
</Tabs.List> <div class="px-2 py-2 text-12-regular text-text-weak">
<Tabs.Content value="changes" class="bg-background-stronger px-3 py-0"> {language.t("common.loading")}
<Switch> {language.t("common.loading.ellipsis")}
<Match when={props.hasReview() || !props.diffsReady()}> </div>
<Show }
when={props.diffsReady()} >
fallback={
<div class="px-2 py-2 text-12-regular text-text-weak">
{language.t("common.loading")}
{language.t("common.loading.ellipsis")}
</div>
}
>
<FileTree
path=""
class="pt-3"
allowed={diffFiles()}
kinds={kinds()}
draggable={false}
active={props.activeDiff}
onFileClick={(node) => props.focusReviewDiff(node.path)}
/>
</Show>
</Match>
</Switch>
</Tabs.Content>
<Tabs.Content value="all" class="bg-background-stronger px-3 py-0">
<Switch>
<Match when={nofiles()}>{empty(language.t("session.files.empty"))}</Match>
<Match when={true}>
<FileTree <FileTree
path="" path=""
class="pt-3" class="pt-3"
modified={diffFiles()} allowed={diffFiles()}
kinds={kinds()} kinds={kinds()}
onFileClick={(node) => openTab(file.tab(node.path))} draggable={false}
active={props.activeDiff}
onFileClick={(node) => props.focusReviewDiff(node.path)}
/> />
</Match> </Show>
</Switch> </Match>
</Tabs.Content> <Match when={true}>{empty(props.empty())}</Match>
</Tabs> </Switch>
</div> </Tabs.Content>
<Show when={fileOpen()}> <Tabs.Content value="all" class="bg-background-stronger px-3 py-0">
<div onPointerDown={() => props.size.start()}> <Switch>
<ResizeHandle <Match when={nofiles()}>{empty(language.t("session.files.empty"))}</Match>
direction="horizontal" <Match when={true}>
edge="start" <FileTree
size={layout.fileTree.width()} path=""
min={200} class="pt-3"
max={480} modified={diffFiles()}
onResize={(width) => { kinds={kinds()}
props.size.touch() onFileClick={(node) => openTab(file.tab(node.path))}
layout.fileTree.resize(width) />
}} </Match>
/> </Switch>
</div> </Tabs.Content>
</Show> </Tabs>
</div> </div>
</Show> <Show when={fileOpen()}>
<div onPointerDown={() => props.size.start()}>
<ResizeHandle
direction="horizontal"
edge="start"
size={layout.fileTree.width()}
min={200}
max={480}
onResize={(width) => {
props.size.touch()
layout.fileTree.resize(width)
}}
/>
</div>
</Show>
</div>
</div> </div>
</aside> </aside>
</Show> </Show>
@@ -7,10 +7,8 @@ import { useLanguage } from "@/context/language"
import { useLayout } from "@/context/layout" import { useLayout } from "@/context/layout"
import { useLocal } from "@/context/local" import { useLocal } from "@/context/local"
import { usePermission } from "@/context/permission" import { usePermission } from "@/context/permission"
import { usePlatform } from "@/context/platform"
import { usePrompt } from "@/context/prompt" import { usePrompt } from "@/context/prompt"
import { useSDK } from "@/context/sdk" import { useSDK } from "@/context/sdk"
import { useSettings } from "@/context/settings"
import { useSync } from "@/context/sync" import { useSync } from "@/context/sync"
import { useTerminal } from "@/context/terminal" import { useTerminal } from "@/context/terminal"
import { showToast } from "@opencode-ai/ui/toast" import { showToast } from "@opencode-ai/ui/toast"
@@ -41,10 +39,8 @@ export const useSessionCommands = (actions: SessionCommandContext) => {
const language = useLanguage() const language = useLanguage()
const local = useLocal() const local = useLocal()
const permission = usePermission() const permission = usePermission()
const platform = usePlatform()
const prompt = usePrompt() const prompt = usePrompt()
const sdk = useSDK() const sdk = useSDK()
const settings = useSettings()
const sync = useSync() const sync = useSync()
const terminal = useTerminal() const terminal = useTerminal()
const layout = useLayout() const layout = useLayout()
@@ -70,10 +66,6 @@ export const useSessionCommands = (actions: SessionCommandContext) => {
}) })
const activeFileTab = tabState.activeFileTab const activeFileTab = tabState.activeFileTab
const closableTab = tabState.closableTab const closableTab = tabState.closableTab
const shown = () =>
platform.platform !== "desktop" ||
import.meta.env.VITE_OPENCODE_CHANNEL !== "beta" ||
settings.general.showFileTree()
const idle = { type: "idle" as const } const idle = { type: "idle" as const }
const status = () => sync.data.session_status[params.id ?? ""] ?? idle const status = () => sync.data.session_status[params.id ?? ""] ?? idle
@@ -465,16 +457,12 @@ export const useSessionCommands = (actions: SessionCommandContext) => {
keybind: "mod+shift+r", keybind: "mod+shift+r",
onSelect: () => view().reviewPanel.toggle(), onSelect: () => view().reviewPanel.toggle(),
}), }),
...(shown() viewCommand({
? [ id: "fileTree.toggle",
viewCommand({ title: language.t("command.fileTree.toggle"),
id: "fileTree.toggle", keybind: "mod+\\",
title: language.t("command.fileTree.toggle"), onSelect: () => layout.fileTree.toggle(),
keybind: "mod+\\", }),
onSelect: () => layout.fileTree.toggle(),
}),
]
: []),
viewCommand({ viewCommand({
id: "input.focus", id: "input.focus",
title: language.t("command.input.focus"), title: language.t("command.input.focus"),
+1 -1
View File
@@ -469,7 +469,7 @@ export function persisted<T>(
state, state,
setState, setState,
init, init,
Object.assign(() => (ready.loading ? false : ready.latest === true), { Object.assign(() => ready() === true, {
promise: init instanceof Promise ? init : undefined, promise: init instanceof Promise ? init : undefined,
}), }),
] ]
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "@opencode-ai/console-app", "name": "@opencode-ai/console-app",
"version": "1.14.18", "version": "1.4.6",
"type": "module", "type": "module",
"license": "MIT", "license": "MIT",
"scripts": { "scripts": {
+1 -8
View File
@@ -11,7 +11,7 @@ export const dict = {
"nav.enterprise": "المؤسسات", "nav.enterprise": "المؤسسات",
"nav.zen": "Zen", "nav.zen": "Zen",
"nav.login": "تسجيل الدخول", "nav.login": "تسجيل الدخول",
"nav.free": "تحميل", "nav.free": "مجانا",
"nav.home": "الرئيسية", "nav.home": "الرئيسية",
"nav.openMenu": "فتح القائمة", "nav.openMenu": "فتح القائمة",
"nav.getStartedFree": "ابدأ مجانا", "nav.getStartedFree": "ابدأ مجانا",
@@ -558,13 +558,6 @@ export const dict = {
"workspace.monthlyLimit.currentUsage.beforeMonth": "الاستخدام الحالي لـ", "workspace.monthlyLimit.currentUsage.beforeMonth": "الاستخدام الحالي لـ",
"workspace.monthlyLimit.currentUsage.beforeAmount": "هو $", "workspace.monthlyLimit.currentUsage.beforeAmount": "هو $",
"workspace.redeem.title": "استرداد قسيمة",
"workspace.redeem.subtitle": "استرد رمز القسيمة للحصول على رصيد أو مزايا.",
"workspace.redeem.placeholder": "أدخل رمز القسيمة",
"workspace.redeem.redeem": "استرداد",
"workspace.redeem.redeeming": "جارٍ الاسترداد...",
"workspace.redeem.success": "تم استرداد القسيمة بنجاح.",
"workspace.reload.title": "إعادة الشحن التلقائي", "workspace.reload.title": "إعادة الشحن التلقائي",
"workspace.reload.disabled.before": "إعادة الشحن التلقائي", "workspace.reload.disabled.before": "إعادة الشحن التلقائي",
"workspace.reload.disabled.state": "معطّل", "workspace.reload.disabled.state": "معطّل",
+1 -8
View File
@@ -11,7 +11,7 @@ export const dict = {
"nav.enterprise": "Enterprise", "nav.enterprise": "Enterprise",
"nav.zen": "Zen", "nav.zen": "Zen",
"nav.login": "Entrar", "nav.login": "Entrar",
"nav.free": "Download", "nav.free": "Grátis",
"nav.home": "Início", "nav.home": "Início",
"nav.openMenu": "Abrir menu", "nav.openMenu": "Abrir menu",
"nav.getStartedFree": "Começar grátis", "nav.getStartedFree": "Começar grátis",
@@ -567,13 +567,6 @@ export const dict = {
"workspace.monthlyLimit.currentUsage.beforeMonth": "Uso atual para", "workspace.monthlyLimit.currentUsage.beforeMonth": "Uso atual para",
"workspace.monthlyLimit.currentUsage.beforeAmount": "é $", "workspace.monthlyLimit.currentUsage.beforeAmount": "é $",
"workspace.redeem.title": "Resgatar Cupom",
"workspace.redeem.subtitle": "Resgate um código de cupom para receber créditos ou vantagens.",
"workspace.redeem.placeholder": "Digite o código do cupom",
"workspace.redeem.redeem": "Resgatar",
"workspace.redeem.redeeming": "Resgatando...",
"workspace.redeem.success": "Cupom resgatado com sucesso.",
"workspace.reload.title": "Recarga Automática", "workspace.reload.title": "Recarga Automática",
"workspace.reload.disabled.before": "A recarga automática está", "workspace.reload.disabled.before": "A recarga automática está",
"workspace.reload.disabled.state": "desativada", "workspace.reload.disabled.state": "desativada",
+1 -8
View File
@@ -11,7 +11,7 @@ export const dict = {
"nav.enterprise": "Enterprise", "nav.enterprise": "Enterprise",
"nav.zen": "Zen", "nav.zen": "Zen",
"nav.login": "Log ind", "nav.login": "Log ind",
"nav.free": "Download", "nav.free": "Gratis",
"nav.home": "Hjem", "nav.home": "Hjem",
"nav.openMenu": "Åbn menu", "nav.openMenu": "Åbn menu",
"nav.getStartedFree": "Kom i gang gratis", "nav.getStartedFree": "Kom i gang gratis",
@@ -563,13 +563,6 @@ export const dict = {
"workspace.monthlyLimit.currentUsage.beforeMonth": "Nuværende brug for", "workspace.monthlyLimit.currentUsage.beforeMonth": "Nuværende brug for",
"workspace.monthlyLimit.currentUsage.beforeAmount": "er $", "workspace.monthlyLimit.currentUsage.beforeAmount": "er $",
"workspace.redeem.title": "Indløs kupon",
"workspace.redeem.subtitle": "Indløs en kuponkode for at få kreditter eller fordele.",
"workspace.redeem.placeholder": "Indtast kuponkode",
"workspace.redeem.redeem": "Indløs",
"workspace.redeem.redeeming": "Indløser...",
"workspace.redeem.success": "Kuponen blev indløst.",
"workspace.reload.title": "Automatisk genopfyldning", "workspace.reload.title": "Automatisk genopfyldning",
"workspace.reload.disabled.before": "Automatisk genopfyldning er", "workspace.reload.disabled.before": "Automatisk genopfyldning er",
"workspace.reload.disabled.state": "deaktiveret", "workspace.reload.disabled.state": "deaktiveret",
+1 -8
View File
@@ -11,7 +11,7 @@ export const dict = {
"nav.enterprise": "Enterprise", "nav.enterprise": "Enterprise",
"nav.zen": "Zen", "nav.zen": "Zen",
"nav.login": "Anmelden", "nav.login": "Anmelden",
"nav.free": "Download", "nav.free": "Kostenlos",
"nav.home": "Startseite", "nav.home": "Startseite",
"nav.openMenu": "Menü öffnen", "nav.openMenu": "Menü öffnen",
"nav.getStartedFree": "Kostenlos starten", "nav.getStartedFree": "Kostenlos starten",
@@ -566,13 +566,6 @@ export const dict = {
"workspace.monthlyLimit.currentUsage.beforeMonth": "Aktuelle Nutzung für", "workspace.monthlyLimit.currentUsage.beforeMonth": "Aktuelle Nutzung für",
"workspace.monthlyLimit.currentUsage.beforeAmount": "ist $", "workspace.monthlyLimit.currentUsage.beforeAmount": "ist $",
"workspace.redeem.title": "Gutschein einlösen",
"workspace.redeem.subtitle": "Löse einen Gutscheincode ein, um Guthaben oder Vorteile zu erhalten.",
"workspace.redeem.placeholder": "Gutscheincode eingeben",
"workspace.redeem.redeem": "Einlösen",
"workspace.redeem.redeeming": "Wird eingelöst...",
"workspace.redeem.success": "Gutschein erfolgreich eingelöst.",
"workspace.reload.title": "Auto-Reload", "workspace.reload.title": "Auto-Reload",
"workspace.reload.disabled.before": "Auto-Reload ist", "workspace.reload.disabled.before": "Auto-Reload ist",
"workspace.reload.disabled.state": "deaktiviert", "workspace.reload.disabled.state": "deaktiviert",
+1 -8
View File
@@ -8,7 +8,7 @@ export const dict = {
"nav.zen": "Zen", "nav.zen": "Zen",
"nav.go": "Go", "nav.go": "Go",
"nav.login": "Login", "nav.login": "Login",
"nav.free": "Download", "nav.free": "Free",
"nav.home": "Home", "nav.home": "Home",
"nav.openMenu": "Open menu", "nav.openMenu": "Open menu",
"nav.getStartedFree": "Get started for free", "nav.getStartedFree": "Get started for free",
@@ -559,13 +559,6 @@ export const dict = {
"workspace.monthlyLimit.currentUsage.beforeMonth": "Current usage for", "workspace.monthlyLimit.currentUsage.beforeMonth": "Current usage for",
"workspace.monthlyLimit.currentUsage.beforeAmount": "is $", "workspace.monthlyLimit.currentUsage.beforeAmount": "is $",
"workspace.redeem.title": "Redeem Coupon",
"workspace.redeem.subtitle": "Redeem a coupon code to claim credits or perks.",
"workspace.redeem.placeholder": "Enter coupon code",
"workspace.redeem.redeem": "Redeem",
"workspace.redeem.redeeming": "Redeeming...",
"workspace.redeem.success": "Coupon redeemed successfully.",
"workspace.reload.title": "Auto Reload", "workspace.reload.title": "Auto Reload",
"workspace.reload.disabled.before": "Auto reload is", "workspace.reload.disabled.before": "Auto reload is",
"workspace.reload.disabled.state": "disabled", "workspace.reload.disabled.state": "disabled",
+1 -8
View File
@@ -11,7 +11,7 @@ export const dict = {
"nav.enterprise": "Enterprise", "nav.enterprise": "Enterprise",
"nav.zen": "Zen", "nav.zen": "Zen",
"nav.login": "Iniciar sesión", "nav.login": "Iniciar sesión",
"nav.free": "Descargar", "nav.free": "Gratis",
"nav.home": "Inicio", "nav.home": "Inicio",
"nav.openMenu": "Abrir menú", "nav.openMenu": "Abrir menú",
"nav.getStartedFree": "Empezar gratis", "nav.getStartedFree": "Empezar gratis",
@@ -567,13 +567,6 @@ export const dict = {
"workspace.monthlyLimit.currentUsage.beforeMonth": "Uso actual para", "workspace.monthlyLimit.currentUsage.beforeMonth": "Uso actual para",
"workspace.monthlyLimit.currentUsage.beforeAmount": "es $", "workspace.monthlyLimit.currentUsage.beforeAmount": "es $",
"workspace.redeem.title": "Canjear cupón",
"workspace.redeem.subtitle": "Canjea un código de cupón para obtener crédito o beneficios.",
"workspace.redeem.placeholder": "Introduce el código del cupón",
"workspace.redeem.redeem": "Canjear",
"workspace.redeem.redeeming": "Canjeando...",
"workspace.redeem.success": "Cupón canjeado correctamente.",
"workspace.reload.title": "Auto Recarga", "workspace.reload.title": "Auto Recarga",
"workspace.reload.disabled.before": "La auto recarga está", "workspace.reload.disabled.before": "La auto recarga está",
"workspace.reload.disabled.state": "deshabilitada", "workspace.reload.disabled.state": "deshabilitada",
+1 -8
View File
@@ -12,7 +12,7 @@ export const dict = {
"nav.enterprise": "Entreprise", "nav.enterprise": "Entreprise",
"nav.zen": "Zen", "nav.zen": "Zen",
"nav.login": "Se connecter", "nav.login": "Se connecter",
"nav.free": "Télécharger", "nav.free": "Gratuit",
"nav.home": "Accueil", "nav.home": "Accueil",
"nav.openMenu": "Ouvrir le menu", "nav.openMenu": "Ouvrir le menu",
"nav.getStartedFree": "Commencer gratuitement", "nav.getStartedFree": "Commencer gratuitement",
@@ -569,13 +569,6 @@ export const dict = {
"workspace.monthlyLimit.currentUsage.beforeMonth": "L'utilisation actuelle pour", "workspace.monthlyLimit.currentUsage.beforeMonth": "L'utilisation actuelle pour",
"workspace.monthlyLimit.currentUsage.beforeAmount": "est de", "workspace.monthlyLimit.currentUsage.beforeAmount": "est de",
"workspace.redeem.title": "Utiliser un coupon",
"workspace.redeem.subtitle": "Utilisez un code promo pour obtenir du crédit ou des avantages.",
"workspace.redeem.placeholder": "Saisissez le code promo",
"workspace.redeem.redeem": "Utiliser",
"workspace.redeem.redeeming": "Utilisation...",
"workspace.redeem.success": "Coupon utilisé avec succès.",
"workspace.reload.title": "Rechargement automatique", "workspace.reload.title": "Rechargement automatique",
"workspace.reload.disabled.before": "Le rechargement automatique est", "workspace.reload.disabled.before": "Le rechargement automatique est",
"workspace.reload.disabled.state": "désactivé", "workspace.reload.disabled.state": "désactivé",
+1 -8
View File
@@ -11,7 +11,7 @@ export const dict = {
"nav.enterprise": "Enterprise", "nav.enterprise": "Enterprise",
"nav.zen": "Zen", "nav.zen": "Zen",
"nav.login": "Accedi", "nav.login": "Accedi",
"nav.free": "Scarica", "nav.free": "Gratis",
"nav.home": "Home", "nav.home": "Home",
"nav.openMenu": "Apri menu", "nav.openMenu": "Apri menu",
"nav.getStartedFree": "Inizia gratis", "nav.getStartedFree": "Inizia gratis",
@@ -565,13 +565,6 @@ export const dict = {
"workspace.monthlyLimit.currentUsage.beforeMonth": "Utilizzo attuale per", "workspace.monthlyLimit.currentUsage.beforeMonth": "Utilizzo attuale per",
"workspace.monthlyLimit.currentUsage.beforeAmount": "è $", "workspace.monthlyLimit.currentUsage.beforeAmount": "è $",
"workspace.redeem.title": "Riscatta Coupon",
"workspace.redeem.subtitle": "Riscatta un codice coupon per ottenere credito o vantaggi.",
"workspace.redeem.placeholder": "Inserisci il codice coupon",
"workspace.redeem.redeem": "Riscatta",
"workspace.redeem.redeeming": "Riscatto in corso...",
"workspace.redeem.success": "Coupon riscattato con successo.",
"workspace.reload.title": "Ricarica Auto", "workspace.reload.title": "Ricarica Auto",
"workspace.reload.disabled.before": "La ricarica auto è", "workspace.reload.disabled.before": "La ricarica auto è",
"workspace.reload.disabled.state": "disabilitata", "workspace.reload.disabled.state": "disabilitata",
+1 -8
View File
@@ -11,7 +11,7 @@ export const dict = {
"nav.enterprise": "エンタープライズ", "nav.enterprise": "エンタープライズ",
"nav.zen": "Zen", "nav.zen": "Zen",
"nav.login": "ログイン", "nav.login": "ログイン",
"nav.free": "ダウンロード", "nav.free": "無料",
"nav.home": "ホーム", "nav.home": "ホーム",
"nav.openMenu": "メニューを開く", "nav.openMenu": "メニューを開く",
"nav.getStartedFree": "無料ではじめる", "nav.getStartedFree": "無料ではじめる",
@@ -564,13 +564,6 @@ export const dict = {
"workspace.monthlyLimit.currentUsage.beforeMonth": "現在の使用状況(", "workspace.monthlyLimit.currentUsage.beforeMonth": "現在の使用状況(",
"workspace.monthlyLimit.currentUsage.beforeAmount": ")は $", "workspace.monthlyLimit.currentUsage.beforeAmount": ")は $",
"workspace.redeem.title": "クーポンを利用",
"workspace.redeem.subtitle": "クーポンコードを利用して、クレジットや特典を受け取ります。",
"workspace.redeem.placeholder": "クーポンコードを入力",
"workspace.redeem.redeem": "利用する",
"workspace.redeem.redeeming": "利用中...",
"workspace.redeem.success": "クーポンを利用しました。",
"workspace.reload.title": "自動チャージ", "workspace.reload.title": "自動チャージ",
"workspace.reload.disabled.before": "自動チャージは", "workspace.reload.disabled.before": "自動チャージは",
"workspace.reload.disabled.state": "無効", "workspace.reload.disabled.state": "無効",
+1 -8
View File
@@ -11,7 +11,7 @@ export const dict = {
"nav.enterprise": "엔터프라이즈", "nav.enterprise": "엔터프라이즈",
"nav.zen": "Zen", "nav.zen": "Zen",
"nav.login": "로그인", "nav.login": "로그인",
"nav.free": "다운로드", "nav.free": "무료",
"nav.home": "홈", "nav.home": "홈",
"nav.openMenu": "메뉴 열기", "nav.openMenu": "메뉴 열기",
"nav.getStartedFree": "무료로 시작하기", "nav.getStartedFree": "무료로 시작하기",
@@ -558,13 +558,6 @@ export const dict = {
"workspace.monthlyLimit.currentUsage.beforeMonth": "현재", "workspace.monthlyLimit.currentUsage.beforeMonth": "현재",
"workspace.monthlyLimit.currentUsage.beforeAmount": "사용량: $", "workspace.monthlyLimit.currentUsage.beforeAmount": "사용량: $",
"workspace.redeem.title": "쿠폰 사용",
"workspace.redeem.subtitle": "쿠폰 코드를 사용해 크레딧이나 혜택을 받으세요.",
"workspace.redeem.placeholder": "쿠폰 코드를 입력하세요",
"workspace.redeem.redeem": "사용",
"workspace.redeem.redeeming": "사용 중...",
"workspace.redeem.success": "쿠폰을 성공적으로 사용했습니다.",
"workspace.reload.title": "자동 충전", "workspace.reload.title": "자동 충전",
"workspace.reload.disabled.before": "자동 충전이", "workspace.reload.disabled.before": "자동 충전이",
"workspace.reload.disabled.state": "비활성화", "workspace.reload.disabled.state": "비활성화",
+1 -8
View File
@@ -11,7 +11,7 @@ export const dict = {
"nav.enterprise": "Enterprise", "nav.enterprise": "Enterprise",
"nav.zen": "Zen", "nav.zen": "Zen",
"nav.login": "Logg inn", "nav.login": "Logg inn",
"nav.free": "Last ned", "nav.free": "Gratis",
"nav.home": "Hjem", "nav.home": "Hjem",
"nav.openMenu": "Åpne meny", "nav.openMenu": "Åpne meny",
"nav.getStartedFree": "Kom i gang gratis", "nav.getStartedFree": "Kom i gang gratis",
@@ -564,13 +564,6 @@ export const dict = {
"workspace.monthlyLimit.currentUsage.beforeMonth": "Gjeldende forbruk for", "workspace.monthlyLimit.currentUsage.beforeMonth": "Gjeldende forbruk for",
"workspace.monthlyLimit.currentUsage.beforeAmount": "er $", "workspace.monthlyLimit.currentUsage.beforeAmount": "er $",
"workspace.redeem.title": "Løs inn kupong",
"workspace.redeem.subtitle": "Løs inn en kupongkode for å få kreditt eller fordeler.",
"workspace.redeem.placeholder": "Skriv inn kupongkode",
"workspace.redeem.redeem": "Løs inn",
"workspace.redeem.redeeming": "Løser inn...",
"workspace.redeem.success": "Kupongen ble løst inn.",
"workspace.reload.title": "Auto-påfyll", "workspace.reload.title": "Auto-påfyll",
"workspace.reload.disabled.before": "Auto-påfyll er", "workspace.reload.disabled.before": "Auto-påfyll er",
"workspace.reload.disabled.state": "deaktivert", "workspace.reload.disabled.state": "deaktivert",
+1 -8
View File
@@ -10,7 +10,7 @@ export const dict = {
"nav.enterprise": "Enterprise", "nav.enterprise": "Enterprise",
"nav.zen": "Zen", "nav.zen": "Zen",
"nav.login": "Zaloguj się", "nav.login": "Zaloguj się",
"nav.free": "Pobierz", "nav.free": "Darmowe",
"nav.home": "Strona główna", "nav.home": "Strona główna",
"nav.openMenu": "Otwórz menu", "nav.openMenu": "Otwórz menu",
"nav.getStartedFree": "Zacznij za darmo", "nav.getStartedFree": "Zacznij za darmo",
@@ -565,13 +565,6 @@ export const dict = {
"workspace.monthlyLimit.currentUsage.beforeMonth": "Aktualne użycie za", "workspace.monthlyLimit.currentUsage.beforeMonth": "Aktualne użycie za",
"workspace.monthlyLimit.currentUsage.beforeAmount": "wynosi $", "workspace.monthlyLimit.currentUsage.beforeAmount": "wynosi $",
"workspace.redeem.title": "Zrealizuj kupon",
"workspace.redeem.subtitle": "Zrealizuj kod kuponu, aby otrzymać środki lub korzyści.",
"workspace.redeem.placeholder": "Wpisz kod kuponu",
"workspace.redeem.redeem": "Zrealizuj",
"workspace.redeem.redeeming": "Realizowanie...",
"workspace.redeem.success": "Kupon został zrealizowany.",
"workspace.reload.title": "Automatyczne doładowanie", "workspace.reload.title": "Automatyczne doładowanie",
"workspace.reload.disabled.before": "Automatyczne doładowanie jest", "workspace.reload.disabled.before": "Automatyczne doładowanie jest",
"workspace.reload.disabled.state": "wyłączone", "workspace.reload.disabled.state": "wyłączone",
+1 -8
View File
@@ -11,7 +11,7 @@ export const dict = {
"nav.enterprise": "Enterprise", "nav.enterprise": "Enterprise",
"nav.zen": "Zen", "nav.zen": "Zen",
"nav.login": "Войти", "nav.login": "Войти",
"nav.free": "Скачать", "nav.free": "Бесплатно",
"nav.home": "Главная", "nav.home": "Главная",
"nav.openMenu": "Открыть меню", "nav.openMenu": "Открыть меню",
"nav.getStartedFree": "Начать бесплатно", "nav.getStartedFree": "Начать бесплатно",
@@ -571,13 +571,6 @@ export const dict = {
"workspace.monthlyLimit.currentUsage.beforeMonth": "Текущее использование за", "workspace.monthlyLimit.currentUsage.beforeMonth": "Текущее использование за",
"workspace.monthlyLimit.currentUsage.beforeAmount": "составляет $", "workspace.monthlyLimit.currentUsage.beforeAmount": "составляет $",
"workspace.redeem.title": "Активировать купон",
"workspace.redeem.subtitle": "Активируйте код купона, чтобы получить кредит или бонусы.",
"workspace.redeem.placeholder": "Введите код купона",
"workspace.redeem.redeem": "Активировать",
"workspace.redeem.redeeming": "Активация...",
"workspace.redeem.success": "Купон успешно активирован.",
"workspace.reload.title": "Автопополнение", "workspace.reload.title": "Автопополнение",
"workspace.reload.disabled.before": "Автопополнение", "workspace.reload.disabled.before": "Автопополнение",
"workspace.reload.disabled.state": "отключено", "workspace.reload.disabled.state": "отключено",
+1 -8
View File
@@ -11,7 +11,7 @@ export const dict = {
"nav.enterprise": "องค์กร", "nav.enterprise": "องค์กร",
"nav.zen": "Zen", "nav.zen": "Zen",
"nav.login": "เข้าสู่ระบบ", "nav.login": "เข้าสู่ระบบ",
"nav.free": "ดาวน์โหลด", "nav.free": "ฟรี",
"nav.home": "หน้าหลัก", "nav.home": "หน้าหลัก",
"nav.openMenu": "เปิดเมนู", "nav.openMenu": "เปิดเมนู",
"nav.getStartedFree": "เริ่มต้นฟรี", "nav.getStartedFree": "เริ่มต้นฟรี",
@@ -560,13 +560,6 @@ export const dict = {
"workspace.monthlyLimit.currentUsage.beforeMonth": "การใช้งานปัจจุบันสำหรับ", "workspace.monthlyLimit.currentUsage.beforeMonth": "การใช้งานปัจจุบันสำหรับ",
"workspace.monthlyLimit.currentUsage.beforeAmount": "คือ $", "workspace.monthlyLimit.currentUsage.beforeAmount": "คือ $",
"workspace.redeem.title": "แลกคูปอง",
"workspace.redeem.subtitle": "แลกรหัสคูปองเพื่อรับเครดิตหรือสิทธิพิเศษ",
"workspace.redeem.placeholder": "กรอกรหัสคูปอง",
"workspace.redeem.redeem": "แลก",
"workspace.redeem.redeeming": "กำลังแลก...",
"workspace.redeem.success": "แลกคูปองสำเร็จ",
"workspace.reload.title": "โหลดซ้ำอัตโนมัติ", "workspace.reload.title": "โหลดซ้ำอัตโนมัติ",
"workspace.reload.disabled.before": "การโหลดซ้ำอัตโนมัติ", "workspace.reload.disabled.before": "การโหลดซ้ำอัตโนมัติ",
"workspace.reload.disabled.state": "ปิดใช้งานอยู่", "workspace.reload.disabled.state": "ปิดใช้งานอยู่",
+1 -8
View File
@@ -11,7 +11,7 @@ export const dict = {
"nav.enterprise": "Kurumsal", "nav.enterprise": "Kurumsal",
"nav.zen": "Zen", "nav.zen": "Zen",
"nav.login": "Giriş", "nav.login": "Giriş",
"nav.free": "İndir", "nav.free": "Ücretsiz",
"nav.home": "Ana sayfa", "nav.home": "Ana sayfa",
"nav.openMenu": "Menüyü aç", "nav.openMenu": "Menüyü aç",
"nav.getStartedFree": "Ücretsiz başla", "nav.getStartedFree": "Ücretsiz başla",
@@ -567,13 +567,6 @@ export const dict = {
"workspace.monthlyLimit.currentUsage.beforeMonth": "Şu anki kullanım", "workspace.monthlyLimit.currentUsage.beforeMonth": "Şu anki kullanım",
"workspace.monthlyLimit.currentUsage.beforeAmount": "$", "workspace.monthlyLimit.currentUsage.beforeAmount": "$",
"workspace.redeem.title": "Kupon Kullan",
"workspace.redeem.subtitle": "Kredi veya avantajlardan yararlanmak için bir kupon kodu kullanın.",
"workspace.redeem.placeholder": "Kupon kodunu girin",
"workspace.redeem.redeem": "Kullan",
"workspace.redeem.redeeming": "Kullanılıyor...",
"workspace.redeem.success": "Kupon başarıyla kullanıldı.",
"workspace.reload.title": "Otomatik Yeniden Yükleme", "workspace.reload.title": "Otomatik Yeniden Yükleme",
"workspace.reload.disabled.before": "Otomatik yeniden yükleme:", "workspace.reload.disabled.before": "Otomatik yeniden yükleme:",
"workspace.reload.disabled.state": "devre dışı", "workspace.reload.disabled.state": "devre dışı",
+1 -8
View File
@@ -11,7 +11,7 @@ export const dict = {
"nav.enterprise": "企业版", "nav.enterprise": "企业版",
"nav.zen": "Zen", "nav.zen": "Zen",
"nav.login": "登录", "nav.login": "登录",
"nav.free": "下载", "nav.free": "免费",
"nav.home": "首页", "nav.home": "首页",
"nav.openMenu": "打开菜单", "nav.openMenu": "打开菜单",
"nav.getStartedFree": "免费开始", "nav.getStartedFree": "免费开始",
@@ -542,13 +542,6 @@ export const dict = {
"workspace.monthlyLimit.currentUsage.beforeMonth": "当前", "workspace.monthlyLimit.currentUsage.beforeMonth": "当前",
"workspace.monthlyLimit.currentUsage.beforeAmount": "的使用量为 $", "workspace.monthlyLimit.currentUsage.beforeAmount": "的使用量为 $",
"workspace.redeem.title": "兑换优惠券",
"workspace.redeem.subtitle": "兑换优惠码以领取充值额度或权益。",
"workspace.redeem.placeholder": "输入优惠码",
"workspace.redeem.redeem": "兑换",
"workspace.redeem.redeeming": "兑换中...",
"workspace.redeem.success": "优惠券兑换成功。",
"workspace.reload.title": "自动充值", "workspace.reload.title": "自动充值",
"workspace.reload.disabled.before": "自动充值已", "workspace.reload.disabled.before": "自动充值已",
"workspace.reload.disabled.state": "禁用", "workspace.reload.disabled.state": "禁用",
+1 -8
View File
@@ -11,7 +11,7 @@ export const dict = {
"nav.enterprise": "企業", "nav.enterprise": "企業",
"nav.zen": "Zen", "nav.zen": "Zen",
"nav.login": "登入", "nav.login": "登入",
"nav.free": "下載", "nav.free": "免費",
"nav.home": "首頁", "nav.home": "首頁",
"nav.openMenu": "開啟選單", "nav.openMenu": "開啟選單",
"nav.getStartedFree": "免費開始使用", "nav.getStartedFree": "免費開始使用",
@@ -542,13 +542,6 @@ export const dict = {
"workspace.monthlyLimit.currentUsage.beforeMonth": "目前", "workspace.monthlyLimit.currentUsage.beforeMonth": "目前",
"workspace.monthlyLimit.currentUsage.beforeAmount": "的使用量為 $", "workspace.monthlyLimit.currentUsage.beforeAmount": "的使用量為 $",
"workspace.redeem.title": "兌換優惠券",
"workspace.redeem.subtitle": "兌換優惠碼以領取儲值額度或權益。",
"workspace.redeem.placeholder": "輸入優惠碼",
"workspace.redeem.redeem": "兌換",
"workspace.redeem.redeeming": "兌換中...",
"workspace.redeem.success": "優惠券兌換成功。",
"workspace.reload.title": "自動儲值", "workspace.reload.title": "自動儲值",
"workspace.reload.disabled.before": "自動儲值已", "workspace.reload.disabled.before": "自動儲值已",
"workspace.reload.disabled.state": "停用", "workspace.reload.disabled.state": "停用",
@@ -1,6 +1,5 @@
import type { APIEvent } from "@solidjs/start/server" import type { APIEvent } from "@solidjs/start/server"
import { AWS } from "@opencode-ai/console-core/aws.js" import { AWS } from "@opencode-ai/console-core/aws.js"
import { Resource } from "@opencode-ai/console-resource"
import { i18n } from "~/i18n" import { i18n } from "~/i18n"
import { localeFromRequest } from "~/lib/language" import { localeFromRequest } from "~/lib/language"
import { createLead } from "~/lib/salesforce" import { createLead } from "~/lib/salesforce"
@@ -15,64 +14,6 @@ interface EnterpriseFormData {
message: string message: string
} }
const EMAIL_OCTOPUS_LIST_ID = "1b381e5e-39bd-11f1-ba4a-cdd4791f0c43"
function splitFullName(fullName: string) {
const parts = fullName
.trim()
.split(/\s+/)
.filter((p) => p.length > 0)
if (parts.length === 0) return { firstName: "", lastName: "" }
if (parts.length === 1) return { firstName: parts[0], lastName: "" }
return { firstName: parts[0], lastName: parts.slice(1).join(" ") }
}
function getEmailOctopusApiKey() {
if (process.env.EMAILOCTOPUS_API_KEY) return process.env.EMAILOCTOPUS_API_KEY
try {
return Resource.EMAILOCTOPUS_API_KEY.value
} catch {
return
}
}
function subscribe(email: string, fullName: string) {
const apiKey = getEmailOctopusApiKey()
if (!apiKey) {
console.warn("Skipping EmailOctopus subscribe: missing API key")
return Promise.resolve(false)
}
const name = splitFullName(fullName)
const fields: Record<string, string> = {}
if (name.firstName) fields.FirstName = name.firstName
if (name.lastName) fields.LastName = name.lastName
const payload: { email_address: string; fields?: Record<string, string> } = { email_address: email }
if (Object.keys(fields).length) payload.fields = fields
return fetch(`https://api.emailoctopus.com/lists/${EMAIL_OCTOPUS_LIST_ID}/contacts`, {
method: "PUT",
headers: {
Authorization: `Bearer ${apiKey}`,
"Content-Type": "application/json",
},
body: JSON.stringify(payload),
}).then(
(res) => {
if (!res.ok) {
console.error("EmailOctopus subscribe failed:", res.status, res.statusText)
return false
}
return true
},
(err) => {
console.error("Failed to subscribe enterprise email:", err)
return false
},
)
}
export async function POST(event: APIEvent) { export async function POST(event: APIEvent) {
const dict = i18n(localeFromRequest(event.request)) const dict = i18n(localeFromRequest(event.request))
try { try {
@@ -100,7 +41,7 @@ ${body.role}<br>
${body.company ? `${body.company}<br>` : ""}${body.email}<br> ${body.company ? `${body.company}<br>` : ""}${body.email}<br>
${body.phone ? `${body.phone}<br>` : ""}`.trim() ${body.phone ? `${body.phone}<br>` : ""}`.trim()
const [lead, mail, octopus] = await Promise.all([ const [lead, mail] = await Promise.all([
createLead({ createLead({
name: body.name, name: body.name,
role: body.role, role: body.role,
@@ -108,9 +49,6 @@ ${body.phone ? `${body.phone}<br>` : ""}`.trim()
email: body.email, email: body.email,
phone: body.phone, phone: body.phone,
message: body.message, message: body.message,
}).catch((err) => {
console.error("Failed to create Salesforce lead:", err)
return false
}), }),
AWS.sendEmail({ AWS.sendEmail({
to: "contact@anoma.ly", to: "contact@anoma.ly",
@@ -124,14 +62,9 @@ ${body.phone ? `${body.phone}<br>` : ""}`.trim()
return false return false
}, },
), ),
subscribe(body.email, body.name),
]) ])
if (!lead && !mail && !octopus) { if (!lead && !mail) {
if (import.meta.env.DEV) {
console.warn("Enterprise inquiry accepted in dev mode without integrations", { email: body.email })
return Response.json({ success: true, message: dict["enterprise.form.success.submitted"] }, { status: 200 })
}
console.error("Enterprise inquiry delivery failed", { email: body.email }) console.error("Enterprise inquiry delivery failed", { email: body.email })
return Response.json({ error: dict["enterprise.form.error.internalServer"] }, { status: 500 }) return Response.json({ error: dict["enterprise.form.error.internalServer"] }, { status: 500 })
} }
@@ -9,7 +9,6 @@ import { Actor } from "@opencode-ai/console-core/actor.js"
import { Resource } from "@opencode-ai/console-resource" import { Resource } from "@opencode-ai/console-resource"
import { LiteData } from "@opencode-ai/console-core/lite.js" import { LiteData } from "@opencode-ai/console-core/lite.js"
import { BlackData } from "@opencode-ai/console-core/black.js" import { BlackData } from "@opencode-ai/console-core/black.js"
import { User } from "@opencode-ai/console-core/user.js"
export async function POST(input: APIEvent) { export async function POST(input: APIEvent) {
const body = await Billing.stripe().webhooks.constructEventAsync( const body = await Billing.stripe().webhooks.constructEventAsync(
@@ -110,8 +109,6 @@ export async function POST(input: APIEvent) {
if (type === "lite") { if (type === "lite") {
const workspaceID = body.data.object.metadata?.workspaceID const workspaceID = body.data.object.metadata?.workspaceID
const userID = body.data.object.metadata?.userID const userID = body.data.object.metadata?.userID
const userEmail = body.data.object.metadata?.userEmail
const coupon = body.data.object.metadata?.coupon
const customerID = body.data.object.customer as string const customerID = body.data.object.customer as string
const invoiceID = body.data.object.latest_invoice as string const invoiceID = body.data.object.latest_invoice as string
const subscriptionID = body.data.object.id as string const subscriptionID = body.data.object.id as string
@@ -159,10 +156,6 @@ export async function POST(input: APIEvent) {
id: Identifier.create("lite"), id: Identifier.create("lite"),
userID: userID, userID: userID,
}) })
if (userEmail && coupon === LiteData.firstMonth100Coupon) {
await Billing.redeemCoupon(userEmail, "GOFREEMONTH")
}
}) })
}) })
} }
@@ -3,7 +3,6 @@ import { BillingSection } from "./billing-section"
import { ReloadSection } from "./reload-section" import { ReloadSection } from "./reload-section"
import { PaymentSection } from "./payment-section" import { PaymentSection } from "./payment-section"
import { BlackSection } from "./black-section" import { BlackSection } from "./black-section"
import { RedeemSection } from "./redeem-section"
import { createMemo, Show } from "solid-js" import { createMemo, Show } from "solid-js"
import { createAsync, useParams } from "@solidjs/router" import { createAsync, useParams } from "@solidjs/router"
import { queryBillingInfo, querySessionInfo } from "../../common" import { queryBillingInfo, querySessionInfo } from "../../common"
@@ -22,7 +21,6 @@ export default function () {
<BlackSection /> <BlackSection />
</Show> </Show>
<BillingSection /> <BillingSection />
<RedeemSection />
<Show when={billingInfo()?.customerID}> <Show when={billingInfo()?.customerID}>
<ReloadSection /> <ReloadSection />
<MonthlyLimitSection /> <MonthlyLimitSection />
@@ -1,61 +0,0 @@
.root {
[data-slot="redeem-container"] {
display: flex;
flex-direction: column;
gap: var(--space-3);
min-width: 20rem;
width: fit-content;
@media (max-width: 30rem) {
width: 100%;
}
}
[data-slot="redeem-form"] {
display: flex;
flex-direction: column;
gap: var(--space-2);
[data-slot="input-row"] {
display: flex;
gap: var(--space-2);
align-items: stretch;
@media (max-width: 30rem) {
flex-direction: column;
}
}
input {
flex: 1;
padding: var(--space-2) 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-size: var(--font-size-sm);
font-family: var(--font-mono);
&:focus {
outline: none;
border-color: var(--color-accent);
}
&::placeholder {
color: var(--color-text-disabled);
}
}
[data-slot="form-error"] {
color: var(--color-danger);
font-size: var(--font-size-sm);
line-height: 1.4;
}
[data-slot="form-success"] {
color: var(--color-success, var(--color-accent));
font-size: var(--font-size-sm);
line-height: 1.4;
}
}
}
@@ -1,71 +0,0 @@
import { json, action, useParams, useSubmission } from "@solidjs/router"
import { Show } from "solid-js"
import { withActor } from "~/context/auth.withActor"
import { Billing } from "@opencode-ai/console-core/billing.js"
import { User } from "@opencode-ai/console-core/user.js"
import { Actor } from "@opencode-ai/console-core/actor.js"
import { CouponType } from "@opencode-ai/console-core/schema/billing.sql.js"
import styles from "./redeem-section.module.css"
import { queryBillingInfo } from "../../common"
import { useI18n } from "~/context/i18n"
import { formError, localizeError } from "~/lib/form-error"
const redeem = action(async (form: FormData) => {
"use server"
const workspaceID = form.get("workspaceID") as string | null
if (!workspaceID) return { error: formError.workspaceRequired }
const code = (form.get("code") as string | null)?.trim().toUpperCase()
if (!code) return { error: "Coupon code is required." }
if (!(CouponType as readonly string[]).includes(code)) return { error: "Invalid coupon code." }
return json(
await withActor(async () => {
const actor = Actor.assert("user")
const email = await User.getAuthEmail(actor.properties.userID)
if (!email) return { error: "No email on account." }
return Billing.redeemCoupon(email, code as (typeof CouponType)[number])
.then(() => ({ error: undefined, data: true }))
.catch((e) => ({ error: e.message as string }))
}, workspaceID),
{ revalidate: queryBillingInfo.key },
)
}, "billing.redeemCoupon")
export function RedeemSection() {
const params = useParams()
const i18n = useI18n()
const submission = useSubmission(redeem)
return (
<section class={styles.root}>
<div data-slot="section-title">
<h2>{i18n.t("workspace.redeem.title")}</h2>
<p>{i18n.t("workspace.redeem.subtitle")}</p>
</div>
<div data-slot="redeem-container">
<form action={redeem} method="post" data-slot="redeem-form">
<div data-slot="input-row">
<input
required
data-component="input"
name="code"
type="text"
autocomplete="off"
placeholder={i18n.t("workspace.redeem.placeholder")}
/>
<button type="submit" data-color="primary" disabled={submission.pending}>
{submission.pending ? i18n.t("workspace.redeem.redeeming") : i18n.t("workspace.redeem.redeem")}
</button>
</div>
<Show when={submission.result && (submission.result as any).error}>
{(err: any) => <div data-slot="form-error">{localizeError(i18n.t, err())}</div>}
</Show>
<Show when={submission.result && !(submission.result as any).error && (submission.result as any).data}>
<div data-slot="form-success">{i18n.t("workspace.redeem.success")}</div>
</Show>
<input type="hidden" name="workspaceID" value={params.id} />
</form>
</div>
</section>
)
}
@@ -45,7 +45,6 @@ import { LiteData } from "@opencode-ai/console-core/lite.js"
import { Resource } from "@opencode-ai/console-resource" import { Resource } from "@opencode-ai/console-resource"
import { i18n, type Key } from "~/i18n" import { i18n, type Key } from "~/i18n"
import { localeFromRequest } from "~/lib/language" import { localeFromRequest } from "~/lib/language"
import { createModelTpmLimiter } from "./modelTpmLimiter"
type ZenData = Awaited<ReturnType<typeof ZenData.list>> type ZenData = Awaited<ReturnType<typeof ZenData.list>>
type RetryOptions = { type RetryOptions = {
@@ -122,8 +121,6 @@ export async function handler(
const authInfo = await authenticate(modelInfo, zenApiKey) const authInfo = await authenticate(modelInfo, zenApiKey)
const billingSource = validateBilling(authInfo, modelInfo) const billingSource = validateBilling(authInfo, modelInfo)
logger.metric({ source: billingSource }) logger.metric({ source: billingSource })
const modelTpmLimiter = createModelTpmLimiter(modelInfo.providers)
const modelTpmLimits = await modelTpmLimiter?.check()
const retriableRequest = async (retry: RetryOptions = { excludeProviders: [], retryCount: 0 }) => { const retriableRequest = async (retry: RetryOptions = { excludeProviders: [], retryCount: 0 }) => {
const providerInfo = selectProvider( const providerInfo = selectProvider(
@@ -136,7 +133,6 @@ export async function handler(
trialProviders, trialProviders,
retry, retry,
stickyProvider, stickyProvider,
modelTpmLimits,
) )
validateModelSettings(billingSource, authInfo) validateModelSettings(billingSource, authInfo)
updateProviderKey(authInfo, providerInfo) updateProviderKey(authInfo, providerInfo)
@@ -233,7 +229,6 @@ export async function handler(
const usageInfo = providerInfo.normalizeUsage(json.usage) const usageInfo = providerInfo.normalizeUsage(json.usage)
const costInfo = calculateCost(modelInfo, usageInfo) const costInfo = calculateCost(modelInfo, usageInfo)
await trialLimiter?.track(usageInfo) await trialLimiter?.track(usageInfo)
await modelTpmLimiter?.track(providerInfo.id, providerInfo.model, usageInfo)
await trackUsage(sessionId, billingSource, authInfo, modelInfo, providerInfo, usageInfo, costInfo) await trackUsage(sessionId, billingSource, authInfo, modelInfo, providerInfo, usageInfo, costInfo)
await reload(billingSource, authInfo, costInfo) await reload(billingSource, authInfo, costInfo)
json.cost = calculateOccurredCost(billingSource, costInfo) json.cost = calculateOccurredCost(billingSource, costInfo)
@@ -283,7 +278,6 @@ export async function handler(
const usageInfo = providerInfo.normalizeUsage(usage) const usageInfo = providerInfo.normalizeUsage(usage)
const costInfo = calculateCost(modelInfo, usageInfo) const costInfo = calculateCost(modelInfo, usageInfo)
await trialLimiter?.track(usageInfo) await trialLimiter?.track(usageInfo)
await modelTpmLimiter?.track(providerInfo.id, providerInfo.model, usageInfo)
await trackUsage(sessionId, billingSource, authInfo, modelInfo, providerInfo, usageInfo, costInfo) await trackUsage(sessionId, billingSource, authInfo, modelInfo, providerInfo, usageInfo, costInfo)
await reload(billingSource, authInfo, costInfo) await reload(billingSource, authInfo, costInfo)
const cost = calculateOccurredCost(billingSource, costInfo) const cost = calculateOccurredCost(billingSource, costInfo)
@@ -439,16 +433,12 @@ export async function handler(
trialProviders: string[] | undefined, trialProviders: string[] | undefined,
retry: RetryOptions, retry: RetryOptions,
stickyProvider: string | undefined, stickyProvider: string | undefined,
modelTpmLimits: Record<string, number> | undefined,
) { ) {
const modelProvider = (() => { const modelProvider = (() => {
// Byok is top priority b/c if user set their own API key, we should use it
// instead of using the sticky provider for the same session
if (authInfo?.provider?.credentials) { if (authInfo?.provider?.credentials) {
return modelInfo.providers.find((provider) => provider.id === modelInfo.byokProvider) return modelInfo.providers.find((provider) => provider.id === modelInfo.byokProvider)
} }
// Always use the same provider for the same session
if (stickyProvider) { if (stickyProvider) {
const provider = modelInfo.providers.find((provider) => provider.id === stickyProvider) const provider = modelInfo.providers.find((provider) => provider.id === stickyProvider)
if (provider) return provider if (provider) return provider
@@ -461,20 +451,10 @@ export async function handler(
} }
if (retry.retryCount !== MAX_FAILOVER_RETRIES) { if (retry.retryCount !== MAX_FAILOVER_RETRIES) {
const allProviders = modelInfo.providers const providers = modelInfo.providers
.filter((provider) => !provider.disabled) .filter((provider) => !provider.disabled)
.filter((provider) => provider.weight !== 0)
.filter((provider) => !retry.excludeProviders.includes(provider.id)) .filter((provider) => !retry.excludeProviders.includes(provider.id))
.filter((provider) => { .flatMap((provider) => Array<typeof provider>(provider.weight ?? 1).fill(provider))
if (!provider.tpmLimit) return true
const usage = modelTpmLimits?.[`${provider.id}/${provider.model}`] ?? 0
return usage < provider.tpmLimit * 1_000_000
})
const topPriority = Math.min(...allProviders.map((p) => p.priority))
const providers = allProviders
.filter((p) => p.priority <= topPriority)
.flatMap((provider) => Array<typeof provider>(provider.weight).fill(provider))
// Use the last 4 characters of session ID to select a provider // Use the last 4 characters of session ID to select a provider
const identifier = sessionId.length ? sessionId : ip const identifier = sessionId.length ? sessionId : ip
@@ -762,8 +742,7 @@ export async function handler(
const billing = authInfo.billing const billing = authInfo.billing
const billingUrl = `https://opencode.ai/workspace/${authInfo.workspaceID}/billing` const billingUrl = `https://opencode.ai/workspace/${authInfo.workspaceID}/billing`
const membersUrl = `https://opencode.ai/workspace/${authInfo.workspaceID}/members` const membersUrl = `https://opencode.ai/workspace/${authInfo.workspaceID}/members`
if (!billing.paymentMethodID && billing.balance <= 0) if (!billing.paymentMethodID) throw new CreditsError(t("zen.api.error.noPaymentMethod", { billingUrl }))
throw new CreditsError(t("zen.api.error.noPaymentMethod", { billingUrl }))
if (billing.balance <= 0) throw new CreditsError(t("zen.api.error.insufficientBalance", { billingUrl })) if (billing.balance <= 0) throw new CreditsError(t("zen.api.error.insufficientBalance", { billingUrl }))
const now = new Date() const now = new Date()
@@ -1,51 +0,0 @@
import { and, Database, eq, inArray, sql } from "@opencode-ai/console-core/drizzle/index.js"
import { ModelRateLimitTable } from "@opencode-ai/console-core/schema/ip.sql.js"
import { UsageInfo } from "./provider/provider"
export function createModelTpmLimiter(providers: { id: string; model: string; tpmLimit?: number }[]) {
const keys = providers.filter((p) => p.tpmLimit).map((p) => `${p.id}/${p.model}`)
if (keys.length === 0) return
const yyyyMMddHHmm = new Date(Date.now())
.toISOString()
.replace(/[^0-9]/g, "")
.substring(0, 12)
return {
check: async () => {
const data = await Database.use((tx) =>
tx
.select()
.from(ModelRateLimitTable)
.where(and(inArray(ModelRateLimitTable.key, keys), eq(ModelRateLimitTable.interval, yyyyMMddHHmm))),
)
// convert to map of model to count
return data.reduce(
(acc, curr) => {
acc[curr.key] = curr.count
return acc
},
{} as Record<string, number>,
)
},
track: async (id: string, model: string, usageInfo: UsageInfo) => {
const key = `${id}/${model}`
if (!keys.includes(key)) return
const usage =
usageInfo.inputTokens +
usageInfo.outputTokens +
(usageInfo.reasoningTokens ?? 0) +
(usageInfo.cacheReadTokens ?? 0) +
(usageInfo.cacheWrite5mTokens ?? 0) +
(usageInfo.cacheWrite1hTokens ?? 0)
if (usage <= 0) return
await Database.use((tx) =>
tx
.insert(ModelRateLimitTable)
.values({ key, interval: yyyyMMddHHmm, count: usage })
.onDuplicateKeyUpdate({ set: { count: sql`${ModelRateLimitTable.count} + ${usage}` } }),
)
},
}
}
@@ -1,6 +0,0 @@
CREATE TABLE `model_rate_limit` (
`key` varchar(255) NOT NULL,
`interval` varchar(40) NOT NULL,
`count` int NOT NULL,
CONSTRAINT PRIMARY KEY(`key`,`interval`)
);
File diff suppressed because it is too large Load Diff
@@ -1,6 +0,0 @@
CREATE TABLE `coupon` (
`email` varchar(255),
`type` enum('BUILDATHON','GOFREEMONTH') NOT NULL,
`time_redeemed` timestamp(3),
CONSTRAINT PRIMARY KEY(`email`,`type`)
);
+1 -1
View File
@@ -1,7 +1,7 @@
{ {
"$schema": "https://json.schemastore.org/package.json", "$schema": "https://json.schemastore.org/package.json",
"name": "@opencode-ai/console-core", "name": "@opencode-ai/console-core",
"version": "1.14.18", "version": "1.4.6",
"private": true, "private": true,
"type": "module", "type": "module",
"license": "MIT", "license": "MIT",
@@ -1,24 +0,0 @@
import { Database } from "../src/drizzle/index.js"
import { CouponTable, CouponType } from "../src/schema/billing.sql.js"
const email = process.argv[2]
const type = process.argv[3] as (typeof CouponType)[number]
if (!email || !type) {
console.error(`Usage: bun create-coupon.ts <email> <${CouponType.join("|")}>`)
process.exit(1)
}
if (!(CouponType as readonly string[]).includes(type)) {
console.error(`Error: type must be one of ${CouponType.join(", ")}`)
process.exit(1)
}
await Database.use((tx) =>
tx.insert(CouponTable).values({
email,
type,
}),
)
console.log(`Created ${type} coupon for ${email}`)
+5 -49
View File
@@ -1,14 +1,6 @@
import { Stripe } from "stripe" import { Stripe } from "stripe"
import { and, Database, eq, isNull, sql } from "./drizzle" import { Database, eq, sql } from "./drizzle"
import { import { BillingTable, LiteTable, PaymentTable, SubscriptionTable, UsageTable } from "./schema/billing.sql"
BillingTable,
CouponTable,
CouponType,
LiteTable,
PaymentTable,
SubscriptionTable,
UsageTable,
} from "./schema/billing.sql"
import { Actor } from "./actor" import { Actor } from "./actor"
import { fn } from "./util/fn" import { fn } from "./util/fn"
import { z } from "zod" import { z } from "zod"
@@ -155,37 +147,6 @@ export namespace Billing {
return amountInMicroCents return amountInMicroCents
} }
export const redeemCoupon = async (email: string, type: (typeof CouponType)[number]) => {
const coupon = await Database.use((tx) =>
tx
.select()
.from(CouponTable)
.where(and(eq(CouponTable.email, email), eq(CouponTable.type, type)))
.then((rows) => rows[0]),
)
if (!coupon) throw new Error("Invalid coupon code")
if (coupon.timeRedeemed) throw new Error("Coupon already redeemed")
if (type === "BUILDATHON") await grantCredit(Actor.workspace(), 500)
await Database.use((tx) =>
tx
.update(CouponTable)
.set({ timeRedeemed: sql`now()` })
.where(and(eq(CouponTable.email, email), eq(CouponTable.type, type))),
)
}
export const hasCoupon = async (email: string, type: (typeof CouponType)[number]) => {
return await Database.use((tx) =>
tx
.select()
.from(CouponTable)
.where(and(eq(CouponTable.email, email), eq(CouponTable.type, type), isNull(CouponTable.timeRedeemed)))
.then((rows) => rows.length > 0),
)
}
export const setMonthlyLimit = fn(z.number(), async (input) => { export const setMonthlyLimit = fn(z.number(), async (input) => {
return await Database.use((tx) => return await Database.use((tx) =>
tx tx
@@ -284,19 +245,16 @@ export namespace Billing {
const user = Actor.assert("user") const user = Actor.assert("user")
const { successUrl, cancelUrl, method } = input const { successUrl, cancelUrl, method } = input
const email = (await User.getAuthEmail(user.properties.userID))! const email = await User.getAuthEmail(user.properties.userID)
const billing = await Billing.get() const billing = await Billing.get()
if (billing.subscriptionID) throw new Error("Already subscribed to Black") if (billing.subscriptionID) throw new Error("Already subscribed to Black")
if (billing.liteSubscriptionID) throw new Error("Already subscribed to Lite") if (billing.liteSubscriptionID) throw new Error("Already subscribed to Lite")
const coupon = (await Billing.hasCoupon(email, "GOFREEMONTH"))
? LiteData.firstMonth100Coupon
: LiteData.firstMonth50Coupon
const createSession = () => const createSession = () =>
Billing.stripe().checkout.sessions.create({ Billing.stripe().checkout.sessions.create({
mode: "subscription", mode: "subscription",
discounts: [{ coupon }], discounts: [{ coupon: LiteData.firstMonthCoupon(email!) }],
...(billing.customerID ...(billing.customerID
? { ? {
customer: billing.customerID, customer: billing.customerID,
@@ -306,7 +264,7 @@ export namespace Billing {
}, },
} }
: { : {
customer_email: email, customer_email: email!,
}), }),
...(() => { ...(() => {
if (method === "alipay") { if (method === "alipay") {
@@ -354,8 +312,6 @@ export namespace Billing {
metadata: { metadata: {
workspaceID: Actor.workspace(), workspaceID: Actor.workspace(),
userID: user.properties.userID, userID: user.properties.userID,
userEmail: email,
coupon,
type: "lite", type: "lite",
}, },
}, },
+6 -2
View File
@@ -11,7 +11,11 @@ export namespace LiteData {
export const productID = fn(z.void(), () => Resource.ZEN_LITE_PRICE.product) export const productID = fn(z.void(), () => Resource.ZEN_LITE_PRICE.product)
export const priceID = fn(z.void(), () => Resource.ZEN_LITE_PRICE.price) export const priceID = fn(z.void(), () => Resource.ZEN_LITE_PRICE.price)
export const priceInr = fn(z.void(), () => Resource.ZEN_LITE_PRICE.priceInr) export const priceInr = fn(z.void(), () => Resource.ZEN_LITE_PRICE.priceInr)
export const firstMonth100Coupon = Resource.ZEN_LITE_PRICE.firstMonth100Coupon export const firstMonthCoupon = fn(z.string(), (email) => {
export const firstMonth50Coupon = Resource.ZEN_LITE_PRICE.firstMonth50Coupon const invitees = Resource.ZEN_LITE_COUPON_FIRST_MONTH_100_INVITEES.value.split(",")
return invitees.includes(email)
? Resource.ZEN_LITE_PRICE.firstMonth100Coupon
: Resource.ZEN_LITE_PRICE.firstMonth50Coupon
})
export const planName = fn(z.void(), () => "lite") export const planName = fn(z.void(), () => "lite")
} }
+4 -11
View File
@@ -34,8 +34,6 @@ export namespace ZenData {
z.object({ z.object({
id: z.string(), id: z.string(),
model: z.string(), model: z.string(),
priority: z.number().optional(),
tpmLimit: z.number().optional(),
weight: z.number().optional(), weight: z.number().optional(),
disabled: z.boolean().optional(), disabled: z.boolean().optional(),
storeModel: z.string().optional(), storeModel: z.string().optional(),
@@ -125,16 +123,10 @@ export namespace ZenData {
), ),
models: (() => { models: (() => {
const normalize = (model: z.infer<typeof ModelSchema>) => { const normalize = (model: z.infer<typeof ModelSchema>) => {
const providers = model.providers.map((p) => ({ const composite = model.providers.find((p) => compositeProviders[p.id].length > 1)
...p,
priority: p.priority ?? Infinity,
weight: p.weight ?? 1,
}))
const composite = providers.find((p) => compositeProviders[p.id].length > 1)
if (!composite) if (!composite)
return { return {
trialProvider: model.trialProvider ? [model.trialProvider] : undefined, trialProvider: model.trialProvider ? [model.trialProvider] : undefined,
providers,
} }
const weightMulti = compositeProviders[composite.id].length const weightMulti = compositeProviders[composite.id].length
@@ -145,16 +137,17 @@ export namespace ZenData {
if (model.trialProvider === composite.id) return compositeProviders[composite.id].map((p) => p.id) if (model.trialProvider === composite.id) return compositeProviders[composite.id].map((p) => p.id)
return [model.trialProvider] return [model.trialProvider]
})(), })(),
providers: providers.flatMap((p) => providers: model.providers.flatMap((p) =>
p.id === composite.id p.id === composite.id
? compositeProviders[p.id].map((sub) => ({ ? compositeProviders[p.id].map((sub) => ({
...p, ...p,
id: sub.id, id: sub.id,
weight: p.weight ?? 1,
})) }))
: [ : [
{ {
...p, ...p,
weight: p.weight * weightMulti, weight: (p.weight ?? 1) * weightMulti,
}, },
], ],
), ),
@@ -1,15 +1,4 @@
import { import { bigint, boolean, index, int, json, mysqlEnum, mysqlTable, uniqueIndex, varchar } from "drizzle-orm/mysql-core"
bigint,
boolean,
index,
int,
json,
mysqlEnum,
mysqlTable,
primaryKey,
uniqueIndex,
varchar,
} from "drizzle-orm/mysql-core"
import { timestamps, ulid, utc, workspaceColumns } from "../drizzle/types" import { timestamps, ulid, utc, workspaceColumns } from "../drizzle/types"
import { workspaceIndexes } from "./workspace.sql" import { workspaceIndexes } from "./workspace.sql"
@@ -132,14 +121,3 @@ export const UsageTable = mysqlTable(
}, },
(table) => [...workspaceIndexes(table), index("usage_time_created").on(table.workspaceID, table.timeCreated)], (table) => [...workspaceIndexes(table), index("usage_time_created").on(table.workspaceID, table.timeCreated)],
) )
export const CouponType = ["BUILDATHON", "GOFREEMONTH"] as const
export const CouponTable = mysqlTable(
"coupon",
{
email: varchar("email", { length: 255 }),
type: mysqlEnum("type", CouponType).notNull(),
timeRedeemed: utc("time_redeemed"),
},
(table) => [primaryKey({ columns: [table.email, table.type] })],
)
@@ -30,13 +30,3 @@ export const KeyRateLimitTable = mysqlTable(
}, },
(table) => [primaryKey({ columns: [table.key, table.interval] })], (table) => [primaryKey({ columns: [table.key, table.interval] })],
) )
export const ModelRateLimitTable = mysqlTable(
"model_rate_limit",
{
key: varchar("key", { length: 255 }).notNull(),
interval: varchar("interval", { length: 40 }).notNull(),
count: int("count").notNull(),
},
(table) => [primaryKey({ columns: [table.key, table.interval] })],
)
+4
View File
@@ -142,6 +142,10 @@ declare module "sst" {
"type": "sst.sst.Secret" "type": "sst.sst.Secret"
"value": string "value": string
} }
"ZEN_LITE_COUPON_FIRST_MONTH_100_INVITEES": {
"type": "sst.sst.Secret"
"value": string
}
"ZEN_LITE_PRICE": { "ZEN_LITE_PRICE": {
"firstMonth100Coupon": string "firstMonth100Coupon": string
"firstMonth50Coupon": string "firstMonth50Coupon": string
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "@opencode-ai/console-function", "name": "@opencode-ai/console-function",
"version": "1.14.18", "version": "1.4.6",
"$schema": "https://json.schemastore.org/package.json", "$schema": "https://json.schemastore.org/package.json",
"private": true, "private": true,
"type": "module", "type": "module",
+4
View File
@@ -142,6 +142,10 @@ declare module "sst" {
"type": "sst.sst.Secret" "type": "sst.sst.Secret"
"value": string "value": string
} }
"ZEN_LITE_COUPON_FIRST_MONTH_100_INVITEES": {
"type": "sst.sst.Secret"
"value": string
}
"ZEN_LITE_PRICE": { "ZEN_LITE_PRICE": {
"firstMonth100Coupon": string "firstMonth100Coupon": string
"firstMonth50Coupon": string "firstMonth50Coupon": string
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "@opencode-ai/console-mail", "name": "@opencode-ai/console-mail",
"version": "1.14.18", "version": "1.4.6",
"dependencies": { "dependencies": {
"@jsx-email/all": "2.2.3", "@jsx-email/all": "2.2.3",
"@jsx-email/cli": "1.4.3", "@jsx-email/cli": "1.4.3",
+4
View File
@@ -142,6 +142,10 @@ declare module "sst" {
"type": "sst.sst.Secret" "type": "sst.sst.Secret"
"value": string "value": string
} }
"ZEN_LITE_COUPON_FIRST_MONTH_100_INVITEES": {
"type": "sst.sst.Secret"
"value": string
}
"ZEN_LITE_PRICE": { "ZEN_LITE_PRICE": {
"firstMonth100Coupon": string "firstMonth100Coupon": string
"firstMonth50Coupon": string "firstMonth50Coupon": string
-41
View File
@@ -1,41 +0,0 @@
{
"$schema": "https://json.schemastore.org/package.json",
"version": "1.4.11",
"name": "@opencode-ai/core",
"type": "module",
"license": "MIT",
"private": true,
"scripts": {
"test": "bun test",
"typecheck": "tsgo --noEmit"
},
"bin": {
"opencode": "./bin/opencode"
},
"exports": {
"./*": "./src/*.ts"
},
"imports": {},
"devDependencies": {
"@tsconfig/bun": "catalog:",
"@types/semver": "catalog:",
"@types/bun": "catalog:",
"@types/npm-package-arg": "6.1.4",
"@types/npmcli__arborist": "6.3.3"
},
"dependencies": {
"@effect/platform-node": "catalog:",
"@npmcli/arborist": "catalog:",
"effect": "catalog:",
"glob": "13.0.5",
"mime-types": "3.0.2",
"minimatch": "10.2.5",
"npm-package-arg": "13.0.2",
"semver": "catalog:",
"xdg-basedir": "5.1.0",
"zod": "catalog:"
},
"overrides": {
"drizzle-orm": "catalog:"
}
}
-2
View File
@@ -1,2 +0,0 @@
import { Layer } from "effect"
export const memoMap = Layer.makeMemoMapUnsafe()
-107
View File
@@ -1,107 +0,0 @@
import { Effect, Layer, Logger } from "effect"
import { FetchHttpClient } from "effect/unstable/http"
import { OtlpLogger, OtlpSerialization } from "effect/unstable/observability"
import * as EffectLogger from "./logger"
import { Flag } from "@/flag/flag"
import { InstallationChannel, InstallationVersion } from "@/installation/version"
import { ensureProcessMetadata } from "@/util/opencode-process"
const base = Flag.OTEL_EXPORTER_OTLP_ENDPOINT
export const enabled = !!base
const processID = crypto.randomUUID()
const headers = Flag.OTEL_EXPORTER_OTLP_HEADERS
? Flag.OTEL_EXPORTER_OTLP_HEADERS.split(",").reduce(
(acc, x) => {
const [key, ...value] = x.split("=")
acc[key] = value.join("=")
return acc
},
{} as Record<string, string>,
)
: undefined
export function resource(): { serviceName: string; serviceVersion: string; attributes: Record<string, string> } {
const processMetadata = ensureProcessMetadata("main")
const attributes: Record<string, string> = (() => {
const value = process.env.OTEL_RESOURCE_ATTRIBUTES
if (!value) return {}
try {
return Object.fromEntries(
value.split(",").map((entry) => {
const index = entry.indexOf("=")
if (index < 1) throw new Error("Invalid OTEL_RESOURCE_ATTRIBUTES entry")
return [decodeURIComponent(entry.slice(0, index)), decodeURIComponent(entry.slice(index + 1))]
}),
)
} catch {
return {}
}
})()
return {
serviceName: "opencode",
serviceVersion: InstallationVersion,
attributes: {
...attributes,
"deployment.environment.name": InstallationChannel,
"opencode.client": Flag.OPENCODE_CLIENT,
"opencode.process_role": processMetadata.processRole,
"opencode.run_id": processMetadata.runID,
"service.instance.id": processID,
},
}
}
function logs() {
return Logger.layer(
[
EffectLogger.logger,
OtlpLogger.make({
url: `${base}/v1/logs`,
resource: resource(),
headers,
}),
],
{ mergeWithExisting: false },
).pipe(Layer.provide(OtlpSerialization.layerJson), Layer.provide(FetchHttpClient.layer))
}
const traces = async () => {
const NodeSdk = await import("@effect/opentelemetry/NodeSdk")
const OTLP = await import("@opentelemetry/exporter-trace-otlp-http")
const SdkBase = await import("@opentelemetry/sdk-trace-base")
// @effect/opentelemetry creates a NodeTracerProvider but never calls
// register(), so the global @opentelemetry/api context manager stays
// as the no-op default. Non-Effect code (like the AI SDK) that calls
// tracer.startActiveSpan() relies on context.active() to find the
// parent span — without a real context manager every span starts a
// new trace. Registering AsyncLocalStorageContextManager fixes this.
const { AsyncLocalStorageContextManager } = await import("@opentelemetry/context-async-hooks")
const { context } = await import("@opentelemetry/api")
const mgr = new AsyncLocalStorageContextManager()
mgr.enable()
context.setGlobalContextManager(mgr)
return NodeSdk.layer(() => ({
resource: resource(),
spanProcessor: new SdkBase.BatchSpanProcessor(
new OTLP.OTLPTraceExporter({
url: `${base}/v1/traces`,
headers,
}),
),
}))
}
export const layer = !base
? EffectLogger.layer
: Layer.unwrap(
Effect.gen(function* () {
const trace = yield* Effect.promise(traces)
return Layer.mergeAll(trace, logs())
}),
)
export const Observability = { enabled, layer }
-237
View File
@@ -1,237 +0,0 @@
export * as AppFileSystem from "./filesystem.js"
import { NodeFileSystem } from "@effect/platform-node"
import { dirname, join, relative, resolve as pathResolve } from "path"
import { realpathSync } from "fs"
import * as NFS from "fs/promises"
import { lookup } from "mime-types"
import { Effect, FileSystem, Layer, Schema, Context } from "effect"
import type { PlatformError } from "effect/PlatformError"
import { Glob } from "./util/glob.js"
export class FileSystemError extends Schema.TaggedErrorClass<FileSystemError>()("FileSystemError", {
method: Schema.String,
cause: Schema.optional(Schema.Defect),
}) {}
export type Error = PlatformError | FileSystemError
export interface DirEntry {
readonly name: string
readonly type: "file" | "directory" | "symlink" | "other"
}
export interface Interface extends FileSystem.FileSystem {
readonly isDir: (path: string) => Effect.Effect<boolean>
readonly isFile: (path: string) => Effect.Effect<boolean>
readonly existsSafe: (path: string) => Effect.Effect<boolean>
readonly readJson: (path: string) => Effect.Effect<unknown, Error>
readonly writeJson: (path: string, data: unknown, mode?: number) => Effect.Effect<void, Error>
readonly ensureDir: (path: string) => Effect.Effect<void, Error>
readonly writeWithDirs: (path: string, content: string | Uint8Array, mode?: number) => Effect.Effect<void, Error>
readonly readDirectoryEntries: (path: string) => Effect.Effect<DirEntry[], Error>
readonly findUp: (target: string, start: string, stop?: string) => Effect.Effect<string[], Error>
readonly up: (options: { targets: string[]; start: string; stop?: string }) => Effect.Effect<string[], Error>
readonly globUp: (pattern: string, start: string, stop?: string) => Effect.Effect<string[], Error>
readonly glob: (pattern: string, options?: Glob.Options) => Effect.Effect<string[], Error>
readonly globMatch: (pattern: string, filepath: string) => boolean
}
export class Service extends Context.Service<Service, Interface>()("@opencode/FileSystem") {}
export const layer = Layer.effect(
Service,
Effect.gen(function* () {
const fs = yield* FileSystem.FileSystem
const existsSafe = Effect.fn("FileSystem.existsSafe")(function* (path: string) {
return yield* fs.exists(path).pipe(Effect.orElseSucceed(() => false))
})
const isDir = Effect.fn("FileSystem.isDir")(function* (path: string) {
const info = yield* fs.stat(path).pipe(Effect.catch(() => Effect.void))
return info?.type === "Directory"
})
const isFile = Effect.fn("FileSystem.isFile")(function* (path: string) {
const info = yield* fs.stat(path).pipe(Effect.catch(() => Effect.void))
return info?.type === "File"
})
const readDirectoryEntries = Effect.fn("FileSystem.readDirectoryEntries")(function* (dirPath: string) {
return yield* Effect.tryPromise({
try: async () => {
const entries = await NFS.readdir(dirPath, { withFileTypes: true })
return entries.map(
(e): DirEntry => ({
name: e.name,
type: e.isDirectory() ? "directory" : e.isSymbolicLink() ? "symlink" : e.isFile() ? "file" : "other",
}),
)
},
catch: (cause) => new FileSystemError({ method: "readDirectoryEntries", cause }),
})
})
const readJson = Effect.fn("FileSystem.readJson")(function* (path: string) {
const text = yield* fs.readFileString(path)
return JSON.parse(text)
})
const writeJson = Effect.fn("FileSystem.writeJson")(function* (path: string, data: unknown, mode?: number) {
const content = JSON.stringify(data, null, 2)
yield* fs.writeFileString(path, content)
if (mode) yield* fs.chmod(path, mode)
})
const ensureDir = Effect.fn("FileSystem.ensureDir")(function* (path: string) {
yield* fs.makeDirectory(path, { recursive: true })
})
const writeWithDirs = Effect.fn("FileSystem.writeWithDirs")(function* (
path: string,
content: string | Uint8Array,
mode?: number,
) {
const write = typeof content === "string" ? fs.writeFileString(path, content) : fs.writeFile(path, content)
yield* write.pipe(
Effect.catchIf(
(e) => e.reason._tag === "NotFound",
() =>
Effect.gen(function* () {
yield* fs.makeDirectory(dirname(path), { recursive: true })
yield* write
}),
),
)
if (mode) yield* fs.chmod(path, mode)
})
const glob = Effect.fn("FileSystem.glob")(function* (pattern: string, options?: Glob.Options) {
return yield* Effect.tryPromise({
try: () => Glob.scan(pattern, options),
catch: (cause) => new FileSystemError({ method: "glob", cause }),
})
})
const findUp = Effect.fn("FileSystem.findUp")(function* (target: string, start: string, stop?: string) {
const result: string[] = []
let current = start
while (true) {
const search = join(current, target)
if (yield* fs.exists(search)) result.push(search)
if (stop === current) break
const parent = dirname(current)
if (parent === current) break
current = parent
}
return result
})
const up = Effect.fn("FileSystem.up")(function* (options: { targets: string[]; start: string; stop?: string }) {
const result: string[] = []
let current = options.start
while (true) {
for (const target of options.targets) {
const search = join(current, target)
if (yield* fs.exists(search)) result.push(search)
}
if (options.stop === current) break
const parent = dirname(current)
if (parent === current) break
current = parent
}
return result
})
const globUp = Effect.fn("FileSystem.globUp")(function* (pattern: string, start: string, stop?: string) {
const result: string[] = []
let current = start
while (true) {
const matches = yield* glob(pattern, { cwd: current, absolute: true, include: "file", dot: true }).pipe(
Effect.catch(() => Effect.succeed([] as string[])),
)
result.push(...matches)
if (stop === current) break
const parent = dirname(current)
if (parent === current) break
current = parent
}
return result
})
return Service.of({
...fs,
existsSafe,
isDir,
isFile,
readDirectoryEntries,
readJson,
writeJson,
ensureDir,
writeWithDirs,
findUp,
up,
globUp,
glob,
globMatch: Glob.match,
})
}),
)
export const defaultLayer = layer.pipe(Layer.provide(NodeFileSystem.layer))
// Pure helpers that don't need Effect (path manipulation, sync operations)
export function mimeType(p: string): string {
return lookup(p) || "application/octet-stream"
}
export function normalizePath(p: string): string {
if (process.platform !== "win32") return p
const resolved = pathResolve(windowsPath(p))
try {
return realpathSync.native(resolved)
} catch {
return resolved
}
}
export function normalizePathPattern(p: string): string {
if (process.platform !== "win32") return p
if (p === "*") return p
const match = p.match(/^(.*)[\\/]\*$/)
if (!match) return normalizePath(p)
const dir = /^[A-Za-z]:$/.test(match[1]) ? match[1] + "\\" : match[1]
return join(normalizePath(dir), "*")
}
export function resolve(p: string): string {
const resolved = pathResolve(windowsPath(p))
try {
return normalizePath(realpathSync(resolved))
} catch (e: any) {
if (e?.code === "ENOENT") return normalizePath(resolved)
throw e
}
}
export function windowsPath(p: string): string {
if (process.platform !== "win32") return p
return p
.replace(/^\/([a-zA-Z]):(?:[\\/]|$)/, (_, drive) => `${drive.toUpperCase()}:/`)
.replace(/^\/([a-zA-Z])(?:\/|$)/, (_, drive) => `${drive.toUpperCase()}:/`)
.replace(/^\/cygdrive\/([a-zA-Z])(?:\/|$)/, (_, drive) => `${drive.toUpperCase()}:/`)
.replace(/^\/mnt\/([a-zA-Z])(?:\/|$)/, (_, drive) => `${drive.toUpperCase()}:/`)
}
export function overlaps(a: string, b: string) {
const relA = relative(a, b)
const relB = relative(b, a)
return !relA || !relA.startsWith("..") || !relB || !relB.startsWith("..")
}
export function contains(parent: string, child: string) {
return !relative(parent, child).startsWith("..")
}
-42
View File
@@ -1,42 +0,0 @@
export * as Global from "./global.js"
import path from "path"
import { xdgData, xdgCache, xdgConfig, xdgState } from "xdg-basedir"
import os from "os"
import { Context, Effect, Layer } from "effect"
export class Service extends Context.Service<Service, Interface>()("@opencode/Global") {}
export interface Interface {
readonly home: string
readonly data: string
readonly cache: string
readonly config: string
readonly state: string
readonly bin: string
readonly log: string
}
export const layer = Layer.effect(
Service,
Effect.gen(function* () {
const app = "opencode"
const home = process.env.OPENCODE_TEST_HOME ?? os.homedir()
const data = path.join(xdgData!, app)
const cache = path.join(xdgCache!, app)
const cfg = path.join(xdgConfig!, app)
const state = path.join(xdgState!, app)
const bin = path.join(cache, "bin")
const log = path.join(data, "log")
return Service.of({
home,
data,
cache,
config: cfg,
state,
bin,
log,
})
}),
)
-294
View File
@@ -1,294 +0,0 @@
export * as Npm from "./npm.js"
import path from "path"
import npa from "npm-package-arg"
import semver from "semver"
import { Effect, Schema, Context, Layer, Option, FileSystem } from "effect"
import { NodeFileSystem } from "@effect/platform-node"
import { AppFileSystem } from "./filesystem.js"
import { Global } from "./global.js"
import { EffectFlock } from "./util/effect-flock.js"
import { makeRuntime } from "../effect/runtime"
export class InstallFailedError extends Schema.TaggedErrorClass<InstallFailedError>()("NpmInstallFailedError", {
add: Schema.Array(Schema.String).pipe(Schema.optional),
dir: Schema.String,
cause: Schema.optional(Schema.Defect),
}) {}
export interface EntryPoint {
readonly directory: string
readonly entrypoint: Option.Option<string>
}
export interface Interface {
readonly add: (pkg: string) => Effect.Effect<EntryPoint, InstallFailedError | EffectFlock.LockError>
readonly install: (
dir: string,
input?: {
add: {
name: string
version?: string
}[]
},
) => Effect.Effect<void, EffectFlock.LockError | InstallFailedError>
readonly outdated: (pkg: string, cachedVersion: string) => Effect.Effect<boolean>
readonly which: (pkg: string) => Effect.Effect<Option.Option<string>>
}
export class Service extends Context.Service<Service, Interface>()("@opencode/Npm") {}
const illegal = process.platform === "win32" ? new Set(["<", ">", ":", '"', "|", "?", "*"]) : undefined
export function sanitize(pkg: string) {
if (!illegal) return pkg
return Array.from(pkg, (char) => (illegal.has(char) || char.charCodeAt(0) < 32 ? "_" : char)).join("")
}
const resolveEntryPoint = (name: string, dir: string): EntryPoint => {
let entrypoint: Option.Option<string>
try {
const resolved = typeof Bun !== "undefined" ? import.meta.resolve(name, dir) : import.meta.resolve(dir)
entrypoint = Option.some(resolved)
} catch {
entrypoint = Option.none()
}
return {
directory: dir,
entrypoint,
}
}
interface ArboristNode {
name: string
path: string
}
interface ArboristTree {
edgesOut: Map<string, { to?: ArboristNode }>
}
export const layer = Layer.effect(
Service,
Effect.gen(function* () {
const afs = yield* AppFileSystem.Service
const global = yield* Global.Service
const fs = yield* FileSystem.FileSystem
const flock = yield* EffectFlock.Service
const directory = (pkg: string) => path.join(global.cache, "packages", sanitize(pkg))
const reify = (input: { dir: string; add?: string[] }) =>
Effect.gen(function* () {
yield* flock.acquire(`npm-install:${input.dir}`)
const { Arborist } = yield* Effect.promise(() => import("@npmcli/arborist"))
const arborist = new Arborist({
path: input.dir,
binLinks: true,
progress: false,
savePrefix: "",
ignoreScripts: true,
})
return yield* Effect.tryPromise({
try: () =>
arborist.reify({
add: input?.add || [],
save: true,
saveType: "prod",
}),
catch: (cause) =>
new InstallFailedError({
cause,
add: input?.add,
dir: input.dir,
}),
}) as Effect.Effect<ArboristTree, InstallFailedError>
}).pipe(
Effect.withSpan("Npm.reify", {
attributes: input,
}),
)
const outdated = Effect.fn("Npm.outdated")(function* (pkg: string, cachedVersion: string) {
const response = yield* Effect.tryPromise({
try: () => fetch(`https://registry.npmjs.org/${pkg}`),
catch: () => undefined,
}).pipe(Effect.orElseSucceed(() => undefined))
if (!response || !response.ok) {
return false
}
const data = yield* Effect.tryPromise({
try: () => response.json() as Promise<{ "dist-tags"?: { latest?: string } }>,
catch: () => undefined,
}).pipe(Effect.orElseSucceed(() => undefined))
const latestVersion = data?.["dist-tags"]?.latest
if (!latestVersion) {
return false
}
const range = /[\s^~*xX<>|=]/.test(cachedVersion)
if (range) return !semver.satisfies(latestVersion, cachedVersion)
return semver.lt(cachedVersion, latestVersion)
})
const add = Effect.fn("Npm.add")(function* (pkg: string) {
const dir = directory(pkg)
const name = (() => {
try {
return npa(pkg).name ?? pkg
} catch {
return pkg
}
})()
if (yield* afs.existsSafe(dir)) {
return resolveEntryPoint(name, path.join(dir, "node_modules", name))
}
const tree = yield* reify({ dir, add: [pkg] })
const first = tree.edgesOut.values().next().value?.to
if (!first) return yield* new InstallFailedError({ add: [pkg], dir })
return resolveEntryPoint(first.name, first.path)
}, Effect.scoped)
const install: Interface["install"] = Effect.fn("Npm.install")(function* (dir, input) {
const canWrite = yield* afs.access(dir, { writable: true }).pipe(
Effect.as(true),
Effect.orElseSucceed(() => false),
)
if (!canWrite) return
const add = input?.add.map((pkg) => [pkg.name, pkg.version].filter(Boolean).join("@")) ?? []
if (
yield* Effect.gen(function* () {
const nodeModulesExists = yield* afs.existsSafe(path.join(dir, "node_modules"))
if (!nodeModulesExists) {
yield* reify({ add, dir })
return true
}
return false
}).pipe(Effect.withSpan("Npm.checkNodeModules"))
)
return
yield* Effect.gen(function* () {
const pkg = yield* afs.readJson(path.join(dir, "package.json")).pipe(Effect.orElseSucceed(() => ({})))
const lock = yield* afs.readJson(path.join(dir, "package-lock.json")).pipe(Effect.orElseSucceed(() => ({})))
const pkgAny = pkg as any
const lockAny = lock as any
const declared = new Set([
...Object.keys(pkgAny?.dependencies || {}),
...Object.keys(pkgAny?.devDependencies || {}),
...Object.keys(pkgAny?.peerDependencies || {}),
...Object.keys(pkgAny?.optionalDependencies || {}),
...(input?.add || []).map((pkg) => pkg.name),
])
const root = lockAny?.packages?.[""] || {}
const locked = new Set([
...Object.keys(root?.dependencies || {}),
...Object.keys(root?.devDependencies || {}),
...Object.keys(root?.peerDependencies || {}),
...Object.keys(root?.optionalDependencies || {}),
])
for (const name of declared) {
if (!locked.has(name)) {
yield* reify({ dir, add })
return
}
}
}).pipe(Effect.withSpan("Npm.checkDirty"))
return
}, Effect.scoped)
const which = Effect.fn("Npm.which")(function* (pkg: string) {
const dir = directory(pkg)
const binDir = path.join(dir, "node_modules", ".bin")
const pick = Effect.fnUntraced(function* () {
const files = yield* fs.readDirectory(binDir).pipe(Effect.catch(() => Effect.succeed([] as string[])))
if (files.length === 0) return Option.none<string>()
if (files.length === 1) return Option.some(files[0])
const pkgJson = yield* afs.readJson(path.join(dir, "node_modules", pkg, "package.json")).pipe(Effect.option)
if (Option.isSome(pkgJson)) {
const parsed = pkgJson.value as { bin?: string | Record<string, string> }
if (parsed?.bin) {
const unscoped = pkg.startsWith("@") ? pkg.split("/")[1] : pkg
const bin = parsed.bin
if (typeof bin === "string") return Option.some(unscoped)
const keys = Object.keys(bin)
if (keys.length === 1) return Option.some(keys[0])
return bin[unscoped] ? Option.some(unscoped) : Option.some(keys[0])
}
}
return Option.some(files[0])
})
return yield* Effect.gen(function* () {
const bin = yield* pick()
if (Option.isSome(bin)) {
return Option.some(path.join(binDir, bin.value))
}
yield* fs.remove(path.join(dir, "package-lock.json")).pipe(Effect.orElseSucceed(() => {}))
yield* add(pkg)
const resolved = yield* pick()
if (Option.isNone(resolved)) return Option.none<string>()
return Option.some(path.join(binDir, resolved.value))
}).pipe(
Effect.scoped,
Effect.orElseSucceed(() => Option.none<string>()),
)
})
return Service.of({
add,
install,
outdated,
which,
})
}),
)
export const defaultLayer = layer.pipe(
Layer.provide(EffectFlock.layer),
Layer.provide(AppFileSystem.layer),
Layer.provide(Global.layer),
Layer.provide(NodeFileSystem.layer),
)
const { runPromise } = makeRuntime(Service, defaultLayer)
export async function install(...args: Parameters<Interface["install"]>) {
return runPromise((svc) => svc.install(...args))
}
export async function add(...args: Parameters<Interface["add"]>) {
const entry = await runPromise((svc) => svc.add(...args))
return {
directory: entry.directory,
entrypoint: Option.getOrUndefined(entry.entrypoint),
}
}
export async function outdated(...args: Parameters<Interface["outdated"]>) {
return runPromise((svc) => svc.outdated(...args))
}
export async function which(...args: Parameters<Interface["which"]>) {
const resolved = await runPromise((svc) => svc.which(...args))
return Option.getOrUndefined(resolved)
}
-268
View File
@@ -1,268 +0,0 @@
import path from "path"
import npa from "npm-package-arg"
import semver from "semver"
import { Effect, Schema, Context, Layer, Option, FileSystem } from "effect"
import { NodeFileSystem } from "@effect/platform-node"
import { AppFileSystem } from "../filesystem"
import { Global } from "../global"
import { EffectFlock } from "../util/effect-flock"
export class InstallFailedError extends Schema.TaggedErrorClass<InstallFailedError>()("NpmInstallFailedError", {
add: Schema.Array(Schema.String).pipe(Schema.optional),
dir: Schema.String,
cause: Schema.optional(Schema.Defect),
}) {}
export interface EntryPoint {
readonly directory: string
readonly entrypoint: Option.Option<string>
}
export interface Interface {
readonly add: (pkg: string) => Effect.Effect<EntryPoint, InstallFailedError | EffectFlock.LockError>
readonly install: (
dir: string,
input?: {
add: {
name: string
version?: string
}[]
},
) => Effect.Effect<void, EffectFlock.LockError | InstallFailedError>
readonly outdated: (pkg: string, cachedVersion: string) => Effect.Effect<boolean>
readonly which: (pkg: string) => Effect.Effect<Option.Option<string>>
}
export class Service extends Context.Service<Service, Interface>()("@opencode/Npm") {}
const illegal = process.platform === "win32" ? new Set(["<", ">", ":", '"', "|", "?", "*"]) : undefined
export function sanitize(pkg: string) {
if (!illegal) return pkg
return Array.from(pkg, (char) => (illegal.has(char) || char.charCodeAt(0) < 32 ? "_" : char)).join("")
}
const resolveEntryPoint = (name: string, dir: string): EntryPoint => {
let entrypoint: Option.Option<string>
try {
const resolved = typeof Bun !== "undefined" ? import.meta.resolve(name, dir) : import.meta.resolve(dir)
entrypoint = Option.some(resolved)
} catch {
entrypoint = Option.none()
}
return {
directory: dir,
entrypoint,
}
}
interface ArboristNode {
name: string
path: string
}
interface ArboristTree {
edgesOut: Map<string, { to?: ArboristNode }>
}
export const layer = Layer.effect(
Service,
Effect.gen(function* () {
const afs = yield* AppFileSystem.Service
const global = yield* Global.Service
const fs = yield* FileSystem.FileSystem
const flock = yield* EffectFlock.Service
const directory = (pkg: string) => path.join(global.cache, "packages", sanitize(pkg))
const reify = (input: { dir: string; add?: string[] }) =>
Effect.gen(function* () {
yield* flock.acquire(`npm-install:${input.dir}`)
const { Arborist } = yield* Effect.promise(() => import("@npmcli/arborist"))
const arborist = new Arborist({
path: input.dir,
binLinks: true,
progress: false,
savePrefix: "",
ignoreScripts: true,
})
return yield* Effect.tryPromise({
try: () =>
arborist.reify({
add: input?.add || [],
save: true,
saveType: "prod",
}),
catch: (cause) =>
new InstallFailedError({
cause,
add: input?.add,
dir: input.dir,
}),
}) as Effect.Effect<ArboristTree, InstallFailedError>
}).pipe(
Effect.withSpan("Npm.reify", {
attributes: input,
}),
)
const outdated = Effect.fn("Npm.outdated")(function* (pkg: string, cachedVersion: string) {
const response = yield* Effect.tryPromise({
try: () => fetch(`https://registry.npmjs.org/${pkg}`),
catch: () => undefined,
}).pipe(Effect.orElseSucceed(() => undefined))
if (!response || !response.ok) {
return false
}
const data = yield* Effect.tryPromise({
try: () => response.json() as Promise<{ "dist-tags"?: { latest?: string } }>,
catch: () => undefined,
}).pipe(Effect.orElseSucceed(() => undefined))
const latestVersion = data?.["dist-tags"]?.latest
if (!latestVersion) {
return false
}
const range = /[\s^~*xX<>|=]/.test(cachedVersion)
if (range) return !semver.satisfies(latestVersion, cachedVersion)
return semver.lt(cachedVersion, latestVersion)
})
const add = Effect.fn("Npm.add")(function* (pkg: string) {
const dir = directory(pkg)
const name = (() => {
try {
return npa(pkg).name ?? pkg
} catch {
return pkg
}
})()
if (yield* afs.existsSafe(dir)) {
return resolveEntryPoint(name, path.join(dir, "node_modules", name))
}
const tree = yield* reify({ dir, add: [pkg] })
const first = tree.edgesOut.values().next().value?.to
if (!first) return yield* new InstallFailedError({ add: [pkg], dir })
return resolveEntryPoint(first.name, first.path)
}, Effect.scoped)
const install: Interface["install"] = Effect.fn("Npm.install")(function* (dir, input) {
const canWrite = yield* afs.access(dir, { writable: true }).pipe(
Effect.as(true),
Effect.orElseSucceed(() => false),
)
if (!canWrite) return
const add = input?.add.map((pkg) => [pkg.name, pkg.version].filter(Boolean).join("@")) ?? []
if (
yield* Effect.gen(function* () {
const nodeModulesExists = yield* afs.existsSafe(path.join(dir, "node_modules"))
if (!nodeModulesExists) {
yield* reify({ add, dir })
return true
}
return false
}).pipe(Effect.withSpan("Npm.checkNodeModules"))
)
return
yield* Effect.gen(function* () {
const pkg = yield* afs.readJson(path.join(dir, "package.json")).pipe(Effect.orElseSucceed(() => ({})))
const lock = yield* afs.readJson(path.join(dir, "package-lock.json")).pipe(Effect.orElseSucceed(() => ({})))
const pkgAny = pkg as any
const lockAny = lock as any
const declared = new Set([
...Object.keys(pkgAny?.dependencies || {}),
...Object.keys(pkgAny?.devDependencies || {}),
...Object.keys(pkgAny?.peerDependencies || {}),
...Object.keys(pkgAny?.optionalDependencies || {}),
...(input?.add || []).map((pkg) => pkg.name),
])
const root = lockAny?.packages?.[""] || {}
const locked = new Set([
...Object.keys(root?.dependencies || {}),
...Object.keys(root?.devDependencies || {}),
...Object.keys(root?.peerDependencies || {}),
...Object.keys(root?.optionalDependencies || {}),
])
for (const name of declared) {
if (!locked.has(name)) {
yield* reify({ dir, add })
return
}
}
}).pipe(Effect.withSpan("Npm.checkDirty"))
return
}, Effect.scoped)
const which = Effect.fn("Npm.which")(function* (pkg: string) {
const dir = directory(pkg)
const binDir = path.join(dir, "node_modules", ".bin")
const pick = Effect.fnUntraced(function* () {
const files = yield* fs.readDirectory(binDir).pipe(Effect.catch(() => Effect.succeed([] as string[])))
if (files.length === 0) return Option.none<string>()
if (files.length === 1) return Option.some(files[0])
const pkgJson = yield* afs.readJson(path.join(dir, "node_modules", pkg, "package.json")).pipe(Effect.option)
if (Option.isSome(pkgJson)) {
const parsed = pkgJson.value as { bin?: string | Record<string, string> }
if (parsed?.bin) {
const unscoped = pkg.startsWith("@") ? pkg.split("/")[1] : pkg
const bin = parsed.bin
if (typeof bin === "string") return Option.some(unscoped)
const keys = Object.keys(bin)
if (keys.length === 1) return Option.some(keys[0])
return bin[unscoped] ? Option.some(unscoped) : Option.some(keys[0])
}
}
return Option.some(files[0])
})
return yield* Effect.gen(function* () {
const bin = yield* pick()
if (Option.isSome(bin)) {
return Option.some(path.join(binDir, bin.value))
}
yield* fs.remove(path.join(dir, "package-lock.json")).pipe(Effect.orElseSucceed(() => {}))
yield* add(pkg)
const resolved = yield* pick()
if (Option.isNone(resolved)) return Option.none<string>()
return Option.some(path.join(binDir, resolved.value))
}).pipe(
Effect.scoped,
Effect.orElseSucceed(() => Option.none<string>()),
)
})
return Service.of({
add,
install,
outdated,
which,
})
}),
)
export const defaultLayer = layer.pipe(
Layer.provide(EffectFlock.layer),
Layer.provide(AppFileSystem.layer),
Layer.provide(Global.layer),
Layer.provide(NodeFileSystem.layer),
)
export * as Npm from "."
-44
View File
@@ -1,44 +0,0 @@
declare module "@npmcli/arborist" {
export interface ArboristOptions {
path: string
binLinks?: boolean
progress?: boolean
savePrefix?: string
ignoreScripts?: boolean
}
export interface ArboristNode {
name: string
path: string
}
export interface ArboristEdge {
to?: ArboristNode
}
export interface ArboristTree {
edgesOut: Map<string, ArboristEdge>
}
export interface ReifyOptions {
add?: string[]
save?: boolean
saveType?: "prod" | "dev" | "optional" | "peer"
}
export class Arborist {
constructor(options: ArboristOptions)
loadVirtual(): Promise<ArboristTree | undefined>
reify(options?: ReifyOptions): Promise<ArboristTree>
}
}
declare var Bun:
| {
file(path: string): {
text(): Promise<string>
json(): Promise<unknown>
}
write(path: string, content: string | Uint8Array): Promise<void>
}
| undefined
-10
View File
@@ -1,10 +0,0 @@
export function findLast<T>(
items: readonly T[],
predicate: (item: T, index: number, items: readonly T[]) => boolean,
): T | undefined {
for (let i = items.length - 1; i >= 0; i -= 1) {
const item = items[i]
if (predicate(item, i, items)) return item
}
return undefined
}
-41
View File
@@ -1,41 +0,0 @@
export namespace Binary {
export function search<T>(array: T[], id: string, compare: (item: T) => string): { found: boolean; index: number } {
let left = 0
let right = array.length - 1
while (left <= right) {
const mid = Math.floor((left + right) / 2)
const midId = compare(array[mid])
if (midId === id) {
return { found: true, index: mid }
} else if (midId < id) {
left = mid + 1
} else {
right = mid - 1
}
}
return { found: false, index: left }
}
export function insert<T>(array: T[], item: T, compare: (item: T) => string): T[] {
const id = compare(item)
let left = 0
let right = array.length
while (left < right) {
const mid = Math.floor((left + right) / 2)
const midId = compare(array[mid])
if (midId < id) {
left = mid + 1
} else {
right = mid
}
}
array.splice(left, 0, item)
return array
}
}
-283
View File
@@ -1,283 +0,0 @@
import path from "path"
import os from "os"
import { randomUUID } from "crypto"
import { Context, Effect, Function, Layer, Option, Schedule, Schema } from "effect"
import type { FileSystem, Scope } from "effect"
import type { PlatformError } from "effect/PlatformError"
import { AppFileSystem } from "../filesystem"
import { Global } from "../global"
import { Hash } from "./hash"
export namespace EffectFlock {
// ---------------------------------------------------------------------------
// Errors
// ---------------------------------------------------------------------------
export class LockTimeoutError extends Schema.TaggedErrorClass<LockTimeoutError>()("LockTimeoutError", {
key: Schema.String,
}) {}
export class LockCompromisedError extends Schema.TaggedErrorClass<LockCompromisedError>()("LockCompromisedError", {
detail: Schema.String,
}) {}
class ReleaseError extends Schema.TaggedErrorClass<ReleaseError>()("ReleaseError", {
detail: Schema.String,
cause: Schema.optional(Schema.Defect),
}) {
override get message() {
return this.detail
}
}
/** Internal: signals "lock is held, retry later". Never leaks to callers. */
class NotAcquired extends Schema.TaggedErrorClass<NotAcquired>()("NotAcquired", {}) {}
export type LockError = LockTimeoutError | LockCompromisedError
// ---------------------------------------------------------------------------
// Timing (baked in — no caller ever overrides these)
// ---------------------------------------------------------------------------
const STALE_MS = 60_000
const TIMEOUT_MS = 5 * 60_000
const BASE_DELAY_MS = 100
const MAX_DELAY_MS = 2_000
const HEARTBEAT_MS = Math.max(100, Math.floor(STALE_MS / 3))
const retrySchedule = Schedule.exponential(BASE_DELAY_MS, 1.7).pipe(
Schedule.either(Schedule.spaced(MAX_DELAY_MS)),
Schedule.jittered,
Schedule.while((meta) => meta.elapsed < TIMEOUT_MS),
)
// ---------------------------------------------------------------------------
// Lock metadata schema
// ---------------------------------------------------------------------------
const LockMetaJson = Schema.fromJsonString(
Schema.Struct({
token: Schema.String,
pid: Schema.Number,
hostname: Schema.String,
createdAt: Schema.String,
}),
)
const decodeMeta = Schema.decodeUnknownSync(LockMetaJson)
const encodeMeta = Schema.encodeSync(LockMetaJson)
// ---------------------------------------------------------------------------
// Service
// ---------------------------------------------------------------------------
export interface Interface {
readonly acquire: (key: string, dir?: string) => Effect.Effect<void, LockError, Scope.Scope>
readonly withLock: {
(key: string, dir?: string): <A, E, R>(body: Effect.Effect<A, E, R>) => Effect.Effect<A, E | LockError, R>
<A, E, R>(body: Effect.Effect<A, E, R>, key: string, dir?: string): Effect.Effect<A, E | LockError, R>
}
}
export class Service extends Context.Service<Service, Interface>()("EffectFlock") {}
// ---------------------------------------------------------------------------
// Layer
// ---------------------------------------------------------------------------
function wall() {
return performance.timeOrigin + performance.now()
}
const mtimeMs = (info: FileSystem.File.Info) => Option.getOrElse(info.mtime, () => new Date(0)).getTime()
const isPathGone = (e: PlatformError) => e.reason._tag === "NotFound" || e.reason._tag === "Unknown"
export const layer: Layer.Layer<Service, never, Global.Service | AppFileSystem.Service> = Layer.effect(
Service,
Effect.gen(function* () {
const global = yield* Global.Service
const fs = yield* AppFileSystem.Service
const lockRoot = path.join(global.state, "locks")
const hostname = os.hostname()
const ensuredDirs = new Set<string>()
// -- helpers (close over fs) --
const safeStat = (file: string) =>
fs.stat(file).pipe(
Effect.catchIf(isPathGone, () => Effect.void),
Effect.orDie,
)
const forceRemove = (target: string) => fs.remove(target, { recursive: true }).pipe(Effect.ignore)
/** Atomic mkdir — returns true if created, false if already exists, dies on other errors. */
const atomicMkdir = (dir: string) =>
fs.makeDirectory(dir, { mode: 0o700 }).pipe(
Effect.as(true),
Effect.catchIf(
(e) => e.reason._tag === "AlreadyExists",
() => Effect.succeed(false),
),
Effect.orDie,
)
/** Write with exclusive create — compromised error if file already exists. */
const exclusiveWrite = (filePath: string, content: string, lockDir: string, detail: string) =>
fs.writeFileString(filePath, content, { flag: "wx" }).pipe(
Effect.catch(() =>
Effect.gen(function* () {
yield* forceRemove(lockDir)
return yield* new LockCompromisedError({ detail })
}),
),
)
const cleanStaleBreaker = Effect.fnUntraced(function* (breakerPath: string) {
const bs = yield* safeStat(breakerPath)
if (bs && wall() - mtimeMs(bs) > STALE_MS) yield* forceRemove(breakerPath)
return false
})
const ensureDir = Effect.fnUntraced(function* (dir: string) {
if (ensuredDirs.has(dir)) return
yield* fs.makeDirectory(dir, { recursive: true }).pipe(Effect.orDie)
ensuredDirs.add(dir)
})
const isStale = Effect.fnUntraced(function* (lockDir: string, heartbeatPath: string, metaPath: string) {
const now = wall()
const hb = yield* safeStat(heartbeatPath)
if (hb) return now - mtimeMs(hb) > STALE_MS
const meta = yield* safeStat(metaPath)
if (meta) return now - mtimeMs(meta) > STALE_MS
const dir = yield* safeStat(lockDir)
if (!dir) return false
return now - mtimeMs(dir) > STALE_MS
})
// -- single lock attempt --
type Handle = { token: string; metaPath: string; heartbeatPath: string; lockDir: string }
const tryAcquireLockDir = (lockDir: string, key: string) =>
Effect.gen(function* () {
const token = randomUUID()
const metaPath = path.join(lockDir, "meta.json")
const heartbeatPath = path.join(lockDir, "heartbeat")
// Atomic mkdir — the POSIX lock primitive
const created = yield* atomicMkdir(lockDir)
if (!created) {
if (!(yield* isStale(lockDir, heartbeatPath, metaPath))) return yield* new NotAcquired()
// Stale — race for breaker ownership
const breakerPath = lockDir + ".breaker"
const claimed = yield* fs.makeDirectory(breakerPath, { mode: 0o700 }).pipe(
Effect.as(true),
Effect.catchIf(
(e) => e.reason._tag === "AlreadyExists",
() => cleanStaleBreaker(breakerPath),
),
Effect.catchIf(isPathGone, () => Effect.succeed(false)),
Effect.orDie,
)
if (!claimed) return yield* new NotAcquired()
// We own the breaker — double-check staleness, nuke, recreate
const recreated = yield* Effect.gen(function* () {
if (!(yield* isStale(lockDir, heartbeatPath, metaPath))) return false
yield* forceRemove(lockDir)
return yield* atomicMkdir(lockDir)
}).pipe(Effect.ensuring(forceRemove(breakerPath)))
if (!recreated) return yield* new NotAcquired()
}
// We own the lock dir — write heartbeat + meta with exclusive create
yield* exclusiveWrite(heartbeatPath, "", lockDir, "heartbeat already existed")
const metaJson = encodeMeta({ token, pid: process.pid, hostname, createdAt: new Date().toISOString() })
yield* exclusiveWrite(metaPath, metaJson, lockDir, "meta.json already existed")
return { token, metaPath, heartbeatPath, lockDir } satisfies Handle
}).pipe(
Effect.withSpan("EffectFlock.tryAcquire", {
attributes: { key },
}),
)
// -- retry wrapper (preserves Handle type) --
const acquireHandle = (lockfile: string, key: string): Effect.Effect<Handle, LockError> =>
tryAcquireLockDir(lockfile, key).pipe(
Effect.retry({
while: (err) => err._tag === "NotAcquired",
schedule: retrySchedule,
}),
Effect.catchTag("NotAcquired", () => Effect.fail(new LockTimeoutError({ key }))),
)
// -- release --
const release = (handle: Handle) =>
Effect.gen(function* () {
const raw = yield* fs.readFileString(handle.metaPath).pipe(
Effect.catch((err) => {
if (isPathGone(err)) return Effect.die(new ReleaseError({ detail: "metadata missing" }))
return Effect.die(err)
}),
)
const parsed = yield* Effect.try({
try: () => decodeMeta(raw),
catch: (cause) => new ReleaseError({ detail: "metadata invalid", cause }),
}).pipe(Effect.orDie)
if (parsed.token !== handle.token) return yield* Effect.die(new ReleaseError({ detail: "token mismatch" }))
yield* forceRemove(handle.lockDir)
})
// -- build service --
const acquire = Effect.fn("EffectFlock.acquire")(function* (key: string, dir?: string) {
const lockDir = dir ?? lockRoot
yield* ensureDir(lockDir)
const lockfile = path.join(lockDir, Hash.fast(key) + ".lock")
// acquireRelease: acquire is uninterruptible, release is guaranteed
const handle = yield* Effect.acquireRelease(acquireHandle(lockfile, key), (handle) => release(handle))
// Heartbeat fiber — scoped, so it's interrupted before release runs
yield* fs
.utimes(handle.heartbeatPath, new Date(), new Date())
.pipe(Effect.ignore, Effect.repeat(Schedule.spaced(HEARTBEAT_MS)), Effect.forkScoped)
})
const withLock: Interface["withLock"] = Function.dual(
(args) => Effect.isEffect(args[0]),
<A, E, R>(body: Effect.Effect<A, E, R>, key: string, dir?: string): Effect.Effect<A, E | LockError, R> =>
Effect.scoped(
Effect.gen(function* () {
yield* acquire(key, dir)
return yield* body
}),
),
)
return Service.of({ acquire, withLock })
}),
)
export const defaultLayer = layer.pipe(Layer.provide(AppFileSystem.defaultLayer), Layer.provide(Global.layer))
}
-51
View File
@@ -1,51 +0,0 @@
export function base64Encode(value: string) {
const bytes = new TextEncoder().encode(value)
const binary = Array.from(bytes, (b) => String.fromCharCode(b)).join("")
return btoa(binary).replace(/\+/g, "-").replace(/\//g, "_").replace(/=/g, "")
}
export function base64Decode(value: string) {
const binary = atob(value.replace(/-/g, "+").replace(/_/g, "/"))
const bytes = Uint8Array.from(binary, (c) => c.charCodeAt(0))
return new TextDecoder().decode(bytes)
}
export async function hash(content: string, algorithm = "SHA-256"): Promise<string> {
const encoder = new TextEncoder()
const data = encoder.encode(content)
const hashBuffer = await crypto.subtle.digest(algorithm, data)
const hashArray = Array.from(new Uint8Array(hashBuffer))
const hashHex = hashArray.map((b) => b.toString(16).padStart(2, "0")).join("")
return hashHex
}
export function checksum(content: string): string | undefined {
if (!content) return undefined
let hash = 0x811c9dc5
for (let i = 0; i < content.length; i++) {
hash ^= content.charCodeAt(i)
hash = Math.imul(hash, 0x01000193)
}
return (hash >>> 0).toString(36)
}
export function sampledChecksum(content: string, limit = 500_000): string | undefined {
if (!content) return undefined
if (content.length <= limit) return checksum(content)
const size = 4096
const points = [
0,
Math.floor(content.length * 0.25),
Math.floor(content.length * 0.5),
Math.floor(content.length * 0.75),
content.length - size,
]
const hashes = points
.map((point) => {
const start = Math.max(0, Math.min(content.length - size, point - Math.floor(size / 2)))
return checksum(content.slice(start, start + size)) ?? ""
})
.join(":")
return `${content.length}:${hashes}`
}
-60
View File
@@ -1,60 +0,0 @@
import z from "zod"
export abstract class NamedError extends Error {
abstract schema(): z.core.$ZodType
abstract toObject(): { name: string; data: any }
static hasName(error: unknown, name: string): boolean {
return (
typeof error === "object" && error !== null && "name" in error && (error as Record<string, unknown>).name === name
)
}
static create<Name extends string, Data extends z.core.$ZodType>(name: Name, data: Data) {
const schema = z
.object({
name: z.literal(name),
data,
})
.meta({
ref: name,
})
const result = class extends NamedError {
public static readonly Schema = schema
public override readonly name = name as Name
constructor(
public readonly data: z.input<Data>,
options?: ErrorOptions,
) {
super(name, options)
this.name = name
}
static isInstance(input: any): input is InstanceType<typeof result> {
return typeof input === "object" && "name" in input && input.name === name
}
schema() {
return schema
}
toObject() {
return {
name: name,
data: this.data,
}
}
}
Object.defineProperty(result, "name", { value: name })
return result
}
public static readonly Unknown = NamedError.create(
"UnknownError",
z.object({
message: z.string(),
}),
)
}
-358
View File
@@ -1,358 +0,0 @@
import path from "path"
import os from "os"
import { randomBytes, randomUUID } from "crypto"
import { mkdir, readFile, rm, stat, utimes, writeFile } from "fs/promises"
import { Hash } from "./hash"
import { Effect } from "effect"
export type FlockGlobal = {
state: string
}
export namespace Flock {
let global: FlockGlobal | undefined
export function setGlobal(g: FlockGlobal) {
global = g
}
const root = () => {
if (!global) throw new Error("Flock global not set")
return path.join(global.state, "locks")
}
// Defaults for callers that do not provide timing options.
const defaultOpts = {
staleMs: 60_000,
timeoutMs: 5 * 60_000,
baseDelayMs: 100,
maxDelayMs: 2_000,
}
export interface WaitEvent {
key: string
attempt: number
delay: number
waited: number
}
export type Wait = (input: WaitEvent) => void | Promise<void>
export interface Options {
dir?: string
signal?: AbortSignal
staleMs?: number
timeoutMs?: number
baseDelayMs?: number
maxDelayMs?: number
onWait?: Wait
}
type Opts = {
staleMs: number
timeoutMs: number
baseDelayMs: number
maxDelayMs: number
}
type Owned = {
acquired: true
startHeartbeat: (intervalMs?: number) => void
release: () => Promise<void>
}
export interface Lease {
release: () => Promise<void>
[Symbol.asyncDispose]: () => Promise<void>
}
function code(err: unknown) {
if (typeof err !== "object" || err === null || !("code" in err)) return
const value = err.code
if (typeof value !== "string") return
return value
}
function sleep(ms: number, signal?: AbortSignal) {
return new Promise<void>((resolve, reject) => {
if (signal?.aborted) {
reject(signal.reason ?? new Error("Aborted"))
return
}
let timer: NodeJS.Timeout | undefined
const done = () => {
signal?.removeEventListener("abort", abort)
resolve()
}
const abort = () => {
if (timer) {
clearTimeout(timer)
}
signal?.removeEventListener("abort", abort)
reject(signal?.reason ?? new Error("Aborted"))
}
signal?.addEventListener("abort", abort, { once: true })
timer = setTimeout(done, ms)
})
}
function jitter(ms: number) {
const j = Math.floor(ms * 0.3)
const d = Math.floor(Math.random() * (2 * j + 1)) - j
return Math.max(0, ms + d)
}
function mono() {
return performance.now()
}
function wall() {
return performance.timeOrigin + mono()
}
async function stats(file: string) {
try {
return await stat(file)
} catch (err) {
const errCode = code(err)
if (errCode === "ENOENT" || errCode === "ENOTDIR") return
throw err
}
}
async function stale(lockDir: string, heartbeatPath: string, metaPath: string, staleMs: number) {
// Stale detection allows automatic recovery after crashed owners.
const now = wall()
const heartbeat = await stats(heartbeatPath)
if (heartbeat) {
return now - heartbeat.mtimeMs > staleMs
}
const meta = await stats(metaPath)
if (meta) {
return now - meta.mtimeMs > staleMs
}
const dir = await stats(lockDir)
if (!dir) {
return false
}
return now - dir.mtimeMs > staleMs
}
async function tryAcquireLockDir(lockDir: string, opts: Opts): Promise<Owned | { acquired: false }> {
const token = randomUUID?.() ?? randomBytes(16).toString("hex")
const metaPath = path.join(lockDir, "meta.json")
const heartbeatPath = path.join(lockDir, "heartbeat")
try {
await mkdir(lockDir, { mode: 0o700 })
} catch (err) {
if (code(err) !== "EEXIST") {
throw err
}
if (!(await stale(lockDir, heartbeatPath, metaPath, opts.staleMs))) {
return { acquired: false }
}
const breakerPath = lockDir + ".breaker"
try {
await mkdir(breakerPath, { mode: 0o700 })
} catch (claimErr) {
const errCode = code(claimErr)
if (errCode === "EEXIST") {
const breaker = await stats(breakerPath)
if (breaker && wall() - breaker.mtimeMs > opts.staleMs) {
await rm(breakerPath, { recursive: true, force: true }).catch(() => undefined)
}
return { acquired: false }
}
if (errCode === "ENOENT" || errCode === "ENOTDIR") {
return { acquired: false }
}
throw claimErr
}
try {
// Breaker ownership ensures only one contender performs stale cleanup.
if (!(await stale(lockDir, heartbeatPath, metaPath, opts.staleMs))) {
return { acquired: false }
}
await rm(lockDir, { recursive: true, force: true })
try {
await mkdir(lockDir, { mode: 0o700 })
} catch (retryErr) {
const errCode = code(retryErr)
if (errCode === "EEXIST" || errCode === "ENOTEMPTY") {
return { acquired: false }
}
throw retryErr
}
} finally {
await rm(breakerPath, { recursive: true, force: true }).catch(() => undefined)
}
}
const meta = {
token,
pid: process.pid,
hostname: os.hostname(),
createdAt: new Date().toISOString(),
}
await writeFile(heartbeatPath, "", { flag: "wx" }).catch(async () => {
await rm(lockDir, { recursive: true, force: true })
throw new Error("Lock acquired but heartbeat already existed (possible compromise).")
})
await writeFile(metaPath, JSON.stringify(meta, null, 2), { flag: "wx" }).catch(async () => {
await rm(lockDir, { recursive: true, force: true })
throw new Error("Lock acquired but meta.json already existed (possible compromise).")
})
let timer: NodeJS.Timeout | undefined
const startHeartbeat = (intervalMs = Math.max(100, Math.floor(opts.staleMs / 3))) => {
if (timer) return
// Heartbeat prevents long critical sections from being evicted as stale.
timer = setInterval(() => {
const t = new Date()
void utimes(heartbeatPath, t, t).catch(() => undefined)
}, intervalMs)
timer.unref?.()
}
const release = async () => {
if (timer) {
clearInterval(timer)
timer = undefined
}
const current = await readFile(metaPath, "utf8")
.then((raw) => {
const parsed = JSON.parse(raw)
if (!parsed || typeof parsed !== "object") return {}
return {
token: "token" in parsed && typeof parsed.token === "string" ? parsed.token : undefined,
}
})
.catch((err) => {
const errCode = code(err)
if (errCode === "ENOENT" || errCode === "ENOTDIR") {
throw new Error("Refusing to release: lock is compromised (metadata missing).")
}
if (err instanceof SyntaxError) {
throw new Error("Refusing to release: lock is compromised (metadata invalid).")
}
throw err
})
// Token check prevents deleting a lock that was re-acquired by another process.
if (current.token !== token) {
throw new Error("Refusing to release: lock token mismatch (not the owner).")
}
await rm(lockDir, { recursive: true, force: true })
}
return {
acquired: true,
startHeartbeat,
release,
}
}
async function acquireLockDir(
lockDir: string,
input: { key: string; onWait?: Wait; signal?: AbortSignal },
opts: Opts,
) {
const stop = mono() + opts.timeoutMs
let attempt = 0
let waited = 0
let delay = opts.baseDelayMs
while (true) {
input.signal?.throwIfAborted()
const res = await tryAcquireLockDir(lockDir, opts)
if (res.acquired) {
return res
}
if (mono() > stop) {
throw new Error(`Timed out waiting for lock: ${input.key}`)
}
attempt += 1
const ms = jitter(delay)
await input.onWait?.({
key: input.key,
attempt,
delay: ms,
waited,
})
await sleep(ms, input.signal)
waited += ms
delay = Math.min(opts.maxDelayMs, Math.floor(delay * 1.7))
}
}
export async function acquire(key: string, input: Options = {}): Promise<Lease> {
input.signal?.throwIfAborted()
const cfg: Opts = {
staleMs: input.staleMs ?? defaultOpts.staleMs,
timeoutMs: input.timeoutMs ?? defaultOpts.timeoutMs,
baseDelayMs: input.baseDelayMs ?? defaultOpts.baseDelayMs,
maxDelayMs: input.maxDelayMs ?? defaultOpts.maxDelayMs,
}
const dir = input.dir ?? root()
await mkdir(dir, { recursive: true })
const lockfile = path.join(dir, Hash.fast(key) + ".lock")
const lock = await acquireLockDir(
lockfile,
{
key,
onWait: input.onWait,
signal: input.signal,
},
cfg,
)
lock.startHeartbeat()
const release = () => lock.release()
return {
release,
[Symbol.asyncDispose]() {
return release()
},
}
}
export async function withLock<T>(key: string, fn: () => Promise<T>, input: Options = {}) {
await using _ = await acquire(key, input)
input.signal?.throwIfAborted()
return await fn()
}
export const effect = Effect.fn("Flock.effect")(function* (key: string, input: Options = {}) {
return yield* Effect.acquireRelease(
Effect.promise((signal) => Flock.acquire(key, { ...input, signal })).pipe(
Effect.withSpan("Flock.acquire", {
attributes: { key },
}),
),
(lock) => Effect.promise(() => lock.release()).pipe(Effect.withSpan("Flock.release")),
).pipe(Effect.asVoid)
})
}
-11
View File
@@ -1,11 +0,0 @@
import { z } from "zod"
export function fn<T extends z.ZodType, Result>(schema: T, cb: (input: z.infer<T>) => Result) {
const result = (input: z.infer<T>) => {
const parsed = schema.parse(input)
return cb(parsed)
}
result.force = (input: z.infer<T>) => cb(input)
result.schema = schema
return result
}
-34
View File
@@ -1,34 +0,0 @@
import { glob, globSync, type GlobOptions } from "glob"
import { minimatch } from "minimatch"
export namespace Glob {
export interface Options {
cwd?: string
absolute?: boolean
include?: "file" | "all"
dot?: boolean
symlink?: boolean
}
function toGlobOptions(options: Options): GlobOptions {
return {
cwd: options.cwd,
absolute: options.absolute,
dot: options.dot,
follow: options.symlink ?? false,
nodir: options.include !== "all",
}
}
export async function scan(pattern: string, options: Options = {}): Promise<string[]> {
return glob(pattern, toGlobOptions(options)) as Promise<string[]>
}
export function scanSync(pattern: string, options: Options = {}): string[] {
return globSync(pattern, toGlobOptions(options)) as string[]
}
export function match(pattern: string, filepath: string): boolean {
return minimatch(filepath, pattern, { dot: true })
}
}

Some files were not shown because too many files have changed in this diff Show More