(props: ChatInputProps)
| 39 | * them so there's no double-handling. |
| 40 | */ |
| 41 | export function ChatInput(props: ChatInputProps): React.ReactElement { |
| 42 | const { |
| 43 | value, onChange, onSubmit, cwd, placeholder, |
| 44 | accentColor = 'cyan', prefix, active = true, busy = false, historyRef, |
| 45 | } = props; |
| 46 | const [cursor, setCursor] = useState(value.length); |
| 47 | const [anchor, setAnchor] = useState<number | null>(null); // selection start, or null |
| 48 | const [attachments, setAttachments] = useState<Attachment[]>([]); |
| 49 | const histIdx = useRef<number | null>(null); |
| 50 | const draft = useRef(''); |
| 51 | |
| 52 | // Keep the cursor in range if `value` is changed from the outside (Esc clears, |
| 53 | // history recall sets a new string, type-ahead resets to ''). |
| 54 | useEffect(() => { |
| 55 | setCursor(c => Math.max(0, Math.min(c, value.length))); |
| 56 | setAnchor(null); |
| 57 | }, [value]); |
| 58 | |
| 59 | const apply = (t: string, c: number) => { |
| 60 | onChange(t); |
| 61 | setCursor(Math.max(0, Math.min(c, t.length))); |
| 62 | setAnchor(null); // any edit collapses the selection |
| 63 | histIdx.current = null; // and drops out of history-recall mode |
| 64 | }; |
| 65 | |
| 66 | // Selection = the half-open range [min(anchor,cursor), max(anchor,cursor)). |
| 67 | const hasSelection = anchor !== null && anchor !== cursor; |
| 68 | const selFrom = anchor === null ? cursor : Math.min(anchor, cursor); |
| 69 | const selTo = anchor === null ? cursor : Math.max(anchor, cursor); |
| 70 | |
| 71 | const clearAll = () => { |
| 72 | onChange(''); |
| 73 | setCursor(0); |
| 74 | setAnchor(null); |
| 75 | setAttachments([]); |
| 76 | histIdx.current = null; |
| 77 | }; |
| 78 | |
| 79 | const submit = () => { |
| 80 | const parts: string[] = []; |
| 81 | if (value.trim()) parts.push(value); |
| 82 | for (const a of attachments) parts.push(a.payload); |
| 83 | const full = parts.join('\n').trim(); |
| 84 | if (!full) return; |
| 85 | onSubmit(full); |
| 86 | onChange(''); |
| 87 | setCursor(0); |
| 88 | setAnchor(null); |
| 89 | setAttachments([]); |
| 90 | histIdx.current = null; |
| 91 | }; |
| 92 | |
| 93 | useInput((input, key) => { |
| 94 | if (!active) return; |
| 95 | if (key.ctrl && input === 'c') return; // parent: interrupt / exit |
| 96 | |
| 97 | // Esc: while busy, let the parent abort the run. While idle, clear the |
| 98 | // typed text AND any paste/image chips (this is what "Esc removes the |
nothing calls this directly
no test coverage detected