diff --git a/CHANGELOG.md b/CHANGELOG.md
index 3f00117c..4be9c26d 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
diff --git a/packages/mcp/src/schemas.ts b/packages/mcp/src/schemas.ts
index 51063579..5535f4bd 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 dda7ab2a..3a9bf15a 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 602082c2..f792f9a3 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 d1f4e03a..1d02f43c 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 181b8a62..070bdf11 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 f69a0d64..7cb18cff 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 e3e9d41a..b682fc60 100644
--- a/packages/web/src/features/search/parser.ts
+++ b/packages/web/src/features/search/parser.ts
@@ -62,6 +62,8 @@ export const parseQuerySyntaxIntoIR = async ({
options: {
isCaseSensitivityEnabled?: boolean;
isRegexEnabled?: boolean;
+ isArchivedExcluded?: boolean;
+ isForkedExcluded?: boolean;
},
prisma: PrismaClient,
}): Promise => {
@@ -76,6 +78,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 +120,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 +328,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 +355,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 +411,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 c90cfdd1..f8e76f0a 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 cb6dc3c2..437d0ca7 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 = {