// @refresh reload import { webviewZoom } from "./webview-zoom" import { render } from "solid-js/web" import { AppBaseProviders, AppInterface, PlatformProvider, Platform, 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" 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 { isPermissionGranted, requestPermission } from "@tauri-apps/plugin-notification" import { relaunch } from "@tauri-apps/plugin-process" import { AsyncStorage } from "@solid-primitives/storage" import { fetch as tauriFetch } from "@tauri-apps/plugin-http" import { Store } from "@tauri-apps/plugin-store" import { Splash } from "@opencode-ai/ui/logo" import { createSignal, Show, Accessor, JSX, createResource, onMount, onCleanup } from "solid-js" import { UPDATER_ENABLED } from "./updater" import { initI18n, t } from "./i18n" import pkg from "../package.json" import "./styles.css" import { commands, InitStep } from "./bindings" import { Channel } from "@tauri-apps/api/core" import { createMenu } from "./menu" const root = document.getElementById("root") if (import.meta.env.DEV && !(root instanceof HTMLElement)) { throw new Error(t("error.dev.rootNotFound")) } void initI18n() let update: Update | null = null 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 = async () => { const startUrls = await getCurrent().catch(() => null) if (startUrls?.length) emitDeepLinks(startUrls) await onOpenUrl((urls) => emitDeepLinks(urls)).catch(() => undefined) } const createPlatform = (password: Accessor): Platform => ({ platform: "desktop", os: (() => { const type = ostype() if (type === "macos" || type === "windows" || type === "linux") return type return undefined })(), version: pkg.version, async openDirectoryPickerDialog(opts) { const result = await open({ directory: true, multiple: opts?.multiple ?? false, title: opts?.title ?? t("desktop.dialog.chooseFolder"), }) return result }, async openFilePickerDialog(opts) { const result = await open({ directory: false, multiple: opts?.multiple ?? false, title: opts?.title ?? t("desktop.dialog.chooseFile"), }) return result }, async saveFilePickerDialog(opts) { const result = await save({ title: opts?.title ?? t("desktop.dialog.saveFile"), defaultPath: opts?.defaultPath, }) return result }, openLink(url: string) { void shellOpen(url).catch(() => undefined) }, openPath(path: string, app?: string) { return openerOpenPath(path, app) }, back() { window.history.back() }, forward() { window.history.forward() }, storage: (() => { type StoreLike = { get(key: string): Promise set(key: string, value: string): Promise delete(key: string): Promise clear(): Promise keys(): Promise length(): Promise } const WRITE_DEBOUNCE_MS = 250 const storeCache = new Map>() const apiCache = new Map Promise }>() const memoryCache = new Map() const flushAll = async () => { const apis = Array.from(apiCache.values()) await Promise.all(apis.map((api) => api.flush().catch(() => undefined))) } if ("addEventListener" in globalThis) { const handleVisibility = () => { if (document.visibilityState !== "hidden") return void flushAll() } window.addEventListener("pagehide", () => void flushAll()) document.addEventListener("visibilitychange", handleVisibility) } const createMemoryStore = () => { const data = new Map() const store: StoreLike = { get: async (key) => data.get(key), set: async (key, value) => { data.set(key, value) }, delete: async (key) => { data.delete(key) }, clear: async () => { data.clear() }, keys: async () => Array.from(data.keys()), length: async () => data.size, } return store } const getStore = (name: string) => { const cached = storeCache.get(name) if (cached) return cached const store = Store.load(name).catch(() => { const cached = memoryCache.get(name) if (cached) return cached const memory = createMemoryStore() memoryCache.set(name, memory) return memory }) storeCache.set(name, store) return store } const createStorage = (name: string) => { const pending = new Map() let timer: ReturnType | undefined let flushing: Promise | undefined const flush = async () => { if (flushing) return flushing flushing = (async () => { const store = await getStore(name) while (pending.size > 0) { const batch = Array.from(pending.entries()) pending.clear() for (const [key, value] of batch) { if (value === null) { await store.delete(key).catch(() => undefined) } else { await store.set(key, value).catch(() => undefined) } } } })().finally(() => { flushing = undefined }) return flushing } const schedule = () => { if (timer) return timer = setTimeout(() => { timer = undefined void flush() }, WRITE_DEBOUNCE_MS) } const api: AsyncStorage & { flush: () => Promise } = { flush, getItem: async (key: string) => { const next = pending.get(key) if (next !== undefined) return next const store = await getStore(name) const value = await store.get(key).catch(() => null) if (value === undefined) return null return value }, setItem: async (key: string, value: string) => { pending.set(key, value) schedule() }, removeItem: async (key: string) => { pending.set(key, null) schedule() }, clear: async () => { pending.clear() const store = await getStore(name) await store.clear().catch(() => undefined) }, key: async (index: number) => { const store = await getStore(name) return (await store.keys().catch(() => []))[index] }, getLength: async () => { const store = await getStore(name) return await store.length().catch(() => 0) }, get length() { return api.getLength() }, } return api } return (name = "default.dat") => { const cached = apiCache.get(name) if (cached) return cached const api = createStorage(name) apiCache.set(name, api) return api } })(), checkUpdate: async () => { if (!UPDATER_ENABLED) return { updateAvailable: false } const next = await check().catch(() => null) if (!next) return { updateAvailable: false } const ok = await next .download() .then(() => true) .catch(() => false) if (!ok) return { updateAvailable: false } update = next return { updateAvailable: true, version: next.version } }, update: async () => { if (!UPDATER_ENABLED || !update) return if (ostype() === "windows") await commands.killSidecar().catch(() => undefined) await update.install().catch(() => undefined) }, restart: async () => { await commands.killSidecar().catch(() => undefined) await relaunch() }, notify: async (title, description, href) => { const granted = await isPermissionGranted().catch(() => false) const permission = granted ? "granted" : await requestPermission().catch(() => "denied") if (permission !== "granted") return const win = getCurrentWindow() const focused = await win.isFocused().catch(() => document.hasFocus()) if (focused) return await Promise.resolve() .then(() => { const notification = new Notification(title, { body: description ?? "", icon: "https://opencode.ai/favicon-96x96-v3.png", }) notification.onclick = () => { const win = getCurrentWindow() void win.show().catch(() => undefined) void win.unminimize().catch(() => undefined) void win.setFocus().catch(() => undefined) if (href) { window.history.pushState(null, "", href) window.dispatchEvent(new PopStateEvent("popstate")) } notification.close() } }) .catch(() => undefined) }, fetch: (input, init) => { const pw = password() const addHeader = (headers: Headers, password: string) => { headers.append("Authorization", `Basic ${btoa(`opencode:${password}`)}`) } if (input instanceof Request) { if (pw) addHeader(input.headers, pw) return tauriFetch(input) } else { const headers = new Headers(init?.headers) if (pw) addHeader(headers, pw) return tauriFetch(input, { ...(init as any), headers: headers, }) } }, getDefaultServerUrl: async () => { const result = await commands.getDefaultServerUrl().catch(() => null) return result }, setDefaultServerUrl: async (url: string | null) => { await commands.setDefaultServerUrl(url) }, parseMarkdown: (markdown: string) => commands.parseMarkdownCommand(markdown), webviewZoom, checkAppExists: async (appName: string) => { return commands.checkAppExists(appName) }, }) let menuTrigger = null as null | ((id: string) => void) createMenu((id) => { menuTrigger?.(id) }) void listenForDeepLinks() render(() => { const [serverPassword, setServerPassword] = createSignal(null) const platform = createPlatform(() => serverPassword()) 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) } } onMount(() => { document.addEventListener("click", handleClick) onCleanup(() => { document.removeEventListener("click", handleClick) }) }) return ( {(data) => { setServerPassword(data().password) window.__OPENCODE__ ??= {} window.__OPENCODE__.serverPassword = data().password ?? undefined function Inner() { const cmd = useCommand() menuTrigger = (id) => cmd.trigger(id) return null } return ( ) }} ) }, root!) type ServerReadyData = { url: string; password: string | null } // Gate component that waits for the server to be ready function ServerGate(props: { children: (data: Accessor) => JSX.Element }) { const [serverData] = createResource(() => commands.awaitInitialization(new Channel() as any)) if (serverData.state === "errored") throw serverData.error return ( // Not using suspense as not all components are compatible with it (undefined refs)
} > {(data) => props.children(data)}
) }