mirror of
https://github.com/google-gemini/gemini-cli.git
synced 2026-05-24 05:04:28 +00:00
fix(windows): resolve interactive shell arrow-key navigation on Windows (#23505)
This commit is contained in:
@@ -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,
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
@@ -1051,7 +1059,9 @@ describe('ShellExecutionService', () => {
|
||||
'-c',
|
||||
'trap \'\' HUP; shopt -u promptvars nullglob extglob nocaseglob dotglob; ls "foo bar"',
|
||||
],
|
||||
expect.any(Object),
|
||||
expect.objectContaining({
|
||||
handleFlowControl: true,
|
||||
}),
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -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,
|
||||
|
||||
@@ -980,6 +980,7 @@ 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,
|
||||
@@ -987,7 +988,16 @@ export class ShellExecutionService {
|
||||
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
|
||||
@@ -1027,6 +1037,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 {
|
||||
|
||||
@@ -488,7 +488,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');
|
||||
});
|
||||
|
||||
@@ -513,7 +517,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');
|
||||
});
|
||||
|
||||
@@ -523,7 +531,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');
|
||||
});
|
||||
|
||||
@@ -532,7 +544,11 @@ 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');
|
||||
});
|
||||
|
||||
@@ -540,7 +556,11 @@ describe('getShellConfiguration', () => {
|
||||
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');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -691,6 +691,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();
|
||||
@@ -700,7 +705,7 @@ export function getShellConfiguration(): ShellConfiguration {
|
||||
) {
|
||||
return {
|
||||
executable: comSpec,
|
||||
argsPrefix: ['-NoProfile', '-Command'],
|
||||
argsPrefix: powershellArgsPrefix,
|
||||
shell: 'powershell',
|
||||
};
|
||||
}
|
||||
@@ -718,7 +723,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',
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user