fix(desktop): open project folders in Windows Explorer on WSL (#1581)

Route Continue in CLI directory opens through wslpath + explorer.exe on
WSL Linux instead of shell.openPath, which often launches Chrome at a
file:// listing via xdg-open.
This commit is contained in:
wuyangfan 2026-05-27 01:10:17 +08:00
parent a6a56099ca
commit f9d852749f
3 changed files with 118 additions and 1 deletions

View file

@ -0,0 +1,54 @@
import { execFile, type ExecFileOptionsWithStringEncoding } from "node:child_process";
import os from "node:os";
import { promisify } from "node:util";
import { shell } from "electron";
const execFileAsync = promisify(execFile) as (
file: string,
args: readonly string[],
options: ExecFileOptionsWithStringEncoding,
) => Promise<{ stdout: string; stderr: string }>;
export function isWslLinux(
release = os.release(),
platform = process.platform,
): boolean {
return platform === "linux" && release.toLowerCase().includes("microsoft");
}
export type OpenProjectDirectoryDeps = {
execFile?: typeof execFileAsync;
isWsl?: () => boolean;
openPath?: (path: string) => Promise<string>;
};
/**
* Open a validated project directory in the host file manager. On WSL Linux,
* `shell.openPath` often delegates to Chrome via xdg-open; route through the
* Windows host Explorer instead (issue #1581).
*/
export async function openProjectDirectoryInFileManager(
resolvedDir: string,
deps: OpenProjectDirectoryDeps = {},
): Promise<string> {
const exec = deps.execFile ?? execFileAsync;
const openPath = deps.openPath ?? ((path: string) => shell.openPath(path));
const isWsl = deps.isWsl ?? (() => isWslLinux());
if (!isWsl()) {
return openPath(resolvedDir);
}
try {
const { stdout } = await exec("wslpath", ["-w", resolvedDir], { timeout: 5000 });
const winPath = stdout.trim();
if (!winPath) {
return "wslpath returned an empty Windows path";
}
await exec("explorer.exe", [winPath], { timeout: 5000 });
return "";
} catch (err) {
return err instanceof Error ? err.message : String(err);
}
}

View file

@ -16,6 +16,7 @@ import {
import type { OpenDesignHostActionResult, OpenDesignHostUpdaterActionOptions } from "@open-design/host"; import type { OpenDesignHostActionResult, OpenDesignHostUpdaterActionOptions } from "@open-design/host";
import { createElectronPdfTarget, exportPdfFromHtml, savePrintReadyDocumentAsPdf } from "./pdf-export.js"; import { createElectronPdfTarget, exportPdfFromHtml, savePrintReadyDocumentAsPdf } from "./pdf-export.js";
import { openProjectDirectoryInFileManager } from "./open-project-directory.js";
import type { PrintReadyPdfOptions } from "./pdf-export.js"; import type { PrintReadyPdfOptions } from "./pdf-export.js";
import type { DesktopUpdater } from "./updater.js"; import type { DesktopUpdater } from "./updater.js";
@ -1198,7 +1199,7 @@ export async function createDesktopRuntime(options: DesktopRuntimeOptions): Prom
const validated = await validateExistingDirectory(resolved.context.resolvedDir); const validated = await validateExistingDirectory(resolved.context.resolvedDir);
if (!validated.ok) return `open-path: ${validated.reason}`; if (!validated.ok) return `open-path: ${validated.reason}`;
try { try {
return await shell.openPath(validated.resolved); return await openProjectDirectoryInFileManager(validated.resolved);
} catch (err) { } catch (err) {
return err instanceof Error ? err.message : String(err); return err instanceof Error ? err.message : String(err);
} }

View file

@ -0,0 +1,62 @@
import { describe, expect, it, vi } from "vitest";
import {
isWslLinux,
openProjectDirectoryInFileManager,
} from "../../src/main/open-project-directory.js";
describe("isWslLinux", () => {
it("detects WSL kernels via microsoft in the release string", () => {
expect(isWslLinux("5.15.133.1-microsoft-standard-WSL2", "linux")).toBe(true);
expect(isWslLinux("6.8.0-45-generic", "linux")).toBe(false);
expect(isWslLinux("5.15.133.1-microsoft-standard-WSL2", "darwin")).toBe(false);
});
});
describe("openProjectDirectoryInFileManager", () => {
it("uses shell.openPath on native Linux", async () => {
const openPath = vi.fn(async () => "");
const execFile = vi.fn();
const result = await openProjectDirectoryInFileManager("/tmp/project", {
isWsl: () => false,
openPath,
execFile,
});
expect(result).toBe("");
expect(openPath).toHaveBeenCalledWith("/tmp/project");
expect(execFile).not.toHaveBeenCalled();
});
it("routes through wslpath and explorer.exe on WSL (#1581)", async () => {
const execFile = vi.fn(async (cmd: string, args: readonly string[]) => {
if (cmd === "wslpath") {
return { stdout: "C:\\Users\\me\\project\r\n", stderr: "" };
}
return { stdout: "", stderr: "" };
});
const openPath = vi.fn();
const result = await openProjectDirectoryInFileManager("/home/me/project", {
isWsl: () => true,
execFile,
openPath,
});
expect(result).toBe("");
expect(execFile).toHaveBeenNthCalledWith(
1,
"wslpath",
["-w", "/home/me/project"],
{ timeout: 5000 },
);
expect(execFile).toHaveBeenNthCalledWith(
2,
"explorer.exe",
["C:\\Users\\me\\project"],
{ timeout: 5000 },
);
expect(openPath).not.toHaveBeenCalled();
});
});