summaryrefslogtreecommitdiffhomepage
path: root/packages/cloud/app
diff options
context:
space:
mode:
authorFrank <[email protected]>2025-09-18 10:59:01 -0400
committerFrank <[email protected]>2025-09-18 10:59:01 -0400
commit4ceabdffa07b1af8d99eb73622a4d549d99ec6d2 (patch)
tree72e2ae62084a9e24cc76caffbd1f30dafc69ea56 /packages/cloud/app
parentc87480cf931a6f8f8b55552558ef521f1918b578 (diff)
downloadopencode-4ceabdffa07b1af8d99eb73622a4d549d99ec6d2.tar.gz
opencode-4ceabdffa07b1af8d99eb73622a4d549d99ec6d2.zip
wip: zen
Diffstat (limited to 'packages/cloud/app')
-rw-r--r--packages/cloud/app/.gitignore28
-rw-r--r--packages/cloud/app/.opencode/agent/css.md149
-rw-r--r--packages/cloud/app/README.md32
-rw-r--r--packages/cloud/app/app.config.ts23
-rw-r--r--packages/cloud/app/package.json25
-rw-r--r--packages/cloud/app/public/favicon.svg5
-rw-r--r--packages/cloud/app/public/robots.txt5
-rw-r--r--packages/cloud/app/public/social-share.pngbin17520 -> 0 bytes
-rw-r--r--packages/cloud/app/public/theme.json182
-rw-r--r--packages/cloud/app/src/app.css1
-rw-r--r--packages/cloud/app/src/app.tsx23
-rw-r--r--packages/cloud/app/src/asset/lander/check.svg2
-rw-r--r--packages/cloud/app/src/asset/lander/copy.svg2
-rw-r--r--packages/cloud/app/src/asset/lander/screenshot-github.pngbin924094 -> 0 bytes
-rw-r--r--packages/cloud/app/src/asset/lander/screenshot-splash.pngbin467281 -> 0 bytes
-rw-r--r--packages/cloud/app/src/asset/lander/screenshot-vscode.pngbin1022418 -> 0 bytes
-rw-r--r--packages/cloud/app/src/asset/lander/screenshot.pngbin606051 -> 0 bytes
-rw-r--r--packages/cloud/app/src/asset/logo-ornate-dark.svg19
-rw-r--r--packages/cloud/app/src/asset/logo-ornate-light.svg18
-rw-r--r--packages/cloud/app/src/asset/logo.svg12
-rw-r--r--packages/cloud/app/src/component/icon.tsx82
-rw-r--r--packages/cloud/app/src/component/workspace/billing-section.module.css114
-rw-r--r--packages/cloud/app/src/component/workspace/billing-section.tsx193
-rw-r--r--packages/cloud/app/src/component/workspace/common.tsx25
-rw-r--r--packages/cloud/app/src/component/workspace/key-section.module.css172
-rw-r--r--packages/cloud/app/src/component/workspace/key-section.tsx182
-rw-r--r--packages/cloud/app/src/component/workspace/monthly-limit-section.module.css102
-rw-r--r--packages/cloud/app/src/component/workspace/monthly-limit-section.tsx139
-rw-r--r--packages/cloud/app/src/component/workspace/new-user-section.module.css163
-rw-r--r--packages/cloud/app/src/component/workspace/new-user-section.tsx97
-rw-r--r--packages/cloud/app/src/component/workspace/payment-section.module.css72
-rw-r--r--packages/cloud/app/src/component/workspace/payment-section.tsx113
-rw-r--r--packages/cloud/app/src/component/workspace/usage-section.module.css88
-rw-r--r--packages/cloud/app/src/component/workspace/usage-section.tsx128
-rw-r--r--packages/cloud/app/src/context/auth.session.ts23
-rw-r--r--packages/cloud/app/src/context/auth.ts83
-rw-r--r--packages/cloud/app/src/context/auth.withActor.ts7
-rw-r--r--packages/cloud/app/src/entry-client.tsx4
-rw-r--r--packages/cloud/app/src/entry-server.tsx28
-rw-r--r--packages/cloud/app/src/global.d.ts1
-rw-r--r--packages/cloud/app/src/middleware.ts5
-rw-r--r--packages/cloud/app/src/routes/[...404].css130
-rw-r--r--packages/cloud/app/src/routes/[...404].tsx38
-rw-r--r--packages/cloud/app/src/routes/auth/authorize.ts7
-rw-r--r--packages/cloud/app/src/routes/auth/callback.ts31
-rw-r--r--packages/cloud/app/src/routes/auth/index.ts13
-rw-r--r--packages/cloud/app/src/routes/debug/index.ts13
-rw-r--r--packages/cloud/app/src/routes/discord.ts5
-rw-r--r--packages/cloud/app/src/routes/docs/[...path].ts20
-rw-r--r--packages/cloud/app/src/routes/docs/index.ts20
-rw-r--r--packages/cloud/app/src/routes/index.css504
-rw-r--r--packages/cloud/app/src/routes/index.tsx183
-rw-r--r--packages/cloud/app/src/routes/s/[id].ts20
-rw-r--r--packages/cloud/app/src/routes/stripe/webhook.ts98
-rw-r--r--packages/cloud/app/src/routes/workspace.css127
-rw-r--r--packages/cloud/app/src/routes/workspace.tsx67
-rw-r--r--packages/cloud/app/src/routes/workspace/[id].css115
-rw-r--r--packages/cloud/app/src/routes/workspace/[id].tsx50
-rw-r--r--packages/cloud/app/src/routes/workspace/index.tsx0
-rw-r--r--packages/cloud/app/src/routes/zen/handler.ts594
-rw-r--r--packages/cloud/app/src/routes/zen/v1/chat/completions.ts54
-rw-r--r--packages/cloud/app/src/routes/zen/v1/messages.ts61
-rw-r--r--packages/cloud/app/src/routes/zen/v1/responses.ts52
-rw-r--r--packages/cloud/app/src/style/base.css9
-rw-r--r--packages/cloud/app/src/style/component/button.css102
-rw-r--r--packages/cloud/app/src/style/index.css8
-rw-r--r--packages/cloud/app/src/style/reset.css76
-rw-r--r--packages/cloud/app/src/style/token/color.css91
-rw-r--r--packages/cloud/app/src/style/token/font.css20
-rw-r--r--packages/cloud/app/src/style/token/space.css46
-rw-r--r--packages/cloud/app/sst-env.d.ts9
-rw-r--r--packages/cloud/app/tsconfig.json21
72 files changed, 0 insertions, 4931 deletions
diff --git a/packages/cloud/app/.gitignore b/packages/cloud/app/.gitignore
deleted file mode 100644
index 751513ce1..000000000
--- a/packages/cloud/app/.gitignore
+++ /dev/null
@@ -1,28 +0,0 @@
-dist
-.wrangler
-.output
-.vercel
-.netlify
-.vinxi
-app.config.timestamp_*.js
-
-# Environment
-.env
-.env*.local
-
-# dependencies
-/node_modules
-
-# IDEs and editors
-/.idea
-.project
-.classpath
-*.launch
-.settings/
-
-# Temp
-gitignore
-
-# System Files
-.DS_Store
-Thumbs.db
diff --git a/packages/cloud/app/.opencode/agent/css.md b/packages/cloud/app/.opencode/agent/css.md
deleted file mode 100644
index d0ec43a48..000000000
--- a/packages/cloud/app/.opencode/agent/css.md
+++ /dev/null
@@ -1,149 +0,0 @@
----
-description: use whenever you are styling a ui with css
----
-
-you are very good at writing clean maintainable css using modern techniques
-
-css is structured like this
-
-```css
-[data-page="home"] {
- [data-component="header"] {
- [data-slot="logo"] {
- }
- }
-}
-```
-
-top level pages are scoped using `data-page`
-
-pages can break down into components using `data-component`
-
-components can break down into slots using `data-slot`
-
-structure things so that this hierarchy is followed IN YOUR CSS - you should rarely need to
-nest components inside other components. you should NEVER nest components inside
-slots. you should NEVER nest slots inside other slots.
-
-**IMPORTANT: This hierarchy rule applies to CSS structure, NOT JSX/DOM structure.**
-
-The hierarchy in css file does NOT have to match the hierarchy in the dom - you
-can put components or slots at the same level in CSS even if one goes inside another in the DOM.
-
-Your JSX can nest however makes semantic sense - components can be inside slots,
-slots can contain components, etc. The DOM structure should be whatever makes the most
-semantic and functional sense.
-
-It is more important to follow the pages -> components -> slots structure IN YOUR CSS,
-while keeping your JSX/DOM structure logical and semantic.
-
-use data attributes to represent different states of the component
-
-```css
-[data-component="modal"] {
- opacity: 0;
-
- &[data-state="open"] {
- opacity: 1;
- }
-}
-```
-
-this will allow jsx to control the syling
-
-avoid selectors that just target an element type like `> span` you should assign
-it a slot name. it's ok to do this sometimes where it makes sense semantically
-like targeting `li` elements in a list
-
-in terms of file structure `./src/style/` contains all universal styling rules.
-these should not contain anything specific to a page
-
-`./src/style/token` contains all the tokens used in the project
-
-`./src/style/component` is for reusable components like buttons or inputs
-
-page specific styles should go next to the page they are styling so
-`./src/routes/about.tsx` should have its styles in `./src/routes/about.css`
-
-`about.css` should be scoped using `data-page="about"`
-
-## Example of correct implementation
-
-JSX can nest however makes sense semantically:
-
-```jsx
-<div data-slot="left">
- <div data-component="title">Section Title</div>
- <div data-slot="content">Content here</div>
-</div>
-```
-
-CSS maintains clean hierarchy regardless of DOM nesting:
-
-```css
-[data-page="home"] {
- [data-component="screenshots"] {
- [data-slot="left"] {
- /* styles */
- }
- [data-slot="content"] {
- /* styles */
- }
- }
-
- [data-component="title"] {
- /* can be at same level even though nested in DOM */
- }
-}
-```
-
-## Reusable Components
-
-If a component is reused across multiple sections of the same page, define it at the page level:
-
-```jsx
-<!-- Used in multiple places on the same page -->
-<section data-component="install">
- <div data-component="method">
- <h3 data-component="title">npm</h3>
- </div>
- <div data-component="method">
- <h3 data-component="title">bun</h3>
- </div>
-</section>
-
-<section data-component="screenshots">
- <div data-slot="left">
- <div data-component="title">Screenshot Title</div>
- </div>
-</section>
-```
-
-```css
-[data-page="home"] {
- /* Reusable title component defined at page level since it's used in multiple components */
- [data-component="title"] {
- text-transform: uppercase;
- font-weight: 400;
- }
-
- [data-component="install"] {
- /* install-specific styles */
- }
-
- [data-component="screenshots"] {
- /* screenshots-specific styles */
- }
-}
-```
-
-This is correct because the `title` component has consistent styling and behavior across the page.
-
-## Key Clarifications
-
-1. **JSX Nesting is Flexible**: Components can be nested inside slots, slots can contain components - whatever makes semantic sense
-2. **CSS Hierarchy is Strict**: Follow pages → components → slots structure in CSS
-3. **Reusable Components**: Define at the appropriate level where they're shared (page level if used across the page, component level if only used within that component)
-4. **DOM vs CSS Structure**: These don't need to match - optimize each for its purpose
-
-See ./src/routes/index.css and ./src/routes/index.tsx for a complete example.
diff --git a/packages/cloud/app/README.md b/packages/cloud/app/README.md
deleted file mode 100644
index 9337430cf..000000000
--- a/packages/cloud/app/README.md
+++ /dev/null
@@ -1,32 +0,0 @@
-# SolidStart
-
-Everything you need to build a Solid project, powered by [`solid-start`](https://start.solidjs.com);
-
-## Creating a project
-
-```bash
-# create a new project in the current directory
-npm init solid@latest
-
-# create a new project in my-app
-npm init solid@latest my-app
-```
-
-## Developing
-
-Once you've created a project and installed dependencies with `npm install` (or `pnpm install` or `yarn`), start a development server:
-
-```bash
-npm run dev
-
-# or start the server and open the app in a new browser tab
-npm run dev -- --open
-```
-
-## Building
-
-Solid apps are built with _presets_, which optimise your project for deployment to different environments.
-
-By default, `npm run build` will generate a Node app that you can run with `npm start`. To use a different preset, add it to the `devDependencies` in `package.json` and specify in your `app.config.js`.
-
-## This project was created with the [Solid CLI](https://github.com/solidjs-community/solid-cli)
diff --git a/packages/cloud/app/app.config.ts b/packages/cloud/app/app.config.ts
deleted file mode 100644
index af013bc81..000000000
--- a/packages/cloud/app/app.config.ts
+++ /dev/null
@@ -1,23 +0,0 @@
-import { defineConfig } from "@solidjs/start/config"
-
-export default defineConfig({
- middleware: "./src/middleware.ts",
- vite: {
- server: {
- allowedHosts: true,
- },
- build: {
- rollupOptions: {
- external: ["cloudflare:workers"],
- },
- minify: false,
- },
- },
- server: {
- compatibilityDate: "2024-09-19",
- preset: "cloudflare_module",
- cloudflare: {
- nodeCompat: true,
- },
- },
-})
diff --git a/packages/cloud/app/package.json b/packages/cloud/app/package.json
deleted file mode 100644
index 577f1dd8d..000000000
--- a/packages/cloud/app/package.json
+++ /dev/null
@@ -1,25 +0,0 @@
-{
- "name": "@opencode/cloud-app",
- "type": "module",
- "scripts": {
- "typecheck": "tsc --noEmit",
- "dev": "vinxi dev --host 0.0.0.0",
- "dev:remote": "VITE_AUTH_URL=https://auth.dev.opencode.ai bun sst shell --stage=dev bun dev",
- "build": "vinxi build && ../../opencode/script/schema.ts ./.output/public/config.json",
- "start": "vinxi start",
- "version": "0.9.11"
- },
- "dependencies": {
- "@ibm/plex": "6.4.1",
- "@openauthjs/openauth": "0.0.0-20250322224806",
- "@solidjs/meta": "^0.29.4",
- "@solidjs/router": "^0.15.0",
- "@solidjs/start": "^1.1.0",
- "solid-js": "catalog:",
- "vinxi": "^0.5.7",
- "@opencode/cloud-core": "workspace:*"
- },
- "engines": {
- "node": ">=22"
- }
-}
diff --git a/packages/cloud/app/public/favicon.svg b/packages/cloud/app/public/favicon.svg
deleted file mode 100644
index 3c81bbdb4..000000000
--- a/packages/cloud/app/public/favicon.svg
+++ /dev/null
@@ -1,5 +0,0 @@
-<svg width="600" height="600" viewBox="0 0 600 600" fill="none" xmlns="http://www.w3.org/2000/svg">
-<rect width="600" height="600" fill="black"/>
-<path fill-rule="evenodd" clip-rule="evenodd" d="M115 180H300V420H115V180ZM253.75 229.044H161.25V370.405H253.75V229.044Z" fill="white"/>
-<path d="M346.25 180H485V229.044H392.5V370.405H485V419.449H346.25V180Z" fill="white"/>
-</svg>
diff --git a/packages/cloud/app/public/robots.txt b/packages/cloud/app/public/robots.txt
deleted file mode 100644
index f88eb1790..000000000
--- a/packages/cloud/app/public/robots.txt
+++ /dev/null
@@ -1,5 +0,0 @@
-User-agent: *
-Allow: /
-
-# Disallow shared content pages
-Disallow: /s/ \ No newline at end of file
diff --git a/packages/cloud/app/public/social-share.png b/packages/cloud/app/public/social-share.png
deleted file mode 100644
index 97f67994d..000000000
--- a/packages/cloud/app/public/social-share.png
+++ /dev/null
Binary files differ
diff --git a/packages/cloud/app/public/theme.json b/packages/cloud/app/public/theme.json
deleted file mode 100644
index b3e97f7ca..000000000
--- a/packages/cloud/app/public/theme.json
+++ /dev/null
@@ -1,182 +0,0 @@
-{
- "$schema": "http://json-schema.org/draft-07/schema#",
- "type": "object",
- "properties": {
- "$schema": {
- "type": "string",
- "description": "JSON schema reference for configuration validation"
- },
- "defs": {
- "type": "object",
- "description": "Color definitions that can be referenced in the theme",
- "patternProperties": {
- "^[a-zA-Z][a-zA-Z0-9_]*$": {
- "oneOf": [
- {
- "type": "string",
- "pattern": "^#[0-9a-fA-F]{6}$",
- "description": "Hex color value"
- },
- {
- "type": "integer",
- "minimum": 0,
- "maximum": 255,
- "description": "ANSI color code (0-255)"
- },
- {
- "type": "string",
- "enum": ["none"],
- "description": "No color (uses terminal default)"
- }
- ]
- }
- },
- "additionalProperties": false
- },
- "theme": {
- "type": "object",
- "description": "Theme color definitions",
- "properties": {
- "primary": { "$ref": "#/definitions/colorValue" },
- "secondary": { "$ref": "#/definitions/colorValue" },
- "accent": { "$ref": "#/definitions/colorValue" },
- "error": { "$ref": "#/definitions/colorValue" },
- "warning": { "$ref": "#/definitions/colorValue" },
- "success": { "$ref": "#/definitions/colorValue" },
- "info": { "$ref": "#/definitions/colorValue" },
- "text": { "$ref": "#/definitions/colorValue" },
- "textMuted": { "$ref": "#/definitions/colorValue" },
- "background": { "$ref": "#/definitions/colorValue" },
- "backgroundPanel": { "$ref": "#/definitions/colorValue" },
- "backgroundElement": { "$ref": "#/definitions/colorValue" },
- "border": { "$ref": "#/definitions/colorValue" },
- "borderActive": { "$ref": "#/definitions/colorValue" },
- "borderSubtle": { "$ref": "#/definitions/colorValue" },
- "diffAdded": { "$ref": "#/definitions/colorValue" },
- "diffRemoved": { "$ref": "#/definitions/colorValue" },
- "diffContext": { "$ref": "#/definitions/colorValue" },
- "diffHunkHeader": { "$ref": "#/definitions/colorValue" },
- "diffHighlightAdded": { "$ref": "#/definitions/colorValue" },
- "diffHighlightRemoved": { "$ref": "#/definitions/colorValue" },
- "diffAddedBg": { "$ref": "#/definitions/colorValue" },
- "diffRemovedBg": { "$ref": "#/definitions/colorValue" },
- "diffContextBg": { "$ref": "#/definitions/colorValue" },
- "diffLineNumber": { "$ref": "#/definitions/colorValue" },
- "diffAddedLineNumberBg": { "$ref": "#/definitions/colorValue" },
- "diffRemovedLineNumberBg": { "$ref": "#/definitions/colorValue" },
- "markdownText": { "$ref": "#/definitions/colorValue" },
- "markdownHeading": { "$ref": "#/definitions/colorValue" },
- "markdownLink": { "$ref": "#/definitions/colorValue" },
- "markdownLinkText": { "$ref": "#/definitions/colorValue" },
- "markdownCode": { "$ref": "#/definitions/colorValue" },
- "markdownBlockQuote": { "$ref": "#/definitions/colorValue" },
- "markdownEmph": { "$ref": "#/definitions/colorValue" },
- "markdownStrong": { "$ref": "#/definitions/colorValue" },
- "markdownHorizontalRule": { "$ref": "#/definitions/colorValue" },
- "markdownListItem": { "$ref": "#/definitions/colorValue" },
- "markdownListEnumeration": { "$ref": "#/definitions/colorValue" },
- "markdownImage": { "$ref": "#/definitions/colorValue" },
- "markdownImageText": { "$ref": "#/definitions/colorValue" },
- "markdownCodeBlock": { "$ref": "#/definitions/colorValue" },
- "syntaxComment": { "$ref": "#/definitions/colorValue" },
- "syntaxKeyword": { "$ref": "#/definitions/colorValue" },
- "syntaxFunction": { "$ref": "#/definitions/colorValue" },
- "syntaxVariable": { "$ref": "#/definitions/colorValue" },
- "syntaxString": { "$ref": "#/definitions/colorValue" },
- "syntaxNumber": { "$ref": "#/definitions/colorValue" },
- "syntaxType": { "$ref": "#/definitions/colorValue" },
- "syntaxOperator": { "$ref": "#/definitions/colorValue" },
- "syntaxPunctuation": { "$ref": "#/definitions/colorValue" }
- },
- "required": ["primary", "secondary", "accent", "text", "textMuted", "background"],
- "additionalProperties": false
- }
- },
- "required": ["theme"],
- "additionalProperties": false,
- "definitions": {
- "colorValue": {
- "oneOf": [
- {
- "type": "string",
- "pattern": "^#[0-9a-fA-F]{6}$",
- "description": "Hex color value (same for dark and light)"
- },
- {
- "type": "integer",
- "minimum": 0,
- "maximum": 255,
- "description": "ANSI color code (0-255, same for dark and light)"
- },
- {
- "type": "string",
- "enum": ["none"],
- "description": "No color (uses terminal default)"
- },
- {
- "type": "string",
- "pattern": "^[a-zA-Z][a-zA-Z0-9_]*$",
- "description": "Reference to another color in the theme or defs"
- },
- {
- "type": "object",
- "properties": {
- "dark": {
- "oneOf": [
- {
- "type": "string",
- "pattern": "^#[0-9a-fA-F]{6}$",
- "description": "Hex color value for dark mode"
- },
- {
- "type": "integer",
- "minimum": 0,
- "maximum": 255,
- "description": "ANSI color code for dark mode"
- },
- {
- "type": "string",
- "enum": ["none"],
- "description": "No color (uses terminal default)"
- },
- {
- "type": "string",
- "pattern": "^[a-zA-Z][a-zA-Z0-9_]*$",
- "description": "Reference to another color for dark mode"
- }
- ]
- },
- "light": {
- "oneOf": [
- {
- "type": "string",
- "pattern": "^#[0-9a-fA-F]{6}$",
- "description": "Hex color value for light mode"
- },
- {
- "type": "integer",
- "minimum": 0,
- "maximum": 255,
- "description": "ANSI color code for light mode"
- },
- {
- "type": "string",
- "enum": ["none"],
- "description": "No color (uses terminal default)"
- },
- {
- "type": "string",
- "pattern": "^[a-zA-Z][a-zA-Z0-9_]*$",
- "description": "Reference to another color for light mode"
- }
- ]
- }
- },
- "required": ["dark", "light"],
- "additionalProperties": false,
- "description": "Separate colors for dark and light modes"
- }
- ]
- }
- }
-}
diff --git a/packages/cloud/app/src/app.css b/packages/cloud/app/src/app.css
deleted file mode 100644
index c0261c422..000000000
--- a/packages/cloud/app/src/app.css
+++ /dev/null
@@ -1 +0,0 @@
-@import "./style/index.css";
diff --git a/packages/cloud/app/src/app.tsx b/packages/cloud/app/src/app.tsx
deleted file mode 100644
index bc3961214..000000000
--- a/packages/cloud/app/src/app.tsx
+++ /dev/null
@@ -1,23 +0,0 @@
-import { MetaProvider, Title, Meta } from "@solidjs/meta"
-import { Router } from "@solidjs/router"
-import { FileRoutes } from "@solidjs/start/router"
-import { ErrorBoundary, Suspense } from "solid-js"
-import "@ibm/plex/css/ibm-plex.css"
-import "./app.css"
-
-export default function App() {
- return (
- <Router
- explicitLinks={true}
- root={(props) => (
- <MetaProvider>
- <Title>opencode</Title>
- <Meta name="description" content="opencode - The AI coding agent built for the terminal." />
- <Suspense>{props.children}</Suspense>
- </MetaProvider>
- )}
- >
- <FileRoutes />
- </Router>
- )
-}
diff --git a/packages/cloud/app/src/asset/lander/check.svg b/packages/cloud/app/src/asset/lander/check.svg
deleted file mode 100644
index 22de6f2a8..000000000
--- a/packages/cloud/app/src/asset/lander/check.svg
+++ /dev/null
@@ -1,2 +0,0 @@
-<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path fill="currentColor" d="M9 16.17L5.53 12.7a.996.996 0 1 0-1.41 1.41l4.18 4.18c.39.39 1.02.39 1.41 0L20.29 7.71a.996.996 0 1 0-1.41-1.41z"/></svg>
-
diff --git a/packages/cloud/app/src/asset/lander/copy.svg b/packages/cloud/app/src/asset/lander/copy.svg
deleted file mode 100644
index f1baac30a..000000000
--- a/packages/cloud/app/src/asset/lander/copy.svg
+++ /dev/null
@@ -1,2 +0,0 @@
-<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 512 512"><rect width="336" height="336" x="128" y="128" fill="none" stroke="currentColor" stroke-linejoin="round" stroke-width="32" rx="57" ry="57"/><path fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="32" d="m383.5 128l.5-24a56.16 56.16 0 0 0-56-56H112a64.19 64.19 0 0 0-64 64v216a56.16 56.16 0 0 0 56 56h24"/></svg>
-
diff --git a/packages/cloud/app/src/asset/lander/screenshot-github.png b/packages/cloud/app/src/asset/lander/screenshot-github.png
deleted file mode 100644
index fda74e641..000000000
--- a/packages/cloud/app/src/asset/lander/screenshot-github.png
+++ /dev/null
Binary files differ
diff --git a/packages/cloud/app/src/asset/lander/screenshot-splash.png b/packages/cloud/app/src/asset/lander/screenshot-splash.png
deleted file mode 100644
index e900673ef..000000000
--- a/packages/cloud/app/src/asset/lander/screenshot-splash.png
+++ /dev/null
Binary files differ
diff --git a/packages/cloud/app/src/asset/lander/screenshot-vscode.png b/packages/cloud/app/src/asset/lander/screenshot-vscode.png
deleted file mode 100644
index b8966a6b8..000000000
--- a/packages/cloud/app/src/asset/lander/screenshot-vscode.png
+++ /dev/null
Binary files differ
diff --git a/packages/cloud/app/src/asset/lander/screenshot.png b/packages/cloud/app/src/asset/lander/screenshot.png
deleted file mode 100644
index feb617585..000000000
--- a/packages/cloud/app/src/asset/lander/screenshot.png
+++ /dev/null
Binary files differ
diff --git a/packages/cloud/app/src/asset/logo-ornate-dark.svg b/packages/cloud/app/src/asset/logo-ornate-dark.svg
deleted file mode 100644
index 2efda934d..000000000
--- a/packages/cloud/app/src/asset/logo-ornate-dark.svg
+++ /dev/null
@@ -1,19 +0,0 @@
-<svg width="289" height="50" viewBox="0 0 289 50" fill="none" xmlns="http://www.w3.org/2000/svg">
-<path d="M8.5 16.5H24.5V33H8.5V16.5Z" fill="white" fill-opacity="0.2"/>
-<path d="M48.5 16.5H64.5V33H48.5V16.5Z" fill="white" fill-opacity="0.2"/>
-<path d="M120.5 16.5H136.5V33H120.5V16.5Z" fill="white" fill-opacity="0.2"/>
-<path d="M160.5 16.5H176.5V33H160.5V16.5Z" fill="white" fill-opacity="0.2"/>
-<path d="M192.5 16.5H208.5V33H192.5V16.5Z" fill="white" fill-opacity="0.2"/>
-<path d="M232.5 16.5H248.5V33H232.5V16.5Z" fill="white" fill-opacity="0.2"/>
-<path d="M264.5 0H288.5V8.5H272.5V16.5H288.5V25H272.5V33H288.5V41.5H264.5V0Z" fill="white" fill-opacity="0.95"/>
-<path d="M248.5 0H224.5V41.5H248.5V33H232.5V8.5H248.5V0Z" fill="white" fill-opacity="0.95"/>
-<path d="M256.5 8.5H248.5V33H256.5V8.5Z" fill="white" fill-opacity="0.95"/>
-<path fill-rule="evenodd" clip-rule="evenodd" d="M184.5 0H216.5V41.5H184.5V0ZM208.5 8.5H192.5V33H208.5V8.5Z" fill="white" fill-opacity="0.95"/>
-<path d="M144.5 8.5H136.5V41.5H144.5V8.5Z" fill="white" fill-opacity="0.5"/>
-<path d="M136.5 0H112.5V41.5H120.5V8.5H136.5V0Z" fill="white" fill-opacity="0.5"/>
-<path d="M80.5 0H104.5V8.5H88.5V16.5H104.5V25H88.5V33H104.5V41.5H80.5V0Z" fill="white" fill-opacity="0.5"/>
-<path fill-rule="evenodd" clip-rule="evenodd" d="M40.5 0H72.5V41.5H48.5V49.5H40.5V0ZM64.5 8.5H48.5V33H64.5V8.5Z" fill="white" fill-opacity="0.5"/>
-<path fill-rule="evenodd" clip-rule="evenodd" d="M0.5 0H32.5V41.5955H0.5V0ZM24.5 8.5H8.5V33H24.5V8.5Z" fill="white" fill-opacity="0.5"/>
-<path d="M152.5 0H176.5V8.5H160.5V33H176.5V41.5H152.5V0Z" fill="white" fill-opacity="0.95"/>
-</svg>
-
diff --git a/packages/cloud/app/src/asset/logo-ornate-light.svg b/packages/cloud/app/src/asset/logo-ornate-light.svg
deleted file mode 100644
index 789223bc4..000000000
--- a/packages/cloud/app/src/asset/logo-ornate-light.svg
+++ /dev/null
@@ -1,18 +0,0 @@
-<svg width="288" height="50" viewBox="0 0 288 50" fill="none" xmlns="http://www.w3.org/2000/svg">
-<path d="M8 16.5H24V33H8V16.5Z" fill="black" fill-opacity="0.15"/>
-<path d="M48 16.5H64V33H48V16.5Z" fill="black" fill-opacity="0.15"/>
-<path d="M120 16.5H136V33H120V16.5Z" fill="black" fill-opacity="0.15"/>
-<path d="M160 16.5H176V33H160V16.5Z" fill="black" fill-opacity="0.15"/>
-<path d="M192 16.5H208V33H192V16.5Z" fill="black" fill-opacity="0.15"/>
-<path d="M232 16.5H248V33H232V16.5Z" fill="black" fill-opacity="0.15"/>
-<path d="M264 0H288V8.5H272V16.5H288V25H272V33H288V41.5H264V0Z" fill="black" fill-opacity="0.95"/>
-<path d="M248 0H224V41.5H248V33H232V8.5H248V0Z" fill="black" fill-opacity="0.95"/>
-<path d="M256 8.5H248V33H256V8.5Z" fill="black" fill-opacity="0.95"/>
-<path fill-rule="evenodd" clip-rule="evenodd" d="M184 0H216V41.5H184V0ZM208 8.5H192V33H208V8.5Z" fill="black" fill-opacity="0.95"/>
-<path d="M144 8.5H136V41.5H144V8.5Z" fill="black" fill-opacity="0.55"/>
-<path d="M136 0H112V41.5H120V8.5H136V0Z" fill="black" fill-opacity="0.55"/>
-<path d="M80 0H104V8.5H88V16.5H104V25H88V33H104V41.5H80V0Z" fill="black" fill-opacity="0.55"/>
-<path fill-rule="evenodd" clip-rule="evenodd" d="M40 0H72V41.5H48V49.5H40V0ZM64 8.5H48V33H64V8.5Z" fill="black" fill-opacity="0.55"/>
-<path fill-rule="evenodd" clip-rule="evenodd" d="M0 0H32V41.5955H0V0ZM24 8.5H8V33H24V8.5Z" fill="black" fill-opacity="0.55"/>
-<path d="M152 0H176V8.5H160V33H176V41.5H152V0Z" fill="black" fill-opacity="0.95"/>
-</svg>
diff --git a/packages/cloud/app/src/asset/logo.svg b/packages/cloud/app/src/asset/logo.svg
deleted file mode 100644
index cbfcccf51..000000000
--- a/packages/cloud/app/src/asset/logo.svg
+++ /dev/null
@@ -1,12 +0,0 @@
-<svg width="289" height="50" viewBox="0 0 289 50" fill="none" xmlns="http://www.w3.org/2000/svg">
-<path d="M264.5 0H288.5V8.5H272.5V16.5H288.5V25H272.5V33H288.5V41.5H264.5V0Z" fill="black"/>
-<path d="M248.5 0H224.5V41.5H248.5V33H232.5V8.5H248.5V0Z" fill="black"/>
-<path d="M256.5 8.5H248.5V33H256.5V8.5Z" fill="black"/>
-<path fill-rule="evenodd" clip-rule="evenodd" d="M184.5 0H216.5V41.5H184.5V0ZM208.5 8.5H192.5V33H208.5V8.5Z" fill="black"/>
-<path d="M144.5 8.5H136.5V41.5H144.5V8.5Z" fill="black"/>
-<path d="M136.5 0H112.5V41.5H120.5V8.5H136.5V0Z" fill="black"/>
-<path d="M80.5 0H104.5V8.5H88.5V16.5H104.5V25H88.5V33H104.5V41.5H80.5V0Z" fill="black"/>
-<path fill-rule="evenodd" clip-rule="evenodd" d="M40.5 0H72.5V41.5H48.5V49.5H40.5V0ZM64.5 8.5H48.5V33H64.5V8.5Z" fill="black"/>
-<path fill-rule="evenodd" clip-rule="evenodd" d="M0.5 0H32.5V41.5955H0.5V0ZM24.5 8.5H8.5V33H24.5V8.5Z" fill="black"/>
-<path d="M152.5 0H176.5V8.5H160.5V33H176.5V41.5H152.5V0Z" fill="black"/>
-</svg>
diff --git a/packages/cloud/app/src/component/icon.tsx b/packages/cloud/app/src/component/icon.tsx
deleted file mode 100644
index a82572e62..000000000
--- a/packages/cloud/app/src/component/icon.tsx
+++ /dev/null
@@ -1,82 +0,0 @@
-import { JSX } from "solid-js"
-
-export function IconLogo(props: JSX.SvgSVGAttributes<SVGSVGElement>) {
- return (
- <svg {...props} viewBox="0 0 289 50" fill="none" xmlns="http://www.w3.org/2000/svg">
- <path d="M264.5 0H288.5V8.5H272.5V16.5H288.5V25H272.5V33H288.5V41.5H264.5V0Z" fill="currentColor" />
- <path d="M248.5 0H224.5V41.5H248.5V33H232.5V8.5H248.5V0Z" fill="currentColor" />
- <path d="M256.5 8.5H248.5V33H256.5V8.5Z" fill="currentColor" />
- <path
- fill-rule="evenodd"
- clip-rule="evenodd"
- d="M184.5 0H216.5V41.5H184.5V0ZM208.5 8.5H192.5V33H208.5V8.5Z"
- fill="currentColor"
- />
- <path d="M144.5 8.5H136.5V41.5H144.5V8.5Z" fill="currentColor" />
- <path d="M136.5 0H112.5V41.5H120.5V8.5H136.5V0Z" fill="currentColor" />
- <path d="M80.5 0H104.5V8.5H88.5V16.5H104.5V25H88.5V33H104.5V41.5H80.5V0Z" fill="currentColor" />
- <path
- fill-rule="evenodd"
- clip-rule="evenodd"
- d="M40.5 0H72.5V41.5H48.5V49.5H40.5V0ZM64.5 8.5H48.5V33H64.5V8.5Z"
- fill="currentColor"
- />
- <path
- fill-rule="evenodd"
- clip-rule="evenodd"
- d="M0.5 0H32.5V41.5955H0.5V0ZM24.5 8.5H8.5V33H24.5V8.5Z"
- fill="currentColor"
- />
- <path d="M152.5 0H176.5V8.5H160.5V33H176.5V41.5H152.5V0Z" fill="currentColor" />
- </svg>
- )
-}
-
-export function IconCopy(props: JSX.SvgSVGAttributes<SVGSVGElement>) {
- return (
- <svg {...props} viewBox="0 0 512 512">
- <rect
- width="336"
- height="336"
- x="128"
- y="128"
- fill="none"
- stroke="currentColor"
- stroke-linejoin="round"
- stroke-width="32"
- rx="57"
- ry="57"
- ></rect>
- <path
- fill="none"
- stroke="currentColor"
- stroke-linecap="round"
- stroke-linejoin="round"
- stroke-width="32"
- d="m383.5 128l.5-24a56.16 56.16 0 0 0-56-56H112a64.19 64.19 0 0 0-64 64v216a56.16 56.16 0 0 0 56 56h24"
- ></path>
- </svg>
- )
-}
-
-export function IconCheck(props: JSX.SvgSVGAttributes<SVGSVGElement>) {
- return (
- <svg {...props} viewBox="0 0 24 24">
- <path
- fill="currentColor"
- d="M9 16.17L5.53 12.7a.996.996 0 1 0-1.41 1.41l4.18 4.18c.39.39 1.02.39 1.41 0L20.29 7.71a.996.996 0 1 0-1.41-1.41z"
- ></path>
- </svg>
- )
-}
-
-export function IconCreditCard(props: JSX.SvgSVGAttributes<SVGSVGElement>) {
- return (
- <svg {...props} viewBox="0 0 24 24">
- <path
- fill="currentColor"
- d="M20 4H4c-1.1 0-1.99.9-1.99 2L2 18c0 1.1.9 2 2 2h16c1.1 0 2-.9 2-2V6c0-1.1-.9-2-2-2zm0 14H4V8h16v10z"
- />
- </svg>
- )
-}
diff --git a/packages/cloud/app/src/component/workspace/billing-section.module.css b/packages/cloud/app/src/component/workspace/billing-section.module.css
deleted file mode 100644
index 0bb5709cb..000000000
--- a/packages/cloud/app/src/component/workspace/billing-section.module.css
+++ /dev/null
@@ -1,114 +0,0 @@
-.root {
- [data-slot="section-content"] {
- display: flex;
- flex-direction: column;
- gap: var(--space-3);
- }
-
- [data-slot="reload-error"] {
- display: flex;
- align-items: center;
- justify-content: space-between;
- gap: var(--space-4);
- padding: var(--space-4);
- border: 1px solid var(--color-border);
- border-radius: var(--border-radius-sm);
-
- p {
- color: var(--color-danger);
- font-size: var(--font-size-sm);
- line-height: 1.4;
- margin: 0;
- flex: 1;
- }
-
- [data-slot="create-form"] {
- display: flex;
- gap: var(--space-2);
- margin: 0;
- flex-shrink: 0;
- }
- }
- [data-slot="payment"] {
- display: flex;
- flex-direction: column;
- gap: var(--space-3);
- padding: var(--space-4);
- border: 1px solid var(--color-border);
- border-radius: var(--border-radius-sm);
- min-width: 14.5rem;
- width: fit-content;
-
- @media (max-width: 30rem) {
- width: 100%;
- }
-
- [data-slot="credit-card"] {
- padding: var(--space-3-5) var(--space-4);
- background-color: var(--color-bg-surface);
- border-radius: var(--border-radius-sm);
- display: flex;
- align-items: center;
- justify-content: space-between;
-
- [data-slot="card-icon"] {
- display: flex;
- align-items: center;
- color: var(--color-text-muted);
- }
-
- [data-slot="card-details"] {
- display: flex;
- align-items: baseline;
- gap: var(--space-1);
-
- [data-slot="secret"] {
- position: relative;
- bottom: 2px;
- font-size: var(--font-size-lg);
- color: var(--color-text-muted);
- font-weight: 400;
- }
-
- [data-slot="number"] {
- font-size: var(--font-size-3xl);
- font-weight: 500;
- color: var(--color-text);
- }
- }
- }
-
- [data-slot="button-row"] {
- display: flex;
- gap: var(--space-2);
- align-items: center;
-
- @media (max-width: 30rem) {
- flex-direction: column;
-
- > button {
- width: 100%;
- }
- }
-
- [data-slot="create-form"] {
- margin: 0;
- }
-
- /* Make Enable Billing button full width when it's the only button */
- > button {
- flex: 1;
- }
- }
- }
- [data-slot="usage"] {
- p {
- font-size: var(--font-size-sm);
- line-height: 1.5;
- color: var(--color-text-secondary);
- b {
- font-weight: 600;
- }
- }
- }
-}
diff --git a/packages/cloud/app/src/component/workspace/billing-section.tsx b/packages/cloud/app/src/component/workspace/billing-section.tsx
deleted file mode 100644
index ec314d9ef..000000000
--- a/packages/cloud/app/src/component/workspace/billing-section.tsx
+++ /dev/null
@@ -1,193 +0,0 @@
-import { json, query, action, useParams, useAction, createAsync, useSubmission } from "@solidjs/router"
-import { createMemo, Show } from "solid-js"
-import { Billing } from "@opencode/cloud-core/billing.js"
-import { withActor } from "~/context/auth.withActor"
-import { IconCreditCard } from "~/component/icon"
-import styles from "./billing-section.module.css"
-
-const createCheckoutUrl = action(async (workspaceID: string, successUrl: string, cancelUrl: string) => {
- "use server"
- return withActor(() => Billing.generateCheckoutUrl({ successUrl, cancelUrl }), workspaceID)
-}, "checkoutUrl")
-
-const reload = action(async (form: FormData) => {
- "use server"
- const workspaceID = form.get("workspaceID")?.toString()
- if (!workspaceID) return { error: "Workspace ID is required" }
- return json(await withActor(() => Billing.reload(), workspaceID), { revalidate: getBillingInfo.key })
-}, "billing.reload")
-
-const disableReload = action(async (form: FormData) => {
- "use server"
- const workspaceID = form.get("workspaceID")?.toString()
- if (!workspaceID) return { error: "Workspace ID is required" }
- return json(await withActor(() => Billing.disableReload(), workspaceID), { revalidate: getBillingInfo.key })
-}, "billing.disableReload")
-
-const createSessionUrl = action(async (workspaceID: string, returnUrl: string) => {
- "use server"
- return withActor(() => Billing.generateSessionUrl({ returnUrl }), workspaceID)
-}, "sessionUrl")
-
-const getBillingInfo = query(async (workspaceID: string) => {
- "use server"
- return withActor(async () => {
- return await Billing.get()
- }, workspaceID)
-}, "billing.get")
-
-export function BillingSection() {
- const params = useParams()
- // ORIGINAL CODE - COMMENTED OUT FOR TESTING
- const balanceInfo = createAsync(() => getBillingInfo(params.id))
- const createCheckoutUrlAction = useAction(createCheckoutUrl)
- const createCheckoutUrlSubmission = useSubmission(createCheckoutUrl)
- const createSessionUrlAction = useAction(createSessionUrl)
- const createSessionUrlSubmission = useSubmission(createSessionUrl)
- const disableReloadSubmission = useSubmission(disableReload)
- const reloadSubmission = useSubmission(reload)
-
- // DUMMY DATA FOR TESTING - UNCOMMENT ONE OF THE SCENARIOS BELOW
-
- // Scenario 1: User has not added billing details and has no balance
- // const balanceInfo = () => ({
- // balance: 0,
- // paymentMethodLast4: null as string | null,
- // reload: false,
- // reloadError: null as string | null,
- // timeReloadError: null as Date | null,
- // })
-
- // Scenario 2: User has not added billing details but has a balance
- // const balanceInfo = () => ({
- // balance: 1500000000, // $15.00
- // paymentMethodLast4: null as string | null,
- // reload: false,
- // reloadError: null as string | null,
- // timeReloadError: null as Date | null
- // })
-
- // Scenario 3: User has added billing details (reload enabled)
- // const balanceInfo = () => ({
- // balance: 750000000, // $7.50
- // paymentMethodLast4: "4242",
- // reload: true,
- // reloadError: null as string | null,
- // timeReloadError: null as Date | null
- // })
-
- // Scenario 4: User has billing details but reload failed
- // const balanceInfo = () => ({
- // balance: 250000000, // $2.50
- // paymentMethodLast4: "4242",
- // reload: true,
- // reloadError: "Your card was declined." as string,
- // timeReloadError: new Date(Date.now() - 3600000) as Date // 1 hour ago
- // })
-
- const balanceAmount = createMemo(() => {
- return ((balanceInfo()?.balance ?? 0) / 100000000).toFixed(2)
- })
-
- return (
- <section class={styles.root}>
- <div data-slot="section-title">
- <h2>Billing</h2>
- <p>
- Manage payments methods. <a href="mailto:[email protected]">Contact us</a> if you have any questions.
- </p>
- </div>
- <div data-slot="section-content">
- <Show when={balanceInfo()?.reloadError}>
- <div data-slot="reload-error">
- <p>
- Reload failed at{" "}
- {balanceInfo()?.timeReloadError!.toLocaleString("en-US", {
- month: "short",
- day: "numeric",
- hour: "numeric",
- minute: "2-digit",
- second: "2-digit",
- })}
- . Reason: {balanceInfo()?.reloadError?.replace(/\.$/, "")}. Please update your payment method and try
- again.
- </p>
- <form action={reload} method="post" data-slot="create-form">
- <input type="hidden" name="workspaceID" value={params.id} />
- <button data-color="primary" type="submit" disabled={reloadSubmission.pending}>
- {reloadSubmission.pending ? "Reloading..." : "Reload"}
- </button>
- </form>
- </div>
- </Show>
- <div data-slot="payment">
- <div data-slot="credit-card">
- <div data-slot="card-icon">
- <IconCreditCard style={{ width: "32px", height: "32px" }} />
- </div>
- <div data-slot="card-details">
- <Show when={balanceInfo()?.paymentMethodLast4} fallback={<span data-slot="number">----</span>}>
- <span data-slot="secret">••••</span>
- <span data-slot="number">{balanceInfo()?.paymentMethodLast4}</span>
- </Show>
- </div>
- </div>
- <div data-slot="button-row">
- <Show
- when={balanceInfo()?.reload}
- fallback={
- <button
- data-color="primary"
- disabled={createCheckoutUrlSubmission.pending}
- onClick={async () => {
- const baseUrl = window.location.href
- const checkoutUrl = await createCheckoutUrlAction(params.id, baseUrl, baseUrl)
- if (checkoutUrl) {
- window.location.href = checkoutUrl
- }
- }}
- >
- {createCheckoutUrlSubmission.pending ? "Loading..." : "Enable Billing"}
- </button>
- }
- >
- <button
- data-color="primary"
- disabled={createSessionUrlSubmission.pending}
- onClick={async () => {
- const baseUrl = window.location.href
- const sessionUrl = await createSessionUrlAction(params.id, baseUrl)
- if (sessionUrl) {
- window.location.href = sessionUrl
- }
- }}
- >
- {createSessionUrlSubmission.pending ? "Loading..." : "Manage Payment Methods"}
- </button>
- <form action={disableReload} method="post" data-slot="create-form">
- <input type="hidden" name="workspaceID" value={params.id} />
- <button data-color="ghost" type="submit" disabled={disableReloadSubmission.pending}>
- {disableReloadSubmission.pending ? "Disabling..." : "Disable"}
- </button>
- </form>
- </Show>
- </div>
- </div>
- <div data-slot="usage">
- <Show when={!balanceInfo()?.reload && !(balanceAmount() === "0.00" || balanceAmount() === "-0.00")}>
- <p>
- You have <b data-slot="value">${balanceAmount() === "-0.00" ? "0.00" : balanceAmount()}</b> remaining in
- your account. You can continue using the API with your remaining balance.
- </p>
- </Show>
- <Show when={balanceInfo()?.reload && !balanceInfo()?.reloadError}>
- <p>
- Your current balance is <b data-slot="value">${balanceAmount() === "-0.00" ? "0.00" : balanceAmount()}</b>
- . We'll automatically reload <b>$20</b> (+$1.23 processing fee) when it reaches <b>$5</b>.
- </p>
- </Show>
- </div>
- </div>
- </section>
- )
-}
diff --git a/packages/cloud/app/src/component/workspace/common.tsx b/packages/cloud/app/src/component/workspace/common.tsx
deleted file mode 100644
index f85fd8423..000000000
--- a/packages/cloud/app/src/component/workspace/common.tsx
+++ /dev/null
@@ -1,25 +0,0 @@
-export function formatDateForTable(date: Date) {
- const options: Intl.DateTimeFormatOptions = {
- day: "numeric",
- month: "short",
- hour: "numeric",
- minute: "2-digit",
- hour12: true,
- }
- return date.toLocaleDateString("en-GB", options).replace(",", ",")
-}
-
-export function formatDateUTC(date: Date) {
- const options: Intl.DateTimeFormatOptions = {
- weekday: "short",
- year: "numeric",
- month: "short",
- day: "numeric",
- hour: "numeric",
- minute: "2-digit",
- second: "2-digit",
- timeZoneName: "short",
- timeZone: "UTC",
- }
- return date.toLocaleDateString("en-US", options)
-}
diff --git a/packages/cloud/app/src/component/workspace/key-section.module.css b/packages/cloud/app/src/component/workspace/key-section.module.css
deleted file mode 100644
index 6a1d0c85f..000000000
--- a/packages/cloud/app/src/component/workspace/key-section.module.css
+++ /dev/null
@@ -1,172 +0,0 @@
-.root {
- [data-component="empty-state"] {
- padding: var(--space-20) var(--space-6);
- text-align: center;
- border: 1px dashed var(--color-border);
- border-radius: var(--border-radius-sm);
- display: flex;
- flex-direction: column;
- gap: var(--space-2);
-
- p {
- line-height: 1.5;
- font-size: var(--font-size-sm);
- color: var(--color-text-muted);
- }
- }
-
- [data-slot="create-form"] {
- display: flex;
- flex-direction: column;
- gap: var(--space-3);
- padding: var(--space-4);
- border: 1px solid var(--color-border);
- border-radius: var(--border-radius-sm);
-
- [data-slot="input-container"] {
- display: flex;
- flex-direction: column;
- gap: var(--space-1);
- }
-
- @media (max-width: 30rem) {
- gap: var(--space-2);
- }
-
- input {
- flex: 1;
- padding: var(--space-2) var(--space-3);
- border: 1px solid var(--color-border);
- border-radius: var(--border-radius-sm);
- background-color: var(--color-bg);
- color: var(--color-text);
- font-size: var(--font-size-sm);
- font-family: var(--font-mono);
-
- &:focus {
- outline: none;
- border-color: var(--color-accent);
- }
-
- &::placeholder {
- color: var(--color-text-disabled);
- }
- }
-
- [data-slot="form-actions"] {
- display: flex;
- gap: var(--space-2);
- }
-
- [data-slot="form-error"] {
- color: var(--color-danger);
- font-size: var(--font-size-sm);
- margin-top: var(--space-1);
- line-height: 1.4;
- }
- }
-
- [data-slot="api-keys-table"] {
- overflow-x: auto;
- }
-
- [data-slot="api-keys-table-element"] {
- width: 100%;
- border-collapse: collapse;
- font-size: var(--font-size-sm);
-
- thead {
- border-bottom: 1px solid var(--color-border);
- }
-
- th {
- padding: var(--space-3) var(--space-4);
- text-align: left;
- font-weight: normal;
- color: var(--color-text-muted);
- text-transform: uppercase;
- }
-
- td {
- padding: var(--space-3) var(--space-4);
- border-bottom: 1px solid var(--color-border-muted);
- color: var(--color-text-muted);
- font-family: var(--font-mono);
-
- &[data-slot="key-name"] {
- color: var(--color-text);
- font-family: var(--font-sans);
- font-weight: 500;
- }
-
- &[data-slot="key-value"] {
- font-family: var(--font-mono);
-
- button {
- display: flex;
- align-items: center;
- gap: var(--space-2);
- padding: var(--space-2) var(--space-3);
- font-size: var(--font-size-sm);
- font-weight: 400;
- border: none;
- background-color: transparent;
- color: var(--color-text-muted);
- font-family: var(--font-mono);
- border-radius: var(--border-radius-sm);
- cursor: pointer;
- transition: all 0.15s ease;
- text-transform: none;
-
- &:hover:not(:disabled) {
- background-color: var(--color-bg-surface);
- color: var(--color-text);
- }
-
- &:disabled {
- cursor: default;
- color: var(--color-text);
- }
-
- span {
- font-family: inherit;
- }
- }
- }
-
- &[data-slot="key-date"] {
- color: var(--color-text);
- }
-
- &[data-slot="key-actions"] {
- font-family: var(--font-sans);
- }
- }
-
- tbody tr {
- &:last-child td {
- border-bottom: none;
- }
- }
-
- @media (max-width: 40rem) {
- th,
- td {
- padding: var(--space-2) var(--space-3);
- font-size: var(--font-size-xs);
- }
-
- th {
- &:nth-child(3) /* Date */ {
- display: none;
- }
- }
-
- td {
- &:nth-child(3) /* Date */ {
- display: none;
- }
- }
- }
- }
-}
diff --git a/packages/cloud/app/src/component/workspace/key-section.tsx b/packages/cloud/app/src/component/workspace/key-section.tsx
deleted file mode 100644
index 4158ce793..000000000
--- a/packages/cloud/app/src/component/workspace/key-section.tsx
+++ /dev/null
@@ -1,182 +0,0 @@
-import { json, query, action, useParams, createAsync, useSubmission } from "@solidjs/router"
-import { createEffect, createSignal, For, Show } from "solid-js"
-import { IconCopy, IconCheck } from "~/component/icon"
-import { Key } from "@opencode/cloud-core/key.js"
-import { withActor } from "~/context/auth.withActor"
-import { createStore } from "solid-js/store"
-import { formatDateUTC, formatDateForTable } from "./common"
-import styles from "./key-section.module.css"
-
-const removeKey = action(async (form: FormData) => {
- "use server"
- const id = form.get("id")?.toString()
- if (!id) return { error: "ID is required" }
- const workspaceID = form.get("workspaceID")?.toString()
- if (!workspaceID) return { error: "Workspace ID is required" }
- return json(await withActor(() => Key.remove({ id }), workspaceID), { revalidate: listKeys.key })
-}, "key.remove")
-
-const createKey = action(async (form: FormData) => {
- "use server"
- const name = form.get("name")?.toString().trim()
- if (!name) return { error: "Name is required" }
- const workspaceID = form.get("workspaceID")?.toString()
- if (!workspaceID) return { error: "Workspace ID is required" }
- return json(
- await withActor(
- () =>
- Key.create({ name })
- .then((data) => ({ error: undefined, data }))
- .catch((e) => ({ error: e.message as string })),
- workspaceID,
- ),
- { revalidate: listKeys.key },
- )
-}, "key.create")
-
-const listKeys = query(async (workspaceID: string) => {
- "use server"
- return withActor(() => Key.list(), workspaceID)
-}, "key.list")
-
-export function KeyCreateForm() {
- const params = useParams()
- const submission = useSubmission(createKey)
- const [store, setStore] = createStore({ show: false })
-
- let input: HTMLInputElement
-
- createEffect(() => {
- if (!submission.pending && submission.result && !submission.result.error) {
- hide()
- }
- })
-
- function show() {
- // submission.clear() does not clear the result in some cases, ie.
- // 1. Create key with empty name => error shows
- // 2. Put in a key name and creates the key => form hides
- // 3. Click add key button again => form shows with the same error if
- // submission.clear() is called only once
- while (true) {
- submission.clear()
- if (!submission.result) break
- }
- setStore("show", true)
- input.focus()
- }
-
- function hide() {
- setStore("show", false)
- }
-
- return (
- <Show
- when={store.show}
- fallback={
- <button data-color="primary" onClick={() => show()}>
- Create API Key
- </button>
- }
- >
- <form action={createKey} method="post" data-slot="create-form">
- <div data-slot="input-container">
- <input ref={(r) => (input = r)} data-component="input" name="name" type="text" placeholder="Enter key name" />
- <Show when={submission.result && submission.result.error}>
- {(err) => <div data-slot="form-error">{err()}</div>}
- </Show>
- </div>
- <input type="hidden" name="workspaceID" value={params.id} />
- <div data-slot="form-actions">
- <button type="reset" data-color="ghost" onClick={() => hide()}>
- Cancel
- </button>
- <button type="submit" data-color="primary" disabled={submission.pending}>
- {submission.pending ? "Creating..." : "Create"}
- </button>
- </div>
- </form>
- </Show>
- )
-}
-
-export function KeySection() {
- const params = useParams()
- const keys = createAsync(() => listKeys(params.id))
-
- function formatKey(key: string) {
- if (key.length <= 11) return key
- return `${key.slice(0, 7)}...${key.slice(-4)}`
- }
-
- return (
- <section class={styles.root}>
- <div data-slot="section-title">
- <h2>API Keys</h2>
- <p>Manage your API keys for accessing opencode services.</p>
- </div>
- <KeyCreateForm />
- <div data-slot="api-keys-table">
- <Show
- when={keys()?.length}
- fallback={
- <div data-component="empty-state">
- <p>Create an opencode Gateway API key</p>
- </div>
- }
- >
- <table data-slot="api-keys-table-element">
- <thead>
- <tr>
- <th>Name</th>
- <th>Key</th>
- <th>Created</th>
- <th></th>
- </tr>
- </thead>
- <tbody>
- <For each={keys()!}>
- {(key) => {
- const [copied, setCopied] = createSignal(false)
- // const submission = useSubmission(removeKey, ([fd]) => fd.get("id")?.toString() === key.id)
- return (
- <tr>
- <td data-slot="key-name">{key.name}</td>
- <td data-slot="key-value">
- <button
- data-color="ghost"
- disabled={copied()}
- onClick={async () => {
- await navigator.clipboard.writeText(key.key)
- setCopied(true)
- setTimeout(() => setCopied(false), 1000)
- }}
- title="Copy API key"
- >
- <span>{formatKey(key.key)}</span>
- <Show when={copied()} fallback={<IconCopy style={{ width: "14px", height: "14px" }} />}>
- <IconCheck style={{ width: "14px", height: "14px" }} />
- </Show>
- </button>
- </td>
- <td data-slot="key-date" title={formatDateUTC(key.timeCreated)}>
- {formatDateForTable(key.timeCreated)}
- </td>
- <td data-slot="key-actions">
- <form action={removeKey} method="post">
- <input type="hidden" name="id" value={key.id} />
- <input type="hidden" name="workspaceID" value={params.id} />
- <button data-color="ghost">Delete</button>
- </form>
- </td>
- </tr>
- )
- }}
- </For>
- </tbody>
- </table>
- </Show>
- </div>
- </section>
- )
-}
diff --git a/packages/cloud/app/src/component/workspace/monthly-limit-section.module.css b/packages/cloud/app/src/component/workspace/monthly-limit-section.module.css
deleted file mode 100644
index 02de058e4..000000000
--- a/packages/cloud/app/src/component/workspace/monthly-limit-section.module.css
+++ /dev/null
@@ -1,102 +0,0 @@
-.root {
- [data-slot="section-content"] {
- display: flex;
- flex-direction: column;
- gap: var(--space-3);
- }
-
- [data-slot="balance"] {
- display: flex;
- flex-direction: column;
- gap: var(--space-3);
- padding: var(--space-4);
- border: 1px solid var(--color-border);
- border-radius: var(--border-radius-sm);
- min-width: 15rem;
- width: fit-content;
-
- @media (max-width: 30rem) {
- width: 100%;
- }
-
- [data-slot="amount"] {
- padding: var(--space-3-5) var(--space-4);
- background-color: var(--color-bg-surface);
- border-radius: var(--border-radius-sm);
- display: flex;
- align-items: baseline;
- gap: var(--space-1);
- justify-content: flex-end;
-
- [data-slot="currency"] {
- position: relative;
- bottom: 2px;
- font-size: var(--font-size-lg);
- color: var(--color-text-muted);
- font-weight: 400;
- }
-
- [data-slot="value"] {
- font-size: var(--font-size-3xl);
- font-weight: 500;
- color: var(--color-text);
- }
- }
-
- [data-slot="create-form"] {
- display: flex;
- flex-direction: column;
- gap: var(--space-3);
- margin-top: var(--space-1);
-
- [data-slot="input-container"] {
- display: flex;
- flex-direction: column;
- gap: var(--space-1);
- }
-
- @media (max-width: 30rem) {
- gap: var(--space-2);
- }
-
- input {
- flex: 1;
- padding: var(--space-2) var(--space-3);
- border: 1px solid var(--color-border);
- border-radius: var(--border-radius-sm);
- background-color: var(--color-bg);
- color: var(--color-text);
- font-size: var(--font-size-sm);
- font-family: var(--font-mono);
-
- &:focus {
- outline: none;
- border-color: var(--color-accent);
- }
-
- &::placeholder {
- color: var(--color-text-disabled);
- }
- }
-
- [data-slot="form-actions"] {
- display: flex;
- gap: var(--space-2);
- justify-content: flex-end;
- }
-
- [data-slot="form-error"] {
- color: var(--color-danger);
- font-size: var(--font-size-sm);
- line-height: 1.4;
- }
- }
- }
-
- [data-slot="usage-status"] {
- font-size: var(--font-size-sm);
- color: var(--color-text-secondary);
- margin: 0;
- line-height: 1.4;
- }
-}
diff --git a/packages/cloud/app/src/component/workspace/monthly-limit-section.tsx b/packages/cloud/app/src/component/workspace/monthly-limit-section.tsx
deleted file mode 100644
index 5c1077ab1..000000000
--- a/packages/cloud/app/src/component/workspace/monthly-limit-section.tsx
+++ /dev/null
@@ -1,139 +0,0 @@
-import { json, query, action, useParams, createAsync, useSubmission } from "@solidjs/router"
-import { createEffect, Show } from "solid-js"
-import { createStore } from "solid-js/store"
-import { withActor } from "~/context/auth.withActor"
-import { Billing } from "@opencode/cloud-core/billing.js"
-import styles from "./monthly-limit-section.module.css"
-
-const getBillingInfo = query(async (workspaceID: string) => {
- "use server"
- return withActor(async () => {
- return await Billing.get()
- }, workspaceID)
-}, "billing.get")
-
-const setMonthlyLimit = action(async (form: FormData) => {
- "use server"
- const limit = form.get("limit")?.toString()
- if (!limit) return { error: "Limit is required." }
- const numericLimit = parseInt(limit)
- if (numericLimit < 0) return { error: "Set a valid monthly limit." }
- const workspaceID = form.get("workspaceID")?.toString()
- if (!workspaceID) return { error: "Workspace ID is required." }
- return json(
- await withActor(
- () =>
- Billing.setMonthlyLimit(numericLimit)
- .then((data) => ({ error: undefined, data }))
- .catch((e) => ({ error: e.message as string })),
- workspaceID,
- ),
- { revalidate: getBillingInfo.key },
- )
-}, "billing.setMonthlyLimit")
-
-export function MonthlyLimitSection() {
- const params = useParams()
- const submission = useSubmission(setMonthlyLimit)
- const [store, setStore] = createStore({ show: false })
- const balanceInfo = createAsync(() => getBillingInfo(params.id))
-
- let input: HTMLInputElement
-
- createEffect(() => {
- if (!submission.pending && submission.result && !submission.result.error) {
- hide()
- }
- })
-
- function show() {
- // submission.clear() does not clear the result in some cases, ie.
- // 1. Create key with empty name => error shows
- // 2. Put in a key name and creates the key => form hides
- // 3. Click add key button again => form shows with the same error if
- // submission.clear() is called only once
- while (true) {
- submission.clear()
- if (!submission.result) break
- }
- setStore("show", true)
- input.focus()
- }
-
- function hide() {
- setStore("show", false)
- }
-
- return (
- <section class={styles.root}>
- <div data-slot="section-title">
- <h2>Monthly Limit</h2>
- <p>Set a monthly spending limit for your account.</p>
- </div>
- <div data-slot="section-content">
- <div data-slot="balance">
- <div data-slot="amount">
- {balanceInfo()?.monthlyLimit ? <span data-slot="currency">$</span> : null}
- <span data-slot="value">{balanceInfo()?.monthlyLimit ?? "-"}</span>
- </div>
- <Show
- when={!store.show}
- fallback={
- <form action={setMonthlyLimit} method="post" data-slot="create-form">
- <div data-slot="input-container">
- <input
- required
- ref={(r) => (input = r)}
- data-component="input"
- name="limit"
- type="number"
- placeholder="50"
- />
- <Show when={submission.result && submission.result.error}>
- {(err) => <div data-slot="form-error">{err()}</div>}
- </Show>
- </div>
- <input type="hidden" name="workspaceID" value={params.id} />
- <div data-slot="form-actions">
- <button type="reset" data-color="ghost" onClick={() => hide()}>
- Cancel
- </button>
- <button type="submit" data-color="primary" disabled={submission.pending}>
- {submission.pending ? "Setting..." : "Set"}
- </button>
- </div>
- </form>
- }
- >
- <button data-color="primary" onClick={() => show()}>
- {balanceInfo()?.monthlyLimit ? "Edit Limit" : "Set Limit"}
- </button>
- </Show>
- </div>
- <Show when={balanceInfo()?.monthlyLimit} fallback={<p data-slot="usage-status">No spending limit set.</p>}>
- <p data-slot="usage-status">
- Current usage for {new Date().toLocaleDateString("en-US", { month: "long", timeZone: "UTC" })} is $
- {(() => {
- const dateLastUsed = balanceInfo()?.timeMonthlyUsageUpdated
- if (!dateLastUsed) return "0"
-
- const current = new Date().toLocaleDateString("en-US", {
- year: "numeric",
- month: "long",
- timeZone: "UTC",
- })
- const lastUsed = dateLastUsed.toLocaleDateString("en-US", {
- year: "numeric",
- month: "long",
- timeZone: "UTC",
- })
- if (current !== lastUsed) return "0"
- return ((balanceInfo()?.monthlyUsage ?? 0) / 100000000).toFixed(2)
- })()}
- .
- </p>
- </Show>
- </div>
- </section>
- )
-}
diff --git a/packages/cloud/app/src/component/workspace/new-user-section.module.css b/packages/cloud/app/src/component/workspace/new-user-section.module.css
deleted file mode 100644
index 2edc7cc14..000000000
--- a/packages/cloud/app/src/component/workspace/new-user-section.module.css
+++ /dev/null
@@ -1,163 +0,0 @@
-.root {
- display: flex;
- flex-direction: column;
- gap: var(--space-8);
- padding: var(--space-6);
- background-color: var(--color-bg-surface);
- border: 1px dashed var(--color-border);
- border-radius: var(--border-radius-sm);
-
- @media (max-width: 30rem) {
- gap: var(--space-8);
- padding: var(--space-4);
- }
-
- [data-component="feature-grid"] {
- display: grid;
- grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
- gap: var(--space-6);
-
- @media (max-width: 30rem) {
- grid-template-columns: 1fr;
- gap: var(--space-4);
- }
-
- [data-slot="feature"] {
- display: flex;
- flex-direction: column;
- gap: var(--space-2);
- padding: var(--space-4);
- border: 1px solid var(--color-border);
- border-radius: var(--border-radius-sm);
-
- h3 {
- font-size: var(--font-size-sm);
- font-weight: 600;
- margin: 0;
- color: var(--color-text);
- text-transform: uppercase;
- letter-spacing: -0.025rem;
- }
-
- p {
- font-size: var(--font-size-sm);
- line-height: 1.5;
- margin: 0;
- color: var(--color-text-muted);
- }
- }
- }
-
- [data-component="api-key-highlight"] {
- display: flex;
- flex-direction: column;
- gap: var(--space-6);
-
- [data-slot="section-title"] {
- display: flex;
- flex-direction: column;
- gap: var(--space-1);
-
- h2 {
- font-size: var(--font-size-md);
- font-weight: 600;
- line-height: 1.2;
- letter-spacing: -0.03125rem;
- margin: 0;
- color: var(--color-text-secondary);
- text-transform: uppercase;
-
- @media (max-width: 30rem) {
- font-size: var(--font-size-md);
- }
- }
- }
-
- [data-slot="key-display"] {
- display: flex;
- flex-direction: column;
- gap: var(--space-3);
-
- [data-slot="key-container"] {
- display: flex;
- gap: var(--space-3);
- padding: var(--space-4);
- border: 2px solid var(--color-accent);
- border-radius: var(--border-radius-sm);
- align-items: center;
-
- @media (max-width: 40rem) {
- flex-direction: column;
- gap: var(--space-3);
- align-items: stretch;
- }
-
- [data-slot="key-value"] {
- flex: 1;
- font-family: var(--font-mono);
- font-size: var(--font-size-sm);
- color: var(--color-text);
- background-color: var(--color-bg);
- padding: var(--space-3);
- border-radius: var(--border-radius-sm);
- border: 1px solid var(--color-border);
- word-break: break-all;
- line-height: 1.4;
-
- @media (max-width: 40rem) {
- font-size: var(--font-size-xs);
- padding: var(--space-2-5);
- }
- }
-
- button {
- display: flex;
- align-items: center;
- gap: var(--space-2);
- padding: var(--space-3) var(--space-4);
- font-size: var(--font-size-sm);
- font-weight: 500;
- white-space: nowrap;
- min-width: 130px;
-
- @media (max-width: 40rem) {
- justify-content: center;
- padding: var(--space-2-5) var(--space-3);
- font-size: var(--font-size-xs);
- min-width: 96px;
- }
- }
- }
- }
- }
-
- [data-component="next-steps"] {
- display: flex;
- flex-direction: column;
- gap: var(--space-6);
-
- ol {
- margin: 0;
- padding-left: 0;
- display: flex;
- flex-direction: column;
- gap: var(--space-2);
- list-style-position: inside;
-
- li {
- font-size: var(--font-size-md);
- line-height: 1.5;
- color: var(--color-text-secondary);
-
- code {
- font-family: var(--font-mono);
- font-size: var(--font-size-sm);
- padding: var(--space-1) var(--space-2);
- border: 1px solid var(--color-border);
- border-radius: var(--border-radius-sm);
- color: var(--color-text);
- }
- }
- }
- }
-}
diff --git a/packages/cloud/app/src/component/workspace/new-user-section.tsx b/packages/cloud/app/src/component/workspace/new-user-section.tsx
deleted file mode 100644
index 6e031e371..000000000
--- a/packages/cloud/app/src/component/workspace/new-user-section.tsx
+++ /dev/null
@@ -1,97 +0,0 @@
-import { query, useParams, createAsync } from "@solidjs/router"
-import { createMemo, createSignal, Show } from "solid-js"
-import { IconCopy, IconCheck } from "~/component/icon"
-import { Key } from "@opencode/cloud-core/key.js"
-import { Billing } from "@opencode/cloud-core/billing.js"
-import { withActor } from "~/context/auth.withActor"
-import styles from "./new-user-section.module.css"
-
-const getUsageInfo = query(async (workspaceID: string) => {
- "use server"
- return withActor(async () => {
- return await Billing.usages()
- }, workspaceID)
-}, "usage.list")
-
-const listKeys = query(async (workspaceID: string) => {
- "use server"
- return withActor(() => Key.list(), workspaceID)
-}, "key.list")
-
-export function NewUserSection() {
- const params = useParams()
- const [copiedKey, setCopiedKey] = createSignal(false)
- const keys = createAsync(() => listKeys(params.id))
- const usage = createAsync(() => getUsageInfo(params.id))
- const isNew = createMemo(() => {
- const keysList = keys()
- const usageList = usage()
- return keysList?.length === 1 && (!usageList || usageList.length === 0)
- })
- const defaultKey = createMemo(() => keys()?.at(-1)?.key)
-
- return (
- <Show when={isNew()}>
- <div class={styles.root}>
- <div data-component="feature-grid">
- <div data-slot="feature">
- <h3>Tested & Verified Models</h3>
- <p>We've benchmarked and tested models specifically for coding agents to ensure the best performance.</p>
- </div>
- <div data-slot="feature">
- <h3>Highest Quality</h3>
- <p>Access models configured for optimal performance - no downgrades or routing to cheaper providers.</p>
- </div>
- <div data-slot="feature">
- <h3>No Lock-in</h3>
- <p>Use Zen with any coding agent, and continue using other providers with opencode whenever you want.</p>
- </div>
- </div>
-
- <div data-component="api-key-highlight">
- <Show when={defaultKey()}>
- <div data-slot="key-display">
- <div data-slot="key-container">
- <code data-slot="key-value">{defaultKey()}</code>
- <button
- data-color="primary"
- disabled={copiedKey()}
- onClick={async () => {
- await navigator.clipboard.writeText(defaultKey() ?? "")
- setCopiedKey(true)
- setTimeout(() => setCopiedKey(false), 2000)
- }}
- title="Copy API key"
- >
- <Show
- when={copiedKey()}
- fallback={
- <>
- <IconCopy style={{ width: "16px", height: "16px" }} /> Copy Key
- </>
- }
- >
- <IconCheck style={{ width: "16px", height: "16px" }} /> Copied!
- </Show>
- </button>
- </div>
- </div>
- </Show>
- </div>
-
- <div data-component="next-steps">
- <ol>
- <li>Enable billing</li>
- <li>
- Run <code>opencode auth login</code> and select opencode
- </li>
- <li>Paste your API key</li>
- <li>
- Start opencode and run <code>/models</code> to select a model
- </li>
- </ol>
- </div>
- </div>
- </Show>
- )
-}
diff --git a/packages/cloud/app/src/component/workspace/payment-section.module.css b/packages/cloud/app/src/component/workspace/payment-section.module.css
deleted file mode 100644
index ea8e2ed42..000000000
--- a/packages/cloud/app/src/component/workspace/payment-section.module.css
+++ /dev/null
@@ -1,72 +0,0 @@
-.root {
- [data-slot="payments-table"] {
- overflow-x: auto;
- }
-
- [data-slot="payments-table-element"] {
- width: 100%;
- border-collapse: collapse;
- font-size: var(--font-size-sm);
-
- thead {
- border-bottom: 1px solid var(--color-border);
- }
-
- th {
- padding: var(--space-3) var(--space-4);
- text-align: left;
- font-weight: normal;
- color: var(--color-text-muted);
- text-transform: uppercase;
- }
-
- td {
- padding: var(--space-3) var(--space-4);
- border-bottom: 1px solid var(--color-border-muted);
- color: var(--color-text-muted);
- font-family: var(--font-mono);
-
- &[data-slot="payment-date"] {
- color: var(--color-text);
- }
-
- &[data-slot="payment-id"] {
- font-family: var(--font-mono);
- font-weight: 400;
- color: var(--color-text-muted);
- max-width: 200px;
- word-break: break-word;
- }
-
- &[data-slot="payment-amount"] {
- color: var(--color-text);
- }
- }
-
- tbody tr {
- &:last-child td {
- border-bottom: none;
- }
- }
-
- @media (max-width: 40rem) {
- th,
- td {
- padding: var(--space-2) var(--space-3);
- font-size: var(--font-size-xs);
- }
-
- th {
- &:nth-child(2) /* Payment ID */ {
- display: none;
- }
- }
-
- td {
- &:nth-child(2) /* Payment ID */ {
- display: none;
- }
- }
- }
- }
-}
diff --git a/packages/cloud/app/src/component/workspace/payment-section.tsx b/packages/cloud/app/src/component/workspace/payment-section.tsx
deleted file mode 100644
index 8cdceebc3..000000000
--- a/packages/cloud/app/src/component/workspace/payment-section.tsx
+++ /dev/null
@@ -1,113 +0,0 @@
-import { Billing } from "@opencode/cloud-core/billing.js"
-import { query, action, useParams, createAsync, useAction } from "@solidjs/router"
-import { For } from "solid-js"
-import { withActor } from "~/context/auth.withActor"
-import { formatDateUTC, formatDateForTable } from "./common"
-import styles from "./payment-section.module.css"
-
-const getPaymentsInfo = query(async (workspaceID: string) => {
- "use server"
- return withActor(async () => {
- return await Billing.payments()
- }, workspaceID)
-}, "payment.list")
-
-const downloadReceipt = action(async (workspaceID: string, paymentID: string) => {
- "use server"
- return withActor(() => Billing.generateReceiptUrl({ paymentID }), workspaceID)
-}, "receipt.download")
-
-export function PaymentSection() {
- const params = useParams()
- // ORIGINAL CODE - COMMENTED OUT FOR TESTING
- const payments = createAsync(() => getPaymentsInfo(params.id))
- const downloadReceiptAction = useAction(downloadReceipt)
-
- // DUMMY DATA FOR TESTING
- // const payments = () => [
- // {
- // id: "pi_3QK1x2FT9vXn4A6r1234567890",
- // paymentID: "pi_3QK1x2FT9vXn4A6r1234567890",
- // timeCreated: new Date(Date.now() - 86400000 * 1).toISOString(), // 1 day ago
- // amount: 2100000000, // $21.00 ($20 + $1 fee)
- // },
- // {
- // id: "pi_3QJ8k7FT9vXn4A6r0987654321",
- // paymentID: "pi_3QJ8k7FT9vXn4A6r0987654321",
- // timeCreated: new Date(Date.now() - 86400000 * 15).toISOString(), // 15 days ago
- // amount: 2100000000, // $21.00
- // },
- // {
- // id: "pi_3QI5m1FT9vXn4A6r5678901234",
- // paymentID: "pi_3QI5m1FT9vXn4A6r5678901234",
- // timeCreated: new Date(Date.now() - 86400000 * 32).toISOString(), // 32 days ago
- // amount: 2100000000, // $21.00
- // },
- // {
- // id: "pi_3QH2n9FT9vXn4A6r3456789012",
- // paymentID: "pi_3QH2n9FT9vXn4A6r3456789012",
- // timeCreated: new Date(Date.now() - 86400000 * 47).toISOString(), // 47 days ago
- // amount: 2100000000, // $21.00
- // },
- // {
- // id: "pi_3QG7p4FT9vXn4A6r7890123456",
- // paymentID: "pi_3QG7p4FT9vXn4A6r7890123456",
- // timeCreated: new Date(Date.now() - 86400000 * 63).toISOString(), // 63 days ago
- // amount: 2100000000, // $21.00
- // },
- // ]
-
- return (
- payments() &&
- payments()!.length > 0 && (
- <section class={styles.root}>
- <div data-slot="section-title">
- <h2>Payments History</h2>
- <p>Recent payment transactions.</p>
- </div>
- <div data-slot="payments-table">
- <table data-slot="payments-table-element">
- <thead>
- <tr>
- <th>Date</th>
- <th>Payment ID</th>
- <th>Amount</th>
- <th>Receipt</th>
- </tr>
- </thead>
- <tbody>
- <For each={payments()!}>
- {(payment) => {
- const date = new Date(payment.timeCreated)
- return (
- <tr>
- <td data-slot="payment-date" title={formatDateUTC(date)}>
- {formatDateForTable(date)}
- </td>
- <td data-slot="payment-id">{payment.id}</td>
- <td data-slot="payment-amount">${((payment.amount ?? 0) / 100000000).toFixed(2)}</td>
- <td data-slot="payment-receipt">
- <button
- onClick={async () => {
- const receiptUrl = await downloadReceiptAction(params.id, payment.paymentID!)
- if (receiptUrl) {
- window.open(receiptUrl, "_blank")
- }
- }}
- data-slot="receipt-button"
- style="cursor: pointer;"
- >
- view
- </button>
- </td>
- </tr>
- )
- }}
- </For>
- </tbody>
- </table>
- </div>
- </section>
- )
- )
-}
diff --git a/packages/cloud/app/src/component/workspace/usage-section.module.css b/packages/cloud/app/src/component/workspace/usage-section.module.css
deleted file mode 100644
index 1a772ba87..000000000
--- a/packages/cloud/app/src/component/workspace/usage-section.module.css
+++ /dev/null
@@ -1,88 +0,0 @@
-.root {
- [data-component="empty-state"] {
- padding: var(--space-20) var(--space-6);
- text-align: center;
- border: 1px dashed var(--color-border);
- border-radius: var(--border-radius-sm);
- display: flex;
- flex-direction: column;
- gap: var(--space-2);
-
- p {
- line-height: 1.5;
- font-size: var(--font-size-sm);
- color: var(--color-text-muted);
- }
- }
-
- [data-slot="usage-table"] {
- overflow-x: auto;
- }
-
- [data-slot="usage-table-element"] {
- width: 100%;
- border-collapse: collapse;
- font-size: var(--font-size-sm);
-
- thead {
- border-bottom: 1px solid var(--color-border);
- }
-
- th {
- padding: var(--space-3) var(--space-4);
- text-align: left;
- font-weight: normal;
- color: var(--color-text-muted);
- text-transform: uppercase;
- }
-
- td {
- padding: var(--space-3) var(--space-4);
- border-bottom: 1px solid var(--color-border-muted);
- color: var(--color-text-muted);
- font-family: var(--font-mono);
-
- &[data-slot="usage-date"] {
- color: var(--color-text);
- }
-
- &[data-slot="usage-model"] {
- font-family: var(--font-sans);
- font-weight: 400;
- color: var(--color-text-secondary);
- max-width: 200px;
- word-break: break-word;
- }
-
- &[data-slot="usage-cost"] {
- color: var(--color-text);
- }
- }
-
- tbody tr {
- &:last-child td {
- border-bottom: none;
- }
- }
-
- @media (max-width: 40rem) {
- th,
- td {
- padding: var(--space-2) var(--space-3);
- font-size: var(--font-size-xs);
- }
-
- th {
- &:nth-child(2) /* Model */ {
- display: none;
- }
- }
-
- td {
- &:nth-child(2) /* Model */ {
- display: none;
- }
- }
- }
- }
-}
diff --git a/packages/cloud/app/src/component/workspace/usage-section.tsx b/packages/cloud/app/src/component/workspace/usage-section.tsx
deleted file mode 100644
index 5d3d3b6c3..000000000
--- a/packages/cloud/app/src/component/workspace/usage-section.tsx
+++ /dev/null
@@ -1,128 +0,0 @@
-import { Billing } from "@opencode/cloud-core/billing.js"
-import { query, useParams, createAsync } from "@solidjs/router"
-import { createMemo, For, Show } from "solid-js"
-import { formatDateUTC, formatDateForTable } from "./common"
-import { withActor } from "~/context/auth.withActor"
-import styles from "./usage-section.module.css"
-
-const getUsageInfo = query(async (workspaceID: string) => {
- "use server"
- return withActor(async () => {
- return await Billing.usages()
- }, workspaceID)
-}, "usage.list")
-
-export function UsageSection() {
- const params = useParams()
- // ORIGINAL CODE - COMMENTED OUT FOR TESTING
- const usage = createAsync(() => getUsageInfo(params.id))
-
- // DUMMY DATA FOR TESTING
- // const usage = () => [
- // {
- // timeCreated: new Date(Date.now() - 86400000 * 0).toISOString(), // Today
- // model: "claude-3-5-sonnet-20241022",
- // inputTokens: 1247,
- // outputTokens: 423,
- // cost: 125400000, // $1.254
- // },
- // {
- // timeCreated: new Date(Date.now() - 86400000 * 0.5).toISOString(), // 12 hours ago
- // model: "claude-3-haiku-20240307",
- // inputTokens: 892,
- // outputTokens: 156,
- // cost: 23500000, // $0.235
- // },
- // {
- // timeCreated: new Date(Date.now() - 86400000 * 1).toISOString(), // Yesterday
- // model: "claude-3-5-sonnet-20241022",
- // inputTokens: 2134,
- // outputTokens: 687,
- // cost: 234700000, // $2.347
- // },
- // {
- // timeCreated: new Date(Date.now() - 86400000 * 1.3).toISOString(), // 1.3 days ago
- // model: "gpt-4o-mini",
- // inputTokens: 567,
- // outputTokens: 234,
- // cost: 8900000, // $0.089
- // },
- // {
- // timeCreated: new Date(Date.now() - 86400000 * 2).toISOString(), // 2 days ago
- // model: "claude-3-opus-20240229",
- // inputTokens: 1893,
- // outputTokens: 945,
- // cost: 445600000, // $4.456
- // },
- // {
- // timeCreated: new Date(Date.now() - 86400000 * 2.7).toISOString(), // 2.7 days ago
- // model: "gpt-4o",
- // inputTokens: 1456,
- // outputTokens: 532,
- // cost: 156800000, // $1.568
- // },
- // {
- // timeCreated: new Date(Date.now() - 86400000 * 3).toISOString(), // 3 days ago
- // model: "claude-3-haiku-20240307",
- // inputTokens: 634,
- // outputTokens: 89,
- // cost: 12300000, // $0.123
- // },
- // {
- // timeCreated: new Date(Date.now() - 86400000 * 4).toISOString(), // 4 days ago
- // model: "claude-3-5-sonnet-20241022",
- // inputTokens: 3245,
- // outputTokens: 1123,
- // cost: 387200000, // $3.872
- // },
- // ]
-
- return (
- <section class={styles.root}>
- <div data-slot="section-title">
- <h2>Usage History</h2>
- <p>Recent API usage and costs.</p>
- </div>
- <div data-slot="usage-table">
- <Show
- when={usage() && usage()!.length > 0}
- fallback={
- <div data-component="empty-state">
- <p>Make your first API call to get started.</p>
- </div>
- }
- >
- <table data-slot="usage-table-element">
- <thead>
- <tr>
- <th>Date</th>
- <th>Model</th>
- <th>Input</th>
- <th>Output</th>
- <th>Cost</th>
- </tr>
- </thead>
- <tbody>
- <For each={usage()!}>
- {(usage) => {
- const date = createMemo(() => new Date(usage.timeCreated))
- return (
- <tr>
- <td data-slot="usage-date" title={formatDateUTC(date())}>
- {formatDateForTable(date())}
- </td>
- <td data-slot="usage-model">{usage.model}</td>
- <td data-slot="usage-tokens">{usage.inputTokens}</td>
- <td data-slot="usage-tokens">{usage.outputTokens}</td>
- <td data-slot="usage-cost">${((usage.cost ?? 0) / 100000000).toFixed(4)}</td>
- </tr>
- )
- }}
- </For>
- </tbody>
- </table>
- </Show>
- </div>
- </section>
- )
-}
diff --git a/packages/cloud/app/src/context/auth.session.ts b/packages/cloud/app/src/context/auth.session.ts
deleted file mode 100644
index 609bc364b..000000000
--- a/packages/cloud/app/src/context/auth.session.ts
+++ /dev/null
@@ -1,23 +0,0 @@
-import { useSession } from "vinxi/http"
-
-export interface AuthSession {
- account?: Record<
- string,
- {
- id: string
- email: string
- }
- >
- current?: string
-}
-
-export function useAuthSession() {
- return useSession<AuthSession>({
- password: "0".repeat(32),
- name: "auth",
- cookie: {
- secure: false,
- httpOnly: true,
- },
- })
-}
diff --git a/packages/cloud/app/src/context/auth.ts b/packages/cloud/app/src/context/auth.ts
deleted file mode 100644
index e08d965b8..000000000
--- a/packages/cloud/app/src/context/auth.ts
+++ /dev/null
@@ -1,83 +0,0 @@
-import { getRequestEvent } from "solid-js/web"
-import { and, Database, eq, inArray } from "@opencode/cloud-core/drizzle/index.js"
-import { WorkspaceTable } from "@opencode/cloud-core/schema/workspace.sql.js"
-import { UserTable } from "@opencode/cloud-core/schema/user.sql.js"
-import { redirect } from "@solidjs/router"
-import { AccountTable } from "@opencode/cloud-core/schema/account.sql.js"
-import { Actor } from "@opencode/cloud-core/actor.js"
-
-import { createClient } from "@openauthjs/openauth/client"
-import { useAuthSession } from "./auth.session"
-
-export const AuthClient = createClient({
- clientID: "app",
- issuer: import.meta.env.VITE_AUTH_URL,
-})
-
-export const getActor = async (workspace?: string): Promise<Actor.Info> => {
- "use server"
- const evt = getRequestEvent()
- if (!evt) throw new Error("No request event")
- if (evt.locals.actor) return evt.locals.actor
- evt.locals.actor = (async () => {
- const auth = await useAuthSession()
- if (!workspace) {
- const account = auth.data.account ?? {}
- const current = account[auth.data.current ?? ""]
- if (current) {
- return {
- type: "account",
- properties: {
- email: current.email,
- accountID: current.id,
- },
- }
- }
- if (Object.keys(account).length > 0) {
- const current = Object.values(account)[0]
- await auth.update((val) => ({
- ...val,
- current: current.id,
- }))
- return {
- type: "account",
- properties: {
- email: current.email,
- accountID: current.id,
- },
- }
- }
- return {
- type: "public",
- properties: {},
- }
- }
- const accounts = Object.keys(auth.data.account ?? {})
- if (accounts.length) {
- const result = await Database.transaction(async (tx) => {
- return await tx
- .select({
- user: UserTable,
- })
- .from(AccountTable)
- .innerJoin(UserTable, and(eq(UserTable.email, AccountTable.email)))
- .innerJoin(WorkspaceTable, eq(WorkspaceTable.id, UserTable.workspaceID))
- .where(and(inArray(AccountTable.id, accounts), eq(WorkspaceTable.id, workspace)))
- .limit(1)
- .execute()
- .then((x) => x[0])
- })
- if (result) {
- return {
- type: "user",
- properties: {
- userID: result.user.id,
- workspaceID: result.user.workspaceID,
- },
- }
- }
- }
- throw redirect("/auth/authorize")
- })()
- return evt.locals.actor
-}
diff --git a/packages/cloud/app/src/context/auth.withActor.ts b/packages/cloud/app/src/context/auth.withActor.ts
deleted file mode 100644
index 4cfd5c3e0..000000000
--- a/packages/cloud/app/src/context/auth.withActor.ts
+++ /dev/null
@@ -1,7 +0,0 @@
-import { Actor } from "@opencode/cloud-core/actor.js"
-import { getActor } from "./auth"
-
-export async function withActor<T>(fn: () => T, workspace?: string) {
- const actor = await getActor(workspace)
- return Actor.provide(actor.type, actor.properties, fn)
-}
diff --git a/packages/cloud/app/src/entry-client.tsx b/packages/cloud/app/src/entry-client.tsx
deleted file mode 100644
index 642deacf7..000000000
--- a/packages/cloud/app/src/entry-client.tsx
+++ /dev/null
@@ -1,4 +0,0 @@
-// @refresh reload
-import { mount, StartClient } from "@solidjs/start/client"
-
-mount(() => <StartClient />, document.getElementById("app")!)
diff --git a/packages/cloud/app/src/entry-server.tsx b/packages/cloud/app/src/entry-server.tsx
deleted file mode 100644
index d5fca6aa5..000000000
--- a/packages/cloud/app/src/entry-server.tsx
+++ /dev/null
@@ -1,28 +0,0 @@
-// @refresh reload
-import { createHandler, StartServer } from "@solidjs/start/server"
-
-export default createHandler(
- () => (
- <StartServer
- document={({ assets, children, scripts }) => (
- <html lang="en">
- <head>
- <meta charset="utf-8" />
- <meta name="viewport" content="width=device-width, initial-scale=1" />
- <link rel="icon" href="/favicon.svg" />
- <meta property="og:image" content="/social-share.png" />
- <meta property="twitter:image" content="/social-share.png" />
- {assets}
- </head>
- <body>
- <div id="app">{children}</div>
- {scripts}
- </body>
- </html>
- )}
- />
- ),
- {
- mode: "async",
- },
-)
diff --git a/packages/cloud/app/src/global.d.ts b/packages/cloud/app/src/global.d.ts
deleted file mode 100644
index dc6f10c22..000000000
--- a/packages/cloud/app/src/global.d.ts
+++ /dev/null
@@ -1 +0,0 @@
-/// <reference types="@solidjs/start/env" />
diff --git a/packages/cloud/app/src/middleware.ts b/packages/cloud/app/src/middleware.ts
deleted file mode 100644
index b49473cbe..000000000
--- a/packages/cloud/app/src/middleware.ts
+++ /dev/null
@@ -1,5 +0,0 @@
-import { defineMiddleware } from "vinxi/http"
-
-export default defineMiddleware({
- onBeforeResponse() {},
-})
diff --git a/packages/cloud/app/src/routes/[...404].css b/packages/cloud/app/src/routes/[...404].css
deleted file mode 100644
index 1edbd0a5a..000000000
--- a/packages/cloud/app/src/routes/[...404].css
+++ /dev/null
@@ -1,130 +0,0 @@
-[data-page="not-found"] {
- --color-text: hsl(224, 10%, 10%);
- --color-text-secondary: hsl(224, 7%, 46%);
- --color-text-dimmed: hsl(224, 6%, 63%);
- --color-text-inverted: hsl(0, 0%, 100%);
-
- --color-border: hsl(224, 6%, 77%);
-}
-
-[data-page="not-found"] {
- @media (prefers-color-scheme: dark) {
- --color-text: hsl(0, 0%, 100%);
- --color-text-secondary: hsl(224, 6%, 66%);
- --color-text-dimmed: hsl(224, 7%, 46%);
- --color-text-inverted: hsl(224, 10%, 10%);
-
- --color-border: hsl(224, 6%, 36%);
- }
-}
-
-[data-page="not-found"] {
- --padding: 3rem;
- --vertical-padding: 1.5rem;
- --heading-font-size: 1.375rem;
-
- @media (max-width: 30rem) {
- --padding: 1rem;
- --vertical-padding: 0.75rem;
- --heading-font-size: 1rem;
- }
-
- font-family: var(--font-mono);
- color: var(--color-text);
- padding: calc(var(--padding) + 1rem);
- min-height: 100vh;
- display: flex;
- align-items: center;
- justify-content: center;
-
- a {
- color: var(--color-text);
- text-decoration: underline;
- text-underline-offset: var(--space-0-75);
- text-decoration-thickness: 1px;
- }
-
- [data-component="content"] {
- max-width: 40rem;
- width: 100%;
- border: 1px solid var(--color-border);
- }
-
- [data-component="top"] {
- padding: var(--padding);
- display: flex;
- flex-direction: column;
- align-items: center;
- gap: calc(var(--vertical-padding) / 2);
- text-align: center;
-
- [data-slot="logo-link"] {
- text-decoration: none;
- }
-
- img {
- height: auto;
- width: clamp(200px, 85vw, 400px);
- }
-
- [data-slot="logo dark"] {
- display: none;
- }
-
- @media (prefers-color-scheme: dark) {
- [data-slot="logo light"] {
- display: none;
- }
- [data-slot="logo dark"] {
- display: block;
- }
- }
-
- [data-slot="title"] {
- line-height: 1.25;
- font-weight: 500;
- text-align: center;
- font-size: var(--heading-font-size);
- color: var(--color-text);
- text-transform: uppercase;
- margin: 0;
- }
- }
-
- [data-component="actions"] {
- border-top: 1px solid var(--color-border);
- display: flex;
-
- [data-slot="action"] {
- flex: 1;
- text-align: center;
- line-height: 1.4;
- padding: var(--vertical-padding) 1rem;
- text-transform: uppercase;
- font-size: 1rem;
-
- a {
- display: block;
- width: 100%;
- height: 100%;
- color: var(--color-text);
- text-decoration: underline;
- text-underline-offset: var(--space-0-75);
- text-decoration-thickness: 1px;
- }
- }
-
- [data-slot="action"] + [data-slot="action"] {
- border-left: 1px solid var(--color-border);
- }
-
- @media (max-width: 40rem) {
- flex-direction: column;
-
- [data-slot="action"] + [data-slot="action"] {
- border-left: none;
- border-top: 1px solid var(--color-border);
- }
- }
- }
-}
diff --git a/packages/cloud/app/src/routes/[...404].tsx b/packages/cloud/app/src/routes/[...404].tsx
deleted file mode 100644
index ba2842b5a..000000000
--- a/packages/cloud/app/src/routes/[...404].tsx
+++ /dev/null
@@ -1,38 +0,0 @@
-import "./[...404].css"
-import { Title } from "@solidjs/meta"
-import { HttpStatusCode } from "@solidjs/start"
-import logoLight from "../asset/logo-ornate-light.svg"
-import logoDark from "../asset/logo-ornate-dark.svg"
-
-export default function NotFound() {
- return (
- <main data-page="not-found">
- <Title>Not Found | opencode</Title>
- <HttpStatusCode code={404} />
- <div data-component="content">
- <section data-component="top">
- <a href="/" data-slot="logo-link">
- <img data-slot="logo light" src={logoLight} alt="opencode logo light" />
- <img data-slot="logo dark" src={logoDark} alt="opencode logo dark" />
- </a>
- <h1 data-slot="title">404 - Page Not Found</h1>
- </section>
-
- <section data-component="actions">
- <div data-slot="action">
- <a href="/">Home</a>
- </div>
- <div data-slot="action">
- <a href="/docs">Docs</a>
- </div>
- <div data-slot="action">
- <a href="https://github.com/sst/opencode">GitHub</a>
- </div>
- <div data-slot="action">
- <a href="/discord">Discord</a>
- </div>
- </section>
- </div>
- </main>
- )
-}
diff --git a/packages/cloud/app/src/routes/auth/authorize.ts b/packages/cloud/app/src/routes/auth/authorize.ts
deleted file mode 100644
index 166466ef8..000000000
--- a/packages/cloud/app/src/routes/auth/authorize.ts
+++ /dev/null
@@ -1,7 +0,0 @@
-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")
- return Response.redirect(result.url, 302)
-}
diff --git a/packages/cloud/app/src/routes/auth/callback.ts b/packages/cloud/app/src/routes/auth/callback.ts
deleted file mode 100644
index 23025b54d..000000000
--- a/packages/cloud/app/src/routes/auth/callback.ts
+++ /dev/null
@@ -1,31 +0,0 @@
-import { redirect } from "@solidjs/router"
-import type { APIEvent } from "@solidjs/start/server"
-import { AuthClient } from "~/context/auth"
-import { useAuthSession } from "~/context/auth.session"
-
-export async function GET(input: APIEvent) {
- const url = new URL(input.request.url)
- const code = url.searchParams.get("code")
- if (!code) throw new Error("No code found")
- const result = await AuthClient.exchange(code, `${url.origin}${url.pathname}`)
- if (result.err) {
- throw new Error(result.err.message)
- }
- const decoded = AuthClient.decode(result.tokens.access, {} as any)
- if (decoded.err) throw new Error(decoded.err.message)
- const session = await useAuthSession()
- const id = decoded.subject.properties.accountID
- await session.update((value) => {
- return {
- ...value,
- account: {
- [id]: {
- id,
- email: decoded.subject.properties.email,
- },
- },
- current: id,
- }
- })
- return redirect("/auth")
-}
diff --git a/packages/cloud/app/src/routes/auth/index.ts b/packages/cloud/app/src/routes/auth/index.ts
deleted file mode 100644
index 308ae2d1d..000000000
--- a/packages/cloud/app/src/routes/auth/index.ts
+++ /dev/null
@@ -1,13 +0,0 @@
-import { Account } from "@opencode/cloud-core/account.js"
-import { redirect } from "@solidjs/router"
-import type { APIEvent } from "@solidjs/start/server"
-import { withActor } from "~/context/auth.withActor"
-
-export async function GET(input: APIEvent) {
- try {
- const workspaces = await withActor(async () => Account.workspaces())
- return redirect(`/workspace/${workspaces[0].id}`)
- } catch {
- return redirect("/auth/authorize")
- }
-}
diff --git a/packages/cloud/app/src/routes/debug/index.ts b/packages/cloud/app/src/routes/debug/index.ts
deleted file mode 100644
index 8c7eb7bd8..000000000
--- a/packages/cloud/app/src/routes/debug/index.ts
+++ /dev/null
@@ -1,13 +0,0 @@
-import type { APIEvent } from "@solidjs/start/server"
-import { json } from "@solidjs/router"
-import { Database } from "@opencode/cloud-core/drizzle/index.js"
-import { UserTable } from "@opencode/cloud-core/schema/user.sql.js"
-
-export async function GET(evt: APIEvent) {
- return json({
- data: await Database.use(async (tx) => {
- const result = await tx.$count(UserTable)
- return result
- }),
- })
-}
diff --git a/packages/cloud/app/src/routes/discord.ts b/packages/cloud/app/src/routes/discord.ts
deleted file mode 100644
index 7088295da..000000000
--- a/packages/cloud/app/src/routes/discord.ts
+++ /dev/null
@@ -1,5 +0,0 @@
-import { redirect } from "@solidjs/router"
-
-export async function GET() {
- return redirect("https://discord.gg/opencode")
-}
diff --git a/packages/cloud/app/src/routes/docs/[...path].ts b/packages/cloud/app/src/routes/docs/[...path].ts
deleted file mode 100644
index f07781583..000000000
--- a/packages/cloud/app/src/routes/docs/[...path].ts
+++ /dev/null
@@ -1,20 +0,0 @@
-import type { APIEvent } from "@solidjs/start/server"
-
-async function handler(evt: APIEvent) {
- const req = evt.request.clone()
- const url = new URL(req.url)
- const targetUrl = `https://docs.opencode.ai${url.pathname}${url.search}`
- const response = await fetch(targetUrl, {
- method: req.method,
- headers: req.headers,
- body: req.body,
- })
- return response
-}
-
-export const GET = handler
-export const POST = handler
-export const PUT = handler
-export const DELETE = handler
-export const OPTIONS = handler
-export const PATCH = handler
diff --git a/packages/cloud/app/src/routes/docs/index.ts b/packages/cloud/app/src/routes/docs/index.ts
deleted file mode 100644
index f07781583..000000000
--- a/packages/cloud/app/src/routes/docs/index.ts
+++ /dev/null
@@ -1,20 +0,0 @@
-import type { APIEvent } from "@solidjs/start/server"
-
-async function handler(evt: APIEvent) {
- const req = evt.request.clone()
- const url = new URL(req.url)
- const targetUrl = `https://docs.opencode.ai${url.pathname}${url.search}`
- const response = await fetch(targetUrl, {
- method: req.method,
- headers: req.headers,
- body: req.body,
- })
- return response
-}
-
-export const GET = handler
-export const POST = handler
-export const PUT = handler
-export const DELETE = handler
-export const OPTIONS = handler
-export const PATCH = handler
diff --git a/packages/cloud/app/src/routes/index.css b/packages/cloud/app/src/routes/index.css
deleted file mode 100644
index fe95bb7ea..000000000
--- a/packages/cloud/app/src/routes/index.css
+++ /dev/null
@@ -1,504 +0,0 @@
-[data-page="home"] {
- --color-text: hsl(224, 10%, 10%);
- --color-text-secondary: hsl(224, 7%, 46%);
- --color-text-dimmed: hsl(224, 6%, 63%);
- --color-text-inverted: hsl(0, 0%, 100%);
-
- --color-border: hsl(224, 6%, 77%);
-}
-
-[data-page="home"] {
- @media (prefers-color-scheme: dark) {
- --color-text: hsl(0, 0%, 100%);
- --color-text-secondary: hsl(224, 6%, 66%);
- --color-text-dimmed: hsl(224, 7%, 46%);
- --color-text-inverted: hsl(224, 10%, 10%);
-
- --color-border: hsl(224, 6%, 36%);
- }
-}
-
-[data-page="home"] {
- --padding: 3rem;
- --vertical-padding: 1.5rem;
- --heading-font-size: 1.375rem;
-
- @media (max-width: 30rem) {
- --padding: 1rem;
- --vertical-padding: 0.75rem;
- --heading-font-size: 1rem;
- }
-
- display: flex;
- gap: var(--vertical-padding);
- flex-direction: column;
- font-family: var(--font-mono);
- color: var(--color-text);
- padding: calc(var(--padding) + 1rem);
-
- a {
- color: var(--color-text);
- text-decoration: underline;
- text-underline-offset: var(--space-0-75);
- text-decoration-thickness: 1px;
- }
-
- [data-component="content"] {
- max-width: 67.5rem;
- margin: 0 auto;
- border: 1px solid var(--color-border);
- }
-
- [data-component="top"] {
- padding: calc(var(--padding) * 1.5) var(--padding) var(--padding);
- position: relative;
- display: flex;
- flex-direction: column;
- align-items: center;
- gap: calc(var(--vertical-padding) / 2);
-
- img {
- height: auto;
- width: clamp(200px, 85vw, 552px);
- }
-
- [data-slot="logo dark"] {
- display: none;
- }
-
- @media (prefers-color-scheme: dark) {
- [data-slot="logo light"] {
- display: none;
- }
- [data-slot="logo dark"] {
- display: block;
- }
- }
-
- [data-slot="title"] {
- line-height: 1.25;
- font-weight: 500;
- text-align: center;
- font-size: var(--heading-font-size);
- color: var(--color-text-secondary);
- text-transform: uppercase;
- }
-
- [data-slot="login"] {
- position: absolute;
- top: 0;
- right: 0;
- border-width: 0 0 1px 1px;
- border-style: solid;
- border-color: var(--color-border);
- background-color: var(--color-bg);
-
- @media (max-width: 30rem) {
- display: none;
- }
-
- a {
- display: block;
- padding: 0.5rem 1rem calc(0.5rem + 4px);
- }
- }
- }
-
- [data-component="cta"] {
- border-top: 1px solid var(--color-border);
- display: flex;
-
- & > div + div {
- border-left: 1px solid var(--color-border);
- }
-
- [data-slot="left"] {
- flex: 0 0 auto;
- text-align: center;
- line-height: 1.4;
- padding: var(--vertical-padding) 2rem;
- text-transform: uppercase;
- font-size: 1.125rem;
-
- @media (max-width: 30rem) {
- font-size: 1rem;
- padding-bottom: calc(var(--vertical-padding) + 4px);
- }
-
- @media (max-width: 30rem) {
- padding-left: 0.5rem;
- padding-right: 0.5rem;
- }
- }
-
- [data-slot="center"] {
- display: none;
-
- @media (max-width: 30rem) {
- display: block;
- flex: 1;
- text-align: center;
- padding: var(--vertical-padding) 0.5rem;
- border-top: 1px solid var(--color-border);
- border-left: none;
- }
- }
-
- [data-slot="right"] {
- flex: 1;
- padding: var(--vertical-padding) 1rem;
- }
-
- @media (max-width: 50rem) {
- flex-direction: column;
-
- [data-slot="right"] {
- border-left: none;
- border-top: 1px solid var(--color-border);
- }
- }
-
- [data-slot="command"] {
- all: unset;
- display: flex;
- align-items: center;
- justify-content: center;
- cursor: pointer;
- color: var(--color-text-secondary);
- font-size: 1.125rem;
- font-family: var(--font-mono);
- gap: var(--space-2);
- width: 100%;
-
- & > span {
- @media (max-width: 24rem) {
- font-size: 0.875rem;
- }
- @media (max-width: 56rem) {
- [data-slot="protocol"] {
- display: none;
- }
- }
- @media (max-width: 38rem) {
- text-align: center;
- span:first-child {
- display: block;
- }
- }
- }
- }
-
- [data-slot="highlight"] {
- color: var(--color-text);
- font-weight: 500;
- }
- }
-
- [data-component="features"] {
- border-top: 1px solid var(--color-border);
- padding: var(--padding);
-
- [data-slot="list"] {
- padding-left: var(--space-4);
- margin: 0;
- list-style: disc;
-
- li {
- margin-bottom: var(--space-4);
- line-height: 1.6;
-
- strong {
- text-transform: uppercase;
- font-weight: 600;
- }
-
- label {
- line-height: 1;
- text-transform: uppercase;
- font-size: 0.75rem;
- letter-spacing: 0.03125rem;
- background: var(--color-border);
- padding: 0.125rem 0.375rem;
- color: var(--color-text-inverted);
- }
- }
-
- li:last-child {
- margin-bottom: 0;
- }
- }
- }
-
- [data-component="install"] {
- border-top: 1px solid var(--color-border);
- display: grid;
- grid-template-columns: 1fr 1fr;
- grid-template-rows: 1fr 1fr;
-
- @media (max-width: 40rem) {
- grid-template-columns: 1fr;
- grid-template-rows: auto;
- }
- }
-
- [data-component="method"] {
- display: flex;
- padding: calc(var(--vertical-padding) / 2) calc(var(--padding) / 2) calc(var(--vertical-padding) / 2 + 0.125rem);
- flex-direction: column;
- text-align: left;
- gap: var(--space-2-5);
-
- @media (max-width: 30rem) {
- gap: 0.3125rem;
- }
-
- @media (max-width: 40rem) {
- text-align: left;
- }
-
- &:nth-child(2) {
- border-left: 1px solid var(--color-border);
-
- @media (max-width: 40rem) {
- border-left: none;
- border-top: 1px solid var(--color-border);
- }
- }
-
- &:nth-child(3) {
- border-top: 1px solid var(--color-border);
- }
-
- &:nth-child(4) {
- border-top: 1px solid var(--color-border);
- border-left: 1px solid var(--color-border);
-
- @media (max-width: 40rem) {
- border-left: none;
- }
- }
-
- [data-component="title"] {
- letter-spacing: -0.03125rem;
- text-transform: uppercase;
- font-weight: normal;
- font-size: 1rem;
- flex-shrink: 0;
- color: var(--color-text-dimmed);
-
- @media (max-width: 30rem) {
- font-size: 0.75rem;
- }
- }
-
- [data-slot="button"] {
- all: unset;
- cursor: pointer;
- display: flex;
- align-items: center;
- color: var(--color-text-secondary);
- gap: var(--space-2-5);
- font-size: 1rem;
-
- @media (max-width: 24rem) {
- font-size: 0.875rem;
- }
-
- strong {
- color: var(--color-text);
- font-weight: 500;
- }
-
- @media (max-width: 40rem) {
- justify-content: flex-start;
- }
-
- @media (max-width: 30rem) {
- justify-content: center;
- }
- }
- }
-
- [data-component="screenshots"] {
- border-top: 1px solid var(--color-border);
-
- figure {
- flex: 1;
- display: flex;
- flex-direction: column;
- gap: calc(var(--padding) / 4);
- padding: calc(var(--padding) / 2);
- border-width: 0;
- border-style: solid;
- border-color: var(--color-border);
- min-height: 0;
- overflow: hidden;
-
- & > div,
- figcaption {
- display: flex;
- align-items: center;
- }
-
- & > div {
- flex: 1;
- min-height: 0;
- display: flex;
- align-items: center;
- justify-content: center;
- }
-
- a {
- display: flex;
- flex: 1;
- min-height: 0;
- align-items: center;
- justify-content: center;
- width: 100%;
- height: 100%;
- }
-
- figcaption {
- letter-spacing: -0.03125rem;
- text-transform: uppercase;
- color: var(--color-text-dimmed);
- flex-shrink: 0;
-
- @media (max-width: 30rem) {
- font-size: 0.75rem;
- }
- }
- }
-
- & > [data-slot="left"] figure {
- height: var(--images-height);
- box-sizing: border-box;
- }
-
- & > [data-slot="right"] figure {
- height: calc(var(--images-height) / 2);
- box-sizing: border-box;
- }
-
- & > [data-slot="left"] img {
- width: 100%;
- height: 100%;
- min-width: 0;
- object-fit: contain;
- }
-
- & > [data-slot="right"] img {
- width: 100%;
- height: calc(100% - 2rem);
- object-fit: contain;
- display: block;
- }
-
- @media (max-width: 30rem) {
- & {
- --images-height: auto;
- grid-template-columns: 1fr;
- grid-template-rows: auto auto;
- }
-
- & > [data-slot="left"] {
- grid-row: 1;
- grid-column: 1;
- }
-
- & > [data-slot="right"] {
- grid-row: 2;
- grid-column: 1;
- border-left: none;
- border-top: 1px solid var(--color-border);
-
- & > [data-slot="row1"],
- & > [data-slot="row2"] {
- height: auto;
- }
- }
-
- & > [data-slot="left"] figure,
- & > [data-slot="right"] figure {
- height: auto;
- }
-
- & > [data-slot="left"] img,
- & > [data-slot="right"] img {
- width: 100%;
- height: auto;
- max-height: none;
- }
- }
- }
-
- [data-component="copy-status"] {
- @media (max-width: 38rem) {
- display: none;
- }
-
- [data-slot="copy"] {
- display: block;
- width: var(--space-4);
- height: var(--space-4);
- color: var(--color-text-dimmed);
-
- [data-copied] & {
- display: none;
- }
- }
-
- [data-slot="check"] {
- display: none;
- width: var(--space-4);
- height: var(--space-4);
- color: var(--color-text);
-
- [data-copied] & {
- display: block;
- }
- }
- }
-
- [data-component="footer"] {
- border-top: 1px solid var(--color-border);
- display: flex;
- flex-direction: row;
-
- [data-slot="cell"] {
- flex: 1;
- text-align: center;
- text-transform: uppercase;
- padding: var(--vertical-padding) 0.5rem;
- }
-
- [data-slot="cell"] + [data-slot="cell"] {
- border-left: 1px solid var(--color-border);
- }
-
- /* Mobile: third column on its own row */
- @media (max-width: 30rem) {
- flex-wrap: wrap;
-
- [data-slot="cell"]:nth-child(1),
- [data-slot="cell"]:nth-child(2) {
- flex: 1;
- }
-
- [data-slot="cell"]:nth-child(3) {
- flex: 1 0 100%;
- border-left: none;
- border-top: 1px solid var(--color-border);
- }
- }
- }
-
- [data-component="legal"] {
- color: var(--color-text-dimmed);
- text-align: center;
-
- a {
- color: var(--color-text-dimmed);
- }
- }
-}
diff --git a/packages/cloud/app/src/routes/index.tsx b/packages/cloud/app/src/routes/index.tsx
deleted file mode 100644
index 9075f4079..000000000
--- a/packages/cloud/app/src/routes/index.tsx
+++ /dev/null
@@ -1,183 +0,0 @@
-import "./index.css"
-import { Title } from "@solidjs/meta"
-import { onCleanup, onMount } from "solid-js"
-import logoLight from "../asset/logo-ornate-light.svg"
-import logoDark from "../asset/logo-ornate-dark.svg"
-import IMG_SPLASH from "../asset/lander/screenshot-splash.png"
-import { IconCopy, IconCheck } from "../component/icon"
-import { createAsync, query } from "@solidjs/router"
-import { getActor } from "~/context/auth"
-import { withActor } from "~/context/auth.withActor"
-import { Account } from "@opencode/cloud-core/account.js"
-
-function CopyStatus() {
- return (
- <div data-component="copy-status">
- <IconCopy data-slot="copy" />
- <IconCheck data-slot="check" />
- </div>
- )
-}
-
-const defaultWorkspace = query(async () => {
- "use server"
- const actor = await getActor()
- if (actor.type === "account") {
- const workspaces = await withActor(() => Account.workspaces())
- return workspaces[0].id
- }
-}, "defaultWorkspace")
-
-export default function Home() {
- const workspace = createAsync(() => defaultWorkspace())
- onMount(() => {
- const commands = document.querySelectorAll("[data-copy]")
- for (const button of commands) {
- const callback = () => {
- const text = button.textContent
- if (text) {
- navigator.clipboard.writeText(text)
- button.setAttribute("data-copied", "")
- setTimeout(() => {
- button.removeAttribute("data-copied")
- }, 1500)
- }
- }
- button.addEventListener("click", callback)
- onCleanup(() => {
- button.removeEventListener("click", callback)
- })
- }
- })
-
- return (
- <main data-page="home">
- <Title>opencode | AI coding agent built for the terminal</Title>
-
- <div data-component="content">
- <section data-component="top">
- <img data-slot="logo light" src={logoLight} alt="opencode logo light" />
- <img data-slot="logo dark" src={logoDark} alt="opencode logo dark" />
- <h1 data-slot="title">The AI coding agent built for the terminal</h1>
- <div data-slot="login">
- <a href="/auth">opencode zen</a>
- </div>
- </section>
-
- <section data-component="cta">
- <div data-slot="left">
- <a href="/docs">Get Started</a>
- </div>
- <div data-slot="center">
- <a href="/auth">opencode zen</a>
- </div>
- <div data-slot="right">
- <button data-copy data-slot="command">
- <span>
- <span>curl -fsSL </span>
- <span data-slot="protocol">https://</span>
- <span data-slot="highlight">opencode.ai/install</span>
- <span> | bash</span>
- </span>
- <CopyStatus />
- </button>
- </div>
- </section>
-
- <section data-component="features">
- <ul data-slot="list">
- <li>
- <strong>Native TUI</strong> A responsive, native, themeable terminal UI
- </li>
- <li>
- <strong>LSP enabled</strong> Automatically loads the right LSPs for the LLM
- </li>
- <li>
- <strong>opencode zen</strong> A <a href="/docs/zen">curated list of models</a> provided by opencode{" "}
- <label>New</label>
- </li>
- <li>
- <strong>Multi-session</strong> Start multiple agents in parallel on the same project
- </li>
- <li>
- <strong>Shareable links</strong> Share a link to any sessions for reference or to debug
- </li>
- <li>
- <strong>Claude Pro</strong> Log in with Anthropic to use your Claude Pro or Max account
- </li>
- <li>
- <strong>Use any model</strong> Supports 75+ LLM providers through{" "}
- <a href="https://models.dev">Models.dev</a>, including local models
- </li>
- </ul>
- </section>
-
- <section data-component="install">
- <div data-component="method">
- <h3 data-component="title">npm</h3>
- <button data-copy data-slot="button">
- <span>
- npm install -g <strong>opencode-ai</strong>
- </span>
- <CopyStatus />
- </button>
- </div>
- <div data-component="method">
- <h3 data-component="title">bun</h3>
- <button data-copy data-slot="button">
- <span>
- bun install -g <strong>opencode-ai</strong>
- </span>
- <CopyStatus />
- </button>
- </div>
- <div data-component="method">
- <h3 data-component="title">homebrew</h3>
- <button data-copy data-slot="button">
- <span>
- brew install <strong>sst/tap/opencode</strong>
- </span>
- <CopyStatus />
- </button>
- </div>
- <div data-component="method">
- <h3 data-component="title">paru</h3>
- <button data-copy data-slot="button">
- <span>
- paru -S <strong>opencode-bin</strong>
- </span>
- <CopyStatus />
- </button>
- </div>
- </section>
-
- <section data-component="screenshots">
- <figure>
- <figcaption>opencode TUI with the tokyonight theme</figcaption>
- <a href="/docs/cli">
- <img src={IMG_SPLASH} alt="opencode TUI with tokyonight theme" />
- </a>
- </figure>
- </section>
-
- <footer data-component="footer">
- <div data-slot="cell">
- <a href="https://x.com/opencode">X.com</a>
- </div>
- <div data-slot="cell">
- <a href="https://github.com/sst/opencode">GitHub</a>
- </div>
- <div data-slot="cell">
- <a href="https://opencode.ai/discord">Discord</a>
- </div>
- </footer>
- </div>
-
- <div data-component="legal">
- <span>
- ©2025 <a href="https://anoma.ly">Anomaly Innovations</a>
- </span>
- </div>
- </main>
- )
-}
diff --git a/packages/cloud/app/src/routes/s/[id].ts b/packages/cloud/app/src/routes/s/[id].ts
deleted file mode 100644
index 3fd1305a0..000000000
--- a/packages/cloud/app/src/routes/s/[id].ts
+++ /dev/null
@@ -1,20 +0,0 @@
-import type { APIEvent } from "@solidjs/start/server"
-
-async function handler(evt: APIEvent) {
- const req = evt.request.clone()
- const url = new URL(req.url)
- const targetUrl = `https://docs.opencode.ai/docs${url.pathname}${url.search}`
- const response = await fetch(targetUrl, {
- method: req.method,
- headers: req.headers,
- body: req.body,
- })
- return response
-}
-
-export const GET = handler
-export const POST = handler
-export const PUT = handler
-export const DELETE = handler
-export const OPTIONS = handler
-export const PATCH = handler
diff --git a/packages/cloud/app/src/routes/stripe/webhook.ts b/packages/cloud/app/src/routes/stripe/webhook.ts
deleted file mode 100644
index 925ede1ac..000000000
--- a/packages/cloud/app/src/routes/stripe/webhook.ts
+++ /dev/null
@@ -1,98 +0,0 @@
-import { Billing } from "@opencode/cloud-core/billing.js"
-import type { APIEvent } from "@solidjs/start/server"
-import { Database, eq, sql } from "@opencode/cloud-core/drizzle/index.js"
-import { BillingTable, PaymentTable } from "@opencode/cloud-core/schema/billing.sql.js"
-import { Identifier } from "@opencode/cloud-core/identifier.js"
-import { centsToMicroCents } from "@opencode/cloud-core/util/price.js"
-import { Actor } from "@opencode/cloud-core/actor.js"
-import { Resource } from "@opencode/cloud-resource"
-
-export async function POST(input: APIEvent) {
- const body = await Billing.stripe().webhooks.constructEventAsync(
- await input.request.text(),
- input.request.headers.get("stripe-signature")!,
- Resource.STRIPE_WEBHOOK_SECRET.value,
- )
-
- console.log(body.type, JSON.stringify(body, null, 2))
- if (body.type === "customer.updated") {
- // check default payment method changed
- const prevInvoiceSettings = body.data.previous_attributes?.invoice_settings ?? {}
- if (!("default_payment_method" in prevInvoiceSettings)) return
-
- const customerID = body.data.object.id
- const paymentMethodID = body.data.object.invoice_settings.default_payment_method as string
-
- if (!customerID) throw new Error("Customer ID not found")
- if (!paymentMethodID) throw new Error("Payment method ID not found")
-
- const paymentMethod = await Billing.stripe().paymentMethods.retrieve(paymentMethodID)
- await Database.use(async (tx) => {
- await tx
- .update(BillingTable)
- .set({
- paymentMethodID,
- paymentMethodLast4: paymentMethod.card!.last4,
- })
- .where(eq(BillingTable.customerID, customerID))
- })
- }
- if (body.type === "checkout.session.completed") {
- const workspaceID = body.data.object.metadata?.workspaceID
- const customerID = body.data.object.customer as string
- const paymentID = body.data.object.payment_intent as string
- const amount = body.data.object.amount_total
-
- if (!workspaceID) throw new Error("Workspace ID not found")
- if (!customerID) throw new Error("Customer ID not found")
- if (!amount) throw new Error("Amount not found")
- if (!paymentID) throw new Error("Payment ID not found")
-
- await Actor.provide("system", { workspaceID }, async () => {
- const customer = await Billing.get()
- if (customer?.customerID && customer.customerID !== customerID) throw new Error("Customer ID mismatch")
-
- // set customer metadata
- if (!customer?.customerID) {
- await Billing.stripe().customers.update(customerID, {
- metadata: {
- workspaceID,
- },
- })
- }
-
- // get payment method for the payment intent
- const paymentIntent = await Billing.stripe().paymentIntents.retrieve(paymentID, {
- expand: ["payment_method"],
- })
- const paymentMethod = paymentIntent.payment_method
- if (!paymentMethod || typeof paymentMethod === "string") throw new Error("Payment method not expanded")
-
- await Database.transaction(async (tx) => {
- await tx
- .update(BillingTable)
- .set({
- balance: sql`${BillingTable.balance} + ${centsToMicroCents(Billing.CHARGE_AMOUNT)}`,
- customerID,
- paymentMethodID: paymentMethod.id,
- paymentMethodLast4: paymentMethod.card!.last4,
- reload: true,
- reloadError: null,
- timeReloadError: null,
- })
- .where(eq(BillingTable.workspaceID, workspaceID))
- await tx.insert(PaymentTable).values({
- workspaceID,
- id: Identifier.create("payment"),
- amount: centsToMicroCents(Billing.CHARGE_AMOUNT),
- paymentID,
- customerID,
- })
- })
- })
- }
-
- console.log("finished handling")
-
- return Response.json("ok", { status: 200 })
-}
diff --git a/packages/cloud/app/src/routes/workspace.css b/packages/cloud/app/src/routes/workspace.css
deleted file mode 100644
index ed94365f0..000000000
--- a/packages/cloud/app/src/routes/workspace.css
+++ /dev/null
@@ -1,127 +0,0 @@
-[data-page="workspace"] {
- line-height: 1;
-
- /* Common elements */
- button {
- padding: var(--space-3) var(--space-4);
- border: 1px solid var(--color-border);
- border-radius: var(--border-radius-sm);
- background-color: var(--color-bg);
- color: var(--color-text);
- font-size: var(--font-size-sm);
- font-family: var(--font-sans);
- font-weight: 500;
- text-transform: uppercase;
- cursor: pointer;
- transition: all 0.15s ease;
-
- &:hover:not(:disabled) {
- background-color: var(--color-surface-hover);
- border-color: var(--color-accent);
- }
-
- &:active {
- transform: translateY(1px);
- }
-
- &:disabled {
- opacity: 0.5;
- transform: none;
- }
-
- &[data-color="primary"] {
- background-color: var(--color-primary);
- border-color: var(--color-primary);
- color: var(--color-primary-text);
-
- &:hover:not(:disabled) {
- background-color: var(--color-primary-hover);
- border-color: var(--color-primary-hover);
- }
- }
-
- &[data-color="ghost"] {
- background-color: transparent;
- border-color: transparent;
- color: var(--color-text-muted);
-
- &:hover:not(:disabled) {
- background-color: var(--color-surface-hover);
- border-color: var(--color-border);
- color: var(--color-text);
- }
- }
- }
-
- a {
- color: var(--color-text);
- text-decoration: underline;
- text-underline-offset: var(--space-0-75);
- text-decoration-thickness: 1px;
- }
-
- /* Workspace Header */
- [data-component="workspace-header"] {
- position: sticky;
- top: 0;
- z-index: 100;
- display: flex;
- justify-content: space-between;
- align-items: center;
- padding: var(--space-4) var(--space-4);
- border-bottom: 1px solid var(--color-border);
- background-color: var(--color-bg);
-
- @media (max-width: 30rem) {
- padding: var(--space-4) var(--space-4);
- }
- }
-
- [data-slot="header-brand"] {
- flex: 0 0 auto;
- padding-top: 4px;
-
- svg {
- width: 138px;
- }
-
- [data-component="site-title"] {
- font-size: var(--font-size-lg);
- font-weight: 600;
- color: var(--color-text);
- text-decoration: none;
- letter-spacing: -0.02em;
- }
- }
-
- [data-slot="header-actions"] {
- display: flex;
- gap: var(--space-4);
- align-items: center;
- font-size: var(--font-size-sm);
-
- [data-slot="user"] {
- color: var(--color-text-muted);
- }
-
- @media (max-width: 30rem) {
- [data-slot="user"] {
- display: none;
- }
- }
-
- a,
- button {
- appearance: none;
- background: none;
- border: none;
- cursor: pointer;
- padding: 0;
- color: var(--color-text);
- text-decoration: underline;
- text-underline-offset: var(--space-0-75);
- text-decoration-thickness: 1px;
- text-transform: uppercase;
- }
- }
-}
diff --git a/packages/cloud/app/src/routes/workspace.tsx b/packages/cloud/app/src/routes/workspace.tsx
deleted file mode 100644
index 3f08a70a0..000000000
--- a/packages/cloud/app/src/routes/workspace.tsx
+++ /dev/null
@@ -1,67 +0,0 @@
-import "./workspace.css"
-import { useAuthSession } from "~/context/auth.session"
-import { IconLogo } from "../component/icon"
-import { withActor } from "~/context/auth.withActor"
-import {
- query,
- action,
- redirect,
- createAsync,
- RouteSectionProps,
- Navigate,
- useNavigate,
- useParams,
- A,
-} from "@solidjs/router"
-import { User } from "@opencode/cloud-core/user.js"
-import { Actor } from "@opencode/cloud-core/actor.js"
-import { getRequestEvent } from "solid-js/web"
-
-const getUserInfo = query(async (workspaceID: string) => {
- "use server"
- return withActor(async () => {
- const actor = Actor.assert("user")
- return await User.fromID(actor.properties.userID)
- }, workspaceID)
-}, "userInfo")
-
-const logout = action(async () => {
- "use server"
- const auth = await useAuthSession()
- const event = getRequestEvent()
- const current = auth.data.current
- if (current)
- await auth.update((val) => {
- delete val.account?.[current]
- const first = Object.keys(val.account ?? {})[0]
- val.current = first
- event!.locals.actor = undefined
- return val
- })
- throw redirect("/")
-})
-
-export default function WorkspaceLayout(props: RouteSectionProps) {
- const params = useParams()
- const userInfo = createAsync(() => getUserInfo(params.id))
- return (
- <main data-page="workspace">
- <header data-component="workspace-header">
- <div data-slot="header-brand">
- <A href="/" data-component="site-title">
- <IconLogo />
- </A>
- </div>
- <div data-slot="header-actions">
- <span data-slot="user">{userInfo()?.email}</span>
- <form action={logout} method="post">
- <button type="submit" formaction={logout}>
- Logout
- </button>
- </form>
- </div>
- </header>
- <div>{props.children}</div>
- </main>
- )
-}
diff --git a/packages/cloud/app/src/routes/workspace/[id].css b/packages/cloud/app/src/routes/workspace/[id].css
deleted file mode 100644
index 8b318a19f..000000000
--- a/packages/cloud/app/src/routes/workspace/[id].css
+++ /dev/null
@@ -1,115 +0,0 @@
-[data-page="workspace-[id]"] {
- max-width: 64rem;
- padding: var(--space-10) var(--space-4);
- margin: 0 auto;
- width: 100%;
- display: flex;
- flex-direction: column;
- gap: var(--space-10);
-
- @media (max-width: 30rem) {
- padding-top: var(--space-4);
- padding-bottom: var(--space-4);
-
- gap: var(--space-8);
- }
-
- [data-slot="sections"] {
- display: flex;
- flex-direction: column;
- gap: var(--space-16);
-
- @media (max-width: 30rem) {
- gap: var(--space-8);
- }
-
- section {
- display: flex;
- flex-direction: column;
- gap: var(--space-8);
-
- @media (max-width: 30rem) {
- gap: var(--space-6);
- }
-
- /* Section titles */
- [data-slot="section-title"] {
- display: flex;
- flex-direction: column;
- gap: var(--space-1);
-
- h2 {
- font-size: var(--font-size-md);
- font-weight: 600;
- line-height: 1.2;
- letter-spacing: -0.03125rem;
- margin: 0;
- color: var(--color-text-secondary);
- text-transform: uppercase;
-
- @media (max-width: 30rem) {
- font-size: var(--font-size-md);
- }
- }
-
- p {
- line-height: 1.5;
- font-size: var(--font-size-md);
- color: var(--color-text-muted);
-
- a {
- color: var(--color-text-muted);
- }
-
- @media (max-width: 30rem) {
- font-size: var(--font-size-sm);
- }
- }
- }
- }
- section:not(:last-child) {
- border-bottom: 1px solid var(--color-border);
- padding-bottom: var(--space-16);
-
- @media (max-width: 30rem) {
- padding-bottom: var(--space-8);
- }
- }
- }
-
- /* Title section */
- [data-component="title-section"] {
- display: flex;
- flex-direction: column;
- gap: var(--space-2);
- padding-bottom: var(--space-8);
- border-bottom: 1px solid var(--color-border);
-
- @media (max-width: 30rem) {
- padding-bottom: var(--space-6);
- }
-
- h1 {
- font-size: var(--font-size-2xl);
- font-weight: 500;
- line-height: 1.2;
- letter-spacing: -0.03125rem;
- margin: 0;
- text-transform: uppercase;
-
- @media (max-width: 30rem) {
- font-size: var(--font-size-xl);
- }
- }
-
- p {
- line-height: 1.5;
- font-size: var(--font-size-md);
- color: var(--color-text-muted);
-
- a {
- color: var(--color-text-muted);
- }
- }
- }
-}
diff --git a/packages/cloud/app/src/routes/workspace/[id].tsx b/packages/cloud/app/src/routes/workspace/[id].tsx
deleted file mode 100644
index 4a2c3424d..000000000
--- a/packages/cloud/app/src/routes/workspace/[id].tsx
+++ /dev/null
@@ -1,50 +0,0 @@
-import "./[id].css"
-import { Billing } from "@opencode/cloud-core/billing.js"
-import { query, useParams, createAsync } from "@solidjs/router"
-import { Show } from "solid-js"
-import { withActor } from "~/context/auth.withActor"
-import { MonthlyLimitSection } from "~/component/workspace/monthly-limit-section"
-import { NewUserSection } from "~/component/workspace/new-user-section"
-import { BillingSection } from "~/component/workspace/billing-section"
-import { PaymentSection } from "~/component/workspace/payment-section"
-import { UsageSection } from "~/component/workspace/usage-section"
-import { KeySection } from "~/component/workspace/key-section"
-
-const getBillingInfo = query(async (workspaceID: string) => {
- "use server"
- return withActor(async () => {
- return await Billing.get()
- }, workspaceID)
-}, "billing.get")
-
-export default function () {
- const params = useParams()
- const balanceInfo = createAsync(() => getBillingInfo(params.id))
-
- return (
- <div data-page="workspace-[id]">
- <section data-component="title-section">
- <h1>Zen</h1>
- <p>
- Curated list of models provided by opencode.{" "}
- <a target="_blank" href="/docs/zen">
- Learn more
- </a>
- .
- </p>
- </section>
-
- <div data-slot="sections">
- <NewUserSection />
- <KeySection />
- <BillingSection />
- <Show when={true}>
- {/*<Show when={balanceInfo()?.reload}>*/}
- <MonthlyLimitSection />
- </Show>
- <UsageSection />
- <PaymentSection />
- </div>
- </div>
- )
-}
diff --git a/packages/cloud/app/src/routes/workspace/index.tsx b/packages/cloud/app/src/routes/workspace/index.tsx
deleted file mode 100644
index e69de29bb..000000000
--- a/packages/cloud/app/src/routes/workspace/index.tsx
+++ /dev/null
diff --git a/packages/cloud/app/src/routes/zen/handler.ts b/packages/cloud/app/src/routes/zen/handler.ts
deleted file mode 100644
index ab1fc6599..000000000
--- a/packages/cloud/app/src/routes/zen/handler.ts
+++ /dev/null
@@ -1,594 +0,0 @@
-import type { APIEvent } from "@solidjs/start/server"
-import path from "node:path"
-import { and, Database, eq, isNull, lt, or, sql } from "@opencode/cloud-core/drizzle/index.js"
-import { KeyTable } from "@opencode/cloud-core/schema/key.sql.js"
-import { BillingTable, PaymentTable, UsageTable } from "@opencode/cloud-core/schema/billing.sql.js"
-import { centsToMicroCents } from "@opencode/cloud-core/util/price.js"
-import { Identifier } from "@opencode/cloud-core/identifier.js"
-import { Resource } from "@opencode/cloud-resource"
-import { Billing } from "../../../../core/src/billing"
-import { Actor } from "@opencode/cloud-core/actor.js"
-
-type ModelCost = {
- input: number
- output: number
- cacheRead?: number
- cacheWrite5m?: number
- cacheWrite1h?: number
-}
-
-type Model = {
- id: string
- auth: boolean
- cost: ModelCost | ((usage: any) => ModelCost)
- headerMappings: Record<string, string>
- providers: Record<
- string,
- {
- api: string
- apiKey: string
- model: string
- weight?: number
- }
- >
-}
-
-export async function handler(
- input: APIEvent,
- opts: {
- modifyBody?: (body: any) => any
- setAuthHeader: (headers: Headers, apiKey: string) => void
- parseApiKey: (headers: Headers) => string | undefined
- onStreamPart: (chunk: string) => void
- getStreamUsage: () => any
- normalizeUsage: (body: any) => {
- inputTokens: number
- outputTokens: number
- reasoningTokens?: number
- cacheReadTokens?: number
- cacheWrite5mTokens?: number
- cacheWrite1hTokens?: number
- }
- },
-) {
- class AuthError extends Error {}
- class CreditsError extends Error {}
- class MonthlyLimitError extends Error {}
- class ModelError extends Error {}
-
- const MODELS: Record<string, Model> = {
- "claude-opus-4-1": {
- id: "claude-opus-4-1" as const,
- auth: true,
- cost: {
- input: 0.000015,
- output: 0.000075,
- cacheRead: 0.0000015,
- cacheWrite5m: 0.00001875,
- cacheWrite1h: 0.00003,
- },
- headerMappings: {},
- providers: {
- anthropic: {
- api: "https://api.anthropic.com",
- apiKey: Resource.ANTHROPIC_API_KEY.value,
- model: "claude-opus-4-1-20250805",
- },
- },
- },
- "claude-sonnet-4": {
- id: "claude-sonnet-4" as const,
- auth: true,
- cost: (usage: any) => {
- const totalInputTokens =
- usage.inputTokens + usage.cacheReadTokens + usage.cacheWrite5mTokens + usage.cacheWrite1hTokens
- return totalInputTokens <= 200_000
- ? {
- input: 0.000003,
- output: 0.000015,
- cacheRead: 0.0000003,
- cacheWrite5m: 0.00000375,
- cacheWrite1h: 0.000006,
- }
- : {
- input: 0.000006,
- output: 0.0000225,
- cacheRead: 0.0000006,
- cacheWrite5m: 0.0000075,
- cacheWrite1h: 0.000012,
- }
- },
- headerMappings: {},
- providers: {
- anthropic: {
- api: "https://api.anthropic.com",
- apiKey: Resource.ANTHROPIC_API_KEY.value,
- model: "claude-sonnet-4-20250514",
- },
- },
- },
- "claude-3-5-haiku": {
- id: "claude-3-5-haiku" as const,
- auth: true,
- cost: {
- input: 0.0000008,
- output: 0.000004,
- cacheRead: 0.00000008,
- cacheWrite5m: 0.000001,
- cacheWrite1h: 0.0000016,
- },
- headerMappings: {},
- providers: {
- anthropic: {
- api: "https://api.anthropic.com",
- apiKey: Resource.ANTHROPIC_API_KEY.value,
- model: "claude-3-5-haiku-20241022",
- },
- },
- },
- "gpt-5": {
- id: "gpt-5" as const,
- auth: true,
- cost: {
- input: 0.00000125,
- output: 0.00001,
- cacheRead: 0.000000125,
- },
- headerMappings: {},
- providers: {
- openai: {
- api: "https://api.openai.com",
- apiKey: Resource.OPENAI_API_KEY.value,
- model: "gpt-5",
- },
- },
- },
- "qwen3-coder": {
- id: "qwen3-coder" as const,
- auth: true,
- cost: {
- input: 0.00000045,
- output: 0.0000018,
- },
- headerMappings: {},
- providers: {
- baseten: {
- api: "https://inference.baseten.co",
- apiKey: Resource.BASETEN_API_KEY.value,
- model: "Qwen/Qwen3-Coder-480B-A35B-Instruct",
- weight: 4,
- },
- fireworks: {
- api: "https://api.fireworks.ai/inference",
- apiKey: Resource.FIREWORKS_API_KEY.value,
- model: "accounts/fireworks/models/qwen3-coder-480b-a35b-instruct",
- weight: 1,
- },
- },
- },
- "kimi-k2": {
- id: "kimi-k2" as const,
- auth: true,
- cost: {
- input: 0.0000006,
- output: 0.0000025,
- },
- headerMappings: {},
- providers: {
- baseten: {
- api: "https://inference.baseten.co",
- apiKey: Resource.BASETEN_API_KEY.value,
- model: "moonshotai/Kimi-K2-Instruct-0905",
- //weight: 4,
- },
- //fireworks: {
- // api: "https://api.fireworks.ai/inference",
- // apiKey: Resource.FIREWORKS_API_KEY.value,
- // model: "accounts/fireworks/models/kimi-k2-instruct-0905",
- // weight: 1,
- //},
- },
- },
- "grok-code": {
- id: "grok-code" as const,
- auth: false,
- cost: {
- input: 0,
- output: 0,
- cacheRead: 0,
- },
- headerMappings: {
- "x-grok-conv-id": "x-opencode-session",
- "x-grok-req-id": "x-opencode-request",
- },
- providers: {
- xai: {
- api: "https://api.x.ai",
- apiKey: Resource.XAI_API_KEY.value,
- model: "grok-code",
- },
- },
- },
- // deprecated
- "qwen/qwen3-coder": {
- id: "qwen/qwen3-coder" as const,
- auth: true,
- cost: {
- input: 0.00000038,
- output: 0.00000153,
- },
- headerMappings: {},
- providers: {
- baseten: {
- api: "https://inference.baseten.co",
- apiKey: Resource.BASETEN_API_KEY.value,
- model: "Qwen/Qwen3-Coder-480B-A35B-Instruct",
- weight: 5,
- },
- fireworks: {
- api: "https://api.fireworks.ai/inference",
- apiKey: Resource.FIREWORKS_API_KEY.value,
- model: "accounts/fireworks/models/qwen3-coder-480b-a35b-instruct",
- weight: 1,
- },
- },
- },
- }
-
- const FREE_WORKSPACES = [
- "wrk_01K46JDFR0E75SG2Q8K172KF3Y", // frank
- ]
-
- const logger = {
- metric: (values: Record<string, any>) => {
- console.log(`_metric:${JSON.stringify(values)}`)
- },
- log: console.log,
- debug: (message: string) => {
- if (Resource.App.stage === "production") return
- console.debug(message)
- },
- }
-
- try {
- const url = new URL(input.request.url)
- const body = await input.request.json()
- logger.debug(JSON.stringify(body))
- logger.metric({
- is_tream: !!body.stream,
- session: input.request.headers.get("x-opencode-session"),
- request: input.request.headers.get("x-opencode-request"),
- })
- const MODEL = validateModel()
- const apiKey = await authenticate()
- const isFree = FREE_WORKSPACES.includes(apiKey?.workspaceID ?? "")
- await checkCreditsAndLimit()
- const providerName = selectProvider()
- const providerData = MODEL.providers[providerName]
- logger.metric({ provider: providerName })
-
- // Request to model provider
- const startTimestamp = Date.now()
- const res = await fetch(path.posix.join(providerData.api, url.pathname.replace(/^\/zen/, "") + url.search), {
- method: "POST",
- headers: (() => {
- const headers = input.request.headers
- headers.delete("host")
- headers.delete("content-length")
- opts.setAuthHeader(headers, providerData.apiKey)
- Object.entries(MODEL.headerMappings ?? {}).forEach(([k, v]) => {
- headers.set(k, headers.get(v)!)
- })
- return headers
- })(),
- body: JSON.stringify({
- ...(opts.modifyBody?.(body) ?? body),
- model: providerData.model,
- }),
- })
-
- // Scrub response headers
- const resHeaders = new Headers()
- const keepHeaders = ["content-type", "cache-control"]
- for (const [k, v] of res.headers.entries()) {
- if (keepHeaders.includes(k.toLowerCase())) {
- resHeaders.set(k, v)
- }
- }
-
- // Handle non-streaming response
- if (!body.stream) {
- const json = await res.json()
- const body = JSON.stringify(json)
- logger.metric({ response_length: body.length })
- logger.debug(body)
- await trackUsage(json.usage)
- await reload()
- return new Response(body, {
- status: res.status,
- statusText: res.statusText,
- headers: resHeaders,
- })
- }
-
- // Handle streaming response
- const stream = new ReadableStream({
- start(c) {
- const reader = res.body?.getReader()
- const decoder = new TextDecoder()
- let buffer = ""
- let responseLength = 0
-
- function pump(): Promise<void> {
- return (
- reader?.read().then(async ({ done, value }) => {
- if (done) {
- logger.metric({ response_length: responseLength })
- const usage = opts.getStreamUsage()
- if (usage) {
- await trackUsage(usage)
- await reload()
- }
- c.close()
- return
- }
-
- if (responseLength === 0) {
- logger.metric({ time_to_first_byte: Date.now() - startTimestamp })
- }
- responseLength += value.length
- buffer += decoder.decode(value, { stream: true })
-
- const parts = buffer.split("\n\n")
- buffer = parts.pop() ?? ""
-
- for (const part of parts) {
- logger.debug(part)
- opts.onStreamPart(part.trim())
- }
-
- c.enqueue(value)
-
- return pump()
- }) || Promise.resolve()
- )
- }
-
- return pump()
- },
- })
-
- return new Response(stream, {
- status: res.status,
- statusText: res.statusText,
- headers: resHeaders,
- })
-
- function validateModel() {
- if (!(body.model in MODELS)) {
- throw new ModelError(`Model ${body.model} not supported`)
- }
- const model = MODELS[body.model as keyof typeof MODELS]
- logger.metric({ model: model.id })
- return model
- }
-
- async function authenticate() {
- try {
- const apiKey = opts.parseApiKey(input.request.headers)
- if (!apiKey) throw new AuthError("Missing API key.")
-
- const key = await Database.use((tx) =>
- tx
- .select({
- id: KeyTable.id,
- workspaceID: KeyTable.workspaceID,
- })
- .from(KeyTable)
- .where(and(eq(KeyTable.key, apiKey), isNull(KeyTable.timeDeleted)))
- .then((rows) => rows[0]),
- )
-
- if (!key) throw new AuthError("Invalid API key.")
- logger.metric({
- api_key: key.id,
- workspace: key.workspaceID,
- })
- return key
- } catch (e) {
- // ignore error if model does not require authentication
- if (!MODEL.auth) return
- throw e
- }
- }
-
- async function checkCreditsAndLimit() {
- if (!apiKey || !MODEL.auth || isFree) return
-
- const billing = await Database.use((tx) =>
- tx
- .select({
- balance: BillingTable.balance,
- paymentMethodID: BillingTable.paymentMethodID,
- monthlyLimit: BillingTable.monthlyLimit,
- monthlyUsage: BillingTable.monthlyUsage,
- timeMonthlyUsageUpdated: BillingTable.timeMonthlyUsageUpdated,
- })
- .from(BillingTable)
- .where(eq(BillingTable.workspaceID, apiKey.workspaceID))
- .then((rows) => rows[0]),
- )
-
- if (!billing.paymentMethodID) throw new CreditsError("No payment method")
- if (billing.balance <= 0) throw new CreditsError("Insufficient balance")
- if (
- billing.monthlyLimit &&
- billing.monthlyUsage &&
- billing.timeMonthlyUsageUpdated &&
- billing.monthlyUsage >= centsToMicroCents(billing.monthlyLimit * 100)
- ) {
- const now = new Date()
- const currentYear = now.getUTCFullYear()
- const currentMonth = now.getUTCMonth()
- const dateYear = billing.timeMonthlyUsageUpdated.getUTCFullYear()
- const dateMonth = billing.timeMonthlyUsageUpdated.getUTCMonth()
- if (currentYear === dateYear && currentMonth === dateMonth)
- throw new MonthlyLimitError(`You have reached your monthly spending limit of $${billing.monthlyLimit}.`)
- }
- }
-
- function selectProvider() {
- const picks = Object.entries(MODEL.providers).flatMap(([name, provider]) =>
- Array<string>(provider.weight ?? 1).fill(name),
- )
- return picks[Math.floor(Math.random() * picks.length)]
- }
-
- async function trackUsage(usage: any) {
- const { inputTokens, outputTokens, reasoningTokens, cacheReadTokens, cacheWrite5mTokens, cacheWrite1hTokens } =
- opts.normalizeUsage(usage)
-
- const modelCost = typeof MODEL.cost === "function" ? MODEL.cost(usage) : MODEL.cost
-
- const inputCost = modelCost.input * inputTokens * 100
- const outputCost = modelCost.output * outputTokens * 100
- const reasoningCost = (() => {
- if (!reasoningTokens) return undefined
- return modelCost.output * reasoningTokens * 100
- })()
- const cacheReadCost = (() => {
- if (!cacheReadTokens) return undefined
- if (!modelCost.cacheRead) return undefined
- return modelCost.cacheRead * cacheReadTokens * 100
- })()
- const cacheWrite5mCost = (() => {
- if (!cacheWrite5mTokens) return undefined
- if (!modelCost.cacheWrite5m) return undefined
- return modelCost.cacheWrite5m * cacheWrite5mTokens * 100
- })()
- const cacheWrite1hCost = (() => {
- if (!cacheWrite1hTokens) return undefined
- if (!modelCost.cacheWrite1h) return undefined
- return modelCost.cacheWrite1h * cacheWrite1hTokens * 100
- })()
- const totalCostInCent =
- inputCost +
- outputCost +
- (reasoningCost ?? 0) +
- (cacheReadCost ?? 0) +
- (cacheWrite5mCost ?? 0) +
- (cacheWrite1hCost ?? 0)
-
- logger.metric({
- "tokens.input": inputTokens,
- "tokens.output": outputTokens,
- "tokens.reasoning": reasoningTokens,
- "tokens.cache_read": cacheReadTokens,
- "tokens.cache_write_5m": cacheWrite5mTokens,
- "tokens.cache_write_1h": cacheWrite1hTokens,
- "cost.input": Math.round(inputCost),
- "cost.output": Math.round(outputCost),
- "cost.reasoning": reasoningCost ? Math.round(reasoningCost) : undefined,
- "cost.cache_read": cacheReadCost ? Math.round(cacheReadCost) : undefined,
- "cost.cache_write_5m": cacheWrite5mCost ? Math.round(cacheWrite5mCost) : undefined,
- "cost.cache_write_1h": cacheWrite1hCost ? Math.round(cacheWrite1hCost) : undefined,
- "cost.total": Math.round(totalCostInCent),
- })
-
- if (!apiKey) return
-
- const cost = isFree ? 0 : centsToMicroCents(totalCostInCent)
- await Database.transaction(async (tx) => {
- await tx.insert(UsageTable).values({
- workspaceID: apiKey.workspaceID,
- id: Identifier.create("usage"),
- model: MODEL.id,
- provider: providerName,
- inputTokens,
- outputTokens,
- reasoningTokens,
- cacheReadTokens,
- cacheWrite5mTokens,
- cacheWrite1hTokens,
- cost,
- })
- await tx
- .update(BillingTable)
- .set({
- balance: sql`${BillingTable.balance} - ${cost}`,
- monthlyUsage: sql`
- CASE
- WHEN MONTH(${BillingTable.timeMonthlyUsageUpdated}) = MONTH(now()) AND YEAR(${BillingTable.timeMonthlyUsageUpdated}) = YEAR(now()) THEN ${BillingTable.monthlyUsage} + ${cost}
- ELSE ${cost}
- END
- `,
- timeMonthlyUsageUpdated: sql`now()`,
- })
- .where(eq(BillingTable.workspaceID, apiKey.workspaceID))
- })
-
- await Database.use((tx) =>
- tx
- .update(KeyTable)
- .set({ timeUsed: sql`now()` })
- .where(eq(KeyTable.id, apiKey.id)),
- )
- }
-
- async function reload() {
- if (!apiKey) return
-
- const lock = await Database.use((tx) =>
- tx
- .update(BillingTable)
- .set({
- timeReloadLockedTill: sql`now() + interval 1 minute`,
- })
- .where(
- and(
- eq(BillingTable.workspaceID, apiKey.workspaceID),
- eq(BillingTable.reload, true),
- lt(BillingTable.balance, centsToMicroCents(Billing.CHARGE_THRESHOLD)),
- or(isNull(BillingTable.timeReloadLockedTill), lt(BillingTable.timeReloadLockedTill, sql`now()`)),
- ),
- ),
- )
- if (lock.rowsAffected === 0) return
-
- await Actor.provide("system", { workspaceID: apiKey.workspaceID }, async () => {
- await Billing.reload()
- })
- }
- } catch (error: any) {
- logger.metric({
- "error.type": error.constructor.name,
- "error.message": error.message,
- })
-
- // Note: both top level "type" and "error.type" fields are used by the @ai-sdk/anthropic client to render the error message.
- if (
- error instanceof AuthError ||
- error instanceof CreditsError ||
- error instanceof MonthlyLimitError ||
- error instanceof ModelError
- )
- return new Response(
- JSON.stringify({
- type: "error",
- error: { type: error.constructor.name, message: error.message },
- }),
- { status: 401 },
- )
-
- return new Response(
- JSON.stringify({
- type: "error",
- error: {
- type: "error",
- message: error.message,
- },
- }),
- { status: 500 },
- )
- }
-}
diff --git a/packages/cloud/app/src/routes/zen/v1/chat/completions.ts b/packages/cloud/app/src/routes/zen/v1/chat/completions.ts
deleted file mode 100644
index 801557324..000000000
--- a/packages/cloud/app/src/routes/zen/v1/chat/completions.ts
+++ /dev/null
@@ -1,54 +0,0 @@
-import type { APIEvent } from "@solidjs/start/server"
-import { handler } from "~/routes/zen/handler"
-
-type Usage = {
- prompt_tokens?: number
- completion_tokens?: number
- total_tokens?: number
- prompt_tokens_details?: {
- text_tokens?: number
- audio_tokens?: number
- image_tokens?: number
- cached_tokens?: number
- }
- completion_tokens_details?: {
- reasoning_tokens?: number
- audio_tokens?: number
- accepted_prediction_tokens?: number
- rejected_prediction_tokens?: number
- }
-}
-
-export function POST(input: APIEvent) {
- let usage: Usage
- return handler(input, {
- modifyBody: (body: any) => ({
- ...body,
- ...(body.stream ? { stream_options: { include_usage: true } } : {}),
- }),
- setAuthHeader: (headers: Headers, apiKey: string) => {
- headers.set("authorization", `Bearer ${apiKey}`)
- },
- parseApiKey: (headers: Headers) => headers.get("authorization")?.split(" ")[1],
- onStreamPart: (chunk: string) => {
- if (!chunk.startsWith("data: ")) return
-
- let json
- try {
- json = JSON.parse(chunk.slice(6)) as { usage?: Usage }
- } catch (e) {
- return
- }
-
- if (!json.usage) return
- usage = json.usage
- },
- getStreamUsage: () => usage,
- normalizeUsage: (usage: Usage) => ({
- inputTokens: usage.prompt_tokens ?? 0,
- outputTokens: usage.completion_tokens ?? 0,
- reasoningTokens: usage.completion_tokens_details?.reasoning_tokens ?? undefined,
- cacheReadTokens: usage.prompt_tokens_details?.cached_tokens ?? undefined,
- }),
- })
-}
diff --git a/packages/cloud/app/src/routes/zen/v1/messages.ts b/packages/cloud/app/src/routes/zen/v1/messages.ts
deleted file mode 100644
index 1fd85d5c7..000000000
--- a/packages/cloud/app/src/routes/zen/v1/messages.ts
+++ /dev/null
@@ -1,61 +0,0 @@
-import type { APIEvent } from "@solidjs/start/server"
-import { handler } from "~/routes/zen/handler"
-
-type Usage = {
- cache_creation?: {
- ephemeral_5m_input_tokens?: number
- ephemeral_1h_input_tokens?: number
- }
- cache_creation_input_tokens?: number
- cache_read_input_tokens?: number
- input_tokens?: number
- output_tokens?: number
- server_tool_use?: {
- web_search_requests?: number
- }
-}
-
-export function POST(input: APIEvent) {
- let usage: Usage
- return handler(input, {
- modifyBody: (body: any) => ({
- ...body,
- service_tier: "standard_only",
- }),
- setAuthHeader: (headers: Headers, apiKey: string) => headers.set("x-api-key", apiKey),
- parseApiKey: (headers: Headers) => headers.get("x-api-key") ?? undefined,
- onStreamPart: (chunk: string) => {
- const data = chunk.split("\n")[1]
- if (!data.startsWith("data: ")) return
-
- let json
- try {
- json = JSON.parse(data.slice(6)) as { usage?: Usage }
- } catch (e) {
- return
- }
-
- if (!json.usage) return
- usage = {
- ...usage,
- ...json.usage,
- cache_creation: {
- ...usage?.cache_creation,
- ...json.usage.cache_creation,
- },
- server_tool_use: {
- ...usage?.server_tool_use,
- ...json.usage.server_tool_use,
- },
- }
- },
- getStreamUsage: () => usage,
- normalizeUsage: (usage: Usage) => ({
- inputTokens: usage.input_tokens ?? 0,
- outputTokens: usage.output_tokens ?? 0,
- cacheReadTokens: usage.cache_read_input_tokens ?? undefined,
- cacheWrite5mTokens: usage.cache_creation?.ephemeral_5m_input_tokens ?? undefined,
- cacheWrite1hTokens: usage.cache_creation?.ephemeral_1h_input_tokens ?? undefined,
- }),
- })
-}
diff --git a/packages/cloud/app/src/routes/zen/v1/responses.ts b/packages/cloud/app/src/routes/zen/v1/responses.ts
deleted file mode 100644
index 486c129b9..000000000
--- a/packages/cloud/app/src/routes/zen/v1/responses.ts
+++ /dev/null
@@ -1,52 +0,0 @@
-import type { APIEvent } from "@solidjs/start/server"
-import { handler } from "~/routes/zen/handler"
-
-type Usage = {
- input_tokens?: number
- input_tokens_details?: {
- cached_tokens?: number
- }
- output_tokens?: number
- output_tokens_details?: {
- reasoning_tokens?: number
- }
- total_tokens?: number
-}
-
-export function POST(input: APIEvent) {
- let usage: Usage
- return handler(input, {
- setAuthHeader: (headers: Headers, apiKey: string) => {
- headers.set("authorization", `Bearer ${apiKey}`)
- },
- parseApiKey: (headers: Headers) => headers.get("authorization")?.split(" ")[1],
- onStreamPart: (chunk: string) => {
- const [event, data] = chunk.split("\n")
- if (event !== "event: response.completed") return
- if (!data.startsWith("data: ")) return
-
- let json
- try {
- json = JSON.parse(data.slice(6)) as { response?: { usage?: Usage } }
- } catch (e) {
- return
- }
-
- if (!json.response?.usage) return
- usage = json.response.usage
- },
- getStreamUsage: () => usage,
- normalizeUsage: (usage: Usage) => {
- const inputTokens = usage.input_tokens ?? 0
- const outputTokens = usage.output_tokens ?? 0
- const reasoningTokens = usage.output_tokens_details?.reasoning_tokens ?? undefined
- const cacheReadTokens = usage.input_tokens_details?.cached_tokens ?? undefined
- return {
- inputTokens: inputTokens - (cacheReadTokens ?? 0),
- outputTokens: outputTokens - (reasoningTokens ?? 0),
- reasoningTokens,
- cacheReadTokens,
- }
- },
- })
-}
diff --git a/packages/cloud/app/src/style/base.css b/packages/cloud/app/src/style/base.css
deleted file mode 100644
index a4847ed43..000000000
--- a/packages/cloud/app/src/style/base.css
+++ /dev/null
@@ -1,9 +0,0 @@
-html {
- line-height: 1;
- background-color: var(--color-bg);
- color: var(--color-text);
-}
-
-body {
- font-family: var(--font-sans);
-}
diff --git a/packages/cloud/app/src/style/component/button.css b/packages/cloud/app/src/style/component/button.css
deleted file mode 100644
index d10f7af53..000000000
--- a/packages/cloud/app/src/style/component/button.css
+++ /dev/null
@@ -1,102 +0,0 @@
-[data-component="button"] {
- display: inline-flex;
- align-items: center;
- justify-content: center;
- gap: var(--space-2);
- padding: var(--space-3) var(--space-4);
- border: 1px solid transparent;
- border-radius: var(--space-2);
- font-family: var(--font-sans);
- font-size: var(--font-size-md);
- font-weight: 500;
- line-height: 1.25;
- cursor: pointer;
- transition: all 0.2s ease-in-out;
- text-decoration: none;
- user-select: none;
-
- &:disabled {
- opacity: 0.5;
- cursor: not-allowed;
- }
-
- &:focus {
- outline: none;
- box-shadow: 0 0 0 2px var(--color-primary);
- }
-
- &[data-color="primary"] {
- background-color: var(--color-primary);
- color: var(--color-primary-text);
- border-color: var(--color-primary);
-
- &:hover:not(:disabled) {
- background-color: var(--color-primary-hover);
- border-color: var(--color-primary-hover);
- }
-
- &:active:not(:disabled) {
- background-color: var(--color-primary-active);
- border-color: var(--color-primary-active);
- }
- }
-
- &[data-color="danger"] {
- background-color: var(--color-danger);
- color: var(--color-danger-text);
- border-color: var(--color-danger);
-
- &:hover:not(:disabled) {
- background-color: var(--color-danger-hover);
- border-color: var(--color-danger-hover);
- }
-
- &:active:not(:disabled) {
- background-color: var(--color-danger-active);
- border-color: var(--color-danger-active);
- }
-
- &:focus {
- box-shadow: 0 0 0 2px var(--color-danger);
- }
- }
-
- &[data-color="warning"] {
- background-color: var(--color-warning);
- color: var(--color-warning-text);
- border-color: var(--color-warning);
-
- &:hover:not(:disabled) {
- background-color: var(--color-warning-hover);
- border-color: var(--color-warning-hover);
- }
-
- &:active:not(:disabled) {
- background-color: var(--color-warning-active);
- border-color: var(--color-warning-active);
- }
-
- &:focus {
- box-shadow: 0 0 0 2px var(--color-warning);
- }
- }
-
- &[data-size="small"] {
- padding: var(--space-2) var(--space-3);
- font-size: var(--font-size-sm);
- gap: var(--space-1-5);
- }
-
- &[data-size="large"] {
- padding: var(--space-4) var(--space-6);
- font-size: var(--font-size-lg);
- gap: var(--space-3);
- }
-
- [data-slot="icon"] {
- display: flex;
- align-items: center;
- width: 1em;
- height: 1em;
- }
-}
diff --git a/packages/cloud/app/src/style/index.css b/packages/cloud/app/src/style/index.css
deleted file mode 100644
index 832a901e8..000000000
--- a/packages/cloud/app/src/style/index.css
+++ /dev/null
@@ -1,8 +0,0 @@
-@import "./token/color.css";
-@import "./token/font.css";
-@import "./token/space.css";
-
-@import "./component/button.css";
-
-@import "./reset.css";
-@import "./base.css";
diff --git a/packages/cloud/app/src/style/reset.css b/packages/cloud/app/src/style/reset.css
deleted file mode 100644
index d331ed724..000000000
--- a/packages/cloud/app/src/style/reset.css
+++ /dev/null
@@ -1,76 +0,0 @@
-/* 1. Use a more-intuitive box-sizing model */
-*,
-*::before,
-*::after {
- box-sizing: border-box;
-}
-
-/* 2. Remove default margin */
-* {
- margin: 0;
-}
-
-/* 3. Enable keyword animations */
-@media (prefers-reduced-motion: no-preference) {
- html {
- interpolate-size: allow-keywords;
- }
-}
-
-body {
- /* 4. Add accessible line-height */
- line-height: 1.5;
- /* 5. Improve text rendering */
- -webkit-font-smoothing: antialiased;
-}
-
-/* 6. Improve media defaults */
-img,
-picture,
-video,
-canvas,
-svg {
- display: block;
- max-width: 100%;
-}
-
-/* 7. Inherit fonts for form controls */
-input,
-button,
-textarea,
-select {
- font: inherit;
-}
-
-/* 8. Avoid text overflows */
-p,
-h1,
-h2,
-h3,
-h4,
-h5,
-h6 {
- overflow-wrap: break-word;
-}
-
-/* 9. Improve line wrapping */
-p {
- text-wrap: pretty;
-}
-
-h1,
-h2,
-h3,
-h4,
-h5,
-h6 {
- text-wrap: balance;
-}
-
-/*
- 10. Create a root stacking context
-*/
-#root,
-#__next {
- isolation: isolate;
-}
diff --git a/packages/cloud/app/src/style/token/color.css b/packages/cloud/app/src/style/token/color.css
deleted file mode 100644
index f1a097d2f..000000000
--- a/packages/cloud/app/src/style/token/color.css
+++ /dev/null
@@ -1,91 +0,0 @@
-:root {
- --color-white: #ffffff;
- --color-black: #000000;
-
- /* Default light theme colors */
- --color-bg: #ffffff;
- --color-bg-surface: #f5f5f7;
- --color-bg-elevated: #ffffff;
-
- --color-text: #1d1d1f;
- --color-text-secondary: #424245;
- --color-text-muted: #6e6e73;
- --color-text-disabled: #86868b;
-
- --color-accent: #007aff;
- --color-accent-hover: #0056b3;
- --color-accent-active: #004085;
-
- --color-success: #30d158;
- --color-warning: #ff9f0a;
- --color-danger: #ff3b30;
-
- --color-border: #d2d2d7;
- --color-border-muted: #e5e5ea;
-
- /* Button colors */
- --color-primary: var(--color-accent);
- --color-primary-hover: var(--color-accent-hover);
- --color-primary-active: var(--color-accent-active);
- --color-primary-text: #ffffff;
-
- --color-danger: #ff3b30;
- --color-danger-hover: #d70015;
- --color-danger-active: #a50011;
- --color-danger-text: #ffffff;
-
- --color-warning: #ff9f0a;
- --color-warning-hover: #cc7f08;
- --color-warning-active: #995f06;
- --color-warning-text: #000000;
-
- /* Surface colors */
- --color-surface: var(--color-bg-surface);
- --color-surface-hover: var(--color-bg-elevated);
- --color-surface-border: var(--color-border);
-}
-
-@media (prefers-color-scheme: dark) {
- :root {
- --color-bg: #0c0c0e;
- --color-bg-surface: #161618;
- --color-bg-elevated: #1c1c1f;
-
- --color-text: #ffffff;
- --color-text-secondary: #c7c7cc;
- --color-text-muted: #a1a1a6;
- --color-text-disabled: #68686f;
-
- --color-accent: #007aff;
- --color-accent-hover: #0056b3;
- --color-accent-active: #004085;
-
- --color-success: #30d158;
- --color-warning: #ff9f0a;
- --color-danger: #ff453a;
-
- --color-border: #38383a;
- --color-border-muted: #2c2c2e;
-
- /* Button colors */
- --color-primary: var(--color-accent);
- --color-primary-hover: var(--color-accent-hover);
- --color-primary-active: var(--color-accent-active);
- --color-primary-text: #ffffff;
-
- --color-danger: #ff453a;
- --color-danger-hover: #d70015;
- --color-danger-active: #a50011;
- --color-danger-text: #ffffff;
-
- --color-warning: #ff9f0a;
- --color-warning-hover: #cc7f08;
- --color-warning-active: #995f06;
- --color-warning-text: #000000;
-
- /* Surface colors */
- --color-surface: var(--color-bg-surface);
- --color-surface-hover: var(--color-bg-elevated);
- --color-surface-border: var(--color-border);
- }
-}
diff --git a/packages/cloud/app/src/style/token/font.css b/packages/cloud/app/src/style/token/font.css
deleted file mode 100644
index 67143e662..000000000
--- a/packages/cloud/app/src/style/token/font.css
+++ /dev/null
@@ -1,20 +0,0 @@
-body {
- --font-size-2xs: 0.6875rem;
- --font-size-xs: 0.75rem;
- --font-size-sm: 0.8125rem;
- --font-size-md: 0.9375rem;
- --font-size-lg: 1.125rem;
- --font-size-xl: 1.25rem;
- --font-size-2xl: 1.5rem;
- --font-size-3xl: 1.875rem;
- --font-size-4xl: 2.25rem;
- --font-size-5xl: 3rem;
- --font-size-6xl: 3.75rem;
- --font-size-7xl: 4.5rem;
- --font-size-8xl: 6rem;
- --font-size-9xl: 8rem;
-
- --font-mono:
- "IBM Plex Mono", ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
- --font-sans: var(--font-mono);
-}
diff --git a/packages/cloud/app/src/style/token/space.css b/packages/cloud/app/src/style/token/space.css
deleted file mode 100644
index 7e1a1b397..000000000
--- a/packages/cloud/app/src/style/token/space.css
+++ /dev/null
@@ -1,46 +0,0 @@
-body {
- --space-0: 0;
- --space-px: 1px;
- --space-0-5: 0.125rem;
- --space-0-75: 0.1875rem;
- --space-1: 0.25rem;
- --space-1-5: 0.375rem;
- --space-2: 0.5rem;
- --space-2-5: 0.625rem;
- --space-3: 0.75rem;
- --space-3-5: 0.875rem;
- --space-4: 1rem;
- --space-4-5: 1.125rem;
- --space-5: 1.25rem;
- --space-6: 1.5rem;
- --space-7: 1.75rem;
- --space-8: 2rem;
- --space-9: 2.25rem;
- --space-10: 2.5rem;
- --space-11: 2.75rem;
- --space-12: 3rem;
- --space-14: 3.5rem;
- --space-16: 4rem;
- --space-17: 4.25rem;
- --space-18: 4.5rem;
- --space-19: 4.75rem;
- --space-20: 5rem;
- --space-24: 6rem;
- --space-28: 7rem;
- --space-32: 8rem;
- --space-36: 9rem;
- --space-40: 10rem;
- --space-44: 11rem;
- --space-48: 12rem;
- --space-52: 13rem;
- --space-56: 14rem;
- --space-60: 15rem;
- --space-64: 16rem;
- --space-72: 18rem;
- --space-80: 20rem;
- --space-96: 24rem;
-
- --border-radius-sm: 0.1875rem;
- --border-radius-md: 0.3125rem;
- --border-radius-lg: 0.5rem;
-}
diff --git a/packages/cloud/app/sst-env.d.ts b/packages/cloud/app/sst-env.d.ts
deleted file mode 100644
index bd5588217..000000000
--- a/packages/cloud/app/sst-env.d.ts
+++ /dev/null
@@ -1,9 +0,0 @@
-/* This file is auto-generated by SST. Do not edit. */
-/* tslint:disable */
-/* eslint-disable */
-/* deno-fmt-ignore-file */
-
-/// <reference path="../../../sst-env.d.ts" />
-
-import "sst"
-export {}
diff --git a/packages/cloud/app/tsconfig.json b/packages/cloud/app/tsconfig.json
deleted file mode 100644
index 07148a458..000000000
--- a/packages/cloud/app/tsconfig.json
+++ /dev/null
@@ -1,21 +0,0 @@
-{
- "$schema": "https://json.schemastore.org/tsconfig",
- "compilerOptions": {
- "target": "ESNext",
- "module": "ESNext",
- "skipLibCheck": true,
- "moduleResolution": "bundler",
- "allowSyntheticDefaultImports": true,
- "esModuleInterop": true,
- "jsx": "preserve",
- "jsxImportSource": "solid-js",
- "allowJs": true,
- "strict": true,
- "noEmit": true,
- "types": ["vinxi/types/client"],
- "isolatedModules": true,
- "paths": {
- "~/*": ["./src/*"]
- }
- }
-}