From 9cb959d4cc450d0907f8bf5265ba01d2aa68bcd0 Mon Sep 17 00:00:00 2001 From: Jiatong Li Date: Fri, 8 Aug 2025 11:36:22 -0700 Subject: [PATCH 01/74] fix(amazonq): skips continuous monitoring when WCS sees workspace as idle (#2066) * fix(amazonq): skips continuous monitoring when WCS sees workspace as idle * fix(amazonq): skips creating remote workspace at the start --------- Co-authored-by: Jiatong Li --- .../agenticChat/agenticChatController.test.ts | 10 + .../agenticChat/agenticChatController.ts | 6 + .../codeWhispererServer.test.ts | 17 ++ .../inline-completion/codeWhispererServer.ts | 3 + .../IdleWorkspaceManager.test.ts | 75 +++++++ .../workspaceContext/IdleWorkspaceManager.ts | 38 ++++ .../dependency/dependencyDiscoverer.ts | 3 +- .../workspaceContextServer.ts | 9 +- .../workspaceFolderManager.test.ts | 91 ++++---- .../workspaceFolderManager.ts | 207 ++++++++++-------- 10 files changed, 320 insertions(+), 139 deletions(-) create mode 100644 server/aws-lsp-codewhisperer/src/language-server/workspaceContext/IdleWorkspaceManager.test.ts create mode 100644 server/aws-lsp-codewhisperer/src/language-server/workspaceContext/IdleWorkspaceManager.ts diff --git a/server/aws-lsp-codewhisperer/src/language-server/agenticChat/agenticChatController.test.ts b/server/aws-lsp-codewhisperer/src/language-server/agenticChat/agenticChatController.test.ts index 48e9e2adec..6c8af7a272 100644 --- a/server/aws-lsp-codewhisperer/src/language-server/agenticChat/agenticChatController.test.ts +++ b/server/aws-lsp-codewhisperer/src/language-server/agenticChat/agenticChatController.test.ts @@ -67,6 +67,7 @@ import { McpManager } from './tools/mcp/mcpManager' import { AgenticChatResultStream } from './agenticChatResultStream' import { AgenticChatError } from './errors' import * as sharedUtils from '../../shared/utils' +import { IdleWorkspaceManager } from '../workspaceContext/IdleWorkspaceManager' describe('AgenticChatController', () => { let mcpInstanceStub: sinon.SinonStub @@ -475,6 +476,15 @@ describe('AgenticChatController', () => { assert.strictEqual(typeof session.conversationId, 'string') }) + it('invokes IdleWorkspaceManager recordActivityTimestamp', async () => { + const recordActivityTimestampStub = sinon.stub(IdleWorkspaceManager, 'recordActivityTimestamp') + + await chatController.onChatPrompt({ tabId: mockTabId, prompt: { prompt: 'Hello' } }, mockCancellationToken) + + sinon.assert.calledOnce(recordActivityTimestampStub) + recordActivityTimestampStub.restore() + }) + it('includes chat history from the database in the request input', async () => { // Mock chat history const mockHistory = [ 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 60bba5885a..72217fae07 100644 --- a/server/aws-lsp-codewhisperer/src/language-server/agenticChat/agenticChatController.ts +++ b/server/aws-lsp-codewhisperer/src/language-server/agenticChat/agenticChatController.ts @@ -230,6 +230,7 @@ import { ActiveUserTracker } from '../../shared/activeUserTracker' import { UserContext } from '../../client/token/codewhispererbearertokenclient' import { CodeWhispererServiceToken } from '../../shared/codeWhispererService' import { DisplayFindings } from './tools/qCodeAnalysis/displayFindings' +import { IdleWorkspaceManager } from '../workspaceContext/IdleWorkspaceManager' type ChatHandlers = Omit< LspHandlers, @@ -722,6 +723,8 @@ export class AgenticChatController implements ChatHandlers { // Phase 1: Initial Setup - This happens only once params.prompt.prompt = sanitizeInput(params.prompt.prompt || '') + IdleWorkspaceManager.recordActivityTimestamp() + const maybeDefaultResponse = !params.prompt.command && getDefaultChatResponse(params.prompt.prompt) if (maybeDefaultResponse) { return maybeDefaultResponse @@ -3285,6 +3288,9 @@ export class AgenticChatController implements ChatHandlers { const metric = new Metric({ cwsprChatConversationType: 'Chat', }) + + IdleWorkspaceManager.recordActivityTimestamp() + const triggerContext = await this.#getInlineChatTriggerContext(params) let response: ChatCommandOutput diff --git a/server/aws-lsp-codewhisperer/src/language-server/inline-completion/codeWhispererServer.test.ts b/server/aws-lsp-codewhisperer/src/language-server/inline-completion/codeWhispererServer.test.ts index 1636c17b84..29390248f7 100644 --- a/server/aws-lsp-codewhisperer/src/language-server/inline-completion/codeWhispererServer.test.ts +++ b/server/aws-lsp-codewhisperer/src/language-server/inline-completion/codeWhispererServer.test.ts @@ -61,6 +61,7 @@ import { INVALID_TOKEN } from '../../shared/constants' import { AmazonQError } from '../../shared/amazonQServiceManager/errors' import * as path from 'path' import { CONTEXT_CHARACTERS_LIMIT } from './constants' +import { IdleWorkspaceManager } from '../workspaceContext/IdleWorkspaceManager' const updateConfiguration = async ( features: TestFeatures, @@ -770,6 +771,22 @@ describe('CodeWhisperer Server', () => { assert.rejects(promise, ResponseError) }) + it('invokes IdleWorkspaceManager recordActivityTimestamp', async () => { + const recordActivityTimestampStub = sinon.stub(IdleWorkspaceManager, 'recordActivityTimestamp') + + await features.doInlineCompletionWithReferences( + { + textDocument: { uri: SOME_FILE.uri }, + position: { line: 0, character: 0 }, + context: { triggerKind: InlineCompletionTriggerKind.Invoked }, + }, + CancellationToken.None + ) + + sinon.assert.calledOnce(recordActivityTimestampStub) + recordActivityTimestampStub.restore() + }) + describe('Supplemental Context', () => { it('should send supplemental context when using token authentication', async () => { const test_service = sinon.createStubInstance( diff --git a/server/aws-lsp-codewhisperer/src/language-server/inline-completion/codeWhispererServer.ts b/server/aws-lsp-codewhisperer/src/language-server/inline-completion/codeWhispererServer.ts index f2b1a2c43d..9ed5a2330f 100644 --- a/server/aws-lsp-codewhisperer/src/language-server/inline-completion/codeWhispererServer.ts +++ b/server/aws-lsp-codewhisperer/src/language-server/inline-completion/codeWhispererServer.ts @@ -56,6 +56,7 @@ import { import { DocumentChangedListener } from './documentChangedListener' import { EditCompletionHandler } from './editCompletionHandler' import { EMPTY_RESULT } from './constants' +import { IdleWorkspaceManager } from '../workspaceContext/IdleWorkspaceManager' const mergeSuggestionsWithRightContext = ( rightFileContext: string, @@ -141,6 +142,8 @@ export const CodewhispererServerFactory = // 2. it is not designed to handle concurrent changes to these state variables. // when one handler is at the API call stage, it has not yet update the session state // but another request can start, causing the state to be incorrect. + IdleWorkspaceManager.recordActivityTimestamp() + if (isOnInlineCompletionHandlerInProgress) { logging.log(`Skip concurrent inline completion`) return EMPTY_RESULT diff --git a/server/aws-lsp-codewhisperer/src/language-server/workspaceContext/IdleWorkspaceManager.test.ts b/server/aws-lsp-codewhisperer/src/language-server/workspaceContext/IdleWorkspaceManager.test.ts new file mode 100644 index 0000000000..a5d81526c9 --- /dev/null +++ b/server/aws-lsp-codewhisperer/src/language-server/workspaceContext/IdleWorkspaceManager.test.ts @@ -0,0 +1,75 @@ +import { IdleWorkspaceManager } from './IdleWorkspaceManager' +import { WorkspaceFolderManager } from './workspaceFolderManager' +import sinon, { stubInterface, StubbedInstance } from 'ts-sinon' + +describe('IdleWorkspaceManager', () => { + let clock: sinon.SinonFakeTimers + let mockWorkspaceFolderManager: StubbedInstance + + beforeEach(() => { + clock = sinon.useFakeTimers() + mockWorkspaceFolderManager = stubInterface() + sinon.stub(WorkspaceFolderManager, 'getInstance').returns(mockWorkspaceFolderManager) + sinon.stub(console, 'error') + }) + + afterEach(() => { + clock.restore() + sinon.restore() + }) + + describe('isSessionIdle', () => { + it('should return false when session is not idle', () => { + IdleWorkspaceManager.recordActivityTimestamp() + + const result = IdleWorkspaceManager.isSessionIdle() + + expect(result).toBe(false) + }) + + it('should return true when session exceeds idle threshold', () => { + IdleWorkspaceManager.recordActivityTimestamp() + clock.tick(31 * 60 * 1000) // 31 minutes + + const result = IdleWorkspaceManager.isSessionIdle() + + expect(result).toBe(true) + }) + }) + + describe('recordActivityTimestamp', () => { + it('should update activity timestamp', async () => { + IdleWorkspaceManager.recordActivityTimestamp() + + expect(IdleWorkspaceManager.isSessionIdle()).toBe(false) + }) + + it('should not trigger workspace check when session was not idle', async () => { + mockWorkspaceFolderManager.isContinuousMonitoringStopped.returns(false) + + IdleWorkspaceManager.recordActivityTimestamp() + + sinon.assert.notCalled(mockWorkspaceFolderManager.checkRemoteWorkspaceStatusAndReact) + }) + + it('should trigger workspace check when session was idle and monitoring is active', async () => { + // Make session idle first + clock.tick(31 * 60 * 1000) + mockWorkspaceFolderManager.isContinuousMonitoringStopped.returns(false) + mockWorkspaceFolderManager.checkRemoteWorkspaceStatusAndReact.resolves() + + IdleWorkspaceManager.recordActivityTimestamp() + + sinon.assert.calledOnce(mockWorkspaceFolderManager.checkRemoteWorkspaceStatusAndReact) + }) + + it('should not trigger workspace check when session was idle but monitoring is stopped', async () => { + clock.tick(31 * 60 * 1000) + mockWorkspaceFolderManager.isContinuousMonitoringStopped.returns(true) + + IdleWorkspaceManager.recordActivityTimestamp() + + sinon.assert.notCalled(mockWorkspaceFolderManager.checkRemoteWorkspaceStatusAndReact) + }) + }) +}) diff --git a/server/aws-lsp-codewhisperer/src/language-server/workspaceContext/IdleWorkspaceManager.ts b/server/aws-lsp-codewhisperer/src/language-server/workspaceContext/IdleWorkspaceManager.ts new file mode 100644 index 0000000000..5a8359ccac --- /dev/null +++ b/server/aws-lsp-codewhisperer/src/language-server/workspaceContext/IdleWorkspaceManager.ts @@ -0,0 +1,38 @@ +import { WorkspaceFolderManager } from './workspaceFolderManager' + +export class IdleWorkspaceManager { + private static readonly idleThreshold = 30 * 60 * 1000 // 30 minutes + private static lastActivityTimestamp = 0 // treat session as idle as the start + + private constructor() {} + + /** + * Records activity timestamp and triggers workspace status check if session was idle. + * + * When transitioning from idle to active, proactively checks remote workspace status + * (if continuous monitoring is enabled) without blocking the current operation. + */ + public static recordActivityTimestamp(): void { + try { + const wasSessionIdle = IdleWorkspaceManager.isSessionIdle() + IdleWorkspaceManager.lastActivityTimestamp = Date.now() + + const workspaceFolderManager = WorkspaceFolderManager.getInstance() + if (workspaceFolderManager && wasSessionIdle && !workspaceFolderManager.isContinuousMonitoringStopped()) { + // Proactively check the remote workspace status instead of waiting for the next scheduled check + // Fire and forget - don't await to avoid blocking + workspaceFolderManager.checkRemoteWorkspaceStatusAndReact().catch(err => { + // ignore errors + }) + } + } catch (err) { + // ignore errors + } + } + + public static isSessionIdle(): boolean { + const currentTime = Date.now() + const timeSinceLastActivity = currentTime - IdleWorkspaceManager.lastActivityTimestamp + return timeSinceLastActivity > IdleWorkspaceManager.idleThreshold + } +} diff --git a/server/aws-lsp-codewhisperer/src/language-server/workspaceContext/dependency/dependencyDiscoverer.ts b/server/aws-lsp-codewhisperer/src/language-server/workspaceContext/dependency/dependencyDiscoverer.ts index 8a4a9755cb..ed387ca2ca 100644 --- a/server/aws-lsp-codewhisperer/src/language-server/workspaceContext/dependency/dependencyDiscoverer.ts +++ b/server/aws-lsp-codewhisperer/src/language-server/workspaceContext/dependency/dependencyDiscoverer.ts @@ -191,7 +191,8 @@ export class DependencyDiscoverer { } } - public resetFromDisposal(): void { + public disposeAndReset(): void { + this.dispose() this.sharedState.isDisposed = false this.sharedState.dependencyUploadedSizeSum = 0 } diff --git a/server/aws-lsp-codewhisperer/src/language-server/workspaceContext/workspaceContextServer.ts b/server/aws-lsp-codewhisperer/src/language-server/workspaceContext/workspaceContextServer.ts index 09336ffb2f..c536c1087d 100644 --- a/server/aws-lsp-codewhisperer/src/language-server/workspaceContext/workspaceContextServer.ts +++ b/server/aws-lsp-codewhisperer/src/language-server/workspaceContext/workspaceContextServer.ts @@ -221,7 +221,6 @@ export const WorkspaceContextServer = (): Server => features => { isLoggedInUsingBearerToken(credentialsProvider) && abTestingEnabled && !workspaceFolderManager.getOptOutStatus() && - !workspaceFolderManager.getServiceQuotaExceededStatus() && workspaceIdentifier ) } @@ -303,17 +302,15 @@ export const WorkspaceContextServer = (): Server => features => { await evaluateABTesting() isWorkflowInitialized = true - workspaceFolderManager.resetAdminOptOutAndServiceQuotaStatus() + workspaceFolderManager.resetAdminOptOutStatus() if (!isUserEligibleForWorkspaceContext()) { return } fileUploadJobManager.startFileUploadJobConsumer() dependencyEventBundler.startDependencyEventBundler() - await Promise.all([ - workspaceFolderManager.initializeWorkspaceStatusMonitor(), - workspaceFolderManager.processNewWorkspaceFolders(workspaceFolders), - ]) + + workspaceFolderManager.initializeWorkspaceStatusMonitor() logging.log(`Workspace context workflow initialized`) } else if (!isLoggedIn) { if (isWorkflowInitialized) { diff --git a/server/aws-lsp-codewhisperer/src/language-server/workspaceContext/workspaceFolderManager.test.ts b/server/aws-lsp-codewhisperer/src/language-server/workspaceContext/workspaceFolderManager.test.ts index 0b4ec53f25..7ab596a930 100644 --- a/server/aws-lsp-codewhisperer/src/language-server/workspaceContext/workspaceFolderManager.test.ts +++ b/server/aws-lsp-codewhisperer/src/language-server/workspaceContext/workspaceFolderManager.test.ts @@ -6,8 +6,8 @@ import { DependencyDiscoverer } from './dependency/dependencyDiscoverer' import { WorkspaceFolder } from 'vscode-languageserver-protocol' import { ArtifactManager } from './artifactManager' import { CodeWhispererServiceToken } from '../../shared/codeWhispererService' -import { CreateWorkspaceResponse } from '../../client/token/codewhispererbearertokenclient' -import { AWSError } from 'aws-sdk' +import { ListWorkspaceMetadataResponse } from '../../client/token/codewhispererbearertokenclient' +import { IdleWorkspaceManager } from './IdleWorkspaceManager' describe('WorkspaceFolderManager', () => { let mockServiceManager: StubbedInstance @@ -33,8 +33,8 @@ describe('WorkspaceFolderManager', () => { sinon.restore() }) - describe('getServiceQuotaExceededStatus', () => { - it('should return true when service quota is exceeded', async () => { + describe('checkRemoteWorkspaceStatusAndReact', () => { + it('should check and react when IDE session is not idle', async () => { // Setup const workspaceFolders: WorkspaceFolder[] = [ { @@ -43,17 +43,20 @@ describe('WorkspaceFolderManager', () => { }, ] - // Mock the createWorkspace method to throw a ServiceQuotaExceededException - const mockError: AWSError = { - name: 'ServiceQuotaExceededException', - message: 'You have too many active running workspaces.', - code: 'ServiceQuotaExceededException', - time: new Date(), - retryable: false, - statusCode: 400, + // Mock IdleSessionManager to return false (not idle) + sinon.stub(IdleWorkspaceManager, 'isSessionIdle').returns(false) + + // Mock successful response + const mockResponse: ListWorkspaceMetadataResponse = { + workspaces: [ + { + workspaceId: 'test-workspace-id', + workspaceStatus: 'CREATED', + }, + ], } - mockCodeWhispererService.createWorkspace.rejects(mockError) + mockCodeWhispererService.listWorkspaceMetadata.resolves(mockResponse as any) // Create the WorkspaceFolderManager instance using the static createInstance method workspaceFolderManager = WorkspaceFolderManager.createInstance( @@ -66,23 +69,24 @@ describe('WorkspaceFolderManager', () => { 'test-workspace-identifier' ) - // Spy on clearAllWorkspaceResources and related methods - const clearAllWorkspaceResourcesSpy = sinon.stub( + // Spy on resetWebSocketClient + const resetWebSocketClientSpy = sinon.stub(workspaceFolderManager as any, 'resetWebSocketClient') + + // Spy on handleWorkspaceCreatedState + const handleWorkspaceCreatedStateSpy = sinon.stub( workspaceFolderManager as any, - 'clearAllWorkspaceResources' + 'handleWorkspaceCreatedState' ) - // Act - trigger the createNewWorkspace method which sets isServiceQuotaExceeded - await (workspaceFolderManager as any).createNewWorkspace() + // Act - trigger the checkRemoteWorkspaceStatusAndReact method + await workspaceFolderManager.checkRemoteWorkspaceStatusAndReact() - // Assert - expect(workspaceFolderManager.getServiceQuotaExceededStatus()).toBe(true) - - // Verify that clearAllWorkspaceResources was called - sinon.assert.calledOnce(clearAllWorkspaceResourcesSpy) + // Verify that resetWebSocketClient was called once + sinon.assert.notCalled(resetWebSocketClientSpy) + sinon.assert.calledOnce(handleWorkspaceCreatedStateSpy) }) - it('should return false when service quota is not exceeded', async () => { + it('should skip checking and reacting when IDE session is idle', async () => { // Setup const workspaceFolders: WorkspaceFolder[] = [ { @@ -91,15 +95,20 @@ describe('WorkspaceFolderManager', () => { }, ] + // Mock IdleSessionManager to return true (idle) + sinon.stub(IdleWorkspaceManager, 'isSessionIdle').returns(true) + // Mock successful response - const mockResponse: CreateWorkspaceResponse = { - workspace: { - workspaceId: 'test-workspace-id', - workspaceStatus: 'RUNNING', - }, + const mockResponse: ListWorkspaceMetadataResponse = { + workspaces: [ + { + workspaceId: 'test-workspace-id', + workspaceStatus: 'CREATED', + }, + ], } - mockCodeWhispererService.createWorkspace.resolves(mockResponse as any) + mockCodeWhispererService.listWorkspaceMetadata.resolves(mockResponse as any) // Create the WorkspaceFolderManager instance using the static createInstance method workspaceFolderManager = WorkspaceFolderManager.createInstance( @@ -112,20 +121,18 @@ describe('WorkspaceFolderManager', () => { 'test-workspace-identifier' ) - // Spy on clearAllWorkspaceResources - const clearAllWorkspaceResourcesSpy = sinon.stub( - workspaceFolderManager as any, - 'clearAllWorkspaceResources' - ) + // Spy on resetWebSocketClient + const resetWebSocketClientSpy = sinon.stub(workspaceFolderManager as any, 'resetWebSocketClient') - // Act - trigger the createNewWorkspace method - await (workspaceFolderManager as any).createNewWorkspace() + // Act - trigger the checkRemoteWorkspaceStatusAndReact method + await workspaceFolderManager.checkRemoteWorkspaceStatusAndReact() - // Assert - expect(workspaceFolderManager.getServiceQuotaExceededStatus()).toBe(false) - - // Verify that clearAllWorkspaceResources was not called - sinon.assert.notCalled(clearAllWorkspaceResourcesSpy) + // Verify that resetWebSocketClient was called once + sinon.assert.calledOnce(resetWebSocketClientSpy) + sinon.assert.calledWith( + mockLogging.log, + sinon.match(/Session is idle, skipping remote workspace status check/) + ) }) }) }) diff --git a/server/aws-lsp-codewhisperer/src/language-server/workspaceContext/workspaceFolderManager.ts b/server/aws-lsp-codewhisperer/src/language-server/workspaceContext/workspaceFolderManager.ts index 00048cd62f..99fc9c4628 100644 --- a/server/aws-lsp-codewhisperer/src/language-server/workspaceContext/workspaceFolderManager.ts +++ b/server/aws-lsp-codewhisperer/src/language-server/workspaceContext/workspaceFolderManager.ts @@ -20,6 +20,7 @@ import { AmazonQTokenServiceManager } from '../../shared/amazonQServiceManager/A import { URI } from 'vscode-uri' import path = require('path') import { isAwsError } from '../../shared/utils' +import { IdleWorkspaceManager } from './IdleWorkspaceManager' interface WorkspaceState { remoteWorkspaceState: WorkspaceStatus @@ -54,8 +55,8 @@ export class WorkspaceFolderManager { private optOutMonitorInterval: NodeJS.Timeout | undefined private messageQueueConsumerInterval: NodeJS.Timeout | undefined private isOptedOut: boolean = false - // Tracks if the user has reached their maximum allowed remote workspaces quota - private isServiceQuotaExceeded: boolean = false + private isCheckingRemoteWorkspaceStatus: boolean = false + private isArtifactUploadedToRemoteWorkspace: boolean = false static createInstance( serviceManager: AmazonQTokenServiceManager, @@ -138,13 +139,8 @@ export class WorkspaceFolderManager { return this.isOptedOut } - getServiceQuotaExceededStatus(): boolean { - return this.isServiceQuotaExceeded - } - - resetAdminOptOutAndServiceQuotaStatus(): void { + resetAdminOptOutStatus(): void { this.isOptedOut = false - this.isServiceQuotaExceeded = false } getWorkspaceState(): WorkspaceState { @@ -324,14 +320,13 @@ export class WorkspaceFolderManager { this.workspaceState.webSocketClient = webSocketClient } - async initializeWorkspaceStatusMonitor() { + initializeWorkspaceStatusMonitor() { this.logging.log(`Initializing workspace status check for workspace [${this.workspaceIdentifier}]`) // Reset workspace ID to force operations to wait for new remote workspace information this.resetRemoteWorkspaceId() - this.artifactManager.resetFromDisposal() - this.dependencyDiscoverer.resetFromDisposal() + this.isArtifactUploadedToRemoteWorkspace = false // Set up message queue consumer if (this.messageQueueConsumerInterval === undefined) { @@ -350,12 +345,8 @@ export class WorkspaceFolderManager { }, this.MESSAGE_PUBLISH_INTERVAL) } - // Perform a one-time checkRemoteWorkspaceStatusAndReact first - // Pass skipUploads as true since it would be handled by processNewWorkspaceFolders - await this.checkRemoteWorkspaceStatusAndReact(true) - // Set up continuous monitoring which periodically invokes checkRemoteWorkspaceStatusAndReact - if (!this.isOptedOut && !this.isServiceQuotaExceeded && this.continuousMonitorInterval === undefined) { + if (!this.isOptedOut && this.continuousMonitorInterval === undefined) { this.logging.log(`Starting continuous monitor for workspace [${this.workspaceIdentifier}]`) this.continuousMonitorInterval = setInterval(async () => { try { @@ -427,61 +418,92 @@ export class WorkspaceFolderManager { }) } - private async checkRemoteWorkspaceStatusAndReact(skipUploads: boolean = false) { - if (this.workspaceFolders.length === 0) { - this.logging.log(`No workspace folders added, skipping workspace status check`) + public async checkRemoteWorkspaceStatusAndReact() { + if (this.isCheckingRemoteWorkspaceStatus) { + // Skip checking remote workspace if a previous check is still in progress return } + this.isCheckingRemoteWorkspaceStatus = true + try { + if (IdleWorkspaceManager.isSessionIdle()) { + this.resetWebSocketClient() + this.logging.log('Session is idle, skipping remote workspace status check') + return + } - this.logging.log(`Checking remote workspace status for workspace [${this.workspaceIdentifier}]`) - const { metadata, optOut, error } = await this.listWorkspaceMetadata(this.workspaceIdentifier) + if (this.workspaceFolders.length === 0) { + this.logging.log(`No workspace folders added, skipping workspace status check`) + return + } - if (optOut) { - this.logging.log('User opted out, clearing all resources and starting opt-out monitor') - this.isOptedOut = true - this.clearAllWorkspaceResources() - this.startOptOutMonitor() - return - } + this.logging.log(`Checking remote workspace status for workspace [${this.workspaceIdentifier}]`) + const { metadata, optOut, error } = await this.listWorkspaceMetadata(this.workspaceIdentifier) - if (error) { - // Do not do anything if we received an exception but not caused by optOut - return - } + if (optOut) { + this.logging.log('User opted out, clearing all resources and starting opt-out monitor') + this.isOptedOut = true + this.clearAllWorkspaceResources() + this.startOptOutMonitor() + return + } - if (!metadata) { - // Workspace no longer exists, Recreate it. - this.resetRemoteWorkspaceId() // workspaceId would change if remote record is gone - await this.handleWorkspaceCreatedState(skipUploads) - return - } + if (error) { + // Do not do anything if we received an exception but not caused by optOut + return + } - this.workspaceState.remoteWorkspaceState = metadata.workspaceStatus - if (this.workspaceState.workspaceId === undefined) { - this.setRemoteWorkspaceId(metadata.workspaceId) - } + if (!metadata) { + // Workspace no longer exists, Recreate it. + this.resetRemoteWorkspaceId() // workspaceId would change if remote record is gone + await this.handleWorkspaceCreatedState() + return + } - switch (metadata.workspaceStatus) { - case 'READY': - // Check if connection exists - const client = this.workspaceState.webSocketClient - if (!client || !client.isConnected()) { - this.logging.log( - `Workspace is ready but no connection exists or connection lost. Re-establishing connection...` - ) - await this.establishConnection(metadata) - } - break - case 'PENDING': - // Schedule an initial connection when pending - await this.waitForInitialConnection() - break - case 'CREATED': - // Workspace has no environment, Recreate it. - await this.handleWorkspaceCreatedState(skipUploads) - break - default: - this.logging.warn(`Unknown workspace status: ${metadata.workspaceStatus}`) + this.workspaceState.remoteWorkspaceState = metadata.workspaceStatus + if (this.workspaceState.workspaceId === undefined) { + this.setRemoteWorkspaceId(metadata.workspaceId) + } + + switch (metadata.workspaceStatus) { + case 'READY': + // Check if connection exists + const client = this.workspaceState.webSocketClient + if (!client || !client.isConnected()) { + this.logging.log( + `Workspace is ready but no connection exists or connection lost. Re-establishing connection...` + ) + let uploadArtifactsPromise: Promise | undefined + if (!this.isArtifactUploadedToRemoteWorkspace) { + uploadArtifactsPromise = this.uploadAllArtifactsToRemoteWorkspace() + } + await this.establishConnection(metadata) + if (uploadArtifactsPromise) { + await uploadArtifactsPromise + } + } + break + case 'PENDING': + // Schedule an initial connection when pending + let uploadArtifactsPromise: Promise | undefined + if (!this.isArtifactUploadedToRemoteWorkspace) { + uploadArtifactsPromise = this.uploadAllArtifactsToRemoteWorkspace() + } + await this.waitForInitialConnection() + if (uploadArtifactsPromise) { + await uploadArtifactsPromise + } + break + case 'CREATED': + // Workspace has no environment, Recreate it. + await this.handleWorkspaceCreatedState() + break + default: + this.logging.warn(`Unknown workspace status: ${metadata.workspaceStatus}`) + } + } catch (error) { + this.logging.error(`Error checking remote workspace status: ${error}`) + } finally { + this.isCheckingRemoteWorkspaceStatus = false } } @@ -515,9 +537,7 @@ export class WorkspaceFolderManager { ) clearInterval(intervalId) this.optOutMonitorInterval = undefined - this.initializeWorkspaceStatusMonitor().catch(error => { - this.logging.error(`Error while initializing workspace status monitoring: ${error}`) - }) + this.initializeWorkspaceStatusMonitor() this.processNewWorkspaceFolders(this.workspaceFolders).catch(error => { this.logging.error(`Error while processing workspace folders: ${error}`) }) @@ -530,25 +550,18 @@ export class WorkspaceFolderManager { } } - private async handleWorkspaceCreatedState(skipUploads: boolean = false): Promise { + private async handleWorkspaceCreatedState(): Promise { this.logging.log(`No READY / PENDING remote workspace found, creating a new one`) // If remote state is CREATED, call create API to create a new workspace - if (this.workspaceState.webSocketClient) { - this.workspaceState.webSocketClient.destroyClient() - this.workspaceState.webSocketClient = undefined - } + this.resetWebSocketClient() const initialResult = await this.createNewWorkspace() // If creation succeeds, establish connection if (initialResult.response) { this.logging.log(`Workspace [${this.workspaceIdentifier}] created successfully, establishing connection`) + const uploadArtifactsPromise = this.uploadAllArtifactsToRemoteWorkspace() await this.waitForInitialConnection() - if (!skipUploads) { - await this.syncSourceCodesToS3(this.workspaceFolders) - this.dependencyDiscoverer.reSyncDependenciesToS3(this.workspaceFolders).catch(e => { - this.logging.warn(`Error during re-syncing dependencies: ${e}`) - }) - } + await uploadArtifactsPromise return } @@ -570,13 +583,27 @@ export class WorkspaceFolderManager { } this.logging.log(`Retry succeeded for workspace creation, establishing connection`) + const uploadArtifactsPromise = this.uploadAllArtifactsToRemoteWorkspace() await this.waitForInitialConnection() - if (!skipUploads) { - await this.syncSourceCodesToS3(this.workspaceFolders) - this.dependencyDiscoverer.reSyncDependenciesToS3(this.workspaceFolders).catch(e => { - this.logging.warn(`Error during re-syncing dependencies: ${e}`) - }) - } + await uploadArtifactsPromise + } + + private async uploadAllArtifactsToRemoteWorkspace() { + // initialize source codes + this.artifactManager.resetFromDisposal() + await this.syncSourceCodesToS3(this.workspaceFolders) + + // initialize dependencies + this.dependencyDiscoverer.disposeAndReset() + this.dependencyDiscoverer.searchDependencies(this.workspaceFolders).catch(e => { + this.logging.warn(`Error during dependency discovery: ${e}`) + }) + + this.isArtifactUploadedToRemoteWorkspace = true + } + + public isContinuousMonitoringStopped(): boolean { + return this.continuousMonitorInterval === undefined } private stopContinuousMonitoring() { @@ -602,15 +629,15 @@ export class WorkspaceFolderManager { } } - private async createNewWorkspace() { - const createWorkspaceResult = await this.createWorkspace(this.workspaceIdentifier) - - this.isServiceQuotaExceeded = createWorkspaceResult.isServiceQuotaExceeded - if (this.isServiceQuotaExceeded) { - // Stop continuous monitor and all actions - this.clearAllWorkspaceResources() + private resetWebSocketClient() { + if (this.workspaceState.webSocketClient) { + this.workspaceState.webSocketClient.destroyClient() + this.workspaceState.webSocketClient = undefined } + } + private async createNewWorkspace() { + const createWorkspaceResult = await this.createWorkspace(this.workspaceIdentifier) const workspaceDetails = createWorkspaceResult.response if (!workspaceDetails) { this.logging.warn(`Failed to create remote workspace for [${this.workspaceIdentifier}]`) From 8db059ab83d94fd7c3ba3eb265044add31c80aea Mon Sep 17 00:00:00 2001 From: Will Lo <96078566+Will-ShaoHua@users.noreply.github.com> Date: Fri, 8 Aug 2025 15:15:12 -0700 Subject: [PATCH 02/74] fix: sessionManager misused because there are 2 types of manager now (#2090) --- .../language-server/inline-completion/codeWhispererServer.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/server/aws-lsp-codewhisperer/src/language-server/inline-completion/codeWhispererServer.ts b/server/aws-lsp-codewhisperer/src/language-server/inline-completion/codeWhispererServer.ts index 9ed5a2330f..aea5760045 100644 --- a/server/aws-lsp-codewhisperer/src/language-server/inline-completion/codeWhispererServer.ts +++ b/server/aws-lsp-codewhisperer/src/language-server/inline-completion/codeWhispererServer.ts @@ -695,9 +695,9 @@ export const CodewhispererServerFactory = session.suggestionType === SuggestionType.EDIT && isAccepted && session.hasEditsPending if (!shouldKeepSessionOpen) { - completionSessionManager.closeSession(session) + sessionManager.closeSession(session) } - const streakLength = editsEnabled ? completionSessionManager.getAndUpdateStreakLength(isAccepted) : 0 + const streakLength = editsEnabled ? sessionManager.getAndUpdateStreakLength(isAccepted) : 0 await emitUserTriggerDecisionTelemetry( telemetry, telemetryService, From 67e3208084addac08416a5a1bc550a49d998c518 Mon Sep 17 00:00:00 2001 From: Sherry Lu <75588211+XiaoxuanLu@users.noreply.github.com> Date: Fri, 8 Aug 2025 15:42:38 -0700 Subject: [PATCH 03/74] chore: mapping adt plugin to abap to let inline suggestion work (#2085) * chore: support inline suggestion in adt plugin for eclipse * fix: add import URI * chore: add detailed error description for uri is empty for language id --- .../inline-completion/codeWhispererServer.ts | 36 +++++++++++++++---- .../inline-completion/constants.ts | 25 +++++++++++++ 2 files changed, 55 insertions(+), 6 deletions(-) diff --git a/server/aws-lsp-codewhisperer/src/language-server/inline-completion/codeWhispererServer.ts b/server/aws-lsp-codewhisperer/src/language-server/inline-completion/codeWhispererServer.ts index aea5760045..6044a0536f 100644 --- a/server/aws-lsp-codewhisperer/src/language-server/inline-completion/codeWhispererServer.ts +++ b/server/aws-lsp-codewhisperer/src/language-server/inline-completion/codeWhispererServer.ts @@ -55,8 +55,9 @@ import { } from './telemetry' import { DocumentChangedListener } from './documentChangedListener' import { EditCompletionHandler } from './editCompletionHandler' -import { EMPTY_RESULT } from './constants' +import { EMPTY_RESULT, ABAP_EXTENSIONS } from './constants' import { IdleWorkspaceManager } from '../workspaceContext/IdleWorkspaceManager' +import { URI } from 'vscode-uri' const mergeSuggestionsWithRightContext = ( rightFileContext: string, @@ -161,7 +162,7 @@ export const CodewhispererServerFactory = if (cursorTracker) { cursorTracker.trackPosition(params.textDocument.uri, params.position) } - const textDocument = await workspace.getTextDocument(params.textDocument.uri) + const textDocument = await getTextDocument(params.textDocument.uri, workspace, logging) const codeWhispererService = amazonQServiceManager.getCodewhispererService() if (params.partialResultToken && currentSession) { @@ -725,10 +726,9 @@ export const CodewhispererServerFactory = userWrittenCodeTracker.customizationArn = customizationArn } logging.debug(`CodePercentageTracker customizationArn updated to ${customizationArn}`) - /* - The flag enableTelemetryEventsToDestination is set to true temporarily. It's value will be determined through destination - configuration post all events migration to STE. It'll be replaced by qConfig['enableTelemetryEventsToDestination'] === true - */ + + // The flag enableTelemetryEventsToDestination is set to true temporarily. It's value will be determined through destination + // configuration post all events migration to STE. It'll be replaced by qConfig['enableTelemetryEventsToDestination'] === true // const enableTelemetryEventsToDestination = true // telemetryService.updateEnableTelemetryEventsToDestination(enableTelemetryEventsToDestination) telemetryService.updateOptOutPreference(optOutTelemetryPreference) @@ -905,3 +905,27 @@ export const CodewhispererServerFactory = export const CodeWhispererServerIAM = CodewhispererServerFactory(getOrThrowBaseIAMServiceManager) export const CodeWhispererServerToken = CodewhispererServerFactory(getOrThrowBaseTokenServiceManager) + +const getLanguageIdFromUri = (uri: string, logging?: any): string => { + try { + const extension = uri.split('.').pop()?.toLowerCase() + return ABAP_EXTENSIONS.has(extension || '') ? 'abap' : '' + } catch (err) { + logging?.log(`Error parsing URI to determine language: ${uri}: ${err}`) + return '' + } +} + +const getTextDocument = async (uri: string, workspace: any, logging: any): Promise => { + let textDocument = await workspace.getTextDocument(uri) + if (!textDocument) { + try { + const content = await workspace.fs.readFile(URI.parse(uri).fsPath) + const languageId = getLanguageIdFromUri(uri) + textDocument = TextDocument.create(uri, languageId, 0, content) + } catch (err) { + logging.log(`Unable to load from ${uri}: ${err}`) + } + } + return textDocument +} diff --git a/server/aws-lsp-codewhisperer/src/language-server/inline-completion/constants.ts b/server/aws-lsp-codewhisperer/src/language-server/inline-completion/constants.ts index 49a33b35de..c5810924b6 100644 --- a/server/aws-lsp-codewhisperer/src/language-server/inline-completion/constants.ts +++ b/server/aws-lsp-codewhisperer/src/language-server/inline-completion/constants.ts @@ -4,3 +4,28 @@ export const CONTEXT_CHARACTERS_LIMIT = 10240 export const EMPTY_RESULT = { sessionId: '', items: [] } export const EDIT_DEBOUNCE_INTERVAL_MS = 500 export const EDIT_STALE_RETRY_COUNT = 3 +// ABAP ADT extensions commonly used with Eclipse +export const ABAP_EXTENSIONS = new Set([ + 'asprog', + 'aclass', + 'asinc', + 'aint', + 'assrvds', + 'asbdef', + 'asddls', + 'astablds', + 'astabldt', + 'amdp', + 'apack', + 'asrv', + 'aobj', + 'aexit', + 'abdef', + 'acinc', + 'asfugr', + 'apfugr', + 'asfunc', + 'asfinc', + 'apfunc', + 'apfinc', +]) From c65428bab2cf5e47badf1e3a9453babcf881e60c Mon Sep 17 00:00:00 2001 From: Tai Lai Date: Fri, 8 Aug 2025 15:44:00 -0700 Subject: [PATCH 04/74] feat(amazonq): read tool ui revamp * feat(amazonq): read tool message revamp (#2049) * feat(amazonq): read tool message revamp * fix tests * feat: file search ui (#2078) * feat: file search ui * fix tests * fix integration tests * remove unnecessary type check * fix: use quotes instead of backticks --- chat-client/package.json | 2 +- chat-client/src/client/mynahUi.ts | 16 +- .../src/tests/agenticChatInteg.test.ts | 13 +- package-lock.json | 8 +- .../agenticChat/agenticChatController.test.ts | 6 +- .../agenticChat/agenticChatController.ts | 188 ++++++++++++------ .../agenticChat/agenticChatResultStream.ts | 62 ++---- .../agenticChat/tools/fileSearch.ts | 4 + 8 files changed, 167 insertions(+), 132 deletions(-) diff --git a/chat-client/package.json b/chat-client/package.json index ef3ba87421..84743206f8 100644 --- a/chat-client/package.json +++ b/chat-client/package.json @@ -27,7 +27,7 @@ "@aws/chat-client-ui-types": "^0.1.56", "@aws/language-server-runtimes": "^0.2.123", "@aws/language-server-runtimes-types": "^0.1.50", - "@aws/mynah-ui": "^4.36.2" + "@aws/mynah-ui": "^4.36.3" }, "devDependencies": { "@types/jsdom": "^21.1.6", diff --git a/chat-client/src/client/mynahUi.ts b/chat-client/src/client/mynahUi.ts index 3b42249331..527dea06f3 100644 --- a/chat-client/src/client/mynahUi.ts +++ b/chat-client/src/client/mynahUi.ts @@ -1353,10 +1353,15 @@ export const createMynahUi = ( fileTreeTitle: '', hideFileCount: true, details: toDetailsWithoutIcon(header.fileList.details), + renderAsPills: + !header.fileList.details || + (Object.values(header.fileList.details).every(detail => !detail.changes) && + (!header.buttons || !header.buttons.some(button => button.id === 'undo-changes')) && + !header.status?.icon), } } if (!isPartialResult) { - if (processedHeader) { + if (processedHeader && !message.header?.status) { processedHeader.status = undefined } } @@ -1369,7 +1374,8 @@ export const createMynahUi = ( processedHeader.buttons !== null && processedHeader.buttons.length > 0) || processedHeader.status !== undefined || - processedHeader.icon !== undefined) + processedHeader.icon !== undefined || + processedHeader.fileList !== undefined) const padding = message.type === 'tool' ? (fileList ? true : message.messageId?.endsWith('_permission')) : undefined @@ -1380,8 +1386,10 @@ export const createMynahUi = ( // Adding this conditional check to show the stop message in the center. const contentHorizontalAlignment: ChatItem['contentHorizontalAlignment'] = undefined - // If message.header?.status?.text is Stopped or Rejected or Ignored or Completed etc.. card should be in disabled state. - const shouldMute = message.header?.status?.text !== undefined && message.header?.status?.text !== 'Completed' + // If message.header?.status?.text is Stopped or Rejected or Ignored etc.. card should be in disabled state. + const shouldMute = + message.header?.status?.text !== undefined && + ['Stopped', 'Rejected', 'Ignored', 'Failed', 'Error'].includes(message.header.status.text) return { body: message.body, 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 897ee02071..efc0467d2f 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 @@ -169,11 +169,11 @@ describe('Q Agentic Chat Server Integration Tests', async () => { expect(decryptedResult.additionalMessages).to.be.an('array') const fsReadMessage = decryptedResult.additionalMessages?.find( - msg => msg.type === 'tool' && msg.fileList?.rootFolderTitle === '1 file read' + msg => msg.type === 'tool' && msg.header?.body === '1 file read' ) expect(fsReadMessage).to.exist const expectedPath = path.join(rootPath, 'test.py') - const actualPaths = fsReadMessage?.fileList?.filePaths?.map(normalizePath) || [] + const actualPaths = fsReadMessage?.header?.fileList?.filePaths?.map(normalizePath) || [] expect(actualPaths).to.include.members([normalizePath(expectedPath)]) expect(fsReadMessage?.messageId?.startsWith('tooluse_')).to.be.true }) @@ -191,10 +191,10 @@ describe('Q Agentic Chat Server Integration Tests', async () => { expect(decryptedResult.additionalMessages).to.be.an('array') const listDirectoryMessage = decryptedResult.additionalMessages?.find( - msg => msg.type === 'tool' && msg.fileList?.rootFolderTitle === '1 directory listed' + msg => msg.type === 'tool' && msg.header?.body === '1 directory listed' ) expect(listDirectoryMessage).to.exist - const actualPaths = listDirectoryMessage?.fileList?.filePaths?.map(normalizePath) || [] + const actualPaths = listDirectoryMessage?.header?.fileList?.filePaths?.map(normalizePath) || [] expect(actualPaths).to.include.members([normalizePath(rootPath)]) expect(listDirectoryMessage?.messageId?.startsWith('tooluse_')).to.be.true }) @@ -371,11 +371,12 @@ describe('Q Agentic Chat Server Integration Tests', async () => { expect(decryptedResult.additionalMessages).to.be.an('array') const fileSearchMessage = decryptedResult.additionalMessages?.find( - msg => msg.type === 'tool' && msg.fileList?.rootFolderTitle === '1 directory searched' + msg => msg.type === 'tool' && msg.header?.body === 'Searched for `test` in ' ) expect(fileSearchMessage).to.exist expect(fileSearchMessage?.messageId?.startsWith('tooluse_')).to.be.true - const actualPaths = fileSearchMessage?.fileList?.filePaths?.map(normalizePath) || [] + expect(fileSearchMessage?.header?.status?.text).to.equal('3 results found') + const actualPaths = fileSearchMessage?.header?.fileList?.filePaths?.map(normalizePath) || [] expect(actualPaths).to.include.members([normalizePath(rootPath)]) }) }) diff --git a/package-lock.json b/package-lock.json index 9bcaeb0f84..70f43839ad 100644 --- a/package-lock.json +++ b/package-lock.json @@ -257,7 +257,7 @@ "@aws/chat-client-ui-types": "^0.1.56", "@aws/language-server-runtimes": "^0.2.123", "@aws/language-server-runtimes-types": "^0.1.50", - "@aws/mynah-ui": "^4.36.2" + "@aws/mynah-ui": "^4.36.3" }, "devDependencies": { "@types/jsdom": "^21.1.6", @@ -4204,9 +4204,9 @@ "link": true }, "node_modules/@aws/mynah-ui": { - "version": "4.36.2", - "resolved": "https://registry.npmjs.org/@aws/mynah-ui/-/mynah-ui-4.36.2.tgz", - "integrity": "sha512-3ibfK2CTj7dlFFdgTIE1DdEyDpy+P3hdP/Fmlx76T9GGSYiGHqwunDSi59L1P61Kj46WADBrQ52mLUQ6FR8Rzg==", + "version": "4.36.3", + "resolved": "https://registry.npmjs.org/@aws/mynah-ui/-/mynah-ui-4.36.3.tgz", + "integrity": "sha512-E8V65uPUlz2aoN1J21Tg2G3NyExL/glS6dceiTDtKh5me1uoPtxQaTecMqF5LMVfoaE9C0wzAtu/U6GkuZvlMg==", "hasInstallScript": true, "license": "Apache License 2.0", "dependencies": { diff --git a/server/aws-lsp-codewhisperer/src/language-server/agenticChat/agenticChatController.test.ts b/server/aws-lsp-codewhisperer/src/language-server/agenticChat/agenticChatController.test.ts index 6c8af7a272..616848d0b5 100644 --- a/server/aws-lsp-codewhisperer/src/language-server/agenticChat/agenticChatController.test.ts +++ b/server/aws-lsp-codewhisperer/src/language-server/agenticChat/agenticChatController.test.ts @@ -451,7 +451,7 @@ describe('AgenticChatController', () => { assert.deepStrictEqual(chatResult, { additionalMessages: [], - body: '\n\nHello World!', + body: '\nHello World!', messageId: 'mock-message-id', buttons: [], codeReference: [], @@ -1150,7 +1150,7 @@ describe('AgenticChatController', () => { sinon.assert.callCount(testFeatures.lsp.sendProgress, mockChatResponseList.length + 1) // response length + 1 loading messages assert.deepStrictEqual(chatResult, { additionalMessages: [], - body: '\n\nHello World!', + body: '\nHello World!', messageId: 'mock-message-id', codeReference: [], buttons: [], @@ -1169,7 +1169,7 @@ describe('AgenticChatController', () => { sinon.assert.callCount(testFeatures.lsp.sendProgress, mockChatResponseList.length + 1) // response length + 1 loading message assert.deepStrictEqual(chatResult, { additionalMessages: [], - body: '\n\nHello World!', + body: '\nHello World!', messageId: 'mock-message-id', buttons: [], codeReference: [], 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 72217fae07..09bf965819 100644 --- a/server/aws-lsp-codewhisperer/src/language-server/agenticChat/agenticChatController.ts +++ b/server/aws-lsp-codewhisperer/src/language-server/agenticChat/agenticChatController.ts @@ -169,7 +169,7 @@ import { ExecuteBash, ExecuteBashParams } from './tools/executeBash' import { ExplanatoryParams, InvokeOutput, ToolApprovalException } from './tools/toolShared' import { validatePathBasic, validatePathExists, validatePaths as validatePathsSync } from './utils/pathValidation' import { GrepSearch, SanitizedRipgrepOutput } from './tools/grepSearch' -import { FileSearch, FileSearchParams } from './tools/fileSearch' +import { FileSearch, FileSearchParams, isFileSearchParams } from './tools/fileSearch' import { FsReplace, FsReplaceParams } from './tools/fsReplace' import { loggingUtils, timeoutUtils } from '@aws/lsp-core' import { diffLines } from 'diff' @@ -1695,8 +1695,7 @@ export class AgenticChatController implements ChatHandlers { // remove progress UI await chatResultStream.removeResultBlockAndUpdateUI(progressPrefix + toolUse.toolUseId) - // fsRead and listDirectory write to an existing card and could show nothing in the current position - if (![FS_WRITE, FS_REPLACE, FS_READ, LIST_DIRECTORY].includes(toolUse.name)) { + if (![FS_WRITE, FS_REPLACE].includes(toolUse.name)) { await this.#showUndoAllIfRequired(chatResultStream, session) } // fsWrite can take a long time, so we render fsWrite Explanatory upon partial streaming responses. @@ -1911,10 +1910,19 @@ export class AgenticChatController implements ChatHandlers { switch (toolUse.name) { case FS_READ: case LIST_DIRECTORY: + const readToolResult = await this.#processReadTool(toolUse, chatResultStream) + if (readToolResult) { + await chatResultStream.writeResultBlock(readToolResult) + } + break case FILE_SEARCH: - const initialListDirResult = this.#processReadOrListOrSearch(toolUse, chatResultStream) - if (initialListDirResult) { - await chatResultStream.writeResultBlock(initialListDirResult) + if (isFileSearchParams(toolUse.input)) { + await this.#processFileSearchTool( + toolUse.input, + toolUse.toolUseId, + result, + chatResultStream + ) } break // no need to write tool result for listDir,fsRead,fileSearch into chat stream @@ -2315,7 +2323,6 @@ export class AgenticChatController implements ChatHandlers { } const toolMsgId = toolUse.toolUseId! - const chatMsgId = chatResultStream.getResult().messageId let headerEmitted = false const initialHeader: ChatMessage['header'] = { @@ -2353,13 +2360,6 @@ export class AgenticChatController implements ChatHandlers { header: completedHeader, }) - await chatResultStream.writeResultBlock({ - type: 'answer', - messageId: chatMsgId, - body: '', - header: undefined, - }) - this.#stoppedToolUses.add(toolMsgId) }, }) @@ -2877,70 +2877,135 @@ export class AgenticChatController implements ChatHandlers { } } - #processReadOrListOrSearch(toolUse: ToolUse, chatResultStream: AgenticChatResultStream): ChatMessage | undefined { - let messageIdToUpdate = toolUse.toolUseId! - const currentId = chatResultStream.getMessageIdToUpdateForTool(toolUse.name!) + async #processFileSearchTool( + toolInput: FileSearchParams, + toolUseId: string, + result: InvokeOutput, + chatResultStream: AgenticChatResultStream + ): Promise { + if (typeof result.output.content !== 'string') return - if (currentId) { - messageIdToUpdate = currentId - } else { - chatResultStream.setMessageIdToUpdateForTool(toolUse.name!, messageIdToUpdate) + const { queryName, path: inputPath } = toolInput + const resultCount = result.output.content + .split('\n') + .filter(line => line.trim().startsWith('[F]') || line.trim().startsWith('[D]')).length + + const chatMessage: ChatMessage = { + type: 'tool', + messageId: toolUseId, + header: { + body: `Searched for "${queryName}" in `, + icon: 'search', + status: { + text: `${resultCount} result${resultCount !== 1 ? 's' : ''} found`, + }, + fileList: { + filePaths: [inputPath], + details: { + [inputPath]: { + description: inputPath, + visibleName: path.basename(inputPath), + clickable: false, + }, + }, + }, + }, } - let currentPaths = [] + await chatResultStream.writeResultBlock(chatMessage) + } + + async #processReadTool( + toolUse: ToolUse, + chatResultStream: AgenticChatResultStream + ): Promise { + let currentPaths: string[] = [] if (toolUse.name === FS_READ) { - currentPaths = (toolUse.input as unknown as FsReadParams)?.paths + currentPaths = (toolUse.input as unknown as FsReadParams)?.paths || [] + } else if (toolUse.name === LIST_DIRECTORY) { + const singlePath = (toolUse.input as unknown as ListDirectoryParams)?.path + if (singlePath) { + currentPaths = [singlePath] + } + } else if (toolUse.name === FILE_SEARCH) { + const queryName = (toolUse.input as unknown as FileSearchParams)?.queryName + if (queryName) { + currentPaths = [queryName] + } } else { - currentPaths.push((toolUse.input as unknown as ListDirectoryParams | FileSearchParams)?.path) + return } - if (!currentPaths) return + if (currentPaths.length === 0) return - for (const currentPath of currentPaths) { - const existingPaths = chatResultStream.getMessageOperation(messageIdToUpdate)?.filePaths || [] - // Check if path already exists in the list - const isPathAlreadyProcessed = existingPaths.some(path => path.relativeFilePath === currentPath) - if (!isPathAlreadyProcessed) { - const currentFileDetail = { - relativeFilePath: currentPath, - lineRanges: [{ first: -1, second: -1 }], - } - chatResultStream.addMessageOperation(messageIdToUpdate, toolUse.name!, [ - ...existingPaths, - currentFileDetail, - ]) + // Check if the last message is the same tool type + const lastMessage = chatResultStream.getLastMessage() + const isSameToolType = + lastMessage?.type === 'tool' && lastMessage.header?.icon === this.#toolToIcon(toolUse.name) + + let allPaths = currentPaths + + if (isSameToolType && lastMessage.messageId) { + // Combine with existing paths and overwrite the last message + const existingPaths = lastMessage.header?.fileList?.filePaths || [] + allPaths = [...existingPaths, ...currentPaths] + + const blockId = chatResultStream.getMessageBlockId(lastMessage.messageId) + if (blockId !== undefined) { + // Create the updated message with combined paths + const updatedMessage = this.#createFileListToolMessage(toolUse, allPaths, lastMessage.messageId) + // Overwrite the existing block + await chatResultStream.overwriteResultBlock(updatedMessage, blockId) + return undefined // Don't return a message since we already wrote it } } + + // Create new message with current paths + return this.#createFileListToolMessage(toolUse, allPaths, toolUse.toolUseId!) + } + + #createFileListToolMessage(toolUse: ToolUse, filePaths: string[], messageId: string): ChatMessage { + const itemCount = filePaths.length let title: string - const itemCount = chatResultStream.getMessageOperation(messageIdToUpdate)?.filePaths.length - const filePathsPushed = chatResultStream.getMessageOperation(messageIdToUpdate)?.filePaths ?? [] - if (!itemCount) { + if (itemCount === 0) { title = 'Gathering context' } else { title = toolUse.name === FS_READ ? `${itemCount} file${itemCount > 1 ? 's' : ''} read` - : toolUse.name === FILE_SEARCH - ? `${itemCount} ${itemCount === 1 ? 'directory' : 'directories'} searched` - : `${itemCount} ${itemCount === 1 ? 'directory' : 'directories'} listed` + : toolUse.name === LIST_DIRECTORY + ? `${itemCount} ${itemCount === 1 ? 'directory' : 'directories'} listed` + : '' } const details: Record = {} - for (const item of filePathsPushed) { - details[item.relativeFilePath] = { - lineRanges: item.lineRanges, - description: item.relativeFilePath, + for (const filePath of filePaths) { + details[filePath] = { + description: filePath, + visibleName: path.basename(filePath), + clickable: toolUse.name === FS_READ, } } - - const fileList: FileList = { - rootFolderTitle: title, - filePaths: filePathsPushed.map(item => item.relativeFilePath), - details, - } return { type: 'tool', - fileList, - messageId: messageIdToUpdate, - body: '', + header: { + body: title, + icon: this.#toolToIcon(toolUse.name), + fileList: { + filePaths, + details, + }, + }, + messageId, + } + } + + #toolToIcon(toolName: string | undefined): string | undefined { + switch (toolName) { + case FS_READ: + return 'eye' + case LIST_DIRECTORY: + return 'check-list' + default: + return undefined } } @@ -2956,14 +3021,7 @@ export class AgenticChatController implements ChatHandlers { return undefined } - let messageIdToUpdate = toolUse.toolUseId! - const currentId = chatResultStream.getMessageIdToUpdateForTool(toolUse.name!) - - if (currentId) { - messageIdToUpdate = currentId - } else { - chatResultStream.setMessageIdToUpdateForTool(toolUse.name!, messageIdToUpdate) - } + const messageIdToUpdate = toolUse.toolUseId! // Extract search results from the tool output const output = result.output.content as SanitizedRipgrepOutput diff --git a/server/aws-lsp-codewhisperer/src/language-server/agenticChat/agenticChatResultStream.ts b/server/aws-lsp-codewhisperer/src/language-server/agenticChat/agenticChatResultStream.ts index 70b3452361..5fb5c39bab 100644 --- a/server/aws-lsp-codewhisperer/src/language-server/agenticChat/agenticChatResultStream.ts +++ b/server/aws-lsp-codewhisperer/src/language-server/agenticChat/agenticChatResultStream.ts @@ -1,4 +1,4 @@ -import { ChatResult, FileDetails, ChatMessage } from '@aws/language-server-runtimes/protocol' +import { ChatResult, ChatMessage } from '@aws/language-server-runtimes/protocol' import { randomUUID } from 'crypto' export interface ResultStreamWriter { @@ -32,33 +32,20 @@ export interface ResultStreamWriter { close(): Promise } +export const progressPrefix = 'progress_' + /** * This class wraps around lsp.sendProgress to provide a more helpful interface for streaming a ChatResult to the client. * ChatResults are grouped into blocks that can be written directly, or streamed in. * In the final message, blocks are seperated by resultDelimiter defined below. */ - -interface FileDetailsWithPath extends FileDetails { - relativeFilePath: string -} - -type OperationType = 'read' | 'write' | 'listDir' - -export const progressPrefix = 'progress_' - -interface FileOperation { - type: OperationType - filePaths: FileDetailsWithPath[] -} export class AgenticChatResultStream { - static readonly resultDelimiter = '\n\n' + static readonly resultDelimiter = '\n' #state = { chatResultBlocks: [] as ChatMessage[], isLocked: false, uuid: randomUUID(), messageId: undefined as string | undefined, - messageIdToUpdateForTool: new Map(), - messageOperations: new Map(), } readonly #sendProgress: (newChatResult: ChatResult | string) => Promise @@ -70,33 +57,6 @@ export class AgenticChatResultStream { return this.#joinResults(this.#state.chatResultBlocks, only) } - setMessageIdToUpdateForTool(toolName: string, messageId: string) { - this.#state.messageIdToUpdateForTool.set(toolName as OperationType, messageId) - } - - getMessageIdToUpdateForTool(toolName: string): string | undefined { - return this.#state.messageIdToUpdateForTool.get(toolName as OperationType) - } - - /** - * Adds a file operation for a specific message - * @param messageId The ID of the message - * @param type The type of operation ('fsRead' or 'listDirectory' or 'fsWrite') - * @param filePaths Array of FileDetailsWithPath involved in the operation - */ - addMessageOperation(messageId: string, type: string, filePaths: FileDetailsWithPath[]) { - this.#state.messageOperations.set(messageId, { type: type as OperationType, filePaths }) - } - - /** - * Gets the file operation details for a specific message - * @param messageId The ID of the message - * @returns The file operation details or undefined if not found - */ - getMessageOperation(messageId: string): FileOperation | undefined { - return this.#state.messageOperations.get(messageId) - } - #joinResults(chatResults: ChatMessage[], only?: string): ChatResult { const result: ChatResult = { body: '', @@ -111,9 +71,9 @@ export class AgenticChatResultStream { return { ...acc, buttons: [...(acc.buttons ?? []), ...(c.buttons ?? [])], - body: acc.body + AgenticChatResultStream.resultDelimiter + c.body, - ...(c.contextList && { contextList: c.contextList }), - header: Object.prototype.hasOwnProperty.call(c, 'header') ? c.header : acc.header, + body: acc.body + (c.body ? AgenticChatResultStream.resultDelimiter + c.body : ''), + ...(c.contextList && c.type !== 'tool' && { contextList: c.contextList }), + header: c.header !== undefined ? c.header : acc.header, codeReference: [...(acc.codeReference ?? []), ...(c.codeReference ?? [])], } } else if (acc.additionalMessages!.some(am => am.messageId === c.messageId)) { @@ -127,7 +87,7 @@ export class AgenticChatResultStream { : am.buttons, body: am.messageId === c.messageId - ? am.body + AgenticChatResultStream.resultDelimiter + c.body + ? am.body + (c.body ? AgenticChatResultStream.resultDelimiter + c.body : '') : am.body, ...(am.messageId === c.messageId && (c.contextList || acc.contextList) && { @@ -161,7 +121,7 @@ export class AgenticChatResultStream { }, }, }), - header: Object.prototype.hasOwnProperty.call(c, 'header') ? c.header : am.header, + ...(am.messageId === c.messageId && c.header !== undefined && { header: c.header }), })), } } else { @@ -246,6 +206,10 @@ export class AgenticChatResultStream { return undefined } + getLastMessage(): ChatMessage { + return this.#state.chatResultBlocks[this.#state.chatResultBlocks.length - 1] + } + getResultStreamWriter(): ResultStreamWriter { // Note: if write calls are not awaited, stream can be out-of-order. if (this.#state.isLocked) { diff --git a/server/aws-lsp-codewhisperer/src/language-server/agenticChat/tools/fileSearch.ts b/server/aws-lsp-codewhisperer/src/language-server/agenticChat/tools/fileSearch.ts index fb6486996e..37d11afe4f 100644 --- a/server/aws-lsp-codewhisperer/src/language-server/agenticChat/tools/fileSearch.ts +++ b/server/aws-lsp-codewhisperer/src/language-server/agenticChat/tools/fileSearch.ts @@ -158,3 +158,7 @@ export class FileSearch { } as const } } + +export function isFileSearchParams(input: any): input is FileSearchParams { + return input && typeof input.path === 'string' && typeof input.queryName === 'string' +} From 1da8730b6ed6ad53b6561368bf722e56d59596a4 Mon Sep 17 00:00:00 2001 From: atontb <104926752+atonaamz@users.noreply.github.com> Date: Fri, 8 Aug 2025 16:19:13 -0700 Subject: [PATCH 05/74] fix: creating a new sesion for Edits trigger with next token (#2094) --- .../inline-completion/codeWhispererServer.ts | 9 +------ .../editCompletionHandler.ts | 25 ++++++++++++++++--- .../session/sessionManager.ts | 1 - .../inline-completion/telemetry.ts | 8 +----- 4 files changed, 24 insertions(+), 19 deletions(-) diff --git a/server/aws-lsp-codewhisperer/src/language-server/inline-completion/codeWhispererServer.ts b/server/aws-lsp-codewhisperer/src/language-server/inline-completion/codeWhispererServer.ts index 6044a0536f..56cda299f8 100644 --- a/server/aws-lsp-codewhisperer/src/language-server/inline-completion/codeWhispererServer.ts +++ b/server/aws-lsp-codewhisperer/src/language-server/inline-completion/codeWhispererServer.ts @@ -502,7 +502,6 @@ export const CodewhispererServerFactory = partialResultToken: suggestionResponse.responseContext.nextToken, } } else { - session.hasEditsPending = suggestionResponse.responseContext.nextToken ? true : false return { items: suggestionResponse.suggestions .map(suggestion => { @@ -691,13 +690,7 @@ export const CodewhispererServerFactory = if (firstCompletionDisplayLatency) emitPerceivedLatencyTelemetry(telemetry, session) // Always emit user trigger decision at session close - // Close session unless Edit suggestion was accepted with more pending - const shouldKeepSessionOpen = - session.suggestionType === SuggestionType.EDIT && isAccepted && session.hasEditsPending - - if (!shouldKeepSessionOpen) { - sessionManager.closeSession(session) - } + sessionManager.closeSession(session) const streakLength = editsEnabled ? sessionManager.getAndUpdateStreakLength(isAccepted) : 0 await emitUserTriggerDecisionTelemetry( telemetry, diff --git a/server/aws-lsp-codewhisperer/src/language-server/inline-completion/editCompletionHandler.ts b/server/aws-lsp-codewhisperer/src/language-server/inline-completion/editCompletionHandler.ts index 5a6f8644b1..3c72e368f4 100644 --- a/server/aws-lsp-codewhisperer/src/language-server/inline-completion/editCompletionHandler.ts +++ b/server/aws-lsp-codewhisperer/src/language-server/inline-completion/editCompletionHandler.ts @@ -120,16 +120,35 @@ export class EditCompletionHandler { } if (params.partialResultToken && currentSession) { + // Close ACTIVE session. We shouldn't record Discard trigger decision for trigger with nextToken. + if (currentSession && currentSession.state === 'ACTIVE') { + this.sessionManager.discardSession(currentSession) + } + + const newSession = this.sessionManager.createSession({ + document: textDocument, + startPosition: params.position, + triggerType: 'AutoTrigger', + language: currentSession.language, + requestContext: currentSession.requestContext, + autoTriggerType: undefined, + triggerCharacter: '', + classifierResult: undefined, + classifierThreshold: undefined, + credentialStartUrl: currentSession.credentialStartUrl, + supplementalMetadata: currentSession.supplementalMetadata, + customizationArn: currentSession.customizationArn, + }) // subsequent paginated requests for current session try { const suggestionResponse = await this.codeWhispererService.generateSuggestions({ - ...currentSession.requestContext, + ...newSession.requestContext, nextToken: `${params.partialResultToken}`, }) return await this.processSuggestionResponse( suggestionResponse, - currentSession, - false, + newSession, + true, params.context.selectedCompletionInfo?.range ) } catch (error) { diff --git a/server/aws-lsp-codewhisperer/src/language-server/inline-completion/session/sessionManager.ts b/server/aws-lsp-codewhisperer/src/language-server/inline-completion/session/sessionManager.ts index 8ac71737cf..cb873a2920 100644 --- a/server/aws-lsp-codewhisperer/src/language-server/inline-completion/session/sessionManager.ts +++ b/server/aws-lsp-codewhisperer/src/language-server/inline-completion/session/sessionManager.ts @@ -80,7 +80,6 @@ export class CodeWhispererSession { includeImportsWithSuggestions?: boolean codewhispererSuggestionImportCount: number = 0 suggestionType?: string - hasEditsPending?: boolean = false // Track the most recent itemId for paginated Edit suggestions constructor(data: SessionData) { diff --git a/server/aws-lsp-codewhisperer/src/language-server/inline-completion/telemetry.ts b/server/aws-lsp-codewhisperer/src/language-server/inline-completion/telemetry.ts index b990ccfceb..d53d141a2b 100644 --- a/server/aws-lsp-codewhisperer/src/language-server/inline-completion/telemetry.ts +++ b/server/aws-lsp-codewhisperer/src/language-server/inline-completion/telemetry.ts @@ -147,13 +147,7 @@ export const emitUserTriggerDecisionTelemetry = async ( streakLength ) - // Mark telemetry as complete unless Edit suggestion was accepted with more pending - const hasPendingEditTelemetry = - session.suggestionType === SuggestionType.EDIT && session.acceptedSuggestionId && session.hasEditsPending - - if (!hasPendingEditTelemetry) { - session.reportedUserDecision = true - } + session.reportedUserDecision = true } export const emitAggregatedUserTriggerDecisionTelemetry = ( From c759910226fa9f684d2ddac12237f4f298a72f84 Mon Sep 17 00:00:00 2001 From: Tai Lai Date: Fri, 8 Aug 2025 17:06:39 -0700 Subject: [PATCH 06/74] chore: bump @aws/mynah-ui to 4.36.4 (#2096) --- chat-client/package.json | 2 +- package-lock.json | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/chat-client/package.json b/chat-client/package.json index 84743206f8..161cb50a0b 100644 --- a/chat-client/package.json +++ b/chat-client/package.json @@ -27,7 +27,7 @@ "@aws/chat-client-ui-types": "^0.1.56", "@aws/language-server-runtimes": "^0.2.123", "@aws/language-server-runtimes-types": "^0.1.50", - "@aws/mynah-ui": "^4.36.3" + "@aws/mynah-ui": "^4.36.4" }, "devDependencies": { "@types/jsdom": "^21.1.6", diff --git a/package-lock.json b/package-lock.json index 70f43839ad..6b55b375a3 100644 --- a/package-lock.json +++ b/package-lock.json @@ -257,7 +257,7 @@ "@aws/chat-client-ui-types": "^0.1.56", "@aws/language-server-runtimes": "^0.2.123", "@aws/language-server-runtimes-types": "^0.1.50", - "@aws/mynah-ui": "^4.36.3" + "@aws/mynah-ui": "^4.36.4" }, "devDependencies": { "@types/jsdom": "^21.1.6", @@ -4204,9 +4204,9 @@ "link": true }, "node_modules/@aws/mynah-ui": { - "version": "4.36.3", - "resolved": "https://registry.npmjs.org/@aws/mynah-ui/-/mynah-ui-4.36.3.tgz", - "integrity": "sha512-E8V65uPUlz2aoN1J21Tg2G3NyExL/glS6dceiTDtKh5me1uoPtxQaTecMqF5LMVfoaE9C0wzAtu/U6GkuZvlMg==", + "version": "4.36.4", + "resolved": "https://registry.npmjs.org/@aws/mynah-ui/-/mynah-ui-4.36.4.tgz", + "integrity": "sha512-vGW4wlNindpr2Ep9x3iuKbrZTXe5KrE8vWpg15DjkN3qK42KMuMEQ67Pqtfgl5EseNYC1ukZm4HIQIMmt+vevA==", "hasInstallScript": true, "license": "Apache License 2.0", "dependencies": { From 40379a887f8d42cc184239ca3175b4e673cc5286 Mon Sep 17 00:00:00 2001 From: andrewyuq <89420755+andrewyuq@users.noreply.github.com> Date: Mon, 11 Aug 2025 10:32:02 -0700 Subject: [PATCH 07/74] fix(amazonq): leverage lcs to find the chars added and removed (#2092) --- .../inline-completion/codePercentage.ts | 14 +++- .../inline-completion/codeWhispererServer.ts | 57 +++++++--------- .../inline-completion/diffUtils.ts | 67 +++++++++++++++++++ 3 files changed, 104 insertions(+), 34 deletions(-) diff --git a/server/aws-lsp-codewhisperer/src/language-server/inline-completion/codePercentage.ts b/server/aws-lsp-codewhisperer/src/language-server/inline-completion/codePercentage.ts index 04c7dc5ab1..0513443a35 100644 --- a/server/aws-lsp-codewhisperer/src/language-server/inline-completion/codePercentage.ts +++ b/server/aws-lsp-codewhisperer/src/language-server/inline-completion/codePercentage.ts @@ -137,10 +137,20 @@ export class CodePercentageTracker { } } + addTotalTokensForEdits(languageId: string, count: number): void { + const languageBucket = this.getLanguageBucket(languageId) + if (count >= INSERT_CUTOFF_THRESHOLD) { + languageBucket.totalTokens += count + } + } + countAcceptedTokens(languageId: string, tokens: string): void { + this.countAcceptedTokensUsingCount(languageId, tokens.length) + } + + countAcceptedTokensUsingCount(languageId: string, count: number): void { const languageBucket = this.getLanguageBucket(languageId) - const tokenCount = tokens.length - languageBucket.acceptedTokens += tokenCount + languageBucket.acceptedTokens += count } countInvocation(languageId: string): void { diff --git a/server/aws-lsp-codewhisperer/src/language-server/inline-completion/codeWhispererServer.ts b/server/aws-lsp-codewhisperer/src/language-server/inline-completion/codeWhispererServer.ts index 56cda299f8..23a72e3528 100644 --- a/server/aws-lsp-codewhisperer/src/language-server/inline-completion/codeWhispererServer.ts +++ b/server/aws-lsp-codewhisperer/src/language-server/inline-completion/codeWhispererServer.ts @@ -46,7 +46,7 @@ import { UserWrittenCodeTracker } from '../../shared/userWrittenCodeTracker' import { RecentEditTracker, RecentEditTrackerDefaultConfig } from './tracker/codeEditTracker' import { CursorTracker } from './tracker/cursorTracker' import { RejectedEditTracker, DEFAULT_REJECTED_EDIT_TRACKER_CONFIG } from './tracker/rejectedEditTracker' -import { getAddedAndDeletedChars } from './diffUtils' +import { getAddedAndDeletedLines, getCharacterDifferences } from './diffUtils' import { emitPerceivedLatencyTelemetry, emitServiceInvocationFailure, @@ -622,36 +622,29 @@ export const CodewhispererServerFactory = ) const isAccepted = acceptedItemId ? true : false const acceptedSuggestion = session.suggestions.find(s => s.itemId === acceptedItemId) - let addedCharactersForEditSuggestion = '' - let deletedCharactersForEditSuggestion = '' - if (acceptedSuggestion !== undefined) { - if (acceptedSuggestion) { - codePercentageTracker.countSuccess(session.language) - if (session.suggestionType === SuggestionType.EDIT && acceptedSuggestion.content) { - // [acceptedSuggestion.insertText] will be undefined for NEP suggestion. Use [acceptedSuggestion.content] instead. - // Since [acceptedSuggestion.content] is in the form of a diff, transform the content into addedCharacters and deletedCharacters. - const addedAndDeletedChars = getAddedAndDeletedChars(acceptedSuggestion.content) - if (addedAndDeletedChars) { - addedCharactersForEditSuggestion = addedAndDeletedChars.addedCharacters - deletedCharactersForEditSuggestion = addedAndDeletedChars.deletedCharacters - - codePercentageTracker.countAcceptedTokens( - session.language, - addedCharactersForEditSuggestion - ) - codePercentageTracker.countTotalTokens( - session.language, - addedCharactersForEditSuggestion, - true - ) - enqueueCodeDiffEntry(session, acceptedSuggestion, addedCharactersForEditSuggestion) - } - } else if (acceptedSuggestion.insertText) { - codePercentageTracker.countAcceptedTokens(session.language, acceptedSuggestion.insertText) - codePercentageTracker.countTotalTokens(session.language, acceptedSuggestion.insertText, true) + let addedLengthForEdits = 0 + let deletedLengthForEdits = 0 + if (acceptedSuggestion) { + codePercentageTracker.countSuccess(session.language) + if (session.suggestionType === SuggestionType.EDIT && acceptedSuggestion.content) { + // [acceptedSuggestion.insertText] will be undefined for NEP suggestion. Use [acceptedSuggestion.content] instead. + // Since [acceptedSuggestion.content] is in the form of a diff, transform the content into addedCharacters and deletedCharacters. + const { addedLines, deletedLines } = getAddedAndDeletedLines(acceptedSuggestion.content) + const charDiffResult = getCharacterDifferences(addedLines, deletedLines) + addedLengthForEdits = charDiffResult.charactersAdded + deletedLengthForEdits = charDiffResult.charactersRemoved + + codePercentageTracker.countAcceptedTokensUsingCount( + session.language, + charDiffResult.charactersAdded + ) + codePercentageTracker.addTotalTokensForEdits(session.language, charDiffResult.charactersAdded) + enqueueCodeDiffEntry(session, acceptedSuggestion, addedLines.join('\n')) + } else if (acceptedSuggestion.insertText) { + codePercentageTracker.countAcceptedTokens(session.language, acceptedSuggestion.insertText) + codePercentageTracker.countTotalTokens(session.language, acceptedSuggestion.insertText, true) - enqueueCodeDiffEntry(session, acceptedSuggestion) - } + enqueueCodeDiffEntry(session, acceptedSuggestion) } } @@ -697,8 +690,8 @@ export const CodewhispererServerFactory = telemetryService, session, timeSinceLastUserModification, - addedCharactersForEditSuggestion.length, - deletedCharactersForEditSuggestion.length, + addedLengthForEdits, + deletedLengthForEdits, addedDiagnostics, removedDiagnostics, streakLength, diff --git a/server/aws-lsp-codewhisperer/src/language-server/inline-completion/diffUtils.ts b/server/aws-lsp-codewhisperer/src/language-server/inline-completion/diffUtils.ts index 66cbeb3f23..0899dd5e46 100644 --- a/server/aws-lsp-codewhisperer/src/language-server/inline-completion/diffUtils.ts +++ b/server/aws-lsp-codewhisperer/src/language-server/inline-completion/diffUtils.ts @@ -213,6 +213,18 @@ export function applyUnifiedDiff(docText: string, unifiedDiff: string): string { } } +export function getAddedAndDeletedLines(unifiedDiff: string): { addedLines: string[]; deletedLines: string[] } { + const lines = unifiedDiff.split('\n') + const addedLines = lines.filter(line => line.startsWith('+') && !line.startsWith('+++')).map(line => line.slice(1)) + const deletedLines = lines + .filter(line => line.startsWith('-') && !line.startsWith('---')) + .map(line => line.slice(1)) + return { + addedLines, + deletedLines, + } +} + // src https://github.com/aws/aws-toolkit-vscode/blob/3921457b0a2094b831beea0d66cc2cbd2a833890/packages/amazonq/src/app/inline/EditRendering/diffUtils.ts#L147 export function getAddedAndDeletedChars(unifiedDiff: string): { addedCharacters: string @@ -254,3 +266,58 @@ export function getAddedAndDeletedChars(unifiedDiff: string): { deletedCharacters, } } + +/** + * Calculate character differences between added and deleted text blocks using LCS + */ +export interface CharDiffResult { + charactersAdded: number + charactersRemoved: number +} + +/** + * Find longest common subsequence length between two strings + */ +function lcsLength(str1: string, str2: string): number[][] { + const m = str1.length + const n = str2.length + const dp = Array(m + 1) + .fill(null) + .map(() => Array(n + 1).fill(0)) + + for (let i = 1; i <= m; i++) { + for (let j = 1; j <= n; j++) { + if (str1[i - 1] === str2[j - 1]) { + dp[i][j] = dp[i - 1][j - 1] + 1 + } else { + dp[i][j] = Math.max(dp[i - 1][j], dp[i][j - 1]) + } + } + } + + return dp +} + +/** + * Calculate character differences between added and deleted blocks + */ +export function getCharacterDifferences(addedLines: string[], deletedLines: string[]): CharDiffResult { + const addedText = addedLines.join('\n') + const deletedText = deletedLines.join('\n') + + if (addedText.length === 0) { + return { charactersAdded: 0, charactersRemoved: deletedText.length } + } + + if (deletedText.length === 0) { + return { charactersAdded: addedText.length, charactersRemoved: 0 } + } + + const lcsTable = lcsLength(deletedText, addedText) + const lcsLen = lcsTable[deletedText.length][addedText.length] + + return { + charactersAdded: addedText.length - lcsLen, + charactersRemoved: deletedText.length - lcsLen, + } +} From a746fe845d5e09563b475f01ce44059dca9fd10f Mon Sep 17 00:00:00 2001 From: Jayakrishna P Date: Mon, 11 Aug 2025 10:54:35 -0700 Subject: [PATCH 08/74] fix: update client name to support Sagemaker AI origin for agentic chat (#2093) Co-authored-by: chungjac --- .../src/shared/utils.test.ts | 18 ++++++++++++++---- .../aws-lsp-codewhisperer/src/shared/utils.ts | 7 ++++++- 2 files changed, 20 insertions(+), 5 deletions(-) diff --git a/server/aws-lsp-codewhisperer/src/shared/utils.test.ts b/server/aws-lsp-codewhisperer/src/shared/utils.test.ts index e83d04bbb3..85cd8817a7 100644 --- a/server/aws-lsp-codewhisperer/src/shared/utils.test.ts +++ b/server/aws-lsp-codewhisperer/src/shared/utils.test.ts @@ -134,26 +134,36 @@ describe('getClientName', () => { }) describe('getOriginFromClientInfo', () => { - it('returns MD_IDE for SMUS-IDE client name', () => { + it('returns MD_IDE for client names starting with SMUS-IDE prefix', () => { const result = getOriginFromClientInfo('AmazonQ-For-SMUS-IDE-1.0.0') assert.strictEqual(result, 'MD_IDE') }) - it('returns MD_IDE for SMUS-CE client name', () => { + it('returns MD_IDE for client names starting with SMUS-CE prefix', () => { const result = getOriginFromClientInfo('AmazonQ-For-SMUS-CE-1.0.0') assert.strictEqual(result, 'MD_IDE') }) - it('returns MD_IDE for client names starting with SMUS-IDE prefix', () => { + it('returns MD_IDE for client names starting with SMAI-CE prefix', () => { + const result = getOriginFromClientInfo('AmazonQ-For-SMAI-CE-1.0.0') + assert.strictEqual(result, 'MD_IDE') + }) + + it('returns MD_IDE for SMUS-IDE client name', () => { const result = getOriginFromClientInfo('AmazonQ-For-SMUS-IDE') assert.strictEqual(result, 'MD_IDE') }) - it('returns MD_IDE for client names starting with SMUS-CE prefix', () => { + it('returns MD_IDE for SMUS-CE client name', () => { const result = getOriginFromClientInfo('AmazonQ-For-SMUS-CE') assert.strictEqual(result, 'MD_IDE') }) + it('returns MD_IDE for SMAI-CE client name', () => { + const result = getOriginFromClientInfo('AmazonQ-For-SMAI-CE') + assert.strictEqual(result, 'MD_IDE') + }) + it('returns IDE for non-SMUS client name', () => { const result = getOriginFromClientInfo('VSCode-Extension') assert.strictEqual(result, 'IDE') diff --git a/server/aws-lsp-codewhisperer/src/shared/utils.ts b/server/aws-lsp-codewhisperer/src/shared/utils.ts index e178de4a51..a7a95d8801 100644 --- a/server/aws-lsp-codewhisperer/src/shared/utils.ts +++ b/server/aws-lsp-codewhisperer/src/shared/utils.ts @@ -381,7 +381,12 @@ export function getClientName(lspParams: InitializeParams | undefined): string | } export function getOriginFromClientInfo(clientName: string | undefined): Origin { - if (clientName?.startsWith('AmazonQ-For-SMUS-IDE') || clientName?.startsWith('AmazonQ-For-SMUS-CE')) { + // TODO: Update with a new origin for SMAI case, as a short-term solution Sagemaker AI CE is using same origin as that of Sagemaker Unified Studio's IDE and CE + if ( + clientName?.startsWith('AmazonQ-For-SMUS-IDE') || + clientName?.startsWith('AmazonQ-For-SMUS-CE') || + clientName?.startsWith('AmazonQ-For-SMAI-CE') + ) { return 'MD_IDE' } return 'IDE' From 0dd7f486acdd09645522105c494fd2b0112923eb Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Mon, 11 Aug 2025 11:20:13 -0700 Subject: [PATCH 09/74] chore(release): release packages from branch main (#2073) Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> --- .release-please-manifest.json | 4 ++-- chat-client/CHANGELOG.md | 7 +++++++ chat-client/package.json | 2 +- package-lock.json | 4 ++-- server/aws-lsp-codewhisperer/CHANGELOG.md | 20 ++++++++++++++++++++ server/aws-lsp-codewhisperer/package.json | 2 +- 6 files changed, 33 insertions(+), 6 deletions(-) diff --git a/.release-please-manifest.json b/.release-please-manifest.json index c3df1b76e7..e6abdf7be2 100644 --- a/.release-please-manifest.json +++ b/.release-please-manifest.json @@ -1,8 +1,8 @@ { - "chat-client": "0.1.31", + "chat-client": "0.1.32", "core/aws-lsp-core": "0.0.13", "server/aws-lsp-antlr4": "0.1.17", - "server/aws-lsp-codewhisperer": "0.0.72", + "server/aws-lsp-codewhisperer": "0.0.73", "server/aws-lsp-json": "0.1.17", "server/aws-lsp-partiql": "0.0.16", "server/aws-lsp-yaml": "0.1.17" diff --git a/chat-client/CHANGELOG.md b/chat-client/CHANGELOG.md index aa178a18e6..1cd9acfbe0 100644 --- a/chat-client/CHANGELOG.md +++ b/chat-client/CHANGELOG.md @@ -1,5 +1,12 @@ # Changelog +## [0.1.32](https://github.com/aws/language-servers/compare/chat-client/v0.1.31...chat-client/v0.1.32) (2025-08-11) + + +### Features + +* **amazonq:** read tool ui revamp ([c65428b](https://github.com/aws/language-servers/commit/c65428bab2cf5e47badf1e3a9453babcf881e60c)) + ## [0.1.31](https://github.com/aws/language-servers/compare/chat-client/v0.1.30...chat-client/v0.1.31) (2025-08-06) diff --git a/chat-client/package.json b/chat-client/package.json index 161cb50a0b..df0378f3e4 100644 --- a/chat-client/package.json +++ b/chat-client/package.json @@ -1,6 +1,6 @@ { "name": "@aws/chat-client", - "version": "0.1.31", + "version": "0.1.32", "description": "AWS Chat Client", "main": "out/index.js", "repository": { diff --git a/package-lock.json b/package-lock.json index 6b55b375a3..b42f206c44 100644 --- a/package-lock.json +++ b/package-lock.json @@ -251,7 +251,7 @@ }, "chat-client": { "name": "@aws/chat-client", - "version": "0.1.31", + "version": "0.1.32", "license": "Apache-2.0", "dependencies": { "@aws/chat-client-ui-types": "^0.1.56", @@ -28670,7 +28670,7 @@ }, "server/aws-lsp-codewhisperer": { "name": "@aws/lsp-codewhisperer", - "version": "0.0.72", + "version": "0.0.73", "bundleDependencies": [ "@amzn/codewhisperer-streaming", "@amzn/amazon-q-developer-streaming-client" diff --git a/server/aws-lsp-codewhisperer/CHANGELOG.md b/server/aws-lsp-codewhisperer/CHANGELOG.md index 8497df84be..1b2f1011df 100644 --- a/server/aws-lsp-codewhisperer/CHANGELOG.md +++ b/server/aws-lsp-codewhisperer/CHANGELOG.md @@ -1,5 +1,25 @@ # Changelog +## [0.0.73](https://github.com/aws/language-servers/compare/lsp-codewhisperer/v0.0.72...lsp-codewhisperer/v0.0.73) (2025-08-11) + + +### Features + +* **amazonq:** read tool ui revamp ([c65428b](https://github.com/aws/language-servers/commit/c65428bab2cf5e47badf1e3a9453babcf881e60c)) + + +### Bug Fixes + +* **amazonq:** add fallback classpath generation ([#2077](https://github.com/aws/language-servers/issues/2077)) ([3a6ef14](https://github.com/aws/language-servers/commit/3a6ef14e78fa2e75b837bba6524751d65038f416)) +* **amazonq:** emit failed status for amazonq_invokeLLM ([#2071](https://github.com/aws/language-servers/issues/2071)) ([ee52a41](https://github.com/aws/language-servers/commit/ee52a41bc869b275fff708d7955b59f43b93bbd4)) +* **amazonq:** fix fallout of [#2051](https://github.com/aws/language-servers/issues/2051) ([#2057](https://github.com/aws/language-servers/issues/2057)) ([565066b](https://github.com/aws/language-servers/commit/565066bb61adda60333c9646db958d4208bcc8af)) +* **amazonq:** leverage lcs to find the chars added and removed ([#2092](https://github.com/aws/language-servers/issues/2092)) ([40379a8](https://github.com/aws/language-servers/commit/40379a887f8d42cc184239ca3175b4e673cc5286)) +* **amazonq:** skips continuous monitoring when WCS sees workspace as idle ([#2066](https://github.com/aws/language-servers/issues/2066)) ([9cb959d](https://github.com/aws/language-servers/commit/9cb959d4cc450d0907f8bf5265ba01d2aa68bcd0)) +* creating a new sesion for Edits trigger with next token ([#2094](https://github.com/aws/language-servers/issues/2094)) ([1da8730](https://github.com/aws/language-servers/commit/1da8730b6ed6ad53b6561368bf722e56d59596a4)) +* remove edit cache logic ([#2079](https://github.com/aws/language-servers/issues/2079)) ([9bc5b9c](https://github.com/aws/language-servers/commit/9bc5b9c1d77e5fee6f518f7f5016d3a0043a5a77)) +* sessionManager misused because there are 2 types of manager now ([#2090](https://github.com/aws/language-servers/issues/2090)) ([8db059a](https://github.com/aws/language-servers/commit/8db059ab83d94fd7c3ba3eb265044add31c80aea)) +* update client name to support Sagemaker AI origin for agentic chat ([#2093](https://github.com/aws/language-servers/issues/2093)) ([a746fe8](https://github.com/aws/language-servers/commit/a746fe845d5e09563b475f01ce44059dca9fd10f)) + ## [0.0.72](https://github.com/aws/language-servers/compare/lsp-codewhisperer/v0.0.71...lsp-codewhisperer/v0.0.72) (2025-08-06) diff --git a/server/aws-lsp-codewhisperer/package.json b/server/aws-lsp-codewhisperer/package.json index cb1bd377fb..fdd34884b1 100644 --- a/server/aws-lsp-codewhisperer/package.json +++ b/server/aws-lsp-codewhisperer/package.json @@ -1,6 +1,6 @@ { "name": "@aws/lsp-codewhisperer", - "version": "0.0.72", + "version": "0.0.73", "description": "CodeWhisperer Language Server", "main": "out/index.js", "repository": { From 2a54bb57b135322edc4d7468976b709f9fcc01bd Mon Sep 17 00:00:00 2001 From: Christopher Christou <39839589+awschristou@users.noreply.github.com> Date: Mon, 11 Aug 2025 13:41:04 -0700 Subject: [PATCH 10/74] chore: format version.json after incrementing (#2068) --- .github/workflows/create-release-candidate-branch.yml | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/.github/workflows/create-release-candidate-branch.yml b/.github/workflows/create-release-candidate-branch.yml index aa37873d2a..2071f69ed4 100644 --- a/.github/workflows/create-release-candidate-branch.yml +++ b/.github/workflows/create-release-candidate-branch.yml @@ -44,6 +44,10 @@ jobs: node-version: '20' cache: 'npm' + # Needed to format the json file being checked in + - name: Install dependencies + run: npm ci + - name: Calculate Release Version id: release-version env: @@ -87,6 +91,9 @@ jobs: git add "$VERSION_FILE" + # Ensure the file does not cause issues when merged to main + npm run format-staged + - name: Create Release Candidate Branch id: release-branch env: @@ -112,5 +119,5 @@ jobs: git config --global user.name "aws-toolkit-automation" # Configure git to use the PAT token for authentication git remote set-url origin "https://x-access-token:${REPO_PAT}@github.com/${{ github.repository }}.git" - git commit -m "Bump agentic version: $RELEASE_VERSION" + git commit -m "chore: bump agentic version: $RELEASE_VERSION" git push --set-upstream origin "$BRANCH_NAME" From c27dc4907c98d9a3b50ad721d8848053188cd9bc Mon Sep 17 00:00:00 2001 From: Sherry Lu <75588211+XiaoxuanLu@users.noreply.github.com> Date: Mon, 11 Aug 2025 18:00:58 -0700 Subject: [PATCH 11/74] chore: merge agentic version 1.27.0 (#2107) * Bump agentic version: 1.27.0 * Revert "feat(amazonq): read tool ui revamp" This reverts commit c65428bab2cf5e47badf1e3a9453babcf881e60c. * fix: the style in version json (#2106) --------- Co-authored-by: aws-toolkit-automation <> Co-authored-by: Tai Lai Co-authored-by: Christopher Christou <39839589+awschristou@users.noreply.github.com> --- .../src/version.json | 2 +- chat-client/src/client/mynahUi.ts | 16 +- .../src/tests/agenticChatInteg.test.ts | 13 +- .../agenticChat/agenticChatController.test.ts | 6 +- .../agenticChat/agenticChatController.ts | 188 ++++++------------ .../agenticChat/agenticChatResultStream.ts | 62 ++++-- .../agenticChat/tools/fileSearch.ts | 4 - 7 files changed, 128 insertions(+), 163 deletions(-) diff --git a/app/aws-lsp-codewhisperer-runtimes/src/version.json b/app/aws-lsp-codewhisperer-runtimes/src/version.json index 2381334b11..4c93c3f549 100644 --- a/app/aws-lsp-codewhisperer-runtimes/src/version.json +++ b/app/aws-lsp-codewhisperer-runtimes/src/version.json @@ -1,3 +1,3 @@ { - "agenticChat": "1.26.0" + "agenticChat": "1.27.0" } diff --git a/chat-client/src/client/mynahUi.ts b/chat-client/src/client/mynahUi.ts index 527dea06f3..3b42249331 100644 --- a/chat-client/src/client/mynahUi.ts +++ b/chat-client/src/client/mynahUi.ts @@ -1353,15 +1353,10 @@ export const createMynahUi = ( fileTreeTitle: '', hideFileCount: true, details: toDetailsWithoutIcon(header.fileList.details), - renderAsPills: - !header.fileList.details || - (Object.values(header.fileList.details).every(detail => !detail.changes) && - (!header.buttons || !header.buttons.some(button => button.id === 'undo-changes')) && - !header.status?.icon), } } if (!isPartialResult) { - if (processedHeader && !message.header?.status) { + if (processedHeader) { processedHeader.status = undefined } } @@ -1374,8 +1369,7 @@ export const createMynahUi = ( processedHeader.buttons !== null && processedHeader.buttons.length > 0) || processedHeader.status !== undefined || - processedHeader.icon !== undefined || - processedHeader.fileList !== undefined) + processedHeader.icon !== undefined) const padding = message.type === 'tool' ? (fileList ? true : message.messageId?.endsWith('_permission')) : undefined @@ -1386,10 +1380,8 @@ export const createMynahUi = ( // Adding this conditional check to show the stop message in the center. const contentHorizontalAlignment: ChatItem['contentHorizontalAlignment'] = undefined - // If message.header?.status?.text is Stopped or Rejected or Ignored etc.. card should be in disabled state. - const shouldMute = - message.header?.status?.text !== undefined && - ['Stopped', 'Rejected', 'Ignored', 'Failed', 'Error'].includes(message.header.status.text) + // If message.header?.status?.text is Stopped or Rejected or Ignored or Completed etc.. card should be in disabled state. + const shouldMute = message.header?.status?.text !== undefined && message.header?.status?.text !== 'Completed' return { body: message.body, 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 efc0467d2f..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 @@ -169,11 +169,11 @@ describe('Q Agentic Chat Server Integration Tests', async () => { expect(decryptedResult.additionalMessages).to.be.an('array') const fsReadMessage = decryptedResult.additionalMessages?.find( - msg => msg.type === 'tool' && msg.header?.body === '1 file read' + msg => msg.type === 'tool' && msg.fileList?.rootFolderTitle === '1 file read' ) expect(fsReadMessage).to.exist const expectedPath = path.join(rootPath, 'test.py') - const actualPaths = fsReadMessage?.header?.fileList?.filePaths?.map(normalizePath) || [] + const actualPaths = fsReadMessage?.fileList?.filePaths?.map(normalizePath) || [] expect(actualPaths).to.include.members([normalizePath(expectedPath)]) expect(fsReadMessage?.messageId?.startsWith('tooluse_')).to.be.true }) @@ -191,10 +191,10 @@ describe('Q Agentic Chat Server Integration Tests', async () => { expect(decryptedResult.additionalMessages).to.be.an('array') const listDirectoryMessage = decryptedResult.additionalMessages?.find( - msg => msg.type === 'tool' && msg.header?.body === '1 directory listed' + msg => msg.type === 'tool' && msg.fileList?.rootFolderTitle === '1 directory listed' ) expect(listDirectoryMessage).to.exist - const actualPaths = listDirectoryMessage?.header?.fileList?.filePaths?.map(normalizePath) || [] + const actualPaths = listDirectoryMessage?.fileList?.filePaths?.map(normalizePath) || [] expect(actualPaths).to.include.members([normalizePath(rootPath)]) expect(listDirectoryMessage?.messageId?.startsWith('tooluse_')).to.be.true }) @@ -371,12 +371,11 @@ describe('Q Agentic Chat Server Integration Tests', async () => { expect(decryptedResult.additionalMessages).to.be.an('array') const fileSearchMessage = decryptedResult.additionalMessages?.find( - msg => msg.type === 'tool' && msg.header?.body === 'Searched for `test` in ' + msg => msg.type === 'tool' && msg.fileList?.rootFolderTitle === '1 directory searched' ) expect(fileSearchMessage).to.exist expect(fileSearchMessage?.messageId?.startsWith('tooluse_')).to.be.true - expect(fileSearchMessage?.header?.status?.text).to.equal('3 results found') - const actualPaths = fileSearchMessage?.header?.fileList?.filePaths?.map(normalizePath) || [] + const actualPaths = fileSearchMessage?.fileList?.filePaths?.map(normalizePath) || [] expect(actualPaths).to.include.members([normalizePath(rootPath)]) }) }) diff --git a/server/aws-lsp-codewhisperer/src/language-server/agenticChat/agenticChatController.test.ts b/server/aws-lsp-codewhisperer/src/language-server/agenticChat/agenticChatController.test.ts index 616848d0b5..6c8af7a272 100644 --- a/server/aws-lsp-codewhisperer/src/language-server/agenticChat/agenticChatController.test.ts +++ b/server/aws-lsp-codewhisperer/src/language-server/agenticChat/agenticChatController.test.ts @@ -451,7 +451,7 @@ describe('AgenticChatController', () => { assert.deepStrictEqual(chatResult, { additionalMessages: [], - body: '\nHello World!', + body: '\n\nHello World!', messageId: 'mock-message-id', buttons: [], codeReference: [], @@ -1150,7 +1150,7 @@ describe('AgenticChatController', () => { sinon.assert.callCount(testFeatures.lsp.sendProgress, mockChatResponseList.length + 1) // response length + 1 loading messages assert.deepStrictEqual(chatResult, { additionalMessages: [], - body: '\nHello World!', + body: '\n\nHello World!', messageId: 'mock-message-id', codeReference: [], buttons: [], @@ -1169,7 +1169,7 @@ describe('AgenticChatController', () => { sinon.assert.callCount(testFeatures.lsp.sendProgress, mockChatResponseList.length + 1) // response length + 1 loading message assert.deepStrictEqual(chatResult, { additionalMessages: [], - body: '\nHello World!', + body: '\n\nHello World!', messageId: 'mock-message-id', buttons: [], codeReference: [], 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 09bf965819..72217fae07 100644 --- a/server/aws-lsp-codewhisperer/src/language-server/agenticChat/agenticChatController.ts +++ b/server/aws-lsp-codewhisperer/src/language-server/agenticChat/agenticChatController.ts @@ -169,7 +169,7 @@ import { ExecuteBash, ExecuteBashParams } from './tools/executeBash' import { ExplanatoryParams, InvokeOutput, ToolApprovalException } from './tools/toolShared' import { validatePathBasic, validatePathExists, validatePaths as validatePathsSync } from './utils/pathValidation' import { GrepSearch, SanitizedRipgrepOutput } from './tools/grepSearch' -import { FileSearch, FileSearchParams, isFileSearchParams } from './tools/fileSearch' +import { FileSearch, FileSearchParams } from './tools/fileSearch' import { FsReplace, FsReplaceParams } from './tools/fsReplace' import { loggingUtils, timeoutUtils } from '@aws/lsp-core' import { diffLines } from 'diff' @@ -1695,7 +1695,8 @@ export class AgenticChatController implements ChatHandlers { // remove progress UI await chatResultStream.removeResultBlockAndUpdateUI(progressPrefix + toolUse.toolUseId) - if (![FS_WRITE, FS_REPLACE].includes(toolUse.name)) { + // fsRead and listDirectory write to an existing card and could show nothing in the current position + if (![FS_WRITE, FS_REPLACE, FS_READ, LIST_DIRECTORY].includes(toolUse.name)) { await this.#showUndoAllIfRequired(chatResultStream, session) } // fsWrite can take a long time, so we render fsWrite Explanatory upon partial streaming responses. @@ -1910,19 +1911,10 @@ export class AgenticChatController implements ChatHandlers { switch (toolUse.name) { case FS_READ: case LIST_DIRECTORY: - const readToolResult = await this.#processReadTool(toolUse, chatResultStream) - if (readToolResult) { - await chatResultStream.writeResultBlock(readToolResult) - } - break case FILE_SEARCH: - if (isFileSearchParams(toolUse.input)) { - await this.#processFileSearchTool( - toolUse.input, - toolUse.toolUseId, - result, - chatResultStream - ) + const initialListDirResult = this.#processReadOrListOrSearch(toolUse, chatResultStream) + if (initialListDirResult) { + await chatResultStream.writeResultBlock(initialListDirResult) } break // no need to write tool result for listDir,fsRead,fileSearch into chat stream @@ -2323,6 +2315,7 @@ export class AgenticChatController implements ChatHandlers { } const toolMsgId = toolUse.toolUseId! + const chatMsgId = chatResultStream.getResult().messageId let headerEmitted = false const initialHeader: ChatMessage['header'] = { @@ -2360,6 +2353,13 @@ export class AgenticChatController implements ChatHandlers { header: completedHeader, }) + await chatResultStream.writeResultBlock({ + type: 'answer', + messageId: chatMsgId, + body: '', + header: undefined, + }) + this.#stoppedToolUses.add(toolMsgId) }, }) @@ -2877,135 +2877,70 @@ export class AgenticChatController implements ChatHandlers { } } - async #processFileSearchTool( - toolInput: FileSearchParams, - toolUseId: string, - result: InvokeOutput, - chatResultStream: AgenticChatResultStream - ): Promise { - if (typeof result.output.content !== 'string') return + #processReadOrListOrSearch(toolUse: ToolUse, chatResultStream: AgenticChatResultStream): ChatMessage | undefined { + let messageIdToUpdate = toolUse.toolUseId! + const currentId = chatResultStream.getMessageIdToUpdateForTool(toolUse.name!) - const { queryName, path: inputPath } = toolInput - const resultCount = result.output.content - .split('\n') - .filter(line => line.trim().startsWith('[F]') || line.trim().startsWith('[D]')).length - - const chatMessage: ChatMessage = { - type: 'tool', - messageId: toolUseId, - header: { - body: `Searched for "${queryName}" in `, - icon: 'search', - status: { - text: `${resultCount} result${resultCount !== 1 ? 's' : ''} found`, - }, - fileList: { - filePaths: [inputPath], - details: { - [inputPath]: { - description: inputPath, - visibleName: path.basename(inputPath), - clickable: false, - }, - }, - }, - }, + if (currentId) { + messageIdToUpdate = currentId + } else { + chatResultStream.setMessageIdToUpdateForTool(toolUse.name!, messageIdToUpdate) } - await chatResultStream.writeResultBlock(chatMessage) - } - - async #processReadTool( - toolUse: ToolUse, - chatResultStream: AgenticChatResultStream - ): Promise { - let currentPaths: string[] = [] + let currentPaths = [] if (toolUse.name === FS_READ) { - currentPaths = (toolUse.input as unknown as FsReadParams)?.paths || [] - } else if (toolUse.name === LIST_DIRECTORY) { - const singlePath = (toolUse.input as unknown as ListDirectoryParams)?.path - if (singlePath) { - currentPaths = [singlePath] - } - } else if (toolUse.name === FILE_SEARCH) { - const queryName = (toolUse.input as unknown as FileSearchParams)?.queryName - if (queryName) { - currentPaths = [queryName] - } + currentPaths = (toolUse.input as unknown as FsReadParams)?.paths } else { - return + currentPaths.push((toolUse.input as unknown as ListDirectoryParams | FileSearchParams)?.path) } - if (currentPaths.length === 0) return - - // Check if the last message is the same tool type - const lastMessage = chatResultStream.getLastMessage() - const isSameToolType = - lastMessage?.type === 'tool' && lastMessage.header?.icon === this.#toolToIcon(toolUse.name) - - let allPaths = currentPaths - - if (isSameToolType && lastMessage.messageId) { - // Combine with existing paths and overwrite the last message - const existingPaths = lastMessage.header?.fileList?.filePaths || [] - allPaths = [...existingPaths, ...currentPaths] + if (!currentPaths) return - const blockId = chatResultStream.getMessageBlockId(lastMessage.messageId) - if (blockId !== undefined) { - // Create the updated message with combined paths - const updatedMessage = this.#createFileListToolMessage(toolUse, allPaths, lastMessage.messageId) - // Overwrite the existing block - await chatResultStream.overwriteResultBlock(updatedMessage, blockId) - return undefined // Don't return a message since we already wrote it + for (const currentPath of currentPaths) { + const existingPaths = chatResultStream.getMessageOperation(messageIdToUpdate)?.filePaths || [] + // Check if path already exists in the list + const isPathAlreadyProcessed = existingPaths.some(path => path.relativeFilePath === currentPath) + if (!isPathAlreadyProcessed) { + const currentFileDetail = { + relativeFilePath: currentPath, + lineRanges: [{ first: -1, second: -1 }], + } + chatResultStream.addMessageOperation(messageIdToUpdate, toolUse.name!, [ + ...existingPaths, + currentFileDetail, + ]) } } - - // Create new message with current paths - return this.#createFileListToolMessage(toolUse, allPaths, toolUse.toolUseId!) - } - - #createFileListToolMessage(toolUse: ToolUse, filePaths: string[], messageId: string): ChatMessage { - const itemCount = filePaths.length let title: string - if (itemCount === 0) { + const itemCount = chatResultStream.getMessageOperation(messageIdToUpdate)?.filePaths.length + const filePathsPushed = chatResultStream.getMessageOperation(messageIdToUpdate)?.filePaths ?? [] + if (!itemCount) { title = 'Gathering context' } else { title = toolUse.name === FS_READ ? `${itemCount} file${itemCount > 1 ? 's' : ''} read` - : toolUse.name === LIST_DIRECTORY - ? `${itemCount} ${itemCount === 1 ? 'directory' : 'directories'} listed` - : '' + : toolUse.name === FILE_SEARCH + ? `${itemCount} ${itemCount === 1 ? 'directory' : 'directories'} searched` + : `${itemCount} ${itemCount === 1 ? 'directory' : 'directories'} listed` } const details: Record = {} - for (const filePath of filePaths) { - details[filePath] = { - description: filePath, - visibleName: path.basename(filePath), - clickable: toolUse.name === FS_READ, + for (const item of filePathsPushed) { + details[item.relativeFilePath] = { + lineRanges: item.lineRanges, + description: item.relativeFilePath, } } + + const fileList: FileList = { + rootFolderTitle: title, + filePaths: filePathsPushed.map(item => item.relativeFilePath), + details, + } return { type: 'tool', - header: { - body: title, - icon: this.#toolToIcon(toolUse.name), - fileList: { - filePaths, - details, - }, - }, - messageId, - } - } - - #toolToIcon(toolName: string | undefined): string | undefined { - switch (toolName) { - case FS_READ: - return 'eye' - case LIST_DIRECTORY: - return 'check-list' - default: - return undefined + fileList, + messageId: messageIdToUpdate, + body: '', } } @@ -3021,7 +2956,14 @@ export class AgenticChatController implements ChatHandlers { return undefined } - const messageIdToUpdate = toolUse.toolUseId! + let messageIdToUpdate = toolUse.toolUseId! + const currentId = chatResultStream.getMessageIdToUpdateForTool(toolUse.name!) + + if (currentId) { + messageIdToUpdate = currentId + } else { + chatResultStream.setMessageIdToUpdateForTool(toolUse.name!, messageIdToUpdate) + } // Extract search results from the tool output const output = result.output.content as SanitizedRipgrepOutput diff --git a/server/aws-lsp-codewhisperer/src/language-server/agenticChat/agenticChatResultStream.ts b/server/aws-lsp-codewhisperer/src/language-server/agenticChat/agenticChatResultStream.ts index 5fb5c39bab..70b3452361 100644 --- a/server/aws-lsp-codewhisperer/src/language-server/agenticChat/agenticChatResultStream.ts +++ b/server/aws-lsp-codewhisperer/src/language-server/agenticChat/agenticChatResultStream.ts @@ -1,4 +1,4 @@ -import { ChatResult, ChatMessage } from '@aws/language-server-runtimes/protocol' +import { ChatResult, FileDetails, ChatMessage } from '@aws/language-server-runtimes/protocol' import { randomUUID } from 'crypto' export interface ResultStreamWriter { @@ -32,20 +32,33 @@ export interface ResultStreamWriter { close(): Promise } -export const progressPrefix = 'progress_' - /** * This class wraps around lsp.sendProgress to provide a more helpful interface for streaming a ChatResult to the client. * ChatResults are grouped into blocks that can be written directly, or streamed in. * In the final message, blocks are seperated by resultDelimiter defined below. */ + +interface FileDetailsWithPath extends FileDetails { + relativeFilePath: string +} + +type OperationType = 'read' | 'write' | 'listDir' + +export const progressPrefix = 'progress_' + +interface FileOperation { + type: OperationType + filePaths: FileDetailsWithPath[] +} export class AgenticChatResultStream { - static readonly resultDelimiter = '\n' + static readonly resultDelimiter = '\n\n' #state = { chatResultBlocks: [] as ChatMessage[], isLocked: false, uuid: randomUUID(), messageId: undefined as string | undefined, + messageIdToUpdateForTool: new Map(), + messageOperations: new Map(), } readonly #sendProgress: (newChatResult: ChatResult | string) => Promise @@ -57,6 +70,33 @@ export class AgenticChatResultStream { return this.#joinResults(this.#state.chatResultBlocks, only) } + setMessageIdToUpdateForTool(toolName: string, messageId: string) { + this.#state.messageIdToUpdateForTool.set(toolName as OperationType, messageId) + } + + getMessageIdToUpdateForTool(toolName: string): string | undefined { + return this.#state.messageIdToUpdateForTool.get(toolName as OperationType) + } + + /** + * Adds a file operation for a specific message + * @param messageId The ID of the message + * @param type The type of operation ('fsRead' or 'listDirectory' or 'fsWrite') + * @param filePaths Array of FileDetailsWithPath involved in the operation + */ + addMessageOperation(messageId: string, type: string, filePaths: FileDetailsWithPath[]) { + this.#state.messageOperations.set(messageId, { type: type as OperationType, filePaths }) + } + + /** + * Gets the file operation details for a specific message + * @param messageId The ID of the message + * @returns The file operation details or undefined if not found + */ + getMessageOperation(messageId: string): FileOperation | undefined { + return this.#state.messageOperations.get(messageId) + } + #joinResults(chatResults: ChatMessage[], only?: string): ChatResult { const result: ChatResult = { body: '', @@ -71,9 +111,9 @@ export class AgenticChatResultStream { return { ...acc, buttons: [...(acc.buttons ?? []), ...(c.buttons ?? [])], - body: acc.body + (c.body ? AgenticChatResultStream.resultDelimiter + c.body : ''), - ...(c.contextList && c.type !== 'tool' && { contextList: c.contextList }), - header: c.header !== undefined ? c.header : acc.header, + body: acc.body + AgenticChatResultStream.resultDelimiter + c.body, + ...(c.contextList && { contextList: c.contextList }), + header: Object.prototype.hasOwnProperty.call(c, 'header') ? c.header : acc.header, codeReference: [...(acc.codeReference ?? []), ...(c.codeReference ?? [])], } } else if (acc.additionalMessages!.some(am => am.messageId === c.messageId)) { @@ -87,7 +127,7 @@ export class AgenticChatResultStream { : am.buttons, body: am.messageId === c.messageId - ? am.body + (c.body ? AgenticChatResultStream.resultDelimiter + c.body : '') + ? am.body + AgenticChatResultStream.resultDelimiter + c.body : am.body, ...(am.messageId === c.messageId && (c.contextList || acc.contextList) && { @@ -121,7 +161,7 @@ export class AgenticChatResultStream { }, }, }), - ...(am.messageId === c.messageId && c.header !== undefined && { header: c.header }), + header: Object.prototype.hasOwnProperty.call(c, 'header') ? c.header : am.header, })), } } else { @@ -206,10 +246,6 @@ export class AgenticChatResultStream { return undefined } - getLastMessage(): ChatMessage { - return this.#state.chatResultBlocks[this.#state.chatResultBlocks.length - 1] - } - getResultStreamWriter(): ResultStreamWriter { // Note: if write calls are not awaited, stream can be out-of-order. if (this.#state.isLocked) { diff --git a/server/aws-lsp-codewhisperer/src/language-server/agenticChat/tools/fileSearch.ts b/server/aws-lsp-codewhisperer/src/language-server/agenticChat/tools/fileSearch.ts index 37d11afe4f..fb6486996e 100644 --- a/server/aws-lsp-codewhisperer/src/language-server/agenticChat/tools/fileSearch.ts +++ b/server/aws-lsp-codewhisperer/src/language-server/agenticChat/tools/fileSearch.ts @@ -158,7 +158,3 @@ export class FileSearch { } as const } } - -export function isFileSearchParams(input: any): input is FileSearchParams { - return input && typeof input.path === 'string' && typeof input.queryName === 'string' -} From 817cfe2656cb1deec6111c699c4ba46b4ba53e00 Mon Sep 17 00:00:00 2001 From: Dung Dong Date: Tue, 12 Aug 2025 17:03:29 -0700 Subject: [PATCH 12/74] fix(amazonq): persist mcp configs in agent json on start-up (#2112) --- .../src/language-server/agenticChat/tools/mcp/mcpUtils.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/aws-lsp-codewhisperer/src/language-server/agenticChat/tools/mcp/mcpUtils.ts b/server/aws-lsp-codewhisperer/src/language-server/agenticChat/tools/mcp/mcpUtils.ts index 45ab34170c..7b212bad49 100644 --- a/server/aws-lsp-codewhisperer/src/language-server/agenticChat/tools/mcp/mcpUtils.ts +++ b/server/aws-lsp-codewhisperer/src/language-server/agenticChat/tools/mcp/mcpUtils.ts @@ -920,8 +920,8 @@ async function migrateConfigToAgent( ...existingAgentConfig, // Merge MCP servers, keeping existing ones if they exist mcpServers: { - ...existingAgentConfig.mcpServers, ...newAgentConfig.mcpServers, + ...existingAgentConfig.mcpServers, }, // Merge tools lists without duplicates tools: [...new Set([...existingAgentConfig.tools, ...newAgentConfig.tools])], From fd6e9a829c6229c276de5340dffce52b426a864d Mon Sep 17 00:00:00 2001 From: invictus <149003065+ashishrp-aws@users.noreply.github.com> Date: Wed, 13 Aug 2025 10:28:53 -0700 Subject: [PATCH 13/74] feat(amazonq): added mcp admin level configuration with GetProfile (#2000) * feat(amazonq): added mcp admin level configuration with GetProfile * feat(amazonq): added UX message for mcp admin control * test: add unit tests for ProfileStatusMonitor static functionality * test: add comprehensive unit tests for MCP admin control features * fix: fix to wait for serviceManager is initialzied to initialize mcp managers * fix: fix for unit test failures * fix: fix for UI changes * fix(amazonq): fix to to rename mcp enabled function and max time limit of 10 seconds * fix: fix to add async initialization for mcp manager * fix: fix to move action buttons to mcpEventHandler for listMcpServers * fix: added try and catch block for mcp initialization * fix: fix for merge conflicts * fix: fix for test failure * fix: remove the unnecessary feature flag * fix: fix for mynah test failure * fix: fix to retry function to common util * fix: fix to add retryUtils --- chat-client/src/client/mcpMynahUi.test.ts | 6 +- chat-client/src/client/mcpMynahUi.ts | 19 +- core/aws-lsp-core/src/index.ts | 1 + core/aws-lsp-core/src/util/retryUtils.test.ts | 120 ++++++++++++ core/aws-lsp-core/src/util/retryUtils.ts | 77 ++++++++ .../client/token/bearer-token-service.json | 85 ++++++++ .../token/codewhispererbearertokenclient.d.ts | 26 +++ .../agenticChat/constants/constants.ts | 2 + .../tools/mcp/mcpEventHandler.test.ts | 105 ++++++++++ .../agenticChat/tools/mcp/mcpEventHandler.ts | 56 +++++- .../tools/mcp/profileStatusMonitor.test.ts | 182 ++++++++++++++++++ .../tools/mcp/profileStatusMonitor.ts | 129 +++++++++++++ .../agenticChat/tools/toolServer.ts | 122 ++++++++++-- .../src/shared/codeWhispererService.ts | 7 + 14 files changed, 899 insertions(+), 38 deletions(-) create mode 100644 core/aws-lsp-core/src/util/retryUtils.test.ts create mode 100644 core/aws-lsp-core/src/util/retryUtils.ts create mode 100644 server/aws-lsp-codewhisperer/src/language-server/agenticChat/tools/mcp/profileStatusMonitor.test.ts create mode 100644 server/aws-lsp-codewhisperer/src/language-server/agenticChat/tools/mcp/profileStatusMonitor.ts diff --git a/chat-client/src/client/mcpMynahUi.test.ts b/chat-client/src/client/mcpMynahUi.test.ts index 9d1dec5407..947e5bc604 100644 --- a/chat-client/src/client/mcpMynahUi.test.ts +++ b/chat-client/src/client/mcpMynahUi.test.ts @@ -107,10 +107,8 @@ describe('McpMynahUi', () => { assert.strictEqual(callArgs.detailedList.header.description, 'Test Description') assert.deepStrictEqual(callArgs.detailedList.header.status, { status: 'success' }) - // Verify the actions in the header - assert.strictEqual(callArgs.detailedList.header.actions.length, 2) - assert.strictEqual(callArgs.detailedList.header.actions[0].id, 'add-new-mcp') - assert.strictEqual(callArgs.detailedList.header.actions[1].id, 'refresh-mcp-list') + // Verify the actions in the header (no default actions are added when header is provided) + assert.strictEqual(callArgs.detailedList.header.actions.length, 0) // Verify the list structure assert.strictEqual(callArgs.detailedList.list.length, 1) diff --git a/chat-client/src/client/mcpMynahUi.ts b/chat-client/src/client/mcpMynahUi.ts index f9216b8168..5ece955dfa 100644 --- a/chat-client/src/client/mcpMynahUi.ts +++ b/chat-client/src/client/mcpMynahUi.ts @@ -272,20 +272,11 @@ export class McpMynahUi { title: params.header.title, description: params.header.description, status: params.header.status, - actions: [ - { - id: MCP_IDS.ADD_NEW, - icon: toMynahIcon('plus'), - status: 'clear', - description: 'Add new MCP', - }, - { - id: MCP_IDS.REFRESH_LIST, - icon: toMynahIcon('refresh'), - status: 'clear', - description: 'Refresh MCP servers', - }, - ], + actions: + params.header.actions?.map(action => ({ + ...action, + icon: action.icon ? toMynahIcon(action.icon) : undefined, + })) || [], } : undefined, filterOptions: params.filterOptions?.map(filter => ({ diff --git a/core/aws-lsp-core/src/index.ts b/core/aws-lsp-core/src/index.ts index 1ad391dc8f..030a4fa2e3 100644 --- a/core/aws-lsp-core/src/index.ts +++ b/core/aws-lsp-core/src/index.ts @@ -19,3 +19,4 @@ export * as workspaceUtils from './util/workspaceUtils' export * as processUtils from './util/processUtils' export * as collectionUtils from './util/collectionUtils' export * as loggingUtils from './util/loggingUtils' +export * as retryUtils from './util/retryUtils' diff --git a/core/aws-lsp-core/src/util/retryUtils.test.ts b/core/aws-lsp-core/src/util/retryUtils.test.ts new file mode 100644 index 0000000000..4223e1f3bf --- /dev/null +++ b/core/aws-lsp-core/src/util/retryUtils.test.ts @@ -0,0 +1,120 @@ +/*! + * Copyright Amazon.com, Inc. or its affiliates. + * All Rights Reserved. SPDX-License-Identifier: Apache-2.0 + */ + +import { expect } from 'chai' +import * as sinon from 'sinon' +import { retryWithBackoff, DEFAULT_MAX_RETRIES, DEFAULT_BASE_DELAY } from './retryUtils' + +describe('retryUtils', () => { + let clock: sinon.SinonFakeTimers + + beforeEach(() => { + clock = sinon.useFakeTimers() + }) + + afterEach(() => { + clock.restore() + }) + + describe('retryWithBackoff', () => { + it('should return result on first success', async () => { + const fn = sinon.stub().resolves('success') + + const result = await retryWithBackoff(fn) + + expect(result).to.equal('success') + expect(fn.callCount).to.equal(1) + }) + + it('should retry on retryable errors', async () => { + const fn = sinon.stub() + fn.onFirstCall().rejects({ code: 'ThrottlingException' }) + fn.onSecondCall().resolves('success') + + const promise = retryWithBackoff(fn) + await clock.tickAsync(DEFAULT_BASE_DELAY) + const result = await promise + + expect(result).to.equal('success') + expect(fn.callCount).to.equal(2) + }) + + it('should not retry on non-retryable client errors', async () => { + const error = { statusCode: 404 } + const fn = sinon.stub().rejects(error) + + try { + await retryWithBackoff(fn) + expect.fail('Expected function to throw') + } catch (e) { + expect(e).to.equal(error) + } + expect(fn.callCount).to.equal(1) + }) + + it('should retry on server errors', async () => { + const fn = sinon.stub() + fn.onFirstCall().rejects({ statusCode: 500 }) + fn.onSecondCall().resolves('success') + + const promise = retryWithBackoff(fn) + await clock.tickAsync(DEFAULT_BASE_DELAY) + const result = await promise + + expect(result).to.equal('success') + expect(fn.callCount).to.equal(2) + }) + + it('should use exponential backoff by default', async () => { + const fn = sinon.stub() + const error = { code: 'ThrottlingException' } + fn.onFirstCall().rejects(error) + fn.onSecondCall().rejects(error) + + const promise = retryWithBackoff(fn) + + // First retry after baseDelay * 1 + await clock.tickAsync(DEFAULT_BASE_DELAY) + // Second retry after baseDelay * 2 + await clock.tickAsync(DEFAULT_BASE_DELAY * 2) + + try { + await promise + expect.fail('Expected function to throw') + } catch (e) { + expect(e).to.equal(error) + } + expect(fn.callCount).to.equal(DEFAULT_MAX_RETRIES) + }) + + it('should respect custom maxRetries', async () => { + const error = { code: 'ThrottlingException' } + const fn = sinon.stub().rejects(error) + + try { + await retryWithBackoff(fn, { maxRetries: 1 }) + expect.fail('Expected function to throw') + } catch (e) { + expect(e).to.equal(error) + } + expect(fn.callCount).to.equal(1) + }) + + it('should use custom isRetryable function', async () => { + const error = { custom: 'error' } + const fn = sinon.stub().rejects(error) + const isRetryable = sinon.stub().returns(false) + + try { + await retryWithBackoff(fn, { isRetryable }) + expect.fail('Expected function to throw') + } catch (e) { + expect(e).to.equal(error) + } + expect(fn.callCount).to.equal(1) + expect(isRetryable.calledWith(error)).to.equal(true) + }) + }) +}) diff --git a/core/aws-lsp-core/src/util/retryUtils.ts b/core/aws-lsp-core/src/util/retryUtils.ts new file mode 100644 index 0000000000..dc135ce23d --- /dev/null +++ b/core/aws-lsp-core/src/util/retryUtils.ts @@ -0,0 +1,77 @@ +/*! + * Copyright Amazon.com, Inc. or its affiliates. + * All Rights Reserved. SPDX-License-Identifier: Apache-2.0 + */ + +// Default retry configuration constants +export const DEFAULT_MAX_RETRIES = 2 +export const DEFAULT_BASE_DELAY = 500 +export const DEFAULT_EXPONENTIAL_BACKOFF = true + +// HTTP status code constants +const CLIENT_ERROR_MIN = 400 +const CLIENT_ERROR_MAX = 500 +const INTERNAL_SERVER_ERROR = 500 +const SERVICE_UNAVAILABLE = 503 + +// AWS error code constants +const THROTTLING_EXCEPTION = 'ThrottlingException' +const INTERNAL_SERVER_EXCEPTION = 'InternalServerException' + +export interface RetryOptions { + /** Maximum number of retry attempts (default: DEFAULT_MAX_RETRIES) */ + maxRetries?: number + /** Base delay in milliseconds (default: DEFAULT_BASE_DELAY) */ + baseDelay?: number + /** Whether to use exponential backoff (default: DEFAULT_EXPONENTIAL_BACKOFF) */ + exponentialBackoff?: boolean + /** Custom function to determine if an error is retryable */ + isRetryable?: (error: any) => boolean +} + +/** + * Default AWS error retry logic + */ +function defaultIsRetryable(error: any): boolean { + const errorCode = error.code || error.name + const statusCode = error.statusCode + + // Fast fail on non-retryable client errors (except throttling) + if (statusCode >= CLIENT_ERROR_MIN && statusCode < CLIENT_ERROR_MAX && errorCode !== THROTTLING_EXCEPTION) { + return false + } + + // Retry on throttling, server errors, and specific status codes + return ( + errorCode === THROTTLING_EXCEPTION || + errorCode === INTERNAL_SERVER_EXCEPTION || + statusCode === INTERNAL_SERVER_ERROR || + statusCode === SERVICE_UNAVAILABLE + ) +} + +/** + * Executes a function with retry logic and exponential backoff + */ +export async function retryWithBackoff(fn: () => Promise, options: RetryOptions = {}): Promise { + const { + maxRetries = DEFAULT_MAX_RETRIES, + baseDelay = DEFAULT_BASE_DELAY, + exponentialBackoff = DEFAULT_EXPONENTIAL_BACKOFF, + isRetryable = defaultIsRetryable, + } = options + + for (let attempt = 0; attempt < maxRetries; attempt++) { + try { + return await fn() + } catch (error: any) { + if (!isRetryable(error) || attempt === maxRetries - 1) { + throw error + } + + const delay = exponentialBackoff ? baseDelay * (attempt + 1) : baseDelay + await new Promise(resolve => setTimeout(resolve, delay)) + } + } + throw new Error('Retry failed') +} diff --git a/server/aws-lsp-codewhisperer/src/client/token/bearer-token-service.json b/server/aws-lsp-codewhisperer/src/client/token/bearer-token-service.json index 2120382d25..a5704fca16 100644 --- a/server/aws-lsp-codewhisperer/src/client/token/bearer-token-service.json +++ b/server/aws-lsp-codewhisperer/src/client/token/bearer-token-service.json @@ -516,6 +516,37 @@ ], "documentation": "

API to get code transformation status.

" }, + "GetProfile": { + "name": "GetProfile", + "http": { + "method": "POST", + "requestUri": "/" + }, + "input": { + "shape": "GetProfileRequest" + }, + "output": { + "shape": "GetProfileResponse" + }, + "errors": [ + { + "shape": "ThrottlingException" + }, + { + "shape": "ResourceNotFoundException" + }, + { + "shape": "InternalServerException" + }, + { + "shape": "ValidationException" + }, + { + "shape": "AccessDeniedException" + } + ], + "documentation": "

Get the requested CodeWhisperer profile.

" + }, "GetUsageLimits": { "name": "GetUsageLimits", "http": { @@ -3126,6 +3157,24 @@ } } }, + "GetProfileRequest": { + "type": "structure", + "required": ["profileArn"], + "members": { + "profileArn": { + "shape": "ProfileArn" + } + } + }, + "GetProfileResponse": { + "type": "structure", + "required": ["profile"], + "members": { + "profile": { + "shape": "ProfileInfo" + } + } + }, "GetTaskAssistCodeGenerationRequest": { "type": "structure", "required": ["conversationId", "codeGenerationId"], @@ -3787,6 +3836,15 @@ "type": "long", "box": true }, + "MCPConfiguration": { + "type": "structure", + "required": ["toggle"], + "members": { + "toggle": { + "shape": "OptInFeatureToggle" + } + } + }, "MemoryEntry": { "type": "structure", "required": ["id", "memoryEntryString", "metadata"], @@ -3995,6 +4053,9 @@ }, "workspaceContext": { "shape": "WorkspaceContext" + }, + "mcpConfiguration": { + "shape": "MCPConfiguration" } } }, @@ -4189,6 +4250,30 @@ } } }, + "ProfileInfo": { + "type": "structure", + "required": ["arn"], + "members": { + "arn": { + "shape": "ProfileArn" + }, + "profileName": { + "shape": "ProfileName" + }, + "description": { + "shape": "ProfileDescription" + }, + "status": { + "shape": "ProfileStatus" + }, + "profileType": { + "shape": "ProfileType" + }, + "optInFeatures": { + "shape": "OptInFeatures" + } + } + }, "ProfileArn": { "type": "string", "max": 950, diff --git a/server/aws-lsp-codewhisperer/src/client/token/codewhispererbearertokenclient.d.ts b/server/aws-lsp-codewhisperer/src/client/token/codewhispererbearertokenclient.d.ts index 2189944d4f..c885612888 100644 --- a/server/aws-lsp-codewhisperer/src/client/token/codewhispererbearertokenclient.d.ts +++ b/server/aws-lsp-codewhisperer/src/client/token/codewhispererbearertokenclient.d.ts @@ -144,6 +144,14 @@ declare class CodeWhispererBearerTokenClient extends Service { * API to get code transformation status. */ getTransformationPlan(callback?: (err: AWSError, data: CodeWhispererBearerTokenClient.Types.GetTransformationPlanResponse) => void): Request; + /** + * Get the requested CodeWhisperer profile. + */ + getProfile(params: CodeWhispererBearerTokenClient.Types.GetProfileRequest, callback?: (err: AWSError, data: CodeWhispererBearerTokenClient.Types.GetProfileResponse) => void): Request; + /** + * Get the requested CodeWhisperer profile. + */ + getProfile(callback?: (err: AWSError, data: CodeWhispererBearerTokenClient.Types.GetProfileResponse) => void): Request; /** * API to get current usage limits */ @@ -961,6 +969,12 @@ declare namespace CodeWhispererBearerTokenClient { jobStatus?: CodeFixJobStatus; suggestedFix?: SuggestedFix; } + export interface GetProfileRequest { + profileArn: ProfileArn; + } + export interface GetProfileResponse { + profile: ProfileInfo; + } export interface GetTaskAssistCodeGenerationRequest { conversationId: ConversationId; codeGenerationId: CodeGenerationId; @@ -1189,6 +1203,9 @@ declare namespace CodeWhispererBearerTokenClient { nextToken?: String; } export type Long = number; + export interface MCPConfiguration { + toggle: OptInFeatureToggle; + } export interface MemoryEntry { /** * A unique identifier for a single memory entry @@ -1260,6 +1277,7 @@ declare namespace CodeWhispererBearerTokenClient { dashboardAnalytics?: DashboardAnalytics; notifications?: Notifications; workspaceContext?: WorkspaceContext; + mcpConfiguration?: MCPConfiguration; } export type OptOutPreference = "OPTIN"|"OPTOUT"|string; export type Origin = "CHATBOT"|"CONSOLE"|"DOCUMENTATION"|"MARKETING"|"MOBILE"|"SERVICE_INTERNAL"|"UNIFIED_SEARCH"|"UNKNOWN"|"MD"|"IDE"|"SAGE_MAKER"|"CLI"|"AI_EDITOR"|"OPENSEARCH_DASHBOARD"|"GITLAB"|string; @@ -1315,6 +1333,14 @@ declare namespace CodeWhispererBearerTokenClient { permissionUpdateRequired?: Boolean; applicationProperties?: ApplicationPropertiesList; } + export interface ProfileInfo { + arn: ProfileArn; + profileName?: ProfileName; + description?: ProfileDescription; + status?: ProfileStatus; + profileType?: ProfileType; + optInFeatures?: OptInFeatures; + } export type ProfileArn = string; export type ProfileDescription = string; export type ProfileList = Profile[]; diff --git a/server/aws-lsp-codewhisperer/src/language-server/agenticChat/constants/constants.ts b/server/aws-lsp-codewhisperer/src/language-server/agenticChat/constants/constants.ts index 09fbb20436..1729c10c1d 100644 --- a/server/aws-lsp-codewhisperer/src/language-server/agenticChat/constants/constants.ts +++ b/server/aws-lsp-codewhisperer/src/language-server/agenticChat/constants/constants.ts @@ -9,6 +9,8 @@ export const RESPONSE_TIMEOUT_PARTIAL_MSG = 'Response processing timed out after export const LOADING_THRESHOLD_MS = 2000 export const CLIENT_TIMEOUT_MS = 245_000 export const RESPONSE_TIMEOUT_MS = 240_000 +export const SERVICE_MANAGER_TIMEOUT_MS = 10_000 //10 seconds +export const SERVICE_MANAGER_POLL_INTERVAL_MS = 100 // LLM Constants export const GENERATE_ASSISTANT_RESPONSE_INPUT_LIMIT = 500_000 diff --git a/server/aws-lsp-codewhisperer/src/language-server/agenticChat/tools/mcp/mcpEventHandler.test.ts b/server/aws-lsp-codewhisperer/src/language-server/agenticChat/tools/mcp/mcpEventHandler.test.ts index 01984a310e..cb645eec43 100644 --- a/server/aws-lsp-codewhisperer/src/language-server/agenticChat/tools/mcp/mcpEventHandler.test.ts +++ b/server/aws-lsp-codewhisperer/src/language-server/agenticChat/tools/mcp/mcpEventHandler.test.ts @@ -240,4 +240,109 @@ describe('McpEventHandler error handling', () => { expect(result.header).to.not.be.undefined expect(result.header.title).to.equal('Edit MCP Server') }) + + describe('#getListMcpServersStatus', () => { + beforeEach(() => { + sinon.restore() + sinon.stub(mcpUtils, 'getGlobalAgentConfigPath').returns('/fake/home/.aws/amazonq/agents/default.json') + saveAgentConfigStub = sinon.stub(mcpUtils, 'saveAgentConfig').resolves() + }) + + it('returns admin disabled status when MCP state is false', async () => { + // Stub ProfileStatusMonitor.getMcpState to return false + const { ProfileStatusMonitor } = await import('./profileStatusMonitor') + sinon.stub(ProfileStatusMonitor, 'getMcpState').returns(false) + + loadStub = sinon.stub(mcpUtils, 'loadAgentConfig').resolves({ + servers: new Map(), + serverNameMapping: new Map(), + errors: new Map(), + agentConfig: { + name: 'test-agent', + version: '1.0.0', + description: 'Test agent', + mcpServers: {}, + tools: [], + allowedTools: [], + toolsSettings: {}, + includedFiles: [], + resources: [], + }, + }) + + await McpManager.init([], features) + const result = await eventHandler.onListMcpServers({}) + + expect(result.header.status).to.deep.equal({ + title: 'MCP functionality has been disabled by your administrator', + icon: 'info', + status: 'info', + }) + }) + + it('returns config error status when MCP state is not false but config errors exist', async () => { + // Stub ProfileStatusMonitor.getMcpState to return true + const { ProfileStatusMonitor } = await import('./profileStatusMonitor') + sinon.stub(ProfileStatusMonitor, 'getMcpState').returns(true) + + const mockErrors = new Map([['file1.json', 'Config error']]) + loadStub = sinon.stub(mcpUtils, 'loadAgentConfig').resolves({ + servers: new Map(), + serverNameMapping: new Map(), + errors: mockErrors, + agentConfig: { + name: 'test-agent', + version: '1.0.0', + description: 'Test agent', + mcpServers: {}, + tools: [], + allowedTools: [], + toolsSettings: {}, + includedFiles: [], + resources: [], + }, + }) + + await McpManager.init([], features) + sinon.stub(McpManager.instance, 'getConfigLoadErrors').returns('File: file1.json, Error: Config error') + + const result = await eventHandler.onListMcpServers({}) + + expect(result.header.status).to.deep.equal({ + title: 'File: file1.json, Error: Config error', + icon: 'cancel-circle', + status: 'error', + }) + }) + + it('returns undefined status when MCP state is not false and no config errors', async () => { + // Stub ProfileStatusMonitor.getMcpState to return true + const { ProfileStatusMonitor } = await import('./profileStatusMonitor') + sinon.stub(ProfileStatusMonitor, 'getMcpState').returns(true) + + loadStub = sinon.stub(mcpUtils, 'loadAgentConfig').resolves({ + servers: new Map(), + serverNameMapping: new Map(), + errors: new Map(), + agentConfig: { + name: 'test-agent', + version: '1.0.0', + description: 'Test agent', + mcpServers: {}, + tools: [], + allowedTools: [], + toolsSettings: {}, + includedFiles: [], + resources: [], + }, + }) + + await McpManager.init([], features) + sinon.stub(McpManager.instance, 'getConfigLoadErrors').returns(undefined) + + const result = await eventHandler.onListMcpServers({}) + + expect(result.header.status).to.be.undefined + }) + }) }) 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 4596bc0646..bf8c91b533 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 @@ -30,6 +30,7 @@ import { } from './mcpTypes' import { TelemetryService } from '../../../../shared/telemetry/telemetryService' import { URI } from 'vscode-uri' +import { ProfileStatusMonitor } from './profileStatusMonitor' interface PermissionOption { label: string @@ -222,18 +223,63 @@ export class McpEventHandler { } // Return the result in the expected format + const mcpState = ProfileStatusMonitor.getMcpState() const header = { title: 'MCP Servers', - description: "Add MCP servers to extend Q's capabilities.", - // only show error on list mcp server page if unable to read mcp.json file - status: configLoadErrors - ? { title: configLoadErrors, icon: 'cancel-circle', status: 'error' as Status } - : undefined, + description: mcpState === false ? '' : "Add MCP servers to extend Q's capabilities.", + status: this.#getListMcpServersStatus(configLoadErrors, mcpState), + actions: this.#getListMcpServersActions(configLoadErrors, mcpState), } return { header, list: groups } } + /** + * Gets the status for the list MCP servers header + */ + #getListMcpServersStatus( + configLoadErrors: string | undefined, + mcpState: boolean | undefined + ): { title: string; icon: string; status: Status } | undefined { + if (mcpState === false) { + return { + title: 'MCP functionality has been disabled by your administrator', + icon: 'info', + status: 'info' as Status, + } + } + + if (configLoadErrors) { + return { title: configLoadErrors, icon: 'cancel-circle', status: 'error' as Status } + } + + return undefined + } + + /** + * Gets the actions for the list MCP servers header + */ + #getListMcpServersActions(configLoadErrors: string | undefined, mcpState: boolean | undefined) { + return mcpState !== false && (!configLoadErrors || configLoadErrors === '') + ? [ + { + id: 'add-new-mcp', + icon: 'plus', + status: 'clear', + text: 'Add new MCP server', + description: 'Add new MCP server', + }, + { + id: 'refresh-mcp-list', + icon: 'refresh', + status: 'clear', + text: 'Refresh MCP servers', + description: 'Refresh MCP servers', + }, + ] + : [] + } + /** * Handles MCP server click events */ diff --git a/server/aws-lsp-codewhisperer/src/language-server/agenticChat/tools/mcp/profileStatusMonitor.test.ts b/server/aws-lsp-codewhisperer/src/language-server/agenticChat/tools/mcp/profileStatusMonitor.test.ts new file mode 100644 index 0000000000..8ee8454374 --- /dev/null +++ b/server/aws-lsp-codewhisperer/src/language-server/agenticChat/tools/mcp/profileStatusMonitor.test.ts @@ -0,0 +1,182 @@ +/*! + * Copyright Amazon.com, Inc. or its affiliates. + * All Rights Reserved. SPDX-License-Identifier: Apache-2.0 + */ + +import { expect } from 'chai' +import * as sinon from 'sinon' +import { ProfileStatusMonitor } from './profileStatusMonitor' +import * as AmazonQTokenServiceManagerModule from '../../../../shared/amazonQServiceManager/AmazonQTokenServiceManager' + +describe('ProfileStatusMonitor', () => { + let profileStatusMonitor: ProfileStatusMonitor + let mockCredentialsProvider: any + let mockWorkspace: any + let mockLogging: any + let mockSdkInitializator: any + let mockOnMcpDisabled: sinon.SinonStub + let mockOnMcpEnabled: sinon.SinonStub + let clock: sinon.SinonFakeTimers + + beforeEach(() => { + clock = sinon.useFakeTimers() + + mockCredentialsProvider = { + hasCredentials: sinon.stub().returns(true), + } + + mockWorkspace = {} + + mockLogging = { + info: sinon.stub(), + debug: sinon.stub(), + } + + mockSdkInitializator = {} + mockOnMcpDisabled = sinon.stub() + mockOnMcpEnabled = sinon.stub() + + profileStatusMonitor = new ProfileStatusMonitor( + mockCredentialsProvider, + mockWorkspace, + mockLogging, + mockSdkInitializator, + mockOnMcpDisabled, + mockOnMcpEnabled + ) + }) + + afterEach(() => { + clock.restore() + sinon.restore() + profileStatusMonitor.stop() + }) + + describe('start', () => { + it('should start monitoring and log info message', () => { + profileStatusMonitor.start() + + expect( + mockLogging.info.calledWith('ProfileStatusMonitor started - checking MCP configuration every 24 hours') + ).to.be.true + }) + + it('should not start multiple times', () => { + profileStatusMonitor.start() + profileStatusMonitor.start() + + expect(mockLogging.info.callCount).to.equal(1) + }) + }) + + describe('stop', () => { + it('should stop monitoring and log info message', () => { + profileStatusMonitor.start() + profileStatusMonitor.stop() + + expect(mockLogging.info.calledWith('ProfileStatusMonitor stopped')).to.be.true + }) + }) + + describe('checkInitialState', () => { + it('should return true when no profile ARN is available', async () => { + sinon.stub(AmazonQTokenServiceManagerModule.AmazonQTokenServiceManager, 'getInstance').returns({ + getActiveProfileArn: () => undefined, + } as any) + + const result = await profileStatusMonitor.checkInitialState() + expect(result).to.be.true + }) + + it('should return true and log debug message on error', async () => { + // Stub the private isMcpEnabled method to throw an error + sinon.stub(profileStatusMonitor as any, 'isMcpEnabled').throws(new Error('Service manager not ready')) + + const result = await profileStatusMonitor.checkInitialState() + expect(result).to.be.true + expect(mockLogging.debug.calledWith(sinon.match('Initial MCP state check failed, defaulting to enabled'))) + .to.be.true + }) + }) + + describe('getMcpState', () => { + beforeEach(() => { + // Reset static state before each test + ;(ProfileStatusMonitor as any).lastMcpState = undefined + }) + + it('should return undefined initially', () => { + expect(ProfileStatusMonitor.getMcpState()).to.be.undefined + }) + + it('should return the last MCP state after it is set', () => { + // Access the private static property through reflection for testing + ;(ProfileStatusMonitor as any).lastMcpState = true + expect(ProfileStatusMonitor.getMcpState()).to.be.true + ;(ProfileStatusMonitor as any).lastMcpState = false + expect(ProfileStatusMonitor.getMcpState()).to.be.false + }) + + it('should be accessible across different instances', () => { + const monitor1 = new ProfileStatusMonitor( + mockCredentialsProvider, + mockWorkspace, + mockLogging, + mockSdkInitializator, + mockOnMcpDisabled, + mockOnMcpEnabled + ) + + const monitor2 = new ProfileStatusMonitor( + mockCredentialsProvider, + mockWorkspace, + mockLogging, + mockSdkInitializator, + mockOnMcpDisabled, + mockOnMcpEnabled + ) + + // Set state through static property + ;(ProfileStatusMonitor as any).lastMcpState = true + + // Should be accessible from both instances + expect(ProfileStatusMonitor.getMcpState()).to.be.true + }) + }) + + describe('static lastMcpState', () => { + beforeEach(() => { + // Reset static state before each test + ;(ProfileStatusMonitor as any).lastMcpState = undefined + }) + + it('should maintain state across multiple instances', () => { + const monitor1 = new ProfileStatusMonitor( + mockCredentialsProvider, + mockWorkspace, + mockLogging, + mockSdkInitializator, + mockOnMcpDisabled, + mockOnMcpEnabled + ) + + const monitor2 = new ProfileStatusMonitor( + mockCredentialsProvider, + mockWorkspace, + mockLogging, + mockSdkInitializator, + mockOnMcpDisabled, + mockOnMcpEnabled + ) + + // Initially undefined + expect(ProfileStatusMonitor.getMcpState()).to.be.undefined + + // Set through internal mechanism (simulating state change) + ;(ProfileStatusMonitor as any).lastMcpState = false + + // Both instances should see the same state + expect(ProfileStatusMonitor.getMcpState()).to.be.false + }) + }) +}) diff --git a/server/aws-lsp-codewhisperer/src/language-server/agenticChat/tools/mcp/profileStatusMonitor.ts b/server/aws-lsp-codewhisperer/src/language-server/agenticChat/tools/mcp/profileStatusMonitor.ts new file mode 100644 index 0000000000..4a1cc705e4 --- /dev/null +++ b/server/aws-lsp-codewhisperer/src/language-server/agenticChat/tools/mcp/profileStatusMonitor.ts @@ -0,0 +1,129 @@ +/*! + * Copyright Amazon.com, Inc. or its affiliates. + * All Rights Reserved. SPDX-License-Identifier: Apache-2.0 + */ + +import { + CredentialsProvider, + Logging, + SDKInitializator, + Workspace, +} from '@aws/language-server-runtimes/server-interface' +import { retryUtils } from '@aws/lsp-core' +import { CodeWhispererServiceToken } from '../../../../shared/codeWhispererService' +import { DEFAULT_AWS_Q_ENDPOINT_URL, DEFAULT_AWS_Q_REGION } from '../../../../shared/constants' +import { AmazonQTokenServiceManager } from '../../../../shared/amazonQServiceManager/AmazonQTokenServiceManager' + +export class ProfileStatusMonitor { + private intervalId?: NodeJS.Timeout + private readonly CHECK_INTERVAL = 24 * 60 * 60 * 1000 // 24 hours + private codeWhispererClient?: CodeWhispererServiceToken + private cachedProfileArn?: string + private static lastMcpState?: boolean + + constructor( + private credentialsProvider: CredentialsProvider, + private workspace: Workspace, + private logging: Logging, + private sdkInitializator: SDKInitializator, + private onMcpDisabled: () => void, + private onMcpEnabled?: () => void + ) {} + + async checkInitialState(): Promise { + try { + const isMcpEnabled = await this.isMcpEnabled() + return isMcpEnabled !== false // Return true if enabled or API failed + } catch (error) { + this.logging.debug(`Initial MCP state check failed, defaulting to enabled: ${error}`) + ProfileStatusMonitor.lastMcpState = true + return true + } + } + + start(): void { + if (this.intervalId) { + return + } + + this.intervalId = setInterval(() => { + void this.isMcpEnabled() + }, this.CHECK_INTERVAL) + + this.logging.info('ProfileStatusMonitor started - checking MCP configuration every 24 hours') + } + + stop(): void { + if (this.intervalId) { + clearInterval(this.intervalId) + this.intervalId = undefined + this.logging.info('ProfileStatusMonitor stopped') + } + } + + private async isMcpEnabled(): Promise { + try { + const profileArn = this.getProfileArn() + if (!profileArn) { + this.logging.debug('No profile ARN available for MCP configuration check') + ProfileStatusMonitor.lastMcpState = true // Default to enabled if no profile + return true + } + + if (!this.codeWhispererClient) { + this.codeWhispererClient = new CodeWhispererServiceToken( + this.credentialsProvider, + this.workspace, + this.logging, + process.env.CODEWHISPERER_REGION || DEFAULT_AWS_Q_REGION, + process.env.CODEWHISPERER_ENDPOINT || DEFAULT_AWS_Q_ENDPOINT_URL, + this.sdkInitializator + ) + this.codeWhispererClient.profileArn = profileArn + } + + const response = await retryUtils.retryWithBackoff(() => + this.codeWhispererClient!.getProfile({ profileArn }) + ) + const isMcpEnabled = response?.profile?.optInFeatures?.mcpConfiguration?.toggle === 'ON' + + if (ProfileStatusMonitor.lastMcpState !== isMcpEnabled) { + ProfileStatusMonitor.lastMcpState = isMcpEnabled + if (!isMcpEnabled) { + this.logging.info('MCP configuration disabled - removing tools') + this.onMcpDisabled() + } else if (isMcpEnabled && this.onMcpEnabled) { + this.logging.info('MCP configuration enabled - initializing tools') + this.onMcpEnabled() + } + } + + return isMcpEnabled + } catch (error) { + this.logging.debug(`MCP configuration check failed, defaulting to enabled: ${error}`) + ProfileStatusMonitor.lastMcpState = true + return true + } + } + + private getProfileArn(): string | undefined { + // Use cached value if available + if (this.cachedProfileArn) { + return this.cachedProfileArn + } + + try { + // Get profile ARN from service manager like in agenticChatController + const serviceManager = AmazonQTokenServiceManager.getInstance() + this.cachedProfileArn = serviceManager.getActiveProfileArn() + return this.cachedProfileArn + } catch (error) { + this.logging.debug(`Failed to get profile ARN: ${error}`) + } + return undefined + } + + static getMcpState(): boolean | undefined { + return ProfileStatusMonitor.lastMcpState + } +} 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 54eba4c852..fd692e6e8e 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 @@ -24,6 +24,9 @@ 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' +import { ProfileStatusMonitor } from './mcp/profileStatusMonitor' +import { AmazonQTokenServiceManager } from '../../../shared/amazonQServiceManager/AmazonQTokenServiceManager' +import { SERVICE_MANAGER_TIMEOUT_MS, SERVICE_MANAGER_POLL_INTERVAL_MS } from '../constants/constants' export const FsToolsServer: Server = ({ workspace, logging, agent, lsp }) => { const fsReadTool = new FsRead({ workspace, lsp, logging }) @@ -199,10 +202,45 @@ export const LspToolsServer: Server = ({ workspace, logging, lsp, agent }) => { return () => {} } -export const McpToolsServer: Server = ({ credentialsProvider, workspace, logging, lsp, agent, telemetry, runtime }) => { +export const McpToolsServer: Server = ({ + credentialsProvider, + workspace, + logging, + lsp, + agent, + telemetry, + runtime, + sdkInitializator, + chat, +}) => { const registered: Record = {} - const allNamespacedTools = new Set() + let profileStatusMonitor: ProfileStatusMonitor | undefined + + function removeAllMcpTools(): void { + logging.info('Removing all MCP tools due to admin configuration') + for (const [server, toolNames] of Object.entries(registered)) { + for (const name of toolNames) { + agent.removeTool(name) + allNamespacedTools.delete(name) + logging.info(`MCP: removed tool ${name}`) + } + registered[server] = [] + } + void McpManager.instance.close(true) //keep the instance but close all servers. + + try { + chat?.sendChatUpdate({ + tabId: 'mcpserver', + data: { + placeholderText: 'mcp-server-update', + messages: [], + }, + }) + } catch (error) { + logging.error(`Failed to send chatOptionsUpdate: ${error}`) + } + } function registerServerTools(server: string, defs: McpToolDefinition[]) { // 1) remove old tools @@ -257,20 +295,13 @@ export const McpToolsServer: Server = ({ credentialsProvider, workspace, logging } } - lsp.onInitialized(async () => { + async function initializeMcp() { try { - if (!enabledMCP(lsp.getClientInitializeParams())) { - logging.warn('MCP is currently not supported') - return - } - const wsUris = workspace.getAllWorkspaceFolders()?.map(f => f.uri) ?? [] - // Get agent paths const wsAgentPaths = getWorkspaceAgentConfigPaths(wsUris) const globalAgentPath = getGlobalAgentConfigPath(workspace.fs.getUserHomeDir()) const allAgentPaths = [...wsAgentPaths, globalAgentPath] - // Migrate config and persona files to agent config await migrateToAgentConfig(workspace, logging, agent) const mgr = await McpManager.init(allAgentPaths, { @@ -282,13 +313,9 @@ export const McpToolsServer: Server = ({ credentialsProvider, workspace, logging runtime, }) - // Clear tool name mapping before registering all tools to avoid conflicts from previous registrations McpManager.instance.clearToolNameMapping() const byServer: Record = {} - - logging.info(`enabled Tools: ${mgr.getEnabledTools().entries()}`) - // only register enabled tools for (const d of mgr.getEnabledTools()) { ;(byServer[d.serverName] ||= []).push(d) } @@ -300,11 +327,76 @@ export const McpToolsServer: Server = ({ credentialsProvider, workspace, logging registerServerTools(server, defs) }) } catch (e) { - console.warn('Caught error during MCP tool initialization; initialization may be incomplete:', e) + logging.error(`Failed to initialize MCP:' ${e}`) + } + } + + lsp.onInitialized(async () => { + try { + if (!enabledMCP(lsp.getClientInitializeParams())) { + logging.warn('MCP is currently not supported') + return + } + + if (sdkInitializator) { + profileStatusMonitor = new ProfileStatusMonitor( + credentialsProvider, + workspace, + logging, + sdkInitializator, + removeAllMcpTools, + async () => { + logging.info('MCP enabled by profile status monitor') + await initializeMcp() + } + ) + + // Wait for profile ARN to be available before checking MCP state + const checkAndInitialize = async () => { + const shouldInitialize = await profileStatusMonitor!.checkInitialState() + if (shouldInitialize) { + logging.info('MCP enabled, initializing immediately') + await initializeMcp() + } + profileStatusMonitor!.start() + } + + // Check if service manager is ready + try { + const serviceManager = AmazonQTokenServiceManager.getInstance() + if (serviceManager.getState() === 'INITIALIZED') { + await checkAndInitialize() + } else { + // Poll for service manager to be ready with 10s timeout + const startTime = Date.now() + const pollForReady = async () => { + if (serviceManager.getState() === 'INITIALIZED') { + await checkAndInitialize() + } else if (Date.now() - startTime < SERVICE_MANAGER_TIMEOUT_MS) { + setTimeout(pollForReady, SERVICE_MANAGER_POLL_INTERVAL_MS) + } else { + logging.warn('Service manager not ready after 10s, defaulting MCP to enabled') + await initializeMcp() + profileStatusMonitor!.start() + } + } + setTimeout(pollForReady, SERVICE_MANAGER_POLL_INTERVAL_MS) + } + } catch (error) { + // Service manager not initialized yet, default to enabled + logging.info('Service manager not ready, defaulting MCP to enabled') + await initializeMcp() + profileStatusMonitor!.start() + } + } + } catch (error) { + console.warn('Caught error during MCP tool initialization; initialization may be incomplete:', error) + logging.error(`Failed to initialize MCP in onInitialized: ${error}`) } }) return async () => { + profileStatusMonitor?.stop() await McpManager.instance.close() } } diff --git a/server/aws-lsp-codewhisperer/src/shared/codeWhispererService.ts b/server/aws-lsp-codewhisperer/src/shared/codeWhispererService.ts index a162ff44e6..aafc0aaf4d 100644 --- a/server/aws-lsp-codewhisperer/src/shared/codeWhispererService.ts +++ b/server/aws-lsp-codewhisperer/src/shared/codeWhispererService.ts @@ -581,6 +581,13 @@ export class CodeWhispererServiceToken extends CodeWhispererServiceBase { return this.client.getCodeAnalysis(this.withProfileArn(request)).promise() } + /** + * @description Get profile details + */ + async getProfile(request: { profileArn: string }) { + return this.client.getProfile(request).promise() + } + /** * @description Once scan completed successfully, send a request to get list of all the findings for the given scan. */ From 91c839857f8aa4d79098189f9fb620b361c51289 Mon Sep 17 00:00:00 2001 From: Lei Gao <97199248+leigaol@users.noreply.github.com> Date: Wed, 13 Aug 2025 14:04:19 -0700 Subject: [PATCH 14/74] fix: Use file context override in the inline completion params for Jupyter Notebook (#2114) --- app/aws-lsp-antlr4-runtimes/package.json | 2 +- app/aws-lsp-buildspec-runtimes/package.json | 2 +- .../package.json | 2 +- .../package.json | 2 +- app/aws-lsp-identity-runtimes/package.json | 2 +- app/aws-lsp-json-runtimes/package.json | 2 +- .../package.json | 2 +- app/aws-lsp-partiql-runtimes/package.json | 2 +- app/aws-lsp-s3-runtimes/package.json | 2 +- app/aws-lsp-yaml-json-webworker/package.json | 2 +- app/aws-lsp-yaml-runtimes/package.json | 2 +- app/hello-world-lsp-runtimes/package.json | 2 +- chat-client/package.json | 2 +- client/vscode/package.json | 2 +- core/aws-lsp-core/package.json | 2 +- .../q-agentic-chat-server/package.json | 2 +- package-lock.json | 70 +++++++++---------- server/aws-lsp-antlr4/package.json | 2 +- server/aws-lsp-buildspec/package.json | 2 +- server/aws-lsp-cloudformation/package.json | 2 +- server/aws-lsp-codewhisperer/package.json | 2 +- .../codeWhispererServer.test.ts | 49 ++++++++++++- .../inline-completion/codeWhispererServer.ts | 46 +++++++++--- .../inline-completion/trigger.ts | 17 ++--- .../src/shared/codeWhispererService.ts | 20 +++--- server/aws-lsp-identity/package.json | 2 +- server/aws-lsp-json/package.json | 2 +- server/aws-lsp-notification/package.json | 2 +- server/aws-lsp-partiql/package.json | 2 +- server/aws-lsp-s3/package.json | 2 +- server/aws-lsp-yaml/package.json | 2 +- server/device-sso-auth-lsp/package.json | 2 +- server/hello-world-lsp/package.json | 2 +- 33 files changed, 163 insertions(+), 95 deletions(-) diff --git a/app/aws-lsp-antlr4-runtimes/package.json b/app/aws-lsp-antlr4-runtimes/package.json index bf7cf47bf1..4289cec4b1 100644 --- a/app/aws-lsp-antlr4-runtimes/package.json +++ b/app/aws-lsp-antlr4-runtimes/package.json @@ -12,7 +12,7 @@ "webpack": "webpack" }, "dependencies": { - "@aws/language-server-runtimes": "^0.2.123", + "@aws/language-server-runtimes": "^0.2.125", "@aws/lsp-antlr4": "*", "antlr4-c3": "^3.4.1", "antlr4ng": "^3.0.4" diff --git a/app/aws-lsp-buildspec-runtimes/package.json b/app/aws-lsp-buildspec-runtimes/package.json index b9c36946b2..700afb3e7f 100644 --- a/app/aws-lsp-buildspec-runtimes/package.json +++ b/app/aws-lsp-buildspec-runtimes/package.json @@ -7,7 +7,7 @@ "compile": "tsc --build" }, "dependencies": { - "@aws/language-server-runtimes": "^0.2.123", + "@aws/language-server-runtimes": "^0.2.125", "@aws/lsp-buildspec": "^0.0.1" } } diff --git a/app/aws-lsp-cloudformation-runtimes/package.json b/app/aws-lsp-cloudformation-runtimes/package.json index a88386db4e..5cc04150cb 100644 --- a/app/aws-lsp-cloudformation-runtimes/package.json +++ b/app/aws-lsp-cloudformation-runtimes/package.json @@ -7,7 +7,7 @@ "compile": "tsc --build" }, "dependencies": { - "@aws/language-server-runtimes": "^0.2.123", + "@aws/language-server-runtimes": "^0.2.125", "@aws/lsp-cloudformation": "^0.0.1" } } diff --git a/app/aws-lsp-codewhisperer-runtimes/package.json b/app/aws-lsp-codewhisperer-runtimes/package.json index 487d2c5b1a..fc31e64589 100644 --- a/app/aws-lsp-codewhisperer-runtimes/package.json +++ b/app/aws-lsp-codewhisperer-runtimes/package.json @@ -23,7 +23,7 @@ "local-build": "node scripts/local-build.js" }, "dependencies": { - "@aws/language-server-runtimes": "^0.2.123", + "@aws/language-server-runtimes": "^0.2.125", "@aws/lsp-codewhisperer": "*", "copyfiles": "^2.4.1", "cross-env": "^7.0.3", diff --git a/app/aws-lsp-identity-runtimes/package.json b/app/aws-lsp-identity-runtimes/package.json index 46abf7d958..869a30cb20 100644 --- a/app/aws-lsp-identity-runtimes/package.json +++ b/app/aws-lsp-identity-runtimes/package.json @@ -7,7 +7,7 @@ "compile": "tsc --build" }, "dependencies": { - "@aws/language-server-runtimes": "^0.2.123", + "@aws/language-server-runtimes": "^0.2.125", "@aws/lsp-identity": "^0.0.1" } } diff --git a/app/aws-lsp-json-runtimes/package.json b/app/aws-lsp-json-runtimes/package.json index 24ae3535ac..f63dd4e985 100644 --- a/app/aws-lsp-json-runtimes/package.json +++ b/app/aws-lsp-json-runtimes/package.json @@ -11,7 +11,7 @@ "webpack": "webpack" }, "dependencies": { - "@aws/language-server-runtimes": "^0.2.123", + "@aws/language-server-runtimes": "^0.2.125", "@aws/lsp-json": "*" }, "devDependencies": { diff --git a/app/aws-lsp-notification-runtimes/package.json b/app/aws-lsp-notification-runtimes/package.json index 1e7641e2a8..faf9e4c24c 100644 --- a/app/aws-lsp-notification-runtimes/package.json +++ b/app/aws-lsp-notification-runtimes/package.json @@ -7,7 +7,7 @@ "compile": "tsc --build" }, "dependencies": { - "@aws/language-server-runtimes": "^0.2.123", + "@aws/language-server-runtimes": "^0.2.125", "@aws/lsp-notification": "^0.0.1" } } diff --git a/app/aws-lsp-partiql-runtimes/package.json b/app/aws-lsp-partiql-runtimes/package.json index 0d5e07cddf..d483f3d0ce 100644 --- a/app/aws-lsp-partiql-runtimes/package.json +++ b/app/aws-lsp-partiql-runtimes/package.json @@ -11,7 +11,7 @@ "package": "npm run compile && npm run compile:webpack" }, "dependencies": { - "@aws/language-server-runtimes": "^0.2.120", + "@aws/language-server-runtimes": "^0.2.125", "@aws/lsp-partiql": "^0.0.5" }, "devDependencies": { diff --git a/app/aws-lsp-s3-runtimes/package.json b/app/aws-lsp-s3-runtimes/package.json index ad84f62776..42efa998be 100644 --- a/app/aws-lsp-s3-runtimes/package.json +++ b/app/aws-lsp-s3-runtimes/package.json @@ -10,7 +10,7 @@ "compile": "tsc --build" }, "dependencies": { - "@aws/language-server-runtimes": "^0.2.123", + "@aws/language-server-runtimes": "^0.2.125", "@aws/lsp-s3": "^0.0.1" } } diff --git a/app/aws-lsp-yaml-json-webworker/package.json b/app/aws-lsp-yaml-json-webworker/package.json index 7079d1fa3b..6190893816 100644 --- a/app/aws-lsp-yaml-json-webworker/package.json +++ b/app/aws-lsp-yaml-json-webworker/package.json @@ -11,7 +11,7 @@ "serve:webpack": "NODE_ENV=development webpack serve" }, "dependencies": { - "@aws/language-server-runtimes": "^0.2.123", + "@aws/language-server-runtimes": "^0.2.125", "@aws/lsp-json": "*", "@aws/lsp-yaml": "*" }, diff --git a/app/aws-lsp-yaml-runtimes/package.json b/app/aws-lsp-yaml-runtimes/package.json index a59f919477..eb76a46e38 100644 --- a/app/aws-lsp-yaml-runtimes/package.json +++ b/app/aws-lsp-yaml-runtimes/package.json @@ -11,7 +11,7 @@ "webpack": "webpack" }, "dependencies": { - "@aws/language-server-runtimes": "^0.2.123", + "@aws/language-server-runtimes": "^0.2.125", "@aws/lsp-yaml": "*" }, "devDependencies": { diff --git a/app/hello-world-lsp-runtimes/package.json b/app/hello-world-lsp-runtimes/package.json index 54018d89d0..14a3a75ff4 100644 --- a/app/hello-world-lsp-runtimes/package.json +++ b/app/hello-world-lsp-runtimes/package.json @@ -15,7 +15,7 @@ }, "dependencies": { "@aws/hello-world-lsp": "^0.0.1", - "@aws/language-server-runtimes": "^0.2.123" + "@aws/language-server-runtimes": "^0.2.125" }, "devDependencies": { "@types/chai": "^4.3.5", diff --git a/chat-client/package.json b/chat-client/package.json index df0378f3e4..2ee60a8bec 100644 --- a/chat-client/package.json +++ b/chat-client/package.json @@ -25,7 +25,7 @@ }, "dependencies": { "@aws/chat-client-ui-types": "^0.1.56", - "@aws/language-server-runtimes": "^0.2.123", + "@aws/language-server-runtimes": "^0.2.125", "@aws/language-server-runtimes-types": "^0.1.50", "@aws/mynah-ui": "^4.36.4" }, diff --git a/client/vscode/package.json b/client/vscode/package.json index 975a395d66..409a4fc50d 100644 --- a/client/vscode/package.json +++ b/client/vscode/package.json @@ -352,7 +352,7 @@ "@aws-sdk/credential-providers": "^3.731.1", "@aws-sdk/types": "^3.734.0", "@aws/chat-client-ui-types": "^0.1.56", - "@aws/language-server-runtimes": "^0.2.123", + "@aws/language-server-runtimes": "^0.2.125", "@types/uuid": "^9.0.8", "@types/vscode": "^1.98.0", "jose": "^5.2.4", diff --git a/core/aws-lsp-core/package.json b/core/aws-lsp-core/package.json index aeff582d34..5a3d438d47 100644 --- a/core/aws-lsp-core/package.json +++ b/core/aws-lsp-core/package.json @@ -28,7 +28,7 @@ "prepack": "shx cp ../../LICENSE ../../NOTICE ../../SECURITY.md ." }, "dependencies": { - "@aws/language-server-runtimes": "^0.2.123", + "@aws/language-server-runtimes": "^0.2.125", "@gerhobbelt/gitignore-parser": "^0.2.0-9", "cross-spawn": "7.0.6", "jose": "^5.2.4", diff --git a/integration-tests/q-agentic-chat-server/package.json b/integration-tests/q-agentic-chat-server/package.json index 5431142f5c..1e93a252dd 100644 --- a/integration-tests/q-agentic-chat-server/package.json +++ b/integration-tests/q-agentic-chat-server/package.json @@ -9,7 +9,7 @@ "test-integ": "npm run compile && mocha --timeout 30000 \"./out/**/*.test.js\" --retries 2" }, "dependencies": { - "@aws/language-server-runtimes": "^0.2.123", + "@aws/language-server-runtimes": "^0.2.125", "@aws/lsp-core": "*" }, "devDependencies": { diff --git a/package-lock.json b/package-lock.json index b42f206c44..d1ac2ece1c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -48,7 +48,7 @@ "name": "@aws/lsp-antlr4-runtimes", "version": "0.0.1", "dependencies": { - "@aws/language-server-runtimes": "^0.2.123", + "@aws/language-server-runtimes": "^0.2.125", "@aws/lsp-antlr4": "*", "antlr4-c3": "^3.4.1", "antlr4ng": "^3.0.4" @@ -71,7 +71,7 @@ "name": "@aws/lsp-buildspec-runtimes", "version": "0.0.1", "dependencies": { - "@aws/language-server-runtimes": "^0.2.123", + "@aws/language-server-runtimes": "^0.2.125", "@aws/lsp-buildspec": "^0.0.1" } }, @@ -79,7 +79,7 @@ "name": "@aws/lsp-cloudformation-runtimes", "version": "0.0.1", "dependencies": { - "@aws/language-server-runtimes": "^0.2.123", + "@aws/language-server-runtimes": "^0.2.125", "@aws/lsp-cloudformation": "^0.0.1" } }, @@ -87,7 +87,7 @@ "name": "@aws/lsp-codewhisperer-runtimes", "version": "0.0.1", "dependencies": { - "@aws/language-server-runtimes": "^0.2.123", + "@aws/language-server-runtimes": "^0.2.125", "@aws/lsp-codewhisperer": "*", "copyfiles": "^2.4.1", "cross-env": "^7.0.3", @@ -120,7 +120,7 @@ "name": "@aws/lsp-identity-runtimes", "version": "0.1.0", "dependencies": { - "@aws/language-server-runtimes": "^0.2.123", + "@aws/language-server-runtimes": "^0.2.125", "@aws/lsp-identity": "^0.0.1" } }, @@ -128,7 +128,7 @@ "name": "@aws/lsp-json-runtimes", "version": "0.0.1", "dependencies": { - "@aws/language-server-runtimes": "^0.2.123", + "@aws/language-server-runtimes": "^0.2.125", "@aws/lsp-json": "*" }, "devDependencies": { @@ -148,7 +148,7 @@ "name": "@aws/lsp-notification-runtimes", "version": "0.1.0", "dependencies": { - "@aws/language-server-runtimes": "^0.2.123", + "@aws/language-server-runtimes": "^0.2.125", "@aws/lsp-notification": "^0.0.1" } }, @@ -156,7 +156,7 @@ "name": "@aws/lsp-partiql-runtimes", "version": "0.0.1", "dependencies": { - "@aws/language-server-runtimes": "^0.2.120", + "@aws/language-server-runtimes": "^0.2.125", "@aws/lsp-partiql": "^0.0.5" }, "devDependencies": { @@ -181,7 +181,7 @@ "name": "@aws/lsp-s3-runtimes", "version": "0.0.1", "dependencies": { - "@aws/language-server-runtimes": "^0.2.123", + "@aws/language-server-runtimes": "^0.2.125", "@aws/lsp-s3": "^0.0.1" }, "bin": { @@ -192,7 +192,7 @@ "name": "@aws/lsp-yaml-json-webworker", "version": "0.0.1", "dependencies": { - "@aws/language-server-runtimes": "^0.2.123", + "@aws/language-server-runtimes": "^0.2.125", "@aws/lsp-json": "*", "@aws/lsp-yaml": "*" }, @@ -212,7 +212,7 @@ "name": "@aws/lsp-yaml-runtimes", "version": "0.0.1", "dependencies": { - "@aws/language-server-runtimes": "^0.2.123", + "@aws/language-server-runtimes": "^0.2.125", "@aws/lsp-yaml": "*" }, "devDependencies": { @@ -234,7 +234,7 @@ "version": "0.0.1", "dependencies": { "@aws/hello-world-lsp": "^0.0.1", - "@aws/language-server-runtimes": "^0.2.123" + "@aws/language-server-runtimes": "^0.2.125" }, "devDependencies": { "@types/chai": "^4.3.5", @@ -255,7 +255,7 @@ "license": "Apache-2.0", "dependencies": { "@aws/chat-client-ui-types": "^0.1.56", - "@aws/language-server-runtimes": "^0.2.123", + "@aws/language-server-runtimes": "^0.2.125", "@aws/language-server-runtimes-types": "^0.1.50", "@aws/mynah-ui": "^4.36.4" }, @@ -280,7 +280,7 @@ "@aws-sdk/credential-providers": "^3.731.1", "@aws-sdk/types": "^3.734.0", "@aws/chat-client-ui-types": "^0.1.56", - "@aws/language-server-runtimes": "^0.2.123", + "@aws/language-server-runtimes": "^0.2.125", "@types/uuid": "^9.0.8", "@types/vscode": "^1.98.0", "jose": "^5.2.4", @@ -296,7 +296,7 @@ "version": "0.0.13", "license": "Apache-2.0", "dependencies": { - "@aws/language-server-runtimes": "^0.2.123", + "@aws/language-server-runtimes": "^0.2.125", "@gerhobbelt/gitignore-parser": "^0.2.0-9", "cross-spawn": "7.0.6", "jose": "^5.2.4", @@ -327,7 +327,7 @@ "name": "@aws/q-agentic-chat-server-integration-tests", "version": "0.0.1", "dependencies": { - "@aws/language-server-runtimes": "^0.2.123", + "@aws/language-server-runtimes": "^0.2.125", "@aws/lsp-core": "*" }, "devDependencies": { @@ -4036,12 +4036,12 @@ "link": true }, "node_modules/@aws/language-server-runtimes": { - "version": "0.2.123", - "resolved": "https://registry.npmjs.org/@aws/language-server-runtimes/-/language-server-runtimes-0.2.123.tgz", - "integrity": "sha512-gxjnBcQY+HR9+F1NXQUEQ6ikJhrLMJEbrpIxlBLILtQ75hVtRDsfGET3KW5Nn0dgbrQTx6VqwvXDfolUkmi06g==", + "version": "0.2.125", + "resolved": "https://registry.npmjs.org/@aws/language-server-runtimes/-/language-server-runtimes-0.2.125.tgz", + "integrity": "sha512-tjXJEagZ6rm09fcgJGu1zbFNzi0+7R1mdNFa6zCIv68c76xq5JHjc++Hne9aOgp61O6BM9uNnX3KR57v9/0E1g==", "license": "Apache-2.0", "dependencies": { - "@aws/language-server-runtimes-types": "^0.1.55", + "@aws/language-server-runtimes-types": "^0.1.56", "@opentelemetry/api": "^1.9.0", "@opentelemetry/api-logs": "^0.200.0", "@opentelemetry/core": "^2.0.0", @@ -4068,9 +4068,9 @@ } }, "node_modules/@aws/language-server-runtimes-types": { - "version": "0.1.55", - "resolved": "https://registry.npmjs.org/@aws/language-server-runtimes-types/-/language-server-runtimes-types-0.1.55.tgz", - "integrity": "sha512-KRy3fTCNGvAQxA4amTODXPuubxrYlqKsyJOXPaIn+YDACwJa7shrOryHg6xrib6uHAHT2fEkcTMk9TT4MRPxQA==", + "version": "0.1.56", + "resolved": "https://registry.npmjs.org/@aws/language-server-runtimes-types/-/language-server-runtimes-types-0.1.56.tgz", + "integrity": "sha512-Md/L750JShCHUsCQUJva51Ofkn/GDBEX8PpZnWUIVqkpddDR00SLQS2smNf4UHtKNJ2fefsfks/Kqfuatjkjvg==", "license": "Apache-2.0", "dependencies": { "vscode-languageserver-textdocument": "^1.0.12", @@ -28608,7 +28608,7 @@ "version": "0.1.17", "license": "Apache-2.0", "dependencies": { - "@aws/language-server-runtimes": "^0.2.123", + "@aws/language-server-runtimes": "^0.2.125", "@aws/lsp-core": "^0.0.13" }, "devDependencies": { @@ -28650,7 +28650,7 @@ "name": "@aws/lsp-buildspec", "version": "0.0.1", "dependencies": { - "@aws/language-server-runtimes": "^0.2.123", + "@aws/language-server-runtimes": "^0.2.125", "@aws/lsp-json": "*", "@aws/lsp-yaml": "*", "vscode-languageserver": "^9.0.1", @@ -28661,7 +28661,7 @@ "name": "@aws/lsp-cloudformation", "version": "0.0.1", "dependencies": { - "@aws/language-server-runtimes": "^0.2.123", + "@aws/language-server-runtimes": "^0.2.125", "@aws/lsp-core": "*", "@aws/lsp-json": "*", "vscode-languageserver": "^9.0.1", @@ -28683,7 +28683,7 @@ "@aws-sdk/util-arn-parser": "^3.723.0", "@aws-sdk/util-retry": "^3.374.0", "@aws/chat-client-ui-types": "^0.1.56", - "@aws/language-server-runtimes": "^0.2.123", + "@aws/language-server-runtimes": "^0.2.125", "@aws/lsp-core": "^0.0.13", "@modelcontextprotocol/sdk": "^1.15.0", "@smithy/node-http-handler": "^2.5.0", @@ -28823,7 +28823,7 @@ "dependencies": { "@aws-sdk/client-sso-oidc": "^3.616.0", "@aws-sdk/token-providers": "^3.744.0", - "@aws/language-server-runtimes": "^0.2.123", + "@aws/language-server-runtimes": "^0.2.125", "@aws/lsp-core": "^0.0.12", "@smithy/node-http-handler": "^3.2.5", "@smithy/shared-ini-file-loader": "^4.0.1", @@ -28888,7 +28888,7 @@ "version": "0.1.17", "license": "Apache-2.0", "dependencies": { - "@aws/language-server-runtimes": "^0.2.123", + "@aws/language-server-runtimes": "^0.2.125", "@aws/lsp-core": "^0.0.13", "vscode-languageserver": "^9.0.1", "vscode-languageserver-textdocument": "^1.0.8" @@ -28905,7 +28905,7 @@ "version": "0.0.1", "license": "Apache-2.0", "dependencies": { - "@aws/language-server-runtimes": "^0.2.123", + "@aws/language-server-runtimes": "^0.2.125", "@aws/lsp-core": "^0.0.12", "vscode-languageserver": "^9.0.1" }, @@ -28966,7 +28966,7 @@ "version": "0.0.16", "license": "Apache-2.0", "dependencies": { - "@aws/language-server-runtimes": "^0.2.123", + "@aws/language-server-runtimes": "^0.2.125", "antlr4-c3": "3.4.2", "antlr4ng": "3.0.14", "web-tree-sitter": "0.22.6" @@ -28988,7 +28988,7 @@ "dependencies": { "@aws-sdk/client-s3": "^3.623.0", "@aws-sdk/types": "^3.734.0", - "@aws/language-server-runtimes": "^0.2.123", + "@aws/language-server-runtimes": "^0.2.125", "@aws/lsp-core": "^0.0.12", "vscode-languageserver": "^9.0.1", "vscode-languageserver-textdocument": "^1.0.8" @@ -29019,7 +29019,7 @@ "hasInstallScript": true, "license": "Apache-2.0", "dependencies": { - "@aws/language-server-runtimes": "^0.2.123", + "@aws/language-server-runtimes": "^0.2.125", "@aws/lsp-core": "^0.0.13", "vscode-languageserver": "^9.0.1", "vscode-languageserver-textdocument": "^1.0.8", @@ -29033,7 +29033,7 @@ "name": "@amzn/device-sso-auth-lsp", "version": "0.0.1", "dependencies": { - "@aws/language-server-runtimes": "^0.2.123", + "@aws/language-server-runtimes": "^0.2.125", "vscode-languageserver": "^9.0.1" }, "devDependencies": { @@ -29044,7 +29044,7 @@ "name": "@aws/hello-world-lsp", "version": "0.0.1", "dependencies": { - "@aws/language-server-runtimes": "^0.2.123", + "@aws/language-server-runtimes": "^0.2.125", "vscode-languageserver": "^9.0.1" }, "devDependencies": { diff --git a/server/aws-lsp-antlr4/package.json b/server/aws-lsp-antlr4/package.json index 9d6c925b40..f540c8ae21 100644 --- a/server/aws-lsp-antlr4/package.json +++ b/server/aws-lsp-antlr4/package.json @@ -28,7 +28,7 @@ "clean": "rm -rf node_modules" }, "dependencies": { - "@aws/language-server-runtimes": "^0.2.123", + "@aws/language-server-runtimes": "^0.2.125", "@aws/lsp-core": "^0.0.13" }, "peerDependencies": { diff --git a/server/aws-lsp-buildspec/package.json b/server/aws-lsp-buildspec/package.json index f59edb5549..2cf0b776ac 100644 --- a/server/aws-lsp-buildspec/package.json +++ b/server/aws-lsp-buildspec/package.json @@ -7,7 +7,7 @@ "compile": "tsc --build" }, "dependencies": { - "@aws/language-server-runtimes": "^0.2.123", + "@aws/language-server-runtimes": "^0.2.125", "@aws/lsp-json": "*", "@aws/lsp-yaml": "*", "vscode-languageserver": "^9.0.1", diff --git a/server/aws-lsp-cloudformation/package.json b/server/aws-lsp-cloudformation/package.json index bfc8ebd7e5..ad01b4457a 100644 --- a/server/aws-lsp-cloudformation/package.json +++ b/server/aws-lsp-cloudformation/package.json @@ -7,7 +7,7 @@ "compile": "tsc --build" }, "dependencies": { - "@aws/language-server-runtimes": "^0.2.123", + "@aws/language-server-runtimes": "^0.2.125", "@aws/lsp-core": "*", "@aws/lsp-json": "*", "vscode-languageserver": "^9.0.1", diff --git a/server/aws-lsp-codewhisperer/package.json b/server/aws-lsp-codewhisperer/package.json index fdd34884b1..8600148090 100644 --- a/server/aws-lsp-codewhisperer/package.json +++ b/server/aws-lsp-codewhisperer/package.json @@ -36,7 +36,7 @@ "@aws-sdk/util-arn-parser": "^3.723.0", "@aws-sdk/util-retry": "^3.374.0", "@aws/chat-client-ui-types": "^0.1.56", - "@aws/language-server-runtimes": "^0.2.123", + "@aws/language-server-runtimes": "^0.2.125", "@aws/lsp-core": "^0.0.13", "@modelcontextprotocol/sdk": "^1.15.0", "@smithy/node-http-handler": "^2.5.0", diff --git a/server/aws-lsp-codewhisperer/src/language-server/inline-completion/codeWhispererServer.test.ts b/server/aws-lsp-codewhisperer/src/language-server/inline-completion/codeWhispererServer.test.ts index 29390248f7..8151602dd4 100644 --- a/server/aws-lsp-codewhisperer/src/language-server/inline-completion/codeWhispererServer.test.ts +++ b/server/aws-lsp-codewhisperer/src/language-server/inline-completion/codeWhispererServer.test.ts @@ -12,7 +12,7 @@ import { TestFeatures } from '@aws/language-server-runtimes/testing' import * as assert from 'assert' import { AWSError } from 'aws-sdk' import sinon, { StubbedInstance } from 'ts-sinon' -import { CodewhispererServerFactory } from './codeWhispererServer' +import { CodewhispererServerFactory, getLanguageIdFromUri } from './codeWhispererServer' import { CodeWhispererServiceBase, CodeWhispererServiceToken, @@ -2427,4 +2427,51 @@ describe('CodeWhisperer Server', () => { TestAmazonQServiceManager.resetInstance() }) }) + describe('getLanguageIdFromUri', () => { + it('should return python for notebook cell URIs', () => { + const uri = 'vscode-notebook-cell:/some/path/notebook.ipynb#cell1' + assert.strictEqual(getLanguageIdFromUri(uri), 'python') + }) + + it('should return abap for files with ABAP extensions', () => { + const uris = ['file:///path/to/file.asprog'] + + uris.forEach(uri => { + assert.strictEqual(getLanguageIdFromUri(uri), 'abap') + }) + }) + + it('should return empty string for non-ABAP files', () => { + const uris = ['file:///path/to/file.js', 'file:///path/to/file.ts', 'file:///path/to/file.py'] + + uris.forEach(uri => { + assert.strictEqual(getLanguageIdFromUri(uri), '') + }) + }) + + it('should return empty string for invalid URIs', () => { + const invalidUris = ['', 'invalid-uri', 'file:///'] + + invalidUris.forEach(uri => { + assert.strictEqual(getLanguageIdFromUri(uri), '') + }) + }) + + it('should log errors when provided with a logging object', () => { + const mockLogger = { + log: sinon.spy(), + } + + const invalidUri = {} as string // Force type error + getLanguageIdFromUri(invalidUri, mockLogger) + + sinon.assert.calledOnce(mockLogger.log) + sinon.assert.calledWith(mockLogger.log, sinon.match(/Error parsing URI to determine language:.*/)) + }) + + it('should handle URIs without extensions', () => { + const uri = 'file:///path/to/file' + assert.strictEqual(getLanguageIdFromUri(uri), '') + }) + }) }) diff --git a/server/aws-lsp-codewhisperer/src/language-server/inline-completion/codeWhispererServer.ts b/server/aws-lsp-codewhisperer/src/language-server/inline-completion/codeWhispererServer.ts index 23a72e3528..c1f2b464b2 100644 --- a/server/aws-lsp-codewhisperer/src/language-server/inline-completion/codeWhispererServer.ts +++ b/server/aws-lsp-codewhisperer/src/language-server/inline-completion/codeWhispererServer.ts @@ -15,13 +15,14 @@ import { } from '@aws/language-server-runtimes/server-interface' import { autoTrigger, getAutoTriggerType, getNormalizeOsName, triggerType } from './auto-trigger/autoTrigger' import { + FileContext, GenerateSuggestionsRequest, GenerateSuggestionsResponse, getFileContext, Suggestion, SuggestionType, } from '../../shared/codeWhispererService' -import { getSupportedLanguageId } from '../../shared/languageDetection' +import { CodewhispererLanguage, getSupportedLanguageId } from '../../shared/languageDetection' import { mergeEditSuggestionsWithFileContext, truncateOverlapWithRightContext } from './mergeRightUtils' import { CodeWhispererSession, SessionManager } from './session/sessionManager' import { CodePercentageTracker } from './codePercentage' @@ -41,7 +42,6 @@ import { AmazonQWorkspaceConfig } from '../../shared/amazonQServiceManager/confi import { hasConnectionExpired } from '../../shared/utils' import { getOrThrowBaseIAMServiceManager } from '../../shared/amazonQServiceManager/AmazonQIAMServiceManager' import { WorkspaceFolderManager } from '../workspaceContext/workspaceFolderManager' -import path = require('path') import { UserWrittenCodeTracker } from '../../shared/userWrittenCodeTracker' import { RecentEditTracker, RecentEditTrackerDefaultConfig } from './tracker/codeEditTracker' import { CursorTracker } from './tracker/cursorTracker' @@ -191,7 +191,10 @@ export const CodewhispererServerFactory = return EMPTY_RESULT } - const inferredLanguageId = getSupportedLanguageId(textDocument) + let inferredLanguageId = getSupportedLanguageId(textDocument) + if (params.fileContextOverride?.programmingLanguage) { + inferredLanguageId = params.fileContextOverride?.programmingLanguage as CodewhispererLanguage + } if (!inferredLanguageId) { logging.log( `textDocument [${params.textDocument.uri}] with languageId [${textDocument.languageId}] not supported` @@ -204,12 +207,29 @@ export const CodewhispererServerFactory = params.context.triggerKind == InlineCompletionTriggerKind.Automatic const maxResults = isAutomaticLspTriggerKind ? 1 : 5 const selectionRange = params.context.selectedCompletionInfo?.range - const fileContext = getFileContext({ - textDocument, - inferredLanguageId, - position: params.position, - workspaceFolder: workspace.getWorkspaceFolder(textDocument.uri), - }) + + // For Jupyter Notebook in VSC, the language server does not have access to + // its internal states including current active cell index, etc + // we rely on VSC to calculate file context + let fileContext: FileContext | undefined = undefined + if (params.fileContextOverride) { + fileContext = { + leftFileContent: params.fileContextOverride.leftFileContent, + rightFileContent: params.fileContextOverride.rightFileContent, + filename: params.fileContextOverride.filename, + fileUri: params.fileContextOverride.fileUri, + programmingLanguage: { + languageName: inferredLanguageId, + }, + } + } else { + fileContext = getFileContext({ + textDocument, + inferredLanguageId, + position: params.position, + workspaceFolder: workspace.getWorkspaceFolder(textDocument.uri), + }) + } const workspaceState = WorkspaceFolderManager.getInstance()?.getWorkspaceState() const workspaceId = workspaceState?.webSocketClient?.isConnected() @@ -327,7 +347,7 @@ export const CodewhispererServerFactory = document: textDocument, startPosition: params.position, triggerType: isAutomaticLspTriggerKind ? 'AutoTrigger' : 'OnDemand', - language: fileContext.programmingLanguage.languageName, + language: fileContext.programmingLanguage.languageName as CodewhispererLanguage, requestContext: requestContext, autoTriggerType: isAutomaticLspTriggerKind ? codewhispererAutoTriggerType : undefined, triggerCharacter: triggerCharacters, @@ -892,8 +912,12 @@ export const CodewhispererServerFactory = export const CodeWhispererServerIAM = CodewhispererServerFactory(getOrThrowBaseIAMServiceManager) export const CodeWhispererServerToken = CodewhispererServerFactory(getOrThrowBaseTokenServiceManager) -const getLanguageIdFromUri = (uri: string, logging?: any): string => { +export const getLanguageIdFromUri = (uri: string, logging?: any): string => { try { + if (uri.startsWith('vscode-notebook-cell:')) { + // use python for now as lsp does not support JL cell language detection + return 'python' + } const extension = uri.split('.').pop()?.toLowerCase() return ABAP_EXTENSIONS.has(extension || '') ? 'abap' : '' } catch (err) { diff --git a/server/aws-lsp-codewhisperer/src/language-server/inline-completion/trigger.ts b/server/aws-lsp-codewhisperer/src/language-server/inline-completion/trigger.ts index 06453355a8..305b9b6e5c 100644 --- a/server/aws-lsp-codewhisperer/src/language-server/inline-completion/trigger.ts +++ b/server/aws-lsp-codewhisperer/src/language-server/inline-completion/trigger.ts @@ -1,24 +1,19 @@ -import { CodewhispererLanguage } from '../../shared/languageDetection' import { SessionManager } from './session/sessionManager' import { InlineCompletionWithReferencesParams } from '@aws/language-server-runtimes/protocol' import { editPredictionAutoTrigger } from './auto-trigger/editPredictionAutoTrigger' import { CursorTracker } from './tracker/cursorTracker' import { RecentEditTracker } from './tracker/codeEditTracker' -import { CodeWhispererServiceBase, CodeWhispererServiceToken } from '../../shared/codeWhispererService' +import { + CodeWhispererServiceBase, + CodeWhispererServiceToken, + ClientFileContext, +} from '../../shared/codeWhispererService' export class NepTrigger {} export function shouldTriggerEdits( service: CodeWhispererServiceBase, - fileContext: { - fileUri: string - filename: string - programmingLanguage: { - languageName: CodewhispererLanguage - } - leftFileContent: string - rightFileContent: string - }, + fileContext: ClientFileContext, inlineParams: InlineCompletionWithReferencesParams, cursorTracker: CursorTracker, recentEditsTracker: RecentEditTracker, diff --git a/server/aws-lsp-codewhisperer/src/shared/codeWhispererService.ts b/server/aws-lsp-codewhisperer/src/shared/codeWhispererService.ts index aafc0aaf4d..01101d68a8 100644 --- a/server/aws-lsp-codewhisperer/src/shared/codeWhispererService.ts +++ b/server/aws-lsp-codewhisperer/src/shared/codeWhispererService.ts @@ -71,20 +71,22 @@ export interface GenerateSuggestionsResponse { responseContext: ResponseContext } +export interface ClientFileContext { + leftFileContent: string + rightFileContent: string + filename: string + fileUri: string + programmingLanguage: { + languageName: CodewhispererLanguage + } +} + export function getFileContext(params: { textDocument: TextDocument position: Position inferredLanguageId: CodewhispererLanguage workspaceFolder: WorkspaceFolder | null | undefined -}): { - fileUri: string - filename: string - programmingLanguage: { - languageName: CodewhispererLanguage - } - leftFileContent: string - rightFileContent: string -} { +}): ClientFileContext { const left = params.textDocument.getText({ start: { line: 0, character: 0 }, end: params.position, diff --git a/server/aws-lsp-identity/package.json b/server/aws-lsp-identity/package.json index e8cb3b2c8a..acaeb6d15d 100644 --- a/server/aws-lsp-identity/package.json +++ b/server/aws-lsp-identity/package.json @@ -26,7 +26,7 @@ "dependencies": { "@aws-sdk/client-sso-oidc": "^3.616.0", "@aws-sdk/token-providers": "^3.744.0", - "@aws/language-server-runtimes": "^0.2.123", + "@aws/language-server-runtimes": "^0.2.125", "@aws/lsp-core": "^0.0.12", "@smithy/node-http-handler": "^3.2.5", "@smithy/shared-ini-file-loader": "^4.0.1", diff --git a/server/aws-lsp-json/package.json b/server/aws-lsp-json/package.json index 298f3820f0..7b1768f995 100644 --- a/server/aws-lsp-json/package.json +++ b/server/aws-lsp-json/package.json @@ -26,7 +26,7 @@ "prepack": "shx cp ../../LICENSE ../../NOTICE ../../SECURITY.md ." }, "dependencies": { - "@aws/language-server-runtimes": "^0.2.123", + "@aws/language-server-runtimes": "^0.2.125", "@aws/lsp-core": "^0.0.13", "vscode-languageserver": "^9.0.1", "vscode-languageserver-textdocument": "^1.0.8" diff --git a/server/aws-lsp-notification/package.json b/server/aws-lsp-notification/package.json index a9e3cf9d4c..45194be66b 100644 --- a/server/aws-lsp-notification/package.json +++ b/server/aws-lsp-notification/package.json @@ -22,7 +22,7 @@ "coverage:report": "c8 report --reporter=html --reporter=text" }, "dependencies": { - "@aws/language-server-runtimes": "^0.2.123", + "@aws/language-server-runtimes": "^0.2.125", "@aws/lsp-core": "^0.0.12", "vscode-languageserver": "^9.0.1" }, diff --git a/server/aws-lsp-partiql/package.json b/server/aws-lsp-partiql/package.json index 9ff685cfdc..263275dced 100644 --- a/server/aws-lsp-partiql/package.json +++ b/server/aws-lsp-partiql/package.json @@ -24,7 +24,7 @@ "out" ], "dependencies": { - "@aws/language-server-runtimes": "^0.2.123", + "@aws/language-server-runtimes": "^0.2.125", "antlr4-c3": "3.4.2", "antlr4ng": "3.0.14", "web-tree-sitter": "0.22.6" diff --git a/server/aws-lsp-s3/package.json b/server/aws-lsp-s3/package.json index caa7801b5f..53355ff18a 100644 --- a/server/aws-lsp-s3/package.json +++ b/server/aws-lsp-s3/package.json @@ -9,7 +9,7 @@ "dependencies": { "@aws-sdk/client-s3": "^3.623.0", "@aws-sdk/types": "^3.734.0", - "@aws/language-server-runtimes": "^0.2.123", + "@aws/language-server-runtimes": "^0.2.125", "@aws/lsp-core": "^0.0.12", "vscode-languageserver": "^9.0.1", "vscode-languageserver-textdocument": "^1.0.8" diff --git a/server/aws-lsp-yaml/package.json b/server/aws-lsp-yaml/package.json index 58fd5e591d..deeddd9694 100644 --- a/server/aws-lsp-yaml/package.json +++ b/server/aws-lsp-yaml/package.json @@ -26,7 +26,7 @@ "postinstall": "node patchYamlPackage.js" }, "dependencies": { - "@aws/language-server-runtimes": "^0.2.123", + "@aws/language-server-runtimes": "^0.2.125", "@aws/lsp-core": "^0.0.13", "vscode-languageserver": "^9.0.1", "vscode-languageserver-textdocument": "^1.0.8", diff --git a/server/device-sso-auth-lsp/package.json b/server/device-sso-auth-lsp/package.json index 573170a3a9..698bbf353e 100644 --- a/server/device-sso-auth-lsp/package.json +++ b/server/device-sso-auth-lsp/package.json @@ -7,7 +7,7 @@ "compile": "tsc --build" }, "dependencies": { - "@aws/language-server-runtimes": "^0.2.123", + "@aws/language-server-runtimes": "^0.2.125", "vscode-languageserver": "^9.0.1" }, "devDependencies": { diff --git a/server/hello-world-lsp/package.json b/server/hello-world-lsp/package.json index 827509803e..a0ad8e8469 100644 --- a/server/hello-world-lsp/package.json +++ b/server/hello-world-lsp/package.json @@ -13,7 +13,7 @@ "coverage:report": "c8 report --reporter=html --reporter=text" }, "dependencies": { - "@aws/language-server-runtimes": "^0.2.123", + "@aws/language-server-runtimes": "^0.2.125", "vscode-languageserver": "^9.0.1" }, "devDependencies": { From 93cf229149ba60491f9f5763793db4a9f570b611 Mon Sep 17 00:00:00 2001 From: Tai Lai Date: Wed, 13 Aug 2025 16:19:26 -0700 Subject: [PATCH 15/74] feat(amazonq): read tool ui revamp (#2113) (#2121) * feat(amazonq): read tool ui revamp * feat(amazonq): read tool message revamp (#2049) * feat(amazonq): read tool message revamp * fix tests * feat: file search ui (#2078) * feat: file search ui * fix tests * fix integration tests * remove unnecessary type check * fix: use quotes instead of backticks * fix header update issue * fix integration test --- chat-client/src/client/mynahUi.ts | 16 +- .../src/tests/agenticChatInteg.test.ts | 13 +- .../agenticChat/agenticChatController.test.ts | 6 +- .../agenticChat/agenticChatController.ts | 188 ++++++++++++------ .../agenticChat/agenticChatResultStream.ts | 63 ++---- .../agenticChat/tools/fileSearch.ts | 4 + 6 files changed, 163 insertions(+), 127 deletions(-) diff --git a/chat-client/src/client/mynahUi.ts b/chat-client/src/client/mynahUi.ts index 3b42249331..527dea06f3 100644 --- a/chat-client/src/client/mynahUi.ts +++ b/chat-client/src/client/mynahUi.ts @@ -1353,10 +1353,15 @@ export const createMynahUi = ( fileTreeTitle: '', hideFileCount: true, details: toDetailsWithoutIcon(header.fileList.details), + renderAsPills: + !header.fileList.details || + (Object.values(header.fileList.details).every(detail => !detail.changes) && + (!header.buttons || !header.buttons.some(button => button.id === 'undo-changes')) && + !header.status?.icon), } } if (!isPartialResult) { - if (processedHeader) { + if (processedHeader && !message.header?.status) { processedHeader.status = undefined } } @@ -1369,7 +1374,8 @@ export const createMynahUi = ( processedHeader.buttons !== null && processedHeader.buttons.length > 0) || processedHeader.status !== undefined || - processedHeader.icon !== undefined) + processedHeader.icon !== undefined || + processedHeader.fileList !== undefined) const padding = message.type === 'tool' ? (fileList ? true : message.messageId?.endsWith('_permission')) : undefined @@ -1380,8 +1386,10 @@ export const createMynahUi = ( // Adding this conditional check to show the stop message in the center. const contentHorizontalAlignment: ChatItem['contentHorizontalAlignment'] = undefined - // If message.header?.status?.text is Stopped or Rejected or Ignored or Completed etc.. card should be in disabled state. - const shouldMute = message.header?.status?.text !== undefined && message.header?.status?.text !== 'Completed' + // If message.header?.status?.text is Stopped or Rejected or Ignored etc.. card should be in disabled state. + const shouldMute = + message.header?.status?.text !== undefined && + ['Stopped', 'Rejected', 'Ignored', 'Failed', 'Error'].includes(message.header.status.text) return { body: message.body, 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 897ee02071..c5a8046bd5 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 @@ -169,11 +169,11 @@ describe('Q Agentic Chat Server Integration Tests', async () => { expect(decryptedResult.additionalMessages).to.be.an('array') const fsReadMessage = decryptedResult.additionalMessages?.find( - msg => msg.type === 'tool' && msg.fileList?.rootFolderTitle === '1 file read' + msg => msg.type === 'tool' && msg.header?.body === '1 file read' ) expect(fsReadMessage).to.exist const expectedPath = path.join(rootPath, 'test.py') - const actualPaths = fsReadMessage?.fileList?.filePaths?.map(normalizePath) || [] + const actualPaths = fsReadMessage?.header?.fileList?.filePaths?.map(normalizePath) || [] expect(actualPaths).to.include.members([normalizePath(expectedPath)]) expect(fsReadMessage?.messageId?.startsWith('tooluse_')).to.be.true }) @@ -191,10 +191,10 @@ describe('Q Agentic Chat Server Integration Tests', async () => { expect(decryptedResult.additionalMessages).to.be.an('array') const listDirectoryMessage = decryptedResult.additionalMessages?.find( - msg => msg.type === 'tool' && msg.fileList?.rootFolderTitle === '1 directory listed' + msg => msg.type === 'tool' && msg.header?.body === '1 directory listed' ) expect(listDirectoryMessage).to.exist - const actualPaths = listDirectoryMessage?.fileList?.filePaths?.map(normalizePath) || [] + const actualPaths = listDirectoryMessage?.header?.fileList?.filePaths?.map(normalizePath) || [] expect(actualPaths).to.include.members([normalizePath(rootPath)]) expect(listDirectoryMessage?.messageId?.startsWith('tooluse_')).to.be.true }) @@ -371,11 +371,12 @@ describe('Q Agentic Chat Server Integration Tests', async () => { expect(decryptedResult.additionalMessages).to.be.an('array') const fileSearchMessage = decryptedResult.additionalMessages?.find( - msg => msg.type === 'tool' && msg.fileList?.rootFolderTitle === '1 directory searched' + msg => msg.type === 'tool' && msg.header?.body === 'Searched for "test" in ' ) expect(fileSearchMessage).to.exist expect(fileSearchMessage?.messageId?.startsWith('tooluse_')).to.be.true - const actualPaths = fileSearchMessage?.fileList?.filePaths?.map(normalizePath) || [] + expect(fileSearchMessage?.header?.status?.text).to.equal('3 results found') + const actualPaths = fileSearchMessage?.header?.fileList?.filePaths?.map(normalizePath) || [] expect(actualPaths).to.include.members([normalizePath(rootPath)]) }) }) diff --git a/server/aws-lsp-codewhisperer/src/language-server/agenticChat/agenticChatController.test.ts b/server/aws-lsp-codewhisperer/src/language-server/agenticChat/agenticChatController.test.ts index 6c8af7a272..616848d0b5 100644 --- a/server/aws-lsp-codewhisperer/src/language-server/agenticChat/agenticChatController.test.ts +++ b/server/aws-lsp-codewhisperer/src/language-server/agenticChat/agenticChatController.test.ts @@ -451,7 +451,7 @@ describe('AgenticChatController', () => { assert.deepStrictEqual(chatResult, { additionalMessages: [], - body: '\n\nHello World!', + body: '\nHello World!', messageId: 'mock-message-id', buttons: [], codeReference: [], @@ -1150,7 +1150,7 @@ describe('AgenticChatController', () => { sinon.assert.callCount(testFeatures.lsp.sendProgress, mockChatResponseList.length + 1) // response length + 1 loading messages assert.deepStrictEqual(chatResult, { additionalMessages: [], - body: '\n\nHello World!', + body: '\nHello World!', messageId: 'mock-message-id', codeReference: [], buttons: [], @@ -1169,7 +1169,7 @@ describe('AgenticChatController', () => { sinon.assert.callCount(testFeatures.lsp.sendProgress, mockChatResponseList.length + 1) // response length + 1 loading message assert.deepStrictEqual(chatResult, { additionalMessages: [], - body: '\n\nHello World!', + body: '\nHello World!', messageId: 'mock-message-id', buttons: [], codeReference: [], 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 72217fae07..09bf965819 100644 --- a/server/aws-lsp-codewhisperer/src/language-server/agenticChat/agenticChatController.ts +++ b/server/aws-lsp-codewhisperer/src/language-server/agenticChat/agenticChatController.ts @@ -169,7 +169,7 @@ import { ExecuteBash, ExecuteBashParams } from './tools/executeBash' import { ExplanatoryParams, InvokeOutput, ToolApprovalException } from './tools/toolShared' import { validatePathBasic, validatePathExists, validatePaths as validatePathsSync } from './utils/pathValidation' import { GrepSearch, SanitizedRipgrepOutput } from './tools/grepSearch' -import { FileSearch, FileSearchParams } from './tools/fileSearch' +import { FileSearch, FileSearchParams, isFileSearchParams } from './tools/fileSearch' import { FsReplace, FsReplaceParams } from './tools/fsReplace' import { loggingUtils, timeoutUtils } from '@aws/lsp-core' import { diffLines } from 'diff' @@ -1695,8 +1695,7 @@ export class AgenticChatController implements ChatHandlers { // remove progress UI await chatResultStream.removeResultBlockAndUpdateUI(progressPrefix + toolUse.toolUseId) - // fsRead and listDirectory write to an existing card and could show nothing in the current position - if (![FS_WRITE, FS_REPLACE, FS_READ, LIST_DIRECTORY].includes(toolUse.name)) { + if (![FS_WRITE, FS_REPLACE].includes(toolUse.name)) { await this.#showUndoAllIfRequired(chatResultStream, session) } // fsWrite can take a long time, so we render fsWrite Explanatory upon partial streaming responses. @@ -1911,10 +1910,19 @@ export class AgenticChatController implements ChatHandlers { switch (toolUse.name) { case FS_READ: case LIST_DIRECTORY: + const readToolResult = await this.#processReadTool(toolUse, chatResultStream) + if (readToolResult) { + await chatResultStream.writeResultBlock(readToolResult) + } + break case FILE_SEARCH: - const initialListDirResult = this.#processReadOrListOrSearch(toolUse, chatResultStream) - if (initialListDirResult) { - await chatResultStream.writeResultBlock(initialListDirResult) + if (isFileSearchParams(toolUse.input)) { + await this.#processFileSearchTool( + toolUse.input, + toolUse.toolUseId, + result, + chatResultStream + ) } break // no need to write tool result for listDir,fsRead,fileSearch into chat stream @@ -2315,7 +2323,6 @@ export class AgenticChatController implements ChatHandlers { } const toolMsgId = toolUse.toolUseId! - const chatMsgId = chatResultStream.getResult().messageId let headerEmitted = false const initialHeader: ChatMessage['header'] = { @@ -2353,13 +2360,6 @@ export class AgenticChatController implements ChatHandlers { header: completedHeader, }) - await chatResultStream.writeResultBlock({ - type: 'answer', - messageId: chatMsgId, - body: '', - header: undefined, - }) - this.#stoppedToolUses.add(toolMsgId) }, }) @@ -2877,70 +2877,135 @@ export class AgenticChatController implements ChatHandlers { } } - #processReadOrListOrSearch(toolUse: ToolUse, chatResultStream: AgenticChatResultStream): ChatMessage | undefined { - let messageIdToUpdate = toolUse.toolUseId! - const currentId = chatResultStream.getMessageIdToUpdateForTool(toolUse.name!) + async #processFileSearchTool( + toolInput: FileSearchParams, + toolUseId: string, + result: InvokeOutput, + chatResultStream: AgenticChatResultStream + ): Promise { + if (typeof result.output.content !== 'string') return - if (currentId) { - messageIdToUpdate = currentId - } else { - chatResultStream.setMessageIdToUpdateForTool(toolUse.name!, messageIdToUpdate) + const { queryName, path: inputPath } = toolInput + const resultCount = result.output.content + .split('\n') + .filter(line => line.trim().startsWith('[F]') || line.trim().startsWith('[D]')).length + + const chatMessage: ChatMessage = { + type: 'tool', + messageId: toolUseId, + header: { + body: `Searched for "${queryName}" in `, + icon: 'search', + status: { + text: `${resultCount} result${resultCount !== 1 ? 's' : ''} found`, + }, + fileList: { + filePaths: [inputPath], + details: { + [inputPath]: { + description: inputPath, + visibleName: path.basename(inputPath), + clickable: false, + }, + }, + }, + }, } - let currentPaths = [] + await chatResultStream.writeResultBlock(chatMessage) + } + + async #processReadTool( + toolUse: ToolUse, + chatResultStream: AgenticChatResultStream + ): Promise { + let currentPaths: string[] = [] if (toolUse.name === FS_READ) { - currentPaths = (toolUse.input as unknown as FsReadParams)?.paths + currentPaths = (toolUse.input as unknown as FsReadParams)?.paths || [] + } else if (toolUse.name === LIST_DIRECTORY) { + const singlePath = (toolUse.input as unknown as ListDirectoryParams)?.path + if (singlePath) { + currentPaths = [singlePath] + } + } else if (toolUse.name === FILE_SEARCH) { + const queryName = (toolUse.input as unknown as FileSearchParams)?.queryName + if (queryName) { + currentPaths = [queryName] + } } else { - currentPaths.push((toolUse.input as unknown as ListDirectoryParams | FileSearchParams)?.path) + return } - if (!currentPaths) return + if (currentPaths.length === 0) return - for (const currentPath of currentPaths) { - const existingPaths = chatResultStream.getMessageOperation(messageIdToUpdate)?.filePaths || [] - // Check if path already exists in the list - const isPathAlreadyProcessed = existingPaths.some(path => path.relativeFilePath === currentPath) - if (!isPathAlreadyProcessed) { - const currentFileDetail = { - relativeFilePath: currentPath, - lineRanges: [{ first: -1, second: -1 }], - } - chatResultStream.addMessageOperation(messageIdToUpdate, toolUse.name!, [ - ...existingPaths, - currentFileDetail, - ]) + // Check if the last message is the same tool type + const lastMessage = chatResultStream.getLastMessage() + const isSameToolType = + lastMessage?.type === 'tool' && lastMessage.header?.icon === this.#toolToIcon(toolUse.name) + + let allPaths = currentPaths + + if (isSameToolType && lastMessage.messageId) { + // Combine with existing paths and overwrite the last message + const existingPaths = lastMessage.header?.fileList?.filePaths || [] + allPaths = [...existingPaths, ...currentPaths] + + const blockId = chatResultStream.getMessageBlockId(lastMessage.messageId) + if (blockId !== undefined) { + // Create the updated message with combined paths + const updatedMessage = this.#createFileListToolMessage(toolUse, allPaths, lastMessage.messageId) + // Overwrite the existing block + await chatResultStream.overwriteResultBlock(updatedMessage, blockId) + return undefined // Don't return a message since we already wrote it } } + + // Create new message with current paths + return this.#createFileListToolMessage(toolUse, allPaths, toolUse.toolUseId!) + } + + #createFileListToolMessage(toolUse: ToolUse, filePaths: string[], messageId: string): ChatMessage { + const itemCount = filePaths.length let title: string - const itemCount = chatResultStream.getMessageOperation(messageIdToUpdate)?.filePaths.length - const filePathsPushed = chatResultStream.getMessageOperation(messageIdToUpdate)?.filePaths ?? [] - if (!itemCount) { + if (itemCount === 0) { title = 'Gathering context' } else { title = toolUse.name === FS_READ ? `${itemCount} file${itemCount > 1 ? 's' : ''} read` - : toolUse.name === FILE_SEARCH - ? `${itemCount} ${itemCount === 1 ? 'directory' : 'directories'} searched` - : `${itemCount} ${itemCount === 1 ? 'directory' : 'directories'} listed` + : toolUse.name === LIST_DIRECTORY + ? `${itemCount} ${itemCount === 1 ? 'directory' : 'directories'} listed` + : '' } const details: Record = {} - for (const item of filePathsPushed) { - details[item.relativeFilePath] = { - lineRanges: item.lineRanges, - description: item.relativeFilePath, + for (const filePath of filePaths) { + details[filePath] = { + description: filePath, + visibleName: path.basename(filePath), + clickable: toolUse.name === FS_READ, } } - - const fileList: FileList = { - rootFolderTitle: title, - filePaths: filePathsPushed.map(item => item.relativeFilePath), - details, - } return { type: 'tool', - fileList, - messageId: messageIdToUpdate, - body: '', + header: { + body: title, + icon: this.#toolToIcon(toolUse.name), + fileList: { + filePaths, + details, + }, + }, + messageId, + } + } + + #toolToIcon(toolName: string | undefined): string | undefined { + switch (toolName) { + case FS_READ: + return 'eye' + case LIST_DIRECTORY: + return 'check-list' + default: + return undefined } } @@ -2956,14 +3021,7 @@ export class AgenticChatController implements ChatHandlers { return undefined } - let messageIdToUpdate = toolUse.toolUseId! - const currentId = chatResultStream.getMessageIdToUpdateForTool(toolUse.name!) - - if (currentId) { - messageIdToUpdate = currentId - } else { - chatResultStream.setMessageIdToUpdateForTool(toolUse.name!, messageIdToUpdate) - } + const messageIdToUpdate = toolUse.toolUseId! // Extract search results from the tool output const output = result.output.content as SanitizedRipgrepOutput diff --git a/server/aws-lsp-codewhisperer/src/language-server/agenticChat/agenticChatResultStream.ts b/server/aws-lsp-codewhisperer/src/language-server/agenticChat/agenticChatResultStream.ts index 70b3452361..195e1e880b 100644 --- a/server/aws-lsp-codewhisperer/src/language-server/agenticChat/agenticChatResultStream.ts +++ b/server/aws-lsp-codewhisperer/src/language-server/agenticChat/agenticChatResultStream.ts @@ -1,4 +1,4 @@ -import { ChatResult, FileDetails, ChatMessage } from '@aws/language-server-runtimes/protocol' +import { ChatResult, ChatMessage } from '@aws/language-server-runtimes/protocol' import { randomUUID } from 'crypto' export interface ResultStreamWriter { @@ -32,33 +32,20 @@ export interface ResultStreamWriter { close(): Promise } +export const progressPrefix = 'progress_' + /** * This class wraps around lsp.sendProgress to provide a more helpful interface for streaming a ChatResult to the client. * ChatResults are grouped into blocks that can be written directly, or streamed in. * In the final message, blocks are seperated by resultDelimiter defined below. */ - -interface FileDetailsWithPath extends FileDetails { - relativeFilePath: string -} - -type OperationType = 'read' | 'write' | 'listDir' - -export const progressPrefix = 'progress_' - -interface FileOperation { - type: OperationType - filePaths: FileDetailsWithPath[] -} export class AgenticChatResultStream { - static readonly resultDelimiter = '\n\n' + static readonly resultDelimiter = '\n' #state = { chatResultBlocks: [] as ChatMessage[], isLocked: false, uuid: randomUUID(), messageId: undefined as string | undefined, - messageIdToUpdateForTool: new Map(), - messageOperations: new Map(), } readonly #sendProgress: (newChatResult: ChatResult | string) => Promise @@ -70,33 +57,6 @@ export class AgenticChatResultStream { return this.#joinResults(this.#state.chatResultBlocks, only) } - setMessageIdToUpdateForTool(toolName: string, messageId: string) { - this.#state.messageIdToUpdateForTool.set(toolName as OperationType, messageId) - } - - getMessageIdToUpdateForTool(toolName: string): string | undefined { - return this.#state.messageIdToUpdateForTool.get(toolName as OperationType) - } - - /** - * Adds a file operation for a specific message - * @param messageId The ID of the message - * @param type The type of operation ('fsRead' or 'listDirectory' or 'fsWrite') - * @param filePaths Array of FileDetailsWithPath involved in the operation - */ - addMessageOperation(messageId: string, type: string, filePaths: FileDetailsWithPath[]) { - this.#state.messageOperations.set(messageId, { type: type as OperationType, filePaths }) - } - - /** - * Gets the file operation details for a specific message - * @param messageId The ID of the message - * @returns The file operation details or undefined if not found - */ - getMessageOperation(messageId: string): FileOperation | undefined { - return this.#state.messageOperations.get(messageId) - } - #joinResults(chatResults: ChatMessage[], only?: string): ChatResult { const result: ChatResult = { body: '', @@ -111,9 +71,9 @@ export class AgenticChatResultStream { return { ...acc, buttons: [...(acc.buttons ?? []), ...(c.buttons ?? [])], - body: acc.body + AgenticChatResultStream.resultDelimiter + c.body, - ...(c.contextList && { contextList: c.contextList }), - header: Object.prototype.hasOwnProperty.call(c, 'header') ? c.header : acc.header, + body: acc.body + (c.body ? AgenticChatResultStream.resultDelimiter + c.body : ''), + ...(c.contextList && c.type !== 'tool' && { contextList: c.contextList }), + header: c.header !== undefined ? c.header : acc.header, codeReference: [...(acc.codeReference ?? []), ...(c.codeReference ?? [])], } } else if (acc.additionalMessages!.some(am => am.messageId === c.messageId)) { @@ -127,9 +87,10 @@ export class AgenticChatResultStream { : am.buttons, body: am.messageId === c.messageId - ? am.body + AgenticChatResultStream.resultDelimiter + c.body + ? am.body + (c.body ? AgenticChatResultStream.resultDelimiter + c.body : '') : am.body, ...(am.messageId === c.messageId && + c.type !== 'tool' && (c.contextList || acc.contextList) && { contextList: { filePaths: [ @@ -161,7 +122,7 @@ export class AgenticChatResultStream { }, }, }), - header: Object.prototype.hasOwnProperty.call(c, 'header') ? c.header : am.header, + ...(am.messageId === c.messageId && c.header !== undefined && { header: c.header }), })), } } else { @@ -246,6 +207,10 @@ export class AgenticChatResultStream { return undefined } + getLastMessage(): ChatMessage { + return this.#state.chatResultBlocks[this.#state.chatResultBlocks.length - 1] + } + getResultStreamWriter(): ResultStreamWriter { // Note: if write calls are not awaited, stream can be out-of-order. if (this.#state.isLocked) { diff --git a/server/aws-lsp-codewhisperer/src/language-server/agenticChat/tools/fileSearch.ts b/server/aws-lsp-codewhisperer/src/language-server/agenticChat/tools/fileSearch.ts index fb6486996e..37d11afe4f 100644 --- a/server/aws-lsp-codewhisperer/src/language-server/agenticChat/tools/fileSearch.ts +++ b/server/aws-lsp-codewhisperer/src/language-server/agenticChat/tools/fileSearch.ts @@ -158,3 +158,7 @@ export class FileSearch { } as const } } + +export function isFileSearchParams(input: any): input is FileSearchParams { + return input && typeof input.path === 'string' && typeof input.queryName === 'string' +} From 8975f10e3e709da20f164397604a54df83202f8b Mon Sep 17 00:00:00 2001 From: Will Lo <96078566+Will-ShaoHua@users.noreply.github.com> Date: Thu, 14 Aug 2025 12:09:38 -0700 Subject: [PATCH 16/74] refactor: improve generateCompletion logging format (#2125) --- .../src/shared/codeWhispererService.ts | 72 ++++++++++--------- 1 file changed, 37 insertions(+), 35 deletions(-) diff --git a/server/aws-lsp-codewhisperer/src/shared/codeWhispererService.ts b/server/aws-lsp-codewhisperer/src/shared/codeWhispererService.ts index 01101d68a8..030f7f7ee4 100644 --- a/server/aws-lsp-codewhisperer/src/shared/codeWhispererService.ts +++ b/server/aws-lsp-codewhisperer/src/shared/codeWhispererService.ts @@ -422,58 +422,60 @@ export class CodeWhispererServiceToken extends CodeWhispererServiceBase { async generateSuggestions(request: GenerateSuggestionsRequest): Promise { // add cancellation check // add error check - if (this.customizationArn) request.customizationArn = this.customizationArn - const beforeApiCall = performance.now() - let recentEditsLogStr = '' - const recentEdits = request.supplementalContexts?.filter(it => it.type === 'PreviousEditorState') - if (recentEdits) { - if (recentEdits.length === 0) { - recentEditsLogStr += `No recent edits` - } else { - recentEditsLogStr += '\n' - for (let i = 0; i < recentEdits.length; i++) { - const e = recentEdits[i] - recentEditsLogStr += `[recentEdits ${i}th]:\n` - recentEditsLogStr += `${e.content}\n` + let logstr = `GenerateCompletion activity:\n` + try { + if (this.customizationArn) request.customizationArn = this.customizationArn + const beforeApiCall = performance.now() + let recentEditsLogStr = '' + const recentEdits = request.supplementalContexts?.filter(it => it.type === 'PreviousEditorState') + if (recentEdits) { + if (recentEdits.length === 0) { + recentEditsLogStr += `No recent edits` + } else { + recentEditsLogStr += '\n' + for (let i = 0; i < recentEdits.length; i++) { + const e = recentEdits[i] + recentEditsLogStr += `[recentEdits ${i}th]:\n` + recentEditsLogStr += `${e.content}\n` + } } } - } - this.logging.info( - `GenerateCompletion request: + logstr += `@@request metadata@@ "endpoint": ${this.codeWhispererEndpoint}, "predictionType": ${request.predictionTypes?.toString() ?? 'Not specified (COMPLETIONS)'}, "filename": ${request.fileContext.filename}, "language": ${request.fileContext.programmingLanguage.languageName}, "supplementalContextCount": ${request.supplementalContexts?.length ?? 0}, "request.nextToken": ${request.nextToken}, - "recentEdits": ${recentEditsLogStr}` - ) + "recentEdits": ${recentEditsLogStr}\n` - const response = await this.client.generateCompletions(this.withProfileArn(request)).promise() + const response = await this.client.generateCompletions(this.withProfileArn(request)).promise() - const responseContext = { - requestId: response?.$response?.requestId, - codewhispererSessionId: response?.$response?.httpResponse?.headers['x-amzn-sessionid'], - nextToken: response.nextToken, - } + const responseContext = { + requestId: response?.$response?.requestId, + codewhispererSessionId: response?.$response?.httpResponse?.headers['x-amzn-sessionid'], + nextToken: response.nextToken, + } - const r = this.mapCodeWhispererApiResponseToSuggestion(response, responseContext) - const firstSuggestionLogstr = r.suggestions.length > 0 ? `\n${r.suggestions[0].content}` : 'No suggestion' + const r = this.mapCodeWhispererApiResponseToSuggestion(response, responseContext) + const firstSuggestionLogstr = r.suggestions.length > 0 ? `\n${r.suggestions[0].content}` : 'No suggestion' - this.logging.info( - `GenerateCompletion response: - "endpoint": ${this.codeWhispererEndpoint}, + logstr += `@@response metadata@@ "requestId": ${responseContext.requestId}, "sessionId": ${responseContext.codewhispererSessionId}, - "responseCompletionCount": ${response.completions?.length ?? 0}, - "responsePredictionCount": ${response.predictions?.length ?? 0}, - "predictionType": ${request.predictionTypes?.toString() ?? ''}, + "response.completions.length": ${response.completions?.length ?? 0}, + "response.predictions.length": ${response.predictions?.length ?? 0}, "latency": ${performance.now() - beforeApiCall}, - "filename": ${request.fileContext.filename}, "response.nextToken": ${response.nextToken}, "firstSuggestion": ${firstSuggestionLogstr}` - ) - return r + + return r + } catch (e) { + logstr += `error: ${(e as Error).message}` + throw e + } finally { + this.logging.info(logstr) + } } private mapCodeWhispererApiResponseToSuggestion( From 0e23e2d29b8cad14403d372b9bbb08ca8ffa7ac7 Mon Sep 17 00:00:00 2001 From: BlakeLazarine Date: Thu, 14 Aug 2025 12:36:43 -0700 Subject: [PATCH 17/74] fix(amazonq): handle case where multiple rules are provided with the same name (#2118) * fix(amazonq): handle case where multiple rules are provided with the same name * fix(amazonq): add unit test for duplicate custom guidelines * fix(amazonq): add unit test for processToolUses * fix(amazonq): set context type for AmazonQ.md to rule * fix(amazonq): add README.md back as rule context type --------- Co-authored-by: Blake Lazarine --- .../agenticChat/agenticChatController.test.ts | 86 ++++++++++++++++++- .../agenticChat/agenticChatController.ts | 25 ++++-- .../tools/qCodeAnalysis/codeReview.test.ts | 28 ++++++ .../tools/qCodeAnalysis/codeReview.ts | 5 ++ 4 files changed, 134 insertions(+), 10 deletions(-) diff --git a/server/aws-lsp-codewhisperer/src/language-server/agenticChat/agenticChatController.test.ts b/server/aws-lsp-codewhisperer/src/language-server/agenticChat/agenticChatController.test.ts index 616848d0b5..a307ce3fc6 100644 --- a/server/aws-lsp-codewhisperer/src/language-server/agenticChat/agenticChatController.test.ts +++ b/server/aws-lsp-codewhisperer/src/language-server/agenticChat/agenticChatController.test.ts @@ -241,7 +241,7 @@ describe('AgenticChatController', () => { testFeatures.agent = { runTool: sinon.stub().resolves({}), getTools: sinon.stub().returns( - ['mock-tool-name', 'mock-tool-name-1', 'mock-tool-name-2'].map(toolName => ({ + ['mock-tool-name', 'mock-tool-name-1', 'mock-tool-name-2', 'codeReview'].map(toolName => ({ toolSpecification: { name: toolName, description: 'Mock tool for testing' }, })) ), @@ -3283,6 +3283,90 @@ ${' '.repeat(8)}} assert.strictEqual(returnValue, undefined) }) }) + + describe('processToolUses', () => { + it('filters rule artifacts from additionalContext for CodeReview tool', async () => { + const mockAdditionalContext = [ + { + type: 'file', + description: '', + name: '', + relativePath: '', + startLine: 0, + endLine: 0, + path: '/test/file.js', + }, + { + type: 'rule', + description: '', + name: '', + relativePath: '', + startLine: 0, + endLine: 0, + path: '/test/rule1.json', + }, + { + type: 'rule', + description: '', + name: '', + relativePath: '', + startLine: 0, + endLine: 0, + path: '/test/rule2.json', + }, + ] + + const toolUse = { + toolUseId: 'test-id', + name: 'codeReview', + input: { fileLevelArtifacts: [{ path: '/test/file.js' }] }, + stop: true, + } + + const runToolStub = testFeatures.agent.runTool as sinon.SinonStub + runToolStub.resolves({}) + + // Create a mock session with toolUseLookup + const mockSession = { + toolUseLookup: new Map(), + pairProgrammingMode: true, + } as any + + // Create a minimal mock of AgenticChatResultStream + const mockChatResultStream = { + removeResultBlockAndUpdateUI: sinon.stub().resolves(), + writeResultBlock: sinon.stub().resolves(1), + overwriteResultBlock: sinon.stub().resolves(), + removeResultBlock: sinon.stub().resolves(), + getMessageBlockId: sinon.stub().returns(undefined), + hasMessage: sinon.stub().returns(false), + updateOngoingProgressResult: sinon.stub().resolves(), + getResult: sinon.stub().returns({ messageId: 'test', body: '' }), + setMessageIdToUpdateForTool: sinon.stub(), + getMessageIdToUpdateForTool: sinon.stub().returns(undefined), + addMessageOperation: sinon.stub(), + getMessageOperation: sinon.stub().returns(undefined), + } + + // Call processToolUses directly + await chatController.processToolUses( + [toolUse], + mockChatResultStream as any, + mockSession, + 'tabId', + mockCancellationToken, + mockAdditionalContext + ) + + // Verify runTool was called with ruleArtifacts + sinon.assert.calledOnce(runToolStub) + const toolInput = runToolStub.firstCall.args[1] + assert.ok(toolInput.ruleArtifacts) + assert.strictEqual(toolInput.ruleArtifacts.length, 2) + assert.strictEqual(toolInput.ruleArtifacts[0].path, '/test/rule1.json') + assert.strictEqual(toolInput.ruleArtifacts[1].path, '/test/rule2.json') + }) + }) }) // The body may include text-based progress updates from tool invocations. 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 09bf965819..991d95a6ad 100644 --- a/server/aws-lsp-codewhisperer/src/language-server/agenticChat/agenticChatController.ts +++ b/server/aws-lsp-codewhisperer/src/language-server/agenticChat/agenticChatController.ts @@ -1387,7 +1387,14 @@ export class AgenticChatController implements ChatHandlers { session.setConversationType('AgenticChatWithToolUse') if (result.success) { // Process tool uses and update the request input for the next iteration - toolResults = await this.#processToolUses(pendingToolUses, chatResultStream, session, tabId, token) + toolResults = await this.processToolUses( + pendingToolUses, + chatResultStream, + session, + tabId, + token, + additionalContext + ) if (toolResults.some(toolResult => this.#shouldSendBackErrorContent(toolResult))) { content = 'There was an error processing one or more tool uses. Try again, do not apologize.' shouldDisplayMessage = false @@ -1662,12 +1669,13 @@ export class AgenticChatController implements ChatHandlers { /** * Processes tool uses by running the tools and collecting results */ - async #processToolUses( + async processToolUses( toolUses: Array, chatResultStream: AgenticChatResultStream, session: ChatSessionService, tabId: string, - token?: CancellationToken + token?: CancellationToken, + additionalContext?: AdditionalContentEntryAddition[] ): Promise { const results: ToolResult[] = [] @@ -1868,12 +1876,11 @@ export class AgenticChatController implements ChatHandlers { if (toolUse.name === CodeReview.toolName) { try { let initialInput = JSON.parse(JSON.stringify(toolUse.input)) - let ruleArtifacts = await this.#additionalContextProvider.collectWorkspaceRules(tabId) - if (ruleArtifacts !== undefined || ruleArtifacts !== null) { - this.#features.logging.info(`RuleArtifacts: ${JSON.stringify(ruleArtifacts)}`) - let pathsToRulesMap = ruleArtifacts.map(ruleArtifact => ({ path: ruleArtifact.id })) - this.#features.logging.info(`PathsToRules: ${JSON.stringify(pathsToRulesMap)}`) - initialInput['ruleArtifacts'] = pathsToRulesMap + + if (additionalContext !== undefined) { + initialInput['ruleArtifacts'] = additionalContext + .filter(c => c.type === 'rule') + .map(c => ({ path: c.path })) } toolUse.input = initialInput } catch (e) { diff --git a/server/aws-lsp-codewhisperer/src/language-server/agenticChat/tools/qCodeAnalysis/codeReview.test.ts b/server/aws-lsp-codewhisperer/src/language-server/agenticChat/tools/qCodeAnalysis/codeReview.test.ts index c587253bbc..3509cd1730 100644 --- a/server/aws-lsp-codewhisperer/src/language-server/agenticChat/tools/qCodeAnalysis/codeReview.test.ts +++ b/server/aws-lsp-codewhisperer/src/language-server/agenticChat/tools/qCodeAnalysis/codeReview.test.ts @@ -385,6 +385,34 @@ describe('CodeReview', () => { expect(error.message).to.include('There are no valid files to scan') } }) + + it('should handle duplicate rule filenames with unique UUIDs', async () => { + const fileArtifacts = [{ path: '/test/file.js' }] + const folderArtifacts: any[] = [] + const ruleArtifacts = [{ path: '/test/path1/rule.json' }, { path: '/test/path2/rule.json' }] + + const mockZip = { + file: sandbox.stub(), + generateAsync: sandbox.stub().resolves(Buffer.from('test')), + } + sandbox.stub(JSZip.prototype, 'file').callsFake(mockZip.file) + sandbox.stub(JSZip.prototype, 'generateAsync').callsFake(mockZip.generateAsync) + sandbox.stub(CodeReviewUtils, 'countZipFiles').returns(3) + sandbox.stub(require('crypto'), 'randomUUID').returns('test-uuid-123') + + await (codeReview as any).prepareFilesAndFoldersForUpload( + fileArtifacts, + folderArtifacts, + ruleArtifacts, + false + ) + + // Verify first file uses original name + expect(mockZip.file.firstCall.args[0]).to.include('/test/file.js') + expect(mockZip.file.secondCall.args[0]).to.include('rule.json') + // Verify second file gets UUID suffix + expect(mockZip.file.thirdCall.args[0]).to.include('rule_test-uuid-123.json') + }) }) describe('collectFindings', () => { diff --git a/server/aws-lsp-codewhisperer/src/language-server/agenticChat/tools/qCodeAnalysis/codeReview.ts b/server/aws-lsp-codewhisperer/src/language-server/agenticChat/tools/qCodeAnalysis/codeReview.ts index adcea2ec33..b1ef4c3e61 100644 --- a/server/aws-lsp-codewhisperer/src/language-server/agenticChat/tools/qCodeAnalysis/codeReview.ts +++ b/server/aws-lsp-codewhisperer/src/language-server/agenticChat/tools/qCodeAnalysis/codeReview.ts @@ -716,6 +716,7 @@ export class CodeReview { * @param customerCodeZip JSZip instance for the customer code */ private async processRuleArtifacts(ruleArtifacts: RuleArtifacts, customerCodeZip: JSZip): Promise { + let ruleNameSet = new Set() for (const artifact of ruleArtifacts) { await CodeReviewUtils.withErrorHandling( async () => { @@ -725,6 +726,10 @@ export class CodeReview { !CodeReviewUtils.shouldSkipFile(fileName) && existsSync(artifact.path) ) { + if (ruleNameSet.has(fileName)) { + fileName = fileName.split('.')[0] + '_' + crypto.randomUUID() + '.' + fileName.split('.')[1] + } + ruleNameSet.add(fileName) const fileContent = await this.workspace.fs.readFile(artifact.path) customerCodeZip.file( `${CodeReview.CUSTOMER_CODE_BASE_PATH}/${CodeReview.RULE_ARTIFACT_PATH}/${fileName}`, From 963b6e9b7887da23a85a826c55a6ed95ff36d956 Mon Sep 17 00:00:00 2001 From: Will Lo <96078566+Will-ShaoHua@users.noreply.github.com> Date: Thu, 14 Aug 2025 14:15:39 -0700 Subject: [PATCH 18/74] perf: remove edit completion retry mechanism on document change (#2124) --- .../inline-completion/codeWhispererServer.ts | 4 +- .../inline-completion/constants.ts | 1 - .../editCompletionHandler.ts | 85 ++++++++----------- .../session/sessionManager.ts | 12 ++- 4 files changed, 48 insertions(+), 54 deletions(-) diff --git a/server/aws-lsp-codewhisperer/src/language-server/inline-completion/codeWhispererServer.ts b/server/aws-lsp-codewhisperer/src/language-server/inline-completion/codeWhispererServer.ts index c1f2b464b2..46a3241978 100644 --- a/server/aws-lsp-codewhisperer/src/language-server/inline-completion/codeWhispererServer.ts +++ b/server/aws-lsp-codewhisperer/src/language-server/inline-completion/codeWhispererServer.ts @@ -633,7 +633,9 @@ export const CodewhispererServerFactory = } if (session.state !== 'ACTIVE') { - logging.log(`ERROR: Trying to record trigger decision for not-active session ${sessionId}`) + logging.log( + `ERROR: Trying to record trigger decision for not-active session ${sessionId} with wrong state ${session.state}` + ) return } diff --git a/server/aws-lsp-codewhisperer/src/language-server/inline-completion/constants.ts b/server/aws-lsp-codewhisperer/src/language-server/inline-completion/constants.ts index c5810924b6..8cab372053 100644 --- a/server/aws-lsp-codewhisperer/src/language-server/inline-completion/constants.ts +++ b/server/aws-lsp-codewhisperer/src/language-server/inline-completion/constants.ts @@ -3,7 +3,6 @@ export const FILENAME_CHARS_LIMIT = 1024 export const CONTEXT_CHARACTERS_LIMIT = 10240 export const EMPTY_RESULT = { sessionId: '', items: [] } export const EDIT_DEBOUNCE_INTERVAL_MS = 500 -export const EDIT_STALE_RETRY_COUNT = 3 // ABAP ADT extensions commonly used with Eclipse export const ABAP_EXTENSIONS = new Set([ 'asprog', diff --git a/server/aws-lsp-codewhisperer/src/language-server/inline-completion/editCompletionHandler.ts b/server/aws-lsp-codewhisperer/src/language-server/inline-completion/editCompletionHandler.ts index 3c72e368f4..28e29ee271 100644 --- a/server/aws-lsp-codewhisperer/src/language-server/inline-completion/editCompletionHandler.ts +++ b/server/aws-lsp-codewhisperer/src/language-server/inline-completion/editCompletionHandler.ts @@ -16,7 +16,6 @@ import { GenerateSuggestionsRequest, GenerateSuggestionsResponse, getFileContext, - SuggestionType, } from '../../shared/codeWhispererService' import { CodeWhispererSession, SessionManager } from './session/sessionManager' import { CursorTracker } from './tracker/cursorTracker' @@ -36,7 +35,7 @@ import { RejectedEditTracker } from './tracker/rejectedEditTracker' import { getErrorMessage, hasConnectionExpired } from '../../shared/utils' import { AmazonQError, AmazonQServiceConnectionExpiredError } from '../../shared/amazonQServiceManager/errors' import { DocumentChangedListener } from './documentChangedListener' -import { EMPTY_RESULT, EDIT_DEBOUNCE_INTERVAL_MS, EDIT_STALE_RETRY_COUNT } from './constants' +import { EMPTY_RESULT, EDIT_DEBOUNCE_INTERVAL_MS } from './constants' export class EditCompletionHandler { private readonly editsEnabled: boolean @@ -74,12 +73,12 @@ export class EditCompletionHandler { */ documentChanged() { if (this.debounceTimeout) { - this.logging.info('[NEP] refresh timeout') - this.debounceTimeout.refresh() - } - - if (this.isWaiting) { - this.hasDocumentChangedSinceInvocation = true + if (this.isWaiting) { + this.hasDocumentChangedSinceInvocation = true + } else { + this.logging.info(`refresh and debounce edits suggestion for another ${EDIT_DEBOUNCE_INTERVAL_MS}`) + this.debounceTimeout.refresh() + } } } @@ -87,9 +86,6 @@ export class EditCompletionHandler { params: InlineCompletionWithReferencesParams, token: CancellationToken ): Promise { - this.hasDocumentChangedSinceInvocation = false - this.debounceTimeout = undefined - // On every new completion request close current inflight session. const currentSession = this.sessionManager.getCurrentSession() if (currentSession && currentSession.state == 'REQUESTING' && !params.partialResultToken) { @@ -156,46 +152,37 @@ export class EditCompletionHandler { } } - // TODO: telemetry, discarded suggestions - // The other easy way to do this is simply not return any suggestion (which is used when retry > 3) - const invokeWithRetry = async (attempt: number = 0): Promise => { - return new Promise(async resolve => { - this.debounceTimeout = setTimeout(async () => { - try { - this.isWaiting = true - const result = await this._invoke( - params, - token, - textDocument, - inferredLanguageId, - currentSession - ).finally(() => { - this.isWaiting = false + return new Promise(async resolve => { + this.debounceTimeout = setTimeout(async () => { + try { + this.isWaiting = true + const result = await this._invoke( + params, + token, + textDocument, + inferredLanguageId, + currentSession + ).finally(() => { + this.isWaiting = false + }) + if (this.hasDocumentChangedSinceInvocation) { + this.logging.info( + 'EditCompletionHandler - Document changed during execution, resolving empty result' + ) + resolve({ + sessionId: SessionManager.getInstance('EDITS').getActiveSession()?.id ?? '', + items: [], }) - if (this.hasDocumentChangedSinceInvocation) { - if (attempt < EDIT_STALE_RETRY_COUNT) { - this.logging.info( - `EditCompletionHandler - Document changed during execution, retrying (attempt ${attempt + 1})` - ) - this.hasDocumentChangedSinceInvocation = false - const retryResult = await invokeWithRetry(attempt + 1) - resolve(retryResult) - } else { - this.logging.info('EditCompletionHandler - Max retries reached, returning empty result') - resolve(EMPTY_RESULT) - } - } else { - this.logging.info('EditCompletionHandler - No document changes, resolving result') - resolve(result) - } - } finally { - this.debounceTimeout = undefined + } else { + this.logging.info('EditCompletionHandler - No document changes, resolving result') + resolve(result) } - }, EDIT_DEBOUNCE_INTERVAL_MS) - }) - } - - return invokeWithRetry() + } finally { + this.debounceTimeout = undefined + this.hasDocumentChangedSinceInvocation = false + } + }, EDIT_DEBOUNCE_INTERVAL_MS) + }) } async _invoke( diff --git a/server/aws-lsp-codewhisperer/src/language-server/inline-completion/session/sessionManager.ts b/server/aws-lsp-codewhisperer/src/language-server/inline-completion/session/sessionManager.ts index cb873a2920..a0ddf742f6 100644 --- a/server/aws-lsp-codewhisperer/src/language-server/inline-completion/session/sessionManager.ts +++ b/server/aws-lsp-codewhisperer/src/language-server/inline-completion/session/sessionManager.ts @@ -14,7 +14,6 @@ import { } from '../../../shared/codeWhispererService' import { CodewhispererLanguage } from '../../../shared/languageDetection' import { CodeWhispererSupplementalContext } from '../../../shared/models/model' -import { Logging } from '@aws/language-server-runtimes/server-interface' type SessionState = 'REQUESTING' | 'ACTIVE' | 'CLOSED' | 'ERROR' | 'DISCARD' export type UserDecision = 'Empty' | 'Filter' | 'Discard' | 'Accept' | 'Ignore' | 'Reject' | 'Unseen' @@ -45,7 +44,13 @@ export class CodeWhispererSession { startTime: number // Time when Session was closed and final state of user decisions is recorded in suggestionsStates closeTime?: number = 0 - state: SessionState + private _state: SessionState + get state(): SessionState { + return this._state + } + private set state(newState: SessionState) { + this._state = newState + } codewhispererSessionId?: string startPosition: Position = { line: 0, @@ -96,7 +101,8 @@ export class CodeWhispererSession { this.classifierThreshold = data.classifierThreshold this.customizationArn = data.customizationArn this.supplementalMetadata = data.supplementalMetadata - this.state = 'REQUESTING' + this._state = 'REQUESTING' + this.startTime = new Date().getTime() } From 0bf825ec41e0593766e34e4c1b21f063931e03d1 Mon Sep 17 00:00:00 2001 From: Richard Li <742829+rli@users.noreply.github.com> Date: Thu, 14 Aug 2025 14:18:35 -0700 Subject: [PATCH 19/74] ci: remove need for custom PAT for release branch workflow (#2126) The token from GitHub Actions is sufficient --- .../workflows/create-release-candidate-branch.yml | 14 ++++---------- 1 file changed, 4 insertions(+), 10 deletions(-) diff --git a/.github/workflows/create-release-candidate-branch.yml b/.github/workflows/create-release-candidate-branch.yml index 2071f69ed4..b447f11803 100644 --- a/.github/workflows/create-release-candidate-branch.yml +++ b/.github/workflows/create-release-candidate-branch.yml @@ -28,14 +28,15 @@ jobs: setupRcBranch: name: Set up a Release Candidate Branch runs-on: ubuntu-latest + permissions: + contents: write steps: - name: Sync code uses: actions/checkout@v4 with: ref: ${{ inputs.commitId }} - # Use RELEASE_CANDIDATE_BRANCH_CREATION_PAT to ensure workflow triggering works - token: ${{ secrets.RELEASE_CANDIDATE_BRANCH_CREATION_PAT }} + token: ${{ secrets.GITHUB_TOKEN }} persist-credentials: true - name: Setup Node.js @@ -109,15 +110,8 @@ jobs: env: BRANCH_NAME: ${{ steps.release-branch.outputs.BRANCH_NAME }} RELEASE_VERSION: ${{ steps.release-version.outputs.RELEASE_VERSION }} - # We use the toolkit-automation account, basically something that - # isn't the default GitHub Token, because you cannot chain actions with that. - # In our case, after pushing a commit (below), we want create-agent-standalone.yml - # to start automatically. - REPO_PAT: ${{ secrets.RELEASE_CANDIDATE_BRANCH_CREATION_PAT }} run: | git config --global user.email "<>" git config --global user.name "aws-toolkit-automation" - # Configure git to use the PAT token for authentication - git remote set-url origin "https://x-access-token:${REPO_PAT}@github.com/${{ github.repository }}.git" - git commit -m "chore: bump agentic version: $RELEASE_VERSION" + git commit --no-verify -m "chore: bump agentic version: $RELEASE_VERSION" git push --set-upstream origin "$BRANCH_NAME" From 971eaa505d948e9d2090c85f9b965f554ea7f2c8 Mon Sep 17 00:00:00 2001 From: chungjac Date: Fri, 15 Aug 2025 15:55:37 -0700 Subject: [PATCH 20/74] fix: proper path handling for additional context (#2129) * fix: proper pathing for additonal context * fix: update existing tests to also mock path.join() --- .../context/additionalContextProvider.test.ts | 117 ++++++++++++++++++ .../context/additionalContextProvider.ts | 2 +- 2 files changed, 118 insertions(+), 1 deletion(-) diff --git a/server/aws-lsp-codewhisperer/src/language-server/agenticChat/context/additionalContextProvider.test.ts b/server/aws-lsp-codewhisperer/src/language-server/agenticChat/context/additionalContextProvider.test.ts index 6c745f20b3..e6742fee1b 100644 --- a/server/aws-lsp-codewhisperer/src/language-server/agenticChat/context/additionalContextProvider.test.ts +++ b/server/aws-lsp-codewhisperer/src/language-server/agenticChat/context/additionalContextProvider.test.ts @@ -174,6 +174,12 @@ describe('AdditionalContextProvider', () => { workspaceFolder: mockWorkspaceFolder, } + // Mock path.join to simulate Unix behavior + sinon.stub(path, 'join').callsFake((...args) => { + // Simulate Unix path.join behavior + return args.join('/').replace(/\\/g, '/') + }) + const explicitContext = [ { id: 'explicit-file', @@ -208,6 +214,9 @@ describe('AdditionalContextProvider', () => { assert.strictEqual(result.length, 1) assert.strictEqual(result[0].name, 'Explicit File') assert.strictEqual(result[0].pinned, false) + + // Restore original path.join + ;(path.join as sinon.SinonStub).restore() }) it('should avoid duplicates between explicit and pinned context', async () => { @@ -220,6 +229,12 @@ describe('AdditionalContextProvider', () => { workspaceFolder: mockWorkspaceFolder, } + // Mock path.join to simulate Unix behavior + sinon.stub(path, 'join').callsFake((...args) => { + // Simulate Unix path.join behavior + return args.join('/').replace(/\\/g, '/') + }) + const sharedContext = { id: 'shared-file', command: 'Shared File', @@ -255,6 +270,9 @@ describe('AdditionalContextProvider', () => { assert.strictEqual(result.length, 1) assert.strictEqual(result[0].name, 'Shared File') assert.strictEqual(result[0].pinned, false) // Should be marked as explicit, not pinned + + // Restore original path.join + ;(path.join as sinon.SinonStub).restore() }) it('should handle Active File context correctly', async () => { @@ -358,6 +376,105 @@ describe('AdditionalContextProvider', () => { assert.strictEqual(triggerContext.contextInfo?.pinnedContextCount.codeContextCount, 1) assert.strictEqual(triggerContext.contextInfo?.pinnedContextCount.promptContextCount, 1) }) + + it('should handle Unix path separators correctly', async () => { + const mockWorkspaceFolder = { uri: URI.file('/workspace').toString(), name: 'test' } + sinon.stub(workspaceUtils, 'getWorkspaceFolderPaths').returns(['/workspace']) + + // Mock path.join to simulate Unix behavior + sinon.stub(path, 'join').callsFake((...args) => { + // Simulate Unix path.join behavior + return args.join('/').replace(/\\/g, '/') + }) + + const explicitContext = [ + { + id: 'unix-prompt', + command: 'Unix Prompt', + label: 'file' as any, + route: ['/Users/test/.aws/amazonq/prompts', 'hello.md'], + }, + ] + + fsExistsStub.callsFake((path: string) => path.includes('.amazonq/rules')) + fsReadDirStub.resolves([]) + + // Reset stub - return data for first call (explicit context), empty for second call (pinned context) + getContextCommandPromptStub.reset() + getContextCommandPromptStub.onFirstCall().resolves([ + { + // promptContextCommands - explicit context + name: 'Unix Prompt', + content: 'content', + filePath: '/Users/test/.aws/amazonq/prompts/hello.md', // Proper Unix path + relativePath: 'hello.md', + startLine: 1, + endLine: 10, + }, + ]) + getContextCommandPromptStub.onSecondCall().resolves([]) // pinnedContextCommands - empty + + const result = await provider.getAdditionalContext( + { workspaceFolder: mockWorkspaceFolder }, + 'tab1', + explicitContext + ) + assert.strictEqual(result.length, 1) + assert.strictEqual(result[0].name, 'Unix Prompt') + + // Restore original path.join + ;(path.join as sinon.SinonStub).restore() + }) + + it('should handle Windows path separators correctly', async () => { + const mockWorkspaceFolder = { uri: URI.file('/workspace').toString(), name: 'test' } + sinon.stub(workspaceUtils, 'getWorkspaceFolderPaths').returns(['/workspace']) + + // Mock path.join to simulate Windows behavior + const originalPathJoin = path.join + sinon.stub(path, 'join').callsFake((...args) => { + // Simulate Windows path.join behavior + return args.join('\\').replace(/\//g, '\\') + }) + + const explicitContext = [ + { + id: 'windows-prompt', + command: 'Windows Prompt', + label: 'file' as any, + route: ['C:\\Users\\test\\.aws\\amazonq\\prompts', 'hello.md'], + }, + ] + + fsExistsStub.callsFake((path: string) => path.includes('.amazonq/rules')) + fsReadDirStub.resolves([]) + + // Reset stub - return data for first call (explicit context), empty for second call (pinned context) + getContextCommandPromptStub.reset() + getContextCommandPromptStub.onFirstCall().resolves([ + { + // promptContextCommands - explicit context + name: 'Windows Prompt', + content: 'content', + filePath: 'C:\\Users\\test\\.aws\\amazonq\\prompts\\hello.md', // Proper Windows path + relativePath: 'hello.md', + startLine: 1, + endLine: 10, + }, + ]) + getContextCommandPromptStub.onSecondCall().resolves([]) // pinnedContextCommands - empty + + const result = await provider.getAdditionalContext( + { workspaceFolder: mockWorkspaceFolder }, + 'tab1', + explicitContext + ) + assert.strictEqual(result.length, 1) + assert.strictEqual(result[0].name, 'Windows Prompt') + + // Restore original path.join + ;(path.join as sinon.SinonStub).restore() + }) }) describe('getFileListFromContext', () => { diff --git a/server/aws-lsp-codewhisperer/src/language-server/agenticChat/context/additionalContextProvider.ts b/server/aws-lsp-codewhisperer/src/language-server/agenticChat/context/additionalContextProvider.ts index d71da1a1fe..733d1df28c 100644 --- a/server/aws-lsp-codewhisperer/src/language-server/agenticChat/context/additionalContextProvider.ts +++ b/server/aws-lsp-codewhisperer/src/language-server/agenticChat/context/additionalContextProvider.ts @@ -406,7 +406,7 @@ export class AdditionalContextProvider { const image = imageMap.get(item.description) if (image) ordered.push(image) } else { - const doc = item.route ? docMap.get(item.route.join('/')) : undefined + const doc = item.route ? docMap.get(path.join(...item.route)) : undefined if (doc) ordered.push(doc) } } From e4e8bbb89e4b597926582bead2b14ffc43f2a7f8 Mon Sep 17 00:00:00 2001 From: Dung Dong Date: Mon, 18 Aug 2025 09:59:45 -0700 Subject: [PATCH 21/74] fix(amazonq): fix regression of mcp config in agent config (#2101) * fix(amazonq): update process-permission-updates to be the same as previous behavior * fix(chat-client): update package.json chat-client to prod * fix(amazonq): retain mcp permissions after disable/enable server * fix: dont call initOneServer on Built-in * fix: deny permission does not persist after restart IDE --------- Co-authored-by: Boyu --- .../agenticChat/tools/mcp/mcpEventHandler.ts | 71 ++++----- .../agenticChat/tools/mcp/mcpManager.ts | 137 +++++++++--------- .../agenticChat/tools/mcp/mcpUtils.ts | 56 +------ 3 files changed, 98 insertions(+), 166 deletions(-) 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 bf8c91b533..5b3ac84e98 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 @@ -35,6 +35,7 @@ import { ProfileStatusMonitor } from './profileStatusMonitor' interface PermissionOption { label: string value: string + description?: string } export class McpEventHandler { @@ -913,20 +914,15 @@ export class McpEventHandler { } const mcpManager = McpManager.instance - // Get the appropriate agent path const agentPath = mcpManager.getAllServerConfigs().get(serverName)?.__configPath__ - - const perm: MCPServerPermission = { - enabled: true, - toolPerms: {}, - __configPath__: agentPath, - } - // Set flag to ignore file changes during permission update this.#isProgrammaticChange = true try { + const perm = mcpManager.getMcpServerPermissions(serverName)! + perm.enabled = true + perm.__configPath__ = agentPath await mcpManager.updateServerPermission(serverName, perm) this.#emitMCPConfigEvent() } catch (error) { @@ -944,21 +940,15 @@ export class McpEventHandler { if (!serverName) { return { id: params.id } } - const mcpManager = McpManager.instance - // Get the appropriate agent path + // Set flag to ignore file changes during permission update const agentPath = mcpManager.getAllServerConfigs().get(serverName)?.__configPath__ - - const perm: MCPServerPermission = { - enabled: false, - toolPerms: {}, - __configPath__: agentPath, - } - // Set flag to ignore file changes during permission update this.#isProgrammaticChange = true - try { + const perm = mcpManager.getMcpServerPermissions(serverName)! + perm.enabled = false + perm.__configPath__ = agentPath await mcpManager.updateServerPermission(serverName, perm) this.#emitMCPConfigEvent() } catch (error) { @@ -1078,17 +1068,16 @@ export class McpEventHandler { // Add tool select options toolsWithPermissions.forEach(item => { const toolName = item.tool.toolName - const currentPermission = this.#getCurrentPermission(item.permission) // For Built-in server, use a special function that doesn't include the 'Deny' option - const permissionOptions = this.#buildPermissionOptions(item.permission) + let permissionOptions = this.#buildPermissionOptions() filterOptions.push({ type: 'select', id: `${toolName}`, title: toolName, description: item.tool.description, - placeholder: currentPermission, options: permissionOptions, + ...{ value: item.permission, boldTitle: true, mandatory: true, hideMandatoryIcon: true }, }) }) @@ -1141,20 +1130,22 @@ export class McpEventHandler { /** * Builds permission options excluding the current one */ - #buildPermissionOptions(currentPermission: string) { + #buildPermissionOptions() { const permissionOptions: PermissionOption[] = [] - if (currentPermission !== McpPermissionType.alwaysAllow) { - permissionOptions.push({ label: 'Always allow', value: McpPermissionType.alwaysAllow }) - } + permissionOptions.push({ + label: 'Ask', + value: McpPermissionType.ask, + description: 'Ask for your approval each time this tool is run', + }) - if (currentPermission !== McpPermissionType.ask) { - permissionOptions.push({ label: 'Ask', value: McpPermissionType.ask }) - } + permissionOptions.push({ + label: 'Always allow', + value: McpPermissionType.alwaysAllow, + description: 'Always allow this tool to run without asking for approval', + }) - if (currentPermission !== McpPermissionType.deny) { - permissionOptions.push({ label: 'Deny', value: McpPermissionType.deny }) - } + permissionOptions.push({ label: 'Deny', value: McpPermissionType.deny, description: 'Never run this tool' }) return permissionOptions } @@ -1203,6 +1194,7 @@ export class McpEventHandler { } const mcpServerPermission = await this.#processPermissionUpdates( + serverName, updatedPermissionConfig, serverConfig?.__configPath__ ) @@ -1400,27 +1392,20 @@ export class McpEventHandler { /** * Processes permission updates from the UI */ - async #processPermissionUpdates(updatedPermissionConfig: any, agentPath: string | undefined) { + async #processPermissionUpdates(serverName: string, updatedPermissionConfig: any, agentPath: string | undefined) { + const builtInToolAgentPath = await this.#getAgentPath() const perm: MCPServerPermission = { enabled: true, toolPerms: {}, - __configPath__: agentPath, + __configPath__: serverName === 'Built-in' ? builtInToolAgentPath : agentPath, } // Process each tool permission setting for (const [key, val] of Object.entries(updatedPermissionConfig)) { if (key === 'scope') continue - // // Get the default permission for this tool from McpManager - // let defaultPermission = McpManager.instance.getToolPerm(serverName, key) - - // // If no default permission is found, use 'alwaysAllow' for Built-in and 'ask' for MCP servers - // if (!defaultPermission) { - // defaultPermission = serverName === 'Built-in' ? 'alwaysAllow' : 'ask' - // } - - // If the value is an empty string (''), skip this tool to preserve its existing permission in the persona file - if (val === '') continue + const currentPerm = McpManager.instance.getToolPerm(serverName, key) + if (val === currentPerm) continue switch (val) { case McpPermissionType.alwaysAllow: perm.toolPerms[key] = McpPermissionType.alwaysAllow diff --git a/server/aws-lsp-codewhisperer/src/language-server/agenticChat/tools/mcp/mcpManager.ts b/server/aws-lsp-codewhisperer/src/language-server/agenticChat/tools/mcp/mcpManager.ts index 04f827ff2c..e639807929 100644 --- a/server/aws-lsp-codewhisperer/src/language-server/agenticChat/tools/mcp/mcpManager.ts +++ b/server/aws-lsp-codewhisperer/src/language-server/agenticChat/tools/mcp/mcpManager.ts @@ -178,14 +178,49 @@ export class McpManager { // Reset permissions map this.mcpServerPermissions.clear() - - // Initialize permissions for servers from agent config + // Create init state for (const [sanitizedName, _] of this.mcpServers.entries()) { - const name = this.serverNameMapping.get(sanitizedName) || sanitizedName - // Set server status to UNINITIALIZED initially this.setState(sanitizedName, McpServerStatus.UNINITIALIZED, 0) + } + // Get all servers that need to be initialized + const serversToInit: Array<[string, MCPServerConfig]> = [] + + for (const [name, cfg] of this.mcpServers.entries()) { + if (this.isServerDisabled(name)) { + this.features.logging.info(`MCP: server '${name}' is disabled by persona settings, skipping`) + this.setState(name, McpServerStatus.DISABLED, 0) + this.emitToolsChanged(name) + continue + } + serversToInit.push([name, cfg]) + } + + // Process servers in batches of 5 at a time + const MAX_CONCURRENT_SERVERS = 5 + const totalServers = serversToInit.length + + if (totalServers > 0) { + this.features.logging.info( + `MCP: initializing ${totalServers} servers with max concurrency of ${MAX_CONCURRENT_SERVERS}` + ) + + // Process servers in batches + for (let i = 0; i < totalServers; i += MAX_CONCURRENT_SERVERS) { + const batch = serversToInit.slice(i, i + MAX_CONCURRENT_SERVERS) + const batchPromises = batch.map(([name, cfg]) => this.initOneServer(name, cfg)) + + this.features.logging.debug( + `MCP: initializing batch of ${batch.length} servers (${i + 1}-${Math.min(i + MAX_CONCURRENT_SERVERS, totalServers)} of ${totalServers})` + ) + await Promise.all(batchPromises) + } + this.features.logging.info(`MCP: completed initialization of ${totalServers} servers`) + } + + for (const [sanitizedName, _] of this.mcpServers.entries()) { + const name = this.serverNameMapping.get(sanitizedName) || sanitizedName // Initialize permissions for this server const serverPrefix = `@${name}` @@ -208,9 +243,18 @@ export class McpManager { }) } else { // Only specific tools are enabled + // get allTools of this server, if it's not in tools --> it's denied + // have to move the logic after all servers finish init, because that's when we have list of tools + const deniedTools = new Set( + this.getAllTools() + .filter(tool => tool.serverName === name) + .map(tool => tool.toolName) + ) this.agentConfig.tools.forEach(tool => { if (tool.startsWith(serverPrefix + '/')) { + // remove this from deniedTools const toolName = tool.substring(serverPrefix.length + 1) + deniedTools.delete(toolName) if (toolName) { // Check if tool is in allowedTools if (this.agentConfig.allowedTools.includes(tool)) { @@ -221,6 +265,11 @@ export class McpManager { } } }) + + // update permission to deny for rest of the tools + deniedTools.forEach(tool => { + toolPerms[tool] = McpPermissionType.deny + }) } this.mcpServerPermissions.set(sanitizedName, { @@ -228,42 +277,6 @@ export class McpManager { toolPerms, }) } - - // Get all servers that need to be initialized - const serversToInit: Array<[string, MCPServerConfig]> = [] - - for (const [name, cfg] of this.mcpServers.entries()) { - if (this.isServerDisabled(name)) { - this.features.logging.info(`MCP: server '${name}' is disabled by persona settings, skipping`) - this.setState(name, McpServerStatus.DISABLED, 0) - this.emitToolsChanged(name) - continue - } - serversToInit.push([name, cfg]) - } - - // Process servers in batches of 5 at a time - const MAX_CONCURRENT_SERVERS = 5 - const totalServers = serversToInit.length - - if (totalServers > 0) { - this.features.logging.info( - `MCP: initializing ${totalServers} servers with max concurrency of ${MAX_CONCURRENT_SERVERS}` - ) - - // Process servers in batches - for (let i = 0; i < totalServers; i += MAX_CONCURRENT_SERVERS) { - const batch = serversToInit.slice(i, i + MAX_CONCURRENT_SERVERS) - const batchPromises = batch.map(([name, cfg]) => this.initOneServer(name, cfg)) - - this.features.logging.debug( - `MCP: initializing batch of ${batch.length} servers (${i + 1}-${Math.min(i + MAX_CONCURRENT_SERVERS, totalServers)} of ${totalServers})` - ) - await Promise.all(batchPromises) - } - - this.features.logging.info(`MCP: completed initialization of ${totalServers} servers`) - } } /** @@ -658,13 +671,7 @@ export class McpManager { } // Save agent config once with all changes - await saveAgentConfig( - this.features.workspace, - this.features.logging, - this.agentConfig, - agentPath, - serverName - ) + await saveAgentConfig(this.features.workspace, this.features.logging, this.agentConfig, agentPath) // Add server tools to tools list after initialization await this.initOneServer(sanitizedName, newCfg) @@ -728,13 +735,7 @@ export class McpManager { }) // Save agent config - await saveAgentConfig( - this.features.workspace, - this.features.logging, - this.agentConfig, - cfg.__configPath__, - unsanitizedName - ) + await saveAgentConfig(this.features.workspace, this.features.logging, this.agentConfig, cfg.__configPath__) // Get all config paths and delete the server from each one const wsUris = this.features.workspace.getAllWorkspaceFolders()?.map(f => f.uri) ?? [] @@ -817,13 +818,7 @@ export class McpManager { this.agentConfig.mcpServers[unsanitizedServerName] = updatedConfig // Save agent config - await saveAgentConfig( - this.features.workspace, - this.features.logging, - this.agentConfig, - agentPath, - unsanitizedServerName - ) + await saveAgentConfig(this.features.workspace, this.features.logging, this.agentConfig, agentPath) } const newCfg: MCPServerConfig = { @@ -1035,13 +1030,7 @@ export class McpManager { // Save agent config const agentPath = perm.__configPath__ if (agentPath) { - await saveAgentConfig( - this.features.workspace, - this.features.logging, - this.agentConfig, - agentPath, - unsanitizedServerName - ) + await saveAgentConfig(this.features.workspace, this.features.logging, this.agentConfig, agentPath) } // Update mcpServerPermissions map @@ -1059,7 +1048,7 @@ export class McpManager { } this.setState(serverName, McpServerStatus.DISABLED, 0) } else { - if (!this.clients.has(serverName)) { + if (!this.clients.has(serverName) && serverName !== 'Built-in') { await this.initOneServer(serverName, this.mcpServers.get(serverName)!) } } @@ -1087,6 +1076,13 @@ export class McpManager { return !this.agentConfig.allowedTools.includes(toolId) } + /** + * get server's tool permission + */ + public getMcpServerPermissions(serverName: string): MCPServerPermission | undefined { + return this.mcpServerPermissions.get(serverName) + } + /** * Returns any errors that occurred during loading of MCP configuration files */ @@ -1152,8 +1148,7 @@ export class McpManager { this.features.workspace, this.features.logging, this.agentConfig, - cfg.__configPath__, - unsanitizedName + cfg.__configPath__ ) } } catch (err) { diff --git a/server/aws-lsp-codewhisperer/src/language-server/agenticChat/tools/mcp/mcpUtils.ts b/server/aws-lsp-codewhisperer/src/language-server/agenticChat/tools/mcp/mcpUtils.ts index 7b212bad49..ffd2cfb4fc 100644 --- a/server/aws-lsp-codewhisperer/src/language-server/agenticChat/tools/mcp/mcpUtils.ts +++ b/server/aws-lsp-codewhisperer/src/language-server/agenticChat/tools/mcp/mcpUtils.ts @@ -958,61 +958,13 @@ export async function saveAgentConfig( workspace: Workspace, logging: Logger, config: AgentConfig, - configPath: string, - serverName?: string + configPath: string ): Promise { try { await workspace.fs.mkdir(path.dirname(configPath), { recursive: true }) - - if (!serverName) { - // Save the whole config - await workspace.fs.writeFile(configPath, JSON.stringify(config, null, 2)) - logging.info(`Saved agent config to ${configPath}`) - return - } - - // Read existing config if it exists, otherwise use default - let existingConfig: any - try { - const configExists = await workspace.fs.exists(configPath) - if (configExists) { - const raw = (await workspace.fs.readFile(configPath)).toString().trim() - existingConfig = raw ? JSON.parse(raw) : JSON.parse(DEFAULT_AGENT_RAW) - } else { - existingConfig = JSON.parse(DEFAULT_AGENT_RAW) - } - } catch (err) { - logging.warn(`Failed to read existing config at ${configPath}: ${err}`) - existingConfig = JSON.parse(DEFAULT_AGENT_RAW) - } - - // Update only the specific server's config - if (config.mcpServers[serverName]) { - existingConfig.mcpServers[serverName] = config.mcpServers[serverName] - } - - // Remove existing tools for this server - const serverToolPattern = `@${serverName}` - existingConfig.tools = existingConfig.tools.filter( - (tool: string) => tool !== serverToolPattern && !tool.startsWith(`${serverToolPattern}/`) - ) - existingConfig.allowedTools = existingConfig.allowedTools.filter( - (tool: string) => tool !== serverToolPattern && !tool.startsWith(`${serverToolPattern}/`) - ) - - // Add only tools for this server - const serverTools = config.tools.filter( - tool => tool === serverToolPattern || tool.startsWith(`${serverToolPattern}/`) - ) - const serverAllowedTools = config.allowedTools.filter( - tool => tool === serverToolPattern || tool.startsWith(`${serverToolPattern}/`) - ) - - existingConfig.tools.push(...serverTools) - existingConfig.allowedTools.push(...serverAllowedTools) - - await workspace.fs.writeFile(configPath, JSON.stringify(existingConfig, null, 2)) - logging.info(`Saved agent config for server ${serverName} to ${configPath}`) + // Save the whole config + await workspace.fs.writeFile(configPath, JSON.stringify(config, null, 2)) + logging.info(`Saved agent config to ${configPath}`) } catch (err: any) { logging.error(`Failed to save agent config to ${configPath}: ${err.message}`) throw err From d397161cc3448c63016e27f5ac2a1917cdaae1cb Mon Sep 17 00:00:00 2001 From: Rajanna-Karthik Date: Mon, 18 Aug 2025 10:04:18 -0700 Subject: [PATCH 22/74] feat: remove project type validation from LSP layer (#2103) * feat: remove project type validation from LSP layer * refactor: remove unused validation functions from LSP layer --- .../resources/SupportedProjects.ts | 3 + .../netTransform/tests/validation.test.ts | 120 +----------------- .../netTransform/transformHandler.ts | 12 -- .../netTransform/validation.ts | 30 +---- 4 files changed, 5 insertions(+), 160 deletions(-) diff --git a/server/aws-lsp-codewhisperer/src/language-server/netTransform/resources/SupportedProjects.ts b/server/aws-lsp-codewhisperer/src/language-server/netTransform/resources/SupportedProjects.ts index a3d6322a07..69881dfc89 100644 --- a/server/aws-lsp-codewhisperer/src/language-server/netTransform/resources/SupportedProjects.ts +++ b/server/aws-lsp-codewhisperer/src/language-server/netTransform/resources/SupportedProjects.ts @@ -1,3 +1,6 @@ +/** + * Reference only - validation moved to backend service. + */ export const supportedProjects = [ 'AspNetCoreMvc', 'AspNetCoreWebApi', diff --git a/server/aws-lsp-codewhisperer/src/language-server/netTransform/tests/validation.test.ts b/server/aws-lsp-codewhisperer/src/language-server/netTransform/tests/validation.test.ts index a8cfcf3475..90ce3d1f7a 100644 --- a/server/aws-lsp-codewhisperer/src/language-server/netTransform/tests/validation.test.ts +++ b/server/aws-lsp-codewhisperer/src/language-server/netTransform/tests/validation.test.ts @@ -1,6 +1,6 @@ import { expect } from 'chai' import { StartTransformRequest, TransformProjectMetadata } from '../models' -import { isProject, isSolution, validateProject, validateSolution } from '../validation' +import { isProject, isSolution } from '../validation' import { supportedProjects, unsupportedViewComponents } from '../resources/SupportedProjects' import mock = require('mock-fs') import { Logging } from '@aws/language-server-runtimes/server-interface' @@ -46,122 +46,4 @@ describe('Test validation functionality', () => { mockStartTransformationRequest.SelectedProjectPath = 'test.csproj' expect(isSolution(mockStartTransformationRequest)).to.equal(false) }) - - it('should return true when project is a supported type', () => { - let mockStartTransformationRequest: StartTransformRequest = sampleStartTransformRequest - const mockProjectMeta = { - Name: '', - ProjectTargetFramework: '', - ProjectPath: 'test.csproj', - SourceCodeFilePaths: [], - ProjectLanguage: '', - ProjectType: 'AspNetCoreMvc', - ExternalReferences: [], - } - mockStartTransformationRequest.ProjectMetadata.push(mockProjectMeta) - - expect(validateProject(mockStartTransformationRequest, mockedLogging)).to.equal(true) - }) - - it('should return false when project is not a supported type', () => { - let mockStartTransformationRequest: StartTransformRequest = sampleStartTransformRequest - const mockProjectMeta = { - Name: '', - ProjectTargetFramework: '', - ProjectPath: 'test.csproj', - SourceCodeFilePaths: [], - ProjectLanguage: '', - ProjectType: 'not supported', - ExternalReferences: [], - } - mockStartTransformationRequest.ProjectMetadata = [] - mockStartTransformationRequest.ProjectMetadata.push(mockProjectMeta) - - expect(validateProject(mockStartTransformationRequest, mockedLogging)).to.equal(false) - }) - - it('should return false when there is no project path that is the same as the selected project path', () => { - let mockStartTransformationRequest: StartTransformRequest = sampleStartTransformRequest - const mockProjectMeta = { - Name: '', - ProjectTargetFramework: '', - ProjectPath: 'different.csproj', - SourceCodeFilePaths: [], - ProjectLanguage: '', - ProjectType: 'AspNetCoreMvc', - ExternalReferences: [], - } - mockStartTransformationRequest.ProjectMetadata = [] - mockStartTransformationRequest.ProjectMetadata.push(mockProjectMeta) - - expect(validateProject(mockStartTransformationRequest, mockedLogging)).to.equal(false) - }) - - // New tests for AspNetWebForms validation - it('should return true when project is AspNetWebForms type', () => { - let mockStartTransformationRequest: StartTransformRequest = sampleStartTransformRequest - const mockProjectMeta = { - Name: '', - ProjectTargetFramework: '', - ProjectPath: 'test.csproj', - SourceCodeFilePaths: [], - ProjectLanguage: '', - ProjectType: 'AspNetWebForms', - ExternalReferences: [], - } - mockStartTransformationRequest.ProjectMetadata = [] - mockStartTransformationRequest.ProjectMetadata.push(mockProjectMeta) - - expect(validateProject(mockStartTransformationRequest, mockedLogging)).to.equal(true) - }) - - it('should not include AspNetWebForms in unsupported projects list', () => { - let mockStartTransformationRequest: StartTransformRequest = sampleStartTransformRequest - - // Add a supported project - const supportedProjectMeta = { - Name: 'Supported', - ProjectTargetFramework: '', - ProjectPath: 'supported.csproj', - SourceCodeFilePaths: [], - ProjectLanguage: '', - ProjectType: 'AspNetCoreMvc', - ExternalReferences: [], - } - - // Add an unsupported project - const unsupportedProjectMeta = { - Name: 'Unsupported', - ProjectTargetFramework: '', - ProjectPath: 'unsupported.csproj', - SourceCodeFilePaths: [], - ProjectLanguage: '', - ProjectType: 'UnsupportedType', - ExternalReferences: [], - } - - // Add an AspNetWebForms project - const webFormsProjectMeta = { - Name: 'WebForms', - ProjectTargetFramework: '', - ProjectPath: 'webforms.csproj', - SourceCodeFilePaths: [], - ProjectLanguage: '', - ProjectType: 'AspNetWebForms', - ExternalReferences: [], - } - - mockStartTransformationRequest.ProjectMetadata = [ - supportedProjectMeta, - unsupportedProjectMeta, - webFormsProjectMeta, - ] - - const unsupportedProjects = validateSolution(mockStartTransformationRequest) - - // Should only contain the unsupported project, not the AspNetWebForms project - expect(unsupportedProjects).to.have.lengthOf(1) - expect(unsupportedProjects[0]).to.equal('unsupported.csproj') - expect(unsupportedProjects).to.not.include('webforms.csproj') - }) }) diff --git a/server/aws-lsp-codewhisperer/src/language-server/netTransform/transformHandler.ts b/server/aws-lsp-codewhisperer/src/language-server/netTransform/transformHandler.ts index b0afaa06f1..b8bc80b30c 100644 --- a/server/aws-lsp-codewhisperer/src/language-server/netTransform/transformHandler.ts +++ b/server/aws-lsp-codewhisperer/src/language-server/netTransform/transformHandler.ts @@ -54,18 +54,6 @@ export class TransformHandler { isProject, this.logging ) - if (isProject) { - let isValid = validation.validateProject(userInputrequest, this.logging) - if (!isValid) { - return { - Error: 'NotSupported', - IsSupported: false, - ContainsUnsupportedViews: containsUnsupportedViews, - } as StartTransformResponse - } - } else { - unsupportedProjects = validation.validateSolution(userInputrequest) - } const artifactManager = new ArtifactManager( this.workspace, diff --git a/server/aws-lsp-codewhisperer/src/language-server/netTransform/validation.ts b/server/aws-lsp-codewhisperer/src/language-server/netTransform/validation.ts index 0d244e09a8..efb443fb0f 100644 --- a/server/aws-lsp-codewhisperer/src/language-server/netTransform/validation.ts +++ b/server/aws-lsp-codewhisperer/src/language-server/netTransform/validation.ts @@ -6,9 +6,7 @@ import { TransformationJob } from '../../client/token/codewhispererbearertokencl import { TransformationErrorCode } from './models' /** - * TEMPORARY HACK: AspNetWebForms project type is allowed in validateProject and validateSolution - * functions without being added to the supportedProjects array. This is to enable WebForms to Blazor - * transformation without officially supporting it yet. + * Project type validation moved to backend service. */ export function isProject(userInputrequest: StartTransformRequest): boolean { @@ -19,32 +17,6 @@ export function isSolution(userInputrequest: StartTransformRequest): boolean { return userInputrequest.SelectedProjectPath.endsWith('.sln') } -export function validateProject(userInputrequest: StartTransformRequest, logging: Logging): boolean { - var selectedProject = userInputrequest.ProjectMetadata.find( - project => project.ProjectPath == userInputrequest.SelectedProjectPath - ) - - if (selectedProject) { - // Temporary hack: Allow AspNetWebForms project type without adding it to supportedProjects - var isValid = - supportedProjects.includes(selectedProject?.ProjectType) || - selectedProject?.ProjectType === 'AspNetWebForms' - logging.log( - `Selected project ${userInputrequest?.SelectedProjectPath} has project type ${selectedProject.ProjectType}` + - (isValid ? '' : ' that is not supported') - ) - return isValid - } - logging.log(`Error occured in verifying selected project with path ${userInputrequest.SelectedProjectPath}`) - return false -} - -export function validateSolution(userInputrequest: StartTransformRequest): string[] { - return userInputrequest.ProjectMetadata.filter( - project => !supportedProjects.includes(project.ProjectType) && project.ProjectType !== 'AspNetWebForms' - ).map(project => project.ProjectPath) -} - export async function checkForUnsupportedViews( userInputRequest: StartTransformRequest, isProject: boolean, From 2a4171a74c15c23c23c481060496162bcc9e6284 Mon Sep 17 00:00:00 2001 From: invictus <149003065+ashishrp-aws@users.noreply.github.com> Date: Mon, 18 Aug 2025 13:04:17 -0700 Subject: [PATCH 23/74] fix: fix for button text and remove profilearn caching (#2137) --- chat-client/src/client/mcpMynahUi.ts | 1 + .../agenticChat/tools/mcp/profileStatusMonitor.ts | 13 +++---------- 2 files changed, 4 insertions(+), 10 deletions(-) diff --git a/chat-client/src/client/mcpMynahUi.ts b/chat-client/src/client/mcpMynahUi.ts index 5ece955dfa..5cdae85da1 100644 --- a/chat-client/src/client/mcpMynahUi.ts +++ b/chat-client/src/client/mcpMynahUi.ts @@ -276,6 +276,7 @@ export class McpMynahUi { params.header.actions?.map(action => ({ ...action, icon: action.icon ? toMynahIcon(action.icon) : undefined, + text: undefined, })) || [], } : undefined, diff --git a/server/aws-lsp-codewhisperer/src/language-server/agenticChat/tools/mcp/profileStatusMonitor.ts b/server/aws-lsp-codewhisperer/src/language-server/agenticChat/tools/mcp/profileStatusMonitor.ts index 4a1cc705e4..a436e152ff 100644 --- a/server/aws-lsp-codewhisperer/src/language-server/agenticChat/tools/mcp/profileStatusMonitor.ts +++ b/server/aws-lsp-codewhisperer/src/language-server/agenticChat/tools/mcp/profileStatusMonitor.ts @@ -18,7 +18,6 @@ export class ProfileStatusMonitor { private intervalId?: NodeJS.Timeout private readonly CHECK_INTERVAL = 24 * 60 * 60 * 1000 // 24 hours private codeWhispererClient?: CodeWhispererServiceToken - private cachedProfileArn?: string private static lastMcpState?: boolean constructor( @@ -85,7 +84,8 @@ export class ProfileStatusMonitor { const response = await retryUtils.retryWithBackoff(() => this.codeWhispererClient!.getProfile({ profileArn }) ) - const isMcpEnabled = response?.profile?.optInFeatures?.mcpConfiguration?.toggle === 'ON' + const mcpConfig = response?.profile?.optInFeatures?.mcpConfiguration + const isMcpEnabled = mcpConfig ? mcpConfig.toggle === 'ON' : true if (ProfileStatusMonitor.lastMcpState !== isMcpEnabled) { ProfileStatusMonitor.lastMcpState = isMcpEnabled @@ -107,16 +107,9 @@ export class ProfileStatusMonitor { } private getProfileArn(): string | undefined { - // Use cached value if available - if (this.cachedProfileArn) { - return this.cachedProfileArn - } - try { - // Get profile ARN from service manager like in agenticChatController const serviceManager = AmazonQTokenServiceManager.getInstance() - this.cachedProfileArn = serviceManager.getActiveProfileArn() - return this.cachedProfileArn + return serviceManager.getActiveProfileArn() } catch (error) { this.logging.debug(`Failed to get profile ARN: ${error}`) } From b3938c1c3ef33e9872d84782d1dd0d6dff617f44 Mon Sep 17 00:00:00 2001 From: tsmithsz <84354541+tsmithsz@users.noreply.github.com> Date: Mon, 18 Aug 2025 15:29:49 -0700 Subject: [PATCH 24/74] chore: bump runtimes to 0.2.126 (#2138) --- app/aws-lsp-antlr4-runtimes/package.json | 2 +- app/aws-lsp-buildspec-runtimes/package.json | 2 +- .../package.json | 2 +- .../package.json | 2 +- app/aws-lsp-identity-runtimes/package.json | 2 +- app/aws-lsp-json-runtimes/package.json | 2 +- .../package.json | 2 +- app/aws-lsp-s3-runtimes/package.json | 2 +- app/aws-lsp-yaml-json-webworker/package.json | 2 +- app/aws-lsp-yaml-runtimes/package.json | 2 +- app/hello-world-lsp-runtimes/package.json | 2 +- chat-client/package.json | 2 +- client/vscode/package.json | 2 +- core/aws-lsp-core/package.json | 2 +- .../q-agentic-chat-server/package.json | 2 +- package-lock.json | 60 +++++++++---------- server/aws-lsp-antlr4/package.json | 2 +- server/aws-lsp-buildspec/package.json | 2 +- server/aws-lsp-cloudformation/package.json | 2 +- server/aws-lsp-codewhisperer/package.json | 2 +- server/aws-lsp-identity/package.json | 2 +- server/aws-lsp-json/package.json | 2 +- server/aws-lsp-notification/package.json | 2 +- server/aws-lsp-partiql/package.json | 2 +- server/aws-lsp-s3/package.json | 2 +- server/aws-lsp-yaml/package.json | 2 +- server/device-sso-auth-lsp/package.json | 2 +- server/hello-world-lsp/package.json | 2 +- 28 files changed, 57 insertions(+), 57 deletions(-) diff --git a/app/aws-lsp-antlr4-runtimes/package.json b/app/aws-lsp-antlr4-runtimes/package.json index 4289cec4b1..30b9b57a65 100644 --- a/app/aws-lsp-antlr4-runtimes/package.json +++ b/app/aws-lsp-antlr4-runtimes/package.json @@ -12,7 +12,7 @@ "webpack": "webpack" }, "dependencies": { - "@aws/language-server-runtimes": "^0.2.125", + "@aws/language-server-runtimes": "^0.2.126", "@aws/lsp-antlr4": "*", "antlr4-c3": "^3.4.1", "antlr4ng": "^3.0.4" diff --git a/app/aws-lsp-buildspec-runtimes/package.json b/app/aws-lsp-buildspec-runtimes/package.json index 700afb3e7f..47a88b907a 100644 --- a/app/aws-lsp-buildspec-runtimes/package.json +++ b/app/aws-lsp-buildspec-runtimes/package.json @@ -7,7 +7,7 @@ "compile": "tsc --build" }, "dependencies": { - "@aws/language-server-runtimes": "^0.2.125", + "@aws/language-server-runtimes": "^0.2.126", "@aws/lsp-buildspec": "^0.0.1" } } diff --git a/app/aws-lsp-cloudformation-runtimes/package.json b/app/aws-lsp-cloudformation-runtimes/package.json index 5cc04150cb..68547449cc 100644 --- a/app/aws-lsp-cloudformation-runtimes/package.json +++ b/app/aws-lsp-cloudformation-runtimes/package.json @@ -7,7 +7,7 @@ "compile": "tsc --build" }, "dependencies": { - "@aws/language-server-runtimes": "^0.2.125", + "@aws/language-server-runtimes": "^0.2.126", "@aws/lsp-cloudformation": "^0.0.1" } } diff --git a/app/aws-lsp-codewhisperer-runtimes/package.json b/app/aws-lsp-codewhisperer-runtimes/package.json index fc31e64589..24e06d87ea 100644 --- a/app/aws-lsp-codewhisperer-runtimes/package.json +++ b/app/aws-lsp-codewhisperer-runtimes/package.json @@ -23,7 +23,7 @@ "local-build": "node scripts/local-build.js" }, "dependencies": { - "@aws/language-server-runtimes": "^0.2.125", + "@aws/language-server-runtimes": "^0.2.126", "@aws/lsp-codewhisperer": "*", "copyfiles": "^2.4.1", "cross-env": "^7.0.3", diff --git a/app/aws-lsp-identity-runtimes/package.json b/app/aws-lsp-identity-runtimes/package.json index 869a30cb20..2bf37011c3 100644 --- a/app/aws-lsp-identity-runtimes/package.json +++ b/app/aws-lsp-identity-runtimes/package.json @@ -7,7 +7,7 @@ "compile": "tsc --build" }, "dependencies": { - "@aws/language-server-runtimes": "^0.2.125", + "@aws/language-server-runtimes": "^0.2.126", "@aws/lsp-identity": "^0.0.1" } } diff --git a/app/aws-lsp-json-runtimes/package.json b/app/aws-lsp-json-runtimes/package.json index f63dd4e985..8a2ea3203a 100644 --- a/app/aws-lsp-json-runtimes/package.json +++ b/app/aws-lsp-json-runtimes/package.json @@ -11,7 +11,7 @@ "webpack": "webpack" }, "dependencies": { - "@aws/language-server-runtimes": "^0.2.125", + "@aws/language-server-runtimes": "^0.2.126", "@aws/lsp-json": "*" }, "devDependencies": { diff --git a/app/aws-lsp-notification-runtimes/package.json b/app/aws-lsp-notification-runtimes/package.json index faf9e4c24c..b1877ae13e 100644 --- a/app/aws-lsp-notification-runtimes/package.json +++ b/app/aws-lsp-notification-runtimes/package.json @@ -7,7 +7,7 @@ "compile": "tsc --build" }, "dependencies": { - "@aws/language-server-runtimes": "^0.2.125", + "@aws/language-server-runtimes": "^0.2.126", "@aws/lsp-notification": "^0.0.1" } } diff --git a/app/aws-lsp-s3-runtimes/package.json b/app/aws-lsp-s3-runtimes/package.json index 42efa998be..a242281ff2 100644 --- a/app/aws-lsp-s3-runtimes/package.json +++ b/app/aws-lsp-s3-runtimes/package.json @@ -10,7 +10,7 @@ "compile": "tsc --build" }, "dependencies": { - "@aws/language-server-runtimes": "^0.2.125", + "@aws/language-server-runtimes": "^0.2.126", "@aws/lsp-s3": "^0.0.1" } } diff --git a/app/aws-lsp-yaml-json-webworker/package.json b/app/aws-lsp-yaml-json-webworker/package.json index 6190893816..8eebf1ea84 100644 --- a/app/aws-lsp-yaml-json-webworker/package.json +++ b/app/aws-lsp-yaml-json-webworker/package.json @@ -11,7 +11,7 @@ "serve:webpack": "NODE_ENV=development webpack serve" }, "dependencies": { - "@aws/language-server-runtimes": "^0.2.125", + "@aws/language-server-runtimes": "^0.2.126", "@aws/lsp-json": "*", "@aws/lsp-yaml": "*" }, diff --git a/app/aws-lsp-yaml-runtimes/package.json b/app/aws-lsp-yaml-runtimes/package.json index eb76a46e38..1428ef3058 100644 --- a/app/aws-lsp-yaml-runtimes/package.json +++ b/app/aws-lsp-yaml-runtimes/package.json @@ -11,7 +11,7 @@ "webpack": "webpack" }, "dependencies": { - "@aws/language-server-runtimes": "^0.2.125", + "@aws/language-server-runtimes": "^0.2.126", "@aws/lsp-yaml": "*" }, "devDependencies": { diff --git a/app/hello-world-lsp-runtimes/package.json b/app/hello-world-lsp-runtimes/package.json index 14a3a75ff4..352bf82f88 100644 --- a/app/hello-world-lsp-runtimes/package.json +++ b/app/hello-world-lsp-runtimes/package.json @@ -15,7 +15,7 @@ }, "dependencies": { "@aws/hello-world-lsp": "^0.0.1", - "@aws/language-server-runtimes": "^0.2.125" + "@aws/language-server-runtimes": "^0.2.126" }, "devDependencies": { "@types/chai": "^4.3.5", diff --git a/chat-client/package.json b/chat-client/package.json index 2ee60a8bec..65dee12960 100644 --- a/chat-client/package.json +++ b/chat-client/package.json @@ -25,7 +25,7 @@ }, "dependencies": { "@aws/chat-client-ui-types": "^0.1.56", - "@aws/language-server-runtimes": "^0.2.125", + "@aws/language-server-runtimes": "^0.2.126", "@aws/language-server-runtimes-types": "^0.1.50", "@aws/mynah-ui": "^4.36.4" }, diff --git a/client/vscode/package.json b/client/vscode/package.json index 409a4fc50d..4cb42a0752 100644 --- a/client/vscode/package.json +++ b/client/vscode/package.json @@ -352,7 +352,7 @@ "@aws-sdk/credential-providers": "^3.731.1", "@aws-sdk/types": "^3.734.0", "@aws/chat-client-ui-types": "^0.1.56", - "@aws/language-server-runtimes": "^0.2.125", + "@aws/language-server-runtimes": "^0.2.126", "@types/uuid": "^9.0.8", "@types/vscode": "^1.98.0", "jose": "^5.2.4", diff --git a/core/aws-lsp-core/package.json b/core/aws-lsp-core/package.json index 5a3d438d47..04081f13fc 100644 --- a/core/aws-lsp-core/package.json +++ b/core/aws-lsp-core/package.json @@ -28,7 +28,7 @@ "prepack": "shx cp ../../LICENSE ../../NOTICE ../../SECURITY.md ." }, "dependencies": { - "@aws/language-server-runtimes": "^0.2.125", + "@aws/language-server-runtimes": "^0.2.126", "@gerhobbelt/gitignore-parser": "^0.2.0-9", "cross-spawn": "7.0.6", "jose": "^5.2.4", diff --git a/integration-tests/q-agentic-chat-server/package.json b/integration-tests/q-agentic-chat-server/package.json index 1e93a252dd..7415e1653e 100644 --- a/integration-tests/q-agentic-chat-server/package.json +++ b/integration-tests/q-agentic-chat-server/package.json @@ -9,7 +9,7 @@ "test-integ": "npm run compile && mocha --timeout 30000 \"./out/**/*.test.js\" --retries 2" }, "dependencies": { - "@aws/language-server-runtimes": "^0.2.125", + "@aws/language-server-runtimes": "^0.2.126", "@aws/lsp-core": "*" }, "devDependencies": { diff --git a/package-lock.json b/package-lock.json index d1ac2ece1c..128ae8083d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -48,7 +48,7 @@ "name": "@aws/lsp-antlr4-runtimes", "version": "0.0.1", "dependencies": { - "@aws/language-server-runtimes": "^0.2.125", + "@aws/language-server-runtimes": "^0.2.126", "@aws/lsp-antlr4": "*", "antlr4-c3": "^3.4.1", "antlr4ng": "^3.0.4" @@ -71,7 +71,7 @@ "name": "@aws/lsp-buildspec-runtimes", "version": "0.0.1", "dependencies": { - "@aws/language-server-runtimes": "^0.2.125", + "@aws/language-server-runtimes": "^0.2.126", "@aws/lsp-buildspec": "^0.0.1" } }, @@ -79,7 +79,7 @@ "name": "@aws/lsp-cloudformation-runtimes", "version": "0.0.1", "dependencies": { - "@aws/language-server-runtimes": "^0.2.125", + "@aws/language-server-runtimes": "^0.2.126", "@aws/lsp-cloudformation": "^0.0.1" } }, @@ -87,7 +87,7 @@ "name": "@aws/lsp-codewhisperer-runtimes", "version": "0.0.1", "dependencies": { - "@aws/language-server-runtimes": "^0.2.125", + "@aws/language-server-runtimes": "^0.2.126", "@aws/lsp-codewhisperer": "*", "copyfiles": "^2.4.1", "cross-env": "^7.0.3", @@ -120,7 +120,7 @@ "name": "@aws/lsp-identity-runtimes", "version": "0.1.0", "dependencies": { - "@aws/language-server-runtimes": "^0.2.125", + "@aws/language-server-runtimes": "^0.2.126", "@aws/lsp-identity": "^0.0.1" } }, @@ -128,7 +128,7 @@ "name": "@aws/lsp-json-runtimes", "version": "0.0.1", "dependencies": { - "@aws/language-server-runtimes": "^0.2.125", + "@aws/language-server-runtimes": "^0.2.126", "@aws/lsp-json": "*" }, "devDependencies": { @@ -148,7 +148,7 @@ "name": "@aws/lsp-notification-runtimes", "version": "0.1.0", "dependencies": { - "@aws/language-server-runtimes": "^0.2.125", + "@aws/language-server-runtimes": "^0.2.126", "@aws/lsp-notification": "^0.0.1" } }, @@ -181,7 +181,7 @@ "name": "@aws/lsp-s3-runtimes", "version": "0.0.1", "dependencies": { - "@aws/language-server-runtimes": "^0.2.125", + "@aws/language-server-runtimes": "^0.2.126", "@aws/lsp-s3": "^0.0.1" }, "bin": { @@ -192,7 +192,7 @@ "name": "@aws/lsp-yaml-json-webworker", "version": "0.0.1", "dependencies": { - "@aws/language-server-runtimes": "^0.2.125", + "@aws/language-server-runtimes": "^0.2.126", "@aws/lsp-json": "*", "@aws/lsp-yaml": "*" }, @@ -212,7 +212,7 @@ "name": "@aws/lsp-yaml-runtimes", "version": "0.0.1", "dependencies": { - "@aws/language-server-runtimes": "^0.2.125", + "@aws/language-server-runtimes": "^0.2.126", "@aws/lsp-yaml": "*" }, "devDependencies": { @@ -234,7 +234,7 @@ "version": "0.0.1", "dependencies": { "@aws/hello-world-lsp": "^0.0.1", - "@aws/language-server-runtimes": "^0.2.125" + "@aws/language-server-runtimes": "^0.2.126" }, "devDependencies": { "@types/chai": "^4.3.5", @@ -255,7 +255,7 @@ "license": "Apache-2.0", "dependencies": { "@aws/chat-client-ui-types": "^0.1.56", - "@aws/language-server-runtimes": "^0.2.125", + "@aws/language-server-runtimes": "^0.2.126", "@aws/language-server-runtimes-types": "^0.1.50", "@aws/mynah-ui": "^4.36.4" }, @@ -280,7 +280,7 @@ "@aws-sdk/credential-providers": "^3.731.1", "@aws-sdk/types": "^3.734.0", "@aws/chat-client-ui-types": "^0.1.56", - "@aws/language-server-runtimes": "^0.2.125", + "@aws/language-server-runtimes": "^0.2.126", "@types/uuid": "^9.0.8", "@types/vscode": "^1.98.0", "jose": "^5.2.4", @@ -296,7 +296,7 @@ "version": "0.0.13", "license": "Apache-2.0", "dependencies": { - "@aws/language-server-runtimes": "^0.2.125", + "@aws/language-server-runtimes": "^0.2.126", "@gerhobbelt/gitignore-parser": "^0.2.0-9", "cross-spawn": "7.0.6", "jose": "^5.2.4", @@ -327,7 +327,7 @@ "name": "@aws/q-agentic-chat-server-integration-tests", "version": "0.0.1", "dependencies": { - "@aws/language-server-runtimes": "^0.2.125", + "@aws/language-server-runtimes": "^0.2.126", "@aws/lsp-core": "*" }, "devDependencies": { @@ -4036,9 +4036,9 @@ "link": true }, "node_modules/@aws/language-server-runtimes": { - "version": "0.2.125", - "resolved": "https://registry.npmjs.org/@aws/language-server-runtimes/-/language-server-runtimes-0.2.125.tgz", - "integrity": "sha512-tjXJEagZ6rm09fcgJGu1zbFNzi0+7R1mdNFa6zCIv68c76xq5JHjc++Hne9aOgp61O6BM9uNnX3KR57v9/0E1g==", + "version": "0.2.126", + "resolved": "https://registry.npmjs.org/@aws/language-server-runtimes/-/language-server-runtimes-0.2.126.tgz", + "integrity": "sha512-dUIKTL6+AOxdberwHLvigSJcbhFv6oUS3POhZWoNlBV9XJZRWwzNW9gkjkUsI03YTVshqMuVHT/HaoRW/hDkIA==", "license": "Apache-2.0", "dependencies": { "@aws/language-server-runtimes-types": "^0.1.56", @@ -28608,7 +28608,7 @@ "version": "0.1.17", "license": "Apache-2.0", "dependencies": { - "@aws/language-server-runtimes": "^0.2.125", + "@aws/language-server-runtimes": "^0.2.126", "@aws/lsp-core": "^0.0.13" }, "devDependencies": { @@ -28650,7 +28650,7 @@ "name": "@aws/lsp-buildspec", "version": "0.0.1", "dependencies": { - "@aws/language-server-runtimes": "^0.2.125", + "@aws/language-server-runtimes": "^0.2.126", "@aws/lsp-json": "*", "@aws/lsp-yaml": "*", "vscode-languageserver": "^9.0.1", @@ -28661,7 +28661,7 @@ "name": "@aws/lsp-cloudformation", "version": "0.0.1", "dependencies": { - "@aws/language-server-runtimes": "^0.2.125", + "@aws/language-server-runtimes": "^0.2.126", "@aws/lsp-core": "*", "@aws/lsp-json": "*", "vscode-languageserver": "^9.0.1", @@ -28683,7 +28683,7 @@ "@aws-sdk/util-arn-parser": "^3.723.0", "@aws-sdk/util-retry": "^3.374.0", "@aws/chat-client-ui-types": "^0.1.56", - "@aws/language-server-runtimes": "^0.2.125", + "@aws/language-server-runtimes": "^0.2.126", "@aws/lsp-core": "^0.0.13", "@modelcontextprotocol/sdk": "^1.15.0", "@smithy/node-http-handler": "^2.5.0", @@ -28823,7 +28823,7 @@ "dependencies": { "@aws-sdk/client-sso-oidc": "^3.616.0", "@aws-sdk/token-providers": "^3.744.0", - "@aws/language-server-runtimes": "^0.2.125", + "@aws/language-server-runtimes": "^0.2.126", "@aws/lsp-core": "^0.0.12", "@smithy/node-http-handler": "^3.2.5", "@smithy/shared-ini-file-loader": "^4.0.1", @@ -28888,7 +28888,7 @@ "version": "0.1.17", "license": "Apache-2.0", "dependencies": { - "@aws/language-server-runtimes": "^0.2.125", + "@aws/language-server-runtimes": "^0.2.126", "@aws/lsp-core": "^0.0.13", "vscode-languageserver": "^9.0.1", "vscode-languageserver-textdocument": "^1.0.8" @@ -28905,7 +28905,7 @@ "version": "0.0.1", "license": "Apache-2.0", "dependencies": { - "@aws/language-server-runtimes": "^0.2.125", + "@aws/language-server-runtimes": "^0.2.126", "@aws/lsp-core": "^0.0.12", "vscode-languageserver": "^9.0.1" }, @@ -28966,7 +28966,7 @@ "version": "0.0.16", "license": "Apache-2.0", "dependencies": { - "@aws/language-server-runtimes": "^0.2.125", + "@aws/language-server-runtimes": "^0.2.126", "antlr4-c3": "3.4.2", "antlr4ng": "3.0.14", "web-tree-sitter": "0.22.6" @@ -28988,7 +28988,7 @@ "dependencies": { "@aws-sdk/client-s3": "^3.623.0", "@aws-sdk/types": "^3.734.0", - "@aws/language-server-runtimes": "^0.2.125", + "@aws/language-server-runtimes": "^0.2.126", "@aws/lsp-core": "^0.0.12", "vscode-languageserver": "^9.0.1", "vscode-languageserver-textdocument": "^1.0.8" @@ -29019,7 +29019,7 @@ "hasInstallScript": true, "license": "Apache-2.0", "dependencies": { - "@aws/language-server-runtimes": "^0.2.125", + "@aws/language-server-runtimes": "^0.2.126", "@aws/lsp-core": "^0.0.13", "vscode-languageserver": "^9.0.1", "vscode-languageserver-textdocument": "^1.0.8", @@ -29033,7 +29033,7 @@ "name": "@amzn/device-sso-auth-lsp", "version": "0.0.1", "dependencies": { - "@aws/language-server-runtimes": "^0.2.125", + "@aws/language-server-runtimes": "^0.2.126", "vscode-languageserver": "^9.0.1" }, "devDependencies": { @@ -29044,7 +29044,7 @@ "name": "@aws/hello-world-lsp", "version": "0.0.1", "dependencies": { - "@aws/language-server-runtimes": "^0.2.125", + "@aws/language-server-runtimes": "^0.2.126", "vscode-languageserver": "^9.0.1" }, "devDependencies": { diff --git a/server/aws-lsp-antlr4/package.json b/server/aws-lsp-antlr4/package.json index f540c8ae21..9ba8186aab 100644 --- a/server/aws-lsp-antlr4/package.json +++ b/server/aws-lsp-antlr4/package.json @@ -28,7 +28,7 @@ "clean": "rm -rf node_modules" }, "dependencies": { - "@aws/language-server-runtimes": "^0.2.125", + "@aws/language-server-runtimes": "^0.2.126", "@aws/lsp-core": "^0.0.13" }, "peerDependencies": { diff --git a/server/aws-lsp-buildspec/package.json b/server/aws-lsp-buildspec/package.json index 2cf0b776ac..3a23338dac 100644 --- a/server/aws-lsp-buildspec/package.json +++ b/server/aws-lsp-buildspec/package.json @@ -7,7 +7,7 @@ "compile": "tsc --build" }, "dependencies": { - "@aws/language-server-runtimes": "^0.2.125", + "@aws/language-server-runtimes": "^0.2.126", "@aws/lsp-json": "*", "@aws/lsp-yaml": "*", "vscode-languageserver": "^9.0.1", diff --git a/server/aws-lsp-cloudformation/package.json b/server/aws-lsp-cloudformation/package.json index ad01b4457a..75223b4791 100644 --- a/server/aws-lsp-cloudformation/package.json +++ b/server/aws-lsp-cloudformation/package.json @@ -7,7 +7,7 @@ "compile": "tsc --build" }, "dependencies": { - "@aws/language-server-runtimes": "^0.2.125", + "@aws/language-server-runtimes": "^0.2.126", "@aws/lsp-core": "*", "@aws/lsp-json": "*", "vscode-languageserver": "^9.0.1", diff --git a/server/aws-lsp-codewhisperer/package.json b/server/aws-lsp-codewhisperer/package.json index 8600148090..78297c702e 100644 --- a/server/aws-lsp-codewhisperer/package.json +++ b/server/aws-lsp-codewhisperer/package.json @@ -36,7 +36,7 @@ "@aws-sdk/util-arn-parser": "^3.723.0", "@aws-sdk/util-retry": "^3.374.0", "@aws/chat-client-ui-types": "^0.1.56", - "@aws/language-server-runtimes": "^0.2.125", + "@aws/language-server-runtimes": "^0.2.126", "@aws/lsp-core": "^0.0.13", "@modelcontextprotocol/sdk": "^1.15.0", "@smithy/node-http-handler": "^2.5.0", diff --git a/server/aws-lsp-identity/package.json b/server/aws-lsp-identity/package.json index acaeb6d15d..0e61d97dc8 100644 --- a/server/aws-lsp-identity/package.json +++ b/server/aws-lsp-identity/package.json @@ -26,7 +26,7 @@ "dependencies": { "@aws-sdk/client-sso-oidc": "^3.616.0", "@aws-sdk/token-providers": "^3.744.0", - "@aws/language-server-runtimes": "^0.2.125", + "@aws/language-server-runtimes": "^0.2.126", "@aws/lsp-core": "^0.0.12", "@smithy/node-http-handler": "^3.2.5", "@smithy/shared-ini-file-loader": "^4.0.1", diff --git a/server/aws-lsp-json/package.json b/server/aws-lsp-json/package.json index 7b1768f995..66604360fa 100644 --- a/server/aws-lsp-json/package.json +++ b/server/aws-lsp-json/package.json @@ -26,7 +26,7 @@ "prepack": "shx cp ../../LICENSE ../../NOTICE ../../SECURITY.md ." }, "dependencies": { - "@aws/language-server-runtimes": "^0.2.125", + "@aws/language-server-runtimes": "^0.2.126", "@aws/lsp-core": "^0.0.13", "vscode-languageserver": "^9.0.1", "vscode-languageserver-textdocument": "^1.0.8" diff --git a/server/aws-lsp-notification/package.json b/server/aws-lsp-notification/package.json index 45194be66b..f6483e3e32 100644 --- a/server/aws-lsp-notification/package.json +++ b/server/aws-lsp-notification/package.json @@ -22,7 +22,7 @@ "coverage:report": "c8 report --reporter=html --reporter=text" }, "dependencies": { - "@aws/language-server-runtimes": "^0.2.125", + "@aws/language-server-runtimes": "^0.2.126", "@aws/lsp-core": "^0.0.12", "vscode-languageserver": "^9.0.1" }, diff --git a/server/aws-lsp-partiql/package.json b/server/aws-lsp-partiql/package.json index 263275dced..efc073c7e9 100644 --- a/server/aws-lsp-partiql/package.json +++ b/server/aws-lsp-partiql/package.json @@ -24,7 +24,7 @@ "out" ], "dependencies": { - "@aws/language-server-runtimes": "^0.2.125", + "@aws/language-server-runtimes": "^0.2.126", "antlr4-c3": "3.4.2", "antlr4ng": "3.0.14", "web-tree-sitter": "0.22.6" diff --git a/server/aws-lsp-s3/package.json b/server/aws-lsp-s3/package.json index 53355ff18a..b574da9bc0 100644 --- a/server/aws-lsp-s3/package.json +++ b/server/aws-lsp-s3/package.json @@ -9,7 +9,7 @@ "dependencies": { "@aws-sdk/client-s3": "^3.623.0", "@aws-sdk/types": "^3.734.0", - "@aws/language-server-runtimes": "^0.2.125", + "@aws/language-server-runtimes": "^0.2.126", "@aws/lsp-core": "^0.0.12", "vscode-languageserver": "^9.0.1", "vscode-languageserver-textdocument": "^1.0.8" diff --git a/server/aws-lsp-yaml/package.json b/server/aws-lsp-yaml/package.json index deeddd9694..e18a72ea29 100644 --- a/server/aws-lsp-yaml/package.json +++ b/server/aws-lsp-yaml/package.json @@ -26,7 +26,7 @@ "postinstall": "node patchYamlPackage.js" }, "dependencies": { - "@aws/language-server-runtimes": "^0.2.125", + "@aws/language-server-runtimes": "^0.2.126", "@aws/lsp-core": "^0.0.13", "vscode-languageserver": "^9.0.1", "vscode-languageserver-textdocument": "^1.0.8", diff --git a/server/device-sso-auth-lsp/package.json b/server/device-sso-auth-lsp/package.json index 698bbf353e..6eba3692a9 100644 --- a/server/device-sso-auth-lsp/package.json +++ b/server/device-sso-auth-lsp/package.json @@ -7,7 +7,7 @@ "compile": "tsc --build" }, "dependencies": { - "@aws/language-server-runtimes": "^0.2.125", + "@aws/language-server-runtimes": "^0.2.126", "vscode-languageserver": "^9.0.1" }, "devDependencies": { diff --git a/server/hello-world-lsp/package.json b/server/hello-world-lsp/package.json index a0ad8e8469..b221272541 100644 --- a/server/hello-world-lsp/package.json +++ b/server/hello-world-lsp/package.json @@ -13,7 +13,7 @@ "coverage:report": "c8 report --reporter=html --reporter=text" }, "dependencies": { - "@aws/language-server-runtimes": "^0.2.125", + "@aws/language-server-runtimes": "^0.2.126", "vscode-languageserver": "^9.0.1" }, "devDependencies": { From f947e1a9da4431d6089b22825f992010c30a470b Mon Sep 17 00:00:00 2001 From: invictus <149003065+ashishrp-aws@users.noreply.github.com> Date: Tue, 19 Aug 2025 08:59:01 -0700 Subject: [PATCH 25/74] fix: fix to add disk caching for mcp admin state (#2139) * fix: fix to add disk caching for mcp admin state * fix: fix to add logging * fix: fix to initialize mcpManager in any case and discover servers based on mcpState * fix: fix for unit test failure --- .../agenticChat/tools/mcp/mcpManager.ts | 18 ++++- .../tools/mcp/profileStatusMonitor.test.ts | 4 +- .../tools/mcp/profileStatusMonitor.ts | 72 ++++++++++++++++--- .../agenticChat/tools/toolServer.ts | 62 +++++++++++----- .../AmazonQTokenServiceManager.ts | 19 +++++ 5 files changed, 142 insertions(+), 33 deletions(-) diff --git a/server/aws-lsp-codewhisperer/src/language-server/agenticChat/tools/mcp/mcpManager.ts b/server/aws-lsp-codewhisperer/src/language-server/agenticChat/tools/mcp/mcpManager.ts index e639807929..844a872485 100644 --- a/server/aws-lsp-codewhisperer/src/language-server/agenticChat/tools/mcp/mcpManager.ts +++ b/server/aws-lsp-codewhisperer/src/language-server/agenticChat/tools/mcp/mcpManager.ts @@ -37,6 +37,7 @@ import { Mutex } from 'async-mutex' import path = require('path') import { URI } from 'vscode-uri' import { sanitizeInput } from '../../../../shared/utils' +import { ProfileStatusMonitor } from './profileStatusMonitor' export const MCP_SERVER_STATUS_CHANGED = 'mcpServerStatusChanged' export const AGENT_TOOLS_CHANGED = 'agentToolsChanged' @@ -85,8 +86,15 @@ export class McpManager { if (!McpManager.#instance) { const mgr = new McpManager(agentPaths, features) McpManager.#instance = mgr - await mgr.discoverAllServers() - features.logging.info(`MCP: discovered ${mgr.mcpTools.length} tools across all servers`) + + const shouldDiscoverServers = ProfileStatusMonitor.getMcpState() + + if (shouldDiscoverServers) { + await mgr.discoverAllServers() + features.logging.info(`MCP: discovered ${mgr.mcpTools.length} tools across all servers`) + } else { + features.logging.info('MCP: initialized without server discovery') + } // Emit MCP configuration metrics const serverConfigs = mgr.getAllServerConfigs() @@ -896,7 +904,11 @@ export class McpManager { // Restore the saved tool name mapping this.setToolNameMapping(savedToolNameMapping) - await this.discoverAllServers() + const shouldDiscoverServers = ProfileStatusMonitor.getMcpState() + + if (shouldDiscoverServers) { + await this.discoverAllServers() + } const reinitializedServerCount = McpManager.#instance?.mcpServers.size this.features.logging.info( diff --git a/server/aws-lsp-codewhisperer/src/language-server/agenticChat/tools/mcp/profileStatusMonitor.test.ts b/server/aws-lsp-codewhisperer/src/language-server/agenticChat/tools/mcp/profileStatusMonitor.test.ts index 8ee8454374..77080bf08a 100644 --- a/server/aws-lsp-codewhisperer/src/language-server/agenticChat/tools/mcp/profileStatusMonitor.test.ts +++ b/server/aws-lsp-codewhisperer/src/language-server/agenticChat/tools/mcp/profileStatusMonitor.test.ts @@ -169,8 +169,8 @@ describe('ProfileStatusMonitor', () => { mockOnMcpEnabled ) - // Initially undefined - expect(ProfileStatusMonitor.getMcpState()).to.be.undefined + // Initially true (default value) + expect(ProfileStatusMonitor.getMcpState()).to.be.true // Set through internal mechanism (simulating state change) ;(ProfileStatusMonitor as any).lastMcpState = false diff --git a/server/aws-lsp-codewhisperer/src/language-server/agenticChat/tools/mcp/profileStatusMonitor.ts b/server/aws-lsp-codewhisperer/src/language-server/agenticChat/tools/mcp/profileStatusMonitor.ts index a436e152ff..ad34550270 100644 --- a/server/aws-lsp-codewhisperer/src/language-server/agenticChat/tools/mcp/profileStatusMonitor.ts +++ b/server/aws-lsp-codewhisperer/src/language-server/agenticChat/tools/mcp/profileStatusMonitor.ts @@ -13,12 +13,22 @@ import { retryUtils } from '@aws/lsp-core' import { CodeWhispererServiceToken } from '../../../../shared/codeWhispererService' import { DEFAULT_AWS_Q_ENDPOINT_URL, DEFAULT_AWS_Q_REGION } from '../../../../shared/constants' import { AmazonQTokenServiceManager } from '../../../../shared/amazonQServiceManager/AmazonQTokenServiceManager' +import * as fs from 'fs' +import * as path from 'path' +import * as os from 'os' +import { EventEmitter } from 'events' + +export const AUTH_SUCCESS_EVENT = 'authSuccess' export class ProfileStatusMonitor { private intervalId?: NodeJS.Timeout private readonly CHECK_INTERVAL = 24 * 60 * 60 * 1000 // 24 hours private codeWhispererClient?: CodeWhispererServiceToken - private static lastMcpState?: boolean + private static lastMcpState: boolean = true + private static readonly MCP_CACHE_DIR = path.join(os.homedir(), '.aws', 'amazonq', 'mcpAdmin') + private static readonly MCP_CACHE_FILE = path.join(ProfileStatusMonitor.MCP_CACHE_DIR, 'mcp-state.json') + private static eventEmitter = new EventEmitter() + private static logging?: Logging constructor( private credentialsProvider: CredentialsProvider, @@ -27,7 +37,15 @@ export class ProfileStatusMonitor { private sdkInitializator: SDKInitializator, private onMcpDisabled: () => void, private onMcpEnabled?: () => void - ) {} + ) { + ProfileStatusMonitor.logging = logging + ProfileStatusMonitor.loadMcpStateFromDisk() + + // Listen for auth success events + ProfileStatusMonitor.eventEmitter.on(AUTH_SUCCESS_EVENT, () => { + void this.isMcpEnabled() + }) + } async checkInitialState(): Promise { try { @@ -35,8 +53,7 @@ export class ProfileStatusMonitor { return isMcpEnabled !== false // Return true if enabled or API failed } catch (error) { this.logging.debug(`Initial MCP state check failed, defaulting to enabled: ${error}`) - ProfileStatusMonitor.lastMcpState = true - return true + return ProfileStatusMonitor.getMcpState() } } @@ -65,7 +82,7 @@ export class ProfileStatusMonitor { const profileArn = this.getProfileArn() if (!profileArn) { this.logging.debug('No profile ARN available for MCP configuration check') - ProfileStatusMonitor.lastMcpState = true // Default to enabled if no profile + ProfileStatusMonitor.setMcpState(true) return true } @@ -88,7 +105,7 @@ export class ProfileStatusMonitor { const isMcpEnabled = mcpConfig ? mcpConfig.toggle === 'ON' : true if (ProfileStatusMonitor.lastMcpState !== isMcpEnabled) { - ProfileStatusMonitor.lastMcpState = isMcpEnabled + ProfileStatusMonitor.setMcpState(isMcpEnabled) if (!isMcpEnabled) { this.logging.info('MCP configuration disabled - removing tools') this.onMcpDisabled() @@ -101,8 +118,7 @@ export class ProfileStatusMonitor { return isMcpEnabled } catch (error) { this.logging.debug(`MCP configuration check failed, defaulting to enabled: ${error}`) - ProfileStatusMonitor.lastMcpState = true - return true + return ProfileStatusMonitor.getMcpState() } } @@ -116,7 +132,45 @@ export class ProfileStatusMonitor { return undefined } - static getMcpState(): boolean | undefined { + static getMcpState(): boolean { return ProfileStatusMonitor.lastMcpState } + + private static loadMcpStateFromDisk(): void { + try { + if (fs.existsSync(ProfileStatusMonitor.MCP_CACHE_FILE)) { + const data = fs.readFileSync(ProfileStatusMonitor.MCP_CACHE_FILE, 'utf8') + const parsed = JSON.parse(data) + ProfileStatusMonitor.lastMcpState = parsed.enabled ?? true + } + } catch (error) { + ProfileStatusMonitor.logging?.debug(`Failed to load MCP state from disk: ${error}`) + } + ProfileStatusMonitor.setMcpState(ProfileStatusMonitor.lastMcpState) + } + + private static saveMcpStateToDisk(): void { + try { + fs.mkdirSync(ProfileStatusMonitor.MCP_CACHE_DIR, { recursive: true }) + fs.writeFileSync( + ProfileStatusMonitor.MCP_CACHE_FILE, + JSON.stringify({ enabled: ProfileStatusMonitor.lastMcpState }) + ) + } catch (error) { + ProfileStatusMonitor.logging?.debug(`Failed to save MCP state to disk: ${error}`) + } + } + + private static setMcpState(enabled: boolean): void { + ProfileStatusMonitor.lastMcpState = enabled + ProfileStatusMonitor.saveMcpStateToDisk() + } + + static resetMcpState(): void { + ProfileStatusMonitor.setMcpState(true) + } + + static emitAuthSuccess(): void { + ProfileStatusMonitor.eventEmitter.emit(AUTH_SUCCESS_EVENT) + } } 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 fd692e6e8e..78871faa69 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 @@ -227,7 +227,16 @@ export const McpToolsServer: Server = ({ } registered[server] = [] } - void McpManager.instance.close(true) //keep the instance but close all servers. + + // Only close McpManager if it has been initialized + try { + if (McpManager.instance) { + void McpManager.instance.close(true) //keep the instance but close all servers. + } + } catch (error) { + // McpManager not initialized, skip closing + logging.debug('McpManager not initialized, skipping close operation') + } try { chat?.sendChatUpdate({ @@ -255,11 +264,19 @@ export const McpToolsServer: Server = ({ // Sanitize the tool name // Check if this tool name is already in use + let toolNameMapping = new Map() + try { + toolNameMapping = McpManager.instance.getToolNameMapping() + } catch (error) { + // McpManager not initialized, use empty mapping + logging.debug('McpManager not initialized, using empty tool name mapping') + } + const namespaced = createNamespacedToolName( def.serverName, def.toolName, allNamespacedTools, - McpManager.instance.getToolNameMapping() + toolNameMapping ) const tool = new McpTool({ logging, workspace, lsp }, def) @@ -315,17 +332,20 @@ export const McpToolsServer: Server = ({ McpManager.instance.clearToolNameMapping() - const byServer: Record = {} - for (const d of mgr.getEnabledTools()) { - ;(byServer[d.serverName] ||= []).push(d) - } - for (const [server, defs] of Object.entries(byServer)) { - registerServerTools(server, defs) - } + // Only register tools if MCP is enabled + if (ProfileStatusMonitor.getMcpState()) { + const byServer: Record = {} + for (const d of mgr.getEnabledTools()) { + ;(byServer[d.serverName] ||= []).push(d) + } + for (const [server, defs] of Object.entries(byServer)) { + registerServerTools(server, defs) + } - mgr.events.on(AGENT_TOOLS_CHANGED, (server: string, defs: McpToolDefinition[]) => { - registerServerTools(server, defs) - }) + mgr.events.on(AGENT_TOOLS_CHANGED, (server: string, defs: McpToolDefinition[]) => { + registerServerTools(server, defs) + }) + } } catch (e) { logging.error(`Failed to initialize MCP:' ${e}`) } @@ -353,11 +373,15 @@ export const McpToolsServer: Server = ({ // Wait for profile ARN to be available before checking MCP state const checkAndInitialize = async () => { - const shouldInitialize = await profileStatusMonitor!.checkInitialState() - if (shouldInitialize) { - logging.info('MCP enabled, initializing immediately') - await initializeMcp() + await profileStatusMonitor!.checkInitialState() + // Always initialize McpManager to handle UI requests + await initializeMcp() + + // Remove tools if MCP is disabled + if (!ProfileStatusMonitor.getMcpState()) { + removeAllMcpTools() } + profileStatusMonitor!.start() } @@ -375,7 +399,7 @@ export const McpToolsServer: Server = ({ } else if (Date.now() - startTime < SERVICE_MANAGER_TIMEOUT_MS) { setTimeout(pollForReady, SERVICE_MANAGER_POLL_INTERVAL_MS) } else { - logging.warn('Service manager not ready after 10s, defaulting MCP to enabled') + logging.warn('Service manager not ready after 10s, initializing MCP manager') await initializeMcp() profileStatusMonitor!.start() } @@ -383,8 +407,8 @@ export const McpToolsServer: Server = ({ setTimeout(pollForReady, SERVICE_MANAGER_POLL_INTERVAL_MS) } } catch (error) { - // Service manager not initialized yet, default to enabled - logging.info('Service manager not ready, defaulting MCP to enabled') + // Service manager not initialized yet, always initialize McpManager + logging.info('Service manager not ready, initializing MCP manager') await initializeMcp() profileStatusMonitor!.start() } diff --git a/server/aws-lsp-codewhisperer/src/shared/amazonQServiceManager/AmazonQTokenServiceManager.ts b/server/aws-lsp-codewhisperer/src/shared/amazonQServiceManager/AmazonQTokenServiceManager.ts index 8db60a69ab..5a919c9192 100644 --- a/server/aws-lsp-codewhisperer/src/shared/amazonQServiceManager/AmazonQTokenServiceManager.ts +++ b/server/aws-lsp-codewhisperer/src/shared/amazonQServiceManager/AmazonQTokenServiceManager.ts @@ -33,6 +33,7 @@ import { getAmazonQRegionAndEndpoint } from './configurationUtils' import { getUserAgent } from '../telemetryUtils' import { StreamingClientServiceToken } from '../streamingClientService' import { parse } from '@aws-sdk/util-arn-parser' +import { ProfileStatusMonitor } from '../../language-server/agenticChat/tools/mcp/profileStatusMonitor' /** * AmazonQTokenServiceManager manages state and provides centralized access to @@ -152,6 +153,9 @@ export class AmazonQTokenServiceManager extends BaseAmazonQServiceManager< this.resetCodewhispererService() this.connectionType = 'none' this.state = 'PENDING_CONNECTION' + + // Reset MCP state cache when auth changes + ProfileStatusMonitor.resetMcpState() } public async handleOnUpdateConfiguration(params: UpdateConfigurationParams, _token: CancellationToken) { @@ -245,6 +249,9 @@ export class AmazonQTokenServiceManager extends BaseAmazonQServiceManager< this.state = 'INITIALIZED' this.log('Initialized Amazon Q service with builderId connection') + // Emit auth success event + ProfileStatusMonitor.emitAuthSuccess() + return } @@ -267,6 +274,9 @@ export class AmazonQTokenServiceManager extends BaseAmazonQServiceManager< this.state = 'INITIALIZED' this.log('Initialized Amazon Q service with identityCenter connection') + // Emit auth success event + ProfileStatusMonitor.emitAuthSuccess() + return } @@ -375,6 +385,9 @@ export class AmazonQTokenServiceManager extends BaseAmazonQServiceManager< `Initialized identityCenter connection to region ${newProfile.identityDetails.region} for profile ${newProfile.arn}` ) + // Emit auth success event + ProfileStatusMonitor.emitAuthSuccess() + return } @@ -385,6 +398,9 @@ export class AmazonQTokenServiceManager extends BaseAmazonQServiceManager< this.activeIdcProfile = newProfile this.state = 'INITIALIZED' + // Emit auth success event + ProfileStatusMonitor.emitAuthSuccess() + return } @@ -428,6 +444,9 @@ export class AmazonQTokenServiceManager extends BaseAmazonQServiceManager< ) this.state = 'INITIALIZED' + // Emit auth success event + ProfileStatusMonitor.emitAuthSuccess() + return } From 43bc9b1120e52f206a4178d0125572aaaccf3c8b Mon Sep 17 00:00:00 2001 From: Will Lo <96078566+Will-ShaoHua@users.noreply.github.com> Date: Tue, 19 Aug 2025 09:44:39 -0700 Subject: [PATCH 26/74] refactor: cleanup old nep code path which is no longered being used in codewhispererServer.ts (#2141) --- .../inline-completion/codeWhispererServer.ts | 124 +++++++----------- 1 file changed, 44 insertions(+), 80 deletions(-) diff --git a/server/aws-lsp-codewhisperer/src/language-server/inline-completion/codeWhispererServer.ts b/server/aws-lsp-codewhisperer/src/language-server/inline-completion/codeWhispererServer.ts index 46a3241978..988bdd418f 100644 --- a/server/aws-lsp-codewhisperer/src/language-server/inline-completion/codeWhispererServer.ts +++ b/server/aws-lsp-codewhisperer/src/language-server/inline-completion/codeWhispererServer.ts @@ -468,93 +468,57 @@ export const CodewhispererServerFactory = return false }) - if (suggestionResponse.suggestionType === SuggestionType.COMPLETION) { - const { includeImportsWithSuggestions } = amazonQServiceManager.getConfiguration() - const suggestionsWithRightContext = mergeSuggestionsWithRightContext( - session.requestContext.fileContext.rightFileContent, - filteredSuggestions, - includeImportsWithSuggestions, - selectionRange - ).filter(suggestion => { - // Discard suggestions that have empty string insertText after right context merge and can't be displayed anymore - if (suggestion.insertText === '') { - session.setSuggestionState(suggestion.itemId, 'Discard') - return false - } - - return true - }) - - suggestionsWithRightContext.forEach(suggestion => { - const cachedSuggestion = session.suggestions.find(s => s.itemId === suggestion.itemId) - if (cachedSuggestion) cachedSuggestion.insertText = suggestion.insertText.toString() - }) + const { includeImportsWithSuggestions } = amazonQServiceManager.getConfiguration() + const suggestionsWithRightContext = mergeSuggestionsWithRightContext( + session.requestContext.fileContext.rightFileContent, + filteredSuggestions, + includeImportsWithSuggestions, + selectionRange + ).filter(suggestion => { + // Discard suggestions that have empty string insertText after right context merge and can't be displayed anymore + if (suggestion.insertText === '') { + session.setSuggestionState(suggestion.itemId, 'Discard') + return false + } - // TODO: need dedupe after right context merging but I don't see one - session.suggestionsAfterRightContextMerge.push(...suggestionsWithRightContext) + return true + }) - session.codewhispererSuggestionImportCount = - session.codewhispererSuggestionImportCount + - suggestionsWithRightContext.reduce((total, suggestion) => { - return total + (suggestion.mostRelevantMissingImports?.length || 0) - }, 0) + suggestionsWithRightContext.forEach(suggestion => { + const cachedSuggestion = session.suggestions.find(s => s.itemId === suggestion.itemId) + if (cachedSuggestion) cachedSuggestion.insertText = suggestion.insertText.toString() + }) - // If after all server-side filtering no suggestions can be displayed, and there is no nextToken - // close session and return empty results - if ( - session.suggestionsAfterRightContextMerge.length === 0 && - !suggestionResponse.responseContext.nextToken - ) { - completionSessionManager.closeSession(session) - await emitUserTriggerDecisionTelemetry( - telemetry, - telemetryService, - session, - timeSinceLastUserModification - ) + // TODO: need dedupe after right context merging but I don't see one + session.suggestionsAfterRightContextMerge.push(...suggestionsWithRightContext) - return EMPTY_RESULT - } + session.codewhispererSuggestionImportCount = + session.codewhispererSuggestionImportCount + + suggestionsWithRightContext.reduce((total, suggestion) => { + return total + (suggestion.mostRelevantMissingImports?.length || 0) + }, 0) - return { - items: suggestionsWithRightContext, - sessionId: session.id, - partialResultToken: suggestionResponse.responseContext.nextToken, - } - } else { - return { - items: suggestionResponse.suggestions - .map(suggestion => { - // Check if this suggestion is similar to a previously rejected edit - const isSimilarToRejected = rejectedEditTracker.isSimilarToRejected( - suggestion.content, - textDocument?.uri || '' - ) + // If after all server-side filtering no suggestions can be displayed, and there is no nextToken + // close session and return empty results + if ( + session.suggestionsAfterRightContextMerge.length === 0 && + !suggestionResponse.responseContext.nextToken + ) { + completionSessionManager.closeSession(session) + await emitUserTriggerDecisionTelemetry( + telemetry, + telemetryService, + session, + timeSinceLastUserModification + ) - if (isSimilarToRejected) { - // Mark as rejected in the session - session.setSuggestionState(suggestion.itemId, 'Reject') - logging.debug( - `[EDIT_PREDICTION] Filtered out suggestion similar to previously rejected edit` - ) - // Return empty item that will be filtered out - return { - insertText: '', - isInlineEdit: true, - itemId: suggestion.itemId, - } - } + return EMPTY_RESULT + } - return { - insertText: suggestion.content, - isInlineEdit: true, - itemId: suggestion.itemId, - } - }) - .filter(item => item.insertText !== ''), - sessionId: session.id, - partialResultToken: suggestionResponse.responseContext.nextToken, - } + return { + items: suggestionsWithRightContext, + sessionId: session.id, + partialResultToken: suggestionResponse.responseContext.nextToken, } } From 5e4435dfaea7bf8c00e6a27b9bb0d40f699d4e01 Mon Sep 17 00:00:00 2001 From: Jiatong Li Date: Tue, 19 Aug 2025 12:12:39 -0700 Subject: [PATCH 27/74] fix(amazonq): add server side control for WCS features (#2128) Co-authored-by: Jiatong Li --- .../workspaceContext/IdleWorkspaceManager.ts | 6 +- .../workspaceContextServer.ts | 3 +- .../workspaceFolderManager.test.ts | 90 +++++++++++++++++++ .../workspaceFolderManager.ts | 47 ++++++++-- 4 files changed, 139 insertions(+), 7 deletions(-) diff --git a/server/aws-lsp-codewhisperer/src/language-server/workspaceContext/IdleWorkspaceManager.ts b/server/aws-lsp-codewhisperer/src/language-server/workspaceContext/IdleWorkspaceManager.ts index 5a8359ccac..1e27a7f762 100644 --- a/server/aws-lsp-codewhisperer/src/language-server/workspaceContext/IdleWorkspaceManager.ts +++ b/server/aws-lsp-codewhisperer/src/language-server/workspaceContext/IdleWorkspaceManager.ts @@ -2,7 +2,7 @@ import { WorkspaceFolderManager } from './workspaceFolderManager' export class IdleWorkspaceManager { private static readonly idleThreshold = 30 * 60 * 1000 // 30 minutes - private static lastActivityTimestamp = 0 // treat session as idle as the start + private static lastActivityTimestamp = 0 // treat session as idle at the start private constructor() {} @@ -30,6 +30,10 @@ export class IdleWorkspaceManager { } } + public static setSessionAsIdle(): void { + IdleWorkspaceManager.lastActivityTimestamp = 0 + } + public static isSessionIdle(): boolean { const currentTime = Date.now() const timeSinceLastActivity = currentTime - IdleWorkspaceManager.lastActivityTimestamp diff --git a/server/aws-lsp-codewhisperer/src/language-server/workspaceContext/workspaceContextServer.ts b/server/aws-lsp-codewhisperer/src/language-server/workspaceContext/workspaceContextServer.ts index c536c1087d..3d638e337a 100644 --- a/server/aws-lsp-codewhisperer/src/language-server/workspaceContext/workspaceContextServer.ts +++ b/server/aws-lsp-codewhisperer/src/language-server/workspaceContext/workspaceContextServer.ts @@ -221,6 +221,7 @@ export const WorkspaceContextServer = (): Server => features => { isLoggedInUsingBearerToken(credentialsProvider) && abTestingEnabled && !workspaceFolderManager.getOptOutStatus() && + !workspaceFolderManager.isFeatureDisabled() && workspaceIdentifier ) } @@ -302,7 +303,7 @@ export const WorkspaceContextServer = (): Server => features => { await evaluateABTesting() isWorkflowInitialized = true - workspaceFolderManager.resetAdminOptOutStatus() + workspaceFolderManager.resetAdminOptOutAndFeatureDisabledStatus() if (!isUserEligibleForWorkspaceContext()) { return } diff --git a/server/aws-lsp-codewhisperer/src/language-server/workspaceContext/workspaceFolderManager.test.ts b/server/aws-lsp-codewhisperer/src/language-server/workspaceContext/workspaceFolderManager.test.ts index 7ab596a930..cbdb71db1d 100644 --- a/server/aws-lsp-codewhisperer/src/language-server/workspaceContext/workspaceFolderManager.test.ts +++ b/server/aws-lsp-codewhisperer/src/language-server/workspaceContext/workspaceFolderManager.test.ts @@ -8,6 +8,7 @@ import { ArtifactManager } from './artifactManager' import { CodeWhispererServiceToken } from '../../shared/codeWhispererService' import { ListWorkspaceMetadataResponse } from '../../client/token/codewhispererbearertokenclient' import { IdleWorkspaceManager } from './IdleWorkspaceManager' +import { AWSError } from 'aws-sdk' describe('WorkspaceFolderManager', () => { let mockServiceManager: StubbedInstance @@ -135,4 +136,93 @@ describe('WorkspaceFolderManager', () => { ) }) }) + + describe('isFeatureDisabled', () => { + it('should return true when feature is disabled', async () => { + // Setup + const workspaceFolders: WorkspaceFolder[] = [ + { + uri: 'file:///test/workspace', + name: 'test-workspace', + }, + ] + + // Mock listWorkspaceMetadata to throw AccessDeniedException with feature not supported + const mockError: AWSError = { + name: 'AccessDeniedException', + message: 'Feature is not supported', + code: 'AccessDeniedException', + time: new Date(), + retryable: false, + statusCode: 403, + } + + mockCodeWhispererService.listWorkspaceMetadata.rejects(mockError) + + // Create the WorkspaceFolderManager instance + workspaceFolderManager = WorkspaceFolderManager.createInstance( + mockServiceManager, + mockLogging, + mockArtifactManager, + mockDependencyDiscoverer, + workspaceFolders, + mockCredentialsProvider, + 'test-workspace-identifier' + ) + + // Spy on clearAllWorkspaceResources and related methods + const clearAllWorkspaceResourcesSpy = sinon.stub( + workspaceFolderManager as any, + 'clearAllWorkspaceResources' + ) + + // Act - trigger listWorkspaceMetadata which sets feature disabled state + await (workspaceFolderManager as any).listWorkspaceMetadata() + + // Assert + expect(workspaceFolderManager.isFeatureDisabled()).toBe(true) + + // Verify that clearAllWorkspaceResources was called + sinon.assert.calledOnce(clearAllWorkspaceResourcesSpy) + }) + + it('should return false when feature is not disabled', async () => { + // Setup + const workspaceFolders: WorkspaceFolder[] = [ + { + uri: 'file:///test/workspace', + name: 'test-workspace', + }, + ] + + // Mock successful response + const mockResponse: ListWorkspaceMetadataResponse = { + workspaces: [ + { + workspaceId: 'test-workspace-id', + workspaceStatus: 'RUNNING', + }, + ], + } + + mockCodeWhispererService.listWorkspaceMetadata.resolves(mockResponse as any) + + // Create the WorkspaceFolderManager instance + workspaceFolderManager = WorkspaceFolderManager.createInstance( + mockServiceManager, + mockLogging, + mockArtifactManager, + mockDependencyDiscoverer, + workspaceFolders, + mockCredentialsProvider, + 'test-workspace-identifier' + ) + + // Act - trigger listWorkspaceMetadata + await (workspaceFolderManager as any).listWorkspaceMetadata() + + // Assert + expect(workspaceFolderManager.isFeatureDisabled()).toBe(false) + }) + }) }) diff --git a/server/aws-lsp-codewhisperer/src/language-server/workspaceContext/workspaceFolderManager.ts b/server/aws-lsp-codewhisperer/src/language-server/workspaceContext/workspaceFolderManager.ts index 99fc9c4628..1644851258 100644 --- a/server/aws-lsp-codewhisperer/src/language-server/workspaceContext/workspaceFolderManager.ts +++ b/server/aws-lsp-codewhisperer/src/language-server/workspaceContext/workspaceFolderManager.ts @@ -55,6 +55,7 @@ export class WorkspaceFolderManager { private optOutMonitorInterval: NodeJS.Timeout | undefined private messageQueueConsumerInterval: NodeJS.Timeout | undefined private isOptedOut: boolean = false + private featureDisabled: boolean = false // Serve as a server-side control. If true, stop WCS features private isCheckingRemoteWorkspaceStatus: boolean = false private isArtifactUploadedToRemoteWorkspace: boolean = false @@ -139,8 +140,13 @@ export class WorkspaceFolderManager { return this.isOptedOut } - resetAdminOptOutStatus(): void { + resetAdminOptOutAndFeatureDisabledStatus(): void { this.isOptedOut = false + this.featureDisabled = false + } + + isFeatureDisabled(): boolean { + return this.featureDisabled } getWorkspaceState(): WorkspaceState { @@ -326,6 +332,7 @@ export class WorkspaceFolderManager { // Reset workspace ID to force operations to wait for new remote workspace information this.resetRemoteWorkspaceId() + IdleWorkspaceManager.setSessionAsIdle() this.isArtifactUploadedToRemoteWorkspace = false // Set up message queue consumer @@ -371,7 +378,9 @@ export class WorkspaceFolderManager { return resolve(false) } - const { metadata, optOut } = await this.listWorkspaceMetadata(this.workspaceIdentifier) + const { metadata, optOut, featureDisabled } = await this.listWorkspaceMetadata( + this.workspaceIdentifier + ) if (optOut) { this.logging.log(`User opted out during initial connection`) @@ -381,6 +390,13 @@ export class WorkspaceFolderManager { return resolve(false) } + if (featureDisabled) { + this.logging.log(`Feature disabled during initial connection`) + this.featureDisabled = true + this.clearAllWorkspaceResources() + return resolve(false) + } + if (!metadata) { // Continue polling by exiting only this iteration return @@ -437,7 +453,9 @@ export class WorkspaceFolderManager { } this.logging.log(`Checking remote workspace status for workspace [${this.workspaceIdentifier}]`) - const { metadata, optOut, error } = await this.listWorkspaceMetadata(this.workspaceIdentifier) + const { metadata, optOut, featureDisabled, error } = await this.listWorkspaceMetadata( + this.workspaceIdentifier + ) if (optOut) { this.logging.log('User opted out, clearing all resources and starting opt-out monitor') @@ -447,6 +465,13 @@ export class WorkspaceFolderManager { return } + if (featureDisabled) { + this.logging.log('Feature disabled, clearing all resources and stoping server-side indexing features') + this.featureDisabled = true + this.clearAllWorkspaceResources() + return + } + if (error) { // Do not do anything if we received an exception but not caused by optOut return @@ -528,7 +553,14 @@ export class WorkspaceFolderManager { if (this.optOutMonitorInterval === undefined) { const intervalId = setInterval(async () => { try { - const { optOut } = await this.listWorkspaceMetadata() + const { optOut, featureDisabled } = await this.listWorkspaceMetadata() + + if (featureDisabled) { + // Stop opt-out monitor when WCS feature is disabled from server-side + this.featureDisabled = true + clearInterval(intervalId) + this.optOutMonitorInterval = undefined + } if (!optOut) { this.isOptedOut = false @@ -735,10 +767,12 @@ export class WorkspaceFolderManager { private async listWorkspaceMetadata(workspaceRoot?: WorkspaceRoot): Promise<{ metadata: WorkspaceMetadata | undefined | null optOut: boolean + featureDisabled: boolean error: any }> { let metadata: WorkspaceMetadata | undefined | null let optOut = false + let featureDisabled = false let error: any try { const params = workspaceRoot ? { workspaceRoot } : {} @@ -754,8 +788,11 @@ export class WorkspaceFolderManager { this.logging.log(`User's administrator opted out server-side workspace context`) optOut = true } + if (isAwsError(e) && e.code === 'AccessDeniedException' && e.message.includes('Feature is not supported')) { + featureDisabled = true + } } - return { metadata, optOut, error } + return { metadata, optOut, featureDisabled, error } } private async createWorkspace(workspaceRoot: WorkspaceRoot): Promise<{ From b76cd85f1f09bcbffed3e368aaf15dd537ddb3f1 Mon Sep 17 00:00:00 2001 From: Will Lo <96078566+Will-ShaoHua@users.noreply.github.com> Date: Tue, 19 Aug 2025 13:00:37 -0700 Subject: [PATCH 28/74] chore: update STE userContext version metadata (#2142) --- .../agenticChat/qAgenticChatServer.ts | 2 +- .../src/language-server/chat/qChatServer.ts | 4 ++- .../inline-completion/codeWhispererServer.ts | 4 ++- .../workspaceContextServer.ts | 7 ++++- .../BaseAmazonQServiceManager.ts | 5 ++++ .../src/shared/telemetryUtils.test.ts | 30 +++++++++++-------- .../src/shared/telemetryUtils.ts | 13 +++++--- 7 files changed, 45 insertions(+), 20 deletions(-) diff --git a/server/aws-lsp-codewhisperer/src/language-server/agenticChat/qAgenticChatServer.ts b/server/aws-lsp-codewhisperer/src/language-server/agenticChat/qAgenticChatServer.ts index 205e5aff69..445ca78d85 100644 --- a/server/aws-lsp-codewhisperer/src/language-server/agenticChat/qAgenticChatServer.ts +++ b/server/aws-lsp-codewhisperer/src/language-server/agenticChat/qAgenticChatServer.ts @@ -110,7 +110,7 @@ export const QAgenticChatServer = ) ) - const userContext = makeUserContextObject(clientParams, runtime.platform, 'CHAT') + const userContext = makeUserContextObject(clientParams, runtime.platform, 'CHAT', amazonQServiceManager.serverInfo) telemetryService.updateUserContext(userContext) chatController = new AgenticChatController( diff --git a/server/aws-lsp-codewhisperer/src/language-server/chat/qChatServer.ts b/server/aws-lsp-codewhisperer/src/language-server/chat/qChatServer.ts index 2452426b1b..d110ea4e41 100644 --- a/server/aws-lsp-codewhisperer/src/language-server/chat/qChatServer.ts +++ b/server/aws-lsp-codewhisperer/src/language-server/chat/qChatServer.ts @@ -59,7 +59,9 @@ export const QChatServerFactory = 'TelemetryService initialized before LSP connection was initialized.' ) ) - telemetryService.updateUserContext(makeUserContextObject(clientParams, runtime.platform, 'CHAT')) + telemetryService.updateUserContext( + makeUserContextObject(clientParams, runtime.platform, 'CHAT', amazonQServiceManager.serverInfo) + ) chatController = new ChatController( chatSessionManagementService, diff --git a/server/aws-lsp-codewhisperer/src/language-server/inline-completion/codeWhispererServer.ts b/server/aws-lsp-codewhisperer/src/language-server/inline-completion/codeWhispererServer.ts index 988bdd418f..33113fa318 100644 --- a/server/aws-lsp-codewhisperer/src/language-server/inline-completion/codeWhispererServer.ts +++ b/server/aws-lsp-codewhisperer/src/language-server/inline-completion/codeWhispererServer.ts @@ -723,7 +723,9 @@ export const CodewhispererServerFactory = ?.inlineCompletionWithReferences?.inlineEditSupport ?? false telemetryService = new TelemetryService(amazonQServiceManager, credentialsProvider, telemetry, logging) - telemetryService.updateUserContext(makeUserContextObject(clientParams, runtime.platform, 'INLINE')) + telemetryService.updateUserContext( + makeUserContextObject(clientParams, runtime.platform, 'INLINE', amazonQServiceManager.serverInfo) + ) codePercentageTracker = new CodePercentageTracker(telemetryService) codeDiffTracker = new CodeDiffTracker( diff --git a/server/aws-lsp-codewhisperer/src/language-server/workspaceContext/workspaceContextServer.ts b/server/aws-lsp-codewhisperer/src/language-server/workspaceContext/workspaceContextServer.ts index 3d638e337a..92dc0823e3 100644 --- a/server/aws-lsp-codewhisperer/src/language-server/workspaceContext/workspaceContextServer.ts +++ b/server/aws-lsp-codewhisperer/src/language-server/workspaceContext/workspaceContextServer.ts @@ -188,7 +188,12 @@ export const WorkspaceContextServer = (): Server => features => { abTestingEnabled = true } else { const clientParams = safeGet(lsp.getClientInitializeParams()) - const userContext = makeUserContextObject(clientParams, runtime.platform, 'CodeWhisperer') ?? { + const userContext = makeUserContextObject( + clientParams, + runtime.platform, + 'CodeWhisperer', + amazonQServiceManager.serverInfo + ) ?? { ideCategory: 'VSCODE', operatingSystem: 'MAC', product: 'CodeWhisperer', diff --git a/server/aws-lsp-codewhisperer/src/shared/amazonQServiceManager/BaseAmazonQServiceManager.ts b/server/aws-lsp-codewhisperer/src/shared/amazonQServiceManager/BaseAmazonQServiceManager.ts index 9c241809a7..cc21cd8766 100644 --- a/server/aws-lsp-codewhisperer/src/shared/amazonQServiceManager/BaseAmazonQServiceManager.ts +++ b/server/aws-lsp-codewhisperer/src/shared/amazonQServiceManager/BaseAmazonQServiceManager.ts @@ -17,6 +17,7 @@ import { } from './configurationUtils' import { AmazonQServiceInitializationError } from './errors' import { StreamingClientServiceBase } from '../streamingClientService' +import { UserContext } from '../../client/token/codewhispererbearertokenclient' export interface QServiceManagerFeatures { lsp: Lsp @@ -86,6 +87,10 @@ export abstract class BaseAmazonQServiceManager< abstract getCodewhispererService(): C abstract getStreamingClient(): S + get serverInfo() { + return this.features.runtime.serverInfo + } + public getConfiguration(): Readonly { return this.configurationCache.getConfig() } diff --git a/server/aws-lsp-codewhisperer/src/shared/telemetryUtils.test.ts b/server/aws-lsp-codewhisperer/src/shared/telemetryUtils.test.ts index 565a9505a4..0cfa7fd768 100644 --- a/server/aws-lsp-codewhisperer/src/shared/telemetryUtils.test.ts +++ b/server/aws-lsp-codewhisperer/src/shared/telemetryUtils.test.ts @@ -1,6 +1,6 @@ import * as assert from 'assert' import * as sinon from 'sinon' -import { InitializeParams, Platform } from '@aws/language-server-runtimes/server-interface' +import { InitializeParams, Platform, ServerInfo } from '@aws/language-server-runtimes/server-interface' import { getUserAgent, makeUserContextObject } from './telemetryUtils' describe('getUserAgent', () => { @@ -115,6 +115,7 @@ describe('getUserAgent', () => { describe('makeUserContextObject', () => { let mockInitializeParams: InitializeParams + let mockServerInfo: ServerInfo // let osStub: sinon.SinonStubbedInstance<{ now: () => number }> beforeEach(() => { @@ -123,10 +124,10 @@ describe('makeUserContextObject', () => { aws: { clientInfo: { name: 'test-custom-client-name', - version: '1.2.3', + version: '1.0.0', extension: { name: 'AmazonQ-For-VSCode', - version: '2.2.2', + version: '2.0.0', }, clientId: 'test-client-id', }, @@ -138,6 +139,11 @@ describe('makeUserContextObject', () => { }, } as InitializeParams + mockServerInfo = { + name: 'foo', + version: '3.0.0', + } + sinon.stub(process, 'platform').value('win32') }) @@ -146,33 +152,33 @@ describe('makeUserContextObject', () => { }) it('should return a valid UserContext object', () => { - const result = makeUserContextObject(mockInitializeParams, 'win32', 'TestProduct') + const result = makeUserContextObject(mockInitializeParams, 'win32', 'TestProduct', mockServerInfo) assert(result) assert.ok('ideCategory' in result) assert.ok('operatingSystem' in result) assert.strictEqual(result.operatingSystem, 'WINDOWS') assert.strictEqual(result.product, 'TestProduct') assert.strictEqual(result.clientId, 'test-client-id') - assert.strictEqual(result.ideVersion, '1.2.3') + assert.strictEqual(result.ideVersion, 'ide=1.0.0;plugin=2.0.0;lsp=3.0.0') }) it('should prefer initializationOptions.aws version over clientInfo version', () => { - const result = makeUserContextObject(mockInitializeParams, 'linux', 'TestProduct') - assert.strictEqual(result?.ideVersion, '1.2.3') + const result = makeUserContextObject(mockInitializeParams, 'linux', 'TestProduct', mockServerInfo) + assert.strictEqual(result?.ideVersion, 'ide=1.0.0;plugin=2.0.0;lsp=3.0.0') }) it('should use clientInfo version if initializationOptions.aws version is not available', () => { // @ts-ignore mockInitializeParams.initializationOptions.aws.clientInfo.version = undefined - const result = makeUserContextObject(mockInitializeParams, 'linux', 'TestProduct') - assert.strictEqual(result?.ideVersion, '1.1.1') + const result = makeUserContextObject(mockInitializeParams, 'linux', 'TestProduct', mockServerInfo) + assert.strictEqual(result?.ideVersion, 'ide=1.1.1;plugin=2.0.0;lsp=3.0.0') }) it('should return undefined if ideCategory is not in IDE_CATEGORY_MAP', () => { // @ts-ignore mockInitializeParams.initializationOptions.aws.clientInfo.extension.name = 'Unknown IDE' - const result = makeUserContextObject(mockInitializeParams, 'linux', 'TestProduct') + const result = makeUserContextObject(mockInitializeParams, 'linux', 'TestProduct', mockServerInfo) assert.strictEqual(result, undefined) }) @@ -188,7 +194,7 @@ describe('makeUserContextObject', () => { // @ts-ignore mockInitializeParams.initializationOptions.aws.clientInfo.extension.name = clientName - const result = makeUserContextObject(mockInitializeParams, 'linux', 'TestProduct') + const result = makeUserContextObject(mockInitializeParams, 'linux', 'TestProduct', mockServerInfo) switch (clientName) { case 'AmazonQ-For-VSCode': assert.strictEqual(result?.ideCategory, 'VSCODE') @@ -222,7 +228,7 @@ describe('makeUserContextObject', () => { ] platforms.forEach(platform => { - const result = makeUserContextObject(mockInitializeParams, platform, 'TestProduct') + const result = makeUserContextObject(mockInitializeParams, platform, 'TestProduct', mockServerInfo) switch (platform) { case 'win32': assert.strictEqual(result?.operatingSystem, 'WINDOWS') diff --git a/server/aws-lsp-codewhisperer/src/shared/telemetryUtils.ts b/server/aws-lsp-codewhisperer/src/shared/telemetryUtils.ts index 73e94d4233..49020a8822 100644 --- a/server/aws-lsp-codewhisperer/src/shared/telemetryUtils.ts +++ b/server/aws-lsp-codewhisperer/src/shared/telemetryUtils.ts @@ -89,15 +89,20 @@ const getOperatingSystem = (platform: Platform) => { export const makeUserContextObject = ( initializeParams: InitializeParams, platform: Platform, - product: string + product: string, + serverInfo: ServerInfo ): UserContext | undefined => { + const ide = getIdeCategory(initializeParams) + const ideVersion = + initializeParams.initializationOptions?.aws?.clientInfo?.version || initializeParams.clientInfo?.version + const pluginVersion = initializeParams.initializationOptions?.aws?.clientInfo?.extension?.version || '' + const lspVersion = serverInfo.version ?? '' const userContext: UserContext = { - ideCategory: getIdeCategory(initializeParams), + ideCategory: ide, operatingSystem: getOperatingSystem(platform), product: product, clientId: initializeParams.initializationOptions?.aws?.clientInfo?.clientId, - ideVersion: - initializeParams.initializationOptions?.aws?.clientInfo?.version || initializeParams.clientInfo?.version, + ideVersion: `ide=${ideVersion};plugin=${pluginVersion};lsp=${lspVersion}`, } if (userContext.ideCategory === 'UNKNOWN' || userContext.operatingSystem === 'UNKNOWN') { From 04588dfc33f0d85dbd488814a474b5e354398df0 Mon Sep 17 00:00:00 2001 From: invictus <149003065+ashishrp-aws@users.noreply.github.com> Date: Tue, 19 Aug 2025 13:12:00 -0700 Subject: [PATCH 29/74] fix: fix to turn on and off MCP servers incase of error based on last state (#2143) Co-authored-by: Laxman Reddy <141967714+laileni-aws@users.noreply.github.com> --- .../agenticChat/tools/mcp/profileStatusMonitor.ts | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/server/aws-lsp-codewhisperer/src/language-server/agenticChat/tools/mcp/profileStatusMonitor.ts b/server/aws-lsp-codewhisperer/src/language-server/agenticChat/tools/mcp/profileStatusMonitor.ts index ad34550270..53a4da9090 100644 --- a/server/aws-lsp-codewhisperer/src/language-server/agenticChat/tools/mcp/profileStatusMonitor.ts +++ b/server/aws-lsp-codewhisperer/src/language-server/agenticChat/tools/mcp/profileStatusMonitor.ts @@ -118,7 +118,13 @@ export class ProfileStatusMonitor { return isMcpEnabled } catch (error) { this.logging.debug(`MCP configuration check failed, defaulting to enabled: ${error}`) - return ProfileStatusMonitor.getMcpState() + const mcpState = ProfileStatusMonitor.getMcpState() + if (!mcpState) { + this.onMcpDisabled() + } else if (this.onMcpEnabled) { + this.onMcpEnabled() + } + return mcpState } } From b8e52682ac2b2337e1d0a32759e8beccde889cee Mon Sep 17 00:00:00 2001 From: Will Lo <96078566+Will-ShaoHua@users.noreply.github.com> Date: Tue, 19 Aug 2025 14:57:14 -0700 Subject: [PATCH 30/74] fix: empty userTriggerDecision not being sent for NEP code path (#2140) * refactor: clean up old NEP code path which is no longer used * fix: empty userTriggerDecision not being sent for NEP code path * test: disable telemetry test temporarily * fix: patch * revert: a * revert: a --- .../editCompletionHandler.ts | 41 +++++-------------- .../inline-completion/telemetry.ts | 30 ++++++++++++++ 2 files changed, 41 insertions(+), 30 deletions(-) diff --git a/server/aws-lsp-codewhisperer/src/language-server/inline-completion/editCompletionHandler.ts b/server/aws-lsp-codewhisperer/src/language-server/inline-completion/editCompletionHandler.ts index 28e29ee271..005b5ab3f0 100644 --- a/server/aws-lsp-codewhisperer/src/language-server/inline-completion/editCompletionHandler.ts +++ b/server/aws-lsp-codewhisperer/src/language-server/inline-completion/editCompletionHandler.ts @@ -23,12 +23,12 @@ import { CodewhispererLanguage, getSupportedLanguageId } from '../../shared/lang import { WorkspaceFolderManager } from '../workspaceContext/workspaceFolderManager' import { shouldTriggerEdits } from './trigger' import { + emitEmptyUserTriggerDecisionTelemetry, emitServiceInvocationFailure, emitServiceInvocationTelemetry, emitUserTriggerDecisionTelemetry, } from './telemetry' import { TelemetryService } from '../../shared/telemetry/telemetryService' -import { mergeEditSuggestionsWithFileContext } from './mergeRightUtils' import { textUtils } from '@aws/lsp-core' import { AmazonQBaseServiceManager } from '../../shared/amazonQServiceManager/BaseAmazonQServiceManager' import { RejectedEditTracker } from './tracker/rejectedEditTracker' @@ -353,35 +353,16 @@ export class EditCompletionHandler { this.sessionManager.activateSession(session) // Process suggestions to apply Empty or Filter filters - const filteredSuggestions = suggestionResponse.suggestions - // Empty suggestion filter - .filter(suggestion => { - if (suggestion.content === '') { - session.setSuggestionState(suggestion.itemId, 'Empty') - return false - } - - return true - }) - // References setting filter - .filter(suggestion => { - // State to track whether code with references should be included in - // the response. No locking or concurrency controls, filtering is done - // right before returning and is only guaranteed to be consistent within - // the context of a single response. - const { includeSuggestionsWithCodeReferences } = this.amazonQServiceManager.getConfiguration() - if (includeSuggestionsWithCodeReferences) { - return true - } - - if (suggestion.references == null || suggestion.references.length === 0) { - return true - } - - // Filter out suggestions that have references when includeSuggestionsWithCodeReferences setting is true - session.setSuggestionState(suggestion.itemId, 'Filter') - return false - }) + if (suggestionResponse.suggestions.length === 0) { + this.sessionManager.closeSession(session) + await emitEmptyUserTriggerDecisionTelemetry( + this.telemetryService, + session, + this.documentChangedListener.timeSinceLastUserModification, + this.editsEnabled ? this.sessionManager.getAndUpdateStreakLength(false) : 0 + ) + return EMPTY_RESULT + } return { items: suggestionResponse.suggestions diff --git a/server/aws-lsp-codewhisperer/src/language-server/inline-completion/telemetry.ts b/server/aws-lsp-codewhisperer/src/language-server/inline-completion/telemetry.ts index d53d141a2b..73e3a526a4 100644 --- a/server/aws-lsp-codewhisperer/src/language-server/inline-completion/telemetry.ts +++ b/server/aws-lsp-codewhisperer/src/language-server/inline-completion/telemetry.ts @@ -106,6 +106,36 @@ export const emitPerceivedLatencyTelemetry = (telemetry: Telemetry, session: Cod }) } +export async function emitEmptyUserTriggerDecisionTelemetry( + telemetryService: TelemetryService, + session: CodeWhispererSession, + timeSinceLastUserModification?: number, + streakLength?: number +) { + // Prevent reporting user decision if it was already sent + if (session.reportedUserDecision) { + return + } + + // Non-blocking + emitAggregatedUserTriggerDecisionTelemetry( + telemetryService, + session, + 'Empty', + timeSinceLastUserModification, + 0, + 0, + [], + [], + streakLength + ) + .then() + .catch(e => {}) + .finally(() => { + session.reportedUserDecision = true + }) +} + export const emitUserTriggerDecisionTelemetry = async ( telemetry: Telemetry, telemetryService: TelemetryService, From c5468cbe6a594d16b7619a6ed37676f9f1045c19 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Wed, 20 Aug 2025 13:03:46 -0700 Subject: [PATCH 31/74] chore(release): release packages from branch main (#2117) --- .release-please-manifest.json | 14 ++++----- chat-client/CHANGELOG.md | 14 +++++++++ chat-client/package.json | 2 +- core/aws-lsp-core/CHANGELOG.md | 12 ++++++++ core/aws-lsp-core/package.json | 2 +- package-lock.json | 22 +++++++------- server/aws-lsp-antlr4/CHANGELOG.md | 14 +++++++++ server/aws-lsp-antlr4/package.json | 4 +-- server/aws-lsp-codewhisperer/CHANGELOG.md | 35 +++++++++++++++++++++++ server/aws-lsp-codewhisperer/package.json | 4 +-- server/aws-lsp-json/CHANGELOG.md | 14 +++++++++ server/aws-lsp-json/package.json | 4 +-- server/aws-lsp-partiql/CHANGELOG.md | 7 +++++ server/aws-lsp-partiql/package.json | 2 +- server/aws-lsp-yaml/CHANGELOG.md | 14 +++++++++ server/aws-lsp-yaml/package.json | 4 +-- 16 files changed, 139 insertions(+), 29 deletions(-) diff --git a/.release-please-manifest.json b/.release-please-manifest.json index e6abdf7be2..159795a30e 100644 --- a/.release-please-manifest.json +++ b/.release-please-manifest.json @@ -1,9 +1,9 @@ { - "chat-client": "0.1.32", - "core/aws-lsp-core": "0.0.13", - "server/aws-lsp-antlr4": "0.1.17", - "server/aws-lsp-codewhisperer": "0.0.73", - "server/aws-lsp-json": "0.1.17", - "server/aws-lsp-partiql": "0.0.16", - "server/aws-lsp-yaml": "0.1.17" + "chat-client": "0.1.33", + "core/aws-lsp-core": "0.0.14", + "server/aws-lsp-antlr4": "0.1.18", + "server/aws-lsp-codewhisperer": "0.0.74", + "server/aws-lsp-json": "0.1.18", + "server/aws-lsp-partiql": "0.0.17", + "server/aws-lsp-yaml": "0.1.18" } diff --git a/chat-client/CHANGELOG.md b/chat-client/CHANGELOG.md index 1cd9acfbe0..995ddc8ae6 100644 --- a/chat-client/CHANGELOG.md +++ b/chat-client/CHANGELOG.md @@ -1,5 +1,19 @@ # Changelog +## [0.1.33](https://github.com/aws/language-servers/compare/chat-client/v0.1.32...chat-client/v0.1.33) (2025-08-19) + + +### Features + +* **amazonq:** added mcp admin level configuration with GetProfile ([#2000](https://github.com/aws/language-servers/issues/2000)) ([fd6e9a8](https://github.com/aws/language-servers/commit/fd6e9a829c6229c276de5340dffce52b426a864d)) +* **amazonq:** read tool ui revamp ([#2113](https://github.com/aws/language-servers/issues/2113)) ([#2121](https://github.com/aws/language-servers/issues/2121)) ([93cf229](https://github.com/aws/language-servers/commit/93cf229149ba60491f9f5763793db4a9f570b611)) + + +### Bug Fixes + +* fix for button text and remove profilearn caching ([#2137](https://github.com/aws/language-servers/issues/2137)) ([2a4171a](https://github.com/aws/language-servers/commit/2a4171a74c15c23c23c481060496162bcc9e6284)) +* Use file context override in the inline completion params for Jupyter Notebook ([#2114](https://github.com/aws/language-servers/issues/2114)) ([91c8398](https://github.com/aws/language-servers/commit/91c839857f8aa4d79098189f9fb620b361c51289)) + ## [0.1.32](https://github.com/aws/language-servers/compare/chat-client/v0.1.31...chat-client/v0.1.32) (2025-08-11) diff --git a/chat-client/package.json b/chat-client/package.json index 65dee12960..1af265fdfe 100644 --- a/chat-client/package.json +++ b/chat-client/package.json @@ -1,6 +1,6 @@ { "name": "@aws/chat-client", - "version": "0.1.32", + "version": "0.1.33", "description": "AWS Chat Client", "main": "out/index.js", "repository": { diff --git a/core/aws-lsp-core/CHANGELOG.md b/core/aws-lsp-core/CHANGELOG.md index 9e2e616f55..0e1e63dd38 100644 --- a/core/aws-lsp-core/CHANGELOG.md +++ b/core/aws-lsp-core/CHANGELOG.md @@ -1,5 +1,17 @@ # Changelog +## [0.0.14](https://github.com/aws/language-servers/compare/lsp-core/v0.0.13...lsp-core/v0.0.14) (2025-08-19) + + +### Features + +* **amazonq:** added mcp admin level configuration with GetProfile ([#2000](https://github.com/aws/language-servers/issues/2000)) ([fd6e9a8](https://github.com/aws/language-servers/commit/fd6e9a829c6229c276de5340dffce52b426a864d)) + + +### Bug Fixes + +* Use file context override in the inline completion params for Jupyter Notebook ([#2114](https://github.com/aws/language-servers/issues/2114)) ([91c8398](https://github.com/aws/language-servers/commit/91c839857f8aa4d79098189f9fb620b361c51289)) + ## [0.0.13](https://github.com/aws/language-servers/compare/lsp-core/v0.0.12...lsp-core/v0.0.13) (2025-08-04) diff --git a/core/aws-lsp-core/package.json b/core/aws-lsp-core/package.json index 04081f13fc..4ecdd4e238 100644 --- a/core/aws-lsp-core/package.json +++ b/core/aws-lsp-core/package.json @@ -1,6 +1,6 @@ { "name": "@aws/lsp-core", - "version": "0.0.13", + "version": "0.0.14", "description": "Core library, contains common code and utilities", "main": "out/index.js", "repository": { diff --git a/package-lock.json b/package-lock.json index 128ae8083d..97b89d43b8 100644 --- a/package-lock.json +++ b/package-lock.json @@ -251,7 +251,7 @@ }, "chat-client": { "name": "@aws/chat-client", - "version": "0.1.32", + "version": "0.1.33", "license": "Apache-2.0", "dependencies": { "@aws/chat-client-ui-types": "^0.1.56", @@ -293,7 +293,7 @@ }, "core/aws-lsp-core": { "name": "@aws/lsp-core", - "version": "0.0.13", + "version": "0.0.14", "license": "Apache-2.0", "dependencies": { "@aws/language-server-runtimes": "^0.2.126", @@ -28605,11 +28605,11 @@ }, "server/aws-lsp-antlr4": { "name": "@aws/lsp-antlr4", - "version": "0.1.17", + "version": "0.1.18", "license": "Apache-2.0", "dependencies": { "@aws/language-server-runtimes": "^0.2.126", - "@aws/lsp-core": "^0.0.13" + "@aws/lsp-core": "^0.0.14" }, "devDependencies": { "@babel/plugin-transform-modules-commonjs": "^7.24.1", @@ -28670,7 +28670,7 @@ }, "server/aws-lsp-codewhisperer": { "name": "@aws/lsp-codewhisperer", - "version": "0.0.73", + "version": "0.0.74", "bundleDependencies": [ "@amzn/codewhisperer-streaming", "@amzn/amazon-q-developer-streaming-client" @@ -28684,7 +28684,7 @@ "@aws-sdk/util-retry": "^3.374.0", "@aws/chat-client-ui-types": "^0.1.56", "@aws/language-server-runtimes": "^0.2.126", - "@aws/lsp-core": "^0.0.13", + "@aws/lsp-core": "^0.0.14", "@modelcontextprotocol/sdk": "^1.15.0", "@smithy/node-http-handler": "^2.5.0", "adm-zip": "^0.5.10", @@ -28885,11 +28885,11 @@ }, "server/aws-lsp-json": { "name": "@aws/lsp-json", - "version": "0.1.17", + "version": "0.1.18", "license": "Apache-2.0", "dependencies": { "@aws/language-server-runtimes": "^0.2.126", - "@aws/lsp-core": "^0.0.13", + "@aws/lsp-core": "^0.0.14", "vscode-languageserver": "^9.0.1", "vscode-languageserver-textdocument": "^1.0.8" }, @@ -28963,7 +28963,7 @@ }, "server/aws-lsp-partiql": { "name": "@aws/lsp-partiql", - "version": "0.0.16", + "version": "0.0.17", "license": "Apache-2.0", "dependencies": { "@aws/language-server-runtimes": "^0.2.126", @@ -29015,12 +29015,12 @@ }, "server/aws-lsp-yaml": { "name": "@aws/lsp-yaml", - "version": "0.1.17", + "version": "0.1.18", "hasInstallScript": true, "license": "Apache-2.0", "dependencies": { "@aws/language-server-runtimes": "^0.2.126", - "@aws/lsp-core": "^0.0.13", + "@aws/lsp-core": "^0.0.14", "vscode-languageserver": "^9.0.1", "vscode-languageserver-textdocument": "^1.0.8", "yaml-language-server": "1.13.0" diff --git a/server/aws-lsp-antlr4/CHANGELOG.md b/server/aws-lsp-antlr4/CHANGELOG.md index bde43f2c3c..e1458575e1 100644 --- a/server/aws-lsp-antlr4/CHANGELOG.md +++ b/server/aws-lsp-antlr4/CHANGELOG.md @@ -1,5 +1,19 @@ # Changelog +## [0.1.18](https://github.com/aws/language-servers/compare/lsp-antlr4/v0.1.17...lsp-antlr4/v0.1.18) (2025-08-19) + + +### Bug Fixes + +* Use file context override in the inline completion params for Jupyter Notebook ([#2114](https://github.com/aws/language-servers/issues/2114)) ([91c8398](https://github.com/aws/language-servers/commit/91c839857f8aa4d79098189f9fb620b361c51289)) + + +### Dependencies + +* The following workspace dependencies were updated + * dependencies + * @aws/lsp-core bumped from ^0.0.13 to ^0.0.14 + ## [0.1.17](https://github.com/aws/language-servers/compare/lsp-antlr4/v0.1.16...lsp-antlr4/v0.1.17) (2025-08-04) diff --git a/server/aws-lsp-antlr4/package.json b/server/aws-lsp-antlr4/package.json index 9ba8186aab..b61239d633 100644 --- a/server/aws-lsp-antlr4/package.json +++ b/server/aws-lsp-antlr4/package.json @@ -1,6 +1,6 @@ { "name": "@aws/lsp-antlr4", - "version": "0.1.17", + "version": "0.1.18", "description": "ANTLR4 language server", "main": "out/index.js", "repository": { @@ -29,7 +29,7 @@ }, "dependencies": { "@aws/language-server-runtimes": "^0.2.126", - "@aws/lsp-core": "^0.0.13" + "@aws/lsp-core": "^0.0.14" }, "peerDependencies": { "antlr4-c3": ">=3.4 < 4", diff --git a/server/aws-lsp-codewhisperer/CHANGELOG.md b/server/aws-lsp-codewhisperer/CHANGELOG.md index 1b2f1011df..048eaee59c 100644 --- a/server/aws-lsp-codewhisperer/CHANGELOG.md +++ b/server/aws-lsp-codewhisperer/CHANGELOG.md @@ -1,5 +1,40 @@ # Changelog +## [0.0.74](https://github.com/aws/language-servers/compare/lsp-codewhisperer/v0.0.73...lsp-codewhisperer/v0.0.74) (2025-08-19) + + +### Features + +* **amazonq:** added mcp admin level configuration with GetProfile ([#2000](https://github.com/aws/language-servers/issues/2000)) ([fd6e9a8](https://github.com/aws/language-servers/commit/fd6e9a829c6229c276de5340dffce52b426a864d)) +* **amazonq:** read tool ui revamp ([#2113](https://github.com/aws/language-servers/issues/2113)) ([#2121](https://github.com/aws/language-servers/issues/2121)) ([93cf229](https://github.com/aws/language-servers/commit/93cf229149ba60491f9f5763793db4a9f570b611)) +* remove project type validation from LSP layer ([#2103](https://github.com/aws/language-servers/issues/2103)) ([d397161](https://github.com/aws/language-servers/commit/d397161cc3448c63016e27f5ac2a1917cdaae1cb)) + + +### Bug Fixes + +* **amazonq:** add server side control for WCS features ([#2128](https://github.com/aws/language-servers/issues/2128)) ([5e4435d](https://github.com/aws/language-servers/commit/5e4435dfaea7bf8c00e6a27b9bb0d40f699d4e01)) +* **amazonq:** fix regression of mcp config in agent config ([#2101](https://github.com/aws/language-servers/issues/2101)) ([e4e8bbb](https://github.com/aws/language-servers/commit/e4e8bbb89e4b597926582bead2b14ffc43f2a7f8)) +* **amazonq:** handle case where multiple rules are provided with the same name ([#2118](https://github.com/aws/language-servers/issues/2118)) ([0e23e2d](https://github.com/aws/language-servers/commit/0e23e2d29b8cad14403d372b9bbb08ca8ffa7ac7)) +* **amazonq:** persist mcp configs in agent json on start-up ([#2112](https://github.com/aws/language-servers/issues/2112)) ([817cfe2](https://github.com/aws/language-servers/commit/817cfe2656cb1deec6111c699c4ba46b4ba53e00)) +* empty userTriggerDecision not being sent for NEP code path ([#2140](https://github.com/aws/language-servers/issues/2140)) ([b8e5268](https://github.com/aws/language-servers/commit/b8e52682ac2b2337e1d0a32759e8beccde889cee)) +* fix for button text and remove profilearn caching ([#2137](https://github.com/aws/language-servers/issues/2137)) ([2a4171a](https://github.com/aws/language-servers/commit/2a4171a74c15c23c23c481060496162bcc9e6284)) +* fix to add disk caching for mcp admin state ([#2139](https://github.com/aws/language-servers/issues/2139)) ([f947e1a](https://github.com/aws/language-servers/commit/f947e1a9da4431d6089b22825f992010c30a470b)) +* fix to turn on and off MCP servers incase of error based on last state ([#2143](https://github.com/aws/language-servers/issues/2143)) ([04588df](https://github.com/aws/language-servers/commit/04588dfc33f0d85dbd488814a474b5e354398df0)) +* proper path handling for additional context ([#2129](https://github.com/aws/language-servers/issues/2129)) ([971eaa5](https://github.com/aws/language-servers/commit/971eaa505d948e9d2090c85f9b965f554ea7f2c8)) +* Use file context override in the inline completion params for Jupyter Notebook ([#2114](https://github.com/aws/language-servers/issues/2114)) ([91c8398](https://github.com/aws/language-servers/commit/91c839857f8aa4d79098189f9fb620b361c51289)) + + +### Performance Improvements + +* remove edit completion retry mechanism on document change ([#2124](https://github.com/aws/language-servers/issues/2124)) ([963b6e9](https://github.com/aws/language-servers/commit/963b6e9b7887da23a85a826c55a6ed95ff36d956)) + + +### Dependencies + +* The following workspace dependencies were updated + * dependencies + * @aws/lsp-core bumped from ^0.0.13 to ^0.0.14 + ## [0.0.73](https://github.com/aws/language-servers/compare/lsp-codewhisperer/v0.0.72...lsp-codewhisperer/v0.0.73) (2025-08-11) diff --git a/server/aws-lsp-codewhisperer/package.json b/server/aws-lsp-codewhisperer/package.json index 78297c702e..c4264e5988 100644 --- a/server/aws-lsp-codewhisperer/package.json +++ b/server/aws-lsp-codewhisperer/package.json @@ -1,6 +1,6 @@ { "name": "@aws/lsp-codewhisperer", - "version": "0.0.73", + "version": "0.0.74", "description": "CodeWhisperer Language Server", "main": "out/index.js", "repository": { @@ -37,7 +37,7 @@ "@aws-sdk/util-retry": "^3.374.0", "@aws/chat-client-ui-types": "^0.1.56", "@aws/language-server-runtimes": "^0.2.126", - "@aws/lsp-core": "^0.0.13", + "@aws/lsp-core": "^0.0.14", "@modelcontextprotocol/sdk": "^1.15.0", "@smithy/node-http-handler": "^2.5.0", "adm-zip": "^0.5.10", diff --git a/server/aws-lsp-json/CHANGELOG.md b/server/aws-lsp-json/CHANGELOG.md index 31c977b978..ecac136295 100644 --- a/server/aws-lsp-json/CHANGELOG.md +++ b/server/aws-lsp-json/CHANGELOG.md @@ -1,5 +1,19 @@ # Changelog +## [0.1.18](https://github.com/aws/language-servers/compare/lsp-json/v0.1.17...lsp-json/v0.1.18) (2025-08-19) + + +### Bug Fixes + +* Use file context override in the inline completion params for Jupyter Notebook ([#2114](https://github.com/aws/language-servers/issues/2114)) ([91c8398](https://github.com/aws/language-servers/commit/91c839857f8aa4d79098189f9fb620b361c51289)) + + +### Dependencies + +* The following workspace dependencies were updated + * dependencies + * @aws/lsp-core bumped from ^0.0.13 to ^0.0.14 + ## [0.1.17](https://github.com/aws/language-servers/compare/lsp-json/v0.1.16...lsp-json/v0.1.17) (2025-08-04) diff --git a/server/aws-lsp-json/package.json b/server/aws-lsp-json/package.json index 66604360fa..d129734444 100644 --- a/server/aws-lsp-json/package.json +++ b/server/aws-lsp-json/package.json @@ -1,6 +1,6 @@ { "name": "@aws/lsp-json", - "version": "0.1.17", + "version": "0.1.18", "description": "JSON Language Server", "main": "out/index.js", "repository": { @@ -27,7 +27,7 @@ }, "dependencies": { "@aws/language-server-runtimes": "^0.2.126", - "@aws/lsp-core": "^0.0.13", + "@aws/lsp-core": "^0.0.14", "vscode-languageserver": "^9.0.1", "vscode-languageserver-textdocument": "^1.0.8" }, diff --git a/server/aws-lsp-partiql/CHANGELOG.md b/server/aws-lsp-partiql/CHANGELOG.md index b1be4c4050..dc68bdb063 100644 --- a/server/aws-lsp-partiql/CHANGELOG.md +++ b/server/aws-lsp-partiql/CHANGELOG.md @@ -1,5 +1,12 @@ # Changelog +## [0.0.17](https://github.com/aws/language-servers/compare/lsp-partiql/v0.0.16...lsp-partiql/v0.0.17) (2025-08-19) + + +### Bug Fixes + +* Use file context override in the inline completion params for Jupyter Notebook ([#2114](https://github.com/aws/language-servers/issues/2114)) ([91c8398](https://github.com/aws/language-servers/commit/91c839857f8aa4d79098189f9fb620b361c51289)) + ## [0.0.16](https://github.com/aws/language-servers/compare/lsp-partiql/v0.0.15...lsp-partiql/v0.0.16) (2025-08-04) diff --git a/server/aws-lsp-partiql/package.json b/server/aws-lsp-partiql/package.json index efc073c7e9..2a5a5bb73f 100644 --- a/server/aws-lsp-partiql/package.json +++ b/server/aws-lsp-partiql/package.json @@ -3,7 +3,7 @@ "author": "Amazon Web Services", "license": "Apache-2.0", "description": "PartiQL language server", - "version": "0.0.16", + "version": "0.0.17", "repository": { "type": "git", "url": "https://github.com/aws/language-servers" diff --git a/server/aws-lsp-yaml/CHANGELOG.md b/server/aws-lsp-yaml/CHANGELOG.md index 965da41cfc..5858e6eb66 100644 --- a/server/aws-lsp-yaml/CHANGELOG.md +++ b/server/aws-lsp-yaml/CHANGELOG.md @@ -1,5 +1,19 @@ # Changelog +## [0.1.18](https://github.com/aws/language-servers/compare/lsp-yaml/v0.1.17...lsp-yaml/v0.1.18) (2025-08-19) + + +### Bug Fixes + +* Use file context override in the inline completion params for Jupyter Notebook ([#2114](https://github.com/aws/language-servers/issues/2114)) ([91c8398](https://github.com/aws/language-servers/commit/91c839857f8aa4d79098189f9fb620b361c51289)) + + +### Dependencies + +* The following workspace dependencies were updated + * dependencies + * @aws/lsp-core bumped from ^0.0.13 to ^0.0.14 + ## [0.1.17](https://github.com/aws/language-servers/compare/lsp-yaml/v0.1.16...lsp-yaml/v0.1.17) (2025-08-04) diff --git a/server/aws-lsp-yaml/package.json b/server/aws-lsp-yaml/package.json index e18a72ea29..7d2b942ab5 100644 --- a/server/aws-lsp-yaml/package.json +++ b/server/aws-lsp-yaml/package.json @@ -1,6 +1,6 @@ { "name": "@aws/lsp-yaml", - "version": "0.1.17", + "version": "0.1.18", "description": "YAML Language Server", "main": "out/index.js", "repository": { @@ -27,7 +27,7 @@ }, "dependencies": { "@aws/language-server-runtimes": "^0.2.126", - "@aws/lsp-core": "^0.0.13", + "@aws/lsp-core": "^0.0.14", "vscode-languageserver": "^9.0.1", "vscode-languageserver-textdocument": "^1.0.8", "yaml-language-server": "1.13.0" From 0767e074c91682a91d2fe7a6b2a7369c4dea280c Mon Sep 17 00:00:00 2001 From: andrewyuq <89420755+andrewyuq@users.noreply.github.com> Date: Thu, 21 Aug 2025 11:48:21 -0700 Subject: [PATCH 32/74] fix(amazonq): don't let flare send discard for the still valid suggestion in JB (#2145) * fix(amazonq): don't let flare send discard for the still valid suggestion in JB if a valid sessionId is returned back to JB, JB will eventually send a decision for it, but when user types a character to reject the current suggestion, on JB side it will 1) send reject for the current one 2) send a new trigger for the latest context. 2) will happen before 1) which will discard the current active session on flare's end. We don't want flare to do that for JB. * fix(amazonq): createSession doesn't need to close the previous session it should be handled together with telemetry reporting * fix(amazonq): fix test * fix(amazonq): test fix attempt * fix(amazonq): test fix attempt --- .../codeWhispererServer.test.ts | 35 +-------------- .../inline-completion/codeWhispererServer.ts | 44 ++++++++----------- .../editCompletionHandler.ts | 1 + .../session/sessionManager.test.ts | 28 +++--------- .../session/sessionManager.ts | 8 ---- .../userTriggerDecision.test.ts | 6 +-- 6 files changed, 29 insertions(+), 93 deletions(-) diff --git a/server/aws-lsp-codewhisperer/src/language-server/inline-completion/codeWhispererServer.test.ts b/server/aws-lsp-codewhisperer/src/language-server/inline-completion/codeWhispererServer.test.ts index 8151602dd4..e15400434c 100644 --- a/server/aws-lsp-codewhisperer/src/language-server/inline-completion/codeWhispererServer.test.ts +++ b/server/aws-lsp-codewhisperer/src/language-server/inline-completion/codeWhispererServer.test.ts @@ -1503,7 +1503,7 @@ describe('CodeWhisperer Server', () => { manager.activateSession(session) const session2 = manager.createSession(sessionData) manager.activateSession(session2) - assert.equal(session.state, 'CLOSED') + assert.equal(session.state, 'ACTIVE') assert.equal(session2.state, 'ACTIVE') await features.doLogInlineCompletionSessionResults(sessionResultData) @@ -2300,39 +2300,6 @@ describe('CodeWhisperer Server', () => { sinon.assert.calledOnceWithExactly(sessionManagerSpy.closeSession, currentSession) }) - it('Manual completion invocation should close previous session', async () => { - const TRIGGER_KIND = InlineCompletionTriggerKind.Invoked - - const result = await features.doInlineCompletionWithReferences( - { - textDocument: { uri: SOME_FILE.uri }, - position: { line: 0, character: 0 }, - // Manual trigger kind - context: { triggerKind: TRIGGER_KIND }, - }, - CancellationToken.None - ) - - assert.deepEqual(result, EXPECTED_RESULT) - const firstSession = sessionManager.getActiveSession() - - // There is ACTIVE session - assert(firstSession) - assert.equal(sessionManager.getCurrentSession(), firstSession) - assert.equal(firstSession.state, 'ACTIVE') - - const secondResult = await features.doInlineCompletionWithReferences( - { - textDocument: { uri: SOME_FILE.uri }, - position: { line: 0, character: 0 }, - context: { triggerKind: TRIGGER_KIND }, - }, - CancellationToken.None - ) - assert.deepEqual(secondResult, { ...EXPECTED_RESULT, sessionId: SESSION_IDS_LOG[1] }) - sinon.assert.called(sessionManagerSpy.closeCurrentSession) - }) - it('should discard inflight session if merge right recommendations resulted in list of empty strings', async () => { // The suggestion returned by generateSuggestions will be equal to the contents of the file // This test fails when the file starts with a new line, probably due to the way we handle right context merge diff --git a/server/aws-lsp-codewhisperer/src/language-server/inline-completion/codeWhispererServer.ts b/server/aws-lsp-codewhisperer/src/language-server/inline-completion/codeWhispererServer.ts index 33113fa318..9f2073d85e 100644 --- a/server/aws-lsp-codewhisperer/src/language-server/inline-completion/codeWhispererServer.ts +++ b/server/aws-lsp-codewhisperer/src/language-server/inline-completion/codeWhispererServer.ts @@ -311,34 +311,27 @@ export const CodewhispererServerFactory = // Close ACTIVE session and record Discard trigger decision immediately if (currentSession && currentSession.state === 'ACTIVE') { - if (editsEnabled && currentSession.suggestionType === SuggestionType.EDIT) { - const mergedSuggestions = mergeEditSuggestionsWithFileContext( + // Emit user trigger decision at session close time for active session + // TODO: yuxqiang workaround to exclude JB from this logic because JB and VSC handle a + // bit differently in the case when there's a new trigger while a reject/discard event is sent + // for the previous trigger + if (ideCategory !== 'JETBRAINS') { + completionSessionManager.discardSession(currentSession) + const streakLength = editsEnabled + ? completionSessionManager.getAndUpdateStreakLength(false) + : 0 + await emitUserTriggerDecisionTelemetry( + telemetry, + telemetryService, currentSession, - textDocument, - fileContext + timeSinceLastUserModification, + 0, + 0, + [], + [], + streakLength ) - - if (mergedSuggestions.length > 0) { - return { - items: mergedSuggestions, - sessionId: currentSession.id, - } - } } - // Emit user trigger decision at session close time for active session - completionSessionManager.discardSession(currentSession) - const streakLength = editsEnabled ? completionSessionManager.getAndUpdateStreakLength(false) : 0 - await emitUserTriggerDecisionTelemetry( - telemetry, - telemetryService, - currentSession, - timeSinceLastUserModification, - 0, - 0, - [], - [], - streakLength - ) } const supplementalMetadata = supplementalContext?.supContextData @@ -529,6 +522,7 @@ export const CodewhispererServerFactory = logging.log('Recommendation failure: ' + error) emitServiceInvocationFailure(telemetry, session, error) + // UTDE telemetry is not needed here because in error cases we don't care about UTDE for errored out sessions completionSessionManager.closeSession(session) let translatedError = error diff --git a/server/aws-lsp-codewhisperer/src/language-server/inline-completion/editCompletionHandler.ts b/server/aws-lsp-codewhisperer/src/language-server/inline-completion/editCompletionHandler.ts index 005b5ab3f0..99737d027c 100644 --- a/server/aws-lsp-codewhisperer/src/language-server/inline-completion/editCompletionHandler.ts +++ b/server/aws-lsp-codewhisperer/src/language-server/inline-completion/editCompletionHandler.ts @@ -403,6 +403,7 @@ export class EditCompletionHandler { this.logging.log('Recommendation failure: ' + error) emitServiceInvocationFailure(this.telemetry, session, error) + // UTDE telemetry is not needed here because in error cases we don't care about UTDE for errored out sessions this.sessionManager.closeSession(session) let translatedError = error diff --git a/server/aws-lsp-codewhisperer/src/language-server/inline-completion/session/sessionManager.test.ts b/server/aws-lsp-codewhisperer/src/language-server/inline-completion/session/sessionManager.test.ts index db34f885ff..bef684eb9b 100644 --- a/server/aws-lsp-codewhisperer/src/language-server/inline-completion/session/sessionManager.test.ts +++ b/server/aws-lsp-codewhisperer/src/language-server/inline-completion/session/sessionManager.test.ts @@ -529,12 +529,12 @@ describe('SessionManager', function () { assert.strictEqual(manager.getCurrentSession()?.state, 'REQUESTING') }) - it('should deactivate previous session when creating a new session', function () { + it('should not deactivate previous session when creating a new session', function () { const manager = SessionManager.getInstance() const session = manager.createSession(data) session.activate() manager.createSession(data) - assert.strictEqual(session.state, 'CLOSED') + assert.strictEqual(session.state, 'ACTIVE') }) it('should set previous active session trigger decision from discarded REQUESTING session', function () { @@ -548,7 +548,7 @@ describe('SessionManager', function () { assert.strictEqual(session2.previousTriggerDecision, 'Discard') }) - it('should set previous active session trigger decision to new session object', function () { + it('should not set previous active session trigger decision to new session object if it is not closed', function () { const manager = SessionManager.getInstance() const session1 = manager.createSession(data) assert.strictEqual(session1?.state, 'REQUESTING') @@ -557,22 +557,8 @@ describe('SessionManager', function () { const session2 = manager.createSession(data) - assert.strictEqual(session1?.state, 'CLOSED') - assert.strictEqual(session2.previousTriggerDecision, 'Empty') - }) - }) - - describe('closeCurrentSession()', function () { - it('should add the current session to the sessions log if it is active', function () { - const manager = SessionManager.getInstance() - const session = manager.createSession(data) - assert.strictEqual(session.state, 'REQUESTING') - session.activate() - assert.strictEqual(session.state, 'ACTIVE') - manager.closeCurrentSession() - assert.strictEqual(manager.getSessionsLog().length, 1) - assert.strictEqual(manager.getSessionsLog()[0], session) - assert.strictEqual(session.state, 'CLOSED') + assert.strictEqual(session1?.state, 'ACTIVE') + assert.strictEqual(session2.previousTriggerDecision, undefined) }) }) @@ -599,7 +585,6 @@ describe('SessionManager', function () { session2.activate() const session3 = manager.createSession(data) session3.activate() - manager.closeCurrentSession() const result = manager.getPreviousSession() assert.strictEqual(result, session3) assert.strictEqual(manager.getSessionsLog().length, 3) @@ -612,7 +597,6 @@ describe('SessionManager', function () { const session2 = manager.createSession(data) const session3 = manager.createSession(data) session3.activate() - manager.closeCurrentSession() const result = manager.getPreviousSession() assert.strictEqual(result, session3) assert.strictEqual(manager.getSessionsLog().length, 3) @@ -632,7 +616,6 @@ describe('SessionManager', function () { session.activate() const session2 = manager.createSession({ ...data, triggerType: 'AutoTrigger' }) session2.activate() - manager.closeCurrentSession() assert.strictEqual(manager.getSessionsLog().length, 2) const sessionId = session.id @@ -644,7 +627,6 @@ describe('SessionManager', function () { const manager = SessionManager.getInstance() const session = manager.createSession(data) session.activate() - manager.closeCurrentSession() assert.strictEqual(manager.getSessionsLog().length, 1) const sessionId = session.id + '1' diff --git a/server/aws-lsp-codewhisperer/src/language-server/inline-completion/session/sessionManager.ts b/server/aws-lsp-codewhisperer/src/language-server/inline-completion/session/sessionManager.ts index a0ddf742f6..235c464234 100644 --- a/server/aws-lsp-codewhisperer/src/language-server/inline-completion/session/sessionManager.ts +++ b/server/aws-lsp-codewhisperer/src/language-server/inline-completion/session/sessionManager.ts @@ -305,8 +305,6 @@ export class SessionManager { } public createSession(data: SessionData): CodeWhispererSession { - this.closeCurrentSession() - // Remove oldest session from log if (this.sessionsLog.length > this.maxHistorySize) { this.sessionsLog.shift() @@ -327,12 +325,6 @@ export class SessionManager { return session } - closeCurrentSession() { - if (this.currentSession) { - this.closeSession(this.currentSession) - } - } - closeSession(session: CodeWhispererSession) { session.close() } diff --git a/server/aws-lsp-codewhisperer/src/language-server/inline-completion/userTriggerDecision.test.ts b/server/aws-lsp-codewhisperer/src/language-server/inline-completion/userTriggerDecision.test.ts index b554b5ef23..c583815b6b 100644 --- a/server/aws-lsp-codewhisperer/src/language-server/inline-completion/userTriggerDecision.test.ts +++ b/server/aws-lsp-codewhisperer/src/language-server/inline-completion/userTriggerDecision.test.ts @@ -505,7 +505,7 @@ describe('Telemetry', () => { sinon.assert.called(telemetryServiceSpy) }) - it('should not emit User Decision event when session results are received after session was closed', async () => { + it('should not emit User Decision event after second trigger is received', async () => { setServiceResponse(DEFAULT_SUGGESTIONS, { ...EXPECTED_RESPONSE_CONTEXT, codewhispererSessionId: 'cwspr-session-id-1', @@ -519,7 +519,7 @@ describe('Telemetry', () => { sinon.assert.notCalled(sessionManagerSpy.closeSession) sinon.assert.notCalled(telemetryServiceSpy) - // Send second completion request to close first one + // Send second completion request should not close first one setServiceResponse(DEFAULT_SUGGESTIONS, { ...EXPECTED_RESPONSE_CONTEXT, codewhispererSessionId: 'cwspr-session-id-2', @@ -528,7 +528,7 @@ describe('Telemetry', () => { assert.equal(firstSession.state, 'DISCARD') assert.notEqual(firstSession, sessionManager.getCurrentSession()) - sinon.assert.calledWithExactly(sessionManagerSpy.closeSession, firstSession) + sinon.assert.notCalled(sessionManagerSpy.closeSession) // Test that session reports it's status when second request is received const expectedEvent = aUserTriggerDecision({ state: 'DISCARD', From d3cd4556c0fc6cf08d93e0e0733798525ea0b7f8 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Thu, 21 Aug 2025 12:20:09 -0700 Subject: [PATCH 33/74] chore(release): release packages from branch main (#2149) Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> --- .release-please-manifest.json | 2 +- package-lock.json | 2 +- server/aws-lsp-codewhisperer/CHANGELOG.md | 7 +++++++ server/aws-lsp-codewhisperer/package.json | 2 +- 4 files changed, 10 insertions(+), 3 deletions(-) diff --git a/.release-please-manifest.json b/.release-please-manifest.json index 159795a30e..49f118f219 100644 --- a/.release-please-manifest.json +++ b/.release-please-manifest.json @@ -2,7 +2,7 @@ "chat-client": "0.1.33", "core/aws-lsp-core": "0.0.14", "server/aws-lsp-antlr4": "0.1.18", - "server/aws-lsp-codewhisperer": "0.0.74", + "server/aws-lsp-codewhisperer": "0.0.75", "server/aws-lsp-json": "0.1.18", "server/aws-lsp-partiql": "0.0.17", "server/aws-lsp-yaml": "0.1.18" diff --git a/package-lock.json b/package-lock.json index 97b89d43b8..8dbdc6d513 100644 --- a/package-lock.json +++ b/package-lock.json @@ -28670,7 +28670,7 @@ }, "server/aws-lsp-codewhisperer": { "name": "@aws/lsp-codewhisperer", - "version": "0.0.74", + "version": "0.0.75", "bundleDependencies": [ "@amzn/codewhisperer-streaming", "@amzn/amazon-q-developer-streaming-client" diff --git a/server/aws-lsp-codewhisperer/CHANGELOG.md b/server/aws-lsp-codewhisperer/CHANGELOG.md index 048eaee59c..a70284a4c2 100644 --- a/server/aws-lsp-codewhisperer/CHANGELOG.md +++ b/server/aws-lsp-codewhisperer/CHANGELOG.md @@ -1,5 +1,12 @@ # Changelog +## [0.0.75](https://github.com/aws/language-servers/compare/lsp-codewhisperer/v0.0.74...lsp-codewhisperer/v0.0.75) (2025-08-21) + + +### Bug Fixes + +* **amazonq:** don't let flare send discard for the still valid suggestion in JB ([#2145](https://github.com/aws/language-servers/issues/2145)) ([0767e07](https://github.com/aws/language-servers/commit/0767e074c91682a91d2fe7a6b2a7369c4dea280c)) + ## [0.0.74](https://github.com/aws/language-servers/compare/lsp-codewhisperer/v0.0.73...lsp-codewhisperer/v0.0.74) (2025-08-19) diff --git a/server/aws-lsp-codewhisperer/package.json b/server/aws-lsp-codewhisperer/package.json index c4264e5988..c0f6c16afd 100644 --- a/server/aws-lsp-codewhisperer/package.json +++ b/server/aws-lsp-codewhisperer/package.json @@ -1,6 +1,6 @@ { "name": "@aws/lsp-codewhisperer", - "version": "0.0.74", + "version": "0.0.75", "description": "CodeWhisperer Language Server", "main": "out/index.js", "repository": { From 28d46a7ff65bbc702606d01a616085e572eb1ab5 Mon Sep 17 00:00:00 2001 From: manodnyab <66754471+manodnyab@users.noreply.github.com> Date: Thu, 21 Aug 2025 19:56:29 -0700 Subject: [PATCH 34/74] chore: merge agentic version 1.29.0 (#2151) * chore: bump agentic version: 1.29.0 * chore: empty commit to start Github Action (#2150) --------- Co-authored-by: aws-toolkit-automation <> --- app/aws-lsp-codewhisperer-runtimes/src/version.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/aws-lsp-codewhisperer-runtimes/src/version.json b/app/aws-lsp-codewhisperer-runtimes/src/version.json index 4c93c3f549..93401eb637 100644 --- a/app/aws-lsp-codewhisperer-runtimes/src/version.json +++ b/app/aws-lsp-codewhisperer-runtimes/src/version.json @@ -1,3 +1,3 @@ { - "agenticChat": "1.27.0" + "agenticChat": "1.29.0" } From 2fb896e094de0bc5a1b4881067e7dcceb3826015 Mon Sep 17 00:00:00 2001 From: Boyu Date: Thu, 21 Aug 2025 22:08:16 -0700 Subject: [PATCH 35/74] feat: add basic OAuth client for remote MCP (#2136) * feat: add basic mcp oauth client * fix: relax condition for triggering oauth flow; add resilience checks for as availability * feat: add unit test for auth client * fix: multiple fixes for remote mcp error and timeout --- .../agenticChat/tools/mcp/mcpEventHandler.ts | 65 +-- .../agenticChat/tools/mcp/mcpManager.ts | 53 ++- .../tools/mcp/mcpOauthClient.test.ts | 120 +++++ .../agenticChat/tools/mcp/mcpOauthClient.ts | 437 ++++++++++++++++++ 4 files changed, 635 insertions(+), 40 deletions(-) create mode 100644 server/aws-lsp-codewhisperer/src/language-server/agenticChat/tools/mcp/mcpOauthClient.test.ts create mode 100644 server/aws-lsp-codewhisperer/src/language-server/agenticChat/tools/mcp/mcpOauthClient.ts 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 5b3ac84e98..a5090319ba 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 @@ -3,7 +3,6 @@ import { MCP_SERVER_STATUS_CHANGED, McpManager } from './mcpManager' import { ChatTelemetryController } from '../../../chat/telemetry/chatTelemetryController' import { ChokidarFileWatcher } from './chokidarFileWatcher' // eslint-disable-next-line import/no-nodejs-modules -import * as path from 'path' import { DetailedListGroup, DetailedListItem, @@ -13,14 +12,7 @@ import { Status, } from '@aws/language-server-runtimes/protocol' -import { - getGlobalMcpConfigPath, - getGlobalAgentConfigPath, - getWorkspaceMcpConfigPaths, - getWorkspaceAgentConfigPaths, - sanitizeName, - normalizePathFromUri, -} from './mcpUtils' +import { getGlobalAgentConfigPath, getWorkspaceAgentConfigPaths, sanitizeName, normalizePathFromUri } from './mcpUtils' import { McpPermissionType, MCPServerConfig, @@ -29,7 +21,6 @@ import { McpServerStatus, } from './mcpTypes' import { TelemetryService } from '../../../../shared/telemetry/telemetryService' -import { URI } from 'vscode-uri' import { ProfileStatusMonitor } from './profileStatusMonitor' interface PermissionOption { @@ -38,6 +29,11 @@ interface PermissionOption { description?: string } +enum TransportType { + STDIO = 'stdio', + HTTP = 'http', +} + export class McpEventHandler { #features: Features #eventListenerRegistered: boolean @@ -393,7 +389,7 @@ export class McpEventHandler { const serverStatusError = this.#getServerStatusError(existingValues.name) || {} // Determine which transport is selected (default to stdio) - const selectedTransport = existingValues.transport || 'stdio' + const selectedTransport = existingValues.transport || TransportType.STDIO return { id: params.id, @@ -432,14 +428,14 @@ export class McpEventHandler { title: 'Transport', mandatory: true, options: [ - { label: 'stdio', value: 'stdio' }, - { label: 'http', value: 'http' }, + { label: TransportType.STDIO, value: TransportType.STDIO }, + { label: TransportType.HTTP, value: TransportType.HTTP }, ], value: selectedTransport, }, ] - if (selectedTransport === 'http') { + if (selectedTransport === TransportType.HTTP) { return [ ...common, { @@ -603,8 +599,13 @@ export class McpEventHandler { errors.push('Either command or url is required') } else if (command && url) { errors.push('Provide either command OR url, not both') - } else if (transport && ((transport === 'stdio' && !command) || (transport !== 'stdio' && !url))) { - errors.push(`${transport === 'stdio' ? 'Command' : 'URL'} is required for ${transport} transport`) + } else if ( + transport && + ((transport === TransportType.STDIO && !command) || (transport !== TransportType.STDIO && !url)) + ) { + errors.push( + `${transport === TransportType.STDIO ? 'Command' : 'URL'} is required for ${transport} transport` + ) } if (values.timeout && values.timeout.trim() !== '') { @@ -692,7 +693,7 @@ export class McpEventHandler { // stdio‑specific parsing let args: string[] = [] let env: Record = {} - if (selectedTransport === 'stdio') { + if (selectedTransport === TransportType.STDIO) { try { args = (Array.isArray(params.optionsValues.args) ? params.optionsValues.args : []) .map((item: any) => @@ -719,7 +720,7 @@ export class McpEventHandler { // http‑specific parsing let headers: Record = {} - if (selectedTransport === 'http') { + if (selectedTransport === TransportType.HTTP) { try { const raw = Array.isArray(params.optionsValues.headers) ? params.optionsValues.headers : [] headers = raw.reduce((acc: Record, item: any) => { @@ -743,7 +744,7 @@ export class McpEventHandler { // build final config (no transport field persisted) let config: MCPServerConfig - if (selectedTransport === 'http') { + if (selectedTransport === TransportType.HTTP) { config = { url: params.optionsValues.url, headers, @@ -786,14 +787,15 @@ export class McpEventHandler { } this.#currentEditingServerName = undefined + this.#serverNameBeforeUpdate = undefined // need to check server state now, as there is possibility of error during server initialization const serverStatusError = this.#getServerStatusError(serverName) this.#telemetryController?.emitMCPServerInitializeEvent({ source: isEditMode ? 'updateServer' : 'addServer', - command: selectedTransport === 'stdio' ? params.optionsValues.command : undefined, - url: selectedTransport === 'http' ? params.optionsValues.url : undefined, + command: selectedTransport === TransportType.STDIO ? params.optionsValues.command : undefined, + url: selectedTransport === TransportType.HTTP ? params.optionsValues.url : undefined, enabled: true, numTools: McpManager.instance.getAllToolsWithPermissions(serverName).length, scope: params.optionsValues['scope'] === 'global' ? 'global' : 'workspace', @@ -1014,7 +1016,7 @@ export class McpEventHandler { } // Respect a user flip first; otherwise fall back to what the stored configuration implies. - const transport = params.optionsValues?.transport ?? (config.url ? 'http' : 'stdio') + const transport = params.optionsValues?.transport ?? (config.url ? TransportType.HTTP : TransportType.STDIO) // Convert stored structures to UI‑friendly lists const argsList = (config.args ?? []).map(a => ({ arg_key: a })) // for stdio @@ -1090,8 +1092,9 @@ export class McpEventHandler { // Clean up transport-specific fields if (optionsValues) { - const transport = optionsValues.transport ?? 'stdio' // Maintain default to 'stdio' - const fieldsToDelete = transport === 'http' ? ['command', 'args', 'env_variables'] : ['url', 'headers'] + const transport = optionsValues.transport ?? TransportType.STDIO // Maintain default to 'stdio' + const fieldsToDelete = + transport === TransportType.HTTP ? ['command', 'args', 'env_variables'] : ['url', 'headers'] fieldsToDelete.forEach(field => delete optionsValues[field]) } @@ -1238,11 +1241,11 @@ export class McpEventHandler { const serverConfig = McpManager.instance.getAllServerConfigs().get(serverName) if (serverConfig) { // Emit server initialize event after permission change - const transportType = serverConfig.command ? 'stdio' : 'http' + const transportType = serverConfig.command?.trim() ? TransportType.STDIO : TransportType.HTTP this.#telemetryController?.emitMCPServerInitializeEvent({ source: 'updatePermission', - command: transportType === 'stdio' ? serverConfig.command : undefined, - url: transportType === 'http' ? serverConfig.url : undefined, + command: transportType === TransportType.STDIO ? serverConfig.command : undefined, + url: transportType === TransportType.HTTP ? serverConfig.url : undefined, enabled: true, numTools: McpManager.instance.getAllToolsWithPermissions(serverName).length, scope: @@ -1310,16 +1313,16 @@ export class McpEventHandler { // Emit server initialize events for all active servers for (const [serverName, config] of serverConfigs.entries()) { - const transportType = config.command ? 'stdio' : 'http' + const transportType = config.command ? TransportType.STDIO : TransportType.HTTP const enabled = !mcpManager.isServerDisabled(serverName) this.#telemetryController?.emitMCPServerInitializeEvent({ source: 'reload', - command: transportType === 'stdio' ? config.command : undefined, - url: transportType === 'http' ? config.url : undefined, + command: transportType === TransportType.STDIO ? config.command : undefined, + url: transportType === TransportType.HTTP ? config.url : undefined, enabled: enabled, numTools: mcpManager.getAllToolsWithPermissions(serverName).length, scope: config.__configPath__ === globalAgentPath ? 'global' : 'workspace', - transportType: 'stdio', + transportType: transportType, languageServerVersion: this.#features.runtime.serverInfo.version, }) } diff --git a/server/aws-lsp-codewhisperer/src/language-server/agenticChat/tools/mcp/mcpManager.ts b/server/aws-lsp-codewhisperer/src/language-server/agenticChat/tools/mcp/mcpManager.ts index 844a872485..7ac6257641 100644 --- a/server/aws-lsp-codewhisperer/src/language-server/agenticChat/tools/mcp/mcpManager.ts +++ b/server/aws-lsp-codewhisperer/src/language-server/agenticChat/tools/mcp/mcpManager.ts @@ -38,6 +38,7 @@ import path = require('path') import { URI } from 'vscode-uri' import { sanitizeInput } from '../../../../shared/utils' import { ProfileStatusMonitor } from './profileStatusMonitor' +import { OAuthClient } from './mcpOauthClient' export const MCP_SERVER_STATUS_CHANGED = 'mcpServerStatusChanged' export const AGENT_TOOLS_CHANGED = 'agentToolsChanged' @@ -299,7 +300,7 @@ export class McpManager { this.features.logging.debug(`MCP: initializing server [${serverName}]`) const client = new Client({ - name: `mcp-client-${serverName}`, + name: `q-chat-plugin`, // Do not use server name in the client name to avoid polluting builder-mcp metrics version: '1.0.0', }) @@ -307,6 +308,7 @@ export class McpManager { const isStdio = !!cfg.command const doConnect = async () => { if (isStdio) { + // stdio transport const mergedEnv = { ...(process.env as Record), // Make sure we do not have empty key and value in mergedEnv, or adding server through UI will fail on Windows @@ -347,11 +349,33 @@ export class McpManager { ) } } else { + // streamable http/SSE transport const base = new URL(cfg.url!) try { + // Use HEAD to check if it needs OAuth + let headers: Record = { ...(cfg.headers ?? {}) } + let needsOAuth = false + try { + const headResp = await fetch(base, { method: 'HEAD', headers }) + const www = headResp.headers.get('www-authenticate') || '' + needsOAuth = headResp.status === 401 || headResp.status === 403 || /bearer/i.test(www) + } catch { + this.features.logging.info(`MCP: HEAD not available`) + } + + if (needsOAuth) { + OAuthClient.initialize(this.features.workspace, this.features.logging) + const bearer = await OAuthClient.getValidAccessToken(base) + // add authorization header if we are able to obtain a bearer token + if (bearer) { + headers = { ...headers, Authorization: `Bearer ${bearer}` } + } + } + try { // try streamable http first - transport = new StreamableHTTPClientTransport(base, this.buildHttpOpts(cfg.headers)) + transport = new StreamableHTTPClientTransport(base, this.buildHttpOpts(headers)) + this.features.logging.info(`MCP: Connecting MCP server using StreamableHTTPClientTransport`) await client.connect(transport) } catch (err) { @@ -359,13 +383,14 @@ export class McpManager { this.features.logging.info( `MCP: streamable http connect failed for [${serverName}], fallback to SSEClientTransport: ${String(err)}` ) - transport = new SSEClientTransport(new URL(cfg.url!), this.buildSseOpts(cfg.headers)) + transport = new SSEClientTransport(new URL(cfg.url!), this.buildSseOpts(headers)) await client.connect(transport) } } catch (err: any) { let errorMessage = err?.message ?? String(err) + const oauthHint = /oauth/i.test(errorMessage) ? ' (OAuth)' : '' throw new AgenticChatError( - `MCP: server '${serverName}' failed to connect: ${errorMessage}`, + `MCP: server '${serverName}' failed to connect${oauthHint}: ${errorMessage}`, 'MCPServerConnectionFailed' ) } @@ -645,7 +670,7 @@ export class McpManager { disabled: cfg.disabled ?? false, } // Only add timeout to agent config if it's not 0 - if (cfg.timeout !== 0) { + if (cfg.timeout !== undefined) { serverConfig.timeout = cfg.timeout } if (cfg.args && cfg.args.length > 0) { @@ -1268,11 +1293,21 @@ export class McpManager { private handleError(server: string | undefined, err: unknown) { const msg = err instanceof Error ? err.message : String(err) - this.features.logging.error(`MCP ERROR${server ? ` [${server}]` : ''}: ${msg}`) + const isBenignSseDisconnect = + /SSE error:\s*TypeError:\s*terminated:\s*Body Timeout Error/i.test(msg) || + /TypeError:\s*terminated:\s*Body Timeout Error/i.test(msg) || + /TypeError:\s*terminated:\s*other side closed/i.test(msg) || + /ECONNRESET|ENETRESET|EPIPE/i.test(msg) - if (server) { - this.setState(server, McpServerStatus.FAILED, 0, msg) - this.emitToolsChanged(server) + if (isBenignSseDisconnect) { + this.features.logging.debug(`MCP SSE idle timeout${server ? ` [${server}]` : ''}: ${msg}`) + } else { + // default path for real errors + this.features.logging.error(`MCP ERROR${server ? ` [${server}]` : ''}: ${msg}`) + if (server) { + this.setState(server, McpServerStatus.FAILED, 0, msg) + this.emitToolsChanged(server) + } } } diff --git a/server/aws-lsp-codewhisperer/src/language-server/agenticChat/tools/mcp/mcpOauthClient.test.ts b/server/aws-lsp-codewhisperer/src/language-server/agenticChat/tools/mcp/mcpOauthClient.test.ts new file mode 100644 index 0000000000..43f68302eb --- /dev/null +++ b/server/aws-lsp-codewhisperer/src/language-server/agenticChat/tools/mcp/mcpOauthClient.test.ts @@ -0,0 +1,120 @@ +/*! + * Copyright Amazon.com, Inc. or its affiliates. + * All Rights Reserved. SPDX-License-Identifier: Apache-2.0 + */ + +import { expect } from 'chai' +import * as sinon from 'sinon' +import * as crypto from 'crypto' +import * as http from 'http' +import { EventEmitter } from 'events' +import * as path from 'path' +import { OAuthClient } from './mcpOauthClient' + +const fakeLogger = { + log: () => {}, + debug: () => {}, + info: () => {}, + warn: () => {}, + error: () => {}, +} + +const fakeWorkspace = { + fs: { + exists: async (_path: string) => false, + readFile: async (_path: string) => Buffer.from('{}'), + writeFile: async (_path: string, _d: any) => {}, + mkdir: async (_dir: string, _opts: any) => {}, + }, +} as any + +function stubFileSystem(tokenObj?: any, regObj?: any): void { + const cacheDir = (OAuthClient as any).cacheDir as string + const tokPath = path.join(cacheDir, 'testkey.token.json') + const regPath = path.join(cacheDir, 'testkey.registration.json') + + const existsStub = sinon.stub(fakeWorkspace.fs, 'exists') + existsStub.callsFake(async (p: any) => { + if (p === tokPath && tokenObj) return true + if (p === regPath && regObj) return true + return false + }) + + const readStub = sinon.stub(fakeWorkspace.fs, 'readFile') + readStub.callsFake(async (p: any) => { + if (p === tokPath && tokenObj) return Buffer.from(JSON.stringify(tokenObj)) + if (p === regPath && regObj) return Buffer.from(JSON.stringify(regObj)) + return Buffer.from('{}') + }) + + sinon.stub(fakeWorkspace.fs, 'writeFile').resolves() + sinon.stub(fakeWorkspace.fs, 'mkdir').resolves() +} + +function stubHttpServer(): void { + sinon.stub(http, 'createServer').callsFake(() => { + const srv = new EventEmitter() as unknown as http.Server & EventEmitter + ;(srv as any).address = () => ({ address: '127.0.0.1', port: 12345, family: 'IPv4' }) + ;(srv as any).listen = (_port?: any, _host?: any, _backlog?: any, cb?: any) => { + if (typeof cb === 'function') cb() + // simulate async readiness like a real server + process.nextTick(() => srv.emit('listening')) + return srv + } + ;(srv as any).close = (cb?: any) => { + if (typeof cb === 'function') cb() + srv.removeAllListeners() + return srv + } + return srv + }) +} + +describe('OAuthClient helpers', () => { + it('computeKey() generates deterministic SHA-256 hex', () => { + const url = new URL('https://example.com/api') + const expected = crypto + .createHash('sha256') + .update(url.origin + url.pathname) + .digest('hex') + const actual = (OAuthClient as any).computeKey(url) + expect(actual).to.equal(expected) + }) + + it('b64url() strips padding and is URL-safe', () => { + const buf = Buffer.from('hello') + const actual = (OAuthClient as any).b64url(buf) + expect(actual).to.equal('aGVsbG8') + }) +}) + +describe('OAuthClient getValidAccessToken()', () => { + const now = Date.now() + + beforeEach(() => { + sinon.restore() + OAuthClient.initialize(fakeWorkspace, fakeLogger as any) + sinon.stub(OAuthClient as any, 'computeKey').returns('testkey') + stubHttpServer() + }) + + afterEach(() => sinon.restore()) + + it('returns cached token when still valid', async () => { + const cachedToken = { + access_token: 'cached_access', + expires_in: 3600, + obtained_at: now - 1_000, + } + const cachedReg = { + client_id: 'cid', + redirect_uri: 'http://localhost:12345', + } + + stubFileSystem(cachedToken, cachedReg) + + const token = await OAuthClient.getValidAccessToken(new URL('https://api.example.com/mcp')) + expect(token).to.equal('cached_access') + expect((http.createServer as any).calledOnce).to.be.true + }) +}) diff --git a/server/aws-lsp-codewhisperer/src/language-server/agenticChat/tools/mcp/mcpOauthClient.ts b/server/aws-lsp-codewhisperer/src/language-server/agenticChat/tools/mcp/mcpOauthClient.ts new file mode 100644 index 0000000000..73ffea1a1d --- /dev/null +++ b/server/aws-lsp-codewhisperer/src/language-server/agenticChat/tools/mcp/mcpOauthClient.ts @@ -0,0 +1,437 @@ +/*! + * Copyright Amazon.com, Inc. or its affiliates. + * All Rights Reserved. SPDX-License-Identifier: Apache-2.0 + */ + +import type { RequestInit } from 'node-fetch' +import * as crypto from 'crypto' +import * as path from 'path' +import { spawn } from 'child_process' +import { URL, URLSearchParams } from 'url' +import * as http from 'http' +import { Logger, Workspace } from '@aws/language-server-runtimes/server-interface' + +interface Token { + access_token: string + expires_in: number + refresh_token?: string + obtained_at: number +} + +interface Meta { + authorization_endpoint: string + token_endpoint: string + registration_endpoint?: string +} + +interface Registration { + client_id: string + client_secret?: string + expires_at?: number + redirect_uri: string +} + +export class OAuthClient { + private static logger: Logger + private static workspace: Workspace + + public static initialize(ws: Workspace, logger: Logger): void { + this.workspace = ws + this.logger = logger + } + + /** + * Return a valid Bearer token, reusing cache or refresh-token if possible, + * otherwise driving one interactive PKCE flow. + */ + public static async getValidAccessToken(mcpBase: URL): Promise { + const key = this.computeKey(mcpBase) + const regPath = path.join(this.cacheDir, `${key}.registration.json`) + const tokPath = path.join(this.cacheDir, `${key}.token.json`) + + // 1) Spin up (or reuse) loopback server + redirect URI + let server: http.Server, redirectUri: string + const savedReg = await this.read(regPath) + if (savedReg) { + const port = Number(new URL(savedReg.redirect_uri).port) + server = http.createServer() + try { + await this.listen(server, port) + redirectUri = savedReg.redirect_uri + this.logger.info(`OAuth: reusing redirect URI ${redirectUri}`) + } catch (e: any) { + if (e.code === 'EADDRINUSE') { + try { + server.close() + } catch { + /* ignore */ + } + this.logger.warn(`Port ${port} in use; falling back to new random port`) + ;({ server, redirectUri } = await this.buildCallbackServer()) + this.logger.info(`OAuth: new redirect URI ${redirectUri}`) + await this.workspace.fs.rm(regPath) + } else { + throw e + } + } + } else { + ;({ server, redirectUri } = await this.buildCallbackServer()) + this.logger.info(`OAuth: new redirect URI ${redirectUri}`) + } + + try { + // 2) Try still-valid cached access_token + const cached = await this.read(tokPath) + if (cached) { + const expiry = cached.obtained_at + cached.expires_in * 1000 + if (Date.now() < expiry) { + this.logger.info(`OAuth: using still‑valid cached token`) + return cached.access_token + } + this.logger.info(`OAuth: cached token expired → try refresh`) + } + + // 3) Discover AS metadata + let meta: Meta + try { + meta = await this.discoverAS(mcpBase) + } catch (e: any) { + throw new Error(`OAuth discovery failed: ${e?.message ?? String(e)}`) + } + // 4) Register (or reuse) a dynamic client + const scopes = ['openid', 'offline_access'] + let reg: Registration + try { + reg = await this.obtainClient(meta, regPath, scopes, redirectUri) + } catch (e: any) { + throw new Error(`OAuth client registration failed: ${e?.message ?? String(e)}`) + } + + // 5) Refresh‑token grant (one shot) + const attemptedRefresh = !!cached?.refresh_token + if (cached?.refresh_token) { + const refreshed = await this.refreshGrant(meta, reg, mcpBase, cached.refresh_token) + if (refreshed) { + await this.write(tokPath, refreshed) + this.logger.info(`OAuth: refresh grant succeeded`) + return refreshed.access_token + } + this.logger.info(`OAuth: refresh grant failed`) + } + + // 6) PKCE interactive flow + try { + const fresh = await this.pkceGrant(meta, reg, mcpBase, scopes, redirectUri, server) + await this.write(tokPath, fresh) + return fresh.access_token + } catch (e: any) { + const suffix = attemptedRefresh ? ' after refresh attempt' : '' + throw new Error(`OAuth authorization (PKCE) failed${suffix}: ${e?.message ?? String(e)}`) + } + } finally { + await new Promise(res => server.close(() => res())) + } + } + + /** Spin up a one‑time HTTP listener on localhost:randomPort */ + private static async buildCallbackServer(): Promise<{ server: http.Server; redirectUri: string }> { + const server = http.createServer() + await this.listen(server, 0) + const port = (server.address() as any).port as number + return { server, redirectUri: `http://localhost:${port}` } + } + + /** Discover OAuth endpoints by HEAD/WWW‑Authenticate, well‑known, or fallback */ + private static async discoverAS(rs: URL): Promise { + // a) HEAD → WWW‑Authenticate → resource_metadata + try { + this.logger.info('MCP OAuth: attempting discovery via WWW-Authenticate header') + const h = await this.fetchCompat(rs.toString(), { method: 'HEAD' }) + const header = h.headers.get('www-authenticate') || '' + const m = /resource_metadata=(?:"([^"]+)"|([^,\s]+))/i.exec(header) + if (m) { + const metaUrl = new URL(m[1] || m[2], rs).toString() + this.logger.info(`OAuth: resource_metadata → ${metaUrl}`) + const raw = await this.json(metaUrl) + return await this.fetchASFromResourceMeta(raw, metaUrl) + } + } catch { + this.logger.info('MCP OAuth: no resource_metadata found in WWW-Authenticate header') + } + + // b) well‑known on resource host + this.logger.info('MCP OAuth: attempting discovery via well-known endpoints') + const probes = [ + new URL('.well-known/oauth-authorization-server', rs).toString(), + new URL('.well-known/openid-configuration', rs).toString(), + `${rs.origin}/.well-known/oauth-authorization-server`, + `${rs.origin}/.well-known/openid-configuration`, + ] + for (const url of probes) { + try { + this.logger.info(`MCP OAuth: probing well-known endpoint → ${url}`) + return await this.json(url) + } catch (error) { + this.logger.info(`OAuth: well-known endpoint probe failed for ${url}`) + } + } + + // c) fallback to static OAuth2 endpoints + const base = (rs.origin + rs.pathname).replace(/\/+$/, '') + this.logger.warn(`OAuth: all discovery attempts failed, synthesizing endpoints from ${base}`) + return { + authorization_endpoint: `${base}/authorize`, + token_endpoint: `${base}/access_token`, + } + } + + /** Follow `authorization_server(s)` in resource_metadata JSON */ + private static async fetchASFromResourceMeta(raw: any, metaUrl: string): Promise { + let asBase = raw.authorization_server + if (!asBase && Array.isArray(raw.authorization_servers)) { + asBase = raw.authorization_servers[0] + } + if (!asBase) { + throw new Error(`resource_metadata at ${metaUrl} lacked authorization_server(s)`) + } + + // Attempt both OAuth‑AS and OIDC well‑known + for (const p of ['.well-known/oauth-authorization-server', '.well-known/openid-configuration']) { + try { + return await this.json(new URL(p, asBase).toString()) + } catch { + // next + } + } + // fallback to static OAuth2 endpoints + this.logger.warn(`OAuth: no well-known on ${asBase}, falling back to static endpoints`) + return { + authorization_endpoint: `${asBase}/authorize`, + token_endpoint: `${asBase}/access_token`, + } + } + + /** DCR: POST client metadata → client_id; cache to disk */ + private static async obtainClient( + meta: Meta, + file: string, + scopes: string[], + redirectUri: string + ): Promise { + const existing = await this.read(file) + if (existing && (!existing.expires_at || existing.expires_at * 1000 > Date.now())) { + this.logger.info(`OAuth: reusing client_id ${existing.client_id}`) + return existing + } + + if (!meta.registration_endpoint) { + throw new Error('OAuth: AS does not support dynamic registration') + } + + const body = { + client_name: 'AWS MCP LSP', + grant_types: ['authorization_code', 'refresh_token'], + response_types: ['code'], + token_endpoint_auth_method: 'none', + scope: scopes.join(' '), + redirect_uris: [redirectUri], + } + const resp: any = await this.json(meta.registration_endpoint, { + method: 'POST', + headers: { 'content-type': 'application/json' }, + body: JSON.stringify(body), + }) + + const reg: Registration = { + client_id: resp.client_id, + client_secret: resp.client_secret, + expires_at: resp.client_secret_expires_at, + redirect_uri: redirectUri, + } + await this.write(file, reg) + return reg + } + + /** Try one refresh_token grant; returns new Token or `undefined` */ + private static async refreshGrant( + meta: Meta, + reg: Registration, + rs: URL, + refresh: string + ): Promise { + const form = new URLSearchParams({ + grant_type: 'refresh_token', + refresh_token: refresh, + client_id: reg.client_id, + resource: rs.toString(), + }) + const res = await this.fetchCompat(meta.token_endpoint, { + method: 'POST', + headers: { 'content-type': 'application/x-www-form-urlencoded' }, + body: form, + }) + if (!res.ok) { + const msg = await res.text().catch(() => '') + this.logger.warn(`OAuth: refresh grant HTTP ${res.status} — ${msg?.slice(0, 300)}`) + return undefined + } + const tokenResponse = (await res.json()) as Record + return { ...(tokenResponse as object), obtained_at: Date.now() } as Token + } + + /** One PKCE flow: browser + loopback → code → token */ + private static async pkceGrant( + meta: Meta, + reg: Registration, + rs: URL, + scopes: string[], + redirectUri: string, + server: http.Server + ): Promise { + // a) generate PKCE params + const verifier = this.b64url(crypto.randomBytes(32)) + const challenge = this.b64url(crypto.createHash('sha256').update(verifier).digest()) + const state = this.b64url(crypto.randomBytes(16)) + + // b) build authorize URL + launch browser + const authz = new URL(meta.authorization_endpoint) + authz.search = new URLSearchParams({ + client_id: reg.client_id, + response_type: 'code', + code_challenge: challenge, + code_challenge_method: 'S256', + resource: rs.toString(), + scope: scopes.join(' '), + redirect_uri: redirectUri, + state: state, + }).toString() + + const opener = + process.platform === 'win32' + ? { cmd: 'cmd', args: ['/c', 'start', authz.toString()] } + : process.platform === 'darwin' + ? { cmd: 'open', args: [authz.toString()] } + : { cmd: 'xdg-open', args: [authz.toString()] } + + void spawn(opener.cmd, opener.args, { detached: true, stdio: 'ignore' }).unref() + + // c) wait for code on our loopback + const { code, rxState, err } = await new Promise<{ code: string; rxState: string; err?: string }>(resolve => { + server.on('request', (req, res) => { + const u = new URL(req.url || '/', redirectUri) + const c = u.searchParams.get('code') || '' + const s = u.searchParams.get('state') || '' + const e = u.searchParams.get('error') || undefined + res.writeHead(200, { 'content-type': 'text/html' }).end('

You may close this tab.

') + resolve({ code: c, rxState: s, err: e }) + }) + }) + if (err) throw new Error(`Authorization error: ${err}`) + if (!code || rxState !== state) throw new Error('Invalid authorization response (state mismatch)') + + // d) exchange code for token + const form2 = new URLSearchParams({ + grant_type: 'authorization_code', + code, + code_verifier: verifier, + client_id: reg.client_id, + redirect_uri: redirectUri, + resource: rs.toString(), + }) + const res2 = await this.fetchCompat(meta.token_endpoint, { + method: 'POST', + headers: { 'content-type': 'application/x-www-form-urlencoded' }, + body: form2, + }) + if (!res2.ok) { + const txt = await res2.text().catch(() => '') + throw new Error(`Token exchange failed (HTTP ${res2.status}): ${txt?.slice(0, 300)}`) + } + const tk = (await res2.json()) as Record + return { ...(tk as object), obtained_at: Date.now() } as Token + } + + /** Fetch + error‑check + parse JSON */ + private static async json(url: string, init?: RequestInit): Promise { + const r = await this.fetchCompat(url, init) + if (!r.ok) { + const txt = await r.text().catch(() => '') + throw new Error(`HTTP ${r.status}@${url} — ${txt}`) + } + return (await r.json()) as T + } + + /** Read & parse JSON file via workspace.fs */ + private static async read(file: string): Promise { + try { + if (!(await this.workspace.fs.exists(file))) return undefined + const buf = await this.workspace.fs.readFile(file) + return JSON.parse(buf.toString()) as T + } catch { + return undefined + } + } + + /** Write JSON, then clamp file perms to 0600 (owner read/write) */ + private static async write(file: string, obj: unknown): Promise { + const dir = path.dirname(file) + await this.workspace.fs.mkdir(dir, { recursive: true }) + await this.workspace.fs.writeFile(file, JSON.stringify(obj, null, 2), { mode: 0o600 }) + } + + /** SHA‑256 of resourceServer URL → hex key */ + private static computeKey(rs: URL): string { + return crypto + .createHash('sha256') + .update(rs.origin + rs.pathname) + .digest('hex') + } + + /** RFC‑7636 base64url without padding */ + private static b64url(buf: Buffer): string { + return buf.toString('base64').replace(/=/g, '').replace(/\+/g, '-').replace(/\//g, '_') + } + + /** Directory for caching registration + tokens */ + private static readonly cacheDir = path.join( + process.env.HOME || process.env.USERPROFILE || '.', + '.aws', + 'sso', + 'cache' + ) + + /** + * Await server.listen() but reject if it emits 'error' (eg EADDRINUSE), + * so callers can handle it immediately instead of hanging. + */ + private static listen(server: http.Server, port: number, host: string = '127.0.0.1'): Promise { + return new Promise((resolve, reject) => { + const onListening = () => { + server.off('error', onError) + resolve() + } + const onError = (err: NodeJS.ErrnoException) => { + server.off('listening', onListening) + reject(err) + } + server.once('listening', onListening) + server.once('error', onError) + server.listen(port, host) + }) + } + + /** + * Fetch compatibility: use global fetch on Node >= 18, otherwise dynamically import('node-fetch'). + * Using Function('return import(...)') avoids downleveling to require() in CJS builds. + */ + private static async fetchCompat(url: string, init?: RequestInit): Promise { + const globalObj = globalThis as any + if (typeof globalObj.fetch === 'function') { + return globalObj.fetch(url as any, init as any) + } + // Dynamic import of ESM node-fetch (only when global fetch is unavailable) + const mod = await (Function('return import("node-fetch")')() as Promise) + const f = mod.default ?? mod + return f(url as any, init as any) + } +} From a6c64f2995a17697e3d71d30a1f411f5cf0db279 Mon Sep 17 00:00:00 2001 From: atontb <104926752+atonaamz@users.noreply.github.com> Date: Fri, 22 Aug 2025 10:06:22 -0700 Subject: [PATCH 36/74] fix: adding streakTracker to track streakLength across Completions and Edits (#2147) --- .../inline-completion/codeWhispererServer.ts | 10 +-- .../editCompletionHandler.ts | 9 +- .../session/sessionManager.test.ts | 42 --------- .../session/sessionManager.ts | 13 --- .../tracker/streakTracker.test.ts | 85 +++++++++++++++++++ .../tracker/streakTracker.ts | 42 +++++++++ 6 files changed, 138 insertions(+), 63 deletions(-) create mode 100644 server/aws-lsp-codewhisperer/src/language-server/inline-completion/tracker/streakTracker.test.ts create mode 100644 server/aws-lsp-codewhisperer/src/language-server/inline-completion/tracker/streakTracker.ts diff --git a/server/aws-lsp-codewhisperer/src/language-server/inline-completion/codeWhispererServer.ts b/server/aws-lsp-codewhisperer/src/language-server/inline-completion/codeWhispererServer.ts index 9f2073d85e..330cf54735 100644 --- a/server/aws-lsp-codewhisperer/src/language-server/inline-completion/codeWhispererServer.ts +++ b/server/aws-lsp-codewhisperer/src/language-server/inline-completion/codeWhispererServer.ts @@ -46,6 +46,7 @@ import { UserWrittenCodeTracker } from '../../shared/userWrittenCodeTracker' import { RecentEditTracker, RecentEditTrackerDefaultConfig } from './tracker/codeEditTracker' import { CursorTracker } from './tracker/cursorTracker' import { RejectedEditTracker, DEFAULT_REJECTED_EDIT_TRACKER_CONFIG } from './tracker/rejectedEditTracker' +import { StreakTracker } from './tracker/streakTracker' import { getAddedAndDeletedLines, getCharacterDifferences } from './diffUtils' import { emitPerceivedLatencyTelemetry, @@ -129,6 +130,7 @@ export const CodewhispererServerFactory = const recentEditTracker = RecentEditTracker.getInstance(logging, RecentEditTrackerDefaultConfig) const cursorTracker = CursorTracker.getInstance() const rejectedEditTracker = RejectedEditTracker.getInstance(logging, DEFAULT_REJECTED_EDIT_TRACKER_CONFIG) + const streakTracker = StreakTracker.getInstance() let editsEnabled = false let isOnInlineCompletionHandlerInProgress = false @@ -317,9 +319,7 @@ export const CodewhispererServerFactory = // for the previous trigger if (ideCategory !== 'JETBRAINS') { completionSessionManager.discardSession(currentSession) - const streakLength = editsEnabled - ? completionSessionManager.getAndUpdateStreakLength(false) - : 0 + const streakLength = editsEnabled ? streakTracker.getAndUpdateStreakLength(false) : 0 await emitUserTriggerDecisionTelemetry( telemetry, telemetryService, @@ -405,7 +405,7 @@ export const CodewhispererServerFactory = if (session.discardInflightSessionOnNewInvocation) { session.discardInflightSessionOnNewInvocation = false completionSessionManager.discardSession(session) - const streakLength = editsEnabled ? completionSessionManager.getAndUpdateStreakLength(false) : 0 + const streakLength = editsEnabled ? streakTracker.getAndUpdateStreakLength(false) : 0 await emitUserTriggerDecisionTelemetry( telemetry, telemetryService, @@ -664,7 +664,7 @@ export const CodewhispererServerFactory = // Always emit user trigger decision at session close sessionManager.closeSession(session) - const streakLength = editsEnabled ? sessionManager.getAndUpdateStreakLength(isAccepted) : 0 + const streakLength = editsEnabled ? streakTracker.getAndUpdateStreakLength(isAccepted) : 0 await emitUserTriggerDecisionTelemetry( telemetry, telemetryService, diff --git a/server/aws-lsp-codewhisperer/src/language-server/inline-completion/editCompletionHandler.ts b/server/aws-lsp-codewhisperer/src/language-server/inline-completion/editCompletionHandler.ts index 99737d027c..193521cce9 100644 --- a/server/aws-lsp-codewhisperer/src/language-server/inline-completion/editCompletionHandler.ts +++ b/server/aws-lsp-codewhisperer/src/language-server/inline-completion/editCompletionHandler.ts @@ -36,12 +36,14 @@ import { getErrorMessage, hasConnectionExpired } from '../../shared/utils' import { AmazonQError, AmazonQServiceConnectionExpiredError } from '../../shared/amazonQServiceManager/errors' import { DocumentChangedListener } from './documentChangedListener' import { EMPTY_RESULT, EDIT_DEBOUNCE_INTERVAL_MS } from './constants' +import { StreakTracker } from './tracker/streakTracker' export class EditCompletionHandler { private readonly editsEnabled: boolean private debounceTimeout: NodeJS.Timeout | undefined private isWaiting: boolean = false private hasDocumentChangedSinceInvocation: boolean = false + private readonly streakTracker: StreakTracker constructor( readonly logging: Logging, @@ -60,6 +62,7 @@ export class EditCompletionHandler { this.editsEnabled = this.clientMetadata.initializationOptions?.aws?.awsClientCapabilities?.textDocument ?.inlineCompletionWithReferences?.inlineEditSupport ?? false + this.streakTracker = StreakTracker.getInstance() } get codeWhispererService() { @@ -264,7 +267,7 @@ export class EditCompletionHandler { if (currentSession && currentSession.state === 'ACTIVE') { // Emit user trigger decision at session close time for active session this.sessionManager.discardSession(currentSession) - const streakLength = this.editsEnabled ? this.sessionManager.getAndUpdateStreakLength(false) : 0 + const streakLength = this.editsEnabled ? this.streakTracker.getAndUpdateStreakLength(false) : 0 await emitUserTriggerDecisionTelemetry( this.telemetry, this.telemetryService, @@ -335,7 +338,7 @@ export class EditCompletionHandler { if (session.discardInflightSessionOnNewInvocation) { session.discardInflightSessionOnNewInvocation = false this.sessionManager.discardSession(session) - const streakLength = this.editsEnabled ? this.sessionManager.getAndUpdateStreakLength(false) : 0 + const streakLength = this.editsEnabled ? this.streakTracker.getAndUpdateStreakLength(false) : 0 await emitUserTriggerDecisionTelemetry( this.telemetry, this.telemetryService, @@ -359,7 +362,7 @@ export class EditCompletionHandler { this.telemetryService, session, this.documentChangedListener.timeSinceLastUserModification, - this.editsEnabled ? this.sessionManager.getAndUpdateStreakLength(false) : 0 + this.editsEnabled ? this.streakTracker.getAndUpdateStreakLength(false) : 0 ) return EMPTY_RESULT } diff --git a/server/aws-lsp-codewhisperer/src/language-server/inline-completion/session/sessionManager.test.ts b/server/aws-lsp-codewhisperer/src/language-server/inline-completion/session/sessionManager.test.ts index bef684eb9b..ddf06ffd40 100644 --- a/server/aws-lsp-codewhisperer/src/language-server/inline-completion/session/sessionManager.test.ts +++ b/server/aws-lsp-codewhisperer/src/language-server/inline-completion/session/sessionManager.test.ts @@ -673,46 +673,4 @@ describe('SessionManager', function () { assert.equal(session.getSuggestionState('id4'), 'Discard') }) }) - - describe('getAndUpdateStreakLength()', function () { - it('should return 0 if user rejects suggestion A', function () { - const manager = SessionManager.getInstance() - - assert.equal(manager.getAndUpdateStreakLength(false), -1) - assert.equal(manager.streakLength, 0) - }) - - it('should return -1 for A and 1 for B if user accepts suggestion A and rejects B', function () { - const manager = SessionManager.getInstance() - - assert.equal(manager.getAndUpdateStreakLength(true), -1) - assert.equal(manager.streakLength, 1) - assert.equal(manager.getAndUpdateStreakLength(false), 1) - assert.equal(manager.streakLength, 0) - }) - - it('should return -1 for A, -1 for B, and 2 for C if user accepts A, accepts B, and rejects C', function () { - const manager = SessionManager.getInstance() - - assert.equal(manager.getAndUpdateStreakLength(true), -1) - assert.equal(manager.streakLength, 1) - assert.equal(manager.getAndUpdateStreakLength(true), -1) - assert.equal(manager.streakLength, 2) - assert.equal(manager.getAndUpdateStreakLength(false), 2) - assert.equal(manager.streakLength, 0) - }) - - it('should return -1 for A, -1 for B, and 1 for C if user accepts A, make an edit, accepts B, and rejects C', function () { - const manager = SessionManager.getInstance() - - assert.equal(manager.getAndUpdateStreakLength(true), -1) - assert.equal(manager.streakLength, 1) - assert.equal(manager.getAndUpdateStreakLength(false), 1) - assert.equal(manager.streakLength, 0) - assert.equal(manager.getAndUpdateStreakLength(true), -1) - assert.equal(manager.streakLength, 1) - assert.equal(manager.getAndUpdateStreakLength(false), 1) - assert.equal(manager.streakLength, 0) - }) - }) }) diff --git a/server/aws-lsp-codewhisperer/src/language-server/inline-completion/session/sessionManager.ts b/server/aws-lsp-codewhisperer/src/language-server/inline-completion/session/sessionManager.ts index 235c464234..34dbb12538 100644 --- a/server/aws-lsp-codewhisperer/src/language-server/inline-completion/session/sessionManager.ts +++ b/server/aws-lsp-codewhisperer/src/language-server/inline-completion/session/sessionManager.ts @@ -282,7 +282,6 @@ export class SessionManager { private currentSession?: CodeWhispererSession private sessionsLog: CodeWhispererSession[] = [] private maxHistorySize = 5 - streakLength: number = 0 // TODO, for user decision telemetry: accepted suggestions (not necessarily the full corresponding session) should be stored for 5 minutes private constructor() {} @@ -362,16 +361,4 @@ export class SessionManager { this.currentSession.activate() } } - - getAndUpdateStreakLength(isAccepted: boolean | undefined): number { - if (!isAccepted && this.streakLength != 0) { - const currentStreakLength = this.streakLength - this.streakLength = 0 - return currentStreakLength - } else if (isAccepted) { - // increment streakLength everytime a suggestion is accepted. - this.streakLength = this.streakLength + 1 - } - return -1 - } } diff --git a/server/aws-lsp-codewhisperer/src/language-server/inline-completion/tracker/streakTracker.test.ts b/server/aws-lsp-codewhisperer/src/language-server/inline-completion/tracker/streakTracker.test.ts new file mode 100644 index 0000000000..4c69879115 --- /dev/null +++ b/server/aws-lsp-codewhisperer/src/language-server/inline-completion/tracker/streakTracker.test.ts @@ -0,0 +1,85 @@ +/*! + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +import * as assert from 'assert' +import { StreakTracker } from './streakTracker' + +describe('StreakTracker', function () { + let tracker: StreakTracker + + beforeEach(function () { + StreakTracker.reset() + tracker = StreakTracker.getInstance() + }) + + afterEach(function () { + StreakTracker.reset() + }) + + describe('getInstance', function () { + it('should return the same instance (singleton)', function () { + const instance1 = StreakTracker.getInstance() + const instance2 = StreakTracker.getInstance() + assert.strictEqual(instance1, instance2) + }) + + it('should create new instance after reset', function () { + const instance1 = StreakTracker.getInstance() + StreakTracker.reset() + const instance2 = StreakTracker.getInstance() + assert.notStrictEqual(instance1, instance2) + }) + }) + + describe('getAndUpdateStreakLength', function () { + it('should return -1 for undefined input', function () { + const result = tracker.getAndUpdateStreakLength(undefined) + assert.strictEqual(result, -1) + }) + + it('should return -1 and increment streak on acceptance', function () { + const result = tracker.getAndUpdateStreakLength(true) + assert.strictEqual(result, -1) + }) + + it('should return -1 for rejection with zero streak', function () { + const result = tracker.getAndUpdateStreakLength(false) + assert.strictEqual(result, -1) + }) + + it('should return previous streak on rejection after acceptances', function () { + tracker.getAndUpdateStreakLength(true) + tracker.getAndUpdateStreakLength(true) + tracker.getAndUpdateStreakLength(true) + + const result = tracker.getAndUpdateStreakLength(false) + assert.strictEqual(result, 3) + }) + + it('should handle acceptance after rejection', function () { + tracker.getAndUpdateStreakLength(true) + tracker.getAndUpdateStreakLength(true) + + const resetResult = tracker.getAndUpdateStreakLength(false) + assert.strictEqual(resetResult, 2) + + tracker.getAndUpdateStreakLength(true) + const newResult = tracker.getAndUpdateStreakLength(true) + assert.strictEqual(newResult, -1) + }) + }) + + describe('cross-instance consistency', function () { + it('should maintain state across getInstance calls', function () { + const tracker1 = StreakTracker.getInstance() + tracker1.getAndUpdateStreakLength(true) + tracker1.getAndUpdateStreakLength(true) + + const tracker2 = StreakTracker.getInstance() + const result = tracker2.getAndUpdateStreakLength(false) + assert.strictEqual(result, 2) + }) + }) +}) diff --git a/server/aws-lsp-codewhisperer/src/language-server/inline-completion/tracker/streakTracker.ts b/server/aws-lsp-codewhisperer/src/language-server/inline-completion/tracker/streakTracker.ts new file mode 100644 index 0000000000..21d56c4d74 --- /dev/null +++ b/server/aws-lsp-codewhisperer/src/language-server/inline-completion/tracker/streakTracker.ts @@ -0,0 +1,42 @@ +/*! + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +/** + * Tracks acceptance streak across both completion and edit suggestion types. + * Shared singleton to maintain consistent streak count between different code paths. + */ +export class StreakTracker { + private static _instance?: StreakTracker + private streakLength: number = 0 + + private constructor() {} + + public static getInstance(): StreakTracker { + if (!StreakTracker._instance) { + StreakTracker._instance = new StreakTracker() + } + return StreakTracker._instance + } + + public static reset() { + StreakTracker._instance = undefined + } + + /** + * Updates and returns the current streak length based on acceptance status. + * @param isAccepted Whether the suggestion was accepted + * @returns Current streak length before update, or -1 if no change + */ + public getAndUpdateStreakLength(isAccepted: boolean | undefined): number { + if (!isAccepted && this.streakLength !== 0) { + const currentStreakLength = this.streakLength + this.streakLength = 0 + return currentStreakLength + } else if (isAccepted) { + this.streakLength += 1 + } + return -1 + } +} From 71b35952333e7581921644ce40fabbc1e6d3c02f Mon Sep 17 00:00:00 2001 From: Boyu Date: Fri, 22 Aug 2025 14:33:11 -0700 Subject: [PATCH 37/74] feat: disable pkce flow during plugin load (#2153) --- .../src/language-server/agenticChat/errors.ts | 1 + .../agenticChat/tools/mcp/mcpManager.ts | 31 +++++++--- .../agenticChat/tools/mcp/mcpOauthClient.ts | 59 ++++++++++++++++--- 3 files changed, 77 insertions(+), 14 deletions(-) diff --git a/server/aws-lsp-codewhisperer/src/language-server/agenticChat/errors.ts b/server/aws-lsp-codewhisperer/src/language-server/agenticChat/errors.ts index 3038269463..819211dfab 100644 --- a/server/aws-lsp-codewhisperer/src/language-server/agenticChat/errors.ts +++ b/server/aws-lsp-codewhisperer/src/language-server/agenticChat/errors.ts @@ -11,6 +11,7 @@ type AgenticChatErrorCode = | 'MCPServerInitTimeout' // mcp server failed to start within allowed time | 'MCPToolExecTimeout' // mcp tool call failed to complete within allowed time | 'MCPServerConnectionFailed' // mcp server failed to connect + | 'MCPServerAuthFailed' // mcp server failed to complete auth flow | 'RequestAborted' // request was aborted by the user | 'RequestThrottled' // request was aborted by the user diff --git a/server/aws-lsp-codewhisperer/src/language-server/agenticChat/tools/mcp/mcpManager.ts b/server/aws-lsp-codewhisperer/src/language-server/agenticChat/tools/mcp/mcpManager.ts index 7ac6257641..555bdb2c89 100644 --- a/server/aws-lsp-codewhisperer/src/language-server/agenticChat/tools/mcp/mcpManager.ts +++ b/server/aws-lsp-codewhisperer/src/language-server/agenticChat/tools/mcp/mcpManager.ts @@ -42,6 +42,10 @@ import { OAuthClient } from './mcpOauthClient' export const MCP_SERVER_STATUS_CHANGED = 'mcpServerStatusChanged' export const AGENT_TOOLS_CHANGED = 'agentToolsChanged' +export enum AuthIntent { + Interactive = 'interactive', + Silent = 'silent', +} /** * Manages MCP servers and their tools @@ -166,7 +170,7 @@ export class McpManager { /** * Load configurations and initialize each enabled server. */ - private async discoverAllServers(): Promise { + private async discoverAllServers(authIntent: AuthIntent = AuthIntent.Silent): Promise { // Load agent config const result = await loadAgentConfig(this.features.workspace, this.features.logging, this.agentPaths) @@ -217,7 +221,7 @@ export class McpManager { // Process servers in batches for (let i = 0; i < totalServers; i += MAX_CONCURRENT_SERVERS) { const batch = serversToInit.slice(i, i + MAX_CONCURRENT_SERVERS) - const batchPromises = batch.map(([name, cfg]) => this.initOneServer(name, cfg)) + const batchPromises = batch.map(([name, cfg]) => this.initOneServer(name, cfg, authIntent)) this.features.logging.debug( `MCP: initializing batch of ${batch.length} servers (${i + 1}-${Math.min(i + MAX_CONCURRENT_SERVERS, totalServers)} of ${totalServers})` @@ -292,7 +296,11 @@ export class McpManager { * Start a server process, connect client, and register its tools. * Errors are logged but do not stop discovery of other servers. */ - private async initOneServer(serverName: string, cfg: MCPServerConfig): Promise { + private async initOneServer( + serverName: string, + cfg: MCPServerConfig, + authIntent: AuthIntent = AuthIntent.Silent + ): Promise { const DEFAULT_SERVER_INIT_TIMEOUT_MS = 60_000 this.setState(serverName, McpServerStatus.INITIALIZING, 0) @@ -365,10 +373,19 @@ export class McpManager { if (needsOAuth) { OAuthClient.initialize(this.features.workspace, this.features.logging) - const bearer = await OAuthClient.getValidAccessToken(base) + const bearer = await OAuthClient.getValidAccessToken(base, { + interactive: authIntent === 'interactive', + }) // add authorization header if we are able to obtain a bearer token if (bearer) { headers = { ...headers, Authorization: `Bearer ${bearer}` } + } else if (authIntent === 'silent') { + // In silent mode we never launch a browser. If we cannot obtain a token + // from cache/refresh, surface a clear auth-required error and stop here. + throw new AgenticChatError( + `MCP: server '${serverName}' requires OAuth. Open "Edit MCP Server" and save to sign in.`, + 'MCPServerAuthFailed' + ) } } @@ -707,7 +724,7 @@ export class McpManager { await saveAgentConfig(this.features.workspace, this.features.logging, this.agentConfig, agentPath) // Add server tools to tools list after initialization - await this.initOneServer(sanitizedName, newCfg) + await this.initOneServer(sanitizedName, newCfg, AuthIntent.Interactive) } catch (err) { this.features.logging.error( `Failed to add MCP server '${serverName}': ${err instanceof Error ? err.message : String(err)}` @@ -872,7 +889,7 @@ export class McpManager { this.setState(serverName, McpServerStatus.DISABLED, 0) this.emitToolsChanged(serverName) } else { - await this.initOneServer(serverName, newCfg) + await this.initOneServer(serverName, newCfg, AuthIntent.Interactive) } } catch (err) { this.handleError(serverName, err) @@ -1086,7 +1103,7 @@ export class McpManager { this.setState(serverName, McpServerStatus.DISABLED, 0) } else { if (!this.clients.has(serverName) && serverName !== 'Built-in') { - await this.initOneServer(serverName, this.mcpServers.get(serverName)!) + await this.initOneServer(serverName, this.mcpServers.get(serverName)!, AuthIntent.Silent) } } diff --git a/server/aws-lsp-codewhisperer/src/language-server/agenticChat/tools/mcp/mcpOauthClient.ts b/server/aws-lsp-codewhisperer/src/language-server/agenticChat/tools/mcp/mcpOauthClient.ts index 73ffea1a1d..86bc7ba027 100644 --- a/server/aws-lsp-codewhisperer/src/language-server/agenticChat/tools/mcp/mcpOauthClient.ts +++ b/server/aws-lsp-codewhisperer/src/language-server/agenticChat/tools/mcp/mcpOauthClient.ts @@ -42,15 +42,55 @@ export class OAuthClient { /** * Return a valid Bearer token, reusing cache or refresh-token if possible, - * otherwise driving one interactive PKCE flow. + * otherwise (when interactive) driving one PKCE flow that may launch a browser. */ - public static async getValidAccessToken(mcpBase: URL): Promise { + public static async getValidAccessToken( + mcpBase: URL, + opts: { interactive?: boolean } = { interactive: true } + ): Promise { + const interactive = opts?.interactive !== false const key = this.computeKey(mcpBase) const regPath = path.join(this.cacheDir, `${key}.registration.json`) const tokPath = path.join(this.cacheDir, `${key}.token.json`) + // ===== Silent branch: try cached token, then refresh, never opens a browser ===== + if (!interactive) { + // 1) cached access token + const cachedTok = await this.read(tokPath) + if (cachedTok) { + const expiry = cachedTok.obtained_at + cachedTok.expires_in * 1000 + if (Date.now() < expiry) { + this.logger.info(`OAuth: using still-valid cached token (silent)`) + return cachedTok.access_token + } + this.logger.info(`OAuth: cached token expired → try refresh (silent)`) + } + + // 2) refresh-token grant (if we have registration and refresh token) + const savedReg = await this.read(regPath) + if (cachedTok?.refresh_token && savedReg) { + try { + const meta = await this.discoverAS(mcpBase) + const refreshed = await this.refreshGrant(meta, savedReg, mcpBase, cachedTok.refresh_token) + if (refreshed) { + await this.write(tokPath, refreshed) + this.logger.info(`OAuth: refresh grant succeeded (silent)`) + return refreshed.access_token + } + this.logger.info(`OAuth: refresh grant did not succeed (silent)`) + } catch (e) { + this.logger.warn(`OAuth: silent refresh failed — ${e instanceof Error ? e.message : String(e)}`) + } + } + + // 3) no token in silent mode → caller should surface auth-required UI + return undefined + } + + // ===== Interactive branch: may open a browser (PKCE) ===== // 1) Spin up (or reuse) loopback server + redirect URI - let server: http.Server, redirectUri: string + let server: http.Server | null = null + let redirectUri: string const savedReg = await this.read(regPath) if (savedReg) { const port = Number(new URL(savedReg.redirect_uri).port) @@ -75,7 +115,9 @@ export class OAuthClient { } } } else { - ;({ server, redirectUri } = await this.buildCallbackServer()) + const created = await this.buildCallbackServer() + server = created.server + redirectUri = created.redirectUri this.logger.info(`OAuth: new redirect URI ${redirectUri}`) } @@ -85,7 +127,7 @@ export class OAuthClient { if (cached) { const expiry = cached.obtained_at + cached.expires_in * 1000 if (Date.now() < expiry) { - this.logger.info(`OAuth: using still‑valid cached token`) + this.logger.info(`OAuth: using still-valid cached token`) return cached.access_token } this.logger.info(`OAuth: cached token expired → try refresh`) @@ -98,6 +140,7 @@ export class OAuthClient { } catch (e: any) { throw new Error(`OAuth discovery failed: ${e?.message ?? String(e)}`) } + // 4) Register (or reuse) a dynamic client const scopes = ['openid', 'offline_access'] let reg: Registration @@ -107,7 +150,7 @@ export class OAuthClient { throw new Error(`OAuth client registration failed: ${e?.message ?? String(e)}`) } - // 5) Refresh‑token grant (one shot) + // 5) Refresh-token grant (one shot) const attemptedRefresh = !!cached?.refresh_token if (cached?.refresh_token) { const refreshed = await this.refreshGrant(meta, reg, mcpBase, cached.refresh_token) @@ -129,7 +172,9 @@ export class OAuthClient { throw new Error(`OAuth authorization (PKCE) failed${suffix}: ${e?.message ?? String(e)}`) } } finally { - await new Promise(res => server.close(() => res())) + if (server) { + await new Promise(res => server!.close(() => res())) + } } } From 7296f9350950a3e28f612d3a9bb75567a6f6a41d Mon Sep 17 00:00:00 2001 From: chungjac Date: Mon, 25 Aug 2025 10:26:21 -0700 Subject: [PATCH 38/74] chore: bump runtimes to 0.2.127 (#2156) --- app/aws-lsp-antlr4-runtimes/package.json | 2 +- app/aws-lsp-buildspec-runtimes/package.json | 2 +- .../package.json | 2 +- .../package.json | 2 +- app/aws-lsp-identity-runtimes/package.json | 2 +- app/aws-lsp-json-runtimes/package.json | 2 +- .../package.json | 2 +- app/aws-lsp-s3-runtimes/package.json | 2 +- app/aws-lsp-yaml-json-webworker/package.json | 2 +- app/aws-lsp-yaml-runtimes/package.json | 2 +- app/hello-world-lsp-runtimes/package.json | 2 +- chat-client/package.json | 2 +- client/vscode/package.json | 2 +- core/aws-lsp-core/package.json | 2 +- .../q-agentic-chat-server/package.json | 2 +- package-lock.json | 61 +++++++++---------- server/aws-lsp-antlr4/package.json | 2 +- server/aws-lsp-buildspec/package.json | 2 +- server/aws-lsp-cloudformation/package.json | 2 +- server/aws-lsp-codewhisperer/package.json | 2 +- server/aws-lsp-identity/package.json | 2 +- server/aws-lsp-json/package.json | 2 +- server/aws-lsp-notification/package.json | 2 +- server/aws-lsp-partiql/package.json | 2 +- server/aws-lsp-s3/package.json | 2 +- server/aws-lsp-yaml/package.json | 2 +- server/device-sso-auth-lsp/package.json | 2 +- server/hello-world-lsp/package.json | 2 +- 28 files changed, 57 insertions(+), 58 deletions(-) diff --git a/app/aws-lsp-antlr4-runtimes/package.json b/app/aws-lsp-antlr4-runtimes/package.json index 30b9b57a65..89fa95dcb3 100644 --- a/app/aws-lsp-antlr4-runtimes/package.json +++ b/app/aws-lsp-antlr4-runtimes/package.json @@ -12,7 +12,7 @@ "webpack": "webpack" }, "dependencies": { - "@aws/language-server-runtimes": "^0.2.126", + "@aws/language-server-runtimes": "^0.2.127", "@aws/lsp-antlr4": "*", "antlr4-c3": "^3.4.1", "antlr4ng": "^3.0.4" diff --git a/app/aws-lsp-buildspec-runtimes/package.json b/app/aws-lsp-buildspec-runtimes/package.json index 47a88b907a..0ad07ddb8f 100644 --- a/app/aws-lsp-buildspec-runtimes/package.json +++ b/app/aws-lsp-buildspec-runtimes/package.json @@ -7,7 +7,7 @@ "compile": "tsc --build" }, "dependencies": { - "@aws/language-server-runtimes": "^0.2.126", + "@aws/language-server-runtimes": "^0.2.127", "@aws/lsp-buildspec": "^0.0.1" } } diff --git a/app/aws-lsp-cloudformation-runtimes/package.json b/app/aws-lsp-cloudformation-runtimes/package.json index 68547449cc..d211149d0d 100644 --- a/app/aws-lsp-cloudformation-runtimes/package.json +++ b/app/aws-lsp-cloudformation-runtimes/package.json @@ -7,7 +7,7 @@ "compile": "tsc --build" }, "dependencies": { - "@aws/language-server-runtimes": "^0.2.126", + "@aws/language-server-runtimes": "^0.2.127", "@aws/lsp-cloudformation": "^0.0.1" } } diff --git a/app/aws-lsp-codewhisperer-runtimes/package.json b/app/aws-lsp-codewhisperer-runtimes/package.json index 24e06d87ea..b890c01a78 100644 --- a/app/aws-lsp-codewhisperer-runtimes/package.json +++ b/app/aws-lsp-codewhisperer-runtimes/package.json @@ -23,7 +23,7 @@ "local-build": "node scripts/local-build.js" }, "dependencies": { - "@aws/language-server-runtimes": "^0.2.126", + "@aws/language-server-runtimes": "^0.2.127", "@aws/lsp-codewhisperer": "*", "copyfiles": "^2.4.1", "cross-env": "^7.0.3", diff --git a/app/aws-lsp-identity-runtimes/package.json b/app/aws-lsp-identity-runtimes/package.json index 2bf37011c3..f33fa80da4 100644 --- a/app/aws-lsp-identity-runtimes/package.json +++ b/app/aws-lsp-identity-runtimes/package.json @@ -7,7 +7,7 @@ "compile": "tsc --build" }, "dependencies": { - "@aws/language-server-runtimes": "^0.2.126", + "@aws/language-server-runtimes": "^0.2.127", "@aws/lsp-identity": "^0.0.1" } } diff --git a/app/aws-lsp-json-runtimes/package.json b/app/aws-lsp-json-runtimes/package.json index 8a2ea3203a..db1949f54b 100644 --- a/app/aws-lsp-json-runtimes/package.json +++ b/app/aws-lsp-json-runtimes/package.json @@ -11,7 +11,7 @@ "webpack": "webpack" }, "dependencies": { - "@aws/language-server-runtimes": "^0.2.126", + "@aws/language-server-runtimes": "^0.2.127", "@aws/lsp-json": "*" }, "devDependencies": { diff --git a/app/aws-lsp-notification-runtimes/package.json b/app/aws-lsp-notification-runtimes/package.json index b1877ae13e..a5eeecae49 100644 --- a/app/aws-lsp-notification-runtimes/package.json +++ b/app/aws-lsp-notification-runtimes/package.json @@ -7,7 +7,7 @@ "compile": "tsc --build" }, "dependencies": { - "@aws/language-server-runtimes": "^0.2.126", + "@aws/language-server-runtimes": "^0.2.127", "@aws/lsp-notification": "^0.0.1" } } diff --git a/app/aws-lsp-s3-runtimes/package.json b/app/aws-lsp-s3-runtimes/package.json index a242281ff2..9feb4f7ddc 100644 --- a/app/aws-lsp-s3-runtimes/package.json +++ b/app/aws-lsp-s3-runtimes/package.json @@ -10,7 +10,7 @@ "compile": "tsc --build" }, "dependencies": { - "@aws/language-server-runtimes": "^0.2.126", + "@aws/language-server-runtimes": "^0.2.127", "@aws/lsp-s3": "^0.0.1" } } diff --git a/app/aws-lsp-yaml-json-webworker/package.json b/app/aws-lsp-yaml-json-webworker/package.json index 8eebf1ea84..fc524cabdc 100644 --- a/app/aws-lsp-yaml-json-webworker/package.json +++ b/app/aws-lsp-yaml-json-webworker/package.json @@ -11,7 +11,7 @@ "serve:webpack": "NODE_ENV=development webpack serve" }, "dependencies": { - "@aws/language-server-runtimes": "^0.2.126", + "@aws/language-server-runtimes": "^0.2.127", "@aws/lsp-json": "*", "@aws/lsp-yaml": "*" }, diff --git a/app/aws-lsp-yaml-runtimes/package.json b/app/aws-lsp-yaml-runtimes/package.json index 1428ef3058..09bb93ee9a 100644 --- a/app/aws-lsp-yaml-runtimes/package.json +++ b/app/aws-lsp-yaml-runtimes/package.json @@ -11,7 +11,7 @@ "webpack": "webpack" }, "dependencies": { - "@aws/language-server-runtimes": "^0.2.126", + "@aws/language-server-runtimes": "^0.2.127", "@aws/lsp-yaml": "*" }, "devDependencies": { diff --git a/app/hello-world-lsp-runtimes/package.json b/app/hello-world-lsp-runtimes/package.json index 352bf82f88..bff976d9b3 100644 --- a/app/hello-world-lsp-runtimes/package.json +++ b/app/hello-world-lsp-runtimes/package.json @@ -15,7 +15,7 @@ }, "dependencies": { "@aws/hello-world-lsp": "^0.0.1", - "@aws/language-server-runtimes": "^0.2.126" + "@aws/language-server-runtimes": "^0.2.127" }, "devDependencies": { "@types/chai": "^4.3.5", diff --git a/chat-client/package.json b/chat-client/package.json index 1af265fdfe..59720a0187 100644 --- a/chat-client/package.json +++ b/chat-client/package.json @@ -25,7 +25,7 @@ }, "dependencies": { "@aws/chat-client-ui-types": "^0.1.56", - "@aws/language-server-runtimes": "^0.2.126", + "@aws/language-server-runtimes": "^0.2.127", "@aws/language-server-runtimes-types": "^0.1.50", "@aws/mynah-ui": "^4.36.4" }, diff --git a/client/vscode/package.json b/client/vscode/package.json index 4cb42a0752..a5d7f66d21 100644 --- a/client/vscode/package.json +++ b/client/vscode/package.json @@ -352,7 +352,7 @@ "@aws-sdk/credential-providers": "^3.731.1", "@aws-sdk/types": "^3.734.0", "@aws/chat-client-ui-types": "^0.1.56", - "@aws/language-server-runtimes": "^0.2.126", + "@aws/language-server-runtimes": "^0.2.127", "@types/uuid": "^9.0.8", "@types/vscode": "^1.98.0", "jose": "^5.2.4", diff --git a/core/aws-lsp-core/package.json b/core/aws-lsp-core/package.json index 4ecdd4e238..54ca980fc1 100644 --- a/core/aws-lsp-core/package.json +++ b/core/aws-lsp-core/package.json @@ -28,7 +28,7 @@ "prepack": "shx cp ../../LICENSE ../../NOTICE ../../SECURITY.md ." }, "dependencies": { - "@aws/language-server-runtimes": "^0.2.126", + "@aws/language-server-runtimes": "^0.2.127", "@gerhobbelt/gitignore-parser": "^0.2.0-9", "cross-spawn": "7.0.6", "jose": "^5.2.4", diff --git a/integration-tests/q-agentic-chat-server/package.json b/integration-tests/q-agentic-chat-server/package.json index 7415e1653e..ffb33f1a4e 100644 --- a/integration-tests/q-agentic-chat-server/package.json +++ b/integration-tests/q-agentic-chat-server/package.json @@ -9,7 +9,7 @@ "test-integ": "npm run compile && mocha --timeout 30000 \"./out/**/*.test.js\" --retries 2" }, "dependencies": { - "@aws/language-server-runtimes": "^0.2.126", + "@aws/language-server-runtimes": "^0.2.127", "@aws/lsp-core": "*" }, "devDependencies": { diff --git a/package-lock.json b/package-lock.json index 8dbdc6d513..e65f8e8c96 100644 --- a/package-lock.json +++ b/package-lock.json @@ -48,7 +48,7 @@ "name": "@aws/lsp-antlr4-runtimes", "version": "0.0.1", "dependencies": { - "@aws/language-server-runtimes": "^0.2.126", + "@aws/language-server-runtimes": "^0.2.127", "@aws/lsp-antlr4": "*", "antlr4-c3": "^3.4.1", "antlr4ng": "^3.0.4" @@ -71,7 +71,7 @@ "name": "@aws/lsp-buildspec-runtimes", "version": "0.0.1", "dependencies": { - "@aws/language-server-runtimes": "^0.2.126", + "@aws/language-server-runtimes": "^0.2.127", "@aws/lsp-buildspec": "^0.0.1" } }, @@ -79,7 +79,7 @@ "name": "@aws/lsp-cloudformation-runtimes", "version": "0.0.1", "dependencies": { - "@aws/language-server-runtimes": "^0.2.126", + "@aws/language-server-runtimes": "^0.2.127", "@aws/lsp-cloudformation": "^0.0.1" } }, @@ -87,7 +87,7 @@ "name": "@aws/lsp-codewhisperer-runtimes", "version": "0.0.1", "dependencies": { - "@aws/language-server-runtimes": "^0.2.126", + "@aws/language-server-runtimes": "^0.2.127", "@aws/lsp-codewhisperer": "*", "copyfiles": "^2.4.1", "cross-env": "^7.0.3", @@ -120,7 +120,7 @@ "name": "@aws/lsp-identity-runtimes", "version": "0.1.0", "dependencies": { - "@aws/language-server-runtimes": "^0.2.126", + "@aws/language-server-runtimes": "^0.2.127", "@aws/lsp-identity": "^0.0.1" } }, @@ -128,7 +128,7 @@ "name": "@aws/lsp-json-runtimes", "version": "0.0.1", "dependencies": { - "@aws/language-server-runtimes": "^0.2.126", + "@aws/language-server-runtimes": "^0.2.127", "@aws/lsp-json": "*" }, "devDependencies": { @@ -148,7 +148,7 @@ "name": "@aws/lsp-notification-runtimes", "version": "0.1.0", "dependencies": { - "@aws/language-server-runtimes": "^0.2.126", + "@aws/language-server-runtimes": "^0.2.127", "@aws/lsp-notification": "^0.0.1" } }, @@ -181,7 +181,7 @@ "name": "@aws/lsp-s3-runtimes", "version": "0.0.1", "dependencies": { - "@aws/language-server-runtimes": "^0.2.126", + "@aws/language-server-runtimes": "^0.2.127", "@aws/lsp-s3": "^0.0.1" }, "bin": { @@ -192,7 +192,7 @@ "name": "@aws/lsp-yaml-json-webworker", "version": "0.0.1", "dependencies": { - "@aws/language-server-runtimes": "^0.2.126", + "@aws/language-server-runtimes": "^0.2.127", "@aws/lsp-json": "*", "@aws/lsp-yaml": "*" }, @@ -212,7 +212,7 @@ "name": "@aws/lsp-yaml-runtimes", "version": "0.0.1", "dependencies": { - "@aws/language-server-runtimes": "^0.2.126", + "@aws/language-server-runtimes": "^0.2.127", "@aws/lsp-yaml": "*" }, "devDependencies": { @@ -234,7 +234,7 @@ "version": "0.0.1", "dependencies": { "@aws/hello-world-lsp": "^0.0.1", - "@aws/language-server-runtimes": "^0.2.126" + "@aws/language-server-runtimes": "^0.2.127" }, "devDependencies": { "@types/chai": "^4.3.5", @@ -255,7 +255,7 @@ "license": "Apache-2.0", "dependencies": { "@aws/chat-client-ui-types": "^0.1.56", - "@aws/language-server-runtimes": "^0.2.126", + "@aws/language-server-runtimes": "^0.2.127", "@aws/language-server-runtimes-types": "^0.1.50", "@aws/mynah-ui": "^4.36.4" }, @@ -280,7 +280,7 @@ "@aws-sdk/credential-providers": "^3.731.1", "@aws-sdk/types": "^3.734.0", "@aws/chat-client-ui-types": "^0.1.56", - "@aws/language-server-runtimes": "^0.2.126", + "@aws/language-server-runtimes": "^0.2.127", "@types/uuid": "^9.0.8", "@types/vscode": "^1.98.0", "jose": "^5.2.4", @@ -296,7 +296,7 @@ "version": "0.0.14", "license": "Apache-2.0", "dependencies": { - "@aws/language-server-runtimes": "^0.2.126", + "@aws/language-server-runtimes": "^0.2.127", "@gerhobbelt/gitignore-parser": "^0.2.0-9", "cross-spawn": "7.0.6", "jose": "^5.2.4", @@ -327,7 +327,7 @@ "name": "@aws/q-agentic-chat-server-integration-tests", "version": "0.0.1", "dependencies": { - "@aws/language-server-runtimes": "^0.2.126", + "@aws/language-server-runtimes": "^0.2.127", "@aws/lsp-core": "*" }, "devDependencies": { @@ -4036,10 +4036,9 @@ "link": true }, "node_modules/@aws/language-server-runtimes": { - "version": "0.2.126", - "resolved": "https://registry.npmjs.org/@aws/language-server-runtimes/-/language-server-runtimes-0.2.126.tgz", - "integrity": "sha512-dUIKTL6+AOxdberwHLvigSJcbhFv6oUS3POhZWoNlBV9XJZRWwzNW9gkjkUsI03YTVshqMuVHT/HaoRW/hDkIA==", - "license": "Apache-2.0", + "version": "0.2.127", + "resolved": "https://registry.npmjs.org/@aws/language-server-runtimes/-/language-server-runtimes-0.2.127.tgz", + "integrity": "sha512-UWCfv49MYaBhxArVBWTEw2XVfIyunbm6EfS9AxSLPudcwrpOg3KAVLooXearmyM/r2hgNDGCQYI8HuKf5FAnew==", "dependencies": { "@aws/language-server-runtimes-types": "^0.1.56", "@opentelemetry/api": "^1.9.0", @@ -28608,7 +28607,7 @@ "version": "0.1.18", "license": "Apache-2.0", "dependencies": { - "@aws/language-server-runtimes": "^0.2.126", + "@aws/language-server-runtimes": "^0.2.127", "@aws/lsp-core": "^0.0.14" }, "devDependencies": { @@ -28650,7 +28649,7 @@ "name": "@aws/lsp-buildspec", "version": "0.0.1", "dependencies": { - "@aws/language-server-runtimes": "^0.2.126", + "@aws/language-server-runtimes": "^0.2.127", "@aws/lsp-json": "*", "@aws/lsp-yaml": "*", "vscode-languageserver": "^9.0.1", @@ -28661,7 +28660,7 @@ "name": "@aws/lsp-cloudformation", "version": "0.0.1", "dependencies": { - "@aws/language-server-runtimes": "^0.2.126", + "@aws/language-server-runtimes": "^0.2.127", "@aws/lsp-core": "*", "@aws/lsp-json": "*", "vscode-languageserver": "^9.0.1", @@ -28683,7 +28682,7 @@ "@aws-sdk/util-arn-parser": "^3.723.0", "@aws-sdk/util-retry": "^3.374.0", "@aws/chat-client-ui-types": "^0.1.56", - "@aws/language-server-runtimes": "^0.2.126", + "@aws/language-server-runtimes": "^0.2.127", "@aws/lsp-core": "^0.0.14", "@modelcontextprotocol/sdk": "^1.15.0", "@smithy/node-http-handler": "^2.5.0", @@ -28823,7 +28822,7 @@ "dependencies": { "@aws-sdk/client-sso-oidc": "^3.616.0", "@aws-sdk/token-providers": "^3.744.0", - "@aws/language-server-runtimes": "^0.2.126", + "@aws/language-server-runtimes": "^0.2.127", "@aws/lsp-core": "^0.0.12", "@smithy/node-http-handler": "^3.2.5", "@smithy/shared-ini-file-loader": "^4.0.1", @@ -28888,7 +28887,7 @@ "version": "0.1.18", "license": "Apache-2.0", "dependencies": { - "@aws/language-server-runtimes": "^0.2.126", + "@aws/language-server-runtimes": "^0.2.127", "@aws/lsp-core": "^0.0.14", "vscode-languageserver": "^9.0.1", "vscode-languageserver-textdocument": "^1.0.8" @@ -28905,7 +28904,7 @@ "version": "0.0.1", "license": "Apache-2.0", "dependencies": { - "@aws/language-server-runtimes": "^0.2.126", + "@aws/language-server-runtimes": "^0.2.127", "@aws/lsp-core": "^0.0.12", "vscode-languageserver": "^9.0.1" }, @@ -28966,7 +28965,7 @@ "version": "0.0.17", "license": "Apache-2.0", "dependencies": { - "@aws/language-server-runtimes": "^0.2.126", + "@aws/language-server-runtimes": "^0.2.127", "antlr4-c3": "3.4.2", "antlr4ng": "3.0.14", "web-tree-sitter": "0.22.6" @@ -28988,7 +28987,7 @@ "dependencies": { "@aws-sdk/client-s3": "^3.623.0", "@aws-sdk/types": "^3.734.0", - "@aws/language-server-runtimes": "^0.2.126", + "@aws/language-server-runtimes": "^0.2.127", "@aws/lsp-core": "^0.0.12", "vscode-languageserver": "^9.0.1", "vscode-languageserver-textdocument": "^1.0.8" @@ -29019,7 +29018,7 @@ "hasInstallScript": true, "license": "Apache-2.0", "dependencies": { - "@aws/language-server-runtimes": "^0.2.126", + "@aws/language-server-runtimes": "^0.2.127", "@aws/lsp-core": "^0.0.14", "vscode-languageserver": "^9.0.1", "vscode-languageserver-textdocument": "^1.0.8", @@ -29033,7 +29032,7 @@ "name": "@amzn/device-sso-auth-lsp", "version": "0.0.1", "dependencies": { - "@aws/language-server-runtimes": "^0.2.126", + "@aws/language-server-runtimes": "^0.2.127", "vscode-languageserver": "^9.0.1" }, "devDependencies": { @@ -29044,7 +29043,7 @@ "name": "@aws/hello-world-lsp", "version": "0.0.1", "dependencies": { - "@aws/language-server-runtimes": "^0.2.126", + "@aws/language-server-runtimes": "^0.2.127", "vscode-languageserver": "^9.0.1" }, "devDependencies": { diff --git a/server/aws-lsp-antlr4/package.json b/server/aws-lsp-antlr4/package.json index b61239d633..2ca49593d7 100644 --- a/server/aws-lsp-antlr4/package.json +++ b/server/aws-lsp-antlr4/package.json @@ -28,7 +28,7 @@ "clean": "rm -rf node_modules" }, "dependencies": { - "@aws/language-server-runtimes": "^0.2.126", + "@aws/language-server-runtimes": "^0.2.127", "@aws/lsp-core": "^0.0.14" }, "peerDependencies": { diff --git a/server/aws-lsp-buildspec/package.json b/server/aws-lsp-buildspec/package.json index 3a23338dac..9754645b7d 100644 --- a/server/aws-lsp-buildspec/package.json +++ b/server/aws-lsp-buildspec/package.json @@ -7,7 +7,7 @@ "compile": "tsc --build" }, "dependencies": { - "@aws/language-server-runtimes": "^0.2.126", + "@aws/language-server-runtimes": "^0.2.127", "@aws/lsp-json": "*", "@aws/lsp-yaml": "*", "vscode-languageserver": "^9.0.1", diff --git a/server/aws-lsp-cloudformation/package.json b/server/aws-lsp-cloudformation/package.json index 75223b4791..13be6a4859 100644 --- a/server/aws-lsp-cloudformation/package.json +++ b/server/aws-lsp-cloudformation/package.json @@ -7,7 +7,7 @@ "compile": "tsc --build" }, "dependencies": { - "@aws/language-server-runtimes": "^0.2.126", + "@aws/language-server-runtimes": "^0.2.127", "@aws/lsp-core": "*", "@aws/lsp-json": "*", "vscode-languageserver": "^9.0.1", diff --git a/server/aws-lsp-codewhisperer/package.json b/server/aws-lsp-codewhisperer/package.json index c0f6c16afd..83ff1cdaee 100644 --- a/server/aws-lsp-codewhisperer/package.json +++ b/server/aws-lsp-codewhisperer/package.json @@ -36,7 +36,7 @@ "@aws-sdk/util-arn-parser": "^3.723.0", "@aws-sdk/util-retry": "^3.374.0", "@aws/chat-client-ui-types": "^0.1.56", - "@aws/language-server-runtimes": "^0.2.126", + "@aws/language-server-runtimes": "^0.2.127", "@aws/lsp-core": "^0.0.14", "@modelcontextprotocol/sdk": "^1.15.0", "@smithy/node-http-handler": "^2.5.0", diff --git a/server/aws-lsp-identity/package.json b/server/aws-lsp-identity/package.json index 0e61d97dc8..9aa4bedcbf 100644 --- a/server/aws-lsp-identity/package.json +++ b/server/aws-lsp-identity/package.json @@ -26,7 +26,7 @@ "dependencies": { "@aws-sdk/client-sso-oidc": "^3.616.0", "@aws-sdk/token-providers": "^3.744.0", - "@aws/language-server-runtimes": "^0.2.126", + "@aws/language-server-runtimes": "^0.2.127", "@aws/lsp-core": "^0.0.12", "@smithy/node-http-handler": "^3.2.5", "@smithy/shared-ini-file-loader": "^4.0.1", diff --git a/server/aws-lsp-json/package.json b/server/aws-lsp-json/package.json index d129734444..ee5fbbc47a 100644 --- a/server/aws-lsp-json/package.json +++ b/server/aws-lsp-json/package.json @@ -26,7 +26,7 @@ "prepack": "shx cp ../../LICENSE ../../NOTICE ../../SECURITY.md ." }, "dependencies": { - "@aws/language-server-runtimes": "^0.2.126", + "@aws/language-server-runtimes": "^0.2.127", "@aws/lsp-core": "^0.0.14", "vscode-languageserver": "^9.0.1", "vscode-languageserver-textdocument": "^1.0.8" diff --git a/server/aws-lsp-notification/package.json b/server/aws-lsp-notification/package.json index f6483e3e32..f19ddc54ca 100644 --- a/server/aws-lsp-notification/package.json +++ b/server/aws-lsp-notification/package.json @@ -22,7 +22,7 @@ "coverage:report": "c8 report --reporter=html --reporter=text" }, "dependencies": { - "@aws/language-server-runtimes": "^0.2.126", + "@aws/language-server-runtimes": "^0.2.127", "@aws/lsp-core": "^0.0.12", "vscode-languageserver": "^9.0.1" }, diff --git a/server/aws-lsp-partiql/package.json b/server/aws-lsp-partiql/package.json index 2a5a5bb73f..dc2fc2ee22 100644 --- a/server/aws-lsp-partiql/package.json +++ b/server/aws-lsp-partiql/package.json @@ -24,7 +24,7 @@ "out" ], "dependencies": { - "@aws/language-server-runtimes": "^0.2.126", + "@aws/language-server-runtimes": "^0.2.127", "antlr4-c3": "3.4.2", "antlr4ng": "3.0.14", "web-tree-sitter": "0.22.6" diff --git a/server/aws-lsp-s3/package.json b/server/aws-lsp-s3/package.json index b574da9bc0..914628d14e 100644 --- a/server/aws-lsp-s3/package.json +++ b/server/aws-lsp-s3/package.json @@ -9,7 +9,7 @@ "dependencies": { "@aws-sdk/client-s3": "^3.623.0", "@aws-sdk/types": "^3.734.0", - "@aws/language-server-runtimes": "^0.2.126", + "@aws/language-server-runtimes": "^0.2.127", "@aws/lsp-core": "^0.0.12", "vscode-languageserver": "^9.0.1", "vscode-languageserver-textdocument": "^1.0.8" diff --git a/server/aws-lsp-yaml/package.json b/server/aws-lsp-yaml/package.json index 7d2b942ab5..fe115132c8 100644 --- a/server/aws-lsp-yaml/package.json +++ b/server/aws-lsp-yaml/package.json @@ -26,7 +26,7 @@ "postinstall": "node patchYamlPackage.js" }, "dependencies": { - "@aws/language-server-runtimes": "^0.2.126", + "@aws/language-server-runtimes": "^0.2.127", "@aws/lsp-core": "^0.0.14", "vscode-languageserver": "^9.0.1", "vscode-languageserver-textdocument": "^1.0.8", diff --git a/server/device-sso-auth-lsp/package.json b/server/device-sso-auth-lsp/package.json index 6eba3692a9..e5085aa6bd 100644 --- a/server/device-sso-auth-lsp/package.json +++ b/server/device-sso-auth-lsp/package.json @@ -7,7 +7,7 @@ "compile": "tsc --build" }, "dependencies": { - "@aws/language-server-runtimes": "^0.2.126", + "@aws/language-server-runtimes": "^0.2.127", "vscode-languageserver": "^9.0.1" }, "devDependencies": { diff --git a/server/hello-world-lsp/package.json b/server/hello-world-lsp/package.json index b221272541..8ab207f8b4 100644 --- a/server/hello-world-lsp/package.json +++ b/server/hello-world-lsp/package.json @@ -13,7 +13,7 @@ "coverage:report": "c8 report --reporter=html --reporter=text" }, "dependencies": { - "@aws/language-server-runtimes": "^0.2.126", + "@aws/language-server-runtimes": "^0.2.127", "vscode-languageserver": "^9.0.1" }, "devDependencies": { From 472220a745cff4fe91a2cabae4ae059a164ceddd Mon Sep 17 00:00:00 2001 From: Boyu Date: Mon, 25 Aug 2025 13:37:00 -0700 Subject: [PATCH 39/74] fix: multiple fixes on auth flow edge cases (#2155) --- .../agenticChat/tools/mcp/mcpEventHandler.ts | 31 ++++++++----- .../agenticChat/tools/mcp/mcpManager.ts | 45 ++++++++++++------- .../tools/mcp/mcpOauthClient.test.ts | 4 +- .../agenticChat/tools/mcp/mcpOauthClient.ts | 33 +++++++++----- 4 files changed, 73 insertions(+), 40 deletions(-) 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 a5090319ba..d167299106 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 @@ -35,6 +35,7 @@ enum TransportType { } export class McpEventHandler { + private static readonly FILE_WATCH_DEBOUNCE_MS = 2000 #features: Features #eventListenerRegistered: boolean #currentEditingServerName: string | undefined @@ -48,6 +49,12 @@ export class McpEventHandler { #lastProgrammaticState: boolean = false #serverNameBeforeUpdate: string | undefined + #releaseProgrammaticAfterDebounce(padMs = 500) { + setTimeout(() => { + this.#isProgrammaticChange = false + }, McpEventHandler.FILE_WATCH_DEBOUNCE_MS + padMs) + } + constructor(features: Features, telemetryService: TelemetryService) { this.#features = features this.#eventListenerRegistered = false @@ -797,7 +804,7 @@ export class McpEventHandler { command: selectedTransport === TransportType.STDIO ? params.optionsValues.command : undefined, url: selectedTransport === TransportType.HTTP ? params.optionsValues.url : undefined, enabled: true, - numTools: McpManager.instance.getAllToolsWithPermissions(serverName).length, + numTools: McpManager.instance.getAllToolsWithPermissions(sanitizedServerName).length, scope: params.optionsValues['scope'] === 'global' ? 'global' : 'workspace', transportType: selectedTransport, languageServerVersion: this.#features.runtime.serverInfo.version, @@ -812,6 +819,7 @@ export class McpEventHandler { // Stay on add/edit page and show error to user // Keep isProgrammaticChange true during error handling to prevent file watcher triggers + this.#releaseProgrammaticAfterDebounce() if (isEditMode) { params.id = 'edit-mcp' params.title = sanitizedServerName @@ -826,7 +834,7 @@ export class McpEventHandler { this.#newlyAddedServers.delete(serverName) } - this.#isProgrammaticChange = false + this.#releaseProgrammaticAfterDebounce() // Go to tools permissions page return this.#handleOpenMcpServer({ id: 'open-mcp-server', title: sanitizedServerName }) @@ -927,9 +935,10 @@ export class McpEventHandler { perm.__configPath__ = agentPath await mcpManager.updateServerPermission(serverName, perm) this.#emitMCPConfigEvent() + this.#releaseProgrammaticAfterDebounce() } catch (error) { this.#features.logging.error(`Failed to enable MCP server: ${error}`) - this.#isProgrammaticChange = false + this.#releaseProgrammaticAfterDebounce() } return { id: params.id } } @@ -953,9 +962,10 @@ export class McpEventHandler { perm.__configPath__ = agentPath await mcpManager.updateServerPermission(serverName, perm) this.#emitMCPConfigEvent() + this.#releaseProgrammaticAfterDebounce() } catch (error) { this.#features.logging.error(`Failed to disable MCP server: ${error}`) - this.#isProgrammaticChange = false + this.#releaseProgrammaticAfterDebounce() } return { id: params.id } @@ -975,11 +985,11 @@ export class McpEventHandler { try { await McpManager.instance.removeServer(serverName) - + this.#releaseProgrammaticAfterDebounce() return { id: params.id } } catch (error) { this.#features.logging.error(`Failed to delete MCP server: ${error}`) - this.#isProgrammaticChange = false + this.#releaseProgrammaticAfterDebounce() return { id: params.id } } } @@ -1262,10 +1272,11 @@ export class McpEventHandler { this.#pendingPermissionConfig = undefined this.#features.logging.info(`Applied permission changes for server: ${serverName}`) + this.#releaseProgrammaticAfterDebounce() return { id: params.id } } catch (error) { this.#features.logging.error(`Failed to save MCP permissions: ${error}`) - this.#isProgrammaticChange = false + this.#releaseProgrammaticAfterDebounce() return { id: params.id } } } @@ -1430,7 +1441,8 @@ export class McpEventHandler { */ #getServerStatusError(serverName: string): { title: string; icon: string; status: Status } | undefined { const serverStates = McpManager.instance.getAllServerStates() - const serverState = serverStates.get(serverName) + const key = serverName ? sanitizeName(serverName) : serverName + const serverState = key ? serverStates.get(key) : undefined if (!serverState) { return undefined @@ -1494,11 +1506,10 @@ export class McpEventHandler { if (!this.#lastProgrammaticState) { await this.#handleRefreshMCPList({ id: 'refresh-mcp-list' }) } else { - this.#isProgrammaticChange = false this.#features.logging.debug('Skipping refresh due to programmatic change') } this.#debounceTimer = null - }, 2000) + }, McpEventHandler.FILE_WATCH_DEBOUNCE_MS) }) } diff --git a/server/aws-lsp-codewhisperer/src/language-server/agenticChat/tools/mcp/mcpManager.ts b/server/aws-lsp-codewhisperer/src/language-server/agenticChat/tools/mcp/mcpManager.ts index 555bdb2c89..ec7d16d277 100644 --- a/server/aws-lsp-codewhisperer/src/language-server/agenticChat/tools/mcp/mcpManager.ts +++ b/server/aws-lsp-codewhisperer/src/language-server/agenticChat/tools/mcp/mcpManager.ts @@ -170,7 +170,7 @@ export class McpManager { /** * Load configurations and initialize each enabled server. */ - private async discoverAllServers(authIntent: AuthIntent = AuthIntent.Silent): Promise { + private async discoverAllServers(): Promise { // Load agent config const result = await loadAgentConfig(this.features.workspace, this.features.logging, this.agentPaths) @@ -221,7 +221,7 @@ export class McpManager { // Process servers in batches for (let i = 0; i < totalServers; i += MAX_CONCURRENT_SERVERS) { const batch = serversToInit.slice(i, i + MAX_CONCURRENT_SERVERS) - const batchPromises = batch.map(([name, cfg]) => this.initOneServer(name, cfg, authIntent)) + const batchPromises = batch.map(([name, cfg]) => this.initOneServer(name, cfg, AuthIntent.Silent)) this.features.logging.debug( `MCP: initializing batch of ${batch.length} servers (${i + 1}-${Math.min(i + MAX_CONCURRENT_SERVERS, totalServers)} of ${totalServers})` @@ -373,19 +373,29 @@ export class McpManager { if (needsOAuth) { OAuthClient.initialize(this.features.workspace, this.features.logging) - const bearer = await OAuthClient.getValidAccessToken(base, { - interactive: authIntent === 'interactive', - }) - // add authorization header if we are able to obtain a bearer token - if (bearer) { - headers = { ...headers, Authorization: `Bearer ${bearer}` } - } else if (authIntent === 'silent') { - // In silent mode we never launch a browser. If we cannot obtain a token - // from cache/refresh, surface a clear auth-required error and stop here. - throw new AgenticChatError( - `MCP: server '${serverName}' requires OAuth. Open "Edit MCP Server" and save to sign in.`, - 'MCPServerAuthFailed' - ) + try { + const bearer = await OAuthClient.getValidAccessToken(base, { + interactive: authIntent === AuthIntent.Interactive, + }) + if (bearer) { + headers = { ...headers, Authorization: `Bearer ${bearer}` } + } else if (authIntent === AuthIntent.Silent) { + throw new AgenticChatError( + `MCP: server '${serverName}' requires OAuth. Open "Edit MCP Server" and save to sign in.`, + 'MCPServerAuthFailed' + ) + } + } catch (e: any) { + const msg = e?.message || '' + const short = /authorization_timed_out/i.test(msg) + ? 'Sign-in timed out. Please try again.' + : /Authorization error|PKCE|access_denied|login|consent|token exchange failed/i.test( + msg + ) + ? 'Sign-in was cancelled or failed. Please try again.' + : `OAuth failed: ${msg}` + + throw new AgenticChatError(`MCP: ${short}`, 'MCPServerAuthFailed') } } @@ -1156,7 +1166,8 @@ export class McpManager { */ public async removeServerFromConfigFile(serverName: string): Promise { try { - const cfg = this.mcpServers.get(serverName) + const sanitized = sanitizeName(serverName) + const cfg = this.mcpServers.get(sanitized) if (!cfg || !cfg.__configPath__) { this.features.logging.warn( `Cannot remove config for server '${serverName}': Config not found or missing path` @@ -1164,7 +1175,7 @@ export class McpManager { return } - const unsanitizedName = this.serverNameMapping.get(serverName) || serverName + const unsanitizedName = this.serverNameMapping.get(sanitized) || serverName // Remove from agent config if (unsanitizedName && this.agentConfig) { diff --git a/server/aws-lsp-codewhisperer/src/language-server/agenticChat/tools/mcp/mcpOauthClient.test.ts b/server/aws-lsp-codewhisperer/src/language-server/agenticChat/tools/mcp/mcpOauthClient.test.ts index 43f68302eb..08f8f1dd5c 100644 --- a/server/aws-lsp-codewhisperer/src/language-server/agenticChat/tools/mcp/mcpOauthClient.test.ts +++ b/server/aws-lsp-codewhisperer/src/language-server/agenticChat/tools/mcp/mcpOauthClient.test.ts @@ -113,7 +113,9 @@ describe('OAuthClient getValidAccessToken()', () => { stubFileSystem(cachedToken, cachedReg) - const token = await OAuthClient.getValidAccessToken(new URL('https://api.example.com/mcp')) + const token = await OAuthClient.getValidAccessToken(new URL('https://api.example.com/mcp'), { + interactive: true, + }) expect(token).to.equal('cached_access') expect((http.createServer as any).calledOnce).to.be.true }) diff --git a/server/aws-lsp-codewhisperer/src/language-server/agenticChat/tools/mcp/mcpOauthClient.ts b/server/aws-lsp-codewhisperer/src/language-server/agenticChat/tools/mcp/mcpOauthClient.ts index 86bc7ba027..1e9c8745d3 100644 --- a/server/aws-lsp-codewhisperer/src/language-server/agenticChat/tools/mcp/mcpOauthClient.ts +++ b/server/aws-lsp-codewhisperer/src/language-server/agenticChat/tools/mcp/mcpOauthClient.ts @@ -9,6 +9,7 @@ import * as path from 'path' import { spawn } from 'child_process' import { URL, URLSearchParams } from 'url' import * as http from 'http' +import * as os from 'os' import { Logger, Workspace } from '@aws/language-server-runtimes/server-interface' interface Token { @@ -46,9 +47,9 @@ export class OAuthClient { */ public static async getValidAccessToken( mcpBase: URL, - opts: { interactive?: boolean } = { interactive: true } + opts: { interactive?: boolean } = { interactive: false } ): Promise { - const interactive = opts?.interactive !== false + const interactive = opts?.interactive === true const key = this.computeKey(mcpBase) const regPath = path.join(this.cacheDir, `${key}.registration.json`) const tokPath = path.join(this.cacheDir, `${key}.token.json`) @@ -333,6 +334,7 @@ export class OAuthClient { redirectUri: string, server: http.Server ): Promise { + const DEFAULT_PKCE_TIMEOUT_MS = 20_000 // a) generate PKCE params const verifier = this.b64url(crypto.randomBytes(32)) const challenge = this.b64url(crypto.createHash('sha256').update(verifier).digest()) @@ -353,7 +355,10 @@ export class OAuthClient { const opener = process.platform === 'win32' - ? { cmd: 'cmd', args: ['/c', 'start', authz.toString()] } + ? { + cmd: 'cmd', + args: ['/c', 'start', '', `"${authz.toString().replace(/"/g, '""')}"`], + } : process.platform === 'darwin' ? { cmd: 'open', args: [authz.toString()] } : { cmd: 'xdg-open', args: [authz.toString()] } @@ -361,17 +366,26 @@ export class OAuthClient { void spawn(opener.cmd, opener.args, { detached: true, stdio: 'ignore' }).unref() // c) wait for code on our loopback - const { code, rxState, err } = await new Promise<{ code: string; rxState: string; err?: string }>(resolve => { + const waitForFlow = new Promise<{ code: string; rxState: string; err?: string; errDesc?: string }>(resolve => { server.on('request', (req, res) => { const u = new URL(req.url || '/', redirectUri) const c = u.searchParams.get('code') || '' const s = u.searchParams.get('state') || '' const e = u.searchParams.get('error') || undefined + const ed = u.searchParams.get('error_description') || undefined res.writeHead(200, { 'content-type': 'text/html' }).end('

You may close this tab.

') - resolve({ code: c, rxState: s, err: e }) + resolve({ code: c, rxState: s, err: e, errDesc: ed }) }) }) - if (err) throw new Error(`Authorization error: ${err}`) + const { code, rxState, err, errDesc } = await Promise.race([ + waitForFlow, + new Promise((_, reject) => + setTimeout(() => reject(new Error('authorization_timed_out')), DEFAULT_PKCE_TIMEOUT_MS) + ), + ]) + if (err) { + throw new Error(`Authorization error: ${err}${errDesc ? ` - ${errDesc}` : ''}`) + } if (!code || rxState !== state) throw new Error('Invalid authorization response (state mismatch)') // d) exchange code for token @@ -438,12 +452,7 @@ export class OAuthClient { } /** Directory for caching registration + tokens */ - private static readonly cacheDir = path.join( - process.env.HOME || process.env.USERPROFILE || '.', - '.aws', - 'sso', - 'cache' - ) + private static readonly cacheDir = path.join(os.homedir(), '.aws', 'sso', 'cache') /** * Await server.listen() but reject if it emits 'error' (eg EADDRINUSE), From b99df82826d0ba1a1d52df578cb80674c90505b9 Mon Sep 17 00:00:00 2001 From: invictus <149003065+ashishrp-aws@users.noreply.github.com> Date: Mon, 25 Aug 2025 14:31:41 -0700 Subject: [PATCH 40/74] feat: update MCP manager and utilities (#2158) Co-authored-by: Boyu --- .../agenticChat/tools/mcp/mcpManager.test.ts | 15 ++- .../agenticChat/tools/mcp/mcpManager.ts | 24 ---- .../agenticChat/tools/mcp/mcpUtils.test.ts | 6 + .../agenticChat/tools/mcp/mcpUtils.ts | 114 +++++++----------- 4 files changed, 56 insertions(+), 103 deletions(-) diff --git a/server/aws-lsp-codewhisperer/src/language-server/agenticChat/tools/mcp/mcpManager.test.ts b/server/aws-lsp-codewhisperer/src/language-server/agenticChat/tools/mcp/mcpManager.test.ts index 7b239abad0..456b8934df 100644 --- a/server/aws-lsp-codewhisperer/src/language-server/agenticChat/tools/mcp/mcpManager.test.ts +++ b/server/aws-lsp-codewhisperer/src/language-server/agenticChat/tools/mcp/mcpManager.test.ts @@ -368,7 +368,7 @@ describe('removeServer()', () => { expect((mgr as any).clients.has('x')).to.be.false }) - it('removes server from all config files', async () => { + it('removes server from agent config', async () => { const mgr = await McpManager.init([], features) const dummy = new Client({ name: 'c', version: 'v' }) ;(mgr as any).clients.set('x', dummy) @@ -395,14 +395,13 @@ describe('removeServer()', () => { await mgr.removeServer('x') - // Verify that writeFile was called for each config path (2 workspace + 1 global) - expect(writeFileStub.callCount).to.equal(3) + // Verify that saveAgentConfig was called + expect(saveAgentConfigStub.calledOnce).to.be.true + expect((mgr as any).clients.has('x')).to.be.false - // Verify the content of the writes (should have removed the server) - writeFileStub.getCalls().forEach(call => { - const content = JSON.parse(call.args[1]) - expect(content.mcpServers).to.not.have.property('x') - }) + // Verify server was removed from agent config + expect((mgr as any).agentConfig.mcpServers).to.not.have.property('x') + expect((mgr as any).agentConfig.tools).to.not.include('@x') }) }) diff --git a/server/aws-lsp-codewhisperer/src/language-server/agenticChat/tools/mcp/mcpManager.ts b/server/aws-lsp-codewhisperer/src/language-server/agenticChat/tools/mcp/mcpManager.ts index ec7d16d277..b66661c1ea 100644 --- a/server/aws-lsp-codewhisperer/src/language-server/agenticChat/tools/mcp/mcpManager.ts +++ b/server/aws-lsp-codewhisperer/src/language-server/agenticChat/tools/mcp/mcpManager.ts @@ -796,30 +796,6 @@ export class McpManager { // Save agent config await saveAgentConfig(this.features.workspace, this.features.logging, this.agentConfig, cfg.__configPath__) - - // Get all config paths and delete the server from each one - const wsUris = this.features.workspace.getAllWorkspaceFolders()?.map(f => f.uri) ?? [] - const wsConfigPaths = getWorkspaceMcpConfigPaths(wsUris) - const globalConfigPath = getGlobalMcpConfigPath(this.features.workspace.fs.getUserHomeDir()) - const allConfigPaths = [...wsConfigPaths, globalConfigPath] - - // Delete the server from all config files - for (const configPath of allConfigPaths) { - try { - await this.mutateConfigFile(configPath, json => { - if (json.mcpServers && json.mcpServers[unsanitizedName]) { - delete json.mcpServers[unsanitizedName] - this.features.logging.info( - `Deleted server '${unsanitizedName}' from config file: ${configPath}` - ) - } - }) - } catch (err) { - this.features.logging.warn( - `Failed to delete server '${unsanitizedName}' from config file ${configPath}: ${err}` - ) - } - } } this.mcpServers.delete(serverName) diff --git a/server/aws-lsp-codewhisperer/src/language-server/agenticChat/tools/mcp/mcpUtils.test.ts b/server/aws-lsp-codewhisperer/src/language-server/agenticChat/tools/mcp/mcpUtils.test.ts index c50f1d62eb..00ec442517 100644 --- a/server/aws-lsp-codewhisperer/src/language-server/agenticChat/tools/mcp/mcpUtils.test.ts +++ b/server/aws-lsp-codewhisperer/src/language-server/agenticChat/tools/mcp/mcpUtils.test.ts @@ -753,6 +753,12 @@ describe('migrateToAgentConfig', () => { }) it('migrates when no existing configs exist', async () => { + // Create empty MCP config to trigger migration + const mcpDir = path.join(tmpDir, '.aws', 'amazonq') + fs.mkdirSync(mcpDir, { recursive: true }) + const mcpPath = path.join(mcpDir, 'mcp.json') + fs.writeFileSync(mcpPath, JSON.stringify({ mcpServers: {} })) + await migrateToAgentConfig(workspace, logger, mockAgent) // Should create default agent config diff --git a/server/aws-lsp-codewhisperer/src/language-server/agenticChat/tools/mcp/mcpUtils.ts b/server/aws-lsp-codewhisperer/src/language-server/agenticChat/tools/mcp/mcpUtils.ts index ffd2cfb4fc..6c5c8a9998 100644 --- a/server/aws-lsp-codewhisperer/src/language-server/agenticChat/tools/mcp/mcpUtils.ts +++ b/server/aws-lsp-codewhisperer/src/language-server/agenticChat/tools/mcp/mcpUtils.ts @@ -175,7 +175,9 @@ const DEFAULT_AGENT_RAW = `{ "README.md", ".amazonq/rules/**/*.md" ], - "resources": [] + "resources": [], + "createHooks": [], + "promptHooks": [] }` const DEFAULT_PERSONA_RAW = `{ @@ -840,48 +842,40 @@ async function migrateConfigToAgent( const normalizedPersonaPath = normalizePathFromUri(personaPath, logging) agentPath = normalizePathFromUri(agentPath) - // Check if agent config exists + // Check if config and agent files exist + const configExists = await workspace.fs.exists(normalizedConfigPath).catch(() => false) const agentExists = await workspace.fs.exists(agentPath).catch(() => false) - // Load existing agent config if it exists - let existingAgentConfig: AgentConfig | undefined + // Only migrate if agent file does not exist + // If config exists, migrate from it; if not, create default agent config if (agentExists) { - try { - const raw = (await workspace.fs.readFile(agentPath)).toString().trim() - existingAgentConfig = raw ? JSON.parse(raw) : undefined - } catch (err) { - logging.warn(`Failed to read existing agent config at ${agentPath}: ${err}`) - } + return } // Read MCP server configs directly from file const serverConfigs: Record = {} try { - const configExists = await workspace.fs.exists(normalizedConfigPath) - - if (configExists) { - const raw = (await workspace.fs.readFile(normalizedConfigPath)).toString().trim() - if (raw) { - const config = JSON.parse(raw) - - if (config.mcpServers && typeof config.mcpServers === 'object') { - // Add each server to the serverConfigs - for (const [name, serverConfig] of Object.entries(config.mcpServers)) { - serverConfigs[name] = { - command: (serverConfig as any).command, - args: Array.isArray((serverConfig as any).args) ? (serverConfig as any).args : undefined, - env: typeof (serverConfig as any).env === 'object' ? (serverConfig as any).env : undefined, - initializationTimeout: - typeof (serverConfig as any).initializationTimeout === 'number' - ? (serverConfig as any).initializationTimeout - : undefined, - timeout: - typeof (serverConfig as any).timeout === 'number' - ? (serverConfig as any).timeout - : undefined, - } - logging.info(`Added server ${name} to serverConfigs`) + const raw = (await workspace.fs.readFile(normalizedConfigPath)).toString().trim() + if (raw) { + const config = JSON.parse(raw) + + if (config.mcpServers && typeof config.mcpServers === 'object') { + // Add each server to the serverConfigs + for (const [name, serverConfig] of Object.entries(config.mcpServers)) { + serverConfigs[name] = { + command: (serverConfig as any).command, + args: Array.isArray((serverConfig as any).args) ? (serverConfig as any).args : undefined, + env: typeof (serverConfig as any).env === 'object' ? (serverConfig as any).env : undefined, + initializationTimeout: + typeof (serverConfig as any).initializationTimeout === 'number' + ? (serverConfig as any).initializationTimeout + : undefined, + timeout: + typeof (serverConfig as any).timeout === 'number' + ? (serverConfig as any).timeout + : undefined, } + logging.info(`Added server ${name} to serverConfigs`) } } } @@ -908,46 +902,24 @@ async function migrateConfigToAgent( } // Convert to agent config - const newAgentConfig = convertPersonaToAgent(personaConfig, serverConfigs, agent) - newAgentConfig.includedFiles = ['AmazonQ.md', 'README.md', '.amazonq/rules/**/*.md'] - newAgentConfig.resources = [] // Initialize with empty array - - // Merge with existing config if available - let finalAgentConfig: AgentConfig - if (existingAgentConfig) { - // Keep existing metadata - finalAgentConfig = { - ...existingAgentConfig, - // Merge MCP servers, keeping existing ones if they exist - mcpServers: { - ...newAgentConfig.mcpServers, - ...existingAgentConfig.mcpServers, - }, - // Merge tools lists without duplicates - tools: [...new Set([...existingAgentConfig.tools, ...newAgentConfig.tools])], - allowedTools: [...new Set([...existingAgentConfig.allowedTools, ...newAgentConfig.allowedTools])], - // Merge tool settings, preferring existing ones - toolsSettings: { - ...newAgentConfig.toolsSettings, - ...existingAgentConfig.toolsSettings, - }, - // Keep other properties from existing config - includedFiles: existingAgentConfig.includedFiles || newAgentConfig.includedFiles, - createHooks: existingAgentConfig.createHooks || newAgentConfig.createHooks, - promptHooks: [ - ...new Set([...(existingAgentConfig.promptHooks || []), ...(newAgentConfig.promptHooks || [])]), - ], - resources: [...new Set([...(existingAgentConfig.resources || []), ...(newAgentConfig.resources || [])])], - } - } else { - finalAgentConfig = newAgentConfig - logging.info(`Using new config (no existing config to merge)`) - } + const agentConfig = convertPersonaToAgent(personaConfig, serverConfigs, agent) + + // Parse default values from DEFAULT_AGENT_RAW + const defaultAgent = JSON.parse(DEFAULT_AGENT_RAW) + + // Add complete agent format sections using default values + agentConfig.name = defaultAgent.name + agentConfig.description = defaultAgent.description + agentConfig.version = defaultAgent.version + agentConfig.includedFiles = defaultAgent.includedFiles + agentConfig.resources = defaultAgent.resources + agentConfig.createHooks = defaultAgent.createHooks + agentConfig.promptHooks = defaultAgent.promptHooks // Save agent config try { - await saveAgentConfig(workspace, logging, finalAgentConfig, agentPath) - logging.info(`Successfully ${existingAgentConfig ? 'updated' : 'created'} agent config at ${agentPath}`) + await saveAgentConfig(workspace, logging, agentConfig, agentPath) + logging.info(`Successfully created agent config at ${agentPath}`) } catch (err) { logging.error(`Failed to save agent config to ${agentPath}: ${err}`) throw err From db45d01adba10e8a04d868e1062f899df4f5b7e4 Mon Sep 17 00:00:00 2001 From: Tai Lai Date: Mon, 25 Aug 2025 18:31:23 -0700 Subject: [PATCH 41/74] fix(amazonq): disable typewriter animation (#2160) --- chat-client/package.json | 2 +- chat-client/src/client/mynahUi.ts | 1 + package-lock.json | 8 ++++---- 3 files changed, 6 insertions(+), 5 deletions(-) diff --git a/chat-client/package.json b/chat-client/package.json index 59720a0187..e4250d42a1 100644 --- a/chat-client/package.json +++ b/chat-client/package.json @@ -27,7 +27,7 @@ "@aws/chat-client-ui-types": "^0.1.56", "@aws/language-server-runtimes": "^0.2.127", "@aws/language-server-runtimes-types": "^0.1.50", - "@aws/mynah-ui": "^4.36.4" + "@aws/mynah-ui": "^4.36.5" }, "devDependencies": { "@types/jsdom": "^21.1.6", diff --git a/chat-client/src/client/mynahUi.ts b/chat-client/src/client/mynahUi.ts index 527dea06f3..d07d0ab291 100644 --- a/chat-client/src/client/mynahUi.ts +++ b/chat-client/src/client/mynahUi.ts @@ -827,6 +827,7 @@ export const createMynahUi = ( // if we want to max user input as 500000, need to configure the maxUserInput as 500096 maxUserInput: 500096, userInputLengthWarningThreshold: 450000, + disableTypewriterAnimation: true, }, } diff --git a/package-lock.json b/package-lock.json index e65f8e8c96..5fc601a1bc 100644 --- a/package-lock.json +++ b/package-lock.json @@ -257,7 +257,7 @@ "@aws/chat-client-ui-types": "^0.1.56", "@aws/language-server-runtimes": "^0.2.127", "@aws/language-server-runtimes-types": "^0.1.50", - "@aws/mynah-ui": "^4.36.4" + "@aws/mynah-ui": "^4.36.5" }, "devDependencies": { "@types/jsdom": "^21.1.6", @@ -4203,9 +4203,9 @@ "link": true }, "node_modules/@aws/mynah-ui": { - "version": "4.36.4", - "resolved": "https://registry.npmjs.org/@aws/mynah-ui/-/mynah-ui-4.36.4.tgz", - "integrity": "sha512-vGW4wlNindpr2Ep9x3iuKbrZTXe5KrE8vWpg15DjkN3qK42KMuMEQ67Pqtfgl5EseNYC1ukZm4HIQIMmt+vevA==", + "version": "4.36.5", + "resolved": "https://registry.npmjs.org/@aws/mynah-ui/-/mynah-ui-4.36.5.tgz", + "integrity": "sha512-HMXqvSpZT84mpY67ChzRDrd73Y9AFZVZ8RcOJ/rNWIXR44uryfNFg2nrvoP4GSn2P+kU8WIPGChHGmyX9N0UgA==", "hasInstallScript": true, "license": "Apache License 2.0", "dependencies": { From 558bc1ac3e43a3d93e4de31031bb54d270eea27a Mon Sep 17 00:00:00 2001 From: manodnyab <66754471+manodnyab@users.noreply.github.com> Date: Tue, 26 Aug 2025 09:03:56 -0700 Subject: [PATCH 42/74] ci: generation of builds action can be triggered manually (#2157) Co-authored-by: Laxman Reddy <141967714+laileni-aws@users.noreply.github.com> --- .github/workflows/create-agent-standalone.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/create-agent-standalone.yml b/.github/workflows/create-agent-standalone.yml index 2807a50aea..f50b5b43b0 100644 --- a/.github/workflows/create-agent-standalone.yml +++ b/.github/workflows/create-agent-standalone.yml @@ -3,10 +3,12 @@ name: Create agent-standalone bundles on: push: branches: [main, feature/*, release/agentic/*] + workflow_dispatch: jobs: build: runs-on: ubuntu-latest + if: github.event_name == 'push' || github.actor_id == github.repository_owner_id steps: - name: Checkout repository From d28df09ae41871430cd53064eac1f3050c95ea84 Mon Sep 17 00:00:00 2001 From: invictus <149003065+ashishrp-aws@users.noreply.github.com> Date: Tue, 26 Aug 2025 10:50:15 -0700 Subject: [PATCH 43/74] fix(amazonq): fix for mcp servers operations to edit server config only (#2165) * fix(amazonq): fix for mcp servers operations to edit server specific config * fix(amazonq): additional mcp server config fixes * fix: resolve test failures * fix: update MCP manager configuration --- .../agenticChat/tools/mcp/mcpManager.test.ts | 36 +++-- .../agenticChat/tools/mcp/mcpManager.ts | 94 +++++++++-- .../agenticChat/tools/mcp/mcpUtils.test.ts | 147 ++++++++++++++++++ .../agenticChat/tools/mcp/mcpUtils.ts | 63 ++++++++ 4 files changed, 306 insertions(+), 34 deletions(-) diff --git a/server/aws-lsp-codewhisperer/src/language-server/agenticChat/tools/mcp/mcpManager.test.ts b/server/aws-lsp-codewhisperer/src/language-server/agenticChat/tools/mcp/mcpManager.test.ts index 456b8934df..0418cea553 100644 --- a/server/aws-lsp-codewhisperer/src/language-server/agenticChat/tools/mcp/mcpManager.test.ts +++ b/server/aws-lsp-codewhisperer/src/language-server/agenticChat/tools/mcp/mcpManager.test.ts @@ -239,12 +239,12 @@ describe('callTool()', () => { describe('addServer()', () => { let loadStub: sinon.SinonStub let initOneStub: sinon.SinonStub - let saveAgentConfigStub: sinon.SinonStub + let saveServerSpecificAgentConfigStub: sinon.SinonStub beforeEach(() => { loadStub = stubAgentConfig() initOneStub = stubInitOneServer() - saveAgentConfigStub = sinon.stub(mcpUtils, 'saveAgentConfig').resolves() + saveServerSpecificAgentConfigStub = sinon.stub(mcpUtils, 'saveServerSpecificAgentConfig').resolves() }) afterEach(async () => { @@ -268,7 +268,7 @@ describe('addServer()', () => { await mgr.addServer('newS', newCfg, 'path.json') - expect(saveAgentConfigStub.calledOnce).to.be.true + expect(saveServerSpecificAgentConfigStub.calledOnce).to.be.true expect(initOneStub.calledOnceWith('newS', sinon.match(newCfg))).to.be.true }) @@ -301,14 +301,14 @@ describe('addServer()', () => { await mgr.addServer('httpSrv', httpCfg, 'http.json') - expect(saveAgentConfigStub.calledOnce).to.be.true + expect(saveServerSpecificAgentConfigStub.calledOnce).to.be.true expect(initOneStub.calledOnceWith('httpSrv', sinon.match(httpCfg))).to.be.true }) }) describe('removeServer()', () => { let loadStub: sinon.SinonStub - let saveAgentConfigStub: sinon.SinonStub + let saveServerSpecificAgentConfigStub: sinon.SinonStub let existsStub: sinon.SinonStub let readFileStub: sinon.SinonStub let writeFileStub: sinon.SinonStub @@ -318,7 +318,7 @@ describe('removeServer()', () => { beforeEach(() => { loadStub = stubAgentConfig() - saveAgentConfigStub = sinon.stub(mcpUtils, 'saveAgentConfig').resolves() + saveServerSpecificAgentConfigStub = sinon.stub(mcpUtils, 'saveServerSpecificAgentConfig').resolves() existsStub = sinon.stub(fakeWorkspace.fs, 'exists').resolves(true) readFileStub = sinon .stub(fakeWorkspace.fs, 'readFile') @@ -364,7 +364,7 @@ describe('removeServer()', () => { } await mgr.removeServer('x') - expect(saveAgentConfigStub.calledOnce).to.be.true + expect(saveServerSpecificAgentConfigStub.calledOnce).to.be.true expect((mgr as any).clients.has('x')).to.be.false }) @@ -395,8 +395,8 @@ describe('removeServer()', () => { await mgr.removeServer('x') - // Verify that saveAgentConfig was called - expect(saveAgentConfigStub.calledOnce).to.be.true + // Verify that saveServerSpecificAgentConfig was called + expect(saveServerSpecificAgentConfigStub.calledOnce).to.be.true expect((mgr as any).clients.has('x')).to.be.false // Verify server was removed from agent config @@ -472,11 +472,11 @@ describe('mutateConfigFile()', () => { describe('updateServer()', () => { let loadStub: sinon.SinonStub let initOneStub: sinon.SinonStub - let saveAgentConfigStub: sinon.SinonStub + let saveServerSpecificAgentConfigStub: sinon.SinonStub beforeEach(() => { initOneStub = stubInitOneServer() - saveAgentConfigStub = sinon.stub(mcpUtils, 'saveAgentConfig').resolves() + saveServerSpecificAgentConfigStub = sinon.stub(mcpUtils, 'saveServerSpecificAgentConfig').resolves() }) afterEach(async () => { @@ -519,11 +519,11 @@ describe('updateServer()', () => { const closeStub = sinon.stub(fakeClient, 'close').resolves() initOneStub.resetHistory() - saveAgentConfigStub.resetHistory() + saveServerSpecificAgentConfigStub.resetHistory() await mgr.updateServer('u1', { timeout: 999 }, 'u.json') - expect(saveAgentConfigStub.calledOnce).to.be.true + expect(saveServerSpecificAgentConfigStub.calledOnce).to.be.true expect(closeStub.calledOnce).to.be.true expect(initOneStub.calledOnceWith('u1', sinon.match.has('timeout', 999))).to.be.true }) @@ -559,11 +559,11 @@ describe('updateServer()', () => { const mgr = McpManager.instance initOneStub.resetHistory() - saveAgentConfigStub.resetHistory() + saveServerSpecificAgentConfigStub.resetHistory() await mgr.updateServer('srv', { command: undefined, url: 'https://new.host/mcp' }, 'z.json') - expect(saveAgentConfigStub.calledOnce).to.be.true + expect(saveServerSpecificAgentConfigStub.calledOnce).to.be.true expect(initOneStub.calledOnceWith('srv', sinon.match({ url: 'https://new.host/mcp' }))).to.be.true }) }) @@ -1061,9 +1061,11 @@ describe('listServersAndTools()', () => { describe('updateServerPermission()', () => { let saveAgentConfigStub: sinon.SinonStub + let saveServerSpecificAgentConfigStub: sinon.SinonStub beforeEach(() => { saveAgentConfigStub = sinon.stub(mcpUtils, 'saveAgentConfig').resolves() + saveServerSpecificAgentConfigStub = sinon.stub(mcpUtils, 'saveServerSpecificAgentConfig').resolves() }) afterEach(async () => { @@ -1112,8 +1114,8 @@ describe('updateServerPermission()', () => { __configPath__: '/p', }) - // Verify saveAgentConfig was called - expect(saveAgentConfigStub.calledOnce).to.be.true + // Verify saveServerSpecificAgentConfig was called + expect(saveServerSpecificAgentConfigStub.calledOnce).to.be.true // Verify the tool permission was updated expect(mgr.requiresApproval('srv', 'tool1')).to.be.false diff --git a/server/aws-lsp-codewhisperer/src/language-server/agenticChat/tools/mcp/mcpManager.ts b/server/aws-lsp-codewhisperer/src/language-server/agenticChat/tools/mcp/mcpManager.ts index b66661c1ea..d0fe8ad9cc 100644 --- a/server/aws-lsp-codewhisperer/src/language-server/agenticChat/tools/mcp/mcpManager.ts +++ b/server/aws-lsp-codewhisperer/src/language-server/agenticChat/tools/mcp/mcpManager.ts @@ -26,6 +26,7 @@ import { isEmptyEnv, loadAgentConfig, saveAgentConfig, + saveServerSpecificAgentConfig, sanitizeName, getGlobalAgentConfigPath, getWorkspaceMcpConfigPaths, @@ -730,8 +731,23 @@ export class McpManager { this.agentConfig.tools.push(serverPrefix) } - // Save agent config once with all changes - await saveAgentConfig(this.features.workspace, this.features.logging, this.agentConfig, agentPath) + // Save server-specific changes to agent config + const serverTools = this.agentConfig.tools.filter( + tool => tool === serverPrefix || tool.startsWith(`${serverPrefix}/`) + ) + const serverAllowedTools = this.agentConfig.allowedTools.filter( + tool => tool === serverPrefix || tool.startsWith(`${serverPrefix}/`) + ) + + await saveServerSpecificAgentConfig( + this.features.workspace, + this.features.logging, + serverName, + serverConfig, + serverTools, + serverAllowedTools, + agentPath + ) // Add server tools to tools list after initialization await this.initOneServer(sanitizedName, newCfg, AuthIntent.Interactive) @@ -794,8 +810,16 @@ export class McpManager { return true }) - // Save agent config - await saveAgentConfig(this.features.workspace, this.features.logging, this.agentConfig, cfg.__configPath__) + // Save server removal to agent config + await saveServerSpecificAgentConfig( + this.features.workspace, + this.features.logging, + unsanitizedName, + null, // null indicates server should be removed + [], + [], + cfg.__configPath__ + ) } this.mcpServers.delete(serverName) @@ -853,8 +877,24 @@ export class McpManager { } this.agentConfig.mcpServers[unsanitizedServerName] = updatedConfig - // Save agent config - await saveAgentConfig(this.features.workspace, this.features.logging, this.agentConfig, agentPath) + // Save server-specific changes to agent config + const serverPrefix = `@${unsanitizedServerName}` + const serverTools = this.agentConfig.tools.filter( + tool => tool === serverPrefix || tool.startsWith(`${serverPrefix}/`) + ) + const serverAllowedTools = this.agentConfig.allowedTools.filter( + tool => tool === serverPrefix || tool.startsWith(`${serverPrefix}/`) + ) + + await saveServerSpecificAgentConfig( + this.features.workspace, + this.features.logging, + unsanitizedServerName, + updatedConfig, + serverTools, + serverAllowedTools, + agentPath + ) } const newCfg: MCPServerConfig = { @@ -1057,6 +1097,12 @@ export class McpManager { } } + // Update mcpServerPermissions map immediately to reflect changes + this.mcpServerPermissions.set(serverName, { + enabled: perm.enabled, + toolPerms: perm.toolPerms || {}, + }) + // Update server enabled/disabled state in agent config if (this.agentConfig.mcpServers[unsanitizedServerName]) { this.agentConfig.mcpServers[unsanitizedServerName].disabled = !perm.enabled @@ -1067,17 +1113,28 @@ export class McpManager { serverConfig.disabled = !perm.enabled } - // Save agent config + // Save only server-specific changes to agent config const agentPath = perm.__configPath__ if (agentPath) { - await saveAgentConfig(this.features.workspace, this.features.logging, this.agentConfig, agentPath) - } + // Collect server-specific tools and allowedTools + const serverPrefix = `@${unsanitizedServerName}` + const serverTools = this.agentConfig.tools.filter( + tool => tool === serverPrefix || tool.startsWith(`${serverPrefix}/`) + ) + const serverAllowedTools = this.agentConfig.allowedTools.filter( + tool => tool === serverPrefix || tool.startsWith(`${serverPrefix}/`) + ) - // Update mcpServerPermissions map - this.mcpServerPermissions.set(serverName, { - enabled: perm.enabled, - toolPerms: perm.toolPerms || {}, - }) + await saveServerSpecificAgentConfig( + this.features.workspace, + this.features.logging, + unsanitizedServerName, + this.agentConfig.mcpServers[unsanitizedServerName], + serverTools, + serverAllowedTools, + agentPath + ) + } // enable/disable server if (this.isServerDisabled(serverName)) { @@ -1184,11 +1241,14 @@ export class McpManager { return true }) - // Save agent config - await saveAgentConfig( + // Save server removal to agent config + await saveServerSpecificAgentConfig( this.features.workspace, this.features.logging, - this.agentConfig, + unsanitizedName, + null, // null indicates server should be removed + [], + [], cfg.__configPath__ ) } diff --git a/server/aws-lsp-codewhisperer/src/language-server/agenticChat/tools/mcp/mcpUtils.test.ts b/server/aws-lsp-codewhisperer/src/language-server/agenticChat/tools/mcp/mcpUtils.test.ts index 00ec442517..43843bd110 100644 --- a/server/aws-lsp-codewhisperer/src/language-server/agenticChat/tools/mcp/mcpUtils.test.ts +++ b/server/aws-lsp-codewhisperer/src/language-server/agenticChat/tools/mcp/mcpUtils.test.ts @@ -22,6 +22,7 @@ import { enabledMCP, normalizePathFromUri, saveAgentConfig, + saveServerSpecificAgentConfig, isEmptyEnv, sanitizeName, convertPersonaToAgent, @@ -788,3 +789,149 @@ describe('migrateToAgentConfig', () => { expect(agentConfig.mcpServers).to.have.property('testServer') }) }) +describe('saveServerSpecificAgentConfig', () => { + let tmpDir: string + let workspace: any + let logger: any + + beforeEach(() => { + tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'saveServerSpecificTest-')) + workspace = { + fs: { + exists: (p: string) => Promise.resolve(fs.existsSync(p)), + readFile: (p: string) => Promise.resolve(Buffer.from(fs.readFileSync(p))), + writeFile: (p: string, d: string) => Promise.resolve(fs.writeFileSync(p, d)), + mkdir: (d: string, opts: any) => Promise.resolve(fs.mkdirSync(d, { recursive: opts.recursive })), + }, + } + logger = { warn: () => {}, info: () => {}, error: () => {} } + }) + + afterEach(() => { + fs.rmSync(tmpDir, { recursive: true, force: true }) + }) + + it('creates new config file when it does not exist', async () => { + const configPath = path.join(tmpDir, 'agent-config.json') + const serverConfig = { command: 'test-cmd', args: ['arg1'] } + const serverTools = ['@testServer'] + const serverAllowedTools = ['@testServer/tool1'] + + await saveServerSpecificAgentConfig( + workspace, + logger, + 'testServer', + serverConfig, + serverTools, + serverAllowedTools, + configPath + ) + + expect(fs.existsSync(configPath)).to.be.true + const content = JSON.parse(fs.readFileSync(configPath, 'utf-8')) + expect(content.mcpServers.testServer).to.deep.equal(serverConfig) + expect(content.tools).to.include('@testServer') + expect(content.allowedTools).to.include('@testServer/tool1') + }) + + it('updates existing config file', async () => { + const configPath = path.join(tmpDir, 'agent-config.json') + + // Create existing config + const existingConfig = { + name: 'existing-agent', + version: '1.0.0', + description: 'Existing agent', + mcpServers: { + existingServer: { command: 'existing-cmd' }, + }, + tools: ['fs_read', '@existingServer'], + allowedTools: ['fs_read'], + toolsSettings: {}, + includedFiles: [], + resources: [], + } + fs.writeFileSync(configPath, JSON.stringify(existingConfig)) + + const serverConfig = { command: 'new-cmd', args: ['arg1'] } + const serverTools = ['@newServer'] + const serverAllowedTools = ['@newServer/tool1'] + + await saveServerSpecificAgentConfig( + workspace, + logger, + 'newServer', + serverConfig, + serverTools, + serverAllowedTools, + configPath + ) + + const content = JSON.parse(fs.readFileSync(configPath, 'utf-8')) + expect(content.name).to.equal('existing-agent') + expect(content.mcpServers.existingServer).to.deep.equal({ command: 'existing-cmd' }) + expect(content.mcpServers.newServer).to.deep.equal(serverConfig) + expect(content.tools).to.include('@newServer') + expect(content.allowedTools).to.include('@newServer/tool1') + }) + + it('removes existing server tools before adding new ones', async () => { + const configPath = path.join(tmpDir, 'agent-config.json') + + // Create existing config with server tools + const existingConfig = { + name: 'test-agent', + version: '1.0.0', + description: 'Test agent', + mcpServers: { + testServer: { command: 'old-cmd' }, + }, + tools: ['fs_read', '@testServer', '@testServer/oldTool'], + allowedTools: ['fs_read', '@testServer/oldAllowedTool'], + toolsSettings: {}, + includedFiles: [], + resources: [], + } + fs.writeFileSync(configPath, JSON.stringify(existingConfig)) + + const serverConfig = { command: 'new-cmd' } + const serverTools = ['@testServer'] + const serverAllowedTools = ['@testServer/newTool'] + + await saveServerSpecificAgentConfig( + workspace, + logger, + 'testServer', + serverConfig, + serverTools, + serverAllowedTools, + configPath + ) + + const content = JSON.parse(fs.readFileSync(configPath, 'utf-8')) + expect(content.tools).to.not.include('@testServer/oldTool') + expect(content.allowedTools).to.not.include('@testServer/oldAllowedTool') + expect(content.tools).to.include('@testServer') + expect(content.allowedTools).to.include('@testServer/newTool') + expect(content.tools).to.include('fs_read') + }) + + it('creates parent directories if they do not exist', async () => { + const configPath = path.join(tmpDir, 'nested', 'dir', 'agent-config.json') + const serverConfig = { command: 'test-cmd' } + const serverTools = ['@testServer'] + const serverAllowedTools: string[] = [] + + await saveServerSpecificAgentConfig( + workspace, + logger, + 'testServer', + serverConfig, + serverTools, + serverAllowedTools, + configPath + ) + + expect(fs.existsSync(configPath)).to.be.true + }) +}) diff --git a/server/aws-lsp-codewhisperer/src/language-server/agenticChat/tools/mcp/mcpUtils.ts b/server/aws-lsp-codewhisperer/src/language-server/agenticChat/tools/mcp/mcpUtils.ts index 6c5c8a9998..ef56a54d22 100644 --- a/server/aws-lsp-codewhisperer/src/language-server/agenticChat/tools/mcp/mcpUtils.ts +++ b/server/aws-lsp-codewhisperer/src/language-server/agenticChat/tools/mcp/mcpUtils.ts @@ -943,6 +943,69 @@ export async function saveAgentConfig( } } +/** + * Save only server-specific changes to agent config file + */ +export async function saveServerSpecificAgentConfig( + workspace: Workspace, + logging: Logger, + serverName: string, + serverConfig: any, + serverTools: string[], + serverAllowedTools: string[], + configPath: string +): Promise { + try { + await workspace.fs.mkdir(path.dirname(configPath), { recursive: true }) + + // Read existing config + let existingConfig: AgentConfig + try { + const raw = await workspace.fs.readFile(configPath) + existingConfig = JSON.parse(raw.toString()) + } catch { + // If file doesn't exist, create minimal config + existingConfig = { + name: 'default-agent', + version: '1.0.0', + description: 'Agent configuration', + mcpServers: {}, + tools: [], + allowedTools: [], + toolsSettings: {}, + includedFiles: [], + resources: [], + } + } + + // Remove existing server tools from arrays + const serverPrefix = `@${serverName}` + existingConfig.tools = existingConfig.tools.filter( + tool => tool !== serverPrefix && !tool.startsWith(`${serverPrefix}/`) + ) + existingConfig.allowedTools = existingConfig.allowedTools.filter( + tool => tool !== serverPrefix && !tool.startsWith(`${serverPrefix}/`) + ) + + if (serverConfig === null) { + // Remove server entirely + delete existingConfig.mcpServers[serverName] + } else { + // Update or add server + existingConfig.mcpServers[serverName] = serverConfig + // Add new server tools + existingConfig.tools.push(...serverTools) + existingConfig.allowedTools.push(...serverAllowedTools) + } + + await workspace.fs.writeFile(configPath, JSON.stringify(existingConfig, null, 2)) + logging.info(`Saved server-specific agent config for ${serverName} to ${configPath}`) + } catch (err: any) { + logging.error(`Failed to save server-specific agent config to ${configPath}: ${err.message}`) + throw err + } +} + export const MAX_TOOL_NAME_LENGTH = 64 /** From bb5f4c6c90566be020a21b32f55741d445509029 Mon Sep 17 00:00:00 2001 From: Shruti Sinha <44882001+shruti0085@users.noreply.github.com> Date: Tue, 26 Aug 2025 12:06:46 -0700 Subject: [PATCH 44/74] fix: allow ci to run on release branches (#2159) Currently release/agentic/* branches used in our release process have rules that require CI to be run. However the corresponding workflows do not actually trigger for release branches. This change ensures no manual overrides are required in the release process and CI mapped with the branch actually runs. --- .github/workflows/lsp-ci.yaml | 4 ++-- .github/workflows/npm-packaging.yaml | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/lsp-ci.yaml b/.github/workflows/lsp-ci.yaml index bda44ec74a..15f87d2428 100644 --- a/.github/workflows/lsp-ci.yaml +++ b/.github/workflows/lsp-ci.yaml @@ -1,9 +1,9 @@ name: Language Server CI on: push: - branches: [main, dev, feature/*] + branches: [main, dev, feature/*, release/agentic/*] pull_request: - branches: [main, dev, feature/*] + branches: [main, dev, feature/*, release/agentic/*] jobs: test: diff --git a/.github/workflows/npm-packaging.yaml b/.github/workflows/npm-packaging.yaml index 724c9a0c05..d4ce77e07c 100644 --- a/.github/workflows/npm-packaging.yaml +++ b/.github/workflows/npm-packaging.yaml @@ -1,9 +1,9 @@ name: NPM Packaging on: push: - branches: [main, dev, feature/*] + branches: [main, dev, feature/*, release/agentic/*] pull_request: - branches: [main, dev, feature/*] + branches: [main, dev, feature/*, release/agentic/*] jobs: build: From 00e11ff48eafaa0baec48177fa4aa6d60048af2f Mon Sep 17 00:00:00 2001 From: Lei Gao <97199248+leigaol@users.noreply.github.com> Date: Tue, 26 Aug 2025 14:07:47 -0700 Subject: [PATCH 45/74] fix: reduce auto trigger frequency for VSC (#2168) * fix: reduce auto trigger frequency for VSC * fix: skip one unit test * fix: skip unit test --------- Co-authored-by: Laxman Reddy <141967714+laileni-aws@users.noreply.github.com> --- .../inline-completion/auto-trigger/autoTrigger.test.ts | 2 +- .../inline-completion/auto-trigger/autoTrigger.ts | 7 ++++++- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/server/aws-lsp-codewhisperer/src/language-server/inline-completion/auto-trigger/autoTrigger.test.ts b/server/aws-lsp-codewhisperer/src/language-server/inline-completion/auto-trigger/autoTrigger.test.ts index 31bc0a224a..6f96c526de 100644 --- a/server/aws-lsp-codewhisperer/src/language-server/inline-completion/auto-trigger/autoTrigger.test.ts +++ b/server/aws-lsp-codewhisperer/src/language-server/inline-completion/auto-trigger/autoTrigger.test.ts @@ -158,7 +158,7 @@ describe('Auto Trigger', async () => { assert.strictEqual(getAutoTriggerType(createContentChange('line1\nline2')), undefined) }) }) - describe('Right Context should trigger validation', () => { + describe.skip('Right Context should trigger validation', () => { it('should not trigger when there is immediate right context in VSCode', () => { const params = createBasicParams({ fileContext: createBasicFileContext('console.', 'log()'), diff --git a/server/aws-lsp-codewhisperer/src/language-server/inline-completion/auto-trigger/autoTrigger.ts b/server/aws-lsp-codewhisperer/src/language-server/inline-completion/auto-trigger/autoTrigger.ts index d9dd7f16d5..9fb2efab8e 100644 --- a/server/aws-lsp-codewhisperer/src/language-server/inline-completion/auto-trigger/autoTrigger.ts +++ b/server/aws-lsp-codewhisperer/src/language-server/inline-completion/auto-trigger/autoTrigger.ts @@ -229,7 +229,12 @@ export const autoTrigger = ( const triggerTypeCoefficient = coefficients.triggerTypeCoefficient[triggerType] ?? 0 const osCoefficient = coefficients.osCoefficient[os] ?? 0 - const charCoefficient = coefficients.charCoefficient[char] ?? 0 + let charCoefficient = coefficients.charCoefficient[char] ?? 0 + // this is a temporary change to lower the auto trigger frequency + if (ide === 'VSCODE') { + charCoefficient = 0 + } + const keyWordCoefficient = coefficients.charCoefficient[keyword] ?? 0 const languageCoefficient = coefficients.languageCoefficient[fileContext.programmingLanguage.languageName] ?? 0 From aa87ae2bd95edc1f38bf90f56093c5bf5ff18c53 Mon Sep 17 00:00:00 2001 From: andrewyuq <89420755+andrewyuq@users.noreply.github.com> Date: Tue, 26 Aug 2025 15:51:07 -0700 Subject: [PATCH 46/74] fix(amazonq): dedupe openTabs supplemental contexts (#2172) --- .../supplementalContextUtil/crossFileContextUtil.ts | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/server/aws-lsp-codewhisperer/src/shared/supplementalContextUtil/crossFileContextUtil.ts b/server/aws-lsp-codewhisperer/src/shared/supplementalContextUtil/crossFileContextUtil.ts index ca5bc40642..06bac73d54 100644 --- a/server/aws-lsp-codewhisperer/src/shared/supplementalContextUtil/crossFileContextUtil.ts +++ b/server/aws-lsp-codewhisperer/src/shared/supplementalContextUtil/crossFileContextUtil.ts @@ -209,8 +209,17 @@ export async function fetchOpenTabsContext( }) } + // Dedupe code chunks based on their filePath + content unique key + const seen = new Set() + const deduped = supplementalContexts.filter(item => { + const key = `${item.filePath}:${item.content}` + if (seen.has(key)) return false + seen.add(key) + return true + }) + // DO NOT send code chunk with empty content - return supplementalContexts.filter(item => item.content.trim().length !== 0) + return deduped.filter(item => item.content.trim().length !== 0) } function findBestKChunkMatches(chunkInput: Chunk, chunkReferences: Chunk[], k: number): Chunk[] { From d7b184cb12979877722fa0293e9aebec91ff2c18 Mon Sep 17 00:00:00 2001 From: Boyu Date: Tue, 26 Aug 2025 21:34:11 -0700 Subject: [PATCH 47/74] fix: fix pkce windows url path (#2173) --- .../agenticChat/tools/mcp/mcpManager.ts | 6 ++-- .../tools/mcp/mcpOauthClient.test.ts | 11 +++++-- .../agenticChat/tools/mcp/mcpOauthClient.ts | 29 +++++++------------ 3 files changed, 23 insertions(+), 23 deletions(-) diff --git a/server/aws-lsp-codewhisperer/src/language-server/agenticChat/tools/mcp/mcpManager.ts b/server/aws-lsp-codewhisperer/src/language-server/agenticChat/tools/mcp/mcpManager.ts index d0fe8ad9cc..027e349cf5 100644 --- a/server/aws-lsp-codewhisperer/src/language-server/agenticChat/tools/mcp/mcpManager.ts +++ b/server/aws-lsp-codewhisperer/src/language-server/agenticChat/tools/mcp/mcpManager.ts @@ -302,7 +302,7 @@ export class McpManager { cfg: MCPServerConfig, authIntent: AuthIntent = AuthIntent.Silent ): Promise { - const DEFAULT_SERVER_INIT_TIMEOUT_MS = 60_000 + const DEFAULT_SERVER_INIT_TIMEOUT_MS = 120_000 this.setState(serverName, McpServerStatus.INITIALIZING, 0) try { @@ -373,7 +373,7 @@ export class McpManager { } if (needsOAuth) { - OAuthClient.initialize(this.features.workspace, this.features.logging) + OAuthClient.initialize(this.features.workspace, this.features.logging, this.features.lsp) try { const bearer = await OAuthClient.getValidAccessToken(base, { interactive: authIntent === AuthIntent.Interactive, @@ -382,7 +382,7 @@ export class McpManager { headers = { ...headers, Authorization: `Bearer ${bearer}` } } else if (authIntent === AuthIntent.Silent) { throw new AgenticChatError( - `MCP: server '${serverName}' requires OAuth. Open "Edit MCP Server" and save to sign in.`, + `Server '${serverName}' requires OAuth. Click on Save to reauthenticate.`, 'MCPServerAuthFailed' ) } diff --git a/server/aws-lsp-codewhisperer/src/language-server/agenticChat/tools/mcp/mcpOauthClient.test.ts b/server/aws-lsp-codewhisperer/src/language-server/agenticChat/tools/mcp/mcpOauthClient.test.ts index 08f8f1dd5c..ea711319a5 100644 --- a/server/aws-lsp-codewhisperer/src/language-server/agenticChat/tools/mcp/mcpOauthClient.test.ts +++ b/server/aws-lsp-codewhisperer/src/language-server/agenticChat/tools/mcp/mcpOauthClient.test.ts @@ -19,6 +19,12 @@ const fakeLogger = { error: () => {}, } +const fakeLsp = { + window: { + showDocument: sinon.stub().resolves({ success: true }), + }, +} as any + const fakeWorkspace = { fs: { exists: async (_path: string) => false, @@ -93,9 +99,10 @@ describe('OAuthClient getValidAccessToken()', () => { beforeEach(() => { sinon.restore() - OAuthClient.initialize(fakeWorkspace, fakeLogger as any) + OAuthClient.initialize(fakeWorkspace, fakeLogger as any, fakeLsp) sinon.stub(OAuthClient as any, 'computeKey').returns('testkey') stubHttpServer() + ;(fakeLsp.window.showDocument as sinon.SinonStub).resetHistory() }) afterEach(() => sinon.restore()) @@ -117,6 +124,6 @@ describe('OAuthClient getValidAccessToken()', () => { interactive: true, }) expect(token).to.equal('cached_access') - expect((http.createServer as any).calledOnce).to.be.true + expect((fakeLsp.window.showDocument as sinon.SinonStub).called).to.be.false }) }) diff --git a/server/aws-lsp-codewhisperer/src/language-server/agenticChat/tools/mcp/mcpOauthClient.ts b/server/aws-lsp-codewhisperer/src/language-server/agenticChat/tools/mcp/mcpOauthClient.ts index 1e9c8745d3..2e207c449e 100644 --- a/server/aws-lsp-codewhisperer/src/language-server/agenticChat/tools/mcp/mcpOauthClient.ts +++ b/server/aws-lsp-codewhisperer/src/language-server/agenticChat/tools/mcp/mcpOauthClient.ts @@ -10,7 +10,7 @@ import { spawn } from 'child_process' import { URL, URLSearchParams } from 'url' import * as http from 'http' import * as os from 'os' -import { Logger, Workspace } from '@aws/language-server-runtimes/server-interface' +import { Logger, Workspace, Lsp } from '@aws/language-server-runtimes/server-interface' interface Token { access_token: string @@ -35,10 +35,12 @@ interface Registration { export class OAuthClient { private static logger: Logger private static workspace: Workspace + private static lsp: Lsp - public static initialize(ws: Workspace, logger: Logger): void { + public static initialize(ws: Workspace, logger: Logger, lsp: Lsp): void { this.workspace = ws this.logger = logger + this.lsp = lsp } /** @@ -95,10 +97,11 @@ export class OAuthClient { const savedReg = await this.read(regPath) if (savedReg) { const port = Number(new URL(savedReg.redirect_uri).port) + const normalized = `http://127.0.0.1:${port}` server = http.createServer() try { - await this.listen(server, port) - redirectUri = savedReg.redirect_uri + await this.listen(server, port, '127.0.0.1') + redirectUri = normalized this.logger.info(`OAuth: reusing redirect URI ${redirectUri}`) } catch (e: any) { if (e.code === 'EADDRINUSE') { @@ -182,9 +185,9 @@ export class OAuthClient { /** Spin up a one‑time HTTP listener on localhost:randomPort */ private static async buildCallbackServer(): Promise<{ server: http.Server; redirectUri: string }> { const server = http.createServer() - await this.listen(server, 0) + await this.listen(server, 0, '127.0.0.1') const port = (server.address() as any).port as number - return { server, redirectUri: `http://localhost:${port}` } + return { server, redirectUri: `http://127.0.0.1:${port}` } } /** Discover OAuth endpoints by HEAD/WWW‑Authenticate, well‑known, or fallback */ @@ -334,7 +337,7 @@ export class OAuthClient { redirectUri: string, server: http.Server ): Promise { - const DEFAULT_PKCE_TIMEOUT_MS = 20_000 + const DEFAULT_PKCE_TIMEOUT_MS = 90_000 // a) generate PKCE params const verifier = this.b64url(crypto.randomBytes(32)) const challenge = this.b64url(crypto.createHash('sha256').update(verifier).digest()) @@ -353,17 +356,7 @@ export class OAuthClient { state: state, }).toString() - const opener = - process.platform === 'win32' - ? { - cmd: 'cmd', - args: ['/c', 'start', '', `"${authz.toString().replace(/"/g, '""')}"`], - } - : process.platform === 'darwin' - ? { cmd: 'open', args: [authz.toString()] } - : { cmd: 'xdg-open', args: [authz.toString()] } - - void spawn(opener.cmd, opener.args, { detached: true, stdio: 'ignore' }).unref() + await this.lsp.window.showDocument({ uri: authz.toString(), external: true }) // c) wait for code on our loopback const waitForFlow = new Promise<{ code: string; rxState: string; err?: string; errDesc?: string }>(resolve => { From 8600c524877abb459e9338399352446c0dcff6f0 Mon Sep 17 00:00:00 2001 From: Laxman Reddy <141967714+laileni-aws@users.noreply.github.com> Date: Wed, 27 Aug 2025 09:14:48 -0700 Subject: [PATCH 48/74] feat: Auto fetch models from listAvailableModels API (#2171) * Updating the model of listAvailableModels (#2064) * fix(amazonq): fix flickering issue for model selection dropdown and agenticCoding toggle (#2065) * fix(amazonq): fix flickering issue for modelId and agenticCoding * fix(amazonq): Fixing flaky tests * feat(amazonq): Fetching models from backend and adding cache implementation. (#2075) * fix: removing and refactoring legacy code before implementing model selection * feat(amazonq): adding cache implementation and fetching models from listAvailableModels api * feat(amazonq): adding selected model in error case * feat(amazonq): adding test cases * fix: addressing comments * fix: fixing test cases and adding modelName to models * fix: minor edits * fix: minor edits * fix: minor modifications in logs * fix: adding default model if api throws any errors * fix: refactoring code * fix: Improve model selection fallback logic when user's preferred model is unavailable (#2089) * fix: if user preferred model does not exist, fall back to default model * fix: minor test changes * fix: to support backward compatibility for vs and eclipse, adding back modelSelection in chat-client (#2095) * fix: check available models from backend before selecting default model from fallback models (#2102) * feat(amazonq): use model display names (#2123) * fix: cached model list should be invalidated on sign out (#2131) * fix: cached model list should be invalidated on sign out * fix test * avoid throwing error * fix: adding default modelId and setting cache ttl to 30 minutes (#2161) * fix: adding defaultmodelId and setting cache ttl to 30 minutes * fix: fixing tests * fix: updating comments * Updating the model of listAvailableModels (#2064) * fix(amazonq): fix flickering issue for model selection dropdown and agenticCoding toggle (#2065) * fix(amazonq): fix flickering issue for modelId and agenticCoding * fix(amazonq): Fixing flaky tests * feat(amazonq): Fetching models from backend and adding cache implementation. (#2075) * fix: removing and refactoring legacy code before implementing model selection * feat(amazonq): adding cache implementation and fetching models from listAvailableModels api * feat(amazonq): adding selected model in error case * feat(amazonq): adding test cases * fix: addressing comments * fix: fixing test cases and adding modelName to models * fix: minor edits * fix: minor edits * fix: minor modifications in logs * fix: adding default model if api throws any errors * fix: refactoring code * fix: Improve model selection fallback logic when user's preferred model is unavailable (#2089) * fix: if user preferred model does not exist, fall back to default model * fix: minor test changes * fix: to support backward compatibility for vs and eclipse, adding back modelSelection in chat-client (#2095) * fix: check available models from backend before selecting default model from fallback models (#2102) * feat(amazonq): use model display names (#2123) * fix: cached model list should be invalidated on sign out (#2131) * fix: cached model list should be invalidated on sign out * fix test * avoid throwing error * fix: adding default modelId and setting cache ttl to 30 minutes (#2161) * fix: adding defaultmodelId and setting cache ttl to 30 minutes * fix: fixing tests * fix: updating comments * fix: lint issue while resolving merge conflicts --------- Co-authored-by: Tai Lai --- chat-client/src/client/chat.ts | 15 - chat-client/src/client/mynahUi.test.ts | 4 +- chat-client/src/client/mynahUi.ts | 23 +- .../src/client/tabs/tabFactory.test.ts | 7 +- chat-client/src/client/tabs/tabFactory.ts | 7 +- .../src/client/texts/modelSelection.test.ts | 51 +-- .../src/client/texts/modelSelection.ts | 14 +- .../sigv4/codewhisperersigv4client.d.ts | 2 +- .../client/token/bearer-token-service.json | 15 + .../token/codewhispererbearertokenclient.d.ts | 10 + .../agenticChat/agenticChatController.test.ts | 330 +++++++++++------- .../agenticChat/agenticChatController.ts | 163 ++++++--- .../constants/modelSelection.test.ts | 58 +-- .../agenticChat/constants/modelSelection.ts | 19 +- .../agenticChat/tools/chatDb/chatDb.test.ts | 69 +++- .../agenticChat/tools/chatDb/chatDb.ts | 51 ++- .../agenticChat/tools/chatDb/util.ts | 12 + .../utils/agenticChatControllerHelper.ts | 16 - .../AmazonQTokenServiceManager.ts | 5 + .../src/shared/codeWhispererService.ts | 7 + .../src/shared/constants.ts | 2 + 21 files changed, 555 insertions(+), 325 deletions(-) delete mode 100644 server/aws-lsp-codewhisperer/src/language-server/agenticChat/utils/agenticChatControllerHelper.ts diff --git a/chat-client/src/client/chat.ts b/chat-client/src/client/chat.ts index d090a33f70..58519d96ef 100644 --- a/chat-client/src/client/chat.ts +++ b/chat-client/src/client/chat.ts @@ -116,7 +116,6 @@ import { InboundChatApi, createMynahUi } from './mynahUi' import { TabFactory } from './tabs/tabFactory' import { ChatClientAdapter } from '../contracts/chatClientAdapter' import { toMynahContextCommand, toMynahIcon } from './utils' -import { modelSelectionForRegion } from './texts/modelSelection' const getDefaultTabConfig = (agenticMode?: boolean) => { return { @@ -264,20 +263,6 @@ export const createChat = ( return option }), }) - } else if (message.params.region) { - // TODO: This can be removed after all clients support aws/chat/listAvailableModels - // get all tabs and update region - const allExistingTabs: MynahUITabStoreModel = mynahUi.getAllTabs() - for (const tabId in allExistingTabs) { - const options = mynahUi.getTabData(tabId).getStore()?.promptInputOptions - mynahUi.updateStore(tabId, { - promptInputOptions: options?.map(option => - option.id === 'model-selection' - ? modelSelectionForRegion[message.params.region] - : option - ), - }) - } } else { tabFactory.setInfoMessages((message.params as ChatOptionsUpdateParams).chatNotifications) } diff --git a/chat-client/src/client/mynahUi.test.ts b/chat-client/src/client/mynahUi.test.ts index 31e64f0e76..727805ebbc 100644 --- a/chat-client/src/client/mynahUi.test.ts +++ b/chat-client/src/client/mynahUi.test.ts @@ -260,7 +260,8 @@ describe('MynahUI', () => { sinon.assert.calledThrice(updateStoreSpy) }) - it('should create a new tab if current tab is loading', () => { + it('should create a new tab if current tab is loading', function (done) { + this.timeout(8000) // clear create tab stub since set up process calls it twice createTabStub.resetHistory() getAllTabsStub.returns({ 'tab-1': { store: { loadingChat: true } } }) @@ -274,6 +275,7 @@ describe('MynahUI', () => { sinon.assert.calledOnceWithExactly(createTabStub, false) sinon.assert.calledThrice(updateStoreSpy) + done() }) it('should not create a new tab if one exists already', () => { diff --git a/chat-client/src/client/mynahUi.ts b/chat-client/src/client/mynahUi.ts index d07d0ab291..15cbeae7ff 100644 --- a/chat-client/src/client/mynahUi.ts +++ b/chat-client/src/client/mynahUi.ts @@ -151,11 +151,20 @@ export const handlePromptInputChange = (mynahUi: MynahUI, tabId: string, options } } + const updatedPromptInputOptions = promptInputOptions?.map(option => { + option.value = optionsValues[option.id] + return option + }) + mynahUi.updateStore(tabId, { - promptInputOptions: promptInputOptions?.map(option => { - option.value = optionsValues[option.id] - return option - }), + promptInputOptions: updatedPromptInputOptions, + }) + + // Store the updated values in tab defaults for new tabs + mynahUi.updateTabDefaults({ + store: { + promptInputOptions: updatedPromptInputOptions, + }, }) } @@ -414,6 +423,12 @@ export const createMynahUi = ( } const tabStore = mynahUi.getTabData(tabId).getStore() + const storedPromptInputOptions = mynahUi.getTabDefaults().store?.promptInputOptions + + // Retrieve stored model selection and pair programming mode from defaults + if (storedPromptInputOptions) { + defaultTabConfig.promptInputOptions = storedPromptInputOptions + } // Tabs can be opened through different methods, including server-initiated 'openTab' requests. // The 'openTab' request is specifically used for loading historical chat sessions with pre-existing messages. diff --git a/chat-client/src/client/tabs/tabFactory.test.ts b/chat-client/src/client/tabs/tabFactory.test.ts index c398c709eb..815e81a22e 100644 --- a/chat-client/src/client/tabs/tabFactory.test.ts +++ b/chat-client/src/client/tabs/tabFactory.test.ts @@ -2,7 +2,7 @@ import { ChatHistory } from '../features/history' import { TabFactory } from './tabFactory' import * as assert from 'assert' import { pairProgrammingPromptInput } from '../texts/pairProgramming' -import { modelSelectionForRegion } from '../texts/modelSelection' +import { modelSelection } from '../texts/modelSelection' describe('tabFactory', () => { describe('getDefaultTabData', () => { @@ -92,10 +92,7 @@ describe('tabFactory', () => { const result = tabFactory.createTab(false) - assert.deepStrictEqual(result.promptInputOptions, [ - pairProgrammingPromptInput, - modelSelectionForRegion['us-east-1'], - ]) + assert.deepStrictEqual(result.promptInputOptions, [pairProgrammingPromptInput, modelSelection]) }) it('should not include model selection when only agentic mode is enabled', () => { diff --git a/chat-client/src/client/tabs/tabFactory.ts b/chat-client/src/client/tabs/tabFactory.ts index 3a6012471a..6df349896c 100644 --- a/chat-client/src/client/tabs/tabFactory.ts +++ b/chat-client/src/client/tabs/tabFactory.ts @@ -11,7 +11,7 @@ import { disclaimerCard } from '../texts/disclaimer' import { ChatMessage } from '@aws/language-server-runtimes-types' import { ChatHistory } from '../features/history' import { pairProgrammingPromptInput, programmerModeCard } from '../texts/pairProgramming' -import { modelSelectionForRegion } from '../texts/modelSelection' +import { modelSelection } from '../texts/modelSelection' export type DefaultTabData = MynahUIDataModel @@ -52,10 +52,7 @@ export class TabFactory { ...this.getDefaultTabData(), ...(disclaimerCardActive ? { promptInputStickyCard: disclaimerCard } : {}), promptInputOptions: this.agenticMode - ? [ - pairProgrammingPromptInput, - ...(this.modelSelectionEnabled ? [modelSelectionForRegion['us-east-1']] : []), - ] + ? [pairProgrammingPromptInput, ...(this.modelSelectionEnabled ? [modelSelection] : [])] : [], cancelButtonWhenLoading: this.agenticMode, // supported for agentic chat only } diff --git a/chat-client/src/client/texts/modelSelection.test.ts b/chat-client/src/client/texts/modelSelection.test.ts index c36dfad976..abd010436e 100644 --- a/chat-client/src/client/texts/modelSelection.test.ts +++ b/chat-client/src/client/texts/modelSelection.test.ts @@ -1,60 +1,11 @@ import * as assert from 'assert' -import { - BedrockModel, - modelSelectionForRegion, - getModelSelectionChatItem, - modelUnavailableBanner, - modelThrottledBanner, -} from './modelSelection' +import { getModelSelectionChatItem, modelUnavailableBanner, modelThrottledBanner } from './modelSelection' import { ChatItemType } from '@aws/mynah-ui' /** * Tests for modelSelection functionality - * - * Note: Some tests are for deprecated code (marked with 'legacy') that is maintained - * for backward compatibility with older clients. These should be removed once - * all clients have been updated to use the new API (aws/chat/listAvailableModels). */ describe('modelSelection', () => { - describe('BedrockModel enum (legacy)', () => { - it('should have the correct model IDs', () => { - assert.strictEqual(BedrockModel.CLAUDE_3_7_SONNET_20250219_V1_0, 'CLAUDE_3_7_SONNET_20250219_V1_0') - assert.strictEqual(BedrockModel.CLAUDE_SONNET_4_20250514_V1_0, 'CLAUDE_SONNET_4_20250514_V1_0') - }) - }) - - describe('modelSelectionForRegion (legacy)', () => { - it('should provide all models for us-east-1 region', () => { - const usEast1ModelSelection = modelSelectionForRegion['us-east-1'] - assert.ok(usEast1ModelSelection, 'usEast1ModelSelection should exist') - assert.ok(usEast1ModelSelection.type === 'select', 'usEast1ModelSelection should be type select') - assert.ok(Array.isArray(usEast1ModelSelection.options), 'options should be an array') - assert.strictEqual(usEast1ModelSelection.options.length, 2, 'should have 2 options') - - const modelIds = usEast1ModelSelection.options.map(option => option.value) - assert.ok(modelIds.includes(BedrockModel.CLAUDE_SONNET_4_20250514_V1_0), 'should include Claude Sonnet 4') - assert.ok( - modelIds.includes(BedrockModel.CLAUDE_3_7_SONNET_20250219_V1_0), - 'should include Claude Sonnet 3.7' - ) - }) - - it('should provide all models for eu-central-1 region', () => { - const euCentral1ModelSelection = modelSelectionForRegion['eu-central-1'] - assert.ok(euCentral1ModelSelection, 'euCentral1ModelSelection should exist') - assert.ok(euCentral1ModelSelection.type === 'select', 'euCentral1ModelSelection should be type select') - assert.ok(Array.isArray(euCentral1ModelSelection.options), 'options should be an array') - assert.strictEqual(euCentral1ModelSelection.options.length, 2, 'should have 2 option') - - const modelIds = euCentral1ModelSelection.options.map(option => option.value) - assert.ok(modelIds.includes(BedrockModel.CLAUDE_SONNET_4_20250514_V1_0), 'should include Claude Sonnet 4') - assert.ok( - modelIds.includes(BedrockModel.CLAUDE_3_7_SONNET_20250219_V1_0), - 'should include Claude Sonnet 3.7' - ) - }) - }) - describe('getModelSelectionChatItem', () => { it('should return a chat item with the correct model name', () => { const modelName = 'Claude Sonnet 4' diff --git a/chat-client/src/client/texts/modelSelection.ts b/chat-client/src/client/texts/modelSelection.ts index 18df833419..a1d0247875 100644 --- a/chat-client/src/client/texts/modelSelection.ts +++ b/chat-client/src/client/texts/modelSelection.ts @@ -13,7 +13,7 @@ type ModelDetails = { } const modelRecord: Record = { - [BedrockModel.CLAUDE_3_7_SONNET_20250219_V1_0]: { label: 'Claude Sonnet 3.7' }, + [BedrockModel.CLAUDE_3_7_SONNET_20250219_V1_0]: { label: 'Claude 3.7 Sonnet' }, [BedrockModel.CLAUDE_SONNET_4_20250514_V1_0]: { label: 'Claude Sonnet 4' }, } @@ -22,24 +22,16 @@ const modelOptions = Object.entries(modelRecord).map(([value, { label }]) => ({ label, })) -const modelSelection: ChatItemFormItem = { +export const modelSelection: ChatItemFormItem = { type: 'select', id: 'model-selection', - options: modelOptions, mandatory: true, hideMandatoryIcon: true, + options: modelOptions, border: false, autoWidth: true, } -/** - * @deprecated use aws/chat/listAvailableModels server request instead - */ -export const modelSelectionForRegion: Record = { - 'us-east-1': modelSelection, - 'eu-central-1': modelSelection, -} - export const getModelSelectionChatItem = (modelName: string): ChatItem => ({ type: ChatItemType.DIRECTIVE, contentHorizontalAlignment: 'center', diff --git a/server/aws-lsp-codewhisperer/src/client/sigv4/codewhisperersigv4client.d.ts b/server/aws-lsp-codewhisperer/src/client/sigv4/codewhisperersigv4client.d.ts index 308ead42ee..6cb67d3ef8 100644 --- a/server/aws-lsp-codewhisperer/src/client/sigv4/codewhisperersigv4client.d.ts +++ b/server/aws-lsp-codewhisperer/src/client/sigv4/codewhisperersigv4client.d.ts @@ -3,7 +3,7 @@ * THIS FILE IS AUTOGENERATED BY 'generateServiceClient.ts'. * DO NOT EDIT BY HAND. */ - + import {Request} from 'aws-sdk/lib/request'; import {Response} from 'aws-sdk/lib/response'; import {AWSError} from 'aws-sdk/lib/error'; diff --git a/server/aws-lsp-codewhisperer/src/client/token/bearer-token-service.json b/server/aws-lsp-codewhisperer/src/client/token/bearer-token-service.json index a5704fca16..7e0533b0f5 100644 --- a/server/aws-lsp-codewhisperer/src/client/token/bearer-token-service.json +++ b/server/aws-lsp-codewhisperer/src/client/token/bearer-token-service.json @@ -3622,6 +3622,10 @@ "shape": "Models", "documentation": "

List of available models

" }, + "defaultModel": { + "shape": "Model", + "documentation": "

Default model set by the client

" + }, "nextToken": { "shape": "Base64EncodedPaginationToken", "documentation": "

Token for retrieving the next page of results

" @@ -3955,6 +3959,10 @@ "shape": "ModelId", "documentation": "

Unique identifier for the model

" }, + "modelName": { + "shape": "ModelName", + "documentation": "

User-facing display name

" + }, "description": { "shape": "Description", "documentation": "

Description of the model

" @@ -3972,6 +3980,13 @@ "min": 1, "pattern": "[a-zA-Z0-9_:.-]+" }, + "ModelName": { + "type": "string", + "documentation": "

Identifier for the model Name

", + "max": 1024, + "min": 1, + "pattern": "[a-zA-Z0-9-_.]+" + }, "ModelMetadata": { "type": "structure", "members": { diff --git a/server/aws-lsp-codewhisperer/src/client/token/codewhispererbearertokenclient.d.ts b/server/aws-lsp-codewhisperer/src/client/token/codewhispererbearertokenclient.d.ts index c885612888..34aa384c14 100644 --- a/server/aws-lsp-codewhisperer/src/client/token/codewhispererbearertokenclient.d.ts +++ b/server/aws-lsp-codewhisperer/src/client/token/codewhispererbearertokenclient.d.ts @@ -3,6 +3,7 @@ * THIS FILE IS AUTOGENERATED BY 'generateServiceClient.ts'. * DO NOT EDIT BY HAND. */ + import {Request} from 'aws-sdk/lib/request'; import {Response} from 'aws-sdk/lib/response'; import {AWSError} from 'aws-sdk/lib/error'; @@ -1132,6 +1133,10 @@ declare namespace CodeWhispererBearerTokenClient { * List of available models */ models: Models; + /** + * Default model set by the client + */ + defaultModel?: Model; /** * Token for retrieving the next page of results */ @@ -1240,6 +1245,10 @@ declare namespace CodeWhispererBearerTokenClient { * Unique identifier for the model */ modelId: ModelId; + /** + * User-facing display name + */ + modelName?: ModelName; /** * Description of the model */ @@ -1250,6 +1259,7 @@ declare namespace CodeWhispererBearerTokenClient { modelMetadata?: ModelMetadata; } export type ModelId = string; + export type ModelName = string; export interface ModelMetadata { /** * Maximum number of input tokens the model can process diff --git a/server/aws-lsp-codewhisperer/src/language-server/agenticChat/agenticChatController.test.ts b/server/aws-lsp-codewhisperer/src/language-server/agenticChat/agenticChatController.test.ts index a307ce3fc6..c1842a6008 100644 --- a/server/aws-lsp-codewhisperer/src/language-server/agenticChat/agenticChatController.test.ts +++ b/server/aws-lsp-codewhisperer/src/language-server/agenticChat/agenticChatController.test.ts @@ -32,6 +32,7 @@ import { InlineChatResult, CancellationTokenSource, ContextCommand, + ChatUpdateParams, } from '@aws/language-server-runtimes/server-interface' import { TestFeatures } from '@aws/language-server-runtimes/testing' import * as assert from 'assert' @@ -56,7 +57,7 @@ import { LocalProjectContextController } from '../../shared/localProjectContextC import { CancellationError } from '@aws/lsp-core' import { ToolApprovalException } from './tools/toolShared' import * as constants from './constants/constants' -import { GENERATE_ASSISTANT_RESPONSE_INPUT_LIMIT, GENERIC_ERROR_MS } from './constants/constants' +import { DEFAULT_MODEL_ID, GENERATE_ASSISTANT_RESPONSE_INPUT_LIMIT, GENERIC_ERROR_MS } from './constants/constants' import { MISSING_BEARER_TOKEN_ERROR } from '../../shared/constants' import { AmazonQError, @@ -368,17 +369,6 @@ describe('AgenticChatController', () => { sinon.assert.calledWithExactly(activeTabSpy.set, mockTabId) }) - it('onTabAdd updates model ID in chat options and session', () => { - const modelId = 'test-model-id' - sinon.stub(ChatDatabase.prototype, 'getModelId').returns(modelId) - chatController.onTabAdd({ tabId: mockTabId }) - - sinon.assert.calledWithExactly(testFeatures.chat.chatOptionsUpdate, { modelId, tabId: mockTabId }) - - const session = chatSessionManagementService.getSession(mockTabId).data - assert.strictEqual(session!.modelId, modelId) - }) - it('onTabChange sets active tab id in telemetryController and emits metrics', () => { chatController.onTabChange({ tabId: mockTabId }) @@ -3003,153 +2993,251 @@ ${' '.repeat(8)}} }) describe('onListAvailableModels', () => { - let tokenServiceManagerStub: sinon.SinonStub + let isCachedModelsValidStub: sinon.SinonStub + let getCachedModelsStub: sinon.SinonStub + let setCachedModelsStub: sinon.SinonStub + let getConnectionTypeStub: sinon.SinonStub + let getActiveProfileArnStub: sinon.SinonStub + let getCodewhispererServiceStub: sinon.SinonStub + let listAvailableModelsStub: sinon.SinonStub beforeEach(() => { - // Create a session with a model ID + // Create a session chatController.onTabAdd({ tabId: mockTabId }) - const session = chatSessionManagementService.getSession(mockTabId).data! - session.modelId = 'CLAUDE_3_7_SONNET_20250219_V1_0' - // Stub the getRegion method - tokenServiceManagerStub = sinon.stub(AmazonQTokenServiceManager.prototype, 'getRegion') + // Stub ChatDatabase methods + isCachedModelsValidStub = sinon.stub(ChatDatabase.prototype, 'isCachedModelsValid') + getCachedModelsStub = sinon.stub(ChatDatabase.prototype, 'getCachedModels') + setCachedModelsStub = sinon.stub(ChatDatabase.prototype, 'setCachedModels') + + // Stub AmazonQTokenServiceManager methods + getConnectionTypeStub = sinon.stub(AmazonQTokenServiceManager.prototype, 'getConnectionType') + getActiveProfileArnStub = sinon.stub(AmazonQTokenServiceManager.prototype, 'getActiveProfileArn') + getCodewhispererServiceStub = sinon.stub(AmazonQTokenServiceManager.prototype, 'getCodewhispererService') + + // Mock listAvailableModels method + listAvailableModelsStub = sinon.stub() + getCodewhispererServiceStub.returns({ + listAvailableModels: listAvailableModelsStub, + }) }) afterEach(() => { - tokenServiceManagerStub.restore() - }) + isCachedModelsValidStub.restore() + getCachedModelsStub.restore() + setCachedModelsStub.restore() + getConnectionTypeStub.restore() + getActiveProfileArnStub.restore() + getCodewhispererServiceStub.restore() + }) + + describe('ListAvailableModels Cache scenarios', () => { + it('should return cached models when cache is valid', async () => { + // Setup valid cache + isCachedModelsValidStub.returns(true) + const cachedData = { + models: [ + { id: 'model1', name: 'Model 1' }, + { id: 'model2', name: 'Model 2' }, + ], + defaultModelId: 'model1', + timestamp: Date.now(), + } + getCachedModelsStub.returns(cachedData) - it('should return all available models for us-east-1 region', async () => { - // Set up the region to be us-east-1 - tokenServiceManagerStub.returns('us-east-1') + const session = chatSessionManagementService.getSession(mockTabId).data! + session.modelId = 'model1' - // Call the method - const params = { tabId: mockTabId } - const result = await chatController.onListAvailableModels(params) + const result = await chatController.onListAvailableModels({ tabId: mockTabId }) - // Verify the result - assert.strictEqual(result.tabId, mockTabId) - assert.strictEqual(result.models.length, 2) - assert.strictEqual(result.selectedModelId, 'CLAUDE_SONNET_4_20250514_V1_0') + // Verify cached data is used + assert.strictEqual(result.tabId, mockTabId) + assert.deepStrictEqual(result.models, cachedData.models) + assert.strictEqual(result.selectedModelId, 'model1') - // Check that the models include both Claude versions - const modelIds = result.models.map(model => model.id) - assert.ok(modelIds.includes('CLAUDE_SONNET_4_20250514_V1_0')) - assert.ok(modelIds.includes('CLAUDE_3_7_SONNET_20250219_V1_0')) - }) + // Verify API was not called + sinon.assert.notCalled(listAvailableModelsStub) + sinon.assert.notCalled(setCachedModelsStub) + }) - it('should return all available models for eu-central-1 region', async () => { - // Set up the region to be eu-central-1 - tokenServiceManagerStub.returns('eu-central-1') + it('should return cached models when cache is valid but has empty models array', async () => { + // Setup cache with empty models + isCachedModelsValidStub.returns(true) + const cachedData = { + models: [], + defaultModelId: undefined, + timestamp: Date.now(), + } + getCachedModelsStub.returns(cachedData) - // Call the method - const params = { tabId: mockTabId } - const result = await chatController.onListAvailableModels(params) + // Should fall back to API call since models array is empty + getConnectionTypeStub.returns('builderId') + getActiveProfileArnStub.returns('test-arn') + listAvailableModelsStub.resolves({ + models: { + model1: { modelId: 'model1' }, + model2: { modelId: 'model2' }, + }, + defaultModel: { modelId: 'model1' }, + }) - // Verify the result - assert.strictEqual(result.tabId, mockTabId) - assert.strictEqual(result.models.length, 2) - assert.strictEqual(result.selectedModelId, 'CLAUDE_SONNET_4_20250514_V1_0') + await chatController.onListAvailableModels({ tabId: mockTabId }) - // Check that the models include both Claude versions - const modelIds = result.models.map(model => model.id) - assert.ok(modelIds.includes('CLAUDE_SONNET_4_20250514_V1_0')) - assert.ok(modelIds.includes('CLAUDE_3_7_SONNET_20250219_V1_0')) - }) + // Verify API was called due to empty cached models + sinon.assert.calledOnce(listAvailableModelsStub) + sinon.assert.calledOnce(setCachedModelsStub) + }) - it('should return all models when region is unknown', async () => { - // Set up the region to be unknown - tokenServiceManagerStub.returns('unknown-region') + it('should return cached models when cache is valid but cachedData is null', async () => { + // Setup cache as valid but returns null + isCachedModelsValidStub.returns(true) + getCachedModelsStub.returns(null) - // Call the method - const params = { tabId: mockTabId } - const result = await chatController.onListAvailableModels(params) + // Should fall back to API call + getConnectionTypeStub.returns('builderId') + getActiveProfileArnStub.returns('test-arn') + listAvailableModelsStub.resolves({ + models: { + model1: { modelId: 'model1' }, + }, + defaultModel: { modelId: 'model1' }, + }) - // Verify the result - assert.strictEqual(result.tabId, mockTabId) - assert.strictEqual(result.models.length, 2) - assert.strictEqual(result.selectedModelId, 'CLAUDE_SONNET_4_20250514_V1_0') + await chatController.onListAvailableModels({ tabId: mockTabId }) + + // Verify API was called + sinon.assert.calledOnce(listAvailableModelsStub) + }) }) - it('should return undefined for selectedModelId when no session data exists', async () => { - // Set up the session to return no session (failure case) - const getSessionStub = sinon.stub(chatSessionManagementService, 'getSession') - getSessionStub.returns({ - data: undefined, - success: false, - error: 'error', + describe('ListAvailableModels API call scenarios', () => { + beforeEach(() => { + // Setup invalid cache to force API call + isCachedModelsValidStub.returns(false) }) - // Call the method - const params = { tabId: 'non-existent-tab' } - const result = await chatController.onListAvailableModels(params) + it('should fetch models from API when cache is invalid', async () => { + getConnectionTypeStub.returns('builderId') + getActiveProfileArnStub.returns('test-profile-arn') - // Verify the result - assert.strictEqual(result.tabId, 'non-existent-tab') - assert.strictEqual(result.models.length, 2) - assert.strictEqual(result.selectedModelId, undefined) + const mockApiResponse = { + models: { + 'claude-3-sonnet': { modelId: 'claude-3-sonnet' }, + 'claude-4-sonnet': { modelId: 'claude-4-sonnet' }, + }, + defaultModel: { modelId: 'claude-3-sonnet' }, + } + listAvailableModelsStub.resolves(mockApiResponse) - getSessionStub.restore() - }) + const result = await chatController.onListAvailableModels({ tabId: mockTabId }) + + // Verify API call was made with correct parameters + sinon.assert.calledOnceWithExactly(listAvailableModelsStub, { + origin: 'IDE', + profileArn: 'test-profile-arn', + }) - it('should fallback to latest available model when saved model is not available in current region', async () => { - // Import the module to stub - const modelSelection = await import('./constants/modelSelection') + // Verify result structure + assert.strictEqual(result.tabId, mockTabId) + assert.strictEqual(result.models.length, 2) + assert.deepStrictEqual(result.models, [ + { id: 'claude-3-sonnet', name: 'claude-3-sonnet' }, + { id: 'claude-4-sonnet', name: 'claude-4-sonnet' }, + ]) - // Create a mock region with only Claude 3.7 - const mockModelOptionsForRegion = { - ...modelSelection.MODEL_OPTIONS_FOR_REGION, - 'test-region-limited': [ - { - id: 'CLAUDE_3_7_SONNET_20250219_V1_0', - name: 'Claude Sonnet 3.7', - }, - ], - } + // Verify cache was updated + sinon.assert.calledOnceWithExactly(setCachedModelsStub, result.models, 'claude-3-sonnet') + }) - // Stub the MODEL_OPTIONS_FOR_REGION - const modelOptionsStub = sinon - .stub(modelSelection, 'MODEL_OPTIONS_FOR_REGION') - .value(mockModelOptionsForRegion) + it('should fall back to hardcoded models when API call fails', async () => { + getConnectionTypeStub.returns('builderId') + listAvailableModelsStub.rejects(new Error('API Error')) - // Set up the region to be the test region (which only has Claude 3.7) - tokenServiceManagerStub.returns('test-region-limited') + const result = await chatController.onListAvailableModels({ tabId: mockTabId }) - // Mock database to return Claude Sonnet 4 (not available in test-region-limited) - const getModelIdStub = sinon.stub(ChatDatabase.prototype, 'getModelId') - getModelIdStub.returns('CLAUDE_SONNET_4_20250514_V1_0') + // Verify fallback to FALLBACK_MODEL_OPTIONS + assert.strictEqual(result.tabId, mockTabId) + assert.strictEqual(result.models.length, 2) // FALLBACK_MODEL_OPTIONS length - // Call the method - const params = { tabId: mockTabId } - const result = await chatController.onListAvailableModels(params) + // Verify cache was not updated due to error + sinon.assert.notCalled(setCachedModelsStub) + }) - // Verify the result falls back to available model - assert.strictEqual(result.tabId, mockTabId) - assert.strictEqual(result.models.length, 1) - assert.strictEqual(result.selectedModelId, 'CLAUDE_3_7_SONNET_20250219_V1_0') + it('should handle API response with no defaultModel', async () => { + getConnectionTypeStub.returns('builderId') - getModelIdStub.restore() - modelOptionsStub.restore() + const mockApiResponse = { + models: { + model1: { modelId: 'model1' }, + }, + defaultModel: undefined, // No default model + } + listAvailableModelsStub.resolves(mockApiResponse) + + const result = await chatController.onListAvailableModels({ tabId: mockTabId }) + + // Verify cache was updated with undefined defaultModelId + sinon.assert.calledOnceWithExactly(setCachedModelsStub, result.models, undefined) + }) }) - it('should use saved model when it is available in current region', async () => { - // Set up the region to be us-east-1 (which has both models) - tokenServiceManagerStub.returns('us-east-1') + describe('Session and model selection scenarios', () => { + beforeEach(() => { + // Setup cache to avoid API calls in these tests + isCachedModelsValidStub.returns(true) + getCachedModelsStub.returns({ + models: [ + { id: 'model1', name: 'Model 1' }, + { id: 'model2', name: 'Model 2' }, + ], + defaultModelId: 'model1', + timestamp: Date.now(), + }) + }) + + it('should return default model when session fails to load', async () => { + const getSessionStub = sinon.stub(chatSessionManagementService, 'getSession') + getSessionStub.returns({ + data: undefined, + success: false, + error: 'Session not found', + }) + + const result = await chatController.onListAvailableModels({ tabId: 'invalid-tab' }) - // Mock database to return Claude 3.7 (available in us-east-1) - const getModelIdStub = sinon.stub(ChatDatabase.prototype, 'getModelId') - getModelIdStub.returns('CLAUDE_3_7_SONNET_20250219_V1_0') + assert.strictEqual(result.tabId, 'invalid-tab') + assert.strictEqual(result.selectedModelId, 'model1') - // Call the method - const params = { tabId: mockTabId } - const result = await chatController.onListAvailableModels(params) + getSessionStub.restore() + }) + + it('should use defaultModelId from cache when session has no modelId', async () => { + const session = chatSessionManagementService.getSession(mockTabId).data! + session.modelId = undefined - // Verify the result uses the saved model - assert.strictEqual(result.tabId, mockTabId) - assert.strictEqual(result.models.length, 2) - assert.strictEqual(result.selectedModelId, 'CLAUDE_3_7_SONNET_20250219_V1_0') + const result = await chatController.onListAvailableModels({ tabId: mockTabId }) - getModelIdStub.restore() + assert.strictEqual(result.selectedModelId, 'model1') // defaultModelId from cache + // Verify session modelId is updated + assert.strictEqual(session.modelId, 'model1') + }) + + it('should fall back to default model when session has no modelId and no defaultModelId in cache', async () => { + getCachedModelsStub.returns({ + models: [{ id: 'model1', name: 'Model 1' }], + defaultModelId: undefined, // No default model + timestamp: Date.now(), + }) + + const session = chatSessionManagementService.getSession(mockTabId).data! + session.modelId = undefined + + const result = await chatController.onListAvailableModels({ tabId: mockTabId }) + + assert.strictEqual(result.selectedModelId, 'claude-sonnet-4') // FALLBACK_MODEL_RECORD[DEFAULT_MODEL_ID].label + // Verify session modelId is updated + assert.strictEqual(session.modelId, 'claude-sonnet-4') + }) }) }) 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 991d95a6ad..8cdba3d291 100644 --- a/server/aws-lsp-codewhisperer/src/language-server/agenticChat/agenticChatController.ts +++ b/server/aws-lsp-codewhisperer/src/language-server/agenticChat/agenticChatController.ts @@ -24,7 +24,6 @@ import { GREP_SEARCH, FILE_SEARCH, EXECUTE_BASH, - CODE_REVIEW, BUTTON_RUN_SHELL_COMMAND, BUTTON_REJECT_SHELL_COMMAND, BUTTON_REJECT_MCP_TOOL, @@ -67,8 +66,6 @@ import { ListAvailableModelsResult, OpenFileDialogParams, OpenFileDialogResult, -} from '@aws/language-server-runtimes/protocol' -import { ApplyWorkspaceEditParams, ErrorCodes, FeedbackParams, @@ -83,6 +80,7 @@ import { TabBarActionParams, CreatePromptParams, FileClickParams, + Model, } from '@aws/language-server-runtimes/protocol' import { CancellationToken, @@ -222,14 +220,14 @@ import { Message as DbMessage, messageToStreamingMessage, } from './tools/chatDb/util' -import { MODEL_OPTIONS, MODEL_OPTIONS_FOR_REGION } from './constants/modelSelection' +import { FALLBACK_MODEL_OPTIONS, FALLBACK_MODEL_RECORD, BEDROCK_MODEL_TO_MODEL_ID } from './constants/modelSelection' import { DEFAULT_IMAGE_VERIFICATION_OPTIONS, verifyServerImage } from '../../shared/imageVerification' import { sanitize } from '@aws/lsp-core/out/util/path' -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' +import { IDE } from '../../shared/constants' import { IdleWorkspaceManager } from '../workspaceContext/IdleWorkspaceManager' type ChatHandlers = Omit< @@ -352,7 +350,7 @@ export class AgenticChatController implements ChatHandlers { // @ts-ignore this.#features.chat.chatOptionsUpdate({ region }) }) - this.#chatHistoryDb = new ChatDatabase(features) + this.#chatHistoryDb = ChatDatabase.getInstance(features) this.#tabBarController = new TabBarController( features, this.#chatHistoryDb, @@ -681,25 +679,133 @@ export class AgenticChatController implements ChatHandlers { return this.#mcpEventHandler.onMcpServerClick(params) } + /** + * Fetches available models either from cache or API + * If cache is valid (less than 5 minutes old), returns cached models + * If cache is invalid or empty, makes an API call and stores results in cache + * If the API throws errors (e.g., throttling), falls back to default models + */ + async #fetchModelsWithCache(): Promise<{ models: Model[]; defaultModelId?: string; errorFromAPI: boolean }> { + let models: Model[] = [] + let defaultModelId: string | undefined + let errorFromAPI = false + + // Check if cache is valid (less than 5 minutes old) + if (this.#chatHistoryDb.isCachedModelsValid()) { + const cachedData = this.#chatHistoryDb.getCachedModels() + if (cachedData && cachedData.models && cachedData.models.length > 0) { + this.#log('Using cached models, last updated at:', new Date(cachedData.timestamp).toISOString()) + return { + models: cachedData.models, + defaultModelId: cachedData.defaultModelId, + errorFromAPI: false, + } + } + } + + // If cache is invalid or empty, make an API call + this.#log('Cache miss or expired, fetching models from API') + try { + const client = AmazonQTokenServiceManager.getInstance().getCodewhispererService() + const responseResult = await client.listAvailableModels({ + origin: IDE, + profileArn: AmazonQTokenServiceManager.getInstance().getConnectionType() + ? AmazonQTokenServiceManager.getInstance().getActiveProfileArn() + : undefined, + }) + + // Wait for the response to be completed before proceeding + this.#log('Model Response: ', JSON.stringify(responseResult, null, 2)) + models = Object.values(responseResult.models).map(({ modelId, modelName }) => ({ + id: modelId, + name: modelName ?? modelId, + })) + defaultModelId = responseResult.defaultModel?.modelId + + // Cache the models with defaultModelId + this.#chatHistoryDb.setCachedModels(models, defaultModelId) + } catch (err) { + // In case of API throttling or other errors, fall back to hardcoded models + this.#log('Error fetching models from API, using fallback models:', fmtError(err)) + errorFromAPI = true + models = FALLBACK_MODEL_OPTIONS + } + + return { + models, + defaultModelId, + errorFromAPI, + } + } + + /** + * This function handles the model selection process for the chat interface. + * It first attempts to retrieve models from cache or API, then determines the appropriate model to select + * based on the following priority: + * 1. When errors occur or session is invalid: Use the default model as a fallback + * 2. When user has previously selected a model: Use that model (or its mapped version if the model ID has changed) + * 3. When there's a default model from the API: Use the server-recommended default model + * 4. Last resort: Use the newest model defined in modelSelection constants + * + * This ensures users maintain consistent model selection across sessions while also handling + * API failures and model ID migrations gracefully. + */ async onListAvailableModels(params: ListAvailableModelsParams): Promise { - const region = AmazonQTokenServiceManager.getInstance().getRegion() - const models = region && MODEL_OPTIONS_FOR_REGION[region] ? MODEL_OPTIONS_FOR_REGION[region] : MODEL_OPTIONS + // Get models from cache or API + const { models, defaultModelId, errorFromAPI } = await this.#fetchModelsWithCache() + + // Get the first fallback model option as default + const defaultModelOption = FALLBACK_MODEL_OPTIONS[1] + const DEFAULT_MODEL_ID = defaultModelId || defaultModelOption?.id const sessionResult = this.#chatSessionManagementService.getSession(params.tabId) const { data: session, success } = sessionResult - if (!success) { + + // Handle error cases by returning default model + if (!success || errorFromAPI) { return { tabId: params.tabId, models: models, + selectedModelId: DEFAULT_MODEL_ID, } } - const savedModelId = this.#chatHistoryDb.getModelId() - const selectedModelId = - savedModelId && models.some(model => model.id === savedModelId) - ? savedModelId - : getLatestAvailableModel(region).id + // Determine selected model ID based on priority + let selectedModelId: string + let modelId = this.#chatHistoryDb.getModelId() + + // Helper function to get model label from FALLBACK_MODEL_RECORD + const getModelLabel = (modelKey: string) => + FALLBACK_MODEL_RECORD[modelKey as keyof typeof FALLBACK_MODEL_RECORD]?.label || modelKey + + // Helper function to map enum model ID to API model ID + const getMappedModelId = (modelKey: string) => + BEDROCK_MODEL_TO_MODEL_ID[modelKey as keyof typeof BEDROCK_MODEL_TO_MODEL_ID] || modelKey + + // Determine selected model ID based on priority + if (modelId) { + const mappedModelId = getMappedModelId(modelId) + + // Priority 1: Use mapped modelId if it exists in available models from backend + if (models.some(model => model.id === mappedModelId)) { + selectedModelId = mappedModelId + } + // Priority 2: Use mapped version if modelId exists in FALLBACK_MODEL_RECORD and no backend models available + else if (models.length === 0 && modelId in FALLBACK_MODEL_RECORD) { + selectedModelId = getModelLabel(modelId) + } + // Priority 3: Fall back to default or system default + else { + selectedModelId = defaultModelId || getMappedModelId(DEFAULT_MODEL_ID) + } + } else { + // No user-selected model - use API default or system default + selectedModelId = defaultModelId || getMappedModelId(DEFAULT_MODEL_ID) + } + + // Store the selected model in the session session.modelId = selectedModelId + return { tabId: params.tabId, models: models, @@ -3580,25 +3686,6 @@ export class AgenticChatController implements ChatHandlers { onSourceLinkClick() {} - /** - * @deprecated use aws/chat/listAvailableModels server request instead - */ - #legacySetModelId(tabId: string, session: ChatSessionService) { - // Since model selection is mandatory, the only time modelId is not set is when the chat history is empty. - // In that case, we use the default modelId. - let modelId = this.#chatHistoryDb.getModelId() ?? DEFAULT_MODEL_ID - - const region = AmazonQTokenServiceManager.getInstance().getRegion() - if (region === 'eu-central-1') { - // Only 3.7 Sonnet is available in eu-central-1 for now - modelId = 'CLAUDE_3_7_SONNET_20250219_V1_0' - // @ts-ignore - this.#features.chat.chatOptionsUpdate({ region }) - } - this.#features.chat.chatOptionsUpdate({ modelId: modelId, tabId: tabId }) - session.modelId = modelId - } - onTabAdd(params: TabAddParams) { this.#telemetryController.activeTabId = params.tabId @@ -3611,11 +3698,14 @@ export class AgenticChatController implements ChatHandlers { if (!success) { return new ResponseError(ErrorCodes.InternalError, sessionResult.error) } - this.#legacySetModelId(params.tabId, session) // Get the saved pair programming mode from the database or default to true if not found const savedPairProgrammingMode = this.#chatHistoryDb.getPairProgrammingMode() session.pairProgrammingMode = savedPairProgrammingMode !== undefined ? savedPairProgrammingMode : true + if (session) { + // Set the logging object on the session + session.setLogging(this.#features.logging) + } // Update the client with the initial pair programming mode this.#features.chat.chatOptionsUpdate({ @@ -3623,11 +3713,6 @@ export class AgenticChatController implements ChatHandlers { // Type assertion to support pairProgrammingMode ...(session.pairProgrammingMode !== undefined ? { pairProgrammingMode: session.pairProgrammingMode } : {}), } as ChatUpdateParams) - - if (success && session) { - // Set the logging object on the session - session.setLogging(this.#features.logging) - } this.setPaidTierMode(params.tabId) } diff --git a/server/aws-lsp-codewhisperer/src/language-server/agenticChat/constants/modelSelection.test.ts b/server/aws-lsp-codewhisperer/src/language-server/agenticChat/constants/modelSelection.test.ts index 3fe300d2fd..f037470bec 100644 --- a/server/aws-lsp-codewhisperer/src/language-server/agenticChat/constants/modelSelection.test.ts +++ b/server/aws-lsp-codewhisperer/src/language-server/agenticChat/constants/modelSelection.test.ts @@ -1,19 +1,19 @@ import * as assert from 'assert' -import { MODEL_OPTIONS, MODEL_OPTIONS_FOR_REGION } from './modelSelection' +import { FALLBACK_MODEL_OPTIONS } from './modelSelection' describe('modelSelection', () => { describe('modelOptions', () => { it('should contain the correct model options', () => { - assert.ok(Array.isArray(MODEL_OPTIONS), 'modelOptions should be an array') - assert.strictEqual(MODEL_OPTIONS.length, 2, 'modelOptions should have 2 items') + assert.ok(Array.isArray(FALLBACK_MODEL_OPTIONS), 'modelOptions should be an array') + assert.strictEqual(FALLBACK_MODEL_OPTIONS.length, 2, 'modelOptions should have 2 items') // Check that the array contains the expected models - const modelIds = MODEL_OPTIONS.map(model => model.id) - assert.ok(modelIds.includes('CLAUDE_SONNET_4_20250514_V1_0'), 'Should include Claude Sonnet 4') - assert.ok(modelIds.includes('CLAUDE_3_7_SONNET_20250219_V1_0'), 'Should include Claude Sonnet 3.7') + const modelIds = FALLBACK_MODEL_OPTIONS.map(model => model.id) + assert.ok(modelIds.includes('CLAUDE_SONNET_4_20250514_V1_0'), 'Should include claude-sonnet-4') + assert.ok(modelIds.includes('CLAUDE_3_7_SONNET_20250219_V1_0'), 'Should include claude-3.7-sonnet') // Check that each model has the required properties - MODEL_OPTIONS.forEach(model => { + FALLBACK_MODEL_OPTIONS.forEach(model => { assert.ok('id' in model, 'Model should have id property') assert.ok('name' in model, 'Model should have name property') assert.strictEqual(typeof model.id, 'string', 'Model id should be a string') @@ -21,47 +21,11 @@ describe('modelSelection', () => { }) // Check specific model names - const claudeSonnet4 = MODEL_OPTIONS.find(model => model.id === 'CLAUDE_SONNET_4_20250514_V1_0') - const claudeSonnet37 = MODEL_OPTIONS.find(model => model.id === 'CLAUDE_3_7_SONNET_20250219_V1_0') + const claudeSonnet4 = FALLBACK_MODEL_OPTIONS.find(model => model.id === 'CLAUDE_SONNET_4_20250514_V1_0') + const claudeSonnet37 = FALLBACK_MODEL_OPTIONS.find(model => model.id === 'CLAUDE_3_7_SONNET_20250219_V1_0') - assert.strictEqual(claudeSonnet4?.name, 'Claude Sonnet 4', 'Claude Sonnet 4 should have correct name') - assert.strictEqual(claudeSonnet37?.name, 'Claude Sonnet 3.7', 'Claude Sonnet 3.7 should have correct name') - }) - }) - - describe('modelOptionsForRegion', () => { - it('should provide all models for us-east-1 region', () => { - const usEast1Models = MODEL_OPTIONS_FOR_REGION['us-east-1'] - assert.deepStrictEqual(usEast1Models, MODEL_OPTIONS, 'us-east-1 should have all models') - assert.strictEqual(usEast1Models.length, 2, 'us-east-1 should have 2 models') - - const modelIds = usEast1Models.map(model => model.id) - assert.ok(modelIds.includes('CLAUDE_SONNET_4_20250514_V1_0'), 'us-east-1 should include Claude Sonnet 4') - assert.ok( - modelIds.includes('CLAUDE_3_7_SONNET_20250219_V1_0'), - 'us-east-1 should include Claude Sonnet 3.7' - ) - }) - - it('should provide all models for eu-central-1 region', () => { - const euCentral1Models = MODEL_OPTIONS_FOR_REGION['eu-central-1'] - assert.deepStrictEqual(euCentral1Models, MODEL_OPTIONS, 'us-east-1 should have all models') - assert.strictEqual(euCentral1Models.length, 2, 'us-east-1 should have 2 models') - - const modelIds = euCentral1Models.map(model => model.id) - assert.ok(modelIds.includes('CLAUDE_SONNET_4_20250514_V1_0'), 'eu-central-1 should include Claude Sonnet 4') - assert.ok( - modelIds.includes('CLAUDE_3_7_SONNET_20250219_V1_0'), - 'eu-central-1 should include Claude Sonnet 3.7' - ) - }) - - it('should fall back to all models for unknown regions', () => { - // Test with a region that doesn't exist in the modelOptionsForRegion map - const unknownRegionModels = MODEL_OPTIONS_FOR_REGION['unknown-region'] - - // Should be undefined since the region doesn't exist in the map - assert.strictEqual(unknownRegionModels, undefined, 'Unknown region should return undefined') + assert.strictEqual(claudeSonnet4?.name, 'Claude Sonnet 4', 'claude-sonnet-4 should have correct name') + assert.strictEqual(claudeSonnet37?.name, 'Claude 3.7 Sonnet', 'claude-3.7-sonnet should have correct name') }) }) }) diff --git a/server/aws-lsp-codewhisperer/src/language-server/agenticChat/constants/modelSelection.ts b/server/aws-lsp-codewhisperer/src/language-server/agenticChat/constants/modelSelection.ts index cbee85f562..46c5446c8c 100644 --- a/server/aws-lsp-codewhisperer/src/language-server/agenticChat/constants/modelSelection.ts +++ b/server/aws-lsp-codewhisperer/src/language-server/agenticChat/constants/modelSelection.ts @@ -1,5 +1,8 @@ import { ListAvailableModelsResult } from '@aws/language-server-runtimes/protocol' +/** + * @deprecated Do not add new models to the enum. + */ export enum BedrockModel { CLAUDE_SONNET_4_20250514_V1_0 = 'CLAUDE_SONNET_4_20250514_V1_0', CLAUDE_3_7_SONNET_20250219_V1_0 = 'CLAUDE_3_7_SONNET_20250219_V1_0', @@ -9,19 +12,19 @@ type ModelDetails = { label: string } -const MODEL_RECORD: Record = { - [BedrockModel.CLAUDE_3_7_SONNET_20250219_V1_0]: { label: 'Claude Sonnet 3.7' }, +export const FALLBACK_MODEL_RECORD: Record = { + [BedrockModel.CLAUDE_3_7_SONNET_20250219_V1_0]: { label: 'Claude 3.7 Sonnet' }, [BedrockModel.CLAUDE_SONNET_4_20250514_V1_0]: { label: 'Claude Sonnet 4' }, } -export const MODEL_OPTIONS: ListAvailableModelsResult['models'] = Object.entries(MODEL_RECORD).map( +export const BEDROCK_MODEL_TO_MODEL_ID: Record = { + [BedrockModel.CLAUDE_3_7_SONNET_20250219_V1_0]: 'claude-3.7-sonnet', + [BedrockModel.CLAUDE_SONNET_4_20250514_V1_0]: 'claude-sonnet-4', +} + +export const FALLBACK_MODEL_OPTIONS: ListAvailableModelsResult['models'] = Object.entries(FALLBACK_MODEL_RECORD).map( ([value, { label }]) => ({ id: value, name: label, }) ) - -export const MODEL_OPTIONS_FOR_REGION: Record = { - 'us-east-1': MODEL_OPTIONS, - 'eu-central-1': MODEL_OPTIONS, -} diff --git a/server/aws-lsp-codewhisperer/src/language-server/agenticChat/tools/chatDb/chatDb.test.ts b/server/aws-lsp-codewhisperer/src/language-server/agenticChat/tools/chatDb/chatDb.test.ts index f4f61f955b..aed5a30643 100644 --- a/server/aws-lsp-codewhisperer/src/language-server/agenticChat/tools/chatDb/chatDb.test.ts +++ b/server/aws-lsp-codewhisperer/src/language-server/agenticChat/tools/chatDb/chatDb.test.ts @@ -57,7 +57,7 @@ describe('ChatDatabase', () => { }, } as unknown as Features - chatDb = new ChatDatabase(mockFeatures) + chatDb = ChatDatabase.getInstance(mockFeatures) }) afterEach(() => { @@ -665,6 +665,73 @@ describe('ChatDatabase', () => { ) }) }) + + describe('Model Cache Management', () => { + beforeEach(async () => { + await chatDb.databaseInitialize(0) + }) + + it('should cache and retrieve models', () => { + const models = [{ id: 'model-1', name: 'Test Model' }] + const defaultModelId = 'model-1' + + chatDb.setCachedModels(models, defaultModelId) + const cached = chatDb.getCachedModels() + + assert.ok(cached, 'Should return cached data') + assert.deepStrictEqual(cached.models, models) + assert.strictEqual(cached.defaultModelId, defaultModelId) + assert.ok(cached.timestamp > 0, 'Should have timestamp') + }) + + it('should validate cache expiry', () => { + const models = [{ id: 'model-1', name: 'Test Model' }] + chatDb.setCachedModels(models) + + // Mock isCachedValid to return false (expired) + const isCachedValidStub = sinon.stub(util, 'isCachedValid').returns(false) + + assert.strictEqual(chatDb.isCachedModelsValid(), false) + + isCachedValidStub.restore() + }) + + it('should clear cached models', () => { + const models = [{ id: 'model-1', name: 'Test Model' }] + chatDb.setCachedModels(models) + + // Verify cache exists + assert.ok(chatDb.getCachedModels(), 'Cache should exist before clearing') + + chatDb.clearCachedModels() + + // Verify cache is cleared + assert.strictEqual(chatDb.getCachedModels(), undefined, 'Cache should be cleared') + }) + + it('should clear model cache via static method when instance exists', () => { + const models = [{ id: 'model-1', name: 'Test Model' }] + chatDb.setCachedModels(models) + + // Verify cache exists + assert.ok(chatDb.getCachedModels(), 'Cache should exist before clearing') + + ChatDatabase.clearModelCache() + + // Verify cache is cleared + assert.strictEqual(chatDb.getCachedModels(), undefined, 'Cache should be cleared via static method') + }) + + it('should handle static clearModelCache when no instance exists', () => { + // Close current instance + chatDb.close() + + // Should not throw when no instance exists + assert.doesNotThrow(() => { + ChatDatabase.clearModelCache() + }, 'Should not throw when no instance exists') + }) + }) }) function uuid(): `${string}-${string}-${string}-${string}-${string}` { throw new Error('Function not implemented.') diff --git a/server/aws-lsp-codewhisperer/src/language-server/agenticChat/tools/chatDb/chatDb.ts b/server/aws-lsp-codewhisperer/src/language-server/agenticChat/tools/chatDb/chatDb.ts index dfcf21308c..2e4f7a099d 100644 --- a/server/aws-lsp-codewhisperer/src/language-server/agenticChat/tools/chatDb/chatDb.ts +++ b/server/aws-lsp-codewhisperer/src/language-server/agenticChat/tools/chatDb/chatDb.ts @@ -24,11 +24,12 @@ import { getMd5WorkspaceId, MessagesWithCharacterCount, estimateCharacterCountFromImageBlock, + isCachedValid, } from './util' import * as crypto from 'crypto' import * as path from 'path' import { Features } from '@aws/language-server-runtimes/server-interface/server' -import { ContextCommand, ConversationItemGroup } from '@aws/language-server-runtimes/protocol' +import { ContextCommand, ConversationItemGroup, Model } from '@aws/language-server-runtimes/protocol' import { ChatMessage, ToolResultStatus } from '@amzn/codewhisperer-streaming' import { ChatItemType } from '@aws/mynah-ui' import { getUserHomeDir } from '@aws/lsp-core/out/util/path' @@ -122,6 +123,12 @@ export class ChatDatabase { return ChatDatabase.#instance } + public static clearModelCache(): void { + if (ChatDatabase.#instance) { + ChatDatabase.#instance.clearCachedModels() + } + } + public close() { this.#db.close() ChatDatabase.#instance = undefined @@ -1084,6 +1091,48 @@ export class ChatDatabase { this.updateSettings({ modelId: modelId === '' ? undefined : modelId }) } + getCachedModels(): { models: Model[]; defaultModelId?: string; timestamp: number } | undefined { + const settings = this.getSettings() + if (settings?.cachedModels && settings?.modelCacheTimestamp) { + return { + models: settings.cachedModels, + defaultModelId: settings.cachedDefaultModelId, + timestamp: settings.modelCacheTimestamp, + } + } + return undefined + } + + setCachedModels(models: Model[], defaultModelId?: string): void { + const currentTimestamp = Date.now() + // Get existing settings to preserve fields like modelId + const existingSettings = this.getSettings() || { modelId: undefined } + this.updateSettings({ + ...existingSettings, + cachedModels: models, + cachedDefaultModelId: defaultModelId, + modelCacheTimestamp: currentTimestamp, + }) + this.#features.logging.log(`Models cached at timestamp: ${currentTimestamp}`) + } + + isCachedModelsValid(): boolean { + const cachedData = this.getCachedModels() + if (!cachedData) return false + return isCachedValid(cachedData.timestamp) + } + + clearCachedModels(): void { + const existingSettings = this.getSettings() || { modelId: undefined } + this.updateSettings({ + ...existingSettings, + cachedModels: undefined, + cachedDefaultModelId: undefined, + modelCacheTimestamp: undefined, + }) + this.#features.logging.log('Model cache cleared') + } + getPairProgrammingMode(): boolean | undefined { const settings = this.getSettings() return settings?.pairProgrammingMode diff --git a/server/aws-lsp-codewhisperer/src/language-server/agenticChat/tools/chatDb/util.ts b/server/aws-lsp-codewhisperer/src/language-server/agenticChat/tools/chatDb/util.ts index 8f3f46b9f4..77496cd96b 100644 --- a/server/aws-lsp-codewhisperer/src/language-server/agenticChat/tools/chatDb/util.ts +++ b/server/aws-lsp-codewhisperer/src/language-server/agenticChat/tools/chatDb/util.ts @@ -10,6 +10,7 @@ import { ConversationItem, ConversationItemGroup, IconType, + Model, ReferenceTrackerInformation, } from '@aws/language-server-runtimes/server-interface' import { @@ -84,6 +85,9 @@ export type Rules = { export type Settings = { modelId: string | undefined pairProgrammingMode?: boolean + cachedModels?: Model[] + cachedDefaultModelId?: string + modelCacheTimestamp?: number } export type Conversation = { @@ -131,6 +135,14 @@ export type MessagesWithCharacterCount = { currentCount: number } +export function isCachedValid(timestamp: number): boolean { + const currentTime = Date.now() + const cacheAge = currentTime - timestamp + const CACHE_TTL = 30 * 60 * 1000 // 30 minutes in milliseconds + + return cacheAge < CACHE_TTL +} + /** * Converts Message to codewhisperer-streaming ChatMessage */ diff --git a/server/aws-lsp-codewhisperer/src/language-server/agenticChat/utils/agenticChatControllerHelper.ts b/server/aws-lsp-codewhisperer/src/language-server/agenticChat/utils/agenticChatControllerHelper.ts deleted file mode 100644 index e29c58fff4..0000000000 --- a/server/aws-lsp-codewhisperer/src/language-server/agenticChat/utils/agenticChatControllerHelper.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { ListAvailableModelsResult } from '@aws/language-server-runtimes/protocol' -import { MODEL_OPTIONS, MODEL_OPTIONS_FOR_REGION } from '../constants/modelSelection' - -/** - * Gets the latest available model for a region, optionally excluding a specific model - * @param region The AWS region - * @param exclude Optional model ID to exclude - * @returns The latest available model - */ -export function getLatestAvailableModel( - region: string | undefined, - exclude?: string -): ListAvailableModelsResult['models'][0] { - const models = region && MODEL_OPTIONS_FOR_REGION[region] ? MODEL_OPTIONS_FOR_REGION[region] : MODEL_OPTIONS - return [...models].reverse().find(model => model.id !== exclude) ?? models[models.length - 1] -} diff --git a/server/aws-lsp-codewhisperer/src/shared/amazonQServiceManager/AmazonQTokenServiceManager.ts b/server/aws-lsp-codewhisperer/src/shared/amazonQServiceManager/AmazonQTokenServiceManager.ts index 5a919c9192..df144e72d6 100644 --- a/server/aws-lsp-codewhisperer/src/shared/amazonQServiceManager/AmazonQTokenServiceManager.ts +++ b/server/aws-lsp-codewhisperer/src/shared/amazonQServiceManager/AmazonQTokenServiceManager.ts @@ -33,6 +33,7 @@ import { getAmazonQRegionAndEndpoint } from './configurationUtils' import { getUserAgent } from '../telemetryUtils' import { StreamingClientServiceToken } from '../streamingClientService' import { parse } from '@aws-sdk/util-arn-parser' +import { ChatDatabase } from '../../language-server/agenticChat/tools/chatDb/chatDb' import { ProfileStatusMonitor } from '../../language-server/agenticChat/tools/mcp/profileStatusMonitor' /** @@ -148,6 +149,10 @@ export class AmazonQTokenServiceManager extends BaseAmazonQServiceManager< if (type === 'iam') { return } + + // Clear model cache when credentials are deleted + ChatDatabase.clearModelCache() + this.cancelActiveProfileChangeToken() this.resetCodewhispererService() diff --git a/server/aws-lsp-codewhisperer/src/shared/codeWhispererService.ts b/server/aws-lsp-codewhisperer/src/shared/codeWhispererService.ts index 030f7f7ee4..2304d81eea 100644 --- a/server/aws-lsp-codewhisperer/src/shared/codeWhispererService.ts +++ b/server/aws-lsp-codewhisperer/src/shared/codeWhispererService.ts @@ -615,6 +615,13 @@ export class CodeWhispererServiceToken extends CodeWhispererServiceBase { return this.client.listAvailableProfiles(request).promise() } + /** + * @description Get list of available models + */ + async listAvailableModels(request: CodeWhispererTokenClient.ListAvailableModelsRequest) { + return this.client.listAvailableModels(request).promise() + } + /** * @description send telemetry event to code whisperer data warehouse */ diff --git a/server/aws-lsp-codewhisperer/src/shared/constants.ts b/server/aws-lsp-codewhisperer/src/shared/constants.ts index cd453a11ba..33f61a079f 100644 --- a/server/aws-lsp-codewhisperer/src/shared/constants.ts +++ b/server/aws-lsp-codewhisperer/src/shared/constants.ts @@ -15,6 +15,8 @@ export const AWS_Q_ENDPOINTS = new Map([ export const AWS_Q_REGION_ENV_VAR = 'AWS_Q_REGION' export const AWS_Q_ENDPOINT_URL_ENV_VAR = 'AWS_Q_ENDPOINT_URL' +export const IDE = 'IDE' + export const Q_CONFIGURATION_SECTION = 'aws.q' export const CODE_WHISPERER_CONFIGURATION_SECTION = 'aws.codeWhisperer' From 23f5ec343cb4e0de32926204dbcf99e51af829f9 Mon Sep 17 00:00:00 2001 From: invictus <149003065+ashishrp-aws@users.noreply.github.com> Date: Wed, 27 Aug 2025 10:16:12 -0700 Subject: [PATCH 49/74] fix(amazonq): fix to add mcp server tool error handling and status for card (#2176) Co-authored-by: Laxman Reddy <141967714+laileni-aws@users.noreply.github.com> --- .../agenticChat/agenticChatController.ts | 51 +++++++++++++++++++ 1 file changed, 51 insertions(+) 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 8cdba3d291..2b41bb938b 100644 --- a/server/aws-lsp-codewhisperer/src/language-server/agenticChat/agenticChatController.ts +++ b/server/aws-lsp-codewhisperer/src/language-server/agenticChat/agenticChatController.ts @@ -2192,6 +2192,50 @@ export class AgenticChatController implements ChatHandlers { ) } + // Handle MCP tool failures + const originalNames = McpManager.instance.getOriginalToolNames(toolUse.name) + if (originalNames && toolUse.toolUseId) { + const { toolName } = originalNames + const cachedToolUse = session.toolUseLookup.get(toolUse.toolUseId) + const cachedButtonBlockId = (cachedToolUse as any)?.cachedButtonBlockId + const customerFacingError = getCustomerFacingErrorMessage(err) + + const errorResult = { + type: 'tool', + messageId: toolUse.toolUseId, + summary: { + content: { + header: { + icon: 'tools', + body: `${toolName}`, + status: { + status: 'error', + icon: 'cancel-circle', + text: 'Error', + description: customerFacingError, + }, + }, + }, + collapsedContent: [ + { + header: { body: 'Parameters' }, + body: `\`\`\`json\n${JSON.stringify(toolUse.input, null, 2)}\n\`\`\``, + }, + { + header: { body: 'Error' }, + body: customerFacingError, + }, + ], + }, + } as ChatResult + + if (cachedButtonBlockId !== undefined) { + await chatResultStream.overwriteResultBlock(errorResult, cachedButtonBlockId) + } else { + await chatResultStream.writeResultBlock(errorResult) + } + } + // display fs write failure status in the UX of that file card if ((toolUse.name === FS_WRITE || toolUse.name === FS_REPLACE) && toolUse.toolUseId) { const existingCard = chatResultStream.getMessageBlockId(toolUse.toolUseId) @@ -4568,6 +4612,13 @@ export class AgenticChatController implements ChatHandlers { await chatResultStream.overwriteResultBlock(toolResultCard, cachedButtonBlockId) } else { // Fallback to creating a new card + if (toolResultCard.summary?.content?.header) { + toolResultCard.summary.content.header.status = { + status: 'success', + icon: 'ok', + text: 'Completed', + } + } this.#log(`Warning: No blockId found for tool use ${toolUse.toolUseId}, creating new card`) await chatResultStream.writeResultBlock(toolResultCard) } From 08720c6c3fa83f9b3b6775d4ae4d848ce145b94b Mon Sep 17 00:00:00 2001 From: Lei Gao <97199248+leigaol@users.noreply.github.com> Date: Wed, 27 Aug 2025 11:18:26 -0700 Subject: [PATCH 50/74] revert: reduce auto trigger frequency for VSC (#2168)" (#2177) This reverts commit 00e11ff48eafaa0baec48177fa4aa6d60048af2f. --- .../inline-completion/auto-trigger/autoTrigger.test.ts | 2 +- .../inline-completion/auto-trigger/autoTrigger.ts | 7 +------ 2 files changed, 2 insertions(+), 7 deletions(-) diff --git a/server/aws-lsp-codewhisperer/src/language-server/inline-completion/auto-trigger/autoTrigger.test.ts b/server/aws-lsp-codewhisperer/src/language-server/inline-completion/auto-trigger/autoTrigger.test.ts index 6f96c526de..31bc0a224a 100644 --- a/server/aws-lsp-codewhisperer/src/language-server/inline-completion/auto-trigger/autoTrigger.test.ts +++ b/server/aws-lsp-codewhisperer/src/language-server/inline-completion/auto-trigger/autoTrigger.test.ts @@ -158,7 +158,7 @@ describe('Auto Trigger', async () => { assert.strictEqual(getAutoTriggerType(createContentChange('line1\nline2')), undefined) }) }) - describe.skip('Right Context should trigger validation', () => { + describe('Right Context should trigger validation', () => { it('should not trigger when there is immediate right context in VSCode', () => { const params = createBasicParams({ fileContext: createBasicFileContext('console.', 'log()'), diff --git a/server/aws-lsp-codewhisperer/src/language-server/inline-completion/auto-trigger/autoTrigger.ts b/server/aws-lsp-codewhisperer/src/language-server/inline-completion/auto-trigger/autoTrigger.ts index 9fb2efab8e..d9dd7f16d5 100644 --- a/server/aws-lsp-codewhisperer/src/language-server/inline-completion/auto-trigger/autoTrigger.ts +++ b/server/aws-lsp-codewhisperer/src/language-server/inline-completion/auto-trigger/autoTrigger.ts @@ -229,12 +229,7 @@ export const autoTrigger = ( const triggerTypeCoefficient = coefficients.triggerTypeCoefficient[triggerType] ?? 0 const osCoefficient = coefficients.osCoefficient[os] ?? 0 - let charCoefficient = coefficients.charCoefficient[char] ?? 0 - // this is a temporary change to lower the auto trigger frequency - if (ide === 'VSCODE') { - charCoefficient = 0 - } - + const charCoefficient = coefficients.charCoefficient[char] ?? 0 const keyWordCoefficient = coefficients.charCoefficient[keyword] ?? 0 const languageCoefficient = coefficients.languageCoefficient[fileContext.programmingLanguage.languageName] ?? 0 From 489334466fa084774d6e4737569468d654dc6359 Mon Sep 17 00:00:00 2001 From: invictus <149003065+ashishrp-aws@users.noreply.github.com> Date: Wed, 27 Aug 2025 11:51:53 -0700 Subject: [PATCH 51/74] fix(amazonq): status message update for mcp tool permission accpetance (#2178) --- .../src/language-server/agenticChat/agenticChatController.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 2b41bb938b..101674a4d9 100644 --- a/server/aws-lsp-codewhisperer/src/language-server/agenticChat/agenticChatController.ts +++ b/server/aws-lsp-codewhisperer/src/language-server/agenticChat/agenticChatController.ts @@ -2607,7 +2607,7 @@ export class AgenticChatController implements ChatHandlers { status: { status: isAccept ? 'success' : 'error', icon: isAccept ? 'ok' : 'cancel', - text: isAccept ? 'Completed' : 'Rejected', + text: isAccept ? 'Accepted' : 'Rejected', }, fileList: undefined, }, From 28567e3ab06a38e13d23df98f0000d62f43293f6 Mon Sep 17 00:00:00 2001 From: Richard Li <742829+rli@users.noreply.github.com> Date: Wed, 27 Aug 2025 14:57:54 -0700 Subject: [PATCH 52/74] deps: update indexing bundle to 042c98e6 (#2174) 042c98e6: optimizing memory usage in Indexing --- .../_bundle-assets/qserver-darwin-arm64.zip | 4 ++-- .../_bundle-assets/qserver-darwin-x64.zip | 4 ++-- .../_bundle-assets/qserver-linux-arm64.zip | 4 ++-- .../_bundle-assets/qserver-linux-x64.zip | 4 ++-- .../_bundle-assets/qserver-win32-x64.zip | 4 ++-- 5 files changed, 10 insertions(+), 10 deletions(-) diff --git a/app/aws-lsp-codewhisperer-runtimes/_bundle-assets/qserver-darwin-arm64.zip b/app/aws-lsp-codewhisperer-runtimes/_bundle-assets/qserver-darwin-arm64.zip index 24fbdb712c..fdd736d602 100644 --- a/app/aws-lsp-codewhisperer-runtimes/_bundle-assets/qserver-darwin-arm64.zip +++ b/app/aws-lsp-codewhisperer-runtimes/_bundle-assets/qserver-darwin-arm64.zip @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:5f651f5364b2417df24d40cbd2d34d69f8b338f3f00aca9c5afd5b4f9ea3d22d -size 96647490 +oid sha256:f4fbd67484ea48363077bbfaa576154196558a4048a862abac84d496fec6b636 +size 96644452 diff --git a/app/aws-lsp-codewhisperer-runtimes/_bundle-assets/qserver-darwin-x64.zip b/app/aws-lsp-codewhisperer-runtimes/_bundle-assets/qserver-darwin-x64.zip index 14d5e51377..4ef652e06a 100644 --- a/app/aws-lsp-codewhisperer-runtimes/_bundle-assets/qserver-darwin-x64.zip +++ b/app/aws-lsp-codewhisperer-runtimes/_bundle-assets/qserver-darwin-x64.zip @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:03ef43ac80e2a16e8c842e92a3f3285d5424f2ea99bba167b9cbb9dabb751262 -size 98328127 +oid sha256:2bcfa7697a0ecdc4ff43f84d1a8c8b7840c144080e51b921dd7c5d3478613a82 +size 98325089 diff --git a/app/aws-lsp-codewhisperer-runtimes/_bundle-assets/qserver-linux-arm64.zip b/app/aws-lsp-codewhisperer-runtimes/_bundle-assets/qserver-linux-arm64.zip index af6aae68c3..dacf7cc25a 100644 --- a/app/aws-lsp-codewhisperer-runtimes/_bundle-assets/qserver-linux-arm64.zip +++ b/app/aws-lsp-codewhisperer-runtimes/_bundle-assets/qserver-linux-arm64.zip @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:33bf50f87b67a330b3dc28f9a2fb1678f5c9cd6eb33beb964b6b068790b05b6c -size 102589811 +oid sha256:1f644daa198d026a091ac87a33fa54b7c1faf1e929ce8ef3ab7e25b7510335e7 +size 102586773 diff --git a/app/aws-lsp-codewhisperer-runtimes/_bundle-assets/qserver-linux-x64.zip b/app/aws-lsp-codewhisperer-runtimes/_bundle-assets/qserver-linux-x64.zip index 4923a60a7f..882e8901d3 100644 --- a/app/aws-lsp-codewhisperer-runtimes/_bundle-assets/qserver-linux-x64.zip +++ b/app/aws-lsp-codewhisperer-runtimes/_bundle-assets/qserver-linux-x64.zip @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:ec71aea47b7cb08c13e94fe4ca284b3656d23266bcdd728a3525abd7939730f0 -size 114552140 +oid sha256:1f6add24d8ae6d1248e3ff8281a8c9e8b402370fd00fcc8bf65c553457715f27 +size 114549102 diff --git a/app/aws-lsp-codewhisperer-runtimes/_bundle-assets/qserver-win32-x64.zip b/app/aws-lsp-codewhisperer-runtimes/_bundle-assets/qserver-win32-x64.zip index fe962b88c0..da87d337a6 100644 --- a/app/aws-lsp-codewhisperer-runtimes/_bundle-assets/qserver-win32-x64.zip +++ b/app/aws-lsp-codewhisperer-runtimes/_bundle-assets/qserver-win32-x64.zip @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:cd8190acad1f1c2b37a818fcf606cc3d2fa4e1929c82ef967ac360b7345864b4 -size 113890248 +oid sha256:5949d0c3d13d02c31f6bf06ea7a0339a851be1824f90c81d48e66c48c79e4930 +size 113887210 From 5a3f481ebe8c6033e3833abcd81799d26c2aa03e Mon Sep 17 00:00:00 2001 From: BlakeLazarine Date: Wed, 27 Aug 2025 15:50:04 -0700 Subject: [PATCH 53/74] feat(amazonq): emit metric for each issue (#2179) Need to emit a metric for each issue found by the agentic reviewer to reach parity with old behavior. --- .../tools/qCodeAnalysis/codeReview.ts | 22 +++++++++++++++++-- .../tools/qCodeAnalysis/codeReviewTypes.ts | 1 + 2 files changed, 21 insertions(+), 2 deletions(-) diff --git a/server/aws-lsp-codewhisperer/src/language-server/agenticChat/tools/qCodeAnalysis/codeReview.ts b/server/aws-lsp-codewhisperer/src/language-server/agenticChat/tools/qCodeAnalysis/codeReview.ts index b1ef4c3e61..d9fd42d621 100644 --- a/server/aws-lsp-codewhisperer/src/language-server/agenticChat/tools/qCodeAnalysis/codeReview.ts +++ b/server/aws-lsp-codewhisperer/src/language-server/agenticChat/tools/qCodeAnalysis/codeReview.ts @@ -439,9 +439,27 @@ export class CodeReview { ) this.logging.info('Findings count grouped by file') - aggregatedCodeScanIssueList.forEach(item => + aggregatedCodeScanIssueList.forEach(item => { this.logging.info(`File path - ${item.filePath} Findings count - ${item.issues.length}`) - ) + item.issues.forEach(issue => + CodeReviewUtils.emitMetric( + { + reason: SuccessMetricName.IssuesDetected, + result: 'Succeeded', + metadata: { + codewhispererCodeScanJobId: jobId, + credentialStartUrl: this.credentialsProvider.getConnectionMetadata()?.sso?.startUrl, + findingId: issue.findingId, + detectorId: issue.detectorId, + ruleId: issue.ruleId, + autoDetected: false, + }, + }, + this.logging, + this.telemetry + ) + ) + }) return { codeReviewId: jobId, diff --git a/server/aws-lsp-codewhisperer/src/language-server/agenticChat/tools/qCodeAnalysis/codeReviewTypes.ts b/server/aws-lsp-codewhisperer/src/language-server/agenticChat/tools/qCodeAnalysis/codeReviewTypes.ts index 15aa32a5aa..728c226ae1 100644 --- a/server/aws-lsp-codewhisperer/src/language-server/agenticChat/tools/qCodeAnalysis/codeReviewTypes.ts +++ b/server/aws-lsp-codewhisperer/src/language-server/agenticChat/tools/qCodeAnalysis/codeReviewTypes.ts @@ -10,6 +10,7 @@ export enum FailedMetricName { } export enum SuccessMetricName { CodeScanSuccess = 'codeScanSuccess', + IssuesDetected = 'issuesDetected', } export type ValidateInputAndSetupResult = { From b5f5373a3363c13275cc63799177f2d6358abfe7 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Wed, 27 Aug 2025 16:20:53 -0700 Subject: [PATCH 54/74] chore(release): release packages from branch main (#2152) Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> --- .release-please-manifest.json | 4 ++-- chat-client/CHANGELOG.md | 12 ++++++++++ chat-client/package.json | 2 +- package-lock.json | 4 ++-- server/aws-lsp-codewhisperer/CHANGELOG.md | 28 +++++++++++++++++++++++ server/aws-lsp-codewhisperer/package.json | 2 +- 6 files changed, 46 insertions(+), 6 deletions(-) diff --git a/.release-please-manifest.json b/.release-please-manifest.json index 49f118f219..e6baf94939 100644 --- a/.release-please-manifest.json +++ b/.release-please-manifest.json @@ -1,8 +1,8 @@ { - "chat-client": "0.1.33", + "chat-client": "0.1.34", "core/aws-lsp-core": "0.0.14", "server/aws-lsp-antlr4": "0.1.18", - "server/aws-lsp-codewhisperer": "0.0.75", + "server/aws-lsp-codewhisperer": "0.0.76", "server/aws-lsp-json": "0.1.18", "server/aws-lsp-partiql": "0.0.17", "server/aws-lsp-yaml": "0.1.18" diff --git a/chat-client/CHANGELOG.md b/chat-client/CHANGELOG.md index 995ddc8ae6..1c516246c6 100644 --- a/chat-client/CHANGELOG.md +++ b/chat-client/CHANGELOG.md @@ -1,5 +1,17 @@ # Changelog +## [0.1.34](https://github.com/aws/language-servers/compare/chat-client/v0.1.33...chat-client/v0.1.34) (2025-08-27) + + +### Features + +* Auto fetch models from listAvailableModels API ([#2171](https://github.com/aws/language-servers/issues/2171)) ([8600c52](https://github.com/aws/language-servers/commit/8600c524877abb459e9338399352446c0dcff6f0)) + + +### Bug Fixes + +* **amazonq:** disable typewriter animation ([#2160](https://github.com/aws/language-servers/issues/2160)) ([db45d01](https://github.com/aws/language-servers/commit/db45d01adba10e8a04d868e1062f899df4f5b7e4)) + ## [0.1.33](https://github.com/aws/language-servers/compare/chat-client/v0.1.32...chat-client/v0.1.33) (2025-08-19) diff --git a/chat-client/package.json b/chat-client/package.json index e4250d42a1..b838360175 100644 --- a/chat-client/package.json +++ b/chat-client/package.json @@ -1,6 +1,6 @@ { "name": "@aws/chat-client", - "version": "0.1.33", + "version": "0.1.34", "description": "AWS Chat Client", "main": "out/index.js", "repository": { diff --git a/package-lock.json b/package-lock.json index 5fc601a1bc..86d2516d9d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -251,7 +251,7 @@ }, "chat-client": { "name": "@aws/chat-client", - "version": "0.1.33", + "version": "0.1.34", "license": "Apache-2.0", "dependencies": { "@aws/chat-client-ui-types": "^0.1.56", @@ -28669,7 +28669,7 @@ }, "server/aws-lsp-codewhisperer": { "name": "@aws/lsp-codewhisperer", - "version": "0.0.75", + "version": "0.0.76", "bundleDependencies": [ "@amzn/codewhisperer-streaming", "@amzn/amazon-q-developer-streaming-client" diff --git a/server/aws-lsp-codewhisperer/CHANGELOG.md b/server/aws-lsp-codewhisperer/CHANGELOG.md index a70284a4c2..8bda2b6803 100644 --- a/server/aws-lsp-codewhisperer/CHANGELOG.md +++ b/server/aws-lsp-codewhisperer/CHANGELOG.md @@ -1,5 +1,33 @@ # Changelog +## [0.0.76](https://github.com/aws/language-servers/compare/lsp-codewhisperer/v0.0.75...lsp-codewhisperer/v0.0.76) (2025-08-27) + + +### Features + +* add basic OAuth client for remote MCP ([#2136](https://github.com/aws/language-servers/issues/2136)) ([2fb896e](https://github.com/aws/language-servers/commit/2fb896e094de0bc5a1b4881067e7dcceb3826015)) +* **amazonq:** emit metric for each issue ([#2179](https://github.com/aws/language-servers/issues/2179)) ([5a3f481](https://github.com/aws/language-servers/commit/5a3f481ebe8c6033e3833abcd81799d26c2aa03e)) +* Auto fetch models from listAvailableModels API ([#2171](https://github.com/aws/language-servers/issues/2171)) ([8600c52](https://github.com/aws/language-servers/commit/8600c524877abb459e9338399352446c0dcff6f0)) +* disable pkce flow during plugin load ([#2153](https://github.com/aws/language-servers/issues/2153)) ([71b3595](https://github.com/aws/language-servers/commit/71b35952333e7581921644ce40fabbc1e6d3c02f)) +* update MCP manager and utilities ([#2158](https://github.com/aws/language-servers/issues/2158)) ([b99df82](https://github.com/aws/language-servers/commit/b99df82826d0ba1a1d52df578cb80674c90505b9)) + + +### Bug Fixes + +* adding streakTracker to track streakLength across Completions and Edits ([#2147](https://github.com/aws/language-servers/issues/2147)) ([a6c64f2](https://github.com/aws/language-servers/commit/a6c64f2995a17697e3d71d30a1f411f5cf0db279)) +* **amazonq:** dedupe openTabs supplemental contexts ([#2172](https://github.com/aws/language-servers/issues/2172)) ([aa87ae2](https://github.com/aws/language-servers/commit/aa87ae2bd95edc1f38bf90f56093c5bf5ff18c53)) +* **amazonq:** fix for mcp servers operations to edit server config only ([#2165](https://github.com/aws/language-servers/issues/2165)) ([d28df09](https://github.com/aws/language-servers/commit/d28df09ae41871430cd53064eac1f3050c95ea84)) +* **amazonq:** fix to add mcp server tool error handling and status for card ([#2176](https://github.com/aws/language-servers/issues/2176)) ([23f5ec3](https://github.com/aws/language-servers/commit/23f5ec343cb4e0de32926204dbcf99e51af829f9)) +* **amazonq:** status message update for mcp tool permission accpetance ([#2178](https://github.com/aws/language-servers/issues/2178)) ([4893344](https://github.com/aws/language-servers/commit/489334466fa084774d6e4737569468d654dc6359)) +* fix pkce windows url path ([#2173](https://github.com/aws/language-servers/issues/2173)) ([d7b184c](https://github.com/aws/language-servers/commit/d7b184cb12979877722fa0293e9aebec91ff2c18)) +* multiple fixes on auth flow edge cases ([#2155](https://github.com/aws/language-servers/issues/2155)) ([472220a](https://github.com/aws/language-servers/commit/472220a745cff4fe91a2cabae4ae059a164ceddd)) +* reduce auto trigger frequency for VSC ([#2168](https://github.com/aws/language-servers/issues/2168)) ([00e11ff](https://github.com/aws/language-servers/commit/00e11ff48eafaa0baec48177fa4aa6d60048af2f)) + + +### Reverts + +* reduce auto trigger frequency for VSC ([#2168](https://github.com/aws/language-servers/issues/2168))" ([#2177](https://github.com/aws/language-servers/issues/2177)) ([08720c6](https://github.com/aws/language-servers/commit/08720c6c3fa83f9b3b6775d4ae4d848ce145b94b)) + ## [0.0.75](https://github.com/aws/language-servers/compare/lsp-codewhisperer/v0.0.74...lsp-codewhisperer/v0.0.75) (2025-08-21) diff --git a/server/aws-lsp-codewhisperer/package.json b/server/aws-lsp-codewhisperer/package.json index 83ff1cdaee..8702e6fab3 100644 --- a/server/aws-lsp-codewhisperer/package.json +++ b/server/aws-lsp-codewhisperer/package.json @@ -1,6 +1,6 @@ { "name": "@aws/lsp-codewhisperer", - "version": "0.0.75", + "version": "0.0.76", "description": "CodeWhisperer Language Server", "main": "out/index.js", "repository": { From 8d5b839285bacc21cf9834867f9c6e5b4d9ad471 Mon Sep 17 00:00:00 2001 From: aws-toolkit-automation <> Date: Thu, 28 Aug 2025 04:04:29 +0000 Subject: [PATCH 55/74] chore: bump agentic version: 1.30.0 --- app/aws-lsp-codewhisperer-runtimes/src/version.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/aws-lsp-codewhisperer-runtimes/src/version.json b/app/aws-lsp-codewhisperer-runtimes/src/version.json index 93401eb637..e842765e3f 100644 --- a/app/aws-lsp-codewhisperer-runtimes/src/version.json +++ b/app/aws-lsp-codewhisperer-runtimes/src/version.json @@ -1,3 +1,3 @@ { - "agenticChat": "1.29.0" + "agenticChat": "1.30.0" } From ecb86a02e066418a3984f8f02d7c9ea71ecf5f5e Mon Sep 17 00:00:00 2001 From: Richard Li Date: Wed, 27 Aug 2025 21:08:22 -0700 Subject: [PATCH 56/74] chore: empty commit to trigger workflow From baf20b7c4050dd4360ecb97c06d21f6b504836b0 Mon Sep 17 00:00:00 2001 From: Richard Li <742829+rli@users.noreply.github.com> Date: Thu, 28 Aug 2025 10:56:30 -0700 Subject: [PATCH 57/74] revert: deps: update indexing bundle to 042c98e6 (#2174) (#2183) This reverts commit 28567e3ab06a38e13d23df98f0000d62f43293f6. indexing bundle contains a crypto polyfill that is needed for some reason --- .../_bundle-assets/qserver-darwin-arm64.zip | 4 ++-- .../_bundle-assets/qserver-darwin-x64.zip | 4 ++-- .../_bundle-assets/qserver-linux-arm64.zip | 4 ++-- .../_bundle-assets/qserver-linux-x64.zip | 4 ++-- .../_bundle-assets/qserver-win32-x64.zip | 4 ++-- 5 files changed, 10 insertions(+), 10 deletions(-) diff --git a/app/aws-lsp-codewhisperer-runtimes/_bundle-assets/qserver-darwin-arm64.zip b/app/aws-lsp-codewhisperer-runtimes/_bundle-assets/qserver-darwin-arm64.zip index fdd736d602..24fbdb712c 100644 --- a/app/aws-lsp-codewhisperer-runtimes/_bundle-assets/qserver-darwin-arm64.zip +++ b/app/aws-lsp-codewhisperer-runtimes/_bundle-assets/qserver-darwin-arm64.zip @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:f4fbd67484ea48363077bbfaa576154196558a4048a862abac84d496fec6b636 -size 96644452 +oid sha256:5f651f5364b2417df24d40cbd2d34d69f8b338f3f00aca9c5afd5b4f9ea3d22d +size 96647490 diff --git a/app/aws-lsp-codewhisperer-runtimes/_bundle-assets/qserver-darwin-x64.zip b/app/aws-lsp-codewhisperer-runtimes/_bundle-assets/qserver-darwin-x64.zip index 4ef652e06a..14d5e51377 100644 --- a/app/aws-lsp-codewhisperer-runtimes/_bundle-assets/qserver-darwin-x64.zip +++ b/app/aws-lsp-codewhisperer-runtimes/_bundle-assets/qserver-darwin-x64.zip @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:2bcfa7697a0ecdc4ff43f84d1a8c8b7840c144080e51b921dd7c5d3478613a82 -size 98325089 +oid sha256:03ef43ac80e2a16e8c842e92a3f3285d5424f2ea99bba167b9cbb9dabb751262 +size 98328127 diff --git a/app/aws-lsp-codewhisperer-runtimes/_bundle-assets/qserver-linux-arm64.zip b/app/aws-lsp-codewhisperer-runtimes/_bundle-assets/qserver-linux-arm64.zip index dacf7cc25a..af6aae68c3 100644 --- a/app/aws-lsp-codewhisperer-runtimes/_bundle-assets/qserver-linux-arm64.zip +++ b/app/aws-lsp-codewhisperer-runtimes/_bundle-assets/qserver-linux-arm64.zip @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:1f644daa198d026a091ac87a33fa54b7c1faf1e929ce8ef3ab7e25b7510335e7 -size 102586773 +oid sha256:33bf50f87b67a330b3dc28f9a2fb1678f5c9cd6eb33beb964b6b068790b05b6c +size 102589811 diff --git a/app/aws-lsp-codewhisperer-runtimes/_bundle-assets/qserver-linux-x64.zip b/app/aws-lsp-codewhisperer-runtimes/_bundle-assets/qserver-linux-x64.zip index 882e8901d3..4923a60a7f 100644 --- a/app/aws-lsp-codewhisperer-runtimes/_bundle-assets/qserver-linux-x64.zip +++ b/app/aws-lsp-codewhisperer-runtimes/_bundle-assets/qserver-linux-x64.zip @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:1f6add24d8ae6d1248e3ff8281a8c9e8b402370fd00fcc8bf65c553457715f27 -size 114549102 +oid sha256:ec71aea47b7cb08c13e94fe4ca284b3656d23266bcdd728a3525abd7939730f0 +size 114552140 diff --git a/app/aws-lsp-codewhisperer-runtimes/_bundle-assets/qserver-win32-x64.zip b/app/aws-lsp-codewhisperer-runtimes/_bundle-assets/qserver-win32-x64.zip index da87d337a6..fe962b88c0 100644 --- a/app/aws-lsp-codewhisperer-runtimes/_bundle-assets/qserver-win32-x64.zip +++ b/app/aws-lsp-codewhisperer-runtimes/_bundle-assets/qserver-win32-x64.zip @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:5949d0c3d13d02c31f6bf06ea7a0339a851be1824f90c81d48e66c48c79e4930 -size 113887210 +oid sha256:cd8190acad1f1c2b37a818fcf606cc3d2fa4e1929c82ef967ac360b7345864b4 +size 113890248 From c53f672b6173ebda530917ccb4e0c2f26f5c8f79 Mon Sep 17 00:00:00 2001 From: tsmithsz <84354541+tsmithsz@users.noreply.github.com> Date: Fri, 29 Aug 2025 10:22:49 -0700 Subject: [PATCH 58/74] fix: emit acceptedLineCount metric and AgenticCodeAccepted interaction type (#2167) --- .../agenticChat/agenticChatController.ts | 13 +++++++++++++ .../chat/telemetry/chatTelemetryController.ts | 4 +++- .../src/shared/telemetry/telemetryService.ts | 1 + .../src/shared/telemetry/types.ts | 1 + 4 files changed, 18 insertions(+), 1 deletion(-) 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 101674a4d9..4a816317d2 100644 --- a/server/aws-lsp-codewhisperer/src/language-server/agenticChat/agenticChatController.ts +++ b/server/aws-lsp-codewhisperer/src/language-server/agenticChat/agenticChatController.ts @@ -2076,6 +2076,19 @@ export class AgenticChatController implements ChatHandlers { this.#abTestingAllocation?.experimentName, this.#abTestingAllocation?.userVariation ) + // Emit acceptedLineCount when write tool is used and code changes are accepted + const beforeLines = cachedToolUse?.fileChange?.before?.split('\n').length ?? 0 + const afterLines = doc?.getText()?.split('\n').length ?? 0 + const acceptedLineCount = afterLines - beforeLines + await this.#telemetryController.emitInteractWithMessageMetric( + tabId, + { + cwsprChatMessageId: chatResult.messageId ?? toolUse.toolUseId, + cwsprChatInteractionType: ChatInteractionType.AgenticCodeAccepted, + codewhispererCustomizationArn: this.#customizationArn, + }, + acceptedLineCount + ) await chatResultStream.writeResultBlock(chatResult) break case CodeReview.toolName: diff --git a/server/aws-lsp-codewhisperer/src/language-server/chat/telemetry/chatTelemetryController.ts b/server/aws-lsp-codewhisperer/src/language-server/chat/telemetry/chatTelemetryController.ts index 8e9cb92624..580bcd4be2 100644 --- a/server/aws-lsp-codewhisperer/src/language-server/chat/telemetry/chatTelemetryController.ts +++ b/server/aws-lsp-codewhisperer/src/language-server/chat/telemetry/chatTelemetryController.ts @@ -421,10 +421,12 @@ export class ChatTelemetryController { public emitInteractWithMessageMetric( tabId: string, - metric: Omit + metric: Omit, + acceptedLineCount?: number ) { return this.#telemetryService.emitChatInteractWithMessage(metric, { conversationId: this.getConversationId(tabId), + acceptedLineCount, }) } diff --git a/server/aws-lsp-codewhisperer/src/shared/telemetry/telemetryService.ts b/server/aws-lsp-codewhisperer/src/shared/telemetry/telemetryService.ts index 6f3ba52028..e5e8cbc471 100644 --- a/server/aws-lsp-codewhisperer/src/shared/telemetry/telemetryService.ts +++ b/server/aws-lsp-codewhisperer/src/shared/telemetry/telemetryService.ts @@ -66,6 +66,7 @@ export class TelemetryService { [ChatInteractionType.Upvote]: 'UPVOTE', [ChatInteractionType.Downvote]: 'DOWNVOTE', [ChatInteractionType.ClickBodyLink]: 'CLICK_BODY_LINK', + [ChatInteractionType.AgenticCodeAccepted]: 'AGENTIC_CODE_ACCEPTED', } constructor( diff --git a/server/aws-lsp-codewhisperer/src/shared/telemetry/types.ts b/server/aws-lsp-codewhisperer/src/shared/telemetry/types.ts index bed82c2939..5bf9fea185 100644 --- a/server/aws-lsp-codewhisperer/src/shared/telemetry/types.ts +++ b/server/aws-lsp-codewhisperer/src/shared/telemetry/types.ts @@ -429,6 +429,7 @@ export enum ChatInteractionType { Upvote = 'upvote', Downvote = 'downvote', ClickBodyLink = 'clickBodyLink', + AgenticCodeAccepted = 'agenticCodeAccepted', } export enum ChatHistoryActionType { From 852b21b66f793102c52e35c2baec07a772e5134a Mon Sep 17 00:00:00 2001 From: Will Lo <96078566+Will-ShaoHua@users.noreply.github.com> Date: Fri, 29 Aug 2025 13:28:28 -0700 Subject: [PATCH 59/74] fix: auto trigger should only respect previous decisions in the past 2mins (#2189) --- .../auto-trigger/autoTrigger.ts | 21 ++++++++++++------- .../inline-completion/codeWhispererServer.ts | 8 +++++-- .../session/sessionManager.ts | 7 +++++++ .../src/shared/telemetry/telemetryService.ts | 1 + 4 files changed, 28 insertions(+), 9 deletions(-) diff --git a/server/aws-lsp-codewhisperer/src/language-server/inline-completion/auto-trigger/autoTrigger.ts b/server/aws-lsp-codewhisperer/src/language-server/inline-completion/auto-trigger/autoTrigger.ts index d9dd7f16d5..adbbeb4662 100644 --- a/server/aws-lsp-codewhisperer/src/language-server/inline-completion/auto-trigger/autoTrigger.ts +++ b/server/aws-lsp-codewhisperer/src/language-server/inline-completion/auto-trigger/autoTrigger.ts @@ -177,7 +177,7 @@ type AutoTriggerParams = { char: string triggerType: string // Left as String intentionally to support future and unknown trigger types os: string - previousDecision: string + previousDecision: string | undefined ide: string lineNum: number } @@ -235,12 +235,19 @@ export const autoTrigger = ( const languageCoefficient = coefficients.languageCoefficient[fileContext.programmingLanguage.languageName] ?? 0 let previousDecisionCoefficient = 0 - if (previousDecision === 'Accept') { - previousDecisionCoefficient = coefficients.prevDecisionAcceptCoefficient - } else if (previousDecision === 'Reject') { - previousDecisionCoefficient = coefficients.prevDecisionRejectCoefficient - } else if (previousDecision === 'Discard' || previousDecision === 'Empty') { - previousDecisionCoefficient = coefficients.prevDecisionOtherCoefficient + switch (previousDecision) { + case 'Accept': + previousDecisionCoefficient = coefficients.prevDecisionAcceptCoefficient + break + case 'Reject': + previousDecisionCoefficient = coefficients.prevDecisionRejectCoefficient + break + case 'Discard': + case 'Empty': + previousDecisionCoefficient = coefficients.prevDecisionOtherCoefficient + break + default: + break } const ideCoefficient = coefficients.ideCoefficient[ide] ?? 0 diff --git a/server/aws-lsp-codewhisperer/src/language-server/inline-completion/codeWhispererServer.ts b/server/aws-lsp-codewhisperer/src/language-server/inline-completion/codeWhispererServer.ts index 330cf54735..c869a534fd 100644 --- a/server/aws-lsp-codewhisperer/src/language-server/inline-completion/codeWhispererServer.ts +++ b/server/aws-lsp-codewhisperer/src/language-server/inline-completion/codeWhispererServer.ts @@ -239,7 +239,11 @@ export const CodewhispererServerFactory = : undefined const previousSession = completionSessionManager.getPreviousSession() - const previousDecision = previousSession?.getAggregatedUserTriggerDecision() ?? '' + // Only refer to decisions in the past 2 mins + const previousDecisionForClassifier = + previousSession && performance.now() - previousSession.decisionMadeTimestamp <= 2 * 60 * 1000 + ? previousSession.getAggregatedUserTriggerDecision() + : undefined let ideCategory: string | undefined = '' const initializeParams = lsp.getClientInitializeParams() if (initializeParams !== undefined) { @@ -280,7 +284,7 @@ export const CodewhispererServerFactory = char: triggerCharacters, // Add the character just inserted, if any, before the invication position ide: ideCategory ?? '', os: getNormalizeOsName(), - previousDecision, // The last decision by the user on the previous invocation + previousDecision: previousDecisionForClassifier, // The last decision by the user on the previous invocation triggerType: codewhispererAutoTriggerType, // The 2 trigger types currently influencing the Auto-Trigger are SpecialCharacter and Enter }, logging diff --git a/server/aws-lsp-codewhisperer/src/language-server/inline-completion/session/sessionManager.ts b/server/aws-lsp-codewhisperer/src/language-server/inline-completion/session/sessionManager.ts index 34dbb12538..2a4af79ff6 100644 --- a/server/aws-lsp-codewhisperer/src/language-server/inline-completion/session/sessionManager.ts +++ b/server/aws-lsp-codewhisperer/src/language-server/inline-completion/session/sessionManager.ts @@ -60,6 +60,13 @@ export class CodeWhispererSession { suggestions: CachedSuggestion[] = [] suggestionsAfterRightContextMerge: InlineCompletionItemWithReferences[] = [] suggestionsStates = new Map() + private _decisionTimestamp = 0 + get decisionMadeTimestamp() { + return this._decisionTimestamp + } + set decisionMadeTimestamp(time: number) { + this._decisionTimestamp = time + } acceptedSuggestionId?: string = undefined responseContext?: ResponseContext triggerType: CodewhispererTriggerType diff --git a/server/aws-lsp-codewhisperer/src/shared/telemetry/telemetryService.ts b/server/aws-lsp-codewhisperer/src/shared/telemetry/telemetryService.ts index e5e8cbc471..b11806a020 100644 --- a/server/aws-lsp-codewhisperer/src/shared/telemetry/telemetryService.ts +++ b/server/aws-lsp-codewhisperer/src/shared/telemetry/telemetryService.ts @@ -200,6 +200,7 @@ export class TelemetryService { removedIdeDiagnostics?: IdeDiagnostic[], streakLength?: number ) { + session.decisionMadeTimestamp = performance.now() if (this.enableTelemetryEventsToDestination) { const data: CodeWhispererUserTriggerDecisionEvent = { codewhispererSessionId: session.codewhispererSessionId || '', From 4fd0def2a7080921683dd712cae9c17a39d4372e Mon Sep 17 00:00:00 2001 From: Will Lo <96078566+Will-ShaoHua@users.noreply.github.com> Date: Fri, 29 Aug 2025 13:28:54 -0700 Subject: [PATCH 60/74] chore: supplemental context log (#2185) --- .../src/shared/codeWhispererService.ts | 2 ++ .../crossFileContextUtil.ts | 10 ++++------ .../supplementalContextUtil.ts | 19 ++++++++++++++++++- 3 files changed, 24 insertions(+), 7 deletions(-) diff --git a/server/aws-lsp-codewhisperer/src/shared/codeWhispererService.ts b/server/aws-lsp-codewhisperer/src/shared/codeWhispererService.ts index 2304d81eea..984bb6da76 100644 --- a/server/aws-lsp-codewhisperer/src/shared/codeWhispererService.ts +++ b/server/aws-lsp-codewhisperer/src/shared/codeWhispererService.ts @@ -444,6 +444,8 @@ export class CodeWhispererServiceToken extends CodeWhispererServiceBase { "endpoint": ${this.codeWhispererEndpoint}, "predictionType": ${request.predictionTypes?.toString() ?? 'Not specified (COMPLETIONS)'}, "filename": ${request.fileContext.filename}, + "leftContextLength": ${request.fileContext.leftFileContent.length}, + rightContextLength: ${request.fileContext.rightFileContent.length}, "language": ${request.fileContext.programmingLanguage.languageName}, "supplementalContextCount": ${request.supplementalContexts?.length ?? 0}, "request.nextToken": ${request.nextToken}, diff --git a/server/aws-lsp-codewhisperer/src/shared/supplementalContextUtil/crossFileContextUtil.ts b/server/aws-lsp-codewhisperer/src/shared/supplementalContextUtil/crossFileContextUtil.ts index 06bac73d54..91f02b9dc7 100644 --- a/server/aws-lsp-codewhisperer/src/shared/supplementalContextUtil/crossFileContextUtil.ts +++ b/server/aws-lsp-codewhisperer/src/shared/supplementalContextUtil/crossFileContextUtil.ts @@ -71,12 +71,13 @@ export async function fetchSupplementalContextForSrc( const supplementalContextConfig = getSupplementalContextConfig(document.languageId) if (supplementalContextConfig === undefined) { - return supplementalContextConfig + return undefined } - //TODO: add logic for other strategies once available + if (supplementalContextConfig === 'codemap') { return await codemapContext(document, position, workspace, cancellationToken, openTabFiles) } + return { supplementalContextItems: [], strategy: 'Empty' } } @@ -264,10 +265,7 @@ function getInputChunk(document: TextDocument, cursorPosition: Position, chunkSi * @returns specifically returning undefined if the langueage is not supported, * otherwise true/false depending on if the language is fully supported or not belonging to the user group */ -function getSupplementalContextConfig( - languageId: TextDocument['languageId'], - _userGroup?: any -): SupplementalContextStrategy | undefined { +function getSupplementalContextConfig(languageId: TextDocument['languageId']): SupplementalContextStrategy | undefined { return isCrossFileSupported(languageId) ? 'codemap' : undefined } diff --git a/server/aws-lsp-codewhisperer/src/shared/supplementalContextUtil/supplementalContextUtil.ts b/server/aws-lsp-codewhisperer/src/shared/supplementalContextUtil/supplementalContextUtil.ts index 8a5658188b..8e0902452f 100644 --- a/server/aws-lsp-codewhisperer/src/shared/supplementalContextUtil/supplementalContextUtil.ts +++ b/server/aws-lsp-codewhisperer/src/shared/supplementalContextUtil/supplementalContextUtil.ts @@ -91,7 +91,24 @@ export async function fetchSupplementalContext( strategy: supplementalContextValue.strategy, } - return truncateSupplementalContext(resBeforeTruncation) + const r = truncateSupplementalContext(resBeforeTruncation) + + let logstr = `@@supplemental context@@ +\tisUtg: ${r.isUtg}, +\tisProcessTimeout: ${r.isProcessTimeout}, +\tcontents.length: ${r.contentsLength}, +\tlatency: ${r.latency}, +\tstrategy: ${r.strategy}, +` + r.supplementalContextItems.forEach((item, index) => { + logstr += `\tChunk [${index}th]:\n` + logstr += `\t\tPath: ${item.filePath}\n` + logstr += `\t\tLength: ${item.content.length}\n` + logstr += `\t\tScore: ${item.score}\n` + }) + logging.info(logstr) + + return r } else { return undefined } From f4e2e6e885e665834a5d7b7cbb5f4ba4ff9bbb65 Mon Sep 17 00:00:00 2001 From: Will Lo <96078566+Will-ShaoHua@users.noreply.github.com> Date: Fri, 29 Aug 2025 13:29:11 -0700 Subject: [PATCH 61/74] fix: should send classifier score after taking sigmoid (#2188) --- .../inline-completion/auto-trigger/autoTrigger.ts | 8 ++++++-- .../inline-completion/userTriggerDecision.test.ts | 4 ++-- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/server/aws-lsp-codewhisperer/src/language-server/inline-completion/auto-trigger/autoTrigger.ts b/server/aws-lsp-codewhisperer/src/language-server/inline-completion/auto-trigger/autoTrigger.ts index adbbeb4662..8990756dbf 100644 --- a/server/aws-lsp-codewhisperer/src/language-server/inline-completion/auto-trigger/autoTrigger.ts +++ b/server/aws-lsp-codewhisperer/src/language-server/inline-completion/auto-trigger/autoTrigger.ts @@ -229,7 +229,9 @@ export const autoTrigger = ( const triggerTypeCoefficient = coefficients.triggerTypeCoefficient[triggerType] ?? 0 const osCoefficient = coefficients.osCoefficient[os] ?? 0 + const charCoefficient = coefficients.charCoefficient[char] ?? 0 + const keyWordCoefficient = coefficients.charCoefficient[keyword] ?? 0 const languageCoefficient = coefficients.languageCoefficient[fileContext.programmingLanguage.languageName] ?? 0 @@ -281,11 +283,13 @@ export const autoTrigger = ( previousDecisionCoefficient + languageCoefficient + leftContextLengthCoefficient - const shouldTrigger = sigmoid(classifierResult) > TRIGGER_THRESHOLD + + const r = sigmoid(classifierResult) + const shouldTrigger = r > TRIGGER_THRESHOLD return { shouldTrigger, - classifierResult, + classifierResult: r, classifierThreshold: TRIGGER_THRESHOLD, } } diff --git a/server/aws-lsp-codewhisperer/src/language-server/inline-completion/userTriggerDecision.test.ts b/server/aws-lsp-codewhisperer/src/language-server/inline-completion/userTriggerDecision.test.ts index c583815b6b..21c398d56a 100644 --- a/server/aws-lsp-codewhisperer/src/language-server/inline-completion/userTriggerDecision.test.ts +++ b/server/aws-lsp-codewhisperer/src/language-server/inline-completion/userTriggerDecision.test.ts @@ -145,7 +145,7 @@ describe('Telemetry', () => { }, } const EMPTY_RESULT = { items: [], sessionId: '' } - const classifierResult = getNormalizeOsName() !== 'Linux' ? 0.4114381148145918 : 0.46733811481459187 + const classifierResult = getNormalizeOsName() !== 'Linux' ? 0.6014326616203989 : 0.61475353067264 let features: TestFeatures let server: Server @@ -1251,7 +1251,7 @@ describe('Telemetry', () => { triggerType: 'AutoTrigger', autoTriggerType: 'SpecialCharacters', triggerCharacter: '(', - classifierResult: getNormalizeOsName() === 'Linux' ? 0.30173811481459184 : 0.2458381148145919, + classifierResult: getNormalizeOsName() === 'Linux' ? 0.5748673583477094 : 0.5611518554232429, classifierThreshold: 0.43, language: 'csharp', requestContext: { From b4975409a3ed518550290b72ac310895a293be4b Mon Sep 17 00:00:00 2001 From: Will Lo <96078566+Will-ShaoHua@users.noreply.github.com> Date: Fri, 29 Aug 2025 13:29:44 -0700 Subject: [PATCH 62/74] perf: only process edit requests 1 at a time (#2187) --- .../inline-completion/editCompletionHandler.ts | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/server/aws-lsp-codewhisperer/src/language-server/inline-completion/editCompletionHandler.ts b/server/aws-lsp-codewhisperer/src/language-server/inline-completion/editCompletionHandler.ts index 193521cce9..550de06708 100644 --- a/server/aws-lsp-codewhisperer/src/language-server/inline-completion/editCompletionHandler.ts +++ b/server/aws-lsp-codewhisperer/src/language-server/inline-completion/editCompletionHandler.ts @@ -45,6 +45,8 @@ export class EditCompletionHandler { private hasDocumentChangedSinceInvocation: boolean = false private readonly streakTracker: StreakTracker + private isInProgress = false + constructor( readonly logging: Logging, readonly clientMetadata: InitializeParams, @@ -89,6 +91,11 @@ export class EditCompletionHandler { params: InlineCompletionWithReferencesParams, token: CancellationToken ): Promise { + if (this.isInProgress) { + this.logging.info(`editCompletionHandler is WIP, skip the request`) + return EMPTY_RESULT + } + // On every new completion request close current inflight session. const currentSession = this.sessionManager.getCurrentSession() if (currentSession && currentSession.state == 'REQUESTING' && !params.partialResultToken) { @@ -118,6 +125,9 @@ export class EditCompletionHandler { return EMPTY_RESULT } + // Not ideally to rely on a state, should improve it and simply make it a debounced API + this.isInProgress = true + if (params.partialResultToken && currentSession) { // Close ACTIVE session. We shouldn't record Discard trigger decision for trigger with nextToken. if (currentSession && currentSession.state === 'ACTIVE') { @@ -152,10 +162,12 @@ export class EditCompletionHandler { ) } catch (error) { return this.handleSuggestionsErrors(error as Error, currentSession) + } finally { + this.isInProgress = false } } - return new Promise(async resolve => { + return new Promise(async resolve => { this.debounceTimeout = setTimeout(async () => { try { this.isWaiting = true @@ -185,6 +197,8 @@ export class EditCompletionHandler { this.hasDocumentChangedSinceInvocation = false } }, EDIT_DEBOUNCE_INTERVAL_MS) + }).finally(() => { + this.isInProgress = false }) } From 66742adfc44f33efbd8dd33b803000e08241e5ce Mon Sep 17 00:00:00 2001 From: atontb <104926752+atonaamz@users.noreply.github.com> Date: Fri, 29 Aug 2025 14:12:15 -0700 Subject: [PATCH 63/74] feat: passing suggestionTypes and pluginVersion/lspVersion to STE (#2180) --- .../src/client/token/bearer-token-service.json | 13 +++++++++++++ .../token/codewhispererbearertokenclient.d.ts | 6 +++++- .../src/shared/telemetry/telemetryService.test.ts | 1 + .../src/shared/telemetry/telemetryService.ts | 5 ++++- .../src/shared/telemetryUtils.ts | 2 ++ 5 files changed, 25 insertions(+), 2 deletions(-) diff --git a/server/aws-lsp-codewhisperer/src/client/token/bearer-token-service.json b/server/aws-lsp-codewhisperer/src/client/token/bearer-token-service.json index 7e0533b0f5..58ae54fb3c 100644 --- a/server/aws-lsp-codewhisperer/src/client/token/bearer-token-service.json +++ b/server/aws-lsp-codewhisperer/src/client/token/bearer-token-service.json @@ -1890,6 +1890,10 @@ "type": "string", "enum": ["BLOCK", "LINE"] }, + "SuggestionType": { + "type": "string", + "enum": ["COMPLETIONS", "EDITS"] + }, "Completions": { "type": "list", "member": { @@ -6299,6 +6303,12 @@ }, "ideVersion": { "shape": "String" + }, + "pluginVersion": { + "shape": "String" + }, + "lspVersion": { + "shape": "String" } } }, @@ -6540,6 +6550,9 @@ }, "streakLength": { "shape": "UserTriggerDecisionEventStreakLengthInteger" + }, + "suggestionType": { + "shape": "SuggestionType" } } }, diff --git a/server/aws-lsp-codewhisperer/src/client/token/codewhispererbearertokenclient.d.ts b/server/aws-lsp-codewhisperer/src/client/token/codewhispererbearertokenclient.d.ts index 34aa384c14..8e64023479 100644 --- a/server/aws-lsp-codewhisperer/src/client/token/codewhispererbearertokenclient.d.ts +++ b/server/aws-lsp-codewhisperer/src/client/token/codewhispererbearertokenclient.d.ts @@ -549,6 +549,7 @@ declare namespace CodeWhispererBearerTokenClient { } export type CompletionContentString = string; export type CompletionType = "BLOCK"|"LINE"|string; + export type SuggestionType = "COMPLETIONS"|"EDITS"|string; export type Completions = Completion[]; export interface ConsoleState { region?: String; @@ -2014,6 +2015,8 @@ declare namespace CodeWhispererBearerTokenClient { product: UserContextProductString; clientId?: UUID; ideVersion?: String; + pluginVersion?: String; + lspVersion?: String; } export type UserContextProductString = string; export interface UserInputMessage { @@ -2127,6 +2130,7 @@ declare namespace CodeWhispererBearerTokenClient { addedCharacterCount?: UserTriggerDecisionEventAddedCharacterCountInteger; deletedCharacterCount?: UserTriggerDecisionEventDeletedCharacterCountInteger; streakLength?: UserTriggerDecisionEventStreakLengthInteger; + suggestionType?: SuggestionType; } export type UserTriggerDecisionEventAddedCharacterCountInteger = number; export type UserTriggerDecisionEventDeletedCharacterCountInteger = number; @@ -2182,4 +2186,4 @@ declare namespace CodeWhispererBearerTokenClient { } export = CodeWhispererBearerTokenClient; - \ No newline at end of file + \ No newline at end of file diff --git a/server/aws-lsp-codewhisperer/src/shared/telemetry/telemetryService.test.ts b/server/aws-lsp-codewhisperer/src/shared/telemetry/telemetryService.test.ts index 04d1cf091c..b6a5592f5f 100644 --- a/server/aws-lsp-codewhisperer/src/shared/telemetry/telemetryService.test.ts +++ b/server/aws-lsp-codewhisperer/src/shared/telemetry/telemetryService.test.ts @@ -277,6 +277,7 @@ describe('TelemetryService', () => { addedIdeDiagnostics: undefined, removedIdeDiagnostics: undefined, streakLength: 0, + suggestionType: 'COMPLETIONS', }, }, optOutPreference: 'OPTIN', diff --git a/server/aws-lsp-codewhisperer/src/shared/telemetry/telemetryService.ts b/server/aws-lsp-codewhisperer/src/shared/telemetry/telemetryService.ts index b11806a020..cd2ba18254 100644 --- a/server/aws-lsp-codewhisperer/src/shared/telemetry/telemetryService.ts +++ b/server/aws-lsp-codewhisperer/src/shared/telemetry/telemetryService.ts @@ -283,14 +283,17 @@ export class TelemetryService { addedIdeDiagnostics: addedIdeDiagnostics, removedIdeDiagnostics: removedIdeDiagnostics, streakLength: streakLength ?? 0, + suggestionType: isInlineEdit ? 'EDITS' : 'COMPLETIONS', } this.logging.info(`Invoking SendTelemetryEvent:UserTriggerDecisionEvent with: + "requestId": ${event.requestId} "suggestionState": ${event.suggestionState} "acceptedCharacterCount": ${event.acceptedCharacterCount} "addedCharacterCount": ${event.addedCharacterCount} "deletedCharacterCount": ${event.deletedCharacterCount} "streakLength": ${event.streakLength} - "firstCompletionDisplayLatency: ${event.recommendationLatencyMilliseconds}`) + "firstCompletionDisplayLatency: ${event.recommendationLatencyMilliseconds} + "suggestionType": ${event.suggestionType}`) return this.invokeSendTelemetryEvent({ userTriggerDecisionEvent: event, }) diff --git a/server/aws-lsp-codewhisperer/src/shared/telemetryUtils.ts b/server/aws-lsp-codewhisperer/src/shared/telemetryUtils.ts index 49020a8822..e682263f7e 100644 --- a/server/aws-lsp-codewhisperer/src/shared/telemetryUtils.ts +++ b/server/aws-lsp-codewhisperer/src/shared/telemetryUtils.ts @@ -103,6 +103,8 @@ export const makeUserContextObject = ( product: product, clientId: initializeParams.initializationOptions?.aws?.clientInfo?.clientId, ideVersion: `ide=${ideVersion};plugin=${pluginVersion};lsp=${lspVersion}`, + pluginVersion: pluginVersion, + lspVersion: lspVersion, } if (userContext.ideCategory === 'UNKNOWN' || userContext.operatingSystem === 'UNKNOWN') { From ef7d7931954f5083e4a5c358e67c6dc652fa1a40 Mon Sep 17 00:00:00 2001 From: Jason Guo <81202082+jguoamz@users.noreply.github.com> Date: Fri, 29 Aug 2025 16:15:27 -0700 Subject: [PATCH 64/74] fix: compact UI is not updated correctly when multiple nudges are displayed (#2192) --- .../language-server/agenticChat/agenticChatController.ts | 1 + .../src/language-server/chat/chatSessionService.ts | 7 +++++++ 2 files changed, 8 insertions(+) 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 4a816317d2..4daa31db64 100644 --- a/server/aws-lsp-codewhisperer/src/language-server/agenticChat/agenticChatController.ts +++ b/server/aws-lsp-codewhisperer/src/language-server/agenticChat/agenticChatController.ts @@ -2707,6 +2707,7 @@ export class AgenticChatController implements ChatHandlers { session.setDeferredToolExecution(messageId, deferred.resolve, deferred.reject) this.#log(`Prompting for compaction approval for messageId: ${messageId}`) await deferred.promise + session.removeDeferredToolExecution(messageId) // Note: we want to overwrite the button block because it already exists in the stream. await resultStream.overwriteResultBlock(this.#getUpdateCompactConfirmResult(messageId), promptBlockId) } diff --git a/server/aws-lsp-codewhisperer/src/language-server/chat/chatSessionService.ts b/server/aws-lsp-codewhisperer/src/language-server/chat/chatSessionService.ts index 92c7eb2c33..13c198a949 100644 --- a/server/aws-lsp-codewhisperer/src/language-server/chat/chatSessionService.ts +++ b/server/aws-lsp-codewhisperer/src/language-server/chat/chatSessionService.ts @@ -78,10 +78,17 @@ export class ChatSessionService { public getDeferredToolExecution(messageId: string): DeferredHandler | undefined { return this.#deferredToolExecution[messageId] } + public setDeferredToolExecution(messageId: string, resolve: any, reject: any) { this.#deferredToolExecution[messageId] = { resolve, reject } } + public removeDeferredToolExecution(messageId: string) { + if (messageId in this.#deferredToolExecution) { + delete this.#deferredToolExecution[messageId] + } + } + public getAllDeferredCompactMessageIds(): string[] { return Object.keys(this.#deferredToolExecution).filter(messageId => messageId.endsWith('_compact')) } From fd71e6cf3fc843242936564061061418edf83f56 Mon Sep 17 00:00:00 2001 From: tsmithsz <84354541+tsmithsz@users.noreply.github.com> Date: Tue, 2 Sep 2025 11:23:18 -0700 Subject: [PATCH 65/74] fix: fix calculation for num-lines contributed by the LLM (#2191) --- .../agenticChat/agenticChatController.ts | 5 +- .../utils/fileModificationMetrics.test.ts | 139 ++++++++++++++++++ .../utils/fileModificationMetrics.ts | 58 ++++++++ 3 files changed, 199 insertions(+), 3 deletions(-) create mode 100644 server/aws-lsp-codewhisperer/src/language-server/agenticChat/utils/fileModificationMetrics.test.ts create mode 100644 server/aws-lsp-codewhisperer/src/language-server/agenticChat/utils/fileModificationMetrics.ts 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 4daa31db64..6c3f07fac2 100644 --- a/server/aws-lsp-codewhisperer/src/language-server/agenticChat/agenticChatController.ts +++ b/server/aws-lsp-codewhisperer/src/language-server/agenticChat/agenticChatController.ts @@ -166,6 +166,7 @@ import { FsWrite, FsWriteParams } from './tools/fsWrite' import { ExecuteBash, ExecuteBashParams } from './tools/executeBash' import { ExplanatoryParams, InvokeOutput, ToolApprovalException } from './tools/toolShared' import { validatePathBasic, validatePathExists, validatePaths as validatePathsSync } from './utils/pathValidation' +import { calculateModifiedLines } from './utils/fileModificationMetrics' import { GrepSearch, SanitizedRipgrepOutput } from './tools/grepSearch' import { FileSearch, FileSearchParams, isFileSearchParams } from './tools/fileSearch' import { FsReplace, FsReplaceParams } from './tools/fsReplace' @@ -2077,9 +2078,7 @@ export class AgenticChatController implements ChatHandlers { this.#abTestingAllocation?.userVariation ) // Emit acceptedLineCount when write tool is used and code changes are accepted - const beforeLines = cachedToolUse?.fileChange?.before?.split('\n').length ?? 0 - const afterLines = doc?.getText()?.split('\n').length ?? 0 - const acceptedLineCount = afterLines - beforeLines + const acceptedLineCount = calculateModifiedLines(toolUse, doc?.getText()) await this.#telemetryController.emitInteractWithMessageMetric( tabId, { diff --git a/server/aws-lsp-codewhisperer/src/language-server/agenticChat/utils/fileModificationMetrics.test.ts b/server/aws-lsp-codewhisperer/src/language-server/agenticChat/utils/fileModificationMetrics.test.ts new file mode 100644 index 0000000000..900b569e7e --- /dev/null +++ b/server/aws-lsp-codewhisperer/src/language-server/agenticChat/utils/fileModificationMetrics.test.ts @@ -0,0 +1,139 @@ +import { calculateModifiedLines } from './fileModificationMetrics' +import { ToolUse } from '@amzn/codewhisperer-streaming' +import { FS_WRITE, FS_REPLACE } from '../constants/toolConstants' +import * as assert from 'assert' + +describe('calculateModifiedLines', () => { + describe('FS_WRITE', () => { + it('should count lines for create command', () => { + const toolUse: ToolUse = { + toolUseId: 'test-1', + name: FS_WRITE, + input: { + command: 'create', + path: '/test/file.txt', + fileText: 'line1\nline2\nline3', + }, + } + const afterContent = 'line1\nline2\nline3' + + assert.strictEqual(calculateModifiedLines(toolUse, afterContent), 3) + }) + + it('should count lines for append command', () => { + const toolUse: ToolUse = { + toolUseId: 'test-2', + name: FS_WRITE, + input: { + command: 'append', + path: '/test/file.txt', + fileText: 'line4\nline5', + }, + } + + assert.strictEqual(calculateModifiedLines(toolUse), 2) + }) + + it('should handle empty content', () => { + const toolUse: ToolUse = { + toolUseId: 'test-3', + name: FS_WRITE, + input: { + command: 'create', + path: '/test/file.txt', + fileText: '', + }, + } + + assert.strictEqual(calculateModifiedLines(toolUse, ''), 0) + }) + }) + + describe('FS_REPLACE', () => { + it('should count replaced lines correctly (double counting)', () => { + const toolUse: ToolUse = { + toolUseId: 'test-4', + name: FS_REPLACE, + input: { + path: '/test/file.txt', + diffs: [ + { + oldStr: 'old line 1\nold line 2\nold line 3', + newStr: 'new line 1\nnew line 2\nnew line 3', + }, + ], + }, + } + + assert.strictEqual(calculateModifiedLines(toolUse), 6) + }) + + it('should count pure deletions', () => { + const toolUse: ToolUse = { + toolUseId: 'test-5', + name: FS_REPLACE, + input: { + path: '/test/file.txt', + diffs: [ + { + oldStr: 'line to delete 1\nline to delete 2', + newStr: '', + }, + ], + }, + } + + assert.strictEqual(calculateModifiedLines(toolUse), 2) + }) + + it('should count pure insertions', () => { + const toolUse: ToolUse = { + toolUseId: 'test-6', + name: FS_REPLACE, + input: { + path: '/test/file.txt', + diffs: [ + { + oldStr: '', + newStr: 'new line 1\nnew line 2', + }, + ], + }, + } + + assert.strictEqual(calculateModifiedLines(toolUse), 2) + }) + + it('should handle multiple diffs', () => { + const toolUse: ToolUse = { + toolUseId: 'test-7', + name: FS_REPLACE, + input: { + path: '/test/file.txt', + diffs: [ + { + oldStr: 'old line 1', + newStr: 'new line 1', + }, + { + oldStr: 'delete this line', + newStr: '', + }, + ], + }, + } + + assert.strictEqual(calculateModifiedLines(toolUse), 3) + }) + }) + + it('should return 0 for unknown tools', () => { + const toolUse: ToolUse = { + toolUseId: 'test-8', + name: 'unknownTool', + input: {}, + } + + assert.strictEqual(calculateModifiedLines(toolUse), 0) + }) +}) diff --git a/server/aws-lsp-codewhisperer/src/language-server/agenticChat/utils/fileModificationMetrics.ts b/server/aws-lsp-codewhisperer/src/language-server/agenticChat/utils/fileModificationMetrics.ts new file mode 100644 index 0000000000..361c886607 --- /dev/null +++ b/server/aws-lsp-codewhisperer/src/language-server/agenticChat/utils/fileModificationMetrics.ts @@ -0,0 +1,58 @@ +import { ToolUse } from '@amzn/codewhisperer-streaming' +import { diffLines } from 'diff' +import { FsWriteParams } from '../tools/fsWrite' +import { FsReplaceParams } from '../tools/fsReplace' +import { FS_WRITE, FS_REPLACE } from '../constants/toolConstants' + +/** + * Counts the number of lines in text, handling different line endings + * @param text The text to count lines in + * @returns The number of lines + */ +function countLines(text?: string): number { + if (!text) return 0 + const parts = text.replace(/\r\n/g, '\n').replace(/\r/g, '\n').split('\n') + return parts.length && parts[parts.length - 1] === '' ? parts.length - 1 : parts.length +} + +/** + * Calculates the actual lines modified by analyzing file modification tools. + * @param toolUse The tool use object + * @param afterContent The content after the tool execution (for FS_WRITE create operations) + * @returns The total number of lines modified (added + removed) + */ +export function calculateModifiedLines(toolUse: ToolUse, afterContent?: string): number { + if (toolUse.name === FS_WRITE) { + const input = toolUse.input as unknown as FsWriteParams + + if (input.command === 'create') { + return countLines(afterContent ?? '') + } else if (input.command === 'append') { + return countLines(input.fileText) + } + } + + if (toolUse.name === FS_REPLACE) { + const input = toolUse.input as unknown as FsReplaceParams + let linesAdded = 0 + let linesRemoved = 0 + + for (const diff of input.diffs || []) { + const oldStr = diff.oldStr ?? '' + const newStr = diff.newStr ?? '' + + const changes = diffLines(oldStr, newStr) + + for (const change of changes) { + if (change.added) { + linesAdded += countLines(change.value) + } else if (change.removed) { + linesRemoved += countLines(change.value) + } + } + } + + return linesAdded + linesRemoved + } + return 0 +} From 94723d46073a1ea8211e7ae8f9dfce3fcb809604 Mon Sep 17 00:00:00 2001 From: Will Lo <96078566+Will-ShaoHua@users.noreply.github.com> Date: Tue, 2 Sep 2025 12:36:57 -0700 Subject: [PATCH 66/74] revert: PR 2172 dedupe openTabs supplemental contexts (#2194) This reverts commit aa87ae2bd95edc1f38bf90f56093c5bf5ff18c53. --- .../supplementalContextUtil/crossFileContextUtil.ts | 11 +---------- 1 file changed, 1 insertion(+), 10 deletions(-) diff --git a/server/aws-lsp-codewhisperer/src/shared/supplementalContextUtil/crossFileContextUtil.ts b/server/aws-lsp-codewhisperer/src/shared/supplementalContextUtil/crossFileContextUtil.ts index 91f02b9dc7..39e97a3b8c 100644 --- a/server/aws-lsp-codewhisperer/src/shared/supplementalContextUtil/crossFileContextUtil.ts +++ b/server/aws-lsp-codewhisperer/src/shared/supplementalContextUtil/crossFileContextUtil.ts @@ -210,17 +210,8 @@ export async function fetchOpenTabsContext( }) } - // Dedupe code chunks based on their filePath + content unique key - const seen = new Set() - const deduped = supplementalContexts.filter(item => { - const key = `${item.filePath}:${item.content}` - if (seen.has(key)) return false - seen.add(key) - return true - }) - // DO NOT send code chunk with empty content - return deduped.filter(item => item.content.trim().length !== 0) + return supplementalContexts.filter(item => item.content.trim().length !== 0) } function findBestKChunkMatches(chunkInput: Chunk, chunkReferences: Chunk[], k: number): Chunk[] { From 58f20649d345f159080006120e23cde559826df1 Mon Sep 17 00:00:00 2001 From: chungjac Date: Tue, 2 Sep 2025 12:56:21 -0700 Subject: [PATCH 67/74] fix: emit errorMessage in addMessage (#2197) * emit errorMessage in addMessage * fix: emit errorMessage in addMessage --- .../language-server/agenticChat/agenticChatController.ts | 6 +++--- .../chat/telemetry/chatTelemetryController.ts | 8 +++++++- .../src/shared/telemetry/telemetryService.test.ts | 1 + .../src/shared/telemetry/telemetryService.ts | 2 ++ 4 files changed, 13 insertions(+), 4 deletions(-) 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 6c3f07fac2..47739e77d1 100644 --- a/server/aws-lsp-codewhisperer/src/language-server/agenticChat/agenticChatController.ts +++ b/server/aws-lsp-codewhisperer/src/language-server/agenticChat/agenticChatController.ts @@ -3371,7 +3371,7 @@ export class AgenticChatController implements ChatHandlers { metric: Metric, agenticCodingMode: boolean ): Promise> { - const errorMessage = getErrorMsg(err) + const errorMessage = getErrorMsg(err) ?? GENERIC_ERROR_MS const requestID = getRequestID(err) ?? '' metric.setDimension('cwsprChatResponseCode', getHttpStatusCode(err) ?? 0) metric.setDimension('languageServerVersion', this.#features.runtime.serverInfo.version) @@ -3381,7 +3381,7 @@ export class AgenticChatController implements ChatHandlers { metric.metric.requestIds = [requestID] metric.metric.cwsprChatMessageId = errorMessageId metric.metric.cwsprChatConversationId = conversationId - await this.#telemetryController.emitAddMessageMetric(tabId, metric.metric, 'Failed') + await this.#telemetryController.emitAddMessageMetric(tabId, metric.metric, 'Failed', errorMessage) if (isUsageLimitError(err)) { if (this.#paidTierMode !== 'paidtier') { @@ -3426,7 +3426,7 @@ export class AgenticChatController implements ChatHandlers { tabId, metric.metric, requestID, - errorMessage ?? GENERIC_ERROR_MS, + errorMessage, agenticCodingMode ) } diff --git a/server/aws-lsp-codewhisperer/src/language-server/chat/telemetry/chatTelemetryController.ts b/server/aws-lsp-codewhisperer/src/language-server/chat/telemetry/chatTelemetryController.ts index 580bcd4be2..797a1f640a 100644 --- a/server/aws-lsp-codewhisperer/src/language-server/chat/telemetry/chatTelemetryController.ts +++ b/server/aws-lsp-codewhisperer/src/language-server/chat/telemetry/chatTelemetryController.ts @@ -294,7 +294,12 @@ export class ChatTelemetryController { }) } - public emitAddMessageMetric(tabId: string, metric: Partial, result?: string) { + public emitAddMessageMetric( + tabId: string, + metric: Partial, + result?: string, + errorMessage?: string + ) { const conversationId = this.getConversationId(tabId) // Store the customization value associated with the message if (metric.cwsprChatMessageId && metric.codewhispererCustomizationArn) { @@ -349,6 +354,7 @@ export class ChatTelemetryController { requestIds: metric.requestIds, experimentName: metric.experimentName, userVariation: metric.userVariation, + errorMessage: errorMessage, } ) } diff --git a/server/aws-lsp-codewhisperer/src/shared/telemetry/telemetryService.test.ts b/server/aws-lsp-codewhisperer/src/shared/telemetry/telemetryService.test.ts index b6a5592f5f..d66ada7707 100644 --- a/server/aws-lsp-codewhisperer/src/shared/telemetry/telemetryService.test.ts +++ b/server/aws-lsp-codewhisperer/src/shared/telemetry/telemetryService.test.ts @@ -859,6 +859,7 @@ describe('TelemetryService', () => { cwsprChatPinnedFileContextCount: undefined, cwsprChatPinnedFolderContextCount: undefined, cwsprChatPinnedPromptContextCount: undefined, + errorMessage: undefined, }, }) }) diff --git a/server/aws-lsp-codewhisperer/src/shared/telemetry/telemetryService.ts b/server/aws-lsp-codewhisperer/src/shared/telemetry/telemetryService.ts index cd2ba18254..78182c076f 100644 --- a/server/aws-lsp-codewhisperer/src/shared/telemetry/telemetryService.ts +++ b/server/aws-lsp-codewhisperer/src/shared/telemetry/telemetryService.ts @@ -564,6 +564,7 @@ export class TelemetryService { requestIds?: string[] experimentName?: string userVariation?: string + errorMessage?: string }> ) { const timeBetweenChunks = params.timeBetweenChunks?.slice(0, 100) @@ -617,6 +618,7 @@ export class TelemetryService { requestIds: truncatedRequestIds, experimentName: additionalParams.experimentName, userVariation: additionalParams.userVariation, + errorMessage: additionalParams.errorMessage, }, }) } From cb2b9a8c25982d1521e1546d7103c49b3cb9dd61 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Wed, 3 Sep 2025 10:00:32 -0700 Subject: [PATCH 68/74] chore(release): release packages from branch main (#2190) Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> --- .release-please-manifest.json | 2 +- package-lock.json | 2 +- server/aws-lsp-codewhisperer/CHANGELOG.md | 27 +++++++++++++++++++++++ server/aws-lsp-codewhisperer/package.json | 2 +- 4 files changed, 30 insertions(+), 3 deletions(-) diff --git a/.release-please-manifest.json b/.release-please-manifest.json index e6baf94939..10e6c765df 100644 --- a/.release-please-manifest.json +++ b/.release-please-manifest.json @@ -2,7 +2,7 @@ "chat-client": "0.1.34", "core/aws-lsp-core": "0.0.14", "server/aws-lsp-antlr4": "0.1.18", - "server/aws-lsp-codewhisperer": "0.0.76", + "server/aws-lsp-codewhisperer": "0.0.77", "server/aws-lsp-json": "0.1.18", "server/aws-lsp-partiql": "0.0.17", "server/aws-lsp-yaml": "0.1.18" diff --git a/package-lock.json b/package-lock.json index 86d2516d9d..70518fd0ab 100644 --- a/package-lock.json +++ b/package-lock.json @@ -28669,7 +28669,7 @@ }, "server/aws-lsp-codewhisperer": { "name": "@aws/lsp-codewhisperer", - "version": "0.0.76", + "version": "0.0.77", "bundleDependencies": [ "@amzn/codewhisperer-streaming", "@amzn/amazon-q-developer-streaming-client" diff --git a/server/aws-lsp-codewhisperer/CHANGELOG.md b/server/aws-lsp-codewhisperer/CHANGELOG.md index 8bda2b6803..4bb50ee8fd 100644 --- a/server/aws-lsp-codewhisperer/CHANGELOG.md +++ b/server/aws-lsp-codewhisperer/CHANGELOG.md @@ -1,5 +1,32 @@ # Changelog +## [0.0.77](https://github.com/aws/language-servers/compare/lsp-codewhisperer/v0.0.76...lsp-codewhisperer/v0.0.77) (2025-09-02) + + +### Features + +* passing suggestionTypes and pluginVersion/lspVersion to STE ([#2180](https://github.com/aws/language-servers/issues/2180)) ([66742ad](https://github.com/aws/language-servers/commit/66742adfc44f33efbd8dd33b803000e08241e5ce)) + + +### Bug Fixes + +* auto trigger should only respect previous decisions in the past 2mins ([#2189](https://github.com/aws/language-servers/issues/2189)) ([852b21b](https://github.com/aws/language-servers/commit/852b21b66f793102c52e35c2baec07a772e5134a)) +* compact UI is not updated correctly when multiple nudges are displayed ([#2192](https://github.com/aws/language-servers/issues/2192)) ([ef7d793](https://github.com/aws/language-servers/commit/ef7d7931954f5083e4a5c358e67c6dc652fa1a40)) +* emit acceptedLineCount metric and AgenticCodeAccepted interaction type ([#2167](https://github.com/aws/language-servers/issues/2167)) ([c53f672](https://github.com/aws/language-servers/commit/c53f672b6173ebda530917ccb4e0c2f26f5c8f79)) +* emit errorMessage in addMessage ([#2197](https://github.com/aws/language-servers/issues/2197)) ([58f2064](https://github.com/aws/language-servers/commit/58f20649d345f159080006120e23cde559826df1)) +* fix calculation for num-lines contributed by the LLM ([#2191](https://github.com/aws/language-servers/issues/2191)) ([fd71e6c](https://github.com/aws/language-servers/commit/fd71e6cf3fc843242936564061061418edf83f56)) +* should send classifier score after taking sigmoid ([#2188](https://github.com/aws/language-servers/issues/2188)) ([f4e2e6e](https://github.com/aws/language-servers/commit/f4e2e6e885e665834a5d7b7cbb5f4ba4ff9bbb65)) + + +### Performance Improvements + +* only process edit requests 1 at a time ([#2187](https://github.com/aws/language-servers/issues/2187)) ([b497540](https://github.com/aws/language-servers/commit/b4975409a3ed518550290b72ac310895a293be4b)) + + +### Reverts + +* PR 2172 dedupe openTabs supplemental contexts ([#2194](https://github.com/aws/language-servers/issues/2194)) ([94723d4](https://github.com/aws/language-servers/commit/94723d46073a1ea8211e7ae8f9dfce3fcb809604)) + ## [0.0.76](https://github.com/aws/language-servers/compare/lsp-codewhisperer/v0.0.75...lsp-codewhisperer/v0.0.76) (2025-08-27) diff --git a/server/aws-lsp-codewhisperer/package.json b/server/aws-lsp-codewhisperer/package.json index 8702e6fab3..aba60487ae 100644 --- a/server/aws-lsp-codewhisperer/package.json +++ b/server/aws-lsp-codewhisperer/package.json @@ -1,6 +1,6 @@ { "name": "@aws/lsp-codewhisperer", - "version": "0.0.76", + "version": "0.0.77", "description": "CodeWhisperer Language Server", "main": "out/index.js", "repository": { From 34bc9bd1d3433bbb1d903eb0f212b10709ea8412 Mon Sep 17 00:00:00 2001 From: mkovelam Date: Wed, 3 Sep 2025 11:14:25 -0700 Subject: [PATCH 69/74] feat: model selection for code review tool (#2196) * feat: enabling model selection for code review tool * feat: added unit test for model selection for code review tool * fix: making modelId required for codeReviewTool in input schema validation * fix: fixing unit tests --------- Co-authored-by: Laxman Reddy <141967714+laileni-aws@users.noreply.github.com> --- .../client/token/bearer-token-service.json | 6 ++ .../token/codewhispererbearertokenclient.d.ts | 4 +- .../agenticChat/agenticChatController.ts | 1 + .../tools/qCodeAnalysis/codeReview.test.ts | 61 +++++++++++++++++++ .../tools/qCodeAnalysis/codeReview.ts | 11 +++- .../tools/qCodeAnalysis/codeReviewSchemas.ts | 1 + .../tools/qCodeAnalysis/codeReviewTypes.ts | 1 + 7 files changed, 83 insertions(+), 2 deletions(-) diff --git a/server/aws-lsp-codewhisperer/src/client/token/bearer-token-service.json b/server/aws-lsp-codewhisperer/src/client/token/bearer-token-service.json index 58ae54fb3c..f54c13cbdd 100644 --- a/server/aws-lsp-codewhisperer/src/client/token/bearer-token-service.json +++ b/server/aws-lsp-codewhisperer/src/client/token/bearer-token-service.json @@ -4822,6 +4822,12 @@ }, "profileArn": { "shape": "ProfileArn" + }, + "languageModelId": { + "shape": "ModelId" + }, + "clientType": { + "shape": "Origin" } } }, diff --git a/server/aws-lsp-codewhisperer/src/client/token/codewhispererbearertokenclient.d.ts b/server/aws-lsp-codewhisperer/src/client/token/codewhispererbearertokenclient.d.ts index 8e64023479..4c8018c84e 100644 --- a/server/aws-lsp-codewhisperer/src/client/token/codewhispererbearertokenclient.d.ts +++ b/server/aws-lsp-codewhisperer/src/client/token/codewhispererbearertokenclient.d.ts @@ -1539,6 +1539,8 @@ declare namespace CodeWhispererBearerTokenClient { codeScanName?: CodeScanName; codeDiffMetadata?: CodeDiffMetadata; profileArn?: ProfileArn; + languageModelId?: ModelId; + clientType?: Origin; } export type StartCodeAnalysisRequestClientTokenString = string; export interface StartCodeAnalysisResponse { @@ -2186,4 +2188,4 @@ declare namespace CodeWhispererBearerTokenClient { } export = CodeWhispererBearerTokenClient; - \ No newline at end of file + \ No newline at end of file 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 47739e77d1..e2fba029c4 100644 --- a/server/aws-lsp-codewhisperer/src/language-server/agenticChat/agenticChatController.ts +++ b/server/aws-lsp-codewhisperer/src/language-server/agenticChat/agenticChatController.ts @@ -1989,6 +1989,7 @@ export class AgenticChatController implements ChatHandlers { .filter(c => c.type === 'rule') .map(c => ({ path: c.path })) } + initialInput['modelId'] = session.modelId toolUse.input = initialInput } catch (e) { this.#features.logging.warn(`could not parse CodeReview tool input: ${e}`) diff --git a/server/aws-lsp-codewhisperer/src/language-server/agenticChat/tools/qCodeAnalysis/codeReview.test.ts b/server/aws-lsp-codewhisperer/src/language-server/agenticChat/tools/qCodeAnalysis/codeReview.test.ts index 3509cd1730..f6f735da09 100644 --- a/server/aws-lsp-codewhisperer/src/language-server/agenticChat/tools/qCodeAnalysis/codeReview.test.ts +++ b/server/aws-lsp-codewhisperer/src/language-server/agenticChat/tools/qCodeAnalysis/codeReview.test.ts @@ -11,6 +11,7 @@ import * as path from 'path' import { expect } from 'chai' import { CancellationError } from '@aws/lsp-core' import * as JSZip from 'jszip' +import { Origin } from '@amzn/codewhisperer-streaming' describe('CodeReview', () => { let sandbox: sinon.SinonSandbox @@ -103,6 +104,7 @@ describe('CodeReview', () => { folderLevelArtifacts: [], ruleArtifacts: [], scopeOfReview: FULL_REVIEW, + modelId: 'claude-4-sonnet', } }) @@ -143,6 +145,61 @@ describe('CodeReview', () => { expect(result.output.kind).to.equal('json') }) + it('should execute successfully and pass languageModelId and clientType to startCodeAnalysis', async () => { + const inputWithModelId = { + ...validInput, + modelId: 'test-model-789', + } + + // Setup mocks for successful execution + mockCodeWhispererClient.createUploadUrl.resolves({ + uploadUrl: 'https://upload.com', + uploadId: 'upload-123', + requestHeaders: {}, + }) + + mockCodeWhispererClient.startCodeAnalysis.resolves({ + jobId: 'job-123', + status: 'Pending', + }) + + mockCodeWhispererClient.getCodeAnalysis.resolves({ + status: 'Completed', + }) + + mockCodeWhispererClient.listCodeAnalysisFindings.resolves({ + codeAnalysisFindings: '[]', + nextToken: undefined, + }) + + sandbox.stub(CodeReviewUtils, 'uploadFileToPresignedUrl').resolves() + sandbox.stub(codeReview as any, 'prepareFilesAndFoldersForUpload').resolves({ + zipBuffer: Buffer.from('test'), + md5Hash: 'hash123', + isCodeDiffPresent: false, + programmingLanguages: new Set(['javascript']), + }) + sandbox.stub(codeReview as any, 'parseFindings').returns([]) + + const result = await codeReview.execute(inputWithModelId, context) + + expect(result.output.success).to.be.true + expect(result.output.kind).to.equal('json') + + // Verify that startCodeAnalysis was called with the correct parameters + expect(mockCodeWhispererClient.startCodeAnalysis.calledOnce).to.be.true + const startAnalysisCall = mockCodeWhispererClient.startCodeAnalysis.getCall(0) + const callArgs = startAnalysisCall.args[0] + + expect(callArgs).to.have.property('languageModelId', 'test-model-789') + expect(callArgs).to.have.property('clientType', Origin.IDE) + expect(callArgs).to.have.property('artifacts') + expect(callArgs).to.have.property('programmingLanguage') + expect(callArgs).to.have.property('clientToken') + expect(callArgs).to.have.property('codeScanName') + expect(callArgs).to.have.property('scope', 'AGENTIC') + }) + it('should handle missing client error', async () => { context.codeWhispererClient = undefined @@ -160,6 +217,7 @@ describe('CodeReview', () => { folderLevelArtifacts: [], ruleArtifacts: [], scopeOfReview: FULL_REVIEW, + modelId: 'claude-4-sonnet', } try { @@ -279,6 +337,7 @@ describe('CodeReview', () => { folderLevelArtifacts: [], ruleArtifacts: [], scopeOfReview: FULL_REVIEW, + modelId: 'claude-4-sonnet', } const context = { @@ -303,6 +362,7 @@ describe('CodeReview', () => { folderLevelArtifacts: [{ path: '/test/folder' }], ruleArtifacts: [], scopeOfReview: CODE_DIFF_REVIEW, + modelId: 'claude-4-sonnet', } const context = { @@ -614,6 +674,7 @@ describe('CodeReview', () => { folderLevelArtifacts: [], ruleArtifacts: [], scopeOfReview: FULL_REVIEW, + modelId: 'claude-4-sonnet', } // Make prepareFilesAndFoldersForUpload throw an error diff --git a/server/aws-lsp-codewhisperer/src/language-server/agenticChat/tools/qCodeAnalysis/codeReview.ts b/server/aws-lsp-codewhisperer/src/language-server/agenticChat/tools/qCodeAnalysis/codeReview.ts index d9fd42d621..ece6a73486 100644 --- a/server/aws-lsp-codewhisperer/src/language-server/agenticChat/tools/qCodeAnalysis/codeReview.ts +++ b/server/aws-lsp-codewhisperer/src/language-server/agenticChat/tools/qCodeAnalysis/codeReview.ts @@ -31,6 +31,7 @@ import { SuccessMetricName, } from './codeReviewTypes' import { CancellationError } from '@aws/lsp-core' +import { Origin } from '@amzn/codewhisperer-streaming' export class CodeReview { private static readonly CUSTOMER_CODE_BASE_PATH = 'customerCodeBaseFolder' @@ -158,6 +159,7 @@ export class CodeReview { const fileArtifacts = validatedInput.fileLevelArtifacts || [] const folderArtifacts = validatedInput.folderLevelArtifacts || [] const ruleArtifacts = validatedInput.ruleArtifacts || [] + const modelId = validatedInput.modelId if (fileArtifacts.length === 0 && folderArtifacts.length === 0) { CodeReviewUtils.emitMetric( @@ -182,7 +184,7 @@ export class CodeReview { const programmingLanguage = 'java' const scanName = 'Standard-' + randomUUID() - this.logging.info(`Agentic scan name: ${scanName}`) + this.logging.info(`Agentic scan name: ${scanName} selectedModel: ${modelId}`) return { fileArtifacts, @@ -192,6 +194,7 @@ export class CodeReview { programmingLanguage, scanName, ruleArtifacts, + modelId, } } @@ -274,6 +277,8 @@ export class CodeReview { codeScanName: setup.scanName, scope: CodeReview.SCAN_SCOPE, codeDiffMetadata: uploadResult.isCodeDiffPresent ? { codeDiffPath: '/code_artifact/codeDiff/' } : undefined, + languageModelId: setup.modelId, + clientType: Origin.IDE, }) if (!createResponse.jobId) { @@ -293,6 +298,7 @@ export class CodeReview { customRules: setup.ruleArtifacts.length, programmingLanguages: Array.from(uploadResult.programmingLanguages), scope: setup.isFullReviewRequest ? FULL_REVIEW : CODE_DIFF_REVIEW, + modelId: setup.modelId, }, }, this.logging, @@ -349,6 +355,7 @@ export class CodeReview { programmingLanguages: Array.from(uploadResult.programmingLanguages), scope: setup.isFullReviewRequest ? FULL_REVIEW : CODE_DIFF_REVIEW, status: status, + modelId: setup.modelId, }, }, this.logging, @@ -382,6 +389,7 @@ export class CodeReview { programmingLanguages: Array.from(uploadResult.programmingLanguages), scope: setup.isFullReviewRequest ? FULL_REVIEW : CODE_DIFF_REVIEW, status: status, + modelId: setup.modelId, }, }, this.logging, @@ -426,6 +434,7 @@ export class CodeReview { programmingLanguages: Array.from(uploadResult.programmingLanguages), scope: setup.isFullReviewRequest ? FULL_REVIEW : CODE_DIFF_REVIEW, latency: Date.now() - this.toolStartTime, + modelId: setup.modelId, }, }, this.logging, diff --git a/server/aws-lsp-codewhisperer/src/language-server/agenticChat/tools/qCodeAnalysis/codeReviewSchemas.ts b/server/aws-lsp-codewhisperer/src/language-server/agenticChat/tools/qCodeAnalysis/codeReviewSchemas.ts index d734412066..e805145244 100644 --- a/server/aws-lsp-codewhisperer/src/language-server/agenticChat/tools/qCodeAnalysis/codeReviewSchemas.ts +++ b/server/aws-lsp-codewhisperer/src/language-server/agenticChat/tools/qCodeAnalysis/codeReviewSchemas.ts @@ -113,6 +113,7 @@ export const Z_CODE_REVIEW_INPUT_SCHEMA = z.object({ }) ) .optional(), + modelId: z.string(), }) /** diff --git a/server/aws-lsp-codewhisperer/src/language-server/agenticChat/tools/qCodeAnalysis/codeReviewTypes.ts b/server/aws-lsp-codewhisperer/src/language-server/agenticChat/tools/qCodeAnalysis/codeReviewTypes.ts index 728c226ae1..c684cfecd3 100644 --- a/server/aws-lsp-codewhisperer/src/language-server/agenticChat/tools/qCodeAnalysis/codeReviewTypes.ts +++ b/server/aws-lsp-codewhisperer/src/language-server/agenticChat/tools/qCodeAnalysis/codeReviewTypes.ts @@ -21,6 +21,7 @@ export type ValidateInputAndSetupResult = { programmingLanguage: string scanName: string ruleArtifacts: RuleArtifacts + modelId?: string } export type PrepareAndUploadArtifactsResult = { From 512502af947dcfed9288be2f67fc58affd4445fe Mon Sep 17 00:00:00 2001 From: invictus <149003065+ashishrp-aws@users.noreply.github.com> Date: Wed, 3 Sep 2025 16:32:17 -0700 Subject: [PATCH 70/74] fix(amazonq): fix to update MCP servers list when last server is removed from agent config (#2206) --- .../src/language-server/agenticChat/tools/mcp/mcpManager.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/server/aws-lsp-codewhisperer/src/language-server/agenticChat/tools/mcp/mcpManager.ts b/server/aws-lsp-codewhisperer/src/language-server/agenticChat/tools/mcp/mcpManager.ts index 027e349cf5..41d721b74f 100644 --- a/server/aws-lsp-codewhisperer/src/language-server/agenticChat/tools/mcp/mcpManager.ts +++ b/server/aws-lsp-codewhisperer/src/language-server/agenticChat/tools/mcp/mcpManager.ts @@ -231,6 +231,9 @@ export class McpManager { } this.features.logging.info(`MCP: completed initialization of ${totalServers} servers`) + } else { + // Emit event to refresh MCP list page when no servers are configured + this.setState('no-servers', McpServerStatus.UNINITIALIZED, 0) } for (const [sanitizedName, _] of this.mcpServers.entries()) { From ab211c48ffcc2171aca7c39d43d7a64be78a08cd Mon Sep 17 00:00:00 2001 From: Will Lo <96078566+Will-ShaoHua@users.noreply.github.com> Date: Wed, 3 Sep 2025 16:38:43 -0700 Subject: [PATCH 71/74] chore: merge agentic version 1.31.0 (#2205) * chore: bump agentic version: 1.31.0 * chore: empty commit to trigger release workflow (#2203) --------- Co-authored-by: aws-toolkit-automation <> --- app/aws-lsp-codewhisperer-runtimes/src/version.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/aws-lsp-codewhisperer-runtimes/src/version.json b/app/aws-lsp-codewhisperer-runtimes/src/version.json index e842765e3f..6d9528a4e7 100644 --- a/app/aws-lsp-codewhisperer-runtimes/src/version.json +++ b/app/aws-lsp-codewhisperer-runtimes/src/version.json @@ -1,3 +1,3 @@ { - "agenticChat": "1.30.0" + "agenticChat": "1.31.0" } From 013aa5913c242451a91ed36b0dcf961a3f8ec697 Mon Sep 17 00:00:00 2001 From: andrewyuq <89420755+andrewyuq@users.noreply.github.com> Date: Wed, 3 Sep 2025 17:03:49 -0700 Subject: [PATCH 72/74] fix(amazonq): add IntelliSense autotriggerType (#2199) --- .../inline-completion/auto-trigger/autoTrigger.ts | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/server/aws-lsp-codewhisperer/src/language-server/inline-completion/auto-trigger/autoTrigger.ts b/server/aws-lsp-codewhisperer/src/language-server/inline-completion/auto-trigger/autoTrigger.ts index 8990756dbf..b6657b229c 100644 --- a/server/aws-lsp-codewhisperer/src/language-server/inline-completion/auto-trigger/autoTrigger.ts +++ b/server/aws-lsp-codewhisperer/src/language-server/inline-completion/auto-trigger/autoTrigger.ts @@ -32,7 +32,7 @@ export type CodewhispererTriggerType = 'AutoTrigger' | 'OnDemand' // Two triggers are explicitly handled, SpecialCharacters and Enter. Everything else is expected to be a trigger // based on regular typing, and is considered a 'Classifier' trigger. -export type CodewhispererAutomatedTriggerType = 'SpecialCharacters' | 'Enter' | 'Classifier' +export type CodewhispererAutomatedTriggerType = 'SpecialCharacters' | 'Enter' | 'Classifier' | 'IntelliSenseAcceptance' /** * Determine the trigger type based on the file context. Currently supports special cases for Special Characters and Enter keys, @@ -104,6 +104,10 @@ function isTabKey(str: string): boolean { return false } +function isIntelliSenseAcceptance(str: string) { + return str === 'IntelliSenseAcceptance' +} + // Reference: https://github.com/aws/aws-toolkit-vscode/blob/amazonq/v1.74.0/packages/core/src/codewhisperer/service/keyStrokeHandler.ts#L222 // Enter, Special character guarantees a trigger // Regular keystroke input will be evaluated by classifier @@ -126,6 +130,8 @@ export const getAutoTriggerType = ( return undefined } else if (isUserTypingSpecialChar(changedText)) { return 'SpecialCharacters' + } else if (isIntelliSenseAcceptance(changedText)) { + return 'IntelliSenseAcceptance' } else if (changedText.length === 1) { return 'Classifier' } else if (new RegExp('^[ ]+$').test(changedText)) { From 8bde8c97e1e3bcd67d9816a3385c50c7765c3b2f Mon Sep 17 00:00:00 2001 From: invictus <149003065+ashishrp-aws@users.noreply.github.com> Date: Fri, 5 Sep 2025 10:53:02 -0700 Subject: [PATCH 73/74] fix(amazonq): fix to correct the client for getProfile request (#2211) * fix(amazonq): fix to correct the client for getProfile request * fix(amazonq): removed unneccessary constructor objects * fix: fix to add assigned VARs in unit tests --- .../tools/mcp/profileStatusMonitor.test.ts | 76 +++++----------- .../tools/mcp/profileStatusMonitor.ts | 29 ++----- .../agenticChat/tools/toolServer.ts | 86 ++++++++----------- 3 files changed, 65 insertions(+), 126 deletions(-) diff --git a/server/aws-lsp-codewhisperer/src/language-server/agenticChat/tools/mcp/profileStatusMonitor.test.ts b/server/aws-lsp-codewhisperer/src/language-server/agenticChat/tools/mcp/profileStatusMonitor.test.ts index 77080bf08a..6fb0e56f9a 100644 --- a/server/aws-lsp-codewhisperer/src/language-server/agenticChat/tools/mcp/profileStatusMonitor.test.ts +++ b/server/aws-lsp-codewhisperer/src/language-server/agenticChat/tools/mcp/profileStatusMonitor.test.ts @@ -3,17 +3,24 @@ * All Rights Reserved. SPDX-License-Identifier: Apache-2.0 */ -import { expect } from 'chai' +import * as chai from 'chai' import * as sinon from 'sinon' import { ProfileStatusMonitor } from './profileStatusMonitor' import * as AmazonQTokenServiceManagerModule from '../../../../shared/amazonQServiceManager/AmazonQTokenServiceManager' +const { expect } = chai + +interface MockLogging { + info: sinon.SinonStub + debug: sinon.SinonStub + error: sinon.SinonStub + warn: sinon.SinonStub + log: sinon.SinonStub +} + describe('ProfileStatusMonitor', () => { let profileStatusMonitor: ProfileStatusMonitor - let mockCredentialsProvider: any - let mockWorkspace: any - let mockLogging: any - let mockSdkInitializator: any + let mockLogging: MockLogging let mockOnMcpDisabled: sinon.SinonStub let mockOnMcpEnabled: sinon.SinonStub let clock: sinon.SinonFakeTimers @@ -21,29 +28,18 @@ describe('ProfileStatusMonitor', () => { beforeEach(() => { clock = sinon.useFakeTimers() - mockCredentialsProvider = { - hasCredentials: sinon.stub().returns(true), - } - - mockWorkspace = {} - mockLogging = { info: sinon.stub(), debug: sinon.stub(), + error: sinon.stub(), + warn: sinon.stub(), + log: sinon.stub(), } - mockSdkInitializator = {} mockOnMcpDisabled = sinon.stub() mockOnMcpEnabled = sinon.stub() - profileStatusMonitor = new ProfileStatusMonitor( - mockCredentialsProvider, - mockWorkspace, - mockLogging, - mockSdkInitializator, - mockOnMcpDisabled, - mockOnMcpEnabled - ) + profileStatusMonitor = new ProfileStatusMonitor(mockLogging, mockOnMcpDisabled, mockOnMcpEnabled) }) afterEach(() => { @@ -118,23 +114,9 @@ describe('ProfileStatusMonitor', () => { }) it('should be accessible across different instances', () => { - const monitor1 = new ProfileStatusMonitor( - mockCredentialsProvider, - mockWorkspace, - mockLogging, - mockSdkInitializator, - mockOnMcpDisabled, - mockOnMcpEnabled - ) - - const monitor2 = new ProfileStatusMonitor( - mockCredentialsProvider, - mockWorkspace, - mockLogging, - mockSdkInitializator, - mockOnMcpDisabled, - mockOnMcpEnabled - ) + const monitor1 = new ProfileStatusMonitor(mockLogging, mockOnMcpDisabled, mockOnMcpEnabled) + + const monitor2 = new ProfileStatusMonitor(mockLogging, mockOnMcpDisabled, mockOnMcpEnabled) // Set state through static property ;(ProfileStatusMonitor as any).lastMcpState = true @@ -151,23 +133,9 @@ describe('ProfileStatusMonitor', () => { }) it('should maintain state across multiple instances', () => { - const monitor1 = new ProfileStatusMonitor( - mockCredentialsProvider, - mockWorkspace, - mockLogging, - mockSdkInitializator, - mockOnMcpDisabled, - mockOnMcpEnabled - ) - - const monitor2 = new ProfileStatusMonitor( - mockCredentialsProvider, - mockWorkspace, - mockLogging, - mockSdkInitializator, - mockOnMcpDisabled, - mockOnMcpEnabled - ) + const monitor1 = new ProfileStatusMonitor(mockLogging, mockOnMcpDisabled, mockOnMcpEnabled) + + const monitor2 = new ProfileStatusMonitor(mockLogging, mockOnMcpDisabled, mockOnMcpEnabled) // Initially true (default value) expect(ProfileStatusMonitor.getMcpState()).to.be.true diff --git a/server/aws-lsp-codewhisperer/src/language-server/agenticChat/tools/mcp/profileStatusMonitor.ts b/server/aws-lsp-codewhisperer/src/language-server/agenticChat/tools/mcp/profileStatusMonitor.ts index 53a4da9090..3489ad81be 100644 --- a/server/aws-lsp-codewhisperer/src/language-server/agenticChat/tools/mcp/profileStatusMonitor.ts +++ b/server/aws-lsp-codewhisperer/src/language-server/agenticChat/tools/mcp/profileStatusMonitor.ts @@ -3,15 +3,9 @@ * All Rights Reserved. SPDX-License-Identifier: Apache-2.0 */ -import { - CredentialsProvider, - Logging, - SDKInitializator, - Workspace, -} from '@aws/language-server-runtimes/server-interface' +import { Logging } from '@aws/language-server-runtimes/server-interface' import { retryUtils } from '@aws/lsp-core' import { CodeWhispererServiceToken } from '../../../../shared/codeWhispererService' -import { DEFAULT_AWS_Q_ENDPOINT_URL, DEFAULT_AWS_Q_REGION } from '../../../../shared/constants' import { AmazonQTokenServiceManager } from '../../../../shared/amazonQServiceManager/AmazonQTokenServiceManager' import * as fs from 'fs' import * as path from 'path' @@ -31,10 +25,7 @@ export class ProfileStatusMonitor { private static logging?: Logging constructor( - private credentialsProvider: CredentialsProvider, - private workspace: Workspace, private logging: Logging, - private sdkInitializator: SDKInitializator, private onMcpDisabled: () => void, private onMcpEnabled?: () => void ) { @@ -79,24 +70,15 @@ export class ProfileStatusMonitor { private async isMcpEnabled(): Promise { try { - const profileArn = this.getProfileArn() + const serviceManager = AmazonQTokenServiceManager.getInstance() + const profileArn = this.getProfileArn(serviceManager) if (!profileArn) { this.logging.debug('No profile ARN available for MCP configuration check') ProfileStatusMonitor.setMcpState(true) return true } - if (!this.codeWhispererClient) { - this.codeWhispererClient = new CodeWhispererServiceToken( - this.credentialsProvider, - this.workspace, - this.logging, - process.env.CODEWHISPERER_REGION || DEFAULT_AWS_Q_REGION, - process.env.CODEWHISPERER_ENDPOINT || DEFAULT_AWS_Q_ENDPOINT_URL, - this.sdkInitializator - ) - this.codeWhispererClient.profileArn = profileArn - } + this.codeWhispererClient = serviceManager.getCodewhispererService() const response = await retryUtils.retryWithBackoff(() => this.codeWhispererClient!.getProfile({ profileArn }) @@ -128,9 +110,8 @@ export class ProfileStatusMonitor { } } - private getProfileArn(): string | undefined { + private getProfileArn(serviceManager: AmazonQTokenServiceManager): string | undefined { try { - const serviceManager = AmazonQTokenServiceManager.getInstance() return serviceManager.getActiveProfileArn() } catch (error) { this.logging.debug(`Failed to get profile ARN: ${error}`) 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 78871faa69..8581622972 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 @@ -210,7 +210,6 @@ export const McpToolsServer: Server = ({ agent, telemetry, runtime, - sdkInitializator, chat, }) => { const registered: Record = {} @@ -358,60 +357,51 @@ export const McpToolsServer: Server = ({ return } - if (sdkInitializator) { - profileStatusMonitor = new ProfileStatusMonitor( - credentialsProvider, - workspace, - logging, - sdkInitializator, - removeAllMcpTools, - async () => { - logging.info('MCP enabled by profile status monitor') - await initializeMcp() - } - ) - - // Wait for profile ARN to be available before checking MCP state - const checkAndInitialize = async () => { - await profileStatusMonitor!.checkInitialState() - // Always initialize McpManager to handle UI requests - await initializeMcp() + profileStatusMonitor = new ProfileStatusMonitor(logging, removeAllMcpTools, async () => { + logging.info('MCP enabled by profile status monitor') + await initializeMcp() + }) - // Remove tools if MCP is disabled - if (!ProfileStatusMonitor.getMcpState()) { - removeAllMcpTools() - } + // Wait for profile ARN to be available before checking MCP state + const checkAndInitialize = async () => { + await profileStatusMonitor!.checkInitialState() + // Always initialize McpManager to handle UI requests + await initializeMcp() - profileStatusMonitor!.start() + // Remove tools if MCP is disabled + if (!ProfileStatusMonitor.getMcpState()) { + removeAllMcpTools() } - // Check if service manager is ready - try { - const serviceManager = AmazonQTokenServiceManager.getInstance() - if (serviceManager.getState() === 'INITIALIZED') { - await checkAndInitialize() - } else { - // Poll for service manager to be ready with 10s timeout - const startTime = Date.now() - const pollForReady = async () => { - if (serviceManager.getState() === 'INITIALIZED') { - await checkAndInitialize() - } else if (Date.now() - startTime < SERVICE_MANAGER_TIMEOUT_MS) { - setTimeout(pollForReady, SERVICE_MANAGER_POLL_INTERVAL_MS) - } else { - logging.warn('Service manager not ready after 10s, initializing MCP manager') - await initializeMcp() - profileStatusMonitor!.start() - } + profileStatusMonitor!.start() + } + + // Check if service manager is ready + try { + const serviceManager = AmazonQTokenServiceManager.getInstance() + if (serviceManager.getState() === 'INITIALIZED') { + await checkAndInitialize() + } else { + // Poll for service manager to be ready with 10s timeout + const startTime = Date.now() + const pollForReady = async () => { + if (serviceManager.getState() === 'INITIALIZED') { + await checkAndInitialize() + } else if (Date.now() - startTime < SERVICE_MANAGER_TIMEOUT_MS) { + setTimeout(pollForReady, SERVICE_MANAGER_POLL_INTERVAL_MS) + } else { + logging.warn('Service manager not ready after 10s, initializing MCP manager') + await initializeMcp() + profileStatusMonitor!.start() } - setTimeout(pollForReady, SERVICE_MANAGER_POLL_INTERVAL_MS) } - } catch (error) { - // Service manager not initialized yet, always initialize McpManager - logging.info('Service manager not ready, initializing MCP manager') - await initializeMcp() - profileStatusMonitor!.start() + setTimeout(pollForReady, SERVICE_MANAGER_POLL_INTERVAL_MS) } + } catch (error) { + // Service manager not initialized yet, always initialize McpManager + logging.info('Service manager not ready, initializing MCP manager') + await initializeMcp() + profileStatusMonitor!.start() } } catch (error) { console.warn('Caught error during MCP tool initialization; initialization may be incomplete:', error) From 2ddcae7a4fac6b89cbc9784911959743ea0a6d11 Mon Sep 17 00:00:00 2001 From: Lei Gao <97199248+leigaol@users.noreply.github.com> Date: Fri, 5 Sep 2025 14:18:13 -0700 Subject: [PATCH 74/74] feat: add support for getSupplementalContext LSP API (#2212) * feat: add supplemental context API * feat: add supplemental context API support --- app/aws-lsp-antlr4-runtimes/package.json | 2 +- app/aws-lsp-buildspec-runtimes/package.json | 2 +- .../package.json | 2 +- .../package.json | 2 +- app/aws-lsp-identity-runtimes/package.json | 2 +- app/aws-lsp-json-runtimes/package.json | 2 +- .../package.json | 2 +- app/aws-lsp-partiql-runtimes/package.json | 2 +- app/aws-lsp-s3-runtimes/package.json | 2 +- app/aws-lsp-yaml-json-webworker/package.json | 2 +- app/aws-lsp-yaml-runtimes/package.json | 2 +- app/hello-world-lsp-runtimes/package.json | 2 +- chat-client/package.json | 2 +- client/vscode/package.json | 2 +- core/aws-lsp-core/package.json | 2 +- .../q-agentic-chat-server/package.json | 2 +- package-lock.json | 63 ++++++++++--------- server/aws-lsp-antlr4/package.json | 2 +- server/aws-lsp-buildspec/package.json | 2 +- server/aws-lsp-cloudformation/package.json | 2 +- server/aws-lsp-codewhisperer/package.json | 2 +- .../localProjectContextServer.ts | 25 +++++++- server/aws-lsp-identity/package.json | 2 +- server/aws-lsp-json/package.json | 2 +- server/aws-lsp-notification/package.json | 2 +- server/aws-lsp-partiql/package.json | 2 +- server/aws-lsp-s3/package.json | 2 +- server/aws-lsp-yaml/package.json | 2 +- server/device-sso-auth-lsp/package.json | 2 +- server/hello-world-lsp/package.json | 2 +- 30 files changed, 84 insertions(+), 60 deletions(-) diff --git a/app/aws-lsp-antlr4-runtimes/package.json b/app/aws-lsp-antlr4-runtimes/package.json index 89fa95dcb3..20409656e1 100644 --- a/app/aws-lsp-antlr4-runtimes/package.json +++ b/app/aws-lsp-antlr4-runtimes/package.json @@ -12,7 +12,7 @@ "webpack": "webpack" }, "dependencies": { - "@aws/language-server-runtimes": "^0.2.127", + "@aws/language-server-runtimes": "^0.2.128", "@aws/lsp-antlr4": "*", "antlr4-c3": "^3.4.1", "antlr4ng": "^3.0.4" diff --git a/app/aws-lsp-buildspec-runtimes/package.json b/app/aws-lsp-buildspec-runtimes/package.json index 0ad07ddb8f..a080ed2edb 100644 --- a/app/aws-lsp-buildspec-runtimes/package.json +++ b/app/aws-lsp-buildspec-runtimes/package.json @@ -7,7 +7,7 @@ "compile": "tsc --build" }, "dependencies": { - "@aws/language-server-runtimes": "^0.2.127", + "@aws/language-server-runtimes": "^0.2.128", "@aws/lsp-buildspec": "^0.0.1" } } diff --git a/app/aws-lsp-cloudformation-runtimes/package.json b/app/aws-lsp-cloudformation-runtimes/package.json index d211149d0d..ad4c547839 100644 --- a/app/aws-lsp-cloudformation-runtimes/package.json +++ b/app/aws-lsp-cloudformation-runtimes/package.json @@ -7,7 +7,7 @@ "compile": "tsc --build" }, "dependencies": { - "@aws/language-server-runtimes": "^0.2.127", + "@aws/language-server-runtimes": "^0.2.128", "@aws/lsp-cloudformation": "^0.0.1" } } diff --git a/app/aws-lsp-codewhisperer-runtimes/package.json b/app/aws-lsp-codewhisperer-runtimes/package.json index b890c01a78..18de7dab8c 100644 --- a/app/aws-lsp-codewhisperer-runtimes/package.json +++ b/app/aws-lsp-codewhisperer-runtimes/package.json @@ -23,7 +23,7 @@ "local-build": "node scripts/local-build.js" }, "dependencies": { - "@aws/language-server-runtimes": "^0.2.127", + "@aws/language-server-runtimes": "^0.2.128", "@aws/lsp-codewhisperer": "*", "copyfiles": "^2.4.1", "cross-env": "^7.0.3", diff --git a/app/aws-lsp-identity-runtimes/package.json b/app/aws-lsp-identity-runtimes/package.json index f33fa80da4..4aa4f9d7f3 100644 --- a/app/aws-lsp-identity-runtimes/package.json +++ b/app/aws-lsp-identity-runtimes/package.json @@ -7,7 +7,7 @@ "compile": "tsc --build" }, "dependencies": { - "@aws/language-server-runtimes": "^0.2.127", + "@aws/language-server-runtimes": "^0.2.128", "@aws/lsp-identity": "^0.0.1" } } diff --git a/app/aws-lsp-json-runtimes/package.json b/app/aws-lsp-json-runtimes/package.json index db1949f54b..78815f73cc 100644 --- a/app/aws-lsp-json-runtimes/package.json +++ b/app/aws-lsp-json-runtimes/package.json @@ -11,7 +11,7 @@ "webpack": "webpack" }, "dependencies": { - "@aws/language-server-runtimes": "^0.2.127", + "@aws/language-server-runtimes": "^0.2.128", "@aws/lsp-json": "*" }, "devDependencies": { diff --git a/app/aws-lsp-notification-runtimes/package.json b/app/aws-lsp-notification-runtimes/package.json index a5eeecae49..cb5e90a9e5 100644 --- a/app/aws-lsp-notification-runtimes/package.json +++ b/app/aws-lsp-notification-runtimes/package.json @@ -7,7 +7,7 @@ "compile": "tsc --build" }, "dependencies": { - "@aws/language-server-runtimes": "^0.2.127", + "@aws/language-server-runtimes": "^0.2.128", "@aws/lsp-notification": "^0.0.1" } } diff --git a/app/aws-lsp-partiql-runtimes/package.json b/app/aws-lsp-partiql-runtimes/package.json index d483f3d0ce..21f089c3d6 100644 --- a/app/aws-lsp-partiql-runtimes/package.json +++ b/app/aws-lsp-partiql-runtimes/package.json @@ -11,7 +11,7 @@ "package": "npm run compile && npm run compile:webpack" }, "dependencies": { - "@aws/language-server-runtimes": "^0.2.125", + "@aws/language-server-runtimes": "^0.2.128", "@aws/lsp-partiql": "^0.0.5" }, "devDependencies": { diff --git a/app/aws-lsp-s3-runtimes/package.json b/app/aws-lsp-s3-runtimes/package.json index 9feb4f7ddc..a5eac053e3 100644 --- a/app/aws-lsp-s3-runtimes/package.json +++ b/app/aws-lsp-s3-runtimes/package.json @@ -10,7 +10,7 @@ "compile": "tsc --build" }, "dependencies": { - "@aws/language-server-runtimes": "^0.2.127", + "@aws/language-server-runtimes": "^0.2.128", "@aws/lsp-s3": "^0.0.1" } } diff --git a/app/aws-lsp-yaml-json-webworker/package.json b/app/aws-lsp-yaml-json-webworker/package.json index fc524cabdc..14f0c58491 100644 --- a/app/aws-lsp-yaml-json-webworker/package.json +++ b/app/aws-lsp-yaml-json-webworker/package.json @@ -11,7 +11,7 @@ "serve:webpack": "NODE_ENV=development webpack serve" }, "dependencies": { - "@aws/language-server-runtimes": "^0.2.127", + "@aws/language-server-runtimes": "^0.2.128", "@aws/lsp-json": "*", "@aws/lsp-yaml": "*" }, diff --git a/app/aws-lsp-yaml-runtimes/package.json b/app/aws-lsp-yaml-runtimes/package.json index 09bb93ee9a..e935c9ee0a 100644 --- a/app/aws-lsp-yaml-runtimes/package.json +++ b/app/aws-lsp-yaml-runtimes/package.json @@ -11,7 +11,7 @@ "webpack": "webpack" }, "dependencies": { - "@aws/language-server-runtimes": "^0.2.127", + "@aws/language-server-runtimes": "^0.2.128", "@aws/lsp-yaml": "*" }, "devDependencies": { diff --git a/app/hello-world-lsp-runtimes/package.json b/app/hello-world-lsp-runtimes/package.json index bff976d9b3..0d372ab621 100644 --- a/app/hello-world-lsp-runtimes/package.json +++ b/app/hello-world-lsp-runtimes/package.json @@ -15,7 +15,7 @@ }, "dependencies": { "@aws/hello-world-lsp": "^0.0.1", - "@aws/language-server-runtimes": "^0.2.127" + "@aws/language-server-runtimes": "^0.2.128" }, "devDependencies": { "@types/chai": "^4.3.5", diff --git a/chat-client/package.json b/chat-client/package.json index b838360175..bf20eecb9d 100644 --- a/chat-client/package.json +++ b/chat-client/package.json @@ -25,7 +25,7 @@ }, "dependencies": { "@aws/chat-client-ui-types": "^0.1.56", - "@aws/language-server-runtimes": "^0.2.127", + "@aws/language-server-runtimes": "^0.2.128", "@aws/language-server-runtimes-types": "^0.1.50", "@aws/mynah-ui": "^4.36.5" }, diff --git a/client/vscode/package.json b/client/vscode/package.json index a5d7f66d21..6b936bfd3f 100644 --- a/client/vscode/package.json +++ b/client/vscode/package.json @@ -352,7 +352,7 @@ "@aws-sdk/credential-providers": "^3.731.1", "@aws-sdk/types": "^3.734.0", "@aws/chat-client-ui-types": "^0.1.56", - "@aws/language-server-runtimes": "^0.2.127", + "@aws/language-server-runtimes": "^0.2.128", "@types/uuid": "^9.0.8", "@types/vscode": "^1.98.0", "jose": "^5.2.4", diff --git a/core/aws-lsp-core/package.json b/core/aws-lsp-core/package.json index 54ca980fc1..3057954dce 100644 --- a/core/aws-lsp-core/package.json +++ b/core/aws-lsp-core/package.json @@ -28,7 +28,7 @@ "prepack": "shx cp ../../LICENSE ../../NOTICE ../../SECURITY.md ." }, "dependencies": { - "@aws/language-server-runtimes": "^0.2.127", + "@aws/language-server-runtimes": "^0.2.128", "@gerhobbelt/gitignore-parser": "^0.2.0-9", "cross-spawn": "7.0.6", "jose": "^5.2.4", diff --git a/integration-tests/q-agentic-chat-server/package.json b/integration-tests/q-agentic-chat-server/package.json index ffb33f1a4e..7018052547 100644 --- a/integration-tests/q-agentic-chat-server/package.json +++ b/integration-tests/q-agentic-chat-server/package.json @@ -9,7 +9,7 @@ "test-integ": "npm run compile && mocha --timeout 30000 \"./out/**/*.test.js\" --retries 2" }, "dependencies": { - "@aws/language-server-runtimes": "^0.2.127", + "@aws/language-server-runtimes": "^0.2.128", "@aws/lsp-core": "*" }, "devDependencies": { diff --git a/package-lock.json b/package-lock.json index 70518fd0ab..e84567b613 100644 --- a/package-lock.json +++ b/package-lock.json @@ -48,7 +48,7 @@ "name": "@aws/lsp-antlr4-runtimes", "version": "0.0.1", "dependencies": { - "@aws/language-server-runtimes": "^0.2.127", + "@aws/language-server-runtimes": "^0.2.128", "@aws/lsp-antlr4": "*", "antlr4-c3": "^3.4.1", "antlr4ng": "^3.0.4" @@ -71,7 +71,7 @@ "name": "@aws/lsp-buildspec-runtimes", "version": "0.0.1", "dependencies": { - "@aws/language-server-runtimes": "^0.2.127", + "@aws/language-server-runtimes": "^0.2.128", "@aws/lsp-buildspec": "^0.0.1" } }, @@ -79,7 +79,7 @@ "name": "@aws/lsp-cloudformation-runtimes", "version": "0.0.1", "dependencies": { - "@aws/language-server-runtimes": "^0.2.127", + "@aws/language-server-runtimes": "^0.2.128", "@aws/lsp-cloudformation": "^0.0.1" } }, @@ -87,7 +87,7 @@ "name": "@aws/lsp-codewhisperer-runtimes", "version": "0.0.1", "dependencies": { - "@aws/language-server-runtimes": "^0.2.127", + "@aws/language-server-runtimes": "^0.2.128", "@aws/lsp-codewhisperer": "*", "copyfiles": "^2.4.1", "cross-env": "^7.0.3", @@ -120,7 +120,7 @@ "name": "@aws/lsp-identity-runtimes", "version": "0.1.0", "dependencies": { - "@aws/language-server-runtimes": "^0.2.127", + "@aws/language-server-runtimes": "^0.2.128", "@aws/lsp-identity": "^0.0.1" } }, @@ -128,7 +128,7 @@ "name": "@aws/lsp-json-runtimes", "version": "0.0.1", "dependencies": { - "@aws/language-server-runtimes": "^0.2.127", + "@aws/language-server-runtimes": "^0.2.128", "@aws/lsp-json": "*" }, "devDependencies": { @@ -148,7 +148,7 @@ "name": "@aws/lsp-notification-runtimes", "version": "0.1.0", "dependencies": { - "@aws/language-server-runtimes": "^0.2.127", + "@aws/language-server-runtimes": "^0.2.128", "@aws/lsp-notification": "^0.0.1" } }, @@ -156,7 +156,7 @@ "name": "@aws/lsp-partiql-runtimes", "version": "0.0.1", "dependencies": { - "@aws/language-server-runtimes": "^0.2.125", + "@aws/language-server-runtimes": "^0.2.128", "@aws/lsp-partiql": "^0.0.5" }, "devDependencies": { @@ -181,7 +181,7 @@ "name": "@aws/lsp-s3-runtimes", "version": "0.0.1", "dependencies": { - "@aws/language-server-runtimes": "^0.2.127", + "@aws/language-server-runtimes": "^0.2.128", "@aws/lsp-s3": "^0.0.1" }, "bin": { @@ -192,7 +192,7 @@ "name": "@aws/lsp-yaml-json-webworker", "version": "0.0.1", "dependencies": { - "@aws/language-server-runtimes": "^0.2.127", + "@aws/language-server-runtimes": "^0.2.128", "@aws/lsp-json": "*", "@aws/lsp-yaml": "*" }, @@ -212,7 +212,7 @@ "name": "@aws/lsp-yaml-runtimes", "version": "0.0.1", "dependencies": { - "@aws/language-server-runtimes": "^0.2.127", + "@aws/language-server-runtimes": "^0.2.128", "@aws/lsp-yaml": "*" }, "devDependencies": { @@ -234,7 +234,7 @@ "version": "0.0.1", "dependencies": { "@aws/hello-world-lsp": "^0.0.1", - "@aws/language-server-runtimes": "^0.2.127" + "@aws/language-server-runtimes": "^0.2.128" }, "devDependencies": { "@types/chai": "^4.3.5", @@ -255,7 +255,7 @@ "license": "Apache-2.0", "dependencies": { "@aws/chat-client-ui-types": "^0.1.56", - "@aws/language-server-runtimes": "^0.2.127", + "@aws/language-server-runtimes": "^0.2.128", "@aws/language-server-runtimes-types": "^0.1.50", "@aws/mynah-ui": "^4.36.5" }, @@ -280,7 +280,7 @@ "@aws-sdk/credential-providers": "^3.731.1", "@aws-sdk/types": "^3.734.0", "@aws/chat-client-ui-types": "^0.1.56", - "@aws/language-server-runtimes": "^0.2.127", + "@aws/language-server-runtimes": "^0.2.128", "@types/uuid": "^9.0.8", "@types/vscode": "^1.98.0", "jose": "^5.2.4", @@ -296,7 +296,7 @@ "version": "0.0.14", "license": "Apache-2.0", "dependencies": { - "@aws/language-server-runtimes": "^0.2.127", + "@aws/language-server-runtimes": "^0.2.128", "@gerhobbelt/gitignore-parser": "^0.2.0-9", "cross-spawn": "7.0.6", "jose": "^5.2.4", @@ -327,7 +327,7 @@ "name": "@aws/q-agentic-chat-server-integration-tests", "version": "0.0.1", "dependencies": { - "@aws/language-server-runtimes": "^0.2.127", + "@aws/language-server-runtimes": "^0.2.128", "@aws/lsp-core": "*" }, "devDependencies": { @@ -4036,9 +4036,10 @@ "link": true }, "node_modules/@aws/language-server-runtimes": { - "version": "0.2.127", - "resolved": "https://registry.npmjs.org/@aws/language-server-runtimes/-/language-server-runtimes-0.2.127.tgz", - "integrity": "sha512-UWCfv49MYaBhxArVBWTEw2XVfIyunbm6EfS9AxSLPudcwrpOg3KAVLooXearmyM/r2hgNDGCQYI8HuKf5FAnew==", + "version": "0.2.128", + "resolved": "https://registry.npmjs.org/@aws/language-server-runtimes/-/language-server-runtimes-0.2.128.tgz", + "integrity": "sha512-C666VAvY2PQ8CQkDzjL/+N9rfcFzY6vuGe733drMwwRVHt8On0B0PQPjy31ZjxHUUcjVp78Nb9vmSUEVBfxGTQ==", + "license": "Apache-2.0", "dependencies": { "@aws/language-server-runtimes-types": "^0.1.56", "@opentelemetry/api": "^1.9.0", @@ -28607,7 +28608,7 @@ "version": "0.1.18", "license": "Apache-2.0", "dependencies": { - "@aws/language-server-runtimes": "^0.2.127", + "@aws/language-server-runtimes": "^0.2.128", "@aws/lsp-core": "^0.0.14" }, "devDependencies": { @@ -28649,7 +28650,7 @@ "name": "@aws/lsp-buildspec", "version": "0.0.1", "dependencies": { - "@aws/language-server-runtimes": "^0.2.127", + "@aws/language-server-runtimes": "^0.2.128", "@aws/lsp-json": "*", "@aws/lsp-yaml": "*", "vscode-languageserver": "^9.0.1", @@ -28660,7 +28661,7 @@ "name": "@aws/lsp-cloudformation", "version": "0.0.1", "dependencies": { - "@aws/language-server-runtimes": "^0.2.127", + "@aws/language-server-runtimes": "^0.2.128", "@aws/lsp-core": "*", "@aws/lsp-json": "*", "vscode-languageserver": "^9.0.1", @@ -28682,7 +28683,7 @@ "@aws-sdk/util-arn-parser": "^3.723.0", "@aws-sdk/util-retry": "^3.374.0", "@aws/chat-client-ui-types": "^0.1.56", - "@aws/language-server-runtimes": "^0.2.127", + "@aws/language-server-runtimes": "^0.2.128", "@aws/lsp-core": "^0.0.14", "@modelcontextprotocol/sdk": "^1.15.0", "@smithy/node-http-handler": "^2.5.0", @@ -28822,7 +28823,7 @@ "dependencies": { "@aws-sdk/client-sso-oidc": "^3.616.0", "@aws-sdk/token-providers": "^3.744.0", - "@aws/language-server-runtimes": "^0.2.127", + "@aws/language-server-runtimes": "^0.2.128", "@aws/lsp-core": "^0.0.12", "@smithy/node-http-handler": "^3.2.5", "@smithy/shared-ini-file-loader": "^4.0.1", @@ -28887,7 +28888,7 @@ "version": "0.1.18", "license": "Apache-2.0", "dependencies": { - "@aws/language-server-runtimes": "^0.2.127", + "@aws/language-server-runtimes": "^0.2.128", "@aws/lsp-core": "^0.0.14", "vscode-languageserver": "^9.0.1", "vscode-languageserver-textdocument": "^1.0.8" @@ -28904,7 +28905,7 @@ "version": "0.0.1", "license": "Apache-2.0", "dependencies": { - "@aws/language-server-runtimes": "^0.2.127", + "@aws/language-server-runtimes": "^0.2.128", "@aws/lsp-core": "^0.0.12", "vscode-languageserver": "^9.0.1" }, @@ -28965,7 +28966,7 @@ "version": "0.0.17", "license": "Apache-2.0", "dependencies": { - "@aws/language-server-runtimes": "^0.2.127", + "@aws/language-server-runtimes": "^0.2.128", "antlr4-c3": "3.4.2", "antlr4ng": "3.0.14", "web-tree-sitter": "0.22.6" @@ -28987,7 +28988,7 @@ "dependencies": { "@aws-sdk/client-s3": "^3.623.0", "@aws-sdk/types": "^3.734.0", - "@aws/language-server-runtimes": "^0.2.127", + "@aws/language-server-runtimes": "^0.2.128", "@aws/lsp-core": "^0.0.12", "vscode-languageserver": "^9.0.1", "vscode-languageserver-textdocument": "^1.0.8" @@ -29018,7 +29019,7 @@ "hasInstallScript": true, "license": "Apache-2.0", "dependencies": { - "@aws/language-server-runtimes": "^0.2.127", + "@aws/language-server-runtimes": "^0.2.128", "@aws/lsp-core": "^0.0.14", "vscode-languageserver": "^9.0.1", "vscode-languageserver-textdocument": "^1.0.8", @@ -29032,7 +29033,7 @@ "name": "@amzn/device-sso-auth-lsp", "version": "0.0.1", "dependencies": { - "@aws/language-server-runtimes": "^0.2.127", + "@aws/language-server-runtimes": "^0.2.128", "vscode-languageserver": "^9.0.1" }, "devDependencies": { @@ -29043,7 +29044,7 @@ "name": "@aws/hello-world-lsp", "version": "0.0.1", "dependencies": { - "@aws/language-server-runtimes": "^0.2.127", + "@aws/language-server-runtimes": "^0.2.128", "vscode-languageserver": "^9.0.1" }, "devDependencies": { diff --git a/server/aws-lsp-antlr4/package.json b/server/aws-lsp-antlr4/package.json index 2ca49593d7..9cad906eee 100644 --- a/server/aws-lsp-antlr4/package.json +++ b/server/aws-lsp-antlr4/package.json @@ -28,7 +28,7 @@ "clean": "rm -rf node_modules" }, "dependencies": { - "@aws/language-server-runtimes": "^0.2.127", + "@aws/language-server-runtimes": "^0.2.128", "@aws/lsp-core": "^0.0.14" }, "peerDependencies": { diff --git a/server/aws-lsp-buildspec/package.json b/server/aws-lsp-buildspec/package.json index 9754645b7d..f0184e0ff8 100644 --- a/server/aws-lsp-buildspec/package.json +++ b/server/aws-lsp-buildspec/package.json @@ -7,7 +7,7 @@ "compile": "tsc --build" }, "dependencies": { - "@aws/language-server-runtimes": "^0.2.127", + "@aws/language-server-runtimes": "^0.2.128", "@aws/lsp-json": "*", "@aws/lsp-yaml": "*", "vscode-languageserver": "^9.0.1", diff --git a/server/aws-lsp-cloudformation/package.json b/server/aws-lsp-cloudformation/package.json index 13be6a4859..89b56ede3f 100644 --- a/server/aws-lsp-cloudformation/package.json +++ b/server/aws-lsp-cloudformation/package.json @@ -7,7 +7,7 @@ "compile": "tsc --build" }, "dependencies": { - "@aws/language-server-runtimes": "^0.2.127", + "@aws/language-server-runtimes": "^0.2.128", "@aws/lsp-core": "*", "@aws/lsp-json": "*", "vscode-languageserver": "^9.0.1", diff --git a/server/aws-lsp-codewhisperer/package.json b/server/aws-lsp-codewhisperer/package.json index aba60487ae..5284a0b41f 100644 --- a/server/aws-lsp-codewhisperer/package.json +++ b/server/aws-lsp-codewhisperer/package.json @@ -36,7 +36,7 @@ "@aws-sdk/util-arn-parser": "^3.723.0", "@aws-sdk/util-retry": "^3.374.0", "@aws/chat-client-ui-types": "^0.1.56", - "@aws/language-server-runtimes": "^0.2.127", + "@aws/language-server-runtimes": "^0.2.128", "@aws/lsp-core": "^0.0.14", "@modelcontextprotocol/sdk": "^1.15.0", "@smithy/node-http-handler": "^2.5.0", diff --git a/server/aws-lsp-codewhisperer/src/language-server/localProjectContext/localProjectContextServer.ts b/server/aws-lsp-codewhisperer/src/language-server/localProjectContext/localProjectContextServer.ts index 1fa2b8301b..9cff865038 100644 --- a/server/aws-lsp-codewhisperer/src/language-server/localProjectContext/localProjectContextServer.ts +++ b/server/aws-lsp-codewhisperer/src/language-server/localProjectContext/localProjectContextServer.ts @@ -1,4 +1,10 @@ -import { InitializeParams, Server, TextDocumentSyncKind } from '@aws/language-server-runtimes/server-interface' +import { + GetSupplementalContextParams, + InitializeParams, + Server, + SupplementalContextItem, + TextDocumentSyncKind, +} from '@aws/language-server-runtimes/server-interface' import { getOrThrowBaseTokenServiceManager } from '../../shared/amazonQServiceManager/AmazonQTokenServiceManager' import { TelemetryService } from '../../shared/telemetry/telemetryService' import { LocalProjectContextController } from '../../shared/localProjectContextController' @@ -127,6 +133,23 @@ export const LocalProjectContextServer = } }) + const onGetSupplementalContext = async ( + param: GetSupplementalContextParams + ): Promise => { + if (localProjectContextController) { + const request = { + query: '', + filePath: param.filePath, + target: 'codemap', + } + const response = await localProjectContextController.queryInlineProjectContext(request) + return response + } + return [] + } + + lsp.extensions.onGetSupplementalContext(onGetSupplementalContext) + lsp.onDidSaveTextDocument(async event => { try { const filePaths = VSCWindowsOverride diff --git a/server/aws-lsp-identity/package.json b/server/aws-lsp-identity/package.json index 9aa4bedcbf..3d90af77e4 100644 --- a/server/aws-lsp-identity/package.json +++ b/server/aws-lsp-identity/package.json @@ -26,7 +26,7 @@ "dependencies": { "@aws-sdk/client-sso-oidc": "^3.616.0", "@aws-sdk/token-providers": "^3.744.0", - "@aws/language-server-runtimes": "^0.2.127", + "@aws/language-server-runtimes": "^0.2.128", "@aws/lsp-core": "^0.0.12", "@smithy/node-http-handler": "^3.2.5", "@smithy/shared-ini-file-loader": "^4.0.1", diff --git a/server/aws-lsp-json/package.json b/server/aws-lsp-json/package.json index ee5fbbc47a..6b2c95bc49 100644 --- a/server/aws-lsp-json/package.json +++ b/server/aws-lsp-json/package.json @@ -26,7 +26,7 @@ "prepack": "shx cp ../../LICENSE ../../NOTICE ../../SECURITY.md ." }, "dependencies": { - "@aws/language-server-runtimes": "^0.2.127", + "@aws/language-server-runtimes": "^0.2.128", "@aws/lsp-core": "^0.0.14", "vscode-languageserver": "^9.0.1", "vscode-languageserver-textdocument": "^1.0.8" diff --git a/server/aws-lsp-notification/package.json b/server/aws-lsp-notification/package.json index f19ddc54ca..740a109f48 100644 --- a/server/aws-lsp-notification/package.json +++ b/server/aws-lsp-notification/package.json @@ -22,7 +22,7 @@ "coverage:report": "c8 report --reporter=html --reporter=text" }, "dependencies": { - "@aws/language-server-runtimes": "^0.2.127", + "@aws/language-server-runtimes": "^0.2.128", "@aws/lsp-core": "^0.0.12", "vscode-languageserver": "^9.0.1" }, diff --git a/server/aws-lsp-partiql/package.json b/server/aws-lsp-partiql/package.json index dc2fc2ee22..e5f59a4411 100644 --- a/server/aws-lsp-partiql/package.json +++ b/server/aws-lsp-partiql/package.json @@ -24,7 +24,7 @@ "out" ], "dependencies": { - "@aws/language-server-runtimes": "^0.2.127", + "@aws/language-server-runtimes": "^0.2.128", "antlr4-c3": "3.4.2", "antlr4ng": "3.0.14", "web-tree-sitter": "0.22.6" diff --git a/server/aws-lsp-s3/package.json b/server/aws-lsp-s3/package.json index 914628d14e..8829f1dd87 100644 --- a/server/aws-lsp-s3/package.json +++ b/server/aws-lsp-s3/package.json @@ -9,7 +9,7 @@ "dependencies": { "@aws-sdk/client-s3": "^3.623.0", "@aws-sdk/types": "^3.734.0", - "@aws/language-server-runtimes": "^0.2.127", + "@aws/language-server-runtimes": "^0.2.128", "@aws/lsp-core": "^0.0.12", "vscode-languageserver": "^9.0.1", "vscode-languageserver-textdocument": "^1.0.8" diff --git a/server/aws-lsp-yaml/package.json b/server/aws-lsp-yaml/package.json index fe115132c8..213221d721 100644 --- a/server/aws-lsp-yaml/package.json +++ b/server/aws-lsp-yaml/package.json @@ -26,7 +26,7 @@ "postinstall": "node patchYamlPackage.js" }, "dependencies": { - "@aws/language-server-runtimes": "^0.2.127", + "@aws/language-server-runtimes": "^0.2.128", "@aws/lsp-core": "^0.0.14", "vscode-languageserver": "^9.0.1", "vscode-languageserver-textdocument": "^1.0.8", diff --git a/server/device-sso-auth-lsp/package.json b/server/device-sso-auth-lsp/package.json index e5085aa6bd..9bca2bf0b7 100644 --- a/server/device-sso-auth-lsp/package.json +++ b/server/device-sso-auth-lsp/package.json @@ -7,7 +7,7 @@ "compile": "tsc --build" }, "dependencies": { - "@aws/language-server-runtimes": "^0.2.127", + "@aws/language-server-runtimes": "^0.2.128", "vscode-languageserver": "^9.0.1" }, "devDependencies": { diff --git a/server/hello-world-lsp/package.json b/server/hello-world-lsp/package.json index 8ab207f8b4..b5944a3223 100644 --- a/server/hello-world-lsp/package.json +++ b/server/hello-world-lsp/package.json @@ -13,7 +13,7 @@ "coverage:report": "c8 report --reporter=html --reporter=text" }, "dependencies": { - "@aws/language-server-runtimes": "^0.2.127", + "@aws/language-server-runtimes": "^0.2.128", "vscode-languageserver": "^9.0.1" }, "devDependencies": {