Add FolderTrustDialog that shows on launch and enables folderTrust setting (#5815)
This commit is contained in:
parent
3af4913ef3
commit
34b5dc7f28
|
@ -486,8 +486,8 @@ export async function loadCliConfig(
|
||||||
ideModeFeature,
|
ideModeFeature,
|
||||||
chatCompression: settings.chatCompression,
|
chatCompression: settings.chatCompression,
|
||||||
folderTrustFeature,
|
folderTrustFeature,
|
||||||
interactive,
|
|
||||||
folderTrust,
|
folderTrust,
|
||||||
|
interactive,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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?');
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -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 ? (
|
||||||
|
|
|
@ -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);
|
||||||
|
});
|
||||||
|
});
|
|
@ -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>
|
||||||
|
);
|
||||||
|
};
|
|
@ -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);
|
||||||
|
});
|
||||||
|
});
|
|
@ -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,
|
||||||
|
};
|
||||||
|
};
|
Loading…
Reference in New Issue