summaryrefslogtreecommitdiffhomepage
path: root/packages/desktop-electron/src/renderer
diff options
context:
space:
mode:
authorBrendan Allan <[email protected]>2026-03-04 15:12:34 +0800
committerGitHub <[email protected]>2026-03-04 15:12:34 +0800
commit5cf235fa6cf7b4c890c68f8ff68a96fcae992abf (patch)
tree25fdfd8ce95ad048fb097822995dcf060e8d6d8b /packages/desktop-electron/src/renderer
parente4f0825c56300286ec0aa82b1006e4006a17e1e1 (diff)
downloadopencode-5cf235fa6cf7b4c890c68f8ff68a96fcae992abf.tar.gz
opencode-5cf235fa6cf7b4c890c68f8ff68a96fcae992abf.zip
desktop: add electron version (#15663)
Diffstat (limited to 'packages/desktop-electron/src/renderer')
-rw-r--r--packages/desktop-electron/src/renderer/cli.ts12
-rw-r--r--packages/desktop-electron/src/renderer/env.d.ts12
-rw-r--r--packages/desktop-electron/src/renderer/i18n/ar.ts26
-rw-r--r--packages/desktop-electron/src/renderer/i18n/br.ts27
-rw-r--r--packages/desktop-electron/src/renderer/i18n/bs.ts28
-rw-r--r--packages/desktop-electron/src/renderer/i18n/da.ts28
-rw-r--r--packages/desktop-electron/src/renderer/i18n/de.ts28
-rw-r--r--packages/desktop-electron/src/renderer/i18n/en.ts27
-rw-r--r--packages/desktop-electron/src/renderer/i18n/es.ts27
-rw-r--r--packages/desktop-electron/src/renderer/i18n/fr.ts28
-rw-r--r--packages/desktop-electron/src/renderer/i18n/index.ts187
-rw-r--r--packages/desktop-electron/src/renderer/i18n/ja.ts28
-rw-r--r--packages/desktop-electron/src/renderer/i18n/ko.ts27
-rw-r--r--packages/desktop-electron/src/renderer/i18n/no.ts28
-rw-r--r--packages/desktop-electron/src/renderer/i18n/pl.ts28
-rw-r--r--packages/desktop-electron/src/renderer/i18n/ru.ts27
-rw-r--r--packages/desktop-electron/src/renderer/i18n/zh.ts26
-rw-r--r--packages/desktop-electron/src/renderer/i18n/zht.ts26
-rw-r--r--packages/desktop-electron/src/renderer/index.html23
-rw-r--r--packages/desktop-electron/src/renderer/index.tsx312
-rw-r--r--packages/desktop-electron/src/renderer/loading.html23
-rw-r--r--packages/desktop-electron/src/renderer/loading.tsx80
-rw-r--r--packages/desktop-electron/src/renderer/styles.css0
-rw-r--r--packages/desktop-electron/src/renderer/updater.ts14
-rw-r--r--packages/desktop-electron/src/renderer/webview-zoom.ts38
25 files changed, 1110 insertions, 0 deletions
diff --git a/packages/desktop-electron/src/renderer/cli.ts b/packages/desktop-electron/src/renderer/cli.ts
new file mode 100644
index 000000000..11d3c1f1b
--- /dev/null
+++ b/packages/desktop-electron/src/renderer/cli.ts
@@ -0,0 +1,12 @@
+import { initI18n, t } from "./i18n"
+
+export async function installCli(): Promise<void> {
+ await initI18n()
+
+ try {
+ const path = await window.api.installCli()
+ window.alert(t("desktop.cli.installed.message", { path }))
+ } catch (e) {
+ window.alert(t("desktop.cli.failed.message", { error: String(e) }))
+ }
+}
diff --git a/packages/desktop-electron/src/renderer/env.d.ts b/packages/desktop-electron/src/renderer/env.d.ts
new file mode 100644
index 000000000..d1590ff04
--- /dev/null
+++ b/packages/desktop-electron/src/renderer/env.d.ts
@@ -0,0 +1,12 @@
+import type { ElectronAPI } from "../preload/types"
+
+declare global {
+ interface Window {
+ api: ElectronAPI
+ __OPENCODE__?: {
+ updaterEnabled?: boolean
+ wsl?: boolean
+ deepLinks?: string[]
+ }
+ }
+}
diff --git a/packages/desktop-electron/src/renderer/i18n/ar.ts b/packages/desktop-electron/src/renderer/i18n/ar.ts
new file mode 100644
index 000000000..fdbf0a804
--- /dev/null
+++ b/packages/desktop-electron/src/renderer/i18n/ar.ts
@@ -0,0 +1,26 @@
+export const dict = {
+ "desktop.menu.checkForUpdates": "التحقق من وجود تحديثات...",
+ "desktop.menu.installCli": "تثبيت CLI...",
+ "desktop.menu.reloadWebview": "إعادة تحميل Webview",
+ "desktop.menu.restart": "إعادة تشغيل",
+
+ "desktop.dialog.chooseFolder": "اختر مجلدًا",
+ "desktop.dialog.chooseFile": "اختر ملفًا",
+ "desktop.dialog.saveFile": "حفظ ملف",
+
+ "desktop.updater.checkFailed.title": "فشل التحقق من التحديثات",
+ "desktop.updater.checkFailed.message": "فشل التحقق من وجود تحديثات",
+ "desktop.updater.none.title": "لا توجد تحديثات متاحة",
+ "desktop.updater.none.message": "أنت تستخدم بالفعل أحدث إصدار من OpenCode",
+ "desktop.updater.downloadFailed.title": "فشل التحديث",
+ "desktop.updater.downloadFailed.message": "فشل تنزيل التحديث",
+ "desktop.updater.downloaded.title": "تم تنزيل التحديث",
+ "desktop.updater.downloaded.prompt": "تم تنزيل إصدار {{version}} من OpenCode، هل ترغب في تثبيته وإعادة تشغيله؟",
+ "desktop.updater.installFailed.title": "فشل التحديث",
+ "desktop.updater.installFailed.message": "فشل تثبيت التحديث",
+
+ "desktop.cli.installed.title": "تم تثبيت CLI",
+ "desktop.cli.installed.message": "تم تثبيت CLI في {{path}}\n\nأعد تشغيل الطرفية لاستخدام الأمر 'opencode'.",
+ "desktop.cli.failed.title": "فشل التثبيت",
+ "desktop.cli.failed.message": "فشل تثبيت CLI: {{error}}",
+}
diff --git a/packages/desktop-electron/src/renderer/i18n/br.ts b/packages/desktop-electron/src/renderer/i18n/br.ts
new file mode 100644
index 000000000..75fe2dc32
--- /dev/null
+++ b/packages/desktop-electron/src/renderer/i18n/br.ts
@@ -0,0 +1,27 @@
+export const dict = {
+ "desktop.menu.checkForUpdates": "Verificar atualizações...",
+ "desktop.menu.installCli": "Instalar CLI...",
+ "desktop.menu.reloadWebview": "Recarregar Webview",
+ "desktop.menu.restart": "Reiniciar",
+
+ "desktop.dialog.chooseFolder": "Escolher uma pasta",
+ "desktop.dialog.chooseFile": "Escolher um arquivo",
+ "desktop.dialog.saveFile": "Salvar arquivo",
+
+ "desktop.updater.checkFailed.title": "Falha ao verificar atualizações",
+ "desktop.updater.checkFailed.message": "Falha ao verificar atualizações",
+ "desktop.updater.none.title": "Nenhuma atualização disponível",
+ "desktop.updater.none.message": "Você já está usando a versão mais recente do OpenCode",
+ "desktop.updater.downloadFailed.title": "Falha na atualização",
+ "desktop.updater.downloadFailed.message": "Falha ao baixar a atualização",
+ "desktop.updater.downloaded.title": "Atualização baixada",
+ "desktop.updater.downloaded.prompt":
+ "A versão {{version}} do OpenCode foi baixada. Você gostaria de instalá-la e reiniciar?",
+ "desktop.updater.installFailed.title": "Falha na atualização",
+ "desktop.updater.installFailed.message": "Falha ao instalar a atualização",
+
+ "desktop.cli.installed.title": "CLI instalada",
+ "desktop.cli.installed.message": "CLI instalada em {{path}}\n\nReinicie seu terminal para usar o comando 'opencode'.",
+ "desktop.cli.failed.title": "Falha na instalação",
+ "desktop.cli.failed.message": "Falha ao instalar a CLI: {{error}}",
+}
diff --git a/packages/desktop-electron/src/renderer/i18n/bs.ts b/packages/desktop-electron/src/renderer/i18n/bs.ts
new file mode 100644
index 000000000..58c266f53
--- /dev/null
+++ b/packages/desktop-electron/src/renderer/i18n/bs.ts
@@ -0,0 +1,28 @@
+export const dict = {
+ "desktop.menu.checkForUpdates": "Provjeri ažuriranja...",
+ "desktop.menu.installCli": "Instaliraj CLI...",
+ "desktop.menu.reloadWebview": "Ponovo učitavanje webview-a",
+ "desktop.menu.restart": "Restartuj",
+
+ "desktop.dialog.chooseFolder": "Odaberi folder",
+ "desktop.dialog.chooseFile": "Odaberi datoteku",
+ "desktop.dialog.saveFile": "Sačuvaj datoteku",
+
+ "desktop.updater.checkFailed.title": "Provjera ažuriranja nije uspjela",
+ "desktop.updater.checkFailed.message": "Nije moguće provjeriti ažuriranja",
+ "desktop.updater.none.title": "Nema dostupnog ažuriranja",
+ "desktop.updater.none.message": "Već koristiš najnoviju verziju OpenCode-a",
+ "desktop.updater.downloadFailed.title": "Ažuriranje nije uspjelo",
+ "desktop.updater.downloadFailed.message": "Neuspjelo preuzimanje ažuriranja",
+ "desktop.updater.downloaded.title": "Ažuriranje preuzeto",
+ "desktop.updater.downloaded.prompt":
+ "Verzija {{version}} OpenCode-a je preuzeta. Želiš li da je instaliraš i ponovo pokreneš aplikaciju?",
+ "desktop.updater.installFailed.title": "Ažuriranje nije uspjelo",
+ "desktop.updater.installFailed.message": "Neuspjela instalacija ažuriranja",
+
+ "desktop.cli.installed.title": "CLI instaliran",
+ "desktop.cli.installed.message":
+ "CLI je instaliran u {{path}}\n\nRestartuj terminal da bi koristio komandu 'opencode'.",
+ "desktop.cli.failed.title": "Instalacija nije uspjela",
+ "desktop.cli.failed.message": "Neuspjela instalacija CLI-a: {{error}}",
+}
diff --git a/packages/desktop-electron/src/renderer/i18n/da.ts b/packages/desktop-electron/src/renderer/i18n/da.ts
new file mode 100644
index 000000000..2109495f7
--- /dev/null
+++ b/packages/desktop-electron/src/renderer/i18n/da.ts
@@ -0,0 +1,28 @@
+export const dict = {
+ "desktop.menu.checkForUpdates": "Tjek for opdateringer...",
+ "desktop.menu.installCli": "Installer CLI...",
+ "desktop.menu.reloadWebview": "Genindlæs Webview",
+ "desktop.menu.restart": "Genstart",
+
+ "desktop.dialog.chooseFolder": "Vælg en mappe",
+ "desktop.dialog.chooseFile": "Vælg en fil",
+ "desktop.dialog.saveFile": "Gem fil",
+
+ "desktop.updater.checkFailed.title": "Opdateringstjek mislykkedes",
+ "desktop.updater.checkFailed.message": "Kunne ikke tjekke for opdateringer",
+ "desktop.updater.none.title": "Ingen opdatering tilgængelig",
+ "desktop.updater.none.message": "Du bruger allerede den nyeste version af OpenCode",
+ "desktop.updater.downloadFailed.title": "Opdatering mislykkedes",
+ "desktop.updater.downloadFailed.message": "Kunne ikke downloade opdateringen",
+ "desktop.updater.downloaded.title": "Opdatering downloadet",
+ "desktop.updater.downloaded.prompt":
+ "Version {{version}} af OpenCode er blevet downloadet. Vil du installere den og genstarte?",
+ "desktop.updater.installFailed.title": "Opdatering mislykkedes",
+ "desktop.updater.installFailed.message": "Kunne ikke installere opdateringen",
+
+ "desktop.cli.installed.title": "CLI installeret",
+ "desktop.cli.installed.message":
+ "CLI installeret i {{path}}\n\nGenstart din terminal for at bruge 'opencode'-kommandoen.",
+ "desktop.cli.failed.title": "Installation mislykkedes",
+ "desktop.cli.failed.message": "Kunne ikke installere CLI: {{error}}",
+}
diff --git a/packages/desktop-electron/src/renderer/i18n/de.ts b/packages/desktop-electron/src/renderer/i18n/de.ts
new file mode 100644
index 000000000..38ad8096e
--- /dev/null
+++ b/packages/desktop-electron/src/renderer/i18n/de.ts
@@ -0,0 +1,28 @@
+export const dict = {
+ "desktop.menu.checkForUpdates": "Nach Updates suchen...",
+ "desktop.menu.installCli": "CLI installieren...",
+ "desktop.menu.reloadWebview": "Webview neu laden",
+ "desktop.menu.restart": "Neustart",
+
+ "desktop.dialog.chooseFolder": "Ordner auswählen",
+ "desktop.dialog.chooseFile": "Datei auswählen",
+ "desktop.dialog.saveFile": "Datei speichern",
+
+ "desktop.updater.checkFailed.title": "Updateprüfung fehlgeschlagen",
+ "desktop.updater.checkFailed.message": "Updates konnten nicht geprüft werden",
+ "desktop.updater.none.title": "Kein Update verfügbar",
+ "desktop.updater.none.message": "Sie verwenden bereits die neueste Version von OpenCode",
+ "desktop.updater.downloadFailed.title": "Update fehlgeschlagen",
+ "desktop.updater.downloadFailed.message": "Update konnte nicht heruntergeladen werden",
+ "desktop.updater.downloaded.title": "Update heruntergeladen",
+ "desktop.updater.downloaded.prompt":
+ "Version {{version}} von OpenCode wurde heruntergeladen. Möchten Sie sie installieren und neu starten?",
+ "desktop.updater.installFailed.title": "Update fehlgeschlagen",
+ "desktop.updater.installFailed.message": "Update konnte nicht installiert werden",
+
+ "desktop.cli.installed.title": "CLI installiert",
+ "desktop.cli.installed.message":
+ "CLI wurde in {{path}} installiert\n\nStarten Sie Ihr Terminal neu, um den Befehl 'opencode' zu verwenden.",
+ "desktop.cli.failed.title": "Installation fehlgeschlagen",
+ "desktop.cli.failed.message": "CLI konnte nicht installiert werden: {{error}}",
+}
diff --git a/packages/desktop-electron/src/renderer/i18n/en.ts b/packages/desktop-electron/src/renderer/i18n/en.ts
new file mode 100644
index 000000000..4c30380d5
--- /dev/null
+++ b/packages/desktop-electron/src/renderer/i18n/en.ts
@@ -0,0 +1,27 @@
+export const dict = {
+ "desktop.menu.checkForUpdates": "Check for Updates...",
+ "desktop.menu.installCli": "Install CLI...",
+ "desktop.menu.reloadWebview": "Reload Webview",
+ "desktop.menu.restart": "Restart",
+
+ "desktop.dialog.chooseFolder": "Choose a folder",
+ "desktop.dialog.chooseFile": "Choose a file",
+ "desktop.dialog.saveFile": "Save file",
+
+ "desktop.updater.checkFailed.title": "Update Check Failed",
+ "desktop.updater.checkFailed.message": "Failed to check for updates",
+ "desktop.updater.none.title": "No Update Available",
+ "desktop.updater.none.message": "You are already using the latest version of OpenCode",
+ "desktop.updater.downloadFailed.title": "Update Failed",
+ "desktop.updater.downloadFailed.message": "Failed to download update",
+ "desktop.updater.downloaded.title": "Update Downloaded",
+ "desktop.updater.downloaded.prompt":
+ "Version {{version}} of OpenCode has been downloaded, would you like to install it and relaunch?",
+ "desktop.updater.installFailed.title": "Update Failed",
+ "desktop.updater.installFailed.message": "Failed to install update",
+
+ "desktop.cli.installed.title": "CLI Installed",
+ "desktop.cli.installed.message": "CLI installed to {{path}}\n\nRestart your terminal to use the 'opencode' command.",
+ "desktop.cli.failed.title": "Installation Failed",
+ "desktop.cli.failed.message": "Failed to install CLI: {{error}}",
+}
diff --git a/packages/desktop-electron/src/renderer/i18n/es.ts b/packages/desktop-electron/src/renderer/i18n/es.ts
new file mode 100644
index 000000000..80504a8f2
--- /dev/null
+++ b/packages/desktop-electron/src/renderer/i18n/es.ts
@@ -0,0 +1,27 @@
+export const dict = {
+ "desktop.menu.checkForUpdates": "Buscar actualizaciones...",
+ "desktop.menu.installCli": "Instalar CLI...",
+ "desktop.menu.reloadWebview": "Recargar Webview",
+ "desktop.menu.restart": "Reiniciar",
+
+ "desktop.dialog.chooseFolder": "Elegir una carpeta",
+ "desktop.dialog.chooseFile": "Elegir un archivo",
+ "desktop.dialog.saveFile": "Guardar archivo",
+
+ "desktop.updater.checkFailed.title": "Comprobación de actualizaciones fallida",
+ "desktop.updater.checkFailed.message": "No se pudieron buscar actualizaciones",
+ "desktop.updater.none.title": "No hay actualizaciones disponibles",
+ "desktop.updater.none.message": "Ya estás usando la versión más reciente de OpenCode",
+ "desktop.updater.downloadFailed.title": "Actualización fallida",
+ "desktop.updater.downloadFailed.message": "No se pudo descargar la actualización",
+ "desktop.updater.downloaded.title": "Actualización descargada",
+ "desktop.updater.downloaded.prompt":
+ "Se ha descargado la versión {{version}} de OpenCode. ¿Quieres instalarla y reiniciar?",
+ "desktop.updater.installFailed.title": "Actualización fallida",
+ "desktop.updater.installFailed.message": "No se pudo instalar la actualización",
+
+ "desktop.cli.installed.title": "CLI instalada",
+ "desktop.cli.installed.message": "CLI instalada en {{path}}\n\nReinicia tu terminal para usar el comando 'opencode'.",
+ "desktop.cli.failed.title": "Instalación fallida",
+ "desktop.cli.failed.message": "No se pudo instalar la CLI: {{error}}",
+}
diff --git a/packages/desktop-electron/src/renderer/i18n/fr.ts b/packages/desktop-electron/src/renderer/i18n/fr.ts
new file mode 100644
index 000000000..4f0bb2b16
--- /dev/null
+++ b/packages/desktop-electron/src/renderer/i18n/fr.ts
@@ -0,0 +1,28 @@
+export const dict = {
+ "desktop.menu.checkForUpdates": "Vérifier les mises à jour...",
+ "desktop.menu.installCli": "Installer la CLI...",
+ "desktop.menu.reloadWebview": "Recharger la Webview",
+ "desktop.menu.restart": "Redémarrer",
+
+ "desktop.dialog.chooseFolder": "Choisir un dossier",
+ "desktop.dialog.chooseFile": "Choisir un fichier",
+ "desktop.dialog.saveFile": "Enregistrer le fichier",
+
+ "desktop.updater.checkFailed.title": "Échec de la vérification des mises à jour",
+ "desktop.updater.checkFailed.message": "Impossible de vérifier les mises à jour",
+ "desktop.updater.none.title": "Aucune mise à jour disponible",
+ "desktop.updater.none.message": "Vous utilisez déjà la dernière version d'OpenCode",
+ "desktop.updater.downloadFailed.title": "Échec de la mise à jour",
+ "desktop.updater.downloadFailed.message": "Impossible de télécharger la mise à jour",
+ "desktop.updater.downloaded.title": "Mise à jour téléchargée",
+ "desktop.updater.downloaded.prompt":
+ "La version {{version}} d'OpenCode a été téléchargée. Voulez-vous l'installer et redémarrer ?",
+ "desktop.updater.installFailed.title": "Échec de la mise à jour",
+ "desktop.updater.installFailed.message": "Impossible d'installer la mise à jour",
+
+ "desktop.cli.installed.title": "CLI installée",
+ "desktop.cli.installed.message":
+ "CLI installée dans {{path}}\n\nRedémarrez votre terminal pour utiliser la commande 'opencode'.",
+ "desktop.cli.failed.title": "Échec de l'installation",
+ "desktop.cli.failed.message": "Impossible d'installer la CLI : {{error}}",
+}
diff --git a/packages/desktop-electron/src/renderer/i18n/index.ts b/packages/desktop-electron/src/renderer/i18n/index.ts
new file mode 100644
index 000000000..81158ad24
--- /dev/null
+++ b/packages/desktop-electron/src/renderer/i18n/index.ts
@@ -0,0 +1,187 @@
+import * as i18n from "@solid-primitives/i18n"
+
+import { dict as desktopEn } from "./en"
+import { dict as desktopZh } from "./zh"
+import { dict as desktopZht } from "./zht"
+import { dict as desktopKo } from "./ko"
+import { dict as desktopDe } from "./de"
+import { dict as desktopEs } from "./es"
+import { dict as desktopFr } from "./fr"
+import { dict as desktopDa } from "./da"
+import { dict as desktopJa } from "./ja"
+import { dict as desktopPl } from "./pl"
+import { dict as desktopRu } from "./ru"
+import { dict as desktopAr } from "./ar"
+import { dict as desktopNo } from "./no"
+import { dict as desktopBr } from "./br"
+import { dict as desktopBs } from "./bs"
+
+import { dict as appEn } from "../../../../app/src/i18n/en"
+import { dict as appZh } from "../../../../app/src/i18n/zh"
+import { dict as appZht } from "../../../../app/src/i18n/zht"
+import { dict as appKo } from "../../../../app/src/i18n/ko"
+import { dict as appDe } from "../../../../app/src/i18n/de"
+import { dict as appEs } from "../../../../app/src/i18n/es"
+import { dict as appFr } from "../../../../app/src/i18n/fr"
+import { dict as appDa } from "../../../../app/src/i18n/da"
+import { dict as appJa } from "../../../../app/src/i18n/ja"
+import { dict as appPl } from "../../../../app/src/i18n/pl"
+import { dict as appRu } from "../../../../app/src/i18n/ru"
+import { dict as appAr } from "../../../../app/src/i18n/ar"
+import { dict as appNo } from "../../../../app/src/i18n/no"
+import { dict as appBr } from "../../../../app/src/i18n/br"
+import { dict as appBs } from "../../../../app/src/i18n/bs"
+
+export type Locale =
+ | "en"
+ | "zh"
+ | "zht"
+ | "ko"
+ | "de"
+ | "es"
+ | "fr"
+ | "da"
+ | "ja"
+ | "pl"
+ | "ru"
+ | "ar"
+ | "no"
+ | "br"
+ | "bs"
+
+type RawDictionary = typeof appEn & typeof desktopEn
+type Dictionary = i18n.Flatten<RawDictionary>
+
+const LOCALES: readonly Locale[] = [
+ "en",
+ "zh",
+ "zht",
+ "ko",
+ "de",
+ "es",
+ "fr",
+ "da",
+ "ja",
+ "pl",
+ "ru",
+ "bs",
+ "ar",
+ "no",
+ "br",
+]
+
+function detectLocale(): Locale {
+ if (typeof navigator !== "object") return "en"
+
+ const languages = navigator.languages?.length ? navigator.languages : [navigator.language]
+ for (const language of languages) {
+ if (!language) continue
+ if (language.toLowerCase().startsWith("zh")) {
+ if (language.toLowerCase().includes("hant")) return "zht"
+ return "zh"
+ }
+ if (language.toLowerCase().startsWith("ko")) return "ko"
+ if (language.toLowerCase().startsWith("de")) return "de"
+ if (language.toLowerCase().startsWith("es")) return "es"
+ if (language.toLowerCase().startsWith("fr")) return "fr"
+ if (language.toLowerCase().startsWith("da")) return "da"
+ if (language.toLowerCase().startsWith("ja")) return "ja"
+ if (language.toLowerCase().startsWith("pl")) return "pl"
+ if (language.toLowerCase().startsWith("ru")) return "ru"
+ if (language.toLowerCase().startsWith("ar")) return "ar"
+ if (
+ language.toLowerCase().startsWith("no") ||
+ language.toLowerCase().startsWith("nb") ||
+ language.toLowerCase().startsWith("nn")
+ )
+ return "no"
+ if (language.toLowerCase().startsWith("pt")) return "br"
+ if (language.toLowerCase().startsWith("bs")) return "bs"
+ }
+
+ return "en"
+}
+
+function parseLocale(value: unknown): Locale | null {
+ if (!value) return null
+ if (typeof value !== "string") return null
+ if ((LOCALES as readonly string[]).includes(value)) return value as Locale
+ return null
+}
+
+function parseRecord(value: unknown) {
+ if (!value || typeof value !== "object") return null
+ if (Array.isArray(value)) return null
+ return value as Record<string, unknown>
+}
+
+function parseStored(value: unknown) {
+ if (typeof value !== "string") return value
+ try {
+ return JSON.parse(value) as unknown
+ } catch {
+ return value
+ }
+}
+
+function pickLocale(value: unknown): Locale | null {
+ const direct = parseLocale(value)
+ if (direct) return direct
+
+ const record = parseRecord(value)
+ if (!record) return null
+
+ return parseLocale(record.locale)
+}
+
+const base = i18n.flatten({ ...appEn, ...desktopEn })
+
+function build(locale: Locale): Dictionary {
+ if (locale === "en") return base
+ if (locale === "zh") return { ...base, ...i18n.flatten(appZh), ...i18n.flatten(desktopZh) }
+ if (locale === "zht") return { ...base, ...i18n.flatten(appZht), ...i18n.flatten(desktopZht) }
+ if (locale === "de") return { ...base, ...i18n.flatten(appDe), ...i18n.flatten(desktopDe) }
+ if (locale === "es") return { ...base, ...i18n.flatten(appEs), ...i18n.flatten(desktopEs) }
+ if (locale === "fr") return { ...base, ...i18n.flatten(appFr), ...i18n.flatten(desktopFr) }
+ if (locale === "da") return { ...base, ...i18n.flatten(appDa), ...i18n.flatten(desktopDa) }
+ if (locale === "ja") return { ...base, ...i18n.flatten(appJa), ...i18n.flatten(desktopJa) }
+ if (locale === "pl") return { ...base, ...i18n.flatten(appPl), ...i18n.flatten(desktopPl) }
+ if (locale === "ru") return { ...base, ...i18n.flatten(appRu), ...i18n.flatten(desktopRu) }
+ if (locale === "ar") return { ...base, ...i18n.flatten(appAr), ...i18n.flatten(desktopAr) }
+ if (locale === "no") return { ...base, ...i18n.flatten(appNo), ...i18n.flatten(desktopNo) }
+ if (locale === "br") return { ...base, ...i18n.flatten(appBr), ...i18n.flatten(desktopBr) }
+ if (locale === "bs") return { ...base, ...i18n.flatten(appBs), ...i18n.flatten(desktopBs) }
+ return { ...base, ...i18n.flatten(appKo), ...i18n.flatten(desktopKo) }
+}
+
+const state = {
+ locale: detectLocale(),
+ dict: base as Dictionary,
+ init: undefined as Promise<Locale> | undefined,
+}
+
+state.dict = build(state.locale)
+
+const translate = i18n.translator(() => state.dict, i18n.resolveTemplate)
+
+export function t(key: keyof Dictionary, params?: Record<string, string | number>) {
+ return translate(key, params)
+}
+
+export function initI18n(): Promise<Locale> {
+ const cached = state.init
+ if (cached) return cached
+
+ const promise = (async () => {
+ const raw = await window.api.storeGet("opencode.global.dat", "language").catch(() => null)
+ const value = parseStored(raw)
+ const next = pickLocale(value) ?? state.locale
+
+ state.locale = next
+ state.dict = build(next)
+ return next
+ })().catch(() => state.locale)
+
+ state.init = promise
+ return promise
+}
diff --git a/packages/desktop-electron/src/renderer/i18n/ja.ts b/packages/desktop-electron/src/renderer/i18n/ja.ts
new file mode 100644
index 000000000..fc485c6f4
--- /dev/null
+++ b/packages/desktop-electron/src/renderer/i18n/ja.ts
@@ -0,0 +1,28 @@
+export const dict = {
+ "desktop.menu.checkForUpdates": "アップデートを確認...",
+ "desktop.menu.installCli": "CLI をインストール...",
+ "desktop.menu.reloadWebview": "Webview を再読み込み",
+ "desktop.menu.restart": "再起動",
+
+ "desktop.dialog.chooseFolder": "フォルダーを選択",
+ "desktop.dialog.chooseFile": "ファイルを選択",
+ "desktop.dialog.saveFile": "ファイルを保存",
+
+ "desktop.updater.checkFailed.title": "アップデートの確認に失敗しました",
+ "desktop.updater.checkFailed.message": "アップデートを確認できませんでした",
+ "desktop.updater.none.title": "利用可能なアップデートはありません",
+ "desktop.updater.none.message": "すでに最新バージョンの OpenCode を使用しています",
+ "desktop.updater.downloadFailed.title": "アップデートに失敗しました",
+ "desktop.updater.downloadFailed.message": "アップデートをダウンロードできませんでした",
+ "desktop.updater.downloaded.title": "アップデートをダウンロードしました",
+ "desktop.updater.downloaded.prompt":
+ "OpenCode のバージョン {{version}} がダウンロードされました。インストールして再起動しますか?",
+ "desktop.updater.installFailed.title": "アップデートに失敗しました",
+ "desktop.updater.installFailed.message": "アップデートをインストールできませんでした",
+
+ "desktop.cli.installed.title": "CLI をインストールしました",
+ "desktop.cli.installed.message":
+ "CLI を {{path}} にインストールしました\n\nターミナルを再起動して 'opencode' コマンドを使用してください。",
+ "desktop.cli.failed.title": "インストールに失敗しました",
+ "desktop.cli.failed.message": "CLI のインストールに失敗しました: {{error}}",
+}
diff --git a/packages/desktop-electron/src/renderer/i18n/ko.ts b/packages/desktop-electron/src/renderer/i18n/ko.ts
new file mode 100644
index 000000000..be27cec86
--- /dev/null
+++ b/packages/desktop-electron/src/renderer/i18n/ko.ts
@@ -0,0 +1,27 @@
+export const dict = {
+ "desktop.menu.checkForUpdates": "업데이트 확인...",
+ "desktop.menu.installCli": "CLI 설치...",
+ "desktop.menu.reloadWebview": "Webview 새로고침",
+ "desktop.menu.restart": "다시 시작",
+
+ "desktop.dialog.chooseFolder": "폴더 선택",
+ "desktop.dialog.chooseFile": "파일 선택",
+ "desktop.dialog.saveFile": "파일 저장",
+
+ "desktop.updater.checkFailed.title": "업데이트 확인 실패",
+ "desktop.updater.checkFailed.message": "업데이트를 확인하지 못했습니다",
+ "desktop.updater.none.title": "사용 가능한 업데이트 없음",
+ "desktop.updater.none.message": "이미 최신 버전의 OpenCode를 사용하고 있습니다",
+ "desktop.updater.downloadFailed.title": "업데이트 실패",
+ "desktop.updater.downloadFailed.message": "업데이트를 다운로드하지 못했습니다",
+ "desktop.updater.downloaded.title": "업데이트 다운로드 완료",
+ "desktop.updater.downloaded.prompt": "OpenCode {{version}} 버전을 다운로드했습니다. 설치하고 다시 실행할까요?",
+ "desktop.updater.installFailed.title": "업데이트 실패",
+ "desktop.updater.installFailed.message": "업데이트를 설치하지 못했습니다",
+
+ "desktop.cli.installed.title": "CLI 설치됨",
+ "desktop.cli.installed.message":
+ "CLI가 {{path}}에 설치되었습니다\n\n터미널을 다시 시작하여 'opencode' 명령을 사용하세요.",
+ "desktop.cli.failed.title": "설치 실패",
+ "desktop.cli.failed.message": "CLI 설치 실패: {{error}}",
+}
diff --git a/packages/desktop-electron/src/renderer/i18n/no.ts b/packages/desktop-electron/src/renderer/i18n/no.ts
new file mode 100644
index 000000000..e39bd7f3b
--- /dev/null
+++ b/packages/desktop-electron/src/renderer/i18n/no.ts
@@ -0,0 +1,28 @@
+export const dict = {
+ "desktop.menu.checkForUpdates": "Se etter oppdateringer...",
+ "desktop.menu.installCli": "Installer CLI...",
+ "desktop.menu.reloadWebview": "Last inn Webview på nytt",
+ "desktop.menu.restart": "Start på nytt",
+
+ "desktop.dialog.chooseFolder": "Velg en mappe",
+ "desktop.dialog.chooseFile": "Velg en fil",
+ "desktop.dialog.saveFile": "Lagre fil",
+
+ "desktop.updater.checkFailed.title": "Oppdateringssjekk mislyktes",
+ "desktop.updater.checkFailed.message": "Kunne ikke se etter oppdateringer",
+ "desktop.updater.none.title": "Ingen oppdatering tilgjengelig",
+ "desktop.updater.none.message": "Du bruker allerede den nyeste versjonen av OpenCode",
+ "desktop.updater.downloadFailed.title": "Oppdatering mislyktes",
+ "desktop.updater.downloadFailed.message": "Kunne ikke laste ned oppdateringen",
+ "desktop.updater.downloaded.title": "Oppdatering lastet ned",
+ "desktop.updater.downloaded.prompt":
+ "Versjon {{version}} av OpenCode er lastet ned. Vil du installere den og starte på nytt?",
+ "desktop.updater.installFailed.title": "Oppdatering mislyktes",
+ "desktop.updater.installFailed.message": "Kunne ikke installere oppdateringen",
+
+ "desktop.cli.installed.title": "CLI installert",
+ "desktop.cli.installed.message":
+ "CLI installert til {{path}}\n\nStart terminalen på nytt for å bruke 'opencode'-kommandoen.",
+ "desktop.cli.failed.title": "Installasjon mislyktes",
+ "desktop.cli.failed.message": "Kunne ikke installere CLI: {{error}}",
+}
diff --git a/packages/desktop-electron/src/renderer/i18n/pl.ts b/packages/desktop-electron/src/renderer/i18n/pl.ts
new file mode 100644
index 000000000..d3ad7ce64
--- /dev/null
+++ b/packages/desktop-electron/src/renderer/i18n/pl.ts
@@ -0,0 +1,28 @@
+export const dict = {
+ "desktop.menu.checkForUpdates": "Sprawdź aktualizacje...",
+ "desktop.menu.installCli": "Zainstaluj CLI...",
+ "desktop.menu.reloadWebview": "Przeładuj Webview",
+ "desktop.menu.restart": "Restartuj",
+
+ "desktop.dialog.chooseFolder": "Wybierz folder",
+ "desktop.dialog.chooseFile": "Wybierz plik",
+ "desktop.dialog.saveFile": "Zapisz plik",
+
+ "desktop.updater.checkFailed.title": "Nie udało się sprawdzić aktualizacji",
+ "desktop.updater.checkFailed.message": "Nie udało się sprawdzić aktualizacji",
+ "desktop.updater.none.title": "Brak dostępnych aktualizacji",
+ "desktop.updater.none.message": "Korzystasz już z najnowszej wersji OpenCode",
+ "desktop.updater.downloadFailed.title": "Aktualizacja nie powiodła się",
+ "desktop.updater.downloadFailed.message": "Nie udało się pobrać aktualizacji",
+ "desktop.updater.downloaded.title": "Aktualizacja pobrana",
+ "desktop.updater.downloaded.prompt":
+ "Pobrano wersję {{version}} OpenCode. Czy chcesz ją zainstalować i uruchomić ponownie?",
+ "desktop.updater.installFailed.title": "Aktualizacja nie powiodła się",
+ "desktop.updater.installFailed.message": "Nie udało się zainstalować aktualizacji",
+
+ "desktop.cli.installed.title": "CLI zainstalowane",
+ "desktop.cli.installed.message":
+ "CLI zainstalowane w {{path}}\n\nUruchom ponownie terminal, aby użyć polecenia 'opencode'.",
+ "desktop.cli.failed.title": "Instalacja nie powiodła się",
+ "desktop.cli.failed.message": "Nie udało się zainstalować CLI: {{error}}",
+}
diff --git a/packages/desktop-electron/src/renderer/i18n/ru.ts b/packages/desktop-electron/src/renderer/i18n/ru.ts
new file mode 100644
index 000000000..8e09cc45b
--- /dev/null
+++ b/packages/desktop-electron/src/renderer/i18n/ru.ts
@@ -0,0 +1,27 @@
+export const dict = {
+ "desktop.menu.checkForUpdates": "Проверить обновления...",
+ "desktop.menu.installCli": "Установить CLI...",
+ "desktop.menu.reloadWebview": "Перезагрузить Webview",
+ "desktop.menu.restart": "Перезапустить",
+
+ "desktop.dialog.chooseFolder": "Выберите папку",
+ "desktop.dialog.chooseFile": "Выберите файл",
+ "desktop.dialog.saveFile": "Сохранить файл",
+
+ "desktop.updater.checkFailed.title": "Не удалось проверить обновления",
+ "desktop.updater.checkFailed.message": "Не удалось проверить обновления",
+ "desktop.updater.none.title": "Обновлений нет",
+ "desktop.updater.none.message": "Вы уже используете последнюю версию OpenCode",
+ "desktop.updater.downloadFailed.title": "Обновление не удалось",
+ "desktop.updater.downloadFailed.message": "Не удалось скачать обновление",
+ "desktop.updater.downloaded.title": "Обновление загружено",
+ "desktop.updater.downloaded.prompt": "Версия OpenCode {{version}} загружена. Хотите установить и перезапустить?",
+ "desktop.updater.installFailed.title": "Обновление не удалось",
+ "desktop.updater.installFailed.message": "Не удалось установить обновление",
+
+ "desktop.cli.installed.title": "CLI установлен",
+ "desktop.cli.installed.message":
+ "CLI установлен в {{path}}\n\nПерезапустите терминал, чтобы использовать команду 'opencode'.",
+ "desktop.cli.failed.title": "Ошибка установки",
+ "desktop.cli.failed.message": "Не удалось установить CLI: {{error}}",
+}
diff --git a/packages/desktop-electron/src/renderer/i18n/zh.ts b/packages/desktop-electron/src/renderer/i18n/zh.ts
new file mode 100644
index 000000000..aeb3a54e0
--- /dev/null
+++ b/packages/desktop-electron/src/renderer/i18n/zh.ts
@@ -0,0 +1,26 @@
+export const dict = {
+ "desktop.menu.checkForUpdates": "检查更新...",
+ "desktop.menu.installCli": "安装 CLI...",
+ "desktop.menu.reloadWebview": "重新加载 Webview",
+ "desktop.menu.restart": "重启",
+
+ "desktop.dialog.chooseFolder": "选择文件夹",
+ "desktop.dialog.chooseFile": "选择文件",
+ "desktop.dialog.saveFile": "保存文件",
+
+ "desktop.updater.checkFailed.title": "检查更新失败",
+ "desktop.updater.checkFailed.message": "无法检查更新",
+ "desktop.updater.none.title": "没有可用更新",
+ "desktop.updater.none.message": "你已经在使用最新版本的 OpenCode",
+ "desktop.updater.downloadFailed.title": "更新失败",
+ "desktop.updater.downloadFailed.message": "无法下载更新",
+ "desktop.updater.downloaded.title": "更新已下载",
+ "desktop.updater.downloaded.prompt": "已下载 OpenCode {{version}} 版本,是否安装并重启?",
+ "desktop.updater.installFailed.title": "更新失败",
+ "desktop.updater.installFailed.message": "无法安装更新",
+
+ "desktop.cli.installed.title": "CLI 已安装",
+ "desktop.cli.installed.message": "CLI 已安装到 {{path}}\n\n重启终端以使用 'opencode' 命令。",
+ "desktop.cli.failed.title": "安装失败",
+ "desktop.cli.failed.message": "无法安装 CLI: {{error}}",
+}
diff --git a/packages/desktop-electron/src/renderer/i18n/zht.ts b/packages/desktop-electron/src/renderer/i18n/zht.ts
new file mode 100644
index 000000000..7fd677aca
--- /dev/null
+++ b/packages/desktop-electron/src/renderer/i18n/zht.ts
@@ -0,0 +1,26 @@
+export const dict = {
+ "desktop.menu.checkForUpdates": "檢查更新...",
+ "desktop.menu.installCli": "安裝 CLI...",
+ "desktop.menu.reloadWebview": "重新載入 Webview",
+ "desktop.menu.restart": "重新啟動",
+
+ "desktop.dialog.chooseFolder": "選擇資料夾",
+ "desktop.dialog.chooseFile": "選擇檔案",
+ "desktop.dialog.saveFile": "儲存檔案",
+
+ "desktop.updater.checkFailed.title": "檢查更新失敗",
+ "desktop.updater.checkFailed.message": "無法檢查更新",
+ "desktop.updater.none.title": "沒有可用更新",
+ "desktop.updater.none.message": "你已在使用最新版的 OpenCode",
+ "desktop.updater.downloadFailed.title": "更新失敗",
+ "desktop.updater.downloadFailed.message": "無法下載更新",
+ "desktop.updater.downloaded.title": "更新已下載",
+ "desktop.updater.downloaded.prompt": "已下載 OpenCode {{version}} 版本,是否安裝並重新啟動?",
+ "desktop.updater.installFailed.title": "更新失敗",
+ "desktop.updater.installFailed.message": "無法安裝更新",
+
+ "desktop.cli.installed.title": "CLI 已安裝",
+ "desktop.cli.installed.message": "CLI 已安裝到 {{path}}\n\n重新啟動終端機以使用 'opencode' 命令。",
+ "desktop.cli.failed.title": "安裝失敗",
+ "desktop.cli.failed.message": "無法安裝 CLI: {{error}}",
+}
diff --git a/packages/desktop-electron/src/renderer/index.html b/packages/desktop-electron/src/renderer/index.html
new file mode 100644
index 000000000..175640819
--- /dev/null
+++ b/packages/desktop-electron/src/renderer/index.html
@@ -0,0 +1,23 @@
+<!doctype html>
+<html lang="en" style="background-color: var(--background-base)">
+ <head>
+ <meta charset="utf-8" />
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
+ <title>OpenCode</title>
+ <link rel="icon" type="image/png" href="/favicon-96x96-v3.png" sizes="96x96" />
+ <link rel="icon" type="image/svg+xml" href="/favicon-v3.svg" />
+ <link rel="shortcut icon" href="/favicon-v3.ico" />
+ <link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon-v3.png" />
+ <link rel="manifest" href="/site.webmanifest" />
+ <meta name="theme-color" content="#F8F7F7" />
+ <meta name="theme-color" content="#131010" media="(prefers-color-scheme: dark)" />
+ <meta property="og:image" content="/social-share.png" />
+ <meta property="twitter:image" content="/social-share.png" />
+ <script id="oc-theme-preload-script" src="/oc-theme-preload.js"></script>
+ </head>
+ <body class="antialiased overscroll-none text-12-regular overflow-hidden">
+ <noscript>You need to enable JavaScript to run this app.</noscript>
+ <div id="root" class="flex flex-col h-dvh"></div>
+ <script src="/index.tsx" type="module"></script>
+ </body>
+</html>
diff --git a/packages/desktop-electron/src/renderer/index.tsx b/packages/desktop-electron/src/renderer/index.tsx
new file mode 100644
index 000000000..b5193d626
--- /dev/null
+++ b/packages/desktop-electron/src/renderer/index.tsx
@@ -0,0 +1,312 @@
+// @refresh reload
+
+import {
+ AppBaseProviders,
+ AppInterface,
+ handleNotificationClick,
+ type Platform,
+ PlatformProvider,
+ ServerConnection,
+ useCommand,
+} from "@opencode-ai/app"
+import { Splash } from "@opencode-ai/ui/logo"
+import type { AsyncStorage } from "@solid-primitives/storage"
+import { type Accessor, createResource, type JSX, onCleanup, onMount, Show } from "solid-js"
+import { render } from "solid-js/web"
+import { MemoryRouter } from "@solidjs/router"
+import pkg from "../../package.json"
+import { initI18n, t } from "./i18n"
+import { UPDATER_ENABLED } from "./updater"
+import { webviewZoom } from "./webview-zoom"
+import "./styles.css"
+import type { ServerReadyData } from "../preload/types"
+
+const root = document.getElementById("root")
+if (import.meta.env.DEV && !(root instanceof HTMLElement)) {
+ throw new Error(t("error.dev.rootNotFound"))
+}
+
+void initI18n()
+
+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 = () => {
+ const startUrls = window.__OPENCODE__?.deepLinks ?? []
+ if (startUrls.length) emitDeepLinks(startUrls)
+ return window.api.onDeepLink((urls) => emitDeepLinks(urls))
+}
+
+const createPlatform = (): Platform => {
+ 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 wslHome = async () => {
+ if (os !== "windows" || !window.__OPENCODE__?.wsl) return undefined
+ return window.api.wslPath("~", "windows").catch(() => undefined)
+ }
+
+ const handleWslPicker = async <T extends string | string[]>(result: T | null): Promise<T | null> => {
+ if (!result || !window.__OPENCODE__?.wsl) return result
+ if (Array.isArray(result)) {
+ return Promise.all(result.map((path) => window.api.wslPath(path, "linux").catch(() => path))) as any
+ }
+ return window.api.wslPath(result, "linux").catch(() => result) as any
+ }
+
+ const storage = (() => {
+ const cache = new Map<string, AsyncStorage>()
+
+ 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
+ }
+ })()
+
+ return {
+ platform: "desktop",
+ os,
+ version: pkg.version,
+
+ async openDirectoryPickerDialog(opts) {
+ const defaultPath = await wslHome()
+ const result = await window.api.openDirectoryPicker({
+ multiple: opts?.multiple ?? false,
+ title: opts?.title ?? t("desktop.dialog.chooseFolder"),
+ defaultPath,
+ })
+ return await handleWslPicker(result)
+ },
+
+ async openFilePickerDialog(opts) {
+ const result = await window.api.openFilePicker({
+ multiple: opts?.multiple ?? false,
+ title: opts?.title ?? t("desktop.dialog.chooseFile"),
+ })
+ return handleWslPicker(result)
+ },
+
+ async saveFilePickerDialog(opts) {
+ const result = await window.api.saveFilePicker({
+ title: opts?.title ?? t("desktop.dialog.saveFile"),
+ defaultPath: opts?.defaultPath,
+ })
+ return handleWslPicker(result)
+ },
+
+ 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
+ const resolvedPath = await (async () => {
+ if (window.__OPENCODE__?.wsl) {
+ const converted = await window.api.wslPath(path, "windows").catch(() => null)
+ if (converted) return converted
+ }
+ return path
+ })()
+ return window.api.openPath(resolvedPath, resolvedApp ?? undefined)
+ }
+ return window.api.openPath(path, app)
+ },
+
+ back() {
+ window.history.back()
+ },
+
+ forward() {
+ window.history.forward()
+ },
+
+ storage,
+
+ checkUpdate: async () => {
+ if (!UPDATER_ENABLED) return { updateAvailable: false }
+ return window.api.checkUpdate()
+ },
+
+ update: async () => {
+ if (!UPDATER_ENABLED) return
+ await window.api.installUpdate()
+ },
+
+ 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)
+ },
+
+ getWslEnabled: async () => {
+ const next = await window.api.getWslConfig().catch(() => null)
+ if (next) return next.enabled
+ return window.__OPENCODE__!.wsl ?? false
+ },
+
+ setWslEnabled: async (enabled) => {
+ await window.api.setWslConfig({ enabled })
+ },
+
+ getDefaultServerUrl: async () => {
+ return window.api.getDefaultServerUrl().catch(() => null)
+ },
+
+ setDefaultServerUrl: async (url: string | null) => {
+ await window.api.setDefaultServerUrl(url)
+ },
+
+ getDisplayBackend: async () => {
+ return window.api.getDisplayBackend().catch(() => null)
+ },
+
+ setDisplayBackend: async (backend) => {
+ await window.api.setDisplayBackend(backend)
+ },
+
+ parseMarkdown: (markdown: string) => window.api.parseMarkdownCommand(markdown),
+
+ webviewZoom,
+
+ 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()
+
+ 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 (
+ <PlatformProvider value={platform}>
+ <AppBaseProviders>
+ <ServerGate>
+ {(data) => {
+ const server: ServerConnection.Sidecar = {
+ displayName: "Local Server",
+ type: "sidecar",
+ variant: "base",
+ http: {
+ url: data().url,
+ username: "opencode",
+ password: data().password ?? undefined,
+ },
+ }
+
+ function Inner() {
+ const cmd = useCommand()
+
+ menuTrigger = (id) => cmd.trigger(id)
+
+ return null
+ }
+
+ return (
+ <AppInterface defaultServer={ServerConnection.key(server)} servers={[server]} router={MemoryRouter}>
+ <Inner />
+ </AppInterface>
+ )
+ }}
+ </ServerGate>
+ </AppBaseProviders>
+ </PlatformProvider>
+ )
+}, root!)
+
+// Gate component that waits for the server to be ready
+function ServerGate(props: { children: (data: Accessor<ServerReadyData>) => JSX.Element }) {
+ const [serverData] = createResource(() => window.api.awaitInitialization(() => undefined))
+ console.log({ serverData })
+ if (serverData.state === "errored") throw serverData.error
+
+ return (
+ <Show
+ when={serverData.state !== "pending" && serverData()}
+ fallback={
+ <div class="h-screen w-screen flex flex-col items-center justify-center bg-background-base">
+ <Splash class="w-16 h-20 opacity-50 animate-pulse" />
+ </div>
+ }
+ >
+ {(data) => props.children(data)}
+ </Show>
+ )
+}
diff --git a/packages/desktop-electron/src/renderer/loading.html b/packages/desktop-electron/src/renderer/loading.html
new file mode 100644
index 000000000..8def243b4
--- /dev/null
+++ b/packages/desktop-electron/src/renderer/loading.html
@@ -0,0 +1,23 @@
+<!doctype html>
+<html lang="en" style="background-color: var(--background-base)">
+ <head>
+ <meta charset="utf-8" />
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
+ <title>OpenCode</title>
+ <link rel="icon" type="image/png" href="/favicon-96x96-v3.png" sizes="96x96" />
+ <link rel="icon" type="image/svg+xml" href="/favicon-v3.svg" />
+ <link rel="shortcut icon" href="/favicon-v3.ico" />
+ <link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon-v3.png" />
+ <link rel="manifest" href="/site.webmanifest" />
+ <meta name="theme-color" content="#F8F7F7" />
+ <meta name="theme-color" content="#131010" media="(prefers-color-scheme: dark)" />
+ <meta property="og:image" content="/social-share.png" />
+ <meta property="twitter:image" content="/social-share.png" />
+ <script id="oc-theme-preload-script" src="/oc-theme-preload.js"></script>
+ </head>
+ <body class="antialiased overscroll-none text-12-regular overflow-hidden">
+ <noscript>You need to enable JavaScript to run this app.</noscript>
+ <div id="root" class="flex flex-col h-dvh"></div>
+ <script src="/loading.tsx" type="module"></script>
+ </body>
+</html>
diff --git a/packages/desktop-electron/src/renderer/loading.tsx b/packages/desktop-electron/src/renderer/loading.tsx
new file mode 100644
index 000000000..165950352
--- /dev/null
+++ b/packages/desktop-electron/src/renderer/loading.tsx
@@ -0,0 +1,80 @@
+import { render } from "solid-js/web"
+import { MetaProvider } from "@solidjs/meta"
+import "@opencode-ai/app/index.css"
+import { Font } from "@opencode-ai/ui/font"
+import { Splash } from "@opencode-ai/ui/logo"
+import { Progress } from "@opencode-ai/ui/progress"
+import "./styles.css"
+import { createEffect, createMemo, createSignal, onCleanup, onMount } from "solid-js"
+import type { InitStep, SqliteMigrationProgress } from "../preload/types"
+
+const root = document.getElementById("root")!
+const lines = ["Just a moment...", "Migrating your database", "This may take a couple of minutes"]
+const delays = [3000, 9000]
+
+render(() => {
+ const [step, setStep] = createSignal<InitStep | null>(null)
+ const [line, setLine] = createSignal(0)
+ const [percent, setPercent] = createSignal(0)
+
+ const phase = createMemo(() => step()?.phase)
+
+ const value = createMemo(() => {
+ if (phase() === "done") return 100
+ return Math.max(25, Math.min(100, percent()))
+ })
+
+ window.api.awaitInitialization((next) => setStep(next as InitStep)).catch(() => undefined)
+
+ onMount(() => {
+ setLine(0)
+ setPercent(0)
+
+ const timers = delays.map((ms, i) => setTimeout(() => setLine(i + 1), ms))
+
+ const listener = window.api.onSqliteMigrationProgress((progress: SqliteMigrationProgress) => {
+ if (progress.type === "InProgress") setPercent(Math.max(0, Math.min(100, progress.value)))
+ if (progress.type === "Done") setPercent(100)
+ })
+
+ onCleanup(() => {
+ listener()
+ timers.forEach(clearTimeout)
+ })
+ })
+
+ createEffect(() => {
+ if (phase() !== "done") return
+
+ const timer = setTimeout(() => window.api.loadingWindowComplete(), 1000)
+ onCleanup(() => clearTimeout(timer))
+ })
+
+ const status = createMemo(() => {
+ if (phase() === "done") return "All done"
+ if (phase() === "sqlite_waiting") return lines[line()]
+ return "Just a moment..."
+ })
+
+ return (
+ <MetaProvider>
+ <div class="w-screen h-screen bg-background-base flex items-center justify-center">
+ <Font />
+ <div class="flex flex-col items-center gap-11">
+ <Splash class="w-20 h-25 opacity-15" />
+ <div class="w-60 flex flex-col items-center gap-4" aria-live="polite">
+ <span class="w-full overflow-hidden text-center text-ellipsis whitespace-nowrap text-text-strong text-14-normal">
+ {status()}
+ </span>
+ <Progress
+ value={value()}
+ class="w-20 [&_[data-slot='progress-track']]:h-1 [&_[data-slot='progress-track']]:border-0 [&_[data-slot='progress-track']]:rounded-none [&_[data-slot='progress-track']]:bg-surface-weak [&_[data-slot='progress-fill']]:rounded-none [&_[data-slot='progress-fill']]:bg-icon-warning-base"
+ aria-label="Database migration progress"
+ getValueLabel={({ value }) => `${Math.round(value)}%`}
+ />
+ </div>
+ </div>
+ </div>
+ </MetaProvider>
+ )
+}, root)
diff --git a/packages/desktop-electron/src/renderer/styles.css b/packages/desktop-electron/src/renderer/styles.css
new file mode 100644
index 000000000..e69de29bb
--- /dev/null
+++ b/packages/desktop-electron/src/renderer/styles.css
diff --git a/packages/desktop-electron/src/renderer/updater.ts b/packages/desktop-electron/src/renderer/updater.ts
new file mode 100644
index 000000000..fe9e601db
--- /dev/null
+++ b/packages/desktop-electron/src/renderer/updater.ts
@@ -0,0 +1,14 @@
+import { initI18n, t } from "./i18n"
+
+export const UPDATER_ENABLED = window.__OPENCODE__?.updaterEnabled ?? false
+
+export async function runUpdater({ alertOnFail }: { alertOnFail: boolean }) {
+ await initI18n()
+ try {
+ await window.api.runUpdater(alertOnFail)
+ } catch {
+ if (alertOnFail) {
+ window.alert(t("desktop.updater.checkFailed.message"))
+ }
+ }
+}
diff --git a/packages/desktop-electron/src/renderer/webview-zoom.ts b/packages/desktop-electron/src/renderer/webview-zoom.ts
new file mode 100644
index 000000000..9c0a3a3a3
--- /dev/null
+++ b/packages/desktop-electron/src/renderer/webview-zoom.ts
@@ -0,0 +1,38 @@
+// Copyright 2019-2024 Tauri Programme within The Commons Conservancy
+// SPDX-License-Identifier: Apache-2.0
+// SPDX-License-Identifier: MIT
+
+import { createSignal } from "solid-js"
+
+const OS_NAME = (() => {
+ if (navigator.userAgent.includes("Mac")) return "macos"
+ if (navigator.userAgent.includes("Windows")) return "windows"
+ if (navigator.userAgent.includes("Linux")) return "linux"
+ return "unknown"
+})()
+
+const [webviewZoom, setWebviewZoom] = createSignal(1)
+
+const MAX_ZOOM_LEVEL = 10
+const MIN_ZOOM_LEVEL = 0.2
+
+const clamp = (value: number) => Math.min(Math.max(value, MIN_ZOOM_LEVEL), MAX_ZOOM_LEVEL)
+
+const applyZoom = (next: number) => {
+ setWebviewZoom(next)
+ void window.api.setZoomFactor(next)
+}
+
+window.addEventListener("keydown", (event) => {
+ if (!(OS_NAME === "macos" ? event.metaKey : event.ctrlKey)) return
+
+ let newZoom = webviewZoom()
+
+ if (event.key === "-") newZoom -= 0.2
+ if (event.key === "=" || event.key === "+") newZoom += 0.2
+ if (event.key === "0") newZoom = 1
+
+ applyZoom(clamp(newZoom))
+})
+
+export { webviewZoom }