feat: Show untrusted status in the Footer (#6210)

Co-authored-by: Jacob Richman <jacob314@gmail.com>
This commit is contained in:
shrutip90
2025-08-14 11:15:48 -07:00
committed by GitHub
parent 69d666cfaf
commit 69c5582723
10 changed files with 221 additions and 60 deletions

View File

@@ -163,6 +163,7 @@ vi.mock('@google/gemini-cli-core', async (importOriginal) => {
getCurrentIde: vi.fn(() => 'vscode'),
getDetectedIdeDisplayName: vi.fn(() => 'VSCode'),
})),
isTrustedFolder: vi.fn(() => true),
};
});
@@ -1118,5 +1119,45 @@ describe('App UI', () => {
await Promise.resolve();
expect(lastFrame()).toContain('Do you trust this folder?');
});
it('should display the folder trust dialog when the feature is enabled but the folder is not trusted', async () => {
const { useFolderTrust } = await import('./hooks/useFolderTrust.js');
vi.mocked(useFolderTrust).mockReturnValue({
isFolderTrustDialogOpen: true,
handleFolderTrustSelect: vi.fn(),
});
mockConfig.isTrustedFolder.mockReturnValue(false);
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?');
});
it('should not display the folder trust dialog when the feature is disabled', async () => {
const { useFolderTrust } = await import('./hooks/useFolderTrust.js');
vi.mocked(useFolderTrust).mockReturnValue({
isFolderTrustDialogOpen: false,
handleFolderTrustSelect: vi.fn(),
});
mockConfig.isTrustedFolder.mockReturnValue(false);
const { lastFrame, unmount } = render(
<App
config={mockConfig as unknown as ServerConfig}
settings={mockSettings}
version={mockVersion}
/>,
);
currentUnmount = unmount;
await Promise.resolve();
expect(lastFrame()).not.toContain('Do you trust this folder?');
});
});
});

View File

@@ -171,6 +171,9 @@ const App = ({ config, settings, startupWarnings = [], version }: AppProps) => {
const [editorError, setEditorError] = useState<string | null>(null);
const [footerHeight, setFooterHeight] = useState<number>(0);
const [corgiMode, setCorgiMode] = useState(false);
const [isTrustedFolderState, setIsTrustedFolder] = useState(
config.isTrustedFolder(),
);
const [currentModel, setCurrentModel] = useState(config.getModel());
const [shellModeActive, setShellModeActive] = useState(false);
const [showErrorDetails, setShowErrorDetails] = useState<boolean>(false);
@@ -254,7 +257,7 @@ const App = ({ config, settings, startupWarnings = [], version }: AppProps) => {
const { isFolderTrustDialogOpen, handleFolderTrustSelect } = useFolderTrust(
settings,
config,
setIsTrustedFolder,
);
const {
@@ -1198,6 +1201,7 @@ const App = ({ config, settings, startupWarnings = [], version }: AppProps) => {
promptTokenCount={sessionStats.lastPromptTokenCount}
nightly={nightly}
vimMode={vimModeEnabled ? vimMode : undefined}
isTrustedFolder={isTrustedFolderState}
/>
</Box>
</Box>

View File

@@ -103,4 +103,57 @@ describe('<Footer />', () => {
expect(lastFrame()).toContain(defaultProps.model);
expect(lastFrame()).toMatch(/\(\d+% context[\s\S]*left\)/);
});
describe('sandbox and trust info', () => {
it('should display untrusted when isTrustedFolder is false', () => {
const { lastFrame } = renderWithWidth(120, {
...defaultProps,
isTrustedFolder: false,
});
expect(lastFrame()).toContain('untrusted');
});
it('should display custom sandbox info when SANDBOX env is set', () => {
vi.stubEnv('SANDBOX', 'gemini-cli-test-sandbox');
const { lastFrame } = renderWithWidth(120, {
...defaultProps,
isTrustedFolder: undefined,
});
expect(lastFrame()).toContain('test');
vi.unstubAllEnvs();
});
it('should display macOS Seatbelt info when SANDBOX is sandbox-exec', () => {
vi.stubEnv('SANDBOX', 'sandbox-exec');
vi.stubEnv('SEATBELT_PROFILE', 'test-profile');
const { lastFrame } = renderWithWidth(120, {
...defaultProps,
isTrustedFolder: true,
});
expect(lastFrame()).toMatch(/macOS Seatbelt.*\(test-profile\)/s);
vi.unstubAllEnvs();
});
it('should display "no sandbox" when SANDBOX is not set and folder is trusted', () => {
// Clear any SANDBOX env var that might be set.
vi.stubEnv('SANDBOX', '');
const { lastFrame } = renderWithWidth(120, {
...defaultProps,
isTrustedFolder: true,
});
expect(lastFrame()).toContain('no sandbox');
vi.unstubAllEnvs();
});
it('should prioritize untrusted message over sandbox info', () => {
vi.stubEnv('SANDBOX', 'gemini-cli-test-sandbox');
const { lastFrame } = renderWithWidth(120, {
...defaultProps,
isTrustedFolder: false,
});
expect(lastFrame()).toContain('untrusted');
expect(lastFrame()).not.toMatch(/test-sandbox/s);
vi.unstubAllEnvs();
});
});
});

View File

@@ -32,6 +32,7 @@ interface FooterProps {
promptTokenCount: number;
nightly: boolean;
vimMode?: string;
isTrustedFolder?: boolean;
}
export const Footer: React.FC<FooterProps> = ({
@@ -47,6 +48,7 @@ export const Footer: React.FC<FooterProps> = ({
promptTokenCount,
nightly,
vimMode,
isTrustedFolder,
}) => {
const { columns: terminalWidth } = useTerminalSize();
@@ -90,7 +92,7 @@ export const Footer: React.FC<FooterProps> = ({
)}
</Box>
{/* Middle Section: Centered Sandbox Info */}
{/* Middle Section: Centered Trust/Sandbox Info */}
<Box
flexGrow={isNarrow ? 0 : 1}
alignItems="center"
@@ -99,7 +101,9 @@ export const Footer: React.FC<FooterProps> = ({
paddingX={isNarrow ? 0 : 1}
paddingTop={isNarrow ? 1 : 0}
>
{process.env.SANDBOX && process.env.SANDBOX !== 'sandbox-exec' ? (
{isTrustedFolder === false ? (
<Text color={theme.status.warning}>untrusted</Text>
) : process.env.SANDBOX && process.env.SANDBOX !== 'sandbox-exec' ? (
<Text color="green">
{process.env.SANDBOX.replace(/^gemini-(?:cli-)?/, '')}
</Text>

View File

@@ -7,7 +7,6 @@
import { vi } from 'vitest';
import { renderHook, act } from '@testing-library/react';
import { useFolderTrust } from './useFolderTrust.js';
import { type Config } from '@google/gemini-cli-core';
import { LoadedSettings } from '../../config/settings.js';
import { FolderTrustChoice } from '../components/FolderTrustDialog.js';
import {
@@ -25,9 +24,10 @@ vi.mock('process', () => ({
describe('useFolderTrust', () => {
let mockSettings: LoadedSettings;
let mockConfig: Config;
let mockTrustedFolders: LoadedTrustedFolders;
let loadTrustedFoldersSpy: vi.SpyInstance;
let isWorkspaceTrustedSpy: vi.SpyInstance;
let onTrustChange: (isTrusted: boolean | undefined) => void;
beforeEach(() => {
mockSettings = {
@@ -38,10 +38,6 @@ describe('useFolderTrust', () => {
setValue: vi.fn(),
} as unknown as LoadedSettings;
mockConfig = {
isTrustedFolder: vi.fn().mockReturnValue(undefined),
} as unknown as Config;
mockTrustedFolders = {
setValue: vi.fn(),
} as unknown as LoadedTrustedFolders;
@@ -49,7 +45,9 @@ describe('useFolderTrust', () => {
loadTrustedFoldersSpy = vi
.spyOn(trustedFolders, 'loadTrustedFolders')
.mockReturnValue(mockTrustedFolders);
isWorkspaceTrustedSpy = vi.spyOn(trustedFolders, 'isWorkspaceTrusted');
(process.cwd as vi.Mock).mockReturnValue('/test/path');
onTrustChange = vi.fn();
});
afterEach(() => {
@@ -57,34 +55,39 @@ describe('useFolderTrust', () => {
});
it('should not open dialog when folder is already trusted', () => {
(mockConfig.isTrustedFolder as vi.Mock).mockReturnValue(true);
isWorkspaceTrustedSpy.mockReturnValue(true);
const { result } = renderHook(() =>
useFolderTrust(mockSettings, mockConfig),
useFolderTrust(mockSettings, onTrustChange),
);
expect(result.current.isFolderTrustDialogOpen).toBe(false);
expect(onTrustChange).toHaveBeenCalledWith(true);
});
it('should not open dialog when folder is already untrusted', () => {
(mockConfig.isTrustedFolder as vi.Mock).mockReturnValue(false);
isWorkspaceTrustedSpy.mockReturnValue(false);
const { result } = renderHook(() =>
useFolderTrust(mockSettings, mockConfig),
useFolderTrust(mockSettings, onTrustChange),
);
expect(result.current.isFolderTrustDialogOpen).toBe(false);
expect(onTrustChange).toHaveBeenCalledWith(false);
});
it('should open dialog when folder trust is undefined', () => {
(mockConfig.isTrustedFolder as vi.Mock).mockReturnValue(undefined);
isWorkspaceTrustedSpy.mockReturnValue(undefined);
const { result } = renderHook(() =>
useFolderTrust(mockSettings, mockConfig),
useFolderTrust(mockSettings, onTrustChange),
);
expect(result.current.isFolderTrustDialogOpen).toBe(true);
expect(onTrustChange).toHaveBeenCalledWith(undefined);
});
it('should handle TRUST_FOLDER choice', () => {
isWorkspaceTrustedSpy.mockReturnValue(undefined);
const { result } = renderHook(() =>
useFolderTrust(mockSettings, mockConfig),
useFolderTrust(mockSettings, onTrustChange),
);
isWorkspaceTrustedSpy.mockReturnValue(true);
act(() => {
result.current.handleFolderTrustSelect(FolderTrustChoice.TRUST_FOLDER);
});
@@ -95,13 +98,16 @@ describe('useFolderTrust', () => {
TrustLevel.TRUST_FOLDER,
);
expect(result.current.isFolderTrustDialogOpen).toBe(false);
expect(onTrustChange).toHaveBeenLastCalledWith(true);
});
it('should handle TRUST_PARENT choice', () => {
isWorkspaceTrustedSpy.mockReturnValue(undefined);
const { result } = renderHook(() =>
useFolderTrust(mockSettings, mockConfig),
useFolderTrust(mockSettings, onTrustChange),
);
isWorkspaceTrustedSpy.mockReturnValue(true);
act(() => {
result.current.handleFolderTrustSelect(FolderTrustChoice.TRUST_PARENT);
});
@@ -111,13 +117,16 @@ describe('useFolderTrust', () => {
TrustLevel.TRUST_PARENT,
);
expect(result.current.isFolderTrustDialogOpen).toBe(false);
expect(onTrustChange).toHaveBeenLastCalledWith(true);
});
it('should handle DO_NOT_TRUST choice', () => {
isWorkspaceTrustedSpy.mockReturnValue(undefined);
const { result } = renderHook(() =>
useFolderTrust(mockSettings, mockConfig),
useFolderTrust(mockSettings, onTrustChange),
);
isWorkspaceTrustedSpy.mockReturnValue(false);
act(() => {
result.current.handleFolderTrustSelect(FolderTrustChoice.DO_NOT_TRUST);
});
@@ -127,11 +136,13 @@ describe('useFolderTrust', () => {
TrustLevel.DO_NOT_TRUST,
);
expect(result.current.isFolderTrustDialogOpen).toBe(false);
expect(onTrustChange).toHaveBeenLastCalledWith(false);
});
it('should do nothing for default choice', () => {
isWorkspaceTrustedSpy.mockReturnValue(undefined);
const { result } = renderHook(() =>
useFolderTrust(mockSettings, mockConfig),
useFolderTrust(mockSettings, onTrustChange),
);
act(() => {
@@ -143,5 +154,6 @@ describe('useFolderTrust', () => {
expect(mockTrustedFolders.setValue).not.toHaveBeenCalled();
expect(mockSettings.setValue).not.toHaveBeenCalled();
expect(result.current.isFolderTrustDialogOpen).toBe(true);
expect(onTrustChange).toHaveBeenCalledWith(undefined);
});
});

View File

@@ -4,42 +4,68 @@
* SPDX-License-Identifier: Apache-2.0
*/
import { useState, useCallback } from 'react';
import { type Config } from '@google/gemini-cli-core';
import { LoadedSettings } from '../../config/settings.js';
import { useState, useCallback, useEffect } from 'react';
import { Settings, LoadedSettings } from '../../config/settings.js';
import { FolderTrustChoice } from '../components/FolderTrustDialog.js';
import { loadTrustedFolders, TrustLevel } from '../../config/trustedFolders.js';
import {
loadTrustedFolders,
TrustLevel,
isWorkspaceTrusted,
} from '../../config/trustedFolders.js';
import * as process from 'process';
export const useFolderTrust = (settings: LoadedSettings, config: Config) => {
const [isFolderTrustDialogOpen, setIsFolderTrustDialogOpen] = useState(
config.isTrustedFolder() === undefined,
export const useFolderTrust = (
settings: LoadedSettings,
onTrustChange: (isTrusted: boolean | undefined) => void,
) => {
const [isTrusted, setIsTrusted] = useState<boolean | undefined>(undefined);
const [isFolderTrustDialogOpen, setIsFolderTrustDialogOpen] = useState(false);
const { folderTrust, folderTrustFeature } = settings.merged;
useEffect(() => {
const trusted = isWorkspaceTrusted({
folderTrust,
folderTrustFeature,
} as Settings);
setIsTrusted(trusted);
setIsFolderTrustDialogOpen(trusted === undefined);
onTrustChange(trusted);
}, [onTrustChange, folderTrust, folderTrustFeature]);
const handleFolderTrustSelect = useCallback(
(choice: FolderTrustChoice) => {
const trustedFolders = loadTrustedFolders();
const cwd = process.cwd();
let trustLevel: TrustLevel;
switch (choice) {
case FolderTrustChoice.TRUST_FOLDER:
trustLevel = TrustLevel.TRUST_FOLDER;
break;
case FolderTrustChoice.TRUST_PARENT:
trustLevel = TrustLevel.TRUST_PARENT;
break;
case FolderTrustChoice.DO_NOT_TRUST:
trustLevel = TrustLevel.DO_NOT_TRUST;
break;
default:
return;
}
trustedFolders.setValue(cwd, trustLevel);
const trusted = isWorkspaceTrusted({
folderTrust,
folderTrustFeature,
} as Settings);
setIsTrusted(trusted);
setIsFolderTrustDialogOpen(false);
onTrustChange(trusted);
},
[onTrustChange, folderTrust, folderTrustFeature],
);
const handleFolderTrustSelect = useCallback((choice: FolderTrustChoice) => {
const trustedFolders = loadTrustedFolders();
const cwd = process.cwd();
let trustLevel: TrustLevel;
switch (choice) {
case FolderTrustChoice.TRUST_FOLDER:
trustLevel = TrustLevel.TRUST_FOLDER;
break;
case FolderTrustChoice.TRUST_PARENT:
trustLevel = TrustLevel.TRUST_PARENT;
break;
case FolderTrustChoice.DO_NOT_TRUST:
trustLevel = TrustLevel.DO_NOT_TRUST;
break;
default:
return;
}
trustedFolders.setValue(cwd, trustLevel);
setIsFolderTrustDialogOpen(false);
}, []);
return {
isTrusted,
isFolderTrustDialogOpen,
handleFolderTrustSelect,
};