import { html, useState, useRef, useCallback, useMemo, useEffect } from '../lib/preact.js'; import { getStatusMeta } from '../utils/status.js'; export function SimpleInput({ sessionId, status, onRespond, autocompleteConfig = null }) { const [text, setText] = useState(''); const [focused, setFocused] = useState(false); const [sending, setSending] = useState(false); const [error, setError] = useState(null); const [triggerInfo, setTriggerInfo] = useState(null); const [showAutocomplete, setShowAutocomplete] = useState(false); const [selectedIndex, setSelectedIndex] = useState(0); const textareaRef = useRef(null); const autocompleteRef = useRef(null); const meta = getStatusMeta(status); // Detect if cursor is at a trigger position for autocomplete const getTriggerInfo = useCallback((value, cursorPos) => { // No config means no autocomplete if (!autocompleteConfig) return null; const { trigger } = autocompleteConfig; // Find the start of the current "word" (after last whitespace before cursor) let wordStart = cursorPos; while (wordStart > 0 && !/\s/.test(value[wordStart - 1])) { wordStart--; } // Check if word starts with this agent's trigger character if (value[wordStart] === trigger) { return { trigger, filterText: value.slice(wordStart + 1, cursorPos).toLowerCase(), replaceStart: wordStart, replaceEnd: cursorPos, }; } return null; }, [autocompleteConfig]); // Filter skills based on user input after trigger const filteredSkills = useMemo(() => { if (!autocompleteConfig || !triggerInfo) return []; const { skills } = autocompleteConfig; const { filterText } = triggerInfo; let filtered = skills; if (filterText) { filtered = skills.filter(s => s.name.toLowerCase().includes(filterText) ); } // Server pre-sorts, but re-sort after filtering for stability return filtered.sort((a, b) => a.name.localeCompare(b.name)); }, [autocompleteConfig, triggerInfo]); // Show/hide autocomplete based on trigger detection useEffect(() => { const shouldShow = triggerInfo !== null; setShowAutocomplete(shouldShow); // Reset selection when dropdown opens if (shouldShow) { setSelectedIndex(0); } }, [triggerInfo]); // Clamp selectedIndex when filtered list changes useEffect(() => { if (filteredSkills.length > 0 && selectedIndex >= filteredSkills.length) { setSelectedIndex(filteredSkills.length - 1); } }, [filteredSkills.length, selectedIndex]); // Insert a selected skill into the text const insertSkill = useCallback((skill) => { if (!triggerInfo || !autocompleteConfig) return; const { trigger } = autocompleteConfig; const { replaceStart, replaceEnd } = triggerInfo; const before = text.slice(0, replaceStart); const after = text.slice(replaceEnd); const inserted = `${trigger}${skill.name} `; setText(before + inserted + after); setShowAutocomplete(false); setTriggerInfo(null); // Move cursor after inserted text const newCursorPos = replaceStart + inserted.length; setTimeout(() => { if (textareaRef.current) { textareaRef.current.selectionStart = newCursorPos; textareaRef.current.selectionEnd = newCursorPos; textareaRef.current.focus(); } }, 0); }, [text, triggerInfo, autocompleteConfig]); const handleSubmit = async (e) => { e.preventDefault(); e.stopPropagation(); if (text.trim() && !sending) { setSending(true); setError(null); try { await onRespond(sessionId, text.trim(), true, 0); setText(''); } catch (err) { setError('Failed to send message'); console.error('SimpleInput send error:', err); } finally { setSending(false); // Refocus the textarea after submission // Use setTimeout to ensure React has re-rendered with disabled=false setTimeout(() => { textareaRef.current?.focus(); }, 0); } } }; return html`
e.stopPropagation()}> ${error && html`
${error}
`}