Skip to content
Merged
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
18 changes: 14 additions & 4 deletions packages/core/src/services/shellExecutionService.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1034,8 +1034,16 @@ describe('ShellExecutionService', () => {

expect(mockPtySpawn).toHaveBeenCalledWith(
'powershell.exe',
['-NoProfile', '-Command', 'chcp 65001 >$null;dir "foo bar"'],
expect.any(Object),
[
'-NoProfile',
'-NonInteractive',
'-Command',
'chcp 65001 >$null;dir "foo bar"',
],
expect.objectContaining({
handleFlowControl: false,
useConpty: true,
}),
);
});

Expand All @@ -1051,7 +1059,9 @@ describe('ShellExecutionService', () => {
'-c',
'shopt -u promptvars nullglob extglob nocaseglob dotglob; ls "foo bar"',
],
expect.any(Object),
expect.objectContaining({
handleFlowControl: true,
}),
);
});
});
Expand Down Expand Up @@ -1644,7 +1654,7 @@ describe('ShellExecutionService child_process fallback', () => {

expect(mockCpSpawn).toHaveBeenCalledWith(
'powershell.exe',
['-NoProfile', '-Command', 'dir "foo bar"'],
['-NoProfile', '-NonInteractive', '-Command', 'dir "foo bar"'],
expect.objectContaining({
shell: false,
detached: false,
Expand Down
19 changes: 18 additions & 1 deletion packages/core/src/services/shellExecutionService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -946,14 +946,24 @@ export class ShellExecutionService {
cwd: finalCwd,
} = prepared;

const isWindowsPlatform = os.platform() === 'win32';
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
const ptyProcess = ptyInfo.module.spawn(finalExecutable, finalArgs, {
cwd: finalCwd,
name: 'xterm-256color',
cols,
rows,
env: finalEnv,
handleFlowControl: true,
// handleFlowControl intercepts XON/XOFF (Ctrl+S/Q) and prevents them
// from reaching the child. On Windows, the flag can interfere with
// ConPTY's internal input routing and cause interactive TUI tools to
// miss key events, so we disable it there.
handleFlowControl: !isWindowsPlatform,
// On Windows, explicitly request ConPTY (introduced in Windows 10 1809).
// Without this, @lydell/node-pty may silently fall back to WinPTY, which
// has known incompatibilities with interactive Node.js TUI applications
// that rely on VT-sequence-based arrow-key navigation.
...(isWindowsPlatform ? { useConpty: true } : {}),
});

// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
Expand Down Expand Up @@ -993,6 +1003,13 @@ export class ShellExecutionService {
}).catch(() => {});
},
isActive: () => {
// On Windows, process.kill(pid, 0) can return false negatives
// for ConPTY-managed shell wrappers (powershell.exe), causing
// writeToPty to silently discard input (including arrow keys).
// Check the internal activePtys map first for reliable status.
if (ShellExecutionService.activePtys.has(ptyPid)) {
return true;
}
try {
return process.kill(ptyPid, 0);
} catch {
Expand Down
30 changes: 25 additions & 5 deletions packages/core/src/utils/shell-utils.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -486,7 +486,11 @@ describe('getShellConfiguration', () => {
it('should return PowerShell configuration by default', () => {
const config = getShellConfiguration();
expect(config.executable).toBe('powershell.exe');
expect(config.argsPrefix).toEqual(['-NoProfile', '-Command']);
expect(config.argsPrefix).toEqual([
'-NoProfile',
'-NonInteractive',
'-Command',
]);
expect(config.shell).toBe('powershell');
});

Expand All @@ -511,7 +515,11 @@ describe('getShellConfiguration', () => {
vi.stubEnv('ComSpec', 'C:\\WINDOWS\\system32\\cmd.exe');
const config = getShellConfiguration();
expect(config.executable).toBe('powershell.exe');
expect(config.argsPrefix).toEqual(['-NoProfile', '-Command']);
expect(config.argsPrefix).toEqual([
'-NoProfile',
'-NonInteractive',
'-Command',
]);
expect(config.shell).toBe('powershell');
});

Expand All @@ -521,7 +529,11 @@ describe('getShellConfiguration', () => {
vi.stubEnv('ComSpec', psPath);
const config = getShellConfiguration();
expect(config.executable).toBe(psPath);
expect(config.argsPrefix).toEqual(['-NoProfile', '-Command']);
expect(config.argsPrefix).toEqual([
'-NoProfile',
'-NonInteractive',
'-Command',
]);
expect(config.shell).toBe('powershell');
});

Expand All @@ -530,15 +542,23 @@ describe('getShellConfiguration', () => {
vi.stubEnv('ComSpec', pwshPath);
const config = getShellConfiguration();
expect(config.executable).toBe(pwshPath);
expect(config.argsPrefix).toEqual(['-NoProfile', '-Command']);
expect(config.argsPrefix).toEqual([
'-NoProfile',
'-NonInteractive',
'-Command',
]);
expect(config.shell).toBe('powershell');
});

it('should be case-insensitive when checking ComSpec', () => {
vi.stubEnv('ComSpec', 'C:\\Path\\To\\POWERSHELL.EXE');
const config = getShellConfiguration();
expect(config.executable).toBe('C:\\Path\\To\\POWERSHELL.EXE');
expect(config.argsPrefix).toEqual(['-NoProfile', '-Command']);
expect(config.argsPrefix).toEqual([
'-NoProfile',
'-NonInteractive',
'-Command',
]);
expect(config.shell).toBe('powershell');
});
});
Expand Down
9 changes: 7 additions & 2 deletions packages/core/src/utils/shell-utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -657,6 +657,11 @@ export function parseCommandDetails(
*/
export function getShellConfiguration(): ShellConfiguration {
if (isWindows()) {
// -NonInteractive prevents PSReadLine from intercepting console input
// events inside the ConPTY session, which otherwise causes interactive
// TUI tools (e.g. pnpm create vite, vim) to receive malformed key events
// and exit when arrow keys are pressed.
const powershellArgsPrefix = ['-NoProfile', '-NonInteractive', '-Command'];
const comSpec = process.env['ComSpec'];
if (comSpec) {
const executable = comSpec.toLowerCase();
Expand All @@ -666,7 +671,7 @@ export function getShellConfiguration(): ShellConfiguration {
) {
return {
executable: comSpec,
argsPrefix: ['-NoProfile', '-Command'],
argsPrefix: powershellArgsPrefix,
shell: 'powershell',
};
}
Expand All @@ -684,7 +689,7 @@ export function getShellConfiguration(): ShellConfiguration {
// Fall back to Windows PowerShell 5.1 when pwsh.exe is not installed.
return {
executable: 'powershell.exe',
argsPrefix: ['-NoProfile', '-Command'],
argsPrefix: powershellArgsPrefix,
shell: 'powershell',
};
}
Expand Down
Loading