Compare commits

..

2 Commits

Author SHA1 Message Date
Dax f697b7960b Merge branch 'dev' into add-beta-branch-trigger 2026-01-29 15:07:44 -05:00
Dax Raad a2c66fe968 ci: add beta branch trigger to publish workflow 2026-01-29 15:07:22 -05:00
143 changed files with 1377 additions and 2793 deletions
+9 -6
View File
@@ -3,17 +3,20 @@ description: "Setup Bun with caching and install dependencies"
runs:
using: "composite"
steps:
- name: Mount Bun Cache
uses: useblacksmith/stickydisk@v1
with:
key: ${{ github.repository }}-bun-cache
path: ~/.bun
- name: Setup Bun
uses: oven-sh/setup-bun@v2
with:
bun-version-file: package.json
- name: Cache ~/.bun
id: cache-bun
uses: actions/cache@v4
with:
path: ~/.bun
key: ${{ runner.os }}-bun-${{ hashFiles('package.json') }}-${{ hashFiles('bun.lockb', 'bun.lock') }}
restore-keys: |
${{ runner.os }}-bun-${{ hashFiles('package.json') }}-
- name: Install dependencies
run: bun install
shell: bash
@@ -1,43 +0,0 @@
name: "Setup Git Committer"
description: "Create app token and configure git user"
inputs:
opencode-app-id:
description: "OpenCode GitHub App ID"
required: true
opencode-app-secret:
description: "OpenCode GitHub App private key"
required: true
outputs:
token:
description: "GitHub App token"
value: ${{ steps.apptoken.outputs.token }}
app-slug:
description: "GitHub App slug"
value: ${{ steps.apptoken.outputs.app-slug }}
runs:
using: "composite"
steps:
- name: Create app token
id: apptoken
uses: actions/create-github-app-token@v2
with:
app-id: ${{ inputs.opencode-app-id }}
private-key: ${{ inputs.opencode-app-secret }}
owner: ${{ github.repository_owner }}
- name: Configure git user
run: |
slug="${{ steps.apptoken.outputs.app-slug }}"
git config --global user.name "${slug}[bot]"
git config --global user.email "${slug}[bot]@users.noreply.github.com"
shell: bash
- name: Clear checkout auth
run: |
git config --local --unset-all http.https://github.com/.extraheader || true
shell: bash
- name: Configure git remote
run: |
git remote set-url origin https://x-access-token:${{ steps.apptoken.outputs.token }}@github.com/${{ github.repository }}
shell: bash
-4
View File
@@ -1,7 +1,3 @@
### What does this PR do?
Please provide a description of the issue (if there is one), the changes you made to fix it, and why they work. It is expected that you understand why your changes work and if you do not understand why at least say as much so a maintainer knows how much to value the pr.
**If you paste a large clearly AI generated description here your PR may be IGNORED or CLOSED!**
### How did you verify your code works?
+6 -10
View File
@@ -15,24 +15,20 @@ jobs:
runs-on: blacksmith-4vcpu-ubuntu-2404
permissions:
contents: write
pull-requests: write
pull-requests: read
steps:
- name: Checkout repository
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Setup Bun
uses: ./.github/actions/setup-bun
- name: Setup Git Committer
id: setup-git-committer
uses: ./.github/actions/setup-git-committer
with:
opencode-app-id: ${{ vars.OPENCODE_APP_ID }}
opencode-app-secret: ${{ secrets.OPENCODE_APP_SECRET }}
- name: Configure Git
run: |
git config user.name "github-actions[bot]"
git config user.email "github-actions[bot]@users.noreply.github.com"
- name: Sync beta branch
env:
GH_TOKEN: ${{ steps.setup-git-committer.outputs.token }}
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: bun script/beta.ts
-45
View File
@@ -1,45 +0,0 @@
name: containers
on:
push:
branches:
- dev
paths:
- packages/containers/**
- .github/workflows/containers.yml
- package.json
workflow_dispatch:
permissions:
contents: read
packages: write
jobs:
build:
runs-on: blacksmith-4vcpu-ubuntu-2404
env:
REGISTRY: ghcr.io/${{ github.repository_owner }}
TAG: "24.04"
steps:
- uses: actions/checkout@v4
- uses: ./.github/actions/setup-bun
- name: Set up QEMU
uses: docker/setup-qemu-action@v3
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Login to GHCR
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.repository_owner }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Build and push containers
run: bun ./packages/containers/script/build.ts --push
env:
REGISTRY: ${{ env.REGISTRY }}
TAG: ${{ env.TAG }}
+9 -8
View File
@@ -4,6 +4,8 @@ on:
push:
branches:
- dev
pull_request:
workflow_dispatch:
jobs:
generate:
@@ -14,17 +16,14 @@ jobs:
steps:
- name: Checkout repository
uses: actions/checkout@v4
with:
token: ${{ secrets.GITHUB_TOKEN }}
repository: ${{ github.event.pull_request.head.repo.full_name || github.repository }}
ref: ${{ github.event.pull_request.head.ref || github.ref_name }}
- name: Setup Bun
uses: ./.github/actions/setup-bun
- name: Setup git committer
id: committer
uses: ./.github/actions/setup-git-committer
with:
opencode-app-id: ${{ vars.OPENCODE_APP_ID }}
opencode-app-secret: ${{ secrets.OPENCODE_APP_SECRET }}
- name: Generate
run: ./script/generate.ts
@@ -34,8 +33,10 @@ jobs:
echo "No changes to commit"
exit 0
fi
git config --local user.email "action@github.com"
git config --local user.name "GitHub Action"
git add -A
git commit -m "chore: generate" --allow-empty
git commit -m "chore: generate"
git push origin HEAD:${{ github.ref_name }} --no-verify
# if ! git push origin HEAD:${{ github.event.pull_request.head.ref || github.ref_name }} --no-verify; then
# echo ""
+5 -7
View File
@@ -36,16 +36,14 @@ jobs:
ref: ${{ github.head_ref || github.ref_name }}
repository: ${{ github.event.pull_request.head.repo.full_name || github.repository }}
- name: Setup git committer
id: committer
uses: ./.github/actions/setup-git-committer
with:
opencode-app-id: ${{ vars.OPENCODE_APP_ID }}
opencode-app-secret: ${{ secrets.OPENCODE_APP_SECRET }}
- name: Setup Nix
uses: nixbuild/nix-quick-install-action@v34
- name: Configure git
run: |
git config --global user.email "action@github.com"
git config --global user.name "Github Action"
- name: Pull latest changes
env:
TARGET_BRANCH: ${{ github.head_ref || github.ref_name }}
+19 -35
View File
@@ -37,14 +37,8 @@ jobs:
steps:
- uses: actions/checkout@v3
with:
fetch-depth: 0
fetch-depth: 1
- uses: ./.github/actions/setup-bun
- name: Install OpenCode
if: inputs.bump || inputs.version
run: bun i -g opencode-ai
- id: version
run: |
./script/version.ts
@@ -52,7 +46,6 @@ jobs:
GH_TOKEN: ${{ github.token }}
OPENCODE_BUMP: ${{ inputs.bump }}
OPENCODE_VERSION: ${{ inputs.version }}
OPENCODE_API_KEY: ${{ secrets.OPENCODE_API_KEY }}
outputs:
version: ${{ steps.version.outputs.version }}
release: ${{ steps.version.outputs.release }}
@@ -65,6 +58,7 @@ jobs:
steps:
- uses: actions/checkout@v3
with:
fetch-depth: 1
fetch-tags: true
- uses: ./.github/actions/setup-bun
@@ -109,6 +103,7 @@ jobs:
steps:
- uses: actions/checkout@v3
with:
fetch-depth: 1
fetch-tags: true
- uses: apple-actions/import-codesign-certs@v2
@@ -133,15 +128,6 @@ jobs:
- uses: ./.github/actions/setup-bun
- name: Cache apt packages
if: contains(matrix.settings.host, 'ubuntu')
uses: actions/cache@v4
with:
path: /var/cache/apt/archives
key: ${{ runner.os }}-${{ matrix.settings.target }}-apt-${{ hashFiles('.github/workflows/publish.yml') }}
restore-keys: |
${{ runner.os }}-${{ matrix.settings.target }}-apt-
- name: install dependencies (ubuntu only)
if: contains(matrix.settings.host, 'ubuntu')
run: |
@@ -163,8 +149,8 @@ jobs:
cd packages/desktop
bun ./scripts/prepare.ts
env:
OPENCODE_VERSION: ${{ needs.version.outputs.version }}
GITHUB_TOKEN: ${{ steps.committer.outputs.token }}
OPENCODE_VERSION: ${{ needs.publish.outputs.version }}
GITHUB_TOKEN: ${{ secrets.SST_GITHUB_TOKEN }}
RUST_TARGET: ${{ matrix.settings.target }}
GH_TOKEN: ${{ github.token }}
GITHUB_RUN_ID: ${{ github.run_id }}
@@ -210,8 +196,12 @@ jobs:
runs-on: blacksmith-4vcpu-ubuntu-2404
steps:
- uses: actions/checkout@v3
with:
fetch-depth: 1
- uses: ./.github/actions/setup-bun
- name: Install OpenCode
if: inputs.bump || inputs.version
run: bun i -g opencode-ai
- name: Login to GitHub Container Registry
uses: docker/login-action@v3
@@ -231,26 +221,19 @@ jobs:
node-version: "24"
registry-url: "https://registry.npmjs.org"
- name: Setup git committer
id: committer
uses: ./.github/actions/setup-git-committer
with:
opencode-app-id: ${{ vars.OPENCODE_APP_ID }}
opencode-app-secret: ${{ secrets.OPENCODE_APP_SECRET }}
- name: Setup Git Identity
run: |
git config --global user.email "opencode@sst.dev"
git config --global user.name "opencode"
git remote set-url origin https://x-access-token:${{ secrets.SST_GITHUB_TOKEN }}@github.com/${{ github.repository }}
- uses: ./.github/actions/setup-bun
- uses: actions/download-artifact@v4
with:
name: opencode-cli
path: packages/opencode/dist
- name: Cache apt packages (AUR)
uses: actions/cache@v4
with:
path: /var/cache/apt/archives
key: ${{ runner.os }}-apt-aur-${{ hashFiles('.github/workflows/publish.yml') }}
restore-keys: |
${{ runner.os }}-apt-aur-
- name: Setup SSH for AUR
run: |
sudo apt-get update
@@ -267,5 +250,6 @@ jobs:
OPENCODE_VERSION: ${{ needs.version.outputs.version }}
OPENCODE_RELEASE: ${{ needs.version.outputs.release }}
AUR_KEY: ${{ secrets.AUR_KEY }}
GITHUB_TOKEN: ${{ steps.committer.outputs.token }}
GITHUB_TOKEN: ${{ secrets.SST_GITHUB_TOKEN }}
OPENCODE_API_KEY: ${{ secrets.OPENCODE_API_KEY }}
NPM_CONFIG_PROVENANCE: false
-1
View File
@@ -1,7 +1,6 @@
- To regenerate the JavaScript SDK, run `./packages/sdk/js/script/build.ts`.
- ALWAYS USE PARALLEL TOOLS WHEN APPLICABLE.
- The default branch in this repo is `dev`.
- Prefer automation: execute requested actions without confirmation unless blocked by missing info or safety/irreversibility.
## Style Guide
+502 -344
View File
File diff suppressed because it is too large Load Diff
-2
View File
@@ -133,8 +133,6 @@ const ZEN_MODELS = [
new sst.Secret("ZEN_MODELS6"),
new sst.Secret("ZEN_MODELS7"),
new sst.Secret("ZEN_MODELS8"),
new sst.Secret("ZEN_MODELS9"),
new sst.Secret("ZEN_MODELS10"),
]
const STRIPE_SECRET_KEY = new sst.Secret("STRIPE_SECRET_KEY")
const STRIPE_PUBLISHABLE_KEY = new sst.Secret("STRIPE_PUBLISHABLE_KEY")
+4 -4
View File
@@ -1,8 +1,8 @@
{
"nodeModules": {
"x86_64-linux": "sha256-gUWzUsk81miIrjg0fZQmsIQG4pZYmEHgzN6BaXI+lfc=",
"aarch64-linux": "sha256-gwEG75ha/ojTO2iAObTmLTtEkXIXJ7BThzfI5CqlJh8=",
"aarch64-darwin": "sha256-20RGG2GkUItCzD67gDdoSLfexttM8abS//FKO9bfjoM=",
"x86_64-darwin": "sha256-i2VawFuR1UbjPVYoybU6aJDJfFo0tcvtl1aM31Y2mTQ="
"x86_64-linux": "sha256-yAtZlh6YR78RwPt0LK/7Pk0qUm0/97+s6ghhZzuoE/0=",
"aarch64-linux": "sha256-6j81rdjQ7Wps9bvfw+mmdwW5p01qUOwX40UZltCTe3Y=",
"aarch64-darwin": "sha256-pDM8M/QMWR6Go5pz3XXsJqcJDHAlHrx2Faijjkzcngo=",
"x86_64-darwin": "sha256-eOAPtMd1n5xYupBOevCLhY1eFy3wzGqFk/EsZocl9Y8="
}
}
@@ -1,5 +1,5 @@
import { test, expect } from "../fixtures"
import { promptSelector } from "../utils"
import { test, expect } from "./fixtures"
import { promptSelector } from "./utils"
test("context panel can be opened from the prompt", async ({ page, sdk, gotoSession }) => {
const title = `e2e smoke context ${Date.now()}`
@@ -1,5 +1,5 @@
import { test, expect } from "../fixtures"
import { modKey } from "../utils"
import { test, expect } from "./fixtures"
import { modKey } from "./utils"
test("can open a file tab from the search palette", async ({ page, gotoSession }) => {
await gotoSession()
@@ -1,4 +1,4 @@
import { test, expect } from "../fixtures"
import { test, expect } from "./fixtures"
test.skip("file tree can expand folders and open a file", async ({ page, gotoSession }) => {
await gotoSession()
@@ -1,5 +1,5 @@
import { test, expect } from "../fixtures"
import { modKey } from "../utils"
import { test, expect } from "./fixtures"
import { modKey } from "./utils"
test("smoke file viewer renders real file content", async ({ page, gotoSession }) => {
await gotoSession()
@@ -1,5 +1,5 @@
import { test, expect } from "../fixtures"
import { serverName } from "../utils"
import { test, expect } from "./fixtures"
import { serverName } from "./utils"
test("home renders and shows core entrypoints", async ({ page }) => {
await page.goto("/")
@@ -1,5 +1,5 @@
import { test, expect } from "../fixtures"
import { promptSelector } from "../utils"
import { test, expect } from "./fixtures"
import { promptSelector } from "./utils"
test("smoke model selection updates prompt footer", async ({ page, gotoSession }) => {
await gotoSession()
@@ -1,5 +1,5 @@
import { test, expect } from "../fixtures"
import { modKey, promptSelector } from "../utils"
import { test, expect } from "./fixtures"
import { modKey, promptSelector } from "./utils"
test("hiding a model removes it from the model picker", async ({ page, gotoSession }) => {
await gotoSession()
@@ -1,5 +1,5 @@
import { test, expect } from "../fixtures"
import { dirPath, promptSelector } from "../utils"
import { test, expect } from "./fixtures"
import { dirPath, promptSelector } from "./utils"
test("project route redirects to /session", async ({ page, directory, slug }) => {
await page.goto(dirPath(directory))
@@ -1,5 +1,5 @@
import { test, expect } from "../fixtures"
import { modKey } from "../utils"
import { test, expect } from "./fixtures"
import { modKey } from "./utils"
test("search palette opens and closes", async ({ page, gotoSession }) => {
await gotoSession()
@@ -1,5 +1,5 @@
import { test, expect } from "../fixtures"
import { promptSelector } from "../utils"
import { test, expect } from "./fixtures"
import { promptSelector } from "./utils"
test("smoke @mention inserts file pill token", async ({ page, gotoSession }) => {
await gotoSession()
@@ -1,5 +1,5 @@
import { test, expect } from "../fixtures"
import { promptSelector } from "../utils"
import { test, expect } from "./fixtures"
import { promptSelector } from "./utils"
test("smoke /open opens file picker dialog", async ({ page, gotoSession }) => {
await gotoSession()
@@ -1,5 +1,5 @@
import { test, expect } from "../fixtures"
import { promptSelector } from "../utils"
import { test, expect } from "./fixtures"
import { promptSelector } from "./utils"
function sessionIDFromUrl(url: string) {
const match = /\/session\/([^/?#]+)/.exec(url)
@@ -1,5 +1,5 @@
import { test, expect } from "../fixtures"
import { serverName, serverUrl } from "../utils"
import { test, expect } from "./fixtures"
import { serverName, serverUrl } from "./utils"
const DEFAULT_SERVER_URL_KEY = "opencode.settings.dat:defaultServerUrl"
@@ -1,5 +1,5 @@
import { test, expect } from "../fixtures"
import { promptSelector } from "../utils"
import { test, expect } from "./fixtures"
import { promptSelector } from "./utils"
test("can open an existing session and type into the prompt", async ({ page, sdk, gotoSession }) => {
const title = `e2e smoke ${Date.now()}`
@@ -1,5 +1,5 @@
import { test, expect } from "../fixtures"
import { modKey, promptSelector } from "../utils"
import { test, expect } from "./fixtures"
import { modKey, promptSelector } from "./utils"
test("smoke providers settings opens provider selector", async ({ page, gotoSession }) => {
await gotoSession()
@@ -1,5 +1,5 @@
import { test, expect } from "../fixtures"
import { modKey } from "../utils"
import { test, expect } from "./fixtures"
import { modKey } from "./utils"
test("smoke settings dialog opens, switches tabs, closes", async ({ page, gotoSession }) => {
await gotoSession()
@@ -1,39 +0,0 @@
import { test, expect } from "../fixtures"
import { modKey, settingsLanguageSelectSelector } from "../utils"
test("smoke changing language updates settings labels", async ({ page, gotoSession }) => {
await page.addInitScript(() => {
localStorage.setItem("opencode.global.dat:language", JSON.stringify({ locale: "en" }))
})
await gotoSession()
const dialog = page.getByRole("dialog")
await page.keyboard.press(`${modKey}+Comma`).catch(() => undefined)
const opened = await dialog
.waitFor({ state: "visible", timeout: 3000 })
.then(() => true)
.catch(() => false)
if (!opened) {
await page.getByRole("button", { name: "Settings" }).first().click()
await expect(dialog).toBeVisible()
}
const heading = dialog.getByRole("heading", { level: 2 })
await expect(heading).toHaveText("General")
const select = dialog.locator(settingsLanguageSelectSelector)
await expect(select).toBeVisible()
await select.locator('[data-slot="select-select-trigger"]').click()
await page.locator('[data-slot="select-select-item"]').filter({ hasText: "Deutsch" }).click()
await expect(heading).toHaveText("Allgemein")
await select.locator('[data-slot="select-select-trigger"]').click()
await page.locator('[data-slot="select-select-item"]').filter({ hasText: "English" }).click()
await expect(heading).toHaveText("General")
})
@@ -1,7 +1,33 @@
import { test, expect } from "../fixtures"
import { modKey, promptSelector } from "../utils"
import { test, expect } from "./fixtures"
import { modKey, promptSelector } from "./utils"
test("sidebar session links navigate to the selected session", async ({ page, slug, sdk, gotoSession }) => {
type Locator = {
first: () => Locator
getAttribute: (name: string) => Promise<string | null>
scrollIntoViewIfNeeded: () => Promise<void>
click: () => Promise<void>
}
type Page = {
locator: (selector: string) => Locator
keyboard: {
press: (key: string) => Promise<void>
}
}
type Fixtures = {
page: Page
slug: string
sdk: {
session: {
create: (input: { title: string }) => Promise<{ data?: { id?: string } }>
delete: (input: { sessionID: string }) => Promise<unknown>
}
}
gotoSession: (sessionID?: string) => Promise<void>
}
test("sidebar session links navigate to the selected session", async ({ page, slug, sdk, gotoSession }: Fixtures) => {
const stamp = Date.now()
const one = await sdk.session.create({ title: `e2e sidebar nav 1 ${stamp}` }).then((r) => r.data)
@@ -1,5 +1,5 @@
import { test, expect } from "../fixtures"
import { modKey } from "../utils"
import { test, expect } from "./fixtures"
import { modKey } from "./utils"
test("sidebar can be collapsed and expanded", async ({ page, gotoSession }) => {
await gotoSession()
@@ -1,5 +1,5 @@
import { test, expect } from "../fixtures"
import { promptSelector, terminalSelector, terminalToggleKey } from "../utils"
import { test, expect } from "./fixtures"
import { promptSelector, terminalSelector, terminalToggleKey } from "./utils"
test("smoke terminal mounts and can create a second tab", async ({ page, gotoSession }) => {
await gotoSession()
@@ -1,5 +1,5 @@
import { test, expect } from "../fixtures"
import { terminalSelector, terminalToggleKey } from "../utils"
import { test, expect } from "./fixtures"
import { terminalSelector, terminalToggleKey } from "./utils"
test("terminal panel can be toggled", async ({ page, gotoSession }) => {
await gotoSession()
-25
View File
@@ -1,25 +0,0 @@
import { test, expect } from "./fixtures"
import { modelVariantCycleSelector } from "./utils"
test("smoke model variant cycle updates label", async ({ page, gotoSession }) => {
await gotoSession()
await page.addStyleTag({
content: `${modelVariantCycleSelector} { display: inline-block !important; }`,
})
const button = page.locator(modelVariantCycleSelector)
const exists = (await button.count()) > 0
test.skip(!exists, "current model has no variants")
if (!exists) return
await expect(button).toBeVisible()
const before = (await button.innerText()).trim()
await button.click()
await expect(button).not.toHaveText(before)
const after = (await button.innerText()).trim()
await button.click()
await expect(button).not.toHaveText(after)
})
@@ -1,5 +1,5 @@
import { test, expect } from "../fixtures"
import { modKey, promptSelector } from "../utils"
import { test, expect } from "./fixtures"
import { modKey, promptSelector } from "./utils"
test("titlebar back/forward navigates between sessions", async ({ page, slug, sdk, gotoSession }) => {
await page.setViewportSize({ width: 1400, height: 800 })
-3
View File
@@ -12,9 +12,6 @@ export const terminalToggleKey = "Control+Backquote"
export const promptSelector = '[data-component="prompt-input"]'
export const terminalSelector = '[data-component="terminal"]'
export const modelVariantCycleSelector = '[data-action="model-variant-cycle"]'
export const settingsLanguageSelectSelector = '[data-action="settings-language"]'
export function createSdk(directory?: string) {
return createOpencodeClient({ baseUrl: serverUrl, directory, throwOnError: true })
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@opencode-ai/app",
"version": "1.1.47",
"version": "0.0.0-ci-202601291718",
"description": "",
"type": "module",
"exports": {
@@ -34,14 +34,11 @@ export const DialogSelectModelUnpaid: Component = () => {
})
return (
<Dialog
title={language.t("dialog.model.select.title")}
class="overflow-y-auto [&_[data-slot=dialog-body]]:overflow-visible [&_[data-slot=dialog-body]]:flex-none"
>
<div class="flex flex-col gap-3 px-2.5">
<Dialog title={language.t("dialog.model.select.title")}>
<div class="flex flex-col gap-3 px-2.5 flex-1 min-h-0">
<div class="text-14-medium text-text-base px-2.5">{language.t("dialog.model.unpaid.freeModels.title")}</div>
<List
class="[&_[data-slot=list-scroll]]:overflow-visible"
class="flex-1 min-h-0 [&_[data-slot=list-scroll]]:flex-1 [&_[data-slot=list-scroll]]:min-h-0"
ref={(ref) => (listRef = ref)}
items={local.model.list}
current={local.model.current()}
@@ -79,6 +76,8 @@ export const DialogSelectModelUnpaid: Component = () => {
</div>
)}
</List>
<div />
<div />
</div>
<div class="px-1.5 pb-1.5">
<div class="w-full rounded-sm border border-border-weak-base bg-surface-raised-base">
@@ -90,7 +90,7 @@ const ModelList: Component<{
export function ModelSelectorPopover<T extends ValidComponent = "div">(props: {
provider?: string
children?: JSX.Element | ((open: boolean) => JSX.Element)
children?: JSX.Element
triggerAs?: T
triggerProps?: ComponentProps<T>
}) {
@@ -182,13 +182,12 @@ export function ModelSelectorPopover<T extends ValidComponent = "div">(props: {
as={props.triggerAs ?? "div"}
{...(props.triggerProps as any)}
>
{typeof props.children === "function" ? props.children(store.open) : props.children}
{props.children}
</Kobalte.Trigger>
<Kobalte.Portal>
<Kobalte.Content
class="w-72 h-80 flex flex-col rounded-md border border-border-base bg-surface-raised-stronger-non-alpha shadow-md z-50 outline-none overflow-hidden"
data-component="model-popover-content"
ref={(el) => setStore("content", el)}
class="w-72 h-80 flex flex-col p-2 rounded-md border border-border-base bg-surface-raised-stronger-non-alpha shadow-md z-50 outline-none overflow-hidden"
onEscapeKeyDown={(event) => {
setStore("dismiss", "escape")
setStore("open", false)
+43 -82
View File
@@ -32,9 +32,7 @@ import { useNavigate, useParams } from "@solidjs/router"
import { useSync } from "@/context/sync"
import { useComments } from "@/context/comments"
import { FileIcon } from "@opencode-ai/ui/file-icon"
import { MorphChevron } from "@opencode-ai/ui/morph-chevron"
import { Button } from "@opencode-ai/ui/button"
import { CycleLabel } from "@opencode-ai/ui/cycle-label"
import { Icon } from "@opencode-ai/ui/icon"
import { ProviderIcon } from "@opencode-ai/ui/provider-icon"
import type { IconName } from "@opencode-ai/ui/icons/provider"
@@ -44,7 +42,6 @@ import { Select } from "@opencode-ai/ui/select"
import { getDirectory, getFilename, getFilenameTruncated } from "@opencode-ai/util/path"
import { useDialog } from "@opencode-ai/ui/context/dialog"
import { ImagePreview } from "@opencode-ai/ui/image-preview"
import { ReasoningIcon } from "@opencode-ai/ui/reasoning-icon"
import { ModelSelectorPopover } from "@/components/dialog-select-model"
import { DialogSelectModelUnpaid } from "@/components/dialog-select-model-unpaid"
import { useProviders } from "@/hooks/use-providers"
@@ -1255,7 +1252,7 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
clearInput()
client.session
.shell({
sessionID: session?.id || "",
sessionID: session.id,
agent,
model,
command: text,
@@ -1278,7 +1275,7 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
clearInput()
client.session
.command({
sessionID: session?.id || "",
sessionID: session.id,
command: commandName,
arguments: args.join(" "),
agent,
@@ -1434,13 +1431,13 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
const optimisticParts = requestParts.map((part) => ({
...part,
sessionID: session?.id || "",
sessionID: session.id,
messageID,
})) as unknown as Part[]
const optimisticMessage: Message = {
id: messageID,
sessionID: session?.id || "",
sessionID: session.id,
role: "user",
time: { created: Date.now() },
agent,
@@ -1451,9 +1448,9 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
if (sessionDirectory === projectDirectory) {
sync.set(
produce((draft) => {
const messages = draft.message[session?.id || ""]
const messages = draft.message[session.id]
if (!messages) {
draft.message[session?.id || ""] = [optimisticMessage]
draft.message[session.id] = [optimisticMessage]
} else {
const result = Binary.search(messages, messageID, (m) => m.id)
messages.splice(result.index, 0, optimisticMessage)
@@ -1469,9 +1466,9 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
globalSync.child(sessionDirectory)[1](
produce((draft) => {
const messages = draft.message[session?.id || ""]
const messages = draft.message[session.id]
if (!messages) {
draft.message[session?.id || ""] = [optimisticMessage]
draft.message[session.id] = [optimisticMessage]
} else {
const result = Binary.search(messages, messageID, (m) => m.id)
messages.splice(result.index, 0, optimisticMessage)
@@ -1488,7 +1485,7 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
if (sessionDirectory === projectDirectory) {
sync.set(
produce((draft) => {
const messages = draft.message[session?.id || ""]
const messages = draft.message[session.id]
if (messages) {
const result = Binary.search(messages, messageID, (m) => m.id)
if (result.found) messages.splice(result.index, 1)
@@ -1501,7 +1498,7 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
globalSync.child(sessionDirectory)[1](
produce((draft) => {
const messages = draft.message[session?.id || ""]
const messages = draft.message[session.id]
if (messages) {
const result = Binary.search(messages, messageID, (m) => m.id)
if (result.found) messages.splice(result.index, 1)
@@ -1522,15 +1519,15 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
const worktree = WorktreeState.get(sessionDirectory)
if (!worktree || worktree.status !== "pending") return true
if (sessionDirectory === projectDirectory && session?.id) {
sync.set("session_status", session?.id, { type: "busy" })
if (sessionDirectory === projectDirectory) {
sync.set("session_status", session.id, { type: "busy" })
}
const controller = new AbortController()
const cleanup = () => {
if (sessionDirectory === projectDirectory && session?.id) {
sync.set("session_status", session?.id, { type: "idle" })
if (sessionDirectory === projectDirectory) {
sync.set("session_status", session.id, { type: "idle" })
}
removeOptimisticMessage()
for (const item of commentItems) {
@@ -1547,7 +1544,7 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
restoreInput()
}
pending.set(session?.id || "", { abort: controller, cleanup })
pending.set(session.id, { abort: controller, cleanup })
const abort = new Promise<Awaited<ReturnType<typeof WorktreeState.wait>>>((resolve) => {
if (controller.signal.aborted) {
@@ -1575,7 +1572,7 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
if (timer.id === undefined) return
clearTimeout(timer.id)
})
pending.delete(session?.id || "")
pending.delete(session.id)
if (controller.signal.aborted) return false
if (result.status === "failed") throw new Error(result.message)
return true
@@ -1585,7 +1582,7 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
const ok = await waitForWorktree()
if (!ok) return
await client.session.prompt({
sessionID: session?.id || "",
sessionID: session.id,
agent,
model,
messageID,
@@ -1595,9 +1592,9 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
}
void send().catch((err) => {
pending.delete(session?.id || "")
if (sessionDirectory === projectDirectory && session?.id) {
sync.set("session_status", session?.id, { type: "idle" })
pending.delete(session.id)
if (sessionDirectory === projectDirectory) {
sync.set("session_status", session.id, { type: "idle" })
}
showToast({
title: language.t("prompt.toast.promptSendFailed.title"),
@@ -1619,28 +1616,6 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
})
}
const currrentModelVariant = createMemo(() => {
const modelVariant = local.model.variant.current() ?? ""
return modelVariant === "xhigh"
? "xHigh"
: modelVariant.length > 0
? modelVariant[0].toUpperCase() + modelVariant.slice(1)
: "Default"
})
const reasoningPercentage = createMemo(() => {
const variants = local.model.variant.list()
const current = local.model.variant.current()
const totalEntries = variants.length + 1
if (totalEntries <= 2 || current === "Default") {
return 0
}
const currentIndex = current ? variants.indexOf(current) + 1 : 0
return ((currentIndex + 1) / totalEntries) * 100
}, [local.model.variant])
return (
<div class="relative size-full _max-h-[320px] flex flex-col gap-3">
<Show when={store.popover}>
@@ -1693,7 +1668,7 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
</>
}
>
<Icon name="brain" size="normal" class="text-icon-info-active shrink-0" />
<Icon name="brain" size="small" class="text-icon-info-active shrink-0" />
<span class="text-14-regular text-text-strong whitespace-nowrap">
@{(item as { type: "agent"; name: string }).name}
</span>
@@ -1754,9 +1729,9 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
}}
>
<Show when={store.dragging}>
<div class="absolute inset-0 z-10 flex items-center justify-center bg-surface-raised-stronger-non-alpha/90 mr-1 pointer-events-none">
<div class="absolute inset-0 z-10 flex items-center justify-center bg-surface-raised-stronger-non-alpha/90 pointer-events-none">
<div class="flex flex-col items-center gap-2 text-text-weak">
<Icon name="photo" size={18} class="text-icon-base stroke-1.5" />
<Icon name="photo" class="size-8" />
<span class="text-14-regular">{language.t("prompt.dropzone.label")}</span>
</div>
</div>
@@ -1795,7 +1770,7 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
}}
>
<div class="flex items-center gap-1.5">
<FileIcon node={{ path: item.path, type: "file" }} class="shrink-0 size-7" />
<FileIcon node={{ path: item.path, type: "file" }} class="shrink-0 size-3.5" />
<div class="flex items-center text-11-regular min-w-0 font-medium">
<span class="text-text-strong whitespace-nowrap">{getFilenameTruncated(item.path, 14)}</span>
<Show when={item.selection}>
@@ -1812,7 +1787,7 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
type="button"
icon="close-small"
variant="ghost"
class="ml-auto size-7 opacity-0 group-hover:opacity-100 transition-all"
class="ml-auto h-5 w-5 opacity-0 group-hover:opacity-100 transition-all"
onClick={(e) => {
e.stopPropagation()
if (item.commentID) comments.remove(item.path, item.commentID)
@@ -1842,7 +1817,7 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
when={attachment.mime.startsWith("image/")}
fallback={
<div class="size-16 rounded-md bg-surface-base flex items-center justify-center border border-border-base">
<Icon name="folder" size="normal" class="size-6 text-text-base" />
<Icon name="folder" class="size-6 text-text-weak" />
</div>
}
>
@@ -1916,7 +1891,7 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
</Show>
</div>
<div class="relative p-3 flex items-center justify-between">
<div class="flex items-center justify-start gap-2">
<div class="flex items-center justify-start gap-0.5">
<Switch>
<Match when={store.mode === "shell"}>
<div class="flex items-center gap-2 px-2 h-6">
@@ -1947,17 +1922,12 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
title={language.t("command.model.choose")}
keybind={command.keybind("model.choose")}
>
<Button
as="div"
variant="ghost"
class="px-2"
onClick={() => dialog.render(<DialogSelectModelUnpaid />, "select-model")}
>
<Button as="div" variant="ghost" onClick={() => dialog.show(() => <DialogSelectModelUnpaid />)}>
<Show when={local.model.current()?.provider?.id}>
<ProviderIcon id={local.model.current()!.provider.id as IconName} class="size-4 shrink-0" />
</Show>
{local.model.current()?.name ?? language.t("dialog.model.select.title")}
<MorphChevron expanded={dialog.isActive("select-model")} />
<Icon name="chevron-down" size="small" />
</Button>
</TooltipKeybind>
}
@@ -1968,15 +1938,11 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
keybind={command.keybind("model.choose")}
>
<ModelSelectorPopover triggerAs={Button} triggerProps={{ variant: "ghost" }}>
{(open) => (
<>
<Show when={local.model.current()?.provider?.id}>
<ProviderIcon id={local.model.current()!.provider.id as IconName} class="size-4 shrink-0" />
</Show>
{local.model.current()?.name ?? language.t("dialog.model.select.title")}
<MorphChevron expanded={open} class="text-text-weak" />
</>
)}
<Show when={local.model.current()?.provider?.id}>
<ProviderIcon id={local.model.current()!.provider.id as IconName} class="size-4 shrink-0" />
</Show>
{local.model.current()?.name ?? language.t("dialog.model.select.title")}
<Icon name="chevron-down" size="small" />
</ModelSelectorPopover>
</TooltipKeybind>
</Show>
@@ -1987,15 +1953,11 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
keybind={command.keybind("model.variant.cycle")}
>
<Button
data-action="model-variant-cycle"
variant="ghost"
class="text-text-strong text-12-regular"
class="text-text-base _hidden group-hover/prompt-input:inline-block capitalize text-12-regular"
onClick={() => local.model.variant.cycle()}
>
<Show when={local.model.variant.list().length > 1}>
<ReasoningIcon percentage={reasoningPercentage()} size={16} strokeWidth={1.25} />
</Show>
<CycleLabel value={currrentModelVariant()} />
{local.model.variant.current() ?? language.t("common.default")}
</Button>
</TooltipKeybind>
</Show>
@@ -2009,7 +1971,7 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
variant="ghost"
onClick={() => permission.toggleAutoAccept(params.id!, sdk.directory)}
classList={{
"_hidden group-hover/prompt-input:flex items-center justify-center": true,
"_hidden group-hover/prompt-input:flex size-6 items-center justify-center": true,
"text-text-base": !permission.isAutoAccepting(params.id!, sdk.directory),
"hover:bg-surface-success-base": permission.isAutoAccepting(params.id!, sdk.directory),
}}
@@ -2031,7 +1993,7 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
</Match>
</Switch>
</div>
<div class="flex items-center gap-1 absolute right-3 bottom-3">
<div class="flex items-center gap-3 absolute right-3 bottom-3">
<input
ref={fileInputRef}
type="file"
@@ -2043,19 +2005,18 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
e.currentTarget.value = ""
}}
/>
<div class="flex items-center gap-1.5 mr-1.5">
<div class="flex items-center gap-2">
<SessionContextUsage />
<Show when={store.mode === "normal"}>
<Tooltip placement="top" value={language.t("prompt.action.attachFile")}>
<Button
type="button"
variant="ghost"
size="small"
class="px-1"
class="size-6"
onClick={() => fileInputRef.click()}
aria-label={language.t("prompt.action.attachFile")}
>
<Icon name="photo" class="size-6 text-icon-base" />
<Icon name="photo" class="size-4.5" />
</Button>
</Tooltip>
</Show>
@@ -2074,7 +2035,7 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
<Match when={true}>
<div class="flex items-center gap-2">
<span>{language.t("prompt.action.send")}</span>
<Icon name="enter" size="normal" class="text-icon-base" />
<Icon name="enter" size="small" class="text-icon-base" />
</div>
</Match>
</Switch>
@@ -2085,7 +2046,7 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
disabled={!prompt.dirty() && !working()}
icon={working() ? "stop" : "arrow-up"}
variant="primary"
class="h-6 w-5.5"
class="h-6 w-4.5"
aria-label={working() ? language.t("prompt.action.stop") : language.t("prompt.action.send")}
/>
</Tooltip>
@@ -64,8 +64,8 @@ export function SessionContextUsage(props: SessionContextUsageProps) {
}
const circle = () => (
<div class="text-icon-base">
<ProgressCircle size={18} percentage={context()?.percentage ?? 0} />
<div class="p-1">
<ProgressCircle size={16} strokeWidth={2} percentage={context()?.percentage ?? 0} />
</div>
)
@@ -101,7 +101,7 @@ export function SessionContextUsage(props: SessionContextUsageProps) {
<Button
type="button"
variant="ghost"
class="size-7 text-icon-base"
class="size-6"
onClick={openContext}
aria-label={language.t("context.usage.view")}
>
@@ -130,7 +130,7 @@ export function SessionHeader() {
<Portal mount={mount()}>
<button
type="button"
class="hidden md:flex w-[320px] max-w-full min-w-0 p-1 pl-1.5 items-center gap-2 justify-between rounded-md border border-border-weak-base bg-surface-raised-base transition-colors cursor-default hover:bg-surface-raised-base-hover focus-visible:bg-surface-raised-base-hover active:bg-surface-raised-base-active"
class="hidden md:flex w-[320px] p-1 pl-1.5 items-center gap-2 justify-between rounded-md border border-border-weak-base bg-surface-raised-base transition-colors cursor-default hover:bg-surface-raised-base-hover focus-visible:bg-surface-raised-base-hover active:bg-surface-raised-base-active"
onClick={() => command.trigger("file.open")}
aria-label={language.t("session.header.searchFiles")}
>
@@ -10,7 +10,6 @@ import { usePlatform } from "@/context/platform"
import { useSettings, monoFontFamily } from "@/context/settings"
import { playSound, SOUND_OPTIONS } from "@/utils/sound"
import { Link } from "./link"
import { ScrollFade } from "@opencode-ai/ui/scroll-fade"
let demoSoundState = {
cleanup: undefined as (() => void) | undefined,
@@ -131,12 +130,7 @@ export const SettingsGeneral: Component = () => {
const soundOptions = [...SOUND_OPTIONS]
return (
<ScrollFade
direction="vertical"
fadeStartSize={0}
fadeEndSize={16}
class="flex flex-col h-full overflow-y-auto no-scrollbar px-4 pb-10 sm:px-10 sm:pb-10"
>
<div class="flex flex-col h-full overflow-y-auto no-scrollbar px-4 pb-10 sm:px-10 sm:pb-10">
<div class="sticky top-0 z-10 bg-[linear-gradient(to_bottom,var(--surface-raised-stronger-non-alpha)_calc(100%_-_24px),transparent)]">
<div class="flex flex-col gap-1 pt-6 pb-8">
<h2 class="text-16-medium text-text-strong">{language.t("settings.tab.general")}</h2>
@@ -154,7 +148,6 @@ export const SettingsGeneral: Component = () => {
description={language.t("settings.general.row.language.description")}
>
<Select
data-action="settings-language"
options={languageOptions()}
current={languageOptions().find((o) => o.value === language.locale())}
value={(o) => o.value}
@@ -401,7 +394,7 @@ export const SettingsGeneral: Component = () => {
</div>
</div>
</div>
</ScrollFade>
</div>
)
}
@@ -9,7 +9,6 @@ import fuzzysort from "fuzzysort"
import { formatKeybind, parseKeybind, useCommand } from "@/context/command"
import { useLanguage } from "@/context/language"
import { useSettings } from "@/context/settings"
import { ScrollFade } from "@opencode-ai/ui/scroll-fade"
const IS_MAC = typeof navigator === "object" && /(Mac|iPod|iPhone|iPad)/.test(navigator.platform)
const PALETTE_ID = "command.palette"
@@ -353,12 +352,7 @@ export const SettingsKeybinds: Component = () => {
})
return (
<ScrollFade
direction="vertical"
fadeStartSize={0}
fadeEndSize={16}
class="flex flex-col h-full overflow-y-auto no-scrollbar px-4 pb-10 sm:px-10 sm:pb-10"
>
<div class="flex flex-col h-full overflow-y-auto no-scrollbar px-4 pb-10 sm:px-10 sm:pb-10">
<div class="sticky top-0 z-10 bg-[linear-gradient(to_bottom,var(--surface-raised-stronger-non-alpha)_calc(100%_-_24px),transparent)]">
<div class="flex flex-col gap-4 pt-6 pb-6 max-w-[720px]">
<div class="flex items-center justify-between gap-4">
@@ -435,6 +429,6 @@ export const SettingsKeybinds: Component = () => {
</div>
</Show>
</div>
</ScrollFade>
</div>
)
}
+12 -23
View File
@@ -132,14 +132,12 @@ export function Titlebar() {
}
return (
<header
class="h-10 shrink-0 bg-background-base relative grid grid-cols-[auto_minmax(0,1fr)_auto] items-center"
data-tauri-drag-region
>
<header class="h-10 shrink-0 bg-background-base flex items-center relative" data-tauri-drag-region>
<div
classList={{
"flex items-center min-w-0": true,
"flex items-center w-full min-w-0": true,
"pl-2": !mac(),
"pr-6": !windows(),
}}
onMouseDown={drag}
data-tauri-drag-region
@@ -220,29 +218,20 @@ export function Titlebar() {
</div>
</div>
<div id="opencode-titlebar-left" class="flex items-center gap-3 min-w-0 px-2" data-tauri-drag-region />
</div>
<div
class="min-w-0 flex items-center justify-center pointer-events-none lg:absolute lg:inset-0 lg:flex lg:items-center lg:justify-center"
data-tauri-drag-region
>
<div id="opencode-titlebar-center" class="pointer-events-auto w-full min-w-0 flex justify-center lg:w-fit" />
</div>
<div
classList={{
"flex items-center min-w-0 justify-end": true,
"pr-6": !windows(),
}}
onMouseDown={drag}
data-tauri-drag-region
>
<div id="opencode-titlebar-right" class="flex items-center gap-3 shrink-0 justify-end" data-tauri-drag-region />
<div class="flex-1 h-full" data-tauri-drag-region />
<div
id="opencode-titlebar-right"
class="flex items-center gap-3 shrink-0 flex-1 justify-end"
data-tauri-drag-region
/>
<Show when={windows()}>
<div class="w-6 shrink-0" />
<div data-tauri-decorum-tb class="flex flex-row" />
</Show>
</div>
<div class="absolute inset-0 flex items-center justify-center pointer-events-none">
<div id="opencode-titlebar-center" class="pointer-events-auto" />
</div>
</header>
)
}
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@opencode-ai/console-app",
"version": "1.1.47",
"version": "0.0.0-ci-202601291718",
"type": "module",
"license": "MIT",
"scripts": {
+1 -1
View File
@@ -1,7 +1,7 @@
{
"$schema": "https://json.schemastore.org/package.json",
"name": "@opencode-ai/console-core",
"version": "1.1.47",
"version": "0.0.0-ci-202601291718",
"private": true,
"type": "module",
"license": "MIT",
@@ -2,21 +2,20 @@
import { $ } from "bun"
import path from "path"
import os from "os"
import { ZenData } from "../src/model"
const stage = process.argv[2]
if (!stage) throw new Error("Stage is required")
const root = path.resolve(process.cwd(), "..", "..", "..")
const PARTS = 10
const PARTS = 8
// read the secret
const ret = await $`bun sst secret list`.cwd(root).text()
const lines = ret.split("\n")
const values = Array.from({ length: PARTS }, (_, i) => {
const value = lines
.find((line) => line.startsWith(`ZEN_MODELS${i + 1}=`))
.find((line) => line.startsWith(`ZEN_MODELS${i + 1}`))
?.split("=")
.slice(1)
.join("=")
@@ -28,6 +27,6 @@ const values = Array.from({ length: PARTS }, (_, i) => {
ZenData.validate(JSON.parse(values.join("")))
// update the secret
const envFile = Bun.file(path.join(os.tmpdir(), `models-${Date.now()}.env`))
await envFile.write(values.map((v, i) => `ZEN_MODELS${i + 1}=${v}`).join("\n"))
await $`bun sst secret load ${envFile.name} --stage ${stage}`.cwd(root)
for (let i = 0; i < PARTS; i++) {
await $`bun sst secret set ZEN_MODELS${i + 1} --stage ${stage} -- ${values[i]}`
}
+5 -6
View File
@@ -2,21 +2,20 @@
import { $ } from "bun"
import path from "path"
import os from "os"
import { ZenData } from "../src/model"
const stage = process.argv[2]
if (!stage) throw new Error("Stage is required")
const root = path.resolve(process.cwd(), "..", "..", "..")
const PARTS = 10
const PARTS = 8
// read the secret
const ret = await $`bun sst secret list --stage ${stage}`.cwd(root).text()
const lines = ret.split("\n")
const values = Array.from({ length: PARTS }, (_, i) => {
const value = lines
.find((line) => line.startsWith(`ZEN_MODELS${i + 1}=`))
.find((line) => line.startsWith(`ZEN_MODELS${i + 1}`))
?.split("=")
.slice(1)
.join("=")
@@ -28,6 +27,6 @@ const values = Array.from({ length: PARTS }, (_, i) => {
ZenData.validate(JSON.parse(values.join("")))
// update the secret
const envFile = Bun.file(path.join(os.tmpdir(), `models-${Date.now()}.env`))
await envFile.write(values.map((v, i) => `ZEN_MODELS${i + 1}=${v}`).join("\n"))
await $`bun sst secret load ${envFile.name}`.cwd(root)
for (let i = 0; i < PARTS; i++) {
await $`bun sst secret set ZEN_MODELS${i + 1} -- ${values[i]}`
}
@@ -7,20 +7,18 @@ import { ZenData } from "../src/model"
const root = path.resolve(process.cwd(), "..", "..", "..")
const models = await $`bun sst secret list`.cwd(root).text()
const PARTS = 10
const PARTS = 8
// read the line starting with "ZEN_MODELS"
const lines = models.split("\n")
const oldValues = Array.from({ length: PARTS }, (_, i) => {
const value = lines
.find((line) => line.startsWith(`ZEN_MODELS${i + 1}=`))
.find((line) => line.startsWith(`ZEN_MODELS${i + 1}`))
?.split("=")
.slice(1)
.join("=")
// TODO
//if (!value) throw new Error(`ZEN_MODELS${i + 1} not found`)
//return value
return value ?? ""
if (!value) throw new Error(`ZEN_MODELS${i + 1} not found`)
return value
})
// store the prettified json to a temp file
@@ -40,6 +38,6 @@ const newValues = Array.from({ length: PARTS }, (_, i) =>
newValue.slice(chunk * i, i === PARTS - 1 ? undefined : chunk * (i + 1)),
)
const envFile = Bun.file(path.join(os.tmpdir(), `models-${Date.now()}.env`))
await envFile.write(newValues.map((v, i) => `ZEN_MODELS${i + 1}=${v}`).join("\n"))
await $`bun sst secret load ${envFile.name}`.cwd(root)
for (let i = 0; i < PARTS; i++) {
await $`bun sst secret set ZEN_MODELS${i + 1} -- ${newValues[i]}`
}
+1 -3
View File
@@ -75,9 +75,7 @@ export namespace ZenData {
Resource.ZEN_MODELS5.value +
Resource.ZEN_MODELS6.value +
Resource.ZEN_MODELS7.value +
Resource.ZEN_MODELS8.value +
Resource.ZEN_MODELS9.value +
Resource.ZEN_MODELS10.value,
Resource.ZEN_MODELS8.value,
)
return ModelsSchema.parse(json)
})
-8
View File
@@ -133,10 +133,6 @@ declare module "sst" {
"type": "sst.sst.Secret"
"value": string
}
"ZEN_MODELS10": {
"type": "sst.sst.Secret"
"value": string
}
"ZEN_MODELS2": {
"type": "sst.sst.Secret"
"value": string
@@ -165,10 +161,6 @@ declare module "sst" {
"type": "sst.sst.Secret"
"value": string
}
"ZEN_MODELS9": {
"type": "sst.sst.Secret"
"value": string
}
"ZEN_SESSION_SECRET": {
"type": "sst.sst.Secret"
"value": string
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@opencode-ai/console-function",
"version": "1.1.47",
"version": "0.0.0-ci-202601291718",
"$schema": "https://json.schemastore.org/package.json",
"private": true,
"type": "module",
-8
View File
@@ -133,10 +133,6 @@ declare module "sst" {
"type": "sst.sst.Secret"
"value": string
}
"ZEN_MODELS10": {
"type": "sst.sst.Secret"
"value": string
}
"ZEN_MODELS2": {
"type": "sst.sst.Secret"
"value": string
@@ -165,10 +161,6 @@ declare module "sst" {
"type": "sst.sst.Secret"
"value": string
}
"ZEN_MODELS9": {
"type": "sst.sst.Secret"
"value": string
}
"ZEN_SESSION_SECRET": {
"type": "sst.sst.Secret"
"value": string
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@opencode-ai/console-mail",
"version": "1.1.47",
"version": "0.0.0-ci-202601291718",
"dependencies": {
"@jsx-email/all": "2.2.3",
"@jsx-email/cli": "1.4.3",
-8
View File
@@ -133,10 +133,6 @@ declare module "sst" {
"type": "sst.sst.Secret"
"value": string
}
"ZEN_MODELS10": {
"type": "sst.sst.Secret"
"value": string
}
"ZEN_MODELS2": {
"type": "sst.sst.Secret"
"value": string
@@ -165,10 +161,6 @@ declare module "sst" {
"type": "sst.sst.Secret"
"value": string
}
"ZEN_MODELS9": {
"type": "sst.sst.Secret"
"value": string
}
"ZEN_SESSION_SECRET": {
"type": "sst.sst.Secret"
"value": string
-38
View File
@@ -1,38 +0,0 @@
# CI containers
Prebuilt images intended to speed up GitHub Actions jobs by baking in
large, slow-to-install dependencies. These are designed for Linux jobs
that can use `job.container` in workflows.
Images
- `base`: Ubuntu 24.04 with common build tools and utilities
- `bun-node`: `base` plus Bun and Node.js 24
- `rust`: `bun-node` plus Rust (stable, minimal profile)
- `tauri-linux`: `rust` plus Tauri Linux build dependencies
- `publish`: `bun-node` plus Docker CLI and AUR tooling
Build
```
REGISTRY=ghcr.io/anomalyco TAG=24.04 bun ./packages/containers/script/build.ts
REGISTRY=ghcr.io/anomalyco TAG=24.04 bun ./packages/containers/script/build.ts --push
```
Workflow usage
```
jobs:
build-cli:
runs-on: ubuntu-latest
container:
image: ghcr.io/anomalyco/build/bun-node:24.04
```
Notes
- These images only help Linux jobs. macOS and Windows jobs cannot run
inside Linux containers.
- `--push` publishes multi-arch (amd64 + arm64) images using Buildx.
- If a job uses Docker Buildx, the container needs access to the host
Docker daemon (or `docker-in-docker` with privileged mode).
-18
View File
@@ -1,18 +0,0 @@
FROM ubuntu:24.04
ARG DEBIAN_FRONTEND=noninteractive
RUN apt-get update \
&& apt-get install -y --no-install-recommends \
build-essential \
ca-certificates \
curl \
git \
jq \
openssh-client \
pkg-config \
python3 \
unzip \
xz-utils \
zip \
&& rm -rf /var/lib/apt/lists/*
-24
View File
@@ -1,24 +0,0 @@
ARG REGISTRY=ghcr.io/anomalyco
FROM ${REGISTRY}/build/base:24.04
SHELL ["/bin/bash", "-lc"]
ARG NODE_VERSION=24.4.0
ARG BUN_VERSION=1.3.5
ENV BUN_INSTALL=/opt/bun
ENV PATH=/opt/bun/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
RUN set -euo pipefail; \
arch=$(uname -m); \
node_arch=x64; \
if [ "$arch" = "aarch64" ]; then node_arch=arm64; fi; \
curl -fsSL "https://nodejs.org/dist/v${NODE_VERSION}/node-v${NODE_VERSION}-linux-${node_arch}.tar.xz" \
| tar -xJf - -C /usr/local --strip-components=1; \
corepack enable
RUN set -euo pipefail; \
curl -fsSL https://bun.sh/install | bash -s -- "bun-v${BUN_VERSION}"; \
bun --version; \
node --version; \
npm --version
-10
View File
@@ -1,10 +0,0 @@
ARG REGISTRY=ghcr.io/anomalyco
FROM ${REGISTRY}/build/bun-node:24.04
ARG DEBIAN_FRONTEND=noninteractive
RUN apt-get update \
&& apt-get install -y --no-install-recommends \
docker.io \
pacman-package-manager \
&& rm -rf /var/lib/apt/lists/*
-13
View File
@@ -1,13 +0,0 @@
ARG REGISTRY=ghcr.io/anomalyco
FROM ${REGISTRY}/build/bun-node:24.04
ARG RUST_TOOLCHAIN=stable
ENV CARGO_HOME=/opt/cargo
ENV RUSTUP_HOME=/opt/rustup
ENV PATH=/opt/cargo/bin:/opt/bun/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
RUN set -euo pipefail; \
curl -fsSL https://sh.rustup.rs | sh -s -- -y --profile minimal --default-toolchain "${RUST_TOOLCHAIN}"; \
rustc --version; \
cargo --version
-77
View File
@@ -1,77 +0,0 @@
#!/usr/bin/env bun
import { $ } from "bun"
import path from "path"
import { fileURLToPath } from "url"
const rootDir = fileURLToPath(new URL("../../..", import.meta.url))
process.chdir(rootDir)
const reg = process.env.REGISTRY ?? "ghcr.io/anomalyco"
const tag = process.env.TAG ?? "24.04"
const push = process.argv.includes("--push") || process.env.PUSH === "1"
const root = path.join(rootDir, "package.json")
const pkg = await Bun.file(root).json()
const manager = pkg.packageManager ?? ""
const bun = manager.startsWith("bun@") ? manager.slice(4) : ""
if (!bun) throw new Error("packageManager must be bun@<version>")
const images = ["base", "bun-node", "rust", "tauri-linux", "publish"]
const setup = async () => {
if (!push) return
const list = await $`docker buildx ls`.text()
if (list.includes("opencode")) {
await $`docker buildx use opencode`
return
}
await $`docker buildx create --name opencode --use`
}
await setup()
const platform = "linux/amd64,linux/arm64"
for (const name of images) {
const image = `${reg}/build/${name}:${tag}`
const file = `packages/containers/${name}/Dockerfile`
if (name === "base") {
if (push) {
console.log(`docker buildx build --platform ${platform} -f ${file} -t ${image} --push .`)
await $`docker buildx build --platform ${platform} -f ${file} -t ${image} --push .`
}
if (!push) {
console.log(`docker build -f ${file} -t ${image} .`)
await $`docker build -f ${file} -t ${image} .`
}
}
if (name === "bun-node") {
if (push) {
console.log(
`docker buildx build --platform ${platform} -f ${file} -t ${image} --build-arg REGISTRY=${reg} --build-arg BUN_VERSION=${bun} --push .`,
)
await $`docker buildx build --platform ${platform} -f ${file} -t ${image} --build-arg REGISTRY=${reg} --build-arg BUN_VERSION=${bun} --push .`
}
if (!push) {
console.log(`docker build -f ${file} -t ${image} --build-arg REGISTRY=${reg} --build-arg BUN_VERSION=${bun} .`)
await $`docker build -f ${file} -t ${image} --build-arg REGISTRY=${reg} --build-arg BUN_VERSION=${bun} .`
}
}
if (name !== "base" && name !== "bun-node") {
if (push) {
console.log(
`docker buildx build --platform ${platform} -f ${file} -t ${image} --build-arg REGISTRY=${reg} --push .`,
)
await $`docker buildx build --platform ${platform} -f ${file} -t ${image} --build-arg REGISTRY=${reg} --push .`
}
if (!push) {
console.log(`docker build -f ${file} -t ${image} --build-arg REGISTRY=${reg} .`)
await $`docker build -f ${file} -t ${image} --build-arg REGISTRY=${reg} .`
}
}
if (push) {
console.log(`pushed ${image}`)
}
}
@@ -1,12 +0,0 @@
ARG REGISTRY=ghcr.io/anomalyco
FROM ${REGISTRY}/build/rust:24.04
ARG DEBIAN_FRONTEND=noninteractive
RUN apt-get update \
&& apt-get install -y --no-install-recommends \
libappindicator3-dev \
libwebkit2gtk-4.1-dev \
librsvg2-dev \
patchelf \
&& rm -rf /var/lib/apt/lists/*
-8
View File
@@ -1,8 +0,0 @@
{
"$schema": "https://json.schemastore.org/tsconfig",
"extends": "@tsconfig/bun/tsconfig.json",
"compilerOptions": {
"lib": ["ESNext", "DOM", "DOM.Iterable"],
"noUncheckedIndexedAccess": false
}
}
+1 -1
View File
@@ -1,7 +1,7 @@
{
"name": "@opencode-ai/desktop",
"private": true,
"version": "1.1.47",
"version": "0.0.0-ci-202601291718",
"type": "module",
"license": "MIT",
"scripts": {
-6
View File
@@ -1,14 +1,8 @@
#!/usr/bin/env bun
import { $ } from "bun"
import { Script } from "@opencode-ai/script"
import { copyBinaryToSidecarFolder, getCurrentSidecar, windowsify } from "./utils"
const pkg = await Bun.file("./package.json").json()
pkg.version = Script.version
await Bun.write("./package.json", JSON.stringify(pkg, null, 2) + "\n")
console.log(`Updated package.json version to ${Script.version}`)
const sidecarConfig = getCurrentSidecar()
const dir = "src-tauri/target/opencode-binaries"
+4 -4
View File
@@ -4914,9 +4914,9 @@ dependencies = [
[[package]]
name = "tauri-plugin-fs"
version = "2.4.5"
version = "2.4.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ed390cc669f937afeb8b28032ce837bac8ea023d975a2e207375ec05afaf1804"
checksum = "47df422695255ecbe7bac7012440eddaeefd026656171eac9559f5243d3230d9"
dependencies = [
"anyhow",
"dunce",
@@ -4936,9 +4936,9 @@ dependencies = [
[[package]]
name = "tauri-plugin-http"
version = "2.5.6"
version = "2.5.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "68bef611ccbfbce67c813959c11b23c1c084d201aa94222de9eba5f9edc3f897"
checksum = "c00685aceab12643cf024f712ab0448ba8fcadf86f2391d49d2e5aa732aacc70"
dependencies = [
"bytes",
"cookie_store",
+1 -1
View File
@@ -28,7 +28,7 @@ tauri-plugin-process = "2"
tauri-plugin-store = "2"
tauri-plugin-window-state = "2"
tauri-plugin-clipboard-manager = "2"
tauri-plugin-http = "2.5.6"
tauri-plugin-http = "2"
tauri-plugin-notification = "2"
tauri-plugin-single-instance = { version = "2", features = ["deep-link"] }
-1
View File
@@ -345,7 +345,6 @@ pub fn run() {
.decorations(false);
let window = window_builder.build().expect("Failed to create window");
let _ = window.show();
#[cfg(windows)]
let _ = window.create_overlay_titlebar();
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@opencode-ai/enterprise",
"version": "1.1.47",
"version": "0.0.0-ci-202601291718",
"private": true,
"type": "module",
"license": "MIT",
-8
View File
@@ -133,10 +133,6 @@ declare module "sst" {
"type": "sst.sst.Secret"
"value": string
}
"ZEN_MODELS10": {
"type": "sst.sst.Secret"
"value": string
}
"ZEN_MODELS2": {
"type": "sst.sst.Secret"
"value": string
@@ -165,10 +161,6 @@ declare module "sst" {
"type": "sst.sst.Secret"
"value": string
}
"ZEN_MODELS9": {
"type": "sst.sst.Secret"
"value": string
}
"ZEN_SESSION_SECRET": {
"type": "sst.sst.Secret"
"value": string
+6 -6
View File
@@ -1,7 +1,7 @@
id = "opencode"
name = "OpenCode"
description = "The open source coding agent."
version = "1.1.47"
version = "0.0.0-ci-202601291718"
schema_version = 1
authors = ["Anomaly"]
repository = "https://github.com/anomalyco/opencode"
@@ -11,26 +11,26 @@ name = "OpenCode"
icon = "./icons/opencode.svg"
[agent_servers.opencode.targets.darwin-aarch64]
archive = "https://github.com/anomalyco/opencode/releases/download/v1.1.47/opencode-darwin-arm64.zip"
archive = "https://github.com/anomalyco/opencode/releases/download/v0.0.0-ci-202601291718/opencode-darwin-arm64.zip"
cmd = "./opencode"
args = ["acp"]
[agent_servers.opencode.targets.darwin-x86_64]
archive = "https://github.com/anomalyco/opencode/releases/download/v1.1.47/opencode-darwin-x64.zip"
archive = "https://github.com/anomalyco/opencode/releases/download/v0.0.0-ci-202601291718/opencode-darwin-x64.zip"
cmd = "./opencode"
args = ["acp"]
[agent_servers.opencode.targets.linux-aarch64]
archive = "https://github.com/anomalyco/opencode/releases/download/v1.1.47/opencode-linux-arm64.tar.gz"
archive = "https://github.com/anomalyco/opencode/releases/download/v0.0.0-ci-202601291718/opencode-linux-arm64.tar.gz"
cmd = "./opencode"
args = ["acp"]
[agent_servers.opencode.targets.linux-x86_64]
archive = "https://github.com/anomalyco/opencode/releases/download/v1.1.47/opencode-linux-x64.tar.gz"
archive = "https://github.com/anomalyco/opencode/releases/download/v0.0.0-ci-202601291718/opencode-linux-x64.tar.gz"
cmd = "./opencode"
args = ["acp"]
[agent_servers.opencode.targets.windows-x86_64]
archive = "https://github.com/anomalyco/opencode/releases/download/v1.1.47/opencode-windows-x64.zip"
archive = "https://github.com/anomalyco/opencode/releases/download/v0.0.0-ci-202601291718/opencode-windows-x64.zip"
cmd = "./opencode.exe"
args = ["acp"]
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@opencode-ai/function",
"version": "1.1.47",
"version": "0.0.0-ci-202601291718",
"$schema": "https://json.schemastore.org/package.json",
"private": true,
"type": "module",
-8
View File
@@ -133,10 +133,6 @@ declare module "sst" {
"type": "sst.sst.Secret"
"value": string
}
"ZEN_MODELS10": {
"type": "sst.sst.Secret"
"value": string
}
"ZEN_MODELS2": {
"type": "sst.sst.Secret"
"value": string
@@ -165,10 +161,6 @@ declare module "sst" {
"type": "sst.sst.Secret"
"value": string
}
"ZEN_MODELS9": {
"type": "sst.sst.Secret"
"value": string
}
"ZEN_SESSION_SECRET": {
"type": "sst.sst.Secret"
"value": string
+2 -2
View File
@@ -1,6 +1,6 @@
{
"$schema": "https://json.schemastore.org/package.json",
"version": "1.1.47",
"version": "0.0.0-ci-202601291718",
"name": "opencode",
"type": "module",
"license": "MIT",
@@ -49,7 +49,7 @@
"dependencies": {
"@actions/core": "1.11.1",
"@actions/github": "6.0.1",
"@agentclientprotocol/sdk": "0.13.0",
"@agentclientprotocol/sdk": "0.12.0",
"@ai-sdk/amazon-bedrock": "3.0.73",
"@ai-sdk/anthropic": "2.0.57",
"@ai-sdk/azure": "2.0.91",
+1 -7
View File
@@ -230,14 +230,8 @@ if (!Script.preview) {
"",
].join("\n")
const token = process.env.GITHUB_TOKEN
if (!token) {
console.error("GITHUB_TOKEN is required to update homebrew tap")
process.exit(1)
}
const tap = `https://x-access-token:${token}@github.com/anomalyco/homebrew-tap.git`
await $`rm -rf ./dist/homebrew-tap`
await $`git clone ${tap} ./dist/homebrew-tap`
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${Script.version}"`
+56 -186
View File
@@ -26,7 +26,6 @@ import {
type ToolCallContent,
type ToolKind,
} from "@agentclientprotocol/sdk"
import { Log } from "../util/log"
import { ACPSessionManager } from "./session"
import type { ACPConfig } from "./types"
@@ -41,11 +40,6 @@ import { LoadAPIKeyError } from "ai"
import type { Event, OpencodeClient, SessionMessageResponse } from "@opencode-ai/sdk/v2"
import { applyPatch } from "diff"
type ModeOption = { id: string; name: string; description?: string }
type ModelOption = { modelId: string; name: string }
const DEFAULT_VARIANT_VALUE = "default"
export namespace ACP {
const log = Log.create({ service: "acp-agent" })
@@ -482,7 +476,7 @@ export namespace ACP {
sessionId,
models: load.models,
modes: load.modes,
_meta: load._meta,
_meta: {},
}
} catch (e) {
const error = MessageV2.fromError(e, {
@@ -535,7 +529,7 @@ export namespace ACP {
providerID: lastUser.model.providerID,
modelID: lastUser.model.modelID,
})
if (result.modes?.availableModes.some((m) => m.id === lastUser.agent)) {
if (result.modes.availableModes.some((m) => m.id === lastUser.agent)) {
result.modes.currentModeId = lastUser.agent
this.sessionManager.setMode(sessionId, lastUser.agent)
}
@@ -962,7 +956,27 @@ export namespace ACP {
}
}
private async loadAvailableModes(directory: string): Promise<ModeOption[]> {
private async loadSessionMode(params: LoadSessionRequest) {
const directory = params.cwd
const model = await defaultModel(this.config, directory)
const sessionId = params.sessionId
const providers = await this.sdk.config.providers({ directory }).then((x) => x.data!.providers)
const entries = providers.sort((a, b) => {
const nameA = a.name.toLowerCase()
const nameB = b.name.toLowerCase()
if (nameA < nameB) return -1
if (nameA > nameB) return 1
return 0
})
const availableModels = entries.flatMap((provider) => {
const models = Provider.sort(Object.values(provider.models))
return models.map((model) => ({
modelId: `${provider.id}/${model.id}`,
name: `${provider.name}/${model.name}`,
}))
})
const agents = await this.config.sdk.app
.agents(
{
@@ -972,56 +986,6 @@ export namespace ACP {
)
.then((resp) => resp.data!)
return agents
.filter((agent) => agent.mode !== "subagent" && !agent.hidden)
.map((agent) => ({
id: agent.name,
name: agent.name,
description: agent.description,
}))
}
private async resolveModeState(
directory: string,
sessionId: string,
): Promise<{ availableModes: ModeOption[]; currentModeId?: string }> {
const availableModes = await this.loadAvailableModes(directory)
const currentModeId =
this.sessionManager.get(sessionId).modeId ||
(await (async () => {
if (!availableModes.length) return undefined
const defaultAgentName = await AgentModule.defaultAgent()
const resolvedModeId =
availableModes.find((mode) => mode.name === defaultAgentName)?.id ?? availableModes[0].id
this.sessionManager.setMode(sessionId, resolvedModeId)
return resolvedModeId
})())
return { availableModes, currentModeId }
}
private async loadSessionMode(params: LoadSessionRequest) {
const directory = params.cwd
const model = await defaultModel(this.config, directory)
const sessionId = params.sessionId
const providers = await this.sdk.config.providers({ directory }).then((x) => x.data!.providers)
const entries = sortProvidersByName(providers)
const availableVariants = modelVariantsFromProviders(entries, model)
const currentVariant = this.sessionManager.getVariant(sessionId)
if (currentVariant && !availableVariants.includes(currentVariant)) {
this.sessionManager.setVariant(sessionId, undefined)
}
const availableModels = buildAvailableModels(entries, { includeVariants: true })
const modeState = await this.resolveModeState(directory, sessionId)
const currentModeId = modeState.currentModeId
const modes = currentModeId
? {
availableModes: modeState.availableModes,
currentModeId,
}
: undefined
const commands = await this.config.sdk.command
.list(
{
@@ -1042,6 +1006,20 @@ export namespace ACP {
description: "compact the session",
})
const availableModes = agents
.filter((agent) => agent.mode !== "subagent" && !agent.hidden)
.map((agent) => ({
id: agent.name,
name: agent.name,
description: agent.description,
}))
const defaultAgentName = await AgentModule.defaultAgent()
const currentModeId = availableModes.find((m) => m.name === defaultAgentName)?.id ?? availableModes[0].id
// Persist the default mode so prompt() uses it immediately
this.sessionManager.setMode(sessionId, currentModeId)
const mcpServers: Record<string, Config.Mcp> = {}
for (const server of params.mcpServers) {
if ("type" in server) {
@@ -1095,46 +1073,40 @@ export namespace ACP {
return {
sessionId,
models: {
currentModelId: formatModelIdWithVariant(model, currentVariant, availableVariants, true),
currentModelId: `${model.providerID}/${model.modelID}`,
availableModels,
},
modes,
_meta: buildVariantMeta({
model,
variant: this.sessionManager.getVariant(sessionId),
availableVariants,
}),
modes: {
availableModes,
currentModeId,
},
_meta: {},
}
}
async unstable_setSessionModel(params: SetSessionModelRequest) {
const session = this.sessionManager.get(params.sessionId)
const providers = await this.sdk.config
.providers({ directory: session.cwd }, { throwOnError: true })
.then((x) => x.data!.providers)
const selection = parseModelSelection(params.modelId, providers)
this.sessionManager.setModel(session.id, selection.model)
this.sessionManager.setVariant(session.id, selection.variant)
const model = Provider.parseModel(params.modelId)
const entries = sortProvidersByName(providers)
const availableVariants = modelVariantsFromProviders(entries, selection.model)
this.sessionManager.setModel(session.id, {
providerID: model.providerID,
modelID: model.modelID,
})
return {
_meta: buildVariantMeta({
model: selection.model,
variant: selection.variant,
availableVariants,
}),
_meta: {},
}
}
async setSessionMode(params: SetSessionModeRequest): Promise<SetSessionModeResponse | void> {
const session = this.sessionManager.get(params.sessionId)
const availableModes = await this.loadAvailableModes(session.cwd)
if (!availableModes.some((mode) => mode.id === params.modeId)) {
throw new Error(`Agent not found: ${params.modeId}`)
}
this.sessionManager.get(params.sessionId)
await this.config.sdk.app
.agents({}, { throwOnError: true })
.then((x) => x.data)
.then((agent) => {
if (!agent) throw new Error(`Agent not found: ${params.modeId}`)
})
this.sessionManager.setMode(params.sessionId, params.modeId)
}
@@ -1251,7 +1223,6 @@ export namespace ACP {
providerID: model.providerID,
modelID: model.modelID,
},
variant: this.sessionManager.getVariant(sessionID),
parts,
agent,
directory,
@@ -1463,105 +1434,4 @@ export namespace ACP {
}
return result
}
function sortProvidersByName<T extends { name: string }>(providers: T[]): T[] {
return [...providers].sort((a, b) => {
const nameA = a.name.toLowerCase()
const nameB = b.name.toLowerCase()
if (nameA < nameB) return -1
if (nameA > nameB) return 1
return 0
})
}
function modelVariantsFromProviders(
providers: Array<{ id: string; models: Record<string, { variants?: Record<string, any> }> }>,
model: { providerID: string; modelID: string },
): string[] {
const provider = providers.find((entry) => entry.id === model.providerID)
if (!provider) return []
const modelInfo = provider.models[model.modelID]
if (!modelInfo?.variants) return []
return Object.keys(modelInfo.variants)
}
function buildAvailableModels(
providers: Array<{ id: string; name: string; models: Record<string, any> }>,
options: { includeVariants?: boolean } = {},
): ModelOption[] {
const includeVariants = options.includeVariants ?? false
return providers.flatMap((provider) => {
const models = Provider.sort(Object.values(provider.models) as any)
return models.flatMap((model) => {
const base: ModelOption = {
modelId: `${provider.id}/${model.id}`,
name: `${provider.name}/${model.name}`,
}
if (!includeVariants || !model.variants) return [base]
const variants = Object.keys(model.variants).filter((variant) => variant !== DEFAULT_VARIANT_VALUE)
const variantOptions = variants.map((variant) => ({
modelId: `${provider.id}/${model.id}/${variant}`,
name: `${provider.name}/${model.name} (${variant})`,
}))
return [base, ...variantOptions]
})
})
}
function formatModelIdWithVariant(
model: { providerID: string; modelID: string },
variant: string | undefined,
availableVariants: string[],
includeVariant: boolean,
) {
const base = `${model.providerID}/${model.modelID}`
if (!includeVariant || !variant || !availableVariants.includes(variant)) return base
return `${base}/${variant}`
}
function buildVariantMeta(input: {
model: { providerID: string; modelID: string }
variant?: string
availableVariants: string[]
}) {
return {
opencode: {
modelId: `${input.model.providerID}/${input.model.modelID}`,
variant: input.variant ?? null,
availableVariants: input.availableVariants,
},
}
}
function parseModelSelection(
modelId: string,
providers: Array<{ id: string; models: Record<string, { variants?: Record<string, any> }> }>,
): { model: { providerID: string; modelID: string }; variant?: string } {
const parsed = Provider.parseModel(modelId)
const provider = providers.find((p) => p.id === parsed.providerID)
if (!provider) {
return { model: parsed, variant: undefined }
}
// Check if modelID exists directly
if (provider.models[parsed.modelID]) {
return { model: parsed, variant: undefined }
}
// Try to extract variant from end of modelID (e.g., "claude-sonnet-4/high" -> model: "claude-sonnet-4", variant: "high")
const segments = parsed.modelID.split("/")
if (segments.length > 1) {
const candidateVariant = segments[segments.length - 1]
const baseModelId = segments.slice(0, -1).join("/")
const baseModelInfo = provider.models[baseModelId]
if (baseModelInfo?.variants && candidateVariant in baseModelInfo.variants) {
return {
model: { providerID: parsed.providerID, modelID: baseModelId },
variant: candidateVariant,
}
}
}
return { model: parsed, variant: undefined }
}
}
-12
View File
@@ -96,18 +96,6 @@ export class ACPSessionManager {
return session
}
getVariant(sessionId: string) {
const session = this.get(sessionId)
return session.variant
}
setVariant(sessionId: string, variant?: string) {
const session = this.get(sessionId)
session.variant = variant
this.sessions.set(sessionId, session)
return session
}
setMode(sessionId: string, modeId: string) {
const session = this.get(sessionId)
session.modeId = modeId
-1
View File
@@ -10,7 +10,6 @@ export interface ACPSessionState {
providerID: string
modelID: string
}
variant?: string
modeId?: string
}
@@ -158,7 +158,7 @@ export function DialogModel(props: { providerID?: string }) {
(item) => item.providerID === value.providerID && item.modelID === value.modelID,
)
if (inFavorites) return false
const inRecents = recents.some(
const inRecents = recentList.some(
(item) => item.providerID === value.providerID && item.modelID === value.modelID,
)
if (inRecents) return false
+24 -26
View File
@@ -32,21 +32,6 @@ import { Event } from "../server/event"
export namespace Config {
const log = Log.create({ service: "config" })
// Managed settings directory for enterprise deployments (highest priority, admin-controlled)
// These settings override all user and project settings
function getManagedConfigDir(): string {
switch (process.platform) {
case "darwin":
return "/Library/Application Support/opencode"
case "win32":
return path.join(process.env.ProgramData || "C:\\ProgramData", "opencode")
default:
return "/etc/opencode"
}
}
const managedConfigDir = process.env.OPENCODE_TEST_MANAGED_CONFIG_DIR || getManagedConfigDir()
// Custom merge function that concatenates array fields instead of replacing them
function mergeConfigConcatArrays(target: Info, source: Info): Info {
const merged = mergeDeep(target, source)
@@ -163,18 +148,8 @@ export namespace Config {
result.plugin.push(...(await loadPlugin(dir)))
}
// Load managed config files last (highest priority) - enterprise admin-controlled
// Kept separate from directories array to avoid write operations when installing plugins
// which would fail on system directories requiring elevated permissions
// This way it only loads config file and not skills/plugins/commands
if (existsSync(managedConfigDir)) {
for (const file of ["opencode.jsonc", "opencode.json"]) {
result = mergeConfigConcatArrays(result, await loadFile(path.join(managedConfigDir, file)))
}
}
// Migrate deprecated mode field to agent field
for (const [name, mode] of Object.entries(result.mode ?? {})) {
for (const [name, mode] of Object.entries(result.mode)) {
result.agent = mergeDeep(result.agent ?? {}, {
[name]: {
...mode,
@@ -1078,6 +1053,29 @@ export namespace Config {
.optional(),
experimental: z
.object({
hook: z
.object({
file_edited: z
.record(
z.string(),
z
.object({
command: z.string().array(),
environment: z.record(z.string(), z.string()).optional(),
})
.array(),
)
.optional(),
session_completed: z
.object({
command: z.string().array(),
environment: z.record(z.string(), z.string()).optional(),
})
.array()
.optional(),
})
.optional(),
chatMaxRetries: z.number().optional().describe("Number of retries for chat completions on failure"),
disable_paste_summary: z.boolean().optional(),
batch_tool: z.boolean().optional().describe("Enable the batch tool"),
openTelemetry: z
+1 -1
View File
@@ -135,7 +135,7 @@ export namespace MCP {
return client.callTool(
{
name: mcpTool.name,
arguments: (args || {}) as Record<string, unknown>,
arguments: args as Record<string, unknown>,
},
CallToolResultSchema,
{
+3
View File
@@ -1,3 +1,4 @@
import os from "os"
import { Installation } from "@/installation"
import { Provider } from "@/provider/provider"
import { Log } from "@/util/log"
@@ -8,6 +9,7 @@ import {
type StreamTextResult,
type Tool,
type ToolSet,
extractReasoningMiddleware,
tool,
jsonSchema,
} from "ai"
@@ -259,6 +261,7 @@ export namespace LLM {
return args.params
},
},
extractReasoningMiddleware({ tagName: "think", startWithReasoning: false }),
],
}),
experimental_telemetry: {
+65 -142
View File
@@ -1,4 +1,4 @@
import { test, expect, describe, mock, afterEach } from "bun:test"
import { test, expect, describe, mock } from "bun:test"
import { Config } from "../../src/config/config"
import { Instance } from "../../src/project/instance"
import { Auth } from "../../src/auth"
@@ -6,23 +6,6 @@ import { tmpdir } from "../fixture/fixture"
import path from "path"
import fs from "fs/promises"
import { pathToFileURL } from "url"
import { Global } from "../../src/global"
// Get managed config directory from environment (set in preload.ts)
const managedConfigDir = process.env.OPENCODE_TEST_MANAGED_CONFIG_DIR!
afterEach(async () => {
await fs.rm(managedConfigDir, { force: true, recursive: true }).catch(() => {})
})
async function writeManagedSettings(settings: object, filename = "opencode.json") {
await fs.mkdir(managedConfigDir, { recursive: true })
await Bun.write(path.join(managedConfigDir, filename), JSON.stringify(settings))
}
async function writeConfig(dir: string, config: object, name = "opencode.json") {
await Bun.write(path.join(dir, name), JSON.stringify(config))
}
test("loads config with defaults when no files exist", async () => {
await using tmp = await tmpdir()
@@ -38,11 +21,14 @@ test("loads config with defaults when no files exist", async () => {
test("loads JSON config file", async () => {
await using tmp = await tmpdir({
init: async (dir) => {
await writeConfig(dir, {
$schema: "https://opencode.ai/config.json",
model: "test/model",
username: "testuser",
})
await Bun.write(
path.join(dir, "opencode.json"),
JSON.stringify({
$schema: "https://opencode.ai/config.json",
model: "test/model",
username: "testuser",
}),
)
},
})
await Instance.provide({
@@ -82,19 +68,21 @@ test("loads JSONC config file", async () => {
test("merges multiple config files with correct precedence", async () => {
await using tmp = await tmpdir({
init: async (dir) => {
await writeConfig(
dir,
{
await Bun.write(
path.join(dir, "opencode.jsonc"),
JSON.stringify({
$schema: "https://opencode.ai/config.json",
model: "base",
username: "base",
},
"opencode.jsonc",
}),
)
await Bun.write(
path.join(dir, "opencode.json"),
JSON.stringify({
$schema: "https://opencode.ai/config.json",
model: "override",
}),
)
await writeConfig(dir, {
$schema: "https://opencode.ai/config.json",
model: "override",
})
},
})
await Instance.provide({
@@ -114,10 +102,13 @@ test("handles environment variable substitution", async () => {
try {
await using tmp = await tmpdir({
init: async (dir) => {
await writeConfig(dir, {
$schema: "https://opencode.ai/config.json",
theme: "{env:TEST_VAR}",
})
await Bun.write(
path.join(dir, "opencode.json"),
JSON.stringify({
$schema: "https://opencode.ai/config.json",
theme: "{env:TEST_VAR}",
}),
)
},
})
await Instance.provide({
@@ -178,10 +169,13 @@ test("handles file inclusion substitution", async () => {
await using tmp = await tmpdir({
init: async (dir) => {
await Bun.write(path.join(dir, "included.txt"), "test_theme")
await writeConfig(dir, {
$schema: "https://opencode.ai/config.json",
theme: "{file:included.txt}",
})
await Bun.write(
path.join(dir, "opencode.json"),
JSON.stringify({
$schema: "https://opencode.ai/config.json",
theme: "{file:included.txt}",
}),
)
},
})
await Instance.provide({
@@ -196,10 +190,13 @@ test("handles file inclusion substitution", async () => {
test("validates config schema and throws on invalid fields", async () => {
await using tmp = await tmpdir({
init: async (dir) => {
await writeConfig(dir, {
$schema: "https://opencode.ai/config.json",
invalid_field: "should cause error",
})
await Bun.write(
path.join(dir, "opencode.json"),
JSON.stringify({
$schema: "https://opencode.ai/config.json",
invalid_field: "should cause error",
}),
)
},
})
await Instance.provide({
@@ -228,16 +225,19 @@ test("throws error for invalid JSON", async () => {
test("handles agent configuration", async () => {
await using tmp = await tmpdir({
init: async (dir) => {
await writeConfig(dir, {
$schema: "https://opencode.ai/config.json",
agent: {
test_agent: {
model: "test/model",
temperature: 0.7,
description: "test agent",
await Bun.write(
path.join(dir, "opencode.json"),
JSON.stringify({
$schema: "https://opencode.ai/config.json",
agent: {
test_agent: {
model: "test/model",
temperature: 0.7,
description: "test agent",
},
},
},
})
}),
)
},
})
await Instance.provide({
@@ -258,16 +258,19 @@ test("handles agent configuration", async () => {
test("handles command configuration", async () => {
await using tmp = await tmpdir({
init: async (dir) => {
await writeConfig(dir, {
$schema: "https://opencode.ai/config.json",
command: {
test_command: {
template: "test template",
description: "test command",
agent: "test_agent",
await Bun.write(
path.join(dir, "opencode.json"),
JSON.stringify({
$schema: "https://opencode.ai/config.json",
command: {
test_command: {
template: "test template",
description: "test command",
agent: "test_agent",
},
},
},
})
}),
)
},
})
await Instance.provide({
@@ -891,86 +894,6 @@ test("migrates legacy write tool to edit permission", async () => {
})
})
// Managed settings tests
// Note: preload.ts sets OPENCODE_TEST_MANAGED_CONFIG which Global.Path.managedConfig uses
test("managed settings override user settings", async () => {
await using tmp = await tmpdir({
init: async (dir) => {
await writeConfig(dir, {
$schema: "https://opencode.ai/config.json",
model: "user/model",
share: "auto",
username: "testuser",
})
},
})
await writeManagedSettings({
$schema: "https://opencode.ai/config.json",
model: "managed/model",
share: "disabled",
})
await Instance.provide({
directory: tmp.path,
fn: async () => {
const config = await Config.get()
expect(config.model).toBe("managed/model")
expect(config.share).toBe("disabled")
expect(config.username).toBe("testuser")
},
})
})
test("managed settings override project settings", async () => {
await using tmp = await tmpdir({
init: async (dir) => {
await writeConfig(dir, {
$schema: "https://opencode.ai/config.json",
autoupdate: true,
disabled_providers: [],
theme: "dark",
})
},
})
await writeManagedSettings({
$schema: "https://opencode.ai/config.json",
autoupdate: false,
disabled_providers: ["openai"],
})
await Instance.provide({
directory: tmp.path,
fn: async () => {
const config = await Config.get()
expect(config.autoupdate).toBe(false)
expect(config.disabled_providers).toEqual(["openai"])
expect(config.theme).toBe("dark")
},
})
})
test("missing managed settings file is not an error", async () => {
await using tmp = await tmpdir({
init: async (dir) => {
await writeConfig(dir, {
$schema: "https://opencode.ai/config.json",
model: "user/model",
})
},
})
await Instance.provide({
directory: tmp.path,
fn: async () => {
const config = await Config.get()
expect(config.model).toBe("user/model")
},
})
})
test("migrates legacy edit tool to edit permission", async () => {
await using tmp = await tmpdir({
init: async (dir) => {
-4
View File
@@ -17,10 +17,6 @@ const testHome = path.join(dir, "home")
await fs.mkdir(testHome, { recursive: true })
process.env["OPENCODE_TEST_HOME"] = testHome
// Set test managed config directory to isolate tests from system managed settings
const testManagedConfigDir = path.join(dir, "managed")
process.env["OPENCODE_TEST_MANAGED_CONFIG_DIR"] = testManagedConfigDir
process.env["XDG_DATA_HOME"] = path.join(dir, "share")
process.env["XDG_CACHE_HOME"] = path.join(dir, "cache")
process.env["XDG_CONFIG_HOME"] = path.join(dir, "config")
+1 -1
View File
@@ -1,7 +1,7 @@
{
"$schema": "https://json.schemastore.org/package.json",
"name": "@opencode-ai/plugin",
"version": "1.1.47",
"version": "0.0.0-ci-202601291718",
"type": "module",
"license": "MIT",
"scripts": {
+1 -1
View File
@@ -1,7 +1,7 @@
{
"$schema": "https://json.schemastore.org/package.json",
"name": "@opencode-ai/sdk",
"version": "1.1.47",
"version": "0.0.0-ci-202601291718",
"type": "module",
"license": "MIT",
"scripts": {
+20
View File
@@ -1779,6 +1779,26 @@ export type Config = {
prune?: boolean
}
experimental?: {
hook?: {
file_edited?: {
[key: string]: Array<{
command: Array<string>
environment?: {
[key: string]: string
}
}>
}
session_completed?: Array<{
command: Array<string>
environment?: {
[key: string]: string
}
}>
}
/**
* Number of retries for chat completions on failure
*/
chatMaxRetries?: number
disable_paste_summary?: boolean
/**
* Enable the batch tool
+63
View File
@@ -9834,6 +9834,69 @@
"experimental": {
"type": "object",
"properties": {
"hook": {
"type": "object",
"properties": {
"file_edited": {
"type": "object",
"propertyNames": {
"type": "string"
},
"additionalProperties": {
"type": "array",
"items": {
"type": "object",
"properties": {
"command": {
"type": "array",
"items": {
"type": "string"
}
},
"environment": {
"type": "object",
"propertyNames": {
"type": "string"
},
"additionalProperties": {
"type": "string"
}
}
},
"required": ["command"]
}
}
},
"session_completed": {
"type": "array",
"items": {
"type": "object",
"properties": {
"command": {
"type": "array",
"items": {
"type": "string"
}
},
"environment": {
"type": "object",
"propertyNames": {
"type": "string"
},
"additionalProperties": {
"type": "string"
}
}
},
"required": ["command"]
}
}
}
},
"chatMaxRetries": {
"description": "Number of retries for chat completions on failure",
"type": "number"
},
"disable_paste_summary": {
"type": "boolean"
},
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@opencode-ai/slack",
"version": "1.1.47",
"version": "0.0.0-ci-202601291718",
"type": "module",
"license": "MIT",
"scripts": {
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@opencode-ai/ui",
"version": "1.1.47",
"version": "0.0.0-ci-202601291718",
"type": "module",
"license": "MIT",
"exports": {
+32 -41
View File
@@ -36,9 +36,7 @@
border-radius: var(--radius-md);
overflow: clip;
color: var(--text-strong);
transition-property: background-color, border-color;
transition-duration: var(--transition-duration);
transition-timing-function: var(--transition-easing);
transition: background-color 0.15s ease;
/* text-12-regular */
font-family: var(--font-family-sans);
@@ -60,48 +58,41 @@
}
}
[data-slot="accordion-arrow"] {
flex-shrink: 0;
width: 16px;
height: 16px;
display: flex;
align-items: center;
justify-content: center;
color: var(--text-weak);
}
&[data-expanded] {
[data-slot="accordion-trigger"] {
border-bottom-left-radius: 0;
border-bottom-right-radius: 0;
}
[data-slot="accordion-content"] {
display: grid;
grid-template-rows: 0fr;
transition-property: grid-template-rows, opacity;
transition-duration: var(--transition-duration);
transition-timing-function: var(--transition-easing);
width: 100%;
> * {
overflow: hidden;
[data-slot="accordion-content"] {
border: 1px solid var(--border-weak-base);
border-top: none;
border-bottom-left-radius: var(--radius-md);
border-bottom-right-radius: var(--radius-md);
}
}
[data-slot="accordion-content"][data-expanded] {
grid-template-rows: 1fr;
}
[data-slot="accordion-content"][data-closed] {
grid-template-rows: 0fr;
}
&[data-expanded] [data-slot="accordion-trigger"] {
border-bottom-left-radius: 0;
border-bottom-right-radius: 0;
}
&[data-expanded] [data-slot="accordion-content"] {
border: 1px solid var(--border-weak-base);
border-top: none;
border-bottom-left-radius: var(--radius-md);
border-bottom-right-radius: var(--radius-md);
height: auto;
[data-slot="accordion-content"] {
overflow: hidden;
width: 100%;
}
}
}
@keyframes slideDown {
from {
height: 0;
}
to {
height: var(--kb-accordion-content-height);
}
}
@keyframes slideUp {
from {
height: var(--kb-accordion-content-height);
}
to {
height: 0;
}
}
+11 -32
View File
@@ -1,7 +1,6 @@
import { Accordion as Kobalte } from "@kobalte/core/accordion"
import { Accessor, createContext, splitProps, useContext } from "solid-js"
import { splitProps } from "solid-js"
import type { ComponentProps, ParentProps } from "solid-js"
import { MorphChevron } from "./morph-chevron"
export interface AccordionProps extends ComponentProps<typeof Kobalte> {}
export interface AccordionItemProps extends ComponentProps<typeof Kobalte.Item> {}
@@ -9,8 +8,6 @@ export interface AccordionHeaderProps extends ComponentProps<typeof Kobalte.Head
export interface AccordionTriggerProps extends ComponentProps<typeof Kobalte.Trigger> {}
export interface AccordionContentProps extends ComponentProps<typeof Kobalte.Content> {}
const AccordionItemContext = createContext<Accessor<boolean>>()
function AccordionRoot(props: AccordionProps) {
const [split, rest] = splitProps(props, ["class", "classList"])
return (
@@ -25,19 +22,17 @@ function AccordionRoot(props: AccordionProps) {
)
}
function AccordionItem(props: AccordionItemProps & { expanded?: boolean }) {
const [split, rest] = splitProps(props, ["class", "classList", "expanded"])
function AccordionItem(props: AccordionItemProps) {
const [split, rest] = splitProps(props, ["class", "classList"])
return (
<AccordionItemContext.Provider value={() => split.expanded ?? false}>
<Kobalte.Item
{...rest}
data-slot="accordion-item"
classList={{
...(split.classList ?? {}),
[split.class ?? ""]: !!split.class,
}}
/>
</AccordionItemContext.Provider>
<Kobalte.Item
{...rest}
data-slot="accordion-item"
classList={{
...(split.classList ?? {}),
[split.class ?? ""]: !!split.class,
}}
/>
)
}
@@ -89,25 +84,9 @@ function AccordionContent(props: ParentProps<AccordionContentProps>) {
)
}
export interface AccordionArrowProps extends ComponentProps<"div"> {
expanded?: boolean
}
function AccordionArrow(props: AccordionArrowProps = {}) {
const [local, rest] = splitProps(props, ["expanded"])
const contextExpanded = useContext(AccordionItemContext)
const isExpanded = () => local.expanded ?? contextExpanded?.() ?? false
return (
<div data-slot="accordion-arrow" {...rest}>
<MorphChevron expanded={isExpanded()} />
</div>
)
}
export const Accordion = Object.assign(AccordionRoot, {
Item: AccordionItem,
Header: AccordionHeader,
Trigger: AccordionTrigger,
Content: AccordionContent,
Arrow: AccordionArrow,
})
+17 -12
View File
@@ -8,13 +8,8 @@
text-decoration: none;
user-select: none;
cursor: default;
padding: 4px 8px;
white-space: nowrap;
transition-property: background-color, border-color, color, box-shadow, opacity;
transition-duration: var(--transition-duration);
transition-timing-function: var(--transition-easing);
outline: none;
line-height: 20px;
white-space: nowrap;
&[data-variant="primary"] {
background-color: var(--button-primary-base);
@@ -99,6 +94,7 @@
&:active:not(:disabled) {
background-color: var(--button-secondary-base);
scale: 0.99;
transition: all 150ms ease-out;
}
&:disabled {
border-color: var(--border-disabled);
@@ -113,27 +109,34 @@
}
&[data-size="small"] {
padding: 2px 8px;
height: 22px;
padding: 0 8px;
&[data-icon] {
padding: 2px 12px 2px 4px;
padding: 0 12px 0 4px;
}
font-size: var(--font-size-small);
line-height: var(--line-height-large);
gap: 4px;
/* text-12-medium */
font-family: var(--font-family-sans);
font-size: var(--font-size-base);
font-size: var(--font-size-small);
font-style: normal;
font-weight: var(--font-weight-medium);
line-height: var(--line-height-large); /* 166.667% */
letter-spacing: var(--letter-spacing-normal);
}
&[data-size="normal"] {
padding: 4px 6px;
height: 24px;
line-height: 24px;
padding: 0 6px;
&[data-icon] {
padding: 4px 12px 4px 4px;
padding: 0 12px 0 4px;
}
font-size: var(--font-size-small);
gap: 6px;
/* text-12-medium */
@@ -145,10 +148,11 @@
}
&[data-size="large"] {
height: 32px;
padding: 6px 12px;
&[data-icon] {
padding: 6px 12px 6px 8px;
padding: 0 12px 0 8px;
}
gap: 4px;
@@ -158,6 +162,7 @@
font-size: 14px;
font-style: normal;
font-weight: var(--font-weight-medium);
line-height: var(--line-height-large); /* 142.857% */
letter-spacing: var(--letter-spacing-normal);
}
+1 -1
View File
@@ -4,7 +4,7 @@ import { Icon, IconProps } from "./icon"
export interface ButtonProps
extends ComponentProps<typeof Kobalte>,
Pick<ComponentProps<"button">, "class" | "classList" | "children" | "style"> {
Pick<ComponentProps<"button">, "class" | "classList" | "children"> {
size?: "small" | "normal" | "large"
variant?: "primary" | "secondary" | "ghost"
icon?: IconProps["name"]
+1 -3
View File
@@ -4,9 +4,7 @@
flex-direction: column;
background-color: var(--surface-inset-base);
border: 1px solid var(--border-weaker-base);
transition-property: background-color, border-color;
transition-duration: var(--transition-duration);
transition-timing-function: var(--transition-easing);
transition: background-color 0.15s ease;
border-radius: var(--radius-md);
padding: 6px 12px;
overflow: clip;
-12
View File
@@ -4,18 +4,6 @@
gap: 12px;
cursor: default;
[data-slot="checkbox-checkbox-control"] {
transition-property: border-color, background-color, box-shadow;
transition-duration: var(--transition-duration);
transition-timing-function: var(--transition-easing);
}
[data-slot="checkbox-checkbox-indicator"] {
transition-property: opacity;
transition-duration: var(--transition-duration);
transition-timing-function: var(--transition-easing);
}
[data-slot="checkbox-checkbox-input"] {
position: absolute;
width: 1px;
+24 -20
View File
@@ -4,9 +4,7 @@
flex-direction: column;
background-color: var(--surface-inset-base);
border: 1px solid var(--border-weaker-base);
transition-property: background-color, border-color;
transition-duration: var(--transition-duration);
transition-timing-function: var(--transition-easing);
transition: background-color 0.15s ease;
border-radius: var(--radius-md);
overflow: clip;
@@ -46,28 +44,16 @@
display: flex;
align-items: center;
justify-content: center;
color: var(--text-weak);
}
}
[data-slot="collapsible-content"] {
display: grid;
grid-template-rows: 0fr;
transition-property: grid-template-rows, opacity;
transition-duration: var(--transition-duration);
transition-timing-function: var(--transition-easing);
overflow: hidden;
/* animation: slideUp 250ms ease-out; */
> * {
overflow: hidden;
}
&[data-expanded] {
grid-template-rows: 1fr;
}
&[data-closed] {
grid-template-rows: 0fr;
}
/* &[data-expanded] { */
/* animation: slideDown 250ms ease-out; */
/* } */
}
&[data-variant="ghost"] {
@@ -97,3 +83,21 @@
}
}
}
@keyframes slideDown {
from {
height: 0;
}
to {
height: var(--kb-collapsible-content-height);
}
}
@keyframes slideUp {
from {
height: var(--kb-collapsible-content-height);
}
to {
height: 0;
}
}
+13 -29
View File
@@ -1,8 +1,6 @@
import { Collapsible as Kobalte, CollapsibleRootProps } from "@kobalte/core/collapsible"
import { Accessor, ComponentProps, createContext, createSignal, ParentProps, splitProps, useContext } from "solid-js"
import { MorphChevron } from "./morph-chevron"
const CollapsibleContext = createContext<Accessor<boolean>>()
import { ComponentProps, ParentProps, splitProps } from "solid-js"
import { Icon } from "./icon"
export interface CollapsibleProps extends ParentProps<CollapsibleRootProps> {
class?: string
@@ -11,30 +9,17 @@ export interface CollapsibleProps extends ParentProps<CollapsibleRootProps> {
}
function CollapsibleRoot(props: CollapsibleProps) {
const [local, others] = splitProps(props, ["class", "classList", "variant", "open", "onOpenChange", "children"])
const [internalOpen, setInternalOpen] = createSignal(local.open ?? false)
const handleOpenChange = (open: boolean) => {
setInternalOpen(open)
local.onOpenChange?.(open)
}
const [local, others] = splitProps(props, ["class", "classList", "variant"])
return (
<CollapsibleContext.Provider value={internalOpen}>
<Kobalte
data-component="collapsible"
data-variant={local.variant || "normal"}
open={local.open}
onOpenChange={handleOpenChange}
classList={{
...(local.classList ?? {}),
[local.class ?? ""]: !!local.class,
}}
{...others}
>
{local.children}
</Kobalte>
</CollapsibleContext.Provider>
<Kobalte
data-component="collapsible"
data-variant={local.variant || "normal"}
classList={{
...(local.classList ?? {}),
[local.class ?? ""]: !!local.class,
}}
{...others}
/>
)
}
@@ -47,10 +32,9 @@ function CollapsibleContent(props: ComponentProps<typeof Kobalte.Content>) {
}
function CollapsibleArrow(props?: ComponentProps<"div">) {
const isOpen = useContext(CollapsibleContext)
return (
<div data-slot="collapsible-arrow" {...(props || {})}>
<MorphChevron expanded={isOpen?.() ?? false} />
<Icon name="chevron-grabber-vertical" size="small" />
</div>
)
}
@@ -1,51 +0,0 @@
.cycle-label {
--c-dur: 200ms;
--c-stag: 30ms;
--c-ease: cubic-bezier(0.25, 0, 0.5, 1);
--c-opacity-start: 0;
--c-opacity-end: 1;
--c-blur-start: 0px;
--c-blur-end: 0px;
--c-skew: 10deg;
display: inline-flex;
position: relative;
transform-style: preserve-3d;
perspective: 500px;
transition: width 200ms var(--c-ease);
will-change: width;
overflow: hidden;
.cycle-char {
display: inline-block;
transform-style: preserve-3d;
min-width: 0.25em;
backface-visibility: hidden;
transition:
transform var(--c-dur) var(--c-ease),
opacity var(--c-dur) var(--c-ease),
filter var(--c-dur) var(--c-ease);
transition-delay: calc(var(--i, 0) * var(--c-stag));
&.enter {
opacity: var(--c-opacity-end);
filter: blur(var(--c-blur-end));
transform: translateY(0) rotateX(0) skewX(0);
}
&.exit {
opacity: var(--c-opacity-start);
filter: blur(var(--c-blur-start));
transform: translateY(50%) rotateX(90deg) skewX(var(--c-skew));
}
&.pre {
opacity: var(--c-opacity-start);
filter: blur(var(--c-blur-start));
transition: none;
transform: translateY(-50%) rotateX(-90deg) skewX(calc(var(--c-skew) * -1));
}
}
}

Some files were not shown because too many files have changed in this diff Show More