Skip to content

Commit 9710a50

Browse files
committed
Update code search to limit results per file and globally
1 parent 20c2a99 commit 9710a50

File tree

4 files changed

+358
-16
lines changed

4 files changed

+358
-16
lines changed

backend/src/tools/definitions/tool/code-search.ts

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,11 +19,18 @@ Use cases:
1919
2020
The pattern supports regular expressions and will search recursively through all files in the project by default. Some tips:
2121
- Be as constraining in the pattern as possible to limit the number of files returned, e.g. if searching for the definition of a function, use "(function foo|const foo)" or "def foo" instead of merely "foo".
22+
- Use Rust-style regex, not grep-style, PCRE, RE2 or JavaScript regex - you must always escape special characters like { and }
23+
- Be as constraining as possible to limit results, e.g. use "(function foo|const foo)" or "def foo" instead of merely "foo"
24+
- Add context to your search with surrounding terms (e.g., "function handleAuth" rather than just "handleAuth")
2225
- Use word boundaries (\\b) to match whole words only
26+
- Use the cwd parameter to narrow your search to specific directories
27+
- For case-sensitive searches like constants (e.g., ERROR vs error), omit the "-i" flag
2328
- Searches file content and filenames
2429
- Automatically ignores binary files, hidden files, and files in .gitignore
2530
26-
Advanced ripgrep flags (use the flags parameter):
31+
32+
ADVANCED RIPGREP FLAGS (use the flags parameter):
33+
2734
- Case sensitivity: "-i" for case-insensitive search
2835
- File type filtering: "-t ts" (TypeScript), "-t js" (JavaScript), "-t py" (Python), etc.
2936
- Exclude file types: "--type-not test" to exclude test files
@@ -37,6 +44,14 @@ Advanced ripgrep flags (use the flags parameter):
3744
3845
Note: Do not use the end_turn tool after this tool! You will want to see the output of this tool before ending your turn.
3946
47+
RESULT LIMITING:
48+
49+
- The maxResults parameter limits the number of results shown per file (default: 15)
50+
- There is also a global limit of 250 total results across all files
51+
- These limits allow you to see results across multiple files without being overwhelmed by matches in a single file
52+
- If a file has more matches than maxResults, you'll see a truncation notice indicating how many results were found
53+
- If the global limit is reached, remaining files will be skipped
54+
4055
Examples:
4156
${getToolCallString(toolName, { pattern: 'foo' })}
4257
${getToolCallString(toolName, { pattern: 'foo\\.bar = 1\\.0' })}

common/src/tools/params/tool/code-search.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,8 +30,10 @@ export const codeSearchParams = {
3030
.int()
3131
.positive()
3232
.optional()
33-
.default(30)
34-
.describe(`Maximum number of results to return. Defaults to 30.`),
33+
.default(15)
34+
.describe(
35+
`Maximum number of results to return per file. Defaults to 15. There is also a global limit of 250 results across all files.`,
36+
),
3537
})
3638
.describe(
3739
`Search for string patterns in the project's files. This tool uses ripgrep (rg), a fast line-oriented search tool. Use this tool only when read_files is not sufficient to find the files you need.`,

npm-app/src/__tests__/tool-handlers.test.ts

Lines changed: 245 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -140,8 +140,251 @@ export interface TestInterface {
140140

141141
// Should contain results limited message when there are more results than maxResults
142142
const stdout = (result[0].value as any).stdout
143-
if (stdout.includes('Results limited to 1 of')) {
144-
expect(stdout).toContain('Results limited to 1 of')
143+
if (stdout.includes('Results limited to')) {
144+
expect(stdout).toContain('Results limited to')
145+
}
146+
})
147+
148+
test('uses default limit of 15 per file when maxResults not specified', async () => {
149+
// Create a file with many lines matching the pattern
150+
const manyLinesContent = Array.from(
151+
{ length: 30 },
152+
(_, i) => `export const TEST_VAR_${i} = 'value${i}';`,
153+
).join('\n')
154+
155+
await fs.promises.writeFile(
156+
path.join(testDataDir, 'many-matches.ts'),
157+
manyLinesContent,
158+
)
159+
160+
// Explicitly not passing maxResults to test default
161+
const parameters: any = {
162+
pattern: 'TEST_VAR_',
163+
cwd: 'src/__tests__/data',
164+
}
165+
166+
const result = await handleCodeSearch(parameters, 'test-id')
167+
const stdout = (result[0].value as any).stdout
168+
169+
// Should limit to 15 results per file by default
170+
const lines = stdout
171+
.split('\n')
172+
.filter((line: string) => line.includes('TEST_VAR_'))
173+
expect(lines.length).toBeLessThanOrEqual(15)
174+
expect(stdout).toContain('Results limited to 15 per file')
175+
})
176+
177+
test('applies per-file limit correctly across multiple files', async () => {
178+
// Create multiple files with many matches each
179+
for (let fileNum = 1; fileNum <= 3; fileNum++) {
180+
const content = Array.from(
181+
{ length: 20 },
182+
(_, i) => `export const VAR_F${fileNum}_${i} = 'value';`,
183+
).join('\n')
184+
185+
await fs.promises.writeFile(
186+
path.join(testDataDir, `file${fileNum}.ts`),
187+
content,
188+
)
189+
}
190+
191+
const parameters = {
192+
pattern: 'VAR_F',
193+
cwd: 'src/__tests__/data',
194+
maxResults: 10,
195+
}
196+
197+
const result = await handleCodeSearch(parameters, 'test-id')
198+
const stdout = (result[0].value as any).stdout
199+
200+
// Each file should be limited to 10 results
201+
expect(stdout).toContain('Results limited to 10 per file')
202+
203+
// Count actual result lines (not truncation messages)
204+
// Split by the truncation message section to only count actual results
205+
const resultsSection = stdout.split('[Results limited to')[0]
206+
const file1Matches = (resultsSection.match(/file1\.ts:/g) || []).length
207+
const file2Matches = (resultsSection.match(/file2\.ts:/g) || []).length
208+
const file3Matches = (resultsSection.match(/file3\.ts:/g) || []).length
209+
210+
// Each file should have at most 10 result lines
211+
expect(file1Matches).toBeLessThanOrEqual(10)
212+
expect(file2Matches).toBeLessThanOrEqual(10)
213+
expect(file3Matches).toBeLessThanOrEqual(10)
214+
})
215+
216+
test('respects global limit of 250 results', async () => {
217+
// Create many files with multiple matches to exceed global limit
218+
for (let fileNum = 1; fileNum <= 30; fileNum++) {
219+
const content = Array.from(
220+
{ length: 15 },
221+
(_, i) => `export const GLOBAL_VAR_${fileNum}_${i} = 'value';`,
222+
).join('\n')
223+
224+
await fs.promises.writeFile(
225+
path.join(testDataDir, `global-test-${fileNum}.ts`),
226+
content,
227+
)
228+
}
229+
230+
// Using default maxResults of 15
231+
const parameters: any = {
232+
pattern: 'GLOBAL_VAR_',
233+
cwd: 'src/__tests__/data',
234+
}
235+
236+
const result = await handleCodeSearch(parameters, 'test-id')
237+
const stdout = (result[0].value as any).stdout
238+
239+
// Count total result lines
240+
const totalMatches = (stdout.match(/GLOBAL_VAR_/g) || []).length
241+
242+
// Should not exceed 250 results
243+
expect(totalMatches).toBeLessThanOrEqual(250)
244+
245+
// Should mention global limit if reached
246+
if (totalMatches === 250) {
247+
expect(stdout).toContain('Global limit of 250 results reached')
248+
}
249+
})
250+
251+
test('shows correct truncation message with per-file limits', async () => {
252+
// Create a file with many matches
253+
const content = Array.from(
254+
{ length: 25 },
255+
(_, i) => `const TRUNC_VAR_${i} = 'value${i}';`,
256+
).join('\n')
257+
258+
await fs.promises.writeFile(
259+
path.join(testDataDir, 'truncate-test.ts'),
260+
content,
261+
)
262+
263+
const parameters = {
264+
pattern: 'TRUNC_VAR_',
265+
cwd: 'src/__tests__/data',
266+
maxResults: 10,
267+
}
268+
269+
const result = await handleCodeSearch(parameters, 'test-id')
270+
const stdout = (result[0].value as any).stdout
271+
272+
// Should show which file was truncated
273+
expect(stdout).toContain('Results limited to 10 per file')
274+
expect(stdout).toContain('truncate-test.ts')
275+
expect(stdout).toMatch(/25 results \(showing 10\)/)
276+
})
277+
278+
test('handles global limit with skipped files message', async () => {
279+
// Create enough files to trigger global limit
280+
for (let fileNum = 1; fileNum <= 25; fileNum++) {
281+
const content = Array.from(
282+
{ length: 12 },
283+
(_, i) => `const SKIP_VAR_${fileNum}_${i} = ${i};`,
284+
).join('\n')
285+
286+
await fs.promises.writeFile(
287+
path.join(testDataDir, `skip-test-${fileNum}.ts`),
288+
content,
289+
)
290+
}
291+
292+
// Using default maxResults of 15
293+
const parameters: any = {
294+
pattern: 'SKIP_VAR_',
295+
cwd: 'src/__tests__/data',
296+
}
297+
298+
const result = await handleCodeSearch(parameters, 'test-id')
299+
const stdout = (result[0].value as any).stdout
300+
301+
// Should show skipped files message
302+
const totalMatches = (stdout.match(/SKIP_VAR_/g) || []).length
303+
304+
if (totalMatches >= 250) {
305+
expect(stdout).toContain('Global limit of 250 results reached')
306+
expect(stdout).toMatch(/\d+ file\(s\) skipped/)
307+
}
308+
})
309+
310+
test('applies remaining global space correctly', async () => {
311+
// Create files where global limit is hit mid-file
312+
for (let fileNum = 1; fileNum <= 20; fileNum++) {
313+
const content = Array.from(
314+
{ length: 15 },
315+
(_, i) => `const SPACE_VAR_${fileNum}_${i} = ${i};`,
316+
).join('\n')
317+
318+
await fs.promises.writeFile(
319+
path.join(testDataDir, `space-test-${fileNum}.ts`),
320+
content,
321+
)
322+
}
323+
324+
const parameters = {
325+
pattern: 'SPACE_VAR_',
326+
cwd: 'src/__tests__/data',
327+
maxResults: 15,
328+
}
329+
330+
const result = await handleCodeSearch(parameters, 'test-id')
331+
const stdout = (result[0].value as any).stdout
332+
333+
// Count total matches - should not exceed 250
334+
const totalMatches = (stdout.match(/SPACE_VAR_/g) || []).length
335+
expect(totalMatches).toBeLessThanOrEqual(250)
336+
})
337+
338+
test('handles case when no results exceed limits', async () => {
339+
// Create files with few matches
340+
await fs.promises.writeFile(
341+
path.join(testDataDir, 'small-file.ts'),
342+
'const SMALL_VAR = 1;\nconst SMALL_VAR_2 = 2;',
343+
)
344+
345+
const parameters = {
346+
pattern: 'SMALL_VAR',
347+
cwd: 'src/__tests__/data',
348+
maxResults: 15,
349+
}
350+
351+
const result = await handleCodeSearch(parameters, 'test-id')
352+
const stdout = (result[0].value as any).stdout
353+
354+
// Should not contain truncation messages
355+
expect(stdout).not.toContain('Results limited to')
356+
expect(stdout).not.toContain('Global limit')
357+
})
358+
359+
test('combines per-file and global limit messages correctly', async () => {
360+
// Create scenario where both limits are triggered
361+
for (let fileNum = 1; fileNum <= 22; fileNum++) {
362+
const content = Array.from(
363+
{ length: 20 },
364+
(_, i) => `const COMBINED_VAR_${fileNum}_${i} = ${i};`,
365+
).join('\n')
366+
367+
await fs.promises.writeFile(
368+
path.join(testDataDir, `combined-test-${fileNum}.ts`),
369+
content,
370+
)
371+
}
372+
373+
const parameters = {
374+
pattern: 'COMBINED_VAR_',
375+
cwd: 'src/__tests__/data',
376+
maxResults: 12,
377+
}
378+
379+
const result = await handleCodeSearch(parameters, 'test-id')
380+
const stdout = (result[0].value as any).stdout
381+
382+
// Should contain both messages
383+
const totalMatches = (stdout.match(/COMBINED_VAR_/g) || []).length
384+
385+
if (totalMatches >= 250) {
386+
expect(stdout).toContain('Results limited to 12 per file')
387+
expect(stdout).toContain('Global limit of 250 results reached')
145388
}
146389
})
147390
})

0 commit comments

Comments
 (0)