Skip to content

Commit 15adbe9

Browse files
Fix AI context selector crash
Fixes #659
1 parent 17e6268 commit 15adbe9

3 files changed

Lines changed: 46 additions & 12 deletions

File tree

src/features/ai/components/mentions/ai-file-selector.tsx

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import {
44
useEffect,
55
useId,
66
useMemo,
7+
useRef,
78
type KeyboardEvent,
89
type ReactNode,
910
type RefObject,
@@ -87,6 +88,7 @@ export function AIFileSelector({
8788
hasLeadingResults = false,
8889
}: AIFileSelectorProps) {
8990
const listboxId = useId();
91+
const lastEmittedResultsSignatureRef = useRef<string | null>(null);
9092
const [debouncedQuery] = useDebounce(query, 50);
9193
const isBackendSearchActive =
9294
useBackendSearch && debouncedQuery.trim().length > 0 && canUseBackendFileSearch(rootFolderPath);
@@ -114,15 +116,24 @@ export function AIFileSelector({
114116

115117
return flattenFileSearchResults(categorizedFiles);
116118
}, [backendHits, categorizedFiles, isBackendSearchActive]);
119+
const resultFiles = useMemo(() => results.map(({ file }) => file), [results]);
120+
const resultFilesSignature = useMemo(
121+
() => resultFiles.map((file) => `${file.path}\0${file.name}`).join("\n"),
122+
[resultFiles],
123+
);
117124

118125
useEffect(() => {
119126
if (selectedIndex <= results.length - 1) return;
120127
onSelectedIndexChange?.(Math.max(results.length - 1, 0));
121128
}, [onSelectedIndexChange, results.length, selectedIndex]);
122129

123130
useEffect(() => {
124-
onResultsChange?.(results.map(({ file }) => file));
125-
}, [onResultsChange, results]);
131+
if (!onResultsChange) return;
132+
if (lastEmittedResultsSignatureRef.current === resultFilesSignature) return;
133+
134+
lastEmittedResultsSignatureRef.current = resultFilesSignature;
135+
onResultsChange(resultFiles);
136+
}, [onResultsChange, resultFiles, resultFilesSignature]);
126137

127138
useEffect(() => {
128139
if (!showSearchInput || !autoFocusSearchInput) return;

src/features/ai/components/selectors/context-selector.tsx

Lines changed: 22 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -102,16 +102,30 @@ export function ContextSelector({
102102

103103
useEffect(() => {
104104
if (!isOpen) return;
105+
let cancelled = false;
105106

106-
getAllProjectFiles().then((projectFiles) => {
107-
const filtered: FileEntry[] = [];
108-
for (const file of projectFiles) {
109-
if (!file.isDir && !shouldIgnoreFile(file.path)) {
110-
filtered.push(file);
107+
getAllProjectFiles()
108+
.then((projectFiles) => {
109+
if (cancelled) return;
110+
111+
const filtered: FileEntry[] = [];
112+
for (const file of projectFiles) {
113+
if (!file.isDir && !shouldIgnoreFile(file.path)) {
114+
filtered.push(file);
115+
}
111116
}
112-
}
113-
setFileItems(filtered);
114-
});
117+
118+
setFileItems(filtered);
119+
})
120+
.catch((error) => {
121+
if (cancelled) return;
122+
console.error("Failed to load context files:", error);
123+
setFileItems([]);
124+
});
125+
126+
return () => {
127+
cancelled = true;
128+
};
115129
}, [isOpen, getAllProjectFiles]);
116130

117131
const bufferByPath = useMemo(

src/ui/command.tsx

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ interface CommandProps {
1313
className?: string;
1414
onClose?: () => void;
1515
placement?: "top" | "bottom";
16+
title?: string;
1617
}
1718

1819
const commandContentVariants = cva(
@@ -34,7 +35,14 @@ const commandItemVariants = cva(
3435
},
3536
);
3637

37-
const Command = ({ isVisible, children, className, onClose, placement = "top" }: CommandProps) => {
38+
const Command = ({
39+
isVisible,
40+
children,
41+
className,
42+
onClose,
43+
placement = "top",
44+
title = "Command palette",
45+
}: CommandProps) => {
3846
const containerClassName =
3947
placement === "bottom"
4048
? "fixed inset-0 z-[10060] flex items-end justify-center px-4 pb-12"
@@ -59,14 +67,15 @@ const Command = ({ isVisible, children, className, onClose, placement = "top" }:
5967
tabIndex={-1}
6068
/>
6169
</DialogPrimitive.Overlay>
62-
<DialogPrimitive.Content asChild aria-label="Command palette">
70+
<DialogPrimitive.Content asChild aria-describedby={undefined}>
6371
<motion.div
6472
initial={{ opacity: 0, scale: 0.95, y: motionY }}
6573
animate={{ opacity: 1, scale: 1, y: 0 }}
6674
exit={{ opacity: 0, scale: 0.95, y: motionY }}
6775
transition={{ duration: 0.15, ease: "easeOut" }}
6876
className={cn(commandContentVariants(), className)}
6977
>
78+
<DialogPrimitive.Title className="sr-only">{title}</DialogPrimitive.Title>
7079
{children}
7180
</motion.div>
7281
</DialogPrimitive.Content>

0 commit comments

Comments
 (0)