From e23c61d7571ca39a161ac316687ec6fd477eee0a Mon Sep 17 00:00:00 2001 From: wilson Date: Tue, 12 Nov 2024 10:38:19 -0500 Subject: [PATCH 1/6] feat(feature dev): Add setting to allow Q to run code and test commands (#5853) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Problem Q /dev team is launching a new feature: Allow Q to execute build/test based on customer's configuration 🎉🎉 ## Solution We have made a couple tweaks to incorporate the user experience - Added a new setting to allow Q to run code and test commands per project ![image](https://github.com/user-attachments/assets/d504ee40-6598-4868-87a4-18e23289945c) - Added a pre-filled prompt option for customer to generate a configuration for this feature - Added a couple follow up buttons throughout the /dev flow and added logic around them ![image](https://github.com/user-attachments/assets/57287301-f295-4394-9cd3-4481edce070b) - Updated copies - Updated `codefileExtensions` set --- ...-10418729-4b46-4126-b1af-623a08f0223c.json | 4 + packages/amazonq/package.json | 5 + packages/core/package.nls.json | 7 + .../core/src/amazonqFeatureDev/constants.ts | 3 + .../controllers/chat/controller.ts | 130 +++++++++++++++--- .../src/amazonqFeatureDev/session/session.ts | 4 + packages/core/src/amazonqFeatureDev/types.ts | 3 + .../core/src/amazonqFeatureDev/util/files.ts | 13 +- .../util/codewhispererSettings.ts | 13 ++ packages/core/src/shared/filetypes.ts | 12 +- .../core/src/shared/settings-amazonq.gen.ts | 1 + .../controllers/chat/controller.test.ts | 10 +- 12 files changed, 178 insertions(+), 27 deletions(-) create mode 100644 packages/amazonq/.changes/next-release/Feature-10418729-4b46-4126-b1af-623a08f0223c.json diff --git a/packages/amazonq/.changes/next-release/Feature-10418729-4b46-4126-b1af-623a08f0223c.json b/packages/amazonq/.changes/next-release/Feature-10418729-4b46-4126-b1af-623a08f0223c.json new file mode 100644 index 00000000000..c957071b238 --- /dev/null +++ b/packages/amazonq/.changes/next-release/Feature-10418729-4b46-4126-b1af-623a08f0223c.json @@ -0,0 +1,4 @@ +{ + "type": "Feature", + "description": "Add setting to allow Q /dev to run code and test commands" +} diff --git a/packages/amazonq/package.json b/packages/amazonq/package.json index a22424453d6..e98b4f03085 100644 --- a/packages/amazonq/package.json +++ b/packages/amazonq/package.json @@ -127,6 +127,11 @@ "markdownDescription": "%AWS.configuration.description.amazonq%", "default": true }, + "amazonQ.devCommandWorkspaceConfigurations": { + "markdownDescription": "%AWS.configuration.description.devCommandWorkspaceConfigurations%", + "type": "object", + "default": {} + }, "amazonQ.importRecommendationForInlineCodeSuggestions": { "type": "boolean", "description": "%AWS.configuration.description.amazonq.importRecommendation%", diff --git a/packages/core/package.nls.json b/packages/core/package.nls.json index bc3d2182452..0f5549424b1 100644 --- a/packages/core/package.nls.json +++ b/packages/core/package.nls.json @@ -21,6 +21,7 @@ "AWS.configuration.enableCodeLenses": "Enable SAM hints in source code and template.yaml files", "AWS.configuration.description.resources.enabledResources": "AWS resources to display in the 'Resources' portion of the explorer.", "AWS.configuration.description.experiments": "Try experimental features and give feedback. Note that experimental features may be removed at any time.\n * `jsonResourceModification` - Enables basic create, update, and delete support for cloud resources via the JSON Resources explorer component.\n * `samSyncCode` - Adds an additional code-only option when synchronizing SAM applications. Code-only synchronizations are faster but can cause drift in the CloudFormation stack. Does nothing when using the legacy SAM deploy feature.\n * `iamPolicyChecks` - Enables IAM Policy Checks feature, allowing users to validate IAM policies against IAM policy grammar, AWS best practices, and specified security standards.", + "AWS.configuration.description.devCommandWorkspaceConfigurations": "Amazon Q: Allow Q /dev to run code and test commands", "AWS.stepFunctions.asl.format.enable.desc": "Enables the default formatter used with Amazon States Language files", "AWS.stepFunctions.asl.maxItemsComputed.desc": "The maximum number of outline symbols and folding regions computed (limited for performance reasons).", "AWS.configuration.description.awssam.debug.api": "API Gateway configuration", @@ -322,12 +323,18 @@ "AWS.amazonq.featureDev.pillText.selectOption": "Choose an option to proceed", "AWS.amazonq.featureDev.pillText.unableGenerateChanges": "Unable to generate any file changes", "AWS.amazonq.featureDev.pillText.provideFeedback": "Provide feedback & regenerate", + "AWS.amazonq.featureDev.pillText.generateDevFile": "Generate devfile to build code", + "AWS.amazonq.featureDev.pillText.acceptForProject": "Yes, use my devfile for this project", + "AWS.amazonq.featureDev.pillText.declineForProject": "No, thanks", "AWS.amazonq.featureDev.answer.generateSuggestion": "Would you like to generate a suggestion for this? You’ll review a file diff before inserting into your project.", "AWS.amazonq.featureDev.answer.qGeneratedCode": "The Amazon Q Developer Agent for software development has generated code for you to review", "AWS.amazonq.featureDev.answer.howCodeCanBeImproved": "How can I improve the code for your use case?", "AWS.amazonq.featureDev.answer.updateCode": "Okay, I updated your code files. Would you like to work on another task?", "AWS.amazonq.featureDev.answer.sessionClosed": "Okay, I've ended this chat session. You can open a new tab to chat or start another workflow.", "AWS.amazonq.featureDev.answer.newTaskChanges": "What new task would you like to work on?", + "AWS.amazonq.featureDev.answer.devFileSuggestion": "For future tasks in this project, I can create a devfile to build and test code as I generate it. This can improve the quality of generated code. To allow me to create a devfile, choose **Generate devfile to build code**.", + "AWS.amazonq.featureDev.answer.settingUpdated": "I've updated your settings so I can run code and test commands based on your devfile for this project. You can update this setting under **Amazon Q: Allow Q /dev to run code and test commands**.", + "AWS.amazonq.featureDev.answer.devFileInRepository": "I noticed that your repository has a `devfile.yaml`. Would you like me to use the devfile to build and test your project as I generate code? \n\nFor more information on using devfiles to improve code generation, see the Amazon Q Developer documentation.", "AWS.amazonq.featureDev.placeholder.chatInputDisabled": "Chat input is disabled", "AWS.amazonq.featureDev.placeholder.additionalImprovements": "Describe your task or issue in detail", "AWS.amazonq.featureDev.placeholder.feedback": "Provide feedback or comments", diff --git a/packages/core/src/amazonqFeatureDev/constants.ts b/packages/core/src/amazonqFeatureDev/constants.ts index 72d490e7ec4..d42566de36c 100644 --- a/packages/core/src/amazonqFeatureDev/constants.ts +++ b/packages/core/src/amazonqFeatureDev/constants.ts @@ -14,6 +14,9 @@ export const featureDevChat = 'featureDevChat' export const featureName = 'Amazon Q Developer Agent for software development' +export const generateDevFilePrompt = + "generate a devfile in my repository. Note that you should only use devfile version 2.0.0 and the only supported command is test, so you should bundle all install, build and test commands in “test”. also you can use “public.ecr.aws/aws-mde/universal-image:latest” as universal image if you aren’t sure which image to use. here is an example for a node repository (but don't assume it's always a node project. look at the existing repository structure before generating the devfile): schemaVersion: 2.0.0 components: - name: dev container: image: public.ecr.aws/aws-mde/universal-image:latest commands: - id: test exec: component: dev commandLine: “npm install && npm run build && npm run test”" + // Max allowed size for file collection export const maxRepoSizeBytes = 200 * 1024 * 1024 diff --git a/packages/core/src/amazonqFeatureDev/controllers/chat/controller.ts b/packages/core/src/amazonqFeatureDev/controllers/chat/controller.ts index aa300b9ddba..7e46e1736e7 100644 --- a/packages/core/src/amazonqFeatureDev/controllers/chat/controller.ts +++ b/packages/core/src/amazonqFeatureDev/controllers/chat/controller.ts @@ -29,7 +29,7 @@ import { } from '../../errors' import { codeGenRetryLimit, defaultRetryLimit } from '../../limits' import { Session } from '../../session/session' -import { featureName } from '../../constants' +import { featureName, generateDevFilePrompt } from '../../constants' import { ChatSessionStorage } from '../../storages/chatSession' import { DevPhase, FollowUpTypes, SessionStatePhase } from '../../types' import { Messenger } from './messenger/messenger' @@ -40,12 +40,13 @@ import { submitFeedback } from '../../../feedback/vue/submitFeedback' import { placeholder } from '../../../shared/vscode/commands2' import { EditorContentController } from '../../../amazonq/commons/controllers/contentController' import { openUrl } from '../../../shared/utilities/vsCodeUtils' -import { getPathsFromZipFilePath } from '../../util/files' +import { checkForDevFile, getPathsFromZipFilePath } from '../../util/files' import { examples, messageWithConversationId } from '../../userFacingText' import { getWorkspaceFoldersByPrefixes } from '../../../shared/utilities/workspaceUtils' import { openDeletedDiff, openDiff } from '../../../amazonq/commons/diff' import { i18n } from '../../../shared/i18n-helper' import globals from '../../../shared/extensionGlobals' +import { CodeWhispererSettings } from '../../../codewhisperer' export const TotalSteps = 3 @@ -147,6 +148,17 @@ export class FeatureDevController { case FollowUpTypes.SendFeedback: this.sendFeedback() break + case FollowUpTypes.AcceptAutoBuild: + return this.processDevCommandWorkspaceSetting(true, data) + case FollowUpTypes.DenyAutoBuild: + return this.processDevCommandWorkspaceSetting(false, data) + case FollowUpTypes.GenerateDevFile: + this.messenger.sendAnswer({ + type: 'system-prompt', + tabID: data?.tabID, + message: i18n('AWS.amazonq.featureDev.pillText.generateDevFile'), + }) + return this.newTask(data, generateDevFilePrompt) } }) this.chatControllerMessageListeners.openDiff.event((data) => { @@ -361,6 +373,16 @@ export class FeatureDevController { return } + const root = session.getWorkspaceRoot() + const autoBuildProjectSetting = CodeWhispererSettings.instance.getDevCommandWorkspaceConfigurations() + const hasDevfile = await checkForDevFile(root) + const isPromptedForAutoBuildFeature = Object.keys(autoBuildProjectSetting).includes(root) + + if (hasDevfile && !isPromptedForAutoBuildFeature) { + await this.promptAllowQCommandsConsent(message.tabID) + return + } + await session.preloader(message.message) if (session.state.phase === DevPhase.CODEGEN) { @@ -374,6 +396,32 @@ export class FeatureDevController { } } + private async promptAllowQCommandsConsent(tabID: string) { + this.messenger.sendAnswer({ + tabID: tabID, + message: i18n('AWS.amazonq.featureDev.answer.devFileInRepository'), + type: 'answer', + }) + + this.messenger.sendAnswer({ + message: undefined, + type: 'system-prompt', + followUps: [ + { + pillText: i18n('AWS.amazonq.featureDev.pillText.acceptForProject'), + type: FollowUpTypes.AcceptAutoBuild, + status: 'success', + }, + { + pillText: i18n('AWS.amazonq.featureDev.pillText.declineForProject'), + type: FollowUpTypes.DenyAutoBuild, + status: 'error', + }, + ], + tabID: tabID, + }) + } + /** * Handle a regular incoming message when a user is in the code generation phase */ @@ -447,8 +495,8 @@ export class FeatureDevController { tabID: tabID, message: remainingIterations === 0 - ? 'Would you like me to add this code to your project?' - : `Would you like me to add this code to your project, or provide feedback for new code? You have ${remainingIterations} out of ${totalIterations} code generations left.`, + ? 'Would you like to add this code to your project?' + : `Would you like to add this code to your project, or provide feedback for new code? You have ${remainingIterations} out of ${totalIterations} code generations left.`, }) } @@ -463,7 +511,7 @@ export class FeatureDevController { // Finish processing the event if (session?.state?.tokenSource?.token.isCancellationRequested) { - this.workOnNewTask( + await this.workOnNewTask( session, session.state.codeGenerationRemainingIterationCount || TotalSteps - (session.state?.currentIteration || 0), @@ -491,12 +539,16 @@ export class FeatureDevController { } } } - private workOnNewTask( + private async workOnNewTask( message: any, remainingIterations: number = 0, totalIterations?: number, isStoppedGeneration: boolean = false ) { + const hasDevFile = await checkForDevFile( + (await this.sessionStorage.getSession(message.tabID)).getWorkspaceRoot() + ) + if (isStoppedGeneration) { this.messenger.sendAnswer({ message: @@ -509,21 +561,37 @@ export class FeatureDevController { } if ((remainingIterations <= 0 && isStoppedGeneration) || !isStoppedGeneration) { + const followUps: Array = [ + { + pillText: i18n('AWS.amazonq.featureDev.pillText.newTask'), + type: FollowUpTypes.NewTask, + status: 'info', + }, + { + pillText: i18n('AWS.amazonq.featureDev.pillText.closeSession'), + type: FollowUpTypes.CloseSession, + status: 'info', + }, + ] + + if (!hasDevFile) { + followUps.push({ + pillText: i18n('AWS.amazonq.featureDev.pillText.generateDevFile'), + type: FollowUpTypes.GenerateDevFile, + status: 'info', + }) + + this.messenger.sendAnswer({ + type: 'answer', + tabID: message.tabID, + message: i18n('AWS.amazonq.featureDev.answer.devFileSuggestion'), + }) + } + this.messenger.sendAnswer({ type: 'system-prompt', tabID: message.tabID, - followUps: [ - { - pillText: i18n('AWS.amazonq.featureDev.pillText.newTask'), - type: FollowUpTypes.NewTask, - status: 'info', - }, - { - pillText: i18n('AWS.amazonq.featureDev.pillText.closeSession'), - type: FollowUpTypes.CloseSession, - status: 'info', - }, - ], + followUps, }) this.messenger.sendChatInputEnabled(message.tabID, false) this.messenger.sendUpdatePlaceholder(message.tabID, i18n('AWS.amazonq.featureDev.pillText.selectOption')) @@ -537,6 +605,20 @@ export class FeatureDevController { i18n('AWS.amazonq.featureDev.placeholder.additionalImprovements') ) } + + private async processDevCommandWorkspaceSetting(setting: boolean, msg: any) { + const root = (await this.sessionStorage.getSession(msg.tabID)).getWorkspaceRoot() + await CodeWhispererSettings.instance.updateDevCommandWorkspaceConfigurations(root, setting) + + this.messenger.sendAnswer({ + message: i18n('AWS.amazonq.featureDev.answer.settingUpdated'), + tabID: msg.tabID, + type: 'answer', + }) + + await this.retryRequest(msg) + } + // TODO add type private async insertCode(message: any) { let session @@ -563,7 +645,7 @@ export class FeatureDevController { canBeVoted: true, }) - this.workOnNewTask( + await this.workOnNewTask( message, session.state.codeGenerationRemainingIterationCount, session.state.codeGenerationTotalIterationCount @@ -854,7 +936,7 @@ export class FeatureDevController { this.sessionStorage.deleteSession(message.tabID) } - private async newTask(message: any) { + private async newTask(message: any, prefilledPrompt?: string) { // Old session for the tab is ending, delete it so we can create a new one for the message id const session = await this.sessionStorage.getSession(message.tabID) telemetry.amazonq_endChat.emit({ @@ -867,8 +949,12 @@ export class FeatureDevController { // Re-run the opening flow, where we check auth + create a session await this.tabOpened(message) - this.messenger.sendChatInputEnabled(message.tabID, true) - this.messenger.sendUpdatePlaceholder(message.tabID, i18n('AWS.amazonq.featureDev.placeholder.describe')) + if (prefilledPrompt) { + await this.processUserChatMessage({ ...message, message: prefilledPrompt }) + } else { + this.messenger.sendChatInputEnabled(message.tabID, true) + this.messenger.sendUpdatePlaceholder(message.tabID, i18n('AWS.amazonq.featureDev.placeholder.describe')) + } } private async closeSession(message: any) { diff --git a/packages/core/src/amazonqFeatureDev/session/session.ts b/packages/core/src/amazonqFeatureDev/session/session.ts index 204e974eee0..e10c7453045 100644 --- a/packages/core/src/amazonqFeatureDev/session/session.ts +++ b/packages/core/src/amazonqFeatureDev/session/session.ts @@ -103,6 +103,10 @@ export class Session { this._state && this._state.updateWorkspaceRoot && this._state.updateWorkspaceRoot(workspaceRootFolder) } + getWorkspaceRoot(): string { + return this.config.workspaceRoots[0] + } + private getSessionStateConfig(): Omit { return { workspaceRoots: this.config.workspaceRoots, diff --git a/packages/core/src/amazonqFeatureDev/types.ts b/packages/core/src/amazonqFeatureDev/types.ts index cf046743425..71c69923f4d 100644 --- a/packages/core/src/amazonqFeatureDev/types.ts +++ b/packages/core/src/amazonqFeatureDev/types.ts @@ -49,6 +49,9 @@ export enum FollowUpTypes { NewTask = 'NewTask', CloseSession = 'CloseSession', SendFeedback = 'SendFeedback', + AcceptAutoBuild = 'AcceptAutoBuild', + DenyAutoBuild = 'DenyAutoBuild', + GenerateDevFile = 'GenerateDevFile', } export type SessionStatePhase = DevPhase.INIT | DevPhase.CODEGEN diff --git a/packages/core/src/amazonqFeatureDev/util/files.ts b/packages/core/src/amazonqFeatureDev/util/files.ts index 1b83bdbe2b5..a14cd0e859d 100644 --- a/packages/core/src/amazonqFeatureDev/util/files.ts +++ b/packages/core/src/amazonqFeatureDev/util/files.ts @@ -19,9 +19,16 @@ import { TelemetryHelper } from './telemetryHelper' import { maxRepoSizeBytes } from '../constants' import { isCodeFile } from '../../shared/filetypes' import { fs } from '../../shared' +import { CodeWhispererSettings } from '../../codewhisperer' const getSha256 = (file: Buffer) => createHash('sha256').update(file).digest('base64') +export async function checkForDevFile(root: string) { + const devFilePath = root + '/devfile.yaml' + const hasDevFile = await fs.existsFile(devFilePath) + return hasDevFile +} + /** * given the root path of the repo it zips its files in memory and generates a checksum for it. */ @@ -34,6 +41,8 @@ export async function prepareRepoData( ) { try { const files = await collectFiles(repoRootPaths, workspaceFolders, true, maxRepoSizeBytes) + const devCommandWorkspaceConfigurations = CodeWhispererSettings.instance.getDevCommandWorkspaceConfigurations() + const useAutoBuildFeature = devCommandWorkspaceConfigurations[repoRootPaths[0]] ?? false let totalBytes = 0 const ignoredExtensionMap = new Map() @@ -41,8 +50,10 @@ export async function prepareRepoData( for (const file of files) { const fileSize = (await fs.stat(file.fileUri)).size const isCodeFile_ = isCodeFile(file.relativeFilePath) + // exclude user's devfile if `useAutoBuildFeature` is set to false + const excludeDevFile = useAutoBuildFeature ? false : file.relativeFilePath === 'devfile.yaml' - if (fileSize >= maxFileSizeBytes || !isCodeFile_) { + if (fileSize >= maxFileSizeBytes || !isCodeFile_ || excludeDevFile) { if (!isCodeFile_) { const re = /(?:\.([^.]+))?$/ const extensionArray = re.exec(file.relativeFilePath) diff --git a/packages/core/src/codewhisperer/util/codewhispererSettings.ts b/packages/core/src/codewhisperer/util/codewhispererSettings.ts index 09c7e2657bd..56cfad2a0b2 100644 --- a/packages/core/src/codewhisperer/util/codewhispererSettings.ts +++ b/packages/core/src/codewhisperer/util/codewhispererSettings.ts @@ -12,6 +12,7 @@ const description = { workspaceIndexWorkerThreads: Number, workspaceIndexUseGPU: Boolean, workspaceIndexMaxSize: Number, + devCommandWorkspaceConfigurations: Object, } export class CodeWhispererSettings extends fromExtensionManifest('amazonQ', description) { @@ -64,6 +65,18 @@ export class CodeWhispererSettings extends fromExtensionManifest('amazonQ', desc return Math.max(this.get('workspaceIndexMaxSize', 250), 1) } + public getDevCommandWorkspaceConfigurations(): { [key: string]: boolean } { + return this.get('devCommandWorkspaceConfigurations', {}) + } + + public async updateDevCommandWorkspaceConfigurations(projectName: string, setting: boolean) { + const projects = this.getDevCommandWorkspaceConfigurations() + + projects[projectName] = setting + + await this.update('devCommandWorkspaceConfigurations', projects) + } + static #instance: CodeWhispererSettings public static get instance() { diff --git a/packages/core/src/shared/filetypes.ts b/packages/core/src/shared/filetypes.ts index 446cabe0a2a..01b9d885739 100644 --- a/packages/core/src/shared/filetypes.ts +++ b/packages/core/src/shared/filetypes.ts @@ -165,6 +165,7 @@ export const codefileExtensions = new Set([ '.cbl', '.cc', '.cfc', + '.cfg', '.cfm', '.cjs', '.clj', @@ -175,6 +176,7 @@ export const codefileExtensions = new Set([ '.cob', '.cobra', '.coffee', + '.config', '.cpp', '.cpy', '.cr', @@ -189,6 +191,7 @@ export const codefileExtensions = new Set([ '.e', '.el', '.elm', + '.env', '.erl', '.ex', '.exs', @@ -204,6 +207,7 @@ export const codefileExtensions = new Set([ '.fsi', '.fsx', '.gd', + '.gitignore', '.go', '.gql', '.graphql', @@ -223,6 +227,7 @@ export const codefileExtensions = new Set([ '.html', '.hy', '.idl', + '.ini', '.io', '.jar', '.java', @@ -236,6 +241,7 @@ export const codefileExtensions = new Set([ '.lgt', '.lhs', '.lisp', + '.lock', '.logtalk', '.lsp', '.lua', @@ -320,14 +326,17 @@ export const codefileExtensions = new Set([ '.ss', '.st', '.sv', + '.svg', '.swift', '.t', '.tcl', '.tf', + '.toml', '.trigger', '.ts', '.tsx', '.tu', + '.txt', '.v', '.vala', '.vapi', @@ -349,5 +358,6 @@ export const codefileExtensions = new Set([ /** Returns true if `filename` is a code file. */ export function isCodeFile(filename: string): boolean { const ext = path.extname(filename).toLowerCase() - return codefileExtensions.has(ext) + const result = codefileExtensions.has(ext) || codefileExtensions.has(filename) + return result } diff --git a/packages/core/src/shared/settings-amazonq.gen.ts b/packages/core/src/shared/settings-amazonq.gen.ts index 4fbe8ab0005..e9dd5016c93 100644 --- a/packages/core/src/shared/settings-amazonq.gen.ts +++ b/packages/core/src/shared/settings-amazonq.gen.ts @@ -21,6 +21,7 @@ export const amazonqSettings = { "ssoCacheError": {} }, "amazonQ.showInlineCodeSuggestionsWithCodeReferences": {}, + "amazonQ.devCommandWorkspaceConfigurations": {}, "amazonQ.importRecommendationForInlineCodeSuggestions": {}, "amazonQ.shareContentWithAWS": {}, "amazonQ.workspaceIndex": {}, diff --git a/packages/core/src/test/amazonqFeatureDev/controllers/chat/controller.test.ts b/packages/core/src/test/amazonqFeatureDev/controllers/chat/controller.test.ts index e7e8ecdb6fc..a81fb3c515b 100644 --- a/packages/core/src/test/amazonqFeatureDev/controllers/chat/controller.test.ts +++ b/packages/core/src/test/amazonqFeatureDev/controllers/chat/controller.test.ts @@ -391,9 +391,14 @@ describe('Controller', () => { message: 'test message', }) - // Wait until the controller has time to process the event + /** + * Wait until the controller has time to process the event + * Sessions should be called twice: + * 1. When the session getWorkspaceRoot is called + * 2. When the controller processes preloader + */ await waitUntil(() => { - return Promise.resolve(getSessionStub.callCount > 0) + return Promise.resolve(getSessionStub.callCount > 1) }, {}) } @@ -432,7 +437,6 @@ describe('Controller', () => { const sendAnswerSpy = sinon.stub(controllerSetup.messenger, 'sendAnswer') const sendErrorMessageSpy = sinon.stub(controllerSetup.messenger, 'sendErrorMessage') const sendMonthlyLimitErrorSpy = sinon.stub(controllerSetup.messenger, 'sendMonthlyLimitError') - await fireChatMessage() switch (error.constructor.name) { From fbeb3a12bfd81bfa15b88c1293fc8fc598e5c7a7 Mon Sep 17 00:00:00 2001 From: wilson Date: Thu, 14 Nov 2024 12:03:37 -0500 Subject: [PATCH 2/6] fix(feature dev): retry message without problem statement #5999 ## Problem retry message without problem statement ## Solution Move latestMessage setter call outside of session in controller as it interrupts with retryMessage functionality --- .../unit/amazonqFeatureDev/session/session.test.ts | 3 ++- .../controllers/chat/controller.ts | 5 ++++- .../core/src/amazonqFeatureDev/session/session.ts | 13 +++++++------ 3 files changed, 13 insertions(+), 8 deletions(-) diff --git a/packages/amazonq/test/unit/amazonqFeatureDev/session/session.test.ts b/packages/amazonq/test/unit/amazonqFeatureDev/session/session.test.ts index f2a08348d23..b033073c42a 100644 --- a/packages/amazonq/test/unit/amazonqFeatureDev/session/session.test.ts +++ b/packages/amazonq/test/unit/amazonqFeatureDev/session/session.test.ts @@ -37,8 +37,9 @@ describe('session', () => { describe('preloader', () => { it('emits start chat telemetry', async () => { const session = await createSession({ messenger, conversationID }) + session.latestMessage = 'implement twosum in typescript' - await session.preloader('implement twosum in typescript') + await session.preloader() assertTelemetry('amazonq_startConversationInvoke', { amazonqConversationId: conversationID, diff --git a/packages/core/src/amazonqFeatureDev/controllers/chat/controller.ts b/packages/core/src/amazonqFeatureDev/controllers/chat/controller.ts index 7e46e1736e7..9011bda4aba 100644 --- a/packages/core/src/amazonqFeatureDev/controllers/chat/controller.ts +++ b/packages/core/src/amazonqFeatureDev/controllers/chat/controller.ts @@ -366,6 +366,9 @@ export class FeatureDevController { getLogger().debug(`${featureName}: Processing message: ${message.message}`) session = await this.sessionStorage.getSession(message.tabID) + // set latestMessage in session as retry would lose context if function returns early + session.latestMessage = message.message + const authState = await AuthUtil.instance.getChatAuthState() if (authState.amazonQ !== 'connected') { await this.messenger.sendAuthNeededExceptionMessage(authState, message.tabID) @@ -383,7 +386,7 @@ export class FeatureDevController { return } - await session.preloader(message.message) + await session.preloader() if (session.state.phase === DevPhase.CODEGEN) { await this.onCodeGeneration(session, message.message, message.tabID) diff --git a/packages/core/src/amazonqFeatureDev/session/session.ts b/packages/core/src/amazonqFeatureDev/session/session.ts index e10c7453045..9d6dff81741 100644 --- a/packages/core/src/amazonqFeatureDev/session/session.ts +++ b/packages/core/src/amazonqFeatureDev/session/session.ts @@ -58,9 +58,9 @@ export class Session { /** * Preload any events that have to run before a chat message can be sent */ - async preloader(msg: string) { + async preloader() { if (!this.preloaderFinished) { - await this.setupConversation(msg) + await this.setupConversation() this.preloaderFinished = true this.messenger.sendAsyncEventProgress(this.tabID, true, undefined) await this.proxyClient.sendFeatureDevTelemetryEvent(this.conversationId) // send the event only once per conversation. @@ -72,10 +72,7 @@ export class Session { * * Starts a conversation with the backend and uploads the repo for the LLMs to be able to use it. */ - private async setupConversation(msg: string) { - // Store the initial message when setting up the conversation so that if it fails we can retry with this message - this._latestMessage = msg - + private async setupConversation() { await telemetry.amazonq_startConversationInvoke.run(async (span) => { this._conversationId = await this.proxyClient.createConversation() getLogger().info(logWithConversationId(this.conversationId)) @@ -221,6 +218,10 @@ export class Session { return this._latestMessage } + set latestMessage(msg: string) { + this._latestMessage = msg + } + get telemetry() { return this._telemetry } From ac761711b2f4806ca66e6bfb088b8e84308cf4e9 Mon Sep 17 00:00:00 2001 From: wilson Date: Thu, 14 Nov 2024 13:39:50 -0500 Subject: [PATCH 3/6] test: update /dev prepareRepoData unit test to include devfile filtering (#5980) ## Problem Follow up PR from #5853 ## Solution Added more unit test coverage --- .../unit/amazonqFeatureDev/util/files.test.ts | 95 ++++++++++++------- 1 file changed, 62 insertions(+), 33 deletions(-) diff --git a/packages/amazonq/test/unit/amazonqFeatureDev/util/files.test.ts b/packages/amazonq/test/unit/amazonqFeatureDev/util/files.test.ts index e8ecefc171e..e18521de84e 100644 --- a/packages/amazonq/test/unit/amazonqFeatureDev/util/files.test.ts +++ b/packages/amazonq/test/unit/amazonqFeatureDev/util/files.test.ts @@ -10,55 +10,84 @@ import { ContentLengthError, maxRepoSizeBytes, } from 'aws-core-vscode/amazonqFeatureDev' -import { assertTelemetry, createTestWorkspace } from 'aws-core-vscode/test' +import { assertTelemetry, getWorkspaceFolder, TestFolder } from 'aws-core-vscode/test' import { fs, AmazonqCreateUpload } from 'aws-core-vscode/shared' -import { Span } from 'aws-core-vscode/telemetry' +import { MetricName, Span } from 'aws-core-vscode/telemetry' import sinon from 'sinon' +import { CodeWhispererSettings } from 'aws-core-vscode/codewhisperer' + +const testDevfilePrepareRepo = async (expectedRepoSize: number, devfileEnabled: boolean) => { + const folder = await TestFolder.create() + await folder.write('devfile.yaml', 'test') + await folder.write('file.md', 'test content') + const workspace = getWorkspaceFolder(folder.path) + sinon + .stub(CodeWhispererSettings.instance, 'getDevCommandWorkspaceConfigurations') + .returns(devfileEnabled ? { [workspace.uri.fsPath]: true } : {}) + + await testPrepareRepoData(workspace, expectedRepoSize) +} + +const testPrepareRepoData = async ( + workspace: vscode.WorkspaceFolder, + expectedRepoSize: number, + expectedTelemetryMetrics?: Array<{ metricName: MetricName; value: any }> +) => { + const telemetry = new TelemetryHelper() + const result = await prepareRepoData([workspace.uri.fsPath], [workspace], telemetry, { + record: () => {}, + } as unknown as Span) + + assert.strictEqual(Buffer.isBuffer(result.zipFileBuffer), true) + // checksum is not the same across different test executions because some unique random folder names are generated + assert.strictEqual(result.zipFileChecksum.length, 44) + assert.strictEqual(telemetry.repositorySize, expectedRepoSize) + + if (expectedTelemetryMetrics) { + expectedTelemetryMetrics.forEach((metric) => { + assertTelemetry(metric.metricName, metric.value) + }) + } +} describe('file utils', () => { describe('prepareRepoData', function () { - it('returns files in the workspace as a zip', async function () { - // these variables are a manual selection of settings for the test in order to test the collectFiles function - const fileAmount = 2 - const fileNamePrefix = 'file' - const fileNameSuffix = '.md' - const fileContent = 'test content' + afterEach(() => { + sinon.restore() + }) - const workspace = await createTestWorkspace(fileAmount, { fileNamePrefix, fileContent, fileNameSuffix }) + it('returns files in the workspace as a zip', async function () { + const folder = await TestFolder.create() + await folder.write('file1.md', 'test content') + await folder.write('file2.md', 'test content') + const workspace = getWorkspaceFolder(folder.path) - const telemetry = new TelemetryHelper() - const result = await prepareRepoData([workspace.uri.fsPath], [workspace], telemetry, { - record: () => {}, - } as unknown as Span) - assert.strictEqual(Buffer.isBuffer(result.zipFileBuffer), true) - // checksum is not the same across different test executions because some unique random folder names are generated - assert.strictEqual(result.zipFileChecksum.length, 44) - assert.strictEqual(telemetry.repositorySize, 24) + await testPrepareRepoData(workspace, 24) }) it('prepareRepoData ignores denied file extensions', async function () { - // these variables are a manual selection of settings for the test in order to test the collectFiles function - const fileAmount = 1 - const fileNamePrefix = 'file' - const fileNameSuffix = '.mp4' - const fileContent = 'test content' + const folder = await TestFolder.create() + await folder.write('file.mp4', 'test content') + const workspace = getWorkspaceFolder(folder.path) - const workspace = await createTestWorkspace(fileAmount, { fileNamePrefix, fileContent, fileNameSuffix }) - const telemetry = new TelemetryHelper() - const result = await prepareRepoData([workspace.uri.fsPath], [workspace], telemetry, { - record: () => {}, - } as unknown as Span) + await testPrepareRepoData(workspace, 0, [ + { metricName: 'amazonq_bundleExtensionIgnored', value: { filenameExt: 'mp4', count: 1 } }, + ]) + }) + + it('should ignore devfile.yaml when setting is disabled', async function () { + await testDevfilePrepareRepo(12, false) + }) - assert.strictEqual(Buffer.isBuffer(result.zipFileBuffer), true) - // checksum is not the same across different test executions because some unique random folder names are generated - assert.strictEqual(result.zipFileChecksum.length, 44) - assert.strictEqual(telemetry.repositorySize, 0) - assertTelemetry('amazonq_bundleExtensionIgnored', { filenameExt: 'mp4', count: 1 }) + it('should include devfile.yaml when setting is enabled', async function () { + await testDevfilePrepareRepo(16, true) }) // Test the logic that allows the customer to modify root source folder it('prepareRepoData throws a ContentLengthError code when repo is too big', async function () { - const workspace = await createTestWorkspace(1, {}) + const folder = await TestFolder.create() + await folder.write('file.md', 'test content') + const workspace = getWorkspaceFolder(folder.path) const telemetry = new TelemetryHelper() sinon.stub(fs, 'stat').resolves({ size: 2 * maxRepoSizeBytes } as vscode.FileStat) From 24bb920a041fd16d5c6ddc81f7aff6e37e138117 Mon Sep 17 00:00:00 2001 From: Karan Ahluwalia Date: Fri, 24 Jan 2025 10:44:03 -0800 Subject: [PATCH 4/6] feat(dev): Update file generation logic and file upload functionality for dev-execution. (#6424) #### Enhanced Development Prompt: - Modified GENERATE_DEV_FILE_PROMPT to support running install, build, and test commands independently. #### Improved Build and Code File Upload Logic: - Updated the logic to enable uploading build files alongside code files --- - Treat all work as PUBLIC. Private `feature/x` branches will not be squash-merged at release time. - Your code changes must meet the guidelines in [CONTRIBUTING.md](https://github.com/aws/aws-toolkit-vscode/blob/master/CONTRIBUTING.md#guidelines). - License: I confirm that my contribution is made under the terms of the Apache 2.0 license. --- packages/core/src/amazonqFeatureDev/constants.ts | 2 +- packages/core/src/shared/filetypes.ts | 9 ++++++++- packages/core/src/test/shared/filetypes.test.ts | 1 + 3 files changed, 10 insertions(+), 2 deletions(-) diff --git a/packages/core/src/amazonqFeatureDev/constants.ts b/packages/core/src/amazonqFeatureDev/constants.ts index d42566de36c..5c00e8d7bfa 100644 --- a/packages/core/src/amazonqFeatureDev/constants.ts +++ b/packages/core/src/amazonqFeatureDev/constants.ts @@ -15,7 +15,7 @@ export const featureDevChat = 'featureDevChat' export const featureName = 'Amazon Q Developer Agent for software development' export const generateDevFilePrompt = - "generate a devfile in my repository. Note that you should only use devfile version 2.0.0 and the only supported command is test, so you should bundle all install, build and test commands in “test”. also you can use “public.ecr.aws/aws-mde/universal-image:latest” as universal image if you aren’t sure which image to use. here is an example for a node repository (but don't assume it's always a node project. look at the existing repository structure before generating the devfile): schemaVersion: 2.0.0 components: - name: dev container: image: public.ecr.aws/aws-mde/universal-image:latest commands: - id: test exec: component: dev commandLine: “npm install && npm run build && npm run test”" + "generate a devfile in my repository. Note that you should only use devfile version 2.0.0 and the only supported commands are install, build and test (are all optional). so you may have to bundle some commands together using '&&'. also you can use ”public.ecr.aws/aws-mde/universal-image:latest” as universal image if you aren’t sure which image to use. here is an example for a node repository (but don't assume it's always a node project. look at the existing repository structure before generating the devfile): schemaVersion: 2.0.0 components: - name: dev container: image: public.ecr.aws/aws-mde/universal-image:latest commands: - id: install exec: component: dev commandLine: ”npm install” - id: build exec: component: dev commandLine: ”npm run build” - id: test exec: component: dev commandLine: ”npm run test”" // Max allowed size for file collection export const maxRepoSizeBytes = 200 * 1024 * 1024 diff --git a/packages/core/src/shared/filetypes.ts b/packages/core/src/shared/filetypes.ts index a53a0d43793..34d6f94f599 100644 --- a/packages/core/src/shared/filetypes.ts +++ b/packages/core/src/shared/filetypes.ts @@ -364,8 +364,15 @@ export const codefileExtensions = new Set([ // Code file names without an extension export const codefileNames = new Set(['Dockerfile', 'Dockerfile.build', 'gradlew', 'mvnw']) +// Build file names +export const buildfileNames = new Set(['gradle/wrapper/gradle-wrapper.jar']) + /** Returns true if `filename` is a code file. */ export function isCodeFile(filename: string): boolean { const ext = path.extname(filename).toLowerCase() - return codefileExtensions.has(ext) || codefileNames.has(path.basename(filename)) + return ( + codefileExtensions.has(ext) || + codefileNames.has(path.basename(filename)) || + buildfileNames.has(path.basename(filename)) + ) } diff --git a/packages/core/src/test/shared/filetypes.test.ts b/packages/core/src/test/shared/filetypes.test.ts index 26e12631419..2ae6e82ff8c 100644 --- a/packages/core/src/test/shared/filetypes.test.ts +++ b/packages/core/src/test/shared/filetypes.test.ts @@ -160,6 +160,7 @@ describe('isCodeFile', () => { 'mvnw', 'build.gradle', 'gradle/wrapper/gradle-wrapper.properties', + 'gradle/wrapper/gradle-wrapper.jar', ] for (const codeFilePath of codeFiles) { assert.strictEqual(isCodeFile(codeFilePath), true) From 8fd89a3d751d9fcf4318c82a102604d8dda37d26 Mon Sep 17 00:00:00 2001 From: Hamed Soleimani Date: Mon, 27 Jan 2025 12:12:12 -0800 Subject: [PATCH 5/6] fix(amazonq): allow including binary files when execution is enabled #6430 ## Problem * Fixes for execution engine ## Solution * Don't drop binary files matching with extra gitignore rules * Don't drop `.gitignore` file from repository. --- .../unit/amazonqFeatureDev/util/files.test.ts | 57 ++++++++++++++----- .../core/src/amazonqFeatureDev/util/files.ts | 11 ++-- packages/core/src/shared/filetypes.ts | 12 +--- .../src/shared/utilities/workspaceUtils.ts | 23 +++++--- .../core/src/test/shared/filetypes.test.ts | 1 - 5 files changed, 67 insertions(+), 37 deletions(-) diff --git a/packages/amazonq/test/unit/amazonqFeatureDev/util/files.test.ts b/packages/amazonq/test/unit/amazonqFeatureDev/util/files.test.ts index e18521de84e..a4e4a7b462c 100644 --- a/packages/amazonq/test/unit/amazonqFeatureDev/util/files.test.ts +++ b/packages/amazonq/test/unit/amazonqFeatureDev/util/files.test.ts @@ -16,23 +16,45 @@ import { MetricName, Span } from 'aws-core-vscode/telemetry' import sinon from 'sinon' import { CodeWhispererSettings } from 'aws-core-vscode/codewhisperer' -const testDevfilePrepareRepo = async (expectedRepoSize: number, devfileEnabled: boolean) => { +import AdmZip from 'adm-zip' + +const testDevfilePrepareRepo = async (devfileEnabled: boolean) => { + const files: Record = { + 'file.md': 'test content', + // only include when execution is enabled + 'devfile.yaml': 'test', + // .git folder is always dropped (because of vscode global exclude rules) + '.git/ref': '####', + // .gitignore should always be included + '.gitignore': 'node_models/*', + // non code files only when dev execution is enabled + 'abc.jar': 'jar-content', + 'data/logo.ico': 'binary-content', + } const folder = await TestFolder.create() - await folder.write('devfile.yaml', 'test') - await folder.write('file.md', 'test content') + + for (const [fileName, content] of Object.entries(files)) { + await folder.write(fileName, content) + } + + const expectedFiles = !devfileEnabled + ? ['./file.md', './.gitignore'] + : ['./devfile.yaml', './file.md', './.gitignore', './abc.jar', 'data/logo.ico'] + const workspace = getWorkspaceFolder(folder.path) sinon .stub(CodeWhispererSettings.instance, 'getDevCommandWorkspaceConfigurations') .returns(devfileEnabled ? { [workspace.uri.fsPath]: true } : {}) - await testPrepareRepoData(workspace, expectedRepoSize) + await testPrepareRepoData(workspace, expectedFiles) } const testPrepareRepoData = async ( workspace: vscode.WorkspaceFolder, - expectedRepoSize: number, + expectedFiles: string[], expectedTelemetryMetrics?: Array<{ metricName: MetricName; value: any }> ) => { + expectedFiles.sort((a, b) => a.localeCompare(b)) const telemetry = new TelemetryHelper() const result = await prepareRepoData([workspace.uri.fsPath], [workspace], telemetry, { record: () => {}, @@ -41,13 +63,18 @@ const testPrepareRepoData = async ( assert.strictEqual(Buffer.isBuffer(result.zipFileBuffer), true) // checksum is not the same across different test executions because some unique random folder names are generated assert.strictEqual(result.zipFileChecksum.length, 44) - assert.strictEqual(telemetry.repositorySize, expectedRepoSize) if (expectedTelemetryMetrics) { - expectedTelemetryMetrics.forEach((metric) => { + for (const metric of expectedTelemetryMetrics) { assertTelemetry(metric.metricName, metric.value) - }) + } } + + // Unzip the buffer and compare the entry names + const zip = new AdmZip(result.zipFileBuffer) + const actualZipEntries = zip.getEntries().map((entry) => entry.entryName) + actualZipEntries.sort((a, b) => a.localeCompare(b)) + assert.deepStrictEqual(actualZipEntries, expectedFiles) } describe('file utils', () => { @@ -62,7 +89,7 @@ describe('file utils', () => { await folder.write('file2.md', 'test content') const workspace = getWorkspaceFolder(folder.path) - await testPrepareRepoData(workspace, 24) + await testPrepareRepoData(workspace, ['./file1.md', './file2.md']) }) it('prepareRepoData ignores denied file extensions', async function () { @@ -70,17 +97,19 @@ describe('file utils', () => { await folder.write('file.mp4', 'test content') const workspace = getWorkspaceFolder(folder.path) - await testPrepareRepoData(workspace, 0, [ - { metricName: 'amazonq_bundleExtensionIgnored', value: { filenameExt: 'mp4', count: 1 } }, - ]) + await testPrepareRepoData( + workspace, + [], + [{ metricName: 'amazonq_bundleExtensionIgnored', value: { filenameExt: 'mp4', count: 1 } }] + ) }) it('should ignore devfile.yaml when setting is disabled', async function () { - await testDevfilePrepareRepo(12, false) + await testDevfilePrepareRepo(false) }) it('should include devfile.yaml when setting is enabled', async function () { - await testDevfilePrepareRepo(16, true) + await testDevfilePrepareRepo(true) }) // Test the logic that allows the customer to modify root source folder diff --git a/packages/core/src/amazonqFeatureDev/util/files.ts b/packages/core/src/amazonqFeatureDev/util/files.ts index 8c74f419493..da355085ecd 100644 --- a/packages/core/src/amazonqFeatureDev/util/files.ts +++ b/packages/core/src/amazonqFeatureDev/util/files.ts @@ -40,9 +40,10 @@ export async function prepareRepoData( zip: AdmZip = new AdmZip() ) { try { - const files = await collectFiles(repoRootPaths, workspaceFolders, true, maxRepoSizeBytes) const devCommandWorkspaceConfigurations = CodeWhispererSettings.instance.getDevCommandWorkspaceConfigurations() const useAutoBuildFeature = devCommandWorkspaceConfigurations[repoRootPaths[0]] ?? false + // We only respect gitignore file rules if useAutoBuildFeature is on, this is to avoid dropping necessary files for building the code (e.g. png files imported in js code) + const files = await collectFiles(repoRootPaths, workspaceFolders, true, maxRepoSizeBytes, !useAutoBuildFeature) let totalBytes = 0 const ignoredExtensionMap = new Map() @@ -59,10 +60,10 @@ export async function prepareRepoData( throw error } const isCodeFile_ = isCodeFile(file.relativeFilePath) - // exclude user's devfile if `useAutoBuildFeature` is set to false - const excludeDevFile = useAutoBuildFeature ? false : file.relativeFilePath === 'devfile.yaml' - - if (fileSize >= maxFileSizeBytes || !isCodeFile_ || excludeDevFile) { + const isDevFile = file.relativeFilePath === 'devfile.yaml' + // When useAutoBuildFeature is on, only respect the gitignore rules filtered earlier and apply the size limit, otherwise, exclude all non code files and gitignore files + const isNonCodeFileAndIgnored = useAutoBuildFeature ? false : !isCodeFile_ || isDevFile + if (fileSize >= maxFileSizeBytes || isNonCodeFileAndIgnored) { if (!isCodeFile_) { const re = /(?:\.([^.]+))?$/ const extensionArray = re.exec(file.relativeFilePath) diff --git a/packages/core/src/shared/filetypes.ts b/packages/core/src/shared/filetypes.ts index 34d6f94f599..843397e6a17 100644 --- a/packages/core/src/shared/filetypes.ts +++ b/packages/core/src/shared/filetypes.ts @@ -233,7 +233,6 @@ export const codefileExtensions = new Set([ '.idl', '.ini', '.io', - '.jar', '.java', '.jl', '.js', @@ -362,17 +361,10 @@ export const codefileExtensions = new Set([ ]) // Code file names without an extension -export const codefileNames = new Set(['Dockerfile', 'Dockerfile.build', 'gradlew', 'mvnw']) - -// Build file names -export const buildfileNames = new Set(['gradle/wrapper/gradle-wrapper.jar']) +export const codefileNames = new Set(['Dockerfile', 'Dockerfile.build', 'gradlew', 'mvnw', '.gitignore']) /** Returns true if `filename` is a code file. */ export function isCodeFile(filename: string): boolean { const ext = path.extname(filename).toLowerCase() - return ( - codefileExtensions.has(ext) || - codefileNames.has(path.basename(filename)) || - buildfileNames.has(path.basename(filename)) - ) + return codefileExtensions.has(ext) || codefileNames.has(path.basename(filename)) } diff --git a/packages/core/src/shared/utilities/workspaceUtils.ts b/packages/core/src/shared/utilities/workspaceUtils.ts index 3894c3b56ff..c650b886016 100644 --- a/packages/core/src/shared/utilities/workspaceUtils.ts +++ b/packages/core/src/shared/utilities/workspaceUtils.ts @@ -268,7 +268,7 @@ export function checkUnsavedChanges(): boolean { return vscode.workspace.textDocuments.some((doc) => doc.isDirty) } -export function getExcludePattern(additionalPatterns: string[] = []) { +export function getExcludePattern(defaultExcludePatterns: boolean = true) { const globAlwaysExcludedDirs = getGlobDirExcludedPatterns().map((pattern) => `**/${pattern}/*`) const extraPatterns = [ '**/package-lock.json', @@ -290,19 +290,24 @@ export function getExcludePattern(additionalPatterns: string[] = []) { '**/License.md', '**/LICENSE.md', ] - const allPatterns = [...globAlwaysExcludedDirs, ...extraPatterns, ...additionalPatterns] + const allPatterns = [...globAlwaysExcludedDirs, ...(defaultExcludePatterns ? extraPatterns : [])] return `{${allPatterns.join(',')}}` } /** * @param rootPath root folder to look for .gitignore files + * @param addExtraPatterns whether to add extra exclude patterns even if not in gitignore * @returns list of glob patterns extracted from .gitignore * These patterns are compatible with vscode exclude patterns */ -async function filterOutGitignoredFiles(rootPath: string, files: vscode.Uri[]): Promise { +async function filterOutGitignoredFiles( + rootPath: string, + files: vscode.Uri[], + defaultExcludePatterns: boolean = true +): Promise { const gitIgnoreFiles = await vscode.workspace.findFiles( new vscode.RelativePattern(rootPath, '**/.gitignore'), - getExcludePattern() + getExcludePattern(defaultExcludePatterns) ) const gitIgnoreFilter = await GitIgnoreFilter.build(gitIgnoreFiles) return gitIgnoreFilter.filterFiles(files) @@ -313,13 +318,15 @@ async function filterOutGitignoredFiles(rootPath: string, files: vscode.Uri[]): * @param sourcePaths the paths where collection starts * @param workspaceFolders the current workspace folders opened * @param respectGitIgnore whether to respect gitignore file + * @param addExtraIgnorePatterns whether to add extra exclude patterns even if not in gitignore * @returns all matched files */ export async function collectFiles( sourcePaths: string[], workspaceFolders: CurrentWsFolders, respectGitIgnore: boolean = true, - maxSize = 200 * 1024 * 1024 // 200 MB + maxSize = 200 * 1024 * 1024, // 200 MB + defaultExcludePatterns: boolean = true ): Promise< { workspaceFolder: vscode.WorkspaceFolder @@ -356,10 +363,12 @@ export async function collectFiles( for (const rootPath of sourcePaths) { const allFiles = await vscode.workspace.findFiles( new vscode.RelativePattern(rootPath, '**'), - getExcludePattern() + getExcludePattern(defaultExcludePatterns) ) - const files = respectGitIgnore ? await filterOutGitignoredFiles(rootPath, allFiles) : allFiles + const files = respectGitIgnore + ? await filterOutGitignoredFiles(rootPath, allFiles, defaultExcludePatterns) + : allFiles for (const file of files) { const relativePath = getWorkspaceRelativePath(file.fsPath, { workspaceFolders }) diff --git a/packages/core/src/test/shared/filetypes.test.ts b/packages/core/src/test/shared/filetypes.test.ts index 2ae6e82ff8c..26e12631419 100644 --- a/packages/core/src/test/shared/filetypes.test.ts +++ b/packages/core/src/test/shared/filetypes.test.ts @@ -160,7 +160,6 @@ describe('isCodeFile', () => { 'mvnw', 'build.gradle', 'gradle/wrapper/gradle-wrapper.properties', - 'gradle/wrapper/gradle-wrapper.jar', ] for (const codeFilePath of codeFiles) { assert.strictEqual(isCodeFile(codeFilePath), true) From e06906fd9de686c7d1087050ac46b81c49edc927 Mon Sep 17 00:00:00 2001 From: Neil Kulkarni <60868290+neilk-aws@users.noreply.github.com> Date: Tue, 28 Jan 2025 13:13:33 -0800 Subject: [PATCH 6/6] docs(amazonq): Update /dev auto build setting strings (#6444) ## Problem "Dev Command Workspace Configurations" is overly broad and too ambiguous with regards to what this setting enables. ## Solution Worked with tech writer to establish a different string to present to customers. --- - Treat all work as PUBLIC. Private `feature/x` branches will not be squash-merged at release time. - Your code changes must meet the guidelines in [CONTRIBUTING.md](https://github.com/aws/aws-toolkit-vscode/blob/master/CONTRIBUTING.md#guidelines). - License: I confirm that my contribution is made under the terms of the Apache 2.0 license. --- packages/amazonq/package.json | 4 ++-- .../test/unit/amazonqFeatureDev/util/files.test.ts | 2 +- packages/core/package.nls.json | 2 +- .../amazonqFeatureDev/controllers/chat/controller.ts | 10 +++++----- packages/core/src/amazonqFeatureDev/util/files.ts | 4 ++-- .../src/codewhisperer/util/codewhispererSettings.ts | 12 ++++++------ packages/core/src/shared/clients/s3Client.ts | 4 ++-- packages/core/src/shared/settings-amazonq.gen.ts | 2 +- 8 files changed, 20 insertions(+), 20 deletions(-) diff --git a/packages/amazonq/package.json b/packages/amazonq/package.json index 4061ca02a4b..31b516ec37d 100644 --- a/packages/amazonq/package.json +++ b/packages/amazonq/package.json @@ -127,8 +127,8 @@ "markdownDescription": "%AWS.configuration.description.amazonq%", "default": true }, - "amazonQ.devCommandWorkspaceConfigurations": { - "markdownDescription": "%AWS.configuration.description.devCommandWorkspaceConfigurations%", + "amazonQ.allowFeatureDevelopmentToRunCodeAndTests": { + "markdownDescription": "%AWS.configuration.description.featureDevelopment.allowRunningCodeAndTests%", "type": "object", "default": {} }, diff --git a/packages/amazonq/test/unit/amazonqFeatureDev/util/files.test.ts b/packages/amazonq/test/unit/amazonqFeatureDev/util/files.test.ts index a4e4a7b462c..0bf7df006b2 100644 --- a/packages/amazonq/test/unit/amazonqFeatureDev/util/files.test.ts +++ b/packages/amazonq/test/unit/amazonqFeatureDev/util/files.test.ts @@ -43,7 +43,7 @@ const testDevfilePrepareRepo = async (devfileEnabled: boolean) => { const workspace = getWorkspaceFolder(folder.path) sinon - .stub(CodeWhispererSettings.instance, 'getDevCommandWorkspaceConfigurations') + .stub(CodeWhispererSettings.instance, 'getAutoBuildSetting') .returns(devfileEnabled ? { [workspace.uri.fsPath]: true } : {}) await testPrepareRepoData(workspace, expectedFiles) diff --git a/packages/core/package.nls.json b/packages/core/package.nls.json index 2b15a28eb58..4c0f70231ff 100644 --- a/packages/core/package.nls.json +++ b/packages/core/package.nls.json @@ -20,7 +20,7 @@ "AWS.configuration.description.suppressPrompts": "Prompts which ask for confirmation. Checking an item suppresses the prompt.", "AWS.configuration.enableCodeLenses": "Enable SAM hints in source code and template.yaml files", "AWS.configuration.description.resources.enabledResources": "AWS resources to display in the 'Resources' portion of the explorer.", - "AWS.configuration.description.devCommandWorkspaceConfigurations": "Amazon Q: Allow Q /dev to run code and test commands", + "AWS.configuration.description.featureDevelopment.allowRunningCodeAndTests": "Allow /dev to run code and test commands", "AWS.configuration.description.experiments": "Try experimental features and give feedback. Note that experimental features may be removed at any time.\n * `jsonResourceModification` - Enables basic create, update, and delete support for cloud resources via the JSON Resources explorer component.\n * `ec2RemoteConnect` - Allows interfacing with EC2 instances with options to start, stop, and establish remote connections. Remote connections are done over SSM and can be through a terminal or a remote VSCode window.", "AWS.stepFunctions.asl.format.enable.desc": "Enables the default formatter used with Amazon States Language files", "AWS.stepFunctions.asl.maxItemsComputed.desc": "The maximum number of outline symbols and folding regions computed (limited for performance reasons).", diff --git a/packages/core/src/amazonqFeatureDev/controllers/chat/controller.ts b/packages/core/src/amazonqFeatureDev/controllers/chat/controller.ts index 8d3943d87ad..6ce82a1a0ab 100644 --- a/packages/core/src/amazonqFeatureDev/controllers/chat/controller.ts +++ b/packages/core/src/amazonqFeatureDev/controllers/chat/controller.ts @@ -157,9 +157,9 @@ export class FeatureDevController { this.sendFeedback() break case FollowUpTypes.AcceptAutoBuild: - return this.processDevCommandWorkspaceSetting(true, data) + return this.processAutoBuildSetting(true, data) case FollowUpTypes.DenyAutoBuild: - return this.processDevCommandWorkspaceSetting(false, data) + return this.processAutoBuildSetting(false, data) case FollowUpTypes.GenerateDevFile: this.messenger.sendAnswer({ type: 'system-prompt', @@ -393,7 +393,7 @@ export class FeatureDevController { } const root = session.getWorkspaceRoot() - const autoBuildProjectSetting = CodeWhispererSettings.instance.getDevCommandWorkspaceConfigurations() + const autoBuildProjectSetting = CodeWhispererSettings.instance.getAutoBuildSetting() const hasDevfile = await checkForDevFile(root) const isPromptedForAutoBuildFeature = Object.keys(autoBuildProjectSetting).includes(root) @@ -676,9 +676,9 @@ export class FeatureDevController { this.messenger.sendUpdatePlaceholder(tabID, i18n('AWS.amazonq.featureDev.placeholder.additionalImprovements')) } - private async processDevCommandWorkspaceSetting(setting: boolean, msg: any) { + private async processAutoBuildSetting(setting: boolean, msg: any) { const root = (await this.sessionStorage.getSession(msg.tabID)).getWorkspaceRoot() - await CodeWhispererSettings.instance.updateDevCommandWorkspaceConfigurations(root, setting) + await CodeWhispererSettings.instance.updateAutoBuildSetting(root, setting) this.messenger.sendAnswer({ message: i18n('AWS.amazonq.featureDev.answer.settingUpdated'), diff --git a/packages/core/src/amazonqFeatureDev/util/files.ts b/packages/core/src/amazonqFeatureDev/util/files.ts index da355085ecd..7e93cf0f9ce 100644 --- a/packages/core/src/amazonqFeatureDev/util/files.ts +++ b/packages/core/src/amazonqFeatureDev/util/files.ts @@ -40,8 +40,8 @@ export async function prepareRepoData( zip: AdmZip = new AdmZip() ) { try { - const devCommandWorkspaceConfigurations = CodeWhispererSettings.instance.getDevCommandWorkspaceConfigurations() - const useAutoBuildFeature = devCommandWorkspaceConfigurations[repoRootPaths[0]] ?? false + const autoBuildSetting = CodeWhispererSettings.instance.getAutoBuildSetting() + const useAutoBuildFeature = autoBuildSetting[repoRootPaths[0]] ?? false // We only respect gitignore file rules if useAutoBuildFeature is on, this is to avoid dropping necessary files for building the code (e.g. png files imported in js code) const files = await collectFiles(repoRootPaths, workspaceFolders, true, maxRepoSizeBytes, !useAutoBuildFeature) diff --git a/packages/core/src/codewhisperer/util/codewhispererSettings.ts b/packages/core/src/codewhisperer/util/codewhispererSettings.ts index 14fc7ef77d2..8a52627aacb 100644 --- a/packages/core/src/codewhisperer/util/codewhispererSettings.ts +++ b/packages/core/src/codewhisperer/util/codewhispererSettings.ts @@ -13,7 +13,7 @@ const description = { workspaceIndexWorkerThreads: Number, workspaceIndexUseGPU: Boolean, workspaceIndexMaxSize: Number, - devCommandWorkspaceConfigurations: Object, + allowFeatureDevelopmentToRunCodeAndTests: Object, ignoredSecurityIssues: ArrayConstructor(String), } @@ -51,16 +51,16 @@ export class CodeWhispererSettings extends fromExtensionManifest('amazonQ', desc return Math.max(this.get('workspaceIndexMaxSize', 250), 1) } - public getDevCommandWorkspaceConfigurations(): { [key: string]: boolean } { - return this.get('devCommandWorkspaceConfigurations', {}) + public getAutoBuildSetting(): { [key: string]: boolean } { + return this.get('allowFeatureDevelopmentToRunCodeAndTests', {}) } - public async updateDevCommandWorkspaceConfigurations(projectName: string, setting: boolean) { - const projects = this.getDevCommandWorkspaceConfigurations() + public async updateAutoBuildSetting(projectName: string, setting: boolean) { + const projects = this.getAutoBuildSetting() projects[projectName] = setting - await this.update('devCommandWorkspaceConfigurations', projects) + await this.update('allowFeatureDevelopmentToRunCodeAndTests', projects) } public getIgnoredSecurityIssues(): string[] { diff --git a/packages/core/src/shared/clients/s3Client.ts b/packages/core/src/shared/clients/s3Client.ts index ae530a68fa3..950360caec5 100644 --- a/packages/core/src/shared/clients/s3Client.ts +++ b/packages/core/src/shared/clients/s3Client.ts @@ -463,9 +463,9 @@ export class DefaultS3Client { MaxKeys: request.maxResults ?? DEFAULT_MAX_KEYS, /** * Set '' as the default prefix to ensure that the bucket's content will be displayed - * when the user has at least list access to the root of the bucket. + * when the user has at least list access to the root of the bucket * https://github.com/aws/aws-toolkit-vscode/issues/4643 - * @default '' + * @default '' */ Prefix: request.folderPath ?? defaultPrefix, ContinuationToken: request.continuationToken, diff --git a/packages/core/src/shared/settings-amazonq.gen.ts b/packages/core/src/shared/settings-amazonq.gen.ts index 0fc9b44d11a..38d575f6617 100644 --- a/packages/core/src/shared/settings-amazonq.gen.ts +++ b/packages/core/src/shared/settings-amazonq.gen.ts @@ -21,7 +21,7 @@ export const amazonqSettings = { "ssoCacheError": {} }, "amazonQ.showInlineCodeSuggestionsWithCodeReferences": {}, - "amazonQ.devCommandWorkspaceConfigurations": {}, + "amazonQ.allowFeatureDevelopmentToRunCodeAndTests": {}, "amazonQ.importRecommendationForInlineCodeSuggestions": {}, "amazonQ.shareContentWithAWS": {}, "amazonQ.workspaceIndex": {},