diff --git a/packages/web/src/features/chat/components/chatThread/tools/searchCodeToolComponent.tsx b/packages/web/src/features/chat/components/chatThread/tools/searchCodeToolComponent.tsx
index a4d961fa..a7792ade 100644
--- a/packages/web/src/features/chat/components/chatThread/tools/searchCodeToolComponent.tsx
+++ b/packages/web/src/features/chat/components/chatThread/tools/searchCodeToolComponent.tsx
@@ -22,11 +22,11 @@ export const SearchCodeToolComponent = ({ part }: { part: SearchCodeToolUIPart }
case 'input-streaming':
return 'Searching...';
case 'input-available':
- return Searching for {part.input.query};
+ return Searching for {part.input.queryRegexp};
case 'output-error':
return '"Search code" tool call failed';
case 'output-available':
- return Searched for {part.input.query};
+ return Searched for {part.input.queryRegexp};
}
}, [part]);
diff --git a/packages/web/src/features/chat/tools.ts b/packages/web/src/features/chat/tools.ts
index 4149635a..3a6be519 100644
--- a/packages/web/src/features/chat/tools.ts
+++ b/packages/web/src/features/chat/tools.ts
@@ -6,7 +6,7 @@ import { isServiceError } from "@/lib/utils";
import { getFileSource } from "../search/fileSourceApi";
import { findSearchBasedSymbolDefinitions, findSearchBasedSymbolReferences } from "../codeNav/actions";
import { FileSourceResponse } from "../search/types";
-import { addLineNumbers } from "./utils";
+import { addLineNumbers, buildSearchQuery } from "./utils";
import { toolNames } from "./constants";
// @NOTE: When adding a new tool, follow these steps:
@@ -139,13 +139,43 @@ export const createCodeSearchTool = (selectedRepos: string[]) => tool({
description: `Fetches code that matches the provided regex pattern in \`query\`. This is NOT a semantic search.
Results are returned as an array of matching files, with the file's URL, repository, and language.`,
inputSchema: z.object({
- query: z.string().describe("The regex pattern to search for in the code"),
+ queryRegexp: z
+ .string()
+ .describe(`The regex pattern to search for in the code.
+
+Queries consist of space-seperated regular expressions. Wrapping expressions in "" combines them. By default, a file must have at least one match for each expression to be included. Examples:
+
+\`foo\` - Match files with regex /foo/
+\`foo bar\` - Match files with regex /foo/ and /bar/
+\`"foo bar"\` - Match files with regex /foo bar/
+\`console\.log\` - Match files with regex /console\.log/
+
+Multiple expressions can be or'd together with or, negated with -, or grouped with (). Examples:
+\`foo or bar\` - Match files with regex /foo/ or /bar/
+\`foo -bar\` - Match files with regex /foo/ but not /bar/
+\`foo (bar or baz)\` - Match files with regex /foo/ and either /bar/ or /baz/
+`),
+ repoNamesFilterRegexp: z
+ .array(z.string())
+ .describe(`Filter results from repos that match the regex. By default all repos are searched.`)
+ .optional(),
+ languageNamesFilter: z
+ .array(z.string())
+ .describe(`Scope the search to the provided languages. The language MUST be formatted as a GitHub linguist language. Examples: Python, JavaScript, TypeScript, Java, C#, C++, PHP, Go, Rust, Ruby, Swift, Kotlin, Shell, C, Dart, HTML, CSS, PowerShell, SQL, R`)
+ .optional(),
+ fileNamesFilterRegexp: z
+ .array(z.string())
+ .describe(`Filter results from filepaths that match the regex. When this option is not specified, all files are searched.`)
+ .optional(),
}),
- execute: async ({ query: _query }) => {
- let query = `${_query}`;
- if (selectedRepos.length > 0) {
- query += ` reposet:${selectedRepos.join(',')}`;
- }
+ execute: async ({ queryRegexp: _query, repoNamesFilterRegexp, languageNamesFilter, fileNamesFilterRegexp }) => {
+ const query = buildSearchQuery({
+ query: _query,
+ repoNamesFilter: selectedRepos,
+ repoNamesFilterRegexp,
+ languageNamesFilter,
+ fileNamesFilterRegexp,
+ });
const response = await search({
query,
diff --git a/packages/web/src/features/chat/utils.test.ts b/packages/web/src/features/chat/utils.test.ts
index 72390378..3ebd4e31 100644
--- a/packages/web/src/features/chat/utils.test.ts
+++ b/packages/web/src/features/chat/utils.test.ts
@@ -1,5 +1,5 @@
import { expect, test, vi } from 'vitest'
-import { fileReferenceToString, getAnswerPartFromAssistantMessage, groupMessageIntoSteps, repairReferences } from './utils'
+import { fileReferenceToString, getAnswerPartFromAssistantMessage, groupMessageIntoSteps, repairReferences, buildSearchQuery } from './utils'
import { FILE_REFERENCE_REGEX, ANSWER_TAG } from './constants';
import { SBChatMessage, SBChatMessagePart } from './types';
@@ -350,4 +350,165 @@ test('repairReferences handles malformed inline code blocks', () => {
const input = 'See `@file:{github.com/sourcebot-dev/sourcebot::packages/web/src/auth.ts`} for details.';
const expected = 'See @file:{github.com/sourcebot-dev/sourcebot::packages/web/src/auth.ts} for details.';
expect(repairReferences(input)).toBe(expected);
+});
+
+test('buildSearchQuery returns base query when no filters provided', () => {
+ const result = buildSearchQuery({
+ query: 'console.log'
+ });
+
+ expect(result).toBe('console.log');
+});
+
+test('buildSearchQuery adds repoNamesFilter correctly', () => {
+ const result = buildSearchQuery({
+ query: 'function test',
+ repoNamesFilter: ['repo1', 'repo2']
+ });
+
+ expect(result).toBe('function test reposet:repo1,repo2');
+});
+
+test('buildSearchQuery adds single repoNamesFilter correctly', () => {
+ const result = buildSearchQuery({
+ query: 'function test',
+ repoNamesFilter: ['myrepo']
+ });
+
+ expect(result).toBe('function test reposet:myrepo');
+});
+
+test('buildSearchQuery ignores empty repoNamesFilter', () => {
+ const result = buildSearchQuery({
+ query: 'function test',
+ repoNamesFilter: []
+ });
+
+ expect(result).toBe('function test');
+});
+
+test('buildSearchQuery adds languageNamesFilter correctly', () => {
+ const result = buildSearchQuery({
+ query: 'class definition',
+ languageNamesFilter: ['typescript', 'javascript']
+ });
+
+ expect(result).toBe('class definition ( lang:typescript or lang:javascript )');
+});
+
+test('buildSearchQuery adds single languageNamesFilter correctly', () => {
+ const result = buildSearchQuery({
+ query: 'class definition',
+ languageNamesFilter: ['python']
+ });
+
+ expect(result).toBe('class definition ( lang:python )');
+});
+
+test('buildSearchQuery ignores empty languageNamesFilter', () => {
+ const result = buildSearchQuery({
+ query: 'class definition',
+ languageNamesFilter: []
+ });
+
+ expect(result).toBe('class definition');
+});
+
+test('buildSearchQuery adds fileNamesFilterRegexp correctly', () => {
+ const result = buildSearchQuery({
+ query: 'import statement',
+ fileNamesFilterRegexp: ['*.ts', '*.js']
+ });
+
+ expect(result).toBe('import statement ( file:*.ts or file:*.js )');
+});
+
+test('buildSearchQuery adds single fileNamesFilterRegexp correctly', () => {
+ const result = buildSearchQuery({
+ query: 'import statement',
+ fileNamesFilterRegexp: ['*.tsx']
+ });
+
+ expect(result).toBe('import statement ( file:*.tsx )');
+});
+
+test('buildSearchQuery ignores empty fileNamesFilterRegexp', () => {
+ const result = buildSearchQuery({
+ query: 'import statement',
+ fileNamesFilterRegexp: []
+ });
+
+ expect(result).toBe('import statement');
+});
+
+test('buildSearchQuery adds repoNamesFilterRegexp correctly', () => {
+ const result = buildSearchQuery({
+ query: 'bug fix',
+ repoNamesFilterRegexp: ['org/repo1', 'org/repo2']
+ });
+
+ expect(result).toBe('bug fix ( repo:org/repo1 or repo:org/repo2 )');
+});
+
+test('buildSearchQuery adds single repoNamesFilterRegexp correctly', () => {
+ const result = buildSearchQuery({
+ query: 'bug fix',
+ repoNamesFilterRegexp: ['myorg/myrepo']
+ });
+
+ expect(result).toBe('bug fix ( repo:myorg/myrepo )');
+});
+
+test('buildSearchQuery ignores empty repoNamesFilterRegexp', () => {
+ const result = buildSearchQuery({
+ query: 'bug fix',
+ repoNamesFilterRegexp: []
+ });
+
+ expect(result).toBe('bug fix');
+});
+
+test('buildSearchQuery combines multiple filters correctly', () => {
+ const result = buildSearchQuery({
+ query: 'authentication',
+ repoNamesFilter: ['backend', 'frontend'],
+ languageNamesFilter: ['typescript', 'javascript'],
+ fileNamesFilterRegexp: ['*.ts', '*.js'],
+ repoNamesFilterRegexp: ['org/auth-*']
+ });
+
+ expect(result).toBe(
+ 'authentication reposet:backend,frontend ( lang:typescript or lang:javascript ) ( file:*.ts or file:*.js ) ( repo:org/auth-* )'
+ );
+});
+
+test('buildSearchQuery handles mixed empty and non-empty filters', () => {
+ const result = buildSearchQuery({
+ query: 'error handling',
+ repoNamesFilter: [],
+ languageNamesFilter: ['python'],
+ fileNamesFilterRegexp: [],
+ repoNamesFilterRegexp: ['error/*']
+ });
+
+ expect(result).toBe('error handling ( lang:python ) ( repo:error/* )');
+});
+
+test('buildSearchQuery handles empty base query', () => {
+ const result = buildSearchQuery({
+ query: '',
+ repoNamesFilter: ['repo1'],
+ languageNamesFilter: ['typescript']
+ });
+
+ expect(result).toBe(' reposet:repo1 ( lang:typescript )');
+});
+
+test('buildSearchQuery handles query with special characters', () => {
+ const result = buildSearchQuery({
+ query: 'console.log("hello world")',
+ repoNamesFilter: ['test-repo']
+ });
+
+ expect(result).toBe('console.log("hello world") reposet:test-repo');
});
\ No newline at end of file
diff --git a/packages/web/src/features/chat/utils.ts b/packages/web/src/features/chat/utils.ts
index d84835ef..6207cb0e 100644
--- a/packages/web/src/features/chat/utils.ts
+++ b/packages/web/src/features/chat/utils.ts
@@ -329,4 +329,40 @@ export const getAnswerPartFromAssistantMessage = (message: SBChatMessage, isStre
}
return undefined;
+}
+
+export const buildSearchQuery = (options: {
+ query: string,
+ repoNamesFilter?: string[],
+ repoNamesFilterRegexp?: string[],
+ languageNamesFilter?: string[],
+ fileNamesFilterRegexp?: string[],
+}) => {
+ const {
+ query: _query,
+ repoNamesFilter,
+ repoNamesFilterRegexp,
+ languageNamesFilter,
+ fileNamesFilterRegexp,
+ } = options;
+
+ let query = `${_query}`;
+
+ if (repoNamesFilter && repoNamesFilter.length > 0) {
+ query += ` reposet:${repoNamesFilter.join(',')}`;
+ }
+
+ if (languageNamesFilter && languageNamesFilter.length > 0) {
+ query += ` ( lang:${languageNamesFilter.join(' or lang:')} )`;
+ }
+
+ if (fileNamesFilterRegexp && fileNamesFilterRegexp.length > 0) {
+ query += ` ( file:${fileNamesFilterRegexp.join(' or file:')} )`;
+ }
+
+ if (repoNamesFilterRegexp && repoNamesFilterRegexp.length > 0) {
+ query += ` ( repo:${repoNamesFilterRegexp.join(' or repo:')} )`;
+ }
+
+ return query;
}
\ No newline at end of file