mirror of
https://github.com/sst/opencode.git
synced 2025-12-23 10:11:41 +00:00
Merge branch 'dev' into opentui
This commit is contained in:
commit
1b10dbd785
46 changed files with 1685 additions and 4503 deletions
1
STATS.md
1
STATS.md
|
|
@ -100,3 +100,4 @@
|
|||
| 2025-10-03 | 446,829 (+5,977) | 359,937 (+11,838) | 806,766 (+17,815) |
|
||||
| 2025-10-04 | 452,561 (+5,732) | 370,386 (+10,449) | 822,947 (+16,181) |
|
||||
| 2025-10-05 | 455,559 (+2,998) | 374,745 (+4,359) | 830,304 (+7,357) |
|
||||
| 2025-10-06 | 460,927 (+5,368) | 379,489 (+4,744) | 840,416 (+10,112) |
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@
|
|||
"dev:remote": "VITE_AUTH_URL=https://auth.dev.opencode.ai bun sst shell --stage=dev bun dev",
|
||||
"build": "vinxi build && ../../opencode/script/schema.ts ./.output/public/config.json",
|
||||
"start": "vinxi start",
|
||||
"version": "0.14.3"
|
||||
"version": "0.14.5"
|
||||
},
|
||||
"dependencies": {
|
||||
"@ibm/plex": "6.4.1",
|
||||
|
|
@ -16,6 +16,7 @@
|
|||
"@openauthjs/openauth": "catalog:",
|
||||
"@kobalte/core": "catalog:",
|
||||
"@jsx-email/render": "1.1.1",
|
||||
"@opencode-ai/console-resource": "workspace:*",
|
||||
"@solidjs/meta": "^0.29.4",
|
||||
"@solidjs/router": "^0.15.0",
|
||||
"@solidjs/start": "^1.1.0",
|
||||
|
|
|
|||
|
|
@ -73,6 +73,7 @@ export const getActor = async (workspace?: string): Promise<Actor.Info> => {
|
|||
properties: {
|
||||
userID: user.id,
|
||||
workspaceID: user.workspaceID,
|
||||
accountID: user.accountID,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
|
|
|||
7
packages/console/app/src/lib/beta.ts
Normal file
7
packages/console/app/src/lib/beta.ts
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
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")
|
||||
|
|
@ -1,11 +1,29 @@
|
|||
import { Account } from "@opencode-ai/console-core/account.js"
|
||||
import { Actor } from "@opencode-ai/console-core/actor.js"
|
||||
import { and, Database, eq, isNull } from "@opencode-ai/console-core/drizzle/index.js"
|
||||
import { UserTable } from "@opencode-ai/console-core/schema/user.sql.js"
|
||||
import { WorkspaceTable } from "@opencode-ai/console-core/schema/workspace.sql.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())
|
||||
const workspaces = await withActor(async () => {
|
||||
const actor = Actor.assert("account")
|
||||
return Database.transaction(async (tx) =>
|
||||
tx
|
||||
.select({ id: WorkspaceTable.id })
|
||||
.from(UserTable)
|
||||
.innerJoin(WorkspaceTable, eq(UserTable.workspaceID, WorkspaceTable.id))
|
||||
.where(
|
||||
and(
|
||||
eq(UserTable.accountID, actor.properties.accountID),
|
||||
isNull(UserTable.timeDeleted),
|
||||
isNull(WorkspaceTable.timeDeleted),
|
||||
),
|
||||
),
|
||||
)
|
||||
})
|
||||
return redirect(`/workspace/${workspaces[0].id}`)
|
||||
} catch {
|
||||
return redirect("/auth/authorize")
|
||||
|
|
|
|||
184
packages/console/app/src/routes/workspace-picker.css
Normal file
184
packages/console/app/src/routes/workspace-picker.css
Normal file
|
|
@ -0,0 +1,184 @@
|
|||
[data-component="workspace-picker"] {
|
||||
position: relative;
|
||||
/* Override blue accent colors with neutral colors */
|
||||
--color-accent: var(--color-border);
|
||||
--color-accent-hover: var(--color-border);
|
||||
--color-accent-active: var(--color-border);
|
||||
--color-primary: var(--color-border);
|
||||
--color-primary-hover: var(--color-border);
|
||||
--color-primary-active: var(--color-border);
|
||||
--color-primary-alpha-20: transparent;
|
||||
|
||||
[data-slot="trigger"] {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: var(--space-2);
|
||||
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-sans);
|
||||
cursor: pointer;
|
||||
min-width: 200px;
|
||||
|
||||
span {
|
||||
flex: 1;
|
||||
text-align: left;
|
||||
font-weight: 500;
|
||||
}
|
||||
}
|
||||
|
||||
[data-slot="chevron"] {
|
||||
flex-shrink: 0;
|
||||
color: var(--color-text-secondary);
|
||||
}
|
||||
|
||||
[data-slot="dropdown"] button {
|
||||
text-decoration: none !important;
|
||||
}
|
||||
|
||||
/* Ensure text inside buttons has no underline */
|
||||
[data-slot="dropdown"] button * {
|
||||
text-decoration: none !important;
|
||||
}
|
||||
|
||||
[data-slot="dropdown"] {
|
||||
position: absolute;
|
||||
top: 100%;
|
||||
left: 0;
|
||||
right: 0;
|
||||
z-index: 1000;
|
||||
margin-top: var(--space-1);
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: var(--border-radius-sm);
|
||||
background-color: var(--color-bg);
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
|
||||
max-height: 240px;
|
||||
overflow-y: auto;
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
}
|
||||
|
||||
[data-slot="option"],
|
||||
[data-slot="create-option"] {
|
||||
width: 100%;
|
||||
padding: var(--space-2-5) var(--space-3);
|
||||
border: none;
|
||||
background: none;
|
||||
color: var(--color-text);
|
||||
font-size: var(--font-size-sm);
|
||||
font-family: var(--font-sans);
|
||||
text-align: left;
|
||||
cursor: pointer;
|
||||
text-decoration: none;
|
||||
|
||||
&:hover {
|
||||
background-color: var(--color-surface);
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
&:focus {
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
&:active {
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
&:first-child {
|
||||
border-top-left-radius: var(--border-radius-sm);
|
||||
border-top-right-radius: var(--border-radius-sm);
|
||||
}
|
||||
|
||||
&:last-child {
|
||||
border-bottom-left-radius: var(--border-radius-sm);
|
||||
border-bottom-right-radius: var(--border-radius-sm);
|
||||
}
|
||||
}
|
||||
|
||||
[data-slot="option"][data-selected="true"] {
|
||||
background-color: transparent;
|
||||
color: var(--color-text);
|
||||
}
|
||||
|
||||
[data-slot="create-option"] {
|
||||
color: var(--color-text-secondary);
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
[data-slot="create-form"] {
|
||||
margin-top: var(--space-4);
|
||||
padding: var(--space-4);
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: var(--border-radius-sm);
|
||||
background-color: var(--color-surface);
|
||||
}
|
||||
|
||||
[data-slot="create-input-group"] {
|
||||
display: flex;
|
||||
gap: var(--space-2);
|
||||
align-items: center;
|
||||
|
||||
@media (max-width: 30rem) {
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
}
|
||||
}
|
||||
|
||||
[data-slot="create-input"] {
|
||||
flex: 1;
|
||||
padding: var(--space-2-5) 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-sans);
|
||||
|
||||
&:focus {
|
||||
outline: none;
|
||||
border-color: var(--color-border);
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
&::placeholder {
|
||||
color: var(--color-text-muted);
|
||||
}
|
||||
}
|
||||
|
||||
button[type="submit"],
|
||||
button[type="button"] {
|
||||
padding: var(--space-2-5) var(--space-4);
|
||||
background-color: var(--color-bg);
|
||||
color: var(--color-text);
|
||||
font-size: var(--font-size-sm);
|
||||
font-family: var(--font-sans);
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
white-space: nowrap;
|
||||
|
||||
&:focus {
|
||||
outline: none;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
&:active {
|
||||
transform: translateY(1px);
|
||||
}
|
||||
|
||||
&[data-color="primary"] {
|
||||
background-color: var(--color-text-secondary);
|
||||
border-color: var(--color-text-secondary);
|
||||
color: var(--color-bg);
|
||||
}
|
||||
|
||||
@media (max-width: 30rem) {
|
||||
flex: 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
144
packages/console/app/src/routes/workspace-picker.tsx
Normal file
144
packages/console/app/src/routes/workspace-picker.tsx
Normal file
|
|
@ -0,0 +1,144 @@
|
|||
import { query, useParams, action, createAsync, redirect } from "@solidjs/router"
|
||||
import { For, Show, createEffect, onCleanup } from "solid-js"
|
||||
import { createStore } from "solid-js/store"
|
||||
import { withActor } from "~/context/auth.withActor"
|
||||
import { Actor } from "@opencode-ai/console-core/actor.js"
|
||||
import { and, Database, eq, isNull } from "@opencode-ai/console-core/drizzle/index.js"
|
||||
import { WorkspaceTable } from "@opencode-ai/console-core/schema/workspace.sql.js"
|
||||
import { UserTable } from "@opencode-ai/console-core/schema/user.sql.js"
|
||||
import { Workspace } from "@opencode-ai/console-core/workspace.js"
|
||||
import "./workspace-picker.css"
|
||||
|
||||
const getWorkspaces = query(async () => {
|
||||
"use server"
|
||||
return withActor(async () => {
|
||||
return Database.transaction((tx) =>
|
||||
tx
|
||||
.select({
|
||||
id: WorkspaceTable.id,
|
||||
name: WorkspaceTable.name,
|
||||
slug: WorkspaceTable.slug,
|
||||
})
|
||||
.from(UserTable)
|
||||
.innerJoin(WorkspaceTable, eq(UserTable.workspaceID, WorkspaceTable.id))
|
||||
.where(and(eq(UserTable.accountID, Actor.account()), isNull(WorkspaceTable.timeDeleted))),
|
||||
)
|
||||
})
|
||||
}, "workspaces")
|
||||
|
||||
const createWorkspace = action(async (form: FormData) => {
|
||||
"use server"
|
||||
const name = form.get("workspaceName") as string
|
||||
if (name?.trim()) {
|
||||
return withActor(async () => {
|
||||
const workspaceID = await Workspace.create({ name: name.trim() })
|
||||
return redirect(`/workspace/${workspaceID}`)
|
||||
})
|
||||
}
|
||||
}, "createWorkspace")
|
||||
|
||||
export function WorkspacePicker() {
|
||||
const params = useParams()
|
||||
const workspaces = createAsync(() => getWorkspaces())
|
||||
const [store, setStore] = createStore({
|
||||
showForm: false,
|
||||
showDropdown: false,
|
||||
})
|
||||
let dropdownRef: HTMLDivElement | undefined
|
||||
|
||||
const currentWorkspace = () => {
|
||||
const ws = workspaces()?.find((w) => w.id === params.id)
|
||||
return ws ? ws.name : "Select workspace"
|
||||
}
|
||||
|
||||
const handleWorkspaceNew = () => {
|
||||
setStore({ showForm: true, showDropdown: false })
|
||||
}
|
||||
|
||||
const handleSelectWorkspace = (workspaceID: string) => {
|
||||
if (workspaceID === params.id) {
|
||||
setStore("showDropdown", false)
|
||||
return
|
||||
}
|
||||
|
||||
window.location.href = `/workspace/${workspaceID}`
|
||||
}
|
||||
|
||||
// Reset signals when workspace ID changes
|
||||
createEffect(() => {
|
||||
params.id
|
||||
setStore("showForm", false)
|
||||
setStore("showDropdown", false)
|
||||
})
|
||||
|
||||
createEffect(() => {
|
||||
const handleClickOutside = (event: MouseEvent) => {
|
||||
if (dropdownRef && !dropdownRef.contains(event.target as Node)) {
|
||||
setStore("showDropdown", false)
|
||||
}
|
||||
}
|
||||
|
||||
document.addEventListener("click", handleClickOutside)
|
||||
onCleanup(() => document.removeEventListener("click", handleClickOutside))
|
||||
})
|
||||
|
||||
return (
|
||||
<div data-component="workspace-picker">
|
||||
<div ref={dropdownRef}>
|
||||
<div data-slot="trigger" onClick={() => setStore("showDropdown", !store.showDropdown)}>
|
||||
<span>{currentWorkspace()}</span>
|
||||
<svg data-slot="chevron" width="12" height="8" viewBox="0 0 12 8" fill="none">
|
||||
<path
|
||||
d="M1 1L6 6L11 1"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
|
||||
<Show when={store.showDropdown}>
|
||||
<div data-slot="dropdown">
|
||||
<For each={workspaces()}>
|
||||
{(workspace) => (
|
||||
<button
|
||||
data-slot="option"
|
||||
data-selected={workspace.id === params.id}
|
||||
type="button"
|
||||
onClick={() => handleSelectWorkspace(workspace.id)}
|
||||
>
|
||||
{workspace.name || workspace.slug}
|
||||
</button>
|
||||
)}
|
||||
</For>
|
||||
<button data-slot="create-option" type="button" onClick={() => handleWorkspaceNew()}>
|
||||
+ Create New Workspace
|
||||
</button>
|
||||
</div>
|
||||
</Show>
|
||||
</div>
|
||||
|
||||
<Show when={store.showForm}>
|
||||
<form data-slot="create-form" action={createWorkspace} method="post">
|
||||
<div data-slot="create-input-group">
|
||||
<input
|
||||
data-slot="create-input"
|
||||
type="text"
|
||||
name="workspaceName"
|
||||
placeholder="Enter workspace name"
|
||||
required
|
||||
autofocus
|
||||
/>
|
||||
<button type="submit" data-color="primary">
|
||||
Create
|
||||
</button>
|
||||
<button type="button" onClick={() => setStore("showForm", false)}>
|
||||
Cancel
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</Show>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
|
@ -1,11 +1,14 @@
|
|||
import { Show } from "solid-js"
|
||||
import { getRequestEvent } from "solid-js/web"
|
||||
import { query, action, redirect, createAsync, RouteSectionProps, useParams, A } from "@solidjs/router"
|
||||
import "./workspace.css"
|
||||
import { useAuthSession } from "~/context/auth.session"
|
||||
import { IconLogo } from "../component/icon"
|
||||
import { WorkspacePicker } from "./workspace-picker"
|
||||
import { withActor } from "~/context/auth.withActor"
|
||||
import { query, action, redirect, createAsync, RouteSectionProps, useParams, A } from "@solidjs/router"
|
||||
import { User } from "@opencode-ai/console-core/user.js"
|
||||
import { Actor } from "@opencode-ai/console-core/actor.js"
|
||||
import { getRequestEvent } from "solid-js/web"
|
||||
import { beta } from "~/lib/beta"
|
||||
|
||||
const getUserInfo = query(async (workspaceID: string) => {
|
||||
"use server"
|
||||
|
|
@ -35,6 +38,7 @@ 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))
|
||||
return (
|
||||
<main data-page="workspace">
|
||||
<header data-component="workspace-header">
|
||||
|
|
@ -44,6 +48,9 @@ export default function WorkspaceLayout(props: RouteSectionProps) {
|
|||
</A>
|
||||
</div>
|
||||
<div data-slot="header-actions">
|
||||
<Show when={isBeta()}>
|
||||
<WorkspacePicker />
|
||||
</Show>
|
||||
<span data-slot="user">{userInfo()?.email}</span>
|
||||
<form action={logout} method="post">
|
||||
<button type="submit" formaction={logout}>
|
||||
|
|
|
|||
|
|
@ -6,28 +6,30 @@ 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"
|
||||
import { withActor } from "~/context/auth.withActor"
|
||||
import { User } from "@opencode-ai/console-core/user.js"
|
||||
import { Resource } from "@opencode-ai/console-resource"
|
||||
import { beta } from "~/lib/beta"
|
||||
|
||||
const getUser = query(async (workspaceID: string) => {
|
||||
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",
|
||||
isBeta: Resource.App.stage === "production" ? workspaceID === "wrk_01K46JDFR0E75SG2Q8K172KF3Y" : true,
|
||||
}
|
||||
}, workspaceID)
|
||||
}, "user.get")
|
||||
|
||||
export default function () {
|
||||
const params = useParams()
|
||||
const data = createAsync(() => getUser(params.id))
|
||||
const userInfo = createAsync(() => getUserInfo(params.id))
|
||||
const isBeta = createAsync(() => beta(params.id))
|
||||
|
||||
return (
|
||||
<div data-page="workspace-[id]">
|
||||
<section data-component="title-section">
|
||||
|
|
@ -44,15 +46,16 @@ export default function () {
|
|||
<div data-slot="sections">
|
||||
<NewUserSection />
|
||||
<KeySection />
|
||||
<Show when={data()?.isAdmin}>
|
||||
<Show when={data()?.isBeta}>
|
||||
<Show when={userInfo()?.isAdmin}>
|
||||
<Show when={isBeta()}>
|
||||
<SettingsSection />
|
||||
<MemberSection />
|
||||
</Show>
|
||||
<BillingSection />
|
||||
<MonthlyLimitSection />
|
||||
</Show>
|
||||
<UsageSection />
|
||||
<Show when={data()?.isAdmin}>
|
||||
<Show when={userInfo()?.isAdmin}>
|
||||
<PaymentSection />
|
||||
</Show>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
124
packages/console/app/src/routes/workspace/settings-section.tsx
Normal file
124
packages/console/app/src/routes/workspace/settings-section.tsx
Normal file
|
|
@ -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 (
|
||||
<section class={styles.root}>
|
||||
<div data-slot="section-title">
|
||||
<h2>Settings</h2>
|
||||
<p>Update your workspace name and preferences.</p>
|
||||
</div>
|
||||
<div data-slot="section-content">
|
||||
<div data-slot="setting">
|
||||
<div data-slot="setting-info">
|
||||
<h3>Workspace Name</h3>
|
||||
<p data-slot="current-value">{workspaceInfo()?.name}</p>
|
||||
</div>
|
||||
<Show
|
||||
when={!store.show}
|
||||
fallback={
|
||||
<form action={updateWorkspace} method="post" data-slot="create-form">
|
||||
<div data-slot="input-container">
|
||||
<input
|
||||
required
|
||||
ref={(r) => (input = r)}
|
||||
data-component="input"
|
||||
name="name"
|
||||
type="text"
|
||||
placeholder="Workspace name"
|
||||
value={workspaceInfo()?.name ?? "Default"}
|
||||
/>
|
||||
<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 ? "Updating..." : "Update"}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
}
|
||||
>
|
||||
<button data-color="primary" onClick={() => show()}>
|
||||
Edit Name
|
||||
</button>
|
||||
</Show>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
1
packages/console/core/migrations/0028_careful_cerise.sql
Normal file
1
packages/console/core/migrations/0028_careful_cerise.sql
Normal file
|
|
@ -0,0 +1 @@
|
|||
ALTER TABLE `workspace` MODIFY COLUMN `name` varchar(255) NOT NULL;
|
||||
709
packages/console/core/migrations/meta/0028_snapshot.json
Normal file
709
packages/console/core/migrations/meta/0028_snapshot.json
Normal file
|
|
@ -0,0 +1,709 @@
|
|||
{
|
||||
"version": "5",
|
||||
"dialect": "mysql",
|
||||
"id": "a331e38c-c2e3-406d-a1ff-b0af7229cd85",
|
||||
"prevId": "05e873f6-1556-4bcb-8e19-14971e37610a",
|
||||
"tables": {
|
||||
"account": {
|
||||
"name": "account",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "varchar(30)",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"time_created": {
|
||||
"name": "time_created",
|
||||
"type": "timestamp(3)",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": "(now())"
|
||||
},
|
||||
"time_updated": {
|
||||
"name": "time_updated",
|
||||
"type": "timestamp(3)",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": "CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3)"
|
||||
},
|
||||
"time_deleted": {
|
||||
"name": "time_deleted",
|
||||
"type": "timestamp(3)",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"email": {
|
||||
"name": "email",
|
||||
"type": "varchar(255)",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
}
|
||||
},
|
||||
"indexes": {
|
||||
"email": {
|
||||
"name": "email",
|
||||
"columns": [
|
||||
"email"
|
||||
],
|
||||
"isUnique": true
|
||||
}
|
||||
},
|
||||
"foreignKeys": {},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {},
|
||||
"checkConstraint": {}
|
||||
},
|
||||
"billing": {
|
||||
"name": "billing",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "varchar(30)",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"workspace_id": {
|
||||
"name": "workspace_id",
|
||||
"type": "varchar(30)",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"time_created": {
|
||||
"name": "time_created",
|
||||
"type": "timestamp(3)",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": "(now())"
|
||||
},
|
||||
"time_updated": {
|
||||
"name": "time_updated",
|
||||
"type": "timestamp(3)",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": "CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3)"
|
||||
},
|
||||
"time_deleted": {
|
||||
"name": "time_deleted",
|
||||
"type": "timestamp(3)",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"customer_id": {
|
||||
"name": "customer_id",
|
||||
"type": "varchar(255)",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"payment_method_id": {
|
||||
"name": "payment_method_id",
|
||||
"type": "varchar(255)",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"payment_method_last4": {
|
||||
"name": "payment_method_last4",
|
||||
"type": "varchar(4)",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"balance": {
|
||||
"name": "balance",
|
||||
"type": "bigint",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"monthly_limit": {
|
||||
"name": "monthly_limit",
|
||||
"type": "int",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"monthly_usage": {
|
||||
"name": "monthly_usage",
|
||||
"type": "bigint",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"time_monthly_usage_updated": {
|
||||
"name": "time_monthly_usage_updated",
|
||||
"type": "timestamp(3)",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"reload": {
|
||||
"name": "reload",
|
||||
"type": "boolean",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"reload_error": {
|
||||
"name": "reload_error",
|
||||
"type": "varchar(255)",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"time_reload_error": {
|
||||
"name": "time_reload_error",
|
||||
"type": "timestamp(3)",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"time_reload_locked_till": {
|
||||
"name": "time_reload_locked_till",
|
||||
"type": "timestamp(3)",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
}
|
||||
},
|
||||
"indexes": {
|
||||
"global_customer_id": {
|
||||
"name": "global_customer_id",
|
||||
"columns": [
|
||||
"customer_id"
|
||||
],
|
||||
"isUnique": true
|
||||
}
|
||||
},
|
||||
"foreignKeys": {},
|
||||
"compositePrimaryKeys": {
|
||||
"billing_workspace_id_id_pk": {
|
||||
"name": "billing_workspace_id_id_pk",
|
||||
"columns": [
|
||||
"workspace_id",
|
||||
"id"
|
||||
]
|
||||
}
|
||||
},
|
||||
"uniqueConstraints": {},
|
||||
"checkConstraint": {}
|
||||
},
|
||||
"payment": {
|
||||
"name": "payment",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "varchar(30)",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"workspace_id": {
|
||||
"name": "workspace_id",
|
||||
"type": "varchar(30)",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"time_created": {
|
||||
"name": "time_created",
|
||||
"type": "timestamp(3)",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": "(now())"
|
||||
},
|
||||
"time_updated": {
|
||||
"name": "time_updated",
|
||||
"type": "timestamp(3)",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": "CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3)"
|
||||
},
|
||||
"time_deleted": {
|
||||
"name": "time_deleted",
|
||||
"type": "timestamp(3)",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"customer_id": {
|
||||
"name": "customer_id",
|
||||
"type": "varchar(255)",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"invoice_id": {
|
||||
"name": "invoice_id",
|
||||
"type": "varchar(255)",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"payment_id": {
|
||||
"name": "payment_id",
|
||||
"type": "varchar(255)",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"amount": {
|
||||
"name": "amount",
|
||||
"type": "bigint",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"time_refunded": {
|
||||
"name": "time_refunded",
|
||||
"type": "timestamp(3)",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
}
|
||||
},
|
||||
"indexes": {},
|
||||
"foreignKeys": {},
|
||||
"compositePrimaryKeys": {
|
||||
"payment_workspace_id_id_pk": {
|
||||
"name": "payment_workspace_id_id_pk",
|
||||
"columns": [
|
||||
"workspace_id",
|
||||
"id"
|
||||
]
|
||||
}
|
||||
},
|
||||
"uniqueConstraints": {},
|
||||
"checkConstraint": {}
|
||||
},
|
||||
"usage": {
|
||||
"name": "usage",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "varchar(30)",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"workspace_id": {
|
||||
"name": "workspace_id",
|
||||
"type": "varchar(30)",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"time_created": {
|
||||
"name": "time_created",
|
||||
"type": "timestamp(3)",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": "(now())"
|
||||
},
|
||||
"time_updated": {
|
||||
"name": "time_updated",
|
||||
"type": "timestamp(3)",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": "CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3)"
|
||||
},
|
||||
"time_deleted": {
|
||||
"name": "time_deleted",
|
||||
"type": "timestamp(3)",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"model": {
|
||||
"name": "model",
|
||||
"type": "varchar(255)",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"provider": {
|
||||
"name": "provider",
|
||||
"type": "varchar(255)",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"input_tokens": {
|
||||
"name": "input_tokens",
|
||||
"type": "int",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"output_tokens": {
|
||||
"name": "output_tokens",
|
||||
"type": "int",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"reasoning_tokens": {
|
||||
"name": "reasoning_tokens",
|
||||
"type": "int",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"cache_read_tokens": {
|
||||
"name": "cache_read_tokens",
|
||||
"type": "int",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"cache_write_5m_tokens": {
|
||||
"name": "cache_write_5m_tokens",
|
||||
"type": "int",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"cache_write_1h_tokens": {
|
||||
"name": "cache_write_1h_tokens",
|
||||
"type": "int",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"cost": {
|
||||
"name": "cost",
|
||||
"type": "bigint",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
}
|
||||
},
|
||||
"indexes": {},
|
||||
"foreignKeys": {},
|
||||
"compositePrimaryKeys": {
|
||||
"usage_workspace_id_id_pk": {
|
||||
"name": "usage_workspace_id_id_pk",
|
||||
"columns": [
|
||||
"workspace_id",
|
||||
"id"
|
||||
]
|
||||
}
|
||||
},
|
||||
"uniqueConstraints": {},
|
||||
"checkConstraint": {}
|
||||
},
|
||||
"key": {
|
||||
"name": "key",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "varchar(30)",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"workspace_id": {
|
||||
"name": "workspace_id",
|
||||
"type": "varchar(30)",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"time_created": {
|
||||
"name": "time_created",
|
||||
"type": "timestamp(3)",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": "(now())"
|
||||
},
|
||||
"time_updated": {
|
||||
"name": "time_updated",
|
||||
"type": "timestamp(3)",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": "CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3)"
|
||||
},
|
||||
"time_deleted": {
|
||||
"name": "time_deleted",
|
||||
"type": "timestamp(3)",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"name": {
|
||||
"name": "name",
|
||||
"type": "varchar(255)",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"key": {
|
||||
"name": "key",
|
||||
"type": "varchar(255)",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"user_id": {
|
||||
"name": "user_id",
|
||||
"type": "varchar(30)",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"time_used": {
|
||||
"name": "time_used",
|
||||
"type": "timestamp(3)",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
}
|
||||
},
|
||||
"indexes": {
|
||||
"global_key": {
|
||||
"name": "global_key",
|
||||
"columns": [
|
||||
"key"
|
||||
],
|
||||
"isUnique": true
|
||||
}
|
||||
},
|
||||
"foreignKeys": {},
|
||||
"compositePrimaryKeys": {
|
||||
"key_workspace_id_id_pk": {
|
||||
"name": "key_workspace_id_id_pk",
|
||||
"columns": [
|
||||
"workspace_id",
|
||||
"id"
|
||||
]
|
||||
}
|
||||
},
|
||||
"uniqueConstraints": {},
|
||||
"checkConstraint": {}
|
||||
},
|
||||
"user": {
|
||||
"name": "user",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "varchar(30)",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"workspace_id": {
|
||||
"name": "workspace_id",
|
||||
"type": "varchar(30)",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"time_created": {
|
||||
"name": "time_created",
|
||||
"type": "timestamp(3)",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": "(now())"
|
||||
},
|
||||
"time_updated": {
|
||||
"name": "time_updated",
|
||||
"type": "timestamp(3)",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": "CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3)"
|
||||
},
|
||||
"time_deleted": {
|
||||
"name": "time_deleted",
|
||||
"type": "timestamp(3)",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"account_id": {
|
||||
"name": "account_id",
|
||||
"type": "varchar(30)",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"email": {
|
||||
"name": "email",
|
||||
"type": "varchar(255)",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"name": {
|
||||
"name": "name",
|
||||
"type": "varchar(255)",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"time_seen": {
|
||||
"name": "time_seen",
|
||||
"type": "timestamp(3)",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"color": {
|
||||
"name": "color",
|
||||
"type": "int",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"role": {
|
||||
"name": "role",
|
||||
"type": "enum('admin','member')",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
}
|
||||
},
|
||||
"indexes": {
|
||||
"user_account_id": {
|
||||
"name": "user_account_id",
|
||||
"columns": [
|
||||
"workspace_id",
|
||||
"account_id"
|
||||
],
|
||||
"isUnique": true
|
||||
},
|
||||
"user_email": {
|
||||
"name": "user_email",
|
||||
"columns": [
|
||||
"workspace_id",
|
||||
"email"
|
||||
],
|
||||
"isUnique": true
|
||||
},
|
||||
"global_account_id": {
|
||||
"name": "global_account_id",
|
||||
"columns": [
|
||||
"account_id"
|
||||
],
|
||||
"isUnique": false
|
||||
},
|
||||
"global_email": {
|
||||
"name": "global_email",
|
||||
"columns": [
|
||||
"email"
|
||||
],
|
||||
"isUnique": false
|
||||
}
|
||||
},
|
||||
"foreignKeys": {},
|
||||
"compositePrimaryKeys": {
|
||||
"user_workspace_id_id_pk": {
|
||||
"name": "user_workspace_id_id_pk",
|
||||
"columns": [
|
||||
"workspace_id",
|
||||
"id"
|
||||
]
|
||||
}
|
||||
},
|
||||
"uniqueConstraints": {},
|
||||
"checkConstraint": {}
|
||||
},
|
||||
"workspace": {
|
||||
"name": "workspace",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "varchar(30)",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"slug": {
|
||||
"name": "slug",
|
||||
"type": "varchar(255)",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"name": {
|
||||
"name": "name",
|
||||
"type": "varchar(255)",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"time_created": {
|
||||
"name": "time_created",
|
||||
"type": "timestamp(3)",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": "(now())"
|
||||
},
|
||||
"time_updated": {
|
||||
"name": "time_updated",
|
||||
"type": "timestamp(3)",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": "CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3)"
|
||||
},
|
||||
"time_deleted": {
|
||||
"name": "time_deleted",
|
||||
"type": "timestamp(3)",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
}
|
||||
},
|
||||
"indexes": {
|
||||
"slug": {
|
||||
"name": "slug",
|
||||
"columns": [
|
||||
"slug"
|
||||
],
|
||||
"isUnique": true
|
||||
}
|
||||
},
|
||||
"foreignKeys": {},
|
||||
"compositePrimaryKeys": {
|
||||
"workspace_id": {
|
||||
"name": "workspace_id",
|
||||
"columns": [
|
||||
"id"
|
||||
]
|
||||
}
|
||||
},
|
||||
"uniqueConstraints": {},
|
||||
"checkConstraint": {}
|
||||
}
|
||||
},
|
||||
"views": {},
|
||||
"_meta": {
|
||||
"schemas": {},
|
||||
"tables": {},
|
||||
"columns": {}
|
||||
},
|
||||
"internal": {
|
||||
"tables": {},
|
||||
"indexes": {}
|
||||
}
|
||||
}
|
||||
|
|
@ -197,6 +197,13 @@
|
|||
"when": 1759553466608,
|
||||
"tag": "0027_hot_wong",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 28,
|
||||
"version": "5",
|
||||
"when": 1759805025276,
|
||||
"tag": "0028_careful_cerise",
|
||||
"breakpoints": true
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"$schema": "https://json.schemastore.org/package.json",
|
||||
"name": "@opencode-ai/console-core",
|
||||
"version": "0.14.3",
|
||||
"version": "0.14.5",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"dependencies": {
|
||||
|
|
|
|||
|
|
@ -1,12 +1,9 @@
|
|||
import { z } from "zod"
|
||||
import { and, eq, getTableColumns, isNull } from "drizzle-orm"
|
||||
import { eq } from "drizzle-orm"
|
||||
import { fn } from "./util/fn"
|
||||
import { Database } from "./drizzle"
|
||||
import { Identifier } from "./identifier"
|
||||
import { AccountTable } from "./schema/account.sql"
|
||||
import { Actor } from "./actor"
|
||||
import { WorkspaceTable } from "./schema/workspace.sql"
|
||||
import { UserTable } from "./schema/user.sql"
|
||||
|
||||
export namespace Account {
|
||||
export const create = fn(
|
||||
|
|
@ -46,16 +43,4 @@ export namespace Account {
|
|||
.then((rows) => rows[0])
|
||||
}),
|
||||
)
|
||||
|
||||
export const workspaces = async () => {
|
||||
const actor = Actor.assert("account")
|
||||
return Database.transaction(async (tx) =>
|
||||
tx
|
||||
.select(getTableColumns(WorkspaceTable))
|
||||
.from(WorkspaceTable)
|
||||
.innerJoin(UserTable, eq(UserTable.workspaceID, WorkspaceTable.id))
|
||||
.where(and(eq(UserTable.accountID, actor.properties.accountID), isNull(WorkspaceTable.timeDeleted)))
|
||||
.execute(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -20,6 +20,7 @@ export namespace Actor {
|
|||
properties: {
|
||||
userID: string
|
||||
workspaceID: string
|
||||
accountID: string
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -71,4 +72,12 @@ export namespace Actor {
|
|||
}
|
||||
throw new Error(`actor of type "${actor.type}" is not associated with a workspace`)
|
||||
}
|
||||
|
||||
export function account() {
|
||||
const actor = use()
|
||||
if ("accountID" in actor.properties) {
|
||||
return actor.properties.accountID
|
||||
}
|
||||
throw new Error(`actor of type "${actor.type}" is not associated with an account`)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { primaryKey, mysqlTable, uniqueIndex, varchar, boolean } from "drizzle-orm/mysql-core"
|
||||
import { primaryKey, mysqlTable, uniqueIndex, varchar } from "drizzle-orm/mysql-core"
|
||||
import { timestamps, ulid } from "../drizzle/types"
|
||||
|
||||
export const WorkspaceTable = mysqlTable(
|
||||
|
|
@ -6,7 +6,7 @@ export const WorkspaceTable = mysqlTable(
|
|||
{
|
||||
id: ulid("id").notNull().primaryKey(),
|
||||
slug: varchar("slug", { length: 255 }),
|
||||
name: varchar("name", { length: 255 }),
|
||||
name: varchar("name", { length: 255 }).notNull(),
|
||||
...timestamps,
|
||||
},
|
||||
(table) => [uniqueIndex("slug").on(table.slug)],
|
||||
|
|
|
|||
|
|
@ -172,8 +172,6 @@ export namespace User {
|
|||
),
|
||||
),
|
||||
)
|
||||
|
||||
return invitations.length
|
||||
})
|
||||
|
||||
export const updateRole = fn(
|
||||
|
|
|
|||
|
|
@ -7,36 +7,69 @@ import { UserTable } from "./schema/user.sql"
|
|||
import { BillingTable } from "./schema/billing.sql"
|
||||
import { WorkspaceTable } from "./schema/workspace.sql"
|
||||
import { Key } from "./key"
|
||||
import { eq, sql } from "drizzle-orm"
|
||||
|
||||
export namespace Workspace {
|
||||
export const create = fn(z.void(), async () => {
|
||||
const account = Actor.assert("account")
|
||||
const workspaceID = Identifier.create("workspace")
|
||||
const userID = Identifier.create("user")
|
||||
await Database.transaction(async (tx) => {
|
||||
await tx.insert(WorkspaceTable).values({
|
||||
id: workspaceID,
|
||||
export const create = fn(
|
||||
z.object({
|
||||
name: z.string().min(1),
|
||||
}),
|
||||
async ({ name }) => {
|
||||
const account = Actor.assert("account")
|
||||
const workspaceID = Identifier.create("workspace")
|
||||
const userID = Identifier.create("user")
|
||||
await Database.transaction(async (tx) => {
|
||||
await tx.insert(WorkspaceTable).values({
|
||||
id: workspaceID,
|
||||
name,
|
||||
})
|
||||
await tx.insert(UserTable).values({
|
||||
workspaceID,
|
||||
id: userID,
|
||||
accountID: account.properties.accountID,
|
||||
name: "",
|
||||
role: "admin",
|
||||
})
|
||||
await tx.insert(BillingTable).values({
|
||||
workspaceID,
|
||||
id: Identifier.create("billing"),
|
||||
balance: 0,
|
||||
})
|
||||
})
|
||||
await tx.insert(UserTable).values({
|
||||
workspaceID,
|
||||
id: userID,
|
||||
accountID: account.properties.accountID,
|
||||
name: "",
|
||||
role: "admin",
|
||||
})
|
||||
await tx.insert(BillingTable).values({
|
||||
workspaceID,
|
||||
id: Identifier.create("billing"),
|
||||
balance: 0,
|
||||
})
|
||||
})
|
||||
await Actor.provide(
|
||||
"system",
|
||||
{
|
||||
workspaceID,
|
||||
},
|
||||
() => Key.create({ userID, name: "Default API Key" }),
|
||||
await Actor.provide(
|
||||
"system",
|
||||
{
|
||||
workspaceID,
|
||||
},
|
||||
() => Key.create({ userID, name: "Default API Key" }),
|
||||
)
|
||||
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)),
|
||||
)
|
||||
},
|
||||
)
|
||||
|
||||
export const remove = fn(z.void(), async () => {
|
||||
await Database.use((tx) =>
|
||||
tx
|
||||
.update(WorkspaceTable)
|
||||
.set({ timeDeleted: sql`now()` })
|
||||
.where(eq(WorkspaceTable.id, Actor.workspace())),
|
||||
)
|
||||
return workspaceID
|
||||
})
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "@opencode-ai/console-function",
|
||||
"version": "0.14.3",
|
||||
"version": "0.14.5",
|
||||
"$schema": "https://json.schemastore.org/package.json",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
|
|
|
|||
|
|
@ -11,6 +11,9 @@ import { Workspace } from "@opencode-ai/console-core/workspace.js"
|
|||
import { Actor } from "@opencode-ai/console-core/actor.js"
|
||||
import { Resource } from "@opencode-ai/console-resource"
|
||||
import { User } from "@opencode-ai/console-core/user.js"
|
||||
import { and, Database, eq, isNull } from "@opencode-ai/console-core/drizzle/index.js"
|
||||
import { WorkspaceTable } from "@opencode-ai/console-core/schema/workspace.sql.js"
|
||||
import { UserTable } from "@opencode-ai/console-core/schema/user.sql.js"
|
||||
|
||||
type Env = {
|
||||
AuthStorage: KVNamespace
|
||||
|
|
@ -123,9 +126,22 @@ export default {
|
|||
})
|
||||
}
|
||||
await Actor.provide("account", { accountID, email }, async () => {
|
||||
const workspaceCount = await User.joinInvitedWorkspaces()
|
||||
if (workspaceCount === 0) {
|
||||
await Workspace.create()
|
||||
await User.joinInvitedWorkspaces()
|
||||
const workspaces = await Database.transaction(async (tx) =>
|
||||
tx
|
||||
.select({ id: WorkspaceTable.id })
|
||||
.from(WorkspaceTable)
|
||||
.innerJoin(UserTable, eq(UserTable.workspaceID, WorkspaceTable.id))
|
||||
.where(
|
||||
and(
|
||||
eq(UserTable.accountID, accountID),
|
||||
isNull(UserTable.timeDeleted),
|
||||
isNull(WorkspaceTable.timeDeleted),
|
||||
),
|
||||
),
|
||||
)
|
||||
if (workspaces.length === 0) {
|
||||
await Workspace.create({ name: "Default" })
|
||||
}
|
||||
})
|
||||
return ctx.subject("account", accountID, { accountID, email })
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"$schema": "https://json.schemastore.org/package.json",
|
||||
"name": "@opencode-ai/console-mail",
|
||||
"version": "0.14.3",
|
||||
"version": "0.14.5",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"dependencies": {
|
||||
|
|
|
|||
|
|
@ -1,20 +1,15 @@
|
|||
{
|
||||
"name": "@opencode-ai/console-scripts",
|
||||
"version": "0.14.3",
|
||||
"version": "0.14.5",
|
||||
"$schema": "https://json.schemastore.org/package.json",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"shell": "sst shell -- bun tsx",
|
||||
"shell-dev": "sst shell --stage dev -- bun tsx",
|
||||
"shell-prod": "sst shell --stage production -- bun tsx"
|
||||
"shell": "sst shell -- bun",
|
||||
"shell-dev": "sst shell --stage dev -- bun",
|
||||
"shell-prod": "sst shell --stage production -- bun"
|
||||
},
|
||||
"dependencies": {
|
||||
"@opencode-ai/console-core": "workspace:*",
|
||||
"tsx": "4.20.5"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "catalog:",
|
||||
"typescript": "catalog:"
|
||||
"@opencode-ai/console-core": "workspace:*"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "@opencode-ai/desktop",
|
||||
"version": "0.14.3",
|
||||
"version": "0.14.5",
|
||||
"description": "",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "@opencode-ai/function",
|
||||
"version": "0.14.3",
|
||||
"version": "0.14.5",
|
||||
"$schema": "https://json.schemastore.org/package.json",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"$schema": "https://json.schemastore.org/package.json",
|
||||
"version": "0.14.3",
|
||||
"version": "0.14.5",
|
||||
"name": "opencode",
|
||||
"type": "module",
|
||||
"private": true,
|
||||
|
|
|
|||
|
|
@ -106,7 +106,7 @@ export const RunCommand = cmd({
|
|||
|
||||
if (args.session) return Session.get(args.session)
|
||||
|
||||
return Session.create()
|
||||
return Session.create({})
|
||||
})()
|
||||
|
||||
if (!session) {
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ import z from "zod/v4"
|
|||
import { Session } from "../session"
|
||||
import { Bus } from "../bus"
|
||||
import { Instance } from "../project/instance"
|
||||
import { withTimeout } from "@/util/timeout"
|
||||
|
||||
export namespace MCP {
|
||||
const log = Log.create({ service: "mcp" })
|
||||
|
|
@ -20,11 +21,13 @@ export namespace MCP {
|
|||
}),
|
||||
)
|
||||
|
||||
type MCPClient = Awaited<ReturnType<typeof experimental_createMCPClient>>
|
||||
|
||||
const state = Instance.state(
|
||||
async () => {
|
||||
const cfg = await Config.get()
|
||||
const clients: {
|
||||
[name: string]: Awaited<ReturnType<typeof experimental_createMCPClient>>
|
||||
[name: string]: MCPClient
|
||||
} = {}
|
||||
for (const [key, mcp] of Object.entries(cfg.mcp ?? {})) {
|
||||
if (mcp.enabled === false) {
|
||||
|
|
@ -135,8 +138,17 @@ export namespace MCP {
|
|||
}
|
||||
}
|
||||
|
||||
for (const [key, client] of Object.entries(clients)) {
|
||||
const result = await withTimeout(client.tools(), 5000).catch(() => {})
|
||||
if (!result) {
|
||||
log.warn("mcp client verification failed, removing client", { key })
|
||||
delete clients[key]
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
clients,
|
||||
config: cfg.mcp ?? {},
|
||||
}
|
||||
},
|
||||
async (state) => {
|
||||
|
|
@ -146,6 +158,23 @@ export namespace MCP {
|
|||
},
|
||||
)
|
||||
|
||||
export async function status() {
|
||||
return state().then((state) => {
|
||||
const result: Record<string, "connected" | "failed" | "disabled"> = {}
|
||||
for (const [key, client] of Object.entries(state.config)) {
|
||||
if (client.enabled === false) {
|
||||
result[key] = "disabled"
|
||||
continue
|
||||
}
|
||||
if (state.clients[key]) {
|
||||
result[key] = "connected"
|
||||
}
|
||||
result[key] = "failed"
|
||||
}
|
||||
return result
|
||||
})
|
||||
}
|
||||
|
||||
export async function clients() {
|
||||
return state().then((state) => state.clients)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -31,7 +31,7 @@ import { SessionRevert } from "../session/revert"
|
|||
import { lazy } from "../util/lazy"
|
||||
import { Todo } from "../session/todo"
|
||||
import { InstanceBootstrap } from "../project/bootstrap"
|
||||
import { Identifier } from "@/id/id"
|
||||
import { MCP } from "../mcp"
|
||||
|
||||
const ERRORS = {
|
||||
400: {
|
||||
|
|
@ -308,7 +308,7 @@ export namespace Server {
|
|||
validator(
|
||||
"param",
|
||||
z.object({
|
||||
id: z.string(),
|
||||
id: Session.get.schema,
|
||||
}),
|
||||
),
|
||||
async (c) => {
|
||||
|
|
@ -336,7 +336,7 @@ export namespace Server {
|
|||
validator(
|
||||
"param",
|
||||
z.object({
|
||||
id: z.string(),
|
||||
id: Session.children.schema,
|
||||
}),
|
||||
),
|
||||
async (c) => {
|
||||
|
|
@ -390,18 +390,10 @@ export namespace Server {
|
|||
},
|
||||
},
|
||||
}),
|
||||
validator(
|
||||
"json",
|
||||
z
|
||||
.object({
|
||||
parentID: z.string().optional(),
|
||||
title: z.string().optional(),
|
||||
})
|
||||
.optional(),
|
||||
),
|
||||
validator("json", Session.create.schema.optional()),
|
||||
async (c) => {
|
||||
const body = c.req.valid("json") ?? {}
|
||||
const session = await Session.create(body.parentID, body.title)
|
||||
const session = await Session.create(body)
|
||||
return c.json(session)
|
||||
},
|
||||
)
|
||||
|
|
@ -424,7 +416,7 @@ export namespace Server {
|
|||
validator(
|
||||
"param",
|
||||
z.object({
|
||||
id: z.string(),
|
||||
id: Session.remove.schema,
|
||||
}),
|
||||
),
|
||||
async (c) => {
|
||||
|
|
@ -495,14 +487,7 @@ export namespace Server {
|
|||
id: z.string().meta({ description: "Session ID" }),
|
||||
}),
|
||||
),
|
||||
validator(
|
||||
"json",
|
||||
z.object({
|
||||
messageID: z.string(),
|
||||
providerID: z.string(),
|
||||
modelID: z.string(),
|
||||
}),
|
||||
),
|
||||
validator("json", Session.initialize.schema.omit({ sessionID: true })),
|
||||
async (c) => {
|
||||
const sessionID = c.req.valid("param").id
|
||||
const body = c.req.valid("json")
|
||||
|
|
@ -529,7 +514,7 @@ export namespace Server {
|
|||
validator(
|
||||
"param",
|
||||
z.object({
|
||||
id: Identifier.schema("session").meta({ description: "Session ID" }),
|
||||
id: Session.fork.schema.shape.sessionID,
|
||||
}),
|
||||
),
|
||||
validator("json", Session.fork.schema.omit({ sessionID: true })),
|
||||
|
|
@ -614,7 +599,7 @@ export namespace Server {
|
|||
validator(
|
||||
"param",
|
||||
z.object({
|
||||
id: z.string(),
|
||||
id: Session.unshare.schema,
|
||||
}),
|
||||
),
|
||||
async (c) => {
|
||||
|
|
@ -717,7 +702,7 @@ export namespace Server {
|
|||
),
|
||||
async (c) => {
|
||||
const params = c.req.valid("param")
|
||||
const message = await Session.getMessage(params.id, params.messageID)
|
||||
const message = await Session.getMessage({ sessionID: params.id, messageID: params.messageID })
|
||||
return c.json(message)
|
||||
},
|
||||
)
|
||||
|
|
@ -1199,6 +1184,26 @@ export namespace Server {
|
|||
return c.json(modes)
|
||||
},
|
||||
)
|
||||
.get(
|
||||
"/mcp",
|
||||
describeRoute({
|
||||
description: "Get MCP server status",
|
||||
operationId: "mcp.status",
|
||||
responses: {
|
||||
200: {
|
||||
description: "MCP server status",
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: resolver(z.any()),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
async (c) => {
|
||||
return c.json(await MCP.status())
|
||||
},
|
||||
)
|
||||
.post(
|
||||
"/tui/append-prompt",
|
||||
describeRoute({
|
||||
|
|
|
|||
|
|
@ -145,7 +145,7 @@ export namespace SessionCompaction {
|
|||
},
|
||||
],
|
||||
})
|
||||
const usage = Session.getUsage(model.info, generated.usage, generated.providerMetadata)
|
||||
const usage = Session.getUsage({ model: model.info, usage: generated.usage, metadata: generated.providerMetadata })
|
||||
msg.cost += usage.cost
|
||||
msg.tokens = usage.tokens
|
||||
msg.summary = true
|
||||
|
|
|
|||
|
|
@ -93,13 +93,21 @@ export namespace Session {
|
|||
),
|
||||
}
|
||||
|
||||
export async function create(parentID?: string, title?: string) {
|
||||
return createNext({
|
||||
parentID,
|
||||
directory: Instance.directory,
|
||||
title,
|
||||
})
|
||||
}
|
||||
export const create = fn(
|
||||
z
|
||||
.object({
|
||||
parentID: Identifier.schema("session").optional(),
|
||||
title: z.string().optional(),
|
||||
})
|
||||
.optional(),
|
||||
async (input) => {
|
||||
return createNext({
|
||||
parentID: input?.parentID,
|
||||
directory: Instance.directory,
|
||||
title: input?.title,
|
||||
})
|
||||
},
|
||||
)
|
||||
|
||||
export const fork = fn(
|
||||
z.object({
|
||||
|
|
@ -132,11 +140,11 @@ export namespace Session {
|
|||
},
|
||||
)
|
||||
|
||||
export async function touch(sessionID: string) {
|
||||
export const touch = fn(Identifier.schema("session"), async (sessionID) => {
|
||||
await update(sessionID, (draft) => {
|
||||
draft.time.updated = Date.now()
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
export async function createNext(input: { id?: string; title?: string; parentID?: string; directory: string }) {
|
||||
const result: Info = {
|
||||
|
|
@ -170,16 +178,16 @@ export namespace Session {
|
|||
return result
|
||||
}
|
||||
|
||||
export async function get(id: string) {
|
||||
export const get = fn(Identifier.schema("session"), async (id) => {
|
||||
const read = await Storage.read<Info>(["session", Instance.project.id, id])
|
||||
return read as Info
|
||||
}
|
||||
})
|
||||
|
||||
export async function getShare(id: string) {
|
||||
export const getShare = fn(Identifier.schema("session"), async (id) => {
|
||||
return Storage.read<ShareInfo>(["share", id])
|
||||
}
|
||||
})
|
||||
|
||||
export async function share(id: string) {
|
||||
export const share = fn(Identifier.schema("session"), async (id) => {
|
||||
const cfg = await Config.get()
|
||||
if (cfg.share === "disabled") {
|
||||
throw new Error("Sharing is disabled in configuration")
|
||||
|
|
@ -202,9 +210,9 @@ export namespace Session {
|
|||
}
|
||||
}
|
||||
return share
|
||||
}
|
||||
})
|
||||
|
||||
export async function unshare(id: string) {
|
||||
export const unshare = fn(Identifier.schema("session"), async (id) => {
|
||||
const share = await getShare(id)
|
||||
if (!share) return
|
||||
await Storage.remove(["share", id])
|
||||
|
|
@ -212,7 +220,7 @@ export namespace Session {
|
|||
draft.share = undefined
|
||||
})
|
||||
await Share.remove(id, share.secret)
|
||||
}
|
||||
})
|
||||
|
||||
export async function update(id: string, editor: (session: Info) => void) {
|
||||
const project = Instance.project
|
||||
|
|
@ -226,7 +234,7 @@ export namespace Session {
|
|||
return result
|
||||
}
|
||||
|
||||
export async function messages(sessionID: string) {
|
||||
export const messages = fn(Identifier.schema("session"), async (sessionID) => {
|
||||
const result = [] as MessageV2.WithParts[]
|
||||
for (const p of await Storage.list(["message", sessionID])) {
|
||||
const read = await Storage.read<MessageV2.Info>(p)
|
||||
|
|
@ -237,16 +245,22 @@ export namespace Session {
|
|||
}
|
||||
result.sort((a, b) => (a.info.id > b.info.id ? 1 : -1))
|
||||
return result
|
||||
}
|
||||
})
|
||||
|
||||
export async function getMessage(sessionID: string, messageID: string) {
|
||||
return {
|
||||
info: await Storage.read<MessageV2.Info>(["message", sessionID, messageID]),
|
||||
parts: await getParts(messageID),
|
||||
}
|
||||
}
|
||||
export const getMessage = fn(
|
||||
z.object({
|
||||
sessionID: Identifier.schema("session"),
|
||||
messageID: Identifier.schema("message"),
|
||||
}),
|
||||
async (input) => {
|
||||
return {
|
||||
info: await Storage.read<MessageV2.Info>(["message", input.sessionID, input.messageID]),
|
||||
parts: await getParts(input.messageID),
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
export async function getParts(messageID: string) {
|
||||
export const getParts = fn(Identifier.schema("message"), async (messageID) => {
|
||||
const result = [] as MessageV2.Part[]
|
||||
for (const item of await Storage.list(["part", messageID])) {
|
||||
const read = await Storage.read<MessageV2.Part>(item)
|
||||
|
|
@ -254,7 +268,7 @@ export namespace Session {
|
|||
}
|
||||
result.sort((a, b) => (a.id > b.id ? 1 : -1))
|
||||
return result
|
||||
}
|
||||
})
|
||||
|
||||
export async function* list() {
|
||||
const project = Instance.project
|
||||
|
|
@ -263,7 +277,7 @@ export namespace Session {
|
|||
}
|
||||
}
|
||||
|
||||
export async function children(parentID: string) {
|
||||
export const children = fn(Identifier.schema("session"), async (parentID) => {
|
||||
const project = Instance.project
|
||||
const result = [] as Session.Info[]
|
||||
for (const item of await Storage.list(["session", project.id])) {
|
||||
|
|
@ -272,9 +286,9 @@ export namespace Session {
|
|||
result.push(session)
|
||||
}
|
||||
return result
|
||||
}
|
||||
})
|
||||
|
||||
export async function remove(sessionID: string) {
|
||||
export const remove = fn(Identifier.schema("session"), async (sessionID) => {
|
||||
const project = Instance.project
|
||||
try {
|
||||
const session = await get(sessionID)
|
||||
|
|
@ -295,56 +309,69 @@ export namespace Session {
|
|||
} catch (e) {
|
||||
log.error(e)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
export async function updateMessage(msg: MessageV2.Info) {
|
||||
export const updateMessage = fn(MessageV2.Info, async (msg) => {
|
||||
await Storage.write(["message", msg.sessionID, msg.id], msg)
|
||||
Bus.publish(MessageV2.Event.Updated, {
|
||||
info: msg,
|
||||
})
|
||||
return msg
|
||||
}
|
||||
})
|
||||
|
||||
export async function removeMessage(sessionID: string, messageID: string) {
|
||||
await Storage.remove(["message", sessionID, messageID])
|
||||
Bus.publish(MessageV2.Event.Removed, {
|
||||
sessionID,
|
||||
messageID,
|
||||
})
|
||||
return messageID
|
||||
}
|
||||
export const removeMessage = fn(
|
||||
z.object({
|
||||
sessionID: Identifier.schema("session"),
|
||||
messageID: Identifier.schema("message"),
|
||||
}),
|
||||
async (input) => {
|
||||
await Storage.remove(["message", input.sessionID, input.messageID])
|
||||
Bus.publish(MessageV2.Event.Removed, {
|
||||
sessionID: input.sessionID,
|
||||
messageID: input.messageID,
|
||||
})
|
||||
return input.messageID
|
||||
},
|
||||
)
|
||||
|
||||
export async function updatePart(part: MessageV2.Part) {
|
||||
export const updatePart = fn(MessageV2.Part, async (part) => {
|
||||
await Storage.write(["part", part.messageID, part.id], part)
|
||||
Bus.publish(MessageV2.Event.PartUpdated, {
|
||||
part,
|
||||
})
|
||||
return part
|
||||
}
|
||||
})
|
||||
|
||||
export function getUsage(model: ModelsDev.Model, usage: LanguageModelUsage, metadata?: ProviderMetadata) {
|
||||
const tokens = {
|
||||
input: usage.inputTokens ?? 0,
|
||||
output: usage.outputTokens ?? 0,
|
||||
reasoning: usage?.reasoningTokens ?? 0,
|
||||
cache: {
|
||||
write: (metadata?.["anthropic"]?.["cacheCreationInputTokens"] ??
|
||||
// @ts-expect-error
|
||||
metadata?.["bedrock"]?.["usage"]?.["cacheWriteInputTokens"] ??
|
||||
0) as number,
|
||||
read: usage.cachedInputTokens ?? 0,
|
||||
},
|
||||
}
|
||||
return {
|
||||
cost: new Decimal(0)
|
||||
.add(new Decimal(tokens.input).mul(model.cost?.input ?? 0).div(1_000_000))
|
||||
.add(new Decimal(tokens.output).mul(model.cost?.output ?? 0).div(1_000_000))
|
||||
.add(new Decimal(tokens.cache.read).mul(model.cost?.cache_read ?? 0).div(1_000_000))
|
||||
.add(new Decimal(tokens.cache.write).mul(model.cost?.cache_write ?? 0).div(1_000_000))
|
||||
.toNumber(),
|
||||
tokens,
|
||||
}
|
||||
}
|
||||
export const getUsage = fn(
|
||||
z.object({
|
||||
model: z.custom<ModelsDev.Model>(),
|
||||
usage: z.custom<LanguageModelUsage>(),
|
||||
metadata: z.custom<ProviderMetadata>().optional(),
|
||||
}),
|
||||
(input) => {
|
||||
const tokens = {
|
||||
input: input.usage.inputTokens ?? 0,
|
||||
output: input.usage.outputTokens ?? 0,
|
||||
reasoning: input.usage?.reasoningTokens ?? 0,
|
||||
cache: {
|
||||
write: (input.metadata?.["anthropic"]?.["cacheCreationInputTokens"] ??
|
||||
// @ts-expect-error
|
||||
input.metadata?.["bedrock"]?.["usage"]?.["cacheWriteInputTokens"] ??
|
||||
0) as number,
|
||||
read: input.usage.cachedInputTokens ?? 0,
|
||||
},
|
||||
}
|
||||
return {
|
||||
cost: new Decimal(0)
|
||||
.add(new Decimal(tokens.input).mul(input.model.cost?.input ?? 0).div(1_000_000))
|
||||
.add(new Decimal(tokens.output).mul(input.model.cost?.output ?? 0).div(1_000_000))
|
||||
.add(new Decimal(tokens.cache.read).mul(input.model.cost?.cache_read ?? 0).div(1_000_000))
|
||||
.add(new Decimal(tokens.cache.write).mul(input.model.cost?.cache_write ?? 0).div(1_000_000))
|
||||
.toNumber(),
|
||||
tokens,
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
export class BusyError extends Error {
|
||||
constructor(public readonly sessionID: string) {
|
||||
|
|
@ -352,27 +379,30 @@ export namespace Session {
|
|||
}
|
||||
}
|
||||
|
||||
export async function initialize(input: {
|
||||
sessionID: string
|
||||
modelID: string
|
||||
providerID: string
|
||||
messageID: string
|
||||
}) {
|
||||
await SessionPrompt.prompt({
|
||||
sessionID: input.sessionID,
|
||||
messageID: input.messageID,
|
||||
model: {
|
||||
providerID: input.providerID,
|
||||
modelID: input.modelID,
|
||||
},
|
||||
parts: [
|
||||
{
|
||||
id: Identifier.ascending("part"),
|
||||
type: "text",
|
||||
text: PROMPT_INITIALIZE.replace("${path}", Instance.worktree),
|
||||
export const initialize = fn(
|
||||
z.object({
|
||||
sessionID: Identifier.schema("session"),
|
||||
modelID: z.string(),
|
||||
providerID: z.string(),
|
||||
messageID: Identifier.schema("message"),
|
||||
}),
|
||||
async (input) => {
|
||||
await SessionPrompt.prompt({
|
||||
sessionID: input.sessionID,
|
||||
messageID: input.messageID,
|
||||
model: {
|
||||
providerID: input.providerID,
|
||||
modelID: input.modelID,
|
||||
},
|
||||
],
|
||||
})
|
||||
await Project.setInitialized(Instance.project.id)
|
||||
}
|
||||
parts: [
|
||||
{
|
||||
id: Identifier.ascending("part"),
|
||||
type: "text",
|
||||
text: PROMPT_INITIALIZE.replace("${path}", Instance.worktree),
|
||||
},
|
||||
],
|
||||
})
|
||||
await Project.setInitialized(Instance.project.id)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -528,6 +528,8 @@ export namespace SessionPrompt {
|
|||
)
|
||||
|
||||
return {
|
||||
title: "",
|
||||
metadata: {},
|
||||
output,
|
||||
}
|
||||
}
|
||||
|
|
@ -1027,7 +1029,11 @@ export namespace SessionPrompt {
|
|||
break
|
||||
|
||||
case "finish-step":
|
||||
const usage = Session.getUsage(input.model, value.usage, value.providerMetadata)
|
||||
const usage = Session.getUsage({
|
||||
model: input.model,
|
||||
usage: value.usage,
|
||||
metadata: value.providerMetadata,
|
||||
})
|
||||
assistantMsg.cost += usage.cost
|
||||
assistantMsg.tokens = usage.tokens
|
||||
await Session.updateMessage(assistantMsg)
|
||||
|
|
|
|||
|
|
@ -20,12 +20,11 @@ export const GrepTool = Tool.define("grep", {
|
|||
const searchPath = params.path || Instance.directory
|
||||
|
||||
const rgPath = await Ripgrep.filepath()
|
||||
const args = ["-n", params.pattern]
|
||||
const args = ["-nH", "--field-match-separator=|", params.pattern]
|
||||
if (params.include) {
|
||||
args.push("--glob", params.include)
|
||||
}
|
||||
args.push(searchPath)
|
||||
args.push("--field-match-separator=|")
|
||||
|
||||
const proc = Bun.spawn([rgPath, ...args], {
|
||||
stdout: "pipe",
|
||||
|
|
|
|||
|
|
@ -26,8 +26,11 @@ export const TaskTool = Tool.define("task", async () => {
|
|||
async execute(params, ctx) {
|
||||
const agent = await Agent.get(params.subagent_type)
|
||||
if (!agent) throw new Error(`Unknown agent type: ${params.subagent_type} is not a valid agent type`)
|
||||
const session = await Session.create(ctx.sessionID, params.description + ` (@${agent.name} subagent)`)
|
||||
const msg = await Session.getMessage(ctx.sessionID, ctx.messageID)
|
||||
const session = await Session.create({
|
||||
parentID: ctx.sessionID,
|
||||
title: params.description + ` (@${agent.name} subagent)`,
|
||||
})
|
||||
const msg = await Session.getMessage({ sessionID: ctx.sessionID, messageID: ctx.messageID })
|
||||
if (msg.info.role !== "assistant") throw new Error("Not an assistant message")
|
||||
const messageID = Identifier.ascending("message")
|
||||
const parts: Record<string, MessageV2.ToolPart> = {}
|
||||
|
|
|
|||
|
|
@ -1,75 +0,0 @@
|
|||
const parser = async () => {
|
||||
try {
|
||||
const { default: Parser } = await import("tree-sitter")
|
||||
const Bash = await import("tree-sitter-bash")
|
||||
const p = new Parser()
|
||||
p.setLanguage(Bash.language as any)
|
||||
return p
|
||||
} catch (e) {
|
||||
const { Parser, Language } = await import("web-tree-sitter")
|
||||
const { default: treeWasm } = await import("web-tree-sitter/web-tree-sitter.wasm" as string, {
|
||||
with: { type: "wasm" },
|
||||
})
|
||||
await Parser.init({
|
||||
locateFile() {
|
||||
return treeWasm
|
||||
},
|
||||
})
|
||||
const { default: bashWasm } = await import("tree-sitter-bash/tree-sitter-bash.wasm" as string, {
|
||||
with: { type: "wasm" },
|
||||
})
|
||||
const bashLanguage = await Language.load(bashWasm)
|
||||
const p = new Parser()
|
||||
p.setLanguage(bashLanguage)
|
||||
return p
|
||||
}
|
||||
}
|
||||
|
||||
const sourceCode = `cd --foo foo/bar && echo "hello" && cd ../baz`
|
||||
|
||||
const tree = await parser().then((p) => p.parse(sourceCode))
|
||||
|
||||
// Function to extract commands and arguments
|
||||
function extractCommands(node: any): Array<{ command: string; args: string[] }> {
|
||||
const commands: Array<{ command: string; args: string[] }> = []
|
||||
|
||||
function traverse(node: any) {
|
||||
if (node.type === "command") {
|
||||
const commandNode = node.child(0)
|
||||
if (commandNode) {
|
||||
const command = commandNode.text
|
||||
const args: string[] = []
|
||||
|
||||
// Extract arguments
|
||||
for (let i = 1; i < node.childCount; i++) {
|
||||
const child = node.child(i)
|
||||
if (child && child.type === "word") {
|
||||
args.push(child.text)
|
||||
}
|
||||
}
|
||||
|
||||
commands.push({ command, args })
|
||||
}
|
||||
}
|
||||
|
||||
// Traverse children
|
||||
for (let i = 0; i < node.childCount; i++) {
|
||||
traverse(node.child(i))
|
||||
}
|
||||
}
|
||||
|
||||
traverse(node)
|
||||
return commands
|
||||
}
|
||||
|
||||
// Extract and display commands
|
||||
console.log("Source code: " + sourceCode)
|
||||
if (!tree) {
|
||||
throw new Error("Failed to parse command")
|
||||
}
|
||||
const commands = extractCommands(tree.rootNode)
|
||||
console.log("Extracted commands:")
|
||||
commands.forEach((cmd, index) => {
|
||||
console.log(`${index + 1}. Command: ${cmd.command}`)
|
||||
console.log(` Args: [${cmd.args.join(", ")}]`)
|
||||
})
|
||||
|
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"$schema": "https://json.schemastore.org/package.json",
|
||||
"name": "@opencode-ai/plugin",
|
||||
"version": "0.14.3",
|
||||
"version": "0.14.5",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"typecheck": "tsc --noEmit",
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"$schema": "https://json.schemastore.org/package.json",
|
||||
"name": "@opencode-ai/sdk",
|
||||
"version": "0.14.3",
|
||||
"version": "0.14.5",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"typecheck": "tsc --noEmit",
|
||||
|
|
|
|||
|
|
@ -82,6 +82,8 @@ import type {
|
|||
AppLogResponses,
|
||||
AppAgentsData,
|
||||
AppAgentsResponses,
|
||||
McpStatusData,
|
||||
McpStatusResponses,
|
||||
TuiAppendPromptData,
|
||||
TuiAppendPromptResponses,
|
||||
TuiOpenHelpData,
|
||||
|
|
@ -567,6 +569,18 @@ class App extends _HeyApiClient {
|
|||
}
|
||||
}
|
||||
|
||||
class Mcp extends _HeyApiClient {
|
||||
/**
|
||||
* Get MCP server status
|
||||
*/
|
||||
public status<ThrowOnError extends boolean = false>(options?: Options<McpStatusData, ThrowOnError>) {
|
||||
return (options?.client ?? this._client).get<McpStatusResponses, unknown, ThrowOnError>({
|
||||
url: "/mcp",
|
||||
...options,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
class Tui extends _HeyApiClient {
|
||||
/**
|
||||
* Append prompt to the TUI
|
||||
|
|
@ -724,6 +738,7 @@ export class OpencodeClient extends _HeyApiClient {
|
|||
find = new Find({ client: this._client })
|
||||
file = new File({ client: this._client })
|
||||
app = new App({ client: this._client })
|
||||
mcp = new Mcp({ client: this._client })
|
||||
tui = new Tui({ client: this._client })
|
||||
auth = new Auth({ client: this._client })
|
||||
event = new Event({ client: this._client })
|
||||
|
|
|
|||
|
|
@ -1496,9 +1496,9 @@ export type SessionTodoResponse = SessionTodoResponses[keyof SessionTodoResponse
|
|||
|
||||
export type SessionInitData = {
|
||||
body?: {
|
||||
messageID: string
|
||||
providerID: string
|
||||
modelID: string
|
||||
providerID: string
|
||||
messageID: string
|
||||
}
|
||||
path: {
|
||||
/**
|
||||
|
|
@ -1526,9 +1526,12 @@ export type SessionForkData = {
|
|||
messageID?: string
|
||||
}
|
||||
path: {
|
||||
<<<<<<< HEAD
|
||||
/**
|
||||
* Session ID
|
||||
*/
|
||||
=======
|
||||
>>>>>>> dev
|
||||
id: string
|
||||
}
|
||||
query?: {
|
||||
|
|
@ -2076,6 +2079,22 @@ export type AppAgentsResponses = {
|
|||
|
||||
export type AppAgentsResponse = AppAgentsResponses[keyof AppAgentsResponses]
|
||||
|
||||
export type McpStatusData = {
|
||||
body?: never
|
||||
path?: never
|
||||
query?: {
|
||||
directory?: string
|
||||
}
|
||||
url: "/mcp"
|
||||
}
|
||||
|
||||
export type McpStatusResponses = {
|
||||
/**
|
||||
* MCP server status
|
||||
*/
|
||||
200: unknown
|
||||
}
|
||||
|
||||
export type TuiAppendPromptData = {
|
||||
body?: {
|
||||
text: string
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"name": "@opencode-ai/web",
|
||||
"type": "module",
|
||||
"version": "0.14.3",
|
||||
"version": "0.14.5",
|
||||
"scripts": {
|
||||
"dev": "astro dev",
|
||||
"dev:remote": "VITE_API_URL=https://api.opencode.ai astro dev",
|
||||
|
|
@ -31,7 +31,7 @@
|
|||
"sharp": "0.32.5",
|
||||
"shiki": "3.4.2",
|
||||
"solid-js": "catalog:",
|
||||
"toolbeam-docs-theme": "0.4.6"
|
||||
"toolbeam-docs-theme": "0.4.7"
|
||||
},
|
||||
"devDependencies": {
|
||||
"opencode": "workspace:*",
|
||||
|
|
|
|||
|
|
@ -69,6 +69,23 @@ Your editor should be able to validate and autocomplete based on the schema.
|
|||
|
||||
---
|
||||
|
||||
### TUI
|
||||
|
||||
You can configure TUI-specific settings through the `tui` option.
|
||||
|
||||
```json title="opencode.json"
|
||||
{
|
||||
"$schema": "https://opencode.ai/config.json",
|
||||
"tui": {
|
||||
"scroll_speed": 3
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
[Learn more about using the TUI here](/docs/tui).
|
||||
|
||||
---
|
||||
|
||||
### Models
|
||||
|
||||
You can configure the providers and models you want to use in your OpenCode config through the `provider`, `model` and `small_model` options.
|
||||
|
|
@ -204,23 +221,6 @@ OpenCode will automatically download any new updates when it starts up. You can
|
|||
|
||||
---
|
||||
|
||||
### TUI
|
||||
|
||||
You can configure TUI-specific settings through the `tui` option.
|
||||
|
||||
```json title="opencode.json"
|
||||
{
|
||||
"$schema": "https://opencode.ai/config.json",
|
||||
"tui": {
|
||||
"scroll_speed": 3
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
[Learn more about using the TUI here](/docs/tui).
|
||||
|
||||
---
|
||||
|
||||
### Formatters
|
||||
|
||||
You can configure code formatters through the `formatter` option.
|
||||
|
|
|
|||
|
|
@ -73,6 +73,10 @@ You can also access our models through the following API endpoints.
|
|||
| Grok Code Fast 1 | grok-code | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
|
||||
| Kimi K2 | kimi-k2 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
|
||||
|
||||
The [model id](/docs/config/#models) in your OpenCode config
|
||||
uses the format `opencode/<model-id>`. For example, for GPT 5 Codex, you would
|
||||
use `opencode/gpt-5-codex` in your config.
|
||||
|
||||
---
|
||||
|
||||
## Pricing
|
||||
|
|
@ -94,6 +98,8 @@ We support a pay-as-you-go model. Below are the prices **per 1M tokens**.
|
|||
| GPT 5 | $1.25 | $10.00 | $0.125 | - |
|
||||
| GPT 5 Codex | $1.25 | $10.00 | $0.125 | - |
|
||||
|
||||
You might notice _Claude Haiku 3.5_ in your usage history. This is a [low cost model](/docs/config/#models) that's used to generate the titles of your sessions.
|
||||
|
||||
:::note
|
||||
Credit card fees are passed along at cost; we don't charge anything beyond that.
|
||||
:::
|
||||
|
|
|
|||
|
|
@ -24,6 +24,10 @@
|
|||
--color-border-weak: hsl(0, 1%, 85%);
|
||||
|
||||
--color-icon: hsl(0, 1%, 55%);
|
||||
|
||||
/* For the share component */
|
||||
--sl-color-bg-surface: var(--sl-color-bg-nav);
|
||||
--sl-color-divider: var(--sl-color-gray-5);
|
||||
}
|
||||
|
||||
|
||||
|
|
@ -294,14 +298,16 @@ body > .page > header, :root[data-has-sidebar] body > .page > header {
|
|||
}
|
||||
|
||||
|
||||
.sl-container ul li a {
|
||||
nav.sidebar .sl-container ul li a,
|
||||
div.right-sidebar .sl-container ul li a {
|
||||
padding: 4px 24px !important;
|
||||
width: 100% !important;
|
||||
color: var(--color-text-weaker);
|
||||
opacity: 50%;
|
||||
}
|
||||
|
||||
.sl-container ul li a:hover {
|
||||
nav.sidebar .sl-container ul li a:hover,
|
||||
div.right-sidebar .sl-container ul li a:hover {
|
||||
background: var(--color-background-weak);
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
|
|
@ -309,12 +315,14 @@ body > .page > header, :root[data-has-sidebar] body > .page > header {
|
|||
}
|
||||
}
|
||||
|
||||
.sl-container ul li ul li {
|
||||
nav.sidebar .sl-container ul li ul li,
|
||||
div.right-sidebar .sl-container ul li ul li {
|
||||
padding: 4px 12px 0 12px !important;
|
||||
}
|
||||
|
||||
|
||||
.sl-container ul li a[aria-current="true"] {
|
||||
nav.sidebar .sl-container ul li a[aria-current="true"],
|
||||
div.right-sidebar .sl-container ul li a[aria-current="true"] {
|
||||
color: var(--color-text-strong) !important;
|
||||
opacity: 100%;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
"name": "opencode",
|
||||
"displayName": "opencode",
|
||||
"description": "opencode for VS Code",
|
||||
"version": "0.14.3",
|
||||
"version": "0.14.5",
|
||||
"publisher": "sst-dev",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue