diff --git a/CHANGELOG.md b/CHANGELOG.md
index 21fd05b6a..31fc2eec9 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -9,6 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Fixed
- Silenced a false-positive `MaxListenersExceededWarning` logged on every request proxied through an external rewrite. [#1697](https://github.com/sourcebot-dev/sourcebot/pull/1697)
+- Fixed selected language and repository filters disappearing when filtering the available options. [#1685](https://github.com/sourcebot-dev/sourcebot/pull/1685)
## [5.1.15] - 2026-09-29
diff --git a/packages/web/src/app/(app)/search/components/filterPanel/filter.test.tsx b/packages/web/src/app/(app)/search/components/filterPanel/filter.test.tsx
new file mode 100644
index 000000000..04d72367b
--- /dev/null
+++ b/packages/web/src/app/(app)/search/components/filterPanel/filter.test.tsx
@@ -0,0 +1,77 @@
+import { cleanup, fireEvent, render, screen } from '@testing-library/react';
+import { afterEach, describe, expect, it, vi } from 'vitest';
+import { TooltipProvider } from '@/components/ui/tooltip';
+import { Filter } from './filter';
+import type { Entry } from './entry';
+
+afterEach(cleanup);
+
+const createEntry = (displayName: string, isSelected = false, count = 1): Entry => ({
+ key: displayName,
+ displayName,
+ count,
+ isSelected,
+ isHidden: false,
+ isDisabled: false,
+});
+
+const renderFilter = (entries: Entry[], onEntryClicked = vi.fn()) => render(
+
+
+ ,
+);
+
+describe('search filters', () => {
+ it('keeps selected languages above matches and allows clearing a hidden-by-search selection', () => {
+ const onEntryClicked = vi.fn();
+ renderFilter([
+ createEntry('C', false, 100),
+ createEntry('PowerShell', true),
+ createEntry('Python'),
+ ], onEntryClicked);
+
+ fireEvent.change(screen.getByPlaceholderText('Filter languages'), { target: { value: 'C' } });
+
+ const selected = screen.getByText('PowerShell');
+ const match = screen.getByText('C');
+ expect(selected.compareDocumentPosition(match) & Node.DOCUMENT_POSITION_FOLLOWING).toBeTruthy();
+ expect(screen.queryByText('Python')).toBeNull();
+
+ fireEvent.click(selected);
+ expect(onEntryClicked).toHaveBeenCalledExactlyOnceWith('PowerShell');
+ });
+
+ it('shows a selected entry once when it also matches the filter', () => {
+ renderFilter([createEntry('Python', true), createEntry('PowerShell')]);
+
+ fireEvent.change(screen.getByPlaceholderText('Filter languages'), { target: { value: 'Python' } });
+
+ expect(screen.getAllByText('Python')).toHaveLength(1);
+ expect(screen.queryByText('PowerShell')).toBeNull();
+ });
+
+ it('preserves selected entries when the filter has no matches', () => {
+ renderFilter([createEntry('PowerShell', true), createEntry('Python')]);
+
+ fireEvent.change(screen.getByPlaceholderText('Filter languages'), { target: { value: 'zzzz' } });
+
+ expect(screen.getByText('PowerShell')).toBeTruthy();
+ expect(screen.queryByText('Python')).toBeNull();
+ });
+
+ it('does not reorder the caller\'s entries when sorting the unfiltered list', () => {
+ const entries = [createEntry('Python'), createEntry('PowerShell', true)];
+ Object.freeze(entries);
+
+ renderFilter(entries);
+
+ expect(entries.map(entry => entry.key)).toEqual(['Python', 'PowerShell']);
+ expect(screen.getByText('PowerShell')).toBeTruthy();
+ });
+});
diff --git a/packages/web/src/app/(app)/search/components/filterPanel/filter.tsx b/packages/web/src/app/(app)/search/components/filterPanel/filter.tsx
index 0a76a50e6..b2c093410 100644
--- a/packages/web/src/app/(app)/search/components/filterPanel/filter.tsx
+++ b/packages/web/src/app/(app)/search/components/filterPanel/filter.tsx
@@ -31,13 +31,14 @@ export const Filter = ({
return entries;
}
- const fuse = new Fuse(entries, {
+ const selectedEntries = entries.filter(entry => entry.isSelected);
+ const fuse = new Fuse(entries.filter(entry => !entry.isSelected), {
keys: ["displayName"],
threshold: 0.3,
});
const result = fuse.search(searchFilter);
- return result.map((result) => result.item);
+ return [...selectedEntries, ...result.map((result) => result.item)];
}, [entries, searchFilter]);
return (
@@ -61,7 +62,7 @@ export const Filter = ({
- {filteredEntries
+ {[...filteredEntries]
.sort((entryA, entryB) => compareEntries(entryB, entryA))
.map((entry) => (