From eb33f78541d574e1523562786e81a424d3660365 Mon Sep 17 00:00:00 2001 From: OOKAMI Date: Wed, 16 Sep 2026 14:36:55 +0800 Subject: [PATCH 1/4] Default WSL launches to Linux $HOME When launching WSL on Windows, ensure the distro starts in the Linux home if no explicit working directory was provided. Adds isWslExecutable and getWslLaunchArgs which append `--cd ~` for wsl.exe calls that lack an explicit cwd or --cd option. startLocalSession now uses getWslLaunchArgs when building shell args. Exports getWslLaunchArgs and adds a unit test verifying the behavior. This prevents WSL from failing early when the parent Windows cwd isn't mounted or accessible to the chosen distro. --- electron/bridges/terminalBridge.cjs | 29 ++++++++++++++++++- .../terminalBridge.outputFlood.test.cjs | 19 ++++++++++++ 2 files changed, 47 insertions(+), 1 deletion(-) diff --git a/electron/bridges/terminalBridge.cjs b/electron/bridges/terminalBridge.cjs index 90a2b2d572..45b3e97fe9 100644 --- a/electron/bridges/terminalBridge.cjs +++ b/electron/bridges/terminalBridge.cjs @@ -851,6 +851,27 @@ function getLocalShellArgs(shellPath) { return []; } +function isWslExecutable(shellPath) { + if (process.platform !== "win32" || typeof shellPath !== "string") return false; + return /(?:^|[\\/])wsl(?:\.exe)?$/i.test(shellPath.trim()); +} + +function getWslLaunchArgs(shellPath, shellArgs, hasExplicitCwd) { + const args = Array.isArray(shellArgs) ? [...shellArgs] : []; + // Without --cd, wsl.exe translates the parent Windows cwd. That can fail + // before the Linux shell starts (for example, when the Windows home is not + // mounted or accessible to the selected distro). Start at Linux $HOME unless + // the caller deliberately supplied a working directory or --cd option. + if ( + isWslExecutable(shellPath) && + !hasExplicitCwd && + !args.some((arg) => arg === "--cd" || arg.startsWith("--cd=")) + ) { + args.push("--cd", "~"); + } + return args; +} + const isUtf8Locale = (value) => typeof value === "string" && /utf-?8/i.test(value); const isEmptyLocale = (value) => { @@ -896,7 +917,12 @@ function startLocalSession(event, payload) { } } const shell = normalizeExecutablePath(resolvedShell) || defaultShell; - const shellArgs = resolvedArgs ?? getLocalShellArgs(shell); + const requestedCwd = typeof payload?.cwd === "string" && payload.cwd.trim().length > 0; + const shellArgs = getWslLaunchArgs( + shell, + resolvedArgs ?? getLocalShellArgs(shell), + requestedCwd, + ); const shellKind = detectShellKind(shell); const { buildTerminalProcessEnv } = require("./httpNetworkProxyBridge.cjs"); const env = applyLocaleDefaults({ @@ -2536,6 +2562,7 @@ module.exports = { registerHandlers, findExecutable, getDefaultLocalShell, + getWslLaunchArgs, startLocalSession, startTelnetSession, startMoshSession, diff --git a/electron/bridges/terminalBridge.outputFlood.test.cjs b/electron/bridges/terminalBridge.outputFlood.test.cjs index e7fd086a55..60c08e7c15 100644 --- a/electron/bridges/terminalBridge.outputFlood.test.cjs +++ b/electron/bridges/terminalBridge.outputFlood.test.cjs @@ -99,6 +99,25 @@ function loadBridgeWithFakes(spawns, sentries) { } } +test("WSL launch arguments use Linux home unless a working directory is explicit", () => { + const bridge = loadBridgeWithFakes([], []); + const platformDescriptor = Object.getOwnPropertyDescriptor(process, "platform"); + + try { + Object.defineProperty(process, "platform", { ...platformDescriptor, value: "win32" }); + assert.deepEqual( + bridge.getWslLaunchArgs("C:\\Windows\\System32\\wsl.exe", ["-d", "Ubuntu"], false), + ["-d", "Ubuntu", "--cd", "~"], + ); + assert.deepEqual( + bridge.getWslLaunchArgs("C:\\Windows\\System32\\wsl.exe", ["-d", "Ubuntu"], true), + ["-d", "Ubuntu"], + ); + } finally { + Object.defineProperty(process, "platform", platformDescriptor); + } +}); + test("Windows local terminals enable the bundled ConPTY implementation required for clear", () => { const spawns = []; const sentries = []; From 4206704cd87a1d1883aef0800a6ae69a2325543c Mon Sep 17 00:00:00 2001 From: OOKAMI Date: Wed, 16 Sep 2026 16:30:56 +0800 Subject: [PATCH 2/4] Insert --cd before WSL command/--exec Ensure WSL launch uses Linux home by inserting --cd "~" before the first bare command or --exec/-e boundary so the option is consumed by WSL rather than passed to the invoked shell/command. Early-return when not a WSL executable, when an explicit cwd is supplied, or when --cd is already present. Also account for WSL options that take a separate value (--distribution/-d, --user/-u). Adds a unit test verifying --cd is placed before --exec. --- electron/bridges/terminalBridge.cjs | 26 ++++++++++++++++--- .../terminalBridge.outputFlood.test.cjs | 8 ++++++ 2 files changed, 30 insertions(+), 4 deletions(-) diff --git a/electron/bridges/terminalBridge.cjs b/electron/bridges/terminalBridge.cjs index 45b3e97fe9..05dd280eff 100644 --- a/electron/bridges/terminalBridge.cjs +++ b/electron/bridges/terminalBridge.cjs @@ -863,12 +863,30 @@ function getWslLaunchArgs(shellPath, shellArgs, hasExplicitCwd) { // mounted or accessible to the selected distro). Start at Linux $HOME unless // the caller deliberately supplied a working directory or --cd option. if ( - isWslExecutable(shellPath) && - !hasExplicitCwd && - !args.some((arg) => arg === "--cd" || arg.startsWith("--cd=")) + !isWslExecutable(shellPath) || + hasExplicitCwd || + args.some((arg) => arg === "--cd" || arg.startsWith("--cd=")) ) { - args.push("--cd", "~"); + return args; + } + + // The tokens following --exec/-e, or the first bare command, are passed to + // Linux verbatim. Account for WSL options with a separate value before + // locating that boundary, then insert --cd before it rather than accidentally + // passing --cd to the shell or command. + let commandIndex = -1; + for (let index = 0; index < args.length; index += 1) { + const arg = args[index]; + if (arg === "--exec" || arg === "-e" || !arg.startsWith("-")) { + commandIndex = index; + break; + } + if (arg === "--distribution" || arg === "-d" || arg === "--user" || arg === "-u") { + index += 1; + } } + const insertAt = commandIndex === -1 ? args.length : commandIndex; + args.splice(insertAt, 0, "--cd", "~"); return args; } diff --git a/electron/bridges/terminalBridge.outputFlood.test.cjs b/electron/bridges/terminalBridge.outputFlood.test.cjs index 60c08e7c15..11ca362bb0 100644 --- a/electron/bridges/terminalBridge.outputFlood.test.cjs +++ b/electron/bridges/terminalBridge.outputFlood.test.cjs @@ -113,6 +113,14 @@ test("WSL launch arguments use Linux home unless a working directory is explicit bridge.getWslLaunchArgs("C:\\Windows\\System32\\wsl.exe", ["-d", "Ubuntu"], true), ["-d", "Ubuntu"], ); + assert.deepEqual( + bridge.getWslLaunchArgs( + "C:\\Windows\\System32\\wsl.exe", + ["-d", "Ubuntu", "--exec", "zsh", "-l"], + false, + ), + ["-d", "Ubuntu", "--cd", "~", "--exec", "zsh", "-l"], + ); } finally { Object.defineProperty(process, "platform", platformDescriptor); } From 6d380d2db661daedb74f9287560977cc568eed27 Mon Sep 17 00:00:00 2001 From: bincxz <16399091+binaricat@users.noreply.github.com> Date: Fri, 18 Sep 2026 11:46:30 +0800 Subject: [PATCH 3/4] fix(terminal): preserve WSL command argument boundaries --- electron/bridges/terminalBridge.cjs | 19 ++++--- .../terminalBridge.outputFlood.test.cjs | 56 +++++++++++++++++++ 2 files changed, 67 insertions(+), 8 deletions(-) diff --git a/electron/bridges/terminalBridge.cjs b/electron/bridges/terminalBridge.cjs index 05dd280eff..05457e3dd2 100644 --- a/electron/bridges/terminalBridge.cjs +++ b/electron/bridges/terminalBridge.cjs @@ -862,26 +862,29 @@ function getWslLaunchArgs(shellPath, shellArgs, hasExplicitCwd) { // before the Linux shell starts (for example, when the Windows home is not // mounted or accessible to the selected distro). Start at Linux $HOME unless // the caller deliberately supplied a working directory or --cd option. - if ( - !isWslExecutable(shellPath) || - hasExplicitCwd || - args.some((arg) => arg === "--cd" || arg.startsWith("--cd=")) - ) { + // WSL also accepts a leading ~ as its legacy home-directory shorthand. + if (!isWslExecutable(shellPath) || hasExplicitCwd || args[0] === "~") { return args; } - // The tokens following --exec/-e, or the first bare command, are passed to + // The tokens following --/--exec/-e, or the first bare command, are passed to // Linux verbatim. Account for WSL options with a separate value before // locating that boundary, then insert --cd before it rather than accidentally // passing --cd to the shell or command. let commandIndex = -1; for (let index = 0; index < args.length; index += 1) { const arg = args[index]; - if (arg === "--exec" || arg === "-e" || !arg.startsWith("-")) { + if (arg === "--" || arg === "--exec" || arg === "-e" || !arg.startsWith("-")) { commandIndex = index; break; } - if (arg === "--distribution" || arg === "-d" || arg === "--user" || arg === "-u") { + // Only WSL's directory option is explicit; a Linux command may itself + // accept --cd without changing the directory WSL starts in. + if (arg === "--cd" || arg.startsWith("--cd=")) return args; + if ( + arg === "--distribution" || arg === "-d" || arg === "--distribution-id" || + arg === "--user" || arg === "-u" || arg === "--shell-type" + ) { index += 1; } } diff --git a/electron/bridges/terminalBridge.outputFlood.test.cjs b/electron/bridges/terminalBridge.outputFlood.test.cjs index 11ca362bb0..06171eb4ff 100644 --- a/electron/bridges/terminalBridge.outputFlood.test.cjs +++ b/electron/bridges/terminalBridge.outputFlood.test.cjs @@ -126,6 +126,62 @@ test("WSL launch arguments use Linux home unless a working directory is explicit } }); +test("WSL default directory stays outside the Linux command and option values", () => { + const bridge = loadBridgeWithFakes([], []); + const platformDescriptor = Object.getOwnPropertyDescriptor(process, "platform"); + const cases = [ + [["~"], ["~"]], + [["~", "-d", "Ubuntu", "--", "zsh"], ["~", "-d", "Ubuntu", "--", "zsh"]], + [["-d", "Ubuntu", "--", "zsh", "-l"], ["-d", "Ubuntu", "--cd", "~", "--", "zsh", "-l"]], + [["-e", "zsh", "-l"], ["--cd", "~", "-e", "zsh", "-l"]], + [["--shell-type", "login", "zsh"], ["--shell-type", "login", "--cd", "~", "zsh"]], + [["--distribution-id", "{distro-id}"], ["--distribution-id", "{distro-id}", "--cd", "~"]], + [["--user", "root", "echo", "--cd"], ["--user", "root", "--cd", "~", "echo", "--cd"]], + [["--exec", "echo", "--cd=/tmp"], ["--cd", "~", "--exec", "echo", "--cd=/tmp"]], + [["--cd", "/tmp", "--", "zsh"], ["--cd", "/tmp", "--", "zsh"]], + [["--cd=/tmp", "-e", "zsh"], ["--cd=/tmp", "-e", "zsh"]], + ]; + try { + Object.defineProperty(process, "platform", { ...platformDescriptor, value: "win32" }); + for (const [args, expected] of cases) { + const input = Object.freeze([...args]); + assert.deepEqual(bridge.getWslLaunchArgs("wsl.exe", input, false), expected, JSON.stringify(args)); + assert.deepEqual(bridge.getWslLaunchArgs("wsl.exe", input, true), args); + assert.deepEqual(bridge.getWslLaunchArgs("powershell.exe", input, false), args); + } + Object.defineProperty(process, "platform", { ...platformDescriptor, value: "linux" }); + assert.deepEqual(bridge.getWslLaunchArgs("/usr/bin/wsl", [], false), []); + } finally { + Object.defineProperty(process, "platform", platformDescriptor); + } +}); + +test("WSL local sessions pass the home directory option to the spawned process", () => { + const spawns = []; + const bridge = loadBridgeWithFakes(spawns, []); + const platformDescriptor = Object.getOwnPropertyDescriptor(process, "platform"); + try { + Object.defineProperty(process, "platform", { ...platformDescriptor, value: "win32" }); + bridge.init({ + sessions: new Map(), + electronModule: { webContents: { fromId: () => ({ send() {} }) } }, + }); + const shellArgs = ["-d", "Ubuntu", "--", "zsh", "-l"]; + const payload = { shell: "C:\\Windows\\System32\\wsl.exe", shellArgs }; + bridge.startLocalSession({ sender: { id: 7 } }, { ...payload, sessionId: "wsl-home" }); + assert.deepEqual(spawns[0].spawnArgs[1], ["-d", "Ubuntu", "--cd", "~", "--", "zsh", "-l"]); + assert.deepEqual(shellArgs, ["-d", "Ubuntu", "--", "zsh", "-l"]); + bridge.startLocalSession( + { sender: { id: 7 } }, + { ...payload, sessionId: "wsl-explicit-cwd", cwd: process.cwd() }, + ); + assert.deepEqual(spawns[1].spawnArgs[1], shellArgs); + assert.equal(spawns[1].spawnArgs[2].cwd, process.cwd()); + } finally { + Object.defineProperty(process, "platform", platformDescriptor); + } +}); + test("Windows local terminals enable the bundled ConPTY implementation required for clear", () => { const spawns = []; const sentries = []; From e4b3c47c69fdfd1cd9b6ff669dc750e445834aa2 Mon Sep 17 00:00:00 2001 From: bincxz <16399091+binaricat@users.noreply.github.com> Date: Fri, 18 Sep 2026 11:49:56 +0800 Subject: [PATCH 4/4] fix(terminal): retain legacy WSL distribution selectors --- electron/bridges/terminalBridge.cjs | 10 +++++++--- electron/bridges/terminalBridge.outputFlood.test.cjs | 4 ++++ 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/electron/bridges/terminalBridge.cjs b/electron/bridges/terminalBridge.cjs index 05457e3dd2..f031e7d6f9 100644 --- a/electron/bridges/terminalBridge.cjs +++ b/electron/bridges/terminalBridge.cjs @@ -862,17 +862,21 @@ function getWslLaunchArgs(shellPath, shellArgs, hasExplicitCwd) { // before the Linux shell starts (for example, when the Windows home is not // mounted or accessible to the selected distro). Start at Linux $HOME unless // the caller deliberately supplied a working directory or --cd option. - // WSL also accepts a leading ~ as its legacy home-directory shorthand. - if (!isWslExecutable(shellPath) || hasExplicitCwd || args[0] === "~") { + if (!isWslExecutable(shellPath) || hasExplicitCwd) { return args; } + // WSL consumes an optional legacy distro GUID, then a home-directory ~, + // before parsing normal options. Keep both in their original positions. + const firstOptionIndex = /^\{?[\da-f]{8}-[\da-f]{4}-[\da-f]{4}-[\da-f]{4}-[\da-f]{12}\}?$/i.test(args[0] || "") ? 1 : 0; + if (args[firstOptionIndex] === "~") return args; + // The tokens following --/--exec/-e, or the first bare command, are passed to // Linux verbatim. Account for WSL options with a separate value before // locating that boundary, then insert --cd before it rather than accidentally // passing --cd to the shell or command. let commandIndex = -1; - for (let index = 0; index < args.length; index += 1) { + for (let index = firstOptionIndex; index < args.length; index += 1) { const arg = args[index]; if (arg === "--" || arg === "--exec" || arg === "-e" || !arg.startsWith("-")) { commandIndex = index; diff --git a/electron/bridges/terminalBridge.outputFlood.test.cjs b/electron/bridges/terminalBridge.outputFlood.test.cjs index 06171eb4ff..9531690800 100644 --- a/electron/bridges/terminalBridge.outputFlood.test.cjs +++ b/electron/bridges/terminalBridge.outputFlood.test.cjs @@ -129,7 +129,11 @@ test("WSL launch arguments use Linux home unless a working directory is explicit test("WSL default directory stays outside the Linux command and option values", () => { const bridge = loadBridgeWithFakes([], []); const platformDescriptor = Object.getOwnPropertyDescriptor(process, "platform"); + const distroGuid = "{01234567-89ab-cdef-0123-456789abcdef}"; const cases = [ + [[distroGuid], [distroGuid, "--cd", "~"]], + [[distroGuid, "~"], [distroGuid, "~"]], + [[distroGuid, "--", "zsh"], [distroGuid, "--cd", "~", "--", "zsh"]], [["~"], ["~"]], [["~", "-d", "Ubuntu", "--", "zsh"], ["~", "-d", "Ubuntu", "--", "zsh"]], [["-d", "Ubuntu", "--", "zsh", "-l"], ["-d", "Ubuntu", "--cd", "~", "--", "zsh", "-l"]],