+
Did you know?
${this.getRandomTip()}
diff --git a/integration-tests/q-agentic-chat-server/package.json b/integration-tests/q-agentic-chat-server/package.json
index af4c76ba96..2e14a1232e 100644
--- a/integration-tests/q-agentic-chat-server/package.json
+++ b/integration-tests/q-agentic-chat-server/package.json
@@ -4,7 +4,7 @@
"description": "Integration tests for Q Agentic Chat Server",
"main": "out/index.js",
"scripts": {
- "clean": "rm -rf out/ node_modules/ tsconfig.tsbuildinfo .tsbuildinfo",
+ "clean": "rimraf out/ node_modules/ tsconfig.tsbuildinfo .tsbuildinfo",
"compile": "tsc --build && cp -r src/tests/testFixture out/tests/",
"test-integ": "npm run compile && mocha --timeout 30000 \"./out/**/*.test.js\" --retries 2"
},
@@ -22,6 +22,7 @@
"jose": "^5.10.0",
"json-rpc-2.0": "^1.7.1",
"mocha": "^11.0.1",
+ "rimraf": "^3.0.2",
"typescript": "^5.0.0",
"yauzl-promise": "^4.0.0"
}
diff --git a/integration-tests/q-agentic-chat-server/src/tests/agenticChatInteg.test.ts b/integration-tests/q-agentic-chat-server/src/tests/agenticChatInteg.test.ts
index 6e0d0dcede..897ee02071 100644
--- a/integration-tests/q-agentic-chat-server/src/tests/agenticChatInteg.test.ts
+++ b/integration-tests/q-agentic-chat-server/src/tests/agenticChatInteg.test.ts
@@ -8,7 +8,7 @@ import { JSONRPCEndpoint, LspClient } from './lspClient'
import { pathToFileURL } from 'url'
import * as crypto from 'crypto'
import { EncryptionInitialization } from '@aws/lsp-core'
-import { authenticateServer, decryptObjectWithKey, encryptObjectWithKey } from './testUtils'
+import { authenticateServer, decryptObjectWithKey, encryptObjectWithKey, normalizePath } from './testUtils'
import { ChatParams, ChatResult } from '@aws/language-server-runtimes/protocol'
import * as fs from 'fs'
@@ -172,7 +172,9 @@ describe('Q Agentic Chat Server Integration Tests', async () => {
msg => msg.type === 'tool' && msg.fileList?.rootFolderTitle === '1 file read'
)
expect(fsReadMessage).to.exist
- expect(fsReadMessage?.fileList?.filePaths).to.include.members([path.join(rootPath, 'test.py')])
+ const expectedPath = path.join(rootPath, 'test.py')
+ const actualPaths = fsReadMessage?.fileList?.filePaths?.map(normalizePath) || []
+ expect(actualPaths).to.include.members([normalizePath(expectedPath)])
expect(fsReadMessage?.messageId?.startsWith('tooluse_')).to.be.true
})
@@ -192,7 +194,8 @@ describe('Q Agentic Chat Server Integration Tests', async () => {
msg => msg.type === 'tool' && msg.fileList?.rootFolderTitle === '1 directory listed'
)
expect(listDirectoryMessage).to.exist
- expect(listDirectoryMessage?.fileList?.filePaths).to.include.members([rootPath])
+ const actualPaths = listDirectoryMessage?.fileList?.filePaths?.map(normalizePath) || []
+ expect(actualPaths).to.include.members([normalizePath(rootPath)])
expect(listDirectoryMessage?.messageId?.startsWith('tooluse_')).to.be.true
})
@@ -216,12 +219,17 @@ describe('Q Agentic Chat Server Integration Tests', async () => {
expect(executeBashMessage?.body).to.include('test.ts')
})
- it('waits for user acceptance when executing mutable bash commands', async () => {
+ it('waits for user acceptance when executing mutable bash commands', async function () {
+ const command =
+ process.platform === 'win32'
+ ? 'echo %date% > timestamp.txt && echo "Timestamp saved"'
+ : 'date > timestamp.txt && echo "Timestamp saved"'
+
const encryptedMessage = await encryptObjectWithKey
(
{
tabId,
prompt: {
- prompt: `Run this command using the executeBash tool: \`date > timestamp.txt && echo "Timestamp saved"\``,
+ prompt: `Run this command using the executeBash tool: \`${command}\``,
},
},
encryptionKey
@@ -367,6 +375,7 @@ describe('Q Agentic Chat Server Integration Tests', async () => {
)
expect(fileSearchMessage).to.exist
expect(fileSearchMessage?.messageId?.startsWith('tooluse_')).to.be.true
- expect(fileSearchMessage?.fileList?.filePaths).to.include.members([rootPath])
+ const actualPaths = fileSearchMessage?.fileList?.filePaths?.map(normalizePath) || []
+ expect(actualPaths).to.include.members([normalizePath(rootPath)])
})
})
diff --git a/integration-tests/q-agentic-chat-server/src/tests/testUtils.ts b/integration-tests/q-agentic-chat-server/src/tests/testUtils.ts
index ae7e5b3905..cd2af069d2 100644
--- a/integration-tests/q-agentic-chat-server/src/tests/testUtils.ts
+++ b/integration-tests/q-agentic-chat-server/src/tests/testUtils.ts
@@ -1,5 +1,6 @@
import { UpdateCredentialsParams } from '@aws/language-server-runtimes/protocol'
import * as jose from 'jose'
+import * as path from 'path'
import { JSONRPCEndpoint } from './lspClient'
/**
@@ -72,3 +73,12 @@ async function setProfile(endpoint: JSONRPCEndpoint, profileArn: string): Promis
settings: { profileArn },
})
}
+
+/**
+ * Normalize paths for cross-platform comparison
+ * @param filePath - The file path to normalize
+ * @returns Normalized path with consistent casing
+ */
+export function normalizePath(filePath: string): string {
+ return path.resolve(filePath).toLowerCase()
+}
diff --git a/package-lock.json b/package-lock.json
index 148726033a..a1ef22659a 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -340,6 +340,7 @@
"jose": "^5.10.0",
"json-rpc-2.0": "^1.7.1",
"mocha": "^11.0.1",
+ "rimraf": "^3.0.2",
"typescript": "^5.0.0",
"yauzl-promise": "^4.0.0"
}
diff --git a/server/aws-lsp-codewhisperer/src/language-server/agenticChat/agenticChatController.ts b/server/aws-lsp-codewhisperer/src/language-server/agenticChat/agenticChatController.ts
index 876a199e90..5be41c165c 100644
--- a/server/aws-lsp-codewhisperer/src/language-server/agenticChat/agenticChatController.ts
+++ b/server/aws-lsp-codewhisperer/src/language-server/agenticChat/agenticChatController.ts
@@ -201,7 +201,10 @@ import { URI } from 'vscode-uri'
import { CommandCategory } from './tools/executeBash'
import { UserWrittenCodeTracker } from '../../shared/userWrittenCodeTracker'
import { CodeReview } from './tools/qCodeAnalysis/codeReview'
-import { FINDINGS_MESSAGE_SUFFIX } from './tools/qCodeAnalysis/codeReviewConstants'
+import {
+ CODE_REVIEW_FINDINGS_MESSAGE_SUFFIX,
+ DISPLAY_FINDINGS_MESSAGE_SUFFIX,
+} from './tools/qCodeAnalysis/codeReviewConstants'
import { McpEventHandler } from './tools/mcp/mcpEventHandler'
import { enabledMCP, createNamespacedToolName } from './tools/mcp/mcpUtils'
import { McpManager } from './tools/mcp/mcpManager'
@@ -225,6 +228,7 @@ import { getLatestAvailableModel } from './utils/agenticChatControllerHelper'
import { ActiveUserTracker } from '../../shared/activeUserTracker'
import { UserContext } from '../../client/token/codewhispererbearertokenclient'
import { CodeWhispererServiceToken } from '../../shared/codeWhispererService'
+import { DisplayFindings } from './tools/qCodeAnalysis/displayFindings'
type ChatHandlers = Omit<
LspHandlers,
@@ -1764,7 +1768,8 @@ export class AgenticChatController implements ChatHandlers {
break
}
case CodeReview.toolName:
- // no need to write tool message for code review
+ case DisplayFindings.toolName:
+ // no need to write tool message for CodeReview or DisplayFindings
break
// — DEFAULT ⇒ Only MCP tools, but can also handle generic tool execution messages
default:
@@ -1940,11 +1945,27 @@ export class AgenticChatController implements ChatHandlers {
) {
await chatResultStream.writeResultBlock({
type: 'tool',
- messageId: toolUse.toolUseId + FINDINGS_MESSAGE_SUFFIX,
+ messageId: toolUse.toolUseId + CODE_REVIEW_FINDINGS_MESSAGE_SUFFIX,
body: (codeReviewResult.output.content as any).findingsByFile,
})
}
break
+ case DisplayFindings.toolName:
+ // no need to write tool result for code review, this is handled by model via chat
+ // Push result in message so that it is picked by IDE plugin to show in issues panel
+ const displayFindingsResult = result as InvokeOutput
+ if (
+ displayFindingsResult?.output?.kind === 'json' &&
+ displayFindingsResult.output.success &&
+ displayFindingsResult.output.content !== undefined
+ ) {
+ await chatResultStream.writeResultBlock({
+ type: 'tool',
+ messageId: toolUse.toolUseId + DISPLAY_FINDINGS_MESSAGE_SUFFIX,
+ body: JSON.stringify(displayFindingsResult.output.content),
+ })
+ }
+ break
// — DEFAULT ⇒ MCP tools
default:
await this.#handleMcpToolResult(toolUse, result, session, chatResultStream)
diff --git a/server/aws-lsp-codewhisperer/src/language-server/agenticChat/tools/mcp/mcpEventHandler.ts b/server/aws-lsp-codewhisperer/src/language-server/agenticChat/tools/mcp/mcpEventHandler.ts
index b4ca871dc6..22befec0cd 100644
--- a/server/aws-lsp-codewhisperer/src/language-server/agenticChat/tools/mcp/mcpEventHandler.ts
+++ b/server/aws-lsp-codewhisperer/src/language-server/agenticChat/tools/mcp/mcpEventHandler.ts
@@ -1145,14 +1145,17 @@ export class McpEventHandler {
try {
// Skip server config check for Built-in server
+ const serverConfig = McpManager.instance.getAllServerConfigs().get(serverName)
if (serverName !== 'Built-in') {
- const serverConfig = McpManager.instance.getAllServerConfigs().get(serverName)
if (!serverConfig) {
throw new Error(`Server '${serverName}' not found`)
}
}
- const mcpServerPermission = await this.#processPermissionUpdates(updatedPermissionConfig)
+ const mcpServerPermission = await this.#processPermissionUpdates(
+ updatedPermissionConfig,
+ serverConfig?.__configPath__
+ )
// Store the permission config instead of applying it immediately
this.#pendingPermissionConfig = {
@@ -1347,10 +1350,7 @@ export class McpEventHandler {
/**
* Processes permission updates from the UI
*/
- async #processPermissionUpdates(updatedPermissionConfig: any) {
- // Get the appropriate agent path
- const agentPath = await this.#getAgentPath()
-
+ async #processPermissionUpdates(updatedPermissionConfig: any, agentPath: string | undefined) {
const perm: MCPServerPermission = {
enabled: true,
toolPerms: {},
diff --git a/server/aws-lsp-codewhisperer/src/language-server/agenticChat/tools/qCodeAnalysis/codeReviewConstants.ts b/server/aws-lsp-codewhisperer/src/language-server/agenticChat/tools/qCodeAnalysis/codeReviewConstants.ts
index e960fd0596..bc7070ab1f 100644
--- a/server/aws-lsp-codewhisperer/src/language-server/agenticChat/tools/qCodeAnalysis/codeReviewConstants.ts
+++ b/server/aws-lsp-codewhisperer/src/language-server/agenticChat/tools/qCodeAnalysis/codeReviewConstants.ts
@@ -211,6 +211,7 @@ export const SKIP_DIRECTORIES = [
'temp',
]
-export const FINDINGS_MESSAGE_SUFFIX = '_codeReviewFindings'
+export const CODE_REVIEW_FINDINGS_MESSAGE_SUFFIX = '_codeReviewFindings'
+export const DISPLAY_FINDINGS_MESSAGE_SUFFIX = '_displayFindings'
export const CODE_REVIEW_METRICS_PARENT_NAME = 'amazonq_codeReviewTool'
diff --git a/server/aws-lsp-codewhisperer/src/language-server/agenticChat/tools/qCodeAnalysis/codeReviewUtils.ts b/server/aws-lsp-codewhisperer/src/language-server/agenticChat/tools/qCodeAnalysis/codeReviewUtils.ts
index 1b94770746..5b6d562f7d 100644
--- a/server/aws-lsp-codewhisperer/src/language-server/agenticChat/tools/qCodeAnalysis/codeReviewUtils.ts
+++ b/server/aws-lsp-codewhisperer/src/language-server/agenticChat/tools/qCodeAnalysis/codeReviewUtils.ts
@@ -278,6 +278,18 @@ export class CodeReviewUtils {
return qCapabilities?.codeReviewInChat || false
}
+ /**
+ * Check if storing display findings in the Code Issues panel is enabled.
+ * @param params Initialize parameters from client
+ * @returns True if display findings is enabled, false otherwise
+ */
+ public static isDisplayFindingsEnabled(params: InitializeParams | undefined): boolean {
+ const qCapabilities = params?.initializationOptions?.aws?.awsClientCapabilities?.q as
+ | QClientCapabilities
+ | undefined
+ return qCapabilities?.displayFindings || false
+ }
+
/**
* Converts a Windows absolute file path to Unix format and removes the drive letter
* @param windowsPath The Windows path to convert
diff --git a/server/aws-lsp-codewhisperer/src/language-server/agenticChat/tools/qCodeAnalysis/displayFindings.test.ts b/server/aws-lsp-codewhisperer/src/language-server/agenticChat/tools/qCodeAnalysis/displayFindings.test.ts
new file mode 100644
index 0000000000..e541a76026
--- /dev/null
+++ b/server/aws-lsp-codewhisperer/src/language-server/agenticChat/tools/qCodeAnalysis/displayFindings.test.ts
@@ -0,0 +1,394 @@
+/*!
+ * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import { DisplayFindings } from './displayFindings'
+import { DISPLAY_FINDINGS_TOOL_NAME } from './displayFindingsConstants'
+import * as sinon from 'sinon'
+import * as path from 'path'
+import { expect } from 'chai'
+import { CancellationError } from '@aws/lsp-core'
+import { CodeReviewFinding } from './codeReviewTypes'
+import { Features } from '@aws/language-server-runtimes/server-interface/server'
+
+describe('DisplayFindings', () => {
+ let sandbox: sinon.SinonSandbox
+ let displayFindings: DisplayFindings
+ let mockFeatures: Pick & Partial
+ let mockCancellationToken: { isCancellationRequested: boolean }
+ let mockWritableStream: { getWriter: sinon.SinonStub }
+ let mockWriter: {
+ write: sinon.SinonStub
+ close: sinon.SinonStub
+ releaseLock: sinon.SinonStub
+ }
+
+ let CODE_REVIEW_FINDING_1: CodeReviewFinding
+
+ let CODE_REVIEW_FINDING_2: CodeReviewFinding
+
+ let INPUT_FINDING_1: {
+ filePath: string
+ startLine: string
+ endLine: string
+ title: string
+ description: string
+ severity: string
+ language: string
+ }
+
+ let INPUT_FINDING_2: {
+ filePath: string
+ startLine: string
+ endLine: string
+ title: string
+ description: string
+ severity: string
+ language: string
+ }
+
+ beforeEach(() => {
+ sandbox = sinon.createSandbox()
+
+ mockWriter = {
+ write: sandbox.stub().resolves(),
+ close: sandbox.stub().resolves(),
+ releaseLock: sandbox.stub(),
+ }
+
+ mockWritableStream = {
+ getWriter: sandbox.stub().returns(mockWriter),
+ }
+
+ mockCancellationToken = {
+ isCancellationRequested: false,
+ }
+
+ mockFeatures = {
+ logging: {
+ info: sandbox.stub(),
+ warn: sandbox.stub(),
+ error: sandbox.stub(),
+ debug: sandbox.stub(),
+ log: sandbox.stub(),
+ },
+ telemetry: {
+ emitMetric: sandbox.stub(),
+ onClientTelemetry: sandbox.stub(),
+ },
+ workspace: {
+ getTextDocument: sandbox.stub(),
+ getAllTextDocuments: sandbox.stub(),
+ getWorkspaceFolder: sandbox.stub(),
+ getAllWorkspaceFolders: sandbox.stub(),
+ fs: {
+ copyFile: sandbox.stub(),
+ exists: sandbox.stub(),
+ getFileSize: sandbox.stub(),
+ getServerDataDirPath: sandbox.stub(),
+ getTempDirPath: sandbox.stub(),
+ getUserHomeDir: sandbox.stub(),
+ readdir: sandbox.stub(),
+ readFile: sandbox.stub(),
+ isFile: sandbox.stub(),
+ rm: sandbox.stub(),
+ writeFile: sandbox.stub(),
+ appendFile: sandbox.stub(),
+ mkdir: sandbox.stub(),
+ readFileSync: sandbox.stub(),
+ },
+ },
+ }
+
+ displayFindings = new DisplayFindings(mockFeatures)
+
+ CODE_REVIEW_FINDING_1 = {
+ filePath: '/test/file1.js',
+ startLine: 10,
+ endLine: 15,
+ title: 'Issue 1',
+ comment: 'Description 1',
+ description: { text: 'Description 1', markdown: 'Description 1' },
+ severity: 'High',
+ language: 'javascript',
+ detectorName: 'DisplayFindings',
+ detectorId: '',
+ findingId: '',
+ relatedVulnerabilities: [],
+ recommendation: { text: '' },
+ suggestedFixes: [],
+ scanJobId: '',
+ autoDetected: false,
+ findingContext: undefined,
+ }
+
+ CODE_REVIEW_FINDING_2 = {
+ filePath: '/test/file2.py',
+ startLine: 5,
+ endLine: 10,
+ title: 'Issue 2',
+ comment: 'Description 2',
+ description: { text: 'Description 2', markdown: 'Description 2' },
+ severity: 'Low',
+ language: 'python',
+ detectorName: 'DisplayFindings',
+ detectorId: '',
+ findingId: '',
+ relatedVulnerabilities: [],
+ recommendation: { text: '' },
+ suggestedFixes: [],
+ scanJobId: '',
+ autoDetected: false,
+ findingContext: undefined,
+ }
+
+ INPUT_FINDING_1 = {
+ filePath: '/test/file1.js',
+ startLine: '10',
+ endLine: '15',
+ title: 'Issue 1',
+ description: 'Description 1',
+ severity: 'High',
+ language: 'javascript',
+ }
+
+ INPUT_FINDING_2 = {
+ filePath: '/test/file2.py',
+ startLine: '5',
+ endLine: '10',
+ title: 'Issue 2',
+ description: 'Description 2',
+ severity: 'Low',
+ language: 'python',
+ }
+ })
+
+ afterEach(() => {
+ sandbox.restore()
+ })
+
+ describe('static properties', () => {
+ it('should have correct tool name', () => {
+ expect(DisplayFindings.toolName).to.equal(DISPLAY_FINDINGS_TOOL_NAME)
+ })
+
+ it('should have tool description', () => {
+ expect(DisplayFindings.toolDescription).to.be.a('string')
+ })
+
+ it('should have input schema', () => {
+ expect(DisplayFindings.inputSchema).to.be.an('object')
+ })
+ })
+
+ describe('execute', () => {
+ let context: any
+ let validInput: any
+
+ beforeEach(() => {
+ context = {
+ cancellationToken: mockCancellationToken,
+ writableStream: mockWritableStream,
+ }
+
+ validInput = {
+ findings: [INPUT_FINDING_1],
+ }
+ })
+
+ it('should execute successfully with valid input', async () => {
+ const result = await displayFindings.execute(validInput, context)
+
+ expect(result.output.success).to.be.true
+ expect(result.output.kind).to.equal('json')
+ expect(result.output.content).to.be.an('array')
+ expect(result.output.content).to.have.length(1)
+ expect((result.output.content as any)[0].filePath).to.equal(path.normalize('/test/file1.js'))
+ expect((result.output.content as any)[0].issues).to.have.length(1)
+ })
+
+ it('should handle multiple findings for same file', async () => {
+ INPUT_FINDING_2.filePath = '/test/file1.js'
+ const inputWithMultipleFindings = {
+ findings: [INPUT_FINDING_1, INPUT_FINDING_2],
+ }
+
+ const result = await displayFindings.execute(inputWithMultipleFindings, context)
+
+ expect(result.output.success).to.be.true
+ expect(result.output.content).to.have.length(1)
+ expect((result.output.content as any)[0].issues).to.have.length(2)
+ })
+
+ it('should handle findings for different files', async () => {
+ const inputWithDifferentFiles = {
+ findings: [INPUT_FINDING_1, INPUT_FINDING_2],
+ }
+
+ const result = await displayFindings.execute(inputWithDifferentFiles, context)
+
+ expect(result.output.success).to.be.true
+ expect(result.output.content).to.have.length(2)
+ expect((result.output.content as any)[0].issues).to.have.length(1)
+ expect((result.output.content as any)[1].issues).to.have.length(1)
+ })
+
+ it('should handle empty findings array', async () => {
+ const emptyInput = { findings: [] }
+
+ const result = await displayFindings.execute(emptyInput, context)
+
+ expect(result.output.success).to.be.true
+ expect(result.output.content).to.be.an('array')
+ expect(result.output.content).to.have.length(0)
+ })
+
+ it('should handle invalid input schema', async () => {
+ const invalidInput = {
+ findings: [
+ {
+ filePath: '/test/file.js',
+ // Missing required fields
+ },
+ ],
+ }
+
+ try {
+ await displayFindings.execute(invalidInput, context)
+ expect.fail('Expected validation error')
+ } catch (error) {
+ expect(error).to.be.instanceOf(Error)
+ }
+ })
+
+ it('should handle cancellation', async () => {
+ mockCancellationToken.isCancellationRequested = true
+
+ try {
+ await displayFindings.execute(validInput, context)
+ expect.fail('Expected cancellation error')
+ } catch (error) {
+ expect(error).to.be.instanceOf(CancellationError)
+ }
+ })
+
+ it('should handle unexpected errors gracefully', async () => {
+ // Make validateInputAndSetup throw an error
+ sandbox.stub(displayFindings as any, 'validateInputAndSetup').rejects(new Error('Unexpected error'))
+
+ try {
+ await displayFindings.execute(validInput, context)
+ expect.fail('Expected error to be thrown')
+ } catch (error: any) {
+ expect(error.message).to.equal('Unexpected error')
+ }
+ })
+ })
+
+ describe('validateInputAndSetup', () => {
+ it('should validate and setup correctly', async () => {
+ const input = {
+ findings: [INPUT_FINDING_1],
+ }
+
+ const context = {
+ cancellationToken: mockCancellationToken,
+ writableStream: mockWritableStream,
+ }
+
+ const result = await (displayFindings as any).validateInputAndSetup(input, context)
+
+ expect(result).to.be.an('array')
+ expect(result).to.have.length(1)
+ expect(result[0].filePath).to.equal('/test/file1.js')
+ })
+ })
+
+ describe('mapToCodeReviewFinding', () => {
+ it('should map DisplayFinding to CodeReviewFinding correctly', () => {
+ const displayFinding = {
+ filePath: '/test/file.js',
+ startLine: '10',
+ endLine: '15',
+ title: 'Test Issue',
+ description: 'Test description',
+ severity: 'High',
+ language: 'javascript',
+ suggestedFixes: ['Fix suggestion'],
+ }
+
+ const result = (displayFindings as any).mapToCodeReviewFinding(displayFinding)
+
+ expect(result.filePath).to.equal('/test/file.js')
+ expect(result.startLine).to.equal(10)
+ expect(result.endLine).to.equal(15)
+ expect(result.title).to.equal('Test Issue')
+ expect(result.comment).to.equal('Test description')
+ expect(result.severity).to.equal('High')
+ expect(result.language).to.equal('javascript')
+ expect(result.suggestedFixes).to.deep.equal(['Fix suggestion'])
+ expect(result.detectorName).to.equal('DisplayFindings')
+ expect(result.autoDetected).to.be.false
+ })
+
+ it('should handle missing suggestedFixes', () => {
+ const displayFinding = {
+ filePath: '/test/file.js',
+ startLine: '10',
+ endLine: '15',
+ title: 'Test Issue',
+ description: 'Test description',
+ severity: 'High',
+ language: 'javascript',
+ }
+
+ const result = (displayFindings as any).mapToCodeReviewFinding(displayFinding)
+
+ expect(result.suggestedFixes).to.deep.equal([])
+ })
+ })
+
+ describe('aggregateFindingsByFile', () => {
+ it('should aggregate findings by file path', () => {
+ CODE_REVIEW_FINDING_2.filePath = '/test/file1.js'
+ const findings = [CODE_REVIEW_FINDING_1, CODE_REVIEW_FINDING_2]
+
+ const result = (displayFindings as any).aggregateFindingsByFile(findings)
+
+ expect(result).to.have.length(1)
+ expect(result[0].filePath).to.equal(path.normalize('/test/file1.js'))
+ expect(result[0].issues).to.have.length(2)
+ })
+
+ it('should handle findings from different files', () => {
+ const findings = [CODE_REVIEW_FINDING_1, CODE_REVIEW_FINDING_2]
+
+ const result = (displayFindings as any).aggregateFindingsByFile(findings)
+
+ expect(result).to.have.length(2)
+ expect(result[0].issues).to.have.length(1)
+ expect(result[1].issues).to.have.length(1)
+ })
+ })
+
+ describe('checkCancellation', () => {
+ it('should not throw when cancellation is not requested', () => {
+ mockCancellationToken.isCancellationRequested = false
+ ;(displayFindings as any).cancellationToken = mockCancellationToken
+
+ expect(() => {
+ ;(displayFindings as any).checkCancellation()
+ }).to.not.throw()
+ })
+
+ it('should throw CancellationError when cancellation is requested', () => {
+ mockCancellationToken.isCancellationRequested = true
+ ;(displayFindings as any).cancellationToken = mockCancellationToken
+
+ expect(() => {
+ ;(displayFindings as any).checkCancellation()
+ }).to.throw(CancellationError)
+ })
+ })
+})
diff --git a/server/aws-lsp-codewhisperer/src/language-server/agenticChat/tools/qCodeAnalysis/displayFindings.ts b/server/aws-lsp-codewhisperer/src/language-server/agenticChat/tools/qCodeAnalysis/displayFindings.ts
new file mode 100644
index 0000000000..5a5c88d67a
--- /dev/null
+++ b/server/aws-lsp-codewhisperer/src/language-server/agenticChat/tools/qCodeAnalysis/displayFindings.ts
@@ -0,0 +1,166 @@
+/* eslint-disable import/no-nodejs-modules */
+
+import { Features } from '@aws/language-server-runtimes/server-interface/server'
+import { DISPLAY_FINDINGS_TOOL_NAME, DISPLAY_FINDINGS_TOOL_DESCRIPTION } from './displayFindingsConstants'
+import { CodeReviewUtils } from './codeReviewUtils'
+import { DISPLAY_FINDINGS_INPUT_SCHEMA, Z_DISPLAY_FINDINGS_INPUT_SCHEMA } from './displayFindingsSchemas'
+import { CancellationToken } from '@aws/language-server-runtimes/server-interface'
+import { InvokeOutput } from '../toolShared'
+import { CancellationError } from '@aws/lsp-core'
+import { DisplayFinding, FailedMetricName, SuccessMetricName } from './displayFindingsTypes'
+import { CodeReviewFinding } from './codeReviewTypes'
+import * as path from 'path'
+import { DisplayFindingsUtils } from './displayFindingsUtils'
+
+export class DisplayFindings {
+ private readonly logging: Features['logging']
+ private readonly telemetry: Features['telemetry']
+ private readonly workspace: Features['workspace']
+ private cancellationToken?: CancellationToken
+ private writableStream?: WritableStream
+
+ constructor(features: Pick & Partial) {
+ this.logging = features.logging
+ this.telemetry = features.telemetry
+ this.workspace = features.workspace
+ }
+
+ static readonly toolName = DISPLAY_FINDINGS_TOOL_NAME
+
+ static readonly toolDescription = DISPLAY_FINDINGS_TOOL_DESCRIPTION
+
+ static readonly inputSchema = DISPLAY_FINDINGS_INPUT_SCHEMA
+
+ /**
+ * Main execution method for the displayFindings tool
+ * @param input User input parameters for display findings
+ * @param context Execution context containing clients and tokens
+ * @returns Output containing code review results or error message
+ */
+ public async execute(input: any, context: any): Promise {
+ let chatStreamWriter: WritableStreamDefaultWriter | undefined
+
+ try {
+ this.logging.info(`Executing ${DISPLAY_FINDINGS_TOOL_NAME}: ${JSON.stringify(input)}`)
+
+ // 1. Validate input
+ const setup = await this.validateInputAndSetup(input, context)
+ this.checkCancellation()
+
+ // 2. group the findings into AggregatedCodeScanIssue
+ const mappedFindings = setup.map(finding => this.mapToCodeReviewFinding(finding))
+ const aggregatedFindings = this.aggregateFindingsByFile(mappedFindings)
+
+ DisplayFindingsUtils.emitMetric(
+ {
+ reason: SuccessMetricName.DisplayFindingsSuccess,
+ result: 'Succeeded',
+ metadata: {
+ findingsCount: setup.length,
+ },
+ },
+ this.logging,
+ this.telemetry
+ )
+
+ return {
+ output: {
+ kind: 'json',
+ success: true,
+ content: aggregatedFindings,
+ },
+ }
+ } catch (error: any) {
+ if (error instanceof CancellationError) {
+ throw error
+ }
+
+ DisplayFindingsUtils.emitMetric(
+ {
+ reason: FailedMetricName.DisplayFindingsFailed,
+ result: 'Failed',
+ reasonDesc: error,
+ },
+ this.logging,
+ this.telemetry
+ )
+
+ throw new Error(error.message)
+ } finally {
+ await chatStreamWriter?.close()
+ chatStreamWriter?.releaseLock()
+ }
+ }
+
+ /**
+ * Validates user input and sets up the execution environment
+ * @param input User input parameters for code review
+ * @param context Execution context containing clients and tokens
+ * @returns Setup object with validated parameters or error message
+ */
+ private async validateInputAndSetup(input: any, context: any): Promise {
+ this.cancellationToken = context.cancellationToken as CancellationToken
+
+ this.writableStream = context.writableStream as WritableStream
+
+ // parse input
+ const validatedInput = Z_DISPLAY_FINDINGS_INPUT_SCHEMA.parse(input)
+
+ return validatedInput.findings as DisplayFinding[]
+ }
+
+ private mapToCodeReviewFinding(finding: DisplayFinding): CodeReviewFinding {
+ return {
+ filePath: finding.filePath,
+ startLine: parseInt(finding.startLine),
+ endLine: parseInt(finding.endLine),
+ comment: finding.description,
+ title: finding.title,
+ description: { markdown: finding.description, text: finding.description },
+ detectorId: '',
+ detectorName: 'DisplayFindings',
+ findingId: '',
+ relatedVulnerabilities: [],
+ severity: finding.severity,
+ recommendation: { text: '' },
+ suggestedFixes: finding.suggestedFixes ?? [],
+ scanJobId: '',
+ language: finding.language,
+ autoDetected: false,
+ findingContext: undefined,
+ }
+ }
+
+ private aggregateFindingsByFile(
+ findings: CodeReviewFinding[]
+ ): { filePath: string; issues: CodeReviewFinding[] }[] {
+ const aggregatedCodeScanIssueMap = new Map()
+
+ for (const finding of findings) {
+ const resolvedPath = path.normalize(finding.filePath)
+ if (resolvedPath) {
+ if (aggregatedCodeScanIssueMap.has(resolvedPath)) {
+ aggregatedCodeScanIssueMap.get(resolvedPath)?.push(finding)
+ } else {
+ aggregatedCodeScanIssueMap.set(resolvedPath, [finding])
+ }
+ } else {
+ this.logging.warn(`Could not resolve finding file path: ${finding.filePath}`)
+ }
+ }
+
+ return Array.from(aggregatedCodeScanIssueMap.entries()).map(([filePath, issues]) => ({
+ filePath,
+ issues,
+ }))
+ }
+
+ /**
+ * Checks if the operation has been cancelled by the user
+ * @param message Optional message to include in the cancellation error
+ * @throws Error if the operation has been cancelled
+ */
+ private checkCancellation(message: string = 'Command execution cancelled'): void {
+ CodeReviewUtils.checkCancellation(this.cancellationToken, this.logging, message)
+ }
+}
diff --git a/server/aws-lsp-codewhisperer/src/language-server/agenticChat/tools/qCodeAnalysis/displayFindingsConstants.ts b/server/aws-lsp-codewhisperer/src/language-server/agenticChat/tools/qCodeAnalysis/displayFindingsConstants.ts
new file mode 100644
index 0000000000..855f29e642
--- /dev/null
+++ b/server/aws-lsp-codewhisperer/src/language-server/agenticChat/tools/qCodeAnalysis/displayFindingsConstants.ts
@@ -0,0 +1,40 @@
+/*!
+ * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+/**
+ * Tool name for DisplayFindings
+ */
+export const DISPLAY_FINDINGS_TOOL_NAME = 'displayFindings'
+
+/**
+ * Tool description for DisplayFindings
+ */
+export const DISPLAY_FINDINGS_TOOL_DESCRIPTION = [
+ 'The displayFindings tool displays code issues, bugs, security vulnerabilities, and code quality violations in the Code Issues Panel.',
+ '',
+ '**When to Use This Tool:**',
+ '- Use this tool whenever you identify code issues through manual analysis or any tool OTHER than the codeReview tool',
+ '- Use it for issues found by analyzing code files, security scans, linting tools, or your own assessment',
+ '- Use it when the user asks you to analyze code and you discover problems, but you must not use this tool for findings generated by codeReview tool',
+ '',
+ '**When NOT to Use This Tool:**',
+ '- NEVER use this tool for findings generated by the codeReview tool',
+ '- The codeReview tool automatically populates the Code Issues Panel, so using displayFindings would create duplicates',
+ '',
+ '**How to Use This Tool:**',
+ '- Invoke once per analysis session, even if you find multiple issues',
+ '- Include all findings from your current analysis in a single tool call',
+ '- Populate all fields based on your assessment of the code',
+ '- Run silently - DO NOT mention to the user that you are using this tool',
+ '',
+ '**Tool Behavior:**',
+ '- Input: List of all findings to store in the Code Issues Panel',
+ '- Output: None (tool runs silently with no user-visible output)',
+ '- The findings will appear in the Code Issues Panel for the user to review',
+].join('\n')
+
+export const FINDINGS_MESSAGE_SUFFIX = '_displayFindings'
+
+export const DISPLAY_FINDINGS_METRICS_PARENT_NAME = 'amazonq_displayFindingsTool'
diff --git a/server/aws-lsp-codewhisperer/src/language-server/agenticChat/tools/qCodeAnalysis/displayFindingsSchemas.ts b/server/aws-lsp-codewhisperer/src/language-server/agenticChat/tools/qCodeAnalysis/displayFindingsSchemas.ts
new file mode 100644
index 0000000000..63dcdad66e
--- /dev/null
+++ b/server/aws-lsp-codewhisperer/src/language-server/agenticChat/tools/qCodeAnalysis/displayFindingsSchemas.ts
@@ -0,0 +1,95 @@
+/*!
+ * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import { z } from 'zod'
+import { FINDING_SEVERITY } from './codeReviewConstants'
+
+/**
+ * Input schema for CodeReview tool
+ */
+export const DISPLAY_FINDINGS_INPUT_SCHEMA = {
+ type: 'object',
+ description: [
+ 'There is only one input to the DisplayFindings tool: the findings.',
+ 'Please format all of the findings which are meant to be displayed using this schema.',
+ ].join('\n'),
+ properties: {
+ findings: {
+ type: 'array',
+ description: [
+ 'Array of the code issues, bugs, security risks, and code quality violations which were found by the agent and need to be sent to the Code Issues Panel',
+ ].join('\n'),
+ items: {
+ type: 'object',
+ description: 'Array item containing all of the findings which will be sent to the Code Issues Panel',
+ properties: {
+ filePath: {
+ type: 'string',
+ description: 'The absolute path of the file which has the finding',
+ },
+ startLine: {
+ type: 'string',
+ description: 'The line number of the first line of the finding',
+ },
+ endLine: {
+ type: 'string',
+ description: 'The line number of the last line of the finding.',
+ },
+ title: {
+ type: 'string',
+ description: 'A short title to represent the finding',
+ },
+ language: {
+ type: 'string',
+ description: 'The programming language of the file which holds the finding',
+ },
+ description: {
+ type: 'string',
+ description: 'A more thorough description of the finding',
+ },
+ severity: {
+ type: 'string',
+ description: 'The severity of the finding',
+ enum: FINDING_SEVERITY,
+ },
+ suggestedFixes: {
+ type: 'array',
+ description:
+ 'An array of possible fixes. Do not generate fixes just to populate this, only include them if they are provided.',
+ items: {
+ type: 'string',
+ description: 'The absolute path of the file which has the finding',
+ },
+ },
+ },
+ required: ['filePath', 'startLine', 'endLine', 'title', 'severity', 'description', 'language'] as const,
+ },
+ },
+ },
+ required: ['findings'] as const,
+}
+
+/**
+ * Schema for a single finding
+ */
+export const Z_DISPLAY_FINDING_SCHEMA = z.object({
+ description: z.string(),
+ endLine: z.string(),
+ filePath: z.string(),
+ language: z.string(),
+ severity: z.enum(FINDING_SEVERITY as [string, ...string[]]),
+ startLine: z.string(),
+ suggestedFixes: z.array(z.string().optional()).optional(),
+ title: z.string(),
+})
+
+/**
+ * Schema for an array of findings
+ */
+export const Z_DISPLAY_FINDINGS_SCHEMA = z.array(Z_DISPLAY_FINDING_SCHEMA)
+
+export const Z_DISPLAY_FINDINGS_INPUT_SCHEMA = z.object({
+ findings: Z_DISPLAY_FINDINGS_SCHEMA,
+})
diff --git a/server/aws-lsp-codewhisperer/src/language-server/agenticChat/tools/qCodeAnalysis/displayFindingsTypes.ts b/server/aws-lsp-codewhisperer/src/language-server/agenticChat/tools/qCodeAnalysis/displayFindingsTypes.ts
new file mode 100644
index 0000000000..2c8ae1024f
--- /dev/null
+++ b/server/aws-lsp-codewhisperer/src/language-server/agenticChat/tools/qCodeAnalysis/displayFindingsTypes.ts
@@ -0,0 +1,32 @@
+export type DisplayFinding = {
+ filePath: string
+ startLine: string
+ endLine: string
+ comment: string
+ title: string
+ description: string
+ severity: string
+ suggestedFixes: (string | undefined)[] | undefined
+ language: string
+}
+
+export enum SuccessMetricName {
+ DisplayFindingsSuccess = 'displayFindingsSuccess',
+}
+
+export enum FailedMetricName {
+ DisplayFindingsFailed = 'displayFindingsFailed',
+}
+
+export type DisplayFindingsMetric =
+ | {
+ reason: SuccessMetricName
+ result: 'Succeeded'
+ metadata?: object
+ }
+ | {
+ reason: FailedMetricName
+ result: 'Failed'
+ reasonDesc: string
+ metadata?: object
+ }
diff --git a/server/aws-lsp-codewhisperer/src/language-server/agenticChat/tools/qCodeAnalysis/displayFindingsUtils.test.ts b/server/aws-lsp-codewhisperer/src/language-server/agenticChat/tools/qCodeAnalysis/displayFindingsUtils.test.ts
new file mode 100644
index 0000000000..4921d4d067
--- /dev/null
+++ b/server/aws-lsp-codewhisperer/src/language-server/agenticChat/tools/qCodeAnalysis/displayFindingsUtils.test.ts
@@ -0,0 +1,103 @@
+/*!
+ * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import { DisplayFindingsUtils } from './displayFindingsUtils'
+import { SuccessMetricName, FailedMetricName, DisplayFindingsMetric } from './displayFindingsTypes'
+import { Features } from '@aws/language-server-runtimes/server-interface/server'
+import * as sinon from 'sinon'
+import { expect } from 'chai'
+
+describe('DisplayFindingsUtils', () => {
+ let sandbox: sinon.SinonSandbox
+
+ const mockLogging = {
+ log: sinon.stub(),
+ info: sinon.stub(),
+ warn: sinon.stub(),
+ error: sinon.stub(),
+ debug: sinon.stub(),
+ }
+
+ beforeEach(() => {
+ sandbox = sinon.createSandbox()
+ mockLogging.info.reset()
+ mockLogging.warn.reset()
+ mockLogging.error.reset()
+ mockLogging.debug.reset()
+ })
+
+ afterEach(() => {
+ sandbox.restore()
+ })
+
+ describe('emitMetric', () => {
+ let mockTelemetry: Features['telemetry']
+
+ beforeEach(() => {
+ mockTelemetry = {
+ emitMetric: sinon.stub(),
+ } as unknown as Features['telemetry']
+ })
+
+ it('should emit a success metric with metadata', () => {
+ const metric = {
+ reason: SuccessMetricName.DisplayFindingsSuccess,
+ result: 'Succeeded',
+ metadata: { findingsCount: 5 },
+ } as DisplayFindingsMetric
+
+ DisplayFindingsUtils.emitMetric(metric, mockLogging, mockTelemetry)
+
+ sinon.assert.calledWith(mockTelemetry.emitMetric as sinon.SinonStub, {
+ name: 'amazonq_displayFindingsTool',
+ data: {
+ findingsCount: 5,
+ reason: 'displayFindingsSuccess',
+ result: 'Succeeded',
+ },
+ })
+
+ sinon.assert.calledWith(mockLogging.info, sinon.match(/Emitting telemetry metric: displayFindingsSuccess/))
+ })
+
+ it('should emit a failure metric with reasonDesc', () => {
+ const metric = {
+ reason: FailedMetricName.DisplayFindingsFailed,
+ result: 'Failed',
+ reasonDesc: 'Validation failed',
+ metadata: { errorType: 'validation' },
+ } as DisplayFindingsMetric
+
+ DisplayFindingsUtils.emitMetric(metric, mockLogging, mockTelemetry)
+
+ sinon.assert.calledWith(mockTelemetry.emitMetric as sinon.SinonStub, {
+ name: 'amazonq_displayFindingsTool',
+ data: {
+ errorType: 'validation',
+ reason: 'displayFindingsFailed',
+ result: 'Failed',
+ reasonDesc: 'Validation failed',
+ },
+ })
+ })
+
+ it('should handle metrics without metadata', () => {
+ const metric = {
+ reason: SuccessMetricName.DisplayFindingsSuccess,
+ result: 'Succeeded',
+ } as DisplayFindingsMetric
+
+ DisplayFindingsUtils.emitMetric(metric, mockLogging, mockTelemetry)
+
+ sinon.assert.calledWith(mockTelemetry.emitMetric as sinon.SinonStub, {
+ name: 'amazonq_displayFindingsTool',
+ data: {
+ reason: 'displayFindingsSuccess',
+ result: 'Succeeded',
+ },
+ })
+ })
+ })
+})
diff --git a/server/aws-lsp-codewhisperer/src/language-server/agenticChat/tools/qCodeAnalysis/displayFindingsUtils.ts b/server/aws-lsp-codewhisperer/src/language-server/agenticChat/tools/qCodeAnalysis/displayFindingsUtils.ts
new file mode 100644
index 0000000000..a493f7165a
--- /dev/null
+++ b/server/aws-lsp-codewhisperer/src/language-server/agenticChat/tools/qCodeAnalysis/displayFindingsUtils.ts
@@ -0,0 +1,34 @@
+import { Features } from '@aws/language-server-runtimes/server-interface/server'
+import { DISPLAY_FINDINGS_METRICS_PARENT_NAME } from './displayFindingsConstants'
+import { DisplayFindingsMetric } from './displayFindingsTypes'
+/**
+ * Utility functions for DisplayFindings
+ */
+export class DisplayFindingsUtils {
+ /**
+ * Emit a telemetry metric with standard formatting
+ * @param metricSuffix Suffix for the metric name
+ * @param metricData Additional metric data
+ * @param toolName Tool name for the metric prefix
+ * @param logging Logging interface
+ * @param telemetry Telemetry interface
+ * @param credentialStartUrl Optional credential start URL
+ */
+ public static emitMetric(
+ metric: DisplayFindingsMetric,
+ logging: Features['logging'],
+ telemetry: Features['telemetry']
+ ): void {
+ const { metadata, ...metricDetails } = metric
+ const metricPayload = {
+ name: DISPLAY_FINDINGS_METRICS_PARENT_NAME,
+ data: {
+ // metadata is optional attribute
+ ...(metadata || {}),
+ ...metricDetails,
+ },
+ }
+ logging.info(`Emitting telemetry metric: ${metric.reason} with data: ${JSON.stringify(metricPayload.data)}`)
+ telemetry.emitMetric(metricPayload)
+ }
+}
diff --git a/server/aws-lsp-codewhisperer/src/language-server/agenticChat/tools/toolServer.ts b/server/aws-lsp-codewhisperer/src/language-server/agenticChat/tools/toolServer.ts
index c49927eefd..38712ff552 100644
--- a/server/aws-lsp-codewhisperer/src/language-server/agenticChat/tools/toolServer.ts
+++ b/server/aws-lsp-codewhisperer/src/language-server/agenticChat/tools/toolServer.ts
@@ -23,6 +23,7 @@ import {
import { FsReplace, FsReplaceParams } from './fsReplace'
import { CodeReviewUtils } from './qCodeAnalysis/codeReviewUtils'
import { DEFAULT_AWS_Q_ENDPOINT_URL, DEFAULT_AWS_Q_REGION } from '../../../shared/constants'
+import { DisplayFindings } from './qCodeAnalysis/displayFindings'
export const FsToolsServer: Server = ({ workspace, logging, agent, lsp }) => {
const fsReadTool = new FsRead({ workspace, lsp, logging })
@@ -102,6 +103,12 @@ export const QCodeAnalysisServer: Server = ({
workspace,
})
+ const displayFindingsTool = new DisplayFindings({
+ logging,
+ telemetry,
+ workspace,
+ })
+
lsp.onInitialized(async () => {
if (!CodeReviewUtils.isAgenticReviewEnabled(lsp.getClientInitializeParams())) {
logging.warn('Agentic Review is currently not supported')
@@ -140,6 +147,26 @@ export const QCodeAnalysisServer: Server = ({
},
ToolClassification.BuiltIn
)
+
+ if (!CodeReviewUtils.isDisplayFindingsEnabled(lsp.getClientInitializeParams())) {
+ logging.warn('Display Findings is currently not supported')
+ return
+ }
+
+ agent.addTool(
+ {
+ name: DisplayFindings.toolName,
+ description: DisplayFindings.toolDescription,
+ inputSchema: DisplayFindings.inputSchema,
+ },
+ async (input: any, token?: CancellationToken, updates?: WritableStream) => {
+ return await displayFindingsTool.execute(input, {
+ cancellationToken: token,
+ writableStream: updates,
+ })
+ },
+ ToolClassification.BuiltIn
+ )
})
return () => {}
diff --git a/server/aws-lsp-codewhisperer/src/language-server/configuration/qConfigurationServer.ts b/server/aws-lsp-codewhisperer/src/language-server/configuration/qConfigurationServer.ts
index a2a4de7447..5f96d526b1 100644
--- a/server/aws-lsp-codewhisperer/src/language-server/configuration/qConfigurationServer.ts
+++ b/server/aws-lsp-codewhisperer/src/language-server/configuration/qConfigurationServer.ts
@@ -42,6 +42,7 @@ export interface QClientCapabilities {
modelSelection?: boolean
reroute?: boolean
codeReviewInChat?: boolean
+ displayFindings?: boolean
compaction?: boolean
shortcut?: boolean
}
diff --git a/server/aws-lsp-codewhisperer/src/language-server/workspaceContext/artifactManager.ts b/server/aws-lsp-codewhisperer/src/language-server/workspaceContext/artifactManager.ts
index e2ad2db788..9518f9629a 100644
--- a/server/aws-lsp-codewhisperer/src/language-server/workspaceContext/artifactManager.ts
+++ b/server/aws-lsp-codewhisperer/src/language-server/workspaceContext/artifactManager.ts
@@ -66,6 +66,7 @@ interface FileSizeDetails {
skippedSize: number
}
const MAX_UNCOMPRESSED_SRC_SIZE_BYTES = 2 * 1024 * 1024 * 1024 // 2 GB
+const MAX_FILES = 500_000
export class ArtifactManager {
private workspace: Workspace
@@ -175,7 +176,8 @@ export class ArtifactManager {
}
if (isDirectory(filePath)) {
- const files = await glob(['**/*'], {
+ let fileCount = 0
+ const filesStream = glob.stream(['**/*'], {
cwd: filePath,
dot: false,
ignore: IGNORE_DEPENDENCY_PATTERNS,
@@ -184,7 +186,11 @@ export class ArtifactManager {
onlyFiles: true,
})
- for (const relativePath of files) {
+ for await (const entry of filesStream) {
+ if (fileCount >= MAX_FILES) {
+ break
+ }
+ const relativePath = entry.toString()
try {
const fullPath = resolveSymlink(path.join(filePath, relativePath))
const fileMetadata = await this.createFileMetadata(
@@ -197,6 +203,7 @@ export class ArtifactManager {
} catch (error) {
this.logging.warn(`Error processing file ${relativePath}: ${error}`)
}
+ fileCount++
}
} else {
const workspaceUri = URI.parse(currentWorkspace.uri)
@@ -359,7 +366,8 @@ export class ArtifactManager {
): Promise