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:
parent
42bedbc3d3
commit
0795e55f0e
|
@ -15,6 +15,7 @@ import {
|
||||||
ConfigParameters,
|
ConfigParameters,
|
||||||
setGeminiMdFilename as setServerGeminiMdFilename,
|
setGeminiMdFilename as setServerGeminiMdFilename,
|
||||||
getCurrentGeminiMdFilename,
|
getCurrentGeminiMdFilename,
|
||||||
|
ApprovalMode,
|
||||||
} from '@gemini-code/core';
|
} from '@gemini-code/core';
|
||||||
import { Settings } from './settings.js';
|
import { Settings } from './settings.js';
|
||||||
import { readPackageUp } from 'read-package-up';
|
import { readPackageUp } from 'read-package-up';
|
||||||
|
@ -38,6 +39,7 @@ interface CliArgs {
|
||||||
prompt: string | undefined;
|
prompt: string | undefined;
|
||||||
all_files: boolean | undefined;
|
all_files: boolean | undefined;
|
||||||
show_memory_usage: boolean | undefined;
|
show_memory_usage: boolean | undefined;
|
||||||
|
yolo: boolean | undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function parseArguments(): Promise<CliArgs> {
|
async function parseArguments(): Promise<CliArgs> {
|
||||||
|
@ -75,6 +77,13 @@ async function parseArguments(): Promise<CliArgs> {
|
||||||
description: 'Show memory usage in status bar',
|
description: 'Show memory usage in status bar',
|
||||||
default: false,
|
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
|
.version() // This will enable the --version flag based on package.json
|
||||||
.help()
|
.help()
|
||||||
.alias('h', 'help')
|
.alias('h', 'help')
|
||||||
|
@ -158,7 +167,7 @@ export async function loadCliConfig(settings: Settings): Promise<Config> {
|
||||||
const configParams: ConfigParameters = {
|
const configParams: ConfigParameters = {
|
||||||
apiKey: apiKeyForServer,
|
apiKey: apiKeyForServer,
|
||||||
model: argv.model || DEFAULT_GEMINI_MODEL,
|
model: argv.model || DEFAULT_GEMINI_MODEL,
|
||||||
sandbox: argv.sandbox ?? settings.sandbox ?? false,
|
sandbox: argv.sandbox ?? settings.sandbox ?? argv.yolo ?? false,
|
||||||
targetDir: process.cwd(),
|
targetDir: process.cwd(),
|
||||||
debugMode,
|
debugMode,
|
||||||
question: argv.prompt || '',
|
question: argv.prompt || '',
|
||||||
|
@ -171,6 +180,7 @@ export async function loadCliConfig(settings: Settings): Promise<Config> {
|
||||||
userAgent,
|
userAgent,
|
||||||
userMemory: memoryContent,
|
userMemory: memoryContent,
|
||||||
geminiMdFileCount: fileCount,
|
geminiMdFileCount: fileCount,
|
||||||
|
approvalMode: argv.yolo || false ? ApprovalMode.YOLO : ApprovalMode.DEFAULT,
|
||||||
vertexai: useVertexAI,
|
vertexai: useVertexAI,
|
||||||
showMemoryUsage:
|
showMemoryUsage:
|
||||||
argv.show_memory_usage || settings.showMemoryUsage || false,
|
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 { render } from 'ink-testing-library';
|
||||||
import { App } from './App.js';
|
import { App } from './App.js';
|
||||||
import { Config as ServerConfig, MCPServerConfig } from '@gemini-code/core';
|
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';
|
import { LoadedSettings, SettingsFile, Settings } from '../config/settings.js';
|
||||||
|
|
||||||
// Define a more complete mock server config based on actual Config
|
// Define a more complete mock server config based on actual Config
|
||||||
|
@ -28,7 +28,7 @@ interface MockServerConfig {
|
||||||
userAgent: string;
|
userAgent: string;
|
||||||
userMemory: string;
|
userMemory: string;
|
||||||
geminiMdFileCount: number;
|
geminiMdFileCount: number;
|
||||||
alwaysSkipModificationConfirmation: boolean;
|
approvalMode: ApprovalMode;
|
||||||
vertexai?: boolean;
|
vertexai?: boolean;
|
||||||
showMemoryUsage?: boolean;
|
showMemoryUsage?: boolean;
|
||||||
|
|
||||||
|
@ -50,8 +50,8 @@ interface MockServerConfig {
|
||||||
setUserMemory: Mock<(newUserMemory: string) => void>;
|
setUserMemory: Mock<(newUserMemory: string) => void>;
|
||||||
getGeminiMdFileCount: Mock<() => number>;
|
getGeminiMdFileCount: Mock<() => number>;
|
||||||
setGeminiMdFileCount: Mock<(count: number) => void>;
|
setGeminiMdFileCount: Mock<(count: number) => void>;
|
||||||
getAlwaysSkipModificationConfirmation: Mock<() => boolean>;
|
getApprovalMode: Mock<() => ApprovalMode>;
|
||||||
setAlwaysSkipModificationConfirmation: Mock<(skip: boolean) => void>;
|
setApprovalMode: Mock<(skip: ApprovalMode) => void>;
|
||||||
getVertexAI: Mock<() => boolean | undefined>;
|
getVertexAI: Mock<() => boolean | undefined>;
|
||||||
getShowMemoryUsage: Mock<() => boolean>;
|
getShowMemoryUsage: Mock<() => boolean>;
|
||||||
}
|
}
|
||||||
|
@ -80,8 +80,7 @@ vi.mock('@gemini-code/core', async (importOriginal) => {
|
||||||
userAgent: opts.userAgent || 'test-agent',
|
userAgent: opts.userAgent || 'test-agent',
|
||||||
userMemory: opts.userMemory || '',
|
userMemory: opts.userMemory || '',
|
||||||
geminiMdFileCount: opts.geminiMdFileCount || 0,
|
geminiMdFileCount: opts.geminiMdFileCount || 0,
|
||||||
alwaysSkipModificationConfirmation:
|
approvalMode: opts.approvalMode ?? ApprovalMode.DEFAULT,
|
||||||
opts.alwaysSkipModificationConfirmation ?? false,
|
|
||||||
vertexai: opts.vertexai,
|
vertexai: opts.vertexai,
|
||||||
showMemoryUsage: opts.showMemoryUsage ?? false,
|
showMemoryUsage: opts.showMemoryUsage ?? false,
|
||||||
|
|
||||||
|
@ -105,10 +104,8 @@ vi.mock('@gemini-code/core', async (importOriginal) => {
|
||||||
setUserMemory: vi.fn(),
|
setUserMemory: vi.fn(),
|
||||||
getGeminiMdFileCount: vi.fn(() => opts.geminiMdFileCount || 0),
|
getGeminiMdFileCount: vi.fn(() => opts.geminiMdFileCount || 0),
|
||||||
setGeminiMdFileCount: vi.fn(),
|
setGeminiMdFileCount: vi.fn(),
|
||||||
getAlwaysSkipModificationConfirmation: vi.fn(
|
getApprovalMode: vi.fn(() => opts.approvalMode ?? ApprovalMode.DEFAULT),
|
||||||
() => opts.alwaysSkipModificationConfirmation ?? false,
|
setApprovalMode: vi.fn(),
|
||||||
),
|
|
||||||
setAlwaysSkipModificationConfirmation: vi.fn(),
|
|
||||||
getVertexAI: vi.fn(() => opts.vertexai),
|
getVertexAI: vi.fn(() => opts.vertexai),
|
||||||
getShowMemoryUsage: vi.fn(() => opts.showMemoryUsage ?? false),
|
getShowMemoryUsage: vi.fn(() => opts.showMemoryUsage ?? false),
|
||||||
};
|
};
|
||||||
|
|
|
@ -44,6 +44,7 @@ import {
|
||||||
getErrorMessage,
|
getErrorMessage,
|
||||||
type Config,
|
type Config,
|
||||||
getCurrentGeminiMdFilename,
|
getCurrentGeminiMdFilename,
|
||||||
|
ApprovalMode,
|
||||||
} from '@gemini-code/core';
|
} from '@gemini-code/core';
|
||||||
import { useLogger } from './hooks/useLogger.js';
|
import { useLogger } from './hooks/useLogger.js';
|
||||||
import { StreamingContext } from './contexts/StreamingContext.js';
|
import { StreamingContext } from './contexts/StreamingContext.js';
|
||||||
|
@ -412,9 +413,12 @@ export const App = ({
|
||||||
)}
|
)}
|
||||||
</Box>
|
</Box>
|
||||||
<Box>
|
<Box>
|
||||||
{showAutoAcceptIndicator && !shellModeActive && (
|
{showAutoAcceptIndicator !== ApprovalMode.DEFAULT &&
|
||||||
<AutoAcceptIndicator />
|
!shellModeActive && (
|
||||||
)}
|
<AutoAcceptIndicator
|
||||||
|
approvalMode={showAutoAcceptIndicator}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
{shellModeActive && <ShellModeIndicator />}
|
{shellModeActive && <ShellModeIndicator />}
|
||||||
</Box>
|
</Box>
|
||||||
</Box>
|
</Box>
|
||||||
|
|
|
@ -7,12 +7,41 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { Box, Text } from 'ink';
|
import { Box, Text } from 'ink';
|
||||||
import { Colors } from '../colors.js';
|
import { Colors } from '../colors.js';
|
||||||
|
import { ApprovalMode } from '@gemini-code/core';
|
||||||
|
|
||||||
export const AutoAcceptIndicator: React.FC = () => (
|
interface AutoAcceptIndicatorProps {
|
||||||
<Box>
|
approvalMode: ApprovalMode;
|
||||||
<Text color={Colors.AccentGreen}>
|
}
|
||||||
accepting edits
|
|
||||||
<Text color={Colors.SubtleComment}> (shift + tab to disable)</Text>
|
export const AutoAcceptIndicator: React.FC<AutoAcceptIndicatorProps> = ({
|
||||||
</Text>
|
approvalMode,
|
||||||
</Box>
|
}) => {
|
||||||
);
|
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 { renderHook, act } from '@testing-library/react';
|
||||||
import { useAutoAcceptIndicator } from './useAutoAcceptIndicator.js';
|
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';
|
import { useInput, type Key as InkKey } from 'ink';
|
||||||
|
|
||||||
vi.mock('ink');
|
vi.mock('ink');
|
||||||
|
@ -31,11 +35,9 @@ vi.mock('@gemini-code/core', async () => {
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
import { Config } from '@gemini-code/core';
|
|
||||||
|
|
||||||
interface MockConfigInstanceShape {
|
interface MockConfigInstanceShape {
|
||||||
getAlwaysSkipModificationConfirmation: Mock<() => boolean>;
|
getApprovalMode: Mock<() => ApprovalMode>;
|
||||||
setAlwaysSkipModificationConfirmation: Mock<(value: boolean) => void>;
|
setApprovalMode: Mock<(value: ApprovalMode) => void>;
|
||||||
getCoreTools: Mock<() => string[]>;
|
getCoreTools: Mock<() => string[]>;
|
||||||
getToolDiscoveryCommand: Mock<() => string | undefined>;
|
getToolDiscoveryCommand: Mock<() => string | undefined>;
|
||||||
getTargetDir: Mock<() => string>;
|
getTargetDir: Mock<() => string>;
|
||||||
|
@ -65,14 +67,16 @@ describe('useAutoAcceptIndicator', () => {
|
||||||
(
|
(
|
||||||
Config as unknown as MockedFunction<() => MockConfigInstanceShape>
|
Config as unknown as MockedFunction<() => MockConfigInstanceShape>
|
||||||
).mockImplementation(() => {
|
).mockImplementation(() => {
|
||||||
const instanceGetAlwaysSkipMock = vi.fn();
|
const instanceGetApprovalModeMock = vi.fn();
|
||||||
const instanceSetAlwaysSkipMock = vi.fn();
|
const instanceSetApprovalModeMock = vi.fn();
|
||||||
|
|
||||||
const instance: MockConfigInstanceShape = {
|
const instance: MockConfigInstanceShape = {
|
||||||
getAlwaysSkipModificationConfirmation:
|
getApprovalMode: instanceGetApprovalModeMock as Mock<
|
||||||
instanceGetAlwaysSkipMock as Mock<() => boolean>,
|
() => ApprovalMode
|
||||||
setAlwaysSkipModificationConfirmation:
|
>,
|
||||||
instanceSetAlwaysSkipMock as Mock<(value: boolean) => void>,
|
setApprovalMode: instanceSetApprovalModeMock as Mock<
|
||||||
|
(value: ApprovalMode) => void
|
||||||
|
>,
|
||||||
getCoreTools: vi.fn().mockReturnValue([]) as Mock<() => string[]>,
|
getCoreTools: vi.fn().mockReturnValue([]) as Mock<() => string[]>,
|
||||||
getToolDiscoveryCommand: vi.fn().mockReturnValue(undefined) as Mock<
|
getToolDiscoveryCommand: vi.fn().mockReturnValue(undefined) as Mock<
|
||||||
() => string | undefined
|
() => string | undefined
|
||||||
|
@ -101,8 +105,8 @@ describe('useAutoAcceptIndicator', () => {
|
||||||
() => { discoverTools: Mock<() => void> }
|
() => { discoverTools: Mock<() => void> }
|
||||||
>,
|
>,
|
||||||
};
|
};
|
||||||
instanceSetAlwaysSkipMock.mockImplementation((value: boolean) => {
|
instanceSetApprovalModeMock.mockImplementation((value: ApprovalMode) => {
|
||||||
instanceGetAlwaysSkipMock.mockReturnValue(value);
|
instanceGetApprovalModeMock.mockReturnValue(value);
|
||||||
});
|
});
|
||||||
return instance;
|
return instance;
|
||||||
});
|
});
|
||||||
|
@ -116,68 +120,99 @@ describe('useAutoAcceptIndicator', () => {
|
||||||
mockConfigInstance = new (Config as any)() as MockConfigInstanceShape;
|
mockConfigInstance = new (Config as any)() as MockConfigInstanceShape;
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should initialize with true if config.getAlwaysSkipModificationConfirmation returns true', () => {
|
it('should initialize with ApprovalMode.AUTO_EDIT if config.getApprovalMode returns ApprovalMode.AUTO_EDIT', () => {
|
||||||
mockConfigInstance.getAlwaysSkipModificationConfirmation.mockReturnValue(
|
mockConfigInstance.getApprovalMode.mockReturnValue(ApprovalMode.AUTO_EDIT);
|
||||||
true,
|
|
||||||
);
|
|
||||||
const { result } = renderHook(() =>
|
const { result } = renderHook(() =>
|
||||||
useAutoAcceptIndicator({
|
useAutoAcceptIndicator({
|
||||||
config: mockConfigInstance as unknown as ActualConfigType,
|
config: mockConfigInstance as unknown as ActualConfigType,
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
expect(result.current).toBe(true);
|
expect(result.current).toBe(ApprovalMode.AUTO_EDIT);
|
||||||
expect(
|
expect(mockConfigInstance.getApprovalMode).toHaveBeenCalledTimes(1);
|
||||||
mockConfigInstance.getAlwaysSkipModificationConfirmation,
|
|
||||||
).toHaveBeenCalledTimes(1);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should initialize with false if config.getAlwaysSkipModificationConfirmation returns false', () => {
|
it('should initialize with ApprovalMode.DEFAULT if config.getApprovalMode returns ApprovalMode.DEFAULT', () => {
|
||||||
mockConfigInstance.getAlwaysSkipModificationConfirmation.mockReturnValue(
|
mockConfigInstance.getApprovalMode.mockReturnValue(ApprovalMode.DEFAULT);
|
||||||
false,
|
|
||||||
);
|
|
||||||
const { result } = renderHook(() =>
|
const { result } = renderHook(() =>
|
||||||
useAutoAcceptIndicator({
|
useAutoAcceptIndicator({
|
||||||
config: mockConfigInstance as unknown as ActualConfigType,
|
config: mockConfigInstance as unknown as ActualConfigType,
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
expect(result.current).toBe(false);
|
expect(result.current).toBe(ApprovalMode.DEFAULT);
|
||||||
expect(
|
expect(mockConfigInstance.getApprovalMode).toHaveBeenCalledTimes(1);
|
||||||
mockConfigInstance.getAlwaysSkipModificationConfirmation,
|
|
||||||
).toHaveBeenCalledTimes(1);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should toggle the indicator and update config when Shift+Tab is pressed', () => {
|
it('should initialize with ApprovalMode.YOLO if config.getApprovalMode returns ApprovalMode.YOLO', () => {
|
||||||
mockConfigInstance.getAlwaysSkipModificationConfirmation.mockReturnValue(
|
mockConfigInstance.getApprovalMode.mockReturnValue(ApprovalMode.YOLO);
|
||||||
false,
|
|
||||||
);
|
|
||||||
const { result } = renderHook(() =>
|
const { result } = renderHook(() =>
|
||||||
useAutoAcceptIndicator({
|
useAutoAcceptIndicator({
|
||||||
config: mockConfigInstance as unknown as ActualConfigType,
|
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(() => {
|
act(() => {
|
||||||
capturedUseInputHandler('', { tab: true, shift: true } as InkKey);
|
capturedUseInputHandler('', { tab: true, shift: true } as InkKey);
|
||||||
});
|
});
|
||||||
expect(
|
expect(mockConfigInstance.setApprovalMode).toHaveBeenCalledWith(
|
||||||
mockConfigInstance.setAlwaysSkipModificationConfirmation,
|
ApprovalMode.AUTO_EDIT,
|
||||||
).toHaveBeenCalledWith(true);
|
);
|
||||||
expect(result.current).toBe(true);
|
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(() => {
|
act(() => {
|
||||||
capturedUseInputHandler('', { tab: true, shift: true } as InkKey);
|
capturedUseInputHandler('', { tab: true, shift: true } as InkKey);
|
||||||
});
|
});
|
||||||
expect(
|
expect(mockConfigInstance.setApprovalMode).toHaveBeenCalledWith(
|
||||||
mockConfigInstance.setAlwaysSkipModificationConfirmation,
|
ApprovalMode.AUTO_EDIT,
|
||||||
).toHaveBeenCalledWith(false);
|
);
|
||||||
expect(result.current).toBe(false);
|
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', () => {
|
it('should not toggle if only one key or other keys combinations are pressed', () => {
|
||||||
mockConfigInstance.getAlwaysSkipModificationConfirmation.mockReturnValue(
|
mockConfigInstance.getApprovalMode.mockReturnValue(ApprovalMode.DEFAULT);
|
||||||
false,
|
|
||||||
);
|
|
||||||
renderHook(() =>
|
renderHook(() =>
|
||||||
useAutoAcceptIndicator({
|
useAutoAcceptIndicator({
|
||||||
config: mockConfigInstance as unknown as ActualConfigType,
|
config: mockConfigInstance as unknown as ActualConfigType,
|
||||||
|
@ -187,29 +222,41 @@ describe('useAutoAcceptIndicator', () => {
|
||||||
act(() => {
|
act(() => {
|
||||||
capturedUseInputHandler('', { tab: true, shift: false } as InkKey);
|
capturedUseInputHandler('', { tab: true, shift: false } as InkKey);
|
||||||
});
|
});
|
||||||
expect(
|
expect(mockConfigInstance.setApprovalMode).not.toHaveBeenCalled();
|
||||||
mockConfigInstance.setAlwaysSkipModificationConfirmation,
|
|
||||||
).not.toHaveBeenCalled();
|
|
||||||
|
|
||||||
act(() => {
|
act(() => {
|
||||||
capturedUseInputHandler('', { tab: false, shift: true } as InkKey);
|
capturedUseInputHandler('', { tab: false, shift: true } as InkKey);
|
||||||
});
|
});
|
||||||
expect(
|
expect(mockConfigInstance.setApprovalMode).not.toHaveBeenCalled();
|
||||||
mockConfigInstance.setAlwaysSkipModificationConfirmation,
|
|
||||||
).not.toHaveBeenCalled();
|
|
||||||
|
|
||||||
act(() => {
|
act(() => {
|
||||||
capturedUseInputHandler('a', { tab: false, shift: false } as InkKey);
|
capturedUseInputHandler('a', { tab: false, shift: false } as InkKey);
|
||||||
});
|
});
|
||||||
expect(
|
expect(mockConfigInstance.setApprovalMode).not.toHaveBeenCalled();
|
||||||
mockConfigInstance.setAlwaysSkipModificationConfirmation,
|
|
||||||
).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)', () => {
|
it('should update indicator when config value changes externally (useEffect dependency)', () => {
|
||||||
mockConfigInstance.getAlwaysSkipModificationConfirmation.mockReturnValue(
|
mockConfigInstance.getApprovalMode.mockReturnValue(ApprovalMode.DEFAULT);
|
||||||
false,
|
|
||||||
);
|
|
||||||
const { result, rerender } = renderHook(
|
const { result, rerender } = renderHook(
|
||||||
(props: { config: ActualConfigType }) => useAutoAcceptIndicator(props),
|
(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(
|
mockConfigInstance.getApprovalMode.mockReturnValue(ApprovalMode.AUTO_EDIT);
|
||||||
true,
|
|
||||||
);
|
|
||||||
|
|
||||||
rerender({ config: mockConfigInstance as unknown as ActualConfigType });
|
rerender({ config: mockConfigInstance as unknown as ActualConfigType });
|
||||||
expect(result.current).toBe(true);
|
expect(result.current).toBe(ApprovalMode.AUTO_EDIT);
|
||||||
expect(
|
expect(mockConfigInstance.getApprovalMode).toHaveBeenCalledTimes(3);
|
||||||
mockConfigInstance.getAlwaysSkipModificationConfirmation,
|
|
||||||
).toHaveBeenCalledTimes(3);
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -6,7 +6,7 @@
|
||||||
|
|
||||||
import { useState, useEffect } from 'react';
|
import { useState, useEffect } from 'react';
|
||||||
import { useInput } from 'ink';
|
import { useInput } from 'ink';
|
||||||
import type { Config } from '@gemini-code/core';
|
import { ApprovalMode, type Config } from '@gemini-code/core';
|
||||||
|
|
||||||
export interface UseAutoAcceptIndicatorArgs {
|
export interface UseAutoAcceptIndicatorArgs {
|
||||||
config: Config;
|
config: Config;
|
||||||
|
@ -14,8 +14,8 @@ export interface UseAutoAcceptIndicatorArgs {
|
||||||
|
|
||||||
export function useAutoAcceptIndicator({
|
export function useAutoAcceptIndicator({
|
||||||
config,
|
config,
|
||||||
}: UseAutoAcceptIndicatorArgs): boolean {
|
}: UseAutoAcceptIndicatorArgs): ApprovalMode {
|
||||||
const currentConfigValue = config.getAlwaysSkipModificationConfirmation();
|
const currentConfigValue = config.getApprovalMode();
|
||||||
const [showAutoAcceptIndicator, setShowAutoAcceptIndicator] =
|
const [showAutoAcceptIndicator, setShowAutoAcceptIndicator] =
|
||||||
useState(currentConfigValue);
|
useState(currentConfigValue);
|
||||||
|
|
||||||
|
@ -23,15 +23,25 @@ export function useAutoAcceptIndicator({
|
||||||
setShowAutoAcceptIndicator(currentConfigValue);
|
setShowAutoAcceptIndicator(currentConfigValue);
|
||||||
}, [currentConfigValue]);
|
}, [currentConfigValue]);
|
||||||
|
|
||||||
useInput((_input, key) => {
|
useInput((input, key) => {
|
||||||
if (key.tab && key.shift) {
|
let nextApprovalMode: ApprovalMode | undefined;
|
||||||
const alwaysAcceptModificationConfirmations =
|
|
||||||
!config.getAlwaysSkipModificationConfirmation();
|
if (key.ctrl && input === 'y') {
|
||||||
config.setAlwaysSkipModificationConfirmation(
|
nextApprovalMode =
|
||||||
alwaysAcceptModificationConfirmations,
|
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
|
// Update local state immediately for responsiveness
|
||||||
setShowAutoAcceptIndicator(alwaysAcceptModificationConfirmations);
|
setShowAutoAcceptIndicator(nextApprovalMode);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -134,6 +134,7 @@ export function useReactToolScheduler(
|
||||||
outputUpdateHandler,
|
outputUpdateHandler,
|
||||||
onAllToolCallsComplete: allToolCallsCompleteHandler,
|
onAllToolCallsComplete: allToolCallsCompleteHandler,
|
||||||
onToolCallsUpdate: toolCallsUpdateHandler,
|
onToolCallsUpdate: toolCallsUpdateHandler,
|
||||||
|
approvalMode: config.getApprovalMode(),
|
||||||
});
|
});
|
||||||
}, [config, onComplete, setPendingHistoryItem]);
|
}, [config, onComplete, setPendingHistoryItem]);
|
||||||
|
|
||||||
|
|
|
@ -28,7 +28,8 @@ import {
|
||||||
ToolCallResponseInfo,
|
ToolCallResponseInfo,
|
||||||
formatLlmContentForFunctionResponse, // Import from core
|
formatLlmContentForFunctionResponse, // Import from core
|
||||||
ToolCall, // Import from core
|
ToolCall, // Import from core
|
||||||
Status as ToolCallStatusType, // Import from core
|
Status as ToolCallStatusType,
|
||||||
|
ApprovalMode, // Import from core
|
||||||
} from '@gemini-code/core';
|
} from '@gemini-code/core';
|
||||||
import {
|
import {
|
||||||
HistoryItemWithoutId,
|
HistoryItemWithoutId,
|
||||||
|
@ -52,6 +53,7 @@ const mockToolRegistry = {
|
||||||
|
|
||||||
const mockConfig = {
|
const mockConfig = {
|
||||||
getToolRegistry: vi.fn(() => mockToolRegistry as unknown as ToolRegistry),
|
getToolRegistry: vi.fn(() => mockToolRegistry as unknown as ToolRegistry),
|
||||||
|
getApprovalMode: vi.fn(() => ApprovalMode.DEFAULT),
|
||||||
};
|
};
|
||||||
|
|
||||||
const mockTool: Tool = {
|
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', () => {
|
describe('useReactToolScheduler', () => {
|
||||||
// TODO(ntaylormullen): The following tests are skipped due to difficulties in
|
// TODO(ntaylormullen): The following tests are skipped due to difficulties in
|
||||||
// reliably testing the asynchronous state updates and interactions with timers.
|
// reliably testing the asynchronous state updates and interactions with timers.
|
||||||
|
|
|
@ -22,6 +22,12 @@ import { ReadManyFilesTool } from '../tools/read-many-files.js';
|
||||||
import { MemoryTool, setGeminiMdFilename } from '../tools/memoryTool.js';
|
import { MemoryTool, setGeminiMdFilename } from '../tools/memoryTool.js';
|
||||||
import { WebSearchTool } from '../tools/web-search.js';
|
import { WebSearchTool } from '../tools/web-search.js';
|
||||||
|
|
||||||
|
export enum ApprovalMode {
|
||||||
|
DEFAULT = 'default',
|
||||||
|
AUTO_EDIT = 'autoEdit',
|
||||||
|
YOLO = 'yolo',
|
||||||
|
}
|
||||||
|
|
||||||
export class MCPServerConfig {
|
export class MCPServerConfig {
|
||||||
constructor(
|
constructor(
|
||||||
// For stdio transport
|
// For stdio transport
|
||||||
|
@ -53,7 +59,7 @@ export interface ConfigParameters {
|
||||||
userAgent: string;
|
userAgent: string;
|
||||||
userMemory?: string;
|
userMemory?: string;
|
||||||
geminiMdFileCount?: number;
|
geminiMdFileCount?: number;
|
||||||
alwaysSkipModificationConfirmation?: boolean;
|
approvalMode?: ApprovalMode;
|
||||||
vertexai?: boolean;
|
vertexai?: boolean;
|
||||||
showMemoryUsage?: boolean;
|
showMemoryUsage?: boolean;
|
||||||
contextFileName?: string;
|
contextFileName?: string;
|
||||||
|
@ -76,7 +82,7 @@ export class Config {
|
||||||
private readonly userAgent: string;
|
private readonly userAgent: string;
|
||||||
private userMemory: string;
|
private userMemory: string;
|
||||||
private geminiMdFileCount: number;
|
private geminiMdFileCount: number;
|
||||||
private alwaysSkipModificationConfirmation: boolean;
|
private approvalMode: ApprovalMode;
|
||||||
private readonly vertexai: boolean | undefined;
|
private readonly vertexai: boolean | undefined;
|
||||||
private readonly showMemoryUsage: boolean;
|
private readonly showMemoryUsage: boolean;
|
||||||
|
|
||||||
|
@ -96,8 +102,7 @@ export class Config {
|
||||||
this.userAgent = params.userAgent;
|
this.userAgent = params.userAgent;
|
||||||
this.userMemory = params.userMemory ?? '';
|
this.userMemory = params.userMemory ?? '';
|
||||||
this.geminiMdFileCount = params.geminiMdFileCount ?? 0;
|
this.geminiMdFileCount = params.geminiMdFileCount ?? 0;
|
||||||
this.alwaysSkipModificationConfirmation =
|
this.approvalMode = params.approvalMode ?? ApprovalMode.DEFAULT;
|
||||||
params.alwaysSkipModificationConfirmation ?? false;
|
|
||||||
this.vertexai = params.vertexai;
|
this.vertexai = params.vertexai;
|
||||||
this.showMemoryUsage = params.showMemoryUsage ?? false;
|
this.showMemoryUsage = params.showMemoryUsage ?? false;
|
||||||
|
|
||||||
|
@ -179,12 +184,12 @@ export class Config {
|
||||||
this.geminiMdFileCount = count;
|
this.geminiMdFileCount = count;
|
||||||
}
|
}
|
||||||
|
|
||||||
getAlwaysSkipModificationConfirmation(): boolean {
|
getApprovalMode(): ApprovalMode {
|
||||||
return this.alwaysSkipModificationConfirmation;
|
return this.approvalMode;
|
||||||
}
|
}
|
||||||
|
|
||||||
setAlwaysSkipModificationConfirmation(skip: boolean): void {
|
setApprovalMode(mode: ApprovalMode): void {
|
||||||
this.alwaysSkipModificationConfirmation = skip;
|
this.approvalMode = mode;
|
||||||
}
|
}
|
||||||
|
|
||||||
getVertexAI(): boolean | undefined {
|
getVertexAI(): boolean | undefined {
|
||||||
|
|
|
@ -12,6 +12,7 @@ import {
|
||||||
ToolCallConfirmationDetails,
|
ToolCallConfirmationDetails,
|
||||||
ToolResult,
|
ToolResult,
|
||||||
ToolRegistry,
|
ToolRegistry,
|
||||||
|
ApprovalMode,
|
||||||
} from '../index.js';
|
} from '../index.js';
|
||||||
import { Part, PartUnion, PartListUnion } from '@google/genai';
|
import { Part, PartUnion, PartListUnion } from '@google/genai';
|
||||||
|
|
||||||
|
@ -159,6 +160,7 @@ interface CoreToolSchedulerOptions {
|
||||||
outputUpdateHandler?: OutputUpdateHandler;
|
outputUpdateHandler?: OutputUpdateHandler;
|
||||||
onAllToolCallsComplete?: AllToolCallsCompleteHandler;
|
onAllToolCallsComplete?: AllToolCallsCompleteHandler;
|
||||||
onToolCallsUpdate?: ToolCallsUpdateHandler;
|
onToolCallsUpdate?: ToolCallsUpdateHandler;
|
||||||
|
approvalMode?: ApprovalMode;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class CoreToolScheduler {
|
export class CoreToolScheduler {
|
||||||
|
@ -168,12 +170,14 @@ export class CoreToolScheduler {
|
||||||
private outputUpdateHandler?: OutputUpdateHandler;
|
private outputUpdateHandler?: OutputUpdateHandler;
|
||||||
private onAllToolCallsComplete?: AllToolCallsCompleteHandler;
|
private onAllToolCallsComplete?: AllToolCallsCompleteHandler;
|
||||||
private onToolCallsUpdate?: ToolCallsUpdateHandler;
|
private onToolCallsUpdate?: ToolCallsUpdateHandler;
|
||||||
|
private approvalMode: ApprovalMode;
|
||||||
|
|
||||||
constructor(options: CoreToolSchedulerOptions) {
|
constructor(options: CoreToolSchedulerOptions) {
|
||||||
this.toolRegistry = options.toolRegistry;
|
this.toolRegistry = options.toolRegistry;
|
||||||
this.outputUpdateHandler = options.outputUpdateHandler;
|
this.outputUpdateHandler = options.outputUpdateHandler;
|
||||||
this.onAllToolCallsComplete = options.onAllToolCallsComplete;
|
this.onAllToolCallsComplete = options.onAllToolCallsComplete;
|
||||||
this.onToolCallsUpdate = options.onToolCallsUpdate;
|
this.onToolCallsUpdate = options.onToolCallsUpdate;
|
||||||
|
this.approvalMode = options.approvalMode ?? ApprovalMode.DEFAULT;
|
||||||
this.abortController = new AbortController();
|
this.abortController = new AbortController();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -324,29 +328,33 @@ export class CoreToolScheduler {
|
||||||
|
|
||||||
const { request: reqInfo, tool: toolInstance } = toolCall;
|
const { request: reqInfo, tool: toolInstance } = toolCall;
|
||||||
try {
|
try {
|
||||||
const confirmationDetails = await toolInstance.shouldConfirmExecute(
|
if (this.approvalMode === ApprovalMode.YOLO) {
|
||||||
reqInfo.args,
|
|
||||||
this.abortController.signal,
|
|
||||||
);
|
|
||||||
|
|
||||||
if (confirmationDetails) {
|
|
||||||
const originalOnConfirm = confirmationDetails.onConfirm;
|
|
||||||
const wrappedConfirmationDetails: ToolCallConfirmationDetails = {
|
|
||||||
...confirmationDetails,
|
|
||||||
onConfirm: (outcome: ToolConfirmationOutcome) =>
|
|
||||||
this.handleConfirmationResponse(
|
|
||||||
reqInfo.callId,
|
|
||||||
originalOnConfirm,
|
|
||||||
outcome,
|
|
||||||
),
|
|
||||||
};
|
|
||||||
this.setStatusInternal(
|
|
||||||
reqInfo.callId,
|
|
||||||
'awaiting_approval',
|
|
||||||
wrappedConfirmationDetails,
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
this.setStatusInternal(reqInfo.callId, 'scheduled');
|
this.setStatusInternal(reqInfo.callId, 'scheduled');
|
||||||
|
} else {
|
||||||
|
const confirmationDetails = await toolInstance.shouldConfirmExecute(
|
||||||
|
reqInfo.args,
|
||||||
|
this.abortController.signal,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (confirmationDetails) {
|
||||||
|
const originalOnConfirm = confirmationDetails.onConfirm;
|
||||||
|
const wrappedConfirmationDetails: ToolCallConfirmationDetails = {
|
||||||
|
...confirmationDetails,
|
||||||
|
onConfirm: (outcome: ToolConfirmationOutcome) =>
|
||||||
|
this.handleConfirmationResponse(
|
||||||
|
reqInfo.callId,
|
||||||
|
originalOnConfirm,
|
||||||
|
outcome,
|
||||||
|
),
|
||||||
|
};
|
||||||
|
this.setStatusInternal(
|
||||||
|
reqInfo.callId,
|
||||||
|
'awaiting_approval',
|
||||||
|
wrappedConfirmationDetails,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
this.setStatusInternal(reqInfo.callId, 'scheduled');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
this.setStatusInternal(
|
this.setStatusInternal(
|
||||||
|
|
|
@ -25,7 +25,7 @@ import { FileDiff } from './tools.js';
|
||||||
import path from 'path';
|
import path from 'path';
|
||||||
import fs from 'fs';
|
import fs from 'fs';
|
||||||
import os from 'os';
|
import os from 'os';
|
||||||
import { Config } from '../config/config.js';
|
import { ApprovalMode, Config } from '../config/config.js';
|
||||||
import { Content, Part, SchemaUnion } from '@google/genai';
|
import { Content, Part, SchemaUnion } from '@google/genai';
|
||||||
|
|
||||||
describe('EditTool', () => {
|
describe('EditTool', () => {
|
||||||
|
@ -41,8 +41,8 @@ describe('EditTool', () => {
|
||||||
|
|
||||||
mockConfig = {
|
mockConfig = {
|
||||||
getTargetDir: () => rootDir,
|
getTargetDir: () => rootDir,
|
||||||
getAlwaysSkipModificationConfirmation: vi.fn(() => false),
|
getApprovalMode: vi.fn(() => false),
|
||||||
setAlwaysSkipModificationConfirmation: vi.fn(),
|
setApprovalMode: vi.fn(),
|
||||||
// getGeminiConfig: () => ({ apiKey: 'test-api-key' }), // This was not a real Config method
|
// getGeminiConfig: () => ({ apiKey: 'test-api-key' }), // This was not a real Config method
|
||||||
// Add other properties/methods of Config if EditTool uses them
|
// Add other properties/methods of Config if EditTool uses them
|
||||||
// Minimal other methods to satisfy Config type if needed by EditTool constructor or other direct uses:
|
// Minimal other methods to satisfy Config type if needed by EditTool constructor or other direct uses:
|
||||||
|
@ -65,12 +65,10 @@ describe('EditTool', () => {
|
||||||
} as unknown as Config;
|
} as unknown as Config;
|
||||||
|
|
||||||
// Reset mocks before each test
|
// Reset mocks before each test
|
||||||
(mockConfig.getAlwaysSkipModificationConfirmation as Mock).mockClear();
|
(mockConfig.getApprovalMode as Mock).mockClear();
|
||||||
(mockConfig.setAlwaysSkipModificationConfirmation as Mock).mockClear();
|
(mockConfig.getApprovalMode as Mock).mockClear();
|
||||||
// Default to not skipping confirmation
|
// Default to not skipping confirmation
|
||||||
(mockConfig.getAlwaysSkipModificationConfirmation as Mock).mockReturnValue(
|
(mockConfig.getApprovalMode as Mock).mockReturnValue(ApprovalMode.DEFAULT);
|
||||||
false,
|
|
||||||
);
|
|
||||||
|
|
||||||
// Reset mocks and set default implementation for ensureCorrectEdit
|
// Reset mocks and set default implementation for ensureCorrectEdit
|
||||||
mockEnsureCorrectEdit.mockReset();
|
mockEnsureCorrectEdit.mockReset();
|
||||||
|
@ -439,9 +437,9 @@ describe('EditTool', () => {
|
||||||
new_string: fileContent,
|
new_string: fileContent,
|
||||||
};
|
};
|
||||||
|
|
||||||
(
|
(mockConfig.getApprovalMode as Mock).mockReturnValueOnce(
|
||||||
mockConfig.getAlwaysSkipModificationConfirmation as Mock
|
ApprovalMode.AUTO_EDIT,
|
||||||
).mockReturnValueOnce(true);
|
);
|
||||||
const result = await tool.execute(params, new AbortController().signal);
|
const result = await tool.execute(params, new AbortController().signal);
|
||||||
|
|
||||||
expect(result.llmContent).toMatch(/Created new file/);
|
expect(result.llmContent).toMatch(/Created new file/);
|
||||||
|
|
|
@ -20,7 +20,7 @@ import { makeRelative, shortenPath } from '../utils/paths.js';
|
||||||
import { isNodeError } from '../utils/errors.js';
|
import { isNodeError } from '../utils/errors.js';
|
||||||
import { ReadFileTool } from './read-file.js';
|
import { ReadFileTool } from './read-file.js';
|
||||||
import { GeminiClient } from '../core/client.js';
|
import { GeminiClient } from '../core/client.js';
|
||||||
import { Config } from '../config/config.js';
|
import { Config, ApprovalMode } from '../config/config.js';
|
||||||
import { ensureCorrectEdit } from '../utils/editCorrector.js';
|
import { ensureCorrectEdit } from '../utils/editCorrector.js';
|
||||||
import { DEFAULT_DIFF_OPTIONS } from './diffOptions.js';
|
import { DEFAULT_DIFF_OPTIONS } from './diffOptions.js';
|
||||||
|
|
||||||
|
@ -281,7 +281,7 @@ Expectation for required parameters:
|
||||||
params: EditToolParams,
|
params: EditToolParams,
|
||||||
abortSignal: AbortSignal,
|
abortSignal: AbortSignal,
|
||||||
): Promise<ToolCallConfirmationDetails | false> {
|
): Promise<ToolCallConfirmationDetails | false> {
|
||||||
if (this.config.getAlwaysSkipModificationConfirmation()) {
|
if (this.config.getApprovalMode() === ApprovalMode.AUTO_EDIT) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
const validationError = this.validateToolParams(params);
|
const validationError = this.validateToolParams(params);
|
||||||
|
@ -356,7 +356,7 @@ Expectation for required parameters:
|
||||||
fileDiff,
|
fileDiff,
|
||||||
onConfirm: async (outcome: ToolConfirmationOutcome) => {
|
onConfirm: async (outcome: ToolConfirmationOutcome) => {
|
||||||
if (outcome === ToolConfirmationOutcome.ProceedAlways) {
|
if (outcome === ToolConfirmationOutcome.ProceedAlways) {
|
||||||
this.config.setAlwaysSkipModificationConfirmation(true);
|
this.config.setApprovalMode(ApprovalMode.AUTO_EDIT);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
|
@ -16,7 +16,7 @@ import {
|
||||||
} from 'vitest';
|
} from 'vitest';
|
||||||
import { ToolRegistry, DiscoveredTool } from './tool-registry.js';
|
import { ToolRegistry, DiscoveredTool } from './tool-registry.js';
|
||||||
import { DiscoveredMCPTool } from './mcp-tool.js';
|
import { DiscoveredMCPTool } from './mcp-tool.js';
|
||||||
import { Config, ConfigParameters } from '../config/config.js';
|
import { ApprovalMode, Config, ConfigParameters } from '../config/config.js';
|
||||||
import { BaseTool, ToolResult } from './tools.js';
|
import { BaseTool, ToolResult } from './tools.js';
|
||||||
import { FunctionDeclaration } from '@google/genai';
|
import { FunctionDeclaration } from '@google/genai';
|
||||||
import { execSync, spawn } from 'node:child_process'; // Import spawn here
|
import { execSync, spawn } from 'node:child_process'; // Import spawn here
|
||||||
|
@ -85,7 +85,7 @@ const baseConfigParams: ConfigParameters = {
|
||||||
userAgent: 'TestAgent/1.0',
|
userAgent: 'TestAgent/1.0',
|
||||||
userMemory: '',
|
userMemory: '',
|
||||||
geminiMdFileCount: 0,
|
geminiMdFileCount: 0,
|
||||||
alwaysSkipModificationConfirmation: false,
|
approvalMode: ApprovalMode.DEFAULT,
|
||||||
vertexai: false,
|
vertexai: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -20,7 +20,7 @@ import {
|
||||||
ToolEditConfirmationDetails,
|
ToolEditConfirmationDetails,
|
||||||
} from './tools.js';
|
} from './tools.js';
|
||||||
import { type EditToolParams } from './edit.js';
|
import { type EditToolParams } from './edit.js';
|
||||||
import { Config } from '../config/config.js';
|
import { ApprovalMode, Config } from '../config/config.js';
|
||||||
import { ToolRegistry } from './tool-registry.js';
|
import { ToolRegistry } from './tool-registry.js';
|
||||||
import path from 'path';
|
import path from 'path';
|
||||||
import fs from 'fs';
|
import fs from 'fs';
|
||||||
|
@ -51,8 +51,8 @@ vi.mocked(ensureCorrectFileContent).mockImplementation(
|
||||||
// Mock Config
|
// Mock Config
|
||||||
const mockConfigInternal = {
|
const mockConfigInternal = {
|
||||||
getTargetDir: () => rootDir,
|
getTargetDir: () => rootDir,
|
||||||
getAlwaysSkipModificationConfirmation: vi.fn(() => false),
|
getApprovalMode: vi.fn(() => ApprovalMode.DEFAULT),
|
||||||
setAlwaysSkipModificationConfirmation: vi.fn(),
|
setApprovalMode: vi.fn(),
|
||||||
getApiKey: () => 'test-key',
|
getApiKey: () => 'test-key',
|
||||||
getModel: () => 'test-model',
|
getModel: () => 'test-model',
|
||||||
getSandbox: () => false,
|
getSandbox: () => false,
|
||||||
|
@ -100,10 +100,8 @@ describe('WriteFileTool', () => {
|
||||||
tool = new WriteFileTool(mockConfig);
|
tool = new WriteFileTool(mockConfig);
|
||||||
|
|
||||||
// Reset mocks before each test
|
// Reset mocks before each test
|
||||||
mockConfigInternal.getAlwaysSkipModificationConfirmation.mockReturnValue(
|
mockConfigInternal.getApprovalMode.mockReturnValue(ApprovalMode.DEFAULT);
|
||||||
false,
|
mockConfigInternal.setApprovalMode.mockClear();
|
||||||
);
|
|
||||||
mockConfigInternal.setAlwaysSkipModificationConfirmation.mockClear();
|
|
||||||
mockEnsureCorrectEdit.mockReset();
|
mockEnsureCorrectEdit.mockReset();
|
||||||
mockEnsureCorrectFileContent.mockReset();
|
mockEnsureCorrectFileContent.mockReset();
|
||||||
|
|
||||||
|
|
|
@ -7,7 +7,7 @@
|
||||||
import fs from 'fs';
|
import fs from 'fs';
|
||||||
import path from 'path';
|
import path from 'path';
|
||||||
import * as Diff from 'diff';
|
import * as Diff from 'diff';
|
||||||
import { Config } from '../config/config.js';
|
import { Config, ApprovalMode } from '../config/config.js';
|
||||||
import {
|
import {
|
||||||
BaseTool,
|
BaseTool,
|
||||||
ToolResult,
|
ToolResult,
|
||||||
|
@ -143,7 +143,7 @@ export class WriteFileTool extends BaseTool<WriteFileToolParams, ToolResult> {
|
||||||
params: WriteFileToolParams,
|
params: WriteFileToolParams,
|
||||||
abortSignal: AbortSignal,
|
abortSignal: AbortSignal,
|
||||||
): Promise<ToolCallConfirmationDetails | false> {
|
): Promise<ToolCallConfirmationDetails | false> {
|
||||||
if (this.config.getAlwaysSkipModificationConfirmation()) {
|
if (this.config.getApprovalMode() === ApprovalMode.AUTO_EDIT) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -186,7 +186,7 @@ export class WriteFileTool extends BaseTool<WriteFileToolParams, ToolResult> {
|
||||||
fileDiff,
|
fileDiff,
|
||||||
onConfirm: async (outcome: ToolConfirmationOutcome) => {
|
onConfirm: async (outcome: ToolConfirmationOutcome) => {
|
||||||
if (outcome === ToolConfirmationOutcome.ProceedAlways) {
|
if (outcome === ToolConfirmationOutcome.ProceedAlways) {
|
||||||
this.config.setAlwaysSkipModificationConfirmation(true);
|
this.config.setApprovalMode(ApprovalMode.AUTO_EDIT);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
Loading…
Reference in New Issue