From 4469878941f56f1ea21415b40fb19296dfed41ca Mon Sep 17 00:00:00 2001 From: jacob314 Date: Thu, 12 Feb 2026 00:02:49 -0800 Subject: [PATCH 1/3] Disable ctrl-S shortcut outside of alternate buffe mode with a warning message. --- packages/cli/src/config/keyBindings.ts | 7 ++---- packages/cli/src/ui/AppContainer.test.tsx | 1 + packages/cli/src/ui/AppContainer.tsx | 22 +++++++++++++++++++ .../src/ui/components/ToastDisplay.test.tsx | 14 ++++++++++++ .../cli/src/ui/components/ToastDisplay.tsx | 9 ++++++++ .../__snapshots__/ToastDisplay.test.tsx.snap | 2 ++ .../cli/src/ui/contexts/UIStateContext.tsx | 1 + 7 files changed, 51 insertions(+), 5 deletions(-) diff --git a/packages/cli/src/config/keyBindings.ts b/packages/cli/src/config/keyBindings.ts index c3f1f70fbe3..35e1bb8041c 100644 --- a/packages/cli/src/config/keyBindings.ts +++ b/packages/cli/src/config/keyBindings.ts @@ -286,10 +286,7 @@ export const defaultKeyBindings: KeyBindingConfig = { [Command.SHOW_SHELL_INPUT_UNFOCUS_WARNING]: [{ key: 'tab', shift: false }], [Command.BACKGROUND_SHELL_SELECT]: [{ key: 'return' }], [Command.BACKGROUND_SHELL_ESCAPE]: [{ key: 'escape' }], - [Command.SHOW_MORE_LINES]: [ - { key: 'o', ctrl: true }, - { key: 's', ctrl: true }, - ], + [Command.SHOW_MORE_LINES]: [{ key: 'o', ctrl: true }], [Command.EXPAND_PASTE]: [{ key: 'o', ctrl: true }], [Command.FOCUS_SHELL_INPUT]: [{ key: 'tab', shift: false }], [Command.UNFOCUS_SHELL_INPUT]: [{ key: 'tab', shift: true }], @@ -501,7 +498,7 @@ export const commandDescriptions: Readonly> = { [Command.CYCLE_APPROVAL_MODE]: 'Cycle through approval modes: default (prompt), auto_edit (auto-approve edits), and plan (read-only).', [Command.SHOW_MORE_LINES]: - 'Expand a height-constrained response to show additional lines when not in alternate buffer mode.', + 'Expand and collapse blocks of content when not in alternate buffer mode.', [Command.EXPAND_PASTE]: 'Expand or collapse a paste placeholder when cursor is over placeholder.', [Command.BACKGROUND_SHELL_SELECT]: diff --git a/packages/cli/src/ui/AppContainer.test.tsx b/packages/cli/src/ui/AppContainer.test.tsx index 028584537d3..73039350f6d 100644 --- a/packages/cli/src/ui/AppContainer.test.tsx +++ b/packages/cli/src/ui/AppContainer.test.tsx @@ -2355,6 +2355,7 @@ describe('AppContainer State Management', () => { expect(disableMouseEvents).toHaveBeenCalled(); } else { expect(disableMouseEvents).not.toHaveBeenCalled(); + expect(capturedUIState.ctrlSPressed).toBe(true); } unmount(); }); diff --git a/packages/cli/src/ui/AppContainer.tsx b/packages/cli/src/ui/AppContainer.tsx index 17e54f4771c..8904f8e953d 100644 --- a/packages/cli/src/ui/AppContainer.tsx +++ b/packages/cli/src/ui/AppContainer.tsx @@ -1398,6 +1398,8 @@ Logging in with Google... Restarting Gemini CLI to continue. const ctrlCTimerRef = useRef(null); const [ctrlDPressCount, setCtrlDPressCount] = useState(0); const ctrlDTimerRef = useRef(null); + const [ctrlSPressed, setCtrlSPressed] = useState(false); + const ctrlSTimerRef = useRef(null); const [constrainHeight, setConstrainHeight] = useState(true); const [ideContextState, setIdeContextState] = useState< IdeContext | undefined @@ -1567,6 +1569,19 @@ Logging in with Google... Restarting Gemini CLI to continue. } }, [ctrlDPressCount, config, setCtrlDPressCount, handleSlashCommand]); + useEffect(() => { + if (ctrlSTimerRef.current) { + clearTimeout(ctrlSTimerRef.current); + ctrlSTimerRef.current = null; + } + if (ctrlSPressed) { + ctrlSTimerRef.current = setTimeout(() => { + setCtrlSPressed(false); + ctrlSTimerRef.current = null; + }, WARNING_PROMPT_DURATION_MS); + } + }, [ctrlSPressed]); + const handleEscapePromptChange = useCallback((showPrompt: boolean) => { setShowEscapePrompt(showPrompt); }, []); @@ -1620,6 +1635,11 @@ Logging in with Google... Restarting Gemini CLI to continue. return true; } else if (keyMatchers[Command.SUSPEND_APP](key)) { handleSuspend(); + } else if ( + keyMatchers[Command.TOGGLE_COPY_MODE](key) && + !isAlternateBuffer + ) { + setCtrlSPressed(true); return true; } @@ -2055,6 +2075,7 @@ Logging in with Google... Restarting Gemini CLI to continue. renderMarkdown, ctrlCPressedOnce: ctrlCPressCount >= 1, ctrlDPressedOnce: ctrlDPressCount >= 1, + ctrlSPressed, showEscapePrompt, shortcutsHelpVisible, cleanUiDetailsVisible, @@ -2166,6 +2187,7 @@ Logging in with Google... Restarting Gemini CLI to continue. renderMarkdown, ctrlCPressCount, ctrlDPressCount, + ctrlSPressed, showEscapePrompt, shortcutsHelpVisible, cleanUiDetailsVisible, diff --git a/packages/cli/src/ui/components/ToastDisplay.test.tsx b/packages/cli/src/ui/components/ToastDisplay.test.tsx index 5f48392749e..9ee4706f0a1 100644 --- a/packages/cli/src/ui/components/ToastDisplay.test.tsx +++ b/packages/cli/src/ui/components/ToastDisplay.test.tsx @@ -29,6 +29,7 @@ describe('ToastDisplay', () => { describe('shouldShowToast', () => { const baseState: Partial = { ctrlCPressedOnce: false, + ctrlSPressed: false, transientMessage: null, ctrlDPressedOnce: false, showEscapePrompt: false, @@ -47,6 +48,12 @@ describe('ToastDisplay', () => { ).toBe(true); }); + it('returns true when ctrlSPressed is true', () => { + expect( + shouldShowToast({ ...baseState, ctrlSPressed: true } as UIState), + ).toBe(true); + }); + it('returns true when transientMessage is present', () => { expect( shouldShowToast({ @@ -113,6 +120,13 @@ describe('ToastDisplay', () => { expect(lastFrame()).toMatchSnapshot(); }); + it('renders Ctrl+S prompt', () => { + const { lastFrame } = renderToastDisplay({ + ctrlSPressed: true, + }); + expect(lastFrame()).toMatchSnapshot(); + }); + it('renders warning message', () => { const { lastFrame } = renderToastDisplay({ transientMessage: { diff --git a/packages/cli/src/ui/components/ToastDisplay.tsx b/packages/cli/src/ui/components/ToastDisplay.tsx index 37d2997e336..2f3822fa5c8 100644 --- a/packages/cli/src/ui/components/ToastDisplay.tsx +++ b/packages/cli/src/ui/components/ToastDisplay.tsx @@ -13,6 +13,7 @@ import { TransientMessageType } from '../../utils/events.js'; export function shouldShowToast(uiState: UIState): boolean { return ( uiState.ctrlCPressedOnce || + uiState.ctrlSPressed || Boolean(uiState.transientMessage) || uiState.ctrlDPressedOnce || (uiState.showEscapePrompt && @@ -30,6 +31,14 @@ export const ToastDisplay: React.FC = () => { ); } + if (uiState.ctrlSPressed) { + return ( + + Use Ctrl+O to expand and collapse blocks of content. + + ); + } + if ( uiState.transientMessage?.type === TransientMessageType.Warning && uiState.transientMessage.text diff --git a/packages/cli/src/ui/components/__snapshots__/ToastDisplay.test.tsx.snap b/packages/cli/src/ui/components/__snapshots__/ToastDisplay.test.tsx.snap index e1c2605cfd4..0e5e415836c 100644 --- a/packages/cli/src/ui/components/__snapshots__/ToastDisplay.test.tsx.snap +++ b/packages/cli/src/ui/components/__snapshots__/ToastDisplay.test.tsx.snap @@ -4,6 +4,8 @@ exports[`ToastDisplay > renders Ctrl+C prompt 1`] = `"Press Ctrl+C again to exit exports[`ToastDisplay > renders Ctrl+D prompt 1`] = `"Press Ctrl+D again to exit."`; +exports[`ToastDisplay > renders Ctrl+S prompt 1`] = `"Use Ctrl+O to expand and collapse blocks of content."`; + exports[`ToastDisplay > renders Escape prompt when buffer is NOT empty 1`] = `"Press Esc again to clear prompt."`; exports[`ToastDisplay > renders Escape prompt when buffer is empty 1`] = `"Press Esc again to rewind."`; diff --git a/packages/cli/src/ui/contexts/UIStateContext.tsx b/packages/cli/src/ui/contexts/UIStateContext.tsx index 54bde1732a3..62c2f9da7dc 100644 --- a/packages/cli/src/ui/contexts/UIStateContext.tsx +++ b/packages/cli/src/ui/contexts/UIStateContext.tsx @@ -118,6 +118,7 @@ export interface UIState { renderMarkdown: boolean; ctrlCPressedOnce: boolean; ctrlDPressedOnce: boolean; + ctrlSPressed: boolean; showEscapePrompt: boolean; shortcutsHelpVisible: boolean; cleanUiDetailsVisible: boolean; From 7f05ef8fadd54197dca3db6b53e3227fa5043b86 Mon Sep 17 00:00:00 2001 From: jacob314 Date: Thu, 12 Feb 2026 00:13:12 -0800 Subject: [PATCH 2/3] fix(cli): use transient messages instead of UIState for ctrl-s warning --- packages/cli/src/ui/AppContainer.test.tsx | 1 - packages/cli/src/ui/AppContainer.tsx | 22 ++++--------------- .../src/ui/components/ToastDisplay.test.tsx | 14 ------------ .../cli/src/ui/components/ToastDisplay.tsx | 9 -------- .../__snapshots__/ToastDisplay.test.tsx.snap | 2 -- .../cli/src/ui/contexts/UIStateContext.tsx | 1 - 6 files changed, 4 insertions(+), 45 deletions(-) diff --git a/packages/cli/src/ui/AppContainer.test.tsx b/packages/cli/src/ui/AppContainer.test.tsx index 73039350f6d..028584537d3 100644 --- a/packages/cli/src/ui/AppContainer.test.tsx +++ b/packages/cli/src/ui/AppContainer.test.tsx @@ -2355,7 +2355,6 @@ describe('AppContainer State Management', () => { expect(disableMouseEvents).toHaveBeenCalled(); } else { expect(disableMouseEvents).not.toHaveBeenCalled(); - expect(capturedUIState.ctrlSPressed).toBe(true); } unmount(); }); diff --git a/packages/cli/src/ui/AppContainer.tsx b/packages/cli/src/ui/AppContainer.tsx index 8904f8e953d..1d91d442562 100644 --- a/packages/cli/src/ui/AppContainer.tsx +++ b/packages/cli/src/ui/AppContainer.tsx @@ -1398,8 +1398,6 @@ Logging in with Google... Restarting Gemini CLI to continue. const ctrlCTimerRef = useRef(null); const [ctrlDPressCount, setCtrlDPressCount] = useState(0); const ctrlDTimerRef = useRef(null); - const [ctrlSPressed, setCtrlSPressed] = useState(false); - const ctrlSTimerRef = useRef(null); const [constrainHeight, setConstrainHeight] = useState(true); const [ideContextState, setIdeContextState] = useState< IdeContext | undefined @@ -1569,19 +1567,6 @@ Logging in with Google... Restarting Gemini CLI to continue. } }, [ctrlDPressCount, config, setCtrlDPressCount, handleSlashCommand]); - useEffect(() => { - if (ctrlSTimerRef.current) { - clearTimeout(ctrlSTimerRef.current); - ctrlSTimerRef.current = null; - } - if (ctrlSPressed) { - ctrlSTimerRef.current = setTimeout(() => { - setCtrlSPressed(false); - ctrlSTimerRef.current = null; - }, WARNING_PROMPT_DURATION_MS); - } - }, [ctrlSPressed]); - const handleEscapePromptChange = useCallback((showPrompt: boolean) => { setShowEscapePrompt(showPrompt); }, []); @@ -1639,7 +1624,10 @@ Logging in with Google... Restarting Gemini CLI to continue. keyMatchers[Command.TOGGLE_COPY_MODE](key) && !isAlternateBuffer ) { - setCtrlSPressed(true); + showTransientMessage({ + text: 'Use Ctrl+O to expand and collapse blocks of content.', + type: TransientMessageType.Warning, + }); return true; } @@ -2075,7 +2063,6 @@ Logging in with Google... Restarting Gemini CLI to continue. renderMarkdown, ctrlCPressedOnce: ctrlCPressCount >= 1, ctrlDPressedOnce: ctrlDPressCount >= 1, - ctrlSPressed, showEscapePrompt, shortcutsHelpVisible, cleanUiDetailsVisible, @@ -2187,7 +2174,6 @@ Logging in with Google... Restarting Gemini CLI to continue. renderMarkdown, ctrlCPressCount, ctrlDPressCount, - ctrlSPressed, showEscapePrompt, shortcutsHelpVisible, cleanUiDetailsVisible, diff --git a/packages/cli/src/ui/components/ToastDisplay.test.tsx b/packages/cli/src/ui/components/ToastDisplay.test.tsx index 9ee4706f0a1..5f48392749e 100644 --- a/packages/cli/src/ui/components/ToastDisplay.test.tsx +++ b/packages/cli/src/ui/components/ToastDisplay.test.tsx @@ -29,7 +29,6 @@ describe('ToastDisplay', () => { describe('shouldShowToast', () => { const baseState: Partial = { ctrlCPressedOnce: false, - ctrlSPressed: false, transientMessage: null, ctrlDPressedOnce: false, showEscapePrompt: false, @@ -48,12 +47,6 @@ describe('ToastDisplay', () => { ).toBe(true); }); - it('returns true when ctrlSPressed is true', () => { - expect( - shouldShowToast({ ...baseState, ctrlSPressed: true } as UIState), - ).toBe(true); - }); - it('returns true when transientMessage is present', () => { expect( shouldShowToast({ @@ -120,13 +113,6 @@ describe('ToastDisplay', () => { expect(lastFrame()).toMatchSnapshot(); }); - it('renders Ctrl+S prompt', () => { - const { lastFrame } = renderToastDisplay({ - ctrlSPressed: true, - }); - expect(lastFrame()).toMatchSnapshot(); - }); - it('renders warning message', () => { const { lastFrame } = renderToastDisplay({ transientMessage: { diff --git a/packages/cli/src/ui/components/ToastDisplay.tsx b/packages/cli/src/ui/components/ToastDisplay.tsx index 2f3822fa5c8..37d2997e336 100644 --- a/packages/cli/src/ui/components/ToastDisplay.tsx +++ b/packages/cli/src/ui/components/ToastDisplay.tsx @@ -13,7 +13,6 @@ import { TransientMessageType } from '../../utils/events.js'; export function shouldShowToast(uiState: UIState): boolean { return ( uiState.ctrlCPressedOnce || - uiState.ctrlSPressed || Boolean(uiState.transientMessage) || uiState.ctrlDPressedOnce || (uiState.showEscapePrompt && @@ -31,14 +30,6 @@ export const ToastDisplay: React.FC = () => { ); } - if (uiState.ctrlSPressed) { - return ( - - Use Ctrl+O to expand and collapse blocks of content. - - ); - } - if ( uiState.transientMessage?.type === TransientMessageType.Warning && uiState.transientMessage.text diff --git a/packages/cli/src/ui/components/__snapshots__/ToastDisplay.test.tsx.snap b/packages/cli/src/ui/components/__snapshots__/ToastDisplay.test.tsx.snap index 0e5e415836c..e1c2605cfd4 100644 --- a/packages/cli/src/ui/components/__snapshots__/ToastDisplay.test.tsx.snap +++ b/packages/cli/src/ui/components/__snapshots__/ToastDisplay.test.tsx.snap @@ -4,8 +4,6 @@ exports[`ToastDisplay > renders Ctrl+C prompt 1`] = `"Press Ctrl+C again to exit exports[`ToastDisplay > renders Ctrl+D prompt 1`] = `"Press Ctrl+D again to exit."`; -exports[`ToastDisplay > renders Ctrl+S prompt 1`] = `"Use Ctrl+O to expand and collapse blocks of content."`; - exports[`ToastDisplay > renders Escape prompt when buffer is NOT empty 1`] = `"Press Esc again to clear prompt."`; exports[`ToastDisplay > renders Escape prompt when buffer is empty 1`] = `"Press Esc again to rewind."`; diff --git a/packages/cli/src/ui/contexts/UIStateContext.tsx b/packages/cli/src/ui/contexts/UIStateContext.tsx index 62c2f9da7dc..54bde1732a3 100644 --- a/packages/cli/src/ui/contexts/UIStateContext.tsx +++ b/packages/cli/src/ui/contexts/UIStateContext.tsx @@ -118,7 +118,6 @@ export interface UIState { renderMarkdown: boolean; ctrlCPressedOnce: boolean; ctrlDPressedOnce: boolean; - ctrlSPressed: boolean; showEscapePrompt: boolean; shortcutsHelpVisible: boolean; cleanUiDetailsVisible: boolean; From e3ea2637074a9452205c7a143e1b487febfbc0d0 Mon Sep 17 00:00:00 2001 From: jacob314 Date: Thu, 12 Feb 2026 00:23:21 -0800 Subject: [PATCH 3/3] fix(cli): remove ctrl-s from showMoreLines tests --- docs/cli/keyboard-shortcuts.md | 50 ++++++++++++------------- packages/cli/src/ui/keyMatchers.test.ts | 7 ++-- 2 files changed, 29 insertions(+), 28 deletions(-) diff --git a/docs/cli/keyboard-shortcuts.md b/docs/cli/keyboard-shortcuts.md index ffc0a39fda8..ac2705f757b 100644 --- a/docs/cli/keyboard-shortcuts.md +++ b/docs/cli/keyboard-shortcuts.md @@ -96,31 +96,31 @@ available combinations. #### App Controls -| Action | Keys | -| ----------------------------------------------------------------------------------------------------- | -------------------------- | -| Toggle detailed error information. | `F12` | -| Toggle the full TODO list. | `Ctrl + T` | -| Show IDE context details. | `Ctrl + G` | -| Toggle Markdown rendering. | `Alt + M` | -| Toggle copy mode when in alternate buffer mode. | `Ctrl + S` | -| Toggle YOLO (auto-approval) mode for tool calls. | `Ctrl + Y` | -| Cycle through approval modes: default (prompt), auto_edit (auto-approve edits), and plan (read-only). | `Shift + Tab` | -| Expand a height-constrained response to show additional lines when not in alternate buffer mode. | `Ctrl + O`
`Ctrl + S` | -| Expand or collapse a paste placeholder when cursor is over placeholder. | `Ctrl + O` | -| Toggle current background shell visibility. | `Ctrl + B` | -| Toggle background shell list. | `Ctrl + L` | -| Kill the active background shell. | `Ctrl + K` | -| Confirm selection in background shell list. | `Enter` | -| Dismiss background shell list. | `Esc` | -| Move focus from background shell to Gemini. | `Shift + Tab` | -| Move focus from background shell list to Gemini. | `Tab (no Shift)` | -| Show warning when trying to move focus away from background shell. | `Tab (no Shift)` | -| Show warning when trying to move focus away from shell input. | `Tab (no Shift)` | -| Move focus from Gemini to the active shell. | `Tab (no Shift)` | -| Move focus from the shell back to Gemini. | `Shift + Tab` | -| Clear the terminal screen and redraw the UI. | `Ctrl + L` | -| Restart the application. | `R` | -| Suspend the CLI and move it to the background. | `Ctrl + Z` | +| Action | Keys | +| ----------------------------------------------------------------------------------------------------- | ---------------- | +| Toggle detailed error information. | `F12` | +| Toggle the full TODO list. | `Ctrl + T` | +| Show IDE context details. | `Ctrl + G` | +| Toggle Markdown rendering. | `Alt + M` | +| Toggle copy mode when in alternate buffer mode. | `Ctrl + S` | +| Toggle YOLO (auto-approval) mode for tool calls. | `Ctrl + Y` | +| Cycle through approval modes: default (prompt), auto_edit (auto-approve edits), and plan (read-only). | `Shift + Tab` | +| Expand and collapse blocks of content when not in alternate buffer mode. | `Ctrl + O` | +| Expand or collapse a paste placeholder when cursor is over placeholder. | `Ctrl + O` | +| Toggle current background shell visibility. | `Ctrl + B` | +| Toggle background shell list. | `Ctrl + L` | +| Kill the active background shell. | `Ctrl + K` | +| Confirm selection in background shell list. | `Enter` | +| Dismiss background shell list. | `Esc` | +| Move focus from background shell to Gemini. | `Shift + Tab` | +| Move focus from background shell list to Gemini. | `Tab (no Shift)` | +| Show warning when trying to move focus away from background shell. | `Tab (no Shift)` | +| Show warning when trying to move focus away from shell input. | `Tab (no Shift)` | +| Move focus from Gemini to the active shell. | `Tab (no Shift)` | +| Move focus from the shell back to Gemini. | `Shift + Tab` | +| Clear the terminal screen and redraw the UI. | `Ctrl + L` | +| Restart the application. | `R` | +| Suspend the CLI and move it to the background. | `Ctrl + Z` | diff --git a/packages/cli/src/ui/keyMatchers.test.ts b/packages/cli/src/ui/keyMatchers.test.ts index 329549c08d5..7a3a0779947 100644 --- a/packages/cli/src/ui/keyMatchers.test.ts +++ b/packages/cli/src/ui/keyMatchers.test.ts @@ -344,11 +344,12 @@ describe('keyMatchers', () => { }, { command: Command.SHOW_MORE_LINES, - positive: [ + positive: [createKey('o', { ctrl: true })], + negative: [ createKey('s', { ctrl: true }), - createKey('o', { ctrl: true }), + createKey('s'), + createKey('l', { ctrl: true }), ], - negative: [createKey('s'), createKey('l', { ctrl: true })], }, // Shell commands