Skip to content

Commit 3faa157

Browse files
committed
feat(feature dev): Add setting to allow Q to run code and test commands
1 parent 56afdf8 commit 3faa157

File tree

12 files changed

+181
-29
lines changed

12 files changed

+181
-29
lines changed
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
{
2+
"type": "Feature",
3+
"description": "Add setting to allow Q /dev to run code and test commands"
4+
}

packages/amazonq/package.json

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -127,6 +127,11 @@
127127
"markdownDescription": "%AWS.configuration.description.amazonq%",
128128
"default": true
129129
},
130+
"amazonQ.devCommandWorkspaceConfigurations": {
131+
"markdownDescription": "%AWS.configuration.description.devCommandWorkspaceConfigurations%",
132+
"type": "object",
133+
"default": {}
134+
},
130135
"amazonQ.importRecommendationForInlineCodeSuggestions": {
131136
"type": "boolean",
132137
"description": "%AWS.configuration.description.amazonq.importRecommendation%",

packages/core/package.nls.json

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
"AWS.configuration.enableCodeLenses": "Enable SAM hints in source code and template.yaml files",
2222
"AWS.configuration.description.resources.enabledResources": "AWS resources to display in the 'Resources' portion of the explorer.",
2323
"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.",
24+
"AWS.configuration.description.devCommandWorkspaceConfigurations": "Amazon Q: Allow Q /dev to run code and test commands",
2425
"AWS.stepFunctions.asl.format.enable.desc": "Enables the default formatter used with Amazon States Language files",
2526
"AWS.stepFunctions.asl.maxItemsComputed.desc": "The maximum number of outline symbols and folding regions computed (limited for performance reasons).",
2627
"AWS.configuration.description.awssam.debug.api": "API Gateway configuration",
@@ -322,12 +323,18 @@
322323
"AWS.amazonq.featureDev.pillText.selectOption": "Choose an option to proceed",
323324
"AWS.amazonq.featureDev.pillText.unableGenerateChanges": "Unable to generate any file changes",
324325
"AWS.amazonq.featureDev.pillText.provideFeedback": "Provide feedback & regenerate",
326+
"AWS.amazonq.featureDev.pillText.generateDevFile": "Generate devfile to build code",
327+
"AWS.amazonq.featureDev.pillText.acceptForProject": "Yes, use my devfile for this project",
328+
"AWS.amazonq.featureDev.pillText.declineForProject": "No, thanks",
325329
"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.",
326330
"AWS.amazonq.featureDev.answer.qGeneratedCode": "The Amazon Q Developer Agent for software development has generated code for you to review",
327331
"AWS.amazonq.featureDev.answer.howCodeCanBeImproved": "How can I improve the code for your use case?",
328332
"AWS.amazonq.featureDev.answer.updateCode": "Okay, I updated your code files. Would you like to work on another task?",
329333
"AWS.amazonq.featureDev.answer.sessionClosed": "Okay, I've ended this chat session. You can open a new tab to chat or start another workflow.",
330334
"AWS.amazonq.featureDev.answer.newTaskChanges": "What new task would you like to work on?",
335+
"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**.",
336+
"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**.",
337+
"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 <a href=\"https://docs.aws.amazon.com/amazonq/latest/qdeveloper-ug/software-dev.html\" target=\"_blank\">Amazon Q Developer documentation</a>.",
331338
"AWS.amazonq.featureDev.placeholder.chatInputDisabled": "Chat input is disabled",
332339
"AWS.amazonq.featureDev.placeholder.additionalImprovements": "Describe your task or issue in detail",
333340
"AWS.amazonq.featureDev.placeholder.feedback": "Provide feedback or comments",

packages/core/src/amazonqFeatureDev/constants.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,9 @@ export const featureDevChat = 'featureDevChat'
1414

1515
export const featureName = 'Amazon Q Developer Agent for software development'
1616

17+
export const generateDevFilePrompt =
18+
"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”"
19+
1720
// Max allowed size for file collection
1821
export const maxRepoSizeBytes = 200 * 1024 * 1024
1922

packages/core/src/amazonqFeatureDev/controllers/chat/controller.ts

Lines changed: 108 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ import {
2929
} from '../../errors'
3030
import { codeGenRetryLimit, defaultRetryLimit } from '../../limits'
3131
import { Session } from '../../session/session'
32-
import { featureName } from '../../constants'
32+
import { featureName, generateDevFilePrompt } from '../../constants'
3333
import { ChatSessionStorage } from '../../storages/chatSession'
3434
import { DevPhase, FollowUpTypes, SessionStatePhase } from '../../types'
3535
import { Messenger } from './messenger/messenger'
@@ -40,12 +40,13 @@ import { submitFeedback } from '../../../feedback/vue/submitFeedback'
4040
import { placeholder } from '../../../shared/vscode/commands2'
4141
import { EditorContentController } from '../../../amazonq/commons/controllers/contentController'
4242
import { openUrl } from '../../../shared/utilities/vsCodeUtils'
43-
import { getPathsFromZipFilePath } from '../../util/files'
43+
import { checkForDevFile, getPathsFromZipFilePath } from '../../util/files'
4444
import { examples, messageWithConversationId } from '../../userFacingText'
4545
import { getWorkspaceFoldersByPrefixes } from '../../../shared/utilities/workspaceUtils'
4646
import { openDeletedDiff, openDiff } from '../../../amazonq/commons/diff'
4747
import { i18n } from '../../../shared/i18n-helper'
4848
import globals from '../../../shared/extensionGlobals'
49+
import { CodeWhispererSettings } from '../../../codewhisperer'
4950

5051
export const TotalSteps = 3
5152

@@ -147,6 +148,17 @@ export class FeatureDevController {
147148
case FollowUpTypes.SendFeedback:
148149
this.sendFeedback()
149150
break
151+
case FollowUpTypes.AcceptAutoBuild:
152+
return this.processDevCommandWorkspaceSetting(true, data)
153+
case FollowUpTypes.DenyAutoBuild:
154+
return this.processDevCommandWorkspaceSetting(false, data)
155+
case FollowUpTypes.GenerateDevFile:
156+
this.messenger.sendAnswer({
157+
type: 'system-prompt',
158+
tabID: data?.tabID,
159+
message: i18n('AWS.amazonq.featureDev.pillText.generateDevFile'),
160+
})
161+
return this.newTask(data, generateDevFilePrompt)
150162
}
151163
})
152164
this.chatControllerMessageListeners.openDiff.event((data) => {
@@ -361,6 +373,16 @@ export class FeatureDevController {
361373
return
362374
}
363375

376+
const root = session.getWorkspaceRoot()
377+
const autoBuildProjectSetting = CodeWhispererSettings.instance.getDevCommandWorkspaceConfigurations()
378+
const hasDevfile = await checkForDevFile(root)
379+
const isPromptedForAutoBuildFeature = Object.keys(autoBuildProjectSetting).includes(root)
380+
381+
if (hasDevfile && !isPromptedForAutoBuildFeature) {
382+
await this.promptAllowQCommandsConsent(message.tabID)
383+
return
384+
}
385+
364386
await session.preloader(message.message)
365387

366388
if (session.state.phase === DevPhase.CODEGEN) {
@@ -374,6 +396,32 @@ export class FeatureDevController {
374396
}
375397
}
376398

399+
private async promptAllowQCommandsConsent(tabID: string) {
400+
this.messenger.sendAnswer({
401+
tabID: tabID,
402+
message: i18n('AWS.amazonq.featureDev.answer.devFileInRepository'),
403+
type: 'answer',
404+
})
405+
406+
this.messenger.sendAnswer({
407+
message: undefined,
408+
type: 'system-prompt',
409+
followUps: [
410+
{
411+
pillText: i18n('AWS.amazonq.featureDev.pillText.acceptForProject'),
412+
type: FollowUpTypes.AcceptAutoBuild,
413+
status: 'success',
414+
},
415+
{
416+
pillText: i18n('AWS.amazonq.featureDev.pillText.declineForProject'),
417+
type: FollowUpTypes.DenyAutoBuild,
418+
status: 'error',
419+
},
420+
],
421+
tabID: tabID,
422+
})
423+
}
424+
377425
/**
378426
* Handle a regular incoming message when a user is in the code generation phase
379427
*/
@@ -447,8 +495,8 @@ export class FeatureDevController {
447495
tabID: tabID,
448496
message:
449497
remainingIterations === 0
450-
? 'Would you like me to add this code to your project?'
451-
: `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.`,
498+
? 'Would you like to add this code to your project?'
499+
: `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.`,
452500
})
453501
}
454502

@@ -463,7 +511,7 @@ export class FeatureDevController {
463511
// Finish processing the event
464512

465513
if (session?.state?.tokenSource?.token.isCancellationRequested) {
466-
this.workOnNewTask(
514+
await this.workOnNewTask(
467515
session,
468516
session.state.codeGenerationRemainingIterationCount ||
469517
TotalSteps - (session.state?.currentIteration || 0),
@@ -491,12 +539,16 @@ export class FeatureDevController {
491539
}
492540
}
493541
}
494-
private workOnNewTask(
542+
private async workOnNewTask(
495543
message: any,
496544
remainingIterations: number = 0,
497545
totalIterations?: number,
498546
isStoppedGeneration: boolean = false
499547
) {
548+
const hasDevFile = await checkForDevFile(
549+
(await this.sessionStorage.getSession(message.tabID)).getWorkspaceRoot()
550+
)
551+
500552
if (isStoppedGeneration) {
501553
this.messenger.sendAnswer({
502554
message:
@@ -509,21 +561,37 @@ export class FeatureDevController {
509561
}
510562

511563
if ((remainingIterations <= 0 && isStoppedGeneration) || !isStoppedGeneration) {
564+
const followUps: Array<ChatItemAction> = [
565+
{
566+
pillText: i18n('AWS.amazonq.featureDev.pillText.newTask'),
567+
type: FollowUpTypes.NewTask,
568+
status: 'info',
569+
},
570+
{
571+
pillText: i18n('AWS.amazonq.featureDev.pillText.closeSession'),
572+
type: FollowUpTypes.CloseSession,
573+
status: 'info',
574+
},
575+
]
576+
577+
if (!hasDevFile) {
578+
followUps.push({
579+
pillText: i18n('AWS.amazonq.featureDev.pillText.generateDevFile'),
580+
type: FollowUpTypes.GenerateDevFile,
581+
status: 'info',
582+
})
583+
584+
this.messenger.sendAnswer({
585+
type: 'answer',
586+
tabID: message.tabID,
587+
message: i18n('AWS.amazonq.featureDev.answer.devFileSuggestion'),
588+
})
589+
}
590+
512591
this.messenger.sendAnswer({
513592
type: 'system-prompt',
514593
tabID: message.tabID,
515-
followUps: [
516-
{
517-
pillText: i18n('AWS.amazonq.featureDev.pillText.newTask'),
518-
type: FollowUpTypes.NewTask,
519-
status: 'info',
520-
},
521-
{
522-
pillText: i18n('AWS.amazonq.featureDev.pillText.closeSession'),
523-
type: FollowUpTypes.CloseSession,
524-
status: 'info',
525-
},
526-
],
594+
followUps,
527595
})
528596
this.messenger.sendChatInputEnabled(message.tabID, false)
529597
this.messenger.sendUpdatePlaceholder(message.tabID, i18n('AWS.amazonq.featureDev.pillText.selectOption'))
@@ -537,6 +605,20 @@ export class FeatureDevController {
537605
i18n('AWS.amazonq.featureDev.placeholder.additionalImprovements')
538606
)
539607
}
608+
609+
private async processDevCommandWorkspaceSetting(setting: boolean, msg: any) {
610+
const root = (await this.sessionStorage.getSession(msg.tabID)).getWorkspaceRoot()
611+
await CodeWhispererSettings.instance.updateDevCommandWorkspaceConfigurations(root, setting)
612+
613+
this.messenger.sendAnswer({
614+
message: i18n('AWS.amazonq.featureDev.answer.settingUpdated'),
615+
tabID: msg.tabID,
616+
type: 'answer',
617+
})
618+
619+
await this.retryRequest(msg)
620+
}
621+
540622
// TODO add type
541623
private async insertCode(message: any) {
542624
let session
@@ -563,7 +645,7 @@ export class FeatureDevController {
563645
canBeVoted: true,
564646
})
565647

566-
this.workOnNewTask(
648+
await this.workOnNewTask(
567649
message,
568650
session.state.codeGenerationRemainingIterationCount,
569651
session.state.codeGenerationTotalIterationCount
@@ -854,7 +936,7 @@ export class FeatureDevController {
854936
this.sessionStorage.deleteSession(message.tabID)
855937
}
856938

857-
private async newTask(message: any) {
939+
private async newTask(message: any, prefilledPrompt?: string) {
858940
// Old session for the tab is ending, delete it so we can create a new one for the message id
859941
const session = await this.sessionStorage.getSession(message.tabID)
860942
telemetry.amazonq_endChat.emit({
@@ -867,8 +949,12 @@ export class FeatureDevController {
867949
// Re-run the opening flow, where we check auth + create a session
868950
await this.tabOpened(message)
869951

870-
this.messenger.sendChatInputEnabled(message.tabID, true)
871-
this.messenger.sendUpdatePlaceholder(message.tabID, i18n('AWS.amazonq.featureDev.placeholder.describe'))
952+
if (prefilledPrompt) {
953+
await this.processUserChatMessage({ ...message, message: prefilledPrompt })
954+
} else {
955+
this.messenger.sendChatInputEnabled(message.tabID, true)
956+
this.messenger.sendUpdatePlaceholder(message.tabID, i18n('AWS.amazonq.featureDev.placeholder.describe'))
957+
}
872958
}
873959

874960
private async closeSession(message: any) {

packages/core/src/amazonqFeatureDev/session/session.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,10 @@ export class Session {
103103
this._state && this._state.updateWorkspaceRoot && this._state.updateWorkspaceRoot(workspaceRootFolder)
104104
}
105105

106+
getWorkspaceRoot(): string {
107+
return this.config.workspaceRoots[0]
108+
}
109+
106110
private getSessionStateConfig(): Omit<SessionStateConfig, 'uploadId'> {
107111
return {
108112
workspaceRoots: this.config.workspaceRoots,

packages/core/src/amazonqFeatureDev/types.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,9 @@ export enum FollowUpTypes {
4949
NewTask = 'NewTask',
5050
CloseSession = 'CloseSession',
5151
SendFeedback = 'SendFeedback',
52+
AcceptAutoBuild = 'AcceptAutoBuild',
53+
DenyAutoBuild = 'DenyAutoBuild',
54+
GenerateDevFile = 'GenerateDevFile',
5255
}
5356

5457
export type SessionStatePhase = DevPhase.INIT | DevPhase.CODEGEN

packages/core/src/amazonqFeatureDev/util/files.ts

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,9 +19,16 @@ import { TelemetryHelper } from './telemetryHelper'
1919
import { maxRepoSizeBytes } from '../constants'
2020
import { isCodeFile } from '../../shared/filetypes'
2121
import { fs } from '../../shared'
22+
import { CodeWhispererSettings } from '../../codewhisperer'
2223

2324
const getSha256 = (file: Buffer) => createHash('sha256').update(file).digest('base64')
2425

26+
export async function checkForDevFile(root: string) {
27+
const devFilePath = root + '/devfile.yaml'
28+
const hasDevFile = await fs.existsFile(devFilePath)
29+
return hasDevFile
30+
}
31+
2532
/**
2633
* given the root path of the repo it zips its files in memory and generates a checksum for it.
2734
*/
@@ -34,15 +41,19 @@ export async function prepareRepoData(
3441
) {
3542
try {
3643
const files = await collectFiles(repoRootPaths, workspaceFolders, true, maxRepoSizeBytes)
44+
const devCommandWorkspaceConfigurations = CodeWhispererSettings.instance.getDevCommandWorkspaceConfigurations()
45+
const useAutoBuildFeature = devCommandWorkspaceConfigurations[repoRootPaths[0]] ?? false
3746

3847
let totalBytes = 0
3948
const ignoredExtensionMap = new Map<string, number>()
4049

4150
for (const file of files) {
4251
const fileSize = (await fs.stat(file.fileUri)).size
4352
const isCodeFile_ = isCodeFile(file.relativeFilePath)
53+
// exclude user's devfile if `useAutoBuildFeature` is set to false
54+
const excludeDevFile = useAutoBuildFeature ? false : file.relativeFilePath === 'devfile.yaml'
4455

45-
if (fileSize >= maxFileSizeBytes || !isCodeFile_) {
56+
if (fileSize >= maxFileSizeBytes || !isCodeFile_ || excludeDevFile) {
4657
if (!isCodeFile_) {
4758
const re = /(?:\.([^.]+))?$/
4859
const extensionArray = re.exec(file.relativeFilePath)

packages/core/src/codewhisperer/util/codewhispererSettings.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ const description = {
1212
workspaceIndexWorkerThreads: Number,
1313
workspaceIndexUseGPU: Boolean,
1414
workspaceIndexMaxSize: Number,
15+
devCommandWorkspaceConfigurations: Object,
1516
}
1617

1718
export class CodeWhispererSettings extends fromExtensionManifest('amazonQ', description) {
@@ -64,6 +65,18 @@ export class CodeWhispererSettings extends fromExtensionManifest('amazonQ', desc
6465
return Math.max(this.get('workspaceIndexMaxSize', 250), 1)
6566
}
6667

68+
public getDevCommandWorkspaceConfigurations(): { [key: string]: boolean } {
69+
return this.get('devCommandWorkspaceConfigurations', {})
70+
}
71+
72+
public async updateDevCommandWorkspaceConfigurations(projectName: string, setting: boolean) {
73+
const projects = this.getDevCommandWorkspaceConfigurations()
74+
75+
projects[projectName] = setting
76+
77+
await this.update('devCommandWorkspaceConfigurations', projects)
78+
}
79+
6780
static #instance: CodeWhispererSettings
6881

6982
public static get instance() {

0 commit comments

Comments
 (0)