mirror of
https://github.com/google-gemini/gemini-cli.git
synced 2026-03-13 15:40:57 -07:00
140 lines
4.2 KiB
TypeScript
140 lines
4.2 KiB
TypeScript
/**
|
|
* @license
|
|
* Copyright 2025 Google LLC
|
|
* SPDX-License-Identifier: Apache-2.0
|
|
*/
|
|
|
|
import type { ThoughtSummary } from '@google/gemini-cli-core';
|
|
import type React from 'react';
|
|
import { Box, Text } from 'ink';
|
|
import { theme } from '../semantic-colors.js';
|
|
import { useStreamingContext } from '../contexts/StreamingContext.js';
|
|
import { StreamingState } from '../types.js';
|
|
import { GeminiRespondingSpinner } from './GeminiRespondingSpinner.js';
|
|
import { formatDuration } from '../utils/formatters.js';
|
|
import { useTerminalSize } from '../hooks/useTerminalSize.js';
|
|
import { isNarrowWidth } from '../utils/isNarrowWidth.js';
|
|
import { INTERACTIVE_SHELL_WAITING_PHRASE } from '../hooks/usePhraseCycler.js';
|
|
|
|
interface LoadingIndicatorProps {
|
|
currentLoadingPhrase?: string;
|
|
elapsedTime: number;
|
|
inline?: boolean;
|
|
rightContent?: React.ReactNode;
|
|
thought?: ThoughtSummary | null;
|
|
thoughtLabel?: string;
|
|
showCancelAndTimer?: boolean;
|
|
}
|
|
|
|
export const LoadingIndicator: React.FC<LoadingIndicatorProps> = ({
|
|
currentLoadingPhrase,
|
|
elapsedTime,
|
|
inline = false,
|
|
rightContent,
|
|
thought,
|
|
thoughtLabel,
|
|
showCancelAndTimer = true,
|
|
}) => {
|
|
const streamingState = useStreamingContext();
|
|
const { columns: terminalWidth } = useTerminalSize();
|
|
const isNarrow = isNarrowWidth(terminalWidth);
|
|
|
|
if (
|
|
streamingState === StreamingState.Idle &&
|
|
!currentLoadingPhrase &&
|
|
!thought
|
|
) {
|
|
return null;
|
|
}
|
|
|
|
// Prioritize the interactive shell waiting phrase over the thought subject
|
|
// because it conveys an actionable state for the user (waiting for input).
|
|
const primaryText =
|
|
currentLoadingPhrase === INTERACTIVE_SHELL_WAITING_PHRASE
|
|
? currentLoadingPhrase
|
|
: thought?.subject
|
|
? (thoughtLabel ?? thought.subject)
|
|
: currentLoadingPhrase;
|
|
const hasThoughtIndicator =
|
|
currentLoadingPhrase !== INTERACTIVE_SHELL_WAITING_PHRASE &&
|
|
Boolean(thought?.subject?.trim());
|
|
const thinkingIndicator = hasThoughtIndicator ? '💬 ' : '';
|
|
|
|
const cancelAndTimerContent =
|
|
showCancelAndTimer &&
|
|
streamingState !== StreamingState.WaitingForConfirmation
|
|
? `(esc to cancel, ${elapsedTime < 60 ? `${elapsedTime}s` : formatDuration(elapsedTime * 1000)})`
|
|
: null;
|
|
|
|
if (inline) {
|
|
return (
|
|
<Box>
|
|
<Box marginRight={1}>
|
|
<GeminiRespondingSpinner
|
|
nonRespondingDisplay={
|
|
streamingState === StreamingState.WaitingForConfirmation
|
|
? '⠏'
|
|
: ''
|
|
}
|
|
/>
|
|
</Box>
|
|
{primaryText && (
|
|
<Text color={theme.text.primary} italic wrap="truncate-end">
|
|
{thinkingIndicator}
|
|
{primaryText}
|
|
</Text>
|
|
)}
|
|
{cancelAndTimerContent && (
|
|
<>
|
|
<Box flexShrink={0} width={1} />
|
|
<Text color={theme.text.secondary}>{cancelAndTimerContent}</Text>
|
|
</>
|
|
)}
|
|
</Box>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<Box paddingLeft={0} flexDirection="column">
|
|
{/* Main loading line */}
|
|
<Box
|
|
width="100%"
|
|
flexDirection={isNarrow ? 'column' : 'row'}
|
|
alignItems={isNarrow ? 'flex-start' : 'center'}
|
|
>
|
|
<Box>
|
|
<Box marginRight={1}>
|
|
<GeminiRespondingSpinner
|
|
nonRespondingDisplay={
|
|
streamingState === StreamingState.WaitingForConfirmation
|
|
? '⠏'
|
|
: ''
|
|
}
|
|
/>
|
|
</Box>
|
|
{primaryText && (
|
|
<Text color={theme.text.primary} italic wrap="truncate-end">
|
|
{thinkingIndicator}
|
|
{primaryText}
|
|
</Text>
|
|
)}
|
|
{!isNarrow && cancelAndTimerContent && (
|
|
<>
|
|
<Box flexShrink={0} width={1} />
|
|
<Text color={theme.text.secondary}>{cancelAndTimerContent}</Text>
|
|
</>
|
|
)}
|
|
</Box>
|
|
{!isNarrow && <Box flexGrow={1}>{/* Spacer */}</Box>}
|
|
{!isNarrow && rightContent && <Box>{rightContent}</Box>}
|
|
</Box>
|
|
{isNarrow && cancelAndTimerContent && (
|
|
<Box>
|
|
<Text color={theme.text.secondary}>{cancelAndTimerContent}</Text>
|
|
</Box>
|
|
)}
|
|
{isNarrow && rightContent && <Box>{rightContent}</Box>}
|
|
</Box>
|
|
);
|
|
};
|