diff options
| author | Frank <[email protected]> | 2025-09-18 01:32:40 -0400 |
|---|---|---|
| committer | Frank <[email protected]> | 2025-09-18 01:32:40 -0400 |
| commit | fc4f281408c56ab12db571a470456212a479edf5 (patch) | |
| tree | 309d23b0c497bc61af6f8e650a6036fa41d7cbdb /cloud/app/src | |
| parent | f8c4f713a5b48892899d0ac195c3470ab7ef764c (diff) | |
| download | opencode-fc4f281408c56ab12db571a470456212a479edf5.tar.gz opencode-fc4f281408c56ab12db571a470456212a479edf5.zip | |
wip: zen
Diffstat (limited to 'cloud/app/src')
61 files changed, 0 insertions, 4452 deletions
diff --git a/cloud/app/src/app.css b/cloud/app/src/app.css deleted file mode 100644 index c0261c422..000000000 --- a/cloud/app/src/app.css +++ /dev/null @@ -1 +0,0 @@ -@import "./style/index.css"; diff --git a/cloud/app/src/app.tsx b/cloud/app/src/app.tsx deleted file mode 100644 index bc3961214..000000000 --- a/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/cloud/app/src/asset/lander/check.svg b/cloud/app/src/asset/lander/check.svg deleted file mode 100644 index 22de6f2a8..000000000 --- a/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/cloud/app/src/asset/lander/copy.svg b/cloud/app/src/asset/lander/copy.svg deleted file mode 100644 index f1baac30a..000000000 --- a/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/cloud/app/src/asset/lander/screenshot-github.png b/cloud/app/src/asset/lander/screenshot-github.png Binary files differdeleted file mode 100644 index fda74e641..000000000 --- a/cloud/app/src/asset/lander/screenshot-github.png +++ /dev/null diff --git a/cloud/app/src/asset/lander/screenshot-splash.png b/cloud/app/src/asset/lander/screenshot-splash.png Binary files differdeleted file mode 100644 index e900673ef..000000000 --- a/cloud/app/src/asset/lander/screenshot-splash.png +++ /dev/null diff --git a/cloud/app/src/asset/lander/screenshot-vscode.png b/cloud/app/src/asset/lander/screenshot-vscode.png Binary files differdeleted file mode 100644 index b8966a6b8..000000000 --- a/cloud/app/src/asset/lander/screenshot-vscode.png +++ /dev/null diff --git a/cloud/app/src/asset/lander/screenshot.png b/cloud/app/src/asset/lander/screenshot.png Binary files differdeleted file mode 100644 index feb617585..000000000 --- a/cloud/app/src/asset/lander/screenshot.png +++ /dev/null diff --git a/cloud/app/src/asset/logo-ornate-dark.svg b/cloud/app/src/asset/logo-ornate-dark.svg deleted file mode 100644 index 2efda934d..000000000 --- a/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/cloud/app/src/asset/logo-ornate-light.svg b/cloud/app/src/asset/logo-ornate-light.svg deleted file mode 100644 index 789223bc4..000000000 --- a/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/cloud/app/src/asset/logo.svg b/cloud/app/src/asset/logo.svg deleted file mode 100644 index cbfcccf51..000000000 --- a/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/cloud/app/src/component/icon.tsx b/cloud/app/src/component/icon.tsx deleted file mode 100644 index a82572e62..000000000 --- a/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/cloud/app/src/component/workspace/billing-section.module.css b/cloud/app/src/component/workspace/billing-section.module.css deleted file mode 100644 index 0bb5709cb..000000000 --- a/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/cloud/app/src/component/workspace/billing-section.tsx b/cloud/app/src/component/workspace/billing-section.tsx deleted file mode 100644 index ec314d9ef..000000000 --- a/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/cloud/app/src/component/workspace/common.tsx b/cloud/app/src/component/workspace/common.tsx deleted file mode 100644 index f85fd8423..000000000 --- a/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/cloud/app/src/component/workspace/key-section.module.css b/cloud/app/src/component/workspace/key-section.module.css deleted file mode 100644 index 6a1d0c85f..000000000 --- a/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/cloud/app/src/component/workspace/key-section.tsx b/cloud/app/src/component/workspace/key-section.tsx deleted file mode 100644 index 4158ce793..000000000 --- a/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/cloud/app/src/component/workspace/monthly-limit-section.module.css b/cloud/app/src/component/workspace/monthly-limit-section.module.css deleted file mode 100644 index 02de058e4..000000000 --- a/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/cloud/app/src/component/workspace/monthly-limit-section.tsx b/cloud/app/src/component/workspace/monthly-limit-section.tsx deleted file mode 100644 index 5c1077ab1..000000000 --- a/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/cloud/app/src/component/workspace/new-user-section.module.css b/cloud/app/src/component/workspace/new-user-section.module.css deleted file mode 100644 index 2edc7cc14..000000000 --- a/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/cloud/app/src/component/workspace/new-user-section.tsx b/cloud/app/src/component/workspace/new-user-section.tsx deleted file mode 100644 index 6e031e371..000000000 --- a/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/cloud/app/src/component/workspace/payment-section.module.css b/cloud/app/src/component/workspace/payment-section.module.css deleted file mode 100644 index ea8e2ed42..000000000 --- a/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/cloud/app/src/component/workspace/payment-section.tsx b/cloud/app/src/component/workspace/payment-section.tsx deleted file mode 100644 index 8cdceebc3..000000000 --- a/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/cloud/app/src/component/workspace/usage-section.module.css b/cloud/app/src/component/workspace/usage-section.module.css deleted file mode 100644 index 1a772ba87..000000000 --- a/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/cloud/app/src/component/workspace/usage-section.tsx b/cloud/app/src/component/workspace/usage-section.tsx deleted file mode 100644 index 5d3d3b6c3..000000000 --- a/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/cloud/app/src/context/auth.session.ts b/cloud/app/src/context/auth.session.ts deleted file mode 100644 index 609bc364b..000000000 --- a/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/cloud/app/src/context/auth.ts b/cloud/app/src/context/auth.ts deleted file mode 100644 index e08d965b8..000000000 --- a/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/cloud/app/src/context/auth.withActor.ts b/cloud/app/src/context/auth.withActor.ts deleted file mode 100644 index 4cfd5c3e0..000000000 --- a/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/cloud/app/src/entry-client.tsx b/cloud/app/src/entry-client.tsx deleted file mode 100644 index 642deacf7..000000000 --- a/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/cloud/app/src/entry-server.tsx b/cloud/app/src/entry-server.tsx deleted file mode 100644 index d5fca6aa5..000000000 --- a/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/cloud/app/src/global.d.ts b/cloud/app/src/global.d.ts deleted file mode 100644 index dc6f10c22..000000000 --- a/cloud/app/src/global.d.ts +++ /dev/null @@ -1 +0,0 @@ -/// <reference types="@solidjs/start/env" /> diff --git a/cloud/app/src/middleware.ts b/cloud/app/src/middleware.ts deleted file mode 100644 index b49473cbe..000000000 --- a/cloud/app/src/middleware.ts +++ /dev/null @@ -1,5 +0,0 @@ -import { defineMiddleware } from "vinxi/http" - -export default defineMiddleware({ - onBeforeResponse() {}, -}) diff --git a/cloud/app/src/routes/[...404].css b/cloud/app/src/routes/[...404].css deleted file mode 100644 index 1edbd0a5a..000000000 --- a/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/cloud/app/src/routes/[...404].tsx b/cloud/app/src/routes/[...404].tsx deleted file mode 100644 index ba2842b5a..000000000 --- a/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/cloud/app/src/routes/auth/authorize.ts b/cloud/app/src/routes/auth/authorize.ts deleted file mode 100644 index 166466ef8..000000000 --- a/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/cloud/app/src/routes/auth/callback.ts b/cloud/app/src/routes/auth/callback.ts deleted file mode 100644 index 23025b54d..000000000 --- a/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/cloud/app/src/routes/auth/index.ts b/cloud/app/src/routes/auth/index.ts deleted file mode 100644 index 308ae2d1d..000000000 --- a/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/cloud/app/src/routes/debug/index.ts b/cloud/app/src/routes/debug/index.ts deleted file mode 100644 index 8c7eb7bd8..000000000 --- a/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/cloud/app/src/routes/discord.ts b/cloud/app/src/routes/discord.ts deleted file mode 100644 index 7088295da..000000000 --- a/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/cloud/app/src/routes/docs/[...path].ts b/cloud/app/src/routes/docs/[...path].ts deleted file mode 100644 index f07781583..000000000 --- a/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/cloud/app/src/routes/docs/index.ts b/cloud/app/src/routes/docs/index.ts deleted file mode 100644 index f07781583..000000000 --- a/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/cloud/app/src/routes/index.css b/cloud/app/src/routes/index.css deleted file mode 100644 index fe95bb7ea..000000000 --- a/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/cloud/app/src/routes/index.tsx b/cloud/app/src/routes/index.tsx deleted file mode 100644 index 9075f4079..000000000 --- a/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/cloud/app/src/routes/s/[id].ts b/cloud/app/src/routes/s/[id].ts deleted file mode 100644 index 3fd1305a0..000000000 --- a/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/cloud/app/src/routes/stripe/webhook.ts b/cloud/app/src/routes/stripe/webhook.ts deleted file mode 100644 index 925ede1ac..000000000 --- a/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/cloud/app/src/routes/workspace.css b/cloud/app/src/routes/workspace.css deleted file mode 100644 index ed94365f0..000000000 --- a/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/cloud/app/src/routes/workspace.tsx b/cloud/app/src/routes/workspace.tsx deleted file mode 100644 index 3f08a70a0..000000000 --- a/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/cloud/app/src/routes/workspace/[id].css b/cloud/app/src/routes/workspace/[id].css deleted file mode 100644 index 8b318a19f..000000000 --- a/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/cloud/app/src/routes/workspace/[id].tsx b/cloud/app/src/routes/workspace/[id].tsx deleted file mode 100644 index 4a2c3424d..000000000 --- a/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/cloud/app/src/routes/workspace/index.tsx b/cloud/app/src/routes/workspace/index.tsx deleted file mode 100644 index e69de29bb..000000000 --- a/cloud/app/src/routes/workspace/index.tsx +++ /dev/null diff --git a/cloud/app/src/routes/zen/handler.ts b/cloud/app/src/routes/zen/handler.ts deleted file mode 100644 index ab1fc6599..000000000 --- a/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/cloud/app/src/routes/zen/v1/chat/completions.ts b/cloud/app/src/routes/zen/v1/chat/completions.ts deleted file mode 100644 index 801557324..000000000 --- a/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/cloud/app/src/routes/zen/v1/messages.ts b/cloud/app/src/routes/zen/v1/messages.ts deleted file mode 100644 index 1fd85d5c7..000000000 --- a/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/cloud/app/src/routes/zen/v1/responses.ts b/cloud/app/src/routes/zen/v1/responses.ts deleted file mode 100644 index 486c129b9..000000000 --- a/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/cloud/app/src/style/base.css b/cloud/app/src/style/base.css deleted file mode 100644 index a4847ed43..000000000 --- a/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/cloud/app/src/style/component/button.css b/cloud/app/src/style/component/button.css deleted file mode 100644 index d10f7af53..000000000 --- a/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/cloud/app/src/style/index.css b/cloud/app/src/style/index.css deleted file mode 100644 index 832a901e8..000000000 --- a/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/cloud/app/src/style/reset.css b/cloud/app/src/style/reset.css deleted file mode 100644 index d331ed724..000000000 --- a/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/cloud/app/src/style/token/color.css b/cloud/app/src/style/token/color.css deleted file mode 100644 index f1a097d2f..000000000 --- a/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/cloud/app/src/style/token/font.css b/cloud/app/src/style/token/font.css deleted file mode 100644 index 67143e662..000000000 --- a/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/cloud/app/src/style/token/space.css b/cloud/app/src/style/token/space.css deleted file mode 100644 index 7e1a1b397..000000000 --- a/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; -} |
