diff --git a/src/renderer/src/components/settings/Settings.tsx b/src/renderer/src/components/settings/Settings.tsx index 7407f0c05..fa89bf442 100644 --- a/src/renderer/src/components/settings/Settings.tsx +++ b/src/renderer/src/components/settings/Settings.tsx @@ -211,6 +211,7 @@ function Settings(): React.JSX.Element { const removeRepo = useAppStore((s) => s.removeRepo) const settingsNavigationTarget = useAppStore((s) => s.settingsNavigationTarget) const clearSettingsTarget = useAppStore((s) => s.clearSettingsTarget) + const settingsSearchInputQuery = useAppStore((s) => s.settingsSearchInputQuery) const settingsSearchQuery = useAppStore((s) => s.settingsSearchQuery) const setSettingsSearchQuery = useAppStore((s) => s.setSettingsSearchQuery) @@ -941,7 +942,7 @@ function Settings(): React.JSX.Element { generalSections={generalNavSections} repoSections={repoNavSections} hasRepos={repos.length > 0} - searchQuery={settingsSearchQuery} + searchQuery={settingsSearchInputQuery} searchInputRef={searchInputRef} onBack={closeSettingsPageWithPromptGuard} onSearchChange={setSettingsSearchQuery} diff --git a/src/renderer/src/store/slices/settings-search-state.test.ts b/src/renderer/src/store/slices/settings-search-state.test.ts new file mode 100644 index 000000000..bb981b0a2 --- /dev/null +++ b/src/renderer/src/store/slices/settings-search-state.test.ts @@ -0,0 +1,56 @@ +import { afterEach, describe, expect, it, vi } from 'vitest' +import { + createSettingsSearchState, + SETTINGS_SEARCH_DEBOUNCE_MS, + type SettingsSearchState +} from './settings-search-state' + +function createTestSearchState(): { + getState: () => SettingsSearchState + setState: (state: Partial) => void +} { + let state = {} as SettingsSearchState + const setState = (updates: Partial): void => { + state = { ...state, ...updates } + } + state = createSettingsSearchState(setState) + return { getState: () => state, setState } +} + +afterEach(() => { + vi.useRealTimers() +}) + +describe('settings search state', () => { + it('updates the search input immediately and debounces the applied filter', () => { + vi.useFakeTimers() + const store = createTestSearchState() + + store.getState().setSettingsSearchQuery('terminal') + + expect(store.getState().settingsSearchInputQuery).toBe('terminal') + expect(store.getState().settingsSearchQuery).toBe('') + + vi.advanceTimersByTime(SETTINGS_SEARCH_DEBOUNCE_MS - 1) + expect(store.getState().settingsSearchQuery).toBe('') + + vi.advanceTimersByTime(1) + expect(store.getState().settingsSearchQuery).toBe('terminal') + }) + + it('clears settings search immediately and cancels pending debounce work', () => { + vi.useFakeTimers() + const store = createTestSearchState() + store.setState({ settingsSearchInputQuery: 'terminal', settingsSearchQuery: 'terminal' }) + + store.getState().setSettingsSearchQuery('agents') + vi.advanceTimersByTime(SETTINGS_SEARCH_DEBOUNCE_MS - 1) + store.getState().setSettingsSearchQuery('') + + expect(store.getState().settingsSearchInputQuery).toBe('') + expect(store.getState().settingsSearchQuery).toBe('') + + vi.advanceTimersByTime(SETTINGS_SEARCH_DEBOUNCE_MS) + expect(store.getState().settingsSearchQuery).toBe('') + }) +}) diff --git a/src/renderer/src/store/slices/settings-search-state.ts b/src/renderer/src/store/slices/settings-search-state.ts new file mode 100644 index 000000000..c691e1199 --- /dev/null +++ b/src/renderer/src/store/slices/settings-search-state.ts @@ -0,0 +1,39 @@ +export const SETTINGS_SEARCH_DEBOUNCE_MS = 150 + +export type SettingsSearchState = { + settingsSearchInputQuery: string + settingsSearchQuery: string + setSettingsSearchQuery: (q: string) => void +} + +export function createSettingsSearchState( + set: (state: Partial) => void +): SettingsSearchState { + let settingsSearchDebounceTimer: ReturnType | null = null + + const clearSettingsSearchDebounce = (): void => { + if (settingsSearchDebounceTimer) { + clearTimeout(settingsSearchDebounceTimer) + settingsSearchDebounceTimer = null + } + } + + return { + settingsSearchInputQuery: '', + settingsSearchQuery: '', + setSettingsSearchQuery: (q) => { + clearSettingsSearchDebounce() + if (q.trim() === '') { + set({ settingsSearchInputQuery: q, settingsSearchQuery: q }) + return + } + set({ settingsSearchInputQuery: q }) + // Why: applying settings search mounts and filters many heavy sections, + // so keep typing responsive while waiting for the query to settle. + settingsSearchDebounceTimer = setTimeout(() => { + settingsSearchDebounceTimer = null + set({ settingsSearchQuery: q }) + }, SETTINGS_SEARCH_DEBOUNCE_MS) + } + } +} diff --git a/src/renderer/src/store/slices/settings.test.ts b/src/renderer/src/store/slices/settings.test.ts index c4ba3a1cc..eda21c407 100644 --- a/src/renderer/src/store/slices/settings.test.ts +++ b/src/renderer/src/store/slices/settings.test.ts @@ -1,4 +1,4 @@ -import { describe, expect, it, vi, beforeEach } from 'vitest' +import { beforeEach, describe, expect, it, vi } from 'vitest' import { createTestStore, makeWorktree } from './store-test-helpers' import type { AppState } from '../types' import type { WorktreeLineage } from '../../../../shared/types' diff --git a/src/renderer/src/store/slices/settings.ts b/src/renderer/src/store/slices/settings.ts index c7dbf37a4..f869a6695 100644 --- a/src/renderer/src/store/slices/settings.ts +++ b/src/renderer/src/store/slices/settings.ts @@ -10,11 +10,10 @@ import { import { normalizeTerminalQuickCommands } from '../../../../shared/terminal-quick-commands' import { normalizeVisibleTaskProviders } from '../../../../shared/task-providers' import { normalizeOpenInApplications } from '../../../../shared/open-in-applications' +import { createSettingsSearchState, type SettingsSearchState } from './settings-search-state' -export type SettingsSlice = { +export type SettingsSlice = SettingsSearchState & { settings: GlobalSettings | null - settingsSearchQuery: string - setSettingsSearchQuery: (q: string) => void fetchSettings: () => Promise updateSettings: (updates: Partial) => Promise switchRuntimeEnvironment: (environmentId: string | null) => Promise @@ -221,8 +220,7 @@ async function verifyRuntimeEnvironmentReachable(environmentId: string | null): export const createSettingsSlice: StateCreator = (set, get) => ({ settings: null, - settingsSearchQuery: '', - setSettingsSearchQuery: (q) => set({ settingsSearchQuery: q }), + ...createSettingsSearchState((state) => set(state)), fetchSettings: async () => { try {