From 46cbcfa500bc3ee2e5f785a09026e099fe7854db Mon Sep 17 00:00:00 2001 From: Bryan Chen Date: Fri, 7 Nov 2025 13:20:55 -0800 Subject: [PATCH 1/4] getErrorsTool display folder instead of individual child files --- src/extension/tools/node/getErrorsTool.tsx | 54 ++++++++++++++----- .../tools/node/test/getErrorsTool.spec.tsx | 17 ++++-- .../node/test/mockFileSystemService.ts | 3 ++ 3 files changed, 56 insertions(+), 18 deletions(-) diff --git a/src/extension/tools/node/getErrorsTool.tsx b/src/extension/tools/node/getErrorsTool.tsx index 93f9920ed6..399ba5089f 100644 --- a/src/extension/tools/node/getErrorsTool.tsx +++ b/src/extension/tools/node/getErrorsTool.tsx @@ -56,8 +56,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 +89,25 @@ 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 as URI | 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 Set(); + for (const d of diagnostics) { + const displayUri = d.inputUri ?? d.uri; + displayUriSet.add(displayUri.toString()); + } + const displayUris = Array.from(displayUriSet).map(uriStr => URI.parse(uriStr)); + const formattedURIs = this.formatURIs(displayUris); + if (options.input.filePaths?.length) { result.toolResultMessage = numDiagnostics === 0 ? new MarkdownString(l10n.t`Checked ${formattedURIs}, no problems found`) : @@ -276,11 +304,9 @@ export class GetErrorsTool extends Disposable implements ICopilotTool { +}export class DiagnosticToolOutput extends PromptElement { constructor( props: PromptElementProps, @IPromptPathRepresentationService private readonly promptPathRepresentationService: IPromptPathRepresentationService, @@ -326,4 +352,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..9f926ef960 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'); } From a433f2f33c4025fa059db8385375c2d0bd8930b5 Mon Sep 17 00:00:00 2001 From: Bryan Chen Date: Fri, 7 Nov 2025 13:49:14 -0800 Subject: [PATCH 2/4] address copilot comments. --- src/extension/tools/node/getErrorsTool.tsx | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/src/extension/tools/node/getErrorsTool.tsx b/src/extension/tools/node/getErrorsTool.tsx index 399ba5089f..7090615a40 100644 --- a/src/extension/tools/node/getErrorsTool.tsx +++ b/src/extension/tools/node/getErrorsTool.tsx @@ -104,8 +104,10 @@ 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), inputUri: undefined as URI | undefined })) + .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); @@ -306,7 +305,9 @@ ToolRegistry.registerTool(GetErrorsTool); interface IDiagnosticToolOutputProps extends BasePromptElementProps { diagnosticsGroups: { context: DiagnosticContext; uri: URI; diagnostics: vscode.Diagnostic[]; inputUri?: URI }[]; maxDiagnostics?: number; -}export class DiagnosticToolOutput extends PromptElement { +} + +export class DiagnosticToolOutput extends PromptElement { constructor( props: PromptElementProps, @IPromptPathRepresentationService private readonly promptPathRepresentationService: IPromptPathRepresentationService, From f9bbf27f615d025fd0f3b3360cbb9e6349d25c80 Mon Sep 17 00:00:00 2001 From: Bryan Chen Date: Fri, 7 Nov 2025 14:26:34 -0800 Subject: [PATCH 3/4] Fix test failure --- .../filesystem/node/test/mockFileSystemService.ts | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/src/platform/filesystem/node/test/mockFileSystemService.ts b/src/platform/filesystem/node/test/mockFileSystemService.ts index 9f926ef960..7e4ee4f44e 100644 --- a/src/platform/filesystem/node/test/mockFileSystemService.ts +++ b/src/platform/filesystem/node/test/mockFileSystemService.ts @@ -96,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 { From f7432dbc3ef7a1fc897a8da7939f3f893aa287ff Mon Sep 17 00:00:00 2001 From: Bryan Chen Date: Mon, 10 Nov 2025 17:18:05 -0800 Subject: [PATCH 4/4] updated per comments --- src/extension/tools/node/getErrorsTool.tsx | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/src/extension/tools/node/getErrorsTool.tsx b/src/extension/tools/node/getErrorsTool.tsx index 7090615a40..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'; @@ -189,13 +190,13 @@ export class GetErrorsTool extends Disposable implements ICopilotTool(); + const displayUriSet = new ResourceSet(); for (const d of diagnostics) { const displayUri = d.inputUri ?? d.uri; - displayUriSet.add(displayUri.toString()); + displayUriSet.add(displayUri); } - const displayUris = Array.from(displayUriSet).map(uriStr => URI.parse(uriStr)); - const formattedURIs = this.formatURIs(displayUris); + + const formattedURIs = this.formatURIs(Array.from(displayUriSet)); if (options.input.filePaths?.length) { result.toolResultMessage = numDiagnostics === 0 ? @@ -303,7 +304,7 @@ export class GetErrorsTool extends Disposable implements ICopilotTool