@@ -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,84 @@ 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+ }
260293
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+ }
315+
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+
339+ println (" nextInsertAction: $filePaths , $deletedFiles " )
340+
341+ if (nextInsertAction == InsertAction .AUTO_CONTINUE ) {
342+ // Insert remaining changes (noop, as there are none), and advance to the next prompt:
343+ insertCode(message.tabId)
344+ } else if (nextInsertAction != prevInsertAction) {
345+ // Update the action displayed to the customer based on the current state:
346+ messenger.sendSystemPrompt(message.tabId, getFollowUpOptions(session.sessionState.phase, nextInsertAction))
347+ }
262348 }
263349
264350 private suspend fun newTabOpened (tabId : String ) {
@@ -308,7 +394,8 @@ class FeatureDevController(
308394 session.sessionState.token?.cancel()
309395 }
310396 }
311- private suspend fun insertCode (tabId : String ) {
397+
398+ suspend fun insertCode (tabId : String ) {
312399 var session: Session ? = null
313400 try {
314401 session = getSessionInfo(tabId)
@@ -325,17 +412,22 @@ class FeatureDevController(
325412 }
326413 }
327414
415+ val rejectedFilesCount = filePaths.count { it.rejected } + deletedFiles.count { it.rejected }
416+ val acceptedFilesCount = filePaths.count { it.changeApplied } + filePaths.count { it.changeApplied }
417+ val remainingFilesCount = filePaths.count() + deletedFiles.count() - acceptedFilesCount - rejectedFilesCount
418+
328419 AmazonqTelemetry .isAcceptedCodeChanges(
329- amazonqNumberOfFilesAccepted = (filePaths.filterNot { it.rejected }.size + deletedFiles.filterNot { it.rejected }.size) * 1.0 ,
420+ amazonqNumberOfFilesAccepted = remainingFilesCount * 1.0 ,
330421 amazonqConversationId = session.conversationId,
331422 enabled = true ,
332423 credentialStartUrl = getStartUrl(project = context.project)
333424 )
334425
335426 session.insertChanges(
336- filePaths = filePaths.filterNot { it.rejected },
337- deletedFiles = deletedFiles.filterNot { it.rejected },
338- references = references
427+ filePaths = filePaths.filterNot { it.rejected || it.changeApplied },
428+ deletedFiles = deletedFiles.filterNot { it.rejected || it.changeApplied },
429+ references = references,
430+ messenger
339431 )
340432
341433 messenger.sendAnswer(
@@ -377,8 +469,11 @@ class FeatureDevController(
377469 }
378470
379471 private suspend fun newTask (tabId : String , isException : Boolean? = false) {
472+ this .disablePreviousFileList(tabId)
473+
380474 val session = getSessionInfo(tabId)
381475 val sessionLatency = System .currentTimeMillis() - session.sessionStartTime
476+
382477 AmazonqTelemetry .endChat(
383478 amazonqConversationId = session.conversationId,
384479 amazonqEndOfTheConversationLatency = sessionLatency.toDouble(),
@@ -399,6 +494,7 @@ class FeatureDevController(
399494 }
400495
401496 private suspend fun closeSession (tabId : String ) {
497+ this .disablePreviousFileList(tabId)
402498 messenger.sendAnswer(
403499 tabId = tabId,
404500 messageType = FeatureDevMessageType .Answer ,
@@ -503,7 +599,7 @@ class FeatureDevController(
503599 tabId = tabId,
504600 followUp = listOf (
505601 FollowUp (
506- pillText = message(" amazonqFeatureDev.follow_up.insert_code " ),
602+ pillText = message(" amazonqFeatureDev.follow_up.insert_all_code " ),
507603 type = FollowUpTypes .INSERT_CODE ,
508604 icon = FollowUpIcons .Ok ,
509605 status = FollowUpStatusType .Success ,
@@ -546,11 +642,28 @@ class FeatureDevController(
546642 }
547643 }
548644
645+ private suspend fun disablePreviousFileList (tabId : String ) {
646+ val session = getSessionInfo(tabId)
647+ when (val sessionState = session.sessionState) {
648+ is PrepareCodeGenerationState -> {
649+ session.disableFileList(sessionState.filePaths, sessionState.deletedFiles, messenger)
650+ }
651+ }
652+ }
653+
654+ private fun storeCodeResultMessageId (message : IncomingFeatureDevMessage .StoreMessageIdMessage ) {
655+ val tabId = message.tabId
656+ val session = getSessionInfo(tabId)
657+ session.storeCodeResultMessageId(message)
658+ }
659+
549660 private suspend fun handleChat (
550661 tabId : String ,
551662 message : String ,
552663 ) {
553664 var session: Session ? = null
665+
666+ this .disablePreviousFileList(tabId)
554667 try {
555668 logger.debug { " $FEATURE_NAME : Processing message: $message " }
556669 session = getSessionInfo(tabId)
0 commit comments