Compare commits

...

81 Commits

Author SHA1 Message Date
Aiden Cline
5e782b8a34 test: add unit test 2026-01-30 16:04:07 -06:00
Aiden Cline
c64ace2a07 fix: adjust resolve parts so that when messages with multiple @ references occur, the tool calls are properly ordered 2026-01-30 15:55:00 -06:00
opencode
e9ef94dc4d release: v1.1.46 2026-01-30 19:19:34 +00:00
opencode-agent[bot]
5495fdde9d chore: generate 2026-01-30 18:16:49 +00:00
Idris Gadi
7d0777a7ff chore(tui): remove unused experimental keys (#11195) 2026-01-30 12:16:01 -06:00
Filip
f48e2e56c9 test(app): change language test (#11295) 2026-01-30 18:04:02 +00:00
opencode-agent[bot]
fe66ca163c chore: generate 2026-01-30 17:58:31 +00:00
Aaron Iker
20619a6a26 feat: Transitions, spacing, scroll fade, prompt area update (#11168)
Co-authored-by: Github Action <action@github.com>
Co-authored-by: Copilot <198982749+Copilot@users.noreply.github.com>
Co-authored-by: aaroniker <4730431+aaroniker@users.noreply.github.com>
2026-01-30 17:57:49 +00:00
Dax Raad
1bbe84ed8d ci 2026-01-30 12:55:58 -05:00
Aiden Cline
21edc00f11 ci: update pr template (#11341) 2026-01-30 12:45:42 -05:00
opencode-agent[bot]
0b91e9087d chore: generate 2026-01-30 17:20:20 +00:00
Frank
7cb84f13d3 wip: zen (#11343) 2026-01-30 12:19:36 -05:00
Dax Raad
1aade4b308 ci 2026-01-30 10:39:59 -05:00
Dax Raad
2e005de670 ci 2026-01-30 10:38:10 -05:00
Dax Raad
e80a99e7bd ci 2026-01-30 10:36:01 -05:00
Dax Raad
9a0132e750 ci 2026-01-30 10:35:39 -05:00
Dax Raad
7fb22ab681 ci 2026-01-30 10:28:54 -05:00
Dax Raad
e4d3b961cd ci 2026-01-30 10:28:12 -05:00
Dax Raad
9cf3e651ca ci 2026-01-30 10:26:55 -05:00
Dax Raad
e0b60d9f34 ci 2026-01-30 10:25:10 -05:00
Dax Raad
3f57f4913d ci 2026-01-30 10:23:19 -05:00
Dax Raad
b9e9c8c763 ci 2026-01-30 10:21:47 -05:00
Dax Raad
0d53f34c43 ci 2026-01-30 10:20:49 -05:00
Dax Raad
0a0b54aa4b ci 2026-01-30 10:15:45 -05:00
Dax Raad
4a4fc48ee8 ci 2026-01-30 10:12:29 -05:00
Dax Raad
5e823fd208 ci 2026-01-30 10:11:05 -05:00
Dax Raad
ad5d495b2c ci 2026-01-30 10:09:27 -05:00
Dax Raad
abb87eac8f ci 2026-01-30 10:08:28 -05:00
Dax Raad
a530c1b5b6 ci 2026-01-30 10:02:05 -05:00
Dax Raad
601744eacd sync 2026-01-30 09:58:54 -05:00
Dax Raad
97a428cf69 ci 2026-01-30 09:57:17 -05:00
Dax Raad
698cf6dfc1 ci 2026-01-30 09:56:21 -05:00
Dax Raad
9493316502 ci 2026-01-30 09:56:06 -05:00
Dax Raad
08fa7f7188 ci 2026-01-30 09:36:57 -05:00
Aiden Cline
11d486707c fix: rm ai sdk middleware that was preventing <think> blocks from being sent back as assistant message content (#11270)
Co-authored-by: opencode-agent[bot] <opencode-agent[bot]@users.noreply.github.com>
2026-01-30 09:36:57 -05:00
opencode
71e0ba271f release: v1.1.45 2026-01-30 14:32:22 +00:00
Dax Raad
d58661d4fb ci 2026-01-30 01:15:24 -05:00
Dax Raad
09f4ef8996 ci 2026-01-30 00:56:45 -05:00
Dax Raad
1794319a4c ci 2026-01-30 00:48:08 -05:00
Dax Raad
8f53794017 ci 2026-01-30 00:46:12 -05:00
Dax Raad
48d6d72e25 ci 2026-01-30 00:44:58 -05:00
Dax Raad
5bef8e316a ci 2026-01-30 00:43:36 -05:00
Dax Raad
08f11f4da6 ci 2026-01-30 00:39:49 -05:00
Dax Raad
9e0747c9b4 ci 2026-01-30 00:39:23 -05:00
Dax Raad
66ec378680 ci 2026-01-30 00:27:51 -05:00
Dax Raad
015eda36ce ci 2026-01-30 00:25:52 -05:00
Dax Raad
e666ddb630 ci 2026-01-30 00:23:52 -05:00
Dax Raad
da7f45bd4c ci 2026-01-30 00:21:30 -05:00
Dax Raad
273e7b8379 ci 2026-01-30 00:18:50 -05:00
Dax Raad
36041c0000 ci 2026-01-30 00:15:21 -05:00
Dax Raad
b5e5d4c92f ci 2026-01-30 00:10:44 -05:00
Dax Raad
b109ab7830 ci 2026-01-30 00:07:56 -05:00
Dax Raad
b28891473f ci 2026-01-30 00:05:05 -05:00
Dax Raad
5d0122b5a9 ci 2026-01-30 00:04:51 -05:00
Dax Raad
1ab4bbc275 ci 2026-01-29 23:58:39 -05:00
Dax Raad
908350c2ea ci 2026-01-29 23:56:56 -05:00
Dax Raad
3fef490187 ci 2026-01-29 23:55:48 -05:00
Dax Raad
3ac05201c6 ci 2026-01-29 23:52:08 -05:00
Dax Raad
2d3c7a0f24 ci 2026-01-29 23:49:53 -05:00
Dax Raad
cd664a189b ci 2026-01-29 23:17:57 -05:00
Dax Raad
849f488744 ci 2026-01-29 23:15:12 -05:00
Dax Raad
5ea1042ffb ci 2026-01-29 23:13:07 -05:00
Dax Raad
71d280d570 ci: fix container build script
Invoke docker build with Bun shell so commands run correctly, and document default automation behavior.
2026-01-29 23:10:50 -05:00
Dax Raad
5cfb5fdd06 ci: add container build workflow
Add prebuilt build images and a publish workflow to speed CI by reusing heavy dependencies.
2026-01-29 23:07:58 -05:00
Dax Raad
30969dc33e ci: cache apt packages to reduce CI build times on ubuntu 2026-01-29 21:51:53 -05:00
adamelmore
5f282c268d fix(app): free model layout 2026-01-29 20:44:38 -06:00
Dax Raad
d3d6e7e275 sync 2026-01-29 21:35:24 -05:00
adamelmore
a70c66eb3f fix(app): free model scroll 2026-01-29 20:26:35 -06:00
adamelmore
60de810d9a fix(app): dialog not closing 2026-01-29 20:26:35 -06:00
Dax Raad
95309c2149 fix(beta): use local git rebase instead of gh pr update-branch 2026-01-29 21:25:27 -05:00
Dax Raad
e9e8d97b0d ci 2026-01-29 21:23:03 -05:00
Dax Raad
553316af2a ci 2026-01-29 21:19:00 -05:00
Dax Raad
f27ee4674a ci 2026-01-29 20:59:33 -05:00
Rahul A Mistry
ad91f9143a fix(app): version to latest to avoid errors for new devs (#11201) 2026-01-29 19:42:59 -06:00
Filip
b43a35b737 test(app): test for toggling model variant (#11221) 2026-01-29 19:05:31 -06:00
Dax Raad
03803621db ci 2026-01-29 19:35:52 -05:00
Dax Raad
81326377f2 ci: trigger publish workflow automatically after beta builds complete 2026-01-29 19:35:05 -05:00
Dax Raad
7ed6f690e9 ci 2026-01-29 19:34:12 -05:00
Dax Raad
1f3bf56640 ci: upgrade bun cache to stickydisk for faster ci builds 2026-01-30 00:31:06 +00:00
opencode
bbc7bdb3fd release: v1.1.43 2026-01-30 00:31:06 +00:00
Dax Raad
a5c01a81ff ci 2026-01-29 19:08:48 -05:00
138 changed files with 2180 additions and 772 deletions

View File

@@ -3,20 +3,17 @@ 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

View File

@@ -0,0 +1,42 @@
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 }}
- 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

View File

@@ -1,3 +1,7 @@
### 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?

View File

@@ -15,20 +15,24 @@ jobs:
runs-on: blacksmith-4vcpu-ubuntu-2404
permissions:
contents: write
pull-requests: read
pull-requests: write
steps:
- name: Checkout repository
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Setup Bun
uses: ./.github/actions/setup-bun
- name: Configure Git
run: |
git config user.name "github-actions[bot]"
git config user.email "github-actions[bot]@users.noreply.github.com"
- 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: Sync beta branch
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
GH_TOKEN: ${{ steps.setup-git-committer.outputs.token }}
run: bun script/beta.ts

45
.github/workflows/containers.yml vendored Normal file
View File

@@ -0,0 +1,45 @@
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 }}

View File

@@ -4,8 +4,6 @@ on:
push:
branches:
- dev
pull_request:
workflow_dispatch:
jobs:
generate:
@@ -16,14 +14,17 @@ 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
@@ -33,10 +34,8 @@ 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"
git commit -m "chore: generate" --allow-empty
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 ""

View File

@@ -36,14 +36,16 @@ 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 }}

View File

@@ -36,7 +36,15 @@ jobs:
if: github.repository == 'anomalyco/opencode'
steps:
- uses: actions/checkout@v3
with:
fetch-depth: 0
- 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
@@ -44,6 +52,7 @@ 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 }}
@@ -124,6 +133,15 @@ 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: |
@@ -145,8 +163,8 @@ jobs:
cd packages/desktop
bun ./scripts/prepare.ts
env:
OPENCODE_VERSION: ${{ needs.publish.outputs.version }}
GITHUB_TOKEN: ${{ secrets.SST_GITHUB_TOKEN }}
OPENCODE_VERSION: ${{ needs.version.outputs.version }}
GITHUB_TOKEN: ${{ steps.committer.outputs.token }}
RUST_TARGET: ${{ matrix.settings.target }}
GH_TOKEN: ${{ github.token }}
GITHUB_RUN_ID: ${{ github.run_id }}
@@ -188,17 +206,13 @@ jobs:
needs:
- version
- build-cli
# - build-tauri
- build-tauri
runs-on: blacksmith-4vcpu-ubuntu-2404
steps:
- uses: actions/checkout@v3
- 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
with:
@@ -217,17 +231,26 @@ jobs:
node-version: "24"
registry-url: "https://registry.npmjs.org"
- 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 }}
- 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 }}
- 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
@@ -244,6 +267,5 @@ jobs:
OPENCODE_VERSION: ${{ needs.version.outputs.version }}
OPENCODE_RELEASE: ${{ needs.version.outputs.release }}
AUR_KEY: ${{ secrets.AUR_KEY }}
GITHUB_TOKEN: ${{ secrets.SST_GITHUB_TOKEN }}
OPENCODE_API_KEY: ${{ secrets.OPENCODE_API_KEY }}
GITHUB_TOKEN: ${{ steps.committer.outputs.token }}
NPM_CONFIG_PROVENANCE: false

View File

@@ -1,6 +1,7 @@
- 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

View File

@@ -23,7 +23,7 @@
},
"packages/app": {
"name": "@opencode-ai/app",
"version": "0.0.0-ci-202601291718",
"version": "1.1.46",
"dependencies": {
"@kobalte/core": "catalog:",
"@opencode-ai/sdk": "workspace:*",
@@ -73,7 +73,7 @@
},
"packages/console/app": {
"name": "@opencode-ai/console-app",
"version": "0.0.0-ci-202601291718",
"version": "1.1.46",
"dependencies": {
"@cloudflare/vite-plugin": "1.15.2",
"@ibm/plex": "6.4.1",
@@ -107,7 +107,7 @@
},
"packages/console/core": {
"name": "@opencode-ai/console-core",
"version": "0.0.0-ci-202601291718",
"version": "1.1.46",
"dependencies": {
"@aws-sdk/client-sts": "3.782.0",
"@jsx-email/render": "1.1.1",
@@ -134,7 +134,7 @@
},
"packages/console/function": {
"name": "@opencode-ai/console-function",
"version": "0.0.0-ci-202601291718",
"version": "1.1.46",
"dependencies": {
"@ai-sdk/anthropic": "2.0.0",
"@ai-sdk/openai": "2.0.2",
@@ -158,7 +158,7 @@
},
"packages/console/mail": {
"name": "@opencode-ai/console-mail",
"version": "0.0.0-ci-202601291718",
"version": "1.1.46",
"dependencies": {
"@jsx-email/all": "2.2.3",
"@jsx-email/cli": "1.4.3",
@@ -182,7 +182,7 @@
},
"packages/desktop": {
"name": "@opencode-ai/desktop",
"version": "0.0.0-ci-202601291718",
"version": "1.1.46",
"dependencies": {
"@opencode-ai/app": "workspace:*",
"@opencode-ai/ui": "workspace:*",
@@ -213,7 +213,7 @@
},
"packages/enterprise": {
"name": "@opencode-ai/enterprise",
"version": "0.0.0-ci-202601291718",
"version": "1.1.46",
"dependencies": {
"@opencode-ai/ui": "workspace:*",
"@opencode-ai/util": "workspace:*",
@@ -242,7 +242,7 @@
},
"packages/function": {
"name": "@opencode-ai/function",
"version": "0.0.0-ci-202601291718",
"version": "1.1.46",
"dependencies": {
"@octokit/auth-app": "8.0.1",
"@octokit/rest": "catalog:",
@@ -258,7 +258,7 @@
},
"packages/opencode": {
"name": "opencode",
"version": "0.0.0-ci-202601291718",
"version": "1.1.46",
"bin": {
"opencode": "./bin/opencode",
},
@@ -362,7 +362,7 @@
},
"packages/plugin": {
"name": "@opencode-ai/plugin",
"version": "0.0.0-ci-202601291718",
"version": "1.1.46",
"dependencies": {
"@opencode-ai/sdk": "workspace:*",
"zod": "catalog:",
@@ -382,7 +382,7 @@
},
"packages/sdk/js": {
"name": "@opencode-ai/sdk",
"version": "0.0.0-ci-202601291718",
"version": "1.1.46",
"devDependencies": {
"@hey-api/openapi-ts": "0.90.10",
"@tsconfig/node22": "catalog:",
@@ -393,7 +393,7 @@
},
"packages/slack": {
"name": "@opencode-ai/slack",
"version": "0.0.0-ci-202601291718",
"version": "1.1.46",
"dependencies": {
"@opencode-ai/sdk": "workspace:*",
"@slack/bolt": "^3.17.1",
@@ -406,7 +406,7 @@
},
"packages/ui": {
"name": "@opencode-ai/ui",
"version": "0.0.0-ci-202601291718",
"version": "1.1.46",
"dependencies": {
"@kobalte/core": "catalog:",
"@opencode-ai/sdk": "workspace:*",
@@ -448,7 +448,7 @@
},
"packages/util": {
"name": "@opencode-ai/util",
"version": "0.0.0-ci-202601291718",
"version": "1.1.46",
"dependencies": {
"zod": "catalog:",
},
@@ -459,7 +459,7 @@
},
"packages/web": {
"name": "@opencode-ai/web",
"version": "0.0.0-ci-202601291718",
"version": "1.1.46",
"dependencies": {
"@astrojs/cloudflare": "12.6.3",
"@astrojs/markdown-remark": "6.3.1",

View File

@@ -133,6 +133,8 @@ 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")

View File

@@ -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("/")

View File

@@ -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))

View File

@@ -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()

View File

@@ -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"

View File

@@ -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()}`

View File

@@ -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 })

View File

@@ -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()

View File

@@ -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()

View File

@@ -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()

View File

@@ -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()

View File

@@ -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()

View File

@@ -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()}`

View File

@@ -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()

View File

@@ -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()

View File

@@ -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)

View File

@@ -0,0 +1,39 @@
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")
})

View File

@@ -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()

View File

@@ -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()

View File

@@ -1,33 +1,7 @@
import { test, expect } from "./fixtures"
import { modKey, promptSelector } from "./utils"
import { test, expect } from "../fixtures"
import { modKey, promptSelector } from "../utils"
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) => {
test("sidebar session links navigate to the selected session", async ({ page, slug, sdk, gotoSession }) => {
const stamp = Date.now()
const one = await sdk.session.create({ title: `e2e sidebar nav 1 ${stamp}` }).then((r) => r.data)

View File

@@ -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()

View File

@@ -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()

View File

@@ -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()

View File

@@ -0,0 +1,25 @@
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)
})

View File

@@ -12,6 +12,9 @@ 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 })

View File

@@ -1,6 +1,6 @@
{
"name": "@opencode-ai/app",
"version": "0.0.0-ci-202601291718",
"version": "1.1.46",
"description": "",
"type": "module",
"exports": {

View File

@@ -34,11 +34,14 @@ export const DialogSelectModelUnpaid: Component = () => {
})
return (
<Dialog title={language.t("dialog.model.select.title")}>
<div class="flex flex-col gap-3 px-2.5 flex-1 min-h-0">
<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">
<div class="text-14-medium text-text-base px-2.5">{language.t("dialog.model.unpaid.freeModels.title")}</div>
<List
class="flex-1 min-h-0 [&_[data-slot=list-scroll]]:flex-1 [&_[data-slot=list-scroll]]:min-h-0"
class="[&_[data-slot=list-scroll]]:overflow-visible"
ref={(ref) => (listRef = ref)}
items={local.model.list}
current={local.model.current()}
@@ -76,8 +79,6 @@ 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">

View File

@@ -90,7 +90,7 @@ const ModelList: Component<{
export function ModelSelectorPopover<T extends ValidComponent = "div">(props: {
provider?: string
children?: JSX.Element
children?: JSX.Element | ((open: boolean) => JSX.Element)
triggerAs?: T
triggerProps?: ComponentProps<T>
}) {
@@ -182,12 +182,13 @@ export function ModelSelectorPopover<T extends ValidComponent = "div">(props: {
as={props.triggerAs ?? "div"}
{...(props.triggerProps as any)}
>
{props.children}
{typeof props.children === "function" ? props.children(store.open) : 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)

View File

@@ -32,7 +32,9 @@ 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"
@@ -42,6 +44,7 @@ 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"
@@ -1252,7 +1255,7 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
clearInput()
client.session
.shell({
sessionID: session.id,
sessionID: session?.id || "",
agent,
model,
command: text,
@@ -1275,7 +1278,7 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
clearInput()
client.session
.command({
sessionID: session.id,
sessionID: session?.id || "",
command: commandName,
arguments: args.join(" "),
agent,
@@ -1431,13 +1434,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,
@@ -1448,9 +1451,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)
@@ -1466,9 +1469,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)
@@ -1485,7 +1488,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)
@@ -1498,7 +1501,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)
@@ -1519,15 +1522,15 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
const worktree = WorktreeState.get(sessionDirectory)
if (!worktree || worktree.status !== "pending") return true
if (sessionDirectory === projectDirectory) {
sync.set("session_status", session.id, { type: "busy" })
if (sessionDirectory === projectDirectory && session?.id) {
sync.set("session_status", session?.id, { type: "busy" })
}
const controller = new AbortController()
const cleanup = () => {
if (sessionDirectory === projectDirectory) {
sync.set("session_status", session.id, { type: "idle" })
if (sessionDirectory === projectDirectory && session?.id) {
sync.set("session_status", session?.id, { type: "idle" })
}
removeOptimisticMessage()
for (const item of commentItems) {
@@ -1544,7 +1547,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) {
@@ -1572,7 +1575,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
@@ -1582,7 +1585,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,
@@ -1592,9 +1595,9 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
}
void send().catch((err) => {
pending.delete(session.id)
if (sessionDirectory === projectDirectory) {
sync.set("session_status", session.id, { type: "idle" })
pending.delete(session?.id || "")
if (sessionDirectory === projectDirectory && session?.id) {
sync.set("session_status", session?.id, { type: "idle" })
}
showToast({
title: language.t("prompt.toast.promptSendFailed.title"),
@@ -1616,6 +1619,28 @@ 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}>
@@ -1668,7 +1693,7 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
</>
}
>
<Icon name="brain" size="small" class="text-icon-info-active shrink-0" />
<Icon name="brain" size="normal" 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>
@@ -1729,9 +1754,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 pointer-events-none">
<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="flex flex-col items-center gap-2 text-text-weak">
<Icon name="photo" class="size-8" />
<Icon name="photo" size={18} class="text-icon-base stroke-1.5" />
<span class="text-14-regular">{language.t("prompt.dropzone.label")}</span>
</div>
</div>
@@ -1770,7 +1795,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-3.5" />
<FileIcon node={{ path: item.path, type: "file" }} class="shrink-0 size-7" />
<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}>
@@ -1787,7 +1812,7 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
type="button"
icon="close-small"
variant="ghost"
class="ml-auto h-5 w-5 opacity-0 group-hover:opacity-100 transition-all"
class="ml-auto size-7 opacity-0 group-hover:opacity-100 transition-all"
onClick={(e) => {
e.stopPropagation()
if (item.commentID) comments.remove(item.path, item.commentID)
@@ -1817,7 +1842,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" class="size-6 text-text-weak" />
<Icon name="folder" size="normal" class="size-6 text-text-base" />
</div>
}
>
@@ -1891,7 +1916,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-0.5">
<div class="flex items-center justify-start gap-2">
<Switch>
<Match when={store.mode === "shell"}>
<div class="flex items-center gap-2 px-2 h-6">
@@ -1922,12 +1947,17 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
title={language.t("command.model.choose")}
keybind={command.keybind("model.choose")}
>
<Button as="div" variant="ghost" onClick={() => dialog.show(() => <DialogSelectModelUnpaid />)}>
<Button
as="div"
variant="ghost"
class="px-2"
onClick={() => dialog.render(<DialogSelectModelUnpaid />, "select-model")}
>
<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" />
<MorphChevron expanded={dialog.isActive("select-model")} />
</Button>
</TooltipKeybind>
}
@@ -1938,11 +1968,15 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
keybind={command.keybind("model.choose")}
>
<ModelSelectorPopover triggerAs={Button} triggerProps={{ variant: "ghost" }}>
<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" />
{(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" />
</>
)}
</ModelSelectorPopover>
</TooltipKeybind>
</Show>
@@ -1953,11 +1987,15 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
keybind={command.keybind("model.variant.cycle")}
>
<Button
data-action="model-variant-cycle"
variant="ghost"
class="text-text-base _hidden group-hover/prompt-input:inline-block capitalize text-12-regular"
class="text-text-strong text-12-regular"
onClick={() => local.model.variant.cycle()}
>
{local.model.variant.current() ?? language.t("common.default")}
<Show when={local.model.variant.list().length > 1}>
<ReasoningIcon percentage={reasoningPercentage()} size={16} strokeWidth={1.25} />
</Show>
<CycleLabel value={currrentModelVariant()} />
</Button>
</TooltipKeybind>
</Show>
@@ -1971,7 +2009,7 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
variant="ghost"
onClick={() => permission.toggleAutoAccept(params.id!, sdk.directory)}
classList={{
"_hidden group-hover/prompt-input:flex size-6 items-center justify-center": true,
"_hidden group-hover/prompt-input:flex 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),
}}
@@ -1993,7 +2031,7 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
</Match>
</Switch>
</div>
<div class="flex items-center gap-3 absolute right-3 bottom-3">
<div class="flex items-center gap-1 absolute right-3 bottom-3">
<input
ref={fileInputRef}
type="file"
@@ -2005,18 +2043,19 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
e.currentTarget.value = ""
}}
/>
<div class="flex items-center gap-2">
<div class="flex items-center gap-1.5 mr-1.5">
<SessionContextUsage />
<Show when={store.mode === "normal"}>
<Tooltip placement="top" value={language.t("prompt.action.attachFile")}>
<Button
type="button"
variant="ghost"
class="size-6"
size="small"
class="px-1"
onClick={() => fileInputRef.click()}
aria-label={language.t("prompt.action.attachFile")}
>
<Icon name="photo" class="size-4.5" />
<Icon name="photo" class="size-6 text-icon-base" />
</Button>
</Tooltip>
</Show>
@@ -2035,7 +2074,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="small" class="text-icon-base" />
<Icon name="enter" size="normal" class="text-icon-base" />
</div>
</Match>
</Switch>
@@ -2046,7 +2085,7 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
disabled={!prompt.dirty() && !working()}
icon={working() ? "stop" : "arrow-up"}
variant="primary"
class="h-6 w-4.5"
class="h-6 w-5.5"
aria-label={working() ? language.t("prompt.action.stop") : language.t("prompt.action.send")}
/>
</Tooltip>

View File

@@ -64,8 +64,8 @@ export function SessionContextUsage(props: SessionContextUsageProps) {
}
const circle = () => (
<div class="p-1">
<ProgressCircle size={16} strokeWidth={2} percentage={context()?.percentage ?? 0} />
<div class="text-icon-base">
<ProgressCircle size={18} percentage={context()?.percentage ?? 0} />
</div>
)
@@ -101,7 +101,7 @@ export function SessionContextUsage(props: SessionContextUsageProps) {
<Button
type="button"
variant="ghost"
class="size-6"
class="size-7 text-icon-base"
onClick={openContext}
aria-label={language.t("context.usage.view")}
>

View File

@@ -10,6 +10,7 @@ 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,
@@ -130,7 +131,12 @@ export const SettingsGeneral: Component = () => {
const soundOptions = [...SOUND_OPTIONS]
return (
<div class="flex flex-col h-full overflow-y-auto no-scrollbar px-4 pb-10 sm:px-10 sm:pb-10">
<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="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>
@@ -148,6 +154,7 @@ 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}
@@ -394,7 +401,7 @@ export const SettingsGeneral: Component = () => {
</div>
</div>
</div>
</div>
</ScrollFade>
)
}

View File

@@ -9,6 +9,7 @@ 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"
@@ -352,7 +353,12 @@ export const SettingsKeybinds: Component = () => {
})
return (
<div class="flex flex-col h-full overflow-y-auto no-scrollbar px-4 pb-10 sm:px-10 sm:pb-10">
<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="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">
@@ -429,6 +435,6 @@ export const SettingsKeybinds: Component = () => {
</div>
</Show>
</div>
</div>
</ScrollFade>
)
}

View File

@@ -1,6 +1,6 @@
{
"name": "@opencode-ai/console-app",
"version": "0.0.0-ci-202601291718",
"version": "1.1.46",
"type": "module",
"license": "MIT",
"scripts": {

View File

@@ -1,7 +1,7 @@
{
"$schema": "https://json.schemastore.org/package.json",
"name": "@opencode-ai/console-core",
"version": "0.0.0-ci-202601291718",
"version": "1.1.46",
"private": true,
"type": "module",
"license": "MIT",

View File

@@ -2,20 +2,21 @@
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 = 8
const PARTS = 10
// 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("=")
@@ -27,6 +28,6 @@ const values = Array.from({ length: PARTS }, (_, i) => {
ZenData.validate(JSON.parse(values.join("")))
// update the secret
for (let i = 0; i < PARTS; i++) {
await $`bun sst secret set ZEN_MODELS${i + 1} --stage ${stage} -- ${values[i]}`
}
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)

View File

@@ -2,20 +2,21 @@
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 = 8
const PARTS = 10
// 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("=")
@@ -27,6 +28,6 @@ const values = Array.from({ length: PARTS }, (_, i) => {
ZenData.validate(JSON.parse(values.join("")))
// update the secret
for (let i = 0; i < PARTS; i++) {
await $`bun sst secret set ZEN_MODELS${i + 1} -- ${values[i]}`
}
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)

View File

@@ -7,18 +7,20 @@ import { ZenData } from "../src/model"
const root = path.resolve(process.cwd(), "..", "..", "..")
const models = await $`bun sst secret list`.cwd(root).text()
const PARTS = 8
const PARTS = 10
// 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("=")
if (!value) throw new Error(`ZEN_MODELS${i + 1} not found`)
return value
// TODO
//if (!value) throw new Error(`ZEN_MODELS${i + 1} not found`)
//return value
return value ?? ""
})
// store the prettified json to a temp file
@@ -38,6 +40,6 @@ const newValues = Array.from({ length: PARTS }, (_, i) =>
newValue.slice(chunk * i, i === PARTS - 1 ? undefined : chunk * (i + 1)),
)
for (let i = 0; i < PARTS; i++) {
await $`bun sst secret set ZEN_MODELS${i + 1} -- ${newValues[i]}`
}
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)

View File

@@ -75,7 +75,9 @@ export namespace ZenData {
Resource.ZEN_MODELS5.value +
Resource.ZEN_MODELS6.value +
Resource.ZEN_MODELS7.value +
Resource.ZEN_MODELS8.value,
Resource.ZEN_MODELS8.value +
Resource.ZEN_MODELS9.value +
Resource.ZEN_MODELS10.value,
)
return ModelsSchema.parse(json)
})

View File

@@ -133,6 +133,10 @@ 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
@@ -161,6 +165,10 @@ 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

View File

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

View File

@@ -133,6 +133,10 @@ 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
@@ -161,6 +165,10 @@ 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

View File

@@ -1,6 +1,6 @@
{
"name": "@opencode-ai/console-mail",
"version": "0.0.0-ci-202601291718",
"version": "1.1.46",
"dependencies": {
"@jsx-email/all": "2.2.3",
"@jsx-email/cli": "1.4.3",

View File

@@ -133,6 +133,10 @@ 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
@@ -161,6 +165,10 @@ 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

View File

@@ -0,0 +1,38 @@
# 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).

View File

@@ -0,0 +1,18 @@
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/*

View File

@@ -0,0 +1,24 @@
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

View File

@@ -0,0 +1,10 @@
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/*

View File

@@ -0,0 +1,13 @@
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

View File

@@ -0,0 +1,77 @@
#!/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}`)
}
}

View File

@@ -0,0 +1,12 @@
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/*

View File

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

View File

@@ -1,7 +1,7 @@
{
"name": "@opencode-ai/desktop",
"private": true,
"version": "0.0.0-ci-202601291718",
"version": "1.1.46",
"type": "module",
"license": "MIT",
"scripts": {

View File

@@ -1,8 +1,14 @@
#!/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"

View File

@@ -4914,9 +4914,9 @@ dependencies = [
[[package]]
name = "tauri-plugin-fs"
version = "2.4.4"
version = "2.4.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "47df422695255ecbe7bac7012440eddaeefd026656171eac9559f5243d3230d9"
checksum = "ed390cc669f937afeb8b28032ce837bac8ea023d975a2e207375ec05afaf1804"
dependencies = [
"anyhow",
"dunce",
@@ -4936,9 +4936,9 @@ dependencies = [
[[package]]
name = "tauri-plugin-http"
version = "2.5.4"
version = "2.5.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c00685aceab12643cf024f712ab0448ba8fcadf86f2391d49d2e5aa732aacc70"
checksum = "68bef611ccbfbce67c813959c11b23c1c084d201aa94222de9eba5f9edc3f897"
dependencies = [
"bytes",
"cookie_store",

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"
tauri-plugin-http = "2.5.6"
tauri-plugin-notification = "2"
tauri-plugin-single-instance = { version = "2", features = ["deep-link"] }

View File

@@ -345,6 +345,7 @@ 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();

View File

@@ -1,6 +1,6 @@
{
"name": "@opencode-ai/enterprise",
"version": "0.0.0-ci-202601291718",
"version": "1.1.46",
"private": true,
"type": "module",
"license": "MIT",

View File

@@ -133,6 +133,10 @@ 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
@@ -161,6 +165,10 @@ 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

View File

@@ -1,7 +1,7 @@
id = "opencode"
name = "OpenCode"
description = "The open source coding agent."
version = "0.0.0-ci-202601291718"
version = "1.1.46"
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/v0.0.0-ci-202601291718/opencode-darwin-arm64.zip"
archive = "https://github.com/anomalyco/opencode/releases/download/v1.1.46/opencode-darwin-arm64.zip"
cmd = "./opencode"
args = ["acp"]
[agent_servers.opencode.targets.darwin-x86_64]
archive = "https://github.com/anomalyco/opencode/releases/download/v0.0.0-ci-202601291718/opencode-darwin-x64.zip"
archive = "https://github.com/anomalyco/opencode/releases/download/v1.1.46/opencode-darwin-x64.zip"
cmd = "./opencode"
args = ["acp"]
[agent_servers.opencode.targets.linux-aarch64]
archive = "https://github.com/anomalyco/opencode/releases/download/v0.0.0-ci-202601291718/opencode-linux-arm64.tar.gz"
archive = "https://github.com/anomalyco/opencode/releases/download/v1.1.46/opencode-linux-arm64.tar.gz"
cmd = "./opencode"
args = ["acp"]
[agent_servers.opencode.targets.linux-x86_64]
archive = "https://github.com/anomalyco/opencode/releases/download/v0.0.0-ci-202601291718/opencode-linux-x64.tar.gz"
archive = "https://github.com/anomalyco/opencode/releases/download/v1.1.46/opencode-linux-x64.tar.gz"
cmd = "./opencode"
args = ["acp"]
[agent_servers.opencode.targets.windows-x86_64]
archive = "https://github.com/anomalyco/opencode/releases/download/v0.0.0-ci-202601291718/opencode-windows-x64.zip"
archive = "https://github.com/anomalyco/opencode/releases/download/v1.1.46/opencode-windows-x64.zip"
cmd = "./opencode.exe"
args = ["acp"]

View File

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

View File

@@ -133,6 +133,10 @@ 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
@@ -161,6 +165,10 @@ 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

View File

@@ -1,6 +1,6 @@
{
"$schema": "https://json.schemastore.org/package.json",
"version": "0.0.0-ci-202601291718",
"version": "1.1.46",
"name": "opencode",
"type": "module",
"license": "MIT",

View File

@@ -37,6 +37,7 @@ await Bun.file(`./dist/${pkg.name}/package.json`).write(
),
)
/*
const tasks = Object.entries(binaries).map(async ([name]) => {
if (process.platform !== "win32") {
await $`chmod -R 755 .`.cwd(`./dist/${name}`)
@@ -52,6 +53,7 @@ const platforms = "linux/amd64,linux/arm64"
const tags = [`${image}:${version}`, `${image}:${Script.channel}`]
const tagFlags = tags.flatMap((t) => ["-t", t])
await $`docker buildx build --platform ${platforms} ${tagFlags} --push .`
*/
// registries
if (!Script.preview) {
@@ -63,6 +65,7 @@ if (!Script.preview) {
const [pkgver, _subver = ""] = Script.version.split(/(-.*)/, 2)
/*
// arch
const binaryPkgbuild = [
"# Maintainer: dax",
@@ -176,6 +179,7 @@ if (!Script.preview) {
}
}
}
*/
// Homebrew formula
const homebrewFormula = [
@@ -230,8 +234,14 @@ 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 https://${process.env["GITHUB_TOKEN"]}@github.com/sst/homebrew-tap.git ./dist/homebrew-tap`
await $`git clone ${tap} ./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}"`

View File

@@ -1078,29 +1078,6 @@ 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

View File

@@ -1,4 +1,3 @@
import os from "os"
import { Installation } from "@/installation"
import { Provider } from "@/provider/provider"
import { Log } from "@/util/log"
@@ -9,7 +8,6 @@ import {
type StreamTextResult,
type Tool,
type ToolSet,
extractReasoningMiddleware,
tool,
jsonSchema,
} from "ai"
@@ -261,7 +259,6 @@ export namespace LLM {
return args.params
},
},
extractReasoningMiddleware({ tagName: "think", startWithReasoning: false }),
],
}),
experimental_telemetry: {

View File

@@ -172,6 +172,14 @@ export namespace SessionProcessor {
case "tool-result": {
const match = toolcalls[value.toolCallId]
if (match && match.state.status === "running") {
const attachments = value.output.attachments?.map(
(attachment: Omit<MessageV2.FilePart, "id" | "messageID" | "sessionID">) => ({
...attachment,
id: Identifier.ascending("part"),
messageID: match.messageID,
sessionID: match.sessionID,
}),
)
await Session.updatePart({
...match,
state: {
@@ -184,7 +192,7 @@ export namespace SessionProcessor {
start: match.state.time.start,
end: Date.now(),
},
attachments: value.output.attachments,
attachments,
},
})

View File

@@ -187,13 +187,17 @@ export namespace SessionPrompt {
text: template,
},
]
const files = ConfigMarkdown.files(template)
const matches = ConfigMarkdown.files(template)
const seen = new Set<string>()
await Promise.all(
files.map(async (match) => {
const name = match[1]
if (seen.has(name)) return
const names = matches
.map((match) => match[1])
.filter((name) => {
if (seen.has(name)) return false
seen.add(name)
return true
})
const resolved = await Promise.all(
names.map(async (name) => {
const filepath = name.startsWith("~/")
? path.join(os.homedir(), name.slice(2))
: path.resolve(Instance.worktree, name)
@@ -201,33 +205,34 @@ export namespace SessionPrompt {
const stats = await fs.stat(filepath).catch(() => undefined)
if (!stats) {
const agent = await Agent.get(name)
if (agent) {
parts.push({
type: "agent",
name: agent.name,
})
}
return
if (!agent) return undefined
return {
type: "agent",
name: agent.name,
} satisfies PromptInput["parts"][number]
}
if (stats.isDirectory()) {
parts.push({
return {
type: "file",
url: `file://${filepath}`,
filename: name,
mime: "application/x-directory",
})
return
} satisfies PromptInput["parts"][number]
}
parts.push({
return {
type: "file",
url: `file://${filepath}`,
filename: name,
mime: "text/plain",
})
} satisfies PromptInput["parts"][number]
}),
)
for (const item of resolved) {
if (!item) continue
parts.push(item)
}
return parts
}
@@ -424,6 +429,12 @@ export namespace SessionPrompt {
assistantMessage.time.completed = Date.now()
await Session.updateMessage(assistantMessage)
if (result && part.state.status === "running") {
const attachments = result.attachments?.map((attachment) => ({
...attachment,
id: Identifier.ascending("part"),
messageID: assistantMessage.id,
sessionID: assistantMessage.sessionID,
}))
await Session.updatePart({
...part,
state: {
@@ -432,7 +443,7 @@ export namespace SessionPrompt {
title: result.title,
metadata: result.metadata,
output: result.output,
attachments: result.attachments,
attachments,
time: {
...part.state.time,
end: Date.now(),
@@ -771,16 +782,13 @@ export namespace SessionPrompt {
)
const textParts: string[] = []
const attachments: MessageV2.FilePart[] = []
const attachments: Omit<MessageV2.FilePart, "id" | "messageID" | "sessionID">[] = []
for (const contentItem of result.content) {
if (contentItem.type === "text") {
textParts.push(contentItem.text)
} else if (contentItem.type === "image") {
attachments.push({
id: Identifier.ascending("part"),
sessionID: input.session.id,
messageID: input.processor.message.id,
type: "file",
mime: contentItem.mimeType,
url: `data:${contentItem.mimeType};base64,${contentItem.data}`,
@@ -792,9 +800,6 @@ export namespace SessionPrompt {
}
if (resource.blob) {
attachments.push({
id: Identifier.ascending("part"),
sessionID: input.session.id,
messageID: input.processor.message.id,
type: "file",
mime: resource.mimeType ?? "application/octet-stream",
url: `data:${resource.mimeType ?? "application/octet-stream"};base64,${resource.blob}`,
@@ -1032,6 +1037,7 @@ export namespace SessionPrompt {
pieces.push(
...result.attachments.map((attachment) => ({
...attachment,
id: Identifier.ascending("part"),
synthetic: true,
filename: attachment.filename ?? part.filename,
messageID: info.id,
@@ -1169,7 +1175,18 @@ export namespace SessionPrompt {
},
]
}),
).then((x) => x.flat())
)
.then((x) => x.flat())
.then((drafts) =>
drafts.map(
(part): MessageV2.Part => ({
...part,
id: Identifier.ascending("part"),
messageID: info.id,
sessionID: input.sessionID,
}),
),
)
await Plugin.trigger(
"chat.message",

View File

@@ -77,6 +77,12 @@ export const BatchTool = Tool.define("batch", async () => {
})
const result = await tool.execute(validatedParams, { ...ctx, callID: partID })
const attachments = result.attachments?.map((attachment) => ({
...attachment,
id: Identifier.ascending("part"),
messageID: ctx.messageID,
sessionID: ctx.sessionID,
}))
await Session.updatePart({
id: partID,
@@ -91,7 +97,7 @@ export const BatchTool = Tool.define("batch", async () => {
output: result.output,
title: result.title,
metadata: result.metadata,
attachments: result.attachments,
attachments,
time: {
start: callStartTime,
end: Date.now(),

View File

@@ -6,7 +6,6 @@ import { LSP } from "../lsp"
import { FileTime } from "../file/time"
import DESCRIPTION from "./read.txt"
import { Instance } from "../project/instance"
import { Identifier } from "../id/id"
import { assertExternalDirectory } from "./external-directory"
import { InstructionPrompt } from "../session/instruction"
@@ -79,9 +78,6 @@ export const ReadTool = Tool.define("read", {
},
attachments: [
{
id: Identifier.ascending("part"),
sessionID: ctx.sessionID,
messageID: ctx.messageID,
type: "file",
mime,
url: `data:${mime};base64,${Buffer.from(await file.bytes()).toString("base64")}`,

View File

@@ -36,7 +36,7 @@ export namespace Tool {
title: string
metadata: M
output: string
attachments?: MessageV2.FilePart[]
attachments?: Omit<MessageV2.FilePart, "id" | "sessionID" | "messageID">[]
}>
formatValidationError?(error: z.ZodError): string
}>

View File

@@ -0,0 +1,62 @@
import path from "path"
import { describe, expect, test } from "bun:test"
import { Session } from "../../src/session"
import { SessionPrompt } from "../../src/session/prompt"
import { MessageV2 } from "../../src/session/message-v2"
import { Instance } from "../../src/project/instance"
import { Log } from "../../src/util/log"
import { tmpdir } from "../fixture/fixture"
Log.init({ print: false })
describe("SessionPrompt ordering", () => {
test("keeps @file order with read output parts", async () => {
await using tmp = await tmpdir({
git: true,
init: async (dir) => {
await Bun.write(path.join(dir, "a.txt"), "28\n")
await Bun.write(path.join(dir, "b.txt"), "42\n")
},
})
await Instance.provide({
directory: tmp.path,
fn: async () => {
const session = await Session.create({})
const template = "What numbers are written in files @a.txt and @b.txt ?"
const parts = await SessionPrompt.resolvePromptParts(template)
const fileParts = parts.filter((part) => part.type === "file")
expect(fileParts.map((part) => part.filename)).toStrictEqual(["a.txt", "b.txt"])
const message = await SessionPrompt.prompt({
sessionID: session.id,
parts,
noReply: true,
})
const stored = await MessageV2.get({ sessionID: session.id, messageID: message.info.id })
const items = stored.parts
const aPath = path.join(tmp.path, "a.txt")
const bPath = path.join(tmp.path, "b.txt")
const sequence = items.flatMap((part) => {
if (part.type === "text") {
if (part.text.includes(aPath)) return ["input:a"]
if (part.text.includes(bPath)) return ["input:b"]
if (part.text.includes("00001| 28")) return ["output:a"]
if (part.text.includes("00001| 42")) return ["output:b"]
return []
}
if (part.type === "file") {
if (part.filename === "a.txt") return ["file:a"]
if (part.filename === "b.txt") return ["file:b"]
}
return []
})
expect(sequence).toStrictEqual(["input:a", "output:a", "file:a", "input:b", "output:b", "file:b"])
await Session.remove(session.id)
},
})
})
})

View File

@@ -1,7 +1,7 @@
{
"$schema": "https://json.schemastore.org/package.json",
"name": "@opencode-ai/plugin",
"version": "0.0.0-ci-202601291718",
"version": "1.1.46",
"type": "module",
"license": "MIT",
"scripts": {

View File

@@ -1,7 +1,7 @@
{
"$schema": "https://json.schemastore.org/package.json",
"name": "@opencode-ai/sdk",
"version": "0.0.0-ci-202601291718",
"version": "1.1.46",
"type": "module",
"license": "MIT",
"scripts": {

View File

@@ -1779,26 +1779,6 @@ 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

View File

@@ -9834,69 +9834,6 @@
"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"
},

View File

@@ -1,6 +1,6 @@
{
"name": "@opencode-ai/slack",
"version": "0.0.0-ci-202601291718",
"version": "1.1.46",
"type": "module",
"license": "MIT",
"scripts": {

View File

@@ -1,6 +1,6 @@
{
"name": "@opencode-ai/ui",
"version": "0.0.0-ci-202601291718",
"version": "1.1.46",
"type": "module",
"license": "MIT",
"exports": {

View File

@@ -36,7 +36,9 @@
border-radius: var(--radius-md);
overflow: clip;
color: var(--text-strong);
transition: background-color 0.15s ease;
transition-property: background-color, border-color;
transition-duration: var(--transition-duration);
transition-timing-function: var(--transition-easing);
/* text-12-regular */
font-family: var(--font-family-sans);
@@ -58,41 +60,48 @@
}
}
&[data-expanded] {
[data-slot="accordion-trigger"] {
border-bottom-left-radius: 0;
border-bottom-right-radius: 0;
}
[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-arrow"] {
flex-shrink: 0;
width: 16px;
height: 16px;
display: flex;
align-items: center;
justify-content: center;
color: var(--text-weak);
}
[data-slot="accordion-content"] {
overflow: hidden;
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"][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;
}
}
}
@keyframes slideDown {
from {
height: 0;
}
to {
height: var(--kb-accordion-content-height);
}
}
@keyframes slideUp {
from {
height: var(--kb-accordion-content-height);
}
to {
height: 0;
}
}

View File

@@ -1,6 +1,7 @@
import { Accordion as Kobalte } from "@kobalte/core/accordion"
import { splitProps } from "solid-js"
import { Accessor, createContext, splitProps, useContext } 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> {}
@@ -8,6 +9,8 @@ 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 (
@@ -22,17 +25,19 @@ function AccordionRoot(props: AccordionProps) {
)
}
function AccordionItem(props: AccordionItemProps) {
const [split, rest] = splitProps(props, ["class", "classList"])
function AccordionItem(props: AccordionItemProps & { expanded?: boolean }) {
const [split, rest] = splitProps(props, ["class", "classList", "expanded"])
return (
<Kobalte.Item
{...rest}
data-slot="accordion-item"
classList={{
...(split.classList ?? {}),
[split.class ?? ""]: !!split.class,
}}
/>
<AccordionItemContext.Provider value={() => split.expanded ?? false}>
<Kobalte.Item
{...rest}
data-slot="accordion-item"
classList={{
...(split.classList ?? {}),
[split.class ?? ""]: !!split.class,
}}
/>
</AccordionItemContext.Provider>
)
}
@@ -84,9 +89,25 @@ 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,
})

View File

@@ -8,8 +8,13 @@
text-decoration: none;
user-select: none;
cursor: default;
outline: none;
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;
&[data-variant="primary"] {
background-color: var(--button-primary-base);
@@ -94,7 +99,6 @@
&:active:not(:disabled) {
background-color: var(--button-secondary-base);
scale: 0.99;
transition: all 150ms ease-out;
}
&:disabled {
border-color: var(--border-disabled);
@@ -109,34 +113,27 @@
}
&[data-size="small"] {
height: 22px;
padding: 0 8px;
padding: 2px 8px;
&[data-icon] {
padding: 0 12px 0 4px;
padding: 2px 12px 2px 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-small);
font-size: var(--font-size-base);
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"] {
height: 24px;
line-height: 24px;
padding: 0 6px;
padding: 4px 6px;
&[data-icon] {
padding: 0 12px 0 4px;
padding: 4px 12px 4px 4px;
}
font-size: var(--font-size-small);
gap: 6px;
/* text-12-medium */
@@ -148,11 +145,10 @@
}
&[data-size="large"] {
height: 32px;
padding: 6px 12px;
&[data-icon] {
padding: 0 12px 0 8px;
padding: 6px 12px 6px 8px;
}
gap: 4px;
@@ -162,7 +158,6 @@
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);
}

View File

@@ -4,7 +4,7 @@ import { Icon, IconProps } from "./icon"
export interface ButtonProps
extends ComponentProps<typeof Kobalte>,
Pick<ComponentProps<"button">, "class" | "classList" | "children"> {
Pick<ComponentProps<"button">, "class" | "classList" | "children" | "style"> {
size?: "small" | "normal" | "large"
variant?: "primary" | "secondary" | "ghost"
icon?: IconProps["name"]

View File

@@ -4,7 +4,9 @@
flex-direction: column;
background-color: var(--surface-inset-base);
border: 1px solid var(--border-weaker-base);
transition: background-color 0.15s ease;
transition-property: background-color, border-color;
transition-duration: var(--transition-duration);
transition-timing-function: var(--transition-easing);
border-radius: var(--radius-md);
padding: 6px 12px;
overflow: clip;

View File

@@ -4,6 +4,18 @@
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;

View File

@@ -4,7 +4,9 @@
flex-direction: column;
background-color: var(--surface-inset-base);
border: 1px solid var(--border-weaker-base);
transition: background-color 0.15s ease;
transition-property: background-color, border-color;
transition-duration: var(--transition-duration);
transition-timing-function: var(--transition-easing);
border-radius: var(--radius-md);
overflow: clip;
@@ -44,16 +46,28 @@
display: flex;
align-items: center;
justify-content: center;
color: var(--text-weak);
}
}
[data-slot="collapsible-content"] {
overflow: hidden;
/* animation: slideUp 250ms ease-out; */
display: grid;
grid-template-rows: 0fr;
transition-property: grid-template-rows, opacity;
transition-duration: var(--transition-duration);
transition-timing-function: var(--transition-easing);
/* &[data-expanded] { */
/* animation: slideDown 250ms ease-out; */
/* } */
> * {
overflow: hidden;
}
&[data-expanded] {
grid-template-rows: 1fr;
}
&[data-closed] {
grid-template-rows: 0fr;
}
}
&[data-variant="ghost"] {
@@ -83,21 +97,3 @@
}
}
}
@keyframes slideDown {
from {
height: 0;
}
to {
height: var(--kb-collapsible-content-height);
}
}
@keyframes slideUp {
from {
height: var(--kb-collapsible-content-height);
}
to {
height: 0;
}
}

View File

@@ -1,6 +1,8 @@
import { Collapsible as Kobalte, CollapsibleRootProps } from "@kobalte/core/collapsible"
import { ComponentProps, ParentProps, splitProps } from "solid-js"
import { Icon } from "./icon"
import { Accessor, ComponentProps, createContext, createSignal, ParentProps, splitProps, useContext } from "solid-js"
import { MorphChevron } from "./morph-chevron"
const CollapsibleContext = createContext<Accessor<boolean>>()
export interface CollapsibleProps extends ParentProps<CollapsibleRootProps> {
class?: string
@@ -9,17 +11,30 @@ export interface CollapsibleProps extends ParentProps<CollapsibleRootProps> {
}
function CollapsibleRoot(props: CollapsibleProps) {
const [local, others] = splitProps(props, ["class", "classList", "variant"])
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)
}
return (
<Kobalte
data-component="collapsible"
data-variant={local.variant || "normal"}
classList={{
...(local.classList ?? {}),
[local.class ?? ""]: !!local.class,
}}
{...others}
/>
<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>
)
}
@@ -32,9 +47,10 @@ function CollapsibleContent(props: ComponentProps<typeof Kobalte.Content>) {
}
function CollapsibleArrow(props?: ComponentProps<"div">) {
const isOpen = useContext(CollapsibleContext)
return (
<div data-slot="collapsible-arrow" {...(props || {})}>
<Icon name="chevron-grabber-vertical" size="small" />
<MorphChevron expanded={isOpen?.() ?? false} />
</div>
)
}

View File

@@ -0,0 +1,51 @@
.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));
}
}
}

View File

@@ -0,0 +1,132 @@
import "./cycle-label.css"
import { createEffect, createSignal, JSX, on } from "solid-js"
export interface CycleLabelProps extends JSX.HTMLAttributes<HTMLSpanElement> {
value: string
onValueChange?: (value: string) => void
duration?: number | ((value: string) => number)
stagger?: number
opacity?: [number, number]
blur?: [number, number]
skewX?: number
onAnimationStart?: () => void
onAnimationEnd?: () => void
}
const segmenter =
typeof Intl !== "undefined" && Intl.Segmenter ? new Intl.Segmenter("en", { granularity: "grapheme" }) : null
const getChars = (text: string): string[] =>
segmenter ? Array.from(segmenter.segment(text), (s) => s.segment) : text.split("")
const wait = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms))
export function CycleLabel(props: CycleLabelProps) {
const getDuration = (text: string) => {
const d = props?.duration ?? 200
return typeof d === "function" ? d(text) : d
}
const stagger = () => props?.stagger ?? 20
const opacity = () => props?.opacity ?? [0, 1]
const blur = () => props?.blur ?? [0, 0]
const skewX = () => props?.skewX ?? 10
let containerRef: HTMLSpanElement | undefined
let isAnimating = false
const [currentText, setCurrentText] = createSignal(props.value)
const setChars = (el: HTMLElement, text: string, state: "enter" | "exit" | "pre" = "enter") => {
el.innerHTML = ""
const chars = getChars(text)
chars.forEach((char, i) => {
const span = document.createElement("span")
span.textContent = char === " " ? "\u00A0" : char
span.className = `cycle-char ${state}`
span.style.setProperty("--i", String(i))
el.appendChild(span)
})
}
const animateToText = async (newText: string) => {
if (!containerRef || isAnimating) return
if (newText === currentText()) return
isAnimating = true
props.onAnimationStart?.()
const dur = getDuration(newText)
const stag = stagger()
containerRef.style.width = containerRef.offsetWidth + "px"
const oldChars = containerRef.querySelectorAll(".cycle-char")
oldChars.forEach((c) => c.classList.replace("enter", "exit"))
const clone = containerRef.cloneNode(false) as HTMLElement
Object.assign(clone.style, {
position: "absolute",
visibility: "hidden",
width: "auto",
transition: "none",
})
setChars(clone, newText)
document.body.appendChild(clone)
const nextWidth = clone.offsetWidth
clone.remove()
const exitTime = oldChars.length * stag + dur
await wait(exitTime * 0.3)
containerRef.style.width = nextWidth + "px"
const widthDur = 200
await wait(widthDur * 0.3)
setChars(containerRef, newText, "pre")
containerRef.offsetWidth
Array.from(containerRef.children).forEach((c) => (c.className = "cycle-char enter"))
setCurrentText(newText)
props.onValueChange?.(newText)
const enterTime = getChars(newText).length * stag + dur
await wait(enterTime)
containerRef.style.width = ""
isAnimating = false
props.onAnimationEnd?.()
}
createEffect(
on(
() => props.value,
(newValue) => {
if (newValue !== currentText()) {
animateToText(newValue)
}
},
),
)
const initRef = (el: HTMLSpanElement) => {
containerRef = el
setChars(el, props.value)
}
return (
<span
ref={initRef}
class={`cycle-label ${props.class ?? ""}`}
style={{
"--c-dur": `${getDuration(currentText())}ms`,
"--c-stag": `${stagger()}ms`,
"--c-opacity-start": opacity()[0],
"--c-opacity-end": opacity()[1],
"--c-blur-start": `${blur()[0]}px`,
"--c-blur-end": `${blur()[1]}px`,
"--c-skew": `${skewX()}deg`,
...(typeof props.style === "object" ? props.style : {}),
}}
/>
)
}

View File

@@ -5,6 +5,16 @@
inset: 0;
z-index: 50;
background-color: hsl(from var(--background-base) h s l / 0.2);
animation: overlayHide var(--transition-duration) var(--transition-easing) forwards;
&[data-expanded] {
animation: overlayShow var(--transition-duration) var(--transition-easing) forwards;
}
@starting-style {
animation: none;
}
}
[data-component="dialog"] {
@@ -25,7 +35,6 @@
flex-direction: column;
align-items: center;
justify-items: start;
overflow: visible;
[data-slot="dialog-content"] {
display: flex;
@@ -35,16 +44,8 @@
width: 100%;
max-height: 100%;
min-height: 280px;
overflow: auto;
pointer-events: auto;
/* Hide scrollbar */
scrollbar-width: none;
-ms-overflow-style: none;
&::-webkit-scrollbar {
display: none;
}
/* padding: 8px; */
/* padding: 8px 8px 0 8px; */
border-radius: var(--radius-xl);
@@ -52,6 +53,16 @@
background-clip: padding-box;
box-shadow: var(--shadow-lg-border-base);
animation: contentHide var(--transition-duration) var(--transition-easing) forwards;
&[data-expanded] {
animation: contentShow var(--transition-duration) var(--transition-easing) forwards;
}
@starting-style {
animation: none;
}
[data-slot="dialog-header"] {
display: flex;
padding: 20px;
@@ -162,7 +173,7 @@
@keyframes contentShow {
from {
opacity: 0;
transform: scale(0.98);
transform: translateY(2.5%) scale(0.975);
}
to {
opacity: 1;
@@ -176,6 +187,6 @@
}
to {
opacity: 0;
transform: scale(0.98);
transform: translateY(-2.5%) scale(0.975);
}
}

View File

@@ -2,26 +2,29 @@
[data-component="dropdown-menu-sub-content"] {
min-width: 8rem;
overflow: hidden;
border: none;
border-radius: var(--radius-md);
border: 1px solid color-mix(in oklch, var(--border-base) 50%, transparent);
box-shadow: var(--shadow-xs-border);
background-clip: padding-box;
background-color: var(--surface-raised-stronger-non-alpha);
padding: 4px;
box-shadow: var(--shadow-md);
z-index: 50;
z-index: 100;
transform-origin: var(--kb-menu-content-transform-origin);
&:focus,
&:focus-visible {
&:focus-within,
&:focus {
outline: none;
}
&[data-closed] {
animation: dropdown-menu-close 0.15s ease-out;
animation: dropdownMenuContentHide var(--transition-duration) var(--transition-easing) forwards;
@starting-style {
animation: none;
}
&[data-expanded] {
animation: dropdown-menu-open 0.15s ease-out;
pointer-events: auto;
animation: dropdownMenuContentShow var(--transition-duration) var(--transition-easing) forwards;
}
}
@@ -38,18 +41,22 @@
padding: 4px 8px;
border-radius: var(--radius-sm);
cursor: default;
user-select: none;
outline: none;
font-family: var(--font-family-sans);
font-size: var(--font-size-small);
font-size: var(--font-size-base);
font-weight: var(--font-weight-medium);
line-height: var(--line-height-large);
letter-spacing: var(--letter-spacing-normal);
color: var(--text-strong);
&[data-highlighted] {
background: var(--surface-raised-base-hover);
transition-property: background-color, color;
transition-duration: var(--transition-duration);
transition-timing-function: var(--transition-easing);
user-select: none;
&:hover {
background-color: var(--surface-raised-base-hover);
}
&[data-disabled] {
@@ -61,6 +68,8 @@
[data-slot="dropdown-menu-sub-trigger"] {
&[data-expanded] {
background: var(--surface-raised-base-hover);
outline: none;
border: none;
}
}
@@ -102,24 +111,24 @@
}
}
@keyframes dropdown-menu-open {
@keyframes dropdownMenuContentShow {
from {
opacity: 0;
transform: scale(0.96);
transform: scaleY(0.95);
}
to {
opacity: 1;
transform: scale(1);
transform: scaleY(1);
}
}
@keyframes dropdown-menu-close {
@keyframes dropdownMenuContentHide {
from {
opacity: 1;
transform: scale(1);
transform: scaleY(1);
}
to {
opacity: 0;
transform: scale(0.96);
transform: scaleY(0.95);
}
}

View File

@@ -24,11 +24,11 @@
}
&[data-closed] {
animation: hover-card-close 0.15s ease-out;
animation: hover-card-close var(--transition-duration) var(--transition-easing);
}
&[data-expanded] {
animation: hover-card-open 0.15s ease-out;
animation: hover-card-open var(--transition-duration) var(--transition-easing);
}
[data-slot="hover-card-body"] {

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