mirror of
https://github.com/stablyai/orca.git
synced 2026-10-21 00:03:46 +00:00
fix(win32): suppress Command Prompt window on IDE launches
- Prefer JetBrains GUI executables (`*64.exe`) over `.cmd` shims to avoid console allocation (STA-3040). - Use `start "" /B` when launching GUI apps via batch scripts; shims chain through console helpers that allocate a visible prompt even with `windowsHide`. `start /B` returns immediately, preventing the lingering window.
This commit is contained in:
@@ -32,6 +32,50 @@ describe('resolveExternalEditorLaunchSpec', () => {
|
||||
})
|
||||
})
|
||||
|
||||
it('prefers JetBrains *64.exe GUI launchers over idea.cmd on Windows', () => {
|
||||
resolveCliCommandMock.mockImplementation((command: string) => {
|
||||
if (command === 'idea64') {
|
||||
return 'C:\\Program Files\\JetBrains\\IntelliJ IDEA\\bin\\idea64.exe'
|
||||
}
|
||||
if (command === 'idea') {
|
||||
return 'C:\\Users\\me\\AppData\\Local\\JetBrains\\Toolbox\\scripts\\idea.cmd'
|
||||
}
|
||||
return command
|
||||
})
|
||||
|
||||
expect(
|
||||
resolveExternalEditorLaunchSpec('idea', 'C:\\workspaces\\orca', { platform: 'win32' })
|
||||
).toEqual({
|
||||
kind: 'executable',
|
||||
hideWindowsConsole: true,
|
||||
spawnCmd: 'C:\\Program Files\\JetBrains\\IntelliJ IDEA\\bin\\idea64.exe',
|
||||
spawnArgs: ['C:\\workspaces\\orca']
|
||||
})
|
||||
expect(resolveCliCommandMock).toHaveBeenCalledWith('idea64', { platform: 'win32' })
|
||||
expect(resolveCliCommandMock).not.toHaveBeenCalledWith('idea', expect.anything())
|
||||
})
|
||||
|
||||
it('falls back to the idea.cmd shim when idea64 is not installed', () => {
|
||||
resolveCliCommandMock.mockImplementation((command: string) => {
|
||||
if (command === 'idea64') {
|
||||
return 'idea64'
|
||||
}
|
||||
if (command === 'idea') {
|
||||
return 'C:\\Users\\me\\AppData\\Local\\JetBrains\\Toolbox\\scripts\\idea.cmd'
|
||||
}
|
||||
return command
|
||||
})
|
||||
|
||||
expect(
|
||||
resolveExternalEditorLaunchSpec('idea', 'C:\\workspaces\\orca', { platform: 'win32' })
|
||||
).toEqual({
|
||||
kind: 'executable',
|
||||
hideWindowsConsole: true,
|
||||
spawnCmd: 'C:\\Users\\me\\AppData\\Local\\JetBrains\\Toolbox\\scripts\\idea.cmd',
|
||||
spawnArgs: ['C:\\workspaces\\orca']
|
||||
})
|
||||
})
|
||||
|
||||
it('appends escaped paths to compound macOS open commands', () => {
|
||||
expect(
|
||||
resolveExternalEditorLaunchSpec('open -a "Typora"', "/tmp/note's.md", {
|
||||
@@ -85,6 +129,19 @@ describe('resolveExternalEditorLaunchSpec', () => {
|
||||
})
|
||||
})
|
||||
|
||||
it('wraps non-start GUI compound Windows commands with start /B', () => {
|
||||
expect(
|
||||
resolveExternalEditorLaunchSpec('code --reuse-window', 'C:\\workspaces\\orca', {
|
||||
platform: 'win32'
|
||||
})
|
||||
).toEqual({
|
||||
kind: 'shell',
|
||||
hideWindowsConsole: true,
|
||||
spawnCmd: getCmdExePath(),
|
||||
spawnArgs: ['/d', '/s', '/c', 'start "" /B code --reuse-window C:\\workspaces\\orca']
|
||||
})
|
||||
})
|
||||
|
||||
it('quotes Windows paths with spaces in compound commands', () => {
|
||||
expect(
|
||||
resolveExternalEditorLaunchSpec('start "" notepad', 'C:\\my notes.md', { platform: 'win32' })
|
||||
@@ -245,7 +302,7 @@ describe('resolveExternalEditorLaunchSpec', () => {
|
||||
).toEqual([pathValue])
|
||||
})
|
||||
|
||||
it('does not rewrite compound VS Code commands', () => {
|
||||
it('does not rewrite compound VS Code commands into WSL remote args', () => {
|
||||
const pathValue = '\\\\wsl.localhost\\Ubuntu\\home\\ada\\project'
|
||||
|
||||
expect(
|
||||
@@ -254,7 +311,7 @@ describe('resolveExternalEditorLaunchSpec', () => {
|
||||
kind: 'shell',
|
||||
hideWindowsConsole: true,
|
||||
spawnCmd: getCmdExePath(),
|
||||
spawnArgs: ['/d', '/s', '/c', `code --reuse-window ${pathValue}`]
|
||||
spawnArgs: ['/d', '/s', '/c', `start "" /B code --reuse-window ${pathValue}`]
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,13 +1,32 @@
|
||||
import { spawn } from 'node:child_process'
|
||||
import { existsSync } from 'node:fs'
|
||||
import { basename, posix, win32 } from 'node:path'
|
||||
import { parseWslUncPath } from '../shared/wsl-paths'
|
||||
import { isVsCodeLauncherExecutable } from '../shared/vscode-remote-ssh-launcher'
|
||||
import { resolveCliCommand } from './codex-cli/command'
|
||||
import { getCmdExePath } from './win32-utils'
|
||||
import { getCmdExePath, getSpawnArgsForWindows } from './win32-utils'
|
||||
|
||||
export const EXTERNAL_EDITOR_CLI_COMMAND = 'code'
|
||||
const WINDOWS_CONSOLE_EDITORS = new Set(['nvim', 'vim'])
|
||||
|
||||
// Why: JetBrains installers ship `*64.exe` GUI-subsystem binaries. The
|
||||
// Toolbox/installer `.cmd`/`.bat` shims often chain through console helpers
|
||||
// (`java.exe`), which allocate a visible Command Prompt even when the parent
|
||||
// spawn uses windowsHide — the STA-3040 "prompt window" loop on Windows.
|
||||
const JETBRAINS_WINDOWS_GUI_LAUNCHERS: Readonly<Record<string, string>> = {
|
||||
idea: 'idea64',
|
||||
webstorm: 'webstorm64',
|
||||
pycharm: 'pycharm64',
|
||||
phpstorm: 'phpstorm64',
|
||||
goland: 'goland64',
|
||||
rider: 'rider64',
|
||||
clion: 'clion64',
|
||||
rubymine: 'rubymine64',
|
||||
datagrip: 'datagrip64',
|
||||
rustrover: 'rustrover64',
|
||||
studio: 'studio64'
|
||||
}
|
||||
|
||||
export type ExternalEditorLaunchSpec =
|
||||
| {
|
||||
kind: 'executable'
|
||||
@@ -133,6 +152,20 @@ function isCompoundShellCommand(command: string): boolean {
|
||||
return /\s/.test(command)
|
||||
}
|
||||
|
||||
function resolveSimpleEditorCommand(command: string, platform: NodeJS.Platform): string {
|
||||
if (platform === 'win32') {
|
||||
const jetbrainsGui = JETBRAINS_WINDOWS_GUI_LAUNCHERS[command.toLowerCase()]
|
||||
if (jetbrainsGui) {
|
||||
const resolvedGui = resolveCliCommand(jetbrainsGui, { platform })
|
||||
// Why: resolveCliCommand returns the input name when nothing is found.
|
||||
if (resolvedGui !== jetbrainsGui) {
|
||||
return resolvedGui
|
||||
}
|
||||
}
|
||||
}
|
||||
return resolveCliCommand(command, { platform })
|
||||
}
|
||||
|
||||
function buildShellLaunchSpec(
|
||||
command: string,
|
||||
pathValue: string,
|
||||
@@ -140,11 +173,19 @@ function buildShellLaunchSpec(
|
||||
): ExternalEditorLaunchSpec {
|
||||
const shellCommand = `${command} ${escapePathForShell(pathValue, platform)}`
|
||||
if (platform === 'win32') {
|
||||
const hideWindowsConsole = !shouldShowWindowsConsole(command, platform, {
|
||||
shellCommand: true
|
||||
})
|
||||
// Why: GUI compound commands (not terminal editors) use start /B so a
|
||||
// Command Prompt does not linger; skip when the user already used `start`.
|
||||
const alreadyUsesStart = /^\s*start(\.exe)?\b/i.test(command)
|
||||
const wrappedCommand =
|
||||
hideWindowsConsole && !alreadyUsesStart ? `start "" /B ${shellCommand}` : shellCommand
|
||||
return {
|
||||
kind: 'shell',
|
||||
hideWindowsConsole: !shouldShowWindowsConsole(command, platform, { shellCommand: true }),
|
||||
hideWindowsConsole,
|
||||
spawnCmd: getCmdExePath(),
|
||||
spawnArgs: ['/d', '/s', '/c', shellCommand]
|
||||
spawnArgs: ['/d', '/s', '/c', wrappedCommand]
|
||||
}
|
||||
}
|
||||
return {
|
||||
@@ -178,7 +219,7 @@ export function resolveExternalEditorLaunchSpec(
|
||||
return buildShellLaunchSpec(trimmed, pathValue, platform)
|
||||
}
|
||||
|
||||
const editorCommand = resolveCliCommand(trimmed, { platform })
|
||||
const editorCommand = resolveSimpleEditorCommand(trimmed, platform)
|
||||
return {
|
||||
kind: 'executable',
|
||||
hideWindowsConsole: !shouldShowWindowsConsole(editorCommand, platform),
|
||||
@@ -217,3 +258,52 @@ export function resolveVsCodeRemoteSshLaunchSpec(
|
||||
spawnArgs: ['--remote', `ssh-remote+${authority}`, pathValue]
|
||||
}
|
||||
}
|
||||
|
||||
function resolveExternalEditorSpawn(launchSpec: ExternalEditorLaunchSpec): {
|
||||
spawnCmd: string
|
||||
spawnArgs: string[]
|
||||
windowsHide: boolean
|
||||
} {
|
||||
// Why: GUI Open In .cmd shims (idea.cmd) use start /B so no Command Prompt
|
||||
// lingers; terminal editors keep the waiting form (nvim needs a console).
|
||||
if (launchSpec.kind === 'executable') {
|
||||
const spawned = getSpawnArgsForWindows(launchSpec.spawnCmd, launchSpec.spawnArgs, {
|
||||
detachedGui: launchSpec.hideWindowsConsole
|
||||
})
|
||||
return { ...spawned, windowsHide: launchSpec.hideWindowsConsole }
|
||||
}
|
||||
return {
|
||||
spawnCmd: launchSpec.spawnCmd,
|
||||
spawnArgs: launchSpec.spawnArgs,
|
||||
windowsHide: launchSpec.hideWindowsConsole
|
||||
}
|
||||
}
|
||||
|
||||
export async function launchExternalEditor(launchSpec: ExternalEditorLaunchSpec): Promise<void> {
|
||||
const { spawnCmd, spawnArgs, windowsHide } = resolveExternalEditorSpawn(launchSpec)
|
||||
await new Promise<void>((resolvePromise, rejectPromise) => {
|
||||
const child = spawn(spawnCmd, spawnArgs, { detached: true, stdio: 'ignore', windowsHide })
|
||||
let settled = false
|
||||
function cleanup(): void {
|
||||
child.off('error', onError)
|
||||
child.off('spawn', onSpawn)
|
||||
}
|
||||
function settle(callback: () => void): void {
|
||||
if (settled) {
|
||||
return
|
||||
}
|
||||
settled = true
|
||||
cleanup()
|
||||
callback()
|
||||
}
|
||||
function onError(error: Error): void {
|
||||
settle(() => rejectPromise(error))
|
||||
}
|
||||
function onSpawn(): void {
|
||||
child.unref()
|
||||
settle(resolvePromise)
|
||||
}
|
||||
child.once('error', onError)
|
||||
child.once('spawn', onSpawn)
|
||||
})
|
||||
}
|
||||
|
||||
+77
-43
@@ -297,9 +297,13 @@ describe('registerShellHandlers', () => {
|
||||
expect(resolveCliCommandMock).toHaveBeenCalledWith(EXTERNAL_EDITOR_CLI_COMMAND, {
|
||||
platform: process.platform
|
||||
})
|
||||
expect(getSpawnArgsForWindowsMock).toHaveBeenCalledWith('editor-cli', [
|
||||
normalize(workspacePath)
|
||||
])
|
||||
expect(getSpawnArgsForWindowsMock).toHaveBeenCalledWith(
|
||||
'editor-cli',
|
||||
[normalize(workspacePath)],
|
||||
{
|
||||
detachedGui: true
|
||||
}
|
||||
)
|
||||
expect(spawnMock).toHaveBeenCalledWith('editor-cli', [normalize(workspacePath)], {
|
||||
detached: true,
|
||||
stdio: 'ignore',
|
||||
@@ -320,9 +324,13 @@ describe('registerShellHandlers', () => {
|
||||
expect(resolveCliCommandMock).toHaveBeenCalledWith(EXTERNAL_EDITOR_CLI_COMMAND, {
|
||||
platform: process.platform
|
||||
})
|
||||
expect(getSpawnArgsForWindowsMock).toHaveBeenCalledWith('editor-cli', [
|
||||
normalize(workspacePath)
|
||||
])
|
||||
expect(getSpawnArgsForWindowsMock).toHaveBeenCalledWith(
|
||||
'editor-cli',
|
||||
[normalize(workspacePath)],
|
||||
{
|
||||
detachedGui: true
|
||||
}
|
||||
)
|
||||
expect(spawnMock).toHaveBeenCalledWith('editor-cli', [normalize(workspacePath)], {
|
||||
detached: true,
|
||||
stdio: 'ignore',
|
||||
@@ -341,9 +349,13 @@ describe('registerShellHandlers', () => {
|
||||
ok: true
|
||||
})
|
||||
expect(resolveCliCommandMock).toHaveBeenCalledWith('cursor', { platform: process.platform })
|
||||
expect(getSpawnArgsForWindowsMock).toHaveBeenCalledWith('editor-cli', [
|
||||
normalize(workspacePath)
|
||||
])
|
||||
expect(getSpawnArgsForWindowsMock).toHaveBeenCalledWith(
|
||||
'editor-cli',
|
||||
[normalize(workspacePath)],
|
||||
{
|
||||
detachedGui: true
|
||||
}
|
||||
)
|
||||
})
|
||||
|
||||
it.runIf(process.platform === 'win32')(
|
||||
@@ -357,11 +369,13 @@ describe('registerShellHandlers', () => {
|
||||
await expect(handler({}, { path: workspacePath, command: 'code' })).resolves.toEqual({
|
||||
ok: true
|
||||
})
|
||||
expect(getSpawnArgsForWindowsMock).toHaveBeenCalledWith(codeShim, [
|
||||
'--remote',
|
||||
'wsl+Ubuntu Preview',
|
||||
'/home/Ada Lovelace/project'
|
||||
])
|
||||
expect(getSpawnArgsForWindowsMock).toHaveBeenCalledWith(
|
||||
codeShim,
|
||||
['--remote', 'wsl+Ubuntu Preview', '/home/Ada Lovelace/project'],
|
||||
{
|
||||
detachedGui: true
|
||||
}
|
||||
)
|
||||
}
|
||||
)
|
||||
|
||||
@@ -377,9 +391,13 @@ describe('registerShellHandlers', () => {
|
||||
ok: true
|
||||
})
|
||||
expect(resolveCliCommandMock).not.toHaveBeenCalled()
|
||||
expect(getSpawnArgsForWindowsMock).toHaveBeenCalledWith(nvimPath, [
|
||||
normalize(workspacePath)
|
||||
])
|
||||
expect(getSpawnArgsForWindowsMock).toHaveBeenCalledWith(
|
||||
nvimPath,
|
||||
[normalize(workspacePath)],
|
||||
{
|
||||
detachedGui: false
|
||||
}
|
||||
)
|
||||
expect(spawnMock).toHaveBeenCalledWith(nvimPath, [normalize(workspacePath)], {
|
||||
detached: true,
|
||||
stdio: 'ignore',
|
||||
@@ -400,18 +418,24 @@ describe('registerShellHandlers', () => {
|
||||
await expect(handler({}, { path: workspacePath, command: 'cursor' })).resolves.toEqual({
|
||||
ok: true
|
||||
})
|
||||
expect(getSpawnArgsForWindowsMock).toHaveBeenCalledWith('/usr/local/bin/cursor', [
|
||||
'--new-window',
|
||||
normalize(workspacePath)
|
||||
])
|
||||
expect(getSpawnArgsForWindowsMock).toHaveBeenCalledWith(
|
||||
'/usr/local/bin/cursor',
|
||||
['--new-window', normalize(workspacePath)],
|
||||
{
|
||||
detachedGui: true
|
||||
}
|
||||
)
|
||||
resolveCliCommandMock.mockReturnValueOnce('C:\\Cursor\\cursor.cmd')
|
||||
await expect(handler({}, { path: workspacePath, command: 'cursor' })).resolves.toEqual({
|
||||
ok: true
|
||||
})
|
||||
expect(getSpawnArgsForWindowsMock).toHaveBeenLastCalledWith('C:\\Cursor\\cursor.cmd', [
|
||||
'--new-window',
|
||||
normalize(workspacePath)
|
||||
])
|
||||
expect(getSpawnArgsForWindowsMock).toHaveBeenLastCalledWith(
|
||||
'C:\\Cursor\\cursor.cmd',
|
||||
['--new-window', normalize(workspacePath)],
|
||||
{
|
||||
detachedGui: true
|
||||
}
|
||||
)
|
||||
})
|
||||
|
||||
it('falls back to VS Code when command is blank', async () => {
|
||||
@@ -438,9 +462,13 @@ describe('registerShellHandlers', () => {
|
||||
expect(resolveCliCommandMock).toHaveBeenCalledWith(EXTERNAL_EDITOR_CLI_COMMAND, {
|
||||
platform: process.platform
|
||||
})
|
||||
expect(getSpawnArgsForWindowsMock).toHaveBeenCalledWith('editor-cli', [
|
||||
normalize(workspacePath)
|
||||
])
|
||||
expect(getSpawnArgsForWindowsMock).toHaveBeenCalledWith(
|
||||
'editor-cli',
|
||||
[normalize(workspacePath)],
|
||||
{
|
||||
detachedGui: true
|
||||
}
|
||||
)
|
||||
expect(spawnMock).toHaveBeenCalledWith('platform-runner', ['platform-arg'], {
|
||||
detached: true,
|
||||
stdio: 'ignore',
|
||||
@@ -510,11 +538,13 @@ describe('registerShellHandlers', () => {
|
||||
handler({}, { path: remotePath, command: 'code', connectionId: 'ssh-1' })
|
||||
).resolves.toEqual({ ok: true })
|
||||
expect(statMock).not.toHaveBeenCalled()
|
||||
expect(getSpawnArgsForWindowsMock).toHaveBeenCalledWith('/usr/local/bin/code', [
|
||||
'--remote',
|
||||
'ssh-remote+builder',
|
||||
remotePath
|
||||
])
|
||||
expect(getSpawnArgsForWindowsMock).toHaveBeenCalledWith(
|
||||
'/usr/local/bin/code',
|
||||
['--remote', 'ssh-remote+builder', remotePath],
|
||||
{
|
||||
detachedGui: true
|
||||
}
|
||||
)
|
||||
})
|
||||
|
||||
it('preserves Windows-form SSH paths and uses the manual port-22 authority', async () => {
|
||||
@@ -535,11 +565,13 @@ describe('registerShellHandlers', () => {
|
||||
handler({}, { path: remotePath, command: 'code', connectionId: 'ssh-1' })
|
||||
).resolves.toEqual({ ok: true })
|
||||
expect(statMock).not.toHaveBeenCalled()
|
||||
expect(getSpawnArgsForWindowsMock).toHaveBeenCalledWith('C:\\Tools\\code.cmd', [
|
||||
'--remote',
|
||||
'ssh-remote+Ada@win-builder.example.com',
|
||||
remotePath
|
||||
])
|
||||
expect(getSpawnArgsForWindowsMock).toHaveBeenCalledWith(
|
||||
'C:\\Tools\\code.cmd',
|
||||
['--remote', 'ssh-remote+Ada@win-builder.example.com', remotePath],
|
||||
{
|
||||
detachedGui: true
|
||||
}
|
||||
)
|
||||
})
|
||||
|
||||
it('opens a manual port-22 target with a host-only authority when username is blank', async () => {
|
||||
@@ -558,11 +590,13 @@ describe('registerShellHandlers', () => {
|
||||
await expect(
|
||||
handler({}, { path: '/srv/project', command: 'code', connectionId: 'ssh-1' })
|
||||
).resolves.toEqual({ ok: true })
|
||||
expect(getSpawnArgsForWindowsMock).toHaveBeenCalledWith('/usr/local/bin/code', [
|
||||
'--remote',
|
||||
'ssh-remote+builder.example.com',
|
||||
'/srv/project'
|
||||
])
|
||||
expect(getSpawnArgsForWindowsMock).toHaveBeenCalledWith(
|
||||
'/usr/local/bin/code',
|
||||
['--remote', 'ssh-remote+builder.example.com', '/srv/project'],
|
||||
{
|
||||
detachedGui: true
|
||||
}
|
||||
)
|
||||
})
|
||||
|
||||
it('rejects relative SSH paths before resolving or spawning a launcher', async () => {
|
||||
|
||||
+2
-47
@@ -1,5 +1,4 @@
|
||||
import { ipcMain, shell, dialog } from 'electron'
|
||||
import { spawn } from 'node:child_process'
|
||||
import { constants, copyFile, readFile, stat } from 'node:fs/promises'
|
||||
import { basename, extname, isAbsolute, normalize, posix, win32 } from 'node:path'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
@@ -10,12 +9,11 @@ import type {
|
||||
} from '../../shared/shell-open-types'
|
||||
import { MAX_REPO_ICON_UPLOAD_BYTES } from '../../shared/repo-icon'
|
||||
import type { Store } from '../persistence'
|
||||
import { getSpawnArgsForWindows } from '../win32-utils'
|
||||
import {
|
||||
EXTERNAL_EDITOR_CLI_COMMAND,
|
||||
launchExternalEditor,
|
||||
resolveExternalEditorLaunchSpec,
|
||||
resolveVsCodeRemoteSshLaunchSpec,
|
||||
type ExternalEditorLaunchSpec
|
||||
resolveVsCodeRemoteSshLaunchSpec
|
||||
} from '../external-editor-launch'
|
||||
import { resolveVsCodeSshAuthority } from '../ssh/vscode-ssh-authority'
|
||||
|
||||
@@ -72,49 +70,6 @@ async function openInFileManager(
|
||||
}
|
||||
}
|
||||
|
||||
async function launchExternalEditor(launchSpec: ExternalEditorLaunchSpec): Promise<void> {
|
||||
const { spawnCmd, spawnArgs } =
|
||||
launchSpec.kind === 'executable'
|
||||
? getSpawnArgsForWindows(launchSpec.spawnCmd, launchSpec.spawnArgs)
|
||||
: { spawnCmd: launchSpec.spawnCmd, spawnArgs: launchSpec.spawnArgs }
|
||||
|
||||
await new Promise<void>((resolvePromise, rejectPromise) => {
|
||||
const child = spawn(spawnCmd, spawnArgs, {
|
||||
detached: true,
|
||||
stdio: 'ignore',
|
||||
// Why: terminal editors such as nvim need a visible console on Windows;
|
||||
// GUI editor launches stay hidden to avoid command-shim flashes.
|
||||
windowsHide: launchSpec.hideWindowsConsole
|
||||
})
|
||||
let settled = false
|
||||
|
||||
function cleanup(): void {
|
||||
child.off('error', onError)
|
||||
child.off('spawn', onSpawn)
|
||||
}
|
||||
|
||||
function settle(callback: () => void): void {
|
||||
if (settled) {
|
||||
return
|
||||
}
|
||||
settled = true
|
||||
cleanup()
|
||||
callback()
|
||||
}
|
||||
|
||||
function onError(error: Error): void {
|
||||
settle(() => rejectPromise(error))
|
||||
}
|
||||
|
||||
function onSpawn(): void {
|
||||
child.unref()
|
||||
settle(resolvePromise)
|
||||
}
|
||||
child.once('error', onError)
|
||||
child.once('spawn', onSpawn)
|
||||
})
|
||||
}
|
||||
|
||||
async function openInExternalEditor(
|
||||
store: Store,
|
||||
request: ShellOpenExternalEditorRequest
|
||||
|
||||
@@ -82,6 +82,26 @@ describe('getSpawnArgsForWindows', () => {
|
||||
}
|
||||
})
|
||||
|
||||
it('routes GUI Open In .cmd launches through start /B so no prompt window lingers', () => {
|
||||
withPlatform('win32', () => {
|
||||
const { spawnCmd, spawnArgs } = getSpawnArgsForWindows(
|
||||
'C:\\Tools\\idea.cmd',
|
||||
['C:\\workspaces\\orca'],
|
||||
{ detachedGui: true }
|
||||
)
|
||||
expect(spawnCmd).toBe(getCmdExePath())
|
||||
expect(spawnArgs).toEqual([
|
||||
'/d',
|
||||
'/c',
|
||||
'start',
|
||||
'""',
|
||||
'/B',
|
||||
'C:\\Tools\\idea.cmd',
|
||||
'C:\\workspaces\\orca'
|
||||
])
|
||||
})
|
||||
})
|
||||
|
||||
it('preserves VS Code WSL remote arguments with spaces through .cmd launchers', () => {
|
||||
withPlatform('win32', () => {
|
||||
const { spawnCmd, spawnArgs } = getSpawnArgsForWindows('C:\\tools\\code.cmd', [
|
||||
|
||||
@@ -8,7 +8,8 @@ export {
|
||||
isWindowsBatchScript,
|
||||
WINDOWS_BATCH_UNSAFE_ARGUMENTS_ERROR,
|
||||
WINDOWS_BATCH_UNSAFE_CHARACTERS_LABEL,
|
||||
UnsafeWindowsBatchArgumentsError
|
||||
UnsafeWindowsBatchArgumentsError,
|
||||
type GetSpawnArgsForWindowsOptions
|
||||
} from '../shared/windows-batch-spawn'
|
||||
|
||||
function execFileWithoutBlocking(
|
||||
|
||||
@@ -39,9 +39,19 @@ function hasUnsafeWindowsBatchSyntax(value: string): boolean {
|
||||
return UNSAFE_WINDOWS_BATCH_SYNTAX.test(value)
|
||||
}
|
||||
|
||||
export type GetSpawnArgsForWindowsOptions = {
|
||||
/**
|
||||
* GUI launchers (Open In apps) should not leave a lingering Command Prompt.
|
||||
* `start "" /B` returns immediately and keeps console-subsystem children of
|
||||
* `.cmd`/`.bat` shims from allocating a fresh visible prompt window.
|
||||
*/
|
||||
detachedGui?: boolean
|
||||
}
|
||||
|
||||
export function getSpawnArgsForWindows(
|
||||
command: string,
|
||||
args: string[]
|
||||
args: string[],
|
||||
options: GetSpawnArgsForWindowsOptions = {}
|
||||
): { spawnCmd: string; spawnArgs: string[] } {
|
||||
if (isWindowsBatchScript(command)) {
|
||||
for (const value of [command, ...args]) {
|
||||
@@ -51,6 +61,14 @@ export function getSpawnArgsForWindows(
|
||||
}
|
||||
|
||||
// Why: separate argv entries let Node quote spaces without breaking cmd.
|
||||
if (options.detachedGui) {
|
||||
// Why: empty title (`""`) is required so `start` does not treat the first
|
||||
// quoted path as a window title; `/B` avoids a new console window.
|
||||
return {
|
||||
spawnCmd: getCmdExePath(),
|
||||
spawnArgs: ['/d', '/c', 'start', '""', '/B', command, ...args]
|
||||
}
|
||||
}
|
||||
return { spawnCmd: getCmdExePath(), spawnArgs: ['/d', '/c', command, ...args] }
|
||||
}
|
||||
return { spawnCmd: command, spawnArgs: args }
|
||||
|
||||
Reference in New Issue
Block a user