Skip to content

Commit 32d8a1e

Browse files
committed
feat(feature dev): Add setting to allow Q to run code and test commands
1 parent 5a2cd7e commit 32d8a1e

File tree

17 files changed

+208
-46
lines changed

17 files changed

+208
-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 & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -28,11 +28,13 @@ import software.aws.toolkits.jetbrains.core.coroutines.EDT
2828
import software.aws.toolkits.jetbrains.services.amazonq.RepoSizeError
2929
import software.aws.toolkits.jetbrains.services.amazonq.apps.AmazonQAppInitContext
3030
import software.aws.toolkits.jetbrains.services.amazonq.auth.AuthController
31+
import software.aws.toolkits.jetbrains.services.amazonq.messages.MessagePublisher
3132
import software.aws.toolkits.jetbrains.services.amazonq.toolwindow.AmazonQToolWindowFactory
3233
import software.aws.toolkits.jetbrains.services.amazonqFeatureDev.CodeIterationLimitException
3334
import software.aws.toolkits.jetbrains.services.amazonqFeatureDev.DEFAULT_RETRY_LIMIT
3435
import software.aws.toolkits.jetbrains.services.amazonqFeatureDev.FEATURE_NAME
3536
import software.aws.toolkits.jetbrains.services.amazonqFeatureDev.FeatureDevException
37+
import software.aws.toolkits.jetbrains.services.amazonqFeatureDev.GENERATE_DEV_FILE_PROMPT
3638
import software.aws.toolkits.jetbrains.services.amazonqFeatureDev.InboundAppMessagesHandler
3739
import software.aws.toolkits.jetbrains.services.amazonqFeatureDev.ModifySourceFolderErrorReason
3840
import software.aws.toolkits.jetbrains.services.amazonqFeatureDev.MonthlyConversationLimitError
@@ -69,6 +71,7 @@ import software.aws.toolkits.jetbrains.services.amazonqFeatureDev.util.selectFol
6971
import software.aws.toolkits.jetbrains.services.cwc.controller.chat.telemetry.FeedbackComment
7072
import software.aws.toolkits.jetbrains.services.cwc.controller.chat.telemetry.getStartUrl
7173
import software.aws.toolkits.jetbrains.services.telemetry.TelemetryService
74+
import software.aws.toolkits.jetbrains.settings.CodeWhispererSettings
7275
import software.aws.toolkits.jetbrains.ui.feedback.FeatureDevFeedbackDialog
7376
import software.aws.toolkits.jetbrains.utils.notifyError
7477
import software.aws.toolkits.resources.message
@@ -121,6 +124,16 @@ class FeatureDevController(
121124
FollowUpTypes.PROVIDE_FEEDBACK_AND_REGENERATE_CODE -> provideFeedbackAndRegenerateCode(message.tabId)
122125
FollowUpTypes.NEW_TASK -> newTask(message.tabId)
123126
FollowUpTypes.CLOSE_SESSION -> closeSession(message.tabId)
127+
FollowUpTypes.ACCEPT_AUTO_BUILD -> handleDevCommandUserSetting(message.tabId, true)
128+
FollowUpTypes.DENY_AUTO_BUILD -> handleDevCommandUserSetting(message.tabId, false)
129+
FollowUpTypes.GENERATE_DEV_FILE -> {
130+
messenger.sendAnswer(
131+
tabId = message.tabId,
132+
messageType = FeatureDevMessageType.SystemPrompt,
133+
message = message("amazonqFeatureDev.follow_up.generate_dev_file")
134+
)
135+
newTask(tabId = message.tabId, prefilledPrompt = GENERATE_DEV_FILE_PROMPT)
136+
}
124137
}
125138
}
126139

@@ -345,20 +358,38 @@ class FeatureDevController(
345358
canBeVoted = true
346359
)
347360

348-
messenger.sendSystemPrompt(
349-
tabId = tabId,
350-
followUp = listOf(
351-
FollowUp(
352-
pillText = message("amazonqFeatureDev.follow_up.new_task"),
353-
type = FollowUpTypes.NEW_TASK,
354-
status = FollowUpStatusType.Info
355-
),
361+
val followUps = mutableListOf(
362+
FollowUp(
363+
pillText = message("amazonqFeatureDev.follow_up.new_task"),
364+
type = FollowUpTypes.NEW_TASK,
365+
status = FollowUpStatusType.Info
366+
),
367+
FollowUp(
368+
pillText = message("amazonqFeatureDev.follow_up.close_session"),
369+
type = FollowUpTypes.CLOSE_SESSION,
370+
status = FollowUpStatusType.Info
371+
),
372+
)
373+
374+
if (!session.context.checkForDevFile()) {
375+
followUps.add(
356376
FollowUp(
357-
pillText = message("amazonqFeatureDev.follow_up.close_session"),
358-
type = FollowUpTypes.CLOSE_SESSION,
377+
pillText = message("amazonqFeatureDev.follow_up.generate_dev_file"),
378+
type = FollowUpTypes.GENERATE_DEV_FILE,
359379
status = FollowUpStatusType.Info
360380
)
361381
)
382+
383+
messenger.sendAnswer(
384+
tabId = tabId,
385+
message = message("amazonqFeatureDev.chat_message.generate_dev_file"),
386+
messageType = FeatureDevMessageType.Answer
387+
)
388+
}
389+
390+
messenger.sendSystemPrompt(
391+
tabId = tabId,
392+
followUp = followUps
362393
)
363394

364395
messenger.sendUpdatePlaceholder(
@@ -376,7 +407,7 @@ class FeatureDevController(
376407
}
377408
}
378409

379-
private suspend fun newTask(tabId: String, isException: Boolean? = false) {
410+
private suspend fun newTask(tabId: String, isException: Boolean? = false, prefilledPrompt: String? = null) {
380411
val session = getSessionInfo(tabId)
381412
val sessionLatency = System.currentTimeMillis() - session.sessionStartTime
382413
AmazonqTelemetry.endChat(
@@ -387,15 +418,30 @@ class FeatureDevController(
387418
chatSessionStorage.deleteSession(tabId)
388419

389420
newTabOpened(tabId)
390-
if (isException != null && !isException) {
391-
messenger.sendAnswer(
392-
tabId = tabId,
393-
messageType = FeatureDevMessageType.Answer,
394-
message = message("amazonqFeatureDev.chat_message.ask_for_new_task")
395-
)
421+
422+
if (prefilledPrompt != null && isException != null && !isException) {
423+
handleChat(tabId = tabId, message = prefilledPrompt)
424+
} else {
425+
if (isException != null && !isException) {
426+
messenger.sendAnswer(
427+
tabId = tabId,
428+
messageType = FeatureDevMessageType.Answer,
429+
message = message("amazonqFeatureDev.chat_message.ask_for_new_task")
430+
)
431+
}
432+
messenger.sendUpdatePlaceholder(tabId = tabId, newPlaceholder = message("amazonqFeatureDev.placeholder.new_plan"))
433+
messenger.sendChatInputEnabledMessage(tabId = tabId, enabled = true)
396434
}
397-
messenger.sendUpdatePlaceholder(tabId = tabId, newPlaceholder = message("amazonqFeatureDev.placeholder.new_plan"))
398-
messenger.sendChatInputEnabledMessage(tabId = tabId, enabled = true)
435+
}
436+
437+
private suspend fun handleDevCommandUserSetting(tabId: String, value: Boolean) {
438+
CodeWhispererSettings.getInstance().toggleAutoBuildFeature(context.project.basePath, value)
439+
messenger.sendAnswer(
440+
tabId = tabId,
441+
message = message("amazonqFeatureDev.chat_message.setting_updated"),
442+
messageType = FeatureDevMessageType.Answer,
443+
)
444+
this.retryRequests(tabId)
399445
}
400446

401447
private suspend fun closeSession(tabId: String) {
@@ -554,6 +600,7 @@ class FeatureDevController(
554600
try {
555601
logger.debug { "$FEATURE_NAME: Processing message: $message" }
556602
session = getSessionInfo(tabId)
603+
session.latestMessage = message
557604

558605
val credentialState = authController.getAuthNeededStates(context.project).amazonQ
559606
if (credentialState != null) {
@@ -566,7 +613,16 @@ class FeatureDevController(
566613
return
567614
}
568615

569-
session.preloader(message, messenger)
616+
val codeWhispererSettings = CodeWhispererSettings.getInstance().getAutoBuildFeatureConfiguration()
617+
val hasDevFile = session.context.checkForDevFile()
618+
val isPromptedForAutoBuildFeature = codeWhispererSettings.containsKey(session.context.getWorkspaceRoot())
619+
620+
if (hasDevFile && !isPromptedForAutoBuildFeature) {
621+
promptAllowQCommandsConsent(messenger, tabId)
622+
return
623+
}
624+
625+
session.preloader(messenger)
570626

571627
when (session.sessionState.phase) {
572628
SessionStatePhase.CODEGEN -> onCodeGeneration(session, message, tabId)
@@ -580,6 +636,30 @@ class FeatureDevController(
580636
}
581637
}
582638

639+
private suspend fun promptAllowQCommandsConsent(messenger: MessagePublisher, tabID: String) {
640+
messenger.sendAnswer(
641+
tabId = tabID,
642+
message = message("amazonqFeatureDev.chat_message.devFileInRepository"),
643+
messageType = FeatureDevMessageType.Answer
644+
)
645+
messenger.sendAnswer(
646+
tabId = tabID,
647+
messageType = FeatureDevMessageType.SystemPrompt,
648+
followUp = listOf(
649+
FollowUp(
650+
pillText = message("amazonqFeatureDev.follow_up.accept_for_project"),
651+
type = FollowUpTypes.ACCEPT_AUTO_BUILD,
652+
status = FollowUpStatusType.Success
653+
),
654+
FollowUp(
655+
pillText = message("amazonqFeatureDev.follow_up.decline_for_project"),
656+
type = FollowUpTypes.DENY_AUTO_BUILD,
657+
status = FollowUpStatusType.Error
658+
)
659+
)
660+
)
661+
}
662+
583663
private suspend fun retryRequests(tabId: String) {
584664
var session: Session? = null
585665
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
@@ -239,6 +239,9 @@ enum class FollowUpTypes(
239239
PROVIDE_FEEDBACK_AND_REGENERATE_CODE("ProvideFeedbackAndRegenerateCode"),
240240
NEW_TASK("NewTask"),
241241
CLOSE_SESSION("CloseSession"),
242+
ACCEPT_AUTO_BUILD("AcceptAutoBuild"),
243+
DENY_AUTO_BUILD("DenyAutoBuild"),
244+
GENERATE_DEV_FILE("GenerateDevFile"),
242245
}
243246

244247
// 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: 14 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -13,12 +13,19 @@ import software.aws.toolkits.jetbrains.services.amazonqFeatureDev.ConversationId
1313
import software.aws.toolkits.jetbrains.services.amazonqFeatureDev.FEATURE_NAME
1414
import software.aws.toolkits.jetbrains.services.amazonqFeatureDev.MAX_PROJECT_SIZE_BYTES
1515
import software.aws.toolkits.jetbrains.services.amazonqFeatureDev.clients.FeatureDevClient
16+
import software.aws.toolkits.jetbrains.services.amazonqFeatureDev.messages.FeatureDevMessageType
17+
import software.aws.toolkits.jetbrains.services.amazonqFeatureDev.messages.FollowUp
18+
import software.aws.toolkits.jetbrains.services.amazonqFeatureDev.messages.FollowUpStatusType
19+
import software.aws.toolkits.jetbrains.services.amazonqFeatureDev.messages.FollowUpTypes
20+
import software.aws.toolkits.jetbrains.services.amazonqFeatureDev.messages.sendAnswer
1621
import software.aws.toolkits.jetbrains.services.amazonqFeatureDev.messages.sendAsyncEventProgress
1722
import software.aws.toolkits.jetbrains.services.amazonqFeatureDev.util.CancellationTokenSource
1823
import software.aws.toolkits.jetbrains.services.amazonqFeatureDev.util.FeatureDevService
1924
import software.aws.toolkits.jetbrains.services.amazonqFeatureDev.util.resolveAndCreateOrUpdateFile
2025
import software.aws.toolkits.jetbrains.services.amazonqFeatureDev.util.resolveAndDeleteFile
2126
import software.aws.toolkits.jetbrains.services.cwc.controller.ReferenceLogController
27+
import software.aws.toolkits.jetbrains.settings.CodeWhispererSettings
28+
import software.aws.toolkits.resources.message
2229

2330
class Session(val tabID: String, val project: Project) {
2431
var context: FeatureDevSessionContext
@@ -52,9 +59,9 @@ class Session(val tabID: String, val project: Project) {
5259
/**
5360
* Preload any events that have to run before a chat message can be sent
5461
*/
55-
suspend fun preloader(msg: String, messenger: MessagePublisher) {
62+
suspend fun preloader(messenger: MessagePublisher) {
5663
if (!preloaderFinished) {
57-
setupConversation(msg, messenger)
64+
setupConversation(messenger)
5865
preloaderFinished = true
5966
messenger.sendAsyncEventProgress(tabId = this.tabID, inProgress = true)
6067
featureDevService.sendFeatureDevEvent(this.conversationId)
@@ -64,10 +71,7 @@ class Session(val tabID: String, val project: Project) {
6471
/**
6572
* Starts a conversation with the backend and uploads the repo for the LLMs to be able to use it.
6673
*/
67-
private fun setupConversation(msg: String, messenger: MessagePublisher) {
68-
// Store the initial message when setting up the conversation so that if it fails we can retry with this message
69-
_latestMessage = msg
70-
74+
private fun setupConversation(messenger: MessagePublisher) {
7175
_conversationId = featureDevService.createConversation()
7276
logger<Session>().info(conversationIDLog(this.conversationId))
7377

@@ -159,8 +163,11 @@ class Session(val tabID: String, val project: Project) {
159163
}
160164
}
161165

162-
val latestMessage: String
166+
var latestMessage: String
163167
get() = this._latestMessage
168+
set(value) {
169+
this._latestMessage = value
170+
}
164171

165172
val retries: Int
166173
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: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,7 @@ import software.aws.toolkits.jetbrains.services.amazonqFeatureDev.util.FeatureDe
6262
import software.aws.toolkits.jetbrains.services.amazonqFeatureDev.util.getFollowUpOptions
6363
import software.aws.toolkits.jetbrains.services.amazonqFeatureDev.util.selectFolder
6464
import software.aws.toolkits.jetbrains.services.amazonqFeatureDev.util.uploadArtifactToS3
65+
import software.aws.toolkits.jetbrains.services.cwc.clients.chat.model.FollowUpType
6566
import software.aws.toolkits.resources.message
6667
import software.aws.toolkits.telemetry.AmazonqTelemetry
6768
import org.mockito.kotlin.verify as mockitoVerify
@@ -159,7 +160,7 @@ class FeatureDevControllerTest : FeatureDevTestBase() {
159160
every { AmazonqTelemetry.endChat(amazonqConversationId = any(), amazonqEndOfTheConversationLatency = any()) } just runs
160161

161162
runTest {
162-
spySession.preloader(userMessage, messenger)
163+
spySession.preloader(messenger)
163164
controller.processFollowupClickedMessage(message)
164165
}
165166

@@ -188,7 +189,7 @@ class FeatureDevControllerTest : FeatureDevTestBase() {
188189
mockkObject(AmazonqTelemetry)
189190
every { AmazonqTelemetry.isProvideFeedbackForCodeGen(amazonqConversationId = any(), enabled = any()) } just runs
190191

191-
spySession.preloader(userMessage, messenger)
192+
spySession.preloader(messenger)
192193
controller.processFollowupClickedMessage(message)
193194

194195
coVerifyOrder {
@@ -240,7 +241,7 @@ class FeatureDevControllerTest : FeatureDevTestBase() {
240241

241242
doNothing().`when`(spySession).insertChanges(any(), any(), any())
242243

243-
spySession.preloader(userMessage, messenger)
244+
spySession.preloader(messenger)
244245
controller.processFollowupClickedMessage(message)
245246

246247
mockitoVerify(
@@ -265,6 +266,7 @@ class FeatureDevControllerTest : FeatureDevTestBase() {
265266
listOf(
266267
FollowUp(FollowUpTypes.NEW_TASK, message("amazonqFeatureDev.follow_up.new_task"), status = FollowUpStatusType.Info),
267268
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)
268270
),
269271
)
270272
messenger.sendUpdatePlaceholder(testTabId, message("amazonqFeatureDev.placeholder.additional_improvements"))
@@ -447,7 +449,7 @@ class FeatureDevControllerTest : FeatureDevTestBase() {
447449
mockkStatic("software.aws.toolkits.jetbrains.services.amazonqFeatureDev.util.FileUtilsKt")
448450
every { selectFolder(any(), any()) } returns null
449451

450-
spySession.preloader(userMessage, messenger)
452+
spySession.preloader(messenger)
451453
controller.processFollowupClickedMessage(message)
452454

453455
coVerifyOrder {
@@ -478,7 +480,7 @@ class FeatureDevControllerTest : FeatureDevTestBase() {
478480
mockkStatic("software.aws.toolkits.jetbrains.services.amazonqFeatureDev.util.FileUtilsKt")
479481
every { selectFolder(any(), any()) } returns LightVirtualFile("/path")
480482

481-
spySession.preloader(userMessage, messenger)
483+
spySession.preloader(messenger)
482484
controller.processFollowupClickedMessage(message)
483485

484486
coVerifyOrder {
@@ -515,7 +517,7 @@ class FeatureDevControllerTest : FeatureDevTestBase() {
515517
mockkStatic("software.aws.toolkits.jetbrains.services.amazonqFeatureDev.util.FileUtilsKt")
516518
every { selectFolder(any(), any()) } returns folder
517519

518-
spySession.preloader(userMessage, messenger)
520+
spySession.preloader(messenger)
519521
controller.processFollowupClickedMessage(message)
520522

521523
coVerify {

0 commit comments

Comments
 (0)