Merge branch 'dev' into brendan/desktop-electron-refactor

This commit is contained in:
Brendan Allan 2026-05-05 13:48:20 +08:00
commit bf353e8c77
No known key found for this signature in database
GPG key ID: 41E835AEA046A32E
509 changed files with 4257 additions and 15298 deletions

View file

@ -1,67 +0,0 @@
// This file has been generated by Tauri Specta. Do not edit this file manually.
import { invoke as __TAURI_INVOKE, Channel } from '@tauri-apps/api/core';
import * as __TAURI_EVENT from "@tauri-apps/api/event";
/** Commands */
export const commands = {
killSidecar: () => __TAURI_INVOKE<void>("kill_sidecar"),
installCli: () => __TAURI_INVOKE<string>("install_cli"),
awaitInitialization: (events: Channel) => __TAURI_INVOKE<ServerReadyData>("await_initialization", { events }),
getDefaultServerUrl: () => __TAURI_INVOKE<string | null>("get_default_server_url"),
setDefaultServerUrl: (url: string | null) => __TAURI_INVOKE<null>("set_default_server_url", { url }),
getWslConfig: () => __TAURI_INVOKE<WslConfig>("get_wsl_config"),
setWslConfig: (config: WslConfig) => __TAURI_INVOKE<null>("set_wsl_config", { config }),
getDisplayBackend: () => __TAURI_INVOKE<"wayland" | "auto" | null>("get_display_backend"),
setDisplayBackend: (backend: LinuxDisplayBackend) => __TAURI_INVOKE<null>("set_display_backend", { backend }),
parseMarkdownCommand: (markdown: string) => __TAURI_INVOKE<string>("parse_markdown_command", { markdown }),
checkAppExists: (appName: string) => __TAURI_INVOKE<boolean>("check_app_exists", { appName }),
wslPath: (path: string, mode: "windows" | "linux" | null) => __TAURI_INVOKE<string>("wsl_path", { path, mode }),
resolveAppPath: (appName: string) => __TAURI_INVOKE<string | null>("resolve_app_path", { appName }),
openPath: (path: string, appName: string | null) => __TAURI_INVOKE<null>("open_path", { path, appName }),
};
/** Events */
export const events = {
loadingWindowComplete: makeEvent<LoadingWindowComplete>("loading-window-complete"),
sqliteMigrationProgress: makeEvent<SqliteMigrationProgress>("sqlite-migration-progress"),
};
/* Types */
export type InitStep = { phase: "server_waiting" } | { phase: "sqlite_waiting" } | { phase: "done" };
export type LinuxDisplayBackend = "wayland" | "auto";
export type LoadingWindowComplete = null;
export type ServerReadyData = {
url: string,
username: string | null,
password: string | null,
};
export type SqliteMigrationProgress = { type: "InProgress"; value: number } | { type: "Done" };
export type WslConfig = {
enabled: boolean,
};
export type WslPathMode = "windows" | "linux";
/* Tauri Specta runtime */
function makeEvent<T>(name: string) {
const base = {
listen: (cb: __TAURI_EVENT.EventCallback<T>) => __TAURI_EVENT.listen(name, cb),
once: (cb: __TAURI_EVENT.EventCallback<T>) => __TAURI_EVENT.once(name, cb),
emit: (payload: T) => __TAURI_EVENT.emit(name, payload) as unknown as (T extends null ? () => Promise<void> : (payload: T) => Promise<void>)
};
const fn = (target: import("@tauri-apps/api/webview").Webview | import("@tauri-apps/api/window").Window) => ({
listen: (cb: __TAURI_EVENT.EventCallback<T>) => target.listen(name, cb),
once: (cb: __TAURI_EVENT.EventCallback<T>) => target.once(name, cb),
emit: (payload: T) => target.emit(name, payload) as unknown as (T extends null ? () => Promise<void> : (payload: T) => Promise<void>)
});
return Object.assign(fn, base);
}

View file

@ -1,43 +0,0 @@
import { message } from "@tauri-apps/plugin-dialog"
import { initI18n, t } from "./i18n"
import { commands } from "./bindings"
function installError(error: unknown) {
const text = String(error)
if (text.includes("CLI installation is only supported on macOS & Linux")) {
return t("desktop.cli.error.unsupportedPlatform")
}
if (text.includes("Sidecar binary not found")) {
return t("desktop.cli.error.sidecarMissing")
}
if (text.includes("Failed to write install script")) {
return t("desktop.cli.error.scriptWriteFailed")
}
if (text.includes("Failed to set script permissions")) {
return t("desktop.cli.error.scriptPermissionFailed")
}
if (text.includes("Failed to run install script")) {
return t("desktop.cli.error.scriptRunFailed")
}
if (text.includes("Install script failed")) {
return t("desktop.cli.error.scriptFailed")
}
if (text.includes("Could not determine install path")) {
return t("desktop.cli.error.installPathUnknown")
}
return text || t("desktop.cli.error.unknown")
}
export async function installCli(): Promise<void> {
await initI18n()
try {
const path = await commands.installCli()
await message(t("desktop.cli.installed.message", { path }), { title: t("desktop.cli.installed.title") })
} catch (e) {
await message(t("desktop.cli.failed.message", { error: installError(e) }), {
title: t("desktop.cli.failed.title"),
})
}
}

View file

@ -1,5 +0,0 @@
if (location.pathname === "/loading") {
void import("./loading")
} else {
void import("./")
}

View file

@ -1,9 +0,0 @@
interface ImportMetaEnv {
readonly VITE_SENTRY_DSN?: string
readonly VITE_SENTRY_ENVIRONMENT?: string
readonly VITE_SENTRY_RELEASE?: string
}
interface ImportMeta {
readonly env: ImportMetaEnv
}

View file

@ -1,59 +0,0 @@
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}}",
"desktop.menu.app": "OpenCode",
"desktop.menu.file": "ملف",
"desktop.menu.edit": "تعديل",
"desktop.menu.view": "عرض",
"desktop.menu.help": "مساعدة",
"desktop.menu.file.newSession": "جلسة جديدة",
"desktop.menu.file.openProject": "فتح مشروع...",
"desktop.menu.view.toggleSidebar": "تبديل الشريط الجانبي",
"desktop.menu.view.toggleTerminal": "تبديل الطرفية",
"desktop.menu.view.toggleFileTree": "تبديل شجرة الملفات",
"desktop.menu.view.back": "رجوع",
"desktop.menu.view.forward": "تقدم",
"desktop.menu.view.previousSession": "الجلسة السابقة",
"desktop.menu.view.nextSession": "الجلسة التالية",
"desktop.menu.help.documentation": "وثائق OpenCode",
"desktop.menu.help.supportForum": "منتدى الدعم",
"desktop.menu.help.shareFeedback": "مشاركة التعليقات",
"desktop.menu.help.reportBug": "الإبلاغ عن خطأ",
"desktop.cli.error.unsupportedPlatform": "تثبيت CLI مدعوم فقط على macOS و Linux.",
"desktop.cli.error.sidecarMissing": "ملف OpenCode CLI الثنائي مفقود. حاول إعادة تثبيت تطبيق سطح المكتب.",
"desktop.cli.error.scriptWriteFailed": "فشل تحضير برنامج تثبيت CLI.",
"desktop.cli.error.scriptPermissionFailed": "فشل جعل برنامج تثبيت CLI قابلاً للتنفيذ.",
"desktop.cli.error.scriptRunFailed": "فشل تشغيل برنامج تثبيت CLI.",
"desktop.cli.error.scriptFailed": "فشل برنامج تثبيت CLI.",
"desktop.cli.error.installPathUnknown": "تعذر تحديد مكان تثبيت CLI.",
"desktop.cli.error.unknown": "خطأ تثبيت غير معروف",
"desktop.loading.status.initial": "لحظة من فضلك...",
"desktop.loading.status.done": "تم الانتهاء",
"desktop.loading.status.migrating": "جارٍ ترحيل قاعدة البيانات الخاصة بك",
"desktop.loading.status.waiting": "قد يستغرق هذا بضع دقائق",
"desktop.loading.progressAria": "تقدم ترحيل قاعدة البيانات",
"desktop.server.local": "خادم محلي",
}

View file

@ -1,61 +0,0 @@
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}}",
"desktop.menu.app": "OpenCode",
"desktop.menu.file": "Arquivo",
"desktop.menu.edit": "Editar",
"desktop.menu.view": "Visualizar",
"desktop.menu.help": "Ajuda",
"desktop.menu.file.newSession": "Nova Sessão",
"desktop.menu.file.openProject": "Abrir Projeto...",
"desktop.menu.view.toggleSidebar": "Alternar Barra Lateral",
"desktop.menu.view.toggleTerminal": "Alternar Terminal",
"desktop.menu.view.toggleFileTree": "Alternar Árvore de Arquivos",
"desktop.menu.view.back": "Voltar",
"desktop.menu.view.forward": "Avançar",
"desktop.menu.view.previousSession": "Sessão Anterior",
"desktop.menu.view.nextSession": "Próxima Sessão",
"desktop.menu.help.documentation": "Documentação do OpenCode",
"desktop.menu.help.supportForum": "Fórum de Suporte",
"desktop.menu.help.shareFeedback": "Compartilhar Feedback",
"desktop.menu.help.reportBug": "Relatar um Bug",
"desktop.cli.error.unsupportedPlatform": "A instalação da CLI é suportada apenas no macOS e Linux.",
"desktop.cli.error.sidecarMissing":
"O binário da CLI do OpenCode está ausente. Tente reinstalar o aplicativo de desktop.",
"desktop.cli.error.scriptWriteFailed": "Falha ao preparar o script de instalação da CLI.",
"desktop.cli.error.scriptPermissionFailed": "Falha ao tornar o script de instalação da CLI executável.",
"desktop.cli.error.scriptRunFailed": "Falha ao executar o script de instalação da CLI.",
"desktop.cli.error.scriptFailed": "O instalador da CLI falhou.",
"desktop.cli.error.installPathUnknown": "Não foi possível determinar onde a CLI foi instalada.",
"desktop.cli.error.unknown": "Erro de instalação desconhecido",
"desktop.loading.status.initial": "Só um momento...",
"desktop.loading.status.done": "Tudo pronto",
"desktop.loading.status.migrating": "Migrando seu banco de dados",
"desktop.loading.status.waiting": "Isso pode levar alguns minutos",
"desktop.loading.progressAria": "Progresso da migração do banco de dados",
"desktop.server.local": "Servidor Local",
}

View file

@ -1,62 +0,0 @@
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}}",
"desktop.menu.app": "OpenCode",
"desktop.menu.file": "Datoteka",
"desktop.menu.edit": "Uredi",
"desktop.menu.view": "Prikaz",
"desktop.menu.help": "Pomoć",
"desktop.menu.file.newSession": "Nova sesija",
"desktop.menu.file.openProject": "Otvori projekat...",
"desktop.menu.view.toggleSidebar": "Prebaci bočnu traku",
"desktop.menu.view.toggleTerminal": "Prebaci terminal",
"desktop.menu.view.toggleFileTree": "Prebaci stablo datoteka",
"desktop.menu.view.back": "Nazad",
"desktop.menu.view.forward": "Naprijed",
"desktop.menu.view.previousSession": "Prethodna sesija",
"desktop.menu.view.nextSession": "Sljedeća sesija",
"desktop.menu.help.documentation": "OpenCode Dokumentacija",
"desktop.menu.help.supportForum": "Forum za podršku",
"desktop.menu.help.shareFeedback": "Podijeli povratne informacije",
"desktop.menu.help.reportBug": "Prijavi grešku",
"desktop.cli.error.unsupportedPlatform": "Instalacija CLI-a je podržana samo na macOS-u i Linux-u.",
"desktop.cli.error.sidecarMissing":
"Nedostaje binarna datoteka OpenCode CLI-a. Pokušaj ponovo instalirati desktop aplikaciju.",
"desktop.cli.error.scriptWriteFailed": "Nije uspjela priprema skripte za instalaciju CLI-a.",
"desktop.cli.error.scriptPermissionFailed": "Nije uspjelo postavljanje izvršnih dozvola za instalaciju CLI-a.",
"desktop.cli.error.scriptRunFailed": "Nije uspjelo pokretanje skripte za instalaciju CLI-a.",
"desktop.cli.error.scriptFailed": "Instalacija CLI-a nije uspjela.",
"desktop.cli.error.installPathUnknown": "Nije bilo moguće utvrditi gdje je instaliran CLI.",
"desktop.cli.error.unknown": "Nepoznata greška pri instalaciji",
"desktop.loading.status.initial": "Samo trenutak...",
"desktop.loading.status.done": "Sve je gotovo",
"desktop.loading.status.migrating": "Migracija baze podataka u toku",
"desktop.loading.status.waiting": "Ovo može potrajati nekoliko minuta",
"desktop.loading.progressAria": "Napredak migracije baze podataka",
"desktop.server.local": "Lokalni server",
}

View file

@ -1,61 +0,0 @@
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}}",
"desktop.menu.app": "OpenCode",
"desktop.menu.file": "Filer",
"desktop.menu.edit": "Rediger",
"desktop.menu.view": "Vis",
"desktop.menu.help": "Hjælp",
"desktop.menu.file.newSession": "Ny session",
"desktop.menu.file.openProject": "Åbn projekt...",
"desktop.menu.view.toggleSidebar": "Slå sidepanel til/fra",
"desktop.menu.view.toggleTerminal": "Slå terminal til/fra",
"desktop.menu.view.toggleFileTree": "Slå filoversigt til/fra",
"desktop.menu.view.back": "Tilbage",
"desktop.menu.view.forward": "Fremad",
"desktop.menu.view.previousSession": "Forrige session",
"desktop.menu.view.nextSession": "Næste session",
"desktop.menu.help.documentation": "OpenCode Dokumentation",
"desktop.menu.help.supportForum": "Supportforum",
"desktop.menu.help.shareFeedback": "Del feedback",
"desktop.menu.help.reportBug": "Rapporter en fejl",
"desktop.cli.error.unsupportedPlatform": "CLI-installation understøttes kun på macOS og Linux.",
"desktop.cli.error.sidecarMissing": "OpenCode CLI-binærfil mangler. Prøv at geninstallere desktop-appen.",
"desktop.cli.error.scriptWriteFailed": "Kunne ikke forberede CLI-installationsscriptet.",
"desktop.cli.error.scriptPermissionFailed": "Kunne ikke gøre CLI-installationsscriptet eksekverbart.",
"desktop.cli.error.scriptRunFailed": "Kunne ikke køre CLI-installationsscriptet.",
"desktop.cli.error.scriptFailed": "CLI-installationsprogrammet mislykkedes.",
"desktop.cli.error.installPathUnknown": "Kunne ikke fastslå, hvor CLI'en blev installeret.",
"desktop.cli.error.unknown": "Ukendt installationsfejl",
"desktop.loading.status.initial": "Lige et øjeblik...",
"desktop.loading.status.done": "Helt færdig",
"desktop.loading.status.migrating": "Migrerer din database",
"desktop.loading.status.waiting": "Dette kan tage et par minutter",
"desktop.loading.progressAria": "Status for databasemigrering",
"desktop.server.local": "Lokal server",
}

View file

@ -1,62 +0,0 @@
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}}",
"desktop.menu.app": "OpenCode",
"desktop.menu.file": "Datei",
"desktop.menu.edit": "Bearbeiten",
"desktop.menu.view": "Ansicht",
"desktop.menu.help": "Hilfe",
"desktop.menu.file.newSession": "Neue Sitzung",
"desktop.menu.file.openProject": "Projekt öffnen...",
"desktop.menu.view.toggleSidebar": "Seitenleiste umschalten",
"desktop.menu.view.toggleTerminal": "Terminal umschalten",
"desktop.menu.view.toggleFileTree": "Dateibaum umschalten",
"desktop.menu.view.back": "Zurück",
"desktop.menu.view.forward": "Vorwärts",
"desktop.menu.view.previousSession": "Vorherige Sitzung",
"desktop.menu.view.nextSession": "Nächste Sitzung",
"desktop.menu.help.documentation": "OpenCode-Dokumentation",
"desktop.menu.help.supportForum": "Support-Forum",
"desktop.menu.help.shareFeedback": "Feedback teilen",
"desktop.menu.help.reportBug": "Einen Fehler melden",
"desktop.cli.error.unsupportedPlatform": "Die CLI-Installation wird nur unter macOS und Linux unterstützt.",
"desktop.cli.error.sidecarMissing":
"Das OpenCode CLI-Binary fehlt. Versuchen Sie, die Desktop-App neu zu installieren.",
"desktop.cli.error.scriptWriteFailed": "Das CLI-Installationsskript konnte nicht vorbereitet werden.",
"desktop.cli.error.scriptPermissionFailed": "Das CLI-Installationsskript konnte nicht ausführbar gemacht werden.",
"desktop.cli.error.scriptRunFailed": "Das CLI-Installationsskript konnte nicht ausgeführt werden.",
"desktop.cli.error.scriptFailed": "Das CLI-Installationsprogramm ist fehlgeschlagen.",
"desktop.cli.error.installPathUnknown": "Es konnte nicht ermittelt werden, wo die CLI installiert wurde.",
"desktop.cli.error.unknown": "Unbekannter Installationsfehler",
"desktop.loading.status.initial": "Einen Moment bitte...",
"desktop.loading.status.done": "Alles erledigt",
"desktop.loading.status.migrating": "Ihre Datenbank wird migriert",
"desktop.loading.status.waiting": "Dies kann einige Minuten dauern",
"desktop.loading.progressAria": "Fortschritt der Datenbankmigration",
"desktop.server.local": "Lokaler Server",
}

View file

@ -1,61 +0,0 @@
export const dict = {
"desktop.menu.checkForUpdates": "Check for Updates...",
"desktop.menu.installCli": "Install CLI...",
"desktop.menu.reloadWebview": "Reload Webview",
"desktop.menu.restart": "Restart",
"desktop.menu.app": "OpenCode",
"desktop.menu.file": "File",
"desktop.menu.edit": "Edit",
"desktop.menu.view": "View",
"desktop.menu.help": "Help",
"desktop.menu.file.newSession": "New Session",
"desktop.menu.file.openProject": "Open Project...",
"desktop.menu.view.toggleSidebar": "Toggle Sidebar",
"desktop.menu.view.toggleTerminal": "Toggle Terminal",
"desktop.menu.view.toggleFileTree": "Toggle File Tree",
"desktop.menu.view.back": "Back",
"desktop.menu.view.forward": "Forward",
"desktop.menu.view.previousSession": "Previous Session",
"desktop.menu.view.nextSession": "Next Session",
"desktop.menu.help.documentation": "OpenCode Documentation",
"desktop.menu.help.supportForum": "Support Forum",
"desktop.menu.help.shareFeedback": "Share Feedback",
"desktop.menu.help.reportBug": "Report a Bug",
"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}}",
"desktop.cli.error.unsupportedPlatform": "CLI installation is only supported on macOS and Linux.",
"desktop.cli.error.sidecarMissing": "OpenCode CLI binary is missing. Try reinstalling the desktop app.",
"desktop.cli.error.scriptWriteFailed": "Failed to prepare CLI installer script.",
"desktop.cli.error.scriptPermissionFailed": "Failed to make CLI installer executable.",
"desktop.cli.error.scriptRunFailed": "Failed to run CLI installer script.",
"desktop.cli.error.scriptFailed": "CLI installer failed.",
"desktop.cli.error.installPathUnknown": "Could not determine where the CLI was installed.",
"desktop.cli.error.unknown": "Unknown installation error",
"desktop.loading.status.initial": "Just a moment...",
"desktop.loading.status.done": "All done",
"desktop.loading.status.migrating": "Migrating your database",
"desktop.loading.status.waiting": "This may take a couple of minutes",
"desktop.loading.progressAria": "Database migration progress",
"desktop.server.local": "Local Server",
}

View file

@ -1,61 +0,0 @@
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}}",
"desktop.menu.app": "OpenCode",
"desktop.menu.file": "Archivo",
"desktop.menu.edit": "Editar",
"desktop.menu.view": "Ver",
"desktop.menu.help": "Ayuda",
"desktop.menu.file.newSession": "Nueva sesión",
"desktop.menu.file.openProject": "Abrir proyecto...",
"desktop.menu.view.toggleSidebar": "Alternar barra lateral",
"desktop.menu.view.toggleTerminal": "Alternar terminal",
"desktop.menu.view.toggleFileTree": "Alternar árbol de archivos",
"desktop.menu.view.back": "Atrás",
"desktop.menu.view.forward": "Adelante",
"desktop.menu.view.previousSession": "Sesión anterior",
"desktop.menu.view.nextSession": "Siguiente sesión",
"desktop.menu.help.documentation": "Documentación de OpenCode",
"desktop.menu.help.supportForum": "Foro de soporte",
"desktop.menu.help.shareFeedback": "Compartir comentarios",
"desktop.menu.help.reportBug": "Informar de un error",
"desktop.cli.error.unsupportedPlatform": "La instalación de la CLI solo es compatible con macOS y Linux.",
"desktop.cli.error.sidecarMissing":
"Falta el binario de la CLI de OpenCode. Intenta reinstalar la aplicación de escritorio.",
"desktop.cli.error.scriptWriteFailed": "No se pudo preparar el script del instalador de la CLI.",
"desktop.cli.error.scriptPermissionFailed": "No se pudo hacer ejecutable el script del instalador de la CLI.",
"desktop.cli.error.scriptRunFailed": "No se pudo ejecutar el script del instalador de la CLI.",
"desktop.cli.error.scriptFailed": "El instalador de la CLI falló.",
"desktop.cli.error.installPathUnknown": "No se pudo determinar dónde se instaló la CLI.",
"desktop.cli.error.unknown": "Error de instalación desconocido",
"desktop.loading.status.initial": "Un momento...",
"desktop.loading.status.done": "Todo listo",
"desktop.loading.status.migrating": "Migrando tu base de datos",
"desktop.loading.status.waiting": "Esto puede tardar unos minutos",
"desktop.loading.progressAria": "Progreso de migración de la base de datos",
"desktop.server.local": "Servidor local",
}

View file

@ -1,62 +0,0 @@
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}}",
"desktop.menu.app": "OpenCode",
"desktop.menu.file": "Fichier",
"desktop.menu.edit": "Édition",
"desktop.menu.view": "Affichage",
"desktop.menu.help": "Aide",
"desktop.menu.file.newSession": "Nouvelle session",
"desktop.menu.file.openProject": "Ouvrir un projet...",
"desktop.menu.view.toggleSidebar": "Basculer la barre latérale",
"desktop.menu.view.toggleTerminal": "Basculer le terminal",
"desktop.menu.view.toggleFileTree": "Basculer l'arborescence des fichiers",
"desktop.menu.view.back": "Retour",
"desktop.menu.view.forward": "Suivant",
"desktop.menu.view.previousSession": "Session précédente",
"desktop.menu.view.nextSession": "Session suivante",
"desktop.menu.help.documentation": "Documentation d'OpenCode",
"desktop.menu.help.supportForum": "Forum d'assistance",
"desktop.menu.help.shareFeedback": "Partager des commentaires",
"desktop.menu.help.reportBug": "Signaler un bug",
"desktop.cli.error.unsupportedPlatform": "L'installation de la CLI n'est prise en charge que sur macOS et Linux.",
"desktop.cli.error.sidecarMissing":
"Le binaire de la CLI OpenCode est manquant. Essayez de réinstaller l'application de bureau.",
"desktop.cli.error.scriptWriteFailed": "Impossible de préparer le script d'installation de la CLI.",
"desktop.cli.error.scriptPermissionFailed": "Impossible de rendre le script d'installation de la CLI exécutable.",
"desktop.cli.error.scriptRunFailed": "Impossible d'exécuter le script d'installation de la CLI.",
"desktop.cli.error.scriptFailed": "L'installateur de la CLI a échoué.",
"desktop.cli.error.installPathUnknown": "Impossible de déterminer où la CLI a été installée.",
"desktop.cli.error.unknown": "Erreur d'installation inconnue",
"desktop.loading.status.initial": "Un instant...",
"desktop.loading.status.done": "Terminé",
"desktop.loading.status.migrating": "Migration de votre base de données",
"desktop.loading.status.waiting": "Cela peut prendre quelques minutes",
"desktop.loading.progressAria": "Progression de la migration de la base de données",
"desktop.server.local": "Serveur local",
}

View file

@ -1,62 +0,0 @@
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}}",
"desktop.menu.app": "OpenCode",
"desktop.menu.file": "ファイル",
"desktop.menu.edit": "編集",
"desktop.menu.view": "表示",
"desktop.menu.help": "ヘルプ",
"desktop.menu.file.newSession": "新しいセッション",
"desktop.menu.file.openProject": "プロジェクトを開く...",
"desktop.menu.view.toggleSidebar": "サイドバーの切り替え",
"desktop.menu.view.toggleTerminal": "ターミナルの切り替え",
"desktop.menu.view.toggleFileTree": "ファイルツリーの切り替え",
"desktop.menu.view.back": "戻る",
"desktop.menu.view.forward": "進む",
"desktop.menu.view.previousSession": "前のセッション",
"desktop.menu.view.nextSession": "次のセッション",
"desktop.menu.help.documentation": "OpenCode ドキュメント",
"desktop.menu.help.supportForum": "サポートフォーラム",
"desktop.menu.help.shareFeedback": "フィードバックを共有",
"desktop.menu.help.reportBug": "バグを報告",
"desktop.cli.error.unsupportedPlatform": "CLI のインストールは macOS と Linux のみでサポートされています。",
"desktop.cli.error.sidecarMissing":
"OpenCode CLI のバイナリが見つかりません。デスクトップアプリを再インストールしてみてください。",
"desktop.cli.error.scriptWriteFailed": "CLI インストーラースクリプトの準備に失敗しました。",
"desktop.cli.error.scriptPermissionFailed": "CLI インストーラースクリプトに実行権限を付与できませんでした。",
"desktop.cli.error.scriptRunFailed": "CLI インストーラースクリプトの実行に失敗しました。",
"desktop.cli.error.scriptFailed": "CLI インストーラーが失敗しました。",
"desktop.cli.error.installPathUnknown": "CLI がどこにインストールされたか特定できませんでした。",
"desktop.cli.error.unknown": "不明なインストールエラー",
"desktop.loading.status.initial": "少々お待ちください...",
"desktop.loading.status.done": "完了しました",
"desktop.loading.status.migrating": "データベースを移行しています",
"desktop.loading.status.waiting": "これには数分かかる場合があります",
"desktop.loading.progressAria": "データベース移行の進行状況",
"desktop.server.local": "ローカルサーバー",
}

View file

@ -1,60 +0,0 @@
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}}",
"desktop.menu.app": "OpenCode",
"desktop.menu.file": "파일",
"desktop.menu.edit": "편집",
"desktop.menu.view": "보기",
"desktop.menu.help": "도움말",
"desktop.menu.file.newSession": "새 세션",
"desktop.menu.file.openProject": "프로젝트 열기...",
"desktop.menu.view.toggleSidebar": "사이드바 전환",
"desktop.menu.view.toggleTerminal": "터미널 전환",
"desktop.menu.view.toggleFileTree": "파일 트리 전환",
"desktop.menu.view.back": "뒤로",
"desktop.menu.view.forward": "앞으로",
"desktop.menu.view.previousSession": "이전 세션",
"desktop.menu.view.nextSession": "다음 세션",
"desktop.menu.help.documentation": "OpenCode 문서",
"desktop.menu.help.supportForum": "지원 포럼",
"desktop.menu.help.shareFeedback": "피드백 공유",
"desktop.menu.help.reportBug": "버그 신고",
"desktop.cli.error.unsupportedPlatform": "CLI 설치는 macOS 및 Linux에서만 지원됩니다.",
"desktop.cli.error.sidecarMissing": "OpenCode CLI 바이너리가 누락되었습니다. 데스크톱 앱을 다시 설치해 보세요.",
"desktop.cli.error.scriptWriteFailed": "CLI 설치 스크립트를 준비하지 못했습니다.",
"desktop.cli.error.scriptPermissionFailed": "CLI 설치 스크립트를 실행 가능하게 만들지 못했습니다.",
"desktop.cli.error.scriptRunFailed": "CLI 설치 스크립트를 실행하지 못했습니다.",
"desktop.cli.error.scriptFailed": "CLI 설치 프로그램이 실패했습니다.",
"desktop.cli.error.installPathUnknown": "CLI가 어디에 설치되었는지 확인할 수 없습니다.",
"desktop.cli.error.unknown": "알 수 없는 설치 오류",
"desktop.loading.status.initial": "잠시만 기다려 주세요...",
"desktop.loading.status.done": "모두 완료되었습니다",
"desktop.loading.status.migrating": "데이터베이스 마이그레이션 중",
"desktop.loading.status.waiting": "이 작업은 몇 분 정도 걸릴 수 있습니다",
"desktop.loading.progressAria": "데이터베이스 마이그레이션 진행률",
"desktop.server.local": "로컬 서버",
}

View file

@ -1,61 +0,0 @@
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}}",
"desktop.menu.app": "OpenCode",
"desktop.menu.file": "Fil",
"desktop.menu.edit": "Rediger",
"desktop.menu.view": "Vis",
"desktop.menu.help": "Hjelp",
"desktop.menu.file.newSession": "Ny sesjon",
"desktop.menu.file.openProject": "Åpne prosjekt...",
"desktop.menu.view.toggleSidebar": "Vis/skjul sidefelt",
"desktop.menu.view.toggleTerminal": "Vis/skjul terminal",
"desktop.menu.view.toggleFileTree": "Vis/skjul filtre",
"desktop.menu.view.back": "Tilbake",
"desktop.menu.view.forward": "Frem",
"desktop.menu.view.previousSession": "Forrige sesjon",
"desktop.menu.view.nextSession": "Neste sesjon",
"desktop.menu.help.documentation": "OpenCode Dokumentasjon",
"desktop.menu.help.supportForum": "Støtteforum",
"desktop.menu.help.shareFeedback": "Del tilbakemelding",
"desktop.menu.help.reportBug": "Rapporter en feil",
"desktop.cli.error.unsupportedPlatform": "CLI-installasjon støttes kun på macOS og Linux.",
"desktop.cli.error.sidecarMissing": "OpenCode CLI-binærfil mangler. Prøv å installere skrivebordsappen på nytt.",
"desktop.cli.error.scriptWriteFailed": "Kunne ikke klargjøre CLI-installasjonsskriptet.",
"desktop.cli.error.scriptPermissionFailed": "Kunne ikke gjøre CLI-installasjonsskriptet kjørbart.",
"desktop.cli.error.scriptRunFailed": "Kunne ikke kjøre CLI-installasjonsskriptet.",
"desktop.cli.error.scriptFailed": "CLI-installasjonsprogrammet mislyktes.",
"desktop.cli.error.installPathUnknown": "Kunne ikke avgjøre hvor CLI ble installert.",
"desktop.cli.error.unknown": "Ukjent installasjonsfeil",
"desktop.loading.status.initial": "Et øyeblikk...",
"desktop.loading.status.done": "Alt ferdig",
"desktop.loading.status.migrating": "Migrerer databasen din",
"desktop.loading.status.waiting": "Dette kan ta et par minutter",
"desktop.loading.progressAria": "Fremdrift for databasemigrering",
"desktop.server.local": "Lokal server",
}

View file

@ -1,62 +0,0 @@
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}}",
"desktop.menu.app": "OpenCode",
"desktop.menu.file": "Plik",
"desktop.menu.edit": "Edycja",
"desktop.menu.view": "Widok",
"desktop.menu.help": "Pomoc",
"desktop.menu.file.newSession": "Nowa sesja",
"desktop.menu.file.openProject": "Otwórz projekt...",
"desktop.menu.view.toggleSidebar": "Przełącz pasek boczny",
"desktop.menu.view.toggleTerminal": "Przełącz terminal",
"desktop.menu.view.toggleFileTree": "Przełącz drzewo plików",
"desktop.menu.view.back": "Wstecz",
"desktop.menu.view.forward": "Dalej",
"desktop.menu.view.previousSession": "Poprzednia sesja",
"desktop.menu.view.nextSession": "Następna sesja",
"desktop.menu.help.documentation": "Dokumentacja OpenCode",
"desktop.menu.help.supportForum": "Forum wsparcia",
"desktop.menu.help.shareFeedback": "Prześlij opinię",
"desktop.menu.help.reportBug": "Zgłoś błąd",
"desktop.cli.error.unsupportedPlatform": "Instalacja CLI jest obsługiwana tylko na macOS i Linux.",
"desktop.cli.error.sidecarMissing":
"Brakuje pliku binarnego OpenCode CLI. Spróbuj ponownie zainstalować aplikację na komputer.",
"desktop.cli.error.scriptWriteFailed": "Nie udało się przygotować skryptu instalatora CLI.",
"desktop.cli.error.scriptPermissionFailed": "Nie udało się nadać uprawnień do wykonania skryptu instalatora CLI.",
"desktop.cli.error.scriptRunFailed": "Nie udało się uruchomić skryptu instalatora CLI.",
"desktop.cli.error.scriptFailed": "Instalator CLI nie powiódł się.",
"desktop.cli.error.installPathUnknown": "Nie udało się ustalić, gdzie zostało zainstalowane CLI.",
"desktop.cli.error.unknown": "Nieznany błąd instalacji",
"desktop.loading.status.initial": "Chwileczkę...",
"desktop.loading.status.done": "Gotowe",
"desktop.loading.status.migrating": "Migrowanie bazy danych",
"desktop.loading.status.waiting": "Może to potrwać kilka minut",
"desktop.loading.progressAria": "Postęp migracji bazy danych",
"desktop.server.local": "Serwer lokalny",
}

View file

@ -1,61 +0,0 @@
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}}",
"desktop.menu.app": "OpenCode",
"desktop.menu.file": "Файл",
"desktop.menu.edit": "Правка",
"desktop.menu.view": "Вид",
"desktop.menu.help": "Справка",
"desktop.menu.file.newSession": "Новая сессия",
"desktop.menu.file.openProject": "Открыть проект...",
"desktop.menu.view.toggleSidebar": "Переключить боковую панель",
"desktop.menu.view.toggleTerminal": "Переключить терминал",
"desktop.menu.view.toggleFileTree": "Переключить дерево файлов",
"desktop.menu.view.back": "Назад",
"desktop.menu.view.forward": "Вперед",
"desktop.menu.view.previousSession": "Предыдущая сессия",
"desktop.menu.view.nextSession": "Следующая сессия",
"desktop.menu.help.documentation": "Документация OpenCode",
"desktop.menu.help.supportForum": "Форум поддержки",
"desktop.menu.help.shareFeedback": "Поделиться отзывом",
"desktop.menu.help.reportBug": "Сообщить об ошибке",
"desktop.cli.error.unsupportedPlatform": "Установка CLI поддерживается только в macOS и Linux.",
"desktop.cli.error.sidecarMissing":
"Отсутствует бинарный файл OpenCode CLI. Попробуйте переустановить настольное приложение.",
"desktop.cli.error.scriptWriteFailed": "Не удалось подготовить скрипт установщика CLI.",
"desktop.cli.error.scriptPermissionFailed": "Не удалось сделать скрипт установщика CLI исполняемым.",
"desktop.cli.error.scriptRunFailed": "Не удалось запустить скрипт установщика CLI.",
"desktop.cli.error.scriptFailed": "Ошибка установщика CLI.",
"desktop.cli.error.installPathUnknown": "Не удалось определить, куда был установлен CLI.",
"desktop.cli.error.unknown": "Неизвестная ошибка установки",
"desktop.loading.status.initial": "Минуточку...",
"desktop.loading.status.done": "Всё готово",
"desktop.loading.status.migrating": "Миграция вашей базы данных",
"desktop.loading.status.waiting": "Это может занять пару минут",
"desktop.loading.progressAria": "Прогресс миграции базы данных",
"desktop.server.local": "Локальный сервер",
}

View file

@ -1,59 +0,0 @@
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}}",
"desktop.menu.app": "OpenCode",
"desktop.menu.file": "文件",
"desktop.menu.edit": "编辑",
"desktop.menu.view": "查看",
"desktop.menu.help": "帮助",
"desktop.menu.file.newSession": "新会话",
"desktop.menu.file.openProject": "打开项目...",
"desktop.menu.view.toggleSidebar": "切换侧边栏",
"desktop.menu.view.toggleTerminal": "切换终端",
"desktop.menu.view.toggleFileTree": "切换文件树",
"desktop.menu.view.back": "后退",
"desktop.menu.view.forward": "前进",
"desktop.menu.view.previousSession": "上一个会话",
"desktop.menu.view.nextSession": "下一个会话",
"desktop.menu.help.documentation": "OpenCode 文档",
"desktop.menu.help.supportForum": "支持论坛",
"desktop.menu.help.shareFeedback": "分享反馈",
"desktop.menu.help.reportBug": "报告错误",
"desktop.cli.error.unsupportedPlatform": "CLI 安装仅在 macOS 和 Linux 上受支持。",
"desktop.cli.error.sidecarMissing": "OpenCode CLI 二进制文件缺失。请尝试重新安装桌面应用程序。",
"desktop.cli.error.scriptWriteFailed": "无法准备 CLI 安装脚本。",
"desktop.cli.error.scriptPermissionFailed": "无法使 CLI 安装脚本可执行。",
"desktop.cli.error.scriptRunFailed": "无法运行 CLI 安装脚本。",
"desktop.cli.error.scriptFailed": "CLI 安装程序失败。",
"desktop.cli.error.installPathUnknown": "无法确定 CLI 的安装位置。",
"desktop.cli.error.unknown": "未知的安装错误",
"desktop.loading.status.initial": "稍等片刻...",
"desktop.loading.status.done": "全部完成",
"desktop.loading.status.migrating": "正在迁移您的数据库",
"desktop.loading.status.waiting": "这可能需要几分钟",
"desktop.loading.progressAria": "数据库迁移进度",
"desktop.server.local": "本地服务器",
}

View file

@ -1,59 +0,0 @@
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}}",
"desktop.menu.app": "OpenCode",
"desktop.menu.file": "檔案",
"desktop.menu.edit": "編輯",
"desktop.menu.view": "檢視",
"desktop.menu.help": "說明",
"desktop.menu.file.newSession": "新工作階段",
"desktop.menu.file.openProject": "開啟專案...",
"desktop.menu.view.toggleSidebar": "切換側邊欄",
"desktop.menu.view.toggleTerminal": "切換終端機",
"desktop.menu.view.toggleFileTree": "切換檔案樹",
"desktop.menu.view.back": "上一步",
"desktop.menu.view.forward": "下一步",
"desktop.menu.view.previousSession": "上一個工作階段",
"desktop.menu.view.nextSession": "下一個工作階段",
"desktop.menu.help.documentation": "OpenCode 文件",
"desktop.menu.help.supportForum": "支援論壇",
"desktop.menu.help.shareFeedback": "分享意見回饋",
"desktop.menu.help.reportBug": "回報錯誤",
"desktop.cli.error.unsupportedPlatform": "CLI 安裝僅支援 macOS 與 Linux。",
"desktop.cli.error.sidecarMissing": "OpenCode CLI 執行檔遺失。請嘗試重新安裝桌面應用程式。",
"desktop.cli.error.scriptWriteFailed": "無法準備 CLI 安裝指令碼。",
"desktop.cli.error.scriptPermissionFailed": "無法將 CLI 安裝指令碼設為可執行。",
"desktop.cli.error.scriptRunFailed": "無法執行 CLI 安裝指令碼。",
"desktop.cli.error.scriptFailed": "CLI 安裝程式失敗。",
"desktop.cli.error.installPathUnknown": "無法確定 CLI 的安裝位置。",
"desktop.cli.error.unknown": "未知的安裝錯誤",
"desktop.loading.status.initial": "稍等片刻...",
"desktop.loading.status.done": "全部完成",
"desktop.loading.status.migrating": "正在移轉您的資料庫",
"desktop.loading.status.waiting": "這可能需要幾分鐘",
"desktop.loading.progressAria": "資料庫移轉進度",
"desktop.server.local": "本機伺服器",
}

View file

@ -1,505 +0,0 @@
// @refresh reload
import {
ACCEPTED_FILE_EXTENSIONS,
filePickerFilters,
AppBaseProviders,
AppInterface,
handleNotificationClick,
loadLocaleDict,
normalizeLocale,
type Locale,
type Platform,
PlatformProvider,
ServerConnection,
useCommand,
} from "@opencode-ai/app"
import * as Sentry from "@sentry/solid"
import type { AsyncStorage } from "@solid-primitives/storage"
import { getCurrentWindow } from "@tauri-apps/api/window"
import { readImage } from "@tauri-apps/plugin-clipboard-manager"
import { getCurrent, onOpenUrl } from "@tauri-apps/plugin-deep-link"
import { open, save } from "@tauri-apps/plugin-dialog"
import { fetch as tauriFetch } from "@tauri-apps/plugin-http"
import { isPermissionGranted, requestPermission } from "@tauri-apps/plugin-notification"
import { type as ostype } from "@tauri-apps/plugin-os"
import { relaunch } from "@tauri-apps/plugin-process"
import { open as shellOpen } from "@tauri-apps/plugin-shell"
import { Store } from "@tauri-apps/plugin-store"
import { check, type Update } from "@tauri-apps/plugin-updater"
import { createResource, onCleanup, onMount, Show } from "solid-js"
import { render } from "solid-js/web"
import pkg from "../package.json"
import { initI18n, t } from "./i18n"
import { UPDATER_ENABLED } from "./updater"
import { webviewZoom } from "./webview-zoom"
import "./styles.css"
import { Channel } from "@tauri-apps/api/core"
import { commands, type InitStep } from "./bindings"
import { createMenu } from "./menu"
const root = document.getElementById("root")
if (import.meta.env.DEV && !(root instanceof HTMLElement)) {
throw new Error(t("error.dev.rootNotFound"))
}
void initI18n()
let update: Update | null = null
const deepLinkEvent = "opencode:deep-link"
const emitDeepLinks = (urls: string[]) => {
if (urls.length === 0) return
window.__OPENCODE__ ??= {}
const pending = window.__OPENCODE__.deepLinks ?? []
window.__OPENCODE__.deepLinks = [...pending, ...urls]
window.dispatchEvent(new CustomEvent(deepLinkEvent, { detail: { urls } }))
}
const listenForDeepLinks = async () => {
const startUrls = await getCurrent().catch(() => null)
if (startUrls?.length) emitDeepLinks(startUrls)
await onOpenUrl((urls) => emitDeepLinks(urls)).catch(() => undefined)
}
const createPlatform = (): Platform => {
const os = (() => {
const type = ostype()
if (type === "macos" || type === "windows" || type === "linux") return type
return undefined
})()
const wslHome = async () => {
if (os !== "windows" || !window.__OPENCODE__?.wsl) return undefined
return commands.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) => commands.wslPath(path, "linux").catch(() => path))) as any
}
return commands.wslPath(result, "linux").catch(() => result) as any
}
return {
platform: "desktop",
os,
version: pkg.version,
async openDirectoryPickerDialog(opts) {
const defaultPath = await wslHome()
const result = await open({
directory: true,
multiple: opts?.multiple ?? false,
title: opts?.title ?? t("desktop.dialog.chooseFolder"),
defaultPath,
})
return await handleWslPicker(result)
},
async openFilePickerDialog(opts) {
const result = await open({
directory: false,
multiple: opts?.multiple ?? false,
title: opts?.title ?? t("desktop.dialog.chooseFile"),
filters: filePickerFilters(opts?.extensions ?? ACCEPTED_FILE_EXTENSIONS),
})
return handleWslPicker(result)
},
async saveFilePickerDialog(opts) {
const result = await save({
title: opts?.title ?? t("desktop.dialog.saveFile"),
defaultPath: opts?.defaultPath,
})
return handleWslPicker(result)
},
openLink(url: string) {
void shellOpen(url).catch(() => undefined)
},
async openPath(path: string, app?: string) {
await commands.openPath(path, app ?? null)
},
back() {
window.history.back()
},
forward() {
window.history.forward()
},
storage: (() => {
type StoreLike = {
get(key: string): Promise<string | null | undefined>
set(key: string, value: string): Promise<unknown>
delete(key: string): Promise<unknown>
clear(): Promise<unknown>
keys(): Promise<string[]>
length(): Promise<number>
}
const WRITE_DEBOUNCE_MS = 250
const storeCache = new Map<string, Promise<StoreLike>>()
const apiCache = new Map<string, AsyncStorage & { flush: () => Promise<void> }>()
const memoryCache = new Map<string, StoreLike>()
const flushAll = async () => {
const apis = Array.from(apiCache.values())
await Promise.all(apis.map((api) => api.flush().catch(() => undefined)))
}
if ("addEventListener" in globalThis) {
const handleVisibility = () => {
if (document.visibilityState !== "hidden") return
void flushAll()
}
window.addEventListener("pagehide", () => void flushAll())
document.addEventListener("visibilitychange", handleVisibility)
}
const createMemoryStore = () => {
const data = new Map<string, string>()
const store: StoreLike = {
get: async (key) => data.get(key),
set: async (key, value) => {
data.set(key, value)
},
delete: async (key) => {
data.delete(key)
},
clear: async () => {
data.clear()
},
keys: async () => Array.from(data.keys()),
length: async () => data.size,
}
return store
}
const getStore = (name: string) => {
const cached = storeCache.get(name)
if (cached) return cached
const store = Store.load(name).catch(() => {
const cached = memoryCache.get(name)
if (cached) return cached
const memory = createMemoryStore()
memoryCache.set(name, memory)
return memory
})
storeCache.set(name, store)
return store
}
const createStorage = (name: string) => {
const pending = new Map<string, string | null>()
let timer: ReturnType<typeof setTimeout> | undefined
let flushing: Promise<void> | undefined
const flush = async () => {
if (flushing) return flushing
flushing = (async () => {
const store = await getStore(name)
while (pending.size > 0) {
const batch = Array.from(pending.entries())
pending.clear()
for (const [key, value] of batch) {
if (value === null) {
await store.delete(key).catch(() => undefined)
} else {
await store.set(key, value).catch(() => undefined)
}
}
}
})().finally(() => {
flushing = undefined
})
return flushing
}
const schedule = () => {
if (timer) return
timer = setTimeout(() => {
timer = undefined
void flush()
}, WRITE_DEBOUNCE_MS)
}
const api: AsyncStorage & { flush: () => Promise<void> } = {
flush,
getItem: async (key: string) => {
const next = pending.get(key)
if (next !== undefined) return next
const store = await getStore(name)
const value = await store.get(key).catch(() => null)
if (value === undefined) return null
return value
},
setItem: async (key: string, value: string) => {
pending.set(key, value)
schedule()
},
removeItem: async (key: string) => {
pending.set(key, null)
schedule()
},
clear: async () => {
pending.clear()
const store = await getStore(name)
await store.clear().catch(() => undefined)
},
key: async (index: number) => {
const store = await getStore(name)
return (await store.keys().catch(() => []))[index]
},
getLength: async () => {
const store = await getStore(name)
return await store.length().catch(() => 0)
},
get length() {
return api.getLength()
},
}
return api
}
return (name = "default.dat") => {
const cached = apiCache.get(name)
if (cached) return cached
const api = createStorage(name)
apiCache.set(name, api)
return api
}
})(),
checkUpdate: async () => {
if (!UPDATER_ENABLED) return { updateAvailable: false }
const next = await check().catch(() => null)
if (!next) return { updateAvailable: false }
const ok = await next
.download()
.then(() => true)
.catch(() => false)
if (!ok) return { updateAvailable: false }
update = next
return { updateAvailable: true, version: next.version }
},
updateAndRestart: async () => {
if (!UPDATER_ENABLED || !update) return
if (ostype() === "windows") await commands.killSidecar().catch(() => undefined)
const installed = await update
.install()
.then(() => true)
.catch(() => false)
if (!installed) return
await relaunch()
},
restart: async () => {
await commands.killSidecar().catch(() => undefined)
await relaunch()
},
notify: async (title, description, href) => {
const granted = await isPermissionGranted().catch(() => false)
const permission = granted ? "granted" : await requestPermission().catch(() => "denied")
if (permission !== "granted") return
const win = getCurrentWindow()
const focused = await win.isFocused().catch(() => document.hasFocus())
if (focused) return
await Promise.resolve()
.then(() => {
const notification = new Notification(title, {
body: description ?? "",
icon: "https://opencode.ai/favicon-96x96-v3.png",
})
notification.onclick = () => {
const win = getCurrentWindow()
void win.show().catch(() => undefined)
void win.unminimize().catch(() => undefined)
void win.setFocus().catch(() => undefined)
handleNotificationClick(href)
notification.close()
}
})
.catch(() => undefined)
},
fetch: (input, init) => {
if (input instanceof Request) {
return tauriFetch(input)
} else {
return tauriFetch(input, init)
}
},
getWslEnabled: async () => {
const next = await commands.getWslConfig().catch(() => null)
if (next) return next.enabled
return window.__OPENCODE__!.wsl ?? false
},
setWslEnabled: async (enabled) => {
await commands.setWslConfig({ enabled })
},
getDefaultServer: async () => {
const url = await commands.getDefaultServerUrl().catch(() => null)
if (!url) return null
return ServerConnection.Key.make(url)
},
setDefaultServer: async (url: string | null) => {
await commands.setDefaultServerUrl(url)
},
getDisplayBackend: async () => {
const result = await commands.getDisplayBackend().catch(() => null)
return result
},
setDisplayBackend: async (backend) => {
await commands.setDisplayBackend(backend)
},
parseMarkdown: (markdown: string) => commands.parseMarkdownCommand(markdown),
webviewZoom,
checkAppExists: async (appName: string) => {
return commands.checkAppExists(appName)
},
async readClipboardImage() {
const image = await readImage().catch(() => null)
if (!image) return null
const bytes = await image.rgba().catch(() => null)
if (!bytes || bytes.length === 0) return null
const size = await image.size().catch(() => null)
if (!size) return null
const canvas = document.createElement("canvas")
canvas.width = size.width
canvas.height = size.height
const ctx = canvas.getContext("2d")
if (!ctx) return null
const imageData = ctx.createImageData(size.width, size.height)
imageData.data.set(bytes)
ctx.putImageData(imageData, 0, 0)
return new Promise<File | null>((resolve) => {
canvas.toBlob((blob) => {
if (!blob) return resolve(null)
resolve(
new File([blob], `pasted-image-${Date.now()}.png`, {
type: "image/png",
}),
)
}, "image/png")
})
},
}
}
let menuTrigger = null as null | ((id: string) => void)
void createMenu((id) => {
menuTrigger?.(id)
})
void listenForDeepLinks()
render(() => {
const platform = createPlatform()
const loadLocale = async () => {
const current = await platform.storage?.("opencode.global.dat").getItem("language")
const legacy = current ? undefined : await platform.storage?.().getItem("language.v1")
const raw = current ?? legacy
if (!raw) return
const locale = raw.match(/"locale"\s*:\s*"([^"]+)"/)?.[1]
if (!locale) return
const next = normalizeLocale(locale)
if (next !== "en") await loadLocaleDict(next)
return next satisfies Locale
}
// Fetch sidecar credentials from Rust (available immediately, before health check)
const [sidecar] = createResource(() => commands.awaitInitialization(new Channel<InitStep>() as any))
const [defaultServer] = createResource(() =>
platform.getDefaultServer?.().then((url) => {
if (url) return ServerConnection.key({ type: "http", http: { url } })
}),
)
const [locale] = createResource(loadLocale)
// Build the sidecar server connection once credentials arrive
const servers = () => {
const data = sidecar()
if (!data) return []
const http = {
url: data.url,
username: data.username ?? undefined,
password: data.password ?? undefined,
}
const server: ServerConnection.Sidecar = {
displayName: t("desktop.server.local"),
type: "sidecar",
variant: "base",
http,
}
return [server] as ServerConnection.Any[]
}
function handleClick(e: MouseEvent) {
const link = (e.target as HTMLElement).closest("a.external-link") as HTMLAnchorElement | null
if (link?.href) {
e.preventDefault()
platform.openLink(link.href)
}
}
function Inner() {
const cmd = useCommand()
menuTrigger = (id) => cmd.trigger(id)
return null
}
onMount(() => {
document.addEventListener("click", handleClick)
onCleanup(() => {
document.removeEventListener("click", handleClick)
})
})
return (
<PlatformProvider value={platform}>
<AppBaseProviders locale={locale.latest}>
<Show when={!defaultServer.loading && !sidecar.loading && !locale.loading}>
{(_) => {
return (
<AppInterface
defaultServer={defaultServer.latest ?? ServerConnection.Key.make("sidecar")}
servers={servers()}
>
<Inner />
</AppInterface>
)
}}
</Show>
</AppBaseProviders>
</PlatformProvider>
)
}, root!)

View file

@ -0,0 +1,148 @@
import { execFileSync } from "node:child_process"
import { existsSync, readFileSync, readdirSync } from "node:fs"
import { dirname, extname, join } from "node:path"
export function checkAppExists(appName: string): boolean {
if (process.platform === "win32") return true
if (process.platform === "linux") return true
return checkMacosApp(appName)
}
export function resolveAppPath(appName: string): string | null {
if (process.platform !== "win32") return appName
return resolveWindowsAppPath(appName)
}
export function wslPath(path: string, mode: "windows" | "linux" | null): string {
if (process.platform !== "win32") return path
const flag = mode === "windows" ? "-w" : "-u"
try {
if (path.startsWith("~")) {
const suffix = path.slice(1)
const cmd = `wslpath ${flag} "$HOME${suffix.replace(/"/g, '\\"')}"`
const output = execFileSync("wsl", ["-e", "sh", "-lc", cmd])
return output.toString().trim()
}
const output = execFileSync("wsl", ["-e", "wslpath", flag, path])
return output.toString().trim()
} catch (error) {
throw new Error(`Failed to run wslpath: ${String(error)}`, { cause: error })
}
}
function checkMacosApp(appName: string) {
const locations = [`/Applications/${appName}.app`, `/System/Applications/${appName}.app`]
const home = process.env.HOME
if (home) locations.push(`${home}/Applications/${appName}.app`)
if (locations.some((location) => existsSync(location))) return true
try {
execFileSync("which", [appName])
return true
} catch {
return false
}
}
function resolveWindowsAppPath(appName: string): string | null {
let output: string
try {
output = execFileSync("where", [appName]).toString()
} catch {
return null
}
const paths = output
.split(/\r?\n/)
.map((line) => line.trim())
.filter((line) => line.length > 0)
const hasExt = (path: string, ext: string) => extname(path).toLowerCase() === `.${ext}`
const exe = paths.find((path) => hasExt(path, "exe"))
if (exe) return exe
const resolveCmd = (path: string) => {
const content = readFileSync(path, "utf8")
for (const token of content.split('"').map((value: string) => value.trim())) {
const lower = token.toLowerCase()
if (!lower.includes(".exe")) continue
const index = lower.indexOf("%~dp0")
if (index >= 0) {
const base = dirname(path)
const suffix = token.slice(index + 5)
const resolved = suffix
.replace(/\//g, "\\")
.split("\\")
.filter((part: string) => part && part !== ".")
.reduce((current: string, part: string) => {
if (part === "..") return dirname(current)
return join(current, part)
}, base)
if (existsSync(resolved)) return resolved
}
if (existsSync(token)) return token
}
return null
}
for (const path of paths) {
if (hasExt(path, "cmd") || hasExt(path, "bat")) {
const resolved = resolveCmd(path)
if (resolved) return resolved
}
if (!extname(path)) {
const cmd = `${path}.cmd`
if (existsSync(cmd)) {
const resolved = resolveCmd(cmd)
if (resolved) return resolved
}
const bat = `${path}.bat`
if (existsSync(bat)) {
const resolved = resolveCmd(bat)
if (resolved) return resolved
}
}
}
const key = appName
.split("")
.filter((value: string) => /[a-z0-9]/i.test(value))
.map((value: string) => value.toLowerCase())
.join("")
if (key) {
for (const path of paths) {
const dirs = [dirname(path), dirname(dirname(path)), dirname(dirname(dirname(path)))]
for (const dir of dirs) {
try {
for (const entry of readdirSync(dir)) {
const candidate = join(dir, entry)
if (!hasExt(candidate, "exe")) continue
const stem = entry.replace(/\.exe$/i, "")
const name = stem
.split("")
.filter((value: string) => /[a-z0-9]/i.test(value))
.map((value: string) => value.toLowerCase())
.join("")
if (name.includes(key) || key.includes(name)) return candidate
}
} catch {
continue
}
}
}
}
return paths[0] ?? null
}

View file

@ -0,0 +1,10 @@
import { app } from "electron"
type Channel = "dev" | "beta" | "prod"
const raw = import.meta.env.OPENCODE_CHANNEL
export const CHANNEL: Channel = raw === "dev" || raw === "beta" || raw === "prod" ? raw : "dev"
export const SETTINGS_STORE = "opencode.settings"
export const DEFAULT_SERVER_URL_KEY = "defaultServerUrl"
export const WSL_ENABLED_KEY = "wslEnabled"
export const UPDATER_ENABLED = app.isPackaged && CHANNEL !== "dev"

29
packages/desktop/src/main/env.d.ts vendored Normal file
View file

@ -0,0 +1,29 @@
interface ImportMetaEnv {
readonly OPENCODE_CHANNEL: string
}
interface ImportMeta {
readonly env: ImportMetaEnv
}
declare module "virtual:opencode-server" {
export namespace Server {
export const listen: typeof import("../../../opencode/dist/types/src/node").Server.listen
export type Listener = import("../../../opencode/dist/types/src/node").Server.Listener
}
export namespace Config {
export const get: typeof import("../../../opencode/dist/types/src/node").Config.get
export type Info = import("../../../opencode/dist/types/src/node").Config.Info
}
export namespace Log {
export const init: typeof import("../../../opencode/dist/types/src/node").Log.init
}
export namespace Database {
export const Path: typeof import("../../../opencode/dist/types/src/node").Database.Path
export const Client: typeof import("../../../opencode/dist/types/src/node").Database.Client
}
export namespace JsonMigration {
export type Progress = import("../../../opencode/dist/types/src/node").JsonMigration.Progress
export const run: typeof import("../../../opencode/dist/types/src/node").JsonMigration.run
}
export const bootstrap: typeof import("../../../opencode/dist/types/src/node").bootstrap
}

View file

@ -0,0 +1,527 @@
import { randomUUID } from "node:crypto"
import { existsSync } from "node:fs"
import { createServer } from "node:net"
import { homedir } from "node:os"
import { join } from "node:path"
import { app, BrowserWindow, dialog } from "electron"
import * as NodeRuntime from "@effect/platform-node/NodeRuntime"
import * as NodeHttpClient from "@effect/platform-node/NodeHttpClient"
import pkg from "electron-updater"
import { Data, Deferred, Effect, Fiber, Option, PubSub, Queue, Ref, Stream, SubscriptionRef } from "effect"
import contextMenu from "electron-context-menu"
contextMenu({ showSaveImageAs: true, showLookUpSelection: false, showSearchWithGoogle: false })
// on macOS apps run in `/` which can cause issues with ripgrep
try {
process.chdir(homedir())
} catch {}
process.env.OPENCODE_DISABLE_EMBEDDED_WEB_UI = "true"
const APP_NAMES: Record<string, string> = {
dev: "OpenCode Dev",
beta: "OpenCode Beta",
prod: "OpenCode",
}
const APP_IDS: Record<string, string> = {
dev: "ai.opencode.desktop.dev",
beta: "ai.opencode.desktop.beta",
prod: "ai.opencode.desktop",
}
const appId = app.isPackaged ? APP_IDS[CHANNEL] : "ai.opencode.desktop.dev"
app.setName(app.isPackaged ? APP_NAMES[CHANNEL] : "OpenCode Dev")
app.setAppUserModelId(appId)
app.setPath("userData", join(app.getPath("appData"), appId))
const { autoUpdater } = pkg
import { InitStep, ServerReadyData, SqliteMigrationProgress, WslConfig } from "../preload/types"
import type { Server } from "virtual:opencode-server"
import { checkAppExists, resolveAppPath, wslPath } from "./apps"
import { CHANNEL, UPDATER_ENABLED } from "./constants"
import { registerIpcHandlers, sendDeepLinks, sendMenuCommand, sendSqliteMigrationProgress } from "./ipc"
import { initLogging } from "./logging"
import { parseMarkdown } from "./markdown"
import { createMenu } from "./menu"
import { getDefaultServerUrl, getWslConfig, setDefaultServerUrl, setWslConfig, spawnLocalServerEffect } from "./server"
import {
createLoadingWindow,
createMainWindow,
registerRendererProtocol,
setBackgroundColor,
setDockIcon,
} from "./windows"
// ---------------------------------------------------------------------------
// State — individual pieces, synchronously allocated at module load.
// ---------------------------------------------------------------------------
const initStep = Effect.runSync(SubscriptionRef.make<InitStep>(InitStep.ServerWaiting()))
const serverReady = Deferred.makeUnsafe<ServerReadyData>()
const loadingComplete = Deferred.makeUnsafe<void>()
const deepLinkQueue = Effect.runSync(Queue.unbounded<string[]>())
const deepLinksConsumed = Deferred.makeUnsafe<void>()
const server = Ref.makeUnsafe<Option.Option<Server.Listener>>(Option.none())
const menuCommands = Effect.runSync(PubSub.unbounded<string>())
const sqliteProgress = Effect.runSync(PubSub.unbounded<SqliteMigrationProgress>())
// ---------------------------------------------------------------------------
// App events (Data.TaggedEnum)
// ---------------------------------------------------------------------------
type AppEvent = Data.TaggedEnum<{
SecondInstance: { readonly argv: readonly string[] }
OpenUrl: { readonly url: string }
BeforeQuit: {}
WillQuit: {}
}>
const appEvent = Data.taggedEnum<AppEvent>()
const handleAppEvent = (
event: AppEvent,
deepLinkQueue: Queue.Queue<string[]>,
mainWindow: BrowserWindow,
server: Ref.Ref<Option.Option<Server.Listener>>,
) =>
appEvent.$match(event, {
SecondInstance: ({ argv }) =>
Effect.gen(function* () {
const urls = argv.filter((arg) => arg.startsWith("opencode://"))
if (urls.length) {
logger.log("deep link received via second-instance", { urls })
yield* Queue.offer(deepLinkQueue, urls)
}
focusMainWindow(mainWindow)
}),
OpenUrl: ({ url }) =>
Effect.gen(function* () {
logger.log("deep link received via open-url", { url })
yield* Queue.offer(deepLinkQueue, [url])
}),
BeforeQuit: () => stopServer(server),
WillQuit: () => stopServer(server),
})
const focusMainWindow = (win: BrowserWindow) => {
win.show()
win.focus()
}
const stopServer = (ref: Ref.Ref<Option.Option<Server.Listener>>) =>
Effect.gen(function* () {
const srv = yield* Ref.get(ref)
if (Option.isSome(srv)) {
yield* Effect.promise(() => srv.value.stop())
yield* Ref.set(ref, Option.none())
}
})
const initialize = Effect.fn("Main.initialize")(function* () {
const needsMigration = !sqliteFileExists()
const port = yield* getSidecarPort
const hostname = "127.0.0.1"
const url = `http://${hostname}:${port}`
const password = randomUUID()
const loadingFiber = yield* Effect.gen(function* () {
logger.log("sidecar connection started", { url })
if (needsMigration) {
const { Database, JsonMigration } = yield* Effect.promise(
() => import("virtual:opencode-server") as Promise<typeof import("virtual:opencode-server")>,
)
const client = Database.Client().$client
const db = yield* Effect.promise(() =>
import("drizzle-orm/node-sqlite/driver").then((m) => m.drizzle({ client })),
)
yield* SubscriptionRef.set(initStep, InitStep.SqliteWaiting())
yield* Effect.promise(() =>
JsonMigration.run(db, {
progress: (event: { current: number; total: number }) => {
const percent = Math.round((event.current / event.total) * 100)
const progress: SqliteMigrationProgress = { type: "InProgress", value: percent }
if (Option.isSome(overlay)) sendSqliteMigrationProgress(overlay.value, progress)
void Effect.runPromise(PubSub.publish(sqliteProgress, progress))
},
}),
)
yield* PubSub.publish(sqliteProgress, { type: "Done" })
}
logger.log("spawning sidecar", { url })
const { listener, health } = yield* spawnLocalServerEffect(hostname, port, password)
yield* Ref.set(server, Option.some(listener))
yield* Deferred.succeed(serverReady, {
url,
username: "opencode",
password,
})
yield* Effect.raceAll([
health,
Effect.sleep("30 seconds").pipe(Effect.flatMap(() => Effect.fail(new Error("Sidecar health check timed out")))),
]).pipe(Effect.catch((error) => Effect.sync(() => logger.error("sidecar health check failed", error))))
logger.log("loading task finished")
return listener
}).pipe(Effect.forkChild)
const overlay = yield* Effect.gen(function* () {
if (!needsMigration) return
const show = yield* Effect.raceAll([
Fiber.join(loadingFiber).pipe(Effect.as(false)),
Effect.sleep("1 second").pipe(Effect.as(true)),
])
if (!show) return
const overlay = createLoadingWindow()
yield* Effect.sleep("1 second")
return overlay
}).pipe(Effect.map(Option.fromNullishOr))
yield* Fiber.join(loadingFiber)
yield* SubscriptionRef.set(initStep, InitStep.Done())
if (Option.isSome(overlay)) {
yield* Deferred.await(loadingComplete)
overlay.value.close()
}
})
// ---------------------------------------------------------------------------
// App lifecycle (imperative Electron shell, thin wrappers around Effects)
// ---------------------------------------------------------------------------
const logger = initLogging()
const shutdown = Effect.gen(function* () {
yield* stopServer(server)
app.exit(0)
})
const registerAppEventListeners = (appEvents: PubSub.PubSub<AppEvent>) => () => {
app.on("second-instance", (_event, argv) => {
PubSub.publishUnsafe(appEvents, appEvent.SecondInstance({ argv }))
})
app.on("open-url", (event, url) => {
event.preventDefault()
PubSub.publishUnsafe(appEvents, appEvent.OpenUrl({ url }))
})
app.on("before-quit", () => {
PubSub.publishUnsafe(appEvents, appEvent.BeforeQuit())
})
app.on("will-quit", () => {
PubSub.publishUnsafe(appEvents, appEvent.WillQuit())
})
for (const signal of ["SIGINT", "SIGTERM"] as const) {
process.on(signal, () => {
void Effect.runPromise(shutdown)
})
}
}
const ensureLoopbackNoProxy = () => {
const loopback = ["127.0.0.1", "localhost", "::1"]
const upsert = (key: string) => {
const items = (process.env[key] ?? "")
.split(",")
.map((value: string) => value.trim())
.filter((value: string) => Boolean(value))
for (const host of loopback) {
if (items.some((value: string) => value.toLowerCase() === host)) continue
items.push(host)
}
process.env[key] = items.join(",")
}
upsert("NO_PROXY")
upsert("no_proxy")
}
const main = Effect.gen(function* () {
logger.log("app starting", {
version: app.getVersion(),
packaged: app.isPackaged,
})
if (!app.requestSingleInstanceLock()) {
app.quit()
return
}
ensureLoopbackNoProxy()
app.commandLine.appendSwitch("proxy-bypass-list", "<-loopback>")
const appEvents = yield* PubSub.unbounded<AppEvent>()
registerAppEventListeners(appEvents)
yield* Effect.promise(() => app.whenReady())
app.setAsDefaultProtocolClient("opencode")
registerRendererProtocol()
setDockIcon()
setupAutoUpdater()
registerIpcHandlersImpl()
yield* initialize()
const mainWindow = createMainWindow()
wireMenu(mainWindow)
yield* Effect.all(
[
Stream.fromPubSub(appEvents).pipe(
Stream.runForEach((event) => handleAppEvent(event, deepLinkQueue, mainWindow, server)),
),
Deferred.await(deepLinksConsumed).pipe(
Effect.andThen(
Stream.fromQueue(deepLinkQueue).pipe(
Stream.runForEach((urls) => Effect.sync(() => sendDeepLinks(mainWindow, urls))),
),
),
),
Stream.fromPubSub(menuCommands).pipe(
Stream.runForEach((id) => Effect.sync(() => sendMenuCommand(mainWindow, id))),
),
],
{ concurrency: "unbounded" },
)
}).pipe(
Effect.catch((error) =>
Effect.sync(() => {
logger.error("initialization failed", error)
app.exit(1)
}),
),
)
main.pipe(Effect.provide(NodeHttpClient.layerFetch), NodeRuntime.runMain())
const wireMenu = (win: BrowserWindow) => {
createMenu({
trigger: (id) => {
sendMenuCommand(win, id)
},
checkForUpdates: () => {
void checkForUpdates(true)
},
reload: () => win.reload(),
relaunch: () => {
void Effect.runPromise(
Effect.gen(function* () {
yield* stopServer(server)
app.relaunch()
app.exit(0)
}),
)
},
})
}
const registerIpcHandlersImpl = () =>
registerIpcHandlers({
killSidecar: () => Effect.runPromise(stopServer(server)),
awaitInitialization: (sendStep) =>
Effect.runPromise(
Effect.gen(function* () {
const currentStep = yield* SubscriptionRef.get(initStep)
sendStep(currentStep)
yield* SubscriptionRef.changes(initStep).pipe(
Stream.runForEach((step) => Effect.sync(() => sendStep(step))),
Effect.forkScoped,
)
logger.log("awaiting server ready")
const res = yield* Deferred.await(serverReady)
logger.log("server ready", { url: res.url })
return res
}).pipe(Effect.scoped),
),
getWindowConfig: () => ({ updaterEnabled: UPDATER_ENABLED }),
consumeInitialDeepLinks: () =>
Effect.runPromise(
Queue.clear(deepLinkQueue).pipe(
Effect.map((links) => links.flat()),
Effect.tap(() => Deferred.succeed(deepLinksConsumed, undefined)),
),
),
getDefaultServerUrl: () => getDefaultServerUrl(),
setDefaultServerUrl: (url) => setDefaultServerUrl(url),
getWslConfig: () => Promise.resolve(getWslConfig()),
setWslConfig: (config: WslConfig) => setWslConfig(config),
getDisplayBackend: () => Promise.resolve(null),
setDisplayBackend: () => Promise.resolve(undefined),
parseMarkdown: (markdown) => Promise.resolve(parseMarkdown(markdown)),
checkAppExists: (appName) => checkAppExists(appName),
wslPath: (path, mode) => Promise.resolve(wslPath(path, mode)),
resolveAppPath: (appName) => Promise.resolve(resolveAppPath(appName)),
loadingWindowComplete: () => Effect.runPromise(Deferred.succeed(loadingComplete, undefined)),
runUpdater: (alertOnFail) => checkForUpdates(alertOnFail),
checkUpdate: () => checkUpdate(),
installUpdate: () => installUpdate(),
setBackgroundColor,
})
const getSidecarPort = Effect.gen(function* () {
const fromEnv = process.env.OPENCODE_PORT
if (fromEnv) {
const parsed = Number.parseInt(fromEnv, 10)
if (!Number.isNaN(parsed)) return parsed
}
const deferred = yield* Deferred.make<number, string>()
const server = createServer()
server.on("error", (e) => Deferred.failSync(deferred, () => e.toString()))
server.listen(0, "127.0.0.1", () => {
const address = server.address()
if (typeof address !== "object" || !address) {
server.close()
Deferred.failSync(deferred, () => "Failed to get port")
return
}
const port = address.port
server.close(() => Effect.runSync(Deferred.succeed(deferred, port)))
})
return yield* Deferred.await(deferred)
})
const sqliteFileExists = () => {
const xdg = process.env.XDG_DATA_HOME
const base = xdg && xdg.length > 0 ? xdg : join(homedir(), ".local", "share")
return existsSync(join(base, "opencode", "opencode.db"))
}
function setupAutoUpdater() {
if (!UPDATER_ENABLED) return
autoUpdater.logger = logger
autoUpdater.channel = "latest"
autoUpdater.allowPrerelease = false
autoUpdater.allowDowngrade = true
autoUpdater.autoDownload = false
autoUpdater.autoInstallOnAppQuit = true
logger.log("auto updater configured", {
channel: autoUpdater.channel,
allowPrerelease: autoUpdater.allowPrerelease,
allowDowngrade: autoUpdater.allowDowngrade,
currentVersion: app.getVersion(),
})
}
let downloadedUpdateVersion: string | undefined
async function checkUpdate() {
if (!UPDATER_ENABLED) return { updateAvailable: false }
if (downloadedUpdateVersion) {
logger.log("returning cached downloaded update", {
version: downloadedUpdateVersion,
})
return { updateAvailable: true, version: downloadedUpdateVersion }
}
logger.log("checking for updates", {
currentVersion: app.getVersion(),
channel: autoUpdater.channel,
allowPrerelease: autoUpdater.allowPrerelease,
allowDowngrade: autoUpdater.allowDowngrade,
})
try {
const result = await autoUpdater.checkForUpdates()
const updateInfo = result?.updateInfo
logger.log("update metadata fetched", {
releaseVersion: updateInfo?.version ?? null,
releaseDate: updateInfo?.releaseDate ?? null,
releaseName: updateInfo?.releaseName ?? null,
files: updateInfo?.files?.map((file) => file.url) ?? [],
})
const version = result?.updateInfo?.version
if (result?.isUpdateAvailable === false || !version) {
logger.log("no update available", {
reason: "provider returned no newer version",
})
return { updateAvailable: false }
}
logger.log("update available", { version })
await autoUpdater.downloadUpdate()
logger.log("update download completed", { version })
downloadedUpdateVersion = version
return { updateAvailable: true, version }
} catch (error) {
logger.error("update check failed", error)
return { updateAvailable: false, failed: true }
}
}
async function installUpdate() {
if (!downloadedUpdateVersion) {
logger.log("install update skipped", {
reason: "no downloaded update ready",
})
return
}
logger.log("installing downloaded update", {
version: downloadedUpdateVersion,
})
void Effect.runPromise(stopServer(server))
autoUpdater.quitAndInstall()
}
async function checkForUpdates(alertOnFail: boolean) {
if (!UPDATER_ENABLED) return
logger.log("checkForUpdates invoked", { alertOnFail })
const result = await checkUpdate()
if (!result.updateAvailable) {
if (result.failed) {
logger.log("no update decision", { reason: "update check failed" })
if (!alertOnFail) return
await dialog.showMessageBox({
type: "error",
message: "Update check failed.",
title: "Update Error",
})
return
}
logger.log("no update decision", { reason: "already up to date" })
if (!alertOnFail) return
await dialog.showMessageBox({
type: "info",
message: "You're up to date.",
title: "No Updates",
})
return
}
const response = await dialog.showMessageBox({
type: "info",
message: `Update ${result.version ?? ""} downloaded. Restart now?`,
title: "Update Ready",
buttons: ["Restart", "Later"],
defaultId: 0,
cancelId: 1,
})
logger.log("update prompt response", {
version: result.version ?? null,
restartNow: response.response === 0,
})
if (response.response === 0) {
await installUpdate()
}
}

View file

@ -0,0 +1,202 @@
import { execFile } from "node:child_process"
import { BrowserWindow, Notification, app, clipboard, dialog, ipcMain, shell } from "electron"
import type { IpcMainEvent, IpcMainInvokeEvent } from "electron"
import type {
InitStep,
ServerReadyData,
SqliteMigrationProgress,
TitlebarTheme,
WindowConfig,
WslConfig,
} from "../preload/types"
import { getStore } from "./store"
import { setTitlebar, updateTitlebar } from "./windows"
const pickerFilters = (ext?: string[]) => {
if (!ext || ext.length === 0) return undefined
return [{ name: "Files", extensions: ext }]
}
type Deps = {
killSidecar: () => void
awaitInitialization: (sendStep: (step: InitStep) => void) => Promise<ServerReadyData>
getWindowConfig: () => Promise<WindowConfig> | WindowConfig
consumeInitialDeepLinks: () => Promise<string[]> | string[]
getDefaultServerUrl: () => Promise<string | null> | string | null
setDefaultServerUrl: (url: string | null) => Promise<void> | void
getWslConfig: () => Promise<WslConfig>
setWslConfig: (config: WslConfig) => Promise<void> | void
getDisplayBackend: () => Promise<string | null>
setDisplayBackend: (backend: string | null) => Promise<void> | void
parseMarkdown: (markdown: string) => Promise<string> | string
checkAppExists: (appName: string) => Promise<boolean> | boolean
wslPath: (path: string, mode: "windows" | "linux" | null) => Promise<string>
resolveAppPath: (appName: string) => Promise<string | null>
loadingWindowComplete: () => void
runUpdater: (alertOnFail: boolean) => Promise<void> | void
checkUpdate: () => Promise<{ updateAvailable: boolean; version?: string }>
installUpdate: () => Promise<void> | void
setBackgroundColor: (color: string) => void
}
export function registerIpcHandlers(deps: Deps) {
ipcMain.handle("kill-sidecar", () => deps.killSidecar())
ipcMain.handle("await-initialization", (event: IpcMainInvokeEvent) => {
const send = (step: InitStep) => event.sender.send("init-step", step)
return deps.awaitInitialization(send)
})
ipcMain.handle("get-window-config", () => deps.getWindowConfig())
ipcMain.handle("consume-initial-deep-links", () => deps.consumeInitialDeepLinks())
ipcMain.handle("get-default-server-url", () => deps.getDefaultServerUrl())
ipcMain.handle("set-default-server-url", (_event: IpcMainInvokeEvent, url: string | null) =>
deps.setDefaultServerUrl(url),
)
ipcMain.handle("get-wsl-config", () => deps.getWslConfig())
ipcMain.handle("set-wsl-config", (_event: IpcMainInvokeEvent, config: WslConfig) => deps.setWslConfig(config))
ipcMain.handle("get-display-backend", () => deps.getDisplayBackend())
ipcMain.handle("set-display-backend", (_event: IpcMainInvokeEvent, backend: string | null) =>
deps.setDisplayBackend(backend),
)
ipcMain.handle("parse-markdown", (_event: IpcMainInvokeEvent, markdown: string) => deps.parseMarkdown(markdown))
ipcMain.handle("check-app-exists", (_event: IpcMainInvokeEvent, appName: string) => deps.checkAppExists(appName))
ipcMain.handle("wsl-path", (_event: IpcMainInvokeEvent, path: string, mode: "windows" | "linux" | null) =>
deps.wslPath(path, mode),
)
ipcMain.handle("resolve-app-path", (_event: IpcMainInvokeEvent, appName: string) => deps.resolveAppPath(appName))
ipcMain.on("loading-window-complete", () => deps.loadingWindowComplete())
ipcMain.handle("run-updater", (_event: IpcMainInvokeEvent, alertOnFail: boolean) => deps.runUpdater(alertOnFail))
ipcMain.handle("check-update", () => deps.checkUpdate())
ipcMain.handle("install-update", () => deps.installUpdate())
ipcMain.handle("set-background-color", (_event: IpcMainInvokeEvent, color: string) => deps.setBackgroundColor(color))
ipcMain.handle("store-get", (_event: IpcMainInvokeEvent, name: string, key: string) => {
const store = getStore(name)
const value = store.get(key)
if (value === undefined || value === null) return null
return typeof value === "string" ? value : JSON.stringify(value)
})
ipcMain.handle("store-set", (_event: IpcMainInvokeEvent, name: string, key: string, value: string) => {
getStore(name).set(key, value)
})
ipcMain.handle("store-delete", (_event: IpcMainInvokeEvent, name: string, key: string) => {
getStore(name).delete(key)
})
ipcMain.handle("store-clear", (_event: IpcMainInvokeEvent, name: string) => {
getStore(name).clear()
})
ipcMain.handle("store-keys", (_event: IpcMainInvokeEvent, name: string) => {
const store = getStore(name)
return Object.keys(store.store)
})
ipcMain.handle("store-length", (_event: IpcMainInvokeEvent, name: string) => {
const store = getStore(name)
return Object.keys(store.store).length
})
ipcMain.handle(
"open-directory-picker",
async (_event: IpcMainInvokeEvent, opts?: { multiple?: boolean; title?: string; defaultPath?: string }) => {
const result = await dialog.showOpenDialog({
properties: ["openDirectory", ...(opts?.multiple ? ["multiSelections" as const] : []), "createDirectory"],
title: opts?.title ?? "Choose a folder",
defaultPath: opts?.defaultPath,
})
if (result.canceled) return null
return opts?.multiple ? result.filePaths : result.filePaths[0]
},
)
ipcMain.handle(
"open-file-picker",
async (
_event: IpcMainInvokeEvent,
opts?: { multiple?: boolean; title?: string; defaultPath?: string; accept?: string[]; extensions?: string[] },
) => {
const result = await dialog.showOpenDialog({
properties: ["openFile", ...(opts?.multiple ? ["multiSelections" as const] : [])],
title: opts?.title ?? "Choose a file",
defaultPath: opts?.defaultPath,
filters: pickerFilters(opts?.extensions),
})
if (result.canceled) return null
return opts?.multiple ? result.filePaths : result.filePaths[0]
},
)
ipcMain.handle(
"save-file-picker",
async (_event: IpcMainInvokeEvent, opts?: { title?: string; defaultPath?: string }) => {
const result = await dialog.showSaveDialog({
title: opts?.title ?? "Save file",
defaultPath: opts?.defaultPath,
})
if (result.canceled) return null
return result.filePath ?? null
},
)
ipcMain.on("open-link", (_event: IpcMainEvent, url: string) => {
void shell.openExternal(url)
})
ipcMain.handle("open-path", async (_event: IpcMainInvokeEvent, path: string, app?: string) => {
if (!app) return shell.openPath(path)
await new Promise<void>((resolve, reject) => {
const [cmd, args] =
process.platform === "darwin" ? (["open", ["-a", app, path]] as const) : ([app, [path]] as const)
execFile(cmd, args, (err) => (err ? reject(err) : resolve()))
})
})
ipcMain.handle("read-clipboard-image", () => {
const image = clipboard.readImage()
if (image.isEmpty()) return null
const buffer = image.toPNG().buffer
const size = image.getSize()
return { buffer, width: size.width, height: size.height }
})
ipcMain.on("show-notification", (_event: IpcMainEvent, title: string, body?: string) => {
new Notification({ title, body }).show()
})
ipcMain.handle("get-window-count", () => BrowserWindow.getAllWindows().length)
ipcMain.handle("get-window-focused", (event: IpcMainInvokeEvent) => {
const win = BrowserWindow.fromWebContents(event.sender)
return win?.isFocused() ?? false
})
ipcMain.handle("set-window-focus", (event: IpcMainInvokeEvent) => {
const win = BrowserWindow.fromWebContents(event.sender)
win?.focus()
})
ipcMain.handle("show-window", (event: IpcMainInvokeEvent) => {
const win = BrowserWindow.fromWebContents(event.sender)
win?.show()
})
ipcMain.on("relaunch", () => {
app.relaunch()
app.exit(0)
})
ipcMain.handle("get-zoom-factor", (event: IpcMainInvokeEvent) => event.sender.getZoomFactor())
ipcMain.handle("set-zoom-factor", (event: IpcMainInvokeEvent, factor: number) => {
event.sender.setZoomFactor(factor)
const win = BrowserWindow.fromWebContents(event.sender)
if (!win) return
updateTitlebar(win)
})
ipcMain.handle("set-titlebar", (event: IpcMainInvokeEvent, theme: TitlebarTheme) => {
const win = BrowserWindow.fromWebContents(event.sender)
if (!win) return
setTitlebar(win, theme)
})
}
export const sendSqliteMigrationProgress = (win: BrowserWindow, progress: SqliteMigrationProgress) =>
win.webContents.send("sqlite-migration-progress", progress)
export const sendMenuCommand = (win: BrowserWindow, id: string) => win.webContents.send("menu-command", id)
export const sendDeepLinks = (win: BrowserWindow, urls: string[]) => win.webContents.send("deep-link", urls)

View file

@ -0,0 +1,40 @@
import log from "electron-log/main.js"
import { readFileSync, readdirSync, statSync, unlinkSync } from "node:fs"
import { dirname, join } from "node:path"
const MAX_LOG_AGE_DAYS = 7
const TAIL_LINES = 1000
export function initLogging() {
log.transports.file.maxSize = 5 * 1024 * 1024
cleanup()
return log
}
export function tail(): string {
try {
const path = log.transports.file.getFile().path
const contents = readFileSync(path, "utf8")
const lines = contents.split("\n")
return lines.slice(Math.max(0, lines.length - TAIL_LINES)).join("\n")
} catch {
return ""
}
}
function cleanup() {
const path = log.transports.file.getFile().path
const dir = dirname(path)
const cutoff = Date.now() - MAX_LOG_AGE_DAYS * 24 * 60 * 60 * 1000
for (const entry of readdirSync(dir)) {
const file = join(dir, entry)
try {
const info = statSync(file)
if (!info.isFile()) continue
if (info.mtimeMs < cutoff) unlinkSync(file)
} catch {
continue
}
}
}

View file

@ -0,0 +1,16 @@
import { marked, type Tokens } from "marked"
const renderer = new marked.Renderer()
renderer.link = ({ href, title, text }: Tokens.Link) => {
const titleAttr = title ? ` title="${title}"` : ""
return `<a href="${href}"${titleAttr} class="external-link" target="_blank" rel="noopener noreferrer">${text}</a>`
}
export function parseMarkdown(input: string) {
return marked(input, {
renderer,
breaks: false,
gfm: true,
})
}

View file

@ -0,0 +1,137 @@
import { Menu, shell } from "electron"
import { UPDATER_ENABLED } from "./constants"
import { createMainWindow } from "./windows"
type Deps = {
trigger: (id: string) => void
checkForUpdates: () => void
reload: () => void
relaunch: () => void
}
export function createMenu(deps: Deps) {
if (process.platform !== "darwin") return
const template: Electron.MenuItemConstructorOptions[] = [
{
label: "OpenCode",
submenu: [
{ role: "about" },
{
label: "Check for Updates...",
enabled: UPDATER_ENABLED,
click: () => deps.checkForUpdates(),
},
{
label: "Reload Webview",
click: () => deps.reload(),
},
{
label: "Restart",
click: () => deps.relaunch(),
},
{ type: "separator" },
{ role: "hide" },
{ role: "hideOthers" },
{ role: "unhide" },
{ type: "separator" },
{ role: "quit" },
],
},
{
label: "File",
submenu: [
{ label: "New Session", accelerator: "Shift+Cmd+S", click: () => deps.trigger("session.new") },
{ label: "Open Project...", accelerator: "Cmd+O", click: () => deps.trigger("project.open") },
{
label: "New Window",
accelerator: "Cmd+Shift+N",
click: () => {
void createMainWindow()
},
},
{ type: "separator" },
{ role: "close" },
],
},
{
label: "Edit",
submenu: [
{ role: "undo" },
{ role: "redo" },
{ type: "separator" },
{ role: "cut" },
{ role: "copy" },
{ role: "paste" },
{ role: "selectAll" },
],
},
{
label: "View",
submenu: [
{ label: "Toggle Sidebar", accelerator: "Cmd+B", click: () => deps.trigger("sidebar.toggle") },
{ label: "Toggle Terminal", accelerator: "Ctrl+`", click: () => deps.trigger("terminal.toggle") },
{ label: "Toggle File Tree", click: () => deps.trigger("fileTree.toggle") },
{ type: "separator" },
{ role: "reload" },
{ role: "toggleDevTools" },
{ type: "separator" },
{ role: "resetZoom" },
{ role: "zoomIn" },
{ role: "zoomOut" },
{ type: "separator" },
{ role: "togglefullscreen" },
],
},
{
label: "Go",
submenu: [
{ label: "Back", accelerator: "Cmd+[", click: () => deps.trigger("common.goBack") },
{ label: "Forward", accelerator: "Cmd+]", click: () => deps.trigger("common.goForward") },
{ type: "separator" },
{
label: "Previous Session",
accelerator: "Option+Up",
click: () => deps.trigger("session.previous"),
},
{
label: "Next Session",
accelerator: "Option+Down",
click: () => deps.trigger("session.next"),
},
{ type: "separator" },
{
label: "Previous Project",
accelerator: "Cmd+Option+Up",
click: () => deps.trigger("project.previous"),
},
{
label: "Next Project",
accelerator: "Cmd+Option+Down",
click: () => deps.trigger("project.next"),
},
],
},
{ role: "windowMenu" },
{
label: "Help",
submenu: [
{ label: "OpenCode Documentation", click: () => shell.openExternal("https://opencode.ai/docs") },
{ label: "Support Forum", click: () => shell.openExternal("https://discord.com/invite/opencode") },
{ type: "separator" },
{ type: "separator" },
{
label: "Share Feedback",
click: () =>
shell.openExternal("https://github.com/anomalyco/opencode/issues/new?template=feature_request.yml"),
},
{
label: "Report a Bug",
click: () => shell.openExternal("https://github.com/anomalyco/opencode/issues/new?template=bug_report.yml"),
},
],
},
]
Menu.setApplicationMenu(Menu.buildFromTemplate(template))
}

View file

@ -0,0 +1,91 @@
import { app } from "electron"
import log from "electron-log/main.js"
import { existsSync, readdirSync, readFileSync } from "node:fs"
import { homedir } from "node:os"
import { join } from "node:path"
import { CHANNEL } from "./constants"
import { getStore } from "./store"
const TAURI_MIGRATED_KEY = "tauriMigrated"
// Resolve the directory where Tauri stored its .dat files for the given app identifier.
// Mirrors Tauri's AppLocalData / AppData resolution per OS.
function tauriDir(id: string) {
switch (process.platform) {
case "darwin":
return join(homedir(), "Library", "Application Support", id)
case "win32":
return join(process.env.APPDATA ?? join(homedir(), "AppData", "Roaming"), id)
default:
return join(process.env.XDG_DATA_HOME ?? join(homedir(), ".local", "share"), id)
}
}
// The Tauri app identifier changes between dev/beta/prod builds.
const TAURI_APP_IDS: Record<string, string> = {
dev: "ai.opencode.desktop.dev",
beta: "ai.opencode.desktop.beta",
prod: "ai.opencode.desktop",
}
function tauriAppId() {
return app.isPackaged ? TAURI_APP_IDS[CHANNEL] : "ai.opencode.desktop.dev"
}
// Migrate a single Tauri .dat file into the corresponding electron-store.
// `opencode.settings.dat` is special: it maps to the `opencode.settings` store
// (the electron-store name without the `.dat` extension). All other .dat files
// keep their full filename as the electron-store name so they match what the
// renderer already passes via IPC (e.g. `"default.dat"`, `"opencode.global.dat"`).
function migrateFile(datPath: string, filename: string) {
let data: Record<string, unknown>
try {
data = JSON.parse(readFileSync(datPath, "utf-8"))
} catch (err) {
log.warn("tauri migration: failed to parse", filename, err)
return
}
// opencode.settings.dat → the electron settings store ("opencode.settings").
// All other .dat files keep their full filename as the store name so they match
// what the renderer passes via IPC (e.g. "default.dat", "opencode.global.dat").
const storeName = filename === "opencode.settings.dat" ? "opencode.settings" : filename
const target = getStore(storeName)
const migrated: string[] = []
const skipped: string[] = []
for (const [key, value] of Object.entries(data)) {
// Don't overwrite values the user has already set in the Electron app.
if (target.has(key)) {
skipped.push(key)
continue
}
target.set(key, value)
migrated.push(key)
}
log.log("tauri migration: migrated", filename, "→", storeName, { migrated, skipped })
}
export function migrate() {
if (getStore().get(TAURI_MIGRATED_KEY)) {
log.log("tauri migration: already done, skipping")
return
}
const dir = tauriDir(tauriAppId())
log.log("tauri migration: starting", { dir })
if (!existsSync(dir)) {
log.log("tauri migration: no tauri data directory found, nothing to migrate")
getStore().set(TAURI_MIGRATED_KEY, true)
return
}
for (const filename of readdirSync(dir)) {
if (!filename.endsWith(".dat")) continue
migrateFile(join(dir, filename), filename)
}
log.log("tauri migration: complete")
getStore().set(TAURI_MIGRATED_KEY, true)
}

View file

@ -0,0 +1,102 @@
import { app } from "electron"
import { Effect, Option } from "effect"
import { DEFAULT_SERVER_URL_KEY, WSL_ENABLED_KEY } from "./constants"
import { getUserShell, loadShellEnv } from "./shell-env"
import { getStore } from "./store"
import { HttpClient, HttpClientResponse } from "effect/unstable/http"
export type WslConfig = { enabled: boolean }
export const getDefaultServerUrl = (): string | null => {
const value = getStore().get(DEFAULT_SERVER_URL_KEY)
return typeof value === "string" ? value : null
}
export const setDefaultServerUrl = (url: string | null) => {
if (url) {
getStore().set(DEFAULT_SERVER_URL_KEY, url)
return
}
getStore().delete(DEFAULT_SERVER_URL_KEY)
}
export const getWslConfig = (): WslConfig => {
const value = getStore().get(WSL_ENABLED_KEY)
return { enabled: typeof value === "boolean" ? value : false }
}
export const setWslConfig = (config: WslConfig) => getStore().set(WSL_ENABLED_KEY, config.enabled)
export const spawnLocalServerEffect = Effect.fn("Server.spawnLocalServer")(function* (
hostname: string,
port: number,
password: string,
) {
prepareServerEnv(password)
const { Log, Server } = yield* Effect.promise(
() => import("virtual:opencode-server") as Promise<typeof import("virtual:opencode-server")>,
)
yield* Effect.promise(() => Log.init({ level: "WARN" }))
const listener = yield* Effect.promise(() =>
Server.listen({
port,
hostname,
username: "opencode",
password,
cors: ["oc://renderer"],
}),
)
const healthCheck = Effect.gen(function* () {
const url = `http://${hostname}:${port}`
while (true) {
const healthy = yield* checkHealthEffect(url, password)
if (healthy) return
yield* Effect.sleep("100 millis")
}
})
return { listener, health: healthCheck }
})
const prepareServerEnv = (password: string) => () => {
const shell = process.platform === "win32" ? null : getUserShell()
const shellEnv = shell ? (loadShellEnv(shell) ?? {}) : {}
const env = {
...process.env,
...shellEnv,
OPENCODE_EXPERIMENTAL_ICON_DISCOVERY: "true",
OPENCODE_EXPERIMENTAL_FILEWATCHER: "true",
OPENCODE_CLIENT: "desktop",
OPENCODE_SERVER_USERNAME: "opencode",
OPENCODE_SERVER_PASSWORD: password,
XDG_STATE_HOME: app.getPath("userData"),
}
Object.assign(process.env, env)
}
export const checkHealthEffect = Effect.fn("Server.checkHealth")(function* (url: string, password?: string | null) {
const httpClient = yield* HttpClient.HttpClient
let healthUrl: URL
try {
healthUrl = new URL("/global/health", url)
} catch {
return false
}
const headers = new Headers()
if (password) {
const auth = Buffer.from(`opencode:${password}`).toString("base64")
headers.set("authorization", `Basic ${auth}`)
}
return yield* httpClient
.get(healthUrl, { headers })
.pipe(
Effect.timeout("3 seconds"),
Effect.flatMap(HttpClientResponse.filterStatusOk),
Effect.option,
Effect.map(Option.isSome),
)
})

View file

@ -0,0 +1,43 @@
import { describe, expect, test } from "bun:test"
import { isNushell, mergeShellEnv, parseShellEnv } from "./shell-env"
describe("shell env", () => {
test("parseShellEnv supports null-delimited pairs", () => {
const env = parseShellEnv(Buffer.from("PATH=/usr/bin:/bin\0FOO=bar=baz\0\0"))
expect(env.PATH).toBe("/usr/bin:/bin")
expect(env.FOO).toBe("bar=baz")
})
test("parseShellEnv ignores invalid entries", () => {
const env = parseShellEnv(Buffer.from("INVALID\0=empty\0OK=1\0"))
expect(Object.keys(env).length).toBe(1)
expect(env.OK).toBe("1")
})
test("mergeShellEnv keeps explicit overrides", () => {
const env = mergeShellEnv(
{
PATH: "/shell/path",
HOME: "/tmp/home",
},
{
PATH: "/desktop/path",
OPENCODE_CLIENT: "desktop",
},
)
expect(env.PATH).toBe("/desktop/path")
expect(env.HOME).toBe("/tmp/home")
expect(env.OPENCODE_CLIENT).toBe("desktop")
})
test("isNushell handles path and binary name", () => {
expect(isNushell("nu")).toBe(true)
expect(isNushell("/opt/homebrew/bin/nu")).toBe(true)
expect(isNushell("C:\\Program Files\\nu.exe")).toBe(true)
expect(isNushell("/bin/zsh")).toBe(false)
})
})

View file

@ -0,0 +1,88 @@
import { spawnSync } from "node:child_process"
import { basename } from "node:path"
const TIMEOUT = 5_000
type Probe = { type: "Loaded"; value: Record<string, string> } | { type: "Timeout" } | { type: "Unavailable" }
export function getUserShell() {
return process.env.SHELL || "/bin/sh"
}
export function parseShellEnv(out: Buffer) {
const env: Record<string, string> = {}
for (const line of out.toString("utf8").split("\0")) {
if (!line) continue
const ix = line.indexOf("=")
if (ix <= 0) continue
env[line.slice(0, ix)] = line.slice(ix + 1)
}
return env
}
function probe(shell: string, mode: "-il" | "-l"): Probe {
const out = spawnSync(shell, [mode, "-c", "env -0"], {
stdio: ["ignore", "pipe", "ignore"],
timeout: TIMEOUT,
windowsHide: true,
})
const err = out.error as NodeJS.ErrnoException | undefined
if (err) {
if (err.code === "ETIMEDOUT") return { type: "Timeout" }
console.log(`[server] Shell env probe failed for ${shell} ${mode}: ${err.message}`)
return { type: "Unavailable" }
}
if (out.status !== 0) {
console.log(`[server] Shell env probe exited with non-zero status for ${shell} ${mode}`)
return { type: "Unavailable" }
}
const env = parseShellEnv(out.stdout)
if (Object.keys(env).length === 0) {
console.log(`[server] Shell env probe returned empty env for ${shell} ${mode}`)
return { type: "Unavailable" }
}
return { type: "Loaded", value: env }
}
export function isNushell(shell: string) {
const name = basename(shell).toLowerCase()
const raw = shell.toLowerCase()
return name === "nu" || name === "nu.exe" || raw.endsWith("\\nu.exe")
}
export function loadShellEnv(shell: string) {
if (isNushell(shell)) {
console.log(`[server] Skipping shell env probe for nushell: ${shell}`)
return null
}
const interactive = probe(shell, "-il")
if (interactive.type === "Loaded") {
console.log(`[server] Loaded shell environment with -il (${Object.keys(interactive.value).length} vars)`)
return interactive.value
}
if (interactive.type === "Timeout") {
console.warn(`[server] Interactive shell env probe timed out: ${shell}`)
return null
}
const login = probe(shell, "-l")
if (login.type === "Loaded") {
console.log(`[server] Loaded shell environment with -l (${Object.keys(login.value).length} vars)`)
return login.value
}
console.warn(`[server] Falling back to app environment: ${shell}`)
return null
}
export function mergeShellEnv(shell: Record<string, string> | null, env: Record<string, string>) {
return {
...shell,
...env,
}
}

View file

@ -0,0 +1,17 @@
import Store from "electron-store"
import { SETTINGS_STORE } from "./constants"
const cache = new Map<string, Store>()
// We cannot instantiate the electron-store at module load time because
// module import hoisting causes this to run before app.setPath("userData", ...)
// in index.ts has executed, which would result in files being written to the default directory
// (e.g. bad: %APPDATA%\@opencode-ai\desktop\opencode.settings vs good: %APPDATA%\ai.opencode.desktop.dev\opencode.settings).
export function getStore(name = SETTINGS_STORE) {
const cached = cache.get(name)
if (cached) return cached
const next = new Store({ name, fileExtension: "", accessPropertiesByDotNotation: false })
cache.set(name, next)
return next
}

View file

@ -0,0 +1,214 @@
import windowState from "electron-window-state"
import { app, BrowserWindow, net, nativeImage, nativeTheme, protocol } from "electron"
import { dirname, isAbsolute, join, relative, resolve } from "node:path"
import { fileURLToPath, pathToFileURL } from "node:url"
import type { TitlebarTheme } from "../preload/types"
const root = dirname(fileURLToPath(import.meta.url))
const rendererRoot = join(root, "../renderer")
const rendererProtocol = "oc"
const rendererHost = "renderer"
protocol.registerSchemesAsPrivileged([
{
scheme: rendererProtocol,
privileges: {
secure: true,
standard: true,
supportFetchAPI: true,
},
},
])
let backgroundColor: string | undefined
const titlebarThemes = new WeakMap<BrowserWindow, Partial<TitlebarTheme>>()
const titlebarHeight = 40
export const setBackgroundColor = (color: string) => {
backgroundColor = color
}
export const getBackgroundColor = () => backgroundColor
function iconsDir() {
return app.isPackaged ? join(process.resourcesPath, "icons") : join(root, "../../resources/icons")
}
function iconPath() {
const ext = process.platform === "win32" ? "ico" : "png"
return join(iconsDir(), `icon.${ext}`)
}
function tone() {
return nativeTheme.shouldUseDarkColors ? "dark" : "light"
}
function overlay(theme: Partial<TitlebarTheme> = {}, zoom = 1) {
const mode = theme.mode ?? tone()
return {
color: "#00000000",
symbolColor: mode === "dark" ? "white" : "black",
height: Math.max(titlebarHeight, Math.round(titlebarHeight * zoom)),
}
}
export function setTitlebar(win: BrowserWindow, theme: Partial<TitlebarTheme> = {}) {
titlebarThemes.set(win, theme)
updateTitlebar(win)
}
export function updateTitlebar(win: BrowserWindow) {
if (process.platform !== "win32") return
win.setTitleBarOverlay(overlay(titlebarThemes.get(win), win.webContents.getZoomFactor()))
}
export function setDockIcon() {
if (process.platform !== "darwin") return
const icon = nativeImage.createFromPath(join(iconsDir(), "dock.png"))
if (!icon.isEmpty()) app.dock?.setIcon(icon)
}
export function createMainWindow() {
const state = windowState({
defaultWidth: 1280,
defaultHeight: 800,
})
const mode = tone()
const bg = getBackgroundColor()
const win = new BrowserWindow({
x: state.x,
y: state.y,
width: state.width,
height: state.height,
show: false,
title: "OpenCode",
icon: iconPath(),
backgroundColor: bg,
...(process.platform === "darwin"
? {
titleBarStyle: "hidden" as const,
trafficLightPosition: { x: 12, y: 14 },
}
: {}),
...(process.platform === "win32"
? {
frame: false,
titleBarStyle: "hidden" as const,
titleBarOverlay: overlay({ mode }),
}
: {}),
webPreferences: {
preload: join(root, "../preload/index.js"),
contextIsolation: true,
nodeIntegration: false,
sandbox: true,
},
})
win.webContents.session.webRequest.onBeforeSendHeaders((details, callback) => {
const { requestHeaders } = details
upsertKeyValue(requestHeaders, "Access-Control-Allow-Origin", ["*"])
callback({ requestHeaders })
})
win.webContents.session.webRequest.onHeadersReceived((details, callback) => {
const { responseHeaders = {} } = details
upsertKeyValue(responseHeaders, "Access-Control-Allow-Origin", ["*"])
upsertKeyValue(responseHeaders, "Access-Control-Allow-Headers", ["*"])
callback({ responseHeaders })
})
state.manage(win)
loadWindow(win, "index.html")
wireZoom(win)
win.once("ready-to-show", () => {
win.show()
})
return win
}
export function createLoadingWindow() {
const mode = tone()
const bg = getBackgroundColor()
const win = new BrowserWindow({
width: 640,
height: 480,
resizable: false,
center: true,
show: true,
icon: iconPath(),
backgroundColor: bg,
...(process.platform === "darwin" ? { titleBarStyle: "hidden" as const } : {}),
...(process.platform === "win32"
? {
frame: false,
titleBarStyle: "hidden" as const,
titleBarOverlay: overlay({ mode }),
}
: {}),
webPreferences: {
preload: join(root, "../preload/index.js"),
contextIsolation: true,
nodeIntegration: false,
sandbox: true,
},
})
loadWindow(win, "loading.html")
return win
}
export function registerRendererProtocol() {
if (protocol.isProtocolHandled(rendererProtocol)) return
protocol.handle(rendererProtocol, (request) => {
const url = new URL(request.url)
if (url.host !== rendererHost) {
return new Response("Not found", { status: 404 })
}
const file = resolve(rendererRoot, `.${decodeURIComponent(url.pathname)}`)
const rel = relative(rendererRoot, file)
if (rel.startsWith("..") || isAbsolute(rel)) {
return new Response("Not found", { status: 404 })
}
return net.fetch(pathToFileURL(file).toString())
})
}
function loadWindow(win: BrowserWindow, html: string) {
const devUrl = process.env.ELECTRON_RENDERER_URL
if (devUrl) {
const url = new URL(html, devUrl)
void win.loadURL(url.toString())
return
}
void win.loadURL(`${rendererProtocol}://${rendererHost}/${html}`)
}
function wireZoom(win: BrowserWindow) {
win.webContents.setZoomFactor(1)
win.webContents.on("zoom-changed", () => {
win.webContents.setZoomFactor(1)
updateTitlebar(win)
})
}
function upsertKeyValue(obj: Record<string, any>, keyToChange: string, value: any) {
const keyToChangeLower = keyToChange.toLowerCase()
for (const key of Object.keys(obj)) {
if (key.toLowerCase() === keyToChangeLower) {
// Reassign old key
obj[key] = value
// Done
return
}
}
// Insert at end instead
obj[keyToChange] = value
}

View file

@ -1,190 +0,0 @@
import { Menu, MenuItem, PredefinedMenuItem, Submenu } from "@tauri-apps/api/menu"
import { openUrl } from "@tauri-apps/plugin-opener"
import { type as ostype } from "@tauri-apps/plugin-os"
import { relaunch } from "@tauri-apps/plugin-process"
import { commands } from "./bindings"
import { installCli } from "./cli"
import { initI18n, t } from "./i18n"
import { runUpdater, UPDATER_ENABLED } from "./updater"
export async function createMenu(trigger: (id: string) => void) {
if (ostype() !== "macos") return
await initI18n()
const menu = await Menu.new({
items: [
await Submenu.new({
text: t("desktop.menu.app"),
items: [
await PredefinedMenuItem.new({
item: { About: null },
}),
await MenuItem.new({
enabled: UPDATER_ENABLED,
action: () => runUpdater({ alertOnFail: true }),
text: t("desktop.menu.checkForUpdates"),
}),
await MenuItem.new({
action: () => installCli(),
text: t("desktop.menu.installCli"),
}),
await MenuItem.new({
action: async () => window.location.reload(),
text: t("desktop.menu.reloadWebview"),
}),
await MenuItem.new({
action: async () => {
await commands.killSidecar().catch(() => undefined)
await relaunch().catch(() => undefined)
},
text: t("desktop.menu.restart"),
}),
await PredefinedMenuItem.new({
item: "Separator",
}),
await PredefinedMenuItem.new({
item: "Hide",
}),
await PredefinedMenuItem.new({
item: "HideOthers",
}),
await PredefinedMenuItem.new({
item: "ShowAll",
}),
await PredefinedMenuItem.new({
item: "Separator",
}),
await PredefinedMenuItem.new({
item: "Quit",
}),
].filter(Boolean),
}),
await Submenu.new({
text: t("desktop.menu.file"),
items: [
await MenuItem.new({
text: t("desktop.menu.file.newSession"),
accelerator: "Shift+Cmd+S",
action: () => trigger("session.new"),
}),
await MenuItem.new({
text: t("desktop.menu.file.openProject"),
accelerator: "Cmd+O",
action: () => trigger("project.open"),
}),
await PredefinedMenuItem.new({
item: "Separator",
}),
await PredefinedMenuItem.new({
item: "CloseWindow",
}),
],
}),
await Submenu.new({
text: t("desktop.menu.edit"),
items: [
await PredefinedMenuItem.new({
item: "Undo",
}),
await PredefinedMenuItem.new({
item: "Redo",
}),
await PredefinedMenuItem.new({
item: "Separator",
}),
await PredefinedMenuItem.new({
item: "Cut",
}),
await PredefinedMenuItem.new({
item: "Copy",
}),
await PredefinedMenuItem.new({
item: "Paste",
}),
await PredefinedMenuItem.new({
item: "SelectAll",
}),
],
}),
await Submenu.new({
text: t("desktop.menu.view"),
items: [
await MenuItem.new({
action: () => trigger("sidebar.toggle"),
text: t("desktop.menu.view.toggleSidebar"),
accelerator: "Cmd+B",
}),
await MenuItem.new({
action: () => trigger("terminal.toggle"),
text: t("desktop.menu.view.toggleTerminal"),
accelerator: "Ctrl+`",
}),
await MenuItem.new({
action: () => trigger("fileTree.toggle"),
text: t("desktop.menu.view.toggleFileTree"),
}),
await PredefinedMenuItem.new({
item: "Separator",
}),
await MenuItem.new({
action: () => trigger("common.goBack"),
text: t("desktop.menu.view.back"),
}),
await MenuItem.new({
action: () => trigger("common.goForward"),
text: t("desktop.menu.view.forward"),
}),
await PredefinedMenuItem.new({
item: "Separator",
}),
await MenuItem.new({
action: () => trigger("session.previous"),
text: t("desktop.menu.view.previousSession"),
accelerator: "Option+ArrowUp",
}),
await MenuItem.new({
action: () => trigger("session.next"),
text: t("desktop.menu.view.nextSession"),
accelerator: "Option+ArrowDown",
}),
await PredefinedMenuItem.new({
item: "Separator",
}),
],
}),
await Submenu.new({
text: t("desktop.menu.help"),
items: [
// missing native macos search
await MenuItem.new({
action: () => openUrl("https://opencode.ai/docs"),
text: t("desktop.menu.help.documentation"),
}),
await MenuItem.new({
action: () => openUrl("https://discord.com/invite/opencode"),
text: t("desktop.menu.help.supportForum"),
}),
await PredefinedMenuItem.new({
item: "Separator",
}),
// await MenuItem.new({
// text: "Release Notes",
// }),
await PredefinedMenuItem.new({
item: "Separator",
}),
await MenuItem.new({
action: () => openUrl("https://github.com/anomalyco/opencode/issues/new?template=feature_request.yml"),
text: t("desktop.menu.help.shareFeedback"),
}),
await MenuItem.new({
action: () => openUrl("https://github.com/anomalyco/opencode/issues/new?template=bug_report.yml"),
text: t("desktop.menu.help.reportBug"),
}),
],
}),
],
})
void menu.setAsAppMenu()
}

View file

@ -0,0 +1,71 @@
import { contextBridge, ipcRenderer } from "electron"
import type { ElectronAPI, InitStep, SqliteMigrationProgress } from "./types"
const api: ElectronAPI = {
killSidecar: () => ipcRenderer.invoke("kill-sidecar"),
installCli: () => ipcRenderer.invoke("install-cli"),
awaitInitialization: (onStep) => {
const handler = (_: unknown, step: InitStep) => onStep(step)
ipcRenderer.on("init-step", handler)
return ipcRenderer.invoke("await-initialization").finally(() => {
ipcRenderer.removeListener("init-step", handler)
})
},
getWindowConfig: () => ipcRenderer.invoke("get-window-config"),
consumeInitialDeepLinks: () => ipcRenderer.invoke("consume-initial-deep-links"),
getDefaultServerUrl: () => ipcRenderer.invoke("get-default-server-url"),
setDefaultServerUrl: (url) => ipcRenderer.invoke("set-default-server-url", url),
getWslConfig: () => ipcRenderer.invoke("get-wsl-config"),
setWslConfig: (config) => ipcRenderer.invoke("set-wsl-config", config),
getDisplayBackend: () => ipcRenderer.invoke("get-display-backend"),
setDisplayBackend: (backend) => ipcRenderer.invoke("set-display-backend", backend),
parseMarkdownCommand: (markdown) => ipcRenderer.invoke("parse-markdown", markdown),
checkAppExists: (appName) => ipcRenderer.invoke("check-app-exists", appName),
wslPath: (path, mode) => ipcRenderer.invoke("wsl-path", path, mode),
resolveAppPath: (appName) => ipcRenderer.invoke("resolve-app-path", appName),
storeGet: (name, key) => ipcRenderer.invoke("store-get", name, key),
storeSet: (name, key, value) => ipcRenderer.invoke("store-set", name, key, value),
storeDelete: (name, key) => ipcRenderer.invoke("store-delete", name, key),
storeClear: (name) => ipcRenderer.invoke("store-clear", name),
storeKeys: (name) => ipcRenderer.invoke("store-keys", name),
storeLength: (name) => ipcRenderer.invoke("store-length", name),
getWindowCount: () => ipcRenderer.invoke("get-window-count"),
onSqliteMigrationProgress: (cb) => {
const handler = (_: unknown, progress: SqliteMigrationProgress) => cb(progress)
ipcRenderer.on("sqlite-migration-progress", handler)
return () => ipcRenderer.removeListener("sqlite-migration-progress", handler)
},
onMenuCommand: (cb) => {
const handler = (_: unknown, id: string) => cb(id)
ipcRenderer.on("menu-command", handler)
return () => ipcRenderer.removeListener("menu-command", handler)
},
onDeepLink: (cb) => {
const handler = (_: unknown, urls: string[]) => cb(urls)
ipcRenderer.on("deep-link", handler)
return () => ipcRenderer.removeListener("deep-link", handler)
},
openDirectoryPicker: (opts) => ipcRenderer.invoke("open-directory-picker", opts),
openFilePicker: (opts) => ipcRenderer.invoke("open-file-picker", opts),
saveFilePicker: (opts) => ipcRenderer.invoke("save-file-picker", opts),
openLink: (url) => ipcRenderer.send("open-link", url),
openPath: (path, app) => ipcRenderer.invoke("open-path", path, app),
readClipboardImage: () => ipcRenderer.invoke("read-clipboard-image"),
showNotification: (title, body) => ipcRenderer.send("show-notification", title, body),
getWindowFocused: () => ipcRenderer.invoke("get-window-focused"),
setWindowFocus: () => ipcRenderer.invoke("set-window-focus"),
showWindow: () => ipcRenderer.invoke("show-window"),
relaunch: () => ipcRenderer.send("relaunch"),
getZoomFactor: () => ipcRenderer.invoke("get-zoom-factor"),
setZoomFactor: (factor) => ipcRenderer.invoke("set-zoom-factor", factor),
setTitlebar: (theme) => ipcRenderer.invoke("set-titlebar", theme),
loadingWindowComplete: () => ipcRenderer.send("loading-window-complete"),
runUpdater: (alertOnFail) => ipcRenderer.invoke("run-updater", alertOnFail),
checkUpdate: () => ipcRenderer.invoke("check-update"),
installUpdate: () => ipcRenderer.invoke("install-update"),
setBackgroundColor: (color: string) => ipcRenderer.invoke("set-background-color", color),
}
contextBridge.exposeInMainWorld("api", api)

View file

@ -0,0 +1,86 @@
import { Data } from "effect"
export type InitStep = Data.TaggedEnum<{
ServerWaiting: {}
SqliteWaiting: {}
Done: {}
}>
export const InitStep = Data.taggedEnum<InitStep>()
export type ServerReadyData = {
url: string
username: string | null
password: string | null
}
export type SqliteMigrationProgress = { type: "InProgress"; value: number } | { type: "Done" }
export type WslConfig = { enabled: boolean }
export type LinuxDisplayBackend = "wayland" | "auto"
export type TitlebarTheme = {
mode: "light" | "dark"
}
export type WindowConfig = {
updaterEnabled: boolean
}
export type ElectronAPI = {
killSidecar: () => Promise<void>
installCli: () => Promise<string>
awaitInitialization: (onStep: (step: InitStep) => void) => Promise<ServerReadyData>
getWindowConfig: () => Promise<WindowConfig>
consumeInitialDeepLinks: () => Promise<string[]>
getDefaultServerUrl: () => Promise<string | null>
setDefaultServerUrl: (url: string | null) => Promise<void>
getWslConfig: () => Promise<WslConfig>
setWslConfig: (config: WslConfig) => Promise<void>
getDisplayBackend: () => Promise<LinuxDisplayBackend | null>
setDisplayBackend: (backend: LinuxDisplayBackend | null) => Promise<void>
parseMarkdownCommand: (markdown: string) => Promise<string>
checkAppExists: (appName: string) => Promise<boolean>
wslPath: (path: string, mode: "windows" | "linux" | null) => Promise<string>
resolveAppPath: (appName: string) => Promise<string | null>
storeGet: (name: string, key: string) => Promise<string | null>
storeSet: (name: string, key: string, value: string) => Promise<void>
storeDelete: (name: string, key: string) => Promise<void>
storeClear: (name: string) => Promise<void>
storeKeys: (name: string) => Promise<string[]>
storeLength: (name: string) => Promise<number>
getWindowCount: () => Promise<number>
onSqliteMigrationProgress: (cb: (progress: SqliteMigrationProgress) => void) => () => void
onMenuCommand: (cb: (id: string) => void) => () => void
onDeepLink: (cb: (urls: string[]) => void) => () => void
openDirectoryPicker: (opts?: {
multiple?: boolean
title?: string
defaultPath?: string
}) => Promise<string | string[] | null>
openFilePicker: (opts?: {
multiple?: boolean
title?: string
defaultPath?: string
accept?: string[]
extensions?: string[]
}) => Promise<string | string[] | null>
saveFilePicker: (opts?: { title?: string; defaultPath?: string }) => Promise<string | null>
openLink: (url: string) => void
openPath: (path: string, app?: string) => Promise<void>
readClipboardImage: () => Promise<{ buffer: ArrayBuffer; width: number; height: number } | null>
showNotification: (title: string, body?: string) => void
getWindowFocused: () => Promise<boolean>
setWindowFocus: () => Promise<void>
showWindow: () => Promise<void>
relaunch: () => void
getZoomFactor: () => Promise<number>
setZoomFactor: (factor: number) => Promise<void>
setTitlebar: (theme: TitlebarTheme) => Promise<void>
loadingWindowComplete: () => void
runUpdater: (alertOnFail: boolean) => Promise<void>
checkUpdate: () => Promise<{ updateAvailable: boolean; version?: string }>
installUpdate: () => Promise<void>
setBackgroundColor: (color: string) => Promise<void>
}

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

10
packages/desktop/src/renderer/env.d.ts vendored Normal file
View file

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

View file

@ -0,0 +1,62 @@
import { describe, expect, test } from "bun:test"
import { join, dirname, resolve } from "node:path"
import { existsSync } from "node:fs"
import { fileURLToPath } from "node:url"
const dir = dirname(fileURLToPath(import.meta.url))
const root = resolve(dir, "../..")
const html = async (name: string) => Bun.file(join(dir, name)).text()
/**
* Packaged Electron windows load renderer HTML via the privileged `oc://`
* protocol. Root-relative asset paths like `src="/foo.js"` would resolve from
* the protocol origin root instead of relative to the current HTML entrypoint.
*
* All local resource references must use relative paths (`./`).
*/
describe("electron renderer html", () => {
for (const name of ["index.html", "loading.html"]) {
describe(name, () => {
test("script src attributes use relative paths", async () => {
const content = await html(name)
const srcs = [...content.matchAll(/\bsrc=["']([^"']+)["']/g)].map((m) => m[1])
for (const src of srcs) {
expect(src).not.toMatch(/^\/[^/]/)
}
})
test("link href attributes use relative paths", async () => {
const content = await html(name)
const hrefs = [...content.matchAll(/<link[^>]+href=["']([^"']+)["']/g)].map((m) => m[1])
for (const href of hrefs) {
expect(href).not.toMatch(/^\/[^/]/)
}
})
test("no web manifest link (not applicable in Electron)", async () => {
const content = await html(name)
expect(content).not.toContain('rel="manifest"')
})
})
}
})
/**
* Vite resolves `publicDir` relative to `root`, not the config file.
* This test reads the actual values from electron.vite.config.ts to catch
* regressions where the publicDir path no longer resolves correctly
* after the renderer root is accounted for.
*/
describe("electron vite publicDir", () => {
test("configured publicDir resolves to a directory with oc-theme-preload.js", async () => {
const config = await Bun.file(join(root, "electron.vite.config.ts")).text()
const pub = config.match(/publicDir:\s*["']([^"']+)["']/)
const rendererRoot = config.match(/root:\s*["']([^"']+)["']/)
expect(pub).not.toBeNull()
expect(rendererRoot).not.toBeNull()
const resolved = resolve(root, rendererRoot![1], pub![1])
expect(existsSync(resolved)).toBe(true)
expect(existsSync(join(resolved, "oc-theme-preload.js"))).toBe(true)
})
})

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

@ -1,5 +1,4 @@
import * as i18n from "@solid-primitives/i18n"
import { Store } from "@tauri-apps/plugin-store"
import { dict as desktopEn } from "./en"
import { dict as desktopZh } from "./zh"
@ -17,21 +16,21 @@ 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"
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"
@ -175,10 +174,7 @@ export function initI18n(): Promise<Locale> {
if (cached) return cached
const promise = (async () => {
const store = await Store.load("opencode.global.dat").catch(() => null)
if (!store) return state.locale
const raw = await store.get("language").catch(() => null)
const raw = await window.api.storeGet("opencode.global.dat", "language").catch(() => null)
const value = parseStored(raw)
const next = pickLocale(value) ?? state.locale

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,22 @@
<!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" />
<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,377 @@
// @refresh reload
import {
ACCEPTED_FILE_EXTENSIONS,
ACCEPTED_FILE_TYPES,
AppBaseProviders,
AppInterface,
handleNotificationClick,
loadLocaleDict,
normalizeLocale,
type Locale,
type Platform,
PlatformProvider,
ServerConnection,
useCommand,
} from "@opencode-ai/app"
import * as Sentry from "@sentry/solid"
import type { AsyncStorage } from "@solid-primitives/storage"
import { MemoryRouter } from "@solidjs/router"
import { createEffect, createResource, onCleanup, onMount, Show } from "solid-js"
import { render } from "solid-js/web"
import pkg from "../../package.json"
import { initI18n, t } from "./i18n"
import { webviewZoom } from "./webview-zoom"
import "./styles.css"
import { useTheme } from "@opencode-ai/ui/theme"
const root = document.getElementById("root")
if (import.meta.env.DEV && !(root instanceof HTMLElement)) {
throw new Error(t("error.dev.rootNotFound"))
}
if (import.meta.env.VITE_SENTRY_DSN) {
Sentry.init({
dsn: import.meta.env.VITE_SENTRY_DSN,
environment: import.meta.env.VITE_SENTRY_ENVIRONMENT ?? import.meta.env.MODE,
release: import.meta.env.VITE_SENTRY_RELEASE ?? `desktop@${pkg.version}`,
initialScope: {
tags: {
platform: "desktop",
},
},
integrations: (integrations) => {
return integrations.filter(
(i) =>
i.name !== "Breadcrumbs" && !(import.meta.env.OPENCODE_CHANNEL === "prod" && i.name === "GlobalHandlers"),
)
},
})
}
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 = () => {
void window.api.consumeInitialDeepLinks().then((urls) => emitDeepLinks(urls))
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 isWslEnabled = async () => {
if (os !== "windows") return false
return window.api
.getWslConfig()
.then((config) => config.enabled)
.catch(() => false)
}
const wslHome = async () => {
if (!(await isWslEnabled())) return undefined
return window.api.wslPath("~", "windows").catch(() => undefined)
}
const handleWslPicker = async <T extends string | string[]>(result: T | null): Promise<T | null> => {
if (!result || !(await isWslEnabled())) 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"),
accept: opts?.accept ?? ACCEPTED_FILE_TYPES,
extensions: opts?.extensions ?? ACCEPTED_FILE_EXTENSIONS,
})
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 (await isWslEnabled()) {
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 () => {
const config = await window.api.getWindowConfig().catch(() => ({ updaterEnabled: false }))
if (!config.updaterEnabled) return { updateAvailable: false }
return window.api.checkUpdate()
},
updateAndRestart: async () => {
const config = await window.api.getWindowConfig().catch(() => ({ updaterEnabled: false }))
if (!config.updaterEnabled) 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: () => isWslEnabled(),
setWslEnabled: async (enabled) => {
await window.api.setWslConfig({ enabled })
},
getDefaultServer: async () => {
const url = await window.api.getDefaultServerUrl().catch(() => null)
if (!url) return null
return ServerConnection.Key.make(url)
},
setDefaultServer: async (url: string | null) => {
await window.api.setDefaultServerUrl(url)
},
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()
const [windowConfig] = createResource(() => window.api.getWindowConfig().catch(() => ({ updaterEnabled: false })))
const loadLocale = async () => {
const current = await platform.storage?.("opencode.global.dat").getItem("language")
const legacy = current ? undefined : await platform.storage?.().getItem("language.v1")
const raw = current ?? legacy
if (!raw) return
const locale = raw.match(/"locale"\s*:\s*"([^"]+)"/)?.[1]
if (!locale) return
const next = normalizeLocale(locale)
if (next !== "en") await loadLocaleDict(next)
return next satisfies Locale
}
const [windowCount] = createResource(() => window.api.getWindowCount())
// Fetch sidecar credentials (available immediately, before health check)
const [sidecar] = createResource(() => window.api.awaitInitialization(() => undefined))
const [defaultServer] = createResource(() =>
platform.getDefaultServer?.().then((url) => {
if (url) return ServerConnection.key({ type: "http", http: { url } })
}),
)
const [locale] = createResource(loadLocale)
const servers = () => {
const data = sidecar()
if (!data) return []
const server: ServerConnection.Sidecar = {
displayName: "Local Server",
type: "sidecar",
variant: "base",
http: {
url: data.url,
username: data.username ?? undefined,
password: data.password ?? undefined,
},
}
return [server] as ServerConnection.Any[]
}
function handleClick(e: MouseEvent) {
const link = (e.target as HTMLElement).closest("a.external-link") as HTMLAnchorElement | null
if (link?.href) {
e.preventDefault()
platform.openLink(link.href)
}
}
function Inner() {
const cmd = useCommand()
menuTrigger = (id) => cmd.trigger(id)
const theme = useTheme()
createEffect(() => {
theme.themeId()
theme.mode()
const bg = getComputedStyle(document.documentElement).getPropertyValue("--background-base").trim()
if (bg) {
void window.api.setBackgroundColor(bg)
}
})
return null
}
onMount(() => {
document.addEventListener("click", handleClick)
onCleanup(() => {
document.removeEventListener("click", handleClick)
})
})
return (
<PlatformProvider value={platform}>
<AppBaseProviders locale={locale.latest}>
<Show
when={
!defaultServer.loading &&
!sidecar.loading &&
!windowConfig.loading &&
!windowCount.loading &&
!locale.loading
}
>
{(_) => {
return (
<AppInterface
defaultServer={defaultServer.latest ?? ServerConnection.Key.make("sidecar")}
servers={servers()}
router={MemoryRouter}
>
<Inner />
</AppInterface>
)
}}
</Show>
</AppBaseProviders>
</PlatformProvider>
)
}, root!)

View file

@ -0,0 +1,22 @@
<!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" />
<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

@ -1,40 +1,30 @@
import { render } from "solid-js/web"
import { MetaProvider } from "@solidjs/meta"
import { render } from "solid-js/web"
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 { commands, events, InitStep } from "./bindings"
import { Channel } from "@tauri-apps/api/core"
import { initI18n, t } from "./i18n"
import type { InitStep, SqliteMigrationProgress } from "../preload/types"
const root = document.getElementById("root")!
const lines = [
t("desktop.loading.status.initial"),
t("desktop.loading.status.migrating"),
t("desktop.loading.status.waiting"),
]
const lines = ["Just a moment...", "Migrating your database", "This may take a couple of minutes"]
const delays = [3000, 9000]
void initI18n()
render(() => {
const [step, setStep] = createSignal<InitStep | null>(null)
const [line, setLine] = createSignal(0)
const [percent, setPercent] = createSignal(0)
const phase = createMemo(() => step()?.phase)
const phase = createMemo(() => step()?._tag)
const value = createMemo(() => {
if (phase() === "done") return 100
if (phase() === "Done") return 100
return Math.max(25, Math.min(100, percent()))
})
const channel = new Channel<InitStep>()
channel.onmessage = (next) => setStep(next)
commands.awaitInitialization(channel as any).catch(() => undefined)
window.api.awaitInitialization((next) => setStep(next)).catch(() => undefined)
onMount(() => {
setLine(0)
@ -42,28 +32,31 @@ render(() => {
const timers = delays.map((ms, i) => setTimeout(() => setLine(i + 1), ms))
const listener = events.sqliteMigrationProgress.listen((e) => {
if (e.payload.type === "InProgress") setPercent(Math.max(0, Math.min(100, e.payload.value)))
if (e.payload.type === "Done") setPercent(100)
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)
setStep({ _tag: "Done" })
}
})
onCleanup(() => {
void listener.then((cb) => cb())
listener()
timers.forEach(clearTimeout)
})
})
createEffect(() => {
if (phase() !== "done") return
if (phase() !== "Done") return
const timer = setTimeout(() => events.loadingWindowComplete.emit(null), 1000)
const timer = setTimeout(() => window.api.loadingWindowComplete(), 1000)
onCleanup(() => clearTimeout(timer))
})
const status = createMemo(() => {
if (phase() === "done") return t("desktop.loading.status.done")
if (phase() === "sqlite_waiting") return lines[line()]
return t("desktop.loading.status.initial")
if (phase() === "Done") return "All done"
if (phase() === "SqliteWaiting") return lines[line()]
return "Just a moment..."
})
return (
@ -79,7 +72,7 @@ render(() => {
<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={t("desktop.loading.progressAria")}
aria-label="Database migration progress"
getValueLabel={({ value }) => `${Math.round(value)}%`}
/>
</div>

View file

View file

@ -0,0 +1,12 @@
import { initI18n, t } from "./i18n"
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,55 @@
// 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)
let requestedZoom = 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) => {
requestedZoom = next
void window.api
.setZoomFactor(next)
.then(() => {
if (requestedZoom !== next) return
setWebviewZoom(next)
})
.catch(() => {
if (requestedZoom !== next) return
requestedZoom = webviewZoom()
})
}
window.addEventListener("keydown", (event) => {
if (!(OS_NAME === "macos" ? event.metaKey : event.ctrlKey)) return
if (event.key === "-") {
event.preventDefault()
applyZoom(clamp(requestedZoom - 0.2))
return
}
if (event.key === "=" || event.key === "+") {
event.preventDefault()
applyZoom(clamp(requestedZoom + 0.2))
return
}
if (event.key === "0") {
event.preventDefault()
applyZoom(1)
}
})
export { webviewZoom }

View file

@ -1,7 +0,0 @@
button.decorum-tb-btn,
button#decorum-tb-minimize,
button#decorum-tb-maximize,
button#decorum-tb-close,
div[data-tauri-decorum-tb] {
height: calc(var(--spacing) * 10) !important;
}

View file

@ -1,51 +0,0 @@
import { check } from "@tauri-apps/plugin-updater"
import { relaunch } from "@tauri-apps/plugin-process"
import { ask, message } from "@tauri-apps/plugin-dialog"
import { type as ostype } from "@tauri-apps/plugin-os"
import { initI18n, t } from "./i18n"
import { commands } from "./bindings"
export const UPDATER_ENABLED = window.__OPENCODE__?.updaterEnabled ?? false
export async function runUpdater({ alertOnFail }: { alertOnFail: boolean }) {
await initI18n()
let update
try {
update = await check()
} catch {
if (alertOnFail)
await message(t("desktop.updater.checkFailed.message"), { title: t("desktop.updater.checkFailed.title") })
return
}
if (!update) {
if (alertOnFail) await message(t("desktop.updater.none.message"), { title: t("desktop.updater.none.title") })
return
}
try {
await update.download()
} catch {
if (alertOnFail)
await message(t("desktop.updater.downloadFailed.message"), { title: t("desktop.updater.downloadFailed.title") })
return
}
const shouldUpdate = await ask(t("desktop.updater.downloaded.prompt", { version: update.version }), {
title: t("desktop.updater.downloaded.title"),
})
if (!shouldUpdate) return
try {
if (ostype() === "windows") await commands.killSidecar()
await update.install()
} catch {
await message(t("desktop.updater.installFailed.message"), { title: t("desktop.updater.installFailed.title") })
return
}
await commands.killSidecar()
await relaunch()
}

View file

@ -1,37 +0,0 @@
// Copyright 2019-2024 Tauri Programme within The Commons Conservancy
// SPDX-License-Identifier: Apache-2.0
// SPDX-License-Identifier: MIT
import { invoke } from "@tauri-apps/api/core"
import { type as ostype } from "@tauri-apps/plugin-os"
import { createSignal } from "solid-js"
const OS_NAME = ostype()
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 invoke("plugin:webview|set_webview_zoom", {
value: 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 }