mirror of
https://github.com/anomalyco/opencode.git
synced 2026-07-17 12:56:41 +02:00
Compare commits
84 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 27db54c859 | |||
| 7a568a457f | |||
| 3ddc69ec2b | |||
| f3d5a71620 | |||
| c6c56ac2cf | |||
| e539efe2b9 | |||
| 687b758882 | |||
| 84e322b0fd | |||
| 8bc4f91fd9 | |||
| 93e633fb7d | |||
| cbe702c09d | |||
| a7a85c94b8 | |||
| 6e0178655b | |||
| e4be557928 | |||
| b9640fc7e4 | |||
| 29f05cb1ee | |||
| 48acab48ad | |||
| 5ae74aa881 | |||
| 6eddf08244 | |||
| 9c7e52b8a1 | |||
| a824064c4c | |||
| 33b2795cc8 | |||
| 10bd044c55 | |||
| c09bcfe531 | |||
| 83227be0ca | |||
| 8ee47a0533 | |||
| a546e88f37 | |||
| e998c9e9cb | |||
| 889087c966 | |||
| 7f3b64c7c4 | |||
| e60a6e3a82 | |||
| 135c8f0e99 | |||
| f02504bb80 | |||
| 40834fdf2f | |||
| fc0588954b | |||
| 75960e3bf3 | |||
| f14ac472a3 | |||
| 9ed93715ef | |||
| b34ca44abe | |||
| 40ba8f3570 | |||
| e543acf923 | |||
| d183568644 | |||
| f27eb8f09e | |||
| ad0545335a | |||
| cfbbae7323 | |||
| 940f971ca0 | |||
| 78ca49a1bc | |||
| 1d54b0e540 | |||
| 7e971d8302 | |||
| 54b3b3fe05 | |||
| 9d012b0621 | |||
| fbb0a93e12 | |||
| e2e7a8d722 | |||
| ce7923adaf | |||
| a26d53151b | |||
| 5eaef6b758 | |||
| c5c38cad9c | |||
| 9918f389e7 | |||
| dd8c424806 | |||
| 078d8a07cf | |||
| 1ee712e549 | |||
| 55315bdffa | |||
| 882b8e1e75 | |||
| 95edbc0ae6 | |||
| 11cd4fb639 | |||
| 9c16bd1e30 | |||
| 5e9d5c734e | |||
| b382d1a467 | |||
| 23f31475e7 | |||
| c0eab9e442 | |||
| 8a1e85d0c8 | |||
| 2793502db2 | |||
| 9f7bd0246c | |||
| a6a4350d10 | |||
| 471b9f4dc4 | |||
| 24fb9b1296 | |||
| 3573019916 | |||
| fc5b353144 | |||
| a4882290aa | |||
| 42771c1db3 | |||
| 2e18a603f0 | |||
| 9819eb0461 | |||
| aa86fb75ad | |||
| 6f5a3d30fd |
@@ -29,7 +29,7 @@
|
||||
},
|
||||
"packages/app": {
|
||||
"name": "@opencode-ai/app",
|
||||
"version": "1.4.11",
|
||||
"version": "1.14.19",
|
||||
"dependencies": {
|
||||
"@kobalte/core": "catalog:",
|
||||
"@opencode-ai/sdk": "workspace:*",
|
||||
@@ -83,7 +83,7 @@
|
||||
},
|
||||
"packages/console/app": {
|
||||
"name": "@opencode-ai/console-app",
|
||||
"version": "1.4.11",
|
||||
"version": "1.14.19",
|
||||
"dependencies": {
|
||||
"@cloudflare/vite-plugin": "1.15.2",
|
||||
"@ibm/plex": "6.4.1",
|
||||
@@ -117,7 +117,7 @@
|
||||
},
|
||||
"packages/console/core": {
|
||||
"name": "@opencode-ai/console-core",
|
||||
"version": "1.4.11",
|
||||
"version": "1.14.19",
|
||||
"dependencies": {
|
||||
"@aws-sdk/client-sts": "3.782.0",
|
||||
"@jsx-email/render": "1.1.1",
|
||||
@@ -144,7 +144,7 @@
|
||||
},
|
||||
"packages/console/function": {
|
||||
"name": "@opencode-ai/console-function",
|
||||
"version": "1.4.11",
|
||||
"version": "1.14.19",
|
||||
"dependencies": {
|
||||
"@ai-sdk/anthropic": "3.0.64",
|
||||
"@ai-sdk/openai": "3.0.48",
|
||||
@@ -168,7 +168,7 @@
|
||||
},
|
||||
"packages/console/mail": {
|
||||
"name": "@opencode-ai/console-mail",
|
||||
"version": "1.4.11",
|
||||
"version": "1.14.19",
|
||||
"dependencies": {
|
||||
"@jsx-email/all": "2.2.3",
|
||||
"@jsx-email/cli": "1.4.3",
|
||||
@@ -192,7 +192,7 @@
|
||||
},
|
||||
"packages/desktop": {
|
||||
"name": "@opencode-ai/desktop",
|
||||
"version": "1.4.11",
|
||||
"version": "1.14.19",
|
||||
"dependencies": {
|
||||
"@opencode-ai/app": "workspace:*",
|
||||
"@opencode-ai/ui": "workspace:*",
|
||||
@@ -225,8 +225,9 @@
|
||||
},
|
||||
"packages/desktop-electron": {
|
||||
"name": "@opencode-ai/desktop-electron",
|
||||
"version": "1.4.11",
|
||||
"version": "1.14.19",
|
||||
"dependencies": {
|
||||
"drizzle-orm": "catalog:",
|
||||
"effect": "catalog:",
|
||||
"electron-context-menu": "4.1.2",
|
||||
"electron-log": "^5",
|
||||
@@ -248,7 +249,7 @@
|
||||
"@types/node": "catalog:",
|
||||
"@typescript/native-preview": "catalog:",
|
||||
"@valibot/to-json-schema": "1.6.0",
|
||||
"electron": "40.4.1",
|
||||
"electron": "41.2.1",
|
||||
"electron-builder": "^26",
|
||||
"electron-vite": "^5",
|
||||
"solid-js": "catalog:",
|
||||
@@ -268,7 +269,7 @@
|
||||
},
|
||||
"packages/enterprise": {
|
||||
"name": "@opencode-ai/enterprise",
|
||||
"version": "1.4.11",
|
||||
"version": "1.14.19",
|
||||
"dependencies": {
|
||||
"@opencode-ai/shared": "workspace:*",
|
||||
"@opencode-ai/ui": "workspace:*",
|
||||
@@ -297,7 +298,7 @@
|
||||
},
|
||||
"packages/function": {
|
||||
"name": "@opencode-ai/function",
|
||||
"version": "1.4.11",
|
||||
"version": "1.14.19",
|
||||
"dependencies": {
|
||||
"@octokit/auth-app": "8.0.1",
|
||||
"@octokit/rest": "catalog:",
|
||||
@@ -313,7 +314,7 @@
|
||||
},
|
||||
"packages/opencode": {
|
||||
"name": "opencode",
|
||||
"version": "1.4.11",
|
||||
"version": "1.14.19",
|
||||
"bin": {
|
||||
"opencode": "./bin/opencode",
|
||||
},
|
||||
@@ -322,7 +323,7 @@
|
||||
"@actions/github": "6.0.1",
|
||||
"@agentclientprotocol/sdk": "0.16.1",
|
||||
"@ai-sdk/alibaba": "1.0.17",
|
||||
"@ai-sdk/amazon-bedrock": "4.0.95",
|
||||
"@ai-sdk/amazon-bedrock": "4.0.96",
|
||||
"@ai-sdk/anthropic": "3.0.71",
|
||||
"@ai-sdk/azure": "3.0.49",
|
||||
"@ai-sdk/cerebras": "2.0.41",
|
||||
@@ -353,6 +354,7 @@
|
||||
"@lydell/node-pty": "catalog:",
|
||||
"@modelcontextprotocol/sdk": "1.27.1",
|
||||
"@npmcli/arborist": "9.4.0",
|
||||
"@npmcli/config": "10.8.1",
|
||||
"@octokit/graphql": "9.0.2",
|
||||
"@octokit/rest": "catalog:",
|
||||
"@openauthjs/openauth": "catalog:",
|
||||
@@ -365,8 +367,8 @@
|
||||
"@opentelemetry/exporter-trace-otlp-http": "0.214.0",
|
||||
"@opentelemetry/sdk-trace-base": "2.6.1",
|
||||
"@opentelemetry/sdk-trace-node": "2.6.1",
|
||||
"@opentui/core": "catalog:",
|
||||
"@opentui/solid": "catalog:",
|
||||
"@opentui/core": "0.1.101",
|
||||
"@opentui/solid": "0.1.101",
|
||||
"@parcel/watcher": "2.5.1",
|
||||
"@pierre/diffs": "catalog:",
|
||||
"@solid-primitives/event-bus": "1.1.2",
|
||||
@@ -404,7 +406,6 @@
|
||||
"opentui-spinner": "0.0.6",
|
||||
"partial-json": "0.1.7",
|
||||
"remeda": "catalog:",
|
||||
"ripgrep": "0.3.1",
|
||||
"semver": "^7.6.3",
|
||||
"solid-js": "catalog:",
|
||||
"strip-ansi": "7.1.2",
|
||||
@@ -458,23 +459,23 @@
|
||||
},
|
||||
"packages/plugin": {
|
||||
"name": "@opencode-ai/plugin",
|
||||
"version": "1.4.11",
|
||||
"version": "1.14.19",
|
||||
"dependencies": {
|
||||
"@opencode-ai/sdk": "workspace:*",
|
||||
"effect": "catalog:",
|
||||
"zod": "catalog:",
|
||||
},
|
||||
"devDependencies": {
|
||||
"@opentui/core": "catalog:",
|
||||
"@opentui/solid": "catalog:",
|
||||
"@opentui/core": "0.1.101",
|
||||
"@opentui/solid": "0.1.101",
|
||||
"@tsconfig/node22": "catalog:",
|
||||
"@types/node": "catalog:",
|
||||
"@typescript/native-preview": "catalog:",
|
||||
"typescript": "catalog:",
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@opentui/core": ">=0.1.100",
|
||||
"@opentui/solid": ">=0.1.100",
|
||||
"@opentui/core": ">=0.1.101",
|
||||
"@opentui/solid": ">=0.1.101",
|
||||
},
|
||||
"optionalPeers": [
|
||||
"@opentui/core",
|
||||
@@ -493,7 +494,7 @@
|
||||
},
|
||||
"packages/sdk/js": {
|
||||
"name": "@opencode-ai/sdk",
|
||||
"version": "1.4.11",
|
||||
"version": "1.14.19",
|
||||
"dependencies": {
|
||||
"cross-spawn": "catalog:",
|
||||
},
|
||||
@@ -508,7 +509,7 @@
|
||||
},
|
||||
"packages/shared": {
|
||||
"name": "@opencode-ai/shared",
|
||||
"version": "1.4.11",
|
||||
"version": "1.14.19",
|
||||
"bin": {
|
||||
"opencode": "./bin/opencode",
|
||||
},
|
||||
@@ -532,7 +533,7 @@
|
||||
},
|
||||
"packages/slack": {
|
||||
"name": "@opencode-ai/slack",
|
||||
"version": "1.4.11",
|
||||
"version": "1.14.19",
|
||||
"dependencies": {
|
||||
"@opencode-ai/sdk": "workspace:*",
|
||||
"@slack/bolt": "^3.17.1",
|
||||
@@ -567,7 +568,7 @@
|
||||
},
|
||||
"packages/ui": {
|
||||
"name": "@opencode-ai/ui",
|
||||
"version": "1.4.11",
|
||||
"version": "1.14.19",
|
||||
"dependencies": {
|
||||
"@kobalte/core": "catalog:",
|
||||
"@opencode-ai/sdk": "workspace:*",
|
||||
@@ -616,7 +617,7 @@
|
||||
},
|
||||
"packages/web": {
|
||||
"name": "@opencode-ai/web",
|
||||
"version": "1.4.11",
|
||||
"version": "1.14.19",
|
||||
"dependencies": {
|
||||
"@astrojs/cloudflare": "12.6.3",
|
||||
"@astrojs/markdown-remark": "6.3.1",
|
||||
@@ -660,6 +661,7 @@
|
||||
"patchedDependencies": {
|
||||
"solid-js@1.9.10": "patches/solid-js@1.9.10.patch",
|
||||
"@standard-community/standard-openapi@0.2.9": "patches/@standard-community%2Fstandard-openapi@0.2.9.patch",
|
||||
"@npmcli/agent@4.0.0": "patches/@npmcli%2Fagent@4.0.0.patch",
|
||||
},
|
||||
"overrides": {
|
||||
"@types/bun": "catalog:",
|
||||
@@ -740,7 +742,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/amazon-bedrock": ["@ai-sdk/amazon-bedrock@4.0.95", "", { "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-qJKWEy+cNx3bLSJi/XpIVhv0P8KO0JFB1SvEroNWN8gKm820SIglBmXS10DTeXJdM5PPbQX4i/wJj5BHEk2LRQ=="],
|
||||
"@ai-sdk/amazon-bedrock": ["@ai-sdk/amazon-bedrock@4.0.96", "", { "dependencies": { "@ai-sdk/anthropic": "3.0.71", "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.23", "@smithy/eventstream-codec": "^4.0.1", "@smithy/util-utf8": "^4.0.0", "aws4fetch": "^1.0.20" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-Mc4Ias2jRMD1jOB6xWtKNPdhECeuCZyIlbr9EAGfBnyBt++sS13ziZh9qv9TdyMCAZJ7xoQcpbchoRJcKwPdpA=="],
|
||||
|
||||
"@ai-sdk/anthropic": ["@ai-sdk/anthropic@3.0.64", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.21" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-rwLi/Rsuj2pYniQXIrvClHvXDzgM4UQHHnvHTWEF14efnlKclG/1ghpNC+adsRujAbCTr6gRsSbDE2vEqriV7g=="],
|
||||
|
||||
@@ -1468,6 +1470,8 @@
|
||||
|
||||
"@npmcli/arborist": ["@npmcli/arborist@9.4.0", "", { "dependencies": { "@isaacs/string-locale-compare": "^1.1.0", "@npmcli/fs": "^5.0.0", "@npmcli/installed-package-contents": "^4.0.0", "@npmcli/map-workspaces": "^5.0.0", "@npmcli/metavuln-calculator": "^9.0.2", "@npmcli/name-from-folder": "^4.0.0", "@npmcli/node-gyp": "^5.0.0", "@npmcli/package-json": "^7.0.0", "@npmcli/query": "^5.0.0", "@npmcli/redact": "^4.0.0", "@npmcli/run-script": "^10.0.0", "bin-links": "^6.0.0", "cacache": "^20.0.1", "common-ancestor-path": "^2.0.0", "hosted-git-info": "^9.0.0", "json-stringify-nice": "^1.1.4", "lru-cache": "^11.2.1", "minimatch": "^10.0.3", "nopt": "^9.0.0", "npm-install-checks": "^8.0.0", "npm-package-arg": "^13.0.0", "npm-pick-manifest": "^11.0.1", "npm-registry-fetch": "^19.0.0", "pacote": "^21.0.2", "parse-conflict-json": "^5.0.1", "proc-log": "^6.0.0", "proggy": "^4.0.0", "promise-all-reject-late": "^1.0.0", "promise-call-limit": "^3.0.1", "semver": "^7.3.7", "ssri": "^13.0.0", "treeverse": "^3.0.0", "walk-up-path": "^4.0.0" }, "bin": { "arborist": "bin/index.js" } }, "sha512-4Bm8hNixJG/sii1PMnag0V9i/sGOX9VRzFrUiZMSBJpGlLR38f+Btl85d07G9GL56xO0l0OZjvrGNYsDYp0xKA=="],
|
||||
|
||||
"@npmcli/config": ["@npmcli/config@10.8.1", "", { "dependencies": { "@npmcli/map-workspaces": "^5.0.0", "@npmcli/package-json": "^7.0.0", "ci-info": "^4.0.0", "ini": "^6.0.0", "nopt": "^9.0.0", "proc-log": "^6.0.0", "semver": "^7.3.5", "walk-up-path": "^4.0.0" } }, "sha512-MAYk9IlIGiyC0c9fnjdBSQfIFPZT0g1MfeSiD1UXTq2zJOLX55jS9/sETJHqw/7LN18JjITrhYfgCfapbmZHiQ=="],
|
||||
|
||||
"@npmcli/fs": ["@npmcli/fs@5.0.0", "", { "dependencies": { "semver": "^7.3.5" } }, "sha512-7OsC1gNORBEawOa5+j2pXN9vsicaIOH5cPXxoR6fJOmH6/EXpJB2CajXOu1fPRFun2m1lktEFX11+P89hqO/og=="],
|
||||
|
||||
"@npmcli/git": ["@npmcli/git@7.0.2", "", { "dependencies": { "@gar/promise-retry": "^1.0.0", "@npmcli/promise-spawn": "^9.0.0", "ini": "^6.0.0", "lru-cache": "^11.2.1", "npm-pick-manifest": "^11.0.1", "proc-log": "^6.0.0", "semver": "^7.3.5", "which": "^6.0.0" } }, "sha512-oeolHDjExNAJAnlYP2qzNjMX/Xi9bmu78C9dIGr4xjobrSKbuMYCph8lTzn4vnW3NjIqVmw/f8BCfouqyJXlRg=="],
|
||||
@@ -1600,21 +1604,21 @@
|
||||
|
||||
"@opentelemetry/semantic-conventions": ["@opentelemetry/semantic-conventions@1.40.0", "", {}, "sha512-cifvXDhcqMwwTlTK04GBNeIe7yyo28Mfby85QXFe1Yk8nmi36Ab/5UQwptOx84SsoGNRg+EVSjwzfSZMy6pmlw=="],
|
||||
|
||||
"@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": ["@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-darwin-arm64": ["@opentui/core-darwin-arm64@0.1.99", "", { "os": "darwin", "cpu": "arm64" }, "sha512-bzVrqeX2vb5iWrc/ftOUOqeUY8XO+qSgoTwj5TXHuwagavgwD3Hpeyjx8+icnTTeM4pao0som1WR9xfye6/X5Q=="],
|
||||
"@opentui/core-darwin-arm64": ["@opentui/core-darwin-arm64@0.1.101", "", { "os": "darwin", "cpu": "arm64" }, "sha512-HtqZh8TIKCH1Nge5J0etBCpzYfPY4fVcq110uJm2As6D/dTTPv8r4J+KkrqoSphkpj/Y2b4t7KpqNHthXA0EVw=="],
|
||||
|
||||
"@opentui/core-darwin-x64": ["@opentui/core-darwin-x64@0.1.99", "", { "os": "darwin", "cpu": "x64" }, "sha512-VE4FrXBYpkxnvkqcCV1a8aN9jyyMJMihVW+V2NLCtp+4yQsj0AapG5TiUSN76XnmSZRptxDy5rBmEempeoIZbg=="],
|
||||
"@opentui/core-darwin-x64": ["@opentui/core-darwin-x64@0.1.101", "", { "os": "darwin", "cpu": "x64" }, "sha512-o5ClQWnGG1inRE2YZAatPw1jPEAJni00amcoIfKBj8e1WS+fQA+iQTq1xFunNcyNPObLDCVuW1X+NrbK9xmPvQ=="],
|
||||
|
||||
"@opentui/core-linux-arm64": ["@opentui/core-linux-arm64@0.1.99", "", { "os": "linux", "cpu": "arm64" }, "sha512-viXQsbpS7yHjYkl7+am32JdvG96QU9lvHh1UiZtpOxcNUUqiYmA2ZwZFPD2Bi54jNyj5l2hjH6YkD3DzE2FEWA=="],
|
||||
"@opentui/core-linux-arm64": ["@opentui/core-linux-arm64@0.1.101", "", { "os": "linux", "cpu": "arm64" }, "sha512-E/weY7DQpaPWGYDPD0CROHowUotqnVlk7Kb6l9+iZCrxm9s7HPRHkcMDVmcWDqHEqa/J879EJcqaUDzDArqC+w=="],
|
||||
|
||||
"@opentui/core-linux-x64": ["@opentui/core-linux-x64@0.1.99", "", { "os": "linux", "cpu": "x64" }, "sha512-WLoEFINOSp0tZSR9y4LUuGc7n4Y7H1wcpjUPzQ9vChkYDXrfZltEanzoDWbDcQ4kZQW5tHVC7LrZHpAsRLwFZg=="],
|
||||
"@opentui/core-linux-x64": ["@opentui/core-linux-x64@0.1.101", "", { "os": "linux", "cpu": "x64" }, "sha512-+Bfr8jLbbR1WREUMCCvSZ44G1+WU2lPqJx7x1StTa9iFNEdicxCdd0QQsO6cnKn5yW+2Pr/FdrqHbxSQw3ejbA=="],
|
||||
|
||||
"@opentui/core-win32-arm64": ["@opentui/core-win32-arm64@0.1.99", "", { "os": "win32", "cpu": "arm64" }, "sha512-yWMOLWCEO8HdrctU1dMkgZC8qGkiO4Dwr4/e11tTvVpRmYhDsP/IR89ZjEEtOwnKwFOFuB/MxvflqaEWVQ2g5Q=="],
|
||||
"@opentui/core-win32-arm64": ["@opentui/core-win32-arm64@0.1.101", "", { "os": "win32", "cpu": "arm64" }, "sha512-LTMIHJzJrVqS8mgpp+tuyVHuqYlicQTvFi/sTsJ6Xswf1asatsvZYsbQByhBLpFT80j10G7uvDa361S5gjCUDA=="],
|
||||
|
||||
"@opentui/core-win32-x64": ["@opentui/core-win32-x64@0.1.99", "", { "os": "win32", "cpu": "x64" }, "sha512-aYRlsL2w8YRL6vPd7/hrqlNVkXU3QowWb01TOvAcHS8UAsXaGFUr47kSDyjxDi1wg1MzmVduCfsC7T3NoThV1w=="],
|
||||
"@opentui/core-win32-x64": ["@opentui/core-win32-x64@0.1.101", "", { "os": "win32", "cpu": "x64" }, "sha512-VaMs5bg6y0tYKptaEK8Hy5wTp4m//wJRKUdW8uvrS9cFgxyovZGuw0+TfK3NgbdeX+8jWm8LEAiak4jle5BABg=="],
|
||||
|
||||
"@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=="],
|
||||
"@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=="],
|
||||
|
||||
"@oslojs/asn1": ["@oslojs/asn1@1.0.0", "", { "dependencies": { "@oslojs/binary": "1.0.0" } }, "sha512-zw/wn0sj0j0QKbIXfIlnEcTviaCzYOY3V5rAyjR6YtOByFtJiT574+8p9Wlach0lZH9fddD4yb9laEAIl4vXQA=="],
|
||||
|
||||
@@ -3026,7 +3030,7 @@
|
||||
|
||||
"ejs": ["ejs@3.1.10", "", { "dependencies": { "jake": "^10.8.5" }, "bin": { "ejs": "bin/cli.js" } }, "sha512-UeJmFfOrAQS8OJWPZ4qtgHyWExa088/MtK5UEyoJGFH67cDEXkZSviOiKRCZ4Xij0zxI3JECgYs3oKx+AizQBA=="],
|
||||
|
||||
"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": ["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-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=="],
|
||||
|
||||
@@ -3306,7 +3310,7 @@
|
||||
|
||||
"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#4af877d", {}, "anomalyco-ghostty-web-4af877d", "sha512-fbEK8mtr7ar4ySsF+JUGjhaZrane7dKphanN+SxHt5XXI6yLMAh/Hpf6sNCOyyVa2UlGCd7YpXG/T2v2RUAX+A=="],
|
||||
"ghostty-web": ["ghostty-web@github:anomalyco/ghostty-web#20bd361", {}, "anomalyco-ghostty-web-20bd361", "sha512-dW0nwaiBBcun9y5WJSvm3HxDLe5o9V0xLCndQvWonRVubU8CS1PHxZpLffyPt1YujPWC13ez03aWxcuKBPYYGQ=="],
|
||||
|
||||
"gifwrap": ["gifwrap@0.10.1", "", { "dependencies": { "image-q": "^4.0.0", "omggif": "^1.0.10" } }, "sha512-2760b1vpJHNmLzZ/ubTtNnEx5WApN/PYWJvXvgS+tL1egTTthayFYIQQNi136FLEDcN/IyEY2EcGpIITD6eYUw=="],
|
||||
|
||||
@@ -4482,8 +4486,6 @@
|
||||
|
||||
"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=="],
|
||||
|
||||
"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=="],
|
||||
|
||||
@@ -236,7 +236,6 @@ new sst.cloudflare.x.SolidStart("Console", {
|
||||
SALESFORCE_INSTANCE_URL,
|
||||
ZEN_BLACK_PRICE,
|
||||
ZEN_LITE_PRICE,
|
||||
new sst.Secret("ZEN_LITE_COUPON_FIRST_MONTH_100_INVITEES"),
|
||||
new sst.Secret("ZEN_LIMITS"),
|
||||
new sst.Secret("ZEN_SESSION_SECRET"),
|
||||
...ZEN_MODELS,
|
||||
|
||||
+4
-4
@@ -1,8 +1,8 @@
|
||||
{
|
||||
"nodeModules": {
|
||||
"x86_64-linux": "sha256-GjpBQhvGLTM6NWX29b/mS+KjrQPl0w9VjQHH5jaK9SM=",
|
||||
"aarch64-linux": "sha256-F5h9p+iZ8CASdUYaYR7O22NwBRa/iT+ZinUxO8lbPTc=",
|
||||
"aarch64-darwin": "sha256-jWo5yvCtjVKRf9i5XUcTTaLtj2+G6+T1Td2llO/cT5I=",
|
||||
"x86_64-darwin": "sha256-LzV+5/8P2mkiFHmt+a8zDeJjRbU8z9nssSA4tzv1HxA="
|
||||
"x86_64-linux": "sha256-DOGOZdPdkcuyDhVAyWHGsL4rrV28S+YFZj/VORuoQ8Q=",
|
||||
"aarch64-linux": "sha256-WRnAaEoKvgFFZ+UkbYtD9gBw0HtV1jdUqv7yUE2uTAQ=",
|
||||
"aarch64-darwin": "sha256-LxIj/dsL88M99T3WLaD9FL6Qdu2TV+kr1RMZaZ3i4WM=",
|
||||
"x86_64-darwin": "sha256-PgIvplw6yz9KN5nBWox3BXZIXDbkJ3ZuDPKKSVF82MU="
|
||||
}
|
||||
}
|
||||
|
||||
+18
-17
@@ -7,6 +7,7 @@
|
||||
sysctl,
|
||||
makeBinaryWrapper,
|
||||
models-dev,
|
||||
ripgrep,
|
||||
installShellFiles,
|
||||
versionCheckHook,
|
||||
writableTmpDirAsHomeHook,
|
||||
@@ -51,25 +52,25 @@ stdenvNoCC.mkDerivation (finalAttrs: {
|
||||
runHook postBuild
|
||||
'';
|
||||
|
||||
installPhase =
|
||||
''
|
||||
runHook preInstall
|
||||
installPhase = ''
|
||||
runHook preInstall
|
||||
|
||||
install -Dm755 dist/opencode-*/bin/opencode $out/bin/opencode
|
||||
install -Dm644 schema.json $out/share/opencode/schema.json
|
||||
''
|
||||
# bun runs sysctl to detect if dunning on rosetta2
|
||||
+ lib.optionalString stdenvNoCC.hostPlatform.isDarwin ''
|
||||
wrapProgram $out/bin/opencode \
|
||||
--prefix PATH : ${
|
||||
lib.makeBinPath [
|
||||
sysctl
|
||||
install -Dm755 dist/opencode-*/bin/opencode $out/bin/opencode
|
||||
install -Dm644 schema.json $out/share/opencode/schema.json
|
||||
|
||||
wrapProgram $out/bin/opencode \
|
||||
--prefix PATH : ${
|
||||
lib.makeBinPath (
|
||||
[
|
||||
ripgrep
|
||||
]
|
||||
}
|
||||
''
|
||||
+ ''
|
||||
runHook postInstall
|
||||
'';
|
||||
# bun runs sysctl to detect if dunning on rosetta2
|
||||
++ lib.optional stdenvNoCC.hostPlatform.isDarwin sysctl
|
||||
)
|
||||
}
|
||||
|
||||
runHook postInstall
|
||||
'';
|
||||
|
||||
postInstall = lib.optionalString (stdenvNoCC.buildPlatform.canExecute stdenvNoCC.hostPlatform) ''
|
||||
# trick yargs into also generating zsh completions
|
||||
|
||||
+2
-1
@@ -7,7 +7,7 @@
|
||||
"packageManager": "bun@1.3.11",
|
||||
"scripts": {
|
||||
"dev": "bun run --cwd packages/opencode --conditions=browser src/index.ts",
|
||||
"dev:desktop": "bun --cwd packages/desktop tauri dev",
|
||||
"dev:desktop": "bun --cwd packages/desktop-electron dev",
|
||||
"dev:web": "bun --cwd packages/app dev",
|
||||
"dev:console": "ulimit -n 10240 2>/dev/null; bun run --cwd packages/console/app dev",
|
||||
"dev:storybook": "bun --cwd packages/storybook storybook",
|
||||
@@ -127,6 +127,7 @@
|
||||
"@types/node": "catalog:"
|
||||
},
|
||||
"patchedDependencies": {
|
||||
"@npmcli/agent@4.0.0": "patches/@npmcli%2Fagent@4.0.0.patch",
|
||||
"@standard-community/standard-openapi@0.2.9": "patches/@standard-community%2Fstandard-openapi@0.2.9.patch",
|
||||
"solid-js@1.9.10": "patches/solid-js@1.9.10.patch"
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@opencode-ai/app",
|
||||
"version": "1.4.11",
|
||||
"version": "1.14.19",
|
||||
"description": "",
|
||||
"type": "module",
|
||||
"exports": {
|
||||
|
||||
Binary file not shown.
+21
-21
@@ -141,13 +141,11 @@ export function AppBaseProviders(props: ParentProps<{ locale?: Locale }>) {
|
||||
<LanguageProvider locale={props.locale}>
|
||||
<UiI18nBridge>
|
||||
<ErrorBoundary fallback={(error) => <ErrorPage error={error} />}>
|
||||
<QueryProvider>
|
||||
<DialogProvider>
|
||||
<MarkedProvider>
|
||||
<FileComponentProvider component={File}>{props.children}</FileComponentProvider>
|
||||
</MarkedProvider>
|
||||
</DialogProvider>
|
||||
</QueryProvider>
|
||||
<DialogProvider>
|
||||
<MarkedProvider>
|
||||
<FileComponentProvider component={File}>{props.children}</FileComponentProvider>
|
||||
</MarkedProvider>
|
||||
</DialogProvider>
|
||||
</ErrorBoundary>
|
||||
</UiI18nBridge>
|
||||
</LanguageProvider>
|
||||
@@ -293,20 +291,22 @@ export function AppInterface(props: {
|
||||
>
|
||||
<ConnectionGate disableHealthCheck={props.disableHealthCheck}>
|
||||
<ServerKey>
|
||||
<GlobalSDKProvider>
|
||||
<GlobalSyncProvider>
|
||||
<Dynamic
|
||||
component={props.router ?? Router}
|
||||
root={(routerProps) => <RouterRoot appChildren={props.children}>{routerProps.children}</RouterRoot>}
|
||||
>
|
||||
<Route path="/" component={HomeRoute} />
|
||||
<Route path="/:dir" component={DirectoryLayout}>
|
||||
<Route path="/" component={SessionIndexRoute} />
|
||||
<Route path="/session/:id?" component={SessionRoute} />
|
||||
</Route>
|
||||
</Dynamic>
|
||||
</GlobalSyncProvider>
|
||||
</GlobalSDKProvider>
|
||||
<QueryProvider>
|
||||
<GlobalSDKProvider>
|
||||
<GlobalSyncProvider>
|
||||
<Dynamic
|
||||
component={props.router ?? Router}
|
||||
root={(routerProps) => <RouterRoot appChildren={props.children}>{routerProps.children}</RouterRoot>}
|
||||
>
|
||||
<Route path="/" component={HomeRoute} />
|
||||
<Route path="/:dir" component={DirectoryLayout}>
|
||||
<Route path="/" component={SessionIndexRoute} />
|
||||
<Route path="/session/:id?" component={SessionRoute} />
|
||||
</Route>
|
||||
</Dynamic>
|
||||
</GlobalSyncProvider>
|
||||
</GlobalSDKProvider>
|
||||
</QueryProvider>
|
||||
</ServerKey>
|
||||
</ConnectionGate>
|
||||
</ServerProvider>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { useFilteredList } from "@opencode-ai/ui/hooks"
|
||||
import { useSpring } from "@opencode-ai/ui/motion-spring"
|
||||
import { createEffect, on, Component, Show, onCleanup, createMemo, createSignal } from "solid-js"
|
||||
import { createEffect, on, Component, Show, onCleanup, createMemo, createSignal, createResource } from "solid-js"
|
||||
import { createStore } from "solid-js/store"
|
||||
import { useLocal } from "@/context/local"
|
||||
import { selectionFromLines, type SelectedLineRange, useFile } from "@/context/file"
|
||||
@@ -54,7 +54,7 @@ import { PromptImageAttachments } from "./prompt-input/image-attachments"
|
||||
import { PromptDragOverlay } from "./prompt-input/drag-overlay"
|
||||
import { promptPlaceholder } from "./prompt-input/placeholder"
|
||||
import { ImagePreview } from "@opencode-ai/ui/image-preview"
|
||||
import { useQuery } from "@tanstack/solid-query"
|
||||
import { useQueries, useQuery } from "@tanstack/solid-query"
|
||||
import { loadAgentsQuery, loadProvidersQuery } from "@/context/global-sync/bootstrap"
|
||||
|
||||
interface PromptInputProps {
|
||||
@@ -1252,16 +1252,21 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
|
||||
}
|
||||
}
|
||||
|
||||
const agentsQuery = useQuery(() => loadAgentsQuery(sdk.directory))
|
||||
const [agentsQuery, globalProvidersQuery, providersQuery] = useQueries(() => ({
|
||||
queries: [loadAgentsQuery(sdk.directory), loadProvidersQuery(null), loadProvidersQuery(sdk.directory)],
|
||||
}))
|
||||
|
||||
const agentsLoading = () => agentsQuery.isLoading
|
||||
|
||||
const globalProvidersQuery = useQuery(() => loadProvidersQuery(null))
|
||||
const providersQuery = useQuery(() => loadProvidersQuery(sdk.directory))
|
||||
|
||||
const providersLoading = () => agentsLoading() || providersQuery.isLoading || globalProvidersQuery.isLoading
|
||||
|
||||
const [promptReady] = createResource(
|
||||
() => prompt.ready().promise,
|
||||
(p) => p,
|
||||
)
|
||||
|
||||
return (
|
||||
<div class="relative size-full _max-h-[320px] flex flex-col gap-0">
|
||||
{(promptReady(), null)}
|
||||
<PromptPopover
|
||||
popover={store.popover}
|
||||
setSlashPopoverRef={(el) => (slashPopoverRef = el)}
|
||||
@@ -1358,15 +1363,13 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
|
||||
}}
|
||||
style={{ "padding-bottom": space }}
|
||||
/>
|
||||
<Show when={!prompt.dirty()}>
|
||||
<div
|
||||
class="absolute top-0 inset-x-0 pl-3 pr-2 pt-2 text-14-regular text-text-weak pointer-events-none whitespace-nowrap truncate"
|
||||
classList={{ "font-mono!": store.mode === "shell" }}
|
||||
style={{ "padding-bottom": space }}
|
||||
>
|
||||
{placeholder()}
|
||||
</div>
|
||||
</Show>
|
||||
<div
|
||||
class="absolute top-0 inset-x-0 pl-3 pr-2 pt-2 text-14-regular text-text-weak pointer-events-none whitespace-nowrap truncate"
|
||||
classList={{ "font-mono!": store.mode === "shell" }}
|
||||
style={{ "padding-bottom": space, display: prompt.dirty() ? "none" : undefined }}
|
||||
>
|
||||
{placeholder()}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
@@ -1457,7 +1460,7 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
|
||||
</div>
|
||||
<div class="flex items-center gap-1.5 min-w-0 flex-1 h-7">
|
||||
<Show when={!agentsLoading()}>
|
||||
<div data-component="prompt-agent-control">
|
||||
<div data-component="prompt-agent-control" style={{ animation: "fade-in 0.3s" }}>
|
||||
<TooltipKeybind
|
||||
placement="top"
|
||||
gutter={4}
|
||||
@@ -1483,7 +1486,7 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
|
||||
</Show>
|
||||
<Show when={!providersLoading()}>
|
||||
<Show when={store.mode !== "shell"}>
|
||||
<div data-component="prompt-model-control">
|
||||
<div data-component="prompt-model-control" style={{ animation: "fade-in 0.3s" }}>
|
||||
<Show
|
||||
when={providers.paid().length > 0}
|
||||
fallback={
|
||||
@@ -1554,7 +1557,7 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
|
||||
</TooltipKeybind>
|
||||
</Show>
|
||||
</div>
|
||||
<div data-component="prompt-variant-control">
|
||||
<div data-component="prompt-variant-control" style={{ animation: "fade-in 0.3s" }}>
|
||||
<TooltipKeybind
|
||||
placement="top"
|
||||
gutter={4}
|
||||
|
||||
@@ -19,6 +19,9 @@ import {
|
||||
sansDefault,
|
||||
sansFontFamily,
|
||||
sansInput,
|
||||
terminalDefault,
|
||||
terminalFontFamily,
|
||||
terminalInput,
|
||||
useSettings,
|
||||
} from "@/context/settings"
|
||||
import { decode64 } from "@/utils/base64"
|
||||
@@ -181,6 +184,7 @@ export const SettingsGeneral: Component = () => {
|
||||
const soundOptions = [noneSound, ...SOUND_OPTIONS]
|
||||
const mono = () => monoInput(settings.appearance.font())
|
||||
const sans = () => sansInput(settings.appearance.uiFont())
|
||||
const terminal = () => terminalInput(settings.appearance.terminalFont())
|
||||
|
||||
const soundSelectProps = (
|
||||
enabled: () => boolean,
|
||||
@@ -451,6 +455,29 @@ export const SettingsGeneral: Component = () => {
|
||||
/>
|
||||
</div>
|
||||
</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>
|
||||
</div>
|
||||
)
|
||||
|
||||
@@ -11,7 +11,7 @@ import { useLanguage } from "@/context/language"
|
||||
import { usePlatform } from "@/context/platform"
|
||||
import { useSDK } from "@/context/sdk"
|
||||
import { useServer } from "@/context/server"
|
||||
import { monoFontFamily, useSettings } from "@/context/settings"
|
||||
import { terminalFontFamily, useSettings } from "@/context/settings"
|
||||
import type { LocalPTY } from "@/context/terminal"
|
||||
import { disposeIfDisposable, getHoveredLinkText, setOptionIfSupported } from "@/utils/runtime-adapters"
|
||||
import { terminalWriter } from "@/utils/terminal-writer"
|
||||
@@ -300,7 +300,7 @@ export const Terminal = (props: TerminalProps) => {
|
||||
})
|
||||
|
||||
createEffect(() => {
|
||||
const font = monoFontFamily(settings.appearance.font())
|
||||
const font = terminalFontFamily(settings.appearance.terminalFont())
|
||||
if (!term) return
|
||||
setOptionIfSupported(term, "fontFamily", font)
|
||||
scheduleFit()
|
||||
@@ -360,7 +360,7 @@ export const Terminal = (props: TerminalProps) => {
|
||||
cols: restoreSize?.cols,
|
||||
rows: restoreSize?.rows,
|
||||
fontSize: 14,
|
||||
fontFamily: monoFontFamily(settings.appearance.font()),
|
||||
fontFamily: terminalFontFamily(settings.appearance.terminalFont()),
|
||||
allowTransparency: false,
|
||||
convertEol: false,
|
||||
theme: terminalColors(),
|
||||
|
||||
@@ -9,7 +9,7 @@ import type {
|
||||
} from "@opencode-ai/sdk/v2/client"
|
||||
import { showToast } from "@opencode-ai/ui/toast"
|
||||
import { getFilename } from "@opencode-ai/shared/util/path"
|
||||
import { createContext, getOwner, onCleanup, onMount, type ParentProps, untrack, useContext } from "solid-js"
|
||||
import { batch, createContext, getOwner, onCleanup, onMount, type ParentProps, untrack, useContext } from "solid-js"
|
||||
import { createStore, produce, reconcile } from "solid-js/store"
|
||||
import { useLanguage } from "@/context/language"
|
||||
import { Persist, persisted } from "@/utils/persist"
|
||||
@@ -223,16 +223,18 @@ function createGlobalSync() {
|
||||
limit,
|
||||
permission: store.permission,
|
||||
})
|
||||
setStore(
|
||||
"sessionTotal",
|
||||
estimateRootSessionTotal({
|
||||
count: nonArchived.length,
|
||||
limit: x.limit,
|
||||
limited: x.limited,
|
||||
}),
|
||||
)
|
||||
setStore("session", reconcile(sessions, { key: "id" }))
|
||||
cleanupDroppedSessionCaches(store, setStore, sessions, setSessionTodo)
|
||||
batch(() => {
|
||||
setStore(
|
||||
"sessionTotal",
|
||||
estimateRootSessionTotal({
|
||||
count: nonArchived.length,
|
||||
limit: x.limit,
|
||||
limited: x.limited,
|
||||
}),
|
||||
)
|
||||
setStore("session", reconcile(sessions, { key: "id" }))
|
||||
cleanupDroppedSessionCaches(store, setStore, sessions, setSessionTodo)
|
||||
})
|
||||
sessionMeta.set(directory, { limit })
|
||||
})
|
||||
.catch((err) => {
|
||||
|
||||
@@ -19,7 +19,6 @@ import type { State, VcsCache } from "./types"
|
||||
import { cmp, normalizeAgentList, normalizeProviderList } from "./utils"
|
||||
import { formatServerError } from "@/utils/server-errors"
|
||||
import { QueryClient, queryOptions, skipToken } from "@tanstack/solid-query"
|
||||
import { loadSessionsQuery } from "../global-sync"
|
||||
|
||||
type GlobalStore = {
|
||||
ready: boolean
|
||||
@@ -82,6 +81,9 @@ export async function bootstrapGlobal(input: {
|
||||
input.setGlobalStore("config", x.data!)
|
||||
}),
|
||||
),
|
||||
]
|
||||
|
||||
const slow = [
|
||||
() =>
|
||||
input.queryClient.fetchQuery({
|
||||
...loadProvidersQuery(null),
|
||||
@@ -93,9 +95,6 @@ export async function bootstrapGlobal(input: {
|
||||
}),
|
||||
),
|
||||
}),
|
||||
]
|
||||
|
||||
const slow = [
|
||||
() =>
|
||||
retry(() =>
|
||||
input.globalSDK.path.get().then((x) => {
|
||||
@@ -183,8 +182,43 @@ function warmSessions(input: {
|
||||
export const loadProvidersQuery = (directory: string | null) =>
|
||||
queryOptions<null>({ queryKey: [directory, "providers"], queryFn: skipToken })
|
||||
|
||||
export const loadAgentsQuery = (directory: string | null) =>
|
||||
queryOptions<null>({ queryKey: [directory, "agents"], queryFn: skipToken })
|
||||
export const loadAgentsQuery = (
|
||||
directory: string | null,
|
||||
sdk?: OpencodeClient,
|
||||
transform?: (x: Awaited<ReturnType<OpencodeClient["app"]["agents"]>>) => void,
|
||||
) =>
|
||||
queryOptions<null>({
|
||||
queryKey: [directory, "agents"],
|
||||
queryFn:
|
||||
sdk && transform
|
||||
? () =>
|
||||
retry(() =>
|
||||
sdk.app
|
||||
.agents()
|
||||
.then(transform)
|
||||
.then(() => null),
|
||||
)
|
||||
: skipToken,
|
||||
})
|
||||
|
||||
export const loadPathQuery = (
|
||||
directory: string | null,
|
||||
sdk?: OpencodeClient,
|
||||
transform?: (x: Awaited<ReturnType<OpencodeClient["path"]["get"]>>) => void,
|
||||
) =>
|
||||
queryOptions<Path>({
|
||||
queryKey: [directory, "path"],
|
||||
queryFn:
|
||||
sdk && transform
|
||||
? () =>
|
||||
retry(() =>
|
||||
sdk.path.get().then(async (x) => {
|
||||
transform(x)
|
||||
return x.data!
|
||||
}),
|
||||
)
|
||||
: skipToken,
|
||||
})
|
||||
|
||||
export async function bootstrapDirectory(input: {
|
||||
directory: string
|
||||
@@ -222,45 +256,27 @@ export async function bootstrapDirectory(input: {
|
||||
input.setStore("lsp", [])
|
||||
if (loading) input.setStore("status", "partial")
|
||||
|
||||
const fast = [() => Promise.resolve(input.loadSessions(input.directory))]
|
||||
|
||||
const errs = errors(await runAll(fast))
|
||||
if (errs.length > 0) {
|
||||
console.error("Failed to bootstrap instance", errs[0])
|
||||
const project = getFilename(input.directory)
|
||||
showToast({
|
||||
variant: "error",
|
||||
title: input.translate("toast.project.reloadFailed.title", { project }),
|
||||
description: formatServerError(errs[0], input.translate),
|
||||
})
|
||||
}
|
||||
|
||||
const rev = (providerRev.get(input.directory) ?? 0) + 1
|
||||
providerRev.set(input.directory, rev)
|
||||
;(async () => {
|
||||
const slow = [
|
||||
() => Promise.resolve(input.loadSessions(input.directory)),
|
||||
() =>
|
||||
input.queryClient.ensureQueryData({
|
||||
...loadAgentsQuery(input.directory),
|
||||
queryFn: () =>
|
||||
retry(() => input.sdk.app.agents().then((x) => input.setStore("agent", normalizeAgentList(x.data)))).then(
|
||||
() => null,
|
||||
),
|
||||
}),
|
||||
input.queryClient.ensureQueryData(
|
||||
loadAgentsQuery(input.directory, input.sdk, (x) => input.setStore("agent", normalizeAgentList(x.data))),
|
||||
),
|
||||
() => retry(() => input.sdk.config.get().then((x) => input.setStore("config", x.data!))),
|
||||
() => retry(() => input.sdk.session.status().then((x) => input.setStore("session_status", x.data!))),
|
||||
() =>
|
||||
seededProject
|
||||
? Promise.resolve()
|
||||
: retry(() => input.sdk.project.current()).then((x) => input.setStore("project", x.data!.id)),
|
||||
() =>
|
||||
seededPath
|
||||
? Promise.resolve()
|
||||
: retry(() =>
|
||||
input.sdk.path.get().then((x) => {
|
||||
input.setStore("path", x.data!)
|
||||
const next = projectID(x.data?.directory ?? input.directory, input.global.project)
|
||||
if (next) input.setStore("project", next)
|
||||
}),
|
||||
),
|
||||
!seededProject &&
|
||||
(() => retry(() => input.sdk.project.current()).then((x) => input.setStore("project", x.data!.id))),
|
||||
!seededPath &&
|
||||
(() =>
|
||||
input.queryClient.ensureQueryData(
|
||||
loadPathQuery(input.directory, input.sdk, (x) => {
|
||||
const next = projectID(x.data?.directory ?? input.directory, input.global.project)
|
||||
if (next) input.setStore("project", next)
|
||||
}),
|
||||
)),
|
||||
() =>
|
||||
retry(() =>
|
||||
input.sdk.vcs.get().then((x) => {
|
||||
@@ -330,7 +346,28 @@ export async function bootstrapDirectory(input: {
|
||||
input.setStore("mcp_ready", true)
|
||||
}),
|
||||
),
|
||||
]
|
||||
() =>
|
||||
input.queryClient.ensureQueryData({
|
||||
...loadProvidersQuery(input.directory),
|
||||
queryFn: () =>
|
||||
retry(() => input.sdk.provider.list())
|
||||
.then((x) => {
|
||||
if (providerRev.get(input.directory) !== rev) return
|
||||
input.setStore("provider", normalizeProviderList(x.data!))
|
||||
input.setStore("provider_ready", true)
|
||||
})
|
||||
.catch((err) => {
|
||||
if (providerRev.get(input.directory) !== rev) console.error("Failed to refresh provider list", err)
|
||||
const project = getFilename(input.directory)
|
||||
showToast({
|
||||
variant: "error",
|
||||
title: input.translate("toast.project.reloadFailed.title", { project }),
|
||||
description: formatServerError(err, input.translate),
|
||||
})
|
||||
})
|
||||
.then(() => null),
|
||||
}),
|
||||
].filter(Boolean) as (() => Promise<any>)[]
|
||||
|
||||
await waitForPaint()
|
||||
const slowErrs = errors(await runAll(slow))
|
||||
@@ -344,29 +381,6 @@ export async function bootstrapDirectory(input: {
|
||||
})
|
||||
}
|
||||
|
||||
if (loading && errs.length === 0 && slowErrs.length === 0) input.setStore("status", "complete")
|
||||
|
||||
const rev = (providerRev.get(input.directory) ?? 0) + 1
|
||||
providerRev.set(input.directory, rev)
|
||||
void input.queryClient.ensureQueryData({
|
||||
...loadSessionsQuery(input.directory),
|
||||
queryFn: () =>
|
||||
retry(() => input.sdk.provider.list())
|
||||
.then((x) => {
|
||||
if (providerRev.get(input.directory) !== rev) return
|
||||
input.setStore("provider", normalizeProviderList(x.data!))
|
||||
input.setStore("provider_ready", true)
|
||||
})
|
||||
.catch((err) => {
|
||||
if (providerRev.get(input.directory) !== rev) console.error("Failed to refresh provider list", err)
|
||||
const project = getFilename(input.directory)
|
||||
showToast({
|
||||
variant: "error",
|
||||
title: input.translate("toast.project.reloadFailed.title", { project }),
|
||||
description: formatServerError(err, input.translate),
|
||||
})
|
||||
})
|
||||
.then(() => null),
|
||||
})
|
||||
if (loading && slowErrs.length === 0) input.setStore("status", "complete")
|
||||
})()
|
||||
}
|
||||
|
||||
@@ -14,6 +14,8 @@ import {
|
||||
type VcsCache,
|
||||
} from "./types"
|
||||
import { canDisposeDirectory, pickDirectoriesToEvict } from "./eviction"
|
||||
import { useQuery } from "@tanstack/solid-query"
|
||||
import { loadPathQuery } from "./bootstrap"
|
||||
|
||||
export function createChildStoreManager(input: {
|
||||
owner: Owner
|
||||
@@ -156,6 +158,8 @@ export function createChildStoreManager(input: {
|
||||
createRoot((dispose) => {
|
||||
const initialMeta = meta[0].value
|
||||
const initialIcon = icon[0].value
|
||||
|
||||
const pathQuery = useQuery(() => loadPathQuery(directory))
|
||||
const child = createStore<State>({
|
||||
project: "",
|
||||
projectMeta: initialMeta,
|
||||
@@ -163,7 +167,11 @@ export function createChildStoreManager(input: {
|
||||
provider_ready: false,
|
||||
provider: { all: [], connected: [], default: {} },
|
||||
config: {},
|
||||
path: { state: "", config: "", worktree: "", directory: "", home: "" },
|
||||
get path() {
|
||||
if (pathQuery.isLoading || !pathQuery.data)
|
||||
return { state: "", config: "", worktree: "", directory: "", home: "" }
|
||||
return pathQuery.data
|
||||
},
|
||||
status: "loading" as const,
|
||||
agent: [],
|
||||
command: [],
|
||||
|
||||
@@ -185,9 +185,9 @@ function createPromptSession(dir: string, id: string | undefined) {
|
||||
|
||||
return {
|
||||
ready,
|
||||
current: createMemo(() => store.prompt),
|
||||
current: () => store.prompt,
|
||||
cursor: createMemo(() => store.cursor),
|
||||
dirty: createMemo(() => !isPromptEqual(store.prompt, DEFAULT_PROMPT)),
|
||||
dirty: () => !isPromptEqual(store.prompt, DEFAULT_PROMPT),
|
||||
context: {
|
||||
items: createMemo(() => store.context.items),
|
||||
add(item: ContextItem) {
|
||||
@@ -277,7 +277,7 @@ export const { use: usePrompt, provider: PromptProvider } = createSimpleContext(
|
||||
const pick = (scope?: Scope) => (scope ? load(scope.dir, scope.id) : session())
|
||||
|
||||
return {
|
||||
ready: () => session().ready(),
|
||||
ready: () => session().ready,
|
||||
current: () => session().current(),
|
||||
cursor: () => session().cursor(),
|
||||
dirty: () => session().dirty(),
|
||||
|
||||
@@ -39,6 +39,7 @@ export interface Settings {
|
||||
fontSize: number
|
||||
mono: string
|
||||
sans: string
|
||||
terminal: string
|
||||
}
|
||||
keybinds: Record<string, string>
|
||||
permissions: {
|
||||
@@ -50,13 +51,17 @@ export interface Settings {
|
||||
|
||||
export const monoDefault = "System Mono"
|
||||
export const sansDefault = "System Sans"
|
||||
export const terminalDefault = "JetBrainsMono Nerd Font Mono"
|
||||
|
||||
const monoFallback =
|
||||
'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 terminalFallback =
|
||||
'"JetBrainsMono Nerd Font Mono", ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace'
|
||||
|
||||
const monoBase = monoFallback
|
||||
const sansBase = sansFallback
|
||||
const terminalBase = terminalFallback
|
||||
|
||||
function input(font: string | undefined) {
|
||||
return font ?? ""
|
||||
@@ -89,6 +94,14 @@ export function sansFontFamily(font: string | undefined) {
|
||||
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 = {
|
||||
general: {
|
||||
autoSave: true,
|
||||
@@ -110,6 +123,7 @@ const defaultSettings: Settings = {
|
||||
fontSize: 14,
|
||||
mono: "",
|
||||
sans: "",
|
||||
terminal: "",
|
||||
},
|
||||
keybinds: {},
|
||||
permissions: {
|
||||
@@ -233,6 +247,10 @@ export const { use: useSettings, provider: SettingsProvider } = createSimpleCont
|
||||
setUIFont(value: string) {
|
||||
setStore("appearance", "sans", value.trim() ? value : "")
|
||||
},
|
||||
terminalFont: withFallback(() => store.appearance?.terminal, defaultSettings.appearance.terminal),
|
||||
setTerminalFont(value: string) {
|
||||
setStore("appearance", "terminal", value.trim() ? value : "")
|
||||
},
|
||||
},
|
||||
keybinds: {
|
||||
get: (action: string) => store.keybinds?.[action],
|
||||
|
||||
@@ -565,7 +565,9 @@ export const dict = {
|
||||
"settings.general.row.theme.title": "السمة",
|
||||
"settings.general.row.theme.description": "تخصيص سمة OpenCode.",
|
||||
"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.description": "خصّص الخط المستخدم في الواجهة بأكملها",
|
||||
"settings.general.row.followup.title": "سلوك المتابعة",
|
||||
|
||||
@@ -572,7 +572,9 @@ export const dict = {
|
||||
"settings.general.row.theme.title": "Tema",
|
||||
"settings.general.row.theme.description": "Personalize como o OpenCode é tematizado.",
|
||||
"settings.general.row.font.title": "Fonte de código",
|
||||
"settings.general.row.font.description": "Personalize a fonte usada em blocos de código e terminais",
|
||||
"settings.general.row.font.description": "Personalize a fonte usada em blocos de código",
|
||||
"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.description": "Personalize a fonte usada em toda a interface",
|
||||
"settings.general.row.followup.title": "Comportamento de acompanhamento",
|
||||
|
||||
@@ -637,7 +637,9 @@ export const dict = {
|
||||
"settings.general.row.theme.title": "Tema",
|
||||
"settings.general.row.theme.description": "Prilagodi temu OpenCode-a.",
|
||||
"settings.general.row.font.title": "Font za kod",
|
||||
"settings.general.row.font.description": "Prilagodi font koji se koristi u blokovima koda i terminalima",
|
||||
"settings.general.row.font.description": "Prilagodi font koji se koristi u blokovima koda",
|
||||
"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.description": "Prilagodi font koji se koristi u cijelom interfejsu",
|
||||
"settings.general.row.followup.title": "Ponašanje nadovezivanja",
|
||||
|
||||
@@ -632,7 +632,9 @@ export const dict = {
|
||||
"settings.general.row.theme.title": "Tema",
|
||||
"settings.general.row.theme.description": "Tilpas hvordan OpenCode er temabestemt.",
|
||||
"settings.general.row.font.title": "Kode-skrifttype",
|
||||
"settings.general.row.font.description": "Tilpas skrifttypen, der bruges i kodeblokke og terminaler",
|
||||
"settings.general.row.font.description": "Tilpas skrifttypen, der bruges i kodeblokke",
|
||||
"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.description": "Tilpas skrifttypen, der bruges i hele brugerfladen",
|
||||
"settings.general.row.followup.title": "Opfølgningsadfærd",
|
||||
|
||||
@@ -582,7 +582,9 @@ export const dict = {
|
||||
"settings.general.row.theme.title": "Thema",
|
||||
"settings.general.row.theme.description": "Das Thema von OpenCode anpassen.",
|
||||
"settings.general.row.font.title": "Code-Schriftart",
|
||||
"settings.general.row.font.description": "Die in Codeblöcken und Terminals verwendete Schriftart anpassen",
|
||||
"settings.general.row.font.description": "Die in Codeblöcken 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.description": "Die im gesamten Interface verwendete Schriftart anpassen",
|
||||
"settings.general.row.followup.title": "Verhalten bei Folgefragen",
|
||||
|
||||
@@ -735,7 +735,9 @@ export const dict = {
|
||||
"settings.general.row.theme.title": "Theme",
|
||||
"settings.general.row.theme.description": "Customise how OpenCode is themed.",
|
||||
"settings.general.row.font.title": "Code Font",
|
||||
"settings.general.row.font.description": "Customise the font used in code blocks and terminals",
|
||||
"settings.general.row.font.description": "Customise the font used in code blocks",
|
||||
"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.description": "Customise the font used throughout the interface",
|
||||
"settings.general.row.followup.title": "Follow-up behavior",
|
||||
|
||||
@@ -640,7 +640,9 @@ export const dict = {
|
||||
"settings.general.row.theme.title": "Tema",
|
||||
"settings.general.row.theme.description": "Personaliza el tema de OpenCode.",
|
||||
"settings.general.row.font.title": "Fuente de código",
|
||||
"settings.general.row.font.description": "Personaliza la fuente usada en bloques de código y terminales",
|
||||
"settings.general.row.font.description": "Personaliza la fuente usada en bloques de código",
|
||||
"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.description": "Personaliza la fuente usada en toda la interfaz",
|
||||
"settings.general.row.followup.title": "Comportamiento de seguimiento",
|
||||
|
||||
@@ -579,7 +579,9 @@ export const dict = {
|
||||
"settings.general.row.theme.title": "Thème",
|
||||
"settings.general.row.theme.description": "Personnaliser le thème d'OpenCode.",
|
||||
"settings.general.row.font.title": "Police de code",
|
||||
"settings.general.row.font.description": "Personnaliser la police utilisée dans les blocs de code et les terminaux",
|
||||
"settings.general.row.font.description": "Personnaliser la police utilisée dans les blocs de code",
|
||||
"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.description": "Personnaliser la police utilisée dans toute l'interface",
|
||||
"settings.general.row.followup.title": "Comportement de suivi",
|
||||
|
||||
@@ -569,7 +569,9 @@ export const dict = {
|
||||
"settings.general.row.theme.title": "テーマ",
|
||||
"settings.general.row.theme.description": "OpenCodeのテーマをカスタマイズします。",
|
||||
"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.description": "インターフェース全体で使用するフォントをカスタマイズします",
|
||||
"settings.general.row.followup.title": "フォローアップの動作",
|
||||
|
||||
@@ -566,7 +566,9 @@ export const dict = {
|
||||
"settings.general.row.theme.title": "테마",
|
||||
"settings.general.row.theme.description": "OpenCode 테마 사용자 지정",
|
||||
"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.description": "인터페이스 전반에 사용되는 글꼴을 사용자 지정",
|
||||
"settings.general.row.followup.title": "후속 조치 동작",
|
||||
|
||||
@@ -640,7 +640,9 @@ export const dict = {
|
||||
"settings.general.row.theme.title": "Tema",
|
||||
"settings.general.row.theme.description": "Tilpass hvordan OpenCode er tematisert.",
|
||||
"settings.general.row.font.title": "Kodefont",
|
||||
"settings.general.row.font.description": "Tilpass skrifttypen som brukes i kodeblokker og terminaler",
|
||||
"settings.general.row.font.description": "Tilpass skrifttypen som brukes i kodeblokker",
|
||||
"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.description": "Tilpass skrifttypen som brukes i hele grensesnittet",
|
||||
"settings.general.row.followup.title": "Oppfølgingsadferd",
|
||||
|
||||
@@ -571,7 +571,9 @@ export const dict = {
|
||||
"settings.general.row.theme.title": "Motyw",
|
||||
"settings.general.row.theme.description": "Dostosuj motyw OpenCode.",
|
||||
"settings.general.row.font.title": "Czcionka kodu",
|
||||
"settings.general.row.font.description": "Dostosuj czcionkę używaną w blokach kodu i terminalach",
|
||||
"settings.general.row.font.description": "Dostosuj czcionkę używaną w blokach kodu",
|
||||
"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.description": "Dostosuj czcionkę używaną w całym interfejsie",
|
||||
"settings.general.row.followup.title": "Zachowanie kontynuacji",
|
||||
|
||||
@@ -637,7 +637,9 @@ export const dict = {
|
||||
"settings.general.row.theme.title": "Тема",
|
||||
"settings.general.row.theme.description": "Настройте оформление OpenCode.",
|
||||
"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.description": "Настройте шрифт, используемый во всем интерфейсе",
|
||||
"settings.general.row.followup.title": "Поведение уточняющих вопросов",
|
||||
|
||||
@@ -631,7 +631,9 @@ export const dict = {
|
||||
"settings.general.row.theme.title": "ธีม",
|
||||
"settings.general.row.theme.description": "ปรับแต่งวิธีการที่ OpenCode มีธีม",
|
||||
"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.description": "ปรับแต่งฟอนต์ที่ใช้ทั่วทั้งอินเทอร์เฟซ",
|
||||
"settings.general.row.followup.title": "พฤติกรรมการติดตามผล",
|
||||
|
||||
@@ -644,7 +644,9 @@ export const dict = {
|
||||
"settings.general.row.theme.title": "Tema",
|
||||
"settings.general.row.theme.description": "OpenCode'un temasını özelleştirin.",
|
||||
"settings.general.row.font.title": "Kod Yazı Tipi",
|
||||
"settings.general.row.font.description": "Kod bloklarında ve terminallerde kullanılan yazı tipini özelleştirin",
|
||||
"settings.general.row.font.description": "Kod bloklarında 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.description": "Arayüz genelinde kullanılan yazı tipini özelleştirin",
|
||||
"settings.general.row.followup.title": "Takip davranışı",
|
||||
|
||||
@@ -631,7 +631,9 @@ export const dict = {
|
||||
"settings.general.row.theme.title": "主题",
|
||||
"settings.general.row.theme.description": "自定义 OpenCode 的主题。",
|
||||
"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.description": "自定义整个界面使用的字体",
|
||||
"settings.general.row.followup.title": "跟进消息行为",
|
||||
|
||||
@@ -626,7 +626,9 @@ export const dict = {
|
||||
"settings.general.row.theme.title": "主題",
|
||||
"settings.general.row.theme.description": "自訂 OpenCode 的主題。",
|
||||
"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.description": "自訂整個介面使用的字型",
|
||||
"settings.general.row.followup.title": "後續追問行為",
|
||||
|
||||
@@ -1,5 +1,12 @@
|
||||
@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 {
|
||||
@keyframes session-progress-whip {
|
||||
0% {
|
||||
@@ -66,4 +73,13 @@
|
||||
width: auto;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes fade-in {
|
||||
from {
|
||||
opacity: 0;
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@ import { DataProvider } from "@opencode-ai/ui/context"
|
||||
import { showToast } from "@opencode-ai/ui/toast"
|
||||
import { base64Encode } from "@opencode-ai/shared/util/encode"
|
||||
import { useLocation, useNavigate, useParams } from "@solidjs/router"
|
||||
import { createEffect, createMemo, type ParentProps, Show } from "solid-js"
|
||||
import { createEffect, createMemo, createResource, type ParentProps, Show } from "solid-js"
|
||||
import { useLanguage } from "@/context/language"
|
||||
import { LocalProvider } from "@/context/local"
|
||||
import { SDKProvider } from "@/context/sdk"
|
||||
@@ -23,11 +23,10 @@ function DirectoryDataProvider(props: ParentProps<{ directory: string }>) {
|
||||
navigate(`/${base64Encode(next)}${path}${location.search}${location.hash}`, { replace: true })
|
||||
})
|
||||
|
||||
createEffect(() => {
|
||||
const id = params.id
|
||||
if (!id) return
|
||||
void sync.session.sync(id)
|
||||
})
|
||||
createResource(
|
||||
() => params.id,
|
||||
(id) => sync.session.sync(id),
|
||||
)
|
||||
|
||||
return (
|
||||
<DataProvider
|
||||
|
||||
@@ -31,7 +31,7 @@ function sortSessions(now: number) {
|
||||
const isRootVisibleSession = (session: Session, directory: string) =>
|
||||
workspaceKey(session.directory) === workspaceKey(directory) && !session.parentID && !session.time?.archived
|
||||
|
||||
const roots = (store: SessionStore) =>
|
||||
export const roots = (store: SessionStore) =>
|
||||
(store.session ?? []).filter((session) => isRootVisibleSession(session, store.path.directory))
|
||||
|
||||
export const sortedRootSessions = (store: SessionStore, now: number) => roots(store).sort(sortSessions(now))
|
||||
|
||||
@@ -321,7 +321,7 @@ export const SortableWorkspace = (props: {
|
||||
const hasMore = createMemo(() => workspaceStore.sessionTotal > count())
|
||||
const query = useQuery(() => ({ ...loadSessionsQuery(props.project.worktree) }))
|
||||
const busy = createMemo(() => props.ctx.isBusy(props.directory))
|
||||
const loading = () => query.isLoading
|
||||
const loading = () => query.isLoading && count() === 0
|
||||
const touch = createMediaQuery("(hover: none)")
|
||||
const showNew = createMemo(() => !loading() && (touch() || count() === 0 || (active() && !params.id)))
|
||||
const loadMore = async () => {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@opencode-ai/console-app",
|
||||
"version": "1.4.11",
|
||||
"version": "1.14.19",
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
"scripts": {
|
||||
|
||||
@@ -11,7 +11,7 @@ export const dict = {
|
||||
"nav.enterprise": "المؤسسات",
|
||||
"nav.zen": "Zen",
|
||||
"nav.login": "تسجيل الدخول",
|
||||
"nav.free": "مجانا",
|
||||
"nav.free": "تحميل",
|
||||
"nav.home": "الرئيسية",
|
||||
"nav.openMenu": "فتح القائمة",
|
||||
"nav.getStartedFree": "ابدأ مجانا",
|
||||
@@ -558,6 +558,13 @@ export const dict = {
|
||||
"workspace.monthlyLimit.currentUsage.beforeMonth": "الاستخدام الحالي لـ",
|
||||
"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.disabled.before": "إعادة الشحن التلقائي",
|
||||
"workspace.reload.disabled.state": "معطّل",
|
||||
|
||||
@@ -11,7 +11,7 @@ export const dict = {
|
||||
"nav.enterprise": "Enterprise",
|
||||
"nav.zen": "Zen",
|
||||
"nav.login": "Entrar",
|
||||
"nav.free": "Grátis",
|
||||
"nav.free": "Download",
|
||||
"nav.home": "Início",
|
||||
"nav.openMenu": "Abrir menu",
|
||||
"nav.getStartedFree": "Começar grátis",
|
||||
@@ -567,6 +567,13 @@ export const dict = {
|
||||
"workspace.monthlyLimit.currentUsage.beforeMonth": "Uso atual para",
|
||||
"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.disabled.before": "A recarga automática está",
|
||||
"workspace.reload.disabled.state": "desativada",
|
||||
|
||||
@@ -11,7 +11,7 @@ export const dict = {
|
||||
"nav.enterprise": "Enterprise",
|
||||
"nav.zen": "Zen",
|
||||
"nav.login": "Log ind",
|
||||
"nav.free": "Gratis",
|
||||
"nav.free": "Download",
|
||||
"nav.home": "Hjem",
|
||||
"nav.openMenu": "Åbn menu",
|
||||
"nav.getStartedFree": "Kom i gang gratis",
|
||||
@@ -563,6 +563,13 @@ export const dict = {
|
||||
"workspace.monthlyLimit.currentUsage.beforeMonth": "Nuværende brug for",
|
||||
"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.disabled.before": "Automatisk genopfyldning er",
|
||||
"workspace.reload.disabled.state": "deaktiveret",
|
||||
|
||||
@@ -11,7 +11,7 @@ export const dict = {
|
||||
"nav.enterprise": "Enterprise",
|
||||
"nav.zen": "Zen",
|
||||
"nav.login": "Anmelden",
|
||||
"nav.free": "Kostenlos",
|
||||
"nav.free": "Download",
|
||||
"nav.home": "Startseite",
|
||||
"nav.openMenu": "Menü öffnen",
|
||||
"nav.getStartedFree": "Kostenlos starten",
|
||||
@@ -566,6 +566,13 @@ export const dict = {
|
||||
"workspace.monthlyLimit.currentUsage.beforeMonth": "Aktuelle Nutzung für",
|
||||
"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.disabled.before": "Auto-Reload ist",
|
||||
"workspace.reload.disabled.state": "deaktiviert",
|
||||
|
||||
@@ -8,7 +8,7 @@ export const dict = {
|
||||
"nav.zen": "Zen",
|
||||
"nav.go": "Go",
|
||||
"nav.login": "Login",
|
||||
"nav.free": "Free",
|
||||
"nav.free": "Download",
|
||||
"nav.home": "Home",
|
||||
"nav.openMenu": "Open menu",
|
||||
"nav.getStartedFree": "Get started for free",
|
||||
@@ -559,6 +559,13 @@ export const dict = {
|
||||
"workspace.monthlyLimit.currentUsage.beforeMonth": "Current usage for",
|
||||
"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.disabled.before": "Auto reload is",
|
||||
"workspace.reload.disabled.state": "disabled",
|
||||
|
||||
@@ -11,7 +11,7 @@ export const dict = {
|
||||
"nav.enterprise": "Enterprise",
|
||||
"nav.zen": "Zen",
|
||||
"nav.login": "Iniciar sesión",
|
||||
"nav.free": "Gratis",
|
||||
"nav.free": "Descargar",
|
||||
"nav.home": "Inicio",
|
||||
"nav.openMenu": "Abrir menú",
|
||||
"nav.getStartedFree": "Empezar gratis",
|
||||
@@ -567,6 +567,13 @@ export const dict = {
|
||||
"workspace.monthlyLimit.currentUsage.beforeMonth": "Uso actual para",
|
||||
"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.disabled.before": "La auto recarga está",
|
||||
"workspace.reload.disabled.state": "deshabilitada",
|
||||
|
||||
@@ -12,7 +12,7 @@ export const dict = {
|
||||
"nav.enterprise": "Entreprise",
|
||||
"nav.zen": "Zen",
|
||||
"nav.login": "Se connecter",
|
||||
"nav.free": "Gratuit",
|
||||
"nav.free": "Télécharger",
|
||||
"nav.home": "Accueil",
|
||||
"nav.openMenu": "Ouvrir le menu",
|
||||
"nav.getStartedFree": "Commencer gratuitement",
|
||||
@@ -569,6 +569,13 @@ export const dict = {
|
||||
"workspace.monthlyLimit.currentUsage.beforeMonth": "L'utilisation actuelle pour",
|
||||
"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.disabled.before": "Le rechargement automatique est",
|
||||
"workspace.reload.disabled.state": "désactivé",
|
||||
|
||||
@@ -11,7 +11,7 @@ export const dict = {
|
||||
"nav.enterprise": "Enterprise",
|
||||
"nav.zen": "Zen",
|
||||
"nav.login": "Accedi",
|
||||
"nav.free": "Gratis",
|
||||
"nav.free": "Scarica",
|
||||
"nav.home": "Home",
|
||||
"nav.openMenu": "Apri menu",
|
||||
"nav.getStartedFree": "Inizia gratis",
|
||||
@@ -565,6 +565,13 @@ export const dict = {
|
||||
"workspace.monthlyLimit.currentUsage.beforeMonth": "Utilizzo attuale per",
|
||||
"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.disabled.before": "La ricarica auto è",
|
||||
"workspace.reload.disabled.state": "disabilitata",
|
||||
|
||||
@@ -11,7 +11,7 @@ export const dict = {
|
||||
"nav.enterprise": "エンタープライズ",
|
||||
"nav.zen": "Zen",
|
||||
"nav.login": "ログイン",
|
||||
"nav.free": "無料",
|
||||
"nav.free": "ダウンロード",
|
||||
"nav.home": "ホーム",
|
||||
"nav.openMenu": "メニューを開く",
|
||||
"nav.getStartedFree": "無料ではじめる",
|
||||
@@ -564,6 +564,13 @@ export const dict = {
|
||||
"workspace.monthlyLimit.currentUsage.beforeMonth": "現在の使用状況(",
|
||||
"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.disabled.before": "自動チャージは",
|
||||
"workspace.reload.disabled.state": "無効",
|
||||
|
||||
@@ -11,7 +11,7 @@ export const dict = {
|
||||
"nav.enterprise": "엔터프라이즈",
|
||||
"nav.zen": "Zen",
|
||||
"nav.login": "로그인",
|
||||
"nav.free": "무료",
|
||||
"nav.free": "다운로드",
|
||||
"nav.home": "홈",
|
||||
"nav.openMenu": "메뉴 열기",
|
||||
"nav.getStartedFree": "무료로 시작하기",
|
||||
@@ -558,6 +558,13 @@ export const dict = {
|
||||
"workspace.monthlyLimit.currentUsage.beforeMonth": "현재",
|
||||
"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.disabled.before": "자동 충전이",
|
||||
"workspace.reload.disabled.state": "비활성화",
|
||||
|
||||
@@ -11,7 +11,7 @@ export const dict = {
|
||||
"nav.enterprise": "Enterprise",
|
||||
"nav.zen": "Zen",
|
||||
"nav.login": "Logg inn",
|
||||
"nav.free": "Gratis",
|
||||
"nav.free": "Last ned",
|
||||
"nav.home": "Hjem",
|
||||
"nav.openMenu": "Åpne meny",
|
||||
"nav.getStartedFree": "Kom i gang gratis",
|
||||
@@ -564,6 +564,13 @@ export const dict = {
|
||||
"workspace.monthlyLimit.currentUsage.beforeMonth": "Gjeldende forbruk for",
|
||||
"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.disabled.before": "Auto-påfyll er",
|
||||
"workspace.reload.disabled.state": "deaktivert",
|
||||
|
||||
@@ -10,7 +10,7 @@ export const dict = {
|
||||
"nav.enterprise": "Enterprise",
|
||||
"nav.zen": "Zen",
|
||||
"nav.login": "Zaloguj się",
|
||||
"nav.free": "Darmowe",
|
||||
"nav.free": "Pobierz",
|
||||
"nav.home": "Strona główna",
|
||||
"nav.openMenu": "Otwórz menu",
|
||||
"nav.getStartedFree": "Zacznij za darmo",
|
||||
@@ -565,6 +565,13 @@ export const dict = {
|
||||
"workspace.monthlyLimit.currentUsage.beforeMonth": "Aktualne użycie za",
|
||||
"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.disabled.before": "Automatyczne doładowanie jest",
|
||||
"workspace.reload.disabled.state": "wyłączone",
|
||||
|
||||
@@ -11,7 +11,7 @@ export const dict = {
|
||||
"nav.enterprise": "Enterprise",
|
||||
"nav.zen": "Zen",
|
||||
"nav.login": "Войти",
|
||||
"nav.free": "Бесплатно",
|
||||
"nav.free": "Скачать",
|
||||
"nav.home": "Главная",
|
||||
"nav.openMenu": "Открыть меню",
|
||||
"nav.getStartedFree": "Начать бесплатно",
|
||||
@@ -571,6 +571,13 @@ export const dict = {
|
||||
"workspace.monthlyLimit.currentUsage.beforeMonth": "Текущее использование за",
|
||||
"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.disabled.before": "Автопополнение",
|
||||
"workspace.reload.disabled.state": "отключено",
|
||||
|
||||
@@ -11,7 +11,7 @@ export const dict = {
|
||||
"nav.enterprise": "องค์กร",
|
||||
"nav.zen": "Zen",
|
||||
"nav.login": "เข้าสู่ระบบ",
|
||||
"nav.free": "ฟรี",
|
||||
"nav.free": "ดาวน์โหลด",
|
||||
"nav.home": "หน้าหลัก",
|
||||
"nav.openMenu": "เปิดเมนู",
|
||||
"nav.getStartedFree": "เริ่มต้นฟรี",
|
||||
@@ -560,6 +560,13 @@ export const dict = {
|
||||
"workspace.monthlyLimit.currentUsage.beforeMonth": "การใช้งานปัจจุบันสำหรับ",
|
||||
"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.disabled.before": "การโหลดซ้ำอัตโนมัติ",
|
||||
"workspace.reload.disabled.state": "ปิดใช้งานอยู่",
|
||||
|
||||
@@ -11,7 +11,7 @@ export const dict = {
|
||||
"nav.enterprise": "Kurumsal",
|
||||
"nav.zen": "Zen",
|
||||
"nav.login": "Giriş",
|
||||
"nav.free": "Ücretsiz",
|
||||
"nav.free": "İndir",
|
||||
"nav.home": "Ana sayfa",
|
||||
"nav.openMenu": "Menüyü aç",
|
||||
"nav.getStartedFree": "Ücretsiz başla",
|
||||
@@ -567,6 +567,13 @@ export const dict = {
|
||||
"workspace.monthlyLimit.currentUsage.beforeMonth": "Şu anki kullanım",
|
||||
"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.disabled.before": "Otomatik yeniden yükleme:",
|
||||
"workspace.reload.disabled.state": "devre dışı",
|
||||
|
||||
@@ -11,7 +11,7 @@ export const dict = {
|
||||
"nav.enterprise": "企业版",
|
||||
"nav.zen": "Zen",
|
||||
"nav.login": "登录",
|
||||
"nav.free": "免费",
|
||||
"nav.free": "下载",
|
||||
"nav.home": "首页",
|
||||
"nav.openMenu": "打开菜单",
|
||||
"nav.getStartedFree": "免费开始",
|
||||
@@ -542,6 +542,13 @@ export const dict = {
|
||||
"workspace.monthlyLimit.currentUsage.beforeMonth": "当前",
|
||||
"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.disabled.before": "自动充值已",
|
||||
"workspace.reload.disabled.state": "禁用",
|
||||
|
||||
@@ -11,7 +11,7 @@ export const dict = {
|
||||
"nav.enterprise": "企業",
|
||||
"nav.zen": "Zen",
|
||||
"nav.login": "登入",
|
||||
"nav.free": "免費",
|
||||
"nav.free": "下載",
|
||||
"nav.home": "首頁",
|
||||
"nav.openMenu": "開啟選單",
|
||||
"nav.getStartedFree": "免費開始使用",
|
||||
@@ -542,6 +542,13 @@ export const dict = {
|
||||
"workspace.monthlyLimit.currentUsage.beforeMonth": "目前",
|
||||
"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.disabled.before": "自動儲值已",
|
||||
"workspace.reload.disabled.state": "停用",
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import type { APIEvent } from "@solidjs/start/server"
|
||||
import { AWS } from "@opencode-ai/console-core/aws.js"
|
||||
import { Resource } from "@opencode-ai/console-resource"
|
||||
import { i18n } from "~/i18n"
|
||||
import { localeFromRequest } from "~/lib/language"
|
||||
import { createLead } from "~/lib/salesforce"
|
||||
@@ -14,6 +15,64 @@ interface EnterpriseFormData {
|
||||
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) {
|
||||
const dict = i18n(localeFromRequest(event.request))
|
||||
try {
|
||||
@@ -41,7 +100,7 @@ ${body.role}<br>
|
||||
${body.company ? `${body.company}<br>` : ""}${body.email}<br>
|
||||
${body.phone ? `${body.phone}<br>` : ""}`.trim()
|
||||
|
||||
const [lead, mail] = await Promise.all([
|
||||
const [lead, mail, octopus] = await Promise.all([
|
||||
createLead({
|
||||
name: body.name,
|
||||
role: body.role,
|
||||
@@ -49,6 +108,9 @@ ${body.phone ? `${body.phone}<br>` : ""}`.trim()
|
||||
email: body.email,
|
||||
phone: body.phone,
|
||||
message: body.message,
|
||||
}).catch((err) => {
|
||||
console.error("Failed to create Salesforce lead:", err)
|
||||
return false
|
||||
}),
|
||||
AWS.sendEmail({
|
||||
to: "contact@anoma.ly",
|
||||
@@ -62,9 +124,14 @@ ${body.phone ? `${body.phone}<br>` : ""}`.trim()
|
||||
return false
|
||||
},
|
||||
),
|
||||
subscribe(body.email, body.name),
|
||||
])
|
||||
|
||||
if (!lead && !mail) {
|
||||
if (!lead && !mail && !octopus) {
|
||||
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 })
|
||||
return Response.json({ error: dict["enterprise.form.error.internalServer"] }, { status: 500 })
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@ import { Actor } from "@opencode-ai/console-core/actor.js"
|
||||
import { Resource } from "@opencode-ai/console-resource"
|
||||
import { LiteData } from "@opencode-ai/console-core/lite.js"
|
||||
import { BlackData } from "@opencode-ai/console-core/black.js"
|
||||
import { User } from "@opencode-ai/console-core/user.js"
|
||||
|
||||
export async function POST(input: APIEvent) {
|
||||
const body = await Billing.stripe().webhooks.constructEventAsync(
|
||||
@@ -109,6 +110,8 @@ export async function POST(input: APIEvent) {
|
||||
if (type === "lite") {
|
||||
const workspaceID = body.data.object.metadata?.workspaceID
|
||||
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 invoiceID = body.data.object.latest_invoice as string
|
||||
const subscriptionID = body.data.object.id as string
|
||||
@@ -156,6 +159,10 @@ export async function POST(input: APIEvent) {
|
||||
id: Identifier.create("lite"),
|
||||
userID: userID,
|
||||
})
|
||||
|
||||
if (userEmail && coupon === LiteData.firstMonth100Coupon) {
|
||||
await Billing.redeemCoupon(userEmail, "GOFREEMONTH")
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ import { BillingSection } from "./billing-section"
|
||||
import { ReloadSection } from "./reload-section"
|
||||
import { PaymentSection } from "./payment-section"
|
||||
import { BlackSection } from "./black-section"
|
||||
import { RedeemSection } from "./redeem-section"
|
||||
import { createMemo, Show } from "solid-js"
|
||||
import { createAsync, useParams } from "@solidjs/router"
|
||||
import { queryBillingInfo, querySessionInfo } from "../../common"
|
||||
@@ -21,6 +22,7 @@ export default function () {
|
||||
<BlackSection />
|
||||
</Show>
|
||||
<BillingSection />
|
||||
<RedeemSection />
|
||||
<Show when={billingInfo()?.customerID}>
|
||||
<ReloadSection />
|
||||
<MonthlyLimitSection />
|
||||
|
||||
@@ -0,0 +1,61 @@
|
||||
.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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
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>
|
||||
)
|
||||
}
|
||||
@@ -762,7 +762,8 @@ export async function handler(
|
||||
const billing = authInfo.billing
|
||||
const billingUrl = `https://opencode.ai/workspace/${authInfo.workspaceID}/billing`
|
||||
const membersUrl = `https://opencode.ai/workspace/${authInfo.workspaceID}/members`
|
||||
if (!billing.paymentMethodID) throw new CreditsError(t("zen.api.error.noPaymentMethod", { billingUrl }))
|
||||
if (!billing.paymentMethodID && billing.balance <= 0)
|
||||
throw new CreditsError(t("zen.api.error.noPaymentMethod", { billingUrl }))
|
||||
if (billing.balance <= 0) throw new CreditsError(t("zen.api.error.insufficientBalance", { billingUrl }))
|
||||
|
||||
const now = new Date()
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
CREATE TABLE `coupon` (
|
||||
`email` varchar(255),
|
||||
`type` enum('BUILDATHON','GOFREEMONTH') NOT NULL,
|
||||
`time_redeemed` timestamp(3),
|
||||
CONSTRAINT PRIMARY KEY(`email`,`type`)
|
||||
);
|
||||
+2619
File diff suppressed because it is too large
Load Diff
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/package.json",
|
||||
"name": "@opencode-ai/console-core",
|
||||
"version": "1.4.11",
|
||||
"version": "1.14.19",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
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}`)
|
||||
@@ -1,6 +1,14 @@
|
||||
import { Stripe } from "stripe"
|
||||
import { Database, eq, sql } from "./drizzle"
|
||||
import { BillingTable, LiteTable, PaymentTable, SubscriptionTable, UsageTable } from "./schema/billing.sql"
|
||||
import { and, Database, eq, isNull, sql } from "./drizzle"
|
||||
import {
|
||||
BillingTable,
|
||||
CouponTable,
|
||||
CouponType,
|
||||
LiteTable,
|
||||
PaymentTable,
|
||||
SubscriptionTable,
|
||||
UsageTable,
|
||||
} from "./schema/billing.sql"
|
||||
import { Actor } from "./actor"
|
||||
import { fn } from "./util/fn"
|
||||
import { z } from "zod"
|
||||
@@ -147,6 +155,37 @@ export namespace Billing {
|
||||
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) => {
|
||||
return await Database.use((tx) =>
|
||||
tx
|
||||
@@ -245,16 +284,19 @@ export namespace Billing {
|
||||
const user = Actor.assert("user")
|
||||
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()
|
||||
|
||||
if (billing.subscriptionID) throw new Error("Already subscribed to Black")
|
||||
if (billing.liteSubscriptionID) throw new Error("Already subscribed to Lite")
|
||||
|
||||
const coupon = (await Billing.hasCoupon(email, "GOFREEMONTH"))
|
||||
? LiteData.firstMonth100Coupon
|
||||
: LiteData.firstMonth50Coupon
|
||||
const createSession = () =>
|
||||
Billing.stripe().checkout.sessions.create({
|
||||
mode: "subscription",
|
||||
discounts: [{ coupon: LiteData.firstMonthCoupon(email!) }],
|
||||
discounts: [{ coupon }],
|
||||
...(billing.customerID
|
||||
? {
|
||||
customer: billing.customerID,
|
||||
@@ -264,7 +306,7 @@ export namespace Billing {
|
||||
},
|
||||
}
|
||||
: {
|
||||
customer_email: email!,
|
||||
customer_email: email,
|
||||
}),
|
||||
...(() => {
|
||||
if (method === "alipay") {
|
||||
@@ -312,6 +354,8 @@ export namespace Billing {
|
||||
metadata: {
|
||||
workspaceID: Actor.workspace(),
|
||||
userID: user.properties.userID,
|
||||
userEmail: email,
|
||||
coupon,
|
||||
type: "lite",
|
||||
},
|
||||
},
|
||||
|
||||
@@ -11,11 +11,7 @@ export namespace LiteData {
|
||||
export const productID = fn(z.void(), () => Resource.ZEN_LITE_PRICE.product)
|
||||
export const priceID = fn(z.void(), () => Resource.ZEN_LITE_PRICE.price)
|
||||
export const priceInr = fn(z.void(), () => Resource.ZEN_LITE_PRICE.priceInr)
|
||||
export const firstMonthCoupon = fn(z.string(), (email) => {
|
||||
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 firstMonth100Coupon = Resource.ZEN_LITE_PRICE.firstMonth100Coupon
|
||||
export const firstMonth50Coupon = Resource.ZEN_LITE_PRICE.firstMonth50Coupon
|
||||
export const planName = fn(z.void(), () => "lite")
|
||||
}
|
||||
|
||||
@@ -1,4 +1,15 @@
|
||||
import { bigint, boolean, index, int, json, mysqlEnum, mysqlTable, uniqueIndex, varchar } from "drizzle-orm/mysql-core"
|
||||
import {
|
||||
bigint,
|
||||
boolean,
|
||||
index,
|
||||
int,
|
||||
json,
|
||||
mysqlEnum,
|
||||
mysqlTable,
|
||||
primaryKey,
|
||||
uniqueIndex,
|
||||
varchar,
|
||||
} from "drizzle-orm/mysql-core"
|
||||
import { timestamps, ulid, utc, workspaceColumns } from "../drizzle/types"
|
||||
import { workspaceIndexes } from "./workspace.sql"
|
||||
|
||||
@@ -121,3 +132,14 @@ export const UsageTable = mysqlTable(
|
||||
},
|
||||
(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] })],
|
||||
)
|
||||
|
||||
Vendored
-4
@@ -142,10 +142,6 @@ declare module "sst" {
|
||||
"type": "sst.sst.Secret"
|
||||
"value": string
|
||||
}
|
||||
"ZEN_LITE_COUPON_FIRST_MONTH_100_INVITEES": {
|
||||
"type": "sst.sst.Secret"
|
||||
"value": string
|
||||
}
|
||||
"ZEN_LITE_PRICE": {
|
||||
"firstMonth100Coupon": string
|
||||
"firstMonth50Coupon": string
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@opencode-ai/console-function",
|
||||
"version": "1.4.11",
|
||||
"version": "1.14.19",
|
||||
"$schema": "https://json.schemastore.org/package.json",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
|
||||
-4
@@ -142,10 +142,6 @@ declare module "sst" {
|
||||
"type": "sst.sst.Secret"
|
||||
"value": string
|
||||
}
|
||||
"ZEN_LITE_COUPON_FIRST_MONTH_100_INVITEES": {
|
||||
"type": "sst.sst.Secret"
|
||||
"value": string
|
||||
}
|
||||
"ZEN_LITE_PRICE": {
|
||||
"firstMonth100Coupon": string
|
||||
"firstMonth50Coupon": string
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@opencode-ai/console-mail",
|
||||
"version": "1.4.11",
|
||||
"version": "1.14.19",
|
||||
"dependencies": {
|
||||
"@jsx-email/all": "2.2.3",
|
||||
"@jsx-email/cli": "1.4.3",
|
||||
|
||||
-4
@@ -142,10 +142,6 @@ declare module "sst" {
|
||||
"type": "sst.sst.Secret"
|
||||
"value": string
|
||||
}
|
||||
"ZEN_LITE_COUPON_FIRST_MONTH_100_INVITEES": {
|
||||
"type": "sst.sst.Secret"
|
||||
"value": string
|
||||
}
|
||||
"ZEN_LITE_PRICE": {
|
||||
"firstMonth100Coupon": string
|
||||
"firstMonth50Coupon": string
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@opencode-ai/desktop-electron",
|
||||
"private": true,
|
||||
"version": "1.4.11",
|
||||
"version": "1.14.19",
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
"homepage": "https://opencode.ai",
|
||||
@@ -30,6 +30,7 @@
|
||||
"electron-store": "^10",
|
||||
"electron-updater": "^6",
|
||||
"electron-window-state": "^5.0.3",
|
||||
"drizzle-orm": "catalog:",
|
||||
"marked": "^15"
|
||||
},
|
||||
"devDependencies": {
|
||||
@@ -45,7 +46,7 @@
|
||||
"@types/node": "catalog:",
|
||||
"@typescript/native-preview": "catalog:",
|
||||
"@valibot/to-json-schema": "1.6.0",
|
||||
"electron": "40.4.1",
|
||||
"electron": "41.2.1",
|
||||
"electron-builder": "^26",
|
||||
"electron-vite": "^5",
|
||||
"solid-js": "catalog:",
|
||||
|
||||
@@ -28,8 +28,10 @@ const APP_IDS: Record<string, string> = {
|
||||
beta: "ai.opencode.desktop.beta",
|
||||
prod: "ai.opencode.desktop",
|
||||
}
|
||||
const appId = app.isPackaged ? APP_IDS[CHANNEL] : "ai.opencode.desktop.dev"
|
||||
app.setName(app.isPackaged ? APP_NAMES[CHANNEL] : "OpenCode Dev")
|
||||
app.setPath("userData", join(app.getPath("appData"), app.isPackaged ? APP_IDS[CHANNEL] : "ai.opencode.desktop.dev"))
|
||||
app.setAppUserModelId(appId)
|
||||
app.setPath("userData", join(app.getPath("appData"), appId))
|
||||
const { autoUpdater } = pkg
|
||||
|
||||
import type { InitStep, ServerReadyData, SqliteMigrationProgress, WslConfig } from "../preload/types"
|
||||
@@ -41,6 +43,7 @@ import { parseMarkdown } from "./markdown"
|
||||
import { createMenu } from "./menu"
|
||||
import { getDefaultServerUrl, getWslConfig, setDefaultServerUrl, setWslConfig, spawnLocalServer } from "./server"
|
||||
import { createLoadingWindow, createMainWindow, setBackgroundColor, setDockIcon } from "./windows"
|
||||
import { drizzle } from "drizzle-orm/node-sqlite/driver"
|
||||
import type { Server } from "virtual:opencode-server"
|
||||
|
||||
const initEmitter = new EventEmitter()
|
||||
@@ -137,15 +140,6 @@ async function initialize() {
|
||||
const url = `http://${hostname}:${port}`
|
||||
const password = randomUUID()
|
||||
|
||||
logger.log("spawning sidecar", { url })
|
||||
const { listener, health } = await spawnLocalServer(hostname, port, password)
|
||||
server = listener
|
||||
serverReady.resolve({
|
||||
url,
|
||||
username: "opencode",
|
||||
password,
|
||||
})
|
||||
|
||||
const loadingTask = (async () => {
|
||||
logger.log("sidecar connection started", { url })
|
||||
|
||||
@@ -156,10 +150,32 @@ async function initialize() {
|
||||
if (progress.type === "Done") sqliteDone?.resolve()
|
||||
})
|
||||
|
||||
if (needsMigration) {
|
||||
const { Database, JsonMigration } = await import("virtual:opencode-server")
|
||||
await JsonMigration.run(drizzle({ client: Database.Client().$client }), {
|
||||
progress: (event: { current: number; total: number }) => {
|
||||
const percent = Math.round(event.current / event.total) * 100
|
||||
initEmitter.emit("sqlite", { type: "InProgress", value: percent })
|
||||
},
|
||||
})
|
||||
initEmitter.emit("sqlite", { type: "Done" })
|
||||
|
||||
sqliteDone?.resolve()
|
||||
}
|
||||
|
||||
if (needsMigration) {
|
||||
await sqliteDone?.promise
|
||||
}
|
||||
|
||||
logger.log("spawning sidecar", { url })
|
||||
const { listener, health } = await spawnLocalServer(hostname, port, password)
|
||||
server = listener
|
||||
serverReady.resolve({
|
||||
url,
|
||||
username: "opencode",
|
||||
password,
|
||||
})
|
||||
|
||||
await Promise.race([
|
||||
health.wait,
|
||||
delay(30_000).then(() => {
|
||||
|
||||
@@ -4,7 +4,7 @@ import { existsSync, readdirSync, readFileSync } from "node:fs"
|
||||
import { homedir } from "node:os"
|
||||
import { join } from "node:path"
|
||||
import { CHANNEL } from "./constants"
|
||||
import { getStore, store } from "./store"
|
||||
import { getStore } from "./store"
|
||||
|
||||
const TAURI_MIGRATED_KEY = "tauriMigrated"
|
||||
|
||||
@@ -67,7 +67,7 @@ function migrateFile(datPath: string, filename: string) {
|
||||
}
|
||||
|
||||
export function migrate() {
|
||||
if (store.get(TAURI_MIGRATED_KEY)) {
|
||||
if (getStore().get(TAURI_MIGRATED_KEY)) {
|
||||
log.log("tauri migration: already done, skipping")
|
||||
return
|
||||
}
|
||||
@@ -77,7 +77,7 @@ export function migrate() {
|
||||
|
||||
if (!existsSync(dir)) {
|
||||
log.log("tauri migration: no tauri data directory found, nothing to migrate")
|
||||
store.set(TAURI_MIGRATED_KEY, true)
|
||||
getStore().set(TAURI_MIGRATED_KEY, true)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -87,5 +87,5 @@ export function migrate() {
|
||||
}
|
||||
|
||||
log.log("tauri migration: complete")
|
||||
store.set(TAURI_MIGRATED_KEY, true)
|
||||
getStore().set(TAURI_MIGRATED_KEY, true)
|
||||
}
|
||||
|
||||
@@ -1,33 +1,33 @@
|
||||
import { app } from "electron"
|
||||
import { DEFAULT_SERVER_URL_KEY, WSL_ENABLED_KEY } from "./constants"
|
||||
import { getUserShell, loadShellEnv } from "./shell-env"
|
||||
import { store } from "./store"
|
||||
import { getStore } from "./store"
|
||||
|
||||
export type WslConfig = { enabled: boolean }
|
||||
|
||||
export type HealthCheck = { wait: Promise<void> }
|
||||
|
||||
export function getDefaultServerUrl(): string | null {
|
||||
const value = store.get(DEFAULT_SERVER_URL_KEY)
|
||||
const value = getStore().get(DEFAULT_SERVER_URL_KEY)
|
||||
return typeof value === "string" ? value : null
|
||||
}
|
||||
|
||||
export function setDefaultServerUrl(url: string | null) {
|
||||
if (url) {
|
||||
store.set(DEFAULT_SERVER_URL_KEY, url)
|
||||
getStore().set(DEFAULT_SERVER_URL_KEY, url)
|
||||
return
|
||||
}
|
||||
|
||||
store.delete(DEFAULT_SERVER_URL_KEY)
|
||||
getStore().delete(DEFAULT_SERVER_URL_KEY)
|
||||
}
|
||||
|
||||
export function getWslConfig(): WslConfig {
|
||||
const value = store.get(WSL_ENABLED_KEY)
|
||||
const value = getStore().get(WSL_ENABLED_KEY)
|
||||
return { enabled: typeof value === "boolean" ? value : false }
|
||||
}
|
||||
|
||||
export function setWslConfig(config: WslConfig) {
|
||||
store.set(WSL_ENABLED_KEY, config.enabled)
|
||||
getStore().set(WSL_ENABLED_KEY, config.enabled)
|
||||
}
|
||||
|
||||
export async function spawnLocalServer(hostname: string, port: number, password: string) {
|
||||
|
||||
@@ -4,6 +4,10 @@ import { SETTINGS_STORE } from "./constants"
|
||||
|
||||
const cache = new Map<string, Store>()
|
||||
|
||||
// We cannot instantiate the electron-store at module load time because
|
||||
// module import hoisting causes this to run before app.setPath("userData", ...)
|
||||
// in index.ts has executed, which would result in files being written to the default directory
|
||||
// (e.g. bad: %APPDATA%\@opencode-ai\desktop-electron\opencode.settings vs good: %APPDATA%\ai.opencode.desktop.dev\opencode.settings).
|
||||
export function getStore(name = SETTINGS_STORE) {
|
||||
const cached = cache.get(name)
|
||||
if (cached) return cached
|
||||
@@ -11,5 +15,3 @@ export function getStore(name = SETTINGS_STORE) {
|
||||
cache.set(name, next)
|
||||
return next
|
||||
}
|
||||
|
||||
export const store = getStore(SETTINGS_STORE)
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@opencode-ai/desktop",
|
||||
"private": true,
|
||||
"version": "1.4.11",
|
||||
"version": "1.14.19",
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
"scripts": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@opencode-ai/enterprise",
|
||||
"version": "1.4.11",
|
||||
"version": "1.14.19",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
|
||||
Vendored
-4
@@ -142,10 +142,6 @@ declare module "sst" {
|
||||
"type": "sst.sst.Secret"
|
||||
"value": string
|
||||
}
|
||||
"ZEN_LITE_COUPON_FIRST_MONTH_100_INVITEES": {
|
||||
"type": "sst.sst.Secret"
|
||||
"value": string
|
||||
}
|
||||
"ZEN_LITE_PRICE": {
|
||||
"firstMonth100Coupon": string
|
||||
"firstMonth50Coupon": string
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
id = "opencode"
|
||||
name = "OpenCode"
|
||||
description = "The open source coding agent."
|
||||
version = "1.4.11"
|
||||
version = "1.14.19"
|
||||
schema_version = 1
|
||||
authors = ["Anomaly"]
|
||||
repository = "https://github.com/anomalyco/opencode"
|
||||
@@ -11,26 +11,26 @@ name = "OpenCode"
|
||||
icon = "./icons/opencode.svg"
|
||||
|
||||
[agent_servers.opencode.targets.darwin-aarch64]
|
||||
archive = "https://github.com/anomalyco/opencode/releases/download/v1.4.11/opencode-darwin-arm64.zip"
|
||||
archive = "https://github.com/anomalyco/opencode/releases/download/v1.14.19/opencode-darwin-arm64.zip"
|
||||
cmd = "./opencode"
|
||||
args = ["acp"]
|
||||
|
||||
[agent_servers.opencode.targets.darwin-x86_64]
|
||||
archive = "https://github.com/anomalyco/opencode/releases/download/v1.4.11/opencode-darwin-x64.zip"
|
||||
archive = "https://github.com/anomalyco/opencode/releases/download/v1.14.19/opencode-darwin-x64.zip"
|
||||
cmd = "./opencode"
|
||||
args = ["acp"]
|
||||
|
||||
[agent_servers.opencode.targets.linux-aarch64]
|
||||
archive = "https://github.com/anomalyco/opencode/releases/download/v1.4.11/opencode-linux-arm64.tar.gz"
|
||||
archive = "https://github.com/anomalyco/opencode/releases/download/v1.14.19/opencode-linux-arm64.tar.gz"
|
||||
cmd = "./opencode"
|
||||
args = ["acp"]
|
||||
|
||||
[agent_servers.opencode.targets.linux-x86_64]
|
||||
archive = "https://github.com/anomalyco/opencode/releases/download/v1.4.11/opencode-linux-x64.tar.gz"
|
||||
archive = "https://github.com/anomalyco/opencode/releases/download/v1.14.19/opencode-linux-x64.tar.gz"
|
||||
cmd = "./opencode"
|
||||
args = ["acp"]
|
||||
|
||||
[agent_servers.opencode.targets.windows-x86_64]
|
||||
archive = "https://github.com/anomalyco/opencode/releases/download/v1.4.11/opencode-windows-x64.zip"
|
||||
archive = "https://github.com/anomalyco/opencode/releases/download/v1.14.19/opencode-windows-x64.zip"
|
||||
cmd = "./opencode.exe"
|
||||
args = ["acp"]
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@opencode-ai/function",
|
||||
"version": "1.4.11",
|
||||
"version": "1.14.19",
|
||||
"$schema": "https://json.schemastore.org/package.json",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
|
||||
Vendored
-4
@@ -142,10 +142,6 @@ declare module "sst" {
|
||||
"type": "sst.sst.Secret"
|
||||
"value": string
|
||||
}
|
||||
"ZEN_LITE_COUPON_FIRST_MONTH_100_INVITEES": {
|
||||
"type": "sst.sst.Secret"
|
||||
"value": string
|
||||
}
|
||||
"ZEN_LITE_PRICE": {
|
||||
"firstMonth100Coupon": string
|
||||
"firstMonth50Coupon": string
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/package.json",
|
||||
"version": "1.4.11",
|
||||
"version": "1.14.19",
|
||||
"name": "opencode",
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
@@ -79,7 +79,7 @@
|
||||
"@actions/github": "6.0.1",
|
||||
"@agentclientprotocol/sdk": "0.16.1",
|
||||
"@ai-sdk/alibaba": "1.0.17",
|
||||
"@ai-sdk/amazon-bedrock": "4.0.95",
|
||||
"@ai-sdk/amazon-bedrock": "4.0.96",
|
||||
"@ai-sdk/anthropic": "3.0.71",
|
||||
"@ai-sdk/azure": "3.0.49",
|
||||
"@ai-sdk/cerebras": "2.0.41",
|
||||
@@ -110,6 +110,7 @@
|
||||
"@lydell/node-pty": "catalog:",
|
||||
"@modelcontextprotocol/sdk": "1.27.1",
|
||||
"@npmcli/arborist": "9.4.0",
|
||||
"@npmcli/config": "10.8.1",
|
||||
"@octokit/graphql": "9.0.2",
|
||||
"@octokit/rest": "catalog:",
|
||||
"@openauthjs/openauth": "catalog:",
|
||||
@@ -122,8 +123,8 @@
|
||||
"@opentelemetry/exporter-trace-otlp-http": "0.214.0",
|
||||
"@opentelemetry/sdk-trace-base": "2.6.1",
|
||||
"@opentelemetry/sdk-trace-node": "2.6.1",
|
||||
"@opentui/core": "catalog:",
|
||||
"@opentui/solid": "catalog:",
|
||||
"@opentui/core": "0.1.101",
|
||||
"@opentui/solid": "0.1.101",
|
||||
"@parcel/watcher": "2.5.1",
|
||||
"@pierre/diffs": "catalog:",
|
||||
"@solid-primitives/event-bus": "1.1.2",
|
||||
@@ -161,7 +162,6 @@
|
||||
"opentui-spinner": "0.0.6",
|
||||
"partial-json": "0.1.7",
|
||||
"remeda": "catalog:",
|
||||
"ripgrep": "0.3.1",
|
||||
"semver": "^7.6.3",
|
||||
"solid-js": "catalog:",
|
||||
"strip-ansi": "7.1.2",
|
||||
|
||||
@@ -187,7 +187,6 @@ for (const item of targets) {
|
||||
const rootPath = path.resolve(dir, "../../node_modules/@opentui/core/parser.worker.js")
|
||||
const parserWorker = fs.realpathSync(fs.existsSync(localPath) ? localPath : rootPath)
|
||||
const workerPath = "./src/cli/cmd/tui/worker.ts"
|
||||
const rgPath = "./src/file/ripgrep.worker.ts"
|
||||
|
||||
// Use platform-specific bunfs root path based on target OS
|
||||
const bunfsRoot = item.os === "win32" ? "B:/~BUN/root/" : "/$bunfs/root/"
|
||||
@@ -212,19 +211,12 @@ for (const item of targets) {
|
||||
windows: {},
|
||||
},
|
||||
files: embeddedFileMap ? { "opencode-web-ui.gen.ts": embeddedFileMap } : {},
|
||||
entrypoints: [
|
||||
"./src/index.ts",
|
||||
parserWorker,
|
||||
workerPath,
|
||||
rgPath,
|
||||
...(embeddedFileMap ? ["opencode-web-ui.gen.ts"] : []),
|
||||
],
|
||||
entrypoints: ["./src/index.ts", parserWorker, workerPath, ...(embeddedFileMap ? ["opencode-web-ui.gen.ts"] : [])],
|
||||
define: {
|
||||
OPENCODE_VERSION: `'${Script.version}'`,
|
||||
OPENCODE_MIGRATIONS: JSON.stringify(migrations),
|
||||
OTUI_TREE_SITTER_WORKER_PATH: bunfsRoot + workerRelativePath,
|
||||
OPENCODE_WORKER_PATH: workerPath,
|
||||
OPENCODE_RIPGREP_WORKER_PATH: rgPath,
|
||||
OPENCODE_CHANNEL: `'${Script.channel}'`,
|
||||
OPENCODE_LIBC: item.os === "linux" ? `'${item.abi ?? "glibc"}'` : "",
|
||||
},
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
#!/usr/bin/env bun
|
||||
|
||||
import { $ } from "bun"
|
||||
import { Effect } from "effect"
|
||||
import pkg from "../package.json"
|
||||
import { Script } from "@opencode-ai/script"
|
||||
import { fileURLToPath } from "url"
|
||||
@@ -9,107 +7,84 @@ import { fileURLToPath } from "url"
|
||||
const dir = fileURLToPath(new URL("..", import.meta.url))
|
||||
process.chdir(dir)
|
||||
|
||||
const published = (name: string, version: string) =>
|
||||
Effect.promise(() => $`npm view ${name}@${version} version`.nothrow()).pipe(
|
||||
Effect.map((result) => result.exitCode === 0),
|
||||
)
|
||||
async function published(name: string, version: string) {
|
||||
return (await $`npm view ${name}@${version} version`.nothrow()).exitCode === 0
|
||||
}
|
||||
|
||||
const sha256 = (path: string) =>
|
||||
Effect.promise(() => $`sha256sum ${path} | cut -d' ' -f1`.text()).pipe(Effect.map((text) => text.trim()))
|
||||
|
||||
const publishPackage = (dir: string, name: string, version: string) =>
|
||||
Effect.gen(function* () {
|
||||
if (yield* published(name, version)) {
|
||||
console.log(`already published ${name}@${version}`)
|
||||
return
|
||||
}
|
||||
if (process.platform !== "win32") yield* Effect.promise(() => $`chmod -R 755 .`.cwd(dir))
|
||||
yield* Effect.promise(() => $`bun pm pack`.cwd(dir))
|
||||
yield* Effect.promise(() => $`npm publish *.tgz --access public --tag ${Script.channel}`.cwd(dir))
|
||||
})
|
||||
|
||||
const binaryVersion = (value: unknown) => {
|
||||
if (
|
||||
typeof value === "object" &&
|
||||
value !== null &&
|
||||
"name" in value &&
|
||||
typeof value.name === "string" &&
|
||||
"version" in value &&
|
||||
typeof value.version === "string"
|
||||
) {
|
||||
return value
|
||||
async function publish(dir: string, name: string, version: string) {
|
||||
// GitHub artifact downloads can drop the executable bit, and Docker uses the
|
||||
// unpacked dist binaries directly rather than the published tarball.
|
||||
if (process.platform !== "win32") await $`chmod -R 755 .`.cwd(dir)
|
||||
if (await published(name, version)) {
|
||||
console.log(`already published ${name}@${version}`)
|
||||
return
|
||||
}
|
||||
throw new Error("invalid dist package manifest")
|
||||
await $`bun pm pack`.cwd(dir)
|
||||
await $`npm publish *.tgz --access public --tag ${Script.channel}`.cwd(dir)
|
||||
}
|
||||
|
||||
const ensureVersion = (value: unknown) => {
|
||||
if (typeof value === "string") return value
|
||||
throw new Error("missing dist package version")
|
||||
const binaries: Record<string, string> = {}
|
||||
for (const filepath of new Bun.Glob("*/package.json").scanSync({ cwd: "./dist" })) {
|
||||
const pkg = await Bun.file(`./dist/${filepath}`).json()
|
||||
binaries[pkg.name] = pkg.version
|
||||
}
|
||||
console.log("binaries", binaries)
|
||||
const version = Object.values(binaries)[0]
|
||||
|
||||
const program = Effect.gen(function* () {
|
||||
const binaries: Record<string, string> = Object.fromEntries(
|
||||
yield* Effect.promise(async () =>
|
||||
Array.fromAsync(new Bun.Glob("*/package.json").scan({ cwd: "./dist" }), async (filepath) => {
|
||||
const current = binaryVersion(await Bun.file(`./dist/${filepath}`).json())
|
||||
return [current.name, current.version] as const
|
||||
}),
|
||||
),
|
||||
)
|
||||
console.log("binaries", binaries)
|
||||
await $`mkdir -p ./dist/${pkg.name}`
|
||||
await $`cp -r ./bin ./dist/${pkg.name}/bin`
|
||||
await $`cp ./script/postinstall.mjs ./dist/${pkg.name}/postinstall.mjs`
|
||||
await Bun.file(`./dist/${pkg.name}/LICENSE`).write(await Bun.file("../../LICENSE").text())
|
||||
|
||||
const version = ensureVersion(Object.values(binaries)[0])
|
||||
await Bun.file(`./dist/${pkg.name}/package.json`).write(
|
||||
JSON.stringify(
|
||||
{
|
||||
name: pkg.name + "-ai",
|
||||
bin: {
|
||||
[pkg.name]: `./bin/${pkg.name}`,
|
||||
},
|
||||
scripts: {
|
||||
postinstall: "bun ./postinstall.mjs || node ./postinstall.mjs",
|
||||
},
|
||||
version: version,
|
||||
license: pkg.license,
|
||||
optionalDependencies: binaries,
|
||||
},
|
||||
null,
|
||||
2,
|
||||
),
|
||||
)
|
||||
|
||||
yield* Effect.promise(() => $`mkdir -p ./dist/${pkg.name}`)
|
||||
yield* Effect.promise(() => $`cp -r ./bin ./dist/${pkg.name}/bin`)
|
||||
yield* Effect.promise(() => $`cp ./script/postinstall.mjs ./dist/${pkg.name}/postinstall.mjs`)
|
||||
yield* Effect.promise(async () =>
|
||||
Bun.file(`./dist/${pkg.name}/LICENSE`).write(await Bun.file("../../LICENSE").text()),
|
||||
)
|
||||
yield* Effect.promise(() =>
|
||||
Bun.write(
|
||||
`./dist/${pkg.name}/package.json`,
|
||||
JSON.stringify(
|
||||
{
|
||||
name: pkg.name + "-ai",
|
||||
bin: { [pkg.name]: `./bin/${pkg.name}` },
|
||||
scripts: { postinstall: "bun ./postinstall.mjs || node ./postinstall.mjs" },
|
||||
version,
|
||||
license: pkg.license,
|
||||
optionalDependencies: binaries,
|
||||
},
|
||||
null,
|
||||
2,
|
||||
),
|
||||
),
|
||||
)
|
||||
const tasks = Object.entries(binaries).map(async ([name]) => {
|
||||
await publish(`./dist/${name}`, name, binaries[name])
|
||||
})
|
||||
await Promise.all(tasks)
|
||||
await publish(`./dist/${pkg.name}`, `${pkg.name}-ai`, version)
|
||||
|
||||
yield* Effect.all(Object.entries(binaries).map(([name, version]) => publishPackage(`./dist/${name}`, name, version)))
|
||||
yield* publishPackage(`./dist/${pkg.name}`, `${pkg.name}-ai`, version)
|
||||
const image = "ghcr.io/anomalyco/opencode"
|
||||
const platforms = "linux/amd64,linux/arm64"
|
||||
const tags = [`${image}:${version}`, `${image}:${Script.channel}`]
|
||||
const tagFlags = tags.flatMap((t) => ["-t", t])
|
||||
|
||||
const image = "ghcr.io/anomalyco/opencode"
|
||||
const tags = [`${image}:${version}`, `${image}:${Script.channel}`]
|
||||
yield* Effect.promise(
|
||||
() => $`docker buildx build --platform linux/amd64,linux/arm64 ${tags.flatMap((t) => ["-t", t])} --push .`,
|
||||
)
|
||||
// registries
|
||||
if (!Script.preview) {
|
||||
await $`docker buildx build --platform ${platforms} ${tagFlags} --push .`
|
||||
// Calculate SHA values
|
||||
const arm64Sha = await $`sha256sum ./dist/opencode-linux-arm64.tar.gz | cut -d' ' -f1`.text().then((x) => x.trim())
|
||||
const x64Sha = await $`sha256sum ./dist/opencode-linux-x64.tar.gz | cut -d' ' -f1`.text().then((x) => x.trim())
|
||||
const macX64Sha = await $`sha256sum ./dist/opencode-darwin-x64.zip | cut -d' ' -f1`.text().then((x) => x.trim())
|
||||
const macArm64Sha = await $`sha256sum ./dist/opencode-darwin-arm64.zip | cut -d' ' -f1`.text().then((x) => x.trim())
|
||||
|
||||
if (Script.preview) return
|
||||
|
||||
const [arm64Sha, x64Sha, macX64Sha, macArm64Sha] = yield* Effect.all([
|
||||
sha256("./dist/opencode-linux-arm64.tar.gz"),
|
||||
sha256("./dist/opencode-linux-x64.tar.gz"),
|
||||
sha256("./dist/opencode-darwin-x64.zip"),
|
||||
sha256("./dist/opencode-darwin-arm64.zip"),
|
||||
])
|
||||
const [pkgver, subver = ""] = Script.version.split(/(-.*)/, 2)
|
||||
const [pkgver, _subver = ""] = Script.version.split(/(-.*)/, 2)
|
||||
|
||||
// arch
|
||||
const binaryPkgbuild = [
|
||||
"# Maintainer: dax",
|
||||
"# Maintainer: adam",
|
||||
"",
|
||||
"pkgname='opencode-bin'",
|
||||
`pkgver=${pkgver}`,
|
||||
`_subver=${subver}`,
|
||||
`_subver=${_subver}`,
|
||||
"options=('!debug' '!strip')",
|
||||
"pkgrel=1",
|
||||
"pkgdesc='The AI coding agent built for the terminal.'",
|
||||
@@ -122,6 +97,7 @@ const program = Effect.gen(function* () {
|
||||
"",
|
||||
`source_aarch64=("\${pkgname}_\${pkgver}_aarch64.tar.gz::https://github.com/anomalyco/opencode/releases/download/v\${pkgver}\${_subver}/opencode-linux-arm64.tar.gz")`,
|
||||
`sha256sums_aarch64=('${arm64Sha}')`,
|
||||
|
||||
`source_x86_64=("\${pkgname}_\${pkgver}_x86_64.tar.gz::https://github.com/anomalyco/opencode/releases/download/v\${pkgver}\${_subver}/opencode-linux-x64.tar.gz")`,
|
||||
`sha256sums_x86_64=('${x64Sha}')`,
|
||||
"",
|
||||
@@ -131,40 +107,37 @@ const program = Effect.gen(function* () {
|
||||
"",
|
||||
].join("\n")
|
||||
|
||||
yield* Effect.promise(async () => {
|
||||
for (const [pkg, pkgbuild] of [["opencode-bin", binaryPkgbuild]]) {
|
||||
for (let i = 0; i < 30; i++) {
|
||||
try {
|
||||
await $`rm -rf ./dist/aur-opencode-bin`
|
||||
await $`git clone ssh://aur@aur.archlinux.org/opencode-bin.git ./dist/aur-opencode-bin`
|
||||
await $`cd ./dist/aur-opencode-bin && git checkout master`
|
||||
await Bun.write(`./dist/aur-opencode-bin/PKGBUILD`, binaryPkgbuild)
|
||||
await $`cd ./dist/aur-opencode-bin && makepkg --printsrcinfo > .SRCINFO`
|
||||
await $`cd ./dist/aur-opencode-bin && git add PKGBUILD .SRCINFO`
|
||||
if ((await $`cd ./dist/aur-opencode-bin && git diff --cached --quiet`.nothrow()).exitCode === 0) return
|
||||
await $`cd ./dist/aur-opencode-bin && git commit -m "Update to v${Script.version}"`
|
||||
await $`cd ./dist/aur-opencode-bin && git push`
|
||||
return
|
||||
} catch {}
|
||||
await $`rm -rf ./dist/aur-${pkg}`
|
||||
await $`git clone ssh://aur@aur.archlinux.org/${pkg}.git ./dist/aur-${pkg}`
|
||||
await $`cd ./dist/aur-${pkg} && git checkout master`
|
||||
await Bun.file(`./dist/aur-${pkg}/PKGBUILD`).write(pkgbuild)
|
||||
await $`cd ./dist/aur-${pkg} && makepkg --printsrcinfo > .SRCINFO`
|
||||
await $`cd ./dist/aur-${pkg} && git add PKGBUILD .SRCINFO`
|
||||
if ((await $`cd ./dist/aur-${pkg} && git diff --cached --quiet`.nothrow()).exitCode === 0) break
|
||||
await $`cd ./dist/aur-${pkg} && git commit -m "Update to v${Script.version}"`
|
||||
await $`cd ./dist/aur-${pkg} && git push`
|
||||
break
|
||||
} catch {
|
||||
continue
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
const token = process.env.GITHUB_TOKEN
|
||||
if (!token) {
|
||||
console.error("GITHUB_TOKEN is required to update homebrew tap")
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
// Homebrew formula
|
||||
const homebrewFormula = [
|
||||
"# typed: false",
|
||||
"# frozen_string_literal: true",
|
||||
"",
|
||||
"# This file was generated by GoReleaser. DO NOT EDIT.",
|
||||
"class Opencode < Formula",
|
||||
' desc "The AI coding agent built for the terminal."',
|
||||
' homepage "https://github.com/anomalyco/opencode"',
|
||||
` desc "The AI coding agent built for the terminal."`,
|
||||
` homepage "https://github.com/anomalyco/opencode"`,
|
||||
` version "${Script.version.split("-")[0]}"`,
|
||||
"",
|
||||
' depends_on "ripgrep"',
|
||||
` depends_on "ripgrep"`,
|
||||
"",
|
||||
" on_macos do",
|
||||
" if Hardware::CPU.intel?",
|
||||
@@ -206,15 +179,18 @@ const program = Effect.gen(function* () {
|
||||
"",
|
||||
].join("\n")
|
||||
|
||||
yield* Effect.promise(async () => {
|
||||
await $`rm -rf ./dist/homebrew-tap`
|
||||
await $`git clone https://x-access-token:${token}@github.com/anomalyco/homebrew-tap.git ./dist/homebrew-tap`
|
||||
await Bun.write("./dist/homebrew-tap/opencode.rb", homebrewFormula)
|
||||
await $`cd ./dist/homebrew-tap && git add opencode.rb`
|
||||
if ((await $`cd ./dist/homebrew-tap && git diff --cached --quiet`.nothrow()).exitCode === 0) return
|
||||
const token = process.env.GITHUB_TOKEN
|
||||
if (!token) {
|
||||
console.error("GITHUB_TOKEN is required to update homebrew tap")
|
||||
process.exit(1)
|
||||
}
|
||||
const tap = `https://x-access-token:${token}@github.com/anomalyco/homebrew-tap.git`
|
||||
await $`rm -rf ./dist/homebrew-tap`
|
||||
await $`git clone ${tap} ./dist/homebrew-tap`
|
||||
await Bun.file("./dist/homebrew-tap/opencode.rb").write(homebrewFormula)
|
||||
await $`cd ./dist/homebrew-tap && git add opencode.rb`
|
||||
if ((await $`cd ./dist/homebrew-tap && git diff --cached --quiet`.nothrow()).exitCode !== 0) {
|
||||
await $`cd ./dist/homebrew-tap && git commit -m "Update to v${Script.version}"`
|
||||
await $`cd ./dist/homebrew-tap && git push`
|
||||
})
|
||||
})
|
||||
|
||||
await Effect.runPromise(program)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,12 +1,19 @@
|
||||
# Schema migration
|
||||
|
||||
Practical reference for migrating data types in `packages/opencode` from Zod-first definitions to Effect Schema with Zod compatibility shims.
|
||||
Practical reference for migrating data types in `packages/opencode` from
|
||||
Zod-first definitions to Effect Schema with Zod compatibility shims.
|
||||
|
||||
## Goal
|
||||
|
||||
Use Effect Schema as the source of truth for domain models, IDs, inputs, outputs, and typed errors.
|
||||
Use Effect Schema as the source of truth for domain models, IDs, inputs,
|
||||
outputs, and typed errors. Keep Zod available at existing HTTP, tool, and
|
||||
compatibility boundaries by exposing a `.zod` static derived from the Effect
|
||||
schema via `@/util/effect-zod`.
|
||||
|
||||
Keep Zod available at existing HTTP, tool, and compatibility boundaries by exposing a `.zod` field derived from the Effect schema.
|
||||
The long-term driver is `specs/effect/http-api.md` — once the HTTP server
|
||||
moves to `@effect/platform`, every Schema-first DTO can flow through
|
||||
`HttpApi` / `HttpRouter` without a zod translation layer, and the entire
|
||||
`effect-zod` walker plus every `.zod` static can be deleted.
|
||||
|
||||
## Preferred shapes
|
||||
|
||||
@@ -24,17 +31,14 @@ export class Info extends Schema.Class<Info>("Foo.Info")({
|
||||
}
|
||||
```
|
||||
|
||||
If the class cannot reference itself cleanly during initialization, use the existing two-step pattern:
|
||||
If the class cannot reference itself cleanly during initialization, use the
|
||||
two-step `withStatics` pattern:
|
||||
|
||||
```ts
|
||||
const _Info = Schema.Struct({
|
||||
export const Info = Schema.Struct({
|
||||
id: FooID,
|
||||
name: Schema.String,
|
||||
})
|
||||
|
||||
export const Info = Object.assign(_Info, {
|
||||
zod: zod(_Info),
|
||||
})
|
||||
}).pipe(withStatics((s) => ({ zod: zod(s) })))
|
||||
```
|
||||
|
||||
### Errors
|
||||
@@ -49,27 +53,89 @@ export class NotFoundError extends Schema.TaggedErrorClass<NotFoundError>()("Foo
|
||||
|
||||
### IDs and branded leaf types
|
||||
|
||||
Keep branded/schema-backed IDs as Effect schemas and expose `static readonly zod` for compatibility when callers still expect Zod.
|
||||
Keep branded/schema-backed IDs as Effect schemas and expose
|
||||
`static readonly zod` for compatibility when callers still expect Zod.
|
||||
|
||||
### Refinements
|
||||
|
||||
Reuse named refinements instead of re-spelling `z.number().int().positive()`
|
||||
in every schema. The `effect-zod` walker translates the Effect versions into
|
||||
the corresponding zod methods, so JSON Schema output (`type: integer`,
|
||||
`exclusiveMinimum`, `pattern`, `format: uuid`, …) is preserved.
|
||||
|
||||
```ts
|
||||
const PositiveInt = Schema.Number.check(Schema.isInt()).check(Schema.isGreaterThan(0))
|
||||
const NonNegativeInt = Schema.Number.check(Schema.isInt()).check(Schema.isGreaterThanOrEqualTo(0))
|
||||
const HexColor = Schema.String.check(Schema.isPattern(/^#[0-9a-fA-F]{6}$/))
|
||||
```
|
||||
|
||||
See `test/util/effect-zod.test.ts` for the full set of translated checks.
|
||||
|
||||
## Compatibility rule
|
||||
|
||||
During migration, route validators, tool parameters, and any existing Zod-based boundary should consume the derived `.zod` schema instead of maintaining a second hand-written Zod schema.
|
||||
During migration, route validators, tool parameters, and any existing
|
||||
Zod-based boundary should consume the derived `.zod` schema instead of
|
||||
maintaining a second hand-written Zod schema.
|
||||
|
||||
The default should be:
|
||||
|
||||
- Effect Schema owns the type
|
||||
- `.zod` exists only as a compatibility surface
|
||||
- new domain models should not start Zod-first unless there is a concrete boundary-specific need
|
||||
- new domain models should not start Zod-first unless there is a concrete
|
||||
boundary-specific need
|
||||
|
||||
## When Zod can stay
|
||||
|
||||
It is fine to keep a Zod-native schema temporarily when:
|
||||
|
||||
- the type is only used at an HTTP or tool boundary
|
||||
- the type is only used at an HTTP or tool boundary and is not reused elsewhere
|
||||
- the validator depends on Zod-only transforms or behavior not yet covered by `zod()`
|
||||
- the migration would force unrelated churn across a large call graph
|
||||
|
||||
When this happens, prefer leaving a short note or TODO rather than silently creating a parallel schema source of truth.
|
||||
When this happens, prefer leaving a short note or TODO rather than silently
|
||||
creating a parallel schema source of truth.
|
||||
|
||||
## Escape hatches
|
||||
|
||||
The walker in `@/util/effect-zod` exposes three explicit escape hatches for
|
||||
cases the pure-Schema path cannot express. Each one stays in the codebase
|
||||
only as long as its upstream or local dependency requires it — inline
|
||||
comments document when each can be deleted.
|
||||
|
||||
### `ZodOverride` annotation
|
||||
|
||||
Replaces the entire derivation with a hand-crafted zod schema. Used when:
|
||||
|
||||
- the target carries external `$ref` metadata (e.g.
|
||||
`config/model-id.ts` points at `https://models.dev/...`)
|
||||
- the target is a zod-only schema that cannot yet be expressed as Schema
|
||||
(e.g. `ConfigAgent.Info`, `ConfigPermission.Info`, `Log.Level`)
|
||||
|
||||
### `ZodPreprocess` annotation
|
||||
|
||||
Wraps the derived zod schema with `z.preprocess(fn, inner)`. Used by
|
||||
`config/permission.ts` to inject `__originalKeys` before parsing, because
|
||||
`Schema.StructWithRest` canonicalises output (known fields first, catchall
|
||||
after) and destroys the user's original property order — which permission
|
||||
rule precedence depends on.
|
||||
|
||||
Tracked upstream as `effect:core/wlh553`: "Schema: add preserveInputOrder
|
||||
(or pre-parse hook) for open structs." Once that lands, `ZodPreprocess` and
|
||||
the `__originalKeys` hack can both be deleted.
|
||||
|
||||
### Local `DeepMutable<T>` in `config/config.ts`
|
||||
|
||||
`Schema.Struct` produces `readonly` types. Some consumer code (notably the
|
||||
`Config` service) mutates `Info` objects directly, so a readonly-stripping
|
||||
utility is needed when casting the derived zod schema's output type.
|
||||
|
||||
`Types.DeepMutable` from effect-smol would be a drop-in, but it widens
|
||||
`unknown` to `{}` in the fallback branch — a bug that affects any schema
|
||||
using `Schema.Record(String, Schema.Unknown)`.
|
||||
|
||||
Tracked upstream as `effect:core/x228my`: "Types.DeepMutable widens unknown
|
||||
to `{}`." Once that lands, the local `DeepMutable` copy can be deleted and
|
||||
`Types.DeepMutable` used directly.
|
||||
|
||||
## Ordering
|
||||
|
||||
@@ -81,19 +147,179 @@ Migrate in this order:
|
||||
4. Service-local internal models
|
||||
5. Route and tool boundary validators that can switch to `.zod`
|
||||
|
||||
This keeps shared types canonical first and makes boundary updates mostly mechanical.
|
||||
This keeps shared types canonical first and makes boundary updates mostly
|
||||
mechanical.
|
||||
|
||||
## Checklist
|
||||
## Progress tracker
|
||||
|
||||
- [ ] Shared `schema.ts` leaf models are Effect Schema-first
|
||||
- [ ] Exported `Info` / `Input` / `Output` types use `Schema.Class` where appropriate
|
||||
- [ ] Domain errors use `Schema.TaggedErrorClass`
|
||||
- [ ] Migrated types expose `.zod` for back compatibility
|
||||
- [ ] Route and tool validators consume derived `.zod` instead of duplicate Zod definitions
|
||||
- [ ] New domain models default to Effect Schema first
|
||||
### `src/config/` ✅ complete
|
||||
|
||||
All of `packages/opencode/src/config/` has been migrated. Files that still
|
||||
import `z` do so only for local `ZodOverride` bridges or for `z.ZodType`
|
||||
type annotations — the `export const <Info|Spec>` values are all Effect
|
||||
Schema at source.
|
||||
|
||||
- [x] skills, formatter, console-state, mcp, lsp, permission (leaves), model-id, command, plugin, provider
|
||||
- [x] server, layout
|
||||
- [x] keybinds
|
||||
- [x] permission#Info
|
||||
- [x] agent
|
||||
- [x] config.ts root
|
||||
|
||||
### `src/*/schema.ts` leaf modules
|
||||
|
||||
These are the highest-priority next targets. Each is a small, self-contained
|
||||
schema module with a clear domain.
|
||||
|
||||
- [ ] `src/control-plane/schema.ts`
|
||||
- [ ] `src/permission/schema.ts`
|
||||
- [ ] `src/project/schema.ts`
|
||||
- [ ] `src/provider/schema.ts`
|
||||
- [ ] `src/pty/schema.ts`
|
||||
- [ ] `src/question/schema.ts`
|
||||
- [ ] `src/session/schema.ts`
|
||||
- [ ] `src/sync/schema.ts`
|
||||
- [ ] `src/tool/schema.ts`
|
||||
|
||||
### Session domain
|
||||
|
||||
Major cluster. Message + event types flow through the SSE API and every SDK
|
||||
output, so byte-identical SDK surface is critical.
|
||||
|
||||
- [ ] `src/session/compaction.ts`
|
||||
- [ ] `src/session/message-v2.ts`
|
||||
- [ ] `src/session/message.ts`
|
||||
- [ ] `src/session/prompt.ts`
|
||||
- [ ] `src/session/revert.ts`
|
||||
- [ ] `src/session/session.ts`
|
||||
- [ ] `src/session/status.ts`
|
||||
- [ ] `src/session/summary.ts`
|
||||
- [ ] `src/session/todo.ts`
|
||||
|
||||
### Provider domain
|
||||
|
||||
- [ ] `src/provider/auth.ts`
|
||||
- [ ] `src/provider/models.ts`
|
||||
- [ ] `src/provider/provider.ts`
|
||||
|
||||
### Tool schemas
|
||||
|
||||
Each tool declares its parameters via a zod schema. Tools are consumed by
|
||||
both the in-process runtime and the AI SDK's tool-calling layer, so the
|
||||
emitted JSON Schema must stay byte-identical.
|
||||
|
||||
- [ ] `src/tool/apply_patch.ts`
|
||||
- [ ] `src/tool/bash.ts`
|
||||
- [ ] `src/tool/codesearch.ts`
|
||||
- [ ] `src/tool/edit.ts`
|
||||
- [ ] `src/tool/glob.ts`
|
||||
- [ ] `src/tool/grep.ts`
|
||||
- [ ] `src/tool/invalid.ts`
|
||||
- [ ] `src/tool/lsp.ts`
|
||||
- [ ] `src/tool/multiedit.ts`
|
||||
- [ ] `src/tool/plan.ts`
|
||||
- [ ] `src/tool/question.ts`
|
||||
- [ ] `src/tool/read.ts`
|
||||
- [ ] `src/tool/registry.ts`
|
||||
- [ ] `src/tool/skill.ts`
|
||||
- [ ] `src/tool/task.ts`
|
||||
- [ ] `src/tool/todo.ts`
|
||||
- [ ] `src/tool/tool.ts`
|
||||
- [ ] `src/tool/webfetch.ts`
|
||||
- [ ] `src/tool/websearch.ts`
|
||||
- [ ] `src/tool/write.ts`
|
||||
|
||||
### HTTP route boundaries
|
||||
|
||||
Every file in `src/server/routes/` uses hono-openapi with zod validators for
|
||||
route inputs/outputs. Migrating these individually is the last step; most
|
||||
will switch to `.zod` derived from the Schema-migrated domain types above,
|
||||
which means touching them is largely mechanical once the domain side is
|
||||
done.
|
||||
|
||||
- [ ] `src/server/error.ts`
|
||||
- [ ] `src/server/event.ts`
|
||||
- [ ] `src/server/projectors.ts`
|
||||
- [ ] `src/server/routes/control/index.ts`
|
||||
- [ ] `src/server/routes/control/workspace.ts`
|
||||
- [ ] `src/server/routes/global.ts`
|
||||
- [ ] `src/server/routes/instance/index.ts`
|
||||
- [ ] `src/server/routes/instance/config.ts`
|
||||
- [ ] `src/server/routes/instance/event.ts`
|
||||
- [ ] `src/server/routes/instance/experimental.ts`
|
||||
- [ ] `src/server/routes/instance/file.ts`
|
||||
- [ ] `src/server/routes/instance/mcp.ts`
|
||||
- [ ] `src/server/routes/instance/permission.ts`
|
||||
- [ ] `src/server/routes/instance/project.ts`
|
||||
- [ ] `src/server/routes/instance/provider.ts`
|
||||
- [ ] `src/server/routes/instance/pty.ts`
|
||||
- [ ] `src/server/routes/instance/question.ts`
|
||||
- [ ] `src/server/routes/instance/session.ts`
|
||||
- [ ] `src/server/routes/instance/sync.ts`
|
||||
- [ ] `src/server/routes/instance/tui.ts`
|
||||
|
||||
The bigger prize for this group is the `@effect/platform` HTTP migration
|
||||
described in `specs/effect/http-api.md`. Once that lands, every one of
|
||||
these files changes shape entirely (`HttpApi.endpoint(...)` and friends),
|
||||
so the Schema-first domain types become a prerequisite rather than a
|
||||
sibling task.
|
||||
|
||||
### Everything else
|
||||
|
||||
Small / shared / control-plane / CLI. Mostly independent; can be done
|
||||
piecewise.
|
||||
|
||||
- [ ] `src/acp/agent.ts`
|
||||
- [ ] `src/agent/agent.ts`
|
||||
- [ ] `src/bus/bus-event.ts`
|
||||
- [ ] `src/bus/index.ts`
|
||||
- [ ] `src/cli/cmd/tui/config/tui-migrate.ts`
|
||||
- [ ] `src/cli/cmd/tui/config/tui-schema.ts`
|
||||
- [ ] `src/cli/cmd/tui/config/tui.ts`
|
||||
- [ ] `src/cli/cmd/tui/event.ts`
|
||||
- [ ] `src/cli/ui.ts`
|
||||
- [ ] `src/command/index.ts`
|
||||
- [ ] `src/control-plane/adaptors/worktree.ts`
|
||||
- [ ] `src/control-plane/types.ts`
|
||||
- [ ] `src/control-plane/workspace.ts`
|
||||
- [ ] `src/file/index.ts`
|
||||
- [ ] `src/file/ripgrep.ts`
|
||||
- [ ] `src/file/watcher.ts`
|
||||
- [ ] `src/format/index.ts`
|
||||
- [ ] `src/id/id.ts`
|
||||
- [ ] `src/ide/index.ts`
|
||||
- [ ] `src/installation/index.ts`
|
||||
- [ ] `src/lsp/client.ts`
|
||||
- [ ] `src/lsp/lsp.ts`
|
||||
- [ ] `src/mcp/auth.ts`
|
||||
- [ ] `src/patch/index.ts`
|
||||
- [ ] `src/plugin/github-copilot/models.ts`
|
||||
- [ ] `src/project/project.ts`
|
||||
- [ ] `src/project/vcs.ts`
|
||||
- [ ] `src/pty/index.ts`
|
||||
- [ ] `src/skill/index.ts`
|
||||
- [ ] `src/snapshot/index.ts`
|
||||
- [ ] `src/storage/db.ts`
|
||||
- [ ] `src/storage/storage.ts`
|
||||
- [ ] `src/sync/index.ts`
|
||||
- [ ] `src/util/fn.ts`
|
||||
- [ ] `src/util/log.ts`
|
||||
- [ ] `src/util/update-schema.ts`
|
||||
- [ ] `src/worktree/index.ts`
|
||||
|
||||
### Do-not-migrate
|
||||
|
||||
- `src/util/effect-zod.ts` — the walker itself. Stays zod-importing forever
|
||||
(it's what emits zod from Schema). Goes away only when the `.zod`
|
||||
compatibility layer is no longer needed anywhere.
|
||||
|
||||
## Notes
|
||||
|
||||
- Use `@/util/effect-zod` for all Schema -> Zod conversion.
|
||||
- Prefer one canonical schema definition. Avoid maintaining parallel Zod and Effect definitions for the same domain type.
|
||||
- Keep the migration incremental. Converting the domain model first is more valuable than converting every boundary in the same change.
|
||||
- Use `@/util/effect-zod` for all Schema → Zod conversion.
|
||||
- Prefer one canonical schema definition. Avoid maintaining parallel Zod and
|
||||
Effect definitions for the same domain type.
|
||||
- Keep the migration incremental. Converting the domain model first is more
|
||||
valuable than converting every boundary in the same change.
|
||||
- Every migrated file should leave the generated SDK output (`packages/sdk/
|
||||
openapi.json` and `packages/sdk/js/src/v2/gen/types.gen.ts`) byte-identical
|
||||
unless the change is deliberately user-visible.
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
You are a helpful AI assistant tasked with summarizing conversations.
|
||||
|
||||
When asked to summarize, provide a detailed but concise summary of the conversation.
|
||||
When asked to summarize, provide a detailed but concise summary of the older conversation history.
|
||||
The most recent turns may be preserved verbatim outside your summary, so focus on information that would still be needed to continue the work with that recent context available.
|
||||
Focus on information that would be helpful for continuing the conversation, including:
|
||||
- What was done
|
||||
- What is currently being worked on
|
||||
|
||||
@@ -25,7 +25,19 @@ export const GenerateCommand = {
|
||||
]
|
||||
}
|
||||
}
|
||||
const json = JSON.stringify(specs, null, 2)
|
||||
const raw = JSON.stringify(specs, null, 2)
|
||||
|
||||
// Format through prettier so output is byte-identical to committed file
|
||||
// regardless of whether ./script/format.ts runs afterward.
|
||||
const prettier = await import("prettier")
|
||||
const babel = await import("prettier/plugins/babel")
|
||||
const estree = await import("prettier/plugins/estree")
|
||||
const format = prettier.format ?? prettier.default?.format
|
||||
const json = await format(raw, {
|
||||
parser: "json",
|
||||
plugins: [babel.default ?? babel, estree.default ?? estree],
|
||||
printWidth: 120,
|
||||
})
|
||||
|
||||
// Wait for stdout to finish writing before process.exit() is called
|
||||
await new Promise<void>((resolve, reject) => {
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { render, TimeToFirstDraw, useKeyboard, useRenderer, useTerminalDimensions } from "@opentui/solid"
|
||||
import * as Clipboard from "@tui/util/clipboard"
|
||||
import * as Selection from "@tui/util/selection"
|
||||
import * as Terminal from "@tui/util/terminal"
|
||||
import { createCliRenderer, MouseButton, type CliRendererConfig } from "@opentui/core"
|
||||
import { RouteProvider, useRoute } from "@tui/context/route"
|
||||
import {
|
||||
@@ -120,12 +119,6 @@ export function tui(input: {
|
||||
const unguard = win32InstallCtrlCGuard()
|
||||
win32DisableProcessedInput()
|
||||
|
||||
const mode = await Terminal.getTerminalBackgroundColor()
|
||||
|
||||
// Re-clear after getTerminalBackgroundColor() — setRawMode(false) restores
|
||||
// the original console mode which re-enables ENABLE_PROCESSED_INPUT.
|
||||
win32DisableProcessedInput()
|
||||
|
||||
const onExit = async () => {
|
||||
unguard?.()
|
||||
resolve()
|
||||
@@ -136,6 +129,7 @@ export function tui(input: {
|
||||
}
|
||||
|
||||
const renderer = await createCliRenderer(rendererConfig(input.config))
|
||||
const mode = (await renderer.waitForThemeMode(1000)) ?? "dark"
|
||||
|
||||
await render(() => {
|
||||
return (
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
import { Global } from "@/global"
|
||||
import { Filesystem } from "@/util"
|
||||
import { Flock } from "@opencode-ai/shared/util/flock"
|
||||
import { rename, rm } from "fs/promises"
|
||||
import { createSignal, type Setter } from "solid-js"
|
||||
import { createStore } from "solid-js/store"
|
||||
import { createStore, unwrap } from "solid-js/store"
|
||||
import { createSimpleContext } from "./helper"
|
||||
import path from "path"
|
||||
|
||||
@@ -11,12 +13,29 @@ export const { use: useKV, provider: KVProvider } = createSimpleContext({
|
||||
const [ready, setReady] = createSignal(false)
|
||||
const [store, setStore] = createStore<Record<string, any>>()
|
||||
const filePath = path.join(Global.Path.state, "kv.json")
|
||||
const lock = `tui-kv:${filePath}`
|
||||
// Queue same-process writes so rapid updates persist in order.
|
||||
let write = Promise.resolve()
|
||||
|
||||
Filesystem.readJson<Record<string, any>>(filePath)
|
||||
// Write to a temp file first so kv.json is only replaced once the JSON is complete, avoiding partial writes if shutdown interrupts persistence.
|
||||
function writeSnapshot(snapshot: Record<string, any>) {
|
||||
const tempPath = `${filePath}.${process.pid}.${Date.now()}.tmp`
|
||||
return Filesystem.writeJson(tempPath, snapshot)
|
||||
.then(() => rename(tempPath, filePath))
|
||||
.catch(async (error) => {
|
||||
await rm(tempPath, { force: true }).catch(() => undefined)
|
||||
throw error
|
||||
})
|
||||
}
|
||||
|
||||
// Read under the same lock used for writes because kv.json is shared across processes.
|
||||
Flock.withLock(lock, () => Filesystem.readJson<Record<string, any>>(filePath))
|
||||
.then((x) => {
|
||||
setStore(x)
|
||||
})
|
||||
.catch(() => {})
|
||||
.catch((error) => {
|
||||
console.error("Failed to read KV state", { filePath, error })
|
||||
})
|
||||
.finally(() => {
|
||||
setReady(true)
|
||||
})
|
||||
@@ -44,7 +63,12 @@ export const { use: useKV, provider: KVProvider } = createSimpleContext({
|
||||
},
|
||||
set(key: string, value: any) {
|
||||
setStore(key, value)
|
||||
void Filesystem.writeJson(filePath, store)
|
||||
const snapshot = structuredClone(unwrap(store))
|
||||
write = write
|
||||
.then(() => Flock.withLock(lock, () => writeSnapshot(snapshot)))
|
||||
.catch((error) => {
|
||||
console.error("Failed to write KV state", { filePath, error })
|
||||
})
|
||||
},
|
||||
}
|
||||
return result
|
||||
|
||||
@@ -467,7 +467,6 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({
|
||||
return store.status
|
||||
},
|
||||
get ready() {
|
||||
return true
|
||||
if (process.env.OPENCODE_FAST_BOOT) return true
|
||||
return store.status !== "loading"
|
||||
},
|
||||
|
||||
@@ -314,8 +314,11 @@ export const { use: useTheme, provider: ThemeProvider } = createSimpleContext({
|
||||
setStore(
|
||||
produce((draft) => {
|
||||
const lock = pick(kv.get("theme_mode_lock"))
|
||||
const mode = pick(kv.get("theme_mode", props.mode))
|
||||
draft.mode = lock ?? mode ?? props.mode
|
||||
const mode = lock ?? props.mode
|
||||
if (!lock && pick(kv.get("theme_mode")) !== undefined) {
|
||||
kv.set("theme_mode", undefined)
|
||||
}
|
||||
draft.mode = mode
|
||||
draft.lock = lock
|
||||
const active = config.theme ?? kv.get("theme", "opencode")
|
||||
draft.active = typeof active === "string" ? active : "opencode"
|
||||
@@ -373,7 +376,7 @@ export const { use: useTheme, provider: ThemeProvider } = createSimpleContext({
|
||||
}
|
||||
|
||||
function apply(mode: "dark" | "light") {
|
||||
kv.set("theme_mode", mode)
|
||||
if (store.lock !== undefined) kv.set("theme_mode", mode)
|
||||
if (store.mode === mode) return
|
||||
setStore("mode", mode)
|
||||
renderer.clearPaletteCache()
|
||||
@@ -389,6 +392,7 @@ export const { use: useTheme, provider: ThemeProvider } = createSimpleContext({
|
||||
function free() {
|
||||
setStore("lock", undefined)
|
||||
kv.set("theme_mode_lock", undefined)
|
||||
kv.set("theme_mode", undefined)
|
||||
const mode = renderer.themeMode
|
||||
if (mode) apply(mode)
|
||||
}
|
||||
@@ -397,7 +401,7 @@ export const { use: useTheme, provider: ThemeProvider } = createSimpleContext({
|
||||
if (store.lock) return
|
||||
apply(mode)
|
||||
}
|
||||
// renderer.on(CliRenderEvents.THEME_MODE, handle)
|
||||
renderer.on(CliRenderEvents.THEME_MODE, handle)
|
||||
|
||||
const refresh = () => {
|
||||
renderer.clearPaletteCache()
|
||||
|
||||
@@ -5,11 +5,11 @@ import type { TextPart } from "@opencode-ai/sdk/v2"
|
||||
import { Locale } from "@/util"
|
||||
import { useSDK } from "@tui/context/sdk"
|
||||
import { useRoute } from "@tui/context/route"
|
||||
import { useDialog } from "../../ui/dialog"
|
||||
import { useDialog, type DialogContext } from "../../ui/dialog"
|
||||
import type { PromptInfo } from "@tui/component/prompt/history"
|
||||
import { strip } from "@tui/component/prompt/part"
|
||||
|
||||
export function DialogForkFromTimeline(props: { sessionID: string; onMove: (messageID: string) => void }) {
|
||||
export function DialogForkFromTimeline(props: { sessionID: string; onMove: (messageID?: string) => void }) {
|
||||
const sync = useSync()
|
||||
const dialog = useDialog()
|
||||
const sdk = useSDK()
|
||||
@@ -19,9 +19,21 @@ export function DialogForkFromTimeline(props: { sessionID: string; onMove: (mess
|
||||
dialog.setSize("large")
|
||||
})
|
||||
|
||||
const options = createMemo((): DialogSelectOption<string>[] => {
|
||||
const options = createMemo((): DialogSelectOption<string | undefined>[] => {
|
||||
const messages = sync.data.message[props.sessionID] ?? []
|
||||
const result = [] as DialogSelectOption<string>[]
|
||||
const fullSession = {
|
||||
title: "Full session",
|
||||
value: undefined,
|
||||
onSelect: async (dialog: DialogContext) => {
|
||||
const forked = await sdk.client.session.fork({ sessionID: props.sessionID })
|
||||
route.navigate({
|
||||
sessionID: forked.data!.id,
|
||||
type: "session",
|
||||
})
|
||||
dialog.clear()
|
||||
},
|
||||
} satisfies DialogSelectOption<string | undefined>
|
||||
const result = [] as DialogSelectOption<string | undefined>[]
|
||||
for (const message of messages) {
|
||||
if (message.role !== "user") continue
|
||||
const part = (sync.data.part[message.id] ?? []).find(
|
||||
@@ -57,9 +69,8 @@ export function DialogForkFromTimeline(props: { sessionID: string; onMove: (mess
|
||||
},
|
||||
})
|
||||
}
|
||||
result.reverse()
|
||||
return result
|
||||
return [fullSession, ...result.reverse()]
|
||||
})
|
||||
|
||||
return <DialogSelect onMove={(option) => props.onMove(option.value)} title="Fork from message" options={options()} />
|
||||
return <DialogSelect onMove={(option) => props.onMove(option.value)} title="Fork session" options={options()} />
|
||||
}
|
||||
|
||||
@@ -451,7 +451,7 @@ export function Session() {
|
||||
},
|
||||
},
|
||||
{
|
||||
title: "Fork from message",
|
||||
title: "Fork session",
|
||||
value: "session.fork",
|
||||
keybind: "session_fork",
|
||||
category: "Session",
|
||||
@@ -462,6 +462,7 @@ export function Session() {
|
||||
dialog.replace(() => (
|
||||
<DialogForkFromTimeline
|
||||
onMove={(messageID) => {
|
||||
if (!messageID) return
|
||||
const child = scroll.getChildren().find((child) => {
|
||||
return child.id === messageID
|
||||
})
|
||||
|
||||
@@ -17,12 +17,6 @@ function parse(color: string): RGBA | null {
|
||||
return null
|
||||
}
|
||||
|
||||
function mode(bg: RGBA | null): "dark" | "light" {
|
||||
if (!bg) return "dark"
|
||||
const luminance = (0.299 * bg.r + 0.587 * bg.g + 0.114 * bg.b) / 255
|
||||
return luminance > 0.5 ? "light" : "dark"
|
||||
}
|
||||
|
||||
/**
|
||||
* Query terminal colors including background, foreground, and palette (0-15).
|
||||
* Uses OSC escape sequences to retrieve actual terminal color values.
|
||||
@@ -100,36 +94,3 @@ export async function colors(): Promise<{
|
||||
}, 1000)
|
||||
})
|
||||
}
|
||||
|
||||
// Keep startup mode detection separate from `colors()`: the TUI boot path only
|
||||
// needs OSC 11 and should resolve on the first background response instead of
|
||||
// waiting on the full palette query used by system theme generation.
|
||||
export async function getTerminalBackgroundColor(): Promise<"dark" | "light"> {
|
||||
if (!process.stdin.isTTY) return "dark"
|
||||
|
||||
return new Promise((resolve) => {
|
||||
let timeout: NodeJS.Timeout
|
||||
|
||||
const cleanup = () => {
|
||||
process.stdin.setRawMode(false)
|
||||
process.stdin.removeListener("data", handler)
|
||||
clearTimeout(timeout)
|
||||
}
|
||||
|
||||
const handler = (data: Buffer) => {
|
||||
const match = data.toString().match(/\x1b]11;([^\x07\x1b]+)/)
|
||||
if (!match) return
|
||||
cleanup()
|
||||
resolve(mode(parse(match[1])))
|
||||
}
|
||||
|
||||
process.stdin.setRawMode(true)
|
||||
process.stdin.on("data", handler)
|
||||
process.stdout.write("\x1b]11;?\x07")
|
||||
|
||||
timeout = setTimeout(() => {
|
||||
cleanup()
|
||||
resolve("dark")
|
||||
}, 1000)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
export * as ConfigAgent from "./agent"
|
||||
|
||||
import { Log } from "../util"
|
||||
import { Schema } from "effect"
|
||||
import z from "zod"
|
||||
import { Bus } from "@/bus"
|
||||
import { zod, ZodOverride } from "@/util/effect-zod"
|
||||
import { Log } from "../util"
|
||||
import { NamedError } from "@opencode-ai/shared/util/error"
|
||||
import { Glob } from "@opencode-ai/shared/util/glob"
|
||||
import { Bus } from "@/bus"
|
||||
import { configEntryNameFromPath } from "./entry-name"
|
||||
import { InvalidError } from "./error"
|
||||
import * as ConfigMarkdown from "./markdown"
|
||||
@@ -13,89 +15,102 @@ import { ConfigPermission } from "./permission"
|
||||
|
||||
const log = Log.create({ service: "config" })
|
||||
|
||||
export const Info = z
|
||||
.object({
|
||||
model: ConfigModelID.zod.optional(),
|
||||
variant: z
|
||||
.string()
|
||||
.optional()
|
||||
.describe("Default model variant for this agent (applies only when using the agent's configured model)."),
|
||||
temperature: z.number().optional(),
|
||||
top_p: z.number().optional(),
|
||||
prompt: z.string().optional(),
|
||||
tools: z.record(z.string(), z.boolean()).optional().describe("@deprecated Use 'permission' field instead"),
|
||||
disable: z.boolean().optional(),
|
||||
description: z.string().optional().describe("Description of when to use the agent"),
|
||||
mode: z.enum(["subagent", "primary", "all"]).optional(),
|
||||
hidden: z
|
||||
.boolean()
|
||||
.optional()
|
||||
.describe("Hide this subagent from the @ autocomplete menu (default: false, only applies to mode: subagent)"),
|
||||
options: z.record(z.string(), z.any()).optional(),
|
||||
color: z
|
||||
.union([
|
||||
z.string().regex(/^#[0-9a-fA-F]{6}$/, "Invalid hex color format"),
|
||||
z.enum(["primary", "secondary", "accent", "success", "warning", "error", "info"]),
|
||||
])
|
||||
.optional()
|
||||
.describe("Hex color code (e.g., #FF5733) or theme color (e.g., primary)"),
|
||||
steps: z
|
||||
.number()
|
||||
.int()
|
||||
.positive()
|
||||
.optional()
|
||||
.describe("Maximum number of agentic iterations before forcing text-only response"),
|
||||
maxSteps: z.number().int().positive().optional().describe("@deprecated Use 'steps' field instead."),
|
||||
permission: ConfigPermission.Info.optional(),
|
||||
})
|
||||
.catchall(z.any())
|
||||
.transform((agent, _ctx) => {
|
||||
const knownKeys = new Set([
|
||||
"name",
|
||||
"model",
|
||||
"variant",
|
||||
"prompt",
|
||||
"description",
|
||||
"temperature",
|
||||
"top_p",
|
||||
"mode",
|
||||
"hidden",
|
||||
"color",
|
||||
"steps",
|
||||
"maxSteps",
|
||||
"options",
|
||||
"permission",
|
||||
"disable",
|
||||
"tools",
|
||||
])
|
||||
const PositiveInt = Schema.Number.check(Schema.isInt()).check(Schema.isGreaterThan(0))
|
||||
|
||||
const options: Record<string, unknown> = { ...agent.options }
|
||||
for (const [key, value] of Object.entries(agent)) {
|
||||
if (!knownKeys.has(key)) options[key] = value
|
||||
const Color = Schema.Union([
|
||||
Schema.String.check(Schema.isPattern(/^#[0-9a-fA-F]{6}$/)),
|
||||
Schema.Literals(["primary", "secondary", "accent", "success", "warning", "error", "info"]),
|
||||
])
|
||||
|
||||
// ConfigPermission.Info is a zod schema (its `.preprocess(...).transform(...)`
|
||||
// shape lives outside the Effect Schema type system), so the walker reaches it
|
||||
// via ZodOverride rather than a pure Schema reference. This preserves the
|
||||
// `$ref: PermissionConfig` emitted in openapi.json.
|
||||
const PermissionRef = Schema.Any.annotate({ [ZodOverride]: ConfigPermission.Info })
|
||||
|
||||
const AgentSchema = Schema.StructWithRest(
|
||||
Schema.Struct({
|
||||
model: Schema.optional(ConfigModelID),
|
||||
variant: Schema.optional(Schema.String).annotate({
|
||||
description: "Default model variant for this agent (applies only when using the agent's configured model).",
|
||||
}),
|
||||
temperature: Schema.optional(Schema.Number),
|
||||
top_p: Schema.optional(Schema.Number),
|
||||
prompt: Schema.optional(Schema.String),
|
||||
tools: Schema.optional(Schema.Record(Schema.String, Schema.Boolean)).annotate({
|
||||
description: "@deprecated Use 'permission' field instead",
|
||||
}),
|
||||
disable: Schema.optional(Schema.Boolean),
|
||||
description: Schema.optional(Schema.String).annotate({ description: "Description of when to use the agent" }),
|
||||
mode: Schema.optional(Schema.Literals(["subagent", "primary", "all"])),
|
||||
hidden: Schema.optional(Schema.Boolean).annotate({
|
||||
description: "Hide this subagent from the @ autocomplete menu (default: false, only applies to mode: subagent)",
|
||||
}),
|
||||
options: Schema.optional(Schema.Record(Schema.String, Schema.Any)),
|
||||
color: Schema.optional(Color).annotate({
|
||||
description: "Hex color code (e.g., #FF5733) or theme color (e.g., primary)",
|
||||
}),
|
||||
steps: Schema.optional(PositiveInt).annotate({
|
||||
description: "Maximum number of agentic iterations before forcing text-only response",
|
||||
}),
|
||||
maxSteps: Schema.optional(PositiveInt).annotate({ description: "@deprecated Use 'steps' field instead." }),
|
||||
permission: Schema.optional(PermissionRef),
|
||||
}),
|
||||
[Schema.Record(Schema.String, Schema.Any)],
|
||||
)
|
||||
|
||||
const KNOWN_KEYS = new Set([
|
||||
"name",
|
||||
"model",
|
||||
"variant",
|
||||
"prompt",
|
||||
"description",
|
||||
"temperature",
|
||||
"top_p",
|
||||
"mode",
|
||||
"hidden",
|
||||
"color",
|
||||
"steps",
|
||||
"maxSteps",
|
||||
"options",
|
||||
"permission",
|
||||
"disable",
|
||||
"tools",
|
||||
])
|
||||
|
||||
// Post-parse normalisation:
|
||||
// - Promote any unknown-but-present keys into `options` so they survive the
|
||||
// round-trip in a well-known field.
|
||||
// - Translate the deprecated `tools: { name: boolean }` map into the new
|
||||
// `permission` shape (write-adjacent tools collapse into `permission.edit`).
|
||||
// - Coalesce `steps ?? maxSteps` so downstream can ignore the deprecated alias.
|
||||
const normalize = (agent: z.infer<typeof Info>) => {
|
||||
const options: Record<string, unknown> = { ...agent.options }
|
||||
for (const [key, value] of Object.entries(agent)) {
|
||||
if (!KNOWN_KEYS.has(key)) options[key] = value
|
||||
}
|
||||
|
||||
const permission: ConfigPermission.Info = {}
|
||||
for (const [tool, enabled] of Object.entries(agent.tools ?? {})) {
|
||||
const action = enabled ? "allow" : "deny"
|
||||
if (tool === "write" || tool === "edit" || tool === "patch" || tool === "multiedit") {
|
||||
permission.edit = action
|
||||
continue
|
||||
}
|
||||
permission[tool] = action
|
||||
}
|
||||
globalThis.Object.assign(permission, agent.permission)
|
||||
|
||||
const permission: ConfigPermission.Info = {}
|
||||
for (const [tool, enabled] of Object.entries(agent.tools ?? {})) {
|
||||
const action = enabled ? "allow" : "deny"
|
||||
if (tool === "write" || tool === "edit" || tool === "patch" || tool === "multiedit") {
|
||||
permission.edit = action
|
||||
continue
|
||||
}
|
||||
permission[tool] = action
|
||||
}
|
||||
Object.assign(permission, agent.permission)
|
||||
return { ...agent, options, permission, steps: agent.steps ?? agent.maxSteps }
|
||||
}
|
||||
|
||||
const steps = agent.steps ?? agent.maxSteps
|
||||
|
||||
return { ...agent, options, permission, steps } as typeof agent & {
|
||||
options?: Record<string, unknown>
|
||||
permission?: ConfigPermission.Info
|
||||
steps?: number
|
||||
}
|
||||
})
|
||||
.meta({
|
||||
ref: "AgentConfig",
|
||||
})
|
||||
export const Info = zod(AgentSchema).transform(normalize).meta({ ref: "AgentConfig" }) as unknown as z.ZodType<
|
||||
Omit<z.infer<ReturnType<typeof zod<typeof AgentSchema>>>, "options" | "permission" | "steps"> & {
|
||||
options?: Record<string, unknown>
|
||||
permission?: ConfigPermission.Info
|
||||
steps?: number
|
||||
}
|
||||
>
|
||||
export type Info = z.infer<typeof Info>
|
||||
|
||||
export async function load(dir: string) {
|
||||
|
||||
@@ -21,9 +21,10 @@ import { isRecord } from "@/util/record"
|
||||
import type { ConsoleState } from "./console-state"
|
||||
import { AppFileSystem } from "@opencode-ai/shared/filesystem"
|
||||
import { InstanceState } from "@/effect"
|
||||
import { Context, Duration, Effect, Exit, Fiber, Layer, Option } from "effect"
|
||||
import { Context, Duration, Effect, Exit, Fiber, Layer, Option, Schema } from "effect"
|
||||
import { EffectFlock } from "@opencode-ai/shared/util/effect-flock"
|
||||
import { InstanceRef } from "@/effect/instance-ref"
|
||||
import { zod, ZodOverride } from "@/util/effect-zod"
|
||||
import { ConfigAgent } from "./agent"
|
||||
import { ConfigCommand } from "./command"
|
||||
import { ConfigFormatter } from "./formatter"
|
||||
@@ -79,152 +80,189 @@ export const Server = ConfigServer.Server.zod
|
||||
export const Layout = ConfigLayout.Layout.zod
|
||||
export type Layout = ConfigLayout.Layout
|
||||
|
||||
export const Info = z
|
||||
.object({
|
||||
$schema: z.string().optional().describe("JSON schema reference for configuration validation"),
|
||||
logLevel: Log.Level.optional().describe("Log level"),
|
||||
server: Server.optional().describe("Server configuration for opencode serve and web commands"),
|
||||
command: z
|
||||
.record(z.string(), ConfigCommand.Info.zod)
|
||||
.optional()
|
||||
.describe("Command configuration, see https://opencode.ai/docs/commands"),
|
||||
skills: ConfigSkills.Info.zod.optional().describe("Additional skill folder paths"),
|
||||
watcher: z
|
||||
.object({
|
||||
ignore: z.array(z.string()).optional(),
|
||||
})
|
||||
.optional(),
|
||||
snapshot: z
|
||||
.boolean()
|
||||
.optional()
|
||||
.describe(
|
||||
"Enable or disable snapshot tracking. When false, filesystem snapshots are not recorded and undoing or reverting will not undo/redo file changes. Defaults to true.",
|
||||
),
|
||||
// User-facing plugin config is stored as Specs; provenance gets attached later while configs are merged.
|
||||
plugin: ConfigPlugin.Spec.zod.array().optional(),
|
||||
share: z
|
||||
.enum(["manual", "auto", "disabled"])
|
||||
.optional()
|
||||
.describe(
|
||||
"Control sharing behavior:'manual' allows manual sharing via commands, 'auto' enables automatic sharing, 'disabled' disables all sharing",
|
||||
),
|
||||
autoshare: z
|
||||
.boolean()
|
||||
.optional()
|
||||
.describe("@deprecated Use 'share' field instead. Share newly created sessions automatically"),
|
||||
autoupdate: z
|
||||
.union([z.boolean(), z.literal("notify")])
|
||||
.optional()
|
||||
.describe(
|
||||
"Automatically update to the latest version. Set to true to auto-update, false to disable, or 'notify' to show update notifications",
|
||||
),
|
||||
disabled_providers: z.array(z.string()).optional().describe("Disable providers that are loaded automatically"),
|
||||
enabled_providers: z
|
||||
.array(z.string())
|
||||
.optional()
|
||||
.describe("When set, ONLY these providers will be enabled. All other providers will be ignored"),
|
||||
model: ConfigModelID.zod.describe("Model to use in the format of provider/model, eg anthropic/claude-2").optional(),
|
||||
small_model: ConfigModelID.zod
|
||||
.describe("Small model to use for tasks like title generation in the format of provider/model")
|
||||
.optional(),
|
||||
default_agent: z
|
||||
.string()
|
||||
.optional()
|
||||
.describe(
|
||||
"Default agent to use when none is specified. Must be a primary agent. Falls back to 'build' if not set or if the specified agent is invalid.",
|
||||
),
|
||||
username: z.string().optional().describe("Custom username to display in conversations instead of system username"),
|
||||
mode: z
|
||||
.object({
|
||||
build: ConfigAgent.Info.optional(),
|
||||
plan: ConfigAgent.Info.optional(),
|
||||
})
|
||||
.catchall(ConfigAgent.Info)
|
||||
.optional()
|
||||
.describe("@deprecated Use `agent` field instead."),
|
||||
agent: z
|
||||
.object({
|
||||
// Schemas that still live at the zod layer (have .transform / .preprocess /
|
||||
// .meta not expressible in current Effect Schema) get referenced via a
|
||||
// ZodOverride-annotated Schema.Any. Walker sees the annotation and emits the
|
||||
// exact zod directly, preserving component $refs.
|
||||
const AgentRef = Schema.Any.annotate({ [ZodOverride]: ConfigAgent.Info })
|
||||
const PermissionRef = Schema.Any.annotate({ [ZodOverride]: ConfigPermission.Info })
|
||||
const LogLevelRef = Schema.Any.annotate({ [ZodOverride]: Log.Level })
|
||||
|
||||
const PositiveInt = Schema.Number.check(Schema.isInt()).check(Schema.isGreaterThan(0))
|
||||
const NonNegativeInt = Schema.Number.check(Schema.isInt()).check(Schema.isGreaterThanOrEqualTo(0))
|
||||
|
||||
const InfoSchema = Schema.Struct({
|
||||
$schema: Schema.optional(Schema.String).annotate({
|
||||
description: "JSON schema reference for configuration validation",
|
||||
}),
|
||||
logLevel: Schema.optional(LogLevelRef).annotate({ description: "Log level" }),
|
||||
server: Schema.optional(ConfigServer.Server).annotate({
|
||||
description: "Server configuration for opencode serve and web commands",
|
||||
}),
|
||||
command: Schema.optional(Schema.Record(Schema.String, ConfigCommand.Info)).annotate({
|
||||
description: "Command configuration, see https://opencode.ai/docs/commands",
|
||||
}),
|
||||
skills: Schema.optional(ConfigSkills.Info).annotate({ description: "Additional skill folder paths" }),
|
||||
watcher: Schema.optional(
|
||||
Schema.Struct({
|
||||
ignore: Schema.optional(Schema.mutable(Schema.Array(Schema.String))),
|
||||
}),
|
||||
),
|
||||
snapshot: Schema.optional(Schema.Boolean).annotate({
|
||||
description:
|
||||
"Enable or disable snapshot tracking. When false, filesystem snapshots are not recorded and undoing or reverting will not undo/redo file changes. Defaults to true.",
|
||||
}),
|
||||
// User-facing plugin config is stored as Specs; provenance gets attached later while configs are merged.
|
||||
plugin: Schema.optional(Schema.mutable(Schema.Array(ConfigPlugin.Spec))),
|
||||
share: Schema.optional(Schema.Literals(["manual", "auto", "disabled"])).annotate({
|
||||
description:
|
||||
"Control sharing behavior:'manual' allows manual sharing via commands, 'auto' enables automatic sharing, 'disabled' disables all sharing",
|
||||
}),
|
||||
autoshare: Schema.optional(Schema.Boolean).annotate({
|
||||
description: "@deprecated Use 'share' field instead. Share newly created sessions automatically",
|
||||
}),
|
||||
autoupdate: Schema.optional(Schema.Union([Schema.Boolean, Schema.Literal("notify")])).annotate({
|
||||
description:
|
||||
"Automatically update to the latest version. Set to true to auto-update, false to disable, or 'notify' to show update notifications",
|
||||
}),
|
||||
disabled_providers: Schema.optional(Schema.mutable(Schema.Array(Schema.String))).annotate({
|
||||
description: "Disable providers that are loaded automatically",
|
||||
}),
|
||||
enabled_providers: Schema.optional(Schema.mutable(Schema.Array(Schema.String))).annotate({
|
||||
description: "When set, ONLY these providers will be enabled. All other providers will be ignored",
|
||||
}),
|
||||
model: Schema.optional(ConfigModelID).annotate({
|
||||
description: "Model to use in the format of provider/model, eg anthropic/claude-2",
|
||||
}),
|
||||
small_model: Schema.optional(ConfigModelID).annotate({
|
||||
description: "Small model to use for tasks like title generation in the format of provider/model",
|
||||
}),
|
||||
default_agent: Schema.optional(Schema.String).annotate({
|
||||
description:
|
||||
"Default agent to use when none is specified. Must be a primary agent. Falls back to 'build' if not set or if the specified agent is invalid.",
|
||||
}),
|
||||
username: Schema.optional(Schema.String).annotate({
|
||||
description: "Custom username to display in conversations instead of system username",
|
||||
}),
|
||||
mode: Schema.optional(
|
||||
Schema.StructWithRest(
|
||||
Schema.Struct({
|
||||
build: Schema.optional(AgentRef),
|
||||
plan: Schema.optional(AgentRef),
|
||||
}),
|
||||
[Schema.Record(Schema.String, AgentRef)],
|
||||
),
|
||||
).annotate({ description: "@deprecated Use `agent` field instead." }),
|
||||
agent: Schema.optional(
|
||||
Schema.StructWithRest(
|
||||
Schema.Struct({
|
||||
// primary
|
||||
plan: ConfigAgent.Info.optional(),
|
||||
build: ConfigAgent.Info.optional(),
|
||||
plan: Schema.optional(AgentRef),
|
||||
build: Schema.optional(AgentRef),
|
||||
// subagent
|
||||
general: ConfigAgent.Info.optional(),
|
||||
explore: ConfigAgent.Info.optional(),
|
||||
general: Schema.optional(AgentRef),
|
||||
explore: Schema.optional(AgentRef),
|
||||
// specialized
|
||||
title: ConfigAgent.Info.optional(),
|
||||
summary: ConfigAgent.Info.optional(),
|
||||
compaction: ConfigAgent.Info.optional(),
|
||||
})
|
||||
.catchall(ConfigAgent.Info)
|
||||
.optional()
|
||||
.describe("Agent configuration, see https://opencode.ai/docs/agents"),
|
||||
provider: z
|
||||
.record(z.string(), ConfigProvider.Info.zod)
|
||||
.optional()
|
||||
.describe("Custom provider configurations and model overrides"),
|
||||
mcp: z
|
||||
.record(
|
||||
z.string(),
|
||||
z.union([
|
||||
ConfigMCP.Info.zod,
|
||||
z
|
||||
.object({
|
||||
enabled: z.boolean(),
|
||||
})
|
||||
.strict(),
|
||||
]),
|
||||
)
|
||||
.optional()
|
||||
.describe("MCP (Model Context Protocol) server configurations"),
|
||||
formatter: ConfigFormatter.Info.zod.optional(),
|
||||
lsp: ConfigLSP.Info.zod.optional(),
|
||||
instructions: z.array(z.string()).optional().describe("Additional instruction files or patterns to include"),
|
||||
layout: Layout.optional().describe("@deprecated Always uses stretch layout."),
|
||||
permission: ConfigPermission.Info.optional(),
|
||||
tools: z.record(z.string(), z.boolean()).optional(),
|
||||
enterprise: z
|
||||
.object({
|
||||
url: z.string().optional().describe("Enterprise URL"),
|
||||
})
|
||||
.optional(),
|
||||
compaction: z
|
||||
.object({
|
||||
auto: z.boolean().optional().describe("Enable automatic compaction when context is full (default: true)"),
|
||||
prune: z.boolean().optional().describe("Enable pruning of old tool outputs (default: true)"),
|
||||
reserved: z
|
||||
.number()
|
||||
.int()
|
||||
.min(0)
|
||||
.optional()
|
||||
.describe("Token buffer for compaction. Leaves enough window to avoid overflow during compaction."),
|
||||
})
|
||||
.optional(),
|
||||
experimental: z
|
||||
.object({
|
||||
disable_paste_summary: z.boolean().optional(),
|
||||
batch_tool: z.boolean().optional().describe("Enable the batch tool"),
|
||||
openTelemetry: z
|
||||
.boolean()
|
||||
.optional()
|
||||
.describe("Enable OpenTelemetry spans for AI SDK calls (using the 'experimental_telemetry' flag)"),
|
||||
primary_tools: z
|
||||
.array(z.string())
|
||||
.optional()
|
||||
.describe("Tools that should only be available to primary agents."),
|
||||
continue_loop_on_deny: z.boolean().optional().describe("Continue the agent loop when a tool call is denied"),
|
||||
mcp_timeout: z
|
||||
.number()
|
||||
.int()
|
||||
.positive()
|
||||
.optional()
|
||||
.describe("Timeout in milliseconds for model context protocol (MCP) requests"),
|
||||
})
|
||||
.optional(),
|
||||
})
|
||||
title: Schema.optional(AgentRef),
|
||||
summary: Schema.optional(AgentRef),
|
||||
compaction: Schema.optional(AgentRef),
|
||||
}),
|
||||
[Schema.Record(Schema.String, AgentRef)],
|
||||
),
|
||||
).annotate({ description: "Agent configuration, see https://opencode.ai/docs/agents" }),
|
||||
provider: Schema.optional(Schema.Record(Schema.String, ConfigProvider.Info)).annotate({
|
||||
description: "Custom provider configurations and model overrides",
|
||||
}),
|
||||
mcp: Schema.optional(
|
||||
Schema.Record(
|
||||
Schema.String,
|
||||
Schema.Union([
|
||||
ConfigMCP.Info,
|
||||
// Matches the legacy `{ enabled: false }` form used to disable a server.
|
||||
Schema.Any.annotate({ [ZodOverride]: z.object({ enabled: z.boolean() }).strict() }),
|
||||
]),
|
||||
),
|
||||
).annotate({ description: "MCP (Model Context Protocol) server configurations" }),
|
||||
formatter: Schema.optional(ConfigFormatter.Info),
|
||||
lsp: Schema.optional(ConfigLSP.Info),
|
||||
instructions: Schema.optional(Schema.mutable(Schema.Array(Schema.String))).annotate({
|
||||
description: "Additional instruction files or patterns to include",
|
||||
}),
|
||||
layout: Schema.optional(ConfigLayout.Layout).annotate({ description: "@deprecated Always uses stretch layout." }),
|
||||
permission: Schema.optional(PermissionRef),
|
||||
tools: Schema.optional(Schema.Record(Schema.String, Schema.Boolean)),
|
||||
enterprise: Schema.optional(
|
||||
Schema.Struct({
|
||||
url: Schema.optional(Schema.String).annotate({ description: "Enterprise URL" }),
|
||||
}),
|
||||
),
|
||||
compaction: Schema.optional(
|
||||
Schema.Struct({
|
||||
auto: Schema.optional(Schema.Boolean).annotate({
|
||||
description: "Enable automatic compaction when context is full (default: true)",
|
||||
}),
|
||||
prune: Schema.optional(Schema.Boolean).annotate({
|
||||
description: "Enable pruning of old tool outputs (default: true)",
|
||||
}),
|
||||
tail_turns: Schema.optional(NonNegativeInt).annotate({
|
||||
description:
|
||||
"Number of recent user turns, including their following assistant/tool responses, to keep verbatim during compaction (default: 2)",
|
||||
}),
|
||||
preserve_recent_tokens: Schema.optional(NonNegativeInt).annotate({
|
||||
description: "Maximum number of tokens from recent turns to preserve verbatim after compaction",
|
||||
}),
|
||||
reserved: Schema.optional(NonNegativeInt).annotate({
|
||||
description: "Token buffer for compaction. Leaves enough window to avoid overflow during compaction.",
|
||||
}),
|
||||
}),
|
||||
),
|
||||
experimental: Schema.optional(
|
||||
Schema.Struct({
|
||||
disable_paste_summary: Schema.optional(Schema.Boolean),
|
||||
batch_tool: Schema.optional(Schema.Boolean).annotate({ description: "Enable the batch tool" }),
|
||||
openTelemetry: Schema.optional(Schema.Boolean).annotate({
|
||||
description: "Enable OpenTelemetry spans for AI SDK calls (using the 'experimental_telemetry' flag)",
|
||||
}),
|
||||
primary_tools: Schema.optional(Schema.mutable(Schema.Array(Schema.String))).annotate({
|
||||
description: "Tools that should only be available to primary agents.",
|
||||
}),
|
||||
continue_loop_on_deny: Schema.optional(Schema.Boolean).annotate({
|
||||
description: "Continue the agent loop when a tool call is denied",
|
||||
}),
|
||||
mcp_timeout: Schema.optional(PositiveInt).annotate({
|
||||
description: "Timeout in milliseconds for model context protocol (MCP) requests",
|
||||
}),
|
||||
}),
|
||||
),
|
||||
})
|
||||
|
||||
// Schema.Struct produces readonly types by default, but the service code
|
||||
// below mutates Info objects directly (e.g. `config.mode = ...`). Strip the
|
||||
// readonly recursively so callers get the same mutable shape zod inferred.
|
||||
//
|
||||
// `Types.DeepMutable` from effect-smol would be a drop-in, but its fallback
|
||||
// branch `{ -readonly [K in keyof T]: ... }` collapses `unknown` to `{}`
|
||||
// (since `keyof unknown = never`), which widens `Record<string, unknown>`
|
||||
// fields like `ConfigPlugin.Options`. The local version gates on
|
||||
// `extends object` so `unknown` passes through.
|
||||
//
|
||||
// Tuple branch preserves `ConfigPlugin.Spec`'s `readonly [string, Options]`
|
||||
// shape (otherwise the general array branch widens it to an array).
|
||||
type DeepMutable<T> = T extends readonly [unknown, ...unknown[]]
|
||||
? { -readonly [K in keyof T]: DeepMutable<T[K]> }
|
||||
: T extends readonly (infer U)[]
|
||||
? DeepMutable<U>[]
|
||||
: T extends object
|
||||
? { -readonly [K in keyof T]: DeepMutable<T[K]> }
|
||||
: T
|
||||
|
||||
// The walker emits `z.object({...})` which is non-strict by default. Config
|
||||
// historically uses `.strict()` (additionalProperties: false in openapi.json),
|
||||
// so layer that on after derivation. Re-apply the Config ref afterward
|
||||
// since `.strict()` strips the walker's meta annotation.
|
||||
export const Info = (zod(InfoSchema) as unknown as z.ZodObject<any>)
|
||||
.strict()
|
||||
.meta({
|
||||
ref: "Config",
|
||||
})
|
||||
.meta({ ref: "Config" }) as unknown as z.ZodType<DeepMutable<Schema.Schema.Type<typeof InfoSchema>>>
|
||||
|
||||
export type Info = z.output<typeof Info> & {
|
||||
// plugin_origins is derived state, not a persisted config field. It keeps each winning plugin spec together
|
||||
|
||||
@@ -1,166 +1,127 @@
|
||||
export * as ConfigKeybinds from "./keybinds"
|
||||
|
||||
import z from "zod"
|
||||
import { Effect, Schema } from "effect"
|
||||
import type z from "zod"
|
||||
import { zod } from "@/util/effect-zod"
|
||||
|
||||
export const Keybinds = z
|
||||
.object({
|
||||
leader: z.string().optional().default("ctrl+x").describe("Leader key for keybind combinations"),
|
||||
app_exit: z.string().optional().default("ctrl+c,ctrl+d,<leader>q").describe("Exit the application"),
|
||||
editor_open: z.string().optional().default("<leader>e").describe("Open external editor"),
|
||||
theme_list: z.string().optional().default("<leader>t").describe("List available themes"),
|
||||
sidebar_toggle: z.string().optional().default("<leader>b").describe("Toggle sidebar"),
|
||||
scrollbar_toggle: z.string().optional().default("none").describe("Toggle session scrollbar"),
|
||||
username_toggle: z.string().optional().default("none").describe("Toggle username visibility"),
|
||||
status_view: z.string().optional().default("<leader>s").describe("View status"),
|
||||
session_export: z.string().optional().default("<leader>x").describe("Export session to editor"),
|
||||
session_new: z.string().optional().default("<leader>n").describe("Create a new session"),
|
||||
session_list: z.string().optional().default("<leader>l").describe("List all sessions"),
|
||||
session_timeline: z.string().optional().default("<leader>g").describe("Show session timeline"),
|
||||
session_fork: z.string().optional().default("none").describe("Fork session from message"),
|
||||
session_rename: z.string().optional().default("ctrl+r").describe("Rename session"),
|
||||
session_delete: z.string().optional().default("ctrl+d").describe("Delete session"),
|
||||
stash_delete: z.string().optional().default("ctrl+d").describe("Delete stash entry"),
|
||||
model_provider_list: z.string().optional().default("ctrl+a").describe("Open provider list from model dialog"),
|
||||
model_favorite_toggle: z.string().optional().default("ctrl+f").describe("Toggle model favorite status"),
|
||||
session_share: z.string().optional().default("none").describe("Share current session"),
|
||||
session_unshare: z.string().optional().default("none").describe("Unshare current session"),
|
||||
session_interrupt: z.string().optional().default("escape").describe("Interrupt current session"),
|
||||
session_compact: z.string().optional().default("<leader>c").describe("Compact the session"),
|
||||
messages_page_up: z.string().optional().default("pageup,ctrl+alt+b").describe("Scroll messages up by one page"),
|
||||
messages_page_down: z
|
||||
.string()
|
||||
.optional()
|
||||
.default("pagedown,ctrl+alt+f")
|
||||
.describe("Scroll messages down by one page"),
|
||||
messages_line_up: z.string().optional().default("ctrl+alt+y").describe("Scroll messages up by one line"),
|
||||
messages_line_down: z.string().optional().default("ctrl+alt+e").describe("Scroll messages down by one line"),
|
||||
messages_half_page_up: z.string().optional().default("ctrl+alt+u").describe("Scroll messages up by half page"),
|
||||
messages_half_page_down: z.string().optional().default("ctrl+alt+d").describe("Scroll messages down by half page"),
|
||||
messages_first: z.string().optional().default("ctrl+g,home").describe("Navigate to first message"),
|
||||
messages_last: z.string().optional().default("ctrl+alt+g,end").describe("Navigate to last message"),
|
||||
messages_next: z.string().optional().default("none").describe("Navigate to next message"),
|
||||
messages_previous: z.string().optional().default("none").describe("Navigate to previous message"),
|
||||
messages_last_user: z.string().optional().default("none").describe("Navigate to last user message"),
|
||||
messages_copy: z.string().optional().default("<leader>y").describe("Copy message"),
|
||||
messages_undo: z.string().optional().default("<leader>u").describe("Undo message"),
|
||||
messages_redo: z.string().optional().default("<leader>r").describe("Redo message"),
|
||||
messages_toggle_conceal: z
|
||||
.string()
|
||||
.optional()
|
||||
.default("<leader>h")
|
||||
.describe("Toggle code block concealment in messages"),
|
||||
tool_details: z.string().optional().default("none").describe("Toggle tool details visibility"),
|
||||
model_list: z.string().optional().default("<leader>m").describe("List available models"),
|
||||
model_cycle_recent: z.string().optional().default("f2").describe("Next recently used model"),
|
||||
model_cycle_recent_reverse: z.string().optional().default("shift+f2").describe("Previous recently used model"),
|
||||
model_cycle_favorite: z.string().optional().default("none").describe("Next favorite model"),
|
||||
model_cycle_favorite_reverse: z.string().optional().default("none").describe("Previous favorite model"),
|
||||
command_list: z.string().optional().default("ctrl+p").describe("List available commands"),
|
||||
agent_list: z.string().optional().default("<leader>a").describe("List agents"),
|
||||
agent_cycle: z.string().optional().default("tab").describe("Next agent"),
|
||||
agent_cycle_reverse: z.string().optional().default("shift+tab").describe("Previous agent"),
|
||||
variant_cycle: z.string().optional().default("ctrl+t").describe("Cycle model variants"),
|
||||
variant_list: z.string().optional().default("none").describe("List model variants"),
|
||||
input_clear: z.string().optional().default("ctrl+c").describe("Clear input field"),
|
||||
input_paste: z.string().optional().default("ctrl+v").describe("Paste from clipboard"),
|
||||
input_submit: z.string().optional().default("return").describe("Submit input"),
|
||||
input_newline: z
|
||||
.string()
|
||||
.optional()
|
||||
.default("shift+return,ctrl+return,alt+return,ctrl+j")
|
||||
.describe("Insert newline in input"),
|
||||
input_move_left: z.string().optional().default("left,ctrl+b").describe("Move cursor left in input"),
|
||||
input_move_right: z.string().optional().default("right,ctrl+f").describe("Move cursor right in input"),
|
||||
input_move_up: z.string().optional().default("up").describe("Move cursor up in input"),
|
||||
input_move_down: z.string().optional().default("down").describe("Move cursor down in input"),
|
||||
input_select_left: z.string().optional().default("shift+left").describe("Select left in input"),
|
||||
input_select_right: z.string().optional().default("shift+right").describe("Select right in input"),
|
||||
input_select_up: z.string().optional().default("shift+up").describe("Select up in input"),
|
||||
input_select_down: z.string().optional().default("shift+down").describe("Select down in input"),
|
||||
input_line_home: z.string().optional().default("ctrl+a").describe("Move to start of line in input"),
|
||||
input_line_end: z.string().optional().default("ctrl+e").describe("Move to end of line in input"),
|
||||
input_select_line_home: z.string().optional().default("ctrl+shift+a").describe("Select to start of line in input"),
|
||||
input_select_line_end: z.string().optional().default("ctrl+shift+e").describe("Select to end of line in input"),
|
||||
input_visual_line_home: z.string().optional().default("alt+a").describe("Move to start of visual line in input"),
|
||||
input_visual_line_end: z.string().optional().default("alt+e").describe("Move to end of visual line in input"),
|
||||
input_select_visual_line_home: z
|
||||
.string()
|
||||
.optional()
|
||||
.default("alt+shift+a")
|
||||
.describe("Select to start of visual line in input"),
|
||||
input_select_visual_line_end: z
|
||||
.string()
|
||||
.optional()
|
||||
.default("alt+shift+e")
|
||||
.describe("Select to end of visual line in input"),
|
||||
input_buffer_home: z.string().optional().default("home").describe("Move to start of buffer in input"),
|
||||
input_buffer_end: z.string().optional().default("end").describe("Move to end of buffer in input"),
|
||||
input_select_buffer_home: z
|
||||
.string()
|
||||
.optional()
|
||||
.default("shift+home")
|
||||
.describe("Select to start of buffer in input"),
|
||||
input_select_buffer_end: z.string().optional().default("shift+end").describe("Select to end of buffer in input"),
|
||||
input_delete_line: z.string().optional().default("ctrl+shift+d").describe("Delete line in input"),
|
||||
input_delete_to_line_end: z.string().optional().default("ctrl+k").describe("Delete to end of line in input"),
|
||||
input_delete_to_line_start: z.string().optional().default("ctrl+u").describe("Delete to start of line in input"),
|
||||
input_backspace: z.string().optional().default("backspace,shift+backspace").describe("Backspace in input"),
|
||||
input_delete: z.string().optional().default("ctrl+d,delete,shift+delete").describe("Delete character in input"),
|
||||
input_undo: z
|
||||
.string()
|
||||
.optional()
|
||||
// On Windows prepend ctrl+z since terminal_suspend releases the binding.
|
||||
.default(process.platform === "win32" ? "ctrl+z,ctrl+-,super+z" : "ctrl+-,super+z")
|
||||
.describe("Undo in input"),
|
||||
input_redo: z.string().optional().default("ctrl+.,super+shift+z").describe("Redo in input"),
|
||||
input_word_forward: z
|
||||
.string()
|
||||
.optional()
|
||||
.default("alt+f,alt+right,ctrl+right")
|
||||
.describe("Move word forward in input"),
|
||||
input_word_backward: z
|
||||
.string()
|
||||
.optional()
|
||||
.default("alt+b,alt+left,ctrl+left")
|
||||
.describe("Move word backward in input"),
|
||||
input_select_word_forward: z
|
||||
.string()
|
||||
.optional()
|
||||
.default("alt+shift+f,alt+shift+right")
|
||||
.describe("Select word forward in input"),
|
||||
input_select_word_backward: z
|
||||
.string()
|
||||
.optional()
|
||||
.default("alt+shift+b,alt+shift+left")
|
||||
.describe("Select word backward in input"),
|
||||
input_delete_word_forward: z
|
||||
.string()
|
||||
.optional()
|
||||
.default("alt+d,alt+delete,ctrl+delete")
|
||||
.describe("Delete word forward in input"),
|
||||
input_delete_word_backward: z
|
||||
.string()
|
||||
.optional()
|
||||
.default("ctrl+w,ctrl+backspace,alt+backspace")
|
||||
.describe("Delete word backward in input"),
|
||||
history_previous: z.string().optional().default("up").describe("Previous history item"),
|
||||
history_next: z.string().optional().default("down").describe("Next history item"),
|
||||
session_child_first: z.string().optional().default("<leader>down").describe("Go to first child session"),
|
||||
session_child_cycle: z.string().optional().default("right").describe("Go to next child session"),
|
||||
session_child_cycle_reverse: z.string().optional().default("left").describe("Go to previous child session"),
|
||||
session_parent: z.string().optional().default("up").describe("Go to parent session"),
|
||||
terminal_suspend: z
|
||||
.string()
|
||||
.optional()
|
||||
.default("ctrl+z")
|
||||
.transform((v) => (process.platform === "win32" ? "none" : v))
|
||||
.describe("Suspend terminal"),
|
||||
terminal_title_toggle: z.string().optional().default("none").describe("Toggle terminal title"),
|
||||
tips_toggle: z.string().optional().default("<leader>h").describe("Toggle tips on home screen"),
|
||||
plugin_manager: z.string().optional().default("none").describe("Open plugin manager dialog"),
|
||||
display_thinking: z.string().optional().default("none").describe("Toggle thinking blocks visibility"),
|
||||
})
|
||||
.strict()
|
||||
.meta({
|
||||
ref: "KeybindsConfig",
|
||||
})
|
||||
// Every keybind field has the same shape: an optional string with a default
|
||||
// binding and a human description. `keybind()` keeps the declaration list
|
||||
// below dense and readable.
|
||||
const keybind = (value: string, description: string) =>
|
||||
Schema.String.pipe(Schema.optional, Schema.withDecodingDefault(Effect.succeed(value))).annotate({ description })
|
||||
|
||||
// Windows prepends ctrl+z to the undo binding because `terminal_suspend`
|
||||
// cannot consume ctrl+z on native Windows terminals (no POSIX suspend).
|
||||
const inputUndoDefault = process.platform === "win32" ? "ctrl+z,ctrl+-,super+z" : "ctrl+-,super+z"
|
||||
|
||||
const KeybindsSchema = Schema.Struct({
|
||||
leader: keybind("ctrl+x", "Leader key for keybind combinations"),
|
||||
app_exit: keybind("ctrl+c,ctrl+d,<leader>q", "Exit the application"),
|
||||
editor_open: keybind("<leader>e", "Open external editor"),
|
||||
theme_list: keybind("<leader>t", "List available themes"),
|
||||
sidebar_toggle: keybind("<leader>b", "Toggle sidebar"),
|
||||
scrollbar_toggle: keybind("none", "Toggle session scrollbar"),
|
||||
username_toggle: keybind("none", "Toggle username visibility"),
|
||||
status_view: keybind("<leader>s", "View status"),
|
||||
session_export: keybind("<leader>x", "Export session to editor"),
|
||||
session_new: keybind("<leader>n", "Create a new session"),
|
||||
session_list: keybind("<leader>l", "List all sessions"),
|
||||
session_timeline: keybind("<leader>g", "Show session timeline"),
|
||||
session_fork: keybind("none", "Fork session from message"),
|
||||
session_rename: keybind("ctrl+r", "Rename session"),
|
||||
session_delete: keybind("ctrl+d", "Delete session"),
|
||||
stash_delete: keybind("ctrl+d", "Delete stash entry"),
|
||||
model_provider_list: keybind("ctrl+a", "Open provider list from model dialog"),
|
||||
model_favorite_toggle: keybind("ctrl+f", "Toggle model favorite status"),
|
||||
session_share: keybind("none", "Share current session"),
|
||||
session_unshare: keybind("none", "Unshare current session"),
|
||||
session_interrupt: keybind("escape", "Interrupt current session"),
|
||||
session_compact: keybind("<leader>c", "Compact the session"),
|
||||
messages_page_up: keybind("pageup,ctrl+alt+b", "Scroll messages up by one page"),
|
||||
messages_page_down: keybind("pagedown,ctrl+alt+f", "Scroll messages down by one page"),
|
||||
messages_line_up: keybind("ctrl+alt+y", "Scroll messages up by one line"),
|
||||
messages_line_down: keybind("ctrl+alt+e", "Scroll messages down by one line"),
|
||||
messages_half_page_up: keybind("ctrl+alt+u", "Scroll messages up by half page"),
|
||||
messages_half_page_down: keybind("ctrl+alt+d", "Scroll messages down by half page"),
|
||||
messages_first: keybind("ctrl+g,home", "Navigate to first message"),
|
||||
messages_last: keybind("ctrl+alt+g,end", "Navigate to last message"),
|
||||
messages_next: keybind("none", "Navigate to next message"),
|
||||
messages_previous: keybind("none", "Navigate to previous message"),
|
||||
messages_last_user: keybind("none", "Navigate to last user message"),
|
||||
messages_copy: keybind("<leader>y", "Copy message"),
|
||||
messages_undo: keybind("<leader>u", "Undo message"),
|
||||
messages_redo: keybind("<leader>r", "Redo message"),
|
||||
messages_toggle_conceal: keybind("<leader>h", "Toggle code block concealment in messages"),
|
||||
tool_details: keybind("none", "Toggle tool details visibility"),
|
||||
model_list: keybind("<leader>m", "List available models"),
|
||||
model_cycle_recent: keybind("f2", "Next recently used model"),
|
||||
model_cycle_recent_reverse: keybind("shift+f2", "Previous recently used model"),
|
||||
model_cycle_favorite: keybind("none", "Next favorite model"),
|
||||
model_cycle_favorite_reverse: keybind("none", "Previous favorite model"),
|
||||
command_list: keybind("ctrl+p", "List available commands"),
|
||||
agent_list: keybind("<leader>a", "List agents"),
|
||||
agent_cycle: keybind("tab", "Next agent"),
|
||||
agent_cycle_reverse: keybind("shift+tab", "Previous agent"),
|
||||
variant_cycle: keybind("ctrl+t", "Cycle model variants"),
|
||||
variant_list: keybind("none", "List model variants"),
|
||||
input_clear: keybind("ctrl+c", "Clear input field"),
|
||||
input_paste: keybind("ctrl+v", "Paste from clipboard"),
|
||||
input_submit: keybind("return", "Submit input"),
|
||||
input_newline: keybind("shift+return,ctrl+return,alt+return,ctrl+j", "Insert newline in input"),
|
||||
input_move_left: keybind("left,ctrl+b", "Move cursor left in input"),
|
||||
input_move_right: keybind("right,ctrl+f", "Move cursor right in input"),
|
||||
input_move_up: keybind("up", "Move cursor up in input"),
|
||||
input_move_down: keybind("down", "Move cursor down in input"),
|
||||
input_select_left: keybind("shift+left", "Select left in input"),
|
||||
input_select_right: keybind("shift+right", "Select right in input"),
|
||||
input_select_up: keybind("shift+up", "Select up in input"),
|
||||
input_select_down: keybind("shift+down", "Select down in input"),
|
||||
input_line_home: keybind("ctrl+a", "Move to start of line in input"),
|
||||
input_line_end: keybind("ctrl+e", "Move to end of line in input"),
|
||||
input_select_line_home: keybind("ctrl+shift+a", "Select to start of line in input"),
|
||||
input_select_line_end: keybind("ctrl+shift+e", "Select to end of line in input"),
|
||||
input_visual_line_home: keybind("alt+a", "Move to start of visual line in input"),
|
||||
input_visual_line_end: keybind("alt+e", "Move to end of visual line in input"),
|
||||
input_select_visual_line_home: keybind("alt+shift+a", "Select to start of visual line in input"),
|
||||
input_select_visual_line_end: keybind("alt+shift+e", "Select to end of visual line in input"),
|
||||
input_buffer_home: keybind("home", "Move to start of buffer in input"),
|
||||
input_buffer_end: keybind("end", "Move to end of buffer in input"),
|
||||
input_select_buffer_home: keybind("shift+home", "Select to start of buffer in input"),
|
||||
input_select_buffer_end: keybind("shift+end", "Select to end of buffer in input"),
|
||||
input_delete_line: keybind("ctrl+shift+d", "Delete line in input"),
|
||||
input_delete_to_line_end: keybind("ctrl+k", "Delete to end of line in input"),
|
||||
input_delete_to_line_start: keybind("ctrl+u", "Delete to start of line in input"),
|
||||
input_backspace: keybind("backspace,shift+backspace", "Backspace in input"),
|
||||
input_delete: keybind("ctrl+d,delete,shift+delete", "Delete character in input"),
|
||||
input_undo: keybind(inputUndoDefault, "Undo in input"),
|
||||
input_redo: keybind("ctrl+.,super+shift+z", "Redo in input"),
|
||||
input_word_forward: keybind("alt+f,alt+right,ctrl+right", "Move word forward in input"),
|
||||
input_word_backward: keybind("alt+b,alt+left,ctrl+left", "Move word backward in input"),
|
||||
input_select_word_forward: keybind("alt+shift+f,alt+shift+right", "Select word forward in input"),
|
||||
input_select_word_backward: keybind("alt+shift+b,alt+shift+left", "Select word backward in input"),
|
||||
input_delete_word_forward: keybind("alt+d,alt+delete,ctrl+delete", "Delete word forward in input"),
|
||||
input_delete_word_backward: keybind("ctrl+w,ctrl+backspace,alt+backspace", "Delete word backward in input"),
|
||||
history_previous: keybind("up", "Previous history item"),
|
||||
history_next: keybind("down", "Next history item"),
|
||||
session_child_first: keybind("<leader>down", "Go to first child session"),
|
||||
session_child_cycle: keybind("right", "Go to next child session"),
|
||||
session_child_cycle_reverse: keybind("left", "Go to previous child session"),
|
||||
session_parent: keybind("up", "Go to parent session"),
|
||||
// `terminal_suspend` was formerly `.default("ctrl+z").transform((v) => win32 ? "none" : v)`,
|
||||
// but `tui.ts` already forces the binding to "none" on win32 before calling
|
||||
// `Keybinds.parse(...)`, so the schema-level transform was redundant.
|
||||
terminal_suspend: keybind("ctrl+z", "Suspend terminal"),
|
||||
terminal_title_toggle: keybind("none", "Toggle terminal title"),
|
||||
tips_toggle: keybind("<leader>h", "Toggle tips on home screen"),
|
||||
plugin_manager: keybind("none", "Open plugin manager dialog"),
|
||||
display_thinking: keybind("none", "Toggle thinking blocks visibility"),
|
||||
}).annotate({ identifier: "KeybindsConfig" })
|
||||
|
||||
export type Keybinds = Schema.Schema.Type<typeof KeybindsSchema>
|
||||
|
||||
// Consumers access `Keybinds.shape` and `Keybinds.shape.X.parse(undefined)`,
|
||||
// which requires the runtime type to be a ZodObject, not just ZodType. Every
|
||||
// field is `string().optional().default(...)` at runtime, so widen to that.
|
||||
export const Keybinds = zod(KeybindsSchema) as unknown as z.ZodObject<
|
||||
Record<keyof Keybinds, z.ZodDefault<z.ZodOptional<z.ZodString>>>
|
||||
>
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user