desktop: add electron version (#15663)

This commit is contained in:
Brendan Allan
2026-03-04 15:12:34 +08:00
committed by GitHub
parent e4f0825c56
commit 5cf235fa6c
223 changed files with 4293 additions and 47 deletions

View File

@@ -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) }))
}
}

View File

@@ -0,0 +1,12 @@
import type { ElectronAPI } from "../preload/types"
declare global {
interface Window {
api: ElectronAPI
__OPENCODE__?: {
updaterEnabled?: boolean
wsl?: boolean
deepLinks?: string[]
}
}
}

View File

@@ -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}}",
}

View File

@@ -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}}",
}

View File

@@ -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}}",
}

View File

@@ -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}}",
}

View File

@@ -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}}",
}

View File

@@ -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}}",
}

View File

@@ -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}}",
}

View File

@@ -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}}",
}

View File

@@ -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
}

View File

@@ -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}}",
}

View File

@@ -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}}",
}

View File

@@ -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}}",
}

View File

@@ -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}}",
}

View File

@@ -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}}",
}

View File

@@ -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}}",
}

View File

@@ -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}}",
}

View File

@@ -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>

View File

@@ -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>
)
}

View File

@@ -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>

View File

@@ -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)

View File

@@ -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"))
}
}
}

View File

@@ -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 }