diff --git a/nix/scripts/canonicalize-node-modules.ts b/nix/scripts/canonicalize-node-modules.ts index faa6f63402..7997a3cd23 100644 --- a/nix/scripts/canonicalize-node-modules.ts +++ b/nix/scripts/canonicalize-node-modules.ts @@ -1,27 +1,32 @@ import { lstat, mkdir, readdir, rm, symlink } from "fs/promises" import { join, relative } from "path" -type SemverLike = { - valid: (value: string) => string | null - rcompare: (left: string, right: string) => number -} - type Entry = { dir: string version: string - label: string } +async function isDirectory(path: string) { + try { + const info = await lstat(path) + return info.isDirectory() + } catch { + return false + } +} + +const isValidSemver = (v: string) => Bun.semver.satisfies(v, "x.x.x") + const root = process.cwd() const bunRoot = join(root, "node_modules/.bun") const linkRoot = join(bunRoot, "node_modules") const directories = (await readdir(bunRoot)).sort() + const versions = new Map() for (const entry of directories) { const full = join(bunRoot, entry) - const info = await lstat(full) - if (!info.isDirectory()) { + if (!(await isDirectory(full))) { continue } const parsed = parseEntry(entry) @@ -29,37 +34,23 @@ for (const entry of directories) { continue } const list = versions.get(parsed.name) ?? [] - list.push({ dir: full, version: parsed.version, label: entry }) + list.push({ dir: full, version: parsed.version }) versions.set(parsed.name, list) } -const semverModule = (await import(join(bunRoot, "node_modules/semver"))) as - | SemverLike - | { - default: SemverLike - } -const semver = "default" in semverModule ? semverModule.default : semverModule const selections = new Map() for (const [slug, list] of versions) { list.sort((a, b) => { - const left = semver.valid(a.version) - const right = semver.valid(b.version) - if (left && right) { - const delta = semver.rcompare(left, right) - if (delta !== 0) { - return delta - } - } - if (left && !right) { - return -1 - } - if (!left && right) { - return 1 - } + const aValid = isValidSemver(a.version) + const bValid = isValidSemver(b.version) + if (aValid && bValid) return -Bun.semver.order(a.version, b.version) + if (aValid) return -1 + if (bValid) return 1 return b.version.localeCompare(a.version) }) - selections.set(slug, list[0]) + const first = list[0] + if (first) selections.set(slug, first) } await rm(linkRoot, { recursive: true, force: true }) @@ -77,10 +68,7 @@ for (const [slug, entry] of Array.from(selections.entries()).sort((a, b) => a[0] await mkdir(parent, { recursive: true }) const linkPath = join(parent, leaf) const desired = join(entry.dir, "node_modules", slug) - const exists = await lstat(desired) - .then((info) => info.isDirectory()) - .catch(() => false) - if (!exists) { + if (!(await isDirectory(desired))) { continue } const relativeTarget = relative(parent, desired) diff --git a/nix/scripts/normalize-bun-binaries.ts b/nix/scripts/normalize-bun-binaries.ts index 531d8fd056..978ab325b7 100644 --- a/nix/scripts/normalize-bun-binaries.ts +++ b/nix/scripts/normalize-bun-binaries.ts @@ -8,7 +8,7 @@ type PackageManifest = { const root = process.cwd() const bunRoot = join(root, "node_modules/.bun") -const bunEntries = (await safeReadDir(bunRoot)).sort() +const bunEntries = (await readdir(bunRoot)).sort() let rewritten = 0 for (const entry of bunEntries) { @@ -45,11 +45,11 @@ for (const entry of bunEntries) { } } -console.log(`[normalize-bun-binaries] rewrote ${rewritten} links`) +console.log(`[normalize-bun-binaries] rebuilt ${rewritten} links`) async function collectPackages(modulesRoot: string) { const found: string[] = [] - const topLevel = (await safeReadDir(modulesRoot)).sort() + const topLevel = (await readdir(modulesRoot)).sort() for (const name of topLevel) { if (name === ".bin" || name === ".bun") { continue @@ -59,7 +59,7 @@ async function collectPackages(modulesRoot: string) { continue } if (name.startsWith("@")) { - const scoped = (await safeReadDir(full)).sort() + const scoped = (await readdir(full)).sort() for (const child of scoped) { const scopedDir = join(full, child) if (await isDirectory(scopedDir)) { @@ -121,14 +121,6 @@ async function isDirectory(path: string) { } } -async function safeReadDir(path: string) { - try { - return await readdir(path) - } catch { - return [] - } -} - function normalizeBinName(name: string) { const slash = name.lastIndexOf("/") if (slash >= 0) { diff --git a/packages/app/src/app.tsx b/packages/app/src/app.tsx index 8a111472ba..5bbe86e209 100644 --- a/packages/app/src/app.tsx +++ b/packages/app/src/app.tsx @@ -84,7 +84,7 @@ function ServerKey(props: ParentProps) { ) } -export function AppInterface(props: { defaultUrl?: string; children?: JSX.Element }) { +export function AppInterface(props: { defaultUrl?: string; children?: JSX.Element; isSidecar?: boolean }) { const platform = usePlatform() const stored = (() => { @@ -106,7 +106,7 @@ export function AppInterface(props: { defaultUrl?: string; children?: JSX.Elemen } return ( - + diff --git a/packages/app/src/components/prompt-input/attachments.ts b/packages/app/src/components/prompt-input/attachments.ts index b384bf7d84..0f778b5181 100644 --- a/packages/app/src/components/prompt-input/attachments.ts +++ b/packages/app/src/components/prompt-input/attachments.ts @@ -31,7 +31,7 @@ export function createPromptAttachments(input: PromptAttachmentsInput) { const dataUrl = reader.result as string const attachment: ImageAttachmentPart = { type: "image", - id: crypto.randomUUID(), + id: crypto.randomUUID?.() ?? Math.random().toString(16).slice(2), filename: file.name, mime: file.type, dataUrl, diff --git a/packages/app/src/components/session/session-header.tsx b/packages/app/src/components/session/session-header.tsx index fcba47004b..1735a42341 100644 --- a/packages/app/src/components/session/session-header.tsx +++ b/packages/app/src/components/session/session-header.tsx @@ -283,7 +283,7 @@ export function SessionHeader() { )} @@ -303,6 +307,7 @@ export function SessionHeader() { {(mount) => (
+ -
{ checking: false, }) + const linux = createMemo(() => platform.platform === "desktop" && platform.os === "linux") + const check = () => { if (!platform.checkUpdate) return setStore("checking", true) @@ -410,13 +414,49 @@ export const SettingsGeneral: Component = () => {
+ + + {(_) => { + const [valueResource, actions] = createResource(() => platform.getDisplayBackend?.()) + const value = () => (valueResource.state === "pending" ? undefined : valueResource.latest) + + const onChange = (checked: boolean) => + platform.setDisplayBackend?.(checked ? "wayland" : "auto").finally(() => actions.refetch()) + + return ( +
+

{language.t("settings.general.section.display")}

+ +
+ + {language.t("settings.general.row.wayland.title")} + + + + + +
+ } + description={language.t("settings.general.row.wayland.description")} + > +
+ +
+ +
+ + ) + }} +
) } interface SettingsRowProps { - title: string + title: string | JSX.Element description: string | JSX.Element children: JSX.Element } diff --git a/packages/app/src/components/status-popover.tsx b/packages/app/src/components/status-popover.tsx index 3354c3d362..6e89990178 100644 --- a/packages/app/src/components/status-popover.tsx +++ b/packages/app/src/components/status-popover.tsx @@ -141,7 +141,7 @@ export function StatusPopover() { triggerProps={{ variant: "ghost", class: - "rounded-sm w-[75px] h-[24px] py-1.5 pr-3 pl-2 gap-2 border-none shadow-none data-[expanded]:bg-surface-raised-base-active", + "rounded-md h-[24px] px-3 gap-2 border border-border-base bg-surface-panel shadow-none data-[expanded]:bg-surface-raised-base-active", style: { scale: 1 }, }} trigger={ diff --git a/packages/app/src/context/comments.tsx b/packages/app/src/context/comments.tsx index d43f3705be..b91f029bc8 100644 --- a/packages/app/src/context/comments.tsx +++ b/packages/app/src/context/comments.tsx @@ -53,7 +53,7 @@ function createCommentSessionState(store: Store, setStore: SetStor const add = (input: Omit) => { const next: LineComment = { - id: crypto.randomUUID(), + id: crypto.randomUUID?.() ?? Math.random().toString(16).slice(2), time: Date.now(), ...input, } diff --git a/packages/app/src/context/file.tsx b/packages/app/src/context/file.tsx index 996ea2aafe..88b70cd41d 100644 --- a/packages/app/src/context/file.tsx +++ b/packages/app/src/context/file.tsx @@ -7,6 +7,7 @@ import { getFilename } from "@opencode-ai/util/path" import { useSDK } from "./sdk" import { useSync } from "./sync" import { useLanguage } from "@/context/language" +import { useLayout } from "@/context/layout" import { createPathHelpers } from "./file/path" import { approxBytes, @@ -50,9 +51,11 @@ export const { use: useFile, provider: FileProvider } = createSimpleContext({ useSync() const params = useParams() const language = useLanguage() + const layout = useLayout() const scope = createMemo(() => sdk.directory) const path = createPathHelpers(scope) + const tabs = layout.tabs(() => `${params.dir}${params.id ? "/" + params.id : ""}`) const inflight = new Map>() const [store, setStore] = createStore<{ @@ -183,6 +186,7 @@ export const { use: useFile, provider: FileProvider } = createSimpleContext({ invalidateFromWatcher(e.details, { normalize: path.normalize, hasFile: (file) => Boolean(store.file[file]), + isOpen: (file) => tabs.all().some((tab) => path.pathFromTab(tab) === file), loadFile: (file) => { void load(file, { force: true }) }, diff --git a/packages/app/src/context/file/watcher.test.ts b/packages/app/src/context/file/watcher.test.ts index 653e0aa752..9536b52536 100644 --- a/packages/app/src/context/file/watcher.test.ts +++ b/packages/app/src/context/file/watcher.test.ts @@ -27,6 +27,37 @@ describe("file watcher invalidation", () => { expect(refresh).toEqual(["src"]) }) + test("reloads files that are open in tabs", () => { + const loads: string[] = [] + + invalidateFromWatcher( + { + type: "file.watcher.updated", + properties: { + file: "src/open.ts", + event: "change", + }, + }, + { + normalize: (input) => input, + hasFile: () => false, + isOpen: (path) => path === "src/open.ts", + loadFile: (path) => loads.push(path), + node: () => ({ + path: "src/open.ts", + type: "file", + name: "open.ts", + absolute: "/repo/src/open.ts", + ignored: false, + }), + isDirLoaded: () => false, + refreshDir: () => {}, + }, + ) + + expect(loads).toEqual(["src/open.ts"]) + }) + test("refreshes only changed loaded directory nodes", () => { const refresh: string[] = [] diff --git a/packages/app/src/context/file/watcher.ts b/packages/app/src/context/file/watcher.ts index a3a98eae4b..fbf7199279 100644 --- a/packages/app/src/context/file/watcher.ts +++ b/packages/app/src/context/file/watcher.ts @@ -8,6 +8,7 @@ type WatcherEvent = { type WatcherOps = { normalize: (input: string) => string hasFile: (path: string) => boolean + isOpen?: (path: string) => boolean loadFile: (path: string) => void node: (path: string) => FileNode | undefined isDirLoaded: (path: string) => boolean @@ -27,7 +28,7 @@ export function invalidateFromWatcher(event: WatcherEvent, ops: WatcherOps) { if (!path) return if (path.startsWith(".git/")) return - if (ops.hasFile(path)) { + if (ops.hasFile(path) || ops.isOpen?.(path)) { ops.loadFile(path) } diff --git a/packages/app/src/context/platform.tsx b/packages/app/src/context/platform.tsx index 3fca502bad..7aa6c65540 100644 --- a/packages/app/src/context/platform.tsx +++ b/packages/app/src/context/platform.tsx @@ -57,6 +57,12 @@ export type Platform = { /** Set the default server URL to use on app startup (platform-specific) */ setDefaultServerUrl?(url: string | null): Promise | void + /** Get the preferred display backend (desktop only) */ + getDisplayBackend?(): Promise | DisplayBackend | null + + /** Set the preferred display backend (desktop only) */ + setDisplayBackend?(backend: DisplayBackend): Promise + /** Parse markdown to HTML using native parser (desktop only, returns unprocessed code blocks) */ parseMarkdown?(markdown: string): Promise @@ -70,6 +76,8 @@ export type Platform = { readClipboardImage?(): Promise } +export type DisplayBackend = "auto" | "wayland" + export const { use: usePlatform, provider: PlatformProvider } = createSimpleContext({ name: "Platform", init: (props: { value: Platform }) => { diff --git a/packages/app/src/context/server.tsx b/packages/app/src/context/server.tsx index 72693e6ef6..351407d91b 100644 --- a/packages/app/src/context/server.tsx +++ b/packages/app/src/context/server.tsx @@ -28,13 +28,14 @@ function projectsKey(url: string) { export const { use: useServer, provider: ServerProvider } = createSimpleContext({ name: "Server", - init: (props: { defaultUrl: string }) => { + init: (props: { defaultUrl: string; isSidecar?: boolean }) => { const platform = usePlatform() const [store, setStore, _, ready] = persisted( Persist.global("server", ["server.v3"]), createStore({ list: [] as string[], + currentSidecarUrl: "", projects: {} as Record, lastProject: {} as Record, }), @@ -59,7 +60,13 @@ export const { use: useServer, provider: ServerProvider } = createSimpleContext( const fallback = normalizeServerUrl(props.defaultUrl) if (fallback && url === fallback) { - setState("active", url) + batch(() => { + if (!store.list.includes(url)) { + // Add the fallback url to the list if it's not already in the list + setStore("list", store.list.length, url) + } + setState("active", url) + }) return } @@ -89,7 +96,20 @@ export const { use: useServer, provider: ServerProvider } = createSimpleContext( if (state.active) return const url = normalizeServerUrl(props.defaultUrl) if (!url) return - setState("active", url) + batch(() => { + // Remove the previous startup sidecar url + if (store.currentSidecarUrl) { + remove(store.currentSidecarUrl) + } + + // Add the new sidecar url + if (props.isSidecar && props.defaultUrl) { + add(props.defaultUrl) + setStore("currentSidecarUrl", props.defaultUrl) + } + + setState("active", url) + }) }) const isReady = createMemo(() => ready() && !!state.active) diff --git a/packages/app/src/i18n/ar.ts b/packages/app/src/i18n/ar.ts index 3778adcd67..201d63660a 100644 --- a/packages/app/src/i18n/ar.ts +++ b/packages/app/src/i18n/ar.ts @@ -44,6 +44,7 @@ export const dict = { "command.session.new": "جلسة جديدة", "command.file.open": "فتح ملف", + "command.tab.close": "إغلاق علامة التبويب", "command.context.addSelection": "إضافة التحديد إلى السياق", "command.context.addSelection.description": "إضافة الأسطر المحددة من الملف الحالي", "command.input.focus": "التركيز على حقل الإدخال", diff --git a/packages/app/src/i18n/br.ts b/packages/app/src/i18n/br.ts index 74bfd8707c..b7f2d74857 100644 --- a/packages/app/src/i18n/br.ts +++ b/packages/app/src/i18n/br.ts @@ -44,6 +44,7 @@ export const dict = { "command.session.new": "Nova sessão", "command.file.open": "Abrir arquivo", + "command.tab.close": "Fechar aba", "command.context.addSelection": "Adicionar seleção ao contexto", "command.context.addSelection.description": "Adicionar as linhas selecionadas do arquivo atual", "command.input.focus": "Focar entrada", diff --git a/packages/app/src/i18n/da.ts b/packages/app/src/i18n/da.ts index 7242fb5849..8ea4907c1b 100644 --- a/packages/app/src/i18n/da.ts +++ b/packages/app/src/i18n/da.ts @@ -44,6 +44,7 @@ export const dict = { "command.session.new": "Ny session", "command.file.open": "Åbn fil", + "command.tab.close": "Luk fane", "command.context.addSelection": "Tilføj markering til kontekst", "command.context.addSelection.description": "Tilføj markerede linjer fra den aktuelle fil", "command.input.focus": "Fokuser inputfelt", diff --git a/packages/app/src/i18n/de.ts b/packages/app/src/i18n/de.ts index bd8acae5e8..a4884a1033 100644 --- a/packages/app/src/i18n/de.ts +++ b/packages/app/src/i18n/de.ts @@ -48,6 +48,7 @@ export const dict = { "command.session.new": "Neue Sitzung", "command.file.open": "Datei öffnen", + "command.tab.close": "Tab schließen", "command.context.addSelection": "Auswahl zum Kontext hinzufügen", "command.context.addSelection.description": "Ausgewählte Zeilen aus der aktuellen Datei hinzufügen", "command.input.focus": "Eingabefeld fokussieren", diff --git a/packages/app/src/i18n/en.ts b/packages/app/src/i18n/en.ts index 8fba6861b0..f4f49f055b 100644 --- a/packages/app/src/i18n/en.ts +++ b/packages/app/src/i18n/en.ts @@ -588,6 +588,7 @@ export const dict = { "settings.general.section.notifications": "System notifications", "settings.general.section.updates": "Updates", "settings.general.section.sounds": "Sound effects", + "settings.general.section.display": "Display", "settings.general.row.language.title": "Language", "settings.general.row.language.description": "Change the display language for OpenCode", @@ -598,6 +599,11 @@ export const dict = { "settings.general.row.font.title": "Font", "settings.general.row.font.description": "Customise the mono font used in code blocks", + "settings.general.row.wayland.title": "Use native Wayland", + "settings.general.row.wayland.description": "Disable X11 fallback on Wayland. Requires restart.", + "settings.general.row.wayland.tooltip": + "On Linux with mixed refresh-rate monitors, native Wayland can be more stable.", + "settings.general.row.releaseNotes.title": "Release notes", "settings.general.row.releaseNotes.description": "Show What's New popups after updates", diff --git a/packages/app/src/i18n/es.ts b/packages/app/src/i18n/es.ts index f9b11ade87..50d9060703 100644 --- a/packages/app/src/i18n/es.ts +++ b/packages/app/src/i18n/es.ts @@ -44,6 +44,7 @@ export const dict = { "command.session.new": "Nueva sesión", "command.file.open": "Abrir archivo", + "command.tab.close": "Cerrar pestaña", "command.context.addSelection": "Añadir selección al contexto", "command.context.addSelection.description": "Añadir las líneas seleccionadas del archivo actual", "command.input.focus": "Enfocar entrada", diff --git a/packages/app/src/i18n/fr.ts b/packages/app/src/i18n/fr.ts index 0cc81e5ea7..7ad39f3406 100644 --- a/packages/app/src/i18n/fr.ts +++ b/packages/app/src/i18n/fr.ts @@ -44,6 +44,7 @@ export const dict = { "command.session.new": "Nouvelle session", "command.file.open": "Ouvrir un fichier", + "command.tab.close": "Fermer l'onglet", "command.context.addSelection": "Ajouter la sélection au contexte", "command.context.addSelection.description": "Ajouter les lignes sélectionnées du fichier actuel", "command.input.focus": "Focus input", diff --git a/packages/app/src/i18n/ja.ts b/packages/app/src/i18n/ja.ts index 337e1b0d34..a39bfbaf33 100644 --- a/packages/app/src/i18n/ja.ts +++ b/packages/app/src/i18n/ja.ts @@ -44,6 +44,7 @@ export const dict = { "command.session.new": "新しいセッション", "command.file.open": "ファイルを開く", + "command.tab.close": "タブを閉じる", "command.context.addSelection": "選択範囲をコンテキストに追加", "command.context.addSelection.description": "現在のファイルから選択した行を追加", "command.input.focus": "入力欄にフォーカス", diff --git a/packages/app/src/i18n/ko.ts b/packages/app/src/i18n/ko.ts index 283bb6f3bd..b5927b2107 100644 --- a/packages/app/src/i18n/ko.ts +++ b/packages/app/src/i18n/ko.ts @@ -48,6 +48,7 @@ export const dict = { "command.session.new": "새 세션", "command.file.open": "파일 열기", + "command.tab.close": "탭 닫기", "command.context.addSelection": "선택 영역을 컨텍스트에 추가", "command.context.addSelection.description": "현재 파일에서 선택한 줄을 추가", "command.input.focus": "입력창 포커스", diff --git a/packages/app/src/i18n/no.ts b/packages/app/src/i18n/no.ts index bbffd0083d..7d8cdd27f3 100644 --- a/packages/app/src/i18n/no.ts +++ b/packages/app/src/i18n/no.ts @@ -47,6 +47,7 @@ export const dict = { "command.session.new": "Ny sesjon", "command.file.open": "Åpne fil", + "command.tab.close": "Lukk fane", "command.context.addSelection": "Legg til markering i kontekst", "command.context.addSelection.description": "Legg til valgte linjer fra gjeldende fil", "command.input.focus": "Fokuser inndata", diff --git a/packages/app/src/i18n/pl.ts b/packages/app/src/i18n/pl.ts index 2d36ca8c18..76a47ea26f 100644 --- a/packages/app/src/i18n/pl.ts +++ b/packages/app/src/i18n/pl.ts @@ -44,6 +44,7 @@ export const dict = { "command.session.new": "Nowa sesja", "command.file.open": "Otwórz plik", + "command.tab.close": "Zamknij kartę", "command.context.addSelection": "Dodaj zaznaczenie do kontekstu", "command.context.addSelection.description": "Dodaj zaznaczone linie z bieżącego pliku", "command.input.focus": "Fokus na pole wejściowe", diff --git a/packages/app/src/i18n/ru.ts b/packages/app/src/i18n/ru.ts index 18b0ba5f47..e83ce37618 100644 --- a/packages/app/src/i18n/ru.ts +++ b/packages/app/src/i18n/ru.ts @@ -44,6 +44,7 @@ export const dict = { "command.session.new": "Новая сессия", "command.file.open": "Открыть файл", + "command.tab.close": "Закрыть вкладку", "command.context.addSelection": "Добавить выделение в контекст", "command.context.addSelection.description": "Добавить выбранные строки из текущего файла", "command.input.focus": "Фокус на поле ввода", diff --git a/packages/app/src/i18n/th.ts b/packages/app/src/i18n/th.ts index d48a7cea66..2be19d15b1 100644 --- a/packages/app/src/i18n/th.ts +++ b/packages/app/src/i18n/th.ts @@ -44,6 +44,7 @@ export const dict = { "command.session.new": "เซสชันใหม่", "command.file.open": "เปิดไฟล์", + "command.tab.close": "ปิดแท็บ", "command.context.addSelection": "เพิ่มส่วนที่เลือกไปยังบริบท", "command.context.addSelection.description": "เพิ่มบรรทัดที่เลือกจากไฟล์ปัจจุบัน", "command.input.focus": "โฟกัสช่องป้อนข้อมูล", diff --git a/packages/app/src/i18n/zh.ts b/packages/app/src/i18n/zh.ts index 070064d1c4..a48f9e5494 100644 --- a/packages/app/src/i18n/zh.ts +++ b/packages/app/src/i18n/zh.ts @@ -48,6 +48,7 @@ export const dict = { "command.session.new": "新建会话", "command.file.open": "打开文件", + "command.tab.close": "关闭标签页", "command.context.addSelection": "将所选内容添加到上下文", "command.context.addSelection.description": "添加当前文件中选中的行", "command.input.focus": "聚焦输入框", diff --git a/packages/app/src/i18n/zht.ts b/packages/app/src/i18n/zht.ts index 39dcd92e27..60363fc99e 100644 --- a/packages/app/src/i18n/zht.ts +++ b/packages/app/src/i18n/zht.ts @@ -48,6 +48,7 @@ export const dict = { "command.session.new": "新增工作階段", "command.file.open": "開啟檔案", + "command.tab.close": "關閉分頁", "command.context.addSelection": "將選取內容加入上下文", "command.context.addSelection.description": "加入目前檔案中選取的行", "command.input.focus": "聚焦輸入框", diff --git a/packages/app/src/index.ts b/packages/app/src/index.ts index fb66820092..59e1431fa8 100644 --- a/packages/app/src/index.ts +++ b/packages/app/src/index.ts @@ -1,3 +1,3 @@ -export { PlatformProvider, type Platform } from "./context/platform" +export { PlatformProvider, type Platform, type DisplayBackend } from "./context/platform" export { AppBaseProviders, AppInterface } from "./app" export { useCommand } from "./context/command" diff --git a/packages/app/src/pages/layout/sidebar-items.tsx b/packages/app/src/pages/layout/sidebar-items.tsx index facfbddc7f..b184c8bff8 100644 --- a/packages/app/src/pages/layout/sidebar-items.tsx +++ b/packages/app/src/pages/layout/sidebar-items.tsx @@ -21,8 +21,11 @@ const OPENCODE_PROJECT_ID = "4b0ea68d7af9a6031a7ffda7ad66e0cb83315750" export const ProjectIcon = (props: { project: LocalProject; class?: string; notify?: boolean }): JSX.Element => { const notification = useNotification() - const unseenCount = createMemo(() => notification.project.unseenCount(props.project.worktree)) - const hasError = createMemo(() => notification.project.unseenHasError(props.project.worktree)) + const dirs = createMemo(() => [props.project.worktree, ...(props.project.sandboxes ?? [])]) + const unseenCount = createMemo(() => + dirs().reduce((total, directory) => total + notification.project.unseenCount(directory), 0), + ) + const hasError = createMemo(() => dirs().some((directory) => notification.project.unseenHasError(directory))) const name = createMemo(() => props.project.name || getFilename(props.project.worktree)) return (
diff --git a/packages/app/src/pages/layout/sidebar-workspace.tsx b/packages/app/src/pages/layout/sidebar-workspace.tsx index d4ada4f850..a7a33f25e1 100644 --- a/packages/app/src/pages/layout/sidebar-workspace.tsx +++ b/packages/app/src/pages/layout/sidebar-workspace.tsx @@ -2,6 +2,7 @@ import { useNavigate, useParams } from "@solidjs/router" import { createEffect, createMemo, For, Show, type Accessor, type JSX } from "solid-js" import { createStore } from "solid-js/store" import { createSortable } from "@thisbeyond/solid-dnd" +import { createMediaQuery } from "@solid-primitives/media" import { base64Encode } from "@opencode-ai/util/encode" import { getFilename } from "@opencode-ai/util/path" import { Button } from "@opencode-ai/ui/button" @@ -114,7 +115,8 @@ export const SortableWorkspace = (props: { const busy = createMemo(() => props.ctx.isBusy(props.directory)) const wasBusy = createMemo((prev) => prev || busy(), false) const loading = createMemo(() => open() && !booted() && sessions().length === 0 && !wasBusy()) - const showNew = createMemo(() => !loading() && (sessions().length === 0 || (active() && !params.id))) + const touch = createMediaQuery("(hover: none)") + const showNew = createMemo(() => !loading() && (touch() || sessions().length === 0 || (active() && !params.id))) const loadMore = async () => { setWorkspaceStore("limit", (limit) => limit + 5) await globalSync.project.loadSessions(props.directory) @@ -270,23 +272,25 @@ export const SortableWorkspace = (props: { - - { - event.preventDefault() - event.stopPropagation() - props.ctx.setHoverSession(undefined) - props.ctx.clearHoverProjectSoon() - navigate(`/${slug()}/session`) - }} - /> - + + + { + event.preventDefault() + event.stopPropagation() + props.ctx.setHoverSession(undefined) + props.ctx.clearHoverProjectSoon() + navigate(`/${slug()}/session`) + }} + /> + +
diff --git a/packages/desktop/src-tauri/Cargo.toml b/packages/desktop/src-tauri/Cargo.toml index 62fc7979c7..2d6f13eca0 100644 --- a/packages/desktop/src-tauri/Cargo.toml +++ b/packages/desktop/src-tauri/Cargo.toml @@ -18,7 +18,7 @@ crate-type = ["staticlib", "cdylib", "rlib"] tauri-build = { version = "2", features = [] } [dependencies] -tauri = { version = "2.9.5", features = ["macos-private-api", "devtools"] } +tauri = { version = "2.9.5", features = ["macos-private-api"] } tauri-plugin-opener = "2" tauri-plugin-deep-link = "2.4.6" tauri-plugin-shell = "2" @@ -56,6 +56,7 @@ webkit2gtk = "=2.0.2" objc2 = "0.6" objc2-web-kit = "0.3" + [target.'cfg(windows)'.dependencies] windows = { version = "0.61", features = [ "Win32_Foundation", diff --git a/packages/desktop/src-tauri/src/lib.rs b/packages/desktop/src-tauri/src/lib.rs index 14105e5dd3..92eead7867 100644 --- a/packages/desktop/src-tauri/src/lib.rs +++ b/packages/desktop/src-tauri/src/lib.rs @@ -2,6 +2,8 @@ mod cli; mod constants; #[cfg(windows)] mod job_object; +#[cfg(target_os = "linux")] +mod linux_display; mod markdown; mod server; mod window_customizer; @@ -194,6 +196,43 @@ fn check_macos_app(app_name: &str) -> bool { .unwrap_or(false) } +#[derive(serde::Serialize, serde::Deserialize, specta::Type)] +#[serde(rename_all = "camelCase")] +pub enum LinuxDisplayBackend { + Wayland, + Auto, +} + +#[tauri::command] +#[specta::specta] +fn get_display_backend() -> Option { + #[cfg(target_os = "linux")] + { + let prefer = linux_display::read_wayland().unwrap_or(false); + return Some(if prefer { + LinuxDisplayBackend::Wayland + } else { + LinuxDisplayBackend::Auto + }); + } + + #[cfg(not(target_os = "linux"))] + None +} + +#[tauri::command] +#[specta::specta] +fn set_display_backend(_app: AppHandle, _backend: LinuxDisplayBackend) -> Result<(), String> { + #[cfg(target_os = "linux")] + { + let prefer = matches!(_backend, LinuxDisplayBackend::Wayland); + return linux_display::write_wayland(&_app, prefer); + } + + #[cfg(not(target_os = "linux"))] + Ok(()) +} + #[cfg(target_os = "linux")] fn check_linux_app(app_name: &str) -> bool { return true; @@ -209,6 +248,8 @@ pub fn run() { await_initialization, server::get_default_server_url, server::set_default_server_url, + get_display_backend, + set_display_backend, markdown::parse_markdown_command, check_app_exists ]) diff --git a/packages/desktop/src-tauri/src/linux_display.rs b/packages/desktop/src-tauri/src/linux_display.rs new file mode 100644 index 0000000000..9e1cf90918 --- /dev/null +++ b/packages/desktop/src-tauri/src/linux_display.rs @@ -0,0 +1,47 @@ +use serde::{Deserialize, Serialize}; +use serde_json::json; +use std::path::PathBuf; +use tauri::AppHandle; +use tauri_plugin_store::StoreExt; + +use crate::constants::SETTINGS_STORE; + +pub const LINUX_DISPLAY_CONFIG_KEY: &str = "linuxDisplayConfig"; + +#[derive(Default, Serialize, Deserialize)] +struct DisplayConfig { + wayland: Option, +} + +fn dir() -> Option { + Some(dirs::data_dir()?.join("ai.opencode.desktop")) +} + +fn path() -> Option { + dir().map(|dir| dir.join(SETTINGS_STORE)) +} + +pub fn read_wayland() -> Option { + let path = path()?; + let raw = std::fs::read_to_string(path).ok()?; + let config = serde_json::from_str::(&raw).ok()?; + config.wayland +} + +pub fn write_wayland(app: &AppHandle, value: bool) -> Result<(), String> { + let store = app + .store(SETTINGS_STORE) + .map_err(|e| format!("Failed to open settings store: {}", e))?; + + store.set( + LINUX_DISPLAY_CONFIG_KEY, + json!(DisplayConfig { + wayland: Some(value), + }), + ); + store + .save() + .map_err(|e| format!("Failed to save settings store: {}", e))?; + + Ok(()) +} diff --git a/packages/desktop/src-tauri/src/main.rs b/packages/desktop/src-tauri/src/main.rs index 9ffee8aa5c..a95c62578c 100644 --- a/packages/desktop/src-tauri/src/main.rs +++ b/packages/desktop/src-tauri/src/main.rs @@ -2,6 +2,9 @@ #![cfg_attr(not(debug_assertions), windows_subsystem = "windows")] // borrowed from https://github.com/skyline69/balatro-mod-manager +#[cfg(target_os = "linux")] +mod display; + #[cfg(target_os = "linux")] fn configure_display_backend() -> Option { use std::env; @@ -23,12 +26,16 @@ fn configure_display_backend() -> Option { return None; } - // Allow users to explicitly keep Wayland if they know their setup is stable. - let allow_wayland = matches!( - env::var("OC_ALLOW_WAYLAND"), - Ok(v) if matches!(v.to_ascii_lowercase().as_str(), "1" | "true" | "yes") - ); + let prefer_wayland = display::read_wayland().unwrap_or(false); + let allow_wayland = prefer_wayland + || matches!( + env::var("OC_ALLOW_WAYLAND"), + Ok(v) if matches!(v.to_ascii_lowercase().as_str(), "1" | "true" | "yes") + ); if allow_wayland { + if prefer_wayland { + return Some("Wayland session detected; using native Wayland from settings".into()); + } return Some("Wayland session detected; respecting OC_ALLOW_WAYLAND=1".into()); } diff --git a/packages/desktop/src/bindings.ts b/packages/desktop/src/bindings.ts index 562a98acae..2db1a624cc 100644 --- a/packages/desktop/src/bindings.ts +++ b/packages/desktop/src/bindings.ts @@ -10,6 +10,8 @@ export const commands = { awaitInitialization: (events: Channel) => __TAURI_INVOKE("await_initialization", { events }), getDefaultServerUrl: () => __TAURI_INVOKE("get_default_server_url"), setDefaultServerUrl: (url: string | null) => __TAURI_INVOKE("set_default_server_url", { url }), + getDisplayBackend: () => __TAURI_INVOKE<"wayland" | "auto" | null>("get_display_backend"), + setDisplayBackend: (backend: LinuxDisplayBackend) => __TAURI_INVOKE("set_display_backend", { backend }), parseMarkdownCommand: (markdown: string) => __TAURI_INVOKE("parse_markdown_command", { markdown }), checkAppExists: (appName: string) => __TAURI_INVOKE("check_app_exists", { appName }), }; @@ -22,6 +24,8 @@ export const events = { /* Types */ export type InitStep = { phase: "server_waiting" } | { phase: "sqlite_waiting" } | { phase: "done" }; +export type LinuxDisplayBackend = "wayland" | "auto"; + export type LoadingWindowComplete = null; export type ServerReadyData = { diff --git a/packages/desktop/src/index.tsx b/packages/desktop/src/index.tsx index dd78224e34..25e9f825c7 100644 --- a/packages/desktop/src/index.tsx +++ b/packages/desktop/src/index.tsx @@ -1,7 +1,14 @@ // @refresh reload import { webviewZoom } from "./webview-zoom" import { render } from "solid-js/web" -import { AppBaseProviders, AppInterface, PlatformProvider, Platform, useCommand } from "@opencode-ai/app" +import { + AppBaseProviders, + AppInterface, + PlatformProvider, + Platform, + DisplayBackend, + useCommand, +} from "@opencode-ai/app" import { open, save } from "@tauri-apps/plugin-dialog" import { getCurrent, onOpenUrl } from "@tauri-apps/plugin-deep-link" import { openPath as openerOpenPath } from "@tauri-apps/plugin-opener" @@ -9,6 +16,7 @@ import { open as shellOpen } from "@tauri-apps/plugin-shell" import { type as ostype } from "@tauri-apps/plugin-os" import { check, Update } from "@tauri-apps/plugin-updater" import { getCurrentWindow } from "@tauri-apps/api/window" +import { invoke } from "@tauri-apps/api/core" import { isPermissionGranted, requestPermission } from "@tauri-apps/plugin-notification" import { relaunch } from "@tauri-apps/plugin-process" import { AsyncStorage } from "@solid-primitives/storage" @@ -338,6 +346,15 @@ const createPlatform = (password: Accessor): Platform => ({ await commands.setDefaultServerUrl(url) }, + getDisplayBackend: async () => { + const result = await invoke("get_display_backend").catch(() => null) + return result + }, + + setDisplayBackend: async (backend) => { + await invoke("set_display_backend", { backend }).catch(() => undefined) + }, + parseMarkdown: (markdown: string) => commands.parseMarkdownCommand(markdown), webviewZoom, @@ -413,7 +430,7 @@ render(() => { } return ( - + ) diff --git a/packages/opencode/src/provider/error.ts b/packages/opencode/src/provider/error.ts new file mode 100644 index 0000000000..2693df04fd --- /dev/null +++ b/packages/opencode/src/provider/error.ts @@ -0,0 +1,191 @@ +import { APICallError } from "ai" +import { STATUS_CODES } from "http" +import { iife } from "@/util/iife" + +export namespace ProviderError { + // Adapted from overflow detection patterns in: + // https://github.com/badlogic/pi-mono/blob/main/packages/ai/src/utils/overflow.ts + const OVERFLOW_PATTERNS = [ + /prompt is too long/i, // Anthropic + /input is too long for requested model/i, // Amazon Bedrock + /exceeds the context window/i, // OpenAI (Completions + Responses API message text) + /input token count.*exceeds the maximum/i, // Google (Gemini) + /maximum prompt length is \d+/i, // xAI (Grok) + /reduce the length of the messages/i, // Groq + /maximum context length is \d+ tokens/i, // OpenRouter + /exceeds the limit of \d+/i, // GitHub Copilot + /exceeds the available context size/i, // llama.cpp server + /greater than the context length/i, // LM Studio + /context window exceeds limit/i, // MiniMax + /exceeded model token limit/i, // Kimi For Coding + /context[_ ]length[_ ]exceeded/i, // Generic fallback + /too many tokens/i, // Generic fallback + /token limit exceeded/i, // Generic fallback + ] + + function isOpenAiErrorRetryable(e: APICallError) { + const status = e.statusCode + if (!status) return e.isRetryable + // openai sometimes returns 404 for models that are actually available + return status === 404 || e.isRetryable + } + + // Providers not reliably handled in this function: + // - z.ai: can accept overflow silently (needs token-count/context-window checks) + function isOverflow(message: string) { + if (OVERFLOW_PATTERNS.some((p) => p.test(message))) return true + + // Providers/status patterns handled outside of regex list: + // - Cerebras: often returns "400 (no body)" / "413 (no body)" + // - Mistral: often returns "400 (no body)" / "413 (no body)" + return /^4(00|13)\s*(status code)?\s*\(no body\)/i.test(message) + } + + function error(providerID: string, error: APICallError) { + if (providerID.includes("github-copilot") && error.statusCode === 403) { + return "Please reauthenticate with the copilot provider to ensure your credentials work properly with OpenCode." + } + + return error.message + } + + function message(providerID: string, e: APICallError) { + return iife(() => { + const msg = e.message + if (msg === "") { + if (e.responseBody) return e.responseBody + if (e.statusCode) { + const err = STATUS_CODES[e.statusCode] + if (err) return err + } + return "Unknown error" + } + + const transformed = error(providerID, e) + if (transformed !== msg) { + return transformed + } + if (!e.responseBody || (e.statusCode && msg !== STATUS_CODES[e.statusCode])) { + return msg + } + + try { + const body = JSON.parse(e.responseBody) + // try to extract common error message fields + const errMsg = body.message || body.error || body.error?.message + if (errMsg && typeof errMsg === "string") { + return `${msg}: ${errMsg}` + } + } catch {} + + return `${msg}: ${e.responseBody}` + }).trim() + } + + function json(input: unknown) { + if (typeof input === "string") { + try { + const result = JSON.parse(input) + if (result && typeof result === "object") return result + return undefined + } catch { + return undefined + } + } + if (typeof input === "object" && input !== null) { + return input + } + return undefined + } + + export type ParsedStreamError = + | { + type: "context_overflow" + message: string + responseBody: string + } + | { + type: "api_error" + message: string + isRetryable: false + responseBody: string + } + + export function parseStreamError(input: unknown): ParsedStreamError | undefined { + const body = json(input) + if (!body) return + + const responseBody = JSON.stringify(body) + if (body.type !== "error") return + + switch (body?.error?.code) { + case "context_length_exceeded": + return { + type: "context_overflow", + message: "Input exceeds context window of this model", + responseBody, + } + case "insufficient_quota": + return { + type: "api_error", + message: "Quota exceeded. Check your plan and billing details.", + isRetryable: false, + responseBody, + } + case "usage_not_included": + return { + type: "api_error", + message: "To use Codex with your ChatGPT plan, upgrade to Plus: https://chatgpt.com/explore/plus.", + isRetryable: false, + responseBody, + } + case "invalid_prompt": + return { + type: "api_error", + message: typeof body?.error?.message === "string" ? body?.error?.message : "Invalid prompt.", + isRetryable: false, + responseBody, + } + } + } + + export type ParsedAPICallError = + | { + type: "context_overflow" + message: string + responseBody?: string + } + | { + type: "api_error" + message: string + statusCode?: number + isRetryable: boolean + responseHeaders?: Record + responseBody?: string + metadata?: Record + } + + export function parseAPICallError(input: { providerID: string; error: APICallError }): ParsedAPICallError { + const m = message(input.providerID, input.error) + if (isOverflow(m)) { + return { + type: "context_overflow", + message: m, + responseBody: input.error.responseBody, + } + } + + const metadata = input.error.url ? { url: input.error.url } : undefined + return { + type: "api_error", + message: m, + statusCode: input.error.statusCode, + isRetryable: input.providerID.startsWith("openai") + ? isOpenAiErrorRetryable(input.error) + : input.error.isRetryable, + responseHeaders: input.error.responseHeaders, + responseBody: input.error.responseBody, + metadata, + } + } +} diff --git a/packages/opencode/src/provider/transform.ts b/packages/opencode/src/provider/transform.ts index 8aab0d4151..01291491d3 100644 --- a/packages/opencode/src/provider/transform.ts +++ b/packages/opencode/src/provider/transform.ts @@ -1,4 +1,4 @@ -import type { APICallError, ModelMessage } from "ai" +import type { ModelMessage } from "ai" import { mergeDeep, unique } from "remeda" import type { JSONSchema7 } from "@ai-sdk/provider" import type { JSONSchema } from "zod/v4/core" @@ -643,6 +643,20 @@ export namespace ProviderTransform { } } + // Enable thinking for reasoning models on alibaba-cn (DashScope). + // DashScope's OpenAI-compatible API requires `enable_thinking: true` in the request body + // to return reasoning_content. Without it, models like kimi-k2.5, qwen-plus, qwen3, qwq, + // deepseek-r1, etc. never output thinking/reasoning tokens. + // Note: kimi-k2-thinking is excluded as it returns reasoning_content by default. + if ( + input.model.providerID === "alibaba-cn" && + input.model.capabilities.reasoning && + input.model.api.npm === "@ai-sdk/openai-compatible" && + !modelId.includes("kimi-k2-thinking") + ) { + result["enable_thinking"] = true + } + if (input.model.api.id.includes("gpt-5") && !input.model.api.id.includes("gpt-5-chat")) { if (!input.model.api.id.includes("gpt-5-pro")) { result["reasoningEffort"] = "medium" @@ -810,19 +824,4 @@ export namespace ProviderTransform { return schema as JSONSchema7 } - - export function error(providerID: string, error: APICallError) { - let message = error.message - if (providerID.includes("github-copilot") && error.statusCode === 403) { - return "Please reauthenticate with the copilot provider to ensure your credentials work properly with OpenCode." - } - if (providerID.includes("github-copilot") && message.includes("The requested model is not supported")) { - return ( - message + - "\n\nMake sure the model is enabled in your copilot settings: https://github.com/settings/copilot/features" - ) - } - - return message - } } diff --git a/packages/opencode/src/session/message-v2.ts b/packages/opencode/src/session/message-v2.ts index 65ac72e050..e45bfc7728 100644 --- a/packages/opencode/src/session/message-v2.ts +++ b/packages/opencode/src/session/message-v2.ts @@ -7,8 +7,7 @@ import { LSP } from "../lsp" import { Snapshot } from "@/snapshot" import { fn } from "@/util/fn" import { Storage } from "@/storage/storage" -import { ProviderTransform } from "@/provider/transform" -import { STATUS_CODES } from "http" +import { ProviderError } from "@/provider/error" import { iife } from "@/util/iife" import { type SystemError } from "bun" import type { Provider } from "@/provider/provider" @@ -35,6 +34,10 @@ export namespace MessageV2 { }), ) export type APIError = z.infer + export const ContextOverflowError = NamedError.create( + "ContextOverflowError", + z.object({ message: z.string(), responseBody: z.string().optional() }), + ) const PartBase = z.object({ id: z.string(), @@ -361,6 +364,7 @@ export namespace MessageV2 { NamedError.Unknown.Schema, OutputLengthError.Schema, AbortedError.Schema, + ContextOverflowError.Schema, APIError.Schema, ]) .optional(), @@ -711,13 +715,6 @@ export namespace MessageV2 { return result } - const isOpenAiErrorRetryable = (e: APICallError) => { - const status = e.statusCode - if (!status) return e.isRetryable - // openai sometimes returns 404 for models that are actually available - return status === 404 || e.isRetryable - } - export function fromError(e: unknown, ctx: { providerID: string }) { switch (true) { case e instanceof DOMException && e.name === "AbortError": @@ -751,52 +748,59 @@ export namespace MessageV2 { { cause: e }, ).toObject() case APICallError.isInstance(e): - const message = iife(() => { - let msg = e.message - if (msg === "") { - if (e.responseBody) return e.responseBody - if (e.statusCode) { - const err = STATUS_CODES[e.statusCode] - if (err) return err - } - return "Unknown error" - } - const transformed = ProviderTransform.error(ctx.providerID, e) - if (transformed !== msg) { - return transformed - } - if (!e.responseBody || (e.statusCode && msg !== STATUS_CODES[e.statusCode])) { - return msg - } + const parsed = ProviderError.parseAPICallError({ + providerID: ctx.providerID, + error: e, + }) + if (parsed.type === "context_overflow") { + return new MessageV2.ContextOverflowError( + { + message: parsed.message, + responseBody: parsed.responseBody, + }, + { cause: e }, + ).toObject() + } - try { - const body = JSON.parse(e.responseBody) - // try to extract common error message fields - const errMsg = body.message || body.error || body.error?.message - if (errMsg && typeof errMsg === "string") { - return `${msg}: ${errMsg}` - } - } catch {} - - return `${msg}: ${e.responseBody}` - }).trim() - - const metadata = e.url ? { url: e.url } : undefined return new MessageV2.APIError( { - message, - statusCode: e.statusCode, - isRetryable: ctx.providerID.startsWith("openai") ? isOpenAiErrorRetryable(e) : e.isRetryable, - responseHeaders: e.responseHeaders, - responseBody: e.responseBody, - metadata, + message: parsed.message, + statusCode: parsed.statusCode, + isRetryable: parsed.isRetryable, + responseHeaders: parsed.responseHeaders, + responseBody: parsed.responseBody, + metadata: parsed.metadata, }, { cause: e }, ).toObject() case e instanceof Error: return new NamedError.Unknown({ message: e.toString() }, { cause: e }).toObject() default: - return new NamedError.Unknown({ message: JSON.stringify(e) }, { cause: e }) + try { + const parsed = ProviderError.parseStreamError(e) + if (parsed) { + if (parsed.type === "context_overflow") { + return new MessageV2.ContextOverflowError( + { + message: parsed.message, + responseBody: parsed.responseBody, + }, + { cause: e }, + ).toObject() + } + return new MessageV2.APIError( + { + message: parsed.message, + isRetryable: parsed.isRetryable, + responseBody: parsed.responseBody, + }, + { + cause: e, + }, + ).toObject() + } + } catch {} + return new NamedError.Unknown({ message: JSON.stringify(e) }, { cause: e }).toObject() } } } diff --git a/packages/opencode/src/session/retry.ts b/packages/opencode/src/session/retry.ts index a71a6a3824..0d9a865b1f 100644 --- a/packages/opencode/src/session/retry.ts +++ b/packages/opencode/src/session/retry.ts @@ -59,6 +59,9 @@ export namespace SessionRetry { } export function retryable(error: ReturnType) { + // DO NOT retry context overflow errors + if (MessageV2.ContextOverflowError.isInstance(error)) return undefined + if (MessageV2.APIError.isInstance(error)) { if (!error.data.isRetryable) return undefined return error.data.message.includes("Overloaded") ? "Provider is overloaded" : error.data.message diff --git a/packages/opencode/test/session/message-v2.test.ts b/packages/opencode/test/session/message-v2.test.ts index 2f632ad1cf..c043754bdb 100644 --- a/packages/opencode/test/session/message-v2.test.ts +++ b/packages/opencode/test/session/message-v2.test.ts @@ -1,4 +1,5 @@ import { describe, expect, test } from "bun:test" +import { APICallError } from "ai" import { MessageV2 } from "../../src/session/message-v2" import type { Provider } from "../../src/provider/provider" @@ -784,3 +785,140 @@ describe("session.message-v2.toModelMessage", () => { ]) }) }) + +describe("session.message-v2.fromError", () => { + test("serializes context_length_exceeded as ContextOverflowError", () => { + const input = { + type: "error", + error: { + code: "context_length_exceeded", + }, + } + const result = MessageV2.fromError(input, { providerID: "test" }) + + expect(result).toStrictEqual({ + name: "ContextOverflowError", + data: { + message: "Input exceeds context window of this model", + responseBody: JSON.stringify(input), + }, + }) + }) + + test("serializes response error codes", () => { + const cases = [ + { + code: "insufficient_quota", + message: "Quota exceeded. Check your plan and billing details.", + }, + { + code: "usage_not_included", + message: "To use Codex with your ChatGPT plan, upgrade to Plus: https://chatgpt.com/explore/plus.", + }, + { + code: "invalid_prompt", + message: "Invalid prompt from test", + }, + ] + + cases.forEach((item) => { + const input = { + type: "error", + error: { + code: item.code, + message: item.code === "invalid_prompt" ? item.message : undefined, + }, + } + const result = MessageV2.fromError(input, { providerID: "test" }) + + expect(result).toStrictEqual({ + name: "APIError", + data: { + message: item.message, + isRetryable: false, + responseBody: JSON.stringify(input), + }, + }) + }) + }) + + test("maps github-copilot 403 to reauth guidance", () => { + const error = new APICallError({ + message: "forbidden", + url: "https://api.githubcopilot.com/v1/chat/completions", + requestBodyValues: {}, + statusCode: 403, + responseHeaders: { "content-type": "application/json" }, + responseBody: '{"error":"forbidden"}', + isRetryable: false, + }) + + const result = MessageV2.fromError(error, { providerID: "github-copilot" }) + + expect(result).toStrictEqual({ + name: "APIError", + data: { + message: + "Please reauthenticate with the copilot provider to ensure your credentials work properly with OpenCode.", + statusCode: 403, + isRetryable: false, + responseHeaders: { "content-type": "application/json" }, + responseBody: '{"error":"forbidden"}', + metadata: { + url: "https://api.githubcopilot.com/v1/chat/completions", + }, + }, + }) + }) + + test("detects context overflow from APICallError provider messages", () => { + const cases = [ + "prompt is too long: 213462 tokens > 200000 maximum", + "Your input exceeds the context window of this model", + "The input token count (1196265) exceeds the maximum number of tokens allowed (1048575)", + "Please reduce the length of the messages or completion", + "400 status code (no body)", + "413 status code (no body)", + ] + + cases.forEach((message) => { + const error = new APICallError({ + message, + url: "https://example.com", + requestBodyValues: {}, + statusCode: 400, + responseHeaders: { "content-type": "application/json" }, + isRetryable: false, + }) + const result = MessageV2.fromError(error, { providerID: "test" }) + expect(MessageV2.ContextOverflowError.isInstance(result)).toBe(true) + }) + }) + + test("does not classify 429 no body as context overflow", () => { + const result = MessageV2.fromError( + new APICallError({ + message: "429 status code (no body)", + url: "https://example.com", + requestBodyValues: {}, + statusCode: 429, + responseHeaders: { "content-type": "application/json" }, + isRetryable: false, + }), + { providerID: "test" }, + ) + expect(MessageV2.ContextOverflowError.isInstance(result)).toBe(false) + expect(MessageV2.APIError.isInstance(result)).toBe(true) + }) + + test("serializes unknown inputs", () => { + const result = MessageV2.fromError(123, { providerID: "test" }) + + expect(result).toStrictEqual({ + name: "UnknownError", + data: { + message: "123", + }, + }) + }) +}) diff --git a/packages/sdk/js/src/v2/gen/types.gen.ts b/packages/sdk/js/src/v2/gen/types.gen.ts index 6580121275..61138551ab 100644 --- a/packages/sdk/js/src/v2/gen/types.gen.ts +++ b/packages/sdk/js/src/v2/gen/types.gen.ts @@ -152,6 +152,14 @@ export type MessageAbortedError = { } } +export type ContextOverflowError = { + name: "ContextOverflowError" + data: { + message: string + responseBody?: string + } +} + export type ApiError = { name: "APIError" data: { @@ -176,7 +184,13 @@ export type AssistantMessage = { created: number completed?: number } - error?: ProviderAuthError | UnknownError | MessageOutputLengthError | MessageAbortedError | ApiError + error?: + | ProviderAuthError + | UnknownError + | MessageOutputLengthError + | MessageAbortedError + | ContextOverflowError + | ApiError parentID: string modelID: string providerID: string @@ -820,7 +834,13 @@ export type EventSessionError = { type: "session.error" properties: { sessionID?: string - error?: ProviderAuthError | UnknownError | MessageOutputLengthError | MessageAbortedError | ApiError + error?: + | ProviderAuthError + | UnknownError + | MessageOutputLengthError + | MessageAbortedError + | ContextOverflowError + | ApiError } } diff --git a/packages/sdk/openapi.json b/packages/sdk/openapi.json index dc4da08701..5d43dccac9 100644 --- a/packages/sdk/openapi.json +++ b/packages/sdk/openapi.json @@ -6344,6 +6344,28 @@ }, "required": ["name", "data"] }, + "ContextOverflowError": { + "type": "object", + "properties": { + "name": { + "type": "string", + "const": "ContextOverflowError" + }, + "data": { + "type": "object", + "properties": { + "message": { + "type": "string" + }, + "responseBody": { + "type": "string" + } + }, + "required": ["message"] + } + }, + "required": ["name", "data"] + }, "APIError": { "type": "object", "properties": { @@ -6429,6 +6451,9 @@ { "$ref": "#/components/schemas/MessageAbortedError" }, + { + "$ref": "#/components/schemas/ContextOverflowError" + }, { "$ref": "#/components/schemas/APIError" } @@ -8196,6 +8221,9 @@ { "$ref": "#/components/schemas/MessageAbortedError" }, + { + "$ref": "#/components/schemas/ContextOverflowError" + }, { "$ref": "#/components/schemas/APIError" } diff --git a/packages/ui/src/assets/icons/app/cursor.svg b/packages/ui/src/assets/icons/app/cursor.svg index c2c8c18199..5aa26e8e71 100644 --- a/packages/ui/src/assets/icons/app/cursor.svg +++ b/packages/ui/src/assets/icons/app/cursor.svg @@ -1 +1 @@ - \ No newline at end of file + diff --git a/packages/ui/src/assets/icons/app/finder.png b/packages/ui/src/assets/icons/app/finder.png index 4edf53bca9..2cff579e61 100644 Binary files a/packages/ui/src/assets/icons/app/finder.png and b/packages/ui/src/assets/icons/app/finder.png differ diff --git a/packages/ui/src/assets/icons/app/zed-dark.svg b/packages/ui/src/assets/icons/app/zed-dark.svg new file mode 100644 index 0000000000..67a99ae4a8 --- /dev/null +++ b/packages/ui/src/assets/icons/app/zed-dark.svg @@ -0,0 +1,15 @@ + + + + + + + + + + diff --git a/packages/ui/src/assets/icons/app/zed.svg b/packages/ui/src/assets/icons/app/zed.svg index 7c9a0e5914..a845bf1815 100644 --- a/packages/ui/src/assets/icons/app/zed.svg +++ b/packages/ui/src/assets/icons/app/zed.svg @@ -1 +1,15 @@ - \ No newline at end of file + + + + + + + + + + diff --git a/packages/ui/src/components/app-icon.css b/packages/ui/src/components/app-icon.css index edcdbcceb5..16cb0dcc1c 100644 --- a/packages/ui/src/components/app-icon.css +++ b/packages/ui/src/components/app-icon.css @@ -1,9 +1,5 @@ img[data-component="app-icon"] { display: block; box-sizing: border-box; - padding: 2px; - border-radius: 0.125rem; - background: var(--smoke-light-2); - border: 1px solid var(--smoke-light-alpha-4); object-fit: contain; } diff --git a/packages/ui/src/components/app-icon.tsx b/packages/ui/src/components/app-icon.tsx index e3f2a0fb23..e91638b989 100644 --- a/packages/ui/src/components/app-icon.tsx +++ b/packages/ui/src/components/app-icon.tsx @@ -1,5 +1,5 @@ import type { Component, ComponentProps } from "solid-js" -import { splitProps } from "solid-js" +import { createSignal, onCleanup, onMount, splitProps } from "solid-js" import type { IconName } from "./app-icons/types" import androidStudio from "../assets/icons/app/android-studio.svg" @@ -15,6 +15,7 @@ import textmate from "../assets/icons/app/textmate.png" import vscode from "../assets/icons/app/vscode.svg" import xcode from "../assets/icons/app/xcode.png" import zed from "../assets/icons/app/zed.svg" +import zedDark from "../assets/icons/app/zed-dark.svg" import sublimetext from "../assets/icons/app/sublimetext.svg" const icons = { @@ -34,17 +35,43 @@ const icons = { "sublime-text": sublimetext, } satisfies Record +const themed: Partial> = { + zed: { + light: zed, + dark: zedDark, + }, +} + +const scheme = () => { + if (typeof document !== "object") return "light" as const + if (document.documentElement.dataset.colorScheme === "dark") return "dark" as const + return "light" as const +} + export type AppIconProps = Omit, "src"> & { id: IconName } export const AppIcon: Component = (props) => { const [local, rest] = splitProps(props, ["id", "class", "classList", "alt", "draggable"]) + const [mode, setMode] = createSignal(scheme()) + + onMount(() => { + const sync = () => setMode(scheme()) + const observer = new MutationObserver(sync) + observer.observe(document.documentElement, { + attributes: true, + attributeFilter: ["data-color-scheme"], + }) + sync() + onCleanup(() => observer.disconnect()) + }) + return ( {local.alt { return icon } -const basenameOf = (p: string) => - p - .replace(/[/\\]+$/, "") - .split(/[\\/]/) - .pop() ?? "" +const basenameOf = (p: string) => p.split("\\").join("/").split("/").filter(Boolean).pop() ?? "" const folderNameVariants = (name: string) => { const n = name.toLowerCase() diff --git a/packages/ui/src/components/select.css b/packages/ui/src/components/select.css index 25dd2eb40b..a765850d49 100644 --- a/packages/ui/src/components/select.css +++ b/packages/ui/src/components/select.css @@ -82,7 +82,7 @@ box-shadow: var(--shadow-xs-border-base); } - &:not([data-expanded]):focus { + &:not([data-expanded]):not(:focus-visible):focus { background-color: transparent; box-shadow: none; } diff --git a/packages/ui/src/components/switch.css b/packages/ui/src/components/switch.css index 89e8447322..579930f554 100644 --- a/packages/ui/src/components/switch.css +++ b/packages/ui/src/components/switch.css @@ -86,9 +86,9 @@ background-color: var(--surface-hover); } - &:focus-within:not([data-readonly]) [data-slot="switch-control"] { + &:not([data-readonly]) [data-slot="switch-input"]:focus-visible ~ [data-slot="switch-control"] { border-color: var(--border-focus); - box-shadow: 0 0 0 2px var(--surface-focus); + box-shadow: var(--shadow-xs-border-focus); } &[data-checked] [data-slot="switch-control"] { diff --git a/packages/ui/src/components/tabs.css b/packages/ui/src/components/tabs.css index 56c3e083f5..7668c29764 100644 --- a/packages/ui/src/components/tabs.css +++ b/packages/ui/src/components/tabs.css @@ -429,6 +429,11 @@ background-color: var(--surface-raised-base-hover); } + &:has([data-slot="tabs-trigger"]:focus-visible) { + background-color: var(--surface-raised-base-hover); + box-shadow: var(--shadow-xs-border-focus); + } + &:has([data-selected]) { background-color: var(--surface-raised-base-active); color: var(--text-strong); diff --git a/packages/ui/src/components/tooltip.tsx b/packages/ui/src/components/tooltip.tsx index b55265b794..055e504654 100644 --- a/packages/ui/src/components/tooltip.tsx +++ b/packages/ui/src/components/tooltip.tsx @@ -1,5 +1,5 @@ import { Tooltip as KobalteTooltip } from "@kobalte/core/tooltip" -import { children, createSignal, Match, onMount, splitProps, Switch, type JSX } from "solid-js" +import { createSignal, Match, splitProps, Switch, type JSX } from "solid-js" import type { ComponentProps } from "solid-js" export interface TooltipProps extends ComponentProps { @@ -40,32 +40,16 @@ export function Tooltip(props: TooltipProps) { "contentStyle", "inactive", "forceOpen", + "value", ]) - const c = children(() => local.children) - - onMount(() => { - const childElements = c() - if (childElements instanceof HTMLElement) { - childElements.addEventListener("focusin", () => setOpen(true)) - childElements.addEventListener("focusout", () => setOpen(false)) - } else if (Array.isArray(childElements)) { - for (const child of childElements) { - if (child instanceof HTMLElement) { - child.addEventListener("focusin", () => setOpen(true)) - child.addEventListener("focusout", () => setOpen(false)) - } - } - } - }) - return ( {local.children} - {c()} + {local.children} - {others.value} + {local.value} {/* */} diff --git a/packages/ui/src/theme/themes/deltarune.json b/packages/ui/src/theme/themes/deltarune.json deleted file mode 100644 index f2ab17ee7f..0000000000 --- a/packages/ui/src/theme/themes/deltarune.json +++ /dev/null @@ -1,231 +0,0 @@ -{ - "$schema": "https://opencode.ai/theme.json", - "defs": { - "darkWorldBg": "#0B0B3B", - "darkWorldDeep": "#050520", - "darkWorldPanel": "#151555", - "krisBlue": "#6A7BC4", - "krisCyan": "#75FBED", - "krisIce": "#C7E3F2", - "susiePurple": "#5B209D", - "susieMagenta": "#A017D0", - "susiePink": "#F983D8", - "ralseiGreen": "#33A56C", - "ralseiTeal": "#40E4D4", - "noelleRose": "#DC8998", - "noelleRed": "#DC1510", - "noelleMint": "#ECFFBB", - "noelleCyan": "#77E0FF", - "noelleAqua": "#BBFFFC", - "gold": "#FBCE3C", - "orange": "#F4A731", - "hotPink": "#EB0095", - "queenPink": "#F983D8", - "cyberGreen": "#00FF00", - "white": "#FFFFFF", - "black": "#000000", - "textMuted": "#8888AA" - }, - "theme": { - "primary": { - "dark": "hotPink", - "light": "susieMagenta" - }, - "secondary": { - "dark": "krisCyan", - "light": "krisBlue" - }, - "accent": { - "dark": "ralseiTeal", - "light": "ralseiGreen" - }, - "error": { - "dark": "noelleRed", - "light": "noelleRed" - }, - "warning": { - "dark": "gold", - "light": "orange" - }, - "success": { - "dark": "ralseiTeal", - "light": "ralseiGreen" - }, - "info": { - "dark": "noelleCyan", - "light": "krisBlue" - }, - "text": { - "dark": "white", - "light": "black" - }, - "textMuted": { - "dark": "textMuted", - "light": "#555577" - }, - "background": { - "dark": "darkWorldBg", - "light": "white" - }, - "backgroundPanel": { - "dark": "darkWorldDeep", - "light": "#F0F0F8" - }, - "backgroundElement": { - "dark": "darkWorldPanel", - "light": "#E5E5F0" - }, - "border": { - "dark": "krisBlue", - "light": "susiePurple" - }, - "borderActive": { - "dark": "hotPink", - "light": "susieMagenta" - }, - "borderSubtle": { - "dark": "#3A3A6A", - "light": "#AAAACC" - }, - "diffAdded": { - "dark": "ralseiTeal", - "light": "ralseiGreen" - }, - "diffRemoved": { - "dark": "hotPink", - "light": "noelleRed" - }, - "diffContext": { - "dark": "textMuted", - "light": "#666688" - }, - "diffHunkHeader": { - "dark": "krisBlue", - "light": "susiePurple" - }, - "diffHighlightAdded": { - "dark": "ralseiGreen", - "light": "ralseiTeal" - }, - "diffHighlightRemoved": { - "dark": "noelleRed", - "light": "hotPink" - }, - "diffAddedBg": { - "dark": "#0A2A2A", - "light": "#D4FFEE" - }, - "diffRemovedBg": { - "dark": "#2A0A2A", - "light": "#FFD4E8" - }, - "diffContextBg": { - "dark": "darkWorldDeep", - "light": "#F5F5FA" - }, - "diffLineNumber": { - "dark": "textMuted", - "light": "#666688" - }, - "diffAddedLineNumberBg": { - "dark": "#082020", - "light": "#E0FFF0" - }, - "diffRemovedLineNumberBg": { - "dark": "#200820", - "light": "#FFE0F0" - }, - "markdownText": { - "dark": "white", - "light": "black" - }, - "markdownHeading": { - "dark": "gold", - "light": "orange" - }, - "markdownLink": { - "dark": "krisCyan", - "light": "krisBlue" - }, - "markdownLinkText": { - "dark": "noelleCyan", - "light": "susiePurple" - }, - "markdownCode": { - "dark": "ralseiTeal", - "light": "ralseiGreen" - }, - "markdownBlockQuote": { - "dark": "textMuted", - "light": "#666688" - }, - "markdownEmph": { - "dark": "susiePink", - "light": "susieMagenta" - }, - "markdownStrong": { - "dark": "hotPink", - "light": "susiePurple" - }, - "markdownHorizontalRule": { - "dark": "krisBlue", - "light": "susiePurple" - }, - "markdownListItem": { - "dark": "gold", - "light": "orange" - }, - "markdownListEnumeration": { - "dark": "krisCyan", - "light": "krisBlue" - }, - "markdownImage": { - "dark": "susieMagenta", - "light": "susiePurple" - }, - "markdownImageText": { - "dark": "susiePink", - "light": "susieMagenta" - }, - "markdownCodeBlock": { - "dark": "white", - "light": "black" - }, - "syntaxComment": { - "dark": "textMuted", - "light": "#666688" - }, - "syntaxKeyword": { - "dark": "hotPink", - "light": "susieMagenta" - }, - "syntaxFunction": { - "dark": "krisCyan", - "light": "krisBlue" - }, - "syntaxVariable": { - "dark": "gold", - "light": "orange" - }, - "syntaxString": { - "dark": "ralseiTeal", - "light": "ralseiGreen" - }, - "syntaxNumber": { - "dark": "noelleRose", - "light": "noelleRed" - }, - "syntaxType": { - "dark": "noelleCyan", - "light": "krisBlue" - }, - "syntaxOperator": { - "dark": "white", - "light": "black" - }, - "syntaxPunctuation": { - "dark": "krisBlue", - "light": "#555577" - } - } -} diff --git a/packages/ui/src/theme/themes/undertale.json b/packages/ui/src/theme/themes/undertale.json deleted file mode 100644 index bfbd60b2a0..0000000000 --- a/packages/ui/src/theme/themes/undertale.json +++ /dev/null @@ -1,232 +0,0 @@ -{ - "$schema": "https://opencode.ai/theme.json", - "defs": { - "black": "#000000", - "white": "#FFFFFF", - "soulRed": "#FF0000", - "soulOrange": "#FF6600", - "soulYellow": "#FFFF00", - "soulGreen": "#00FF00", - "soulAqua": "#00FFFF", - "soulBlue": "#0000FF", - "soulPurple": "#FF00FF", - "ruinsPurple": "#A349A4", - "ruinsDark": "#380A43", - "snowdinBlue": "#6BA3E5", - "hotlandOrange": "#FF7F27", - "coreGray": "#3A3949", - "battleBg": "#0D0D1A", - "battlePanel": "#1A1A2E", - "uiYellow": "#FFC90E", - "textGray": "#909090", - "damageRed": "#FF3333", - "healGreen": "#00FF00", - "saveYellow": "#FFFF00", - "determinationRed": "#FF0000", - "mttPink": "#FF6EB4", - "waterfall": "#283197", - "waterfallGlow": "#00BFFF" - }, - "theme": { - "primary": { - "dark": "soulRed", - "light": "determinationRed" - }, - "secondary": { - "dark": "uiYellow", - "light": "uiYellow" - }, - "accent": { - "dark": "soulAqua", - "light": "soulBlue" - }, - "error": { - "dark": "damageRed", - "light": "soulRed" - }, - "warning": { - "dark": "uiYellow", - "light": "hotlandOrange" - }, - "success": { - "dark": "healGreen", - "light": "soulGreen" - }, - "info": { - "dark": "soulAqua", - "light": "waterfallGlow" - }, - "text": { - "dark": "white", - "light": "black" - }, - "textMuted": { - "dark": "textGray", - "light": "coreGray" - }, - "background": { - "dark": "black", - "light": "white" - }, - "backgroundPanel": { - "dark": "battleBg", - "light": "#F0F0F0" - }, - "backgroundElement": { - "dark": "battlePanel", - "light": "#E5E5E5" - }, - "border": { - "dark": "white", - "light": "black" - }, - "borderActive": { - "dark": "soulRed", - "light": "determinationRed" - }, - "borderSubtle": { - "dark": "#555555", - "light": "#AAAAAA" - }, - "diffAdded": { - "dark": "healGreen", - "light": "soulGreen" - }, - "diffRemoved": { - "dark": "damageRed", - "light": "soulRed" - }, - "diffContext": { - "dark": "textGray", - "light": "coreGray" - }, - "diffHunkHeader": { - "dark": "soulAqua", - "light": "soulBlue" - }, - "diffHighlightAdded": { - "dark": "soulGreen", - "light": "healGreen" - }, - "diffHighlightRemoved": { - "dark": "soulRed", - "light": "determinationRed" - }, - "diffAddedBg": { - "dark": "#002200", - "light": "#CCFFCC" - }, - "diffRemovedBg": { - "dark": "#220000", - "light": "#FFCCCC" - }, - "diffContextBg": { - "dark": "battleBg", - "light": "#F5F5F5" - }, - "diffLineNumber": { - "dark": "textGray", - "light": "coreGray" - }, - "diffAddedLineNumberBg": { - "dark": "#001A00", - "light": "#E0FFE0" - }, - "diffRemovedLineNumberBg": { - "dark": "#1A0000", - "light": "#FFE0E0" - }, - "markdownText": { - "dark": "white", - "light": "black" - }, - "markdownHeading": { - "dark": "uiYellow", - "light": "hotlandOrange" - }, - "markdownLink": { - "dark": "soulAqua", - "light": "soulBlue" - }, - "markdownLinkText": { - "dark": "waterfallGlow", - "light": "waterfall" - }, - "markdownCode": { - "dark": "healGreen", - "light": "soulGreen" - }, - "markdownBlockQuote": { - "dark": "textGray", - "light": "coreGray" - }, - "markdownEmph": { - "dark": "mttPink", - "light": "soulPurple" - }, - "markdownStrong": { - "dark": "soulRed", - "light": "determinationRed" - }, - "markdownHorizontalRule": { - "dark": "white", - "light": "black" - }, - "markdownListItem": { - "dark": "uiYellow", - "light": "uiYellow" - }, - "markdownListEnumeration": { - "dark": "uiYellow", - "light": "uiYellow" - }, - "markdownImage": { - "dark": "ruinsPurple", - "light": "soulPurple" - }, - "markdownImageText": { - "dark": "mttPink", - "light": "ruinsPurple" - }, - "markdownCodeBlock": { - "dark": "white", - "light": "black" - }, - "syntaxComment": { - "dark": "textGray", - "light": "coreGray" - }, - "syntaxKeyword": { - "dark": "soulRed", - "light": "determinationRed" - }, - "syntaxFunction": { - "dark": "soulAqua", - "light": "soulBlue" - }, - "syntaxVariable": { - "dark": "uiYellow", - "light": "hotlandOrange" - }, - "syntaxString": { - "dark": "healGreen", - "light": "soulGreen" - }, - "syntaxNumber": { - "dark": "mttPink", - "light": "soulPurple" - }, - "syntaxType": { - "dark": "waterfallGlow", - "light": "waterfall" - }, - "syntaxOperator": { - "dark": "white", - "light": "black" - }, - "syntaxPunctuation": { - "dark": "textGray", - "light": "coreGray" - } - } -} diff --git a/packages/web/src/content/docs/providers.mdx b/packages/web/src/content/docs/providers.mdx index 6852672149..e7befcf026 100644 --- a/packages/web/src/content/docs/providers.mdx +++ b/packages/web/src/content/docs/providers.mdx @@ -791,8 +791,6 @@ To use your GitHub Copilot subscription with opencode: :::note Some models might need a [Pro+ subscription](https://github.com/features/copilot/plans) to use. - -Some models need to be manually enabled in your [GitHub Copilot settings](https://docs.github.com/en/copilot/how-tos/use-ai-models/configure-access-to-ai-models#setup-for-individual-use). ::: 1. Run the `/connect` command and search for GitHub Copilot. diff --git a/themes/deltarune.json b/themes/deltarune.json deleted file mode 100644 index f2ab17ee7f..0000000000 --- a/themes/deltarune.json +++ /dev/null @@ -1,231 +0,0 @@ -{ - "$schema": "https://opencode.ai/theme.json", - "defs": { - "darkWorldBg": "#0B0B3B", - "darkWorldDeep": "#050520", - "darkWorldPanel": "#151555", - "krisBlue": "#6A7BC4", - "krisCyan": "#75FBED", - "krisIce": "#C7E3F2", - "susiePurple": "#5B209D", - "susieMagenta": "#A017D0", - "susiePink": "#F983D8", - "ralseiGreen": "#33A56C", - "ralseiTeal": "#40E4D4", - "noelleRose": "#DC8998", - "noelleRed": "#DC1510", - "noelleMint": "#ECFFBB", - "noelleCyan": "#77E0FF", - "noelleAqua": "#BBFFFC", - "gold": "#FBCE3C", - "orange": "#F4A731", - "hotPink": "#EB0095", - "queenPink": "#F983D8", - "cyberGreen": "#00FF00", - "white": "#FFFFFF", - "black": "#000000", - "textMuted": "#8888AA" - }, - "theme": { - "primary": { - "dark": "hotPink", - "light": "susieMagenta" - }, - "secondary": { - "dark": "krisCyan", - "light": "krisBlue" - }, - "accent": { - "dark": "ralseiTeal", - "light": "ralseiGreen" - }, - "error": { - "dark": "noelleRed", - "light": "noelleRed" - }, - "warning": { - "dark": "gold", - "light": "orange" - }, - "success": { - "dark": "ralseiTeal", - "light": "ralseiGreen" - }, - "info": { - "dark": "noelleCyan", - "light": "krisBlue" - }, - "text": { - "dark": "white", - "light": "black" - }, - "textMuted": { - "dark": "textMuted", - "light": "#555577" - }, - "background": { - "dark": "darkWorldBg", - "light": "white" - }, - "backgroundPanel": { - "dark": "darkWorldDeep", - "light": "#F0F0F8" - }, - "backgroundElement": { - "dark": "darkWorldPanel", - "light": "#E5E5F0" - }, - "border": { - "dark": "krisBlue", - "light": "susiePurple" - }, - "borderActive": { - "dark": "hotPink", - "light": "susieMagenta" - }, - "borderSubtle": { - "dark": "#3A3A6A", - "light": "#AAAACC" - }, - "diffAdded": { - "dark": "ralseiTeal", - "light": "ralseiGreen" - }, - "diffRemoved": { - "dark": "hotPink", - "light": "noelleRed" - }, - "diffContext": { - "dark": "textMuted", - "light": "#666688" - }, - "diffHunkHeader": { - "dark": "krisBlue", - "light": "susiePurple" - }, - "diffHighlightAdded": { - "dark": "ralseiGreen", - "light": "ralseiTeal" - }, - "diffHighlightRemoved": { - "dark": "noelleRed", - "light": "hotPink" - }, - "diffAddedBg": { - "dark": "#0A2A2A", - "light": "#D4FFEE" - }, - "diffRemovedBg": { - "dark": "#2A0A2A", - "light": "#FFD4E8" - }, - "diffContextBg": { - "dark": "darkWorldDeep", - "light": "#F5F5FA" - }, - "diffLineNumber": { - "dark": "textMuted", - "light": "#666688" - }, - "diffAddedLineNumberBg": { - "dark": "#082020", - "light": "#E0FFF0" - }, - "diffRemovedLineNumberBg": { - "dark": "#200820", - "light": "#FFE0F0" - }, - "markdownText": { - "dark": "white", - "light": "black" - }, - "markdownHeading": { - "dark": "gold", - "light": "orange" - }, - "markdownLink": { - "dark": "krisCyan", - "light": "krisBlue" - }, - "markdownLinkText": { - "dark": "noelleCyan", - "light": "susiePurple" - }, - "markdownCode": { - "dark": "ralseiTeal", - "light": "ralseiGreen" - }, - "markdownBlockQuote": { - "dark": "textMuted", - "light": "#666688" - }, - "markdownEmph": { - "dark": "susiePink", - "light": "susieMagenta" - }, - "markdownStrong": { - "dark": "hotPink", - "light": "susiePurple" - }, - "markdownHorizontalRule": { - "dark": "krisBlue", - "light": "susiePurple" - }, - "markdownListItem": { - "dark": "gold", - "light": "orange" - }, - "markdownListEnumeration": { - "dark": "krisCyan", - "light": "krisBlue" - }, - "markdownImage": { - "dark": "susieMagenta", - "light": "susiePurple" - }, - "markdownImageText": { - "dark": "susiePink", - "light": "susieMagenta" - }, - "markdownCodeBlock": { - "dark": "white", - "light": "black" - }, - "syntaxComment": { - "dark": "textMuted", - "light": "#666688" - }, - "syntaxKeyword": { - "dark": "hotPink", - "light": "susieMagenta" - }, - "syntaxFunction": { - "dark": "krisCyan", - "light": "krisBlue" - }, - "syntaxVariable": { - "dark": "gold", - "light": "orange" - }, - "syntaxString": { - "dark": "ralseiTeal", - "light": "ralseiGreen" - }, - "syntaxNumber": { - "dark": "noelleRose", - "light": "noelleRed" - }, - "syntaxType": { - "dark": "noelleCyan", - "light": "krisBlue" - }, - "syntaxOperator": { - "dark": "white", - "light": "black" - }, - "syntaxPunctuation": { - "dark": "krisBlue", - "light": "#555577" - } - } -} diff --git a/themes/undertale.json b/themes/undertale.json deleted file mode 100644 index bfbd60b2a0..0000000000 --- a/themes/undertale.json +++ /dev/null @@ -1,232 +0,0 @@ -{ - "$schema": "https://opencode.ai/theme.json", - "defs": { - "black": "#000000", - "white": "#FFFFFF", - "soulRed": "#FF0000", - "soulOrange": "#FF6600", - "soulYellow": "#FFFF00", - "soulGreen": "#00FF00", - "soulAqua": "#00FFFF", - "soulBlue": "#0000FF", - "soulPurple": "#FF00FF", - "ruinsPurple": "#A349A4", - "ruinsDark": "#380A43", - "snowdinBlue": "#6BA3E5", - "hotlandOrange": "#FF7F27", - "coreGray": "#3A3949", - "battleBg": "#0D0D1A", - "battlePanel": "#1A1A2E", - "uiYellow": "#FFC90E", - "textGray": "#909090", - "damageRed": "#FF3333", - "healGreen": "#00FF00", - "saveYellow": "#FFFF00", - "determinationRed": "#FF0000", - "mttPink": "#FF6EB4", - "waterfall": "#283197", - "waterfallGlow": "#00BFFF" - }, - "theme": { - "primary": { - "dark": "soulRed", - "light": "determinationRed" - }, - "secondary": { - "dark": "uiYellow", - "light": "uiYellow" - }, - "accent": { - "dark": "soulAqua", - "light": "soulBlue" - }, - "error": { - "dark": "damageRed", - "light": "soulRed" - }, - "warning": { - "dark": "uiYellow", - "light": "hotlandOrange" - }, - "success": { - "dark": "healGreen", - "light": "soulGreen" - }, - "info": { - "dark": "soulAqua", - "light": "waterfallGlow" - }, - "text": { - "dark": "white", - "light": "black" - }, - "textMuted": { - "dark": "textGray", - "light": "coreGray" - }, - "background": { - "dark": "black", - "light": "white" - }, - "backgroundPanel": { - "dark": "battleBg", - "light": "#F0F0F0" - }, - "backgroundElement": { - "dark": "battlePanel", - "light": "#E5E5E5" - }, - "border": { - "dark": "white", - "light": "black" - }, - "borderActive": { - "dark": "soulRed", - "light": "determinationRed" - }, - "borderSubtle": { - "dark": "#555555", - "light": "#AAAAAA" - }, - "diffAdded": { - "dark": "healGreen", - "light": "soulGreen" - }, - "diffRemoved": { - "dark": "damageRed", - "light": "soulRed" - }, - "diffContext": { - "dark": "textGray", - "light": "coreGray" - }, - "diffHunkHeader": { - "dark": "soulAqua", - "light": "soulBlue" - }, - "diffHighlightAdded": { - "dark": "soulGreen", - "light": "healGreen" - }, - "diffHighlightRemoved": { - "dark": "soulRed", - "light": "determinationRed" - }, - "diffAddedBg": { - "dark": "#002200", - "light": "#CCFFCC" - }, - "diffRemovedBg": { - "dark": "#220000", - "light": "#FFCCCC" - }, - "diffContextBg": { - "dark": "battleBg", - "light": "#F5F5F5" - }, - "diffLineNumber": { - "dark": "textGray", - "light": "coreGray" - }, - "diffAddedLineNumberBg": { - "dark": "#001A00", - "light": "#E0FFE0" - }, - "diffRemovedLineNumberBg": { - "dark": "#1A0000", - "light": "#FFE0E0" - }, - "markdownText": { - "dark": "white", - "light": "black" - }, - "markdownHeading": { - "dark": "uiYellow", - "light": "hotlandOrange" - }, - "markdownLink": { - "dark": "soulAqua", - "light": "soulBlue" - }, - "markdownLinkText": { - "dark": "waterfallGlow", - "light": "waterfall" - }, - "markdownCode": { - "dark": "healGreen", - "light": "soulGreen" - }, - "markdownBlockQuote": { - "dark": "textGray", - "light": "coreGray" - }, - "markdownEmph": { - "dark": "mttPink", - "light": "soulPurple" - }, - "markdownStrong": { - "dark": "soulRed", - "light": "determinationRed" - }, - "markdownHorizontalRule": { - "dark": "white", - "light": "black" - }, - "markdownListItem": { - "dark": "uiYellow", - "light": "uiYellow" - }, - "markdownListEnumeration": { - "dark": "uiYellow", - "light": "uiYellow" - }, - "markdownImage": { - "dark": "ruinsPurple", - "light": "soulPurple" - }, - "markdownImageText": { - "dark": "mttPink", - "light": "ruinsPurple" - }, - "markdownCodeBlock": { - "dark": "white", - "light": "black" - }, - "syntaxComment": { - "dark": "textGray", - "light": "coreGray" - }, - "syntaxKeyword": { - "dark": "soulRed", - "light": "determinationRed" - }, - "syntaxFunction": { - "dark": "soulAqua", - "light": "soulBlue" - }, - "syntaxVariable": { - "dark": "uiYellow", - "light": "hotlandOrange" - }, - "syntaxString": { - "dark": "healGreen", - "light": "soulGreen" - }, - "syntaxNumber": { - "dark": "mttPink", - "light": "soulPurple" - }, - "syntaxType": { - "dark": "waterfallGlow", - "light": "waterfall" - }, - "syntaxOperator": { - "dark": "white", - "light": "black" - }, - "syntaxPunctuation": { - "dark": "textGray", - "light": "coreGray" - } - } -}