Skip to content

Commit c5559ef

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

File tree

18 files changed

+216
-46
lines changed

18 files changed

+216
-46
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
@@ -31,11 +31,13 @@ import software.aws.toolkits.jetbrains.core.coroutines.EDT
3131
import software.aws.toolkits.jetbrains.services.amazonq.RepoSizeError
3232
import software.aws.toolkits.jetbrains.services.amazonq.apps.AmazonQAppInitContext
3333
import software.aws.toolkits.jetbrains.services.amazonq.auth.AuthController
34+
import software.aws.toolkits.jetbrains.services.amazonq.messages.MessagePublisher
3435
import software.aws.toolkits.jetbrains.services.amazonq.toolwindow.AmazonQToolWindowFactory
3536
import software.aws.toolkits.jetbrains.services.amazonqFeatureDev.CodeIterationLimitException
3637
import software.aws.toolkits.jetbrains.services.amazonqFeatureDev.DEFAULT_RETRY_LIMIT
3738
import software.aws.toolkits.jetbrains.services.amazonqFeatureDev.FEATURE_NAME
3839
import software.aws.toolkits.jetbrains.services.amazonqFeatureDev.FeatureDevException
40+
import software.aws.toolkits.jetbrains.services.amazonqFeatureDev.GENERATE_DEV_FILE_PROMPT
3941
import software.aws.toolkits.jetbrains.services.amazonqFeatureDev.InboundAppMessagesHandler
4042
import software.aws.toolkits.jetbrains.services.amazonqFeatureDev.ModifySourceFolderErrorReason
4143
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

@@ -434,20 +447,38 @@ class FeatureDevController(
434447
canBeVoted = true
435448
)
436449

437-
messenger.sendSystemPrompt(
438-
tabId = tabId,
439-
followUp = listOf(
440-
FollowUp(
441-
pillText = message("amazonqFeatureDev.follow_up.new_task"),
442-
type = FollowUpTypes.NEW_TASK,
443-
status = FollowUpStatusType.Info
444-
),
450+
val followUps = mutableListOf(
451+
FollowUp(
452+
pillText = message("amazonqFeatureDev.follow_up.new_task"),
453+
type = FollowUpTypes.NEW_TASK,
454+
status = FollowUpStatusType.Info
455+
),
456+
FollowUp(
457+
pillText = message("amazonqFeatureDev.follow_up.close_session"),
458+
type = FollowUpTypes.CLOSE_SESSION,
459+
status = FollowUpStatusType.Info
460+
),
461+
)
462+
463+
if (!session.context.checkForDevFile()) {
464+
followUps.add(
445465
FollowUp(
446-
pillText = message("amazonqFeatureDev.follow_up.close_session"),
447-
type = FollowUpTypes.CLOSE_SESSION,
466+
pillText = message("amazonqFeatureDev.follow_up.generate_dev_file"),
467+
type = FollowUpTypes.GENERATE_DEV_FILE,
448468
status = FollowUpStatusType.Info
449469
)
450470
)
471+
472+
messenger.sendAnswer(
473+
tabId = tabId,
474+
message = message("amazonqFeatureDev.chat_message.generate_dev_file"),
475+
messageType = FeatureDevMessageType.Answer
476+
)
477+
}
478+
479+
messenger.sendSystemPrompt(
480+
tabId = tabId,
481+
followUp = followUps
451482
)
452483

453484
messenger.sendUpdatePlaceholder(
@@ -465,9 +496,7 @@ class FeatureDevController(
465496
}
466497
}
467498

468-
private suspend fun newTask(tabId: String, isException: Boolean? = false) {
469-
this.disablePreviousFileList(tabId)
470-
499+
private suspend fun newTask(tabId: String, isException: Boolean? = false, prefilledPrompt: String? = null) {
471500
val session = getSessionInfo(tabId)
472501
val sessionLatency = System.currentTimeMillis() - session.sessionStartTime
473502

@@ -479,15 +508,30 @@ class FeatureDevController(
479508
chatSessionStorage.deleteSession(tabId)
480509

481510
newTabOpened(tabId)
482-
if (isException != null && !isException) {
483-
messenger.sendAnswer(
484-
tabId = tabId,
485-
messageType = FeatureDevMessageType.Answer,
486-
message = message("amazonqFeatureDev.chat_message.ask_for_new_task")
487-
)
511+
512+
if (prefilledPrompt != null && isException != null && !isException) {
513+
handleChat(tabId = tabId, message = prefilledPrompt)
514+
} else {
515+
if (isException != null && !isException) {
516+
messenger.sendAnswer(
517+
tabId = tabId,
518+
messageType = FeatureDevMessageType.Answer,
519+
message = message("amazonqFeatureDev.chat_message.ask_for_new_task")
520+
)
521+
}
522+
messenger.sendUpdatePlaceholder(tabId = tabId, newPlaceholder = message("amazonqFeatureDev.placeholder.new_plan"))
523+
messenger.sendChatInputEnabledMessage(tabId = tabId, enabled = true)
488524
}
489-
messenger.sendUpdatePlaceholder(tabId = tabId, newPlaceholder = message("amazonqFeatureDev.placeholder.new_plan"))
490-
messenger.sendChatInputEnabledMessage(tabId = tabId, enabled = true)
525+
}
526+
527+
private suspend fun handleDevCommandUserSetting(tabId: String, value: Boolean) {
528+
CodeWhispererSettings.getInstance().toggleAutoBuildFeature(context.project.basePath, value)
529+
messenger.sendAnswer(
530+
tabId = tabId,
531+
message = message("amazonqFeatureDev.chat_message.setting_updated"),
532+
messageType = FeatureDevMessageType.Answer,
533+
)
534+
this.retryRequests(tabId)
491535
}
492536

493537
private suspend fun closeSession(tabId: String) {
@@ -664,6 +708,7 @@ class FeatureDevController(
664708
try {
665709
logger.debug { "$FEATURE_NAME: Processing message: $message" }
666710
session = getSessionInfo(tabId)
711+
session.latestMessage = message
667712

668713
val credentialState = authController.getAuthNeededStates(context.project).amazonQ
669714
if (credentialState != null) {
@@ -676,7 +721,16 @@ class FeatureDevController(
676721
return
677722
}
678723

679-
session.preloader(message, messenger)
724+
val codeWhispererSettings = CodeWhispererSettings.getInstance().getAutoBuildFeatureConfiguration()
725+
val hasDevFile = session.context.checkForDevFile()
726+
val isPromptedForAutoBuildFeature = codeWhispererSettings.containsKey(session.context.getWorkspaceRoot())
727+
728+
if (hasDevFile && !isPromptedForAutoBuildFeature) {
729+
promptAllowQCommandsConsent(messenger, tabId)
730+
return
731+
}
732+
733+
session.preloader(messenger)
680734

681735
when (session.sessionState.phase) {
682736
SessionStatePhase.CODEGEN -> onCodeGeneration(session, message, tabId)
@@ -690,6 +744,30 @@ class FeatureDevController(
690744
}
691745
}
692746

747+
private suspend fun promptAllowQCommandsConsent(messenger: MessagePublisher, tabID: String) {
748+
messenger.sendAnswer(
749+
tabId = tabID,
750+
message = message("amazonqFeatureDev.chat_message.devFileInRepository"),
751+
messageType = FeatureDevMessageType.Answer
752+
)
753+
messenger.sendAnswer(
754+
tabId = tabID,
755+
messageType = FeatureDevMessageType.SystemPrompt,
756+
followUp = listOf(
757+
FollowUp(
758+
pillText = message("amazonqFeatureDev.follow_up.accept_for_project"),
759+
type = FollowUpTypes.ACCEPT_AUTO_BUILD,
760+
status = FollowUpStatusType.Success
761+
),
762+
FollowUp(
763+
pillText = message("amazonqFeatureDev.follow_up.decline_for_project"),
764+
type = FollowUpTypes.DENY_AUTO_BUILD,
765+
status = FollowUpStatusType.Error
766+
)
767+
)
768+
)
769+
}
770+
693771
private suspend fun retryRequests(tabId: String) {
694772
var session: Session? = null
695773
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
@@ -247,6 +247,9 @@ enum class FollowUpTypes(
247247
PROVIDE_FEEDBACK_AND_REGENERATE_CODE("ProvideFeedbackAndRegenerateCode"),
248248
NEW_TASK("NewTask"),
249249
CLOSE_SESSION("CloseSession"),
250+
ACCEPT_AUTO_BUILD("AcceptAutoBuild"),
251+
DENY_AUTO_BUILD("DenyAutoBuild"),
252+
GENERATE_DEV_FILE("GenerateDevFile"),
250253
}
251254

252255
// Util classes

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

Lines changed: 3 additions & 1 deletion
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
@@ -47,7 +48,8 @@ class PrepareCodeGenerationState(
4748
messenger.sendAnswerPart(tabId = this.tabID, message = message("amazonqFeatureDev.chat_message.uploading_code"))
4849
messenger.sendUpdatePlaceholder(tabId = this.tabID, newPlaceholder = message("amazonqFeatureDev.chat_message.uploading_code"))
4950

50-
val repoZipResult = config.repoContext.getProjectZip()
51+
val isAutoBuildFeatureEnabled = CodeWhispererSettings.getInstance().isAutoBuildFeatureEnabled(this.config.repoContext.getWorkspaceRoot())
52+
val repoZipResult = config.repoContext.getProjectZip(isAutoBuildFeatureEnabled = isAutoBuildFeatureEnabled)
5153
val zipFileChecksum = repoZipResult.checksum
5254
zipFileLength = repoZipResult.contentLength
5355
val fileToUpload = repoZipResult.payload

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
@@ -55,9 +55,9 @@ class Session(val tabID: String, val project: Project) {
5555
/**
5656
* Preload any events that have to run before a chat message can be sent
5757
*/
58-
suspend fun preloader(msg: String, messenger: MessagePublisher) {
58+
suspend fun preloader(messenger: MessagePublisher) {
5959
if (!preloaderFinished) {
60-
setupConversation(msg, messenger)
60+
setupConversation(messenger)
6161
preloaderFinished = true
6262
messenger.sendAsyncEventProgress(tabId = this.tabID, inProgress = true)
6363
featureDevService.sendFeatureDevEvent(this.conversationId)
@@ -67,10 +67,7 @@ class Session(val tabID: String, val project: Project) {
6767
/**
6868
* Starts a conversation with the backend and uploads the repo for the LLMs to be able to use it.
6969
*/
70-
private fun setupConversation(msg: String, messenger: MessagePublisher) {
71-
// Store the initial message when setting up the conversation so that if it fails we can retry with this message
72-
_latestMessage = msg
73-
70+
private fun setupConversation(messenger: MessagePublisher) {
7471
_conversationId = featureDevService.createConversation()
7572
logger<Session>().info(conversationIDLog(this.conversationId))
7673

@@ -204,8 +201,11 @@ class Session(val tabID: String, val project: Project) {
204201
}
205202
}
206203

207-
val latestMessage: String
204+
var latestMessage: String
208205
get() = this._latestMessage
206+
set(value) {
207+
this._latestMessage = value
208+
}
209209

210210
val retries: Int
211211
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
@@ -160,7 +160,7 @@ class FeatureDevControllerTest : FeatureDevTestBase() {
160160
every { AmazonqTelemetry.endChat(amazonqConversationId = any(), amazonqEndOfTheConversationLatency = any()) } just runs
161161

162162
runTest {
163-
spySession.preloader(userMessage, messenger)
163+
spySession.preloader(messenger)
164164
controller.processFollowupClickedMessage(message)
165165
}
166166

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

192-
spySession.preloader(userMessage, messenger)
192+
spySession.preloader(messenger)
193193
controller.processFollowupClickedMessage(message)
194194

195195
coVerifyOrder {
@@ -241,7 +241,7 @@ class FeatureDevControllerTest : FeatureDevTestBase() {
241241

242242
doReturn(Unit).`when`(spySession).insertChanges(any(), any(), any(), any())
243243

244-
spySession.preloader(userMessage, messenger)
244+
spySession.preloader(messenger)
245245
controller.processFollowupClickedMessage(message)
246246

247247
mockitoVerify(
@@ -266,6 +266,7 @@ class FeatureDevControllerTest : FeatureDevTestBase() {
266266
listOf(
267267
FollowUp(FollowUpTypes.NEW_TASK, message("amazonqFeatureDev.follow_up.new_task"), status = FollowUpStatusType.Info),
268268
FollowUp(FollowUpTypes.CLOSE_SESSION, message("amazonqFeatureDev.follow_up.close_session"), status = FollowUpStatusType.Info),
269+
FollowUp(FollowUpTypes.GENERATE_DEV_FILE, message("amazonqFeatureDev.follow_up.generate_dev_file"), status = FollowUpStatusType.Info)
269270
),
270271
)
271272
messenger.sendUpdatePlaceholder(testTabId, message("amazonqFeatureDev.placeholder.additional_improvements"))
@@ -565,7 +566,7 @@ class FeatureDevControllerTest : FeatureDevTestBase() {
565566
mockkStatic("software.aws.toolkits.jetbrains.services.amazonqFeatureDev.util.FileUtilsKt")
566567
every { selectFolder(any(), any()) } returns null
567568

568-
spySession.preloader(userMessage, messenger)
569+
spySession.preloader(messenger)
569570
controller.processFollowupClickedMessage(message)
570571

571572
coVerifyOrder {
@@ -596,7 +597,7 @@ class FeatureDevControllerTest : FeatureDevTestBase() {
596597
mockkStatic("software.aws.toolkits.jetbrains.services.amazonqFeatureDev.util.FileUtilsKt")
597598
every { selectFolder(any(), any()) } returns LightVirtualFile("/path")
598599

599-
spySession.preloader(userMessage, messenger)
600+
spySession.preloader(messenger)
600601
controller.processFollowupClickedMessage(message)
601602

602603
coVerifyOrder {
@@ -633,7 +634,7 @@ class FeatureDevControllerTest : FeatureDevTestBase() {
633634
mockkStatic("software.aws.toolkits.jetbrains.services.amazonqFeatureDev.util.FileUtilsKt")
634635
every { selectFolder(any(), any()) } returns folder
635636

636-
spySession.preloader(userMessage, messenger)
637+
spySession.preloader(messenger)
637638
controller.processFollowupClickedMessage(message)
638639

639640
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
@@ -87,14 +87,14 @@ class PrepareCodeGenerationStateTest : FeatureDevTestBase() {
8787
val repoZipResult = ZipCreationResult(mockFile, testChecksumSha, testContentLength)
8888
val action = SessionStateAction("test-task", userMessage)
8989

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

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

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)