diff --git a/.opencode/command/ai-deps.md b/.opencode/command/ai-deps.md
new file mode 100644
index 00000000000..4d23c76a4d5
--- /dev/null
+++ b/.opencode/command/ai-deps.md
@@ -0,0 +1,24 @@
+---
+description: "Bump AI sdk dependencies minor / patch versions only"
+---
+
+Please read @package.json and @packages/opencode/package.json.
+
+Your job is to look into AI SDK dependencies, figure out if they have versions that can be upgraded (minor or patch versions ONLY no major ignore major changes).
+
+I want a report of every dependency and the version that can be upgraded to.
+What would be even better is if you can give me links to the changelog for each dependency, or at least some reference info so I can see what bugs were fixed or new features were added.
+
+Consider using subagents for each dep to save your context window.
+
+Here is a short list of some deps (please be comprehensive tho):
+
+- "ai"
+- "@ai-sdk/openai"
+- "@ai-sdk/anthropic"
+- "@openrouter/ai-sdk-provider"
+- etc, etc
+
+DO NOT upgrade the dependencies yet, just make a list of all dependencies and their versions that can be upgraded to minor or patch versions only.
+
+Write up your findings to ai-sdk-updates.md
diff --git a/SECURITY.md b/SECURITY.md
index 537834e4dec..3a653d01c6e 100644
--- a/SECURITY.md
+++ b/SECURITY.md
@@ -1,3 +1,32 @@
+# Security
+
+## Threat Model
+
+### Overview
+
+OpenCode is an AI-powered coding assistant that runs locally on your machine. It provides an agent system with access to powerful tools including shell execution, file operations, and web access.
+
+### No Sandbox
+
+OpenCode does **not** sandbox the agent. The permission system exists as a UX feature to help users stay aware of what actions the agent is taking - it prompts for confirmation before executing commands, writing files, etc. However, it is not designed to provide security isolation.
+
+If you need true isolation, run OpenCode inside a Docker container or VM.
+
+### Server Mode
+
+Server mode is opt-in only. When enabled, set `OPENCODE_SERVER_PASSWORD` to require HTTP Basic Auth. Without this, the server runs unauthenticated (with a warning). It is the end user's responsibility to secure the server - any functionality it provides is not a vulnerability.
+
+### Out of Scope
+
+| Category | Rationale |
+| ------------------------------- | ----------------------------------------------------------------------- |
+| **Server access when opted-in** | If you enable server mode, API access is expected behavior |
+| **Sandbox escapes** | The permission system is not a sandbox (see above) |
+| **LLM provider data handling** | Data sent to your configured LLM provider is governed by their policies |
+| **MCP server behavior** | External MCP servers you configure are outside our trust boundary |
+
+---
+
# Reporting Security Issues
We appreciate your efforts to responsibly disclose your findings, and will make every effort to acknowledge your contributions.
diff --git a/STATS.md b/STATS.md
index ac4b788bae0..b6e03b01b04 100644
--- a/STATS.md
+++ b/STATS.md
@@ -200,3 +200,4 @@
| 2026-01-11 | 2,836,394 (+204,371) | 1,530,479 (+26,809) | 4,366,873 (+231,180) |
| 2026-01-12 | 3,053,594 (+217,200) | 1,553,671 (+23,192) | 4,607,265 (+240,392) |
| 2026-01-13 | 3,297,078 (+243,484) | 1,595,062 (+41,391) | 4,892,140 (+284,875) |
+| 2026-01-14 | 3,568,928 (+271,850) | 1,645,362 (+50,300) | 5,214,290 (+322,150) |
diff --git a/bun.lock b/bun.lock
index bdaef9db3f0..6f61a3a7a8a 100644
--- a/bun.lock
+++ b/bun.lock
@@ -22,7 +22,7 @@
},
"packages/app": {
"name": "@opencode-ai/app",
- "version": "1.1.19",
+ "version": "1.1.20",
"dependencies": {
"@kobalte/core": "catalog:",
"@opencode-ai/sdk": "workspace:*",
@@ -70,7 +70,7 @@
},
"packages/console/app": {
"name": "@opencode-ai/console-app",
- "version": "1.1.19",
+ "version": "1.1.20",
"dependencies": {
"@cloudflare/vite-plugin": "1.15.2",
"@ibm/plex": "6.4.1",
@@ -84,10 +84,12 @@
"@solidjs/meta": "catalog:",
"@solidjs/router": "catalog:",
"@solidjs/start": "catalog:",
+ "@stripe/stripe-js": "8.6.1",
"chart.js": "4.5.1",
"nitro": "3.0.1-alpha.1",
"solid-js": "catalog:",
"solid-list": "0.3.0",
+ "solid-stripe": "0.8.1",
"vite": "catalog:",
"zod": "catalog:",
},
@@ -99,7 +101,7 @@
},
"packages/console/core": {
"name": "@opencode-ai/console-core",
- "version": "1.1.19",
+ "version": "1.1.20",
"dependencies": {
"@aws-sdk/client-sts": "3.782.0",
"@jsx-email/render": "1.1.1",
@@ -126,7 +128,7 @@
},
"packages/console/function": {
"name": "@opencode-ai/console-function",
- "version": "1.1.19",
+ "version": "1.1.20",
"dependencies": {
"@ai-sdk/anthropic": "2.0.0",
"@ai-sdk/openai": "2.0.2",
@@ -150,7 +152,7 @@
},
"packages/console/mail": {
"name": "@opencode-ai/console-mail",
- "version": "1.1.19",
+ "version": "1.1.20",
"dependencies": {
"@jsx-email/all": "2.2.3",
"@jsx-email/cli": "1.4.3",
@@ -174,7 +176,7 @@
},
"packages/desktop": {
"name": "@opencode-ai/desktop",
- "version": "1.1.19",
+ "version": "1.1.20",
"dependencies": {
"@opencode-ai/app": "workspace:*",
"@opencode-ai/ui": "workspace:*",
@@ -203,7 +205,7 @@
},
"packages/enterprise": {
"name": "@opencode-ai/enterprise",
- "version": "1.1.19",
+ "version": "1.1.20",
"dependencies": {
"@opencode-ai/ui": "workspace:*",
"@opencode-ai/util": "workspace:*",
@@ -232,7 +234,7 @@
},
"packages/function": {
"name": "@opencode-ai/function",
- "version": "1.1.19",
+ "version": "1.1.20",
"dependencies": {
"@octokit/auth-app": "8.0.1",
"@octokit/rest": "catalog:",
@@ -248,7 +250,7 @@
},
"packages/opencode": {
"name": "opencode",
- "version": "1.1.19",
+ "version": "1.1.20",
"bin": {
"opencode": "./bin/opencode",
},
@@ -276,7 +278,7 @@
"@ai-sdk/vercel": "1.0.31",
"@ai-sdk/xai": "2.0.51",
"@clack/prompts": "1.0.0-alpha.1",
- "@gitlab/gitlab-ai-provider": "3.1.0",
+ "@gitlab/gitlab-ai-provider": "3.1.1",
"@hono/standard-validator": "0.1.5",
"@hono/zod-validator": "catalog:",
"@modelcontextprotocol/sdk": "1.25.2",
@@ -352,7 +354,7 @@
},
"packages/plugin": {
"name": "@opencode-ai/plugin",
- "version": "1.1.19",
+ "version": "1.1.20",
"dependencies": {
"@opencode-ai/sdk": "workspace:*",
"zod": "catalog:",
@@ -372,7 +374,7 @@
},
"packages/sdk/js": {
"name": "@opencode-ai/sdk",
- "version": "1.1.19",
+ "version": "1.1.20",
"devDependencies": {
"@hey-api/openapi-ts": "0.88.1",
"@tsconfig/node22": "catalog:",
@@ -383,7 +385,7 @@
},
"packages/slack": {
"name": "@opencode-ai/slack",
- "version": "1.1.19",
+ "version": "1.1.20",
"dependencies": {
"@opencode-ai/sdk": "workspace:*",
"@slack/bolt": "^3.17.1",
@@ -396,7 +398,7 @@
},
"packages/ui": {
"name": "@opencode-ai/ui",
- "version": "1.1.19",
+ "version": "1.1.20",
"dependencies": {
"@kobalte/core": "catalog:",
"@opencode-ai/sdk": "workspace:*",
@@ -436,7 +438,7 @@
},
"packages/util": {
"name": "@opencode-ai/util",
- "version": "1.1.19",
+ "version": "1.1.20",
"dependencies": {
"zod": "catalog:",
},
@@ -447,7 +449,7 @@
},
"packages/web": {
"name": "@opencode-ai/web",
- "version": "1.1.19",
+ "version": "1.1.20",
"dependencies": {
"@astrojs/cloudflare": "12.6.3",
"@astrojs/markdown-remark": "6.3.1",
@@ -503,7 +505,7 @@
"@tailwindcss/vite": "4.1.11",
"@tsconfig/bun": "1.0.9",
"@tsconfig/node22": "22.0.2",
- "@types/bun": "1.3.4",
+ "@types/bun": "1.3.6",
"@types/luxon": "3.7.1",
"@types/node": "22.13.9",
"@typescript/native-preview": "7.0.0-dev.20251207.1",
@@ -911,7 +913,7 @@
"@fontsource/inter": ["@fontsource/inter@5.2.8", "", {}, "sha512-P6r5WnJoKiNVV+zvW2xM13gNdFhAEpQ9dQJHt3naLvfg+LkF2ldgSLiF4T41lf1SQCM9QmkqPTn4TH568IRagg=="],
- "@gitlab/gitlab-ai-provider": ["@gitlab/gitlab-ai-provider@3.1.0", "", { "dependencies": { "@anthropic-ai/sdk": "^0.71.0", "@anycable/core": "^0.9.2", "graphql-request": "^6.1.0", "isomorphic-ws": "^5.0.0", "socket.io-client": "^4.8.1", "vscode-jsonrpc": "^8.2.1", "zod": "^3.25.76" }, "peerDependencies": { "@ai-sdk/provider": ">=2.0.0", "@ai-sdk/provider-utils": ">=3.0.0" } }, "sha512-S0MVXsogrwbOboA/8L0CY5sBXg2HrrO8gdeUeHd9yLZDPsggFD0FzcSuzO5vBO6geUOpruRa8Hqrbb6WWu7Frw=="],
+ "@gitlab/gitlab-ai-provider": ["@gitlab/gitlab-ai-provider@3.1.1", "", { "dependencies": { "@anthropic-ai/sdk": "^0.71.0", "@anycable/core": "^0.9.2", "graphql-request": "^6.1.0", "isomorphic-ws": "^5.0.0", "socket.io-client": "^4.8.1", "vscode-jsonrpc": "^8.2.1", "zod": "^3.25.76" }, "peerDependencies": { "@ai-sdk/provider": ">=2.0.0", "@ai-sdk/provider-utils": ">=3.0.0" } }, "sha512-7AtFrCflq2NzC99bj7YaqbQDCZyaScM1+L4ujllV5syiRTFE239Uhnd/yEkPXa7sUAnNRfN3CWusCkQ2zK/q9g=="],
"@graphql-typed-document-node/core": ["@graphql-typed-document-node/core@3.2.0", "", { "peerDependencies": { "graphql": "^0.8.0 || ^0.9.0 || ^0.10.0 || ^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0" } }, "sha512-mB9oAsNCm9aM3/SOv4YtBMqZbYj10R7dkq8byBqxGY/ncFwhf2oQzMV+LCRlWoDSEBJ3COiR1yeDvMtsoOsuFQ=="],
@@ -1663,6 +1665,8 @@
"@standard-schema/spec": ["@standard-schema/spec@1.0.0", "", {}, "sha512-m2bOd0f2RT9k8QJx1JN85cZYyH1RqFBdlwtkSlf4tBDYLCiiZnv1fIIwacK6cqwXavOydf0NPToMQgpKq+dVlA=="],
+ "@stripe/stripe-js": ["@stripe/stripe-js@8.6.1", "", {}, "sha512-UJ05U2062XDgydbUcETH1AoRQLNhigQ2KmDn1BG8sC3xfzu6JKg95Qt6YozdzFpxl1Npii/02m2LEWFt1RYjVA=="],
+
"@swc/helpers": ["@swc/helpers@0.5.17", "", { "dependencies": { "tslib": "^2.8.0" } }, "sha512-5IKx/Y13RsYd+sauPb2x+U/xZikHjolzfuDgTAl/Tdf3Q8rslRvC19NKDLgAJQ6wsqADk10ntlv08nPFw/gO/A=="],
"@tailwindcss/node": ["@tailwindcss/node@4.1.11", "", { "dependencies": { "@ampproject/remapping": "^2.3.0", "enhanced-resolve": "^5.18.1", "jiti": "^2.4.2", "lightningcss": "1.30.1", "magic-string": "^0.30.17", "source-map-js": "^1.2.1", "tailwindcss": "4.1.11" } }, "sha512-yzhzuGRmv5QyU9qLNg4GTlYI6STedBWRE7NjxP45CsFYYq9taI0zJXZBMqIC/c8fViNLhmrbpSFS57EoxUmD6Q=="],
@@ -1769,7 +1773,7 @@
"@types/braces": ["@types/braces@3.0.5", "", {}, "sha512-SQFof9H+LXeWNz8wDe7oN5zu7ket0qwMu5vZubW4GCJ8Kkeh6nBWUz87+KTz/G3Kqsrp0j/W253XJb3KMEeg3w=="],
- "@types/bun": ["@types/bun@1.3.4", "", { "dependencies": { "bun-types": "1.3.4" } }, "sha512-EEPTKXHP+zKGPkhRLv+HI0UEX8/o+65hqARxLy8Ov5rIxMBPNTjeZww00CIihrIQGEQBYg+0roO5qOnS/7boGA=="],
+ "@types/bun": ["@types/bun@1.3.6", "", { "dependencies": { "bun-types": "1.3.6" } }, "sha512-uWCv6FO/8LcpREhenN1d1b6fcspAB+cefwD7uti8C8VffIv0Um08TKMn98FynpTiU38+y2dUO55T11NgDt8VAA=="],
"@types/chai": ["@types/chai@5.2.3", "", { "dependencies": { "@types/deep-eql": "*", "assertion-error": "^2.0.1" } }, "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA=="],
@@ -2071,7 +2075,7 @@
"bun-pty": ["bun-pty@0.4.4", "", {}, "sha512-WK4G6uWsZgu1v4hKIlw6G1q2AOf8Rbga2Yr7RnxArVjjyb+mtVa/CFc9GOJf+OYSJSH8k7LonAtQOVeNAddRyg=="],
- "bun-types": ["bun-types@1.3.4", "", { "dependencies": { "@types/node": "*" } }, "sha512-5ua817+BZPZOlNaRgGBpZJOSAQ9RQ17pkwPD0yR7CfJg+r8DgIILByFifDTa+IPDDxzf5VNhtNlcKqFzDgJvlQ=="],
+ "bun-types": ["bun-types@1.3.6", "", { "dependencies": { "@types/node": "*" } }, "sha512-OlFwHcnNV99r//9v5IIOgQ9Uk37gZqrNMCcqEaExdkVq3Avwqok1bJFmvGMCkCE0FqzdY8VMOZpfpR3lwI+CsQ=="],
"bun-webgpu": ["bun-webgpu@0.1.4", "", { "dependencies": { "@webgpu/types": "^0.1.60" }, "optionalDependencies": { "bun-webgpu-darwin-arm64": "^0.1.4", "bun-webgpu-darwin-x64": "^0.1.4", "bun-webgpu-linux-x64": "^0.1.4", "bun-webgpu-win32-x64": "^0.1.4" } }, "sha512-Kw+HoXl1PMWJTh9wvh63SSRofTA8vYBFCw0XEP1V1fFdQEDhI8Sgf73sdndE/oDpN/7CMx0Yv/q8FCvO39ROMQ=="],
@@ -3557,6 +3561,8 @@
"solid-refresh": ["solid-refresh@0.6.3", "", { "dependencies": { "@babel/generator": "^7.23.6", "@babel/helper-module-imports": "^7.22.15", "@babel/types": "^7.23.6" }, "peerDependencies": { "solid-js": "^1.3" } }, "sha512-F3aPsX6hVw9ttm5LYlth8Q15x6MlI/J3Dn+o3EQyRTtTxidepSTwAYdozt01/YA+7ObcciagGEyXIopGZzQtbA=="],
+ "solid-stripe": ["solid-stripe@0.8.1", "", { "peerDependencies": { "@stripe/stripe-js": ">=1.44.1 <8.0.0", "solid-js": "^1.6.0" } }, "sha512-l2SkWoe51rsvk9u1ILBRWyCHODZebChSGMR6zHYJTivTRC0XWrRnNNKs5x1PYXsaIU71KYI6ov5CZB5cOtGLWw=="],
+
"solid-use": ["solid-use@0.9.1", "", { "peerDependencies": { "solid-js": "^1.7" } }, "sha512-UwvXDVPlrrbj/9ewG9ys5uL2IO4jSiwys2KPzK4zsnAcmEl7iDafZWW1Mo4BSEWOmQCGK6IvpmGHo1aou8iOFw=="],
"source-map": ["source-map@0.7.6", "", {}, "sha512-i5uvt8C3ikiWeNZSVZNWcfZPItFQOsYTUAOkcUPGd8DqDy1uOUikjt5dG+uRlwyvR108Fb9DOd4GvXfT0N2/uQ=="],
diff --git a/flake.lock b/flake.lock
index 3e4611cf5e9..5ef276f0a08 100644
--- a/flake.lock
+++ b/flake.lock
@@ -2,11 +2,11 @@
"nodes": {
"nixpkgs": {
"locked": {
- "lastModified": 1768178648,
- "narHash": "sha256-kz/F6mhESPvU1diB7tOM3nLcBfQe7GU7GQCymRlTi/s=",
+ "lastModified": 1768302833,
+ "narHash": "sha256-h5bRFy9bco+8QcK7rGoOiqMxMbmn21moTACofNLRMP4=",
"owner": "NixOS",
"repo": "nixpkgs",
- "rev": "3fbab70c6e69c87ea2b6e48aa6629da2aa6a23b0",
+ "rev": "61db79b0c6b838d9894923920b612048e1201926",
"type": "github"
},
"original": {
diff --git a/infra/console.ts b/infra/console.ts
index 1368ef202aa..17e4deab6e2 100644
--- a/infra/console.ts
+++ b/infra/console.ts
@@ -122,6 +122,7 @@ const ZEN_MODELS = [
]
const ZEN_BLACK = new sst.Secret("ZEN_BLACK")
const STRIPE_SECRET_KEY = new sst.Secret("STRIPE_SECRET_KEY")
+const STRIPE_PUBLISHABLE_KEY = new sst.Secret("STRIPE_PUBLISHABLE_KEY")
const AUTH_API_URL = new sst.Linkable("AUTH_API_URL", {
properties: { value: auth.url.apply((url) => url!) },
})
@@ -177,6 +178,7 @@ new sst.cloudflare.x.SolidStart("Console", {
//VITE_DOCS_URL: web.url.apply((url) => url!),
//VITE_API_URL: gateway.url.apply((url) => url!),
VITE_AUTH_URL: auth.url.apply((url) => url!),
+ VITE_STRIPE_PUBLISHABLE_KEY: STRIPE_PUBLISHABLE_KEY.value,
},
transform: {
server: {
diff --git a/install b/install
index 757694481c4..22b7ca39ed7 100755
--- a/install
+++ b/install
@@ -369,7 +369,7 @@ case $current_shell in
config_files="$HOME/.config/fish/config.fish"
;;
zsh)
- config_files="$HOME/.zshrc $HOME/.zshenv $XDG_CONFIG_HOME/zsh/.zshrc $XDG_CONFIG_HOME/zsh/.zshenv"
+ config_files="${ZDOTDIR:-$HOME}/.zshrc ${ZDOTDIR:-$HOME}/.zshenv $XDG_CONFIG_HOME/zsh/.zshrc $XDG_CONFIG_HOME/zsh/.zshenv"
;;
bash)
config_files="$HOME/.bashrc $HOME/.bash_profile $HOME/.profile $XDG_CONFIG_HOME/bash/.bashrc $XDG_CONFIG_HOME/bash/.bash_profile"
diff --git a/nix/hashes.json b/nix/hashes.json
index a25b9376e5d..2792318be15 100644
--- a/nix/hashes.json
+++ b/nix/hashes.json
@@ -1,6 +1,6 @@
{
"nodeModules": {
- "x86_64-linux": "sha256-x6A/XT1i3bjakfAj0A1wV4n2s9rpflMDceTeppdP6tE=",
- "aarch64-darwin": "sha256-RkamQYbpjJqpHHf76em9lPgeI9k4/kaCf7T+4xHaizY="
+ "x86_64-linux": "sha256-GKdu7nan/9ioBtgL3cUeuVLNKUDio10LeQrn7BPgbng=",
+ "aarch64-darwin": "sha256-STLB1J65VjauvPM+BqCyTQQkHPoVmUhDvVEdH3WTJP4="
}
}
diff --git a/package.json b/package.json
index d134a187a7c..9aa069d52cc 100644
--- a/package.json
+++ b/package.json
@@ -4,7 +4,7 @@
"description": "AI-powered development tool",
"private": true,
"type": "module",
- "packageManager": "bun@1.3.5",
+ "packageManager": "bun@1.3.6",
"scripts": {
"dev": "bun run --cwd packages/opencode --conditions=browser src/index.ts",
"typecheck": "bun turbo typecheck",
@@ -21,7 +21,7 @@
"packages/slack"
],
"catalog": {
- "@types/bun": "1.3.4",
+ "@types/bun": "1.3.6",
"@octokit/rest": "22.0.0",
"@hono/zod-validator": "0.4.2",
"ulid": "3.0.1",
diff --git a/packages/app/package.json b/packages/app/package.json
index 8a128158606..168602cda02 100644
--- a/packages/app/package.json
+++ b/packages/app/package.json
@@ -1,6 +1,6 @@
{
"name": "@opencode-ai/app",
- "version": "1.1.19",
+ "version": "1.1.20",
"description": "",
"type": "module",
"exports": {
diff --git a/packages/app/src/context/global-sync.tsx b/packages/app/src/context/global-sync.tsx
index c11edd292d1..ea0b90d5de7 100644
--- a/packages/app/src/context/global-sync.tsx
+++ b/packages/app/src/context/global-sync.tsx
@@ -38,6 +38,7 @@ type State = {
config: Config
path: Path
session: Session[]
+ sessionTotal: number
session_status: {
[sessionID: string]: SessionStatus
}
@@ -98,6 +99,7 @@ function createGlobalSync() {
agent: [],
command: [],
session: [],
+ sessionTotal: 0,
session_status: {},
session_diff: {},
todo: {},
@@ -117,8 +119,10 @@ function createGlobalSync() {
async function loadSessions(directory: string) {
const [store, setStore] = child(directory)
- globalSDK.client.session
- .list({ directory })
+ const limit = store.limit
+
+ return globalSDK.client.session
+ .list({ directory, roots: true })
.then((x) => {
const fourHoursAgo = Date.now() - 4 * 60 * 60 * 1000
const nonArchived = (x.data ?? [])
@@ -128,10 +132,12 @@ function createGlobalSync() {
.sort((a, b) => a.id.localeCompare(b.id))
// Include up to the limit, plus any updated in the last 4 hours
const sessions = nonArchived.filter((s, i) => {
- if (i < store.limit) return true
+ if (i < limit) return true
const updated = new Date(s.time?.updated ?? s.time?.created).getTime()
return updated > fourHoursAgo
})
+ // Store total session count (used for "load more" pagination)
+ setStore("sessionTotal", nonArchived.length)
setStore("session", reconcile(sessions, { key: "id" }))
})
.catch((err) => {
diff --git a/packages/app/src/pages/layout.tsx b/packages/app/src/pages/layout.tsx
index cffefd5634d..39f397ac466 100644
--- a/packages/app/src/pages/layout.tsx
+++ b/packages/app/src/pages/layout.tsx
@@ -944,7 +944,7 @@ export default function Layout(props: ParentProps) {
.toSorted(sortSessions),
)
const rootSessions = createMemo(() => sessions().filter((s) => !s.parentID))
- const hasMoreSessions = createMemo(() => store.session.length >= store.limit)
+ const hasMoreSessions = createMemo(() => store.sessionTotal > store.session.length)
const loadMoreSessions = async () => {
setProjectStore("limit", (limit) => limit + 5)
await globalSync.project.loadSessions(props.project.worktree)
diff --git a/packages/console/app/package.json b/packages/console/app/package.json
index dff2adef7ac..918277eaff2 100644
--- a/packages/console/app/package.json
+++ b/packages/console/app/package.json
@@ -1,12 +1,12 @@
{
"name": "@opencode-ai/console-app",
- "version": "1.1.19",
+ "version": "1.1.20",
"type": "module",
"license": "MIT",
"scripts": {
"typecheck": "tsgo --noEmit",
"dev": "vite dev --host 0.0.0.0",
- "dev:remote": "VITE_AUTH_URL=https://auth.dev.opencode.ai bun sst shell --stage=dev bun dev",
+ "dev:remote": "VITE_AUTH_URL=https://auth.dev.opencode.ai VITE_STRIPE_PUBLISHABLE_KEY=pk_test_51RtuLNE7fOCwHSD4mewwzFejyytjdGoSDK7CAvhbffwaZnPbNb2rwJICw6LTOXCmWO320fSNXvb5NzI08RZVkAxd00syfqrW7t bun sst shell --stage=dev bun dev",
"build": "./script/generate-sitemap.ts && vite build && ../../opencode/script/schema.ts ./.output/public/config.json",
"start": "vite start"
},
@@ -23,10 +23,12 @@
"@solidjs/meta": "catalog:",
"@solidjs/router": "catalog:",
"@solidjs/start": "catalog:",
+ "@stripe/stripe-js": "8.6.1",
"chart.js": "4.5.1",
"nitro": "3.0.1-alpha.1",
"solid-js": "catalog:",
"solid-list": "0.3.0",
+ "solid-stripe": "0.8.1",
"vite": "catalog:",
"zod": "catalog:"
},
diff --git a/packages/console/app/public/social-share-black.png b/packages/console/app/public/social-share-black.png
new file mode 120000
index 00000000000..5baa00483b5
--- /dev/null
+++ b/packages/console/app/public/social-share-black.png
@@ -0,0 +1 @@
+../../../ui/src/assets/images/social-share-black.png
\ No newline at end of file
diff --git a/packages/console/app/src/component/footer.tsx b/packages/console/app/src/component/footer.tsx
index 5eac75967ac..27f8ddd65f1 100644
--- a/packages/console/app/src/component/footer.tsx
+++ b/packages/console/app/src/component/footer.tsx
@@ -24,6 +24,9 @@ export function Footer() {
+
diff --git a/packages/console/app/src/routes/auth/callback.ts b/packages/console/app/src/routes/auth/[...callback].ts
similarity index 91%
rename from packages/console/app/src/routes/auth/callback.ts
rename to packages/console/app/src/routes/auth/[...callback].ts
index 9b7296791d4..36a9c5194d0 100644
--- a/packages/console/app/src/routes/auth/callback.ts
+++ b/packages/console/app/src/routes/auth/[...callback].ts
@@ -5,6 +5,7 @@ import { useAuthSession } from "~/context/auth"
export async function GET(input: APIEvent) {
const url = new URL(input.request.url)
+
try {
const code = url.searchParams.get("code")
if (!code) throw new Error("No code found")
@@ -27,7 +28,7 @@ export async function GET(input: APIEvent) {
current: id,
}
})
- return redirect("/auth")
+ return redirect(url.pathname === "/auth/callback" ? "/auth" : url.pathname.replace("/auth/callback", ""))
} catch (e: any) {
return new Response(
JSON.stringify({
diff --git a/packages/console/app/src/routes/auth/authorize.ts b/packages/console/app/src/routes/auth/authorize.ts
index 166466ef859..0f0651ae36b 100644
--- a/packages/console/app/src/routes/auth/authorize.ts
+++ b/packages/console/app/src/routes/auth/authorize.ts
@@ -2,6 +2,9 @@ import type { APIEvent } from "@solidjs/start/server"
import { AuthClient } from "~/context/auth"
export async function GET(input: APIEvent) {
- const result = await AuthClient.authorize(new URL("./callback", input.request.url).toString(), "code")
+ const url = new URL(input.request.url)
+ const cont = url.searchParams.get("continue") ?? ""
+ const callbackUrl = new URL(`./callback${cont}`, input.request.url)
+ const result = await AuthClient.authorize(callbackUrl.toString(), "code")
return Response.redirect(result.url, 302)
}
diff --git a/packages/console/app/src/routes/black.css b/packages/console/app/src/routes/black.css
new file mode 100644
index 00000000000..99353f27332
--- /dev/null
+++ b/packages/console/app/src/routes/black.css
@@ -0,0 +1,805 @@
+[data-page="black"] {
+ background: #000;
+ min-height: 100vh;
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ justify-content: stretch;
+ font-family: var(--font-mono);
+ color: #fff;
+
+ [data-component="header-gradient"] {
+ position: absolute;
+ top: 0;
+ left: 0;
+ width: 100%;
+ height: 288px;
+ background: linear-gradient(180deg, rgba(255, 255, 255, 0.1) 0%, rgba(0, 0, 0, 0) 100%);
+ }
+
+ [data-component="header"] {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ justify-content: center;
+ padding-top: 40px;
+ flex-shrink: 0;
+ }
+
+ [data-component="content"] {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ width: 100%;
+ flex-grow: 1;
+
+ [data-slot="hero"] {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ text-align: center;
+ gap: 8px;
+ margin-top: 40px;
+ padding: 0 20px;
+
+ @media (min-width: 768px) {
+ margin-top: 60px;
+ }
+
+ h1 {
+ color: rgba(255, 255, 255, 0.92);
+ font-size: 18px;
+ font-style: normal;
+ font-weight: 400;
+ line-height: 160%;
+ margin: 0;
+
+ @media (min-width: 768px) {
+ font-size: 22px;
+ }
+ }
+
+ p {
+ color: rgba(255, 255, 255, 0.59);
+ font-size: 18px;
+ font-style: normal;
+ font-weight: 400;
+ line-height: 160%;
+ margin: 0;
+
+ @media (min-width: 768px) {
+ font-size: 22px;
+ }
+ }
+ }
+
+ [data-slot="hero-black"] {
+ margin-top: 40px;
+ padding: 0 20px;
+
+ @media (min-width: 768px) {
+ margin-top: 60px;
+ }
+
+ svg {
+ --hero-black-fill-from: hsl(0 0% 100%);
+ --hero-black-fill-to: hsl(0 0% 100% / 0%);
+ --hero-black-stroke-from: hsl(0 0% 100% / 60%);
+ --hero-black-stroke-to: hsl(0 0% 100% / 0%);
+
+ width: 100%;
+ max-width: 590px;
+ height: auto;
+ filter: drop-shadow(0 0 20px rgba(255, 255, 255, 0.1));
+ mask-image: linear-gradient(to bottom, black, transparent);
+ stroke-width: 1.5;
+
+ [data-slot="black-fill"] {
+ fill: url(#hero-black-fill-gradient);
+ }
+
+ [data-slot="black-stroke"] {
+ fill: url(#hero-black-stroke-gradient);
+ }
+ }
+ }
+
+ [data-slot="cta"] {
+ display: flex;
+ flex-direction: column;
+ gap: 32px;
+ align-items: center;
+ text-align: center;
+ margin-top: -32px;
+ width: 100%;
+
+ @media (min-width: 768px) {
+ margin-top: -16px;
+ }
+
+ [data-slot="heading"] {
+ color: rgba(255, 255, 255, 0.92);
+ text-align: center;
+ font-size: 18px;
+ font-style: normal;
+ font-weight: 400;
+ line-height: 160%;
+
+ span {
+ display: inline-block;
+ }
+ }
+
+ [data-slot="subheading"] {
+ color: rgba(255, 255, 255, 0.59);
+ font-size: 15px;
+ font-style: normal;
+ font-weight: 400;
+ line-height: 160%;
+
+ @media (min-width: 768px) {
+ font-size: 18px;
+ line-height: 160%;
+ }
+ }
+
+ [data-slot="button"] {
+ display: inline-flex;
+ height: 40px;
+ padding: 0 12px;
+ justify-content: center;
+ align-items: center;
+ gap: 8px;
+ border-radius: 4px;
+ background: rgba(255, 255, 255, 0.92);
+ text-decoration: none;
+ color: #000;
+ font-family: var(--font-mono);
+ font-size: 16px;
+ font-style: normal;
+ font-weight: 500;
+ line-height: normal;
+
+ &:hover {
+ background: #e0e0e0;
+ }
+
+ &:active {
+ transform: scale(0.98);
+ }
+ }
+
+ [data-slot="back-soon"] {
+ color: rgba(255, 255, 255, 0.59);
+ text-align: center;
+ font-size: 13px;
+ font-style: normal;
+ font-weight: 400;
+ line-height: 160%;
+ }
+
+ [data-slot="follow-us"] {
+ display: inline-flex;
+ height: 40px;
+ padding: 0 12px;
+ justify-content: center;
+ align-items: center;
+ gap: 8px;
+ border-radius: 4px;
+ border: 1px solid rgba(255, 255, 255, 0.17);
+ color: rgba(255, 255, 255, 0.59);
+ font-family: var(--font-mono);
+ font-size: 14px;
+ font-style: normal;
+ font-weight: 400;
+ line-height: normal;
+ text-decoration: none;
+ }
+
+ [data-slot="pricing"] {
+ display: flex;
+ flex-direction: column;
+ gap: 16px;
+ width: 100%;
+ max-width: 680px;
+ padding: 0 20px;
+ box-sizing: border-box;
+ }
+
+ [data-slot="pricing-card"] {
+ display: flex;
+ flex-direction: column;
+ align-items: flex-start;
+ border: 1px solid rgba(255, 255, 255, 0.17);
+ border-radius: 5px;
+ text-decoration: none;
+ background: #000;
+ text-align: left;
+ overflow: hidden;
+ width: 100%;
+ transition: border-color 200ms ease;
+
+ &:hover:not([data-selected="true"]) {
+ border-color: rgba(255, 255, 255, 0.35);
+ }
+
+ [data-slot="card-trigger"] {
+ display: flex;
+ flex-direction: column;
+ align-items: flex-start;
+ width: 100%;
+ padding: 24px;
+ background: transparent;
+ border: none;
+ cursor: pointer;
+ font-family: inherit;
+ text-align: left;
+ transition: padding 200ms ease;
+
+ &:disabled {
+ cursor: default;
+ }
+ }
+
+ &[data-selected="true"] {
+ [data-slot="amount"] {
+ font-size: 22px;
+ }
+
+ [data-slot="terms"] {
+ animation: reveal 500ms cubic-bezier(0.25, 0, 0.5, 1) forwards;
+ }
+
+ [data-slot="actions"] {
+ [data-slot="continue"] {
+ animation-delay: 200ms;
+ }
+ }
+ }
+
+ &[data-collapsed="true"] {
+ [data-slot="card-trigger"] {
+ padding: 20px 24px;
+ }
+
+ [data-slot="plan-header"] {
+ flex-direction: row;
+ }
+
+ [data-slot="amount"] {
+ font-size: 20px;
+ }
+ }
+
+ &[data-selected="false"][data-collapsed="false"] {
+ [data-slot="amount"] {
+ font-size: 22px;
+ }
+
+ [data-slot="period"],
+ [data-slot="multiplier"] {
+ font-size: 14px;
+ }
+ }
+
+ [data-slot="plan-header"] {
+ display: flex;
+ flex-direction: column;
+ width: 100%;
+ gap: 12px;
+ transition: gap 200ms ease;
+ }
+
+ [data-slot="plan-icon"] {
+ color: rgba(255, 255, 255, 0.59);
+ flex-shrink: 0;
+ }
+
+ [data-slot="price"] {
+ display: flex;
+ flex-wrap: wrap;
+ align-items: baseline;
+ gap: 8px;
+ line-height: 24px;
+ margin: 0;
+ }
+
+ [data-slot="amount"] {
+ color: rgba(255, 255, 255, 0.92);
+ font-weight: 500;
+ }
+
+ [data-slot="content"] {
+ width: 100%;
+ }
+
+ [data-slot="period"],
+ [data-slot="multiplier"] {
+ color: rgba(255, 255, 255, 0.59);
+ }
+
+ [data-slot="billing"] {
+ color: rgba(255, 255, 255, 0.59);
+ font-size: 14px;
+ }
+
+ [data-slot="multiplier"] {
+ color: rgba(255, 255, 255, 0.39);
+
+ &::before {
+ content: "·";
+ margin-right: 8px;
+ }
+ }
+
+ [data-slot="terms"] {
+ list-style: none;
+ padding: 0 24px 24px 24px;
+ margin: 0;
+ display: flex;
+ flex-direction: column;
+ gap: 12px;
+ text-align: left;
+ width: 100%;
+ opacity: 0;
+ mask-image: linear-gradient(to bottom, black 0%, black 50%, transparent 100%);
+ mask-repeat: no-repeat;
+ mask-size: 100% 200%;
+ mask-position: 0% 320%;
+ }
+
+ [data-slot="terms"] li {
+ color: rgba(255, 255, 255, 0.59);
+ font-size: 13px;
+ line-height: 1.2;
+ padding-left: 16px;
+ position: relative;
+
+ &::before {
+ content: "▪";
+ position: absolute;
+ left: 0;
+ color: rgba(255, 255, 255, 0.39);
+ }
+ }
+
+ [data-slot="actions"] {
+ display: flex;
+ gap: 16px;
+ margin-top: 8px;
+ padding: 0 24px 24px 24px;
+ box-sizing: border-box;
+ width: 100%;
+ }
+
+ [data-slot="actions"] button,
+ [data-slot="actions"] a {
+ flex: 1;
+ display: inline-flex;
+ height: 48px;
+ padding: 0 16px;
+ justify-content: center;
+ align-items: center;
+ border-radius: 4px;
+ font-family: var(--font-mono);
+ font-size: 16px;
+ font-weight: 400;
+ text-decoration: none;
+ cursor: pointer;
+ transition-property: background-color, border-color;
+ transition-duration: 200ms;
+ transition-timing-function: cubic-bezier(0.25, 0, 0.5, 1);
+ }
+
+ [data-slot="cancel"] {
+ border: 1px solid var(--border-base, rgba(255, 255, 255, 0.17));
+ background: var(--surface-raised-base, rgba(255, 255, 255, 0.06));
+ background-clip: border-box;
+ color: rgba(255, 255, 255, 0.92);
+
+ &:hover {
+ background: var(--surface-raised-base, rgba(255, 255, 255, 0.08));
+ border-color: rgba(255, 255, 255, 0.25);
+ }
+ }
+
+ [data-slot="continue"] {
+ background: rgb(255, 255, 255);
+ color: rgb(0, 0, 0);
+
+ &:hover {
+ background: rgb(255, 255, 255, 0.9);
+ }
+ }
+ }
+
+ [data-slot="fine-print"] {
+ color: rgba(255, 255, 255, 0.39);
+ text-align: center;
+ font-size: 13px;
+ font-style: normal;
+ font-weight: 400;
+ line-height: 160%;
+
+ a {
+ color: rgba(255, 255, 255, 0.39);
+ text-decoration: underline;
+ }
+ }
+ }
+
+ /* Subscribe page styles */
+ [data-slot="subscribe-form"] {
+ display: flex;
+ flex-direction: column;
+ gap: 32px;
+ align-items: center;
+ margin-top: -18px;
+ width: 100%;
+ max-width: 540px;
+ padding: 0 20px;
+
+ @media (min-width: 768px) {
+ margin-top: 40px;
+ padding: 0;
+ }
+
+ [data-slot="form-card"] {
+ width: 100%;
+ border: 1px solid rgba(255, 255, 255, 0.17);
+ border-radius: 4px;
+ padding: 24px;
+ display: flex;
+ flex-direction: column;
+ gap: 20px;
+ }
+
+ [data-slot="plan-header"] {
+ display: flex;
+ flex-direction: column;
+ gap: 8px;
+ }
+
+ [data-slot="title"] {
+ color: rgba(255, 255, 255, 0.92);
+ font-size: 16px;
+ font-weight: 400;
+ margin-bottom: 8px;
+ }
+
+ [data-slot="icon"] {
+ color: rgba(255, 255, 255, 0.59);
+ }
+
+ [data-slot="price"] {
+ display: flex;
+ flex-wrap: wrap;
+ align-items: baseline;
+ gap: 8px;
+ }
+
+ [data-slot="amount"] {
+ color: rgba(255, 255, 255, 0.92);
+ font-size: 24px;
+ font-weight: 500;
+ }
+
+ [data-slot="period"] {
+ color: rgba(255, 255, 255, 0.59);
+ font-size: 14px;
+ }
+
+ [data-slot="multiplier"] {
+ color: rgba(255, 255, 255, 0.39);
+ font-size: 13px;
+
+ &::before {
+ content: "·";
+ margin: 0 8px;
+ }
+ }
+
+ [data-slot="divider"] {
+ height: 1px;
+ background: rgba(255, 255, 255, 0.17);
+ }
+
+ [data-slot="section-title"] {
+ color: rgba(255, 255, 255, 0.92);
+ font-size: 16px;
+ font-weight: 400;
+ }
+
+ [data-slot="tax-id-section"] {
+ display: flex;
+ flex-direction: column;
+ gap: 8px;
+
+ [data-slot="label"] {
+ color: rgba(255, 255, 255, 0.59);
+ font-size: 14px;
+ }
+
+ [data-slot="input"] {
+ width: 100%;
+ height: 44px;
+ padding: 0 12px;
+ background: #1a1a1a;
+ border: 1px solid rgba(255, 255, 255, 0.17);
+ border-radius: 4px;
+ color: #ffffff;
+ font-family: var(--font-mono);
+ font-size: 14px;
+ outline: none;
+ transition: border-color 0.15s ease;
+
+ &::placeholder {
+ color: rgba(255, 255, 255, 0.39);
+ }
+
+ &:focus {
+ border-color: rgba(255, 255, 255, 0.35);
+ }
+ }
+ }
+
+ [data-slot="checkout-form"] {
+ display: flex;
+ flex-direction: column;
+ gap: 20px;
+ }
+
+ [data-slot="error"] {
+ color: #ff6b6b;
+ font-size: 14px;
+ }
+
+ [data-slot="submit-button"] {
+ width: 100%;
+ height: 48px;
+ background: rgba(255, 255, 255, 0.92);
+ border: none;
+ border-radius: 4px;
+ color: #000;
+ font-family: var(--font-mono);
+ font-size: 16px;
+ font-weight: 500;
+ cursor: pointer;
+ transition: background 0.15s ease;
+
+ &:hover:not(:disabled) {
+ background: #e0e0e0;
+ }
+
+ &:disabled {
+ opacity: 0.5;
+ cursor: not-allowed;
+ }
+ }
+
+ [data-slot="charge-notice"] {
+ color: #d4a500;
+ font-size: 14px;
+ text-align: center;
+ }
+
+ [data-slot="success"] {
+ display: flex;
+ flex-direction: column;
+ gap: 24px;
+
+ [data-slot="title"] {
+ color: rgba(255, 255, 255, 0.92);
+ font-size: 18px;
+ font-weight: 400;
+ margin: 0;
+ }
+
+ [data-slot="details"] {
+ display: flex;
+ flex-direction: column;
+ gap: 16px;
+
+ > div {
+ display: flex;
+ justify-content: space-between;
+ align-items: baseline;
+ gap: 16px;
+ }
+
+ dt {
+ color: rgba(255, 255, 255, 0.59);
+ font-size: 14px;
+ font-weight: 400;
+ }
+
+ dd {
+ color: rgba(255, 255, 255, 0.92);
+ font-size: 14px;
+ font-weight: 400;
+ margin: 0;
+ text-align: right;
+ }
+ }
+
+ [data-slot="charge-notice"] {
+ color: #d4a500;
+ font-size: 14px;
+ text-align: left;
+ }
+ }
+
+ [data-slot="loading"] {
+ display: flex;
+ justify-content: center;
+ padding: 40px 0;
+
+ p {
+ color: rgba(255, 255, 255, 0.59);
+ font-size: 14px;
+ }
+ }
+
+ [data-slot="fine-print"] {
+ color: rgba(255, 255, 255, 0.39);
+ text-align: center;
+ font-size: 13px;
+ font-style: italic;
+
+ a {
+ color: rgba(255, 255, 255, 0.39);
+ text-decoration: underline;
+ }
+ }
+
+ [data-slot="workspace-picker"] {
+ [data-slot="workspace-list"] {
+ width: 100%;
+ padding: 0;
+ margin: 0;
+ list-style: none;
+ display: flex;
+ flex-direction: column;
+ align-items: flex-start;
+ gap: 8px;
+ align-self: stretch;
+ outline: none;
+ overflow-y: auto;
+ max-height: 240px;
+ scrollbar-width: none;
+
+ &::-webkit-scrollbar {
+ display: none;
+ }
+
+ [data-slot="workspace-item"] {
+ width: 100%;
+ display: flex;
+ padding: 8px 12px;
+ align-items: center;
+ gap: 8px;
+ align-self: stretch;
+ cursor: pointer;
+
+ [data-slot="selected-icon"] {
+ visibility: hidden;
+ color: rgba(255, 255, 255, 0.39);
+ font-family: "IBM Plex Mono", monospace;
+ font-size: 16px;
+ font-style: normal;
+ font-weight: 400;
+ line-height: 160%;
+ }
+
+ span:last-child {
+ color: rgba(255, 255, 255, 0.92);
+ font-size: 16px;
+ font-style: normal;
+ font-weight: 400;
+ line-height: 160%;
+ }
+
+ &:hover,
+ &[data-active="true"] {
+ background: #161616;
+
+ [data-slot="selected-icon"] {
+ visibility: visible;
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+
+ [data-component="footer"] {
+ display: flex;
+ flex-direction: column;
+ width: 100%;
+ justify-content: center;
+ align-items: center;
+ gap: 24px;
+ flex-shrink: 0;
+
+ @media (min-width: 768px) {
+ height: 120px;
+ }
+
+ [data-slot="footer-content"] {
+ display: flex;
+ gap: 24px;
+ align-items: center;
+ justify-content: center;
+
+ @media (min-width: 768px) {
+ gap: 40px;
+ }
+
+ span,
+ a {
+ color: rgba(255, 255, 255, 0.39);
+ font-family: var(--font-mono);
+ font-size: 16px;
+ font-style: normal;
+ font-weight: 400;
+ line-height: normal;
+ text-decoration: none;
+ }
+
+ [data-slot="github-stars"] {
+ color: rgba(255, 255, 255, 0.25);
+ font-family: var(--font-mono);
+ font-size: 16px;
+ font-style: normal;
+ font-weight: 400;
+ line-height: normal;
+ }
+
+ [data-slot="anomaly"] {
+ display: none;
+
+ @media (min-width: 768px) {
+ display: block;
+ }
+ }
+ }
+
+ [data-slot="anomaly-alt"] {
+ color: rgba(255, 255, 255, 0.39);
+ font-family: var(--font-mono);
+ font-size: 16px;
+ font-style: normal;
+ font-weight: 400;
+ line-height: normal;
+ text-decoration: none;
+ margin-bottom: 24px;
+
+ a {
+ color: rgba(255, 255, 255, 0.39);
+ font-family: "JetBrains Mono Nerd Font", monospace;
+ font-size: 16px;
+ font-style: normal;
+ font-weight: 400;
+ line-height: normal;
+ text-decoration: none;
+ }
+
+ @media (min-width: 768px) {
+ display: none;
+ }
+ }
+ }
+}
+
+::view-transition-group(*) {
+ animation-duration: 200ms;
+ animation-timing-function: cubic-bezier(0.25, 0, 0.5, 1);
+}
+
+@keyframes reveal {
+ 100% {
+ mask-position: 0% 0%;
+ opacity: 1;
+ }
+}
diff --git a/packages/console/app/src/routes/black.tsx b/packages/console/app/src/routes/black.tsx
new file mode 100644
index 00000000000..bd836347070
--- /dev/null
+++ b/packages/console/app/src/routes/black.tsx
@@ -0,0 +1,174 @@
+import { A, createAsync, RouteSectionProps } from "@solidjs/router"
+import { Title, Meta, Link } from "@solidjs/meta"
+import { createMemo } from "solid-js"
+import { github } from "~/lib/github"
+import { config } from "~/config"
+import "./black.css"
+
+export default function BlackLayout(props: RouteSectionProps) {
+ const githubData = createAsync(() => github())
+ const starCount = createMemo(() =>
+ githubData()?.stars
+ ? new Intl.NumberFormat("en-US", {
+ notation: "compact",
+ compactDisplay: "short",
+ }).format(githubData()!.stars!)
+ : config.github.starsFormatted.compact,
+ )
+
+ return (
+
+
OpenCode Black | Access all the world's best coding models
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Access all the world's best coding models
+
Including Claude, GPT, Gemini and more
+
+
+
+
+ {props.children}
+
+
+
+ )
+}
diff --git a/packages/console/app/src/routes/black/common.tsx b/packages/console/app/src/routes/black/common.tsx
new file mode 100644
index 00000000000..39844abee38
--- /dev/null
+++ b/packages/console/app/src/routes/black/common.tsx
@@ -0,0 +1,62 @@
+import { Match, Switch } from "solid-js"
+
+export const plans = [
+ { id: "20", multiplier: null },
+ { id: "100", multiplier: "5x more usage than Black 20" },
+ { id: "200", multiplier: "20x more usage than Black 20" },
+] as const
+
+export type PlanID = (typeof plans)[number]["id"]
+export type Plan = (typeof plans)[number]
+
+export function PlanIcon(props: { plan: string }) {
+ return (
+
+
+
+
+
+
+
+
+
+
+
+ )
+}
diff --git a/packages/console/app/src/routes/black/index.css b/packages/console/app/src/routes/black/index.css
deleted file mode 100644
index 418598792fb..00000000000
--- a/packages/console/app/src/routes/black/index.css
+++ /dev/null
@@ -1,409 +0,0 @@
-[data-page="black"] {
- background: #000;
- min-height: 100vh;
- display: flex;
- flex-direction: column;
- align-items: center;
- justify-content: stretch;
- font-family: var(--font-mono);
- color: #fff;
-
- [data-component="header-gradient"] {
- position: absolute;
- top: 0;
- left: 0;
- width: 100%;
- height: 288px;
- background: linear-gradient(180deg, rgba(255, 255, 255, 0.16) 0%, rgba(0, 0, 0, 0) 100%);
- }
-
- [data-component="header"] {
- display: flex;
- flex-direction: column;
- align-items: center;
- justify-content: center;
- padding-top: 40px;
- flex-shrink: 0;
-
- /* [data-component="header-logo"] { */
- /* } */
- }
-
- [data-component="content"] {
- display: flex;
- flex-direction: column;
- align-items: center;
- width: 100%;
- flex-grow: 1;
-
- [data-slot="hero-black"] {
- margin-top: 110px;
-
- @media (min-width: 768px) {
- margin-top: 150px;
- }
- }
-
- [data-slot="cta"] {
- display: flex;
- flex-direction: column;
- gap: 32px;
- align-items: center;
- text-align: center;
- margin-top: -18px;
-
- @media (min-width: 768px) {
- margin-top: 40px;
- }
-
- [data-slot="heading"] {
- color: rgba(255, 255, 255, 0.92);
- text-align: center;
- font-size: 18px;
- font-style: normal;
- font-weight: 400;
- line-height: 160%; /* 28.8px */
-
- span {
- display: inline-block;
- }
- }
- [data-slot="subheading"] {
- color: rgba(255, 255, 255, 0.59);
- font-size: 15px;
- font-style: normal;
- font-weight: 400;
- line-height: 160%;
-
- @media (min-width: 768px) {
- font-size: 18px;
- line-height: 160%;
- }
- }
- [data-slot="button"] {
- display: inline-flex;
- height: 40px;
- padding: 0 12px;
- justify-content: center;
- align-items: center;
- gap: 8px;
- border-radius: 4px;
- background: rgba(255, 255, 255, 0.92);
- text-decoration: none;
- color: #000;
- font-family: "JetBrains Mono Nerd Font";
- font-size: 16px;
- font-style: normal;
- font-weight: 500;
- line-height: normal;
-
- &:hover {
- background: #e0e0e0;
- }
-
- &:active {
- transform: scale(0.98);
- }
- }
- [data-slot="back-soon"] {
- color: rgba(255, 255, 255, 0.59);
- text-align: center;
- font-size: 13px;
- font-style: normal;
- font-weight: 400;
- line-height: 160%; /* 20.8px */
- }
- [data-slot="follow-us"] {
- display: inline-flex;
- height: 40px;
- padding: 0 12px;
- justify-content: center;
- align-items: center;
- gap: 8px;
- border-radius: 4px;
- border: 1px solid rgba(255, 255, 255, 0.17);
- color: rgba(255, 255, 255, 0.59);
- font-family: "JetBrains Mono Nerd Font";
- font-size: 14px;
- font-style: normal;
- font-weight: 400;
- line-height: normal;
- text-decoration: none;
- }
-
- [data-slot="pricing"] {
- display: flex;
- flex-direction: column;
- gap: 16px;
- width: 100%;
- max-width: 540px;
- padding: 0 20px;
-
- @media (min-width: 768px) {
- padding: 0;
- }
- }
-
- [data-slot="pricing-card"] {
- display: flex;
- flex-direction: column;
- gap: 12px;
- padding: 20px;
- border: 1px solid rgba(255, 255, 255, 0.17);
- border-radius: 4px;
- text-decoration: none;
- transition: border-color 0.15s ease;
- background: transparent;
- cursor: pointer;
- text-align: left;
-
- &:hover {
- border-color: rgba(255, 255, 255, 0.35);
- }
-
- [data-slot="icon"] {
- color: rgba(255, 255, 255, 0.59);
- }
-
- [data-slot="price"] {
- display: flex;
- flex-wrap: wrap;
- align-items: baseline;
- gap: 8px;
- }
-
- [data-slot="amount"] {
- color: rgba(255, 255, 255, 0.92);
- font-size: 24px;
- font-weight: 500;
- }
-
- [data-slot="period"] {
- color: rgba(255, 255, 255, 0.59);
- font-size: 14px;
- }
-
- [data-slot="multiplier"] {
- color: rgba(255, 255, 255, 0.39);
- font-size: 14px;
-
- &::before {
- content: "·";
- margin-right: 8px;
- }
- }
- }
-
- [data-slot="selected-plan"] {
- display: flex;
- flex-direction: column;
- gap: 32px;
- width: fit-content;
- max-width: calc(100% - 40px);
- margin: 0 auto;
- }
-
- [data-slot="selected-card"] {
- display: flex;
- flex-direction: column;
- gap: 16px;
- padding: 20px;
- border: 1px solid rgba(255, 255, 255, 0.17);
- border-radius: 4px;
- width: fit-content;
-
- [data-slot="icon"] {
- color: rgba(255, 255, 255, 0.59);
- }
-
- [data-slot="price"] {
- display: flex;
- flex-wrap: wrap;
- align-items: baseline;
- gap: 8px;
- }
-
- [data-slot="amount"] {
- color: rgba(255, 255, 255, 0.92);
- font-size: 24px;
- font-weight: 500;
- }
-
- [data-slot="period"] {
- color: rgba(255, 255, 255, 0.59);
- font-size: 14px;
- }
-
- [data-slot="multiplier"] {
- color: rgba(255, 255, 255, 0.39);
- font-size: 14px;
-
- &::before {
- content: "·";
- margin-right: 8px;
- }
- }
-
- [data-slot="terms"] {
- list-style: none;
- padding: 0;
- margin: 0;
- display: flex;
- flex-direction: column;
- gap: 12px;
- text-align: left;
-
- li {
- color: rgba(255, 255, 255, 0.59);
- font-size: 13px;
- line-height: 1.5;
- padding-left: 16px;
- position: relative;
- white-space: nowrap;
-
- &::before {
- content: "▪";
- position: absolute;
- left: 0;
- color: rgba(255, 255, 255, 0.39);
- }
- }
- }
-
- [data-slot="actions"] {
- display: flex;
- gap: 16px;
- margin-top: 8px;
-
- button,
- a {
- flex: 1;
- display: inline-flex;
- height: 48px;
- padding: 0 16px;
- justify-content: center;
- align-items: center;
- border-radius: 4px;
- font-family: var(--font-mono);
- font-size: 16px;
- font-weight: 400;
- text-decoration: none;
- cursor: pointer;
- }
-
- [data-slot="cancel"] {
- background: transparent;
- border: 1px solid rgba(255, 255, 255, 0.17);
- color: rgba(255, 255, 255, 0.92);
-
- &:hover {
- border-color: rgba(255, 255, 255, 0.35);
- }
- }
-
- [data-slot="continue"] {
- background: rgba(255, 255, 255, 0.17);
- border: 1px solid rgba(255, 255, 255, 0.17);
- color: rgba(255, 255, 255, 0.59);
-
- &:hover {
- background: rgba(255, 255, 255, 0.25);
- }
- }
- }
- }
-
- [data-slot="fine-print"] {
- color: rgba(255, 255, 255, 0.39);
- text-align: center;
- font-size: 13px;
- font-style: normal;
- font-weight: 400;
- line-height: 160%; /* 20.8px */
- font-style: italic;
-
- a {
- color: rgba(255, 255, 255, 0.39);
- text-decoration: underline;
- }
- }
- }
- }
-
- [data-component="footer"] {
- display: flex;
- flex-direction: column;
- width: 100%;
- justify-content: center;
- align-items: center;
- gap: 24px;
- flex-shrink: 0;
-
- @media (min-width: 768px) {
- height: 120px;
- }
-
- [data-slot="footer-content"] {
- display: flex;
- gap: 24px;
- align-items: center;
- justify-content: center;
-
- @media (min-width: 768px) {
- gap: 40px;
- }
-
- span,
- a {
- color: rgba(255, 255, 255, 0.39);
- font-family: "JetBrains Mono Nerd Font";
- font-size: 16px;
- font-style: normal;
- font-weight: 400;
- line-height: normal;
- text-decoration: none;
- }
-
- [data-slot="github-stars"] {
- color: rgba(255, 255, 255, 0.25);
- font-family: "JetBrains Mono Nerd Font";
- font-size: 16px;
- font-style: normal;
- font-weight: 400;
- line-height: normal;
- }
-
- [data-slot="anomaly"] {
- display: none;
-
- @media (min-width: 768px) {
- display: block;
- }
- }
- }
- [data-slot="anomaly-alt"] {
- color: rgba(255, 255, 255, 0.39);
- font-family: "JetBrains Mono Nerd Font";
- font-size: 16px;
- font-style: normal;
- font-weight: 400;
- line-height: normal;
- text-decoration: none;
- margin-bottom: 24px;
-
- a {
- color: rgba(255, 255, 255, 0.39);
- font-family: "JetBrains Mono Nerd Font";
- font-size: 16px;
- font-style: normal;
- font-weight: 400;
- line-height: normal;
- text-decoration: none;
- }
-
- @media (min-width: 768px) {
- display: none;
- }
- }
- }
-}
diff --git a/packages/console/app/src/routes/black/index.tsx b/packages/console/app/src/routes/black/index.tsx
index f5a375adf87..c46fdc0dabb 100644
--- a/packages/console/app/src/routes/black/index.tsx
+++ b/packages/console/app/src/routes/black/index.tsx
@@ -1,276 +1,149 @@
-import { A, createAsync, useSearchParams } from "@solidjs/router"
-import "./index.css"
+import { A, useSearchParams } from "@solidjs/router"
import { Title } from "@solidjs/meta"
-import { github } from "~/lib/github"
-import { createMemo, createSignal, For, Match, Show, Switch } from "solid-js"
-import { config } from "~/config"
-
-const plans = [
- { id: "20", amount: 20, multiplier: null },
- { id: "100", amount: 100, multiplier: "6x more usage than Black 20" },
- { id: "200", amount: 200, multiplier: "21x more usage than Black 20" },
-] as const
-
-function PlanIcon(props: { plan: string }) {
- return (
-
-
-
-
-
-
-
-
-
-
-
- )
-}
+import { createMemo, createSignal, For, onMount, Show } from "solid-js"
+import { PlanIcon, plans } from "./common"
export default function Black() {
const [params] = useSearchParams()
- const [selected, setSelected] = createSignal(params.plan as string | null)
- const selectedPlan = createMemo(() => plans.find((p) => p.id === selected()))
+ const [selected, setSelected] = createSignal((params.plan as string) || null)
+ const [mounted, setMounted] = createSignal(false)
- const githubData = createAsync(() => github())
- const starCount = createMemo(() =>
- githubData()?.stars
- ? new Intl.NumberFormat("en-US", {
- notation: "compact",
- compactDisplay: "short",
- }).format(githubData()!.stars!)
- : config.github.starsFormatted.compact,
- )
+ onMount(() => {
+ requestAnimationFrame(() => setMounted(true))
+ })
+
+ const transition = (action: () => void) => {
+ if (mounted() && "startViewTransition" in document) {
+ ;(document as any).startViewTransition(action)
+ return
+ }
+
+ action()
+ }
+
+ const select = (planId: string) => {
+ if (selected() === planId) {
+ return
+ }
+
+ transition(() => setSelected(planId))
+ }
+
+ const cancel = () => {
+ transition(() => setSelected(null))
+ }
return (
-
+ <>
opencode
-
-
-
-
-
-
-
-
-
-
-
-
-
- Access all the world's best coding models
-
-
Including Claude, GPT, Gemini, and more
-
-
-
-
-
- {(plan) => (
-