From a44ea49cf7fbb4da5ea9bfc154b91bc9fbb10b72 Mon Sep 17 00:00:00 2001 From: Keith Guerin Date: Thu, 26 Mar 2026 17:18:40 -0700 Subject: [PATCH] feat(cli): unify session modes in footer and stabilize Composer layout --- Composer.tsx.conflicted | 457 ++++++++++++++++ composer-layout-spec.md | 79 +++ packages/cli/src/config/footerItems.ts | 31 +- .../components/ApprovalModeIndicator.test.tsx | 60 -- .../ui/components/ApprovalModeIndicator.tsx | 69 --- .../cli/src/ui/components/Composer.test.tsx | 430 +++------------ packages/cli/src/ui/components/Composer.tsx | 134 ++++- packages/cli/src/ui/components/Footer.tsx | 54 +- .../components/RawMarkdownIndicator.test.tsx | 42 -- .../ui/components/RawMarkdownIndicator.tsx | 23 - .../ui/components/ShellModeIndicator.test.tsx | 18 - .../src/ui/components/ShellModeIndicator.tsx | 18 - .../cli/src/ui/components/ShortcutsHint.tsx | 29 + packages/cli/src/ui/components/StatusRow.tsx | 513 ++++++------------ .../components/UnifiedModeIndicator.test.tsx | 113 ++++ .../ui/components/UnifiedModeIndicator.tsx | 103 ++++ .../__snapshots__/Composer.test.tsx.snap | 20 +- 17 files changed, 1201 insertions(+), 992 deletions(-) create mode 100644 Composer.tsx.conflicted create mode 100644 composer-layout-spec.md delete mode 100644 packages/cli/src/ui/components/ApprovalModeIndicator.test.tsx delete mode 100644 packages/cli/src/ui/components/ApprovalModeIndicator.tsx delete mode 100644 packages/cli/src/ui/components/RawMarkdownIndicator.test.tsx delete mode 100644 packages/cli/src/ui/components/RawMarkdownIndicator.tsx delete mode 100644 packages/cli/src/ui/components/ShellModeIndicator.test.tsx delete mode 100644 packages/cli/src/ui/components/ShellModeIndicator.tsx create mode 100644 packages/cli/src/ui/components/ShortcutsHint.tsx create mode 100644 packages/cli/src/ui/components/UnifiedModeIndicator.test.tsx create mode 100644 packages/cli/src/ui/components/UnifiedModeIndicator.tsx diff --git a/Composer.tsx.conflicted b/Composer.tsx.conflicted new file mode 100644 index 0000000000..ddcb389344 --- /dev/null +++ b/Composer.tsx.conflicted @@ -0,0 +1,457 @@ +/** + * @license + * Copyright 2026 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +<<<<<<< HEAD +import { Box, useIsScreenReaderEnabled } from 'ink'; +import { useState, useEffect } from 'react'; +import { useConfig } from '../contexts/ConfigContext.js'; +import { useSettings } from '../contexts/SettingsContext.js'; +import { useUIState } from '../contexts/UIStateContext.js'; +import { useUIActions } from '../contexts/UIActionsContext.js'; +import { useVimMode } from '../contexts/VimModeContext.js'; +import { useAlternateBuffer } from '../hooks/useAlternateBuffer.js'; +import { useTerminalSize } from '../hooks/useTerminalSize.js'; +import { isNarrowWidth } from '../utils/isNarrowWidth.js'; +======= +import { useState, useEffect, useMemo } from 'react'; +import { Box, useIsScreenReaderEnabled } from 'ink'; +import { CoreToolCallStatus } from '@google/gemini-cli-core'; +import { LoadingIndicator } from './LoadingIndicator.js'; +import { StatusDisplay } from './StatusDisplay.js'; +>>>>>>> 96b0876e6 (feat(cli): unify session modes in footer and reorganize composer layout) +import { ToastDisplay, shouldShowToast } from './ToastDisplay.js'; +import { DetailedMessagesDisplay } from './DetailedMessagesDisplay.js'; +import { ShortcutsHelp } from './ShortcutsHelp.js'; +import { ShortcutsHint } from './ShortcutsHint.js'; +import { InputPrompt } from './InputPrompt.js'; +import { Footer } from './Footer.js'; +import { StatusRow } from './StatusRow.js'; +import { ShowMoreLines } from './ShowMoreLines.js'; +import { QueuedMessageDisplay } from './QueuedMessageDisplay.js'; +import { ContextUsageDisplay } from './ContextUsageDisplay.js'; +import { HorizontalLine } from './shared/HorizontalLine.js'; +import { OverflowProvider } from '../contexts/OverflowContext.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 { useAlternateBuffer } from '../hooks/useAlternateBuffer.js'; +import { StreamingState, type HistoryItemToolGroup } from '../types.js'; +import { ConfigInitDisplay } from '../components/ConfigInitDisplay.js'; +import { TodoTray } from './messages/Todo.js'; +<<<<<<< HEAD +import { useComposerStatus } from '../hooks/useComposerStatus.js'; +======= +import { getInlineThinkingMode } from '../utils/inlineThinkingMode.js'; +import { isContextUsageHigh } from '../utils/contextUsage.js'; +>>>>>>> 96b0876e6 (feat(cli): unify session modes in footer and reorganize composer layout) + +export const Composer = ({ isFocused = true }: { isFocused?: boolean }) => { + const config = useConfig(); + const settings = useSettings(); + const isScreenReaderEnabled = useIsScreenReaderEnabled(); + const uiState = useUIState(); + const uiActions = useUIActions(); + const { vimEnabled, vimMode } = useVimMode(); + const inlineThinkingMode = getInlineThinkingMode(settings); + const terminalWidth = uiState.terminalWidth; + const isNarrow = isNarrowWidth(terminalWidth); + const debugConsoleMaxHeight = Math.floor(Math.max(terminalWidth * 0.2, 5)); + const [suggestionsVisible, setSuggestionsVisible] = useState(false); + + const isAlternateBuffer = useAlternateBuffer(); +<<<<<<< HEAD +======= + const { showApprovalModeIndicator } = uiState; +>>>>>>> 96b0876e6 (feat(cli): unify session modes in footer and reorganize composer layout) + const showUiDetails = uiState.cleanUiDetailsVisible; + const suggestionsPosition = isAlternateBuffer ? 'above' : 'below'; + const hideContextSummary = + suggestionsVisible && suggestionsPosition === 'above'; + + const { hasPendingActionRequired, shouldCollapseDuringApproval } = + useComposerStatus(); + + const isPassiveShortcutsHelpState = + uiState.isInputActive && + uiState.streamingState === 'idle' && + !hasPendingActionRequired; + + const { setShortcutsHelpVisible } = uiActions; + + useEffect(() => { + if (uiState.shortcutsHelpVisible && !isPassiveShortcutsHelpState) { + setShortcutsHelpVisible(false); + } + }, [ + uiState.shortcutsHelpVisible, + isPassiveShortcutsHelpState, + setShortcutsHelpVisible, + ]); + +<<<<<<< HEAD + const showShortcutsHelp = + uiState.shortcutsHelpVisible && + uiState.streamingState === 'idle' && + !hasPendingActionRequired; + +======= + const hideUiDetailsForSuggestions = + suggestionsVisible && suggestionsPosition === 'above'; + const hideShortcutsHintForSuggestions = hideUiDetailsForSuggestions; + const isModelIdle = uiState.streamingState === StreamingState.Idle; + const isModelResponding = + uiState.streamingState === StreamingState.Responding; + const isBufferEmpty = uiState.buffer.text.length === 0; + const canShowShortcutsHint = + (isModelIdle || isModelResponding) && + isBufferEmpty && + !hasPendingActionRequired; + + const [showShortcutsHintDebounced, setShowShortcutsHintDebounced] = + useState(canShowShortcutsHint); + + useEffect(() => { + if (!canShowShortcutsHint) { + setShowShortcutsHintDebounced(false); + return; + } + + const timeout = setTimeout(() => { + setShowShortcutsHintDebounced(true); + }, 200); + + return () => clearTimeout(timeout); + }, [canShowShortcutsHint]); + + /** + * Use the setting if provided, otherwise default to true for the new UX. + * This allows tests to override the collapse behavior. + */ + const shouldCollapseDuringApproval = + settings.merged.ui.collapseDrawerDuringApproval !== false; + +>>>>>>> 96b0876e6 (feat(cli): unify session modes in footer and reorganize composer layout) + if (hasPendingActionRequired && shouldCollapseDuringApproval) { + return null; + } + + const showShortcutsHelp = + uiState.shortcutsHelpVisible && + uiState.streamingState === StreamingState.Idle && + !hasPendingActionRequired; + const hasToast = shouldShowToast(uiState); +<<<<<<< HEAD + const hideUiDetailsForSuggestions = + suggestionsVisible && suggestionsPosition === 'above'; + + // Mini Mode VIP Flags (Pure Content Triggers) + const showMinimalToast = hasToast; +======= + const showLoadingIndicator = + (!uiState.embeddedShellFocused || uiState.isBackgroundShellVisible) && + uiState.streamingState === StreamingState.Responding && + !hasPendingActionRequired; + + const hasMinimalStatusBleedThrough = shouldShowToast(uiState); + + const showMinimalContextBleedThrough = + !settings.merged.ui.footer.hideContextPercentage && + isContextUsageHigh( + uiState.sessionStats.lastPromptTokenCount, + typeof uiState.currentModel === 'string' + ? uiState.currentModel + : undefined, + ); + + const shouldReserveSpaceForShortcutsHint = + settings.merged.ui.showShortcutsHint && + !hideShortcutsHintForSuggestions && + !hasPendingActionRequired; + const showShortcutsHint = + shouldReserveSpaceForShortcutsHint && showShortcutsHintDebounced; + const showMinimalInlineLoading = !showUiDetails && showLoadingIndicator; + const showMinimalBleedThroughRow = + !showUiDetails && + (hasMinimalStatusBleedThrough || showMinimalContextBleedThrough); + const showMinimalMetaRow = + !showUiDetails && + (showMinimalInlineLoading || + showMinimalBleedThroughRow || + shouldReserveSpaceForShortcutsHint); + + const loadingPhrases = settings.merged.ui.loadingPhrases; + const showTips = loadingPhrases === 'tips' || loadingPhrases === 'all'; + const showWit = loadingPhrases === 'witty' || loadingPhrases === 'all'; +>>>>>>> 96b0876e6 (feat(cli): unify session modes in footer and reorganize composer layout) + + return ( + + {(!uiState.slashCommands || + !uiState.isConfigInitialized || + uiState.isResuming) && ( + + )} + + {showUiDetails && ( + + )} + + {showUiDetails && } + +<<<<<<< HEAD + {showShortcutsHelp && } + + {(showUiDetails || showMinimalToast) && ( + + + + )} + + + +======= + + + + {showUiDetails && (hasToast ? : null)} + + + {showUiDetails && showShortcutsHint && } + + + {showMinimalMetaRow && ( + + + {showMinimalInlineLoading && ( + + )} + {hasMinimalStatusBleedThrough && ( + + + + )} + + {(showMinimalContextBleedThrough || + shouldReserveSpaceForShortcutsHint) && ( + + {showMinimalContextBleedThrough && ( + + )} + + {showShortcutsHint && } + + + )} + + )} + {showShortcutsHelp && } + {showUiDetails && } + {showUiDetails && ( + + + {showLoadingIndicator && ( + + )} + + + + {!showLoadingIndicator && ( + + )} + + + )} +>>>>>>> 96b0876e6 (feat(cli): unify session modes in footer and reorganize composer layout) + + + {showUiDetails && uiState.showErrorDetails && ( + + + + + + + )} + + {uiState.isInputActive && ( + + )} + + {showUiDetails && + !settings.merged.ui.hideFooter && + !isScreenReaderEnabled &&