mirror of
https://github.com/QwenLM/qwen-code.git
synced 2025-12-20 16:57:46 +00:00
feat: Add --yolo mode that automatically accepts all tools executions (#695)
Co-authored-by: N. Taylor Mullen <ntaylormullen@google.com>
This commit is contained in:
@@ -15,6 +15,7 @@ import {
|
||||
ConfigParameters,
|
||||
setGeminiMdFilename as setServerGeminiMdFilename,
|
||||
getCurrentGeminiMdFilename,
|
||||
ApprovalMode,
|
||||
} from '@gemini-code/core';
|
||||
import { Settings } from './settings.js';
|
||||
import { readPackageUp } from 'read-package-up';
|
||||
@@ -38,6 +39,7 @@ interface CliArgs {
|
||||
prompt: string | undefined;
|
||||
all_files: boolean | undefined;
|
||||
show_memory_usage: boolean | undefined;
|
||||
yolo: boolean | undefined;
|
||||
}
|
||||
|
||||
async function parseArguments(): Promise<CliArgs> {
|
||||
@@ -75,6 +77,13 @@ async function parseArguments(): Promise<CliArgs> {
|
||||
description: 'Show memory usage in status bar',
|
||||
default: false,
|
||||
})
|
||||
.option('yolo', {
|
||||
alias: 'y',
|
||||
type: 'boolean',
|
||||
description:
|
||||
'Automatically accept all actions (aka YOLO mode, see https://www.youtube.com/watch?v=xvFZjo5PgG0 for more details)?',
|
||||
default: false,
|
||||
})
|
||||
.version() // This will enable the --version flag based on package.json
|
||||
.help()
|
||||
.alias('h', 'help')
|
||||
@@ -158,7 +167,7 @@ export async function loadCliConfig(settings: Settings): Promise<Config> {
|
||||
const configParams: ConfigParameters = {
|
||||
apiKey: apiKeyForServer,
|
||||
model: argv.model || DEFAULT_GEMINI_MODEL,
|
||||
sandbox: argv.sandbox ?? settings.sandbox ?? false,
|
||||
sandbox: argv.sandbox ?? settings.sandbox ?? argv.yolo ?? false,
|
||||
targetDir: process.cwd(),
|
||||
debugMode,
|
||||
question: argv.prompt || '',
|
||||
@@ -171,6 +180,7 @@ export async function loadCliConfig(settings: Settings): Promise<Config> {
|
||||
userAgent,
|
||||
userMemory: memoryContent,
|
||||
geminiMdFileCount: fileCount,
|
||||
approvalMode: argv.yolo || false ? ApprovalMode.YOLO : ApprovalMode.DEFAULT,
|
||||
vertexai: useVertexAI,
|
||||
showMemoryUsage:
|
||||
argv.show_memory_usage || settings.showMemoryUsage || false,
|
||||
|
||||
@@ -8,7 +8,7 @@ import { describe, it, expect, vi, beforeEach, afterEach, Mock } from 'vitest';
|
||||
import { render } from 'ink-testing-library';
|
||||
import { App } from './App.js';
|
||||
import { Config as ServerConfig, MCPServerConfig } from '@gemini-code/core';
|
||||
import type { ToolRegistry } from '@gemini-code/core';
|
||||
import { ApprovalMode, ToolRegistry } from '@gemini-code/core';
|
||||
import { LoadedSettings, SettingsFile, Settings } from '../config/settings.js';
|
||||
|
||||
// Define a more complete mock server config based on actual Config
|
||||
@@ -28,7 +28,7 @@ interface MockServerConfig {
|
||||
userAgent: string;
|
||||
userMemory: string;
|
||||
geminiMdFileCount: number;
|
||||
alwaysSkipModificationConfirmation: boolean;
|
||||
approvalMode: ApprovalMode;
|
||||
vertexai?: boolean;
|
||||
showMemoryUsage?: boolean;
|
||||
|
||||
@@ -50,8 +50,8 @@ interface MockServerConfig {
|
||||
setUserMemory: Mock<(newUserMemory: string) => void>;
|
||||
getGeminiMdFileCount: Mock<() => number>;
|
||||
setGeminiMdFileCount: Mock<(count: number) => void>;
|
||||
getAlwaysSkipModificationConfirmation: Mock<() => boolean>;
|
||||
setAlwaysSkipModificationConfirmation: Mock<(skip: boolean) => void>;
|
||||
getApprovalMode: Mock<() => ApprovalMode>;
|
||||
setApprovalMode: Mock<(skip: ApprovalMode) => void>;
|
||||
getVertexAI: Mock<() => boolean | undefined>;
|
||||
getShowMemoryUsage: Mock<() => boolean>;
|
||||
}
|
||||
@@ -80,8 +80,7 @@ vi.mock('@gemini-code/core', async (importOriginal) => {
|
||||
userAgent: opts.userAgent || 'test-agent',
|
||||
userMemory: opts.userMemory || '',
|
||||
geminiMdFileCount: opts.geminiMdFileCount || 0,
|
||||
alwaysSkipModificationConfirmation:
|
||||
opts.alwaysSkipModificationConfirmation ?? false,
|
||||
approvalMode: opts.approvalMode ?? ApprovalMode.DEFAULT,
|
||||
vertexai: opts.vertexai,
|
||||
showMemoryUsage: opts.showMemoryUsage ?? false,
|
||||
|
||||
@@ -105,10 +104,8 @@ vi.mock('@gemini-code/core', async (importOriginal) => {
|
||||
setUserMemory: vi.fn(),
|
||||
getGeminiMdFileCount: vi.fn(() => opts.geminiMdFileCount || 0),
|
||||
setGeminiMdFileCount: vi.fn(),
|
||||
getAlwaysSkipModificationConfirmation: vi.fn(
|
||||
() => opts.alwaysSkipModificationConfirmation ?? false,
|
||||
),
|
||||
setAlwaysSkipModificationConfirmation: vi.fn(),
|
||||
getApprovalMode: vi.fn(() => opts.approvalMode ?? ApprovalMode.DEFAULT),
|
||||
setApprovalMode: vi.fn(),
|
||||
getVertexAI: vi.fn(() => opts.vertexai),
|
||||
getShowMemoryUsage: vi.fn(() => opts.showMemoryUsage ?? false),
|
||||
};
|
||||
|
||||
@@ -44,6 +44,7 @@ import {
|
||||
getErrorMessage,
|
||||
type Config,
|
||||
getCurrentGeminiMdFilename,
|
||||
ApprovalMode,
|
||||
} from '@gemini-code/core';
|
||||
import { useLogger } from './hooks/useLogger.js';
|
||||
import { StreamingContext } from './contexts/StreamingContext.js';
|
||||
@@ -412,9 +413,12 @@ export const App = ({
|
||||
)}
|
||||
</Box>
|
||||
<Box>
|
||||
{showAutoAcceptIndicator && !shellModeActive && (
|
||||
<AutoAcceptIndicator />
|
||||
)}
|
||||
{showAutoAcceptIndicator !== ApprovalMode.DEFAULT &&
|
||||
!shellModeActive && (
|
||||
<AutoAcceptIndicator
|
||||
approvalMode={showAutoAcceptIndicator}
|
||||
/>
|
||||
)}
|
||||
{shellModeActive && <ShellModeIndicator />}
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
@@ -7,12 +7,41 @@
|
||||
import React from 'react';
|
||||
import { Box, Text } from 'ink';
|
||||
import { Colors } from '../colors.js';
|
||||
import { ApprovalMode } from '@gemini-code/core';
|
||||
|
||||
export const AutoAcceptIndicator: React.FC = () => (
|
||||
<Box>
|
||||
<Text color={Colors.AccentGreen}>
|
||||
accepting edits
|
||||
<Text color={Colors.SubtleComment}> (shift + tab to disable)</Text>
|
||||
</Text>
|
||||
</Box>
|
||||
);
|
||||
interface AutoAcceptIndicatorProps {
|
||||
approvalMode: ApprovalMode;
|
||||
}
|
||||
|
||||
export const AutoAcceptIndicator: React.FC<AutoAcceptIndicatorProps> = ({
|
||||
approvalMode,
|
||||
}) => {
|
||||
let textColor = '';
|
||||
let textContent = '';
|
||||
let subText = '';
|
||||
|
||||
switch (approvalMode) {
|
||||
case ApprovalMode.AUTO_EDIT:
|
||||
textColor = Colors.AccentGreen;
|
||||
textContent = 'accepting edits';
|
||||
subText = ' (shift + tab to toggle)';
|
||||
break;
|
||||
case ApprovalMode.YOLO:
|
||||
textColor = Colors.AccentRed;
|
||||
textContent = 'YOLO mode';
|
||||
subText = ' (ctrl + y to toggle)';
|
||||
break;
|
||||
case ApprovalMode.DEFAULT:
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
return (
|
||||
<Box>
|
||||
<Text color={textColor}>
|
||||
{textContent}
|
||||
{subText && <Text color={Colors.SubtleComment}>{subText}</Text>}
|
||||
</Text>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -16,7 +16,11 @@ import {
|
||||
import { renderHook, act } from '@testing-library/react';
|
||||
import { useAutoAcceptIndicator } from './useAutoAcceptIndicator.js';
|
||||
|
||||
import type { Config as ActualConfigType } from '@gemini-code/core';
|
||||
import {
|
||||
Config,
|
||||
Config as ActualConfigType,
|
||||
ApprovalMode,
|
||||
} from '@gemini-code/core';
|
||||
import { useInput, type Key as InkKey } from 'ink';
|
||||
|
||||
vi.mock('ink');
|
||||
@@ -31,11 +35,9 @@ vi.mock('@gemini-code/core', async () => {
|
||||
};
|
||||
});
|
||||
|
||||
import { Config } from '@gemini-code/core';
|
||||
|
||||
interface MockConfigInstanceShape {
|
||||
getAlwaysSkipModificationConfirmation: Mock<() => boolean>;
|
||||
setAlwaysSkipModificationConfirmation: Mock<(value: boolean) => void>;
|
||||
getApprovalMode: Mock<() => ApprovalMode>;
|
||||
setApprovalMode: Mock<(value: ApprovalMode) => void>;
|
||||
getCoreTools: Mock<() => string[]>;
|
||||
getToolDiscoveryCommand: Mock<() => string | undefined>;
|
||||
getTargetDir: Mock<() => string>;
|
||||
@@ -65,14 +67,16 @@ describe('useAutoAcceptIndicator', () => {
|
||||
(
|
||||
Config as unknown as MockedFunction<() => MockConfigInstanceShape>
|
||||
).mockImplementation(() => {
|
||||
const instanceGetAlwaysSkipMock = vi.fn();
|
||||
const instanceSetAlwaysSkipMock = vi.fn();
|
||||
const instanceGetApprovalModeMock = vi.fn();
|
||||
const instanceSetApprovalModeMock = vi.fn();
|
||||
|
||||
const instance: MockConfigInstanceShape = {
|
||||
getAlwaysSkipModificationConfirmation:
|
||||
instanceGetAlwaysSkipMock as Mock<() => boolean>,
|
||||
setAlwaysSkipModificationConfirmation:
|
||||
instanceSetAlwaysSkipMock as Mock<(value: boolean) => void>,
|
||||
getApprovalMode: instanceGetApprovalModeMock as Mock<
|
||||
() => ApprovalMode
|
||||
>,
|
||||
setApprovalMode: instanceSetApprovalModeMock as Mock<
|
||||
(value: ApprovalMode) => void
|
||||
>,
|
||||
getCoreTools: vi.fn().mockReturnValue([]) as Mock<() => string[]>,
|
||||
getToolDiscoveryCommand: vi.fn().mockReturnValue(undefined) as Mock<
|
||||
() => string | undefined
|
||||
@@ -101,8 +105,8 @@ describe('useAutoAcceptIndicator', () => {
|
||||
() => { discoverTools: Mock<() => void> }
|
||||
>,
|
||||
};
|
||||
instanceSetAlwaysSkipMock.mockImplementation((value: boolean) => {
|
||||
instanceGetAlwaysSkipMock.mockReturnValue(value);
|
||||
instanceSetApprovalModeMock.mockImplementation((value: ApprovalMode) => {
|
||||
instanceGetApprovalModeMock.mockReturnValue(value);
|
||||
});
|
||||
return instance;
|
||||
});
|
||||
@@ -116,68 +120,99 @@ describe('useAutoAcceptIndicator', () => {
|
||||
mockConfigInstance = new (Config as any)() as MockConfigInstanceShape;
|
||||
});
|
||||
|
||||
it('should initialize with true if config.getAlwaysSkipModificationConfirmation returns true', () => {
|
||||
mockConfigInstance.getAlwaysSkipModificationConfirmation.mockReturnValue(
|
||||
true,
|
||||
);
|
||||
it('should initialize with ApprovalMode.AUTO_EDIT if config.getApprovalMode returns ApprovalMode.AUTO_EDIT', () => {
|
||||
mockConfigInstance.getApprovalMode.mockReturnValue(ApprovalMode.AUTO_EDIT);
|
||||
const { result } = renderHook(() =>
|
||||
useAutoAcceptIndicator({
|
||||
config: mockConfigInstance as unknown as ActualConfigType,
|
||||
}),
|
||||
);
|
||||
expect(result.current).toBe(true);
|
||||
expect(
|
||||
mockConfigInstance.getAlwaysSkipModificationConfirmation,
|
||||
).toHaveBeenCalledTimes(1);
|
||||
expect(result.current).toBe(ApprovalMode.AUTO_EDIT);
|
||||
expect(mockConfigInstance.getApprovalMode).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('should initialize with false if config.getAlwaysSkipModificationConfirmation returns false', () => {
|
||||
mockConfigInstance.getAlwaysSkipModificationConfirmation.mockReturnValue(
|
||||
false,
|
||||
);
|
||||
it('should initialize with ApprovalMode.DEFAULT if config.getApprovalMode returns ApprovalMode.DEFAULT', () => {
|
||||
mockConfigInstance.getApprovalMode.mockReturnValue(ApprovalMode.DEFAULT);
|
||||
const { result } = renderHook(() =>
|
||||
useAutoAcceptIndicator({
|
||||
config: mockConfigInstance as unknown as ActualConfigType,
|
||||
}),
|
||||
);
|
||||
expect(result.current).toBe(false);
|
||||
expect(
|
||||
mockConfigInstance.getAlwaysSkipModificationConfirmation,
|
||||
).toHaveBeenCalledTimes(1);
|
||||
expect(result.current).toBe(ApprovalMode.DEFAULT);
|
||||
expect(mockConfigInstance.getApprovalMode).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('should toggle the indicator and update config when Shift+Tab is pressed', () => {
|
||||
mockConfigInstance.getAlwaysSkipModificationConfirmation.mockReturnValue(
|
||||
false,
|
||||
);
|
||||
it('should initialize with ApprovalMode.YOLO if config.getApprovalMode returns ApprovalMode.YOLO', () => {
|
||||
mockConfigInstance.getApprovalMode.mockReturnValue(ApprovalMode.YOLO);
|
||||
const { result } = renderHook(() =>
|
||||
useAutoAcceptIndicator({
|
||||
config: mockConfigInstance as unknown as ActualConfigType,
|
||||
}),
|
||||
);
|
||||
expect(result.current).toBe(false);
|
||||
expect(result.current).toBe(ApprovalMode.YOLO);
|
||||
expect(mockConfigInstance.getApprovalMode).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('should toggle the indicator and update config when Shift+Tab or Ctrl+Y is pressed', () => {
|
||||
mockConfigInstance.getApprovalMode.mockReturnValue(ApprovalMode.DEFAULT);
|
||||
const { result } = renderHook(() =>
|
||||
useAutoAcceptIndicator({
|
||||
config: mockConfigInstance as unknown as ActualConfigType,
|
||||
}),
|
||||
);
|
||||
expect(result.current).toBe(ApprovalMode.DEFAULT);
|
||||
|
||||
act(() => {
|
||||
capturedUseInputHandler('', { tab: true, shift: true } as InkKey);
|
||||
});
|
||||
expect(
|
||||
mockConfigInstance.setAlwaysSkipModificationConfirmation,
|
||||
).toHaveBeenCalledWith(true);
|
||||
expect(result.current).toBe(true);
|
||||
expect(mockConfigInstance.setApprovalMode).toHaveBeenCalledWith(
|
||||
ApprovalMode.AUTO_EDIT,
|
||||
);
|
||||
expect(result.current).toBe(ApprovalMode.AUTO_EDIT);
|
||||
|
||||
act(() => {
|
||||
capturedUseInputHandler('y', { ctrl: true } as InkKey);
|
||||
});
|
||||
expect(mockConfigInstance.setApprovalMode).toHaveBeenCalledWith(
|
||||
ApprovalMode.YOLO,
|
||||
);
|
||||
expect(result.current).toBe(ApprovalMode.YOLO);
|
||||
|
||||
act(() => {
|
||||
capturedUseInputHandler('y', { ctrl: true } as InkKey);
|
||||
});
|
||||
expect(mockConfigInstance.setApprovalMode).toHaveBeenCalledWith(
|
||||
ApprovalMode.DEFAULT,
|
||||
);
|
||||
expect(result.current).toBe(ApprovalMode.DEFAULT);
|
||||
|
||||
act(() => {
|
||||
capturedUseInputHandler('y', { ctrl: true } as InkKey);
|
||||
});
|
||||
expect(mockConfigInstance.setApprovalMode).toHaveBeenCalledWith(
|
||||
ApprovalMode.YOLO,
|
||||
);
|
||||
expect(result.current).toBe(ApprovalMode.YOLO);
|
||||
|
||||
act(() => {
|
||||
capturedUseInputHandler('', { tab: true, shift: true } as InkKey);
|
||||
});
|
||||
expect(
|
||||
mockConfigInstance.setAlwaysSkipModificationConfirmation,
|
||||
).toHaveBeenCalledWith(false);
|
||||
expect(result.current).toBe(false);
|
||||
expect(mockConfigInstance.setApprovalMode).toHaveBeenCalledWith(
|
||||
ApprovalMode.AUTO_EDIT,
|
||||
);
|
||||
expect(result.current).toBe(ApprovalMode.AUTO_EDIT);
|
||||
|
||||
act(() => {
|
||||
capturedUseInputHandler('', { tab: true, shift: true } as InkKey);
|
||||
});
|
||||
expect(mockConfigInstance.setApprovalMode).toHaveBeenCalledWith(
|
||||
ApprovalMode.DEFAULT,
|
||||
);
|
||||
expect(result.current).toBe(ApprovalMode.DEFAULT);
|
||||
});
|
||||
|
||||
it('should not toggle if only Tab, only Shift, or other keys are pressed', () => {
|
||||
mockConfigInstance.getAlwaysSkipModificationConfirmation.mockReturnValue(
|
||||
false,
|
||||
);
|
||||
it('should not toggle if only one key or other keys combinations are pressed', () => {
|
||||
mockConfigInstance.getApprovalMode.mockReturnValue(ApprovalMode.DEFAULT);
|
||||
renderHook(() =>
|
||||
useAutoAcceptIndicator({
|
||||
config: mockConfigInstance as unknown as ActualConfigType,
|
||||
@@ -187,29 +222,41 @@ describe('useAutoAcceptIndicator', () => {
|
||||
act(() => {
|
||||
capturedUseInputHandler('', { tab: true, shift: false } as InkKey);
|
||||
});
|
||||
expect(
|
||||
mockConfigInstance.setAlwaysSkipModificationConfirmation,
|
||||
).not.toHaveBeenCalled();
|
||||
expect(mockConfigInstance.setApprovalMode).not.toHaveBeenCalled();
|
||||
|
||||
act(() => {
|
||||
capturedUseInputHandler('', { tab: false, shift: true } as InkKey);
|
||||
});
|
||||
expect(
|
||||
mockConfigInstance.setAlwaysSkipModificationConfirmation,
|
||||
).not.toHaveBeenCalled();
|
||||
expect(mockConfigInstance.setApprovalMode).not.toHaveBeenCalled();
|
||||
|
||||
act(() => {
|
||||
capturedUseInputHandler('a', { tab: false, shift: false } as InkKey);
|
||||
});
|
||||
expect(
|
||||
mockConfigInstance.setAlwaysSkipModificationConfirmation,
|
||||
).not.toHaveBeenCalled();
|
||||
expect(mockConfigInstance.setApprovalMode).not.toHaveBeenCalled();
|
||||
|
||||
act(() => {
|
||||
capturedUseInputHandler('y', { tab: true } as InkKey);
|
||||
});
|
||||
expect(mockConfigInstance.setApprovalMode).not.toHaveBeenCalled();
|
||||
|
||||
act(() => {
|
||||
capturedUseInputHandler('a', { ctrl: true } as InkKey);
|
||||
});
|
||||
expect(mockConfigInstance.setApprovalMode).not.toHaveBeenCalled();
|
||||
|
||||
act(() => {
|
||||
capturedUseInputHandler('y', { shift: true } as InkKey);
|
||||
});
|
||||
expect(mockConfigInstance.setApprovalMode).not.toHaveBeenCalled();
|
||||
|
||||
act(() => {
|
||||
capturedUseInputHandler('a', { ctrl: true, shift: true } as InkKey);
|
||||
});
|
||||
expect(mockConfigInstance.setApprovalMode).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should update indicator when config value changes externally (useEffect dependency)', () => {
|
||||
mockConfigInstance.getAlwaysSkipModificationConfirmation.mockReturnValue(
|
||||
false,
|
||||
);
|
||||
mockConfigInstance.getApprovalMode.mockReturnValue(ApprovalMode.DEFAULT);
|
||||
const { result, rerender } = renderHook(
|
||||
(props: { config: ActualConfigType }) => useAutoAcceptIndicator(props),
|
||||
{
|
||||
@@ -218,16 +265,12 @@ describe('useAutoAcceptIndicator', () => {
|
||||
},
|
||||
},
|
||||
);
|
||||
expect(result.current).toBe(false);
|
||||
expect(result.current).toBe(ApprovalMode.DEFAULT);
|
||||
|
||||
mockConfigInstance.getAlwaysSkipModificationConfirmation.mockReturnValue(
|
||||
true,
|
||||
);
|
||||
mockConfigInstance.getApprovalMode.mockReturnValue(ApprovalMode.AUTO_EDIT);
|
||||
|
||||
rerender({ config: mockConfigInstance as unknown as ActualConfigType });
|
||||
expect(result.current).toBe(true);
|
||||
expect(
|
||||
mockConfigInstance.getAlwaysSkipModificationConfirmation,
|
||||
).toHaveBeenCalledTimes(3);
|
||||
expect(result.current).toBe(ApprovalMode.AUTO_EDIT);
|
||||
expect(mockConfigInstance.getApprovalMode).toHaveBeenCalledTimes(3);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
|
||||
import { useState, useEffect } from 'react';
|
||||
import { useInput } from 'ink';
|
||||
import type { Config } from '@gemini-code/core';
|
||||
import { ApprovalMode, type Config } from '@gemini-code/core';
|
||||
|
||||
export interface UseAutoAcceptIndicatorArgs {
|
||||
config: Config;
|
||||
@@ -14,8 +14,8 @@ export interface UseAutoAcceptIndicatorArgs {
|
||||
|
||||
export function useAutoAcceptIndicator({
|
||||
config,
|
||||
}: UseAutoAcceptIndicatorArgs): boolean {
|
||||
const currentConfigValue = config.getAlwaysSkipModificationConfirmation();
|
||||
}: UseAutoAcceptIndicatorArgs): ApprovalMode {
|
||||
const currentConfigValue = config.getApprovalMode();
|
||||
const [showAutoAcceptIndicator, setShowAutoAcceptIndicator] =
|
||||
useState(currentConfigValue);
|
||||
|
||||
@@ -23,15 +23,25 @@ export function useAutoAcceptIndicator({
|
||||
setShowAutoAcceptIndicator(currentConfigValue);
|
||||
}, [currentConfigValue]);
|
||||
|
||||
useInput((_input, key) => {
|
||||
if (key.tab && key.shift) {
|
||||
const alwaysAcceptModificationConfirmations =
|
||||
!config.getAlwaysSkipModificationConfirmation();
|
||||
config.setAlwaysSkipModificationConfirmation(
|
||||
alwaysAcceptModificationConfirmations,
|
||||
);
|
||||
useInput((input, key) => {
|
||||
let nextApprovalMode: ApprovalMode | undefined;
|
||||
|
||||
if (key.ctrl && input === 'y') {
|
||||
nextApprovalMode =
|
||||
config.getApprovalMode() === ApprovalMode.YOLO
|
||||
? ApprovalMode.DEFAULT
|
||||
: ApprovalMode.YOLO;
|
||||
} else if (key.tab && key.shift) {
|
||||
nextApprovalMode =
|
||||
config.getApprovalMode() === ApprovalMode.AUTO_EDIT
|
||||
? ApprovalMode.DEFAULT
|
||||
: ApprovalMode.AUTO_EDIT;
|
||||
}
|
||||
|
||||
if (nextApprovalMode) {
|
||||
config.setApprovalMode(nextApprovalMode);
|
||||
// Update local state immediately for responsiveness
|
||||
setShowAutoAcceptIndicator(alwaysAcceptModificationConfirmations);
|
||||
setShowAutoAcceptIndicator(nextApprovalMode);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -134,6 +134,7 @@ export function useReactToolScheduler(
|
||||
outputUpdateHandler,
|
||||
onAllToolCallsComplete: allToolCallsCompleteHandler,
|
||||
onToolCallsUpdate: toolCallsUpdateHandler,
|
||||
approvalMode: config.getApprovalMode(),
|
||||
});
|
||||
}, [config, onComplete, setPendingHistoryItem]);
|
||||
|
||||
|
||||
@@ -28,7 +28,8 @@ import {
|
||||
ToolCallResponseInfo,
|
||||
formatLlmContentForFunctionResponse, // Import from core
|
||||
ToolCall, // Import from core
|
||||
Status as ToolCallStatusType, // Import from core
|
||||
Status as ToolCallStatusType,
|
||||
ApprovalMode, // Import from core
|
||||
} from '@gemini-code/core';
|
||||
import {
|
||||
HistoryItemWithoutId,
|
||||
@@ -52,6 +53,7 @@ const mockToolRegistry = {
|
||||
|
||||
const mockConfig = {
|
||||
getToolRegistry: vi.fn(() => mockToolRegistry as unknown as ToolRegistry),
|
||||
getApprovalMode: vi.fn(() => ApprovalMode.DEFAULT),
|
||||
};
|
||||
|
||||
const mockTool: Tool = {
|
||||
@@ -205,6 +207,109 @@ describe('formatLlmContentForFunctionResponse', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('useReactToolScheduler in YOLO Mode', () => {
|
||||
let onComplete: Mock;
|
||||
let setPendingHistoryItem: Mock;
|
||||
|
||||
beforeEach(() => {
|
||||
onComplete = vi.fn();
|
||||
setPendingHistoryItem = vi.fn();
|
||||
mockToolRegistry.getTool.mockClear();
|
||||
(mockToolRequiresConfirmation.execute as Mock).mockClear();
|
||||
(mockToolRequiresConfirmation.shouldConfirmExecute as Mock).mockClear();
|
||||
|
||||
// IMPORTANT: Enable YOLO mode for this test suite
|
||||
(mockConfig.getApprovalMode as Mock).mockReturnValue(ApprovalMode.YOLO);
|
||||
|
||||
vi.useFakeTimers();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.clearAllTimers();
|
||||
vi.useRealTimers();
|
||||
// IMPORTANT: Disable YOLO mode after this test suite
|
||||
(mockConfig.getApprovalMode as Mock).mockReturnValue(ApprovalMode.DEFAULT);
|
||||
});
|
||||
|
||||
const renderSchedulerInYoloMode = () =>
|
||||
renderHook(() =>
|
||||
useReactToolScheduler(
|
||||
onComplete,
|
||||
mockConfig as unknown as Config,
|
||||
setPendingHistoryItem,
|
||||
),
|
||||
);
|
||||
|
||||
it('should skip confirmation and execute tool directly when yoloMode is true', async () => {
|
||||
mockToolRegistry.getTool.mockReturnValue(mockToolRequiresConfirmation);
|
||||
const expectedOutput = 'YOLO Confirmed output';
|
||||
(mockToolRequiresConfirmation.execute as Mock).mockResolvedValue({
|
||||
llmContent: expectedOutput,
|
||||
returnDisplay: 'YOLO Formatted tool output',
|
||||
} as ToolResult);
|
||||
|
||||
const { result } = renderSchedulerInYoloMode();
|
||||
const schedule = result.current[1];
|
||||
const request: ToolCallRequestInfo = {
|
||||
callId: 'yoloCall',
|
||||
name: 'mockToolRequiresConfirmation',
|
||||
args: { data: 'any data' },
|
||||
};
|
||||
|
||||
act(() => {
|
||||
schedule(request);
|
||||
});
|
||||
|
||||
await act(async () => {
|
||||
await vi.runAllTimersAsync(); // Process validation
|
||||
});
|
||||
await act(async () => {
|
||||
await vi.runAllTimersAsync(); // Process scheduling
|
||||
});
|
||||
await act(async () => {
|
||||
await vi.runAllTimersAsync(); // Process execution
|
||||
});
|
||||
|
||||
// Check that shouldConfirmExecute was NOT called
|
||||
expect(
|
||||
mockToolRequiresConfirmation.shouldConfirmExecute,
|
||||
).not.toHaveBeenCalled();
|
||||
|
||||
// Check that execute WAS called
|
||||
expect(mockToolRequiresConfirmation.execute).toHaveBeenCalledWith(
|
||||
request.args,
|
||||
expect.any(AbortSignal),
|
||||
undefined,
|
||||
);
|
||||
|
||||
// Check that onComplete was called with success
|
||||
expect(onComplete).toHaveBeenCalledWith([
|
||||
expect.objectContaining({
|
||||
status: 'success',
|
||||
request,
|
||||
response: expect.objectContaining({
|
||||
resultDisplay: 'YOLO Formatted tool output',
|
||||
responseParts: expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
functionResponse: expect.objectContaining({
|
||||
response: { output: expectedOutput },
|
||||
}),
|
||||
}),
|
||||
]),
|
||||
}),
|
||||
}),
|
||||
]);
|
||||
|
||||
// Ensure no confirmation UI was triggered (setPendingHistoryItem should not have been called with confirmation details)
|
||||
const setPendingHistoryItemCalls = setPendingHistoryItem.mock.calls;
|
||||
const confirmationCall = setPendingHistoryItemCalls.find((call) => {
|
||||
const item = typeof call[0] === 'function' ? call[0]({}) : call[0];
|
||||
return item?.tools?.[0]?.confirmationDetails;
|
||||
});
|
||||
expect(confirmationCall).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe('useReactToolScheduler', () => {
|
||||
// TODO(ntaylormullen): The following tests are skipped due to difficulties in
|
||||
// reliably testing the asynchronous state updates and interactions with timers.
|
||||
|
||||
Reference in New Issue
Block a user