summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
-rw-r--r--packages/console/app/src/lib/beta.ts7
-rw-r--r--packages/console/app/src/routes/workspace.tsx16
-rw-r--r--packages/console/app/src/routes/workspace/[id].css156
-rw-r--r--packages/console/app/src/routes/workspace/[id].tsx89
-rw-r--r--packages/console/app/src/routes/workspace/[id]/billing/billing-section.module.css (renamed from packages/console/app/src/routes/workspace/billing-section.module.css)0
-rw-r--r--packages/console/app/src/routes/workspace/[id]/billing/billing-section.tsx (renamed from packages/console/app/src/routes/workspace/billing-section.tsx)0
-rw-r--r--packages/console/app/src/routes/workspace/[id]/billing/index.css116
-rw-r--r--packages/console/app/src/routes/workspace/[id]/billing/index.tsx24
-rw-r--r--packages/console/app/src/routes/workspace/[id]/billing/monthly-limit-section.module.css (renamed from packages/console/app/src/routes/workspace/monthly-limit-section.module.css)0
-rw-r--r--packages/console/app/src/routes/workspace/[id]/billing/monthly-limit-section.tsx (renamed from packages/console/app/src/routes/workspace/monthly-limit-section.tsx)0
-rw-r--r--packages/console/app/src/routes/workspace/[id]/billing/payment-section.module.css (renamed from packages/console/app/src/routes/workspace/payment-section.module.css)0
-rw-r--r--packages/console/app/src/routes/workspace/[id]/billing/payment-section.tsx (renamed from packages/console/app/src/routes/workspace/payment-section.tsx)10
-rw-r--r--packages/console/app/src/routes/workspace/[id]/common.tsx25
-rw-r--r--packages/console/app/src/routes/workspace/[id]/index.css116
-rw-r--r--packages/console/app/src/routes/workspace/[id]/index.tsx39
-rw-r--r--packages/console/app/src/routes/workspace/[id]/keys/index.css116
-rw-r--r--packages/console/app/src/routes/workspace/[id]/keys/index.tsx12
-rw-r--r--packages/console/app/src/routes/workspace/[id]/keys/key-section.module.css (renamed from packages/console/app/src/routes/workspace/key-section.module.css)0
-rw-r--r--packages/console/app/src/routes/workspace/[id]/keys/key-section.tsx (renamed from packages/console/app/src/routes/workspace/key-section.tsx)2
-rw-r--r--packages/console/app/src/routes/workspace/[id]/members/index.css116
-rw-r--r--packages/console/app/src/routes/workspace/[id]/members/index.tsx12
-rw-r--r--packages/console/app/src/routes/workspace/[id]/members/member-section.module.css (renamed from packages/console/app/src/routes/workspace/member-section.module.css)0
-rw-r--r--packages/console/app/src/routes/workspace/[id]/members/member-section.tsx (renamed from packages/console/app/src/routes/workspace/member-section.tsx)0
-rw-r--r--packages/console/app/src/routes/workspace/[id]/model-section.module.css (renamed from packages/console/app/src/routes/workspace/model-section.module.css)0
-rw-r--r--packages/console/app/src/routes/workspace/[id]/model-section.tsx (renamed from packages/console/app/src/routes/workspace/model-section.tsx)0
-rw-r--r--packages/console/app/src/routes/workspace/[id]/new-user-section.module.css (renamed from packages/console/app/src/routes/workspace/new-user-section.module.css)0
-rw-r--r--packages/console/app/src/routes/workspace/[id]/new-user-section.tsx (renamed from packages/console/app/src/routes/workspace/new-user-section.tsx)0
-rw-r--r--packages/console/app/src/routes/workspace/[id]/provider-section.module.css (renamed from packages/console/app/src/routes/workspace/provider-section.module.css)0
-rw-r--r--packages/console/app/src/routes/workspace/[id]/provider-section.tsx (renamed from packages/console/app/src/routes/workspace/provider-section.tsx)0
-rw-r--r--packages/console/app/src/routes/workspace/[id]/settings/index.css116
-rw-r--r--packages/console/app/src/routes/workspace/[id]/settings/index.tsx12
-rw-r--r--packages/console/app/src/routes/workspace/[id]/settings/settings-section.module.css (renamed from packages/console/app/src/routes/workspace/settings-section.module.css)0
-rw-r--r--packages/console/app/src/routes/workspace/[id]/settings/settings-section.tsx (renamed from packages/console/app/src/routes/workspace/settings-section.tsx)0
-rw-r--r--packages/console/app/src/routes/workspace/[id]/usage-section.module.css (renamed from packages/console/app/src/routes/workspace/usage-section.module.css)0
-rw-r--r--packages/console/app/src/routes/workspace/[id]/usage-section.tsx (renamed from packages/console/app/src/routes/workspace/usage-section.tsx)0
-rw-r--r--packages/console/app/src/routes/workspace/common.tsx37
-rw-r--r--packages/console/app/src/routes/workspace/index.tsx0
37 files changed, 809 insertions, 212 deletions
diff --git a/packages/console/app/src/lib/beta.ts b/packages/console/app/src/lib/beta.ts
deleted file mode 100644
index d60a735ee..000000000
--- a/packages/console/app/src/lib/beta.ts
+++ /dev/null
@@ -1,7 +0,0 @@
-import { query } from "@solidjs/router"
-import { Resource } from "@opencode-ai/console-resource"
-
-export const beta = query(async (workspaceID?: string) => {
- "use server"
- return Resource.App.stage === "production" ? workspaceID === "wrk_01K46JDFR0E75SG2Q8K172KF3Y" : true
-}, "beta")
diff --git a/packages/console/app/src/routes/workspace.tsx b/packages/console/app/src/routes/workspace.tsx
index ac394f585..f87123d31 100644
--- a/packages/console/app/src/routes/workspace.tsx
+++ b/packages/console/app/src/routes/workspace.tsx
@@ -8,16 +8,16 @@ import { WorkspacePicker } from "./workspace-picker"
import { withActor } from "~/context/auth.withActor"
import { User } from "@opencode-ai/console-core/user.js"
import { Actor } from "@opencode-ai/console-core/actor.js"
-import { beta } from "~/lib/beta"
+import { querySessionInfo } from "./workspace/common"
-const getUserInfo = query(async (workspaceID: string) => {
+const getUserEmail = query(async (workspaceID: string) => {
"use server"
return withActor(async () => {
const actor = Actor.assert("user")
const email = await User.getAccountEmail(actor.properties.userID)
- return { email }
+ return email
}, workspaceID)
-}, "userInfo")
+}, "userEmail")
const logout = action(async () => {
"use server"
@@ -37,8 +37,8 @@ const logout = action(async () => {
export default function WorkspaceLayout(props: RouteSectionProps) {
const params = useParams()
- const userInfo = createAsync(() => getUserInfo(params.id))
- const isBeta = createAsync(() => beta(params.id))
+ const userEmail = createAsync(() => getUserEmail(params.id))
+ const sessionInfo = createAsync(() => querySessionInfo(params.id))
return (
<main data-page="workspace">
<header data-component="workspace-header">
@@ -48,10 +48,10 @@ export default function WorkspaceLayout(props: RouteSectionProps) {
</A>
</div>
<div data-slot="header-actions">
- <Show when={isBeta()}>
+ <Show when={sessionInfo()?.isBeta}>
<WorkspacePicker />
</Show>
- <span data-slot="user">{userInfo()?.email}</span>
+ <span data-slot="user">{userEmail()}</span>
<form action={logout} method="post">
<button type="submit" formaction={logout}>
Logout
diff --git a/packages/console/app/src/routes/workspace/[id].css b/packages/console/app/src/routes/workspace/[id].css
index 8b318a19f..399d7e737 100644
--- a/packages/console/app/src/routes/workspace/[id].css
+++ b/packages/console/app/src/routes/workspace/[id].css
@@ -1,115 +1,63 @@
-[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);
+[data-page="workspace"] {
+ line-height: 1;
+}
- a {
- color: var(--color-text-muted);
- }
+/* Workspace Layout */
+[data-component="workspace-container"] {
+ display: flex;
+ height: calc(100vh - 73px);
+}
- @media (max-width: 30rem) {
- font-size: var(--font-size-sm);
- }
- }
- }
+[data-component="workspace-nav"] {
+ width: 240px;
+ flex-shrink: 0;
+ border-right: 1px solid var(--color-border);
+ padding: var(--space-6) var(--space-4);
+ display: flex;
+ flex-direction: column;
+ gap: var(--space-2);
+
+ [data-nav-button] {
+ padding: var(--space-3) var(--space-4);
+ border-radius: var(--border-radius-sm);
+ color: var(--color-text-muted);
+ text-decoration: none;
+ font-size: var(--font-size-sm);
+ font-weight: 500;
+ transition: all 0.15s ease;
+
+ &:hover {
+ background-color: var(--color-surface-hover);
+ color: var(--color-text);
}
- 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);
- }
+ &.active {
+ background-color: var(--color-surface-hover);
+ color: var(--color-text);
}
}
+}
- /* 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;
+[data-component="workspace-content"] {
+ flex: 1;
+ padding: var(--space-6) var(--space-8);
+ overflow-y: auto;
- @media (max-width: 30rem) {
- font-size: var(--font-size-xl);
- }
- }
+ @media (max-width: 48rem) {
+ padding: var(--space-6) var(--space-4);
+ }
+}
- p {
- line-height: 1.5;
- font-size: var(--font-size-md);
- color: var(--color-text-muted);
+@media (max-width: 48rem) {
+ [data-component="workspace-container"] {
+ flex-direction: column;
+ }
- a {
- color: var(--color-text-muted);
- }
- }
+ [data-component="workspace-nav"] {
+ width: 100%;
+ flex-direction: row;
+ border-right: none;
+ border-bottom: 1px solid var(--color-border);
+ padding: var(--space-4);
}
-}
+} \ No newline at end of file
diff --git a/packages/console/app/src/routes/workspace/[id].tsx b/packages/console/app/src/routes/workspace/[id].tsx
index 15aeb57a0..1da24dc32 100644
--- a/packages/console/app/src/routes/workspace/[id].tsx
+++ b/packages/console/app/src/routes/workspace/[id].tsx
@@ -1,70 +1,35 @@
-import "./[id].css"
-import { MonthlyLimitSection } from "./monthly-limit-section"
-import { NewUserSection } from "./new-user-section"
-import { BillingSection } from "./billing-section"
-import { PaymentSection } from "./payment-section"
-import { UsageSection } from "./usage-section"
-import { KeySection } from "./key-section"
-import { MemberSection } from "./member-section"
-import { SettingsSection } from "./settings-section"
-import { ModelSection } from "./model-section"
-import { ProviderSection } from "./provider-section"
import { Show } from "solid-js"
-import { createAsync, query, useParams } from "@solidjs/router"
-import { Actor } from "@opencode-ai/console-core/actor.js"
-import { withActor } from "~/context/auth.withActor"
-import { User } from "@opencode-ai/console-core/user.js"
-import { beta } from "~/lib/beta"
-
-const getUserInfo = query(async (workspaceID: string) => {
- "use server"
- return withActor(async () => {
- const actor = Actor.assert("user")
- const user = await User.fromID(actor.properties.userID)
- return {
- isAdmin: user?.role === "admin",
- }
- }, workspaceID)
-}, "user.get")
+import { createAsync, RouteSectionProps, useParams, A } from "@solidjs/router"
+import { querySessionInfo } from "./common"
+import "./[id].css"
-export default function () {
+export default function WorkspaceLayout(props: RouteSectionProps) {
const params = useParams()
- const userInfo = createAsync(() => getUserInfo(params.id))
- const isBeta = createAsync(() => beta(params.id))
-
+ const userInfo = createAsync(() => querySessionInfo(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 />
- <Show when={isBeta()}>
- <MemberSection />
- </Show>
- <Show when={userInfo()?.isAdmin}>
- <Show when={isBeta()}>
- <SettingsSection />
- <ModelSection />
- <ProviderSection />
+ <main data-page="workspace">
+ <div data-component="workspace-container">
+ <nav data-component="workspace-nav">
+ <A href={`/workspace/${params.id}`} end activeClass="active" data-nav-button>
+ Zen
+ </A>
+ <Show when={userInfo()?.isAdmin}>
+ <A href={`/workspace/${params.id}/billing`} activeClass="active" data-nav-button>
+ Billing
+ </A>
</Show>
- <BillingSection />
- <MonthlyLimitSection />
- </Show>
- <UsageSection />
- <Show when={userInfo()?.isAdmin}>
- <PaymentSection />
- </Show>
+ <A href={`/workspace/${params.id}/keys`} activeClass="active" data-nav-button>
+ API Keys
+ </A>
+ <A href={`/workspace/${params.id}/members`} activeClass="active" data-nav-button>
+ Members
+ </A>
+ <A href={`/workspace/${params.id}/settings`} activeClass="active" data-nav-button>
+ Settings
+ </A>
+ </nav>
+ <div data-component="workspace-content">{props.children}</div>
</div>
- </div>
+ </main>
)
}
diff --git a/packages/console/app/src/routes/workspace/billing-section.module.css b/packages/console/app/src/routes/workspace/[id]/billing/billing-section.module.css
index 0bb5709cb..0bb5709cb 100644
--- a/packages/console/app/src/routes/workspace/billing-section.module.css
+++ b/packages/console/app/src/routes/workspace/[id]/billing/billing-section.module.css
diff --git a/packages/console/app/src/routes/workspace/billing-section.tsx b/packages/console/app/src/routes/workspace/[id]/billing/billing-section.tsx
index 295ad3396..295ad3396 100644
--- a/packages/console/app/src/routes/workspace/billing-section.tsx
+++ b/packages/console/app/src/routes/workspace/[id]/billing/billing-section.tsx
diff --git a/packages/console/app/src/routes/workspace/[id]/billing/index.css b/packages/console/app/src/routes/workspace/[id]/billing/index.css
new file mode 100644
index 000000000..5124c78cf
--- /dev/null
+++ b/packages/console/app/src/routes/workspace/[id]/billing/index.css
@@ -0,0 +1,116 @@
+[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);
+ }
+ }
+ }
+} \ No newline at end of file
diff --git a/packages/console/app/src/routes/workspace/[id]/billing/index.tsx b/packages/console/app/src/routes/workspace/[id]/billing/index.tsx
new file mode 100644
index 000000000..913d4f92f
--- /dev/null
+++ b/packages/console/app/src/routes/workspace/[id]/billing/index.tsx
@@ -0,0 +1,24 @@
+import "./index.css"
+import { MonthlyLimitSection } from "./monthly-limit-section"
+import { BillingSection } from "./billing-section"
+import { PaymentSection } from "./payment-section"
+import { Show } from "solid-js"
+import { createAsync, useParams } from "@solidjs/router"
+import { querySessionInfo } from "../../common"
+
+export default function () {
+ const params = useParams()
+ const userInfo = createAsync(() => querySessionInfo(params.id))
+
+ return (
+ <div data-page="workspace-[id]">
+ <div data-slot="sections">
+ <Show when={userInfo()?.isAdmin}>
+ <BillingSection />
+ <MonthlyLimitSection />
+ <PaymentSection />
+ </Show>
+ </div>
+ </div>
+ )
+}
diff --git a/packages/console/app/src/routes/workspace/monthly-limit-section.module.css b/packages/console/app/src/routes/workspace/[id]/billing/monthly-limit-section.module.css
index 02de058e4..02de058e4 100644
--- a/packages/console/app/src/routes/workspace/monthly-limit-section.module.css
+++ b/packages/console/app/src/routes/workspace/[id]/billing/monthly-limit-section.module.css
diff --git a/packages/console/app/src/routes/workspace/monthly-limit-section.tsx b/packages/console/app/src/routes/workspace/[id]/billing/monthly-limit-section.tsx
index dbeda115c..dbeda115c 100644
--- a/packages/console/app/src/routes/workspace/monthly-limit-section.tsx
+++ b/packages/console/app/src/routes/workspace/[id]/billing/monthly-limit-section.tsx
diff --git a/packages/console/app/src/routes/workspace/payment-section.module.css b/packages/console/app/src/routes/workspace/[id]/billing/payment-section.module.css
index 2e1afe78b..2e1afe78b 100644
--- a/packages/console/app/src/routes/workspace/payment-section.module.css
+++ b/packages/console/app/src/routes/workspace/[id]/billing/payment-section.module.css
diff --git a/packages/console/app/src/routes/workspace/payment-section.tsx b/packages/console/app/src/routes/workspace/[id]/billing/payment-section.tsx
index c35a50660..d3520bea4 100644
--- a/packages/console/app/src/routes/workspace/payment-section.tsx
+++ b/packages/console/app/src/routes/workspace/[id]/billing/payment-section.tsx
@@ -1,8 +1,8 @@
import { Billing } from "@opencode-ai/console-core/billing.js"
import { query, action, useParams, createAsync, useAction } from "@solidjs/router"
-import { For } from "solid-js"
+import { For, Show } from "solid-js"
import { withActor } from "~/context/auth.withActor"
-import { formatDateUTC, formatDateForTable } from "./common"
+import { formatDateUTC, formatDateForTable } from "../common"
import styles from "./payment-section.module.css"
const getPaymentsInfo = query(async (workspaceID: string) => {
@@ -19,7 +19,6 @@ const downloadReceipt = action(async (workspaceID: string, paymentID: string) =>
export function PaymentSection() {
const params = useParams()
- // ORIGINAL CODE - COMMENTED OUT FOR TESTING
const payments = createAsync(() => getPaymentsInfo(params.id))
const downloadReceiptAction = useAction(downloadReceipt)
@@ -58,8 +57,7 @@ export function PaymentSection() {
// ]
return (
- payments() &&
- payments()!.length > 0 && (
+ <Show when={payments() && payments()!.length > 0}>
<section class={styles.root}>
<div data-slot="section-title">
<h2>Payments History</h2>
@@ -109,6 +107,6 @@ export function PaymentSection() {
</table>
</div>
</section>
- )
+ </Show>
)
}
diff --git a/packages/console/app/src/routes/workspace/[id]/common.tsx b/packages/console/app/src/routes/workspace/[id]/common.tsx
new file mode 100644
index 000000000..f85fd8423
--- /dev/null
+++ b/packages/console/app/src/routes/workspace/[id]/common.tsx
@@ -0,0 +1,25 @@
+export function formatDateForTable(date: Date) {
+ const options: Intl.DateTimeFormatOptions = {
+ day: "numeric",
+ month: "short",
+ hour: "numeric",
+ minute: "2-digit",
+ hour12: true,
+ }
+ return date.toLocaleDateString("en-GB", options).replace(",", ",")
+}
+
+export function formatDateUTC(date: Date) {
+ const options: Intl.DateTimeFormatOptions = {
+ weekday: "short",
+ year: "numeric",
+ month: "short",
+ day: "numeric",
+ hour: "numeric",
+ minute: "2-digit",
+ second: "2-digit",
+ timeZoneName: "short",
+ timeZone: "UTC",
+ }
+ return date.toLocaleDateString("en-US", options)
+}
diff --git a/packages/console/app/src/routes/workspace/[id]/index.css b/packages/console/app/src/routes/workspace/[id]/index.css
new file mode 100644
index 000000000..5124c78cf
--- /dev/null
+++ b/packages/console/app/src/routes/workspace/[id]/index.css
@@ -0,0 +1,116 @@
+[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);
+ }
+ }
+ }
+} \ No newline at end of file
diff --git a/packages/console/app/src/routes/workspace/[id]/index.tsx b/packages/console/app/src/routes/workspace/[id]/index.tsx
new file mode 100644
index 000000000..1345bf40f
--- /dev/null
+++ b/packages/console/app/src/routes/workspace/[id]/index.tsx
@@ -0,0 +1,39 @@
+import "./index.css"
+import { NewUserSection } from "./new-user-section"
+import { UsageSection } from "./usage-section"
+import { MemberSection } from "./members/member-section"
+import { SettingsSection } from "./settings/settings-section"
+import { ModelSection } from "./model-section"
+import { ProviderSection } from "./provider-section"
+import { Show } from "solid-js"
+import { createAsync, useParams } from "@solidjs/router"
+import { querySessionInfo } from "../common"
+
+export default function () {
+ const params = useParams()
+ const userInfo = createAsync(() => querySessionInfo(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 />
+ <Show when={userInfo()?.isAdmin}>
+ <ModelSection />
+ <ProviderSection />
+ </Show>
+ <UsageSection />
+ </div>
+ </div>
+ )
+}
diff --git a/packages/console/app/src/routes/workspace/[id]/keys/index.css b/packages/console/app/src/routes/workspace/[id]/keys/index.css
new file mode 100644
index 000000000..5124c78cf
--- /dev/null
+++ b/packages/console/app/src/routes/workspace/[id]/keys/index.css
@@ -0,0 +1,116 @@
+[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);
+ }
+ }
+ }
+} \ No newline at end of file
diff --git a/packages/console/app/src/routes/workspace/[id]/keys/index.tsx b/packages/console/app/src/routes/workspace/[id]/keys/index.tsx
new file mode 100644
index 000000000..0fd3cdbd3
--- /dev/null
+++ b/packages/console/app/src/routes/workspace/[id]/keys/index.tsx
@@ -0,0 +1,12 @@
+import "./index.css"
+import { KeySection } from "./key-section"
+
+export default function () {
+ return (
+ <div data-page="workspace-[id]">
+ <div data-slot="sections">
+ <KeySection />
+ </div>
+ </div>
+ )
+}
diff --git a/packages/console/app/src/routes/workspace/key-section.module.css b/packages/console/app/src/routes/workspace/[id]/keys/key-section.module.css
index 6a1d0c85f..6a1d0c85f 100644
--- a/packages/console/app/src/routes/workspace/key-section.module.css
+++ b/packages/console/app/src/routes/workspace/[id]/keys/key-section.module.css
diff --git a/packages/console/app/src/routes/workspace/key-section.tsx b/packages/console/app/src/routes/workspace/[id]/keys/key-section.tsx
index 3b7e399aa..22b82ae05 100644
--- a/packages/console/app/src/routes/workspace/key-section.tsx
+++ b/packages/console/app/src/routes/workspace/[id]/keys/key-section.tsx
@@ -4,7 +4,7 @@ import { IconCopy, IconCheck } from "~/component/icon"
import { Key } from "@opencode-ai/console-core/key.js"
import { withActor } from "~/context/auth.withActor"
import { createStore } from "solid-js/store"
-import { formatDateUTC, formatDateForTable } from "./common"
+import { formatDateUTC, formatDateForTable } from "../common"
import styles from "./key-section.module.css"
import { Actor } from "@opencode-ai/console-core/actor.js"
diff --git a/packages/console/app/src/routes/workspace/[id]/members/index.css b/packages/console/app/src/routes/workspace/[id]/members/index.css
new file mode 100644
index 000000000..5124c78cf
--- /dev/null
+++ b/packages/console/app/src/routes/workspace/[id]/members/index.css
@@ -0,0 +1,116 @@
+[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);
+ }
+ }
+ }
+} \ No newline at end of file
diff --git a/packages/console/app/src/routes/workspace/[id]/members/index.tsx b/packages/console/app/src/routes/workspace/[id]/members/index.tsx
new file mode 100644
index 000000000..5845e144c
--- /dev/null
+++ b/packages/console/app/src/routes/workspace/[id]/members/index.tsx
@@ -0,0 +1,12 @@
+import "./index.css"
+import { MemberSection } from "./member-section"
+
+export default function () {
+ return (
+ <div data-page="workspace-[id]">
+ <div data-slot="sections">
+ <MemberSection />
+ </div>
+ </div>
+ )
+}
diff --git a/packages/console/app/src/routes/workspace/member-section.module.css b/packages/console/app/src/routes/workspace/[id]/members/member-section.module.css
index 16b6ff8d2..16b6ff8d2 100644
--- a/packages/console/app/src/routes/workspace/member-section.module.css
+++ b/packages/console/app/src/routes/workspace/[id]/members/member-section.module.css
diff --git a/packages/console/app/src/routes/workspace/member-section.tsx b/packages/console/app/src/routes/workspace/[id]/members/member-section.tsx
index b13e8e5ed..b13e8e5ed 100644
--- a/packages/console/app/src/routes/workspace/member-section.tsx
+++ b/packages/console/app/src/routes/workspace/[id]/members/member-section.tsx
diff --git a/packages/console/app/src/routes/workspace/model-section.module.css b/packages/console/app/src/routes/workspace/[id]/model-section.module.css
index 5a98c9b15..5a98c9b15 100644
--- a/packages/console/app/src/routes/workspace/model-section.module.css
+++ b/packages/console/app/src/routes/workspace/[id]/model-section.module.css
diff --git a/packages/console/app/src/routes/workspace/model-section.tsx b/packages/console/app/src/routes/workspace/[id]/model-section.tsx
index 4128b4a2c..4128b4a2c 100644
--- a/packages/console/app/src/routes/workspace/model-section.tsx
+++ b/packages/console/app/src/routes/workspace/[id]/model-section.tsx
diff --git a/packages/console/app/src/routes/workspace/new-user-section.module.css b/packages/console/app/src/routes/workspace/[id]/new-user-section.module.css
index 2edc7cc14..2edc7cc14 100644
--- a/packages/console/app/src/routes/workspace/new-user-section.module.css
+++ b/packages/console/app/src/routes/workspace/[id]/new-user-section.module.css
diff --git a/packages/console/app/src/routes/workspace/new-user-section.tsx b/packages/console/app/src/routes/workspace/[id]/new-user-section.tsx
index b694801cc..b694801cc 100644
--- a/packages/console/app/src/routes/workspace/new-user-section.tsx
+++ b/packages/console/app/src/routes/workspace/[id]/new-user-section.tsx
diff --git a/packages/console/app/src/routes/workspace/provider-section.module.css b/packages/console/app/src/routes/workspace/[id]/provider-section.module.css
index 5f18862f5..5f18862f5 100644
--- a/packages/console/app/src/routes/workspace/provider-section.module.css
+++ b/packages/console/app/src/routes/workspace/[id]/provider-section.module.css
diff --git a/packages/console/app/src/routes/workspace/provider-section.tsx b/packages/console/app/src/routes/workspace/[id]/provider-section.tsx
index 856b3a6a2..856b3a6a2 100644
--- a/packages/console/app/src/routes/workspace/provider-section.tsx
+++ b/packages/console/app/src/routes/workspace/[id]/provider-section.tsx
diff --git a/packages/console/app/src/routes/workspace/[id]/settings/index.css b/packages/console/app/src/routes/workspace/[id]/settings/index.css
new file mode 100644
index 000000000..5124c78cf
--- /dev/null
+++ b/packages/console/app/src/routes/workspace/[id]/settings/index.css
@@ -0,0 +1,116 @@
+[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);
+ }
+ }
+ }
+} \ No newline at end of file
diff --git a/packages/console/app/src/routes/workspace/[id]/settings/index.tsx b/packages/console/app/src/routes/workspace/[id]/settings/index.tsx
new file mode 100644
index 000000000..972154aa3
--- /dev/null
+++ b/packages/console/app/src/routes/workspace/[id]/settings/index.tsx
@@ -0,0 +1,12 @@
+import "./index.css"
+import { SettingsSection } from "./settings-section"
+
+export default function () {
+ return (
+ <div data-page="workspace-[id]">
+ <div data-slot="sections">
+ <SettingsSection />
+ </div>
+ </div>
+ )
+}
diff --git a/packages/console/app/src/routes/workspace/settings-section.module.css b/packages/console/app/src/routes/workspace/[id]/settings/settings-section.module.css
index e3a5ad508..e3a5ad508 100644
--- a/packages/console/app/src/routes/workspace/settings-section.module.css
+++ b/packages/console/app/src/routes/workspace/[id]/settings/settings-section.module.css
diff --git a/packages/console/app/src/routes/workspace/settings-section.tsx b/packages/console/app/src/routes/workspace/[id]/settings/settings-section.tsx
index 0fc0158da..0fc0158da 100644
--- a/packages/console/app/src/routes/workspace/settings-section.tsx
+++ b/packages/console/app/src/routes/workspace/[id]/settings/settings-section.tsx
diff --git a/packages/console/app/src/routes/workspace/usage-section.module.css b/packages/console/app/src/routes/workspace/[id]/usage-section.module.css
index 1a772ba87..1a772ba87 100644
--- a/packages/console/app/src/routes/workspace/usage-section.module.css
+++ b/packages/console/app/src/routes/workspace/[id]/usage-section.module.css
diff --git a/packages/console/app/src/routes/workspace/usage-section.tsx b/packages/console/app/src/routes/workspace/[id]/usage-section.tsx
index 9f65fe5f7..9f65fe5f7 100644
--- a/packages/console/app/src/routes/workspace/usage-section.tsx
+++ b/packages/console/app/src/routes/workspace/[id]/usage-section.tsx
diff --git a/packages/console/app/src/routes/workspace/common.tsx b/packages/console/app/src/routes/workspace/common.tsx
index f85fd8423..d1f1aba81 100644
--- a/packages/console/app/src/routes/workspace/common.tsx
+++ b/packages/console/app/src/routes/workspace/common.tsx
@@ -1,25 +1,14 @@
-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(",", ",")
-}
+import { Resource } from "@opencode-ai/console-resource"
+import { Actor } from "@opencode-ai/console-core/actor.js"
+import { query } from "@solidjs/router"
+import { withActor } from "~/context/auth.withActor"
-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)
-}
+export const querySessionInfo = query(async (workspaceID: string) => {
+ "use server"
+ return withActor(() => {
+ return {
+ isAdmin: Actor.userRole() === "admin",
+ isBeta: Resource.App.stage === "production" ? workspaceID === "wrk_01K46JDFR0E75SG2Q8K172KF3Y" : true,
+ }
+ }, workspaceID)
+}, "session.get")
diff --git a/packages/console/app/src/routes/workspace/index.tsx b/packages/console/app/src/routes/workspace/index.tsx
deleted file mode 100644
index e69de29bb..000000000
--- a/packages/console/app/src/routes/workspace/index.tsx
+++ /dev/null