Skip to content

Commit 3f0ff11

Browse files
wip
1 parent aebd8df commit 3f0ff11

File tree

4 files changed

+237
-10
lines changed

4 files changed

+237
-10
lines changed

packages/web/src/features/chat/components/chatThread/tools/searchCodeToolComponent.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,11 +22,11 @@ export const SearchCodeToolComponent = ({ part }: { part: SearchCodeToolUIPart }
2222
case 'input-streaming':
2323
return 'Searching...';
2424
case 'input-available':
25-
return <span>Searching for <CodeSnippet>{part.input.query}</CodeSnippet></span>;
25+
return <span>Searching for <CodeSnippet>{part.input.queryRegexp}</CodeSnippet></span>;
2626
case 'output-error':
2727
return '"Search code" tool call failed';
2828
case 'output-available':
29-
return <span>Searched for <CodeSnippet>{part.input.query}</CodeSnippet></span>;
29+
return <span>Searched for <CodeSnippet>{part.input.queryRegexp}</CodeSnippet></span>;
3030
}
3131
}, [part]);
3232

packages/web/src/features/chat/tools.ts

Lines changed: 37 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import { isServiceError } from "@/lib/utils";
66
import { getFileSource } from "../search/fileSourceApi";
77
import { findSearchBasedSymbolDefinitions, findSearchBasedSymbolReferences } from "../codeNav/actions";
88
import { FileSourceResponse } from "../search/types";
9-
import { addLineNumbers } from "./utils";
9+
import { addLineNumbers, buildSearchQuery } from "./utils";
1010
import { toolNames } from "./constants";
1111

1212
// @NOTE: When adding a new tool, follow these steps:
@@ -139,13 +139,43 @@ export const createCodeSearchTool = (selectedRepos: string[]) => tool({
139139
description: `Fetches code that matches the provided regex pattern in \`query\`. This is NOT a semantic search.
140140
Results are returned as an array of matching files, with the file's URL, repository, and language.`,
141141
inputSchema: z.object({
142-
query: z.string().describe("The regex pattern to search for in the code"),
142+
queryRegexp: z
143+
.string()
144+
.describe(`The regex pattern to search for in the code.
145+
146+
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:
147+
148+
\`foo\` - Match files with regex /foo/
149+
\`foo bar\` - Match files with regex /foo/ and /bar/
150+
\`"foo bar"\` - Match files with regex /foo bar/
151+
\`console\.log\` - Match files with regex /console\.log/
152+
153+
Multiple expressions can be or'd together with or, negated with -, or grouped with (). Examples:
154+
\`foo or bar\` - Match files with regex /foo/ or /bar/
155+
\`foo -bar\` - Match files with regex /foo/ but not /bar/
156+
\`foo (bar or baz)\` - Match files with regex /foo/ and either /bar/ or /baz/
157+
`),
158+
repoNamesFilterRegexp: z
159+
.array(z.string())
160+
.describe(`Filter results from repos that match the regex. By default all repos are searched.`)
161+
.optional(),
162+
languageNamesFilter: z
163+
.array(z.string())
164+
.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`)
165+
.optional(),
166+
fileNamesFilterRegexp: z
167+
.array(z.string())
168+
.describe(`Filter results from filepaths that match the regex. When this option is not specified, all files are searched.`)
169+
.optional(),
143170
}),
144-
execute: async ({ query: _query }) => {
145-
let query = `${_query}`;
146-
if (selectedRepos.length > 0) {
147-
query += ` reposet:${selectedRepos.join(',')}`;
148-
}
171+
execute: async ({ queryRegexp: _query, repoNamesFilterRegexp, languageNamesFilter, fileNamesFilterRegexp }) => {
172+
const query = buildSearchQuery({
173+
query: _query,
174+
repoNamesFilter: selectedRepos,
175+
repoNamesFilterRegexp,
176+
languageNamesFilter,
177+
fileNamesFilterRegexp,
178+
});
149179

150180
const response = await search({
151181
query,

packages/web/src/features/chat/utils.test.ts

Lines changed: 162 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { expect, test, vi } from 'vitest'
2-
import { fileReferenceToString, getAnswerPartFromAssistantMessage, groupMessageIntoSteps, repairReferences } from './utils'
2+
import { fileReferenceToString, getAnswerPartFromAssistantMessage, groupMessageIntoSteps, repairReferences, buildSearchQuery } from './utils'
33
import { FILE_REFERENCE_REGEX, ANSWER_TAG } from './constants';
44
import { SBChatMessage, SBChatMessagePart } from './types';
55

@@ -350,4 +350,165 @@ test('repairReferences handles malformed inline code blocks', () => {
350350
const input = 'See `@file:{github.com/sourcebot-dev/sourcebot::packages/web/src/auth.ts`} for details.';
351351
const expected = 'See @file:{github.com/sourcebot-dev/sourcebot::packages/web/src/auth.ts} for details.';
352352
expect(repairReferences(input)).toBe(expected);
353+
});
354+
355+
test('buildSearchQuery returns base query when no filters provided', () => {
356+
const result = buildSearchQuery({
357+
query: 'console.log'
358+
});
359+
360+
expect(result).toBe('console.log');
361+
});
362+
363+
test('buildSearchQuery adds repoNamesFilter correctly', () => {
364+
const result = buildSearchQuery({
365+
query: 'function test',
366+
repoNamesFilter: ['repo1', 'repo2']
367+
});
368+
369+
expect(result).toBe('function test reposet:repo1,repo2');
370+
});
371+
372+
test('buildSearchQuery adds single repoNamesFilter correctly', () => {
373+
const result = buildSearchQuery({
374+
query: 'function test',
375+
repoNamesFilter: ['myrepo']
376+
});
377+
378+
expect(result).toBe('function test reposet:myrepo');
379+
});
380+
381+
test('buildSearchQuery ignores empty repoNamesFilter', () => {
382+
const result = buildSearchQuery({
383+
query: 'function test',
384+
repoNamesFilter: []
385+
});
386+
387+
expect(result).toBe('function test');
388+
});
389+
390+
test('buildSearchQuery adds languageNamesFilter correctly', () => {
391+
const result = buildSearchQuery({
392+
query: 'class definition',
393+
languageNamesFilter: ['typescript', 'javascript']
394+
});
395+
396+
expect(result).toBe('class definition ( lang:typescript or lang:javascript )');
397+
});
398+
399+
test('buildSearchQuery adds single languageNamesFilter correctly', () => {
400+
const result = buildSearchQuery({
401+
query: 'class definition',
402+
languageNamesFilter: ['python']
403+
});
404+
405+
expect(result).toBe('class definition ( lang:python )');
406+
});
407+
408+
test('buildSearchQuery ignores empty languageNamesFilter', () => {
409+
const result = buildSearchQuery({
410+
query: 'class definition',
411+
languageNamesFilter: []
412+
});
413+
414+
expect(result).toBe('class definition');
415+
});
416+
417+
test('buildSearchQuery adds fileNamesFilterRegexp correctly', () => {
418+
const result = buildSearchQuery({
419+
query: 'import statement',
420+
fileNamesFilterRegexp: ['*.ts', '*.js']
421+
});
422+
423+
expect(result).toBe('import statement ( file:*.ts or file:*.js )');
424+
});
425+
426+
test('buildSearchQuery adds single fileNamesFilterRegexp correctly', () => {
427+
const result = buildSearchQuery({
428+
query: 'import statement',
429+
fileNamesFilterRegexp: ['*.tsx']
430+
});
431+
432+
expect(result).toBe('import statement ( file:*.tsx )');
433+
});
434+
435+
test('buildSearchQuery ignores empty fileNamesFilterRegexp', () => {
436+
const result = buildSearchQuery({
437+
query: 'import statement',
438+
fileNamesFilterRegexp: []
439+
});
440+
441+
expect(result).toBe('import statement');
442+
});
443+
444+
test('buildSearchQuery adds repoNamesFilterRegexp correctly', () => {
445+
const result = buildSearchQuery({
446+
query: 'bug fix',
447+
repoNamesFilterRegexp: ['org/repo1', 'org/repo2']
448+
});
449+
450+
expect(result).toBe('bug fix ( repo:org/repo1 or repo:org/repo2 )');
451+
});
452+
453+
test('buildSearchQuery adds single repoNamesFilterRegexp correctly', () => {
454+
const result = buildSearchQuery({
455+
query: 'bug fix',
456+
repoNamesFilterRegexp: ['myorg/myrepo']
457+
});
458+
459+
expect(result).toBe('bug fix ( repo:myorg/myrepo )');
460+
});
461+
462+
test('buildSearchQuery ignores empty repoNamesFilterRegexp', () => {
463+
const result = buildSearchQuery({
464+
query: 'bug fix',
465+
repoNamesFilterRegexp: []
466+
});
467+
468+
expect(result).toBe('bug fix');
469+
});
470+
471+
test('buildSearchQuery combines multiple filters correctly', () => {
472+
const result = buildSearchQuery({
473+
query: 'authentication',
474+
repoNamesFilter: ['backend', 'frontend'],
475+
languageNamesFilter: ['typescript', 'javascript'],
476+
fileNamesFilterRegexp: ['*.ts', '*.js'],
477+
repoNamesFilterRegexp: ['org/auth-*']
478+
});
479+
480+
expect(result).toBe(
481+
'authentication reposet:backend,frontend ( lang:typescript or lang:javascript ) ( file:*.ts or file:*.js ) ( repo:org/auth-* )'
482+
);
483+
});
484+
485+
test('buildSearchQuery handles mixed empty and non-empty filters', () => {
486+
const result = buildSearchQuery({
487+
query: 'error handling',
488+
repoNamesFilter: [],
489+
languageNamesFilter: ['python'],
490+
fileNamesFilterRegexp: [],
491+
repoNamesFilterRegexp: ['error/*']
492+
});
493+
494+
expect(result).toBe('error handling ( lang:python ) ( repo:error/* )');
495+
});
496+
497+
test('buildSearchQuery handles empty base query', () => {
498+
const result = buildSearchQuery({
499+
query: '',
500+
repoNamesFilter: ['repo1'],
501+
languageNamesFilter: ['typescript']
502+
});
503+
504+
expect(result).toBe(' reposet:repo1 ( lang:typescript )');
505+
});
506+
507+
test('buildSearchQuery handles query with special characters', () => {
508+
const result = buildSearchQuery({
509+
query: 'console.log("hello world")',
510+
repoNamesFilter: ['test-repo']
511+
});
512+
513+
expect(result).toBe('console.log("hello world") reposet:test-repo');
353514
});

packages/web/src/features/chat/utils.ts

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -329,4 +329,40 @@ export const getAnswerPartFromAssistantMessage = (message: SBChatMessage, isStre
329329
}
330330

331331
return undefined;
332+
}
333+
334+
export const buildSearchQuery = (options: {
335+
query: string,
336+
repoNamesFilter?: string[],
337+
repoNamesFilterRegexp?: string[],
338+
languageNamesFilter?: string[],
339+
fileNamesFilterRegexp?: string[],
340+
}) => {
341+
const {
342+
query: _query,
343+
repoNamesFilter,
344+
repoNamesFilterRegexp,
345+
languageNamesFilter,
346+
fileNamesFilterRegexp,
347+
} = options;
348+
349+
let query = `${_query}`;
350+
351+
if (repoNamesFilter && repoNamesFilter.length > 0) {
352+
query += ` reposet:${repoNamesFilter.join(',')}`;
353+
}
354+
355+
if (languageNamesFilter && languageNamesFilter.length > 0) {
356+
query += ` ( lang:${languageNamesFilter.join(' or lang:')} )`;
357+
}
358+
359+
if (fileNamesFilterRegexp && fileNamesFilterRegexp.length > 0) {
360+
query += ` ( file:${fileNamesFilterRegexp.join(' or file:')} )`;
361+
}
362+
363+
if (repoNamesFilterRegexp && repoNamesFilterRegexp.length > 0) {
364+
query += ` ( repo:${repoNamesFilterRegexp.join(' or repo:')} )`;
365+
}
366+
367+
return query;
332368
}

0 commit comments

Comments
 (0)