Compare commits

..

13 Commits

Author SHA1 Message Date
Adam
ff748b82ca fix(app): simplify themes (#17274) 2026-03-13 10:12:58 +00:00
Frank
9fafa57562 go: upi pay 2026-03-13 03:24:33 -04:00
Aiden Cline
f8475649da chore: cleanup migrate from global code (#17292) 2026-03-12 23:54:11 -05:00
Michael Dwan
b94e110a4c fix(opencode): sessions lost after git init in existing project (#16814)
Co-authored-by: Aiden Cline <63023139+rekram1-node@users.noreply.github.com>
2026-03-12 23:18:59 -05:00
Luke Parker
f0bba10b12 fix(e2e): fail fast on config dependency installs (#17280) 2026-03-13 14:08:51 +10:00
Adam
d961981e25 fix(app): list item background colors 2026-03-12 22:23:51 -05:00
Adam
5576662200 feat(app): missing themes (#17275) 2026-03-12 22:21:02 -05:00
Tom Ryder
4a2a046d79 fix: filter empty content blocks for Bedrock provider (#14586) 2026-03-12 22:13:09 -05:00
opencode-agent[bot]
8f8c74cfb8 chore: generate 2026-03-13 02:33:45 +00:00
Kit Langton
092f654f63 fix(cli): hide console command from help output (#17277) 2026-03-12 22:22:16 -04:00
Luke Parker
96b1d8f639 fix(app): stabilize todo dock e2e with composer probe (#17267) 2026-03-13 12:21:50 +10:00
opencode-agent[bot]
dcb17c6a67 chore: generate 2026-03-13 02:10:27 +00:00
Kit Langton
dd68b85f58 refactor(provider): effectify ProviderAuthService (#17227) 2026-03-13 02:08:37 +00:00
81 changed files with 3307 additions and 835 deletions

2
.gitignore vendored
View File

@@ -17,7 +17,7 @@ ts-dist
/result
refs
Session.vim
opencode.json
/opencode.json
a.out
target
.scripts

View File

@@ -176,6 +176,25 @@ await page.keyboard.press(`${modKey}+Comma`) // Open settings
- These helpers use the fixture-enabled test-only terminal driver and wait for output after the terminal writer settles.
- Avoid `waitForTimeout` and custom DOM or `data-*` readiness checks.
### Wait on state
- Never use wall-clock waits like `page.waitForTimeout(...)` to make a test pass
- Avoid race-prone flows that assume work is finished after an action
- Wait or poll on observable state with `expect(...)`, `expect.poll(...)`, or existing helpers
- Prefer locator assertions like `toBeVisible()`, `toHaveCount(0)`, and `toHaveAttribute(...)` for normal UI state, and reserve `expect.poll(...)` for probe, mock, or backend state
### Add hooks
- If required state is not observable from the UI, add a small test-only driver or probe in app code instead of sleeps or fragile DOM checks
- Keep these hooks minimal and purpose-built, following the style of `packages/app/src/testing/terminal.ts`
- Test-only hooks must be inert unless explicitly enabled; do not add normal-runtime listeners, reactive subscriptions, or per-update allocations for e2e ceremony
- When mocking routes or APIs, expose explicit mock state and wait on that before asserting post-action UI
### Prefer helpers
- Prefer fluent helpers and drivers when they make intent obvious and reduce locator-heavy noise
- Use direct locators when the interaction is simple and a helper would not add clarity
## Writing New Tests
1. Choose appropriate folder or create new one

View File

@@ -1,12 +1,16 @@
import { test, expect } from "../fixtures"
import { cleanupSession, clearSessionDockSeed, seedSessionQuestion, seedSessionTodos } from "../actions"
import {
composerEvent,
type ComposerDriverState,
type ComposerProbeState,
type ComposerWindow,
} from "../../src/testing/session-composer"
import { cleanupSession, clearSessionDockSeed, seedSessionQuestion } from "../actions"
import {
permissionDockSelector,
promptSelector,
questionDockSelector,
sessionComposerDockSelector,
sessionTodoDockSelector,
sessionTodoListSelector,
sessionTodoToggleButtonSelector,
} from "../selectors"
@@ -42,12 +46,8 @@ async function withDockSeed<T>(sdk: Sdk, sessionID: string, fn: () => Promise<T>
async function clearPermissionDock(page: any, label: RegExp) {
const dock = page.locator(permissionDockSelector)
for (let i = 0; i < 3; i++) {
const count = await dock.count()
if (count === 0) return
await dock.getByRole("button", { name: label }).click()
await page.waitForTimeout(150)
}
await expect(dock).toBeVisible()
await dock.getByRole("button", { name: label }).click()
}
async function setAutoAccept(page: any, enabled: boolean) {
@@ -59,6 +59,120 @@ async function setAutoAccept(page: any, enabled: boolean) {
await expect(button).toHaveAttribute("aria-pressed", enabled ? "true" : "false")
}
async function expectQuestionBlocked(page: any) {
await expect(page.locator(questionDockSelector)).toBeVisible()
await expect(page.locator(promptSelector)).toHaveCount(0)
}
async function expectQuestionOpen(page: any) {
await expect(page.locator(questionDockSelector)).toHaveCount(0)
await expect(page.locator(promptSelector)).toBeVisible()
}
async function expectPermissionBlocked(page: any) {
await expect(page.locator(permissionDockSelector)).toBeVisible()
await expect(page.locator(promptSelector)).toHaveCount(0)
}
async function expectPermissionOpen(page: any) {
await expect(page.locator(permissionDockSelector)).toHaveCount(0)
await expect(page.locator(promptSelector)).toBeVisible()
}
async function todoDock(page: any, sessionID: string) {
await page.addInitScript(() => {
const win = window as ComposerWindow
win.__opencode_e2e = {
...win.__opencode_e2e,
composer: {
enabled: true,
sessions: {},
},
}
})
const write = async (driver: ComposerDriverState | undefined) => {
await page.evaluate(
(input) => {
const win = window as ComposerWindow
const composer = win.__opencode_e2e?.composer
if (!composer?.enabled) throw new Error("Composer e2e driver is not enabled")
composer.sessions ??= {}
const prev = composer.sessions[input.sessionID] ?? {}
if (!input.driver) {
if (!prev.probe) {
delete composer.sessions[input.sessionID]
} else {
composer.sessions[input.sessionID] = { probe: prev.probe }
}
} else {
composer.sessions[input.sessionID] = {
...prev,
driver: input.driver,
}
}
window.dispatchEvent(new CustomEvent(input.event, { detail: { sessionID: input.sessionID } }))
},
{ event: composerEvent, sessionID, driver },
)
}
const read = () =>
page.evaluate((sessionID) => {
const win = window as ComposerWindow
return win.__opencode_e2e?.composer?.sessions?.[sessionID]?.probe ?? null
}, sessionID) as Promise<ComposerProbeState | null>
const api = {
async clear() {
await write(undefined)
return api
},
async open(todos: NonNullable<ComposerDriverState["todos"]>) {
await write({ live: true, todos })
return api
},
async finish(todos: NonNullable<ComposerDriverState["todos"]>) {
await write({ live: false, todos })
return api
},
async expectOpen(states: ComposerProbeState["states"]) {
await expect.poll(read, { timeout: 10_000 }).toMatchObject({
mounted: true,
collapsed: false,
hidden: false,
count: states.length,
states,
})
return api
},
async expectCollapsed(states: ComposerProbeState["states"]) {
await expect.poll(read, { timeout: 10_000 }).toMatchObject({
mounted: true,
collapsed: true,
hidden: true,
count: states.length,
states,
})
return api
},
async expectClosed() {
await expect.poll(read, { timeout: 10_000 }).toMatchObject({ mounted: false })
return api
},
async collapse() {
await page.locator(sessionTodoToggleButtonSelector).click()
return api
},
async expand() {
await page.locator(sessionTodoToggleButtonSelector).click()
return api
},
}
return api
}
async function withMockPermission<T>(
page: any,
request: {
@@ -70,7 +184,7 @@ async function withMockPermission<T>(
always?: string[]
},
opts: { child?: any } | undefined,
fn: () => Promise<T>,
fn: (state: { resolved: () => Promise<void> }) => Promise<T>,
) {
let pending = [
{
@@ -119,8 +233,14 @@ async function withMockPermission<T>(
if (sessionList) await page.route("**/session?*", sessionList)
const state = {
async resolved() {
await expect.poll(() => pending.length, { timeout: 10_000 }).toBe(0)
},
}
try {
return await fn()
return await fn(state)
} finally {
await page.unroute("**/permission", list)
await page.unroute("**/session/*/permissions/*", reply)
@@ -173,14 +293,12 @@ test("blocked question flow unblocks after submit", async ({ page, sdk, gotoSess
})
const dock = page.locator(questionDockSelector)
await expect.poll(() => dock.count(), { timeout: 10_000 }).toBe(1)
await expect(page.locator(promptSelector)).toHaveCount(0)
await expectQuestionBlocked(page)
await dock.locator('[data-slot="question-option"]').first().click()
await dock.getByRole("button", { name: /submit/i }).click()
await expect.poll(() => page.locator(questionDockSelector).count(), { timeout: 10_000 }).toBe(0)
await expect(page.locator(promptSelector)).toBeVisible()
await expectQuestionOpen(page)
})
})
})
@@ -199,15 +317,14 @@ test("blocked permission flow supports allow once", async ({ page, sdk, gotoSess
metadata: { description: "Need permission for command" },
},
undefined,
async () => {
async (state) => {
await page.goto(page.url())
await expect.poll(() => page.locator(permissionDockSelector).count(), { timeout: 10_000 }).toBe(1)
await expect(page.locator(promptSelector)).toHaveCount(0)
await expectPermissionBlocked(page)
await clearPermissionDock(page, /allow once/i)
await state.resolved()
await page.goto(page.url())
await expect.poll(() => page.locator(permissionDockSelector).count(), { timeout: 10_000 }).toBe(0)
await expect(page.locator(promptSelector)).toBeVisible()
await expectPermissionOpen(page)
},
)
})
@@ -226,15 +343,14 @@ test("blocked permission flow supports reject", async ({ page, sdk, gotoSession
patterns: ["/tmp/opencode-e2e-perm-reject"],
},
undefined,
async () => {
async (state) => {
await page.goto(page.url())
await expect.poll(() => page.locator(permissionDockSelector).count(), { timeout: 10_000 }).toBe(1)
await expect(page.locator(promptSelector)).toHaveCount(0)
await expectPermissionBlocked(page)
await clearPermissionDock(page, /deny/i)
await state.resolved()
await page.goto(page.url())
await expect.poll(() => page.locator(permissionDockSelector).count(), { timeout: 10_000 }).toBe(0)
await expect(page.locator(promptSelector)).toBeVisible()
await expectPermissionOpen(page)
},
)
})
@@ -254,15 +370,14 @@ test("blocked permission flow supports allow always", async ({ page, sdk, gotoSe
metadata: { description: "Need permission for command" },
},
undefined,
async () => {
async (state) => {
await page.goto(page.url())
await expect.poll(() => page.locator(permissionDockSelector).count(), { timeout: 10_000 }).toBe(1)
await expect(page.locator(promptSelector)).toHaveCount(0)
await expectPermissionBlocked(page)
await clearPermissionDock(page, /allow always/i)
await state.resolved()
await page.goto(page.url())
await expect.poll(() => page.locator(permissionDockSelector).count(), { timeout: 10_000 }).toBe(0)
await expect(page.locator(promptSelector)).toBeVisible()
await expectPermissionOpen(page)
},
)
})
@@ -301,14 +416,12 @@ test("child session question request blocks parent dock and unblocks after submi
})
const dock = page.locator(questionDockSelector)
await expect.poll(() => dock.count(), { timeout: 10_000 }).toBe(1)
await expect(page.locator(promptSelector)).toHaveCount(0)
await expectQuestionBlocked(page)
await dock.locator('[data-slot="question-option"]').first().click()
await dock.getByRole("button", { name: /submit/i }).click()
await expect.poll(() => page.locator(questionDockSelector).count(), { timeout: 10_000 }).toBe(0)
await expect(page.locator(promptSelector)).toBeVisible()
await expectQuestionOpen(page)
})
} finally {
await cleanupSession({ sdk, sessionID: child.id })
@@ -344,17 +457,15 @@ test("child session permission request blocks parent dock and supports allow onc
metadata: { description: "Need child permission" },
},
{ child },
async () => {
async (state) => {
await page.goto(page.url())
const dock = page.locator(permissionDockSelector)
await expect.poll(() => dock.count(), { timeout: 10_000 }).toBe(1)
await expect(page.locator(promptSelector)).toHaveCount(0)
await expectPermissionBlocked(page)
await clearPermissionDock(page, /allow once/i)
await state.resolved()
await page.goto(page.url())
await expect.poll(() => page.locator(permissionDockSelector).count(), { timeout: 10_000 }).toBe(0)
await expect(page.locator(promptSelector)).toBeVisible()
await expectPermissionOpen(page)
},
)
} finally {
@@ -365,36 +476,31 @@ test("child session permission request blocks parent dock and supports allow onc
test("todo dock transitions and collapse behavior", async ({ page, sdk, gotoSession }) => {
await withDockSession(sdk, "e2e composer dock todo", async (session) => {
await withDockSeed(sdk, session.id, async () => {
await gotoSession(session.id)
const dock = await todoDock(page, session.id)
await gotoSession(session.id)
await expect(page.locator(sessionComposerDockSelector)).toBeVisible()
await seedSessionTodos(sdk, {
sessionID: session.id,
todos: [
{ content: "first task", status: "pending", priority: "high" },
{ content: "second task", status: "in_progress", priority: "medium" },
],
})
try {
await dock.open([
{ content: "first task", status: "pending", priority: "high" },
{ content: "second task", status: "in_progress", priority: "medium" },
])
await dock.expectOpen(["pending", "in_progress"])
await expect.poll(() => page.locator(sessionTodoDockSelector).count(), { timeout: 10_000 }).toBe(1)
await expect(page.locator(sessionTodoListSelector)).toBeVisible()
await dock.collapse()
await dock.expectCollapsed(["pending", "in_progress"])
await page.locator(sessionTodoToggleButtonSelector).click()
await expect(page.locator(sessionTodoListSelector)).toBeHidden()
await dock.expand()
await dock.expectOpen(["pending", "in_progress"])
await page.locator(sessionTodoToggleButtonSelector).click()
await expect(page.locator(sessionTodoListSelector)).toBeVisible()
await seedSessionTodos(sdk, {
sessionID: session.id,
todos: [
{ content: "first task", status: "completed", priority: "high" },
{ content: "second task", status: "cancelled", priority: "medium" },
],
})
await expect.poll(() => page.locator(sessionTodoDockSelector).count(), { timeout: 10_000 }).toBe(0)
})
await dock.finish([
{ content: "first task", status: "completed", priority: "high" },
{ content: "second task", status: "cancelled", priority: "medium" },
])
await dock.expectClosed()
} finally {
await dock.clear()
}
})
})
@@ -414,8 +520,7 @@ test("keyboard focus stays off prompt while blocked", async ({ page, sdk, gotoSe
],
})
await expect.poll(() => page.locator(questionDockSelector).count(), { timeout: 10_000 }).toBe(1)
await expect(page.locator(promptSelector)).toHaveCount(0)
await expectQuestionBlocked(page)
await page.locator("main").click({ position: { x: 5, y: 5 } })
await page.keyboard.type("abc")

View File

@@ -73,6 +73,7 @@ const serverEnv = {
OPENCODE_E2E_MESSAGE: "Seeded for UI e2e",
OPENCODE_E2E_MODEL: "opencode/gpt-5-nano",
OPENCODE_CLIENT: "app",
OPENCODE_STRICT_CONFIG_DEPS: "true",
} satisfies Record<string, string>
const runnerEnv = {

View File

@@ -121,7 +121,7 @@ function ServerForm(props: ServerFormProps) {
return (
<div class="px-5">
<div class="bg-surface-raised-base rounded-md p-5 flex flex-col gap-3">
<div class="bg-surface-base rounded-md p-5 flex flex-col gap-3">
<div class="flex-1 min-w-0 [&_[data-slot=input-wrapper]]:relative">
<TextField
type="text"
@@ -542,7 +542,7 @@ export function DialogSelectServer() {
if (x) select(x)
}}
divider={true}
class="px-5 [&_[data-slot=list-search-wrapper]]:w-full [&_[data-slot=list-scroll]]h-[300px] [&_[data-slot=list-scroll]]:overflow-y-auto [&_[data-slot=list-items]]:bg-surface-raised-base [&_[data-slot=list-items]]:rounded-md [&_[data-slot=list-item]]:min-h-14 [&_[data-slot=list-item]]:p-3 [&_[data-slot=list-item]]:!bg-transparent"
class="px-5 [&_[data-slot=list-search-wrapper]]:w-full [&_[data-slot=list-scroll]]h-[300px] [&_[data-slot=list-scroll]]:overflow-y-auto [&_[data-slot=list-items]]:bg-surface-base [&_[data-slot=list-items]]:rounded-md [&_[data-slot=list-item]]:min-h-14 [&_[data-slot=list-item]]:p-3 [&_[data-slot=list-item]]:!bg-transparent"
>
{(i) => {
const key = ServerConnection.key(i)

View File

@@ -12,6 +12,7 @@ import { usePlatform } from "@/context/platform"
import { useSettings, monoFontFamily } from "@/context/settings"
import { playSound, SOUND_OPTIONS } from "@/utils/sound"
import { Link } from "./link"
import { SettingsList } from "./settings-list"
let demoSoundState = {
cleanup: undefined as (() => void) | undefined,
@@ -177,7 +178,7 @@ export const SettingsGeneral: Component = () => {
const GeneralSection = () => (
<div class="flex flex-col gap-1">
<div class="bg-surface-raised-base px-4 rounded-lg">
<SettingsList>
<SettingsRow
title={language.t("settings.general.row.language.title")}
description={language.t("settings.general.row.language.description")}
@@ -248,7 +249,7 @@ export const SettingsGeneral: Component = () => {
triggerStyle={{ "min-width": "180px" }}
/>
</SettingsRow>
</div>
</SettingsList>
</div>
)
@@ -256,7 +257,7 @@ export const SettingsGeneral: Component = () => {
<div class="flex flex-col gap-1">
<h3 class="text-14-medium text-text-strong pb-2">{language.t("settings.general.section.appearance")}</h3>
<div class="bg-surface-raised-base px-4 rounded-lg">
<SettingsList>
<SettingsRow
title={language.t("settings.general.row.colorScheme.title")}
description={language.t("settings.general.row.colorScheme.description")}
@@ -333,7 +334,7 @@ export const SettingsGeneral: Component = () => {
)}
</Select>
</SettingsRow>
</div>
</SettingsList>
</div>
)
@@ -341,7 +342,7 @@ export const SettingsGeneral: Component = () => {
<div class="flex flex-col gap-1">
<h3 class="text-14-medium text-text-strong pb-2">{language.t("settings.general.section.notifications")}</h3>
<div class="bg-surface-raised-base px-4 rounded-lg">
<SettingsList>
<SettingsRow
title={language.t("settings.general.notifications.agent.title")}
description={language.t("settings.general.notifications.agent.description")}
@@ -377,7 +378,7 @@ export const SettingsGeneral: Component = () => {
/>
</div>
</SettingsRow>
</div>
</SettingsList>
</div>
)
@@ -385,7 +386,7 @@ export const SettingsGeneral: Component = () => {
<div class="flex flex-col gap-1">
<h3 class="text-14-medium text-text-strong pb-2">{language.t("settings.general.section.sounds")}</h3>
<div class="bg-surface-raised-base px-4 rounded-lg">
<SettingsList>
<SettingsRow
title={language.t("settings.general.sounds.agent.title")}
description={language.t("settings.general.sounds.agent.description")}
@@ -430,7 +431,7 @@ export const SettingsGeneral: Component = () => {
)}
/>
</SettingsRow>
</div>
</SettingsList>
</div>
)
@@ -438,7 +439,7 @@ export const SettingsGeneral: Component = () => {
<div class="flex flex-col gap-1">
<h3 class="text-14-medium text-text-strong pb-2">{language.t("settings.general.section.updates")}</h3>
<div class="bg-surface-raised-base px-4 rounded-lg">
<SettingsList>
<SettingsRow
title={language.t("settings.updates.row.startup.title")}
description={language.t("settings.updates.row.startup.description")}
@@ -474,7 +475,7 @@ export const SettingsGeneral: Component = () => {
: language.t("settings.updates.action.checkNow")}
</Button>
</SettingsRow>
</div>
</SettingsList>
</div>
)
@@ -504,7 +505,7 @@ export const SettingsGeneral: Component = () => {
<div class="flex flex-col gap-1">
<h3 class="text-14-medium text-text-strong pb-2">{language.t("settings.desktop.section.wsl")}</h3>
<div class="bg-surface-raised-base px-4 rounded-lg">
<SettingsList>
<SettingsRow
title={language.t("settings.desktop.wsl.title")}
description={language.t("settings.desktop.wsl.description")}
@@ -517,7 +518,7 @@ export const SettingsGeneral: Component = () => {
/>
</div>
</SettingsRow>
</div>
</SettingsList>
</div>
)
}}
@@ -537,7 +538,7 @@ export const SettingsGeneral: Component = () => {
<div class="flex flex-col gap-1">
<h3 class="text-14-medium text-text-strong pb-2">{language.t("settings.general.section.display")}</h3>
<div class="bg-surface-raised-base px-4 rounded-lg">
<SettingsList>
<SettingsRow
title={
<div class="flex items-center gap-2">
@@ -555,7 +556,7 @@ export const SettingsGeneral: Component = () => {
<Switch checked={value() === "wayland"} onChange={onChange} />
</div>
</SettingsRow>
</div>
</SettingsList>
</div>
)
}}

View File

@@ -9,6 +9,7 @@ import fuzzysort from "fuzzysort"
import { formatKeybind, parseKeybind, useCommand } from "@/context/command"
import { useLanguage } from "@/context/language"
import { useSettings } from "@/context/settings"
import { SettingsList } from "./settings-list"
const IS_MAC = typeof navigator === "object" && /(Mac|iPod|iPhone|iPad)/.test(navigator.platform)
const PALETTE_ID = "command.palette"
@@ -406,7 +407,7 @@ export const SettingsKeybinds: Component = () => {
<Show when={(filtered().get(group) ?? []).length > 0}>
<div class="flex flex-col gap-1">
<h3 class="text-14-medium text-text-strong pb-2">{language.t(groupKey[group])}</h3>
<div class="bg-surface-raised-base px-4 rounded-lg">
<SettingsList>
<For each={filtered().get(group) ?? []}>
{(id) => (
<div class="flex items-center justify-between gap-4 py-3 border-b border-border-weak-base last:border-none">
@@ -432,7 +433,7 @@ export const SettingsKeybinds: Component = () => {
</div>
)}
</For>
</div>
</SettingsList>
</div>
</Show>
)}

View File

@@ -0,0 +1,5 @@
import { type Component, type JSX } from "solid-js"
export const SettingsList: Component<{ children: JSX.Element }> = (props) => {
return <div class="bg-surface-base px-4 rounded-lg">{props.children}</div>
}

View File

@@ -8,6 +8,7 @@ import { type Component, For, Show } from "solid-js"
import { useLanguage } from "@/context/language"
import { useModels } from "@/context/models"
import { popularProviders } from "@/hooks/use-providers"
import { SettingsList } from "./settings-list"
type ModelItem = ReturnType<ReturnType<typeof useModels>["list"]>[number]
@@ -100,7 +101,7 @@ export const SettingsModels: Component = () => {
<ProviderIcon id={group.category} class="size-5 shrink-0 icon-strong-base" />
<span class="text-14-medium text-text-strong">{group.items[0].provider.name}</span>
</div>
<div class="bg-surface-raised-base px-4 rounded-lg">
<SettingsList>
<For each={group.items}>
{(item) => {
const key = { providerID: item.provider.id, modelID: item.id }
@@ -124,7 +125,7 @@ export const SettingsModels: Component = () => {
)
}}
</For>
</div>
</SettingsList>
</div>
)}
</For>

View File

@@ -11,6 +11,7 @@ import { useGlobalSync } from "@/context/global-sync"
import { DialogConnectProvider } from "./dialog-connect-provider"
import { DialogSelectProvider } from "./dialog-select-provider"
import { DialogCustomProvider } from "./dialog-custom-provider"
import { SettingsList } from "./settings-list"
type ProviderSource = "env" | "api" | "config" | "custom"
type ProviderItem = ReturnType<ReturnType<typeof useProviders>["connected"]>[number]
@@ -136,7 +137,7 @@ export const SettingsProviders: Component = () => {
<div class="flex flex-col gap-8 max-w-[720px]">
<div class="flex flex-col gap-1" data-component="connected-providers-section">
<h3 class="text-14-medium text-text-strong pb-2">{language.t("settings.providers.section.connected")}</h3>
<div class="bg-surface-raised-base px-4 rounded-lg">
<SettingsList>
<Show
when={connected().length > 0}
fallback={
@@ -169,12 +170,12 @@ export const SettingsProviders: Component = () => {
)}
</For>
</Show>
</div>
</SettingsList>
</div>
<div class="flex flex-col gap-1">
<h3 class="text-14-medium text-text-strong pb-2">{language.t("settings.providers.section.popular")}</h3>
<div class="bg-surface-raised-base px-4 rounded-lg">
<SettingsList>
<For each={popular()}>
{(item) => (
<div class="flex flex-wrap items-center justify-between gap-4 min-h-16 py-3 border-b border-border-weak-base last:border-none">
@@ -232,7 +233,7 @@ export const SettingsProviders: Component = () => {
{language.t("common.connect")}
</Button>
</div>
</div>
</SettingsList>
<Button
variant="ghost"

View File

@@ -44,9 +44,9 @@ export function SessionComposerRegion(props: {
}) {
const prompt = usePrompt()
const language = useLanguage()
const { sessionKey } = useSessionKey()
const route = useSessionKey()
const handoffPrompt = createMemo(() => getSessionHandoff(sessionKey())?.prompt)
const handoffPrompt = createMemo(() => getSessionHandoff(route.sessionKey())?.prompt)
const previewPrompt = () =>
prompt
@@ -62,7 +62,7 @@ export function SessionComposerRegion(props: {
createEffect(() => {
if (!prompt.ready()) return
setSessionHandoff(sessionKey(), { prompt: previewPrompt() })
setSessionHandoff(route.sessionKey(), { prompt: previewPrompt() })
})
const [store, setStore] = createStore({
@@ -85,7 +85,7 @@ export function SessionComposerRegion(props: {
}
createEffect(() => {
sessionKey()
route.sessionKey()
const ready = props.ready
const delay = 140
@@ -194,6 +194,7 @@ export function SessionComposerRegion(props: {
>
<div ref={(el) => setStore("body", el)}>
<SessionTodoDock
sessionID={route.params.id}
todos={props.state.todos()}
title={language.t("session.todo.title")}
collapseLabel={language.t("session.todo.collapse")}

View File

@@ -1,4 +1,4 @@
import { createEffect, createMemo, on, onCleanup } from "solid-js"
import { createEffect, createMemo, on, onCleanup, onMount } from "solid-js"
import { createStore } from "solid-js/store"
import type { PermissionRequest, QuestionRequest, Todo } from "@opencode-ai/sdk/v2"
import { useParams } from "@solidjs/router"
@@ -8,6 +8,7 @@ import { useLanguage } from "@/context/language"
import { usePermission } from "@/context/permission"
import { useSDK } from "@/context/sdk"
import { useSync } from "@/context/sync"
import { composerDriver, composerEnabled, composerEvent } from "@/testing/session-composer"
import { sessionPermissionRequest, sessionQuestionRequest } from "./session-request-tree"
export const todoState = (input: {
@@ -47,7 +48,50 @@ export function createSessionComposerState(options?: { closeMs?: number | (() =>
return !!permissionRequest() || !!questionRequest()
})
const [test, setTest] = createStore({
on: false,
live: undefined as boolean | undefined,
todos: undefined as Todo[] | undefined,
})
const pull = () => {
const id = params.id
if (!id) {
setTest({ on: false, live: undefined, todos: undefined })
return
}
const next = composerDriver(id)
if (!next) {
setTest({ on: false, live: undefined, todos: undefined })
return
}
setTest({
on: true,
live: next.live,
todos: next.todos?.map((todo) => ({ ...todo })),
})
}
onMount(() => {
if (!composerEnabled()) return
pull()
createEffect(on(() => params.id, pull, { defer: true }))
const onEvent = (event: Event) => {
const detail = (event as CustomEvent<{ sessionID?: string }>).detail
if (detail?.sessionID !== params.id) return
pull()
}
window.addEventListener(composerEvent, onEvent)
onCleanup(() => window.removeEventListener(composerEvent, onEvent))
})
const todos = createMemo((): Todo[] => {
if (test.on && test.todos !== undefined) return test.todos
const id = params.id
if (!id) return []
return globalSync.data.session_todo[id] ?? []
@@ -64,7 +108,10 @@ export function createSessionComposerState(options?: { closeMs?: number | (() =>
})
const busy = createMemo(() => status().type !== "idle")
const live = createMemo(() => busy() || blocked())
const live = createMemo(() => {
if (test.on && test.live !== undefined) return test.live
return busy() || blocked()
})
const [store, setStore] = createStore({
responding: undefined as string | undefined,
@@ -116,6 +163,10 @@ export function createSessionComposerState(options?: { closeMs?: number | (() =>
// Keep stale turn todos from reopening if the model never clears them.
const clear = () => {
if (test.on && test.todos !== undefined) {
setTest("todos", [])
return
}
const id = params.id
if (!id) return
globalSync.todo.set(id, [])

View File

@@ -8,6 +8,7 @@ import { TextReveal } from "@opencode-ai/ui/text-reveal"
import { TextStrikethrough } from "@opencode-ai/ui/text-strikethrough"
import { Index, createEffect, createMemo, on, onCleanup } from "solid-js"
import { createStore } from "solid-js/store"
import { composerEnabled, composerProbe } from "@/testing/session-composer"
function dot(status: Todo["status"]) {
if (status !== "in_progress") return undefined
@@ -35,6 +36,7 @@ function dot(status: Todo["status"]) {
}
export function SessionTodoDock(props: {
sessionID?: string
todos: Todo[]
title: string
collapseLabel: string
@@ -69,6 +71,8 @@ export function SessionTodoDock(props: {
const off = createMemo(() => hide() > 0.98)
const turn = createMemo(() => Math.max(0, Math.min(1, value())))
const full = createMemo(() => Math.max(78, store.height))
const e2e = composerEnabled()
const probe = composerProbe(props.sessionID)
let contentRef: HTMLDivElement | undefined
createEffect(() => {
@@ -83,6 +87,23 @@ export function SessionTodoDock(props: {
onCleanup(() => observer.disconnect())
})
createEffect(() => {
if (!e2e) return
probe.set({
mounted: true,
collapsed: store.collapsed,
hidden: store.collapsed || off(),
count: props.todos.length,
states: props.todos.map((todo) => todo.status),
})
})
onCleanup(() => {
if (!e2e) return
probe.drop()
})
return (
<DockTray
data-component="session-todo-dock"

View File

@@ -0,0 +1,84 @@
import type { Todo } from "@opencode-ai/sdk/v2"
export const composerEvent = "opencode:e2e:composer"
export type ComposerDriverState = {
live?: boolean
todos?: Array<Pick<Todo, "content" | "status" | "priority">>
}
export type ComposerProbeState = {
mounted: boolean
collapsed: boolean
hidden: boolean
count: number
states: Todo["status"][]
}
type ComposerState = {
driver?: ComposerDriverState
probe?: ComposerProbeState
}
export type ComposerWindow = Window & {
__opencode_e2e?: {
composer?: {
enabled?: boolean
sessions?: Record<string, ComposerState>
}
}
}
const clone = (driver: ComposerDriverState) => ({
live: driver.live,
todos: driver.todos?.map((todo) => ({ ...todo })),
})
export const composerEnabled = () => {
if (typeof window === "undefined") return false
return (window as ComposerWindow).__opencode_e2e?.composer?.enabled === true
}
const root = () => {
if (!composerEnabled()) return
const state = (window as ComposerWindow).__opencode_e2e?.composer
if (!state) return
state.sessions ??= {}
return state.sessions
}
export const composerDriver = (sessionID?: string) => {
if (!sessionID) return
const state = root()?.[sessionID]?.driver
if (!state) return
return clone(state)
}
export const composerProbe = (sessionID?: string) => {
const set = (next: ComposerProbeState) => {
if (!sessionID) return
const sessions = root()
if (!sessions) return
const prev = sessions[sessionID] ?? {}
sessions[sessionID] = {
...prev,
probe: {
...next,
states: [...next.states],
},
}
}
return {
set,
drop() {
set({
mounted: false,
collapsed: false,
hidden: true,
count: 0,
states: [],
})
},
}
}

View File

@@ -76,6 +76,14 @@ export function IconAlipay(props: JSX.SvgSVGAttributes<SVGSVGElement>) {
)
}
export function IconWechat(props: JSX.SvgSVGAttributes<SVGSVGElement>) {
return (
<svg {...props} viewBox="0 0 24 24" fill="currentColor" xmlns="http://www.w3.org/2000/svg">
<path d="M9.27099 14.6689C8.9532 14.8312 8.56403 14.7122 8.39132 14.4L8.3477 14.3054L6.53019 10.3069C6.52269 10.2588 6.52269 10.2097 6.53019 10.1615C6.53017 10.0735 6.56564 9.98916 6.62857 9.9276C6.6915 9.86603 6.7766 9.83243 6.86462 9.83438C6.93567 9.83269 7.00508 9.85582 7.06091 9.89981L9.24191 11.4265C9.40329 11.5346 9.59293 11.5928 9.78716 11.5937C9.90424 11.5945 10.0203 11.5723 10.1289 11.5283L20.176 7.02816C18.091 4.72544 15.1103 3.43931 12.0045 3.5022C6.4793 3.5022 2.00098 7.23172 2.00098 11.87C2.06681 14.4052 3.35646 16.7515 5.4615 18.1658C5.6878 18.3326 5.78402 18.6241 5.70141 18.8928L5.25067 20.594C5.22336 20.6714 5.20625 20.7521 5.19978 20.8339C5.19777 20.9232 5.23236 21.0094 5.29552 21.0726C5.35868 21.1358 5.44491 21.1703 5.5342 21.1684C5.60098 21.1645 5.66583 21.1445 5.72322 21.1102L7.90423 19.8452C8.06383 19.7467 8.2474 19.6939 8.43494 19.6925C8.53352 19.6923 8.63157 19.707 8.72574 19.7361C9.78781 20.0363 10.8863 20.188 11.99 20.1869C17.5152 20.1869 22.001 16.4574 22.001 11.8554C22.0108 10.4834 21.6301 9.13687 20.903 7.97326L9.35096 14.6253L9.27099 14.6689Z" />
</svg>
)
}
export function IconChevron(props: JSX.SvgSVGAttributes<SVGSVGElement>) {
return (
<svg {...props} width="8" height="6" viewBox="0 0 8 6" fill="none" xmlns="http://www.w3.org/2000/svg">

View File

@@ -541,6 +541,7 @@ export const dict = {
"workspace.billing.addAction": "إضافة",
"workspace.billing.addBalance": "إضافة رصيد",
"workspace.billing.alipay": "Alipay",
"workspace.billing.wechat": "WeChat Pay",
"workspace.billing.linkedToStripe": "مرتبط بـ Stripe",
"workspace.billing.manage": "إدارة",
"workspace.billing.enable": "تمكين الفوترة",

View File

@@ -550,6 +550,7 @@ export const dict = {
"workspace.billing.addAction": "Adicionar",
"workspace.billing.addBalance": "Adicionar Saldo",
"workspace.billing.alipay": "Alipay",
"workspace.billing.wechat": "WeChat Pay",
"workspace.billing.linkedToStripe": "Vinculado ao Stripe",
"workspace.billing.manage": "Gerenciar",
"workspace.billing.enable": "Ativar Faturamento",

View File

@@ -546,6 +546,7 @@ export const dict = {
"workspace.billing.addAction": "Tilføj",
"workspace.billing.addBalance": "Tilføj saldo",
"workspace.billing.alipay": "Alipay",
"workspace.billing.wechat": "WeChat Pay",
"workspace.billing.linkedToStripe": "Forbundet til Stripe",
"workspace.billing.manage": "Administrer",
"workspace.billing.enable": "Aktiver fakturering",

View File

@@ -549,6 +549,7 @@ export const dict = {
"workspace.billing.addAction": "Hinzufügen",
"workspace.billing.addBalance": "Guthaben aufladen",
"workspace.billing.alipay": "Alipay",
"workspace.billing.wechat": "WeChat Pay",
"workspace.billing.linkedToStripe": "Mit Stripe verbunden",
"workspace.billing.manage": "Verwalten",
"workspace.billing.enable": "Abrechnung aktivieren",

View File

@@ -541,6 +541,7 @@ export const dict = {
"workspace.billing.addAction": "Add",
"workspace.billing.addBalance": "Add Balance",
"workspace.billing.alipay": "Alipay",
"workspace.billing.wechat": "WeChat Pay",
"workspace.billing.linkedToStripe": "Linked to Stripe",
"workspace.billing.manage": "Manage",
"workspace.billing.enable": "Enable Billing",

View File

@@ -550,6 +550,7 @@ export const dict = {
"workspace.billing.addAction": "Añadir",
"workspace.billing.addBalance": "Añadir Saldo",
"workspace.billing.alipay": "Alipay",
"workspace.billing.wechat": "WeChat Pay",
"workspace.billing.linkedToStripe": "Vinculado con Stripe",
"workspace.billing.manage": "Gestionar",
"workspace.billing.enable": "Habilitar Facturación",

View File

@@ -552,6 +552,7 @@ export const dict = {
"workspace.billing.addAction": "Ajouter",
"workspace.billing.addBalance": "Ajouter un solde",
"workspace.billing.alipay": "Alipay",
"workspace.billing.wechat": "WeChat Pay",
"workspace.billing.linkedToStripe": "Lié à Stripe",
"workspace.billing.manage": "Gérer",
"workspace.billing.enable": "Activer la facturation",

View File

@@ -548,6 +548,7 @@ export const dict = {
"workspace.billing.addAction": "Aggiungi",
"workspace.billing.addBalance": "Aggiungi Saldo",
"workspace.billing.alipay": "Alipay",
"workspace.billing.wechat": "WeChat Pay",
"workspace.billing.linkedToStripe": "Collegato a Stripe",
"workspace.billing.manage": "Gestisci",
"workspace.billing.enable": "Abilita Fatturazione",

View File

@@ -547,6 +547,7 @@ export const dict = {
"workspace.billing.addAction": "追加",
"workspace.billing.addBalance": "残高を追加",
"workspace.billing.alipay": "Alipay",
"workspace.billing.wechat": "WeChat Pay",
"workspace.billing.linkedToStripe": "Stripeと連携済み",
"workspace.billing.manage": "管理",
"workspace.billing.enable": "課金を有効にする",

View File

@@ -541,6 +541,7 @@ export const dict = {
"workspace.billing.addAction": "추가",
"workspace.billing.addBalance": "잔액 추가",
"workspace.billing.alipay": "Alipay",
"workspace.billing.wechat": "WeChat Pay",
"workspace.billing.linkedToStripe": "Stripe에 연결됨",
"workspace.billing.manage": "관리",
"workspace.billing.enable": "결제 활성화",

View File

@@ -547,6 +547,7 @@ export const dict = {
"workspace.billing.addAction": "Legg til",
"workspace.billing.addBalance": "Legg til saldo",
"workspace.billing.alipay": "Alipay",
"workspace.billing.wechat": "WeChat Pay",
"workspace.billing.linkedToStripe": "Koblet til Stripe",
"workspace.billing.manage": "Administrer",
"workspace.billing.enable": "Aktiver fakturering",

View File

@@ -548,6 +548,7 @@ export const dict = {
"workspace.billing.addAction": "Dodaj",
"workspace.billing.addBalance": "Doładuj saldo",
"workspace.billing.alipay": "Alipay",
"workspace.billing.wechat": "WeChat Pay",
"workspace.billing.linkedToStripe": "Połączono ze Stripe",
"workspace.billing.manage": "Zarządzaj",
"workspace.billing.enable": "Włącz rozliczenia",

View File

@@ -554,6 +554,7 @@ export const dict = {
"workspace.billing.addAction": "Пополнить",
"workspace.billing.addBalance": "Пополнить баланс",
"workspace.billing.alipay": "Alipay",
"workspace.billing.wechat": "WeChat Pay",
"workspace.billing.linkedToStripe": "Привязано к Stripe",
"workspace.billing.manage": "Управление",
"workspace.billing.enable": "Включить оплату",

View File

@@ -543,6 +543,7 @@ export const dict = {
"workspace.billing.addAction": "เพิ่ม",
"workspace.billing.addBalance": "เพิ่มยอดคงเหลือ",
"workspace.billing.alipay": "Alipay",
"workspace.billing.wechat": "WeChat Pay",
"workspace.billing.linkedToStripe": "เชื่อมโยงกับ Stripe",
"workspace.billing.manage": "จัดการ",
"workspace.billing.enable": "เปิดใช้งานการเรียกเก็บเงิน",

View File

@@ -550,6 +550,7 @@ export const dict = {
"workspace.billing.addAction": "Ekle",
"workspace.billing.addBalance": "Bakiye Ekle",
"workspace.billing.alipay": "Alipay",
"workspace.billing.wechat": "WeChat Pay",
"workspace.billing.linkedToStripe": "Stripe'a bağlı",
"workspace.billing.manage": "Yönet",
"workspace.billing.enable": "Faturalandırmayı Etkinleştir",

View File

@@ -524,6 +524,7 @@ export const dict = {
"workspace.billing.addAction": "充值",
"workspace.billing.addBalance": "充值余额",
"workspace.billing.alipay": "支付宝",
"workspace.billing.wechat": "微信支付",
"workspace.billing.linkedToStripe": "已关联 Stripe",
"workspace.billing.manage": "管理",
"workspace.billing.enable": "启用计费",

View File

@@ -524,6 +524,7 @@ export const dict = {
"workspace.billing.addAction": "儲值",
"workspace.billing.addBalance": "儲值餘額",
"workspace.billing.alipay": "支付寶",
"workspace.billing.wechat": "微信支付",
"workspace.billing.linkedToStripe": "已連結 Stripe",
"workspace.billing.manage": "管理",
"workspace.billing.enable": "啟用帳務",

View File

@@ -3,7 +3,7 @@ import { createMemo, Match, Show, Switch, createEffect } from "solid-js"
import { createStore } from "solid-js/store"
import { Billing } from "@opencode-ai/console-core/billing.js"
import { withActor } from "~/context/auth.withActor"
import { IconAlipay, IconCreditCard, IconStripe } from "~/component/icon"
import { IconAlipay, IconCreditCard, IconStripe, IconWechat } from "~/component/icon"
import styles from "./billing-section.module.css"
import { createCheckoutUrl, formatBalance, queryBillingInfo } from "../../common"
import { useI18n } from "~/context/i18n"
@@ -208,6 +208,9 @@ export function BillingSection() {
<Match when={billingInfo()?.paymentMethodType === "alipay"}>
<IconAlipay style={{ width: "24px", height: "24px" }} />
</Match>
<Match when={billingInfo()?.paymentMethodType === "wechat_pay"}>
<IconWechat style={{ width: "24px", height: "24px" }} />
</Match>
</Switch>
</div>
<div data-slot="card-details">
@@ -224,6 +227,9 @@ export function BillingSection() {
<Match when={billingInfo()?.paymentMethodType === "alipay"}>
<span data-slot="type">{i18n.t("workspace.billing.alipay")}</span>
</Match>
<Match when={billingInfo()?.paymentMethodType === "wechat_pay"}>
<span data-slot="type">{i18n.t("workspace.billing.wechat")}</span>
</Match>
</Switch>
</div>
<button

View File

@@ -213,12 +213,10 @@ export namespace Billing {
enabled: true,
},
payment_method_options: {
alipay: {},
card: {
setup_future_usage: "on_session",
},
},
payment_method_types: ["card", "alipay"],
//payment_method_data: {
// allow_redisplay: "always",
//},
@@ -269,7 +267,6 @@ export namespace Billing {
customer_email: email!,
}),
currency: "usd",
payment_method_types: ["card", "alipay"],
tax_id_collection: {
enabled: true,
},

View File

@@ -10,15 +10,20 @@ const now = Date.now()
const seed = async () => {
const { Instance } = await import("../src/project/instance")
const { InstanceBootstrap } = await import("../src/project/bootstrap")
const { Config } = await import("../src/config/config")
const { Session } = await import("../src/session")
const { MessageID, PartID } = await import("../src/session/schema")
const { Project } = await import("../src/project/project")
const { ModelID, ProviderID } = await import("../src/provider/schema")
const { ToolRegistry } = await import("../src/tool/registry")
await Instance.provide({
directory: dir,
init: InstanceBootstrap,
fn: async () => {
await Config.waitForDependencies()
await ToolRegistry.ids()
const session = await Session.create({ title })
const messageID = MessageID.ascending()
const partID = PartID.ascending()

View File

@@ -4,7 +4,6 @@ import { Log } from "../util/log"
import path from "path"
import { Filesystem } from "../util/filesystem"
import { NamedError } from "@opencode-ai/util/error"
import { text } from "node:stream/consumers"
import { Lock } from "../util/lock"
import { PackageRegistry } from "./registry"
import { proxied } from "@/util/proxied"
@@ -13,32 +12,29 @@ import { Process } from "../util/process"
export namespace BunProc {
const log = Log.create({ service: "bun" })
export async function run(cmd: string[], options?: Process.Options) {
export async function run(cmd: string[], options?: Process.RunOptions) {
const full = [which(), ...cmd]
log.info("running", {
cmd: [which(), ...cmd],
cmd: full,
...options,
})
const result = Process.spawn([which(), ...cmd], {
...options,
stdout: "pipe",
stderr: "pipe",
const result = await Process.run(full, {
cwd: options?.cwd,
abort: options?.abort,
kill: options?.kill,
timeout: options?.timeout,
nothrow: options?.nothrow,
env: {
...process.env,
...options?.env,
BUN_BE_BUN: "1",
},
})
const code = await result.exited
const stdout = result.stdout ? await text(result.stdout) : undefined
const stderr = result.stderr ? await text(result.stderr) : undefined
log.info("done", {
code,
stdout,
stderr,
code: result.code,
stdout: result.stdout.toString(),
stderr: result.stderr.toString(),
})
if (code !== 0) {
throw new Error(`Command failed with exit code ${code}`)
}
return result
}

View File

@@ -195,7 +195,7 @@ export const OrgsCommand = cmd({
export const ConsoleCommand = cmd({
command: "console",
describe: "manage console account",
describe: false,
builder: (yargs) =>
yargs
.command({

View File

@@ -36,6 +36,7 @@ import { iife } from "@/util/iife"
import { Account } from "@/account"
import { ConfigPaths } from "./paths"
import { Filesystem } from "@/util/filesystem"
import { Process } from "@/util/process"
export namespace Config {
const ModelId = z.string().meta({ $ref: "https://models.dev/model-schema.json#/$defs/Model" })
@@ -296,6 +297,26 @@ export namespace Config {
],
{ cwd: dir },
).catch((err) => {
if (err instanceof Process.RunFailedError) {
const detail = {
dir,
cmd: err.cmd,
code: err.code,
stdout: err.stdout.toString(),
stderr: err.stderr.toString(),
}
if (Flag.OPENCODE_STRICT_CONFIG_DEPS) {
log.error("failed to install dependencies", detail)
throw err
}
log.warn("failed to install dependencies", detail)
return
}
if (Flag.OPENCODE_STRICT_CONFIG_DEPS) {
log.error("failed to install dependencies", { dir, error: err })
throw err
}
log.warn("failed to install dependencies", { dir, error: err })
})
}

View File

@@ -63,6 +63,7 @@ export namespace Flag {
export const OPENCODE_MODELS_PATH = process.env["OPENCODE_MODELS_PATH"]
export const OPENCODE_DISABLE_CHANNEL_DB = truthy("OPENCODE_DISABLE_CHANNEL_DB")
export const OPENCODE_SKIP_MIGRATIONS = truthy("OPENCODE_SKIP_MIGRATIONS")
export const OPENCODE_STRICT_CONFIG_DEPS = truthy("OPENCODE_STRICT_CONFIG_DEPS")
function number(key: string) {
const value = process.env[key]

View File

@@ -1,3 +1,4 @@
import { Effect } from "effect"
import { Log } from "@/util/log"
import { Context } from "../util/context"
import { Project } from "./project"
@@ -5,6 +6,7 @@ import { State } from "./state"
import { iife } from "@/util/iife"
import { GlobalBus } from "@/bus/global"
import { Filesystem } from "@/util/filesystem"
import { InstanceState } from "@/util/instance-state"
interface Context {
directory: string
@@ -106,7 +108,7 @@ export const Instance = {
async reload(input: { directory: string; init?: () => Promise<any>; project?: Project.Info; worktree?: string }) {
const directory = Filesystem.resolve(input.directory)
Log.Default.info("reloading instance", { directory })
await State.dispose(directory)
await Promise.all([State.dispose(directory), Effect.runPromise(InstanceState.dispose(directory))])
cache.delete(directory)
const next = track(directory, boot({ ...input, directory }))
emit(directory)
@@ -114,7 +116,7 @@ export const Instance = {
},
async dispose() {
Log.Default.info("disposing instance", { directory: Instance.directory })
await State.dispose(Instance.directory)
await Promise.all([State.dispose(Instance.directory), Effect.runPromise(InstanceState.dispose(Instance.directory))])
cache.delete(Instance.directory)
emit(Instance.directory)
},

View File

@@ -1,12 +1,11 @@
import z from "zod"
import { Filesystem } from "../util/filesystem"
import path from "path"
import { Database, eq } from "../storage/db"
import { and, Database, eq } from "../storage/db"
import { ProjectTable } from "./project.sql"
import { SessionTable } from "../session/session.sql"
import { Log } from "../util/log"
import { Flag } from "@/flag/flag"
import { work } from "../util/queue"
import { fn } from "@opencode-ai/util/fn"
import { BusEvent } from "@/bus/bus-event"
import { iife } from "@/util/iife"
@@ -218,23 +217,18 @@ export namespace Project {
})
const row = Database.use((db) => db.select().from(ProjectTable).where(eq(ProjectTable.id, data.id)).get())
const existing = await iife(async () => {
if (row) return fromRow(row)
const fresh: Info = {
id: data.id,
worktree: data.worktree,
vcs: data.vcs as Info["vcs"],
sandboxes: [],
time: {
created: Date.now(),
updated: Date.now(),
},
}
if (data.id !== ProjectID.global) {
await migrateFromGlobal(data.id, data.worktree)
}
return fresh
})
const existing = row
? fromRow(row)
: {
id: data.id,
worktree: data.worktree,
vcs: data.vcs as Info["vcs"],
sandboxes: [] as string[],
time: {
created: Date.now(),
updated: Date.now(),
},
}
if (Flag.OPENCODE_EXPERIMENTAL_ICON_DISCOVERY) discover(existing)
@@ -277,6 +271,18 @@ export namespace Project {
Database.use((db) =>
db.insert(ProjectTable).values(insert).onConflictDoUpdate({ target: ProjectTable.id, set: updateSet }).run(),
)
// Runs after upsert so the target project row exists (FK constraint).
// Runs on every startup because sessions created before git init
// accumulate under "global" and need migrating whenever they appear.
if (data.id !== ProjectID.global) {
Database.use((db) =>
db
.update(SessionTable)
.set({ project_id: data.id })
.where(and(eq(SessionTable.project_id, ProjectID.global), eq(SessionTable.directory, data.worktree)))
.run(),
)
}
GlobalBus.emit("event", {
payload: {
type: Event.Updated.type,
@@ -310,28 +316,6 @@ export namespace Project {
return
}
async function migrateFromGlobal(id: ProjectID, worktree: string) {
const row = Database.use((db) => db.select().from(ProjectTable).where(eq(ProjectTable.id, ProjectID.global)).get())
if (!row) return
const sessions = Database.use((db) =>
db.select().from(SessionTable).where(eq(SessionTable.project_id, ProjectID.global)).all(),
)
if (sessions.length === 0) return
log.info("migrating sessions from global", { newProjectID: id, worktree, count: sessions.length })
await work(10, sessions, async (row) => {
// Skip sessions that belong to a different directory
if (row.directory && row.directory !== worktree) return
log.info("migrating session", { sessionID: row.id, from: ProjectID.global, to: id })
Database.use((db) => db.update(SessionTable).set({ project_id: id }).where(eq(SessionTable.id, row.id)).run())
}).catch((error) => {
log.error("failed to migrate sessions from global to project", { error, projectId: id })
})
}
export function setInitialized(id: ProjectID) {
Database.use((db) =>
db

View File

@@ -0,0 +1,169 @@
import { Effect, Layer, Record, ServiceMap, Struct } from "effect"
import { Instance } from "@/project/instance"
import { Plugin } from "../plugin"
import { filter, fromEntries, map, pipe } from "remeda"
import type { AuthOuathResult } from "@opencode-ai/plugin"
import { NamedError } from "@opencode-ai/util/error"
import * as Auth from "@/auth/service"
import { InstanceState } from "@/util/instance-state"
import { ProviderID } from "./schema"
import z from "zod"
export const Method = z
.object({
type: z.union([z.literal("oauth"), z.literal("api")]),
label: z.string(),
})
.meta({
ref: "ProviderAuthMethod",
})
export type Method = z.infer<typeof Method>
export const Authorization = z
.object({
url: z.string(),
method: z.union([z.literal("auto"), z.literal("code")]),
instructions: z.string(),
})
.meta({
ref: "ProviderAuthAuthorization",
})
export type Authorization = z.infer<typeof Authorization>
export const OauthMissing = NamedError.create(
"ProviderAuthOauthMissing",
z.object({
providerID: ProviderID.zod,
}),
)
export const OauthCodeMissing = NamedError.create(
"ProviderAuthOauthCodeMissing",
z.object({
providerID: ProviderID.zod,
}),
)
export const OauthCallbackFailed = NamedError.create("ProviderAuthOauthCallbackFailed", z.object({}))
export type ProviderAuthError =
| Auth.AuthServiceError
| InstanceType<typeof OauthMissing>
| InstanceType<typeof OauthCodeMissing>
| InstanceType<typeof OauthCallbackFailed>
export namespace ProviderAuthService {
export interface Service {
/** Get available auth methods for each provider (e.g. OAuth, API key). */
readonly methods: () => Effect.Effect<Record<string, Method[]>>
/** Start an OAuth authorization flow for a provider. Returns the URL to redirect to. */
readonly authorize: (input: { providerID: ProviderID; method: number }) => Effect.Effect<Authorization | undefined>
/** Complete an OAuth flow after the user has authorized. Exchanges the code/callback for credentials. */
readonly callback: (input: {
providerID: ProviderID
method: number
code?: string
}) => Effect.Effect<void, ProviderAuthError>
/** Set an API key directly for a provider (no OAuth flow). */
readonly api: (input: { providerID: ProviderID; key: string }) => Effect.Effect<void, Auth.AuthServiceError>
}
}
export class ProviderAuthService extends ServiceMap.Service<ProviderAuthService, ProviderAuthService.Service>()(
"@opencode/ProviderAuth",
) {
static readonly layer = Layer.effect(
ProviderAuthService,
Effect.gen(function* () {
const auth = yield* Auth.AuthService
const state = yield* InstanceState.make({
lookup: () =>
Effect.promise(async () => {
const methods = pipe(
await Plugin.list(),
filter((x) => x.auth?.provider !== undefined),
map((x) => [x.auth!.provider, x.auth!] as const),
fromEntries(),
)
return { methods, pending: new Map<ProviderID, AuthOuathResult>() }
}),
})
const methods = Effect.fn("ProviderAuthService.methods")(function* () {
const x = yield* InstanceState.get(state)
return Record.map(x.methods, (y) => y.methods.map((z): Method => Struct.pick(z, ["type", "label"])))
})
const authorize = Effect.fn("ProviderAuthService.authorize")(function* (input: {
providerID: ProviderID
method: number
}) {
const s = yield* InstanceState.get(state)
const method = s.methods[input.providerID].methods[input.method]
if (method.type !== "oauth") return
const result = yield* Effect.promise(() => method.authorize())
s.pending.set(input.providerID, result)
return {
url: result.url,
method: result.method,
instructions: result.instructions,
}
})
const callback = Effect.fn("ProviderAuthService.callback")(function* (input: {
providerID: ProviderID
method: number
code?: string
}) {
const s = yield* InstanceState.get(state)
const match = s.pending.get(input.providerID)
if (!match) return yield* Effect.fail(new OauthMissing({ providerID: input.providerID }))
if (match.method === "code" && !input.code)
return yield* Effect.fail(new OauthCodeMissing({ providerID: input.providerID }))
const result = yield* Effect.promise(() =>
match.method === "code" ? match.callback(input.code!) : match.callback(),
)
if (!result || result.type !== "success") return yield* Effect.fail(new OauthCallbackFailed({}))
if ("key" in result) {
yield* auth.set(input.providerID, {
type: "api",
key: result.key,
})
}
if ("refresh" in result) {
yield* auth.set(input.providerID, {
type: "oauth",
access: result.access,
refresh: result.refresh,
expires: result.expires,
...(result.accountId ? { accountId: result.accountId } : {}),
})
}
})
const api = Effect.fn("ProviderAuthService.api")(function* (input: { providerID: ProviderID; key: string }) {
yield* auth.set(input.providerID, {
type: "api",
key: input.key,
})
})
return ProviderAuthService.of({
methods,
authorize,
callback,
api,
})
}),
)
static readonly defaultLayer = ProviderAuthService.layer.pipe(Layer.provide(Auth.AuthService.defaultLayer))
}

View File

@@ -1,75 +1,36 @@
import { Instance } from "@/project/instance"
import { Plugin } from "../plugin"
import { map, filter, pipe, fromEntries, mapValues } from "remeda"
import { Effect, ManagedRuntime } from "effect"
import z from "zod"
import { fn } from "@/util/fn"
import type { AuthOuathResult, Hooks } from "@opencode-ai/plugin"
import { NamedError } from "@opencode-ai/util/error"
import { Auth } from "@/auth"
import * as S from "./auth-service"
import { ProviderID } from "./schema"
export namespace ProviderAuth {
const state = Instance.state(async () => {
const methods = pipe(
await Plugin.list(),
filter((x) => x.auth?.provider !== undefined),
map((x) => [x.auth!.provider, x.auth!] as const),
fromEntries(),
)
return { methods, pending: {} as Record<string, AuthOuathResult> }
})
// Separate runtime: ProviderAuthService can't join the shared runtime because
// runtime.ts → auth-service.ts → provider/auth.ts creates a circular import.
// AuthService is stateless file I/O so the duplicate instance is harmless.
const rt = ManagedRuntime.make(S.ProviderAuthService.defaultLayer)
export const Method = z
.object({
type: z.union([z.literal("oauth"), z.literal("api")]),
label: z.string(),
})
.meta({
ref: "ProviderAuthMethod",
})
export type Method = z.infer<typeof Method>
function runPromise<A>(f: (service: S.ProviderAuthService.Service) => Effect.Effect<A, S.ProviderAuthError>) {
return rt.runPromise(S.ProviderAuthService.use(f))
}
export namespace ProviderAuth {
export const Method = S.Method
export type Method = S.Method
export async function methods() {
const s = await state().then((x) => x.methods)
return mapValues(s, (x) =>
x.methods.map(
(y): Method => ({
type: y.type,
label: y.label,
}),
),
)
return runPromise((service) => service.methods())
}
export const Authorization = z
.object({
url: z.string(),
method: z.union([z.literal("auto"), z.literal("code")]),
instructions: z.string(),
})
.meta({
ref: "ProviderAuthAuthorization",
})
export type Authorization = z.infer<typeof Authorization>
export const Authorization = S.Authorization
export type Authorization = S.Authorization
export const authorize = fn(
z.object({
providerID: ProviderID.zod,
method: z.number(),
}),
async (input): Promise<Authorization | undefined> => {
const auth = await state().then((s) => s.methods[input.providerID])
const method = auth.methods[input.method]
if (method.type === "oauth") {
const result = await method.authorize()
await state().then((s) => (s.pending[input.providerID] = result))
return {
url: result.url,
method: result.method,
instructions: result.instructions,
}
}
},
async (input): Promise<Authorization | undefined> => runPromise((service) => service.authorize(input)),
)
export const callback = fn(
@@ -78,44 +39,7 @@ export namespace ProviderAuth {
method: z.number(),
code: z.string().optional(),
}),
async (input) => {
const match = await state().then((s) => s.pending[input.providerID])
if (!match) throw new OauthMissing({ providerID: input.providerID })
let result
if (match.method === "code") {
if (!input.code) throw new OauthCodeMissing({ providerID: input.providerID })
result = await match.callback(input.code)
}
if (match.method === "auto") {
result = await match.callback()
}
if (result?.type === "success") {
if ("key" in result) {
await Auth.set(input.providerID, {
type: "api",
key: result.key,
})
}
if ("refresh" in result) {
const info: Auth.Info = {
type: "oauth",
access: result.access,
refresh: result.refresh,
expires: result.expires,
}
if (result.accountId) {
info.accountId = result.accountId
}
await Auth.set(input.providerID, info)
}
return
}
throw new OauthCallbackFailed({})
},
async (input) => runPromise((service) => service.callback(input)),
)
export const api = fn(
@@ -123,26 +47,10 @@ export namespace ProviderAuth {
providerID: ProviderID.zod,
key: z.string(),
}),
async (input) => {
await Auth.set(input.providerID, {
type: "api",
key: input.key,
})
},
async (input) => runPromise((service) => service.api(input)),
)
export const OauthMissing = NamedError.create(
"ProviderAuthOauthMissing",
z.object({
providerID: ProviderID.zod,
}),
)
export const OauthCodeMissing = NamedError.create(
"ProviderAuthOauthCodeMissing",
z.object({
providerID: ProviderID.zod,
}),
)
export const OauthCallbackFailed = NamedError.create("ProviderAuthOauthCallbackFailed", z.object({}))
export import OauthMissing = S.OauthMissing
export import OauthCodeMissing = S.OauthCodeMissing
export import OauthCallbackFailed = S.OauthCallbackFailed
}

View File

@@ -51,7 +51,7 @@ export namespace ProviderTransform {
): ModelMessage[] {
// Anthropic rejects messages with empty content - filter out empty string messages
// and remove empty text/reasoning parts from array content
if (model.api.npm === "@ai-sdk/anthropic") {
if (model.api.npm === "@ai-sdk/anthropic" || model.api.npm === "@ai-sdk/amazon-bedrock") {
msgs = msgs
.map((msg) => {
if (typeof msg.content === "string") {

View File

@@ -1,92 +0,0 @@
import { Schema, SchemaAST } from "effect"
import z from "zod"
export function zod<S extends Schema.Top>(schema: S): z.ZodType<Schema.Schema.Type<S>> {
return walk(schema.ast) as z.ZodType<Schema.Schema.Type<S>>
}
function walk(ast: SchemaAST.AST): z.ZodTypeAny {
const out = body(ast)
const desc = SchemaAST.resolveDescription(ast)
const ref = SchemaAST.resolveIdentifier(ast)
const next = desc ? out.describe(desc) : out
return ref ? next.meta({ ref }) : next
}
function body(ast: SchemaAST.AST): z.ZodTypeAny {
if (SchemaAST.isOptional(ast)) return opt(ast)
switch (ast._tag) {
case "String":
return z.string()
case "Number":
return z.number()
case "Boolean":
return z.boolean()
case "Null":
return z.null()
case "Undefined":
return z.undefined()
case "Any":
case "Unknown":
return z.unknown()
case "Never":
return z.never()
case "Literal":
return z.literal(ast.literal)
case "Union":
return union(ast)
case "Objects":
return object(ast)
case "Arrays":
return array(ast)
case "Declaration":
return decl(ast)
default:
return fail(ast)
}
}
function opt(ast: SchemaAST.AST): z.ZodTypeAny {
if (ast._tag !== "Union") return fail(ast)
const items = ast.types.filter((item) => item._tag !== "Undefined")
if (items.length === 1) return walk(items[0]).optional()
if (items.length > 1)
return z.union(items.map(walk) as [z.ZodTypeAny, z.ZodTypeAny, ...Array<z.ZodTypeAny>]).optional()
return z.undefined().optional()
}
function union(ast: SchemaAST.Union): z.ZodTypeAny {
const items = ast.types.map(walk)
if (items.length === 1) return items[0]
if (items.length < 2) return fail(ast)
return z.union(items as [z.ZodTypeAny, z.ZodTypeAny, ...Array<z.ZodTypeAny>])
}
function object(ast: SchemaAST.Objects): z.ZodTypeAny {
if (ast.propertySignatures.length === 0 && ast.indexSignatures.length === 1) {
const sig = ast.indexSignatures[0]
if (sig.parameter._tag !== "String") return fail(ast)
return z.record(z.string(), walk(sig.type))
}
if (ast.indexSignatures.length > 0) return fail(ast)
return z.object(Object.fromEntries(ast.propertySignatures.map((sig) => [String(sig.name), walk(sig.type)])))
}
function array(ast: SchemaAST.Arrays): z.ZodTypeAny {
if (ast.elements.length > 0) return fail(ast)
if (ast.rest.length !== 1) return fail(ast)
return z.array(walk(ast.rest[0]))
}
function decl(ast: SchemaAST.Declaration): z.ZodTypeAny {
if (ast.typeParameters.length !== 1) return fail(ast)
return walk(ast.typeParameters[0])
}
function fail(ast: SchemaAST.AST): never {
const ref = SchemaAST.resolveIdentifier(ast)
throw new Error(`unsupported effect schema: ${ref ?? ast._tag}`)
}

View File

@@ -0,0 +1,51 @@
import { Effect, ScopedCache, Scope } from "effect"
import { Instance } from "@/project/instance"
const TypeId = Symbol.for("@opencode/InstanceState")
type Task = (key: string) => Effect.Effect<void>
const tasks = new Set<Task>()
export namespace InstanceState {
export interface State<A, E = never, R = never> {
readonly [TypeId]: typeof TypeId
readonly cache: ScopedCache.ScopedCache<string, A, E, R>
}
export const make = <A, E = never, R = never>(input: {
lookup: (key: string) => Effect.Effect<A, E, R>
release?: (value: A, key: string) => Effect.Effect<void>
}): Effect.Effect<State<A, E, R>, never, R | Scope.Scope> =>
Effect.gen(function* () {
const cache = yield* ScopedCache.make<string, A, E, R>({
capacity: Number.POSITIVE_INFINITY,
lookup: (key) =>
Effect.acquireRelease(input.lookup(key), (value) =>
input.release ? input.release(value, key) : Effect.void,
),
})
const task: Task = (key) => ScopedCache.invalidate(cache, key)
tasks.add(task)
yield* Effect.addFinalizer(() => Effect.sync(() => void tasks.delete(task)))
return {
[TypeId]: TypeId,
cache,
}
})
export const get = <A, E, R>(self: State<A, E, R>) => ScopedCache.get(self.cache, Instance.directory)
export const has = <A, E, R>(self: State<A, E, R>) => ScopedCache.has(self.cache, Instance.directory)
export const invalidate = <A, E, R>(self: State<A, E, R>) => ScopedCache.invalidate(self.cache, Instance.directory)
export const dispose = (key: string) =>
Effect.all(
[...tasks].map((task) => task(key)),
{ concurrency: "unbounded" },
)
}

View File

@@ -42,6 +42,8 @@ export async function tmpdir<T>(options?: TmpDirOptions<T>) {
if (options?.git) {
await $`git init`.cwd(dirpath).quiet()
await $`git config core.fsmonitor false`.cwd(dirpath).quiet()
await $`git config user.email "test@opencode.test"`.cwd(dirpath).quiet()
await $`git config user.name "Test"`.cwd(dirpath).quiet()
await $`git commit --allow-empty -m "root commit ${dirpath}"`.cwd(dirpath).quiet()
}
if (options?.config) {

View File

@@ -0,0 +1,140 @@
import { describe, expect, test } from "bun:test"
import { Project } from "../../src/project/project"
import { Database, eq } from "../../src/storage/db"
import { SessionTable } from "../../src/session/session.sql"
import { ProjectTable } from "../../src/project/project.sql"
import { ProjectID } from "../../src/project/schema"
import { SessionID } from "../../src/session/schema"
import { Log } from "../../src/util/log"
import { $ } from "bun"
import { tmpdir } from "../fixture/fixture"
Log.init({ print: false })
function uid() {
return SessionID.make(crypto.randomUUID())
}
function seed(opts: { id: SessionID; dir: string; project: ProjectID }) {
const now = Date.now()
Database.use((db) =>
db
.insert(SessionTable)
.values({
id: opts.id,
project_id: opts.project,
slug: opts.id,
directory: opts.dir,
title: "test",
version: "0.0.0-test",
time_created: now,
time_updated: now,
})
.run(),
)
}
function ensureGlobal() {
Database.use((db) =>
db
.insert(ProjectTable)
.values({
id: ProjectID.global,
worktree: "/",
time_created: Date.now(),
time_updated: Date.now(),
sandboxes: [],
})
.onConflictDoNothing()
.run(),
)
}
describe("migrateFromGlobal", () => {
test("migrates global sessions on first project creation", async () => {
// 1. Start with git init but no commits — creates "global" project row
await using tmp = await tmpdir()
await $`git init`.cwd(tmp.path).quiet()
await $`git config user.name "Test"`.cwd(tmp.path).quiet()
await $`git config user.email "test@opencode.test"`.cwd(tmp.path).quiet()
const { project: pre } = await Project.fromDirectory(tmp.path)
expect(pre.id).toBe(ProjectID.global)
// 2. Seed a session under "global" with matching directory
const id = uid()
seed({ id, dir: tmp.path, project: ProjectID.global })
// 3. Make a commit so the project gets a real ID
await $`git commit --allow-empty -m "root"`.cwd(tmp.path).quiet()
const { project: real } = await Project.fromDirectory(tmp.path)
expect(real.id).not.toBe(ProjectID.global)
// 4. The session should have been migrated to the real project ID
const row = Database.use((db) => db.select().from(SessionTable).where(eq(SessionTable.id, id)).get())
expect(row).toBeDefined()
expect(row!.project_id).toBe(real.id)
})
test("migrates global sessions even when project row already exists", async () => {
// 1. Create a repo with a commit — real project ID created immediately
await using tmp = await tmpdir({ git: true })
const { project } = await Project.fromDirectory(tmp.path)
expect(project.id).not.toBe(ProjectID.global)
// 2. Ensure "global" project row exists (as it would from a prior no-git session)
ensureGlobal()
// 3. Seed a session under "global" with matching directory.
// This simulates a session created before git init that wasn't
// present when the real project row was first created.
const id = uid()
seed({ id, dir: tmp.path, project: ProjectID.global })
// 4. Call fromDirectory again — project row already exists,
// so the current code skips migration entirely. This is the bug.
await Project.fromDirectory(tmp.path)
const row = Database.use((db) => db.select().from(SessionTable).where(eq(SessionTable.id, id)).get())
expect(row).toBeDefined()
expect(row!.project_id).toBe(project.id)
})
test("does not claim sessions with empty directory", async () => {
await using tmp = await tmpdir({ git: true })
const { project } = await Project.fromDirectory(tmp.path)
expect(project.id).not.toBe(ProjectID.global)
ensureGlobal()
// Legacy sessions may lack a directory value.
// Without a matching origin directory, they should remain global.
const id = uid()
seed({ id, dir: "", project: ProjectID.global })
await Project.fromDirectory(tmp.path)
const row = Database.use((db) => db.select().from(SessionTable).where(eq(SessionTable.id, id)).get())
expect(row).toBeDefined()
expect(row!.project_id).toBe(ProjectID.global)
})
test("does not steal sessions from unrelated directories", async () => {
await using tmp = await tmpdir({ git: true })
const { project } = await Project.fromDirectory(tmp.path)
expect(project.id).not.toBe(ProjectID.global)
ensureGlobal()
// Seed a session under "global" but for a DIFFERENT directory
const id = uid()
seed({ id, dir: "/some/other/dir", project: ProjectID.global })
await Project.fromDirectory(tmp.path)
const row = Database.use((db) => db.select().from(SessionTable).where(eq(SessionTable.id, id)).get())
expect(row).toBeDefined()
// Should remain under "global" — not stolen
expect(row!.project_id).toBe(ProjectID.global)
})
})

View File

@@ -0,0 +1,115 @@
import { afterEach, expect, test } from "bun:test"
import { Instance } from "../../src/project/instance"
import { tmpdir } from "../fixture/fixture"
afterEach(async () => {
await Instance.disposeAll()
})
test("Instance.state caches values for the same instance", async () => {
await using tmp = await tmpdir()
let n = 0
const state = Instance.state(() => ({ n: ++n }))
await Instance.provide({
directory: tmp.path,
fn: async () => {
const a = state()
const b = state()
expect(a).toBe(b)
expect(n).toBe(1)
},
})
})
test("Instance.state isolates values by directory", async () => {
await using a = await tmpdir()
await using b = await tmpdir()
let n = 0
const state = Instance.state(() => ({ n: ++n }))
const x = await Instance.provide({
directory: a.path,
fn: async () => state(),
})
const y = await Instance.provide({
directory: b.path,
fn: async () => state(),
})
const z = await Instance.provide({
directory: a.path,
fn: async () => state(),
})
expect(x).toBe(z)
expect(x).not.toBe(y)
expect(n).toBe(2)
})
test("Instance.state is disposed on instance reload", async () => {
await using tmp = await tmpdir()
const seen: string[] = []
let n = 0
const state = Instance.state(
() => ({ n: ++n }),
async (value) => {
seen.push(String(value.n))
},
)
const a = await Instance.provide({
directory: tmp.path,
fn: async () => state(),
})
await Instance.reload({ directory: tmp.path })
const b = await Instance.provide({
directory: tmp.path,
fn: async () => state(),
})
expect(a).not.toBe(b)
expect(seen).toEqual(["1"])
})
test("Instance.state is disposed on disposeAll", async () => {
await using a = await tmpdir()
await using b = await tmpdir()
const seen: string[] = []
const state = Instance.state(
() => ({ dir: Instance.directory }),
async (value) => {
seen.push(value.dir)
},
)
await Instance.provide({
directory: a.path,
fn: async () => state(),
})
await Instance.provide({
directory: b.path,
fn: async () => state(),
})
await Instance.disposeAll()
expect(seen.sort()).toEqual([a.path, b.path].sort())
})
test("Instance.state dedupes concurrent promise initialization", async () => {
await using tmp = await tmpdir()
let n = 0
const state = Instance.state(async () => {
n += 1
await Bun.sleep(10)
return { n }
})
const [a, b] = await Instance.provide({
directory: tmp.path,
fn: async () => Promise.all([state(), state()]),
})
expect(a).toBe(b)
expect(n).toBe(1)
})

View File

@@ -0,0 +1,20 @@
import { afterEach, expect, test } from "bun:test"
import { Auth } from "../../src/auth"
import { ProviderAuth } from "../../src/provider/auth"
import { ProviderID } from "../../src/provider/schema"
afterEach(async () => {
await Auth.remove("test-provider-auth")
})
test("ProviderAuth.api persists auth via AuthService", async () => {
await ProviderAuth.api({
providerID: ProviderID.make("test-provider-auth"),
key: "sk-test",
})
expect(await Auth.get("test-provider-auth")).toEqual({
type: "api",
key: "sk-test",
})
})

View File

@@ -1096,6 +1096,38 @@ describe("ProviderTransform.message - anthropic empty content filtering", () =>
expect(result[0].content[1]).toEqual({ type: "text", text: "Result" })
})
test("filters empty content for bedrock provider", () => {
const bedrockModel = {
...anthropicModel,
id: "amazon-bedrock/anthropic.claude-opus-4-6",
providerID: "amazon-bedrock",
api: {
id: "anthropic.claude-opus-4-6",
url: "https://bedrock-runtime.us-east-1.amazonaws.com",
npm: "@ai-sdk/amazon-bedrock",
},
}
const msgs = [
{ role: "user", content: "Hello" },
{ role: "assistant", content: "" },
{
role: "assistant",
content: [
{ type: "text", text: "" },
{ type: "text", text: "Answer" },
],
},
] as any[]
const result = ProviderTransform.message(msgs, bedrockModel, {})
expect(result).toHaveLength(2)
expect(result[0].content).toBe("Hello")
expect(result[1].content).toHaveLength(1)
expect(result[1].content[0]).toEqual({ type: "text", text: "Answer" })
})
test("does not filter for non-anthropic providers", () => {
const openaiModel = {
...anthropicModel,

View File

@@ -1,61 +0,0 @@
import { describe, expect, test } from "bun:test"
import { Schema } from "effect"
import { zod } from "../../src/util/effect-zod"
describe("util.effect-zod", () => {
test("converts class schemas for route dto shapes", () => {
class Method extends Schema.Class<Method>("ProviderAuthMethod")({
type: Schema.Union([Schema.Literal("oauth"), Schema.Literal("api")]),
label: Schema.String,
}) {}
const out = zod(Method)
expect(out.meta()?.ref).toBe("ProviderAuthMethod")
expect(
out.parse({
type: "oauth",
label: "OAuth",
}),
).toEqual({
type: "oauth",
label: "OAuth",
})
})
test("converts structs with optional fields, arrays, and records", () => {
const out = zod(
Schema.Struct({
foo: Schema.optional(Schema.String),
bar: Schema.Array(Schema.Number),
baz: Schema.Record(Schema.String, Schema.Boolean),
}),
)
expect(
out.parse({
bar: [1, 2],
baz: { ok: true },
}),
).toEqual({
bar: [1, 2],
baz: { ok: true },
})
expect(
out.parse({
foo: "hi",
bar: [1],
baz: { ok: false },
}),
).toEqual({
foo: "hi",
bar: [1],
baz: { ok: false },
})
})
test("throws for unsupported tuple schemas", () => {
expect(() => zod(Schema.Tuple([Schema.String, Schema.Number]))).toThrow("unsupported effect schema")
})
})

View File

@@ -0,0 +1,139 @@
import { afterEach, expect, test } from "bun:test"
import { Effect } from "effect"
import { Instance } from "../../src/project/instance"
import { InstanceState } from "../../src/util/instance-state"
import { tmpdir } from "../fixture/fixture"
async function access<A, E>(state: InstanceState.State<A, E>, dir: string) {
return Instance.provide({
directory: dir,
fn: () => Effect.runPromise(InstanceState.get(state)),
})
}
afterEach(async () => {
await Instance.disposeAll()
})
test("InstanceState caches values for the same instance", async () => {
await using tmp = await tmpdir()
let n = 0
await Effect.runPromise(
Effect.scoped(
Effect.gen(function* () {
const state = yield* InstanceState.make({
lookup: () => Effect.sync(() => ({ n: ++n })),
})
const a = yield* Effect.promise(() => access(state, tmp.path))
const b = yield* Effect.promise(() => access(state, tmp.path))
expect(a).toBe(b)
expect(n).toBe(1)
}),
),
)
})
test("InstanceState isolates values by directory", async () => {
await using a = await tmpdir()
await using b = await tmpdir()
let n = 0
await Effect.runPromise(
Effect.scoped(
Effect.gen(function* () {
const state = yield* InstanceState.make({
lookup: (dir) => Effect.sync(() => ({ dir, n: ++n })),
})
const x = yield* Effect.promise(() => access(state, a.path))
const y = yield* Effect.promise(() => access(state, b.path))
const z = yield* Effect.promise(() => access(state, a.path))
expect(x).toBe(z)
expect(x).not.toBe(y)
expect(n).toBe(2)
}),
),
)
})
test("InstanceState is disposed on instance reload", async () => {
await using tmp = await tmpdir()
const seen: string[] = []
let n = 0
await Effect.runPromise(
Effect.scoped(
Effect.gen(function* () {
const state = yield* InstanceState.make({
lookup: () => Effect.sync(() => ({ n: ++n })),
release: (value) =>
Effect.sync(() => {
seen.push(String(value.n))
}),
})
const a = yield* Effect.promise(() => access(state, tmp.path))
yield* Effect.promise(() => Instance.reload({ directory: tmp.path }))
const b = yield* Effect.promise(() => access(state, tmp.path))
expect(a).not.toBe(b)
expect(seen).toEqual(["1"])
}),
),
)
})
test("InstanceState is disposed on disposeAll", async () => {
await using a = await tmpdir()
await using b = await tmpdir()
const seen: string[] = []
await Effect.runPromise(
Effect.scoped(
Effect.gen(function* () {
const state = yield* InstanceState.make({
lookup: (dir) => Effect.sync(() => ({ dir })),
release: (value) =>
Effect.sync(() => {
seen.push(value.dir)
}),
})
yield* Effect.promise(() => access(state, a.path))
yield* Effect.promise(() => access(state, b.path))
yield* Effect.promise(() => Instance.disposeAll())
expect(seen.sort()).toEqual([a.path, b.path].sort())
}),
),
)
})
test("InstanceState dedupes concurrent lookups for the same directory", async () => {
await using tmp = await tmpdir()
let n = 0
await Effect.runPromise(
Effect.scoped(
Effect.gen(function* () {
const state = yield* InstanceState.make({
lookup: () =>
Effect.promise(async () => {
n += 1
await Bun.sleep(10)
return { n }
}),
})
const [a, b] = yield* Effect.promise(() => Promise.all([access(state, tmp.path), access(state, tmp.path)]))
expect(a).toBe(b)
expect(n).toBe(1)
}),
),
)
})

View File

@@ -136,23 +136,23 @@ export function generateScale(seed: HexColor, isDark: boolean): HexColor[] {
const lightSteps = isDark
? [
0.182,
0.21,
0.261,
0.302,
0.341,
0.387,
0.443,
0.514,
base.l,
Math.max(0, base.l - 0.017),
Math.min(0.94, Math.max(0.84, base.l + 0.02)),
0.975,
0.118,
0.138,
0.167,
0.202,
0.246,
0.304,
0.378,
0.468,
clamp(base.l * 0.825, 0.53, 0.705),
clamp(base.l * 0.89, 0.61, 0.79),
clamp(base.l + 0.033, 0.868, 0.943),
0.984,
]
: [0.993, 0.983, 0.962, 0.936, 0.906, 0.866, 0.811, 0.74, base.l, Math.max(0, base.l - 0.036), 0.49, 0.27]
const chromaMultipliers = isDark
? [0.34, 0.45, 0.64, 0.82, 0.96, 1.06, 1.14, 1.2, 1.24, 1.28, 1.34, 1.08]
? [0.52, 0.68, 0.86, 1.02, 1.14, 1.24, 1.36, 1.48, 1.56, 1.64, 1.62, 1.15]
: [0.12, 0.24, 0.46, 0.68, 0.84, 0.98, 1.08, 1.16, 1.22, 1.26, 1.18, 0.98]
for (let i = 0; i < 12; i++) {
@@ -180,26 +180,26 @@ export function generateNeutralScale(seed: HexColor, isDark: boolean, ink?: HexC
const sink = (tone: number) =>
oklchToHex({
l: base.l * (1 - tone),
c: base.c * Math.max(0, 1 - tone * 0.3),
c: base.c * Math.max(0, 1 - tone * (isDark ? 0.12 : 0.3)),
h: base.h,
})
const bg = isDark
? sink(clamp(0.06 + Math.max(0, base.l - 0.18) * 0.22 + base.c * 1.4, 0.06, 0.14))
? sink(clamp(0.19 + Math.max(0, base.l - 0.12) * 0.33 + base.c * 1.95, 0.17, 0.27))
: base.l < 0.82
? lift(0.86)
: lift(clamp(0.1 + base.c * 3.2 + Math.max(0, 0.95 - base.l) * 0.35, 0.1, 0.28))
const steps = isDark
? [0, 0.03, 0.055, 0.085, 0.125, 0.18, 0.255, 0.35, 0.5, 0.67, 0.84, 0.975]
? [0, 0.018, 0.039, 0.064, 0.097, 0.143, 0.212, 0.31, 0.46, 0.649, 0.845, 0.984]
: [0, 0.022, 0.042, 0.068, 0.102, 0.146, 0.208, 0.296, 0.432, 0.61, 0.81, 0.965]
return steps.map((step) => mixColors(bg, ink, step))
}
const base = hexToOklch(seed)
const scale: HexColor[] = []
const neutralChroma = Math.min(base.c, isDark ? 0.05 : 0.04)
const neutralChroma = Math.min(base.c, isDark ? 0.068 : 0.04)
const lightSteps = isDark
? [0.2, 0.226, 0.256, 0.277, 0.301, 0.325, 0.364, 0.431, base.l, 0.593, 0.706, 0.946]
? [0.138, 0.156, 0.178, 0.202, 0.232, 0.272, 0.326, 0.404, clamp(base.l * 0.83, 0.43, 0.55), 0.596, 0.719, 0.956]
: [0.991, 0.979, 0.964, 0.946, 0.931, 0.913, 0.891, 0.83, base.l, 0.617, 0.542, 0.205]
for (let i = 0; i < 12; i++) {

View File

@@ -1,37 +1,79 @@
import type { DesktopTheme } from "./types"
import oc2ThemeJson from "./themes/oc-2.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"
import nightowlThemeJson from "./themes/nightowl.json"
import vesperThemeJson from "./themes/vesper.json"
import carbonfoxThemeJson from "./themes/carbonfox.json"
import gruvboxThemeJson from "./themes/gruvbox.json"
import auraThemeJson from "./themes/aura.json"
import amoledThemeJson from "./themes/amoled.json"
import auraThemeJson from "./themes/aura.json"
import ayuThemeJson from "./themes/ayu.json"
import carbonfoxThemeJson from "./themes/carbonfox.json"
import catppuccinThemeJson from "./themes/catppuccin.json"
import catppuccinFrappeThemeJson from "./themes/catppuccin-frappe.json"
import catppuccinMacchiatoThemeJson from "./themes/catppuccin-macchiato.json"
import cobalt2ThemeJson from "./themes/cobalt2.json"
import cursorThemeJson from "./themes/cursor.json"
import draculaThemeJson from "./themes/dracula.json"
import everforestThemeJson from "./themes/everforest.json"
import flexokiThemeJson from "./themes/flexoki.json"
import githubThemeJson from "./themes/github.json"
import gruvboxThemeJson from "./themes/gruvbox.json"
import kanagawaThemeJson from "./themes/kanagawa.json"
import lucentOrngThemeJson from "./themes/lucent-orng.json"
import materialThemeJson from "./themes/material.json"
import matrixThemeJson from "./themes/matrix.json"
import mercuryThemeJson from "./themes/mercury.json"
import monokaiThemeJson from "./themes/monokai.json"
import nightowlThemeJson from "./themes/nightowl.json"
import nordThemeJson from "./themes/nord.json"
import oneDarkThemeJson from "./themes/one-dark.json"
import oneDarkProThemeJson from "./themes/onedarkpro.json"
import opencodeThemeJson from "./themes/opencode.json"
import orngThemeJson from "./themes/orng.json"
import osakaJadeThemeJson from "./themes/osaka-jade.json"
import palenightThemeJson from "./themes/palenight.json"
import rosepineThemeJson from "./themes/rosepine.json"
import shadesOfPurpleThemeJson from "./themes/shadesofpurple.json"
import solarizedThemeJson from "./themes/solarized.json"
import synthwave84ThemeJson from "./themes/synthwave84.json"
import tokyonightThemeJson from "./themes/tokyonight.json"
import vercelThemeJson from "./themes/vercel.json"
import vesperThemeJson from "./themes/vesper.json"
import zenburnThemeJson from "./themes/zenburn.json"
export const oc2Theme = oc2ThemeJson 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 nightowlTheme = nightowlThemeJson as DesktopTheme
export const vesperTheme = vesperThemeJson as DesktopTheme
export const carbonfoxTheme = carbonfoxThemeJson as DesktopTheme
export const gruvboxTheme = gruvboxThemeJson as DesktopTheme
export const auraTheme = auraThemeJson as DesktopTheme
export const amoledTheme = amoledThemeJson as DesktopTheme
export const auraTheme = auraThemeJson as DesktopTheme
export const ayuTheme = ayuThemeJson as DesktopTheme
export const carbonfoxTheme = carbonfoxThemeJson as DesktopTheme
export const catppuccinTheme = catppuccinThemeJson as DesktopTheme
export const catppuccinFrappeTheme = catppuccinFrappeThemeJson as DesktopTheme
export const catppuccinMacchiatoTheme = catppuccinMacchiatoThemeJson as DesktopTheme
export const cobalt2Theme = cobalt2ThemeJson as DesktopTheme
export const cursorTheme = cursorThemeJson as DesktopTheme
export const draculaTheme = draculaThemeJson as DesktopTheme
export const everforestTheme = everforestThemeJson as DesktopTheme
export const flexokiTheme = flexokiThemeJson as DesktopTheme
export const githubTheme = githubThemeJson as DesktopTheme
export const gruvboxTheme = gruvboxThemeJson as DesktopTheme
export const kanagawaTheme = kanagawaThemeJson as DesktopTheme
export const lucentOrngTheme = lucentOrngThemeJson as DesktopTheme
export const materialTheme = materialThemeJson as DesktopTheme
export const matrixTheme = matrixThemeJson as DesktopTheme
export const mercuryTheme = mercuryThemeJson as DesktopTheme
export const monokaiTheme = monokaiThemeJson as DesktopTheme
export const nightowlTheme = nightowlThemeJson as DesktopTheme
export const nordTheme = nordThemeJson as DesktopTheme
export const oneDarkTheme = oneDarkThemeJson as DesktopTheme
export const oneDarkProTheme = oneDarkProThemeJson as DesktopTheme
export const opencodeTheme = opencodeThemeJson as DesktopTheme
export const orngTheme = orngThemeJson as DesktopTheme
export const osakaJadeTheme = osakaJadeThemeJson as DesktopTheme
export const palenightTheme = palenightThemeJson as DesktopTheme
export const rosepineTheme = rosepineThemeJson as DesktopTheme
export const shadesOfPurpleTheme = shadesOfPurpleThemeJson as DesktopTheme
export const solarizedTheme = solarizedThemeJson as DesktopTheme
export const synthwave84Theme = synthwave84ThemeJson as DesktopTheme
export const tokyonightTheme = tokyonightThemeJson as DesktopTheme
export const vercelTheme = vercelThemeJson as DesktopTheme
export const vesperTheme = vesperThemeJson as DesktopTheme
export const zenburnTheme = zenburnThemeJson as DesktopTheme
export const DEFAULT_THEMES: Record<string, DesktopTheme> = {
"oc-2": oc2Theme,
@@ -40,14 +82,35 @@ export const DEFAULT_THEMES: Record<string, DesktopTheme> = {
ayu: ayuTheme,
carbonfox: carbonfoxTheme,
catppuccin: catppuccinTheme,
"catppuccin-frappe": catppuccinFrappeTheme,
"catppuccin-macchiato": catppuccinMacchiatoTheme,
cobalt2: cobalt2Theme,
cursor: cursorTheme,
dracula: draculaTheme,
everforest: everforestTheme,
flexoki: flexokiTheme,
github: githubTheme,
gruvbox: gruvboxTheme,
kanagawa: kanagawaTheme,
"lucent-orng": lucentOrngTheme,
material: materialTheme,
matrix: matrixTheme,
mercury: mercuryTheme,
monokai: monokaiTheme,
nightowl: nightowlTheme,
nord: nordTheme,
"one-dark": oneDarkTheme,
onedarkpro: oneDarkProTheme,
opencode: opencodeTheme,
orng: orngTheme,
"osaka-jade": osakaJadeTheme,
palenight: palenightTheme,
rosepine: rosepineTheme,
shadesofpurple: shadesOfPurpleTheme,
solarized: solarizedTheme,
synthwave84: synthwave84Theme,
tokyonight: tokyonightTheme,
vercel: vercelTheme,
vesper: vesperTheme,
zenburn: zenburnTheme,
}

View File

@@ -87,7 +87,7 @@
"type": "object",
"description": "A compact semantic palette used to derive the full theme programmatically",
"additionalProperties": false,
"required": ["neutral", "primary", "success", "warning", "error", "info"],
"required": ["neutral", "ink", "primary", "success", "warning", "error", "info"],
"properties": {
"neutral": {
"$ref": "#/definitions/HexColor",
@@ -95,7 +95,7 @@
},
"ink": {
"$ref": "#/definitions/HexColor",
"description": "Optional foreground or chrome color used to derive text and border tones"
"description": "Foreground or chrome color used to derive text and border tones"
},
"primary": {
"$ref": "#/definitions/HexColor",

View File

@@ -36,15 +36,40 @@ export { ThemeProvider, useTheme, type ColorScheme } from "./context"
export {
DEFAULT_THEMES,
oc2Theme,
tokyonightTheme,
draculaTheme,
monokaiTheme,
solarizedTheme,
nordTheme,
catppuccinTheme,
amoledTheme,
auraTheme,
ayuTheme,
oneDarkProTheme,
shadesOfPurpleTheme,
carbonfoxTheme,
catppuccinTheme,
catppuccinFrappeTheme,
catppuccinMacchiatoTheme,
cobalt2Theme,
cursorTheme,
draculaTheme,
everforestTheme,
flexokiTheme,
githubTheme,
gruvboxTheme,
kanagawaTheme,
lucentOrngTheme,
materialTheme,
matrixTheme,
mercuryTheme,
monokaiTheme,
nightowlTheme,
nordTheme,
oneDarkTheme,
oneDarkProTheme,
opencodeTheme,
orngTheme,
osakaJadeTheme,
palenightTheme,
rosepineTheme,
shadesOfPurpleTheme,
solarizedTheme,
synthwave84Theme,
tokyonightTheme,
vercelTheme,
vesperTheme,
zenburnTheme,
} from "./default-themes"

View File

@@ -13,33 +13,21 @@ export function resolveThemeVariant(variant: ThemeVariant, isDark: boolean): Res
const error = generateScale(colors.error, isDark)
const info = generateScale(colors.info, isDark)
const interactive = generateScale(colors.interactive, isDark)
const hasInk = colors.compact && Boolean(colors.ink)
const noInk = colors.compact && !hasInk
const shadow = noInk && !isDark ? generateNeutralScale(colors.neutral, true) : neutral
const amber = generateScale(
shift(colors.warning, isDark ? { h: -16, l: -0.058, c: 1.14 } : { h: -22, l: -0.082, c: 0.94 }),
isDark,
)
const blue = generateScale(shift(colors.interactive, { h: -12, l: 0.128, c: 1.12 }), isDark)
const brandl = noInk && isDark ? generateScale(colors.primary, false) : primary
const successl = noInk && isDark ? generateScale(colors.success, false) : success
const warningl = noInk && isDark ? generateScale(colors.warning, false) : warning
const infol = noInk && isDark ? generateScale(colors.info, false) : info
const interl = noInk && isDark ? generateScale(colors.interactive, false) : interactive
const diffAdd = generateScale(
colors.diffAdd ??
(noInk
? shift(colors.success, { c: isDark ? 0.54 : 0.6, l: isDark ? 0.22 : 0.16 })
: shift(colors.success, { c: isDark ? 0.7 : 0.55, l: isDark ? -0.18 : 0.14 })),
colors.diffAdd ?? shift(colors.success, { c: isDark ? 0.7 : 0.55, l: isDark ? -0.18 : 0.14 }),
isDark,
)
const diffDelete = generateScale(
colors.diffDelete ??
(noInk ? colors.error : shift(colors.error, { c: isDark ? 0.82 : 0.7, l: isDark ? -0.08 : 0.08 })),
colors.diffDelete ?? shift(colors.error, { c: isDark ? 0.82 : 0.7, l: isDark ? -0.08 : 0.08 }),
isDark,
)
const ink = colors.ink ?? colors.neutral
const tint = hasInk ? hexToOklch(ink) : undefined
const tint = colors.compact ? hexToOklch(ink) : undefined
const body = tint
? shift(ink, {
l: isDark ? Math.max(0, 0.88 - tint.l) * 0.4 : -Math.max(0, tint.l - 0.18) * 0.24,
@@ -48,7 +36,7 @@ export function resolveThemeVariant(variant: ThemeVariant, isDark: boolean): Res
: undefined
const backgroundOverride = overrides["background-base"]
const backgroundHex = getHex(backgroundOverride)
const overlay = noInk || (Boolean(backgroundOverride) && !backgroundHex)
const overlay = Boolean(backgroundOverride) && !backgroundHex
const content = (seed: HexColor, scale: HexColor[]) => {
const base = hexToOklch(seed)
const value = isDark ? (base.l > 0.84 ? shift(seed, { c: 1.18 }) : scale[10]) : scale[10]
@@ -56,7 +44,6 @@ export function resolveThemeVariant(variant: ThemeVariant, isDark: boolean): Res
}
const modified = () => {
if (!colors.compact) return isDark ? "#ffba92" : "#FF8C00"
if (!hasInk) return isDark ? "#ffba92" : "#FF8C00"
const warningHue = hexToOklch(colors.warning).h
const deleteHue = hexToOklch(colors.diffDelete ?? colors.error).h
const delta = Math.abs(((((deleteHue - warningHue) % 360) + 540) % 360) - 180)
@@ -76,73 +63,36 @@ export function resolveThemeVariant(variant: ThemeVariant, isDark: boolean): Res
stronger: alphaTone(seed, alpha.stronger),
}
}
const compactBackground =
colors.compact && !hasInk
? isDark
? {
base: shift(blend(colors.neutral, "#000000", 0.145), { c: 0 }),
weak: shift(blend(colors.neutral, "#000000", 0.27), { c: 0 }),
strong: shift(blend(colors.neutral, "#000000", 0.165), { c: 0 }),
stronger: shift(blend(colors.neutral, "#000000", 0.19), { c: 0 }),
}
: {
base: blend(colors.neutral, "#ffffff", 0.066),
weak: blend(colors.neutral, "#ffffff", 0.11),
strong: blend(colors.neutral, "#ffffff", 0.024),
stronger: blend(colors.neutral, "#ffffff", 0.024),
}
: undefined
const compactInkBackground =
colors.compact && hasInk && isDark
? {
base: neutral[0],
weak: neutral[1],
strong: neutral[0],
stronger: neutral[2],
}
: undefined
const background = backgroundHex ?? compactInkBackground?.base ?? compactBackground?.base ?? neutral[0]
const background = backgroundHex ?? neutral[0]
const alphaTone = (color: HexColor, alpha: number) =>
overlay ? (withAlpha(color, alpha) as ColorValue) : blend(color, background, alpha)
const borderTone = (light: number, dark: number) =>
alphaTone(
ink,
isDark ? Math.min(1, dark + 0.024 + (colors.compact && hasInk ? 0.08 : 0)) : Math.min(1, light + 0.024),
)
const diffHiddenSurface = noInk
? {
base: blue[isDark ? 1 : 2],
weak: blue[isDark ? 0 : 1],
weaker: blue[isDark ? 2 : 0],
strong: blue[4],
stronger: blue[isDark ? 10 : 8],
}
: surface(
isDark ? shift(colors.interactive, { c: 0.55, l: 0 }) : shift(colors.interactive, { c: 0.45, l: 0.08 }),
isDark
? { base: 0.14, weak: 0.08, weaker: 0.18, strong: 0.26, stronger: 0.42 }
: { base: 0.12, weak: 0.08, weaker: 0.16, strong: 0.24, stronger: 0.36 },
)
alphaTone(ink, isDark ? Math.min(1, dark + 0.024 + (colors.compact ? 0.08 : 0)) : Math.min(1, light + 0.024))
const diffHiddenSurface = surface(
isDark ? shift(colors.interactive, { c: 0.55, l: 0 }) : shift(colors.interactive, { c: 0.45, l: 0.08 }),
isDark
? { base: 0.14, weak: 0.08, weaker: 0.18, strong: 0.26, stronger: 0.42 }
: { base: 0.12, weak: 0.08, weaker: 0.16, strong: 0.24, stronger: 0.36 },
)
const neutralAlpha = noInk ? generateNeutralOverlayScale(neutral, isDark) : generateNeutralAlphaScale(neutral, isDark)
const brandb = brandl[isDark ? 9 : 8]
const brandh = brandl[isDark ? 10 : 9]
const interb = interactive[isDark ? 5 : 4]
const interh = interactive[isDark ? 6 : 5]
const interw = interactive[isDark ? 4 : 3]
const succb = success[isDark ? 5 : 4]
const succw = success[isDark ? 4 : 3]
const neutralAlpha = generateNeutralAlphaScale(neutral, isDark)
const brandb = primary[8]
const brandh = primary[9]
const interb = interactive[isDark ? 6 : 4]
const interh = interactive[isDark ? 7 : 5]
const interw = interactive[isDark ? 5 : 3]
const succb = success[isDark ? 6 : 4]
const succw = success[isDark ? 5 : 3]
const succs = success[10]
const warnb = (noInk && isDark ? warningl : warning)[isDark ? 5 : 4]
const warnw = (noInk && isDark ? warningl : warning)[isDark ? 4 : 3]
const warns = (noInk && isDark ? warningl : warning)[10]
const critb = error[isDark ? 5 : 4]
const critw = error[isDark ? 4 : 3]
const warnb = warning[isDark ? 6 : 4]
const warnw = warning[isDark ? 5 : 3]
const warns = warning[10]
const critb = error[isDark ? 6 : 4]
const critw = error[isDark ? 5 : 3]
const crits = error[10]
const infob = (noInk && isDark ? infol : info)[isDark ? 5 : 4]
const infow = (noInk && isDark ? infol : info)[isDark ? 4 : 3]
const infos = (noInk && isDark ? infol : info)[10]
const infob = info[isDark ? 6 : 4]
const infow = info[isDark ? 5 : 3]
const infos = info[10]
const lum = (hex: HexColor) => {
const rgb = hexToRgb(hex)
const lift = (v: number) => (v <= 0.04045 ? v / 12.92 : Math.pow((v + 0.055) / 1.055, 2.4))
@@ -163,11 +113,10 @@ export function resolveThemeVariant(variant: ThemeVariant, isDark: boolean): Res
const tokens: ResolvedTheme = {}
tokens["background-base"] = compactInkBackground?.base ?? compactBackground?.base ?? neutral[0]
tokens["background-weak"] = compactInkBackground?.weak ?? compactBackground?.weak ?? neutral[2]
tokens["background-strong"] = compactInkBackground?.strong ?? compactBackground?.strong ?? neutral[0]
tokens["background-stronger"] =
compactInkBackground?.stronger ?? compactBackground?.stronger ?? (isDark ? neutral[1] : "#fcfcfc")
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]
@@ -183,8 +132,8 @@ export function resolveThemeVariant(variant: ThemeVariant, isDark: boolean): Res
: (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 ? (hasInk ? neutral[1] : neutral[0]) : noInk ? shadow[0] : neutral[11]
tokens["surface-float-base-hover"] = isDark ? (hasInk ? neutral[2] : neutral[1]) : noInk ? shadow[1] : neutral[10]
tokens["surface-float-base"] = isDark ? neutral[1] : neutral[11]
tokens["surface-float-base-hover"] = isDark ? neutral[2] : 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]
@@ -202,7 +151,7 @@ export function resolveThemeVariant(variant: ThemeVariant, isDark: boolean): Res
tokens["surface-interactive-base"] = interb
tokens["surface-interactive-hover"] = interh
tokens["surface-interactive-weak"] = interw
tokens["surface-interactive-weak-hover"] = noInk && isDark ? interl[5] : interb
tokens["surface-interactive-weak-hover"] = interb
tokens["surface-success-base"] = succb
tokens["surface-success-weak"] = succw
@@ -236,42 +185,22 @@ export function resolveThemeVariant(variant: ThemeVariant, isDark: boolean): Res
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-hover"] = isDark ? neutral[2] : neutral[1]
tokens["input-active"] = isDark ? interactive[6] : interactive[0]
tokens["input-selected"] = isDark ? interactive[7] : interactive[3]
tokens["input-focus"] = isDark ? interactive[6] : interactive[0]
tokens["input-disabled"] = neutral[3]
tokens["text-base"] = hasInk ? (body as HexColor) : noInk ? (isDark ? neutralAlpha[10] : neutral[10]) : neutral[10]
tokens["text-weak"] = hasInk
? shift(body as HexColor, { l: isDark ? -0.11 : 0.11, c: 0.9 })
: noInk
? isDark
? neutralAlpha[8]
: neutral[8]
: neutral[8]
tokens["text-weaker"] = hasInk
tokens["text-base"] = colors.compact ? (body as HexColor) : neutral[10]
tokens["text-weak"] = colors.compact ? shift(body as HexColor, { l: isDark ? -0.11 : 0.11, c: 0.9 }) : neutral[8]
tokens["text-weaker"] = colors.compact
? shift(body as HexColor, { l: isDark ? -0.2 : 0.21, c: isDark ? 0.78 : 0.72 })
: noInk
? isDark
? neutralAlpha[7]
: neutral[7]
: neutral[7]
tokens["text-strong"] = hasInk
? isDark && colors.compact
: neutral[7]
tokens["text-strong"] = colors.compact
? isDark
? blend("#ffffff", body as HexColor, 0.9)
: shift(body as HexColor, { l: isDark ? 0.055 : -0.07, c: 1.04 })
: noInk
? isDark
? neutralAlpha[11]
: neutral[11]
: neutral[11]
if (noInk && isDark) {
tokens["text-base"] = withAlpha("#ffffff", 0.618) as ColorValue
tokens["text-weak"] = withAlpha("#ffffff", 0.422) as ColorValue
tokens["text-weaker"] = withAlpha("#ffffff", 0.284) as ColorValue
tokens["text-strong"] = withAlpha("#ffffff", 0.936) as ColorValue
}
: shift(body as HexColor, { l: -0.07, c: 1.04 })
: neutral[11]
tokens["text-invert-base"] = isDark ? neutral[10] : neutral[1]
tokens["text-invert-weak"] = isDark ? neutral[8] : neutral[2]
tokens["text-invert-weaker"] = isDark ? neutral[7] : neutral[3]
@@ -287,7 +216,7 @@ export function resolveThemeVariant(variant: ThemeVariant, isDark: boolean): Res
tokens["text-on-warning-base"] = on(warnb)
tokens["text-on-info-base"] = on(infob)
tokens["text-diff-add-base"] = diffAdd[10]
tokens["text-diff-delete-base"] = diffDelete[isDark ? 8 : 9]
tokens["text-diff-delete-base"] = diffDelete[9]
tokens["text-diff-delete-strong"] = diffDelete[11]
tokens["text-diff-add-strong"] = diffAdd[isDark ? 7 : 11]
tokens["text-on-info-weak"] = on(infob)
@@ -301,170 +230,84 @@ export function resolveThemeVariant(variant: ThemeVariant, isDark: boolean): Res
tokens["text-on-brand-strong"] = on(brandh)
tokens["button-primary-base"] = neutral[11]
tokens["button-secondary-base"] = noInk ? (isDark ? neutral[1] : neutral[0]) : isDark ? neutral[2] : neutral[0]
tokens["button-secondary-hover"] = noInk ? (isDark ? neutral[1] : neutral[1]) : isDark ? neutral[3] : neutral[1]
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]
if (noInk) {
const tone = (alpha: number) => alphaTone((isDark ? "#ffffff" : "#000000") as HexColor, alpha)
if (isDark) {
tokens["surface-base"] = tone(0.045)
tokens["surface-base-hover"] = tone(0.065)
tokens["surface-base-active"] = tone(0.095)
tokens["surface-raised-base"] = tone(0.085)
tokens["surface-raised-base-hover"] = tone(0.115)
tokens["surface-raised-base-active"] = tone(0.15)
tokens["surface-raised-strong"] = tone(0.115)
tokens["surface-raised-strong-hover"] = tone(0.17)
tokens["surface-raised-stronger"] = tone(0.17)
tokens["surface-raised-stronger-hover"] = tone(0.22)
tokens["surface-weak"] = tone(0.115)
tokens["surface-weaker"] = tone(0.15)
tokens["surface-strong"] = tone(0.22)
tokens["surface-raised-stronger-non-alpha"] = neutral[1]
tokens["surface-inset-base"] = withAlpha("#000000", 0.5) as ColorValue
tokens["surface-inset-base-hover"] = tokens["surface-inset-base"]
tokens["surface-inset-strong"] = withAlpha("#000000", 0.8) as ColorValue
tokens["surface-inset-strong-hover"] = tokens["surface-inset-strong"]
tokens["button-secondary-hover"] = tone(0.065)
tokens["button-ghost-hover"] = tone(0.045)
tokens["button-ghost-hover2"] = tone(0.095)
tokens["input-base"] = neutral[1]
tokens["input-hover"] = neutral[1]
tokens["input-selected"] = interactive[1]
tokens["surface-diff-skip-base"] = "#00000000"
}
tokens["border-base"] = colors.compact ? borderTone(0.22, 0.16) : neutralAlpha[6]
tokens["border-hover"] = colors.compact ? borderTone(0.28, 0.2) : neutralAlpha[7]
tokens["border-active"] = colors.compact ? borderTone(0.34, 0.24) : neutralAlpha[8]
tokens["border-selected"] = withAlpha(interactive[8], isDark ? 0.9 : 0.99) as ColorValue
tokens["border-disabled"] = colors.compact ? borderTone(0.18, 0.12) : neutralAlpha[7]
tokens["border-focus"] = colors.compact ? borderTone(0.34, 0.24) : neutralAlpha[8]
tokens["border-weak-base"] = colors.compact ? borderTone(0.1, 0.08) : neutralAlpha[isDark ? 5 : 4]
tokens["border-strong-base"] = colors.compact ? borderTone(0.34, 0.24) : neutralAlpha[isDark ? 7 : 6]
tokens["border-strong-hover"] = colors.compact ? borderTone(0.4, 0.28) : neutralAlpha[7]
tokens["border-strong-active"] = colors.compact ? borderTone(0.46, 0.32) : neutralAlpha[isDark ? 7 : 6]
tokens["border-strong-selected"] = withAlpha(interactive[5], 0.6) as ColorValue
tokens["border-strong-disabled"] = colors.compact ? borderTone(0.14, 0.1) : neutralAlpha[5]
tokens["border-strong-focus"] = colors.compact ? borderTone(0.46, 0.32) : neutralAlpha[isDark ? 7 : 6]
tokens["border-weak-hover"] = colors.compact ? borderTone(0.16, 0.12) : neutralAlpha[isDark ? 6 : 5]
tokens["border-weak-active"] = colors.compact ? borderTone(0.22, 0.16) : neutralAlpha[isDark ? 7 : 6]
tokens["border-weak-selected"] = withAlpha(interactive[4], isDark ? 0.6 : 0.5) as ColorValue
tokens["border-weak-disabled"] = colors.compact ? borderTone(0.08, 0.06) : neutralAlpha[5]
tokens["border-weak-focus"] = colors.compact ? borderTone(0.22, 0.16) : neutralAlpha[isDark ? 7 : 6]
tokens["border-weaker-base"] = colors.compact ? borderTone(0.06, 0.04) : neutralAlpha[2]
if (!isDark) {
tokens["surface-base"] = tone(0.045)
tokens["surface-base-hover"] = tone(0.08)
tokens["surface-base-active"] = tone(0.105)
tokens["surface-raised-base"] = tone(0.05)
tokens["surface-raised-base-hover"] = tone(0.08)
tokens["surface-raised-base-active"] = tone(0.125)
tokens["surface-raised-strong"] = neutral[0]
tokens["surface-raised-strong-hover"] = "#ffffff"
tokens["surface-raised-stronger"] = "#ffffff"
tokens["surface-raised-stronger-hover"] = "#ffffff"
tokens["surface-weak"] = tone(0.08)
tokens["surface-weaker"] = tone(0.105)
tokens["surface-strong"] = "#ffffff"
tokens["surface-raised-stronger-non-alpha"] = "#ffffff"
tokens["surface-inset-strong"] = tone(0.09)
tokens["surface-inset-strong-hover"] = tokens["surface-inset-strong"]
tokens["button-secondary-hover"] = blend("#ffffff", background, 0.04)
tokens["button-ghost-hover"] = tone(0.045)
tokens["button-ghost-hover2"] = tone(0.08)
tokens["input-base"] = neutral[0]
tokens["input-hover"] = neutral[1]
}
tokens["surface-base-interactive-active"] = withAlpha(colors.interactive, isDark ? 0.18 : 0.12) as ColorValue
}
tokens["border-base"] = hasInk ? borderTone(0.22, 0.16) : neutralAlpha[6]
tokens["border-hover"] = hasInk ? borderTone(0.28, 0.2) : neutralAlpha[7]
tokens["border-active"] = hasInk ? borderTone(0.34, 0.24) : neutralAlpha[8]
tokens["border-selected"] = noInk
? isDark
? interactive[10]
: (withAlpha(colors.interactive, 0.99) as ColorValue)
: (withAlpha(interactive[8], isDark ? 0.9 : 0.99) as ColorValue)
tokens["border-disabled"] = hasInk ? borderTone(0.18, 0.12) : neutralAlpha[7]
tokens["border-focus"] = hasInk ? borderTone(0.34, 0.24) : neutralAlpha[8]
tokens["border-weak-base"] = hasInk
? borderTone(0.1, 0.08)
: noInk
? isDark
? neutral[3]
: blend(neutral[4], neutral[5], 0.5)
: neutralAlpha[isDark ? 5 : 4]
tokens["border-strong-base"] = hasInk ? borderTone(0.34, 0.24) : neutralAlpha[isDark ? 7 : 6]
tokens["border-strong-hover"] = hasInk ? borderTone(0.4, 0.28) : neutralAlpha[7]
tokens["border-strong-active"] = hasInk ? borderTone(0.46, 0.32) : neutralAlpha[isDark ? 7 : 6]
tokens["border-strong-selected"] = noInk
? (withAlpha(colors.interactive, isDark ? 0.62 : 0.31) as ColorValue)
: (withAlpha(interactive[5], 0.6) as ColorValue)
tokens["border-strong-disabled"] = hasInk ? borderTone(0.14, 0.1) : neutralAlpha[5]
tokens["border-strong-focus"] = hasInk ? borderTone(0.46, 0.32) : neutralAlpha[isDark ? 7 : 6]
tokens["border-weak-hover"] = hasInk ? borderTone(0.16, 0.12) : neutralAlpha[isDark ? 6 : 5]
tokens["border-weak-active"] = hasInk ? borderTone(0.22, 0.16) : neutralAlpha[isDark ? 7 : 6]
tokens["border-weak-selected"] = noInk
? (withAlpha(colors.interactive, isDark ? 0.62 : 0.24) as ColorValue)
: (withAlpha(interactive[4], isDark ? 0.6 : 0.5) as ColorValue)
tokens["border-weak-disabled"] = hasInk ? borderTone(0.08, 0.06) : neutralAlpha[5]
tokens["border-weak-focus"] = hasInk ? borderTone(0.22, 0.16) : neutralAlpha[isDark ? 7 : 6]
tokens["border-weaker-base"] = hasInk
? borderTone(0.06, 0.04)
: noInk
? isDark
? blend(neutral[1], neutral[2], 0.5)
: blend(neutral[2], neutral[3], 0.5)
: neutralAlpha[2]
if (noInk) {
const line = (l: number, d: number) => alphaTone((isDark ? "#ffffff" : "#000000") as HexColor, isDark ? d : l)
tokens["border-base"] = line(0.162, 0.195)
tokens["border-hover"] = line(0.236, 0.284)
tokens["border-active"] = line(0.46, 0.418)
tokens["border-disabled"] = tokens["border-hover"]
tokens["border-focus"] = tokens["border-active"]
}
tokens["border-interactive-base"] = (noInk && isDark ? interl : interactive)[7]
tokens["border-interactive-hover"] = (noInk && isDark ? interl : interactive)[8]
tokens["border-interactive-active"] = (noInk && isDark ? interl : interactive)[9]
tokens["border-interactive-selected"] = (noInk && isDark ? interl : interactive)[9]
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"] = (noInk && isDark ? interl : interactive)[9]
tokens["border-interactive-focus"] = interactive[8]
tokens["border-success-base"] = (noInk && isDark ? successl : success)[6]
tokens["border-success-hover"] = (noInk && isDark ? successl : success)[7]
tokens["border-success-selected"] = (noInk && isDark ? successl : success)[9]
tokens["border-warning-base"] = (noInk && isDark ? warningl : warning)[6]
tokens["border-warning-hover"] = (noInk && isDark ? warningl : warning)[7]
tokens["border-warning-selected"] = (noInk && isDark ? warningl : warning)[9]
tokens["border-critical-base"] = error[isDark ? 5 : 6]
tokens["border-critical-hover"] = error[7]
tokens["border-critical-selected"] = error[9]
tokens["border-info-base"] = (noInk && isDark ? infol : info)[6]
tokens["border-info-hover"] = (noInk && isDark ? infol : info)[7]
tokens["border-info-selected"] = (noInk && isDark ? infol : info)[9]
tokens["border-success-base"] = success[isDark ? 6 : 6]
tokens["border-success-hover"] = success[isDark ? 7 : 7]
tokens["border-success-selected"] = success[8]
tokens["border-warning-base"] = warning[isDark ? 6 : 6]
tokens["border-warning-hover"] = warning[isDark ? 7 : 7]
tokens["border-warning-selected"] = warning[8]
tokens["border-critical-base"] = error[isDark ? 6 : 6]
tokens["border-critical-hover"] = error[isDark ? 7 : 7]
tokens["border-critical-selected"] = error[8]
tokens["border-info-base"] = info[isDark ? 6 : 6]
tokens["border-info-hover"] = info[isDark ? 7 : 7]
tokens["border-info-selected"] = info[8]
tokens["border-color"] = "#ffffff"
tokens["icon-base"] = hasInk && !isDark ? tokens["text-weak"] : neutral[isDark ? 9 : 8]
tokens["icon-hover"] = hasInk && !isDark ? tokens["text-base"] : neutral[10]
tokens["icon-active"] = hasInk && !isDark ? tokens["text-strong"] : neutral[11]
tokens["icon-selected"] = hasInk && !isDark ? tokens["text-strong"] : neutral[11]
tokens["icon-base"] = colors.compact && !isDark ? tokens["text-weak"] : neutral[isDark ? 9 : 8]
tokens["icon-hover"] = colors.compact && !isDark ? tokens["text-base"] : neutral[10]
tokens["icon-active"] = colors.compact && !isDark ? tokens["text-strong"] : neutral[11]
tokens["icon-selected"] = colors.compact && !isDark ? tokens["text-strong"] : neutral[11]
tokens["icon-disabled"] = neutral[isDark ? 6 : 7]
tokens["icon-focus"] = hasInk && !isDark ? tokens["text-strong"] : neutral[11]
tokens["icon-focus"] = colors.compact && !isDark ? tokens["text-strong"] : neutral[11]
tokens["icon-invert-base"] = isDark ? neutral[0] : "#ffffff"
tokens["icon-weak-base"] = neutral[isDark ? 5 : 6]
tokens["icon-weak-hover"] = noInk && isDark ? blend(neutral[11], neutral[10], 0.74) : neutral[isDark ? 11 : 7]
tokens["icon-weak-active"] = noInk && isDark ? blend(neutral[11], neutral[10], 0.52) : neutral[8]
tokens["icon-weak-hover"] = neutral[isDark ? 11 : 7]
tokens["icon-weak-active"] = neutral[8]
tokens["icon-weak-selected"] = neutral[isDark ? 8 : 9]
tokens["icon-weak-disabled"] = noInk && isDark ? neutral[11] : neutral[isDark ? 3 : 5]
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"] = noInk && isDark ? neutral[6] : neutral[7]
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[9]
tokens["icon-interactive-base"] = interactive[8]
tokens["icon-success-base"] = success[isDark ? 8 : 6]
tokens["icon-success-hover"] = success[isDark ? 9 : 7]
tokens["icon-success-hover"] = success[9]
tokens["icon-success-active"] = success[10]
tokens["icon-warning-base"] = amber[isDark ? 8 : 6]
tokens["icon-warning-hover"] = amber[7]
tokens["icon-warning-hover"] = amber[9]
tokens["icon-warning-active"] = amber[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-critical-hover"] = error[9]
tokens["icon-critical-active"] = error[10]
tokens["icon-info-base"] = info[isDark ? 8 : 6]
tokens["icon-info-hover"] = info[isDark ? 9 : 7]
tokens["icon-info-active"] = info[10]
tokens["icon-on-brand-base"] = on(brandb)
tokens["icon-on-brand-hover"] = on(brandh)
@@ -492,84 +335,45 @@ export function resolveThemeVariant(variant: ThemeVariant, isDark: boolean): Res
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["icon-diff-delete-base"] = diffDelete[9]
tokens["icon-diff-delete-hover"] = diffDelete[isDark ? 10 : 10]
tokens["icon-diff-modified-base"] = modified()
if (colors.compact) {
if (!hasInk) {
tokens["syntax-comment"] = "var(--text-weak)"
tokens["syntax-regexp"] = "var(--text-base)"
tokens["syntax-string"] = isDark ? "#00ceb9" : "#006656"
tokens["syntax-keyword"] = content(colors.accent, accent)
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[10]
tokens["syntax-warning"] = amber[10]
tokens["syntax-critical"] = error[10]
tokens["syntax-info"] = isDark ? "#93e9f6" : "#0092a8"
tokens["syntax-diff-add"] = diffAdd[10]
tokens["syntax-diff-delete"] = diffDelete[10]
tokens["syntax-diff-unknown"] = "#ff0000"
tokens["syntax-comment"] = "var(--text-weak)"
tokens["syntax-regexp"] = "var(--text-base)"
tokens["syntax-string"] = content(colors.success, success)
tokens["syntax-keyword"] = content(colors.accent, accent)
tokens["syntax-primitive"] = content(colors.primary, primary)
tokens["syntax-operator"] = isDark ? "var(--text-weak)" : "var(--text-base)"
tokens["syntax-variable"] = "var(--text-strong)"
tokens["syntax-property"] = content(colors.info, info)
tokens["syntax-type"] = content(colors.warning, warning)
tokens["syntax-constant"] = content(colors.accent, accent)
tokens["syntax-punctuation"] = isDark ? "var(--text-weak)" : "var(--text-base)"
tokens["syntax-object"] = "var(--text-strong)"
tokens["syntax-success"] = success[10]
tokens["syntax-warning"] = amber[10]
tokens["syntax-critical"] = error[10]
tokens["syntax-info"] = content(colors.info, info)
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"
}
if (hasInk) {
tokens["syntax-comment"] = "var(--text-weak)"
tokens["syntax-regexp"] = "var(--text-base)"
tokens["syntax-string"] = content(colors.success, success)
tokens["syntax-keyword"] = content(colors.accent, accent)
tokens["syntax-primitive"] = content(colors.primary, primary)
tokens["syntax-operator"] = isDark ? "var(--text-weak)" : "var(--text-base)"
tokens["syntax-variable"] = "var(--text-strong)"
tokens["syntax-property"] = content(colors.info, info)
tokens["syntax-type"] = content(colors.warning, warning)
tokens["syntax-constant"] = content(colors.accent, accent)
tokens["syntax-punctuation"] = isDark ? "var(--text-weak)" : "var(--text-base)"
tokens["syntax-object"] = "var(--text-strong)"
tokens["syntax-success"] = success[10]
tokens["syntax-warning"] = amber[10]
tokens["syntax-critical"] = error[10]
tokens["syntax-info"] = content(colors.info, info)
tokens["syntax-diff-add"] = diffAdd[10]
tokens["syntax-diff-delete"] = diffDelete[10]
tokens["syntax-diff-unknown"] = "#ff0000"
tokens["markdown-heading"] = content(colors.primary, primary)
tokens["markdown-text"] = tokens["text-base"]
tokens["markdown-link"] = content(colors.interactive, interactive)
tokens["markdown-link-text"] = content(colors.info, info)
tokens["markdown-code"] = content(colors.success, success)
tokens["markdown-block-quote"] = content(colors.warning, warning)
tokens["markdown-emph"] = content(colors.warning, warning)
tokens["markdown-strong"] = content(colors.accent, accent)
tokens["markdown-horizontal-rule"] = tokens["border-base"]
tokens["markdown-list-item"] = content(colors.interactive, interactive)
tokens["markdown-list-enumeration"] = content(colors.info, info)
tokens["markdown-image"] = content(colors.interactive, interactive)
tokens["markdown-image-text"] = content(colors.info, info)
tokens["markdown-code-block"] = tokens["text-base"]
}
tokens["markdown-heading"] = content(colors.primary, primary)
tokens["markdown-text"] = tokens["text-base"]
tokens["markdown-link"] = content(colors.interactive, interactive)
tokens["markdown-link-text"] = content(colors.info, info)
tokens["markdown-code"] = content(colors.success, success)
tokens["markdown-block-quote"] = content(colors.warning, warning)
tokens["markdown-emph"] = content(colors.warning, warning)
tokens["markdown-strong"] = content(colors.accent, accent)
tokens["markdown-horizontal-rule"] = tokens["border-base"]
tokens["markdown-list-item"] = content(colors.interactive, interactive)
tokens["markdown-list-enumeration"] = content(colors.info, info)
tokens["markdown-image"] = content(colors.interactive, interactive)
tokens["markdown-image-text"] = content(colors.info, info)
tokens["markdown-code-block"] = tokens["text-base"]
}
if (!colors.compact) {
@@ -626,7 +430,7 @@ export function resolveThemeVariant(variant: ThemeVariant, isDark: boolean): Res
tokens[key] = value
}
if (hasInk && "text-weak" in overrides && !("text-weaker" in overrides)) {
if (colors.compact && "text-weak" in overrides && !("text-weaker" in overrides)) {
const weak = tokens["text-weak"]
if (weak.startsWith("#")) {
tokens["text-weaker"] = shift(weak as HexColor, { l: isDark ? -0.12 : 0.12, c: 0.75 })
@@ -635,7 +439,7 @@ export function resolveThemeVariant(variant: ThemeVariant, isDark: boolean): Res
}
}
if (colors.compact && hasInk) {
if (colors.compact) {
if (!("markdown-text" in overrides)) {
tokens["markdown-text"] = tokens["text-base"]
}
@@ -709,17 +513,9 @@ function getColors(variant: ThemeVariant): ThemeColors {
throw new Error("Theme variant requires `palette` or `seeds`")
}
function generateNeutralOverlayScale(neutralScale: HexColor[], isDark: boolean): ColorValue[] {
const alphas = isDark
? [0, 0.034, 0.063, 0.084, 0.109, 0.138, 0.181, 0.266, 0.404, 0.468, 0.603, 0.928]
: [0.014, 0.034, 0.055, 0.075, 0.096, 0.118, 0.151, 0.232, 0.453, 0.492, 0.574, 0.915]
const color = (isDark ? "#ffffff" : "#000000") as HexColor
return alphas.map((alpha) => withAlpha(color, alpha) as ColorValue)
}
function generateNeutralAlphaScale(neutralScale: HexColor[], isDark: boolean): HexColor[] {
const alphas = isDark
? [0.05, 0.085, 0.13, 0.18, 0.24, 0.31, 0.4, 0.52, 0.64, 0.76, 0.88, 0.98]
? [0.038, 0.066, 0.1, 0.142, 0.19, 0.252, 0.334, 0.446, 0.58, 0.718, 0.854, 0.985]
: [0.03, 0.06, 0.1, 0.145, 0.2, 0.265, 0.35, 0.47, 0.61, 0.74, 0.86, 0.97]
return alphas.map((alpha) => blend(neutralScale[11], neutralScale[0], alpha))

View File

@@ -0,0 +1,85 @@
{
"$schema": "https://opencode.ai/desktop-theme.json",
"name": "Catppuccin Frappe",
"id": "catppuccin-frappe",
"light": {
"palette": {
"neutral": "#303446",
"ink": "#c6d0f5",
"primary": "#8da4e2",
"accent": "#f4b8e4",
"success": "#a6d189",
"warning": "#e5c890",
"error": "#e78284",
"info": "#81c8be"
},
"overrides": {
"text-weak": "#b5bfe2",
"syntax-comment": "#949cb8",
"syntax-keyword": "#ca9ee6",
"syntax-string": "#a6d189",
"syntax-primitive": "#8da4e2",
"syntax-variable": "#e78284",
"syntax-property": "#99d1db",
"syntax-type": "#e5c890",
"syntax-constant": "#ef9f76",
"syntax-operator": "#99d1db",
"syntax-punctuation": "#c6d0f5",
"syntax-object": "#e78284",
"markdown-heading": "#ca9ee6",
"markdown-text": "#c6d0f5",
"markdown-link": "#8da4e2",
"markdown-link-text": "#99d1db",
"markdown-code": "#a6d189",
"markdown-block-quote": "#e5c890",
"markdown-emph": "#e5c890",
"markdown-strong": "#ef9f76",
"markdown-horizontal-rule": "#a5adce",
"markdown-list-item": "#8da4e2",
"markdown-list-enumeration": "#99d1db",
"markdown-image": "#8da4e2",
"markdown-image-text": "#99d1db",
"markdown-code-block": "#c6d0f5"
}
},
"dark": {
"palette": {
"neutral": "#303446",
"ink": "#c6d0f5",
"primary": "#8da4e2",
"accent": "#f4b8e4",
"success": "#a6d189",
"warning": "#e5c890",
"error": "#e78284",
"info": "#81c8be"
},
"overrides": {
"text-weak": "#b5bfe2",
"syntax-comment": "#949cb8",
"syntax-keyword": "#ca9ee6",
"syntax-string": "#a6d189",
"syntax-primitive": "#8da4e2",
"syntax-variable": "#e78284",
"syntax-property": "#99d1db",
"syntax-type": "#e5c890",
"syntax-constant": "#ef9f76",
"syntax-operator": "#99d1db",
"syntax-punctuation": "#c6d0f5",
"syntax-object": "#e78284",
"markdown-heading": "#ca9ee6",
"markdown-text": "#c6d0f5",
"markdown-link": "#8da4e2",
"markdown-link-text": "#99d1db",
"markdown-code": "#a6d189",
"markdown-block-quote": "#e5c890",
"markdown-emph": "#e5c890",
"markdown-strong": "#ef9f76",
"markdown-horizontal-rule": "#a5adce",
"markdown-list-item": "#8da4e2",
"markdown-list-enumeration": "#99d1db",
"markdown-image": "#8da4e2",
"markdown-image-text": "#99d1db",
"markdown-code-block": "#c6d0f5"
}
}
}

View File

@@ -0,0 +1,85 @@
{
"$schema": "https://opencode.ai/desktop-theme.json",
"name": "Catppuccin Macchiato",
"id": "catppuccin-macchiato",
"light": {
"palette": {
"neutral": "#24273a",
"ink": "#cad3f5",
"primary": "#8aadf4",
"accent": "#f5bde6",
"success": "#a6da95",
"warning": "#eed49f",
"error": "#ed8796",
"info": "#8bd5ca"
},
"overrides": {
"text-weak": "#b8c0e0",
"syntax-comment": "#939ab7",
"syntax-keyword": "#c6a0f6",
"syntax-string": "#a6da95",
"syntax-primitive": "#8aadf4",
"syntax-variable": "#ed8796",
"syntax-property": "#91d7e3",
"syntax-type": "#eed49f",
"syntax-constant": "#f5a97f",
"syntax-operator": "#91d7e3",
"syntax-punctuation": "#cad3f5",
"syntax-object": "#ed8796",
"markdown-heading": "#c6a0f6",
"markdown-text": "#cad3f5",
"markdown-link": "#8aadf4",
"markdown-link-text": "#91d7e3",
"markdown-code": "#a6da95",
"markdown-block-quote": "#eed49f",
"markdown-emph": "#eed49f",
"markdown-strong": "#f5a97f",
"markdown-horizontal-rule": "#a5adcb",
"markdown-list-item": "#8aadf4",
"markdown-list-enumeration": "#91d7e3",
"markdown-image": "#8aadf4",
"markdown-image-text": "#91d7e3",
"markdown-code-block": "#cad3f5"
}
},
"dark": {
"palette": {
"neutral": "#24273a",
"ink": "#cad3f5",
"primary": "#8aadf4",
"accent": "#f5bde6",
"success": "#a6da95",
"warning": "#eed49f",
"error": "#ed8796",
"info": "#8bd5ca"
},
"overrides": {
"text-weak": "#b8c0e0",
"syntax-comment": "#939ab7",
"syntax-keyword": "#c6a0f6",
"syntax-string": "#a6da95",
"syntax-primitive": "#8aadf4",
"syntax-variable": "#ed8796",
"syntax-property": "#91d7e3",
"syntax-type": "#eed49f",
"syntax-constant": "#f5a97f",
"syntax-operator": "#91d7e3",
"syntax-punctuation": "#cad3f5",
"syntax-object": "#ed8796",
"markdown-heading": "#c6a0f6",
"markdown-text": "#cad3f5",
"markdown-link": "#8aadf4",
"markdown-link-text": "#91d7e3",
"markdown-code": "#a6da95",
"markdown-block-quote": "#eed49f",
"markdown-emph": "#eed49f",
"markdown-strong": "#f5a97f",
"markdown-horizontal-rule": "#a5adcb",
"markdown-list-item": "#8aadf4",
"markdown-list-enumeration": "#91d7e3",
"markdown-image": "#8aadf4",
"markdown-image-text": "#91d7e3",
"markdown-code-block": "#cad3f5"
}
}
}

View File

@@ -0,0 +1,87 @@
{
"$schema": "https://opencode.ai/desktop-theme.json",
"name": "Cobalt2",
"id": "cobalt2",
"light": {
"palette": {
"neutral": "#ffffff",
"ink": "#193549",
"primary": "#0066cc",
"accent": "#00acc1",
"success": "#4caf50",
"warning": "#ff9800",
"error": "#e91e63",
"info": "#ff5722"
},
"overrides": {
"text-weak": "#5c6b7d",
"syntax-comment": "#5c6b7d",
"syntax-keyword": "#ff5722",
"syntax-string": "#4caf50",
"syntax-primitive": "#ff9800",
"syntax-variable": "#193549",
"syntax-property": "#00acc1",
"syntax-type": "#00acc1",
"syntax-constant": "#e91e63",
"syntax-operator": "#ff5722",
"syntax-punctuation": "#193549",
"syntax-object": "#193549",
"markdown-heading": "#ff9800",
"markdown-text": "#193549",
"markdown-link": "#0066cc",
"markdown-link-text": "#00acc1",
"markdown-code": "#4caf50",
"markdown-block-quote": "#5c6b7d",
"markdown-emph": "#ff5722",
"markdown-strong": "#e91e63",
"markdown-horizontal-rule": "#d3dae3",
"markdown-list-item": "#0066cc",
"markdown-list-enumeration": "#00acc1",
"markdown-image": "#0066cc",
"markdown-image-text": "#00acc1",
"markdown-code-block": "#193549"
}
},
"dark": {
"palette": {
"neutral": "#193549",
"ink": "#ffffff",
"primary": "#0088ff",
"accent": "#2affdf",
"success": "#9eff80",
"warning": "#ffc600",
"error": "#ff0088",
"info": "#ff9d00",
"diffAdd": "#b9ff9f",
"diffDelete": "#ff5fb3"
},
"overrides": {
"text-weak": "#adb7c9",
"syntax-comment": "#0088ff",
"syntax-keyword": "#ff9d00",
"syntax-string": "#9eff80",
"syntax-primitive": "#ffc600",
"syntax-variable": "#ffffff",
"syntax-property": "#2affdf",
"syntax-type": "#2affdf",
"syntax-constant": "#ff628c",
"syntax-operator": "#ff9d00",
"syntax-punctuation": "#ffffff",
"syntax-object": "#ffffff",
"markdown-heading": "#ffc600",
"markdown-text": "#ffffff",
"markdown-link": "#0088ff",
"markdown-link-text": "#2affdf",
"markdown-code": "#9eff80",
"markdown-block-quote": "#adb7c9",
"markdown-emph": "#ff9d00",
"markdown-strong": "#ff628c",
"markdown-horizontal-rule": "#2d5a7b",
"markdown-list-item": "#0088ff",
"markdown-list-enumeration": "#2affdf",
"markdown-image": "#0088ff",
"markdown-image-text": "#2affdf",
"markdown-code-block": "#ffffff"
}
}
}

View File

@@ -0,0 +1,91 @@
{
"$schema": "https://opencode.ai/desktop-theme.json",
"name": "Cursor",
"id": "cursor",
"light": {
"palette": {
"neutral": "#fcfcfc",
"ink": "#141414",
"primary": "#6f9ba6",
"accent": "#6f9ba6",
"success": "#1f8a65",
"warning": "#db704b",
"error": "#cf2d56",
"info": "#3c7cab",
"interactive": "#206595",
"diffAdd": "#55a583",
"diffDelete": "#e75e78"
},
"overrides": {
"text-weak": "#141414ad",
"syntax-comment": "#141414ad",
"syntax-keyword": "#b3003f",
"syntax-string": "#9e94d5",
"syntax-primitive": "#db704b",
"syntax-variable": "#141414",
"syntax-property": "#141414ad",
"syntax-type": "#206595",
"syntax-constant": "#b8448b",
"syntax-operator": "#141414",
"syntax-punctuation": "#141414",
"syntax-object": "#141414",
"markdown-heading": "#206595",
"markdown-text": "#141414",
"markdown-link": "#206595",
"markdown-link-text": "#141414ad",
"markdown-code": "#1f8a65",
"markdown-block-quote": "#141414ad",
"markdown-emph": "#141414",
"markdown-strong": "#141414",
"markdown-horizontal-rule": "#141414ad",
"markdown-list-item": "#141414",
"markdown-list-enumeration": "#141414ad",
"markdown-image": "#206595",
"markdown-image-text": "#141414ad",
"markdown-code-block": "#141414"
}
},
"dark": {
"palette": {
"neutral": "#181818",
"ink": "#e4e4e4",
"primary": "#88c0d0",
"accent": "#88c0d0",
"success": "#3fa266",
"warning": "#f1b467",
"error": "#e34671",
"info": "#81a1c1",
"interactive": "#82D2CE",
"diffAdd": "#70b489",
"diffDelete": "#fc6b83"
},
"overrides": {
"text-weak": "#e4e4e45e",
"syntax-comment": "#e4e4e45e",
"syntax-keyword": "#82D2CE",
"syntax-string": "#E394DC",
"syntax-primitive": "#EFB080",
"syntax-variable": "#e4e4e4",
"syntax-property": "#81a1c1",
"syntax-type": "#EFB080",
"syntax-constant": "#F8C762",
"syntax-operator": "#e4e4e4",
"syntax-punctuation": "#e4e4e4",
"syntax-object": "#e4e4e4",
"markdown-heading": "#AAA0FA",
"markdown-text": "#e4e4e4",
"markdown-link": "#82D2CE",
"markdown-link-text": "#81a1c1",
"markdown-code": "#E394DC",
"markdown-block-quote": "#e4e4e45e",
"markdown-emph": "#82D2CE",
"markdown-strong": "#F8C762",
"markdown-horizontal-rule": "#e4e4e45e",
"markdown-list-item": "#e4e4e4",
"markdown-list-enumeration": "#88c0d0",
"markdown-image": "#88c0d0",
"markdown-image-text": "#81a1c1",
"markdown-code-block": "#e4e4e4"
}
}
}

View File

@@ -0,0 +1,89 @@
{
"$schema": "https://opencode.ai/desktop-theme.json",
"name": "Everforest",
"id": "everforest",
"light": {
"palette": {
"neutral": "#fdf6e3",
"ink": "#5c6a72",
"primary": "#8da101",
"accent": "#df69ba",
"success": "#8da101",
"warning": "#f57d26",
"error": "#f85552",
"info": "#35a77c",
"diffAdd": "#4db380",
"diffDelete": "#f52a65"
},
"overrides": {
"text-weak": "#a6b0a0",
"syntax-comment": "#a6b0a0",
"syntax-keyword": "#df69ba",
"syntax-string": "#8da101",
"syntax-primitive": "#8da101",
"syntax-variable": "#f85552",
"syntax-property": "#35a77c",
"syntax-type": "#dfa000",
"syntax-constant": "#f57d26",
"syntax-operator": "#35a77c",
"syntax-punctuation": "#5c6a72",
"syntax-object": "#f85552",
"markdown-heading": "#df69ba",
"markdown-text": "#5c6a72",
"markdown-link": "#8da101",
"markdown-link-text": "#35a77c",
"markdown-code": "#8da101",
"markdown-block-quote": "#dfa000",
"markdown-emph": "#dfa000",
"markdown-strong": "#f57d26",
"markdown-horizontal-rule": "#a6b0a0",
"markdown-list-item": "#8da101",
"markdown-list-enumeration": "#35a77c",
"markdown-image": "#8da101",
"markdown-image-text": "#35a77c",
"markdown-code-block": "#5c6a72"
}
},
"dark": {
"palette": {
"neutral": "#2d353b",
"ink": "#d3c6aa",
"primary": "#a7c080",
"accent": "#d699b6",
"success": "#a7c080",
"warning": "#e69875",
"error": "#e67e80",
"info": "#83c092",
"diffAdd": "#b8db87",
"diffDelete": "#e26a75"
},
"overrides": {
"text-weak": "#7a8478",
"syntax-comment": "#7a8478",
"syntax-keyword": "#d699b6",
"syntax-string": "#a7c080",
"syntax-primitive": "#a7c080",
"syntax-variable": "#e67e80",
"syntax-property": "#83c092",
"syntax-type": "#dbbc7f",
"syntax-constant": "#e69875",
"syntax-operator": "#83c092",
"syntax-punctuation": "#d3c6aa",
"syntax-object": "#e67e80",
"markdown-heading": "#d699b6",
"markdown-text": "#d3c6aa",
"markdown-link": "#a7c080",
"markdown-link-text": "#83c092",
"markdown-code": "#a7c080",
"markdown-block-quote": "#dbbc7f",
"markdown-emph": "#dbbc7f",
"markdown-strong": "#e69875",
"markdown-horizontal-rule": "#7a8478",
"markdown-list-item": "#a7c080",
"markdown-list-enumeration": "#83c092",
"markdown-image": "#a7c080",
"markdown-image-text": "#83c092",
"markdown-code-block": "#d3c6aa"
}
}
}

View File

@@ -0,0 +1,86 @@
{
"$schema": "https://opencode.ai/desktop-theme.json",
"name": "Flexoki",
"id": "flexoki",
"light": {
"palette": {
"neutral": "#FFFCF0",
"ink": "#100F0F",
"primary": "#205EA6",
"accent": "#BC5215",
"success": "#66800B",
"warning": "#BC5215",
"error": "#AF3029",
"info": "#24837B"
},
"overrides": {
"text-weak": "#6F6E69",
"syntax-comment": "#6F6E69",
"syntax-keyword": "#66800B",
"syntax-string": "#24837B",
"syntax-primitive": "#BC5215",
"syntax-variable": "#205EA6",
"syntax-property": "#24837B",
"syntax-type": "#AD8301",
"syntax-constant": "#5E409D",
"syntax-operator": "#6F6E69",
"syntax-punctuation": "#6F6E69",
"syntax-object": "#205EA6",
"markdown-heading": "#5E409D",
"markdown-text": "#100F0F",
"markdown-link": "#205EA6",
"markdown-link-text": "#24837B",
"markdown-code": "#24837B",
"markdown-block-quote": "#AD8301",
"markdown-emph": "#AD8301",
"markdown-strong": "#BC5215",
"markdown-horizontal-rule": "#6F6E69",
"markdown-list-item": "#BC5215",
"markdown-list-enumeration": "#24837B",
"markdown-image": "#A02F6F",
"markdown-image-text": "#24837B",
"markdown-code-block": "#100F0F"
}
},
"dark": {
"palette": {
"neutral": "#100F0F",
"ink": "#CECDC3",
"primary": "#DA702C",
"accent": "#8B7EC8",
"success": "#879A39",
"warning": "#DA702C",
"error": "#D14D41",
"info": "#3AA99F",
"interactive": "#4385BE"
},
"overrides": {
"text-weak": "#6F6E69",
"syntax-comment": "#6F6E69",
"syntax-keyword": "#879A39",
"syntax-string": "#3AA99F",
"syntax-primitive": "#DA702C",
"syntax-variable": "#4385BE",
"syntax-property": "#3AA99F",
"syntax-type": "#D0A215",
"syntax-constant": "#8B7EC8",
"syntax-operator": "#B7B5AC",
"syntax-punctuation": "#B7B5AC",
"syntax-object": "#4385BE",
"markdown-heading": "#8B7EC8",
"markdown-text": "#CECDC3",
"markdown-link": "#4385BE",
"markdown-link-text": "#3AA99F",
"markdown-code": "#3AA99F",
"markdown-block-quote": "#D0A215",
"markdown-emph": "#D0A215",
"markdown-strong": "#DA702C",
"markdown-horizontal-rule": "#6F6E69",
"markdown-list-item": "#DA702C",
"markdown-list-enumeration": "#3AA99F",
"markdown-image": "#CE5D97",
"markdown-image-text": "#3AA99F",
"markdown-code-block": "#CECDC3"
}
}
}

View File

@@ -0,0 +1,85 @@
{
"$schema": "https://opencode.ai/desktop-theme.json",
"name": "GitHub",
"id": "github",
"light": {
"palette": {
"neutral": "#ffffff",
"ink": "#24292f",
"primary": "#0969da",
"accent": "#1b7c83",
"success": "#1a7f37",
"warning": "#9a6700",
"error": "#cf222e",
"info": "#bc4c00"
},
"overrides": {
"text-weak": "#57606a",
"syntax-comment": "#57606a",
"syntax-keyword": "#cf222e",
"syntax-string": "#0969da",
"syntax-primitive": "#8250df",
"syntax-variable": "#bc4c00",
"syntax-property": "#1b7c83",
"syntax-type": "#bc4c00",
"syntax-constant": "#1b7c83",
"syntax-operator": "#cf222e",
"syntax-punctuation": "#24292f",
"syntax-object": "#bc4c00",
"markdown-heading": "#0969da",
"markdown-text": "#24292f",
"markdown-link": "#0969da",
"markdown-link-text": "#1b7c83",
"markdown-code": "#bf3989",
"markdown-block-quote": "#57606a",
"markdown-emph": "#9a6700",
"markdown-strong": "#bc4c00",
"markdown-horizontal-rule": "#d0d7de",
"markdown-list-item": "#0969da",
"markdown-list-enumeration": "#1b7c83",
"markdown-image": "#0969da",
"markdown-image-text": "#1b7c83",
"markdown-code-block": "#24292f"
}
},
"dark": {
"palette": {
"neutral": "#0d1117",
"ink": "#c9d1d9",
"primary": "#58a6ff",
"accent": "#39c5cf",
"success": "#3fb950",
"warning": "#e3b341",
"error": "#f85149",
"info": "#d29922"
},
"overrides": {
"text-weak": "#8b949e",
"syntax-comment": "#8b949e",
"syntax-keyword": "#ff7b72",
"syntax-string": "#39c5cf",
"syntax-primitive": "#bc8cff",
"syntax-variable": "#d29922",
"syntax-property": "#39c5cf",
"syntax-type": "#d29922",
"syntax-constant": "#58a6ff",
"syntax-operator": "#ff7b72",
"syntax-punctuation": "#c9d1d9",
"syntax-object": "#d29922",
"markdown-heading": "#58a6ff",
"markdown-text": "#c9d1d9",
"markdown-link": "#58a6ff",
"markdown-link-text": "#39c5cf",
"markdown-code": "#ff7b72",
"markdown-block-quote": "#8b949e",
"markdown-emph": "#e3b341",
"markdown-strong": "#d29922",
"markdown-horizontal-rule": "#30363d",
"markdown-list-item": "#58a6ff",
"markdown-list-enumeration": "#39c5cf",
"markdown-image": "#58a6ff",
"markdown-image-text": "#39c5cf",
"markdown-code-block": "#c9d1d9"
}
}
}

View File

@@ -0,0 +1,89 @@
{
"$schema": "https://opencode.ai/desktop-theme.json",
"name": "Kanagawa",
"id": "kanagawa",
"light": {
"palette": {
"neutral": "#F2E9DE",
"ink": "#54433A",
"primary": "#2D4F67",
"accent": "#D27E99",
"success": "#98BB6C",
"warning": "#D7A657",
"error": "#E82424",
"info": "#76946A",
"diffAdd": "#89AF5B",
"diffDelete": "#D61F1F"
},
"overrides": {
"text-weak": "#9E9389",
"syntax-comment": "#9E9389",
"syntax-keyword": "#957FB8",
"syntax-string": "#98BB6C",
"syntax-primitive": "#2D4F67",
"syntax-variable": "#54433A",
"syntax-property": "#76946A",
"syntax-type": "#C38D9D",
"syntax-constant": "#D7A657",
"syntax-operator": "#D27E99",
"syntax-punctuation": "#54433A",
"syntax-object": "#54433A",
"markdown-heading": "#957FB8",
"markdown-text": "#54433A",
"markdown-link": "#2D4F67",
"markdown-link-text": "#76946A",
"markdown-code": "#98BB6C",
"markdown-block-quote": "#9E9389",
"markdown-emph": "#C38D9D",
"markdown-strong": "#D7A657",
"markdown-horizontal-rule": "#9E9389",
"markdown-list-item": "#2D4F67",
"markdown-list-enumeration": "#76946A",
"markdown-image": "#2D4F67",
"markdown-image-text": "#76946A",
"markdown-code-block": "#54433A"
}
},
"dark": {
"palette": {
"neutral": "#1F1F28",
"ink": "#DCD7BA",
"primary": "#7E9CD8",
"accent": "#D27E99",
"success": "#98BB6C",
"warning": "#D7A657",
"error": "#E82424",
"info": "#76946A",
"diffAdd": "#A9D977",
"diffDelete": "#F24A4A"
},
"overrides": {
"text-weak": "#727169",
"syntax-comment": "#727169",
"syntax-keyword": "#957FB8",
"syntax-string": "#98BB6C",
"syntax-primitive": "#7E9CD8",
"syntax-variable": "#DCD7BA",
"syntax-property": "#76946A",
"syntax-type": "#C38D9D",
"syntax-constant": "#D7A657",
"syntax-operator": "#D27E99",
"syntax-punctuation": "#DCD7BA",
"syntax-object": "#DCD7BA",
"markdown-heading": "#957FB8",
"markdown-text": "#DCD7BA",
"markdown-link": "#7E9CD8",
"markdown-link-text": "#76946A",
"markdown-code": "#98BB6C",
"markdown-block-quote": "#727169",
"markdown-emph": "#C38D9D",
"markdown-strong": "#D7A657",
"markdown-horizontal-rule": "#727169",
"markdown-list-item": "#7E9CD8",
"markdown-list-enumeration": "#76946A",
"markdown-image": "#7E9CD8",
"markdown-image-text": "#76946A",
"markdown-code-block": "#DCD7BA"
}
}
}

View File

@@ -0,0 +1,87 @@
{
"$schema": "https://opencode.ai/desktop-theme.json",
"name": "Lucent Orng",
"id": "lucent-orng",
"light": {
"palette": {
"neutral": "#fff5f0",
"ink": "#1a1a1a",
"primary": "#EC5B2B",
"accent": "#c94d24",
"success": "#0062d1",
"warning": "#EC5B2B",
"error": "#d1383d",
"info": "#318795",
"diffDelete": "#f52a65"
},
"overrides": {
"text-weak": "#8a8a8a",
"syntax-comment": "#8a8a8a",
"syntax-keyword": "#EC5B2B",
"syntax-string": "#0062d1",
"syntax-primitive": "#c94d24",
"syntax-variable": "#d1383d",
"syntax-property": "#318795",
"syntax-type": "#b0851f",
"syntax-constant": "#EC5B2B",
"syntax-operator": "#318795",
"syntax-punctuation": "#1a1a1a",
"syntax-object": "#d1383d",
"markdown-heading": "#EC5B2B",
"markdown-text": "#1a1a1a",
"markdown-link": "#EC5B2B",
"markdown-link-text": "#318795",
"markdown-code": "#0062d1",
"markdown-block-quote": "#b0851f",
"markdown-emph": "#b0851f",
"markdown-strong": "#EC5B2B",
"markdown-horizontal-rule": "#8a8a8a",
"markdown-list-item": "#EC5B2B",
"markdown-list-enumeration": "#318795",
"markdown-image": "#EC5B2B",
"markdown-image-text": "#318795",
"markdown-code-block": "#1a1a1a"
}
},
"dark": {
"palette": {
"neutral": "#2a1a15",
"ink": "#eeeeee",
"primary": "#EC5B2B",
"accent": "#FFF7F1",
"success": "#6ba1e6",
"warning": "#EC5B2B",
"error": "#e06c75",
"info": "#56b6c2",
"diffDelete": "#e26a75"
},
"overrides": {
"text-weak": "#808080",
"syntax-comment": "#808080",
"syntax-keyword": "#EC5B2B",
"syntax-string": "#6ba1e6",
"syntax-primitive": "#EE7948",
"syntax-variable": "#e06c75",
"syntax-property": "#56b6c2",
"syntax-type": "#e5c07b",
"syntax-constant": "#FFF7F1",
"syntax-operator": "#56b6c2",
"syntax-punctuation": "#eeeeee",
"syntax-object": "#e06c75",
"markdown-heading": "#EC5B2B",
"markdown-text": "#eeeeee",
"markdown-link": "#EC5B2B",
"markdown-link-text": "#56b6c2",
"markdown-code": "#6ba1e6",
"markdown-block-quote": "#FFF7F1",
"markdown-emph": "#e5c07b",
"markdown-strong": "#EE7948",
"markdown-horizontal-rule": "#808080",
"markdown-list-item": "#EC5B2B",
"markdown-list-enumeration": "#56b6c2",
"markdown-image": "#EC5B2B",
"markdown-image-text": "#56b6c2",
"markdown-code-block": "#eeeeee"
}
}
}

View File

@@ -0,0 +1,87 @@
{
"$schema": "https://opencode.ai/desktop-theme.json",
"name": "Material",
"id": "material",
"light": {
"palette": {
"neutral": "#fafafa",
"ink": "#263238",
"primary": "#6182b8",
"accent": "#39adb5",
"success": "#91b859",
"warning": "#ffb300",
"error": "#e53935",
"info": "#f4511e",
"interactive": "#39adb5"
},
"overrides": {
"text-weak": "#90a4ae",
"syntax-comment": "#90a4ae",
"syntax-keyword": "#7c4dff",
"syntax-string": "#91b859",
"syntax-primitive": "#6182b8",
"syntax-variable": "#263238",
"syntax-property": "#7c4dff",
"syntax-type": "#ffb300",
"syntax-constant": "#f4511e",
"syntax-operator": "#39adb5",
"syntax-punctuation": "#263238",
"syntax-object": "#263238",
"markdown-heading": "#6182b8",
"markdown-text": "#263238",
"markdown-link": "#39adb5",
"markdown-link-text": "#7c4dff",
"markdown-code": "#91b859",
"markdown-block-quote": "#90a4ae",
"markdown-emph": "#ffb300",
"markdown-strong": "#f4511e",
"markdown-horizontal-rule": "#e0e0e0",
"markdown-list-item": "#6182b8",
"markdown-list-enumeration": "#39adb5",
"markdown-image": "#39adb5",
"markdown-image-text": "#7c4dff",
"markdown-code-block": "#263238"
}
},
"dark": {
"palette": {
"neutral": "#263238",
"ink": "#eeffff",
"primary": "#82aaff",
"accent": "#89ddff",
"success": "#c3e88d",
"warning": "#ffcb6b",
"error": "#f07178",
"info": "#ffcb6b",
"interactive": "#89ddff"
},
"overrides": {
"text-weak": "#546e7a",
"syntax-comment": "#546e7a",
"syntax-keyword": "#c792ea",
"syntax-string": "#c3e88d",
"syntax-primitive": "#82aaff",
"syntax-variable": "#eeffff",
"syntax-property": "#c792ea",
"syntax-type": "#ffcb6b",
"syntax-constant": "#ffcb6b",
"syntax-operator": "#89ddff",
"syntax-punctuation": "#eeffff",
"syntax-object": "#eeffff",
"markdown-heading": "#82aaff",
"markdown-text": "#eeffff",
"markdown-link": "#89ddff",
"markdown-link-text": "#c792ea",
"markdown-code": "#c3e88d",
"markdown-block-quote": "#546e7a",
"markdown-emph": "#ffcb6b",
"markdown-strong": "#ffcb6b",
"markdown-horizontal-rule": "#37474f",
"markdown-list-item": "#82aaff",
"markdown-list-enumeration": "#89ddff",
"markdown-image": "#89ddff",
"markdown-image-text": "#c792ea",
"markdown-code-block": "#eeffff"
}
}
}

View File

@@ -0,0 +1,91 @@
{
"$schema": "https://opencode.ai/desktop-theme.json",
"name": "Matrix",
"id": "matrix",
"light": {
"palette": {
"neutral": "#eef3ea",
"ink": "#203022",
"primary": "#1cc24b",
"accent": "#c770ff",
"success": "#1cc24b",
"warning": "#e6ff57",
"error": "#ff4b4b",
"info": "#30b3ff",
"interactive": "#30b3ff",
"diffAdd": "#5dac7e",
"diffDelete": "#d53a3a"
},
"overrides": {
"text-weak": "#748476",
"syntax-comment": "#748476",
"syntax-keyword": "#c770ff",
"syntax-string": "#1cc24b",
"syntax-primitive": "#30b3ff",
"syntax-variable": "#203022",
"syntax-property": "#24f6d9",
"syntax-type": "#e6ff57",
"syntax-constant": "#ffa83d",
"syntax-operator": "#24f6d9",
"syntax-punctuation": "#203022",
"syntax-object": "#203022",
"markdown-heading": "#24f6d9",
"markdown-text": "#203022",
"markdown-link": "#30b3ff",
"markdown-link-text": "#24f6d9",
"markdown-code": "#1cc24b",
"markdown-block-quote": "#748476",
"markdown-emph": "#ffa83d",
"markdown-strong": "#e6ff57",
"markdown-horizontal-rule": "#748476",
"markdown-list-item": "#30b3ff",
"markdown-list-enumeration": "#24f6d9",
"markdown-image": "#30b3ff",
"markdown-image-text": "#24f6d9",
"markdown-code-block": "#203022"
}
},
"dark": {
"palette": {
"neutral": "#0a0e0a",
"ink": "#62ff94",
"primary": "#2eff6a",
"accent": "#c770ff",
"success": "#62ff94",
"warning": "#e6ff57",
"error": "#ff4b4b",
"info": "#30b3ff",
"interactive": "#30b3ff",
"diffAdd": "#77ffaf",
"diffDelete": "#ff7171"
},
"overrides": {
"text-weak": "#8ca391",
"syntax-comment": "#8ca391",
"syntax-keyword": "#c770ff",
"syntax-string": "#1cc24b",
"syntax-primitive": "#30b3ff",
"syntax-variable": "#62ff94",
"syntax-property": "#24f6d9",
"syntax-type": "#e6ff57",
"syntax-constant": "#ffa83d",
"syntax-operator": "#24f6d9",
"syntax-punctuation": "#62ff94",
"syntax-object": "#62ff94",
"markdown-heading": "#00efff",
"markdown-text": "#62ff94",
"markdown-link": "#30b3ff",
"markdown-link-text": "#24f6d9",
"markdown-code": "#1cc24b",
"markdown-block-quote": "#8ca391",
"markdown-emph": "#ffa83d",
"markdown-strong": "#e6ff57",
"markdown-horizontal-rule": "#8ca391",
"markdown-list-item": "#30b3ff",
"markdown-list-enumeration": "#24f6d9",
"markdown-image": "#30b3ff",
"markdown-image-text": "#24f6d9",
"markdown-code-block": "#62ff94"
}
}
}

View File

@@ -0,0 +1,86 @@
{
"$schema": "https://opencode.ai/desktop-theme.json",
"name": "Mercury",
"id": "mercury",
"light": {
"palette": {
"neutral": "#ffffff",
"ink": "#363644",
"primary": "#5266eb",
"accent": "#8da4f5",
"success": "#036e43",
"warning": "#a44200",
"error": "#b0175f",
"info": "#007f95",
"interactive": "#465bd1"
},
"overrides": {
"text-weak": "#70707d",
"syntax-comment": "#70707d",
"syntax-keyword": "#465bd1",
"syntax-string": "#036e43",
"syntax-primitive": "#5266eb",
"syntax-variable": "#007f95",
"syntax-property": "#5266eb",
"syntax-type": "#007f95",
"syntax-constant": "#a44200",
"syntax-operator": "#465bd1",
"syntax-punctuation": "#363644",
"syntax-object": "#007f95",
"markdown-heading": "#1e1e2a",
"markdown-text": "#363644",
"markdown-link": "#465bd1",
"markdown-link-text": "#5266eb",
"markdown-code": "#036e43",
"markdown-block-quote": "#70707d",
"markdown-emph": "#a44200",
"markdown-strong": "#1e1e2a",
"markdown-horizontal-rule": "#7073931a",
"markdown-list-item": "#1e1e2a",
"markdown-list-enumeration": "#5266eb",
"markdown-image": "#465bd1",
"markdown-image-text": "#5266eb",
"markdown-code-block": "#363644"
}
},
"dark": {
"palette": {
"neutral": "#171721",
"ink": "#dddde5",
"primary": "#8da4f5",
"accent": "#8da4f5",
"success": "#77c599",
"warning": "#fc9b6f",
"error": "#fc92b4",
"info": "#77becf"
},
"overrides": {
"text-weak": "#9d9da8",
"syntax-comment": "#9d9da8",
"syntax-keyword": "#8da4f5",
"syntax-string": "#77c599",
"syntax-primitive": "#8da4f5",
"syntax-variable": "#77becf",
"syntax-property": "#a7b6f8",
"syntax-type": "#77becf",
"syntax-constant": "#fc9b6f",
"syntax-operator": "#8da4f5",
"syntax-punctuation": "#dddde5",
"syntax-object": "#77becf",
"markdown-heading": "#ffffff",
"markdown-text": "#dddde5",
"markdown-link": "#8da4f5",
"markdown-link-text": "#a7b6f8",
"markdown-code": "#77c599",
"markdown-block-quote": "#9d9da8",
"markdown-emph": "#fc9b6f",
"markdown-strong": "#f4f5f9",
"markdown-horizontal-rule": "#b4b7c81f",
"markdown-list-item": "#ffffff",
"markdown-list-enumeration": "#8da4f5",
"markdown-image": "#8da4f5",
"markdown-image-text": "#a7b6f8",
"markdown-code-block": "#dddde5"
}
}
}

View File

@@ -4,7 +4,8 @@
"id": "oc-2",
"light": {
"palette": {
"neutral": "#8f8f8f",
"neutral": "#f7f7f7",
"ink": "#171311",
"primary": "#dcde8d",
"success": "#12c905",
"warning": "#ffdc17",
@@ -30,7 +31,8 @@
},
"dark": {
"palette": {
"neutral": "#707070",
"neutral": "#151515",
"ink": "#f1ece8",
"primary": "#fab283",
"success": "#12c905",
"warning": "#fcd53a",

View File

@@ -0,0 +1,89 @@
{
"$schema": "https://opencode.ai/desktop-theme.json",
"name": "One Dark",
"id": "one-dark",
"light": {
"palette": {
"neutral": "#fafafa",
"ink": "#383a42",
"primary": "#4078f2",
"accent": "#0184bc",
"success": "#50a14f",
"warning": "#c18401",
"error": "#e45649",
"info": "#986801",
"diffAdd": "#489447",
"diffDelete": "#d65145"
},
"overrides": {
"text-weak": "#a0a1a7",
"syntax-comment": "#a0a1a7",
"syntax-keyword": "#a626a4",
"syntax-string": "#50a14f",
"syntax-primitive": "#4078f2",
"syntax-variable": "#e45649",
"syntax-property": "#0184bc",
"syntax-type": "#c18401",
"syntax-constant": "#986801",
"syntax-operator": "#0184bc",
"syntax-punctuation": "#383a42",
"syntax-object": "#e45649",
"markdown-heading": "#a626a4",
"markdown-text": "#383a42",
"markdown-link": "#4078f2",
"markdown-link-text": "#0184bc",
"markdown-code": "#50a14f",
"markdown-block-quote": "#a0a1a7",
"markdown-emph": "#c18401",
"markdown-strong": "#986801",
"markdown-horizontal-rule": "#a0a1a7",
"markdown-list-item": "#4078f2",
"markdown-list-enumeration": "#0184bc",
"markdown-image": "#4078f2",
"markdown-image-text": "#0184bc",
"markdown-code-block": "#383a42"
}
},
"dark": {
"palette": {
"neutral": "#282c34",
"ink": "#abb2bf",
"primary": "#61afef",
"accent": "#56b6c2",
"success": "#98c379",
"warning": "#e5c07b",
"error": "#e06c75",
"info": "#d19a66",
"diffAdd": "#aad482",
"diffDelete": "#e8828b"
},
"overrides": {
"text-weak": "#5c6370",
"syntax-comment": "#5c6370",
"syntax-keyword": "#c678dd",
"syntax-string": "#98c379",
"syntax-primitive": "#61afef",
"syntax-variable": "#e06c75",
"syntax-property": "#56b6c2",
"syntax-type": "#e5c07b",
"syntax-constant": "#d19a66",
"syntax-operator": "#56b6c2",
"syntax-punctuation": "#abb2bf",
"syntax-object": "#e06c75",
"markdown-heading": "#c678dd",
"markdown-text": "#abb2bf",
"markdown-link": "#61afef",
"markdown-link-text": "#56b6c2",
"markdown-code": "#98c379",
"markdown-block-quote": "#5c6370",
"markdown-emph": "#e5c07b",
"markdown-strong": "#d19a66",
"markdown-horizontal-rule": "#5c6370",
"markdown-list-item": "#61afef",
"markdown-list-enumeration": "#56b6c2",
"markdown-image": "#61afef",
"markdown-image-text": "#56b6c2",
"markdown-code-block": "#abb2bf"
}
}
}

View File

@@ -0,0 +1,89 @@
{
"$schema": "https://opencode.ai/desktop-theme.json",
"name": "OpenCode",
"id": "opencode",
"light": {
"palette": {
"neutral": "#ffffff",
"ink": "#1a1a1a",
"primary": "#3b7dd8",
"accent": "#d68c27",
"success": "#3d9a57",
"warning": "#d68c27",
"error": "#d1383d",
"info": "#318795",
"diffAdd": "#4db380",
"diffDelete": "#f52a65"
},
"overrides": {
"text-weak": "#8a8a8a",
"syntax-comment": "#8a8a8a",
"syntax-keyword": "#d68c27",
"syntax-string": "#3d9a57",
"syntax-primitive": "#3b7dd8",
"syntax-variable": "#d1383d",
"syntax-property": "#318795",
"syntax-type": "#b0851f",
"syntax-constant": "#d68c27",
"syntax-operator": "#318795",
"syntax-punctuation": "#1a1a1a",
"syntax-object": "#d1383d",
"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"
}
},
"dark": {
"palette": {
"neutral": "#0a0a0a",
"ink": "#eeeeee",
"primary": "#fab283",
"accent": "#9d7cd8",
"success": "#7fd88f",
"warning": "#f5a742",
"error": "#e06c75",
"info": "#56b6c2",
"diffAdd": "#b8db87",
"diffDelete": "#e26a75"
},
"overrides": {
"text-weak": "#808080",
"syntax-comment": "#808080",
"syntax-keyword": "#9d7cd8",
"syntax-string": "#7fd88f",
"syntax-primitive": "#fab283",
"syntax-variable": "#e06c75",
"syntax-property": "#56b6c2",
"syntax-type": "#e5c07b",
"syntax-constant": "#f5a742",
"syntax-operator": "#56b6c2",
"syntax-punctuation": "#eeeeee",
"syntax-object": "#e06c75",
"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"
}
}
}

View File

@@ -0,0 +1,87 @@
{
"$schema": "https://opencode.ai/desktop-theme.json",
"name": "Orng",
"id": "orng",
"light": {
"palette": {
"neutral": "#ffffff",
"ink": "#1a1a1a",
"primary": "#EC5B2B",
"accent": "#c94d24",
"success": "#0062d1",
"warning": "#EC5B2B",
"error": "#d1383d",
"info": "#318795",
"diffDelete": "#f52a65"
},
"overrides": {
"text-weak": "#8a8a8a",
"syntax-comment": "#8a8a8a",
"syntax-keyword": "#EC5B2B",
"syntax-string": "#0062d1",
"syntax-primitive": "#c94d24",
"syntax-variable": "#d1383d",
"syntax-property": "#318795",
"syntax-type": "#b0851f",
"syntax-constant": "#EC5B2B",
"syntax-operator": "#318795",
"syntax-punctuation": "#1a1a1a",
"syntax-object": "#d1383d",
"markdown-heading": "#EC5B2B",
"markdown-text": "#1a1a1a",
"markdown-link": "#EC5B2B",
"markdown-link-text": "#318795",
"markdown-code": "#0062d1",
"markdown-block-quote": "#b0851f",
"markdown-emph": "#b0851f",
"markdown-strong": "#EC5B2B",
"markdown-horizontal-rule": "#8a8a8a",
"markdown-list-item": "#EC5B2B",
"markdown-list-enumeration": "#318795",
"markdown-image": "#EC5B2B",
"markdown-image-text": "#318795",
"markdown-code-block": "#1a1a1a"
}
},
"dark": {
"palette": {
"neutral": "#0a0a0a",
"ink": "#eeeeee",
"primary": "#EC5B2B",
"accent": "#FFF7F1",
"success": "#6ba1e6",
"warning": "#EC5B2B",
"error": "#e06c75",
"info": "#56b6c2",
"diffDelete": "#e26a75"
},
"overrides": {
"text-weak": "#808080",
"syntax-comment": "#808080",
"syntax-keyword": "#EC5B2B",
"syntax-string": "#6ba1e6",
"syntax-primitive": "#EE7948",
"syntax-variable": "#e06c75",
"syntax-property": "#56b6c2",
"syntax-type": "#e5c07b",
"syntax-constant": "#FFF7F1",
"syntax-operator": "#56b6c2",
"syntax-punctuation": "#eeeeee",
"syntax-object": "#e06c75",
"markdown-heading": "#EC5B2B",
"markdown-text": "#eeeeee",
"markdown-link": "#EC5B2B",
"markdown-link-text": "#56b6c2",
"markdown-code": "#6ba1e6",
"markdown-block-quote": "#FFF7F1",
"markdown-emph": "#e5c07b",
"markdown-strong": "#EE7948",
"markdown-horizontal-rule": "#808080",
"markdown-list-item": "#EC5B2B",
"markdown-list-enumeration": "#56b6c2",
"markdown-image": "#EC5B2B",
"markdown-image-text": "#56b6c2",
"markdown-code-block": "#eeeeee"
}
}
}

View File

@@ -0,0 +1,88 @@
{
"$schema": "https://opencode.ai/desktop-theme.json",
"name": "Osaka Jade",
"id": "osaka-jade",
"light": {
"palette": {
"neutral": "#F6F5DD",
"ink": "#111c18",
"primary": "#1faa90",
"accent": "#3d7a52",
"success": "#3d7a52",
"warning": "#b5a020",
"error": "#c7392d",
"info": "#1faa90"
},
"overrides": {
"text-weak": "#53685B",
"syntax-comment": "#53685B",
"syntax-keyword": "#1faa90",
"syntax-string": "#3d7a52",
"syntax-primitive": "#3d7560",
"syntax-variable": "#111c18",
"syntax-property": "#3d7a52",
"syntax-type": "#3d7a52",
"syntax-constant": "#a8527a",
"syntax-operator": "#b5a020",
"syntax-punctuation": "#111c18",
"syntax-object": "#111c18",
"markdown-heading": "#1faa90",
"markdown-text": "#111c18",
"markdown-link": "#1faa90",
"markdown-link-text": "#3d7a52",
"markdown-code": "#3d7a52",
"markdown-block-quote": "#53685B",
"markdown-emph": "#a8527a",
"markdown-strong": "#111c18",
"markdown-horizontal-rule": "#53685B",
"markdown-list-item": "#1faa90",
"markdown-list-enumeration": "#1faa90",
"markdown-image": "#1faa90",
"markdown-image-text": "#3d7a52",
"markdown-code-block": "#111c18"
}
},
"dark": {
"palette": {
"neutral": "#111c18",
"ink": "#C1C497",
"primary": "#2DD5B7",
"accent": "#549e6a",
"success": "#549e6a",
"warning": "#E5C736",
"error": "#FF5345",
"info": "#2DD5B7",
"interactive": "#8CD3CB",
"diffAdd": "#63b07a",
"diffDelete": "#db9f9c"
},
"overrides": {
"text-weak": "#53685B",
"syntax-comment": "#53685B",
"syntax-keyword": "#2DD5B7",
"syntax-string": "#63b07a",
"syntax-primitive": "#509475",
"syntax-variable": "#C1C497",
"syntax-property": "#549e6a",
"syntax-type": "#549e6a",
"syntax-constant": "#D2689C",
"syntax-operator": "#459451",
"syntax-punctuation": "#C1C497",
"syntax-object": "#C1C497",
"markdown-heading": "#2DD5B7",
"markdown-text": "#C1C497",
"markdown-link": "#8CD3CB",
"markdown-link-text": "#549e6a",
"markdown-code": "#63b07a",
"markdown-block-quote": "#53685B",
"markdown-emph": "#D2689C",
"markdown-strong": "#C1C497",
"markdown-horizontal-rule": "#53685B",
"markdown-list-item": "#2DD5B7",
"markdown-list-enumeration": "#8CD3CB",
"markdown-image": "#8CD3CB",
"markdown-image-text": "#549e6a",
"markdown-code-block": "#C1C497"
}
}
}

View File

@@ -0,0 +1,85 @@
{
"$schema": "https://opencode.ai/desktop-theme.json",
"name": "Palenight",
"id": "palenight",
"light": {
"palette": {
"neutral": "#fafafa",
"ink": "#292d3e",
"primary": "#4976eb",
"accent": "#00acc1",
"success": "#91b859",
"warning": "#ffb300",
"error": "#e53935",
"info": "#f4511e"
},
"overrides": {
"text-weak": "#8796b0",
"syntax-comment": "#8796b0",
"syntax-keyword": "#a854f2",
"syntax-string": "#91b859",
"syntax-primitive": "#4976eb",
"syntax-variable": "#292d3e",
"syntax-property": "#00acc1",
"syntax-type": "#ffb300",
"syntax-constant": "#f4511e",
"syntax-operator": "#00acc1",
"syntax-punctuation": "#292d3e",
"syntax-object": "#292d3e",
"markdown-heading": "#a854f2",
"markdown-text": "#292d3e",
"markdown-link": "#4976eb",
"markdown-link-text": "#00acc1",
"markdown-code": "#91b859",
"markdown-block-quote": "#8796b0",
"markdown-emph": "#ffb300",
"markdown-strong": "#f4511e",
"markdown-horizontal-rule": "#8796b0",
"markdown-list-item": "#4976eb",
"markdown-list-enumeration": "#00acc1",
"markdown-image": "#4976eb",
"markdown-image-text": "#00acc1",
"markdown-code-block": "#292d3e"
}
},
"dark": {
"palette": {
"neutral": "#292d3e",
"ink": "#a6accd",
"primary": "#82aaff",
"accent": "#89ddff",
"success": "#c3e88d",
"warning": "#ffcb6b",
"error": "#f07178",
"info": "#f78c6c"
},
"overrides": {
"text-weak": "#676e95",
"syntax-comment": "#676e95",
"syntax-keyword": "#c792ea",
"syntax-string": "#c3e88d",
"syntax-primitive": "#82aaff",
"syntax-variable": "#a6accd",
"syntax-property": "#89ddff",
"syntax-type": "#ffcb6b",
"syntax-constant": "#f78c6c",
"syntax-operator": "#89ddff",
"syntax-punctuation": "#a6accd",
"syntax-object": "#a6accd",
"markdown-heading": "#c792ea",
"markdown-text": "#a6accd",
"markdown-link": "#82aaff",
"markdown-link-text": "#89ddff",
"markdown-code": "#c3e88d",
"markdown-block-quote": "#676e95",
"markdown-emph": "#ffcb6b",
"markdown-strong": "#f78c6c",
"markdown-horizontal-rule": "#676e95",
"markdown-list-item": "#82aaff",
"markdown-list-enumeration": "#89ddff",
"markdown-image": "#82aaff",
"markdown-image-text": "#89ddff",
"markdown-code-block": "#a6accd"
}
}
}

View File

@@ -0,0 +1,85 @@
{
"$schema": "https://opencode.ai/desktop-theme.json",
"name": "Rose Pine",
"id": "rosepine",
"light": {
"palette": {
"neutral": "#faf4ed",
"ink": "#575279",
"primary": "#31748f",
"accent": "#d7827e",
"success": "#286983",
"warning": "#ea9d34",
"error": "#b4637a",
"info": "#56949f"
},
"overrides": {
"text-weak": "#9893a5",
"syntax-comment": "#9893a5",
"syntax-keyword": "#286983",
"syntax-string": "#ea9d34",
"syntax-primitive": "#d7827e",
"syntax-variable": "#575279",
"syntax-property": "#d7827e",
"syntax-type": "#56949f",
"syntax-constant": "#907aa9",
"syntax-operator": "#797593",
"syntax-punctuation": "#797593",
"syntax-object": "#575279",
"markdown-heading": "#907aa9",
"markdown-text": "#575279",
"markdown-link": "#31748f",
"markdown-link-text": "#d7827e",
"markdown-code": "#286983",
"markdown-block-quote": "#9893a5",
"markdown-emph": "#ea9d34",
"markdown-strong": "#b4637a",
"markdown-horizontal-rule": "#dfdad9",
"markdown-list-item": "#31748f",
"markdown-list-enumeration": "#d7827e",
"markdown-image": "#31748f",
"markdown-image-text": "#d7827e",
"markdown-code-block": "#575279"
}
},
"dark": {
"palette": {
"neutral": "#191724",
"ink": "#e0def4",
"primary": "#9ccfd8",
"accent": "#ebbcba",
"success": "#31748f",
"warning": "#f6c177",
"error": "#eb6f92",
"info": "#9ccfd8"
},
"overrides": {
"text-weak": "#6e6a86",
"syntax-comment": "#6e6a86",
"syntax-keyword": "#31748f",
"syntax-string": "#f6c177",
"syntax-primitive": "#ebbcba",
"syntax-variable": "#e0def4",
"syntax-property": "#ebbcba",
"syntax-type": "#9ccfd8",
"syntax-constant": "#c4a7e7",
"syntax-operator": "#908caa",
"syntax-punctuation": "#908caa",
"syntax-object": "#e0def4",
"markdown-heading": "#c4a7e7",
"markdown-text": "#e0def4",
"markdown-link": "#9ccfd8",
"markdown-link-text": "#ebbcba",
"markdown-code": "#31748f",
"markdown-block-quote": "#6e6a86",
"markdown-emph": "#f6c177",
"markdown-strong": "#eb6f92",
"markdown-horizontal-rule": "#403d52",
"markdown-list-item": "#9ccfd8",
"markdown-list-enumeration": "#ebbcba",
"markdown-image": "#9ccfd8",
"markdown-image-text": "#ebbcba",
"markdown-code-block": "#e0def4"
}
}
}

View File

@@ -0,0 +1,87 @@
{
"$schema": "https://opencode.ai/desktop-theme.json",
"name": "Synthwave '84",
"id": "synthwave84",
"light": {
"palette": {
"neutral": "#fafafa",
"ink": "#262335",
"primary": "#00bcd4",
"accent": "#9c27b0",
"success": "#4caf50",
"warning": "#ff9800",
"error": "#f44336",
"info": "#ff5722"
},
"overrides": {
"text-weak": "#5c5c8a",
"syntax-comment": "#5c5c8a",
"syntax-keyword": "#e91e63",
"syntax-string": "#ff9800",
"syntax-primitive": "#ff5722",
"syntax-variable": "#262335",
"syntax-property": "#9c27b0",
"syntax-type": "#00bcd4",
"syntax-constant": "#9c27b0",
"syntax-operator": "#e91e63",
"syntax-punctuation": "#262335",
"syntax-object": "#262335",
"markdown-heading": "#e91e63",
"markdown-text": "#262335",
"markdown-link": "#00bcd4",
"markdown-link-text": "#9c27b0",
"markdown-code": "#4caf50",
"markdown-block-quote": "#5c5c8a",
"markdown-emph": "#ff9800",
"markdown-strong": "#ff5722",
"markdown-horizontal-rule": "#e0e0e0",
"markdown-list-item": "#00bcd4",
"markdown-list-enumeration": "#9c27b0",
"markdown-image": "#00bcd4",
"markdown-image-text": "#9c27b0",
"markdown-code-block": "#262335"
}
},
"dark": {
"palette": {
"neutral": "#262335",
"ink": "#ffffff",
"primary": "#36f9f6",
"accent": "#b084eb",
"success": "#72f1b8",
"warning": "#fede5d",
"error": "#fe4450",
"info": "#ff8b39",
"diffAdd": "#97f1d8",
"diffDelete": "#ff5e5b"
},
"overrides": {
"text-weak": "#848bbd",
"syntax-comment": "#848bbd",
"syntax-keyword": "#ff7edb",
"syntax-string": "#fede5d",
"syntax-primitive": "#ff8b39",
"syntax-variable": "#ffffff",
"syntax-property": "#b084eb",
"syntax-type": "#36f9f6",
"syntax-constant": "#b084eb",
"syntax-operator": "#ff7edb",
"syntax-punctuation": "#ffffff",
"syntax-object": "#ffffff",
"markdown-heading": "#ff7edb",
"markdown-text": "#ffffff",
"markdown-link": "#36f9f6",
"markdown-link-text": "#b084eb",
"markdown-code": "#72f1b8",
"markdown-block-quote": "#848bbd",
"markdown-emph": "#fede5d",
"markdown-strong": "#ff8b39",
"markdown-horizontal-rule": "#495495",
"markdown-list-item": "#36f9f6",
"markdown-list-enumeration": "#b084eb",
"markdown-image": "#36f9f6",
"markdown-image-text": "#b084eb",
"markdown-code-block": "#ffffff"
}
}
}

View File

@@ -0,0 +1,90 @@
{
"$schema": "https://opencode.ai/desktop-theme.json",
"name": "Vercel",
"id": "vercel",
"light": {
"palette": {
"neutral": "#FFFFFF",
"ink": "#171717",
"primary": "#0070F3",
"accent": "#8E4EC6",
"success": "#388E3C",
"warning": "#FF9500",
"error": "#DC3545",
"info": "#0070F3",
"diffAdd": "#46A758",
"diffDelete": "#E5484D"
},
"overrides": {
"text-weak": "#666666",
"syntax-comment": "#888888",
"syntax-keyword": "#E93D82",
"syntax-string": "#46A758",
"syntax-primitive": "#8E4EC6",
"syntax-variable": "#0070F3",
"syntax-property": "#12A594",
"syntax-type": "#12A594",
"syntax-constant": "#FFB224",
"syntax-operator": "#E93D82",
"syntax-punctuation": "#171717",
"syntax-object": "#0070F3",
"markdown-heading": "#8E4EC6",
"markdown-text": "#171717",
"markdown-link": "#0070F3",
"markdown-link-text": "#12A594",
"markdown-code": "#46A758",
"markdown-block-quote": "#666666",
"markdown-emph": "#FFB224",
"markdown-strong": "#E93D82",
"markdown-horizontal-rule": "#999999",
"markdown-list-item": "#171717",
"markdown-list-enumeration": "#0070F3",
"markdown-image": "#12A594",
"markdown-image-text": "#12A594",
"markdown-code-block": "#171717"
}
},
"dark": {
"palette": {
"neutral": "#000000",
"ink": "#EDEDED",
"primary": "#0070F3",
"accent": "#8E4EC6",
"success": "#46A758",
"warning": "#FFB224",
"error": "#E5484D",
"info": "#52A8FF",
"interactive": "#52A8FF",
"diffAdd": "#63C46D",
"diffDelete": "#FF6166"
},
"overrides": {
"text-weak": "#878787",
"syntax-comment": "#878787",
"syntax-keyword": "#F75590",
"syntax-string": "#63C46D",
"syntax-primitive": "#BF7AF0",
"syntax-variable": "#52A8FF",
"syntax-property": "#0AC7AC",
"syntax-type": "#0AC7AC",
"syntax-constant": "#F2A700",
"syntax-operator": "#F75590",
"syntax-punctuation": "#EDEDED",
"syntax-object": "#52A8FF",
"markdown-heading": "#BF7AF0",
"markdown-text": "#EDEDED",
"markdown-link": "#52A8FF",
"markdown-link-text": "#0AC7AC",
"markdown-code": "#63C46D",
"markdown-block-quote": "#878787",
"markdown-emph": "#F2A700",
"markdown-strong": "#F75590",
"markdown-horizontal-rule": "#454545",
"markdown-list-item": "#EDEDED",
"markdown-list-enumeration": "#52A8FF",
"markdown-image": "#0AC7AC",
"markdown-image-text": "#50E3C2",
"markdown-code-block": "#EDEDED"
}
}
}

View File

@@ -0,0 +1,87 @@
{
"$schema": "https://opencode.ai/desktop-theme.json",
"name": "Zenburn",
"id": "zenburn",
"light": {
"palette": {
"neutral": "#ffffef",
"ink": "#3f3f3f",
"primary": "#5f7f8f",
"accent": "#5f8f8f",
"success": "#5f8f5f",
"warning": "#8f8f5f",
"error": "#8f5f5f",
"info": "#8f7f5f"
},
"overrides": {
"text-weak": "#6f6f6f",
"syntax-comment": "#5f7f5f",
"syntax-keyword": "#8f8f5f",
"syntax-string": "#8f5f5f",
"syntax-primitive": "#5f7f8f",
"syntax-variable": "#3f3f3f",
"syntax-property": "#5f8f8f",
"syntax-type": "#5f8f8f",
"syntax-constant": "#5f8f5f",
"syntax-operator": "#8f8f5f",
"syntax-punctuation": "#3f3f3f",
"syntax-object": "#3f3f3f",
"markdown-heading": "#8f8f5f",
"markdown-text": "#3f3f3f",
"markdown-link": "#5f7f8f",
"markdown-link-text": "#5f8f8f",
"markdown-code": "#5f8f5f",
"markdown-block-quote": "#6f6f6f",
"markdown-emph": "#8f8f5f",
"markdown-strong": "#8f7f5f",
"markdown-horizontal-rule": "#6f6f6f",
"markdown-list-item": "#5f7f8f",
"markdown-list-enumeration": "#5f8f8f",
"markdown-image": "#5f7f8f",
"markdown-image-text": "#5f8f8f",
"markdown-code-block": "#3f3f3f"
}
},
"dark": {
"palette": {
"neutral": "#3f3f3f",
"ink": "#dcdccc",
"primary": "#8cd0d3",
"accent": "#93e0e3",
"success": "#7f9f7f",
"warning": "#f0dfaf",
"error": "#cc9393",
"info": "#dfaf8f",
"diffAdd": "#8fb28f",
"diffDelete": "#dca3a3"
},
"overrides": {
"text-weak": "#9f9f9f",
"syntax-comment": "#7f9f7f",
"syntax-keyword": "#f0dfaf",
"syntax-string": "#cc9393",
"syntax-primitive": "#8cd0d3",
"syntax-variable": "#dcdccc",
"syntax-property": "#93e0e3",
"syntax-type": "#93e0e3",
"syntax-constant": "#8fb28f",
"syntax-operator": "#f0dfaf",
"syntax-punctuation": "#dcdccc",
"syntax-object": "#dcdccc",
"markdown-heading": "#f0dfaf",
"markdown-text": "#dcdccc",
"markdown-link": "#8cd0d3",
"markdown-link-text": "#93e0e3",
"markdown-code": "#7f9f7f",
"markdown-block-quote": "#9f9f9f",
"markdown-emph": "#e0cf9f",
"markdown-strong": "#dfaf8f",
"markdown-horizontal-rule": "#9f9f9f",
"markdown-list-item": "#8cd0d3",
"markdown-list-enumeration": "#93e0e3",
"markdown-image": "#8cd0d3",
"markdown-image-text": "#93e0e3",
"markdown-code-block": "#dcdccc"
}
}
}

View File

@@ -20,7 +20,7 @@ export interface ThemeSeedColors {
export interface ThemePaletteColors {
neutral: HexColor
ink?: HexColor
ink: HexColor
primary: HexColor
success: HexColor
warning: HexColor