mirror of
https://github.com/google-gemini/gemini-cli.git
synced 2026-07-17 05:20:23 -07:00
feat(cli): enhance folder trust with configuration discovery and security warnings (#19492)
Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com>
This commit is contained in:
@@ -8,14 +8,25 @@ import { Box, Text } from 'ink';
|
||||
import type React from 'react';
|
||||
import { useEffect, useState, useCallback } from 'react';
|
||||
import { theme } from '../semantic-colors.js';
|
||||
import stripAnsi from 'strip-ansi';
|
||||
import type { RadioSelectItem } from './shared/RadioButtonSelect.js';
|
||||
import { RadioButtonSelect } from './shared/RadioButtonSelect.js';
|
||||
import { MaxSizedBox } from './shared/MaxSizedBox.js';
|
||||
import { Scrollable } from './shared/Scrollable.js';
|
||||
import { useKeypress } from '../hooks/useKeypress.js';
|
||||
import * as process from 'node:process';
|
||||
import * as path from 'node:path';
|
||||
import { relaunchApp } from '../../utils/processUtils.js';
|
||||
import { runExitCleanup } from '../../utils/cleanup.js';
|
||||
import { ExitCodes } from '@google/gemini-cli-core';
|
||||
import {
|
||||
ExitCodes,
|
||||
type FolderDiscoveryResults,
|
||||
} from '@google/gemini-cli-core';
|
||||
import { useUIState } from '../contexts/UIStateContext.js';
|
||||
import { useAlternateBuffer } from '../hooks/useAlternateBuffer.js';
|
||||
import { OverflowProvider } from '../contexts/OverflowContext.js';
|
||||
import { ShowMoreLines } from './ShowMoreLines.js';
|
||||
import { StickyHeader } from './StickyHeader.js';
|
||||
|
||||
export enum FolderTrustChoice {
|
||||
TRUST_FOLDER = 'trust_folder',
|
||||
@@ -26,13 +37,19 @@ export enum FolderTrustChoice {
|
||||
interface FolderTrustDialogProps {
|
||||
onSelect: (choice: FolderTrustChoice) => void;
|
||||
isRestarting?: boolean;
|
||||
discoveryResults?: FolderDiscoveryResults | null;
|
||||
}
|
||||
|
||||
export const FolderTrustDialog: React.FC<FolderTrustDialogProps> = ({
|
||||
onSelect,
|
||||
isRestarting,
|
||||
discoveryResults,
|
||||
}) => {
|
||||
const [exiting, setExiting] = useState(false);
|
||||
const { terminalHeight, terminalWidth, constrainHeight } = useUIState();
|
||||
const isAlternateBuffer = useAlternateBuffer();
|
||||
|
||||
const isExpanded = !constrainHeight;
|
||||
|
||||
useEffect(() => {
|
||||
let timer: ReturnType<typeof setTimeout>;
|
||||
@@ -87,48 +104,214 @@ export const FolderTrustDialog: React.FC<FolderTrustDialogProps> = ({
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<Box flexDirection="column" width="100%">
|
||||
<Box
|
||||
flexDirection="column"
|
||||
borderStyle="round"
|
||||
borderColor={theme.status.warning}
|
||||
padding={1}
|
||||
marginLeft={1}
|
||||
marginRight={1}
|
||||
>
|
||||
<Box flexDirection="column" marginBottom={1}>
|
||||
<Text bold color={theme.text.primary}>
|
||||
Do you trust this folder?
|
||||
</Text>
|
||||
<Text color={theme.text.primary}>
|
||||
Trusting a folder allows Gemini to execute commands it suggests.
|
||||
This is a security feature to prevent accidental execution in
|
||||
untrusted directories.
|
||||
</Text>
|
||||
</Box>
|
||||
const hasDiscovery =
|
||||
discoveryResults &&
|
||||
(discoveryResults.commands.length > 0 ||
|
||||
discoveryResults.mcps.length > 0 ||
|
||||
discoveryResults.hooks.length > 0 ||
|
||||
discoveryResults.skills.length > 0 ||
|
||||
discoveryResults.settings.length > 0);
|
||||
|
||||
<RadioButtonSelect
|
||||
items={options}
|
||||
onSelect={onSelect}
|
||||
isFocused={!isRestarting}
|
||||
/>
|
||||
const hasWarnings =
|
||||
discoveryResults && discoveryResults.securityWarnings.length > 0;
|
||||
|
||||
const hasErrors =
|
||||
discoveryResults &&
|
||||
discoveryResults.discoveryErrors &&
|
||||
discoveryResults.discoveryErrors.length > 0;
|
||||
|
||||
const dialogWidth = terminalWidth - 2;
|
||||
const borderColor = theme.status.warning;
|
||||
|
||||
// Header: 3 lines
|
||||
// Options: options.length + 2 lines for margins
|
||||
// Footer: 1 line
|
||||
// Safety margin: 2 lines
|
||||
const overhead = 3 + options.length + 2 + 1 + 2;
|
||||
const scrollableHeight = Math.max(4, terminalHeight - overhead);
|
||||
|
||||
const groups = [
|
||||
{ label: 'Commands', items: discoveryResults?.commands ?? [] },
|
||||
{ label: 'MCP Servers', items: discoveryResults?.mcps ?? [] },
|
||||
{ label: 'Hooks', items: discoveryResults?.hooks ?? [] },
|
||||
{ label: 'Skills', items: discoveryResults?.skills ?? [] },
|
||||
{ label: 'Setting overrides', items: discoveryResults?.settings ?? [] },
|
||||
].filter((g) => g.items.length > 0);
|
||||
|
||||
const discoveryContent = (
|
||||
<Box flexDirection="column">
|
||||
<Box marginBottom={1}>
|
||||
<Text color={theme.text.primary}>
|
||||
Trusting a folder allows Gemini CLI to load its local configurations,
|
||||
including custom commands, hooks, MCP servers, agent skills, and
|
||||
settings. These configurations could execute code on your behalf or
|
||||
change the behavior of the CLI.
|
||||
</Text>
|
||||
</Box>
|
||||
{isRestarting && (
|
||||
<Box marginLeft={1} marginTop={1}>
|
||||
<Text color={theme.status.warning}>
|
||||
Gemini CLI is restarting to apply the trust changes...
|
||||
|
||||
{hasErrors && (
|
||||
<Box flexDirection="column" marginBottom={1}>
|
||||
<Text color={theme.status.error} bold>
|
||||
❌ Discovery Errors:
|
||||
</Text>
|
||||
{discoveryResults.discoveryErrors.map((error, i) => (
|
||||
<Box key={i} marginLeft={2}>
|
||||
<Text color={theme.status.error}>• {stripAnsi(error)}</Text>
|
||||
</Box>
|
||||
))}
|
||||
</Box>
|
||||
)}
|
||||
{exiting && (
|
||||
<Box marginLeft={1} marginTop={1}>
|
||||
<Text color={theme.status.warning}>
|
||||
A folder trust level must be selected to continue. Exiting since
|
||||
escape was pressed.
|
||||
|
||||
{hasWarnings && (
|
||||
<Box flexDirection="column" marginBottom={1}>
|
||||
<Text color={theme.status.warning} bold>
|
||||
⚠️ Security Warnings:
|
||||
</Text>
|
||||
{discoveryResults.securityWarnings.map((warning, i) => (
|
||||
<Box key={i} marginLeft={2}>
|
||||
<Text color={theme.status.warning}>• {stripAnsi(warning)}</Text>
|
||||
</Box>
|
||||
))}
|
||||
</Box>
|
||||
)}
|
||||
|
||||
{hasDiscovery && (
|
||||
<Box flexDirection="column" marginBottom={1}>
|
||||
<Text color={theme.text.primary} bold>
|
||||
This folder contains:
|
||||
</Text>
|
||||
{groups.map((group) => (
|
||||
<Box key={group.label} flexDirection="column" marginLeft={2}>
|
||||
<Text color={theme.text.primary} bold>
|
||||
• {group.label} ({group.items.length}):
|
||||
</Text>
|
||||
{group.items.map((item, idx) => (
|
||||
<Box key={idx} marginLeft={2}>
|
||||
<Text color={theme.text.primary}>- {stripAnsi(item)}</Text>
|
||||
</Box>
|
||||
))}
|
||||
</Box>
|
||||
))}
|
||||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
);
|
||||
|
||||
const title = (
|
||||
<Text bold color={theme.text.primary}>
|
||||
Do you trust the files in this folder?
|
||||
</Text>
|
||||
);
|
||||
|
||||
const selectOptions = (
|
||||
<RadioButtonSelect
|
||||
items={options}
|
||||
onSelect={onSelect}
|
||||
isFocused={!isRestarting}
|
||||
/>
|
||||
);
|
||||
|
||||
const renderContent = () => {
|
||||
if (isAlternateBuffer) {
|
||||
return (
|
||||
<Box flexDirection="column" width={dialogWidth}>
|
||||
<StickyHeader
|
||||
width={dialogWidth}
|
||||
isFirst={true}
|
||||
borderColor={borderColor}
|
||||
borderDimColor={false}
|
||||
>
|
||||
{title}
|
||||
</StickyHeader>
|
||||
|
||||
<Box
|
||||
flexDirection="column"
|
||||
borderLeft={true}
|
||||
borderRight={true}
|
||||
borderColor={borderColor}
|
||||
borderStyle="round"
|
||||
borderTop={false}
|
||||
borderBottom={false}
|
||||
width={dialogWidth}
|
||||
>
|
||||
<Scrollable
|
||||
hasFocus={!isRestarting}
|
||||
height={scrollableHeight}
|
||||
width={dialogWidth - 2}
|
||||
>
|
||||
<Box flexDirection="column" paddingX={1}>
|
||||
{discoveryContent}
|
||||
</Box>
|
||||
</Scrollable>
|
||||
|
||||
<Box paddingX={1} marginY={1}>
|
||||
{selectOptions}
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
<Box
|
||||
height={0}
|
||||
width={dialogWidth}
|
||||
borderLeft={true}
|
||||
borderRight={true}
|
||||
borderTop={false}
|
||||
borderBottom={true}
|
||||
borderColor={borderColor}
|
||||
borderStyle="round"
|
||||
/>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Box
|
||||
flexDirection="column"
|
||||
borderStyle="round"
|
||||
borderColor={borderColor}
|
||||
padding={1}
|
||||
width="100%"
|
||||
>
|
||||
<Box marginBottom={1}>{title}</Box>
|
||||
|
||||
<MaxSizedBox
|
||||
maxHeight={isExpanded ? undefined : Math.max(4, terminalHeight - 12)}
|
||||
overflowDirection="bottom"
|
||||
>
|
||||
{discoveryContent}
|
||||
</MaxSizedBox>
|
||||
|
||||
<Box marginTop={1}>{selectOptions}</Box>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<OverflowProvider>
|
||||
<Box flexDirection="column" width="100%">
|
||||
<Box flexDirection="column" marginLeft={1} marginRight={1}>
|
||||
{renderContent()}
|
||||
</Box>
|
||||
|
||||
<Box paddingX={2} marginBottom={1}>
|
||||
<ShowMoreLines constrainHeight={constrainHeight} />
|
||||
</Box>
|
||||
|
||||
{isRestarting && (
|
||||
<Box marginLeft={1} marginTop={1}>
|
||||
<Text color={theme.status.warning}>
|
||||
Gemini CLI is restarting to apply the trust changes...
|
||||
</Text>
|
||||
</Box>
|
||||
)}
|
||||
{exiting && (
|
||||
<Box marginLeft={1} marginTop={1}>
|
||||
<Text color={theme.status.warning}>
|
||||
A folder trust level must be selected to continue. Exiting since
|
||||
escape was pressed.
|
||||
</Text>
|
||||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
</OverflowProvider>
|
||||
);
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user