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:
@@ -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>
|
||||||
|
|||||||
Reference in New Issue
Block a user