Merge branch 'dev' into changelog-updates
commit
a96f3010a6
30
bun.lock
30
bun.lock
|
|
@ -22,7 +22,7 @@
|
|||
},
|
||||
"packages/app": {
|
||||
"name": "@opencode-ai/app",
|
||||
"version": "1.0.206",
|
||||
"version": "1.0.207",
|
||||
"dependencies": {
|
||||
"@kobalte/core": "catalog:",
|
||||
"@opencode-ai/sdk": "workspace:*",
|
||||
|
|
@ -70,7 +70,7 @@
|
|||
},
|
||||
"packages/console/app": {
|
||||
"name": "@opencode-ai/console-app",
|
||||
"version": "1.0.206",
|
||||
"version": "1.0.207",
|
||||
"dependencies": {
|
||||
"@cloudflare/vite-plugin": "1.15.2",
|
||||
"@ibm/plex": "6.4.1",
|
||||
|
|
@ -98,7 +98,7 @@
|
|||
},
|
||||
"packages/console/core": {
|
||||
"name": "@opencode-ai/console-core",
|
||||
"version": "1.0.206",
|
||||
"version": "1.0.207",
|
||||
"dependencies": {
|
||||
"@aws-sdk/client-sts": "3.782.0",
|
||||
"@jsx-email/render": "1.1.1",
|
||||
|
|
@ -125,7 +125,7 @@
|
|||
},
|
||||
"packages/console/function": {
|
||||
"name": "@opencode-ai/console-function",
|
||||
"version": "1.0.206",
|
||||
"version": "1.0.207",
|
||||
"dependencies": {
|
||||
"@ai-sdk/anthropic": "2.0.0",
|
||||
"@ai-sdk/openai": "2.0.2",
|
||||
|
|
@ -149,7 +149,7 @@
|
|||
},
|
||||
"packages/console/mail": {
|
||||
"name": "@opencode-ai/console-mail",
|
||||
"version": "1.0.206",
|
||||
"version": "1.0.207",
|
||||
"dependencies": {
|
||||
"@jsx-email/all": "2.2.3",
|
||||
"@jsx-email/cli": "1.4.3",
|
||||
|
|
@ -173,7 +173,7 @@
|
|||
},
|
||||
"packages/desktop": {
|
||||
"name": "@opencode-ai/desktop",
|
||||
"version": "1.0.206",
|
||||
"version": "1.0.207",
|
||||
"dependencies": {
|
||||
"@opencode-ai/app": "workspace:*",
|
||||
"@solid-primitives/storage": "catalog:",
|
||||
|
|
@ -200,7 +200,7 @@
|
|||
},
|
||||
"packages/enterprise": {
|
||||
"name": "@opencode-ai/enterprise",
|
||||
"version": "1.0.206",
|
||||
"version": "1.0.207",
|
||||
"dependencies": {
|
||||
"@opencode-ai/ui": "workspace:*",
|
||||
"@opencode-ai/util": "workspace:*",
|
||||
|
|
@ -229,7 +229,7 @@
|
|||
},
|
||||
"packages/function": {
|
||||
"name": "@opencode-ai/function",
|
||||
"version": "1.0.206",
|
||||
"version": "1.0.207",
|
||||
"dependencies": {
|
||||
"@octokit/auth-app": "8.0.1",
|
||||
"@octokit/rest": "catalog:",
|
||||
|
|
@ -245,7 +245,7 @@
|
|||
},
|
||||
"packages/opencode": {
|
||||
"name": "opencode",
|
||||
"version": "1.0.206",
|
||||
"version": "1.0.207",
|
||||
"bin": {
|
||||
"opencode": "./bin/opencode",
|
||||
},
|
||||
|
|
@ -347,7 +347,7 @@
|
|||
},
|
||||
"packages/plugin": {
|
||||
"name": "@opencode-ai/plugin",
|
||||
"version": "1.0.206",
|
||||
"version": "1.0.207",
|
||||
"dependencies": {
|
||||
"@opencode-ai/sdk": "workspace:*",
|
||||
"zod": "catalog:",
|
||||
|
|
@ -367,7 +367,7 @@
|
|||
},
|
||||
"packages/sdk/js": {
|
||||
"name": "@opencode-ai/sdk",
|
||||
"version": "1.0.206",
|
||||
"version": "1.0.207",
|
||||
"devDependencies": {
|
||||
"@hey-api/openapi-ts": "0.88.1",
|
||||
"@tsconfig/node22": "catalog:",
|
||||
|
|
@ -378,7 +378,7 @@
|
|||
},
|
||||
"packages/slack": {
|
||||
"name": "@opencode-ai/slack",
|
||||
"version": "1.0.206",
|
||||
"version": "1.0.207",
|
||||
"dependencies": {
|
||||
"@opencode-ai/sdk": "workspace:*",
|
||||
"@slack/bolt": "^3.17.1",
|
||||
|
|
@ -391,7 +391,7 @@
|
|||
},
|
||||
"packages/ui": {
|
||||
"name": "@opencode-ai/ui",
|
||||
"version": "1.0.206",
|
||||
"version": "1.0.207",
|
||||
"dependencies": {
|
||||
"@kobalte/core": "catalog:",
|
||||
"@opencode-ai/sdk": "workspace:*",
|
||||
|
|
@ -426,7 +426,7 @@
|
|||
},
|
||||
"packages/util": {
|
||||
"name": "@opencode-ai/util",
|
||||
"version": "1.0.206",
|
||||
"version": "1.0.207",
|
||||
"dependencies": {
|
||||
"zod": "catalog:",
|
||||
},
|
||||
|
|
@ -437,7 +437,7 @@
|
|||
},
|
||||
"packages/web": {
|
||||
"name": "@opencode-ai/web",
|
||||
"version": "1.0.206",
|
||||
"version": "1.0.207",
|
||||
"dependencies": {
|
||||
"@astrojs/cloudflare": "12.6.3",
|
||||
"@astrojs/markdown-remark": "6.3.1",
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "@opencode-ai/app",
|
||||
"version": "1.0.206",
|
||||
"version": "1.0.207",
|
||||
"description": "",
|
||||
"type": "module",
|
||||
"exports": {
|
||||
|
|
|
|||
|
|
@ -3,7 +3,6 @@ import { createSimpleContext } from "@opencode-ai/ui/context"
|
|||
import { useDialog } from "@opencode-ai/ui/context/dialog"
|
||||
import { Dialog } from "@opencode-ai/ui/dialog"
|
||||
import { List } from "@opencode-ai/ui/list"
|
||||
import { useTheme } from "@opencode-ai/ui/theme"
|
||||
|
||||
const IS_MAC = typeof navigator === "object" && /(Mac|iPod|iPhone|iPad)/.test(navigator.platform)
|
||||
|
||||
|
|
@ -27,6 +26,7 @@ export interface CommandOption {
|
|||
suggested?: boolean
|
||||
disabled?: boolean
|
||||
onSelect?: (source?: "palette" | "keybind" | "slash") => void
|
||||
onHighlight?: () => (() => void) | void
|
||||
}
|
||||
|
||||
export function parseKeybind(config: string): Keybind[] {
|
||||
|
|
@ -116,24 +116,18 @@ export function formatKeybind(config: string): string {
|
|||
|
||||
function DialogCommand(props: { options: CommandOption[] }) {
|
||||
const dialog = useDialog()
|
||||
const theme = useTheme()
|
||||
let cleanup: (() => void) | void
|
||||
let committed = false
|
||||
|
||||
const handleMove = (option: CommandOption | undefined) => {
|
||||
if (!option) return
|
||||
if (option.id.startsWith("theme.set.")) {
|
||||
const id = option.id.replace("theme.set.", "")
|
||||
theme.previewTheme(id)
|
||||
} else if (option.id.startsWith("theme.scheme.") && !option.id.includes("cycle")) {
|
||||
const scheme = option.id.replace("theme.scheme.", "") as "light" | "dark" | "system"
|
||||
theme.previewColorScheme(scheme)
|
||||
}
|
||||
cleanup?.()
|
||||
cleanup = option?.onHighlight?.()
|
||||
}
|
||||
|
||||
const handleSelect = (option: CommandOption | undefined) => {
|
||||
if (option) {
|
||||
theme.commitPreview()
|
||||
committed = true
|
||||
cleanup = undefined
|
||||
dialog.close()
|
||||
option.onSelect?.("palette")
|
||||
}
|
||||
|
|
@ -141,7 +135,7 @@ function DialogCommand(props: { options: CommandOption[] }) {
|
|||
|
||||
onCleanup(() => {
|
||||
if (!committed) {
|
||||
theme.cancelPreview()
|
||||
cleanup?.()
|
||||
}
|
||||
})
|
||||
|
||||
|
|
|
|||
|
|
@ -49,7 +49,7 @@ import { Header } from "@/components/header"
|
|||
import { useDialog } from "@opencode-ai/ui/context/dialog"
|
||||
import { useTheme, type ColorScheme } from "@opencode-ai/ui/theme"
|
||||
import { DialogSelectProvider } from "@/components/dialog-select-provider"
|
||||
import { useCommand } from "@/context/command"
|
||||
import { useCommand, type CommandOption } from "@/context/command"
|
||||
import { ConstrainDragXAxis } from "@/utils/solid-dnd"
|
||||
|
||||
export default function Layout(props: ParentProps) {
|
||||
|
|
@ -323,7 +323,7 @@ export default function Layout(props: ParentProps) {
|
|||
}
|
||||
|
||||
command.register(() => {
|
||||
const commands = [
|
||||
const commands: CommandOption[] = [
|
||||
{
|
||||
id: "sidebar.toggle",
|
||||
title: "Toggle sidebar",
|
||||
|
|
@ -387,7 +387,11 @@ export default function Layout(props: ParentProps) {
|
|||
id: `theme.set.${id}`,
|
||||
title: `Use theme: ${definition.name ?? id}`,
|
||||
category: "Theme",
|
||||
onSelect: () => theme.setTheme(id),
|
||||
onSelect: () => theme.commitPreview(),
|
||||
onHighlight: () => {
|
||||
theme.previewTheme(id)
|
||||
return () => theme.cancelPreview()
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
|
|
@ -404,7 +408,11 @@ export default function Layout(props: ParentProps) {
|
|||
id: `theme.scheme.${scheme}`,
|
||||
title: `Use color scheme: ${colorSchemeLabel[scheme]}`,
|
||||
category: "Theme",
|
||||
onSelect: () => theme.setColorScheme(scheme),
|
||||
onSelect: () => theme.commitPreview(),
|
||||
onHighlight: () => {
|
||||
theme.previewColorScheme(scheme)
|
||||
return () => theme.cancelPreview()
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "@opencode-ai/console-app",
|
||||
"version": "1.0.206",
|
||||
"version": "1.0.207",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"typecheck": "tsgo --noEmit",
|
||||
|
|
|
|||
|
|
@ -0,0 +1,365 @@
|
|||
import { Title } from "@solidjs/meta"
|
||||
import { createAsync, query, useParams } from "@solidjs/router"
|
||||
import { createSignal, For, Show } from "solid-js"
|
||||
import { Database, desc, eq } from "@opencode-ai/console-core/drizzle/index.js"
|
||||
import { BenchmarkTable } from "@opencode-ai/console-core/schema/benchmark.sql.js"
|
||||
|
||||
interface TaskSource {
|
||||
repo: string
|
||||
from: string
|
||||
to: string
|
||||
}
|
||||
|
||||
interface Judge {
|
||||
score: number
|
||||
rationale: string
|
||||
judge: string
|
||||
}
|
||||
|
||||
interface ScoreDetail {
|
||||
criterion: string
|
||||
weight: number
|
||||
average: number
|
||||
variance?: number
|
||||
judges?: Judge[]
|
||||
}
|
||||
|
||||
interface RunUsage {
|
||||
input: number
|
||||
output: number
|
||||
cost: number
|
||||
}
|
||||
|
||||
interface Run {
|
||||
task: string
|
||||
model: string
|
||||
agent: string
|
||||
score: {
|
||||
final: number
|
||||
base: number
|
||||
penalty: number
|
||||
}
|
||||
scoreDetails: ScoreDetail[]
|
||||
usage?: RunUsage
|
||||
duration?: number
|
||||
}
|
||||
|
||||
interface Prompt {
|
||||
commit: string
|
||||
prompt: string
|
||||
}
|
||||
|
||||
interface AverageUsage {
|
||||
input: number
|
||||
output: number
|
||||
cost: number
|
||||
}
|
||||
|
||||
interface Task {
|
||||
averageScore: number
|
||||
averageDuration?: number
|
||||
averageUsage?: AverageUsage
|
||||
model?: string
|
||||
agent?: string
|
||||
summary?: string
|
||||
runs?: Run[]
|
||||
task: {
|
||||
id: string
|
||||
source: TaskSource
|
||||
prompts?: Prompt[]
|
||||
}
|
||||
}
|
||||
|
||||
interface BenchmarkResult {
|
||||
averageScore: number
|
||||
tasks: Task[]
|
||||
}
|
||||
|
||||
async function getTaskDetail(benchmarkId: string, taskId: string) {
|
||||
"use server"
|
||||
const rows = await Database.use((tx) =>
|
||||
tx.select().from(BenchmarkTable).where(eq(BenchmarkTable.id, benchmarkId)).limit(1),
|
||||
)
|
||||
if (!rows[0]) return null
|
||||
const parsed = JSON.parse(rows[0].result) as BenchmarkResult
|
||||
const task = parsed.tasks.find((t) => t.task.id === taskId)
|
||||
return task ?? null
|
||||
}
|
||||
|
||||
const queryTaskDetail = query(getTaskDetail, "benchmark.task.detail")
|
||||
|
||||
function formatDuration(ms: number): string {
|
||||
const seconds = Math.floor(ms / 1000)
|
||||
const minutes = Math.floor(seconds / 60)
|
||||
const remainingSeconds = seconds % 60
|
||||
if (minutes > 0) {
|
||||
return `${minutes}m ${remainingSeconds}s`
|
||||
}
|
||||
return `${remainingSeconds}s`
|
||||
}
|
||||
|
||||
export default function BenchDetail() {
|
||||
const params = useParams()
|
||||
const [benchmarkId, taskId] = (params.id ?? "").split(":")
|
||||
const task = createAsync(() => queryTaskDetail(benchmarkId, taskId))
|
||||
|
||||
return (
|
||||
<main data-page="bench-detail">
|
||||
<Title>Benchmark - {taskId}</Title>
|
||||
<div style={{ padding: "1rem" }}>
|
||||
<Show when={task()} fallback={<p>Task not found</p>}>
|
||||
<div style={{ "margin-bottom": "1rem" }}>
|
||||
<div>
|
||||
<strong>Agent: </strong>
|
||||
{task()?.agent ?? "N/A"}
|
||||
</div>
|
||||
<div>
|
||||
<strong>Model: </strong>
|
||||
{task()?.model ?? "N/A"}
|
||||
</div>
|
||||
<div>
|
||||
<strong>Task: </strong>
|
||||
{task()!.task.id}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style={{ "margin-bottom": "1rem" }}>
|
||||
<div>
|
||||
<strong>Repo: </strong>
|
||||
<a
|
||||
href={`https://github.com/${task()!.task.source.repo}`}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
style={{ color: "#0066cc" }}
|
||||
>
|
||||
{task()!.task.source.repo}
|
||||
</a>
|
||||
</div>
|
||||
<div>
|
||||
<strong>From: </strong>
|
||||
<a
|
||||
href={`https://github.com/${task()!.task.source.repo}/commit/${task()!.task.source.from}`}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
style={{ color: "#0066cc" }}
|
||||
>
|
||||
{task()!.task.source.from.slice(0, 7)}
|
||||
</a>
|
||||
</div>
|
||||
<div>
|
||||
<strong>To: </strong>
|
||||
<a
|
||||
href={`https://github.com/${task()!.task.source.repo}/commit/${task()!.task.source.to}`}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
style={{ color: "#0066cc" }}
|
||||
>
|
||||
{task()!.task.source.to.slice(0, 7)}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Show when={task()?.task.prompts && task()!.task.prompts!.length > 0}>
|
||||
<div style={{ "margin-bottom": "1rem" }}>
|
||||
<strong>Prompt:</strong>
|
||||
<For each={task()!.task.prompts}>
|
||||
{(p) => (
|
||||
<div style={{ "margin-top": "0.5rem" }}>
|
||||
<div style={{ "font-size": "0.875rem", color: "#666" }}>Commit: {p.commit.slice(0, 7)}</div>
|
||||
<p style={{ "margin-top": "0.25rem", "white-space": "pre-wrap" }}>{p.prompt}</p>
|
||||
</div>
|
||||
)}
|
||||
</For>
|
||||
</div>
|
||||
</Show>
|
||||
|
||||
<hr style={{ margin: "1rem 0", border: "none", "border-top": "1px solid #ccc" }} />
|
||||
|
||||
<div style={{ "margin-bottom": "1rem" }}>
|
||||
<div>
|
||||
<strong>Average Duration: </strong>
|
||||
{task()?.averageDuration ? formatDuration(task()!.averageDuration!) : "N/A"}
|
||||
</div>
|
||||
<div>
|
||||
<strong>Average Score: </strong>
|
||||
{task()?.averageScore?.toFixed(3) ?? "N/A"}
|
||||
</div>
|
||||
<div>
|
||||
<strong>Average Cost: </strong>
|
||||
{task()?.averageUsage?.cost ? `$${task()!.averageUsage!.cost.toFixed(4)}` : "N/A"}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Show when={task()?.summary}>
|
||||
<div style={{ "margin-bottom": "1rem" }}>
|
||||
<strong>Summary:</strong>
|
||||
<p style={{ "margin-top": "0.5rem", "white-space": "pre-wrap" }}>{task()!.summary}</p>
|
||||
</div>
|
||||
</Show>
|
||||
|
||||
<Show when={task()?.runs && task()!.runs!.length > 0}>
|
||||
<div style={{ "margin-bottom": "1rem" }}>
|
||||
<strong>Runs:</strong>
|
||||
<table style={{ "margin-top": "0.5rem", "border-collapse": "collapse", width: "100%" }}>
|
||||
<thead>
|
||||
<tr>
|
||||
<th style={{ border: "1px solid #ccc", padding: "0.5rem", "text-align": "left" }}>Run</th>
|
||||
<th
|
||||
style={{
|
||||
border: "1px solid #ccc",
|
||||
padding: "0.5rem",
|
||||
"text-align": "left",
|
||||
"white-space": "nowrap",
|
||||
}}
|
||||
>
|
||||
Score (Base - Penalty)
|
||||
</th>
|
||||
<th style={{ border: "1px solid #ccc", padding: "0.5rem", "text-align": "left" }}>Cost</th>
|
||||
<th style={{ border: "1px solid #ccc", padding: "0.5rem", "text-align": "left" }}>Duration</th>
|
||||
<For each={task()!.runs![0]?.scoreDetails}>
|
||||
{(detail) => (
|
||||
<th style={{ border: "1px solid #ccc", padding: "0.5rem", "text-align": "left" }}>
|
||||
{detail.criterion} ({detail.weight})
|
||||
</th>
|
||||
)}
|
||||
</For>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<For each={task()!.runs}>
|
||||
{(run, index) => (
|
||||
<tr>
|
||||
<td style={{ border: "1px solid #ccc", padding: "0.5rem" }}>{index() + 1}</td>
|
||||
<td style={{ border: "1px solid #ccc", padding: "0.5rem", "white-space": "nowrap" }}>
|
||||
{run.score.final.toFixed(3)} ({run.score.base.toFixed(3)} - {run.score.penalty.toFixed(3)})
|
||||
</td>
|
||||
<td style={{ border: "1px solid #ccc", padding: "0.5rem" }}>
|
||||
{run.usage?.cost ? `$${run.usage.cost.toFixed(4)}` : "N/A"}
|
||||
</td>
|
||||
<td style={{ border: "1px solid #ccc", padding: "0.5rem" }}>
|
||||
{run.duration ? formatDuration(run.duration) : "N/A"}
|
||||
</td>
|
||||
<For each={run.scoreDetails}>
|
||||
{(detail) => (
|
||||
<td style={{ border: "1px solid #ccc", padding: "0.5rem" }}>
|
||||
<For each={detail.judges}>
|
||||
{(judge) => (
|
||||
<span
|
||||
style={{
|
||||
color: judge.score === 1 ? "green" : judge.score === 0 ? "red" : "inherit",
|
||||
"margin-right": "0.25rem",
|
||||
}}
|
||||
>
|
||||
{judge.score === 1 ? "✓" : judge.score === 0 ? "✗" : judge.score}
|
||||
</span>
|
||||
)}
|
||||
</For>
|
||||
</td>
|
||||
)}
|
||||
</For>
|
||||
</tr>
|
||||
)}
|
||||
</For>
|
||||
</tbody>
|
||||
</table>
|
||||
<For each={task()!.runs}>
|
||||
{(run, index) => (
|
||||
<div style={{ "margin-top": "1rem" }}>
|
||||
<h3 style={{ margin: "0 0 0.5rem 0" }}>Run {index() + 1}</h3>
|
||||
<div>
|
||||
<strong>Score: </strong>
|
||||
{run.score.final.toFixed(3)} (Base: {run.score.base.toFixed(3)} - Penalty:{" "}
|
||||
{run.score.penalty.toFixed(3)})
|
||||
</div>
|
||||
<For each={run.scoreDetails}>
|
||||
{(detail) => (
|
||||
<div style={{ "margin-top": "1rem", "padding-left": "1rem", "border-left": "2px solid #ccc" }}>
|
||||
<div>
|
||||
{detail.criterion} (weight: {detail.weight}){" "}
|
||||
<For each={detail.judges}>
|
||||
{(judge) => (
|
||||
<span
|
||||
style={{
|
||||
color: judge.score === 1 ? "green" : judge.score === 0 ? "red" : "inherit",
|
||||
"margin-right": "0.25rem",
|
||||
}}
|
||||
>
|
||||
{judge.score === 1 ? "✓" : judge.score === 0 ? "✗" : judge.score}
|
||||
</span>
|
||||
)}
|
||||
</For>
|
||||
</div>
|
||||
<Show when={detail.judges && detail.judges.length > 0}>
|
||||
<For each={detail.judges}>
|
||||
{(judge) => {
|
||||
const [expanded, setExpanded] = createSignal(false)
|
||||
return (
|
||||
<div style={{ "margin-top": "0.5rem", "padding-left": "1rem" }}>
|
||||
<div
|
||||
style={{ "font-size": "0.875rem", cursor: "pointer" }}
|
||||
onClick={() => setExpanded(!expanded())}
|
||||
>
|
||||
<span style={{ "margin-right": "0.5rem" }}>{expanded() ? "▼" : "▶"}</span>
|
||||
<span
|
||||
style={{
|
||||
color: judge.score === 1 ? "green" : judge.score === 0 ? "red" : "inherit",
|
||||
}}
|
||||
>
|
||||
{judge.score === 1 ? "✓" : judge.score === 0 ? "✗" : judge.score}
|
||||
</span>{" "}
|
||||
{judge.judge}
|
||||
</div>
|
||||
<Show when={expanded()}>
|
||||
<p
|
||||
style={{
|
||||
margin: "0.25rem 0 0 0",
|
||||
"white-space": "pre-wrap",
|
||||
"font-size": "0.875rem",
|
||||
}}
|
||||
>
|
||||
{judge.rationale}
|
||||
</p>
|
||||
</Show>
|
||||
</div>
|
||||
)
|
||||
}}
|
||||
</For>
|
||||
</Show>
|
||||
</div>
|
||||
)}
|
||||
</For>
|
||||
</div>
|
||||
)}
|
||||
</For>
|
||||
</div>
|
||||
</Show>
|
||||
|
||||
{(() => {
|
||||
const [jsonExpanded, setJsonExpanded] = createSignal(false)
|
||||
return (
|
||||
<div style={{ "margin-top": "1rem" }}>
|
||||
<button
|
||||
style={{
|
||||
cursor: "pointer",
|
||||
padding: "0.75rem 1.5rem",
|
||||
"font-size": "1rem",
|
||||
background: "#f0f0f0",
|
||||
border: "1px solid #ccc",
|
||||
"border-radius": "4px",
|
||||
}}
|
||||
onClick={() => setJsonExpanded(!jsonExpanded())}
|
||||
>
|
||||
<span style={{ "margin-right": "0.5rem" }}>{jsonExpanded() ? "▼" : "▶"}</span>
|
||||
Raw JSON
|
||||
</button>
|
||||
<Show when={jsonExpanded()}>
|
||||
<pre>{JSON.stringify(task(), null, 2)}</pre>
|
||||
</Show>
|
||||
</div>
|
||||
)
|
||||
})()}
|
||||
</Show>
|
||||
</div>
|
||||
</main>
|
||||
)
|
||||
}
|
||||
|
|
@ -1,52 +1,12 @@
|
|||
import { Title } from "@solidjs/meta"
|
||||
import { createAsync, query } from "@solidjs/router"
|
||||
import { createMemo, createSignal, For, Show } from "solid-js"
|
||||
import { A, createAsync, query } from "@solidjs/router"
|
||||
import { createMemo, For, Show } from "solid-js"
|
||||
import { Database, desc } from "@opencode-ai/console-core/drizzle/index.js"
|
||||
import { BenchmarkTable } from "@opencode-ai/console-core/schema/benchmark.sql.js"
|
||||
|
||||
interface TaskSource {
|
||||
repo: string
|
||||
from: string
|
||||
to: string
|
||||
}
|
||||
|
||||
interface ScoreDetail {
|
||||
criterion: string
|
||||
weight: number
|
||||
average: number
|
||||
}
|
||||
|
||||
interface Run {
|
||||
task: string
|
||||
model: string
|
||||
agent: string
|
||||
score: {
|
||||
final: number
|
||||
base: number
|
||||
penalty: number
|
||||
}
|
||||
scoreDetails: ScoreDetail[]
|
||||
}
|
||||
|
||||
interface Prompt {
|
||||
commit: string
|
||||
prompt: string
|
||||
}
|
||||
|
||||
interface Task {
|
||||
averageScore: number
|
||||
summary?: string
|
||||
runs?: Run[]
|
||||
task: {
|
||||
id: string
|
||||
source: TaskSource
|
||||
prompts?: Prompt[]
|
||||
}
|
||||
}
|
||||
|
||||
interface BenchmarkResult {
|
||||
averageScore: number
|
||||
tasks: Task[]
|
||||
tasks: { averageScore: number; task: { id: string } }[]
|
||||
}
|
||||
|
||||
async function getBenchmarks() {
|
||||
|
|
@ -57,17 +17,15 @@ async function getBenchmarks() {
|
|||
return rows.map((row) => {
|
||||
const parsed = JSON.parse(row.result) as BenchmarkResult
|
||||
const taskScores: Record<string, number> = {}
|
||||
const taskData: Record<string, Task> = {}
|
||||
for (const t of parsed.tasks) {
|
||||
taskScores[t.task.id] = t.averageScore
|
||||
taskData[t.task.id] = t
|
||||
}
|
||||
return {
|
||||
id: row.id,
|
||||
agent: row.agent,
|
||||
model: row.model,
|
||||
averageScore: parsed.averageScore,
|
||||
taskScores,
|
||||
taskData,
|
||||
}
|
||||
})
|
||||
}
|
||||
|
|
@ -76,7 +34,6 @@ const queryBenchmarks = query(getBenchmarks, "benchmarks.list")
|
|||
|
||||
export default function Bench() {
|
||||
const benchmarks = createAsync(() => queryBenchmarks())
|
||||
const [modalTask, setModalTask] = createSignal<Task | null>(null)
|
||||
|
||||
const taskIds = createMemo(() => {
|
||||
const ids = new Set<string>()
|
||||
|
|
@ -89,34 +46,32 @@ export default function Bench() {
|
|||
})
|
||||
|
||||
return (
|
||||
<main data-page="bench">
|
||||
<main data-page="bench" style={{ padding: "2rem" }}>
|
||||
<Title>Benchmark</Title>
|
||||
<table>
|
||||
<h1 style={{ "margin-bottom": "1.5rem" }}>Benchmarks</h1>
|
||||
<table style={{ "border-collapse": "collapse", width: "100%" }}>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Agent</th>
|
||||
<th>Model</th>
|
||||
<th>Final Score</th>
|
||||
<For each={taskIds()}>{(id) => <th>{id}</th>}</For>
|
||||
<th style={{ "text-align": "left", padding: "0.75rem" }}>Agent</th>
|
||||
<th style={{ "text-align": "left", padding: "0.75rem" }}>Model</th>
|
||||
<th style={{ "text-align": "left", padding: "0.75rem" }}>Score</th>
|
||||
<For each={taskIds()}>{(id) => <th style={{ "text-align": "left", padding: "0.75rem" }}>{id}</th>}</For>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<For each={benchmarks()}>
|
||||
{(row) => (
|
||||
<tr>
|
||||
<td>{row.agent}</td>
|
||||
<td>{row.model}</td>
|
||||
<td>{row.averageScore.toFixed(3)}</td>
|
||||
<td style={{ padding: "0.75rem" }}>{row.agent}</td>
|
||||
<td style={{ padding: "0.75rem" }}>{row.model}</td>
|
||||
<td style={{ padding: "0.75rem" }}>{row.averageScore.toFixed(3)}</td>
|
||||
<For each={taskIds()}>
|
||||
{(id) => (
|
||||
<td>
|
||||
<Show when={row.taskData[id]} fallback={row.taskScores[id]?.toFixed(3) ?? ""}>
|
||||
<span
|
||||
style={{ cursor: "pointer", "text-decoration": "underline" }}
|
||||
onClick={() => setModalTask(row.taskData[id])}
|
||||
>
|
||||
<td style={{ padding: "0.75rem" }}>
|
||||
<Show when={row.taskScores[id] !== undefined} fallback="">
|
||||
<A href={`/bench/${row.id}:${id}`} style={{ color: "#0066cc" }}>
|
||||
{row.taskScores[id]?.toFixed(3)}
|
||||
</span>
|
||||
</A>
|
||||
</Show>
|
||||
</td>
|
||||
)}
|
||||
|
|
@ -126,134 +81,6 @@ export default function Bench() {
|
|||
</For>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<Show when={modalTask()}>
|
||||
<div
|
||||
data-component="modal-overlay"
|
||||
style={{
|
||||
position: "fixed",
|
||||
inset: "0",
|
||||
background: "rgba(0, 0, 0, 0.5)",
|
||||
display: "flex",
|
||||
"align-items": "center",
|
||||
"justify-content": "center",
|
||||
"z-index": "1000",
|
||||
}}
|
||||
onClick={() => setModalTask(null)}
|
||||
>
|
||||
<div
|
||||
data-component="modal"
|
||||
style={{
|
||||
background: "var(--color-background, #fff)",
|
||||
padding: "1rem",
|
||||
"border-radius": "8px",
|
||||
"max-width": "80vw",
|
||||
"max-height": "80vh",
|
||||
overflow: "auto",
|
||||
}}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
<div style={{ "margin-bottom": "1rem", color: "#000" }}>
|
||||
<div>
|
||||
<strong>Repo: </strong>
|
||||
<a
|
||||
href={`https://github.com/${modalTask()!.task.source.repo}`}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
style={{ color: "#0066cc" }}
|
||||
>
|
||||
{modalTask()!.task.source.repo}
|
||||
</a>
|
||||
</div>
|
||||
<div>
|
||||
<strong>From: </strong>
|
||||
<a
|
||||
href={`https://github.com/${modalTask()!.task.source.repo}/commit/${modalTask()!.task.source.from}`}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
style={{ color: "#0066cc" }}
|
||||
>
|
||||
{modalTask()!.task.source.from.slice(0, 7)}
|
||||
</a>
|
||||
</div>
|
||||
<div>
|
||||
<strong>To: </strong>
|
||||
<a
|
||||
href={`https://github.com/${modalTask()!.task.source.repo}/commit/${modalTask()!.task.source.to}`}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
style={{ color: "#0066cc" }}
|
||||
>
|
||||
{modalTask()!.task.source.to.slice(0, 7)}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<Show when={modalTask()?.task.prompts && modalTask()!.task.prompts!.length > 0}>
|
||||
<div style={{ "margin-bottom": "1rem", color: "#000" }}>
|
||||
<strong>Prompt:</strong>
|
||||
<For each={modalTask()!.task.prompts}>
|
||||
{(p) => (
|
||||
<div style={{ "margin-top": "0.5rem" }}>
|
||||
<div style={{ "font-size": "0.875rem", color: "#666" }}>Commit: {p.commit.slice(0, 7)}</div>
|
||||
<p style={{ "margin-top": "0.25rem", "white-space": "pre-wrap" }}>{p.prompt}</p>
|
||||
</div>
|
||||
)}
|
||||
</For>
|
||||
</div>
|
||||
</Show>
|
||||
<Show when={modalTask()?.runs && modalTask()!.runs!.length > 0}>
|
||||
<div style={{ "margin-bottom": "1rem", color: "#000" }}>
|
||||
<strong>Runs:</strong>
|
||||
<table style={{ "margin-top": "0.5rem", "border-collapse": "collapse", width: "100%" }}>
|
||||
<thead>
|
||||
<tr>
|
||||
<th style={{ border: "1px solid #ccc", padding: "0.5rem", "text-align": "left" }}>Run</th>
|
||||
<th style={{ border: "1px solid #ccc", padding: "0.5rem", "text-align": "left" }}>Final</th>
|
||||
<th style={{ border: "1px solid #ccc", padding: "0.5rem", "text-align": "left" }}>Base</th>
|
||||
<th style={{ border: "1px solid #ccc", padding: "0.5rem", "text-align": "left" }}>Penalty</th>
|
||||
<For each={modalTask()!.runs![0]?.scoreDetails}>
|
||||
{(detail) => (
|
||||
<th style={{ border: "1px solid #ccc", padding: "0.5rem", "text-align": "left" }}>
|
||||
{detail.criterion} ({detail.weight})
|
||||
</th>
|
||||
)}
|
||||
</For>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<For each={modalTask()!.runs}>
|
||||
{(run, index) => (
|
||||
<tr>
|
||||
<td style={{ border: "1px solid #ccc", padding: "0.5rem" }}>{index() + 1}</td>
|
||||
<td style={{ border: "1px solid #ccc", padding: "0.5rem" }}>{run.score.final.toFixed(3)}</td>
|
||||
<td style={{ border: "1px solid #ccc", padding: "0.5rem" }}>{run.score.base.toFixed(3)}</td>
|
||||
<td style={{ border: "1px solid #ccc", padding: "0.5rem" }}>
|
||||
{run.score.penalty.toFixed(3)}
|
||||
</td>
|
||||
<For each={run.scoreDetails}>
|
||||
{(detail) => (
|
||||
<td style={{ border: "1px solid #ccc", padding: "0.5rem" }}>
|
||||
{detail.average.toFixed(3)}
|
||||
</td>
|
||||
)}
|
||||
</For>
|
||||
</tr>
|
||||
)}
|
||||
</For>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</Show>
|
||||
<Show when={modalTask()?.summary}>
|
||||
<div style={{ "margin-bottom": "1rem", color: "#000" }}>
|
||||
<strong>Summary:</strong>
|
||||
<p style={{ "margin-top": "0.5rem", "white-space": "pre-wrap" }}>{modalTask()!.summary}</p>
|
||||
</div>
|
||||
</Show>
|
||||
<pre style={{ color: "#000" }}>{JSON.stringify(modalTask(), null, 2)}</pre>
|
||||
</div>
|
||||
</div>
|
||||
</Show>
|
||||
</main>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"$schema": "https://json.schemastore.org/package.json",
|
||||
"name": "@opencode-ai/console-core",
|
||||
"version": "1.0.206",
|
||||
"version": "1.0.207",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"dependencies": {
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "@opencode-ai/console-function",
|
||||
"version": "1.0.206",
|
||||
"version": "1.0.207",
|
||||
"$schema": "https://json.schemastore.org/package.json",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "@opencode-ai/console-mail",
|
||||
"version": "1.0.206",
|
||||
"version": "1.0.207",
|
||||
"dependencies": {
|
||||
"@jsx-email/all": "2.2.3",
|
||||
"@jsx-email/cli": "1.4.3",
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"name": "@opencode-ai/desktop",
|
||||
"private": true,
|
||||
"version": "1.0.206",
|
||||
"version": "1.0.207",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"typecheck": "tsgo -b",
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "@opencode-ai/enterprise",
|
||||
"version": "1.0.206",
|
||||
"version": "1.0.207",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
id = "opencode"
|
||||
name = "OpenCode"
|
||||
description = "The open source coding agent."
|
||||
version = "1.0.206"
|
||||
version = "1.0.207"
|
||||
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.206/opencode-darwin-arm64.zip"
|
||||
archive = "https://github.com/sst/opencode/releases/download/v1.0.207/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.206/opencode-darwin-x64.zip"
|
||||
archive = "https://github.com/sst/opencode/releases/download/v1.0.207/opencode-darwin-x64.zip"
|
||||
cmd = "./opencode"
|
||||
args = ["acp"]
|
||||
|
||||
[agent_servers.opencode.targets.linux-aarch64]
|
||||
archive = "https://github.com/sst/opencode/releases/download/v1.0.206/opencode-linux-arm64.tar.gz"
|
||||
archive = "https://github.com/sst/opencode/releases/download/v1.0.207/opencode-linux-arm64.tar.gz"
|
||||
cmd = "./opencode"
|
||||
args = ["acp"]
|
||||
|
||||
[agent_servers.opencode.targets.linux-x86_64]
|
||||
archive = "https://github.com/sst/opencode/releases/download/v1.0.206/opencode-linux-x64.tar.gz"
|
||||
archive = "https://github.com/sst/opencode/releases/download/v1.0.207/opencode-linux-x64.tar.gz"
|
||||
cmd = "./opencode"
|
||||
args = ["acp"]
|
||||
|
||||
[agent_servers.opencode.targets.windows-x86_64]
|
||||
archive = "https://github.com/sst/opencode/releases/download/v1.0.206/opencode-windows-x64.zip"
|
||||
archive = "https://github.com/sst/opencode/releases/download/v1.0.207/opencode-windows-x64.zip"
|
||||
cmd = "./opencode.exe"
|
||||
args = ["acp"]
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "@opencode-ai/function",
|
||||
"version": "1.0.206",
|
||||
"version": "1.0.207",
|
||||
"$schema": "https://json.schemastore.org/package.json",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"$schema": "https://json.schemastore.org/package.json",
|
||||
"version": "1.0.206",
|
||||
"version": "1.0.207",
|
||||
"name": "opencode",
|
||||
"type": "module",
|
||||
"private": true,
|
||||
|
|
|
|||
|
|
@ -165,29 +165,44 @@ export namespace Provider {
|
|||
}
|
||||
},
|
||||
"amazon-bedrock": async () => {
|
||||
const [awsProfile, awsAccessKeyId, awsBearerToken, awsRegion] = await Promise.all([
|
||||
Env.get("AWS_PROFILE"),
|
||||
Env.get("AWS_ACCESS_KEY_ID"),
|
||||
Env.get("AWS_BEARER_TOKEN_BEDROCK"),
|
||||
Env.get("AWS_REGION"),
|
||||
])
|
||||
const auth = await Auth.get("amazon-bedrock")
|
||||
const awsProfile = Env.get("AWS_PROFILE")
|
||||
const awsAccessKeyId = Env.get("AWS_ACCESS_KEY_ID")
|
||||
const awsRegion = Env.get("AWS_REGION")
|
||||
|
||||
const awsBearerToken = iife(() => {
|
||||
const envToken = Env.get("AWS_BEARER_TOKEN_BEDROCK")
|
||||
if (envToken) return envToken
|
||||
if (auth?.type === "api") {
|
||||
Env.set("AWS_BEARER_TOKEN_BEDROCK", auth.key)
|
||||
return auth.key
|
||||
}
|
||||
return undefined
|
||||
})
|
||||
|
||||
if (!awsProfile && !awsAccessKeyId && !awsBearerToken) return { autoload: false }
|
||||
|
||||
const region = awsRegion ?? "us-east-1"
|
||||
const defaultRegion = awsRegion ?? "us-east-1"
|
||||
|
||||
const { fromNodeProviderChain } = await import(await BunProc.install("@aws-sdk/credential-providers"))
|
||||
return {
|
||||
autoload: true,
|
||||
options: {
|
||||
region,
|
||||
region: defaultRegion,
|
||||
credentialProvider: fromNodeProviderChain(),
|
||||
},
|
||||
async getModel(sdk: any, modelID: string, _options?: Record<string, any>) {
|
||||
async getModel(sdk: any, modelID: string, options?: Record<string, any>) {
|
||||
// Skip region prefixing if model already has global prefix
|
||||
if (modelID.startsWith("global.")) {
|
||||
return sdk.languageModel(modelID)
|
||||
}
|
||||
|
||||
// Region resolution precedence (highest to lowest):
|
||||
// 1. options.region from opencode.json provider config
|
||||
// 2. defaultRegion from AWS_REGION environment variable
|
||||
// 3. Default "us-east-1" (baked into defaultRegion)
|
||||
const region = options?.region ?? defaultRegion
|
||||
|
||||
let regionPrefix = region.split("-")[0]
|
||||
|
||||
switch (regionPrefix) {
|
||||
|
|
|
|||
|
|
@ -32,44 +32,59 @@ export namespace Skill {
|
|||
}),
|
||||
)
|
||||
|
||||
const SKILL_GLOB = new Bun.Glob("skill/**/SKILL.md")
|
||||
const OPENCODE_SKILL_GLOB = new Bun.Glob("skill/**/SKILL.md")
|
||||
const CLAUDE_SKILL_GLOB = new Bun.Glob(".claude/skills/**/SKILL.md")
|
||||
|
||||
export const state = Instance.state(async () => {
|
||||
const directories = await Config.directories()
|
||||
const skills: Record<string, Info> = {}
|
||||
|
||||
const addSkill = async (match: string) => {
|
||||
const md = await ConfigMarkdown.parse(match)
|
||||
if (!md) {
|
||||
return
|
||||
}
|
||||
|
||||
const parsed = Info.pick({ name: true, description: true }).safeParse(md.data)
|
||||
if (!parsed.success) return
|
||||
|
||||
// Warn on duplicate skill names
|
||||
if (skills[parsed.data.name]) {
|
||||
log.warn("duplicate skill name", {
|
||||
name: parsed.data.name,
|
||||
existing: skills[parsed.data.name].location,
|
||||
duplicate: match,
|
||||
})
|
||||
}
|
||||
|
||||
skills[parsed.data.name] = {
|
||||
name: parsed.data.name,
|
||||
description: parsed.data.description,
|
||||
location: match,
|
||||
}
|
||||
}
|
||||
|
||||
for (const dir of directories) {
|
||||
for await (const match of SKILL_GLOB.scan({
|
||||
for await (const match of OPENCODE_SKILL_GLOB.scan({
|
||||
cwd: dir,
|
||||
absolute: true,
|
||||
onlyFiles: true,
|
||||
followSymlinks: true,
|
||||
})) {
|
||||
const md = await ConfigMarkdown.parse(match)
|
||||
if (!md) {
|
||||
continue
|
||||
}
|
||||
|
||||
const parsed = Info.pick({ name: true, description: true }).safeParse(md.data)
|
||||
if (!parsed.success) continue
|
||||
|
||||
// Warn on duplicate skill names
|
||||
if (skills[parsed.data.name]) {
|
||||
log.warn("duplicate skill name", {
|
||||
name: parsed.data.name,
|
||||
existing: skills[parsed.data.name].location,
|
||||
duplicate: match,
|
||||
})
|
||||
}
|
||||
|
||||
skills[parsed.data.name] = {
|
||||
name: parsed.data.name,
|
||||
description: parsed.data.description,
|
||||
location: match,
|
||||
}
|
||||
await addSkill(match)
|
||||
}
|
||||
}
|
||||
|
||||
for await (const match of CLAUDE_SKILL_GLOB.scan({
|
||||
cwd: Instance.worktree,
|
||||
absolute: true,
|
||||
onlyFiles: true,
|
||||
followSymlinks: true,
|
||||
dot: true,
|
||||
})) {
|
||||
await addSkill(match)
|
||||
}
|
||||
|
||||
return skills
|
||||
})
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,236 @@
|
|||
import { test, expect } from "bun:test"
|
||||
import path from "path"
|
||||
import { tmpdir } from "../fixture/fixture"
|
||||
import { Instance } from "../../src/project/instance"
|
||||
import { Provider } from "../../src/provider/provider"
|
||||
import { Env } from "../../src/env"
|
||||
import { Auth } from "../../src/auth"
|
||||
import { Global } from "../../src/global"
|
||||
|
||||
test("Bedrock: config region takes precedence over AWS_REGION env var", async () => {
|
||||
await using tmp = await tmpdir({
|
||||
init: async (dir) => {
|
||||
await Bun.write(
|
||||
path.join(dir, "opencode.json"),
|
||||
JSON.stringify({
|
||||
$schema: "https://opencode.ai/config.json",
|
||||
provider: {
|
||||
"amazon-bedrock": {
|
||||
options: {
|
||||
region: "eu-west-1",
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
)
|
||||
},
|
||||
})
|
||||
await Instance.provide({
|
||||
directory: tmp.path,
|
||||
init: async () => {
|
||||
Env.set("AWS_REGION", "us-east-1")
|
||||
Env.set("AWS_PROFILE", "default")
|
||||
},
|
||||
fn: async () => {
|
||||
const providers = await Provider.list()
|
||||
expect(providers["amazon-bedrock"]).toBeDefined()
|
||||
// Region from config should be used (not env var)
|
||||
expect(providers["amazon-bedrock"].options?.region).toBe("eu-west-1")
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
test("Bedrock: falls back to AWS_REGION env var when no config", async () => {
|
||||
await using tmp = await tmpdir({
|
||||
init: async (dir) => {
|
||||
await Bun.write(
|
||||
path.join(dir, "opencode.json"),
|
||||
JSON.stringify({
|
||||
$schema: "https://opencode.ai/config.json",
|
||||
}),
|
||||
)
|
||||
},
|
||||
})
|
||||
await Instance.provide({
|
||||
directory: tmp.path,
|
||||
init: async () => {
|
||||
Env.set("AWS_REGION", "eu-west-1")
|
||||
Env.set("AWS_PROFILE", "default")
|
||||
},
|
||||
fn: async () => {
|
||||
const providers = await Provider.list()
|
||||
expect(providers["amazon-bedrock"]).toBeDefined()
|
||||
expect(providers["amazon-bedrock"].options?.region).toBe("eu-west-1")
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
test("Bedrock: without explicit region config, uses AWS_REGION env or defaults", async () => {
|
||||
await using tmp = await tmpdir({
|
||||
init: async (dir) => {
|
||||
await Bun.write(
|
||||
path.join(dir, "opencode.json"),
|
||||
JSON.stringify({
|
||||
$schema: "https://opencode.ai/config.json",
|
||||
}),
|
||||
)
|
||||
},
|
||||
})
|
||||
await Instance.provide({
|
||||
directory: tmp.path,
|
||||
init: async () => {
|
||||
Env.set("AWS_PROFILE", "default")
|
||||
// AWS_REGION might be set in the environment, use that or default
|
||||
},
|
||||
fn: async () => {
|
||||
const providers = await Provider.list()
|
||||
expect(providers["amazon-bedrock"]).toBeDefined()
|
||||
// Should have some region set (either from env or default)
|
||||
expect(providers["amazon-bedrock"].options?.region).toBeDefined()
|
||||
expect(typeof providers["amazon-bedrock"].options?.region).toBe("string")
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
test("Bedrock: uses config region in provider options", async () => {
|
||||
await using tmp = await tmpdir({
|
||||
init: async (dir) => {
|
||||
await Bun.write(
|
||||
path.join(dir, "opencode.json"),
|
||||
JSON.stringify({
|
||||
$schema: "https://opencode.ai/config.json",
|
||||
provider: {
|
||||
"amazon-bedrock": {
|
||||
options: {
|
||||
region: "eu-north-1",
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
)
|
||||
},
|
||||
})
|
||||
await Instance.provide({
|
||||
directory: tmp.path,
|
||||
init: async () => {
|
||||
Env.set("AWS_PROFILE", "default")
|
||||
},
|
||||
fn: async () => {
|
||||
const providers = await Provider.list()
|
||||
const bedrockProvider = providers["amazon-bedrock"]
|
||||
expect(bedrockProvider).toBeDefined()
|
||||
expect(bedrockProvider.options?.region).toBe("eu-north-1")
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
test("Bedrock: respects config region for different instances", async () => {
|
||||
// First instance with EU config
|
||||
await using tmp1 = await tmpdir({
|
||||
init: async (dir) => {
|
||||
await Bun.write(
|
||||
path.join(dir, "opencode.json"),
|
||||
JSON.stringify({
|
||||
$schema: "https://opencode.ai/config.json",
|
||||
provider: {
|
||||
"amazon-bedrock": {
|
||||
options: {
|
||||
region: "eu-west-1",
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
)
|
||||
},
|
||||
})
|
||||
|
||||
await Instance.provide({
|
||||
directory: tmp1.path,
|
||||
init: async () => {
|
||||
Env.set("AWS_PROFILE", "default")
|
||||
Env.set("AWS_REGION", "us-east-1")
|
||||
},
|
||||
fn: async () => {
|
||||
const providers1 = await Provider.list()
|
||||
expect(providers1["amazon-bedrock"].options?.region).toBe("eu-west-1")
|
||||
},
|
||||
})
|
||||
|
||||
// Second instance with US config
|
||||
await using tmp2 = await tmpdir({
|
||||
init: async (dir) => {
|
||||
await Bun.write(
|
||||
path.join(dir, "opencode.json"),
|
||||
JSON.stringify({
|
||||
$schema: "https://opencode.ai/config.json",
|
||||
provider: {
|
||||
"amazon-bedrock": {
|
||||
options: {
|
||||
region: "us-west-2",
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
)
|
||||
},
|
||||
})
|
||||
|
||||
await Instance.provide({
|
||||
directory: tmp2.path,
|
||||
init: async () => {
|
||||
Env.set("AWS_PROFILE", "default")
|
||||
Env.set("AWS_REGION", "eu-west-1")
|
||||
},
|
||||
fn: async () => {
|
||||
const providers2 = await Provider.list()
|
||||
expect(providers2["amazon-bedrock"].options?.region).toBe("us-west-2")
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
test("Bedrock: loads when bearer token from auth.json is present", async () => {
|
||||
await using tmp = await tmpdir({
|
||||
init: async (dir) => {
|
||||
await Bun.write(
|
||||
path.join(dir, "opencode.json"),
|
||||
JSON.stringify({
|
||||
$schema: "https://opencode.ai/config.json",
|
||||
provider: {
|
||||
"amazon-bedrock": {
|
||||
options: {
|
||||
region: "eu-west-1",
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
)
|
||||
},
|
||||
})
|
||||
|
||||
// Setup auth.json with bearer token for amazon-bedrock
|
||||
const authPath = path.join(Global.Path.data, "auth.json")
|
||||
await Bun.write(
|
||||
authPath,
|
||||
JSON.stringify({
|
||||
"amazon-bedrock": {
|
||||
type: "api",
|
||||
key: "test-bearer-token",
|
||||
},
|
||||
}),
|
||||
)
|
||||
|
||||
await Instance.provide({
|
||||
directory: tmp.path,
|
||||
init: async () => {
|
||||
// Clear env vars so only auth.json should trigger autoload
|
||||
Env.set("AWS_PROFILE", "")
|
||||
Env.set("AWS_ACCESS_KEY_ID", "")
|
||||
Env.set("AWS_BEARER_TOKEN_BEDROCK", "")
|
||||
},
|
||||
fn: async () => {
|
||||
const providers = await Provider.list()
|
||||
expect(providers["amazon-bedrock"]).toBeDefined()
|
||||
expect(providers["amazon-bedrock"].options?.region).toBe("eu-west-1")
|
||||
},
|
||||
})
|
||||
})
|
||||
|
|
@ -101,31 +101,31 @@ test("returns empty array when no skills exist", async () => {
|
|||
})
|
||||
})
|
||||
|
||||
// test("discovers skills from .claude/skills/ directory", async () => {
|
||||
// await using tmp = await tmpdir({
|
||||
// git: true,
|
||||
// init: async (dir) => {
|
||||
// const skillDir = path.join(dir, ".claude", "skills", "claude-skill")
|
||||
// await Bun.write(
|
||||
// path.join(skillDir, "SKILL.md"),
|
||||
// `---
|
||||
// name: claude-skill
|
||||
// description: A skill in the .claude/skills directory.
|
||||
// ---
|
||||
test("discovers skills from .claude/skills/ directory", async () => {
|
||||
await using tmp = await tmpdir({
|
||||
git: true,
|
||||
init: async (dir) => {
|
||||
const skillDir = path.join(dir, ".claude", "skills", "claude-skill")
|
||||
await Bun.write(
|
||||
path.join(skillDir, "SKILL.md"),
|
||||
`---
|
||||
name: claude-skill
|
||||
description: A skill in the .claude/skills directory.
|
||||
---
|
||||
|
||||
// # Claude Skill
|
||||
// `,
|
||||
// )
|
||||
// },
|
||||
// })
|
||||
# Claude Skill
|
||||
`,
|
||||
)
|
||||
},
|
||||
})
|
||||
|
||||
// await Instance.provide({
|
||||
// directory: tmp.path,
|
||||
// fn: async () => {
|
||||
// const skills = await Skill.all()
|
||||
// expect(skills.length).toBe(1)
|
||||
// expect(skills[0].name).toBe("claude-skill")
|
||||
// expect(skills[0].location).toContain(".claude/skills/claude-skill/SKILL.md")
|
||||
// },
|
||||
// })
|
||||
// })
|
||||
await Instance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
const skills = await Skill.all()
|
||||
expect(skills.length).toBe(1)
|
||||
expect(skills[0].name).toBe("claude-skill")
|
||||
expect(skills[0].location).toContain(".claude/skills/claude-skill/SKILL.md")
|
||||
},
|
||||
})
|
||||
})
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"$schema": "https://json.schemastore.org/package.json",
|
||||
"name": "@opencode-ai/plugin",
|
||||
"version": "1.0.206",
|
||||
"version": "1.0.207",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"typecheck": "tsgo --noEmit",
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"$schema": "https://json.schemastore.org/package.json",
|
||||
"name": "@opencode-ai/sdk",
|
||||
"version": "1.0.206",
|
||||
"version": "1.0.207",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"typecheck": "tsgo --noEmit",
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "@opencode-ai/slack",
|
||||
"version": "1.0.206",
|
||||
"version": "1.0.207",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "bun run src/index.ts",
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "@opencode-ai/ui",
|
||||
"version": "1.0.206",
|
||||
"version": "1.0.207",
|
||||
"type": "module",
|
||||
"exports": {
|
||||
"./*": "./src/components/*.tsx",
|
||||
|
|
|
|||
|
|
@ -390,12 +390,12 @@
|
|||
conic-gradient(
|
||||
from var(--border-angle),
|
||||
transparent 0deg,
|
||||
transparent 270deg,
|
||||
transparent 0deg,
|
||||
var(--border-warning-strong, var(--border-warning-selected)) 300deg,
|
||||
var(--border-warning-base) 360deg
|
||||
)
|
||||
border-box;
|
||||
animation: chase-border 1.5s linear infinite;
|
||||
animation: chase-border 2.5s linear infinite;
|
||||
pointer-events: none;
|
||||
z-index: -1;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -807,19 +807,19 @@ ToolRegistry.register({
|
|||
</div>
|
||||
}
|
||||
>
|
||||
<Show when={props.metadata.filediff}>
|
||||
<Show when={props.metadata.filediff?.path || props.input.filePath}>
|
||||
<div data-component="edit-content">
|
||||
<Dynamic
|
||||
component={diffComponent}
|
||||
before={{
|
||||
name: props.metadata.filediff.path,
|
||||
contents: props.metadata.filediff.before,
|
||||
cacheKey: checksum(props.metadata.filediff.before),
|
||||
name: props.metadata?.filediff?.file || props.input.filePath,
|
||||
contents: props.metadata?.filediff?.before || props.input.oldString,
|
||||
cacheKey: checksum(props.metadata?.filediff?.before || props.input.oldString),
|
||||
}}
|
||||
after={{
|
||||
name: props.metadata.filediff.path,
|
||||
contents: props.metadata.filediff.after,
|
||||
cacheKey: checksum(props.metadata.filediff.after),
|
||||
name: props.metadata?.filediff?.file || props.input.filePath,
|
||||
contents: props.metadata?.filediff?.after || props.input.newString,
|
||||
cacheKey: checksum(props.metadata?.filediff?.after || props.input.newString),
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,52 +1,24 @@
|
|||
import {
|
||||
createContext,
|
||||
useContext,
|
||||
createSignal,
|
||||
onMount,
|
||||
onCleanup,
|
||||
createEffect,
|
||||
type JSX,
|
||||
type Accessor,
|
||||
} from "solid-js"
|
||||
import { onMount, onCleanup, createEffect } from "solid-js"
|
||||
import { createStore } from "solid-js/store"
|
||||
import type { DesktopTheme } from "./types"
|
||||
import { resolveThemeVariant, themeToCss } from "./resolve"
|
||||
import { DEFAULT_THEMES } from "./default-themes"
|
||||
import { createSimpleContext } from "../context/helper"
|
||||
|
||||
export type ColorScheme = "light" | "dark" | "system"
|
||||
|
||||
interface ThemeContextValue {
|
||||
themeId: Accessor<string>
|
||||
colorScheme: Accessor<ColorScheme>
|
||||
mode: Accessor<"light" | "dark">
|
||||
themes: Accessor<Record<string, DesktopTheme>>
|
||||
setTheme: (id: string) => void
|
||||
setColorScheme: (scheme: ColorScheme) => void
|
||||
registerTheme: (theme: DesktopTheme) => void
|
||||
previewTheme: (id: string) => void
|
||||
previewColorScheme: (scheme: ColorScheme) => void
|
||||
commitPreview: () => void
|
||||
cancelPreview: () => void
|
||||
}
|
||||
|
||||
const ThemeContext = createContext<ThemeContextValue>()
|
||||
|
||||
const STORAGE_KEYS = {
|
||||
THEME_ID: "opencode-theme-id",
|
||||
COLOR_SCHEME: "opencode-color-scheme",
|
||||
THEME_CSS_PREFIX: "opencode-theme-css",
|
||||
THEME_CSS_LIGHT: "opencode-theme-css-light",
|
||||
THEME_CSS_DARK: "opencode-theme-css-dark",
|
||||
} as const
|
||||
|
||||
function getThemeCacheKey(themeId: string, mode: "light" | "dark"): string {
|
||||
return `${STORAGE_KEYS.THEME_CSS_PREFIX}-${themeId}-${mode}`
|
||||
}
|
||||
|
||||
const THEME_STYLE_ID = "oc-theme"
|
||||
|
||||
function ensureThemeStyleElement(): HTMLStyleElement {
|
||||
const existing = document.getElementById(THEME_STYLE_ID) as HTMLStyleElement | null
|
||||
if (existing) {
|
||||
return existing
|
||||
}
|
||||
if (existing) return existing
|
||||
const element = document.createElement("style")
|
||||
element.id = THEME_STYLE_ID
|
||||
document.head.appendChild(element)
|
||||
|
|
@ -57,16 +29,15 @@ function getSystemMode(): "light" | "dark" {
|
|||
return window.matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light"
|
||||
}
|
||||
|
||||
function applyThemeCss(theme: DesktopTheme, themeId: string, mode: "light" | "dark"): void {
|
||||
function applyThemeCss(theme: DesktopTheme, themeId: string, mode: "light" | "dark") {
|
||||
const isDark = mode === "dark"
|
||||
const variant = isDark ? theme.dark : theme.light
|
||||
const tokens = resolveThemeVariant(variant, isDark)
|
||||
const css = themeToCss(tokens)
|
||||
|
||||
if (themeId !== "oc-1") {
|
||||
const cacheKey = getThemeCacheKey(themeId, mode)
|
||||
try {
|
||||
localStorage.setItem(cacheKey, css)
|
||||
localStorage.setItem(isDark ? STORAGE_KEYS.THEME_CSS_DARK : STORAGE_KEYS.THEME_CSS_LIGHT, css)
|
||||
} catch {}
|
||||
}
|
||||
|
||||
|
|
@ -76,170 +47,134 @@ function applyThemeCss(theme: DesktopTheme, themeId: string, mode: "light" | "da
|
|||
${css}
|
||||
}`
|
||||
|
||||
const preloadStyle = document.getElementById("oc-theme-preload")
|
||||
if (preloadStyle) {
|
||||
preloadStyle.remove()
|
||||
}
|
||||
|
||||
const themeStyleElement = ensureThemeStyleElement()
|
||||
themeStyleElement.textContent = fullCss
|
||||
|
||||
document.getElementById("oc-theme-preload")?.remove()
|
||||
ensureThemeStyleElement().textContent = fullCss
|
||||
document.documentElement.dataset.theme = themeId
|
||||
document.documentElement.dataset.colorScheme = mode
|
||||
}
|
||||
|
||||
function cacheThemeVariants(theme: DesktopTheme, themeId: string): void {
|
||||
function cacheThemeVariants(theme: DesktopTheme, themeId: string) {
|
||||
if (themeId === "oc-1") return
|
||||
|
||||
for (const mode of ["light", "dark"] as const) {
|
||||
const isDark = mode === "dark"
|
||||
const variant = isDark ? theme.dark : theme.light
|
||||
const tokens = resolveThemeVariant(variant, isDark)
|
||||
const css = themeToCss(tokens)
|
||||
const cacheKey = getThemeCacheKey(themeId, mode)
|
||||
try {
|
||||
localStorage.setItem(cacheKey, css)
|
||||
localStorage.setItem(isDark ? STORAGE_KEYS.THEME_CSS_DARK : STORAGE_KEYS.THEME_CSS_LIGHT, css)
|
||||
} catch {}
|
||||
}
|
||||
}
|
||||
|
||||
export function ThemeProvider(props: { children: JSX.Element; defaultTheme?: string }) {
|
||||
const [themes, setThemes] = createSignal<Record<string, DesktopTheme>>(DEFAULT_THEMES)
|
||||
const [themeId, setThemeIdSignal] = createSignal(props.defaultTheme ?? "oc-1")
|
||||
const [colorScheme, setColorSchemeSignal] = createSignal<ColorScheme>("system")
|
||||
const [mode, setMode] = createSignal<"light" | "dark">(getSystemMode())
|
||||
const [previewThemeId, setPreviewThemeId] = createSignal<string | null>(null)
|
||||
const [previewScheme, setPreviewScheme] = createSignal<ColorScheme | null>(null)
|
||||
export const { use: useTheme, provider: ThemeProvider } = createSimpleContext({
|
||||
name: "Theme",
|
||||
init: (props: { defaultTheme?: string }) => {
|
||||
const [store, setStore] = createStore({
|
||||
themes: DEFAULT_THEMES as Record<string, DesktopTheme>,
|
||||
themeId: props.defaultTheme ?? "oc-1",
|
||||
colorScheme: "system" as ColorScheme,
|
||||
mode: getSystemMode(),
|
||||
previewThemeId: null as string | null,
|
||||
previewScheme: null as ColorScheme | null,
|
||||
})
|
||||
|
||||
onMount(() => {
|
||||
const mediaQuery = window.matchMedia("(prefers-color-scheme: dark)")
|
||||
const handler = () => {
|
||||
if (colorScheme() === "system") {
|
||||
setMode(getSystemMode())
|
||||
onMount(() => {
|
||||
const mediaQuery = window.matchMedia("(prefers-color-scheme: dark)")
|
||||
const handler = () => {
|
||||
if (store.colorScheme === "system") {
|
||||
setStore("mode", getSystemMode())
|
||||
}
|
||||
}
|
||||
}
|
||||
mediaQuery.addEventListener("change", handler)
|
||||
onCleanup(() => mediaQuery.removeEventListener("change", handler))
|
||||
mediaQuery.addEventListener("change", handler)
|
||||
onCleanup(() => mediaQuery.removeEventListener("change", handler))
|
||||
|
||||
const savedTheme = localStorage.getItem(STORAGE_KEYS.THEME_ID)
|
||||
const savedScheme = localStorage.getItem(STORAGE_KEYS.COLOR_SCHEME) as ColorScheme | null
|
||||
if (savedTheme && themes()[savedTheme]) {
|
||||
setThemeIdSignal(savedTheme)
|
||||
}
|
||||
if (savedScheme) {
|
||||
setColorSchemeSignal(savedScheme)
|
||||
if (savedScheme !== "system") {
|
||||
setMode(savedScheme)
|
||||
const savedTheme = localStorage.getItem(STORAGE_KEYS.THEME_ID)
|
||||
const savedScheme = localStorage.getItem(STORAGE_KEYS.COLOR_SCHEME) as ColorScheme | null
|
||||
if (savedTheme && store.themes[savedTheme]) {
|
||||
setStore("themeId", savedTheme)
|
||||
}
|
||||
if (savedScheme) {
|
||||
setStore("colorScheme", savedScheme)
|
||||
if (savedScheme !== "system") {
|
||||
setStore("mode", savedScheme)
|
||||
}
|
||||
}
|
||||
const currentTheme = store.themes[store.themeId]
|
||||
if (currentTheme) {
|
||||
cacheThemeVariants(currentTheme, store.themeId)
|
||||
}
|
||||
})
|
||||
|
||||
createEffect(() => {
|
||||
const theme = store.themes[store.themeId]
|
||||
if (theme) {
|
||||
applyThemeCss(theme, store.themeId, store.mode)
|
||||
}
|
||||
})
|
||||
|
||||
const setTheme = (id: string) => {
|
||||
const theme = store.themes[id]
|
||||
if (!theme) {
|
||||
console.warn(`Theme "${id}" not found`)
|
||||
return
|
||||
}
|
||||
setStore("themeId", id)
|
||||
localStorage.setItem(STORAGE_KEYS.THEME_ID, id)
|
||||
cacheThemeVariants(theme, id)
|
||||
}
|
||||
const currentTheme = themes()[themeId()]
|
||||
if (currentTheme) {
|
||||
cacheThemeVariants(currentTheme, themeId())
|
||||
|
||||
const setColorScheme = (scheme: ColorScheme) => {
|
||||
setStore("colorScheme", scheme)
|
||||
localStorage.setItem(STORAGE_KEYS.COLOR_SCHEME, scheme)
|
||||
setStore("mode", scheme === "system" ? getSystemMode() : scheme)
|
||||
}
|
||||
})
|
||||
|
||||
createEffect(() => {
|
||||
const id = themeId()
|
||||
const m = mode()
|
||||
const theme = themes()[id]
|
||||
if (theme) {
|
||||
applyThemeCss(theme, id, m)
|
||||
return {
|
||||
themeId: () => store.themeId,
|
||||
colorScheme: () => store.colorScheme,
|
||||
mode: () => store.mode,
|
||||
themes: () => store.themes,
|
||||
setTheme,
|
||||
setColorScheme,
|
||||
registerTheme: (theme: DesktopTheme) => setStore("themes", theme.id, theme),
|
||||
previewTheme: (id: string) => {
|
||||
const theme = store.themes[id]
|
||||
if (!theme) return
|
||||
setStore("previewThemeId", id)
|
||||
const previewMode = store.previewScheme
|
||||
? store.previewScheme === "system"
|
||||
? getSystemMode()
|
||||
: store.previewScheme
|
||||
: store.mode
|
||||
applyThemeCss(theme, id, previewMode)
|
||||
},
|
||||
previewColorScheme: (scheme: ColorScheme) => {
|
||||
setStore("previewScheme", scheme)
|
||||
const previewMode = scheme === "system" ? getSystemMode() : scheme
|
||||
const id = store.previewThemeId ?? store.themeId
|
||||
const theme = store.themes[id]
|
||||
if (theme) {
|
||||
applyThemeCss(theme, id, previewMode)
|
||||
}
|
||||
},
|
||||
commitPreview: () => {
|
||||
if (store.previewThemeId) {
|
||||
setTheme(store.previewThemeId)
|
||||
}
|
||||
if (store.previewScheme) {
|
||||
setColorScheme(store.previewScheme)
|
||||
}
|
||||
setStore("previewThemeId", null)
|
||||
setStore("previewScheme", null)
|
||||
},
|
||||
cancelPreview: () => {
|
||||
setStore("previewThemeId", null)
|
||||
setStore("previewScheme", null)
|
||||
const theme = store.themes[store.themeId]
|
||||
if (theme) {
|
||||
applyThemeCss(theme, store.themeId, store.mode)
|
||||
}
|
||||
},
|
||||
}
|
||||
})
|
||||
|
||||
const setTheme = (id: string) => {
|
||||
const theme = themes()[id]
|
||||
if (!theme) {
|
||||
console.warn(`Theme "${id}" not found`)
|
||||
return
|
||||
}
|
||||
setThemeIdSignal(id)
|
||||
localStorage.setItem(STORAGE_KEYS.THEME_ID, id)
|
||||
cacheThemeVariants(theme, id)
|
||||
}
|
||||
|
||||
const setColorSchemePref = (scheme: ColorScheme) => {
|
||||
setColorSchemeSignal(scheme)
|
||||
localStorage.setItem(STORAGE_KEYS.COLOR_SCHEME, scheme)
|
||||
if (scheme === "system") {
|
||||
setMode(getSystemMode())
|
||||
} else {
|
||||
setMode(scheme)
|
||||
}
|
||||
}
|
||||
|
||||
const registerTheme = (theme: DesktopTheme) => {
|
||||
setThemes((prev) => ({
|
||||
...prev,
|
||||
[theme.id]: theme,
|
||||
}))
|
||||
}
|
||||
|
||||
const previewTheme = (id: string) => {
|
||||
const theme = themes()[id]
|
||||
if (!theme) return
|
||||
setPreviewThemeId(id)
|
||||
const previewMode = previewScheme() ? (previewScheme() === "system" ? getSystemMode() : previewScheme()!) : mode()
|
||||
applyThemeCss(theme, id, previewMode as "light" | "dark")
|
||||
}
|
||||
|
||||
const previewColorScheme = (scheme: ColorScheme) => {
|
||||
setPreviewScheme(scheme)
|
||||
const previewMode = scheme === "system" ? getSystemMode() : scheme
|
||||
const id = previewThemeId() ?? themeId()
|
||||
const theme = themes()[id]
|
||||
if (theme) {
|
||||
applyThemeCss(theme, id, previewMode)
|
||||
}
|
||||
}
|
||||
|
||||
const commitPreview = () => {
|
||||
const id = previewThemeId()
|
||||
const scheme = previewScheme()
|
||||
if (id) {
|
||||
setTheme(id)
|
||||
}
|
||||
if (scheme) {
|
||||
setColorSchemePref(scheme)
|
||||
}
|
||||
setPreviewThemeId(null)
|
||||
setPreviewScheme(null)
|
||||
}
|
||||
|
||||
const cancelPreview = () => {
|
||||
setPreviewThemeId(null)
|
||||
setPreviewScheme(null)
|
||||
const theme = themes()[themeId()]
|
||||
if (theme) {
|
||||
applyThemeCss(theme, themeId(), mode())
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<ThemeContext.Provider
|
||||
value={{
|
||||
themeId,
|
||||
colorScheme,
|
||||
mode,
|
||||
themes,
|
||||
setTheme,
|
||||
setColorScheme: setColorSchemePref,
|
||||
registerTheme,
|
||||
previewTheme,
|
||||
previewColorScheme,
|
||||
commitPreview,
|
||||
cancelPreview,
|
||||
}}
|
||||
>
|
||||
{props.children}
|
||||
</ThemeContext.Provider>
|
||||
)
|
||||
}
|
||||
|
||||
export function useTheme(): ThemeContextValue {
|
||||
const ctx = useContext(ThemeContext)
|
||||
if (!ctx) {
|
||||
throw new Error("useTheme must be used within a ThemeProvider")
|
||||
}
|
||||
return ctx
|
||||
}
|
||||
},
|
||||
})
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "@opencode-ai/util",
|
||||
"version": "1.0.206",
|
||||
"version": "1.0.207",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"exports": {
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"name": "@opencode-ai/web",
|
||||
"type": "module",
|
||||
"version": "1.0.206",
|
||||
"version": "1.0.207",
|
||||
"scripts": {
|
||||
"dev": "astro dev",
|
||||
"dev:remote": "VITE_API_URL=https://api.opencode.ai astro dev",
|
||||
|
|
|
|||
|
|
@ -354,12 +354,21 @@ If you have a large number of MCP servers you may want to only enable them per a
|
|||
|
||||
#### Glob patterns
|
||||
|
||||
The glob pattern uses simple regex globbing patterns.
|
||||
The glob pattern uses simple regex globbing patterns:
|
||||
|
||||
- `*` matches zero or more of any character
|
||||
- `*` matches zero or more of any character (e.g., `"my-mcp*"` matches `my-mcp_search`, `my-mcp_list`, etc.)
|
||||
- `?` matches exactly one character
|
||||
- All other characters match literally
|
||||
|
||||
:::note
|
||||
MCP server tools are registered with server name as prefix, so to diable all tools for a server simply use:
|
||||
|
||||
```
|
||||
"mymcpservername_*": false
|
||||
```
|
||||
|
||||
:::
|
||||
|
||||
---
|
||||
|
||||
## Examples
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
"name": "opencode",
|
||||
"displayName": "opencode",
|
||||
"description": "opencode for VS Code",
|
||||
"version": "1.0.206",
|
||||
"version": "1.0.207",
|
||||
"publisher": "sst-dev",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
|
|
|
|||
Loading…
Reference in New Issue