feat(plan): Extend Shift+Tab Mode Cycling to include Plan Mode (#17177)

This commit is contained in:
Adib234
2026-01-21 10:19:47 -05:00
committed by GitHub
parent b703c87a64
commit 0605e6e3e9
13 changed files with 163 additions and 100 deletions

View File

@@ -15,7 +15,7 @@ import {
} from 'vitest';
import { act } from 'react';
import { renderHook } from '../../test-utils/render.js';
import { useAutoAcceptIndicator } from './useAutoAcceptIndicator.js';
import { useApprovalModeIndicator } from './useApprovalModeIndicator.js';
import { Config, ApprovalMode } from '@google/gemini-cli-core';
import type { Config as ActualConfigType } from '@google/gemini-cli-core';
@@ -37,6 +37,7 @@ interface MockConfigInstanceShape {
getApprovalMode: Mock<() => ApprovalMode>;
setApprovalMode: Mock<(value: ApprovalMode) => void>;
isYoloModeDisabled: Mock<() => boolean>;
isPlanEnabled: Mock<() => boolean>;
isTrustedFolder: Mock<() => boolean>;
getCoreTools: Mock<() => string[]>;
getToolDiscoveryCommand: Mock<() => string | undefined>;
@@ -55,7 +56,7 @@ interface MockConfigInstanceShape {
type UseKeypressHandler = (key: Key) => void;
describe('useAutoAcceptIndicator', () => {
describe('useApprovalModeIndicator', () => {
let mockConfigInstance: MockConfigInstanceShape;
let capturedUseKeypressHandler: UseKeypressHandler;
let mockedUseKeypress: MockedFunction<typeof useKeypress>;
@@ -66,7 +67,9 @@ describe('useAutoAcceptIndicator', () => {
(
Config as unknown as MockedFunction<() => MockConfigInstanceShape>
).mockImplementation(() => {
const instanceGetApprovalModeMock = vi.fn();
const instanceGetApprovalModeMock = vi
.fn()
.mockReturnValue(ApprovalMode.DEFAULT);
const instanceSetApprovalModeMock = vi.fn();
const instance: MockConfigInstanceShape = {
@@ -77,6 +80,7 @@ describe('useAutoAcceptIndicator', () => {
(value: ApprovalMode) => void
>,
isYoloModeDisabled: vi.fn().mockReturnValue(false),
isPlanEnabled: vi.fn().mockReturnValue(false),
isTrustedFolder: vi.fn().mockReturnValue(true) as Mock<() => boolean>,
getCoreTools: vi.fn().mockReturnValue([]) as Mock<() => string[]>,
getToolDiscoveryCommand: vi.fn().mockReturnValue(undefined) as Mock<
@@ -126,7 +130,7 @@ describe('useAutoAcceptIndicator', () => {
it('should initialize with ApprovalMode.AUTO_EDIT if config.getApprovalMode returns ApprovalMode.AUTO_EDIT', () => {
mockConfigInstance.getApprovalMode.mockReturnValue(ApprovalMode.AUTO_EDIT);
const { result } = renderHook(() =>
useAutoAcceptIndicator({
useApprovalModeIndicator({
config: mockConfigInstance as unknown as ActualConfigType,
addItem: vi.fn(),
}),
@@ -138,7 +142,7 @@ describe('useAutoAcceptIndicator', () => {
it('should initialize with ApprovalMode.DEFAULT if config.getApprovalMode returns ApprovalMode.DEFAULT', () => {
mockConfigInstance.getApprovalMode.mockReturnValue(ApprovalMode.DEFAULT);
const { result } = renderHook(() =>
useAutoAcceptIndicator({
useApprovalModeIndicator({
config: mockConfigInstance as unknown as ActualConfigType,
addItem: vi.fn(),
}),
@@ -150,7 +154,7 @@ describe('useAutoAcceptIndicator', () => {
it('should initialize with ApprovalMode.YOLO if config.getApprovalMode returns ApprovalMode.YOLO', () => {
mockConfigInstance.getApprovalMode.mockReturnValue(ApprovalMode.YOLO);
const { result } = renderHook(() =>
useAutoAcceptIndicator({
useApprovalModeIndicator({
config: mockConfigInstance as unknown as ActualConfigType,
addItem: vi.fn(),
}),
@@ -159,16 +163,17 @@ describe('useAutoAcceptIndicator', () => {
expect(mockConfigInstance.getApprovalMode).toHaveBeenCalledTimes(1);
});
it('should toggle the indicator and update config when Shift+Tab or Ctrl+Y is pressed', () => {
it('should cycle the indicator and update config when Shift+Tab or Ctrl+Y is pressed', () => {
mockConfigInstance.getApprovalMode.mockReturnValue(ApprovalMode.DEFAULT);
const { result } = renderHook(() =>
useAutoAcceptIndicator({
useApprovalModeIndicator({
config: mockConfigInstance as unknown as ActualConfigType,
addItem: vi.fn(),
}),
);
expect(result.current).toBe(ApprovalMode.DEFAULT);
// Shift+Tab cycles to AUTO_EDIT
act(() => {
capturedUseKeypressHandler({
name: 'tab',
@@ -188,22 +193,7 @@ describe('useAutoAcceptIndicator', () => {
);
expect(result.current).toBe(ApprovalMode.YOLO);
act(() => {
capturedUseKeypressHandler({ name: 'y', ctrl: true } as Key);
});
expect(mockConfigInstance.setApprovalMode).toHaveBeenCalledWith(
ApprovalMode.DEFAULT,
);
expect(result.current).toBe(ApprovalMode.DEFAULT);
act(() => {
capturedUseKeypressHandler({ name: 'y', ctrl: true } as Key);
});
expect(mockConfigInstance.setApprovalMode).toHaveBeenCalledWith(
ApprovalMode.YOLO,
);
expect(result.current).toBe(ApprovalMode.YOLO);
// Shift+Tab cycles back to DEFAULT (since PLAN is disabled by default in mock)
act(() => {
capturedUseKeypressHandler({
name: 'tab',
@@ -215,22 +205,67 @@ describe('useAutoAcceptIndicator', () => {
);
expect(result.current).toBe(ApprovalMode.AUTO_EDIT);
// Ctrl+Y toggles YOLO
act(() => {
capturedUseKeypressHandler({ name: 'y', ctrl: true } as Key);
});
expect(mockConfigInstance.setApprovalMode).toHaveBeenCalledWith(
ApprovalMode.YOLO,
);
expect(result.current).toBe(ApprovalMode.YOLO);
// Shift+Tab from YOLO jumps to AUTO_EDIT
act(() => {
capturedUseKeypressHandler({
name: 'tab',
shift: true,
} as Key);
});
expect(mockConfigInstance.setApprovalMode).toHaveBeenCalledWith(
ApprovalMode.AUTO_EDIT,
);
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 -> PLAN
act(() => {
capturedUseKeypressHandler({ name: 'tab', shift: true } as Key);
});
expect(mockConfigInstance.setApprovalMode).toHaveBeenCalledWith(
ApprovalMode.PLAN,
);
// PLAN -> AUTO_EDIT
act(() => {
capturedUseKeypressHandler({ name: 'tab', shift: true } as Key);
});
expect(mockConfigInstance.setApprovalMode).toHaveBeenCalledWith(
ApprovalMode.AUTO_EDIT,
);
// AUTO_EDIT -> DEFAULT
act(() => {
capturedUseKeypressHandler({ name: 'tab', shift: true } as Key);
});
expect(mockConfigInstance.setApprovalMode).toHaveBeenCalledWith(
ApprovalMode.DEFAULT,
);
expect(result.current).toBe(ApprovalMode.DEFAULT);
});
it('should not toggle if only one key or other keys combinations are pressed', () => {
mockConfigInstance.getApprovalMode.mockReturnValue(ApprovalMode.DEFAULT);
renderHook(() =>
useAutoAcceptIndicator({
useApprovalModeIndicator({
config: mockConfigInstance as unknown as ActualConfigType,
addItem: vi.fn(),
}),
@@ -290,7 +325,7 @@ describe('useAutoAcceptIndicator', () => {
mockConfigInstance.getApprovalMode.mockReturnValue(ApprovalMode.DEFAULT);
const { result, rerender } = renderHook(
(props: { config: ActualConfigType; addItem: () => void }) =>
useAutoAcceptIndicator(props),
useApprovalModeIndicator(props),
{
initialProps: {
config: mockConfigInstance as unknown as ActualConfigType,
@@ -324,7 +359,7 @@ describe('useAutoAcceptIndicator', () => {
});
const mockAddItem = vi.fn();
const { result } = renderHook(() =>
useAutoAcceptIndicator({
useApprovalModeIndicator({
config: mockConfigInstance as unknown as ActualConfigType,
addItem: mockAddItem,
}),
@@ -354,7 +389,7 @@ describe('useAutoAcceptIndicator', () => {
});
const mockAddItem = vi.fn();
const { result } = renderHook(() =>
useAutoAcceptIndicator({
useApprovalModeIndicator({
config: mockConfigInstance as unknown as ActualConfigType,
addItem: mockAddItem,
}),
@@ -382,7 +417,7 @@ describe('useAutoAcceptIndicator', () => {
mockConfigInstance.getApprovalMode.mockReturnValue(ApprovalMode.YOLO);
const mockAddItem = vi.fn();
renderHook(() =>
useAutoAcceptIndicator({
useApprovalModeIndicator({
config: mockConfigInstance as unknown as ActualConfigType,
addItem: mockAddItem,
}),
@@ -404,7 +439,7 @@ describe('useAutoAcceptIndicator', () => {
);
const mockAddItem = vi.fn();
renderHook(() =>
useAutoAcceptIndicator({
useApprovalModeIndicator({
config: mockConfigInstance as unknown as ActualConfigType,
addItem: mockAddItem,
}),
@@ -433,7 +468,7 @@ describe('useAutoAcceptIndicator', () => {
const mockAddItem = vi.fn();
renderHook(() =>
useAutoAcceptIndicator({
useApprovalModeIndicator({
config: mockConfigInstance as unknown as ActualConfigType,
addItem: mockAddItem,
}),
@@ -484,7 +519,7 @@ describe('useAutoAcceptIndicator', () => {
mockConfigInstance.getApprovalMode.mockReturnValue(ApprovalMode.DEFAULT);
const mockAddItem = vi.fn();
const { result } = renderHook(() =>
useAutoAcceptIndicator({
useApprovalModeIndicator({
config: mockConfigInstance as unknown as ActualConfigType,
addItem: mockAddItem,
}),
@@ -517,7 +552,7 @@ describe('useAutoAcceptIndicator', () => {
const mockOnApprovalModeChange = vi.fn();
renderHook(() =>
useAutoAcceptIndicator({
useApprovalModeIndicator({
config: mockConfigInstance as unknown as ActualConfigType,
onApprovalModeChange: mockOnApprovalModeChange,
}),
@@ -539,7 +574,7 @@ describe('useAutoAcceptIndicator', () => {
const mockOnApprovalModeChange = vi.fn();
renderHook(() =>
useAutoAcceptIndicator({
useApprovalModeIndicator({
config: mockConfigInstance as unknown as ActualConfigType,
onApprovalModeChange: mockOnApprovalModeChange,
}),
@@ -563,7 +598,7 @@ describe('useAutoAcceptIndicator', () => {
const mockOnApprovalModeChange = vi.fn();
renderHook(() =>
useAutoAcceptIndicator({
useApprovalModeIndicator({
config: mockConfigInstance as unknown as ActualConfigType,
onApprovalModeChange: mockOnApprovalModeChange,
}),
@@ -583,7 +618,7 @@ describe('useAutoAcceptIndicator', () => {
mockConfigInstance.getApprovalMode.mockReturnValue(ApprovalMode.DEFAULT);
renderHook(() =>
useAutoAcceptIndicator({
useApprovalModeIndicator({
config: mockConfigInstance as unknown as ActualConfigType,
}),
);
@@ -604,7 +639,7 @@ describe('useAutoAcceptIndicator', () => {
const mockOnApprovalModeChange = vi.fn();
renderHook(() =>
useAutoAcceptIndicator({
useApprovalModeIndicator({
config: mockConfigInstance as unknown as ActualConfigType,
onApprovalModeChange: mockOnApprovalModeChange,
}),

View File

@@ -11,25 +11,24 @@ import { keyMatchers, Command } from '../keyMatchers.js';
import type { HistoryItemWithoutId } from '../types.js';
import { MessageType } from '../types.js';
export interface UseAutoAcceptIndicatorArgs {
export interface UseApprovalModeIndicatorArgs {
config: Config;
addItem?: (item: HistoryItemWithoutId, timestamp: number) => void;
onApprovalModeChange?: (mode: ApprovalMode) => void;
isActive?: boolean;
}
export function useAutoAcceptIndicator({
export function useApprovalModeIndicator({
config,
addItem,
onApprovalModeChange,
isActive = true,
}: UseAutoAcceptIndicatorArgs): ApprovalMode {
}: UseApprovalModeIndicatorArgs): ApprovalMode {
const currentConfigValue = config.getApprovalMode();
const [showAutoAcceptIndicator, setShowAutoAcceptIndicator] =
useState(currentConfigValue);
const [showApprovalMode, setApprovalMode] = useState(currentConfigValue);
useEffect(() => {
setShowAutoAcceptIndicator(currentConfigValue);
setApprovalMode(currentConfigValue);
}, [currentConfigValue]);
useKeypress(
@@ -56,18 +55,32 @@ export function useAutoAcceptIndicator({
config.getApprovalMode() === ApprovalMode.YOLO
? ApprovalMode.DEFAULT
: ApprovalMode.YOLO;
} else if (keyMatchers[Command.TOGGLE_AUTO_EDIT](key)) {
nextApprovalMode =
config.getApprovalMode() === ApprovalMode.AUTO_EDIT
? ApprovalMode.DEFAULT
: ApprovalMode.AUTO_EDIT;
} else if (keyMatchers[Command.CYCLE_APPROVAL_MODE](key)) {
const currentMode = config.getApprovalMode();
switch (currentMode) {
case ApprovalMode.DEFAULT:
nextApprovalMode = config.isPlanEnabled()
? ApprovalMode.PLAN
: ApprovalMode.AUTO_EDIT;
break;
case ApprovalMode.PLAN:
nextApprovalMode = ApprovalMode.AUTO_EDIT;
break;
case ApprovalMode.AUTO_EDIT:
nextApprovalMode = ApprovalMode.DEFAULT;
break;
case ApprovalMode.YOLO:
nextApprovalMode = ApprovalMode.AUTO_EDIT;
break;
default:
}
}
if (nextApprovalMode) {
try {
config.setApprovalMode(nextApprovalMode);
// Update local state immediately for responsiveness
setShowAutoAcceptIndicator(nextApprovalMode);
setApprovalMode(nextApprovalMode);
// Notify the central handler about the approval mode change
onApprovalModeChange?.(nextApprovalMode);
@@ -87,5 +100,5 @@ export function useAutoAcceptIndicator({
{ isActive },
);
return showAutoAcceptIndicator;
return showApprovalMode;
}