Compare commits

..

26 Commits

Author SHA1 Message Date
Aiden Cline 106eeb8536 tweak 2025-12-29 10:51:47 -06:00
Aiden Cline 888361892d ci: update changelog gen 2025-12-29 10:48:59 -06:00
Aiden Cline a96f3010a6 Merge branch 'dev' into changelog-updates 2025-12-28 22:57:20 -06:00
Aiden Cline 619127c2ed ci: add changelog script 2025-12-28 22:57:12 -06:00
GitHub Action 6963f96d4b chore: generate 2025-12-29 04:56:54 +00:00
Alice Alexandra Moore 05a9e7ce7a docs: clarify that MCP tools require glob patterns to disable (#6306)
Co-authored-by: Aiden Cline <63023139+rekram1-node@users.noreply.github.com>
2025-12-28 22:56:22 -06:00
GitHub Action 896d18ab3f chore: generate 2025-12-29 04:44:48 +00:00
Grégoire Morpain 893888536a fix(bedrock): support region and bearer token configuration (#6332)
Co-authored-by: Aiden Cline <63023139+rekram1-node@users.noreply.github.com>
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2025-12-28 22:44:15 -06:00
GitHub Action c6221fc8b3 chore: generate 2025-12-29 04:39:43 +00:00
Connor Adams ae67f43ff0 feat: add support for .claude/skills directory (#6252) 2025-12-28 22:39:10 -06:00
opencode 76880dce0d release: v1.0.207 2025-12-29 01:57:53 +00:00
Adam aafffb5b4b chore: cleanup 2025-12-28 19:54:22 -06:00
Adam a71c9e3f2e fix(desktop): edit diffs 2025-12-28 19:49:39 -06:00
Adam 0156f03e0e chore: cleanup theme stuff 2025-12-28 19:27:36 -06:00
Frank e0bb96a9f9 wip: bench 2025-12-28 20:00:49 -05:00
Daniel Polito 82e5d6d458 Desktop: Sync LSP updates (#6305) 2025-12-28 16:07:36 -06:00
Adam a4411c21b6 feat(desktop): theme preview 2025-12-28 15:47:05 -06:00
Frank 9d61370ac4 sync 2025-12-28 15:33:18 -05:00
Frank f3febd6e39 wip: benchmark 2025-12-28 14:55:05 -05:00
GitHub Action f12d55bf1e chore: generate 2025-12-28 19:13:43 +00:00
Matt Silverlock 0c19b71f42 docs: add plugin configuration documentation (#6301)
Co-authored-by: OpenCode <opencode@example.com>
2025-12-28 13:13:11 -06:00
Mohak S 70fa66397e docs: add opencode-notifier plugin to ecosystem (#6283) 2025-12-28 13:09:38 -06:00
Daniel Polito 6e8cd3174c Include current working directory in local MCP transport (#6303) 2025-12-28 13:09:24 -06:00
GitHub Action 5bfffbe083 chore: generate 2025-12-28 19:06:59 +00:00
Didier Durand 29d8557d41 doc: fix typos in various files (#6294) 2025-12-28 13:06:25 -06:00
Didier Durand ffd20b4477 chore: activate code coverage in bun test config (#6297) 2025-12-28 19:05:55 +00:00
44 changed files with 1326 additions and 426 deletions
+15 -15
View File
@@ -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
View File
@@ -1,6 +1,6 @@
{
"name": "@opencode-ai/app",
"version": "1.0.206",
"version": "1.0.207",
"description": "",
"type": "module",
"exports": {
+25 -6
View File
@@ -26,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[] {
@@ -115,6 +116,28 @@ export function formatKeybind(config: string): string {
function DialogCommand(props: { options: CommandOption[] }) {
const dialog = useDialog()
let cleanup: (() => void) | void
let committed = false
const handleMove = (option: CommandOption | undefined) => {
cleanup?.()
cleanup = option?.onHighlight?.()
}
const handleSelect = (option: CommandOption | undefined) => {
if (option) {
committed = true
cleanup = undefined
dialog.close()
option.onSelect?.("palette")
}
}
onCleanup(() => {
if (!committed) {
cleanup?.()
}
})
return (
<Dialog title="Commands">
@@ -125,12 +148,8 @@ function DialogCommand(props: { options: CommandOption[] }) {
key={(x) => x?.id}
filterKeys={["title", "description", "category"]}
groupBy={(x) => x.category ?? ""}
onSelect={(option) => {
if (option) {
dialog.close()
option.onSelect?.("palette")
}
}}
onMove={handleMove}
onSelect={handleSelect}
>
{(option) => (
<div class="w-full flex items-center justify-between gap-4">
+9
View File
@@ -361,6 +361,15 @@ function createGlobalSync() {
)
break
}
case "lsp.updated": {
const sdk = createOpencodeClient({
baseUrl: globalSDK.url,
directory,
throwOnError: true,
})
sdk.lsp.status().then((x) => setStore("lsp", x.data ?? []))
break
}
}
})
+12 -4
View File
@@ -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
View File
@@ -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>
)
}
+45 -11
View File
@@ -1,20 +1,31 @@
import { Title } from "@solidjs/meta"
import { createAsync, query } from "@solidjs/router"
import { For } 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 BenchmarkResult {
averageScore: number
tasks: { averageScore: number; task: { id: string } }[]
}
async function getBenchmarks() {
"use server"
const rows = await Database.use((tx) =>
tx.select().from(BenchmarkTable).orderBy(desc(BenchmarkTable.timeCreated)).limit(100),
)
return rows.map((row) => {
const parsed = JSON.parse(row.result) as { averageScore: number }
const parsed = JSON.parse(row.result) as BenchmarkResult
const taskScores: Record<string, number> = {}
for (const t of parsed.tasks) {
taskScores[t.task.id] = t.averageScore
}
return {
id: row.id,
agent: row.agent,
model: row.model,
averageScore: parsed.averageScore,
taskScores,
}
})
}
@@ -24,24 +35,47 @@ const queryBenchmarks = query(getBenchmarks, "benchmarks.list")
export default function Bench() {
const benchmarks = createAsync(() => queryBenchmarks())
const taskIds = createMemo(() => {
const ids = new Set<string>()
for (const row of benchmarks() ?? []) {
for (const id of Object.keys(row.taskScores)) {
ids.add(id)
}
}
return [...ids].sort()
})
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>Average Score</th>
<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}</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 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)}
</A>
</Show>
</td>
)}
</For>
</tr>
)}
</For>
+1 -1
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -1,6 +1,6 @@
{
"name": "@opencode-ai/enterprise",
"version": "1.0.206",
"version": "1.0.207",
"private": true,
"type": "module",
"scripts": {
+6 -6
View File
@@ -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
View File
@@ -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",
+2
View File
@@ -2,3 +2,5 @@ preload = ["@opentui/solid/preload"]
[test]
preload = ["./test/preload.ts"]
# Enable code coverage
coverage = true
+1 -1
View File
@@ -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,
+1 -1
View File
@@ -98,7 +98,7 @@ export namespace Config {
for (const file of ["opencode.jsonc", "opencode.json"]) {
log.debug(`loading config from ${path.join(dir, file)}`)
result = mergeConfigWithPlugins(result, await loadFile(path.join(dir, file)))
// to satisy the type checker
// to satisfy the type checker
result.agent ??= {}
result.mode ??= {}
result.plugin ??= []
+3
View File
@@ -308,10 +308,12 @@ export namespace MCP {
if (mcp.type === "local") {
const [cmd, ...args] = mcp.command
const cwd = Instance.directory
const transport = new StdioClientTransport({
stderr: "ignore",
command: cmd,
args,
cwd,
env: {
...process.env,
...(cmd === "opencode" ? { BUN_BE_BUN: "1" } : {}),
@@ -334,6 +336,7 @@ export namespace MCP {
log.error("local mcp startup failed", {
key,
command: mcp.command,
cwd,
error: error instanceof Error ? error.message : String(error),
})
status = {
+24 -9
View File
@@ -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) {
@@ -68,7 +68,7 @@ Carefully read the issue and think hard about a plan to solve it before coding.
- Use the `webfetch` tool to search google by fetching the URL `https://www.google.com/search?q=your+search+query`.
- After fetching, review the content returned by the fetch tool.
- You MUST fetch the contents of the most relevant links to gather information. Do not rely on the summary that you find in the search results.
- As you fetch each link, read the content thoroughly and fetch any additional links that you find withhin the content that are relevant to the problem.
- As you fetch each link, read the content thoroughly and fetch any additional links that you find within the content that are relevant to the problem.
- Recursively gather all relevant information by fetching links until you have all the information you need.
## 5. Develop a Detailed Plan
@@ -222,7 +222,7 @@ The messages you send before tool calls should describe what is immediately abou
Your final message should read naturally, like an update from a concise teammate. For casual conversation, brainstorming tasks, or quick questions from the user, respond in a friendly, conversational tone. You should ask questions, suggest ideas, and adapt to the users style. If you've finished a large amount of work, when describing what you've done to the user, you should follow the final answer formatting guidelines to communicate substantive changes. You don't need to add structured formatting for one-word answers, greetings, or purely conversational exchanges.
You can skip heavy formatting for single, simple actions or confirmations. In these cases, respond in plain sentences with any relevant next step or quick option. Reserve multi-section structured responses for results that need grouping or explanation.
You can skip heavy formatting for single, simple actions or confirmations. In these cases, respond in plain sentences with any relevant next step or quick option. Reserve multisection structured responses for results that need grouping or explanation.
The user is working on the same computer as you, and has access to your work. As such there's no need to show the full contents of large files you have already written unless the user explicitly asks for them. Similarly, if you've created or modified files using `edit`, there's no need to tell users to "save the file" or "copy the code into a file"—just reference the file path.
@@ -259,7 +259,7 @@ You are producing plain text that will later be styled by the CLI. Follow these
**File References**
When referencing files in your response, make sure to include the relevant start line and always follow the below rules:
* Use inline code to make file paths clickable.
* Each reference should have a stand alone path. Even if it's the same file.
* Each reference should have a standalone path. Even if it's the same file.
* Accepted: absolute, workspacerelative, a/ or b/ diff prefixes, or bare filename/suffix.
* Line/column (1based, optional): :line[:column] or #Lline[Ccolumn] (column defaults to 1).
* Do not use URIs like file://, vscode://, or https://.
+39 -24
View File
@@ -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
})
+1 -1
View File
@@ -2,5 +2,5 @@
- Supports glob patterns like "**/*.js" or "src/**/*.ts"
- Returns matching file paths sorted by modification time
- Use this tool when you need to find files by name patterns
- When you are doing an open ended search that may require multiple rounds of globbing and grepping, use the Task tool instead
- When you are doing an open-ended search that may require multiple rounds of globbing and grepping, use the Task tool instead
- You have the capability to call multiple tools in a single response. It is always better to speculatively perform multiple searches as a batch that are potentially useful.
@@ -14,7 +14,7 @@ but this is not:
We also need to deal with files followed by @commas, ones
with @file-extensions.md, even @multiple.extensions.bak,
hidden directorys like @.config/ or files like @.bashrc
hidden directories like @.config/ or files like @.bashrc
and ones at the end of a sentence like @foo.md.
Also shouldn't forget @/absolute/paths.txt with and @/without/extensions,
@@ -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
View File
@@ -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")
},
})
})
+2 -2
View File
@@ -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",
@@ -24,4 +24,4 @@
"typescript": "catalog:",
"@typescript/native-preview": "catalog:"
}
}
}
+2 -2
View File
@@ -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",
@@ -29,4 +29,4 @@
"publishConfig": {
"directory": "dist"
}
}
}
+1 -1
View File
@@ -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
View File
@@ -1,6 +1,6 @@
{
"name": "@opencode-ai/ui",
"version": "1.0.206",
"version": "1.0.207",
"type": "module",
"exports": {
"./*": "./src/components/*.tsx",
+8
View File
@@ -15,6 +15,7 @@ export interface ListProps<T> extends FilteredListProps<T> {
children: (item: T) => JSX.Element
emptyMessage?: string
onKeyEvent?: (event: KeyboardEvent, item: T | undefined) => void
onMove?: (item: T | undefined) => void
activeIcon?: IconProps["name"]
filter?: string
search?: ListSearchProps | boolean
@@ -82,6 +83,13 @@ export function List<T>(props: ListProps<T> & { ref?: (ref: ListRef) => void })
element?.scrollIntoView({ block: "center", behavior: "smooth" })
})
createEffect(() => {
const all = flat()
const current = active()
const item = all.find((x) => props.key(x) === current)
props.onMove?.(item)
})
const handleSelect = (item: T | undefined, index: number) => {
props.onSelect?.(item, index)
}
+2 -2
View File
@@ -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
View File
@@ -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>
+116 -131
View File
@@ -1,48 +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
}
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)
@@ -53,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 {}
}
@@ -72,124 +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())
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,
}))
}
return (
<ThemeContext.Provider
value={{
themeId,
colorScheme,
mode,
themes,
setTheme,
setColorScheme: setColorSchemePref,
registerTheme,
}}
>
{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
View File
@@ -1,6 +1,6 @@
{
"name": "@opencode-ai/util",
"version": "1.0.206",
"version": "1.0.207",
"private": true,
"type": "module",
"exports": {
+1 -1
View File
@@ -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",
+17
View File
@@ -378,6 +378,23 @@ You can configure MCP servers you want to use through the `mcp` option.
---
### Plugins
[Plugins](/docs/plugins) extend OpenCode with custom tools, hooks, and integrations.
Place plugin files in `.opencode/plugin/` or `~/.config/opencode/plugin/`. You can also load plugins from npm through the `plugin` option.
```json title="opencode.json"
{
"$schema": "https://opencode.ai/config.json",
"plugin": ["opencode-helicone-session", "@my-org/custom-plugin"]
}
```
[Learn more here](/docs/plugins).
---
### Instructions
You can configure the instructions for the model you're using through the `instructions` option.
@@ -33,6 +33,7 @@ You can also check out [awesome-opencode](https://github.com/awesome-opencode/aw
| [opencode-morph-fast-apply](https://github.com/JRedeker/opencode-morph-fast-apply) | 10x faster code editing with Morph Fast Apply API and lazy edit markers |
| [oh-my-opencode](https://github.com/code-yeongyu/oh-my-opencode) | Background agents, pre-built LSP/AST/MCP tools, curated agents, Claude Code compatible |
| [opencode-notificator](https://github.com/panta/opencode-notificator) | Desktop notifications and sound alerts for OpenCode sessions |
| [opencode-notifier](https://github.com/mohak34/opencode-notifier) | Desktop notifications and sound alerts for permission, completion, and error events |
| [opencode-zellij-namer](https://github.com/24601/opencode-zellij-namer) | AI-powered automatic Zellij session naming based on OpenCode context |
---
+11 -2
View File
@@ -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
+55 -9
View File
@@ -9,6 +9,61 @@ For examples, check out the [plugins](/docs/ecosystem#plugins) created by the co
---
## Use a plugin
There are two ways to load plugins.
---
### From local files
Place JavaScript or TypeScript files in the plugin directory.
- `.opencode/plugin/` - Project-level plugins
- `~/.config/opencode/plugin/` - Global plugins
Files in these directories are automatically loaded at startup.
---
### From npm
Specify npm packages in your config file.
```json title="opencode.json"
{
"$schema": "https://opencode.ai/config.json",
"plugin": ["opencode-helicone-session", "opencode-wakatime", "@my-org/custom-plugin"]
}
```
Both regular and scoped npm packages are supported.
Browse available plugins in the [ecosystem](/docs/ecosystem#plugins).
---
### How plugins are installed
**npm plugins** are installed automatically using Bun at startup. Packages and their dependencies are cached in `~/.cache/opencode/node_modules/`.
**Local plugins** are loaded directly from the plugin directory. Dependencies are not installed automatically. If your local plugin requires external packages, publish it to npm instead and add it to your config.
---
### Load order
Plugins are loaded from all sources and all hooks run in sequence. The load order is:
1. Global config (`~/.config/opencode/opencode.json`)
2. Project config (`opencode.json`)
3. Global plugin directory (`~/.config/opencode/plugin/`)
4. Project plugin directory (`.opencode/plugin/`)
Duplicate npm packages with the same name and version are loaded once. However, a local plugin and an npm plugin with similar names are both loaded separately.
---
## Create a plugin
A plugin is a **JavaScript/TypeScript module** that exports one or more plugin
@@ -16,15 +71,6 @@ functions. Each function receives a context object and returns a hooks object.
---
### Location
Plugins are loaded from:
1. `.opencode/plugin` directory either in your project
2. Or, globally in `~/.config/opencode/plugin`
---
### Basic structure
```js title=".opencode/plugin/example.js"
+274
View File
@@ -0,0 +1,274 @@
#!/usr/bin/env bun
import { $ } from "bun"
import { createOpencode } from "@opencode-ai/sdk"
import { parseArgs } from "util"
export const team = [
"actions-user",
"opencode",
"rekram1-node",
"thdxr",
"kommander",
"jayair",
"fwang",
"adamdotdevin",
"iamdavidhill",
"opencode-agent[bot]",
]
export async function getLatestRelease() {
return fetch("https://api.github.com/repos/sst/opencode/releases/latest")
.then((res) => {
if (!res.ok) throw new Error(res.statusText)
return res.json()
})
.then((data: any) => data.tag_name.replace(/^v/, ""))
}
type Commit = {
hash: string
author: string | null
message: string
areas: Set<string>
}
export async function getCommits(from: string, to: string): Promise<Commit[]> {
const fromRef = from.startsWith("v") ? from : `v${from}`
const toRef = to === "HEAD" ? to : to.startsWith("v") ? to : `v${to}`
// Get commit data with GitHub usernames from the API
const compare =
await $`gh api "/repos/sst/opencode/compare/${fromRef}...${toRef}" --jq '.commits[] | {sha: .sha, login: .author.login, message: .commit.message}'`.text()
const commitData = new Map<string, { login: string | null; message: string }>()
for (const line of compare.split("\n").filter(Boolean)) {
const data = JSON.parse(line) as { sha: string; login: string | null; message: string }
commitData.set(data.sha, { login: data.login, message: data.message.split("\n")[0] ?? "" })
}
// Get commits that touch the relevant packages
const log =
await $`git log ${fromRef}..${toRef} --oneline --format="%H" -- packages/opencode packages/sdk packages/plugin packages/desktop packages/app sdks/vscode packages/extensions github`.text()
const hashes = log.split("\n").filter(Boolean)
const commits: Commit[] = []
for (const hash of hashes) {
const data = commitData.get(hash)
if (!data) continue
const message = data.message
if (message.match(/^(ignore:|test:|chore:|ci:|release:)/i)) continue
const files = await $`git diff-tree --no-commit-id --name-only -r ${hash}`.text()
const areas = new Set<string>()
for (const file of files.split("\n").filter(Boolean)) {
if (file.startsWith("packages/opencode/src/cli/cmd/tui/")) areas.add("tui")
else if (file.startsWith("packages/opencode/")) areas.add("core")
else if (file.startsWith("packages/desktop/src-tauri/")) areas.add("tauri")
else if (file.startsWith("packages/desktop/")) areas.add("app")
else if (file.startsWith("packages/app/")) areas.add("app")
else if (file.startsWith("packages/sdk/")) areas.add("sdk")
else if (file.startsWith("packages/plugin/")) areas.add("plugin")
else if (file.startsWith("packages/extensions/")) areas.add("extensions/zed")
else if (file.startsWith("sdks/vscode/")) areas.add("extensions/vscode")
else if (file.startsWith("github/")) areas.add("github")
}
if (areas.size === 0) continue
commits.push({
hash: hash.slice(0, 7),
author: data.login,
message,
areas,
})
}
return commits
}
const sections = {
core: "Core",
tui: "TUI",
app: "Desktop",
tauri: "Desktop",
sdk: "SDK",
plugin: "SDK",
"extensions/zed": "Extensions",
"extensions/vscode": "Extensions",
github: "Extensions",
} as const
function getSection(areas: Set<string>): string {
// Priority order for multi-area commits
const priority = ["core", "tui", "app", "tauri", "sdk", "plugin", "extensions/zed", "extensions/vscode", "github"]
for (const area of priority) {
if (areas.has(area)) return sections[area as keyof typeof sections]
}
return "Core"
}
async function summarizeCommit(
opencode: Awaited<ReturnType<typeof createOpencode>>,
sessionId: string,
message: string,
): Promise<string> {
console.log("summarizing commit:", message)
const result = await opencode.client.session
.prompt({
path: { id: sessionId },
body: {
model: { providerID: "opencode", modelID: "claude-sonnet-4-5" },
tools: {
"*": false,
},
parts: [
{
type: "text",
text: `Summarize this commit message for a changelog entry. Return ONLY a single line summary starting with a capital letter. Be concise but specific. If the commit message is already well-written, just clean it up (capitalize, fix typos, proper grammar). Do not include any prefixes like "fix:" or "feat:".
Commit: ${message}`,
},
],
},
signal: AbortSignal.timeout(120_000),
})
.then((x) => x.data?.parts?.find((y) => y.type === "text")?.text ?? message)
return result.trim()
}
export async function generateChangelog(commits: Commit[], opencode: Awaited<ReturnType<typeof createOpencode>>) {
const session = await opencode.client.session.create()
const grouped = new Map<string, string[]>()
for (const commit of commits) {
const section = getSection(commit.areas)
const summary = await summarizeCommit(opencode, session.data!.id, commit.message)
const attribution = commit.author && !team.includes(commit.author) ? ` (@${commit.author})` : ""
const entry = `- ${summary}${attribution}`
if (!grouped.has(section)) grouped.set(section, [])
grouped.get(section)!.push(entry)
}
const sectionOrder = ["Core", "TUI", "Desktop", "SDK", "Extensions"]
const lines: string[] = []
for (const section of sectionOrder) {
const entries = grouped.get(section)
if (!entries || entries.length === 0) continue
lines.push(`## ${section}`)
lines.push(...entries)
}
return lines
}
export async function getContributors(from: string, to: string) {
const fromRef = from.startsWith("v") ? from : `v${from}`
const toRef = to === "HEAD" ? to : to.startsWith("v") ? to : `v${to}`
const compare =
await $`gh api "/repos/sst/opencode/compare/${fromRef}...${toRef}" --jq '.commits[] | {login: .author.login, message: .commit.message}'`.text()
const contributors = new Map<string, string[]>()
for (const line of compare.split("\n").filter(Boolean)) {
const { login, message } = JSON.parse(line) as { login: string | null; message: string }
const title = message.split("\n")[0] ?? ""
if (title.match(/^(ignore:|test:|chore:|ci:|release:)/i)) continue
if (login && !team.includes(login)) {
if (!contributors.has(login)) contributors.set(login, [])
contributors.get(login)?.push(title)
}
}
return contributors
}
export async function buildNotes(from: string, to: string) {
const commits = await getCommits(from, to)
if (commits.length === 0) {
return []
}
console.log("generating changelog since " + from)
const opencode = await createOpencode({ port: 5044 })
const notes: string[] = []
try {
const lines = await generateChangelog(commits, opencode)
notes.push(...lines)
console.log("---- Generated Changelog ----")
console.log(notes.join("\n"))
console.log("-----------------------------")
} catch (error) {
if (error instanceof Error && error.name === "TimeoutError") {
console.log("Changelog generation timed out, using raw commits")
for (const commit of commits) {
const attribution = commit.author && !team.includes(commit.author) ? ` (@${commit.author})` : ""
notes.push(`- ${commit.message}${attribution}`)
}
} else {
throw error
}
} finally {
opencode.server.close()
}
const contributors = await getContributors(from, to)
if (contributors.size > 0) {
notes.push("")
notes.push(`**Thank you to ${contributors.size} community contributor${contributors.size > 1 ? "s" : ""}:**`)
for (const [username, userCommits] of contributors) {
notes.push(`- @${username}:`)
for (const c of userCommits) {
notes.push(` - ${c}`)
}
}
}
return notes
}
// CLI entrypoint
if (import.meta.main) {
const { values } = parseArgs({
args: Bun.argv.slice(2),
options: {
from: { type: "string", short: "f" },
to: { type: "string", short: "t", default: "HEAD" },
help: { type: "boolean", short: "h", default: false },
},
})
if (values.help) {
console.log(`
Usage: bun script/changelog.ts [options]
Options:
-f, --from <version> Starting version (default: latest GitHub release)
-t, --to <ref> Ending ref (default: HEAD)
-h, --help Show this help message
Examples:
bun script/changelog.ts # Latest release to HEAD
bun script/changelog.ts --from 1.0.200 # v1.0.200 to HEAD
bun script/changelog.ts -f 1.0.200 -t 1.0.205
`)
process.exit(0)
}
const to = values.to!
const from = values.from ?? (await getLatestRelease())
console.log(`Generating changelog: v${from} -> ${to}\n`)
const notes = await buildNotes(from, to)
console.log("\n=== Final Notes ===")
console.log(notes.join("\n"))
}
+4 -150
View File
@@ -1,162 +1,16 @@
#!/usr/bin/env bun
import { $ } from "bun"
import { createOpencode } from "@opencode-ai/sdk"
import { Script } from "@opencode-ai/script"
import { buildNotes, getLatestRelease } from "./changelog"
const notes = [] as string[]
let notes: string[] = []
console.log("=== publishing ===\n")
if (!Script.preview) {
const previous = await fetch("https://api.github.com/repos/sst/opencode/releases/latest")
.then((res) => {
if (!res.ok) throw new Error(res.statusText)
return res.json()
})
.then((data: any) => data.tag_name.replace(/^v/, ""))
const log =
await $`git log v${previous}..HEAD --oneline --format="%h %s" -- packages/opencode packages/sdk packages/plugin packages/desktop packages/app`.text()
const commits = log.split("\n").filter((line) => line && !line.match(/^\w+ (ignore:|test:|chore:|ci:)/i))
const team = [
"actions-user",
"opencode",
"rekram1-node",
"thdxr",
"kommander",
"jayair",
"fwang",
"adamdotdevin",
"iamdavidhill",
"opencode-agent[bot]",
]
async function generateChangelog() {
const opencode = await createOpencode()
const session = await opencode.client.session.create()
console.log("generating changelog since " + previous)
const raw = await opencode.client.session
.prompt({
path: {
id: session.data!.id,
},
body: {
model: {
providerID: "opencode",
modelID: "claude-sonnet-4-5",
},
parts: [
{
type: "text",
text: `
Analyze these commits and generate a changelog of all notable user facing changes, grouped by area.
Each commit below includes:
- [author: username] showing the GitHub username of the commit author
- [areas: ...] showing which areas of the codebase were modified
Commits between ${previous} and HEAD:
${commits.join("\n")}
Group the changes into these categories based on the [areas: ...] tags (omit any category with no changes):
- **TUI**: Changes to "opencode" area (the terminal/CLI interface)
- **Desktop**: Changes to "app" or "tauri" areas (the desktop application)
- **SDK**: Changes to "sdk" or "plugin" areas (the SDK and plugin system)
- **Extensions**: Changes to "extensions/zed", "extensions/vscode", or "github" areas (editor extensions and GitHub Action)
- **Other**: Any user-facing changes that don't fit the above categories
Excluded areas (omit these entirely unless they contain user-facing changes like refactors that may affect behavior):
- "nix", "infra", "script" - CI/build infrastructure
- "ui", "docs", "web", "console", "enterprise", "function", "util", "identity", "slack" - internal packages
Rules:
- Use the [areas: ...] tags to determine the correct category. If a commit touches multiple areas, put it in the most relevant user-facing category.
- ONLY include commits that have user-facing impact. Omit purely internal changes (CI, build scripts, internal tooling).
- However, DO include refactors that touch user-facing code - refactors can introduce bugs or change behavior.
- Do NOT make general statements about "improvements", be very specific about what was changed.
- For commits that are already well-written and descriptive, avoid rewording them. Simply capitalize the first letter, fix any misspellings, and ensure proper English grammar.
- DO NOT read any other commits than the ones listed above (THIS IS IMPORTANT TO AVOID DUPLICATING THINGS IN OUR CHANGELOG).
- If a commit was made and then reverted do not include it in the changelog. If the commits only include a revert but not the original commit, then include the revert in the changelog.
- Omit categories that have no changes.
- For community contributors: if the [author: username] is NOT in the team list, add (@username) at the end of the changelog entry. This is REQUIRED for all non-team contributors.
- The team members are: ${team.join(", ")}. Do NOT add @ mentions for team members.
IMPORTANT: ONLY return the grouped changelog, do not include any other information. Do not include a preamble like "Based on my analysis..." or "Here is the changelog..."
<example>
## TUI
- Added experimental support for the Ty language server (@OpeOginni)
- Added /fork slash command for keyboard-friendly session forking (@ariane-emory)
- Increased retry attempts for failed requests
- Fixed model validation before executing slash commands (@devxoul)
## Desktop
- Added shell mode support
- Fixed prompt history navigation and optimistic prompt duplication
- Disabled pinch-to-zoom on Linux (@Brendonovich)
## Extensions
- Added OIDC_BASE_URL support for custom GitHub App installations (@elithrar)
</example>
`,
},
],
},
})
.then((x) => x.data?.parts?.find((y) => y.type === "text")?.text)
opencode.server.close()
return raw
}
const timeout = new Promise<null>((resolve) => setTimeout(() => resolve(null), 120_000))
const raw = await Promise.race([generateChangelog(), timeout])
if (raw) {
for (const line of raw.split("\n")) {
if (line.startsWith("- ")) {
notes.push(line)
}
}
console.log("---- Generated Changelog ----")
console.log(notes.join("\n"))
console.log("-----------------------------")
} else {
console.log("Changelog generation timed out, using raw commits")
for (const commit of commits) {
const message = commit.replace(/^\w+ /, "")
notes.push(`- ${message}`)
}
}
const compare =
await $`gh api "/repos/sst/opencode/compare/v${previous}...HEAD" --jq '.commits[] | {login: .author.login, message: .commit.message}'`.text()
const contributors = new Map<string, string[]>()
for (const line of compare.split("\n").filter(Boolean)) {
const { login, message } = JSON.parse(line) as { login: string | null; message: string }
const title = message.split("\n")[0] ?? ""
if (title.match(/^(ignore:|test:|chore:|ci:|release:)/i)) continue
if (login && !team.includes(login)) {
if (!contributors.has(login)) contributors.set(login, [])
contributors.get(login)?.push(title)
}
}
if (contributors.size > 0) {
notes.push("")
notes.push(`**Thank you to ${contributors.size} community contributor${contributors.size > 1 ? "s" : ""}:**`)
for (const [username, userCommits] of contributors) {
notes.push(`- @${username}:`)
for (const commit of userCommits) {
notes.push(` - ${commit}`)
}
}
}
const previous = await getLatestRelease()
notes = await buildNotes(previous, "HEAD")
}
const pkgjsons = await Array.fromAsync(
+1 -1
View File
@@ -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",