Skip to content

Commit 88bd700

Browse files
C Tiddtverney
andcommitted
feat(amazonq): Implement file-level acceptance for /dev.
Co-authored-by: Thiago Verney <[email protected]>
1 parent 7f3b222 commit 88bd700

File tree

20 files changed

+347
-96
lines changed

20 files changed

+347
-96
lines changed

plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonqFeatureDev/FeatureDevApp.kt

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,8 @@ class FeatureDevApp : AmazonQApp {
4242
"insert_code_at_cursor_position" to IncomingFeatureDevMessage.InsertCodeAtCursorPosition::class,
4343
"open-diff" to IncomingFeatureDevMessage.OpenDiff::class,
4444
"file-click" to IncomingFeatureDevMessage.FileClicked::class,
45-
"stop-response" to IncomingFeatureDevMessage.StopResponse::class
45+
"stop-response" to IncomingFeatureDevMessage.StopResponse::class,
46+
"store-code-result-message-id" to IncomingFeatureDevMessage.StoreMessageIdMessage::class
4647
)
4748

4849
scope.launch {
@@ -84,6 +85,7 @@ class FeatureDevApp : AmazonQApp {
8485
is IncomingFeatureDevMessage.OpenDiff -> inboundAppMessagesHandler.processOpenDiff(message)
8586
is IncomingFeatureDevMessage.FileClicked -> inboundAppMessagesHandler.processFileClicked(message)
8687
is IncomingFeatureDevMessage.StopResponse -> inboundAppMessagesHandler.processStopMessage(message)
88+
is IncomingFeatureDevMessage.StoreMessageIdMessage -> inboundAppMessagesHandler.processStoreCodeResultMessageId(message)
8789
}
8890
}
8991

plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonqFeatureDev/InboundAppMessagesHandler.kt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ package software.aws.toolkits.jetbrains.services.amazonqFeatureDev
66
import software.aws.toolkits.jetbrains.services.amazonqFeatureDev.messages.IncomingFeatureDevMessage
77

88
interface InboundAppMessagesHandler {
9+
910
suspend fun processPromptChatMessage(message: IncomingFeatureDevMessage.ChatPrompt)
1011
suspend fun processNewTabCreatedMessage(message: IncomingFeatureDevMessage.NewTabCreated)
1112
suspend fun processTabRemovedMessage(message: IncomingFeatureDevMessage.TabRemoved)
@@ -18,4 +19,5 @@ interface InboundAppMessagesHandler {
1819
suspend fun processOpenDiff(message: IncomingFeatureDevMessage.OpenDiff)
1920
suspend fun processFileClicked(message: IncomingFeatureDevMessage.FileClicked)
2021
suspend fun processStopMessage(message: IncomingFeatureDevMessage.StopResponse)
22+
suspend fun processStoreCodeResultMessageId(message: IncomingFeatureDevMessage.StoreMessageIdMessage)
2123
}

plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonqFeatureDev/controller/FeatureDevController.kt

Lines changed: 118 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,10 @@ package software.aws.toolkits.jetbrains.services.amazonqFeatureDev.controller
55

66
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
77
import com.intellij.diff.DiffContentFactory
8-
import com.intellij.diff.DiffManager
8+
import com.intellij.diff.chains.SimpleDiffRequestChain
99
import com.intellij.diff.contents.EmptyContent
10+
import com.intellij.diff.editor.ChainDiffVirtualFile
11+
import com.intellij.diff.editor.DiffEditorTabFilesManager
1012
import com.intellij.diff.requests.SimpleDiffRequest
1113
import com.intellij.ide.BrowserUtil
1214
import com.intellij.openapi.application.runInEdt
@@ -17,6 +19,7 @@ import com.intellij.openapi.fileEditor.FileEditorManager
1719
import com.intellij.openapi.project.Project
1820
import com.intellij.openapi.vfs.VfsUtil
1921
import com.intellij.openapi.wm.ToolWindowManager
22+
import kotlinx.coroutines.delay
2023
import kotlinx.coroutines.withContext
2124
import software.amazon.awssdk.services.toolkittelemetry.model.Sentiment
2225
import software.aws.toolkits.core.utils.debug
@@ -65,7 +68,10 @@ import software.aws.toolkits.jetbrains.services.amazonqFeatureDev.session.Prepar
6568
import software.aws.toolkits.jetbrains.services.amazonqFeatureDev.session.Session
6669
import software.aws.toolkits.jetbrains.services.amazonqFeatureDev.session.SessionStatePhase
6770
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
6873
import software.aws.toolkits.jetbrains.services.amazonqFeatureDev.util.selectFolder
74+
import software.aws.toolkits.jetbrains.services.codewhisperer.util.content
6975
import software.aws.toolkits.jetbrains.services.cwc.controller.chat.telemetry.FeedbackComment
7076
import software.aws.toolkits.jetbrains.services.cwc.controller.chat.telemetry.getStartUrl
7177
import 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)

plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonqFeatureDev/controller/FeatureDevControllerExtensions.kt

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ import software.aws.toolkits.jetbrains.services.amazonqFeatureDev.session.Prepar
2323
import software.aws.toolkits.jetbrains.services.amazonqFeatureDev.session.Session
2424
import software.aws.toolkits.jetbrains.services.amazonqFeatureDev.session.SessionState
2525
import software.aws.toolkits.jetbrains.services.amazonqFeatureDev.util.CancellationTokenSource
26+
import software.aws.toolkits.jetbrains.services.amazonqFeatureDev.util.InsertAction
2627
import software.aws.toolkits.jetbrains.services.amazonqFeatureDev.util.getFollowUpOptions
2728
import software.aws.toolkits.jetbrains.utils.notifyInfo
2829
import software.aws.toolkits.resources.message
@@ -88,7 +89,7 @@ suspend fun FeatureDevController.onCodeGeneration(
8889
}
8990

9091
// Atm this is the only possible path as codegen is mocked to return empty.
91-
if (filePaths.size or deletedFiles.size == 0) {
92+
if (filePaths.isEmpty() && deletedFiles.isEmpty()) {
9293
messenger.sendAnswer(
9394
tabId = tabId,
9495
messageType = FeatureDevMessageType.Answer,
@@ -132,7 +133,7 @@ suspend fun FeatureDevController.onCodeGeneration(
132133
)
133134
}
134135

135-
messenger.sendSystemPrompt(tabId = tabId, followUp = getFollowUpOptions(session.sessionState.phase))
136+
messenger.sendSystemPrompt(tabId = tabId, followUp = getFollowUpOptions(session.sessionState.phase, InsertAction.ALL))
136137

137138
messenger.sendUpdatePlaceholder(tabId = tabId, newPlaceholder = message("amazonqFeatureDev.placeholder.after_code_generation"))
138139
} finally {

plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonqFeatureDev/messages/FeatureDevMessage.kt

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,12 @@ sealed interface IncomingFeatureDevMessage : FeatureDevBaseMessage {
2424
@JsonProperty("tabID") val tabId: String,
2525
) : IncomingFeatureDevMessage
2626

27+
data class StoreMessageIdMessage(
28+
@JsonProperty("tabID") val tabId: String,
29+
val command: String,
30+
val messageId: String?,
31+
) : IncomingFeatureDevMessage
32+
2733
data class NewTabCreated(
2834
val command: String,
2935
@JsonProperty("tabID") val tabId: String,
@@ -149,6 +155,7 @@ data class FileComponent(
149155
val filePaths: List<NewFileZipInfo>,
150156
val deletedFiles: List<DeletedFileInfo>,
151157
val messageId: String,
158+
val disableFileActions: Boolean = false,
152159
) : UiMessage(
153160
tabId = tabId,
154161
type = "updateFileComponent"
@@ -198,6 +205,7 @@ data class CodeResultMessage(
198205
val filePaths: List<NewFileZipInfo>,
199206
val deletedFiles: List<DeletedFileInfo>,
200207
val references: List<CodeReference>,
208+
val messageId: String?,
201209
) : UiMessage(
202210
tabId = tabId,
203211
type = "codeResultMessage"

0 commit comments

Comments
 (0)