From 20bb86ab638203f4e3e696c2766a4346963caa23 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=A8=8B=E5=BA=8F=E5=91=98=E9=98=BF=E6=B1=9F=28Relakkes?= =?UTF-8?q?=29?= Date: Sat, 11 Jul 2026 04:37:30 +0800 Subject: [PATCH] fix(server): keep Explorer windows visible #983 Do not pass windowsHide to detached GUI open targets, which forwards SW_HIDE to Explorer and IDE windows on Windows. Preserve detached process lifetime and ignored stdio. Tests: open-target 33/33; server 1624/1624; coverage 5/5 with changed lines 6/6; real macOS Finder reveal passed for a temporary CJK and ampersand path. Remaining validation: packaged Windows 11 Explorer and IDE window smoke. Confidence: high Scope-risk: narrow --- .../open-target-launch-options.test.ts | 50 +++++++++++++++++++ src/server/services/openTargetService.ts | 17 +++++-- 2 files changed, 62 insertions(+), 5 deletions(-) create mode 100644 src/server/__tests__/open-target-launch-options.test.ts 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..335cee90 --- /dev/null +++ b/src/server/__tests__/open-target-launch-options.test.ts @@ -0,0 +1,50 @@ +import { describe, expect, it } from 'bun:test' +import { mkdir, mkdtemp, rm, writeFile } from 'node:fs/promises' +import { tmpdir } from 'node:os' +import { delimiter, join } from 'node:path' +import { + createOpenTargetService, + getDefaultLaunchSpawnOptions, +} from '../services/openTargetService.js' + +describe('open target launch options', () => { + it('does not hide external application windows on Windows', () => { + const options = getDefaultLaunchSpawnOptions() as Record + + expect(options.windowsHide).toBeUndefined() + expect(options).toEqual({ + detached: true, + stdio: 'ignore', + }) + }) + + it.skipIf(process.platform === 'win32')('uses those options in the production launcher', async () => { + const root = await mkdtemp(join(tmpdir(), 'cc-haha-open-target-launch-')) + const binDir = join(root, 'bin') + const targetDir = join(root, 'target') + const commandPath = join(binDir, 'code') + const originalPath = process.env.PATH + + await mkdir(binDir) + await mkdir(targetDir) + await writeFile(commandPath, '#!/bin/sh\nexit 0\n', { mode: 0o755 }) + process.env.PATH = `${binDir}${delimiter}${originalPath ?? ''}` + + try { + const service = createOpenTargetService({ + platform: 'linux', + commandExists: async (command) => command === 'code', + }) + + await expect(service.openTarget({ targetId: 'vscode', path: targetDir })) + .resolves.toMatchObject({ ok: true, targetId: 'vscode' }) + } finally { + if (originalPath === undefined) { + delete process.env.PATH + } else { + process.env.PATH = originalPath + } + await rm(root, { recursive: true, force: true }) + } + }) +}) diff --git a/src/server/services/openTargetService.ts b/src/server/services/openTargetService.ts index 20a17c60..8a29505c 100644 --- a/src/server/services/openTargetService.ts +++ b/src/server/services/openTargetService.ts @@ -265,6 +265,17 @@ async function defaultPathExists(targetPath: string): Promise { } } +/** + * Keep external applications detached without setting `windowsHide`: on + * Windows that flag passes SW_HIDE to GUI targets such as Explorer and IDEs. + */ +export function getDefaultLaunchSpawnOptions() { + return { + detached: true as const, + stdio: 'ignore' as const, + } +} + async function defaultLaunch(command: string, args: string[]): Promise { return await new Promise((resolveLaunch) => { let settled = false @@ -275,11 +286,7 @@ async function defaultLaunch(command: string, args: string[]): Promise { settle({