Add footer configuration settings (#7419)

Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com>
This commit is contained in:
Miguel Solorio
2025-09-02 10:35:43 -07:00
committed by GitHub
parent 6a581a695f
commit 0a7f5be81f
8 changed files with 231 additions and 85 deletions

View File

@@ -45,6 +45,10 @@ const MIGRATION_MAP: Record<string, string> = {
hideTips: 'ui.hideTips',
hideBanner: 'ui.hideBanner',
hideFooter: 'ui.hideFooter',
hideCWD: 'ui.footer.hideCWD',
hideSandboxStatus: 'ui.footer.hideSandboxStatus',
hideModelInfo: 'ui.footer.hideModelInfo',
hideContextSummary: 'ui.hideContextSummary',
showMemoryUsage: 'ui.showMemoryUsage',
showLineNumbers: 'ui.showLineNumbers',
showCitations: 'ui.showCitations',

View File

@@ -192,6 +192,55 @@ export const SETTINGS_SCHEMA = {
description: 'Hide the application banner',
showInDialog: true,
},
hideContextSummary: {
type: 'boolean',
label: 'Hide Context Summary',
category: 'UI',
requiresRestart: false,
default: false,
description:
'Hide the context summary (GEMINI.md, MCP servers) above the input.',
showInDialog: true,
},
footer: {
type: 'object',
label: 'Footer',
category: 'UI',
requiresRestart: false,
default: {},
description: 'Settings for the footer.',
showInDialog: false,
properties: {
hideCWD: {
type: 'boolean',
label: 'Hide CWD',
category: 'UI',
requiresRestart: false,
default: false,
description:
'Hide the current working directory path in the footer.',
showInDialog: true,
},
hideSandboxStatus: {
type: 'boolean',
label: 'Hide Sandbox Status',
category: 'UI',
requiresRestart: false,
default: false,
description: 'Hide the sandbox status indicator in the footer.',
showInDialog: true,
},
hideModelInfo: {
type: 'boolean',
label: 'Hide Model Info',
category: 'UI',
requiresRestart: false,
default: false,
description: 'Hide the model name and context usage in the footer.',
showInDialog: true,
},
},
},
hideFooter: {
type: 'boolean',
label: 'Hide Footer',
@@ -793,3 +842,9 @@ type InferSettings<T extends SettingsSchema> = {
};
export type Settings = InferSettings<typeof SETTINGS_SCHEMA>;
export interface FooterSettings {
hideCWD?: boolean;
hideSandboxStatus?: boolean;
hideModelInfo?: boolean;
}

View File

@@ -653,6 +653,42 @@ describe('App UI', () => {
);
});
it('should not display context summary when hideContextSummary is true', async () => {
mockSettings = createMockSettings({
workspace: {
ui: { hideContextSummary: true },
},
});
vi.mocked(ideContext.getIdeContext).mockReturnValue({
workspaceState: {
openFiles: [
{
path: '/path/to/my-file.ts',
isActive: true,
selectedText: 'hello',
timestamp: 0,
},
],
},
});
mockConfig.getGeminiMdFileCount.mockReturnValue(1);
mockConfig.getAllGeminiMdFilenames.mockReturnValue(['GEMINI.md']);
const { lastFrame, unmount } = renderWithProviders(
<App
config={mockConfig as unknown as ServerConfig}
settings={mockSettings}
version={mockVersion}
/>,
);
currentUnmount = unmount;
await Promise.resolve();
const output = lastFrame();
expect(output).not.toContain('Using:');
expect(output).not.toContain('open file');
expect(output).not.toContain('GEMINI.md file');
});
it('should display default "GEMINI.md" in footer when contextFileName is not set and count is 1', async () => {
mockConfig.getGeminiMdFileCount.mockReturnValue(1);
mockConfig.getAllGeminiMdFilenames.mockReturnValue(['GEMINI.md']);

View File

@@ -983,6 +983,8 @@ const App = ({ config, settings, startupWarnings = [], version }: AppProps) => {
? " Press 'i' for INSERT mode and 'Esc' for NORMAL mode."
: ' Type your message or @path/to/file';
const hideContextSummary = settings.merged.ui?.hideContextSummary ?? false;
return (
<StreamingContext.Provider value={streamingState}>
<Box flexDirection="column" width="90%">
@@ -1216,7 +1218,6 @@ const App = ({ config, settings, startupWarnings = [], version }: AppProps) => {
}
elapsedTime={elapsedTime}
/>
{/* Display queued messages below loading indicator */}
{messageQueue.length > 0 && (
<Box flexDirection="column" marginTop={1}>
@@ -1248,10 +1249,11 @@ const App = ({ config, settings, startupWarnings = [], version }: AppProps) => {
)}
</Box>
)}
<Box
marginTop={1}
justifyContent="space-between"
justifyContent={
hideContextSummary ? 'flex-start' : 'space-between'
}
width="100%"
flexDirection={isNarrow ? 'column' : 'row'}
alignItems={isNarrow ? 'flex-start' : 'center'}
@@ -1270,7 +1272,7 @@ const App = ({ config, settings, startupWarnings = [], version }: AppProps) => {
</Text>
) : showEscapePrompt ? (
<Text color={Colors.Gray}>Press Esc again to clear.</Text>
) : (
) : !hideContextSummary ? (
<ContextSummaryDisplay
ideContext={ideContextState}
geminiMdFileCount={geminiMdFileCount}
@@ -1279,9 +1281,12 @@ const App = ({ config, settings, startupWarnings = [], version }: AppProps) => {
blockedMcpServers={config.getBlockedMcpServers()}
showToolDescriptions={showToolDescriptions}
/>
)}
) : null}
</Box>
<Box paddingTop={isNarrow ? 1 : 0}>
<Box
paddingTop={isNarrow ? 1 : 0}
marginLeft={hideContextSummary ? 1 : 2}
>
{showAutoAcceptIndicator !== ApprovalMode.DEFAULT &&
!shellModeActive && (
<AutoAcceptIndicator
@@ -1291,7 +1296,6 @@ const App = ({ config, settings, startupWarnings = [], version }: AppProps) => {
{shellModeActive && <ShellModeIndicator />}
</Box>
</Box>
{showErrorDetails && (
<OverflowProvider>
<Box flexDirection="column">
@@ -1306,7 +1310,6 @@ const App = ({ config, settings, startupWarnings = [], version }: AppProps) => {
</Box>
</OverflowProvider>
)}
{isInputActive && (
<InputPrompt
buffer={buffer}
@@ -1380,6 +1383,9 @@ const App = ({ config, settings, startupWarnings = [], version }: AppProps) => {
nightly={nightly}
vimMode={vimModeEnabled ? vimMode : undefined}
isTrustedFolder={isTrustedFolderState}
hideCWD={settings.merged.ui?.footer?.hideCWD}
hideSandboxStatus={settings.merged.ui?.footer?.hideSandboxStatus}
hideModelInfo={settings.merged.ui?.footer?.hideModelInfo}
/>
)}
</Box>

View File

@@ -6,14 +6,14 @@ exports[`App UI > should render correctly with the prompt input box 1`] = `
╭────────────────────────────────────────────────────────────────────────────────────────╮
│ > Type your message or @path/to/file │
╰────────────────────────────────────────────────────────────────────────────────────────╯
/test/dir no sandbox (see /docs) model (100% context left)"
/test/dir no sandbox (see /docs) model (100% context left)"
`;
exports[`App UI > should render the initial UI correctly 1`] = `
" I'm Feeling Lucky (esc to cancel, 0s)
/test/dir no sandbox (see /docs) model (100% context left)"
/test/dir no sandbox (see /docs) model (100% context left)"
`;
exports[`App UI > when in a narrow terminal > should render with a column layout 1`] = `
@@ -27,5 +27,5 @@ dir
no sandbox (see /docs)
model (100% context left)| ✖ 5 errors (ctrl+o for details)"
model (100% context left) | ✖ 5 errors (ctrl+o for details)"
`;

View File

@@ -156,4 +156,31 @@ describe('<Footer />', () => {
vi.unstubAllEnvs();
});
});
describe('visibility toggles', () => {
it('should hide CWD when hideCWD is true', () => {
const { lastFrame } = renderWithWidth(120, {
...defaultProps,
hideCWD: true,
});
expect(lastFrame()).not.toContain(defaultProps.targetDir);
});
it('should hide sandbox status when hideSandboxStatus is true', () => {
const { lastFrame } = renderWithWidth(120, {
...defaultProps,
isTrustedFolder: true,
hideSandboxStatus: true,
});
expect(lastFrame()).not.toContain('no sandbox');
});
it('should hide model info when hideModelInfo is true', () => {
const { lastFrame } = renderWithWidth(120, {
...defaultProps,
hideModelInfo: true,
});
expect(lastFrame()).not.toContain(defaultProps.model);
});
});
});

View File

@@ -33,6 +33,9 @@ interface FooterProps {
nightly: boolean;
vimMode?: string;
isTrustedFolder?: boolean;
hideCWD?: boolean;
hideSandboxStatus?: boolean;
hideModelInfo?: boolean;
}
export const Footer: React.FC<FooterProps> = ({
@@ -49,6 +52,9 @@ export const Footer: React.FC<FooterProps> = ({
nightly,
vimMode,
isTrustedFolder,
hideCWD = false,
hideSandboxStatus = false,
hideModelInfo = false,
}) => {
const { columns: terminalWidth } = useTerminalSize();
@@ -60,95 +66,107 @@ export const Footer: React.FC<FooterProps> = ({
? path.basename(tildeifyPath(targetDir))
: shortenPath(tildeifyPath(targetDir), pathLength);
const justifyContent = hideCWD && hideModelInfo ? 'center' : 'space-between';
return (
<Box
justifyContent="space-between"
justifyContent={justifyContent}
width="100%"
flexDirection={isNarrow ? 'column' : 'row'}
alignItems={isNarrow ? 'flex-start' : 'center'}
>
<Box>
{debugMode && <DebugProfiler />}
{vimMode && <Text color={theme.text.secondary}>[{vimMode}] </Text>}
{nightly ? (
<Gradient colors={theme.ui.gradient}>
<Text>
{!hideCWD && (
<Box>
{debugMode && <DebugProfiler />}
{vimMode && <Text color={theme.text.secondary}>[{vimMode}] </Text>}
{nightly ? (
<Gradient colors={theme.ui.gradient}>
<Text>
{displayPath}
{branchName && <Text> ({branchName}*)</Text>}
</Text>
</Gradient>
) : (
<Text color={theme.text.link}>
{displayPath}
{branchName && <Text> ({branchName}*)</Text>}
{branchName && (
<Text color={theme.text.secondary}> ({branchName}*)</Text>
)}
</Text>
</Gradient>
) : (
<Text color={theme.text.link}>
{displayPath}
{branchName && (
<Text color={theme.text.secondary}> ({branchName}*)</Text>
)}
</Text>
)}
{debugMode && (
<Text color={theme.status.error}>
{' ' + (debugMessage || '--debug')}
</Text>
)}
</Box>
)}
{debugMode && (
<Text color={theme.status.error}>
{' ' + (debugMessage || '--debug')}
</Text>
)}
</Box>
)}
{/* Middle Section: Centered Trust/Sandbox Info */}
<Box
flexGrow={isNarrow ? 0 : 1}
alignItems="center"
justifyContent={isNarrow ? 'flex-start' : 'center'}
display="flex"
paddingX={isNarrow ? 0 : 1}
paddingTop={isNarrow ? 1 : 0}
>
{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>
) : process.env['SANDBOX'] === 'sandbox-exec' ? (
<Text color={theme.status.warning}>
macOS Seatbelt{' '}
<Text color={theme.text.secondary}>
({process.env['SEATBELT_PROFILE']})
{!hideSandboxStatus && (
<Box
flexGrow={isNarrow || hideCWD || hideModelInfo ? 0 : 1}
alignItems="center"
justifyContent={isNarrow || hideCWD ? 'flex-start' : 'center'}
display="flex"
paddingX={isNarrow ? 0 : 1}
paddingTop={isNarrow ? 1 : 0}
>
{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>
</Text>
) : (
<Text color={theme.status.error}>
no sandbox <Text color={theme.text.secondary}>(see /docs)</Text>
</Text>
)}
</Box>
) : process.env['SANDBOX'] === 'sandbox-exec' ? (
<Text color={theme.status.warning}>
macOS Seatbelt{' '}
<Text color={theme.text.secondary}>
({process.env['SEATBELT_PROFILE']})
</Text>
</Text>
) : (
<Text color={theme.status.error}>
no sandbox <Text color={theme.text.secondary}>(see /docs)</Text>
</Text>
)}
</Box>
)}
{/* Right Section: Gemini Label and Console Summary */}
<Box alignItems="center" paddingTop={isNarrow ? 1 : 0}>
<Text color={theme.text.accent}>
{isNarrow ? '' : ' '}
{model}{' '}
<ContextUsageDisplay
promptTokenCount={promptTokenCount}
model={model}
/>
</Text>
{corgiMode && (
<Text>
<Text color={theme.ui.symbol}>| </Text>
<Text color={theme.status.error}></Text>
<Text color={theme.text.primary}>(´</Text>
<Text color={theme.status.error}></Text>
<Text color={theme.text.primary}>`)</Text>
<Text color={theme.status.error}>▼ </Text>
</Text>
)}
{!showErrorDetails && errorCount > 0 && (
<Box>
<Text color={theme.ui.symbol}>| </Text>
<ConsoleSummaryDisplay errorCount={errorCount} />
{!hideModelInfo && (
<Box alignItems="center">
<Text color={theme.text.accent}>
{isNarrow ? '' : ' '}
{model}{' '}
<ContextUsageDisplay
promptTokenCount={promptTokenCount}
model={model}
/>
</Text>
{showMemoryUsage && <MemoryUsageDisplay />}
</Box>
)}
{showMemoryUsage && <MemoryUsageDisplay />}
<Box alignItems="center" paddingLeft={2}>
{corgiMode && (
<Text>
{!hideModelInfo && <Text color={theme.ui.symbol}>| </Text>}
<Text color={theme.status.error}></Text>
<Text color={theme.text.primary}>(´</Text>
<Text color={theme.status.error}></Text>
<Text color={theme.text.primary}>`)</Text>
<Text color={theme.status.error}>▼ </Text>
</Text>
)}
{!showErrorDetails && errorCount > 0 && (
<Box>
{!hideModelInfo && <Text color={theme.ui.symbol}>| </Text>}
<ConsoleSummaryDisplay errorCount={errorCount} />
</Box>
)}
</Box>
</Box>
</Box>
);

View File

@@ -30,7 +30,7 @@ export const MemoryUsageDisplay: React.FC = () => {
return (
<Box>
<Text color={Colors.Gray}>| </Text>
<Text color={Colors.Gray}> | </Text>
<Text color={memoryUsageColor}>{memoryUsage}</Text>
</Box>
);