-
Notifications
You must be signed in to change notification settings - Fork 732
feat: add highlight for file mentions in chat input #299
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Conversation
WalkthroughEnhances the ChatInterface component with a file-mention feature that displays inline file path mentions in an overlay. Adds state management for tracking mentions, a memoized regex pattern for matching, synchronized scrolling between overlay and textarea, and modifies file selection to insert paths without the preceding @ symbol. Changes
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~25 minutes Poem
🚥 Pre-merge checks | ✅ 2 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing touches
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 3
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
src/components/ChatInterface.jsx (1)
4613-4625: GuardselectFileagainst invalidatSymbolPosition.
IfselectFile()is called whenatSymbolPosition === -1(racey UI state, future refactor, etc.),slice(0, -1)will corrupt input.Proposed patch
const selectFile = (file) => { + if (!file || atSymbolPosition < 0) return; const textBeforeAt = input.slice(0, atSymbolPosition); const textAfterAtQuery = input.slice(atSymbolPosition); const spaceIndex = textAfterAtQuery.indexOf(' '); const textAfterQuery = spaceIndex !== -1 ? textAfterAtQuery.slice(spaceIndex) : ''; const newInput = textBeforeAt + file.path + ' ' + textAfterQuery; const newCursorPos = textBeforeAt.length + file.path.length + 1;
🤖 Fix all issues with AI agents
In `@src/components/ChatInterface.jsx`:
- Around line 96-98: escapeRegExp can throw if passed a non-string; add a guard
in the escapeRegExp function to handle null/undefined and non-string inputs by
coercing the input to a string (or returning an empty string for null/undefined)
before calling replace so it never calls .replace on a non-string value.
🧹 Nitpick comments (1)
src/components/ChatInterface.jsx (1)
1862-1876: State/refs addition is fine; consider clearingfileMentionswhen input is sent/cleared.
Right nowfileMentionscan grow unbounded across messages, even though it’s only used for the currentinput.
📜 Review details
Configuration used: Repository UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (1)
src/components/ChatInterface.jsx
✏️ Tip: You can disable this entire section by setting review_details to false in your review settings.
| function escapeRegExp(value) { | ||
| return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
escapeRegExp looks correct; add a tiny guard for non-strings.
If value can ever be non-string (defensive), this will throw.
Proposed patch
function escapeRegExp(value) {
- return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
+ return String(value ?? '').replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
}🤖 Prompt for AI Agents
In `@src/components/ChatInterface.jsx` around lines 96 - 98, escapeRegExp can
throw if passed a non-string; add a guard in the escapeRegExp function to handle
null/undefined and non-string inputs by coercing the input to a string (or
returning an empty string for null/undefined) before calling replace so it never
calls .replace on a non-string value.
| 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) ? ( | ||
| <span | ||
| key={`mention-${index}`} | ||
| className="bg-blue-200/70 -ml-0.5 dark:bg-blue-300/40 px-0.5 rounded-md box-decoration-clone text-transparent" | ||
| > | ||
| {part} | ||
| </span> | ||
| ) : ( | ||
| <span key={`text-${index}`}>{part}</span> | ||
| ) | ||
| )); | ||
| }, [fileMentionRegex, fileMentionSet]); | ||
|
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Overlay highlight can desync due to mention span padding/margins affecting wrapping.
px-0.5 and -ml-0.5 change inline layout and can cause different line breaks vs the textarea, so highlights won’t line up (especially near wrap boundaries).
Proposed patch (layout-neutral highlight)
return parts.map((part, index) => (
fileMentionSet.has(part) ? (
<span
key={`mention-${index}`}
- className="bg-blue-200/70 -ml-0.5 dark:bg-blue-300/40 px-0.5 rounded-md box-decoration-clone text-transparent"
+ className="bg-blue-200/70 dark:bg-blue-300/40 rounded-sm box-decoration-clone text-transparent"
>
{part}
</span>
) : (
<span key={`text-${index}`}>{part}</span>
)
));📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| 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) ? ( | |
| <span | |
| key={`mention-${index}`} | |
| className="bg-blue-200/70 -ml-0.5 dark:bg-blue-300/40 px-0.5 rounded-md box-decoration-clone text-transparent" | |
| > | |
| {part} | |
| </span> | |
| ) : ( | |
| <span key={`text-${index}`}>{part}</span> | |
| ) | |
| )); | |
| }, [fileMentionRegex, fileMentionSet]); | |
| 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) ? ( | |
| <span | |
| key={`mention-${index}`} | |
| className="bg-blue-200/70 dark:bg-blue-300/40 rounded-sm box-decoration-clone text-transparent" | |
| > | |
| {part} | |
| </span> | |
| ) : ( | |
| <span key={`text-${index}`}>{part}</span> | |
| ) | |
| )); | |
| }, [fileMentionRegex, fileMentionSet]); |
| const syncInputOverlayScroll = useCallback((target) => { | ||
| if (!inputHighlightRef.current || !target) return; | ||
| inputHighlightRef.current.scrollTop = target.scrollTop; | ||
| inputHighlightRef.current.scrollLeft = target.scrollLeft; | ||
| }, []); | ||
|
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Scroll sync likely broken: overlay uses overflow-hidden, so scrollTop/Left updates may not scroll content.
If the textarea scrolls (multi-line input), highlights may “stay put”.
Proposed patch (make overlay scrollable + hide scrollbars)
- const syncInputOverlayScroll = useCallback((target) => {
+ const syncInputOverlayScroll = useCallback((target) => {
if (!inputHighlightRef.current || !target) return;
inputHighlightRef.current.scrollTop = target.scrollTop;
inputHighlightRef.current.scrollLeft = target.scrollLeft;
}, []); <div
ref={inputHighlightRef}
aria-hidden="true"
- className="absolute inset-0 pointer-events-none overflow-hidden rounded-2xl"
+ className="absolute inset-0 pointer-events-none overflow-auto rounded-2xl no-scrollbar"
> <style>
{`
details[open] .details-chevron {
transform: rotate(180deg);
}
+ .no-scrollbar::-webkit-scrollbar { display: none; }
+ .no-scrollbar { scrollbar-width: none; }
`}
</style>Also applies to: 5424-5433, 5441-5450
This pull request enhances the chat input experience by adding visual highlighting for file mentions within the input box. It introduces logic to detect, track, and highlight file mentions, and ensures the highlighted overlay stays in sync with user input and scrolling. The
@symbol is also removed from the file mentions since the AI agents were thinking that the@was actually part of the file. The main changes are grouped below:File Mention Detection and Highlighting:
fileMentionsand logic to track file paths inserted via the file dropdown, ensuring that mentioned files are recognized and not duplicated.renderInputWithMentionsfunction to render file mentions as visually distinct spans within the input overlay.Input Overlay and Synchronization:
syncInputOverlayScrollfunction and event handlers.File Mention Insertion Behavior:
@symbol, so only the file path is inserted and tracked as a mention.Utility Improvements:
escapeRegExputility function to safely build regular expressions for file mention detection.Resolves Issue 279 (#279)
Summary by CodeRabbit
✏️ Tip: You can customize this high-level summary in your review settings.