From e2845b4913fda9fb2ec2aa1a5d5c9a85e60ad027 Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Thu, 3 Sep 2026 19:07:24 -0700 Subject: [PATCH 1/3] fix(server): reveal normalized paths in File Explorer --- apps/server/src/process/externalLauncher.test.ts | 14 ++++++++++---- apps/server/src/process/externalLauncher.ts | 10 +++++++++- 2 files changed, 19 insertions(+), 5 deletions(-) diff --git a/apps/server/src/process/externalLauncher.test.ts b/apps/server/src/process/externalLauncher.test.ts index a72b42b60b75..030453e2f06f 100644 --- a/apps/server/src/process/externalLauncher.test.ts +++ b/apps/server/src/process/externalLauncher.test.ts @@ -206,7 +206,9 @@ it.effect("reveals a file in File Explorer through PowerShell on Windows", () => const launcher = yield* ExternalLauncher.ExternalLauncher; yield* launcher.launchEditor({ editor: "file-manager", - cwd: "C:\\workspace with spaces\\media\\author's clip.mp4", + // Web file links normalize separators even when the server runs on + // Windows. Explorer's `/select` switch requires Windows separators. + cwd: "C:/workspace with spaces/media/author's clip.mp4", reveal: true, }); return yield* launcher.resolveFileManagerRevealKind(); @@ -261,8 +263,12 @@ it.skipIf(process.platform !== "win32")( const outputPath = NodePath.join(tempDir, "argv.txt"); NodeFS.writeFileSync(recorderPath, `@echo off\r\n>"${outputPath}" echo(%*\r\n`); - const target = "C:\\workspace with spaces\\media\\author's clip.mp4"; - const source = ExternalLauncher.buildFileExplorerRevealPowerShellSource(recorderPath, target); + const target = "C:/workspace with spaces/media/author's clip.mp4"; + const explorerTarget = target.replaceAll("/", "\\"); + const source = ExternalLauncher.buildFileExplorerRevealPowerShellSource( + recorderPath, + explorerTarget, + ); const powerShellPath = `${process.env.SYSTEMROOT ?? "C:\\Windows"}\\System32\\WindowsPowerShell\\v1.0\\powershell.exe`; NodeChildProcess.execFileSync( powerShellPath, @@ -290,7 +296,7 @@ it.skipIf(process.platform !== "win32")( } await sleep(200); const recorded = NodeFS.readFileSync(outputPath, "utf8").trim(); - assert.equal(recorded, `/select,"${target}"`); + assert.equal(recorded, `/select,"${explorerTarget}"`); } finally { NodeFS.rmSync(tempDir, { recursive: true, force: true }); } diff --git a/apps/server/src/process/externalLauncher.ts b/apps/server/src/process/externalLauncher.ts index 96e6470311f4..fbde96ff58b1 100644 --- a/apps/server/src/process/externalLauncher.ts +++ b/apps/server/src/process/externalLauncher.ts @@ -609,6 +609,10 @@ function fileExplorerRevealLaunch( }; } +function normalizeWindowsFileManagerPath(target: string): string { + return target.replaceAll("/", "\\"); +} + const resolveFileManagerRevealLaunch = Effect.fn("resolveFileManagerRevealLaunch")(function* ( target: string, platform: NodeJS.Platform, @@ -626,7 +630,11 @@ const resolveFileManagerRevealLaunch = Effect.fn("resolveFileManagerRevealLaunch } if (platform === "win32") { - return fileExplorerRevealLaunch(target, target, resolvePowerShellPath(env)); + return fileExplorerRevealLaunch( + target, + normalizeWindowsFileManagerPath(target), + resolvePowerShellPath(env), + ); } if ( From 3e4dc3b08e867c7e7deff20777ec8d5f7396e2a3 Mon Sep 17 00:00:00 2001 From: Utkarsh Patil <73941998+UtkarshUsername@users.noreply.github.com> Date: Mon, 31 Aug 2026 03:14:10 +0530 Subject: [PATCH 2/3] fix(windows): strip quotes from repaired PATH --- .../src/shell/DesktopShellEnvironment.test.ts | 3 ++- apps/desktop/src/shell/DesktopShellEnvironment.ts | 11 +++++++---- packages/shared/src/shell.test.ts | 14 ++++++++++++-- packages/shared/src/shell.ts | 12 ++++++++---- 4 files changed, 29 insertions(+), 11 deletions(-) diff --git a/apps/desktop/src/shell/DesktopShellEnvironment.test.ts b/apps/desktop/src/shell/DesktopShellEnvironment.test.ts index 28955debf7b1..5a76402b1d34 100644 --- a/apps/desktop/src/shell/DesktopShellEnvironment.test.ts +++ b/apps/desktop/src/shell/DesktopShellEnvironment.test.ts @@ -320,7 +320,7 @@ describe("DesktopShellEnvironment", () => { FNM_DIR: "C:\\Users\\testuser\\AppData\\Roaming\\fnm", FNM_MULTISHELL_PATH: "C:\\Users\\testuser\\AppData\\Local\\fnm_multishells\\123", }) - : envOutput({ PATH: "C:\\Custom\\Bin;C:\\Windows\\System32" }); + : envOutput({ PATH: 'C:\\Custom\\Bin;C:";C:\\Windows\\System32' }); }, }); @@ -337,6 +337,7 @@ describe("DesktopShellEnvironment", () => { "C:\\Users\\testuser\\.bun\\bin", "C:\\Users\\testuser\\scoop\\shims", "C:\\Custom\\Bin", + "C:", ].join(";"), ); assert.equal(env.FNM_DIR, "C:\\Users\\testuser\\AppData\\Roaming\\fnm"); diff --git a/apps/desktop/src/shell/DesktopShellEnvironment.ts b/apps/desktop/src/shell/DesktopShellEnvironment.ts index e065bf55d046..b4610eee5c84 100644 --- a/apps/desktop/src/shell/DesktopShellEnvironment.ts +++ b/apps/desktop/src/shell/DesktopShellEnvironment.ts @@ -151,6 +151,9 @@ const pathComparisonKey = (entry: string, platform: NodeJS.Platform) => { return platform === "win32" ? normalized.toLowerCase() : normalized; }; +const sanitizePathEntry = (entry: string, platform: NodeJS.Platform) => + platform === "win32" ? entry.replaceAll('"', "") : entry; + const mergePaths = ( platform: NodeJS.Platform, values: ReadonlyArray>, @@ -163,14 +166,14 @@ const mergePaths = ( if (Option.isNone(value)) continue; for (const entry of value.value.split(delimiter)) { - const trimmed = entry.trim(); - if (trimmed.length === 0) continue; + const sanitized = sanitizePathEntry(entry.trim(), platform); + if (sanitized.length === 0) continue; - const key = pathComparisonKey(trimmed, platform); + const key = pathComparisonKey(sanitized, platform); if (key.length === 0 || seen.has(key)) continue; seen.add(key); - entries.push(trimmed); + entries.push(sanitized); } } diff --git a/packages/shared/src/shell.test.ts b/packages/shared/src/shell.test.ts index 89ec055f599e..e032af50ca53 100644 --- a/packages/shared/src/shell.test.ts +++ b/packages/shared/src/shell.test.ts @@ -304,7 +304,7 @@ describe("readEnvironmentFromWindowsShell", () => { }); describe("mergePathValues", () => { - it("dedupes case-insensitively on Windows while preserving preferred order", () => { + it("sanitizes and dedupes Windows entries while preserving preferred order", () => { expect( mergePathValues( 'C:\\Users\\testuser\\AppData\\Roaming\\npm;"C:\\Program Files\\nodejs"', @@ -312,10 +312,20 @@ describe("mergePathValues", () => { "win32", ), ).toBe( - 'C:\\Users\\testuser\\AppData\\Roaming\\npm;"C:\\Program Files\\nodejs";C:\\Windows\\System32', + "C:\\Users\\testuser\\AppData\\Roaming\\npm;C:\\Program Files\\nodejs;C:\\Windows\\System32", ); }); + it("removes stray quotes from Windows entries", () => { + expect( + mergePathValues( + 'C:\\Windows\\System32;C:\\cloudflared.exe;C:";C:\\Program Files\\nodejs', + undefined, + "win32", + ), + ).toBe("C:\\Windows\\System32;C:\\cloudflared.exe;C:;C:\\Program Files\\nodejs"); + }); + it("dedupes case-sensitively on POSIX", () => { expect(mergePathValues("/usr/local/bin:/usr/bin", "/usr/bin:/USR/BIN", "linux")).toBe( "/usr/local/bin:/usr/bin:/USR/BIN", diff --git a/packages/shared/src/shell.ts b/packages/shared/src/shell.ts index da6db2166765..1fdd35cfd0ab 100644 --- a/packages/shared/src/shell.ts +++ b/packages/shared/src/shell.ts @@ -414,6 +414,10 @@ function normalizePathEntryForComparison(entry: string, platform: NodeJS.Platfor return platform === "win32" ? normalized.toLowerCase() : normalized; } +function sanitizePathEntry(entry: string, platform: NodeJS.Platform): string { + return platform === "win32" ? entry.replaceAll('"', "") : entry; +} + export function mergePathValues( preferredPath: string | undefined, inheritedPath: string | undefined, @@ -427,14 +431,14 @@ export function mergePathValues( if (!rawValue) continue; for (const entry of rawValue.split(delimiter)) { - const trimmed = entry.trim(); - if (trimmed.length === 0) continue; + const sanitized = sanitizePathEntry(entry.trim(), platform); + if (sanitized.length === 0) continue; - const normalized = normalizePathEntryForComparison(trimmed, platform); + const normalized = normalizePathEntryForComparison(sanitized, platform); if (normalized.length === 0 || seen.has(normalized)) continue; seen.add(normalized); - merged.push(trimmed); + merged.push(sanitized); } } From 61b92dd03074f958d0c420e1ff4377e91034c45e Mon Sep 17 00:00:00 2001 From: Utkarsh Patil <73941998+UtkarshUsername@users.noreply.github.com> Date: Tue, 1 Sep 2026 05:18:57 +0530 Subject: [PATCH 3/3] fix(shared): preserve Windows shell PATH priority --- packages/shared/src/shell.test.ts | 10 +++++----- packages/shared/src/shell.ts | 4 +++- 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/packages/shared/src/shell.test.ts b/packages/shared/src/shell.test.ts index e032af50ca53..e3046c03abed 100644 --- a/packages/shared/src/shell.test.ts +++ b/packages/shared/src/shell.test.ts @@ -460,7 +460,7 @@ effectIt.layer(NodeServices.layer)("resolveSpawnCommand", (it) => { }); effectIt.layer(NodeServices.layer)("resolveWindowsEnvironment", (it) => { - it.effect("returns the baseline no-profile PATH patch when node is already available", () => + it.effect("uses known CLI directories as a fallback without changing shell PATH priority", () => Effect.gen(function* () { const readEnvironment = vi.fn( (_names: ReadonlyArray, options?: { loadProfile?: boolean }) => @@ -483,6 +483,8 @@ effectIt.layer(NodeServices.layer)("resolveWindowsEnvironment", (it) => { ), ).toEqual({ PATH: [ + "C:\\Shell\\Bin", + "C:\\Windows\\System32", "C:\\Users\\testuser\\AppData\\Roaming\\npm", "C:\\Users\\testuser\\AppData\\Local\\Programs\\nodejs", "C:\\Users\\testuser\\AppData\\Local\\Volta\\bin", @@ -490,8 +492,6 @@ effectIt.layer(NodeServices.layer)("resolveWindowsEnvironment", (it) => { "C:\\Users\\testuser\\.local\\bin", "C:\\Users\\testuser\\.bun\\bin", "C:\\Users\\testuser\\scoop\\shims", - "C:\\Shell\\Bin", - "C:\\Windows\\System32", ].join(";"), }); expect(readEnvironment).toHaveBeenCalledTimes(1); @@ -532,6 +532,7 @@ effectIt.layer(NodeServices.layer)("resolveWindowsEnvironment", (it) => { PATH: [ "C:\\Profile\\Node", "C:\\Windows\\System32", + "C:\\Shell\\Bin", "C:\\Users\\testuser\\AppData\\Roaming\\npm", "C:\\Users\\testuser\\AppData\\Local\\Programs\\nodejs", "C:\\Users\\testuser\\AppData\\Local\\Volta\\bin", @@ -539,7 +540,6 @@ effectIt.layer(NodeServices.layer)("resolveWindowsEnvironment", (it) => { "C:\\Users\\testuser\\.local\\bin", "C:\\Users\\testuser\\.bun\\bin", "C:\\Users\\testuser\\scoop\\shims", - "C:\\Shell\\Bin", ].join(";"), FNM_DIR: "C:\\Users\\testuser\\AppData\\Roaming\\fnm", FNM_MULTISHELL_PATH: "C:\\Users\\testuser\\AppData\\Local\\fnm_multishells\\123", @@ -576,11 +576,11 @@ effectIt.layer(NodeServices.layer)("resolveWindowsEnvironment", (it) => { ), ).toEqual({ PATH: [ + "C:\\Windows\\System32", "C:\\Users\\testuser\\AppData\\Roaming\\npm", "C:\\Users\\testuser\\.local\\bin", "C:\\Users\\testuser\\.bun\\bin", "C:\\Users\\testuser\\scoop\\shims", - "C:\\Windows\\System32", ].join(";"), FNM_DIR: "C:\\Users\\testuser\\AppData\\Roaming\\fnm", }); diff --git a/packages/shared/src/shell.ts b/packages/shared/src/shell.ts index 1fdd35cfd0ab..4c86c8886312 100644 --- a/packages/shared/src/shell.ts +++ b/packages/shared/src/shell.ts @@ -728,7 +728,9 @@ export const resolveWindowsEnvironment = Effect.fn("shell.resolveWindowsEnvironm }).PATH; const mergedPath = mergePathValues(shellPath, inheritedPath, "win32"); const knownCliPath = resolveKnownWindowsCliDirs(env).join(WINDOWS_PATH_DELIMITER); - const baselinePath = mergePathValues(knownCliPath, mergedPath, "win32"); + // Preserve the order a user's shell uses. These directories fill gaps when + // desktop apps launch without the full interactive-shell PATH. + const baselinePath = mergePathValues(mergedPath, knownCliPath, "win32"); const baselinePatch: Partial = baselinePath ? { PATH: baselinePath } : {}; const baselineEnv = mergeWindowsEnv(env, baselinePatch);