Files
gemini-cli/packages/cli/src/ui/components/Footer.tsx
Keith Guerin f451f747f4 feat(cli): implement stable 2-row footer layout with responsive collision handling
This commit introduces a new, more stable footer architecture that addresses
several long-standing UX issues:
- Stabilizes the layout by anchoring mode indicators and context summaries
- Protects safety indicators (YOLO/Plan) from being hidden by notifications
- Decouples ambient tips/wit from real system status to prevent confusion
- Implements intelligent collision detection for narrow terminal windows
- Keeps input visible but disabled during tool approval pauses
- Enhances visual consistency with unified status colors and hook icons
2026-03-01 08:59:31 -08:00

200 lines
6.6 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* @license
* Copyright 2026 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import type React from 'react';
import { Box, Text } from 'ink';
import { theme } from '../semantic-colors.js';
import {
shortenPath,
tildeifyPath,
getDisplayString,
} from '@google/gemini-cli-core';
import { ConsoleSummaryDisplay } from './ConsoleSummaryDisplay.js';
import process from 'node:process';
import { MemoryUsageDisplay } from './MemoryUsageDisplay.js';
import { ContextUsageDisplay } from './ContextUsageDisplay.js';
import { QuotaDisplay } from './QuotaDisplay.js';
import { DebugProfiler } from './DebugProfiler.js';
import { isDevelopment } from '../../utils/installationInfo.js';
import { useUIState } from '../contexts/UIStateContext.js';
import { useConfig } from '../contexts/ConfigContext.js';
import { useSettings } from '../contexts/SettingsContext.js';
import { useVimMode } from '../contexts/VimModeContext.js';
export const Footer: React.FC = () => {
const uiState = useUIState();
const config = useConfig();
const settings = useSettings();
const { vimEnabled, vimMode } = useVimMode();
const {
model,
targetDir,
debugMode,
branchName,
debugMessage,
corgiMode,
errorCount,
showErrorDetails,
promptTokenCount,
isTrustedFolder,
terminalWidth,
quotaStats,
} = {
model: uiState.currentModel,
targetDir: config.getTargetDir(),
debugMode: config.getDebugMode(),
branchName: uiState.branchName,
debugMessage: uiState.debugMessage,
corgiMode: uiState.corgiMode,
errorCount: uiState.errorCount,
showErrorDetails: uiState.showErrorDetails,
promptTokenCount: uiState.sessionStats.lastPromptTokenCount,
isTrustedFolder: uiState.isTrustedFolder,
terminalWidth: uiState.terminalWidth,
quotaStats: uiState.quota.stats,
};
const showMemoryUsage =
config.getDebugMode() || settings.merged.ui.showMemoryUsage;
const isFullErrorVerbosity = settings.merged.ui.errorVerbosity === 'full';
const showErrorSummary =
!showErrorDetails && errorCount > 0 && (isFullErrorVerbosity || debugMode);
const hideCWD = settings.merged.ui.footer.hideCWD;
const hideSandboxStatus = settings.merged.ui.footer.hideSandboxStatus;
const hideModelInfo = settings.merged.ui.footer.hideModelInfo;
const hideContextPercentage = settings.merged.ui.footer.hideContextPercentage;
const pathLength = Math.max(20, Math.floor(terminalWidth * 0.25));
const displayPath = shortenPath(tildeifyPath(targetDir), pathLength);
const justifyContent = hideCWD && hideModelInfo ? 'center' : 'space-between';
const displayVimMode = vimEnabled ? vimMode : undefined;
const showDebugProfiler = debugMode || isDevelopment;
return (
<Box
justifyContent={justifyContent}
width={terminalWidth}
flexDirection="row"
alignItems="center"
paddingX={1}
paddingBottom={0}
marginBottom={0}
>
{(showDebugProfiler || displayVimMode || !hideCWD) && (
<Box>
{showDebugProfiler && <DebugProfiler />}
{displayVimMode && (
<Text color={theme.text.secondary}>[{displayVimMode}] </Text>
)}
{!hideCWD && (
<Text color={theme.text.primary}>
{displayPath}
{branchName && (
<Text color={theme.text.secondary}> ({branchName}*)</Text>
)}
</Text>
)}
{debugMode && (
<Text color={theme.status.error}>
{' ' + (debugMessage || '--debug')}
</Text>
)}
</Box>
)}
{/* Middle Section: Centered Trust/Sandbox Info */}
{!hideSandboxStatus && (
<Box
flexGrow={1}
alignItems="center"
justifyContent="center"
display="flex"
>
{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']})
</Text>
</Text>
) : (
<Text color={theme.status.error}>
no sandbox
{terminalWidth >= 100 && (
<Text color={theme.text.secondary}> (see /docs)</Text>
)}
</Text>
)}
</Box>
)}
{/* Right Section: Gemini Label and Console Summary */}
{!hideModelInfo && (
<Box alignItems="center" justifyContent="flex-end">
<Box alignItems="center">
<Text color={theme.text.primary}>
<Text color={theme.text.secondary}>/model </Text>
{getDisplayString(model)}
{!hideContextPercentage && (
<>
{' '}
<ContextUsageDisplay
promptTokenCount={promptTokenCount}
model={model}
terminalWidth={terminalWidth}
/>
</>
)}
{quotaStats && (
<>
{' '}
<QuotaDisplay
remaining={quotaStats.remaining}
limit={quotaStats.limit}
resetTime={quotaStats.resetTime}
terse={true}
/>
</>
)}
</Text>
{showMemoryUsage && <MemoryUsageDisplay />}
</Box>
<Box alignItems="center">
{corgiMode && (
<Box paddingLeft={1} flexDirection="row">
<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>
</Box>
)}
{showErrorSummary && (
<Box paddingLeft={1} flexDirection="row">
<Text color={theme.ui.comment}>| </Text>
<ConsoleSummaryDisplay errorCount={errorCount} />
</Box>
)}
</Box>
</Box>
)}
</Box>
);
};