Merge branch 'dev' into opentui

This commit is contained in:
Dax Raad 2025-10-14 14:39:35 -04:00
commit 1ba238d149
28 changed files with 465 additions and 286 deletions

View file

@ -58,6 +58,7 @@ jobs:
./script/publish.ts
env:
OPENCODE_BUMP: ${{ inputs.bump }}
OPENCODE_TAG: latest
GITHUB_TOKEN: ${{ secrets.SST_GITHUB_TOKEN }}
AUR_KEY: ${{ secrets.AUR_KEY }}
NPM_CONFIG_TOKEN: ${{ secrets.NPM_TOKEN }}

View file

@ -30,7 +30,5 @@ jobs:
run: |
./packages/opencode/script/publish.ts
env:
OPENCODE_SNAPSHOT: true
OPENCODE_TAG: ${{ github.ref_name }}
GITHUB_TOKEN: ${{ secrets.SST_GITHUB_TOKEN }}
NPM_CONFIG_TOKEN: ${{ secrets.NPM_TOKEN }}

View file

@ -4,6 +4,7 @@
"": {
"name": "opencode",
"dependencies": {
"@opencode-ai/script": "workspace:*",
"@opencode-ai/sdk": "workspace:*",
},
"devDependencies": {
@ -114,7 +115,7 @@
},
"packages/css": {
"name": "@opencode-ai/css",
"version": "0.15.0",
"version": "0.15.2",
},
"packages/desktop": {
"name": "@opencode-ai/desktop",
@ -220,6 +221,7 @@
"@ai-sdk/google-vertex": "3.0.16",
"@babel/core": "7.28.4",
"@octokit/webhooks-types": "7.6.1",
"@opencode-ai/script": "workspace:*",
"@parcel/watcher-win32-x64": "2.5.1",
"@standard-schema/spec": "1.0.0",
"@tsconfig/bun": "catalog:",
@ -246,6 +248,12 @@
"typescript": "catalog:",
},
},
"packages/script": {
"name": "@opencode-ai/script",
"devDependencies": {
"@types/bun": "catalog:",
},
},
"packages/sdk/js": {
"name": "@opencode-ai/sdk",
"version": "0.15.2",
@ -270,7 +278,7 @@
},
"packages/ui": {
"name": "@opencode-ai/ui",
"version": "0.15.0",
"version": "0.15.2",
"dependencies": {
"@kobalte/core": "catalog:",
"@opencode-ai/css": "workspace:*",
@ -902,6 +910,8 @@
"@opencode-ai/plugin": ["@opencode-ai/plugin@workspace:packages/plugin"],
"@opencode-ai/script": ["@opencode-ai/script@workspace:packages/script"],
"@opencode-ai/sdk": ["@opencode-ai/sdk@workspace:packages/sdk/js"],
"@opencode-ai/slack": ["@opencode-ai/slack@workspace:packages/slack"],

View file

@ -51,7 +51,8 @@
"turbo": "2.5.6"
},
"dependencies": {
"@opencode-ai/sdk": "workspace:*"
"@opencode-ai/sdk": "workspace:*",
"@opencode-ai/script": "workspace:*"
},
"repository": {
"type": "git",

View file

@ -1,4 +1,3 @@
import { Show } from "solid-js"
import { query, createAsync, RouteSectionProps, useParams, A } from "@solidjs/router"
import "./workspace.css"
import { IconWorkspaceLogo } from "../component/icon"
@ -7,7 +6,6 @@ import { UserMenu } from "./user-menu"
import { withActor } from "~/context/auth.withActor"
import { User } from "@opencode-ai/console-core/user.js"
import { Actor } from "@opencode-ai/console-core/actor.js"
import { querySessionInfo } from "./workspace/common"
const getUserEmail = query(async (workspaceID: string) => {
"use server"
@ -21,7 +19,6 @@ const getUserEmail = query(async (workspaceID: string) => {
export default function WorkspaceLayout(props: RouteSectionProps) {
const params = useParams()
const userEmail = createAsync(() => getUserEmail(params.id))
const sessionInfo = createAsync(() => querySessionInfo(params.id))
return (
<main data-page="workspace">
<header data-component="workspace-header">
@ -29,9 +26,7 @@ export default function WorkspaceLayout(props: RouteSectionProps) {
<A href="/" data-component="site-title">
<IconWorkspaceLogo />
</A>
<Show when={sessionInfo()?.isBeta}>
<WorkspacePicker />
</Show>
<WorkspacePicker />
</div>
<div data-slot="header-actions">
<UserMenu email={userEmail()} />

View file

@ -1,3 +1,4 @@
import type { KVNamespace } from "@cloudflare/workers-types"
import { z } from "zod"
import { issuer } from "@openauthjs/openauth"
import type { Theme } from "@openauthjs/openauth/ui/theme"
@ -94,6 +95,7 @@ export default {
// }),
},
storage: CloudflareStorage({
// @ts-ignore
namespace: env.AuthStorage,
}),
subjects,

View file

@ -1,13 +1,12 @@
{
"name": "@opencode-ai/css",
"version": "0.15.0",
"version": "0.15.2",
"type": "module",
"exports": {
".": "./src/index.css",
"./*": "./src/*"
},
"scripts": {
"build": "bun run build.ts",
"dev": "bun run dev.ts"
}
}

View file

@ -1,20 +1,4 @@
[data-component="select"] {
display: inline-flex;
align-items: center;
gap: calc(var(--spacing) * 2);
border-style: solid;
border-width: 1px;
border-radius: var(--radius-md);
border-color: var(--color-smoke-4);
font-family: var(--font-sans);
font-size: var(--text-base);
line-height: var(--text-base--line-height);
font-weight: var(--font-weight-normal);
cursor: pointer;
transition: all 0.2s ease-in-out;
text-decoration: none;
user-select: none;
&:disabled {
opacity: 0.5;
cursor: not-allowed;
@ -28,38 +12,35 @@
[data-slot="section"] {
font-size: var(--text-xs);
line-height: var(--text-xs--line-height);
font-weight: var(--font-weight-normal);
font-weight: var(--font-weight-light);
text-transform: uppercase;
color: var(--text-default-text-weak);
opacity: 0.6;
margin-top: calc(var(--spacing) * 3);
margin-left: calc(var(--spacing) * 2);
&:first-child {
margin-top: calc(var(--spacing) * 0);
margin-top: 0;
}
}
[data-slot="item"] {
/* "relative flex cursor-pointer select-none items-center": true, */
/* "rounded-sm px-2 py-0.5 text-xs outline-none text-text": true, */
/* "transition-colors data-[disabled]:pointer-events-none": true, */
/* "data-[highlighted]:bg-background-element data-[disabled]:opacity-50": true, */
position: relative;
display: flex;
align-items: center;
justify-content: center;
gap: calc(var(--spacing) * 2);
border-style: solid;
border-width: 1px;
border-radius: var(--radius-md);
font-family: var(--font-sans);
font-size: var(--text-base);
line-height: var(--text-base--line-height);
font-weight: var(--font-weight-normal);
padding: calc(var(--spacing) * 2) calc(var(--spacing) * 2);
border-radius: var(--radius-sm);
font-size: var(--text-xs);
line-height: var(--text-xs--line-height);
color: var(--text-default-text);
cursor: pointer;
transition: all 0.2s ease-in-out;
text-decoration: none;
transition:
background-color 0.2s ease-in-out,
color 0.2s ease-in-out;
outline: none;
user-select: none;
&[data-highlighted] {
background-color: var(--color-background-element);
background-color: var(--surface-default-surface);
}
&[data-disabled] {
@ -67,16 +48,7 @@
opacity: 0.5;
}
/* [data-slot="item-label"] { */
/* font-size: var(--text-xs); */
/* line-height: var(--text-xs--line-height); */
/* font-weight: var(--font-weight-normal); */
/* } */
[data-slot="item-indicator"] {
/* display: flex; */
/* align-items: center; */
/* gap: calc(var(--spacing) * 1); */
margin-left: auto;
}
}
@ -88,46 +60,61 @@
white-space: nowrap;
}
[data-slot="icon"] {
/* "group size-fit shrink-0 text-text-muted transition-transform duration-100": true, */
width: fit-content;
height: fit-content;
flex-shrink: 0;
color: var(--text-default-text-weak);
transition: transform 0.1s ease-in-out;
}
}
}
[data-component="select-content"] {
/* "min-w-32 overflow-hidden rounded-md border border-border-subtle/40": true, */
/* "bg-background-panel p-1 shadow-md z-50": true, */
/* "data-[closed]:animate-out data-[closed]:fade-out-0 data-[closed]:zoom-out-95": true, */
/* "data-[expanded]:animate-in data-[expanded]:fade-in-0 data-[expanded]:zoom-in-95": true, */
min-width: 8rem;
overflow: hidden;
border-radius: var(--radius-md);
border-width: 1px;
border-style: solid;
border-color: var(--color-smoke-4);
background-color: var(--color-smoke-2);
border-color: var(--border-default-border-weak);
background-color: var(--surface-raised-surface-raised);
padding: calc(var(--spacing) * 1);
box-shadow: var(--shadow-md);
z-index: 50;
/* &[data-closed] { */
/* animation: fade-out-0 0.2s ease-out; */
/* animation-fill-mode: forwards; */
/* animation-delay: 0.2s; */
/* opacity: 0; */
/* } */
/* &[data-expanded] { */
/* animation: fade-in-0 0.2s ease-out; */
/* animation-fill-mode: forwards; */
/* animation-delay: 0.2s; */
/* opacity: 1; */
/* } */
&[data-closed] {
animation: select-close 0.15s ease-out;
}
&[data-expanded] {
animation: select-open 0.15s ease-out;
}
[data-slot="list"] {
/* overflow-y-auto max-h-48 whitespace-nowrap overflow-x-hidden */
overflow-y: auto;
max-height: 12rem;
white-space: nowrap;
overflow-x: hidden;
}
}
@keyframes select-open {
from {
opacity: 0;
transform: scale(0.95);
}
to {
opacity: 1;
transform: scale(1);
}
}
@keyframes select-close {
from {
opacity: 1;
transform: scale(1);
}
to {
opacity: 0;
transform: scale(0.95);
}
}

View file

@ -0,0 +1,98 @@
[data-component="tabs"] {
display: flex;
flex-direction: column;
height: 100%;
& [data-slot="list"] {
position: relative;
display: flex;
align-items: center;
background-color: var(--surface-default-surface);
overflow-x: auto;
/* Hide scrollbar */
scrollbar-width: none;
-ms-overflow-style: none;
&::-webkit-scrollbar {
display: none;
}
/* Divider between tabs */
& > [data-slot="trigger"]:not(:first-child) {
border-left: 1px solid var(--border-default-border-weak);
}
/* After element to fill remaining space */
&::after {
content: "";
display: block;
flex-grow: 1;
height: calc(var(--spacing) * 8);
border-left: 1px solid var(--border-default-border-weak);
border-bottom: 1px solid var(--border-default-border-weak);
}
&:empty::after {
border-left: none;
}
}
& [data-slot="trigger"] {
position: relative;
padding: 0 calc(var(--spacing) * 3);
height: calc(var(--spacing) * 8);
display: flex;
align-items: center;
font-size: var(--text-sm);
font-weight: var(--font-weight-medium);
color: var(--text-default-text-weak);
cursor: pointer;
white-space: nowrap;
flex-shrink: 0;
border-bottom: 1px solid var(--border-default-border-weak);
background-color: transparent;
transition:
background-color 0.15s ease,
color 0.15s ease;
&:disabled {
pointer-events: none;
opacity: 0.5;
}
&:focus-visible {
outline: none;
box-shadow: 0 0 0 2px var(--border-default-border-focus);
}
&[data-selected] {
color: var(--text-default-text);
background-color: var(--surface-panel-surface);
border-bottom-color: transparent;
}
&:hover:not(:disabled):not([data-selected]) {
color: var(--text-default-text);
}
}
& [data-slot="content"] {
background-color: var(--surface-panel-surface);
overflow-y: auto;
flex: 1;
/* Hide scrollbar */
scrollbar-width: none;
-ms-overflow-style: none;
&::-webkit-scrollbar {
display: none;
}
&:focus-visible {
outline: none;
box-shadow: 0 0 0 2px var(--border-default-border-focus);
}
}
}

View file

@ -8,5 +8,6 @@
@import "./components/button.css" layer(components);
@import "./components/icon.css" layer(components);
@import "./components/select.css" layer(components);
@import "./components/tabs.css" layer(components);
@import "./utilities.css" layer(utilities);

View file

@ -213,11 +213,22 @@ export default function Page() {
})
}
const plus = (
<IconButton
class="text-text-muted/60 peer-data-[selected]/tab:opacity-100 peer-data-[selected]/tab:text-text peer-data-[selected]/tab:hover:bg-border-subtle hover:opacity-100 peer-hover/tab:opacity-100"
size="xs"
variant="secondary"
onClick={() => setStore("fileSelectOpen", true)}
>
<Icon name="plus" size={12} />
</IconButton>
)
return (
<div class="relative h-screen flex flex-col">
<header class="h-10 shrink-0 bg-background-panel"></header>
<main class="h-[calc(100vh-2.5rem)] flex">
<div class="shrink-0 w-64">
<header class="h-12 shrink-0"></header>
<main class="h-[calc(100vh-3rem)] flex">
<div class="hidden shrink-0 w-64">
<SessionList />
</div>
<div class="grow w-full min-w-0 overflow-y-auto flex justify-center">
@ -256,92 +267,15 @@ export default function Page() {
<div class="hidden grow min-w-0">
<EditorPane onFileClick={handleFileClick} />
</div>
<div class="absolute bottom-4 right-4 border border-border-subtle/60 p-2 rounded-xl bg-background w-xl flex flex-col gap-2 z-50">
<div class="flex items-center gap-2">
<Select
options={sync.data.session}
current={local.session.active()}
placeholder="New Session"
value={(x) => x.id}
label={(x) => x.title}
onSelect={(s) => local.session.setActive(s?.id)}
class="bg-transparent! max-w-48 pl-0! text-text-muted!"
/>
<Show when={local.session.active()}>
<>
<div>/</div>
<Select
options={sync.data.message[local.session.active()!.id]?.filter((m) => m.role === "user") ?? []}
label={(m) => sync.data.part[m.id].find((p) => p.type === "text")!.text}
class="bg-transparent! max-w-48 pl-0! text-text-muted!"
/>
</>
</Show>
</div>
<div class="h-72 text-xs overflow-x-scroll no-scrollbar w-full min-w-0">
<Tabs
class="relative grow w-full flex flex-col gap-1 h-full"
value={local.context.activeFile()?.path}
onChange={local.context.setActiveFile}
>
<div class="sticky top-0 shrink-0 flex items-center gap-1">
<IconButton
class="text-text-muted/60 peer-data-[selected]/tab:opacity-100 peer-data-[selected]/tab:text-text peer-data-[selected]/tab:hover:bg-border-subtle hover:opacity-100 peer-hover/tab:opacity-100"
size="xs"
variant="secondary"
onClick={() => setStore("fileSelectOpen", true)}
>
<Icon name="plus" size={12} />
</IconButton>
<Tabs.List class="grow after:hidden! h-full divide-none! gap-1">
<For each={local.context.files()}>
{(file) => (
<KobalteTabs.Trigger
value={file.path}
class="h-full"
// onClick={() => props.onTabClick(props.file)}
>
<div class="flex items-center gap-x-1 rounded-md bg-background-panel px-2 h-full">
<FileIcon node={file} class="shrink-0 size-3!" />
<span class="text-xs text-text whitespace-nowrap">{getFilename(file.path)}</span>
</div>
</KobalteTabs.Trigger>
)}
</For>
</Tabs.List>
</div>
<For each={local.context.files()}>
{(file) => (
<Tabs.Content value={file.path} class="grow h-full pt-1 select-text rounded-md">
<Code path={file.path} code={file.content?.content ?? ""} />
</Tabs.Content>
)}
</For>
</Tabs>
</div>
<div class="absolute bottom-4 inset-x-0 p-2 flex flex-col justify-center items-center gap-2 z-50">
<PromptForm
class="w-xl"
onSubmit={handlePromptSubmit}
onOpenModelSelect={() => setStore("modelSelectOpen", true)}
onInputRefChange={(element: HTMLTextAreaElement | undefined) => {
inputRef = element ?? undefined
}}
/>
<div class="hidden relative flex-1 min-h-0 overflow-y-auto overflow-x-hidden">
<Show when={local.session.active()}>
{(activeSession) => (
<div class="relative">
<div class="sticky top-0 bg-background z-50 px-2 h-8 border-b border-border-subtle/30">
<div class="h-full flex items-center gap-2">
<h2 class="text-sm font-medium text-text truncate">
{activeSession().title || "Untitled Session"}
</h2>
</div>
</div>
<SessionTimeline session={activeSession().id} />
</div>
)}
</Show>
</div>
</div>
</main>
<Show when={store.modelSelectOpen}>

View file

@ -31,8 +31,13 @@
"@types/yargs": "17.0.33",
"typescript": "catalog:",
"vscode-languageserver-types": "3.17.5",
<<<<<<< HEAD
"why-is-node-running": "3.2.2",
"zod-to-json-schema": "3.24.5"
=======
"zod-to-json-schema": "3.24.5",
"@opencode-ai/script": "workspace:*"
>>>>>>> dev
},
"dependencies": {
"@clack/prompts": "1.0.0-alpha.1",

View file

@ -8,6 +8,7 @@ const dir = new URL("..", import.meta.url).pathname
process.chdir(dir)
import pkg from "../package.json"
import { Script } from "@opencode-ai/script"
const singleFlag = process.argv.includes("--single")
@ -28,7 +29,6 @@ const targets = singleFlag
await $`rm -rf dist`
const binaries: Record<string, string> = {}
const version = process.env["OPENCODE_VERSION"] ?? "dev"
for (const [os, arch] of targets) {
console.log(`building ${os}-${arch}`)
const name = `${pkg.name}-${os}-${arch}`
@ -51,7 +51,7 @@ for (const [os, arch] of targets) {
compile: {
target: `bun-${os}-${arch}` as any,
outfile: `dist/${name}/bin/opencode`,
execArgv: [`--user-agent=opencode/${version}`, `--env-file=""`, `--`],
execArgv: [`--user-agent=opencode/${Script.version}`, `--env-file=""`, `--`],
windows: {},
},
entrypoints: [
@ -60,7 +60,7 @@ for (const [os, arch] of targets) {
"./src/cli/cmd/tui/worker.ts",
],
define: {
OPENCODE_VERSION: `'${version}'`,
OPENCODE_VERSION: `'${Script.version}'`,
OTUI_TREE_SITTER_WORKER_PATH: "/$bunfs/root/../../node_modules/@opentui/core/parser.worker.js",
},
})
@ -70,7 +70,7 @@ for (const [os, arch] of targets) {
JSON.stringify(
{
name,
version,
version: Script.version,
os: [os === "windows" ? "win32" : os],
cpu: [arch],
},
@ -78,7 +78,7 @@ for (const [os, arch] of targets) {
2,
),
)
binaries[name] = version
binaries[name] = Script.version
}
export { binaries }

View file

@ -1,20 +1,10 @@
#!/usr/bin/env bun
import { $ } from "bun"
import pkg from "../package.json"
import { Script } from "@opencode-ai/script"
const dir = new URL("..", import.meta.url).pathname
process.chdir(dir)
import { $ } from "bun"
import pkg from "../package.json"
const snapshot = process.env["OPENCODE_SNAPSHOT"] === "true"
let version = process.env["OPENCODE_VERSION"]
const tag = process.env["OPENCODE_TAG"] ?? (snapshot ? "snapshot" : "latest")
if (!version && snapshot) {
version = `0.0.0-${tag}-${new Date().toISOString().slice(0, 16).replace(/[-:T]/g, "")}`
process.env["OPENCODE_VERSION"] = version
}
if (!version) throw new Error("OPENCODE_VERSION is required")
console.log(`publishing ${version}`)
const { binaries } = await import("./build.ts")
{
@ -38,7 +28,7 @@ await Bun.file(`./dist/${pkg.name}/package.json`).write(
preinstall: "node ./preinstall.mjs",
postinstall: "node ./postinstall.mjs",
},
version,
version: Script.version,
optionalDependencies: binaries,
},
null,
@ -46,11 +36,11 @@ await Bun.file(`./dist/${pkg.name}/package.json`).write(
),
)
for (const [name] of Object.entries(binaries)) {
await $`cd dist/${name} && chmod 777 -R . && bun publish --access public --tag ${tag}`
await $`cd dist/${name} && chmod 777 -R . && bun publish --access public --tag ${Script.tag}`
}
await $`cd ./dist/${pkg.name} && bun publish --access public --tag ${tag}`
await $`cd ./dist/${pkg.name} && bun publish --access public --tag ${Script.tag}`
if (!snapshot) {
if (!Script.preview) {
for (const key of Object.keys(binaries)) {
await $`cd dist/${key}/bin && zip -r ../../${key}.zip *`
}
@ -67,7 +57,7 @@ if (!snapshot) {
"# Maintainer: adam",
"",
"pkgname='opencode-bin'",
`pkgver=${version.split("-")[0]}`,
`pkgver=${Script.version.split("-")[0]}`,
"options=('!debug' '!strip')",
"pkgrel=1",
"pkgdesc='The AI coding agent built for the terminal.'",
@ -78,10 +68,10 @@ if (!snapshot) {
"conflicts=('opencode')",
"depends=('fzf' 'ripgrep')",
"",
`source_aarch64=("\${pkgname}_\${pkgver}_aarch64.zip::https://github.com/sst/opencode/releases/download/v${version}/opencode-linux-arm64.zip")`,
`source_aarch64=("\${pkgname}_\${pkgver}_aarch64.zip::https://github.com/sst/opencode/releases/download/v${Script.version}/opencode-linux-arm64.zip")`,
`sha256sums_aarch64=('${arm64Sha}')`,
"",
`source_x86_64=("\${pkgname}_\${pkgver}_x86_64.zip::https://github.com/sst/opencode/releases/download/v${version}/opencode-linux-x64.zip")`,
`source_x86_64=("\${pkgname}_\${pkgver}_x86_64.zip::https://github.com/sst/opencode/releases/download/v${Script.version}/opencode-linux-x64.zip")`,
`sha256sums_x86_64=('${x64Sha}')`,
"",
"package() {",
@ -96,7 +86,7 @@ if (!snapshot) {
"# Maintainer: adam",
"",
"pkgname='opencode'",
`pkgver=${version.split("-")[0]}`,
`pkgver=${Script.version.split("-")[0]}`,
"options=('!debug' '!strip')",
"pkgrel=1",
"pkgdesc='The AI coding agent built for the terminal.'",
@ -108,7 +98,7 @@ if (!snapshot) {
"depends=('fzf' 'ripgrep')",
"makedepends=('git' 'bun-bin' 'go')",
"",
`source=("opencode-\${pkgver}.tar.gz::https://github.com/sst/opencode/archive/v${version}.tar.gz")`,
`source=("opencode-\${pkgver}.tar.gz::https://github.com/sst/opencode/archive/v${Script.version}.tar.gz")`,
`sha256sums=('SKIP')`,
"",
"build() {",
@ -139,7 +129,7 @@ if (!snapshot) {
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 commit -m "Update to v${Script.version}"`
await $`cd ./dist/aur-${pkg} && git push`
break
} catch (e) {
@ -157,11 +147,11 @@ if (!snapshot) {
"class Opencode < Formula",
` desc "The AI coding agent built for the terminal."`,
` homepage "https://github.com/sst/opencode"`,
` version "${version.split("-")[0]}"`,
` version "${Script.version.split("-")[0]}"`,
"",
" on_macos do",
" if Hardware::CPU.intel?",
` url "https://github.com/sst/opencode/releases/download/v${version}/opencode-darwin-x64.zip"`,
` url "https://github.com/sst/opencode/releases/download/v${Script.version}/opencode-darwin-x64.zip"`,
` sha256 "${macX64Sha}"`,
"",
" def install",
@ -169,7 +159,7 @@ if (!snapshot) {
" end",
" end",
" if Hardware::CPU.arm?",
` url "https://github.com/sst/opencode/releases/download/v${version}/opencode-darwin-arm64.zip"`,
` url "https://github.com/sst/opencode/releases/download/v${Script.version}/opencode-darwin-arm64.zip"`,
` sha256 "${macArm64Sha}"`,
"",
" def install",
@ -180,14 +170,14 @@ if (!snapshot) {
"",
" on_linux do",
" if Hardware::CPU.intel? and Hardware::CPU.is_64_bit?",
` url "https://github.com/sst/opencode/releases/download/v${version}/opencode-linux-x64.zip"`,
` url "https://github.com/sst/opencode/releases/download/v${Script.version}/opencode-linux-x64.zip"`,
` sha256 "${x64Sha}"`,
" def install",
' bin.install "opencode"',
" end",
" end",
" if Hardware::CPU.arm? and Hardware::CPU.is_64_bit?",
` url "https://github.com/sst/opencode/releases/download/v${version}/opencode-linux-arm64.zip"`,
` url "https://github.com/sst/opencode/releases/download/v${Script.version}/opencode-linux-arm64.zip"`,
` sha256 "${arm64Sha}"`,
" def install",
' bin.install "opencode"',
@ -203,6 +193,6 @@ if (!snapshot) {
await $`git clone https://${process.env["GITHUB_TOKEN"]}@github.com/sst/homebrew-tap.git ./dist/homebrew-tap`
await Bun.file("./dist/homebrew-tap/opencode.rb").write(homebrewFormula)
await $`cd ./dist/homebrew-tap && git add opencode.rb`
await $`cd ./dist/homebrew-tap && git commit -m "Update to v${version}"`
await $`cd ./dist/homebrew-tap && git commit -m "Update to v${Script.version}"`
await $`cd ./dist/homebrew-tap && git push`
}

View file

@ -27,9 +27,19 @@ export const UpgradeCommand = {
const detectedMethod = await Installation.method()
const method = (args.method as Installation.Method) ?? detectedMethod
if (method === "unknown") {
prompts.log.error(`opencode is installed to ${process.execPath} and seems to be managed by a package manager`)
prompts.outro("Done")
return
prompts.log.error(`opencode is installed to ${process.execPath} and may be managed by a package manager`)
const install = await prompts.select({
message: "Install anyways?",
options: [
{ label: "Yes", value: true },
{ label: "No", value: false },
],
initialValue: false,
})
if (!install) {
prompts.outro("Done")
return
}
}
prompts.log.info("Using method: " + method)
const target = args.target ? args.target.replace(/^v/, "") : await Installation.latest()

View file

@ -50,6 +50,7 @@ export namespace Installation {
export async function method() {
if (process.execPath.includes(path.join(".opencode", "bin"))) return "curl"
if (process.execPath.includes(path.join(".local", "bin"))) return "curl"
const exec = process.execPath.toLowerCase()
const checks = [

View file

@ -0,0 +1,10 @@
{
"$schema": "https://json.schemastore.org/package",
"name": "@opencode-ai/script",
"devDependencies": {
"@types/bun": "catalog:"
},
"exports": {
".": "./src/index.ts"
}
}

View file

@ -0,0 +1,35 @@
import { $ } from "bun"
if (process.versions.bun !== "1.3.0") {
throw new Error("This script requires bun@1.3.0")
}
const TAG = process.env["OPENCODE_TAG"] ?? (await $`git branch --show-current`.text().then((x) => x.trim()))
const IS_PREVIEW = TAG !== "latest"
const VERSION = await (async () => {
if (IS_PREVIEW) return `0.0.0-${new Date().toISOString().slice(0, 16).replace(/[-:T]/g, "")}`
const version = await fetch("https://registry.npmjs.org/opencode-ai/latest")
.then((res) => {
if (!res.ok) throw new Error(res.statusText)
return res.json()
})
.then((data: any) => data.version)
const [major, minor, patch] = version.split(".").map((x: string) => Number(x) || 0)
const t = process.env["OPENCODE_BUMP"]?.toLowerCase()
if (t === "major") return `${major + 1}.0.0`
if (t === "minor") return `${major}.${minor + 1}.0`
return `${major}.${minor}.${patch + 1}`
})()
export const Script = {
get tag() {
return TAG
},
get version() {
return VERSION
},
get preview() {
return IS_PREVIEW
},
}
console.log(`opencode script`, JSON.stringify(Script, null, 2))

View file

@ -0,0 +1,8 @@
{
"$schema": "https://json.schemastore.org/tsconfig",
"extends": "@tsconfig/bun/tsconfig.json",
"compilerOptions": {
"lib": ["ESNext", "DOM", "DOM.Iterable"],
"noUncheckedIndexedAccess": false
}
}

View file

@ -1,5 +1,5 @@
<!doctype html>
<html lang="en" class="light">
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />

View file

@ -1,6 +1,6 @@
{
"name": "@opencode-ai/ui",
"version": "0.15.0",
"version": "0.15.2",
"type": "module",
"exports": {
".": "./src/components/index.ts",

View file

@ -1,66 +1,100 @@
import type { Component } from "solid-js"
import { Button } from "./components/button"
import { Select } from "./components"
import { Button, Select, Tabs } from "./components"
import "@opencode-ai/css"
import "./index.css"
const App: Component = () => {
const Content = (props: { dark?: boolean }) => (
<div class={`${props.dark ? "dark" : ""}`}>
<h3>Buttons</h3>
<section>
<Button variant="primary" size="normal">
Normal Primary
</Button>
<Button variant="secondary" size="normal">
Normal Secondary
</Button>
<Button variant="ghost" size="normal">
Normal Ghost
</Button>
<Button variant="primary" size="large">
Large Primary
</Button>
<Button variant="secondary" size="large">
Large Secondary
</Button>
<Button variant="ghost" size="large">
Large Ghost
</Button>
</section>
<h3>Select</h3>
<section>
<Select
// we have to pass dark bc of the portal,
// normally wouldn't be needed bc root element
// would have theme class
class={props.dark ? "dark" : ""}
variant="primary"
options={["Option 1", "Option 2", "Option 3"]}
placeholder="Select Primary"
/>
<Select
variant="secondary"
class={props.dark ? "dark" : ""}
options={["Option 1", "Option 2", "Option 3"]}
placeholder="Select Secondary"
/>
<Select
variant="ghost"
class={props.dark ? "dark" : ""}
options={["Option 1", "Option 2", "Option 3"]}
placeholder="Select Ghost"
/>
</section>
<h3>Tabs</h3>
<section>
<Tabs defaultValue="tab1" style={{ width: "100%" }}>
<Tabs.List>
<Tabs.Trigger value="tab1">Tab 1</Tabs.Trigger>
<Tabs.Trigger value="tab2">Tab 2</Tabs.Trigger>
<Tabs.Trigger value="tab3">Tab 3</Tabs.Trigger>
<Tabs.Trigger value="tab4" disabled>
Disabled Tab
</Tabs.Trigger>
</Tabs.List>
<Tabs.Content value="tab1">
<div style={{ padding: "16px" }}>
<h4>Tab 1 Content</h4>
<p>This is the content for the first tab.</p>
</div>
</Tabs.Content>
<Tabs.Content value="tab2">
<div style={{ padding: "16px" }}>
<h4>Tab 2 Content</h4>
<p>This is the content for the second tab.</p>
</div>
</Tabs.Content>
<Tabs.Content value="tab3">
<div style={{ padding: "16px" }}>
<h4>Tab 3 Content</h4>
<p>This is the content for the third tab.</p>
</div>
</Tabs.Content>
<Tabs.Content value="tab4">
<div style={{ padding: "16px" }}>
<h4>Tab 4 Content</h4>
<p>This tab should be disabled.</p>
</div>
</Tabs.Content>
</Tabs>
</section>
</div>
)
return (
<main>
<div class="light">
<h3>Buttons</h3>
<section>
<Button variant="primary" size="normal">
Normal Primary
</Button>
<Button variant="secondary" size="normal">
Normal Secondary
</Button>
<Button variant="ghost" size="normal">
Normal Ghost
</Button>
<Button variant="primary" size="large">
Large Primary
</Button>
<Button variant="secondary" size="large">
Large Secondary
</Button>
<Button variant="ghost" size="large">
Large Ghost
</Button>
</section>
<h3>Select</h3>
<section>
<Select options={["a", "b", "c"]} onSelect={(x) => console.log(x)} placeholder="Select" />
</section>
</div>
<div class="dark">
<h3>Buttons</h3>
<section>
<Button variant="primary" size="normal">
Normal Primary
</Button>
<Button variant="secondary" size="normal">
Normal Secondary
</Button>
<Button variant="ghost" size="normal">
Normal Ghost
</Button>
<Button variant="primary" size="large">
Large Primary
</Button>
<Button variant="secondary" size="large">
Large Secondary
</Button>
<Button variant="ghost" size="large">
Large Ghost
</Button>
</section>
<h3>Select</h3>
<section>
<Select options={["a", "b", "c"]} onSelect={(x) => console.log(x)} placeholder="Select" />
</section>
</div>
<Content />
<Content dark />
</main>
)
}

View file

@ -1,3 +1,4 @@
export * from "./button"
export * from "./icon"
export * from "./select"
export * from "./tabs"

View file

@ -79,11 +79,17 @@ export function Select<T>(props: SelectProps<T> & ButtonProps) {
}}
</Kobalte.Value>
<Kobalte.Icon data-slot="icon">
<Icon name="chevron-down" size={16} class="-my-2 group-data-[expanded]:rotate-180" />
<Icon name="chevron-down" size={16} />
</Kobalte.Icon>
</Kobalte.Trigger>
<Kobalte.Portal>
<Kobalte.Content data-component="select-content">
<Kobalte.Content
classList={{
...(props.classList ?? {}),
[props.class ?? ""]: !!props.class,
}}
data-component="select-content"
>
<Kobalte.Listbox data-slot="list" />
</Kobalte.Content>
</Kobalte.Portal>

View file

@ -0,0 +1,74 @@
import { Tabs as Kobalte } from "@kobalte/core/tabs"
import { splitProps } from "solid-js"
import type { ComponentProps, ParentProps } from "solid-js"
export interface TabsProps extends ComponentProps<typeof Kobalte> {}
export interface TabsListProps extends ComponentProps<typeof Kobalte.List> {}
export interface TabsTriggerProps extends ComponentProps<typeof Kobalte.Trigger> {}
export interface TabsContentProps extends ComponentProps<typeof Kobalte.Content> {}
function TabsRoot(props: TabsProps) {
const [split, rest] = splitProps(props, ["class", "classList"])
return (
<Kobalte
{...rest}
data-component="tabs"
classList={{
...(split.classList ?? {}),
[split.class ?? ""]: !!split.class,
}}
/>
)
}
function TabsList(props: TabsListProps) {
const [split, rest] = splitProps(props, ["class", "classList"])
return (
<Kobalte.List
{...rest}
data-slot="list"
classList={{
...(split.classList ?? {}),
[split.class ?? ""]: !!split.class,
}}
/>
)
}
function TabsTrigger(props: ParentProps<TabsTriggerProps>) {
const [split, rest] = splitProps(props, ["class", "classList", "children"])
return (
<Kobalte.Trigger
{...rest}
data-slot="trigger"
classList={{
...(split.classList ?? {}),
[split.class ?? ""]: !!split.class,
}}
>
{split.children}
</Kobalte.Trigger>
)
}
function TabsContent(props: ParentProps<TabsContentProps>) {
const [split, rest] = splitProps(props, ["class", "classList", "children"])
return (
<Kobalte.Content
{...rest}
data-slot="content"
classList={{
...(split.classList ?? {}),
[split.class ?? ""]: !!split.class,
}}
>
{split.children}
</Kobalte.Content>
)
}
export const Tabs = Object.assign(TabsRoot, {
List: TabsList,
Trigger: TabsTrigger,
Content: TabsContent,
})

View file

@ -12,7 +12,7 @@
}
main > div {
flex: 1;
padding: 3rem;
padding: 2rem;
min-width: 0;
overflow-y: auto;
overflow-x: hidden;
@ -24,6 +24,7 @@
font-size: 1.25rem;
font-weight: 600;
margin: 0 0 1rem 0;
margin-bottom: -1rem;
}
section {
display: flex;

View file

@ -1,6 +1,5 @@
/* @refresh reload */
import { render } from "solid-js/web"
import "solid-devtools"
import App from "./app"

View file

@ -2,34 +2,13 @@
import { $ } from "bun"
import { createOpencode } from "@opencode-ai/sdk"
if (process.versions.bun !== "1.3.0") {
throw new Error("This script requires bun@1.3.0")
}
import { Script } from "@opencode-ai/script"
const notes = [] as string[]
console.log("=== publishing ===\n")
const snapshot = process.env["OPENCODE_SNAPSHOT"] === "true"
const version = await (async () => {
if (snapshot) return `0.0.0-${new Date().toISOString().slice(0, 16).replace(/[-:T]/g, "")}`
if (process.env["OPENCODE_VERSION"]) return process.env["OPENCODE_VERSION"]
const npmVersion = await fetch("https://registry.npmjs.org/opencode-ai/latest")
.then((res) => {
if (!res.ok) throw new Error(res.statusText)
return res.json()
})
.then((data: any) => data.version)
const [major, minor, patch] = npmVersion.split(".").map((x: string) => Number(x) || 0)
const t = process.env["OPENCODE_BUMP"]?.toLowerCase()
if (t === "major") return `${major + 1}.0.0`
if (t === "minor") return `${major}.${minor + 1}.0`
return `${major}.${minor}.${patch + 1}`
})()
process.env["OPENCODE_VERSION"] = version
console.log("version:", version)
if (!snapshot) {
if (!Script.preview) {
const previous = await fetch("https://registry.npmjs.org/opencode-ai/latest")
.then((res) => {
if (!res.ok) throw new Error(res.statusText)
@ -97,7 +76,7 @@ const pkgjsons = await Array.fromAsync(
for (const file of pkgjsons) {
let pkg = await Bun.file(file).text()
pkg = pkg.replaceAll(/"version": "[^"]+"/g, `"version": "${version}"`)
pkg = pkg.replaceAll(/"version": "[^"]+"/g, `"version": "${Script.version}"`)
console.log("updated:", file)
await Bun.file(file).write(pkg)
}
@ -115,12 +94,12 @@ await import(`../packages/plugin/script/publish.ts`)
const dir = new URL("..", import.meta.url).pathname
process.chdir(dir)
if (!snapshot) {
await $`git commit -am "release: v${version}"`
await $`git tag v${version}`
if (!Script.preview) {
await $`git commit -am "release: v${Script.version}"`
await $`git tag v${Script.version}`
await $`git fetch origin`
await $`git cherry-pick HEAD..origin/dev`.nothrow()
await $`git push origin HEAD --tags --no-verify --force`
await $`gh release create v${version} --title "v${version}" --notes ${notes.join("\n") ?? "No notable changes"} ./packages/opencode/dist/*.zip`
await $`gh release create v${Script.version} --title "v${Script.version}" --notes ${notes.join("\n") ?? "No notable changes"} ./packages/opencode/dist/*.zip`
}