Skip to content

Commit a8672fd

Browse files
authored
fix(chat): Add maxDepth parameter to ListDirectory tool (aws#6919)
## Problem - ListDirectory tool will get called too many times if the directory has many subdirectories ## Solution - Add maxDepth parameter to ListDirectory tool to allow recursively listing directories - Minor fix on the tool_spec --- - Treat all work as PUBLIC. Private `feature/x` branches will not be squash-merged at release time. - Your code changes must meet the guidelines in [CONTRIBUTING.md](https://github.com/aws/aws-toolkit-vscode/blob/master/CONTRIBUTING.md#guidelines). - License: I confirm that my contribution is made under the terms of the Apache 2.0 license.
1 parent 66da962 commit a8672fd

File tree

4 files changed

+60
-13
lines changed

4 files changed

+60
-13
lines changed

packages/core/src/codewhispererChat/tools/listDirectory.ts

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,20 +12,26 @@ import path from 'path'
1212

1313
export interface ListDirectoryParams {
1414
path: string
15+
maxDepth?: number
1516
}
1617

1718
export class ListDirectory {
1819
private fsPath: string
20+
private maxDepth?: number
1921
private readonly logger = getLogger('listDirectory')
2022

2123
constructor(params: ListDirectoryParams) {
2224
this.fsPath = params.path
25+
this.maxDepth = params.maxDepth
2326
}
2427

2528
public async validate(): Promise<void> {
2629
if (!this.fsPath || this.fsPath.trim().length === 0) {
2730
throw new Error('Path cannot be empty.')
2831
}
32+
if (this.maxDepth !== undefined && this.maxDepth < 0) {
33+
throw new Error('MaxDepth cannot be negative.')
34+
}
2935

3036
const sanitized = sanitizePath(this.fsPath)
3137
this.fsPath = sanitized
@@ -44,14 +50,21 @@ export class ListDirectory {
4450

4551
public queueDescription(updates: Writable): void {
4652
const fileName = path.basename(this.fsPath)
47-
updates.write(`Listing directory: ${fileName}`)
53+
if (this.maxDepth === undefined) {
54+
updates.write(`Listing directory recursively: ${fileName}`)
55+
} else if (this.maxDepth === 0) {
56+
updates.write(`Listing directory: ${fileName}`)
57+
} else {
58+
const level = this.maxDepth > 1 ? 'levels' : 'level'
59+
updates.write(`Listing directory: ${fileName} limited to ${this.maxDepth} subfolder ${level}`)
60+
}
4861
updates.end()
4962
}
5063

5164
public async invoke(updates?: Writable): Promise<InvokeOutput> {
5265
try {
5366
const fileUri = vscode.Uri.file(this.fsPath)
54-
const listing = await readDirectoryRecursively(fileUri, 0)
67+
const listing = await readDirectoryRecursively(fileUri, this.maxDepth)
5568
return this.createOutput(listing.join('\n'))
5669
} catch (error: any) {
5770
this.logger.error(`Failed to list directory "${this.fsPath}": ${error.message || error}`)

packages/core/src/codewhispererChat/tools/tool_index.json

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"fsRead": {
33
"name": "fsRead",
4-
"description": "A tool for reading a file. \n* This tool returns the contents of a file, and the optional `readRange` determines what range of lines will be read from the specified file.",
4+
"description": "A tool for reading a file.\n * This tool returns the contents of a file, and the optional `readRange` determines what range of lines will be read from the specified file.",
55
"inputSchema": {
66
"type": "object",
77
"properties": {
@@ -10,7 +10,7 @@
1010
"type": "string"
1111
},
1212
"readRange": {
13-
"description": "Optional parameter when reading files.\n* If none is given, the full file is shown. If provided, the file will be shown in the indicated line number range, e.g. [11, 12] will show lines 11 and 12. Indexing at 1 to start. Setting `[startLine, -1]` shows all lines from `startLine` to the end of the file.",
13+
"description": "Optional parameter when reading files.\n * If none is given, the full file is shown. If provided, the file will be shown in the indicated line number range, e.g. [11, 12] will show lines 11 and 12. Indexing at 1 to start. Setting `[startLine, -1]` shows all lines from `startLine` to the end of the file.",
1414
"items": {
1515
"type": "integer"
1616
},
@@ -75,7 +75,7 @@
7575
},
7676
"listDirectory": {
7777
"name": "listDirectory",
78-
"description": "List the contents of a directory.\n * Use this tool for discovery, before using more targeted tools like fsRead.\n *Useful to try to understand the file structure before diving deeper into specific files.\n *Can be used to explore the codebase.\n *Results clearly distinguish between files, directories or symlinks with [FILE], [DIR] and [LINK] prefixes.",
78+
"description": "List the contents of a directory and its subdirectories.\n * Use this tool for discovery, before using more targeted tools like fsRead.\n *Useful to try to understand the file structure before diving deeper into specific files.\n *Can be used to explore the codebase.\n *Results clearly distinguish between files, directories or symlinks with [FILE], [DIR] and [LINK] prefixes.",
7979
"inputSchema": {
8080
"type": "object",
8181
"properties": {
@@ -86,6 +86,10 @@
8686
"path": {
8787
"type": "string",
8888
"description": "Absolute path to a directory, e.g., `/repo`."
89+
},
90+
"maxDepth": {
91+
"type": "integer",
92+
"description": "Maximum depth to traverse when listing directories. Use `0` to list only the specified directory, `1` to include immediate subdirectories, etc. If it's not provided, it will list all subdirectories recursively."
8993
}
9094
},
9195
"required": ["path"]

packages/core/src/shared/utilities/workspaceUtils.ts

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -693,16 +693,17 @@ export function formatListing(name: string, fileType: vscode.FileType, fullPath:
693693
* You can either pass a custom callback or rely on the default `formatListing`.
694694
*
695695
* @param dirUri The folder to begin traversing
696-
* @param maxDepth Maximum depth to descend (0 => just this folder)
696+
* @param maxDepth Maximum depth to descend (0 => just this folder, if it's missing => recursively)
697697
* @param customFormatCallback Optional. If given, it will override the default line-formatting
698698
*/
699699
export async function readDirectoryRecursively(
700700
dirUri: vscode.Uri,
701-
maxDepth: number,
701+
maxDepth?: number,
702702
customFormatCallback?: (name: string, fileType: vscode.FileType, fullPath: string) => string
703703
): Promise<string[]> {
704704
const logger = getLogger()
705-
logger.info(`Reading directory: ${dirUri.fsPath} to max depth: ${maxDepth}`)
705+
const depthDescription = maxDepth === undefined ? 'unlimited' : maxDepth
706+
logger.info(`Reading directory: ${dirUri.fsPath} to max depth: ${depthDescription}`)
706707

707708
const queue: Array<{ uri: vscode.Uri; depth: number }> = [{ uri: dirUri, depth: 0 }]
708709
const results: string[] = []
@@ -711,7 +712,7 @@ export async function readDirectoryRecursively(
711712

712713
while (queue.length > 0) {
713714
const { uri, depth } = queue.shift()!
714-
if (depth > maxDepth) {
715+
if (maxDepth !== undefined && depth > maxDepth) {
715716
logger.info(`Skipping directory: ${uri.fsPath} (depth ${depth} > max ${maxDepth})`)
716717
continue
717718
}
@@ -729,7 +730,7 @@ export async function readDirectoryRecursively(
729730
const childUri = vscode.Uri.joinPath(uri, name)
730731
results.push(formatter(name, fileType, childUri.fsPath))
731732

732-
if (fileType === vscode.FileType.Directory && depth < maxDepth) {
733+
if (fileType === vscode.FileType.Directory && (maxDepth === undefined || depth < maxDepth)) {
733734
queue.push({ uri: childUri, depth: depth + 1 })
734735
}
735736
}

packages/core/src/test/codewhispererChat/tools/listDirectory.test.ts

Lines changed: 32 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,13 +15,40 @@ describe('ListDirectory Tool', () => {
1515
})
1616

1717
it('throws if path is empty', async () => {
18-
const listDirectory = new ListDirectory({ path: '' })
18+
const listDirectory = new ListDirectory({ path: '', maxDepth: 0 })
1919
await assert.rejects(listDirectory.validate(), /Path cannot be empty/i, 'Expected an error about empty path')
2020
})
2121

22+
it('throws if maxDepth is negative', async () => {
23+
const listDirectory = new ListDirectory({ path: '~', maxDepth: -1 })
24+
await assert.rejects(
25+
listDirectory.validate(),
26+
/MaxDepth cannot be negative/i,
27+
'Expected an error about negative maxDepth'
28+
)
29+
})
30+
2231
it('lists directory contents', async () => {
2332
await testFolder.mkdir('subfolder')
2433
await testFolder.write('fileA.txt', 'fileA content')
34+
35+
const listDirectory = new ListDirectory({ path: testFolder.path, maxDepth: 0 })
36+
await listDirectory.validate()
37+
const result = await listDirectory.invoke(process.stdout)
38+
39+
const lines = result.output.content.split('\n')
40+
const hasFileA = lines.some((line: string | string[]) => line.includes('[FILE] ') && line.includes('fileA.txt'))
41+
const hasSubfolder = lines.some(
42+
(line: string | string[]) => line.includes('[DIR] ') && line.includes('subfolder')
43+
)
44+
45+
assert.ok(hasFileA, 'Should list fileA.txt in the directory output')
46+
assert.ok(hasSubfolder, 'Should list the subfolder in the directory output')
47+
})
48+
49+
it('lists directory contents recursively', async () => {
50+
await testFolder.mkdir('subfolder')
51+
await testFolder.write('fileA.txt', 'fileA content')
2552
await testFolder.write(path.join('subfolder', 'fileB.md'), '# fileB')
2653

2754
const listDirectory = new ListDirectory({ path: testFolder.path })
@@ -33,14 +60,16 @@ describe('ListDirectory Tool', () => {
3360
const hasSubfolder = lines.some(
3461
(line: string | string[]) => line.includes('[DIR] ') && line.includes('subfolder')
3562
)
63+
const hasFileB = lines.some((line: string | string[]) => line.includes('[FILE] ') && line.includes('fileB.md'))
3664

3765
assert.ok(hasFileA, 'Should list fileA.txt in the directory output')
3866
assert.ok(hasSubfolder, 'Should list the subfolder in the directory output')
67+
assert.ok(hasFileB, 'Should list fileB.md in the subfolder in the directory output')
3968
})
4069

4170
it('throws error if path does not exist', async () => {
4271
const missingPath = path.join(testFolder.path, 'no_such_file.txt')
43-
const listDirectory = new ListDirectory({ path: missingPath })
72+
const listDirectory = new ListDirectory({ path: missingPath, maxDepth: 0 })
4473

4574
await assert.rejects(
4675
listDirectory.validate(),
@@ -50,7 +79,7 @@ describe('ListDirectory Tool', () => {
5079
})
5180

5281
it('expands ~ path', async () => {
53-
const listDirectory = new ListDirectory({ path: '~' })
82+
const listDirectory = new ListDirectory({ path: '~', maxDepth: 0 })
5483
await listDirectory.validate()
5584
const result = await listDirectory.invoke(process.stdout)
5685

0 commit comments

Comments
 (0)