mirror of
https://github.com/google-gemini/gemini-cli.git
synced 2026-03-14 08:01:02 -07:00
Co-authored-by: Jacob Richman <jacob314@gmail.com> Co-authored-by: Arya Gummadi <aryagummadi@google.com>
212 lines
7.6 KiB
TypeScript
212 lines
7.6 KiB
TypeScript
/**
|
|
* @license
|
|
* Copyright 2025 Google LLC
|
|
* SPDX-License-Identifier: Apache-2.0
|
|
*/
|
|
|
|
import { Box, Text } from 'ink';
|
|
import { useMemo } from 'react';
|
|
import { LoadingIndicator } from './LoadingIndicator.js';
|
|
import { ContextSummaryDisplay } from './ContextSummaryDisplay.js';
|
|
import { AutoAcceptIndicator } from './AutoAcceptIndicator.js';
|
|
import { ShellModeIndicator } from './ShellModeIndicator.js';
|
|
import { DetailedMessagesDisplay } from './DetailedMessagesDisplay.js';
|
|
import { InputPrompt, calculatePromptWidths } from './InputPrompt.js';
|
|
import { Footer, type FooterProps } from './Footer.js';
|
|
import { ShowMoreLines } from './ShowMoreLines.js';
|
|
import { OverflowProvider } from '../contexts/OverflowContext.js';
|
|
import { theme } from '../semantic-colors.js';
|
|
import { isNarrowWidth } from '../utils/isNarrowWidth.js';
|
|
import { useUIState } from '../contexts/UIStateContext.js';
|
|
import { useUIActions } from '../contexts/UIActionsContext.js';
|
|
import { useVimMode } from '../contexts/VimModeContext.js';
|
|
import { useConfig } from '../contexts/ConfigContext.js';
|
|
import { useSettings } from '../contexts/SettingsContext.js';
|
|
import { ApprovalMode } from '@google/gemini-cli-core';
|
|
import { StreamingState } from '../types.js';
|
|
import { ConfigInitDisplay } from '../components/ConfigInitDisplay.js';
|
|
|
|
const MAX_DISPLAYED_QUEUED_MESSAGES = 3;
|
|
|
|
export const Composer = () => {
|
|
const config = useConfig();
|
|
const settings = useSettings();
|
|
const uiState = useUIState();
|
|
const uiActions = useUIActions();
|
|
const { vimEnabled, vimMode } = useVimMode();
|
|
const terminalWidth = process.stdout.columns;
|
|
const isNarrow = isNarrowWidth(terminalWidth);
|
|
const debugConsoleMaxHeight = Math.floor(Math.max(terminalWidth * 0.2, 5));
|
|
|
|
const { contextFileNames, showAutoAcceptIndicator } = uiState;
|
|
|
|
// Use the container width of InputPrompt for width of DetailedMessagesDisplay
|
|
const { containerWidth } = useMemo(
|
|
() => calculatePromptWidths(uiState.terminalWidth),
|
|
[uiState.terminalWidth],
|
|
);
|
|
|
|
// Build footer props from context values
|
|
const footerProps: Omit<FooterProps, 'vimMode'> = {
|
|
model: config.getModel(),
|
|
targetDir: config.getTargetDir(),
|
|
debugMode: config.getDebugMode(),
|
|
branchName: uiState.branchName,
|
|
debugMessage: uiState.debugMessage,
|
|
corgiMode: uiState.corgiMode,
|
|
errorCount: uiState.errorCount,
|
|
showErrorDetails: uiState.showErrorDetails,
|
|
showMemoryUsage:
|
|
config.getDebugMode() || settings.merged.ui?.showMemoryUsage || false,
|
|
promptTokenCount: uiState.sessionStats.lastPromptTokenCount,
|
|
nightly: uiState.nightly,
|
|
isTrustedFolder: uiState.isTrustedFolder,
|
|
hideCWD: settings.merged.ui?.footer?.hideCWD || false,
|
|
hideSandboxStatus: settings.merged.ui?.footer?.hideSandboxStatus || false,
|
|
hideModelInfo: settings.merged.ui?.footer?.hideModelInfo || false,
|
|
};
|
|
|
|
return (
|
|
<Box flexDirection="column">
|
|
{!uiState.shellFocused && (
|
|
<LoadingIndicator
|
|
thought={
|
|
uiState.streamingState === StreamingState.WaitingForConfirmation ||
|
|
config.getAccessibility()?.disableLoadingPhrases
|
|
? undefined
|
|
: uiState.thought
|
|
}
|
|
currentLoadingPhrase={
|
|
config.getAccessibility()?.disableLoadingPhrases
|
|
? undefined
|
|
: uiState.currentLoadingPhrase
|
|
}
|
|
elapsedTime={uiState.elapsedTime}
|
|
/>
|
|
)}
|
|
|
|
{!uiState.isConfigInitialized && <ConfigInitDisplay />}
|
|
|
|
{uiState.messageQueue.length > 0 && (
|
|
<Box flexDirection="column" marginTop={1}>
|
|
{uiState.messageQueue
|
|
.slice(0, MAX_DISPLAYED_QUEUED_MESSAGES)
|
|
.map((message, index) => {
|
|
const preview = message.replace(/\s+/g, ' ');
|
|
|
|
return (
|
|
<Box key={index} paddingLeft={2} width="100%">
|
|
<Text dimColor wrap="truncate">
|
|
{preview}
|
|
</Text>
|
|
</Box>
|
|
);
|
|
})}
|
|
{uiState.messageQueue.length > MAX_DISPLAYED_QUEUED_MESSAGES && (
|
|
<Box paddingLeft={2}>
|
|
<Text dimColor>
|
|
... (+
|
|
{uiState.messageQueue.length -
|
|
MAX_DISPLAYED_QUEUED_MESSAGES}{' '}
|
|
more)
|
|
</Text>
|
|
</Box>
|
|
)}
|
|
</Box>
|
|
)}
|
|
|
|
<Box
|
|
marginTop={1}
|
|
justifyContent={
|
|
settings.merged.ui?.hideContextSummary
|
|
? 'flex-start'
|
|
: 'space-between'
|
|
}
|
|
width="100%"
|
|
flexDirection={isNarrow ? 'column' : 'row'}
|
|
alignItems={isNarrow ? 'flex-start' : 'center'}
|
|
>
|
|
<Box marginRight={1}>
|
|
{process.env['GEMINI_SYSTEM_MD'] && (
|
|
<Text color={theme.status.error}>|⌐■_■| </Text>
|
|
)}
|
|
{uiState.ctrlCPressedOnce ? (
|
|
<Text color={theme.status.warning}>
|
|
Press Ctrl+C again to exit.
|
|
</Text>
|
|
) : uiState.ctrlDPressedOnce ? (
|
|
<Text color={theme.status.warning}>
|
|
Press Ctrl+D again to exit.
|
|
</Text>
|
|
) : uiState.showEscapePrompt ? (
|
|
<Text color={theme.text.secondary}>Press Esc again to clear.</Text>
|
|
) : (
|
|
!settings.merged.ui?.hideContextSummary && (
|
|
<ContextSummaryDisplay
|
|
ideContext={uiState.ideContextState}
|
|
geminiMdFileCount={uiState.geminiMdFileCount}
|
|
contextFileNames={contextFileNames}
|
|
mcpServers={config.getMcpServers()}
|
|
blockedMcpServers={config.getBlockedMcpServers()}
|
|
showToolDescriptions={uiState.showToolDescriptions}
|
|
/>
|
|
)
|
|
)}
|
|
</Box>
|
|
<Box paddingTop={isNarrow ? 1 : 0}>
|
|
{showAutoAcceptIndicator !== ApprovalMode.DEFAULT &&
|
|
!uiState.shellModeActive && (
|
|
<AutoAcceptIndicator approvalMode={showAutoAcceptIndicator} />
|
|
)}
|
|
{uiState.shellModeActive && <ShellModeIndicator />}
|
|
</Box>
|
|
</Box>
|
|
|
|
{uiState.showErrorDetails && (
|
|
<OverflowProvider>
|
|
<Box flexDirection="column">
|
|
<DetailedMessagesDisplay
|
|
messages={uiState.filteredConsoleMessages}
|
|
maxHeight={
|
|
uiState.constrainHeight ? debugConsoleMaxHeight : undefined
|
|
}
|
|
width={containerWidth}
|
|
/>
|
|
<ShowMoreLines constrainHeight={uiState.constrainHeight} />
|
|
</Box>
|
|
</OverflowProvider>
|
|
)}
|
|
|
|
{uiState.isInputActive && (
|
|
<InputPrompt
|
|
buffer={uiState.buffer}
|
|
inputWidth={uiState.inputWidth}
|
|
suggestionsWidth={uiState.suggestionsWidth}
|
|
onSubmit={uiActions.handleFinalSubmit}
|
|
userMessages={uiState.userMessages}
|
|
onClearScreen={uiActions.handleClearScreen}
|
|
config={config}
|
|
slashCommands={uiState.slashCommands}
|
|
commandContext={uiState.commandContext}
|
|
shellModeActive={uiState.shellModeActive}
|
|
setShellModeActive={uiActions.setShellModeActive}
|
|
approvalMode={showAutoAcceptIndicator}
|
|
onEscapePromptChange={uiActions.onEscapePromptChange}
|
|
focus={uiState.isFocused}
|
|
vimHandleInput={uiActions.vimHandleInput}
|
|
isShellFocused={uiState.shellFocused}
|
|
placeholder={
|
|
vimEnabled
|
|
? " Press 'i' for INSERT mode and 'Esc' for NORMAL mode."
|
|
: ' Type your message or @path/to/file'
|
|
}
|
|
/>
|
|
)}
|
|
|
|
{!settings.merged.ui?.hideFooter && (
|
|
<Footer {...footerProps} vimMode={vimEnabled ? vimMode : undefined} />
|
|
)}
|
|
</Box>
|
|
);
|
|
};
|