/** * @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 = ({ 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()); // Avoid "Thinking... Thinking..." duplication if primaryText already starts with "Thinking" const thinkingIndicator = hasThoughtIndicator && !primaryText?.startsWith('Thinking') ? 'Thinking... ' : ''; const cancelAndTimerContent = showCancelAndTimer && streamingState !== StreamingState.WaitingForConfirmation ? `(esc to cancel, ${elapsedTime < 60 ? `${elapsedTime}s` : formatDuration(elapsedTime * 1000)})` : null; if (inline) { return ( {primaryText && ( {thinkingIndicator} {primaryText} {primaryText === INTERACTIVE_SHELL_WAITING_PHRASE && ( {' '} (press tab to focus) )} )} {cancelAndTimerContent && ( <> {cancelAndTimerContent} )} ); } return ( {/* Main loading line */} {primaryText && ( {thinkingIndicator} {primaryText} {primaryText === INTERACTIVE_SHELL_WAITING_PHRASE && ( {' '} (press tab to focus) )} )} {!isNarrow && cancelAndTimerContent && ( <> {cancelAndTimerContent} )} {!isNarrow && {/* Spacer */}} {!isNarrow && rightContent && {rightContent}} {isNarrow && cancelAndTimerContent && ( {cancelAndTimerContent} )} {isNarrow && rightContent && {rightContent}} ); };