diff --git a/packages/cli/src/test-utils/render.tsx b/packages/cli/src/test-utils/render.tsx
index 5f5ae4b8dc..00586c7081 100644
--- a/packages/cli/src/test-utils/render.tsx
+++ b/packages/cli/src/test-utils/render.tsx
@@ -602,6 +602,7 @@ const mockUIActions: UIActions = {
getPreferredEditor: vi.fn(),
clearAccountSuspension: vi.fn(),
setVoiceModeEnabled: vi.fn(),
+ cycleApprovalMode: vi.fn(),
};
import { type TextBuffer } from '../ui/components/shared/text-buffer.js';
diff --git a/packages/cli/src/ui/AppContainer.tsx b/packages/cli/src/ui/AppContainer.tsx
index 313f377b02..b167c0b64e 100644
--- a/packages/cli/src/ui/AppContainer.tsx
+++ b/packages/cli/src/ui/AppContainer.tsx
@@ -2296,13 +2296,14 @@ Logging in with Google... Restarting Gemini CLI to continue.
streamingState === StreamingState.Idle &&
!hasPendingActionRequired;
- const showApprovalModeIndicator = useApprovalModeIndicator({
- config,
- addItem: historyManager.addItem,
- onApprovalModeChange: handleApprovalModeChangeWithUiReveal,
- isActive: !embeddedShellFocused,
- allowPlanMode,
- });
+ const { approvalMode: showApprovalModeIndicator, cycleApprovalMode } =
+ useApprovalModeIndicator({
+ config,
+ addItem: historyManager.addItem,
+ onApprovalModeChange: handleApprovalModeChangeWithUiReveal,
+ isActive: !embeddedShellFocused,
+ allowPlanMode,
+ });
useRunEventNotifications({
notificationsEnabled,
@@ -2789,6 +2790,7 @@ Logging in with Google... Restarting Gemini CLI to continue.
setVoiceModeEnabled: (value: boolean) => {
setVoiceModeEnabled(value);
},
+ cycleApprovalMode,
}),
[
handleThemeSelect,
@@ -2848,6 +2850,7 @@ Logging in with Google... Restarting Gemini CLI to continue.
historyManager,
getPreferredEditor,
setVoiceModeEnabled,
+ cycleApprovalMode,
],
);
diff --git a/packages/cli/src/ui/components/ApprovalModeIndicator.test.tsx b/packages/cli/src/ui/components/ApprovalModeIndicator.test.tsx
index 1b2decbe16..c03ebb5092 100644
--- a/packages/cli/src/ui/components/ApprovalModeIndicator.test.tsx
+++ b/packages/cli/src/ui/components/ApprovalModeIndicator.test.tsx
@@ -4,21 +4,26 @@
* SPDX-License-Identifier: Apache-2.0
*/
-import { render } from '../../test-utils/render.js';
+import { renderWithProviders } from '../../test-utils/render.js';
import { ApprovalModeIndicator } from './ApprovalModeIndicator.js';
-import { describe, it, expect } from 'vitest';
+import { describe, it, expect, vi } from 'vitest';
import { ApprovalMode } from '@google/gemini-cli-core';
+import { useMouseClick } from '../hooks/useMouseClick.js';
+
+vi.mock('../hooks/useMouseClick.js', () => ({
+ useMouseClick: vi.fn(),
+}));
describe('ApprovalModeIndicator', () => {
it('renders correctly for AUTO_EDIT mode', async () => {
- const { lastFrame } = await render(
+ const { lastFrame } = await renderWithProviders(
,
);
expect(lastFrame()).toMatchSnapshot();
});
it('renders correctly for AUTO_EDIT mode with plan enabled', async () => {
- const { lastFrame } = await render(
+ const { lastFrame } = await renderWithProviders(
{
});
it('renders correctly for PLAN mode', async () => {
- const { lastFrame } = await render(
+ const { lastFrame } = await renderWithProviders(
,
);
expect(lastFrame()).toMatchSnapshot();
});
it('renders correctly for YOLO mode', async () => {
- const { lastFrame } = await render(
+ const { lastFrame } = await renderWithProviders(
,
);
expect(lastFrame()).toMatchSnapshot();
});
it('renders correctly for DEFAULT mode', async () => {
- const { lastFrame } = await render(
+ const { lastFrame } = await renderWithProviders(
,
);
expect(lastFrame()).toMatchSnapshot();
});
it('renders correctly for DEFAULT mode with plan enabled', async () => {
- const { lastFrame } = await render(
+ const { lastFrame } = await renderWithProviders(
{
);
expect(lastFrame()).toMatchSnapshot();
});
+
+ it('renders correctly when mouse mode is enabled', async () => {
+ const { lastFrame } = await renderWithProviders(
+ ,
+ {
+ uiState: { mouseMode: true },
+ },
+ );
+ expect(lastFrame()).toContain('click or Shift+Tab');
+ });
+
+ it('calls cycleApprovalMode when clicked', async () => {
+ const cycleApprovalMode = vi.fn();
+ let clickHandler: () => void = () => {};
+ vi.mocked(useMouseClick).mockImplementation((_ref, handler) => {
+ clickHandler = handler as () => void;
+ });
+
+ await renderWithProviders(
+ ,
+ {
+ uiActions: { cycleApprovalMode },
+ },
+ );
+
+ clickHandler();
+ expect(cycleApprovalMode).toHaveBeenCalled();
+ });
});
diff --git a/packages/cli/src/ui/components/ApprovalModeIndicator.tsx b/packages/cli/src/ui/components/ApprovalModeIndicator.tsx
index 7e8f388c82..3309a1b5af 100644
--- a/packages/cli/src/ui/components/ApprovalModeIndicator.tsx
+++ b/packages/cli/src/ui/components/ApprovalModeIndicator.tsx
@@ -4,27 +4,40 @@
* SPDX-License-Identifier: Apache-2.0
*/
-import type React from 'react';
+import type { FC } from 'react';
+import { useRef } from 'react';
import { Box, Text } from 'ink';
import { theme } from '../semantic-colors.js';
import { ApprovalMode } from '@google/gemini-cli-core';
import { formatCommand } from '../key/keybindingUtils.js';
import { Command } from '../key/keyBindings.js';
+import { useMouseClick } from '../hooks/useMouseClick.js';
+import { useUIActions } from '../contexts/UIActionsContext.js';
+import { useUIState } from '../contexts/UIStateContext.js';
interface ApprovalModeIndicatorProps {
approvalMode: ApprovalMode;
allowPlanMode?: boolean;
}
-export const ApprovalModeIndicator: React.FC = ({
+export const ApprovalModeIndicator: FC = ({
approvalMode,
allowPlanMode,
}) => {
+ const { mouseMode } = useUIState();
+ const { cycleApprovalMode } = useUIActions();
+ const boxRef = useRef(null);
+
+ useMouseClick(boxRef, () => {
+ cycleApprovalMode();
+ });
+
let textColor = '';
let textContent = '';
let subText = '';
const cycleHint = formatCommand(Command.CYCLE_APPROVAL_MODE);
+ const clickHint = mouseMode ? 'click or ' : '';
const yoloHint = formatCommand(Command.TOGGLE_YOLO);
switch (approvalMode) {
@@ -32,13 +45,13 @@ export const ApprovalModeIndicator: React.FC = ({
textColor = theme.status.warning;
textContent = 'auto-accept edits';
subText = allowPlanMode
- ? `${cycleHint} to plan`
- : `${cycleHint} to manual`;
+ ? `${clickHint}${cycleHint} to plan`
+ : `${clickHint}${cycleHint} to manual`;
break;
case ApprovalMode.PLAN:
textColor = theme.status.success;
textContent = 'plan';
- subText = `${cycleHint} to manual`;
+ subText = `${clickHint}${cycleHint} to manual`;
break;
case ApprovalMode.YOLO:
textColor = theme.status.error;
@@ -49,12 +62,12 @@ export const ApprovalModeIndicator: React.FC = ({
default:
textColor = theme.text.accent;
textContent = '';
- subText = `${cycleHint} to accept edits`;
+ subText = `${clickHint}${cycleHint} to accept edits`;
break;
}
return (
-
+
{textContent ? textContent : null}
{subText ? (
diff --git a/packages/cli/src/ui/contexts/UIActionsContext.tsx b/packages/cli/src/ui/contexts/UIActionsContext.tsx
index cb89758300..540efd574f 100644
--- a/packages/cli/src/ui/contexts/UIActionsContext.tsx
+++ b/packages/cli/src/ui/contexts/UIActionsContext.tsx
@@ -97,6 +97,7 @@ export interface UIActions {
getPreferredEditor: () => EditorType | undefined;
clearAccountSuspension: () => void;
setVoiceModeEnabled: (value: boolean) => void;
+ cycleApprovalMode: () => void;
}
export const UIActionsContext = createContext(null);
diff --git a/packages/cli/src/ui/hooks/useApprovalModeIndicator.test.ts b/packages/cli/src/ui/hooks/useApprovalModeIndicator.test.ts
index 9771d10d83..c5c24fb5f3 100644
--- a/packages/cli/src/ui/hooks/useApprovalModeIndicator.test.ts
+++ b/packages/cli/src/ui/hooks/useApprovalModeIndicator.test.ts
@@ -146,7 +146,7 @@ describe('useApprovalModeIndicator', () => {
addItem: vi.fn(),
}),
);
- expect(result.current).toBe(ApprovalMode.AUTO_EDIT);
+ expect(result.current.approvalMode).toBe(ApprovalMode.AUTO_EDIT);
expect(mockConfigInstance.getApprovalMode).toHaveBeenCalledTimes(1);
});
@@ -158,7 +158,7 @@ describe('useApprovalModeIndicator', () => {
addItem: vi.fn(),
}),
);
- expect(result.current).toBe(ApprovalMode.DEFAULT);
+ expect(result.current.approvalMode).toBe(ApprovalMode.DEFAULT);
expect(mockConfigInstance.getApprovalMode).toHaveBeenCalledTimes(1);
});
@@ -170,7 +170,7 @@ describe('useApprovalModeIndicator', () => {
addItem: vi.fn(),
}),
);
- expect(result.current).toBe(ApprovalMode.YOLO);
+ expect(result.current.approvalMode).toBe(ApprovalMode.YOLO);
expect(mockConfigInstance.getApprovalMode).toHaveBeenCalledTimes(1);
});
@@ -182,7 +182,7 @@ describe('useApprovalModeIndicator', () => {
addItem: vi.fn(),
}),
);
- expect(result.current).toBe(ApprovalMode.DEFAULT);
+ expect(result.current.approvalMode).toBe(ApprovalMode.DEFAULT);
// Shift+Tab cycles to AUTO_EDIT
act(() => {
@@ -194,7 +194,7 @@ describe('useApprovalModeIndicator', () => {
expect(mockConfigInstance.setApprovalMode).toHaveBeenCalledWith(
ApprovalMode.AUTO_EDIT,
);
- expect(result.current).toBe(ApprovalMode.AUTO_EDIT);
+ expect(result.current.approvalMode).toBe(ApprovalMode.AUTO_EDIT);
act(() => {
capturedUseKeypressHandler({ name: 'y', ctrl: true } as Key);
@@ -202,7 +202,7 @@ describe('useApprovalModeIndicator', () => {
expect(mockConfigInstance.setApprovalMode).toHaveBeenCalledWith(
ApprovalMode.YOLO,
);
- expect(result.current).toBe(ApprovalMode.YOLO);
+ expect(result.current.approvalMode).toBe(ApprovalMode.YOLO);
// Shift+Tab cycles back to AUTO_EDIT (from YOLO)
act(() => {
@@ -214,7 +214,7 @@ describe('useApprovalModeIndicator', () => {
expect(mockConfigInstance.setApprovalMode).toHaveBeenCalledWith(
ApprovalMode.AUTO_EDIT,
);
- expect(result.current).toBe(ApprovalMode.AUTO_EDIT);
+ expect(result.current.approvalMode).toBe(ApprovalMode.AUTO_EDIT);
// Ctrl+Y toggles YOLO
act(() => {
@@ -223,7 +223,7 @@ describe('useApprovalModeIndicator', () => {
expect(mockConfigInstance.setApprovalMode).toHaveBeenCalledWith(
ApprovalMode.YOLO,
);
- expect(result.current).toBe(ApprovalMode.YOLO);
+ expect(result.current.approvalMode).toBe(ApprovalMode.YOLO);
// Shift+Tab from YOLO jumps to AUTO_EDIT
act(() => {
@@ -235,7 +235,7 @@ describe('useApprovalModeIndicator', () => {
expect(mockConfigInstance.setApprovalMode).toHaveBeenCalledWith(
ApprovalMode.AUTO_EDIT,
);
- expect(result.current).toBe(ApprovalMode.AUTO_EDIT);
+ expect(result.current.approvalMode).toBe(ApprovalMode.AUTO_EDIT);
});
it('should not toggle if only one key or other keys combinations are pressed', async () => {
@@ -309,7 +309,7 @@ describe('useApprovalModeIndicator', () => {
},
},
);
- expect(result.current).toBe(ApprovalMode.DEFAULT);
+ expect(result.current.approvalMode).toBe(ApprovalMode.DEFAULT);
mockConfigInstance.getApprovalMode.mockReturnValue(ApprovalMode.AUTO_EDIT);
@@ -317,7 +317,7 @@ describe('useApprovalModeIndicator', () => {
config: mockConfigInstance as unknown as ActualConfigType,
addItem: vi.fn(),
});
- expect(result.current).toBe(ApprovalMode.AUTO_EDIT);
+ expect(result.current.approvalMode).toBe(ApprovalMode.AUTO_EDIT);
expect(mockConfigInstance.getApprovalMode).toHaveBeenCalledTimes(3);
});
@@ -341,7 +341,7 @@ describe('useApprovalModeIndicator', () => {
}),
);
- expect(result.current).toBe(ApprovalMode.DEFAULT);
+ expect(result.current.approvalMode).toBe(ApprovalMode.DEFAULT);
act(() => {
capturedUseKeypressHandler({ name: 'y', ctrl: true } as Key);
@@ -371,7 +371,7 @@ describe('useApprovalModeIndicator', () => {
}),
);
- expect(result.current).toBe(ApprovalMode.DEFAULT);
+ expect(result.current.approvalMode).toBe(ApprovalMode.DEFAULT);
act(() => {
capturedUseKeypressHandler({
@@ -504,7 +504,7 @@ describe('useApprovalModeIndicator', () => {
}),
);
- expect(result.current).toBe(ApprovalMode.DEFAULT);
+ expect(result.current.approvalMode).toBe(ApprovalMode.DEFAULT);
act(() => {
capturedUseKeypressHandler({ name: 'y', ctrl: true } as Key);
@@ -521,7 +521,7 @@ describe('useApprovalModeIndicator', () => {
expect.any(Number),
);
// The mode should not change
- expect(result.current).toBe(ApprovalMode.DEFAULT);
+ expect(result.current.approvalMode).toBe(ApprovalMode.DEFAULT);
});
it('should show admin error message when YOLO mode is disabled by admin', async () => {
diff --git a/packages/cli/src/ui/hooks/useApprovalModeIndicator.ts b/packages/cli/src/ui/hooks/useApprovalModeIndicator.ts
index 1dd6c6468e..70d249819c 100644
--- a/packages/cli/src/ui/hooks/useApprovalModeIndicator.ts
+++ b/packages/cli/src/ui/hooks/useApprovalModeIndicator.ts
@@ -4,7 +4,7 @@
* SPDX-License-Identifier: Apache-2.0
*/
-import { useState, useEffect } from 'react';
+import { useState, useEffect, useCallback } from 'react';
import {
ApprovalMode,
type Config,
@@ -23,13 +23,19 @@ export interface UseApprovalModeIndicatorArgs {
allowPlanMode?: boolean;
}
+export interface UseApprovalModeIndicatorResult {
+ approvalMode: ApprovalMode;
+ cycleApprovalMode: () => void;
+ toggleYolo: () => void;
+}
+
export function useApprovalModeIndicator({
config,
addItem,
onApprovalModeChange,
isActive = true,
allowPlanMode = false,
-}: UseApprovalModeIndicatorArgs): ApprovalMode {
+}: UseApprovalModeIndicatorArgs): UseApprovalModeIndicatorResult {
const keyMatchers = useKeyMatchers();
const currentConfigValue = config.getApprovalMode();
const [showApprovalMode, setApprovalMode] = useState(currentConfigValue);
@@ -38,84 +44,107 @@ export function useApprovalModeIndicator({
setApprovalMode(currentConfigValue);
}, [currentConfigValue]);
- useKeypress(
- (key) => {
- let nextApprovalMode: ApprovalMode | undefined;
-
- if (keyMatchers[Command.TOGGLE_YOLO](key)) {
- if (
- config.isYoloModeDisabled() &&
- config.getApprovalMode() !== ApprovalMode.YOLO
- ) {
- if (addItem) {
- let text =
- 'You cannot enter YOLO mode since it is disabled in your settings.';
- const adminSettings = config.getRemoteAdminSettings();
- const hasSettings =
- adminSettings && Object.keys(adminSettings).length > 0;
- if (hasSettings && !adminSettings.strictModeDisabled) {
- text = getAdminErrorMessage('YOLO mode', config);
- }
-
- addItem(
- {
- type: MessageType.WARNING,
- text,
- },
- Date.now(),
- );
- }
- return;
+ const toggleYolo = useCallback(() => {
+ if (
+ config.isYoloModeDisabled() &&
+ config.getApprovalMode() !== ApprovalMode.YOLO
+ ) {
+ if (addItem) {
+ let text =
+ 'You cannot enter YOLO mode since it is disabled in your settings.';
+ const adminSettings = config.getRemoteAdminSettings();
+ const hasSettings =
+ adminSettings && Object.keys(adminSettings).length > 0;
+ if (hasSettings && !adminSettings.strictModeDisabled) {
+ text = getAdminErrorMessage('YOLO mode', config);
}
- nextApprovalMode =
- config.getApprovalMode() === ApprovalMode.YOLO
- ? ApprovalMode.DEFAULT
- : ApprovalMode.YOLO;
- } else if (keyMatchers[Command.CYCLE_APPROVAL_MODE](key)) {
- const currentMode = config.getApprovalMode();
- switch (currentMode) {
- case ApprovalMode.DEFAULT:
- nextApprovalMode = ApprovalMode.AUTO_EDIT;
- break;
- case ApprovalMode.AUTO_EDIT:
- nextApprovalMode = allowPlanMode
- ? ApprovalMode.PLAN
- : ApprovalMode.DEFAULT;
- break;
- case ApprovalMode.PLAN:
- nextApprovalMode = ApprovalMode.DEFAULT;
- break;
- case ApprovalMode.YOLO:
- nextApprovalMode = ApprovalMode.AUTO_EDIT;
- break;
- default:
+
+ addItem(
+ {
+ type: MessageType.WARNING,
+ text,
+ },
+ Date.now(),
+ );
+ }
+ return;
+ }
+ const nextApprovalMode =
+ config.getApprovalMode() === ApprovalMode.YOLO
+ ? ApprovalMode.DEFAULT
+ : ApprovalMode.YOLO;
+
+ try {
+ config.setApprovalMode(nextApprovalMode);
+ setApprovalMode(nextApprovalMode);
+ onApprovalModeChange?.(nextApprovalMode);
+ } catch (e) {
+ if (addItem) {
+ addItem(
+ {
+ type: MessageType.INFO,
+ text: e instanceof Error ? e.message : String(e),
+ },
+ Date.now(),
+ );
+ }
+ }
+ }, [config, addItem, onApprovalModeChange]);
+
+ const cycleApprovalMode = useCallback(() => {
+ const currentMode = config.getApprovalMode();
+ let nextApprovalMode: ApprovalMode | undefined;
+ switch (currentMode) {
+ case ApprovalMode.DEFAULT:
+ nextApprovalMode = ApprovalMode.AUTO_EDIT;
+ break;
+ case ApprovalMode.AUTO_EDIT:
+ nextApprovalMode = allowPlanMode
+ ? ApprovalMode.PLAN
+ : ApprovalMode.DEFAULT;
+ break;
+ case ApprovalMode.PLAN:
+ nextApprovalMode = ApprovalMode.DEFAULT;
+ break;
+ case ApprovalMode.YOLO:
+ nextApprovalMode = ApprovalMode.AUTO_EDIT;
+ break;
+ default:
+ }
+
+ if (nextApprovalMode) {
+ try {
+ config.setApprovalMode(nextApprovalMode);
+ setApprovalMode(nextApprovalMode);
+ onApprovalModeChange?.(nextApprovalMode);
+ } catch (e) {
+ if (addItem) {
+ addItem(
+ {
+ type: MessageType.INFO,
+ text: e instanceof Error ? e.message : String(e),
+ },
+ Date.now(),
+ );
}
}
+ }
+ }, [config, allowPlanMode, onApprovalModeChange, addItem]);
- if (nextApprovalMode) {
- try {
- config.setApprovalMode(nextApprovalMode);
- // Update local state immediately for responsiveness
- setApprovalMode(nextApprovalMode);
-
- // Notify the central handler about the approval mode change
- onApprovalModeChange?.(nextApprovalMode);
- } catch (e) {
- if (addItem) {
- addItem(
- {
- type: MessageType.INFO,
- // eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
- text: (e as Error).message,
- },
- Date.now(),
- );
- }
- }
+ useKeypress(
+ (key) => {
+ if (keyMatchers[Command.TOGGLE_YOLO](key)) {
+ toggleYolo();
+ } else if (keyMatchers[Command.CYCLE_APPROVAL_MODE](key)) {
+ cycleApprovalMode();
}
},
{ isActive },
);
- return showApprovalMode;
+ return {
+ approvalMode: showApprovalMode,
+ cycleApprovalMode,
+ toggleYolo,
+ };
}