Skip to content

Conversation

@blackmammoth
Copy link
Collaborator

@blackmammoth blackmammoth commented Jan 15, 2026

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:

  • Added state management for fileMentions and logic to track file paths inserted via the file dropdown, ensuring that mentioned files are recognized and not duplicated.
  • Implemented logic to detect active file mentions in the input, sort them by length, and generate a regular expression for highlighting.
  • Created the renderInputWithMentions function to render file mentions as visually distinct spans within the input overlay.

Input Overlay and Synchronization:

  • Added a transparent overlay above the textarea to display the input with highlighted mentions, and ensured the overlay scrolls in sync with the textarea using the syncInputOverlayScroll function and event handlers.

File Mention Insertion Behavior:

  • Updated the file mention insertion logic to remove the @ symbol, so only the file path is inserted and tracked as a mention.

Utility Improvements:

  • Added the escapeRegExp utility function to safely build regular expressions for file mention detection.

Resolves Issue 279 (#279)

Summary by CodeRabbit

  • New Features
    • Added dynamic visual overlay that highlights file path mentions as you compose messages in the chat input
    • Implemented synchronized scrolling between the input field and the mention highlight display area
    • Improved file mention tracking and management with automatic duplicate prevention and better organization of referenced files

✏️ Tip: You can customize this high-level summary in your review settings.

@blackmammoth blackmammoth requested a review from viper151 January 15, 2026 18:18
@coderabbitai
Copy link
Contributor

coderabbitai bot commented Jan 15, 2026

Walkthrough

Enhances 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

Cohort / File(s) Summary
File Mention Overlay Feature
src/components/ChatInterface.jsx
Introduces dynamic overlay rendering of highlighted file path mentions in input text. Adds escapeRegExp helper, state variables (fileMentions, activeFileMentions, sortedFileMentions, fileMentionRegex, fileMentionSet), memoized computations, and renderInputWithMentions callback. Modifies file selection to insert file path directly (without @) and track mention in state. Implements syncInputOverlayScroll to synchronize overlay scrolling with textarea.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Poem

🐰 A mention so fine, with files all in sight,
Highlighted overlay, glowing so bright!
Regex escapes dance, scrolling synced true,
File paths now sparkle—mention review! ✨

🚥 Pre-merge checks | ✅ 2 | ❌ 1
❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 66.67% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly describes the main change: adding visual highlighting for file mentions in the chat input, which aligns with the core functionality introduced in the changeset.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
  • 📝 Generate docstrings

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Contributor

@coderabbitai coderabbitai bot left a 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: Guard selectFile against invalid atSymbolPosition.
If selectFile() is called when atSymbolPosition === -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 clearing fileMentions when input is sent/cleared.
Right now fileMentions can grow unbounded across messages, even though it’s only used for the current input.

📜 Review details

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between f8d1ec7 and 9da8e69.

📒 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.

Comment on lines +96 to +98
function escapeRegExp(value) {
return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

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.

Comment on lines +4002 to +4038
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]);

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

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.

Suggested change
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]);

Comment on lines +4718 to +4723
const syncInputOverlayScroll = useCallback((target) => {
if (!inputHighlightRef.current || !target) return;
inputHighlightRef.current.scrollTop = target.scrollTop;
inputHighlightRef.current.scrollLeft = target.scrollLeft;
}, []);

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants