diff --git a/CHANGELOG.md b/CHANGELOG.md
index 9fa7e3ad8..c1c1f8765 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
### Added
- Added pagination and UTC time range filtering to the audit GET endpoint. [#949](https://github.com/sourcebot-dev/sourcebot/pull/949)
+- Added AI Search Assist — describe what you're looking for in natural language and AI will generate a code search query for you. [#951](https://github.com/sourcebot-dev/sourcebot/pull/951)
### Fixed
- Fixed search query parser rejecting parenthesized regex alternation in filter values (e.g. `file:(test|spec)`, `-file:(test|spec)`). [#946](https://github.com/sourcebot-dev/sourcebot/pull/946)
diff --git a/docs/docs.json b/docs/docs.json
index 2767e357c..dd8f3f620 100644
--- a/docs/docs.json
+++ b/docs/docs.json
@@ -39,6 +39,7 @@
"pages": [
"docs/features/search/overview",
"docs/features/search/syntax-reference",
+ "docs/features/search/ai-search-assist",
"docs/features/search/multi-branch-indexing",
"docs/features/search/search-contexts"
]
diff --git a/docs/docs/features/search/ai-search-assist.mdx b/docs/docs/features/search/ai-search-assist.mdx
new file mode 100644
index 000000000..63ed88169
--- /dev/null
+++ b/docs/docs/features/search/ai-search-assist.mdx
@@ -0,0 +1,26 @@
+---
+title: AI Search Assist
+---
+
+AI Search Assist lets you describe what you're looking for in natural language and automatically generates a search query for you.
+
+
+
+
+## How it works
+
+1. Click the wand icon () in the search bar to open the AI Search Assist panel.
+2. Describe what you're looking for in natural language.
+3. Click **Generate** or press ↵ to generate the query.
+4. The generated query is inserted into the search bar.
+
+## Requirements
+
+AI Search Assist requires a language model to be configured. See [Configure Language Models](/docs/configuration/language-model-providers) for setup instructions.
diff --git a/docs/images/search-assist.mp4 b/docs/images/search-assist.mp4
new file mode 100644
index 000000000..ea371460e
Binary files /dev/null and b/docs/images/search-assist.mp4 differ
diff --git a/packages/queryLanguage/src/index.ts b/packages/queryLanguage/src/index.ts
index 00cfbaade..23a622c36 100644
--- a/packages/queryLanguage/src/index.ts
+++ b/packages/queryLanguage/src/index.ts
@@ -4,4 +4,5 @@ type Tree = ReturnType;
type SyntaxNode = Tree['topNode'];
export type { Tree, SyntaxNode };
export * from "./parser";
-export * from "./parser.terms";
\ No newline at end of file
+export * from "./parser.terms";
+export { SEARCH_SYNTAX_DESCRIPTION } from "./syntaxDescription";
\ No newline at end of file
diff --git a/packages/queryLanguage/src/syntaxDescription.ts b/packages/queryLanguage/src/syntaxDescription.ts
new file mode 100644
index 000000000..1f8de0997
--- /dev/null
+++ b/packages/queryLanguage/src/syntaxDescription.ts
@@ -0,0 +1,99 @@
+/**
+ * LLM-readable description of the Sourcebot search query syntax.
+ * Keep this in sync with query.grammar and tokens.ts when the syntax changes.
+ */
+export const SEARCH_SYNTAX_DESCRIPTION = String.raw`
+# Sourcebot Search Query Syntax
+
+## Search terms
+
+Bare words search across file content and are interpreted as case-sensitive regular expressions:
+ useState — matches files containing "useState"
+ useS?tate — matches files containing "useState" or "usetate"
+ ^import — matches lines beginning with "import"
+ error.*handler — matches "error" followed by "handler" on the same line
+
+Wrap terms in double quotes to match a phrase with spaces:
+ "password reset" — matches files containing the phrase "password reset"
+
+## Filters
+
+Narrow searches with prefix:value syntax:
+
+ file: — filter by file path
+ lang: — filter by language. Uses linguist language definitions (e.g. TypeScript, Python, Go, Rust, Java)
+ repo: — filter by repository name
+ sym: — filter by symbol name
+ rev: — filter by git branch or tag
+
+All filter values are interpreted as case-sensitive regular expressions.
+A plain word matches as a substring. No forward slashes around values.
+
+## Boolean logic
+
+Space = AND. All space-separated terms must match.
+ useState lang:TypeScript — TypeScript files containing useState
+
+or = OR (must be lowercase, not at start/end of query).
+ auth or login — files containing "auth" or "login"
+
+- = negation. Only valid before a filter or a parenthesized group.
+ -file:test — exclude paths matching /test/
+ -(file:test or file:spec) — exclude test and spec files
+
+## Grouping
+
+Parentheses group expressions:
+ (auth or login) lang:TypeScript
+ -(file:test or file:spec)
+
+## Quoting
+
+Wrap a value in double quotes when it contains spaces:
+ "password reset"
+ "error handler"
+
+When the quoted value itself contains double-quote characters, escape each one as \":
+ "\"key\": \"value\"" — matches the literal text: "key": "value"
+
+For unquoted values, escape regex metacharacters with a single backslash:
+ file:package\.json — matches literal "package.json"
+
+## Examples
+
+Input: find all TODO comments
+Output: //\s*TODO
+
+Input: find TypeScript files that use useState
+Output: lang:TypeScript useState
+
+Input: find files that import from react
+Output: lang:TypeScript "from \"react\""
+
+Input: find all test files
+Output: file:(test|spec)
+
+Input: find all API route handlers
+Output: file:route\.(ts|js)$
+
+Input: find package.json files that depend on react
+Output: file:package\.json "\"react\": \""
+
+Input: find package.json files with beta or alpha dependencies
+Output: file:package\.json "\"[^\"]+\": \"[^\"]*-(beta|alpha)"
+
+Input: find package.json files where next is pinned to version 15
+Output: file:package\.json "\"next\": \"\\^?15\\."
+
+Input: find next versions less than 15
+Output: file:package\.json "\"next\": \"\^?(1[0-4]|[1-9])\."
+
+Input: find log4j versions 2.3.x or lower
+Output: file:package\.json "\"log4j\": \"\^?2\.([0-2]|3)\."
+
+Input: find TypeScript files that import from react or react-dom
+Output: lang:TypeScript "from \"(react|react-dom)\""
+
+Input: find files with password reset logic, excluding tests
+Output: "password reset" -file:test
+`.trim();
diff --git a/packages/web/src/app/[domain]/browse/layout.tsx b/packages/web/src/app/[domain]/browse/layout.tsx
index 47b07118d..b5b7d1374 100644
--- a/packages/web/src/app/[domain]/browse/layout.tsx
+++ b/packages/web/src/app/[domain]/browse/layout.tsx
@@ -1,5 +1,6 @@
import { auth } from "@/auth";
import { LayoutClient } from "./layoutClient";
+import { getConfiguredLanguageModelsInfo } from "@/features/chat/actions";
interface LayoutProps {
children: React.ReactNode;
@@ -9,8 +10,9 @@ export default async function Layout({
children,
}: LayoutProps) {
const session = await auth();
+ const languageModels = await getConfiguredLanguageModelsInfo();
return (
-
+ 0}>
{children}
)
diff --git a/packages/web/src/app/[domain]/browse/layoutClient.tsx b/packages/web/src/app/[domain]/browse/layoutClient.tsx
index 87f7b4048..8128b5717 100644
--- a/packages/web/src/app/[domain]/browse/layoutClient.tsx
+++ b/packages/web/src/app/[domain]/browse/layoutClient.tsx
@@ -16,11 +16,13 @@ import { Session } from "next-auth";
interface LayoutProps {
children: React.ReactNode;
session: Session | null;
+ isSearchAssistSupported: boolean;
}
export function LayoutClient({
children,
session,
+ isSearchAssistSupported,
}: LayoutProps) {
const { repoName, revisionName } = useBrowseParams();
const domain = useDomain();
@@ -38,6 +40,7 @@ export function LayoutClient({
query: `repo:^${escapeStringRegexp(repoName)}$${revisionName ? ` rev:${revisionName}` : ''} `,
}}
className="w-full"
+ isSearchAssistSupported={isSearchAssistSupported}
/>
void;
+ onQueryGenerated: (query: string) => void;
+ className?: string;
+}
+
+export const SearchAssistBox = ({
+ isEnabled,
+ onBlur,
+ onQueryGenerated,
+ className,
+}: SearchAssistBoxProps) => {
+ const [query, setQuery] = useState("");
+ const boxRef = useRef(null);
+ const inputRef = useRef(null);
+ const [isLoading, setIsLoading] = useState(false);
+ const { toast } = useToast();
+ const captureEvent = useCaptureEvent();
+
+ const translateQuery = useCallback(async () => {
+ if (!query.trim() || isLoading) {
+ return;
+ }
+
+ setIsLoading(true);
+ try {
+ const result = await translateSearchQuery({ prompt: query });
+ if (isServiceError(result)) {
+ toast({ title: "Failed to generate query", description: result.message ?? "An unexpected error occurred.", variant: "destructive" });
+ captureEvent('wa_search_assist_generate_failed', {});
+ return;
+ }
+ onQueryGenerated(result.query);
+ captureEvent('wa_search_assist_query_generated', {});
+ setQuery("");
+ } catch {
+ toast({ title: "Failed to generate query", description: "An unexpected error occurred.", variant: "destructive" });
+ captureEvent('wa_search_assist_generate_failed', {});
+ } finally {
+ setIsLoading(false);
+ }
+ }, [query, isLoading, toast, onQueryGenerated, captureEvent]);
+
+ const onExampleClicked = useCallback((example: string) => {
+ setQuery(example);
+ inputRef.current?.focus();
+ captureEvent('wa_search_assist_example_clicked', { example });
+ }, [captureEvent]);
+
+ if (!isEnabled) {
+ return null;
+ }
+
+ return (
+
{
+ // Don't close if focus is moving to another element within this box
+ if (boxRef.current?.contains(e.relatedTarget as Node)) {
+ return;
+ }
+
+ onBlur();
+ }}
+ >
+
+
Generate a query
+
+
+
+
+
+
Describe what you're looking for in natural language and AI will generate a search query for you.