@@ -5,8 +5,10 @@ package software.aws.toolkits.jetbrains.services.amazonqFeatureDev.controller
5
5
6
6
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
7
7
import com.intellij.diff.DiffContentFactory
8
- import com.intellij.diff.DiffManager
8
+ import com.intellij.diff.chains.SimpleDiffRequestChain
9
9
import com.intellij.diff.contents.EmptyContent
10
+ import com.intellij.diff.editor.ChainDiffVirtualFile
11
+ import com.intellij.diff.editor.DiffEditorTabFilesManager
10
12
import com.intellij.diff.requests.SimpleDiffRequest
11
13
import com.intellij.ide.BrowserUtil
12
14
import com.intellij.openapi.application.runInEdt
@@ -17,6 +19,7 @@ import com.intellij.openapi.fileEditor.FileEditorManager
17
19
import com.intellij.openapi.project.Project
18
20
import com.intellij.openapi.vfs.VfsUtil
19
21
import com.intellij.openapi.wm.ToolWindowManager
22
+ import kotlinx.coroutines.delay
20
23
import kotlinx.coroutines.withContext
21
24
import software.amazon.awssdk.services.toolkittelemetry.model.Sentiment
22
25
import software.aws.toolkits.core.utils.debug
@@ -65,7 +68,10 @@ import software.aws.toolkits.jetbrains.services.amazonqFeatureDev.session.Prepar
65
68
import software.aws.toolkits.jetbrains.services.amazonqFeatureDev.session.Session
66
69
import software.aws.toolkits.jetbrains.services.amazonqFeatureDev.session.SessionStatePhase
67
70
import 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
68
73
import software.aws.toolkits.jetbrains.services.amazonqFeatureDev.util.selectFolder
74
+ import software.aws.toolkits.jetbrains.services.codewhisperer.util.content
69
75
import software.aws.toolkits.jetbrains.services.cwc.controller.chat.telemetry.FeedbackComment
70
76
import software.aws.toolkits.jetbrains.services.cwc.controller.chat.telemetry.getStartUrl
71
77
import software.aws.toolkits.jetbrains.services.telemetry.TelemetryService
@@ -86,13 +92,19 @@ class FeatureDevController(
86
92
val messenger = context.messagesFromAppToUi
87
93
val toolWindow = ToolWindowManager .getInstance(context.project).getToolWindow(AmazonQToolWindowFactory .WINDOW_ID )
88
94
95
+ private val diffVirtualFiles = mutableMapOf<String , ChainDiffVirtualFile >()
96
+
89
97
override suspend fun processPromptChatMessage (message : IncomingFeatureDevMessage .ChatPrompt ) {
90
98
handleChat(
91
99
tabId = message.tabId,
92
100
message = message.chatMessage
93
101
)
94
102
}
95
103
104
+ override suspend fun processStoreCodeResultMessageId (message : IncomingFeatureDevMessage .StoreMessageIdMessage ) {
105
+ storeCodeResultMessageId(message)
106
+ }
107
+
96
108
override suspend fun processStopMessage (message : IncomingFeatureDevMessage .StopResponse ) {
97
109
handleStopMessage(message)
98
110
}
@@ -126,6 +138,7 @@ class FeatureDevController(
126
138
127
139
override suspend fun processChatItemVotedMessage (message : IncomingFeatureDevMessage .ChatItemVotedMessage ) {
128
140
logger.debug { " $FEATURE_NAME : Processing ChatItemVotedMessage: $message " }
141
+ this .disablePreviousFileList(message.tabId)
129
142
130
143
val session = chatSessionStorage.getSession(message.tabId, context.project)
131
144
when (message.vote) {
@@ -192,6 +205,18 @@ class FeatureDevController(
192
205
}
193
206
}
194
207
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
+
195
220
override suspend fun processOpenDiff (message : IncomingFeatureDevMessage .OpenDiff ) {
196
221
val session = getSessionInfo(message.tabId)
197
222
@@ -223,9 +248,7 @@ class FeatureDevController(
223
248
DiffContentFactory .getInstance().create(newFileContent)
224
249
}
225
250
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 ))
229
252
}
230
253
}
231
254
else -> {
@@ -244,21 +267,81 @@ class FeatureDevController(
244
267
val fileToUpdate = message.filePath
245
268
val session = getSessionInfo(message.tabId)
246
269
val messageId = message.messageId
270
+ val action = message.actionName
247
271
248
272
var filePaths: List <NewFileZipInfo > = emptyList()
249
273
var deletedFiles: List <DeletedFileInfo > = emptyList()
274
+ var references: List <CodeReferenceGenerated > = emptyList()
250
275
when (val state = session.sessionState) {
251
276
is PrepareCodeGenerationState -> {
252
277
filePaths = state.filePaths
253
278
deletedFiles = state.deletedFiles
279
+ references = state.references
254
280
}
255
281
}
256
282
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
+ }
260
315
316
+ // Update the state of the tree view:
261
317
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
+ }
262
345
}
263
346
264
347
private suspend fun newTabOpened (tabId : String ) {
@@ -308,7 +391,8 @@ class FeatureDevController(
308
391
session.sessionState.token?.cancel()
309
392
}
310
393
}
311
- private suspend fun insertCode (tabId : String ) {
394
+
395
+ suspend fun insertCode (tabId : String ) {
312
396
var session: Session ? = null
313
397
try {
314
398
session = getSessionInfo(tabId)
@@ -325,17 +409,22 @@ class FeatureDevController(
325
409
}
326
410
}
327
411
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
+
328
416
AmazonqTelemetry .isAcceptedCodeChanges(
329
- amazonqNumberOfFilesAccepted = (filePaths.filterNot { it.rejected }.size + deletedFiles.filterNot { it.rejected }.size) * 1.0 ,
417
+ amazonqNumberOfFilesAccepted = remainingFilesCount.toDouble() ,
330
418
amazonqConversationId = session.conversationId,
331
419
enabled = true ,
332
420
credentialStartUrl = getStartUrl(project = context.project)
333
421
)
334
422
335
423
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
339
428
)
340
429
341
430
messenger.sendAnswer(
@@ -377,8 +466,11 @@ class FeatureDevController(
377
466
}
378
467
379
468
private suspend fun newTask (tabId : String , isException : Boolean? = false) {
469
+ this .disablePreviousFileList(tabId)
470
+
380
471
val session = getSessionInfo(tabId)
381
472
val sessionLatency = System .currentTimeMillis() - session.sessionStartTime
473
+
382
474
AmazonqTelemetry .endChat(
383
475
amazonqConversationId = session.conversationId,
384
476
amazonqEndOfTheConversationLatency = sessionLatency.toDouble(),
@@ -399,6 +491,7 @@ class FeatureDevController(
399
491
}
400
492
401
493
private suspend fun closeSession (tabId : String ) {
494
+ this .disablePreviousFileList(tabId)
402
495
messenger.sendAnswer(
403
496
tabId = tabId,
404
497
messageType = FeatureDevMessageType .Answer ,
@@ -503,7 +596,7 @@ class FeatureDevController(
503
596
tabId = tabId,
504
597
followUp = listOf (
505
598
FollowUp (
506
- pillText = message(" amazonqFeatureDev.follow_up.insert_code " ),
599
+ pillText = message(" amazonqFeatureDev.follow_up.insert_all_code " ),
507
600
type = FollowUpTypes .INSERT_CODE ,
508
601
icon = FollowUpIcons .Ok ,
509
602
status = FollowUpStatusType .Success ,
@@ -546,11 +639,28 @@ class FeatureDevController(
546
639
}
547
640
}
548
641
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
+
549
657
private suspend fun handleChat (
550
658
tabId : String ,
551
659
message : String ,
552
660
) {
553
661
var session: Session ? = null
662
+
663
+ this .disablePreviousFileList(tabId)
554
664
try {
555
665
logger.debug { " $FEATURE_NAME : Processing message: $message " }
556
666
session = getSessionInfo(tabId)
0 commit comments