Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"type" : "feature",
"description" : "Add setting to allow Q /dev to run code and test commands"
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,9 @@ const val FEATURE_EVALUATION_PRODUCT_NAME = "FeatureDev"

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

@Suppress("MaxLineLength")
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\""

// Max number of times a user can attempt to retry a code generation request if it fails
const val CODE_GENERATION_RETRY_LIMIT = 3

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,11 +31,13 @@ import software.aws.toolkits.jetbrains.core.coroutines.EDT
import software.aws.toolkits.jetbrains.services.amazonq.RepoSizeError
import software.aws.toolkits.jetbrains.services.amazonq.apps.AmazonQAppInitContext
import software.aws.toolkits.jetbrains.services.amazonq.auth.AuthController
import software.aws.toolkits.jetbrains.services.amazonq.messages.MessagePublisher
import software.aws.toolkits.jetbrains.services.amazonq.toolwindow.AmazonQToolWindowFactory
import software.aws.toolkits.jetbrains.services.amazonqFeatureDev.CodeIterationLimitException
import software.aws.toolkits.jetbrains.services.amazonqFeatureDev.DEFAULT_RETRY_LIMIT
import software.aws.toolkits.jetbrains.services.amazonqFeatureDev.FEATURE_NAME
import software.aws.toolkits.jetbrains.services.amazonqFeatureDev.FeatureDevException
import software.aws.toolkits.jetbrains.services.amazonqFeatureDev.GENERATE_DEV_FILE_PROMPT
import software.aws.toolkits.jetbrains.services.amazonqFeatureDev.InboundAppMessagesHandler
import software.aws.toolkits.jetbrains.services.amazonqFeatureDev.ModifySourceFolderErrorReason
import software.aws.toolkits.jetbrains.services.amazonqFeatureDev.MonthlyConversationLimitError
Expand Down Expand Up @@ -75,6 +77,7 @@ import software.aws.toolkits.jetbrains.services.codewhisperer.util.content
import software.aws.toolkits.jetbrains.services.cwc.controller.chat.telemetry.FeedbackComment
import software.aws.toolkits.jetbrains.services.cwc.controller.chat.telemetry.getStartUrl
import software.aws.toolkits.jetbrains.services.telemetry.TelemetryService
import software.aws.toolkits.jetbrains.settings.CodeWhispererSettings
import software.aws.toolkits.jetbrains.ui.feedback.FeatureDevFeedbackDialog
import software.aws.toolkits.jetbrains.utils.notifyError
import software.aws.toolkits.resources.message
Expand Down Expand Up @@ -133,6 +136,16 @@ class FeatureDevController(
FollowUpTypes.PROVIDE_FEEDBACK_AND_REGENERATE_CODE -> provideFeedbackAndRegenerateCode(message.tabId)
FollowUpTypes.NEW_TASK -> newTask(message.tabId)
FollowUpTypes.CLOSE_SESSION -> closeSession(message.tabId)
FollowUpTypes.ACCEPT_AUTO_BUILD -> handleDevCommandUserSetting(message.tabId, true)
FollowUpTypes.DENY_AUTO_BUILD -> handleDevCommandUserSetting(message.tabId, false)
FollowUpTypes.GENERATE_DEV_FILE -> {
messenger.sendAnswer(
tabId = message.tabId,
messageType = FeatureDevMessageType.SystemPrompt,
message = message("amazonqFeatureDev.follow_up.generate_dev_file")

Check warning

Code scanning / QDJVMC

Usage of redundant or deprecated syntax or deprecated symbols

'message(String, vararg Any): String' is deprecated. Use extension-specific localization bundle instead
)
newTask(tabId = message.tabId, prefilledPrompt = GENERATE_DEV_FILE_PROMPT)
}
}
}

Expand Down Expand Up @@ -439,20 +452,38 @@ class FeatureDevController(
canBeVoted = true
)

messenger.sendSystemPrompt(
tabId = tabId,
followUp = listOf(
FollowUp(
pillText = message("amazonqFeatureDev.follow_up.new_task"),
type = FollowUpTypes.NEW_TASK,
status = FollowUpStatusType.Info
),
val followUps = mutableListOf(
FollowUp(
pillText = message("amazonqFeatureDev.follow_up.new_task"),

Check warning

Code scanning / QDJVMC

Usage of redundant or deprecated syntax or deprecated symbols

'message(String, vararg Any): String' is deprecated. Use extension-specific localization bundle instead
type = FollowUpTypes.NEW_TASK,
status = FollowUpStatusType.Info
),
FollowUp(
pillText = message("amazonqFeatureDev.follow_up.close_session"),
type = FollowUpTypes.CLOSE_SESSION,
status = FollowUpStatusType.Info
),
)

if (!session.context.checkForDevFile()) {
followUps.add(
FollowUp(
pillText = message("amazonqFeatureDev.follow_up.close_session"),
type = FollowUpTypes.CLOSE_SESSION,
pillText = message("amazonqFeatureDev.follow_up.generate_dev_file"),
type = FollowUpTypes.GENERATE_DEV_FILE,
status = FollowUpStatusType.Info
)
)

messenger.sendAnswer(
tabId = tabId,
message = message("amazonqFeatureDev.chat_message.generate_dev_file"),
messageType = FeatureDevMessageType.Answer
)
}

messenger.sendSystemPrompt(
tabId = tabId,
followUp = followUps
)

messenger.sendUpdatePlaceholder(
Expand All @@ -470,9 +501,7 @@ class FeatureDevController(
}
}

private suspend fun newTask(tabId: String, isException: Boolean? = false) {
this.disablePreviousFileList(tabId)

private suspend fun newTask(tabId: String, isException: Boolean? = false, prefilledPrompt: String? = null) {
val session = getSessionInfo(tabId)
val sessionLatency = System.currentTimeMillis() - session.sessionStartTime

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

newTabOpened(tabId)
if (isException != null && !isException) {
messenger.sendAnswer(
tabId = tabId,
messageType = FeatureDevMessageType.Answer,
message = message("amazonqFeatureDev.chat_message.ask_for_new_task")
)

if (prefilledPrompt != null && isException != null && !isException) {
handleChat(tabId = tabId, message = prefilledPrompt)
} else {
if (isException != null && !isException) {
messenger.sendAnswer(
tabId = tabId,
messageType = FeatureDevMessageType.Answer,
message = message("amazonqFeatureDev.chat_message.ask_for_new_task")
)
}
messenger.sendUpdatePlaceholder(tabId = tabId, newPlaceholder = message("amazonqFeatureDev.placeholder.new_plan"))
messenger.sendChatInputEnabledMessage(tabId = tabId, enabled = true)
}
messenger.sendUpdatePlaceholder(tabId = tabId, newPlaceholder = message("amazonqFeatureDev.placeholder.new_plan"))
messenger.sendChatInputEnabledMessage(tabId = tabId, enabled = true)
}

private suspend fun handleDevCommandUserSetting(tabId: String, value: Boolean) {
CodeWhispererSettings.getInstance().toggleAutoBuildFeature(context.project.basePath, value)
messenger.sendAnswer(
tabId = tabId,
message = message("amazonqFeatureDev.chat_message.setting_updated"),
messageType = FeatureDevMessageType.Answer,
)
this.retryRequests(tabId)
}

private suspend fun closeSession(tabId: String) {
Expand Down Expand Up @@ -669,6 +713,7 @@ class FeatureDevController(
try {
logger.debug { "$FEATURE_NAME: Processing message: $message" }
session = getSessionInfo(tabId)
session.latestMessage = message

val credentialState = authController.getAuthNeededStates(context.project).amazonQ
if (credentialState != null) {
Expand All @@ -681,7 +726,16 @@ class FeatureDevController(
return
}

session.preloader(message, messenger)
val codeWhispererSettings = CodeWhispererSettings.getInstance().getAutoBuildFeatureConfiguration()
val hasDevFile = session.context.checkForDevFile()
val isPromptedForAutoBuildFeature = codeWhispererSettings.containsKey(session.context.getWorkspaceRoot())

if (hasDevFile && !isPromptedForAutoBuildFeature) {
promptAllowQCommandsConsent(messenger, tabId)
return
}

session.preloader(messenger)

when (session.sessionState.phase) {
SessionStatePhase.CODEGEN -> onCodeGeneration(session, message, tabId)
Expand All @@ -695,6 +749,30 @@ class FeatureDevController(
}
}

private suspend fun promptAllowQCommandsConsent(messenger: MessagePublisher, tabID: String) {
messenger.sendAnswer(
tabId = tabID,
message = message("amazonqFeatureDev.chat_message.devFileInRepository"),
messageType = FeatureDevMessageType.Answer
)
messenger.sendAnswer(
tabId = tabID,
messageType = FeatureDevMessageType.SystemPrompt,
followUp = listOf(
FollowUp(
pillText = message("amazonqFeatureDev.follow_up.accept_for_project"),
type = FollowUpTypes.ACCEPT_AUTO_BUILD,
status = FollowUpStatusType.Success
),
FollowUp(
pillText = message("amazonqFeatureDev.follow_up.decline_for_project"),

Check warning

Code scanning / QDJVMC

Usage of redundant or deprecated syntax or deprecated symbols

'message(String, vararg Any): String' is deprecated. Use extension-specific localization bundle instead
type = FollowUpTypes.DENY_AUTO_BUILD,
status = FollowUpStatusType.Error
)
)
)
}

private suspend fun retryRequests(tabId: String) {
var session: Session? = null
try {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -247,6 +247,9 @@ enum class FollowUpTypes(
PROVIDE_FEEDBACK_AND_REGENERATE_CODE("ProvideFeedbackAndRegenerateCode"),
NEW_TASK("NewTask"),
CLOSE_SESSION("CloseSession"),
ACCEPT_AUTO_BUILD("AcceptAutoBuild"),
DENY_AUTO_BUILD("DenyAutoBuild"),
GENERATE_DEV_FILE("GenerateDevFile"),
}

// Util classes
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import software.aws.toolkits.jetbrains.services.amazonqFeatureDev.util.Cancellat
import software.aws.toolkits.jetbrains.services.amazonqFeatureDev.util.deleteUploadArtifact
import software.aws.toolkits.jetbrains.services.amazonqFeatureDev.util.uploadArtifactToS3
import software.aws.toolkits.jetbrains.services.cwc.controller.chat.telemetry.getStartUrl
import software.aws.toolkits.jetbrains.settings.CodeWhispererSettings
import software.aws.toolkits.resources.message
import software.aws.toolkits.telemetry.AmazonqTelemetry
import software.aws.toolkits.telemetry.AmazonqUploadIntent
Expand Down Expand Up @@ -47,7 +48,8 @@ class PrepareCodeGenerationState(
messenger.sendAnswerPart(tabId = this.tabID, message = message("amazonqFeatureDev.chat_message.uploading_code"))
messenger.sendUpdatePlaceholder(tabId = this.tabID, newPlaceholder = message("amazonqFeatureDev.chat_message.uploading_code"))

val repoZipResult = config.repoContext.getProjectZip()
val isAutoBuildFeatureEnabled = CodeWhispererSettings.getInstance().isAutoBuildFeatureEnabled(this.config.repoContext.getWorkspaceRoot())
val repoZipResult = config.repoContext.getProjectZip(isAutoBuildFeatureEnabled = isAutoBuildFeatureEnabled)
val zipFileChecksum = repoZipResult.checksum
zipFileLength = repoZipResult.contentLength
val fileToUpload = repoZipResult.payload
Expand Down Expand Up @@ -94,7 +96,7 @@ class PrepareCodeGenerationState(
credentialStartUrl = getStartUrl(config.featureDevService.project)
)
}
// It is essential to interact with the next state outside of try-catch block for the telemetry to capture events for the states separately
// It is essential to interact with the next state outside try-catch block for the telemetry to capture events for the states separately
return nextState.interact(action)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -55,9 +55,9 @@ class Session(val tabID: String, val project: Project) {
/**
* Preload any events that have to run before a chat message can be sent
*/
suspend fun preloader(msg: String, messenger: MessagePublisher) {
suspend fun preloader(messenger: MessagePublisher) {
if (!preloaderFinished) {
setupConversation(msg, messenger)
setupConversation(messenger)
preloaderFinished = true
messenger.sendAsyncEventProgress(tabId = this.tabID, inProgress = true)
featureDevService.sendFeatureDevEvent(this.conversationId)
Expand All @@ -67,10 +67,7 @@ class Session(val tabID: String, val project: Project) {
/**
* Starts a conversation with the backend and uploads the repo for the LLMs to be able to use it.
*/
private fun setupConversation(msg: String, messenger: MessagePublisher) {
// Store the initial message when setting up the conversation so that if it fails we can retry with this message
_latestMessage = msg

private fun setupConversation(messenger: MessagePublisher) {
_conversationId = featureDevService.createConversation()
logger<Session>().info(conversationIDLog(this.conversationId))

Expand Down Expand Up @@ -204,8 +201,11 @@ class Session(val tabID: String, val project: Project) {
}
}

val latestMessage: String
var latestMessage: String
get() = this._latestMessage
set(value) {
this._latestMessage = value
}

val retries: Int
get() = codegenRetries
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ class FeatureDevSessionContextTest : FeatureDevTestBase() {
@Test
fun testWithInvalidFile() {
val txtFile = mock<VirtualFile>()
whenever(txtFile.extension).thenReturn("txt")
whenever(txtFile.extension).thenReturn("mp4")
assertFalse(featureDevSessionContext.isFileExtensionAllowed(txtFile))
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -160,7 +160,7 @@ class FeatureDevControllerTest : FeatureDevTestBase() {
every { AmazonqTelemetry.endChat(amazonqConversationId = any(), amazonqEndOfTheConversationLatency = any()) } just runs

runTest {
spySession.preloader(userMessage, messenger)
spySession.preloader(messenger)
controller.processFollowupClickedMessage(message)
}

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

spySession.preloader(userMessage, messenger)
spySession.preloader(messenger)
controller.processFollowupClickedMessage(message)

coVerifyOrder {
Expand Down Expand Up @@ -241,7 +241,7 @@ class FeatureDevControllerTest : FeatureDevTestBase() {

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

spySession.preloader(userMessage, messenger)
spySession.preloader(messenger)
controller.processFollowupClickedMessage(message)

mockitoVerify(
Expand All @@ -266,6 +266,7 @@ class FeatureDevControllerTest : FeatureDevTestBase() {
listOf(
FollowUp(FollowUpTypes.NEW_TASK, message("amazonqFeatureDev.follow_up.new_task"), status = FollowUpStatusType.Info),
FollowUp(FollowUpTypes.CLOSE_SESSION, message("amazonqFeatureDev.follow_up.close_session"), status = FollowUpStatusType.Info),
FollowUp(FollowUpTypes.GENERATE_DEV_FILE, message("amazonqFeatureDev.follow_up.generate_dev_file"), status = FollowUpStatusType.Info)
),
)
messenger.sendUpdatePlaceholder(testTabId, message("amazonqFeatureDev.placeholder.additional_improvements"))
Expand Down Expand Up @@ -565,7 +566,7 @@ class FeatureDevControllerTest : FeatureDevTestBase() {
mockkStatic("software.aws.toolkits.jetbrains.services.amazonqFeatureDev.util.FileUtilsKt")
every { selectFolder(any(), any()) } returns null

spySession.preloader(userMessage, messenger)
spySession.preloader(messenger)
controller.processFollowupClickedMessage(message)

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

spySession.preloader(userMessage, messenger)
spySession.preloader(messenger)
controller.processFollowupClickedMessage(message)

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

spySession.preloader(userMessage, messenger)
spySession.preloader(messenger)
controller.processFollowupClickedMessage(message)

coVerify {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -87,14 +87,14 @@ class PrepareCodeGenerationStateTest : FeatureDevTestBase() {
val repoZipResult = ZipCreationResult(mockFile, testChecksumSha, testContentLength)
val action = SessionStateAction("test-task", userMessage)

whenever(repoContext.getProjectZip()).thenReturn(repoZipResult)
whenever(repoContext.getProjectZip(false)).thenReturn(repoZipResult)
every { featureDevService.createUploadUrl(any(), any(), any(), any()) } returns exampleCreateUploadUrlResponse

runTest {
val actual = prepareCodeGenerationState.interact(action)
assertThat(actual.nextState).isInstanceOf(PrepareCodeGenerationState::class.java)
}
assertThat(prepareCodeGenerationState.phase).isEqualTo(SessionStatePhase.CODEGEN)
verify(repoContext, times(1)).getProjectZip()
verify(repoContext, times(1)).getProjectZip(false)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ class SessionTest : FeatureDevTestBase() {
fun `test preloader`() = runTest {
whenever(featureDevClient.createTaskAssistConversation()).thenReturn(exampleCreateTaskAssistConversationResponse)

session.preloader(userMessage, messenger)
session.preloader(messenger)
assertThat(session.conversationId).isEqualTo(testConversationId)
assertThat(session.sessionState).isInstanceOf(PrepareCodeGenerationState::class.java)
verify(featureDevClient, times(1)).createTaskAssistConversation()
Expand Down
Loading
Loading