Compare commits
55
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
106eeb8536 | ||
|
|
888361892d | ||
|
|
a96f3010a6 | ||
|
|
619127c2ed | ||
|
|
6963f96d4b | ||
|
|
05a9e7ce7a | ||
|
|
896d18ab3f | ||
|
|
893888536a | ||
|
|
c6221fc8b3 | ||
|
|
ae67f43ff0 | ||
|
|
76880dce0d | ||
|
|
aafffb5b4b | ||
|
|
a71c9e3f2e | ||
|
|
0156f03e0e | ||
|
|
e0bb96a9f9 | ||
|
|
82e5d6d458 | ||
|
|
a4411c21b6 | ||
|
|
9d61370ac4 | ||
|
|
f3febd6e39 | ||
|
|
f12d55bf1e | ||
|
|
0c19b71f42 | ||
|
|
70fa66397e | ||
|
|
6e8cd3174c | ||
|
|
5bfffbe083 | ||
|
|
29d8557d41 | ||
|
|
ffd20b4477 | ||
|
|
2abaa46e23 | ||
|
|
0cbbb20d22 | ||
|
|
81c5e7b9ed | ||
|
|
ddf4897eaa | ||
|
|
040939fb72 | ||
|
|
f89b83a6d7 | ||
|
|
82a876da4d | ||
|
|
69a15ae9c1 | ||
|
|
18c8e5f451 | ||
|
|
ba3a1cfa0b | ||
|
|
d8563160f7 | ||
|
|
4a9ff9412e | ||
|
|
d6db6ff198 | ||
|
|
79c263494f | ||
|
|
1b5bf32ce5 | ||
|
|
2e972b3fdc | ||
|
|
d70e9fb01e | ||
|
|
fc082a0f14 | ||
|
|
953e4e9446 | ||
|
|
7ea0d37ee3 | ||
|
|
e35d97f9d7 | ||
|
|
2c0d9a46cb | ||
|
|
2fe7a7f2d3 | ||
|
|
8a2f4ddf70 | ||
|
|
7a94d7a2c5 | ||
|
|
de28fafb47 | ||
|
|
9d485dd307 | ||
|
|
613813ac12 | ||
|
|
7617f59441 |
@@ -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) |
|
||||
|
||||
@@ -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
@@ -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
@@ -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,6 +1,6 @@
|
||||
{
|
||||
"name": "@opencode-ai/app",
|
||||
"version": "1.0.204",
|
||||
"version": "1.0.207",
|
||||
"description": "",
|
||||
"type": "module",
|
||||
"exports": {
|
||||
|
||||
+45
-42
@@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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)
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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,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,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": {
|
||||
|
||||
@@ -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,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,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",
|
||||
|
||||
@@ -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,7 +1,7 @@
|
||||
{
|
||||
"name": "@opencode-ai/desktop",
|
||||
"private": true,
|
||||
"version": "1.0.204",
|
||||
"version": "1.0.207",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"typecheck": "tsgo -b",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@opencode-ai/enterprise",
|
||||
"version": "1.0.204",
|
||||
"version": "1.0.207",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
id = "opencode"
|
||||
name = "OpenCode"
|
||||
description = "The open source coding agent."
|
||||
version = "1.0.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,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,3 +2,5 @@ preload = ["@opentui/solid/preload"]
|
||||
|
||||
[test]
|
||||
preload = ["./test/preload.ts"]
|
||||
# Enable code coverage
|
||||
coverage = true
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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),
|
||||
)
|
||||
})
|
||||
}
|
||||
@@ -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({
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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 = {
|
||||
|
||||
@@ -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 user’s 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, workspace‑relative, a/ or b/ diff prefixes, or bare filename/suffix.
|
||||
* Line/column (1‑based, optional): :line[:column] or #Lline[Ccolumn] (column defaults to 1).
|
||||
* Do not use URIs like file://, vscode://, or https://.
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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
|
||||
})
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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")
|
||||
},
|
||||
})
|
||||
})
|
||||
@@ -101,31 +101,31 @@ test("returns empty array when no skills exist", async () => {
|
||||
})
|
||||
})
|
||||
|
||||
// test("discovers skills from .claude/skills/ directory", async () => {
|
||||
// await using tmp = await tmpdir({
|
||||
// git: true,
|
||||
// init: async (dir) => {
|
||||
// const skillDir = path.join(dir, ".claude", "skills", "claude-skill")
|
||||
// await Bun.write(
|
||||
// path.join(skillDir, "SKILL.md"),
|
||||
// `---
|
||||
// name: claude-skill
|
||||
// description: A skill in the .claude/skills directory.
|
||||
// ---
|
||||
test("discovers skills from .claude/skills/ directory", async () => {
|
||||
await using tmp = await tmpdir({
|
||||
git: true,
|
||||
init: async (dir) => {
|
||||
const skillDir = path.join(dir, ".claude", "skills", "claude-skill")
|
||||
await Bun.write(
|
||||
path.join(skillDir, "SKILL.md"),
|
||||
`---
|
||||
name: claude-skill
|
||||
description: A skill in the .claude/skills directory.
|
||||
---
|
||||
|
||||
// # Claude Skill
|
||||
// `,
|
||||
// )
|
||||
// },
|
||||
// })
|
||||
# Claude Skill
|
||||
`,
|
||||
)
|
||||
},
|
||||
})
|
||||
|
||||
// await Instance.provide({
|
||||
// directory: tmp.path,
|
||||
// fn: async () => {
|
||||
// const skills = await Skill.all()
|
||||
// expect(skills.length).toBe(1)
|
||||
// expect(skills[0].name).toBe("claude-skill")
|
||||
// expect(skills[0].location).toContain(".claude/skills/claude-skill/SKILL.md")
|
||||
// },
|
||||
// })
|
||||
// })
|
||||
await Instance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
const skills = await Skill.all()
|
||||
expect(skills.length).toBe(1)
|
||||
expect(skills[0].name).toBe("claude-skill")
|
||||
expect(skills[0].location).toContain(".claude/skills/claude-skill/SKILL.md")
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/package.json",
|
||||
"name": "@opencode-ai/plugin",
|
||||
"version": "1.0.204",
|
||||
"version": "1.0.207",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"typecheck": "tsgo --noEmit",
|
||||
@@ -24,4 +24,4 @@
|
||||
"typescript": "catalog:",
|
||||
"@typescript/native-preview": "catalog:"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,6 +1,6 @@
|
||||
{
|
||||
"name": "@opencode-ai/slack",
|
||||
"version": "1.0.204",
|
||||
"version": "1.0.207",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "bun run src/index.ts",
|
||||
|
||||
@@ -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/*",
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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,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,
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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})`
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
},
|
||||
}
|
||||
},
|
||||
})
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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"
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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 ")
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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,6 +1,6 @@
|
||||
{
|
||||
"name": "@opencode-ai/util",
|
||||
"version": "1.0.204",
|
||||
"version": "1.0.207",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"exports": {
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 |
|
||||
|
||||
---
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"
|
||||
|
||||
Executable
+274
@@ -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
@@ -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(
|
||||
|
||||
@@ -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",
|
||||
|
||||
Reference in New Issue
Block a user