Add FolderTrustDialog that shows on launch and enables folderTrust setting (#5815)

This commit is contained in:
shrutip90 2025-08-08 11:02:27 -07:00 committed by GitHub
parent 3af4913ef3
commit 34b5dc7f28
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 244 additions and 1 deletions

View File

@ -486,8 +486,8 @@ export async function loadCliConfig(
ideModeFeature, ideModeFeature,
chatCompression: settings.chatCompression, chatCompression: settings.chatCompression,
folderTrustFeature, folderTrustFeature,
interactive,
folderTrust, folderTrust,
interactive,
}); });
} }

View File

@ -203,6 +203,13 @@ vi.mock('./hooks/useAuthCommand', () => ({
})), })),
})); }));
vi.mock('./hooks/useFolderTrust', () => ({
useFolderTrust: vi.fn(() => ({
isFolderTrustDialogOpen: false,
handleFolderTrustSelect: vi.fn(),
})),
}));
vi.mock('./hooks/useLogger', () => ({ vi.mock('./hooks/useLogger', () => ({
useLogger: vi.fn(() => ({ useLogger: vi.fn(() => ({
getPreviousUserMessages: vi.fn().mockResolvedValue([]), getPreviousUserMessages: vi.fn().mockResolvedValue([]),
@ -1091,4 +1098,25 @@ describe('App UI', () => {
expect(lastFrame()).toMatchSnapshot(); expect(lastFrame()).toMatchSnapshot();
}); });
}); });
describe('FolderTrustDialog', () => {
it('should display the folder trust dialog when isFolderTrustDialogOpen is true', async () => {
const { useFolderTrust } = await import('./hooks/useFolderTrust.js');
vi.mocked(useFolderTrust).mockReturnValue({
isFolderTrustDialogOpen: true,
handleFolderTrustSelect: vi.fn(),
});
const { lastFrame, unmount } = render(
<App
config={mockConfig as unknown as ServerConfig}
settings={mockSettings}
version={mockVersion}
/>,
);
currentUnmount = unmount;
await Promise.resolve();
expect(lastFrame()).toContain('Do you trust this folder?');
});
});
}); });

View File

@ -22,6 +22,7 @@ import { useGeminiStream } from './hooks/useGeminiStream.js';
import { useLoadingIndicator } from './hooks/useLoadingIndicator.js'; import { useLoadingIndicator } from './hooks/useLoadingIndicator.js';
import { useThemeCommand } from './hooks/useThemeCommand.js'; import { useThemeCommand } from './hooks/useThemeCommand.js';
import { useAuthCommand } from './hooks/useAuthCommand.js'; import { useAuthCommand } from './hooks/useAuthCommand.js';
import { useFolderTrust } from './hooks/useFolderTrust.js';
import { useEditorSettings } from './hooks/useEditorSettings.js'; import { useEditorSettings } from './hooks/useEditorSettings.js';
import { useSlashCommandProcessor } from './hooks/slashCommandProcessor.js'; import { useSlashCommandProcessor } from './hooks/slashCommandProcessor.js';
import { useAutoAcceptIndicator } from './hooks/useAutoAcceptIndicator.js'; import { useAutoAcceptIndicator } from './hooks/useAutoAcceptIndicator.js';
@ -36,6 +37,7 @@ import { ThemeDialog } from './components/ThemeDialog.js';
import { AuthDialog } from './components/AuthDialog.js'; import { AuthDialog } from './components/AuthDialog.js';
import { AuthInProgress } from './components/AuthInProgress.js'; import { AuthInProgress } from './components/AuthInProgress.js';
import { EditorSettingsDialog } from './components/EditorSettingsDialog.js'; import { EditorSettingsDialog } from './components/EditorSettingsDialog.js';
import { FolderTrustDialog } from './components/FolderTrustDialog.js';
import { ShellConfirmationDialog } from './components/ShellConfirmationDialog.js'; import { ShellConfirmationDialog } from './components/ShellConfirmationDialog.js';
import { Colors } from './colors.js'; import { Colors } from './colors.js';
import { loadHierarchicalGeminiMemory } from '../config/config.js'; import { loadHierarchicalGeminiMemory } from '../config/config.js';
@ -240,6 +242,9 @@ const App = ({ config, settings, startupWarnings = [], version }: AppProps) => {
handleThemeHighlight, handleThemeHighlight,
} = useThemeCommand(settings, setThemeError, addItem); } = useThemeCommand(settings, setThemeError, addItem);
const { isFolderTrustDialogOpen, handleFolderTrustSelect } =
useFolderTrust(settings);
const { const {
isAuthDialogOpen, isAuthDialogOpen,
openAuthDialog, openAuthDialog,
@ -905,6 +910,8 @@ const App = ({ config, settings, startupWarnings = [], version }: AppProps) => {
description="If you select Yes, we'll install an extension that allows the CLI to access your open files and display diffs directly in VS Code." description="If you select Yes, we'll install an extension that allows the CLI to access your open files and display diffs directly in VS Code."
onComplete={handleIdePromptComplete} onComplete={handleIdePromptComplete}
/> />
) : isFolderTrustDialogOpen ? (
<FolderTrustDialog onSelect={handleFolderTrustSelect} />
) : shellConfirmationRequest ? ( ) : shellConfirmationRequest ? (
<ShellConfirmationDialog request={shellConfirmationRequest} /> <ShellConfirmationDialog request={shellConfirmationRequest} />
) : isThemeDialogOpen ? ( ) : isThemeDialogOpen ? (

View File

@ -0,0 +1,29 @@
/**
* @license
* Copyright 2025 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import { render } from 'ink-testing-library';
import { vi } from 'vitest';
import { FolderTrustDialog, FolderTrustChoice } from './FolderTrustDialog.js';
describe('FolderTrustDialog', () => {
it('should render the dialog with title and description', () => {
const { lastFrame } = render(<FolderTrustDialog onSelect={vi.fn()} />);
expect(lastFrame()).toContain('Do you trust this folder?');
expect(lastFrame()).toContain(
'Trusting a folder allows Gemini to execute commands it suggests.',
);
});
it('should call onSelect with DO_NOT_TRUST when escape is pressed', () => {
const onSelect = vi.fn();
const { stdin } = render(<FolderTrustDialog onSelect={onSelect} />);
stdin.write('\u001B'); // Simulate escape key
expect(onSelect).toHaveBeenCalledWith(FolderTrustChoice.DO_NOT_TRUST);
});
});

View File

@ -0,0 +1,70 @@
/**
* @license
* Copyright 2025 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import { Box, Text, useInput } from 'ink';
import React from 'react';
import { Colors } from '../colors.js';
import {
RadioButtonSelect,
RadioSelectItem,
} from './shared/RadioButtonSelect.js';
export enum FolderTrustChoice {
TRUST_FOLDER = 'trust_folder',
TRUST_PARENT = 'trust_parent',
DO_NOT_TRUST = 'do_not_trust',
}
interface FolderTrustDialogProps {
onSelect: (choice: FolderTrustChoice) => void;
}
export const FolderTrustDialog: React.FC<FolderTrustDialogProps> = ({
onSelect,
}) => {
useInput((_, key) => {
if (key.escape) {
onSelect(FolderTrustChoice.DO_NOT_TRUST);
}
});
const options: Array<RadioSelectItem<FolderTrustChoice>> = [
{
label: 'Trust folder',
value: FolderTrustChoice.TRUST_FOLDER,
},
{
label: 'Trust parent folder',
value: FolderTrustChoice.TRUST_PARENT,
},
{
label: "Don't trust (esc)",
value: FolderTrustChoice.DO_NOT_TRUST,
},
];
return (
<Box
flexDirection="column"
borderStyle="round"
borderColor={Colors.AccentYellow}
padding={1}
width="100%"
marginLeft={1}
>
<Box flexDirection="column" marginBottom={1}>
<Text bold>Do you trust this folder?</Text>
<Text>
Trusting a folder allows Gemini to execute commands it suggests. This
is a security feature to prevent accidental execution in untrusted
directories.
</Text>
</Box>
<RadioButtonSelect items={options} onSelect={onSelect} isFocused />
</Box>
);
};

View File

@ -0,0 +1,78 @@
/**
* @license
* Copyright 2025 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import { renderHook, act } from '@testing-library/react';
import { vi } from 'vitest';
import { useFolderTrust } from './useFolderTrust.js';
import { LoadedSettings, SettingScope } from '../../config/settings.js';
import { FolderTrustChoice } from '../components/FolderTrustDialog.js';
describe('useFolderTrust', () => {
it('should set isFolderTrustDialogOpen to true when folderTrustFeature is true and folderTrust is undefined', () => {
const settings = {
merged: {
folderTrustFeature: true,
folderTrust: undefined,
},
setValue: vi.fn(),
} as unknown as LoadedSettings;
const { result } = renderHook(() => useFolderTrust(settings));
expect(result.current.isFolderTrustDialogOpen).toBe(true);
});
it('should set isFolderTrustDialogOpen to false when folderTrustFeature is false', () => {
const settings = {
merged: {
folderTrustFeature: false,
folderTrust: undefined,
},
setValue: vi.fn(),
} as unknown as LoadedSettings;
const { result } = renderHook(() => useFolderTrust(settings));
expect(result.current.isFolderTrustDialogOpen).toBe(false);
});
it('should set isFolderTrustDialogOpen to false when folderTrust is defined', () => {
const settings = {
merged: {
folderTrustFeature: true,
folderTrust: true,
},
setValue: vi.fn(),
} as unknown as LoadedSettings;
const { result } = renderHook(() => useFolderTrust(settings));
expect(result.current.isFolderTrustDialogOpen).toBe(false);
});
it('should call setValue and set isFolderTrustDialogOpen to false on handleFolderTrustSelect', () => {
const settings = {
merged: {
folderTrustFeature: true,
folderTrust: undefined,
},
setValue: vi.fn(),
} as unknown as LoadedSettings;
const { result } = renderHook(() => useFolderTrust(settings));
act(() => {
result.current.handleFolderTrustSelect(FolderTrustChoice.TRUST_FOLDER);
});
expect(settings.setValue).toHaveBeenCalledWith(
SettingScope.User,
'folderTrust',
true,
);
expect(result.current.isFolderTrustDialogOpen).toBe(false);
});
});

View File

@ -0,0 +1,31 @@
/**
* @license
* Copyright 2025 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import { useState, useCallback } from 'react';
import { LoadedSettings, SettingScope } from '../../config/settings.js';
import { FolderTrustChoice } from '../components/FolderTrustDialog.js';
export const useFolderTrust = (settings: LoadedSettings) => {
const [isFolderTrustDialogOpen, setIsFolderTrustDialogOpen] = useState(
!!settings.merged.folderTrustFeature &&
// TODO: Update to avoid showing dialog for folders that are trusted.
settings.merged.folderTrust === undefined,
);
const handleFolderTrustSelect = useCallback(
(_choice: FolderTrustChoice) => {
// TODO: Store folderPath in the trusted folders config file based on the choice.
settings.setValue(SettingScope.User, 'folderTrust', true);
setIsFolderTrustDialogOpen(false);
},
[settings],
);
return {
isFolderTrustDialogOpen,
handleFolderTrustSelect,
};
};