2025-08-08 11:02:27 -07:00
|
|
|
|
/**
|
|
|
|
|
|
* @license
|
|
|
|
|
|
* Copyright 2025 Google LLC
|
|
|
|
|
|
* SPDX-License-Identifier: Apache-2.0
|
|
|
|
|
|
*/
|
|
|
|
|
|
|
2025-08-12 14:05:49 -07:00
|
|
|
|
import { Box, Text } from 'ink';
|
2025-08-26 00:04:53 +02:00
|
|
|
|
import type React from 'react';
|
2026-01-06 10:09:09 -08:00
|
|
|
|
import { useEffect, useState, useCallback } from 'react';
|
2025-09-10 10:57:07 -07:00
|
|
|
|
import { theme } from '../semantic-colors.js';
|
2026-02-20 10:21:03 -08:00
|
|
|
|
import stripAnsi from 'strip-ansi';
|
2025-08-26 00:04:53 +02:00
|
|
|
|
import type { RadioSelectItem } from './shared/RadioButtonSelect.js';
|
|
|
|
|
|
import { RadioButtonSelect } from './shared/RadioButtonSelect.js';
|
2026-02-20 10:21:03 -08:00
|
|
|
|
import { MaxSizedBox } from './shared/MaxSizedBox.js';
|
|
|
|
|
|
import { Scrollable } from './shared/Scrollable.js';
|
2025-08-12 14:05:49 -07:00
|
|
|
|
import { useKeypress } from '../hooks/useKeypress.js';
|
2025-08-25 22:11:27 +02:00
|
|
|
|
import * as process from 'node:process';
|
2025-08-29 19:01:06 +00:00
|
|
|
|
import * as path from 'node:path';
|
2025-09-17 13:05:40 -07:00
|
|
|
|
import { relaunchApp } from '../../utils/processUtils.js';
|
2025-11-26 08:13:21 +05:30
|
|
|
|
import { runExitCleanup } from '../../utils/cleanup.js';
|
2026-02-20 10:21:03 -08:00
|
|
|
|
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';
|
2025-08-08 11:02:27 -07:00
|
|
|
|
|
|
|
|
|
|
export enum FolderTrustChoice {
|
|
|
|
|
|
TRUST_FOLDER = 'trust_folder',
|
|
|
|
|
|
TRUST_PARENT = 'trust_parent',
|
|
|
|
|
|
DO_NOT_TRUST = 'do_not_trust',
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
interface FolderTrustDialogProps {
|
|
|
|
|
|
onSelect: (choice: FolderTrustChoice) => void;
|
2025-08-21 00:38:12 -07:00
|
|
|
|
isRestarting?: boolean;
|
2026-02-20 10:21:03 -08:00
|
|
|
|
discoveryResults?: FolderDiscoveryResults | null;
|
2025-08-08 11:02:27 -07:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
export const FolderTrustDialog: React.FC<FolderTrustDialogProps> = ({
|
|
|
|
|
|
onSelect,
|
2025-08-21 00:38:12 -07:00
|
|
|
|
isRestarting,
|
2026-02-20 10:21:03 -08:00
|
|
|
|
discoveryResults,
|
2025-08-08 11:02:27 -07:00
|
|
|
|
}) => {
|
2025-10-14 07:27:40 -07:00
|
|
|
|
const [exiting, setExiting] = useState(false);
|
2026-02-20 10:21:03 -08:00
|
|
|
|
const { terminalHeight, terminalWidth, constrainHeight } = useUIState();
|
|
|
|
|
|
const isAlternateBuffer = useAlternateBuffer();
|
|
|
|
|
|
|
|
|
|
|
|
const isExpanded = !constrainHeight;
|
2026-01-06 10:09:09 -08:00
|
|
|
|
|
2025-09-17 13:05:40 -07:00
|
|
|
|
useEffect(() => {
|
2026-01-06 10:09:09 -08:00
|
|
|
|
let timer: ReturnType<typeof setTimeout>;
|
|
|
|
|
|
if (isRestarting) {
|
2026-03-03 22:18:12 -08:00
|
|
|
|
timer = setTimeout(relaunchApp, 250);
|
2026-01-06 10:09:09 -08:00
|
|
|
|
}
|
|
|
|
|
|
return () => {
|
|
|
|
|
|
if (timer) clearTimeout(timer);
|
2025-09-17 13:05:40 -07:00
|
|
|
|
};
|
|
|
|
|
|
}, [isRestarting]);
|
|
|
|
|
|
|
2026-01-06 10:09:09 -08:00
|
|
|
|
const handleExit = useCallback(() => {
|
|
|
|
|
|
setExiting(true);
|
|
|
|
|
|
// Give time for the UI to render the exiting message
|
|
|
|
|
|
setTimeout(async () => {
|
|
|
|
|
|
await runExitCleanup();
|
|
|
|
|
|
process.exit(ExitCodes.FATAL_CANCELLATION_ERROR);
|
|
|
|
|
|
}, 100);
|
|
|
|
|
|
}, []);
|
|
|
|
|
|
|
2025-08-12 14:05:49 -07:00
|
|
|
|
useKeypress(
|
|
|
|
|
|
(key) => {
|
|
|
|
|
|
if (key.name === 'escape') {
|
2026-01-06 10:09:09 -08:00
|
|
|
|
handleExit();
|
2026-01-27 14:26:00 -08:00
|
|
|
|
return true;
|
2025-08-12 14:05:49 -07:00
|
|
|
|
}
|
2026-01-27 14:26:00 -08:00
|
|
|
|
return false;
|
2025-08-12 14:05:49 -07:00
|
|
|
|
},
|
2025-08-21 00:38:12 -07:00
|
|
|
|
{ isActive: !isRestarting },
|
|
|
|
|
|
);
|
|
|
|
|
|
|
2025-09-10 16:31:12 +00:00
|
|
|
|
const dirName = path.basename(process.cwd());
|
2025-08-29 19:01:06 +00:00
|
|
|
|
const parentFolder = path.basename(path.dirname(process.cwd()));
|
|
|
|
|
|
|
2025-08-08 11:02:27 -07:00
|
|
|
|
const options: Array<RadioSelectItem<FolderTrustChoice>> = [
|
|
|
|
|
|
{
|
2025-09-10 16:31:12 +00:00
|
|
|
|
label: `Trust folder (${dirName})`,
|
2025-08-08 11:02:27 -07:00
|
|
|
|
value: FolderTrustChoice.TRUST_FOLDER,
|
2025-09-28 14:50:47 -07:00
|
|
|
|
key: `Trust folder (${dirName})`,
|
2025-08-08 11:02:27 -07:00
|
|
|
|
},
|
|
|
|
|
|
{
|
2025-08-29 19:01:06 +00:00
|
|
|
|
label: `Trust parent folder (${parentFolder})`,
|
2025-08-08 11:02:27 -07:00
|
|
|
|
value: FolderTrustChoice.TRUST_PARENT,
|
2025-09-28 14:50:47 -07:00
|
|
|
|
key: `Trust parent folder (${parentFolder})`,
|
2025-08-08 11:02:27 -07:00
|
|
|
|
},
|
|
|
|
|
|
{
|
2025-10-14 07:27:40 -07:00
|
|
|
|
label: "Don't trust",
|
2025-08-08 11:02:27 -07:00
|
|
|
|
value: FolderTrustChoice.DO_NOT_TRUST,
|
2025-10-14 07:27:40 -07:00
|
|
|
|
key: "Don't trust",
|
2025-08-08 11:02:27 -07:00
|
|
|
|
},
|
|
|
|
|
|
];
|
|
|
|
|
|
|
2026-02-20 10:21:03 -08:00
|
|
|
|
const hasDiscovery =
|
|
|
|
|
|
discoveryResults &&
|
|
|
|
|
|
(discoveryResults.commands.length > 0 ||
|
|
|
|
|
|
discoveryResults.mcps.length > 0 ||
|
|
|
|
|
|
discoveryResults.hooks.length > 0 ||
|
|
|
|
|
|
discoveryResults.skills.length > 0 ||
|
|
|
|
|
|
discoveryResults.settings.length > 0);
|
|
|
|
|
|
|
|
|
|
|
|
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>
|
|
|
|
|
|
|
|
|
|
|
|
{hasErrors && (
|
2025-08-21 00:38:12 -07:00
|
|
|
|
<Box flexDirection="column" marginBottom={1}>
|
2026-02-20 10:21:03 -08:00
|
|
|
|
<Text color={theme.status.error} bold>
|
|
|
|
|
|
❌ Discovery Errors:
|
2025-08-21 00:38:12 -07:00
|
|
|
|
</Text>
|
2026-02-20 10:21:03 -08:00
|
|
|
|
{discoveryResults.discoveryErrors.map((error, i) => (
|
|
|
|
|
|
<Box key={i} marginLeft={2}>
|
|
|
|
|
|
<Text color={theme.status.error}>• {stripAnsi(error)}</Text>
|
|
|
|
|
|
</Box>
|
|
|
|
|
|
))}
|
2025-08-21 00:38:12 -07:00
|
|
|
|
</Box>
|
2026-02-20 10:21:03 -08:00
|
|
|
|
)}
|
2025-08-08 11:02:27 -07:00
|
|
|
|
|
2026-02-20 10:21:03 -08:00
|
|
|
|
{hasWarnings && (
|
|
|
|
|
|
<Box flexDirection="column" marginBottom={1}>
|
|
|
|
|
|
<Text color={theme.status.warning} bold>
|
|
|
|
|
|
⚠️ Security Warnings:
|
2025-08-21 00:38:12 -07:00
|
|
|
|
</Text>
|
2026-02-20 10:21:03 -08:00
|
|
|
|
{discoveryResults.securityWarnings.map((warning, i) => (
|
|
|
|
|
|
<Box key={i} marginLeft={2}>
|
|
|
|
|
|
<Text color={theme.status.warning}>• {stripAnsi(warning)}</Text>
|
|
|
|
|
|
</Box>
|
|
|
|
|
|
))}
|
2025-08-21 00:38:12 -07:00
|
|
|
|
</Box>
|
|
|
|
|
|
)}
|
2026-02-20 10:21:03 -08:00
|
|
|
|
|
|
|
|
|
|
{hasDiscovery && (
|
|
|
|
|
|
<Box flexDirection="column" marginBottom={1}>
|
|
|
|
|
|
<Text color={theme.text.primary} bold>
|
|
|
|
|
|
This folder contains:
|
2025-10-14 07:27:40 -07:00
|
|
|
|
</Text>
|
2026-02-20 10:21:03 -08:00
|
|
|
|
{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>
|
|
|
|
|
|
))}
|
2025-10-14 07:27:40 -07:00
|
|
|
|
</Box>
|
|
|
|
|
|
)}
|
2025-08-08 11:02:27 -07:00
|
|
|
|
</Box>
|
|
|
|
|
|
);
|
2026-02-20 10:21:03 -08:00
|
|
|
|
|
|
|
|
|
|
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>
|
|
|
|
|
|
);
|
|
|
|
|
|
};
|
|
|
|
|
|
|
2026-02-20 16:26:11 -08:00
|
|
|
|
const content = (
|
|
|
|
|
|
<Box flexDirection="column" width="100%">
|
|
|
|
|
|
<Box flexDirection="column" marginLeft={1} marginRight={1}>
|
|
|
|
|
|
{renderContent()}
|
|
|
|
|
|
</Box>
|
|
|
|
|
|
|
|
|
|
|
|
<Box paddingX={2} marginBottom={1}>
|
|
|
|
|
|
<ShowMoreLines constrainHeight={constrainHeight} />
|
|
|
|
|
|
</Box>
|
2026-02-20 10:21:03 -08:00
|
|
|
|
|
2026-02-20 16:26:11 -08:00
|
|
|
|
{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>
|
2026-02-20 10:21:03 -08:00
|
|
|
|
</Box>
|
2026-02-20 16:26:11 -08:00
|
|
|
|
)}
|
|
|
|
|
|
</Box>
|
|
|
|
|
|
);
|
2026-02-20 10:21:03 -08:00
|
|
|
|
|
2026-03-07 11:04:22 -08:00
|
|
|
|
return <OverflowProvider>{content}</OverflowProvider>;
|
2025-08-08 11:02:27 -07:00
|
|
|
|
};
|