Skip to content

Commit f83c2e0

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

File tree

16 files changed

+196
-46
lines changed

16 files changed

+196
-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/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: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -52,9 +52,9 @@ class Session(val tabID: String, val project: Project) {
5252
/**
5353
* Preload any events that have to run before a chat message can be sent
5454
*/
55-
suspend fun preloader(msg: String, messenger: MessagePublisher) {
55+
suspend fun preloader(messenger: MessagePublisher) {
5656
if (!preloaderFinished) {
57-
setupConversation(msg, messenger)
57+
setupConversation(messenger)
5858
preloaderFinished = true
5959
messenger.sendAsyncEventProgress(tabId = this.tabID, inProgress = true)
6060
featureDevService.sendFeatureDevEvent(this.conversationId)
@@ -64,10 +64,7 @@ class Session(val tabID: String, val project: Project) {
6464
/**
6565
* Starts a conversation with the backend and uploads the repo for the LLMs to be able to use it.
6666
*/
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-
67+
private fun setupConversation(messenger: MessagePublisher) {
7168
_conversationId = featureDevService.createConversation()
7269
logger<Session>().info(conversationIDLog(this.conversationId))
7370

@@ -159,8 +156,11 @@ class Session(val tabID: String, val project: Project) {
159156
}
160157
}
161158

162-
val latestMessage: String
159+
var latestMessage: String
163160
get() = this._latestMessage
161+
set(value) {
162+
this._latestMessage = value
163+
}
164164

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

161161
runTest {
162-
spySession.preloader(userMessage, messenger)
162+
spySession.preloader(messenger)
163163
controller.processFollowupClickedMessage(message)
164164
}
165165

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

191-
spySession.preloader(userMessage, messenger)
191+
spySession.preloader(messenger)
192192
controller.processFollowupClickedMessage(message)
193193

194194
coVerifyOrder {
@@ -240,7 +240,7 @@ class FeatureDevControllerTest : FeatureDevTestBase() {
240240

241241
doNothing().`when`(spySession).insertChanges(any(), any(), any())
242242

243-
spySession.preloader(userMessage, messenger)
243+
spySession.preloader(messenger)
244244
controller.processFollowupClickedMessage(message)
245245

246246
mockitoVerify(
@@ -265,6 +265,7 @@ class FeatureDevControllerTest : FeatureDevTestBase() {
265265
listOf(
266266
FollowUp(FollowUpTypes.NEW_TASK, message("amazonqFeatureDev.follow_up.new_task"), status = FollowUpStatusType.Info),
267267
FollowUp(FollowUpTypes.CLOSE_SESSION, message("amazonqFeatureDev.follow_up.close_session"), status = FollowUpStatusType.Info),
268+
FollowUp(FollowUpTypes.GENERATE_DEV_FILE, message("amazonqFeatureDev.follow_up.generate_dev_file"), status = FollowUpStatusType.Info)
268269
),
269270
)
270271
messenger.sendUpdatePlaceholder(testTabId, message("amazonqFeatureDev.placeholder.additional_improvements"))
@@ -447,7 +448,7 @@ class FeatureDevControllerTest : FeatureDevTestBase() {
447448
mockkStatic("software.aws.toolkits.jetbrains.services.amazonqFeatureDev.util.FileUtilsKt")
448449
every { selectFolder(any(), any()) } returns null
449450

450-
spySession.preloader(userMessage, messenger)
451+
spySession.preloader(messenger)
451452
controller.processFollowupClickedMessage(message)
452453

453454
coVerifyOrder {
@@ -478,7 +479,7 @@ class FeatureDevControllerTest : FeatureDevTestBase() {
478479
mockkStatic("software.aws.toolkits.jetbrains.services.amazonqFeatureDev.util.FileUtilsKt")
479480
every { selectFolder(any(), any()) } returns LightVirtualFile("/path")
480481

481-
spySession.preloader(userMessage, messenger)
482+
spySession.preloader(messenger)
482483
controller.processFollowupClickedMessage(message)
483484

484485
coVerifyOrder {
@@ -515,7 +516,7 @@ class FeatureDevControllerTest : FeatureDevTestBase() {
515516
mockkStatic("software.aws.toolkits.jetbrains.services.amazonqFeatureDev.util.FileUtilsKt")
516517
every { selectFolder(any(), any()) } returns folder
517518

518-
spySession.preloader(userMessage, messenger)
519+
spySession.preloader(messenger)
519520
controller.processFollowupClickedMessage(message)
520521

521522
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
@@ -57,7 +57,7 @@ class SessionTest : FeatureDevTestBase() {
5757
fun `test preloader`() = runTest {
5858
whenever(featureDevClient.createTaskAssistConversation()).thenReturn(exampleCreateTaskAssistConversationResponse)
5959

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

plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/settings/CodeWhispererConfigurable.kt

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,29 @@ class CodeWhispererConfigurable(private val project: Project) :
113113
}
114114
}
115115

116+
group("Amazon Q: Allow Q /dev to run code and test commands") {
117+
row {
118+
val settings = codeWhispererSettings.getAutoBuildFeatureConfiguration()
119+
for ((key) in settings) {
120+
checkBox(key).apply {
121+
connect.subscribe(
122+
ToolkitConnectionManagerListener.TOPIC,
123+
object : ToolkitConnectionManagerListener {
124+
override fun activeConnectionChanged(newConnection: ToolkitConnection?) {
125+
enabled(isCodeWhispererEnabled(project))
126+
}
127+
}
128+
)
129+
130+
bindSelected(
131+
getter = { codeWhispererSettings.isAutoBuildFeatureEnabled(key) },
132+
setter = { newValue -> codeWhispererSettings.toggleAutoBuildFeature(key, newValue) }
133+
)
134+
}
135+
}
136+
}
137+
}
138+
116139
group(message("aws.settings.codewhisperer.group.q_chat")) {
117140
row {
118141
checkBox(message("aws.settings.codewhisperer.project_context")).apply {

0 commit comments

Comments
 (0)