feat(ui): composer UX refresh

- Implement refreshed multi-row status area with flattened visibility logic.
- Stabilize Composer row heights to prevent layout jumping during debounce and typing.
- Refactor renderStatusRow to use a direct flow for Mini(ized) mode, Shell Waiting, and status states.
- Relocate ToastDisplay to top Composer row
- Migrate Composer tests to use real ToastDisplay component and content-based assertions.
- Regenerate all CLI UI snapshots to match the final architecture.
This commit is contained in:
Jarrod Whelan
2026-03-04 16:26:38 -08:00
committed by Jarrod Whelan
parent cd7dced951
commit c210b57ab9
52 changed files with 1326 additions and 1082 deletions

View File

@@ -15,30 +15,47 @@ 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';
import { GENERIC_WORKING_LABEL } from '../textConstants.js';
import type { LoadingPhrasesMode } from '../../config/settings.js';
interface LoadingIndicatorProps {
currentLoadingPhrase?: string;
wittyPhrase?: string;
showWit?: boolean;
showTips?: boolean;
loadingPhrases?: LoadingPhrasesMode;
errorVerbosity?: 'low' | 'full';
elapsedTime: number;
inline?: boolean;
rightContent?: React.ReactNode;
thought?: ThoughtSummary | null;
thoughtLabel?: string;
showCancelAndTimer?: boolean;
forceRealStatusOnly?: boolean;
}
export const LoadingIndicator: React.FC<LoadingIndicatorProps> = ({
currentLoadingPhrase,
wittyPhrase,
showWit: showWitProp,
showTips: _showTipsProp,
loadingPhrases = 'all',
errorVerbosity: _errorVerbosity = 'full',
elapsedTime,
inline = false,
rightContent,
thought,
thoughtLabel,
showCancelAndTimer = true,
forceRealStatusOnly = false,
}) => {
const streamingState = useStreamingContext();
const { columns: terminalWidth } = useTerminalSize();
const isNarrow = isNarrowWidth(terminalWidth);
const showWit =
showWitProp ?? (loadingPhrases === 'witty' || loadingPhrases === 'all');
if (
streamingState === StreamingState.Idle &&
!currentLoadingPhrase &&
@@ -54,7 +71,10 @@ export const LoadingIndicator: React.FC<LoadingIndicatorProps> = ({
? currentLoadingPhrase
: thought?.subject
? (thoughtLabel ?? thought.subject)
: currentLoadingPhrase;
: currentLoadingPhrase ||
(streamingState === StreamingState.Responding
? GENERIC_WORKING_LABEL
: undefined);
const hasThoughtIndicator =
currentLoadingPhrase !== INTERACTIVE_SHELL_WAITING_PHRASE &&
Boolean(thought?.subject?.trim());
@@ -67,9 +87,21 @@ export const LoadingIndicator: React.FC<LoadingIndicatorProps> = ({
const cancelAndTimerContent =
showCancelAndTimer &&
streamingState !== StreamingState.WaitingForConfirmation
? `(esc to cancel, ${elapsedTime < 60 ? `${elapsedTime}s` : formatDuration(elapsedTime * 1000)})`
? `esc to cancel, ${elapsedTime < 60 ? `${elapsedTime}s` : formatDuration(elapsedTime * 1000)}`
: null;
const wittyPhraseNode =
!forceRealStatusOnly &&
showWit &&
wittyPhrase &&
primaryText === GENERIC_WORKING_LABEL ? (
<Box marginLeft={1}>
<Text color={theme.text.secondary} dimColor italic>
{wittyPhrase}
</Text>
</Box>
) : null;
if (inline) {
return (
<Box>
@@ -96,6 +128,7 @@ export const LoadingIndicator: React.FC<LoadingIndicatorProps> = ({
)}
</Box>
)}
{wittyPhraseNode}
{cancelAndTimerContent && (
<>
<Box flexShrink={0} width={1} />
@@ -138,6 +171,7 @@ export const LoadingIndicator: React.FC<LoadingIndicatorProps> = ({
)}
</Box>
)}
{wittyPhraseNode}
{!isNarrow && cancelAndTimerContent && (
<>
<Box flexShrink={0} width={1} />