@@ -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,21 +267,81 @@ 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()
274+ var references: List <CodeReferenceGenerated > = emptyList()
250275 when (val state = session.sessionState) {
251276 is PrepareCodeGenerationState -> {
252277 filePaths = state.filePaths
253278 deletedFiles = state.deletedFiles
279+ references = state.references
254280 }
255281 }
256282
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 }
283+ fun insertAction (): InsertAction =
284+ if (filePaths.all { it.changeApplied } && deletedFiles.all { it.changeApplied }) {
285+ InsertAction .AUTO_CONTINUE
286+ } else if (filePaths.all { it.changeApplied || it.rejected } && deletedFiles.all { it.changeApplied || it.rejected }) {
287+ InsertAction .CONTINUE
288+ } else if (filePaths.any { it.changeApplied || it.rejected } || deletedFiles.any { it.changeApplied || it.rejected }) {
289+ InsertAction .REMAINING
290+ } else {
291+ InsertAction .ALL
292+ }
293+
294+ val prevInsertAction = insertAction()
295+
296+ if (action == " accept-change" ) {
297+ session.insertChanges(
298+ filePaths = filePaths.filter { it.zipFilePath == fileToUpdate },
299+ deletedFiles = deletedFiles.filter { it.zipFilePath == fileToUpdate },
300+ references = references, // Add all references (not attributed per-file)
301+ messenger
302+ )
303+
304+ AmazonqTelemetry .isAcceptedCodeChanges(
305+ amazonqNumberOfFilesAccepted = 1.0 ,
306+ amazonqConversationId = session.conversationId,
307+ enabled = true ,
308+ credentialStartUrl = getStartUrl(project = context.project)
309+ )
310+ } else {
311+ // Mark the file as rejected or not depending on the previous state
312+ filePaths.find { it.zipFilePath == fileToUpdate }?.let { it.rejected = ! it.rejected }
313+ deletedFiles.find { it.zipFilePath == fileToUpdate }?.let { it.rejected = ! it.rejected }
314+ }
260315
316+ // Update the state of the tree view:
261317 messenger.updateFileComponent(message.tabId, filePaths, deletedFiles, messageId)
318+
319+ // Then, if the accepted file is not a deletion, open a diff to show the changes are applied:
320+ if (action == " accept-change" && deletedFiles.none { it.zipFilePath == fileToUpdate }) {
321+ var pollAttempt = 0
322+ val pollDelayMs = 10L
323+ while (pollAttempt < 5 ) {
324+ val file = VfsUtil .findRelativeFile(message.filePath, session.context.selectedSourceFolder)
325+ // Wait for the file to be created and/or updated to the new content:
326+ if (file != null && file.content() == filePaths.find { it.zipFilePath == fileToUpdate }?.fileContent) {
327+ // Open a diff, showing the changes have been applied and the file now has identical left/right state:
328+ this .processOpenDiff(IncomingFeatureDevMessage .OpenDiff (message.tabId, fileToUpdate, false ))
329+ break
330+ } else {
331+ pollAttempt++
332+ delay(pollDelayMs)
333+ }
334+ }
335+ }
336+
337+ val nextInsertAction = insertAction()
338+ if (nextInsertAction == InsertAction .AUTO_CONTINUE ) {
339+ // Insert remaining changes (noop, as there are none), and advance to the next prompt:
340+ insertCode(message.tabId)
341+ } else if (nextInsertAction != prevInsertAction) {
342+ // Update the action displayed to the customer based on the current state:
343+ messenger.sendSystemPrompt(message.tabId, getFollowUpOptions(session.sessionState.phase, nextInsertAction))
344+ }
262345 }
263346
264347 private suspend fun newTabOpened (tabId : String ) {
@@ -308,7 +391,8 @@ class FeatureDevController(
308391 session.sessionState.token?.cancel()
309392 }
310393 }
311- private suspend fun insertCode (tabId : String ) {
394+
395+ suspend fun insertCode (tabId : String ) {
312396 var session: Session ? = null
313397 try {
314398 session = getSessionInfo(tabId)
@@ -325,17 +409,22 @@ class FeatureDevController(
325409 }
326410 }
327411
412+ val rejectedFilesCount = filePaths.count { it.rejected } + deletedFiles.count { it.rejected }
413+ val acceptedFilesCount = filePaths.count { it.changeApplied } + filePaths.count { it.changeApplied }
414+ val remainingFilesCount = filePaths.count() + deletedFiles.count() - acceptedFilesCount - rejectedFilesCount
415+
328416 AmazonqTelemetry .isAcceptedCodeChanges(
329- amazonqNumberOfFilesAccepted = (filePaths.filterNot { it.rejected }.size + deletedFiles.filterNot { it.rejected }.size) * 1.0 ,
417+ amazonqNumberOfFilesAccepted = remainingFilesCount.toDouble() ,
330418 amazonqConversationId = session.conversationId,
331419 enabled = true ,
332420 credentialStartUrl = getStartUrl(project = context.project)
333421 )
334422
335423 session.insertChanges(
336- filePaths = filePaths.filterNot { it.rejected },
337- deletedFiles = deletedFiles.filterNot { it.rejected },
338- references = references
424+ filePaths = filePaths.filterNot { it.rejected || it.changeApplied },
425+ deletedFiles = deletedFiles.filterNot { it.rejected || it.changeApplied },
426+ references = references,
427+ messenger
339428 )
340429
341430 messenger.sendAnswer(
@@ -377,8 +466,11 @@ class FeatureDevController(
377466 }
378467
379468 private suspend fun newTask (tabId : String , isException : Boolean? = false) {
469+ this .disablePreviousFileList(tabId)
470+
380471 val session = getSessionInfo(tabId)
381472 val sessionLatency = System .currentTimeMillis() - session.sessionStartTime
473+
382474 AmazonqTelemetry .endChat(
383475 amazonqConversationId = session.conversationId,
384476 amazonqEndOfTheConversationLatency = sessionLatency.toDouble(),
@@ -399,6 +491,7 @@ class FeatureDevController(
399491 }
400492
401493 private suspend fun closeSession (tabId : String ) {
494+ this .disablePreviousFileList(tabId)
402495 messenger.sendAnswer(
403496 tabId = tabId,
404497 messageType = FeatureDevMessageType .Answer ,
@@ -503,7 +596,7 @@ class FeatureDevController(
503596 tabId = tabId,
504597 followUp = listOf (
505598 FollowUp (
506- pillText = message(" amazonqFeatureDev.follow_up.insert_code " ),
599+ pillText = message(" amazonqFeatureDev.follow_up.insert_all_code " ),
507600 type = FollowUpTypes .INSERT_CODE ,
508601 icon = FollowUpIcons .Ok ,
509602 status = FollowUpStatusType .Success ,
@@ -546,11 +639,28 @@ class FeatureDevController(
546639 }
547640 }
548641
642+ private suspend fun disablePreviousFileList (tabId : String ) {
643+ val session = getSessionInfo(tabId)
644+ when (val sessionState = session.sessionState) {
645+ is PrepareCodeGenerationState -> {
646+ session.disableFileList(sessionState.filePaths, sessionState.deletedFiles, messenger)
647+ }
648+ }
649+ }
650+
651+ private fun storeCodeResultMessageId (message : IncomingFeatureDevMessage .StoreMessageIdMessage ) {
652+ val tabId = message.tabId
653+ val session = getSessionInfo(tabId)
654+ session.storeCodeResultMessageId(message)
655+ }
656+
549657 private suspend fun handleChat (
550658 tabId : String ,
551659 message : String ,
552660 ) {
553661 var session: Session ? = null
662+
663+ this .disablePreviousFileList(tabId)
554664 try {
555665 logger.debug { " $FEATURE_NAME : Processing message: $message " }
556666 session = getSessionInfo(tabId)
0 commit comments