Skip to content

Commit 37f05ce

Browse files
committed
feat(feature dev): Add setting to allow Q to run code and test commands
1 parent 5ef4624 commit 37f05ce

File tree

12 files changed

+177
-30
lines changed

12 files changed

+177
-30
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: 59 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ import software.aws.toolkits.jetbrains.services.amazonqFeatureDev.CodeIterationL
3333
import software.aws.toolkits.jetbrains.services.amazonqFeatureDev.DEFAULT_RETRY_LIMIT
3434
import software.aws.toolkits.jetbrains.services.amazonqFeatureDev.FEATURE_NAME
3535
import software.aws.toolkits.jetbrains.services.amazonqFeatureDev.FeatureDevException
36+
import software.aws.toolkits.jetbrains.services.amazonqFeatureDev.GENERATE_DEV_FILE_PROMPT
3637
import software.aws.toolkits.jetbrains.services.amazonqFeatureDev.InboundAppMessagesHandler
3738
import software.aws.toolkits.jetbrains.services.amazonqFeatureDev.ModifySourceFolderErrorReason
3839
import software.aws.toolkits.jetbrains.services.amazonqFeatureDev.MonthlyConversationLimitError
@@ -69,6 +70,7 @@ import software.aws.toolkits.jetbrains.services.amazonqFeatureDev.util.selectFol
6970
import software.aws.toolkits.jetbrains.services.cwc.controller.chat.telemetry.FeedbackComment
7071
import software.aws.toolkits.jetbrains.services.cwc.controller.chat.telemetry.getStartUrl
7172
import software.aws.toolkits.jetbrains.services.telemetry.TelemetryService
73+
import software.aws.toolkits.jetbrains.settings.CodeWhispererSettings
7274
import software.aws.toolkits.jetbrains.ui.feedback.FeatureDevFeedbackDialog
7375
import software.aws.toolkits.jetbrains.utils.notifyError
7476
import software.aws.toolkits.resources.message
@@ -121,6 +123,9 @@ class FeatureDevController(
121123
FollowUpTypes.PROVIDE_FEEDBACK_AND_REGENERATE_CODE -> provideFeedbackAndRegenerateCode(message.tabId)
122124
FollowUpTypes.NEW_TASK -> newTask(message.tabId)
123125
FollowUpTypes.CLOSE_SESSION -> closeSession(message.tabId)
126+
FollowUpTypes.ACCEPT_AUTO_BUILD -> handleDevCommandUserSetting(message.tabId, true)
127+
FollowUpTypes.DENY_AUTO_BUILD -> handleDevCommandUserSetting(message.tabId, false)
128+
FollowUpTypes.GENERATE_DEV_FILE -> newTask(tabId = message.tabId, prefilledPrompt = GENERATE_DEV_FILE_PROMPT)
124129
}
125130
}
126131

@@ -345,20 +350,38 @@ class FeatureDevController(
345350
canBeVoted = true
346351
)
347352

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-
),
353+
val followUps = mutableListOf(
354+
FollowUp(
355+
pillText = message("amazonqFeatureDev.follow_up.new_task"),
356+
type = FollowUpTypes.NEW_TASK,
357+
status = FollowUpStatusType.Info
358+
),
359+
FollowUp(
360+
pillText = message("amazonqFeatureDev.follow_up.close_session"),
361+
type = FollowUpTypes.CLOSE_SESSION,
362+
status = FollowUpStatusType.Info
363+
),
364+
)
365+
366+
if (!session.context.checkForDevFile()) {
367+
followUps.add(
356368
FollowUp(
357-
pillText = message("amazonqFeatureDev.follow_up.close_session"),
358-
type = FollowUpTypes.CLOSE_SESSION,
369+
pillText = message("amazonqFeatureDev.follow_up.generate_dev_file"),
370+
type = FollowUpTypes.GENERATE_DEV_FILE,
359371
status = FollowUpStatusType.Info
360372
)
361373
)
374+
375+
messenger.sendAnswer(
376+
tabId = tabId,
377+
message = message("amazonqFeatureDev.chat_message.generate_dev_file"),
378+
messageType = FeatureDevMessageType.Answer
379+
)
380+
}
381+
382+
messenger.sendSystemPrompt(
383+
tabId = tabId,
384+
followUp = followUps
362385
)
363386

364387
messenger.sendUpdatePlaceholder(
@@ -376,7 +399,7 @@ class FeatureDevController(
376399
}
377400
}
378401

379-
private suspend fun newTask(tabId: String, isException: Boolean? = false) {
402+
private suspend fun newTask(tabId: String, isException: Boolean? = false, prefilledPrompt: String? = null) {
380403
val session = getSessionInfo(tabId)
381404
val sessionLatency = System.currentTimeMillis() - session.sessionStartTime
382405
AmazonqTelemetry.endChat(
@@ -387,15 +410,35 @@ class FeatureDevController(
387410
chatSessionStorage.deleteSession(tabId)
388411

389412
newTabOpened(tabId)
390-
if (isException != null && !isException) {
413+
414+
if (prefilledPrompt != null && isException != null && !isException) {
391415
messenger.sendAnswer(
392416
tabId = tabId,
393-
messageType = FeatureDevMessageType.Answer,
394-
message = message("amazonqFeatureDev.chat_message.ask_for_new_task")
417+
messageType = FeatureDevMessageType.SystemPrompt,
418+
message = message("amazonqFeatureDev.follow_up.generate_dev_file")
395419
)
420+
handleChat(tabId = tabId, message = prefilledPrompt)
421+
} else {
422+
if (isException != null && !isException) {
423+
messenger.sendAnswer(
424+
tabId = tabId,
425+
messageType = FeatureDevMessageType.Answer,
426+
message = message("amazonqFeatureDev.chat_message.ask_for_new_task")
427+
)
428+
}
429+
messenger.sendUpdatePlaceholder(tabId = tabId, newPlaceholder = message("amazonqFeatureDev.placeholder.new_plan"))
430+
messenger.sendChatInputEnabledMessage(tabId = tabId, enabled = true)
396431
}
397-
messenger.sendUpdatePlaceholder(tabId = tabId, newPlaceholder = message("amazonqFeatureDev.placeholder.new_plan"))
398-
messenger.sendChatInputEnabledMessage(tabId = tabId, enabled = true)
432+
}
433+
434+
private suspend fun handleDevCommandUserSetting(tabId: String, value: Boolean) {
435+
CodeWhispererSettings.getInstance().toggleAutoBuildFeature(context.project.basePath, value)
436+
messenger.sendAnswer(
437+
tabId = tabId,
438+
message = message("amazonqFeatureDev.chat_message.setting_updated"),
439+
messageType = FeatureDevMessageType.Answer,
440+
)
441+
this.retryRequests(tabId)
399442
}
400443

401444
private suspend fun closeSession(tabId: String) {

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.projectRoot.path)
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: 41 additions & 6 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
@@ -53,21 +60,49 @@ class Session(val tabID: String, val project: Project) {
5360
* Preload any events that have to run before a chat message can be sent
5461
*/
5562
suspend fun preloader(msg: String, messenger: MessagePublisher) {
56-
if (!preloaderFinished) {
57-
setupConversation(msg, messenger)
63+
// Store the initial message when setting up the conversation so that if it fails we can retry with this message
64+
_latestMessage = msg
65+
66+
val codeWhispererSettings = CodeWhispererSettings.getInstance().getAutoBuildFeatureConfiguration()
67+
val hasDevFile = context.checkForDevFile()
68+
if (hasDevFile && !codeWhispererSettings.containsKey(context.projectRoot.path)) {
69+
promptAllowQCommandsConsent(messenger)
70+
} else if (!preloaderFinished) {
71+
setupConversation(messenger)
5872
preloaderFinished = true
5973
messenger.sendAsyncEventProgress(tabId = this.tabID, inProgress = true)
6074
featureDevService.sendFeatureDevEvent(this.conversationId)
6175
}
6276
}
6377

78+
private suspend fun promptAllowQCommandsConsent(messenger: MessagePublisher) {
79+
messenger.sendAnswer(
80+
tabId = this.tabID,
81+
message = message("amazonqFeatureDev.chat_message.devFileInRepository"),
82+
messageType = FeatureDevMessageType.Answer
83+
)
84+
messenger.sendAnswer(
85+
tabId = this.tabID,
86+
messageType = FeatureDevMessageType.SystemPrompt,
87+
followUp = listOf(
88+
FollowUp(
89+
pillText = message("amazonqFeatureDev.follow_up.accept_for_project"),
90+
type = FollowUpTypes.ACCEPT_AUTO_BUILD,
91+
status = FollowUpStatusType.Success
92+
),
93+
FollowUp(
94+
pillText = message("amazonqFeatureDev.follow_up.decline_for_project"),
95+
type = FollowUpTypes.DENY_AUTO_BUILD,
96+
status = FollowUpStatusType.Error
97+
)
98+
)
99+
)
100+
}
101+
64102
/**
65103
* Starts a conversation with the backend and uploads the repo for the LLMs to be able to use it.
66104
*/
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-
105+
private fun setupConversation(messenger: MessagePublisher) {
71106
_conversationId = featureDevService.createConversation()
72107
logger<Session>().info(conversationIDLog(this.conversationId))
73108

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 {

plugins/amazonq/shared/jetbrains-community/src/software/aws/toolkits/jetbrains/settings/CodeWhispererSettings.kt

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,16 @@ class CodeWhispererSettings : PersistentStateComponent<CodeWhispererConfiguratio
2727
false
2828
)
2929

30+
fun getAutoBuildFeatureConfiguration() = state.projectAutoBuildConfigurationMap
31+
32+
fun toggleAutoBuildFeature(project: String?, value: Boolean) {
33+
if (project == null) return
34+
35+
state.projectAutoBuildConfigurationMap[project] = value
36+
}
37+
38+
fun isAutoBuildFeatureEnabled(project: String?) = state.projectAutoBuildConfigurationMap.getOrDefault(project, false)
39+
3040
fun toggleImportAdder(value: Boolean) {
3141
state.value[CodeWhispererConfigurationType.IsImportAdderEnabled] = value
3242
}
@@ -115,6 +125,7 @@ class CodeWhispererConfiguration : BaseState() {
115125
@get:Property
116126
val value by map<CodeWhispererConfigurationType, Boolean>()
117127
val intValue by map<CodeWhispererIntConfigurationType, Int>()
128+
val projectAutoBuildConfigurationMap by map<String, Boolean>()
118129
}
119130

120131
enum class CodeWhispererConfigurationType {

plugins/core/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/FeatureDevSessionContext.kt

Lines changed: 15 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -46,8 +46,6 @@ class FeatureDevSessionContext(val project: Project, val maxProjectSizeBytes: Lo
4646
"\\.svn",
4747
"\\.hg/?",
4848
"\\.rvm",
49-
"\\.git/?",
50-
"\\.gitignore",
5149
"\\.project",
5250
"\\.gem",
5351
"/\\.idea/?",
@@ -71,7 +69,7 @@ class FeatureDevSessionContext(val project: Project, val maxProjectSizeBytes: Lo
7169
// projectRoot: is the directory where the project is located when selected to open a project.
7270
val projectRoot = project.guessProjectDir() ?: error("Cannot guess base directory for project ${project.name}")
7371

74-
// selectedSourceFolder": is the directory selected in replacement of the root, this happens when the project is too big to bundle for uploading.
72+
// selectedSourceFolder: is the directory selected in replacement of the root, this happens when the project is too big to bundle for uploading.
7573
private var _selectedSourceFolder = projectRoot
7674
private var ignorePatternsWithGitIgnore = emptyList<Regex>()
7775
private val gitIgnoreFile = File(selectedSourceFolder.path, ".gitignore")
@@ -80,10 +78,15 @@ class FeatureDevSessionContext(val project: Project, val maxProjectSizeBytes: Lo
8078
ignorePatternsWithGitIgnore = (ignorePatterns + parseGitIgnore().map { Regex(it) }).toList()
8179
}
8280

83-
fun getProjectZip(): ZipCreationResult {
81+
fun checkForDevFile(): Boolean {
82+
val devFile = File(projectRoot.path, "/devfile.yaml")
83+
return devFile.exists()
84+
}
85+
86+
fun getProjectZip(isAutoBuildFeatureEnabled: Boolean?): ZipCreationResult {
8487
val zippedProject = runBlocking {
8588
withBackgroundProgress(project, AwsCoreBundle.message("amazonqFeatureDev.placeholder.generating_code")) {
86-
zipFiles(selectedSourceFolder)
89+
zipFiles(selectedSourceFolder, isAutoBuildFeatureEnabled)
8790
}
8891
}
8992
val checkSum256: String = Base64.getEncoder().encodeToString(DigestUtils.sha256(FileInputStream(zippedProject)))
@@ -117,7 +120,7 @@ class FeatureDevSessionContext(val project: Project, val maxProjectSizeBytes: Lo
117120
return deferredResults.any { it.await() }
118121
}
119122

120-
suspend fun zipFiles(projectRoot: VirtualFile): File = withContext(getCoroutineBgContext()) {
123+
suspend fun zipFiles(projectRoot: VirtualFile, isAutoBuildFeatureEnabled: Boolean?): File = withContext(getCoroutineBgContext()) {
121124
val files = mutableListOf<VirtualFile>()
122125
val ignoredExtensionMap = mutableMapOf<String, Long>().withDefault { 0L }
123126
var totalSize: Long = 0
@@ -127,11 +130,17 @@ class FeatureDevSessionContext(val project: Project, val maxProjectSizeBytes: Lo
127130
object : VirtualFileVisitor<Unit>() {
128131
override fun visitFile(file: VirtualFile): Boolean {
129132
val isFileIgnoredByExtension = runBlocking { ignoreFileByExtension(file) }
133+
val isDevFile = if (isAutoBuildFeatureEnabled == true) false else file.path.contains("devfile.yaml")
130134
if (isFileIgnoredByExtension) {
131135
val extension = file.extension.orEmpty()
132136
ignoredExtensionMap[extension] = (ignoredExtensionMap[extension] ?: 0) + 1
133137
return false
134138
}
139+
140+
if (isDevFile) {
141+
return false
142+
}
143+
135144
val isFileIgnoredByPattern = runBlocking { ignoreFile(file.name) }
136145
if (isFileIgnoredByPattern) {
137146
return false

0 commit comments

Comments
 (0)