Compare commits

..
Author SHA1 Message Date
Aiden Cline 106eeb8536 tweak
test / test (push) Has been cancelled
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
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 MorpainGitHubAiden Clinegithub-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
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 AdamsandGitHub ae67f43ff0 feat: add support for .claude/skills directory (#6252) 2025-12-28 22:39:10 -06:00
opencode 76880dce0d release: v1.0.207
Update Nix Hashes / update (push) Has been cancelled
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 PolitoandGitHub 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
0c19b71f42 docs: add plugin configuration documentation (#6301)
Co-authored-by: OpenCode <opencode@example.com>
2025-12-28 13:13:11 -06:00
Mohak SandGitHub 70fa66397e docs: add opencode-notifier plugin to ecosystem (#6283) 2025-12-28 13:09:38 -06:00
Daniel PolitoandGitHub 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 DurandandGitHub 29d8557d41 doc: fix typos in various files (#6294) 2025-12-28 13:06:25 -06:00
Didier Durandandopencode ffd20b4477 chore: activate code coverage in bun test config (#6297) 2025-12-28 19:05:55 +00:00
opencode 2abaa46e23 release: v1.0.206
Update Nix Hashes / update (push) Has been cancelled
2025-12-28 19:05:54 +00:00
GitHub Action 0cbbb20d22 chore: generate
publish-vscode / publish (push) Has been cancelled
Update Nix Hashes / update (push) Has been cancelled
2025-12-28 18:54:55 +00:00
Frank 81c5e7b9ed wip: benchmark 2025-12-28 13:54:11 -05:00
opencode ddf4897eaa release: v1.0.205
Update Nix Hashes / update (push) Has been cancelled
2025-12-28 18:37:58 +00:00
Adam 040939fb72 chore: cleanup theme stuff 2025-12-28 10:21:32 -06:00
Adam f89b83a6d7 chore: cleanup theme stuff 2025-12-28 10:14:30 -06:00
Adam 82a876da4d chore: cleanup 2025-12-28 06:41:59 -06:00
GitHub Action 69a15ae9c1 ignore: update download stats 2025-12-28 2025-12-28 12:04:31 +00:00
Adam 18c8e5f451 chore: cleanup 2025-12-28 05:47:22 -06:00
Adam ba3a1cfa0b chore: cleanup 2025-12-28 05:47:21 -06:00
Github Action d8563160f7 Update Nix flake.lock and hashes 2025-12-28 11:13:54 +00:00
Adam 4a9ff9412e feat(desktop): themes 2025-12-28 05:12:36 -06:00
Matt SilverlockandGitHub d6db6ff198 fix: handle non-text response parts in GitHub action (#6173) 2025-12-27 21:24:10 -06:00
Aiden Cline 79c263494f tweak: inform agent if no skills are available 2025-12-27 21:20:00 -06:00
Adam 1b5bf32ce5 chore: permissions ux 2025-12-27 20:40:25 -06:00
Adam 2e972b3fdc fix(desktop): copy/paste in terminal 2025-12-27 20:18:59 -06:00
Adam d70e9fb01e chore(desktop): cleanup 2025-12-27 19:59:16 -06:00
Adam fc082a0f14 fix(desktop): drag file over entire body to attach 2025-12-27 19:49:35 -06:00
Adam 953e4e9446 chore(desktop): vertical tabs 2025-12-27 19:43:52 -06:00
7ea0d37ee3 Thinking & tool call visibility settings for /copy and /export (#6243)
Co-authored-by: Aiden Cline <aidenpcline@gmail.com>
2025-12-27 19:32:33 -06:00
scarfandGitHub e35d97f9d7 feat: add bash shell completions (#6239) 2025-12-27 19:14:56 -06:00
GitHub Action 2c0d9a46cb chore: generate 2025-12-28 01:12:02 +00:00
2fe7a7f2d3 docs: document attach command (#6254)
Co-authored-by: Black_Fox <radekliska@gmail.com>
2025-12-27 19:11:30 -06:00
Connor AdamsandGitHub 8a2f4ddf70 chore: update INVALID_DIRS to include plural 'skills' directory (#6255) 2025-12-27 19:10:51 -06:00
processtraderGitHubopencode-agent[bot] <opencode-agent[bot]@users.noreply.github.com>rekram1-node
7a94d7a2c5 fix: stats command to correctly handle --days 0 for current day statistics (#6259)
Co-authored-by: opencode-agent[bot] <opencode-agent[bot]@users.noreply.github.com>
Co-authored-by: rekram1-node <rekram1-node@users.noreply.github.com>
2025-12-27 19:10:23 -06:00
Aiden Cline de28fafb47 fix: search all recent models instead of only top 5 in TUI /models command 2025-12-27 19:07:38 -06:00
9d485dd307 docs: add opencode-notificator to ecosystem plugins list (#6269)
Co-authored-by: Ivan Pantic <panta@talentkit.io>
Co-authored-by: Aiden Cline <63023139+rekram1-node@users.noreply.github.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-12-27 18:54:27 -06:00
GitHub Action 613813ac12 chore: generate 2025-12-28 00:53:48 +00:00
ewiredandGitHub 7617f59441 Allow line numbers and ranges in autocomplete (#4238) 2025-12-27 18:53:17 -06:00
92 changed files with 6121 additions and 966 deletions
+1
View File
@@ -183,3 +183,4 @@
| 2025-12-25 | 1,333,032 (+23,709) | 1,217,283 (+13,516) | 2,550,315 (+37,225) |
| 2025-12-26 | 1,352,411 (+19,379) | 1,227,615 (+10,332) | 2,580,026 (+29,711) |
| 2025-12-27 | 1,371,771 (+19,360) | 1,238,236 (+10,621) | 2,610,007 (+29,981) |
| 2025-12-28 | 1,390,388 (+18,617) | 1,245,690 (+7,454) | 2,636,078 (+26,071) |
+15 -15
View File
@@ -22,7 +22,7 @@
},
"packages/app": {
"name": "@opencode-ai/app",
"version": "1.0.204",
"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.204",
"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.204",
"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.204",
"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.204",
"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.204",
"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.204",
"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.204",
"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.204",
"version": "1.0.207",
"bin": {
"opencode": "./bin/opencode",
},
@@ -347,7 +347,7 @@
},
"packages/plugin": {
"name": "@opencode-ai/plugin",
"version": "1.0.204",
"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.204",
"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.204",
"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.204",
"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.204",
"version": "1.0.207",
"dependencies": {
"zod": "catalog:",
},
@@ -437,7 +437,7 @@
},
"packages/web": {
"name": "@opencode-ai/web",
"version": "1.0.204",
"version": "1.0.207",
"dependencies": {
"@astrojs/cloudflare": "12.6.3",
"@astrojs/markdown-remark": "6.3.1",
Generated
+3 -3
View File
@@ -2,11 +2,11 @@
"nodes": {
"nixpkgs": {
"locked": {
"lastModified": 1766747458,
"narHash": "sha256-m63jjuo/ygo8ztkCziYh5OOIbTSXUDkKbqw3Vuqu4a4=",
"lastModified": 1766840161,
"narHash": "sha256-Ss/LHpJJsng8vz1Pe33RSGIWUOcqM1fjrehjUkdrWio=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "c633f572eded8c4f3c75b8010129854ed404a6ce",
"rev": "3edc4a30ed3903fdf6f90c837f961fa6b49582d1",
"type": "github"
},
"original": {
+31 -6
View File
@@ -1,5 +1,5 @@
<!doctype html>
<html lang="en">
<html lang="en" style="background-color: var(--background-base)">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
@@ -13,14 +13,39 @@
<meta name="theme-color" content="#131010" media="(prefers-color-scheme: dark)" />
<meta property="og:image" content="/social-share.png" />
<meta property="twitter:image" content="/social-share.png" />
</head>
<body class="antialiased overscroll-none text-12-regular overflow-hidden">
<script>
<!-- Theme preload script - applies cached theme to avoid FOUC -->
<script id="oc-theme-preload-script">
;(function () {
const savedTheme = localStorage.getItem("theme") || "oc-1"
document.documentElement.setAttribute("data-theme", savedTheme)
var themeId = localStorage.getItem("opencode-theme-id")
if (!themeId) return
var scheme = localStorage.getItem("opencode-color-scheme") || "system"
var isDark = scheme === "dark" || (scheme === "system" && matchMedia("(prefers-color-scheme: dark)").matches)
var mode = isDark ? "dark" : "light"
document.documentElement.dataset.theme = themeId
document.documentElement.dataset.colorScheme = mode
if (themeId === "oc-1") return
var css = localStorage.getItem("opencode-theme-css-" + themeId + "-" + mode)
if (css) {
var style = document.createElement("style")
style.id = "oc-theme-preload"
style.textContent =
":root{color-scheme:" +
mode +
";--text-mix-blend-mode:" +
(isDark ? "plus-lighter" : "multiply") +
";" +
css +
"}"
document.head.appendChild(style)
}
})()
</script>
</head>
<body class="antialiased overscroll-none text-12-regular overflow-hidden">
<noscript>You need to enable JavaScript to run this app.</noscript>
<div id="root" class="flex flex-col h-screen"></div>
<script src="/src/entry.tsx" type="module"></script>
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@opencode-ai/app",
"version": "1.0.204",
"version": "1.0.207",
"description": "",
"type": "module",
"exports": {
+45 -42
View File
@@ -8,6 +8,7 @@ import { DiffComponentProvider } from "@opencode-ai/ui/context/diff"
import { CodeComponentProvider } from "@opencode-ai/ui/context/code"
import { Diff } from "@opencode-ai/ui/diff"
import { Code } from "@opencode-ai/ui/code"
import { ThemeProvider } from "@opencode-ai/ui/theme"
import { GlobalSyncProvider } from "@/context/global-sync"
import { LayoutProvider } from "@/context/layout"
import { GlobalSDKProvider } from "@/context/global-sdk"
@@ -45,48 +46,50 @@ export function App() {
return (
<MetaProvider>
<Font />
<ErrorBoundary fallback={(error) => <ErrorPage error={error} />}>
<DialogProvider>
<MarkedProvider>
<DiffComponentProvider component={Diff}>
<CodeComponentProvider component={Code}>
<GlobalSDKProvider url={url}>
<GlobalSyncProvider>
<LayoutProvider>
<NotificationProvider>
<Router
root={(props) => (
<CommandProvider>
<Layout>{props.children}</Layout>
</CommandProvider>
)}
>
<Route path="/" component={Home} />
<Route path="/:dir" component={DirectoryLayout}>
<Route path="/" component={() => <Navigate href="session" />} />
<Route
path="/session/:id?"
component={(p) => (
<Show when={p.params.id || true} keyed>
<TerminalProvider>
<PromptProvider>
<Session />
</PromptProvider>
</TerminalProvider>
</Show>
)}
/>
</Route>
</Router>
</NotificationProvider>
</LayoutProvider>
</GlobalSyncProvider>
</GlobalSDKProvider>
</CodeComponentProvider>
</DiffComponentProvider>
</MarkedProvider>
</DialogProvider>
</ErrorBoundary>
<ThemeProvider>
<ErrorBoundary fallback={(error) => <ErrorPage error={error} />}>
<DialogProvider>
<MarkedProvider>
<DiffComponentProvider component={Diff}>
<CodeComponentProvider component={Code}>
<GlobalSDKProvider url={url}>
<GlobalSyncProvider>
<LayoutProvider>
<NotificationProvider>
<Router
root={(props) => (
<CommandProvider>
<Layout>{props.children}</Layout>
</CommandProvider>
)}
>
<Route path="/" component={Home} />
<Route path="/:dir" component={DirectoryLayout}>
<Route path="/" component={() => <Navigate href="session" />} />
<Route
path="/session/:id?"
component={(p) => (
<Show when={p.params.id || true} keyed>
<TerminalProvider>
<PromptProvider>
<Session />
</PromptProvider>
</TerminalProvider>
</Show>
)}
/>
</Route>
</Router>
</NotificationProvider>
</LayoutProvider>
</GlobalSyncProvider>
</GlobalSDKProvider>
</CodeComponentProvider>
</DiffComponentProvider>
</MarkedProvider>
</DialogProvider>
</ErrorBoundary>
</ThemeProvider>
</MetaProvider>
)
}
+12 -9
View File
@@ -248,6 +248,7 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
}
const handlePaste = async (event: ClipboardEvent) => {
if (!isFocused()) return
const clipboardData = event.clipboardData
if (!clipboardData) return
@@ -270,7 +271,7 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
addPart({ type: "text", content: plainText, start: 0, end: 0 })
}
const handleDragOver = (event: DragEvent) => {
const handleGlobalDragOver = (event: DragEvent) => {
event.preventDefault()
const hasFiles = event.dataTransfer?.types.includes("Files")
if (hasFiles) {
@@ -278,15 +279,14 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
}
}
const handleDragLeave = (event: DragEvent) => {
const related = event.relatedTarget as Node | null
const form = event.currentTarget as HTMLElement
if (!related || !form.contains(related)) {
const handleGlobalDragLeave = (event: DragEvent) => {
// relatedTarget is null when leaving the document window
if (!event.relatedTarget) {
setStore("dragging", false)
}
}
const handleDrop = async (event: DragEvent) => {
const handleGlobalDrop = async (event: DragEvent) => {
event.preventDefault()
setStore("dragging", false)
@@ -302,9 +302,15 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
onMount(() => {
editorRef.addEventListener("paste", handlePaste)
document.addEventListener("dragover", handleGlobalDragOver)
document.addEventListener("dragleave", handleGlobalDragLeave)
document.addEventListener("drop", handleGlobalDrop)
})
onCleanup(() => {
editorRef.removeEventListener("paste", handlePaste)
document.removeEventListener("dragover", handleGlobalDragOver)
document.removeEventListener("dragleave", handleGlobalDragLeave)
document.removeEventListener("drop", handleGlobalDrop)
})
createEffect(() => {
@@ -1024,9 +1030,6 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
</Show>
<form
onSubmit={handleSubmit}
onDragOver={handleDragOver}
onDragLeave={handleDragLeave}
onDrop={handleDrop}
classList={{
"bg-surface-raised-stronger-non-alpha shadow-xs-border relative": true,
"rounded-md overflow-clip focus-within:shadow-xs-border": true,
+95 -17
View File
@@ -1,9 +1,9 @@
import { Ghostty, Terminal as Term, FitAddon } from "ghostty-web"
import { ComponentProps, onCleanup, onMount, splitProps } from "solid-js"
import { ComponentProps, createEffect, createSignal, onCleanup, onMount, splitProps } from "solid-js"
import { useSDK } from "@/context/sdk"
import { SerializeAddon } from "@/addons/serialize"
import { LocalPTY } from "@/context/terminal"
import { usePrefersDark } from "@solid-primitives/media"
import { resolveThemeVariant, useTheme } from "@opencode-ai/ui/theme"
export interface TerminalProps extends ComponentProps<"div"> {
pty: LocalPTY
@@ -12,8 +12,28 @@ export interface TerminalProps extends ComponentProps<"div"> {
onConnectError?: (error: unknown) => void
}
type TerminalColors = {
background: string
foreground: string
cursor: string
}
const DEFAULT_TERMINAL_COLORS: Record<"light" | "dark", TerminalColors> = {
light: {
background: "#fcfcfc",
foreground: "#211e1e",
cursor: "#211e1e",
},
dark: {
background: "#191515",
foreground: "#d4d4d4",
cursor: "#d4d4d4",
},
}
export const Terminal = (props: TerminalProps) => {
const sdk = useSDK()
const theme = useTheme()
let container!: HTMLDivElement
const [local, others] = splitProps(props, ["pty", "class", "classList", "onConnectError"])
let ws: WebSocket
@@ -22,7 +42,64 @@ export const Terminal = (props: TerminalProps) => {
let serializeAddon: SerializeAddon
let fitAddon: FitAddon
let handleResize: () => void
const prefersDark = usePrefersDark()
const getTerminalColors = (): TerminalColors => {
const mode = theme.mode()
const fallback = DEFAULT_TERMINAL_COLORS[mode]
const currentTheme = theme.themes()[theme.themeId()]
if (!currentTheme) return fallback
const variant = mode === "dark" ? currentTheme.dark : currentTheme.light
if (!variant?.seeds) return fallback
const resolved = resolveThemeVariant(variant, mode === "dark")
const text = resolved["text-base"] ?? fallback.foreground
const background = resolved["background-stronger"] ?? fallback.background
return {
background,
foreground: text,
cursor: text,
}
}
const [terminalColors, setTerminalColors] = createSignal<TerminalColors>(getTerminalColors())
createEffect(() => {
const colors = getTerminalColors()
setTerminalColors(colors)
if (!term) return
const setOption = (term as unknown as { setOption?: (key: string, value: TerminalColors) => void }).setOption
if (!setOption) return
setOption("theme", colors)
})
const focusTerminal = () => term?.focus()
const copySelection = () => {
if (!term || !term.hasSelection()) return false
const selection = term.getSelection()
if (!selection) return false
const clipboard = navigator.clipboard
if (clipboard?.writeText) {
clipboard.writeText(selection).catch(() => {})
return true
}
if (!document.body) return false
const textarea = document.createElement("textarea")
textarea.value = selection
textarea.setAttribute("readonly", "")
textarea.style.position = "fixed"
textarea.style.opacity = "0"
document.body.appendChild(textarea)
textarea.select()
const copied = document.execCommand("copy")
document.body.removeChild(textarea)
return copied
}
const handlePointerDown = () => {
const activeElement = document.activeElement
if (activeElement instanceof HTMLElement && activeElement !== container) {
activeElement.blur()
}
focusTerminal()
}
onMount(async () => {
ghostty = await Ghostty.load()
@@ -33,23 +110,22 @@ export const Terminal = (props: TerminalProps) => {
fontSize: 14,
fontFamily: "IBM Plex Mono, monospace",
allowTransparency: true,
theme: prefersDark()
? {
background: "#191515",
foreground: "#d4d4d4",
cursor: "#d4d4d4",
}
: {
background: "#fcfcfc",
foreground: "#211e1e",
cursor: "#211e1e",
},
theme: terminalColors(),
scrollback: 10_000,
ghostty,
})
term.attachCustomKeyEventHandler((event) => {
const key = event.key.toLowerCase()
if (key === "c") {
const macCopy = event.metaKey && !event.ctrlKey && !event.altKey
const linuxCopy = event.ctrlKey && event.shiftKey && !event.metaKey
if ((macCopy || linuxCopy) && copySelection()) {
event.preventDefault()
return true
}
}
// allow for ctrl-` to toggle terminal in parent
if (event.ctrlKey && event.key.toLowerCase() === "`") {
if (event.ctrlKey && key === "`") {
event.preventDefault()
return true
}
@@ -62,6 +138,8 @@ export const Terminal = (props: TerminalProps) => {
term.loadAddon(fitAddon)
term.open(container)
container.addEventListener("pointerdown", handlePointerDown)
focusTerminal()
if (local.pty.buffer) {
if (local.pty.rows && local.pty.cols) {
@@ -75,8 +153,6 @@ export const Terminal = (props: TerminalProps) => {
fitAddon.fit()
}
container.focus()
fitAddon.observeResize()
handleResize = () => fitAddon.fit()
window.addEventListener("resize", handleResize)
@@ -134,6 +210,7 @@ export const Terminal = (props: TerminalProps) => {
if (handleResize) {
window.removeEventListener("resize", handleResize)
}
container.removeEventListener("pointerdown", handlePointerDown)
if (serializeAddon && props.onCleanup) {
const buffer = serializeAddon.serialize()
props.onCleanup({
@@ -153,6 +230,7 @@ export const Terminal = (props: TerminalProps) => {
ref={container}
data-component="terminal"
data-prevent-autofocus
style={{ "background-color": terminalColors().background }}
classList={{
...(local.classList ?? {}),
"size-full px-6 py-3 font-mono": true,
+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 -13
View File
@@ -82,7 +82,6 @@ function createGlobalSync() {
})
const children: Record<string, ReturnType<typeof createStore<State>>> = {}
const permissionListeners: Set<(info: { directory: string; permission: Permission }) => void> = new Set()
function child(directory: string) {
if (!directory) console.error("No directory provided")
if (!children[directory]) {
@@ -330,7 +329,6 @@ function createGlobalSync() {
}
case "permission.updated": {
const permissions = store.permission[event.properties.sessionID]
const isNew = !permissions || !permissions.find((p) => p.id === event.properties.id)
if (!permissions) {
setStore("permission", event.properties.sessionID, [event.properties])
} else {
@@ -347,11 +345,6 @@ function createGlobalSync() {
}),
)
}
if (isNew) {
for (const listener of permissionListeners) {
listener({ directory, permission: event.properties })
}
}
break
}
case "permission.replied": {
@@ -368,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
}
}
})
@@ -439,12 +441,6 @@ function createGlobalSync() {
project: {
loadSessions,
},
permission: {
onUpdated(listener: (info: { directory: string; permission: Permission }) => void) {
permissionListeners.add(listener)
return () => permissionListeners.delete(listener)
},
},
}
}
+168 -55
View File
@@ -41,14 +41,15 @@ import {
} from "@thisbeyond/solid-dnd"
import type { DragEvent } from "@thisbeyond/solid-dnd"
import { useProviders } from "@/hooks/use-providers"
import { showToast, Toast } from "@opencode-ai/ui/toast"
import { showToast, Toast, toaster } from "@opencode-ai/ui/toast"
import { useGlobalSDK } from "@/context/global-sdk"
import { useNotification } from "@/context/notification"
import { Binary } from "@opencode-ai/util/binary"
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) {
@@ -89,6 +90,41 @@ export default function Layout(props: ParentProps) {
const providers = useProviders()
const dialog = useDialog()
const command = useCommand()
const theme = useTheme()
const availableThemeEntries = createMemo(() => Object.entries(theme.themes()))
const colorSchemeOrder: ColorScheme[] = ["system", "light", "dark"]
const colorSchemeLabel: Record<ColorScheme, string> = {
system: "System",
light: "Light",
dark: "Dark",
}
function cycleTheme(direction = 1) {
const ids = availableThemeEntries().map(([id]) => id)
if (ids.length === 0) return
const currentIndex = ids.indexOf(theme.themeId())
const nextIndex = currentIndex === -1 ? 0 : (currentIndex + direction + ids.length) % ids.length
const nextThemeId = ids[nextIndex]
theme.setTheme(nextThemeId)
const nextTheme = theme.themes()[nextThemeId]
showToast({
title: "Theme switched",
description: nextTheme?.name ?? nextThemeId,
})
}
function cycleColorScheme(direction = 1) {
const current = theme.colorScheme()
const currentIndex = colorSchemeOrder.indexOf(current)
const nextIndex =
currentIndex === -1 ? 0 : (currentIndex + direction + colorSchemeOrder.length) % colorSchemeOrder.length
const next = colorSchemeOrder[nextIndex]
theme.setColorScheme(next)
showToast({
title: "Color scheme",
description: colorSchemeLabel[next],
})
}
onMount(async () => {
if (platform.checkUpdate && platform.update && platform.restart) {
@@ -118,7 +154,15 @@ export default function Layout(props: ParentProps) {
})
onMount(() => {
const unsub = globalSync.permission.onUpdated(({ directory, permission }) => {
const seenSessions = new Set<string>()
const toastBySession = new Map<string, number>()
const unsub = globalSDK.event.listen((e) => {
if (e.details?.type !== "permission.updated") return
const directory = e.name
const permission = e.details.properties
const sessionKey = `${directory}:${permission.sessionID}`
if (seenSessions.has(sessionKey)) return
seenSessions.add(sessionKey)
const currentDir = params.dir ? base64Decode(params.dir) : undefined
const currentSession = params.id
if (directory === currentDir && permission.sessionID === currentSession) return
@@ -127,7 +171,7 @@ export default function Layout(props: ParentProps) {
if (directory === currentDir && session?.parentID === currentSession) return
const sessionTitle = session?.title ?? "New session"
const projectName = getFilename(directory)
showToast({
const toastId = showToast({
persistent: true,
icon: "checklist",
title: "Permission required",
@@ -138,7 +182,6 @@ export default function Layout(props: ParentProps) {
onClick: () => {
navigate(`/${base64Encode(directory)}/session/${permission.sessionID}`)
},
dismissAfter: true,
},
{
label: "Dismiss",
@@ -146,8 +189,33 @@ export default function Layout(props: ParentProps) {
},
],
})
toastBySession.set(sessionKey, toastId)
})
onCleanup(unsub)
createEffect(() => {
const currentDir = params.dir ? base64Decode(params.dir) : undefined
const currentSession = params.id
if (!currentDir || !currentSession) return
const sessionKey = `${currentDir}:${currentSession}`
const toastId = toastBySession.get(sessionKey)
if (toastId !== undefined) {
toaster.dismiss(toastId)
toastBySession.delete(sessionKey)
seenSessions.delete(sessionKey)
}
const [store] = globalSync.child(currentDir)
const childSessions = store.session.filter((s) => s.parentID === currentSession)
for (const child of childSessions) {
const childKey = `${currentDir}:${child.id}`
const childToastId = toastBySession.get(childKey)
if (childToastId !== undefined) {
toaster.dismiss(childToastId)
toastBySession.delete(childKey)
seenSessions.delete(childKey)
}
}
})
})
function sortSessions(a: Session, b: Session) {
@@ -254,57 +322,102 @@ export default function Layout(props: ParentProps) {
}
}
command.register(() => [
{
id: "sidebar.toggle",
title: "Toggle sidebar",
category: "View",
keybind: "mod+b",
onSelect: () => layout.sidebar.toggle(),
},
...(platform.openDirectoryPickerDialog
? [
{
id: "project.open",
title: "Open project",
category: "Project",
keybind: "mod+o",
onSelect: () => chooseProject(),
},
]
: []),
{
id: "provider.connect",
title: "Connect provider",
category: "Provider",
onSelect: () => connectProvider(),
},
{
id: "session.previous",
title: "Previous session",
category: "Session",
keybind: "alt+arrowup",
onSelect: () => navigateSessionByOffset(-1),
},
{
id: "session.next",
title: "Next session",
category: "Session",
keybind: "alt+arrowdown",
onSelect: () => navigateSessionByOffset(1),
},
{
id: "session.archive",
title: "Archive session",
category: "Session",
keybind: "mod+shift+backspace",
disabled: !params.dir || !params.id,
onSelect: () => {
const session = currentSessions().find((s) => s.id === params.id)
if (session) archiveSession(session)
command.register(() => {
const commands: CommandOption[] = [
{
id: "sidebar.toggle",
title: "Toggle sidebar",
category: "View",
keybind: "mod+b",
onSelect: () => layout.sidebar.toggle(),
},
},
])
...(platform.openDirectoryPickerDialog
? [
{
id: "project.open",
title: "Open project",
category: "Project",
keybind: "mod+o",
onSelect: () => chooseProject(),
},
]
: []),
{
id: "provider.connect",
title: "Connect provider",
category: "Provider",
onSelect: () => connectProvider(),
},
{
id: "session.previous",
title: "Previous session",
category: "Session",
keybind: "alt+arrowup",
onSelect: () => navigateSessionByOffset(-1),
},
{
id: "session.next",
title: "Next session",
category: "Session",
keybind: "alt+arrowdown",
onSelect: () => navigateSessionByOffset(1),
},
{
id: "session.archive",
title: "Archive session",
category: "Session",
keybind: "mod+shift+backspace",
disabled: !params.dir || !params.id,
onSelect: () => {
const session = currentSessions().find((s) => s.id === params.id)
if (session) archiveSession(session)
},
},
{
id: "theme.cycle",
title: "Cycle theme",
category: "Theme",
keybind: "mod+shift+t",
onSelect: () => cycleTheme(1),
},
]
for (const [id, definition] of availableThemeEntries()) {
commands.push({
id: `theme.set.${id}`,
title: `Use theme: ${definition.name ?? id}`,
category: "Theme",
onSelect: () => theme.commitPreview(),
onHighlight: () => {
theme.previewTheme(id)
return () => theme.cancelPreview()
},
})
}
commands.push({
id: "theme.scheme.cycle",
title: "Cycle color scheme",
category: "Theme",
keybind: "mod+shift+s",
onSelect: () => cycleColorScheme(1),
})
for (const scheme of colorSchemeOrder) {
commands.push({
id: `theme.scheme.${scheme}`,
title: `Use color scheme: ${colorSchemeLabel[scheme]}`,
category: "Theme",
onSelect: () => theme.commitPreview(),
onHighlight: () => {
theme.previewColorScheme(scheme)
return () => theme.cancelPreview()
},
})
}
return commands
})
function connectProvider() {
dialog.show(() => <DialogSelectProvider />)
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@opencode-ai/console-app",
"version": "1.0.204",
"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>
)
}
@@ -0,0 +1,86 @@
import { Title } from "@solidjs/meta"
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 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,
}
})
}
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" style={{ padding: "2rem" }}>
<Title>Benchmark</Title>
<h1 style={{ "margin-bottom": "1.5rem" }}>Benchmarks</h1>
<table style={{ "border-collapse": "collapse", width: "100%" }}>
<thead>
<tr>
<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 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>
</tbody>
</table>
</main>
)
}
@@ -0,0 +1,29 @@
import type { APIEvent } from "@solidjs/start/server"
import { Database } from "@opencode-ai/console-core/drizzle/index.js"
import { BenchmarkTable } from "@opencode-ai/console-core/schema/benchmark.sql.js"
import { Identifier } from "@opencode-ai/console-core/identifier.js"
interface SubmissionBody {
model: string
agent: string
result: string
}
export async function POST(event: APIEvent) {
const body = (await event.request.json()) as SubmissionBody
if (!body.model || !body.agent || !body.result) {
return Response.json({ error: "All fields are required" }, { status: 400 })
}
await Database.use((tx) =>
tx.insert(BenchmarkTable).values({
id: Identifier.create("benchmark"),
model: body.model,
agent: body.agent,
result: body.result,
}),
)
return Response.json({ success: true }, { status: 200 })
}
@@ -0,0 +1,12 @@
CREATE TABLE `benchmark` (
`id` varchar(30) NOT NULL,
`time_created` timestamp(3) NOT NULL DEFAULT (now()),
`time_updated` timestamp(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3),
`time_deleted` timestamp(3),
`model` varchar(64) NOT NULL,
`agent` varchar(64) NOT NULL,
`result` mediumtext NOT NULL,
CONSTRAINT `benchmark_id_pk` PRIMARY KEY(`id`)
);
--> statement-breakpoint
CREATE INDEX `time_created` ON `benchmark` (`time_created`);
File diff suppressed because it is too large Load Diff
@@ -274,6 +274,13 @@
"when": 1764110043942,
"tag": "0038_famous_magik",
"breakpoints": true
},
{
"idx": 39,
"version": "5",
"when": 1766946179892,
"tag": "0039_striped_forge",
"breakpoints": true
}
]
}
+1 -1
View File
@@ -1,7 +1,7 @@
{
"$schema": "https://json.schemastore.org/package.json",
"name": "@opencode-ai/console-core",
"version": "1.0.204",
"version": "1.0.207",
"private": true,
"type": "module",
"dependencies": {
+1
View File
@@ -5,6 +5,7 @@ export namespace Identifier {
const prefixes = {
account: "acc",
auth: "aut",
benchmark: "ben",
billing: "bil",
key: "key",
model: "mod",
@@ -0,0 +1,14 @@
import { index, mediumtext, mysqlTable, primaryKey, varchar } from "drizzle-orm/mysql-core"
import { id, timestamps } from "../drizzle/types"
export const BenchmarkTable = mysqlTable(
"benchmark",
{
id: id(),
...timestamps,
model: varchar("model", { length: 64 }).notNull(),
agent: varchar("agent", { length: 64 }).notNull(),
result: mediumtext("result").notNull(),
},
(table) => [primaryKey({ columns: [table.id] }), index("time_created").on(table.timeCreated)],
)
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@opencode-ai/console-function",
"version": "1.0.204",
"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.204",
"version": "1.0.207",
"dependencies": {
"@jsx-email/all": "2.2.3",
"@jsx-email/cli": "1.4.3",
+31 -6
View File
@@ -1,5 +1,5 @@
<!doctype html>
<html lang="en">
<html lang="en" style="background-color: var(--background-base)">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
@@ -13,14 +13,39 @@
<meta name="theme-color" content="#131010" media="(prefers-color-scheme: dark)" />
<meta property="og:image" content="/social-share.png" />
<meta property="twitter:image" content="/social-share.png" />
</head>
<body class="antialiased overscroll-none text-12-regular overflow-hidden">
<script>
<!-- Theme preload script - applies cached theme to avoid FOUC -->
<script id="oc-theme-preload-script">
;(function () {
const savedTheme = localStorage.getItem("theme") || "oc-1"
document.documentElement.setAttribute("data-theme", savedTheme)
var themeId = localStorage.getItem("opencode-theme-id")
if (!themeId) return
var scheme = localStorage.getItem("opencode-color-scheme") || "system"
var isDark = scheme === "dark" || (scheme === "system" && matchMedia("(prefers-color-scheme: dark)").matches)
var mode = isDark ? "dark" : "light"
document.documentElement.dataset.theme = themeId
document.documentElement.dataset.colorScheme = mode
if (themeId === "oc-1") return
var css = localStorage.getItem("opencode-theme-css-" + themeId + "-" + mode)
if (css) {
var style = document.createElement("style")
style.id = "oc-theme-preload"
style.textContent =
":root{color-scheme:" +
mode +
";--text-mix-blend-mode:" +
(isDark ? "plus-lighter" : "multiply") +
";" +
css +
"}"
document.head.appendChild(style)
}
})()
</script>
</head>
<body class="antialiased overscroll-none text-12-regular overflow-hidden">
<noscript>You need to enable JavaScript to run this app.</noscript>
<div id="root" class="flex flex-col h-screen"></div>
<script src="/src/index.tsx" type="module"></script>
+1 -1
View File
@@ -1,7 +1,7 @@
{
"name": "@opencode-ai/desktop",
"private": true,
"version": "1.0.204",
"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.204",
"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.204"
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.204/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.204/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.204/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.204/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.204/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.204",
"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.204",
"version": "1.0.207",
"name": "opencode",
"type": "module",
"private": true,
+57 -3
View File
@@ -158,6 +158,29 @@ export function parseGitHubRemote(url: string): { owner: string; repo: string }
return { owner: match[1], repo: match[2] }
}
/**
* Extracts displayable text from assistant response parts.
* Returns null for tool-only or reasoning-only responses (signals summary needed).
* Throws for truly unusable responses (empty, step-start only, etc.).
*/
export function extractResponseText(parts: MessageV2.Part[]): string | null {
// Priority 1: Look for text parts
const textPart = parts.findLast((p) => p.type === "text")
if (textPart) return textPart.text
// Priority 2: Reasoning-only - return null to signal summary needed
const reasoningPart = parts.findLast((p) => p.type === "reasoning")
if (reasoningPart) return null
// Priority 3: Tool-only - return null to signal summary needed
const toolParts = parts.filter((p) => p.type === "tool" && p.state.status === "completed")
if (toolParts.length > 0) return null
// No usable parts - throw with debug info
const partTypes = parts.map((p) => p.type).join(", ") || "none"
throw new Error(`Failed to parse response. Part types found: [${partTypes}]`)
}
export const GithubCommand = cmd({
command: "github",
describe: "manage GitHub agent",
@@ -890,10 +913,41 @@ export const GithubRunCommand = cmd({
)
}
const match = result.parts.findLast((p) => p.type === "text")
if (!match) throw new Error("Failed to parse the text response")
const text = extractResponseText(result.parts)
if (text) return text
return match.text
// No text part (tool-only or reasoning-only) - ask agent to summarize
console.log("Requesting summary from agent...")
const summary = await SessionPrompt.prompt({
sessionID: session.id,
messageID: Identifier.ascending("message"),
model: {
providerID,
modelID,
},
tools: { "*": false }, // Disable all tools to force text response
parts: [
{
id: Identifier.ascending("part"),
type: "text",
text: "Summarize the actions (tool calls & reasoning) you did for the user in 1-2 sentences.",
},
],
})
if (summary.info.role === "assistant" && summary.info.error) {
console.error(summary.info)
throw new Error(
`${summary.info.error.name}: ${"message" in summary.info.error ? summary.info.error.message : ""}`,
)
}
const summaryText = extractResponseText(summary.parts)
if (!summaryText) {
throw new Error("Failed to get summary from agent")
}
return summaryText
}
async function getOidcToken() {
+14 -5
View File
@@ -82,12 +82,21 @@ async function getAllSessions(): Promise<Session.Info[]> {
return sessions
}
async function aggregateSessionStats(days?: number, projectFilter?: string): Promise<SessionStats> {
export async function aggregateSessionStats(days?: number, projectFilter?: string): Promise<SessionStats> {
const sessions = await getAllSessions()
const DAYS_IN_SECOND = 24 * 60 * 60 * 1000
const cutoffTime = days ? Date.now() - days * DAYS_IN_SECOND : 0
const MS_IN_DAY = 24 * 60 * 60 * 1000
let filteredSessions = days ? sessions.filter((session) => session.time.updated >= cutoffTime) : sessions
const cutoffTime = (() => {
if (days === undefined) return 0
if (days === 0) {
const now = new Date()
now.setHours(0, 0, 0, 0)
return now.getTime()
}
return Date.now() - days * MS_IN_DAY
})()
let filteredSessions = cutoffTime > 0 ? sessions.filter((session) => session.time.updated >= cutoffTime) : sessions
if (projectFilter !== undefined) {
if (projectFilter === "") {
@@ -198,7 +207,7 @@ async function aggregateSessionStats(days?: number, projectFilter?: string): Pro
}
}
const actualDays = Math.max(1, Math.ceil((latestTime - earliestTime) / DAYS_IN_SECOND))
const actualDays = Math.max(1, Math.ceil((latestTime - earliestTime) / MS_IN_DAY))
stats.dateRange = {
earliest: earliestTime,
latest: latestTime,
@@ -37,11 +37,9 @@ export function DialogModel(props: { providerID?: string }) {
const recents = local.model.recent()
const recentList = showExtra()
? recents
.filter(
(item) => !favorites.some((fav) => fav.providerID === item.providerID && fav.modelID === item.modelID),
)
.slice(0, 5)
? recents.filter(
(item) => !favorites.some((fav) => fav.providerID === item.providerID && fav.modelID === item.modelID),
)
: []
const favoriteOptions = favorites.flatMap((item) => {
@@ -182,7 +180,10 @@ export function DialogModel(props: { providerID?: string }) {
// Apply fuzzy filtering to each section separately, maintaining section order
if (q) {
const filteredFavorites = fuzzysort.go(q, favoriteOptions, { keys: ["title"] }).map((x) => x.obj)
const filteredRecents = fuzzysort.go(q, recentOptions, { keys: ["title"] }).map((x) => x.obj)
const filteredRecents = fuzzysort
.go(q, recentOptions, { keys: ["title"] })
.map((x) => x.obj)
.slice(0, 5)
const filteredProviders = fuzzysort.go(q, providerOptions, { keys: ["title", "category"] }).map((x) => x.obj)
const filteredPopular = fuzzysort.go(q, popularProviders, { keys: ["title"] }).map((x) => x.obj)
return [...filteredFavorites, ...filteredRecents, ...filteredProviders, ...filteredPopular]
@@ -12,6 +12,38 @@ import { useTerminalDimensions } from "@opentui/solid"
import { Locale } from "@/util/locale"
import type { PromptInfo } from "./history"
function removeLineRange(input: string) {
const hashIndex = input.lastIndexOf("#")
return hashIndex !== -1 ? input.substring(0, hashIndex) : input
}
function extractLineRange(input: string) {
const hashIndex = input.lastIndexOf("#")
if (hashIndex === -1) {
return { baseQuery: input }
}
const baseName = input.substring(0, hashIndex)
const linePart = input.substring(hashIndex + 1)
const lineMatch = linePart.match(/^(\d+)(?:-(\d*))?$/)
if (!lineMatch) {
return { baseQuery: baseName }
}
const startLine = Number(lineMatch[1])
const endLine = lineMatch[2] && startLine < Number(lineMatch[2]) ? Number(lineMatch[2]) : undefined
return {
lineRange: {
baseName,
startLine,
endLine,
},
baseQuery: baseName,
}
}
export type AutocompleteRef = {
onInput: (value: string) => void
onKeyDown: (e: KeyEvent) => void
@@ -142,9 +174,11 @@ export function Autocomplete(props: {
async (query) => {
if (!store.visible || store.visible === "/") return []
const { lineRange, baseQuery } = extractLineRange(query ?? "")
// Get files from SDK
const result = await sdk.client.find.files({
query: query ?? "",
query: baseQuery,
})
const options: AutocompleteOption[] = []
@@ -153,15 +187,27 @@ export function Autocomplete(props: {
if (!result.error && result.data) {
const width = props.anchor().width - 4
options.push(
...result.data.map(
(item): AutocompleteOption => ({
display: Locale.truncateMiddle(item, width),
...result.data.map((item): AutocompleteOption => {
let url = `file://${process.cwd()}/${item}`
let filename = item
if (lineRange && !item.endsWith("/")) {
filename = `${item}#${lineRange.startLine}${lineRange.endLine ? `-${lineRange.endLine}` : ""}`
const urlObj = new URL(url)
urlObj.searchParams.set("start", String(lineRange.startLine))
if (lineRange.endLine !== undefined) {
urlObj.searchParams.set("end", String(lineRange.endLine))
}
url = urlObj.toString()
}
return {
display: Locale.truncateMiddle(filename, width),
onSelect: () => {
insertPart(item, {
insertPart(filename, {
type: "file",
mime: "text/plain",
filename: item,
url: `file://${process.cwd()}/${item}`,
filename,
url,
source: {
type: "file",
text: {
@@ -173,8 +219,8 @@ export function Autocomplete(props: {
},
})
},
}),
),
}
}),
)
}
@@ -383,8 +429,8 @@ export function Autocomplete(props: {
return prev
}
const result = fuzzysort.go(currentFilter, mixed, {
keys: [(obj) => obj.display.trimEnd(), "description", (obj) => obj.aliases?.join(" ") ?? ""],
const result = fuzzysort.go(removeLineRange(currentFilter), mixed, {
keys: [(obj) => removeLineRange(obj.display.trimEnd()), "description", (obj) => obj.aliases?.join(" ") ?? ""],
limit: 10,
scoreFn: (objResults) => {
const displayResult = objResults[0]
@@ -52,7 +52,6 @@ import { DialogMessage } from "./dialog-message"
import type { PromptInfo } from "../../component/prompt/history"
import { iife } from "@/util/iife"
import { DialogConfirm } from "@tui/ui/dialog-confirm"
import { DialogPrompt } from "@tui/ui/dialog-prompt"
import { DialogTimeline } from "./dialog-timeline"
import { DialogForkFromTimeline } from "./dialog-fork-from-timeline"
import { DialogSessionRename } from "../../component/dialog-session-rename"
@@ -67,7 +66,7 @@ import stripAnsi from "strip-ansi"
import { Footer } from "./footer.tsx"
import { usePromptRef } from "../../context/prompt"
import { Filesystem } from "@/util/filesystem"
import { DialogSubagent } from "./dialog-subagent.tsx"
import { DialogExportOptions } from "../../ui/dialog-export-options"
addDefaultParsers(parsers.parsers)
@@ -784,8 +783,22 @@ export function Session() {
for (const part of parts) {
if (part.type === "text" && !part.synthetic) {
transcript += `${part.text}\n\n`
} else if (part.type === "reasoning") {
if (showThinking()) {
transcript += `_Thinking:_\n\n${part.text}\n\n`
}
} else if (part.type === "tool") {
transcript += `\`\`\`\nTool: ${part.tool}\n\`\`\`\n\n`
transcript += `\`\`\`\nTool: ${part.tool}\n`
if (showDetails() && part.state.input) {
transcript += `\n**Input:**\n\`\`\`json\n${JSON.stringify(part.state.input, null, 2)}\n\`\`\``
}
if (showDetails() && part.state.status === "completed" && part.state.output) {
transcript += `\n**Output:**\n\`\`\`\n${part.state.output}\n\`\`\``
}
if (showDetails() && part.state.status === "error" && part.state.error) {
transcript += `\n**Error:**\n\`\`\`\n${part.state.error}\n\`\`\``
}
transcript += `\n\`\`\`\n\n`
}
}
@@ -812,6 +825,14 @@ export function Session() {
const sessionData = session()
const sessionMessages = messages()
const defaultFilename = `session-${sessionData.id.slice(0, 8)}.md`
const options = await DialogExportOptions.show(dialog, defaultFilename, showThinking(), showDetails())
if (options === null) return
const { filename: customFilename, thinking: includeThinking, toolDetails: includeToolDetails } = options
let transcript = `# ${sessionData.title}\n\n`
transcript += `**Session ID:** ${sessionData.id}\n`
transcript += `**Created:** ${new Date(sessionData.time.created).toLocaleString()}\n`
@@ -826,22 +847,28 @@ export function Session() {
for (const part of parts) {
if (part.type === "text" && !part.synthetic) {
transcript += `${part.text}\n\n`
} else if (part.type === "reasoning") {
if (includeThinking) {
transcript += `_Thinking:_\n\n${part.text}\n\n`
}
} else if (part.type === "tool") {
transcript += `\`\`\`\nTool: ${part.tool}\n\`\`\`\n\n`
transcript += `\`\`\`\nTool: ${part.tool}\n`
if (includeToolDetails && part.state.input) {
transcript += `\n**Input:**\n\`\`\`json\n${JSON.stringify(part.state.input, null, 2)}\n\`\`\``
}
if (includeToolDetails && part.state.status === "completed" && part.state.output) {
transcript += `\n**Output:**\n\`\`\`\n${part.state.output}\n\`\`\``
}
if (includeToolDetails && part.state.status === "error" && part.state.error) {
transcript += `\n**Error:**\n\`\`\`\n${part.state.error}\n\`\`\``
}
transcript += `\n\`\`\`\n\n`
}
}
transcript += `---\n\n`
}
// Prompt for optional filename
const customFilename = await DialogPrompt.show(dialog, "Export filename", {
value: `session-${sessionData.id.slice(0, 8)}.md`,
})
// Cancel if user pressed escape
if (customFilename === null) return
// Save to file in current working directory
const exportDir = process.cwd()
const filename = customFilename.trim()
@@ -0,0 +1,148 @@
import { TextareaRenderable, TextAttributes } from "@opentui/core"
import { useTheme } from "../context/theme"
import { useDialog, type DialogContext } from "./dialog"
import { createStore } from "solid-js/store"
import { onMount, Show, type JSX } from "solid-js"
import { useKeyboard } from "@opentui/solid"
export type DialogExportOptionsProps = {
defaultFilename: string
defaultThinking: boolean
defaultToolDetails: boolean
onConfirm?: (options: { filename: string; thinking: boolean; toolDetails: boolean }) => void
onCancel?: () => void
}
export function DialogExportOptions(props: DialogExportOptionsProps) {
const dialog = useDialog()
const { theme } = useTheme()
let textarea: TextareaRenderable
const [store, setStore] = createStore({
thinking: props.defaultThinking,
toolDetails: props.defaultToolDetails,
active: "filename" as "filename" | "thinking" | "toolDetails",
})
useKeyboard((evt) => {
if (evt.name === "return") {
props.onConfirm?.({
filename: textarea.plainText,
thinking: store.thinking,
toolDetails: store.toolDetails,
})
}
if (evt.name === "tab") {
const order: Array<"filename" | "thinking" | "toolDetails"> = ["filename", "thinking", "toolDetails"]
const currentIndex = order.indexOf(store.active)
const nextIndex = (currentIndex + 1) % order.length
setStore("active", order[nextIndex])
evt.preventDefault()
}
if (evt.name === "space") {
if (store.active === "thinking") setStore("thinking", !store.thinking)
if (store.active === "toolDetails") setStore("toolDetails", !store.toolDetails)
evt.preventDefault()
}
})
onMount(() => {
dialog.setSize("medium")
setTimeout(() => {
textarea.focus()
}, 1)
textarea.gotoLineEnd()
})
return (
<box paddingLeft={2} paddingRight={2} gap={1}>
<box flexDirection="row" justifyContent="space-between">
<text attributes={TextAttributes.BOLD} fg={theme.text}>
Export Options
</text>
<text fg={theme.textMuted}>esc</text>
</box>
<box gap={1}>
<box>
<text fg={theme.text}>Filename:</text>
</box>
<textarea
onSubmit={() => {
props.onConfirm?.({
filename: textarea.plainText,
thinking: store.thinking,
toolDetails: store.toolDetails,
})
}}
height={3}
keyBindings={[{ name: "return", action: "submit" }]}
ref={(val: TextareaRenderable) => (textarea = val)}
initialValue={props.defaultFilename}
placeholder="Enter filename"
textColor={theme.text}
focusedTextColor={theme.text}
cursorColor={theme.text}
/>
</box>
<box flexDirection="column">
<box
flexDirection="row"
gap={2}
paddingLeft={1}
backgroundColor={store.active === "thinking" ? theme.backgroundElement : undefined}
onMouseUp={() => setStore("active", "thinking")}
>
<text fg={store.active === "thinking" ? theme.primary : theme.textMuted}>
{store.thinking ? "[x]" : "[ ]"}
</text>
<text fg={store.active === "thinking" ? theme.primary : theme.text}>Include thinking</text>
</box>
<box
flexDirection="row"
gap={2}
paddingLeft={1}
backgroundColor={store.active === "toolDetails" ? theme.backgroundElement : undefined}
onMouseUp={() => setStore("active", "toolDetails")}
>
<text fg={store.active === "toolDetails" ? theme.primary : theme.textMuted}>
{store.toolDetails ? "[x]" : "[ ]"}
</text>
<text fg={store.active === "toolDetails" ? theme.primary : theme.text}>Include tool details</text>
</box>
</box>
<Show when={store.active !== "filename"}>
<text fg={theme.textMuted} paddingBottom={1}>
Press <span style={{ fg: theme.text }}>space</span> to toggle, <span style={{ fg: theme.text }}>return</span>{" "}
to confirm
</text>
</Show>
<Show when={store.active === "filename"}>
<text fg={theme.textMuted} paddingBottom={1}>
Press <span style={{ fg: theme.text }}>return</span> to confirm, <span style={{ fg: theme.text }}>tab</span>{" "}
for options
</text>
</Show>
</box>
)
}
DialogExportOptions.show = (
dialog: DialogContext,
defaultFilename: string,
defaultThinking: boolean,
defaultToolDetails: boolean,
) => {
return new Promise<{ filename: string; thinking: boolean; toolDetails: boolean } | null>((resolve) => {
dialog.replace(
() => (
<DialogExportOptions
defaultFilename={defaultFilename}
defaultThinking={defaultThinking}
defaultToolDetails={defaultToolDetails}
onConfirm={(options) => resolve(options)}
onCancel={() => resolve(null)}
/>
),
() => resolve(null),
)
})
}
+2 -2
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 ??= []
@@ -155,7 +155,7 @@ export namespace Config {
}
})
const INVALID_DIRS = new Bun.Glob(`{${["agents", "commands", "plugins", "tools"].join(",")}}/`)
const INVALID_DIRS = new Bun.Glob(`{${["agents", "commands", "plugins", "tools", "skills"].join(",")}}/`)
async function assertValid(dir: string) {
const invalid = await Array.fromAsync(
INVALID_DIRS.scan({
+1
View File
@@ -77,6 +77,7 @@ const cli = yargs(hideBin(process.argv))
})
})
.usage("\n" + UI.logo())
.completion("completion", "generate shell completion script")
.command(AcpCommand)
.command(McpCommand)
.command(TuiThreadCommand)
+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://.
-1
View File
@@ -2,7 +2,6 @@ import { Ripgrep } from "../file/ripgrep"
import { Global } from "../global"
import { Filesystem } from "../util/filesystem"
import { Config } from "../config/config"
import { Skill } from "../skill"
import { Instance } from "../project/instance"
import path from "path"
+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.
+18 -13
View File
@@ -26,20 +26,25 @@ export const SkillTool: Tool.Info<typeof parameters> = {
})
}
const description =
accessibleSkills.length === 0
? "Load a skill to get detailed instructions for a specific task. No skills are currently available."
: [
"Load a skill to get detailed instructions for a specific task.",
"Skills provide specialized knowledge and step-by-step guidance.",
"Use this when a task matches an available skill's description.",
"<available_skills>",
...accessibleSkills.flatMap((skill) => [
` <skill>`,
` <name>${skill.name}</name>`,
` <description>${skill.description}</description>`,
` </skill>`,
]),
"</available_skills>",
].join(" ")
return {
description: [
"Load a skill to get detailed instructions for a specific task.",
"Skills provide specialized knowledge and step-by-step guidance.",
"Use this when a task matches an available skill's description.",
"<available_skills>",
...accessibleSkills.flatMap((skill) => [
` <skill>`,
` <name>${skill.name}</name>`,
` <description>${skill.description}</description>`,
` </skill>`,
]),
"</available_skills>",
].join(" "),
description,
parameters,
async execute(params, ctx) {
const agent = await Agent.get(ctx.agent)
@@ -0,0 +1,129 @@
import { test, expect, describe } from "bun:test"
import { extractResponseText } from "../../src/cli/cmd/github"
import type { MessageV2 } from "../../src/session/message-v2"
// Helper to create minimal valid parts
function createTextPart(text: string): MessageV2.Part {
return {
id: "1",
sessionID: "s",
messageID: "m",
type: "text" as const,
text,
}
}
function createReasoningPart(text: string): MessageV2.Part {
return {
id: "1",
sessionID: "s",
messageID: "m",
type: "reasoning" as const,
text,
time: { start: 0 },
}
}
function createToolPart(tool: string, title: string, status: "completed" | "running" = "completed"): MessageV2.Part {
if (status === "completed") {
return {
id: "1",
sessionID: "s",
messageID: "m",
type: "tool" as const,
callID: "c1",
tool,
state: {
status: "completed",
input: {},
output: "",
title,
metadata: {},
time: { start: 0, end: 1 },
},
}
}
return {
id: "1",
sessionID: "s",
messageID: "m",
type: "tool" as const,
callID: "c1",
tool,
state: {
status: "running",
input: {},
time: { start: 0 },
},
}
}
function createStepStartPart(): MessageV2.Part {
return {
id: "1",
sessionID: "s",
messageID: "m",
type: "step-start" as const,
}
}
describe("extractResponseText", () => {
test("returns text from text part", () => {
const parts = [createTextPart("Hello world")]
expect(extractResponseText(parts)).toBe("Hello world")
})
test("returns last text part when multiple exist", () => {
const parts = [createTextPart("First"), createTextPart("Last")]
expect(extractResponseText(parts)).toBe("Last")
})
test("returns text even when tool parts follow", () => {
const parts = [createTextPart("I'll help with that."), createToolPart("todowrite", "3 todos")]
expect(extractResponseText(parts)).toBe("I'll help with that.")
})
test("returns null for reasoning-only response (signals summary needed)", () => {
const parts = [createReasoningPart("Let me think about this...")]
expect(extractResponseText(parts)).toBeNull()
})
test("returns null for tool-only response (signals summary needed)", () => {
// This is the exact scenario from the bug report - todowrite with no text
const parts = [createToolPart("todowrite", "8 todos")]
expect(extractResponseText(parts)).toBeNull()
})
test("returns null for multiple completed tools", () => {
const parts = [
createToolPart("read", "src/file.ts"),
createToolPart("edit", "src/file.ts"),
createToolPart("bash", "bun test"),
]
expect(extractResponseText(parts)).toBeNull()
})
test("ignores running tool parts (throws since no completed tools)", () => {
const parts = [createToolPart("bash", "", "running")]
expect(() => extractResponseText(parts)).toThrow("Failed to parse response")
})
test("throws with part types on empty array", () => {
expect(() => extractResponseText([])).toThrow("Part types found: [none]")
})
test("throws with part types on unhandled parts", () => {
const parts = [createStepStartPart()]
expect(() => extractResponseText(parts)).toThrow("Part types found: [step-start]")
})
test("prefers text over reasoning when both present", () => {
const parts = [createReasoningPart("Internal thinking..."), createTextPart("Final answer")]
expect(extractResponseText(parts)).toBe("Final answer")
})
test("prefers text over tools when both present", () => {
const parts = [createToolPart("read", "src/file.ts"), createTextPart("Here's what I found")]
expect(extractResponseText(parts)).toBe("Here's what I found")
})
})
@@ -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.204",
"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.204",
"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.204",
"version": "1.0.207",
"type": "module",
"scripts": {
"dev": "bun run src/index.ts",
+4 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@opencode-ai/ui",
"version": "1.0.204",
"version": "1.0.207",
"type": "module",
"exports": {
"./*": "./src/components/*.tsx",
@@ -11,6 +11,9 @@
"./context/*": "./src/context/*.tsx",
"./styles": "./src/styles/index.css",
"./styles/tailwind": "./src/styles/tailwind/index.css",
"./theme": "./src/theme/index.ts",
"./theme/*": "./src/theme/*.ts",
"./theme/context": "./src/theme/context.tsx",
"./icons/provider": "./src/components/provider-icons/types.ts",
"./icons/file-type": "./src/components/file-icons/types.ts",
"./fonts/*": "./src/assets/fonts/*",
+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)
}
+5 -2
View File
@@ -137,6 +137,9 @@
[data-slot="message-part-tool-error-message"] {
color: var(--text-on-critical-weak);
max-height: 240px;
overflow-y: auto;
word-break: break-word;
}
}
@@ -387,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>
@@ -343,6 +343,8 @@
.error-card {
color: var(--text-on-critical-base);
max-height: 240px;
overflow-y: auto;
}
[data-slot="session-turn-collapsible-content-inner"] {
@@ -328,6 +328,12 @@ export function SessionTurn(
}
})
createEffect(() => {
if (permissionParts().length > 0) {
autoScroll.forceScrollToBottom()
}
})
createEffect(() => {
if (working() || !isLastUserMessage()) return
+66
View File
@@ -205,4 +205,70 @@
/* [data-slot="tabs-content"] { */
/* } */
}
&[data-orientation="vertical"] {
flex-direction: row;
[data-slot="tabs-list"] {
flex-direction: column;
width: auto;
height: 100%;
overflow-x: hidden;
overflow-y: auto;
&::after {
width: 100%;
height: auto;
flex-grow: 1;
border-bottom: none;
border-right: 1px solid var(--border-weak-base);
}
}
[data-slot="tabs-trigger-wrapper"] {
width: 100%;
height: auto;
border-bottom: none;
border-right: 1px solid var(--border-weak-base);
&:has([data-selected]) {
border-right-color: transparent;
}
}
[data-slot="tabs-content"] {
overflow-x: auto;
overflow-y: auto;
}
&[data-variant="alt"] {
[data-slot="tabs-list"] {
padding-left: 0;
padding-right: 0;
padding-top: 24px;
padding-bottom: 24px;
border-bottom: none;
border-right: 1px solid var(--border-weak-base);
&::after {
border: none;
}
}
[data-slot="tabs-trigger-wrapper"] {
border-bottom: none;
border-right-width: 2px;
border-right-style: solid;
border-right-color: transparent;
[data-slot="tabs-trigger"] {
border-bottom: none;
}
&:has([data-selected]) {
border-right-color: var(--icon-strong-base);
}
}
}
}
}
+4 -1
View File
@@ -4,6 +4,7 @@ import type { ComponentProps, ParentProps } from "solid-js"
export interface TabsProps extends ComponentProps<typeof Kobalte> {
variant?: "normal" | "alt"
orientation?: "horizontal" | "vertical"
}
export interface TabsListProps extends ComponentProps<typeof Kobalte.List> {}
export interface TabsTriggerProps extends ComponentProps<typeof Kobalte.Trigger> {
@@ -16,12 +17,14 @@ export interface TabsTriggerProps extends ComponentProps<typeof Kobalte.Trigger>
export interface TabsContentProps extends ComponentProps<typeof Kobalte.Content> {}
function TabsRoot(props: TabsProps) {
const [split, rest] = splitProps(props, ["class", "classList", "variant"])
const [split, rest] = splitProps(props, ["class", "classList", "variant", "orientation"])
return (
<Kobalte
{...rest}
orientation={split.orientation}
data-component="tabs"
data-variant={split.variant || "normal"}
data-orientation={split.orientation || "horizontal"}
classList={{
...(split.classList ?? {}),
[split.class ?? ""]: !!split.class,
+1 -4
View File
@@ -92,7 +92,6 @@ export type ToastVariant = "default" | "success" | "error" | "loading"
export interface ToastAction {
label: string
onClick: "dismiss" | (() => void)
dismissAfter?: boolean
}
export interface ToastOptions {
@@ -132,10 +131,8 @@ export function showToast(options: ToastOptions | string) {
onClick={() => {
if (typeof action.onClick === "function") {
action.onClick()
if (action.dismissAfter) toaster.dismiss(props.toastId)
} else {
toaster.dismiss(props.toastId)
}
toaster.dismiss(props.toastId)
}}
>
{action.label}
@@ -35,6 +35,22 @@ export function createAutoScroll(options: AutoScrollOptions) {
})
}
function forceScrollToBottom() {
if (!scrollRef) return
setStore("userScrolled", false)
isAutoScrolling = true
if (autoScrollTimeout) clearTimeout(autoScrollTimeout)
autoScrollTimeout = setTimeout(() => {
isAutoScrolling = false
}, 1000)
scrollRef.scrollTo({
top: scrollRef.scrollHeight,
behavior: "smooth",
})
}
function handleScroll() {
if (!scrollRef) return
@@ -161,6 +177,7 @@ export function createAutoScroll(options: AutoScrollOptions) {
handleScroll,
handleInteraction,
scrollToBottom,
forceScrollToBottom,
userScrolled: () => store.userScrolled,
}
}
+2 -477
View File
@@ -68,7 +68,7 @@
color-scheme: light;
--text-mix-blend-mode: multiply;
/* OC-1-light */
/* OC-1 fallback variables (light) */
--background-base: #f8f7f7;
--background-weak: var(--smoke-light-3);
--background-strong: var(--smoke-light-1);
@@ -324,7 +324,7 @@
color-scheme: dark;
--text-mix-blend-mode: plus-lighter;
/* OC-1-dark */
/* OC-1 fallback variables (dark) */
--background-base: var(--smoke-dark-1);
--background-weak: #1c1717;
--background-strong: #151313;
@@ -577,478 +577,3 @@
--avatar-text-lime: #c4f042;
}
}
html[data-theme="oc-2-paper"] {
/* OC-2-paper */
--background-base: #f7f8f8;
--background-weak: var(--ink-light-3);
--background-strong: var(--ink-light-1);
--background-stronger: #fcfcfc;
--surface-base: var(--ink-light-alpha-2);
--base: var(--ink-light-alpha-2);
--surface-base-hover: #0005050f;
--surface-base-active: var(--ink-light-alpha-3);
--surface-base-interactive-active: var(--cobalt-light-alpha-3);
--base2: var(--ink-light-alpha-2);
--base3: var(--ink-light-alpha-2);
--surface-inset-base: var(--ink-light-alpha-2);
--surface-inset-base-hover: var(--ink-light-alpha-3);
--surface-inset-strong: #001f1f17;
--surface-inset-strong-hover: #001f1f17;
--surface-raised-base: var(--ink-light-alpha-1);
--surface-float-base: var(--ink-dark-1);
--surface-float-base-hover: var(--ink-dark-2);
--surface-raised-base-hover: var(--ink-light-alpha-2);
--surface-raised-base-active: var(--ink-light-alpha-3);
--surface-raised-strong: var(--ink-light-1);
--surface-raised-strong-hover: var(--white);
--surface-raised-stronger: var(--white);
--surface-raised-stronger-hover: var(--white);
--surface-weak: var(--ink-light-alpha-3);
--surface-weaker: var(--ink-light-alpha-4);
--surface-strong: #ffffff;
--surface-raised-stronger-non-alpha: var(--white);
--surface-brand-base: var(--yuzu-light-9);
--surface-brand-hover: var(--yuzu-light-10);
--surface-interactive-base: var(--cobalt-light-3);
--surface-interactive-hover: var(--cobalt-light-4);
--surface-interactive-weak: var(--cobalt-light-2);
--surface-interactive-weak-hover: var(--cobalt-light-3);
--surface-success-base: var(--apple-light-3);
--surface-success-weak: var(--apple-light-2);
--surface-success-strong: var(--apple-light-9);
--surface-warning-base: var(--solaris-light-3);
--surface-warning-weak: var(--solaris-light-2);
--surface-warning-strong: var(--solaris-light-9);
--surface-critical-base: var(--ember-light-3);
--surface-critical-weak: var(--ember-light-2);
--surface-critical-strong: var(--ember-light-9);
--surface-info-base: var(--lilac-light-3);
--surface-info-weak: var(--lilac-light-2);
--surface-info-strong: var(--lilac-light-9);
--surface-diff-unchanged-base: #ffffff00;
--surface-diff-skip-base: var(--ink-light-2);
--surface-diff-hidden-base: var(--blue-light-3);
--surface-diff-hidden-weak: var(--blue-light-2);
--surface-diff-hidden-weaker: var(--blue-light-1);
--surface-diff-hidden-strong: var(--blue-light-5);
--surface-diff-hidden-stronger: var(--blue-light-9);
--surface-diff-add-base: var(--mint-light-3);
--surface-diff-add-weak: var(--mint-light-2);
--surface-diff-add-weaker: var(--mint-light-1);
--surface-diff-add-strong: var(--mint-light-5);
--surface-diff-add-stronger: var(--mint-light-9);
--surface-diff-delete-base: var(--ember-light-3);
--surface-diff-delete-weak: var(--ember-light-2);
--surface-diff-delete-weaker: var(--ember-light-1);
--surface-diff-delete-strong: var(--ember-light-6);
--surface-diff-delete-stronger: var(--ember-light-9);
--text-base: var(--ink-light-11);
--input-base: var(--ink-light-1);
--input-hover: var(--ink-light-2);
--input-active: var(--cobalt-light-1);
--input-selected: var(--cobalt-light-4);
--input-focus: var(--cobalt-light-1);
--input-disabled: var(--ink-light-4);
--text-weak: var(--ink-light-9);
--text-weaker: var(--ink-light-8);
--text-strong: var(--ink-light-12);
--text-interactive-base: var(--cobalt-light-9);
--text-on-brand-base: var(--ink-light-alpha-11);
--text-on-interactive-base: var(--ink-light-1);
--text-on-interactive-weak: var(--ink-light-alpha-11);
--text-on-success-base: var(--apple-light-10);
--text-on-critical-base: var(--ember-light-10);
--text-on-critical-weak: var(--ember-light-8);
--text-on-critical-strong: var(--ember-light-12);
--text-on-warning-base: var(--ink-dark-alpha-11);
--text-on-info-base: var(--ink-dark-alpha-11);
--text-diff-add-base: var(--mint-light-11);
--text-diff-delete-base: var(--ember-light-10);
--text-diff-delete-strong: var(--ember-light-12);
--text-diff-add-strong: var(--mint-light-12);
--text-on-info-weak: var(--ink-dark-alpha-9);
--text-on-info-strong: var(--ink-dark-alpha-12);
--text-on-warning-weak: var(--ink-dark-alpha-9);
--text-on-warning-strong: var(--ink-dark-alpha-12);
--text-on-success-weak: var(--apple-light-6);
--text-on-success-strong: var(--apple-light-12);
--text-on-brand-weak: var(--ink-light-alpha-9);
--text-on-brand-weaker: var(--ink-light-alpha-8);
--text-on-brand-strong: var(--ink-light-alpha-12);
--button-secondary-base: #fcfdfd;
--button-secondary-hover: #f9fafa;
--border-base: var(--ink-light-alpha-7);
--border-hover: var(--ink-light-alpha-8);
--border-active: var(--ink-light-alpha-9);
--border-selected: var(--cobalt-light-alpha-9);
--border-disabled: var(--ink-light-alpha-8);
--border-focus: var(--ink-light-alpha-9);
--border-weak-base: var(--ink-light-alpha-5);
--border-strong-base: var(--ink-light-alpha-7);
--border-strong-hover: var(--ink-light-alpha-8);
--border-strong-active: var(--ink-light-alpha-7);
--border-strong-selected: var(--cobalt-light-alpha-6);
--border-strong-disabled: var(--ink-light-alpha-6);
--border-strong-focus: var(--ink-light-alpha-7);
--border-weak-hover: var(--ink-light-alpha-6);
--border-weak-active: var(--ink-light-alpha-7);
--border-weak-selected: var(--cobalt-light-alpha-5);
--border-weak-disabled: var(--ink-light-alpha-6);
--border-weak-focus: var(--ink-light-alpha-7);
--border-interactive-base: var(--cobalt-light-7);
--border-interactive-hover: var(--cobalt-light-8);
--border-interactive-active: var(--cobalt-light-9);
--border-interactive-selected: var(--cobalt-light-9);
--border-interactive-disabled: var(--ink-light-8);
--border-interactive-focus: var(--cobalt-light-9);
--border-success-base: var(--apple-light-6);
--border-success-hover: var(--apple-light-7);
--border-success-selected: var(--apple-light-9);
--border-warning-base: var(--solaris-light-6);
--border-warning-hover: var(--solaris-light-7);
--border-warning-selected: var(--solaris-light-9);
--border-critical-base: var(--ember-light-6);
--border-critical-hover: var(--ember-light-7);
--border-critical-selected: var(--ember-light-9);
--border-info-base: var(--lilac-light-6);
--border-info-hover: var(--lilac-light-7);
--border-info-selected: var(--lilac-light-9);
--icon-base: var(--ink-light-9);
--icon-hover: var(--ink-light-11);
--icon-active: var(--ink-light-12);
--icon-selected: var(--ink-light-12);
--icon-disabled: var(--ink-light-8);
--icon-focus: var(--ink-light-12);
--icon-invert-base: #ffffff;
--icon-weak-base: var(--ink-light-7);
--icon-weak-hover: var(--ink-light-8);
--icon-weak-active: var(--ink-light-9);
--icon-weak-selected: var(--ink-light-10);
--icon-weak-disabled: var(--ink-light-6);
--icon-weak-focus: var(--ink-light-9);
--icon-strong-base: var(--ink-light-12);
--icon-strong-hover: #131515;
--icon-strong-active: #020202;
--icon-strong-selected: #020202;
--icon-strong-disabled: var(--ink-light-8);
--icon-strong-focus: #020202;
--icon-brand-base: var(--ink-light-12);
--icon-interactive-base: var(--cobalt-light-9);
--icon-success-base: var(--apple-light-7);
--icon-success-hover: var(--apple-light-8);
--icon-success-active: var(--apple-light-11);
--icon-warning-base: var(--amber-light-7);
--icon-warning-hover: var(--amber-light-8);
--icon-warning-active: var(--amber-light-11);
--icon-critical-base: var(--ember-light-10);
--icon-critical-hover: var(--ember-light-11);
--icon-critical-active: var(--ember-light-12);
--icon-info-base: var(--lilac-light-7);
--icon-info-hover: var(--lilac-light-8);
--icon-info-active: var(--lilac-light-11);
--icon-on-brand-base: var(--ink-light-alpha-11);
--icon-on-brand-hover: var(--ink-light-alpha-12);
--icon-on-brand-selected: var(--ink-light-alpha-12);
--icon-on-interactive-base: var(--ink-light-1);
--icon-agent-plan-base: var(--purple-light-9);
--icon-agent-docs-base: var(--amber-light-9);
--icon-agent-ask-base: var(--cyan-light-9);
--icon-agent-build-base: var(--cobalt-light-9);
--icon-on-success-base: var(--apple-light-alpha-9);
--icon-on-success-hover: var(--apple-light-alpha-10);
--icon-on-success-selected: var(--apple-light-alpha-11);
--icon-on-warning-base: var(--amber-lightalpha-9);
--icon-on-warning-hover: var(--amber-lightalpha-10);
--icon-on-warning-selected: var(--amber-lightalpha-11);
--icon-on-critical-base: var(--ember-light-alpha-9);
--icon-on-critical-hover: var(--ember-light-alpha-10);
--icon-on-critical-selected: var(--ember-light-alpha-11);
--icon-on-info-base: var(--lilac-light-9);
--icon-on-info-hover: var(--lilac-light-alpha-10);
--icon-on-info-selected: var(--lilac-light-alpha-11);
--icon-diff-add-base: var(--mint-light-11);
--icon-diff-add-hover: var(--mint-light-12);
--icon-diff-add-active: var(--mint-light-12);
--icon-diff-delete-base: var(--ember-light-10);
--icon-diff-delete-hover: var(--ember-light-11);
--syntax-comment: var(--text-weak);
--syntax-regexp: var(--text-base);
--syntax-string: #007663;
--syntax-keyword: var(--text-weak);
--syntax-primitive: #fb7f51;
--syntax-operator: var(--text-weak);
--syntax-variable: var(--text-strong);
--syntax-property: #ec6cc8;
--syntax-type: #738400;
--syntax-constant: #00b2b9;
--syntax-punctuation: var(--text-weak);
--syntax-object: var(--text-strong);
--syntax-success: var(--apple-light-10);
--syntax-warning: var(--amber-light-10);
--syntax-critical: var(--ember-light-9);
--syntax-info: #0091a7;
--syntax-diff-add: var(--mint-light-11);
--syntax-diff-delete: var(--ember-light-11);
--syntax-diff-unknown: #ff0000;
--markdown-heading: #d68c27;
--markdown-text: #1a1a1a;
--markdown-link: #3b7dd8;
--markdown-link-text: #318795;
--markdown-code: #3d9a57;
--markdown-block-quote: #b0851f;
--markdown-emph: #b0851f;
--markdown-strong: #d68c27;
--markdown-horizontal-rule: #8a8a8a;
--markdown-list-item: #3b7dd8;
--markdown-list-enumeration: #318795;
--markdown-image: #3b7dd8;
--markdown-image-text: #318795;
--markdown-code-block: #1a1a1a;
--border-color: #ffffff;
--border-weaker-base: var(--ink-light-alpha-3);
--border-weaker-hover: var(--ink-light-alpha-4);
--border-weaker-active: var(--ink-light-alpha-6);
--border-weaker-selected: var(--cobalt-light-alpha-4);
--border-weaker-disabled: var(--ink-light-alpha-2);
--border-weaker-focus: var(--ink-light-alpha-6);
--button-ghost-hover: var(--ink-light-alpha-2);
--button-ghost-hover2: var(--ink-light-alpha-3);
@media (prefers-color-scheme: dark) {
--background-base: var(--ink-dark-1);
--background-weak: #171c1c;
--background-strong: #131515;
--background-stronger: #151919;
--surface-base: var(--ink-dark-alpha-2);
--base: var(--ink-dark-alpha-2);
--surface-base-hover: #b8e0e00f;
--surface-base-active: var(--ink-dark-alpha-3);
--surface-base-interactive-active: var(--cobalt-light-alpha-3);
--base2: var(--ink-dark-alpha-2);
--base3: var(--ink-dark-alpha-2);
--surface-inset-base: #0b0e0e7f;
--surface-inset-base-hover: #0b0e0e7f;
--surface-inset-strong: #050606cc;
--surface-inset-strong-hover: #050606cc;
--surface-raised-base: var(--ink-light-alpha-1);
--surface-float-base: var(--ink-dark-1);
--surface-float-base-hover: var(--ink-dark-2);
--surface-raised-base-hover: var(--ink-light-alpha-2);
--surface-raised-base-active: var(--ink-light-alpha-3);
--surface-raised-strong: var(--ink-light-1);
--surface-raised-strong-hover: var(--white);
--surface-raised-stronger: var(--white);
--surface-raised-stronger-hover: var(--white);
--surface-weak: var(--ink-dark-alpha-4);
--surface-weaker: var(--ink-dark-alpha-5);
--surface-strong: var(--ink-dark-alpha-7);
--surface-raised-stronger-non-alpha: var(--white);
--surface-brand-base: var(--yuzu-light-9);
--surface-brand-hover: var(--yuzu-light-10);
--surface-interactive-base: var(--cobalt-light-3);
--surface-interactive-hover: var(--cobalt-light-4);
--surface-interactive-weak: var(--cobalt-light-2);
--surface-interactive-weak-hover: var(--cobalt-light-3);
--surface-success-base: var(--apple-light-3);
--surface-success-weak: var(--apple-light-2);
--surface-success-strong: var(--apple-light-9);
--surface-warning-base: var(--solaris-light-3);
--surface-warning-weak: var(--solaris-light-2);
--surface-warning-strong: var(--solaris-light-9);
--surface-critical-base: var(--ember-light-3);
--surface-critical-weak: var(--ember-light-2);
--surface-critical-strong: var(--ember-light-9);
--surface-info-base: var(--lilac-light-3);
--surface-info-weak: var(--lilac-light-2);
--surface-info-strong: var(--lilac-light-9);
--surface-diff-unchanged-base: #ffffff00;
--surface-diff-skip-base: var(--ink-light-2);
--surface-diff-hidden-base: var(--blue-light-3);
--surface-diff-hidden-weak: var(--blue-light-2);
--surface-diff-hidden-weaker: var(--blue-light-1);
--surface-diff-hidden-strong: var(--blue-light-5);
--surface-diff-hidden-stronger: var(--blue-light-9);
--surface-diff-add-base: var(--mint-light-3);
--surface-diff-add-weak: var(--mint-light-2);
--surface-diff-add-weaker: var(--mint-light-1);
--surface-diff-add-strong: var(--mint-light-5);
--surface-diff-add-stronger: var(--mint-light-9);
--surface-diff-delete-base: var(--ember-light-3);
--surface-diff-delete-weak: var(--ember-light-2);
--surface-diff-delete-weaker: var(--ember-light-1);
--surface-diff-delete-strong: var(--ember-light-6);
--surface-diff-delete-stronger: var(--ember-light-9);
--text-base: var(--ink-light-11);
--input-base: var(--ink-light-1);
--input-hover: var(--ink-light-2);
--input-active: var(--cobalt-light-1);
--input-selected: var(--cobalt-light-4);
--input-focus: var(--cobalt-light-1);
--input-disabled: var(--ink-light-4);
--text-weak: var(--ink-light-9);
--text-weaker: var(--ink-light-8);
--text-strong: var(--ink-light-12);
--text-interactive-base: var(--cobalt-light-9);
--text-on-brand-base: var(--ink-light-alpha-11);
--text-on-interactive-base: var(--ink-light-1);
--text-on-interactive-weak: var(--ink-light-alpha-11);
--text-on-success-base: var(--apple-light-10);
--text-on-critical-base: var(--ember-light-10);
--text-on-critical-weak: var(--ember-light-8);
--text-on-critical-strong: var(--ember-light-12);
--text-on-warning-base: var(--ink-dark-alpha-11);
--text-on-info-base: var(--ink-dark-alpha-11);
--text-diff-add-base: var(--mint-light-11);
--text-diff-delete-base: var(--ember-light-10);
--text-diff-delete-strong: var(--ember-light-12);
--text-diff-add-strong: var(--mint-light-12);
--text-on-info-weak: var(--ink-dark-alpha-9);
--text-on-info-strong: var(--ink-dark-alpha-12);
--text-on-warning-weak: var(--ink-dark-alpha-9);
--text-on-warning-strong: var(--ink-dark-alpha-12);
--text-on-success-weak: var(--apple-light-6);
--text-on-success-strong: var(--apple-light-12);
--text-on-brand-weak: var(--ink-light-alpha-9);
--text-on-brand-weaker: var(--ink-light-alpha-8);
--text-on-brand-strong: var(--ink-light-alpha-12);
--button-secondary-base: #fcfdfd;
--button-secondary-hover: #f9fafa;
--border-base: var(--ink-light-alpha-7);
--border-hover: var(--ink-light-alpha-8);
--border-active: var(--ink-light-alpha-9);
--border-selected: var(--cobalt-light-alpha-9);
--border-disabled: var(--ink-light-alpha-8);
--border-focus: var(--ink-light-alpha-9);
--border-weak-base: var(--ink-light-alpha-5);
--border-strong-base: var(--ink-light-alpha-7);
--border-strong-hover: var(--ink-light-alpha-8);
--border-strong-active: var(--ink-light-alpha-7);
--border-strong-selected: var(--cobalt-light-alpha-6);
--border-strong-disabled: var(--ink-light-alpha-6);
--border-strong-focus: var(--ink-light-alpha-7);
--border-weak-hover: var(--ink-light-alpha-6);
--border-weak-active: var(--ink-light-alpha-7);
--border-weak-selected: var(--cobalt-light-alpha-5);
--border-weak-disabled: var(--ink-light-alpha-6);
--border-weak-focus: var(--ink-light-alpha-7);
--border-interactive-base: var(--cobalt-light-7);
--border-interactive-hover: var(--cobalt-light-8);
--border-interactive-active: var(--cobalt-light-9);
--border-interactive-selected: var(--cobalt-light-9);
--border-interactive-disabled: var(--ink-light-8);
--border-interactive-focus: var(--cobalt-light-9);
--border-success-base: var(--apple-light-6);
--border-success-hover: var(--apple-light-7);
--border-success-selected: var(--apple-light-9);
--border-warning-base: var(--solaris-light-6);
--border-warning-hover: var(--solaris-light-7);
--border-warning-selected: var(--solaris-light-9);
--border-critical-base: var(--ember-light-6);
--border-critical-hover: var(--ember-light-7);
--border-critical-selected: var(--ember-light-9);
--border-info-base: var(--lilac-light-6);
--border-info-hover: var(--lilac-light-7);
--border-info-selected: var(--lilac-light-9);
--icon-base: var(--ink-light-9);
--icon-hover: var(--ink-light-11);
--icon-active: var(--ink-light-12);
--icon-selected: var(--ink-light-12);
--icon-disabled: var(--ink-light-8);
--icon-focus: var(--ink-light-12);
--icon-invert-base: #ffffff;
--icon-weak-base: var(--ink-light-7);
--icon-weak-hover: var(--ink-light-8);
--icon-weak-active: var(--ink-light-9);
--icon-weak-selected: var(--ink-light-10);
--icon-weak-disabled: var(--ink-light-6);
--icon-weak-focus: var(--ink-light-9);
--icon-strong-base: var(--ink-light-12);
--icon-strong-hover: #131515;
--icon-strong-active: #020202;
--icon-strong-selected: #020202;
--icon-strong-disabled: var(--ink-light-8);
--icon-strong-focus: #020202;
--icon-brand-base: var(--ink-light-12);
--icon-interactive-base: var(--cobalt-light-9);
--icon-success-base: var(--apple-light-7);
--icon-success-hover: var(--apple-light-8);
--icon-success-active: var(--apple-light-11);
--icon-warning-base: var(--amber-light-7);
--icon-warning-hover: var(--amber-light-8);
--icon-warning-active: var(--amber-light-11);
--icon-critical-base: var(--ember-light-10);
--icon-critical-hover: var(--ember-light-11);
--icon-critical-active: var(--ember-light-12);
--icon-info-base: var(--lilac-light-7);
--icon-info-hover: var(--lilac-light-8);
--icon-info-active: var(--lilac-light-11);
--icon-on-brand-base: var(--ink-light-alpha-11);
--icon-on-brand-hover: var(--ink-light-alpha-12);
--icon-on-brand-selected: var(--ink-light-alpha-12);
--icon-on-interactive-base: var(--ink-light-1);
--icon-agent-plan-base: var(--purple-light-9);
--icon-agent-docs-base: var(--amber-light-9);
--icon-agent-ask-base: var(--cyan-light-9);
--icon-agent-build-base: var(--cobalt-light-9);
--icon-on-success-base: var(--apple-light-alpha-9);
--icon-on-success-hover: var(--apple-light-alpha-10);
--icon-on-success-selected: var(--apple-light-alpha-11);
--icon-on-warning-base: var(--amber-lightalpha-9);
--icon-on-warning-hover: var(--amber-lightalpha-10);
--icon-on-warning-selected: var(--amber-lightalpha-11);
--icon-on-critical-base: var(--ember-light-alpha-9);
--icon-on-critical-hover: var(--ember-light-alpha-10);
--icon-on-critical-selected: var(--ember-light-alpha-11);
--icon-on-info-base: var(--lilac-light-9);
--icon-on-info-hover: var(--lilac-light-alpha-10);
--icon-on-info-selected: var(--lilac-light-alpha-11);
--icon-diff-add-base: var(--mint-light-11);
--icon-diff-add-hover: var(--mint-light-12);
--icon-diff-add-active: var(--mint-light-12);
--icon-diff-delete-base: var(--ember-light-10);
--icon-diff-delete-hover: var(--ember-light-11);
--syntax-comment: var(--text-weak);
--syntax-regexp: var(--text-base);
--syntax-string: #007663;
--syntax-keyword: var(--text-weak);
--syntax-primitive: #fb7f51;
--syntax-operator: var(--text-weak);
--syntax-variable: var(--text-strong);
--syntax-property: #ec6cc8;
--syntax-type: #738400;
--syntax-constant: #00b2b9;
--syntax-punctuation: var(--text-weak);
--syntax-object: var(--text-strong);
--syntax-success: var(--apple-light-10);
--syntax-warning: var(--amber-light-10);
--syntax-critical: var(--ember-light-9);
--syntax-info: #0091a7;
--syntax-diff-add: var(--mint-light-11);
--syntax-diff-delete: var(--ember-light-11);
--syntax-diff-unknown: #ff0000;
--markdown-heading: #d68c27;
--markdown-text: #1a1a1a;
--markdown-link: #3b7dd8;
--markdown-link-text: #318795;
--markdown-code: #3d9a57;
--markdown-block-quote: #b0851f;
--markdown-emph: #b0851f;
--markdown-strong: #d68c27;
--markdown-horizontal-rule: #8a8a8a;
--markdown-list-item: #3b7dd8;
--markdown-list-enumeration: #318795;
--markdown-image: #3b7dd8;
--markdown-image-text: #318795;
--markdown-code-block: #1a1a1a;
--border-color: #ffffff;
--border-weaker-base: var(--ink-light-alpha-3);
--border-weaker-hover: var(--ink-light-alpha-4);
--border-weaker-active: var(--ink-light-alpha-6);
--border-weaker-selected: var(--cobalt-light-alpha-4);
--border-weaker-disabled: var(--ink-light-alpha-2);
--border-weaker-focus: var(--ink-light-alpha-6);
--button-ghost-hover: var(--ink-light-alpha-2);
--button-ghost-hover2: var(--ink-light-alpha-3);
}
}
+194
View File
@@ -0,0 +1,194 @@
import type { HexColor, OklchColor } from "./types"
export function hexToRgb(hex: HexColor): { r: number; g: number; b: number } {
const h = hex.replace("#", "")
const full =
h.length === 3
? h
.split("")
.map((c) => c + c)
.join("")
: h
const num = parseInt(full, 16)
return {
r: ((num >> 16) & 255) / 255,
g: ((num >> 8) & 255) / 255,
b: (num & 255) / 255,
}
}
export function rgbToHex(r: number, g: number, b: number): HexColor {
const toHex = (v: number) => {
const clamped = Math.max(0, Math.min(1, v))
const int = Math.round(clamped * 255)
return int.toString(16).padStart(2, "0")
}
return `#${toHex(r)}${toHex(g)}${toHex(b)}`
}
function linearToSrgb(c: number): number {
if (c <= 0.0031308) return c * 12.92
return 1.055 * Math.pow(c, 1 / 2.4) - 0.055
}
function srgbToLinear(c: number): number {
if (c <= 0.04045) return c / 12.92
return Math.pow((c + 0.055) / 1.055, 2.4)
}
export function rgbToOklch(r: number, g: number, b: number): OklchColor {
const lr = srgbToLinear(r)
const lg = srgbToLinear(g)
const lb = srgbToLinear(b)
const l_ = 0.4122214708 * lr + 0.5363325363 * lg + 0.0514459929 * lb
const m_ = 0.2119034982 * lr + 0.6806995451 * lg + 0.1073969566 * lb
const s_ = 0.0883024619 * lr + 0.2817188376 * lg + 0.6299787005 * lb
const l = Math.cbrt(l_)
const m = Math.cbrt(m_)
const s = Math.cbrt(s_)
const L = 0.2104542553 * l + 0.793617785 * m - 0.0040720468 * s
const a = 1.9779984951 * l - 2.428592205 * m + 0.4505937099 * s
const bOk = 0.0259040371 * l + 0.7827717662 * m - 0.808675766 * s
const C = Math.sqrt(a * a + bOk * bOk)
let H = Math.atan2(bOk, a) * (180 / Math.PI)
if (H < 0) H += 360
return { l: L, c: C, h: H }
}
export function oklchToRgb(oklch: OklchColor): { r: number; g: number; b: number } {
const { l: L, c: C, h: H } = oklch
const a = C * Math.cos((H * Math.PI) / 180)
const b = C * Math.sin((H * Math.PI) / 180)
const l = L + 0.3963377774 * a + 0.2158037573 * b
const m = L - 0.1055613458 * a - 0.0638541728 * b
const s = L - 0.0894841775 * a - 1.291485548 * b
const l3 = l * l * l
const m3 = m * m * m
const s3 = s * s * s
const lr = 4.0767416621 * l3 - 3.3077115913 * m3 + 0.2309699292 * s3
const lg = -1.2684380046 * l3 + 2.6097574011 * m3 - 0.3413193965 * s3
const lb = -0.0041960863 * l3 - 0.7034186147 * m3 + 1.707614701 * s3
return {
r: linearToSrgb(lr),
g: linearToSrgb(lg),
b: linearToSrgb(lb),
}
}
export function hexToOklch(hex: HexColor): OklchColor {
const { r, g, b } = hexToRgb(hex)
return rgbToOklch(r, g, b)
}
export function oklchToHex(oklch: OklchColor): HexColor {
const { r, g, b } = oklchToRgb(oklch)
return rgbToHex(r, g, b)
}
export function generateScale(seed: HexColor, isDark: boolean): HexColor[] {
const base = hexToOklch(seed)
const scale: HexColor[] = []
const lightSteps = isDark
? [0.15, 0.18, 0.22, 0.26, 0.32, 0.38, 0.46, 0.56, base.l, base.l - 0.05, 0.75, 0.93]
: [0.99, 0.97, 0.94, 0.9, 0.85, 0.79, 0.72, 0.64, base.l, base.l + 0.05, 0.45, 0.25]
const chromaMultipliers = isDark
? [0.15, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.85, 1, 1, 0.9, 0.6]
: [0.1, 0.15, 0.25, 0.35, 0.45, 0.55, 0.7, 0.85, 1, 1, 0.95, 0.85]
for (let i = 0; i < 12; i++) {
scale.push(
oklchToHex({
l: lightSteps[i],
c: base.c * chromaMultipliers[i],
h: base.h,
}),
)
}
return scale
}
export function generateNeutralScale(seed: HexColor, isDark: boolean): HexColor[] {
const base = hexToOklch(seed)
const scale: HexColor[] = []
const neutralChroma = Math.min(base.c, 0.02)
const lightSteps = isDark
? [0.13, 0.16, 0.2, 0.24, 0.28, 0.33, 0.4, 0.52, 0.58, 0.66, 0.82, 0.96]
: [0.995, 0.98, 0.96, 0.94, 0.91, 0.88, 0.84, 0.78, 0.62, 0.56, 0.46, 0.2]
for (let i = 0; i < 12; i++) {
scale.push(
oklchToHex({
l: lightSteps[i],
c: neutralChroma,
h: base.h,
}),
)
}
return scale
}
export function generateAlphaScale(scale: HexColor[], isDark: boolean): HexColor[] {
const alphas = isDark
? [0.02, 0.04, 0.08, 0.12, 0.16, 0.2, 0.26, 0.36, 0.44, 0.52, 0.76, 0.96]
: [0.01, 0.03, 0.06, 0.09, 0.12, 0.15, 0.2, 0.28, 0.48, 0.56, 0.64, 0.88]
return scale.map((hex, i) => {
const { r, g, b } = hexToRgb(hex)
const a = alphas[i]
const bg = isDark ? 0 : 1
const blendedR = r * a + bg * (1 - a)
const blendedG = g * a + bg * (1 - a)
const blendedB = b * a + bg * (1 - a)
return rgbToHex(blendedR, blendedG, blendedB)
})
}
export function mixColors(color1: HexColor, color2: HexColor, amount: number): HexColor {
const c1 = hexToOklch(color1)
const c2 = hexToOklch(color2)
return oklchToHex({
l: c1.l + (c2.l - c1.l) * amount,
c: c1.c + (c2.c - c1.c) * amount,
h: c1.h + (c2.h - c1.h) * amount,
})
}
export function lighten(color: HexColor, amount: number): HexColor {
const oklch = hexToOklch(color)
return oklchToHex({
...oklch,
l: Math.min(1, oklch.l + amount),
})
}
export function darken(color: HexColor, amount: number): HexColor {
const oklch = hexToOklch(color)
return oklchToHex({
...oklch,
l: Math.max(0, oklch.l - amount),
})
}
export function withAlpha(color: HexColor, alpha: number): string {
const { r, g, b } = hexToRgb(color)
return `rgba(${Math.round(r * 255)}, ${Math.round(g * 255)}, ${Math.round(b * 255)}, ${alpha})`
}
+180
View File
@@ -0,0 +1,180 @@
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"
const STORAGE_KEYS = {
THEME_ID: "opencode-theme-id",
COLOR_SCHEME: "opencode-color-scheme",
THEME_CSS_LIGHT: "opencode-theme-css-light",
THEME_CSS_DARK: "opencode-theme-css-dark",
} as const
const THEME_STYLE_ID = "oc-theme"
function ensureThemeStyleElement(): HTMLStyleElement {
const existing = document.getElementById(THEME_STYLE_ID) as HTMLStyleElement | null
if (existing) return existing
const element = document.createElement("style")
element.id = THEME_STYLE_ID
document.head.appendChild(element)
return element
}
function getSystemMode(): "light" | "dark" {
return window.matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light"
}
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") {
try {
localStorage.setItem(isDark ? STORAGE_KEYS.THEME_CSS_DARK : STORAGE_KEYS.THEME_CSS_LIGHT, css)
} catch {}
}
const fullCss = `:root {
color-scheme: ${mode};
--text-mix-blend-mode: ${isDark ? "plus-lighter" : "multiply"};
${css}
}`
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) {
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)
try {
localStorage.setItem(isDark ? STORAGE_KEYS.THEME_CSS_DARK : STORAGE_KEYS.THEME_CSS_LIGHT, css)
} catch {}
}
}
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))
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 setColorScheme = (scheme: ColorScheme) => {
setStore("colorScheme", scheme)
localStorage.setItem(STORAGE_KEYS.COLOR_SCHEME, scheme)
setStore("mode", scheme === "system" ? getSystemMode() : scheme)
}
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)
}
},
}
},
})
+35
View File
@@ -0,0 +1,35 @@
import type { DesktopTheme } from "./types"
import oc1ThemeJson from "./themes/oc-1.json"
import tokyoThemeJson from "./themes/tokyonight.json"
import draculaThemeJson from "./themes/dracula.json"
import monokaiThemeJson from "./themes/monokai.json"
import solarizedThemeJson from "./themes/solarized.json"
import nordThemeJson from "./themes/nord.json"
import catppuccinThemeJson from "./themes/catppuccin.json"
import ayuThemeJson from "./themes/ayu.json"
import oneDarkProThemeJson from "./themes/onedarkpro.json"
import shadesOfPurpleThemeJson from "./themes/shadesofpurple.json"
export const oc1Theme = oc1ThemeJson as DesktopTheme
export const tokyonightTheme = tokyoThemeJson as DesktopTheme
export const draculaTheme = draculaThemeJson as DesktopTheme
export const monokaiTheme = monokaiThemeJson as DesktopTheme
export const solarizedTheme = solarizedThemeJson as DesktopTheme
export const nordTheme = nordThemeJson as DesktopTheme
export const catppuccinTheme = catppuccinThemeJson as DesktopTheme
export const ayuTheme = ayuThemeJson as DesktopTheme
export const oneDarkProTheme = oneDarkProThemeJson as DesktopTheme
export const shadesOfPurpleTheme = shadesOfPurpleThemeJson as DesktopTheme
export const DEFAULT_THEMES: Record<string, DesktopTheme> = {
"oc-1": oc1Theme,
tokyonight: tokyonightTheme,
dracula: draculaTheme,
monokai: monokaiTheme,
solarized: solarizedTheme,
nord: nordTheme,
catppuccin: catppuccinTheme,
ayu: ayuTheme,
onedarkpro: oneDarkProTheme,
shadesofpurple: shadesOfPurpleTheme,
}
@@ -0,0 +1,104 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"$id": "https://opencode.ai/desktop-theme.json",
"title": "OpenCode Desktop Theme",
"description": "A theme definition for the OpenCode desktop application",
"type": "object",
"required": ["name", "id", "light", "dark"],
"properties": {
"$schema": {
"type": "string",
"description": "JSON Schema reference"
},
"name": {
"type": "string",
"description": "Human-readable theme name"
},
"id": {
"type": "string",
"description": "Unique theme identifier (slug)",
"pattern": "^[a-z0-9-]+$"
},
"light": {
"$ref": "#/definitions/ThemeVariant",
"description": "Light mode color variant"
},
"dark": {
"$ref": "#/definitions/ThemeVariant",
"description": "Dark mode color variant"
}
},
"definitions": {
"HexColor": {
"type": "string",
"pattern": "^#([0-9a-fA-F]{3}|[0-9a-fA-F]{4}|[0-9a-fA-F]{6}|[0-9a-fA-F]{8})$",
"description": "A hex color value like #fff, #ffff, #ffffff, or #ffffffff"
},
"ColorValue": {
"type": "string",
"pattern": "^(#([0-9a-fA-F]{3}|[0-9a-fA-F]{4}|[0-9a-fA-F]{6}|[0-9a-fA-F]{8})|var\(--[a-z0-9-]+\))$",
"description": "Either a hex color value (#rgb/#rgba/#rrggbb/#rrggbbaa) or a CSS variable reference"
},
"ThemeSeedColors": {
"type": "object",
"description": "The minimum set of colors needed to generate a theme",
"required": ["neutral", "primary", "success", "warning", "error", "info", "interactive", "diffAdd", "diffDelete"],
"properties": {
"neutral": {
"$ref": "#/definitions/HexColor",
"description": "Base neutral color for generating the gray scale"
},
"primary": {
"$ref": "#/definitions/HexColor",
"description": "Primary brand/accent color"
},
"success": {
"$ref": "#/definitions/HexColor",
"description": "Success state color (typically green)"
},
"warning": {
"$ref": "#/definitions/HexColor",
"description": "Warning state color (typically yellow/orange)"
},
"error": {
"$ref": "#/definitions/HexColor",
"description": "Error/critical state color (typically red)"
},
"info": {
"$ref": "#/definitions/HexColor",
"description": "Informational state color (typically purple/blue)"
},
"interactive": {
"$ref": "#/definitions/HexColor",
"description": "Interactive element color (links, buttons)"
},
"diffAdd": {
"$ref": "#/definitions/HexColor",
"description": "Color for diff additions"
},
"diffDelete": {
"$ref": "#/definitions/HexColor",
"description": "Color for diff deletions"
}
}
},
"ThemeVariant": {
"type": "object",
"description": "A theme variant (light or dark) with seed colors and optional overrides",
"required": ["seeds"],
"properties": {
"seeds": {
"$ref": "#/definitions/ThemeSeedColors",
"description": "Seed colors used to generate the full palette"
},
"overrides": {
"type": "object",
"description": "Optional direct overrides for any CSS variable (without -- prefix)",
"additionalProperties": {
"$ref": "#/definitions/ColorValue"
}
}
}
}
}
}
+44
View File
@@ -0,0 +1,44 @@
export type {
DesktopTheme,
ThemeSeedColors,
ThemeVariant,
HexColor,
OklchColor,
ResolvedTheme,
ColorValue,
CssVarRef,
} from "./types"
export {
hexToRgb,
rgbToHex,
hexToOklch,
oklchToHex,
rgbToOklch,
oklchToRgb,
generateScale,
generateNeutralScale,
generateAlphaScale,
mixColors,
lighten,
darken,
withAlpha,
} from "./color"
export { resolveThemeVariant, resolveTheme, themeToCss } from "./resolve"
export { applyTheme, loadThemeFromUrl, getActiveTheme, removeTheme, setColorScheme } from "./loader"
export { ThemeProvider, useTheme, type ColorScheme } from "./context"
export {
DEFAULT_THEMES,
oc1Theme,
tokyonightTheme,
draculaTheme,
monokaiTheme,
solarizedTheme,
nordTheme,
catppuccinTheme,
ayuTheme,
oneDarkProTheme,
shadesOfPurpleTheme,
} from "./default-themes"
+103
View File
@@ -0,0 +1,103 @@
import type { DesktopTheme, ResolvedTheme } from "./types"
import { resolveThemeVariant, themeToCss } from "./resolve"
let activeTheme: DesktopTheme | null = null
const THEME_STYLE_ID = "opencode-theme"
function ensureLoaderStyleElement(): HTMLStyleElement {
const existing = document.getElementById(THEME_STYLE_ID) as HTMLStyleElement | null
if (existing) {
return existing
}
const element = document.createElement("style")
element.id = THEME_STYLE_ID
document.head.appendChild(element)
return element
}
export function applyTheme(theme: DesktopTheme, themeId?: string): void {
activeTheme = theme
const lightTokens = resolveThemeVariant(theme.light, false)
const darkTokens = resolveThemeVariant(theme.dark, true)
const targetThemeId = themeId ?? theme.id
const css = buildThemeCss(lightTokens, darkTokens, targetThemeId)
const themeStyleElement = ensureLoaderStyleElement()
themeStyleElement.textContent = css
document.documentElement.setAttribute("data-theme", targetThemeId)
}
function buildThemeCss(light: ResolvedTheme, dark: ResolvedTheme, themeId: string): string {
const isDefaultTheme = themeId === "oc-1"
const lightCss = themeToCss(light)
const darkCss = themeToCss(dark)
if (isDefaultTheme) {
return `
:root {
color-scheme: light;
--text-mix-blend-mode: multiply;
${lightCss}
@media (prefers-color-scheme: dark) {
color-scheme: dark;
--text-mix-blend-mode: plus-lighter;
${darkCss}
}
}
`
}
return `
html[data-theme="${themeId}"] {
color-scheme: light;
--text-mix-blend-mode: multiply;
${lightCss}
@media (prefers-color-scheme: dark) {
color-scheme: dark;
--text-mix-blend-mode: plus-lighter;
${darkCss}
}
}
`
}
export async function loadThemeFromUrl(url: string): Promise<DesktopTheme> {
const response = await fetch(url)
if (!response.ok) {
throw new Error(`Failed to load theme from ${url}: ${response.statusText}`)
}
return response.json()
}
export function getActiveTheme(): DesktopTheme | null {
const activeId = document.documentElement.getAttribute("data-theme")
if (!activeId) {
return null
}
if (activeTheme?.id === activeId) {
return activeTheme
}
return null
}
export function removeTheme(): void {
activeTheme = null
const existingElement = document.getElementById(THEME_STYLE_ID)
if (existingElement) {
existingElement.remove()
}
document.documentElement.removeAttribute("data-theme")
}
export function setColorScheme(scheme: "light" | "dark" | "auto"): void {
if (scheme === "auto") {
document.documentElement.style.removeProperty("color-scheme")
} else {
document.documentElement.style.setProperty("color-scheme", scheme)
}
}
+325
View File
@@ -0,0 +1,325 @@
import type { ColorValue, DesktopTheme, HexColor, ResolvedTheme, ThemeVariant } from "./types"
import { generateNeutralScale, generateScale, hexToOklch, oklchToHex, withAlpha } from "./color"
export function resolveThemeVariant(variant: ThemeVariant, isDark: boolean): ResolvedTheme {
const { seeds, overrides = {} } = variant
const neutral = generateNeutralScale(seeds.neutral, isDark)
const primary = generateScale(seeds.primary, isDark)
const success = generateScale(seeds.success, isDark)
const warning = generateScale(seeds.warning, isDark)
const error = generateScale(seeds.error, isDark)
const info = generateScale(seeds.info, isDark)
const interactive = generateScale(seeds.interactive, isDark)
const diffAdd = generateScale(seeds.diffAdd, isDark)
const diffDelete = generateScale(seeds.diffDelete, isDark)
const neutralAlpha = generateNeutralAlphaScale(neutral, isDark)
const tokens: ResolvedTheme = {}
tokens["background-base"] = neutral[0]
tokens["background-weak"] = neutral[2]
tokens["background-strong"] = neutral[0]
tokens["background-stronger"] = isDark ? neutral[1] : "#fcfcfc"
tokens["surface-base"] = neutralAlpha[1]
tokens["base"] = neutralAlpha[1]
tokens["surface-base-hover"] = neutralAlpha[2]
tokens["surface-base-active"] = neutralAlpha[2]
tokens["surface-base-interactive-active"] = withAlpha(interactive[2], 0.3) as ColorValue
tokens["base2"] = neutralAlpha[1]
tokens["base3"] = neutralAlpha[1]
tokens["surface-inset-base"] = neutralAlpha[1]
tokens["surface-inset-base-hover"] = neutralAlpha[2]
tokens["surface-inset-strong"] = isDark
? (withAlpha(neutral[0], 0.5) as ColorValue)
: (withAlpha(neutral[3], 0.09) as ColorValue)
tokens["surface-inset-strong-hover"] = tokens["surface-inset-strong"]
tokens["surface-raised-base"] = neutralAlpha[0]
tokens["surface-float-base"] = isDark ? neutral[0] : neutral[11]
tokens["surface-float-base-hover"] = isDark ? neutral[1] : neutral[10]
tokens["surface-raised-base-hover"] = neutralAlpha[1]
tokens["surface-raised-base-active"] = neutralAlpha[2]
tokens["surface-raised-strong"] = isDark ? neutralAlpha[3] : neutral[0]
tokens["surface-raised-strong-hover"] = isDark ? neutralAlpha[5] : "#ffffff"
tokens["surface-raised-stronger"] = isDark ? neutralAlpha[5] : "#ffffff"
tokens["surface-raised-stronger-hover"] = isDark ? neutralAlpha[6] : "#ffffff"
tokens["surface-weak"] = neutralAlpha[2]
tokens["surface-weaker"] = neutralAlpha[3]
tokens["surface-strong"] = isDark ? neutralAlpha[6] : "#ffffff"
tokens["surface-raised-stronger-non-alpha"] = isDark ? neutral[2] : "#ffffff"
tokens["surface-brand-base"] = primary[8]
tokens["surface-brand-hover"] = primary[9]
tokens["surface-interactive-base"] = interactive[2]
tokens["surface-interactive-hover"] = interactive[3]
tokens["surface-interactive-weak"] = interactive[1]
tokens["surface-interactive-weak-hover"] = interactive[2]
tokens["surface-success-base"] = success[2]
tokens["surface-success-weak"] = success[1]
tokens["surface-success-strong"] = success[8]
tokens["surface-warning-base"] = warning[2]
tokens["surface-warning-weak"] = warning[1]
tokens["surface-warning-strong"] = warning[8]
tokens["surface-critical-base"] = error[2]
tokens["surface-critical-weak"] = error[1]
tokens["surface-critical-strong"] = error[8]
tokens["surface-info-base"] = info[2]
tokens["surface-info-weak"] = info[1]
tokens["surface-info-strong"] = info[8]
tokens["surface-diff-unchanged-base"] = isDark ? neutral[0] : "#ffffff00"
tokens["surface-diff-skip-base"] = isDark ? neutralAlpha[0] : neutral[1]
tokens["surface-diff-hidden-base"] = interactive[isDark ? 1 : 2]
tokens["surface-diff-hidden-weak"] = interactive[isDark ? 0 : 1]
tokens["surface-diff-hidden-weaker"] = interactive[isDark ? 2 : 0]
tokens["surface-diff-hidden-strong"] = interactive[4]
tokens["surface-diff-hidden-stronger"] = interactive[isDark ? 10 : 8]
tokens["surface-diff-add-base"] = diffAdd[2]
tokens["surface-diff-add-weak"] = diffAdd[isDark ? 3 : 1]
tokens["surface-diff-add-weaker"] = diffAdd[isDark ? 2 : 0]
tokens["surface-diff-add-strong"] = diffAdd[4]
tokens["surface-diff-add-stronger"] = diffAdd[isDark ? 10 : 8]
tokens["surface-diff-delete-base"] = diffDelete[2]
tokens["surface-diff-delete-weak"] = diffDelete[isDark ? 3 : 1]
tokens["surface-diff-delete-weaker"] = diffDelete[isDark ? 2 : 0]
tokens["surface-diff-delete-strong"] = diffDelete[isDark ? 4 : 5]
tokens["surface-diff-delete-stronger"] = diffDelete[isDark ? 10 : 8]
tokens["input-base"] = isDark ? neutral[1] : neutral[0]
tokens["input-hover"] = neutral[1]
tokens["input-active"] = interactive[0]
tokens["input-selected"] = interactive[3]
tokens["input-focus"] = interactive[0]
tokens["input-disabled"] = neutral[3]
tokens["text-base"] = neutral[10]
tokens["text-weak"] = neutral[8]
tokens["text-weaker"] = neutral[7]
tokens["text-strong"] = neutral[11]
tokens["text-invert-base"] = isDark ? neutral[10] : neutralAlpha[10]
tokens["text-invert-weak"] = isDark ? neutral[8] : neutralAlpha[8]
tokens["text-invert-weaker"] = isDark ? neutral[7] : neutralAlpha[7]
tokens["text-invert-strong"] = isDark ? neutral[11] : neutralAlpha[11]
tokens["text-interactive-base"] = interactive[isDark ? 10 : 8]
tokens["text-on-brand-base"] = neutralAlpha[10]
tokens["text-on-interactive-base"] = isDark ? neutral[11] : neutral[0]
tokens["text-on-interactive-weak"] = neutralAlpha[10]
tokens["text-on-success-base"] = success[isDark ? 8 : 9]
tokens["text-on-critical-base"] = error[isDark ? 8 : 9]
tokens["text-on-critical-weak"] = error[7]
tokens["text-on-critical-strong"] = error[11]
tokens["text-on-warning-base"] = neutralAlpha[10]
tokens["text-on-info-base"] = neutralAlpha[10]
tokens["text-diff-add-base"] = diffAdd[10]
tokens["text-diff-delete-base"] = diffDelete[isDark ? 8 : 9]
tokens["text-diff-delete-strong"] = diffDelete[11]
tokens["text-diff-add-strong"] = diffAdd[isDark ? 7 : 11]
tokens["text-on-info-weak"] = neutralAlpha[8]
tokens["text-on-info-strong"] = neutralAlpha[11]
tokens["text-on-warning-weak"] = neutralAlpha[8]
tokens["text-on-warning-strong"] = neutralAlpha[11]
tokens["text-on-success-weak"] = success[isDark ? 7 : 5]
tokens["text-on-success-strong"] = success[11]
tokens["text-on-brand-weak"] = neutralAlpha[8]
tokens["text-on-brand-weaker"] = neutralAlpha[7]
tokens["text-on-brand-strong"] = neutralAlpha[11]
tokens["button-secondary-base"] = isDark ? neutral[2] : neutral[0]
tokens["button-secondary-hover"] = isDark ? neutral[3] : neutral[1]
tokens["button-ghost-hover"] = neutralAlpha[1]
tokens["button-ghost-hover2"] = neutralAlpha[2]
tokens["border-base"] = neutralAlpha[6]
tokens["border-hover"] = neutralAlpha[7]
tokens["border-active"] = neutralAlpha[8]
tokens["border-selected"] = withAlpha(interactive[8], isDark ? 0.9 : 0.99) as ColorValue
tokens["border-disabled"] = neutralAlpha[7]
tokens["border-focus"] = neutralAlpha[8]
tokens["border-weak-base"] = neutralAlpha[isDark ? 5 : 4]
tokens["border-strong-base"] = neutralAlpha[isDark ? 7 : 6]
tokens["border-strong-hover"] = neutralAlpha[7]
tokens["border-strong-active"] = neutralAlpha[isDark ? 7 : 6]
tokens["border-strong-selected"] = withAlpha(interactive[5], 0.6) as ColorValue
tokens["border-strong-disabled"] = neutralAlpha[5]
tokens["border-strong-focus"] = neutralAlpha[isDark ? 7 : 6]
tokens["border-weak-hover"] = neutralAlpha[isDark ? 6 : 5]
tokens["border-weak-active"] = neutralAlpha[isDark ? 7 : 6]
tokens["border-weak-selected"] = withAlpha(interactive[4], isDark ? 0.6 : 0.5) as ColorValue
tokens["border-weak-disabled"] = neutralAlpha[5]
tokens["border-weak-focus"] = neutralAlpha[isDark ? 7 : 6]
tokens["border-weaker-base"] = neutralAlpha[2]
tokens["border-weaker-hover"] = neutralAlpha[3]
tokens["border-weaker-active"] = neutralAlpha[5]
tokens["border-weaker-selected"] = withAlpha(interactive[3], isDark ? 0.3 : 0.4) as ColorValue
tokens["border-weaker-disabled"] = neutralAlpha[1]
tokens["border-weaker-focus"] = neutralAlpha[5]
tokens["border-interactive-base"] = interactive[6]
tokens["border-interactive-hover"] = interactive[7]
tokens["border-interactive-active"] = interactive[8]
tokens["border-interactive-selected"] = interactive[8]
tokens["border-interactive-disabled"] = neutral[7]
tokens["border-interactive-focus"] = interactive[8]
tokens["border-success-base"] = success[5]
tokens["border-success-hover"] = success[6]
tokens["border-success-selected"] = success[8]
tokens["border-warning-base"] = warning[5]
tokens["border-warning-hover"] = warning[6]
tokens["border-warning-selected"] = warning[8]
tokens["border-critical-base"] = error[isDark ? 4 : 5]
tokens["border-critical-hover"] = error[6]
tokens["border-critical-selected"] = error[8]
tokens["border-info-base"] = info[5]
tokens["border-info-hover"] = info[6]
tokens["border-info-selected"] = info[8]
tokens["border-color"] = "#ffffff"
tokens["icon-base"] = neutral[8]
tokens["icon-hover"] = neutral[isDark ? 9 : 10]
tokens["icon-active"] = neutral[isDark ? 10 : 11]
tokens["icon-selected"] = neutral[11]
tokens["icon-disabled"] = neutral[isDark ? 6 : 7]
tokens["icon-focus"] = neutral[11]
tokens["icon-invert-base"] = isDark ? neutral[0] : "#ffffff"
tokens["icon-weak-base"] = neutral[isDark ? 5 : 6]
tokens["icon-weak-hover"] = neutral[6]
tokens["icon-weak-active"] = neutral[7]
tokens["icon-weak-selected"] = neutral[8]
tokens["icon-weak-disabled"] = neutral[isDark ? 3 : 5]
tokens["icon-weak-focus"] = neutral[8]
tokens["icon-strong-base"] = neutral[11]
tokens["icon-strong-hover"] = isDark ? "#f6f3f3" : "#151313"
tokens["icon-strong-active"] = isDark ? "#fcfcfc" : "#020202"
tokens["icon-strong-selected"] = isDark ? "#fdfcfc" : "#020202"
tokens["icon-strong-disabled"] = neutral[7]
tokens["icon-strong-focus"] = isDark ? "#fdfcfc" : "#020202"
tokens["icon-brand-base"] = isDark ? "#ffffff" : neutral[11]
tokens["icon-interactive-base"] = interactive[8]
tokens["icon-success-base"] = success[isDark ? 6 : 6]
tokens["icon-success-hover"] = success[7]
tokens["icon-success-active"] = success[10]
tokens["icon-warning-base"] = warning[6]
tokens["icon-warning-hover"] = warning[7]
tokens["icon-warning-active"] = warning[10]
tokens["icon-critical-base"] = error[isDark ? 8 : 9]
tokens["icon-critical-hover"] = error[10]
tokens["icon-critical-active"] = error[11]
tokens["icon-info-base"] = info[isDark ? 6 : 6]
tokens["icon-info-hover"] = info[7]
tokens["icon-info-active"] = info[10]
tokens["icon-on-brand-base"] = neutralAlpha[10]
tokens["icon-on-brand-hover"] = neutralAlpha[11]
tokens["icon-on-brand-selected"] = neutralAlpha[11]
tokens["icon-on-interactive-base"] = isDark ? neutral[11] : neutral[0]
tokens["icon-agent-plan-base"] = info[8]
tokens["icon-agent-docs-base"] = warning[8]
tokens["icon-agent-ask-base"] = interactive[8]
tokens["icon-agent-build-base"] = interactive[isDark ? 10 : 8]
tokens["icon-on-success-base"] = withAlpha(success[8], 0.9) as ColorValue
tokens["icon-on-success-hover"] = withAlpha(success[9], 0.9) as ColorValue
tokens["icon-on-success-selected"] = withAlpha(success[10], 0.9) as ColorValue
tokens["icon-on-warning-base"] = withAlpha(warning[8], 0.9) as ColorValue
tokens["icon-on-warning-hover"] = withAlpha(warning[9], 0.9) as ColorValue
tokens["icon-on-warning-selected"] = withAlpha(warning[10], 0.9) as ColorValue
tokens["icon-on-critical-base"] = withAlpha(error[8], 0.9) as ColorValue
tokens["icon-on-critical-hover"] = withAlpha(error[9], 0.9) as ColorValue
tokens["icon-on-critical-selected"] = withAlpha(error[10], 0.9) as ColorValue
tokens["icon-on-info-base"] = info[8]
tokens["icon-on-info-hover"] = withAlpha(info[9], 0.9) as ColorValue
tokens["icon-on-info-selected"] = withAlpha(info[10], 0.9) as ColorValue
tokens["icon-diff-add-base"] = diffAdd[10]
tokens["icon-diff-add-hover"] = diffAdd[isDark ? 9 : 11]
tokens["icon-diff-add-active"] = diffAdd[isDark ? 10 : 11]
tokens["icon-diff-delete-base"] = diffDelete[isDark ? 8 : 9]
tokens["icon-diff-delete-hover"] = diffDelete[isDark ? 9 : 10]
tokens["syntax-comment"] = "var(--text-weak)"
tokens["syntax-regexp"] = "var(--text-base)"
tokens["syntax-string"] = isDark ? "#00ceb9" : "#006656"
tokens["syntax-keyword"] = "var(--text-weak)"
tokens["syntax-primitive"] = isDark ? "#ffba92" : "#fb4804"
tokens["syntax-operator"] = isDark ? "var(--text-weak)" : "var(--text-base)"
tokens["syntax-variable"] = "var(--text-strong)"
tokens["syntax-property"] = isDark ? "#ff9ae2" : "#ed6dc8"
tokens["syntax-type"] = isDark ? "#ecf58c" : "#596600"
tokens["syntax-constant"] = isDark ? "#93e9f6" : "#007b80"
tokens["syntax-punctuation"] = isDark ? "var(--text-weak)" : "var(--text-base)"
tokens["syntax-object"] = "var(--text-strong)"
tokens["syntax-success"] = success[9]
tokens["syntax-warning"] = warning[9]
tokens["syntax-critical"] = error[isDark ? 9 : 9]
tokens["syntax-info"] = isDark ? "#93e9f6" : "#0092a8"
tokens["syntax-diff-add"] = diffAdd[10]
tokens["syntax-diff-delete"] = diffDelete[10]
tokens["syntax-diff-unknown"] = "#ff0000"
tokens["markdown-heading"] = isDark ? "#9d7cd8" : "#d68c27"
tokens["markdown-text"] = isDark ? "#eeeeee" : "#1a1a1a"
tokens["markdown-link"] = isDark ? "#fab283" : "#3b7dd8"
tokens["markdown-link-text"] = isDark ? "#56b6c2" : "#318795"
tokens["markdown-code"] = isDark ? "#7fd88f" : "#3d9a57"
tokens["markdown-block-quote"] = isDark ? "#e5c07b" : "#b0851f"
tokens["markdown-emph"] = isDark ? "#e5c07b" : "#b0851f"
tokens["markdown-strong"] = isDark ? "#f5a742" : "#d68c27"
tokens["markdown-horizontal-rule"] = isDark ? "#808080" : "#8a8a8a"
tokens["markdown-list-item"] = isDark ? "#fab283" : "#3b7dd8"
tokens["markdown-list-enumeration"] = isDark ? "#56b6c2" : "#318795"
tokens["markdown-image"] = isDark ? "#fab283" : "#3b7dd8"
tokens["markdown-image-text"] = isDark ? "#56b6c2" : "#318795"
tokens["markdown-code-block"] = isDark ? "#eeeeee" : "#1a1a1a"
tokens["avatar-background-pink"] = isDark ? "#501b3f" : "#feeef8"
tokens["avatar-background-mint"] = isDark ? "#033a34" : "#e1fbf4"
tokens["avatar-background-orange"] = isDark ? "#5f2a06" : "#fff1e7"
tokens["avatar-background-purple"] = isDark ? "#432155" : "#f9f1fe"
tokens["avatar-background-cyan"] = isDark ? "#0f3058" : "#e7f9fb"
tokens["avatar-background-lime"] = isDark ? "#2b3711" : "#eefadc"
tokens["avatar-text-pink"] = isDark ? "#e34ba9" : "#cd1d8d"
tokens["avatar-text-mint"] = isDark ? "#95f3d9" : "#147d6f"
tokens["avatar-text-orange"] = isDark ? "#ff802b" : "#ed5f00"
tokens["avatar-text-purple"] = isDark ? "#9d5bd2" : "#8445bc"
tokens["avatar-text-cyan"] = isDark ? "#369eff" : "#0894b3"
tokens["avatar-text-lime"] = isDark ? "#c4f042" : "#5d770d"
for (const [key, value] of Object.entries(overrides)) {
tokens[key] = value
}
return tokens
}
function generateNeutralAlphaScale(neutralScale: HexColor[], isDark: boolean): HexColor[] {
const alphas = isDark
? [0.02, 0.04, 0.08, 0.12, 0.16, 0.2, 0.26, 0.36, 0.44, 0.52, 0.72, 0.94]
: [0.01, 0.03, 0.06, 0.09, 0.12, 0.15, 0.2, 0.27, 0.46, 0.61, 0.5, 0.87]
return neutralScale.map((hex, i) => {
const baseOklch = hexToOklch(hex)
const targetL = isDark ? 0.1 + alphas[i] * 0.8 : 1 - alphas[i] * 0.8
return oklchToHex({
...baseOklch,
l: baseOklch.l * alphas[i] + targetL * (1 - alphas[i]),
})
})
}
export function resolveTheme(theme: DesktopTheme): { light: ResolvedTheme; dark: ResolvedTheme } {
return {
light: resolveThemeVariant(theme.light, false),
dark: resolveThemeVariant(theme.dark, true),
}
}
export function themeToCss(tokens: ResolvedTheme): string {
return Object.entries(tokens)
.map(([key, value]) => `--${key}: ${value};`)
.join("\n ")
}
+131
View File
@@ -0,0 +1,131 @@
{
"$schema": "https://opencode.ai/desktop-theme.json",
"name": "Ayu",
"id": "ayu",
"light": {
"seeds": {
"neutral": "#fdfaf4",
"primary": "#55b4d4",
"success": "#6ac782",
"warning": "#f2ae49",
"error": "#f05f65",
"info": "#36a3d9",
"interactive": "#55b4d4",
"diffAdd": "#b8df8a",
"diffDelete": "#f05f65"
},
"overrides": {
"background-base": "#fdfaf4",
"background-weak": "#f6f0e7",
"background-strong": "#f1ebe2",
"background-stronger": "#ece4da",
"border-weak-base": "#e6ddcf",
"border-weak-hover": "#dcd3c5",
"border-weak-active": "#d1c9ba",
"border-weak-selected": "#c6bfaf",
"border-weak-disabled": "#f7f0e6",
"border-weak-focus": "#cbc4b6",
"border-base": "#bfb3a3",
"border-hover": "#b4a898",
"border-active": "#a99e8e",
"border-selected": "#9e9383",
"border-disabled": "#efe5d8",
"border-focus": "#b09f8f",
"border-strong-base": "#8f806f",
"border-strong-hover": "#837465",
"border-strong-active": "#77685a",
"border-strong-selected": "#6b5d51",
"border-strong-disabled": "#d8cabc",
"border-strong-focus": "#7c6d5e",
"surface-diff-add-base": "#eef5e4",
"surface-diff-delete-base": "#fde5e5",
"surface-diff-hidden-base": "#e3edf3",
"text-base": "#5c6773",
"text-weak": "#8a939f",
"text-strong": "#2a3038",
"syntax-string": "#86b300",
"syntax-primitive": "#f28779",
"syntax-property": "#55b4d4",
"syntax-type": "#f29e32",
"syntax-constant": "#36a3d9",
"syntax-info": "#36a3d9",
"markdown-heading": "#55b4d4",
"markdown-text": "#5c6773",
"markdown-link": "#55b4d4",
"markdown-link-text": "#36a3d9",
"markdown-code": "#86b300",
"markdown-block-quote": "#f29e32",
"markdown-emph": "#f29e32",
"markdown-strong": "#f28779",
"markdown-horizontal-rule": "#d7cec0",
"markdown-list-item": "#55b4d4",
"markdown-list-enumeration": "#36a3d9",
"markdown-image": "#55b4d4",
"markdown-image-text": "#36a3d9",
"markdown-code-block": "#55b4d4"
}
},
"dark": {
"seeds": {
"neutral": "#0f1419",
"primary": "#39bae6",
"success": "#7fd962",
"warning": "#ebb062",
"error": "#ff8f77",
"info": "#73d0ff",
"interactive": "#39bae6",
"diffAdd": "#5cc885",
"diffDelete": "#ff8f77"
},
"overrides": {
"background-base": "#0f1419",
"background-weak": "#121920",
"background-strong": "#0d1116",
"background-stronger": "#0a0e13",
"border-weak-base": "#262c34",
"border-weak-hover": "#2b323d",
"border-weak-active": "#303746",
"border-weak-selected": "#363d50",
"border-weak-disabled": "#080b0f",
"border-weak-focus": "#323a48",
"border-base": "#3d4555",
"border-hover": "#454d61",
"border-active": "#4c556d",
"border-selected": "#545d79",
"border-disabled": "#0e1218",
"border-focus": "#495368",
"border-strong-base": "#626c81",
"border-strong-hover": "#6c7690",
"border-strong-active": "#76819f",
"border-strong-selected": "#808bae",
"border-strong-disabled": "#151b23",
"border-strong-focus": "#6f7a96",
"surface-diff-add-base": "#102922",
"surface-diff-delete-base": "#2b1718",
"surface-diff-hidden-base": "#182028",
"text-base": "#ced0d6",
"text-weak": "#8f9aa5",
"text-strong": "#f6f7f9",
"syntax-string": "#b8cc52",
"syntax-primitive": "#f59074",
"syntax-property": "#39bae6",
"syntax-type": "#ebb062",
"syntax-constant": "#73d0ff",
"syntax-info": "#73d0ff",
"markdown-heading": "#39bae6",
"markdown-text": "#ced0d6",
"markdown-link": "#39bae6",
"markdown-link-text": "#73d0ff",
"markdown-code": "#b8cc52",
"markdown-block-quote": "#ebb062",
"markdown-emph": "#ebb062",
"markdown-strong": "#f59074",
"markdown-horizontal-rule": "#1f2630",
"markdown-list-item": "#39bae6",
"markdown-list-enumeration": "#73d0ff",
"markdown-image": "#39bae6",
"markdown-image-text": "#73d0ff",
"markdown-code-block": "#ced0d6"
}
}
}
@@ -0,0 +1,131 @@
{
"$schema": "https://opencode.ai/desktop-theme.json",
"name": "Catppuccin",
"id": "catppuccin",
"light": {
"seeds": {
"neutral": "#f5e0dc",
"primary": "#7287fd",
"success": "#40a02b",
"warning": "#df8e1d",
"error": "#d20f39",
"info": "#04a5e5",
"interactive": "#7287fd",
"diffAdd": "#a6d189",
"diffDelete": "#e78284"
},
"overrides": {
"background-base": "#f5e0dc",
"background-weak": "#f2d8d4",
"background-strong": "#f9e8e4",
"background-stronger": "#fdeeee",
"border-weak-base": "#e0cfd3",
"border-weak-hover": "#d6c4c8",
"border-weak-active": "#cdb9be",
"border-weak-selected": "#c2aeb4",
"border-weak-disabled": "#fbeff2",
"border-weak-focus": "#c7b4ba",
"border-base": "#bca6b2",
"border-hover": "#b19ca8",
"border-active": "#a6929e",
"border-selected": "#9a8894",
"border-disabled": "#f3e4e7",
"border-focus": "#ab97a1",
"border-strong-base": "#83677f",
"border-strong-hover": "#775b73",
"border-strong-active": "#6b5068",
"border-strong-selected": "#5f465d",
"border-strong-disabled": "#d9c5cf",
"border-strong-focus": "#714f66",
"surface-diff-add-base": "#edf5e6",
"surface-diff-delete-base": "#fde1e3",
"surface-diff-hidden-base": "#e4e2f6",
"text-base": "#4c4f69",
"text-weak": "#6c6f85",
"text-strong": "#1f1f2a",
"syntax-string": "#40a02b",
"syntax-primitive": "#d20f39",
"syntax-property": "#7287fd",
"syntax-type": "#df8e1d",
"syntax-constant": "#04a5e5",
"syntax-info": "#04a5e5",
"markdown-heading": "#7287fd",
"markdown-text": "#4c4f69",
"markdown-link": "#7287fd",
"markdown-link-text": "#04a5e5",
"markdown-code": "#40a02b",
"markdown-block-quote": "#df8e1d",
"markdown-emph": "#df8e1d",
"markdown-strong": "#d20f39",
"markdown-horizontal-rule": "#d4c5cf",
"markdown-list-item": "#7287fd",
"markdown-list-enumeration": "#04a5e5",
"markdown-image": "#7287fd",
"markdown-image-text": "#04a5e5",
"markdown-code-block": "#7287fd"
}
},
"dark": {
"seeds": {
"neutral": "#1e1e2e",
"primary": "#b4befe",
"success": "#a6d189",
"warning": "#f4b8e4",
"error": "#f38ba8",
"info": "#89dceb",
"interactive": "#b4befe",
"diffAdd": "#94e2d5",
"diffDelete": "#f38ba8"
},
"overrides": {
"background-base": "#1e1e2e",
"background-weak": "#211f31",
"background-strong": "#1c1c29",
"background-stronger": "#191926",
"border-weak-base": "#35324a",
"border-weak-hover": "#393655",
"border-weak-active": "#403c61",
"border-weak-selected": "#47436d",
"border-weak-disabled": "#141426",
"border-weak-focus": "#3d3a63",
"border-base": "#4a4763",
"border-hover": "#524f70",
"border-active": "#5a577d",
"border-selected": "#625f8a",
"border-disabled": "#1b1a2c",
"border-focus": "#575379",
"border-strong-base": "#6e6a8c",
"border-strong-hover": "#787497",
"border-strong-active": "#8380a2",
"border-strong-selected": "#8d8bad",
"border-strong-disabled": "#232237",
"border-strong-focus": "#7b779b",
"surface-diff-add-base": "#1d2c30",
"surface-diff-delete-base": "#2c1f2a",
"surface-diff-hidden-base": "#232538",
"text-base": "#cdd6f4",
"text-weak": "#a6adc8",
"text-strong": "#f4f2ff",
"syntax-string": "#a6e3a1",
"syntax-primitive": "#f38ba8",
"syntax-property": "#b4befe",
"syntax-type": "#f9e2af",
"syntax-constant": "#89dceb",
"syntax-info": "#89dceb",
"markdown-heading": "#b4befe",
"markdown-text": "#cdd6f4",
"markdown-link": "#b4befe",
"markdown-link-text": "#89dceb",
"markdown-code": "#a6e3a1",
"markdown-block-quote": "#f9e2af",
"markdown-emph": "#f9e2af",
"markdown-strong": "#f38ba8",
"markdown-horizontal-rule": "#2e2d45",
"markdown-list-item": "#b4befe",
"markdown-list-enumeration": "#89dceb",
"markdown-image": "#b4befe",
"markdown-image-text": "#89dceb",
"markdown-code-block": "#cdd6f4"
}
}
}
+131
View File
@@ -0,0 +1,131 @@
{
"$schema": "https://opencode.ai/desktop-theme.json",
"name": "Dracula",
"id": "dracula",
"light": {
"seeds": {
"neutral": "#f8f8f2",
"primary": "#7c6bf5",
"success": "#2fbf71",
"warning": "#f7a14d",
"error": "#d9536f",
"info": "#1d7fc5",
"interactive": "#7c6bf5",
"diffAdd": "#9fe3b3",
"diffDelete": "#f8a1b8"
},
"overrides": {
"background-base": "#f8f8f2",
"background-weak": "#f1f2ed",
"background-strong": "#f6f6f1",
"background-stronger": "#f2f2ec",
"border-weak-base": "#e2e3da",
"border-weak-hover": "#d8d9d0",
"border-weak-active": "#cfd0c7",
"border-weak-selected": "#c4c6bc",
"border-weak-disabled": "#eceee3",
"border-weak-focus": "#c9cabf",
"border-base": "#c4c6ba",
"border-hover": "#b8baae",
"border-active": "#abada3",
"border-selected": "#979a90",
"border-disabled": "#e5e7dd",
"border-focus": "#b0b2a7",
"border-strong-base": "#9fa293",
"border-strong-hover": "#8e9185",
"border-strong-active": "#7e8176",
"border-strong-selected": "#6f7268",
"border-strong-disabled": "#c7c9be",
"border-strong-focus": "#878b7f",
"surface-diff-add-base": "#e4f5e6",
"surface-diff-delete-base": "#fae4eb",
"surface-diff-hidden-base": "#dedfe9",
"text-base": "#1f1f2f",
"text-weak": "#52526b",
"text-strong": "#05040c",
"syntax-string": "#2fbf71",
"syntax-primitive": "#d16090",
"syntax-property": "#7c6bf5",
"syntax-type": "#f7a14d",
"syntax-constant": "#1d7fc5",
"syntax-info": "#1d7fc5",
"markdown-heading": "#7c6bf5",
"markdown-text": "#1f1f2f",
"markdown-link": "#7c6bf5",
"markdown-link-text": "#1d7fc5",
"markdown-code": "#2fbf71",
"markdown-block-quote": "#f7a14d",
"markdown-emph": "#f7a14d",
"markdown-strong": "#d16090",
"markdown-horizontal-rule": "#c3c5d4",
"markdown-list-item": "#7c6bf5",
"markdown-list-enumeration": "#1d7fc5",
"markdown-image": "#7c6bf5",
"markdown-image-text": "#1d7fc5",
"markdown-code-block": "#1d7fc5"
}
},
"dark": {
"seeds": {
"neutral": "#1d1e28",
"primary": "#bd93f9",
"success": "#50fa7b",
"warning": "#ffb86c",
"error": "#ff5555",
"info": "#8be9fd",
"interactive": "#bd93f9",
"diffAdd": "#2fb27d",
"diffDelete": "#ff6b81"
},
"overrides": {
"background-base": "#14151f",
"background-weak": "#181926",
"background-strong": "#161722",
"background-stronger": "#191a26",
"border-weak-base": "#2d2f3c",
"border-weak-hover": "#303244",
"border-weak-active": "#35364c",
"border-weak-selected": "#3b3d55",
"border-weak-disabled": "#1e1f2b",
"border-weak-focus": "#383a50",
"border-base": "#3f415a",
"border-hover": "#464967",
"border-active": "#4d5073",
"border-selected": "#55587f",
"border-disabled": "#272834",
"border-focus": "#4a4d6d",
"border-strong-base": "#606488",
"border-strong-hover": "#6a6e96",
"border-strong-active": "#7378a3",
"border-strong-selected": "#7d82b1",
"border-strong-disabled": "#343649",
"border-strong-focus": "#6f739c",
"surface-diff-add-base": "#1f2a2f",
"surface-diff-delete-base": "#2d1f27",
"surface-diff-hidden-base": "#24253a",
"text-base": "#f8f8f2",
"text-weak": "#b6b9e4",
"text-strong": "#ffffff",
"syntax-string": "#50fa7b",
"syntax-primitive": "#ff79c6",
"syntax-property": "#bd93f9",
"syntax-type": "#ffb86c",
"syntax-constant": "#8be9fd",
"syntax-info": "#8be9fd",
"markdown-heading": "#bd93f9",
"markdown-text": "#f8f8f2",
"markdown-link": "#bd93f9",
"markdown-link-text": "#8be9fd",
"markdown-code": "#50fa7b",
"markdown-block-quote": "#ffb86c",
"markdown-emph": "#ffb86c",
"markdown-strong": "#ff79c6",
"markdown-horizontal-rule": "#44475a",
"markdown-list-item": "#bd93f9",
"markdown-list-enumeration": "#8be9fd",
"markdown-image": "#bd93f9",
"markdown-image-text": "#8be9fd",
"markdown-code-block": "#f8f8f2"
}
}
}
+131
View File
@@ -0,0 +1,131 @@
{
"$schema": "https://opencode.ai/desktop-theme.json",
"name": "Monokai",
"id": "monokai",
"light": {
"seeds": {
"neutral": "#fdf8ec",
"primary": "#bf7bff",
"success": "#4fb54b",
"warning": "#f1a948",
"error": "#e54b4b",
"info": "#2d9ad7",
"interactive": "#bf7bff",
"diffAdd": "#bfe7a3",
"diffDelete": "#f6a3ae"
},
"overrides": {
"background-base": "#fdf8ec",
"background-weak": "#f8f2e6",
"background-strong": "#fbf5e8",
"background-stronger": "#f7efdd",
"border-weak-base": "#e9e0cf",
"border-weak-hover": "#dfd5c3",
"border-weak-active": "#d5cab7",
"border-weak-selected": "#cabfad",
"border-weak-disabled": "#f3ebdd",
"border-weak-focus": "#d0c2b1",
"border-base": "#c7b9a5",
"border-hover": "#bcae98",
"border-active": "#b0a28c",
"border-selected": "#a49781",
"border-disabled": "#efe5d6",
"border-focus": "#b6a893",
"border-strong-base": "#998b76",
"border-strong-hover": "#8a7c67",
"border-strong-active": "#7a6d58",
"border-strong-selected": "#6c604c",
"border-strong-disabled": "#d7cabc",
"border-strong-focus": "#82745f",
"surface-diff-add-base": "#e8f7e1",
"surface-diff-delete-base": "#fde5e4",
"surface-diff-hidden-base": "#e9e0d0",
"text-base": "#292318",
"text-weak": "#6d5c40",
"text-strong": "#1c150c",
"syntax-string": "#4fb54b",
"syntax-primitive": "#d9487c",
"syntax-property": "#bf7bff",
"syntax-type": "#f1a948",
"syntax-constant": "#2d9ad7",
"syntax-info": "#2d9ad7",
"markdown-heading": "#bf7bff",
"markdown-text": "#292318",
"markdown-link": "#bf7bff",
"markdown-link-text": "#2d9ad7",
"markdown-code": "#4fb54b",
"markdown-block-quote": "#f1a948",
"markdown-emph": "#f1a948",
"markdown-strong": "#d9487c",
"markdown-horizontal-rule": "#cdbdab",
"markdown-list-item": "#bf7bff",
"markdown-list-enumeration": "#2d9ad7",
"markdown-image": "#bf7bff",
"markdown-image-text": "#2d9ad7",
"markdown-code-block": "#2d9ad7"
}
},
"dark": {
"seeds": {
"neutral": "#272822",
"primary": "#ae81ff",
"success": "#a6e22e",
"warning": "#fd971f",
"error": "#f92672",
"info": "#66d9ef",
"interactive": "#ae81ff",
"diffAdd": "#4d7f2a",
"diffDelete": "#f4477c"
},
"overrides": {
"background-base": "#23241e",
"background-weak": "#27281f",
"background-strong": "#25261f",
"background-stronger": "#292a23",
"border-weak-base": "#343528",
"border-weak-hover": "#393a2d",
"border-weak-active": "#3f4033",
"border-weak-selected": "#454639",
"border-weak-disabled": "#1d1e16",
"border-weak-focus": "#414235",
"border-base": "#494a3a",
"border-hover": "#50523f",
"border-active": "#585a45",
"border-selected": "#60624b",
"border-disabled": "#23241b",
"border-focus": "#555741",
"border-strong-base": "#6a6c55",
"border-strong-hover": "#73755d",
"border-strong-active": "#7d7f66",
"border-strong-selected": "#878970",
"border-strong-disabled": "#2c2d23",
"border-strong-focus": "#7a7c63",
"surface-diff-add-base": "#1e2a1d",
"surface-diff-delete-base": "#301c24",
"surface-diff-hidden-base": "#2f2f24",
"text-base": "#f8f8f2",
"text-weak": "#c5c5c0",
"text-strong": "#ffffff",
"syntax-string": "#a6e22e",
"syntax-primitive": "#f92672",
"syntax-property": "#ae81ff",
"syntax-type": "#fd971f",
"syntax-constant": "#66d9ef",
"syntax-info": "#66d9ef",
"markdown-heading": "#ae81ff",
"markdown-text": "#f8f8f2",
"markdown-link": "#ae81ff",
"markdown-link-text": "#66d9ef",
"markdown-code": "#a6e22e",
"markdown-block-quote": "#fd971f",
"markdown-emph": "#fd971f",
"markdown-strong": "#f92672",
"markdown-horizontal-rule": "#3b3c34",
"markdown-list-item": "#ae81ff",
"markdown-list-enumeration": "#66d9ef",
"markdown-image": "#ae81ff",
"markdown-image-text": "#66d9ef",
"markdown-code-block": "#f8f8f2"
}
}
}
+131
View File
@@ -0,0 +1,131 @@
{
"$schema": "https://opencode.ai/desktop-theme.json",
"name": "Nord",
"id": "nord",
"light": {
"seeds": {
"neutral": "#eceff4",
"primary": "#5e81ac",
"success": "#8fbcbb",
"warning": "#d08770",
"error": "#bf616a",
"info": "#81a1c1",
"interactive": "#5e81ac",
"diffAdd": "#a3be8c",
"diffDelete": "#bf616a"
},
"overrides": {
"background-base": "#eceff4",
"background-weak": "#e4e8f0",
"background-strong": "#f1f3f8",
"background-stronger": "#f6f8fc",
"border-weak-base": "#d5dbe7",
"border-weak-hover": "#c9d0de",
"border-weak-active": "#bec5d4",
"border-weak-selected": "#b2bacc",
"border-weak-disabled": "#f0f3fa",
"border-weak-focus": "#b9bfd0",
"border-base": "#afb7cb",
"border-hover": "#a3abc1",
"border-active": "#979fb7",
"border-selected": "#8b94ad",
"border-disabled": "#e5e9f2",
"border-focus": "#9ca4ba",
"border-strong-base": "#757f97",
"border-strong-hover": "#69718a",
"border-strong-active": "#5d647d",
"border-strong-selected": "#525970",
"border-strong-disabled": "#c9cedc",
"border-strong-focus": "#636c84",
"surface-diff-add-base": "#e4f0e4",
"surface-diff-delete-base": "#f4e1e4",
"surface-diff-hidden-base": "#dfe6f2",
"text-base": "#2e3440",
"text-weak": "#4c566a",
"text-strong": "#1f2530",
"syntax-string": "#a3be8c",
"syntax-primitive": "#bf616a",
"syntax-property": "#5e81ac",
"syntax-type": "#d08770",
"syntax-constant": "#81a1c1",
"syntax-info": "#81a1c1",
"markdown-heading": "#5e81ac",
"markdown-text": "#2e3440",
"markdown-link": "#5e81ac",
"markdown-link-text": "#81a1c1",
"markdown-code": "#a3be8c",
"markdown-block-quote": "#d08770",
"markdown-emph": "#d08770",
"markdown-strong": "#bf616a",
"markdown-horizontal-rule": "#cbd3e1",
"markdown-list-item": "#5e81ac",
"markdown-list-enumeration": "#81a1c1",
"markdown-image": "#5e81ac",
"markdown-image-text": "#81a1c1",
"markdown-code-block": "#5e81ac"
}
},
"dark": {
"seeds": {
"neutral": "#2e3440",
"primary": "#88c0d0",
"success": "#a3be8c",
"warning": "#d08770",
"error": "#bf616a",
"info": "#81a1c1",
"interactive": "#88c0d0",
"diffAdd": "#81a1c1",
"diffDelete": "#bf616a"
},
"overrides": {
"background-base": "#1f2430",
"background-weak": "#222938",
"background-strong": "#1c202a",
"background-stronger": "#181c24",
"border-weak-base": "#343a47",
"border-weak-hover": "#383f50",
"border-weak-active": "#3d4458",
"border-weak-selected": "#434a62",
"border-weak-disabled": "#151923",
"border-weak-focus": "#3f4359",
"border-base": "#4a5163",
"border-hover": "#515870",
"border-active": "#585f7c",
"border-selected": "#606889",
"border-disabled": "#1b202a",
"border-focus": "#545b78",
"border-strong-base": "#6a7492",
"border-strong-hover": "#747e9f",
"border-strong-active": "#7e88ac",
"border-strong-selected": "#8993b9",
"border-strong-disabled": "#232836",
"border-strong-focus": "#76819f",
"surface-diff-add-base": "#1f2e33",
"surface-diff-delete-base": "#2e212a",
"surface-diff-hidden-base": "#222b3a",
"text-base": "#e5e9f0",
"text-weak": "#a4adbf",
"text-strong": "#f8fafc",
"syntax-string": "#a3be8c",
"syntax-primitive": "#d57780",
"syntax-property": "#88c0d0",
"syntax-type": "#eac196",
"syntax-constant": "#81a1c1",
"syntax-info": "#81a1c1",
"markdown-heading": "#88c0d0",
"markdown-text": "#e5e9f0",
"markdown-link": "#88c0d0",
"markdown-link-text": "#81a1c1",
"markdown-code": "#a3be8c",
"markdown-block-quote": "#d08770",
"markdown-emph": "#d08770",
"markdown-strong": "#bf616a",
"markdown-horizontal-rule": "#2f384a",
"markdown-list-item": "#88c0d0",
"markdown-list-enumeration": "#81a1c1",
"markdown-image": "#88c0d0",
"markdown-image-text": "#81a1c1",
"markdown-code-block": "#cbd3e1"
}
}
}
+535
View File
@@ -0,0 +1,535 @@
{
"$schema": "https://opencode.ai/desktop-theme.json",
"name": "OC-1",
"id": "oc-1",
"light": {
"seeds": {
"neutral": "#8e8b8b",
"primary": "#dcde8d",
"success": "#12c905",
"warning": "#ffdc17",
"error": "#fc533a",
"info": "#a753ae",
"interactive": "#034cff",
"diffAdd": "#9ff29a",
"diffDelete": "#fc533a"
},
"overrides": {
"background-base": "#f8f7f7",
"background-weak": "var(--smoke-light-3)",
"background-strong": "var(--smoke-light-1)",
"background-stronger": "#fcfcfc",
"surface-base": "var(--smoke-light-alpha-2)",
"base": "var(--smoke-light-alpha-2)",
"surface-base-hover": "#0500000f",
"surface-base-active": "var(--smoke-light-alpha-3)",
"surface-base-interactive-active": "var(--cobalt-light-alpha-3)",
"base2": "var(--smoke-light-alpha-2)",
"base3": "var(--smoke-light-alpha-2)",
"surface-inset-base": "var(--smoke-light-alpha-2)",
"surface-inset-base-hover": "var(--smoke-light-alpha-3)",
"surface-inset-strong": "#1f000017",
"surface-inset-strong-hover": "#1f000017",
"surface-raised-base": "var(--smoke-light-alpha-1)",
"surface-float-base": "var(--smoke-dark-1)",
"surface-float-base-hover": "var(--smoke-dark-2)",
"surface-raised-base-hover": "var(--smoke-light-alpha-2)",
"surface-raised-base-active": "var(--smoke-light-alpha-3)",
"surface-raised-strong": "var(--smoke-light-1)",
"surface-raised-strong-hover": "var(--white)",
"surface-raised-stronger": "var(--white)",
"surface-raised-stronger-hover": "var(--white)",
"surface-weak": "var(--smoke-light-alpha-3)",
"surface-weaker": "var(--smoke-light-alpha-4)",
"surface-strong": "#ffffff",
"surface-raised-stronger-non-alpha": "var(--white)",
"surface-brand-base": "var(--yuzu-light-9)",
"surface-brand-hover": "var(--yuzu-light-10)",
"surface-interactive-base": "var(--cobalt-light-3)",
"surface-interactive-hover": "var(--cobalt-light-4)",
"surface-interactive-weak": "var(--cobalt-light-2)",
"surface-interactive-weak-hover": "var(--cobalt-light-3)",
"surface-success-base": "var(--apple-light-3)",
"surface-success-weak": "var(--apple-light-2)",
"surface-success-strong": "var(--apple-light-9)",
"surface-warning-base": "var(--solaris-light-3)",
"surface-warning-weak": "var(--solaris-light-2)",
"surface-warning-strong": "var(--solaris-light-9)",
"surface-critical-base": "var(--ember-light-3)",
"surface-critical-weak": "var(--ember-light-2)",
"surface-critical-strong": "var(--ember-light-9)",
"surface-info-base": "var(--lilac-light-3)",
"surface-info-weak": "var(--lilac-light-2)",
"surface-info-strong": "var(--lilac-light-9)",
"surface-diff-unchanged-base": "#ffffff00",
"surface-diff-skip-base": "var(--smoke-light-2)",
"surface-diff-hidden-base": "var(--blue-light-3)",
"surface-diff-hidden-weak": "var(--blue-light-2)",
"surface-diff-hidden-weaker": "var(--blue-light-1)",
"surface-diff-hidden-strong": "var(--blue-light-5)",
"surface-diff-hidden-stronger": "var(--blue-light-9)",
"surface-diff-add-base": "#dafbe0",
"surface-diff-add-weak": "var(--mint-light-2)",
"surface-diff-add-weaker": "var(--mint-light-1)",
"surface-diff-add-strong": "var(--mint-light-5)",
"surface-diff-add-stronger": "var(--mint-light-9)",
"surface-diff-delete-base": "var(--ember-light-3)",
"surface-diff-delete-weak": "var(--ember-light-2)",
"surface-diff-delete-weaker": "var(--ember-light-1)",
"surface-diff-delete-strong": "var(--ember-light-6)",
"surface-diff-delete-stronger": "var(--ember-light-9)",
"input-base": "var(--smoke-light-1)",
"input-hover": "var(--smoke-light-2)",
"input-active": "var(--cobalt-light-1)",
"input-selected": "var(--cobalt-light-4)",
"input-focus": "var(--cobalt-light-1)",
"input-disabled": "var(--smoke-light-4)",
"text-base": "var(--smoke-light-11)",
"text-weak": "var(--smoke-light-9)",
"text-weaker": "var(--smoke-light-8)",
"text-strong": "var(--smoke-light-12)",
"text-invert-base": "var(--smoke-dark-alpha-11)",
"text-invert-weak": "var(--smoke-dark-alpha-9)",
"text-invert-weaker": "var(--smoke-dark-alpha-8)",
"text-invert-strong": "var(--smoke-dark-alpha-12)",
"text-interactive-base": "var(--cobalt-light-9)",
"text-on-brand-base": "var(--smoke-light-alpha-11)",
"text-on-interactive-base": "var(--smoke-light-1)",
"text-on-interactive-weak": "var(--smoke-dark-alpha-11)",
"text-on-success-base": "var(--apple-light-10)",
"text-on-critical-base": "var(--ember-light-10)",
"text-on-critical-weak": "var(--ember-light-8)",
"text-on-critical-strong": "var(--ember-light-12)",
"text-on-warning-base": "var(--smoke-dark-alpha-11)",
"text-on-info-base": "var(--smoke-dark-alpha-11)",
"text-diff-add-base": "var(--mint-light-11)",
"text-diff-delete-base": "var(--ember-light-10)",
"text-diff-delete-strong": "var(--ember-light-12)",
"text-diff-add-strong": "var(--mint-light-12)",
"text-on-info-weak": "var(--smoke-dark-alpha-9)",
"text-on-info-strong": "var(--smoke-dark-alpha-12)",
"text-on-warning-weak": "var(--smoke-dark-alpha-9)",
"text-on-warning-strong": "var(--smoke-dark-alpha-12)",
"text-on-success-weak": "var(--apple-light-6)",
"text-on-success-strong": "var(--apple-light-12)",
"text-on-brand-weak": "var(--smoke-light-alpha-9)",
"text-on-brand-weaker": "var(--smoke-light-alpha-8)",
"text-on-brand-strong": "var(--smoke-light-alpha-12)",
"button-secondary-base": "#fdfcfc",
"button-secondary-hover": "#faf9f9",
"border-base": "var(--smoke-light-alpha-7)",
"border-hover": "var(--smoke-light-alpha-8)",
"border-active": "var(--smoke-light-alpha-9)",
"border-selected": "var(--cobalt-light-alpha-9)",
"border-disabled": "var(--smoke-light-alpha-8)",
"border-focus": "var(--smoke-light-alpha-9)",
"border-weak-base": "var(--smoke-light-alpha-5)",
"border-strong-base": "var(--smoke-light-alpha-7)",
"border-strong-hover": "var(--smoke-light-alpha-8)",
"border-strong-active": "var(--smoke-light-alpha-7)",
"border-strong-selected": "var(--cobalt-light-alpha-6)",
"border-strong-disabled": "var(--smoke-light-alpha-6)",
"border-strong-focus": "var(--smoke-light-alpha-7)",
"border-weak-hover": "var(--smoke-light-alpha-6)",
"border-weak-active": "var(--smoke-light-alpha-7)",
"border-weak-selected": "var(--cobalt-light-alpha-5)",
"border-weak-disabled": "var(--smoke-light-alpha-6)",
"border-weak-focus": "var(--smoke-light-alpha-7)",
"border-interactive-base": "var(--cobalt-light-7)",
"border-interactive-hover": "var(--cobalt-light-8)",
"border-interactive-active": "var(--cobalt-light-9)",
"border-interactive-selected": "var(--cobalt-light-9)",
"border-interactive-disabled": "var(--smoke-light-8)",
"border-interactive-focus": "var(--cobalt-light-9)",
"border-success-base": "var(--apple-light-6)",
"border-success-hover": "var(--apple-light-7)",
"border-success-selected": "var(--apple-light-9)",
"border-warning-base": "var(--solaris-light-6)",
"border-warning-hover": "var(--solaris-light-7)",
"border-warning-selected": "var(--solaris-light-9)",
"border-critical-base": "var(--ember-light-6)",
"border-critical-hover": "var(--ember-light-7)",
"border-critical-selected": "var(--ember-light-9)",
"border-info-base": "var(--lilac-light-6)",
"border-info-hover": "var(--lilac-light-7)",
"border-info-selected": "var(--lilac-light-9)",
"icon-base": "var(--smoke-light-9)",
"icon-hover": "var(--smoke-light-11)",
"icon-active": "var(--smoke-light-12)",
"icon-selected": "var(--smoke-light-12)",
"icon-disabled": "var(--smoke-light-8)",
"icon-focus": "var(--smoke-light-12)",
"icon-invert-base": "#ffffff",
"icon-weak-base": "var(--smoke-light-7)",
"icon-weak-hover": "var(--smoke-light-8)",
"icon-weak-active": "var(--smoke-light-9)",
"icon-weak-selected": "var(--smoke-light-10)",
"icon-weak-disabled": "var(--smoke-light-6)",
"icon-weak-focus": "var(--smoke-light-9)",
"icon-strong-base": "var(--smoke-light-12)",
"icon-strong-hover": "#151313",
"icon-strong-active": "#020202",
"icon-strong-selected": "#020202",
"icon-strong-disabled": "var(--smoke-light-8)",
"icon-strong-focus": "#020202",
"icon-brand-base": "var(--smoke-light-12)",
"icon-interactive-base": "var(--cobalt-light-9)",
"icon-success-base": "var(--apple-light-7)",
"icon-success-hover": "var(--apple-light-8)",
"icon-success-active": "var(--apple-light-11)",
"icon-warning-base": "var(--amber-light-7)",
"icon-warning-hover": "var(--amber-light-8)",
"icon-warning-active": "var(--amber-light-11)",
"icon-critical-base": "var(--ember-light-10)",
"icon-critical-hover": "var(--ember-light-11)",
"icon-critical-active": "var(--ember-light-12)",
"icon-info-base": "var(--lilac-light-7)",
"icon-info-hover": "var(--lilac-light-8)",
"icon-info-active": "var(--lilac-light-11)",
"icon-on-brand-base": "var(--smoke-light-alpha-11)",
"icon-on-brand-hover": "var(--smoke-light-alpha-12)",
"icon-on-brand-selected": "var(--smoke-light-alpha-12)",
"icon-on-interactive-base": "var(--smoke-light-1)",
"icon-agent-plan-base": "var(--purple-light-9)",
"icon-agent-docs-base": "var(--amber-light-9)",
"icon-agent-ask-base": "var(--cyan-light-9)",
"icon-agent-build-base": "var(--cobalt-light-9)",
"icon-on-success-base": "var(--apple-light-alpha-9)",
"icon-on-success-hover": "var(--apple-light-alpha-10)",
"icon-on-success-selected": "var(--apple-light-alpha-11)",
"icon-on-warning-base": "var(--amber-lightalpha-9)",
"icon-on-warning-hover": "var(--amber-lightalpha-10)",
"icon-on-warning-selected": "var(--amber-lightalpha-11)",
"icon-on-critical-base": "var(--ember-light-alpha-9)",
"icon-on-critical-hover": "var(--ember-light-alpha-10)",
"icon-on-critical-selected": "var(--ember-light-alpha-11)",
"icon-on-info-base": "var(--lilac-light-9)",
"icon-on-info-hover": "var(--lilac-light-alpha-10)",
"icon-on-info-selected": "var(--lilac-light-alpha-11)",
"icon-diff-add-base": "var(--mint-light-11)",
"icon-diff-add-hover": "var(--mint-light-12)",
"icon-diff-add-active": "var(--mint-light-12)",
"icon-diff-delete-base": "var(--ember-light-10)",
"icon-diff-delete-hover": "var(--ember-light-11)",
"syntax-comment": "var(--text-weak)",
"syntax-regexp": "var(--text-base)",
"syntax-string": "#006656",
"syntax-keyword": "var(--text-weak)",
"syntax-primitive": "#fb4804",
"syntax-operator": "var(--text-base)",
"syntax-variable": "var(--text-strong)",
"syntax-property": "#ed6dc8",
"syntax-type": "#596600",
"syntax-constant": "#007b80",
"syntax-punctuation": "var(--text-base)",
"syntax-object": "var(--text-strong)",
"syntax-success": "var(--apple-light-10)",
"syntax-warning": "var(--amber-light-10)",
"syntax-critical": "var(--ember-light-10)",
"syntax-info": "#0092a8",
"syntax-diff-add": "var(--mint-light-11)",
"syntax-diff-delete": "var(--ember-light-11)",
"syntax-diff-unknown": "#ff0000",
"markdown-heading": "#d68c27",
"markdown-text": "#1a1a1a",
"markdown-link": "#3b7dd8",
"markdown-link-text": "#318795",
"markdown-code": "#3d9a57",
"markdown-block-quote": "#b0851f",
"markdown-emph": "#b0851f",
"markdown-strong": "#d68c27",
"markdown-horizontal-rule": "#8a8a8a",
"markdown-list-item": "#3b7dd8",
"markdown-list-enumeration": "#318795",
"markdown-image": "#3b7dd8",
"markdown-image-text": "#318795",
"markdown-code-block": "#1a1a1a",
"border-color": "#ffffff",
"border-weaker-base": "var(--smoke-light-alpha-3)",
"border-weaker-hover": "var(--smoke-light-alpha-4)",
"border-weaker-active": "var(--smoke-light-alpha-6)",
"border-weaker-selected": "var(--cobalt-light-alpha-4)",
"border-weaker-disabled": "var(--smoke-light-alpha-2)",
"border-weaker-focus": "var(--smoke-light-alpha-6)",
"button-ghost-hover": "var(--smoke-light-alpha-2)",
"button-ghost-hover2": "var(--smoke-light-alpha-3)",
"avatar-background-pink": "#feeef8",
"avatar-background-mint": "#e1fbf4",
"avatar-background-orange": "#fff1e7",
"avatar-background-purple": "#f9f1fe",
"avatar-background-cyan": "#e7f9fb",
"avatar-background-lime": "#eefadc",
"avatar-text-pink": "#cd1d8d",
"avatar-text-mint": "#147d6f",
"avatar-text-orange": "#ed5f00",
"avatar-text-purple": "#8445bc",
"avatar-text-cyan": "#0894b3",
"avatar-text-lime": "#5d770d"
}
},
"dark": {
"seeds": {
"neutral": "#716c6b",
"primary": "#fab283",
"success": "#12c905",
"warning": "#fcd53a",
"error": "#fc533a",
"info": "#edb2f1",
"interactive": "#034cff",
"diffAdd": "#c8ffc4",
"diffDelete": "#fc533a"
},
"overrides": {
"background-base": "var(--smoke-dark-1)",
"background-weak": "#1c1717",
"background-strong": "#151313",
"background-stronger": "#191515",
"surface-base": "var(--smoke-dark-alpha-2)",
"base": "var(--smoke-dark-alpha-2)",
"surface-base-hover": "#e0b7b716",
"surface-base-active": "var(--smoke-dark-alpha-3)",
"surface-base-interactive-active": "var(--cobalt-dark-alpha-2)",
"base2": "var(--smoke-dark-alpha-2)",
"base3": "var(--smoke-dark-alpha-2)",
"surface-inset-base": "#0e0b0b7f",
"surface-inset-base-hover": "#0e0b0b7f",
"surface-inset-strong": "#060505cc",
"surface-inset-strong-hover": "#060505cc",
"surface-raised-base": "var(--smoke-dark-alpha-3)",
"surface-float-base": "var(--smoke-dark-1)",
"surface-float-base-hover": "var(--smoke-dark-2)",
"surface-raised-base-hover": "var(--smoke-dark-alpha-4)",
"surface-raised-base-active": "var(--smoke-dark-alpha-5)",
"surface-raised-strong": "var(--smoke-dark-alpha-4)",
"surface-raised-strong-hover": "var(--smoke-dark-alpha-6)",
"surface-raised-stronger": "var(--smoke-dark-alpha-6)",
"surface-raised-stronger-hover": "var(--smoke-dark-alpha-7)",
"surface-weak": "var(--smoke-dark-alpha-4)",
"surface-weaker": "var(--smoke-dark-alpha-5)",
"surface-strong": "var(--smoke-dark-alpha-7)",
"surface-raised-stronger-non-alpha": "var(--smoke-dark-3)",
"surface-brand-base": "var(--yuzu-light-9)",
"surface-brand-hover": "var(--yuzu-light-10)",
"surface-interactive-base": "var(--cobalt-light-3)",
"surface-interactive-hover": "var(--cobalt-light-4)",
"surface-interactive-weak": "var(--cobalt-light-2)",
"surface-interactive-weak-hover": "var(--cobalt-light-3)",
"surface-success-base": "var(--apple-light-3)",
"surface-success-weak": "var(--apple-light-2)",
"surface-success-strong": "var(--apple-light-9)",
"surface-warning-base": "var(--solaris-light-3)",
"surface-warning-weak": "var(--solaris-light-2)",
"surface-warning-strong": "var(--solaris-light-9)",
"surface-critical-base": "var(--ember-dark-3)",
"surface-critical-weak": "var(--ember-dark-2)",
"surface-critical-strong": "var(--ember-dark-9)",
"surface-info-base": "var(--lilac-light-3)",
"surface-info-weak": "var(--lilac-light-2)",
"surface-info-strong": "var(--lilac-light-9)",
"surface-diff-unchanged-base": "var(--smoke-dark-1)",
"surface-diff-skip-base": "var(--smoke-dark-alpha-1)",
"surface-diff-hidden-base": "var(--blue-dark-2)",
"surface-diff-hidden-weak": "var(--blue-dark-1)",
"surface-diff-hidden-weaker": "var(--blue-dark-3)",
"surface-diff-hidden-strong": "var(--blue-dark-5)",
"surface-diff-hidden-stronger": "var(--blue-dark-11)",
"surface-diff-add-base": "var(--mint-dark-3)",
"surface-diff-add-weak": "var(--mint-dark-4)",
"surface-diff-add-weaker": "var(--mint-dark-3)",
"surface-diff-add-strong": "var(--mint-dark-5)",
"surface-diff-add-stronger": "var(--mint-dark-11)",
"surface-diff-delete-base": "var(--ember-dark-3)",
"surface-diff-delete-weak": "var(--ember-dark-4)",
"surface-diff-delete-weaker": "var(--ember-dark-3)",
"surface-diff-delete-strong": "var(--ember-dark-5)",
"surface-diff-delete-stronger": "var(--ember-dark-11)",
"input-base": "var(--smoke-dark-2)",
"input-hover": "var(--smoke-dark-2)",
"input-active": "var(--cobalt-dark-1)",
"input-selected": "var(--cobalt-dark-2)",
"input-focus": "var(--cobalt-dark-1)",
"input-disabled": "var(--smoke-dark-4)",
"text-base": "var(--smoke-dark-alpha-11)",
"text-weak": "var(--smoke-dark-alpha-9)",
"text-weaker": "var(--smoke-dark-alpha-8)",
"text-strong": "var(--smoke-dark-alpha-12)",
"text-invert-base": "var(--smoke-dark-alpha-11)",
"text-invert-weak": "var(--smoke-dark-alpha-9)",
"text-invert-weaker": "var(--smoke-dark-alpha-8)",
"text-invert-strong": "var(--smoke-dark-alpha-12)",
"text-interactive-base": "var(--cobalt-dark-11)",
"text-on-brand-base": "var(--smoke-dark-alpha-11)",
"text-on-interactive-base": "var(--smoke-dark-12)",
"text-on-interactive-weak": "var(--smoke-dark-alpha-11)",
"text-on-success-base": "var(--apple-dark-9)",
"text-on-critical-base": "var(--ember-dark-9)",
"text-on-critical-weak": "var(--ember-dark-8)",
"text-on-critical-strong": "var(--ember-dark-12)",
"text-on-warning-base": "var(--smoke-dark-alpha-11)",
"text-on-info-base": "var(--smoke-dark-alpha-11)",
"text-diff-add-base": "var(--mint-dark-11)",
"text-diff-delete-base": "var(--ember-dark-9)",
"text-diff-delete-strong": "var(--ember-dark-12)",
"text-diff-add-strong": "var(--mint-dark-8)",
"text-on-info-weak": "var(--smoke-dark-alpha-9)",
"text-on-info-strong": "var(--smoke-dark-alpha-12)",
"text-on-warning-weak": "var(--smoke-dark-alpha-9)",
"text-on-warning-strong": "var(--smoke-dark-alpha-12)",
"text-on-success-weak": "var(--apple-dark-8)",
"text-on-success-strong": "var(--apple-dark-12)",
"text-on-brand-weak": "var(--smoke-dark-alpha-9)",
"text-on-brand-weaker": "var(--smoke-dark-alpha-8)",
"text-on-brand-strong": "var(--smoke-dark-alpha-12)",
"button-secondary-base": "#231f1f",
"button-secondary-hover": "#2a2727",
"border-base": "var(--smoke-dark-alpha-7)",
"border-hover": "var(--smoke-dark-alpha-8)",
"border-active": "var(--smoke-dark-alpha-9)",
"border-selected": "var(--cobalt-dark-alpha-11)",
"border-disabled": "var(--smoke-dark-alpha-8)",
"border-focus": "var(--smoke-dark-alpha-9)",
"border-weak-base": "var(--smoke-dark-alpha-6)",
"border-strong-base": "var(--smoke-dark-alpha-8)",
"border-strong-hover": "var(--smoke-dark-alpha-7)",
"border-strong-active": "var(--smoke-dark-alpha-8)",
"border-strong-selected": "var(--cobalt-dark-alpha-6)",
"border-strong-disabled": "var(--smoke-dark-alpha-6)",
"border-strong-focus": "var(--smoke-dark-alpha-8)",
"border-weak-hover": "var(--smoke-dark-alpha-7)",
"border-weak-active": "var(--smoke-dark-alpha-8)",
"border-weak-selected": "var(--cobalt-dark-alpha-6)",
"border-weak-disabled": "var(--smoke-dark-alpha-6)",
"border-weak-focus": "var(--smoke-dark-alpha-8)",
"border-interactive-base": "var(--cobalt-light-7)",
"border-interactive-hover": "var(--cobalt-light-8)",
"border-interactive-active": "var(--cobalt-light-9)",
"border-interactive-selected": "var(--cobalt-light-9)",
"border-interactive-disabled": "var(--smoke-light-8)",
"border-interactive-focus": "var(--cobalt-light-9)",
"border-success-base": "var(--apple-light-6)",
"border-success-hover": "var(--apple-light-7)",
"border-success-selected": "var(--apple-light-9)",
"border-warning-base": "var(--solaris-light-6)",
"border-warning-hover": "var(--solaris-light-7)",
"border-warning-selected": "var(--solaris-light-9)",
"border-critical-base": "var(--ember-dark-5)",
"border-critical-hover": "var(--ember-dark-7)",
"border-critical-selected": "var(--ember-dark-9)",
"border-info-base": "var(--lilac-light-6)",
"border-info-hover": "var(--lilac-light-7)",
"border-info-selected": "var(--lilac-light-9)",
"icon-base": "var(--smoke-dark-9)",
"icon-hover": "var(--smoke-dark-10)",
"icon-active": "var(--smoke-dark-11)",
"icon-selected": "var(--smoke-dark-12)",
"icon-disabled": "var(--smoke-dark-7)",
"icon-focus": "var(--smoke-dark-12)",
"icon-invert-base": "var(--smoke-dark-1)",
"icon-weak-base": "var(--smoke-dark-6)",
"icon-weak-hover": "var(--smoke-light-7)",
"icon-weak-active": "var(--smoke-light-8)",
"icon-weak-selected": "var(--smoke-light-9)",
"icon-weak-disabled": "var(--smoke-light-4)",
"icon-weak-focus": "var(--smoke-light-9)",
"icon-strong-base": "var(--smoke-dark-12)",
"icon-strong-hover": "#f6f3f3",
"icon-strong-active": "#fcfcfc",
"icon-strong-selected": "#fdfcfc",
"icon-strong-disabled": "var(--smoke-dark-8)",
"icon-strong-focus": "#fdfcfc",
"icon-brand-base": "var(--white)",
"icon-interactive-base": "var(--cobalt-dark-9)",
"icon-success-base": "var(--apple-dark-7)",
"icon-success-hover": "var(--apple-dark-8)",
"icon-success-active": "var(--apple-dark-11)",
"icon-warning-base": "var(--amber-dark-7)",
"icon-warning-hover": "var(--amber-dark-8)",
"icon-warning-active": "var(--amber-dark-11)",
"icon-critical-base": "var(--ember-dark-9)",
"icon-critical-hover": "var(--ember-dark-11)",
"icon-critical-active": "var(--ember-dark-12)",
"icon-info-base": "var(--lilac-dark-7)",
"icon-info-hover": "var(--lilac-dark-8)",
"icon-info-active": "var(--lilac-dark-11)",
"icon-on-brand-base": "var(--smoke-light-alpha-11)",
"icon-on-brand-hover": "var(--smoke-light-alpha-12)",
"icon-on-brand-selected": "var(--smoke-light-alpha-12)",
"icon-on-interactive-base": "var(--smoke-dark-12)",
"icon-agent-plan-base": "var(--purple-dark-9)",
"icon-agent-docs-base": "var(--amber-dark-9)",
"icon-agent-ask-base": "var(--cyan-dark-9)",
"icon-agent-build-base": "var(--cobalt-dark-11)",
"icon-on-success-base": "var(--apple-dark-alpha-9)",
"icon-on-success-hover": "var(--apple-dark-alpha-10)",
"icon-on-success-selected": "var(--apple-dark-alpha-11)",
"icon-on-warning-base": "var(--amber-darkalpha-9)",
"icon-on-warning-hover": "var(--amber-darkalpha-10)",
"icon-on-warning-selected": "var(--amber-darkalpha-11)",
"icon-on-critical-base": "var(--ember-dark-alpha-9)",
"icon-on-critical-hover": "var(--ember-dark-alpha-10)",
"icon-on-critical-selected": "var(--ember-dark-alpha-11)",
"icon-on-info-base": "var(--lilac-dark-9)",
"icon-on-info-hover": "var(--lilac-dark-alpha-10)",
"icon-on-info-selected": "var(--lilac-dark-alpha-11)",
"icon-diff-add-base": "var(--mint-dark-11)",
"icon-diff-add-hover": "var(--mint-dark-10)",
"icon-diff-add-active": "var(--mint-dark-11)",
"icon-diff-delete-base": "var(--ember-dark-9)",
"icon-diff-delete-hover": "var(--ember-dark-10)",
"syntax-comment": "var(--text-weak)",
"syntax-regexp": "var(--text-base)",
"syntax-string": "#00ceb9",
"syntax-keyword": "var(--text-weak)",
"syntax-primitive": "#ffba92",
"syntax-operator": "var(--text-weak)",
"syntax-variable": "var(--text-strong)",
"syntax-property": "#ff9ae2",
"syntax-type": "#ecf58c",
"syntax-constant": "#93e9f6",
"syntax-punctuation": "var(--text-weak)",
"syntax-object": "var(--text-strong)",
"syntax-success": "var(--apple-dark-10)",
"syntax-warning": "var(--amber-dark-10)",
"syntax-critical": "var(--ember-dark-10)",
"syntax-info": "#93e9f6",
"syntax-diff-add": "var(--mint-dark-11)",
"syntax-diff-delete": "var(--ember-dark-11)",
"syntax-diff-unknown": "#ff0000",
"markdown-heading": "#9d7cd8",
"markdown-text": "#eeeeee",
"markdown-link": "#fab283",
"markdown-link-text": "#56b6c2",
"markdown-code": "#7fd88f",
"markdown-block-quote": "#e5c07b",
"markdown-emph": "#e5c07b",
"markdown-strong": "#f5a742",
"markdown-horizontal-rule": "#808080",
"markdown-list-item": "#fab283",
"markdown-list-enumeration": "#56b6c2",
"markdown-image": "#fab283",
"markdown-image-text": "#56b6c2",
"markdown-code-block": "#eeeeee",
"border-color": "#ffffff",
"border-weaker-base": "var(--smoke-dark-alpha-3)",
"border-weaker-hover": "var(--smoke-dark-alpha-4)",
"border-weaker-active": "var(--smoke-dark-alpha-6)",
"border-weaker-selected": "var(--cobalt-dark-alpha-3)",
"border-weaker-disabled": "var(--smoke-dark-alpha-2)",
"border-weaker-focus": "var(--smoke-dark-alpha-6)",
"button-ghost-hover": "var(--smoke-dark-alpha-2)",
"button-ghost-hover2": "var(--smoke-dark-alpha-3)",
"avatar-background-pink": "#501b3f",
"avatar-background-mint": "#033a34",
"avatar-background-orange": "#5f2a06",
"avatar-background-purple": "#432155",
"avatar-background-cyan": "#0f3058",
"avatar-background-lime": "#2b3711",
"avatar-text-pink": "#e34ba9",
"avatar-text-mint": "#95f3d9",
"avatar-text-orange": "#ff802b",
"avatar-text-purple": "#9d5bd2",
"avatar-text-cyan": "#369eff",
"avatar-text-lime": "#c4f042"
}
}
}
@@ -0,0 +1,131 @@
{
"$schema": "https://opencode.ai/desktop-theme.json",
"name": "One Dark Pro",
"id": "onedarkpro",
"light": {
"seeds": {
"neutral": "#f5f6f8",
"primary": "#528bff",
"success": "#4fa66d",
"warning": "#d19a66",
"error": "#e06c75",
"info": "#61afef",
"interactive": "#528bff",
"diffAdd": "#c2ebcf",
"diffDelete": "#f7c1c5"
},
"overrides": {
"background-base": "#f5f6f8",
"background-weak": "#eef0f4",
"background-strong": "#fafbfc",
"background-stronger": "#ffffff",
"border-weak-base": "#dee2eb",
"border-weak-hover": "#d4d9e3",
"border-weak-active": "#caced6",
"border-weak-selected": "#bec4d0",
"border-weak-disabled": "#f4f6fb",
"border-weak-focus": "#c4cada",
"border-base": "#b5bccd",
"border-hover": "#aab1c2",
"border-active": "#a0a7b8",
"border-selected": "#959cae",
"border-disabled": "#eceef4",
"border-focus": "#a6adbf",
"border-strong-base": "#747c92",
"border-strong-hover": "#6a7287",
"border-strong-active": "#60687c",
"border-strong-selected": "#565e71",
"border-strong-disabled": "#cbd0dd",
"border-strong-focus": "#666d82",
"surface-diff-add-base": "#e5f4ea",
"surface-diff-delete-base": "#fde7ea",
"surface-diff-hidden-base": "#e4e8f4",
"text-base": "#2b303b",
"text-weak": "#6b717f",
"text-strong": "#0e1118",
"syntax-string": "#4fa66d",
"syntax-primitive": "#d85462",
"syntax-property": "#528bff",
"syntax-type": "#d19a66",
"syntax-constant": "#61afef",
"syntax-info": "#61afef",
"markdown-heading": "#528bff",
"markdown-text": "#2b303b",
"markdown-link": "#528bff",
"markdown-link-text": "#61afef",
"markdown-code": "#4fa66d",
"markdown-block-quote": "#d19a66",
"markdown-emph": "#d19a66",
"markdown-strong": "#d85462",
"markdown-horizontal-rule": "#d3d7e4",
"markdown-list-item": "#528bff",
"markdown-list-enumeration": "#61afef",
"markdown-image": "#528bff",
"markdown-image-text": "#61afef",
"markdown-code-block": "#528bff"
}
},
"dark": {
"seeds": {
"neutral": "#1e222a",
"primary": "#61afef",
"success": "#98c379",
"warning": "#e5c07b",
"error": "#e06c75",
"info": "#56b6c2",
"interactive": "#61afef",
"diffAdd": "#4b815a",
"diffDelete": "#b2555f"
},
"overrides": {
"background-base": "#1e222a",
"background-weak": "#212631",
"background-strong": "#1b1f27",
"background-stronger": "#171b23",
"border-weak-base": "#323848",
"border-weak-hover": "#363d52",
"border-weak-active": "#3c435c",
"border-weak-selected": "#424967",
"border-weak-disabled": "#141720",
"border-weak-focus": "#3f4560",
"border-base": "#4a5164",
"border-hover": "#515871",
"border-active": "#585f7e",
"border-selected": "#60688a",
"border-disabled": "#1a1e27",
"border-focus": "#555c79",
"border-strong-base": "#6a7390",
"border-strong-hover": "#737c9d",
"border-strong-active": "#7d87ab",
"border-strong-selected": "#8791b8",
"border-strong-disabled": "#212533",
"border-strong-focus": "#7680a2",
"surface-diff-add-base": "#1c2a26",
"surface-diff-delete-base": "#2a1c22",
"surface-diff-hidden-base": "#232836",
"text-base": "#abb2bf",
"text-weak": "#818899",
"text-strong": "#f6f7fb",
"syntax-string": "#98c379",
"syntax-primitive": "#e06c75",
"syntax-property": "#61afef",
"syntax-type": "#e5c07b",
"syntax-constant": "#56b6c2",
"syntax-info": "#56b6c2",
"markdown-heading": "#61afef",
"markdown-text": "#abb2bf",
"markdown-link": "#61afef",
"markdown-link-text": "#56b6c2",
"markdown-code": "#98c379",
"markdown-block-quote": "#e5c07b",
"markdown-emph": "#e5c07b",
"markdown-strong": "#e06c75",
"markdown-horizontal-rule": "#2d3444",
"markdown-list-item": "#61afef",
"markdown-list-enumeration": "#56b6c2",
"markdown-image": "#61afef",
"markdown-image-text": "#56b6c2",
"markdown-code-block": "#abb2bf"
}
}
}
@@ -0,0 +1,131 @@
{
"$schema": "https://opencode.ai/desktop-theme.json",
"name": "Shades of Purple",
"id": "shadesofpurple",
"light": {
"seeds": {
"neutral": "#f7ebff",
"primary": "#7a5af8",
"success": "#3dd598",
"warning": "#f7c948",
"error": "#ff6bd5",
"info": "#62d4ff",
"interactive": "#7a5af8",
"diffAdd": "#c8f8da",
"diffDelete": "#ffc3ef"
},
"overrides": {
"background-base": "#f7ebff",
"background-weak": "#f2e2ff",
"background-strong": "#fbf2ff",
"background-stronger": "#fff7ff",
"border-weak-base": "#e5d3ff",
"border-weak-hover": "#dac8f5",
"border-weak-active": "#d1bdeb",
"border-weak-selected": "#c6b3e1",
"border-weak-disabled": "#fcf6ff",
"border-weak-focus": "#ccb9e7",
"border-base": "#baa4d5",
"border-hover": "#b098cb",
"border-active": "#a68dc2",
"border-selected": "#9b82b8",
"border-disabled": "#f1e7ff",
"border-focus": "#a692c6",
"border-strong-base": "#8769a9",
"border-strong-hover": "#7b5c9d",
"border-strong-active": "#704f91",
"border-strong-selected": "#664587",
"border-strong-disabled": "#d8c4f0",
"border-strong-focus": "#755495",
"surface-diff-add-base": "#edf8f1",
"surface-diff-delete-base": "#ffe4f4",
"surface-diff-hidden-base": "#e9e4ff",
"text-base": "#3b2c59",
"text-weak": "#6c568f",
"text-strong": "#1c1033",
"syntax-string": "#3dd598",
"syntax-primitive": "#ff6bd5",
"syntax-property": "#7a5af8",
"syntax-type": "#f7c948",
"syntax-constant": "#62d4ff",
"syntax-info": "#62d4ff",
"markdown-heading": "#7a5af8",
"markdown-text": "#3b2c59",
"markdown-link": "#7a5af8",
"markdown-link-text": "#62d4ff",
"markdown-code": "#3dd598",
"markdown-block-quote": "#f7c948",
"markdown-emph": "#f7c948",
"markdown-strong": "#ff6bd5",
"markdown-horizontal-rule": "#decbed",
"markdown-list-item": "#7a5af8",
"markdown-list-enumeration": "#62d4ff",
"markdown-image": "#7a5af8",
"markdown-image-text": "#62d4ff",
"markdown-code-block": "#7a5af8"
}
},
"dark": {
"seeds": {
"neutral": "#1a102b",
"primary": "#c792ff",
"success": "#7be0b0",
"warning": "#ffd580",
"error": "#ff7ac6",
"info": "#7dd4ff",
"interactive": "#c792ff",
"diffAdd": "#53c39f",
"diffDelete": "#d85aa0"
},
"overrides": {
"background-base": "#1a102b",
"background-weak": "#1f1434",
"background-strong": "#1c122f",
"background-stronger": "#170e26",
"border-weak-base": "#352552",
"border-weak-hover": "#3a2a5d",
"border-weak-active": "#402f68",
"border-weak-selected": "#463674",
"border-weak-disabled": "#10091b",
"border-weak-focus": "#3d2d65",
"border-base": "#4d3a73",
"border-hover": "#553f7f",
"border-active": "#5d468c",
"border-selected": "#654c99",
"border-disabled": "#150d21",
"border-focus": "#594283",
"border-strong-base": "#7659b0",
"border-strong-hover": "#8262be",
"border-strong-active": "#8e6ccc",
"border-strong-selected": "#9a77da",
"border-strong-disabled": "#1c122c",
"border-strong-focus": "#8666c4",
"surface-diff-add-base": "#142c27",
"surface-diff-delete-base": "#2d1424",
"surface-diff-hidden-base": "#231737",
"text-base": "#f5f0ff",
"text-weak": "#c9b6ff",
"text-strong": "#ffffff",
"syntax-string": "#7be0b0",
"syntax-primitive": "#ff7ac6",
"syntax-property": "#c792ff",
"syntax-type": "#ffd580",
"syntax-constant": "#7dd4ff",
"syntax-info": "#7dd4ff",
"markdown-heading": "#c792ff",
"markdown-text": "#f5f0ff",
"markdown-link": "#c792ff",
"markdown-link-text": "#7dd4ff",
"markdown-code": "#7be0b0",
"markdown-block-quote": "#ffd580",
"markdown-emph": "#ffd580",
"markdown-strong": "#ff7ac6",
"markdown-horizontal-rule": "#2d1d41",
"markdown-list-item": "#c792ff",
"markdown-list-enumeration": "#7dd4ff",
"markdown-image": "#c792ff",
"markdown-image-text": "#7dd4ff",
"markdown-code-block": "#f5f0ff"
}
}
}
+131
View File
@@ -0,0 +1,131 @@
{
"$schema": "https://opencode.ai/desktop-theme.json",
"name": "Solarized",
"id": "solarized",
"light": {
"seeds": {
"neutral": "#fdf6e3",
"primary": "#268bd2",
"success": "#859900",
"warning": "#b58900",
"error": "#dc322f",
"info": "#2aa198",
"interactive": "#268bd2",
"diffAdd": "#c6dc7a",
"diffDelete": "#f2a1a1"
},
"overrides": {
"background-base": "#fdf6e3",
"background-weak": "#f6efda",
"background-strong": "#faf3dc",
"background-stronger": "#f6edd4",
"border-weak-base": "#e3e0cd",
"border-weak-hover": "#d9d4c2",
"border-weak-active": "#cfcab7",
"border-weak-selected": "#c5c0ad",
"border-weak-disabled": "#f2edda",
"border-weak-focus": "#cbc6b2",
"border-base": "#bcb5a0",
"border-hover": "#b1aa96",
"border-active": "#a59f8c",
"border-selected": "#999382",
"border-disabled": "#ede7d4",
"border-focus": "#aca58f",
"border-strong-base": "#8c8572",
"border-strong-hover": "#7f7866",
"border-strong-active": "#716b5b",
"border-strong-selected": "#645f50",
"border-strong-disabled": "#d5cdb8",
"border-strong-focus": "#78715f",
"surface-diff-add-base": "#eef5d6",
"surface-diff-delete-base": "#fde4dd",
"surface-diff-hidden-base": "#e3ecf3",
"text-base": "#586e75",
"text-weak": "#7a8c8e",
"text-strong": "#073642",
"syntax-string": "#859900",
"syntax-primitive": "#d33682",
"syntax-property": "#268bd2",
"syntax-type": "#b58900",
"syntax-constant": "#2aa198",
"syntax-info": "#2aa198",
"markdown-heading": "#268bd2",
"markdown-text": "#586e75",
"markdown-link": "#268bd2",
"markdown-link-text": "#2aa198",
"markdown-code": "#859900",
"markdown-block-quote": "#b58900",
"markdown-emph": "#b58900",
"markdown-strong": "#d33682",
"markdown-horizontal-rule": "#cfd1bf",
"markdown-list-item": "#268bd2",
"markdown-list-enumeration": "#2aa198",
"markdown-image": "#268bd2",
"markdown-image-text": "#2aa198",
"markdown-code-block": "#2aa198"
}
},
"dark": {
"seeds": {
"neutral": "#002b36",
"primary": "#6c71c4",
"success": "#859900",
"warning": "#b58900",
"error": "#dc322f",
"info": "#2aa198",
"interactive": "#6c71c4",
"diffAdd": "#4c7654",
"diffDelete": "#c34b4b"
},
"overrides": {
"background-base": "#001f27",
"background-weak": "#022733",
"background-strong": "#01222b",
"background-stronger": "#032830",
"border-weak-base": "#20373f",
"border-weak-hover": "#243e47",
"border-weak-active": "#28434f",
"border-weak-selected": "#2d4958",
"border-weak-disabled": "#0f2026",
"border-weak-focus": "#2a4552",
"border-base": "#31505b",
"border-hover": "#365765",
"border-active": "#3c5e70",
"border-selected": "#42657a",
"border-disabled": "#13272e",
"border-focus": "#3a5a6b",
"border-strong-base": "#4a7887",
"border-strong-hover": "#528294",
"border-strong-active": "#5a8ca1",
"border-strong-selected": "#6396ae",
"border-strong-disabled": "#1b323b",
"border-strong-focus": "#56879a",
"surface-diff-add-base": "#0f2f29",
"surface-diff-delete-base": "#321c1c",
"surface-diff-hidden-base": "#0f3844",
"text-base": "#93a1a1",
"text-weak": "#6c7f80",
"text-strong": "#fdf6e3",
"syntax-string": "#859900",
"syntax-primitive": "#d33682",
"syntax-property": "#6c71c4",
"syntax-type": "#b58900",
"syntax-constant": "#2aa198",
"syntax-info": "#2aa198",
"markdown-heading": "#6c71c4",
"markdown-text": "#93a1a1",
"markdown-link": "#6c71c4",
"markdown-link-text": "#2aa198",
"markdown-code": "#859900",
"markdown-block-quote": "#b58900",
"markdown-emph": "#b58900",
"markdown-strong": "#d33682",
"markdown-horizontal-rule": "#0e3b46",
"markdown-list-item": "#6c71c4",
"markdown-list-enumeration": "#2aa198",
"markdown-image": "#6c71c4",
"markdown-image-text": "#2aa198",
"markdown-code-block": "#93a1a1"
}
}
}
@@ -0,0 +1,155 @@
{
"$schema": "https://opencode.ai/desktop-theme.json",
"name": "Tokyonight",
"id": "tokyonight",
"light": {
"seeds": {
"neutral": "#e1e2e7",
"primary": "#2e7de9",
"success": "#587539",
"warning": "#8c6c3e",
"error": "#c94060",
"info": "#007197",
"interactive": "#2e7de9",
"diffAdd": "#4f8f7b",
"diffDelete": "#d05f7c"
},
"overrides": {
"background-base": "#e1e2e7",
"background-weak": "#dee0ea",
"background-strong": "#e5e6ee",
"background-stronger": "#e9eaf1",
"border-weak-base": "#cdd0dc",
"border-weak-hover": "#c3c6d2",
"border-weak-active": "#b9bcc8",
"border-weak-selected": "#aeb2bf",
"border-weak-disabled": "#e6e7ef",
"border-weak-focus": "#b3b6c3",
"border-base": "#a7abbb",
"border-hover": "#9ba0b1",
"border-active": "#9095a8",
"border-selected": "#83889e",
"border-disabled": "#dedfe6",
"border-focus": "#9599a8",
"border-strong-base": "#757b90",
"border-strong-hover": "#6a7084",
"border-strong-active": "#5f6578",
"border-strong-selected": "#545a6d",
"border-strong-disabled": "#c4c6d0",
"border-strong-focus": "#666b7f",
"surface-diff-add-base": "#dfe7da",
"surface-diff-delete-base": "#f4dadd",
"surface-diff-hidden-base": "#cfd1dd",
"text-base": "#273153",
"text-weak": "#5c6390",
"text-strong": "#1c2544",
"syntax-string": "#587539",
"syntax-primitive": "#b15c00",
"syntax-property": "#9854f1",
"syntax-type": "#3760bf",
"syntax-constant": "#007197",
"syntax-info": "#007197",
"markdown-heading": "#9854f1",
"markdown-text": "#273153",
"markdown-link": "#2e7de9",
"markdown-link-text": "#007197",
"markdown-code": "#587539",
"markdown-block-quote": "#8c6c3e",
"markdown-emph": "#8c6c3e",
"markdown-strong": "#b15c00",
"markdown-horizontal-rule": "#a1a6c5",
"markdown-list-item": "#2e7de9",
"markdown-list-enumeration": "#007197",
"markdown-image": "#2e7de9",
"markdown-image-text": "#007197",
"markdown-code-block": "#3760bf"
}
},
"dark": {
"seeds": {
"neutral": "#1a1b26",
"primary": "#7aa2f7",
"success": "#9ece6a",
"warning": "#e0af68",
"error": "#f7768e",
"info": "#7dcfff",
"interactive": "#7aa2f7",
"diffAdd": "#41a6b5",
"diffDelete": "#c34043"
},
"overrides": {
"background-base": "#0f111a",
"background-weak": "#111428",
"background-strong": "#101324",
"background-stronger": "#13172a",
"border-weak-base": "#25283b",
"border-weak-hover": "#292c43",
"border-weak-active": "#2e314b",
"border-weak-selected": "#343755",
"border-weak-disabled": "#151727",
"border-weak-focus": "#30324f",
"border-base": "#3a3e57",
"border-hover": "#414264",
"border-active": "#474972",
"border-selected": "#4f507f",
"border-disabled": "#1c1d2d",
"border-focus": "#45496f",
"border-strong-base": "#5a5f82",
"border-strong-hover": "#646994",
"border-strong-active": "#6f74a6",
"border-strong-selected": "#7a7fb8",
"border-strong-disabled": "#23243a",
"border-strong-focus": "#6a6f9f",
"surface-base": "#1f2335",
"base": "#1f2335",
"surface-base-hover": "#232840",
"surface-base-active": "#262c46",
"surface-base-interactive-active": "#2b3357",
"base2": "#1f2335",
"base3": "#1f2335",
"surface-inset-base": "#161a2ab3",
"surface-inset-base-hover": "#161a2acc",
"surface-inset-strong": "#0d111fcc",
"surface-inset-strong-hover": "#0d111fcc",
"surface-raised-base": "#242a42",
"surface-float-base": "#242b45",
"surface-float-base-hover": "#2a3154",
"surface-raised-base-hover": "#272e49",
"surface-raised-base-active": "#2c3353",
"surface-raised-strong": "#31385a",
"surface-raised-strong-hover": "#373f6b",
"surface-raised-stronger": "#3b4261",
"surface-raised-stronger-hover": "#444c82",
"surface-weak": "#1b2033",
"surface-weaker": "#181d2d",
"surface-strong": "#323858",
"surface-raised-stronger-non-alpha": "#2b3150",
"surface-diff-add-base": "#1c2a38",
"surface-diff-delete-base": "#2a1f32",
"surface-diff-hidden-base": "#24283b",
"text-base": "#c0caf5",
"text-weak": "#7a88cf",
"text-strong": "#eaeaff",
"syntax-string": "#9ece6a",
"syntax-primitive": "#ff9e64",
"syntax-property": "#bb9af7",
"syntax-type": "#e0af68",
"syntax-constant": "#7dcfff",
"syntax-info": "#7dcfff",
"markdown-heading": "#bb9af7",
"markdown-text": "#c0caf5",
"markdown-link": "#7aa2f7",
"markdown-link-text": "#7dcfff",
"markdown-code": "#9ece6a",
"markdown-block-quote": "#e0af68",
"markdown-emph": "#e0af68",
"markdown-strong": "#ff9e64",
"markdown-horizontal-rule": "#3b4261",
"markdown-list-item": "#7aa2f7",
"markdown-list-enumeration": "#7dcfff",
"markdown-image": "#7aa2f7",
"markdown-image-text": "#7dcfff",
"markdown-code-block": "#c0caf5"
}
}
}
+53
View File
@@ -0,0 +1,53 @@
export type HexColor = `#${string}`
export interface OklchColor {
l: number // Lightness 0-1
c: number // Chroma 0-0.4+
h: number // Hue 0-360
}
export interface ThemeSeedColors {
neutral: HexColor
primary: HexColor
success: HexColor
warning: HexColor
error: HexColor
info: HexColor
interactive: HexColor
diffAdd: HexColor
diffDelete: HexColor
}
export interface ThemeVariant {
seeds: ThemeSeedColors
overrides?: Record<string, ColorValue>
}
export interface DesktopTheme {
$schema?: string
name: string
id: string
light: ThemeVariant
dark: ThemeVariant
}
export type TokenCategory =
| "background"
| "surface"
| "text"
| "border"
| "icon"
| "input"
| "button"
| "syntax"
| "markdown"
| "diff"
| "avatar"
export type ThemeToken = string
export type CssVarRef = `var(--${string})`
export type ColorValue = HexColor | CssVarRef
export type ResolvedTheme = Record<ThemeToken, ColorValue>
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@opencode-ai/util",
"version": "1.0.204",
"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.204",
"version": "1.0.207",
"scripts": {
"dev": "astro dev",
"dev:remote": "VITE_API_URL=https://api.opencode.ai astro dev",
+28 -1
View File
@@ -57,6 +57,33 @@ opencode agent [command]
---
### attach
Attach a terminal to an already running OpenCode backend server started via `serve` or `web` commands.
```bash
opencode attach [url]
```
This allows using the TUI with a remote OpenCode backend. For example:
```bash
# Start the backend server for web/mobile access
opencode web --port 4096 --hostname 0.0.0.0
# In another terminal, attach the TUI to the running backend
opencode attach http://10.20.30.40:4096
```
#### Flags
| Flag | Short | Description |
| ----------- | ----- | --------------------------------- |
| `--dir` | | Working directory to start TUI in |
| `--session` | `-s` | Session ID to continue |
---
#### create
Create a new agent with custom configuration.
@@ -325,7 +352,7 @@ opencode run --attach http://localhost:4096 "Explain async/await in JavaScript"
### serve
Start a headless opencode server for API access. Check out the [server docs](/docs/server) for the full HTTP interface.
Start a headless OpenCode server for API access. Check out the [server docs](/docs/server) for the full HTTP interface.
```bash
opencode serve
+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.
@@ -32,6 +32,8 @@ You can also check out [awesome-opencode](https://github.com/awesome-opencode/aw
| [opencode-md-table-formatter](https://github.com/franlol/opencode-md-table-formatter/tree/main) | Clean up markdown tables produced by LLMs |
| [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.204",
"version": "1.0.207",
"publisher": "sst-dev",
"repository": {
"type": "git",