import React from 'react'; import { renderPlaceholder } from '../hooks/renderPlaceholder.js'; import { usePasteHandler } from '../hooks/usePasteHandler.js'; import { useDeclaredCursor } from '@anthropic/ink'; import { Ansi, Box, Text, useInput } from '@anthropic/ink'; import type { BaseInputState, BaseTextInputProps } from '../types/textInputTypes.js'; import type { TextHighlight } from '../utils/textHighlighting.js'; import { HighlightedInput } from './PromptInput/ShimmeredInput.js'; type BaseTextInputComponentProps = BaseTextInputProps & { inputState: BaseInputState; children?: React.ReactNode; terminalFocus: boolean; highlights?: TextHighlight[]; invert?: (text: string) => string; hidePlaceholderText?: boolean; }; /** * A base component for text inputs that handles rendering and basic input */ export function BaseTextInput({ inputState, children, terminalFocus, invert, hidePlaceholderText, ...props }: BaseTextInputComponentProps): React.ReactNode { const { onInput, renderedValue, cursorLine, cursorColumn } = inputState; // Park the native terminal cursor at the input caret. Terminal emulators // position IME preedit text at the physical cursor, and screen readers / // screen magnifiers track it — so parking here makes CJK input appear // inline and lets accessibility tools follow the input. The Box ref below // is the yoga layout origin; (cursorLine, cursorColumn) is relative to it. // Only active when the input is focused, showing its cursor, and the // terminal itself has focus. const cursorRef = useDeclaredCursor({ line: cursorLine, column: cursorColumn, active: Boolean(props.focus && props.showCursor && terminalFocus), }); const { wrappedOnInput, isPasting } = usePasteHandler({ onPaste: props.onPaste, onInput: (input, key) => { // Prevent Enter key from triggering submission during paste if (isPasting && key.return) { return; } onInput(input, key); }, onImagePaste: props.onImagePaste, }); // Notify parent when paste state changes const { onIsPastingChange } = props; React.useEffect(() => { if (onIsPastingChange) { onIsPastingChange(isPasting); } }, [isPasting, onIsPastingChange]); const { showPlaceholder, renderedPlaceholder } = renderPlaceholder({ placeholder: props.placeholder, value: props.value, showCursor: props.showCursor, focus: props.focus, terminalFocus, invert, hidePlaceholderText, }); useInput(wrappedOnInput, { isActive: props.focus }); // Show argument hint only when we have a value and the hint is provided // Only show the argument hint when: // 1. We have a hint to show // 2. We have a command typed (value is not empty) // 3. The command doesn't have arguments yet (no text after the space) // 4. We're actually typing a command (the value starts with /) const commandWithoutArgs = (props.value && props.value.trim().indexOf(' ') === -1) || (props.value && props.value.endsWith(' ')); const showArgumentHint = Boolean( props.argumentHint && props.value && commandWithoutArgs && props.value.startsWith('/'), ); // Filter out highlights that contain the cursor position const cursorFiltered = props.showCursor && props.highlights ? props.highlights.filter(h => h.dimColor || props.cursorOffset < h.start || props.cursorOffset >= h.end) : props.highlights; // Adjust highlights for viewport windowing: highlight positions reference the // full input text, but renderedValue only contains the windowed subset. const { viewportCharOffset, viewportCharEnd } = inputState; const filteredHighlights = cursorFiltered && viewportCharOffset > 0 ? cursorFiltered .filter(h => h.end > viewportCharOffset && h.start < viewportCharEnd) .map(h => ({ ...h, start: Math.max(0, h.start - viewportCharOffset), end: h.end - viewportCharOffset, })) : cursorFiltered; const hasHighlights = filteredHighlights && filteredHighlights.length > 0; if (hasHighlights) { return ( {showArgumentHint && ( {props.value?.endsWith(' ') ? '' : ' '} {props.argumentHint} )} {children} ); } return ( {showPlaceholder && props.placeholderElement ? ( props.placeholderElement ) : showPlaceholder && renderedPlaceholder ? ( {renderedPlaceholder} ) : ( {renderedValue} )} {showArgumentHint && ( {props.value?.endsWith(' ') ? '' : ' '} {props.argumentHint} )} {children} ); }