This commit is contained in:
bkellam 2025-07-27 16:33:05 -07:00
parent 4343b3c3d5
commit 2e722e3fd7
4 changed files with 237 additions and 10 deletions

View file

@ -22,11 +22,11 @@ export const SearchCodeToolComponent = ({ part }: { part: SearchCodeToolUIPart }
case 'input-streaming':
return 'Searching...';
case 'input-available':
return <span>Searching for <CodeSnippet>{part.input.query}</CodeSnippet></span>;
return <span>Searching for <CodeSnippet>{part.input.queryRegexp}</CodeSnippet></span>;
case 'output-error':
return '"Search code" tool call failed';
case 'output-available':
return <span>Searched for <CodeSnippet>{part.input.query}</CodeSnippet></span>;
return <span>Searched for <CodeSnippet>{part.input.queryRegexp}</CodeSnippet></span>;
}
}, [part]);

View file

@ -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,

View file

@ -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');
});

View file

@ -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;
}