mirror of
https://github.com/google-gemini/gemini-cli.git
synced 2026-04-24 12:04:56 -07:00
feat(cli): remove Plan Mode from rotation when actively working (#19262)
This commit is contained in:
@@ -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]:
|
||||
|
||||
@@ -155,6 +155,7 @@ const baseMockUiState = {
|
||||
currentModel: 'gemini-pro',
|
||||
terminalBackgroundColor: 'black',
|
||||
cleanUiDetailsVisible: false,
|
||||
allowPlanMode: true,
|
||||
activePtyId: undefined,
|
||||
backgroundShells: new Map(),
|
||||
backgroundShellHeight: 0,
|
||||
|
||||
@@ -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,
|
||||
@@ -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({
|
||||
...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!();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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 {
|
||||
@@ -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 &&
|
||||
@@ -2031,6 +2036,7 @@ Logging in with Google... Restarting Gemini CLI to continue.
|
||||
messageQueue,
|
||||
queueErrorMessage,
|
||||
showApprovalModeIndicator,
|
||||
allowPlanMode,
|
||||
currentModel,
|
||||
quota: {
|
||||
userTier,
|
||||
@@ -2145,6 +2151,7 @@ Logging in with Google... Restarting Gemini CLI to continue.
|
||||
messageQueue,
|
||||
queueErrorMessage,
|
||||
showApprovalModeIndicator,
|
||||
allowPlanMode,
|
||||
userTier,
|
||||
quotaStats,
|
||||
proQuotaRequest,
|
||||
|
||||
@@ -21,7 +21,7 @@ describe('ApprovalModeIndicator', () => {
|
||||
const { lastFrame } = render(
|
||||
<ApprovalModeIndicator
|
||||
approvalMode={ApprovalMode.AUTO_EDIT}
|
||||
isPlanEnabled={true}
|
||||
allowPlanMode={true}
|
||||
/>,
|
||||
);
|
||||
expect(lastFrame()).toMatchSnapshot();
|
||||
@@ -52,7 +52,7 @@ describe('ApprovalModeIndicator', () => {
|
||||
const { lastFrame } = render(
|
||||
<ApprovalModeIndicator
|
||||
approvalMode={ApprovalMode.DEFAULT}
|
||||
isPlanEnabled={true}
|
||||
allowPlanMode={true}
|
||||
/>,
|
||||
);
|
||||
expect(lastFrame()).toMatchSnapshot();
|
||||
|
||||
@@ -11,7 +11,7 @@ import { ApprovalMode } from '@google/gemini-cli-core';
|
||||
|
||||
interface ApprovalModeIndicatorProps {
|
||||
approvalMode: ApprovalMode;
|
||||
isPlanEnabled?: boolean;
|
||||
allowPlanMode?: boolean;
|
||||
}
|
||||
|
||||
export const APPROVAL_MODE_TEXT = {
|
||||
@@ -26,7 +26,7 @@ export const APPROVAL_MODE_TEXT = {
|
||||
|
||||
export const ApprovalModeIndicator: React.FC<ApprovalModeIndicatorProps> = ({
|
||||
approvalMode,
|
||||
isPlanEnabled,
|
||||
allowPlanMode,
|
||||
}) => {
|
||||
let textColor = '';
|
||||
let textContent = '';
|
||||
@@ -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;
|
||||
|
||||
@@ -346,7 +346,7 @@ export const Composer = ({ isFocused = true }: { isFocused?: boolean }) => {
|
||||
{showApprovalIndicator && (
|
||||
<ApprovalModeIndicator
|
||||
approvalMode={showApprovalModeIndicator}
|
||||
isPlanEnabled={config.isPlanEnabled()}
|
||||
allowPlanMode={uiState.allowPlanMode}
|
||||
/>
|
||||
)}
|
||||
{!showLoadingIndicator && (
|
||||
|
||||
@@ -130,6 +130,7 @@ export interface UIState {
|
||||
messageQueue: string[];
|
||||
queueErrorMessage: string | null;
|
||||
showApprovalModeIndicator: ApprovalMode;
|
||||
allowPlanMode: boolean;
|
||||
// Quota-related state
|
||||
quota: QuotaState;
|
||||
currentModel: string;
|
||||
|
||||
@@ -236,41 +236,6 @@ describe('useApprovalModeIndicator', () => {
|
||||
expect(result.current).toBe(ApprovalMode.AUTO_EDIT);
|
||||
});
|
||||
|
||||
it('should cycle through DEFAULT -> AUTO_EDIT -> PLAN -> DEFAULT when plan is enabled', () => {
|
||||
mockConfigInstance.getApprovalMode.mockReturnValue(ApprovalMode.DEFAULT);
|
||||
mockConfigInstance.isPlanEnabled.mockReturnValue(true);
|
||||
renderHook(() =>
|
||||
useApprovalModeIndicator({
|
||||
config: mockConfigInstance as unknown as ActualConfigType,
|
||||
addItem: vi.fn(),
|
||||
}),
|
||||
);
|
||||
|
||||
// DEFAULT -> AUTO_EDIT
|
||||
act(() => {
|
||||
capturedUseKeypressHandler({ name: 'tab', shift: true } as Key);
|
||||
});
|
||||
expect(mockConfigInstance.setApprovalMode).toHaveBeenCalledWith(
|
||||
ApprovalMode.AUTO_EDIT,
|
||||
);
|
||||
|
||||
// AUTO_EDIT -> PLAN
|
||||
act(() => {
|
||||
capturedUseKeypressHandler({ name: 'tab', shift: true } as Key);
|
||||
});
|
||||
expect(mockConfigInstance.setApprovalMode).toHaveBeenCalledWith(
|
||||
ApprovalMode.PLAN,
|
||||
);
|
||||
|
||||
// PLAN -> DEFAULT
|
||||
act(() => {
|
||||
capturedUseKeypressHandler({ name: 'tab', shift: true } as Key);
|
||||
});
|
||||
expect(mockConfigInstance.setApprovalMode).toHaveBeenCalledWith(
|
||||
ApprovalMode.DEFAULT,
|
||||
);
|
||||
});
|
||||
|
||||
it('should not toggle if only one key or other keys combinations are pressed', () => {
|
||||
mockConfigInstance.getApprovalMode.mockReturnValue(ApprovalMode.DEFAULT);
|
||||
renderHook(() =>
|
||||
@@ -729,4 +694,44 @@ describe('useApprovalModeIndicator', () => {
|
||||
ApprovalMode.AUTO_EDIT,
|
||||
);
|
||||
});
|
||||
|
||||
it('should cycle to PLAN when allowPlanMode is true', () => {
|
||||
mockConfigInstance.getApprovalMode.mockReturnValue(ApprovalMode.AUTO_EDIT);
|
||||
|
||||
renderHook(() =>
|
||||
useApprovalModeIndicator({
|
||||
config: mockConfigInstance as unknown as ActualConfigType,
|
||||
addItem: vi.fn(),
|
||||
allowPlanMode: true,
|
||||
}),
|
||||
);
|
||||
|
||||
// AUTO_EDIT -> PLAN
|
||||
act(() => {
|
||||
capturedUseKeypressHandler({ name: 'tab', shift: true } as Key);
|
||||
});
|
||||
expect(mockConfigInstance.setApprovalMode).toHaveBeenCalledWith(
|
||||
ApprovalMode.PLAN,
|
||||
);
|
||||
});
|
||||
|
||||
it('should cycle to DEFAULT when allowPlanMode is false', () => {
|
||||
mockConfigInstance.getApprovalMode.mockReturnValue(ApprovalMode.AUTO_EDIT);
|
||||
|
||||
renderHook(() =>
|
||||
useApprovalModeIndicator({
|
||||
config: mockConfigInstance as unknown as ActualConfigType,
|
||||
addItem: vi.fn(),
|
||||
allowPlanMode: false,
|
||||
}),
|
||||
);
|
||||
|
||||
// AUTO_EDIT -> DEFAULT
|
||||
act(() => {
|
||||
capturedUseKeypressHandler({ name: 'tab', shift: true } as Key);
|
||||
});
|
||||
expect(mockConfigInstance.setApprovalMode).toHaveBeenCalledWith(
|
||||
ApprovalMode.DEFAULT,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -20,6 +20,7 @@ export interface UseApprovalModeIndicatorArgs {
|
||||
addItem?: (item: HistoryItemWithoutId, timestamp: number) => void;
|
||||
onApprovalModeChange?: (mode: ApprovalMode) => void;
|
||||
isActive?: boolean;
|
||||
allowPlanMode?: boolean;
|
||||
}
|
||||
|
||||
export function useApprovalModeIndicator({
|
||||
@@ -27,6 +28,7 @@ export function useApprovalModeIndicator({
|
||||
addItem,
|
||||
onApprovalModeChange,
|
||||
isActive = true,
|
||||
allowPlanMode = false,
|
||||
}: UseApprovalModeIndicatorArgs): ApprovalMode {
|
||||
const currentConfigValue = config.getApprovalMode();
|
||||
const [showApprovalMode, setApprovalMode] = useState(currentConfigValue);
|
||||
@@ -75,7 +77,7 @@ export function useApprovalModeIndicator({
|
||||
nextApprovalMode = ApprovalMode.AUTO_EDIT;
|
||||
break;
|
||||
case ApprovalMode.AUTO_EDIT:
|
||||
nextApprovalMode = config.isPlanEnabled()
|
||||
nextApprovalMode = allowPlanMode
|
||||
? ApprovalMode.PLAN
|
||||
: ApprovalMode.DEFAULT;
|
||||
break;
|
||||
|
||||
Reference in New Issue
Block a user