From 568444133a40da19feefd4c8090b4727723d18e4 Mon Sep 17 00:00:00 2001 From: Taylor Mullen Date: Wed, 4 Feb 2026 22:10:54 -0800 Subject: [PATCH] fix(cli): ensure Shift+Tab unfocuses interactive shell - Update Command.UNFOCUS_SHELL_INPUT to explicitly require shift: true. - Allow UNFOCUS_SHELL_INPUT to bubble up from ShellInputPrompt. - Update AppContainer to handle UNFOCUS_SHELL_INPUT for unfocusing the shell. - Add unit tests for ShellInputPrompt and AppContainer. --- packages/cli/src/config/keyBindings.ts | 2 +- packages/cli/src/ui/AppContainer.test.tsx | 22 +++++++++++++++++++ packages/cli/src/ui/AppContainer.tsx | 5 +++-- .../ui/components/ShellInputPrompt.test.tsx | 18 +++++++++++++++ .../src/ui/components/ShellInputPrompt.tsx | 5 +++++ 5 files changed, 49 insertions(+), 3 deletions(-) diff --git a/packages/cli/src/config/keyBindings.ts b/packages/cli/src/config/keyBindings.ts index 9b6a903a4b..efd2c61bda 100644 --- a/packages/cli/src/config/keyBindings.ts +++ b/packages/cli/src/config/keyBindings.ts @@ -288,7 +288,7 @@ export const defaultKeyBindings: KeyBindingConfig = { { key: 's', ctrl: true }, ], [Command.FOCUS_SHELL_INPUT]: [{ key: 'tab', shift: false }], - [Command.UNFOCUS_SHELL_INPUT]: [{ key: 'tab' }], + [Command.UNFOCUS_SHELL_INPUT]: [{ key: 'tab', shift: true }], [Command.CLEAR_SCREEN]: [{ key: 'l', ctrl: true }], [Command.RESTART_APP]: [{ key: 'r' }], [Command.SUSPEND_APP]: [{ key: 'z', ctrl: true }], diff --git a/packages/cli/src/ui/AppContainer.test.tsx b/packages/cli/src/ui/AppContainer.test.tsx index 3ee4e89ea5..ff42557a95 100644 --- a/packages/cli/src/ui/AppContainer.test.tsx +++ b/packages/cli/src/ui/AppContainer.test.tsx @@ -1940,6 +1940,28 @@ describe('AppContainer State Management', () => { unmount(); }); }); + + describe('Shell Focus', () => { + it('should unfocus shell when Shift+Tab is pressed', async () => { + mockedUseGeminiStream.mockReturnValue({ + ...DEFAULT_GEMINI_STREAM_MOCK, + activePtyId: 1, + }); + + await setupKeypressTest(); + + act(() => { + capturedUIActions.setEmbeddedShellFocused(true); + }); + rerender(); + expect(capturedUIState.embeddedShellFocused).toBe(true); + + pressKey({ name: 'tab', shift: true }); + + expect(capturedUIState.embeddedShellFocused).toBe(false); + unmount(); + }); + }); }); describe('Copy Mode (CTRL+S)', () => { diff --git a/packages/cli/src/ui/AppContainer.tsx b/packages/cli/src/ui/AppContainer.tsx index 7c10569902..06f2dd903e 100644 --- a/packages/cli/src/ui/AppContainer.tsx +++ b/packages/cli/src/ui/AppContainer.tsx @@ -1500,10 +1500,11 @@ Logging in with Google... Restarting Gemini CLI to continue. setConstrainHeight(false); return true; } else if ( - keyMatchers[Command.FOCUS_SHELL_INPUT](key) && + (keyMatchers[Command.FOCUS_SHELL_INPUT](key) || + keyMatchers[Command.UNFOCUS_SHELL_INPUT](key)) && (activePtyId || (isBackgroundShellVisible && backgroundShells.size > 0)) ) { - if (key.name === 'tab' && key.shift) { + if (keyMatchers[Command.UNFOCUS_SHELL_INPUT](key)) { // Always change focus setEmbeddedShellFocused(false); return true; diff --git a/packages/cli/src/ui/components/ShellInputPrompt.test.tsx b/packages/cli/src/ui/components/ShellInputPrompt.test.tsx index 5a204b0580..57e9c017b7 100644 --- a/packages/cli/src/ui/components/ShellInputPrompt.test.tsx +++ b/packages/cli/src/ui/components/ShellInputPrompt.test.tsx @@ -111,4 +111,22 @@ describe('ShellInputPrompt', () => { expect(mockWriteToPty).not.toHaveBeenCalled(); }); + + it('bubbles up Shift+Tab for unfocusing', () => { + render(); + + const handler = mockUseKeypress.mock.calls[0][0]; + + const result = handler({ + name: 'tab', + shift: true, + alt: false, + ctrl: false, + cmd: false, + sequence: '\x1b[Z', + }); + + expect(result).toBe(false); + expect(mockWriteToPty).not.toHaveBeenCalled(); + }); }); diff --git a/packages/cli/src/ui/components/ShellInputPrompt.tsx b/packages/cli/src/ui/components/ShellInputPrompt.tsx index 4f956ae262..976831f1f4 100644 --- a/packages/cli/src/ui/components/ShellInputPrompt.tsx +++ b/packages/cli/src/ui/components/ShellInputPrompt.tsx @@ -40,6 +40,11 @@ export const ShellInputPrompt: React.FC = ({ return false; } + // Allow unfocus to bubble up + if (keyMatchers[Command.UNFOCUS_SHELL_INPUT](key)) { + return false; + } + if (key.ctrl && key.shift && key.name === 'up') { ShellExecutionService.scrollPty(activeShellPtyId, -1); return true;