diff --git a/src/components/ChatInterface.jsx b/src/components/ChatInterface.jsx index a9d5110..909d718 100644 --- a/src/components/ChatInterface.jsx +++ b/src/components/ChatInterface.jsx @@ -93,6 +93,10 @@ function unescapeWithMathProtection(text) { return processedText; } +function escapeRegExp(value) { + return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); +} + // Small wrapper to keep markdown behavior consistent in one place const Markdown = ({ children, className }) => { const content = normalizeInlineCodeFences(String(children ?? '')); @@ -1855,6 +1859,7 @@ function ChatInterface({ selectedProject, selectedSession, ws, sendMessage, mess const messagesEndRef = useRef(null); const textareaRef = useRef(null); const inputContainerRef = useRef(null); + const inputHighlightRef = useRef(null); const scrollContainerRef = useRef(null); const isLoadingSessionRef = useRef(false); // Track session loading to prevent multiple scrolls const isLoadingMoreRef = useRef(false); @@ -1867,6 +1872,7 @@ function ChatInterface({ selectedProject, selectedSession, ws, sendMessage, mess const [debouncedInput, setDebouncedInput] = useState(''); const [showFileDropdown, setShowFileDropdown] = useState(false); const [fileList, setFileList] = useState([]); + const [fileMentions, setFileMentions] = useState([]); const [filteredFiles, setFilteredFiles] = useState([]); const [selectedFileIndex, setSelectedFileIndex] = useState(-1); const [cursorPosition, setCursorPosition] = useState(0); @@ -3962,6 +3968,7 @@ function ChatInterface({ selectedProject, selectedSession, ws, sendMessage, mess return result; }; + // Handle @ symbol detection and file filtering useEffect(() => { const textBeforeCursor = input.slice(0, cursorPosition); @@ -3992,6 +3999,43 @@ function ChatInterface({ selectedProject, selectedSession, ws, sendMessage, mess } }, [input, cursorPosition, fileList]); + const activeFileMentions = useMemo(() => { + if (!input || fileMentions.length === 0) return []; + return fileMentions.filter(path => input.includes(path)); + }, [fileMentions, input]); + + const sortedFileMentions = useMemo(() => { + if (activeFileMentions.length === 0) return []; + const unique = Array.from(new Set(activeFileMentions)); + return unique.sort((a, b) => b.length - a.length); + }, [activeFileMentions]); + + const fileMentionRegex = useMemo(() => { + if (sortedFileMentions.length === 0) return null; + const pattern = sortedFileMentions.map(escapeRegExp).join('|'); + return new RegExp(`(${pattern})`, 'g'); + }, [sortedFileMentions]); + + const fileMentionSet = useMemo(() => new Set(sortedFileMentions), [sortedFileMentions]); + + const renderInputWithMentions = useCallback((text) => { + if (!text) return ''; + if (!fileMentionRegex) return text; + const parts = text.split(fileMentionRegex); + return parts.map((part, index) => ( + fileMentionSet.has(part) ? ( + + {part} + + ) : ( + {part} + ) + )); + }, [fileMentionRegex, fileMentionSet]); + // Debounced input handling useEffect(() => { const timer = setTimeout(() => { @@ -4566,8 +4610,8 @@ function ChatInterface({ selectedProject, selectedSession, ws, sendMessage, mess const spaceIndex = textAfterAtQuery.indexOf(' '); const textAfterQuery = spaceIndex !== -1 ? textAfterAtQuery.slice(spaceIndex) : ''; - const newInput = textBeforeAt + '@' + file.path + ' ' + textAfterQuery; - const newCursorPos = textBeforeAt.length + 1 + file.path.length + 1; + const newInput = textBeforeAt + file.path + ' ' + textAfterQuery; + const newCursorPos = textBeforeAt.length + file.path.length + 1; // Immediately ensure focus is maintained if (textareaRef.current && !textareaRef.current.matches(':focus')) { @@ -4577,6 +4621,7 @@ function ChatInterface({ selectedProject, selectedSession, ws, sendMessage, mess // Update input and cursor position setInput(newInput); setCursorPosition(newCursorPos); + setFileMentions(prev => (prev.includes(file.path) ? prev : [...prev, file.path])); // Hide dropdown setShowFileDropdown(false); @@ -4670,6 +4715,12 @@ function ChatInterface({ selectedProject, selectedSession, ws, sendMessage, mess } }; + const syncInputOverlayScroll = useCallback((target) => { + if (!inputHighlightRef.current || !target) return; + inputHighlightRef.current.scrollTop = target.scrollTop; + inputHighlightRef.current.scrollLeft = target.scrollLeft; + }, []); + const handleTextareaClick = (e) => { setCursorPosition(e.target.selectionStart); }; @@ -5370,6 +5421,16 @@ function ChatInterface({ selectedProject, selectedSession, ws, sendMessage, mess