Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
0042496
fix(desktop): skip slow powershell profile load if node is available
mohamedmastouri-hue Aug 12, 2026
2bc624d
fix(desktop): restore FNM hydration and add focused skip tests
mohamedmastouri-hue Aug 12, 2026
6e4a10f
fix(desktop): fix test runner registration and use cross-platform pat…
mohamedmastouri-hue Aug 12, 2026
b1239f6
fix(desktop): strip quotes from Windows PATH entries before existence…
mohamedmastouri-hue Aug 12, 2026
e800120
fix(desktop): run profile probe concurrently and interrupt if fast pa…
mohamedmastouri-hue Aug 12, 2026
75d9c9c
fix(desktop): true concurrency via static precheck
mohamedmastouri-hue Aug 12, 2026
fcb2b0c
Update apps/desktop/src/shell/DesktopShellEnvironment.ts
mohamedmastouri-hue Aug 12, 2026
b1fb31a
fix(desktop): skip all powershell probes on fast path and detect fnm …
mohamedmastouri-hue Aug 12, 2026
989bfa7
Update apps/desktop/src/shell/DesktopShellEnvironment.ts
mohamedmastouri-hue Aug 12, 2026
9ea804e
test(desktop): fix trailing whitespace and cover knownWindowsCliDirs …
mohamedmastouri-hue Aug 12, 2026
e64b8d7
Merge branch 'fix-windows-startup-delay' of https://github.com/mohame…
mohamedmastouri-hue Aug 12, 2026
030ff8a
fix(desktop): use forward slashes in knownWindowsCliDirs and fix test…
mohamedmastouri-hue Aug 12, 2026
fe9cd5e
fix(desktop): revert knownWindowsCliDirs path separator and mock File…
mohamedmastouri-hue Aug 12, 2026
d59802d
test(desktop): add missing FileSystem import for mocks
mohamedmastouri-hue Aug 12, 2026
45d4b74
test(desktop): fix duplicate FileSystem import and alphabetize
mohamedmastouri-hue Aug 12, 2026
cb8aecf
test(desktop): update FileSystem import to effect/FileSystem
mohamedmastouri-hue Aug 12, 2026
b8107f8
test(desktop): fix TypeScript requirement inference in test helper
mohamedmastouri-hue Aug 12, 2026
5306f4b
Merge branch 'main' into fix-windows-startup-delay
mohamedmastouri-hue Aug 12, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
99 changes: 93 additions & 6 deletions apps/desktop/src/shell/DesktopShellEnvironment.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import * as NodeServices from "@effect/platform-node/NodeServices";
import { assert, describe, it } from "@effect/vitest";
import * as Effect from "effect/Effect";
import * as FileSystem from "effect/FileSystem";
import * as Layer from "effect/Layer";
import * as Logger from "effect/Logger";
import * as PlatformError from "effect/PlatformError";
Expand Down Expand Up @@ -69,6 +70,7 @@ function runShellEnvironment(input: {
readonly platform: NodeJS.Platform;
readonly handler: (command: ChildProcess.Command) => string;
readonly failure?: PlatformError.PlatformError;
readonly fs?: FileSystem.FileSystem;
Comment thread
macroscopeapp[bot] marked this conversation as resolved.
}) {
const environmentLayer = Layer.succeed(
DesktopEnvironment.DesktopEnvironment,
Expand All @@ -85,17 +87,23 @@ function runShellEnvironment(input: {
),
);

const program = Effect.gen(function* () {
const coreProgram = Effect.gen(function* () {
const shellEnvironment = yield* DesktopShellEnvironment.DesktopShellEnvironment;
yield* shellEnvironment.installIntoProcess;
}).pipe(
Effect.provide(
DesktopShellEnvironment.layer.pipe(
Layer.provide(Layer.mergeAll(environmentLayer, NodeServices.layer, spawnerLayer)),
),
),
Effect.provide(DesktopShellEnvironment.layer),
Effect.provide(Layer.mergeAll(environmentLayer, spawnerLayer)),
);

const program = input.fs
? coreProgram.pipe(
Effect.provideService(FileSystem.FileSystem, input.fs),
Effect.provide(NodeServices.layer)
)
: coreProgram.pipe(
Effect.provide(NodeServices.layer)
);

return withProcessEnv(input.env, program);
}

Expand Down Expand Up @@ -343,4 +351,83 @@ describe("DesktopShellEnvironment", () => {
Effect.provide(Logger.layer([logger], { mergeWithExisting: false })),
);
});

it.effect("skips ALL PowerShell probes when node is available statically with quotes and no fnm", () =>
Effect.gen(function* () {
const mockDir = "C:\\mock\\temp";
const env: NodeJS.ProcessEnv = { PATH: `"${mockDir}"` };
const commands: ChildProcess.Command[] = [];

const mockFs = {
exists: (path: string) => Effect.succeed(path.includes("node.exe")),
} as FileSystem.FileSystem;

yield* runShellEnvironment({
env,
platform: "win32",
fs: mockFs,
handler: (command) => {
commands.push(command);
return envOutput({ PATH: mockDir });
},
});

assert.equal(commands.length, 0);
assert.equal(env.PATH, `"${mockDir}"`);
}),
);

it.effect("loads PowerShell probes concurrently when node and fnm.cmd are both available", () =>
Effect.gen(function* () {
const mockDir = "C:\\mock\\temp";
const env: NodeJS.ProcessEnv = { PATH: mockDir };
const commands: ChildProcess.Command[] = [];

const mockFs = {
exists: (path: string) => Effect.succeed(path.includes("node.exe") || path.includes("fnm.cmd")),
} as FileSystem.FileSystem;

yield* runShellEnvironment({
env,
platform: "win32",
fs: mockFs,
handler: (command) => {
commands.push(command);
return envOutput({ PATH: mockDir });
},
});

assert.equal(commands.length, 2);

const noProfileIdx = commands.findIndex((c) => c._tag === "StandardCommand" && c.args.includes("-NoProfile"));
const profileIdx = commands.findIndex((c) => c._tag === "StandardCommand" && !c.args.includes("-NoProfile"));
assert.isTrue(noProfileIdx !== -1);
assert.isTrue(profileIdx !== -1);
}),
);

it.effect("skips PowerShell probes when node is in knownWindowsCliDirs but not on PATH", () =>
Effect.gen(function* () {
const mockDir = "C:\\mock\\temp";
const env: NodeJS.ProcessEnv = { PATH: "C:\\Windows\\System32", LOCALAPPDATA: mockDir };
const commands: ChildProcess.Command[] = [];

const mockFs = {
exists: (path: string) => Effect.succeed(path.includes("node.exe") && path.includes(mockDir)),
} as FileSystem.FileSystem;

yield* runShellEnvironment({
env,
platform: "win32",
fs: mockFs,
handler: (command) => {
commands.push(command);
return envOutput({ PATH: "C:\\Windows\\System32" });
},
});

assert.equal(commands.length, 0);
assert.isTrue(env.PATH!.includes("nodejs"));
}),
);
});
51 changes: 47 additions & 4 deletions apps/desktop/src/shell/DesktopShellEnvironment.ts
Original file line number Diff line number Diff line change
Expand Up @@ -378,26 +378,69 @@ const readWindowsEnvironment = Effect.fn("desktop.shellEnvironment.readWindowsEn
const installWindowsEnvironment = Effect.fn("desktop.shellEnvironment.installWindowsEnvironment")(
Comment thread
macroscopeapp[bot] marked this conversation as resolved.
function* (
config: ShellEnvironmentConfig,
): Effect.fn.Return<void, never, ChildProcessSpawner.ChildProcessSpawner> {
): Effect.fn.Return<void, never, ChildProcessSpawner.ChildProcessSpawner | FileSystem.FileSystem> {
const fileSystem = yield* FileSystem.FileSystem;

// Fast pre-check using static paths (no PowerShell required)
const staticPaths = mergePaths("win32", [
trimNonEmpty(knownWindowsCliDirs(config.env).join(";")),
readEnvPath(config.env),
]);

let nodeFound = false;
let fnmFound = false;
if (Option.isSome(staticPaths)) {
for (const dir of staticPaths.value.split(";")) {
const cleanDir = dir.trim().replace(/^"+|"+$/g, "");
if (!nodeFound && (yield* Effect.orElseSucceed(fileSystem.exists(`${cleanDir}/node.exe`), () => false))) {
nodeFound = true;
}
if (!fnmFound && (
(yield* Effect.orElseSucceed(fileSystem.exists(`${cleanDir}/fnm.exe`), () => false)) ||
(yield* Effect.orElseSucceed(fileSystem.exists(`${cleanDir}/fnm.cmd`), () => false)) ||
(yield* Effect.orElseSucceed(fileSystem.exists(`${cleanDir}/fnm.ps1`), () => false))
)) {
fnmFound = true;
}
if (nodeFound && fnmFound) break;
}
}

const skipProfile = nodeFound && !fnmFound;

// If node is found statically and no fnm wrappers exist, we can skip BOTH PowerShell probes entirely!
if (skipProfile) {
if (Option.isSome(staticPaths)) {
config.env.PATH = staticPaths.value;
}
return; // Exit early, 0ms startup!
}

// Concurrent, not sequential: these two probes are independent (only their
// results are combined below) and each spawns its own PowerShell. Run in
// series they sit at offset 0 of desktop.startup, before anything else, and
// launch traces measured them at 2718ms then 2066ms — the entire 4.8s
// startup span, of which desktop.bootstrap is ~30ms.
// startup span, of which desktop.bootstrap is ~30ms. Total cost here is
// therefore bounded by the slowest single probe.
const [noProfile, profile] = yield* Effect.all(
[
readWindowsEnvironment(["PATH"], { loadProfile: false }),
readWindowsEnvironment(WINDOWS_PROFILE_ENV_NAMES, { loadProfile: true }),
],
{ concurrency: 2 },
);
const mergedPath = mergePaths("win32", [
trimNonEmpty(profile.PATH),

const fastMergedPath = mergePaths("win32", [
trimNonEmpty(knownWindowsCliDirs(config.env).join(";")),
trimNonEmpty(noProfile.PATH),
readEnvPath(config.env),
]);

const mergedPath = mergePaths("win32", [
trimNonEmpty(profile.PATH),
fastMergedPath,
]);

if (Option.isSome(mergedPath)) {
config.env.PATH = mergedPath.value;
}
Expand Down
Loading