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
53 changes: 27 additions & 26 deletions docs/cli/keyboard-shortcuts.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 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` |
| 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). Plan mode is skipped when the agent is busy. | `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` |

<!-- KEYBINDINGS-AUTOGEN:END -->

Expand All @@ -138,7 +138,8 @@ available combinations.
details when no completion/search interaction is active. The selected mode is
remembered for future sessions. Full UI remains the default on first run, and
single `Tab` keeps its existing completion/focus behavior.
- `Shift + Tab` (while typing in the prompt): Cycle approval modes.
- `Shift + Tab` (while typing in the prompt): Cycle approval modes: default,
auto-edit, and plan (skipped when agent is busy).
- `\` (at end of a line) + `Enter`: Insert a newline without leaving single-line
mode.
- `Esc` pressed twice quickly: Clear the input prompt if it is not empty,
Expand Down
4 changes: 4 additions & 0 deletions docs/cli/plan-mode.md
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,10 @@ You can enter Plan Mode in three ways:

1. **Keyboard Shortcut:** Press `Shift+Tab` to cycle through approval modes
(`Default` -> `Auto-Edit` -> `Plan`).

> **Note:** Plan Mode is automatically removed from the rotation when the
> agent is actively processing or showing confirmation dialogs.

2. **Command:** Type `/plan` in the input box.
3. **Natural Language:** Ask the agent to "start a plan for...". The agent will
then call the [`enter_plan_mode`] tool to switch modes.
Expand Down
2 changes: 1 addition & 1 deletion packages/cli/src/config/keyBindings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -496,7 +496,7 @@ export const commandDescriptions: Readonly<Record<Command, string>> = {
[Command.TOGGLE_COPY_MODE]: 'Toggle copy mode when in alternate buffer mode.',
[Command.TOGGLE_YOLO]: 'Toggle YOLO (auto-approval) mode for tool calls.',
[Command.CYCLE_APPROVAL_MODE]:
'Cycle through approval modes: default (prompt), auto_edit (auto-approve edits), and plan (read-only).',
'Cycle through approval modes: default (prompt), auto_edit (auto-approve edits), and plan (read-only). Plan mode is skipped when the agent is busy.',
[Command.SHOW_MORE_LINES]:
'Expand and collapse blocks of content when not in alternate buffer mode.',
[Command.EXPAND_PASTE]:
Expand Down
1 change: 1 addition & 0 deletions packages/cli/src/test-utils/render.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -155,6 +155,7 @@ const baseMockUiState = {
currentModel: 'gemini-pro',
terminalBackgroundColor: 'black',
cleanUiDetailsVisible: false,
allowPlanMode: true,
activePtyId: undefined,
backgroundShells: new Map(),
backgroundShellHeight: 0,
Expand Down
95 changes: 95 additions & 0 deletions packages/cli/src/ui/AppContainer.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,7 @@ import ansiEscapes from 'ansi-escapes';
import { mergeSettings, type LoadedSettings } from '../config/settings.js';
import type { InitializationResult } from '../core/initializer.js';
import { useQuotaAndFallback } from './hooks/useQuotaAndFallback.js';
import { StreamingState } from './types.js';
import { UIStateContext, type UIState } from './contexts/UIStateContext.js';
import {
UIActionsContext,
Expand Down Expand Up @@ -2979,4 +2980,98 @@ describe('AppContainer State Management', () => {
},
);
});

describe('Plan Mode Availability', () => {
it('should allow plan mode when enabled and idle', async () => {
vi.spyOn(mockConfig, 'isPlanEnabled').mockReturnValue(true);
mockedUseGeminiStream.mockReturnValue({

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: please parameterize these tests as well

...DEFAULT_GEMINI_STREAM_MOCK,
pendingHistoryItems: [],
});

let unmount: () => void;
await act(async () => {
const result = renderAppContainer();
unmount = result.unmount;
});

await waitFor(() => {
expect(capturedUIState).toBeTruthy();
expect(capturedUIState.allowPlanMode).toBe(true);
});
unmount!();
});

it('should NOT allow plan mode when disabled in config', async () => {
vi.spyOn(mockConfig, 'isPlanEnabled').mockReturnValue(false);
mockedUseGeminiStream.mockReturnValue({
...DEFAULT_GEMINI_STREAM_MOCK,
pendingHistoryItems: [],
});

let unmount: () => void;
await act(async () => {
const result = renderAppContainer();
unmount = result.unmount;
});

await waitFor(() => {
expect(capturedUIState).toBeTruthy();
expect(capturedUIState.allowPlanMode).toBe(false);
});
unmount!();
});

it('should NOT allow plan mode when streaming', async () => {
vi.spyOn(mockConfig, 'isPlanEnabled').mockReturnValue(true);
mockedUseGeminiStream.mockReturnValue({
...DEFAULT_GEMINI_STREAM_MOCK,
streamingState: StreamingState.Responding,
pendingHistoryItems: [],
});

let unmount: () => void;
await act(async () => {
const result = renderAppContainer();
unmount = result.unmount;
});

await waitFor(() => {
expect(capturedUIState).toBeTruthy();
expect(capturedUIState.allowPlanMode).toBe(false);
});
unmount!();
});

it('should NOT allow plan mode when a tool is awaiting confirmation', async () => {
vi.spyOn(mockConfig, 'isPlanEnabled').mockReturnValue(true);
mockedUseGeminiStream.mockReturnValue({
...DEFAULT_GEMINI_STREAM_MOCK,
streamingState: StreamingState.Idle,
pendingHistoryItems: [
{
type: 'tool_group',
tools: [
{
name: 'test_tool',
status: CoreToolCallStatus.AwaitingApproval,
},
],
},
],
});

let unmount: () => void;
await act(async () => {
const result = renderAppContainer();
unmount = result.unmount;
});

await waitFor(() => {
expect(capturedUIState).toBeTruthy();
expect(capturedUIState.allowPlanMode).toBe(false);
});
unmount!();
});
});
});
23 changes: 15 additions & 8 deletions packages/cli/src/ui/AppContainer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -1087,14 +1087,6 @@ Logging in with Google... Restarting Gemini CLI to continue.
],
);

// Auto-accept indicator
const showApprovalModeIndicator = useApprovalModeIndicator({
config,
addItem: historyManager.addItem,
onApprovalModeChange: handleApprovalModeChangeWithUiReveal,
isActive: !embeddedShellFocused,
});

const { isMcpReady } = useMcpStatus(config);

const {
Expand Down Expand Up @@ -1897,6 +1889,19 @@ Logging in with Google... Restarting Gemini CLI to continue.
!!validationRequest ||
!!customDialog;

const allowPlanMode =
config.isPlanEnabled() &&
streamingState === StreamingState.Idle &&
!hasPendingActionRequired;

const showApprovalModeIndicator = useApprovalModeIndicator({
config,
addItem: historyManager.addItem,
onApprovalModeChange: handleApprovalModeChangeWithUiReveal,
isActive: !embeddedShellFocused,
allowPlanMode,
});

const isPassiveShortcutsHelpState =
isInputActive &&
streamingState === StreamingState.Idle &&
Expand Down Expand Up @@ -2031,6 +2036,7 @@ Logging in with Google... Restarting Gemini CLI to continue.
messageQueue,
queueErrorMessage,
showApprovalModeIndicator,
allowPlanMode,
currentModel,
quota: {
userTier,
Expand Down Expand Up @@ -2145,6 +2151,7 @@ Logging in with Google... Restarting Gemini CLI to continue.
messageQueue,
queueErrorMessage,
showApprovalModeIndicator,
allowPlanMode,
userTier,
quotaStats,
proQuotaRequest,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ describe('ApprovalModeIndicator', () => {
const { lastFrame } = render(
<ApprovalModeIndicator
approvalMode={ApprovalMode.AUTO_EDIT}
isPlanEnabled={true}
allowPlanMode={true}
/>,
);
expect(lastFrame()).toMatchSnapshot();
Expand Down Expand Up @@ -52,7 +52,7 @@ describe('ApprovalModeIndicator', () => {
const { lastFrame } = render(
<ApprovalModeIndicator
approvalMode={ApprovalMode.DEFAULT}
isPlanEnabled={true}
allowPlanMode={true}
/>,
);
expect(lastFrame()).toMatchSnapshot();
Expand Down
6 changes: 3 additions & 3 deletions packages/cli/src/ui/components/ApprovalModeIndicator.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import { ApprovalMode } from '@google/gemini-cli-core';

interface ApprovalModeIndicatorProps {
approvalMode: ApprovalMode;
isPlanEnabled?: boolean;
allowPlanMode?: boolean;
}

export const APPROVAL_MODE_TEXT = {
Expand All @@ -26,7 +26,7 @@ export const APPROVAL_MODE_TEXT = {

export const ApprovalModeIndicator: React.FC<ApprovalModeIndicatorProps> = ({
approvalMode,
isPlanEnabled,
allowPlanMode,
}) => {
let textColor = '';
let textContent = '';
Expand All @@ -36,7 +36,7 @@ export const ApprovalModeIndicator: React.FC<ApprovalModeIndicatorProps> = ({
case ApprovalMode.AUTO_EDIT:
textColor = theme.status.warning;
textContent = APPROVAL_MODE_TEXT.AUTO_EDIT;
subText = isPlanEnabled
subText = allowPlanMode
? APPROVAL_MODE_TEXT.HINT_SWITCH_TO_PLAN_MODE
: APPROVAL_MODE_TEXT.HINT_SWITCH_TO_MANUAL_MODE;
break;
Expand Down
2 changes: 1 addition & 1 deletion packages/cli/src/ui/components/Composer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -346,7 +346,7 @@ export const Composer = ({ isFocused = true }: { isFocused?: boolean }) => {
{showApprovalIndicator && (
<ApprovalModeIndicator
approvalMode={showApprovalModeIndicator}
isPlanEnabled={config.isPlanEnabled()}
allowPlanMode={uiState.allowPlanMode}
/>
)}
{!showLoadingIndicator && (
Expand Down
1 change: 1 addition & 0 deletions packages/cli/src/ui/contexts/UIStateContext.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,7 @@ export interface UIState {
messageQueue: string[];
queueErrorMessage: string | null;
showApprovalModeIndicator: ApprovalMode;
allowPlanMode: boolean;
// Quota-related state
quota: QuotaState;
currentModel: string;
Expand Down
Loading
Loading