Sfoglia il codice sorgente

Merge branch 'dev' into changelog-updates

Aiden Cline 3 mesi fa
parent
commit
a96f3010a6

+ 15 - 15
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 - 1
packages/app/package.json

@@ -1,6 +1,6 @@
 {
   "name": "@opencode-ai/app",
-  "version": "1.0.206",
+  "version": "1.0.207",
   "description": "",
   "type": "module",
   "exports": {

+ 6 - 12
packages/app/src/context/command.tsx

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

+ 12 - 4
packages/app/src/pages/layout.tsx

@@ -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 - 1
packages/console/app/package.json

@@ -1,6 +1,6 @@
 {
   "name": "@opencode-ai/console-app",
-  "version": "1.0.206",
+  "version": "1.0.207",
   "type": "module",
   "scripts": {
     "typecheck": "tsgo --noEmit",

+ 365 - 0
packages/console/app/src/routes/bench/[id].tsx

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

+ 18 - 191
packages/console/app/src/routes/bench/index.tsx

@@ -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 - 1
packages/console/core/package.json

@@ -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 - 1
packages/console/function/package.json

@@ -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 - 1
packages/console/mail/package.json

@@ -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 - 1
packages/desktop/package.json

@@ -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 - 1
packages/enterprise/package.json

@@ -1,6 +1,6 @@
 {
   "name": "@opencode-ai/enterprise",
-  "version": "1.0.206",
+  "version": "1.0.207",
   "private": true,
   "type": "module",
   "scripts": {

+ 6 - 6
packages/extensions/zed/extension.toml

@@ -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 - 1
packages/function/package.json

@@ -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 - 1
packages/opencode/package.json

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

+ 24 - 9
packages/opencode/src/provider/provider.ts

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

+ 39 - 24
packages/opencode/src/skill/skill.ts

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

+ 236 - 0
packages/opencode/test/provider/amazon-bedrock.test.ts

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

+ 26 - 26
packages/opencode/test/skill/skill.test.ts

@@ -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 - 1
packages/plugin/package.json

@@ -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 - 1
packages/sdk/js/package.json

@@ -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 - 1
packages/slack/package.json

@@ -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 - 1
packages/ui/package.json

@@ -1,6 +1,6 @@
 {
   "name": "@opencode-ai/ui",
-  "version": "1.0.206",
+  "version": "1.0.207",
   "type": "module",
   "exports": {
     "./*": "./src/components/*.tsx",

+ 2 - 2
packages/ui/src/components/message-part.css

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

+ 7 - 7
packages/ui/src/components/message-part.tsx

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

+ 114 - 179
packages/ui/src/theme/context.tsx

@@ -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)
-
-  onMount(() => {
-    const mediaQuery = window.matchMedia("(prefers-color-scheme: dark)")
-    const handler = () => {
-      if (colorScheme() === "system") {
-        setMode(getSystemMode())
+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 (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)
       }
-    }
-    const currentTheme = themes()[themeId()]
-    if (currentTheme) {
-      cacheThemeVariants(currentTheme, themeId())
-    }
-  })
-
-  createEffect(() => {
-    const id = themeId()
-    const m = mode()
-    const theme = themes()[id]
-    if (theme) {
-      applyThemeCss(theme, id, m)
-    }
-  })
-
-  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,
-    }))
-  }
+      if (savedScheme) {
+        setStore("colorScheme", savedScheme)
+        if (savedScheme !== "system") {
+          setStore("mode", savedScheme)
+        }
+      }
+      const currentTheme = store.themes[store.themeId]
+      if (currentTheme) {
+        cacheThemeVariants(currentTheme, store.themeId)
+      }
+    })
 
-  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")
-  }
+    createEffect(() => {
+      const theme = store.themes[store.themeId]
+      if (theme) {
+        applyThemeCss(theme, store.themeId, store.mode)
+      }
+    })
 
-  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 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 commitPreview = () => {
-    const id = previewThemeId()
-    const scheme = previewScheme()
-    if (id) {
-      setTheme(id)
-    }
-    if (scheme) {
-      setColorSchemePref(scheme)
+    const setColorScheme = (scheme: ColorScheme) => {
+      setStore("colorScheme", scheme)
+      localStorage.setItem(STORAGE_KEYS.COLOR_SCHEME, scheme)
+      setStore("mode", scheme === "system" ? getSystemMode() : scheme)
     }
-    setPreviewThemeId(null)
-    setPreviewScheme(null)
-  }
 
-  const cancelPreview = () => {
-    setPreviewThemeId(null)
-    setPreviewScheme(null)
-    const theme = themes()[themeId()]
-    if (theme) {
-      applyThemeCss(theme, themeId(), mode())
+    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)
+        }
+      },
     }
-  }
-
-  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 - 1
packages/util/package.json

@@ -1,6 +1,6 @@
 {
   "name": "@opencode-ai/util",
-  "version": "1.0.206",
+  "version": "1.0.207",
   "private": true,
   "type": "module",
   "exports": {

+ 1 - 1
packages/web/package.json

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

+ 11 - 2
packages/web/src/content/docs/mcp-servers.mdx

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

+ 1 - 1
sdks/vscode/package.json

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