Skip to content

Commit e0d8680

Browse files
committed
feat(feature dev): Add setting to allow Q to run code and test commands (#5077)
Q /dev team is launching a new feature: Allow Q to execute build/test based on customer's configuration 🎉🎉 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 - 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 - Updated copies - Updated `codefileExtensions` set
1 parent e0ddf0b commit e0d8680

File tree

17 files changed

+211
-47
lines changed

17 files changed

+211
-47
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+
}

plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonqFeatureDev/FeatureDevConstants.kt

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,9 @@ const val FEATURE_EVALUATION_PRODUCT_NAME = "FeatureDev"
77

88
const val FEATURE_NAME = "Amazon Q Developer Agent for software development"
99

10+
@Suppress("MaxLineLength")
11+
const val GENERATE_DEV_FILE_PROMPT = "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\""
12+
1013
// Max number of times a user can attempt to retry a code generation request if it fails
1114
const val CODE_GENERATION_RETRY_LIMIT = 3
1215

plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonqFeatureDev/controller/FeatureDevController.kt

Lines changed: 100 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -32,11 +32,13 @@ import software.aws.toolkits.jetbrains.core.coroutines.EDT
3232
import software.aws.toolkits.jetbrains.services.amazonq.RepoSizeError
3333
import software.aws.toolkits.jetbrains.services.amazonq.apps.AmazonQAppInitContext
3434
import software.aws.toolkits.jetbrains.services.amazonq.auth.AuthController
35+
import software.aws.toolkits.jetbrains.services.amazonq.messages.MessagePublisher
3536
import software.aws.toolkits.jetbrains.services.amazonq.toolwindow.AmazonQToolWindowFactory
3637
import software.aws.toolkits.jetbrains.services.amazonqFeatureDev.CodeIterationLimitException
3738
import software.aws.toolkits.jetbrains.services.amazonqFeatureDev.DEFAULT_RETRY_LIMIT
3839
import software.aws.toolkits.jetbrains.services.amazonqFeatureDev.FEATURE_NAME
3940
import software.aws.toolkits.jetbrains.services.amazonqFeatureDev.FeatureDevException
41+
import software.aws.toolkits.jetbrains.services.amazonqFeatureDev.GENERATE_DEV_FILE_PROMPT
4042
import software.aws.toolkits.jetbrains.services.amazonqFeatureDev.InboundAppMessagesHandler
4143
import software.aws.toolkits.jetbrains.services.amazonqFeatureDev.ModifySourceFolderErrorReason
4244
import software.aws.toolkits.jetbrains.services.amazonqFeatureDev.MonthlyConversationLimitError
@@ -75,6 +77,7 @@ import software.aws.toolkits.jetbrains.services.codewhisperer.util.content
7577
import software.aws.toolkits.jetbrains.services.cwc.controller.chat.telemetry.FeedbackComment
7678
import software.aws.toolkits.jetbrains.services.cwc.controller.chat.telemetry.getStartUrl
7779
import software.aws.toolkits.jetbrains.services.telemetry.TelemetryService
80+
import software.aws.toolkits.jetbrains.settings.CodeWhispererSettings
7881
import software.aws.toolkits.jetbrains.ui.feedback.FeatureDevFeedbackDialog
7982
import software.aws.toolkits.jetbrains.utils.notifyError
8083
import software.aws.toolkits.resources.message
@@ -133,6 +136,16 @@ class FeatureDevController(
133136
FollowUpTypes.PROVIDE_FEEDBACK_AND_REGENERATE_CODE -> provideFeedbackAndRegenerateCode(message.tabId)
134137
FollowUpTypes.NEW_TASK -> newTask(message.tabId)
135138
FollowUpTypes.CLOSE_SESSION -> closeSession(message.tabId)
139+
FollowUpTypes.ACCEPT_AUTO_BUILD -> handleDevCommandUserSetting(message.tabId, true)
140+
FollowUpTypes.DENY_AUTO_BUILD -> handleDevCommandUserSetting(message.tabId, false)
141+
FollowUpTypes.GENERATE_DEV_FILE -> {
142+
messenger.sendAnswer(
143+
tabId = message.tabId,
144+
messageType = FeatureDevMessageType.SystemPrompt,
145+
message = message("amazonqFeatureDev.follow_up.generate_dev_file")
146+
)
147+
newTask(tabId = message.tabId, prefilledPrompt = GENERATE_DEV_FILE_PROMPT)
148+
}
136149
}
137150
}
138151

@@ -439,20 +452,38 @@ class FeatureDevController(
439452
canBeVoted = true
440453
)
441454

442-
messenger.sendSystemPrompt(
443-
tabId = tabId,
444-
followUp = listOf(
445-
FollowUp(
446-
pillText = message("amazonqFeatureDev.follow_up.new_task"),
447-
type = FollowUpTypes.NEW_TASK,
448-
status = FollowUpStatusType.Info
449-
),
455+
val followUps = mutableListOf(
456+
FollowUp(
457+
pillText = message("amazonqFeatureDev.follow_up.new_task"),
458+
type = FollowUpTypes.NEW_TASK,
459+
status = FollowUpStatusType.Info
460+
),
461+
FollowUp(
462+
pillText = message("amazonqFeatureDev.follow_up.close_session"),
463+
type = FollowUpTypes.CLOSE_SESSION,
464+
status = FollowUpStatusType.Info
465+
),
466+
)
467+
468+
if (!session.context.checkForDevFile()) {
469+
followUps.add(
450470
FollowUp(
451-
pillText = message("amazonqFeatureDev.follow_up.close_session"),
452-
type = FollowUpTypes.CLOSE_SESSION,
471+
pillText = message("amazonqFeatureDev.follow_up.generate_dev_file"),
472+
type = FollowUpTypes.GENERATE_DEV_FILE,
453473
status = FollowUpStatusType.Info
454474
)
455475
)
476+
477+
messenger.sendAnswer(
478+
tabId = tabId,
479+
message = message("amazonqFeatureDev.chat_message.generate_dev_file"),
480+
messageType = FeatureDevMessageType.Answer
481+
)
482+
}
483+
484+
messenger.sendSystemPrompt(
485+
tabId = tabId,
486+
followUp = followUps
456487
)
457488

458489
messenger.sendUpdatePlaceholder(
@@ -470,9 +501,7 @@ class FeatureDevController(
470501
}
471502
}
472503

473-
private suspend fun newTask(tabId: String, isException: Boolean? = false) {
474-
this.disablePreviousFileList(tabId)
475-
504+
private suspend fun newTask(tabId: String, isException: Boolean? = false, prefilledPrompt: String? = null) {
476505
val session = getSessionInfo(tabId)
477506
val sessionLatency = System.currentTimeMillis() - session.sessionStartTime
478507

@@ -484,15 +513,30 @@ class FeatureDevController(
484513
chatSessionStorage.deleteSession(tabId)
485514

486515
newTabOpened(tabId)
487-
if (isException != null && !isException) {
488-
messenger.sendAnswer(
489-
tabId = tabId,
490-
messageType = FeatureDevMessageType.Answer,
491-
message = message("amazonqFeatureDev.chat_message.ask_for_new_task")
492-
)
516+
517+
if (prefilledPrompt != null && isException != null && !isException) {
518+
handleChat(tabId = tabId, message = prefilledPrompt)
519+
} else {
520+
if (isException != null && !isException) {
521+
messenger.sendAnswer(
522+
tabId = tabId,
523+
messageType = FeatureDevMessageType.Answer,
524+
message = message("amazonqFeatureDev.chat_message.ask_for_new_task")
525+
)
526+
}
527+
messenger.sendUpdatePlaceholder(tabId = tabId, newPlaceholder = message("amazonqFeatureDev.placeholder.new_plan"))
528+
messenger.sendChatInputEnabledMessage(tabId = tabId, enabled = true)
493529
}
494-
messenger.sendUpdatePlaceholder(tabId = tabId, newPlaceholder = message("amazonqFeatureDev.placeholder.new_plan"))
495-
messenger.sendChatInputEnabledMessage(tabId = tabId, enabled = true)
530+
}
531+
532+
private suspend fun handleDevCommandUserSetting(tabId: String, value: Boolean) {
533+
CodeWhispererSettings.getInstance().toggleAutoBuildFeature(context.project.basePath, value)
534+
messenger.sendAnswer(
535+
tabId = tabId,
536+
message = message("amazonqFeatureDev.chat_message.setting_updated"),
537+
messageType = FeatureDevMessageType.Answer,
538+
)
539+
this.retryRequests(tabId)
496540
}
497541

498542
private suspend fun closeSession(tabId: String) {
@@ -669,6 +713,7 @@ class FeatureDevController(
669713
try {
670714
logger.debug { "$FEATURE_NAME: Processing message: $message" }
671715
session = getSessionInfo(tabId)
716+
session.latestMessage = message
672717

673718
val credentialState = authController.getAuthNeededStates(context.project).amazonQ
674719
if (credentialState != null) {
@@ -681,7 +726,16 @@ class FeatureDevController(
681726
return
682727
}
683728

684-
session.preloader(message, messenger)
729+
val codeWhispererSettings = CodeWhispererSettings.getInstance().getAutoBuildFeatureConfiguration()
730+
val hasDevFile = session.context.checkForDevFile()
731+
val isPromptedForAutoBuildFeature = codeWhispererSettings.containsKey(session.context.getWorkspaceRoot())
732+
733+
if (hasDevFile && !isPromptedForAutoBuildFeature) {
734+
promptAllowQCommandsConsent(messenger, tabId)
735+
return
736+
}
737+
738+
session.preloader(messenger)
685739

686740
when (session.sessionState.phase) {
687741
SessionStatePhase.CODEGEN -> onCodeGeneration(session, message, tabId)
@@ -695,6 +749,30 @@ class FeatureDevController(
695749
}
696750
}
697751

752+
private suspend fun promptAllowQCommandsConsent(messenger: MessagePublisher, tabID: String) {
753+
messenger.sendAnswer(
754+
tabId = tabID,
755+
message = message("amazonqFeatureDev.chat_message.devFileInRepository"),
756+
messageType = FeatureDevMessageType.Answer
757+
)
758+
messenger.sendAnswer(
759+
tabId = tabID,
760+
messageType = FeatureDevMessageType.SystemPrompt,
761+
followUp = listOf(
762+
FollowUp(
763+
pillText = message("amazonqFeatureDev.follow_up.accept_for_project"),
764+
type = FollowUpTypes.ACCEPT_AUTO_BUILD,
765+
status = FollowUpStatusType.Success
766+
),
767+
FollowUp(
768+
pillText = message("amazonqFeatureDev.follow_up.decline_for_project"),
769+
type = FollowUpTypes.DENY_AUTO_BUILD,
770+
status = FollowUpStatusType.Error
771+
)
772+
)
773+
)
774+
}
775+
698776
private suspend fun retryRequests(tabId: String) {
699777
var session: Session? = null
700778
try {

plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonqFeatureDev/messages/FeatureDevMessage.kt

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -250,6 +250,9 @@ enum class FollowUpTypes(
250250
PROVIDE_FEEDBACK_AND_REGENERATE_CODE("ProvideFeedbackAndRegenerateCode"),
251251
NEW_TASK("NewTask"),
252252
CLOSE_SESSION("CloseSession"),
253+
ACCEPT_AUTO_BUILD("AcceptAutoBuild"),
254+
DENY_AUTO_BUILD("DenyAutoBuild"),
255+
GENERATE_DEV_FILE("GenerateDevFile"),
253256
}
254257

255258
// Util classes

plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonqFeatureDev/session/PrepareCodeGenerationState.kt

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import software.aws.toolkits.jetbrains.services.amazonqFeatureDev.util.Cancellat
1313
import software.aws.toolkits.jetbrains.services.amazonqFeatureDev.util.deleteUploadArtifact
1414
import software.aws.toolkits.jetbrains.services.amazonqFeatureDev.util.uploadArtifactToS3
1515
import software.aws.toolkits.jetbrains.services.cwc.controller.chat.telemetry.getStartUrl
16+
import software.aws.toolkits.jetbrains.settings.CodeWhispererSettings
1617
import software.aws.toolkits.resources.message
1718
import software.aws.toolkits.telemetry.AmazonqTelemetry
1819
import software.aws.toolkits.telemetry.AmazonqUploadIntent
@@ -48,7 +49,8 @@ class PrepareCodeGenerationState(
4849
messenger.sendAnswerPart(tabId = this.tabID, message = message("amazonqFeatureDev.chat_message.uploading_code"))
4950
messenger.sendUpdatePlaceholder(tabId = this.tabID, newPlaceholder = message("amazonqFeatureDev.chat_message.uploading_code"))
5051

51-
val repoZipResult = config.repoContext.getProjectZip()
52+
val isAutoBuildFeatureEnabled = CodeWhispererSettings.getInstance().isAutoBuildFeatureEnabled(this.config.repoContext.getWorkspaceRoot())
53+
val repoZipResult = config.repoContext.getProjectZip(isAutoBuildFeatureEnabled = isAutoBuildFeatureEnabled)
5254
val zipFileChecksum = repoZipResult.checksum
5355
zipFileLength = repoZipResult.contentLength
5456
val fileToUpload = repoZipResult.payload
@@ -96,7 +98,7 @@ class PrepareCodeGenerationState(
9698
credentialStartUrl = getStartUrl(config.featureDevService.project)
9799
)
98100
}
99-
// It is essential to interact with the next state outside of try-catch block for the telemetry to capture events for the states separately
101+
// It is essential to interact with the next state outside try-catch block for the telemetry to capture events for the states separately
100102
return nextState.interact(action)
101103
}
102104
}

plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonqFeatureDev/session/Session.kt

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -67,9 +67,9 @@ class Session(val tabID: String, val project: Project) {
6767
/**
6868
* Preload any events that have to run before a chat message can be sent
6969
*/
70-
suspend fun preloader(msg: String, messenger: MessagePublisher) {
70+
suspend fun preloader(messenger: MessagePublisher) {
7171
if (!preloaderFinished) {
72-
setupConversation(msg, messenger)
72+
setupConversation(messenger)
7373
preloaderFinished = true
7474
messenger.sendAsyncEventProgress(tabId = this.tabID, inProgress = true)
7575
featureDevService.sendFeatureDevEvent(this.conversationId)
@@ -79,10 +79,7 @@ class Session(val tabID: String, val project: Project) {
7979
/**
8080
* Starts a conversation with the backend and uploads the repo for the LLMs to be able to use it.
8181
*/
82-
private fun setupConversation(msg: String, messenger: MessagePublisher) {
83-
// Store the initial message when setting up the conversation so that if it fails we can retry with this message
84-
_latestMessage = msg
85-
82+
private fun setupConversation(messenger: MessagePublisher) {
8683
_conversationId = featureDevService.createConversation()
8784
logger<Session>().info(conversationIDLog(this.conversationId))
8885

@@ -253,8 +250,11 @@ class Session(val tabID: String, val project: Project) {
253250
}
254251
}
255252

256-
val latestMessage: String
253+
var latestMessage: String
257254
get() = this._latestMessage
255+
set(value) {
256+
this._latestMessage = value
257+
}
258258

259259
val retries: Int
260260
get() = codegenRetries

plugins/amazonq/chat/jetbrains-community/tst/software/aws/toolkits/jetbrains/services/amazonqFeatureDev/FeatureDevSessionContextTest.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ class FeatureDevSessionContextTest : FeatureDevTestBase() {
4646
@Test
4747
fun testWithInvalidFile() {
4848
val txtFile = mock<VirtualFile>()
49-
whenever(txtFile.extension).thenReturn("txt")
49+
whenever(txtFile.extension).thenReturn("mp4")
5050
assertFalse(featureDevSessionContext.isFileExtensionAllowed(txtFile))
5151
}
5252
}

plugins/amazonq/chat/jetbrains-community/tst/software/aws/toolkits/jetbrains/services/amazonqFeatureDev/controller/FeatureDevControllerTest.kt

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -161,7 +161,7 @@ class FeatureDevControllerTest : FeatureDevTestBase() {
161161
every { AmazonqTelemetry.endChat(amazonqConversationId = any(), amazonqEndOfTheConversationLatency = any()) } just runs
162162

163163
runTest {
164-
spySession.preloader(userMessage, messenger)
164+
spySession.preloader(messenger)
165165
controller.processFollowupClickedMessage(message)
166166
}
167167

@@ -190,7 +190,7 @@ class FeatureDevControllerTest : FeatureDevTestBase() {
190190
mockkObject(AmazonqTelemetry)
191191
every { AmazonqTelemetry.isProvideFeedbackForCodeGen(amazonqConversationId = any(), enabled = any()) } just runs
192192

193-
spySession.preloader(userMessage, messenger)
193+
spySession.preloader(messenger)
194194
controller.processFollowupClickedMessage(message)
195195

196196
coVerifyOrder {
@@ -243,7 +243,7 @@ class FeatureDevControllerTest : FeatureDevTestBase() {
243243

244244
doReturn(Unit).`when`(spySession).insertChanges(any(), any(), any(), any())
245245

246-
spySession.preloader(userMessage, messenger)
246+
spySession.preloader(messenger)
247247
controller.processFollowupClickedMessage(message)
248248

249249
mockitoVerify(
@@ -268,6 +268,7 @@ class FeatureDevControllerTest : FeatureDevTestBase() {
268268
listOf(
269269
FollowUp(FollowUpTypes.NEW_TASK, message("amazonqFeatureDev.follow_up.new_task"), status = FollowUpStatusType.Info),
270270
FollowUp(FollowUpTypes.CLOSE_SESSION, message("amazonqFeatureDev.follow_up.close_session"), status = FollowUpStatusType.Info),
271+
FollowUp(FollowUpTypes.GENERATE_DEV_FILE, message("amazonqFeatureDev.follow_up.generate_dev_file"), status = FollowUpStatusType.Info)
271272
),
272273
)
273274
messenger.sendUpdatePlaceholder(testTabId, message("amazonqFeatureDev.placeholder.additional_improvements"))
@@ -573,7 +574,7 @@ class FeatureDevControllerTest : FeatureDevTestBase() {
573574
mockkStatic("software.aws.toolkits.jetbrains.common.util.FileUtilsKt")
574575
every { selectFolder(any(), any()) } returns null
575576

576-
spySession.preloader(userMessage, messenger)
577+
spySession.preloader(messenger)
577578
controller.processFollowupClickedMessage(message)
578579

579580
coVerifyOrder {
@@ -604,7 +605,7 @@ class FeatureDevControllerTest : FeatureDevTestBase() {
604605
mockkStatic("software.aws.toolkits.jetbrains.common.util.FileUtilsKt")
605606
every { selectFolder(any(), any()) } returns LightVirtualFile("/path")
606607

607-
spySession.preloader(userMessage, messenger)
608+
spySession.preloader(messenger)
608609
controller.processFollowupClickedMessage(message)
609610

610611
coVerifyOrder {
@@ -641,7 +642,7 @@ class FeatureDevControllerTest : FeatureDevTestBase() {
641642
mockkStatic("software.aws.toolkits.jetbrains.common.util.FileUtilsKt")
642643
every { selectFolder(any(), any()) } returns folder
643644

644-
spySession.preloader(userMessage, messenger)
645+
spySession.preloader(messenger)
645646
controller.processFollowupClickedMessage(message)
646647

647648
coVerify {

plugins/amazonq/chat/jetbrains-community/tst/software/aws/toolkits/jetbrains/services/amazonqFeatureDev/session/PrepareCodeGenerationStateTest.kt

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -88,14 +88,14 @@ class PrepareCodeGenerationStateTest : FeatureDevTestBase() {
8888
val repoZipResult = ZipCreationResult(mockFile, testChecksumSha, testContentLength)
8989
val action = SessionStateAction("test-task", userMessage)
9090

91-
whenever(repoContext.getProjectZip()).thenReturn(repoZipResult)
91+
whenever(repoContext.getProjectZip(false)).thenReturn(repoZipResult)
9292
every { featureDevService.createUploadUrl(any(), any(), any(), any()) } returns exampleCreateUploadUrlResponse
9393

9494
runTest {
9595
val actual = prepareCodeGenerationState.interact(action)
9696
assertThat(actual.nextState).isInstanceOf(PrepareCodeGenerationState::class.java)
9797
}
9898
assertThat(prepareCodeGenerationState.phase).isEqualTo(SessionStatePhase.CODEGEN)
99-
verify(repoContext, times(1)).getProjectZip()
99+
verify(repoContext, times(1)).getProjectZip(false)
100100
}
101101
}

plugins/amazonq/chat/jetbrains-community/tst/software/aws/toolkits/jetbrains/services/amazonqFeatureDev/session/SessionTest.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,7 @@ class SessionTest : FeatureDevTestBase() {
5858
fun `test preloader`() = runTest {
5959
whenever(featureDevClient.createTaskAssistConversation()).thenReturn(exampleCreateTaskAssistConversationResponse)
6060

61-
session.preloader(userMessage, messenger)
61+
session.preloader(messenger)
6262
assertThat(session.conversationId).isEqualTo(testConversationId)
6363
assertThat(session.sessionState).isInstanceOf(PrepareCodeGenerationState::class.java)
6464
verify(featureDevClient, times(1)).createTaskAssistConversation()

0 commit comments

Comments
 (0)