import { feature } from 'bun:bundle' import * as React from 'react' import { memo, type ReactNode, useMemo, useRef } from 'react' import { isBridgeEnabled } from '../../bridge/bridgeEnabled.js' import { getBridgeStatus } from '../../bridge/bridgeStatusUtil.js' import { useSetPromptOverlay } from '../../context/promptOverlayContext.js' import type { VerificationStatus } from '../../hooks/useApiKeyVerification.js' import type { IDESelection } from '../../hooks/useIdeSelection.js' import { useSettings } from '../../hooks/useSettings.js' import { useTerminalSize } from '../../hooks/useTerminalSize.js' import { Box, Text } from '@anthropic/ink' import type { MCPServerConnection } from '../../services/mcp/types.js' import { useAppState } from '../../state/AppState.js' import type { ToolPermissionContext } from '../../Tool.js' import type { Message } from '../../types/message.js' import type { PromptInputMode, VimMode } from '../../types/textInputTypes.js' import type { AutoUpdaterResult } from '../../utils/autoUpdater.js' import { isFullscreenEnvEnabled } from '../../utils/fullscreen.js' import { isUndercover } from '../../utils/undercover.js' import { CoordinatorTaskPanel, useCoordinatorTaskCount, } from '../CoordinatorAgentStatus.js' import { getLastAssistantMessageId, StatusLine, statusLineShouldDisplay, } from '../StatusLine.js' import { Notifications } from './Notifications.js' import { PromptInputFooterLeftSide } from './PromptInputFooterLeftSide.js' import { PromptInputFooterSuggestions, type SuggestionItem, } from './PromptInputFooterSuggestions.js' import { PromptInputHelpMenu } from './PromptInputHelpMenu.js' type Props = { apiKeyStatus: VerificationStatus debug: boolean exitMessage: { show: boolean key?: string } vimMode: VimMode | undefined mode: PromptInputMode autoUpdaterResult: AutoUpdaterResult | null isAutoUpdating: boolean verbose: boolean onAutoUpdaterResult: (result: AutoUpdaterResult) => void onChangeIsUpdating: (isUpdating: boolean) => void suggestions: SuggestionItem[] selectedSuggestion: number maxColumnWidth?: number toolPermissionContext: ToolPermissionContext helpOpen: boolean suppressHint: boolean isLoading: boolean tasksSelected: boolean teamsSelected: boolean bridgeSelected: boolean tmuxSelected: boolean teammateFooterIndex?: number ideSelection: IDESelection | undefined mcpClients?: MCPServerConnection[] isPasting?: boolean isInputWrapped?: boolean messages: Message[] isSearching: boolean historyQuery: string setHistoryQuery: (query: string) => void historyFailedMatch: boolean onOpenTasksDialog?: (taskId?: string) => void } function PromptInputFooter({ apiKeyStatus, debug, exitMessage, vimMode, mode, autoUpdaterResult, isAutoUpdating, verbose, onAutoUpdaterResult, onChangeIsUpdating, suggestions, selectedSuggestion, maxColumnWidth, toolPermissionContext, helpOpen, suppressHint: suppressHintFromProps, isLoading, tasksSelected, teamsSelected, bridgeSelected, tmuxSelected, teammateFooterIndex, ideSelection, mcpClients, isPasting = false, isInputWrapped = false, messages, isSearching, historyQuery, setHistoryQuery, historyFailedMatch, onOpenTasksDialog, }: Props): ReactNode { const settings = useSettings() const { columns, rows } = useTerminalSize() const messagesRef = useRef(messages) messagesRef.current = messages const lastAssistantMessageId = useMemo( () => getLastAssistantMessageId(messages), [messages], ) const isNarrow = columns < 80 // In fullscreen the bottom slot is flexShrink:0, so every row here is a row // stolen from the ScrollBox. Drop the optional StatusLine first. Non-fullscreen // has terminal scrollback to absorb overflow, so we never hide StatusLine there. const isFullscreen = isFullscreenEnvEnabled() const isShort = isFullscreen && rows < 24 // Pill highlights when tasks is the active footer item AND no specific // agent row is selected. When coordinatorTaskIndex >= 0 the pointer has // moved into CoordinatorTaskPanel, so the pill should un-highlight. // coordinatorTaskCount === 0 covers the bash-only case (no agent rows // exist, pill is the only selectable item). const coordinatorTaskCount = useCoordinatorTaskCount() const coordinatorTaskIndex = useAppState(s => s.coordinatorTaskIndex) const pillSelected = tasksSelected && (coordinatorTaskCount === 0 || coordinatorTaskIndex < 0) // Hide `? for shortcuts` if the user has a custom status line, or during ctrl-r const suppressHint = suppressHintFromProps || statusLineShouldDisplay(settings) || isSearching // Fullscreen: portal data to FullscreenLayout — see promptOverlayContext.tsx const overlayData = useMemo( () => isFullscreen && suggestions.length ? { suggestions, selectedSuggestion, maxColumnWidth } : null, [isFullscreen, suggestions, selectedSuggestion, maxColumnWidth], ) useSetPromptOverlay(overlayData) if (suggestions.length && !isFullscreen) { return ( ) } if (helpOpen) { return ( ) } return ( <> {mode === 'prompt' && !isShort && !exitMessage.show && !isPasting && statusLineShouldDisplay(settings) && ( )} {isFullscreen ? null : ( )} {process.env.USER_TYPE === 'ant' && isUndercover() && ( undercover )} {process.env.USER_TYPE === 'ant' && } ) } export default memo(PromptInputFooter) type BridgeStatusProps = { bridgeSelected: boolean } function BridgeStatusIndicator({ bridgeSelected, }: BridgeStatusProps): React.ReactNode { if (!feature('BRIDGE_MODE')) return null // biome-ignore lint/correctness/useHookAtTopLevel: feature() is a compile-time constant const enabled = useAppState(s => s.replBridgeEnabled) // biome-ignore lint/correctness/useHookAtTopLevel: feature() is a compile-time constant const connected = useAppState(s => s.replBridgeConnected) // biome-ignore lint/correctness/useHookAtTopLevel: feature() is a compile-time constant const sessionActive = useAppState(s => s.replBridgeSessionActive) // biome-ignore lint/correctness/useHookAtTopLevel: feature() is a compile-time constant const reconnecting = useAppState(s => s.replBridgeReconnecting) // biome-ignore lint/correctness/useHookAtTopLevel: feature() is a compile-time constant const explicit = useAppState(s => s.replBridgeExplicit) // Failed state is surfaced via notification (useReplBridge), not a footer pill. if (!isBridgeEnabled() || !enabled) return null const status = getBridgeStatus({ error: undefined, connected, sessionActive, reconnecting, }) // For implicit (config-driven) remote, only show the reconnecting state if (!explicit && status.label !== 'Remote Control reconnecting') { return null } return ( {status.label} {bridgeSelected && · Enter to view} ) }