// @refresh reload import { ACCEPTED_FILE_EXTENSIONS, AppBaseProviders, AppInterface, handleNotificationClick, loadLocaleDict, normalizeLocale, type Locale, type Platform, PlatformProvider, ServerConnection, useCommand, useWslServers, } from "@opencode-ai/app" import type { UpdaterState } from "@opencode-ai/app/updater" import * as Sentry from "@sentry/solid" import type { AsyncStorage } from "@solid-primitives/storage" import { MemoryRouter } from "@solidjs/router" import { createEffect, createMemo, createResource, createSignal, onCleanup, onMount, Show } from "solid-js" import { render } from "solid-js/web" import pkg from "../../package.json" import { initI18n, t } from "./i18n" import { initializationData, initializationReady } from "./initialization" import { resetZoom, setPinchZoomEnabled, webviewZoom, zoomIn, zoomOut } from "./webview-zoom" import { availableStartupServer, readyWslConnections } from "./wsl/connections" import "./styles.css" import { Splash } from "@opencode-ai/ui/logo" import { useTheme } from "@opencode-ai/ui/theme/context" const root = document.getElementById("root") if (import.meta.env.DEV && !(root instanceof HTMLElement)) { throw new Error(t("error.dev.rootNotFound")) } if (import.meta.env.VITE_SENTRY_DSN) { Sentry.init({ dsn: import.meta.env.VITE_SENTRY_DSN, environment: import.meta.env.VITE_SENTRY_ENVIRONMENT ?? import.meta.env.MODE, release: import.meta.env.VITE_SENTRY_RELEASE ?? `desktop@${pkg.version}`, initialScope: { tags: { platform: "desktop", }, }, integrations: (integrations) => { return integrations.filter( (i) => i.name !== "Breadcrumbs" && !( import.meta.env.OPENCODE_CHANNEL === "prod" && (i.name === "GlobalHandlers" || i.name === "BrowserApiErrors") ), ) }, }) } void initI18n() const [updaterState, setUpdaterState] = createSignal({ status: "disabled" }) void window.api.updater.subscribe(setUpdaterState) const deepLinkEvent = "opencode:deep-link" const emitDeepLinks = (urls: string[]) => { if (urls.length === 0) return window.__OPENCODE__ ??= {} const pending = window.__OPENCODE__.deepLinks ?? [] window.__OPENCODE__.deepLinks = [...pending, ...urls] window.dispatchEvent(new CustomEvent(deepLinkEvent, { detail: { urls } })) } const listenForDeepLinks = () => { void window.api.consumeInitialDeepLinks().then((urls) => emitDeepLinks(urls)) return window.api.onDeepLink((urls) => emitDeepLinks(urls)) } const createPlatform = (): Platform => { const attachmentPaths = new WeakMap() const os = (() => { const ua = navigator.userAgent if (ua.includes("Mac")) return "macos" if (ua.includes("Windows")) return "windows" if (ua.includes("Linux")) return "linux" return undefined })() const runDesktopMenuAction: Platform["runDesktopMenuAction"] = (action) => { switch (action) { case "view.resetZoom": resetZoom() return case "view.zoomIn": zoomIn() return case "view.zoomOut": zoomOut() return } return window.api.runDesktopMenuAction(action) } const storage = (() => { const cache = new Map() const createStorage = (name: string) => { const api: AsyncStorage = { getItem: (key: string) => window.api.storeGet(name, key), setItem: (key: string, value: string) => window.api.storeSet(name, key, value), removeItem: (key: string) => window.api.storeDelete(name, key), clear: () => window.api.storeClear(name), key: async (index: number) => (await window.api.storeKeys(name))[index], getLength: () => window.api.storeLength(name), get length() { return api.getLength() }, } return api } return (name = "default.dat") => { const cached = cache.get(name) if (cached) return cached const api = createStorage(name) cache.set(name, api) return api } })() const wslServersApi = os === "windows" ? window.api.wslServers : undefined return { platform: "desktop", os, version: pkg.version, async openDirectoryPickerDialog(opts) { return window.api.openDirectoryPicker({ multiple: opts?.multiple ?? false, title: opts?.title ?? t("desktop.dialog.chooseFolder"), }) }, async openAttachmentPickerDialog(opts, onFile) { const result = await window.api.openFilePicker({ multiple: opts?.multiple ?? false, title: opts?.title ?? t("desktop.dialog.chooseFile"), defaultPath: opts?.defaultPath, extensions: opts?.extensions ?? ACCEPTED_FILE_EXTENSIONS, }) if (!result) return try { for (const file of result.files) { const selected = new File([await window.api.readPickedFile(result.token, file.path)], file.name) attachmentPaths.set(selected, file.path) await onFile(selected) } } finally { await window.api.releasePickedFiles(result.token) } }, getPathForFile(file) { return attachmentPaths.get(file) ?? window.api.getPathForFile(file) }, async saveFilePickerDialog(opts) { return window.api.saveFilePicker({ title: opts?.title ?? t("desktop.dialog.saveFile"), defaultPath: opts?.defaultPath, }) }, openLink(url: string) { window.api.openLink(url) }, async openPath(path: string, app?: string) { if (os === "windows") { const resolvedApp = app ? await window.api.resolveAppPath(app).catch(() => null) : null return window.api.openPath(path, resolvedApp ?? undefined) } return window.api.openPath(path, app) }, back() { window.history.back() }, forward() { window.history.forward() }, storage, updater: { state: updaterState, check: () => window.api.updater.check(), install: () => window.api.updater.install(), }, exportDebugLogs: () => window.api.exportDebugLogs(), recordFatalRendererError: (error) => window.api.recordFatalRendererError(error), restart: async () => { await window.api.killSidecar().catch(() => undefined) window.api.relaunch() }, notify: async (title, description, href) => { const focused = await window.api.getWindowFocused().catch(() => document.hasFocus()) if (focused) return const notification = new Notification(title, { body: description ?? "", icon: "https://opencode.ai/favicon-96x96-v3.png", }) notification.onclick = () => { void window.api.showWindow() void window.api.setWindowFocus() handleNotificationClick(href) notification.close() } }, fetch: (input, init) => { if (input instanceof Request) return fetch(input) return fetch(input, init) }, getDefaultServer: async () => { const url = await window.api.getDefaultServerUrl().catch(() => null) if (!url) return null return ServerConnection.Key.make(url) }, setDefaultServer: async (url: string | null) => { await window.api.setDefaultServerUrl(url) }, wslServers: wslServersApi, getDisplayBackend: async () => { return window.api.getDisplayBackend().catch(() => null) }, setDisplayBackend: async (backend) => { await window.api.setDisplayBackend(backend) }, parseMarkdown: (markdown: string) => window.api.parseMarkdownCommand(markdown), webviewZoom, getPinchZoomEnabled: () => window.api.getPinchZoomEnabled(), setPinchZoomEnabled, runDesktopMenuAction, checkAppExists: async (appName: string) => { return window.api.checkAppExists(appName) }, async readClipboardImage() { const image = await window.api.readClipboardImage().catch(() => null) if (!image) return null const blob = new Blob([image.buffer], { type: "image/png" }) return new File([blob], `pasted-image-${Date.now()}.png`, { type: "image/png", }) }, } } let menuTrigger = null as null | ((id: string) => void) window.api.onMenuCommand((id) => { menuTrigger?.(id) }) listenForDeepLinks() render(() => { const platform = createPlatform() const loadLocale = async () => { const current = await platform.storage?.("opencode.global.dat").getItem("language") const legacy = current ? undefined : await platform.storage?.().getItem("language.v1") const raw = current ?? legacy if (!raw) return const locale = raw.match(/"locale"\s*:\s*"([^"]+)"/)?.[1] if (!locale) return const next = normalizeLocale(locale) if (next !== "en") await loadLocaleDict(next) return next satisfies Locale } const [windowCount] = createResource(() => window.api.getWindowCount()) // Fetch sidecar credentials (available immediately, before health check) const [sidecar] = createResource(() => window.api.awaitInitialization()) const [defaultServer] = createResource(() => platform.getDefaultServer?.()) const [locale] = createResource(loadLocale) function handleClick(e: MouseEvent) { const link = (e.target as HTMLElement).closest("a.external-link") as HTMLAnchorElement | null if (link?.href) { e.preventDefault() platform.openLink(link.href) } } function Inner() { const cmd = useCommand() menuTrigger = (id) => cmd.trigger(id) const theme = useTheme() createEffect(() => { theme.themeId() theme.mode() const bg = getComputedStyle(document.documentElement).getPropertyValue("--background-base").trim() if (bg) { void window.api.setBackgroundColor(bg) } }) return null } function App() { const wslServers = useWslServers() const splash = (
) const ready = createMemo( () => !defaultServer.loading && !sidecar.loading && !windowCount.loading && !locale.loading, ) const servers = createMemo(() => { const data = initializationData(sidecar) const list: ServerConnection.Any[] = [] if (data) { list.push({ displayName: "Local Server", type: "sidecar", variant: "base", http: { url: data.url, username: data.username ?? undefined, password: data.password ?? undefined, }, }) } list.push(...readyWslConnections(wslServers.data)) return list }) const effectiveDefaultServer = createMemo(() => ServerConnection.Key.make(availableStartupServer(defaultServer.latest, wslServers.data)), ) return ( {(key) => ( )} ) } onMount(() => { document.addEventListener("click", handleClick) onCleanup(() => { document.removeEventListener("click", handleClick) }) }) return ( {(_) => } ) }, root!)