diff --git a/src/extension/tools/node/getErrorsTool.tsx b/src/extension/tools/node/getErrorsTool.tsx index 93f9920ed6..cdef1f9066 100644 --- a/src/extension/tools/node/getErrorsTool.tsx +++ b/src/extension/tools/node/getErrorsTool.tsx @@ -17,6 +17,7 @@ import { isLocation } from '../../../util/common/types'; import { coalesce } from '../../../util/vs/base/common/arrays'; import { CancellationToken } from '../../../util/vs/base/common/cancellation'; import { Disposable } from '../../../util/vs/base/common/lifecycle'; +import { ResourceSet } from '../../../util/vs/base/common/map'; import { isEqualOrParent } from '../../../util/vs/base/common/resources'; import { URI } from '../../../util/vs/base/common/uri'; import { IInstantiationService } from '../../../util/vs/platform/instantiation/common/instantiation'; @@ -56,8 +57,8 @@ export class GetErrorsTool extends Disposable implements ICopilotTool { - const results: Array<{ uri: URI; diagnostics: vscode.Diagnostic[] }> = []; + public getDiagnostics(paths: { uri: URI; range: Range | undefined }[]): Array<{ uri: URI; diagnostics: vscode.Diagnostic[]; inputUri?: URI }> { + const results: Array<{ uri: URI; diagnostics: vscode.Diagnostic[]; inputUri?: URI }> = []; // for notebooks, we need to find the cell matching the range and get diagnostics for that cell const nonNotebookPaths = paths.filter(p => { @@ -89,11 +90,27 @@ export class GetErrorsTool extends Disposable implements ICopilotTool 0) { const diagnostics = pendingDiagnostics.filter(d => ranges.some(range => d.range.intersection(range))); - results.push({ uri: resource, diagnostics }); + results.push({ uri: resource, diagnostics, inputUri }); } } @@ -129,7 +146,7 @@ export class GetErrorsTool extends Disposable implements ICopilotTool, token: CancellationToken) { const getAll = () => this.languageDiagnosticsService.getAllDiagnostics() - .map(d => ({ uri: d[0], diagnostics: d[1].filter(e => e.severity <= DiagnosticSeverity.Warning) })) + .map(d => ({ uri: d[0], diagnostics: d[1].filter(e => e.severity <= DiagnosticSeverity.Warning), inputUri: undefined })) // filter any documents w/o warnings or errors .filter(d => d.diagnostics.length > 0); @@ -146,14 +163,15 @@ export class GetErrorsTool extends Disposable implements ICopilotTool { + const diagnostics = coalesce(await Promise.all(ds.map((async ({ uri, diagnostics, inputUri }) => { try { const document = await this.workspaceService.openTextDocumentAndSnapshot(uri); checkCancellation(token); return { uri, diagnostics, - context: { document, language: getLanguage(document) } + context: { document, language: getLanguage(document) }, + inputUri }; } catch (e) { this.logService.error(e, 'get_errors failed to open doc with diagnostics'); @@ -169,7 +187,17 @@ export class GetErrorsTool extends Disposable implements ICopilotTool acc + diagnostics.length, 0); - const formattedURIs = this.formatURIs(diagnostics.map(d => d.uri)); + + // For display message, use inputUri if available (indicating file was found via folder input), otherwise use the file uri + // Deduplicate URIs since multiple files may have the same inputUri + const displayUriSet = new ResourceSet(); + for (const d of diagnostics) { + const displayUri = d.inputUri ?? d.uri; + displayUriSet.add(displayUri); + } + + const formattedURIs = this.formatURIs(Array.from(displayUriSet)); + if (options.input.filePaths?.length) { result.toolResultMessage = numDiagnostics === 0 ? new MarkdownString(l10n.t`Checked ${formattedURIs}, no problems found`) : @@ -326,4 +354,4 @@ export class DiagnosticToolOutput extends PromptElement; } -} +} \ No newline at end of file diff --git a/src/extension/tools/node/test/getErrorsTool.spec.tsx b/src/extension/tools/node/test/getErrorsTool.spec.tsx index 071538a370..eee132c848 100644 --- a/src/extension/tools/node/test/getErrorsTool.spec.tsx +++ b/src/extension/tools/node/test/getErrorsTool.spec.tsx @@ -4,6 +4,8 @@ *--------------------------------------------------------------------------------------------*/ import { afterEach, beforeEach, expect, suite, test } from 'vitest'; +import { IFileSystemService } from '../../../../platform/filesystem/common/fileSystemService'; +import { MockFileSystemService } from '../../../../platform/filesystem/node/test/mockFileSystemService'; import { ILanguageDiagnosticsService } from '../../../../platform/languages/common/languageDiagnosticsService'; import { TestLanguageDiagnosticsService } from '../../../../platform/languages/common/testLanguageDiagnosticsService'; import { IPromptPathRepresentationService } from '../../../../platform/prompts/common/promptPathRepresentationService'; @@ -25,9 +27,11 @@ suite('GetErrorsTool - Tool Invocation', () => { let accessor: ITestingServicesAccessor; let collection: TestingServiceCollection; let diagnosticsService: TestLanguageDiagnosticsService; + let fileSystemService: MockFileSystemService; let tool: GetErrorsTool; const workspaceFolder = URI.file('/test/workspace'); + const srcFolder = URI.file('/test/workspace/src'); const tsFile1 = URI.file('/test/workspace/src/file1.ts'); const tsFile2 = URI.file('/test/workspace/src/file2.ts'); const jsFile = URI.file('/test/workspace/lib/file.js'); @@ -50,6 +54,11 @@ suite('GetErrorsTool - Tool Invocation', () => { diagnosticsService = new TestLanguageDiagnosticsService(); collection.define(ILanguageDiagnosticsService, diagnosticsService); + // Set up file system service to mock directories + fileSystemService = new MockFileSystemService(); + fileSystemService.mockDirectory(srcFolder, []); + collection.define(IFileSystemService, fileSystemService); + accessor = collection.createTestingAccessor(); // Create the tool instance @@ -125,8 +134,8 @@ suite('GetErrorsTool - Tool Invocation', () => { // Should find diagnostics for files in the src folder expect(results).toEqual([ - { uri: tsFile1, diagnostics: diagnosticsService.getDiagnostics(tsFile1).filter(d => d.severity <= DiagnosticSeverity.Warning) }, - { uri: tsFile2, diagnostics: diagnosticsService.getDiagnostics(tsFile2).filter(d => d.severity <= DiagnosticSeverity.Warning) } + { uri: tsFile1, diagnostics: diagnosticsService.getDiagnostics(tsFile1).filter(d => d.severity <= DiagnosticSeverity.Warning), inputUri: srcFolder }, + { uri: tsFile2, diagnostics: diagnosticsService.getDiagnostics(tsFile2).filter(d => d.severity <= DiagnosticSeverity.Warning), inputUri: srcFolder } ]); }); @@ -171,8 +180,8 @@ suite('GetErrorsTool - Tool Invocation', () => { // Should only include tsFile1 and tsFile2, not infoHintOnlyFile (which has no Warning/Error) expect(results).toEqual([ - { uri: tsFile1, diagnostics: diagnosticsService.getDiagnostics(tsFile1).filter(d => d.severity <= DiagnosticSeverity.Warning) }, - { uri: tsFile2, diagnostics: diagnosticsService.getDiagnostics(tsFile2).filter(d => d.severity <= DiagnosticSeverity.Warning) } + { uri: tsFile1, diagnostics: diagnosticsService.getDiagnostics(tsFile1).filter(d => d.severity <= DiagnosticSeverity.Warning), inputUri: srcFolder }, + { uri: tsFile2, diagnostics: diagnosticsService.getDiagnostics(tsFile2).filter(d => d.severity <= DiagnosticSeverity.Warning), inputUri: srcFolder } ]); }); diff --git a/src/platform/filesystem/node/test/mockFileSystemService.ts b/src/platform/filesystem/node/test/mockFileSystemService.ts index ec169bd3ff..7e4ee4f44e 100644 --- a/src/platform/filesystem/node/test/mockFileSystemService.ts +++ b/src/platform/filesystem/node/test/mockFileSystemService.ts @@ -74,6 +74,9 @@ export class MockFileSystemService implements IFileSystemService { const mtime = this.mockMtimes.get(uriString) ?? Date.now(); return { type: FileType.File as unknown as FileType, ctime: Date.now() - 1000, mtime, size: contents.length }; } + if (this.mockDirs.has(uriString)) { + return { type: FileType.Directory as unknown as FileType, ctime: Date.now() - 1000, mtime: Date.now(), size: 0 }; + } throw new Error('ENOENT'); } @@ -93,6 +96,18 @@ export class MockFileSystemService implements IFileSystemService { const uriString = uri.toString(); const text = new TextDecoder().decode(content); this.mockFiles.set(uriString, text); + + // add the file to the mock directory listing of its parent directory + const parentUri = uriString.substring(0, uriString.lastIndexOf('/')); + if (this.mockDirs.has(parentUri)) { + const entries = this.mockDirs.get(parentUri)!; + const fileName = uriString.substring(uriString.lastIndexOf('/') + 1); + if (!entries.find(e => e[0] === fileName)) { + entries.push([fileName, FileType.File]); + } + } else { + this.mockDirs.set(parentUri, [[uriString.substring(uriString.lastIndexOf('/') + 1), FileType.File]]); + } } async delete(uri: URI, options?: { recursive?: boolean; useTrash?: boolean }): Promise {