import { View, Text, ScrollView, TextInput, Pressable, Platform } from 'react-native' import { ArrowUp, ChevronDown, ChevronsRight, Keyboard as KeyboardIcon, Monitor, Plus, Smartphone } from 'lucide-react-native' import { triggerMediumImpact } from '../platform/haptics' import { createTerminalLiveAccessoryInput } from '../terminal/terminal-live-accessory-input' import { getTerminalCommandKeyboardType, getTerminalLiveInputKeyboardType } from '../terminal/terminal-keyboard-type' import { MobileTerminalLiveInputStatus } from './MobileTerminalLiveInputStatus' import { MobileTerminalInputActions } from './MobileTerminalInputActions' import { isTerminalPhoneDisplayMode } from './mobile-session-route-helpers' import { colors } from '../theme/mobile-theme' import { styles } from './mobile-session-styles' import type { MobileSessionController } from './use-mobile-session-controller' export function MobileSessionCommandDock({ controller }: { controller: MobileSessionController }) { const { insets, bufferedTerminalDraftState, autocompleteEnabled, liveInputCapture, activeHandle, customKeys, setShowCustomKeyModal, setDeleteKeyTarget, visibleBuiltInAccessoryKeys, terminalModes, canPaste, dictationMode, liveInputRef, commandInputRef, handleLiveInputChange, handleLiveInputKeyPress, handleLiveInputSubmit, getLiveInteractionGeneration, getSendCompletionGeneration, dismissKeyboardAfterAgentSend, activeSessionTab, canSend, canCompose, liveInputEnabled, focusLiveInput, showNativeChat, dictation, cancelDictation, handleDictationToggle, handleDictationPressIn, handleDictationPressOut, toggleDisplayMode, handleSend, handleAccessoryKey, dismissSoftwareKeyboard, toggleLiveInput, stopAccessoryRepeat, startAccessoryRepeat, handlePaste, isAttaching, attachImage, activeMarkdownTab, activeFileTab, activeBrowserTab, keyboardLift } = controller return ( !activeMarkdownTab && !activeFileTab && !activeBrowserTab && !showNativeChat && ( {/* Accessory keys */} {/* Why: fixed keyboard escape hatch; outside ScrollView + shortcut path so it can't scroll away or be hidden (#5106). */} {keyboardLift > 0 && ( [ styles.keyboardDismissKey, pressed && styles.accessoryKeyPressed ]} onPress={dismissSoftwareKeyboard} hitSlop={8} accessibilityRole="button" accessibilityLabel="Dismiss keyboard" accessibilityHint="Hides the software keyboard and keeps the current terminal session open." > )} {/* Why: default tap handling makes the first accessory-key tap dismiss the keyboard and get swallowed (#5106). */} [ styles.accessoryKey, pressed && styles.accessoryKeyPressed, !canSend && styles.accessoryKeyDisabled ]} disabled={!canSend} onPress={() => { if (activeHandle) { void toggleDisplayMode(activeHandle) } }} accessibilityLabel={ isTerminalPhoneDisplayMode(activeHandle, terminalModes) ? 'Switch to desktop mode' : 'Switch to phone mode' } > {isTerminalPhoneDisplayMode(activeHandle, terminalModes) ? ( ) : ( )} [ styles.accessoryKey, liveInputEnabled && styles.accessoryKeyActive, pressed && styles.accessoryKeyPressed, !canCompose && styles.accessoryKeyDisabled ]} // Why: offline, live mode is dead but the buffered box still composes — keep the escape hatch tappable (#6713). disabled={!canCompose} onPress={toggleLiveInput} accessibilityLabel={ liveInputEnabled ? 'Switch to buffered command input' : 'Switch to live terminal input' } > {canPaste && ( [ styles.accessoryKey, pressed && styles.accessoryKeyPressed, !canSend && styles.accessoryKeyDisabled ]} disabled={!canSend} onPress={() => void handlePaste()} accessibilityLabel="Paste from clipboard" > Paste )} {visibleBuiltInAccessoryKeys.map((key) => ( [ styles.accessoryKey, pressed && styles.accessoryKeyPressed, !canSend && styles.accessoryKeyDisabled ]} disabled={!canSend} onPressIn={() => { if (!key.repeatable) { return } const input = createTerminalLiveAccessoryInput(key) void handleAccessoryKey(input) startAccessoryRepeat(input) }} onPressOut={() => { if (key.repeatable) { stopAccessoryRepeat() } }} onPress={() => { if (key.repeatable) { return } void handleAccessoryKey(createTerminalLiveAccessoryInput(key)) }} accessibilityLabel={key.accessibilityLabel ?? `Send ${key.label}`} > {key.label} ))} {customKeys.map((key) => ( [ styles.accessoryKey, styles.customAccessoryKey, pressed && styles.accessoryKeyPressed, !canSend && styles.accessoryKeyDisabled ]} disabled={!canSend} onPress={() => void handleAccessoryKey({ bytes: key.bytes })} onLongPress={() => { triggerMediumImpact() setDeleteKeyTarget(key) }} delayLongPress={400} accessibilityLabel={`Send ${key.label}`} > {key.label} ))} [styles.accessoryKey, pressed && styles.accessoryKeyPressed]} onPress={() => setShowCustomKeyModal(true)} accessibilityLabel="Add custom shortcut" > {/* Input bar */} {liveInputEnabled ? ( [ styles.liveInputFocusTarget, pressed && styles.liveInputFocusTargetPressed, !canSend && styles.liveInputFocusTargetDisabled ]} disabled={!canSend} onPress={focusLiveInput} accessibilityRole="button" accessibilityLabel="Show keyboard for live terminal input" accessibilityHint="Typed text is sent directly to the active terminal" > void attachImage('library')} onAttachFile={() => void attachImage('files')} onDictationToggle={handleDictationToggle} onDictationPressIn={handleDictationPressIn} onDictationPressOut={handleDictationPressOut} onDictationCancel={cancelDictation} /> { const submit = handleLiveInputSubmit() const sendOrigin = { tab: activeSessionTab, generation: getSendCompletionGeneration(), interaction: getLiveInteractionGeneration() } void submit.then((accepted) => dismissKeyboardAfterAgentSend( sendOrigin, accepted && sendOrigin.interaction === getLiveInteractionGeneration() ) ) }} placeholder="" showSoftInputOnFocus autoCapitalize="none" autoCorrect={false} spellCheck={false} smartInsertDelete={false} // Why: iOS textContentType overrides autoComplete and can narrow the keyboard; keep IME switching available. autoComplete="off" keyboardType={getTerminalLiveInputKeyboardType(Platform.OS)} returnKeyType="default" blurOnSubmit={false} editable={canSend} importantForAutofill="no" /> ) : ( void handleSend()} /> void attachImage('library')} onAttachFile={() => void attachImage('files')} onDictationToggle={handleDictationToggle} onDictationPressIn={handleDictationPressIn} onDictationPressOut={handleDictationPressOut} onDictationCancel={cancelDictation} /> void handleSend()} accessibilityLabel="Send command" > )} ) ) }