/** * @license * Copyright 2025 Vybestack LLC * SPDX-License-Identifier: Apache-2.0 */ import type { ThoughtSummary } from '@vybestack/llxprt-code-core'; import type React from 'react'; import { Box, Text } from 'ink'; import { Colors } from '../colors.js'; import { useStreamingContext } from '../contexts/StreamingContext.js'; import { StreamingState } from '../types.js'; import { RespondingSpinner } from './RespondingSpinner.js'; import { formatDuration } from '../utils/formatters.js'; import { INTERACTIVE_SHELL_WAITING_PHRASE } from '../hooks/usePhraseCycler.js'; import { firstNonEmptyString } from '../../utils/coalesce.js'; /** * Format timer text for display. */ function formatTimerText(elapsedTime: number): string { return elapsedTime < 60 ? `${elapsedTime}s` : formatDuration(elapsedTime * 1000); } interface LoadingIndicatorProps { currentLoadingPhrase?: string; elapsedTime: number; rightContent?: React.ReactNode; thought?: ThoughtSummary | null; } export const LoadingIndicator: React.FC = ({ currentLoadingPhrase, elapsedTime, rightContent, thought, }) => { const streamingState = useStreamingContext(); if (streamingState === StreamingState.Idle) { return null; } const hasRightContent = Boolean(rightContent); const isShellFocusHint = currentLoadingPhrase === INTERACTIVE_SHELL_WAITING_PHRASE; const isActionRequired = streamingState === StreamingState.WaitingForConfirmation || isShellFocusHint; const primaryText = isActionRequired ? currentLoadingPhrase : firstNonEmptyString(thought?.subject, currentLoadingPhrase); const timerText = streamingState === StreamingState.WaitingForConfirmation ? '' : ` (esc to cancel, ${formatTimerText(elapsedTime)})`; const lineText = primaryText ? `${primaryText}${timerText}` : timerText.trimStart(); return ( {/* Main loading line */} {lineText && ( {lineText} )} {} {hasRightContent ? ( {rightContent} ) : null} ); };