Merge branch 'dev' into opentui

This commit is contained in:
Dax Raad 2025-10-12 01:14:15 -04:00
commit 58576cf07a
44 changed files with 486 additions and 675 deletions

20
.github/actions/setup-bun/action.yml vendored Normal file
View file

@ -0,0 +1,20 @@
name: "Setup Bun"
description: "Setup Bun with caching and install dependencies"
runs:
using: "composite"
steps:
- name: Setup Bun
uses: oven-sh/setup-bun@v2
- name: Cache ~/.bun
id: cache-bun
uses: actions/cache@v4
with:
path: ~/.bun
key: ${{ runner.os }}-bun-${{ hashFiles('bun.lockb', 'bun.lock') }}
restore-keys: |
${{ runner.os }}-bun-
- name: Install dependencies
run: bun install
shell: bash

View file

@ -15,11 +15,7 @@ jobs:
steps:
- uses: actions/checkout@v3
- uses: oven-sh/setup-bun@v1
with:
bun-version: 1.3.0
- run: bun install
- uses: ./.github/actions/setup-bun
- run: bun sst deploy --stage=${{ github.ref_name }}
env:

View file

@ -20,13 +20,10 @@ jobs:
token: ${{ secrets.GITHUB_TOKEN }}
- name: Setup Bun
uses: oven-sh/setup-bun@v1
with:
bun-version: 1.3.0
uses: ./.github/actions/setup-bun
- name: run
run: |
bun install
./script/format.ts
env:
CI: true

View file

@ -19,16 +19,13 @@ jobs:
with:
fetch-depth: 0
- uses: oven-sh/setup-bun@v2
with:
bun-version: 1.3.0
- uses: ./.github/actions/setup-bun
- run: git fetch --force --tags
- run: bun install -g @vscode/vsce
- name: Publish
run: |
bun install
./script/publish
working-directory: ./sdks/vscode
env:

View file

@ -35,18 +35,7 @@ jobs:
cache: true
cache-dependency-path: go.sum
- uses: oven-sh/setup-bun@v2
with:
bun-version: 1.3.0
- name: Cache ~/.bun
id: cache-bun
uses: actions/cache@v3
with:
path: ~/.bun
key: ${{ runner.os }}-bun-1-3-0-${{ hashFiles('bun.lock') }}
restore-keys: |
${{ runner.os }}-bun-1-3-0-
- uses: ./.github/actions/setup-bun
- name: Install makepkg
run: |
@ -60,8 +49,6 @@ jobs:
git config --global user.email "opencode@sst.dev"
git config --global user.name "opencode"
ssh-keyscan -H aur.archlinux.org >> ~/.ssh/known_hosts || true
- name: Install dependencies
run: bun install
- name: Install OpenCode
run: curl -fsSL https://opencode.ai/install | bash

View file

@ -24,21 +24,7 @@ jobs:
cache: true
cache-dependency-path: go.sum
- uses: oven-sh/setup-bun@v2
with:
bun-version: 1.3.0
- name: Cache ~/.bun
id: cache-bun
uses: actions/cache@v3
with:
path: ~/.bun
key: ${{ runner.os }}-bun-1-3-0-${{ hashFiles('bun.lock') }}
restore-keys: |
${{ runner.os }}-bun-1-3-0-
- name: Install dependencies
run: bun install
- uses: ./.github/actions/setup-bun
- name: Publish
run: |

View file

@ -16,9 +16,7 @@ jobs:
uses: actions/checkout@v4
- name: Setup Bun
uses: oven-sh/setup-bun@v2
with:
bun-version: latest
uses: ./.github/actions/setup-bun
- name: Run stats script
run: bun script/stats.ts

View file

@ -18,15 +18,12 @@ jobs:
token: ${{ secrets.GITHUB_TOKEN }}
- name: Setup Bun
uses: oven-sh/setup-bun@v1
with:
bun-version: 1.3.0
uses: ./.github/actions/setup-bun
- name: run
run: |
git config --global user.email "bot@opencode.ai"
git config --global user.name "opencode"
bun install
bun turbo test
env:
CI: true

View file

@ -13,12 +13,7 @@ jobs:
uses: actions/checkout@v4
- name: Setup Bun
uses: oven-sh/setup-bun@v1
with:
bun-version: 1.3.0
- name: Install dependencies
run: bun install
uses: ./.github/actions/setup-bun
- name: Run typecheck
run: bun typecheck

View file

@ -105,3 +105,4 @@
| 2025-10-08 | 474,643 (+7,307) | 394,139 (+8,701) | 868,782 (+16,008) |
| 2025-10-09 | 479,203 (+4,560) | 400,526 (+6,387) | 879,729 (+10,947) |
| 2025-10-10 | 484,374 (+5,171) | 406,015 (+5,489) | 890,389 (+10,660) |
| 2025-10-11 | 488,427 (+4,053) | 414,699 (+8,684) | 903,126 (+12,737) |

View file

@ -3,11 +3,14 @@
"workspaces": {
"": {
"name": "opencode",
"dependencies": {
"@opencode-ai/sdk": "workspace:*",
},
"devDependencies": {
"@tsconfig/bun": "1.0.9",
"husky": "9.1.7",
"prettier": "3.6.2",
"sst": "3.17.13",
"sst": "3.17.19",
"turbo": "2.5.6",
},
},
@ -28,10 +31,13 @@
"vinxi": "^0.5.7",
"zod": "catalog:",
},
"devDependencies": {
"typescript": "catalog:",
},
},
"packages/console/core": {
"name": "@opencode-ai/console-core",
"version": "0.14.7",
"version": "0.15.0",
"dependencies": {
"@aws-sdk/client-sts": "3.782.0",
"@jsx-email/render": "1.1.1",
@ -52,11 +58,12 @@
"@types/node": "24.7.1",
"drizzle-kit": "0.30.5",
"mysql2": "3.14.4",
"typescript": "catalog:",
},
},
"packages/console/function": {
"name": "@opencode-ai/console-function",
"version": "0.14.7",
"version": "0.15.0",
"dependencies": {
"@ai-sdk/anthropic": "2.0.0",
"@ai-sdk/openai": "2.0.2",
@ -79,7 +86,7 @@
},
"packages/console/mail": {
"name": "@opencode-ai/console-mail",
"version": "0.14.7",
"version": "0.15.0",
"dependencies": {
"@jsx-email/all": "2.2.3",
"@jsx-email/cli": "1.4.3",
@ -100,14 +107,14 @@
},
"packages/console/scripts": {
"name": "@opencode-ai/console-scripts",
"version": "0.14.7",
"version": "0.15.0",
"dependencies": {
"@opencode-ai/console-core": "workspace:*",
},
},
"packages/desktop": {
"name": "@opencode-ai/desktop",
"version": "0.14.7",
"version": "0.15.0",
"dependencies": {
"@kobalte/core": "catalog:",
"@opencode-ai/sdk": "workspace:*",
@ -142,7 +149,7 @@
},
"packages/function": {
"name": "@opencode-ai/function",
"version": "0.14.7",
"version": "0.15.0",
"dependencies": {
"@octokit/auth-app": "8.0.1",
"@octokit/rest": "22.0.0",
@ -157,7 +164,7 @@
},
"packages/opencode": {
"name": "opencode",
"version": "0.14.7",
"version": "0.15.0",
"bin": {
"opencode": "./bin/opencode",
},
@ -223,7 +230,7 @@
},
"packages/plugin": {
"name": "@opencode-ai/plugin",
"version": "0.14.7",
"version": "0.15.0",
"dependencies": {
"@opencode-ai/sdk": "workspace:*",
"zod": "catalog:",
@ -236,7 +243,7 @@
},
"packages/sdk/js": {
"name": "@opencode-ai/sdk",
"version": "0.14.7",
"version": "0.15.0",
"devDependencies": {
"@hey-api/openapi-ts": "0.81.0",
"@tsconfig/node22": "catalog:",
@ -246,7 +253,7 @@
},
"packages/web": {
"name": "@opencode-ai/web",
"version": "0.14.7",
"version": "0.15.0",
"dependencies": {
"@astrojs/cloudflare": "12.6.3",
"@astrojs/markdown-remark": "6.3.1",
@ -2881,23 +2888,21 @@
"sqlstring": ["sqlstring@2.3.3", "", {}, "sha512-qC9iz2FlN7DQl3+wjwn3802RTyjCx7sDvfQEXchwa6CWOx07/WVfh91gBmQ9fahw8snwGEWU3xGzOt4tFyHLxg=="],
"sst": ["sst@3.17.13", "", { "dependencies": { "aws-sdk": "2.1692.0", "aws4fetch": "1.0.18", "jose": "5.2.3", "opencontrol": "0.0.6", "openid-client": "5.6.4" }, "optionalDependencies": { "sst-darwin-arm64": "3.17.13", "sst-darwin-x64": "3.17.13", "sst-linux-arm64": "3.17.13", "sst-linux-x64": "3.17.13", "sst-linux-x86": "3.17.13", "sst-win32-arm64": "3.17.13", "sst-win32-x64": "3.17.13", "sst-win32-x86": "3.17.13" }, "bin": { "sst": "bin/sst.mjs" } }, "sha512-NaNTZL7uk2AsXzPBySQy7aqXlStXorR+bA785NxvCbskwkc44nVSQcEsvX5tdsD6/jrWpw9tDy4sStv2ycLAng=="],
"sst": ["sst@3.17.19", "", { "dependencies": { "aws-sdk": "2.1692.0", "aws4fetch": "1.0.18", "jose": "5.2.3", "opencontrol": "0.0.6", "openid-client": "5.6.4" }, "optionalDependencies": { "sst-darwin-arm64": "3.17.19", "sst-darwin-x64": "3.17.19", "sst-linux-arm64": "3.17.19", "sst-linux-x64": "3.17.19", "sst-linux-x86": "3.17.19", "sst-win32-arm64": "3.17.19", "sst-win32-x64": "3.17.19", "sst-win32-x86": "3.17.19" }, "bin": { "sst": "bin/sst.mjs" } }, "sha512-j0FlQhFZW+QWCczzqfPr6fZAF0Um7lP1tbGdd7zkbjFlxdk9BUBI4CYXUnopC6KaTMtjvpfg3XRF7v0bDc9g+A=="],
"sst-darwin-arm64": ["sst-darwin-arm64@3.17.13", "", { "os": "darwin", "cpu": "arm64" }, "sha512-HZaDReT/c+2CcEnFkYjMty63II2ckQrUniiSdoEH6eAWyU1Iy7UwKK4I2GYm+5dy9xeSBaOKga6FMdLqFxIiUg=="],
"sst-darwin-arm64": ["sst-darwin-arm64@3.17.19", "", { "os": "darwin", "cpu": "arm64" }, "sha512-6FeEgPqXkRT3o5qV0xktJ1eUiscJiPLBcGaxOxIEClpkVggZM83hO7Nizx/cAaAMhr1XQhbOZcKYueDHPdUY+Q=="],
"sst-darwin-x64": ["sst-darwin-x64@3.17.13", "", { "os": "darwin", "cpu": "x64" }, "sha512-1DlYMrmrI5RY3/Ob039JatgvDKZ5QNtyRkVu0WcnsOvcxFE4dzrCiYKYHg2A+FMDl+H1qcwy2gGA3BTwC9in1w=="],
"sst-darwin-x64": ["sst-darwin-x64@3.17.19", "", { "os": "darwin", "cpu": "x64" }, "sha512-/z78dxfLHG8FtOhpjMnYSpKSdQjfdyKbq+cL3eud2+g2BQr7IyQ8BWNGimk2oadh38V3r6dO1/5aVJh3x3l1rg=="],
"sst-linux-arm64": ["sst-linux-arm64@3.17.13", "", { "os": "linux", "cpu": "arm64" }, "sha512-A4+ZamchUdaX0pqvYWZ+r7OP1bruwEj9qgWT5kU7Q5pqaotIsEitYQi0q9nZFKH+5mXYesUwSy5FA+Q8T3X/Rg=="],
"sst-linux-arm64": ["sst-linux-arm64@3.17.19", "", { "os": "linux", "cpu": "arm64" }, "sha512-vbcMjiuLVxZ7352ajGlMqsS4J5AkAYvjLmsEALySUBVQhJUO9U7pk2P+Orfn702ZcO+6+NkGG9AL/g3K9EM1Tg=="],
"sst-linux-x64": ["sst-linux-x64@3.17.13", "", { "os": "linux", "cpu": "x64" }, "sha512-yhKZc5CojqjB2DnyeVka5jeRb4oc3lBx8Qf6or0w4cs47SBIqyvO0iR/3IeKvRRL1hiEUeUF8r/q83rQo9jZMw=="],
"sst-linux-x64": ["sst-linux-x64@3.17.19", "", { "os": "linux", "cpu": "x64" }, "sha512-gkNNmuHyvKjcb7RwMyoUH4wtgd7/bH7vUlMbcVsDzwt38y7+iTxyPMbcihucw42wDQRaDJtkDneSqj08U+MTFQ=="],
"sst-linux-x86": ["sst-linux-x86@3.17.13", "", { "os": "linux", "cpu": "none" }, "sha512-G1FIUmpUaECB/3CV5EO/y1QmV5mQ8RUkFeZq64oyILEEaMzSWWKz0glHzQ3+p316VE74MzbktiWRqsCKQy8GeA=="],
"sst-linux-x86": ["sst-linux-x86@3.17.19", "", { "os": "linux", "cpu": "none" }, "sha512-Bsvunkh4onZRVv4Rxq7bT/63qQOg2KJoQKhAQtFkJdbri/cOA2QWkzqH8+pC5Sv9rSvbcIJAEIhMXILC0pqCJw=="],
"sst-win32-arm64": ["sst-win32-arm64@3.17.13", "", { "os": "win32", "cpu": "arm64" }, "sha512-9uCiIXmsGoxGeNWgM81x/d6V/vecjoiHXhBUPDGlQ1Dct1SbtHAgaf/g2ec5XwSQb9B/tne4qk81eMlTl83Z1A=="],
"sst-win32-x64": ["sst-win32-x64@3.17.19", "", { "os": "win32", "cpu": "x64" }, "sha512-zgxSkGWZ1dewAr4R3slN/d3X9yumQDvAUOlJiX/6QE9Z67t/XNlow4+5i3L2oz4WHAFi59Un12YxbfM+RsBDmA=="],
"sst-win32-x64": ["sst-win32-x64@3.17.13", "", { "os": "win32", "cpu": "x64" }, "sha512-hTuj4rFSCI/9tX4NMUpNJ69Q9td/giekELDRNv03ys8TpJGoGvPT8D6VD1eX7j1CQnOZIgeEphfW9WmCXkLaIA=="],
"sst-win32-x86": ["sst-win32-x86@3.17.13", "", { "os": "win32", "cpu": "none" }, "sha512-AuMDGux+H1kPckKJ7Szgi04EpBoOKh/v5zFNAPjvWSkcWcGZ+hsBUx3h/FO/AkGK3RnlLMRj4CQQLoa10RSSIg=="],
"sst-win32-x86": ["sst-win32-x86@3.17.19", "", { "os": "win32", "cpu": "none" }, "sha512-z8S0kyb0ibz9Q3cNYDpcKYX47jys7j/mdebC8HUhtED1qKEAfqQ1vsR+zvWyN64Z9Ijj7aPi1KwNV6Et3d7F8g=="],
"stackframe": ["stackframe@1.3.4", "", {}, "sha512-oeVtt7eWQS+Na6F//S4kJ2K2VbRlS9D43mAlMyVpVWovy9o+jfgH8O9agzANzaiLjclA0oYzUXEM4PurhSUChw=="],

View file

@ -39,9 +39,12 @@
"@tsconfig/bun": "1.0.9",
"husky": "9.1.7",
"prettier": "3.6.2",
"sst": "3.17.13",
"sst": "3.17.19",
"turbo": "2.5.6"
},
"dependencies": {
"@opencode-ai/sdk": "workspace:*"
},
"repository": {
"type": "git",
"url": "https://github.com/sst/opencode"

View file

@ -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.7"
"version": "0.15.0"
},
"dependencies": {
"@ibm/plex": "6.4.1",
@ -24,6 +24,9 @@
"vinxi": "^0.5.7",
"zod": "catalog:"
},
"devDependencies": {
"typescript": "catalog:"
},
"engines": {
"node": ">=22"
}

View file

@ -0,0 +1,80 @@
[data-component="dropdown"] {
position: relative;
[data-slot="trigger"] {
display: flex;
align-items: center;
justify-content: space-between;
gap: var(--space-2);
padding: var(--space-2) var(--space-3);
border: none;
border-radius: var(--border-radius-sm);
background-color: transparent;
color: var(--color-text);
font-size: var(--font-size-sm);
font-family: var(--font-sans);
cursor: pointer;
transition: all 0.15s ease;
&:hover {
background-color: var(--color-surface-hover);
}
span {
flex: 1;
text-align: left;
font-weight: 500;
}
}
[data-slot="chevron"] {
flex-shrink: 0;
color: var(--color-text-secondary);
}
[data-slot="dropdown"] {
position: absolute;
top: 100%;
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);
min-width: 160px;
&[data-align="left"] {
left: 0;
}
&[data-align="right"] {
right: 0;
}
@media (prefers-color-scheme: dark) {
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
}
}
[data-slot="item"] {
display: block;
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;
transition: background-color 0.15s ease;
&:hover {
background-color: var(--color-bg-surface);
}
&[data-selected="true"] {
background-color: var(--color-accent-alpha);
}
}
}

View file

@ -0,0 +1,79 @@
import { JSX, Show, createEffect, onCleanup } from "solid-js"
import { createStore } from "solid-js/store"
import { IconChevron } from "./icon"
import "./dropdown.css"
interface DropdownProps {
trigger: JSX.Element | string
children: JSX.Element
open?: boolean
onOpenChange?: (open: boolean) => void
align?: "left" | "right"
class?: string
}
export function Dropdown(props: DropdownProps) {
const [store, setStore] = createStore({
isOpen: props.open ?? false,
})
let dropdownRef: HTMLDivElement | undefined
createEffect(() => {
if (props.open !== undefined) {
setStore("isOpen", props.open)
}
})
createEffect(() => {
const handleClickOutside = (event: MouseEvent) => {
if (dropdownRef && !dropdownRef.contains(event.target as Node)) {
setStore("isOpen", false)
props.onOpenChange?.(false)
}
}
document.addEventListener("click", handleClickOutside)
onCleanup(() => document.removeEventListener("click", handleClickOutside))
})
const toggle = () => {
const newValue = !store.isOpen
setStore("isOpen", newValue)
props.onOpenChange?.(newValue)
}
return (
<div data-component="dropdown" class={props.class} ref={dropdownRef}>
<button data-slot="trigger" type="button" onClick={toggle}>
{typeof props.trigger === "string" ? <span>{props.trigger}</span> : props.trigger}
<IconChevron data-slot="chevron" />
</button>
<Show when={store.isOpen}>
<div data-slot="dropdown" data-align={props.align ?? "left"}>
{props.children}
</div>
</Show>
</div>
)
}
interface DropdownItemProps {
children: JSX.Element
selected?: boolean
onClick?: () => void
type?: "button" | "submit" | "reset"
}
export function DropdownItem(props: DropdownItemProps) {
return (
<button
data-slot="item"
data-selected={props.selected ?? false}
type={props.type ?? "button"}
onClick={props.onClick}
>
{props.children}
</button>
)
}

View file

@ -73,7 +73,7 @@ export function IconCopy(props: JSX.SvgSVGAttributes<SVGSVGElement>) {
<svg {...props} width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path
d="M8.75 8.75V2.75H21.25V15.25H15.25M15.25 8.75H2.75V21.25H15.25V8.75Z"
stroke="#8E8B8B"
stroke="currentColor"
stroke-width="1.5"
stroke-linecap="square"
/>

View file

@ -1,68 +1,17 @@
[data-component="user-menu"] {
position: relative;
[data-slot="trigger"] {
display: flex;
align-items: center;
justify-content: space-between;
gap: var(--space-2);
padding: var(--space-2) var(--space-3);
border: none;
border-radius: var(--border-radius-sm);
background-color: transparent;
color: var(--color-text);
font-size: var(--font-size-sm);
font-family: var(--font-sans);
cursor: pointer;
transition: all 0.15s ease;
&:hover {
background-color: var(--color-surface-hover);
}
span {
flex: 1;
text-align: left;
font-weight: 500;
[data-component="dropdown"] {
[data-slot="trigger"] span {
color: var(--color-text-muted);
}
}
[data-slot="chevron"] {
flex-shrink: 0;
color: var(--color-text-secondary);
}
[data-slot="dropdown"] {
position: absolute;
top: 100%;
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);
min-width: 160px;
@media (prefers-color-scheme: dark) {
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
[data-slot="dropdown"] {
form {
width: 100%;
}
}
form {
width: 100%;
[data-slot="item"] {
color: var(--color-danger);
}
}
[data-slot="item"],
[data-slot="create-item"] {
width: 100%;
padding: var(--space-2-5) var(--space-3);
border: none;
background: none;
color: var(--color-danger);
font-size: var(--font-size-sm);
font-family: var(--font-sans);
text-align: left;
}
}

View file

@ -1,9 +1,7 @@
import { Show, onCleanup, createEffect } from "solid-js"
import { createStore } from "solid-js/store"
import { action, redirect } from "@solidjs/router"
import { getRequestEvent } from "solid-js/web"
import { useAuthSession } from "~/context/auth.session"
import { IconChevron } from "~/component/icon"
import { Dropdown } from "~/component/dropdown"
import "./user-menu.css"
const logout = action(async () => {
@ -23,41 +21,15 @@ const logout = action(async () => {
})
export function UserMenu(props: { email: string | null | undefined }) {
const [store, setStore] = createStore({
showDropdown: false,
})
let dropdownRef: HTMLDivElement | undefined
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="user-menu">
<div ref={dropdownRef}>
<button data-slot="trigger" type="button" onClick={() => setStore("showDropdown", !store.showDropdown)}>
<span>{props.email}</span>
<IconChevron data-slot="chevron" />
</button>
<Show when={store.showDropdown}>
<div data-slot="dropdown">
<form action={logout} method="post">
<button type="submit" formaction={logout} data-slot="item">
Logout
</button>
</form>
</div>
</Show>
</div>
<Dropdown trigger={props.email ?? ""} align="right">
<form action={logout} method="post">
<button type="submit" formaction={logout} data-slot="item">
Logout
</button>
</form>
</Dropdown>
</div>
)
}

View file

@ -1,66 +1,23 @@
[data-component="workspace-picker"] {
position: relative;
[data-slot="trigger"] {
/* Override blue accent colors with neutral colors for dropdown trigger */
--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;
display: flex;
align-items: center;
justify-content: space-between;
gap: var(--space-2);
padding: var(--space-2) var(--space-3);
border: none;
border-radius: var(--border-radius-sm);
background-color: transparent;
color: var(--color-text);
font-size: var(--font-size-sm);
font-family: var(--font-sans);
cursor: pointer;
transition: all 0.15s ease;
&:hover {
background-color: var(--color-surface-hover);
[data-component="dropdown"] {
[data-slot="trigger"] {
/* Override blue accent colors with neutral colors for dropdown trigger */
--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;
}
span {
flex: 1;
text-align: left;
font-weight: 500;
color: var(--color-text);
[data-slot="dropdown"] {
max-height: 240px;
overflow-y: auto;
min-width: 200px;
}
}
[data-slot="chevron"] {
flex-shrink: 0;
color: var(--color-text-secondary);
}
[data-slot="dropdown"] {
position: absolute;
top: 100%;
left: 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;
min-width: 200px;
@media (prefers-color-scheme: dark) {
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
}
}
[data-slot="item"],
[data-slot="create-item"] {
width: 100%;
padding: var(--space-2-5) var(--space-3);
@ -70,6 +27,12 @@
font-size: var(--font-size-sm);
font-family: var(--font-sans);
text-align: left;
cursor: pointer;
transition: background-color 0.15s ease;
&:hover {
background-color: var(--color-bg-surface);
}
}
[data-slot="create-form"] {

View file

@ -1,5 +1,5 @@
import { query, useParams, action, createAsync, redirect, useSubmission } from "@solidjs/router"
import { For, Show, createEffect, onCleanup } from "solid-js"
import { For, Show, createEffect } from "solid-js"
import { createStore } from "solid-js/store"
import { withActor } from "~/context/auth.withActor"
import { Actor } from "@opencode-ai/console-core/actor.js"
@ -7,7 +7,7 @@ import { and, Database, eq, isNull } from "@opencode-ai/console-core/drizzle/ind
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 { IconChevron } from "~/component/icon"
import { Dropdown, DropdownItem } from "~/component/dropdown"
import { Modal } from "~/component/modal"
import "./workspace-picker.css"
@ -45,9 +45,7 @@ export function WorkspacePicker() {
const submission = useSubmission(createWorkspace)
const [store, setStore] = createStore({
showForm: false,
showDropdown: false,
})
let dropdownRef: HTMLDivElement | undefined
let inputRef: HTMLInputElement | undefined
const currentWorkspace = () => {
@ -56,7 +54,7 @@ export function WorkspacePicker() {
}
const handleWorkspaceNew = () => {
setStore({ showForm: true, showDropdown: false })
setStore("showForm", true)
}
createEffect(() => {
@ -66,11 +64,7 @@ export function WorkspacePicker() {
})
const handleSelectWorkspace = (workspaceID: string) => {
if (workspaceID === params.id) {
setStore("showDropdown", false)
return
}
if (workspaceID === params.id) return
window.location.href = `/workspace/${workspaceID}`
}
@ -78,48 +72,22 @@ export function WorkspacePicker() {
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}>
<button data-slot="trigger" type="button" onClick={() => setStore("showDropdown", !store.showDropdown)}>
<span>{currentWorkspace()}</span>
<IconChevron data-slot="chevron" />
<Dropdown trigger={currentWorkspace()} align="left">
<For each={workspaces()}>
{(workspace) => (
<DropdownItem selected={workspace.id === params.id} onClick={() => handleSelectWorkspace(workspace.id)}>
{workspace.name || workspace.slug}
</DropdownItem>
)}
</For>
<button data-slot="create-item" type="button" onClick={() => handleWorkspaceNew()}>
+ Create New Workspace
</button>
<Show when={store.showDropdown}>
<div data-slot="dropdown">
<For each={workspaces()}>
{(workspace) => (
<button
data-slot="item"
data-selected={workspace.id === params.id}
type="button"
onClick={() => handleSelectWorkspace(workspace.id)}
>
{workspace.name || workspace.slug}
</button>
)}
</For>
<button data-slot="create-item" type="button" onClick={() => handleWorkspaceNew()}>
+ Create New Workspace
</button>
</div>
</Show>
</div>
</Dropdown>
<Modal open={store.showForm} onClose={() => setStore("showForm", false)} title="Create New Workspace">
<form data-slot="create-form" action={createWorkspace} method="post">

View file

@ -30,31 +30,33 @@ export default function () {
</a>
.
</span>
<span data-slot="billing-info">
<Show
when={billingInfo()?.reload}
fallback={
<button
data-color="primary"
data-size="sm"
disabled={createCheckoutUrlSubmission.pending}
onClick={async () => {
const baseUrl = window.location.href
const checkoutUrl = await createCheckoutUrlAction(params.id, baseUrl, baseUrl)
if (checkoutUrl) {
window.location.href = checkoutUrl
}
}}
>
{createCheckoutUrlSubmission.pending ? "Loading..." : "Enable billing"}
</button>
}
>
<span data-slot="balance">
Current balance: <b>${balanceAmount() === "-0.00" ? "0.00" : balanceAmount()}</b>
</span>
</Show>
</span>
<Show when={userInfo()?.isAdmin}>
<span data-slot="billing-info">
<Show
when={billingInfo()?.reload}
fallback={
<button
data-color="primary"
data-size="sm"
disabled={createCheckoutUrlSubmission.pending}
onClick={async () => {
const baseUrl = window.location.href
const checkoutUrl = await createCheckoutUrlAction(params.id, baseUrl, baseUrl)
if (checkoutUrl) {
window.location.href = checkoutUrl
}
}}
>
{createCheckoutUrlSubmission.pending ? "Loading..." : "Enable billing"}
</button>
}
>
<span data-slot="balance">
Current balance: <b>${balanceAmount() === "-0.00" ? "0.00" : balanceAmount()}</b>
</span>
</Show>
</span>
</Show>
</p>
</section>

View file

@ -92,97 +92,12 @@
line-height: 1.4;
margin-top: calc(var(--space-1) * -1);
}
[data-slot="role-selector"] {
position: relative;
[data-slot="trigger"] {
display: flex;
align-items: center;
justify-content: space-between;
gap: var(--space-2);
width: 100%;
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);
line-height: 1.5;
cursor: pointer;
transition: all 0.15s ease;
&:hover {
border-color: var(--color-accent);
}
&:focus {
outline: none;
border-color: var(--color-accent);
box-shadow: 0 0 0 3px var(--color-accent-alpha);
}
[data-slot="chevron"] {
opacity: 0.6;
transition: transform 0.15s ease;
}
}
[data-slot="dropdown"] {
position: absolute;
top: 100%;
left: 0;
z-index: 10;
margin-top: var(--space-1);
padding: var(--space-1);
background-color: var(--color-bg);
border: 1px solid var(--color-border);
border-radius: var(--border-radius-sm);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
min-width: 280px;
width: max-content;
[data-slot="item"] {
display: block;
width: 100%;
padding: var(--space-2) var(--space-3);
border: none;
background-color: transparent;
color: var(--color-text);
font-size: var(--font-size-sm);
text-align: left;
cursor: pointer;
border-radius: var(--border-radius-sm);
transition: background-color 0.15s ease;
&:hover {
background-color: var(--color-bg-surface);
}
&[data-selected="true"] {
background-color: var(--color-accent-alpha);
}
div {
strong {
display: block;
color: var(--color-text);
margin-bottom: var(--space-1);
}
p {
font-size: var(--font-size-xs);
color: var(--color-text-muted);
margin: 0;
}
}
}
}
}
}
[data-slot="members-table"] {
overflow-x: auto;
padding-bottom: 200px;
margin-bottom: -200px;
}
[data-slot="members-table-element"] {
@ -224,125 +139,7 @@
&[data-slot="member-role"] {
font-family: var(--font-mono);
[data-slot="role-selector"] {
position: relative;
[data-slot="trigger"] {
display: flex;
align-items: center;
justify-content: space-between;
gap: var(--space-2);
width: 100%;
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);
line-height: 1.5;
cursor: pointer;
transition: all 0.15s ease;
font-family: var(--font-sans);
&:hover {
border-color: var(--color-accent);
}
&:focus {
outline: none;
border-color: var(--color-accent);
box-shadow: 0 0 0 3px var(--color-accent-alpha);
}
[data-slot="chevron"] {
opacity: 0.6;
transition: transform 0.15s ease;
}
}
[data-slot="dropdown"] {
position: absolute;
top: 100%;
left: 0;
z-index: 10;
margin-top: var(--space-1);
padding: var(--space-1);
background-color: var(--color-bg);
border: 1px solid var(--color-border);
border-radius: var(--border-radius-sm);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
min-width: 280px;
width: max-content;
[data-slot="item"] {
display: block;
width: 100%;
padding: var(--space-2) var(--space-3);
border: none;
background-color: transparent;
color: var(--color-text);
font-size: var(--font-size-sm);
text-align: left;
cursor: pointer;
border-radius: var(--border-radius-sm);
transition: background-color 0.15s ease;
&:hover {
background-color: var(--color-bg-surface);
}
&[data-selected="true"] {
background-color: var(--color-accent-alpha);
}
div {
strong {
display: block;
color: var(--color-text);
margin-bottom: var(--space-1);
}
p {
font-size: var(--font-size-xs);
color: var(--color-text-muted);
margin: 0;
}
}
}
}
}
button {
display: flex;
align-items: center;
gap: var(--space-2);
padding: var(--space-2) var(--space-3);
font-size: var(--font-size-sm);
font-weight: 400;
border: none;
background-color: transparent;
color: var(--color-text-muted);
font-family: var(--font-mono);
border-radius: var(--border-radius-sm);
cursor: pointer;
transition: all 0.15s ease;
text-transform: none;
&:hover:not(:disabled) {
background-color: var(--color-bg-surface);
color: var(--color-text);
}
&:disabled {
cursor: default;
color: var(--color-text);
}
span {
font-family: inherit;
}
}
text-transform: capitalize;
}
&[data-slot="member-usage"] {

View file

@ -1,12 +1,12 @@
import { json, query, action, useParams, createAsync, useSubmission } from "@solidjs/router"
import { createEffect, createSignal, For, Show, onCleanup } from "solid-js"
import { createEffect, For, Show } from "solid-js"
import { withActor } from "~/context/auth.withActor"
import { createStore } from "solid-js/store"
import styles from "./member-section.module.css"
import { UserRole } from "@opencode-ai/console-core/schema/user.sql.js"
import { Actor } from "@opencode-ai/console-core/actor.js"
import { User } from "@opencode-ai/console-core/user.js"
import { IconChevron } from "~/component/icon"
import { RoleDropdown } from "./role-dropdown"
const listMembers = query(async (workspaceID: string) => {
"use server"
@ -92,29 +92,15 @@ function MemberRow(props: { member: any; workspaceID: string; actorID: string; a
const [store, setStore] = createStore({
editing: false,
selectedRole: props.member.role as (typeof UserRole)[number],
showRoleDropdown: false,
limit: "",
})
let roleDropdownRef: HTMLDivElement | undefined
createEffect(() => {
if (!submission.pending && submission.result && !submission.result.error) {
setStore("editing", false)
}
})
createEffect(() => {
const handleClickOutside = (event: MouseEvent) => {
if (roleDropdownRef && !roleDropdownRef.contains(event.target as Node)) {
setStore("showRoleDropdown", false)
}
}
document.addEventListener("click", handleClickOutside)
onCleanup(() => document.removeEventListener("click", handleClickOutside))
})
function show() {
while (true) {
submission.clear()
@ -127,7 +113,6 @@ function MemberRow(props: { member: any; workspaceID: string; actorID: string; a
function hide() {
setStore("editing", false)
setStore("showRoleDropdown", false)
}
function getUsageDisplay() {
@ -145,17 +130,11 @@ function MemberRow(props: { member: any; workspaceID: string; actorID: string; a
month: "long",
timeZone: "UTC",
})
const usage = current === lastUsed ? (props.member.monthlyUsage ?? 0) : 0
return (usage / 100000000).toFixed(2)
return current === lastUsed ? (props.member.monthlyUsage ?? 0) : 0
})()
const limit = props.member.monthlyLimit ? `$${props.member.monthlyLimit}` : "no limit"
return `$${currentUsage} / ${limit}`
}
const roleLabels = {
admin: { title: "Admin", description: "Can manage models, members, and billing" },
member: { title: "Member", description: "Can only generate API keys for themselves" },
return `$${(currentUsage / 100000000).toFixed(2)} / ${limit}`
}
return (
@ -163,48 +142,11 @@ function MemberRow(props: { member: any; workspaceID: string; actorID: string; a
<td data-slot="member-email">{props.member.accountEmail ?? props.member.email}</td>
<td data-slot="member-role">
<Show when={store.editing && !isCurrentUser()} fallback={<span>{props.member.role}</span>}>
<div data-slot="role-selector" ref={roleDropdownRef}>
<button
data-slot="trigger"
type="button"
onClick={() => setStore("showRoleDropdown", !store.showRoleDropdown)}
>
<span>{roleLabels[store.selectedRole].title}</span>
<IconChevron data-slot="chevron" />
</button>
<Show when={store.showRoleDropdown}>
<div data-slot="dropdown">
<button
data-slot="item"
data-selected={store.selectedRole === "admin"}
type="button"
onClick={() => {
setStore("selectedRole", "admin")
setStore("showRoleDropdown", false)
}}
>
<div>
<strong>Admin</strong>
<p>{roleLabels.admin.description}</p>
</div>
</button>
<button
data-slot="item"
data-selected={store.selectedRole === "member"}
type="button"
onClick={() => {
setStore("selectedRole", "member")
setStore("showRoleDropdown", false)
}}
>
<div>
<strong>{roleLabels.member.title}</strong>
<p>{roleLabels.member.description}</p>
</div>
</button>
</div>
</Show>
</div>
<RoleDropdown
value={store.selectedRole}
options={roleOptions}
onChange={(value) => setStore("selectedRole", value as (typeof UserRole)[number])}
/>
</Show>
</td>
<td data-slot="member-usage">
@ -260,6 +202,11 @@ function MemberRow(props: { member: any; workspaceID: string; actorID: string; a
)
}
const roleOptions = [
{ value: "admin", description: "Can manage models, members, and billing" },
{ value: "member", description: "Can only generate API keys for themselves" },
]
export function MemberSection() {
const params = useParams()
const data = createAsync(() => listMembers(params.id))
@ -267,12 +214,10 @@ export function MemberSection() {
const [store, setStore] = createStore({
show: false,
selectedRole: "member" as (typeof UserRole)[number],
showRoleDropdown: false,
limit: "",
})
let input: HTMLInputElement
let roleDropdownRef: HTMLDivElement | undefined
createEffect(() => {
if (!submission.pending && submission.result && !submission.result.error) {
@ -280,17 +225,6 @@ export function MemberSection() {
}
})
createEffect(() => {
const handleClickOutside = (event: MouseEvent) => {
if (roleDropdownRef && !roleDropdownRef.contains(event.target as Node)) {
setStore("showRoleDropdown", false)
}
}
document.addEventListener("click", handleClickOutside)
onCleanup(() => document.removeEventListener("click", handleClickOutside))
})
function show() {
while (true) {
submission.clear()
@ -304,12 +238,6 @@ export function MemberSection() {
function hide() {
setStore("show", false)
setStore("showRoleDropdown", false)
}
const roleLabels = {
admin: { title: "Admin", description: "Can manage models, members, and billing" },
member: { title: "Member", description: "Can only generate API keys for themselves" },
}
return (
@ -340,48 +268,11 @@ export function MemberSection() {
</div>
<div data-slot="input-field">
<p>Role</p>
<div data-slot="role-selector" ref={roleDropdownRef}>
<button
data-slot="trigger"
type="button"
onClick={() => setStore("showRoleDropdown", !store.showRoleDropdown)}
>
<span>{roleLabels[store.selectedRole].title}</span>
<IconChevron data-slot="chevron" />
</button>
<Show when={store.showRoleDropdown}>
<div data-slot="dropdown">
<button
data-slot="item"
data-selected={store.selectedRole === "admin"}
type="button"
onClick={() => {
setStore("selectedRole", "admin")
setStore("showRoleDropdown", false)
}}
>
<div>
<strong>Admin</strong>
<p>{roleLabels.admin.description}</p>
</div>
</button>
<button
data-slot="item"
data-selected={store.selectedRole === "member"}
type="button"
onClick={() => {
setStore("selectedRole", "member")
setStore("showRoleDropdown", false)
}}
>
<div>
<strong>{roleLabels.member.title}</strong>
<p>{roleLabels.member.description}</p>
</div>
</button>
</div>
</Show>
</div>
<RoleDropdown
value={store.selectedRole}
options={roleOptions}
onChange={(value) => setStore("selectedRole", value as (typeof UserRole)[number])}
/>
</div>
<div data-slot="input-field">
<p>Monthly spending limit</p>

View file

@ -0,0 +1,72 @@
.role-dropdown {
[data-slot="trigger"] {
border: 1px solid var(--color-border);
background-color: var(--color-bg);
width: 100%;
text-transform: capitalize;
padding: var(--space-2) var(--space-3);
border-radius: var(--border-radius-sm);
color: var(--color-text);
font-size: var(--font-size-sm);
line-height: 1.5;
min-width: 0;
&:hover {
border-color: var(--color-accent);
background-color: var(--color-bg);
}
&:focus {
outline: none;
border-color: var(--color-accent);
box-shadow: 0 0 0 3px var(--color-accent-alpha);
}
}
[data-slot="chevron"] {
opacity: 0.6;
}
[data-slot="dropdown"] {
padding: var(--space-1);
min-width: 280px;
width: max-content;
}
[data-slot="role-item"] {
display: block;
width: 100%;
padding: var(--space-2) var(--space-3);
border: none;
background-color: transparent;
color: var(--color-text);
font-size: var(--font-size-sm);
text-align: left;
cursor: pointer;
border-radius: var(--border-radius-sm);
transition: background-color 0.15s ease;
&:hover {
background-color: var(--color-bg-surface);
}
&[data-selected="true"] {
background-color: var(--color-accent-alpha);
}
div {
strong {
display: block;
color: var(--color-text);
margin-bottom: var(--space-1);
text-transform: capitalize;
}
p {
font-size: var(--font-size-xs);
color: var(--color-text-muted);
margin: 0;
}
}
}
}

View file

@ -0,0 +1,43 @@
import { createSignal } from "solid-js"
import { Dropdown } from "~/component/dropdown"
import "./role-dropdown.css"
interface RoleOption {
value: string
description: string
}
interface RoleDropdownProps {
value: string
options: RoleOption[]
onChange: (value: string) => void
}
export function RoleDropdown(props: RoleDropdownProps) {
const [open, setOpen] = createSignal(false)
const handleSelect = (value: string) => {
props.onChange(value)
setOpen(false)
}
return (
<Dropdown trigger={props.value} open={open()} onOpenChange={setOpen} class="role-dropdown">
<>
{props.options.map((option) => (
<button
data-slot="role-item"
data-selected={props.value === option.value}
type="button"
onClick={() => handleSelect(option.value)}
>
<div>
<strong>{option.value}</strong>
<p>{option.description}</p>
</div>
</button>
))}
</>
</Dropdown>
)
}

View file

@ -10,9 +10,10 @@ const getModelsInfo = query(async (workspaceID: string) => {
"use server"
return withActor(async () => {
return {
all: Object.keys(ZenModel.list())
.filter((model) => !["claude-3-5-haiku", "glm-4.6", "qwen3-max"].includes(model))
.sort(([a], [b]) => a.localeCompare(b)),
all: Object.entries(ZenModel.list())
.filter(([id, _model]) => !["claude-3-5-haiku", "qwen3-max"].includes(id))
.sort(([_idA, modelA], [_idB, modelB]) => modelA.name.localeCompare(modelB.name))
.map(([id, model]) => ({ id, name: model.name })),
disabled: await Model.listDisabled(),
}
}, workspaceID)
@ -62,14 +63,14 @@ export function ModelSection() {
</thead>
<tbody>
<For each={modelsInfo()!.all}>
{(modelId) => {
const isEnabled = createMemo(() => !modelsInfo()!.disabled.includes(modelId))
{({ id, name }) => {
const isEnabled = createMemo(() => !modelsInfo()!.disabled.includes(id))
return (
<tr data-slot="model-row" data-disabled={!isEnabled()}>
<td data-slot="model-name">{modelId}</td>
<td data-slot="model-name">{name}</td>
<td data-slot="model-toggle">
<form action={updateModel} method="post">
<input type="hidden" name="model" value={modelId} />
<input type="hidden" name="model" value={id} />
<input type="hidden" name="workspaceID" value={params.id} />
<input type="hidden" name="enabled" value={isEnabled().toString()} />
<label data-slot="model-toggle-label">

View file

@ -28,7 +28,14 @@ export function NewUserSection() {
const usageList = usage()
return keysList?.length === 1 && (!usageList || usageList.length === 0)
})
const defaultKey = createMemo(() => keys()?.at(-1)?.key)
const defaultKey = createMemo(() => {
const key = keys()?.at(-1)?.key
if (!key) return undefined
return {
actual: key,
masked: key.slice(0, 8) + "*".repeat(key.length - 12) + key.slice(-4),
}
})
return (
<Show when={isNew()}>
@ -52,12 +59,12 @@ export function NewUserSection() {
<Show when={defaultKey()}>
<div data-slot="key-display">
<div data-slot="key-container">
<code data-slot="key-value">{defaultKey()}</code>
<code data-slot="key-value">{defaultKey()?.masked}</code>
<button
data-color="primary"
disabled={copiedKey()}
onClick={async () => {
await navigator.clipboard.writeText(defaultKey() ?? "")
await navigator.clipboard.writeText(defaultKey()?.actual ?? "")
setCopiedKey(true)
setTimeout(() => setCopiedKey(false), 2000)
}}

View file

@ -1,7 +1,7 @@
{
"$schema": "https://json.schemastore.org/package.json",
"name": "@opencode-ai/console-core",
"version": "0.14.7",
"version": "0.15.0",
"private": true,
"type": "module",
"dependencies": {
@ -35,6 +35,7 @@
"@types/bun": "1.3.0",
"@types/node": "24.7.1",
"drizzle-kit": "0.30.5",
"mysql2": "3.14.4"
"mysql2": "3.14.4",
"typescript": "catalog:"
}
}

View file

@ -18,6 +18,7 @@ export namespace Billing {
export const stripe = () =>
new Stripe(Resource.STRIPE_SECRET_KEY.value, {
apiVersion: "2025-03-31.basil",
httpClient: Stripe.createFetchHttpClient(),
})
export const get = async () => {

View file

@ -17,6 +17,7 @@ export namespace ZenModel {
})
export const ModelSchema = z.object({
name: z.string(),
cost: ModelCostSchema,
cost200K: ModelCostSchema.optional(),
allowAnonymous: z.boolean().optional(),

View file

@ -134,7 +134,7 @@ export namespace User {
const { InviteEmail } = await import("@opencode-ai/console-mail/InviteEmail.jsx")
await AWS.sendEmail({
to: email,
subject: `You've been invited to join the ${workspaceID} workspace on OpenCode Zen`,
subject: `You've been invited to join the ${emailInfo.workspaceName} workspace on OpenCode Console`,
body: render(
// @ts-ignore
InviteEmail({

View file

@ -1,6 +1,6 @@
{
"name": "@opencode-ai/console-function",
"version": "0.14.7",
"version": "0.15.0",
"$schema": "https://json.schemastore.org/package.json",
"private": true,
"type": "module",

View file

@ -35,7 +35,7 @@ export const InviteEmail = ({
assetsUrl = LOCAL_ASSETS_URL,
}: InviteEmailProps) => {
const subject = `You've been invited to join the ${workspaceName} workspace on OpenCode Console`
const messagePlain = `${inviter} invited you to join the ${workspaceName} workspace (${workspaceID}).`
const messagePlain = `${inviter} invited you to join the ${workspaceName} workspace.`
const url = `${CONSOLE_URL}workspace/${workspaceID}`
return (
<Html lang="en">
@ -67,7 +67,7 @@ export const InviteEmail = ({
<Link style={medium} href={url}>
<B>{workspaceName}</B>
</Link>{" "}
workspace ({workspaceID}) in the{" "}
workspace in the{" "}
<Link style={medium} href={`${CONSOLE_URL}zen`}>
OpenCode Console
</Link>

View file

@ -1,6 +1,6 @@
{
"name": "@opencode-ai/console-mail",
"version": "0.14.7",
"version": "0.15.0",
"dependencies": {
"@jsx-email/all": "2.2.3",
"@jsx-email/cli": "1.4.3",

View file

@ -1,6 +1,6 @@
{
"name": "@opencode-ai/console-scripts",
"version": "0.14.7",
"version": "0.15.0",
"$schema": "https://json.schemastore.org/package.json",
"private": true,
"type": "module",

View file

@ -1,6 +1,6 @@
{
"name": "@opencode-ai/desktop",
"version": "0.14.7",
"version": "0.15.0",
"description": "",
"type": "module",
"scripts": {

View file

@ -1,6 +1,6 @@
{
"name": "@opencode-ai/function",
"version": "0.14.7",
"version": "0.15.0",
"$schema": "https://json.schemastore.org/package.json",
"private": true,
"type": "module",

View file

@ -1,6 +1,6 @@
{
"$schema": "https://json.schemastore.org/package.json",
"version": "0.14.7",
"version": "0.15.0",
"name": "opencode",
"type": "module",
"private": true,

View file

@ -62,7 +62,6 @@ if (!snapshot) {
const macArm64Sha = await $`sha256sum ./dist/opencode-darwin-arm64.zip | cut -d' ' -f1`.text().then((x) => x.trim())
// arch
/*
const binaryPkgbuild = [
"# Maintainer: dax",
"# Maintainer: adam",
@ -132,23 +131,22 @@ if (!snapshot) {
["opencode-bin", binaryPkgbuild],
["opencode", sourcePkgbuild],
]) {
await $`rm -rf ./dist/aur-${pkg}`
while (true) {
for (let i = 0; i < 30; i++) {
try {
await $`rm -rf ./dist/aur-${pkg}`
await $`git clone ssh://aur@aur.archlinux.org/${pkg}.git ./dist/aur-${pkg}`
await $`cd ./dist/aur-${pkg} && git checkout master`
await Bun.file(`./dist/aur-${pkg}/PKGBUILD`).write(pkgbuild)
await $`cd ./dist/aur-${pkg} && makepkg --printsrcinfo > .SRCINFO`
await $`cd ./dist/aur-${pkg} && git add PKGBUILD .SRCINFO`
await $`cd ./dist/aur-${pkg} && git commit -m "Update to v${version}"`
await $`cd ./dist/aur-${pkg} && git push`
break
} catch (e) {
continue
}
}
await $`cd ./dist/aur-${pkg} && git checkout master`
await Bun.file(`./dist/aur-${pkg}/PKGBUILD`).write(pkgbuild)
await $`cd ./dist/aur-${pkg} && makepkg --printsrcinfo > .SRCINFO`
await $`cd ./dist/aur-${pkg} && git add PKGBUILD .SRCINFO`
await $`cd ./dist/aur-${pkg} && git commit -m "Update to v${version}"`
await $`cd ./dist/aur-${pkg} && git push`
}
*/
// Homebrew formula
const homebrewFormula = [

View file

@ -1,6 +1,5 @@
import z from "zod/v4"
import { exec } from "child_process"
import { spawn } from "child_process"
import { Tool } from "./tool"
import DESCRIPTION from "./bash.txt"
import { lazy } from "../util/lazy"
@ -148,9 +147,11 @@ export const BashTool = Tool.define("bash", {
}
*/
const process = exec(params.command, {
const process = spawn(params.command, {
shell: true,
cwd: Instance.directory,
signal: ctx.abort,
stdio: ["ignore", "pipe", "pipe"],
timeout,
})
process.stdin?.end()

View file

@ -1,7 +1,7 @@
{
"$schema": "https://json.schemastore.org/package.json",
"name": "@opencode-ai/plugin",
"version": "0.14.7",
"version": "0.15.0",
"type": "module",
"scripts": {
"typecheck": "tsc --noEmit",

View file

@ -1,7 +1,7 @@
{
"$schema": "https://json.schemastore.org/package.json",
"name": "@opencode-ai/sdk",
"version": "0.14.7",
"version": "0.15.0",
"type": "module",
"scripts": {
"typecheck": "tsc --noEmit",

View file

@ -1,7 +1,7 @@
{
"name": "@opencode-ai/web",
"type": "module",
"version": "0.14.7",
"version": "0.15.0",
"scripts": {
"dev": "astro dev",
"dev:remote": "VITE_API_URL=https://api.opencode.ai astro dev",

View file

@ -2,7 +2,7 @@
"name": "opencode",
"displayName": "opencode",
"description": "opencode for VS Code",
"version": "0.14.7",
"version": "0.15.0",
"publisher": "sst-dev",
"repository": {
"type": "git",