diff --git a/src/server/__tests__/open-target-launch-options.test.ts b/src/server/__tests__/open-target-launch-options.test.ts new file mode 100644 index 00000000..10c86c9b --- /dev/null +++ b/src/server/__tests__/open-target-launch-options.test.ts @@ -0,0 +1,27 @@ +import { describe, expect, it } from 'bun:test' +import { getDefaultLaunchSpawnOptions } from '../services/openTargetService.js' + +describe('getDefaultLaunchSpawnOptions', () => { + it('detaches the child process so VS Code/Cursor outlive cc-haha', () => { + expect(getDefaultLaunchSpawnOptions().detached).toBe(true) + }) + + it("ignores stdio so it can't keep the parent alive on graceful exit", () => { + expect(getDefaultLaunchSpawnOptions().stdio).toBe('ignore') + }) + + // Regression guard for the "VS Code starts but no window appears" bug: + // setting windowsHide: true forwards SW_HIDE as the initial nCmdShow to + // GUI-subsystem .exe targets, which Windows then applies to the app's + // first ShowWindow() call — keeping the main window hidden even though + // the process is fully running. See the docblock on the helper. + it('does NOT set windowsHide (would hide the GUI window on Windows)', () => { + const options = getDefaultLaunchSpawnOptions() as Record + expect(options.windowsHide).toBeUndefined() + }) + + it('does not pass any extra fields that could change spawn semantics', () => { + const options = getDefaultLaunchSpawnOptions() + expect(Object.keys(options).sort()).toEqual(['detached', 'stdio']) + }) +}) diff --git a/src/server/services/openTargetService.ts b/src/server/services/openTargetService.ts index 45f7dd3b..f0fbe17d 100644 --- a/src/server/services/openTargetService.ts +++ b/src/server/services/openTargetService.ts @@ -265,6 +265,29 @@ async function defaultPathExists(targetPath: string): Promise { } } +/** + * Returns the {@link SpawnOptions} we want for {@link defaultLaunch}. + * + * Critically, on Windows we do **not** set `windowsHide: true`. For console + * subsystem commands like `cmd.exe /c start ...` it would hide the brief cmd + * flash, but for GUI subsystem executables (VS Code's `Code.exe`, + * `Cursor.exe`, `explorer.exe`, ...) Windows treats `STARTUPINFO.wShowWindow + * = SW_HIDE` as the initial `nCmdShow` handed to the app's first + * `ShowWindow()` call. The process starts, the main window is created, and + * then it stays hidden — matching the bug report "VS Code is in Task Manager + * but no window appears". The brief cmd flash from the file-manager fallback + * (`cmd.exe /c start "" path`) is acceptable; cmd /c start exits in + * milliseconds. + * + * Exported for unit testing — callers should keep using {@link defaultLaunch}. + */ +export function getDefaultLaunchSpawnOptions(): { + detached: true + stdio: 'ignore' +} { + return { detached: true, stdio: 'ignore' } +} + async function defaultLaunch(command: string, args: string[]): Promise { return await new Promise((resolveLaunch) => { let settled = false @@ -275,11 +298,7 @@ async function defaultLaunch(command: string, args: string[]): Promise { settle({