refactor: scope mention autocomplete lists to active tab

Files/directories/recentFiles render only on the 'files' tab; agents
only on 'agents'. Memoize visible slices and drop loading spinner when
the files tab is inactive.
This commit is contained in:
Bohdan Triapitsyn
2026-04-24 12:30:11 +03:00
parent 6a70d51ea7
commit 8e610ca8b3
@@ -18,6 +18,8 @@ type AgentInfo = {
description?: string; description?: string;
mode?: string | null; mode?: string | null;
}; };
const EMPTY_FILES: FileInfo[] = [];
const EMPTY_AGENTS: AgentInfo[] = [];
export interface FileMentionHandle { export interface FileMentionHandle {
handleKeyDown: (key: string) => void; handleKeyDown: (key: string) => void;
@@ -84,8 +86,6 @@ export const FileMentionAutocomplete = React.forwardRef<FileMentionHandle, FileM
const containerRef = React.useRef<HTMLDivElement | null>(null); const containerRef = React.useRef<HTMLDivElement | null>(null);
const ignoreTabClickRef = React.useRef(false); const ignoreTabClickRef = React.useRef(false);
const normalizedSearchQuery = (searchQuery ?? '').trim(); const normalizedSearchQuery = (searchQuery ?? '').trim();
const visibleAgents = normalizedSearchQuery.length > 0 ? agents : agents.slice(0, 2);
const recentFiles = React.useMemo(() => { const recentFiles = React.useMemo(() => {
if (!projectRoot || !projectTabs) { if (!projectRoot || !projectTabs) {
return [] as FileInfo[]; return [] as FileInfo[];
@@ -123,6 +123,15 @@ export const FileMentionAutocomplete = React.forwardRef<FileMentionHandle, FileM
return mapped; return mapped;
}, [normalizedSearchQuery, projectRoot, projectTabs]); }, [normalizedSearchQuery, projectRoot, projectTabs]);
const visibleAgents = React.useMemo(
() => activeTab === 'agents'
? (normalizedSearchQuery.length > 0 ? agents : agents.slice(0, 2))
: EMPTY_AGENTS,
[activeTab, agents, normalizedSearchQuery.length],
);
const visibleDirectories = activeTab === 'files' ? directories : EMPTY_FILES;
const visibleRecentFiles = activeTab === 'files' ? recentFiles : EMPTY_FILES;
const visibleFiles = activeTab === 'files' ? files : EMPTY_FILES;
React.useEffect(() => { React.useEffect(() => {
const handlePointerDown = (event: MouseEvent | TouchEvent) => { const handlePointerDown = (event: MouseEvent | TouchEvent) => {
@@ -278,7 +287,7 @@ export const FileMentionAutocomplete = React.forwardRef<FileMentionHandle, FileM
setSelectedIndex(0); setSelectedIndex(0);
setOverflowMap({}); setOverflowMap({});
setMarqueeDurations({}); setMarqueeDurations({});
}, [files, directories, recentFiles.length, visibleAgents.length]); }, [visibleFiles, visibleDirectories, visibleRecentFiles.length, visibleAgents.length]);
React.useEffect(() => { React.useEffect(() => {
itemRefs.current[selectedIndex]?.scrollIntoView({ itemRefs.current[selectedIndex]?.scrollIntoView({
@@ -325,7 +334,7 @@ export const FileMentionAutocomplete = React.forwardRef<FileMentionHandle, FileM
} }
window.removeEventListener('resize', updateOverflow); window.removeEventListener('resize', updateOverflow);
}; };
}, [files, directories]); }, [visibleFiles, visibleDirectories]);
React.useEffect(() => { React.useEffect(() => {
const labelNode = labelRefs.current[selectedIndex]; const labelNode = labelRefs.current[selectedIndex];
@@ -369,7 +378,7 @@ export const FileMentionAutocomplete = React.forwardRef<FileMentionHandle, FileM
return; return;
} }
const total = visibleAgents.length + directories.length + recentFiles.length + files.length; const total = visibleAgents.length + visibleDirectories.length + visibleRecentFiles.length + visibleFiles.length;
if (total === 0) { if (total === 0) {
return; return;
} }
@@ -394,23 +403,23 @@ export const FileMentionAutocomplete = React.forwardRef<FileMentionHandle, FileM
return; return;
} }
const dirIndex = safeIndex - visibleAgents.length; const dirIndex = safeIndex - visibleAgents.length;
if (dirIndex < directories.length) { if (dirIndex < visibleDirectories.length) {
const dir = directories[dirIndex]; const dir = visibleDirectories[dirIndex];
if (dir) { if (dir) {
handleFileSelect(dir); handleFileSelect(dir);
} }
return; return;
} }
const fileIndex = dirIndex - directories.length; const fileIndex = dirIndex - visibleDirectories.length;
const selectedFile = fileIndex < recentFiles.length const selectedFile = fileIndex < visibleRecentFiles.length
? recentFiles[fileIndex] ? visibleRecentFiles[fileIndex]
: files[fileIndex - recentFiles.length]; : visibleFiles[fileIndex - visibleRecentFiles.length];
if (selectedFile) { if (selectedFile) {
handleFileSelect(selectedFile); handleFileSelect(selectedFile);
} }
} }
} }
}), [files, directories, recentFiles, visibleAgents, selectedIndex, onClose, handleFileSelect, handleAgentPick]); }), [visibleFiles, visibleDirectories, visibleRecentFiles, visibleAgents, selectedIndex, onClose, handleFileSelect, handleAgentPick]);
const getFileIcon = (file: FileInfo) => { const getFileIcon = (file: FileInfo) => {
const ext = file.extension?.toLowerCase(); const ext = file.extension?.toLowerCase();
@@ -483,7 +492,7 @@ export const FileMentionAutocomplete = React.forwardRef<FileMentionHandle, FileM
</div> </div>
) : null} ) : null}
<ScrollableOverlay outerClassName="flex-1 min-h-0" className="px-0"> <ScrollableOverlay outerClassName="flex-1 min-h-0" className="px-0">
{loading ? ( {activeTab === 'files' && loading ? (
<div className="flex items-center justify-center py-4"> <div className="flex items-center justify-center py-4">
<RiRefreshLine className="h-4 w-4 animate-spin text-muted-foreground" /> <RiRefreshLine className="h-4 w-4 animate-spin text-muted-foreground" />
</div> </div>
@@ -516,10 +525,10 @@ export const FileMentionAutocomplete = React.forwardRef<FileMentionHandle, FileM
Type to search more agents Type to search more agents
</div> </div>
)} )}
{visibleAgents.length > 0 && (directories.length > 0 || recentFiles.length > 0 || files.length > 0) && ( {visibleAgents.length > 0 && (visibleDirectories.length > 0 || visibleRecentFiles.length > 0 || visibleFiles.length > 0) && (
<div className="my-1 border-t border-border/60" /> <div className="my-1 border-t border-border/60" />
)} )}
{directories.map((dir, index) => { {visibleDirectories.map((dir, index) => {
const rowIndex = visibleAgents.length + index; const rowIndex = visibleAgents.length + index;
const relativePath = dir.relativePath || dir.name; const relativePath = dir.relativePath || dir.name;
const displayPath = truncatePathMiddle(relativePath, { maxLength: 60 }); const displayPath = truncatePathMiddle(relativePath, { maxLength: 60 });
@@ -543,11 +552,11 @@ export const FileMentionAutocomplete = React.forwardRef<FileMentionHandle, FileM
</div> </div>
); );
})} })}
{directories.length > 0 && (recentFiles.length > 0 || files.length > 0) && ( {visibleDirectories.length > 0 && (visibleRecentFiles.length > 0 || visibleFiles.length > 0) && (
<div className="my-1 border-t border-border/60" /> <div className="my-1 border-t border-border/60" />
)} )}
{recentFiles.map((file, index) => { {visibleRecentFiles.map((file, index) => {
const rowIndex = visibleAgents.length + directories.length + index; const rowIndex = visibleAgents.length + visibleDirectories.length + index;
const relativePath = file.relativePath || file.name; const relativePath = file.relativePath || file.name;
const displayPath = truncatePathMiddle(relativePath, { maxLength: 60 }); const displayPath = truncatePathMiddle(relativePath, { maxLength: 60 });
const isSelected = selectedIndex === rowIndex; const isSelected = selectedIndex === rowIndex;
@@ -595,11 +604,11 @@ export const FileMentionAutocomplete = React.forwardRef<FileMentionHandle, FileM
</div> </div>
); );
})} })}
{recentFiles.length > 0 && files.length > 0 && ( {visibleRecentFiles.length > 0 && visibleFiles.length > 0 && (
<div className="my-1 border-t border-border/60" /> <div className="my-1 border-t border-border/60" />
)} )}
{files.map((file, index) => { {visibleFiles.map((file, index) => {
const rowIndex = visibleAgents.length + directories.length + recentFiles.length + index; const rowIndex = visibleAgents.length + visibleDirectories.length + visibleRecentFiles.length + index;
const relativePath = file.relativePath || file.name; const relativePath = file.relativePath || file.name;
const displayPath = truncatePathMiddle(relativePath, { maxLength: 60 }); const displayPath = truncatePathMiddle(relativePath, { maxLength: 60 });
const isSelected = selectedIndex === rowIndex; const isSelected = selectedIndex === rowIndex;
@@ -652,7 +661,7 @@ export const FileMentionAutocomplete = React.forwardRef<FileMentionHandle, FileM
</React.Fragment> </React.Fragment>
); );
})} })}
{files.length === 0 && directories.length === 0 && recentFiles.length === 0 && visibleAgents.length === 0 && ( {visibleFiles.length === 0 && visibleDirectories.length === 0 && visibleRecentFiles.length === 0 && visibleAgents.length === 0 && (
<div className="px-3 py-2 typography-ui-label text-muted-foreground"> <div className="px-3 py-2 typography-ui-label text-muted-foreground">
No matches found No matches found
</div> </div>