diff --git a/packages/amazonq/src/app/chat/node/activateAgents.ts b/packages/amazonq/src/app/chat/node/activateAgents.ts index 954f2892eda..cd0309d7f2d 100644 --- a/packages/amazonq/src/app/chat/node/activateAgents.ts +++ b/packages/amazonq/src/app/chat/node/activateAgents.ts @@ -11,9 +11,6 @@ export function activateAgents() { const appInitContext = DefaultAmazonQAppInitContext.instance amazonqNode.cwChatAppInit(appInitContext) - amazonqNode.featureDevChatAppInit(appInitContext) amazonqNode.gumbyChatAppInit(appInitContext) - amazonqNode.testChatAppInit(appInitContext) - amazonqNode.docChatAppInit(appInitContext) scanChatAppInit(appInitContext) } diff --git a/packages/amazonq/test/e2e/amazonq/doc.test.ts b/packages/amazonq/test/e2e/amazonq/doc.test.ts deleted file mode 100644 index 20d281fe7b8..00000000000 --- a/packages/amazonq/test/e2e/amazonq/doc.test.ts +++ /dev/null @@ -1,492 +0,0 @@ -/*! - * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. - * SPDX-License-Identifier: Apache-2.0 - */ - -import vscode from 'vscode' -import assert from 'assert' -import { qTestingFramework } from './framework/framework' -import { getTestWindow, registerAuthHook, toTextEditor, using } from 'aws-core-vscode/test' -import { loginToIdC } from './utils/setup' -import { Messenger } from './framework/messenger' -import { FollowUpTypes } from 'aws-core-vscode/amazonq' -import { fs, i18n, sleep } from 'aws-core-vscode/shared' -import { - docGenerationProgressMessage, - DocGenerationStep, - docGenerationSuccessMessage, - docRejectConfirmation, - Mode, -} from 'aws-core-vscode/amazonqDoc' - -describe('Amazon Q Doc Generation', async function () { - let framework: qTestingFramework - let tab: Messenger - let workspaceUri: vscode.Uri - let rootReadmeFileUri: vscode.Uri - - type testProjectConfig = { - path: string - language: string - mockFile: string - mockContent: string - } - const testProjects: testProjectConfig[] = [ - { - path: 'ts-plain-sam-app', - language: 'TypeScript', - mockFile: 'bubbleSort.ts', - mockContent: ` - function bubbleSort(arr: number[]): number[] { - const n = arr.length; - for (let i = 0; i < n - 1; i++) { - for (let j = 0; j < n - i - 1; j++) { - if (arr[j] > arr[j + 1]) { - [arr[j], arr[j + 1]] = [arr[j + 1], arr[j]]; - } - } - } - return arr; - }`, - }, - { - path: 'ruby-plain-sam-app', - language: 'Ruby', - mockFile: 'bubble_sort.rb', - mockContent: ` - def bubble_sort(arr) - n = arr.length - (n-1).times do |i| - (0..n-i-2).each do |j| - if arr[j] > arr[j+1] - arr[j], arr[j+1] = arr[j+1], arr[j] - end - end - end - arr - end`, - }, - { - path: 'js-plain-sam-app', - language: 'JavaScript', - mockFile: 'bubbleSort.js', - mockContent: ` - function bubbleSort(arr) { - const n = arr.length; - for (let i = 0; i < n - 1; i++) { - for (let j = 0; j < n - i - 1; j++) { - if (arr[j] > arr[j + 1]) { - [arr[j], arr[j + 1]] = [arr[j + 1], arr[j]]; - } - } - } - return arr; - }`, - }, - { - path: 'java11-plain-maven-sam-app', - language: 'Java', - mockFile: 'BubbleSort.java', - mockContent: ` - public static void bubbleSort(int[] arr) { - int n = arr.length; - for (int i = 0; i < n - 1; i++) { - for (int j = 0; j < n - i - 1; j++) { - if (arr[j] > arr[j + 1]) { - int temp = arr[j]; - arr[j] = arr[j + 1]; - arr[j + 1] = temp; - } - } - } - }`, - }, - { - path: 'go1-plain-sam-app', - language: 'Go', - mockFile: 'bubble_sort.go', - mockContent: ` - func bubbleSort(arr []int) []int { - n := len(arr) - for i := 0; i < n-1; i++ { - for j := 0; j < n-i-1; j++ { - if arr[j] > arr[j+1] { - arr[j], arr[j+1] = arr[j+1], arr[j] - } - } - } - return arr - }`, - }, - { - path: 'python3.7-plain-sam-app', - language: 'Python', - mockFile: 'bubble_sort.py', - mockContent: ` - def bubble_sort(arr): - n = len(arr) - for i in range(n-1): - for j in range(0, n-i-1): - if arr[j] > arr[j+1]: - arr[j], arr[j+1] = arr[j+1], arr[j] - return arr`, - }, - ] - - const docUtils = { - async initializeDocOperation(operation: 'create' | 'update' | 'edit') { - console.log(`Initializing documentation ${operation} operation`) - - switch (operation) { - case 'create': - await tab.waitForButtons([FollowUpTypes.CreateDocumentation, FollowUpTypes.UpdateDocumentation]) - tab.clickButton(FollowUpTypes.CreateDocumentation) - await tab.waitForText(i18n('AWS.amazonq.doc.answer.createReadme')) - break - case 'update': - await tab.waitForButtons([FollowUpTypes.CreateDocumentation, FollowUpTypes.UpdateDocumentation]) - tab.clickButton(FollowUpTypes.UpdateDocumentation) - await tab.waitForButtons([FollowUpTypes.SynchronizeDocumentation, FollowUpTypes.EditDocumentation]) - tab.clickButton(FollowUpTypes.SynchronizeDocumentation) - await tab.waitForText(i18n('AWS.amazonq.doc.answer.updateReadme')) - break - case 'edit': - await tab.waitForButtons([FollowUpTypes.UpdateDocumentation]) - tab.clickButton(FollowUpTypes.UpdateDocumentation) - await tab.waitForButtons([FollowUpTypes.SynchronizeDocumentation, FollowUpTypes.EditDocumentation]) - tab.clickButton(FollowUpTypes.EditDocumentation) - await tab.waitForText(i18n('AWS.amazonq.doc.answer.updateReadme')) - break - } - }, - - async handleFolderSelection(testProject: testProjectConfig) { - console.table({ - 'Test in project': { - Path: testProject.path, - Language: testProject.language, - }, - }) - - const projectUri = vscode.Uri.joinPath(workspaceUri, testProject.path) - const readmeFileUri = vscode.Uri.joinPath(projectUri, 'README.md') - - // Cleanup existing README - await fs.delete(readmeFileUri, { force: true }) - - await tab.waitForButtons([FollowUpTypes.ProceedFolderSelection, FollowUpTypes.ChooseFolder]) - tab.clickButton(FollowUpTypes.ChooseFolder) - getTestWindow().onDidShowDialog((d) => d.selectItem(projectUri)) - - return readmeFileUri - }, - - async executeDocumentationFlow(operation: 'create' | 'update' | 'edit', msg?: string) { - const mode = { - create: Mode.CREATE, - update: Mode.SYNC, - edit: Mode.EDIT, - }[operation] - - console.log(`Executing documentation ${operation} flow`) - - await tab.waitForButtons([FollowUpTypes.ProceedFolderSelection]) - tab.clickButton(FollowUpTypes.ProceedFolderSelection) - - if (mode === Mode.EDIT && msg) { - tab.addChatMessage({ prompt: msg }) - } - await tab.waitForText(docGenerationProgressMessage(DocGenerationStep.SUMMARIZING_FILES, mode)) - await tab.waitForText(`${docGenerationSuccessMessage(mode)} ${i18n('AWS.amazonq.doc.answer.codeResult')}`) - await tab.waitForButtons([ - FollowUpTypes.AcceptChanges, - FollowUpTypes.MakeChanges, - FollowUpTypes.RejectChanges, - ]) - }, - - async verifyResult(action: FollowUpTypes, readmeFileUri?: vscode.Uri, shouldExist = true) { - tab.clickButton(action) - - if (action === FollowUpTypes.RejectChanges) { - await tab.waitForText(docRejectConfirmation) - assert.deepStrictEqual(tab.getChatItems().pop()?.body, docRejectConfirmation) - } - await tab.waitForButtons([FollowUpTypes.NewTask, FollowUpTypes.CloseSession]) - - if (readmeFileUri) { - const fileExists = await fs.exists(readmeFileUri) - console.log(`README file exists: ${fileExists}, Expected: ${shouldExist}`) - assert.strictEqual( - fileExists, - shouldExist, - shouldExist - ? 'README file was not saved to the appropriate folder' - : 'README file should not be saved to the folder' - ) - if (fileExists) { - await fs.delete(readmeFileUri, { force: true }) - } - } - }, - - async prepareMockFile(testProject: testProjectConfig) { - const folderUri = vscode.Uri.joinPath(workspaceUri, testProject.path) - const mockFileUri = vscode.Uri.joinPath(folderUri, testProject.mockFile) - await toTextEditor(testProject.mockContent, testProject.mockFile, folderUri.path) - return mockFileUri - }, - - getRandomTestProject() { - const randomIndex = Math.floor(Math.random() * testProjects.length) - return testProjects[randomIndex] - }, - async setupTest() { - tab = framework.createTab() - tab.addChatMessage({ command: '/doc' }) - tab = framework.getSelectedTab() - await tab.waitForChatFinishesLoading() - }, - } - /** - * Executes a test method with automatic retry capability for retryable errors. - * Uses Promise.race to detect errors during test execution without hanging. - */ - async function retryIfRequired(testMethod: () => Promise, maxAttempts: number = 3) { - const errorMessages = { - tooManyRequests: 'Too many requests', - unexpectedError: 'Encountered an unexpected error when processing the request', - } - const hasRetryableError = () => { - const lastTwoMessages = tab - .getChatItems() - .slice(-2) - .map((item) => item.body) - return lastTwoMessages.some( - (body) => body?.includes(errorMessages.unexpectedError) || body?.includes(errorMessages.tooManyRequests) - ) - } - for (let attempt = 1; attempt <= maxAttempts; attempt++) { - console.log(`Attempt ${attempt}/${maxAttempts}`) - const errorDetectionPromise = new Promise((_, reject) => { - const errorCheckInterval = setInterval(() => { - if (hasRetryableError()) { - clearInterval(errorCheckInterval) - reject(new Error('Retryable error detected')) - } - }, 1000) - }) - try { - await Promise.race([testMethod(), errorDetectionPromise]) - return - } catch (error) { - if (attempt === maxAttempts) { - assert.fail(`Test failed after ${maxAttempts} attempts`) - } - console.log(`Attempt ${attempt} failed, retrying...`) - await sleep(1000 * attempt) - await docUtils.setupTest() - } - } - } - before(async function () { - /** - * The tests are getting throttled, only run them on stable for now - * - * TODO: Re-enable for all versions once the backend can handle them - */ - - const testVersion = process.env['VSCODE_TEST_VERSION'] - if (testVersion && testVersion !== 'stable') { - this.skip() - } - - await using(registerAuthHook('amazonq-test-account'), async () => { - await loginToIdC() - }) - }) - - beforeEach(() => { - registerAuthHook('amazonq-test-account') - framework = new qTestingFramework('doc', true, []) - tab = framework.createTab() - const wsFolders = vscode.workspace.workspaceFolders - if (!wsFolders?.length) { - assert.fail('Workspace folder not found') - } - workspaceUri = wsFolders[0].uri - rootReadmeFileUri = vscode.Uri.joinPath(workspaceUri, 'README.md') - }) - - afterEach(() => { - framework.removeTab(tab.tabID) - framework.dispose() - }) - - describe('Quick action availability', () => { - it('Shows /doc command when doc generation is enabled', async () => { - const command = tab.findCommand('/doc') - if (!command.length) { - assert.fail('Could not find command') - } - - if (command.length > 1) { - assert.fail('Found too many commands with the name /doc') - } - }) - - it('Hide /doc command when doc generation is NOT enabled', () => { - // The beforeEach registers a framework which accepts requests. If we don't dispose before building a new one we have duplicate messages - framework.dispose() - framework = new qTestingFramework('doc', false, []) - const tab = framework.createTab() - const command = tab.findCommand('/doc') - if (command.length > 0) { - assert.fail('Found command when it should not have been found') - } - }) - }) - - describe('/doc entry', () => { - beforeEach(async function () { - await docUtils.setupTest() - }) - - it('Display create and update options on initial load', async () => { - await tab.waitForButtons([FollowUpTypes.CreateDocumentation, FollowUpTypes.UpdateDocumentation]) - }) - it('Return to the select create or update documentation state when cancel button clicked', async () => { - await tab.waitForButtons([FollowUpTypes.CreateDocumentation, FollowUpTypes.UpdateDocumentation]) - tab.clickButton(FollowUpTypes.UpdateDocumentation) - await tab.waitForButtons([FollowUpTypes.SynchronizeDocumentation, FollowUpTypes.EditDocumentation]) - tab.clickButton(FollowUpTypes.SynchronizeDocumentation) - await tab.waitForButtons([ - FollowUpTypes.ProceedFolderSelection, - FollowUpTypes.ChooseFolder, - FollowUpTypes.CancelFolderSelection, - ]) - tab.clickButton(FollowUpTypes.CancelFolderSelection) - await tab.waitForChatFinishesLoading() - const followupButton = tab.getFollowUpButton(FollowUpTypes.CreateDocumentation) - if (!followupButton) { - assert.fail('Could not find follow up button for create or update readme') - } - }) - }) - - describe('README Creation', () => { - let testProject: testProjectConfig - beforeEach(async function () { - await docUtils.setupTest() - testProject = docUtils.getRandomTestProject() - }) - - it('Create and save README in root folder when accepted', async () => { - await retryIfRequired(async () => { - await docUtils.initializeDocOperation('create') - await docUtils.executeDocumentationFlow('create') - await docUtils.verifyResult(FollowUpTypes.AcceptChanges, rootReadmeFileUri, true) - }) - }) - it('Create and save README in subfolder when accepted', async () => { - await retryIfRequired(async () => { - await docUtils.initializeDocOperation('create') - const readmeFileUri = await docUtils.handleFolderSelection(testProject) - await docUtils.executeDocumentationFlow('create') - await docUtils.verifyResult(FollowUpTypes.AcceptChanges, readmeFileUri, true) - }) - }) - - it('Discard README in subfolder when rejected', async () => { - await retryIfRequired(async () => { - await docUtils.initializeDocOperation('create') - const readmeFileUri = await docUtils.handleFolderSelection(testProject) - await docUtils.executeDocumentationFlow('create') - await docUtils.verifyResult(FollowUpTypes.RejectChanges, readmeFileUri, false) - }) - }) - }) - - describe('README Editing', () => { - beforeEach(async function () { - await docUtils.setupTest() - }) - - it('Apply specific content changes when requested', async () => { - await retryIfRequired(async () => { - await docUtils.initializeDocOperation('edit') - await docUtils.executeDocumentationFlow('edit', 'remove the repository structure section') - await docUtils.verifyResult(FollowUpTypes.AcceptChanges, rootReadmeFileUri, true) - }) - }) - - it('Handle unrelated prompts with appropriate error message', async () => { - await retryIfRequired(async () => { - await docUtils.initializeDocOperation('edit') - await tab.waitForButtons([FollowUpTypes.ProceedFolderSelection]) - tab.clickButton(FollowUpTypes.ProceedFolderSelection) - tab.addChatMessage({ prompt: 'tell me about the weather' }) - await tab.waitForEvent(() => - tab - .getChatItems() - .some(({ body }) => body?.startsWith(i18n('AWS.amazonq.doc.error.promptUnrelated'))) - ) - await tab.waitForEvent(() => { - const store = tab.getStore() - return ( - !store.promptInputDisabledState && - store.promptInputPlaceholder === i18n('AWS.amazonq.doc.placeholder.editReadme') - ) - }) - }) - }) - }) - describe('README Updates', () => { - let testProject: testProjectConfig - let mockFileUri: vscode.Uri - - beforeEach(async function () { - await docUtils.setupTest() - testProject = docUtils.getRandomTestProject() - }) - afterEach(async function () { - // Clean up mock file - if (mockFileUri) { - await fs.delete(mockFileUri, { force: true }) - } - }) - - it('Update README with code change in subfolder', async () => { - mockFileUri = await docUtils.prepareMockFile(testProject) - await retryIfRequired(async () => { - await docUtils.initializeDocOperation('update') - const readmeFileUri = await docUtils.handleFolderSelection(testProject) - await docUtils.executeDocumentationFlow('update') - await docUtils.verifyResult(FollowUpTypes.AcceptChanges, readmeFileUri, true) - }) - }) - it('Update root README and incorporate additional changes', async () => { - // Cleanup any existing README - await fs.delete(rootReadmeFileUri, { force: true }) - mockFileUri = await docUtils.prepareMockFile(testProject) - await retryIfRequired(async () => { - await docUtils.initializeDocOperation('update') - await docUtils.executeDocumentationFlow('update') - tab.clickButton(FollowUpTypes.MakeChanges) - tab.addChatMessage({ prompt: 'remove the repository structure section' }) - - await tab.waitForText(docGenerationProgressMessage(DocGenerationStep.SUMMARIZING_FILES, Mode.SYNC)) - await tab.waitForText( - `${docGenerationSuccessMessage(Mode.SYNC)} ${i18n('AWS.amazonq.doc.answer.codeResult')}` - ) - await tab.waitForButtons([ - FollowUpTypes.AcceptChanges, - FollowUpTypes.MakeChanges, - FollowUpTypes.RejectChanges, - ]) - - await docUtils.verifyResult(FollowUpTypes.AcceptChanges, rootReadmeFileUri, true) - }) - }) - }) -}) diff --git a/packages/amazonq/test/e2e/amazonq/featureDev.test.ts b/packages/amazonq/test/e2e/amazonq/featureDev.test.ts deleted file mode 100644 index 87099e2a2d0..00000000000 --- a/packages/amazonq/test/e2e/amazonq/featureDev.test.ts +++ /dev/null @@ -1,345 +0,0 @@ -/*! - * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. - * SPDX-License-Identifier: Apache-2.0 - */ - -import assert from 'assert' -import { qTestingFramework } from './framework/framework' -import sinon from 'sinon' -import { registerAuthHook, using } from 'aws-core-vscode/test' -import { loginToIdC } from './utils/setup' -import { Messenger } from './framework/messenger' -import { FollowUpTypes } from 'aws-core-vscode/amazonq' -import { sleep } from 'aws-core-vscode/shared' - -describe('Amazon Q Feature Dev', function () { - let framework: qTestingFramework - let tab: Messenger - - const prompt = 'Add current timestamp into blank.txt' - const iteratePrompt = `Add a new section in readme to explain your change` - const fileLevelAcceptPrompt = `${prompt} and ${iteratePrompt}` - const informationCard = - 'After you provide a task, I will:\n1. Generate code based on your description and the code in your workspace\n2. Provide a list of suggestions for you to review and add to your workspace\n3. If needed, iterate based on your feedback\nTo learn more, visit the [user guide](https://docs.aws.amazon.com/amazonq/latest/qdeveloper-ug/software-dev.html)' - const tooManyRequestsWaitTime = 100000 - - async function waitForText(text: string) { - await tab.waitForText(text, { - waitIntervalInMs: 250, - waitTimeoutInMs: 2000, - }) - } - - async function iterate(prompt: string) { - tab.addChatMessage({ prompt }) - - await retryIfRequired( - async () => { - // Wait for a backend response - await tab.waitForChatFinishesLoading() - }, - () => {} - ) - } - - async function clickActionButton(filePath: string, actionName: string) { - tab.clickFileActionButton(filePath, actionName) - await tab.waitForEvent(() => !tab.hasAction(filePath, actionName), { - waitIntervalInMs: 500, - waitTimeoutInMs: 600000, - }) - } - - /** - * Wait for the original request to finish. - * If the response has a retry button or encountered a guardrails error, continue retrying - * - * This allows the e2e tests to recover from potential one off backend problems/random guardrails - */ - async function retryIfRequired(waitUntilReady: () => Promise, request?: () => void) { - await waitUntilReady() - - const findAnotherTopic = 'find another topic to discuss' - const tooManyRequests = 'Too many requests' - const failureState = (message: string) => { - return ( - tab.getChatItems().pop()?.body?.includes(message) || - tab.getChatItems().slice(-2).shift()?.body?.includes(message) - ) - } - while ( - tab.hasButton(FollowUpTypes.Retry) || - (request && (failureState(findAnotherTopic) || failureState(tooManyRequests))) - ) { - if (tab.hasButton(FollowUpTypes.Retry)) { - console.log('Retrying request') - tab.clickButton(FollowUpTypes.Retry) - await waitUntilReady() - } else if (failureState(tooManyRequests)) { - // 3 versions of the e2e tests are running at the same time in the ci so we occassionally need to wait before continuing - request && request() - await sleep(tooManyRequestsWaitTime) - } else { - // We've hit guardrails, re-make the request and wait again - request && request() - await waitUntilReady() - } - } - - // The backend never recovered - if (tab.hasButton(FollowUpTypes.SendFeedback)) { - assert.fail('Encountered an error when attempting to call the feature dev backend. Could not continue') - } - } - - before(async function () { - /** - * The tests are getting throttled, only run them on stable for now - * - * TODO: Re-enable for all versions once the backend can handle them - */ - const testVersion = process.env['VSCODE_TEST_VERSION'] - if (testVersion && testVersion !== 'stable') { - this.skip() - } - - await using(registerAuthHook('amazonq-test-account'), async () => { - await loginToIdC() - }) - }) - - beforeEach(() => { - registerAuthHook('amazonq-test-account') - framework = new qTestingFramework('featuredev', true, []) - tab = framework.createTab() - }) - - afterEach(() => { - framework.removeTab(tab.tabID) - framework.dispose() - sinon.restore() - }) - - describe('Quick action availability', () => { - it('Shows /dev when feature dev is enabled', async () => { - const command = tab.findCommand('/dev') - if (!command) { - assert.fail('Could not find command') - } - - if (command.length > 1) { - assert.fail('Found too many commands with the name /dev') - } - }) - - it('Does NOT show /dev when feature dev is NOT enabled', () => { - // The beforeEach registers a framework which accepts requests. If we don't dispose before building a new one we have duplicate messages - framework.dispose() - framework = new qTestingFramework('featuredev', false, []) - const tab = framework.createTab() - const command = tab.findCommand('/dev') - if (command.length > 0) { - assert.fail('Found command when it should not have been found') - } - }) - }) - - describe('/dev entry', () => { - before(async () => { - tab = framework.createTab() - tab.addChatMessage({ command: '/dev' }) // This would create a new tab for feature dev. - tab = framework.getSelectedTab() - }) - - it('should display information card', async () => { - await retryIfRequired( - async () => { - await tab.waitForChatFinishesLoading() - }, - () => { - const lastChatItems = tab.getChatItems().pop() - assert.deepStrictEqual(lastChatItems?.body, informationCard) - } - ) - }) - }) - - describe('/dev {msg} entry', async () => { - beforeEach(async function () { - const isMultiIterationTestsEnabled = process.env['AMAZONQ_FEATUREDEV_ITERATION_TEST'] // Controls whether to enable multiple iteration testing for Amazon Q feature development - if (!isMultiIterationTestsEnabled) { - this.skip() - } else { - this.timeout(900000) // Code Gen with multi-iterations requires longer than default timeout(5 mins). - } - tab = framework.createTab() - tab.addChatMessage({ command: '/dev', prompt }) - tab = framework.getSelectedTab() - await retryIfRequired( - async () => { - await tab.waitForChatFinishesLoading() - }, - () => {} - ) - }) - - afterEach(async function () { - // currentTest.state is undefined if a beforeEach fails - if ( - this.currentTest?.state === undefined || - this.currentTest?.isFailed() || - this.currentTest?.isPending() - ) { - // Since the tests are long running this may help in diagnosing the issue - console.log('Current chat items at failure') - console.log(JSON.stringify(tab.getChatItems(), undefined, 4)) - } - }) - - it('Clicks accept code and click new task', async () => { - await retryIfRequired(async () => { - await Promise.any([ - tab.waitForButtons([FollowUpTypes.InsertCode, FollowUpTypes.ProvideFeedbackAndRegenerateCode]), - tab.waitForButtons([FollowUpTypes.Retry]), - ]) - }) - tab.clickButton(FollowUpTypes.InsertCode) - await tab.waitForButtons([FollowUpTypes.NewTask, FollowUpTypes.CloseSession]) - tab.clickButton(FollowUpTypes.NewTask) - await waitForText('What new task would you like to work on?') - assert.deepStrictEqual(tab.getChatItems().pop()?.body, 'What new task would you like to work on?') - }) - - it('Iterates on codegen', async () => { - await retryIfRequired(async () => { - await Promise.any([ - tab.waitForButtons([FollowUpTypes.InsertCode, FollowUpTypes.ProvideFeedbackAndRegenerateCode]), - tab.waitForButtons([FollowUpTypes.Retry]), - ]) - }) - tab.clickButton(FollowUpTypes.ProvideFeedbackAndRegenerateCode) - await tab.waitForChatFinishesLoading() - await iterate(iteratePrompt) - tab.clickButton(FollowUpTypes.InsertCode) - await tab.waitForButtons([FollowUpTypes.NewTask, FollowUpTypes.CloseSession]) - }) - }) - - describe('file-level accepts', async () => { - beforeEach(async function () { - tab = framework.createTab() - tab.addChatMessage({ command: '/dev', prompt: fileLevelAcceptPrompt }) - tab = framework.getSelectedTab() - await retryIfRequired( - async () => { - await tab.waitForChatFinishesLoading() - }, - () => { - tab.addChatMessage({ prompt }) - } - ) - await retryIfRequired(async () => { - await Promise.any([ - tab.waitForButtons([FollowUpTypes.InsertCode, FollowUpTypes.ProvideFeedbackAndRegenerateCode]), - tab.waitForButtons([FollowUpTypes.Retry]), - ]) - }) - }) - - describe('fileList', async () => { - it('has both accept-change and reject-change action buttons for file', async () => { - const filePath = tab.getFilePaths()[0] - assert.ok(tab.getActionsByFilePath(filePath).length === 2) - assert.ok(tab.hasAction(filePath, 'accept-change')) - assert.ok(tab.hasAction(filePath, 'reject-change')) - }) - - it('has only revert-rejection action button for rejected file', async () => { - const filePath = tab.getFilePaths()[0] - await clickActionButton(filePath, 'reject-change') - - assert.ok(tab.getActionsByFilePath(filePath).length === 1) - assert.ok(tab.hasAction(filePath, 'revert-rejection')) - }) - - it('does not have any of the action buttons for accepted file', async () => { - const filePath = tab.getFilePaths()[0] - await clickActionButton(filePath, 'accept-change') - - assert.ok(tab.getActionsByFilePath(filePath).length === 0) - }) - - it('disables all action buttons when new task is clicked', async () => { - tab.clickButton(FollowUpTypes.InsertCode) - await tab.waitForButtons([FollowUpTypes.NewTask, FollowUpTypes.CloseSession]) - tab.clickButton(FollowUpTypes.NewTask) - await waitForText('What new task would you like to work on?') - - const filePaths = tab.getFilePaths() - for (const filePath of filePaths) { - assert.ok(tab.getActionsByFilePath(filePath).length === 0) - } - }) - - it('disables all action buttons when close session is clicked', async () => { - tab.clickButton(FollowUpTypes.InsertCode) - await tab.waitForButtons([FollowUpTypes.NewTask, FollowUpTypes.CloseSession]) - tab.clickButton(FollowUpTypes.CloseSession) - await waitForText( - "Okay, I've ended this chat session. You can open a new tab to chat or start another workflow." - ) - - const filePaths = tab.getFilePaths() - for (const filePath of filePaths) { - assert.ok(tab.getActionsByFilePath(filePath).length === 0) - } - }) - }) - - describe('accept button', async () => { - describe('button text', async () => { - it('shows "Accept all changes" when no files are accepted or rejected, and "Accept remaining changes" otherwise', async () => { - let insertCodeButton = tab.getFollowUpButton(FollowUpTypes.InsertCode) - assert.ok(insertCodeButton.pillText === 'Accept all changes') - - const filePath = tab.getFilePaths()[0] - await clickActionButton(filePath, 'reject-change') - - insertCodeButton = tab.getFollowUpButton(FollowUpTypes.InsertCode) - assert.ok(insertCodeButton.pillText === 'Accept remaining changes') - - await clickActionButton(filePath, 'revert-rejection') - - insertCodeButton = tab.getFollowUpButton(FollowUpTypes.InsertCode) - assert.ok(insertCodeButton.pillText === 'Accept all changes') - - await clickActionButton(filePath, 'accept-change') - - insertCodeButton = tab.getFollowUpButton(FollowUpTypes.InsertCode) - assert.ok(insertCodeButton.pillText === 'Accept remaining changes') - }) - - it('shows "Continue" when all files are either accepted or rejected, with at least one of them rejected', async () => { - const filePaths = tab.getFilePaths() - for (const filePath of filePaths) { - await clickActionButton(filePath, 'reject-change') - } - - const insertCodeButton = tab.getFollowUpButton(FollowUpTypes.InsertCode) - assert.ok(insertCodeButton.pillText === 'Continue') - }) - }) - - it('disappears and automatically moves on to the next step when all changes are accepted', async () => { - const filePaths = tab.getFilePaths() - for (const filePath of filePaths) { - await clickActionButton(filePath, 'accept-change') - } - await tab.waitForButtons([FollowUpTypes.NewTask, FollowUpTypes.CloseSession]) - - assert.ok(tab.hasButton(FollowUpTypes.InsertCode) === false) - assert.ok(tab.hasButton(FollowUpTypes.ProvideFeedbackAndRegenerateCode) === false) - }) - }) - }) -}) diff --git a/packages/amazonq/test/e2e/amazonq/testGen.test.ts b/packages/amazonq/test/e2e/amazonq/testGen.test.ts deleted file mode 100644 index 21db83fd6e8..00000000000 --- a/packages/amazonq/test/e2e/amazonq/testGen.test.ts +++ /dev/null @@ -1,209 +0,0 @@ -/*! - * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. - * SPDX-License-Identifier: Apache-2.0 - */ - -import assert from 'assert' -import vscode from 'vscode' -import { qTestingFramework } from './framework/framework' -import sinon from 'sinon' -import { Messenger } from './framework/messenger' -import { FollowUpTypes } from 'aws-core-vscode/amazonq' -import { registerAuthHook, using, TestFolder, closeAllEditors, getTestWorkspaceFolder } from 'aws-core-vscode/test' -import { loginToIdC } from './utils/setup' -import { waitUntil, workspaceUtils } from 'aws-core-vscode/shared' -import * as path from 'path' - -describe('Amazon Q Test Generation', function () { - let framework: qTestingFramework - let tab: Messenger - - const testFiles = [ - { - language: 'python', - filePath: 'testGenFolder/src/main/math.py', - testFilePath: 'testGenFolder/src/test/test_math.py', - }, - { - language: 'java', - filePath: 'testGenFolder/src/main/Math.java', - testFilePath: 'testGenFolder/src/test/MathTest.java', - }, - ] - - // handles opening the file since /test must be called on an active file - async function setupTestDocument(filePath: string, language: string) { - const document = await waitUntil(async () => { - const doc = await workspaceUtils.openTextDocument(filePath) - return doc - }, {}) - - if (!document) { - assert.fail(`Failed to open ${language} file`) - } - - await waitUntil(async () => { - await vscode.window.showTextDocument(document, { preview: false }) - }, {}) - - const activeEditor = vscode.window.activeTextEditor - if (!activeEditor || activeEditor.document.uri.fsPath !== document.uri.fsPath) { - assert.fail(`Failed to make ${language} file active`) - } - } - - async function waitForChatItems(index: number) { - await tab.waitForEvent(() => tab.getChatItems().length > index, { - waitTimeoutInMs: 5000, - waitIntervalInMs: 1000, - }) - } - - // clears test file to a blank file - // not cleaning up test file may possibly cause bloat in CI since testFixtures does not get reset - async function cleanupTestFile(testFilePath: string) { - const workspaceFolder = getTestWorkspaceFolder() - const absoluteTestFilePath = path.join(workspaceFolder, testFilePath) - const testFileUri = vscode.Uri.file(absoluteTestFilePath) - await vscode.workspace.fs.writeFile(testFileUri, Buffer.from('', 'utf-8')) - } - - before(async function () { - await using(registerAuthHook('amazonq-test-account'), async () => { - await loginToIdC() - }) - }) - - beforeEach(async () => { - registerAuthHook('amazonq-test-account') - framework = new qTestingFramework('testgen', true, []) - tab = framework.createTab() - }) - - afterEach(async () => { - // Close all editors to prevent conflicts with subsequent tests trying to open the same file - await closeAllEditors() - framework.removeTab(tab.tabID) - framework.dispose() - sinon.restore() - }) - - describe('Quick action availability', () => { - it('Shows /test when test generation is enabled', async () => { - const command = tab.findCommand('/test') - if (!command.length) { - assert.fail('Could not find command') - } - if (command.length > 1) { - assert.fail('Found too many commands with the name /test') - } - }) - - it('Does NOT show /test when test generation is NOT enabled', () => { - // The beforeEach registers a framework which accepts requests. If we don't dispose before building a new one we have duplicate messages - framework.dispose() - framework = new qTestingFramework('testgen', false, []) - const tab = framework.createTab() - const command = tab.findCommand('/test') - if (command.length > 0) { - assert.fail('Found command when it should not have been found') - } - }) - }) - - describe('/test entry', () => { - describe('External file out of project', async () => { - let testFolder: TestFolder - let fileName: string - - beforeEach(async () => { - testFolder = await TestFolder.create() - fileName = 'math.py' - const filePath = await testFolder.write(fileName, 'def add(a, b): return a + b') - - const document = await vscode.workspace.openTextDocument(filePath) - await vscode.window.showTextDocument(document, { preview: false }) - }) - - it('/test for external file redirects to chat', async () => { - tab.addChatMessage({ command: '/test' }) - await tab.waitForChatFinishesLoading() - - await waitForChatItems(3) - const externalFileMessage = tab.getChatItems()[3] - - assert.deepStrictEqual(externalFileMessage.type, 'answer') - assert.deepStrictEqual( - externalFileMessage.body, - `I can't generate tests for ${fileName} because the file is outside of workspace scope.
I can still provide examples, instructions and code suggestions.` - ) - }) - }) - - for (const { language, filePath, testFilePath } of testFiles) { - describe(`/test on ${language} file`, () => { - beforeEach(async () => { - await waitUntil(async () => await setupTestDocument(filePath, language), {}) - - tab.addChatMessage({ command: '/test' }) - await tab.waitForChatFinishesLoading() - - await tab.waitForButtons([FollowUpTypes.ViewDiff]) - tab.clickButton(FollowUpTypes.ViewDiff) - await tab.waitForChatFinishesLoading() - }) - - describe('View diff of test file', async () => { - it('Clicks on view diff', async () => { - const chatItems = tab.getChatItems() - const viewDiffMessage = chatItems[5] - - assert.deepStrictEqual(viewDiffMessage.type, 'answer') - const expectedEnding = - 'Please see the unit tests generated below. Click “View diff” to review the changes in the code editor.' - assert.strictEqual( - viewDiffMessage.body?.includes(expectedEnding), - true, - `View diff message does not contain phrase: ${expectedEnding}` - ) - }) - }) - - describe('Accept unit tests', async () => { - afterEach(async () => { - // this e2e test generates unit tests, so we want to clean them up after this test is done - await waitUntil(async () => { - await cleanupTestFile(testFilePath) - }, {}) - }) - - it('Clicks on accept', async () => { - await tab.waitForButtons([FollowUpTypes.AcceptCode, FollowUpTypes.RejectCode]) - tab.clickButton(FollowUpTypes.AcceptCode) - await tab.waitForChatFinishesLoading() - - await waitForChatItems(7) - const acceptedMessage = tab.getChatItems()[7] - - assert.deepStrictEqual(acceptedMessage?.type, 'answer-part') - assert.deepStrictEqual(acceptedMessage?.followUp?.options?.[0].pillText, 'Accepted') - }) - }) - - describe('Reject unit tests', async () => { - it('Clicks on reject', async () => { - await tab.waitForButtons([FollowUpTypes.AcceptCode, FollowUpTypes.RejectCode]) - tab.clickButton(FollowUpTypes.RejectCode) - await tab.waitForChatFinishesLoading() - - await waitForChatItems(7) - const rejectedMessage = tab.getChatItems()[7] - - assert.deepStrictEqual(rejectedMessage?.type, 'answer-part') - assert.deepStrictEqual(rejectedMessage?.followUp?.options?.[0].pillText, 'Rejected') - }) - }) - }) - } - }) -}) diff --git a/packages/core/src/amazonq/indexNode.ts b/packages/core/src/amazonq/indexNode.ts index 88a3a4bba37..628b5d626cd 100644 --- a/packages/core/src/amazonq/indexNode.ts +++ b/packages/core/src/amazonq/indexNode.ts @@ -7,7 +7,6 @@ * These agents have underlying requirements on node dependencies (e.g. jsdom, admzip) */ export { init as cwChatAppInit } from '../codewhispererChat/app' -export { init as featureDevChatAppInit } from '../amazonqFeatureDev/app' +export { init as featureDevChatAppInit } from '../amazonqFeatureDev/app' // TODO: Remove this export { init as gumbyChatAppInit } from '../amazonqGumby/app' -export { init as testChatAppInit } from '../amazonqTest/app' -export { init as docChatAppInit } from '../amazonqDoc/app' +export { init as docChatAppInit } from '../amazonqDoc/app' // TODO: Remove this diff --git a/packages/core/src/amazonq/webview/ui/apps/amazonqCommonsConnector.ts b/packages/core/src/amazonq/webview/ui/apps/amazonqCommonsConnector.ts index 04ed6907795..68983b6c188 100644 --- a/packages/core/src/amazonq/webview/ui/apps/amazonqCommonsConnector.ts +++ b/packages/core/src/amazonq/webview/ui/apps/amazonqCommonsConnector.ts @@ -7,13 +7,7 @@ import { ChatItem, ChatItemAction, ChatItemType, ChatPrompt } from '@aws/mynah-u import { ExtensionMessage } from '../commands' import { AuthFollowUpType } from '../followUps/generator' import { getTabCommandFromTabType, isTabType, TabType } from '../storages/tabsStorage' -import { - docUserGuide, - userGuideURL as featureDevUserGuide, - helpMessage, - reviewGuideUrl, - testGuideUrl, -} from '../texts/constants' +import { helpMessage, reviewGuideUrl } from '../texts/constants' import { linkToDocsHome } from '../../../../codewhisperer/models/constants' import { createClickTelemetry, createOpenAgentTelemetry } from '../telemetry/actions' @@ -110,18 +104,9 @@ export class Connector { private processUserGuideLink(tabType: TabType, actionId: string) { let userGuideLink = '' switch (tabType) { - case 'featuredev': - userGuideLink = featureDevUserGuide - break - case 'testgen': - userGuideLink = testGuideUrl - break case 'review': userGuideLink = reviewGuideUrl break - case 'doc': - userGuideLink = docUserGuide - break case 'gumby': userGuideLink = linkToDocsHome break diff --git a/packages/core/src/amazonq/webview/ui/apps/docChatConnector.ts b/packages/core/src/amazonq/webview/ui/apps/docChatConnector.ts deleted file mode 100644 index 96822c8336c..00000000000 --- a/packages/core/src/amazonq/webview/ui/apps/docChatConnector.ts +++ /dev/null @@ -1,226 +0,0 @@ -/*! - * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. - * SPDX-License-Identifier: Apache-2.0 - */ - -import { ChatItem, ChatItemType, FeedbackPayload, MynahIcons, ProgressField } from '@aws/mynah-ui' -import { TabType } from '../storages/tabsStorage' -import { DiffTreeFileInfo } from '../diffTree/types' -import { BaseConnectorProps, BaseConnector } from './baseConnector' - -export interface ConnectorProps extends BaseConnectorProps { - onAsyncEventProgress: (tabID: string, inProgress: boolean, message: string) => void - sendFeedback?: (tabId: string, feedbackPayload: FeedbackPayload) => void | undefined - onFileComponentUpdate: ( - tabID: string, - filePaths: DiffTreeFileInfo[], - deletedFiles: DiffTreeFileInfo[], - messageId: string, - disableFileActions: boolean - ) => void - onFileActionClick: (tabID: string, messageId: string, filePath: string, actionName: string) => void - onUpdatePlaceholder: (tabID: string, newPlaceholder: string) => void - onUpdatePromptProgress: (tabID: string, progressField: ProgressField) => void - onChatInputEnabled: (tabID: string, enabled: boolean) => void - onUpdateAuthentication: (featureDevEnabled: boolean, authenticatingTabIDs: string[]) => void -} - -export class Connector extends BaseConnector { - private readonly onFileComponentUpdate - private readonly onAsyncEventProgress - private readonly updatePlaceholder - private readonly chatInputEnabled - private readonly onUpdateAuthentication - private readonly updatePromptProgress - - override getTabType(): TabType { - return 'doc' - } - - constructor(props: ConnectorProps) { - super(props) - this.onFileComponentUpdate = props.onFileComponentUpdate - this.onAsyncEventProgress = props.onAsyncEventProgress - this.updatePlaceholder = props.onUpdatePlaceholder - this.chatInputEnabled = props.onChatInputEnabled - this.onUpdateAuthentication = props.onUpdateAuthentication - this.updatePromptProgress = props.onUpdatePromptProgress - } - - onOpenDiff = (tabID: string, filePath: string, deleted: boolean): void => { - this.sendMessageToExtension({ - command: 'open-diff', - tabID, - filePath, - deleted, - tabType: this.getTabType(), - }) - } - onFileActionClick = (tabID: string, messageId: string, filePath: string, actionName: string): void => { - this.sendMessageToExtension({ - command: 'file-click', - tabID, - messageId, - filePath, - actionName, - tabType: this.getTabType(), - }) - } - - private processFolderConfirmationMessage = async (messageData: any, folderPath: string): Promise => { - if (this.onChatAnswerReceived !== undefined) { - const answer: ChatItem = { - type: ChatItemType.ANSWER, - body: messageData.message ?? undefined, - messageId: messageData.messageID ?? messageData.triggerID ?? '', - fileList: { - rootFolderTitle: undefined, - fileTreeTitle: '', - filePaths: [folderPath], - details: { - [folderPath]: { - icon: MynahIcons.FOLDER, - clickable: false, - }, - }, - }, - followUp: { - text: '', - options: messageData.followUps, - }, - } - this.onChatAnswerReceived(messageData.tabID, answer, messageData) - } - } - - private processChatMessage = async (messageData: any): Promise => { - if (this.onChatAnswerReceived !== undefined) { - const answer: ChatItem = { - type: messageData.messageType, - body: messageData.message ?? undefined, - messageId: messageData.messageID ?? messageData.triggerID ?? '', - relatedContent: undefined, - canBeVoted: messageData.canBeVoted, - snapToTop: messageData.snapToTop, - followUp: - messageData.followUps !== undefined && messageData.followUps.length > 0 - ? { - text: - messageData.messageType === ChatItemType.SYSTEM_PROMPT - ? '' - : 'Select one of the following...', - options: messageData.followUps, - } - : undefined, - } - this.onChatAnswerReceived(messageData.tabID, answer, messageData) - } - } - - private processCodeResultMessage = async (messageData: any): Promise => { - if (this.onChatAnswerReceived !== undefined) { - const answer: ChatItem = { - type: ChatItemType.ANSWER, - relatedContent: undefined, - followUp: undefined, - canBeVoted: false, - codeReference: messageData.references, - // TODO get the backend to store a message id in addition to conversationID - messageId: - messageData.codeGenerationId ?? - messageData.messageID ?? - messageData.triggerID ?? - messageData.conversationID, - fileList: { - rootFolderTitle: 'Documentation', - fileTreeTitle: 'Documents ready', - filePaths: messageData.filePaths.map((f: DiffTreeFileInfo) => f.zipFilePath), - deletedFiles: messageData.deletedFiles.map((f: DiffTreeFileInfo) => f.zipFilePath), - }, - body: '', - } - this.onChatAnswerReceived(messageData.tabID, answer, messageData) - } - } - - handleMessageReceive = async (messageData: any): Promise => { - if (messageData.type === 'updateFileComponent') { - this.onFileComponentUpdate( - messageData.tabID, - messageData.filePaths, - messageData.deletedFiles, - messageData.messageId, - messageData.disableFileActions - ) - return - } - - if (messageData.type === 'chatMessage') { - await this.processChatMessage(messageData) - return - } - - if (messageData.type === 'folderConfirmationMessage') { - await this.processFolderConfirmationMessage(messageData, messageData.folderPath) - return - } - - if (messageData.type === 'codeResultMessage') { - await this.processCodeResultMessage(messageData) - return - } - - if (messageData.type === 'asyncEventProgressMessage') { - this.onAsyncEventProgress(messageData.tabID, messageData.inProgress, messageData.message ?? undefined) - return - } - - if (messageData.type === 'updatePlaceholderMessage') { - this.updatePlaceholder(messageData.tabID, messageData.newPlaceholder) - return - } - - if (messageData.type === 'chatInputEnabledMessage') { - this.chatInputEnabled(messageData.tabID, messageData.enabled) - return - } - - if (messageData.type === 'authenticationUpdateMessage') { - this.onUpdateAuthentication(messageData.featureEnabled, messageData.authenticatingTabIDs) - return - } - - if (messageData.type === 'openNewTabMessage') { - this.onNewTab(this.getTabType()) - return - } - - if (messageData.type === 'updatePromptProgress') { - this.updatePromptProgress(messageData.tabID, messageData.progressField) - return - } - - // For other message types, call the base class handleMessageReceive - await this.baseHandleMessageReceive(messageData) - } - - onCustomFormAction( - tabId: string, - action: { - id: string - text?: string | undefined - formItemValues?: Record | undefined - } - ) { - if (action === undefined) { - return - } - this.sendMessageToExtension({ - command: 'form-action-click', - action: action.id, - formSelectedValues: action.formItemValues, - tabType: 'doc', - tabID: tabId, - }) - } -} diff --git a/packages/core/src/amazonq/webview/ui/apps/featureDevChatConnector.ts b/packages/core/src/amazonq/webview/ui/apps/featureDevChatConnector.ts deleted file mode 100644 index 1f6d33a1ec4..00000000000 --- a/packages/core/src/amazonq/webview/ui/apps/featureDevChatConnector.ts +++ /dev/null @@ -1,212 +0,0 @@ -/*! - * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. - * SPDX-License-Identifier: Apache-2.0 - */ - -import { ChatItem, ChatItemType, FeedbackPayload } from '@aws/mynah-ui' -import { TabType } from '../storages/tabsStorage' -import { getActions } from '../diffTree/actions' -import { DiffTreeFileInfo } from '../diffTree/types' -import { BaseConnector, BaseConnectorProps } from './baseConnector' - -export interface ConnectorProps extends BaseConnectorProps { - onAsyncEventProgress: ( - tabID: string, - inProgress: boolean, - message: string, - messageId: string | undefined, - enableStopAction: boolean - ) => void - onChatAnswerUpdated?: (tabID: string, message: ChatItem) => void - sendFeedback?: (tabId: string, feedbackPayload: FeedbackPayload) => void | undefined - onFileComponentUpdate: ( - tabID: string, - filePaths: DiffTreeFileInfo[], - deletedFiles: DiffTreeFileInfo[], - messageId: string, - disableFileActions: boolean - ) => void - onFileActionClick: (tabID: string, messageId: string, filePath: string, actionName: string) => void - onUpdatePlaceholder: (tabID: string, newPlaceholder: string) => void - onChatInputEnabled: (tabID: string, enabled: boolean) => void - onUpdateAuthentication: (featureDevEnabled: boolean, authenticatingTabIDs: string[]) => void -} - -export class Connector extends BaseConnector { - private readonly onFileComponentUpdate - private readonly onChatAnswerUpdated - private readonly onAsyncEventProgress - private readonly updatePlaceholder - private readonly chatInputEnabled - private readonly onUpdateAuthentication - - override getTabType(): TabType { - return 'featuredev' - } - - constructor(props: ConnectorProps) { - super(props) - this.onFileComponentUpdate = props.onFileComponentUpdate - this.onAsyncEventProgress = props.onAsyncEventProgress - this.updatePlaceholder = props.onUpdatePlaceholder - this.chatInputEnabled = props.onChatInputEnabled - this.onUpdateAuthentication = props.onUpdateAuthentication - this.onChatAnswerUpdated = props.onChatAnswerUpdated - } - - onOpenDiff = (tabID: string, filePath: string, deleted: boolean, messageId?: string): void => { - this.sendMessageToExtension({ - command: 'open-diff', - tabID, - filePath, - deleted, - messageId, - tabType: this.getTabType(), - }) - } - onFileActionClick = (tabID: string, messageId: string, filePath: string, actionName: string): void => { - this.sendMessageToExtension({ - command: 'file-click', - tabID, - messageId, - filePath, - actionName, - tabType: this.getTabType(), - }) - } - - private createAnswer = (messageData: any): ChatItem => { - return { - type: messageData.messageType, - body: messageData.message ?? undefined, - messageId: messageData.messageId ?? messageData.messageID ?? messageData.triggerID ?? '', - relatedContent: undefined, - canBeVoted: messageData.canBeVoted ?? undefined, - snapToTop: messageData.snapToTop ?? undefined, - followUp: - messageData.followUps !== undefined && Array.isArray(messageData.followUps) - ? { - text: - messageData.messageType === ChatItemType.SYSTEM_PROMPT || - messageData.followUps.length === 0 - ? '' - : 'Please follow up with one of these', - options: messageData.followUps, - } - : undefined, - } - } - - private processChatMessage = async (messageData: any): Promise => { - if (this.onChatAnswerReceived !== undefined) { - const answer = this.createAnswer(messageData) - this.onChatAnswerReceived(messageData.tabID, answer, messageData) - } - } - - private processCodeResultMessage = async (messageData: any): Promise => { - if (this.onChatAnswerReceived !== undefined) { - const messageId = - messageData.codeGenerationId ?? - messageData.messageId ?? - messageData.messageID ?? - messageData.triggerID ?? - messageData.conversationID - this.sendMessageToExtension({ - tabID: messageData.tabID, - command: 'store-code-result-message-id', - messageId, - tabType: 'featuredev', - }) - const actions = getActions([...messageData.filePaths, ...messageData.deletedFiles]) - const answer: ChatItem = { - type: ChatItemType.ANSWER, - relatedContent: undefined, - followUp: undefined, - canBeVoted: true, - codeReference: messageData.references, - messageId, - fileList: { - rootFolderTitle: 'Changes', - filePaths: messageData.filePaths.map((f: DiffTreeFileInfo) => f.zipFilePath), - deletedFiles: messageData.deletedFiles.map((f: DiffTreeFileInfo) => f.zipFilePath), - actions, - }, - body: '', - } - this.onChatAnswerReceived(messageData.tabID, answer, messageData) - } - } - - handleMessageReceive = async (messageData: any): Promise => { - if (messageData.type === 'updateFileComponent') { - this.onFileComponentUpdate( - messageData.tabID, - messageData.filePaths, - messageData.deletedFiles, - messageData.messageId, - messageData.disableFileActions - ) - return - } - if (messageData.type === 'updateChatAnswer') { - const answer = this.createAnswer(messageData) - this.onChatAnswerUpdated?.(messageData.tabID, answer) - return - } - - if (messageData.type === 'chatMessage') { - await this.processChatMessage(messageData) - return - } - - if (messageData.type === 'codeResultMessage') { - await this.processCodeResultMessage(messageData) - return - } - - if (messageData.type === 'asyncEventProgressMessage') { - const enableStopAction = true - this.onAsyncEventProgress( - messageData.tabID, - messageData.inProgress, - messageData.message ?? undefined, - messageData.messageId ?? undefined, - enableStopAction - ) - return - } - - if (messageData.type === 'updatePlaceholderMessage') { - this.updatePlaceholder(messageData.tabID, messageData.newPlaceholder) - return - } - - if (messageData.type === 'chatInputEnabledMessage') { - this.chatInputEnabled(messageData.tabID, messageData.enabled) - return - } - - if (messageData.type === 'authenticationUpdateMessage') { - this.onUpdateAuthentication(messageData.featureEnabled, messageData.authenticatingTabIDs) - return - } - - if (messageData.type === 'openNewTabMessage') { - this.onNewTab('featuredev') - return - } - - // For other message types, call the base class handleMessageReceive - await this.baseHandleMessageReceive(messageData) - } - - sendFeedback = (tabId: string, feedbackPayload: FeedbackPayload): void | undefined => { - this.sendMessageToExtension({ - command: 'chat-item-feedback', - ...feedbackPayload, - tabType: this.getTabType(), - tabID: tabId, - }) - } -} diff --git a/packages/core/src/amazonq/webview/ui/apps/testChatConnector.ts b/packages/core/src/amazonq/webview/ui/apps/testChatConnector.ts deleted file mode 100644 index 35fb0bc0683..00000000000 --- a/packages/core/src/amazonq/webview/ui/apps/testChatConnector.ts +++ /dev/null @@ -1,293 +0,0 @@ -/*! - * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. - * SPDX-License-Identifier: Apache-2.0 - * - * This class is responsible for listening to and processing events - * from the webview and translating them into events to be handled by the extension, - * and events from the extension and translating them into events to be handled by the webview. - */ - -import { ChatItem, ChatItemType, MynahIcons, ProgressField } from '@aws/mynah-ui' -import { ExtensionMessage } from '../commands' -import { TabsStorage, TabType } from '../storages/tabsStorage' -import { TestMessageType } from '../../../../amazonqTest/chat/views/connector/connector' -import { ChatPayload } from '../connector' -import { BaseConnector, BaseConnectorProps } from './baseConnector' -import { FollowUpTypes } from '../../../commons/types' - -export interface ConnectorProps extends BaseConnectorProps { - sendMessageToExtension: (message: ExtensionMessage) => void - onChatAnswerReceived?: (tabID: string, message: ChatItem, messageData: any) => void - onRunTestMessageReceived?: (tabID: string, showRunTestMessage: boolean) => void - onChatAnswerUpdated?: (tabID: string, message: ChatItem) => void - onQuickHandlerCommand: (tabID: string, command: string, eventId?: string) => void - onWarning: (tabID: string, message: string, title: string) => void - onError: (tabID: string, message: string, title: string) => void - onUpdateAuthentication: (testEnabled: boolean, authenticatingTabIDs: string[]) => void - onChatInputEnabled: (tabID: string, enabled: boolean) => void - onUpdatePlaceholder: (tabID: string, newPlaceholder: string) => void - onUpdatePromptProgress: (tabID: string, progressField: ProgressField) => void - tabsStorage: TabsStorage -} - -export interface MessageData { - tabID: string - type: TestMessageType -} -// TODO: Refactor testChatConnector, scanChatConnector and other apps connector files post RIV -export class Connector extends BaseConnector { - override getTabType(): TabType { - return 'testgen' - } - readonly onAuthenticationUpdate - override readonly sendMessageToExtension - override readonly onChatAnswerReceived - private readonly onChatAnswerUpdated - private readonly chatInputEnabled - private readonly updatePlaceholder - private readonly updatePromptProgress - override readonly onError - private readonly tabStorage - private readonly runTestMessageReceived - - constructor(props: ConnectorProps) { - super(props) - this.runTestMessageReceived = props.onRunTestMessageReceived - this.sendMessageToExtension = props.sendMessageToExtension - this.onChatAnswerReceived = props.onChatAnswerReceived - this.onChatAnswerUpdated = props.onChatAnswerUpdated - this.chatInputEnabled = props.onChatInputEnabled - this.updatePlaceholder = props.onUpdatePlaceholder - this.updatePromptProgress = props.onUpdatePromptProgress - this.onAuthenticationUpdate = props.onUpdateAuthentication - this.onError = props.onError - this.tabStorage = props.tabsStorage - } - - startTestGen(tabID: string, prompt: string) { - this.sendMessageToExtension({ - tabID: tabID, - command: 'start-test-gen', - tabType: 'testgen', - prompt, - }) - } - - requestAnswer = (tabID: string, payload: ChatPayload) => { - this.tabStorage.updateTabStatus(tabID, 'busy') - this.sendMessageToExtension({ - tabID: tabID, - command: 'chat-prompt', - chatMessage: payload.chatMessage, - chatCommand: payload.chatCommand, - tabType: 'testgen', - }) - } - - onCustomFormAction( - tabId: string, - messageId: string, - action: { - id: string - text?: string | undefined - description?: string | undefined - formItemValues?: Record | undefined - } - ) { - if (action === undefined) { - return - } - - this.sendMessageToExtension({ - command: 'form-action-click', - action: action.id, - formSelectedValues: action.formItemValues, - tabType: 'testgen', - tabID: tabId, - description: action.description, - }) - - if (this.onChatAnswerUpdated === undefined) { - return - } - const answer: ChatItem = { - type: ChatItemType.ANSWER, - messageId: messageId, - buttons: [], - } - // TODO: Add more cases for Accept/Reject/viewDiff. - switch (action.id) { - case 'Provide-Feedback': - answer.buttons = [ - { - keepCardAfterClick: true, - text: 'Thanks for providing feedback.', - id: 'utg_provided_feedback', - status: 'success', - position: 'outside', - disabled: true, - }, - ] - break - default: - break - } - this.onChatAnswerUpdated(tabId, answer) - } - - onFileDiff = (tabID: string, filePath: string, deleted: boolean, messageId?: string): void => { - if (this.onChatAnswerReceived === undefined) { - return - } - // Open diff view - this.sendMessageToExtension({ - command: 'open-diff', - tabID, - filePath, - deleted, - messageId, - tabType: 'testgen', - }) - this.onChatAnswerReceived( - tabID, - { - type: ChatItemType.ANSWER, - messageId: messageId, - followUp: { - text: ' ', - options: [ - { - type: FollowUpTypes.AcceptCode, - pillText: 'Accept', - status: 'success', - icon: MynahIcons.OK, - }, - { - type: FollowUpTypes.RejectCode, - pillText: 'Reject', - status: 'error', - icon: MynahIcons.REVERT, - }, - ], - }, - }, - {} - ) - } - - private processChatMessage = async (messageData: any): Promise => { - if (this.onChatAnswerReceived === undefined) { - return - } - if (messageData.command === 'test' && this.runTestMessageReceived) { - this.runTestMessageReceived(messageData.tabID, true) - return - } - if (messageData.message !== undefined) { - const answer: ChatItem = { - type: messageData.messageType, - messageId: messageData.messageId ?? messageData.triggerID, - body: messageData.message, - canBeVoted: false, - informationCard: messageData.informationCard, - buttons: messageData.buttons ?? [], - } - this.onChatAnswerReceived(messageData.tabID, answer, messageData) - } - } - // Displays the test generation summary message in the /test Tab before generating unit tests - private processChatSummaryMessage = async (messageData: any): Promise => { - if (this.onChatAnswerUpdated === undefined) { - return - } - if (messageData.message !== undefined) { - const answer: ChatItem = { - type: messageData.messageType, - messageId: messageData.messageId ?? messageData.triggerID, - body: messageData.message, - canBeVoted: true, - footer: messageData.filePath - ? { - fileList: { - rootFolderTitle: undefined, - fileTreeTitle: '', - filePaths: [messageData.filePath], - details: { - [messageData.filePath]: { - icon: MynahIcons.FILE, - description: `Generating tests in ${messageData.filePath}`, - }, - }, - }, - } - : {}, - } - this.onChatAnswerUpdated(messageData.tabID, answer) - } - } - - override processAuthNeededException = async (messageData: any): Promise => { - if (this.onChatAnswerReceived === undefined) { - return - } - - this.onChatAnswerReceived( - messageData.tabID, - { - type: ChatItemType.SYSTEM_PROMPT, - body: messageData.message, - }, - messageData - ) - } - - private processBuildProgressMessage = async ( - messageData: { type: TestMessageType } & Record - ): Promise => { - if (this.onChatAnswerReceived === undefined) { - return - } - const answer: ChatItem = { - type: messageData.messageType, - canBeVoted: messageData.canBeVoted, - messageId: messageData.messageId, - followUp: messageData.followUps, - fileList: messageData.fileList, - body: messageData.message, - codeReference: messageData.codeReference, - } - this.onChatAnswerReceived(messageData.tabID, answer, messageData) - } - - // This handles messages received from the extension, to be forwarded to the webview - handleMessageReceive = async (messageData: { type: TestMessageType } & Record) => { - switch (messageData.type) { - case 'authNeededException': - await this.processAuthNeededException(messageData) - break - case 'authenticationUpdateMessage': - this.onAuthenticationUpdate(messageData.testEnabled, messageData.authenticatingTabIDs) - break - case 'chatInputEnabledMessage': - this.chatInputEnabled(messageData.tabID, messageData.enabled) - break - case 'chatMessage': - await this.processChatMessage(messageData) - break - case 'chatSummaryMessage': - await this.processChatSummaryMessage(messageData) - break - case 'updatePlaceholderMessage': - this.updatePlaceholder(messageData.tabID, messageData.newPlaceholder) - break - case 'buildProgressMessage': - await this.processBuildProgressMessage(messageData) - break - case 'updatePromptProgress': - this.updatePromptProgress(messageData.tabID, messageData.progressField) - break - case 'errorMessage': - this.onError(messageData.tabID, messageData.message, messageData.title) - } - } -} diff --git a/packages/core/src/amazonq/webview/ui/connector.ts b/packages/core/src/amazonq/webview/ui/connector.ts index 1c31f6cc842..cc1b010375a 100644 --- a/packages/core/src/amazonq/webview/ui/connector.ts +++ b/packages/core/src/amazonq/webview/ui/connector.ts @@ -19,12 +19,9 @@ import { DetailedList, } from '@aws/mynah-ui' import { Connector as CWChatConnector } from './apps/cwChatConnector' -import { Connector as FeatureDevChatConnector } from './apps/featureDevChatConnector' import { Connector as AmazonQCommonsConnector } from './apps/amazonqCommonsConnector' import { Connector as GumbyChatConnector } from './apps/gumbyChatConnector' import { Connector as ScanChatConnector } from './apps/scanChatConnector' -import { Connector as TestChatConnector } from './apps/testChatConnector' -import { Connector as docChatConnector } from './apps/docChatConnector' import { ExtensionMessage } from './commands' import { TabType, TabsStorage } from './storages/tabsStorage' import { WelcomeFollowupType } from './apps/amazonqCommonsConnector' @@ -123,11 +120,8 @@ export class Connector { private readonly sendMessageToExtension private readonly onMessageReceived private readonly cwChatConnector - private readonly featureDevChatConnector private readonly gumbyChatConnector private readonly scanChatConnector - private readonly testChatConnector - private readonly docChatConnector private readonly tabsStorage private readonly amazonqCommonsConnector: AmazonQCommonsConnector @@ -137,11 +131,8 @@ export class Connector { this.sendMessageToExtension = props.sendMessageToExtension this.onMessageReceived = props.onMessageReceived this.cwChatConnector = new CWChatConnector(props as ConnectorProps) - this.featureDevChatConnector = new FeatureDevChatConnector(props) - this.docChatConnector = new docChatConnector(props) this.gumbyChatConnector = new GumbyChatConnector(props) this.scanChatConnector = new ScanChatConnector(props) - this.testChatConnector = new TestChatConnector(props) this.amazonqCommonsConnector = new AmazonQCommonsConnector({ sendMessageToExtension: this.sendMessageToExtension, onWelcomeFollowUpClicked: props.onWelcomeFollowUpClicked, @@ -172,20 +163,12 @@ export class Connector { case 'cwc': this.cwChatConnector.onResponseBodyLinkClick(tabID, messageId, link) break - case 'featuredev': - this.featureDevChatConnector.onResponseBodyLinkClick(tabID, messageId, link) - break case 'gumby': this.gumbyChatConnector.onResponseBodyLinkClick(tabID, messageId, link) break case 'review': this.scanChatConnector.onResponseBodyLinkClick(tabID, messageId, link) break - case 'testgen': - this.testChatConnector.onResponseBodyLinkClick(tabID, messageId, link) - break - case 'doc': - this.docChatConnector.onResponseBodyLinkClick(tabID, messageId, link) } } @@ -201,8 +184,6 @@ export class Connector { switch (this.tabsStorage.getTab(tabID)?.type) { case 'gumby': return this.gumbyChatConnector.requestAnswer(tabID, payload) - case 'testgen': - return this.testChatConnector.requestAnswer(tabID, payload) } } @@ -210,10 +191,6 @@ export class Connector { new Promise((resolve, reject) => { if (this.isUIReady) { switch (this.tabsStorage.getTab(tabID)?.type) { - case 'featuredev': - return this.featureDevChatConnector.requestGenerativeAIAnswer(tabID, messageId, payload) - case 'doc': - return this.docChatConnector.requestGenerativeAIAnswer(tabID, messageId, payload) default: return this.cwChatConnector.requestGenerativeAIAnswer(tabID, messageId, payload) } @@ -247,10 +224,6 @@ export class Connector { } } - startTestGen = (tabID: string, prompt: string): void => { - this.testChatConnector.startTestGen(tabID, prompt) - } - transform = (tabID: string): void => { this.gumbyChatConnector.transform(tabID) } @@ -261,9 +234,6 @@ export class Connector { onStopChatResponse = (tabID: string): void => { switch (this.tabsStorage.getTab(tabID)?.type) { - case 'featuredev': - this.featureDevChatConnector.onStopChatResponse(tabID) - break case 'cwc': this.cwChatConnector.onStopChatResponse(tabID) break @@ -283,16 +253,10 @@ export class Connector { if (messageData.sender === 'CWChat') { await this.cwChatConnector.handleMessageReceive(messageData) - } else if (messageData.sender === 'featureDevChat') { - await this.featureDevChatConnector.handleMessageReceive(messageData) } else if (messageData.sender === 'gumbyChat') { await this.gumbyChatConnector.handleMessageReceive(messageData) } else if (messageData.sender === 'scanChat') { await this.scanChatConnector.handleMessageReceive(messageData) - } else if (messageData.sender === 'testChat') { - await this.testChatConnector.handleMessageReceive(messageData) - } else if (messageData.sender === 'docChat') { - await this.docChatConnector.handleMessageReceive(messageData) } else if (messageData.sender === 'amazonqCore') { await this.amazonqCommonsConnector.handleMessageReceive(messageData) } @@ -323,20 +287,6 @@ export class Connector { case 'review': this.scanChatConnector.onTabAdd(tabID) break - case 'testgen': - this.testChatConnector.onTabAdd(tabID) - break - } - } - - onKnownTabOpen = (tabID: string): void => { - switch (this.tabsStorage.getTab(tabID)?.type) { - case 'featuredev': - this.featureDevChatConnector.onTabOpen(tabID) - break - case 'doc': - this.docChatConnector.onTabOpen(tabID) - break } } @@ -372,23 +322,6 @@ export class Connector { codeBlockLanguage ) break - case 'featuredev': - this.featureDevChatConnector.onCodeInsertToCursorPosition( - tabID, - messageId, - code, - type, - codeReference, - eventId, - codeBlockIndex, - totalCodeBlocks, - userIntent, - codeBlockLanguage - ) - break - case 'testgen': - this.testChatConnector.onCodeInsertToCursorPosition(tabID, messageId, code, type, codeReference) - break } } @@ -477,20 +410,6 @@ export class Connector { codeBlockLanguage ) break - case 'featuredev': - this.featureDevChatConnector.onCopyCodeToClipboard( - tabID, - messageId, - code, - type, - codeReference, - eventId, - codeBlockIndex, - totalCodeBlocks, - userIntent, - codeBlockLanguage - ) - break } } @@ -501,21 +420,12 @@ export class Connector { case 'cwc': this.cwChatConnector.onTabRemove(tabID) break - case 'featuredev': - this.featureDevChatConnector.onTabRemove(tabID) - break - case 'doc': - this.docChatConnector.onTabRemove(tabID) - break case 'gumby': this.gumbyChatConnector.onTabRemove(tabID) break case 'review': this.scanChatConnector.onTabRemove(tabID) break - case 'testgen': - this.testChatConnector.onTabRemove(tabID) - break } } @@ -564,8 +474,6 @@ export class Connector { const tabType = this.tabsStorage.getTab(tabID)?.type switch (tabType) { case 'cwc': - case 'doc': - case 'featuredev': this.amazonqCommonsConnector.authFollowUpClicked(tabID, tabType, authType) } } @@ -578,49 +486,20 @@ export class Connector { case 'unknown': this.amazonqCommonsConnector.followUpClicked(tabID, followUp) break - case 'featuredev': - this.featureDevChatConnector.followUpClicked(tabID, messageId, followUp) - break - case 'testgen': - this.testChatConnector.followUpClicked(tabID, messageId, followUp) - break case 'review': this.scanChatConnector.followUpClicked(tabID, messageId, followUp) break - case 'doc': - this.docChatConnector.followUpClicked(tabID, messageId, followUp) - break default: this.cwChatConnector.followUpClicked(tabID, messageId, followUp) break } } - onFileActionClick = (tabID: string, messageId: string, filePath: string, actionName: string): void => { - switch (this.tabsStorage.getTab(tabID)?.type) { - case 'featuredev': - this.featureDevChatConnector.onFileActionClick(tabID, messageId, filePath, actionName) - break - case 'doc': - this.docChatConnector.onFileActionClick(tabID, messageId, filePath, actionName) - break - } - } - onFileClick = (tabID: string, filePath: string, deleted: boolean, messageId?: string): void => { switch (this.tabsStorage.getTab(tabID)?.type) { - case 'featuredev': - this.featureDevChatConnector.onOpenDiff(tabID, filePath, deleted, messageId) - break - case 'testgen': - this.testChatConnector.onFileDiff(tabID, filePath, deleted, messageId) - break case 'review': this.scanChatConnector.onFileClick(tabID, filePath, messageId) break - case 'doc': - this.docChatConnector.onOpenDiff(tabID, filePath, deleted) - break case 'cwc': this.cwChatConnector.onFileClick(tabID, filePath, messageId) break @@ -629,12 +508,6 @@ export class Connector { sendFeedback = (tabId: string, feedbackPayload: FeedbackPayload): void | undefined => { switch (this.tabsStorage.getTab(tabId)?.type) { - case 'featuredev': - this.featureDevChatConnector.sendFeedback(tabId, feedbackPayload) - break - case 'testgen': - this.testChatConnector.onSendFeedback(tabId, feedbackPayload) - break case 'cwc': this.cwChatConnector.onSendFeedback(tabId, feedbackPayload) break @@ -669,15 +542,9 @@ export class Connector { case 'cwc': this.cwChatConnector.onChatItemVoted(tabId, messageId, vote) break - case 'featuredev': - this.featureDevChatConnector.onChatItemVoted(tabId, messageId, vote) - break case 'review': this.scanChatConnector.onChatItemVoted(tabId, messageId, vote) break - case 'testgen': - this.testChatConnector.onChatItemVoted(tabId, messageId, vote) - break } } @@ -715,15 +582,9 @@ export class Connector { case 'gumby': this.gumbyChatConnector.onCustomFormAction(tabId, action) break - case 'testgen': - this.testChatConnector.onCustomFormAction(tabId, messageId ?? '', action) - break case 'review': this.scanChatConnector.onCustomFormAction(tabId, action) break - case 'doc': - this.docChatConnector.onCustomFormAction(tabId, action) - break case 'cwc': if (action.id === `open-settings`) { this.sendMessageToExtension({ diff --git a/packages/core/src/amazonq/webview/ui/connectorAdapter.ts b/packages/core/src/amazonq/webview/ui/connectorAdapter.ts index 1de1d8556c4..e645e74bd25 100644 --- a/packages/core/src/amazonq/webview/ui/connectorAdapter.ts +++ b/packages/core/src/amazonq/webview/ui/connectorAdapter.ts @@ -70,13 +70,7 @@ export class HybridChatAdapter implements ChatClientAdapter { } isSupportedQuickAction(command: string): boolean { - return ( - command === '/dev' || - command === '/test' || - command === '/review' || - command === '/doc' || - command === '/transform' - ) + return command === '/review' || command === '/transform' } handleQuickAction(prompt: ChatPrompt, tabId: string, eventId: string | undefined): void { @@ -85,11 +79,8 @@ export class HybridChatAdapter implements ChatClientAdapter { get initialQuickActions(): QuickActionCommandGroup[] { const tabDataGenerator = new TabDataGenerator({ - isDocEnabled: this.enableAgents, - isFeatureDevEnabled: this.enableAgents, isGumbyEnabled: this.enableAgents, isScanEnabled: this.enableAgents, - isTestEnabled: this.enableAgents, disabledCommands: this.disabledCommands, commandHighlight: this.featureConfigsSerialized.find(([name]) => name === 'highlightCommand')?.[1], }) diff --git a/packages/core/src/amazonq/webview/ui/followUps/generator.ts b/packages/core/src/amazonq/webview/ui/followUps/generator.ts index cce5726398f..a275cbaae6d 100644 --- a/packages/core/src/amazonq/webview/ui/followUps/generator.ts +++ b/packages/core/src/amazonq/webview/ui/followUps/generator.ts @@ -42,32 +42,6 @@ export class FollowUpGenerator { public generateWelcomeBlockForTab(tabType: TabType): FollowUpsBlock { switch (tabType) { - case 'featuredev': - return { - text: 'Ask a follow up question', - options: [ - { - pillText: 'What are some examples of tasks?', - type: 'DevExamples', - }, - ], - } - case 'doc': - return { - text: 'Select one of the following...', - options: [ - { - pillText: 'Create a README', - prompt: 'Create a README', - type: 'CreateDocumentation', - }, - { - pillText: 'Update an existing README', - prompt: 'Update an existing README', - type: 'UpdateDocumentation', - }, - ], - } default: return { text: 'Try Examples:', diff --git a/packages/core/src/amazonq/webview/ui/followUps/handler.ts b/packages/core/src/amazonq/webview/ui/followUps/handler.ts index 1fd38643827..6024a93ddee 100644 --- a/packages/core/src/amazonq/webview/ui/followUps/handler.ts +++ b/packages/core/src/amazonq/webview/ui/followUps/handler.ts @@ -3,12 +3,12 @@ * SPDX-License-Identifier: Apache-2.0 */ -import { ChatItemAction, ChatItemType, MynahIcons, MynahUI } from '@aws/mynah-ui' +import { ChatItemAction, ChatItemType, MynahUI } from '@aws/mynah-ui' import { Connector } from '../connector' import { TabsStorage } from '../storages/tabsStorage' import { WelcomeFollowupType } from '../apps/amazonqCommonsConnector' import { AuthFollowUpType } from './generator' -import { FollowUpTypes, MynahUIRef } from '../../../commons/types' +import { MynahUIRef } from '../../../commons/types' export interface FollowUpInteractionHandlerProps { mynahUIRef: MynahUIRef @@ -74,82 +74,6 @@ export class FollowUpInteractionHandler { } } - const addChatItem = (tabID: string, messageId: string, options: any[]) => { - this.mynahUI?.addChatItem(tabID, { - type: ChatItemType.ANSWER_PART, - messageId, - followUp: { - text: '', - options, - }, - }) - } - - const ViewDiffOptions = [ - { - icon: MynahIcons.OK, - pillText: 'Accept', - status: 'success', - type: FollowUpTypes.AcceptCode, - }, - { - icon: MynahIcons.REVERT, - pillText: 'Reject', - status: 'error', - type: FollowUpTypes.RejectCode, - }, - ] - - const AcceptCodeOptions = [ - { - icon: MynahIcons.OK, - pillText: 'Accepted', - status: 'success', - disabled: true, - }, - ] - - const RejectCodeOptions = [ - { - icon: MynahIcons.REVERT, - pillText: 'Rejected', - status: 'error', - disabled: true, - }, - ] - - const ViewCodeDiffAfterIterationOptions = [ - { - icon: MynahIcons.OK, - pillText: 'Accept', - status: 'success', - type: FollowUpTypes.AcceptCode, - }, - { - icon: MynahIcons.REVERT, - pillText: 'Reject', - status: 'error', - type: FollowUpTypes.RejectCode, // TODO: Add new Followup Action for "Reject" - }, - ] - - if (this.tabsStorage.getTab(tabID)?.type === 'testgen') { - switch (followUp.type) { - case FollowUpTypes.ViewDiff: - addChatItem(tabID, messageId, ViewDiffOptions) - break - case FollowUpTypes.AcceptCode: - addChatItem(tabID, messageId, AcceptCodeOptions) - break - case FollowUpTypes.RejectCode: - addChatItem(tabID, messageId, RejectCodeOptions) - break - case FollowUpTypes.ViewCodeDiffAfterIteration: - addChatItem(tabID, messageId, ViewCodeDiffAfterIterationOptions) - break - } - } - this.connector.onFollowUpClicked(tabID, messageId, followUp) } diff --git a/packages/core/src/amazonq/webview/ui/main.ts b/packages/core/src/amazonq/webview/ui/main.ts index 7d5bd48eaeb..54696982ae0 100644 --- a/packages/core/src/amazonq/webview/ui/main.ts +++ b/packages/core/src/amazonq/webview/ui/main.ts @@ -91,11 +91,8 @@ export class WebviewUIHandler { tabDataGenerator?: TabDataGenerator // are agents enabled - isFeatureDevEnabled: boolean isGumbyEnabled: boolean isScanEnabled: boolean - isTestEnabled: boolean - isDocEnabled: boolean isSMUS: boolean isSM: boolean @@ -166,21 +163,15 @@ export class WebviewUIHandler { }, }) - this.isFeatureDevEnabled = enableAgents this.isGumbyEnabled = enableAgents this.isScanEnabled = enableAgents - this.isTestEnabled = enableAgents - this.isDocEnabled = enableAgents this.featureConfigs = tryNewMap(featureConfigsSerialized) const highlightCommand = this.featureConfigs.get('highlightCommand') this.tabDataGenerator = new TabDataGenerator({ - isFeatureDevEnabled: enableAgents, isGumbyEnabled: enableAgents, isScanEnabled: enableAgents, - isTestEnabled: enableAgents, - isDocEnabled: enableAgents, disabledCommands, commandHighlight: highlightCommand, regionProfile, // TODO @@ -195,31 +186,22 @@ export class WebviewUIHandler { this.quickActionHandler?.handle(chatPrompt, tabId) }, onUpdateAuthentication: (isAmazonQEnabled: boolean, authenticatingTabIDs: string[]): void => { - this.isFeatureDevEnabled = isAmazonQEnabled this.isGumbyEnabled = isAmazonQEnabled this.isScanEnabled = isAmazonQEnabled - this.isTestEnabled = isAmazonQEnabled - this.isDocEnabled = isAmazonQEnabled this.quickActionHandler = new QuickActionHandler({ mynahUIRef: this.mynahUIRef, connector: this.connector!, tabsStorage: this.tabsStorage, - isFeatureDevEnabled: this.isFeatureDevEnabled, isGumbyEnabled: this.isGumbyEnabled, isScanEnabled: this.isScanEnabled, - isTestEnabled: this.isTestEnabled, - isDocEnabled: this.isDocEnabled, hybridChat, disabledCommands, }) this.tabDataGenerator = new TabDataGenerator({ - isFeatureDevEnabled: this.isFeatureDevEnabled, isGumbyEnabled: this.isGumbyEnabled, isScanEnabled: this.isScanEnabled, - isTestEnabled: this.isTestEnabled, - isDocEnabled: this.isDocEnabled, disabledCommands, commandHighlight: highlightCommand, regionProfile, // TODO @@ -244,8 +226,7 @@ export class WebviewUIHandler { if ( this.tabsStorage.getTab(tabID)?.type === 'gumby' || - this.tabsStorage.getTab(tabID)?.type === 'review' || - this.tabsStorage.getTab(tabID)?.type === 'testgen' + this.tabsStorage.getTab(tabID)?.type === 'review' ) { this.mynahUI?.updateStore(tabID, { promptInputDisabledState: false, @@ -567,7 +548,6 @@ export class WebviewUIHandler { return } this.tabsStorage.updateTabTypeFromUnknown(newTabID, tabType) - this.connector?.onKnownTabOpen(newTabID) this.connector?.onUpdateTabType(newTabID) this.mynahUI?.updateStore(newTabID, { @@ -716,11 +696,7 @@ export class WebviewUIHandler { } const tabType = this.tabsStorage.getTab(tabID)?.type - if (tabType === 'featuredev') { - this.mynahUI?.addChatItem(tabID, { - type: ChatItemType.ANSWER_STREAM, - }) - } else if (tabType === 'gumby') { + if (tabType === 'gumby') { this.connector?.requestAnswer(tabID, { chatMessage: prompt.prompt ?? '', }) @@ -973,9 +949,6 @@ export class WebviewUIHandler { onFollowUpClicked: (tabID, messageId, followUp) => { this.followUpsInteractionHandler?.onFollowUpClicked(tabID, messageId, followUp) }, - onFileActionClick: async (tabID: string, messageId: string, filePath: string, actionName: string) => { - this.connector?.onFileActionClick(tabID, messageId, filePath, actionName) - }, onFileClick: this.connector.onFileClick, tabs: { 'tab-1': { @@ -1037,11 +1010,8 @@ export class WebviewUIHandler { mynahUIRef: this.mynahUIRef, connector: this.connector, tabsStorage: this.tabsStorage, - isFeatureDevEnabled: this.isFeatureDevEnabled, isGumbyEnabled: this.isGumbyEnabled, isScanEnabled: this.isScanEnabled, - isTestEnabled: this.isTestEnabled, - isDocEnabled: this.isDocEnabled, hybridChat, }) this.textMessageHandler = new TextMessageHandler({ @@ -1053,11 +1023,8 @@ export class WebviewUIHandler { mynahUIRef: this.mynahUIRef, connector: this.connector, tabsStorage: this.tabsStorage, - isFeatureDevEnabled: this.isFeatureDevEnabled, isGumbyEnabled: this.isGumbyEnabled, isScanEnabled: this.isScanEnabled, - isTestEnabled: this.isTestEnabled, - isDocEnabled: this.isDocEnabled, }) } @@ -1102,12 +1069,6 @@ export class WebviewUIHandler { }, } } - // Show only "Copy" option for codeblocks in Q Test Tab - if (tab?.type === 'testgen') { - return { - 'insert-to-cursor': undefined, - } - } // Default will show "Copy" and "Insert at cursor" for codeblocks return {} } diff --git a/packages/core/src/amazonq/webview/ui/messages/controller.ts b/packages/core/src/amazonq/webview/ui/messages/controller.ts index a41d6a1f7f5..37a8077f8ae 100644 --- a/packages/core/src/amazonq/webview/ui/messages/controller.ts +++ b/packages/core/src/amazonq/webview/ui/messages/controller.ts @@ -14,11 +14,8 @@ export interface MessageControllerProps { mynahUIRef: MynahUIRef connector: Connector tabsStorage: TabsStorage - isFeatureDevEnabled: boolean isGumbyEnabled: boolean isScanEnabled: boolean - isTestEnabled: boolean - isDocEnabled: boolean disabledCommands?: string[] } @@ -33,11 +30,8 @@ export class MessageController { this.connector = props.connector this.tabsStorage = props.tabsStorage this.tabDataGenerator = new TabDataGenerator({ - isFeatureDevEnabled: props.isFeatureDevEnabled, isGumbyEnabled: props.isGumbyEnabled, isScanEnabled: props.isScanEnabled, - isTestEnabled: props.isTestEnabled, - isDocEnabled: props.isDocEnabled, disabledCommands: props.disabledCommands, }) } diff --git a/packages/core/src/amazonq/webview/ui/quickActions/generator.ts b/packages/core/src/amazonq/webview/ui/quickActions/generator.ts index 8500a04911d..0cc7740f2ec 100644 --- a/packages/core/src/amazonq/webview/ui/quickActions/generator.ts +++ b/packages/core/src/amazonq/webview/ui/quickActions/generator.ts @@ -8,28 +8,19 @@ import { TabType } from '../storages/tabsStorage' import { MynahIcons } from '@aws/mynah-ui' export interface QuickActionGeneratorProps { - isFeatureDevEnabled: boolean isGumbyEnabled: boolean isScanEnabled: boolean - isTestEnabled: boolean - isDocEnabled: boolean disableCommands?: string[] } export class QuickActionGenerator { - public isFeatureDevEnabled: boolean private isGumbyEnabled: boolean private isScanEnabled: boolean - private isTestEnabled: boolean - private isDocEnabled: boolean private disabledCommands: string[] constructor(props: QuickActionGeneratorProps) { - this.isFeatureDevEnabled = props.isFeatureDevEnabled this.isGumbyEnabled = props.isGumbyEnabled this.isScanEnabled = props.isScanEnabled - this.isTestEnabled = props.isTestEnabled - this.isDocEnabled = props.isDocEnabled this.disabledCommands = props.disableCommands ?? [] } @@ -43,7 +34,7 @@ export class QuickActionGenerator { const quickActionCommands = [ { commands: [ - ...(this.isFeatureDevEnabled && !this.disabledCommands.includes('/dev') + ...(!this.disabledCommands.includes('/dev') ? [ { command: '/dev', @@ -53,7 +44,7 @@ export class QuickActionGenerator { }, ] : []), - ...(this.isTestEnabled && !this.disabledCommands.includes('/test') + ...(!this.disabledCommands.includes('/test') ? [ { command: '/test', @@ -72,7 +63,7 @@ export class QuickActionGenerator { }, ] : []), - ...(this.isDocEnabled && !this.disabledCommands.includes('/doc') + ...(!this.disabledCommands.includes('/doc') ? [ { command: '/doc', @@ -120,10 +111,6 @@ export class QuickActionGenerator { description: '', unavailableItems: [], }, - featuredev: { - description: "This command isn't available in /dev", - unavailableItems: ['/help', '/clear'], - }, review: { description: "This command isn't available in /review", unavailableItems: ['/help', '/clear'], @@ -132,14 +119,6 @@ export class QuickActionGenerator { description: "This command isn't available in /transform", unavailableItems: ['/dev', '/test', '/doc', '/review', '/help', '/clear'], }, - testgen: { - description: "This command isn't available in /test", - unavailableItems: ['/help', '/clear'], - }, - doc: { - description: "This command isn't available in /doc", - unavailableItems: ['/help', '/clear'], - }, welcome: { description: '', unavailableItems: ['/clear'], diff --git a/packages/core/src/amazonq/webview/ui/quickActions/handler.ts b/packages/core/src/amazonq/webview/ui/quickActions/handler.ts index 6b017e419c0..ff23fb72635 100644 --- a/packages/core/src/amazonq/webview/ui/quickActions/handler.ts +++ b/packages/core/src/amazonq/webview/ui/quickActions/handler.ts @@ -3,7 +3,7 @@ * SPDX-License-Identifier: Apache-2.0 */ -import { ChatItemType, ChatPrompt, MynahUI, NotificationType, MynahIcons } from '@aws/mynah-ui' +import { ChatItemType, ChatPrompt, MynahUI, NotificationType } from '@aws/mynah-ui' import { TabDataGenerator } from '../tabs/generator' import { Connector } from '../connector' import { TabsStorage, TabType } from '../storages/tabsStorage' @@ -14,11 +14,8 @@ export interface QuickActionsHandlerProps { mynahUIRef: { mynahUI: MynahUI | undefined } connector: Connector tabsStorage: TabsStorage - isFeatureDevEnabled: boolean isGumbyEnabled: boolean isScanEnabled: boolean - isTestEnabled: boolean - isDocEnabled: boolean hybridChat?: boolean disabledCommands?: string[] } @@ -36,30 +33,21 @@ export class QuickActionHandler { private connector: Connector private tabsStorage: TabsStorage private tabDataGenerator: TabDataGenerator - private isFeatureDevEnabled: boolean private isGumbyEnabled: boolean private isScanEnabled: boolean - private isTestEnabled: boolean - private isDocEnabled: boolean private isHybridChatEnabled: boolean constructor(props: QuickActionsHandlerProps) { this.mynahUIRef = props.mynahUIRef this.connector = props.connector this.tabsStorage = props.tabsStorage - this.isDocEnabled = props.isDocEnabled this.tabDataGenerator = new TabDataGenerator({ - isFeatureDevEnabled: props.isFeatureDevEnabled, isGumbyEnabled: props.isGumbyEnabled, isScanEnabled: props.isScanEnabled, - isTestEnabled: props.isTestEnabled, - isDocEnabled: props.isDocEnabled, disabledCommands: props.disabledCommands, }) - this.isFeatureDevEnabled = props.isFeatureDevEnabled this.isGumbyEnabled = props.isGumbyEnabled this.isScanEnabled = props.isScanEnabled - this.isTestEnabled = props.isTestEnabled this.isHybridChatEnabled = props.hybridChat ?? false } @@ -71,15 +59,6 @@ export class QuickActionHandler { public handle(chatPrompt: ChatPrompt, tabID: string, eventId?: string) { this.tabsStorage.resetTabTimer(tabID) switch (chatPrompt.command) { - case '/dev': - this.handleCommand({ - chatPrompt, - tabID, - taskName: 'Q - Dev', - tabType: 'featuredev', - isEnabled: this.isFeatureDevEnabled, - }) - break case '/help': this.handleHelpCommand(tabID) break @@ -89,18 +68,6 @@ export class QuickActionHandler { case '/review': this.handleScanCommand(tabID, eventId) break - case '/test': - this.handleTestCommand(chatPrompt, tabID, eventId) - break - case '/doc': - this.handleCommand({ - chatPrompt, - tabID, - taskName: 'Q - Doc', - tabType: 'doc', - isEnabled: this.isDocEnabled, - }) - break case '/clear': this.handleClearCommand(tabID) break @@ -145,7 +112,6 @@ export class QuickActionHandler { return } else { this.tabsStorage.updateTabTypeFromUnknown(affectedTabId, 'review') - this.connector.onKnownTabOpen(affectedTabId) this.connector.onUpdateTabType(affectedTabId) // reset chat history @@ -163,126 +129,6 @@ export class QuickActionHandler { } } - private handleTestCommand(chatPrompt: ChatPrompt, tabID: string | undefined, eventId: string | undefined) { - if (!this.isTestEnabled || !this.mynahUI) { - return - } - const testTabId = this.tabsStorage.getTabs().find((tab) => tab.type === 'testgen')?.id - const realPromptText = chatPrompt.escapedPrompt?.trim() ?? '' - - if (testTabId !== undefined) { - this.mynahUI.selectTab(testTabId, eventId || '') - this.connector.onTabChange(testTabId) - this.connector.startTestGen(testTabId, realPromptText) - return - } - - // if there is no test tab, open a new one - const affectedTabId: string | undefined = this.addTab(tabID) - - if (affectedTabId === undefined) { - this.mynahUI.notify({ - content: uiComponentsTexts.noMoreTabsTooltip, - type: NotificationType.WARNING, - }) - return - } else { - this.tabsStorage.updateTabTypeFromUnknown(affectedTabId, 'testgen') - this.connector.onKnownTabOpen(affectedTabId) - this.connector.onUpdateTabType(affectedTabId) - - // reset chat history - this.mynahUI.updateStore(affectedTabId, { - chatItems: [], - }) - - // creating a new tab and printing some title - this.mynahUI.updateStore( - affectedTabId, - this.tabDataGenerator.getTabData('testgen', realPromptText === '', 'Q - Test') - ) - - this.connector.startTestGen(affectedTabId, realPromptText) - } - } - - private handleCommand(props: HandleCommandProps) { - if (!props.isEnabled || !this.mynahUI) { - return - } - - const realPromptText = props.chatPrompt?.escapedPrompt?.trim() ?? '' - - const affectedTabId = this.addTab(props.tabID) - - if (affectedTabId === undefined) { - this.mynahUI.notify({ - content: uiComponentsTexts.noMoreTabsTooltip, - type: NotificationType.WARNING, - }) - return - } else { - this.tabsStorage.updateTabTypeFromUnknown(affectedTabId, props.tabType) - this.connector.onKnownTabOpen(affectedTabId) - this.connector.onUpdateTabType(affectedTabId) - - this.mynahUI.updateStore(affectedTabId, { chatItems: [] }) - - if (props.tabType === 'featuredev') { - this.mynahUI.updateStore( - affectedTabId, - this.tabDataGenerator.getTabData(props.tabType, false, props.taskName) - ) - } else { - this.mynahUI.updateStore( - affectedTabId, - this.tabDataGenerator.getTabData(props.tabType, realPromptText === '', props.taskName) - ) - } - - const addInformationCard = (tabId: string) => { - if (props.tabType === 'featuredev') { - this.mynahUI?.addChatItem(tabId, { - type: ChatItemType.ANSWER, - informationCard: { - title: 'Feature development', - description: 'Amazon Q Developer Agent for Software Development', - icon: MynahIcons.BUG, - content: { - body: [ - 'After you provide a task, I will:', - '1. Generate code based on your description and the code in your workspace', - '2. Provide a list of suggestions for you to review and add to your workspace', - '3. If needed, iterate based on your feedback', - 'To learn more, visit the [user guide](https://docs.aws.amazon.com/amazonq/latest/qdeveloper-ug/software-dev.html)', - ].join('\n'), - }, - }, - }) - } - } - if (realPromptText !== '') { - this.mynahUI.addChatItem(affectedTabId, { - type: ChatItemType.PROMPT, - body: realPromptText, - }) - addInformationCard(affectedTabId) - - this.mynahUI.updateStore(affectedTabId, { - loadingChat: true, - cancelButtonWhenLoading: false, - promptInputDisabledState: true, - }) - - void this.connector.requestGenerativeAIAnswer(affectedTabId, '', { - chatMessage: realPromptText, - }) - } else { - addInformationCard(affectedTabId) - } - } - } - private handleGumbyCommand(tabID: string, eventId: string | undefined) { if (!this.isGumbyEnabled || !this.mynahUI) { return @@ -319,7 +165,6 @@ export class QuickActionHandler { return } else { this.tabsStorage.updateTabTypeFromUnknown(affectedTabId, 'gumby') - this.connector.onKnownTabOpen(affectedTabId) this.connector.onUpdateTabType(affectedTabId) // reset chat history diff --git a/packages/core/src/amazonq/webview/ui/storages/tabsStorage.ts b/packages/core/src/amazonq/webview/ui/storages/tabsStorage.ts index f9a419fed96..92fa7c5a07e 100644 --- a/packages/core/src/amazonq/webview/ui/storages/tabsStorage.ts +++ b/packages/core/src/amazonq/webview/ui/storages/tabsStorage.ts @@ -4,17 +4,7 @@ */ export type TabStatus = 'free' | 'busy' | 'dead' -const TabTypes = [ - 'cwc', - 'featuredev', - 'gumby', - 'review', - 'testgen', - 'doc', - 'agentWalkthrough', - 'welcome', - 'unknown', -] as const +const TabTypes = ['cwc', 'gumby', 'review', 'agentWalkthrough', 'welcome', 'unknown'] as const export type TabType = (typeof TabTypes)[number] export function isTabType(value: string): value is TabType { return (TabTypes as readonly string[]).includes(value) @@ -22,16 +12,10 @@ export function isTabType(value: string): value is TabType { export function getTabCommandFromTabType(tabType: TabType): string { switch (tabType) { - case 'featuredev': - return '/dev' - case 'doc': - return '/doc' case 'gumby': return '/transform' case 'review': return '/review' - case 'testgen': - return '/test' default: return '' } diff --git a/packages/core/src/amazonq/webview/ui/tabs/constants.ts b/packages/core/src/amazonq/webview/ui/tabs/constants.ts index 9448f32d528..ead70679b7f 100644 --- a/packages/core/src/amazonq/webview/ui/tabs/constants.ts +++ b/packages/core/src/amazonq/webview/ui/tabs/constants.ts @@ -4,7 +4,6 @@ */ import { TabType } from '../storages/tabsStorage' import { QuickActionCommandGroup } from '@aws/mynah-ui' -import { userGuideURL } from '../texts/constants' const qChatIntroMessage = `Hi, I'm Amazon Q. I can answer your software development questions. Ask me to explain, debug, or optimize your code. @@ -48,18 +47,6 @@ export const commonTabData: TabTypeData = { export const TabTypeDataMap: Record, TabTypeData> = { unknown: commonTabData, cwc: commonTabData, - featuredev: { - title: 'Q - Dev', - placeholder: 'Describe your task or issue in as much detail as possible', - welcome: `I can generate code to accomplish a task or resolve an issue. - -After you provide a description, I will: -1. Generate code based on your description and the code in your workspace -2. Provide a list of suggestions for you to review and add to your workspace -3. If needed, iterate based on your feedback - -To learn more, visit the [User Guide](${userGuideURL}).`, - }, gumby: { title: 'Q - Code Transformation', placeholder: 'Open a new tab to chat with Q', @@ -71,16 +58,4 @@ To learn more, visit the [User Guide](${userGuideURL}).`, placeholder: `Ask a question or enter "/" for quick actions`, welcome: `Welcome to code reviews. I can help you identify code issues and provide suggested fixes for the active file or workspace you have opened in your IDE.`, }, - testgen: { - title: 'Q - Test', - placeholder: `Waiting on your inputs...`, - welcome: `Welcome to unit test generation. I can help you generate unit tests for your active file.`, - }, - doc: { - title: 'Q - Doc Generation', - placeholder: 'Ask Amazon Q to generate documentation for your project', - welcome: `Welcome to doc generation! - -I can help generate documentation for your code. To get started, choose what type of doc update you'd like to make.`, - }, } diff --git a/packages/core/src/amazonq/webview/ui/tabs/generator.ts b/packages/core/src/amazonq/webview/ui/tabs/generator.ts index 9698a9d8076..2331a0721c7 100644 --- a/packages/core/src/amazonq/webview/ui/tabs/generator.ts +++ b/packages/core/src/amazonq/webview/ui/tabs/generator.ts @@ -13,11 +13,8 @@ import { FeatureContext } from '../../../../shared/featureConfig' import { RegionProfile } from '../../../../codewhisperer/models/model' export interface TabDataGeneratorProps { - isFeatureDevEnabled: boolean isGumbyEnabled: boolean isScanEnabled: boolean - isTestEnabled: boolean - isDocEnabled: boolean disabledCommands?: string[] commandHighlight?: FeatureContext regionProfile?: RegionProfile @@ -32,11 +29,8 @@ export class TabDataGenerator { constructor(props: TabDataGeneratorProps) { this.followUpsGenerator = new FollowUpGenerator() this.quickActionsGenerator = new QuickActionGenerator({ - isFeatureDevEnabled: props.isFeatureDevEnabled, isGumbyEnabled: props.isGumbyEnabled, isScanEnabled: props.isScanEnabled, - isTestEnabled: props.isTestEnabled, - isDocEnabled: props.isDocEnabled, disableCommands: props.disabledCommands, }) this.highlightCommand = props.commandHighlight diff --git a/packages/core/src/amazonqDoc/app.ts b/packages/core/src/amazonqDoc/app.ts index 929cf1d45de..52985b82a00 100644 --- a/packages/core/src/amazonqDoc/app.ts +++ b/packages/core/src/amazonqDoc/app.ts @@ -6,7 +6,6 @@ import * as vscode from 'vscode' import { ChatControllerEventEmitters, DocController } from './controllers/chat/controller' import { AmazonQAppInitContext } from '../amazonq/apps/initContext' -import { MessagePublisher } from '../amazonq/messages/messagePublisher' import { MessageListener } from '../amazonq/messages/messageListener' import { fromQueryToParameters } from '../shared/utilities/uriUtils' import { getLogger } from '../shared/logger/logger' @@ -78,8 +77,6 @@ export function init(appContext: AmazonQAppInitContext) { webViewMessageListener: new MessageListener(docChatUIInputEventEmitter), }) - appContext.registerWebViewToAppMessagePublisher(new MessagePublisher(docChatUIInputEventEmitter), 'doc') - const debouncedEvent = debounce(async () => { const authenticated = (await AuthUtil.instance.getChatAuthState()).amazonQ === 'connected' let authenticatingSessionIDs: string[] = [] diff --git a/packages/core/src/amazonqFeatureDev/app.ts b/packages/core/src/amazonqFeatureDev/app.ts index a016d2ba481..fd0652fd0e4 100644 --- a/packages/core/src/amazonqFeatureDev/app.ts +++ b/packages/core/src/amazonqFeatureDev/app.ts @@ -7,7 +7,6 @@ import * as vscode from 'vscode' import { UIMessageListener } from './views/actions/uiMessageListener' import { ChatControllerEventEmitters, FeatureDevController } from './controllers/chat/controller' import { AmazonQAppInitContext } from '../amazonq/apps/initContext' -import { MessagePublisher } from '../amazonq/messages/messagePublisher' import { MessageListener } from '../amazonq/messages/messageListener' import { fromQueryToParameters } from '../shared/utilities/uriUtils' import { getLogger } from '../shared/logger/logger' @@ -81,11 +80,6 @@ export function init(appContext: AmazonQAppInitContext) { webViewMessageListener: new MessageListener(featureDevChatUIInputEventEmitter), }) - appContext.registerWebViewToAppMessagePublisher( - new MessagePublisher(featureDevChatUIInputEventEmitter), - 'featuredev' - ) - const debouncedEvent = debounce(async () => { const authenticated = (await AuthUtil.instance.getChatAuthState()).amazonQ === 'connected' let authenticatingSessionIDs: string[] = [] diff --git a/packages/core/src/amazonqTest/app.ts b/packages/core/src/amazonqTest/app.ts deleted file mode 100644 index 6c638c13b71..00000000000 --- a/packages/core/src/amazonqTest/app.ts +++ /dev/null @@ -1,76 +0,0 @@ -/*! - * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. - * SPDX-License-Identifier: Apache-2.0 - */ - -import * as vscode from 'vscode' -import { AmazonQAppInitContext } from '../amazonq/apps/initContext' -import { MessagePublisher } from '../amazonq/messages/messagePublisher' -import { MessageListener } from '../amazonq/messages/messageListener' -import { AuthUtil } from '../codewhisperer/util/authUtil' -import { ChatSessionManager } from './chat/storages/chatSession' -import { TestController, TestChatControllerEventEmitters } from './chat/controller/controller' -import { AppToWebViewMessageDispatcher } from './chat/views/connector/connector' -import { Messenger } from './chat/controller/messenger/messenger' -import { UIMessageListener } from './chat/views/actions/uiMessageListener' -import { debounce } from 'lodash' -import { testGenState } from '../codewhisperer/models/model' - -export function init(appContext: AmazonQAppInitContext) { - const testChatControllerEventEmitters: TestChatControllerEventEmitters = { - tabOpened: new vscode.EventEmitter(), - tabClosed: new vscode.EventEmitter(), - authClicked: new vscode.EventEmitter(), - startTestGen: new vscode.EventEmitter(), - processHumanChatMessage: new vscode.EventEmitter(), - updateTargetFileInfo: new vscode.EventEmitter(), - showCodeGenerationResults: new vscode.EventEmitter(), - openDiff: new vscode.EventEmitter(), - formActionClicked: new vscode.EventEmitter(), - followUpClicked: new vscode.EventEmitter(), - sendUpdatePromptProgress: new vscode.EventEmitter(), - errorThrown: new vscode.EventEmitter(), - insertCodeAtCursorPosition: new vscode.EventEmitter(), - processResponseBodyLinkClick: new vscode.EventEmitter(), - processChatItemVotedMessage: new vscode.EventEmitter(), - processChatItemFeedbackMessage: new vscode.EventEmitter(), - } - const dispatcher = new AppToWebViewMessageDispatcher(appContext.getAppsToWebViewMessagePublisher()) - const messenger = new Messenger(dispatcher) - - new TestController(testChatControllerEventEmitters, messenger, appContext.onDidChangeAmazonQVisibility.event) - - const testChatUIInputEventEmitter = new vscode.EventEmitter() - - new UIMessageListener({ - chatControllerEventEmitters: testChatControllerEventEmitters, - webViewMessageListener: new MessageListener(testChatUIInputEventEmitter), - }) - - appContext.registerWebViewToAppMessagePublisher(new MessagePublisher(testChatUIInputEventEmitter), 'testgen') - - const debouncedEvent = debounce(async () => { - const authenticated = (await AuthUtil.instance.getChatAuthState()).amazonQ === 'connected' - let authenticatingSessionID = '' - - if (authenticated) { - const session = ChatSessionManager.Instance.getSession() - - if (session.isTabOpen() && session.isAuthenticating) { - authenticatingSessionID = session.tabID! - session.isAuthenticating = false - } - } - - messenger.sendAuthenticationUpdate(authenticated, [authenticatingSessionID]) - }, 500) - - AuthUtil.instance.secondaryAuth.onDidChangeActiveConnection(() => { - return debouncedEvent() - }) - AuthUtil.instance.regionProfileManager.onDidChangeRegionProfile(() => { - return debouncedEvent() - }) - testGenState.setChatControllers(testChatControllerEventEmitters) - // TODO: Add testGen provider for creating new files after test generation if they does not exist -} diff --git a/packages/core/src/amazonqTest/chat/controller/controller.ts b/packages/core/src/amazonqTest/chat/controller/controller.ts deleted file mode 100644 index 747cca57e8e..00000000000 --- a/packages/core/src/amazonqTest/chat/controller/controller.ts +++ /dev/null @@ -1,1464 +0,0 @@ -/*! - * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. - * SPDX-License-Identifier: Apache-2.0 - * - * This class is responsible for responding to UI events by calling - * the Test extension. - */ -import * as vscode from 'vscode' -import path from 'path' -import { FollowUps, Messenger, TestNamedMessages } from './messenger/messenger' -import { AuthController } from '../../../amazonq/auth/controller' -import { ChatSessionManager } from '../storages/chatSession' -import { BuildStatus, ConversationState, Session } from '../session/session' -import { AuthUtil } from '../../../codewhisperer/util/authUtil' -import { - buildProgressField, - cancellingProgressField, - cancelTestGenButton, - errorProgressField, - testGenBuildProgressMessage, - testGenCompletedField, - testGenProgressField, - testGenSummaryMessage, - maxUserPromptLength, -} from '../../models/constants' -import MessengerUtils, { ButtonActions } from './messenger/messengerUtils' -import { getTelemetryReasonDesc, isAwsError } from '../../../shared/errors' -import { ChatItemType } from '../../../amazonq/commons/model' -import { ChatItemButton, MynahIcons, ProgressField } from '@aws/mynah-ui' -import { FollowUpTypes } from '../../../amazonq/commons/types' -import { - cancelBuild, - runBuildCommand, - startTestGenerationProcess, -} from '../../../codewhisperer/commands/startTestGeneration' -import { UserIntent } from '@amzn/codewhisperer-streaming' -import { getSelectedCustomization } from '../../../codewhisperer/util/customizationUtil' -import { createCodeWhispererChatStreamingClient } from '../../../shared/clients/codewhispererChatClient' -import { - ChatItemVotedMessage, - ChatTriggerType, - TriggerPayload, -} from '../../../codewhispererChat/controllers/chat/model' -import { triggerPayloadToChatRequest } from '../../../codewhispererChat/controllers/chat/chatRequest/converter' -import { EditorContentController } from '../../../amazonq/commons/controllers/contentController' -import { amazonQTabSuffix } from '../../../shared/constants' -import { applyChanges } from '../../../shared/utilities/textDocumentUtilities' -import { telemetry } from '../../../shared/telemetry/telemetry' -import { CodeWhispererSettings } from '../../../codewhisperer/util/codewhispererSettings' -import globals from '../../../shared/extensionGlobals' -import { openUrl } from '../../../shared/utilities/vsCodeUtils' -import { getLogger } from '../../../shared/logger/logger' -import { i18n } from '../../../shared/i18n-helper' -import { sleep } from '../../../shared/utilities/timeoutUtils' -import { fs } from '../../../shared/fs/fs' -import { randomUUID } from '../../../shared/crypto' -import { tempDirPath, testGenerationLogsDir } from '../../../shared/filesystemUtilities' -import { CodeReference } from '../../../codewhispererChat/view/connector/connector' -import { TelemetryHelper } from '../../../codewhisperer/util/telemetryHelper' -import { Reference, testGenState } from '../../../codewhisperer/models/model' -import { - referenceLogText, - TestGenerationBuildStep, - tooManyRequestErrorMessage, - unitTestGenerationCancelMessage, - utgLimitReached, -} from '../../../codewhisperer/models/constants' -import { UserWrittenCodeTracker } from '../../../codewhisperer/tracker/userWrittenCodeTracker' -import { ReferenceLogViewProvider } from '../../../codewhisperer/service/referenceLogViewProvider' -import { TargetFileInfo } from '../../../codewhisperer/client/codewhispereruserclient' -import { submitFeedback } from '../../../feedback/vue/submitFeedback' -import { placeholder } from '../../../shared/vscode/commands2' -import { Auth } from '../../../auth/auth' -import { defaultContextLengths } from '../../../codewhispererChat/constants' - -export interface TestChatControllerEventEmitters { - readonly tabOpened: vscode.EventEmitter - readonly tabClosed: vscode.EventEmitter - readonly authClicked: vscode.EventEmitter - readonly startTestGen: vscode.EventEmitter - readonly processHumanChatMessage: vscode.EventEmitter - readonly updateTargetFileInfo: vscode.EventEmitter - readonly showCodeGenerationResults: vscode.EventEmitter - readonly openDiff: vscode.EventEmitter - readonly formActionClicked: vscode.EventEmitter - readonly followUpClicked: vscode.EventEmitter - readonly sendUpdatePromptProgress: vscode.EventEmitter - readonly errorThrown: vscode.EventEmitter - readonly insertCodeAtCursorPosition: vscode.EventEmitter - readonly processResponseBodyLinkClick: vscode.EventEmitter - readonly processChatItemVotedMessage: vscode.EventEmitter - readonly processChatItemFeedbackMessage: vscode.EventEmitter -} - -type OpenDiffMessage = { - tabID: string - messageId: string - filePath: string - codeGenerationId: string -} - -export class TestController { - private readonly messenger: Messenger - private readonly sessionStorage: ChatSessionManager - private authController: AuthController - private readonly editorContentController: EditorContentController - tempResultDirPath = path.join(tempDirPath, 'q-testgen') - - public constructor( - private readonly chatControllerMessageListeners: TestChatControllerEventEmitters, - messenger: Messenger, - onDidChangeAmazonQVisibility: vscode.Event - ) { - this.messenger = messenger - this.sessionStorage = ChatSessionManager.Instance - this.authController = new AuthController() - this.editorContentController = new EditorContentController() - - this.chatControllerMessageListeners.tabOpened.event((data) => { - return this.tabOpened(data) - }) - - this.chatControllerMessageListeners.tabClosed.event((data) => { - return this.tabClosed(data) - }) - - this.chatControllerMessageListeners.authClicked.event((data) => { - this.authClicked(data) - }) - - this.chatControllerMessageListeners.startTestGen.event(async (data) => { - await this.startTestGen(data, false) - }) - - this.chatControllerMessageListeners.processHumanChatMessage.event((data) => { - return this.processHumanChatMessage(data) - }) - - this.chatControllerMessageListeners.formActionClicked.event((data) => { - return this.handleFormActionClicked(data) - }) - - this.chatControllerMessageListeners.updateTargetFileInfo.event((data) => { - return this.updateTargetFileInfo(data) - }) - - this.chatControllerMessageListeners.showCodeGenerationResults.event((data) => { - return this.showCodeGenerationResults(data) - }) - - this.chatControllerMessageListeners.openDiff.event((data) => { - return this.openDiff(data) - }) - - this.chatControllerMessageListeners.sendUpdatePromptProgress.event((data) => { - return this.handleUpdatePromptProgress(data) - }) - - this.chatControllerMessageListeners.errorThrown.event((data) => { - return this.handleErrorMessage(data) - }) - - this.chatControllerMessageListeners.insertCodeAtCursorPosition.event((data) => { - return this.handleInsertCodeAtCursorPosition(data) - }) - - this.chatControllerMessageListeners.processResponseBodyLinkClick.event((data) => { - return this.processLink(data) - }) - - this.chatControllerMessageListeners.processChatItemVotedMessage.event((data) => { - this.processChatItemVotedMessage(data).catch((e) => { - getLogger().error('processChatItemVotedMessage failed: %s', (e as Error).message) - }) - }) - - this.chatControllerMessageListeners.processChatItemFeedbackMessage.event((data) => { - this.processChatItemFeedbackMessage(data).catch((e) => { - getLogger().error('processChatItemFeedbackMessage failed: %s', (e as Error).message) - }) - }) - - this.chatControllerMessageListeners.followUpClicked.event((data) => { - switch (data.followUp.type) { - case FollowUpTypes.ViewDiff: - return this.openDiff(data) - case FollowUpTypes.AcceptCode: - return this.acceptCode(data) - case FollowUpTypes.RejectCode: - return this.endSession(data, FollowUpTypes.RejectCode) - case FollowUpTypes.ContinueBuildAndExecute: - return this.handleBuildIteration(data) - case FollowUpTypes.BuildAndExecute: - return this.checkForInstallationDependencies(data) - case FollowUpTypes.ModifyCommands: - return this.modifyBuildCommand(data) - case FollowUpTypes.SkipBuildAndFinish: - return this.endSession(data, FollowUpTypes.SkipBuildAndFinish) - case FollowUpTypes.InstallDependenciesAndContinue: - return this.handleInstallDependencies(data) - case FollowUpTypes.ViewCodeDiffAfterIteration: - return this.openDiff(data) - } - }) - - AuthUtil.instance.regionProfileManager.onDidChangeRegionProfile(() => { - this.sessionStorage.removeActiveTab() - }) - } - - /** - * Basic Functions - */ - private async tabOpened(message: any) { - const session: Session = this.sessionStorage.getSession() - const tabID = this.sessionStorage.setActiveTab(message.tabID) - const logger = getLogger() - logger.debug('Tab opened Processing message tabId: %s', message.tabID) - - // check if authentication has expired - try { - logger.debug(`Q - Test: Session created with id: ${session.tabID}`) - - const authState = await AuthUtil.instance.getChatAuthState() - if (authState.amazonQ !== 'connected') { - void this.messenger.sendAuthNeededExceptionMessage(authState, tabID) - session.isAuthenticating = true - return - } - } catch (err: any) { - logger.error('tabOpened failed: %O', err) - this.messenger.sendErrorMessage(err.message, message.tabID) - } - } - - private async processChatItemVotedMessage(message: ChatItemVotedMessage) { - const session = this.sessionStorage.getSession() - - telemetry.amazonq_feedback.emit({ - featureId: 'amazonQTest', - amazonqConversationId: session.startTestGenerationRequestId, - credentialStartUrl: AuthUtil.instance.startUrl, - interactionType: message.vote, - }) - } - - private async processChatItemFeedbackMessage(message: any) { - const session = this.sessionStorage.getSession() - - await globals.telemetry.postFeedback({ - comment: `${JSON.stringify({ - type: 'testgen-chat-answer-feedback', - amazonqConversationId: session.startTestGenerationRequestId, - reason: message?.selectedOption, - userComment: message?.comment, - })}`, - sentiment: 'Negative', - }) - } - - private async tabClosed(data: any) { - getLogger().debug('Tab closed with data tab id: %s', data.tabID) - await this.sessionCleanUp() - getLogger().debug('Removing active tab') - this.sessionStorage.removeActiveTab() - } - - private authClicked(message: any) { - this.authController.handleAuth(message.authType) - - this.messenger.sendMessage('Follow instructions to re-authenticate ...', message.tabID, 'answer') - - // Explicitly ensure the user goes through the re-authenticate flow - this.messenger.sendChatInputEnabled(message.tabID, false) - } - - private processLink(message: any) { - void openUrl(vscode.Uri.parse(message.link)) - } - - private handleInsertCodeAtCursorPosition(message: any) { - this.editorContentController.insertTextAtCursorPosition(message.code, () => {}) - } - - private checkCodeDiffLengthAndBuildStatus(state: { codeDiffLength: number; buildStatus: BuildStatus }): boolean { - return state.codeDiffLength !== 0 && state.buildStatus !== BuildStatus.SUCCESS - } - - // Displaying error message to the user in the chat tab - private async handleErrorMessage(data: any) { - testGenState.setToNotStarted() - // eslint-disable-next-line unicorn/no-null - this.messenger.sendUpdatePromptProgress(data.tabID, null) - const session = this.sessionStorage.getSession() - const isCancel = data.error.uiMessage === unitTestGenerationCancelMessage - let telemetryErrorMessage = getTelemetryReasonDesc(data.error) - if (session.stopIteration) { - telemetryErrorMessage = getTelemetryReasonDesc(data.error.uiMessage.replaceAll('```', '')) - } - TelemetryHelper.instance.sendTestGenerationToolkitEvent( - session, - session.isSupportedLanguage, - true, - isCancel ? 'Cancelled' : 'Failed', - session.startTestGenerationRequestId, - performance.now() - session.testGenerationStartTime, - telemetryErrorMessage, - session.isCodeBlockSelected, - session.artifactsUploadDuration, - session.srcPayloadSize, - session.srcZipFileSize, - session.charsOfCodeAccepted, - session.numberOfTestsGenerated, - session.linesOfCodeGenerated, - session.charsOfCodeGenerated, - session.numberOfTestsGenerated, - session.linesOfCodeGenerated, - undefined, - isCancel ? 'CANCELLED' : 'FAILED' - ) - if (session.stopIteration) { - // Error from Science - this.messenger.sendMessage( - data.error.uiMessage.replaceAll('```', ''), - data.tabID, - 'answer', - 'testGenErrorMessage', - this.getFeedbackButtons() - ) - } else { - isCancel - ? this.messenger.sendMessage( - data.error.uiMessage, - data.tabID, - 'answer', - 'testGenErrorMessage', - this.getFeedbackButtons() - ) - : this.sendErrorMessage(data) - } - await this.sessionCleanUp() - return - } - // Client side error messages - private sendErrorMessage(data: { - tabID: string - error: { uiMessage: string; message: string; code: string; statusCode: string } - }) { - const { error, tabID } = data - - // If user reached monthly limit for builderId - if (error.code === 'CreateTestJobError') { - if (error.message.includes(utgLimitReached)) { - getLogger().error('Monthly quota reached for QSDA actions.') - return this.messenger.sendMessage( - i18n('AWS.amazonq.featureDev.error.monthlyLimitReached'), - tabID, - 'answer', - 'testGenErrorMessage', - this.getFeedbackButtons() - ) - } - if (error.message.includes('Too many requests')) { - getLogger().error(error.message) - return this.messenger.sendErrorMessage(tooManyRequestErrorMessage, tabID) - } - } - if (isAwsError(error)) { - if (error.code === 'ThrottlingException') { - // TODO: use the explicitly modeled exception reason for quota vs throttle{ - getLogger().error(error.message) - this.messenger.sendErrorMessage(tooManyRequestErrorMessage, tabID) - return - } - // other service errors: - // AccessDeniedException - should not happen because access is validated before this point in the client - // ValidationException - shouldn't happen because client should not send malformed requests - // ConflictException - should not happen because the client will maintain proper state - // InternalServerException - shouldn't happen but needs to be caught - getLogger().error('Other error message: %s', error.message) - this.messenger.sendErrorMessage('', tabID) - return - } - // other unexpected errors (TODO enumerate all other failure cases) - getLogger().error('Other error message: %s', error.uiMessage) - this.messenger.sendErrorMessage('', tabID) - } - - // This function handles actions if user clicked on any Button one of these cases will be executed - private async handleFormActionClicked(data: any) { - const typedAction = MessengerUtils.stringToEnumValue(ButtonActions, data.action as any) - let getFeedbackCommentData = '' - switch (typedAction) { - case ButtonActions.STOP_TEST_GEN: - testGenState.setToCancelling() - telemetry.ui_click.emit({ elementId: 'unitTestGeneration_cancelTestGenerationProgress' }) - break - case ButtonActions.STOP_BUILD: - cancelBuild() - void this.handleUpdatePromptProgress({ status: 'cancel', tabID: data.tabID }) - telemetry.ui_click.emit({ elementId: 'unitTestGeneration_cancelBuildProgress' }) - this.messenger.sendChatInputEnabled(data.tabID, true) - await this.sessionCleanUp() - break - case ButtonActions.PROVIDE_FEEDBACK: - getFeedbackCommentData = `Q Test Generation: RequestId: ${this.sessionStorage.getSession().startTestGenerationRequestId}, TestGenerationJobId: ${this.sessionStorage.getSession().testGenerationJob?.testGenerationJobId}` - void submitFeedback(placeholder, 'Amazon Q', getFeedbackCommentData) - telemetry.ui_click.emit({ elementId: 'unitTestGeneration_provideFeedback' }) - break - } - } - // This function handles actions if user gives any input from the chatInput box - private async processHumanChatMessage(data: { prompt: string; tabID: string }) { - const session = this.sessionStorage.getSession() - const conversationState = session.conversationState - - if (conversationState === ConversationState.WAITING_FOR_BUILD_COMMMAND_INPUT) { - this.messenger.sendChatInputEnabled(data.tabID, false) - this.sessionStorage.getSession().conversationState = ConversationState.IDLE - session.updatedBuildCommands = [data.prompt] - const updatedCommands = session.updatedBuildCommands.join('\n') - this.messenger.sendMessage(`Updated command to \`${updatedCommands}\``, data.tabID, 'prompt') - await this.checkForInstallationDependencies(data) - return - } else { - await this.startTestGen(data, false) - } - } - // This function takes filePath as input parameter and returns file language - private async getLanguageForFilePath(filePath: string): Promise { - try { - const document = await vscode.workspace.openTextDocument(filePath) - return document.languageId - } catch (error) { - return 'plaintext' - } - } - - private getFeedbackButtons(): ChatItemButton[] { - const buttons: ChatItemButton[] = [] - if (Auth.instance.isInternalAmazonUser()) { - buttons.push({ - keepCardAfterClick: true, - text: 'How can we make /test better?', - id: ButtonActions.PROVIDE_FEEDBACK, - disabled: false, // allow button to be re-clicked - position: 'outside', - icon: 'comment' as MynahIcons, - }) - } - return buttons - } - - /** - * Start Test Generation and show the code results - */ - - private async startTestGen(message: any, regenerateTests: boolean) { - const session: Session = this.sessionStorage.getSession() - // Perform session cleanup before start of unit test generation workflow unless there is an existing job in progress. - if (!ChatSessionManager.Instance.getIsInProgress()) { - await this.sessionCleanUp() - } - const tabID = this.sessionStorage.setActiveTab(message.tabID) - getLogger().debug('startTestGen message: %O', message) - getLogger().debug('startTestGen tabId: %O', message.tabID) - let fileName = '' - let filePath = '' - let userFacingMessage = '' - let userPrompt = '' - session.testGenerationStartTime = performance.now() - - try { - if (ChatSessionManager.Instance.getIsInProgress()) { - void vscode.window.showInformationMessage( - "There is already a test generation job in progress. Cancel current job or wait until it's finished to try again." - ) - return - } - if (testGenState.isCancelling()) { - void vscode.window.showInformationMessage( - 'There is a test generation job being cancelled. Please wait for cancellation to finish.' - ) - return - } - // Truncating the user prompt if the prompt is more than 4096. - userPrompt = message.prompt.slice(0, maxUserPromptLength) - - // check that the session is authenticated - const authState = await AuthUtil.instance.getChatAuthState() - if (authState.amazonQ !== 'connected') { - void this.messenger.sendAuthNeededExceptionMessage(authState, tabID) - session.isAuthenticating = true - return - } - - // check that a project/workspace is open - const workspaceFolders = vscode.workspace.workspaceFolders - if (workspaceFolders === undefined || workspaceFolders.length === 0) { - this.messenger.sendUnrecoverableErrorResponse('no-project-found', tabID) - return - } - - // check if IDE has active file open. - const activeEditor = vscode.window.activeTextEditor - // also check all open editors and allow this to proceed if only one is open (even if not main focus) - const allVisibleEditors = vscode.window.visibleTextEditors - const openFileEditors = allVisibleEditors.filter((editor) => editor.document.uri.scheme === 'file') - const hasOnlyOneOpenFileSplitView = openFileEditors.length === 1 - getLogger().debug(`hasOnlyOneOpenSplitView: ${hasOnlyOneOpenFileSplitView}`) - // is not a file if the currently highlighted window is not a file, and there is either more than one or no file windows open - const isNotFile = activeEditor?.document.uri.scheme !== 'file' && !hasOnlyOneOpenFileSplitView - getLogger().debug(`activeEditor: ${activeEditor}, isNotFile: ${isNotFile}`) - if (!activeEditor || isNotFile) { - this.messenger.sendUnrecoverableErrorResponse( - isNotFile ? 'invalid-file-type' : 'no-open-file-found', - tabID - ) - this.messenger.sendUpdatePlaceholder( - tabID, - 'Please open and highlight a source code file in order to generate tests.' - ) - this.messenger.sendChatInputEnabled(tabID, true) - this.sessionStorage.getSession().conversationState = ConversationState.WAITING_FOR_INPUT - return - } - - const fileEditorToTest = hasOnlyOneOpenFileSplitView ? openFileEditors[0] : activeEditor - getLogger().debug(`File path: ${fileEditorToTest.document.uri.fsPath}`) - filePath = fileEditorToTest.document.uri.fsPath - fileName = path.basename(filePath) - userFacingMessage = userPrompt - ? regenerateTests - ? `${userPrompt}` - : `/test ${userPrompt}` - : `/test Generate unit tests for \`${fileName}\`` - - session.hasUserPromptSupplied = userPrompt.length > 0 - - // displaying user message prompt in Test tab - this.messenger.sendMessage(userFacingMessage, tabID, 'prompt') - this.messenger.sendChatInputEnabled(tabID, false) - this.sessionStorage.getSession().conversationState = ConversationState.IN_PROGRESS - this.messenger.sendUpdatePromptProgress(message.tabID, testGenProgressField) - - const language = await this.getLanguageForFilePath(filePath) - session.fileLanguage = language - const workspaceFolder = vscode.workspace.getWorkspaceFolder(fileEditorToTest.document.uri) - - /* - For Re:Invent 2024 we are supporting only java and python for unit test generation, rest of the languages shows the similar experience as CWC - */ - if (!['java', 'python'].includes(language) || workspaceFolder === undefined) { - if (!workspaceFolder) { - // File is outside of workspace - const unsupportedMessage = `I can't generate tests for ${fileName} because the file is outside of workspace scope.
I can still provide examples, instructions and code suggestions.` - this.messenger.sendMessage(unsupportedMessage, tabID, 'answer') - } - // Keeping this metric as is. TODO - Change to true once we support through other feature - session.isSupportedLanguage = false - await this.onCodeGeneration( - session, - userPrompt, - tabID, - fileName, - filePath, - workspaceFolder !== undefined - ) - } else { - this.messenger.sendCapabilityCard({ tabID }) - this.messenger.sendMessage(testGenSummaryMessage(fileName), message.tabID, 'answer-part') - - // Grab the selection from the fileEditorToTest and get the vscode Range - const selection = fileEditorToTest.selection - let selectionRange = undefined - if ( - selection.start.line !== selection.end.line || - selection.start.character !== selection.end.character - ) { - selectionRange = new vscode.Range( - selection.start.line, - selection.start.character, - selection.end.line, - selection.end.character - ) - } - session.isCodeBlockSelected = selectionRange !== undefined - session.isSupportedLanguage = true - - /** - * Zip the project - * Create pre-signed URL and upload artifact to S3 - * send API request to startTestGeneration API - * Poll from getTestGeneration API - * Get Diff from exportResultArchive API - */ - ChatSessionManager.Instance.setIsInProgress(true) - await startTestGenerationProcess(filePath, message.prompt, tabID, true, selectionRange) - } - } catch (err: any) { - // TODO: refactor error handling to be more robust - ChatSessionManager.Instance.setIsInProgress(false) - getLogger().error('startTestGen failed: %O', err) - this.messenger.sendUpdatePromptProgress(message.tabID, cancellingProgressField) - this.sendErrorMessage({ tabID, error: err }) - this.messenger.sendChatInputEnabled(tabID, true) - this.sessionStorage.getSession().conversationState = ConversationState.WAITING_FOR_INPUT - await sleep(2000) - // eslint-disable-next-line unicorn/no-null - this.messenger.sendUpdatePromptProgress(message.tabID, null) - } - } - - // Updating Progress bar - private async handleUpdatePromptProgress(data: any) { - const getProgressField = (status: string): ProgressField | null => { - switch (status) { - case 'Completed': - return testGenCompletedField - case 'Error': - return errorProgressField - case 'cancel': - return cancellingProgressField - case 'InProgress': - default: - return { - status: 'info', - text: 'Generating unit tests...', - value: data.progressRate, - valueText: data.progressRate.toString() + '%', - actions: [cancelTestGenButton], - } - } - } - this.messenger.sendUpdatePromptProgress(data.tabID, getProgressField(data.status)) - - await sleep(2000) - - // don't flash the bar when generation in progress - if (data.status !== 'InProgress') { - // eslint-disable-next-line unicorn/no-null - this.messenger.sendUpdatePromptProgress(data.tabID, null) - } - } - - private async updateTargetFileInfo(message: { - tabID: string - targetFileInfo?: TargetFileInfo - testGenerationJobGroupName: string - testGenerationJobId: string - type: ChatItemType - filePath: string - }) { - this.messenger.sendShortSummary({ - type: 'answer', - tabID: message.tabID, - message: testGenSummaryMessage( - path.basename(message.targetFileInfo?.filePath ?? message.filePath), - message.targetFileInfo?.filePlan?.replaceAll('```', '') - ), - canBeVoted: true, - filePath: message.targetFileInfo?.testFilePath, - }) - } - - private async showCodeGenerationResults(data: { tabID: string; filePath: string; projectName: string }) { - const session = this.sessionStorage.getSession() - // return early if references are disabled and there are references - if (!CodeWhispererSettings.instance.isSuggestionsWithCodeReferencesEnabled() && session.references.length > 0) { - void vscode.window.showInformationMessage('Your settings do not allow code generation with references.') - await this.endSession(data, FollowUpTypes.SkipBuildAndFinish) - await this.sessionCleanUp() - return - } - const followUps: FollowUps = { - text: '', - options: [ - { - pillText: `View diff`, - type: FollowUpTypes.ViewDiff, - status: 'primary', - }, - ], - } - session.generatedFilePath = data.filePath - try { - const tempFilePath = path.join(this.tempResultDirPath, 'resultArtifacts', data.filePath) - const newContent = await fs.readFileText(tempFilePath) - const workspaceFolder = vscode.workspace.workspaceFolders?.[0] - let linesGenerated = newContent.split('\n').length - let charsGenerated = newContent.length - if (workspaceFolder) { - const projectPath = workspaceFolder.uri.fsPath - const absolutePath = path.join(projectPath, data.filePath) - const fileExists = await fs.existsFile(absolutePath) - if (fileExists) { - const originalContent = await fs.readFileText(absolutePath) - linesGenerated -= originalContent.split('\n').length - charsGenerated -= originalContent.length - } - } - session.linesOfCodeGenerated = linesGenerated > 0 ? linesGenerated : 0 - session.charsOfCodeGenerated = charsGenerated > 0 ? charsGenerated : 0 - } catch (e: any) { - getLogger().debug('failed to get chars and lines of code generated from test generation result: %O', e) - } - - this.messenger.sendBuildProgressMessage({ - tabID: data.tabID, - messageType: 'answer', - codeGenerationId: '', - message: `${session.jobSummary}\n\n Please see the unit tests generated below. Click “View diff” to review the changes in the code editor.`, - canBeVoted: true, - messageId: '', - followUps, - fileList: { - fileTreeTitle: 'READY FOR REVIEW', - rootFolderTitle: data.projectName, - filePaths: [data.filePath], - }, - codeReference: session.references.map( - (ref: Reference) => - ({ - ...ref, - information: `${ref.licenseName} - ${ref.repository}`, - }) as CodeReference - ), - }) - this.messenger.sendChatInputEnabled(data.tabID, false) - this.messenger.sendUpdatePlaceholder(data.tabID, `Select View diff to see the generated unit tests.`) - this.sessionStorage.getSession().conversationState = ConversationState.IDLE - } - - private async openDiff(message: OpenDiffMessage) { - const session = this.sessionStorage.getSession() - const filePath = session.generatedFilePath - const absolutePath = path.join(session.projectRootPath, filePath) - const fileExists = await fs.existsFile(absolutePath) - const leftUri = fileExists ? vscode.Uri.file(absolutePath) : vscode.Uri.from({ scheme: 'untitled' }) - const rightUri = vscode.Uri.file(path.join(this.tempResultDirPath, 'resultArtifacts', filePath)) - const fileName = path.basename(absolutePath) - await vscode.commands.executeCommand('vscode.diff', leftUri, rightUri, `${fileName} ${amazonQTabSuffix}`) - telemetry.ui_click.emit({ elementId: 'unitTestGeneration_viewDiff' }) - session.latencyOfTestGeneration = performance.now() - session.testGenerationStartTime - this.messenger.sendUpdatePlaceholder(message.tabID, `Please select an action to proceed (Accept or Reject)`) - } - - private async acceptCode(message: any) { - const session = this.sessionStorage.getSession() - session.acceptedJobId = session.listOfTestGenerationJobId[session.listOfTestGenerationJobId.length - 1] - const filePath = session.generatedFilePath - const absolutePath = path.join(session.projectRootPath, filePath) - const fileExists = await fs.existsFile(absolutePath) - const buildCommand = session.updatedBuildCommands?.join(' ') - - const tempFilePath = path.join(this.tempResultDirPath, 'resultArtifacts', filePath) - const updatedContent = await fs.readFileText(tempFilePath) - let acceptedLines = updatedContent.split('\n').length - let acceptedChars = updatedContent.length - if (fileExists) { - const originalContent = await fs.readFileText(absolutePath) - acceptedLines -= originalContent.split('\n').length - acceptedLines = acceptedLines < 0 ? 0 : acceptedLines - acceptedChars -= originalContent.length - acceptedChars = acceptedChars < 0 ? 0 : acceptedChars - UserWrittenCodeTracker.instance.onQStartsMakingEdits() - const document = await vscode.workspace.openTextDocument(absolutePath) - await applyChanges( - document, - new vscode.Range(document.lineAt(0).range.start, document.lineAt(document.lineCount - 1).range.end), - updatedContent - ) - UserWrittenCodeTracker.instance.onQFinishesEdits() - } else { - await fs.writeFile(absolutePath, updatedContent) - } - session.charsOfCodeAccepted = acceptedChars - session.linesOfCodeAccepted = acceptedLines - - // add accepted references to reference log, if any - const fileName = path.basename(session.generatedFilePath) - const time = new Date().toLocaleString() - // TODO: this is duplicated in basicCommands.ts for scan (codewhisperer). Fix this later. - for (const reference of session.references) { - getLogger().debug('Processing reference: %O', reference) - // Log values for debugging - getLogger().debug('updatedContent: %s', updatedContent) - getLogger().debug( - 'start: %d, end: %d', - reference.recommendationContentSpan?.start, - reference.recommendationContentSpan?.end - ) - // given a start and end index, figure out which line number they belong to when splitting a string on /n characters - const getLineNumber = (content: string, index: number): number => { - const lines = content.slice(0, index).split('\n') - return lines.length - } - const startLine = getLineNumber(updatedContent, reference.recommendationContentSpan!.start) - const endLine = getLineNumber(updatedContent, reference.recommendationContentSpan!.end) - getLogger().debug('startLine: %d, endLine: %d', startLine, endLine) - - const code = updatedContent.slice( - reference.recommendationContentSpan?.start, - reference.recommendationContentSpan?.end - ) - getLogger().debug('Extracted code slice: %s', code) - const referenceLog = - `[${time}] Accepted recommendation ` + - referenceLogText( - `
${code}
`, - reference.licenseName!, - reference.repository!, - fileName, - startLine === endLine ? `(line at ${startLine})` : `(lines from ${startLine} to ${endLine})` - ) + - '
' - getLogger().debug('Adding reference log: %s', referenceLog) - ReferenceLogViewProvider.instance.addReferenceLog(referenceLog) - } - - // TODO: see if there's a better way to check if active file is a diff - if (vscode.window.tabGroups.activeTabGroup.activeTab?.label.includes(amazonQTabSuffix)) { - await vscode.commands.executeCommand('workbench.action.closeActiveEditor') - } - const document = await vscode.workspace.openTextDocument(absolutePath) - await vscode.window.showTextDocument(document) - // TODO: send the message once again once build is enabled - // this.messenger.sendMessage('Accepted', message.tabID, 'prompt') - telemetry.ui_click.emit({ elementId: 'unitTestGeneration_acceptDiff' }) - - getLogger().info( - `Generated unit tests are accepted for ${session.fileLanguage ?? 'plaintext'} language with jobId: ${session.listOfTestGenerationJobId[0]}, jobGroupName: ${session.testGenerationJobGroupName}, result: Succeeded` - ) - TelemetryHelper.instance.sendTestGenerationToolkitEvent( - session, - true, - true, - 'Succeeded', - session.startTestGenerationRequestId, - session.latencyOfTestGeneration, - undefined, - session.isCodeBlockSelected, - session.artifactsUploadDuration, - session.srcPayloadSize, - session.srcZipFileSize, - session.charsOfCodeAccepted, - session.numberOfTestsGenerated, - session.linesOfCodeAccepted, - session.charsOfCodeGenerated, - session.numberOfTestsGenerated, - session.linesOfCodeGenerated, - undefined, - 'ACCEPTED' - ) - - await this.endSession(message, FollowUpTypes.SkipBuildAndFinish) - return - - if (session.listOfTestGenerationJobId.length === 1) { - this.startInitialBuild(message) - this.messenger.sendChatInputEnabled(message.tabID, false) - } else if (session.listOfTestGenerationJobId.length < 4) { - const remainingIterations = 4 - session.listOfTestGenerationJobId.length - - let userMessage = 'Would you like Amazon Q to build and execute again, and fix errors?' - if (buildCommand) { - userMessage += ` I will be running this build command: \`${buildCommand}\`` - } - userMessage += `\nYou have ${remainingIterations} iteration${remainingIterations > 1 ? 's' : ''} left.` - - const followUps: FollowUps = { - text: '', - options: [ - { - pillText: `Rebuild`, - type: FollowUpTypes.ContinueBuildAndExecute, - status: 'primary', - }, - { - pillText: `Skip and finish`, - type: FollowUpTypes.SkipBuildAndFinish, - status: 'primary', - }, - ], - } - this.messenger.sendBuildProgressMessage({ - tabID: message.tabID, - messageType: 'answer', - codeGenerationId: '', - message: userMessage, - canBeVoted: false, - messageId: '', - followUps: followUps, - }) - this.messenger.sendChatInputEnabled(message.tabID, false) - } else { - this.sessionStorage.getSession().listOfTestGenerationJobId = [] - this.messenger.sendMessage( - 'You have gone through both iterations and this unit test generation workflow is complete.', - message.tabID, - 'answer' - ) - await this.sessionCleanUp() - } - await fs.delete(this.tempResultDirPath, { recursive: true }) - } - - /** - * Handle a regular incoming message when a user is in the code generation phase - */ - private async onCodeGeneration( - session: Session, - message: string, - tabID: string, - fileName: string, - filePath: string, - fileInWorkspace: boolean - ) { - try { - // TODO: Write this entire gen response to basiccommands and call here. - const editorText = await fs.readFileText(filePath) - - const triggerPayload: TriggerPayload = { - query: `Generate unit tests for the following part of my code: ${message?.trim() || fileName}`, - codeSelection: undefined, - trigger: ChatTriggerType.ChatMessage, - fileText: editorText, - fileLanguage: session.fileLanguage, - filePath: filePath, - message: `Generate unit tests for the following part of my code: ${message?.trim() || fileName}`, - matchPolicy: undefined, - codeQuery: undefined, - userIntent: UserIntent.GENERATE_UNIT_TESTS, - customization: getSelectedCustomization(), - profile: AuthUtil.instance.regionProfileManager.activeRegionProfile, - context: [], - relevantTextDocuments: [], - additionalContents: [], - documentReferences: [], - useRelevantDocuments: false, - contextLengths: { - ...defaultContextLengths, - }, - } - const chatRequest = triggerPayloadToChatRequest(triggerPayload) - const client = await createCodeWhispererChatStreamingClient() - const response = await client.generateAssistantResponse(chatRequest) - UserWrittenCodeTracker.instance.onQFeatureInvoked() - await this.messenger.sendAIResponse( - response, - session, - tabID, - randomUUID.toString(), - triggerPayload, - fileName, - fileInWorkspace - ) - } finally { - this.messenger.sendChatInputEnabled(tabID, true) - this.messenger.sendUpdatePlaceholder(tabID, `/test Generate unit tests...`) - this.sessionStorage.getSession().conversationState = ConversationState.WAITING_FOR_INPUT - } - } - - // TODO: Check if there are more cases to endSession if yes create a enum or type for step - private async endSession(data: any, step: FollowUpTypes) { - this.messenger.sendMessage( - 'Unit test generation completed.', - data.tabID, - 'answer', - 'testGenEndSessionMessage', - this.getFeedbackButtons() - ) - - const session = this.sessionStorage.getSession() - if (step === FollowUpTypes.RejectCode) { - TelemetryHelper.instance.sendTestGenerationToolkitEvent( - session, - true, - true, - 'Succeeded', - session.startTestGenerationRequestId, - session.latencyOfTestGeneration, - undefined, - session.isCodeBlockSelected, - session.artifactsUploadDuration, - session.srcPayloadSize, - session.srcZipFileSize, - 0, - 0, - 0, - session.charsOfCodeGenerated, - session.numberOfTestsGenerated, - session.linesOfCodeGenerated, - undefined, - 'REJECTED' - ) - telemetry.ui_click.emit({ elementId: 'unitTestGeneration_rejectDiff' }) - } - - await this.sessionCleanUp() - - // this.messenger.sendMessage(`Unit test generation workflow is completed.`, data.tabID, 'answer') - this.messenger.sendChatInputEnabled(data.tabID, true) - return - } - - /** - * BUILD LOOP IMPLEMENTATION - */ - - private startInitialBuild(data: any) { - // TODO: Remove the fallback build command after stable version of backend build command. - const userMessage = `Would you like me to help build and execute the test? I will need you to let me know what build command to run if you do.` - const followUps: FollowUps = { - text: '', - options: [ - { - pillText: `Specify command then build and execute`, - type: FollowUpTypes.ModifyCommands, - status: 'primary', - }, - { - pillText: `Skip and finish`, - type: FollowUpTypes.SkipBuildAndFinish, - status: 'primary', - }, - ], - } - this.messenger.sendBuildProgressMessage({ - tabID: data.tabID, - messageType: 'answer', - codeGenerationId: '', - message: userMessage, - canBeVoted: false, - messageId: '', - followUps: followUps, - }) - this.messenger.sendChatInputEnabled(data.tabID, false) - } - - private async checkForInstallationDependencies(data: any) { - // const session: Session = this.sessionStorage.getSession() - // const listOfInstallationDependencies = session.testGenerationJob?.shortAnswer?.installationDependencies || [] - // MOCK: As there is no installation dependencies in shortAnswer - const listOfInstallationDependencies = [''] - const installationDependencies = listOfInstallationDependencies.join('\n') - - this.messenger.sendMessage('Build and execute', data.tabID, 'prompt') - telemetry.ui_click.emit({ elementId: 'unitTestGeneration_buildAndExecute' }) - - if (installationDependencies.length > 0) { - this.messenger.sendBuildProgressMessage({ - tabID: data.tabID, - messageType: 'answer', - codeGenerationId: '', - message: `Looks like you don’t have ${listOfInstallationDependencies.length > 1 ? `these` : `this`} ${listOfInstallationDependencies.length} required package${listOfInstallationDependencies.length > 1 ? `s` : ``} installed.\n\`\`\`sh\n${installationDependencies}\n`, - canBeVoted: false, - messageId: '', - followUps: { - text: '', - options: [ - { - pillText: `Install and continue`, - type: FollowUpTypes.InstallDependenciesAndContinue, - status: 'primary', - }, - { - pillText: `Skip and finish`, - type: FollowUpTypes.SkipBuildAndFinish, - status: 'primary', - }, - ], - }, - }) - } else { - await this.startLocalBuildExecution(data) - } - } - - private async handleInstallDependencies(data: any) { - this.messenger.sendMessage('Installation dependencies and continue', data.tabID, 'prompt') - telemetry.ui_click.emit({ elementId: 'unitTestGeneration_installDependenciesAndContinue' }) - void this.startLocalBuildExecution(data) - } - - private async handleBuildIteration(data: any) { - this.messenger.sendMessage('Proceed with Iteration', data.tabID, 'prompt') - telemetry.ui_click.emit({ elementId: 'unitTestGeneration_proceedWithIteration' }) - await this.startLocalBuildExecution(data) - } - - private async startLocalBuildExecution(data: any) { - const session: Session = this.sessionStorage.getSession() - // const installationDependencies = session.shortAnswer?.installationDependencies ?? [] - // MOCK: ignoring the installation case until backend send response - const installationDependencies: string[] = [] - const buildCommands = session.updatedBuildCommands - if (!buildCommands) { - throw new Error('Build command not found') - return - } - - this.messenger.sendBuildProgressMessage({ - tabID: data.tabID, - messageType: 'answer-part', - codeGenerationId: TestNamedMessages.TEST_GENERATION_BUILD_STATUS_MESSAGE, - message: testGenBuildProgressMessage(TestGenerationBuildStep.START_STEP), - canBeVoted: false, - messageId: TestNamedMessages.TEST_GENERATION_BUILD_STATUS_MESSAGE, - }) - - this.messenger.sendUpdatePromptProgress(data.tabID, buildProgressField) - - if (installationDependencies.length > 0 && session.listOfTestGenerationJobId.length < 2) { - this.messenger.sendBuildProgressMessage({ - tabID: data.tabID, - messageType: 'answer-part', - codeGenerationId: TestNamedMessages.TEST_GENERATION_BUILD_STATUS_MESSAGE, - message: testGenBuildProgressMessage(TestGenerationBuildStep.INSTALL_DEPENDENCIES, 'current'), - canBeVoted: false, - messageId: TestNamedMessages.TEST_GENERATION_BUILD_STATUS_MESSAGE, - }) - - const status = await runBuildCommand(installationDependencies) - // TODO: Add separate status for installation dependencies - session.buildStatus = status - if (status === BuildStatus.FAILURE) { - this.messenger.sendBuildProgressMessage({ - tabID: data.tabID, - messageType: 'answer-part', - codeGenerationId: TestNamedMessages.TEST_GENERATION_BUILD_STATUS_MESSAGE, - message: testGenBuildProgressMessage(TestGenerationBuildStep.INSTALL_DEPENDENCIES, 'error'), - canBeVoted: false, - messageId: TestNamedMessages.TEST_GENERATION_BUILD_STATUS_MESSAGE, - }) - } - if (status === BuildStatus.CANCELLED) { - this.messenger.sendBuildProgressMessage({ - tabID: data.tabID, - messageType: 'answer-part', - codeGenerationId: TestNamedMessages.TEST_GENERATION_BUILD_STATUS_MESSAGE, - message: testGenBuildProgressMessage(TestGenerationBuildStep.INSTALL_DEPENDENCIES, 'error'), - canBeVoted: false, - messageId: TestNamedMessages.TEST_GENERATION_BUILD_STATUS_MESSAGE, - }) - this.messenger.sendMessage('Installation dependencies Cancelled', data.tabID, 'prompt') - this.messenger.sendMessage( - 'Unit test generation workflow is complete. You have 25 out of 30 Amazon Q Developer Agent invocations left this month.', - data.tabID, - 'answer' - ) - return - } - this.messenger.sendBuildProgressMessage({ - tabID: data.tabID, - messageType: 'answer-part', - codeGenerationId: TestNamedMessages.TEST_GENERATION_BUILD_STATUS_MESSAGE, - message: testGenBuildProgressMessage(TestGenerationBuildStep.INSTALL_DEPENDENCIES, 'done'), - canBeVoted: false, - messageId: TestNamedMessages.TEST_GENERATION_BUILD_STATUS_MESSAGE, - }) - } - - this.messenger.sendBuildProgressMessage({ - tabID: data.tabID, - messageType: 'answer-part', - codeGenerationId: TestNamedMessages.TEST_GENERATION_BUILD_STATUS_MESSAGE, - message: testGenBuildProgressMessage(TestGenerationBuildStep.RUN_BUILD, 'current'), - canBeVoted: false, - messageId: TestNamedMessages.TEST_GENERATION_BUILD_STATUS_MESSAGE, - }) - - const buildStatus = await runBuildCommand(buildCommands) - session.buildStatus = buildStatus - - if (buildStatus === BuildStatus.FAILURE) { - this.messenger.sendBuildProgressMessage({ - tabID: data.tabID, - messageType: 'answer-part', - codeGenerationId: TestNamedMessages.TEST_GENERATION_BUILD_STATUS_MESSAGE, - message: testGenBuildProgressMessage(TestGenerationBuildStep.RUN_BUILD, 'error'), - canBeVoted: false, - messageId: TestNamedMessages.TEST_GENERATION_BUILD_STATUS_MESSAGE, - }) - } else if (buildStatus === BuildStatus.CANCELLED) { - this.messenger.sendBuildProgressMessage({ - tabID: data.tabID, - messageType: 'answer-part', - codeGenerationId: TestNamedMessages.TEST_GENERATION_BUILD_STATUS_MESSAGE, - message: testGenBuildProgressMessage(TestGenerationBuildStep.RUN_BUILD, 'error'), - canBeVoted: false, - messageId: TestNamedMessages.TEST_GENERATION_BUILD_STATUS_MESSAGE, - }) - this.messenger.sendMessage('Build Cancelled', data.tabID, 'prompt') - this.messenger.sendMessage('Unit test generation workflow is complete.', data.tabID, 'answer') - return - } else { - // Build successful - this.messenger.sendBuildProgressMessage({ - tabID: data.tabID, - messageType: 'answer-part', - codeGenerationId: TestNamedMessages.TEST_GENERATION_BUILD_STATUS_MESSAGE, - message: testGenBuildProgressMessage(TestGenerationBuildStep.RUN_BUILD, 'done'), - canBeVoted: false, - messageId: TestNamedMessages.TEST_GENERATION_BUILD_STATUS_MESSAGE, - }) - } - - // Running execution tests - this.messenger.sendBuildProgressMessage({ - tabID: data.tabID, - messageType: 'answer-part', - codeGenerationId: TestNamedMessages.TEST_GENERATION_BUILD_STATUS_MESSAGE, - message: testGenBuildProgressMessage(TestGenerationBuildStep.RUN_EXECUTION_TESTS, 'current'), - canBeVoted: false, - messageId: TestNamedMessages.TEST_GENERATION_BUILD_STATUS_MESSAGE, - }) - // After running tests - this.messenger.sendBuildProgressMessage({ - tabID: data.tabID, - messageType: 'answer-part', - codeGenerationId: TestNamedMessages.TEST_GENERATION_BUILD_STATUS_MESSAGE, - message: testGenBuildProgressMessage(TestGenerationBuildStep.RUN_EXECUTION_TESTS, 'done'), - canBeVoted: false, - messageId: TestNamedMessages.TEST_GENERATION_BUILD_STATUS_MESSAGE, - }) - if (session.buildStatus !== BuildStatus.SUCCESS) { - this.messenger.sendBuildProgressMessage({ - tabID: data.tabID, - messageType: 'answer-part', - codeGenerationId: TestNamedMessages.TEST_GENERATION_BUILD_STATUS_MESSAGE, - message: testGenBuildProgressMessage(TestGenerationBuildStep.FIXING_TEST_CASES, 'current'), - canBeVoted: false, - messageId: TestNamedMessages.TEST_GENERATION_BUILD_STATUS_MESSAGE, - }) - await startTestGenerationProcess(session.sourceFilePath, '', data.tabID, false) - } - // TODO: Skip this if startTestGenerationProcess timeouts - if (session.generatedFilePath) { - await this.showTestCaseSummary(data) - } - } - - private async showTestCaseSummary(data: { tabID: string }) { - const session: Session = this.sessionStorage.getSession() - let codeDiffLength = 0 - if (session.buildStatus !== BuildStatus.SUCCESS) { - // Check the generated test file content, if fileContent length is 0, exit the unit test generation workflow. - const tempFilePath = path.join(this.tempResultDirPath, 'resultArtifacts', session.generatedFilePath) - const codeDiffFileContent = await fs.readFileText(tempFilePath) - codeDiffLength = codeDiffFileContent.length - this.messenger.sendBuildProgressMessage({ - tabID: data.tabID, - messageType: 'answer-part', - codeGenerationId: TestNamedMessages.TEST_GENERATION_BUILD_STATUS_MESSAGE, - message: testGenBuildProgressMessage(TestGenerationBuildStep.FIXING_TEST_CASES + 1, 'done'), - canBeVoted: false, - messageId: TestNamedMessages.TEST_GENERATION_BUILD_STATUS_MESSAGE, - }) - } - - this.messenger.sendBuildProgressMessage({ - tabID: data.tabID, - messageType: 'answer-part', - codeGenerationId: TestNamedMessages.TEST_GENERATION_BUILD_STATUS_MESSAGE, - message: testGenBuildProgressMessage(TestGenerationBuildStep.PROCESS_TEST_RESULTS, 'current'), - canBeVoted: false, - messageId: TestNamedMessages.TEST_GENERATION_BUILD_STATUS_MESSAGE, - }) - - this.messenger.sendBuildProgressMessage({ - tabID: data.tabID, - messageType: 'answer-part', - codeGenerationId: TestNamedMessages.TEST_GENERATION_BUILD_STATUS_MESSAGE, - message: testGenBuildProgressMessage(TestGenerationBuildStep.PROCESS_TEST_RESULTS, 'done'), - canBeVoted: false, - messageId: TestNamedMessages.TEST_GENERATION_BUILD_STATUS_MESSAGE, - }) - - const followUps: FollowUps = { - text: '', - options: [ - { - pillText: `View diff`, - type: FollowUpTypes.ViewCodeDiffAfterIteration, - status: 'primary', - }, - ], - } - this.messenger.sendBuildProgressMessage({ - tabID: data.tabID, - messageType: 'answer-part', - codeGenerationId: TestNamedMessages.TEST_GENERATION_BUILD_STATUS_MESSAGE, - message: testGenBuildProgressMessage(TestGenerationBuildStep.PROCESS_TEST_RESULTS + 1), - canBeVoted: true, - messageId: TestNamedMessages.TEST_GENERATION_BUILD_STATUS_MESSAGE, - followUps: undefined, - fileList: this.checkCodeDiffLengthAndBuildStatus({ codeDiffLength, buildStatus: session.buildStatus }) - ? { - fileTreeTitle: 'READY FOR REVIEW', - rootFolderTitle: 'tests', - filePaths: [session.generatedFilePath], - } - : undefined, - }) - this.messenger.sendBuildProgressMessage({ - tabID: data.tabID, - messageType: 'answer', - codeGenerationId: TestNamedMessages.TEST_GENERATION_BUILD_STATUS_MESSAGE, - message: undefined, - canBeVoted: false, - messageId: TestNamedMessages.TEST_GENERATION_BUILD_STATUS_MESSAGE, - followUps: this.checkCodeDiffLengthAndBuildStatus({ codeDiffLength, buildStatus: session.buildStatus }) - ? followUps - : undefined, - fileList: undefined, - }) - - this.messenger.sendUpdatePromptProgress(data.tabID, testGenCompletedField) - await sleep(2000) - // eslint-disable-next-line unicorn/no-null - this.messenger.sendUpdatePromptProgress(data.tabID, null) - this.messenger.sendChatInputEnabled(data.tabID, false) - - if (codeDiffLength === 0 || session.buildStatus === BuildStatus.SUCCESS) { - this.messenger.sendMessage('Unit test generation workflow is complete.', data.tabID, 'answer') - await this.sessionCleanUp() - } - } - - private modifyBuildCommand(data: any) { - this.sessionStorage.getSession().conversationState = ConversationState.WAITING_FOR_BUILD_COMMMAND_INPUT - this.messenger.sendMessage('Specify commands then build', data.tabID, 'prompt') - telemetry.ui_click.emit({ elementId: 'unitTestGeneration_modifyCommand' }) - this.messenger.sendMessage( - 'Sure, provide all command lines you’d like me to run to build.', - data.tabID, - 'answer' - ) - this.messenger.sendUpdatePlaceholder(data.tabID, 'Waiting on your Inputs') - this.messenger.sendChatInputEnabled(data.tabID, true) - } - - /** Perform Session CleanUp in below cases - * UTG success - * End Session with Reject or SkipAndFinish - * After finishing 3 build loop iterations - * Error while generating unit tests - * Closing a Q-Test tab - * Progress bar cancel - */ - private async sessionCleanUp() { - const session = this.sessionStorage.getSession() - const groupName = session.testGenerationJobGroupName - const filePath = session.generatedFilePath - getLogger().debug('Entering sessionCleanUp function with filePath: %s and groupName: %s', filePath, groupName) - - vscode.window.tabGroups.all.flatMap(({ tabs }) => - tabs.map((tab) => { - if (tab.label === `${path.basename(filePath)} ${amazonQTabSuffix}`) { - const tabClosed = vscode.window.tabGroups.close(tab) - if (!tabClosed) { - getLogger().error('ChatDiff: Unable to close the diff view tab for %s', tab.label) - } - } - }) - ) - - getLogger().debug( - 'listOfTestGenerationJobId length: %d, groupName: %s', - session.listOfTestGenerationJobId.length, - groupName - ) - if (session.listOfTestGenerationJobId.length && groupName) { - for (const id of session.listOfTestGenerationJobId) { - if (id === session.acceptedJobId) { - TelemetryHelper.instance.sendTestGenerationEvent( - groupName, - id, - session.fileLanguage, - session.numberOfTestsGenerated, - session.numberOfTestsGenerated, // this is number of accepted test cases, now they can only accept all - session.linesOfCodeGenerated, - session.linesOfCodeAccepted, - session.charsOfCodeGenerated, - session.charsOfCodeAccepted - ) - } else { - TelemetryHelper.instance.sendTestGenerationEvent( - groupName, - id, - session.fileLanguage, - session.numberOfTestsGenerated, - 0, - session.linesOfCodeGenerated, - 0, - session.charsOfCodeGenerated, - 0 - ) - } - } - } - session.listOfTestGenerationJobId = [] - session.testGenerationJobGroupName = undefined - // session.testGenerationJob = undefined - session.updatedBuildCommands = undefined - session.shortAnswer = undefined - session.testCoveragePercentage = 0 - session.conversationState = ConversationState.IDLE - session.sourceFilePath = '' - session.generatedFilePath = '' - session.projectRootPath = '' - session.stopIteration = false - session.fileLanguage = undefined - ChatSessionManager.Instance.setIsInProgress(false) - session.linesOfCodeGenerated = 0 - session.linesOfCodeAccepted = 0 - session.charsOfCodeGenerated = 0 - session.charsOfCodeAccepted = 0 - session.acceptedJobId = '' - session.numberOfTestsGenerated = 0 - if (session.tabID) { - getLogger().debug('Setting input state with tabID: %s', session.tabID) - this.messenger.sendChatInputEnabled(session.tabID, true) - this.messenger.sendUpdatePlaceholder(session.tabID, 'Enter "/" for quick actions') - } - getLogger().debug( - 'Deleting output.log and temp result directory. testGenerationLogsDir: %s', - testGenerationLogsDir - ) - const outputLogPath = path.join(testGenerationLogsDir, 'output.log') - if (await fs.existsFile(outputLogPath)) { - await fs.delete(outputLogPath) - } - if ( - await fs - .stat(this.tempResultDirPath) - .then(() => true) - .catch(() => false) - ) { - await fs.delete(this.tempResultDirPath, { recursive: true }) - } - } - - // TODO: return build command when product approves - // private getBuildCommands = (): string[] => { - // const session = this.sessionStorage.getSession() - // if (session.updatedBuildCommands?.length) { - // return [...session.updatedBuildCommands] - // } - - // // For Internal amazon users only - // if (Auth.instance.isInternalAmazonUser()) { - // return ['brazil-build release'] - // } - - // if (session.shortAnswer && Array.isArray(session.shortAnswer?.buildCommands)) { - // return [...session.shortAnswer.buildCommands] - // } - - // return ['source qdev-wbr/.venv/bin/activate && pytest --continue-on-collection-errors'] - // } -} diff --git a/packages/core/src/amazonqTest/chat/controller/messenger/messenger.ts b/packages/core/src/amazonqTest/chat/controller/messenger/messenger.ts deleted file mode 100644 index 5541ef389c5..00000000000 --- a/packages/core/src/amazonqTest/chat/controller/messenger/messenger.ts +++ /dev/null @@ -1,365 +0,0 @@ -/*! - * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. - * SPDX-License-Identifier: Apache-2.0 - * - * This class controls the presentation of the various chat bubbles presented by the - * Q Test. - * - * As much as possible, all strings used in the experience should originate here. - */ - -import { AuthFollowUpType, AuthMessageDataMap } from '../../../../amazonq/auth/model' -import { FeatureAuthState } from '../../../../codewhisperer/util/authUtil' -import { - AppToWebViewMessageDispatcher, - AuthNeededException, - AuthenticationUpdateMessage, - BuildProgressMessage, - CapabilityCardMessage, - ChatInputEnabledMessage, - ChatMessage, - ChatSummaryMessage, - ErrorMessage, - UpdatePlaceholderMessage, - UpdatePromptProgressMessage, -} from '../../views/connector/connector' -import { ChatItemType } from '../../../../amazonq/commons/model' -import { ChatItemAction, ChatItemButton, ProgressField } from '@aws/mynah-ui' -import * as CodeWhispererConstants from '../../../../codewhisperer/models/constants' -import { TriggerPayload } from '../../../../codewhispererChat/controllers/chat/model' -import { - CodeWhispererStreamingServiceException, - GenerateAssistantResponseCommandOutput, -} from '@amzn/codewhisperer-streaming' -import { Session } from '../../session/session' -import { CodeReference } from '../../../../amazonq/webview/ui/apps/amazonqCommonsConnector' -import { getHttpStatusCode, getRequestId, getTelemetryReasonDesc, ToolkitError } from '../../../../shared/errors' -import { sleep, waitUntil } from '../../../../shared/utilities/timeoutUtils' -import { keys } from '../../../../shared/utilities/tsUtils' -import { cancellingProgressField, testGenCompletedField } from '../../../models/constants' -import { testGenState } from '../../../../codewhisperer/models/model' -import { TelemetryHelper } from '../../../../codewhisperer/util/telemetryHelper' - -export type UnrecoverableErrorType = 'no-project-found' | 'no-open-file-found' | 'invalid-file-type' - -export enum TestNamedMessages { - TEST_GENERATION_BUILD_STATUS_MESSAGE = 'testGenerationBuildStatusMessage', -} - -export interface FollowUps { - text?: string - options?: ChatItemAction[] -} - -export interface FileList { - fileTreeTitle?: string - rootFolderTitle?: string - filePaths?: string[] -} - -export interface SendBuildProgressMessageParams { - tabID: string - messageType: ChatItemType - codeGenerationId: string - message?: string - canBeVoted: boolean - messageId?: string - followUps?: FollowUps - fileList?: FileList - codeReference?: CodeReference[] -} - -export class Messenger { - public constructor(private readonly dispatcher: AppToWebViewMessageDispatcher) {} - - public sendCapabilityCard(params: { tabID: string }) { - this.dispatcher.sendChatMessage(new CapabilityCardMessage(params.tabID)) - } - - public sendMessage( - message: string, - tabID: string, - messageType: ChatItemType, - messageId?: string, - buttons?: ChatItemButton[] - ) { - this.dispatcher.sendChatMessage( - new ChatMessage({ message, messageType, messageId: messageId, buttons: buttons }, tabID) - ) - } - - public sendShortSummary(params: { - message?: string - type: ChatItemType - tabID: string - messageID?: string - canBeVoted?: boolean - filePath?: string - }) { - this.dispatcher.sendChatSummaryMessage( - new ChatSummaryMessage( - { - message: params.message, - messageType: params.type, - messageId: params.messageID, - canBeVoted: params.canBeVoted, - filePath: params.filePath, - }, - params.tabID - ) - ) - } - - public sendChatInputEnabled(tabID: string, enabled: boolean) { - this.dispatcher.sendChatInputEnabled(new ChatInputEnabledMessage(tabID, enabled)) - } - - public sendUpdatePlaceholder(tabID: string, newPlaceholder: string) { - this.dispatcher.sendUpdatePlaceholder(new UpdatePlaceholderMessage(tabID, newPlaceholder)) - } - - public sendUpdatePromptProgress(tabID: string, progressField: ProgressField | null) { - this.dispatcher.sendUpdatePromptProgress(new UpdatePromptProgressMessage(tabID, progressField)) - } - - public async sendAuthNeededExceptionMessage(credentialState: FeatureAuthState, tabID: string) { - let authType: AuthFollowUpType = 'full-auth' - let message = AuthMessageDataMap[authType].message - - switch (credentialState.amazonQ) { - case 'disconnected': - authType = 'full-auth' - message = AuthMessageDataMap[authType].message - break - case 'unsupported': - authType = 'use-supported-auth' - message = AuthMessageDataMap[authType].message - break - case 'expired': - authType = 're-auth' - message = AuthMessageDataMap[authType].message - break - } - - this.dispatcher.sendAuthNeededExceptionMessage(new AuthNeededException(message, authType, tabID)) - } - - public sendAuthenticationUpdate(testEnabled: boolean, authenticatingTabIDs: string[]) { - this.dispatcher.sendAuthenticationUpdate(new AuthenticationUpdateMessage(testEnabled, authenticatingTabIDs)) - } - - /** - * This method renders an error message with a button at the end that will try the - * transformation again from the beginning. This message is meant for errors that are - * completely unrecoverable: the job cannot be completed in its current state, - * and the flow must be tried again. - */ - public sendUnrecoverableErrorResponse(type: UnrecoverableErrorType, tabID: string) { - let message = '...' - switch (type) { - case 'no-project-found': - message = CodeWhispererConstants.noOpenProjectsFoundChatTestGenMessage - break - case 'no-open-file-found': - message = CodeWhispererConstants.noOpenFileFoundChatMessage - break - case 'invalid-file-type': - message = CodeWhispererConstants.invalidFileTypeChatMessage - break - } - this.sendMessage(message, tabID, 'answer-stream') - } - - public sendErrorMessage(errorMessage: string, tabID: string) { - this.dispatcher.sendErrorMessage( - new ErrorMessage(CodeWhispererConstants.genericErrorMessage, errorMessage, tabID) - ) - } - - // To show the response of unsupported languages to the user in the Q-Test tab - public async sendAIResponse( - response: GenerateAssistantResponseCommandOutput, - session: Session, - tabID: string, - triggerID: string, - triggerPayload: TriggerPayload, - fileName: string, - fileInWorkspace: boolean - ) { - let message = '' - let messageId = response.$metadata.requestId ?? '' - let codeReference: CodeReference[] = [] - - if (response.generateAssistantResponseResponse === undefined) { - throw new ToolkitError( - `Empty response from Q Developer service. Request ID: ${response.$metadata.requestId}` - ) - } - - const eventCounts = new Map() - waitUntil( - async () => { - for await (const chatEvent of response.generateAssistantResponseResponse!) { - for (const key of keys(chatEvent)) { - if ((chatEvent[key] as any) !== undefined) { - eventCounts.set(key, (eventCounts.get(key) ?? 0) + 1) - } - } - - if ( - chatEvent.codeReferenceEvent?.references !== undefined && - chatEvent.codeReferenceEvent.references.length > 0 - ) { - codeReference = [ - ...codeReference, - ...chatEvent.codeReferenceEvent.references.map((reference) => ({ - ...reference, - recommendationContentSpan: { - start: reference.recommendationContentSpan?.start ?? 0, - end: reference.recommendationContentSpan?.end ?? 0, - }, - information: `Reference code under **${reference.licenseName}** license from repository \`${reference.repository}\``, - })), - ] - } - if (testGenState.isCancelling()) { - return true - } - if ( - chatEvent.assistantResponseEvent?.content !== undefined && - chatEvent.assistantResponseEvent.content.length > 0 - ) { - message += chatEvent.assistantResponseEvent.content - this.dispatcher.sendBuildProgressMessage( - new BuildProgressMessage({ - tabID, - messageType: 'answer-part', - codeGenerationId: '', - message, - canBeVoted: false, - messageId, - followUps: undefined, - fileList: undefined, - }) - ) - } - } - return true - }, - { timeout: 60000, truthy: true } - ) - .catch((error: any) => { - let errorMessage = 'Error reading chat stream.' - let statusCode = undefined - let requestID = undefined - if (error instanceof CodeWhispererStreamingServiceException) { - errorMessage = error.message - statusCode = getHttpStatusCode(error) ?? 0 - requestID = getRequestId(error) - } - let message = 'This error is reported to the team automatically. Please try sending your message again.' - if (errorMessage !== undefined) { - message += `\n\nDetails: ${errorMessage}` - } - - if (statusCode !== undefined) { - message += `\n\nStatus Code: ${statusCode}` - } - - if (requestID !== undefined) { - messageId = requestID - message += `\n\nRequest ID: ${requestID}` - } - this.sendMessage(message.trim(), tabID, 'answer') - }) - .finally(async () => { - if (testGenState.isCancelling()) { - this.sendMessage(CodeWhispererConstants.unitTestGenerationCancelMessage, tabID, 'answer') - TelemetryHelper.instance.sendTestGenerationToolkitEvent( - session, - false, - fileInWorkspace, - 'Cancelled', - messageId, - performance.now() - session.testGenerationStartTime, - getTelemetryReasonDesc( - `TestGenCancelled: ${CodeWhispererConstants.unitTestGenerationCancelMessage}` - ), - undefined, - undefined, - undefined, - undefined, - undefined, - undefined, - undefined, - undefined, - undefined, - undefined, - 'TestGenCancelled', - 'CANCELLED' - ) - this.dispatcher.sendUpdatePromptProgress( - new UpdatePromptProgressMessage(tabID, cancellingProgressField) - ) - await sleep(500) - } else { - TelemetryHelper.instance.sendTestGenerationToolkitEvent( - session, - false, - fileInWorkspace, - 'Succeeded', - messageId, - performance.now() - session.testGenerationStartTime, - undefined, - undefined, - undefined, - undefined, - undefined, - undefined, - undefined, - undefined, - undefined, - undefined, - undefined, - undefined, - 'ACCEPTED' - ) - this.dispatcher.sendUpdatePromptProgress( - new UpdatePromptProgressMessage(tabID, testGenCompletedField) - ) - await sleep(500) - } - testGenState.setToNotStarted() - // eslint-disable-next-line unicorn/no-null - this.dispatcher.sendUpdatePromptProgress(new UpdatePromptProgressMessage(tabID, null)) - }) - } - - // To show the Build progress in the chat - public sendBuildProgressMessage(params: SendBuildProgressMessageParams) { - const { - tabID, - messageType, - codeGenerationId, - message, - canBeVoted, - messageId, - followUps, - fileList, - codeReference, - } = params - this.dispatcher.sendBuildProgressMessage( - new BuildProgressMessage({ - tabID, - messageType, - codeGenerationId, - message, - canBeVoted, - messageId, - followUps, - fileList, - codeReference, - }) - ) - } -} diff --git a/packages/core/src/amazonqTest/chat/controller/messenger/messengerUtils.ts b/packages/core/src/amazonqTest/chat/controller/messenger/messengerUtils.ts deleted file mode 100644 index 1eecc0aa4cd..00000000000 --- a/packages/core/src/amazonqTest/chat/controller/messenger/messengerUtils.ts +++ /dev/null @@ -1,31 +0,0 @@ -/*! - * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. - * SPDX-License-Identifier: Apache-2.0 - * - */ - -// These enums map to string IDs -export enum ButtonActions { - ACCEPT = 'Accept', - MODIFY = 'Modify', - REJECT = 'Reject', - VIEW_DIFF = 'View-Diff', - STOP_TEST_GEN = 'Stop-Test-Generation', - STOP_BUILD = 'Stop-Build-Process', - PROVIDE_FEEDBACK = 'Provide-Feedback', -} - -// TODO: Refactor the common functionality between Transform, FeatureDev, CWSPRChat, Scan and UTG to a new Folder. - -export default class MessengerUtils { - static stringToEnumValue = ( - enumObject: T, - value: `${T[K]}` - ): T[K] => { - if (Object.values(enumObject).includes(value)) { - return value as unknown as T[K] - } else { - throw new Error('Value provided was not found in Enum') - } - } -} diff --git a/packages/core/src/amazonqTest/chat/session/session.ts b/packages/core/src/amazonqTest/chat/session/session.ts deleted file mode 100644 index 4e3780e6f99..00000000000 --- a/packages/core/src/amazonqTest/chat/session/session.ts +++ /dev/null @@ -1,77 +0,0 @@ -/*! - * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. - * SPDX-License-Identifier: Apache-2.0 - */ - -import { ShortAnswer, Reference } from '../../../codewhisperer/models/model' -import { TargetFileInfo, TestGenerationJob } from '../../../codewhisperer/client/codewhispereruserclient' - -export enum ConversationState { - IDLE, - JOB_SUBMITTED, - WAITING_FOR_INPUT, - WAITING_FOR_BUILD_COMMMAND_INPUT, - WAITING_FOR_REGENERATE_INPUT, - IN_PROGRESS, -} - -export enum BuildStatus { - SUCCESS, - FAILURE, - CANCELLED, -} - -export class Session { - // Used to keep track of whether or not the current session is currently authenticating/needs authenticating - public isAuthenticating: boolean = false - - // A tab may or may not be currently open - public tabID: string | undefined - - // This is unique per each test generation cycle - public testGenerationJobGroupName: string | undefined = undefined - public listOfTestGenerationJobId: string[] = [] - public startTestGenerationRequestId: string | undefined = undefined - public testGenerationJob: TestGenerationJob | undefined - - // Start Test generation - public isSupportedLanguage: boolean = false - public conversationState: ConversationState = ConversationState.IDLE - public shortAnswer: ShortAnswer | undefined - public sourceFilePath: string = '' - public generatedFilePath: string = '' - public projectRootPath: string = '' - public fileLanguage: string | undefined = 'plaintext' - public stopIteration: boolean = false - public targetFileInfo: TargetFileInfo | undefined - public jobSummary: string = '' - - // Telemetry - public testGenerationStartTime: number = 0 - public hasUserPromptSupplied: boolean = false - public isCodeBlockSelected: boolean = false - public srcPayloadSize: number = 0 - public srcZipFileSize: number = 0 - public artifactsUploadDuration: number = 0 - public numberOfTestsGenerated: number = 0 - public linesOfCodeGenerated: number = 0 - public linesOfCodeAccepted: number = 0 - public charsOfCodeGenerated: number = 0 - public charsOfCodeAccepted: number = 0 - public latencyOfTestGeneration: number = 0 - - // TODO: Take values from ShortAnswer or TestGenerationJob - // Build loop - public buildStatus: BuildStatus = BuildStatus.SUCCESS - public updatedBuildCommands: string[] | undefined = undefined - public testCoveragePercentage: number = 90 - public isInProgress: boolean = false - public acceptedJobId = '' - public references: Reference[] = [] - - constructor() {} - - public isTabOpen(): boolean { - return this.tabID !== undefined - } -} diff --git a/packages/core/src/amazonqTest/chat/storages/chatSession.ts b/packages/core/src/amazonqTest/chat/storages/chatSession.ts deleted file mode 100644 index a8a3ccf429d..00000000000 --- a/packages/core/src/amazonqTest/chat/storages/chatSession.ts +++ /dev/null @@ -1,61 +0,0 @@ -/*! - * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. - * SPDX-License-Identifier: Apache-2.0 - * - */ - -import { Session } from '../session/session' -import { getLogger } from '../../../shared/logger/logger' - -export class SessionNotFoundError extends Error {} - -export class ChatSessionManager { - private static _instance: ChatSessionManager - private activeSession: Session | undefined - private isInProgress: boolean = false - - constructor() {} - - public static get Instance() { - return this._instance || (this._instance = new this()) - } - - private createSession(): Session { - this.activeSession = new Session() - return this.activeSession - } - - public getSession(): Session { - if (this.activeSession === undefined) { - return this.createSession() - } - - return this.activeSession - } - - public getIsInProgress(): boolean { - return this.isInProgress - } - - public setIsInProgress(value: boolean): void { - this.isInProgress = value - } - - public setActiveTab(tabID: string): string { - getLogger().debug(`Setting active tab: ${tabID}, activeSession: ${this.activeSession}`) - if (this.activeSession !== undefined) { - this.activeSession.tabID = tabID - return tabID - } - - throw new SessionNotFoundError() - } - - public removeActiveTab(): void { - getLogger().debug(`Removing active tab and deleting activeSession: ${this.activeSession}`) - if (this.activeSession !== undefined) { - this.activeSession.tabID = undefined - this.activeSession = undefined - } - } -} diff --git a/packages/core/src/amazonqTest/chat/views/actions/uiMessageListener.ts b/packages/core/src/amazonqTest/chat/views/actions/uiMessageListener.ts deleted file mode 100644 index e44c002cdf9..00000000000 --- a/packages/core/src/amazonqTest/chat/views/actions/uiMessageListener.ts +++ /dev/null @@ -1,161 +0,0 @@ -/*! - * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. - * SPDX-License-Identifier: Apache-2.0 - */ - -import { MessageListener } from '../../../../amazonq/messages/messageListener' -import { ExtensionMessage } from '../../../../amazonq/webview/ui/commands' -import { TestChatControllerEventEmitters } from '../../controller/controller' - -type UIMessage = ExtensionMessage & { - tabID?: string -} - -export interface UIMessageListenerProps { - readonly chatControllerEventEmitters: TestChatControllerEventEmitters - readonly webViewMessageListener: MessageListener -} - -export class UIMessageListener { - private testControllerEventsEmitters: TestChatControllerEventEmitters | undefined - private webViewMessageListener: MessageListener - - constructor(props: UIMessageListenerProps) { - this.testControllerEventsEmitters = props.chatControllerEventEmitters - this.webViewMessageListener = props.webViewMessageListener - - // Now we are listening to events that get sent from amazonq/webview/actions/actionListener (e.g. the tab) - this.webViewMessageListener.onMessage((msg) => { - this.handleMessage(msg) - }) - } - - private handleMessage(msg: ExtensionMessage) { - switch (msg.command) { - case 'new-tab-was-created': - this.tabOpened(msg) - break - case 'tab-was-removed': - this.tabClosed(msg) - break - case 'auth-follow-up-was-clicked': - this.authClicked(msg) - break - case 'start-test-gen': - this.startTestGen(msg) - break - case 'chat-prompt': - this.processChatPrompt(msg) - break - case 'form-action-click': - this.formActionClicked(msg) - break - case 'follow-up-was-clicked': - this.followUpClicked(msg) - break - case 'open-diff': - this.openDiff(msg) - break - case 'insert_code_at_cursor_position': - this.insertCodeAtCursorPosition(msg) - break - case 'response-body-link-click': - this.processResponseBodyLinkClick(msg) - break - case 'chat-item-voted': - this.chatItemVoted(msg) - break - case 'chat-item-feedback': - this.chatItemFeedback(msg) - break - } - } - - private tabOpened(msg: UIMessage) { - this.testControllerEventsEmitters?.tabOpened.fire({ - tabID: msg.tabID, - }) - } - - private tabClosed(msg: UIMessage) { - this.testControllerEventsEmitters?.tabClosed.fire({ - tabID: msg.tabID, - }) - } - - private authClicked(msg: UIMessage) { - this.testControllerEventsEmitters?.authClicked.fire({ - tabID: msg.tabID, - authType: msg.authType, - }) - } - - private startTestGen(msg: UIMessage) { - this.testControllerEventsEmitters?.startTestGen.fire({ - tabID: msg.tabID, - prompt: msg.prompt, - }) - } - - // Takes user input from chat input box. - private processChatPrompt(msg: UIMessage) { - this.testControllerEventsEmitters?.processHumanChatMessage.fire({ - prompt: msg.chatMessage, - tabID: msg.tabID, - }) - } - - private formActionClicked(msg: UIMessage) { - this.testControllerEventsEmitters?.formActionClicked.fire({ - ...msg, - }) - } - - private followUpClicked(msg: any) { - this.testControllerEventsEmitters?.followUpClicked.fire({ - followUp: msg.followUp, - tabID: msg.tabID, - }) - } - - private openDiff(msg: any) { - this.testControllerEventsEmitters?.openDiff.fire({ - tabID: msg.tabID, - filePath: msg.filePath, - deleted: msg.deleted, - messageId: msg.messageId, - }) - } - - private insertCodeAtCursorPosition(msg: any) { - this.testControllerEventsEmitters?.insertCodeAtCursorPosition.fire({ - command: msg.command, - messageId: msg.messageId, - tabID: msg.tabID, - code: msg.code, - insertionTargetType: msg.insertionTargetType, - codeReference: msg.codeReference, - }) - } - - private processResponseBodyLinkClick(msg: UIMessage) { - this.testControllerEventsEmitters?.processResponseBodyLinkClick.fire({ - command: msg.command, - messageId: msg.messageId, - tabID: msg.tabID, - link: msg.link, - }) - } - - private chatItemVoted(msg: any) { - this.testControllerEventsEmitters?.processChatItemVotedMessage.fire({ - tabID: msg.tabID, - command: msg.command, - vote: msg.vote, - }) - } - - private chatItemFeedback(msg: any) { - this.testControllerEventsEmitters?.processChatItemFeedbackMessage.fire(msg) - } -} diff --git a/packages/core/src/amazonqTest/chat/views/connector/connector.ts b/packages/core/src/amazonqTest/chat/views/connector/connector.ts deleted file mode 100644 index 86c7b446b97..00000000000 --- a/packages/core/src/amazonqTest/chat/views/connector/connector.ts +++ /dev/null @@ -1,256 +0,0 @@ -/*! - * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. - * SPDX-License-Identifier: Apache-2.0 - */ - -import { AuthFollowUpType } from '../../../../amazonq/auth/model' -import { MessagePublisher } from '../../../../amazonq/messages/messagePublisher' -import { ChatItemAction, ChatItemButton, ProgressField, ChatItemContent } from '@aws/mynah-ui/dist/static' -import { ChatItemType } from '../../../../amazonq/commons/model' -import { testChat } from '../../../models/constants' -import { MynahIcons } from '@aws/mynah-ui' -import { SendBuildProgressMessageParams } from '../../controller/messenger/messenger' -import { CodeReference } from '../../../../codewhispererChat/view/connector/connector' - -class UiMessage { - readonly time: number = Date.now() - readonly sender: string = testChat - readonly type: TestMessageType = 'chatMessage' - readonly status: string = 'info' - - public constructor(protected tabID: string) {} -} - -export type TestMessageType = - | 'authenticationUpdateMessage' - | 'authNeededException' - | 'chatMessage' - | 'chatInputEnabledMessage' - | 'updatePlaceholderMessage' - | 'errorMessage' - | 'updatePromptProgress' - | 'chatSummaryMessage' - | 'buildProgressMessage' - -export class AuthenticationUpdateMessage { - readonly time: number = Date.now() - readonly sender: string = testChat - readonly type: TestMessageType = 'authenticationUpdateMessage' - - constructor( - readonly testEnabled: boolean, - readonly authenticatingTabIDs: string[] - ) {} -} - -export class UpdatePromptProgressMessage extends UiMessage { - readonly progressField: ProgressField | null - override type: TestMessageType = 'updatePromptProgress' - constructor(tabID: string, progressField: ProgressField | null) { - super(tabID) - this.progressField = progressField - } -} - -export class AuthNeededException extends UiMessage { - override type: TestMessageType = 'authNeededException' - - constructor( - readonly message: string, - readonly authType: AuthFollowUpType, - tabID: string - ) { - super(tabID) - } -} - -export interface ChatMessageProps { - readonly message: string | undefined - readonly messageId?: string | undefined - readonly messageType: ChatItemType - readonly buttons?: ChatItemButton[] - readonly followUps?: ChatItemAction[] - readonly canBeVoted?: boolean - readonly filePath?: string - readonly informationCard?: ChatItemContent['informationCard'] -} - -export class ChatMessage extends UiMessage { - readonly message: string | undefined - readonly messageId?: string | undefined - readonly messageType: ChatItemType - readonly canBeVoted?: boolean - readonly buttons?: ChatItemButton[] - readonly informationCard: ChatItemContent['informationCard'] - override type: TestMessageType = 'chatMessage' - - constructor(props: ChatMessageProps, tabID: string) { - super(tabID) - this.message = props.message - this.messageType = props.messageType - this.messageId = props.messageId || undefined - this.canBeVoted = props.canBeVoted || undefined - this.informationCard = props.informationCard || undefined - this.buttons = props.buttons || undefined - } -} - -export class ChatSummaryMessage extends UiMessage { - readonly message: string | undefined - readonly messageId?: string | undefined - readonly messageType: ChatItemType - readonly buttons: ChatItemButton[] - readonly canBeVoted?: boolean - readonly filePath?: string - override type: TestMessageType = 'chatSummaryMessage' - - constructor(props: ChatMessageProps, tabID: string) { - super(tabID) - this.message = props.message - this.messageType = props.messageType - this.buttons = props.buttons || [] - this.messageId = props.messageId || undefined - this.canBeVoted = props.canBeVoted - this.filePath = props.filePath - } -} - -export class ChatInputEnabledMessage extends UiMessage { - override type: TestMessageType = 'chatInputEnabledMessage' - - constructor( - tabID: string, - readonly enabled: boolean - ) { - super(tabID) - } -} - -export class UpdatePlaceholderMessage extends UiMessage { - readonly newPlaceholder: string - override type: TestMessageType = 'updatePlaceholderMessage' - - constructor(tabID: string, newPlaceholder: string) { - super(tabID) - this.newPlaceholder = newPlaceholder - } -} - -export class CapabilityCardMessage extends ChatMessage { - constructor(tabID: string) { - super( - { - message: '', - messageType: 'answer', - informationCard: { - title: '/test - Unit test generation', - description: 'Generate unit tests for selected code', - content: { - body: `I can generate unit tests for the active file or open project in your IDE. - -I can do things like: -- Add unit tests for highlighted functions -- Generate tests for null and empty inputs - -To learn more, visit the [Amazon Q Developer User Guide](https://docs.aws.amazon.com/amazonq/latest/qdeveloper-ug/test-generation.html).`, - }, - icon: 'check-list' as MynahIcons, - }, - }, - tabID - ) - } -} - -export class ErrorMessage extends UiMessage { - readonly title!: string - readonly message!: string - override type: TestMessageType = 'errorMessage' - - constructor(title: string, message: string, tabID: string) { - super(tabID) - this.title = title - this.message = message - } -} - -export class BuildProgressMessage extends UiMessage { - readonly message: string | undefined - readonly codeGenerationId!: string - readonly messageId?: string - readonly followUps?: { - text?: string - options?: ChatItemAction[] - } - readonly fileList?: { - fileTreeTitle?: string - rootFolderTitle?: string - filePaths?: string[] - } - readonly codeReference?: CodeReference[] - readonly canBeVoted: boolean - readonly messageType: ChatItemType - override type: TestMessageType = 'buildProgressMessage' - - constructor({ - tabID, - messageType, - codeGenerationId, - message, - canBeVoted, - messageId, - followUps, - fileList, - codeReference, - }: SendBuildProgressMessageParams) { - super(tabID) - this.messageType = messageType - this.codeGenerationId = codeGenerationId - this.message = message - this.canBeVoted = canBeVoted - this.messageId = messageId - this.followUps = followUps - this.fileList = fileList - this.codeReference = codeReference - } -} - -export class AppToWebViewMessageDispatcher { - constructor(private readonly appsToWebViewMessagePublisher: MessagePublisher) {} - - public sendChatMessage(message: ChatMessage) { - this.appsToWebViewMessagePublisher.publish(message) - } - - public sendChatSummaryMessage(message: ChatSummaryMessage) { - this.appsToWebViewMessagePublisher.publish(message) - } - - public sendUpdatePlaceholder(message: UpdatePlaceholderMessage) { - this.appsToWebViewMessagePublisher.publish(message) - } - - public sendAuthenticationUpdate(message: AuthenticationUpdateMessage) { - this.appsToWebViewMessagePublisher.publish(message) - } - - public sendAuthNeededExceptionMessage(message: AuthNeededException) { - this.appsToWebViewMessagePublisher.publish(message) - } - - public sendChatInputEnabled(message: ChatInputEnabledMessage) { - this.appsToWebViewMessagePublisher.publish(message) - } - - public sendErrorMessage(message: ErrorMessage) { - this.appsToWebViewMessagePublisher.publish(message) - } - - public sendBuildProgressMessage(message: BuildProgressMessage) { - this.appsToWebViewMessagePublisher.publish(message) - } - - public sendUpdatePromptProgress(message: UpdatePromptProgressMessage) { - this.appsToWebViewMessagePublisher.publish(message) - } -} diff --git a/packages/core/src/amazonqTest/error.ts b/packages/core/src/amazonqTest/error.ts deleted file mode 100644 index a6694b35863..00000000000 --- a/packages/core/src/amazonqTest/error.ts +++ /dev/null @@ -1,67 +0,0 @@ -/*! - * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. - * SPDX-License-Identifier: Apache-2.0 - */ -import { ToolkitError } from '../shared/errors' - -export const technicalErrorCustomerFacingMessage = - 'I am experiencing technical difficulties at the moment. Please try again in a few minutes.' -const defaultTestGenErrorMessage = 'Amazon Q encountered an error while generating tests. Try again later.' -export class TestGenError extends ToolkitError { - constructor( - error: string, - code: string, - public uiMessage: string - ) { - super(error, { code }) - } -} -export class ProjectZipError extends TestGenError { - constructor(error: string) { - super(error, 'ProjectZipError', defaultTestGenErrorMessage) - } -} -export class InvalidSourceZipError extends TestGenError { - constructor() { - super('Failed to create valid source zip', 'InvalidSourceZipError', defaultTestGenErrorMessage) - } -} -export class CreateUploadUrlError extends TestGenError { - constructor(errorMessage: string) { - super(errorMessage, 'CreateUploadUrlError', technicalErrorCustomerFacingMessage) - } -} -export class UploadTestArtifactToS3Error extends TestGenError { - constructor(error: string) { - super(error, 'UploadTestArtifactToS3Error', technicalErrorCustomerFacingMessage) - } -} -export class CreateTestJobError extends TestGenError { - constructor(error: string) { - super(error, 'CreateTestJobError', technicalErrorCustomerFacingMessage) - } -} -export class TestGenTimedOutError extends TestGenError { - constructor() { - super( - 'Test generation failed. Amazon Q timed out.', - 'TestGenTimedOutError', - technicalErrorCustomerFacingMessage - ) - } -} -export class TestGenStoppedError extends TestGenError { - constructor() { - super('Unit test generation cancelled.', 'TestGenCancelled', 'Unit test generation cancelled.') - } -} -export class TestGenFailedError extends TestGenError { - constructor(error?: string) { - super(error ?? 'Test generation failed', 'TestGenFailedError', error ?? technicalErrorCustomerFacingMessage) - } -} -export class ExportResultsArchiveError extends TestGenError { - constructor(error?: string) { - super(error ?? 'Test generation failed', 'ExportResultsArchiveError', technicalErrorCustomerFacingMessage) - } -} diff --git a/packages/core/src/amazonqTest/index.ts b/packages/core/src/amazonqTest/index.ts deleted file mode 100644 index 06f5ebb63f9..00000000000 --- a/packages/core/src/amazonqTest/index.ts +++ /dev/null @@ -1,6 +0,0 @@ -/*! - * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. - * SPDX-License-Identifier: Apache-2.0 - */ - -export { default as MessengerUtils } from './chat/controller/messenger/messengerUtils' diff --git a/packages/core/src/amazonqTest/models/constants.ts b/packages/core/src/amazonqTest/models/constants.ts deleted file mode 100644 index 547cbdb3663..00000000000 --- a/packages/core/src/amazonqTest/models/constants.ts +++ /dev/null @@ -1,147 +0,0 @@ -/*! - * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. - * SPDX-License-Identifier: Apache-2.0 - */ -import { ProgressField, MynahIcons, ChatItemButton } from '@aws/mynah-ui' -import { ButtonActions } from '../chat/controller/messenger/messengerUtils' -import { TestGenerationBuildStep } from '../../codewhisperer/models/constants' -import { ChatSessionManager } from '../chat/storages/chatSession' -import { BuildStatus } from '../chat/session/session' - -// For uniquely identifiying which chat messages should be routed to Test -export const testChat = 'testChat' - -export const maxUserPromptLength = 4096 // user prompt character limit from MPS and API model. - -export const cancelTestGenButton: ChatItemButton = { - id: ButtonActions.STOP_TEST_GEN, - text: 'Cancel', - icon: 'cancel' as MynahIcons, -} - -export const testGenProgressField: ProgressField = { - status: 'default', - value: -1, - text: 'Generating unit tests...', - actions: [cancelTestGenButton], -} - -export const testGenCompletedField: ProgressField = { - status: 'success', - value: 100, - text: 'Complete...', - actions: [], -} - -export const cancellingProgressField: ProgressField = { - status: 'warning', - text: 'Cancelling...', - value: -1, - actions: [], -} - -export const cancelBuildProgressButton: ChatItemButton = { - id: ButtonActions.STOP_BUILD, - text: 'Cancel', - icon: 'cancel' as MynahIcons, -} - -export const buildProgressField: ProgressField = { - status: 'default', - value: -1, - text: 'Executing...', - actions: [cancelBuildProgressButton], -} - -export const errorProgressField: ProgressField = { - status: 'error', - text: 'Error...Input needed', - value: -1, - actions: [cancelBuildProgressButton], -} - -export const testGenSummaryMessage = ( - fileName: string, - planSummary?: string -) => `Sure. This may take a few minutes. I'll share updates here as I work on this. - -**Generating unit tests for the following methods in \`${fileName}\`** -${planSummary ? `\n\n${planSummary}` : ''} -` - -const checkIcons = { - wait: '☐', - current: '☐', - done: '', - error: '❌', -} - -interface StepStatus { - step: TestGenerationBuildStep - status: 'wait' | 'current' | 'done' | 'error' -} - -const stepStatuses: StepStatus[] = [] - -export const testGenBuildProgressMessage = (currentStep: TestGenerationBuildStep, status?: string) => { - const session = ChatSessionManager.Instance.getSession() - const statusText = BuildStatus[session.buildStatus].toLowerCase() - const icon = session.buildStatus === BuildStatus.SUCCESS ? checkIcons['done'] : checkIcons['error'] - let message = `Sure. This may take a few minutes and I'll share updates on my progress here. -**Progress summary**\n\n` - - if (currentStep === TestGenerationBuildStep.START_STEP) { - return message.trim() - } - - updateStepStatuses(currentStep, status) - - if (currentStep >= TestGenerationBuildStep.RUN_BUILD) { - message += `${getIconForStep(TestGenerationBuildStep.RUN_BUILD)} Started build execution\n` - } - - if (currentStep >= TestGenerationBuildStep.RUN_EXECUTION_TESTS) { - message += `${getIconForStep(TestGenerationBuildStep.RUN_EXECUTION_TESTS)} Executing tests\n` - } - - if (currentStep >= TestGenerationBuildStep.FIXING_TEST_CASES && session.buildStatus === BuildStatus.FAILURE) { - message += `${getIconForStep(TestGenerationBuildStep.FIXING_TEST_CASES)} Fixing errors in tests\n\n` - } - - if (currentStep > TestGenerationBuildStep.PROCESS_TEST_RESULTS) { - message += `**Test case summary** -${session.shortAnswer?.testCoverage ? `- Unit test coverage ${session.shortAnswer?.testCoverage}%` : ``} -${icon} Build ${statusText} -${icon} Assertion ${statusText}` - // TODO: Update Assertion % - } - - return message.trim() -} -// TODO: Work on UX to show the build error in the progress message -const updateStepStatuses = (currentStep: TestGenerationBuildStep, status?: string) => { - for (let step = TestGenerationBuildStep.INSTALL_DEPENDENCIES; step <= currentStep; step++) { - const stepStatus: StepStatus = { - step: step, - status: 'wait', - } - - if (step === currentStep) { - stepStatus.status = status === 'failed' ? 'error' : 'current' - } else if (step < currentStep) { - stepStatus.status = 'done' - } - - const existingIndex = stepStatuses.findIndex((s) => s.step === step) - if (existingIndex !== -1) { - stepStatuses[existingIndex] = stepStatus - } else { - stepStatuses.push(stepStatus) - } - } -} - -const getIconForStep = (step: TestGenerationBuildStep) => { - const stepStatus = stepStatuses.find((s) => s.step === step) - return stepStatus ? checkIcons[stepStatus.status] : checkIcons.wait -} diff --git a/packages/core/src/codewhisperer/commands/startTestGeneration.ts b/packages/core/src/codewhisperer/commands/startTestGeneration.ts deleted file mode 100644 index e99fd499e5a..00000000000 --- a/packages/core/src/codewhisperer/commands/startTestGeneration.ts +++ /dev/null @@ -1,259 +0,0 @@ -/*! - * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. - * SPDX-License-Identifier: Apache-2.0 - */ - -import { getLogger } from '../../shared/logger/logger' -import { ZipUtil } from '../util/zipUtil' -import { ArtifactMap } from '../client/codewhisperer' -import { testGenerationLogsDir } from '../../shared/filesystemUtilities' -import { - createTestJob, - exportResultsArchive, - getPresignedUrlAndUploadTestGen, - pollTestJobStatus, - throwIfCancelled, -} from '../service/testGenHandler' -import path from 'path' -import { testGenState } from '../models/model' -import { ChatSessionManager } from '../../amazonqTest/chat/storages/chatSession' -import { ChildProcess, spawn } from 'child_process' // eslint-disable-line no-restricted-imports -import { BuildStatus } from '../../amazonqTest/chat/session/session' -import { fs } from '../../shared/fs/fs' -import { Range } from '../client/codewhispereruserclient' -import { AuthUtil } from '../indexNode' - -// eslint-disable-next-line unicorn/no-null -let spawnResult: ChildProcess | null = null -let isCancelled = false -export async function startTestGenerationProcess( - filePath: string, - userInputPrompt: string, - tabID: string, - initialExecution: boolean, - selectionRange?: Range -) { - const logger = getLogger() - const session = ChatSessionManager.Instance.getSession() - const profile = AuthUtil.instance.regionProfileManager.activeRegionProfile - // TODO: Step 0: Initial Test Gen telemetry - try { - logger.verbose(`Starting Test Generation `) - logger.verbose(`Tab ID: ${tabID} !== ${session.tabID}`) - if (tabID !== session.tabID) { - logger.verbose(`Tab ID mismatch: ${tabID} !== ${session.tabID}`) - return - } - /** - * Step 1: Zip the project - */ - - const zipUtil = new ZipUtil() - if (initialExecution) { - const projectPath = zipUtil.getProjectPath(filePath) ?? '' - const relativeTargetPath = path.relative(projectPath, filePath) - session.listOfTestGenerationJobId = [] - session.shortAnswer = undefined - session.sourceFilePath = relativeTargetPath - session.projectRootPath = projectPath - session.listOfTestGenerationJobId = [] - } - const zipMetadata = await zipUtil.generateZipTestGen(session.projectRootPath, initialExecution) - session.srcPayloadSize = zipMetadata.buildPayloadSizeInBytes - session.srcZipFileSize = zipMetadata.zipFileSizeInBytes - - /** - * Step 2: Get presigned Url, upload and clean up - */ - throwIfCancelled() - if (!shouldContinueRunning(tabID)) { - return - } - let artifactMap: ArtifactMap = {} - const uploadStartTime = performance.now() - try { - artifactMap = await getPresignedUrlAndUploadTestGen(zipMetadata, profile) - } finally { - const outputLogPath = path.join(testGenerationLogsDir, 'output.log') - if (await fs.existsFile(outputLogPath)) { - await fs.delete(outputLogPath) - } - await zipUtil.removeTmpFiles(zipMetadata) - session.artifactsUploadDuration = performance.now() - uploadStartTime - } - - /** - * Step 3: Create scan job with startTestGeneration - */ - throwIfCancelled() - if (!shouldContinueRunning(tabID)) { - return - } - const sessionFilePath = session.sourceFilePath - const testJob = await createTestJob( - artifactMap, - [ - { - relativeTargetPath: sessionFilePath, - targetLineRangeList: selectionRange ? [selectionRange] : [], - }, - ], - userInputPrompt, - undefined, - profile - ) - if (!testJob.testGenerationJob) { - throw Error('Test job not found') - } - session.testGenerationJob = testJob.testGenerationJob - - /** - * Step 4: Polling mechanism on test job status with getTestGenStatus - */ - throwIfCancelled() - if (!shouldContinueRunning(tabID)) { - return - } - await pollTestJobStatus( - testJob.testGenerationJob.testGenerationJobId, - testJob.testGenerationJob.testGenerationJobGroupName, - filePath, - initialExecution, - profile - ) - // TODO: Send status to test summary - throwIfCancelled() - if (!shouldContinueRunning(tabID)) { - return - } - /** - * Step 5: Process and show the view diff by getting the results from exportResultsArchive - */ - // https://github.com/aws/aws-toolkit-vscode/blob/0164d4145e58ae036ddf3815455ea12a159d491d/packages/core/src/codewhisperer/service/transformByQ/transformationResultsViewProvider.ts#L314-L405 - await exportResultsArchive( - artifactMap.SourceCode, - testJob.testGenerationJob.testGenerationJobGroupName, - testJob.testGenerationJob.testGenerationJobId, - path.basename(session.projectRootPath), - session.projectRootPath, - initialExecution - ) - } catch (error) { - logger.error(`startTestGenerationProcess failed: %O`, error) - // TODO: Send error message to Chat - testGenState.getChatControllers()?.errorThrown.fire({ - tabID: session.tabID, - error: error, - }) - } finally { - testGenState.setToNotStarted() - } -} - -export function shouldContinueRunning(tabID: string): boolean { - if (tabID !== ChatSessionManager.Instance.getSession().tabID) { - getLogger().verbose(`Tab ID mismatch: ${tabID} !== ${ChatSessionManager.Instance.getSession().tabID}`) - return false - } - return true -} - -/** - * Run client side build with given build commands - */ -export async function runBuildCommand(listofBuildCommand: string[]): Promise { - for (const buildCommand of listofBuildCommand) { - try { - await fs.mkdir(testGenerationLogsDir) - const tmpFile = path.join(testGenerationLogsDir, 'output.log') - const result = await runLocalBuild(buildCommand, tmpFile) - if (result.isCancelled) { - return BuildStatus.CANCELLED - } - if (result.code !== 0) { - return BuildStatus.FAILURE - } - } catch (error) { - getLogger().error(`Build process error`) - return BuildStatus.FAILURE - } - } - return BuildStatus.SUCCESS -} - -function runLocalBuild( - buildCommand: string, - tmpFile: string -): Promise<{ code: number | null; isCancelled: boolean; message: string }> { - return new Promise(async (resolve, reject) => { - const environment = process.env - const repositoryPath = ChatSessionManager.Instance.getSession().projectRootPath - const [command, ...args] = buildCommand.split(' ') - getLogger().info(`Build process started for command: ${buildCommand}, for path: ${repositoryPath}`) - - let buildLogs = '' - - spawnResult = spawn(command, args, { - cwd: repositoryPath, - shell: true, - env: environment, - }) - - if (spawnResult.stdout) { - spawnResult.stdout.on('data', async (data) => { - const output = data.toString().trim() - getLogger().info(`BUILD OUTPUT: ${output}`) - buildLogs += output - }) - } - - if (spawnResult.stderr) { - spawnResult.stderr.on('data', async (data) => { - const output = data.toString().trim() - getLogger().warn(`BUILD ERROR: ${output}`) - buildLogs += output - }) - } - - spawnResult.on('close', async (code) => { - let message = '' - if (isCancelled) { - message = 'Build cancelled' - getLogger().info('BUILD CANCELLED') - } else if (code === 0) { - message = 'Build successful' - getLogger().info('BUILD SUCCESSFUL') - } else { - message = `Build failed with exit code ${code}` - getLogger().info(`BUILD FAILED with exit code ${code}`) - } - - try { - await fs.writeFile(tmpFile, buildLogs) - getLogger().info(`Build logs written to ${tmpFile}`) - } catch (error) { - getLogger().error(`Failed to write build logs to ${tmpFile}: ${error}`) - } - - resolve({ code, isCancelled, message }) - - // eslint-disable-next-line unicorn/no-null - spawnResult = null - isCancelled = false - }) - - spawnResult.on('error', (error) => { - reject(new Error(`Failed to start build process: ${error.message}`)) - }) - }) -} - -export function cancelBuild() { - if (spawnResult) { - isCancelled = true - spawnResult.kill() - getLogger().info('Build cancellation requested') - } else { - getLogger().info('No active build to cancel') - } -} diff --git a/packages/core/src/codewhisperer/models/model.ts b/packages/core/src/codewhisperer/models/model.ts index 2869098325e..70f520440fa 100644 --- a/packages/core/src/codewhisperer/models/model.ts +++ b/packages/core/src/codewhisperer/models/model.ts @@ -18,7 +18,6 @@ import globals from '../../shared/extensionGlobals' import { ChatControllerEventEmitters } from '../../amazonqGumby/chat/controller/controller' import { TransformationSteps } from '../client/codewhispereruserclient' import { Messenger } from '../../amazonqGumby/chat/controller/messenger/messenger' -import { TestChatControllerEventEmitters } from '../../amazonqTest/chat/controller/controller' import { ScanChatControllerEventEmitters } from '../../amazonqScan/controller' import { localize } from '../../shared/utilities/vsCodeUtils' @@ -372,55 +371,6 @@ export interface CodeLine { number: number } -/** - * Unit Test Generation - */ -enum TestGenStatus { - NotStarted, - Running, - Cancelling, -} -// TODO: Refactor model of /scan and /test -export class TestGenState { - // Define a constructor for this class - private testGenState: TestGenStatus = TestGenStatus.NotStarted - - protected chatControllers: TestChatControllerEventEmitters | undefined = undefined - - public isNotStarted() { - return this.testGenState === TestGenStatus.NotStarted - } - - public isRunning() { - return this.testGenState === TestGenStatus.Running - } - - public isCancelling() { - return this.testGenState === TestGenStatus.Cancelling - } - - public setToNotStarted() { - this.testGenState = TestGenStatus.NotStarted - } - - public setToCancelling() { - this.testGenState = TestGenStatus.Cancelling - } - - public setToRunning() { - this.testGenState = TestGenStatus.Running - } - - public setChatControllers(controllers: TestChatControllerEventEmitters) { - this.chatControllers = controllers - } - public getChatControllers() { - return this.chatControllers - } -} - -export const testGenState: TestGenState = new TestGenState() - enum CodeFixStatus { NotStarted, Running, diff --git a/packages/core/src/codewhisperer/service/securityScanHandler.ts b/packages/core/src/codewhisperer/service/securityScanHandler.ts index b83fdbebb1a..14485642aed 100644 --- a/packages/core/src/codewhisperer/service/securityScanHandler.ts +++ b/packages/core/src/codewhisperer/service/securityScanHandler.ts @@ -35,13 +35,11 @@ import { SecurityScanTimedOutError, UploadArtifactToS3Error, } from '../models/errors' -import { getTelemetryReasonDesc, isAwsError } from '../../shared/errors' +import { getTelemetryReasonDesc } from '../../shared/errors' import { CodeWhispererSettings } from '../util/codewhispererSettings' import { detectCommentAboveLine } from '../../shared/utilities/commentUtils' import { runtimeLanguageContext } from '../util/runtimeLanguageContext' import { FeatureUseCase } from '../models/constants' -import { UploadTestArtifactToS3Error } from '../../amazonqTest/error' -import { ChatSessionManager } from '../../amazonqTest/chat/storages/chatSession' import { AmazonqCreateUpload, Span, telemetry } from '../../shared/telemetry/telemetry' import { AuthUtil } from '../util/authUtil' @@ -432,10 +430,7 @@ export async function uploadArtifactToS3( } else { errorMessage = errorDesc ?? defaultMessage } - if (isAwsError(error) && featureUseCase === FeatureUseCase.TEST_GENERATION) { - ChatSessionManager.Instance.getSession().startTestGenerationRequestId = error.requestId - } - throw isCodeScan ? new UploadArtifactToS3Error(errorMessage) : new UploadTestArtifactToS3Error(errorMessage) + throw new UploadArtifactToS3Error(errorMessage) } finally { getLogger().debug(`Upload to S3 response details: x-amz-request-id: ${requestId}, x-amz-id-2: ${id2}`) if (span) { diff --git a/packages/core/src/codewhisperer/service/testGenHandler.ts b/packages/core/src/codewhisperer/service/testGenHandler.ts deleted file mode 100644 index 5ca8ca665da..00000000000 --- a/packages/core/src/codewhisperer/service/testGenHandler.ts +++ /dev/null @@ -1,326 +0,0 @@ -/*! - * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. - * SPDX-License-Identifier: Apache-2.0 - */ - -import { ZipMetadata } from '../util/zipUtil' -import { getLogger } from '../../shared/logger/logger' -import * as CodeWhispererConstants from '../models/constants' -import * as codewhispererClient from '../client/codewhisperer' -import * as codeWhisperer from '../client/codewhisperer' -import CodeWhispererUserClient, { - ArtifactMap, - CreateUploadUrlRequest, - TargetCode, -} from '../client/codewhispereruserclient' -import { - CreateTestJobError, - CreateUploadUrlError, - ExportResultsArchiveError, - InvalidSourceZipError, - TestGenFailedError, - TestGenStoppedError, - TestGenTimedOutError, -} from '../../amazonqTest/error' -import { getMd5, uploadArtifactToS3 } from './securityScanHandler' -import { testGenState, Reference, RegionProfile } from '../models/model' -import { ChatSessionManager } from '../../amazonqTest/chat/storages/chatSession' -import { createCodeWhispererChatStreamingClient } from '../../shared/clients/codewhispererChatClient' -import { downloadExportResultArchive } from '../../shared/utilities/download' -import AdmZip from 'adm-zip' -import path from 'path' -import { ExportIntent } from '@amzn/codewhisperer-streaming' -import { glob } from 'glob' -import { UserWrittenCodeTracker } from '../tracker/userWrittenCodeTracker' -import { randomUUID } from '../../shared/crypto' -import { sleep } from '../../shared/utilities/timeoutUtils' -import { tempDirPath } from '../../shared/filesystemUtilities' -import fs from '../../shared/fs/fs' -import { AuthUtil } from '../util/authUtil' - -// TODO: Get TestFileName and Framework and to error message -export function throwIfCancelled() { - // TODO: fileName will be '' if user gives propt without opening - if (testGenState.isCancelling()) { - throw new TestGenStoppedError() - } -} - -export async function getPresignedUrlAndUploadTestGen(zipMetadata: ZipMetadata, profile: RegionProfile | undefined) { - const logger = getLogger() - if (zipMetadata.zipFilePath === '') { - getLogger().error('Failed to create valid source zip') - throw new InvalidSourceZipError() - } - const srcReq: CreateUploadUrlRequest = { - contentMd5: getMd5(zipMetadata.zipFilePath), - artifactType: 'SourceCode', - uploadIntent: CodeWhispererConstants.testGenUploadIntent, - profileArn: profile?.arn, - } - logger.verbose(`Prepare for uploading src context...`) - const srcResp = await codeWhisperer.codeWhispererClient.createUploadUrl(srcReq).catch((err) => { - getLogger().error(`Failed getting presigned url for uploading src context. Request id: ${err.requestId}`) - throw new CreateUploadUrlError(err.message) - }) - logger.verbose(`CreateUploadUrlRequest requestId: ${srcResp.$response.requestId}`) - logger.verbose(`Complete Getting presigned Url for uploading src context.`) - logger.verbose(`Uploading src context...`) - await uploadArtifactToS3(zipMetadata.zipFilePath, srcResp, CodeWhispererConstants.FeatureUseCase.TEST_GENERATION) - logger.verbose(`Complete uploading src context.`) - const artifactMap: ArtifactMap = { - SourceCode: srcResp.uploadId, - } - return artifactMap -} - -export async function createTestJob( - artifactMap: codewhispererClient.ArtifactMap, - relativeTargetPath: TargetCode[], - userInputPrompt: string, - clientToken?: string, - profile?: RegionProfile -) { - const logger = getLogger() - logger.verbose(`Creating test job and starting startTestGeneration...`) - - // JS will minify this input object - fix that - const targetCodeList = relativeTargetPath.map((targetCode) => ({ - relativeTargetPath: targetCode.relativeTargetPath, - targetLineRangeList: targetCode.targetLineRangeList?.map((range) => ({ - start: { line: range.start.line, character: range.start.character }, - end: { line: range.end.line, character: range.end.character }, - })), - })) - logger.debug('updated target code list: %O', targetCodeList) - const req: CodeWhispererUserClient.StartTestGenerationRequest = { - uploadId: artifactMap.SourceCode, - targetCodeList, - userInput: userInputPrompt, - testGenerationJobGroupName: ChatSessionManager.Instance.getSession().testGenerationJobGroupName ?? randomUUID(), // TODO: remove fallback - clientToken, - profileArn: profile?.arn, - } - logger.debug('Unit test generation request body: %O', req) - logger.debug('target code list: %O', req.targetCodeList[0]) - const firstTargetCodeList = req.targetCodeList?.[0] - const firstTargetLineRangeList = firstTargetCodeList?.targetLineRangeList?.[0] - logger.debug('target line range list: %O', firstTargetLineRangeList) - logger.debug('target line range start: %O', firstTargetLineRangeList?.start) - logger.debug('target line range end: %O', firstTargetLineRangeList?.end) - - const resp = await codewhispererClient.codeWhispererClient.startTestGeneration(req).catch((err) => { - ChatSessionManager.Instance.getSession().startTestGenerationRequestId = err.requestId - logger.error(`Failed creating test job. Request id: ${err.requestId}`) - throw new CreateTestJobError(err.message) - }) - logger.info('Unit test generation request id: %s', resp.$response.requestId) - logger.debug('Unit test generation data: %O', resp.$response.data) - ChatSessionManager.Instance.getSession().startTestGenerationRequestId = resp.$response.requestId - if (resp.$response.error) { - logger.error('Unit test generation error: %O', resp.$response.error) - } - if (resp.testGenerationJob) { - ChatSessionManager.Instance.getSession().listOfTestGenerationJobId.push( - resp.testGenerationJob?.testGenerationJobId - ) - ChatSessionManager.Instance.getSession().testGenerationJobGroupName = - resp.testGenerationJob?.testGenerationJobGroupName - } - return resp -} - -export async function pollTestJobStatus( - jobId: string, - jobGroupName: string, - filePath: string, - initialExecution: boolean, - profile?: RegionProfile -) { - const session = ChatSessionManager.Instance.getSession() - const pollingStartTime = performance.now() - // We don't expect to get results immediately, so sleep for some time initially to not make unnecessary calls - await sleep(CodeWhispererConstants.testGenPollingDelaySeconds) - - const logger = getLogger() - logger.verbose(`Polling testgen job status...`) - let status = CodeWhispererConstants.TestGenerationJobStatus.IN_PROGRESS - while (true) { - throwIfCancelled() - const req: CodeWhispererUserClient.GetTestGenerationRequest = { - testGenerationJobId: jobId, - testGenerationJobGroupName: jobGroupName, - profileArn: profile?.arn, - } - const resp = await codewhispererClient.codeWhispererClient.getTestGeneration(req) - logger.verbose('pollTestJobStatus request id: %s', resp.$response.requestId) - logger.debug('pollTestJobStatus testGenerationJob %O', resp.testGenerationJob) - ChatSessionManager.Instance.getSession().testGenerationJob = resp.testGenerationJob - const progressRate = resp.testGenerationJob?.progressRate ?? 0 - testGenState.getChatControllers()?.sendUpdatePromptProgress.fire({ - tabID: ChatSessionManager.Instance.getSession().tabID, - status: 'InProgress', - progressRate, - }) - const jobSummary = resp.testGenerationJob?.jobSummary ?? '' - const jobSummaryNoBackticks = jobSummary.replace(/^`+|`+$/g, '') - ChatSessionManager.Instance.getSession().jobSummary = jobSummaryNoBackticks - const packageInfoList = resp.testGenerationJob?.packageInfoList ?? [] - const packageInfo = packageInfoList[0] - const targetFileInfo = packageInfo?.targetFileInfoList?.[0] - - if (packageInfo) { - // TODO: will need some fields from packageInfo such as buildCommand, packagePlan, packageSummary - } - if (targetFileInfo) { - if (targetFileInfo.numberOfTestMethods) { - session.numberOfTestsGenerated = Number(targetFileInfo.numberOfTestMethods) - } - if (targetFileInfo.codeReferences) { - session.references = targetFileInfo.codeReferences as Reference[] - } - if (initialExecution) { - session.generatedFilePath = targetFileInfo.testFilePath ?? '' - const currentPlanSummary = session.targetFileInfo?.filePlan - const newPlanSummary = targetFileInfo?.filePlan - - if (currentPlanSummary !== newPlanSummary && newPlanSummary) { - const chatControllers = testGenState.getChatControllers() - if (chatControllers) { - const currentSession = ChatSessionManager.Instance.getSession() - chatControllers.updateTargetFileInfo.fire({ - tabID: currentSession.tabID, - targetFileInfo, - testGenerationJobGroupName: resp.testGenerationJob?.testGenerationJobGroupName, - testGenerationJobId: resp.testGenerationJob?.testGenerationJobId, - filePath, - }) - } - } - } - } - ChatSessionManager.Instance.getSession().targetFileInfo = targetFileInfo - status = resp.testGenerationJob?.status as CodeWhispererConstants.TestGenerationJobStatus - if (status === CodeWhispererConstants.TestGenerationJobStatus.FAILED) { - session.numberOfTestsGenerated = 0 - logger.verbose(`Test generation failed.`) - if (resp.testGenerationJob?.jobStatusReason) { - session.stopIteration = true - throw new TestGenFailedError(resp.testGenerationJob?.jobStatusReason) - } else { - throw new TestGenFailedError() - } - } else if (status === CodeWhispererConstants.TestGenerationJobStatus.COMPLETED) { - logger.verbose(`testgen job status: ${status}`) - logger.verbose(`Complete polling test job status.`) - break - } - throwIfCancelled() - await sleep(CodeWhispererConstants.testGenJobPollingIntervalMilliseconds) - const elapsedTime = performance.now() - pollingStartTime - if (elapsedTime > CodeWhispererConstants.testGenJobTimeoutMilliseconds) { - logger.verbose(`testgen job status: ${status}`) - logger.verbose(`testgen job failed. Amazon Q timed out.`) - throw new TestGenTimedOutError() - } - } - return status -} - -/** - * Download the zip from exportResultsArchieve API and store in temp zip - */ -export async function exportResultsArchive( - uploadId: string, - groupName: string, - jobId: string, - projectName: string, - projectPath: string, - initialExecution: boolean -) { - // TODO: Make a common Temp folder - const pathToArchiveDir = path.join(tempDirPath, 'q-testgen') - - const archivePathExists = await fs.existsDir(pathToArchiveDir) - if (archivePathExists) { - await fs.delete(pathToArchiveDir, { recursive: true }) - } - await fs.mkdir(pathToArchiveDir) - - let downloadErrorMessage = undefined - - const session = ChatSessionManager.Instance.getSession() - try { - const pathToArchive = path.join(pathToArchiveDir, 'QTestGeneration.zip') - // Download and deserialize the zip - await downloadResultArchive(uploadId, groupName, jobId, pathToArchive) - const zip = new AdmZip(pathToArchive) - zip.extractAllTo(pathToArchiveDir, true) - - const testFilePathFromResponse = session?.targetFileInfo?.testFilePath - const testFilePath = testFilePathFromResponse - ? testFilePathFromResponse.split('/').slice(1).join('/') // remove the project name - : await getTestFilePathFromZip(pathToArchiveDir) - if (initialExecution) { - testGenState.getChatControllers()?.showCodeGenerationResults.fire({ - tabID: session.tabID, - filePath: testFilePath, - projectName, - }) - - // If User accepts the diff - testGenState.getChatControllers()?.sendUpdatePromptProgress.fire({ - tabID: ChatSessionManager.Instance.getSession().tabID, - status: 'Completed', - }) - } - } catch (e) { - session.numberOfTestsGenerated = 0 - downloadErrorMessage = (e as Error).message - getLogger().error(`Unit Test Generation: ExportResultArchive error = ${downloadErrorMessage}`) - throw new ExportResultsArchiveError(downloadErrorMessage) - } -} - -async function getTestFilePathFromZip(pathToArchiveDir: string) { - const resultArtifactsDir = path.join(pathToArchiveDir, 'resultArtifacts') - const paths = await glob([resultArtifactsDir + '/**/*', '!**/.DS_Store'], { nodir: true }) - const absolutePath = paths[0] - const result = path.relative(resultArtifactsDir, absolutePath) - return result -} - -export async function downloadResultArchive( - uploadId: string, - testGenerationJobGroupName: string, - testGenerationJobId: string, - pathToArchive: string -) { - let downloadErrorMessage = undefined - const cwStreamingClient = await createCodeWhispererChatStreamingClient() - - try { - await downloadExportResultArchive( - cwStreamingClient, - { - exportId: uploadId, - exportIntent: ExportIntent.UNIT_TESTS, - exportContext: { - unitTestGenerationExportContext: { - testGenerationJobGroupName, - testGenerationJobId, - }, - }, - }, - pathToArchive, - AuthUtil.instance.regionProfileManager.activeRegionProfile - ) - } catch (e: any) { - downloadErrorMessage = (e as Error).message - getLogger().error(`Unit Test Generation: ExportResultArchive error = ${downloadErrorMessage}`) - throw new ExportResultsArchiveError(downloadErrorMessage) - } finally { - cwStreamingClient.destroy() - UserWrittenCodeTracker.instance.onQFeatureInvoked() - } -} diff --git a/packages/core/src/codewhisperer/util/telemetryHelper.ts b/packages/core/src/codewhisperer/util/telemetryHelper.ts index 060a5ecb282..89c04afe572 100644 --- a/packages/core/src/codewhisperer/util/telemetryHelper.ts +++ b/packages/core/src/codewhisperer/util/telemetryHelper.ts @@ -13,7 +13,6 @@ import { CodewhispererPreviousSuggestionState, CodewhispererUserDecision, CodewhispererUserTriggerDecision, - Status, telemetry, } from '../../shared/telemetry/telemetry' import { CodewhispererCompletionType, CodewhispererSuggestionState } from '../../shared/telemetry/telemetry' @@ -28,7 +27,6 @@ import { CodeWhispererSupplementalContext } from '../models/model' import { FeatureConfigProvider } from '../../shared/featureConfig' import CodeWhispererUserClient, { CodeScanRemediationsEventType } from '../client/codewhispereruserclient' import { CodeAnalysisScope as CodeAnalysisScopeClientSide } from '../models/constants' -import { Session } from '../../amazonqTest/chat/session/session' import { sleep } from '../../shared/utilities/timeoutUtils' import { getDiagnosticsDifferences, getDiagnosticsOfCurrentFile, toIdeDiagnostics } from './diagnosticsUtil' import { Auth } from '../../auth/auth' @@ -71,54 +69,6 @@ export class TelemetryHelper { return (this.#instance ??= new this()) } - public sendTestGenerationToolkitEvent( - session: Session, - isSupportedLanguage: boolean, - isFileInWorkspace: boolean, - result: 'Succeeded' | 'Failed' | 'Cancelled', - requestId?: string, - perfClientLatency?: number, - reasonDesc?: string, - isCodeBlockSelected?: boolean, - artifactsUploadDuration?: number, - buildPayloadBytes?: number, - buildZipFileBytes?: number, - acceptedCharactersCount?: number, - acceptedCount?: number, - acceptedLinesCount?: number, - generatedCharactersCount?: number, - generatedCount?: number, - generatedLinesCount?: number, - reason?: string, - status?: Status - ) { - telemetry.amazonq_utgGenerateTests.emit({ - cwsprChatProgrammingLanguage: session.fileLanguage ?? 'plaintext', - hasUserPromptSupplied: session.hasUserPromptSupplied, - isSupportedLanguage: session.isSupportedLanguage, - isFileInWorkspace: isFileInWorkspace, - result: result, - artifactsUploadDuration: artifactsUploadDuration, - buildPayloadBytes: buildPayloadBytes, - buildZipFileBytes: buildZipFileBytes, - credentialStartUrl: AuthUtil.instance.startUrl, - acceptedCharactersCount: acceptedCharactersCount, - acceptedCount: acceptedCount, - acceptedLinesCount: acceptedLinesCount, - generatedCharactersCount: generatedCharactersCount, - generatedCount: generatedCount, - generatedLinesCount: generatedLinesCount, - isCodeBlockSelected: isCodeBlockSelected, - jobGroup: session.testGenerationJobGroupName, - jobId: session.listOfTestGenerationJobId[0], - perfClientLatency: perfClientLatency, - requestId: requestId, - reasonDesc: reasonDesc, - reason: reason, - status: status, - }) - } - public recordServiceInvocationTelemetry( requestId: string, sessionId: string, diff --git a/packages/core/src/codewhisperer/util/zipUtil.ts b/packages/core/src/codewhisperer/util/zipUtil.ts index 32687a6452c..719116efdc7 100644 --- a/packages/core/src/codewhisperer/util/zipUtil.ts +++ b/packages/core/src/codewhisperer/util/zipUtil.ts @@ -4,7 +4,7 @@ */ import * as vscode from 'vscode' import path from 'path' -import { tempDirPath, testGenerationLogsDir } from '../../shared/filesystemUtilities' +import { tempDirPath } from '../../shared/filesystemUtilities' import { getLogger } from '../../shared/logger/logger' import * as CodeWhispererConstants from '../models/constants' import { ToolkitError } from '../../shared/errors' @@ -21,7 +21,6 @@ import { } from '../models/errors' import { FeatureUseCase } from '../models/constants' import { ChildProcess, ChildProcessOptions } from '../../shared/utilities/processUtils' -import { ProjectZipError } from '../../amazonqTest/error' import { removeAnsi } from '../../shared/utilities/textUtilities' import { normalize } from '../../shared/utilities/pathUtils' import { ZipStream } from '../../shared/utilities/zipStream' @@ -570,56 +569,6 @@ export class ZipUtil { } } - public async generateZipTestGen(projectPath: string, initialExecution: boolean): Promise { - try { - // const repoMapFile = await LspClient.instance.getRepoMapJSON() - const zipDirPath = this.getZipDirPath(FeatureUseCase.TEST_GENERATION) - - const metadataDir = path.join(zipDirPath, 'utgRequiredArtifactsDir') - - // Create directories - const dirs = { - metadata: metadataDir, - buildAndExecuteLogDir: path.join(metadataDir, 'buildAndExecuteLogDir'), - repoMapDir: path.join(metadataDir, 'repoMapData'), - testCoverageDir: path.join(metadataDir, 'testCoverageDir'), - } - await Promise.all(Object.values(dirs).map((dir) => fs.mkdir(dir))) - - // if (await fs.exists(repoMapFile)) { - // await fs.copy(repoMapFile, path.join(dirs.repoMapDir, 'repoMapData.json')) - // await fs.delete(repoMapFile) - // } - - if (!initialExecution) { - await this.processTestCoverageFiles(dirs.testCoverageDir) - - const sourcePath = path.join(testGenerationLogsDir, 'output.log') - const targetPath = path.join(dirs.buildAndExecuteLogDir, 'output.log') - if (await fs.exists(sourcePath)) { - await fs.copy(sourcePath, targetPath) - } - } - - const zipFilePath: string = await this.zipProject(FeatureUseCase.TEST_GENERATION, projectPath, metadataDir) - const zipFileSize = (await fs.stat(zipFilePath)).size - return { - rootDir: zipDirPath, - zipFilePath: zipFilePath, - srcPayloadSizeInBytes: this._totalSize, - scannedFiles: new Set(this._pickedSourceFiles), - zipFileSizeInBytes: zipFileSize, - buildPayloadSizeInBytes: this._totalBuildSize, - lines: this._totalLines, - language: this._language, - } - } catch (error) { - getLogger().error('Zip error caused by: %s', error) - throw new ProjectZipError( - error instanceof Error ? error.message : 'Unknown error occurred during zip operation' - ) - } - } // TODO: Refactor this public async removeTmpFiles(zipMetadata: ZipMetadata, scope?: CodeWhispererConstants.CodeAnalysisScope) { const logger = getLoggerForScope(scope) diff --git a/packages/core/src/shared/db/chatDb/util.ts b/packages/core/src/shared/db/chatDb/util.ts index fc681b2b5a5..316cbe5660c 100644 --- a/packages/core/src/shared/db/chatDb/util.ts +++ b/packages/core/src/shared/db/chatDb/util.ts @@ -267,16 +267,10 @@ function getTabTypeIcon(tabType: TabType): MynahIconsType { switch (tabType) { case 'cwc': return 'chat' - case 'doc': - return 'file' case 'review': return 'bug' case 'gumby': return 'transform' - case 'testgen': - return 'check-list' - case 'featuredev': - return 'code-block' default: return 'chat' } diff --git a/packages/core/src/shared/filesystemUtilities.ts b/packages/core/src/shared/filesystemUtilities.ts index 54ca5b4b0e1..6414fd11b66 100644 --- a/packages/core/src/shared/filesystemUtilities.ts +++ b/packages/core/src/shared/filesystemUtilities.ts @@ -20,8 +20,6 @@ export const tempDirPath = path.join( 'aws-toolkit-vscode' ) -export const testGenerationLogsDir = path.join(tempDirPath, 'testGenerationLogs') - export async function getDirSize(dirPath: string, startTime: number, duration: number): Promise { if (performance.now() - startTime > duration) { getLogger().warn('getDirSize: exceeds time limit') diff --git a/packages/core/src/test/codewhisperer/zipUtil.test.ts b/packages/core/src/test/codewhisperer/zipUtil.test.ts index e6c4f4148e5..102bf2fc441 100644 --- a/packages/core/src/test/codewhisperer/zipUtil.test.ts +++ b/packages/core/src/test/codewhisperer/zipUtil.test.ts @@ -7,15 +7,12 @@ import assert from 'assert' import vscode from 'vscode' import sinon from 'sinon' import { join } from 'path' -import path from 'path' import JSZip from 'jszip' import { getTestWorkspaceFolder } from '../../testInteg/integrationTestsUtilities' import { ZipUtil } from '../../codewhisperer/util/zipUtil' import { CodeAnalysisScope, codeScanTruncDirPrefix } from '../../codewhisperer/models/constants' import { ToolkitError } from '../../shared/errors' import { fs } from '../../shared/fs/fs' -import { tempDirPath } from '../../shared/filesystemUtilities' -import { CodeWhispererConstants } from '../../codewhisperer/indexNode' describe('zipUtil', function () { const workspaceFolder = getTestWorkspaceFolder() @@ -140,43 +137,4 @@ describe('zipUtil', function () { assert.ok(files.includes(join('workspaceFolder', 'workspaceFolder', 'App.java'))) }) }) - - describe('generateZipTestGen', function () { - let zipUtil: ZipUtil - let getZipDirPathStub: sinon.SinonStub - let testTempDirPath: string - - beforeEach(function () { - zipUtil = new ZipUtil() - testTempDirPath = path.join(tempDirPath, CodeWhispererConstants.TestGenerationTruncDirPrefix) - getZipDirPathStub = sinon.stub(zipUtil, 'getZipDirPath') - getZipDirPathStub.callsFake(() => testTempDirPath) - }) - - afterEach(function () { - sinon.restore() - }) - - it('should generate zip for test generation successfully', async function () { - const mkdirSpy = sinon.spy(fs, 'mkdir') - - const result = await zipUtil.generateZipTestGen(appRoot, false) - - assert.ok(mkdirSpy.calledWith(path.join(testTempDirPath, 'utgRequiredArtifactsDir'))) - assert.ok( - mkdirSpy.calledWith(path.join(testTempDirPath, 'utgRequiredArtifactsDir', 'buildAndExecuteLogDir')) - ) - assert.ok(mkdirSpy.calledWith(path.join(testTempDirPath, 'utgRequiredArtifactsDir', 'repoMapData'))) - assert.ok(mkdirSpy.calledWith(path.join(testTempDirPath, 'utgRequiredArtifactsDir', 'testCoverageDir'))) - - assert.strictEqual(result.rootDir, testTempDirPath) - assert.strictEqual(result.zipFilePath, testTempDirPath + CodeWhispererConstants.codeScanZipExt) - assert.ok(result.srcPayloadSizeInBytes > 0) - assert.strictEqual(result.buildPayloadSizeInBytes, 0) - assert.ok(result.zipFileSizeInBytes > 0) - assert.strictEqual(result.lines, 150) - assert.strictEqual(result.language, 'java') - assert.strictEqual(result.scannedFiles.size, 4) - }) - }) })