Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
f2b36ad
feat: Ctrl+O to expand paste placeholder
jackwotherspoon Feb 2, 2026
e7c4126
docs: add Ctrl+O shortcut for paste expansion
jackwotherspoon Feb 2, 2026
8ae4f7f
chore: consolidate hint and warnings
jackwotherspoon Feb 2, 2026
6956890
chore: review frontend comments
jackwotherspoon Feb 2, 2026
8e98c1c
Merge branch 'main' into ctrl-o-placeholder
jackwotherspoon Feb 2, 2026
a4fab49
chore: update docs
jackwotherspoon Feb 2, 2026
04bb3e5
Merge branch 'main' into ctrl-o-placeholder
jackwotherspoon Feb 2, 2026
165e5e5
Merge branch 'main' into ctrl-o-placeholder
jackwotherspoon Feb 4, 2026
062f823
chore: expand paste based on cursor position
jackwotherspoon Feb 4, 2026
cdc66bd
chore: allow right of placeholder to count
jackwotherspoon Feb 4, 2026
c19bc4d
Merge branch 'main' into ctrl-o-placeholder
jackwotherspoon Feb 4, 2026
b6acdc1
Merge branch 'main' into ctrl-o-placeholder
jackwotherspoon Feb 4, 2026
21d2fa5
chore: reviewer feedback
jackwotherspoon Feb 5, 2026
f7d89f4
Merge branch 'main' into ctrl-o-placeholder
jackwotherspoon Feb 5, 2026
2b00e8a
Merge branch 'main' into ctrl-o-placeholder
jackwotherspoon Feb 5, 2026
a92bc03
chore: lint
jackwotherspoon Feb 5, 2026
5359a6e
Merge branch 'main' into ctrl-o-placeholder
jackwotherspoon Feb 5, 2026
b263562
Merge branch 'main' into ctrl-o-placeholder
jackwotherspoon Feb 5, 2026
277b557
chore: update test
jackwotherspoon Feb 5, 2026
5bebdca
Merge branch 'main' into ctrl-o-placeholder
jackwotherspoon Feb 5, 2026
d353b4b
Merge branch 'main' into ctrl-o-placeholder
jackwotherspoon Feb 5, 2026
db6cbdd
Merge branch 'main' into ctrl-o-placeholder
jackwotherspoon Feb 6, 2026
b4bac35
chore: merge in main
jackwotherspoon Feb 9, 2026
bf0bb49
Merge branch 'main' into ctrl-o-placeholder
jackwotherspoon Feb 9, 2026
123f252
chore: remove unnecessary code
jackwotherspoon Feb 9, 2026
0a4ca26
Merge branch 'main' into ctrl-o-placeholder
jackwotherspoon Feb 9, 2026
ff597de
Merge branch 'main' into ctrl-o-placeholder
jackwotherspoon Feb 10, 2026
35e0b13
Merge branch 'main' into ctrl-o-placeholder
jackwotherspoon Feb 10, 2026
1318963
Merge branch 'main' into ctrl-o-placeholder
jackwotherspoon Feb 10, 2026
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
8 changes: 5 additions & 3 deletions docs/cli/keyboard-shortcuts.md
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,7 @@ available combinations.
| 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`<br />`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` |
Expand Down Expand Up @@ -139,6 +140,7 @@ available combinations.
single-line input, navigate backward or forward through prompt history.
- `Number keys (1-9, multi-digit)` inside selection dialogs: Jump directly to
the numbered radio option and confirm when the full number is entered.
- `Double-click` on a paste placeholder (`[Pasted Text: X lines]`) in alternate
buffer mode: Expand to view full content inline. Double-click again to
collapse.
- `Ctrl + O`: Expand or collapse paste placeholders (`[Pasted Text: X lines]`)
inline when the cursor is over the placeholder.
- `Double-click` on a paste placeholder (alternate buffer mode only): Expand to
view full content inline. Double-click again to collapse.
5 changes: 5 additions & 0 deletions packages/cli/src/config/keyBindings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,7 @@ export enum Command {
TOGGLE_YOLO = 'app.toggleYolo',
CYCLE_APPROVAL_MODE = 'app.cycleApprovalMode',
SHOW_MORE_LINES = 'app.showMoreLines',
EXPAND_PASTE = 'app.expandPaste',
FOCUS_SHELL_INPUT = 'app.focusShellInput',
UNFOCUS_SHELL_INPUT = 'app.unfocusShellInput',
CLEAR_SCREEN = 'app.clearScreen',
Expand Down Expand Up @@ -289,6 +290,7 @@ export const defaultKeyBindings: KeyBindingConfig = {
{ key: 'o', ctrl: true },
{ key: 's', 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 }],
[Command.CLEAR_SCREEN]: [{ key: 'l', ctrl: true }],
Expand Down Expand Up @@ -399,6 +401,7 @@ export const commandCategories: readonly CommandCategory[] = [
Command.TOGGLE_YOLO,
Command.CYCLE_APPROVAL_MODE,
Command.SHOW_MORE_LINES,
Command.EXPAND_PASTE,
Command.TOGGLE_BACKGROUND_SHELL,
Command.TOGGLE_BACKGROUND_SHELL_LIST,
Command.KILL_BACKGROUND_SHELL,
Expand Down Expand Up @@ -499,6 +502,8 @@ export const commandDescriptions: Readonly<Record<Command, string>> = {
'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.',
[Command.EXPAND_PASTE]:
'Expand or collapse a paste placeholder when cursor is over placeholder.',
[Command.BACKGROUND_SHELL_SELECT]:
'Confirm selection in background shell list.',
[Command.BACKGROUND_SHELL_ESCAPE]: 'Dismiss background shell list.',
Expand Down
1 change: 0 additions & 1 deletion packages/cli/src/test-utils/render.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -200,7 +200,6 @@ const mockUIActions: UIActions = {
setActiveBackgroundShellPid: vi.fn(),
setIsBackgroundShellListOpen: vi.fn(),
setAuthContext: vi.fn(),
handleWarning: vi.fn(),
handleRestart: vi.fn(),
handleNewAgentsSelect: vi.fn(),
};
Expand Down
80 changes: 45 additions & 35 deletions packages/cli/src/ui/AppContainer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -105,7 +105,7 @@ import { useShellInactivityStatus } from './hooks/useShellInactivityStatus.js';
import { useFolderTrust } from './hooks/useFolderTrust.js';
import { useIdeTrustListener } from './hooks/useIdeTrustListener.js';
import { type IdeIntegrationNudgeResult } from './IdeIntegrationNudge.js';
import { appEvents, AppEvent } from '../utils/events.js';
import { appEvents, AppEvent, TransientMessageType } from '../utils/events.js';
import { type UpdateObject } from './utils/updateCheck.js';
import { setUpdateHandler } from '../utils/handleAutoUpdate.js';
import { registerCleanup, runExitCleanup } from '../utils/cleanup.js';
Expand Down Expand Up @@ -142,6 +142,7 @@ import { LoginWithGoogleRestartDialog } from './auth/LoginWithGoogleRestartDialo
import { NewAgentsChoice } from './components/NewAgentsNotification.js';
import { isSlashCommand } from './utils/commandUtils.js';
import { useTerminalTheme } from './hooks/useTerminalTheme.js';
import { useTimedMessage } from './hooks/useTimedMessage.js';
import { isITerm2 } from './utils/terminalUtils.js';

function isToolExecuting(pendingHistoryItems: HistoryItemWithoutId[]) {
Expand Down Expand Up @@ -1286,7 +1287,11 @@ Logging in with Google... Restarting Gemini CLI to continue.
>();
const [showEscapePrompt, setShowEscapePrompt] = useState(false);
const [showIdeRestartPrompt, setShowIdeRestartPrompt] = useState(false);
const [warningMessage, setWarningMessage] = useState<string | null>(null);

const [transientMessage, showTransientMessage] = useTimedMessage<{
text: string;
type: TransientMessageType;
}>(WARNING_PROMPT_DURATION_MS);

const { isFolderTrustDialogOpen, handleFolderTrustSelect, isRestarting } =
useFolderTrust(settings, setIsTrustedFolder, historyManager.addItem);
Expand All @@ -1298,41 +1303,42 @@ Logging in with Google... Restarting Gemini CLI to continue.

useIncludeDirsTrust(config, isTrustedFolder, historyManager, setCustomDialog);

const warningTimeoutRef = useRef<NodeJS.Timeout | null>(null);
const tabFocusTimeoutRef = useRef<NodeJS.Timeout | null>(null);

const handleWarning = useCallback((message: string) => {
setWarningMessage(message);
if (warningTimeoutRef.current) {
clearTimeout(warningTimeoutRef.current);
}
warningTimeoutRef.current = setTimeout(() => {
setWarningMessage(null);
}, WARNING_PROMPT_DURATION_MS);
}, []);

// Handle timeout cleanup on unmount
useEffect(
() => () => {
if (warningTimeoutRef.current) {
clearTimeout(warningTimeoutRef.current);
}
if (tabFocusTimeoutRef.current) {
clearTimeout(tabFocusTimeoutRef.current);
}
},
[],
);

useEffect(() => {
const handleTransientMessage = (payload: {
message: string;
type: TransientMessageType;
}) => {
showTransientMessage({ text: payload.message, type: payload.type });
};

const handleSelectionWarning = () => {
showTransientMessage({
text: 'Press Ctrl-S to enter selection mode to copy text.',
type: TransientMessageType.Warning,
});
};
const handlePasteTimeout = () => {
handleWarning('Paste Timed out. Possibly due to slow connection.');
showTransientMessage({
text: 'Paste Timed out. Possibly due to slow connection.',
type: TransientMessageType.Warning,
});
};

appEvents.on(AppEvent.TransientMessage, handleTransientMessage);
appEvents.on(AppEvent.SelectionWarning, handleSelectionWarning);
appEvents.on(AppEvent.PasteTimeout, handlePasteTimeout);

return () => {
appEvents.off(AppEvent.TransientMessage, handleTransientMessage);
appEvents.off(AppEvent.SelectionWarning, handleSelectionWarning);
appEvents.off(AppEvent.PasteTimeout, handlePasteTimeout);
if (tabFocusTimeoutRef.current) {
clearTimeout(tabFocusTimeoutRef.current);
}
};
}, [handleWarning]);
}, [showTransientMessage]);

useEffect(() => {
if (ideNeedsRestart) {
Expand Down Expand Up @@ -1500,7 +1506,10 @@ Logging in with Google... Restarting Gemini CLI to continue.
const undoMessage = isITerm2()
? 'Undo has been moved to Option + Z'
: 'Undo has been moved to Alt/Option + Z or Cmd + Z';
handleWarning(undoMessage);
showTransientMessage({
text: undoMessage,
type: TransientMessageType.Warning,
});
return true;
} else if (keyMatchers[Command.SHOW_FULL_TODOS](key)) {
setShowFullTodos((prev) => !prev);
Expand Down Expand Up @@ -1540,7 +1549,10 @@ Logging in with Google... Restarting Gemini CLI to continue.
if (lastOutputTimeRef.current === capturedTime) {
setEmbeddedShellFocused(false);
} else {
handleWarning('Use Shift+Tab to unfocus');
showTransientMessage({
text: 'Use Shift+Tab to unfocus',
type: TransientMessageType.Warning,
});
}
}, 150);
return false;
Expand Down Expand Up @@ -1620,7 +1632,7 @@ Logging in with Google... Restarting Gemini CLI to continue.
setIsBackgroundShellListOpen,
lastOutputTimeRef,
tabFocusTimeoutRef,
handleWarning,
showTransientMessage,
],
);

Expand Down Expand Up @@ -1903,7 +1915,7 @@ Logging in with Google... Restarting Gemini CLI to continue.
showDebugProfiler,
customDialog,
copyModeEnabled,
warningMessage,
transientMessage,
bannerData,
bannerVisible,
terminalBackgroundColor: config.getTerminalBackground(),
Expand Down Expand Up @@ -2013,7 +2025,7 @@ Logging in with Google... Restarting Gemini CLI to continue.
apiKeyDefaultValue,
authState,
copyModeEnabled,
warningMessage,
transientMessage,
bannerData,
bannerVisible,
config,
Expand Down Expand Up @@ -2070,7 +2082,6 @@ Logging in with Google... Restarting Gemini CLI to continue.
handleApiKeyCancel,
setBannerVisible,
setShortcutsHelpVisible,
handleWarning,
setEmbeddedShellFocused,
dismissBackgroundShell,
setActiveBackgroundShellPid,
Expand Down Expand Up @@ -2147,7 +2158,6 @@ Logging in with Google... Restarting Gemini CLI to continue.
handleApiKeyCancel,
setBannerVisible,
setShortcutsHelpVisible,
handleWarning,
setEmbeddedShellFocused,
dismissBackgroundShell,
setActiveBackgroundShellPid,
Expand Down
10 changes: 5 additions & 5 deletions packages/cli/src/ui/components/BackgroundShellDisplay.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
*/

import { render } from '../../test-utils/render.js';
import { describe, it, expect, vi, beforeEach } from 'vitest';
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
import { BackgroundShellDisplay } from './BackgroundShellDisplay.js';
import { type BackgroundShell } from '../hooks/shellCommandProcessor.js';
import { ShellExecutionService } from '@google/gemini-cli-core';
Expand All @@ -20,16 +20,12 @@ const delay = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms));
const mockDismissBackgroundShell = vi.fn();
const mockSetActiveBackgroundShellPid = vi.fn();
const mockSetIsBackgroundShellListOpen = vi.fn();
const mockHandleWarning = vi.fn();
const mockSetEmbeddedShellFocused = vi.fn();

vi.mock('../contexts/UIActionsContext.js', () => ({
useUIActions: () => ({
dismissBackgroundShell: mockDismissBackgroundShell,
setActiveBackgroundShellPid: mockSetActiveBackgroundShellPid,
setIsBackgroundShellListOpen: mockSetIsBackgroundShellListOpen,
handleWarning: mockHandleWarning,
setEmbeddedShellFocused: mockSetEmbeddedShellFocused,
}),
}));

Expand Down Expand Up @@ -103,6 +99,10 @@ vi.mock('./shared/ScrollableList.js', () => ({
),
}));

afterEach(() => {
vi.restoreAllMocks();
});

const createMockKey = (overrides: Partial<Key>): Key => ({
name: '',
ctrl: false,
Expand Down
Loading
Loading