@@ -5,8 +5,10 @@ package software.aws.toolkits.jetbrains.services.amazonqFeatureDev.controller
55
66import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
77import com.intellij.diff.DiffContentFactory
8- import com.intellij.diff.DiffManager
8+ import com.intellij.diff.chains.SimpleDiffRequestChain
99import com.intellij.diff.contents.EmptyContent
10+ import com.intellij.diff.editor.ChainDiffVirtualFile
11+ import com.intellij.diff.editor.DiffEditorTabFilesManager
1012import com.intellij.diff.requests.SimpleDiffRequest
1113import com.intellij.ide.BrowserUtil
1214import com.intellij.openapi.application.runInEdt
@@ -17,6 +19,7 @@ import com.intellij.openapi.fileEditor.FileEditorManager
1719import com.intellij.openapi.project.Project
1820import com.intellij.openapi.vfs.VfsUtil
1921import com.intellij.openapi.wm.ToolWindowManager
22+ import kotlinx.coroutines.delay
2023import kotlinx.coroutines.withContext
2124import software.amazon.awssdk.services.toolkittelemetry.model.Sentiment
2225import software.aws.toolkits.core.utils.debug
@@ -65,7 +68,10 @@ import software.aws.toolkits.jetbrains.services.amazonqFeatureDev.session.Prepar
6568import software.aws.toolkits.jetbrains.services.amazonqFeatureDev.session.Session
6669import software.aws.toolkits.jetbrains.services.amazonqFeatureDev.session.SessionStatePhase
6770import software.aws.toolkits.jetbrains.services.amazonqFeatureDev.storage.ChatSessionStorage
71+ import software.aws.toolkits.jetbrains.services.amazonqFeatureDev.util.InsertAction
72+ import software.aws.toolkits.jetbrains.services.amazonqFeatureDev.util.getFollowUpOptions
6873import software.aws.toolkits.jetbrains.services.amazonqFeatureDev.util.selectFolder
74+ import software.aws.toolkits.jetbrains.services.codewhisperer.util.content
6975import software.aws.toolkits.jetbrains.services.cwc.controller.chat.telemetry.FeedbackComment
7076import software.aws.toolkits.jetbrains.services.cwc.controller.chat.telemetry.getStartUrl
7177import software.aws.toolkits.jetbrains.services.telemetry.TelemetryService
@@ -86,13 +92,19 @@ class FeatureDevController(
8692 val messenger = context.messagesFromAppToUi
8793 val toolWindow = ToolWindowManager .getInstance(context.project).getToolWindow(AmazonQToolWindowFactory .WINDOW_ID )
8894
95+ private val diffVirtualFiles = mutableMapOf<String , ChainDiffVirtualFile >()
96+
8997 override suspend fun processPromptChatMessage (message : IncomingFeatureDevMessage .ChatPrompt ) {
9098 handleChat(
9199 tabId = message.tabId,
92100 message = message.chatMessage
93101 )
94102 }
95103
104+ override suspend fun processStoreCodeResultMessageId (message : IncomingFeatureDevMessage .StoreMessageIdMessage ) {
105+ storeCodeResultMessageId(message)
106+ }
107+
96108 override suspend fun processStopMessage (message : IncomingFeatureDevMessage .StopResponse ) {
97109 handleStopMessage(message)
98110 }
@@ -126,6 +138,7 @@ class FeatureDevController(
126138
127139 override suspend fun processChatItemVotedMessage (message : IncomingFeatureDevMessage .ChatItemVotedMessage ) {
128140 logger.debug { " $FEATURE_NAME : Processing ChatItemVotedMessage: $message " }
141+ this .disablePreviousFileList(message.tabId)
129142
130143 val session = chatSessionStorage.getSession(message.tabId, context.project)
131144 when (message.vote) {
@@ -192,6 +205,18 @@ class FeatureDevController(
192205 }
193206 }
194207
208+ private fun putDiff (filePath : String , request : SimpleDiffRequest ) {
209+ // Close any existing diff and open a new diff, as the diff virtual file does not appear to allow replacing content directly:
210+ val existingDiff = diffVirtualFiles[filePath]
211+ if (existingDiff != null ) {
212+ FileEditorManager .getInstance(context.project).closeFile(existingDiff)
213+ }
214+
215+ val newDiff = ChainDiffVirtualFile (SimpleDiffRequestChain (request), filePath)
216+ DiffEditorTabFilesManager .getInstance(context.project).showDiffFile(newDiff, true )
217+ diffVirtualFiles[filePath] = newDiff
218+ }
219+
195220 override suspend fun processOpenDiff (message : IncomingFeatureDevMessage .OpenDiff ) {
196221 val session = getSessionInfo(message.tabId)
197222
@@ -223,9 +248,7 @@ class FeatureDevController(
223248 DiffContentFactory .getInstance().create(newFileContent)
224249 }
225250
226- val request = SimpleDiffRequest (message.filePath, leftDiffContent, rightDiffContent, null , null )
227-
228- DiffManager .getInstance().showDiff(project, request)
251+ putDiff(message.filePath, SimpleDiffRequest (message.filePath, leftDiffContent, rightDiffContent, null , null ))
229252 }
230253 }
231254 else -> {
@@ -244,6 +267,7 @@ class FeatureDevController(
244267 val fileToUpdate = message.filePath
245268 val session = getSessionInfo(message.tabId)
246269 val messageId = message.messageId
270+ val action = message.actionName
247271
248272 var filePaths: List <NewFileZipInfo > = emptyList()
249273 var deletedFiles: List <DeletedFileInfo > = emptyList()
@@ -254,11 +278,69 @@ class FeatureDevController(
254278 }
255279 }
256280
257- // Mark the file as rejected or not depending on the previous state
258- filePaths.find { it.zipFilePath == fileToUpdate }?.let { it.rejected = ! it.rejected }
259- deletedFiles.find { it.zipFilePath == fileToUpdate }?.let { it.rejected = ! it.rejected }
281+ fun insertAction (): InsertAction =
282+ if (filePaths.all { it.changeApplied } && deletedFiles.all { it.changeApplied }) {
283+ InsertAction .AUTO_CONTINUE
284+ } else if (filePaths.all { it.changeApplied || it.rejected } && deletedFiles.all { it.changeApplied || it.rejected }) {
285+ InsertAction .CONTINUE
286+ } else if (filePaths.any { it.changeApplied || it.rejected } || deletedFiles.any { it.changeApplied || it.rejected }) {
287+ InsertAction .REMAINING
288+ } else {
289+ InsertAction .ALL
290+ }
291+
292+ val prevInsertAction = insertAction()
293+
294+ if (action == " accept-change" ) {
295+ session.insertChanges(
296+ filePaths = filePaths.filter { it.zipFilePath == fileToUpdate },
297+ deletedFiles = deletedFiles.filter { it.zipFilePath == fileToUpdate },
298+ references = emptyList(),
299+ messenger
300+ )
301+
302+ AmazonqTelemetry .isAcceptedCodeChanges(
303+ amazonqNumberOfFilesAccepted = 1.0 ,
304+ amazonqConversationId = session.conversationId,
305+ enabled = true ,
306+ credentialStartUrl = getStartUrl(project = context.project)
307+ )
308+ } else {
309+ // Mark the file as rejected or not depending on the previous state
310+ filePaths.find { it.zipFilePath == fileToUpdate }?.let { it.rejected = ! it.rejected }
311+ deletedFiles.find { it.zipFilePath == fileToUpdate }?.let { it.rejected = ! it.rejected }
312+ }
260313
314+ // Update the state of the tree view:
261315 messenger.updateFileComponent(message.tabId, filePaths, deletedFiles, messageId)
316+
317+ // Then, if the accepted file is not a deletion, open a diff to show the changes are applied:
318+ if (action == " accept-change" && deletedFiles.none { it.zipFilePath == fileToUpdate }) {
319+ var pollAttempt = 0
320+ val pollDelayMs = 10L
321+ while (pollAttempt < 5 ) {
322+ val file = VfsUtil .findRelativeFile(message.filePath, session.context.selectedSourceFolder)
323+ // Wait for the file to be created and/or updated to the new content:
324+ if (file != null && file.content() == filePaths.find { it.zipFilePath == fileToUpdate }?.fileContent) {
325+ // Open a diff, showing the changes have been applied and the file now has identical left/right state:
326+ this .processOpenDiff(IncomingFeatureDevMessage .OpenDiff (message.tabId, fileToUpdate, false ))
327+ break
328+ } else {
329+ pollAttempt++
330+ delay(pollDelayMs)
331+ }
332+ }
333+ }
334+
335+ val nextInsertAction = insertAction()
336+
337+ if (nextInsertAction == InsertAction .AUTO_CONTINUE ) {
338+ // Insert remaining changes (noop, as there are none), and advance to the next prompt:
339+ insertCode(message.tabId)
340+ } else if (nextInsertAction != prevInsertAction) {
341+ // Update the action displayed to the customer based on the current state:
342+ messenger.sendSystemPrompt(message.tabId, getFollowUpOptions(session.sessionState.phase, nextInsertAction))
343+ }
262344 }
263345
264346 private suspend fun newTabOpened (tabId : String ) {
@@ -325,8 +407,12 @@ class FeatureDevController(
325407 }
326408 }
327409
410+ val rejectedFilesCount = filePaths.count { it.rejected } + deletedFiles.count { it.rejected }
411+ val acceptedFilesCount = filePaths.count { it.changeApplied } + filePaths.count { it.changeApplied }
412+ val remainingFilesCount = filePaths.count() + deletedFiles.count() - acceptedFilesCount - rejectedFilesCount
413+
328414 AmazonqTelemetry .isAcceptedCodeChanges(
329- amazonqNumberOfFilesAccepted = (filePaths.filterNot { it.rejected }.size + deletedFiles.filterNot { it.rejected }.size) * 1.0 ,
415+ amazonqNumberOfFilesAccepted = remainingFilesCount * 1.0 ,
330416 amazonqConversationId = session.conversationId,
331417 enabled = true ,
332418 credentialStartUrl = getStartUrl(project = context.project)
@@ -335,7 +421,8 @@ class FeatureDevController(
335421 session.insertChanges(
336422 filePaths = filePaths.filterNot { it.rejected },
337423 deletedFiles = deletedFiles.filterNot { it.rejected },
338- references = references
424+ references = references,
425+ messenger
339426 )
340427
341428 messenger.sendAnswer(
@@ -377,8 +464,11 @@ class FeatureDevController(
377464 }
378465
379466 private suspend fun newTask (tabId : String , isException : Boolean? = false) {
467+ this .disablePreviousFileList(tabId)
468+
380469 val session = getSessionInfo(tabId)
381470 val sessionLatency = System .currentTimeMillis() - session.sessionStartTime
471+
382472 AmazonqTelemetry .endChat(
383473 amazonqConversationId = session.conversationId,
384474 amazonqEndOfTheConversationLatency = sessionLatency.toDouble(),
@@ -399,6 +489,7 @@ class FeatureDevController(
399489 }
400490
401491 private suspend fun closeSession (tabId : String ) {
492+ this .disablePreviousFileList(tabId)
402493 messenger.sendAnswer(
403494 tabId = tabId,
404495 messageType = FeatureDevMessageType .Answer ,
@@ -503,7 +594,7 @@ class FeatureDevController(
503594 tabId = tabId,
504595 followUp = listOf (
505596 FollowUp (
506- pillText = message(" amazonqFeatureDev.follow_up.insert_code " ),
597+ pillText = message(" amazonqFeatureDev.follow_up.insert_all_code " ),
507598 type = FollowUpTypes .INSERT_CODE ,
508599 icon = FollowUpIcons .Ok ,
509600 status = FollowUpStatusType .Success ,
@@ -546,11 +637,28 @@ class FeatureDevController(
546637 }
547638 }
548639
640+ private suspend fun disablePreviousFileList (tabId : String ) {
641+ val session = getSessionInfo(tabId)
642+ when (val sessionState = session.sessionState) {
643+ is PrepareCodeGenerationState -> {
644+ session.disableFileList(sessionState.filePaths, sessionState.deletedFiles, messenger)
645+ }
646+ }
647+ }
648+
649+ private fun storeCodeResultMessageId (message : IncomingFeatureDevMessage .StoreMessageIdMessage ) {
650+ val tabId = message.tabId
651+ val session = getSessionInfo(tabId)
652+ session.storeCodeResultMessageId(message)
653+ }
654+
549655 private suspend fun handleChat (
550656 tabId : String ,
551657 message : String ,
552658 ) {
553659 var session: Session ? = null
660+
661+ this .disablePreviousFileList(tabId)
554662 try {
555663 logger.debug { " $FEATURE_NAME : Processing message: $message " }
556664 session = getSessionInfo(tabId)
0 commit comments