+
{
inputRef = el
@@ -895,18 +794,3 @@ export default function Page() {
)
}
-
-function StickyAccordionHeader(props: ParentProps<{ class?: string }>) {
- return (
-
- {props.children}
-
- )
-}
diff --git a/packages/extensions/zed/extension.toml b/packages/extensions/zed/extension.toml
index 220780709..5572d0d63 100644
--- a/packages/extensions/zed/extension.toml
+++ b/packages/extensions/zed/extension.toml
@@ -1,7 +1,7 @@
id = "opencode"
name = "OpenCode"
description = "The AI coding agent built for the terminal"
-version = "1.0.62"
+version = "1.0.65"
schema_version = 1
authors = ["Anomaly"]
repository = "https://github.com/sst/opencode"
@@ -11,26 +11,26 @@ name = "OpenCode"
icon = "./icons/opencode.svg"
[agent_servers.opencode.targets.darwin-aarch64]
-archive = "https://github.com/sst/opencode/releases/download/v1.0.62/opencode-darwin-arm64.zip"
+archive = "https://github.com/sst/opencode/releases/download/v1.0.65/opencode-darwin-arm64.zip"
cmd = "./opencode"
args = ["acp"]
[agent_servers.opencode.targets.darwin-x86_64]
-archive = "https://github.com/sst/opencode/releases/download/v1.0.62/opencode-darwin-x64.zip"
+archive = "https://github.com/sst/opencode/releases/download/v1.0.65/opencode-darwin-x64.zip"
cmd = "./opencode"
args = ["acp"]
[agent_servers.opencode.targets.linux-aarch64]
-archive = "https://github.com/sst/opencode/releases/download/v1.0.62/opencode-linux-arm64.zip"
+archive = "https://github.com/sst/opencode/releases/download/v1.0.65/opencode-linux-arm64.zip"
cmd = "./opencode"
args = ["acp"]
[agent_servers.opencode.targets.linux-x86_64]
-archive = "https://github.com/sst/opencode/releases/download/v1.0.62/opencode-linux-x64.zip"
+archive = "https://github.com/sst/opencode/releases/download/v1.0.65/opencode-linux-x64.zip"
cmd = "./opencode"
args = ["acp"]
[agent_servers.opencode.targets.windows-x86_64]
-archive = "https://github.com/sst/opencode/releases/download/v1.0.62/opencode-windows-x64.zip"
+archive = "https://github.com/sst/opencode/releases/download/v1.0.65/opencode-windows-x64.zip"
cmd = "./opencode.exe"
args = ["acp"]
diff --git a/packages/function/package.json b/packages/function/package.json
index dbe47b50d..911ac02c6 100644
--- a/packages/function/package.json
+++ b/packages/function/package.json
@@ -1,6 +1,6 @@
{
"name": "@opencode-ai/function",
- "version": "1.0.62",
+ "version": "1.0.65",
"$schema": "https://json.schemastore.org/package.json",
"private": true,
"type": "module",
diff --git a/packages/opencode/bin/opencode b/packages/opencode/bin/opencode
index 8f75eb189..e35cc0094 100755
--- a/packages/opencode/bin/opencode
+++ b/packages/opencode/bin/opencode
@@ -1,61 +1,84 @@
-#!/bin/sh
-set -e
+#!/usr/bin/env node
-if [ -n "$OPENCODE_BIN_PATH" ]; then
- resolved="$OPENCODE_BIN_PATH"
-else
- # Get the real path of this script, resolving any symlinks
- script_path="$0"
- while [ -L "$script_path" ]; do
- link_target="$(readlink "$script_path")"
- case "$link_target" in
- /*) script_path="$link_target" ;;
- *) script_path="$(dirname "$script_path")/$link_target" ;;
- esac
- done
- script_dir="$(dirname "$script_path")"
- script_dir="$(cd "$script_dir" && pwd)"
-
- # Map platform names
- case "$(uname -s)" in
- Darwin) platform="darwin" ;;
- Linux) platform="linux" ;;
- MINGW*|CYGWIN*|MSYS*) platform="win32" ;;
- *) platform="$(uname -s | tr '[:upper:]' '[:lower:]')" ;;
- esac
-
- # Map architecture names
- case "$(uname -m)" in
- x86_64|amd64) arch="x64" ;;
- aarch64) arch="arm64" ;;
- armv7l) arch="arm" ;;
- *) arch="$(uname -m)" ;;
- esac
-
- name="opencode-${platform}-${arch}"
- binary="opencode"
- [ "$platform" = "win32" ] && binary="opencode.exe"
-
- # Search for the binary starting from real script location
- resolved=""
- current_dir="$script_dir"
- while [ "$current_dir" != "/" ]; do
- candidate="$current_dir/node_modules/$name/bin/$binary"
- if [ -f "$candidate" ]; then
- resolved="$candidate"
- break
- fi
- current_dir="$(dirname "$current_dir")"
- done
-
- if [ -z "$resolved" ]; then
- printf "It seems that your package manager failed to install the right version of the opencode CLI for your platform. You can try manually installing the \"%s\" package\n" "$name" >&2
- exit 1
- fi
-fi
+const childProcess = require("child_process")
+const fs = require("fs")
+const path = require("path")
+const os = require("os")
-# Handle SIGINT gracefully
-trap '' INT
+function run(target) {
+ const result = childProcess.spawnSync(target, process.argv.slice(2), {
+ stdio: "inherit",
+ })
+ if (result.error) {
+ console.error(result.error.message)
+ process.exit(1)
+ }
+ const code = typeof result.status === "number" ? result.status : 0
+ process.exit(code)
+}
-# Execute the binary with all arguments
-exec "$resolved" "$@"
+const envPath = process.env.OPENCODE_BIN_PATH
+if (envPath) {
+ run(envPath)
+}
+
+const scriptPath = fs.realpathSync(__filename)
+const scriptDir = path.dirname(scriptPath)
+
+const platformMap = {
+ darwin: "darwin",
+ linux: "linux",
+ win32: "windows",
+}
+const archMap = {
+ x64: "x64",
+ arm64: "arm64",
+ arm: "arm",
+}
+
+let platform = platformMap[os.platform()]
+if (!platform) {
+ platform = os.platform()
+}
+let arch = archMap[os.arch()]
+if (!arch) {
+ arch = os.arch()
+}
+const base = "opencode-" + platform + "-" + arch
+const binary = platform === "windows" ? "opencode.exe" : "opencode"
+
+function findBinary(startDir) {
+ let current = startDir
+ for (;;) {
+ const modules = path.join(current, "node_modules")
+ if (fs.existsSync(modules)) {
+ const entries = fs.readdirSync(modules)
+ for (const entry of entries) {
+ if (!entry.startsWith(base)) {
+ continue
+ }
+ const candidate = path.join(modules, entry, "bin", binary)
+ if (fs.existsSync(candidate)) {
+ return candidate
+ }
+ }
+ }
+ const parent = path.dirname(current)
+ if (parent === current) {
+ return
+ }
+ current = parent
+ }
+}
+
+const resolved = findBinary(scriptDir)
+if (!resolved) {
+ console.error(
+ 'It seems that your package manager failed to install the right version of the opencode CLI for your platform. You can try manually installing the "' +
+ base +
+ '" package',
+ )
+ process.exit(1)
+}
+
+run(resolved)
diff --git a/packages/opencode/bin/opencode.cmd b/packages/opencode/bin/opencode.cmd
deleted file mode 100644
index 775bfe688..000000000
--- a/packages/opencode/bin/opencode.cmd
+++ /dev/null
@@ -1,58 +0,0 @@
-@echo off
-setlocal enabledelayedexpansion
-
-if defined OPENCODE_BIN_PATH (
- set "resolved=%OPENCODE_BIN_PATH%"
- goto :execute
-)
-
-rem Get the directory of this script
-set "script_dir=%~dp0"
-set "script_dir=%script_dir:~0,-1%"
-
-rem Detect platform and architecture
-set "platform=windows"
-
-rem Detect architecture
-if "%PROCESSOR_ARCHITECTURE%"=="AMD64" (
- set "arch=x64"
-) else if "%PROCESSOR_ARCHITECTURE%"=="ARM64" (
- set "arch=arm64"
-) else if "%PROCESSOR_ARCHITECTURE%"=="x86" (
- set "arch=x86"
-) else (
- set "arch=x64"
-)
-
-set "name=opencode-!platform!-!arch!"
-set "binary=opencode.exe"
-
-rem Search for the binary starting from script location
-set "resolved="
-set "current_dir=%script_dir%"
-
-:search_loop
-set "candidate=%current_dir%\node_modules\%name%\bin\%binary%"
-if exist "%candidate%" (
- set "resolved=%candidate%"
- goto :execute
-)
-
-rem Move up one directory
-for %%i in ("%current_dir%") do set "parent_dir=%%~dpi"
-set "parent_dir=%parent_dir:~0,-1%"
-
-rem Check if we've reached the root
-if "%current_dir%"=="%parent_dir%" goto :not_found
-set "current_dir=%parent_dir%"
-goto :search_loop
-
-:not_found
-echo It seems that your package manager failed to install the right version of the opencode CLI for your platform. You can try manually installing the "%name%" package >&2
-exit /b 1
-
-:execute
-rem Execute the binary with all arguments in the same console window
-rem Use start /b /wait to ensure it runs in the current shell context for all shells
-start /b /wait "" "%resolved%" %*
-exit /b %ERRORLEVEL%
diff --git a/packages/opencode/package.json b/packages/opencode/package.json
index 76dcffb84..764a40683 100644
--- a/packages/opencode/package.json
+++ b/packages/opencode/package.json
@@ -1,6 +1,6 @@
{
"$schema": "https://json.schemastore.org/package.json",
- "version": "1.0.62",
+ "version": "1.0.65",
"name": "opencode",
"type": "module",
"private": true,
diff --git a/packages/opencode/script/build.ts b/packages/opencode/script/build.ts
index 97631d907..502baed02 100755
--- a/packages/opencode/script/build.ts
+++ b/packages/opencode/script/build.ts
@@ -66,11 +66,11 @@ const allTargets: {
avx2: false,
},
{
- os: "windows",
+ os: "win32",
arch: "x64",
},
{
- os: "windows",
+ os: "win32",
arch: "x64",
avx2: false,
},
@@ -88,7 +88,8 @@ await $`bun install --os="*" --cpu="*" @parcel/watcher@${pkg.dependencies["@parc
for (const item of targets) {
const name = [
pkg.name,
- item.os,
+ // changing to win32 flags npm for some reason
+ item.os === "win32" ? "windows" : item.os,
item.arch,
item.avx2 === false ? "baseline" : undefined,
item.abi === undefined ? undefined : item.abi,
@@ -115,7 +116,7 @@ for (const item of targets) {
entrypoints: ["./src/index.ts", parserWorker, workerPath],
define: {
OPENCODE_VERSION: `'${Script.version}'`,
- OTUI_TREE_SITTER_WORKER_PATH: "/$bunfs/root/" + path.relative(dir, parserWorker),
+ OTUI_TREE_SITTER_WORKER_PATH: "/$bunfs/root/" + path.relative(dir, parserWorker).replaceAll("\\", "/"),
OPENCODE_WORKER_PATH: workerPath,
OPENCODE_CHANNEL: `'${Script.channel}'`,
},
@@ -127,7 +128,7 @@ for (const item of targets) {
{
name,
version: Script.version,
- os: [item.os === "windows" ? "win32" : item.os],
+ os: [item.os],
cpu: [item.arch],
},
null,
diff --git a/packages/opencode/script/postinstall.mjs b/packages/opencode/script/postinstall.mjs
index b875d158f..78f022c9f 100644
--- a/packages/opencode/script/postinstall.mjs
+++ b/packages/opencode/script/postinstall.mjs
@@ -50,79 +50,66 @@ function detectPlatformAndArch() {
function findBinary() {
const { platform, arch } = detectPlatformAndArch()
const packageName = `opencode-${platform}-${arch}`
- const binary = platform === "windows" ? "opencode.exe" : "opencode"
+ const binaryName = platform === "windows" ? "opencode.exe" : "opencode"
try {
// Use require.resolve to find the package
const packageJsonPath = require.resolve(`${packageName}/package.json`)
const packageDir = path.dirname(packageJsonPath)
- const binaryPath = path.join(packageDir, "bin", binary)
+ const binaryPath = path.join(packageDir, "bin", binaryName)
if (!fs.existsSync(binaryPath)) {
throw new Error(`Binary not found at ${binaryPath}`)
}
- return binaryPath
+ return { binaryPath, binaryName }
} catch (error) {
throw new Error(`Could not find package ${packageName}: ${error.message}`)
}
}
-async function regenerateWindowsCmdWrappers() {
- console.log("Windows + npm detected: Forcing npm to rebuild bin links")
+function prepareBinDirectory(binaryName) {
+ const binDir = path.join(__dirname, "bin")
+ const targetPath = path.join(binDir, binaryName)
- try {
- const { execSync } = require("child_process")
- const pkgPath = path.join(__dirname, "..")
+ // Ensure bin directory exists
+ if (!fs.existsSync(binDir)) {
+ fs.mkdirSync(binDir, { recursive: true })
+ }
- // npm_config_global is string | undefined
- // if it exists, the value is true
- const isGlobal = process.env.npm_config_global === "true" || pkgPath.includes(path.join("npm", "node_modules"))
+ // Remove existing binary/symlink if it exists
+ if (fs.existsSync(targetPath)) {
+ fs.unlinkSync(targetPath)
+ }
- // The npm rebuild command does 2 things - Execute lifecycle scripts and rebuild bin links
- // We want to skip lifecycle scripts to avoid infinite loops, so we use --ignore-scripts
- const cmd = `npm rebuild opencode-ai --ignore-scripts${isGlobal ? " -g" : ""}`
- const opts = {
- stdio: "inherit",
- shell: true,
- ...(isGlobal ? {} : { cwd: path.join(pkgPath, "..", "..") }), // For local, run from project root
- }
+ return { binDir, targetPath }
+}
- console.log(`Running: ${cmd}`)
- execSync(cmd, opts)
- console.log("Successfully rebuilt npm bin links")
- } catch (error) {
- console.error("Error rebuilding npm links:", error.message)
- console.error("npm rebuild failed. You may need to manually run: npm rebuild opencode-ai --ignore-scripts")
+function symlinkBinary(sourcePath, binaryName) {
+ const { targetPath } = prepareBinDirectory(binaryName)
+
+ fs.symlinkSync(sourcePath, targetPath)
+ console.log(`opencode binary symlinked: ${targetPath} -> ${sourcePath}`)
+
+ // Verify the file exists after operation
+ if (!fs.existsSync(targetPath)) {
+ throw new Error(`Failed to symlink binary to ${targetPath}`)
}
}
async function main() {
try {
if (os.platform() === "win32") {
- // NPM eg format - npm/11.4.2 node/v24.4.1 win32 x64
- // Bun eg format - bun/1.2.19 npm/? node/v24.3.0 win32 x64
- if (process.env.npm_config_user_agent.startsWith("npm")) {
- await regenerateWindowsCmdWrappers()
- } else {
- console.log("Windows detected but not npm, skipping postinstall")
- }
+ // On Windows, the .exe is already included in the package and bin field points to it
+ // No postinstall setup needed
+ console.log("Windows detected: binary setup not needed (using packaged .exe)")
return
}
- const binaryPath = findBinary()
- const binScript = path.join(__dirname, "bin", "opencode")
-
- // Remove existing bin script if it exists
- if (fs.existsSync(binScript)) {
- fs.unlinkSync(binScript)
- }
-
- // Create symlink to the actual binary
- fs.symlinkSync(binaryPath, binScript)
- console.log(`opencode binary symlinked: ${binScript} -> ${binaryPath}`)
+ const { binaryPath, binaryName } = findBinary()
+ symlinkBinary(binaryPath, binaryName)
} catch (error) {
- console.error("Failed to create opencode binary symlink:", error.message)
+ console.error("Failed to setup opencode binary:", error.message)
process.exit(1)
}
}
diff --git a/packages/opencode/script/preinstall.mjs b/packages/opencode/script/preinstall.mjs
deleted file mode 100644
index dfe46d9e7..000000000
--- a/packages/opencode/script/preinstall.mjs
+++ /dev/null
@@ -1,44 +0,0 @@
-#!/usr/bin/env node
-
-import fs from "fs"
-import path from "path"
-import os from "os"
-import { fileURLToPath } from "url"
-
-const __dirname = path.dirname(fileURLToPath(import.meta.url))
-
-function main() {
- if (os.platform() !== "win32") {
- console.log("Non-Windows platform detected, skipping preinstall")
- return
- }
-
- console.log("Windows detected: Modifying package.json bin entry")
-
- // Read package.json
- const packageJsonPath = path.join(__dirname, "package.json")
- const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, "utf8"))
-
- // Modify bin to point to .cmd file on Windows
- packageJson.bin = {
- opencode: "./bin/opencode.cmd",
- }
-
- // Write it back
- fs.writeFileSync(packageJsonPath, JSON.stringify(packageJson, null, 2))
- console.log("Updated package.json bin to use opencode.cmd")
-
- // Now you can also remove the Unix script if you want
- const unixScript = path.join(__dirname, "bin", "opencode")
- if (fs.existsSync(unixScript)) {
- console.log("Removing Unix shell script")
- fs.unlinkSync(unixScript)
- }
-}
-
-try {
- main()
-} catch (error) {
- console.error("Preinstall script error:", error.message)
- process.exit(0)
-}
diff --git a/packages/opencode/script/publish.ts b/packages/opencode/script/publish.ts
index 3e989cc6a..d5afe56fb 100755
--- a/packages/opencode/script/publish.ts
+++ b/packages/opencode/script/publish.ts
@@ -2,8 +2,9 @@
import { $ } from "bun"
import pkg from "../package.json"
import { Script } from "@opencode-ai/script"
+import { fileURLToPath } from "url"
-const dir = new URL("..", import.meta.url).pathname
+const dir = fileURLToPath(new URL("..", import.meta.url))
process.chdir(dir)
const { binaries } = await import("./build.ts")
@@ -15,8 +16,8 @@ const { binaries } = await import("./build.ts")
await $`mkdir -p ./dist/${pkg.name}`
await $`cp -r ./bin ./dist/${pkg.name}/bin`
-await $`cp ./script/preinstall.mjs ./dist/${pkg.name}/preinstall.mjs`
await $`cp ./script/postinstall.mjs ./dist/${pkg.name}/postinstall.mjs`
+
await Bun.file(`./dist/${pkg.name}/package.json`).write(
JSON.stringify(
{
@@ -25,7 +26,6 @@ await Bun.file(`./dist/${pkg.name}/package.json`).write(
[pkg.name]: `./bin/${pkg.name}`,
},
scripts: {
- preinstall: "bun ./preinstall.mjs || node ./preinstall.mjs",
postinstall: "bun ./postinstall.mjs || node ./postinstall.mjs",
},
version: Script.version,
@@ -36,7 +36,15 @@ await Bun.file(`./dist/${pkg.name}/package.json`).write(
),
)
for (const [name] of Object.entries(binaries)) {
- await $`cd dist/${name} && chmod 777 -R . && bun publish --access public --tag ${Script.channel}`
+ try {
+ process.chdir(`./dist/${name}`)
+ if (process.platform !== "win32") {
+ await $`chmod 755 -R .`
+ }
+ await $`bun publish --access public --tag ${Script.channel}`
+ } finally {
+ process.chdir(dir)
+ }
}
await $`cd ./dist/${pkg.name} && bun publish --access public --tag ${Script.channel}`
diff --git a/packages/opencode/src/cli/cmd/tui/context/exit.tsx b/packages/opencode/src/cli/cmd/tui/context/exit.tsx
index 7d7feaa28..612c39157 100644
--- a/packages/opencode/src/cli/cmd/tui/context/exit.tsx
+++ b/packages/opencode/src/cli/cmd/tui/context/exit.tsx
@@ -1,13 +1,18 @@
import { useRenderer } from "@opentui/solid"
import { createSimpleContext } from "./helper"
+import { FormatError } from "@/cli/error"
export const { use: useExit, provider: ExitProvider } = createSimpleContext({
name: "Exit",
init: (input: { onExit?: () => Promise
}) => {
const renderer = useRenderer()
- return async () => {
+ return async (reason?: any) => {
renderer.destroy()
await input.onExit?.()
+ if (reason) {
+ const formatted = FormatError(reason) ?? JSON.stringify(reason)
+ process.stderr.write(formatted + "\n")
+ }
process.exit(0)
}
},
diff --git a/packages/opencode/src/cli/cmd/tui/context/sdk.tsx b/packages/opencode/src/cli/cmd/tui/context/sdk.tsx
index 655c68022..8b7564eb5 100644
--- a/packages/opencode/src/cli/cmd/tui/context/sdk.tsx
+++ b/packages/opencode/src/cli/cmd/tui/context/sdk.tsx
@@ -10,11 +10,6 @@ export const { use: useSDK, provider: SDKProvider } = createSimpleContext({
const sdk = createOpencodeClient({
baseUrl: props.url,
signal: abort.signal,
- fetch: (req) => {
- // @ts-ignore
- req.timeout = false
- return fetch(req)
- },
})
const emitter = createGlobalEmitter<{
diff --git a/packages/opencode/src/cli/cmd/tui/context/sync.tsx b/packages/opencode/src/cli/cmd/tui/context/sync.tsx
index 7fcbbc57c..2c994a4a3 100644
--- a/packages/opencode/src/cli/cmd/tui/context/sync.tsx
+++ b/packages/opencode/src/cli/cmd/tui/context/sync.tsx
@@ -18,6 +18,8 @@ import { useSDK } from "@tui/context/sdk"
import { Binary } from "@/util/binary"
import { createSimpleContext } from "./helper"
import type { Snapshot } from "@/snapshot"
+import { useExit } from "./exit"
+import { onMount } from "solid-js"
export const { use: useSync, provider: SyncProvider } = createSimpleContext({
name: "Sync",
@@ -226,29 +228,37 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({
}
})
- // blocking
- Promise.all([
- sdk.client.config.providers({ throwOnError: true }).then((x) => setStore("provider", x.data!.providers)),
- sdk.client.app.agents({ throwOnError: true }).then((x) => setStore("agent", x.data ?? [])),
- sdk.client.config.get({ throwOnError: true }).then((x) => setStore("config", x.data!)),
- ]).then(() => {
- setStore("status", "partial")
- // non-blocking
+ const exit = useExit()
+
+ onMount(() => {
+ // blocking
Promise.all([
- sdk.client.session.list().then((x) =>
- setStore(
- "session",
- (x.data ?? []).toSorted((a, b) => a.id.localeCompare(b.id)),
- ),
- ),
- sdk.client.command.list().then((x) => setStore("command", x.data ?? [])),
- sdk.client.lsp.status().then((x) => setStore("lsp", x.data!)),
- sdk.client.mcp.status().then((x) => setStore("mcp", x.data!)),
- sdk.client.formatter.status().then((x) => setStore("formatter", x.data!)),
- sdk.client.session.status().then((x) => setStore("session_status", x.data!)),
- ]).then(() => {
- setStore("status", "complete")
- })
+ sdk.client.config.providers({ throwOnError: true }).then((x) => setStore("provider", x.data!.providers)),
+ sdk.client.app.agents({ throwOnError: true }).then((x) => setStore("agent", x.data ?? [])),
+ sdk.client.config.get({ throwOnError: true }).then((x) => setStore("config", x.data!)),
+ ])
+ .then(() => {
+ setStore("status", "partial")
+ // non-blocking
+ Promise.all([
+ sdk.client.session.list().then((x) =>
+ setStore(
+ "session",
+ (x.data ?? []).toSorted((a, b) => a.id.localeCompare(b.id)),
+ ),
+ ),
+ sdk.client.command.list().then((x) => setStore("command", x.data ?? [])),
+ sdk.client.lsp.status().then((x) => setStore("lsp", x.data!)),
+ sdk.client.mcp.status().then((x) => setStore("mcp", x.data!)),
+ sdk.client.formatter.status().then((x) => setStore("formatter", x.data!)),
+ sdk.client.session.status().then((x) => setStore("session_status", x.data!)),
+ ]).then(() => {
+ setStore("status", "complete")
+ })
+ })
+ .catch(async (e) => {
+ await exit(e)
+ })
})
const result = {
diff --git a/packages/opencode/src/cli/cmd/tui/routes/session/index.tsx b/packages/opencode/src/cli/cmd/tui/routes/session/index.tsx
index 64605e9df..1e5536a1e 100644
--- a/packages/opencode/src/cli/cmd/tui/routes/session/index.tsx
+++ b/packages/opencode/src/cli/cmd/tui/routes/session/index.tsx
@@ -17,7 +17,14 @@ import { useRoute, useRouteData } from "@tui/context/route"
import { useSync } from "@tui/context/sync"
import { SplitBorder } from "@tui/component/border"
import { useTheme } from "@tui/context/theme"
-import { BoxRenderable, ScrollBoxRenderable, addDefaultParsers } from "@opentui/core"
+import {
+ BoxRenderable,
+ ScrollBoxRenderable,
+ TextAttributes,
+ addDefaultParsers,
+ MacOSScrollAccel,
+ type ScrollAcceleration,
+} from "@opentui/core"
import { Prompt, type PromptRef } from "@tui/component/prompt"
import type { AssistantMessage, Part, ToolPart, UserMessage, TextPart, ReasoningPart } from "@opencode-ai/sdk"
import { useLocal } from "@tui/context/local"
@@ -61,6 +68,16 @@ import stripAnsi from "strip-ansi"
addDefaultParsers(parsers.parsers)
+class CustomSpeedScroll implements ScrollAcceleration {
+ constructor(private speed: number) {}
+
+ tick(_now?: number): number {
+ return this.speed
+ }
+
+ reset(): void {}
+}
+
const context = createContext<{
width: number
conceal: () => boolean
@@ -99,6 +116,17 @@ export function Session() {
const sidebarVisible = createMemo(() => sidebar() === "show" || (sidebar() === "auto" && wide()))
const contentWidth = createMemo(() => dimensions().width - (sidebarVisible() ? 42 : 0) - 4)
+ const scrollAcceleration = createMemo(() => {
+ const tui = sync.data.config.tui
+ if (tui?.scroll_acceleration?.enabled) {
+ return new MacOSScrollAccel()
+ }
+ if (tui?.scroll_speed) {
+ return new CustomSpeedScroll(tui.scroll_speed)
+ }
+ return undefined
+ })
+
createEffect(async () => {
await sync.session
.sync(route.sessionID)
@@ -695,6 +723,7 @@ export function Session() {
stickyScroll={true}
stickyStart="bottom"
flexGrow={1}
+ scrollAcceleration={scrollAcceleration()}
>
{(message, index) => (
@@ -1229,7 +1258,7 @@ ToolRegistry.register({
{(value) => {value}}
-
+
@@ -1439,16 +1468,16 @@ ToolRegistry.register({
-
+
-
+
-
+
diff --git a/packages/opencode/src/cli/cmd/tui/ui/toast.tsx b/packages/opencode/src/cli/cmd/tui/ui/toast.tsx
index 24d630c73..7e9684170 100644
--- a/packages/opencode/src/cli/cmd/tui/ui/toast.tsx
+++ b/packages/opencode/src/cli/cmd/tui/ui/toast.tsx
@@ -31,11 +31,11 @@ export function Toast() {
customBorderChars={SplitBorder.customBorderChars}
>
-
+
{current().title}
- {current().message}
+ {current().message}
)}
diff --git a/packages/opencode/src/cli/cmd/tui/worker.ts b/packages/opencode/src/cli/cmd/tui/worker.ts
index 32cd5562a..db1eb8ecb 100644
--- a/packages/opencode/src/cli/cmd/tui/worker.ts
+++ b/packages/opencode/src/cli/cmd/tui/worker.ts
@@ -43,6 +43,7 @@ export const rpc = {
}
},
async shutdown() {
+ Log.Default.info("worker shutting down")
await Instance.disposeAll()
await server.stop(true)
},
diff --git a/packages/opencode/src/config/config.ts b/packages/opencode/src/config/config.ts
index e6bed238f..3ca8c25de 100644
--- a/packages/opencode/src/config/config.ts
+++ b/packages/opencode/src/config/config.ts
@@ -437,7 +437,13 @@ export namespace Config {
})
export const TUI = z.object({
- scroll_speed: z.number().min(1).optional().default(2).describe("TUI scroll speed"),
+ scroll_speed: z.number().min(1).optional().default(1).describe("TUI scroll speed"),
+ scroll_acceleration: z
+ .object({
+ enabled: z.boolean().describe("Enable scroll acceleration"),
+ })
+ .optional()
+ .describe("Scroll acceleration settings"),
})
export const Layout = z.enum(["auto", "stretch"]).meta({
diff --git a/packages/opencode/src/format/index.ts b/packages/opencode/src/format/index.ts
index 0cc7bcdc7..9cb4545b0 100644
--- a/packages/opencode/src/format/index.ts
+++ b/packages/opencode/src/format/index.ts
@@ -41,6 +41,9 @@ export namespace Format {
extensions: [],
...item,
})
+
+ if (result.command.length === 0) continue
+
result.enabled = async () => true
result.name = name
formatters[name] = result
diff --git a/packages/opencode/src/project/instance.ts b/packages/opencode/src/project/instance.ts
index 39625e087..6e8ebb7a0 100644
--- a/packages/opencode/src/project/instance.ts
+++ b/packages/opencode/src/project/instance.ts
@@ -53,10 +53,14 @@ export const Instance = {
await State.dispose(Instance.directory)
},
async disposeAll() {
+ Log.Default.info("disposing all instances")
for (const [_key, value] of cache) {
- await context.provide(await value, async () => {
- await Instance.dispose()
- })
+ const awaited = await value.catch(() => {})
+ if (awaited) {
+ await context.provide(await value, async () => {
+ await Instance.dispose()
+ })
+ }
}
cache.clear()
},
diff --git a/packages/opencode/src/provider/transform.ts b/packages/opencode/src/provider/transform.ts
index e578d806f..668f30412 100644
--- a/packages/opencode/src/provider/transform.ts
+++ b/packages/opencode/src/provider/transform.ts
@@ -176,7 +176,7 @@ export namespace ProviderTransform {
}
export function maxOutputTokens(
- providerID: string,
+ npm: string,
options: Record,
modelLimit: number,
globalLimit: number,
@@ -184,7 +184,7 @@ export namespace ProviderTransform {
const modelCap = modelLimit || globalLimit
const standardLimit = Math.min(modelCap, globalLimit)
- if (providerID === "anthropic") {
+ if (npm === "@ai-sdk/anthropic") {
const thinking = options?.["thinking"]
const budgetTokens = typeof thinking?.["budgetTokens"] === "number" ? thinking["budgetTokens"] : 0
const enabled = thinking?.["type"] === "enabled"
diff --git a/packages/opencode/src/tool/read.ts b/packages/opencode/src/tool/read.ts
index 4d8e15bfb..fc33463a3 100644
--- a/packages/opencode/src/tool/read.ts
+++ b/packages/opencode/src/tool/read.ts
@@ -11,6 +11,7 @@ import { Provider } from "../provider/provider"
import { Identifier } from "../id/id"
import { Permission } from "../permission"
import { Agent } from "@/agent/agent"
+import { iife } from "@/util/iife"
const DEFAULT_READ_LIMIT = 2000
const MAX_LINE_LENGTH = 2000
@@ -48,6 +49,19 @@ export const ReadTool = Tool.define("read", {
}
}
+ const block = (() => {
+ const whitelist = [".env.example", ".env.sample"]
+
+ if (whitelist.some((w) => filepath.endsWith(w))) return false
+ if (filepath.includes(".env")) return true
+
+ return false
+ })()
+
+ if (block) {
+ throw new Error(`The user has blocked you from reading ${filepath}, DO NOT make further attempts to read it`)
+ }
+
const file = Bun.file(filepath)
if (!(await file.exists())) {
const dir = path.dirname(filepath)
diff --git a/packages/opencode/test/provider/transform.test.ts b/packages/opencode/test/provider/transform.test.ts
new file mode 100644
index 000000000..e6080d54c
--- /dev/null
+++ b/packages/opencode/test/provider/transform.test.ts
@@ -0,0 +1,98 @@
+import { describe, expect, test } from "bun:test"
+import { ProviderTransform } from "../../src/provider/transform"
+
+const OUTPUT_TOKEN_MAX = 32000
+
+describe("ProviderTransform.maxOutputTokens", () => {
+ test("returns 32k when modelLimit > 32k", () => {
+ const modelLimit = 100000
+ const result = ProviderTransform.maxOutputTokens("@ai-sdk/openai", {}, modelLimit, OUTPUT_TOKEN_MAX)
+ expect(result).toBe(OUTPUT_TOKEN_MAX)
+ })
+
+ test("returns modelLimit when modelLimit < 32k", () => {
+ const modelLimit = 16000
+ const result = ProviderTransform.maxOutputTokens("@ai-sdk/openai", {}, modelLimit, OUTPUT_TOKEN_MAX)
+ expect(result).toBe(16000)
+ })
+
+ describe("azure", () => {
+ test("returns 32k when modelLimit > 32k", () => {
+ const modelLimit = 100000
+ const result = ProviderTransform.maxOutputTokens("@ai-sdk/azure", {}, modelLimit, OUTPUT_TOKEN_MAX)
+ expect(result).toBe(OUTPUT_TOKEN_MAX)
+ })
+
+ test("returns modelLimit when modelLimit < 32k", () => {
+ const modelLimit = 16000
+ const result = ProviderTransform.maxOutputTokens("@ai-sdk/azure", {}, modelLimit, OUTPUT_TOKEN_MAX)
+ expect(result).toBe(16000)
+ })
+ })
+
+ describe("bedrock", () => {
+ test("returns 32k when modelLimit > 32k", () => {
+ const modelLimit = 100000
+ const result = ProviderTransform.maxOutputTokens("@ai-sdk/amazon-bedrock", {}, modelLimit, OUTPUT_TOKEN_MAX)
+ expect(result).toBe(OUTPUT_TOKEN_MAX)
+ })
+
+ test("returns modelLimit when modelLimit < 32k", () => {
+ const modelLimit = 16000
+ const result = ProviderTransform.maxOutputTokens("@ai-sdk/amazon-bedrock", {}, modelLimit, OUTPUT_TOKEN_MAX)
+ expect(result).toBe(16000)
+ })
+ })
+
+ describe("anthropic without thinking options", () => {
+ test("returns 32k when modelLimit > 32k", () => {
+ const modelLimit = 100000
+ const result = ProviderTransform.maxOutputTokens("@ai-sdk/anthropic", {}, modelLimit, OUTPUT_TOKEN_MAX)
+ expect(result).toBe(OUTPUT_TOKEN_MAX)
+ })
+
+ test("returns modelLimit when modelLimit < 32k", () => {
+ const modelLimit = 16000
+ const result = ProviderTransform.maxOutputTokens("@ai-sdk/anthropic", {}, modelLimit, OUTPUT_TOKEN_MAX)
+ expect(result).toBe(16000)
+ })
+ })
+
+ describe("anthropic with thinking options", () => {
+ test("returns 32k when budgetTokens + 32k <= modelLimit", () => {
+ const modelLimit = 100000
+ const options = {
+ thinking: {
+ type: "enabled",
+ budgetTokens: 10000,
+ },
+ }
+ const result = ProviderTransform.maxOutputTokens("@ai-sdk/anthropic", options, modelLimit, OUTPUT_TOKEN_MAX)
+ expect(result).toBe(OUTPUT_TOKEN_MAX)
+ })
+
+ test("returns modelLimit - budgetTokens when budgetTokens + 32k > modelLimit", () => {
+ const modelLimit = 50000
+ const options = {
+ thinking: {
+ type: "enabled",
+ budgetTokens: 30000,
+ },
+ }
+ const result = ProviderTransform.maxOutputTokens("@ai-sdk/anthropic", options, modelLimit, OUTPUT_TOKEN_MAX)
+ expect(result).toBe(20000)
+ })
+
+ test("returns 32k when thinking type is not enabled", () => {
+ const modelLimit = 100000
+ const options = {
+ thinking: {
+ type: "disabled",
+ budgetTokens: 10000,
+ },
+ }
+ const result = ProviderTransform.maxOutputTokens("@ai-sdk/anthropic", options, modelLimit, OUTPUT_TOKEN_MAX)
+ expect(result).toBe(OUTPUT_TOKEN_MAX)
+ })
+ })
+})
diff --git a/packages/plugin/package.json b/packages/plugin/package.json
index 3ecd6c95b..2f13f35f2 100644
--- a/packages/plugin/package.json
+++ b/packages/plugin/package.json
@@ -1,7 +1,7 @@
{
"$schema": "https://json.schemastore.org/package.json",
"name": "@opencode-ai/plugin",
- "version": "1.0.62",
+ "version": "1.0.65",
"type": "module",
"scripts": {
"typecheck": "tsgo --noEmit",
diff --git a/packages/sdk/js/package.json b/packages/sdk/js/package.json
index 4758dba1a..29d4a4c03 100644
--- a/packages/sdk/js/package.json
+++ b/packages/sdk/js/package.json
@@ -1,7 +1,7 @@
{
"$schema": "https://json.schemastore.org/package.json",
"name": "@opencode-ai/sdk",
- "version": "1.0.62",
+ "version": "1.0.65",
"type": "module",
"scripts": {
"typecheck": "tsgo --noEmit",
diff --git a/packages/sdk/js/src/client.ts b/packages/sdk/js/src/client.ts
index 29b9de906..ac9aa1519 100644
--- a/packages/sdk/js/src/client.ts
+++ b/packages/sdk/js/src/client.ts
@@ -6,6 +6,17 @@ import { type Config } from "./gen/client/types.gen.js"
import { OpencodeClient } from "./gen/sdk.gen.js"
export function createOpencodeClient(config?: Config) {
+ if (!config?.fetch) {
+ config = {
+ ...config,
+ fetch: (req) => {
+ // @ts-ignore
+ req.timeout = false
+ return fetch(req)
+ },
+ }
+ }
+
const client = createClient(config)
return new OpencodeClient({ client })
}
diff --git a/packages/sdk/js/src/gen/types.gen.ts b/packages/sdk/js/src/gen/types.gen.ts
index 941521928..bc33c7dc9 100644
--- a/packages/sdk/js/src/gen/types.gen.ts
+++ b/packages/sdk/js/src/gen/types.gen.ts
@@ -301,6 +301,15 @@ export type Config = {
* TUI scroll speed
*/
scroll_speed?: number
+ /**
+ * Scroll acceleration settings
+ */
+ scroll_acceleration?: {
+ /**
+ * Enable scroll acceleration
+ */
+ enabled: boolean
+ }
}
/**
* Command configuration, see https://opencode.ai/docs/commands
diff --git a/packages/slack/package.json b/packages/slack/package.json
index e240199a5..978893b4b 100644
--- a/packages/slack/package.json
+++ b/packages/slack/package.json
@@ -1,6 +1,6 @@
{
"name": "@opencode-ai/slack",
- "version": "1.0.62",
+ "version": "1.0.65",
"type": "module",
"scripts": {
"dev": "bun run src/index.ts",
diff --git a/packages/ui/package.json b/packages/ui/package.json
index 42f5a6afb..4b239012c 100644
--- a/packages/ui/package.json
+++ b/packages/ui/package.json
@@ -1,6 +1,6 @@
{
"name": "@opencode-ai/ui",
- "version": "1.0.62",
+ "version": "1.0.65",
"type": "module",
"exports": {
".": "./src/components/index.ts",
diff --git a/packages/ui/script/colors.txt b/packages/ui/script/colors.txt
index 5b5cfa73a..f84966853 100644
--- a/packages/ui/script/colors.txt
+++ b/packages/ui/script/colors.txt
@@ -44,9 +44,9 @@
--surface-info-base: var(--lilac-light-3);
--surface-info-weak: var(--lilac-light-2);
--surface-info-strong: var(--lilac-light-9);
+--surface-diff-unchanged-base: #FFFFFF00;
--surface-diff-skip-base: var(--smoke-light-2);
--surface-diff-hidden-base: var(--blue-light-3);
---surface-diff-unchanged-base: #FFFFFF00;
--surface-diff-hidden-weak: var(--blue-light-2);
--surface-diff-hidden-weaker: var(--blue-light-1);
--surface-diff-hidden-strong: var(--blue-light-5);
@@ -95,7 +95,7 @@
--text-on-brand-weaker: var(--smoke-light-alpha-8);
--text-on-brand-strong: var(--smoke-light-alpha-12);
--button-secondary-base: #FDFCFC;
---button-secondary-base-hover: #FAF9F9;
+--button-secondary-hover: #FAF9F9;
--border-base: var(--smoke-light-alpha-7);
--border-hover: var(--smoke-light-alpha-8);
--border-active: var(--smoke-light-alpha-9);
@@ -190,20 +190,25 @@
--icon-diff-add-active: var(--mint-light-12);
--icon-diff-delete-base: var(--ember-light-10);
--icon-diff-delete-hover: var(--ember-light-11);
---syntax-comment: #8A8A8A;
---syntax-string: #D68C27;
---syntax-keyword: #3B7DD8;
---syntax-function: #D1383D;
---syntax-number: #3D9A57;
---syntax-operator: #D68C27;
---syntax-variable: #B0851F;
---syntax-type: #318795;
---syntax-constant: #953170;
---syntax-punctuation: #1A1A1A;
---syntax-success: var(--apple-dark-10);
+--syntax-comment: var(--text-weaker);
+--syntax-regexp: var(--text-base);
+--syntax-string: #007663;
+--syntax-keyword: var(--text-weak);
+--syntax-primitive: #FB7F51;
+--syntax-operator: var(--text-weak);
+--syntax-variable: var(--text-strong);
+--syntax-property: #EC6CC8;
+--syntax-type: #738400;
+--syntax-constant: #00B2B9;
+--syntax-punctuation: var(--text-weaker);
+--syntax-object: var(--text-strong);
+--syntax-success: var(--apple-light-10);
--syntax-warning: var(--amber-light-10);
---syntax-critical: var(--ember-dark-9);
---syntax-info: var(--lilac-dark-11);
+--syntax-critical: var(--ember-light-9);
+--syntax-info: #0091A7;
+--syntax-diff-add: var(--mint-light-11);
+--syntax-diff-delete: var(--ember-light-11);
+--syntax-diff-unknown: #FF0000;
--markdown-heading: #D68C27;
--markdown-text: #1A1A1A;
--markdown-link: #3B7DD8;
diff --git a/packages/ui/script/tailwind.ts b/packages/ui/script/tailwind.ts
index 1c7064690..6e558a8fa 100644
--- a/packages/ui/script/tailwind.ts
+++ b/packages/ui/script/tailwind.ts
@@ -11,7 +11,7 @@ for (const line of colors.split("\n")) {
}
const output = `
-/* Generated by script/colors.ts */
+/* Generated by script/tailwind.ts */
/* Do not edit this file manually */
@theme {
diff --git a/packages/ui/src/components/accordion.css b/packages/ui/src/components/accordion.css
index f949dc948..8cfac06b0 100644
--- a/packages/ui/src/components/accordion.css
+++ b/packages/ui/src/components/accordion.css
@@ -24,7 +24,7 @@
[data-slot="accordion-trigger"] {
width: 100%;
display: flex;
- height: 40px;
+ height: 32px;
padding: 8px 12px;
justify-content: space-between;
align-items: center;
@@ -63,23 +63,24 @@
margin-bottom: 8px;
[data-slot="accordion-trigger"] {
- border-radius: 8px 8px 0 0;
+ border-top-left-radius: var(--radius-md);
+ border-top-right-radius: var(--radius-md);
}
[data-slot="accordion-content"] {
border: 1px solid var(--border-weak-base);
border-top: none;
- border-bottom-left-radius: 8px;
- border-bottom-right-radius: 8px;
+ border-bottom-left-radius: var(--radius-md);
+ border-bottom-right-radius: var(--radius-md);
}
[data-slot="accordion-item"]:has(+ &) {
&[data-closed] {
- border-bottom-left-radius: 8px;
- border-bottom-right-radius: 8px;
+ border-bottom-left-radius: var(--radius-md);
+ border-bottom-right-radius: var(--radius-md);
[data-slot="accordion-trigger"] {
- border-bottom-left-radius: 8px;
- border-bottom-right-radius: 8px;
+ border-bottom-left-radius: var(--radius-md);
+ border-bottom-right-radius: var(--radius-md);
}
}
margin-bottom: 8px;
@@ -89,8 +90,8 @@
margin-top: 8px;
[data-slot="accordion-trigger"] {
- border-top-left-radius: 8px;
- border-top-right-radius: 8px;
+ border-top-left-radius: var(--radius-md);
+ border-top-right-radius: var(--radius-md);
}
}
}
@@ -106,8 +107,8 @@
&[data-closed] {
[data-slot="accordion-trigger"] {
- border-top-left-radius: 8px;
- border-top-right-radius: 8px;
+ border-top-left-radius: var(--radius-md);
+ border-top-right-radius: var(--radius-md);
}
}
}
@@ -117,8 +118,8 @@
&[data-closed] {
[data-slot="accordion-trigger"] {
- border-bottom-left-radius: 8px;
- border-bottom-right-radius: 8px;
+ border-bottom-left-radius: var(--radius-md);
+ border-bottom-right-radius: var(--radius-md);
}
}
}
diff --git a/packages/ui/src/components/accordion.tsx b/packages/ui/src/components/accordion.tsx
index 02f00b7be..535d38e3d 100644
--- a/packages/ui/src/components/accordion.tsx
+++ b/packages/ui/src/components/accordion.tsx
@@ -1,11 +1,9 @@
import { Accordion as Kobalte } from "@kobalte/core/accordion"
-import { createSignal, splitProps } from "solid-js"
+import { splitProps } from "solid-js"
import type { ComponentProps, ParentProps } from "solid-js"
export interface AccordionProps extends ComponentProps {}
-export interface AccordionItemProps extends ComponentProps {
- defaultOpen?: boolean
-}
+export interface AccordionItemProps extends ComponentProps {}
export interface AccordionHeaderProps extends ComponentProps {}
export interface AccordionTriggerProps extends ComponentProps {}
export interface AccordionContentProps extends ComponentProps {}
@@ -25,14 +23,11 @@ function AccordionRoot(props: AccordionProps) {
}
function AccordionItem(props: AccordionItemProps) {
- const [split, rest] = splitProps(props, ["class", "classList", "defaultOpen"])
- const [open, setOpen] = createSignal(split.defaultOpen ?? false)
+ const [split, rest] = splitProps(props, ["class", "classList"])
return (
`,
"align-right": ``,
expand: ``,
+ collapse: ``,
}
export interface IconProps extends ComponentProps<"svg"> {
diff --git a/packages/ui/src/components/list.css b/packages/ui/src/components/list.css
index 67db2c619..2136c4f92 100644
--- a/packages/ui/src/components/list.css
+++ b/packages/ui/src/components/list.css
@@ -14,7 +14,7 @@
padding: 4px 12px;
text-align: left;
- border-radius: 6px;
+ border-radius: var(--radius-md);
transition: background-color 0.2s ease-in-out;
&[data-active="true"] {
diff --git a/packages/ui/src/components/select-dialog.css b/packages/ui/src/components/select-dialog.css
index 41d8f3921..6ca8fa0f3 100644
--- a/packages/ui/src/components/select-dialog.css
+++ b/packages/ui/src/components/select-dialog.css
@@ -7,7 +7,7 @@
gap: 12px;
align-self: stretch;
- border-radius: 8px;
+ border-radius: var(--radius-md);
background: var(--surface-base);
[data-slot="input-container"] {
@@ -100,7 +100,7 @@
align-items: center;
&[data-active="true"] {
- border-radius: 8px;
+ border-radius: var(--radius-md);
background: var(--surface-raised-base-hover);
}
}
diff --git a/packages/ui/src/components/select.css b/packages/ui/src/components/select.css
index 79445420a..7bafa39f7 100644
--- a/packages/ui/src/components/select.css
+++ b/packages/ui/src/components/select.css
@@ -21,7 +21,7 @@
[data-component="select-content"] {
min-width: 4rem;
overflow: hidden;
- border-radius: 8px;
+ border-radius: var(--radius-md);
border-width: 1px;
border-style: solid;
border-color: var(--border-weak-base);
@@ -60,7 +60,7 @@
display: flex;
align-items: center;
padding: 0 6px 0 6px;
- border-radius: 6px;
+ border-radius: var(--radius-sm);
/* text-12-medium */
font-family: var(--font-family-sans);
diff --git a/packages/ui/src/components/tabs.css b/packages/ui/src/components/tabs.css
index 67f289283..91efbe50d 100644
--- a/packages/ui/src/components/tabs.css
+++ b/packages/ui/src/components/tabs.css
@@ -6,7 +6,7 @@
background-color: var(--background-stronger);
overflow: clip;
- [data-slot="list"] {
+ [data-slot="tabs-list"] {
height: 48px;
width: 100%;
position: relative;
@@ -36,12 +36,12 @@
}
}
- [data-slot="trigger"] {
+ [data-slot="tabs-trigger-wrapper"] {
position: relative;
height: 100%;
- padding: 14px 24px;
display: flex;
align-items: center;
+ gap: 12px;
color: var(--text-base);
/* text-14-medium */
@@ -58,6 +58,23 @@
border-right: 1px solid var(--border-weak-base);
background-color: var(--background-base);
+ [data-slot="tabs-trigger"] {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ padding: 14px 24px;
+ }
+
+ [data-slot="tabs-trigger-close-button"] {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ }
+
+ [data-component="icon-button"] {
+ margin: -0.25rem;
+ }
+
&:disabled {
pointer-events: none;
color: var(--text-weaker);
@@ -66,17 +83,38 @@
outline: none;
box-shadow: 0 0 0 2px var(--border-focus);
}
- &[data-selected] {
+ &:has([data-hidden]) {
+ [data-slot="tabs-trigger-close-button"] {
+ opacity: 0;
+ }
+
+ &:hover {
+ [data-slot="tabs-trigger-close-button"] {
+ opacity: 1;
+ }
+ }
+ }
+ &:has([data-selected]) {
color: var(--text-strong);
background-color: transparent;
border-bottom-color: transparent;
+ [data-slot="tabs-trigger-close-button"] {
+ opacity: 1;
+ }
}
&:hover:not(:disabled):not([data-selected]) {
color: var(--text-strong);
}
+ &:has([data-slot="tabs-trigger-close-button"]) {
+ padding-right: 12px;
+
+ [data-slot="tabs-trigger"] {
+ padding-right: 0;
+ }
+ }
}
- [data-slot="content"] {
+ [data-slot="tabs-content"] {
overflow-y: auto;
flex: 1;
diff --git a/packages/ui/src/components/tabs.tsx b/packages/ui/src/components/tabs.tsx
index 5e047a7ca..e2f72f158 100644
--- a/packages/ui/src/components/tabs.tsx
+++ b/packages/ui/src/components/tabs.tsx
@@ -1,10 +1,13 @@
import { Tabs as Kobalte } from "@kobalte/core/tabs"
-import { splitProps } from "solid-js"
+import { Show, splitProps, type JSX } from "solid-js"
import type { ComponentProps, ParentProps } from "solid-js"
export interface TabsProps extends ComponentProps {}
export interface TabsListProps extends ComponentProps {}
-export interface TabsTriggerProps extends ComponentProps {}
+export interface TabsTriggerProps extends ComponentProps {
+ hideCloseButton?: boolean
+ closeButton?: JSX.Element
+}
export interface TabsContentProps extends ComponentProps {}
function TabsRoot(props: TabsProps) {
@@ -26,7 +29,7 @@ function TabsList(props: TabsListProps) {
return (
) {
- const [split, rest] = splitProps(props, ["class", "classList", "children"])
+ const [split, rest] = splitProps(props, ["class", "classList", "children", "closeButton", "hideCloseButton"])
return (
-
- {split.children}
-
+
+ {split.children}
+
+
+ {(closeButton) => (
+
+ {closeButton()}
+
+ )}
+
+
)
}
@@ -56,7 +67,7 @@ function TabsContent(props: ParentProps