Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
edfba50d6e | ||
|
|
b8b16391df | ||
|
|
ec26d78450 | ||
|
|
49e8d324dd | ||
|
|
60b9ba4db7 | ||
|
|
a1a401dc9b | ||
|
|
2f1547ebaf | ||
|
|
62da1e7682 | ||
|
|
28a06e52fc | ||
|
|
5a5d981c4e | ||
|
|
62a7781e04 | ||
|
|
2d03cf4a9a | ||
|
|
4025d30b24 | ||
|
|
dd3c443d8c | ||
|
|
4aaa9f9e30 | ||
|
|
31c805bc97 | ||
|
|
94f2ed1b84 | ||
|
|
86dc66eae9 | ||
|
|
9814dc6526 | ||
|
|
f09c859974 | ||
|
|
1fcdb0246a | ||
|
|
340ad9e9ed | ||
|
|
76d814e747 | ||
|
|
9941e70d29 | ||
|
|
2afb812620 | ||
|
|
ad2fc1ce4b | ||
|
|
5acc368ef4 | ||
|
|
e2dc89c6f3 | ||
|
|
e1581183ff | ||
|
|
f195c952fc | ||
|
|
34b1045a01 | ||
|
|
aa9956d46d | ||
|
|
cec5a752bb | ||
|
|
0f0a4be253 | ||
|
|
0e88dd99e7 | ||
|
|
f965db9e13 | ||
|
|
519d344470 | ||
|
|
a78605f8ea | ||
|
|
c0bc020ad6 | ||
|
|
6a994d6c67 | ||
|
|
0e669bbd18 | ||
|
|
00a6f228f2 | ||
|
|
50f6df3f3a | ||
|
|
c1e744f099 | ||
|
|
714cbe6dbd | ||
|
|
3077ff3f0c | ||
|
|
9b5fbbe398 | ||
|
|
b5912f50c1 | ||
|
|
0364de0af6 | ||
|
|
d0dae93c73 | ||
|
|
7c14627fc6 | ||
|
|
876ce31fef | ||
|
|
c3743b26e8 | ||
|
|
b97d254dcc | ||
|
|
087d356d41 | ||
|
|
08c17e831f | ||
|
|
dad0f579fb | ||
|
|
b819034c50 | ||
|
|
dcbe09bc9f | ||
|
|
7180c3a7fb | ||
|
|
9440f7f5b9 | ||
|
|
13e9e317b1 | ||
|
|
06bd34c953 | ||
|
|
05dca9f698 | ||
|
|
dedcb9ba91 | ||
|
|
b18ff8fd68 | ||
|
|
9ded717fc1 | ||
|
|
9b812d9609 | ||
|
|
aa8ad494b8 | ||
|
|
46953462d9 | ||
|
|
077c91b95d | ||
|
|
31d89590e1 | ||
|
|
46422a4575 | ||
|
|
977f48a97b | ||
|
|
0cab11f26b | ||
|
|
eaf5697b06 | ||
|
|
36160ef28f |
@@ -2,6 +2,9 @@
|
||||
"$schema": "https://opencode.ai/config.json",
|
||||
"provider": {},
|
||||
"permission": {},
|
||||
"reference": {
|
||||
"effect": "github.com/Effect-TS/effect-smol",
|
||||
},
|
||||
"mcp": {},
|
||||
"tools": {
|
||||
"github-triage": false,
|
||||
|
||||
@@ -47,6 +47,12 @@ obj.b
|
||||
const { a, b } = obj
|
||||
```
|
||||
|
||||
### Imports
|
||||
|
||||
- Never alias imports. Do not use `import { foo as bar } from "..."` or renamed imports like `resolve as pathResolve`.
|
||||
- Never use star imports. Do not use `import * as Foo from "..."` or `import type * as Foo from "..."`.
|
||||
- If a namespace-style value is needed, import the module's own exported namespace by name, for example `import { Project } from "@opencode-ai/core/project"`, then reference `Project.ID`.
|
||||
|
||||
### Variables
|
||||
|
||||
Prefer `const` over `let`. Use ternaries or early returns instead of reassignment.
|
||||
|
||||
@@ -29,7 +29,7 @@
|
||||
},
|
||||
"packages/app": {
|
||||
"name": "@opencode-ai/app",
|
||||
"version": "1.15.10",
|
||||
"version": "1.15.11",
|
||||
"dependencies": {
|
||||
"@kobalte/core": "catalog:",
|
||||
"@opencode-ai/core": "workspace:*",
|
||||
@@ -85,7 +85,7 @@
|
||||
},
|
||||
"packages/console/app": {
|
||||
"name": "@opencode-ai/console-app",
|
||||
"version": "1.15.10",
|
||||
"version": "1.15.11",
|
||||
"dependencies": {
|
||||
"@cloudflare/vite-plugin": "1.15.2",
|
||||
"@ibm/plex": "6.4.1",
|
||||
@@ -121,7 +121,7 @@
|
||||
},
|
||||
"packages/console/core": {
|
||||
"name": "@opencode-ai/console-core",
|
||||
"version": "1.15.10",
|
||||
"version": "1.15.11",
|
||||
"dependencies": {
|
||||
"@aws-sdk/client-sts": "3.782.0",
|
||||
"@jsx-email/render": "1.1.1",
|
||||
@@ -148,7 +148,7 @@
|
||||
},
|
||||
"packages/console/function": {
|
||||
"name": "@opencode-ai/console-function",
|
||||
"version": "1.15.10",
|
||||
"version": "1.15.11",
|
||||
"dependencies": {
|
||||
"@ai-sdk/anthropic": "3.0.64",
|
||||
"@ai-sdk/openai": "3.0.48",
|
||||
@@ -170,7 +170,7 @@
|
||||
},
|
||||
"packages/console/mail": {
|
||||
"name": "@opencode-ai/console-mail",
|
||||
"version": "1.15.10",
|
||||
"version": "1.15.11",
|
||||
"dependencies": {
|
||||
"@jsx-email/all": "2.2.3",
|
||||
"@jsx-email/cli": "1.4.3",
|
||||
@@ -194,7 +194,7 @@
|
||||
},
|
||||
"packages/console/support": {
|
||||
"name": "@opencode-ai/console-support",
|
||||
"version": "0.0.0",
|
||||
"version": "1.15.11",
|
||||
"dependencies": {
|
||||
"@cloudflare/vite-plugin": "1.15.2",
|
||||
"@opencode-ai/console-core": "workspace:*",
|
||||
@@ -214,7 +214,7 @@
|
||||
},
|
||||
"packages/core": {
|
||||
"name": "@opencode-ai/core",
|
||||
"version": "1.15.10",
|
||||
"version": "1.15.11",
|
||||
"bin": {
|
||||
"opencode": "./bin/opencode",
|
||||
},
|
||||
@@ -242,8 +242,11 @@
|
||||
"@aws-sdk/credential-providers": "3.993.0",
|
||||
"@effect/opentelemetry": "catalog:",
|
||||
"@effect/platform-node": "catalog:",
|
||||
"@effect/sql-sqlite-bun": "catalog:",
|
||||
"@npmcli/arborist": "9.4.0",
|
||||
"@npmcli/config": "10.8.1",
|
||||
"@opencode-ai/effect-drizzle-sqlite": "workspace:*",
|
||||
"@opencode-ai/effect-sqlite-node": "workspace:*",
|
||||
"@openrouter/ai-sdk-provider": "2.8.1",
|
||||
"@opentelemetry/api": "1.9.0",
|
||||
"@opentelemetry/context-async-hooks": "2.6.1",
|
||||
@@ -251,6 +254,7 @@
|
||||
"@opentelemetry/sdk-trace-base": "2.6.1",
|
||||
"ai-gateway-provider": "3.1.2",
|
||||
"cross-spawn": "catalog:",
|
||||
"drizzle-orm": "catalog:",
|
||||
"effect": "catalog:",
|
||||
"gitlab-ai-provider": "6.7.0",
|
||||
"glob": "13.0.5",
|
||||
@@ -271,11 +275,12 @@
|
||||
"@types/npm-package-arg": "6.1.4",
|
||||
"@types/npmcli__arborist": "6.3.3",
|
||||
"@types/semver": "catalog:",
|
||||
"drizzle-kit": "catalog:",
|
||||
},
|
||||
},
|
||||
"packages/desktop": {
|
||||
"name": "@opencode-ai/desktop",
|
||||
"version": "1.15.10",
|
||||
"version": "1.15.11",
|
||||
"dependencies": {
|
||||
"@zip.js/zip.js": "2.7.62",
|
||||
"drizzle-orm": "catalog:",
|
||||
@@ -330,7 +335,7 @@
|
||||
},
|
||||
"packages/effect-drizzle-sqlite": {
|
||||
"name": "@opencode-ai/effect-drizzle-sqlite",
|
||||
"version": "1.15.10",
|
||||
"version": "1.15.11",
|
||||
"dependencies": {
|
||||
"drizzle-orm": "catalog:",
|
||||
"effect": "catalog:",
|
||||
@@ -342,9 +347,21 @@
|
||||
"@typescript/native-preview": "catalog:",
|
||||
},
|
||||
},
|
||||
"packages/effect-sqlite-node": {
|
||||
"name": "@opencode-ai/effect-sqlite-node",
|
||||
"version": "1.15.10",
|
||||
"dependencies": {
|
||||
"effect": "catalog:",
|
||||
},
|
||||
"devDependencies": {
|
||||
"@tsconfig/bun": "catalog:",
|
||||
"@types/node": "catalog:",
|
||||
"@typescript/native-preview": "catalog:",
|
||||
},
|
||||
},
|
||||
"packages/enterprise": {
|
||||
"name": "@opencode-ai/enterprise",
|
||||
"version": "1.15.10",
|
||||
"version": "1.15.11",
|
||||
"dependencies": {
|
||||
"@opencode-ai/core": "workspace:*",
|
||||
"@opencode-ai/ui": "workspace:*",
|
||||
@@ -374,7 +391,7 @@
|
||||
},
|
||||
"packages/function": {
|
||||
"name": "@opencode-ai/function",
|
||||
"version": "1.15.10",
|
||||
"version": "1.15.11",
|
||||
"dependencies": {
|
||||
"@octokit/auth-app": "8.0.1",
|
||||
"@octokit/rest": "catalog:",
|
||||
@@ -390,7 +407,7 @@
|
||||
},
|
||||
"packages/http-recorder": {
|
||||
"name": "@opencode-ai/http-recorder",
|
||||
"version": "1.15.10",
|
||||
"version": "1.15.11",
|
||||
"dependencies": {
|
||||
"@effect/platform-node": "catalog:",
|
||||
"effect": "catalog:",
|
||||
@@ -403,7 +420,7 @@
|
||||
},
|
||||
"packages/llm": {
|
||||
"name": "@opencode-ai/llm",
|
||||
"version": "1.15.10",
|
||||
"version": "1.15.11",
|
||||
"dependencies": {
|
||||
"@smithy/eventstream-codec": "4.2.14",
|
||||
"@smithy/util-utf8": "4.2.2",
|
||||
@@ -421,7 +438,7 @@
|
||||
},
|
||||
"packages/opencode": {
|
||||
"name": "opencode",
|
||||
"version": "1.15.10",
|
||||
"version": "1.15.11",
|
||||
"bin": {
|
||||
"opencode": "./bin/opencode",
|
||||
},
|
||||
@@ -478,6 +495,7 @@
|
||||
"@solid-primitives/event-bus": "1.1.2",
|
||||
"@solid-primitives/scheduled": "1.5.2",
|
||||
"@standard-schema/spec": "1.0.0",
|
||||
"@types/ws": "8.18.1",
|
||||
"@zip.js/zip.js": "2.7.62",
|
||||
"ai": "catalog:",
|
||||
"ai-gateway-provider": "3.1.2",
|
||||
@@ -519,6 +537,7 @@
|
||||
"vscode-jsonrpc": "8.2.1",
|
||||
"web-tree-sitter": "0.25.10",
|
||||
"which": "6.0.1",
|
||||
"ws": "8.21.0",
|
||||
"xdg-basedir": "5.1.0",
|
||||
"yargs": "18.0.0",
|
||||
"zod": "catalog:",
|
||||
@@ -549,7 +568,6 @@
|
||||
"@types/which": "3.0.4",
|
||||
"@types/yargs": "17.0.33",
|
||||
"@typescript/native-preview": "catalog:",
|
||||
"drizzle-kit": "catalog:",
|
||||
"drizzle-orm": "catalog:",
|
||||
"prettier": "3.6.2",
|
||||
"typescript": "catalog:",
|
||||
@@ -559,7 +577,7 @@
|
||||
},
|
||||
"packages/plugin": {
|
||||
"name": "@opencode-ai/plugin",
|
||||
"version": "1.15.10",
|
||||
"version": "1.15.11",
|
||||
"dependencies": {
|
||||
"@opencode-ai/sdk": "workspace:*",
|
||||
"effect": "catalog:",
|
||||
@@ -575,9 +593,9 @@
|
||||
"typescript": "catalog:",
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@opentui/core": ">=0.2.15",
|
||||
"@opentui/keymap": ">=0.2.15",
|
||||
"@opentui/solid": ">=0.2.15",
|
||||
"@opentui/core": ">=0.2.16",
|
||||
"@opentui/keymap": ">=0.2.16",
|
||||
"@opentui/solid": ">=0.2.16",
|
||||
},
|
||||
"optionalPeers": [
|
||||
"@opentui/core",
|
||||
@@ -597,7 +615,7 @@
|
||||
},
|
||||
"packages/sdk/js": {
|
||||
"name": "@opencode-ai/sdk",
|
||||
"version": "1.15.10",
|
||||
"version": "1.15.11",
|
||||
"dependencies": {
|
||||
"cross-spawn": "catalog:",
|
||||
},
|
||||
@@ -612,7 +630,7 @@
|
||||
},
|
||||
"packages/slack": {
|
||||
"name": "@opencode-ai/slack",
|
||||
"version": "1.15.10",
|
||||
"version": "1.15.11",
|
||||
"dependencies": {
|
||||
"@opencode-ai/sdk": "workspace:*",
|
||||
"@slack/bolt": "^3.17.1",
|
||||
@@ -625,7 +643,7 @@
|
||||
},
|
||||
"packages/stats/app": {
|
||||
"name": "@opencode-ai/stats-app",
|
||||
"version": "1.14.50",
|
||||
"version": "1.15.11",
|
||||
"dependencies": {
|
||||
"@opencode-ai/stats-core": "workspace:*",
|
||||
"@opencode-ai/ui": "workspace:*",
|
||||
@@ -648,7 +666,7 @@
|
||||
},
|
||||
"packages/stats/core": {
|
||||
"name": "@opencode-ai/stats-core",
|
||||
"version": "1.14.50",
|
||||
"version": "1.15.11",
|
||||
"dependencies": {
|
||||
"@aws-sdk/client-athena": "3.933.0",
|
||||
"@planetscale/database": "1.19.0",
|
||||
@@ -667,7 +685,7 @@
|
||||
},
|
||||
"packages/stats/server": {
|
||||
"name": "@opencode-ai/stats-server",
|
||||
"version": "1.14.50",
|
||||
"version": "1.15.11",
|
||||
"dependencies": {
|
||||
"@aws-sdk/client-firehose": "3.933.0",
|
||||
"@effect/platform-node": "catalog:",
|
||||
@@ -707,7 +725,7 @@
|
||||
},
|
||||
"packages/ui": {
|
||||
"name": "@opencode-ai/ui",
|
||||
"version": "1.15.10",
|
||||
"version": "1.15.11",
|
||||
"dependencies": {
|
||||
"@kobalte/core": "catalog:",
|
||||
"@opencode-ai/core": "workspace:*",
|
||||
@@ -756,7 +774,7 @@
|
||||
},
|
||||
"packages/web": {
|
||||
"name": "@opencode-ai/web",
|
||||
"version": "1.15.10",
|
||||
"version": "1.15.11",
|
||||
"dependencies": {
|
||||
"@astrojs/cloudflare": "12.6.3",
|
||||
"@astrojs/markdown-remark": "6.3.1",
|
||||
@@ -824,9 +842,9 @@
|
||||
"@npmcli/arborist": "9.4.0",
|
||||
"@octokit/rest": "22.0.0",
|
||||
"@openauthjs/openauth": "0.0.0-20250322224806",
|
||||
"@opentui/core": "0.2.15",
|
||||
"@opentui/keymap": "0.2.15",
|
||||
"@opentui/solid": "0.2.15",
|
||||
"@opentui/core": "0.2.16",
|
||||
"@opentui/keymap": "0.2.16",
|
||||
"@opentui/solid": "0.2.16",
|
||||
"@pierre/diffs": "1.1.0-beta.18",
|
||||
"@playwright/test": "1.59.1",
|
||||
"@sentry/solid": "10.36.0",
|
||||
@@ -1660,6 +1678,8 @@
|
||||
|
||||
"@opencode-ai/effect-drizzle-sqlite": ["@opencode-ai/effect-drizzle-sqlite@workspace:packages/effect-drizzle-sqlite"],
|
||||
|
||||
"@opencode-ai/effect-sqlite-node": ["@opencode-ai/effect-sqlite-node@workspace:packages/effect-sqlite-node"],
|
||||
|
||||
"@opencode-ai/enterprise": ["@opencode-ai/enterprise@workspace:packages/enterprise"],
|
||||
|
||||
"@opencode-ai/function": ["@opencode-ai/function@workspace:packages/function"],
|
||||
@@ -1716,23 +1736,23 @@
|
||||
|
||||
"@opentelemetry/semantic-conventions": ["@opentelemetry/semantic-conventions@1.40.0", "", {}, "sha512-cifvXDhcqMwwTlTK04GBNeIe7yyo28Mfby85QXFe1Yk8nmi36Ab/5UQwptOx84SsoGNRg+EVSjwzfSZMy6pmlw=="],
|
||||
|
||||
"@opentui/core": ["@opentui/core@0.2.15", "", { "dependencies": { "bun-ffi-structs": "0.2.2", "diff": "9.0.0", "marked": "17.0.1", "string-width": "7.2.0", "strip-ansi": "7.1.2", "yoga-layout": "3.2.1" }, "optionalDependencies": { "@opentui/core-darwin-arm64": "0.2.15", "@opentui/core-darwin-x64": "0.2.15", "@opentui/core-linux-arm64": "0.2.15", "@opentui/core-linux-x64": "0.2.15", "@opentui/core-win32-arm64": "0.2.15", "@opentui/core-win32-x64": "0.2.15" }, "peerDependencies": { "web-tree-sitter": "0.25.10" } }, "sha512-YGHttdZWScMcSvtYgZkLR6VhUO1OoUiQzwYjZgIusf5eCkPLD8PapH+PTMVqAiX16CHO6JxfMlkHv5qDiHAccQ=="],
|
||||
"@opentui/core": ["@opentui/core@0.2.16", "", { "dependencies": { "bun-ffi-structs": "0.2.2", "diff": "9.0.0", "marked": "17.0.1", "string-width": "7.2.0", "strip-ansi": "7.1.2", "yoga-layout": "3.2.1" }, "optionalDependencies": { "@opentui/core-darwin-arm64": "0.2.16", "@opentui/core-darwin-x64": "0.2.16", "@opentui/core-linux-arm64": "0.2.16", "@opentui/core-linux-x64": "0.2.16", "@opentui/core-win32-arm64": "0.2.16", "@opentui/core-win32-x64": "0.2.16" }, "peerDependencies": { "web-tree-sitter": "0.25.10" } }, "sha512-4vWN15Zc3nsXJlOiHhhpqkBXD+wrNFKxCPtiTiillZYDRre+XsZogVTOOGUDwaBIC23OSxq7imezLmmtShVBEA=="],
|
||||
|
||||
"@opentui/core-darwin-arm64": ["@opentui/core-darwin-arm64@0.2.15", "", { "os": "darwin", "cpu": "arm64" }, "sha512-s25f9GmZd6wxNM5ExRmwwnLT+NLCKxnTWuO9aObOlqsXfLMGHQZrb6YwgAn/PSTua98KmH7GJCVWdPgZ/P+0RQ=="],
|
||||
"@opentui/core-darwin-arm64": ["@opentui/core-darwin-arm64@0.2.16", "", { "os": "darwin", "cpu": "arm64" }, "sha512-aFb2Yp+oqDu3h6VCWi7xpQ9yjpKSQcROzGGfHgqC6Nd3U+uiLfPJBkmiI87iK0opCggCFj5TkKI004050DmGjg=="],
|
||||
|
||||
"@opentui/core-darwin-x64": ["@opentui/core-darwin-x64@0.2.15", "", { "os": "darwin", "cpu": "x64" }, "sha512-GyaipN+nOcEr8rcTO2mqKTGmOBk0C300I69fLtubD3BadHcMI1DVNlQrcf/J1mkQEuMYbmBTi/1hT1ybWGr2Mw=="],
|
||||
"@opentui/core-darwin-x64": ["@opentui/core-darwin-x64@0.2.16", "", { "os": "darwin", "cpu": "x64" }, "sha512-KimiHE0j7EsTB5P8doW0lr1eH5iZKLPKWQO+tmy1VcdYr/TzqhdHSvGuJXrZvfTFi9/rV57Eq0d7964Ri9O0vQ=="],
|
||||
|
||||
"@opentui/core-linux-arm64": ["@opentui/core-linux-arm64@0.2.15", "", { "os": "linux", "cpu": "arm64" }, "sha512-h+uyufselGT4afKMP8Lg4yUl5Kp+DJBlhu3XpWXhphE5Pnq5+f0uGBr4P+34CNcWxMsDnvagSQLFRCS4rGrOWA=="],
|
||||
"@opentui/core-linux-arm64": ["@opentui/core-linux-arm64@0.2.16", "", { "os": "linux", "cpu": "arm64" }, "sha512-4fCwRCfTtUgS/5QcSEkSuBjgQymSOUWXgrXG2ycrf3Swi0QhKDA/pVjwLrUJ6eF+/8mQyQSEV72T8MxMO3M2qg=="],
|
||||
|
||||
"@opentui/core-linux-x64": ["@opentui/core-linux-x64@0.2.15", "", { "os": "linux", "cpu": "x64" }, "sha512-jx+NImPq4wSp3Apfe7tlixiEJNnRyECTRJRWhGF6ZJz4PwFfgK2UHZKYR0DZHbV8nYawoDNQPJDXEWcoZShnMg=="],
|
||||
"@opentui/core-linux-x64": ["@opentui/core-linux-x64@0.2.16", "", { "os": "linux", "cpu": "x64" }, "sha512-KgQBGjiucw4e7gM+R8qOzHWBFhjCY1IfCrGjW3Wzxv2hKUlL+mPhelaeJwnEqtNxMUdVTYjlwlu3IHxslXMJWQ=="],
|
||||
|
||||
"@opentui/core-win32-arm64": ["@opentui/core-win32-arm64@0.2.15", "", { "os": "win32", "cpu": "arm64" }, "sha512-2SQQLvf3sgmToxrNika9AdcccKrjPJEn5jW6sSv0oEixNBzUzW41vSZZG4LM/V3lL8eg0LoYDnRZeKLB4gwSqQ=="],
|
||||
"@opentui/core-win32-arm64": ["@opentui/core-win32-arm64@0.2.16", "", { "os": "win32", "cpu": "arm64" }, "sha512-C6WqEI3VkXatXraMgSFXZjEXq0pzURGjRpFAJZYmuVDmpqE57o7E80Np2UkdZ6m5kpJDt4mRyu3krc/P825iNQ=="],
|
||||
|
||||
"@opentui/core-win32-x64": ["@opentui/core-win32-x64@0.2.15", "", { "os": "win32", "cpu": "x64" }, "sha512-SVMVgnC7LVEm+yVZKdmmhRBj/xAT94PanT+UCcHxaCWK+OLmv/AX+ohHq2m0odup6iXcEqj+7mAltO9fgJLFIg=="],
|
||||
"@opentui/core-win32-x64": ["@opentui/core-win32-x64@0.2.16", "", { "os": "win32", "cpu": "x64" }, "sha512-kCX3CMTns6DMCFDNTDV4sjmBKyA/iEvzaVhl/jYi4JRIVT2zcy1lo+lhXT5mPgYHmJZu8Uye6j3Zi3c7Z2Me5A=="],
|
||||
|
||||
"@opentui/keymap": ["@opentui/keymap@0.2.15", "", { "dependencies": { "@opentui/core": "0.2.15" }, "peerDependencies": { "@opentui/react": "0.2.15", "@opentui/solid": "0.2.15", "react": ">=19.2.0", "solid-js": "1.9.12" }, "optionalPeers": ["@opentui/react", "@opentui/solid", "react", "solid-js"] }, "sha512-wqQp6y7P2jZZJiOMwupxjGryuSWCs+njjglwW/xny9J17gomBmUvTIcIIWNG0Jv+EGO9ScBzCScGlwBHFhHyYw=="],
|
||||
"@opentui/keymap": ["@opentui/keymap@0.2.16", "", { "dependencies": { "@opentui/core": "0.2.16" }, "peerDependencies": { "@opentui/react": "0.2.16", "@opentui/solid": "0.2.16", "react": ">=19.2.0", "solid-js": "1.9.12" }, "optionalPeers": ["@opentui/react", "@opentui/solid", "react", "solid-js"] }, "sha512-YBLQfNLbU2kx49bjEY9rrFoNlvIoi5qNJfRcOt6frvnR3C6MLl0/8hZY+vMQ2PEQWeEiNejFnl1lQw+z4Nk2FQ=="],
|
||||
|
||||
"@opentui/solid": ["@opentui/solid@0.2.15", "", { "dependencies": { "@babel/core": "7.28.0", "@babel/preset-typescript": "7.27.1", "@opentui/core": "0.2.15", "babel-plugin-module-resolver": "5.0.2", "babel-preset-solid": "1.9.12", "entities": "7.0.1", "s-js": "^0.4.9" }, "peerDependencies": { "solid-js": "1.9.12" } }, "sha512-CViepAjsCWXwrLndMt+qlLo7cooVX7DXwSJHNizw7mfrRJtOPzSYJZCIk1vF4IJTWffCHygoYMe3uSeKvzAcbw=="],
|
||||
"@opentui/solid": ["@opentui/solid@0.2.16", "", { "dependencies": { "@babel/core": "7.28.0", "@babel/preset-typescript": "7.27.1", "@opentui/core": "0.2.16", "babel-plugin-module-resolver": "5.0.2", "babel-preset-solid": "1.9.12", "entities": "7.0.1", "s-js": "^0.4.9" }, "peerDependencies": { "solid-js": "1.9.12" } }, "sha512-2Q+v1PPpXXr+sALi9Aj6I5Jvo7xDfbmstYjRLL7lW3Hghh9i7ONQKpt/gyDDRbhSsYrhxKYTNenF9OxgoXkTHg=="],
|
||||
|
||||
"@oslojs/asn1": ["@oslojs/asn1@1.0.0", "", { "dependencies": { "@oslojs/binary": "1.0.0" } }, "sha512-zw/wn0sj0j0QKbIXfIlnEcTviaCzYOY3V5rAyjR6YtOByFtJiT574+8p9Wlach0lZH9fddD4yb9laEAIl4vXQA=="],
|
||||
|
||||
@@ -5138,7 +5158,7 @@
|
||||
|
||||
"write-file-atomic": ["write-file-atomic@7.0.1", "", { "dependencies": { "signal-exit": "^4.0.1" } }, "sha512-OTIk8iR8/aCRWBqvxrzxR0hgxWpnYBblY1S5hDWBQfk/VFmJwzmJgQFN3WsoUKHISv2eAwe+PpbUzyL1CKTLXg=="],
|
||||
|
||||
"ws": ["ws@8.18.0", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw=="],
|
||||
"ws": ["ws@8.21.0", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-Vsp28b7DRcimFQvrqu2Wek3z1iYxDCWqHYB8Qsnk/S4RfaCQzPGPyBNuVjJV3cd6UiKtUtp6sNM77gWvzcCH+g=="],
|
||||
|
||||
"wsl-utils": ["wsl-utils@0.3.1", "", { "dependencies": { "is-wsl": "^3.1.0", "powershell-utils": "^0.1.0" } }, "sha512-g/eziiSUNBSsdDJtCLB8bdYEUMj4jR7AGeUo96p/3dTafgjHhpF4RiCFPiRILwjQoDXx5MqkBr4fwWtR3Ky4Wg=="],
|
||||
|
||||
@@ -5496,6 +5516,8 @@
|
||||
|
||||
"@cloudflare/kv-asset-handler/mime": ["mime@3.0.0", "", { "bin": { "mime": "cli.js" } }, "sha512-jSCU7/VB1loIWBZe14aEYHU/+1UMEHoaO7qxCOVJOw9GgH72VAWppxNcjU+x9a2k3GSIBXNKxXQFqRvvZ7vr3A=="],
|
||||
|
||||
"@cloudflare/vite-plugin/ws": ["ws@8.18.0", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw=="],
|
||||
|
||||
"@cspotcode/source-map-support/@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.9", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.0.3", "@jridgewell/sourcemap-codec": "^1.4.10" } }, "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ=="],
|
||||
|
||||
"@develar/schema-utils/ajv": ["ajv@6.14.0", "", { "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", "json-schema-traverse": "^0.4.1", "uri-js": "^4.2.2" } }, "sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw=="],
|
||||
@@ -5958,6 +5980,8 @@
|
||||
|
||||
"miniflare/undici": ["undici@7.14.0", "", {}, "sha512-Vqs8HTzjpQXZeXdpsfChQTlafcMQaaIwnGwLam1wudSSjlJeQ3bw1j+TLPePgrCnCpUXx7Ba5Pdpf5OBih62NQ=="],
|
||||
|
||||
"miniflare/ws": ["ws@8.18.0", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw=="],
|
||||
|
||||
"miniflare/zod": ["zod@3.22.3", "", {}, "sha512-EjIevzuJRiRPbVH4mGc8nApb/lVLKVpmUhAaR5R5doKGfAnGJ6Gr3CViAVjP+4FWSxCsybeWQdcgCtbX+7oZug=="],
|
||||
|
||||
"minipass-flush/minipass": ["minipass@3.3.6", "", { "dependencies": { "yallist": "^4.0.0" } }, "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw=="],
|
||||
@@ -6074,6 +6098,8 @@
|
||||
|
||||
"storybook/open": ["open@10.2.0", "", { "dependencies": { "default-browser": "^5.2.1", "define-lazy-prop": "^3.0.0", "is-inside-container": "^1.0.0", "wsl-utils": "^0.1.0" } }, "sha512-YgBpdJHPyQ2UE5x+hlSXcnejzAvD0b22U2OuAP+8OnlJT+PjWPxtgmGqKKc+RgTM63U9gN0YzrYc71R2WT/hTA=="],
|
||||
|
||||
"storybook/ws": ["ws@8.18.0", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw=="],
|
||||
|
||||
"storybook-solidjs-vite/vite-plugin-solid": ["vite-plugin-solid@2.11.12", "", { "dependencies": { "@babel/core": "^7.23.3", "@types/babel__core": "^7.20.4", "babel-preset-solid": "^1.8.4", "merge-anything": "^5.1.7", "solid-refresh": "^0.6.3", "vitefu": "^1.0.4" }, "peerDependencies": { "@testing-library/jest-dom": "^5.16.6 || ^5.17.0 || ^6.*", "solid-js": "^1.7.2", "vite": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0" }, "optionalPeers": ["@testing-library/jest-dom"] }, "sha512-FgjPcx2OwX9h6f28jli7A4bG7PP3te8uyakE5iqsmpq3Jqi1TWLgSroC9N6cMfGRU2zXsl4Q6ISvTr2VL0QHpA=="],
|
||||
|
||||
"string-width-cjs/emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="],
|
||||
|
||||
+2
-2
@@ -209,8 +209,8 @@ const ingestConfig = new sst.Linkable("LakeIngestConfig", {
|
||||
const ingestService = new sst.aws.Service("LakeIngestService", {
|
||||
cluster: lakeCluster,
|
||||
architecture: "arm64",
|
||||
cpu: "0.5 vCPU",
|
||||
memory: "1 GB",
|
||||
cpu: "1 vCPU",
|
||||
memory: "4 GB",
|
||||
image: {
|
||||
context: ".",
|
||||
dockerfile: "packages/stats/server/Dockerfile",
|
||||
|
||||
+1
-3
@@ -5,10 +5,8 @@ export const domain = (() => {
|
||||
})()
|
||||
|
||||
export const zoneID = "430ba34c138cfb5360826c4909f99be8"
|
||||
// Dev owns the shared AWS lake/stats infra for all non-production stages.
|
||||
export const awsStage = $app.stage === "production" ? "production" : "dev"
|
||||
// Temporarily omit AWS infra so SST removes the lake/stats resources.
|
||||
export const deployAws = false
|
||||
export const deployAws = $app.stage === awsStage
|
||||
|
||||
new cloudflare.RegionalHostname("RegionalHostname", {
|
||||
hostname: domain,
|
||||
|
||||
+4
-4
@@ -1,8 +1,8 @@
|
||||
{
|
||||
"nodeModules": {
|
||||
"x86_64-linux": "sha256-FT8N4SBP7OmVu73OwNyPJvBoxFd2+IXzNnFubB8y6J0=",
|
||||
"aarch64-linux": "sha256-rGst01voY1/qr/fIMEgfL2zep/+W2RhjJh5pKs+/P3s=",
|
||||
"aarch64-darwin": "sha256-+oTdR3CuR88Ra3QQsDn7ixNwJf0YjOxySTSiRsPPUOg=",
|
||||
"x86_64-darwin": "sha256-3ZkB83KWtp6N7u9xhzmCouv27UuisXYvr2SBap4k++E="
|
||||
"x86_64-linux": "sha256-KFVkxs0jtN1EXvwuxFMOlWg6BDr8L6K/wJZ/N73p8cI=",
|
||||
"aarch64-linux": "sha256-7OLglRBBt5prwb9e0I0iMTXi3MrDfDI+8xcDrxtq/JI=",
|
||||
"aarch64-darwin": "sha256-Q7iE6xJyomDnvqUgsHh+FlCnJFI6vK0zJSfI9BBYitQ=",
|
||||
"x86_64-darwin": "sha256-9lVsq2Bw74Vgphg6GSo/HxCtDXUyabxcS/vE1Um/Hfw="
|
||||
}
|
||||
}
|
||||
|
||||
+4
-4
@@ -10,7 +10,7 @@
|
||||
"dev:desktop": "bun --cwd packages/desktop dev",
|
||||
"dev:web": "bun --cwd packages/app dev",
|
||||
"dev:console": "ulimit -n 10240 2>/dev/null; bun run --cwd packages/console/app dev",
|
||||
"dev:stats": "bun run --cwd packages/stats/app dev",
|
||||
"dev:stats": "bun sst shell --stage=production -- bun run --cwd packages/stats/app dev",
|
||||
"dev:storybook": "bun --cwd packages/storybook storybook",
|
||||
"lint": "oxlint",
|
||||
"typecheck": "bun turbo typecheck",
|
||||
@@ -38,9 +38,9 @@
|
||||
"@types/cross-spawn": "6.0.6",
|
||||
"@octokit/rest": "22.0.0",
|
||||
"@hono/zod-validator": "0.4.2",
|
||||
"@opentui/core": "0.2.15",
|
||||
"@opentui/keymap": "0.2.15",
|
||||
"@opentui/solid": "0.2.15",
|
||||
"@opentui/core": "0.2.16",
|
||||
"@opentui/keymap": "0.2.16",
|
||||
"@opentui/solid": "0.2.16",
|
||||
"ulid": "3.0.1",
|
||||
"@kobalte/core": "0.13.11",
|
||||
"@types/luxon": "3.7.1",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@opencode-ai/app",
|
||||
"version": "1.15.10",
|
||||
"version": "1.15.11",
|
||||
"description": "",
|
||||
"type": "module",
|
||||
"exports": {
|
||||
|
||||
@@ -14,6 +14,7 @@ import { QueryClient, QueryClientProvider } from "@tanstack/solid-query"
|
||||
import { Effect } from "effect"
|
||||
import {
|
||||
type Component,
|
||||
createEffect,
|
||||
createMemo,
|
||||
createResource,
|
||||
createSignal,
|
||||
@@ -40,7 +41,7 @@ import { NotificationProvider } from "@/context/notification"
|
||||
import { PermissionProvider } from "@/context/permission"
|
||||
import { PromptProvider } from "@/context/prompt"
|
||||
import { ServerConnection, ServerProvider, serverName, useServer } from "@/context/server"
|
||||
import { SettingsProvider } from "@/context/settings"
|
||||
import { SettingsProvider, useSettings } from "@/context/settings"
|
||||
import { TerminalProvider } from "@/context/terminal"
|
||||
import DirectoryLayout from "@/pages/directory-layout"
|
||||
import Layout from "@/pages/layout"
|
||||
@@ -48,11 +49,6 @@ import { ErrorPage } from "./pages/error"
|
||||
import { useCheckServerHealth } from "./utils/server-health"
|
||||
import { ServersProvider } from "./context/servers"
|
||||
|
||||
if (import.meta.env.VITE_OPENCODE_CHANNEL !== "prod") {
|
||||
document.body.classList.remove("text-12-regular")
|
||||
document.body.classList.add("font-(family-name:--font-family-text)", "text-[13px]", "font-[440]")
|
||||
}
|
||||
|
||||
const HomeRoute = lazy(() => import("@/pages/home"))
|
||||
const Session = lazy(() => import("@/pages/session"))
|
||||
|
||||
@@ -97,9 +93,26 @@ function QueryProvider(props: ParentProps) {
|
||||
return <QueryClientProvider client={client}>{props.children}</QueryClientProvider>
|
||||
}
|
||||
|
||||
function BodyDesignClass() {
|
||||
const settings = useSettings()
|
||||
|
||||
createEffect(() => {
|
||||
if (typeof document === "undefined") return
|
||||
|
||||
const enabled = settings.general.newLayoutDesigns()
|
||||
document.body.classList.toggle("text-12-regular", !enabled)
|
||||
document.body.classList.toggle("font-(family-name:--font-family-text)", enabled)
|
||||
document.body.classList.toggle("text-[13px]", enabled)
|
||||
document.body.classList.toggle("font-[440]", enabled)
|
||||
})
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
function AppShellProviders(props: ParentProps) {
|
||||
return (
|
||||
<SettingsProvider>
|
||||
<BodyDesignClass />
|
||||
<PermissionProvider>
|
||||
<LayoutProvider>
|
||||
<NotificationProvider>
|
||||
|
||||
@@ -79,8 +79,6 @@ import { pathKey } from "@/utils/path-key"
|
||||
import { base64Encode } from "@opencode-ai/core/util/encode"
|
||||
import { displayName } from "@/pages/layout/helpers"
|
||||
|
||||
const USE_V2_INPUT = import.meta.env.VITE_OPENCODE_CHANNEL !== "prod"
|
||||
|
||||
interface PromptInputProps {
|
||||
class?: string
|
||||
variant?: "dock" | "new-session"
|
||||
@@ -1456,7 +1454,7 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
|
||||
t={(key) => language.t(key as Parameters<typeof language.t>[0])}
|
||||
/>
|
||||
<Switch>
|
||||
<Match when={USE_V2_INPUT}>
|
||||
<Match when={settings.general.newLayoutDesigns()}>
|
||||
<div class="flex flex-col gap-3">
|
||||
<DockShellForm
|
||||
data-component={newSession() ? "session-new-composer" : "session-composer"}
|
||||
@@ -1528,7 +1526,7 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
|
||||
onKeyDown={handleKeyDown}
|
||||
classList={{
|
||||
"select-text": true,
|
||||
"min-h-[52px] w-full px-4 pt-4 pb-2 focus:outline-none whitespace-pre-wrap leading-5 text-[13px] font-[440] text-v2-text-text-faint [font-family:Inter,var(--font-family-sans)]": true,
|
||||
"min-h-[52px] w-full px-4 pt-4 pb-2 focus:outline-none whitespace-pre-wrap leading-5 text-[13px] font-[440] text-v2-text-text-base": true,
|
||||
"[&_[data-type=file]]:text-syntax-property": true,
|
||||
"[&_[data-type=agent]]:text-syntax-type": true,
|
||||
"font-mono!": store.mode === "shell",
|
||||
|
||||
@@ -45,8 +45,6 @@ const OPEN_APPS = [
|
||||
"sublime-text",
|
||||
] as const
|
||||
|
||||
const USE_V2_TITLEBAR = import.meta.env.VITE_OPENCODE_CHANNEL !== "prod"
|
||||
|
||||
type OpenApp = (typeof OPEN_APPS)[number]
|
||||
type OS = "macos" | "windows" | "linux" | "unknown"
|
||||
|
||||
@@ -157,11 +155,11 @@ export function SessionHeader() {
|
||||
})
|
||||
const hotkey = createMemo(() => command.keybind("file.open"))
|
||||
const os = createMemo(() => detectOS(platform))
|
||||
const isDesktopV2 = platform.platform === "desktop" && USE_V2_TITLEBAR
|
||||
const search = createMemo(() => (isDesktopV2 ? settings.general.showSearch() : true))
|
||||
const tree = createMemo(() => (isDesktopV2 ? settings.general.showFileTree() : true))
|
||||
const term = createMemo(() => (isDesktopV2 ? settings.general.showTerminal() : true))
|
||||
const status = createMemo(() => (isDesktopV2 ? settings.general.showStatus() : true))
|
||||
const isDesktopV2 = createMemo(() => platform.platform === "desktop" && settings.general.newLayoutDesigns())
|
||||
const search = createMemo(() => (isDesktopV2() ? settings.general.showSearch() : true))
|
||||
const tree = createMemo(() => (isDesktopV2() ? settings.general.showFileTree() : true))
|
||||
const term = createMemo(() => (isDesktopV2() ? settings.general.showTerminal() : true))
|
||||
const status = createMemo(() => (isDesktopV2() ? settings.general.showStatus() : true))
|
||||
|
||||
const [exists, setExists] = createStore<Partial<Record<OpenApp, boolean>>>({
|
||||
finder: true,
|
||||
|
||||
@@ -399,6 +399,18 @@ export const SettingsGeneral: Component = () => {
|
||||
/>
|
||||
</div>
|
||||
</SettingsRow>
|
||||
|
||||
<SettingsRow
|
||||
title={language.t("settings.general.row.newLayoutDesigns.title")}
|
||||
description={language.t("settings.general.row.newLayoutDesigns.description")}
|
||||
>
|
||||
<div data-action="settings-new-layout-designs">
|
||||
<Switch
|
||||
checked={settings.general.newLayoutDesigns()}
|
||||
onChange={(checked) => settings.general.setNewLayoutDesigns(checked)}
|
||||
/>
|
||||
</div>
|
||||
</SettingsRow>
|
||||
</SettingsList>
|
||||
</div>
|
||||
)
|
||||
@@ -802,7 +814,7 @@ export const SettingsGeneral: Component = () => {
|
||||
|
||||
<DisplaySection />
|
||||
|
||||
<Show when={desktop() && import.meta.env.VITE_OPENCODE_CHANNEL !== "prod"}>
|
||||
<Show when={desktop()}>
|
||||
<AdvancedSection />
|
||||
</Show>
|
||||
</div>
|
||||
|
||||
@@ -55,7 +55,6 @@ const legacyTitlebarHeight = 40
|
||||
const v2TitlebarHeight = 44
|
||||
const minTitlebarZoom = 0.25
|
||||
const windowsControlsBaseWidth = 138 // 3 native Windows caption buttons at 46px each.
|
||||
const USE_V2_TITLEBAR = import.meta.env.VITE_OPENCODE_CHANNEL !== "prod"
|
||||
|
||||
const makeSessionHref = (b64Dir: string, sessionId: string) => `/${b64Dir}/session/${sessionId}`
|
||||
|
||||
@@ -75,6 +74,7 @@ export function Titlebar(props: { update?: TitlebarUpdate }) {
|
||||
const navigate = useNavigate()
|
||||
const location = useLocation()
|
||||
const params = useParams()
|
||||
const useV2Titlebar = createMemo(() => settings.general.newLayoutDesigns())
|
||||
|
||||
const mac = createMemo(() => platform.platform === "desktop" && platform.os === "macos")
|
||||
const windows = createMemo(() => platform.platform === "desktop" && platform.os === "windows")
|
||||
@@ -85,7 +85,7 @@ export function Titlebar(props: { update?: TitlebarUpdate }) {
|
||||
const titlebarZoom = () => (windows() ? Math.max(zoom(), minTitlebarZoom) : zoom())
|
||||
const counterZoom = () => (windows() && titlebarZoom() < 1 ? 1 / titlebarZoom() : 1)
|
||||
const minHeight = () => {
|
||||
const height = USE_V2_TITLEBAR ? v2TitlebarHeight : legacyTitlebarHeight
|
||||
const height = useV2Titlebar() ? v2TitlebarHeight : legacyTitlebarHeight
|
||||
if (mac()) return `${height / zoom()}px`
|
||||
if (windows()) return `${height / Math.min(titlebarZoom(), 1)}px`
|
||||
return undefined
|
||||
@@ -119,7 +119,7 @@ export function Titlebar(props: { update?: TitlebarUpdate }) {
|
||||
const canBack = createMemo(() => history.index > 0)
|
||||
const canForward = createMemo(() => history.index < history.stack.length - 1)
|
||||
const hasProjects = createMemo(() => layout.projects.list().length > 0)
|
||||
const nav = createMemo(() => (USE_V2_TITLEBAR ? settings.general.showNavigation() : true))
|
||||
const nav = createMemo(() => (useV2Titlebar() ? settings.general.showNavigation() : true))
|
||||
const updateState = createMemo<TitlebarUpdatePillState>(() => {
|
||||
const version = props.update?.version()
|
||||
return {
|
||||
@@ -222,8 +222,8 @@ export function Titlebar(props: { update?: TitlebarUpdate }) {
|
||||
<header
|
||||
classList={{
|
||||
"shrink-0 relative overflow-hidden flex flex-row": true,
|
||||
"h-11 bg-v2-background-bg-deep": USE_V2_TITLEBAR,
|
||||
"h-10 bg-background-base": !USE_V2_TITLEBAR,
|
||||
"h-11 bg-v2-background-bg-deep": useV2Titlebar(),
|
||||
"h-10 bg-background-base": !useV2Titlebar(),
|
||||
}}
|
||||
style={{
|
||||
"min-height": minHeight(),
|
||||
@@ -239,7 +239,7 @@ export function Titlebar(props: { update?: TitlebarUpdate }) {
|
||||
onDblClick={maximize}
|
||||
>
|
||||
<Switch>
|
||||
<Match when={USE_V2_TITLEBAR}>
|
||||
<Match when={useV2Titlebar()}>
|
||||
{(_) => {
|
||||
const serverSync = useServerSync()
|
||||
const navigate = useNavigate()
|
||||
@@ -490,7 +490,6 @@ export function Titlebar(props: { update?: TitlebarUpdate }) {
|
||||
project={projectForSession(tab.info, projects(), projectByID())}
|
||||
directory={tab.dir}
|
||||
onClose={() => tabsStoreActions.removeTab(tab.href)}
|
||||
hideClose={tabsEnriched().length < 2}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
@@ -737,14 +736,13 @@ function TabNavItem(props: {
|
||||
title: string
|
||||
project?: LocalProject
|
||||
directory: string
|
||||
hideClose?: boolean
|
||||
onClose: () => void
|
||||
}) {
|
||||
const match = useMatch(() => props.href)
|
||||
const isActive = () => !!match()
|
||||
return (
|
||||
<div
|
||||
class="group relative flex h-7 min-w-24 max-w-60 flex-row items-center gap-1.5 overflow-hidden whitespace-nowrap rounded-[6px] bg-[var(--tab-bg)] pl-1.5 [--tab-bg:var(--v2-background-bg-deep)] hover:[--tab-bg:var(--v2-background-bg-layer-02)] data-[active='true']:[--tab-bg:var(--v2-background-bg-layer-02)]"
|
||||
class="group relative flex h-7 min-w-24 max-w-60 flex-row items-center gap-1.5 overflow-hidden whitespace-nowrap rounded-[6px] bg-[var(--tab-bg)] px-1.5 [--tab-bg:var(--v2-background-bg-deep)] hover:[--tab-bg:var(--v2-background-bg-layer-02)] data-[active='true']:[--tab-bg:var(--v2-background-bg-layer-02)]"
|
||||
data-active={isActive()}
|
||||
>
|
||||
<a
|
||||
@@ -755,7 +753,7 @@ function TabNavItem(props: {
|
||||
<span class="text-clip leading-5">{props.title}</span>
|
||||
</a>
|
||||
|
||||
<div class="absolute right-0 inset-y-0 flex flex-row items-center pr-1 py-1 w-8 pl-2">
|
||||
<div class="absolute not-group-hover:not-group-data-[active=true]:left-52 group-hover:right-0 group-data-[active=true]:right-0 inset-y-0 flex flex-row items-center pr-1 py-1 w-8 pl-2">
|
||||
<div
|
||||
class="absolute inset-0 bg-(image:--inactive-bg) group-hover:bg-(image:--active-bg) group-data-[active=true]:bg-(image:--active-bg)"
|
||||
style={{
|
||||
@@ -766,7 +764,7 @@ function TabNavItem(props: {
|
||||
<IconButtonV2
|
||||
size="small"
|
||||
variant="ghost-muted"
|
||||
class="opacity-0 group-hover:opacity-100 group-data-[active='true']:opacity-100"
|
||||
class="opacity-0 group-hover:opacity-100 group-data-[active='true']:opacity-100 z-10"
|
||||
onClick={props.onClose}
|
||||
icon={<IconV2 name="xmark-small" />}
|
||||
/>
|
||||
|
||||
@@ -33,6 +33,7 @@ export interface Settings {
|
||||
editToolPartsExpanded: boolean
|
||||
showSessionProgressBar: boolean
|
||||
showCustomAgents: boolean
|
||||
newLayoutDesigns?: boolean
|
||||
}
|
||||
updates: {
|
||||
startup: boolean
|
||||
@@ -54,6 +55,7 @@ export interface Settings {
|
||||
export const monoDefault = "System Mono"
|
||||
export const sansDefault = "System Sans"
|
||||
export const terminalDefault = "JetBrainsMono Nerd Font Mono"
|
||||
export const newLayoutDesignsDefault = import.meta.env.VITE_OPENCODE_CHANNEL !== "prod"
|
||||
|
||||
const monoFallback =
|
||||
'ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace'
|
||||
@@ -242,6 +244,10 @@ export const { use: useSettings, provider: SettingsProvider } = createSimpleCont
|
||||
setShowCustomAgents(value: boolean) {
|
||||
setStore("general", "showCustomAgents", value)
|
||||
},
|
||||
newLayoutDesigns: withFallback(() => store.general?.newLayoutDesigns, newLayoutDesignsDefault),
|
||||
setNewLayoutDesigns(value: boolean) {
|
||||
setStore("general", "newLayoutDesigns", value)
|
||||
},
|
||||
},
|
||||
updates: {
|
||||
startup: withFallback(() => store.updates?.startup, defaultSettings.updates.startup),
|
||||
|
||||
@@ -791,6 +791,8 @@ export const dict = {
|
||||
"settings.general.row.showSessionProgressBar.title": "Show session progress bar",
|
||||
"settings.general.row.showSessionProgressBar.description":
|
||||
"Display the animated progress bar at the top of the session when the agent is working",
|
||||
"settings.general.row.newLayoutDesigns.title": "New layout and designs",
|
||||
"settings.general.row.newLayoutDesigns.description": "Enable the redesigned layout, home, composer, and session UI",
|
||||
"settings.general.row.pinchZoom.title": "Pinch to zoom",
|
||||
"settings.general.row.pinchZoom.description": "Allow trackpad pinch and Ctrl-scroll gestures to zoom",
|
||||
|
||||
|
||||
+109
-104
@@ -31,8 +31,8 @@ import { messageAgentColor } from "@/utils/agent"
|
||||
import { sessionPermissionRequest } from "@/pages/session/composer/session-request-tree"
|
||||
import { ServerHealthIndicator } from "@/components/server/server-row"
|
||||
import { useServers } from "@/context/servers"
|
||||
import { useSettings } from "@/context/settings"
|
||||
|
||||
const USE_HOME_DESIGN = import.meta.env.VITE_OPENCODE_CHANNEL !== "prod"
|
||||
const HOME_SESSION_LIMIT = 15
|
||||
const HOME_ROW =
|
||||
"flex min-w-0 w-full shrink-0 cursor-default items-center rounded-[6px] border-0 bg-transparent text-left text-v2-text-text-muted transition-colors duration-[120ms] ease-in-out hover:bg-v2-overlay-simple-overlay-hover focus-visible:bg-v2-overlay-simple-overlay-hover focus-visible:outline-none"
|
||||
@@ -52,8 +52,12 @@ type HomeSessionGroup = {
|
||||
}
|
||||
|
||||
export default function Home() {
|
||||
if (USE_HOME_DESIGN) return <HomeDesign />
|
||||
return <LegacyHome />
|
||||
const settings = useSettings()
|
||||
return (
|
||||
<Show when={settings.general.newLayoutDesigns()} fallback={<LegacyHome />}>
|
||||
<HomeDesign />
|
||||
</Show>
|
||||
)
|
||||
}
|
||||
|
||||
function HomeDesign() {
|
||||
@@ -68,13 +72,11 @@ function HomeDesign() {
|
||||
const [state, setState] = createStore({ search: "", project: undefined as string | undefined })
|
||||
|
||||
const projects = createMemo(() => layout.projects.list())
|
||||
const selectedProject = createMemo(
|
||||
() => projects().find((project) => project.worktree === state.project) ?? projects()[0],
|
||||
)
|
||||
const selectedProject = createMemo(() => projects().find((project) => project.worktree === state.project))
|
||||
const directories = (project: LocalProject) => [project.worktree, ...(project.sandboxes ?? [])]
|
||||
const projectDirectories = createMemo(() => {
|
||||
const project = selectedProject()
|
||||
if (!project) return []
|
||||
if (!project) return [...projects().flatMap((project) => directories(project))]
|
||||
return directories(project)
|
||||
})
|
||||
const search = createMemo(() => state.search.trim())
|
||||
@@ -89,8 +91,8 @@ function HomeDesign() {
|
||||
const projectByID = createMemo(
|
||||
() => new Map(projects().flatMap((project) => (project.id ? [[project.id, project] as const] : []))),
|
||||
)
|
||||
const records = createMemo(() =>
|
||||
[
|
||||
const records = createMemo(() => {
|
||||
return [
|
||||
...new Map(
|
||||
projectDirectories()
|
||||
.flatMap((directory) => sortedRootSessions(sync.child(directory, { bootstrap: false })[0], Date.now()))
|
||||
@@ -112,8 +114,8 @@ function HomeDesign() {
|
||||
if (!value) return true
|
||||
return `${record.session.title} ${record.projectName}`.toLowerCase().includes(value)
|
||||
})
|
||||
.slice(0, HOME_SESSION_LIMIT),
|
||||
)
|
||||
.slice(0, HOME_SESSION_LIMIT)
|
||||
})
|
||||
const groups = createMemo(() => groupSessions(records(), language))
|
||||
|
||||
function selectProject(directory: string) {
|
||||
@@ -220,7 +222,7 @@ function HomeDesign() {
|
||||
aria-label={language.t("sidebar.project.recentSessions")}
|
||||
>
|
||||
<Show
|
||||
when={selectedProject()}
|
||||
when={projectDirectories().length > 0}
|
||||
fallback={
|
||||
<HomeEmptyState
|
||||
icon="folder-add-left"
|
||||
@@ -298,8 +300,8 @@ function HomeProjectColumn(props: {
|
||||
const layout = useLayout()
|
||||
const projects = createMemo(() => layout.projects.list())
|
||||
return (
|
||||
<aside class="flex min-w-0 flex-col lg:pt-[52px]" aria-label={props.language.t("home.projects")}>
|
||||
<div class="flex h-7 min-w-0 items-center justify-between pl-3">
|
||||
<aside class="flex min-w-0 flex-col lg:pt-[52px] gap-4" aria-label={props.language.t("home.projects")}>
|
||||
<div class="flex h-7 min-w-0 items-center justify-between pl-1.5">
|
||||
<div class={HOME_SECTION_LABEL}>{props.language.t("home.projects")}</div>
|
||||
<IconButtonV2
|
||||
data-action="home-add-project"
|
||||
@@ -311,8 +313,8 @@ function HomeProjectColumn(props: {
|
||||
aria-label={props.language.t("home.project.add")}
|
||||
/>
|
||||
</div>
|
||||
<For
|
||||
each={servers.list()}
|
||||
<Show
|
||||
when={servers.list().length > 1}
|
||||
fallback={
|
||||
<ProjectList
|
||||
projects={projects()}
|
||||
@@ -328,61 +330,63 @@ function HomeProjectColumn(props: {
|
||||
/>
|
||||
}
|
||||
>
|
||||
{(server) => {
|
||||
const key = ServerConnection.key(server)
|
||||
const healthy = () => !!servers.health[key]?.healthy
|
||||
const [open, setOpen] = createSignal(true)
|
||||
<For each={servers.list()}>
|
||||
{(server) => {
|
||||
const key = ServerConnection.key(server)
|
||||
const healthy = () => !!servers.health[key]?.healthy
|
||||
const [open, setOpen] = createSignal(true)
|
||||
|
||||
return (
|
||||
<div class="mt-4 max-h-[min(572px,calc(100vh_-_300px))] min-w-0 [scrollbar-width:none] [&::-webkit-scrollbar]:hidden">
|
||||
<div class="relative h-7 group">
|
||||
<button
|
||||
class="w-full h-full px-1.5 gap-2 flex flex-row items-center hover:not-disabled:bg-v2-overlay-simple-overlay-hover rounded-[4px]"
|
||||
disabled={!healthy()}
|
||||
onClick={() => setOpen((o) => !o)}
|
||||
>
|
||||
<div class="size-4 flex items-center justify-center">
|
||||
<ServerHealthIndicator health={servers.health[key]} />
|
||||
</div>
|
||||
<div class="flex flex-row items-center gap-1">
|
||||
<span>{server.displayName ?? new URL(server.http.url).host}</span>
|
||||
<Show when={healthy()}>
|
||||
<IconV2
|
||||
name="outline-chevron-down"
|
||||
class="text-v2-icon-icon-muted data-[open=false]:-rotate-90"
|
||||
data-open={open()}
|
||||
/>
|
||||
</Show>
|
||||
</div>
|
||||
</button>
|
||||
<IconButtonV2
|
||||
class="absolute right-1 inset-y-1 opacity-0 group-hover:opacity-100"
|
||||
name="out"
|
||||
variant="ghost-muted"
|
||||
size="small"
|
||||
icon={<IconV2 name="outline-dots" class="text-v2-icon-icon-muted" />}
|
||||
/>
|
||||
return (
|
||||
<div class="max-h-[min(572px,calc(100vh_-_300px))] min-w-0 [scrollbar-width:none] [&::-webkit-scrollbar]:hidden">
|
||||
<div class="relative h-7 group">
|
||||
<button
|
||||
class="w-full h-full px-1.5 gap-2 flex flex-row items-center hover:not-disabled:bg-v2-overlay-simple-overlay-hover rounded-[4px]"
|
||||
disabled={!healthy()}
|
||||
onClick={() => setOpen((o) => !o)}
|
||||
>
|
||||
<div class="size-4 flex items-center justify-center">
|
||||
<ServerHealthIndicator health={servers.health[key]} />
|
||||
</div>
|
||||
<div class="flex flex-row items-center gap-1">
|
||||
<span>{server.displayName ?? new URL(server.http.url).host}</span>
|
||||
<Show when={healthy()}>
|
||||
<IconV2
|
||||
name="outline-chevron-down"
|
||||
class="text-v2-icon-icon-muted data-[open=false]:-rotate-90"
|
||||
data-open={open()}
|
||||
/>
|
||||
</Show>
|
||||
</div>
|
||||
</button>
|
||||
<IconButtonV2
|
||||
class="absolute right-1 inset-y-1 opacity-0 group-hover:opacity-100"
|
||||
name="out"
|
||||
variant="ghost-muted"
|
||||
size="small"
|
||||
icon={<IconV2 name="outline-dots" class="text-v2-icon-icon-muted" />}
|
||||
/>
|
||||
</div>
|
||||
<Show when={healthy() && open()}>
|
||||
<div class="h-px bg-v2-border-border-base mx-3 my-1" />
|
||||
<ProjectList
|
||||
projects={projects()}
|
||||
selectedProject={props.selectedProject}
|
||||
onSelectedProjectChange={props.selectProject}
|
||||
onChooseProject={props.chooseProject}
|
||||
openNewSession={props.openNewSession}
|
||||
editProject={props.editProject}
|
||||
closeProject={props.closeProject}
|
||||
clearNotifications={props.clearNotifications}
|
||||
unseenCount={props.unseenCount}
|
||||
language={props.language}
|
||||
/>
|
||||
</Show>
|
||||
</div>
|
||||
<Show when={healthy() && open()}>
|
||||
<div class="h-px bg-v2-border-border-base mx-3 my-1" />
|
||||
<ProjectList
|
||||
projects={projects()}
|
||||
selectedProject={props.selectedProject}
|
||||
onSelectedProjectChange={props.selectProject}
|
||||
onChooseProject={props.chooseProject}
|
||||
openNewSession={props.openNewSession}
|
||||
editProject={props.editProject}
|
||||
closeProject={props.closeProject}
|
||||
clearNotifications={props.clearNotifications}
|
||||
unseenCount={props.unseenCount}
|
||||
language={props.language}
|
||||
/>
|
||||
</Show>
|
||||
</div>
|
||||
)
|
||||
}}
|
||||
</For>
|
||||
<div class="mt-4 flex min-w-0 flex-col gap-1">
|
||||
)
|
||||
}}
|
||||
</For>
|
||||
</Show>
|
||||
<div class="flex min-w-0 flex-col gap-1">
|
||||
<button
|
||||
type="button"
|
||||
class={`${HOME_PROJECT_NAV_ROW} text-v2-text-text-faint [&>[data-slot=icon-svg]]:text-v2-icon-icon-muted`}
|
||||
@@ -416,12 +420,14 @@ function HomeProjectRow(props: {
|
||||
language: ReturnType<typeof useLanguage>
|
||||
}) {
|
||||
const name = createMemo(() => displayName(props.project))
|
||||
const [menuOpen, setMenuOpen] = createSignal(false)
|
||||
|
||||
return (
|
||||
<div class="group/project relative flex h-8 min-w-0 items-center rounded-[6px] hover:bg-v2-overlay-simple-overlay-hover focus-within:bg-v2-overlay-simple-overlay-hover">
|
||||
<div class="group/project relative flex h-8 min-w-0 items-center rounded-[6px]">
|
||||
<button
|
||||
type="button"
|
||||
data-component="home-project-row"
|
||||
class={`${HOME_PROJECT_NAV_ROW} pr-16`}
|
||||
class={`${HOME_PROJECT_NAV_ROW} pr-16 peer`}
|
||||
classList={{ "bg-v2-overlay-simple-overlay-hover": props.selected }}
|
||||
data-selected={props.selected ? "" : undefined}
|
||||
aria-current={props.selected ? "page" : undefined}
|
||||
@@ -430,7 +436,37 @@ function HomeProjectRow(props: {
|
||||
<HomeProjectAvatar project={props.project} />
|
||||
<span>{name()}</span>
|
||||
</button>
|
||||
<div class="absolute right-1 top-1/2 flex -translate-y-1/2 items-center gap-0.5 opacity-0 transition-opacity group-hover/project:opacity-100 group-focus-within/project:opacity-100">
|
||||
<div
|
||||
class="absolute right-1 top-1/2 flex -translate-y-1/2 items-center gap-0.5 opacity-0 transition-opacity group-hover/project:opacity-100 peer-focus-visible:opacity-100 focus-within:opacity-100 data-[menu=true]:opacity-100"
|
||||
data-menu={menuOpen()}
|
||||
>
|
||||
<MenuV2 gutter={4} modal={false} placement="bottom-end" open={menuOpen()} onOpenChange={setMenuOpen}>
|
||||
<MenuV2.Trigger
|
||||
as={IconButtonV2}
|
||||
data-action="home-project-menu"
|
||||
variant="ghost-muted"
|
||||
size="small"
|
||||
icon={<IconV2 name="outline-dots" />}
|
||||
aria-label={props.language.t("common.moreOptions")}
|
||||
/>
|
||||
<MenuV2.Portal>
|
||||
<MenuV2.Content>
|
||||
<MenuV2.Item onSelect={() => props.openNewSession(props.project.worktree)}>
|
||||
{props.language.t("command.session.new")}
|
||||
</MenuV2.Item>
|
||||
<MenuV2.Item onSelect={() => props.editProject(props.project)}>
|
||||
{props.language.t("common.edit")}
|
||||
</MenuV2.Item>
|
||||
<MenuV2.Item disabled={props.unseenCount === 0} onSelect={() => props.clearNotifications(props.project)}>
|
||||
{props.language.t("sidebar.project.clearNotifications")}
|
||||
</MenuV2.Item>
|
||||
<MenuV2.Separator />
|
||||
<MenuV2.Item onSelect={() => props.closeProject(props.project.worktree)}>
|
||||
{props.language.t("common.close")}
|
||||
</MenuV2.Item>
|
||||
</MenuV2.Content>
|
||||
</MenuV2.Portal>
|
||||
</MenuV2>
|
||||
<IconButtonV2
|
||||
data-action="home-project-new-session"
|
||||
variant="ghost-muted"
|
||||
@@ -442,37 +478,6 @@ function HomeProjectRow(props: {
|
||||
props.openNewSession(props.project.worktree)
|
||||
}}
|
||||
/>
|
||||
<MenuV2 gutter={4} modal={false} placement="bottom-end">
|
||||
<MenuV2.Trigger
|
||||
as={IconButtonV2}
|
||||
data-action="home-project-menu"
|
||||
variant="ghost-muted"
|
||||
size="small"
|
||||
icon={<IconV2 name="menu" />}
|
||||
aria-label={props.language.t("common.moreOptions")}
|
||||
/>
|
||||
<MenuV2.Portal>
|
||||
<MenuV2.Content>
|
||||
<MenuV2.Item onSelect={() => props.openNewSession(props.project.worktree)}>
|
||||
<Icon name="new-session" size="small" />
|
||||
{props.language.t("command.session.new")}
|
||||
</MenuV2.Item>
|
||||
<MenuV2.Item onSelect={() => props.editProject(props.project)}>
|
||||
<Icon name="edit" size="small" />
|
||||
{props.language.t("common.edit")}
|
||||
</MenuV2.Item>
|
||||
<MenuV2.Item disabled={props.unseenCount === 0} onSelect={() => props.clearNotifications(props.project)}>
|
||||
<Icon name="circle-check" size="small" />
|
||||
{props.language.t("sidebar.project.clearNotifications")}
|
||||
</MenuV2.Item>
|
||||
<MenuV2.Separator />
|
||||
<MenuV2.Item onSelect={() => props.closeProject(props.project.worktree)}>
|
||||
<Icon name="close" size="small" />
|
||||
{props.language.t("common.close")}
|
||||
</MenuV2.Item>
|
||||
</MenuV2.Content>
|
||||
</MenuV2.Portal>
|
||||
</MenuV2>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
@@ -561,7 +566,7 @@ function HomeSessionGroupHeader(props: { title: string; onNewSession?: () => voi
|
||||
variant="ghost"
|
||||
size="normal"
|
||||
icon="edit"
|
||||
class="h-7 px-2 text-v2-text-text-muted [font-weight:530]"
|
||||
class="h-7 px-2 text-v2-text-text-muted"
|
||||
onClick={onNewSession()}
|
||||
>
|
||||
{language.t("command.session.new")}
|
||||
|
||||
+160
-160
@@ -89,8 +89,6 @@ import {
|
||||
import { ProjectDragOverlay, SortableProject, type ProjectSidebarContext } from "./layout/sidebar-project"
|
||||
import { SidebarContent } from "./layout/sidebar-shell"
|
||||
|
||||
const USE_NEW_DESIGN = import.meta.env.VITE_OPENCODE_CHANNEL !== "prod"
|
||||
|
||||
export default function Layout(props: ParentProps) {
|
||||
const [store, setStore, , ready] = persisted(
|
||||
Persist.global("layout.page", ["layout.page.v1"]),
|
||||
@@ -129,6 +127,7 @@ export default function Layout(props: ParentProps) {
|
||||
const command = useCommand()
|
||||
const theme = useTheme()
|
||||
const language = useLanguage()
|
||||
const newDesign = createMemo(() => settings.general.newLayoutDesigns())
|
||||
const initialDirectory = decode64(params.dir)
|
||||
const location = useLocation()
|
||||
const route = createMemo(() => {
|
||||
@@ -154,7 +153,7 @@ export default function Layout(props: ParentProps) {
|
||||
const currentDir = createMemo(() => route().dir)
|
||||
|
||||
const [state, setState] = createStore({
|
||||
autoselect: !initialDirectory && !USE_NEW_DESIGN,
|
||||
autoselect: !initialDirectory && !newDesign(),
|
||||
busyWorkspaces: {} as Record<string, boolean>,
|
||||
hoverProject: undefined as string | undefined,
|
||||
scrollSessionKey: undefined as string | undefined,
|
||||
@@ -1142,7 +1141,7 @@ export default function Layout(props: ParentProps) {
|
||||
},
|
||||
]
|
||||
|
||||
if (!USE_NEW_DESIGN)
|
||||
if (!newDesign())
|
||||
Array.from({ length: 9 }, (_, i) => {
|
||||
const index = i
|
||||
const number = index + 1
|
||||
@@ -1821,7 +1820,7 @@ export default function Layout(props: ParentProps) {
|
||||
createEffect(() => {
|
||||
document.documentElement.style.setProperty(
|
||||
"--dialog-left-margin",
|
||||
USE_NEW_DESIGN ? "0px" : `${layout.sidebar.opened() ? layout.sidebar.width() : 48}px`,
|
||||
newDesign() ? "0px" : `${layout.sidebar.opened() ? layout.sidebar.width() : 48}px`,
|
||||
)
|
||||
})
|
||||
|
||||
@@ -2363,179 +2362,180 @@ export default function Layout(props: ParentProps) {
|
||||
/>
|
||||
)
|
||||
|
||||
if (USE_NEW_DESIGN) {
|
||||
return (
|
||||
<div class="relative bg-v2-background-bg-deep flex-1 min-h-0 min-w-0 flex flex-col select-none [&_input]:select-text [&_textarea]:select-text [&_[contenteditable]]:select-text">
|
||||
return (
|
||||
<Show
|
||||
when={!newDesign()}
|
||||
fallback={
|
||||
<div class="relative bg-v2-background-bg-deep flex-1 min-h-0 min-w-0 flex flex-col select-none [&_input]:select-text [&_textarea]:select-text [&_[contenteditable]]:select-text">
|
||||
{autoselecting() ?? ""}
|
||||
<Titlebar update={titlebarUpdate} />
|
||||
<main
|
||||
class="flex-1 min-h-0 min-w-0 overflow-x-hidden flex flex-col items-start contain-strict bg-v2-background-bg-base"
|
||||
classList={{
|
||||
"m-2 mt-0 rounded-[10px] shadow-[var(--v2-elevation-raised)] overflow-hidden": !!params.id || !params.dir,
|
||||
}}
|
||||
>
|
||||
<Show when={!autoselecting.loading} fallback={<div class="size-full" />}>
|
||||
{props.children}
|
||||
</Show>
|
||||
</main>
|
||||
{import.meta.env.DEV && <DebugBar />}
|
||||
<Toast.Region />
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<div class="relative bg-background-base flex-1 min-h-0 min-w-0 flex flex-col select-none [&_input]:select-text [&_textarea]:select-text [&_[contenteditable]]:select-text">
|
||||
{autoselecting() ?? ""}
|
||||
<Titlebar update={titlebarUpdate} />
|
||||
<main
|
||||
class="flex-1 min-h-0 min-w-0 overflow-x-hidden flex flex-col items-start contain-strict bg-v2-background-bg-base"
|
||||
classList={{
|
||||
"m-2 mt-0 rounded-[10px] shadow-[var(--v2-elevation-raised)] overflow-hidden": !!params.id || !params.dir,
|
||||
}}
|
||||
>
|
||||
<Show when={!autoselecting.loading} fallback={<div class="size-full" />}>
|
||||
{props.children}
|
||||
</Show>
|
||||
</main>
|
||||
{import.meta.env.DEV && <DebugBar />}
|
||||
<Toast.Region />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div class="relative bg-background-base flex-1 min-h-0 min-w-0 flex flex-col select-none [&_input]:select-text [&_textarea]:select-text [&_[contenteditable]]:select-text">
|
||||
{autoselecting() ?? ""}
|
||||
<Titlebar update={titlebarUpdate} />
|
||||
<Show when={updateVersion() !== undefined}>
|
||||
<UpdateAvailableToast version={updateVersion() ?? ""} install={installUpdate} language={language} />
|
||||
</Show>
|
||||
<div class="flex-1 min-h-0 min-w-0 flex">
|
||||
<div class="flex-1 min-h-0 relative">
|
||||
<div class="size-full relative overflow-x-hidden">
|
||||
<nav
|
||||
aria-label={language.t("sidebar.nav.projectsAndSessions")}
|
||||
data-component="sidebar-nav-desktop"
|
||||
classList={{
|
||||
"hidden xl:block": true,
|
||||
"absolute inset-y-0 left-0": true,
|
||||
"z-10": true,
|
||||
}}
|
||||
style={{ width: `${side()}px` }}
|
||||
ref={(el) => {
|
||||
setState("nav", el)
|
||||
}}
|
||||
onMouseEnter={() => {
|
||||
disarm()
|
||||
}}
|
||||
onMouseLeave={() => {
|
||||
aim.reset()
|
||||
if (!sidebarHovering()) return
|
||||
|
||||
arm()
|
||||
}}
|
||||
>
|
||||
<div class="@container w-full h-full contain-strict">{sidebarContent()}</div>
|
||||
</nav>
|
||||
|
||||
<Show when={layout.sidebar.opened()}>
|
||||
<div
|
||||
class="hidden xl:block absolute inset-y-0 z-30 w-0 overflow-visible"
|
||||
style={{ left: `${side()}px` }}
|
||||
onPointerDown={() => setState("sizing", true)}
|
||||
>
|
||||
<ResizeHandle
|
||||
direction="horizontal"
|
||||
size={layout.sidebar.width()}
|
||||
min={244}
|
||||
max={typeof window === "undefined" ? 1000 : window.innerWidth * 0.3 + 64}
|
||||
onResize={(w) => {
|
||||
setState("sizing", true)
|
||||
if (sizet !== undefined) clearTimeout(sizet)
|
||||
sizet = window.setTimeout(() => setState("sizing", false), 120)
|
||||
layout.sidebar.resize(w)
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</Show>
|
||||
|
||||
<div
|
||||
class="hidden xl:block pointer-events-none absolute top-0 right-0 z-0 border-t border-border-weaker-base"
|
||||
style={{ left: "calc(4rem + 12px)" }}
|
||||
/>
|
||||
|
||||
<div class="xl:hidden">
|
||||
<div
|
||||
classList={{
|
||||
"fixed inset-x-0 top-10 bottom-0 z-40 transition-opacity duration-200": true,
|
||||
"opacity-100 pointer-events-auto": layout.mobileSidebar.opened(),
|
||||
"opacity-0 pointer-events-none": !layout.mobileSidebar.opened(),
|
||||
}}
|
||||
onClick={(e) => {
|
||||
if (e.target === e.currentTarget) layout.mobileSidebar.hide()
|
||||
}}
|
||||
/>
|
||||
<Show when={updateVersion() !== undefined}>
|
||||
<UpdateAvailableToast version={updateVersion() ?? ""} install={installUpdate} language={language} />
|
||||
</Show>
|
||||
<div class="flex-1 min-h-0 min-w-0 flex">
|
||||
<div class="flex-1 min-h-0 relative">
|
||||
<div class="size-full relative overflow-x-hidden">
|
||||
<nav
|
||||
aria-label={language.t("sidebar.nav.projectsAndSessions")}
|
||||
data-component="sidebar-nav-mobile"
|
||||
data-component="sidebar-nav-desktop"
|
||||
classList={{
|
||||
"@container fixed top-10 bottom-0 left-0 z-50 w-full max-w-[400px] overflow-hidden border-r border-border-weaker-base bg-background-base transition-transform duration-200 ease-out": true,
|
||||
"translate-x-0": layout.mobileSidebar.opened(),
|
||||
"-translate-x-full": !layout.mobileSidebar.opened(),
|
||||
"hidden xl:block": true,
|
||||
"absolute inset-y-0 left-0": true,
|
||||
"z-10": true,
|
||||
}}
|
||||
style={{ width: `${side()}px` }}
|
||||
ref={(el) => {
|
||||
setState("nav", el)
|
||||
}}
|
||||
onMouseEnter={() => {
|
||||
disarm()
|
||||
}}
|
||||
onMouseLeave={() => {
|
||||
aim.reset()
|
||||
if (!sidebarHovering()) return
|
||||
|
||||
arm()
|
||||
}}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
{sidebarContent(true)}
|
||||
<div class="@container w-full h-full contain-strict">{sidebarContent()}</div>
|
||||
</nav>
|
||||
</div>
|
||||
|
||||
<div
|
||||
classList={{
|
||||
"absolute inset-0": true,
|
||||
"xl:inset-y-0 xl:right-0 xl:left-[var(--main-left)]": true,
|
||||
"z-20": true,
|
||||
"transition-[left] duration-200 ease-[cubic-bezier(0.22,1,0.36,1)] will-change-[left] motion-reduce:transition-none":
|
||||
!state.sizing,
|
||||
}}
|
||||
style={{
|
||||
"--main-left": layout.sidebar.opened() ? `${side()}px` : "4rem",
|
||||
}}
|
||||
>
|
||||
<main
|
||||
<Show when={layout.sidebar.opened()}>
|
||||
<div
|
||||
class="hidden xl:block absolute inset-y-0 z-30 w-0 overflow-visible"
|
||||
style={{ left: `${side()}px` }}
|
||||
onPointerDown={() => setState("sizing", true)}
|
||||
>
|
||||
<ResizeHandle
|
||||
direction="horizontal"
|
||||
size={layout.sidebar.width()}
|
||||
min={244}
|
||||
max={typeof window === "undefined" ? 1000 : window.innerWidth * 0.3 + 64}
|
||||
onResize={(w) => {
|
||||
setState("sizing", true)
|
||||
if (sizet !== undefined) clearTimeout(sizet)
|
||||
sizet = window.setTimeout(() => setState("sizing", false), 120)
|
||||
layout.sidebar.resize(w)
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</Show>
|
||||
|
||||
<div
|
||||
class="hidden xl:block pointer-events-none absolute top-0 right-0 z-0 border-t border-border-weaker-base"
|
||||
style={{ left: "calc(4rem + 12px)" }}
|
||||
/>
|
||||
|
||||
<div class="xl:hidden">
|
||||
<div
|
||||
classList={{
|
||||
"fixed inset-x-0 top-10 bottom-0 z-40 transition-opacity duration-200": true,
|
||||
"opacity-100 pointer-events-auto": layout.mobileSidebar.opened(),
|
||||
"opacity-0 pointer-events-none": !layout.mobileSidebar.opened(),
|
||||
}}
|
||||
onClick={(e) => {
|
||||
if (e.target === e.currentTarget) layout.mobileSidebar.hide()
|
||||
}}
|
||||
/>
|
||||
<nav
|
||||
aria-label={language.t("sidebar.nav.projectsAndSessions")}
|
||||
data-component="sidebar-nav-mobile"
|
||||
classList={{
|
||||
"@container fixed top-10 bottom-0 left-0 z-50 w-full max-w-[400px] overflow-hidden border-r border-border-weaker-base bg-background-base transition-transform duration-200 ease-out": true,
|
||||
"translate-x-0": layout.mobileSidebar.opened(),
|
||||
"-translate-x-full": !layout.mobileSidebar.opened(),
|
||||
}}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
{sidebarContent(true)}
|
||||
</nav>
|
||||
</div>
|
||||
|
||||
<div
|
||||
classList={{
|
||||
"size-full overflow-x-hidden flex flex-col items-start contain-strict border-t border-border-weak-base bg-background-base xl:border-l xl:rounded-tl-[12px]": true,
|
||||
"absolute inset-0": true,
|
||||
"xl:inset-y-0 xl:right-0 xl:left-[var(--main-left)]": true,
|
||||
"z-20": true,
|
||||
"transition-[left] duration-200 ease-[cubic-bezier(0.22,1,0.36,1)] will-change-[left] motion-reduce:transition-none":
|
||||
!state.sizing,
|
||||
}}
|
||||
style={{
|
||||
"--main-left": layout.sidebar.opened() ? `${side()}px` : "4rem",
|
||||
}}
|
||||
>
|
||||
<Show when={!autoselecting.loading} fallback={<div class="size-full" />}>
|
||||
{props.children}
|
||||
<main
|
||||
classList={{
|
||||
"size-full overflow-x-hidden flex flex-col items-start contain-strict border-t border-border-weak-base bg-background-base xl:border-l xl:rounded-tl-[12px]": true,
|
||||
}}
|
||||
>
|
||||
<Show when={!autoselecting.loading} fallback={<div class="size-full" />}>
|
||||
{props.children}
|
||||
</Show>
|
||||
</main>
|
||||
</div>
|
||||
|
||||
<div
|
||||
classList={{
|
||||
"hidden xl:flex absolute inset-y-0 left-16 z-30": true,
|
||||
"opacity-100 translate-x-0 pointer-events-auto": state.peeked && !layout.sidebar.opened(),
|
||||
"opacity-0 -translate-x-2 pointer-events-none": !state.peeked || layout.sidebar.opened(),
|
||||
"transition-[opacity,transform] motion-reduce:transition-none": true,
|
||||
"duration-180 ease-out": state.peeked && !layout.sidebar.opened(),
|
||||
"duration-120 ease-in": !state.peeked || layout.sidebar.opened(),
|
||||
}}
|
||||
onMouseMove={disarm}
|
||||
onMouseEnter={() => {
|
||||
disarm()
|
||||
aim.reset()
|
||||
}}
|
||||
onPointerDown={disarm}
|
||||
onMouseLeave={() => {
|
||||
arm()
|
||||
}}
|
||||
>
|
||||
<Show when={peekProject()}>
|
||||
<SidebarPanel project={peekProject} merged={false} />
|
||||
</Show>
|
||||
</main>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
classList={{
|
||||
"hidden xl:flex absolute inset-y-0 left-16 z-30": true,
|
||||
"opacity-100 translate-x-0 pointer-events-auto": state.peeked && !layout.sidebar.opened(),
|
||||
"opacity-0 -translate-x-2 pointer-events-none": !state.peeked || layout.sidebar.opened(),
|
||||
"transition-[opacity,transform] motion-reduce:transition-none": true,
|
||||
"duration-180 ease-out": state.peeked && !layout.sidebar.opened(),
|
||||
"duration-120 ease-in": !state.peeked || layout.sidebar.opened(),
|
||||
}}
|
||||
onMouseMove={disarm}
|
||||
onMouseEnter={() => {
|
||||
disarm()
|
||||
aim.reset()
|
||||
}}
|
||||
onPointerDown={disarm}
|
||||
onMouseLeave={() => {
|
||||
arm()
|
||||
}}
|
||||
>
|
||||
<Show when={peekProject()}>
|
||||
<SidebarPanel project={peekProject} merged={false} />
|
||||
</Show>
|
||||
</div>
|
||||
|
||||
<div
|
||||
classList={{
|
||||
"hidden xl:block pointer-events-none absolute inset-y-0 right-0 z-25 overflow-hidden": true,
|
||||
"opacity-100 translate-x-0": state.peeked && !layout.sidebar.opened(),
|
||||
"opacity-0 -translate-x-2": !state.peeked || layout.sidebar.opened(),
|
||||
"transition-[opacity,transform] motion-reduce:transition-none": true,
|
||||
"duration-180 ease-out": state.peeked && !layout.sidebar.opened(),
|
||||
"duration-120 ease-in": !state.peeked || layout.sidebar.opened(),
|
||||
}}
|
||||
style={{ left: `calc(4rem + ${panel()}px)` }}
|
||||
>
|
||||
<div class="h-full w-px" style={{ "box-shadow": "var(--shadow-sidebar-overlay)" }} />
|
||||
<div
|
||||
classList={{
|
||||
"hidden xl:block pointer-events-none absolute inset-y-0 right-0 z-25 overflow-hidden": true,
|
||||
"opacity-100 translate-x-0": state.peeked && !layout.sidebar.opened(),
|
||||
"opacity-0 -translate-x-2": !state.peeked || layout.sidebar.opened(),
|
||||
"transition-[opacity,transform] motion-reduce:transition-none": true,
|
||||
"duration-180 ease-out": state.peeked && !layout.sidebar.opened(),
|
||||
"duration-120 ease-in": !state.peeked || layout.sidebar.opened(),
|
||||
}}
|
||||
style={{ left: `calc(4rem + ${panel()}px)` }}
|
||||
>
|
||||
<div class="h-full w-px" style={{ "box-shadow": "var(--shadow-sidebar-overlay)" }} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{import.meta.env.DEV && <DebugBar />}
|
||||
</div>
|
||||
{import.meta.env.DEV && <DebugBar />}
|
||||
<Toast.Region />
|
||||
</div>
|
||||
<Toast.Region />
|
||||
</div>
|
||||
</Show>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -75,7 +75,6 @@ const emptyFollowups: FollowupItem[] = []
|
||||
|
||||
type ChangeMode = "git" | "branch" | "turn"
|
||||
type VcsMode = "git" | "branch"
|
||||
const USE_NEW_SESSION_DESIGN = import.meta.env.VITE_OPENCODE_CHANNEL !== "prod"
|
||||
|
||||
type SessionHistoryWindowInput = {
|
||||
sessionID: () => string | undefined
|
||||
@@ -198,6 +197,7 @@ export default function Page() {
|
||||
const [searchParams, setSearchParams] = useSearchParams<{ prompt?: string }>()
|
||||
const location = useLocation()
|
||||
const { params, sessionKey, tabs, view } = useSessionLayout()
|
||||
const newSessionDesign = createMemo(() => settings.general.newLayoutDesigns())
|
||||
|
||||
createEffect(() => {
|
||||
if (!prompt.ready()) return
|
||||
@@ -265,7 +265,7 @@ export default function Page() {
|
||||
const isDesktop = createMediaQuery("(min-width: 768px)")
|
||||
const size = createSizing()
|
||||
const isV2NewSessionPage = () =>
|
||||
shouldUseV2NewSessionPage({ channel: import.meta.env.VITE_OPENCODE_CHANNEL, sessionID: params.id })
|
||||
shouldUseV2NewSessionPage({ newLayoutDesigns: newSessionDesign(), sessionID: params.id })
|
||||
const desktopReviewOpen = createMemo(() => isDesktop() && view().reviewPanel.opened() && !isV2NewSessionPage())
|
||||
const desktopFileTreeOpen = createMemo(() => isDesktop() && layout.fileTree.opened() && !isV2NewSessionPage())
|
||||
const desktopSidePanelOpen = createMemo(() => desktopReviewOpen() || desktopFileTreeOpen())
|
||||
@@ -1798,14 +1798,14 @@ export default function Page() {
|
||||
</Show>
|
||||
</Match>
|
||||
<Match when={true}>
|
||||
<Show when={USE_NEW_SESSION_DESIGN} fallback={<NewSessionView worktree={newSessionWorktree()} />}>
|
||||
<Show when={newSessionDesign()} fallback={<NewSessionView worktree={newSessionWorktree()} />}>
|
||||
<NewSessionDesignView>{composerRegion("inline")}</NewSessionDesignView>
|
||||
</Show>
|
||||
</Match>
|
||||
</Switch>
|
||||
</div>
|
||||
|
||||
<Show when={params.id || !USE_NEW_SESSION_DESIGN}>{composerRegion("dock")}</Show>
|
||||
<Show when={params.id || !newSessionDesign()}>{composerRegion("dock")}</Show>
|
||||
|
||||
<Show when={desktopReviewOpen()}>
|
||||
<div onPointerDown={() => size.start()}>
|
||||
|
||||
@@ -2,13 +2,13 @@ import { describe, expect, test } from "bun:test"
|
||||
import { shouldUseV2NewSessionPage } from "./new-session-layout"
|
||||
|
||||
describe("shouldUseV2NewSessionPage", () => {
|
||||
test("keeps prod session pages on the legacy layout", () => {
|
||||
expect(shouldUseV2NewSessionPage({ channel: "prod", sessionID: "ses_123" })).toBe(false)
|
||||
expect(shouldUseV2NewSessionPage({ channel: "prod" })).toBe(false)
|
||||
test("keeps disabled pages on the legacy layout", () => {
|
||||
expect(shouldUseV2NewSessionPage({ newLayoutDesigns: false, sessionID: "ses_123" })).toBe(false)
|
||||
expect(shouldUseV2NewSessionPage({ newLayoutDesigns: false })).toBe(false)
|
||||
})
|
||||
|
||||
test("uses the v2 layout only for non-prod new-session pages", () => {
|
||||
expect(shouldUseV2NewSessionPage({ channel: "dev" })).toBe(true)
|
||||
expect(shouldUseV2NewSessionPage({ channel: "dev", sessionID: "ses_123" })).toBe(false)
|
||||
test("uses the v2 layout only for enabled new-session pages", () => {
|
||||
expect(shouldUseV2NewSessionPage({ newLayoutDesigns: true })).toBe(true)
|
||||
expect(shouldUseV2NewSessionPage({ newLayoutDesigns: true, sessionID: "ses_123" })).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
export function shouldUseV2NewSessionPage(input: { channel?: "dev" | "beta" | "prod"; sessionID?: string }) {
|
||||
return input.channel !== "prod" && !input.sessionID
|
||||
export function shouldUseV2NewSessionPage(input: { newLayoutDesigns: boolean; sessionID?: string }) {
|
||||
return input.newLayoutDesigns && !input.sessionID
|
||||
}
|
||||
|
||||
@@ -28,8 +28,6 @@ import { createOpenSessionFileTab, createSessionTabs, getTabReorderIndex, type S
|
||||
import { setSessionHandoff } from "@/pages/session/handoff"
|
||||
import { useSessionLayout } from "@/pages/session/session-layout"
|
||||
|
||||
const USE_DESKTOP_V2 = import.meta.env.VITE_OPENCODE_CHANNEL !== "prod"
|
||||
|
||||
type RenderDiff = (SnapshotFileDiff & { file: string }) | VcsFileDiff
|
||||
|
||||
function renderDiff(value: SnapshotFileDiff | VcsFileDiff): value is RenderDiff {
|
||||
@@ -60,7 +58,7 @@ export function SessionSidePanel(props: {
|
||||
const { sessionKey, tabs, view, params } = useSessionLayout()
|
||||
|
||||
const isDesktop = createMediaQuery("(min-width: 768px)")
|
||||
const desktopV2 = () => platform.platform === "desktop" && USE_DESKTOP_V2
|
||||
const desktopV2 = () => platform.platform === "desktop" && settings.general.newLayoutDesigns()
|
||||
const shown = createMemo(() => (desktopV2() ? settings.general.showFileTree() : true))
|
||||
|
||||
const reviewOpen = createMemo(() => isDesktop() && view().reviewPanel.opened())
|
||||
@@ -205,7 +203,7 @@ export function SessionSidePanel(props: {
|
||||
})
|
||||
|
||||
return (
|
||||
<Show when={isDesktop() && !(import.meta.env.VITE_OPENCODE_CHANNEL !== "prod" && !params.id)}>
|
||||
<Show when={isDesktop() && !(settings.general.newLayoutDesigns() && !params.id)}>
|
||||
<aside
|
||||
id="review-panel"
|
||||
aria-label={language.t("session.panel.reviewAndFiles")}
|
||||
|
||||
@@ -20,8 +20,6 @@ import { extractPromptFromParts } from "@/utils/prompt"
|
||||
import { UserMessage } from "@opencode-ai/sdk/v2"
|
||||
import { useSessionLayout } from "@/pages/session/session-layout"
|
||||
|
||||
const USE_DESKTOP_V2 = import.meta.env.VITE_OPENCODE_CHANNEL !== "prod"
|
||||
|
||||
export type SessionCommandContext = {
|
||||
navigateMessageByOffset: (offset: number) => void
|
||||
setActiveMessage: (message: UserMessage | undefined) => void
|
||||
@@ -72,7 +70,7 @@ export const useSessionCommands = (actions: SessionCommandContext) => {
|
||||
})
|
||||
const activeFileTab = tabState.activeFileTab
|
||||
const closableTab = tabState.closableTab
|
||||
const desktopV2 = () => platform.platform === "desktop" && USE_DESKTOP_V2
|
||||
const desktopV2 = () => platform.platform === "desktop" && settings.general.newLayoutDesigns()
|
||||
const shown = () => (desktopV2() ? settings.general.showFileTree() : true)
|
||||
|
||||
const messages = () => {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@opencode-ai/console-app",
|
||||
"version": "1.15.10",
|
||||
"version": "1.15.11",
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
"scripts": {
|
||||
|
||||
@@ -64,11 +64,12 @@ function LimitsGraph(props: { href: string }) {
|
||||
{ id: "glm-5.1", name: "GLM-5.1", req: 880, d: "100ms" },
|
||||
{ id: "qwen3.7-max", name: "Qwen3.7 Max", req: 950, d: "280ms" },
|
||||
{ id: "kimi-k2.6", name: "Kimi K2.6", req: 1150, d: "150ms" },
|
||||
{ id: "mimo-v2.5-pro", name: "MiMo-V2.5-Pro", req: 1290, d: "150ms" },
|
||||
{ id: "mimo-v2.5-pro", name: "MiMo-V2.5-Pro", req: 3250, d: "150ms" },
|
||||
{ id: "qwen3.6-plus", name: "Qwen3.6 Plus", req: 3300, d: "280ms" },
|
||||
{ id: "minimax-m2.7", name: "MiniMax M2.7", req: 3400, d: "300ms" },
|
||||
{ id: "deepseek-v4-pro", name: "DeepSeek V4 Pro", req: 3450, d: "200ms" },
|
||||
{ id: "qwen3.5-plus", name: "Qwen3.5 Plus", req: 10200, d: "360ms" },
|
||||
{ id: "mimo-v2.5", name: "MiMo-V2.5", req: 30100, d: "340ms" },
|
||||
{ id: "deepseek-v4-flash", name: "DeepSeek V4 Flash", req: 31650, d: "340ms" },
|
||||
]
|
||||
|
||||
|
||||
@@ -483,27 +483,29 @@ export async function handler(
|
||||
modelTpsLimits: Record<string, boolean> | undefined,
|
||||
) {
|
||||
const modelProvider = (() => {
|
||||
const allProviders = modelInfo.providers.filter((provider) => !provider.disabled)
|
||||
|
||||
// Byok is top priority b/c if user set their own API key, we should use it
|
||||
// instead of using the sticky provider for the same session
|
||||
if (authInfo?.provider?.credentials) {
|
||||
return modelInfo.providers.find((provider) => provider.id === modelInfo.byokProvider)
|
||||
return allProviders.find((provider) => provider.id === modelInfo.byokProvider)
|
||||
}
|
||||
|
||||
// Always use the same provider for the same session
|
||||
if (stickyProvider) {
|
||||
const provider = modelInfo.providers.find((provider) => provider.id === stickyProvider)
|
||||
const provider = allProviders.find((provider) => provider.id === stickyProvider)
|
||||
if (provider) return provider
|
||||
}
|
||||
|
||||
if (trialProviders) {
|
||||
const trialProvider = trialProviders[Math.floor(Math.random() * trialProviders.length)]
|
||||
const provider = modelInfo.providers.find((provider) => provider.id === trialProvider)
|
||||
const provider = allProviders.find((provider) => provider.id === trialProvider)
|
||||
if (provider) return provider
|
||||
}
|
||||
|
||||
if (retry.retryCount !== MAX_FAILOVER_RETRIES) {
|
||||
let topPriority = Infinity
|
||||
const providers = modelInfo.providers
|
||||
const providers = allProviders
|
||||
.filter((provider) => !provider.disabled)
|
||||
.filter((provider) => provider.weight !== 0)
|
||||
.filter((provider) => !retry.excludeProviders.includes(provider.id))
|
||||
@@ -536,7 +538,7 @@ export async function handler(
|
||||
}
|
||||
|
||||
// fallback provider
|
||||
return modelInfo.providers.find((provider) => provider.id === modelInfo.fallbackProvider)
|
||||
return allProviders.find((provider) => provider.id === modelInfo.fallbackProvider)
|
||||
})()
|
||||
|
||||
if (!modelProvider) throw new ModelError(t("zen.api.error.noProviderAvailable"))
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
import { Database, eq, and, sql, inArray } from "@opencode-ai/console-core/drizzle/index.js"
|
||||
import { IpRateLimitTable } from "@opencode-ai/console-core/schema/ip.sql.js"
|
||||
import { FreeUsageLimitError } from "./error"
|
||||
import { logger } from "./logger"
|
||||
import { buildRateLimitKey, getRedis } from "./redis"
|
||||
@@ -22,7 +20,6 @@ export function createRateLimiter(modelId: string, rateLimit: number | undefined
|
||||
|
||||
const ip = !rawIp.length ? "unknown" : rawIp
|
||||
const now = Date.now()
|
||||
const lifetimeInterval = ""
|
||||
const dailyInterval = rateLimit ? `${buildYYYYMMDD(now)}${modelId.substring(0, 2)}` : buildYYYYMMDD(now)
|
||||
const retryAfter = getRetryAfterDay(now)
|
||||
const redis = getRedis()
|
||||
@@ -32,33 +29,12 @@ export function createRateLimiter(modelId: string, rateLimit: number | undefined
|
||||
|
||||
return {
|
||||
check: async () => {
|
||||
const [counts, rows] = await Promise.all([
|
||||
redis.mget<(string | number | null)[]>(isDefaultModel ? [lifetimeKey, dailyKey] : [dailyKey]).catch(() => []),
|
||||
Database.use((tx) =>
|
||||
tx
|
||||
.select({ interval: IpRateLimitTable.interval, count: IpRateLimitTable.count })
|
||||
.from(IpRateLimitTable)
|
||||
.where(
|
||||
and(
|
||||
eq(IpRateLimitTable.ip, ip),
|
||||
isDefaultModel
|
||||
? inArray(IpRateLimitTable.interval, [lifetimeInterval, dailyInterval])
|
||||
: inArray(IpRateLimitTable.interval, [dailyInterval]),
|
||||
),
|
||||
),
|
||||
),
|
||||
])
|
||||
const redisLifetimeCount = isDefaultModel ? Number(counts[0] ?? 0) : 0
|
||||
const redisDailyCount = Number(counts[isDefaultModel ? 1 : 0] ?? 0)
|
||||
const databaseLifetimeCount = rows.find((r) => r.interval === lifetimeInterval)?.count ?? 0
|
||||
const databaseDailyCount = rows.find((r) => r.interval === dailyInterval)?.count ?? 0
|
||||
const lifetimeCount = Math.max(redisLifetimeCount, databaseLifetimeCount)
|
||||
const dailyCount = Math.max(redisDailyCount, databaseDailyCount)
|
||||
const counts = await redis.mget<(string | number | null)[]>(isDefaultModel ? [lifetimeKey, dailyKey] : [dailyKey])
|
||||
const lifetimeCount = isDefaultModel ? Number(counts[0] ?? 0) : 0
|
||||
const dailyCount = Number(counts[isDefaultModel ? 1 : 0] ?? 0)
|
||||
logger.debug(`rate limit lifetime: ${lifetimeCount}, daily: ${dailyCount}`)
|
||||
|
||||
isNew = isDefaultModel && lifetimeCount < dailyLimit * 7
|
||||
if (isDefaultModel && databaseLifetimeCount > redisLifetimeCount)
|
||||
await redis.set(lifetimeKey, databaseLifetimeCount).catch(() => {})
|
||||
|
||||
if ((isNew && dailyCount >= dailyLimit * 2) || (!isNew && dailyCount >= dailyLimit))
|
||||
throw new FreeUsageLimitError(dict["zen.api.error.rateLimitExceeded"], retryAfter)
|
||||
@@ -68,18 +44,7 @@ export function createRateLimiter(modelId: string, rateLimit: number | undefined
|
||||
pipeline.incr(dailyKey)
|
||||
pipeline.expire(dailyKey, retryAfter)
|
||||
if (isNew) pipeline.incr(lifetimeKey)
|
||||
await Promise.all([
|
||||
pipeline.exec().catch(() => {}),
|
||||
Database.use((tx) =>
|
||||
tx
|
||||
.insert(IpRateLimitTable)
|
||||
.values([
|
||||
{ ip, interval: dailyInterval, count: 1 },
|
||||
...(isNew ? [{ ip, interval: lifetimeInterval, count: 1 }] : []),
|
||||
])
|
||||
.onDuplicateKeyUpdate({ set: { count: sql`${IpRateLimitTable.count} + 1` } }),
|
||||
),
|
||||
])
|
||||
await pipeline.exec()
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { Database, eq, and, sql } from "@opencode-ai/console-core/drizzle/index.js"
|
||||
import { KeyRateLimitTable } from "@opencode-ai/console-core/schema/ip.sql.js"
|
||||
import { RateLimitError } from "./error"
|
||||
import { buildRateLimitKey, getRedis } from "./redis"
|
||||
import { i18n } from "~/i18n"
|
||||
import { localeFromRequest } from "~/lib/language"
|
||||
|
||||
@@ -19,26 +18,20 @@ export function createRateLimiter(
|
||||
.replace(/[^0-9]/g, "")
|
||||
.substring(0, 12)
|
||||
const interval = `${modelId.substring(0, 27)}-${yyyyMMddHHmm}`
|
||||
const redis = getRedis()
|
||||
const key = buildRateLimitKey("key", zenApiKey, interval)
|
||||
|
||||
return {
|
||||
check: async () => {
|
||||
const rows = await Database.use((tx) =>
|
||||
tx
|
||||
.select({ interval: KeyRateLimitTable.interval, count: KeyRateLimitTable.count })
|
||||
.from(KeyRateLimitTable)
|
||||
.where(and(eq(KeyRateLimitTable.key, zenApiKey), eq(KeyRateLimitTable.interval, interval))),
|
||||
).then((rows) => rows[0])
|
||||
const count = rows?.count ?? 0
|
||||
const count = Number((await redis.mget<(string | number | null)[]>([key]))[0] ?? 0)
|
||||
|
||||
if (count >= LIMIT) throw new RateLimitError(dict["zen.api.error.rateLimitExceeded"], 60)
|
||||
},
|
||||
track: async () => {
|
||||
await Database.use((tx) =>
|
||||
tx
|
||||
.insert(KeyRateLimitTable)
|
||||
.values({ key: zenApiKey, interval, count: 1 })
|
||||
.onDuplicateKeyUpdate({ set: { count: sql`${KeyRateLimitTable.count} + 1` } }),
|
||||
)
|
||||
const pipeline = redis.pipeline()
|
||||
pipeline.incr(key)
|
||||
pipeline.expire(key, 60)
|
||||
await pipeline.exec()
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/package.json",
|
||||
"name": "@opencode-ai/console-core",
|
||||
"version": "1.15.10",
|
||||
"version": "1.15.11",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@opencode-ai/console-function",
|
||||
"version": "1.15.10",
|
||||
"version": "1.15.11",
|
||||
"$schema": "https://json.schemastore.org/package.json",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@opencode-ai/console-mail",
|
||||
"version": "1.15.10",
|
||||
"version": "1.15.11",
|
||||
"dependencies": {
|
||||
"@jsx-email/all": "2.2.3",
|
||||
"@jsx-email/cli": "1.4.3",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@opencode-ai/console-support",
|
||||
"version": "0.0.0",
|
||||
"version": "1.15.11",
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
"scripts": {
|
||||
|
||||
@@ -2,7 +2,7 @@ import { defineConfig } from "drizzle-kit"
|
||||
|
||||
export default defineConfig({
|
||||
dialect: "sqlite",
|
||||
schema: "./src/**/*.sql.ts",
|
||||
schema: ["./src/**/*.sql.ts", "./src/**/sql.ts"],
|
||||
out: "./migration",
|
||||
dbCredentials: {
|
||||
url: "/home/thdxr/.local/share/opencode/opencode.db",
|
||||
@@ -1,11 +1,13 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/package.json",
|
||||
"version": "1.15.10",
|
||||
"version": "1.15.11",
|
||||
"name": "@opencode-ai/core",
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"db": "bun drizzle-kit",
|
||||
"migration": "bun run script/migration.ts",
|
||||
"test": "bun test",
|
||||
"test:ci": "mkdir -p .artifacts/unit && bun test --timeout 30000 --reporter=junit --reporter-outfile=.artifacts/unit/junit.xml",
|
||||
"typecheck": "tsgo --noEmit"
|
||||
@@ -16,14 +18,21 @@
|
||||
"exports": {
|
||||
"./*": "./src/*.ts"
|
||||
},
|
||||
"imports": {},
|
||||
"imports": {
|
||||
"#sqlite": {
|
||||
"bun": "./src/database/sqlite.bun.ts",
|
||||
"node": "./src/database/sqlite.node.ts",
|
||||
"default": "./src/database/sqlite.bun.ts"
|
||||
}
|
||||
},
|
||||
"devDependencies": {
|
||||
"@tsconfig/bun": "catalog:",
|
||||
"@types/bun": "catalog:",
|
||||
"@types/cross-spawn": "catalog:",
|
||||
"@types/npm-package-arg": "6.1.4",
|
||||
"@types/npmcli__arborist": "6.3.3",
|
||||
"@types/semver": "catalog:"
|
||||
"@types/semver": "catalog:",
|
||||
"drizzle-kit": "catalog:"
|
||||
},
|
||||
"dependencies": {
|
||||
"@ai-sdk/alibaba": "1.0.17",
|
||||
@@ -49,8 +58,11 @@
|
||||
"@aws-sdk/credential-providers": "3.993.0",
|
||||
"@effect/opentelemetry": "catalog:",
|
||||
"@effect/platform-node": "catalog:",
|
||||
"@effect/sql-sqlite-bun": "catalog:",
|
||||
"@npmcli/arborist": "9.4.0",
|
||||
"@npmcli/config": "10.8.1",
|
||||
"@opencode-ai/effect-drizzle-sqlite": "workspace:*",
|
||||
"@opencode-ai/effect-sqlite-node": "workspace:*",
|
||||
"@opentelemetry/api": "1.9.0",
|
||||
"@opentelemetry/context-async-hooks": "2.6.1",
|
||||
"@opentelemetry/exporter-trace-otlp-http": "0.214.0",
|
||||
@@ -58,6 +70,7 @@
|
||||
"@openrouter/ai-sdk-provider": "2.8.1",
|
||||
"ai-gateway-provider": "3.1.2",
|
||||
"cross-spawn": "catalog:",
|
||||
"drizzle-orm": "catalog:",
|
||||
"effect": "catalog:",
|
||||
"gitlab-ai-provider": "6.7.0",
|
||||
"glob": "13.0.5",
|
||||
|
||||
@@ -0,0 +1,113 @@
|
||||
#!/usr/bin/env bun
|
||||
|
||||
import { $ } from "bun"
|
||||
import fs from "fs/promises"
|
||||
import os from "os"
|
||||
import path from "path"
|
||||
import { pathToFileURL } from "url"
|
||||
|
||||
const root = path.resolve(import.meta.dirname, "../../..")
|
||||
const sqlDir = path.join(root, "packages/core/migration")
|
||||
const tsDir = path.join(root, "packages/core/src/database/migration")
|
||||
const registry = path.join(root, "packages/core/src/database/migration.gen.ts")
|
||||
|
||||
if (Bun.argv.includes("--check")) {
|
||||
await check()
|
||||
process.exit(0)
|
||||
}
|
||||
|
||||
await $`bun drizzle-kit generate`.cwd(path.join(root, "packages/core"))
|
||||
|
||||
const sqlMigrations = (await Array.fromAsync(new Bun.Glob("*/migration.sql").scan({ cwd: sqlDir })))
|
||||
.map((file) => file.split("/")[0])
|
||||
.filter((name) => name !== undefined)
|
||||
.sort()
|
||||
|
||||
for (const name of sqlMigrations) {
|
||||
if (await Bun.file(path.join(tsDir, `${name}.ts`)).exists()) continue
|
||||
await Bun.write(path.join(tsDir, `${name}.ts`), renderMigration(name, await Bun.file(path.join(sqlDir, name, "migration.sql")).text()))
|
||||
}
|
||||
|
||||
await Bun.write(registry, renderRegistry(sqlMigrations))
|
||||
|
||||
async function check() {
|
||||
const temporary = await fs.mkdtemp(path.join(os.tmpdir(), "opencode-core-migration-check-"))
|
||||
const output = path.join(temporary, "migration")
|
||||
try {
|
||||
await fs.cp(sqlDir, output, { recursive: true })
|
||||
const config = path.join(temporary, "drizzle.config.ts")
|
||||
await Bun.write(
|
||||
config,
|
||||
`import config from ${JSON.stringify(pathToFileURL(path.join(root, "packages/core/drizzle.config.ts")).href)}
|
||||
|
||||
export default { ...config, out: ${JSON.stringify(output)} }
|
||||
`,
|
||||
)
|
||||
const before = await snapshot(output)
|
||||
await $`bun drizzle-kit generate --config ${config}`.cwd(path.join(root, "packages/core"))
|
||||
const after = await snapshot(output)
|
||||
if (JSON.stringify(after) !== JSON.stringify(before)) {
|
||||
throw new Error("Core schema has ungenerated database migrations. Run `bun script/migration.ts` from packages/core.")
|
||||
}
|
||||
|
||||
const migrations = before
|
||||
.map((entry) => entry.path.split("/")[0])
|
||||
.filter((name, index, all) => name !== undefined && all.indexOf(name) === index)
|
||||
.sort()
|
||||
for (const name of migrations) {
|
||||
if (await Bun.file(path.join(tsDir, `${name}.ts`)).exists()) continue
|
||||
throw new Error(`Database migration TypeScript wrapper is missing for ${name}. Run \`bun script/migration.ts\` from packages/core.`)
|
||||
}
|
||||
if ((await Bun.file(registry).text()) !== renderRegistry(migrations)) {
|
||||
throw new Error("Database migration registry is stale. Run `bun script/migration.ts` from packages/core.")
|
||||
}
|
||||
} finally {
|
||||
await fs.rm(temporary, { recursive: true, force: true })
|
||||
}
|
||||
}
|
||||
|
||||
async function snapshot(directory: string) {
|
||||
const files = await Array.fromAsync(new Bun.Glob("**/*").scan({ cwd: directory, onlyFiles: true }))
|
||||
return Promise.all(
|
||||
files.sort().map(async (file) => ({ path: file, contents: await Bun.file(path.join(directory, file)).text() })),
|
||||
)
|
||||
}
|
||||
|
||||
function renderMigration(name: string, sql: string) {
|
||||
return `import { Effect } from "effect"
|
||||
import type { DatabaseMigration } from "../migration"
|
||||
|
||||
export default {
|
||||
id: ${JSON.stringify(name)},
|
||||
up(tx) {
|
||||
return Effect.gen(function* () {
|
||||
${sql
|
||||
.split("--> statement-breakpoint")
|
||||
.map((statement) => statement.trim())
|
||||
.filter((statement) => statement.length > 0)
|
||||
.map(renderRun)
|
||||
.join("\n")}
|
||||
})
|
||||
},
|
||||
} satisfies DatabaseMigration.Migration
|
||||
`
|
||||
}
|
||||
|
||||
function renderRun(statement: string) {
|
||||
const lines = statement.replaceAll("\t", " ").split("\n")
|
||||
if (lines.length === 1) return ` yield* tx.run(\`${escapeTemplate(lines[0])}\`)`
|
||||
return ` yield* tx.run(\`\n${lines.map((line) => ` ${escapeTemplate(line)}`).join("\n")}\n \`)`
|
||||
}
|
||||
|
||||
function escapeTemplate(line: string) {
|
||||
return line.replaceAll("\\", "\\\\").replaceAll("`", "\\`").replaceAll("${", "\\${")
|
||||
}
|
||||
|
||||
function renderRegistry(names: string[]) {
|
||||
return `import type { DatabaseMigration } from "./migration"
|
||||
|
||||
export const migrations = (await Promise.all([
|
||||
${names.map((name) => ` import("./migration/${name}"),`).join("\n")}
|
||||
])).map((module) => module.default) satisfies DatabaseMigration.Migration[]
|
||||
`
|
||||
}
|
||||
+89
-307
@@ -1,319 +1,101 @@
|
||||
import path from "path"
|
||||
import { Effect, Layer, Option, Schema, Context, SynchronizedRef } from "effect"
|
||||
import { Identifier } from "./util/identifier"
|
||||
import { NonNegativeInt, withStatics } from "./schema"
|
||||
import { Global } from "./global"
|
||||
import { AppFileSystem } from "./filesystem"
|
||||
import { EventV2 } from "./event"
|
||||
export * as AccountV2 from "./account"
|
||||
|
||||
export const ID = Schema.String.pipe(
|
||||
Schema.brand("AccountV2.ID"),
|
||||
withStatics((schema) => ({ create: () => schema.make("acc_" + Identifier.ascending()) })),
|
||||
)
|
||||
export type ID = typeof ID.Type
|
||||
import { Schema } from "effect"
|
||||
import type * as HttpClientError from "effect/unstable/http/HttpClientError"
|
||||
|
||||
export const ServiceID = Schema.String.pipe(Schema.brand("ServiceID"))
|
||||
export type ServiceID = typeof ServiceID.Type
|
||||
export const ID = Schema.String.pipe(Schema.brand("AccountID"))
|
||||
export type ID = Schema.Schema.Type<typeof ID>
|
||||
|
||||
export class OAuthCredential extends Schema.Class<OAuthCredential>("AccountV2.OAuthCredential")({
|
||||
type: Schema.Literal("oauth"),
|
||||
refresh: Schema.String,
|
||||
access: Schema.String,
|
||||
expires: NonNegativeInt,
|
||||
}) {}
|
||||
export const OrgID = Schema.String.pipe(Schema.brand("OrgID"))
|
||||
export type OrgID = Schema.Schema.Type<typeof OrgID>
|
||||
|
||||
export class ApiKeyCredential extends Schema.Class<ApiKeyCredential>("AccountV2.ApiKeyCredential")({
|
||||
type: Schema.Literal("api"),
|
||||
key: Schema.String,
|
||||
metadata: Schema.optional(Schema.Record(Schema.String, Schema.String)),
|
||||
}) {}
|
||||
export const AccessToken = Schema.String.pipe(Schema.brand("AccessToken"))
|
||||
export type AccessToken = Schema.Schema.Type<typeof AccessToken>
|
||||
|
||||
export const Credential = Schema.Union([OAuthCredential, ApiKeyCredential])
|
||||
.pipe(Schema.toTaggedUnion("type"))
|
||||
.annotate({
|
||||
identifier: "AccountV2.Credential",
|
||||
})
|
||||
export type Credential = Schema.Schema.Type<typeof Credential>
|
||||
export const RefreshToken = Schema.String.pipe(Schema.brand("RefreshToken"))
|
||||
export type RefreshToken = Schema.Schema.Type<typeof RefreshToken>
|
||||
|
||||
export class Info extends Schema.Class<Info>("AccountV2.Info")({
|
||||
export const DeviceCode = Schema.String.pipe(Schema.brand("DeviceCode"))
|
||||
export type DeviceCode = Schema.Schema.Type<typeof DeviceCode>
|
||||
|
||||
export const UserCode = Schema.String.pipe(Schema.brand("UserCode"))
|
||||
export type UserCode = Schema.Schema.Type<typeof UserCode>
|
||||
|
||||
export class Info extends Schema.Class<Info>("Account")({
|
||||
id: ID,
|
||||
serviceID: ServiceID,
|
||||
description: Schema.String,
|
||||
credential: Credential,
|
||||
email: Schema.String,
|
||||
url: Schema.String,
|
||||
active_org_id: Schema.NullOr(OrgID),
|
||||
}) {}
|
||||
|
||||
export class FileWriteError extends Schema.TaggedErrorClass<FileWriteError>()("AccountV2.FileWriteError", {
|
||||
operation: Schema.Union([Schema.Literal("migrate"), Schema.Literal("write")]),
|
||||
export class Org extends Schema.Class<Org>("Org")({
|
||||
id: OrgID,
|
||||
name: Schema.String,
|
||||
}) {}
|
||||
|
||||
export class AccountRepoError extends Schema.TaggedErrorClass<AccountRepoError>()("AccountRepoError", {
|
||||
message: Schema.String,
|
||||
cause: Schema.optional(Schema.Defect),
|
||||
}) {}
|
||||
|
||||
export class AccountServiceError extends Schema.TaggedErrorClass<AccountServiceError>()("AccountServiceError", {
|
||||
message: Schema.String,
|
||||
cause: Schema.optional(Schema.Defect),
|
||||
}) {}
|
||||
|
||||
export class AccountTransportError extends Schema.TaggedErrorClass<AccountTransportError>()("AccountTransportError", {
|
||||
method: Schema.String,
|
||||
url: Schema.String,
|
||||
description: Schema.optional(Schema.String),
|
||||
cause: Schema.optional(Schema.Defect),
|
||||
}) {
|
||||
static fromHttpClientError(error: HttpClientError.TransportError): AccountTransportError {
|
||||
return new AccountTransportError({
|
||||
method: error.request.method,
|
||||
url: error.request.url,
|
||||
description: error.description,
|
||||
cause: error.cause,
|
||||
})
|
||||
}
|
||||
|
||||
override get message(): string {
|
||||
return [
|
||||
`Could not reach ${this.method} ${this.url}.`,
|
||||
`This failed before the server returned an HTTP response.`,
|
||||
this.description,
|
||||
`Check your network, proxy, or VPN configuration and try again.`,
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join("\n")
|
||||
}
|
||||
}
|
||||
|
||||
export type AccountError = AccountRepoError | AccountServiceError | AccountTransportError
|
||||
|
||||
export class Login extends Schema.Class<Login>("Login")({
|
||||
code: DeviceCode,
|
||||
user: UserCode,
|
||||
url: Schema.String,
|
||||
server: Schema.String,
|
||||
expiry: Schema.Duration,
|
||||
interval: Schema.Duration,
|
||||
}) {}
|
||||
|
||||
export class PollSuccess extends Schema.TaggedClass<PollSuccess>()("PollSuccess", {
|
||||
email: Schema.String,
|
||||
}) {}
|
||||
|
||||
export class PollPending extends Schema.TaggedClass<PollPending>()("PollPending", {}) {}
|
||||
|
||||
export class PollSlow extends Schema.TaggedClass<PollSlow>()("PollSlow", {}) {}
|
||||
|
||||
export class PollExpired extends Schema.TaggedClass<PollExpired>()("PollExpired", {}) {}
|
||||
|
||||
export class PollDenied extends Schema.TaggedClass<PollDenied>()("PollDenied", {}) {}
|
||||
|
||||
export class PollError extends Schema.TaggedClass<PollError>()("PollError", {
|
||||
cause: Schema.Defect,
|
||||
}) {}
|
||||
|
||||
export type Error = FileWriteError
|
||||
|
||||
export const Event = {
|
||||
Added: EventV2.define({
|
||||
type: "account.added",
|
||||
schema: {
|
||||
account: Info,
|
||||
},
|
||||
}),
|
||||
Removed: EventV2.define({
|
||||
type: "account.removed",
|
||||
schema: {
|
||||
account: Info,
|
||||
},
|
||||
}),
|
||||
Switched: EventV2.define({
|
||||
type: "account.switched",
|
||||
schema: {
|
||||
serviceID: ServiceID,
|
||||
from: Schema.optional(ID),
|
||||
to: Schema.optional(ID),
|
||||
},
|
||||
}),
|
||||
}
|
||||
|
||||
interface Writable {
|
||||
version: 2
|
||||
accounts: Record<string, Info>
|
||||
active: Record<string, ID>
|
||||
}
|
||||
|
||||
const decodeV1 = Schema.decodeUnknownOption(Schema.Record(Schema.String, Credential))
|
||||
|
||||
function migrate(old: Record<string, unknown>): Writable {
|
||||
const accounts: Record<string, Info> = {}
|
||||
const active: Record<string, ID> = {}
|
||||
for (const [serviceID, value] of Object.entries(old)) {
|
||||
const decoded = Option.getOrElse(decodeV1({ [serviceID]: value }), () => ({}))
|
||||
const parsed = (decoded as Record<string, Credential>)[serviceID]
|
||||
if (!parsed) continue
|
||||
const id = Identifier.ascending()
|
||||
const account = ID.make(id)
|
||||
const brandedServiceID = ServiceID.make(serviceID)
|
||||
accounts[id] = new Info({
|
||||
id: account,
|
||||
serviceID: brandedServiceID,
|
||||
description: "default",
|
||||
credential: parsed,
|
||||
})
|
||||
active[brandedServiceID] = account
|
||||
}
|
||||
return { version: 2, accounts, active }
|
||||
}
|
||||
|
||||
export interface Interface {
|
||||
readonly get: (id: ID) => Effect.Effect<Info | undefined, Error>
|
||||
readonly all: () => Effect.Effect<Info[], Error>
|
||||
readonly create: (input: {
|
||||
serviceID: ServiceID
|
||||
credential: Credential
|
||||
description?: string
|
||||
}) => Effect.Effect<Info | undefined, Error>
|
||||
readonly update: (id: ID, updates: Partial<Pick<Info, "description" | "credential">>) => Effect.Effect<void, Error>
|
||||
readonly remove: (id: ID) => Effect.Effect<void, Error>
|
||||
readonly activate: (id: ID) => Effect.Effect<void, Error>
|
||||
readonly active: (serviceID: ServiceID) => Effect.Effect<Info | undefined, Error>
|
||||
readonly forService: (serviceID: ServiceID) => Effect.Effect<Info[], Error>
|
||||
}
|
||||
|
||||
export class Service extends Context.Service<Service, Interface>()("@opencode/v2/Account") {}
|
||||
|
||||
export const layer = Layer.effect(
|
||||
Service,
|
||||
Effect.gen(function* () {
|
||||
const fsys = yield* AppFileSystem.Service
|
||||
const global = yield* Global.Service
|
||||
const events = yield* EventV2.Service
|
||||
const file = path.join(global.data, "account.json")
|
||||
const legacyFile = path.join(global.data, "auth.json")
|
||||
|
||||
const writeMigrated = Effect.fnUntraced(function* (raw: Record<string, unknown>) {
|
||||
const migrated = migrate(raw)
|
||||
yield* fsys
|
||||
.writeJson(file, migrated, 0o600)
|
||||
.pipe(Effect.mapError((cause) => new FileWriteError({ operation: "migrate", cause })))
|
||||
return migrated
|
||||
})
|
||||
|
||||
const parseAuthContent = () => {
|
||||
try {
|
||||
return JSON.parse(process.env.OPENCODE_AUTH_CONTENT ?? "")
|
||||
} catch {}
|
||||
}
|
||||
|
||||
const load: () => Effect.Effect<Writable, Error> = Effect.fnUntraced(function* () {
|
||||
if (process.env.OPENCODE_AUTH_CONTENT) {
|
||||
const raw = parseAuthContent()
|
||||
if (raw && typeof raw === "object") {
|
||||
if ("version" in raw && raw.version === 2) return raw as Writable
|
||||
return yield* writeMigrated(raw as Record<string, unknown>)
|
||||
}
|
||||
return { version: 2, accounts: {}, active: {} }
|
||||
}
|
||||
|
||||
const legacy = yield* fsys.readJson(legacyFile).pipe(Effect.orElseSucceed(() => null))
|
||||
if (legacy && typeof legacy === "object") return yield* writeMigrated(legacy as Record<string, unknown>)
|
||||
|
||||
const raw = yield* fsys.readJson(file).pipe(Effect.orElseSucceed(() => null))
|
||||
|
||||
if (raw && typeof raw === "object") {
|
||||
if ("version" in raw && raw.version === 2) return raw as Writable
|
||||
return yield* writeMigrated(raw as Record<string, unknown>)
|
||||
}
|
||||
|
||||
return { version: 2, accounts: {}, active: {} }
|
||||
})
|
||||
|
||||
const write = (data: Writable) =>
|
||||
fsys
|
||||
.writeJson(file, data, 0o600)
|
||||
.pipe(Effect.mapError((cause) => new FileWriteError({ operation: "write", cause })))
|
||||
|
||||
const state = SynchronizedRef.makeUnsafe(
|
||||
yield* load().pipe(Effect.orElseSucceed((): Writable => ({ version: 2, accounts: {}, active: {} }))),
|
||||
)
|
||||
|
||||
const activate = Effect.fn("AccountV2.activate")(function* (id: ID) {
|
||||
const data = yield* SynchronizedRef.get(state)
|
||||
const account = data.accounts[id]
|
||||
if (!account) return
|
||||
const activated = yield* SynchronizedRef.modifyEffect(
|
||||
state,
|
||||
Effect.fnUntraced(function* (data) {
|
||||
const nextAccount = data.accounts[id]
|
||||
if (!nextAccount) return [undefined, data] as const
|
||||
|
||||
const next = { ...data, active: { ...data.active, [nextAccount.serviceID]: id } }
|
||||
yield* write(next)
|
||||
return [{ serviceID: nextAccount.serviceID, from: data.active[nextAccount.serviceID], to: id }, next] as const
|
||||
}),
|
||||
)
|
||||
if (activated) yield* events.publish(Event.Switched, activated)
|
||||
})
|
||||
|
||||
const result: Interface = {
|
||||
get: Effect.fn("AccountV2.get")(function* (id) {
|
||||
return (yield* SynchronizedRef.get(state)).accounts[id]
|
||||
}),
|
||||
|
||||
all: Effect.fn("AccountV2.all")(function* () {
|
||||
return Object.values((yield* SynchronizedRef.get(state)).accounts)
|
||||
}),
|
||||
|
||||
active: Effect.fn("AccountV2.active")(function* (serviceID) {
|
||||
const data = yield* SynchronizedRef.get(state)
|
||||
return (
|
||||
data.accounts[data.active[serviceID]] ?? Object.values(data.accounts).find((a) => a.serviceID === serviceID)
|
||||
)
|
||||
}),
|
||||
|
||||
forService: Effect.fn("AccountV2.list")(function* (serviceID) {
|
||||
return Object.values((yield* SynchronizedRef.get(state)).accounts).filter((a) => a.serviceID === serviceID)
|
||||
}),
|
||||
|
||||
create: Effect.fn("AccountV2.add")(function* (input) {
|
||||
const id = ID.make(Identifier.ascending())
|
||||
const account = new Info({
|
||||
id,
|
||||
serviceID: input.serviceID,
|
||||
description: input.description ?? "default",
|
||||
credential: input.credential,
|
||||
})
|
||||
const added = yield* SynchronizedRef.modifyEffect(
|
||||
state,
|
||||
Effect.fnUntraced(function* (data) {
|
||||
const next = {
|
||||
...data,
|
||||
accounts: { ...data.accounts, [account.id]: account },
|
||||
active: { ...data.active, [account.serviceID]: account.id },
|
||||
}
|
||||
|
||||
yield* write(next)
|
||||
return [
|
||||
{
|
||||
account,
|
||||
switched: { serviceID: account.serviceID, from: data.active[account.serviceID], to: account.id },
|
||||
},
|
||||
next,
|
||||
] as const
|
||||
}),
|
||||
)
|
||||
yield* events.publish(Event.Added, { account: added.account })
|
||||
yield* events.publish(Event.Switched, added.switched)
|
||||
return added.account
|
||||
}),
|
||||
|
||||
update: Effect.fn("AccountV2.update")(function* (id, updates) {
|
||||
const existing = (yield* SynchronizedRef.get(state)).accounts[id]
|
||||
if (!existing) return
|
||||
yield* SynchronizedRef.modifyEffect(
|
||||
state,
|
||||
Effect.fnUntraced(function* (data) {
|
||||
if (!data.accounts[id]) return [undefined, data] as const
|
||||
|
||||
const next = {
|
||||
...data,
|
||||
accounts: {
|
||||
...data.accounts,
|
||||
[id]: new Info({
|
||||
id,
|
||||
serviceID: existing.serviceID,
|
||||
description: updates.description ?? existing.description,
|
||||
credential: updates.credential ?? existing.credential,
|
||||
}),
|
||||
},
|
||||
}
|
||||
|
||||
yield* write(next)
|
||||
return [undefined, next] as const
|
||||
}),
|
||||
)
|
||||
}),
|
||||
|
||||
remove: Effect.fn("AccountV2.remove")(function* (id) {
|
||||
const removed = yield* SynchronizedRef.modifyEffect(
|
||||
state,
|
||||
Effect.fnUntraced(function* (data) {
|
||||
const accounts = { ...data.accounts }
|
||||
const active = { ...data.active }
|
||||
const removed = accounts[id]
|
||||
if (!removed) return [undefined, data] as const
|
||||
const wasActive = active[removed.serviceID] === id
|
||||
delete accounts[id]
|
||||
const replacement = Object.values(accounts).find((account) => account.serviceID === removed.serviceID)
|
||||
if (wasActive) {
|
||||
if (replacement) active[removed.serviceID] = replacement.id
|
||||
else delete active[removed.serviceID]
|
||||
}
|
||||
|
||||
const next = { ...data, accounts, active }
|
||||
yield* write(next)
|
||||
return [
|
||||
{
|
||||
account: removed,
|
||||
switched: wasActive ? { serviceID: removed.serviceID, from: id, to: replacement?.id } : undefined,
|
||||
},
|
||||
next,
|
||||
] as const
|
||||
}),
|
||||
)
|
||||
if (removed) {
|
||||
yield* events.publish(Event.Removed, { account: removed.account })
|
||||
if (removed.switched) yield* events.publish(Event.Switched, removed.switched)
|
||||
}
|
||||
}),
|
||||
|
||||
activate,
|
||||
}
|
||||
|
||||
return Service.of(result)
|
||||
}),
|
||||
)
|
||||
|
||||
export const defaultLayer = layer.pipe(
|
||||
Layer.provide(AppFileSystem.defaultLayer),
|
||||
Layer.provide(Global.defaultLayer),
|
||||
Layer.provide(EventV2.defaultLayer),
|
||||
)
|
||||
|
||||
export * as AccountV2 from "./account"
|
||||
export const PollResult = Schema.Union([PollSuccess, PollPending, PollSlow, PollExpired, PollDenied, PollError])
|
||||
export type PollResult = Schema.Schema.Type<typeof PollResult>
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
import { sqliteTable, text, integer, primaryKey } from "drizzle-orm/sqlite-core"
|
||||
|
||||
import { type AccessToken, type AccountID, type OrgID, type RefreshToken } from "./schema"
|
||||
import { Timestamps } from "../storage/schema.sql"
|
||||
import { AccountV2 } from "../account"
|
||||
import { Timestamps } from "../database/schema.sql"
|
||||
|
||||
export const AccountTable = sqliteTable("account", {
|
||||
id: text().$type<AccountID>().primaryKey(),
|
||||
id: text().$type<AccountV2.ID>().primaryKey(),
|
||||
email: text().notNull(),
|
||||
url: text().notNull(),
|
||||
access_token: text().$type<AccessToken>().notNull(),
|
||||
refresh_token: text().$type<RefreshToken>().notNull(),
|
||||
access_token: text().$type<AccountV2.AccessToken>().notNull(),
|
||||
refresh_token: text().$type<AccountV2.RefreshToken>().notNull(),
|
||||
token_expiry: integer(),
|
||||
...Timestamps,
|
||||
})
|
||||
@@ -16,9 +16,9 @@ export const AccountTable = sqliteTable("account", {
|
||||
export const AccountStateTable = sqliteTable("account_state", {
|
||||
id: integer().primaryKey(),
|
||||
active_account_id: text()
|
||||
.$type<AccountID>()
|
||||
.$type<AccountV2.ID>()
|
||||
.references(() => AccountTable.id, { onDelete: "set null" }),
|
||||
active_org_id: text().$type<OrgID>(),
|
||||
active_org_id: text().$type<AccountV2.OrgID>(),
|
||||
})
|
||||
|
||||
// LEGACY
|
||||
@@ -27,8 +27,8 @@ export const ControlAccountTable = sqliteTable(
|
||||
{
|
||||
email: text().notNull(),
|
||||
url: text().notNull(),
|
||||
access_token: text().$type<AccessToken>().notNull(),
|
||||
refresh_token: text().$type<RefreshToken>().notNull(),
|
||||
access_token: text().$type<AccountV2.AccessToken>().notNull(),
|
||||
refresh_token: text().$type<AccountV2.RefreshToken>().notNull(),
|
||||
token_expiry: integer(),
|
||||
active: integer({ mode: "boolean" })
|
||||
.notNull()
|
||||
@@ -0,0 +1,326 @@
|
||||
export * as Auth from "./auth"
|
||||
|
||||
import path from "path"
|
||||
import { Effect, Layer, Option, Schema, Context, SynchronizedRef } from "effect"
|
||||
import { Identifier } from "./util/identifier"
|
||||
import { NonNegativeInt, withStatics } from "./schema"
|
||||
import { Global } from "./global"
|
||||
import { AppFileSystem } from "./filesystem"
|
||||
import { EventV2 } from "./event"
|
||||
|
||||
export const ID = Schema.String.pipe(
|
||||
Schema.brand("Auth.ID"),
|
||||
withStatics((schema) => ({ create: () => schema.make("acc_" + Identifier.ascending()) })),
|
||||
)
|
||||
export type ID = typeof ID.Type
|
||||
|
||||
export const ServiceID = Schema.String.pipe(Schema.brand("ServiceID"))
|
||||
export type ServiceID = typeof ServiceID.Type
|
||||
|
||||
export const OrgID = Schema.String.pipe(Schema.brand("OrgID"))
|
||||
export type OrgID = typeof OrgID.Type
|
||||
export const AccessToken = Schema.String.pipe(Schema.brand("AccessToken"))
|
||||
export type AccessToken = typeof AccessToken.Type
|
||||
export const RefreshToken = Schema.String.pipe(Schema.brand("RefreshToken"))
|
||||
export type RefreshToken = typeof RefreshToken.Type
|
||||
|
||||
export class OAuthCredential extends Schema.Class<OAuthCredential>("Auth.OAuthCredential")({
|
||||
type: Schema.Literal("oauth"),
|
||||
refresh: Schema.String,
|
||||
access: Schema.String,
|
||||
expires: NonNegativeInt,
|
||||
}) {}
|
||||
|
||||
export class ApiKeyCredential extends Schema.Class<ApiKeyCredential>("Auth.ApiKeyCredential")({
|
||||
type: Schema.Literal("api"),
|
||||
key: Schema.String,
|
||||
metadata: Schema.optional(Schema.Record(Schema.String, Schema.String)),
|
||||
}) {}
|
||||
|
||||
export const Credential = Schema.Union([OAuthCredential, ApiKeyCredential])
|
||||
.pipe(Schema.toTaggedUnion("type"))
|
||||
.annotate({
|
||||
identifier: "Auth.Credential",
|
||||
})
|
||||
export type Credential = Schema.Schema.Type<typeof Credential>
|
||||
|
||||
export class Info extends Schema.Class<Info>("Auth.Info")({
|
||||
id: ID,
|
||||
serviceID: ServiceID,
|
||||
description: Schema.String,
|
||||
credential: Credential,
|
||||
}) {}
|
||||
|
||||
export class FileWriteError extends Schema.TaggedErrorClass<FileWriteError>()("Auth.FileWriteError", {
|
||||
operation: Schema.Union([Schema.Literal("migrate"), Schema.Literal("write")]),
|
||||
cause: Schema.Defect,
|
||||
}) {}
|
||||
|
||||
export type Error = FileWriteError
|
||||
|
||||
export const Event = {
|
||||
Added: EventV2.define({
|
||||
type: "account.added",
|
||||
schema: {
|
||||
account: Info,
|
||||
},
|
||||
}),
|
||||
Removed: EventV2.define({
|
||||
type: "account.removed",
|
||||
schema: {
|
||||
account: Info,
|
||||
},
|
||||
}),
|
||||
Switched: EventV2.define({
|
||||
type: "account.switched",
|
||||
schema: {
|
||||
serviceID: ServiceID,
|
||||
from: Schema.optional(ID),
|
||||
to: Schema.optional(ID),
|
||||
},
|
||||
}),
|
||||
}
|
||||
|
||||
interface Writable {
|
||||
version: 2
|
||||
accounts: Record<string, Info>
|
||||
active: Record<string, ID>
|
||||
}
|
||||
|
||||
const decodeV1 = Schema.decodeUnknownOption(Schema.Record(Schema.String, Credential))
|
||||
|
||||
function migrate(old: Record<string, unknown>): Writable {
|
||||
const accounts: Record<string, Info> = {}
|
||||
const active: Record<string, ID> = {}
|
||||
for (const [serviceID, value] of Object.entries(old)) {
|
||||
const decoded = Option.getOrElse(decodeV1({ [serviceID]: value }), () => ({}))
|
||||
const parsed = (decoded as Record<string, Credential>)[serviceID]
|
||||
if (!parsed) continue
|
||||
const id = Identifier.ascending()
|
||||
const account = ID.make(id)
|
||||
const brandedServiceID = ServiceID.make(serviceID)
|
||||
accounts[id] = new Info({
|
||||
id: account,
|
||||
serviceID: brandedServiceID,
|
||||
description: "default",
|
||||
credential: parsed,
|
||||
})
|
||||
active[brandedServiceID] = account
|
||||
}
|
||||
return { version: 2, accounts, active }
|
||||
}
|
||||
|
||||
export interface Interface {
|
||||
readonly get: (id: ID) => Effect.Effect<Info | undefined, Error>
|
||||
readonly all: () => Effect.Effect<Info[], Error>
|
||||
readonly create: (input: {
|
||||
serviceID: ServiceID
|
||||
credential: Credential
|
||||
description?: string
|
||||
}) => Effect.Effect<Info | undefined, Error>
|
||||
readonly update: (id: ID, updates: Partial<Pick<Info, "description" | "credential">>) => Effect.Effect<void, Error>
|
||||
readonly remove: (id: ID) => Effect.Effect<void, Error>
|
||||
readonly activate: (id: ID) => Effect.Effect<void, Error>
|
||||
readonly active: (serviceID: ServiceID) => Effect.Effect<Info | undefined, Error>
|
||||
readonly forService: (serviceID: ServiceID) => Effect.Effect<Info[], Error>
|
||||
}
|
||||
|
||||
export class Service extends Context.Service<Service, Interface>()("@opencode/v2/Account") {}
|
||||
|
||||
export const layer = Layer.effect(
|
||||
Service,
|
||||
Effect.gen(function* () {
|
||||
const fsys = yield* AppFileSystem.Service
|
||||
const global = yield* Global.Service
|
||||
const events = yield* EventV2.Service
|
||||
const file = path.join(global.data, "account.json")
|
||||
const legacyFile = path.join(global.data, "auth.json")
|
||||
|
||||
const writeMigrated = Effect.fnUntraced(function* (raw: Record<string, unknown>) {
|
||||
const migrated = migrate(raw)
|
||||
yield* fsys
|
||||
.writeJson(file, migrated, 0o600)
|
||||
.pipe(Effect.mapError((cause) => new FileWriteError({ operation: "migrate", cause })))
|
||||
return migrated
|
||||
})
|
||||
|
||||
const parseAuthContent = () => {
|
||||
try {
|
||||
return JSON.parse(process.env.OPENCODE_AUTH_CONTENT ?? "")
|
||||
} catch {}
|
||||
}
|
||||
|
||||
const load: () => Effect.Effect<Writable, Error> = Effect.fnUntraced(function* () {
|
||||
if (process.env.OPENCODE_AUTH_CONTENT) {
|
||||
const raw = parseAuthContent()
|
||||
if (raw && typeof raw === "object") {
|
||||
if ("version" in raw && raw.version === 2) return raw as Writable
|
||||
return yield* writeMigrated(raw as Record<string, unknown>)
|
||||
}
|
||||
return { version: 2, accounts: {}, active: {} }
|
||||
}
|
||||
|
||||
const legacy = yield* fsys.readJson(legacyFile).pipe(Effect.orElseSucceed(() => null))
|
||||
if (legacy && typeof legacy === "object") return yield* writeMigrated(legacy as Record<string, unknown>)
|
||||
|
||||
const raw = yield* fsys.readJson(file).pipe(Effect.orElseSucceed(() => null))
|
||||
|
||||
if (raw && typeof raw === "object") {
|
||||
if ("version" in raw && raw.version === 2) return raw as Writable
|
||||
return yield* writeMigrated(raw as Record<string, unknown>)
|
||||
}
|
||||
|
||||
return { version: 2, accounts: {}, active: {} }
|
||||
})
|
||||
|
||||
const write = (data: Writable) =>
|
||||
fsys
|
||||
.writeJson(file, data, 0o600)
|
||||
.pipe(Effect.mapError((cause) => new FileWriteError({ operation: "write", cause })))
|
||||
|
||||
const state = SynchronizedRef.makeUnsafe(
|
||||
yield* load().pipe(Effect.orElseSucceed((): Writable => ({ version: 2, accounts: {}, active: {} }))),
|
||||
)
|
||||
|
||||
const activate = Effect.fn("Auth.activate")(function* (id: ID) {
|
||||
const data = yield* SynchronizedRef.get(state)
|
||||
const account = data.accounts[id]
|
||||
if (!account) return
|
||||
const activated = yield* SynchronizedRef.modifyEffect(
|
||||
state,
|
||||
Effect.fnUntraced(function* (data) {
|
||||
const nextAccount = data.accounts[id]
|
||||
if (!nextAccount) return [undefined, data] as const
|
||||
|
||||
const next = { ...data, active: { ...data.active, [nextAccount.serviceID]: id } }
|
||||
yield* write(next)
|
||||
return [{ serviceID: nextAccount.serviceID, from: data.active[nextAccount.serviceID], to: id }, next] as const
|
||||
}),
|
||||
)
|
||||
if (activated) yield* events.publish(Event.Switched, activated)
|
||||
})
|
||||
|
||||
const result: Interface = {
|
||||
get: Effect.fn("Auth.get")(function* (id) {
|
||||
return (yield* SynchronizedRef.get(state)).accounts[id]
|
||||
}),
|
||||
|
||||
all: Effect.fn("Auth.all")(function* () {
|
||||
return Object.values((yield* SynchronizedRef.get(state)).accounts)
|
||||
}),
|
||||
|
||||
active: Effect.fn("Auth.active")(function* (serviceID) {
|
||||
const data = yield* SynchronizedRef.get(state)
|
||||
return (
|
||||
data.accounts[data.active[serviceID]] ?? Object.values(data.accounts).find((a) => a.serviceID === serviceID)
|
||||
)
|
||||
}),
|
||||
|
||||
forService: Effect.fn("Auth.list")(function* (serviceID) {
|
||||
return Object.values((yield* SynchronizedRef.get(state)).accounts).filter((a) => a.serviceID === serviceID)
|
||||
}),
|
||||
|
||||
create: Effect.fn("Auth.add")(function* (input) {
|
||||
const id = ID.make(Identifier.ascending())
|
||||
const account = new Info({
|
||||
id,
|
||||
serviceID: input.serviceID,
|
||||
description: input.description ?? "default",
|
||||
credential: input.credential,
|
||||
})
|
||||
const added = yield* SynchronizedRef.modifyEffect(
|
||||
state,
|
||||
Effect.fnUntraced(function* (data) {
|
||||
const next = {
|
||||
...data,
|
||||
accounts: { ...data.accounts, [account.id]: account },
|
||||
active: { ...data.active, [account.serviceID]: account.id },
|
||||
}
|
||||
|
||||
yield* write(next)
|
||||
return [
|
||||
{
|
||||
account,
|
||||
switched: { serviceID: account.serviceID, from: data.active[account.serviceID], to: account.id },
|
||||
},
|
||||
next,
|
||||
] as const
|
||||
}),
|
||||
)
|
||||
yield* events.publish(Event.Added, { account: added.account })
|
||||
yield* events.publish(Event.Switched, added.switched)
|
||||
return added.account
|
||||
}),
|
||||
|
||||
update: Effect.fn("Auth.update")(function* (id, updates) {
|
||||
const existing = (yield* SynchronizedRef.get(state)).accounts[id]
|
||||
if (!existing) return
|
||||
yield* SynchronizedRef.modifyEffect(
|
||||
state,
|
||||
Effect.fnUntraced(function* (data) {
|
||||
if (!data.accounts[id]) return [undefined, data] as const
|
||||
|
||||
const next = {
|
||||
...data,
|
||||
accounts: {
|
||||
...data.accounts,
|
||||
[id]: new Info({
|
||||
id,
|
||||
serviceID: existing.serviceID,
|
||||
description: updates.description ?? existing.description,
|
||||
credential: updates.credential ?? existing.credential,
|
||||
}),
|
||||
},
|
||||
}
|
||||
|
||||
yield* write(next)
|
||||
return [undefined, next] as const
|
||||
}),
|
||||
)
|
||||
}),
|
||||
|
||||
remove: Effect.fn("Auth.remove")(function* (id) {
|
||||
const removed = yield* SynchronizedRef.modifyEffect(
|
||||
state,
|
||||
Effect.fnUntraced(function* (data) {
|
||||
const accounts = { ...data.accounts }
|
||||
const active = { ...data.active }
|
||||
const removed = accounts[id]
|
||||
if (!removed) return [undefined, data] as const
|
||||
const wasActive = active[removed.serviceID] === id
|
||||
delete accounts[id]
|
||||
const replacement = Object.values(accounts).find((account) => account.serviceID === removed.serviceID)
|
||||
if (wasActive) {
|
||||
if (replacement) active[removed.serviceID] = replacement.id
|
||||
else delete active[removed.serviceID]
|
||||
}
|
||||
|
||||
const next = { ...data, accounts, active }
|
||||
yield* write(next)
|
||||
return [
|
||||
{
|
||||
account: removed,
|
||||
switched: wasActive ? { serviceID: removed.serviceID, from: id, to: replacement?.id } : undefined,
|
||||
},
|
||||
next,
|
||||
] as const
|
||||
}),
|
||||
)
|
||||
if (removed) {
|
||||
yield* events.publish(Event.Removed, { account: removed.account })
|
||||
if (removed.switched) yield* events.publish(Event.Switched, removed.switched)
|
||||
}
|
||||
}),
|
||||
|
||||
activate,
|
||||
}
|
||||
|
||||
return Service.of(result)
|
||||
}),
|
||||
)
|
||||
|
||||
export const defaultLayer = layer.pipe(
|
||||
Layer.provide(AppFileSystem.defaultLayer),
|
||||
Layer.provide(Global.defaultLayer),
|
||||
Layer.provide(EventV2.defaultLayer),
|
||||
)
|
||||
+5
-5
@@ -1,17 +1,17 @@
|
||||
import { sqliteTable, text, integer } from "drizzle-orm/sqlite-core"
|
||||
import { ProjectTable } from "../project/project.sql"
|
||||
import type { ProjectID } from "../project/schema"
|
||||
import type { WorkspaceID } from "./schema"
|
||||
import { ProjectTable } from "../project/sql"
|
||||
import { ProjectV2 } from "../project"
|
||||
import { WorkspaceV2 } from "../workspace"
|
||||
|
||||
export const WorkspaceTable = sqliteTable("workspace", {
|
||||
id: text().$type<WorkspaceID>().primaryKey(),
|
||||
id: text().$type<WorkspaceV2.ID>().primaryKey(),
|
||||
type: text().notNull(),
|
||||
name: text().notNull().default(""),
|
||||
branch: text(),
|
||||
directory: text(),
|
||||
extra: text({ mode: "json" }),
|
||||
project_id: text()
|
||||
.$type<ProjectID>()
|
||||
.$type<ProjectV2.ID>()
|
||||
.notNull()
|
||||
.references(() => ProjectTable.id, { onDelete: "cascade" }),
|
||||
time_used: integer()
|
||||
@@ -0,0 +1,60 @@
|
||||
export * as Database from "./database"
|
||||
|
||||
import { EffectDrizzleSqlite } from "@opencode-ai/effect-drizzle-sqlite"
|
||||
import { layer as sqliteLayer } from "#sqlite"
|
||||
import { Context, Effect, Layer } from "effect"
|
||||
import { Global } from "../global"
|
||||
import { Flag } from "../flag/flag"
|
||||
import { isAbsolute, join } from "path"
|
||||
import { DatabaseMigration } from "./migration"
|
||||
import { InstallationChannel } from "../installation/version"
|
||||
|
||||
const makeDatabase = EffectDrizzleSqlite.makeWithDefaults()
|
||||
type DatabaseShape = Effect.Success<typeof makeDatabase>
|
||||
|
||||
export interface Interface {
|
||||
db: DatabaseShape
|
||||
}
|
||||
|
||||
export class Service extends Context.Service<Service, Interface>()("@opencode/v2/storage/Database") {}
|
||||
|
||||
export const layer = Layer.effect(
|
||||
Service,
|
||||
Effect.gen(function* () {
|
||||
const db = yield* makeDatabase
|
||||
|
||||
yield* db.run("PRAGMA journal_mode = WAL")
|
||||
yield* db.run("PRAGMA synchronous = NORMAL")
|
||||
yield* db.run("PRAGMA busy_timeout = 5000")
|
||||
yield* db.run("PRAGMA cache_size = -64000")
|
||||
yield* db.run("PRAGMA foreign_keys = ON")
|
||||
yield* db.run("PRAGMA wal_checkpoint(PASSIVE)")
|
||||
yield* DatabaseMigration.apply(db)
|
||||
|
||||
return { db }
|
||||
}).pipe(Effect.orDie),
|
||||
)
|
||||
|
||||
export function layerFromPath(filename: string) {
|
||||
return layer.pipe(Layer.provide(sqliteLayer({ filename })))
|
||||
}
|
||||
|
||||
export function path() {
|
||||
if (Flag.OPENCODE_DB) {
|
||||
if (Flag.OPENCODE_DB === ":memory:" || isAbsolute(Flag.OPENCODE_DB)) return Flag.OPENCODE_DB
|
||||
return join(Global.Path.data, Flag.OPENCODE_DB)
|
||||
}
|
||||
if (
|
||||
["latest", "beta", "prod"].includes(InstallationChannel) ||
|
||||
process.env.OPENCODE_DISABLE_CHANNEL_DB === "1" ||
|
||||
process.env.OPENCODE_DISABLE_CHANNEL_DB === "true"
|
||||
)
|
||||
return join(Global.Path.data, "opencode.db")
|
||||
return join(Global.Path.data, `opencode-${InstallationChannel.replace(/[^a-zA-Z0-9._-]/g, "-")}.db`)
|
||||
}
|
||||
|
||||
export const defaultLayer = Layer.unwrap(
|
||||
Effect.gen(function* () {
|
||||
return layerFromPath(path())
|
||||
}),
|
||||
).pipe(Layer.provide(Global.defaultLayer))
|
||||
@@ -0,0 +1,24 @@
|
||||
import type { DatabaseMigration } from "./migration"
|
||||
|
||||
export const migrations = (await Promise.all([
|
||||
import("./migration/20260127222353_familiar_lady_ursula"),
|
||||
import("./migration/20260211171708_add_project_commands"),
|
||||
import("./migration/20260213144116_wakeful_the_professor"),
|
||||
import("./migration/20260225215848_workspace"),
|
||||
import("./migration/20260227213759_add_session_workspace_id"),
|
||||
import("./migration/20260228203230_blue_harpoon"),
|
||||
import("./migration/20260303231226_add_workspace_fields"),
|
||||
import("./migration/20260309230000_move_org_to_state"),
|
||||
import("./migration/20260312043431_session_message_cursor"),
|
||||
import("./migration/20260323234822_events"),
|
||||
import("./migration/20260410174513_workspace-name"),
|
||||
import("./migration/20260413175956_chief_energizer"),
|
||||
import("./migration/20260423070820_add_icon_url_override"),
|
||||
import("./migration/20260427172553_slow_nightmare"),
|
||||
import("./migration/20260428004200_add_session_path"),
|
||||
import("./migration/20260501142318_next_venus"),
|
||||
import("./migration/20260504145000_add_sync_owner"),
|
||||
import("./migration/20260507164347_add_workspace_time"),
|
||||
import("./migration/20260510033149_session_usage"),
|
||||
import("./migration/20260511000411_data_migration_state"),
|
||||
])).map((module) => module.default) satisfies DatabaseMigration.Migration[]
|
||||
@@ -0,0 +1,58 @@
|
||||
export * as DatabaseMigration from "./migration"
|
||||
|
||||
import { sql } from "drizzle-orm"
|
||||
import { Effect } from "effect"
|
||||
import type { EffectDrizzleSqlite } from "@opencode-ai/effect-drizzle-sqlite"
|
||||
import { migrations } from "./migration.gen"
|
||||
|
||||
type Database = EffectDrizzleSqlite.EffectSQLiteDatabase
|
||||
type Transaction = Parameters<Parameters<Database["transaction"]>[0]>[0]
|
||||
|
||||
export type Migration = {
|
||||
id: string
|
||||
up: (tx: Transaction) => Effect.Effect<void, unknown>
|
||||
}
|
||||
|
||||
export function apply(db: Database) {
|
||||
return applyOnly(db, migrations)
|
||||
}
|
||||
|
||||
export function applyOnly(db: Database, input: Migration[]) {
|
||||
return Effect.gen(function* () {
|
||||
yield* db.run(
|
||||
sql`CREATE TABLE IF NOT EXISTS ${sql.identifier("migration")} (id TEXT PRIMARY KEY, time_completed INTEGER NOT NULL)`,
|
||||
)
|
||||
let completed = new Set(
|
||||
(yield* db.all<{ id: string }>(sql`SELECT id FROM ${sql.identifier("migration")}`)).map((row) => row.id),
|
||||
)
|
||||
if (completed.size === 0) {
|
||||
// Existing installs used Drizzle's migration journal. Seed the new
|
||||
// journal once so TypeScript migrations don't replay old SQL.
|
||||
if (
|
||||
yield* db.get(sql`SELECT name FROM sqlite_master WHERE type = 'table' AND name = ${"__drizzle_migrations"}`)
|
||||
) {
|
||||
yield* db.run(sql`
|
||||
INSERT OR IGNORE INTO ${sql.identifier("migration")} (id, time_completed)
|
||||
SELECT name, ${Date.now()}
|
||||
FROM ${sql.identifier("__drizzle_migrations")}
|
||||
WHERE name IS NOT NULL
|
||||
`)
|
||||
completed = new Set(
|
||||
(yield* db.all<{ id: string }>(sql`SELECT id FROM ${sql.identifier("migration")}`)).map((row) => row.id),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
for (const migration of input) {
|
||||
if (completed.has(migration.id)) continue
|
||||
yield* db.transaction((tx) =>
|
||||
Effect.gen(function* () {
|
||||
yield* migration.up(tx)
|
||||
yield* tx.run(
|
||||
sql`INSERT INTO ${sql.identifier("migration")} (id, time_completed) VALUES (${migration.id}, ${Date.now()})`,
|
||||
)
|
||||
}),
|
||||
)
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,107 @@
|
||||
import { Effect } from "effect"
|
||||
import type { DatabaseMigration } from "../migration"
|
||||
|
||||
export default {
|
||||
id: "20260127222353_familiar_lady_ursula",
|
||||
up(tx) {
|
||||
return Effect.gen(function* () {
|
||||
yield* tx.run(`
|
||||
CREATE TABLE \`project\` (
|
||||
\`id\` text PRIMARY KEY,
|
||||
\`worktree\` text NOT NULL,
|
||||
\`vcs\` text,
|
||||
\`name\` text,
|
||||
\`icon_url\` text,
|
||||
\`icon_color\` text,
|
||||
\`time_created\` integer NOT NULL,
|
||||
\`time_updated\` integer NOT NULL,
|
||||
\`time_initialized\` integer,
|
||||
\`sandboxes\` text NOT NULL
|
||||
);
|
||||
`)
|
||||
yield* tx.run(`
|
||||
CREATE TABLE \`message\` (
|
||||
\`id\` text PRIMARY KEY,
|
||||
\`session_id\` text NOT NULL,
|
||||
\`time_created\` integer NOT NULL,
|
||||
\`time_updated\` integer NOT NULL,
|
||||
\`data\` text NOT NULL,
|
||||
CONSTRAINT \`fk_message_session_id_session_id_fk\` FOREIGN KEY (\`session_id\`) REFERENCES \`session\`(\`id\`) ON DELETE CASCADE
|
||||
);
|
||||
`)
|
||||
yield* tx.run(`
|
||||
CREATE TABLE \`part\` (
|
||||
\`id\` text PRIMARY KEY,
|
||||
\`message_id\` text NOT NULL,
|
||||
\`session_id\` text NOT NULL,
|
||||
\`time_created\` integer NOT NULL,
|
||||
\`time_updated\` integer NOT NULL,
|
||||
\`data\` text NOT NULL,
|
||||
CONSTRAINT \`fk_part_message_id_message_id_fk\` FOREIGN KEY (\`message_id\`) REFERENCES \`message\`(\`id\`) ON DELETE CASCADE
|
||||
);
|
||||
`)
|
||||
yield* tx.run(`
|
||||
CREATE TABLE \`permission\` (
|
||||
\`project_id\` text PRIMARY KEY,
|
||||
\`time_created\` integer NOT NULL,
|
||||
\`time_updated\` integer NOT NULL,
|
||||
\`data\` text NOT NULL,
|
||||
CONSTRAINT \`fk_permission_project_id_project_id_fk\` FOREIGN KEY (\`project_id\`) REFERENCES \`project\`(\`id\`) ON DELETE CASCADE
|
||||
);
|
||||
`)
|
||||
yield* tx.run(`
|
||||
CREATE TABLE \`session\` (
|
||||
\`id\` text PRIMARY KEY,
|
||||
\`project_id\` text NOT NULL,
|
||||
\`parent_id\` text,
|
||||
\`slug\` text NOT NULL,
|
||||
\`directory\` text NOT NULL,
|
||||
\`title\` text NOT NULL,
|
||||
\`version\` text NOT NULL,
|
||||
\`share_url\` text,
|
||||
\`summary_additions\` integer,
|
||||
\`summary_deletions\` integer,
|
||||
\`summary_files\` integer,
|
||||
\`summary_diffs\` text,
|
||||
\`revert\` text,
|
||||
\`permission\` text,
|
||||
\`time_created\` integer NOT NULL,
|
||||
\`time_updated\` integer NOT NULL,
|
||||
\`time_compacting\` integer,
|
||||
\`time_archived\` integer,
|
||||
CONSTRAINT \`fk_session_project_id_project_id_fk\` FOREIGN KEY (\`project_id\`) REFERENCES \`project\`(\`id\`) ON DELETE CASCADE
|
||||
);
|
||||
`)
|
||||
yield* tx.run(`
|
||||
CREATE TABLE \`todo\` (
|
||||
\`session_id\` text NOT NULL,
|
||||
\`content\` text NOT NULL,
|
||||
\`status\` text NOT NULL,
|
||||
\`priority\` text NOT NULL,
|
||||
\`position\` integer NOT NULL,
|
||||
\`time_created\` integer NOT NULL,
|
||||
\`time_updated\` integer NOT NULL,
|
||||
CONSTRAINT \`todo_pk\` PRIMARY KEY(\`session_id\`, \`position\`),
|
||||
CONSTRAINT \`fk_todo_session_id_session_id_fk\` FOREIGN KEY (\`session_id\`) REFERENCES \`session\`(\`id\`) ON DELETE CASCADE
|
||||
);
|
||||
`)
|
||||
yield* tx.run(`
|
||||
CREATE TABLE \`session_share\` (
|
||||
\`session_id\` text PRIMARY KEY,
|
||||
\`id\` text NOT NULL,
|
||||
\`secret\` text NOT NULL,
|
||||
\`url\` text NOT NULL,
|
||||
\`time_created\` integer NOT NULL,
|
||||
\`time_updated\` integer NOT NULL,
|
||||
CONSTRAINT \`fk_session_share_session_id_session_id_fk\` FOREIGN KEY (\`session_id\`) REFERENCES \`session\`(\`id\`) ON DELETE CASCADE
|
||||
);
|
||||
`)
|
||||
yield* tx.run(`CREATE INDEX \`message_session_idx\` ON \`message\` (\`session_id\`);`)
|
||||
yield* tx.run(`CREATE INDEX \`part_message_idx\` ON \`part\` (\`message_id\`);`)
|
||||
yield* tx.run(`CREATE INDEX \`part_session_idx\` ON \`part\` (\`session_id\`);`)
|
||||
yield* tx.run(`CREATE INDEX \`session_project_idx\` ON \`session\` (\`project_id\`);`)
|
||||
yield* tx.run(`CREATE INDEX \`session_parent_idx\` ON \`session\` (\`parent_id\`);`)
|
||||
yield* tx.run(`CREATE INDEX \`todo_session_idx\` ON \`todo\` (\`session_id\`);`)
|
||||
})
|
||||
},
|
||||
} satisfies DatabaseMigration.Migration
|
||||
@@ -0,0 +1,11 @@
|
||||
import { Effect } from "effect"
|
||||
import type { DatabaseMigration } from "../migration"
|
||||
|
||||
export default {
|
||||
id: "20260211171708_add_project_commands",
|
||||
up(tx) {
|
||||
return Effect.gen(function* () {
|
||||
yield* tx.run(`ALTER TABLE \`project\` ADD \`commands\` text;`)
|
||||
})
|
||||
},
|
||||
} satisfies DatabaseMigration.Migration
|
||||
@@ -0,0 +1,23 @@
|
||||
import { Effect } from "effect"
|
||||
import type { DatabaseMigration } from "../migration"
|
||||
|
||||
export default {
|
||||
id: "20260213144116_wakeful_the_professor",
|
||||
up(tx) {
|
||||
return Effect.gen(function* () {
|
||||
yield* tx.run(`
|
||||
CREATE TABLE \`control_account\` (
|
||||
\`email\` text NOT NULL,
|
||||
\`url\` text NOT NULL,
|
||||
\`access_token\` text NOT NULL,
|
||||
\`refresh_token\` text NOT NULL,
|
||||
\`token_expiry\` integer,
|
||||
\`active\` integer NOT NULL,
|
||||
\`time_created\` integer NOT NULL,
|
||||
\`time_updated\` integer NOT NULL,
|
||||
CONSTRAINT \`control_account_pk\` PRIMARY KEY(\`email\`, \`url\`)
|
||||
);
|
||||
`)
|
||||
})
|
||||
},
|
||||
} satisfies DatabaseMigration.Migration
|
||||
@@ -0,0 +1,19 @@
|
||||
import { Effect } from "effect"
|
||||
import type { DatabaseMigration } from "../migration"
|
||||
|
||||
export default {
|
||||
id: "20260225215848_workspace",
|
||||
up(tx) {
|
||||
return Effect.gen(function* () {
|
||||
yield* tx.run(`
|
||||
CREATE TABLE \`workspace\` (
|
||||
\`id\` text PRIMARY KEY,
|
||||
\`branch\` text,
|
||||
\`project_id\` text NOT NULL,
|
||||
\`config\` text NOT NULL,
|
||||
CONSTRAINT \`fk_workspace_project_id_project_id_fk\` FOREIGN KEY (\`project_id\`) REFERENCES \`project\`(\`id\`) ON DELETE CASCADE
|
||||
);
|
||||
`)
|
||||
})
|
||||
},
|
||||
} satisfies DatabaseMigration.Migration
|
||||
@@ -0,0 +1,12 @@
|
||||
import { Effect } from "effect"
|
||||
import type { DatabaseMigration } from "../migration"
|
||||
|
||||
export default {
|
||||
id: "20260227213759_add_session_workspace_id",
|
||||
up(tx) {
|
||||
return Effect.gen(function* () {
|
||||
yield* tx.run(`ALTER TABLE \`session\` ADD \`workspace_id\` text;`)
|
||||
yield* tx.run(`CREATE INDEX \`session_workspace_idx\` ON \`session\` (\`workspace_id\`);`)
|
||||
})
|
||||
},
|
||||
} satisfies DatabaseMigration.Migration
|
||||
@@ -0,0 +1,30 @@
|
||||
import { Effect } from "effect"
|
||||
import type { DatabaseMigration } from "../migration"
|
||||
|
||||
export default {
|
||||
id: "20260228203230_blue_harpoon",
|
||||
up(tx) {
|
||||
return Effect.gen(function* () {
|
||||
yield* tx.run(`
|
||||
CREATE TABLE \`account\` (
|
||||
\`id\` text PRIMARY KEY,
|
||||
\`email\` text NOT NULL,
|
||||
\`url\` text NOT NULL,
|
||||
\`access_token\` text NOT NULL,
|
||||
\`refresh_token\` text NOT NULL,
|
||||
\`token_expiry\` integer,
|
||||
\`selected_org_id\` text,
|
||||
\`time_created\` integer NOT NULL,
|
||||
\`time_updated\` integer NOT NULL
|
||||
);
|
||||
`)
|
||||
yield* tx.run(`
|
||||
CREATE TABLE \`account_state\` (
|
||||
\`id\` integer PRIMARY KEY NOT NULL,
|
||||
\`active_account_id\` text,
|
||||
FOREIGN KEY (\`active_account_id\`) REFERENCES \`account\`(\`id\`) ON UPDATE no action ON DELETE set null
|
||||
);
|
||||
`)
|
||||
})
|
||||
},
|
||||
} satisfies DatabaseMigration.Migration
|
||||
@@ -0,0 +1,15 @@
|
||||
import { Effect } from "effect"
|
||||
import type { DatabaseMigration } from "../migration"
|
||||
|
||||
export default {
|
||||
id: "20260303231226_add_workspace_fields",
|
||||
up(tx) {
|
||||
return Effect.gen(function* () {
|
||||
yield* tx.run(`ALTER TABLE \`workspace\` ADD \`type\` text NOT NULL;`)
|
||||
yield* tx.run(`ALTER TABLE \`workspace\` ADD \`name\` text;`)
|
||||
yield* tx.run(`ALTER TABLE \`workspace\` ADD \`directory\` text;`)
|
||||
yield* tx.run(`ALTER TABLE \`workspace\` ADD \`extra\` text;`)
|
||||
yield* tx.run(`ALTER TABLE \`workspace\` DROP COLUMN \`config\`;`)
|
||||
})
|
||||
},
|
||||
} satisfies DatabaseMigration.Migration
|
||||
@@ -0,0 +1,13 @@
|
||||
import { Effect } from "effect"
|
||||
import type { DatabaseMigration } from "../migration"
|
||||
|
||||
export default {
|
||||
id: "20260309230000_move_org_to_state",
|
||||
up(tx) {
|
||||
return Effect.gen(function* () {
|
||||
yield* tx.run(`ALTER TABLE \`account_state\` ADD \`active_org_id\` text;`)
|
||||
yield* tx.run(`UPDATE \`account_state\` SET \`active_org_id\` = (SELECT \`selected_org_id\` FROM \`account\` WHERE \`account\`.\`id\` = \`account_state\`.\`active_account_id\`);`)
|
||||
yield* tx.run(`ALTER TABLE \`account\` DROP COLUMN \`selected_org_id\`;`)
|
||||
})
|
||||
},
|
||||
} satisfies DatabaseMigration.Migration
|
||||
@@ -0,0 +1,14 @@
|
||||
import { Effect } from "effect"
|
||||
import type { DatabaseMigration } from "../migration"
|
||||
|
||||
export default {
|
||||
id: "20260312043431_session_message_cursor",
|
||||
up(tx) {
|
||||
return Effect.gen(function* () {
|
||||
yield* tx.run(`DROP INDEX IF EXISTS \`message_session_idx\`;`)
|
||||
yield* tx.run(`DROP INDEX IF EXISTS \`part_message_idx\`;`)
|
||||
yield* tx.run(`CREATE INDEX \`message_session_time_created_id_idx\` ON \`message\` (\`session_id\`,\`time_created\`,\`id\`);`)
|
||||
yield* tx.run(`CREATE INDEX \`part_message_id_id_idx\` ON \`part\` (\`message_id\`,\`id\`);`)
|
||||
})
|
||||
},
|
||||
} satisfies DatabaseMigration.Migration
|
||||
@@ -0,0 +1,26 @@
|
||||
import { Effect } from "effect"
|
||||
import type { DatabaseMigration } from "../migration"
|
||||
|
||||
export default {
|
||||
id: "20260323234822_events",
|
||||
up(tx) {
|
||||
return Effect.gen(function* () {
|
||||
yield* tx.run(`
|
||||
CREATE TABLE \`event_sequence\` (
|
||||
\`aggregate_id\` text PRIMARY KEY,
|
||||
\`seq\` integer NOT NULL
|
||||
);
|
||||
`)
|
||||
yield* tx.run(`
|
||||
CREATE TABLE \`event\` (
|
||||
\`id\` text PRIMARY KEY,
|
||||
\`aggregate_id\` text NOT NULL,
|
||||
\`seq\` integer NOT NULL,
|
||||
\`type\` text NOT NULL,
|
||||
\`data\` text NOT NULL,
|
||||
CONSTRAINT \`fk_event_aggregate_id_event_sequence_aggregate_id_fk\` FOREIGN KEY (\`aggregate_id\`) REFERENCES \`event_sequence\`(\`aggregate_id\`) ON DELETE CASCADE
|
||||
);
|
||||
`)
|
||||
})
|
||||
},
|
||||
} satisfies DatabaseMigration.Migration
|
||||
@@ -0,0 +1,27 @@
|
||||
import { Effect } from "effect"
|
||||
import type { DatabaseMigration } from "../migration"
|
||||
|
||||
export default {
|
||||
id: "20260410174513_workspace-name",
|
||||
up(tx) {
|
||||
return Effect.gen(function* () {
|
||||
yield* tx.run(`PRAGMA foreign_keys=OFF;`)
|
||||
yield* tx.run(`
|
||||
CREATE TABLE \`__new_workspace\` (
|
||||
\`id\` text PRIMARY KEY,
|
||||
\`type\` text NOT NULL,
|
||||
\`name\` text DEFAULT '' NOT NULL,
|
||||
\`branch\` text,
|
||||
\`directory\` text,
|
||||
\`extra\` text,
|
||||
\`project_id\` text NOT NULL,
|
||||
CONSTRAINT \`fk_workspace_project_id_project_id_fk\` FOREIGN KEY (\`project_id\`) REFERENCES \`project\`(\`id\`) ON DELETE CASCADE
|
||||
);
|
||||
`)
|
||||
yield* tx.run(`INSERT INTO \`__new_workspace\`(\`id\`, \`type\`, \`branch\`, \`name\`, \`directory\`, \`extra\`, \`project_id\`) SELECT \`id\`, \`type\`, \`branch\`, \`name\`, \`directory\`, \`extra\`, \`project_id\` FROM \`workspace\`;`)
|
||||
yield* tx.run(`DROP TABLE \`workspace\`;`)
|
||||
yield* tx.run(`ALTER TABLE \`__new_workspace\` RENAME TO \`workspace\`;`)
|
||||
yield* tx.run(`PRAGMA foreign_keys=ON;`)
|
||||
})
|
||||
},
|
||||
} satisfies DatabaseMigration.Migration
|
||||
@@ -0,0 +1,24 @@
|
||||
import { Effect } from "effect"
|
||||
import type { DatabaseMigration } from "../migration"
|
||||
|
||||
export default {
|
||||
id: "20260413175956_chief_energizer",
|
||||
up(tx) {
|
||||
return Effect.gen(function* () {
|
||||
yield* tx.run(`
|
||||
CREATE TABLE \`session_entry\` (
|
||||
\`id\` text PRIMARY KEY,
|
||||
\`session_id\` text NOT NULL,
|
||||
\`type\` text NOT NULL,
|
||||
\`time_created\` integer NOT NULL,
|
||||
\`time_updated\` integer NOT NULL,
|
||||
\`data\` text NOT NULL,
|
||||
CONSTRAINT \`fk_session_entry_session_id_session_id_fk\` FOREIGN KEY (\`session_id\`) REFERENCES \`session\`(\`id\`) ON DELETE CASCADE
|
||||
);
|
||||
`)
|
||||
yield* tx.run(`CREATE INDEX \`session_entry_session_idx\` ON \`session_entry\` (\`session_id\`);`)
|
||||
yield* tx.run(`CREATE INDEX \`session_entry_session_type_idx\` ON \`session_entry\` (\`session_id\`,\`type\`);`)
|
||||
yield* tx.run(`CREATE INDEX \`session_entry_time_created_idx\` ON \`session_entry\` (\`time_created\`);`)
|
||||
})
|
||||
},
|
||||
} satisfies DatabaseMigration.Migration
|
||||
@@ -0,0 +1,14 @@
|
||||
import { Effect } from "effect"
|
||||
import type { DatabaseMigration } from "../migration"
|
||||
|
||||
export default {
|
||||
id: "20260423070820_add_icon_url_override",
|
||||
up(tx) {
|
||||
return Effect.gen(function* () {
|
||||
yield* tx.run(`
|
||||
ALTER TABLE \`project\` ADD \`icon_url_override\` text;
|
||||
UPDATE \`project\` SET \`icon_url_override\` = \`icon_url\` WHERE \`icon_url\` IS NOT NULL;
|
||||
`)
|
||||
})
|
||||
},
|
||||
} satisfies DatabaseMigration.Migration
|
||||
@@ -0,0 +1,28 @@
|
||||
import { Effect } from "effect"
|
||||
import type { DatabaseMigration } from "../migration"
|
||||
|
||||
export default {
|
||||
id: "20260427172553_slow_nightmare",
|
||||
up(tx) {
|
||||
return Effect.gen(function* () {
|
||||
yield* tx.run(`
|
||||
CREATE TABLE \`session_message\` (
|
||||
\`id\` text PRIMARY KEY,
|
||||
\`session_id\` text NOT NULL,
|
||||
\`type\` text NOT NULL,
|
||||
\`time_created\` integer NOT NULL,
|
||||
\`time_updated\` integer NOT NULL,
|
||||
\`data\` text NOT NULL,
|
||||
CONSTRAINT \`fk_session_message_session_id_session_id_fk\` FOREIGN KEY (\`session_id\`) REFERENCES \`session\`(\`id\`) ON DELETE CASCADE
|
||||
);
|
||||
`)
|
||||
yield* tx.run(`DROP INDEX IF EXISTS \`session_entry_session_idx\`;`)
|
||||
yield* tx.run(`DROP INDEX IF EXISTS \`session_entry_session_type_idx\`;`)
|
||||
yield* tx.run(`DROP INDEX IF EXISTS \`session_entry_time_created_idx\`;`)
|
||||
yield* tx.run(`CREATE INDEX \`session_message_session_idx\` ON \`session_message\` (\`session_id\`);`)
|
||||
yield* tx.run(`CREATE INDEX \`session_message_session_type_idx\` ON \`session_message\` (\`session_id\`,\`type\`);`)
|
||||
yield* tx.run(`CREATE INDEX \`session_message_time_created_idx\` ON \`session_message\` (\`time_created\`);`)
|
||||
yield* tx.run(`DROP TABLE \`session_entry\`;`)
|
||||
})
|
||||
},
|
||||
} satisfies DatabaseMigration.Migration
|
||||
@@ -0,0 +1,11 @@
|
||||
import { Effect } from "effect"
|
||||
import type { DatabaseMigration } from "../migration"
|
||||
|
||||
export default {
|
||||
id: "20260428004200_add_session_path",
|
||||
up(tx) {
|
||||
return Effect.gen(function* () {
|
||||
yield* tx.run(`ALTER TABLE \`session\` ADD \`path\` text;`)
|
||||
})
|
||||
},
|
||||
} satisfies DatabaseMigration.Migration
|
||||
@@ -0,0 +1,12 @@
|
||||
import { Effect } from "effect"
|
||||
import type { DatabaseMigration } from "../migration"
|
||||
|
||||
export default {
|
||||
id: "20260501142318_next_venus",
|
||||
up(tx) {
|
||||
return Effect.gen(function* () {
|
||||
yield* tx.run(`ALTER TABLE \`session\` ADD \`agent\` text;`)
|
||||
yield* tx.run(`ALTER TABLE \`session\` ADD \`model\` text;`)
|
||||
})
|
||||
},
|
||||
} satisfies DatabaseMigration.Migration
|
||||
@@ -0,0 +1,11 @@
|
||||
import { Effect } from "effect"
|
||||
import type { DatabaseMigration } from "../migration"
|
||||
|
||||
export default {
|
||||
id: "20260504145000_add_sync_owner",
|
||||
up(tx) {
|
||||
return Effect.gen(function* () {
|
||||
yield* tx.run(`ALTER TABLE \`event_sequence\` ADD \`owner_id\` text;`)
|
||||
})
|
||||
},
|
||||
} satisfies DatabaseMigration.Migration
|
||||
@@ -0,0 +1,11 @@
|
||||
import { Effect } from "effect"
|
||||
import type { DatabaseMigration } from "../migration"
|
||||
|
||||
export default {
|
||||
id: "20260507164347_add_workspace_time",
|
||||
up(tx) {
|
||||
return Effect.gen(function* () {
|
||||
yield* tx.run(`ALTER TABLE \`workspace\` ADD \`time_used\` integer NOT NULL DEFAULT 0;`)
|
||||
})
|
||||
},
|
||||
} satisfies DatabaseMigration.Migration
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user