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}
)
}