Skip to content

Commit 9c5a9d8

Browse files
committed
feat(feature dev): Add setting to allow Q to run code and test commands
1 parent 1f9b8a4 commit 9c5a9d8

File tree

11 files changed

+184
-31
lines changed

11 files changed

+184
-31
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: 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: 75 additions & 24 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,12 @@ export class FeatureDevController {
147148
case FollowUpTypes.SendFeedback:
148149
this.sendFeedback()
149150
break
151+
case FollowUpTypes.AcceptAutoBuild:
152+
return this.processAutoBuildSetting(true, data)
153+
case FollowUpTypes.DenyAutoBuild:
154+
return this.processAutoBuildSetting(false, data)
155+
case FollowUpTypes.GenerateDevFile:
156+
return this.newTask(data, generateDevFilePrompt)
150157
}
151158
})
152159
this.chatControllerMessageListeners.openDiff.event((data) => {
@@ -361,9 +368,9 @@ export class FeatureDevController {
361368
return
362369
}
363370

364-
await session.preloader(message.message)
371+
const isPreloaderFinished = await session.preloader(message.message)
365372

366-
if (session.state.phase === DevPhase.CODEGEN) {
373+
if (isPreloaderFinished && session.state.phase === DevPhase.CODEGEN) {
367374
await this.onCodeGeneration(session, message.message, message.tabID)
368375
}
369376
} catch (err: any) {
@@ -447,8 +454,8 @@ export class FeatureDevController {
447454
tabID: tabID,
448455
message:
449456
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.`,
457+
? 'Would you like to add this code to your project?'
458+
: `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.`,
452459
})
453460
}
454461

@@ -463,7 +470,7 @@ export class FeatureDevController {
463470
// Finish processing the event
464471

465472
if (session?.state?.tokenSource?.token.isCancellationRequested) {
466-
this.workOnNewTask(
473+
await this.workOnNewTask(
467474
session,
468475
session.state.codeGenerationRemainingIterationCount ||
469476
TotalSteps - (session.state?.currentIteration || 0),
@@ -491,12 +498,16 @@ export class FeatureDevController {
491498
}
492499
}
493500
}
494-
private workOnNewTask(
501+
private async workOnNewTask(
495502
message: any,
496503
remainingIterations: number = 0,
497504
totalIterations?: number,
498505
isStoppedGeneration: boolean = false
499506
) {
507+
const hasDevFile = await checkForDevFile(
508+
(await this.sessionStorage.getSession(message.tabID)).getWorkspaceRoot()
509+
)
510+
500511
if (isStoppedGeneration) {
501512
this.messenger.sendAnswer({
502513
message:
@@ -509,21 +520,37 @@ export class FeatureDevController {
509520
}
510521

511522
if ((remainingIterations <= 0 && isStoppedGeneration) || !isStoppedGeneration) {
523+
const followUps: Array<ChatItemAction> = [
524+
{
525+
pillText: i18n('AWS.amazonq.featureDev.pillText.newTask'),
526+
type: FollowUpTypes.NewTask,
527+
status: 'info',
528+
},
529+
{
530+
pillText: i18n('AWS.amazonq.featureDev.pillText.closeSession'),
531+
type: FollowUpTypes.CloseSession,
532+
status: 'info',
533+
},
534+
]
535+
536+
if (!hasDevFile) {
537+
followUps.push({
538+
pillText: i18n('AWS.amazonq.featureDev.pillText.generateDevFile'),
539+
type: FollowUpTypes.GenerateDevFile,
540+
status: 'info',
541+
})
542+
543+
this.messenger.sendAnswer({
544+
type: 'answer',
545+
tabID: message.tabID,
546+
message: i18n('AWS.amazonq.featureDev.answer.devFileSuggestion'),
547+
})
548+
}
549+
512550
this.messenger.sendAnswer({
513551
type: 'system-prompt',
514552
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-
],
553+
followUps,
527554
})
528555
this.messenger.sendChatInputEnabled(message.tabID, false)
529556
this.messenger.sendUpdatePlaceholder(message.tabID, i18n('AWS.amazonq.featureDev.pillText.selectOption'))
@@ -537,6 +564,20 @@ export class FeatureDevController {
537564
i18n('AWS.amazonq.featureDev.placeholder.additionalImprovements')
538565
)
539566
}
567+
568+
private async processAutoBuildSetting(setting: boolean, msg: any) {
569+
const root = (await this.sessionStorage.getSession(msg.tabID)).getWorkspaceRoot()
570+
await CodeWhispererSettings.instance.updateDevCommandWorkspaceConfigurations(root, setting)
571+
572+
this.messenger.sendAnswer({
573+
message: i18n('AWS.amazonq.featureDev.answer.settingUpdated'),
574+
tabID: msg.tabID,
575+
type: 'answer',
576+
})
577+
578+
await this.retryRequest(msg)
579+
}
580+
540581
// TODO add type
541582
private async insertCode(message: any) {
542583
let session
@@ -563,7 +604,7 @@ export class FeatureDevController {
563604
canBeVoted: true,
564605
})
565606

566-
this.workOnNewTask(
607+
await this.workOnNewTask(
567608
message,
568609
session.state.codeGenerationRemainingIterationCount,
569610
session.state.codeGenerationTotalIterationCount
@@ -854,7 +895,7 @@ export class FeatureDevController {
854895
this.sessionStorage.deleteSession(message.tabID)
855896
}
856897

857-
private async newTask(message: any) {
898+
private async newTask(message: any, prefilledPrompt?: string) {
858899
// Old session for the tab is ending, delete it so we can create a new one for the message id
859900
const session = await this.sessionStorage.getSession(message.tabID)
860901
telemetry.amazonq_endChat.emit({
@@ -867,8 +908,18 @@ export class FeatureDevController {
867908
// Re-run the opening flow, where we check auth + create a session
868909
await this.tabOpened(message)
869910

870-
this.messenger.sendChatInputEnabled(message.tabID, true)
871-
this.messenger.sendUpdatePlaceholder(message.tabID, i18n('AWS.amazonq.featureDev.placeholder.describe'))
911+
if (prefilledPrompt) {
912+
this.messenger.sendAnswer({
913+
type: 'system-prompt',
914+
tabID: message.tabID,
915+
message: i18n('AWS.amazonq.featureDev.pillText.generateDevFile'),
916+
})
917+
918+
await this.processUserChatMessage({ ...message, message: prefilledPrompt })
919+
} else {
920+
this.messenger.sendChatInputEnabled(message.tabID, true)
921+
this.messenger.sendUpdatePlaceholder(message.tabID, i18n('AWS.amazonq.featureDev.placeholder.describe'))
922+
}
872923
}
873924

874925
private async closeSession(message: any) {

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

Lines changed: 51 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import * as path from 'path'
77

88
import { ConversationNotStartedState, PrepareCodeGenState } from './sessionState'
99
import {
10+
FollowUpTypes,
1011
type DeletedFileInfo,
1112
type Interaction,
1213
type NewFileInfo,
@@ -26,6 +27,10 @@ import { ReferenceLogViewProvider } from '../../codewhisperer/service/referenceL
2627
import { AuthUtil } from '../../codewhisperer/util/authUtil'
2728
import { getLogger } from '../../shared'
2829
import { logWithConversationId } from '../userFacingText'
30+
import { checkForDevFile } from '../util/files'
31+
import { CodeWhispererSettings } from '../../codewhisperer'
32+
import { i18n } from '../../shared/i18n-helper'
33+
2934
export class Session {
3035
private _state?: SessionState | Omit<SessionState, 'uploadId'>
3136
private task: string = ''
@@ -59,14 +64,51 @@ export class Session {
5964
* Preload any events that have to run before a chat message can be sent
6065
*/
6166
async preloader(msg: string) {
62-
if (!this.preloaderFinished) {
63-
await this.setupConversation(msg)
64-
this.preloaderFinished = true
65-
this.messenger.sendAsyncEventProgress(this.tabID, true, undefined)
66-
await this.proxyClient.sendFeatureDevTelemetryEvent(this.conversationId) // send the event only once per conversation.
67+
const root = this.config.workspaceRoots[0]
68+
const autoBuildProjectSetting = CodeWhispererSettings.instance.getDevCommandWorkspaceConfigurations()
69+
const hasDevfile = await checkForDevFile(root)
70+
const isPromptedForAutoBuildFeature = Object.keys(autoBuildProjectSetting).includes(root)
71+
72+
if (hasDevfile && !isPromptedForAutoBuildFeature) {
73+
await this.promptAllowQCommandsConsent(this.tabID)
74+
return false
75+
} else {
76+
if (!this.preloaderFinished) {
77+
await this.setupConversation(msg)
78+
this.preloaderFinished = true
79+
this.messenger.sendAsyncEventProgress(this.tabID, true, undefined)
80+
await this.proxyClient.sendFeatureDevTelemetryEvent(this.conversationId) // send the event only once per conversation.
81+
}
82+
return this.preloaderFinished
6783
}
6884
}
6985

86+
private async promptAllowQCommandsConsent(tabID: string) {
87+
this.messenger.sendAnswer({
88+
tabID: tabID,
89+
message: i18n('AWS.amazonq.featureDev.answer.devFileInRepository'),
90+
type: 'answer',
91+
})
92+
93+
this.messenger.sendAnswer({
94+
message: undefined,
95+
type: 'system-prompt',
96+
followUps: [
97+
{
98+
pillText: i18n('AWS.amazonq.featureDev.pillText.acceptForProject'),
99+
type: FollowUpTypes.AcceptAutoBuild,
100+
status: 'success',
101+
},
102+
{
103+
pillText: i18n('AWS.amazonq.featureDev.pillText.declineForProject'),
104+
type: FollowUpTypes.DenyAutoBuild,
105+
status: 'error',
106+
},
107+
],
108+
tabID: tabID,
109+
})
110+
}
111+
70112
/**
71113
* setupConversation
72114
*
@@ -103,6 +145,10 @@ export class Session {
103145
this._state && this._state.updateWorkspaceRoot && this._state.updateWorkspaceRoot(workspaceRootFolder)
104146
}
105147

148+
getWorkspaceRoot(): string {
149+
return this.config.workspaceRoots[0]
150+
}
151+
106152
private getSessionStateConfig(): Omit<SessionStateConfig, 'uploadId'> {
107153
return {
108154
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)

0 commit comments

Comments
 (0)