mirror of
https://github.com/claude-code-best/claude-code.git
synced 2026-06-17 05:45:51 +00:00
163 lines
5.0 KiB
TypeScript
163 lines
5.0 KiB
TypeScript
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 (
|
|
<Box ref={cursorRef}>
|
|
<HighlightedInput
|
|
text={renderedValue}
|
|
highlights={filteredHighlights}
|
|
/>
|
|
{showArgumentHint && (
|
|
<Text dimColor>
|
|
{props.value?.endsWith(' ') ? '' : ' '}
|
|
{props.argumentHint}
|
|
</Text>
|
|
)}
|
|
{children}
|
|
</Box>
|
|
)
|
|
}
|
|
|
|
return (
|
|
<Box ref={cursorRef}>
|
|
<Text wrap="truncate-end" dimColor={props.dimColor}>
|
|
{showPlaceholder && props.placeholderElement ? (
|
|
props.placeholderElement
|
|
) : showPlaceholder && renderedPlaceholder ? (
|
|
<Ansi>{renderedPlaceholder}</Ansi>
|
|
) : (
|
|
<Ansi>{renderedValue}</Ansi>
|
|
)}
|
|
{showArgumentHint && (
|
|
<Text dimColor>
|
|
{props.value?.endsWith(' ') ? '' : ' '}
|
|
{props.argumentHint}
|
|
</Text>
|
|
)}
|
|
{children}
|
|
</Text>
|
|
</Box>
|
|
)
|
|
}
|