Browse Source

feat(desktop): add WSL backend mode (#12914)

Brendan Allan 2 weeks ago
parent
commit
213a87234d

+ 1 - 1
packages/app/src/app.tsx

@@ -43,7 +43,7 @@ function UiI18nBridge(props: ParentProps) {
 
 declare global {
   interface Window {
-    __OPENCODE__?: { updaterEnabled?: boolean; serverPassword?: string; deepLinks?: string[] }
+    __OPENCODE__?: { updaterEnabled?: boolean; serverPassword?: string; deepLinks?: string[]; wsl?: boolean }
   }
 }
 

+ 28 - 0
packages/app/src/components/settings-general.tsx

@@ -367,6 +367,34 @@ export const SettingsGeneral: Component = () => {
           </div>
         </div>
 
+        <Show when={platform.platform === "desktop" && platform.os === "windows" && platform.getWslEnabled}>
+          {(_) => {
+            const [enabledResource, actions] = createResource(() => platform.getWslEnabled?.())
+            const enabled = () => (enabledResource.state === "pending" ? undefined : enabledResource.latest)
+
+            return (
+              <div class="flex flex-col gap-1">
+                <h3 class="text-14-medium text-text-strong pb-2">{language.t("settings.desktop.section.wsl")}</h3>
+
+                <div class="bg-surface-raised-base px-4 rounded-lg">
+                  <SettingsRow
+                    title={language.t("settings.desktop.wsl.title")}
+                    description={language.t("settings.desktop.wsl.description")}
+                  >
+                    <div data-action="settings-wsl">
+                      <Switch
+                        checked={enabled() ?? false}
+                        disabled={enabledResource.state === "pending"}
+                        onChange={(checked) => platform.setWslEnabled?.(checked)?.finally(() => actions.refetch())}
+                      />
+                    </div>
+                  </SettingsRow>
+                </div>
+              </div>
+            )
+          }}
+        </Show>
+
         {/* Updates Section */}
         <div class="flex flex-col gap-1">
           <h3 class="text-14-medium text-text-strong pb-2">{language.t("settings.general.section.updates")}</h3>

+ 6 - 0
packages/app/src/context/platform.tsx

@@ -57,6 +57,12 @@ export type Platform = {
   /** Set the default server URL to use on app startup (platform-specific) */
   setDefaultServerUrl?(url: string | null): Promise<void> | void
 
+  /** Get the configured WSL integration (desktop only) */
+  getWslEnabled?(): Promise<boolean>
+
+  /** Set the configured WSL integration (desktop only) */
+  setWslEnabled?(config: boolean): Promise<void> | void
+
   /** Get the preferred display backend (desktop only) */
   getDisplayBackend?(): Promise<DisplayBackend | null> | DisplayBackend | null
 

+ 3 - 0
packages/app/src/i18n/ar.ts

@@ -508,6 +508,9 @@ export const dict = {
   "settings.section.server": "الخادم",
   "settings.tab.general": "عام",
   "settings.tab.shortcuts": "اختصارات",
+  "settings.desktop.section.wsl": "WSL",
+  "settings.desktop.wsl.title": "WSL integration",
+  "settings.desktop.wsl.description": "Run the OpenCode server inside WSL on Windows.",
 
   "settings.general.section.appearance": "المظهر",
   "settings.general.section.notifications": "إشعارات النظام",

+ 3 - 0
packages/app/src/i18n/br.ts

@@ -512,6 +512,9 @@ export const dict = {
   "settings.section.server": "Servidor",
   "settings.tab.general": "Geral",
   "settings.tab.shortcuts": "Atalhos",
+  "settings.desktop.section.wsl": "WSL",
+  "settings.desktop.wsl.title": "WSL integration",
+  "settings.desktop.wsl.description": "Run the OpenCode server inside WSL on Windows.",
 
   "settings.general.section.appearance": "Aparência",
   "settings.general.section.notifications": "Notificações do sistema",

+ 3 - 0
packages/app/src/i18n/bs.ts

@@ -539,6 +539,9 @@ export const dict = {
   "settings.section.server": "Server",
   "settings.tab.general": "Opšte",
   "settings.tab.shortcuts": "Prečice",
+  "settings.desktop.section.wsl": "WSL",
+  "settings.desktop.wsl.title": "WSL integration",
+  "settings.desktop.wsl.description": "Run the OpenCode server inside WSL on Windows.",
 
   "settings.general.section.appearance": "Izgled",
   "settings.general.section.notifications": "Sistemske obavijesti",

+ 3 - 0
packages/app/src/i18n/da.ts

@@ -512,6 +512,9 @@ export const dict = {
   "settings.section.server": "Server",
   "settings.tab.general": "Generelt",
   "settings.tab.shortcuts": "Genveje",
+  "settings.desktop.section.wsl": "WSL",
+  "settings.desktop.wsl.title": "WSL integration",
+  "settings.desktop.wsl.description": "Run the OpenCode server inside WSL on Windows.",
 
   "settings.general.section.appearance": "Udseende",
   "settings.general.section.notifications": "Systemmeddelelser",

+ 3 - 0
packages/app/src/i18n/de.ts

@@ -556,6 +556,9 @@ export const dict = {
   "settings.section.server": "Server",
   "settings.tab.general": "Allgemein",
   "settings.tab.shortcuts": "Tastenkombinationen",
+  "settings.desktop.section.wsl": "WSL",
+  "settings.desktop.wsl.title": "WSL integration",
+  "settings.desktop.wsl.description": "Run the OpenCode server inside WSL on Windows.",
 
   "settings.general.section.appearance": "Erscheinungsbild",
   "settings.general.section.notifications": "Systembenachrichtigungen",

+ 3 - 0
packages/app/src/i18n/en.ts

@@ -583,6 +583,9 @@ export const dict = {
   "settings.section.server": "Server",
   "settings.tab.general": "General",
   "settings.tab.shortcuts": "Shortcuts",
+  "settings.desktop.section.wsl": "WSL",
+  "settings.desktop.wsl.title": "WSL integration",
+  "settings.desktop.wsl.description": "Run the OpenCode server inside WSL on Windows.",
 
   "settings.general.section.appearance": "Appearance",
   "settings.general.section.notifications": "System notifications",

+ 3 - 0
packages/app/src/i18n/es.ts

@@ -515,6 +515,9 @@ export const dict = {
   "settings.section.server": "Servidor",
   "settings.tab.general": "General",
   "settings.tab.shortcuts": "Atajos",
+  "settings.desktop.section.wsl": "WSL",
+  "settings.desktop.wsl.title": "WSL integration",
+  "settings.desktop.wsl.description": "Run the OpenCode server inside WSL on Windows.",
 
   "settings.general.section.appearance": "Apariencia",
   "settings.general.section.notifications": "Notificaciones del sistema",

+ 3 - 0
packages/app/src/i18n/fr.ts

@@ -522,6 +522,9 @@ export const dict = {
   "settings.section.server": "Serveur",
   "settings.tab.general": "Général",
   "settings.tab.shortcuts": "Raccourcis",
+  "settings.desktop.section.wsl": "WSL",
+  "settings.desktop.wsl.title": "WSL integration",
+  "settings.desktop.wsl.description": "Run the OpenCode server inside WSL on Windows.",
 
   "settings.general.section.appearance": "Apparence",
   "settings.general.section.notifications": "Notifications système",

+ 3 - 0
packages/app/src/i18n/ja.ts

@@ -507,6 +507,9 @@ export const dict = {
   "settings.section.server": "サーバー",
   "settings.tab.general": "一般",
   "settings.tab.shortcuts": "ショートカット",
+  "settings.desktop.section.wsl": "WSL",
+  "settings.desktop.wsl.title": "WSL integration",
+  "settings.desktop.wsl.description": "Run the OpenCode server inside WSL on Windows.",
 
   "settings.general.section.appearance": "外観",
   "settings.general.section.notifications": "システム通知",

+ 3 - 0
packages/app/src/i18n/ko.ts

@@ -513,6 +513,9 @@ export const dict = {
   "settings.section.server": "서버",
   "settings.tab.general": "일반",
   "settings.tab.shortcuts": "단축키",
+  "settings.desktop.section.wsl": "WSL",
+  "settings.desktop.wsl.title": "WSL integration",
+  "settings.desktop.wsl.description": "Run the OpenCode server inside WSL on Windows.",
 
   "settings.general.section.appearance": "모양",
   "settings.general.section.notifications": "시스템 알림",

+ 3 - 0
packages/app/src/i18n/no.ts

@@ -515,6 +515,9 @@ export const dict = {
   "settings.section.server": "Server",
   "settings.tab.general": "Generelt",
   "settings.tab.shortcuts": "Snarveier",
+  "settings.desktop.section.wsl": "WSL",
+  "settings.desktop.wsl.title": "WSL integration",
+  "settings.desktop.wsl.description": "Run the OpenCode server inside WSL on Windows.",
 
   "settings.general.section.appearance": "Utseende",
   "settings.general.section.notifications": "Systemvarsler",

+ 3 - 0
packages/app/src/i18n/pl.ts

@@ -514,6 +514,9 @@ export const dict = {
   "settings.section.server": "Serwer",
   "settings.tab.general": "Ogólne",
   "settings.tab.shortcuts": "Skróty",
+  "settings.desktop.section.wsl": "WSL",
+  "settings.desktop.wsl.title": "WSL integration",
+  "settings.desktop.wsl.description": "Run the OpenCode server inside WSL on Windows.",
 
   "settings.general.section.appearance": "Wygląd",
   "settings.general.section.notifications": "Powiadomienia systemowe",

+ 3 - 0
packages/app/src/i18n/ru.ts

@@ -517,6 +517,9 @@ export const dict = {
   "settings.section.server": "Сервер",
   "settings.tab.general": "Основные",
   "settings.tab.shortcuts": "Горячие клавиши",
+  "settings.desktop.section.wsl": "WSL",
+  "settings.desktop.wsl.title": "WSL integration",
+  "settings.desktop.wsl.description": "Run the OpenCode server inside WSL on Windows.",
 
   "settings.general.section.appearance": "Внешний вид",
   "settings.general.section.notifications": "Системные уведомления",

+ 3 - 0
packages/app/src/i18n/th.ts

@@ -516,6 +516,9 @@ export const dict = {
   "settings.section.server": "เซิร์ฟเวอร์",
   "settings.tab.general": "ทั่วไป",
   "settings.tab.shortcuts": "ทางลัด",
+  "settings.desktop.section.wsl": "WSL",
+  "settings.desktop.wsl.title": "WSL integration",
+  "settings.desktop.wsl.description": "Run the OpenCode server inside WSL on Windows.",
 
   "settings.general.section.appearance": "รูปลักษณ์",
   "settings.general.section.notifications": "การแจ้งเตือนระบบ",

+ 3 - 0
packages/app/src/i18n/zh.ts

@@ -548,6 +548,9 @@ export const dict = {
   "settings.section.server": "服务器",
   "settings.tab.general": "通用",
   "settings.tab.shortcuts": "快捷键",
+  "settings.desktop.section.wsl": "WSL",
+  "settings.desktop.wsl.title": "WSL integration",
+  "settings.desktop.wsl.description": "Run the OpenCode server inside WSL on Windows.",
 
   "settings.general.section.appearance": "外观",
   "settings.general.section.notifications": "系统通知",

+ 3 - 0
packages/app/src/i18n/zht.ts

@@ -545,6 +545,9 @@ export const dict = {
   "settings.section.server": "伺服器",
   "settings.tab.general": "一般",
   "settings.tab.shortcuts": "快速鍵",
+  "settings.desktop.section.wsl": "WSL",
+  "settings.desktop.wsl.title": "WSL integration",
+  "settings.desktop.wsl.description": "Run the OpenCode server inside WSL on Windows.",
 
   "settings.general.section.appearance": "外觀",
   "settings.general.section.notifications": "系統通知",

+ 4 - 0
packages/desktop/AGENTS.md

@@ -0,0 +1,4 @@
+# Desktop package notes
+
+- Never call `invoke` manually in this package.
+- Use the generated bindings in `packages/desktop/src/bindings.ts` for core commands/events.

+ 115 - 26
packages/desktop/src-tauri/src/cli.rs

@@ -3,8 +3,12 @@ use tauri_plugin_shell::{
     ShellExt,
     process::{Command, CommandChild, CommandEvent},
 };
+use tauri_plugin_store::StoreExt;
 
-use crate::{LogState, constants::MAX_LOG_ENTRIES};
+use crate::{
+    LogState,
+    constants::{MAX_LOG_ENTRIES, SETTINGS_STORE, WSL_ENABLED_KEY},
+};
 
 const CLI_INSTALL_DIR: &str = ".opencode/bin";
 const CLI_BINARY_NAME: &str = "opencode";
@@ -21,7 +25,7 @@ pub struct Config {
 }
 
 pub async fn get_config(app: &AppHandle) -> Option<Config> {
-    create_command(app, "debug config")
+    create_command(app, "debug config", &[])
         .output()
         .await
         .inspect_err(|e| eprintln!("Failed to read OC config: {e}"))
@@ -150,25 +154,106 @@ fn get_user_shell() -> String {
     std::env::var("SHELL").unwrap_or_else(|_| "/bin/sh".to_string())
 }
 
-pub fn create_command(app: &tauri::AppHandle, args: &str) -> Command {
+fn is_wsl_enabled(app: &tauri::AppHandle) -> bool {
+    let Ok(store) = app.store(SETTINGS_STORE) else {
+        return false;
+    };
+
+    store
+        .get(WSL_ENABLED_KEY)
+        .as_ref()
+        .and_then(|value| value.as_bool())
+        .unwrap_or(false)
+}
+
+fn shell_escape(input: &str) -> String {
+    if input.is_empty() {
+        return "''".to_string();
+    }
+
+    let mut escaped = String::from("'");
+    escaped.push_str(&input.replace("'", "'\"'\"'"));
+    escaped.push('\'');
+    escaped
+}
+
+pub fn create_command(app: &tauri::AppHandle, args: &str, extra_env: &[(&str, String)]) -> Command {
     let state_dir = app
         .path()
         .resolve("", BaseDirectory::AppLocalData)
         .expect("Failed to resolve app local data dir");
 
-    #[cfg(target_os = "windows")]
-    return app
-        .shell()
-        .sidecar("opencode-cli")
-        .unwrap()
-        .args(args.split_whitespace())
-        .env("OPENCODE_EXPERIMENTAL_ICON_DISCOVERY", "true")
-        .env("OPENCODE_EXPERIMENTAL_FILEWATCHER", "true")
-        .env("OPENCODE_CLIENT", "desktop")
-        .env("XDG_STATE_HOME", &state_dir);
-
-    #[cfg(not(target_os = "windows"))]
-    return {
+    let mut envs = vec![
+        (
+            "OPENCODE_EXPERIMENTAL_ICON_DISCOVERY".to_string(),
+            "true".to_string(),
+        ),
+        (
+            "OPENCODE_EXPERIMENTAL_FILEWATCHER".to_string(),
+            "true".to_string(),
+        ),
+        ("OPENCODE_CLIENT".to_string(), "desktop".to_string()),
+        (
+            "XDG_STATE_HOME".to_string(),
+            state_dir.to_string_lossy().to_string(),
+        ),
+    ];
+    envs.extend(
+        extra_env
+            .iter()
+            .map(|(key, value)| (key.to_string(), value.clone())),
+    );
+
+    if cfg!(windows) {
+        if is_wsl_enabled(app) {
+            println!("WSL is enabled, spawning CLI server in WSL.");
+            let version = app.package_info().version.to_string();
+            let mut script = vec![
+                "set -e".to_string(),
+                "BIN=\"$HOME/.opencode/bin/opencode\"".to_string(),
+                "if [ ! -x \"$BIN\" ]; then".to_string(),
+                format!(
+                    "  curl -fsSL https://opencode.ai/install | bash -s -- --version {} --no-modify-path",
+                    shell_escape(&version)
+                ),
+                "fi".to_string(),
+            ];
+
+            let mut env_prefix = vec![
+                "OPENCODE_EXPERIMENTAL_ICON_DISCOVERY=true".to_string(),
+                "OPENCODE_EXPERIMENTAL_FILEWATCHER=true".to_string(),
+                "OPENCODE_CLIENT=desktop".to_string(),
+                "XDG_STATE_HOME=\"$HOME/.local/state\"".to_string(),
+            ];
+            env_prefix.extend(
+                envs.iter()
+                    .filter(|(key, _)| key != "OPENCODE_EXPERIMENTAL_ICON_DISCOVERY")
+                    .filter(|(key, _)| key != "OPENCODE_EXPERIMENTAL_FILEWATCHER")
+                    .filter(|(key, _)| key != "OPENCODE_CLIENT")
+                    .filter(|(key, _)| key != "XDG_STATE_HOME")
+                    .map(|(key, value)| format!("{}={}", key, shell_escape(value))),
+            );
+
+            script.push(format!("{} exec \"$BIN\" {}", env_prefix.join(" "), args));
+
+            return app
+                .shell()
+                .command("wsl")
+                .args(["-e", "bash", "-lc", &script.join("\n")]);
+        } else {
+            let mut cmd = app
+                .shell()
+                .sidecar("opencode-cli")
+                .unwrap()
+                .args(args.split_whitespace());
+
+            for (key, value) in envs {
+                cmd = cmd.env(key, value);
+            }
+
+            return cmd;
+        }
+    } else {
         let sidecar = get_sidecar_path(app);
         let shell = get_user_shell();
 
@@ -178,14 +263,14 @@ pub fn create_command(app: &tauri::AppHandle, args: &str) -> Command {
             format!("\"{}\" {}", sidecar.display(), args)
         };
 
-        app.shell()
-            .command(&shell)
-            .env("OPENCODE_EXPERIMENTAL_ICON_DISCOVERY", "true")
-            .env("OPENCODE_EXPERIMENTAL_FILEWATCHER", "true")
-            .env("OPENCODE_CLIENT", "desktop")
-            .env("XDG_STATE_HOME", &state_dir)
-            .args(["-il", "-c", &cmd])
-    };
+        let mut cmd = app.shell().command(&shell).args(["-il", "-c", &cmd]);
+
+        for (key, value) in envs {
+            cmd = cmd.env(key, value);
+        }
+
+        cmd
+    }
 }
 
 pub fn serve(app: &AppHandle, hostname: &str, port: u32, password: &str) -> CommandChild {
@@ -194,12 +279,16 @@ pub fn serve(app: &AppHandle, hostname: &str, port: u32, password: &str) -> Comm
 
     println!("spawning sidecar on port {port}");
 
+    let envs = [
+        ("OPENCODE_SERVER_USERNAME", "opencode".to_string()),
+        ("OPENCODE_SERVER_PASSWORD", password.to_string()),
+    ];
+
     let (mut rx, child) = create_command(
         app,
         format!("serve --hostname {hostname} --port {port}").as_str(),
+        &envs,
     )
-    .env("OPENCODE_SERVER_USERNAME", "opencode")
-    .env("OPENCODE_SERVER_PASSWORD", password)
     .spawn()
     .expect("Failed to spawn opencode");
 

+ 1 - 0
packages/desktop/src-tauri/src/constants.rs

@@ -2,6 +2,7 @@ use tauri_plugin_window_state::StateFlags;
 
 pub const SETTINGS_STORE: &str = "opencode.settings.dat";
 pub const DEFAULT_SERVER_URL_KEY: &str = "defaultServerUrl";
+pub const WSL_ENABLED_KEY: &str = "wslEnabled";
 pub const UPDATER_ENABLED: bool = option_env!("TAURI_SIGNING_PRIVATE_KEY").is_some();
 pub const MAX_LOG_ENTRIES: usize = 200;
 

+ 85 - 22
packages/desktop/src-tauri/src/lib.rs

@@ -52,6 +52,13 @@ enum InitStep {
     Done,
 }
 
+#[derive(serde::Deserialize, specta::Type)]
+#[serde(rename_all = "snake_case")]
+enum WslPathMode {
+    Windows,
+    Linux,
+}
+
 struct InitState {
     current: watch::Receiver<InitStep>,
 }
@@ -392,32 +399,50 @@ fn check_linux_app(app_name: &str) -> bool {
     return true;
 }
 
+#[tauri::command]
+#[specta::specta]
+fn wsl_path(path: String, mode: Option<WslPathMode>) -> Result<String, String> {
+    if !cfg(windows) {
+        return Ok(path);
+    }
+
+    let flag = match mode.unwrap_or(WslPathMode::Linux) {
+        WslPathMode::Windows => "-w",
+        WslPathMode::Linux => "-u",
+    };
+
+    let output = if path.starts_with('~') {
+        let suffix = path.strip_prefix('~').unwrap_or("");
+        let escaped = suffix.replace('"', "\\\"");
+        let cmd = format!("wslpath {flag} \"$HOME{escaped}\"");
+        Command::new("wsl")
+            .args(["-e", "sh", "-lc", &cmd])
+            .output()
+            .map_err(|e| format!("Failed to run wslpath: {e}"))?
+    } else {
+        Command::new("wsl")
+            .args(["-e", "wslpath", flag, &path])
+            .output()
+            .map_err(|e| format!("Failed to run wslpath: {e}"))?
+    };
+
+    if !output.status.success() {
+        let stderr = String::from_utf8_lossy(&output.stderr).trim().to_string();
+        if stderr.is_empty() {
+            return Err("wslpath failed".to_string());
+        }
+        return Err(stderr);
+    }
+
+    Ok(String::from_utf8_lossy(&output.stdout).trim().to_string())
+}
+
 #[cfg_attr(mobile, tauri::mobile_entry_point)]
 pub fn run() {
-    let builder = tauri_specta::Builder::<tauri::Wry>::new()
-        // Then register them (separated by a comma)
-        .commands(tauri_specta::collect_commands![
-            kill_sidecar,
-            cli::install_cli,
-            await_initialization,
-            server::get_default_server_url,
-            server::set_default_server_url,
-            get_display_backend,
-            set_display_backend,
-            markdown::parse_markdown_command,
-            check_app_exists,
-            resolve_app_path
-        ])
-        .events(tauri_specta::collect_events![LoadingWindowComplete])
-        .error_handling(tauri_specta::ErrorHandlingMode::Throw);
+    let builder = make_specta_builder();
 
     #[cfg(debug_assertions)] // <- Only export on non-release builds
-    builder
-        .export(
-            specta_typescript::Typescript::default(),
-            "../src/bindings.ts",
-        )
-        .expect("Failed to export typescript bindings");
+    export_types(&builder);
 
     #[cfg(all(target_os = "macos", not(debug_assertions)))]
     let _ = std::process::Command::new("killall")
@@ -476,6 +501,44 @@ pub fn run() {
         });
 }
 
+fn make_specta_builder() -> tauri_specta::Builder<tauri::Wry> {
+    tauri_specta::Builder::<tauri::Wry>::new()
+        // Then register them (separated by a comma)
+        .commands(tauri_specta::collect_commands![
+            kill_sidecar,
+            cli::install_cli,
+            await_initialization,
+            server::get_default_server_url,
+            server::set_default_server_url,
+            server::get_wsl_config,
+            server::set_wsl_config,
+            get_display_backend,
+            set_display_backend,
+            markdown::parse_markdown_command,
+            check_app_exists,
+            wsl_path,
+            resolve_app_path
+        ])
+        .events(tauri_specta::collect_events![LoadingWindowComplete])
+        .error_handling(tauri_specta::ErrorHandlingMode::Throw)
+}
+
+fn export_types(builder: &tauri_specta::Builder<tauri::Wry>) {
+    builder
+        .export(
+            specta_typescript::Typescript::default(),
+            "../src/bindings.ts",
+        )
+        .expect("Failed to export typescript bindings");
+}
+
+#[cfg(test)]
+#[test]
+fn test_export_types() {
+    let builder = make_specta_builder();
+    export_types(&builder);
+}
+
 #[derive(tauri_specta::Event, serde::Deserialize, specta::Type)]
 struct LoadingWindowComplete;
 

+ 44 - 1
packages/desktop/src-tauri/src/server.rs

@@ -8,9 +8,20 @@ use tokio::task::JoinHandle;
 
 use crate::{
     cli,
-    constants::{DEFAULT_SERVER_URL_KEY, SETTINGS_STORE},
+    constants::{DEFAULT_SERVER_URL_KEY, SETTINGS_STORE, WSL_ENABLED_KEY},
 };
 
+#[derive(Clone, serde::Serialize, serde::Deserialize, specta::Type, Debug)]
+pub struct WslConfig {
+    pub enabled: bool,
+}
+
+impl Default for WslConfig {
+    fn default() -> Self {
+        Self { enabled: false }
+    }
+}
+
 #[tauri::command]
 #[specta::specta]
 pub fn get_default_server_url(app: AppHandle) -> Result<Option<String>, String> {
@@ -48,6 +59,38 @@ pub async fn set_default_server_url(app: AppHandle, url: Option<String>) -> Resu
     Ok(())
 }
 
+#[tauri::command]
+#[specta::specta]
+pub fn get_wsl_config(app: AppHandle) -> Result<WslConfig, String> {
+    let store = app
+        .store(SETTINGS_STORE)
+        .map_err(|e| format!("Failed to open settings store: {}", e))?;
+
+    let enabled = store
+        .get(WSL_ENABLED_KEY)
+        .as_ref()
+        .and_then(|v| v.as_bool())
+        .unwrap_or(false);
+
+    Ok(WslConfig { enabled })
+}
+
+#[tauri::command]
+#[specta::specta]
+pub fn set_wsl_config(app: AppHandle, config: WslConfig) -> Result<(), String> {
+    let store = app
+        .store(SETTINGS_STORE)
+        .map_err(|e| format!("Failed to open settings store: {}", e))?;
+
+    store.set(WSL_ENABLED_KEY, serde_json::Value::Bool(config.enabled));
+
+    store
+        .save()
+        .map_err(|e| format!("Failed to save settings: {}", e))?;
+
+    Ok(())
+}
+
 pub async fn get_saved_server_url(app: &tauri::AppHandle) -> Option<String> {
     if let Some(url) = get_default_server_url(app.clone()).ok().flatten() {
         println!("Using desktop-specific custom URL: {url}");

+ 10 - 1
packages/desktop/src-tauri/src/windows.rs

@@ -1,4 +1,7 @@
-use crate::constants::{UPDATER_ENABLED, window_state_flags};
+use crate::{
+    constants::{UPDATER_ENABLED, window_state_flags},
+    server::get_wsl_config,
+};
 use std::{ops::Deref, time::Duration};
 use tauri::{AppHandle, Manager, Runtime, WebviewUrl, WebviewWindow, WebviewWindowBuilder};
 use tauri_plugin_window_state::AppHandleExt;
@@ -22,6 +25,11 @@ impl MainWindow {
             return Ok(Self(window));
         }
 
+        let wsl_enabled = get_wsl_config(app.clone())
+            .ok()
+            .map(|v| v.enabled)
+            .unwrap_or(false);
+
         let window_builder = base_window_config(
             WebviewWindowBuilder::new(app, Self::LABEL, WebviewUrl::App("/".into())),
             app,
@@ -36,6 +44,7 @@ impl MainWindow {
             r#"
             window.__OPENCODE__ ??= {{}};
             window.__OPENCODE__.updaterEnabled = {UPDATER_ENABLED};
+            window.__OPENCODE__.wsl = {wsl_enabled};
           "#
         ));
 

+ 9 - 0
packages/desktop/src/bindings.ts

@@ -10,10 +10,13 @@ export const commands = {
 	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 }),
 };
 
@@ -34,6 +37,12 @@ export type ServerReadyData = {
 		password: string | null,
 	};
 
+export type WslConfig = {
+		enabled: boolean,
+	};
+
+export type WslPathMode = "windows" | "linux";
+
 /* Tauri Specta runtime */
 function makeEvent<T>(name: string) {
     const base = {

+ 337 - 301
packages/desktop/src/index.tsx

@@ -16,7 +16,6 @@ import { open as shellOpen } from "@tauri-apps/plugin-shell"
 import { type as ostype } from "@tauri-apps/plugin-os"
 import { check, Update } from "@tauri-apps/plugin-updater"
 import { getCurrentWindow } from "@tauri-apps/api/window"
-import { invoke } from "@tauri-apps/api/core"
 import { isPermissionGranted, requestPermission } from "@tauri-apps/plugin-notification"
 import { relaunch } from "@tauri-apps/plugin-process"
 import { AsyncStorage } from "@solid-primitives/storage"
@@ -30,7 +29,7 @@ import { UPDATER_ENABLED } from "./updater"
 import { initI18n, t } from "./i18n"
 import pkg from "../package.json"
 import "./styles.css"
-import { commands, InitStep } from "./bindings"
+import { commands, InitStep, type WslConfig } from "./bindings"
 import { Channel } from "@tauri-apps/api/core"
 import { createMenu } from "./menu"
 
@@ -59,338 +58,374 @@ const listenForDeepLinks = async () => {
   await onOpenUrl((urls) => emitDeepLinks(urls)).catch(() => undefined)
 }
 
-const createPlatform = (password: Accessor<string | null>): Platform => ({
-  platform: "desktop",
-  os: (() => {
+const createPlatform = (password: Accessor<string | null>): Platform => {
+  const os = (() => {
     const type = ostype()
     if (type === "macos" || type === "windows" || type === "linux") return type
     return undefined
-  })(),
-  version: pkg.version,
-
-  async openDirectoryPickerDialog(opts) {
-    const result = await open({
-      directory: true,
-      multiple: opts?.multiple ?? false,
-      title: opts?.title ?? t("desktop.dialog.chooseFolder"),
-    })
-    return result
-  },
-
-  async openFilePickerDialog(opts) {
-    const result = await open({
-      directory: false,
-      multiple: opts?.multiple ?? false,
-      title: opts?.title ?? t("desktop.dialog.chooseFile"),
-    })
-    return result
-  },
+  })()
 
-  async saveFilePickerDialog(opts) {
-    const result = await save({
-      title: opts?.title ?? t("desktop.dialog.saveFile"),
-      defaultPath: opts?.defaultPath,
-    })
-    return result
-  },
-
-  openLink(url: string) {
-    void shellOpen(url).catch(() => undefined)
-  },
-
-  async openPath(path: string, app?: string) {
-    const os = ostype()
-    if (os === "windows" && app) {
-      const resolvedApp = await commands.resolveAppPath(app)
-      return openerOpenPath(path, resolvedApp || app)
-    }
-    return openerOpenPath(path, app)
-  },
-
-  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 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
+  }
 
-    const WRITE_DEBOUNCE_MS = 250
+  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"),
+      })
+      return handleWslPicker(result)
+    },
 
-    const storeCache = new Map<string, Promise<StoreLike>>()
-    const apiCache = new Map<string, AsyncStorage & { flush: () => Promise<void> }>()
-    const memoryCache = new Map<string, StoreLike>()
+    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) {
+      const os = ostype()
+      if (os === "windows") {
+        const resolvedApp = (app && (await commands.resolveAppPath(app))) || app
+        const resolvedPath = await (async () => {
+          if (window.__OPENCODE__?.wsl) {
+            const converted = await commands.wslPath(path, "windows").catch(() => null)
+            if (converted) return converted
+          }
 
-    const flushAll = async () => {
-      const apis = Array.from(apiCache.values())
-      await Promise.all(apis.map((api) => api.flush().catch(() => undefined)))
-    }
+          return path
+        })()
+        return openerOpenPath(resolvedPath, resolvedApp)
+      }
+      return openerOpenPath(path, app)
+    },
+
+    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>
+      }
 
-    if ("addEventListener" in globalThis) {
-      const handleVisibility = () => {
-        if (document.visibilityState !== "hidden") return
-        void flushAll()
+      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)))
       }
 
-      window.addEventListener("pagehide", () => void flushAll())
-      document.addEventListener("visibilitychange", handleVisibility)
-    }
+      if ("addEventListener" in globalThis) {
+        const handleVisibility = () => {
+          if (document.visibilityState !== "hidden") return
+          void flushAll()
+        }
 
-    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,
+        window.addEventListener("pagehide", () => void flushAll())
+        document.addEventListener("visibilitychange", handleVisibility)
       }
-      return store
-    }
 
-    const getStore = (name: string) => {
-      const cached = storeCache.get(name)
-      if (cached) return cached
+      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 store = Store.load(name).catch(() => {
-        const cached = memoryCache.get(name)
+      const getStore = (name: string) => {
+        const cached = storeCache.get(name)
         if (cached) return cached
 
-        const memory = createMemoryStore()
-        memoryCache.set(name, memory)
-        return memory
-      })
-
-      storeCache.set(name, store)
-      return store
-    }
+        const store = Store.load(name).catch(() => {
+          const cached = memoryCache.get(name)
+          if (cached) return cached
 
-    const createStorage = (name: string) => {
-      const pending = new Map<string, string | null>()
-      let timer: ReturnType<typeof setTimeout> | undefined
-      let flushing: Promise<void> | undefined
+          const memory = createMemoryStore()
+          memoryCache.set(name, memory)
+          return memory
+        })
 
-      const flush = async () => {
-        if (flushing) return flushing
+        storeCache.set(name, store)
+        return store
+      }
 
-        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)
+      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
-        })
+          })().finally(() => {
+            flushing = undefined
+          })
 
-        return flushing
-      }
+          return flushing
+        }
 
-      const schedule = () => {
-        if (timer) return
-        timer = setTimeout(() => {
-          timer = undefined
-          void flush()
-        }, WRITE_DEBOUNCE_MS)
-      }
+        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()
-        },
-      }
+        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 api
+      }
 
-    return (name = "default.dat") => {
-      const cached = apiCache.get(name)
-      if (cached) return cached
+      return (name = "default.dat") => {
+        const cached = apiCache.get(name)
+        if (cached) return cached
 
-      const api = createStorage(name)
-      apiCache.set(name, api)
-      return api
-    }
-  })(),
-
-  checkUpdate: async () => {
-    if (!UPDATER_ENABLED) return { updateAvailable: false }
-    const next = await check().catch(() => null)
-    if (!next) return { updateAvailable: false }
-    const ok = await next
-      .download()
-      .then(() => true)
-      .catch(() => false)
-    if (!ok) return { updateAvailable: false }
-    update = next
-    return { updateAvailable: true, version: next.version }
-  },
-
-  update: async () => {
-    if (!UPDATER_ENABLED || !update) return
-    if (ostype() === "windows") await commands.killSidecar().catch(() => undefined)
-    await update.install().catch(() => undefined)
-  },
-
-  restart: async () => {
-    await commands.killSidecar().catch(() => undefined)
-    await relaunch()
-  },
-
-  notify: async (title, description, href) => {
-    const granted = await isPermissionGranted().catch(() => false)
-    const permission = granted ? "granted" : await requestPermission().catch(() => "denied")
-    if (permission !== "granted") return
-
-    const win = getCurrentWindow()
-    const focused = await win.isFocused().catch(() => document.hasFocus())
-    if (focused) return
-
-    await Promise.resolve()
-      .then(() => {
-        const notification = new Notification(title, {
-          body: description ?? "",
-          icon: "https://opencode.ai/favicon-96x96-v3.png",
-        })
-        notification.onclick = () => {
-          const win = getCurrentWindow()
-          void win.show().catch(() => undefined)
-          void win.unminimize().catch(() => undefined)
-          void win.setFocus().catch(() => undefined)
-          if (href) {
-            window.history.pushState(null, "", href)
-            window.dispatchEvent(new PopStateEvent("popstate"))
+        const api = createStorage(name)
+        apiCache.set(name, api)
+        return api
+      }
+    })(),
+
+    checkUpdate: async () => {
+      if (!UPDATER_ENABLED) return { updateAvailable: false }
+      const next = await check().catch(() => null)
+      if (!next) return { updateAvailable: false }
+      const ok = await next
+        .download()
+        .then(() => true)
+        .catch(() => false)
+      if (!ok) return { updateAvailable: false }
+      update = next
+      return { updateAvailable: true, version: next.version }
+    },
+
+    update: async () => {
+      if (!UPDATER_ENABLED || !update) return
+      if (ostype() === "windows") await commands.killSidecar().catch(() => undefined)
+      await update.install().catch(() => undefined)
+    },
+
+    restart: async () => {
+      await commands.killSidecar().catch(() => undefined)
+      await relaunch()
+    },
+
+    notify: async (title, description, href) => {
+      const granted = await isPermissionGranted().catch(() => false)
+      const permission = granted ? "granted" : await requestPermission().catch(() => "denied")
+      if (permission !== "granted") return
+
+      const win = getCurrentWindow()
+      const focused = await win.isFocused().catch(() => document.hasFocus())
+      if (focused) return
+
+      await Promise.resolve()
+        .then(() => {
+          const notification = new Notification(title, {
+            body: description ?? "",
+            icon: "https://opencode.ai/favicon-96x96-v3.png",
+          })
+          notification.onclick = () => {
+            const win = getCurrentWindow()
+            void win.show().catch(() => undefined)
+            void win.unminimize().catch(() => undefined)
+            void win.setFocus().catch(() => undefined)
+            if (href) {
+              window.history.pushState(null, "", href)
+              window.dispatchEvent(new PopStateEvent("popstate"))
+            }
+            notification.close()
           }
-          notification.close()
-        }
-      })
-      .catch(() => undefined)
-  },
+        })
+        .catch(() => undefined)
+    },
 
-  fetch: (input, init) => {
-    const pw = password()
+    fetch: (input, init) => {
+      const pw = password()
 
-    const addHeader = (headers: Headers, password: string) => {
-      headers.append("Authorization", `Basic ${btoa(`opencode:${password}`)}`)
-    }
+      const addHeader = (headers: Headers, password: string) => {
+        headers.append("Authorization", `Basic ${btoa(`opencode:${password}`)}`)
+      }
 
-    if (input instanceof Request) {
-      if (pw) addHeader(input.headers, pw)
-      return tauriFetch(input)
-    } else {
-      const headers = new Headers(init?.headers)
-      if (pw) addHeader(headers, pw)
-      return tauriFetch(input, {
-        ...(init as any),
-        headers: headers,
+      if (input instanceof Request) {
+        if (pw) addHeader(input.headers, pw)
+        return tauriFetch(input)
+      } else {
+        const headers = new Headers(init?.headers)
+        if (pw) addHeader(headers, pw)
+        return tauriFetch(input, {
+          ...(init as any),
+          headers: headers,
+        })
+      }
+    },
+
+    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 })
+    },
+
+    getDefaultServerUrl: async () => {
+      const result = await commands.getDefaultServerUrl().catch(() => null)
+      return result
+    },
+
+    setDefaultServerUrl: 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")
       })
-    }
-  },
-
-  getDefaultServerUrl: async () => {
-    const result = await commands.getDefaultServerUrl().catch(() => null)
-    return result
-  },
-
-  setDefaultServerUrl: async (url: string | null) => {
-    await commands.setDefaultServerUrl(url)
-  },
-
-  getDisplayBackend: async () => {
-    const result = await invoke<DisplayBackend | null>("get_display_backend").catch(() => null)
-    return result
-  },
-
-  setDisplayBackend: async (backend) => {
-    await invoke("set_display_backend", { backend }).catch(() => undefined)
-  },
-
-  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)
 createMenu((id) => {
@@ -400,6 +435,7 @@ void listenForDeepLinks()
 
 render(() => {
   const [serverPassword, setServerPassword] = createSignal<string | null>(null)
+
   const platform = createPlatform(() => serverPassword())
 
   function handleClick(e: MouseEvent) {