diff --git a/.release-please-manifest.json b/.release-please-manifest.json index 9ae14a3a11..8c80b00b09 100644 --- a/.release-please-manifest.json +++ b/.release-please-manifest.json @@ -2,7 +2,7 @@ "chat-client": "0.1.51", "core/aws-lsp-core": "0.0.21", "server/aws-lsp-antlr4": "0.1.25", - "server/aws-lsp-codewhisperer": "0.0.112", + "server/aws-lsp-codewhisperer": "0.0.114", "server/aws-lsp-json": "0.1.26", "server/aws-lsp-partiql": "0.0.23", "server/aws-lsp-yaml": "0.1.26" 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 6f0038c9d7..8e6a8bbb8f 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:09b75b788854e2c2f08b9fa73c671e476f7e20b8284521f544ea7f2e2c82d3fa -size 96549602 +oid sha256:f59a63572dbadb648fe60741b41d929cbd2735a72312fedd07dc37bf9b9a78e8 +size 3080924 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 709c9d1052..8e6a8bbb8f 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:f13048f6989d01f8a5b8d9743ca2efa023cc4ae0c05efcd4fc0cb22f4b2dd5c3 -size 98233434 +oid sha256:f59a63572dbadb648fe60741b41d929cbd2735a72312fedd07dc37bf9b9a78e8 +size 3080924 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 d47ede8677..8e6a8bbb8f 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:6e119ae06538b7bfe7ce0050d88909c64989b10c481477e24bdd6ab9f6152846 -size 102483123 +oid sha256:f59a63572dbadb648fe60741b41d929cbd2735a72312fedd07dc37bf9b9a78e8 +size 3080924 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 5aeec68248..8e6a8bbb8f 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:8aea05af87c620a7be4cb58b4b9b1a579e5726b1eb3682e55c42302ff19d853d -size 114470426 +oid sha256:f59a63572dbadb648fe60741b41d929cbd2735a72312fedd07dc37bf9b9a78e8 +size 3080924 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 1d3937e552..8e6a8bbb8f 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:aafb3ef97fca6ba0369f7bfc48b5846e2b4f4fdec0014aae58be70f49cc42116 -size 113755807 +oid sha256:f59a63572dbadb648fe60741b41d929cbd2735a72312fedd07dc37bf9b9a78e8 +size 3080924 diff --git a/app/aws-lsp-codewhisperer-runtimes/src/version.json b/app/aws-lsp-codewhisperer-runtimes/src/version.json index 430db61483..388c9a8a55 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.64.0" + "agenticChat": "1.66.0" } diff --git a/chat-client/package.json b/chat-client/package.json index d6dcc0c496..15bb68c5e1 100644 --- a/chat-client/package.json +++ b/chat-client/package.json @@ -25,7 +25,7 @@ }, "dependencies": { "@aws/chat-client-ui-types": "0.1.68", - "@aws/language-server-runtimes": "^0.3.16", + "@aws/language-server-runtimes": "^0.3.17", "@aws/language-server-runtimes-types": "^0.1.64", "@aws/mynah-ui": "^4.40.1" }, diff --git a/package-lock.json b/package-lock.json index 5a47697e47..295b7915b0 100644 --- a/package-lock.json +++ b/package-lock.json @@ -255,7 +255,7 @@ "license": "Apache-2.0", "dependencies": { "@aws/chat-client-ui-types": "0.1.68", - "@aws/language-server-runtimes": "^0.3.16", + "@aws/language-server-runtimes": "^0.3.17", "@aws/language-server-runtimes-types": "^0.1.64", "@aws/mynah-ui": "^4.40.1" }, @@ -4506,9 +4506,9 @@ } }, "node_modules/@aws/language-server-runtimes": { - "version": "0.3.16", - "resolved": "https://registry.npmjs.org/@aws/language-server-runtimes/-/language-server-runtimes-0.3.16.tgz", - "integrity": "sha512-i5Rlnq1VUWpihGyd65o5gRqA8rxnkWZkx0WLsBCpuD9Lpztscwq2Si6f1dhhKK59905nG/xNE1xvRVAlXxc0IA==", + "version": "0.3.17", + "resolved": "https://registry.npmjs.org/@aws/language-server-runtimes/-/language-server-runtimes-0.3.17.tgz", + "integrity": "sha512-yA7A7o5YChUlOT0zip9vGQu2Q5+UnHW/39cn7LKsTH0VD8ZiB8I8/4SXUggV3as2Vy7nD447xJGVkqjYqlngRA==", "license": "Apache-2.0", "dependencies": { "@aws/language-server-runtimes-types": "^0.1.64", @@ -31102,7 +31102,7 @@ }, "server/aws-lsp-codewhisperer": { "name": "@aws/lsp-codewhisperer", - "version": "0.0.112", + "version": "0.0.114", "bundleDependencies": [ "@amzn/codewhisperer", "@amzn/codewhisperer-runtime", @@ -31122,7 +31122,7 @@ "@aws-sdk/util-arn-parser": "^3.723.0", "@aws-sdk/util-retry": "^3.374.0", "@aws/chat-client-ui-types": "0.1.68", - "@aws/language-server-runtimes": "^0.3.16", + "@aws/language-server-runtimes": "^0.3.17", "@aws/lsp-core": "^0.0.21", "@modelcontextprotocol/sdk": "^1.23.0", "@mozilla/readability": "^0.6.0", diff --git a/server/aws-lsp-codewhisperer/CHANGELOG.md b/server/aws-lsp-codewhisperer/CHANGELOG.md index 059db3379a..6a6b4e16f3 100644 --- a/server/aws-lsp-codewhisperer/CHANGELOG.md +++ b/server/aws-lsp-codewhisperer/CHANGELOG.md @@ -1,5 +1,26 @@ # Changelog +## [0.0.114](https://github.com/aws/language-servers/compare/lsp-codewhisperer/v0.0.113...lsp-codewhisperer/v0.0.114) (2026-05-05) + + +### Bug Fixes + +* **amazonq:** route inline chat through getChatResponse for correct API selection ([#2713](https://github.com/aws/language-servers/issues/2713)) ([#2714](https://github.com/aws/language-servers/issues/2714)) ([b6226e7](https://github.com/aws/language-servers/commit/b6226e758d52d8db85c844cab82e6b604566e2ef)) + +## [0.0.113](https://github.com/aws/language-servers/compare/lsp-codewhisperer/v0.0.112...lsp-codewhisperer/v0.0.113) (2026-04-29) + + +### Features + +* **amazonq:** add consent prompt for workspace-scoped MCP servers ([#2708](https://github.com/aws/language-servers/issues/2708)) ([7b8595a](https://github.com/aws/language-servers/commit/7b8595a4e638562f79d5f71dcf22b0c700490458)) + + +### Bug Fixes + +* **amazonq:** improve MCP consent gate reliability and cleanup ([#2711](https://github.com/aws/language-servers/issues/2711)) ([f5aa1a3](https://github.com/aws/language-servers/commit/f5aa1a3b25aa38bfe8dd0e830b5839e1cea1d410)) +* deprecate [@workspace](https://github.com/workspace) vector search + fix [@folder](https://github.com/folder) files not appearing in context ([#2698](https://github.com/aws/language-servers/issues/2698)) ([ae7d3fc](https://github.com/aws/language-servers/commit/ae7d3fcd26f57d6cc5d3d26dd5ec79983c4103df)) +* guard workspaceFolderManager null reference in updateConfiguration ([#2695](https://github.com/aws/language-servers/issues/2695)) ([dcd7829](https://github.com/aws/language-servers/commit/dcd78298766d09902ba51cb12547780f518d48a9)) + ## [0.0.112](https://github.com/aws/language-servers/compare/lsp-codewhisperer/v0.0.111...lsp-codewhisperer/v0.0.112) (2026-04-07) diff --git a/server/aws-lsp-codewhisperer/package.json b/server/aws-lsp-codewhisperer/package.json index 269aabbfa8..fcb88fd504 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.112", + "version": "0.0.114", "description": "CodeWhisperer Language Server", "main": "out/index.js", "repository": { @@ -38,7 +38,7 @@ "@aws-sdk/util-arn-parser": "^3.723.0", "@aws-sdk/util-retry": "^3.374.0", "@aws/chat-client-ui-types": "0.1.68", - "@aws/language-server-runtimes": "^0.3.16", + "@aws/language-server-runtimes": "^0.3.17", "@aws/lsp-core": "^0.0.21", "@modelcontextprotocol/sdk": "^1.23.0", "@mozilla/readability": "^0.6.0", 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 fe32f4402c..7f90c54fb6 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 @@ -257,11 +257,7 @@ describe('AgenticChatController', () => { } as any // Using 'as any' to prevent type errors when the Agent interface is updated with new methods additionalContextProviderStub = sinon.stub(AdditionalContextProvider.prototype, 'getAdditionalContext') - additionalContextProviderStub.callsFake(async (triggerContext, _, context: ContextCommand[]) => { - // When @workspace is in the context, set hasWorkspace flag - if (context && context.some(item => item.command === '@workspace')) { - triggerContext.hasWorkspace = true - } + additionalContextProviderStub.callsFake(async () => { return [] }) // @ts-ignore @@ -1415,60 +1411,6 @@ describe('AgenticChatController', () => { extractDocumentContextStub.restore() }) - it('parses relevant document and includes as requestInput if @workspace context is included', async () => { - const localProjectContextController = new LocalProjectContextController('client-name', [], logging) - const mockRelevantDocs = [ - { filePath: '/test/1.ts', content: 'text', id: 'id-1', index: 0, vec: [1] }, - { filePath: '/test/2.ts', content: 'text2', id: 'id-2', index: 0, vec: [1] }, - ] - - sinon.stub(LocalProjectContextController, 'getInstance').resolves(localProjectContextController) - sinon.stub(localProjectContextController, 'isIndexingEnabled').returns(true) - sinon.stub(localProjectContextController, 'queryVectorIndex').resolves(mockRelevantDocs) - - await chatController.onChatPrompt( - { - tabId: 'tab', - prompt: { - prompt: '@workspace help me understand this code', - escapedPrompt: '@workspace help me understand this code', - }, - context: [{ command: '@workspace' }], - }, - mockCancellationToken - ) - - const calledRequestInput: GenerateAssistantResponseCommandInput = - generateAssistantResponseStub.firstCall.firstArg - - assert.deepStrictEqual( - calledRequestInput.conversationState?.currentMessage?.userInputMessage?.userInputMessageContext - ?.editorState, - { - workspaceFolders: [], - relevantDocuments: [ - { - endLine: -1, - path: '/test/1.ts', - relativeFilePath: '1.ts', - startLine: -1, - text: 'text', - type: ContentType.WORKSPACE, - }, - { - endLine: -1, - path: '/test/2.ts', - relativeFilePath: '2.ts', - startLine: -1, - text: 'text2', - type: ContentType.WORKSPACE, - }, - ], - useRelevantDocuments: true, - } - ) - }) - it('leaves cursorState as undefined if cursorState is not passed', async () => { const documentContextObject = { programmingLanguage: 'typescript', @@ -2217,8 +2159,8 @@ describe('AgenticChatController', () => { assert.deepStrictEqual(chatResult, expectedCompleteInlineChatResult) }) - it('returns a ResponseError if sendMessage returns an error', async () => { - sendMessageStub.callsFake(() => { + it('returns a ResponseError if generateAssistantResponse returns an error', async () => { + generateAssistantResponseStub.callsFake(() => { throw new Error('Error') }) @@ -2230,8 +2172,8 @@ describe('AgenticChatController', () => { assert.ok(chatResult instanceof ResponseError) }) - it('returns a Response error if sendMessage returns an auth error', async () => { - sendMessageStub.callsFake(() => { + it('returns a Response error if generateAssistantResponse returns an auth error', async () => { + generateAssistantResponseStub.callsFake(() => { throw new Error('Error') }) @@ -2247,12 +2189,12 @@ describe('AgenticChatController', () => { }) it('returns a ResponseError if response streams return an error event', async () => { - sendMessageStub.callsFake(() => { + generateAssistantResponseStub.callsFake(() => { return Promise.resolve({ $metadata: { requestId: mockMessageId, }, - sendMessageResponse: createIterableResponse([ + generateAssistantResponseResponse: createIterableResponse([ // ["Hello ", "World"] ...mockChatResponseList.slice(1, 3), { error: { message: 'some error' } }, @@ -2271,12 +2213,12 @@ describe('AgenticChatController', () => { }) it('returns a ResponseError if response streams return an invalid state event', async () => { - sendMessageStub.callsFake(() => { + generateAssistantResponseStub.callsFake(() => { return Promise.resolve({ $metadata: { requestId: mockMessageId, }, - sendMessageResponse: createIterableResponse([ + generateAssistantResponseResponse: createIterableResponse([ // ["Hello ", "World"] ...mockChatResponseList.slice(1, 3), { invalidStateEvent: { message: 'invalid state' } }, @@ -2337,7 +2279,8 @@ describe('AgenticChatController', () => { mockCancellationToken ) - const calledRequestInput: SendMessageCommandInput = sendMessageStub.firstCall.firstArg + const calledRequestInput: GenerateAssistantResponseCommandInput = + generateAssistantResponseStub.firstCall.firstArg assert.strictEqual( calledRequestInput.conversationState?.currentMessage?.userInputMessage?.userInputMessageContext @@ -2362,7 +2305,8 @@ describe('AgenticChatController', () => { mockCancellationToken ) - const calledRequestInput: SendMessageCommandInput = sendMessageStub.firstCall.firstArg + const calledRequestInput: GenerateAssistantResponseCommandInput = + generateAssistantResponseStub.firstCall.firstArg assert.strictEqual( calledRequestInput.conversationState?.currentMessage?.userInputMessage?.userInputMessageContext @@ -2388,7 +2332,8 @@ describe('AgenticChatController', () => { mockCancellationToken ) - const calledRequestInput: SendMessageCommandInput = sendMessageStub.firstCall.firstArg + const calledRequestInput: GenerateAssistantResponseCommandInput = + generateAssistantResponseStub.firstCall.firstArg assert.deepStrictEqual( calledRequestInput.conversationState?.currentMessage?.userInputMessage?.userInputMessageContext 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 ebd415e5af..6a8846c3cb 100644 --- a/server/aws-lsp-codewhisperer/src/language-server/agenticChat/agenticChatController.ts +++ b/server/aws-lsp-codewhisperer/src/language-server/agenticChat/agenticChatController.ts @@ -37,12 +37,7 @@ import { SUFFIX_UNDOALL, SUFFIX_EXPLANATION, } from './constants/toolConstants' -import { - SendMessageCommandInput, - SendMessageCommandOutput, - ChatCommandInput, - ChatCommandOutput, -} from '../../shared/streamingClientService' +import { SendMessageCommandInput, ChatCommandInput, ChatCommandOutput } from '../../shared/streamingClientService' import { Button, Status, @@ -3537,7 +3532,6 @@ export class AgenticChatController implements ChatHandlers { if (triggerContext.contextInfo) { metric.mergeWith({ cwsprChatHasContextList: triggerContext.documentReference?.filePaths?.length ? true : false, - cwsprChatHasWorkspaceContext: triggerContext.hasWorkspace ?? false, cwsprChatFolderContextCount: triggerContext.contextInfo.contextCount.folderContextCount, cwsprChatFileContextCount: triggerContext.contextInfo.contextCount.fileContextCount, cwsprChatRuleContextCount: triggerContext.contextInfo.contextCount.activeRuleContextCount, @@ -3777,8 +3771,8 @@ export class AgenticChatController implements ChatHandlers { throw new Error('amazonQServiceManager is not initialized') } - const client = this.#serviceManager.getStreamingClient() - response = await client.sendMessage(requestInput as SendMessageCommandInput) + const session = new ChatSessionService(this.#serviceManager, this.#features.lsp, this.#features.logging) + response = await session.getChatResponse(requestInput) this.#log('Response for inline chat', JSON.stringify(response.$metadata), JSON.stringify(response)) } catch (err) { if (err instanceof AmazonQServicePendingSigninError || err instanceof AmazonQServicePendingProfileError) { @@ -4698,14 +4692,21 @@ export class AgenticChatController implements ChatHandlers { } async #processSendMessageResponseForInlineChat( - response: SendMessageCommandOutput, + response: ChatCommandOutput, metric: Metric, partialResultToken?: string | number ): Promise> { const requestId = response.$metadata.requestId! const chatEventParser = new ChatEventParser(requestId, metric) - for await (const chatEvent of response.sendMessageResponse!) { + let chatEventStream = undefined + if ('generateAssistantResponseResponse' in response) { + chatEventStream = response.generateAssistantResponseResponse + } else if ('sendMessageResponse' in response) { + chatEventStream = response.sendMessageResponse + } + + for await (const chatEvent of chatEventStream!) { const result = chatEventParser.processPartialEvent(chatEvent) // terminate early when there is an error 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 c841c994bc..3be50a289e 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 @@ -326,27 +326,6 @@ describe('AdditionalContextProvider', () => { assert.strictEqual(triggerContext.cursorState, undefined) }) - it('should set hasWorkspace flag when @workspace is present', async () => { - const mockWorkspaceFolder = { - uri: URI.file('/workspace').toString(), - name: 'test', - } - sinon.stub(workspaceUtils, 'getWorkspaceFolderPaths').returns(['/workspace']) - const triggerContext: TriggerContext = { - workspaceFolder: mockWorkspaceFolder, - } - - const workspaceContext = [{ id: '@workspace', command: 'Workspace', label: 'folder' }] - ;(chatHistoryDb.getPinnedContext as sinon.SinonStub).returns(workspaceContext) - - fsExistsStub.resolves(false) - getContextCommandPromptStub.resolves([]) - - await provider.getAdditionalContext(triggerContext, 'tab1') - - assert.strictEqual(triggerContext.hasWorkspace, true) - }) - it('should count context types correctly', async () => { const mockWorkspaceFolder = { uri: URI.file('/workspace').toString(), 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 95370fb402..60a880a956 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 @@ -436,9 +436,6 @@ export class AdditionalContextProvider { contextInfo = contextInfo.filter(item => item.id !== ACTIVE_EDITOR_CONTEXT_ID) } - if (contextInfo.some(item => item.id === '@workspace')) { - triggerContext.hasWorkspace = true - } // Handle code symbol ID mismatches between indexing sessions // When a workspace is re-indexed, code symbols receive new IDs // If a pinned symbol's ID is no longer found in the current index: @@ -609,8 +606,19 @@ export class AdditionalContextProvider { const image = imageMap.get(item.description) if (image) ordered.push(image) } else { - const doc = item.route ? docMap.get(path.join(...item.route)) : undefined - if (doc) ordered.push(doc) + const itemPath = item.route ? path.join(...item.route) : undefined + if (itemPath) { + const doc = docMap.get(itemPath) + if (doc) { + ordered.push(doc) + } else if (item.label === 'folder') { + // Folder expands into multiple file entries — match all children + const children = docEntries.filter( + entry => !entry.pinned && entry.path.startsWith(itemPath + path.sep) + ) + ordered.push(...children) + } + } } } // Append pinned context entries (docs and images) diff --git a/server/aws-lsp-codewhisperer/src/language-server/agenticChat/context/agenticChatTriggerContext.ts b/server/aws-lsp-codewhisperer/src/language-server/agenticChat/context/agenticChatTriggerContext.ts index 8d9c19c13a..3b9efadc94 100644 --- a/server/aws-lsp-codewhisperer/src/language-server/agenticChat/context/agenticChatTriggerContext.ts +++ b/server/aws-lsp-codewhisperer/src/language-server/agenticChat/context/agenticChatTriggerContext.ts @@ -14,6 +14,7 @@ import { EnvState, Origin, ImageBlock, + RelevantTextDocument, } from '@amzn/codewhisperer-streaming' import { BedrockTools, @@ -22,18 +23,15 @@ import { InlineChatParams, FileList, TextDocument, - OPEN_WORKSPACE_INDEX_SETTINGS_BUTTON_ID, } from '@aws/language-server-runtimes/server-interface' import { Features } from '../../types' import { DocumentContext, DocumentContextExtractor } from '../../chat/contexts/documentContext' import { workspaceUtils } from '@aws/lsp-core' import { URI } from 'vscode-uri' -import { LocalProjectContextController } from '../../../shared/localProjectContextController' import * as path from 'path' -import { RelevantTextDocument } from '@amzn/codewhisperer-streaming' import { languageByExtension } from '../../../shared/languageDetection' import { AgenticChatResultStream } from '../agenticChatResultStream' -import { ContextInfo, mergeFileLists, mergeRelevantTextDocuments } from './contextUtils' +import { ContextInfo } from './contextUtils' import { WorkspaceFolderManager } from '../../workspaceContext/workspaceFolderManager' import { getRelativePathWithWorkspaceFolder } from '../../workspaceContext/util' import { ChatCommandInput } from '../../../shared/streamingClientService' @@ -47,7 +45,6 @@ export interface TriggerContext extends Partial { * Represents the context transparency list displayed at the top of the assistant response. */ documentReference?: FileList - hasWorkspace?: boolean } export type LineInfo = { startLine: number; endLine: number } @@ -178,7 +175,6 @@ export class AgenticChatTriggerContext { const { prompt } = params const workspaceFolders = workspaceUtils.getWorkspaceFolderPaths(this.#workspace).slice(0, maxWorkspaceFolders) const defaultEditorState = { workspaceFolders } - const hasWorkspace = triggerContext.hasWorkspace // prompt.prompt is what user typed in the input, should be sent to backend // prompt.escapedPrompt is HTML serialized string, which should only be used for UI. @@ -190,10 +186,6 @@ export class AgenticChatTriggerContext { promptContent = promptContent.replace(/\*\*@sage\*\*/g, '@sage') } - if (hasWorkspace) { - promptContent = promptContent?.replace(/\*\*@workspace\*\*/, '') - } - // Append remote workspaceId if it exists // Only append workspaceId to GenerateCompletions when WebSocket client is connected const remoteWsFolderManager = WorkspaceFolderManager.getInstance() @@ -204,15 +196,7 @@ export class AgenticChatTriggerContext { undefined this.#logging.info(`remote workspaceId: ${workspaceId}`) - // Get workspace documents if @workspace is used - let relevantDocuments = hasWorkspace - ? await this.#getRelevantDocuments(promptContent ?? '', chatResultStream) - : [] - - const workspaceFileList = mergeRelevantTextDocuments(relevantDocuments) - triggerContext.documentReference = triggerContext.documentReference - ? mergeFileLists(triggerContext.documentReference, workspaceFileList) - : workspaceFileList + const relevantDocuments: RelevantTextDocumentAddition[] = [] // Add @context in prompt to relevantDocuments if (additionalContent) { for (const item of additionalContent.filter(item => !item.pinned)) { @@ -444,81 +428,4 @@ export class AgenticChatTriggerContext { return [...uris] } - - async #getRelevantDocuments( - prompt: string, - chatResultStream?: AgenticChatResultStream - ): Promise { - const localProjectContextController = await LocalProjectContextController.getInstance() - if (!localProjectContextController.isIndexingEnabled() && chatResultStream) { - await chatResultStream.writeResultBlock({ - body: `To add your workspace as context, enable local indexing in your IDE settings. After enabling, add @workspace to your question, and I'll generate a response using your workspace as context.`, - buttons: [ - { - id: OPEN_WORKSPACE_INDEX_SETTINGS_BUTTON_ID, - text: 'Open settings', - icon: 'external', - keepCardAfterClick: false, - status: 'info', - }, - ], - }) - return [] - } - - let relevantTextDocuments = await this.#queryRelevantDocuments(prompt, localProjectContextController) - relevantTextDocuments = relevantTextDocuments.filter(doc => doc.text && doc.text.length > 0) - for (const relevantDocument of relevantTextDocuments) { - if (relevantDocument.text && relevantDocument.text.length > workspaceChunkMaxSize) { - relevantDocument.text = relevantDocument.text.substring(0, workspaceChunkMaxSize) - this.#logging.debug(`Truncating @workspace chunk: ${relevantDocument.relativeFilePath} `) - } - } - - return relevantTextDocuments - } - - async #queryRelevantDocuments( - prompt: string, - localProjectContextController: LocalProjectContextController - ): Promise { - try { - const chunks = await localProjectContextController.queryVectorIndex({ query: prompt }) - const relevantTextDocuments: RelevantTextDocumentAddition[] = [] - if (!chunks) { - return relevantTextDocuments - } - - for (const chunk of chunks) { - const text = chunk.context ?? chunk.content - const baseDocument = { - text, - path: chunk.filePath, - relativeFilePath: chunk.relativePath ?? path.basename(chunk.filePath), - startLine: chunk.startLine ?? -1, - endLine: chunk.endLine ?? -1, - } - - if (chunk.programmingLanguage && chunk.programmingLanguage !== 'unknown') { - relevantTextDocuments.push({ - ...baseDocument, - programmingLanguage: { - languageName: chunk.programmingLanguage, - }, - type: ContentType.WORKSPACE, - }) - } else { - relevantTextDocuments.push({ - ...baseDocument, - type: ContentType.WORKSPACE, - }) - } - } - - return relevantTextDocuments - } catch (e) { - this.#logging.error(`Error querying query vector index to get relevant documents: ${e}`) - return [] - } - } } diff --git a/server/aws-lsp-codewhisperer/src/language-server/agenticChat/context/contextCommandsProvider.preservation.test.ts b/server/aws-lsp-codewhisperer/src/language-server/agenticChat/context/contextCommandsProvider.preservation.test.ts index 5fbc63b540..c37a3d7359 100644 --- a/server/aws-lsp-codewhisperer/src/language-server/agenticChat/context/contextCommandsProvider.preservation.test.ts +++ b/server/aws-lsp-codewhisperer/src/language-server/agenticChat/context/contextCommandsProvider.preservation.test.ts @@ -61,7 +61,6 @@ describe('Preservation: Context Commands Provider Small Payload Behavior', () => sinon.stub(LocalProjectContextController, 'getInstance').resolves({ onContextItemsUpdated: sinon.stub(), - onIndexingInProgressChanged: sinon.stub(), } as any) provider = new ContextCommandsProvider( diff --git a/server/aws-lsp-codewhisperer/src/language-server/agenticChat/context/contextCommandsProvider.test.ts b/server/aws-lsp-codewhisperer/src/language-server/agenticChat/context/contextCommandsProvider.test.ts index 811f3d4cf2..9d942ca453 100644 --- a/server/aws-lsp-codewhisperer/src/language-server/agenticChat/context/contextCommandsProvider.test.ts +++ b/server/aws-lsp-codewhisperer/src/language-server/agenticChat/context/contextCommandsProvider.test.ts @@ -1,4 +1,4 @@ -import { ContextCommandsProvider, CONTEXT_COMMAND_PAYLOAD_CAP, INDEXING_THROTTLE_MS } from './contextCommandsProvider' +import { ContextCommandsProvider, CONTEXT_COMMAND_PAYLOAD_CAP } from './contextCommandsProvider' import * as sinon from 'sinon' import * as fs from 'fs' import { TestFeatures } from '@aws/language-server-runtimes/testing' @@ -26,7 +26,6 @@ describe('ContextCommandsProvider', () => { sinon.stub(LocalProjectContextController, 'getInstance').resolves({ onContextItemsUpdated: sinon.stub(), - onIndexingInProgressChanged: sinon.stub(), } as any) provider = new ContextCommandsProvider( @@ -107,40 +106,6 @@ describe('ContextCommandsProvider', () => { }) }) - describe('onIndexingInProgressChanged', () => { - it('should update workspacePending and call processContextCommandUpdate after throttle window', async () => { - const clock = sinon.useFakeTimers() - let capturedCallback: ((indexingInProgress: boolean) => void) | undefined - - const mockController = { - onContextItemsUpdated: sinon.stub(), - set onIndexingInProgressChanged(callback: (indexingInProgress: boolean) => void) { - capturedCallback = callback - }, - } - - const processUpdateSpy = sinon.spy(provider, 'processContextCommandUpdate') - ;(LocalProjectContextController.getInstance as sinon.SinonStub).resolves(mockController as any) - - // Set initial state to false so condition is met - ;(provider as any).workspacePending = false - - await (provider as any).registerContextCommandHandler() - - capturedCallback?.(true) - - // Not called yet — still within throttle window - sinon.assert.notCalled(processUpdateSpy) - - // Advance past the throttle window - clock.tick(INDEXING_THROTTLE_MS) - - sinon.assert.calledWith(processUpdateSpy, []) - - clock.restore() - }) - }) - describe('setFilesAndFoldersFailed', () => { it('should set filesAndFoldersFailed to true and filesAndFoldersPending to false', () => { provider.setFilesAndFoldersFailed(true) diff --git a/server/aws-lsp-codewhisperer/src/language-server/agenticChat/context/contextCommandsProvider.ts b/server/aws-lsp-codewhisperer/src/language-server/agenticChat/context/contextCommandsProvider.ts index cc7c3d12f6..770262e60b 100644 --- a/server/aws-lsp-codewhisperer/src/language-server/agenticChat/context/contextCommandsProvider.ts +++ b/server/aws-lsp-codewhisperer/src/language-server/agenticChat/context/contextCommandsProvider.ts @@ -15,14 +15,6 @@ import { LocalProjectContextController } from '../../../shared/localProjectConte import { URI } from 'vscode-uri' import { activeFileCmd } from './additionalContextProvider' -/** - * Throttle window (in ms) for coalescing rapid `onIndexingInProgressChanged` - * callbacks. When indexing status toggles rapidly (e.g. true→false→true), - * only the final state triggers a `processContextCommandUpdate` call after - * this delay elapses with no further changes. - */ -export const INDEXING_THROTTLE_MS = 500 - /** * Maximum items in the initial `sendContextCommands` push. * The client shows these when the user presses `@` before typing. @@ -69,10 +61,7 @@ export class ContextCommandsProvider implements Disposable { private codeSymbolsFailed = false private filesAndFoldersPending = true private filesAndFoldersFailed = false - private workspacePending = true private initialStateSent = false - /** Handle for the pending indexing-change throttle timer */ - private indexingThrottleTimer?: ReturnType constructor( private readonly logging: Logging, private readonly chat: Chat, @@ -101,28 +90,6 @@ export class ContextCommandsProvider implements Disposable { controller.onContextItemsUpdated = async contextItems => { await this.processContextCommandUpdate(contextItems) } - controller.onIndexingInProgressChanged = (indexingInProgress: boolean) => { - if (this.workspacePending !== indexingInProgress) { - this.workspacePending = indexingInProgress - - // Coalesce rapid indexing status toggles: cancel any pending - // throttle timer and start a new one. Only the final state - // after the throttle window triggers processContextCommandUpdate. - if (this.indexingThrottleTimer !== undefined) { - clearTimeout(this.indexingThrottleTimer) - } - this.indexingThrottleTimer = setTimeout(async () => { - this.indexingThrottleTimer = undefined - try { - const items = await controller.getContextCommandItems() - await this.processContextCommandUpdate(items) - } catch (e) { - this.logging.error(`Error fetching context command items: ${e}`) - void this.processContextCommandUpdate([]) - } - }, INDEXING_THROTTLE_MS) - } - } } catch (e) { this.logging.warn(`Error processing context command update: ${e}`) } @@ -319,13 +286,7 @@ export class ContextCommandsProvider implements Disposable { placeholder: 'Select an image file', } - const workspaceCmd: ContextCommand = { - command: '@workspace', - id: '@workspace', - description: 'Reference all code in workspace', - disabledText: this.workspacePending ? 'pending' : undefined, - } - const commands = [workspaceCmd, folderCmdGroup, fileCmdGroup, codeCmdGroup, promptCmdGroup] + const commands = [folderCmdGroup, fileCmdGroup, codeCmdGroup, promptCmdGroup] if (imageContextEnabled) { commands.push(imageCmdGroup) @@ -402,9 +363,6 @@ export class ContextCommandsProvider implements Disposable { } dispose() { - if (this.indexingThrottleTimer !== undefined) { - clearTimeout(this.indexingThrottleTimer) - } void this.promptFileWatcher?.close() } } diff --git a/server/aws-lsp-codewhisperer/src/language-server/agenticChat/tools/codeSearch.test.ts b/server/aws-lsp-codewhisperer/src/language-server/agenticChat/tools/codeSearch.test.ts deleted file mode 100644 index 0488346778..0000000000 --- a/server/aws-lsp-codewhisperer/src/language-server/agenticChat/tools/codeSearch.test.ts +++ /dev/null @@ -1,171 +0,0 @@ -import * as assert from 'assert' -import { CodeSearch, CodeSearchOutput } from './codeSearch' -import { testFolder } from '@aws/lsp-core' -import * as path from 'path' -import * as fs from 'fs/promises' -import { TestFeatures } from '@aws/language-server-runtimes/testing' -import { Features } from '@aws/language-server-runtimes/server-interface/server' -import { LocalProjectContextController } from '../../../shared/localProjectContextController' -import { Chunk } from 'local-indexing' -import { stub, restore, SinonStub } from 'sinon' - -describe('CodeSearch Tool', () => { - let tempFolder: testFolder.TestFolder - let testFeatures: TestFeatures - let mockLocalProjectContextController: Partial - let getInstanceStub: SinonStub - - before(async () => { - testFeatures = new TestFeatures() - testFeatures.workspace.fs.exists = path => - fs.access(path).then( - () => true, - () => false - ) - tempFolder = await testFolder.TestFolder.create() - - mockLocalProjectContextController = { - isEnabled: true, - queryVectorIndex: stub().resolves([]), - } - - // Stub the getInstance method - getInstanceStub = stub(LocalProjectContextController, 'getInstance').resolves( - mockLocalProjectContextController as LocalProjectContextController - ) - }) - - after(async () => { - await tempFolder.delete() - restore() // Restore all stubbed methods - }) - - it('invalidates empty query', async () => { - const codeSearch = new CodeSearch(testFeatures) - await assert.rejects( - codeSearch.validate({ query: '' }), - /Code search query cannot be empty/i, - 'Expected an error about empty query' - ) - }) - - it('returns empty results when no matches found', async () => { - const codeSearch = new CodeSearch(testFeatures) - const result = await codeSearch.invoke({ query: 'nonexistent code' }) - - assert.strictEqual(result.output.kind, 'text') - assert.strictEqual(result.output.content, 'No code matches found for code search.') - }) - - it('returns formatted results when matches found', async () => { - // Create mock chunks that would be returned from vector search - const mockChunks: Chunk[] = [ - { - content: 'function testFunction() { return true; }', - filePath: path.join(tempFolder.path, 'test.js'), - relativePath: 'test.js', - startLine: 1, - endLine: 3, - programmingLanguage: 'javascript', - id: '', - index: 0, - vec: [], - }, - ] - - // Configure the mock to return our test chunks - ;(mockLocalProjectContextController.queryVectorIndex as SinonStub).resolves(mockChunks) - - const codeSearch = new CodeSearch(testFeatures) - const result = await codeSearch.invoke({ query: 'testFunction' }) - - assert.strictEqual(result.output.kind, 'json') - const content = result.output.content as CodeSearchOutput[] - assert.strictEqual(Array.isArray(content), true) - assert.strictEqual(content.length, 1) - assert.strictEqual(content[0].text, 'function testFunction() { return true; }') - assert.strictEqual(content[0].relativeFilePath, 'test.js') - assert.strictEqual(content[0].startLine, 1) - assert.strictEqual(content[0].endLine, 3) - assert.strictEqual(content[0].programmingLanguage?.languageName, 'javascript') - }) - - it('handles chunks without programming language', async () => { - // Create mock chunks without programming language - const mockChunks: Chunk[] = [ - { - content: 'Some plain text content', - filePath: path.join(tempFolder.path, 'readme.txt'), - relativePath: 'readme.txt', - startLine: 1, - endLine: 1, - id: '', - index: 0, - vec: [], - }, - ] - - // Configure the mock to return our test chunks - ;(mockLocalProjectContextController.queryVectorIndex as SinonStub).resolves(mockChunks) - - const codeSearch = new CodeSearch(testFeatures) - const result = await codeSearch.invoke({ query: 'plain text' }) - - assert.strictEqual(result.output.kind, 'json') - const content = result.output.content as CodeSearchOutput[] - assert.strictEqual(content.length, 1) - assert.strictEqual(content[0].text, 'Some plain text content') - assert.strictEqual(content[0].relativeFilePath, 'readme.txt') - assert.strictEqual(content[0].programmingLanguage, undefined) - }) - - it('uses default workspace folder when path not provided', async () => { - const codeSearch = new CodeSearch(testFeatures) - await codeSearch.invoke({ query: 'test query' }) - - // Verify that queryVectorIndex was called - assert.strictEqual((mockLocalProjectContextController.queryVectorIndex as SinonStub).called, true) - }) - - it('handles errors from LocalProjectContextController', async () => { - // Configure the mock to throw an error - ;(mockLocalProjectContextController.queryVectorIndex as SinonStub).rejects(new Error('Test error')) - - const codeSearch = new CodeSearch(testFeatures) - await assert.rejects( - codeSearch.invoke({ query: 'error test' }), - /Failed to perform code search/, - 'Expected an error when vector search fails' - ) - }) - - it('provides correct queue description', async () => { - const codeSearch = new CodeSearch(testFeatures) - - // Create a mock WritableStream - let capturedDescription = '' - const mockWriter = { - write: async (content: string) => { - capturedDescription = content - return Promise.resolve() - }, - close: async () => Promise.resolve(), - releaseLock: () => {}, - } - const mockStream = { - getWriter: () => mockWriter, - } as unknown as WritableStream - - await codeSearch.queueDescription({ query: 'test query' }, mockStream, true) - assert.strictEqual(capturedDescription, 'Performing code search for "test query" in ') - }) - - it('returns correct tool specification', () => { - const codeSearch = new CodeSearch(testFeatures) - const spec = codeSearch.getSpec() - - assert.strictEqual(spec.name, 'codeSearch') - assert.ok(spec.description.includes('Find snippets of code')) - assert.deepStrictEqual(spec.inputSchema.required, ['query']) - }) -}) diff --git a/server/aws-lsp-codewhisperer/src/language-server/agenticChat/tools/codeSearch.ts b/server/aws-lsp-codewhisperer/src/language-server/agenticChat/tools/codeSearch.ts deleted file mode 100644 index 167175e71b..0000000000 --- a/server/aws-lsp-codewhisperer/src/language-server/agenticChat/tools/codeSearch.ts +++ /dev/null @@ -1,194 +0,0 @@ -import { CommandValidation, InvokeOutput, requiresPathAcceptance, validatePath } from './toolShared' -import { Features } from '@aws/language-server-runtimes/server-interface/server' -import { getWorkspaceFolderPaths } from '@aws/lsp-core/out/util/workspaceUtils' -import { LocalProjectContextController } from '../../../shared/localProjectContextController' -import { Chunk } from 'local-indexing' -import { RelevantTextDocument } from '@amzn/codewhisperer-streaming' -import { LineInfo } from '../context/agenticChatTriggerContext' -import path = require('path') - -export interface CodeSearchParams { - query: string -} - -export type CodeSearchOutput = RelevantTextDocument & LineInfo - -export class CodeSearch { - private readonly logging: Features['logging'] - private readonly workspace: Features['workspace'] - private readonly lsp: Features['lsp'] - constructor(features: Pick) { - this.logging = features.logging - this.workspace = features.workspace - this.lsp = features.lsp - } - - public async validate(params: CodeSearchParams): Promise { - if (!params.query || params.query.trim().length === 0) { - throw new Error('Code search query cannot be empty.') - } - const searchPath = this.getOrSetSearchPath() - - if (searchPath) { - await validatePath(searchPath, this.workspace.fs.exists) - } - } - - public async queueDescription(params: CodeSearchParams, updates: WritableStream, requiresAcceptance: boolean) { - const writer = updates.getWriter() - const closeWriter = async (w: WritableStreamDefaultWriter) => { - await w.close() - w.releaseLock() - } - if (!requiresAcceptance) { - await writer.write('') - await closeWriter(writer) - return - } - - const path = this.getOrSetSearchPath() - await writer.write(`Performing code search for "${params.query}" in ${path}`) - await closeWriter(writer) - } - - public async invoke(params: CodeSearchParams): Promise { - const path = this.getOrSetSearchPath() - - try { - const results = await this.executeCodeSearch(params.query) - return this.createOutput( - !results || results.length === 0 ? 'No code matches found for code search.' : results - ) - } catch (error: any) { - this.logging.error( - `Failed to perform code search for "${params.query}" in workspace "${path}": ${error.message || error}` - ) - throw new Error( - `Failed to perform code search for "${params.query}" in workspace"${path}": ${error.message || error}` - ) - } - } - - private getOrSetSearchPath(path?: string): string { - let searchPath = '' - if (path && path.trim().length !== 0) { - searchPath = path - } else { - // Handle optional path parameter - // Use current workspace folder as default if path is not provided - const workspaceFolders = getWorkspaceFolderPaths(this.workspace) - if (workspaceFolders && workspaceFolders.length !== 0) { - this.logging.debug(`Using default workspace folder: ${workspaceFolders[0]}`) - searchPath = workspaceFolders[0] - } - } - return searchPath - } - - private async executeCodeSearch(query: string): Promise { - this.logging.info(`Executing code search for "${query}" in "${path}"`) - const localProjectContextController = await LocalProjectContextController.getInstance() - - if (!localProjectContextController.isEnabled) { - this.logging.warn(`Error during code search: local project context controller is disabled`) - throw new Error(`Error during code search: Amazon Q Workspace Index disabled, - please update the configuration to enable Amazon Q workspace Index`) - } - try { - // TODO: we need to handle the validation of workspace indexing status once localProjectContextController support - // check the indexing status. - // Use the localProjectContextController to query the vector index - const searchResults = await localProjectContextController.queryVectorIndex({ query: query }) - const sanitizedSearchResults = this.parseChunksToCodeSearchOutput(searchResults) - this.logging.info(`Code searched succeed with num of results: "${sanitizedSearchResults.length}"`) - return sanitizedSearchResults - } catch (error: any) { - this.logging.error(`Error during code search: ${error.message || error}`) - throw error - } - } - - /** - * Parses chunks from vector index search into CodeSearchOutput format - * Based on the queryRelevantDocuments method pattern - */ - private parseChunksToCodeSearchOutput(chunks: Chunk[]): CodeSearchOutput[] { - const codeSearchResults: CodeSearchOutput[] = [] - if (!chunks) { - return codeSearchResults - } - - for (const chunk of chunks) { - // Extract content and context - const text = chunk.content || '' - const relativeFilePath = chunk.relativePath ?? path.basename(chunk.filePath) - - // Extract line information - const startLine = chunk.startLine ?? -1 - const endLine = chunk.endLine ?? -1 - - // Create the base search result - const baseSearchResult = { - text, - relativeFilePath, - startLine, - endLine, - } - - // Add programming language information if available - if (chunk.programmingLanguage && chunk.programmingLanguage !== 'unknown') { - codeSearchResults.push({ - ...baseSearchResult, - programmingLanguage: { - languageName: chunk.programmingLanguage, - }, - }) - } else { - codeSearchResults.push(baseSearchResult) - } - } - - return codeSearchResults - } - - private createOutput(content: string | any[]): InvokeOutput { - if (typeof content === 'string') { - return { - output: { - kind: 'text', - content: content, - }, - } - } else { - return { - output: { - kind: 'json', - content: content, - }, - } - } - } - - public getSpec() { - return { - name: 'codeSearch', - description: - "Find snippets of code from the codebase most relevant to the search query.\nThis is a semantic search tool, so the query should ask for something semantically matching what is needed.\nUnless there is a clear reason to use your own search query, please just reuse the user's exact query with their wording.\nTheir exact wording/phrasing can often be helpful for the semantic search query. Keeping the same exact question format can also be helpful.", - inputSchema: { - type: 'object', - properties: { - query: { - type: 'string', - description: 'The search query to find relevant code.', - }, - explanation: { - type: 'string', - description: - 'One sentence explanation as to why this tool is being used, and how it contributes to the goal', - }, - }, - required: ['query'], - }, - } as const - } -} diff --git a/server/aws-lsp-codewhisperer/src/language-server/agenticChat/tools/mcp/mcpConsentStore.test.ts b/server/aws-lsp-codewhisperer/src/language-server/agenticChat/tools/mcp/mcpConsentStore.test.ts new file mode 100644 index 0000000000..bf3fdb47c9 --- /dev/null +++ b/server/aws-lsp-codewhisperer/src/language-server/agenticChat/tools/mcp/mcpConsentStore.test.ts @@ -0,0 +1,181 @@ +/*! + * Copyright Amazon.com, Inc. or its affiliates. + * All Rights Reserved. SPDX-License-Identifier: Apache-2.0 + */ + +import { expect } from 'chai' +import * as fs from 'fs' +import * as os from 'os' +import * as path from 'path' +import { + fingerprintServerConfig, + fingerprintWorkspace, + hasApproval, + recordApproval, + removeApproval, +} from './mcpConsentStore' +import type { MCPServerConfig } from './mcpTypes' + +describe('mcpConsentStore', () => { + let tmpHome: string + let workspace: any + let logger: any + + beforeEach(() => { + tmpHome = fs.mkdtempSync(path.join(os.tmpdir(), 'mcpConsentTest-')) + 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: (p: string, _opts: any) => Promise.resolve(fs.mkdirSync(p, { recursive: true })), + getUserHomeDir: () => tmpHome, + }, + } + logger = { warn: () => {}, info: () => {}, error: () => {} } + }) + + afterEach(() => { + fs.rmSync(tmpHome, { recursive: true, force: true }) + }) + + describe('fingerprintServerConfig', () => { + it('is deterministic for identical config', () => { + const cfg: MCPServerConfig = { command: 'sh', args: ['-c', 'echo hi'] } + expect(fingerprintServerConfig(cfg)).to.equal(fingerprintServerConfig({ ...cfg })) + }) + + it('differs when command changes', () => { + const a: MCPServerConfig = { command: 'sh', args: ['-c', 'echo hi'] } + const b: MCPServerConfig = { command: 'bash', args: ['-c', 'echo hi'] } + expect(fingerprintServerConfig(a)).to.not.equal(fingerprintServerConfig(b)) + }) + + it('differs when args change', () => { + const a: MCPServerConfig = { command: 'sh', args: ['-c', 'echo hi'] } + const b: MCPServerConfig = { command: 'sh', args: ['-c', 'echo bye'] } + expect(fingerprintServerConfig(a)).to.not.equal(fingerprintServerConfig(b)) + }) + + it('differs when env changes', () => { + const a: MCPServerConfig = { command: 'sh', args: [], env: { FOO: '1' } } + const b: MCPServerConfig = { command: 'sh', args: [], env: { FOO: '2' } } + expect(fingerprintServerConfig(a)).to.not.equal(fingerprintServerConfig(b)) + }) + + it('is stable regardless of env key order', () => { + const a: MCPServerConfig = { command: 'sh', args: [], env: { A: '1', B: '2' } } + const b: MCPServerConfig = { command: 'sh', args: [], env: { B: '2', A: '1' } } + expect(fingerprintServerConfig(a)).to.equal(fingerprintServerConfig(b)) + }) + + it('differs when url changes', () => { + const a: MCPServerConfig = { url: 'https://a.example' } + const b: MCPServerConfig = { url: 'https://b.example' } + expect(fingerprintServerConfig(a)).to.not.equal(fingerprintServerConfig(b)) + }) + }) + + describe('fingerprintWorkspace', () => { + it('is keyed on the directory of the config, not the filename', () => { + const a = fingerprintWorkspace('/foo/bar/.amazonq/mcp.json') + const b = fingerprintWorkspace('/foo/bar/.amazonq/agents/default.json') + // both live under /foo/bar/.amazonq's parent-dir once; path.dirname differs though + expect(a).to.not.equal(b) + }) + + it('is deterministic for the same path', () => { + const p = '/foo/bar/.amazonq/mcp.json' + expect(fingerprintWorkspace(p)).to.equal(fingerprintWorkspace(p)) + }) + }) + + describe('hasApproval / recordApproval', () => { + const cfg: MCPServerConfig = { command: 'sh', args: ['-c', 'echo ok'] } + const configPath = '/tmp/ws-a/.amazonq/mcp.json' + + it('returns false when store is empty', async () => { + expect(await hasApproval(workspace, logger, 'poc', cfg, configPath)).to.be.false + }) + + it('records and finds an approval for same (name, config, workspace)', async () => { + await recordApproval(workspace, logger, 'poc', cfg, configPath) + expect(await hasApproval(workspace, logger, 'poc', cfg, configPath)).to.be.true + }) + + it('matches via fingerprint even when workspace path differs', async () => { + await recordApproval(workspace, logger, 'poc', cfg, '/tmp/ws-a/.amazonq/mcp.json') + expect(await hasApproval(workspace, logger, 'poc', cfg, '/tmp/ws-b/.amazonq/mcp.json')).to.be.true + }) + + it('matches via workspaceHash even when fingerprint differs', async () => { + await recordApproval(workspace, logger, 'poc', cfg, configPath) + const mutated: MCPServerConfig = { command: 'sh', args: ['-c', 'echo different'] } + // Same workspace, different fingerprint — should still match via workspaceHash fallback + expect(await hasApproval(workspace, logger, 'poc', mutated, configPath)).to.be.true + }) + + it('does not match when both fingerprint and workspace differ', async () => { + await recordApproval(workspace, logger, 'poc', cfg, configPath) + const mutated: MCPServerConfig = { command: 'sh', args: ['-c', 'curl evil'] } + // Different fingerprint AND different workspace — no match + expect(await hasApproval(workspace, logger, 'poc', mutated, '/tmp/ws-other/.amazonq/mcp.json')).to.be.false + }) + + it('does not match when server name differs', async () => { + await recordApproval(workspace, logger, 'poc', cfg, configPath) + expect(await hasApproval(workspace, logger, 'other', cfg, configPath)).to.be.false + }) + + it('dedupes repeated approvals for the same key', async () => { + await recordApproval(workspace, logger, 'poc', cfg, configPath) + await recordApproval(workspace, logger, 'poc', cfg, configPath) + const stored = JSON.parse( + fs.readFileSync(path.join(tmpHome, '.aws', 'amazonq', 'mcp-approvals.json')).toString() + ) + expect(stored.approvals).to.have.lengthOf(1) + }) + + it('evicts stale entry when config changes for same server and workspace', async () => { + await recordApproval(workspace, logger, 'poc', cfg, configPath) + const mutated: MCPServerConfig = { command: 'sh', args: ['-c', 'echo changed'] } + await recordApproval(workspace, logger, 'poc', mutated, configPath) + const stored = JSON.parse( + fs.readFileSync(path.join(tmpHome, '.aws', 'amazonq', 'mcp-approvals.json')).toString() + ) + // Should have exactly 1 entry — the old fingerprint was evicted + expect(stored.approvals).to.have.lengthOf(1) + expect(stored.approvals[0].fingerprint).to.equal(fingerprintServerConfig(mutated)) + }) + + it('ignores a store with unrecognized version', async () => { + const storeDir = path.join(tmpHome, '.aws', 'amazonq') + fs.mkdirSync(storeDir, { recursive: true }) + fs.writeFileSync(path.join(storeDir, 'mcp-approvals.json'), JSON.stringify({ version: 999, approvals: [] })) + // record should still work (overwrites with v1) + await recordApproval(workspace, logger, 'poc', cfg, configPath) + expect(await hasApproval(workspace, logger, 'poc', cfg, configPath)).to.be.true + }) + + it('treats a malformed store as empty', async () => { + const storeDir = path.join(tmpHome, '.aws', 'amazonq') + fs.mkdirSync(storeDir, { recursive: true }) + fs.writeFileSync(path.join(storeDir, 'mcp-approvals.json'), 'not json') + expect(await hasApproval(workspace, logger, 'poc', cfg, configPath)).to.be.false + }) + + it('removeApproval clears a previously recorded approval', async () => { + await recordApproval(workspace, logger, 'poc', cfg, configPath) + expect(await hasApproval(workspace, logger, 'poc', cfg, configPath)).to.be.true + await removeApproval(workspace, logger, 'poc', configPath) + expect(await hasApproval(workspace, logger, 'poc', cfg, configPath)).to.be.false + }) + + it('removeApproval is a no-op when no matching server name exists', async () => { + await recordApproval(workspace, logger, 'poc', cfg, configPath) + await removeApproval(workspace, logger, 'other', configPath) + // Original approval should still be there + expect(await hasApproval(workspace, logger, 'poc', cfg, configPath)).to.be.true + }) + }) +}) diff --git a/server/aws-lsp-codewhisperer/src/language-server/agenticChat/tools/mcp/mcpConsentStore.ts b/server/aws-lsp-codewhisperer/src/language-server/agenticChat/tools/mcp/mcpConsentStore.ts new file mode 100644 index 0000000000..349cd32114 --- /dev/null +++ b/server/aws-lsp-codewhisperer/src/language-server/agenticChat/tools/mcp/mcpConsentStore.ts @@ -0,0 +1,135 @@ +/** + * Copyright Amazon.com, Inc. or its affiliates. + * All Rights Reserved. SPDX-License-Identifier: Apache-2.0 + */ + +import { createHash } from 'crypto' +import * as path from 'path' +import type { Workspace, Logging } from '@aws/language-server-runtimes/server-interface' +import type { MCPServerConfig } from './mcpTypes' + +const APPROVALS_FILE = 'mcp-approvals.json' +const STORE_VERSION = 1 + +interface Approval { + serverName: string + fingerprint: string + workspaceHash: string + approvedAt: string +} + +interface ApprovalStore { + version: number + approvals: Approval[] +} + +/** + * SHA-256 of a canonical JSON form of the server's execution-relevant fields. + * Any change to command/args/env/url yields a new fingerprint, invalidating + * prior approvals — so mutation of the config re-prompts. + */ +export function fingerprintServerConfig(cfg: MCPServerConfig): string { + const canonical = { + command: cfg.command ?? null, + args: cfg.args ?? [], + env: cfg.env ? Object.fromEntries(Object.entries(cfg.env).sort(([a], [b]) => a.localeCompare(b))) : {}, + url: cfg.url ?? null, + } + return 'sha256:' + createHash('sha256').update(JSON.stringify(canonical)).digest('hex') +} + +/** Hash of the workspace path so approval is scoped to (workspace, config). + * Normalizes the path to forward slashes for cross-platform consistency. */ +export function fingerprintWorkspace(configPath: string): string { + const normalized = path.resolve(path.dirname(configPath)).replace(/\\/g, '/') + return 'sha256:' + createHash('sha256').update(normalized).digest('hex') +} + +function getStorePath(workspace: Workspace): string { + return path.join(workspace.fs.getUserHomeDir(), '.aws', 'amazonq', APPROVALS_FILE) +} + +async function readStore(workspace: Workspace, logging: Logging): Promise { + const file = getStorePath(workspace) + try { + if (!(await workspace.fs.exists(file))) { + return { version: STORE_VERSION, approvals: [] } + } + const raw = (await workspace.fs.readFile(file)).toString() + const parsed = JSON.parse(raw) as ApprovalStore + if (parsed?.version !== STORE_VERSION || !Array.isArray(parsed.approvals)) { + logging.warn(`MCP consent store: unrecognized format at ${file}, treating as empty`) + return { version: STORE_VERSION, approvals: [] } + } + return parsed + } catch (e: any) { + logging.warn(`MCP consent store: failed to read ${file}: ${e?.message}`) + return { version: STORE_VERSION, approvals: [] } + } +} + +async function writeStore(workspace: Workspace, logging: Logging, store: ApprovalStore): Promise { + const file = getStorePath(workspace) + try { + await workspace.fs.mkdir(path.dirname(file), { recursive: true }) + await workspace.fs.writeFile(file, JSON.stringify(store, null, 2)) + } catch (e: any) { + logging.warn(`MCP consent store: failed to write ${file}: ${e?.message}`) + } +} + +export async function hasApproval( + workspace: Workspace, + logging: Logging, + serverName: string, + cfg: MCPServerConfig, + configPath: string +): Promise { + const store = await readStore(workspace, logging) + const fp = fingerprintServerConfig(cfg) + const wh = fingerprintWorkspace(configPath) + // Primary match: (serverName, fingerprint) — the fingerprint captures the full + // execution-relevant config (command/args/env/url). This works even if the + // workspaceHash varies between reloads due to configPath format differences. + // Fallback match: (serverName, workspaceHash) — covers cases where the + // fingerprint changes slightly between reloads (e.g., config migration adds + // default values) but the workspace is the same. + return store.approvals.some(a => a.serverName === serverName && (a.fingerprint === fp || a.workspaceHash === wh)) +} + +export async function recordApproval( + workspace: Workspace, + logging: Logging, + serverName: string, + cfg: MCPServerConfig, + configPath: string +): Promise { + const store = await readStore(workspace, logging) + const fp = fingerprintServerConfig(cfg) + const wh = fingerprintWorkspace(configPath) + // Replace any prior approval for the same (server, workspace) — this evicts + // stale entries when the config changes (fingerprint differs). + store.approvals = store.approvals.filter(a => !(a.serverName === serverName && a.workspaceHash === wh)) + store.approvals.push({ + serverName, + fingerprint: fp, + workspaceHash: wh, + approvedAt: new Date().toISOString(), + }) + await writeStore(workspace, logging, store) +} + +export async function removeApproval( + workspace: Workspace, + logging: Logging, + serverName: string, + configPath: string +): Promise { + const store = await readStore(workspace, logging) + const before = store.approvals.length + store.approvals = store.approvals.filter(a => a.serverName !== serverName) + if (store.approvals.length < before) { + await writeStore(workspace, logging, store) + logging.info(`MCP consent store: removed approval for '${serverName}'`) + } +} 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 170abcc3db..0667d149cc 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 @@ -1936,3 +1936,150 @@ describe('addRegistryServer with additional headers/env', () => { expect(agentCfg.env).to.be.undefined }) }) + +describe('consent gate for workspace-scoped MCP servers (P417451767)', () => { + const fakeHome = '/home/testuser' + const globalMcp = mcpUtils.getGlobalMcpConfigPath(fakeHome) + const workspaceMcp = '/tmp/ws-a/.amazonq/mcp.json' + + let showMessageStub: sinon.SinonStub + let hasApprovalStub: sinon.SinonStub + let recordApprovalStub: sinon.SinonStub + let setStateSpy: sinon.SinonSpy + + async function buildMgr(): Promise { + const consentStore = require('./mcpConsentStore') + hasApprovalStub = sinon.stub(consentStore, 'hasApproval').resolves(false) + recordApprovalStub = sinon.stub(consentStore, 'recordApproval').resolves() + + showMessageStub = sinon.stub() + const featuresWithPrompt = { + ...features, + workspace: { + ...fakeWorkspace, + fs: { ...fakeWorkspace.fs, getUserHomeDir: () => fakeHome }, + }, + lsp: { window: { showMessageRequest: showMessageStub } }, + } + sinon.stub(mcpUtils, 'loadAgentConfig').resolves({ + servers: new Map(), + serverNameMapping: new Map(), + errors: new Map(), + agentConfig: { + name: 'test', + description: '', + mcpServers: {}, + tools: [], + allowedTools: [], + toolsSettings: {}, + includedFiles: [], + resources: [], + }, + }) + const mgr = await McpManager.init([], featuresWithPrompt as any) + setStateSpy = sinon.spy(mgr as any, 'setState') + return mgr + } + + afterEach(async () => { + sinon.restore() + try { + await McpManager.instance.close() + } catch {} + }) + + it('does not prompt for global-scoped config', async () => { + const mgr = await buildMgr() + const cfg: MCPServerConfig = { command: 'sh', args: [], __configPath__: globalMcp } + // Fail fast after gate (cleanupExistingServer is safe to call on unknown server) + try { + await (mgr as any).initOneServerInternal('svc', cfg) + } catch {} + expect(showMessageStub.called).to.be.false + }) + + it('does not prompt for global agent config path', async () => { + const mgr = await buildMgr() + const globalAgent = mcpUtils.getGlobalAgentConfigPath(fakeHome) + const cfg: MCPServerConfig = { command: 'sh', args: [], __configPath__: globalAgent } + try { + await (mgr as any).initOneServerInternal('svc', cfg) + } catch {} + expect(showMessageStub.called).to.be.false + }) + + it('does not prompt for global persona config path', async () => { + const mgr = await buildMgr() + const globalPersona = mcpUtils.getGlobalPersonaConfigPath(fakeHome) + const cfg: MCPServerConfig = { command: 'sh', args: [], __configPath__: globalPersona } + try { + await (mgr as any).initOneServerInternal('svc', cfg) + } catch {} + expect(showMessageStub.called).to.be.false + }) + + it('prompts for workspace-scoped config when no prior approval', async () => { + const mgr = await buildMgr() + showMessageStub.resolves({ title: 'Deny' }) + const cfg: MCPServerConfig = { command: 'sh', args: ['-c', 'x'], __configPath__: workspaceMcp } + try { + await (mgr as any).initOneServerInternal('svc', cfg) + } catch {} + expect(showMessageStub.calledOnce).to.be.true + }) + + it('denial sets DISABLED state and caches the decision', async () => { + const mgr = await buildMgr() + showMessageStub.resolves({ title: 'Deny' }) + const cfg: MCPServerConfig = { command: 'sh', args: ['-c', 'x'], __configPath__: workspaceMcp } + try { + await (mgr as any).initOneServerInternal('svc', cfg) + } catch {} + expect(setStateSpy.calledWith('svc', McpServerStatus.DISABLED, 0, 'consent not granted')).to.be.true + + // Second call with same cfg should not re-prompt + showMessageStub.resetHistory() + try { + await (mgr as any).initOneServerInternal('svc', cfg) + } catch {} + expect(showMessageStub.called).to.be.false + }) + + it('mutation of args invalidates session denial (fingerprint change)', async () => { + const mgr = await buildMgr() + showMessageStub.resolves({ title: 'Deny' }) + const cfg1: MCPServerConfig = { command: 'sh', args: ['-c', 'x'], __configPath__: workspaceMcp } + try { + await (mgr as any).initOneServerInternal('svc', cfg1) + } catch {} + expect(showMessageStub.calledOnce).to.be.true + + // Mutate args — fingerprint changes, denial cache key differs, prompt should fire again + showMessageStub.resetHistory() + const cfg2: MCPServerConfig = { command: 'sh', args: ['-c', 'y'], __configPath__: workspaceMcp } + try { + await (mgr as any).initOneServerInternal('svc', cfg2) + } catch {} + expect(showMessageStub.calledOnce).to.be.true + }) + + it('prior approval short-circuits prompt', async () => { + const mgr = await buildMgr() + hasApprovalStub.resolves(true) + const cfg: MCPServerConfig = { command: 'sh', args: [], __configPath__: workspaceMcp } + try { + await (mgr as any).initOneServerInternal('svc', cfg) + } catch {} + expect(showMessageStub.called).to.be.false + }) + + it('allow records approval', async () => { + const mgr = await buildMgr() + showMessageStub.resolves({ title: 'Allow for this server' }) + const cfg: MCPServerConfig = { command: 'sh', args: ['-c', 'x'], __configPath__: workspaceMcp } + try { + await (mgr as any).initOneServerInternal('svc', cfg) + } catch {} + expect(recordApprovalStub.calledOnce).to.be.true + }) +}) 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 f5ab29d946..61e7ea75fe 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 @@ -33,12 +33,16 @@ import { getGlobalAgentConfigPath, getWorkspaceMcpConfigPaths, getGlobalMcpConfigPath, + getGlobalPersonaConfigPath, + normalizePathFromUri, } from './mcpUtils' import { AgenticChatError } from '../../errors' import { EventEmitter } from 'events' import { Mutex } from 'async-mutex' import path = require('path') import { URI } from 'vscode-uri' +import { MessageType } from '@aws/language-server-runtimes/protocol' +import { hasApproval, recordApproval, removeApproval, fingerprintServerConfig } from './mcpConsentStore' import { sanitizeInput } from '../../../../shared/utils' import { ProfileStatusMonitor } from './profileStatusMonitor' import { OAuthClient } from './mcpOauthClient' @@ -78,6 +82,7 @@ export class McpManager { private currentRegistry: McpRegistryData | null = null private registryUrlProvided: boolean = false private isPeriodicSync: boolean = false + private sessionDeniedConsent = new Set() private constructor( private agentPaths: string[], @@ -408,6 +413,69 @@ export class McpManager { ): Promise { const DEFAULT_SERVER_INIT_TIMEOUT_MS = 120_000 + // Consent gate for workspace-scoped MCP configs (P417451767). + // Workspace-scoped configs live in a folder the user opened and may be attacker-controlled. + // Global configs (~/.aws/amazonq/...) are user-authored and trusted implicitly. + const home = this.features.workspace.fs.getUserHomeDir() + const configPath = cfg.__configPath__ + ? normalizePathFromUri(cfg.__configPath__, this.features.logging) + : undefined + const globalMcp = getGlobalMcpConfigPath(home) + const globalAgent = getGlobalAgentConfigPath(home) + const globalPersona = getGlobalPersonaConfigPath(home) + const isWorkspaceScoped = + !!configPath && configPath !== globalMcp && configPath !== globalAgent && configPath !== globalPersona + if (isWorkspaceScoped && configPath) { + const denyKey = `${serverName}|${configPath}|${fingerprintServerConfig(cfg)}` + if (this.sessionDeniedConsent.has(denyKey)) { + this.setState(serverName, McpServerStatus.DISABLED, 0, 'consent not granted') + return + } + const approved = await hasApproval( + this.features.workspace, + this.features.logging, + serverName, + cfg, + configPath + ) + if (!approved) { + const cmdLine = [cfg.command ?? cfg.url ?? '(none)', ...(cfg.args ?? [])].join(' ').slice(0, 200) + const allowBtn = { title: 'Allow for this server' } + const denyBtn = { title: 'Deny' } + let choice: { title: string } | null | undefined + try { + choice = await this.features.lsp.window.showMessageRequest({ + type: MessageType.Warning, + message: + `Amazon Q — Untrusted MCP Server\n\n` + + `A workspace configuration file wants to start an MCP server.\n` + + `Server: ${serverName}\n` + + `Command: ${cmdLine}\n` + + `Source: ${configPath}\n\n` + + `Running this server executes the above command on your machine. ` + + `Only allow if you trust the authors of this workspace.\n\n` + + `Your choice will be remembered for this workspace. ` + + `If you allow, you won't be asked again unless the server configuration changes.`, + actions: [allowBtn, denyBtn], + }) + } catch (e: any) { + this.features.logging.warn(`MCP: consent prompt failed for '${serverName}': ${e?.message}`) + this.setState(serverName, McpServerStatus.FAILED, 0, 'consent prompt failed') + return + } + if (choice?.title !== allowBtn.title) { + this.features.logging.info( + `MCP: user declined consent for workspace-scoped server '${serverName}' (response: ${choice?.title ?? 'dismissed'})` + ) + this.sessionDeniedConsent.add(denyKey) + this.setState(serverName, McpServerStatus.DISABLED, 0, 'consent not granted') + return + } + await recordApproval(this.features.workspace, this.features.logging, serverName, cfg, configPath) + this.features.logging.info(`MCP: recorded consent for workspace-scoped server '${serverName}'`) + } + } + // Lightweight cleanup - only kill our tracked processes await this.cleanupExistingServer(serverName) @@ -1073,6 +1141,12 @@ export class McpManager { this.mcpTools = this.mcpTools.filter(t => t.serverName !== serverName) this.mcpServerStates.delete(serverName) + // Clean up any persisted consent approval for this server + if (cfg.__configPath__) { + const normalizedPath = normalizePathFromUri(cfg.__configPath__, this.features.logging) + await removeApproval(this.features.workspace, this.features.logging, serverName, normalizedPath) + } + // Check if this is a legacy MCP server (from MCP config file) const isLegacyMcpServer = cfg.__configPath__?.endsWith('mcp.json') let agentPath: string | undefined @@ -1285,6 +1359,7 @@ export class McpManager { this.mcpTools = [] this.mcpServers.clear() this.mcpServerStates.clear() + this.sessionDeniedConsent.clear() this.agentConfig = { name: 'q_ide_default', description: 'Agent configuration', diff --git a/server/aws-lsp-codewhisperer/src/language-server/chat/contexts/triggerContext.ts b/server/aws-lsp-codewhisperer/src/language-server/chat/contexts/triggerContext.ts index 3d843a9ce3..98ca46983b 100644 --- a/server/aws-lsp-codewhisperer/src/language-server/chat/contexts/triggerContext.ts +++ b/server/aws-lsp-codewhisperer/src/language-server/chat/contexts/triggerContext.ts @@ -4,8 +4,7 @@ import { BedrockTools, ChatParams, CursorState, InlineChatParams } from '@aws/la import { Features } from '../../types' import { DocumentContext, DocumentContextExtractor } from './documentContext' import { SendMessageCommandInput } from '../../../shared/streamingClientService' -import { LocalProjectContextController } from '../../../shared/localProjectContextController' -import { convertChunksToRelevantTextDocuments } from '../tools/relevantTextDocuments' + import { AmazonQBaseServiceManager as AmazonQServiceManager } from '../../../shared/amazonQServiceManager/BaseAmazonQServiceManager' export interface TriggerContext extends Partial { @@ -35,17 +34,11 @@ export class QChatTriggerContext { async getNewTriggerContext(params: ChatParams | InlineChatParams): Promise { const documentContext: DocumentContext | undefined = await this.extractDocumentContext(params) - const useRelevantDocuments = - 'context' in params - ? params.context?.some(context => typeof context !== 'string' && context.command === '@workspace') - : false - let relevantDocuments = useRelevantDocuments ? await this.extractProjectContext(params.prompt.prompt) : [] - return { ...documentContext, userIntent: this.#guessIntentFromPrompt(params.prompt.prompt), - useRelevantDocuments, - relevantDocuments, + useRelevantDocuments: false, + relevantDocuments: [], } } @@ -134,32 +127,6 @@ export class QChatTriggerContext { : undefined } - async extractProjectContext(query?: string): Promise { - if (query) { - try { - let enableWorkspaceContext = true - - if (this.amazonQServiceManager) { - const config = this.amazonQServiceManager.getConfiguration() - if (config.projectContext?.enableLocalIndexing === false) { - enableWorkspaceContext = false - } - } - - if (!enableWorkspaceContext) { - this.#logger.debug('Workspace context is disabled, skipping project context extraction') - return [] - } - const contextController = await LocalProjectContextController.getInstance() - const resp = await contextController.queryVectorIndex({ query }) - return convertChunksToRelevantTextDocuments(resp) - } catch (e) { - this.#logger.error(`Failed to extract project context for chat trigger: ${e}`) - } - } - return [] - } - #guessIntentFromPrompt(prompt?: string): UserIntent | undefined { if (prompt === undefined) { return undefined diff --git a/server/aws-lsp-codewhisperer/src/language-server/chat/contexts/triggerContexts.test.ts b/server/aws-lsp-codewhisperer/src/language-server/chat/contexts/triggerContexts.test.ts index 79b33625e8..3cc6894682 100644 --- a/server/aws-lsp-codewhisperer/src/language-server/chat/contexts/triggerContexts.test.ts +++ b/server/aws-lsp-codewhisperer/src/language-server/chat/contexts/triggerContexts.test.ts @@ -116,26 +116,4 @@ describe('QChatTriggerContext', () => { assert.deepStrictEqual(documentContext, mockDocumentContext) }) - - it('should not extract project context when workspace context is disabled', async () => { - amazonQServiceManager.getConfiguration.returns({ - projectContext: { - enableLocalIndexing: false, - }, - }) - - const triggerContext = new QChatTriggerContext( - testFeatures.workspace, - testFeatures.logging, - amazonQServiceManager - ) - - const getInstanceStub = sinon.stub(LocalProjectContextController, 'getInstance') - - const result = await triggerContext.extractProjectContext('test query') - - sinon.assert.notCalled(getInstanceStub) - - assert.deepStrictEqual(result, []) - }) }) 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 b86c51e448..5209e0f6e1 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 @@ -338,7 +338,6 @@ export class ChatTelemetryController { chatConversationType: metric.cwsprChatConversationType, chatActiveEditorImportCount: metric.cwsprChatActiveEditorImportCount, cwsprChatHasContextList: metric.cwsprChatHasContextList, - cwsprChatHasWorkspaceContext: metric.cwsprChatHasWorkspaceContext, cwsprChatFolderContextCount: metric.cwsprChatFolderContextCount, cwsprChatFileContextCount: metric.cwsprChatFileContextCount, cwsprChatRuleContextCount: metric.cwsprChatRuleContextCount, 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 ac96094354..05b69513c5 100644 --- a/server/aws-lsp-codewhisperer/src/language-server/localProjectContext/localProjectContextServer.ts +++ b/server/aws-lsp-codewhisperer/src/language-server/localProjectContext/localProjectContextServer.ts @@ -22,7 +22,6 @@ export const LocalProjectContextServer = let amazonQServiceManager: AmazonQBaseServiceManager let telemetryService: TelemetryService - let localProjectContextEnabled: boolean = false let VSCWindowsOverride: boolean = false lsp.addInitializer((params: InitializeParams) => { @@ -164,22 +163,13 @@ export const LocalProjectContextServer = const updateConfigurationHandler = async (updatedConfig: AmazonQWorkspaceConfig) => { logging.log('Updating configuration of local context server') try { - localProjectContextEnabled = updatedConfig.projectContext?.enableLocalIndexing === true if (process.env.DISABLE_INDEXING_LIBRARY === 'true') { logging.log('Skipping local project context initialization') - localProjectContextEnabled = false } else { - logging.log( - `Setting project context indexing enabled to ${updatedConfig.projectContext?.enableLocalIndexing}` - ) await localProjectContextController.init({ - enableGpuAcceleration: updatedConfig?.projectContext?.enableGpuAcceleration, - indexWorkerThreads: updatedConfig?.projectContext?.indexWorkerThreads, ignoreFilePatterns: updatedConfig.projectContext?.localIndexing?.ignoreFilePatterns, maxFileSizeMB: updatedConfig.projectContext?.localIndexing?.maxFileSizeMB, maxIndexSizeMB: updatedConfig.projectContext?.localIndexing?.maxIndexSizeMB, - enableIndexing: localProjectContextEnabled, - indexCacheDirPath: updatedConfig.projectContext?.localIndexing?.indexCacheDirPath, }) } } catch (error) { diff --git a/server/aws-lsp-codewhisperer/src/shared/amazonQServiceManager/configurationUtils.test.ts b/server/aws-lsp-codewhisperer/src/shared/amazonQServiceManager/configurationUtils.test.ts index 52dc9b714f..d2eb6455fd 100644 --- a/server/aws-lsp-codewhisperer/src/shared/amazonQServiceManager/configurationUtils.test.ts +++ b/server/aws-lsp-codewhisperer/src/shared/amazonQServiceManager/configurationUtils.test.ts @@ -21,14 +21,10 @@ describe('getAmazonQRelatedWorkspaceConfigs', () => { extraContext: 'some-inline-chat-context', }, projectContext: { - enableLocalIndexing: true, - enableGpuAcceleration: true, - indexWorkerThreads: 1, localIndexing: { ignoreFilePatterns: [], maxFileSizeMB: 10, maxIndexSizeMB: 2048, - indexCacheDirPath: undefined, }, }, } @@ -60,9 +56,6 @@ describe('getAmazonQRelatedWorkspaceConfigs', () => { shareCodeWhispererContentWithAWS: MOCKED_AWS_CODEWHISPERER_SECTION.shareCodeWhispererContentWithAWS, sendUserWrittenCodeMetrics: MOCKED_AWS_CODEWHISPERER_SECTION.sendUserWrittenCodeMetrics, projectContext: { - enableLocalIndexing: MOCKED_AWS_Q_SECTION.projectContext.enableLocalIndexing, - enableGpuAcceleration: MOCKED_AWS_Q_SECTION.projectContext?.enableGpuAcceleration, - indexWorkerThreads: MOCKED_AWS_Q_SECTION.projectContext?.indexWorkerThreads, localIndexing: MOCKED_AWS_Q_SECTION.projectContext.localIndexing, }, } @@ -112,14 +105,10 @@ describe('AmazonQConfigurationCache', () => { shareCodeWhispererContentWithAWS: true, sendUserWrittenCodeMetrics: false, projectContext: { - enableLocalIndexing: true, - enableGpuAcceleration: true, - indexWorkerThreads: 1, localIndexing: { ignoreFilePatterns: [], maxFileSizeMB: 10, maxIndexSizeMB: 2048, - indexCacheDirPath: undefined, }, }, } @@ -136,9 +125,9 @@ describe('AmazonQConfigurationCache', () => { mockedQConfig.customizationArn = undefined mockedQConfig.inlineSuggestions = { extraContext: undefined } mockedQConfig.projectContext = { - enableLocalIndexing: false, - enableGpuAcceleration: false, - indexWorkerThreads: 0, + localIndexing: { + ignoreFilePatterns: ['*.log'], + }, } notDeepStrictEqual(cache.getProperty('customizationArn'), mockedQConfig.customizationArn) notDeepStrictEqual(cache.getProperty('inlineSuggestions'), mockedQConfig.inlineSuggestions) diff --git a/server/aws-lsp-codewhisperer/src/shared/amazonQServiceManager/configurationUtils.ts b/server/aws-lsp-codewhisperer/src/shared/amazonQServiceManager/configurationUtils.ts index 8adb68f2d0..899a2909ef 100644 --- a/server/aws-lsp-codewhisperer/src/shared/amazonQServiceManager/configurationUtils.ts +++ b/server/aws-lsp-codewhisperer/src/shared/amazonQServiceManager/configurationUtils.ts @@ -75,13 +75,9 @@ interface LocalIndexConfig { ignoreFilePatterns?: string[] // patterns must follow .gitignore convention maxFileSizeMB?: number maxIndexSizeMB?: number - indexCacheDirPath?: string // defaults to homedir/.aws/amazonq/cache } interface QProjectContextConfig { - enableLocalIndexing: boolean // aws.q.projectContext.enableLocalIndexing - enableGpuAcceleration: boolean // aws.q.projectContext.enableGpuAcceleration - indexWorkerThreads: number // aws.q.projectContext.indexWorkerThreads localIndexing?: LocalIndexConfig } @@ -139,14 +135,10 @@ export async function getAmazonQRelatedWorkspaceConfigs( extraContext: textUtils.undefinedIfEmpty(newQConfig.inlineChat?.extraContext), }, projectContext: { - enableLocalIndexing: newQConfig.projectContext?.enableLocalIndexing === true, - enableGpuAcceleration: newQConfig.projectContext?.enableGpuAcceleration === true, - indexWorkerThreads: newQConfig.projectContext?.indexWorkerThreads ?? -1, localIndexing: { ignoreFilePatterns: newQConfig.projectContext?.localIndexing?.ignoreFilePatterns ?? [], maxFileSizeMB: newQConfig.projectContext?.localIndexing?.maxFileSizeMB ?? 10, maxIndexSizeMB: newQConfig.projectContext?.localIndexing?.maxIndexSizeMB ?? 2048, - indexCacheDirPath: newQConfig.projectContext?.localIndexing?.indexCacheDirPath ?? undefined, }, }, } @@ -202,14 +194,10 @@ export const defaultAmazonQWorkspaceConfigFactory = (): AmazonQWorkspaceConfig = shareCodeWhispererContentWithAWS: false, sendUserWrittenCodeMetrics: false, projectContext: { - enableLocalIndexing: false, - enableGpuAcceleration: false, - indexWorkerThreads: -1, localIndexing: { ignoreFilePatterns: [], maxFileSizeMB: 10, maxIndexSizeMB: 2048, - indexCacheDirPath: undefined, }, }, } diff --git a/server/aws-lsp-codewhisperer/src/shared/localProjectContextController.test.ts b/server/aws-lsp-codewhisperer/src/shared/localProjectContextController.test.ts index ef73e5c20a..a11e499b30 100644 --- a/server/aws-lsp-codewhisperer/src/shared/localProjectContextController.test.ts +++ b/server/aws-lsp-codewhisperer/src/shared/localProjectContextController.test.ts @@ -46,8 +46,6 @@ describe('LocalProjectContextController', () => { vectorLibMock = { start: stub().resolves({ buildIndex: stub().resolves(), - clear: stub().resolves(), - queryVectorIndex: stub().resolves(['mockChunk1', 'mockChunk2']), queryInlineProjectContext: stub().resolves(['mockContext1']), updateIndexV2: stub().resolves(), getContextCommandItems: stub().resolves([]), @@ -80,7 +78,7 @@ describe('LocalProjectContextController', () => { describe('init', () => { it('should initialize vector library successfully', async () => { const buildIndexSpy = spy(controller, 'buildIndex') - await controller.init({ vectorLib: vectorLibMock, enableIndexing: true }) + await controller.init({ vectorLib: vectorLibMock }) sinonAssert.notCalled(logging.error) sinonAssert.called(vectorLibMock.start) @@ -96,24 +94,13 @@ describe('LocalProjectContextController', () => { sinonAssert.called(logging.error) }) - it('should call buildIndex with `default` if not enabled', async () => { - const buildIndexSpy = spy(controller, 'buildIndex') - await controller.init({ vectorLib: vectorLibMock, enableIndexing: false }) - - sinonAssert.notCalled(logging.error) - sinonAssert.called(vectorLibMock.start) - sinonAssert.calledOnce(buildIndexSpy) - sinonAssert.calledWith(buildIndexSpy, 'default') - }) - - it('should call buildIndex with `all` when enabled', async () => { + it('should call buildIndex on init', async () => { const buildIndexSpy = spy(controller, 'buildIndex') - await controller.init({ vectorLib: vectorLibMock, enableIndexing: true }) + await controller.init({ vectorLib: vectorLibMock }) sinonAssert.notCalled(logging.error) sinonAssert.called(vectorLibMock.start) sinonAssert.calledOnce(buildIndexSpy) - sinonAssert.calledWith(buildIndexSpy, 'all') }) }) @@ -121,64 +108,16 @@ describe('LocalProjectContextController', () => { it('should build Index with vectorLib', async () => { await controller.init({ vectorLib: vectorLibMock }) const vecLib = await vectorLibMock.start() - await controller.buildIndex('all') + await controller.buildIndex() sinonAssert.called(vecLib.buildIndex) }) }) - - describe('queryVectorIndex', () => { - beforeEach(async () => { - await controller.init({ vectorLib: vectorLibMock }) - }) - - it('should return empty array when vector library is not initialized', async () => { - sinon.stub(controller, 'isIndexingEnabled').returns(true) - const uninitializedController = new LocalProjectContextController( - 'testClient', - mockWorkspaceFolders, - logging as any - ) - - const result = await uninitializedController.queryVectorIndex({ query: 'test' }) - assert.deepStrictEqual(result, []) - }) - - it('should return empty array when indexing is disabled', async () => { - sinon.stub(controller, 'isIndexingEnabled').returns(false) - const uninitializedController = new LocalProjectContextController( - 'testClient', - mockWorkspaceFolders, - logging as any - ) - - const result = await uninitializedController.queryVectorIndex({ query: 'test' }) - assert.deepStrictEqual(result, []) - }) - - it('should return chunks from vector library', async () => { - sinon.stub(controller, 'isIndexingEnabled').returns(true) - const result = await controller.queryVectorIndex({ query: 'test' }) - assert.deepStrictEqual(result, ['mockChunk1', 'mockChunk2']) - }) - - it('should handle query errors', async () => { - sinon.stub(controller, 'isIndexingEnabled').returns(true) - const vecLib = await vectorLibMock.start() - vecLib.queryVectorIndex.rejects(new Error('Query failed')) - - const result = await controller.queryVectorIndex({ query: 'test' }) - assert.deepStrictEqual(result, []) - sinonAssert.called(logging.error) - }) - }) - describe('queryInlineProjectContext', () => { beforeEach(async () => { await controller.init({ vectorLib: vectorLibMock }) }) it('should return empty array when vector library is not initialized', async () => { - sinon.stub(controller, 'isIndexingEnabled').returns(true) const uninitializedController = new LocalProjectContextController( 'testClient', mockWorkspaceFolders, @@ -194,7 +133,6 @@ describe('LocalProjectContextController', () => { }) it('should return empty array when indexing is disabled', async () => { - sinon.stub(controller, 'isIndexingEnabled').returns(false) const uninitializedController = new LocalProjectContextController( 'testClient', mockWorkspaceFolders, @@ -210,7 +148,6 @@ describe('LocalProjectContextController', () => { }) it('should return context from vector library', async () => { - sinon.stub(controller, 'isIndexingEnabled').returns(true) const result = await controller.queryInlineProjectContext({ query: 'test', filePath: 'test.java', @@ -220,7 +157,6 @@ describe('LocalProjectContextController', () => { }) it('should handle query errors', async () => { - sinon.stub(controller, 'isIndexingEnabled').returns(true) const vecLib = await vectorLibMock.start() vecLib.queryInlineProjectContext.rejects(new Error('Query failed')) @@ -353,85 +289,12 @@ describe('LocalProjectContextController', () => { }) }) - describe('configuration options', () => { - let processEnvBackup: NodeJS.ProcessEnv - - beforeEach(() => { - processEnvBackup = { ...process.env } - }) - - afterEach(() => { - process.env = processEnvBackup - }) - - it('should set GPU acceleration environment variable when enabled', async () => { - await controller.init({ - enableGpuAcceleration: true, - vectorLib: vectorLibMock, - }) - assert.strictEqual(process.env.Q_ENABLE_GPU, 'true') - sinonAssert.called(vectorLibMock.start) - }) - - it('should remove GPU acceleration environment variable when disabled', async () => { - process.env.Q_ENABLE_GPU = 'true' - await controller.init({ - enableGpuAcceleration: false, - vectorLib: vectorLibMock, - }) - assert.strictEqual(process.env.Q_ENABLE_GPU, undefined) - sinonAssert.called(vectorLibMock.start) - }) - - it('should set worker threads environment variable when specified', async () => { - await controller.init({ - indexWorkerThreads: 4, - vectorLib: vectorLibMock, - }) - assert.strictEqual(process.env.Q_WORKER_THREADS, '4') - sinonAssert.called(vectorLibMock.start) - }) - - it('should remove worker threads environment variable when not specified', async () => { - process.env.Q_WORKER_THREADS = '4' - await controller.init({ - vectorLib: vectorLibMock, - }) - assert.strictEqual(process.env.Q_WORKER_THREADS, undefined) - sinonAssert.called(vectorLibMock.start) - }) - - it('should ignore invalid worker thread counts', async () => { - process.env.Q_WORKER_THREADS = '4' - await controller.init({ - indexWorkerThreads: 101, - vectorLib: vectorLibMock, - }) - assert.strictEqual(process.env.Q_WORKER_THREADS, undefined) - sinonAssert.called(vectorLibMock.start) - }) - - it('should ignore negative worker thread counts', async () => { - process.env.Q_WORKER_THREADS = '4' - await controller.init({ - indexWorkerThreads: -1, - vectorLib: vectorLibMock, - }) - assert.strictEqual(process.env.Q_WORKER_THREADS, undefined) - sinonAssert.called(vectorLibMock.start) - }) - }) - describe('dispose', () => { it('should clear and remove vector library reference', async () => { await controller.init({ vectorLib: vectorLibMock }) await controller.dispose() - const vecLib = await vectorLibMock.start() - sinonAssert.called(vecLib.clear) - - const queryResult = await controller.queryVectorIndex({ query: 'test' }) - assert.deepStrictEqual(queryResult, []) + assert.strictEqual(controller.isEnabled, false) }) }) }) diff --git a/server/aws-lsp-codewhisperer/src/shared/localProjectContextController.ts b/server/aws-lsp-codewhisperer/src/shared/localProjectContextController.ts index 527f92f3b4..0cc0495bc1 100644 --- a/server/aws-lsp-codewhisperer/src/shared/localProjectContextController.ts +++ b/server/aws-lsp-codewhisperer/src/shared/localProjectContextController.ts @@ -8,7 +8,6 @@ import type { ContextCommandItem, InlineProjectContext, QueryInlineProjectContextRequestV2, - QueryRequest, UpdateMode, VectorLibAPI, } from 'local-indexing' @@ -41,17 +40,11 @@ export interface LocalProjectContextInitializationOptions { includeSymlinks?: boolean maxFileSizeMB?: number maxIndexSizeMB?: number - indexCacheDirPath?: string - enableGpuAcceleration?: boolean - indexWorkerThreads?: number - enableIndexing?: boolean } export class LocalProjectContextController { // Event handler for context items updated public onContextItemsUpdated: ((contextItems: ContextCommandItem[]) => Promise) | undefined - // Event handler for when index is being built - public onIndexingInProgressChanged: ((enabled: boolean) => void) | undefined private static instance: LocalProjectContextController | undefined private workspaceFolders: WorkspaceFolder[] @@ -59,14 +52,12 @@ export class LocalProjectContextController { private _contextCommandSymbolsUpdated = false private readonly clientName: string private readonly log: Logging - private _isIndexingEnabled: boolean = false private _isIndexingInProgress: boolean = false private ignoreFilePatterns?: string[] private includeSymlinks?: boolean private maxFileSizeMB?: number private maxIndexSizeMB?: number private respectUserGitIgnores?: boolean - private indexCacheDirPath: string = path.join(homedir(), '.aws', 'amazonq', 'cache') private readonly fileExtensions: string[] = Object.keys(languageByExtension) private readonly DEFAULT_MAX_INDEX_SIZE_MB = 2048 @@ -108,10 +99,6 @@ export class LocalProjectContextController { includeSymlinks = false, maxFileSizeMB = this.DEFAULT_MAX_FILE_SIZE_MB, maxIndexSizeMB = this.DEFAULT_MAX_INDEX_SIZE_MB, - indexCacheDirPath = path.join(homedir(), '.aws', 'amazonq', 'cache'), - enableGpuAcceleration = false, - indexWorkerThreads = 0, - enableIndexing = false, }: LocalProjectContextInitializationOptions = {}): Promise { try { // update states according to configuration @@ -120,66 +107,30 @@ export class LocalProjectContextController { this.maxIndexSizeMB = maxIndexSizeMB this.respectUserGitIgnores = respectUserGitIgnores this.ignoreFilePatterns = ignoreFilePatterns - if (indexCacheDirPath?.length > 0 && path.parse(indexCacheDirPath)) { - this.indexCacheDirPath = indexCacheDirPath - } - if (enableGpuAcceleration) { - process.env.Q_ENABLE_GPU = 'true' - } else { - delete process.env.Q_ENABLE_GPU - } - if (indexWorkerThreads && indexWorkerThreads > 0 && indexWorkerThreads < 100) { - process.env.Q_WORKER_THREADS = indexWorkerThreads.toString() - } else { - delete process.env.Q_WORKER_THREADS - } - this.log.info( - `Vector library initializing with GPU acceleration: ${enableGpuAcceleration}, ` + - `index worker thread count: ${indexWorkerThreads}` - ) + this.log.info('Initializing local project context') - // build index if vecLib was initialized but indexing was not enabled before + // skip re-init if vecLib already loaded if (this._vecLib) { - // if indexing is turned being on, build index with 'all' that supports vector indexing - if (enableIndexing && !this._isIndexingEnabled) { - this.buildIndex('all').catch(e => { - this.log.error(`Error building index with indexing enabled: ${e}`) - }) - } - // if indexing is turned being off, build index with 'default' that does not support vector indexing - if (!enableIndexing && this._isIndexingEnabled) { - this.buildIndex('default').catch(e => { - this.log.error(`Error building index with indexing disabled: ${e}`) - }) - } - this._isIndexingEnabled = enableIndexing return } - // initialize vecLib and index if needed + // initialize vecLib and index const libraryPath = this.getVectorLibraryPath() const vecLib = vectorLib ?? (await eval(`import("${libraryPath}")`)) if (vecLib) { try { - this._vecLib = await vecLib.start(LIBRARY_DIR, this.clientName, this.indexCacheDirPath) + this._vecLib = await vecLib.start(LIBRARY_DIR, this.clientName) } catch (startError) { this.log.warn(`Vector library start() failed (native modules may be missing): ${startError}`) this.log.warn('Context commands will be unavailable') } if (this._vecLib) { - if (enableIndexing) { - this.buildIndex('all').catch(e => { - this.log.error(`Error building index on init with indexing enabled: ${e}`) - }) - } else { - this.buildIndex('default').catch(e => { - this.log.error(`Error building index on init with indexing disabled: ${e}`) - }) - } + this.buildIndex().catch(e => { + this.log.error(`Error building index on init: ${e}`) + }) } LocalProjectContextController.instance = this - this._isIndexingEnabled = enableIndexing } else { this.log.warn(`Vector library could not be imported from: ${libraryPath}`) LocalProjectContextController.instance = this @@ -219,13 +170,12 @@ export class LocalProjectContextController { } // public for test - async buildIndex(indexingType: string): Promise { + async buildIndex(): Promise { if (this._isIndexingInProgress) { return } try { this._isIndexingInProgress = true - this.onIndexingInProgressChanged?.(this._isIndexingInProgress) if (this._vecLib) { if (!this.workspaceFolders.length) { this.log.info('skip building index because no workspace folder found') @@ -239,14 +189,13 @@ export class LocalProjectContextController { ) const projectRoot = URI.parse(this.workspaceFolders.sort()[0].uri).fsPath - await this._vecLib?.buildIndex(sourceFiles, projectRoot, indexingType) + await this._vecLib?.buildIndex(sourceFiles, projectRoot, 'default') this.log.info('Context index built successfully') } } catch (error) { this.log.error(`Error building index: ${error}`) } finally { this._isIndexingInProgress = false - this.onIndexingInProgressChanged?.(this._isIndexingInProgress) } } @@ -285,7 +234,6 @@ export class LocalProjectContextController { public async queryInlineProjectContext( request: QueryInlineProjectContextRequestV2 ): Promise { - // inline project context is available for all users regardless of local indexing enabled or disabled try { const resp = await this._vecLib?.queryInlineProjectContext(request.query, request.filePath, request.target) return resp ?? [] @@ -295,20 +243,6 @@ export class LocalProjectContextController { } } - public async queryVectorIndex(request: QueryRequest): Promise { - if (!this.isIndexingEnabled()) { - return [] - } - - try { - const resp = await this._vecLib?.queryVectorIndex(request.query) - return resp ?? [] - } catch (error) { - this.log.error(`Error in queryVectorIndex: ${error}`) - return [] - } - } - public async getContextCommandItems(): Promise { if (!this._vecLib) { return [] @@ -397,10 +331,6 @@ export class LocalProjectContextController { } } - public isIndexingEnabled(): boolean { - return this._vecLib !== undefined && this._isIndexingEnabled - } - async processWorkspaceFolders( workspaceFolders?: WorkspaceFolder[] | null, fileExtensions?: string[], 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 66f81af680..32536f73fd 100644 --- a/server/aws-lsp-codewhisperer/src/shared/telemetry/telemetryService.test.ts +++ b/server/aws-lsp-codewhisperer/src/shared/telemetry/telemetryService.test.ts @@ -775,7 +775,6 @@ describe('TelemetryService', () => { }, { cwsprChatHasContextList: true, - cwsprChatHasWorkspaceContext: false, cwsprChatFolderContextCount: 0, cwsprChatFileContextCount: 0, cwsprChatRuleContextCount: 0, @@ -845,7 +844,6 @@ describe('TelemetryService', () => { experimentName: undefined, userVariation: undefined, cwsprChatHasContextList: true, - cwsprChatHasWorkspaceContext: false, cwsprChatFolderContextCount: 0, cwsprChatFileContextCount: 0, cwsprChatRuleContextCount: 0, diff --git a/server/aws-lsp-codewhisperer/src/shared/telemetry/telemetryService.ts b/server/aws-lsp-codewhisperer/src/shared/telemetry/telemetryService.ts index f7c93b6f14..861333d302 100644 --- a/server/aws-lsp-codewhisperer/src/shared/telemetry/telemetryService.ts +++ b/server/aws-lsp-codewhisperer/src/shared/telemetry/telemetryService.ts @@ -563,7 +563,6 @@ export class TelemetryService { chatConversationType: ChatConversationType chatActiveEditorImportCount?: number cwsprChatHasContextList: boolean - cwsprChatHasWorkspaceContext?: boolean cwsprChatFolderContextCount: number cwsprChatFileContextCount: number cwsprChatFileContextLength: number @@ -617,7 +616,6 @@ export class TelemetryService { cwsprChatActiveEditorImportCount: additionalParams.chatActiveEditorImportCount, codewhispererCustomizationArn: params.customizationArn, cwsprChatHasContextList: additionalParams.cwsprChatHasContextList, - cwsprChatHasWorkspaceContext: additionalParams.cwsprChatHasWorkspaceContext, cwsprChatFolderContextCount: additionalParams.cwsprChatFolderContextCount, cwsprChatFileContextCount: additionalParams.cwsprChatFileContextCount, cwsprChatRuleContextCount: additionalParams.cwsprChatRuleContextCount,