Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
5 changes: 5 additions & 0 deletions .changeset/fix-windows-console-windows.md
Original file line number Diff line number Diff line change
@@ -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).
7 changes: 6 additions & 1 deletion apps/kimi-code/src/utils/open-url.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,16 @@
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'
? ['open', [url]]
: process.platform === 'win32'
? ['cmd', ['/c', 'start', '', url]]
: ['xdg-open', [url]];
execFile(command[0], command[1], () => {});
execFile(command[0], command[1], SPAWN_OPTIONS, () => {});
}
50 changes: 50 additions & 0 deletions apps/kimi-code/test/utils/open-url.test.ts
Original file line number Diff line number Diff line change
@@ -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 });
}
});
});
16 changes: 16 additions & 0 deletions packages/kap-server/src/lib/fileLaunch.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Expand Down Expand Up @@ -184,13 +186,27 @@ 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<void> {
return new Promise<void>((resolve, reject) => {
let settled = false;
const child = spawn(cmd.command, cmd.args, {
detached: true,
stdio: 'ignore',
shell: cmd.shell,
// 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;
Expand Down
65 changes: 65 additions & 0 deletions packages/kap-server/test/fileLaunch.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
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', () => {
async function launchAndCapture(cmd: Parameters<typeof launchDetached>[0]) {
const child = Object.assign(new EventEmitter(), { unref: vi.fn() });
mocks.spawn.mockReturnValue(child);
const promise = launchDetached(cmd);
child.emit('spawn');
await promise;
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',
[],
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', () => {
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 }),
);
});
});