Skip to content

Commit cf36b6a

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

File tree

20 files changed

+328
-92
lines changed

20 files changed

+328
-92
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: 100 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import com.intellij.openapi.fileEditor.FileEditorManager
1717
import com.intellij.openapi.project.Project
1818
import com.intellij.openapi.vfs.VfsUtil
1919
import com.intellij.openapi.wm.ToolWindowManager
20+
import kotlinx.coroutines.delay
2021
import kotlinx.coroutines.withContext
2122
import software.amazon.awssdk.services.toolkittelemetry.model.Sentiment
2223
import software.aws.toolkits.core.utils.debug
@@ -65,7 +66,10 @@ import software.aws.toolkits.jetbrains.services.amazonqFeatureDev.session.Prepar
6566
import software.aws.toolkits.jetbrains.services.amazonqFeatureDev.session.Session
6667
import software.aws.toolkits.jetbrains.services.amazonqFeatureDev.session.SessionStatePhase
6768
import software.aws.toolkits.jetbrains.services.amazonqFeatureDev.storage.ChatSessionStorage
69+
import software.aws.toolkits.jetbrains.services.amazonqFeatureDev.util.InsertAction
70+
import software.aws.toolkits.jetbrains.services.amazonqFeatureDev.util.getFollowUpOptions
6871
import software.aws.toolkits.jetbrains.services.amazonqFeatureDev.util.selectFolder
72+
import software.aws.toolkits.jetbrains.services.codewhisperer.util.content
6973
import software.aws.toolkits.jetbrains.services.cwc.controller.chat.telemetry.FeedbackComment
7074
import software.aws.toolkits.jetbrains.services.cwc.controller.chat.telemetry.getStartUrl
7175
import software.aws.toolkits.jetbrains.services.telemetry.TelemetryService
@@ -93,6 +97,10 @@ class FeatureDevController(
9397
)
9498
}
9599

100+
override suspend fun processStoreCodeResultMessageId(message: IncomingFeatureDevMessage.StoreMessageIdMessage) {
101+
storeCodeResultMessageId(message)
102+
}
103+
96104
override suspend fun processStopMessage(message: IncomingFeatureDevMessage.StopResponse) {
97105
handleStopMessage(message)
98106
}
@@ -126,6 +134,7 @@ class FeatureDevController(
126134

127135
override suspend fun processChatItemVotedMessage(message: IncomingFeatureDevMessage.ChatItemVotedMessage) {
128136
logger.debug { "$FEATURE_NAME: Processing ChatItemVotedMessage: $message" }
137+
this.disablePreviousFileList(message.tabId)
129138

130139
val session = chatSessionStorage.getSession(message.tabId, context.project)
131140
when (message.vote) {
@@ -244,6 +253,7 @@ class FeatureDevController(
244253
val fileToUpdate = message.filePath
245254
val session = getSessionInfo(message.tabId)
246255
val messageId = message.messageId
256+
val action = message.actionName
247257

248258
var filePaths: List<NewFileZipInfo> = emptyList()
249259
var deletedFiles: List<DeletedFileInfo> = emptyList()
@@ -254,11 +264,69 @@ class FeatureDevController(
254264
}
255265
}
256266

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 }
267+
fun insertAction(): InsertAction =
268+
if (filePaths.all { it.changeApplied } && deletedFiles.all { it.changeApplied }) {
269+
InsertAction.AUTO_CONTINUE
270+
} else if (filePaths.all { it.changeApplied || it.rejected } && deletedFiles.all { it.changeApplied || it.rejected }) {
271+
InsertAction.CONTINUE
272+
} else if (filePaths.any { it.changeApplied || it.rejected } || deletedFiles.any { it.changeApplied || it.rejected }) {
273+
InsertAction.REMAINING
274+
} else {
275+
InsertAction.ALL
276+
}
260277

278+
val prevInsertAction = insertAction()
279+
280+
if (action == "accept-change") {
281+
session.insertChanges(
282+
filePaths = filePaths.filter { it.zipFilePath == fileToUpdate },
283+
deletedFiles = deletedFiles.filter { it.zipFilePath == fileToUpdate },
284+
references = emptyList(),
285+
messenger
286+
)
287+
288+
AmazonqTelemetry.isAcceptedCodeChanges(
289+
amazonqNumberOfFilesAccepted = 1.0,
290+
amazonqConversationId = session.conversationId,
291+
enabled = true,
292+
credentialStartUrl = getStartUrl(project = context.project)
293+
)
294+
} else {
295+
// Mark the file as rejected or not depending on the previous state
296+
filePaths.find { it.zipFilePath == fileToUpdate }?.let { it.rejected = !it.rejected }
297+
deletedFiles.find { it.zipFilePath == fileToUpdate }?.let { it.rejected = !it.rejected }
298+
}
299+
300+
// Update the state of the tree view:
261301
messenger.updateFileComponent(message.tabId, filePaths, deletedFiles, messageId)
302+
303+
// Then, if the accepted file is not a deletion, open a diff to show the changes are applied:
304+
if (action == "accept-change" && deletedFiles.none { it.zipFilePath == fileToUpdate }) {
305+
var pollAttempt = 0
306+
val pollDelayMs = 10L
307+
while (pollAttempt < 5) {
308+
val file = VfsUtil.findRelativeFile(message.filePath, session.context.selectedSourceFolder)
309+
// Wait for the file to be created and/or updated to the new content:
310+
if (file != null && file.content() == filePaths.find { it.zipFilePath == fileToUpdate }?.fileContent) {
311+
// Open a diff, showing the changes have been applied and the file now has identical left/right state:
312+
this.processOpenDiff(IncomingFeatureDevMessage.OpenDiff(message.tabId, fileToUpdate, false))
313+
break
314+
} else {
315+
pollAttempt++
316+
delay(pollDelayMs)
317+
}
318+
}
319+
}
320+
321+
val nextInsertAction = insertAction()
322+
323+
if (nextInsertAction == InsertAction.AUTO_CONTINUE) {
324+
// Insert remaining changes (noop, as there are none), and advance to the next prompt:
325+
insertCode(message.tabId)
326+
} else if (nextInsertAction != prevInsertAction) {
327+
// Update the action displayed to the customer based on the current state:
328+
messenger.sendSystemPrompt(message.tabId, getFollowUpOptions(session.sessionState.phase, nextInsertAction))
329+
}
262330
}
263331

264332
private suspend fun newTabOpened(tabId: String) {
@@ -325,8 +393,12 @@ class FeatureDevController(
325393
}
326394
}
327395

396+
val rejectedFilesCount = filePaths.count { it.rejected } + deletedFiles.count { it.rejected }
397+
val acceptedFilesCount = filePaths.count { it.changeApplied } + filePaths.count { it.changeApplied }
398+
val remainingFilesCount = filePaths.count() + deletedFiles.count() - acceptedFilesCount - rejectedFilesCount
399+
328400
AmazonqTelemetry.isAcceptedCodeChanges(
329-
amazonqNumberOfFilesAccepted = (filePaths.filterNot { it.rejected }.size + deletedFiles.filterNot { it.rejected }.size) * 1.0,
401+
amazonqNumberOfFilesAccepted = remainingFilesCount * 1.0,
330402
amazonqConversationId = session.conversationId,
331403
enabled = true,
332404
credentialStartUrl = getStartUrl(project = context.project)
@@ -335,7 +407,8 @@ class FeatureDevController(
335407
session.insertChanges(
336408
filePaths = filePaths.filterNot { it.rejected },
337409
deletedFiles = deletedFiles.filterNot { it.rejected },
338-
references = references
410+
references = references,
411+
messenger
339412
)
340413

341414
messenger.sendAnswer(
@@ -377,8 +450,11 @@ class FeatureDevController(
377450
}
378451

379452
private suspend fun newTask(tabId: String, isException: Boolean? = false) {
453+
this.disablePreviousFileList(tabId)
454+
380455
val session = getSessionInfo(tabId)
381456
val sessionLatency = System.currentTimeMillis() - session.sessionStartTime
457+
382458
AmazonqTelemetry.endChat(
383459
amazonqConversationId = session.conversationId,
384460
amazonqEndOfTheConversationLatency = sessionLatency.toDouble(),
@@ -399,6 +475,7 @@ class FeatureDevController(
399475
}
400476

401477
private suspend fun closeSession(tabId: String) {
478+
this.disablePreviousFileList(tabId)
402479
messenger.sendAnswer(
403480
tabId = tabId,
404481
messageType = FeatureDevMessageType.Answer,
@@ -503,7 +580,7 @@ class FeatureDevController(
503580
tabId = tabId,
504581
followUp = listOf(
505582
FollowUp(
506-
pillText = message("amazonqFeatureDev.follow_up.insert_code"),
583+
pillText = message("amazonqFeatureDev.follow_up.insert_all_code"),
507584
type = FollowUpTypes.INSERT_CODE,
508585
icon = FollowUpIcons.Ok,
509586
status = FollowUpStatusType.Success,
@@ -546,11 +623,28 @@ class FeatureDevController(
546623
}
547624
}
548625

626+
private suspend fun disablePreviousFileList(tabId: String) {
627+
val session = getSessionInfo(tabId)
628+
when (val sessionState = session.sessionState) {
629+
is PrepareCodeGenerationState -> {
630+
session.disableFileList(sessionState.filePaths, sessionState.deletedFiles, messenger)
631+
}
632+
}
633+
}
634+
635+
private fun storeCodeResultMessageId(message: IncomingFeatureDevMessage.StoreMessageIdMessage) {
636+
val tabId = message.tabId
637+
val session = getSessionInfo(tabId)
638+
session.storeCodeResultMessageId(message)
639+
}
640+
549641
private suspend fun handleChat(
550642
tabId: String,
551643
message: String,
552644
) {
553645
var session: Session? = null
646+
647+
this.disablePreviousFileList(tabId)
554648
try {
555649
logger.debug { "$FEATURE_NAME: Processing message: $message" }
556650
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)