Frank 4 месяцев назад
Родитель
Сommit
51e9979457
37 измененных файлов с 809 добавлено и 212 удалено
  1. 0 7
      packages/console/app/src/lib/beta.ts
  2. 8 8
      packages/console/app/src/routes/workspace.tsx
  3. 52 104
      packages/console/app/src/routes/workspace/[id].css
  4. 27 62
      packages/console/app/src/routes/workspace/[id].tsx
  5. 0 0
      packages/console/app/src/routes/workspace/[id]/billing/billing-section.module.css
  6. 0 0
      packages/console/app/src/routes/workspace/[id]/billing/billing-section.tsx
  7. 116 0
      packages/console/app/src/routes/workspace/[id]/billing/index.css
  8. 24 0
      packages/console/app/src/routes/workspace/[id]/billing/index.tsx
  9. 0 0
      packages/console/app/src/routes/workspace/[id]/billing/monthly-limit-section.module.css
  10. 0 0
      packages/console/app/src/routes/workspace/[id]/billing/monthly-limit-section.tsx
  11. 0 0
      packages/console/app/src/routes/workspace/[id]/billing/payment-section.module.css
  12. 4 6
      packages/console/app/src/routes/workspace/[id]/billing/payment-section.tsx
  13. 25 0
      packages/console/app/src/routes/workspace/[id]/common.tsx
  14. 116 0
      packages/console/app/src/routes/workspace/[id]/index.css
  15. 39 0
      packages/console/app/src/routes/workspace/[id]/index.tsx
  16. 116 0
      packages/console/app/src/routes/workspace/[id]/keys/index.css
  17. 12 0
      packages/console/app/src/routes/workspace/[id]/keys/index.tsx
  18. 0 0
      packages/console/app/src/routes/workspace/[id]/keys/key-section.module.css
  19. 1 1
      packages/console/app/src/routes/workspace/[id]/keys/key-section.tsx
  20. 116 0
      packages/console/app/src/routes/workspace/[id]/members/index.css
  21. 12 0
      packages/console/app/src/routes/workspace/[id]/members/index.tsx
  22. 0 0
      packages/console/app/src/routes/workspace/[id]/members/member-section.module.css
  23. 0 0
      packages/console/app/src/routes/workspace/[id]/members/member-section.tsx
  24. 0 0
      packages/console/app/src/routes/workspace/[id]/model-section.module.css
  25. 0 0
      packages/console/app/src/routes/workspace/[id]/model-section.tsx
  26. 0 0
      packages/console/app/src/routes/workspace/[id]/new-user-section.module.css
  27. 0 0
      packages/console/app/src/routes/workspace/[id]/new-user-section.tsx
  28. 0 0
      packages/console/app/src/routes/workspace/[id]/provider-section.module.css
  29. 0 0
      packages/console/app/src/routes/workspace/[id]/provider-section.tsx
  30. 116 0
      packages/console/app/src/routes/workspace/[id]/settings/index.css
  31. 12 0
      packages/console/app/src/routes/workspace/[id]/settings/index.tsx
  32. 0 0
      packages/console/app/src/routes/workspace/[id]/settings/settings-section.module.css
  33. 0 0
      packages/console/app/src/routes/workspace/[id]/settings/settings-section.tsx
  34. 0 0
      packages/console/app/src/routes/workspace/[id]/usage-section.module.css
  35. 0 0
      packages/console/app/src/routes/workspace/[id]/usage-section.tsx
  36. 13 24
      packages/console/app/src/routes/workspace/common.tsx
  37. 0 0
      packages/console/app/src/routes/workspace/index.tsx

+ 0 - 7
packages/console/app/src/lib/beta.ts

@@ -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")

+ 8 - 8
packages/console/app/src/routes/workspace.tsx

@@ -8,16 +8,16 @@ import { WorkspacePicker } from "./workspace-picker"
 import { withActor } from "~/context/auth.withActor"
 import { withActor } from "~/context/auth.withActor"
 import { User } from "@opencode-ai/console-core/user.js"
 import { User } from "@opencode-ai/console-core/user.js"
 import { Actor } from "@opencode-ai/console-core/actor.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"
   "use server"
   return withActor(async () => {
   return withActor(async () => {
     const actor = Actor.assert("user")
     const actor = Actor.assert("user")
     const email = await User.getAccountEmail(actor.properties.userID)
     const email = await User.getAccountEmail(actor.properties.userID)
-    return { email }
+    return email
   }, workspaceID)
   }, workspaceID)
-}, "userInfo")
+}, "userEmail")
 
 
 const logout = action(async () => {
 const logout = action(async () => {
   "use server"
   "use server"
@@ -37,8 +37,8 @@ const logout = action(async () => {
 
 
 export default function WorkspaceLayout(props: RouteSectionProps) {
 export default function WorkspaceLayout(props: RouteSectionProps) {
   const params = useParams()
   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 (
   return (
     <main data-page="workspace">
     <main data-page="workspace">
       <header data-component="workspace-header">
       <header data-component="workspace-header">
@@ -48,10 +48,10 @@ export default function WorkspaceLayout(props: RouteSectionProps) {
           </A>
           </A>
         </div>
         </div>
         <div data-slot="header-actions">
         <div data-slot="header-actions">
-          <Show when={isBeta()}>
+          <Show when={sessionInfo()?.isBeta}>
             <WorkspacePicker />
             <WorkspacePicker />
           </Show>
           </Show>
-          <span data-slot="user">{userInfo()?.email}</span>
+          <span data-slot="user">{userEmail()}</span>
           <form action={logout} method="post">
           <form action={logout} method="post">
             <button type="submit" formaction={logout}>
             <button type="submit" formaction={logout}>
               Logout
               Logout

+ 52 - 104
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);
   }
   }
-}
+}

+ 27 - 62
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 { 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 params = useParams()
-  const userInfo = createAsync(() => getUserInfo(params.id))
-  const isBeta = createAsync(() => beta(params.id))
-
+  const userInfo = createAsync(() => querySessionInfo(params.id))
   return (
   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>
           </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>
-    </div>
+    </main>
   )
   )
 }
 }

+ 0 - 0
packages/console/app/src/routes/workspace/billing-section.module.css → packages/console/app/src/routes/workspace/[id]/billing/billing-section.module.css


+ 0 - 0
packages/console/app/src/routes/workspace/billing-section.tsx → packages/console/app/src/routes/workspace/[id]/billing/billing-section.tsx


+ 116 - 0
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);
+      }
+    }
+  }
+}

+ 24 - 0
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>
+  )
+}

+ 0 - 0
packages/console/app/src/routes/workspace/monthly-limit-section.module.css → packages/console/app/src/routes/workspace/[id]/billing/monthly-limit-section.module.css


+ 0 - 0
packages/console/app/src/routes/workspace/monthly-limit-section.tsx → packages/console/app/src/routes/workspace/[id]/billing/monthly-limit-section.tsx


+ 0 - 0
packages/console/app/src/routes/workspace/payment-section.module.css → packages/console/app/src/routes/workspace/[id]/billing/payment-section.module.css


+ 4 - 6
packages/console/app/src/routes/workspace/payment-section.tsx → 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 { Billing } from "@opencode-ai/console-core/billing.js"
 import { query, action, useParams, createAsync, useAction } from "@solidjs/router"
 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 { withActor } from "~/context/auth.withActor"
-import { formatDateUTC, formatDateForTable } from "./common"
+import { formatDateUTC, formatDateForTable } from "../common"
 import styles from "./payment-section.module.css"
 import styles from "./payment-section.module.css"
 
 
 const getPaymentsInfo = query(async (workspaceID: string) => {
 const getPaymentsInfo = query(async (workspaceID: string) => {
@@ -19,7 +19,6 @@ const downloadReceipt = action(async (workspaceID: string, paymentID: string) =>
 
 
 export function PaymentSection() {
 export function PaymentSection() {
   const params = useParams()
   const params = useParams()
-  // ORIGINAL CODE - COMMENTED OUT FOR TESTING
   const payments = createAsync(() => getPaymentsInfo(params.id))
   const payments = createAsync(() => getPaymentsInfo(params.id))
   const downloadReceiptAction = useAction(downloadReceipt)
   const downloadReceiptAction = useAction(downloadReceipt)
 
 
@@ -58,8 +57,7 @@ export function PaymentSection() {
   // ]
   // ]
 
 
   return (
   return (
-    payments() &&
-    payments()!.length > 0 && (
+    <Show when={payments() && payments()!.length > 0}>
       <section class={styles.root}>
       <section class={styles.root}>
         <div data-slot="section-title">
         <div data-slot="section-title">
           <h2>Payments History</h2>
           <h2>Payments History</h2>
@@ -109,6 +107,6 @@ export function PaymentSection() {
           </table>
           </table>
         </div>
         </div>
       </section>
       </section>
-    )
+    </Show>
   )
   )
 }
 }

+ 25 - 0
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)
+}

+ 116 - 0
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);
+      }
+    }
+  }
+}

+ 39 - 0
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>
+  )
+}

+ 116 - 0
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);
+      }
+    }
+  }
+}

+ 12 - 0
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>
+  )
+}

+ 0 - 0
packages/console/app/src/routes/workspace/key-section.module.css → packages/console/app/src/routes/workspace/[id]/keys/key-section.module.css


+ 1 - 1
packages/console/app/src/routes/workspace/key-section.tsx → 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 { Key } from "@opencode-ai/console-core/key.js"
 import { withActor } from "~/context/auth.withActor"
 import { withActor } from "~/context/auth.withActor"
 import { createStore } from "solid-js/store"
 import { createStore } from "solid-js/store"
-import { formatDateUTC, formatDateForTable } from "./common"
+import { formatDateUTC, formatDateForTable } from "../common"
 import styles from "./key-section.module.css"
 import styles from "./key-section.module.css"
 import { Actor } from "@opencode-ai/console-core/actor.js"
 import { Actor } from "@opencode-ai/console-core/actor.js"
 
 

+ 116 - 0
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);
+      }
+    }
+  }
+}

+ 12 - 0
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>
+  )
+}

+ 0 - 0
packages/console/app/src/routes/workspace/member-section.module.css → packages/console/app/src/routes/workspace/[id]/members/member-section.module.css


+ 0 - 0
packages/console/app/src/routes/workspace/member-section.tsx → packages/console/app/src/routes/workspace/[id]/members/member-section.tsx


+ 0 - 0
packages/console/app/src/routes/workspace/model-section.module.css → packages/console/app/src/routes/workspace/[id]/model-section.module.css


+ 0 - 0
packages/console/app/src/routes/workspace/model-section.tsx → packages/console/app/src/routes/workspace/[id]/model-section.tsx


+ 0 - 0
packages/console/app/src/routes/workspace/new-user-section.module.css → packages/console/app/src/routes/workspace/[id]/new-user-section.module.css


+ 0 - 0
packages/console/app/src/routes/workspace/new-user-section.tsx → packages/console/app/src/routes/workspace/[id]/new-user-section.tsx


+ 0 - 0
packages/console/app/src/routes/workspace/provider-section.module.css → packages/console/app/src/routes/workspace/[id]/provider-section.module.css


+ 0 - 0
packages/console/app/src/routes/workspace/provider-section.tsx → packages/console/app/src/routes/workspace/[id]/provider-section.tsx


+ 116 - 0
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);
+      }
+    }
+  }
+}

+ 12 - 0
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>
+  )
+}

+ 0 - 0
packages/console/app/src/routes/workspace/settings-section.module.css → packages/console/app/src/routes/workspace/[id]/settings/settings-section.module.css


+ 0 - 0
packages/console/app/src/routes/workspace/settings-section.tsx → packages/console/app/src/routes/workspace/[id]/settings/settings-section.tsx


+ 0 - 0
packages/console/app/src/routes/workspace/usage-section.module.css → packages/console/app/src/routes/workspace/[id]/usage-section.module.css


+ 0 - 0
packages/console/app/src/routes/workspace/usage-section.tsx → packages/console/app/src/routes/workspace/[id]/usage-section.tsx


+ 13 - 24
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")

+ 0 - 0
packages/console/app/src/routes/workspace/index.tsx