diff --git a/packages/console/app/src/routes/workspace/[id].tsx b/packages/console/app/src/routes/workspace/[id].tsx
index 2e59395b..09b14056 100644
--- a/packages/console/app/src/routes/workspace/[id].tsx
+++ b/packages/console/app/src/routes/workspace/[id].tsx
@@ -6,6 +6,7 @@ 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 { Show } from "solid-js"
import { createAsync, query, useParams } from "@solidjs/router"
import { Actor } from "@opencode-ai/console-core/actor.js"
@@ -28,6 +29,7 @@ export default function () {
const params = useParams()
const userInfo = createAsync(() => getUserInfo(params.id))
const isBeta = createAsync(() => beta(params.id))
+
return (
@@ -46,6 +48,7 @@ export default function () {
+
diff --git a/packages/console/app/src/routes/workspace/settings-section.module.css b/packages/console/app/src/routes/workspace/settings-section.module.css
new file mode 100644
index 00000000..e3a5ad50
--- /dev/null
+++ b/packages/console/app/src/routes/workspace/settings-section.module.css
@@ -0,0 +1,95 @@
+.root {
+ [data-slot="section-content"] {
+ display: flex;
+ flex-direction: column;
+ gap: var(--space-4);
+ }
+
+ [data-slot="setting"] {
+ display: flex;
+ align-items: flex-start;
+ justify-content: space-between;
+ gap: var(--space-4);
+ padding: var(--space-4);
+ border: 1px solid var(--color-border);
+ border-radius: var(--border-radius-sm);
+
+ @media (max-width: 30rem) {
+ flex-direction: column;
+ gap: var(--space-3);
+ }
+ }
+
+ [data-slot="setting-info"] {
+ flex: 1;
+ display: flex;
+ flex-direction: column;
+ gap: var(--space-1);
+
+ h3 {
+ font-size: var(--font-size-md);
+ font-weight: 500;
+ line-height: 1.2;
+ margin: 0;
+ color: var(--color-text);
+ }
+
+ [data-slot="current-value"] {
+ font-size: var(--font-size-sm);
+ color: var(--color-text-muted);
+ line-height: 1.4;
+ margin: 0;
+ }
+ }
+
+ [data-slot="create-form"] {
+ display: flex;
+ flex-direction: column;
+ gap: var(--space-3);
+ min-width: 15rem;
+ width: fit-content;
+
+ @media (max-width: 30rem) {
+ width: 100%;
+ min-width: auto;
+ }
+
+ [data-slot="input-container"] {
+ display: flex;
+ flex-direction: column;
+ gap: var(--space-1);
+ }
+
+ 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;
+ }
+ }
+}
diff --git a/packages/console/app/src/routes/workspace/settings-section.tsx b/packages/console/app/src/routes/workspace/settings-section.tsx
new file mode 100644
index 00000000..0fc0158d
--- /dev/null
+++ b/packages/console/app/src/routes/workspace/settings-section.tsx
@@ -0,0 +1,124 @@
+import { json, action, useParams, useSubmission, createAsync, query } from "@solidjs/router"
+import { createEffect, Show } from "solid-js"
+import { createStore } from "solid-js/store"
+import { withActor } from "~/context/auth.withActor"
+import { Workspace } from "@opencode-ai/console-core/workspace.js"
+import styles from "./settings-section.module.css"
+import { Database, eq } from "@opencode-ai/console-core/drizzle/index.js"
+import { WorkspaceTable } from "@opencode-ai/console-core/schema/workspace.sql.js"
+
+const getWorkspaceInfo = query(async (workspaceID: string) => {
+ "use server"
+ return withActor(
+ () =>
+ Database.use((tx) =>
+ tx
+ .select({
+ id: WorkspaceTable.id,
+ name: WorkspaceTable.name,
+ slug: WorkspaceTable.slug,
+ })
+ .from(WorkspaceTable)
+ .where(eq(WorkspaceTable.id, workspaceID))
+ .then((rows) => rows[0] || null),
+ ),
+ workspaceID,
+ )
+}, "workspace.get")
+
+const updateWorkspace = action(async (form: FormData) => {
+ "use server"
+ const name = form.get("name")?.toString().trim()
+ if (!name) return { error: "Workspace name is required." }
+ if (name.length > 255) return { error: "Name must be 255 characters or less." }
+ const workspaceID = form.get("workspaceID")?.toString()
+ if (!workspaceID) return { error: "Workspace ID is required." }
+ return json(
+ await withActor(
+ () =>
+ Workspace.update({ name })
+ .then(() => ({ error: undefined }))
+ .catch((e) => ({ error: e.message as string })),
+ workspaceID,
+ ),
+ )
+}, "workspace.update")
+
+export function SettingsSection() {
+ const params = useParams()
+ const workspaceInfo = createAsync(() => getWorkspaceInfo(params.id))
+ const submission = useSubmission(updateWorkspace)
+ const [store, setStore] = createStore({ show: false })
+
+ let input: HTMLInputElement
+
+ createEffect(() => {
+ if (!submission.pending && submission.result && !submission.result.error) {
+ hide()
+ }
+ })
+
+ function show() {
+ while (true) {
+ submission.clear()
+ if (!submission.result) break
+ }
+ setStore("show", true)
+ input.focus()
+ }
+
+ function hide() {
+ setStore("show", false)
+ }
+
+ return (
+
+
+
Settings
+
Update your workspace name and preferences.
+
+
+
+
+
Workspace Name
+
{workspaceInfo()?.name}
+
+
+
+
(input = r)}
+ data-component="input"
+ name="name"
+ type="text"
+ placeholder="Workspace name"
+ value={workspaceInfo()?.name ?? "Default"}
+ />
+
+ {(err) => {err()}
}
+
+
+
+
+
+
+
+
+ }
+ >
+
+
+
+
+
+ )
+}
diff --git a/packages/console/core/src/workspace.ts b/packages/console/core/src/workspace.ts
index 36d66e15..f9591632 100644
--- a/packages/console/core/src/workspace.ts
+++ b/packages/console/core/src/workspace.ts
@@ -7,6 +7,7 @@ import { UserTable } from "./schema/user.sql"
import { BillingTable } from "./schema/billing.sql"
import { WorkspaceTable } from "./schema/workspace.sql"
import { Key } from "./key"
+import { eq } from "drizzle-orm"
export namespace Workspace {
export const create = fn(
@@ -45,4 +46,21 @@ export namespace Workspace {
return workspaceID
},
)
+
+ export const update = fn(
+ z.object({
+ name: z.string().min(1).max(255),
+ }),
+ async ({ name }) => {
+ const workspaceID = Actor.workspace()
+ return await Database.use((tx) =>
+ tx
+ .update(WorkspaceTable)
+ .set({
+ name,
+ })
+ .where(eq(WorkspaceTable.id, workspaceID)),
+ )
+ },
+ )
}