From 475f5e54caa028de2bbf7d23e94c0157b01c226a Mon Sep 17 00:00:00 2001 From: Kushal Date: Tue, 30 Dec 2025 00:50:08 +0530 Subject: [PATCH 1/3] Adding checkboxes to exclude archived and forked repos --- packages/mcp/src/schemas.ts | 2 + .../components/searchBar/searchBar.tsx | 53 ++++++++++++++- .../search/components/searchResultsPage.tsx | 8 +++ packages/web/src/app/[domain]/search/page.tsx | 6 ++ .../app/[domain]/search/useStreamedSearch.ts | 10 ++- .../web/src/components/ui/dropdown-menu.tsx | 6 +- packages/web/src/features/search/parser.ts | 65 ++++++++++++++++++- packages/web/src/features/search/types.ts | 2 + packages/web/src/lib/types.ts | 2 + 9 files changed, 147 insertions(+), 7 deletions(-) diff --git a/packages/mcp/src/schemas.ts b/packages/mcp/src/schemas.ts index 510635792..5535f4bd4 100644 --- a/packages/mcp/src/schemas.ts +++ b/packages/mcp/src/schemas.ts @@ -27,6 +27,8 @@ export const searchOptionsSchema = z.object({ whole: z.boolean().optional(), // Whether to return the whole file as part of the response. isRegexEnabled: z.boolean().optional(), // Whether to enable regular expression search. isCaseSensitivityEnabled: z.boolean().optional(), // Whether to enable case sensitivity. + isArchivedExcluded: z.boolean().optional(), // Whether to exclude archived repositories. + isForkedExcluded: z.boolean().optional(), // Whether to exclude forked repositories. }); export const searchRequestSchema = z.object({ diff --git a/packages/web/src/app/[domain]/components/searchBar/searchBar.tsx b/packages/web/src/app/[domain]/components/searchBar/searchBar.tsx index dda7ab2ab..3a9bf15a4 100644 --- a/packages/web/src/app/[domain]/components/searchBar/searchBar.tsx +++ b/packages/web/src/app/[domain]/components/searchBar/searchBar.tsx @@ -44,7 +44,14 @@ import { Toggle } from "@/components/ui/toggle"; import { useDomain } from "@/hooks/useDomain"; import { createAuditAction } from "@/ee/features/audit/actions"; import tailwind from "@/tailwind"; -import { CaseSensitiveIcon, RegexIcon } from "lucide-react"; +import { CaseSensitiveIcon, RegexIcon, Settings2 } from "lucide-react"; +import { + DropdownMenu, + DropdownMenuTrigger, + DropdownMenuContent, + DropdownMenuLabel, + DropdownMenuCheckboxItem, +} from "@/components/ui/dropdown-menu"; interface SearchBarProps { className?: string; @@ -52,6 +59,8 @@ interface SearchBarProps { defaults?: { isRegexEnabled?: boolean; isCaseSensitivityEnabled?: boolean; + isArchivedExcluded?: boolean; + isForkedExcluded?: boolean; query?: string; } autoFocus?: boolean; @@ -99,6 +108,8 @@ export const SearchBar = ({ defaults: { isRegexEnabled: defaultIsRegexEnabled = false, isCaseSensitivityEnabled: defaultIsCaseSensitivityEnabled = false, + isArchivedExcluded: defaultIsArchivedExcluded = false, + isForkedExcluded: defaultIsForkedExcluded = false, query: defaultQuery = "", } = {} }: SearchBarProps) => { @@ -112,6 +123,8 @@ export const SearchBar = ({ const [isHistorySearchEnabled, setIsHistorySearchEnabled] = useState(false); const [isRegexEnabled, setIsRegexEnabled] = useState(defaultIsRegexEnabled); const [isCaseSensitivityEnabled, setIsCaseSensitivityEnabled] = useState(defaultIsCaseSensitivityEnabled); + const [isArchivedExcluded, setIsArchivedExcluded] = useState(defaultIsArchivedExcluded); + const [isForkedExcluded, setIsForkedExcluded] = useState(defaultIsForkedExcluded); const focusEditor = useCallback(() => editorRef.current?.view?.focus(), []); const focusSuggestionsBox = useCallback(() => suggestionBoxRef.current?.focus(), []); @@ -227,9 +240,11 @@ export const SearchBar = ({ [SearchQueryParams.query, query], [SearchQueryParams.isRegexEnabled, isRegexEnabled ? "true" : null], [SearchQueryParams.isCaseSensitivityEnabled, isCaseSensitivityEnabled ? "true" : null], + [SearchQueryParams.isArchivedExcluded, isArchivedExcluded ? "true" : null], + [SearchQueryParams.isForkedExcluded, isForkedExcluded ? "true" : null], ); router.push(url); - }, [domain, router, isRegexEnabled, isCaseSensitivityEnabled]); + }, [domain, router, isRegexEnabled, isCaseSensitivityEnabled, isArchivedExcluded, isForkedExcluded]); return (
+ + + + + + { }} + > + + + + + + Search settings + + + + Search + setIsArchivedExcluded(Boolean(v))} + > + Exclude archived repositories + + setIsForkedExcluded(Boolean(v))} + > + Exclude forked repositories + + + diff --git a/packages/web/src/app/[domain]/search/components/searchResultsPage.tsx b/packages/web/src/app/[domain]/search/components/searchResultsPage.tsx index 602082c25..f792f9a36 100644 --- a/packages/web/src/app/[domain]/search/components/searchResultsPage.tsx +++ b/packages/web/src/app/[domain]/search/components/searchResultsPage.tsx @@ -40,6 +40,8 @@ interface SearchResultsPageProps { defaultMaxMatchCount: number; isRegexEnabled: boolean; isCaseSensitivityEnabled: boolean; + isArchivedExcluded: boolean; + isForkedExcluded: boolean; } export const SearchResultsPage = ({ @@ -47,6 +49,8 @@ export const SearchResultsPage = ({ defaultMaxMatchCount, isRegexEnabled, isCaseSensitivityEnabled, + isArchivedExcluded, + isForkedExcluded, }: SearchResultsPageProps) => { const router = useRouter(); const { setSearchHistory } = useSearchHistory(); @@ -75,6 +79,8 @@ export const SearchResultsPage = ({ whole: false, isRegexEnabled, isCaseSensitivityEnabled, + isArchivedExcluded, + isForkedExcluded, }); useEffect(() => { @@ -175,6 +181,8 @@ export const SearchResultsPage = ({ defaults={{ isRegexEnabled, isCaseSensitivityEnabled, + isArchivedExcluded, + isForkedExcluded, query: searchQuery, }} className="w-full" diff --git a/packages/web/src/app/[domain]/search/page.tsx b/packages/web/src/app/[domain]/search/page.tsx index d1f4e03a7..1d02f43c2 100644 --- a/packages/web/src/app/[domain]/search/page.tsx +++ b/packages/web/src/app/[domain]/search/page.tsx @@ -8,6 +8,8 @@ interface SearchPageProps { query?: string; isRegexEnabled?: "true" | "false"; isCaseSensitivityEnabled?: "true" | "false"; + isArchivedExcluded?: "true" | "false"; + isForkedExcluded?: "true" | "false"; }>; } @@ -17,6 +19,8 @@ export default async function SearchPage(props: SearchPageProps) { const query = searchParams?.query; const isRegexEnabled = searchParams?.isRegexEnabled === "true"; const isCaseSensitivityEnabled = searchParams?.isCaseSensitivityEnabled === "true"; + const isArchivedExcluded = searchParams?.isArchivedExcluded === "true"; + const isForkedExcluded = searchParams?.isForkedExcluded === "true"; if (query === undefined || query.length === 0) { return @@ -28,6 +32,8 @@ export default async function SearchPage(props: SearchPageProps) { defaultMaxMatchCount={env.DEFAULT_MAX_MATCH_COUNT} isRegexEnabled={isRegexEnabled} isCaseSensitivityEnabled={isCaseSensitivityEnabled} + isArchivedExcluded={isArchivedExcluded} + isForkedExcluded={isForkedExcluded} /> ) } diff --git a/packages/web/src/app/[domain]/search/useStreamedSearch.ts b/packages/web/src/app/[domain]/search/useStreamedSearch.ts index 181b8a62c..070bdf11f 100644 --- a/packages/web/src/app/[domain]/search/useStreamedSearch.ts +++ b/packages/web/src/app/[domain]/search/useStreamedSearch.ts @@ -27,6 +27,8 @@ const createCacheKey = (params: SearchRequest): string => { whole: params.whole, isRegexEnabled: params.isRegexEnabled, isCaseSensitivityEnabled: params.isCaseSensitivityEnabled, + isArchivedExcluded: params.isArchivedExcluded, + isForkedExcluded: params.isForkedExcluded, }); }; @@ -34,7 +36,7 @@ const isCacheValid = (entry: CacheEntry): boolean => { return Date.now() - entry.timestamp < CACHE_TTL; }; -export const useStreamedSearch = ({ query, matches, contextLines, whole, isRegexEnabled, isCaseSensitivityEnabled }: SearchRequest) => { +export const useStreamedSearch = ({ query, matches, contextLines, whole, isRegexEnabled, isCaseSensitivityEnabled, isArchivedExcluded, isForkedExcluded }: SearchRequest) => { const [state, setState] = useState<{ isStreaming: boolean, isExhaustive: boolean, @@ -86,6 +88,8 @@ export const useStreamedSearch = ({ query, matches, contextLines, whole, isRegex whole, isRegexEnabled, isCaseSensitivityEnabled, + isArchivedExcluded, + isForkedExcluded, }); // Check if we have a valid cached result. If so, use it. @@ -129,6 +133,8 @@ export const useStreamedSearch = ({ query, matches, contextLines, whole, isRegex whole, isRegexEnabled, isCaseSensitivityEnabled, + isArchivedExcluded, + isForkedExcluded, source: 'sourcebot-web-client' } satisfies SearchRequest), signal: abortControllerRef.current.signal, @@ -279,6 +285,8 @@ export const useStreamedSearch = ({ query, matches, contextLines, whole, isRegex whole, isRegexEnabled, isCaseSensitivityEnabled, + isArchivedExcluded, + isForkedExcluded, cancel, ]); diff --git a/packages/web/src/components/ui/dropdown-menu.tsx b/packages/web/src/components/ui/dropdown-menu.tsx index f69a0d64c..7cb18cff1 100644 --- a/packages/web/src/components/ui/dropdown-menu.tsx +++ b/packages/web/src/components/ui/dropdown-menu.tsx @@ -105,9 +105,9 @@ const DropdownMenuCheckboxItem = React.forwardRef< checked={checked} {...props} > - - - + + + {children} diff --git a/packages/web/src/features/search/parser.ts b/packages/web/src/features/search/parser.ts index e3e9d41a9..872c10c68 100644 --- a/packages/web/src/features/search/parser.ts +++ b/packages/web/src/features/search/parser.ts @@ -62,13 +62,19 @@ export const parseQuerySyntaxIntoIR = async ({ options: { isCaseSensitivityEnabled?: boolean; isRegexEnabled?: boolean; + isArchivedExcluded?: boolean; + isForkedExcluded?: boolean; }, prisma: PrismaClient, }): Promise => { + console.log("IS ARCHIVED EXCLUDED:", options.isArchivedExcluded); + console.log("IS FORKED EXCLUDED:", options.isForkedExcluded); + try { // First parse the query into a Lezer tree. const tree = parser.parse(query); + // Debug logs removed - use structured logging if needed. // Then transform the tree into the intermediate representation. return transformTreeToIR({ @@ -76,6 +82,8 @@ export const parseQuerySyntaxIntoIR = async ({ input: query, isCaseSensitivityEnabled: options.isCaseSensitivityEnabled ?? false, isRegexEnabled: options.isRegexEnabled ?? false, + isArchivedExcluded: options.isArchivedExcluded ?? false, + isForkedExcluded: options.isForkedExcluded ?? false, onExpandSearchContext: async (contextName: string) => { const context = await prisma.searchContext.findUnique({ where: { @@ -116,12 +124,16 @@ const transformTreeToIR = async ({ input, isCaseSensitivityEnabled, isRegexEnabled, + isArchivedExcluded, + isForkedExcluded, onExpandSearchContext, }: { tree: Tree; input: string; isCaseSensitivityEnabled: boolean; isRegexEnabled: boolean; + isArchivedExcluded: boolean; + isForkedExcluded: boolean; onExpandSearchContext: (contextName: string) => Promise; }): Promise => { const transformNode = async (node: SyntaxNode): Promise => { @@ -320,6 +332,9 @@ const transformTreeToIR = async ({ } case ArchivedExpr: { + // We'll set the value of isArchivedExcluded to false as the query takes precedence over checkbox. + isArchivedExcluded = false; + const rawValue = value.toLowerCase(); if (!isArchivedValue(rawValue)) { @@ -344,6 +359,9 @@ const transformTreeToIR = async ({ }; } case ForkExpr: { + // We'll set the value of isForkedExcluded to false as the query takes precedence over checkbox. + isForkedExcluded = false; + const rawValue = value.toLowerCase(); if (!isForkValue(rawValue)) { @@ -397,7 +415,52 @@ const transformTreeToIR = async ({ } } - return transformNode(tree.topNode); + + // return await transformNode(tree.topNode); + const root = await transformNode(tree.topNode); + + // If the tree does not contain explicit archived/fork prefixes, add + // default raw_config flags to exclude archived/forks by default. + const defaultNodes: QueryIR[] = []; + + if (isArchivedExcluded) { + defaultNodes.push({ + raw_config: { + flags: ['FLAG_NO_ARCHIVED'] + }, + query: 'raw_config' + }); + } + + if (isForkedExcluded) { + defaultNodes.push({ + raw_config: { + flags: ['FLAG_NO_FORKS'] + }, + query: 'raw_config' + }); + } + + if (defaultNodes.length === 0) { + return root; + } + + // If the root is already an AND, append defaults; otherwise create an AND + if (root.and && Array.isArray(root.and.children)) { + return { + and: { + children: [...root.and.children, ...defaultNodes] + }, + query: 'and' + }; + } + + return { + and: { + children: [root, ...defaultNodes] + }, + query: 'and' + }; } const getChildren = (node: SyntaxNode): SyntaxNode[] => { diff --git a/packages/web/src/features/search/types.ts b/packages/web/src/features/search/types.ts index c90cfdd14..f8e76f0a4 100644 --- a/packages/web/src/features/search/types.ts +++ b/packages/web/src/features/search/types.ts @@ -89,6 +89,8 @@ export const searchOptionsSchema = z.object({ whole: z.boolean().optional(), // Whether to return the whole file as part of the response. isRegexEnabled: z.boolean().optional(), // Whether to enable regular expression search. isCaseSensitivityEnabled: z.boolean().optional(), // Whether to enable case sensitivity. + isArchivedExcluded: z.boolean().optional(), // Whether to exclude archived repositories. + isForkedExcluded: z.boolean().optional(), // Whether to exclude forked repositories. }); export type SearchOptions = z.infer; diff --git a/packages/web/src/lib/types.ts b/packages/web/src/lib/types.ts index cb6dc3c2f..437d0ca7d 100644 --- a/packages/web/src/lib/types.ts +++ b/packages/web/src/lib/types.ts @@ -11,6 +11,8 @@ export enum SearchQueryParams { matches = "matches", isRegexEnabled = "isRegexEnabled", isCaseSensitivityEnabled = "isCaseSensitivityEnabled", + isArchivedExcluded = "isArchivedExcluded", + isForkedExcluded = "isForkedExcluded", } export type ApiKeyPayload = { From ae5f1e230e8e25287a714ba638fc86200ed54c1c Mon Sep 17 00:00:00 2001 From: Kushal Date: Tue, 30 Dec 2025 12:39:46 +0530 Subject: [PATCH 2/3] clearing off debugging logs --- packages/web/src/features/search/parser.ts | 4 ---- 1 file changed, 4 deletions(-) diff --git a/packages/web/src/features/search/parser.ts b/packages/web/src/features/search/parser.ts index 872c10c68..b682fc60b 100644 --- a/packages/web/src/features/search/parser.ts +++ b/packages/web/src/features/search/parser.ts @@ -68,13 +68,9 @@ export const parseQuerySyntaxIntoIR = async ({ prisma: PrismaClient, }): Promise => { - console.log("IS ARCHIVED EXCLUDED:", options.isArchivedExcluded); - console.log("IS FORKED EXCLUDED:", options.isForkedExcluded); - try { // First parse the query into a Lezer tree. const tree = parser.parse(query); - // Debug logs removed - use structured logging if needed. // Then transform the tree into the intermediate representation. return transformTreeToIR({ From f3d3cca5343e2f6b065ded1f387129d2b81ffeaf Mon Sep 17 00:00:00 2001 From: Kushal Date: Wed, 31 Dec 2025 00:49:04 +0530 Subject: [PATCH 3/3] Updating changelog --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3f00117ca..4be9c26db 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 ### Changed - Changed the default `/repos` pagination size to 20. [#706](https://github.com/sourcebot-dev/sourcebot/pull/706) +- Added checkbox to exclude archived and forked repositories from settings dropdown in the query search tab. [#663](https://github.com/sourcebot-dev/sourcebot/issues/663) ## [4.10.7] - 2025-12-29