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 '../../ink.js'; 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 : } {("external" as string) === 'ant' && isUndercover() && undercover} {("external" as string) === '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_0 => s_0.replBridgeConnected); // biome-ignore lint/correctness/useHookAtTopLevel: feature() is a compile-time constant const sessionActive = useAppState(s_1 => s_1.replBridgeSessionActive); // biome-ignore lint/correctness/useHookAtTopLevel: feature() is a compile-time constant const reconnecting = useAppState(s_2 => s_2.replBridgeReconnecting); // biome-ignore lint/correctness/useHookAtTopLevel: feature() is a compile-time constant const explicit = useAppState(s_3 => s_3.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} ; }