mirror of
https://github.com/anomalyco/opencode.git
synced 2026-03-13 10:14:26 +00:00
Compare commits
13 Commits
effect-zod
...
dev
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ff748b82ca | ||
|
|
9fafa57562 | ||
|
|
f8475649da | ||
|
|
b94e110a4c | ||
|
|
f0bba10b12 | ||
|
|
d961981e25 | ||
|
|
5576662200 | ||
|
|
4a2a046d79 | ||
|
|
8f8c74cfb8 | ||
|
|
092f654f63 | ||
|
|
96b1d8f639 | ||
|
|
dcb17c6a67 | ||
|
|
dd68b85f58 |
2
.gitignore
vendored
2
.gitignore
vendored
@@ -17,7 +17,7 @@ ts-dist
|
||||
/result
|
||||
refs
|
||||
Session.vim
|
||||
opencode.json
|
||||
/opencode.json
|
||||
a.out
|
||||
target
|
||||
.scripts
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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 = {
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
}}
|
||||
|
||||
@@ -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>
|
||||
)}
|
||||
|
||||
5
packages/app/src/components/settings-list.tsx
Normal file
5
packages/app/src/components/settings-list.tsx
Normal 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>
|
||||
}
|
||||
@@ -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>
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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")}
|
||||
|
||||
@@ -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, [])
|
||||
|
||||
@@ -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"
|
||||
|
||||
84
packages/app/src/testing/session-composer.ts
Normal file
84
packages/app/src/testing/session-composer.ts
Normal 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: [],
|
||||
})
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -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">
|
||||
|
||||
@@ -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": "تمكين الفوترة",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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": "課金を有効にする",
|
||||
|
||||
@@ -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": "결제 활성화",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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": "Включить оплату",
|
||||
|
||||
@@ -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": "เปิดใช้งานการเรียกเก็บเงิน",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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": "启用计费",
|
||||
|
||||
@@ -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": "啟用帳務",
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -195,7 +195,7 @@ export const OrgsCommand = cmd({
|
||||
|
||||
export const ConsoleCommand = cmd({
|
||||
command: "console",
|
||||
describe: "manage console account",
|
||||
describe: false,
|
||||
builder: (yargs) =>
|
||||
yargs
|
||||
.command({
|
||||
|
||||
@@ -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 })
|
||||
})
|
||||
}
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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)
|
||||
},
|
||||
|
||||
@@ -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
|
||||
|
||||
169
packages/opencode/src/provider/auth-service.ts
Normal file
169
packages/opencode/src/provider/auth-service.ts
Normal 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))
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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") {
|
||||
|
||||
@@ -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}`)
|
||||
}
|
||||
51
packages/opencode/src/util/instance-state.ts
Normal file
51
packages/opencode/src/util/instance-state.ts
Normal 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" },
|
||||
)
|
||||
}
|
||||
@@ -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) {
|
||||
|
||||
140
packages/opencode/test/project/migrate-global.test.ts
Normal file
140
packages/opencode/test/project/migrate-global.test.ts
Normal 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)
|
||||
})
|
||||
})
|
||||
115
packages/opencode/test/project/state.test.ts
Normal file
115
packages/opencode/test/project/state.test.ts
Normal 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)
|
||||
})
|
||||
20
packages/opencode/test/provider/auth.test.ts
Normal file
20
packages/opencode/test/provider/auth.test.ts
Normal 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",
|
||||
})
|
||||
})
|
||||
@@ -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,
|
||||
|
||||
@@ -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")
|
||||
})
|
||||
})
|
||||
139
packages/opencode/test/util/instance-state.test.ts
Normal file
139
packages/opencode/test/util/instance-state.test.ts
Normal 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)
|
||||
}),
|
||||
),
|
||||
)
|
||||
})
|
||||
@@ -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++) {
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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))
|
||||
|
||||
85
packages/ui/src/theme/themes/catppuccin-frappe.json
Normal file
85
packages/ui/src/theme/themes/catppuccin-frappe.json
Normal 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"
|
||||
}
|
||||
}
|
||||
}
|
||||
85
packages/ui/src/theme/themes/catppuccin-macchiato.json
Normal file
85
packages/ui/src/theme/themes/catppuccin-macchiato.json
Normal 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"
|
||||
}
|
||||
}
|
||||
}
|
||||
87
packages/ui/src/theme/themes/cobalt2.json
Normal file
87
packages/ui/src/theme/themes/cobalt2.json
Normal 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"
|
||||
}
|
||||
}
|
||||
}
|
||||
91
packages/ui/src/theme/themes/cursor.json
Normal file
91
packages/ui/src/theme/themes/cursor.json
Normal 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"
|
||||
}
|
||||
}
|
||||
}
|
||||
89
packages/ui/src/theme/themes/everforest.json
Normal file
89
packages/ui/src/theme/themes/everforest.json
Normal 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"
|
||||
}
|
||||
}
|
||||
}
|
||||
86
packages/ui/src/theme/themes/flexoki.json
Normal file
86
packages/ui/src/theme/themes/flexoki.json
Normal 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"
|
||||
}
|
||||
}
|
||||
}
|
||||
85
packages/ui/src/theme/themes/github.json
Normal file
85
packages/ui/src/theme/themes/github.json
Normal 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"
|
||||
}
|
||||
}
|
||||
}
|
||||
89
packages/ui/src/theme/themes/kanagawa.json
Normal file
89
packages/ui/src/theme/themes/kanagawa.json
Normal 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"
|
||||
}
|
||||
}
|
||||
}
|
||||
87
packages/ui/src/theme/themes/lucent-orng.json
Normal file
87
packages/ui/src/theme/themes/lucent-orng.json
Normal 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"
|
||||
}
|
||||
}
|
||||
}
|
||||
87
packages/ui/src/theme/themes/material.json
Normal file
87
packages/ui/src/theme/themes/material.json
Normal 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"
|
||||
}
|
||||
}
|
||||
}
|
||||
91
packages/ui/src/theme/themes/matrix.json
Normal file
91
packages/ui/src/theme/themes/matrix.json
Normal 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"
|
||||
}
|
||||
}
|
||||
}
|
||||
86
packages/ui/src/theme/themes/mercury.json
Normal file
86
packages/ui/src/theme/themes/mercury.json
Normal 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"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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",
|
||||
|
||||
89
packages/ui/src/theme/themes/one-dark.json
Normal file
89
packages/ui/src/theme/themes/one-dark.json
Normal 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"
|
||||
}
|
||||
}
|
||||
}
|
||||
89
packages/ui/src/theme/themes/opencode.json
Normal file
89
packages/ui/src/theme/themes/opencode.json
Normal 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"
|
||||
}
|
||||
}
|
||||
}
|
||||
87
packages/ui/src/theme/themes/orng.json
Normal file
87
packages/ui/src/theme/themes/orng.json
Normal 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"
|
||||
}
|
||||
}
|
||||
}
|
||||
88
packages/ui/src/theme/themes/osaka-jade.json
Normal file
88
packages/ui/src/theme/themes/osaka-jade.json
Normal 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"
|
||||
}
|
||||
}
|
||||
}
|
||||
85
packages/ui/src/theme/themes/palenight.json
Normal file
85
packages/ui/src/theme/themes/palenight.json
Normal 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"
|
||||
}
|
||||
}
|
||||
}
|
||||
85
packages/ui/src/theme/themes/rosepine.json
Normal file
85
packages/ui/src/theme/themes/rosepine.json
Normal 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"
|
||||
}
|
||||
}
|
||||
}
|
||||
87
packages/ui/src/theme/themes/synthwave84.json
Normal file
87
packages/ui/src/theme/themes/synthwave84.json
Normal 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"
|
||||
}
|
||||
}
|
||||
}
|
||||
90
packages/ui/src/theme/themes/vercel.json
Normal file
90
packages/ui/src/theme/themes/vercel.json
Normal 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"
|
||||
}
|
||||
}
|
||||
}
|
||||
87
packages/ui/src/theme/themes/zenburn.json
Normal file
87
packages/ui/src/theme/themes/zenburn.json
Normal 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"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -20,7 +20,7 @@ export interface ThemeSeedColors {
|
||||
|
||||
export interface ThemePaletteColors {
|
||||
neutral: HexColor
|
||||
ink?: HexColor
|
||||
ink: HexColor
|
||||
primary: HexColor
|
||||
success: HexColor
|
||||
warning: HexColor
|
||||
|
||||
Reference in New Issue
Block a user