perf: add search debounce and increase global search timeout
- SearchBar: 300ms debounce with local input state, flush on Enter - CommandPalette: global search debounce 200→400ms - Show "500+" when search results are capped - Fix: syncSearchMatchesWithRendered now updates searchMatchItemIds Ported from upstream 5c7f921e (SearchBar/CommandPalette parts).
This commit is contained in:
parent
c21350713c
commit
22387ca0cb
3 changed files with 63 additions and 7 deletions
|
|
@ -288,7 +288,7 @@ export const CommandPalette = (): React.JSX.Element | null => {
|
|||
setLoading(false);
|
||||
}
|
||||
}
|
||||
}, 200);
|
||||
}, 400);
|
||||
|
||||
return () => clearTimeout(timeoutId);
|
||||
}, [query, selectedProjectId, commandPaletteOpen, searchMode, globalSearchEnabled]);
|
||||
|
|
|
|||
|
|
@ -1,14 +1,19 @@
|
|||
/**
|
||||
* SearchBar - In-session search interface component.
|
||||
* Appears at the top of the chat view when Cmd+F is pressed.
|
||||
*
|
||||
* Uses a local input state with debouncing to avoid triggering expensive
|
||||
* search on every keystroke.
|
||||
*/
|
||||
|
||||
import { useEffect, useRef } from 'react';
|
||||
import { useCallback, useEffect, useRef, useState } from 'react';
|
||||
|
||||
import { useStore } from '@renderer/store';
|
||||
import { ChevronDown, ChevronUp, X } from 'lucide-react';
|
||||
import { useShallow } from 'zustand/react/shallow';
|
||||
|
||||
const SEARCH_DEBOUNCE_MS = 300;
|
||||
|
||||
interface SearchBarProps {
|
||||
tabId?: string;
|
||||
}
|
||||
|
|
@ -19,6 +24,7 @@ export const SearchBar = ({ tabId }: SearchBarProps): React.JSX.Element | null =
|
|||
searchVisible,
|
||||
searchResultCount,
|
||||
currentSearchIndex,
|
||||
searchResultsCapped,
|
||||
conversation,
|
||||
setSearchQuery,
|
||||
hideSearch,
|
||||
|
|
@ -30,6 +36,7 @@ export const SearchBar = ({ tabId }: SearchBarProps): React.JSX.Element | null =
|
|||
searchVisible: s.searchVisible,
|
||||
searchResultCount: s.searchResultCount,
|
||||
currentSearchIndex: s.currentSearchIndex,
|
||||
searchResultsCapped: s.searchResultsCapped,
|
||||
conversation: tabId
|
||||
? (s.tabSessionData[tabId]?.conversation ?? s.conversation)
|
||||
: s.conversation,
|
||||
|
|
@ -40,8 +47,43 @@ export const SearchBar = ({ tabId }: SearchBarProps): React.JSX.Element | null =
|
|||
}))
|
||||
);
|
||||
|
||||
// Local input value for responsive typing — debounced before triggering search
|
||||
const [localQuery, setLocalQuery] = useState(searchQuery);
|
||||
const debounceRef = useRef<ReturnType<typeof setTimeout>>(
|
||||
0 as unknown as ReturnType<typeof setTimeout>
|
||||
);
|
||||
|
||||
const inputRef = useRef<HTMLInputElement>(null);
|
||||
|
||||
// Sync local state when store query changes externally (e.g., hideSearch clears it)
|
||||
useEffect(() => {
|
||||
setLocalQuery(searchQuery);
|
||||
}, [searchQuery]);
|
||||
|
||||
// Debounced search dispatch
|
||||
const handleChange = useCallback(
|
||||
(value: string) => {
|
||||
setLocalQuery(value);
|
||||
clearTimeout(debounceRef.current);
|
||||
|
||||
// Clear immediately when input is emptied
|
||||
if (!value.trim()) {
|
||||
setSearchQuery('', conversation);
|
||||
return;
|
||||
}
|
||||
|
||||
debounceRef.current = setTimeout(() => {
|
||||
setSearchQuery(value, conversation);
|
||||
}, SEARCH_DEBOUNCE_MS);
|
||||
},
|
||||
[conversation, setSearchQuery]
|
||||
);
|
||||
|
||||
// Cleanup timeout on unmount
|
||||
useEffect(() => {
|
||||
return () => clearTimeout(debounceRef.current);
|
||||
}, []);
|
||||
|
||||
// Auto-focus input when search becomes visible
|
||||
useEffect(() => {
|
||||
if (searchVisible && inputRef.current) {
|
||||
|
|
@ -55,6 +97,11 @@ export const SearchBar = ({ tabId }: SearchBarProps): React.JSX.Element | null =
|
|||
if (e.key === 'Escape') {
|
||||
hideSearch();
|
||||
} else if (e.key === 'Enter') {
|
||||
// Flush any pending debounce immediately on Enter
|
||||
clearTimeout(debounceRef.current);
|
||||
if (localQuery !== searchQuery) {
|
||||
setSearchQuery(localQuery, conversation);
|
||||
}
|
||||
if (e.shiftKey) {
|
||||
previousSearchResult();
|
||||
} else {
|
||||
|
|
@ -67,14 +114,18 @@ export const SearchBar = ({ tabId }: SearchBarProps): React.JSX.Element | null =
|
|||
return null;
|
||||
}
|
||||
|
||||
const resultLabel = searchResultsCapped
|
||||
? `${currentSearchIndex + 1} of ${searchResultCount}+`
|
||||
: `${currentSearchIndex + 1} of ${searchResultCount}`;
|
||||
|
||||
return (
|
||||
<div className="absolute right-4 top-2 z-20 flex items-center gap-2 rounded-lg border border-border bg-surface px-3 py-2 shadow-lg">
|
||||
{/* Search input */}
|
||||
<input
|
||||
ref={inputRef}
|
||||
type="text"
|
||||
value={searchQuery}
|
||||
onChange={(e) => setSearchQuery(e.target.value, conversation)}
|
||||
value={localQuery}
|
||||
onChange={(e) => handleChange(e.target.value)}
|
||||
onKeyDown={handleKeyDown}
|
||||
placeholder="Find in conversation..."
|
||||
className="w-48 rounded border border-border bg-surface-raised px-3 py-1.5 text-sm text-text focus:border-text-secondary focus:outline-none"
|
||||
|
|
@ -83,9 +134,7 @@ export const SearchBar = ({ tabId }: SearchBarProps): React.JSX.Element | null =
|
|||
{/* Result count */}
|
||||
{searchQuery && (
|
||||
<span className="whitespace-nowrap text-xs text-text-secondary">
|
||||
{searchResultCount > 0
|
||||
? `${currentSearchIndex + 1} of ${searchResultCount}`
|
||||
: 'No results'}
|
||||
{searchResultCount > 0 ? resultLabel : 'No results'}
|
||||
</span>
|
||||
)}
|
||||
|
||||
|
|
|
|||
|
|
@ -397,10 +397,17 @@ export const createConversationSlice: StateCreator<AppState, [], [], Conversatio
|
|||
});
|
||||
}
|
||||
|
||||
// Rebuild matchItemIds from synced matches
|
||||
const syncedMatchItemIds = new Set<string>();
|
||||
for (const match of nextMatches) {
|
||||
syncedMatchItemIds.add(match.itemId);
|
||||
}
|
||||
|
||||
set({
|
||||
searchMatches: nextMatches,
|
||||
searchResultCount: nextMatches.length,
|
||||
currentSearchIndex: newCurrentIndex,
|
||||
searchMatchItemIds: syncedMatchItemIds,
|
||||
});
|
||||
},
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue