Skip to content

Commit 4a39405

Browse files
committed
chore: merge conflicts again
2 parents b7aa874 + 91bfef8 commit 4a39405

File tree

4 files changed

+369
-6
lines changed

4 files changed

+369
-6
lines changed

server/aws-lsp-codewhisperer/src/language-server/agenticChat/agenticChatController.ts

Lines changed: 13 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,7 @@ import { FsWrite, FsWriteParams, getDiffChanges } from './tools/fsWrite'
102102
import { ExecuteBash, ExecuteBashOutput, ExecuteBashParams } from './tools/executeBash'
103103
import { ExplanatoryParams, InvokeOutput, ToolApprovalException } from './tools/toolShared'
104104
import { ModelServiceException } from './errors'
105+
import { FileSearch, FileSearchParams } from './tools/fileSearch'
105106
import { GrepSearch, SanitizedRipgrepOutput } from './tools/grepSearch'
106107

107108
type ChatHandlers = Omit<
@@ -458,7 +459,7 @@ export class AgenticChatController implements ChatHandlers {
458459
})
459460
}
460461

461-
// Phase 3: Request Executio
462+
// Phase 3: Request Execution
462463
const response = await this.fetchModelResponse(currentRequestInput, i =>
463464
session.generateAssistantResponse(i)
464465
)
@@ -587,6 +588,7 @@ export class AgenticChatController implements ChatHandlers {
587588
switch (toolUse.name) {
588589
case 'fsRead':
589590
case 'listDirectory':
591+
case 'fileSearch':
590592
case 'grepSearch':
591593
case 'fsWrite':
592594
case 'executeBash': {
@@ -595,6 +597,7 @@ export class AgenticChatController implements ChatHandlers {
595597
listDirectory: { Tool: ListDirectory },
596598
fsWrite: { Tool: FsWrite },
597599
executeBash: { Tool: ExecuteBash },
600+
fileSearch: { Tool: FileSearch },
598601
grepSearch: { Tool: GrepSearch },
599602
}
600603

@@ -629,8 +632,8 @@ export class AgenticChatController implements ChatHandlers {
629632
await chatResultStream.writeResultBlock({ messageId: loadingMessageId, type: 'answer' })
630633
this.#features.chat.sendChatUpdate({ tabId, state: { inProgress: true } })
631634

632-
if (['fsRead', 'listDirectory'].includes(toolUse.name)) {
633-
const initialListDirResult = this.#processReadOrList(toolUse, chatResultStream)
635+
if (['fsRead', 'listDirectory', 'fileSearch'].includes(toolUse.name)) {
636+
const initialListDirResult = this.#processReadOrListOrSearch(toolUse, chatResultStream)
634637
if (initialListDirResult) {
635638
await chatResultStream.writeResultBlock(initialListDirResult)
636639
}
@@ -672,6 +675,8 @@ export class AgenticChatController implements ChatHandlers {
672675
switch (toolUse.name) {
673676
case 'fsRead':
674677
case 'listDirectory':
678+
case 'fileSearch':
679+
// no need to write tool result for listDir,fsRead,fileSearch into chat stream
675680
case 'executeBash':
676681
// no need to write tool result for listDir and fsRead into chat stream
677682
// executeBash will stream the output instead of waiting until the end
@@ -937,7 +942,7 @@ export class AgenticChatController implements ChatHandlers {
937942
}
938943
}
939944

940-
#processReadOrList(toolUse: ToolUse, chatResultStream: AgenticChatResultStream): ChatMessage | undefined {
945+
#processReadOrListOrSearch(toolUse: ToolUse, chatResultStream: AgenticChatResultStream): ChatMessage | undefined {
941946
let messageIdToUpdate = toolUse.toolUseId!
942947
const currentId = chatResultStream.getMessageIdToUpdateForTool(toolUse.name!)
943948

@@ -947,7 +952,7 @@ export class AgenticChatController implements ChatHandlers {
947952
chatResultStream.setMessageIdToUpdateForTool(toolUse.name!, messageIdToUpdate)
948953
}
949954

950-
const currentPath = (toolUse.input as unknown as FsReadParams | ListDirectoryParams)?.path
955+
const currentPath = (toolUse.input as unknown as FsReadParams | ListDirectoryParams | FileSearchParams)?.path
951956
if (!currentPath) return
952957
const existingPaths = chatResultStream.getMessageOperation(messageIdToUpdate)?.filePaths || []
953958
// Check if path already exists in the list
@@ -971,7 +976,9 @@ export class AgenticChatController implements ChatHandlers {
971976
title =
972977
toolUse.name === 'fsRead'
973978
? `${itemCount} file${itemCount > 1 ? 's' : ''} read`
974-
: `${itemCount} ${itemCount === 1 ? 'directory' : 'directories'} listed`
979+
: toolUse.name === 'fileSearch'
980+
? `${itemCount} ${itemCount === 1 ? 'directory' : 'directories'} searched`
981+
: `${itemCount} ${itemCount === 1 ? 'directory' : 'directories'} listed`
975982
}
976983
const fileDetails: Record<string, FileDetails> = {}
977984
for (const item of filePathsPushed) {
Lines changed: 211 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,211 @@
1+
import * as assert from 'assert'
2+
import { FileSearch } from './fileSearch'
3+
import { testFolder } from '@aws/lsp-core'
4+
import * as path from 'path'
5+
import * as fs from 'fs/promises'
6+
import { TestFeatures } from '@aws/language-server-runtimes/testing'
7+
import { Features } from '@aws/language-server-runtimes/server-interface/server'
8+
9+
describe('FileSearch Tool', () => {
10+
let tempFolder: testFolder.TestFolder
11+
let testFeatures: TestFeatures
12+
13+
before(async () => {
14+
testFeatures = new TestFeatures()
15+
// @ts-ignore does not require all fs operations to be implemented
16+
testFeatures.workspace.fs = {
17+
exists: path =>
18+
fs
19+
.access(path)
20+
.then(() => true)
21+
.catch(() => false),
22+
readdir: path => fs.readdir(path, { withFileTypes: true }),
23+
} as Features['workspace']['fs']
24+
tempFolder = await testFolder.TestFolder.create()
25+
})
26+
27+
after(async () => {
28+
await tempFolder.delete()
29+
})
30+
31+
it('invalidates empty path', async () => {
32+
const fileSearch = new FileSearch(testFeatures)
33+
await assert.rejects(
34+
fileSearch.validate({ path: '', pattern: '.*' }),
35+
/Path cannot be empty/i,
36+
'Expected an error about empty path'
37+
)
38+
})
39+
40+
it('invalidates invalid regex pattern', async () => {
41+
const fileSearch = new FileSearch(testFeatures)
42+
await assert.rejects(
43+
fileSearch.validate({ path: tempFolder.path, pattern: '[' }),
44+
/Invalid regex pattern/i,
45+
'Expected an error about invalid regex pattern'
46+
)
47+
})
48+
49+
it('invalidates negative maxDepth', async () => {
50+
const fileSearch = new FileSearch(testFeatures)
51+
await assert.rejects(
52+
fileSearch.validate({ path: '~', pattern: '.*', maxDepth: -1 }),
53+
/MaxDepth cannot be negative/i,
54+
'Expected an error about negative maxDepth'
55+
)
56+
})
57+
58+
it('searches for files matching pattern', async () => {
59+
await tempFolder.write('fileA.txt', 'fileA content')
60+
await tempFolder.write('fileB.md', '# fileB content')
61+
await tempFolder.write('fileC.js', 'console.log("fileC");')
62+
63+
const fileSearch = new FileSearch(testFeatures)
64+
const result = await fileSearch.invoke({
65+
path: tempFolder.path,
66+
pattern: '\\.txt$',
67+
maxDepth: 0,
68+
})
69+
70+
assert.strictEqual(result.output.kind, 'text')
71+
const lines = result.output.content.split('\n')
72+
const hasFileA = lines.some(line => line.includes('[F] ') && line.includes('fileA.txt'))
73+
const hasFileB = lines.some(line => line.includes('[F] ') && line.includes('fileB.md'))
74+
75+
assert.ok(hasFileA, 'Should find fileA.txt matching the pattern')
76+
assert.ok(!hasFileB, 'Should not find fileB.md as it does not match the pattern')
77+
})
78+
79+
it('searches recursively in subdirectories', async () => {
80+
const subfolder = await tempFolder.nest('subfolder')
81+
await tempFolder.write('fileA.txt', 'fileA content')
82+
await subfolder.write('fileB.txt', 'fileB content')
83+
await subfolder.write('fileC.md', '# fileC content')
84+
85+
const fileSearch = new FileSearch(testFeatures)
86+
const result = await fileSearch.invoke({
87+
path: tempFolder.path,
88+
pattern: '\\.txt$',
89+
})
90+
91+
assert.strictEqual(result.output.kind, 'text')
92+
const lines = result.output.content.split('\n')
93+
const hasFileA = lines.some(line => line.includes('[F] ') && line.includes('fileA.txt'))
94+
const hasFileB = lines.some(line => line.includes('[F] ') && line.includes('fileB.txt'))
95+
const hasFileC = lines.some(line => line.includes('[F] ') && line.includes('fileC.md'))
96+
97+
assert.ok(hasFileA, 'Should find fileA.txt in root directory')
98+
assert.ok(hasFileB, 'Should find fileB.txt in subfolder')
99+
assert.ok(!hasFileC, 'Should not find fileC.md as it does not match the pattern')
100+
})
101+
102+
it('respects maxDepth parameter', async () => {
103+
const subfolder1 = await tempFolder.nest('subfolder1')
104+
const subfolder2 = await subfolder1.nest('subfolder2')
105+
106+
await tempFolder.write('root.txt', 'root content')
107+
await subfolder1.write('level1.txt', 'level1 content')
108+
await subfolder2.write('level2.txt', 'level2 content')
109+
110+
const fileSearch = new FileSearch(testFeatures)
111+
const result = await fileSearch.invoke({
112+
path: tempFolder.path,
113+
pattern: '\\.txt$',
114+
maxDepth: 1,
115+
})
116+
117+
assert.strictEqual(result.output.kind, 'text')
118+
const lines = result.output.content.split('\n')
119+
const hasRootFile = lines.some(line => line.includes('[F] ') && line.includes('root.txt'))
120+
const hasLevel1File = lines.some(line => line.includes('[F] ') && line.includes('level1.txt'))
121+
const hasLevel2File = lines.some(line => line.includes('[F] ') && line.includes('level2.txt'))
122+
123+
assert.ok(hasRootFile, 'Should find root.txt in root directory')
124+
assert.ok(hasLevel1File, 'Should find level1.txt in subfolder1')
125+
assert.ok(!hasLevel2File, 'Should not find level2.txt as it exceeds maxDepth')
126+
})
127+
128+
it('performs case-insensitive search by default', async () => {
129+
await tempFolder.write('FileUpper.txt', 'upper case filename')
130+
await tempFolder.write('fileLower.txt', 'lower case filename')
131+
132+
const fileSearch = new FileSearch(testFeatures)
133+
const result = await fileSearch.invoke({
134+
path: tempFolder.path,
135+
pattern: 'file',
136+
maxDepth: 0,
137+
})
138+
139+
assert.strictEqual(result.output.kind, 'text')
140+
const lines = result.output.content.split('\n')
141+
const hasUpperFile = lines.some(line => line.includes('[F] ') && line.includes('FileUpper.txt'))
142+
const hasLowerFile = lines.some(line => line.includes('[F] ') && line.includes('fileLower.txt'))
143+
144+
assert.ok(hasUpperFile, 'Should find FileUpper.txt with case-insensitive search')
145+
assert.ok(hasLowerFile, 'Should find fileLower.txt with case-insensitive search')
146+
})
147+
148+
it('performs case-sensitive search when specified', async () => {
149+
await tempFolder.write('FileUpper.txt', 'upper case filename')
150+
await tempFolder.write('fileLower.txt', 'lower case filename')
151+
152+
const fileSearch = new FileSearch(testFeatures)
153+
const result = await fileSearch.invoke({
154+
path: tempFolder.path,
155+
pattern: 'file',
156+
maxDepth: 0,
157+
caseSensitive: true,
158+
})
159+
160+
assert.strictEqual(result.output.kind, 'text')
161+
const lines = result.output.content.split('\n')
162+
const hasUpperFile = lines.some(line => line.includes('[F] ') && line.includes('FileUpper.txt'))
163+
const hasLowerFile = lines.some(line => line.includes('[F] ') && line.includes('fileLower.txt'))
164+
165+
assert.ok(!hasUpperFile, 'Should not find FileUpper.txt with case-sensitive search')
166+
assert.ok(hasLowerFile, 'Should find fileLower.txt with case-sensitive search')
167+
})
168+
169+
it('ignores excluded directories', async () => {
170+
const nodeModules = await tempFolder.nest('node_modules')
171+
await tempFolder.write('regular.txt', 'regular content')
172+
await nodeModules.write('excluded.txt', 'excluded content')
173+
174+
const fileSearch = new FileSearch(testFeatures)
175+
const result = await fileSearch.invoke({
176+
path: tempFolder.path,
177+
pattern: '\\.txt$',
178+
})
179+
180+
assert.strictEqual(result.output.kind, 'text')
181+
const lines = result.output.content.split('\n')
182+
const hasRegularFile = lines.some(line => line.includes('[F] ') && line.includes('regular.txt'))
183+
const hasExcludedFile = lines.some(line => line.includes('[F] ') && line.includes('excluded.txt'))
184+
185+
assert.ok(hasRegularFile, 'Should find regular.txt in root directory')
186+
assert.ok(!hasExcludedFile, 'Should not find excluded.txt in node_modules directory')
187+
})
188+
189+
it('throws error if path does not exist', async () => {
190+
const missingPath = path.join(tempFolder.path, 'no_such_directory')
191+
const fileSearch = new FileSearch(testFeatures)
192+
193+
await assert.rejects(
194+
fileSearch.invoke({ path: missingPath, pattern: '.*' }),
195+
/Failed to search directory/i,
196+
'Expected an error about non-existent path'
197+
)
198+
})
199+
200+
it('expands ~ path', async () => {
201+
const fileSearch = new FileSearch(testFeatures)
202+
const result = await fileSearch.invoke({
203+
path: '~',
204+
pattern: '.*',
205+
maxDepth: 0,
206+
})
207+
208+
assert.strictEqual(result.output.kind, 'text')
209+
assert.ok(result.output.content.length > 0)
210+
})
211+
})

0 commit comments

Comments
 (0)