From ebdbc92edad2e0371fe80d89a401daaa9f6fdb7b Mon Sep 17 00:00:00 2001 From: liujunGH <46551088+liujunGH@users.noreply.github.com> Date: Sun, 19 Jul 2026 18:17:40 +0800 Subject: [PATCH 1/2] fix(cli): hide Windows console windows from `kimi web` and file opens MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `openUrl` (used by `kimi web`, `kimi vis`, the login flow) and kap-server's `launchDetached` / win32 `where` probe spawned processes without `windowsHide`, so Windows allocated a visible console window — flashing a cmd.exe window when opening the browser and pinning one for detached children. Same class as #957 / #1336 / #1466; same one-option fix, kept unconditional (ignored off Windows, matching kaos). --- .changeset/fix-windows-console-windows.md | 5 +++ apps/kimi-code/src/utils/open-url.ts | 7 ++- apps/kimi-code/test/utils/open-url.test.ts | 50 +++++++++++++++++++++ packages/kap-server/src/lib/fileLaunch.ts | 6 +++ packages/kap-server/test/fileLaunch.test.ts | 43 ++++++++++++++++++ 5 files changed, 110 insertions(+), 1 deletion(-) create mode 100644 .changeset/fix-windows-console-windows.md create mode 100644 apps/kimi-code/test/utils/open-url.test.ts create mode 100644 packages/kap-server/test/fileLaunch.test.ts diff --git a/.changeset/fix-windows-console-windows.md b/.changeset/fix-windows-console-windows.md new file mode 100644 index 0000000000..0a9fbe3138 --- /dev/null +++ b/.changeset/fix-windows-console-windows.md @@ -0,0 +1,5 @@ +--- +"@moonshot-ai/kimi-code": patch +--- + +Windows: stop flashing a console window when `kimi web` opens the browser and when the web UI launches files/editors. `openUrl`, `launchDetached`, and the win32 `where` probe now pass `windowsHide: true`, matching the earlier fixes for spawned commands (#957), the background updater (#1336), and hooks (#1466). diff --git a/apps/kimi-code/src/utils/open-url.ts b/apps/kimi-code/src/utils/open-url.ts index 4112d9c6f0..33ae63efbf 100644 --- a/apps/kimi-code/src/utils/open-url.ts +++ b/apps/kimi-code/src/utils/open-url.ts @@ -1,5 +1,10 @@ import { execFile } from 'node:child_process'; +// `windowsHide` is ignored off Windows (kept unconditional, matching kaos). +// Without it, a console-less parent (e.g. a GUI-launched `kimi web`) makes +// Windows allocate a visible console for the transient `cmd /c start`. +const SPAWN_OPTIONS = { windowsHide: true } as const; + export function openUrl(url: string): void { const command: [string, string[]] = process.platform === 'darwin' @@ -7,5 +12,5 @@ export function openUrl(url: string): void { : process.platform === 'win32' ? ['cmd', ['/c', 'start', '', url]] : ['xdg-open', [url]]; - execFile(command[0], command[1], () => {}); + execFile(command[0], command[1], SPAWN_OPTIONS, () => {}); } diff --git a/apps/kimi-code/test/utils/open-url.test.ts b/apps/kimi-code/test/utils/open-url.test.ts new file mode 100644 index 0000000000..d72d5667d3 --- /dev/null +++ b/apps/kimi-code/test/utils/open-url.test.ts @@ -0,0 +1,50 @@ +import { beforeEach, describe, expect, it, vi } from 'vitest'; + +const mocks = vi.hoisted(() => ({ + execFile: vi.fn(), +})); + +vi.mock('node:child_process', () => ({ + execFile: mocks.execFile, +})); + +import { openUrl } from '#/utils/open-url'; + +describe('openUrl', () => { + beforeEach(() => { + vi.clearAllMocks(); + }); + + it('passes windowsHide so opening the browser never flashes a console window on Windows', () => { + openUrl('http://127.0.0.1:58627/#token=t'); + expect(mocks.execFile).toHaveBeenCalledWith( + expect.any(String), + expect.any(Array), + expect.objectContaining({ windowsHide: true }), + expect.any(Function), + ); + }); + + it('uses cmd /c start on win32, open on darwin, xdg-open elsewhere', () => { + const originalPlatform = process.platform; + try { + for (const [platform, expectedCommand] of [ + ['win32', 'cmd'], + ['darwin', 'open'], + ['linux', 'xdg-open'], + ] as const) { + Object.defineProperty(process, 'platform', { value: platform }); + mocks.execFile.mockClear(); + openUrl('http://example.com'); + expect(mocks.execFile).toHaveBeenCalledWith( + expectedCommand, + expect.any(Array), + expect.objectContaining({ windowsHide: true }), + expect.any(Function), + ); + } + } finally { + Object.defineProperty(process, 'platform', { value: originalPlatform }); + } + }); +}); diff --git a/packages/kap-server/src/lib/fileLaunch.ts b/packages/kap-server/src/lib/fileLaunch.ts index a2071cdbf5..567656b61f 100644 --- a/packages/kap-server/src/lib/fileLaunch.ts +++ b/packages/kap-server/src/lib/fileLaunch.ts @@ -122,6 +122,8 @@ function commandExists(command: string, platform: NodeJS.Platform): boolean { if (platform === 'win32') { const result = spawnSync('cmd', ['/c', 'where', command], { stdio: 'ignore', + // Hide the transient cmd.exe console window on Windows. + windowsHide: true, }); return result.status === 0; } @@ -191,6 +193,10 @@ export async function launchDetached(cmd: LaunchCommand): Promise { detached: true, stdio: 'ignore', shell: cmd.shell, + // On Windows a detached child gets its own console window; hide it so + // "open file" from the web UI does not flash a console (see also the + // background updater's identical guard in cli/update/preflight.ts). + windowsHide: true, }); child.once('error', (err) => { if (settled) return; diff --git a/packages/kap-server/test/fileLaunch.test.ts b/packages/kap-server/test/fileLaunch.test.ts new file mode 100644 index 0000000000..05768b090c --- /dev/null +++ b/packages/kap-server/test/fileLaunch.test.ts @@ -0,0 +1,43 @@ +import { EventEmitter } from 'node:events'; + +import { describe, expect, it, vi } from 'vitest'; + +const mocks = vi.hoisted(() => ({ + spawn: vi.fn(), + spawnSync: vi.fn(), +})); + +vi.mock('node:child_process', () => ({ + spawn: mocks.spawn, + spawnSync: mocks.spawnSync, +})); + +import { getAvailableOpenInApps, launchDetached } from '../src/lib/fileLaunch'; + +describe('launchDetached', () => { + it('hides the console window a detached child would get on Windows', async () => { + const child = Object.assign(new EventEmitter(), { unref: vi.fn() }); + mocks.spawn.mockReturnValue(child); + const promise = launchDetached({ command: 'code', args: ['file.ts'], shell: true }); + child.emit('spawn'); + await promise; + expect(child.unref).toHaveBeenCalled(); + expect(mocks.spawn).toHaveBeenCalledWith( + 'code', + ['file.ts'], + expect.objectContaining({ detached: true, windowsHide: true }), + ); + }); +}); + +describe('commandExists probe on win32', () => { + it('hides the transient cmd.exe console window', () => { + mocks.spawnSync.mockReturnValue({ status: 0 }); + getAvailableOpenInApps('win32'); + expect(mocks.spawnSync).toHaveBeenCalledWith( + 'cmd', + ['/c', 'where', expect.any(String)], + expect.objectContaining({ stdio: 'ignore', windowsHide: true }), + ); + }); +}); From 222085222adc5050d7d180d030baf84f436192c6 Mon Sep 17 00:00:00 2001 From: liujunGH <46551088+liujunGH@users.noreply.github.com> Date: Sun, 19 Jul 2026 20:14:51 +0800 Subject: [PATCH 2/2] fix(kap-server): only hide console windows for console children MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Codex review: `windowsHide` maps to SW_HIDE, which would also hide the FIRST window of GUI children — e.g. explorer.exe launched by the web UI's reveal action. Apply it only when the direct child is console-subsystem (a shell shim or cmd.exe itself); GUI targets stay visible. --- packages/kap-server/src/lib/fileLaunch.ts | 18 +++++++++--- packages/kap-server/test/fileLaunch.test.ts | 32 +++++++++++++++++---- 2 files changed, 41 insertions(+), 9 deletions(-) diff --git a/packages/kap-server/src/lib/fileLaunch.ts b/packages/kap-server/src/lib/fileLaunch.ts index 567656b61f..3377a80449 100644 --- a/packages/kap-server/src/lib/fileLaunch.ts +++ b/packages/kap-server/src/lib/fileLaunch.ts @@ -186,6 +186,15 @@ function openInMacApp( return openFileCommandFor(absolutePath, undefined, process.env, platform); } +/** True when the spawned child is console-subsystem on Windows (a shell or + * cmd.exe itself), so its console window should be hidden. GUI binaries + * (explorer.exe, editors) keep their windows visible. */ +function launchesConsole(cmd: LaunchCommand): boolean { + if (cmd.shell === true) return true; + const base = cmd.command.split(/[\\/]/).pop()?.toLowerCase(); + return base === 'cmd' || base === 'cmd.exe'; +} + export async function launchDetached(cmd: LaunchCommand): Promise { return new Promise((resolve, reject) => { let settled = false; @@ -193,10 +202,11 @@ export async function launchDetached(cmd: LaunchCommand): Promise { detached: true, stdio: 'ignore', shell: cmd.shell, - // On Windows a detached child gets its own console window; hide it so - // "open file" from the web UI does not flash a console (see also the - // background updater's identical guard in cli/update/preflight.ts). - windowsHide: true, + // Hide the console window Windows allocates for console-subsystem + // children (shell shims / cmd.exe). GUI targets (explorer.exe, + // editors) must stay visible — SW_HIDE would hide their first + // window too. + windowsHide: launchesConsole(cmd), }); child.once('error', (err) => { if (settled) return; diff --git a/packages/kap-server/test/fileLaunch.test.ts b/packages/kap-server/test/fileLaunch.test.ts index 05768b090c..19e41e3793 100644 --- a/packages/kap-server/test/fileLaunch.test.ts +++ b/packages/kap-server/test/fileLaunch.test.ts @@ -15,19 +15,41 @@ vi.mock('node:child_process', () => ({ import { getAvailableOpenInApps, launchDetached } from '../src/lib/fileLaunch'; describe('launchDetached', () => { - it('hides the console window a detached child would get on Windows', async () => { + async function launchAndCapture(cmd: Parameters[0]) { const child = Object.assign(new EventEmitter(), { unref: vi.fn() }); mocks.spawn.mockReturnValue(child); - const promise = launchDetached({ command: 'code', args: ['file.ts'], shell: true }); + const promise = launchDetached(cmd); child.emit('spawn'); await promise; - expect(child.unref).toHaveBeenCalled(); + return mocks.spawn.mock.calls.at(-1); + } + + it('hides the console window for shell-shim launches', async () => { + await launchAndCapture({ command: 'code file.ts', args: [], shell: true }); expect(mocks.spawn).toHaveBeenCalledWith( - 'code', - ['file.ts'], + 'code file.ts', + [], expect.objectContaining({ detached: true, windowsHide: true }), ); }); + + it('hides the console window for direct cmd.exe launches', async () => { + await launchAndCapture({ command: 'cmd', args: ['/c', 'start', '""', 'C:\\f'] }); + expect(mocks.spawn).toHaveBeenCalledWith( + 'cmd', + ['/c', 'start', '""', 'C:\\f'], + expect.objectContaining({ windowsHide: true }), + ); + }); + + it('keeps GUI windows visible (explorer.exe reveal)', async () => { + await launchAndCapture({ command: 'explorer.exe', args: ['/select,C:\\f'] }); + expect(mocks.spawn).toHaveBeenCalledWith( + 'explorer.exe', + ['/select,C:\\f'], + expect.objectContaining({ windowsHide: false }), + ); + }); }); describe('commandExists probe on win32', () => {