Skip to content

Commit 834611f

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

File tree

18 files changed

+412
-142
lines changed

18 files changed

+412
-142
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: 105 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -14,9 +14,11 @@ import com.intellij.openapi.command.WriteCommandAction
1414
import com.intellij.openapi.editor.Caret
1515
import com.intellij.openapi.editor.Editor
1616
import com.intellij.openapi.fileEditor.FileEditorManager
17+
import com.intellij.openapi.fileEditor.OpenFileDescriptor
1718
import com.intellij.openapi.project.Project
1819
import com.intellij.openapi.vfs.VfsUtil
1920
import com.intellij.openapi.wm.ToolWindowManager
21+
import kotlinx.coroutines.delay
2022
import kotlinx.coroutines.withContext
2123
import software.amazon.awssdk.services.toolkittelemetry.model.Sentiment
2224
import software.aws.toolkits.core.utils.debug
@@ -65,6 +67,8 @@ import software.aws.toolkits.jetbrains.services.amazonqFeatureDev.session.Prepar
6567
import software.aws.toolkits.jetbrains.services.amazonqFeatureDev.session.Session
6668
import software.aws.toolkits.jetbrains.services.amazonqFeatureDev.session.SessionStatePhase
6769
import software.aws.toolkits.jetbrains.services.amazonqFeatureDev.storage.ChatSessionStorage
70+
import software.aws.toolkits.jetbrains.services.amazonqFeatureDev.util.InsertAction
71+
import software.aws.toolkits.jetbrains.services.amazonqFeatureDev.util.getFollowUpOptions
6872
import software.aws.toolkits.jetbrains.services.amazonqFeatureDev.util.selectFolder
6973
import software.aws.toolkits.jetbrains.services.cwc.controller.chat.telemetry.FeedbackComment
7074
import software.aws.toolkits.jetbrains.services.cwc.controller.chat.telemetry.getStartUrl
@@ -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,74 @@ 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+
val isAllApplied = filePaths.all { it.changeApplied } && deletedFiles.all { it.changeApplied }
269+
val isAllRejected = filePaths.all { it.rejected } && deletedFiles.all { it.rejected }
270+
val isAnyRejected = filePaths.any { it.rejected } || deletedFiles.any { it.rejected }
271+
272+
return if (isAllRejected) {
273+
InsertAction.CONTINUE
274+
} else if (isAnyRejected) {
275+
InsertAction.REMAINING
276+
} else if (isAllApplied) {
277+
InsertAction.AUTO_CONTINUE
278+
} else {
279+
InsertAction.ALL
280+
}
281+
}
282+
283+
val prevInsertAction = insertAction()
284+
285+
if (action == "accept-change") {
286+
session.insertChanges(
287+
filePaths = filePaths.filter { it.zipFilePath == fileToUpdate },
288+
deletedFiles = deletedFiles.filter { it.zipFilePath == fileToUpdate },
289+
references = emptyList(),
290+
messenger
291+
)
292+
293+
AmazonqTelemetry.isAcceptedCodeChanges(
294+
amazonqNumberOfFilesAccepted = 1.0,
295+
amazonqConversationId = session.conversationId,
296+
enabled = true,
297+
credentialStartUrl = getStartUrl(project = context.project)
298+
)
299+
} else {
300+
// Mark the file as rejected or not depending on the previous state
301+
filePaths.find { it.zipFilePath == fileToUpdate }?.let { it.rejected = !it.rejected }
302+
deletedFiles.find { it.zipFilePath == fileToUpdate }?.let { it.rejected = !it.rejected }
303+
}
260304

305+
// Update the state of the tree view:
261306
messenger.updateFileComponent(message.tabId, filePaths, deletedFiles, messageId)
307+
308+
// Then, if the accepted file is not a deletion, open it:
309+
if (action == "accept-change" && deletedFiles.none { it.zipFilePath == fileToUpdate }) {
310+
var pollAttempt = 0
311+
val pollDelayMs = 10L
312+
while (pollAttempt < 5) {
313+
val file = VfsUtil.findRelativeFile(message.filePath, session.context.selectedSourceFolder)
314+
if (file != null) {
315+
runInEdt {
316+
OpenFileDescriptor(context.project, file).navigate(true)
317+
}
318+
break
319+
} else {
320+
pollAttempt++
321+
delay(pollDelayMs)
322+
}
323+
}
324+
}
325+
326+
val nextInsertAction = insertAction()
327+
328+
if (nextInsertAction == InsertAction.AUTO_CONTINUE) {
329+
// Insert remaining changes (noop, as there are none), and advance to the next prompt:
330+
insertCode(message.tabId)
331+
} else if (nextInsertAction != prevInsertAction) {
332+
// Update the action displayed to the customer based on the current state:
333+
messenger.sendSystemPrompt(message.tabId, getFollowUpOptions(session.sessionState.phase, nextInsertAction))
334+
}
262335
}
263336

264337
private suspend fun newTabOpened(tabId: String) {
@@ -325,8 +398,12 @@ class FeatureDevController(
325398
}
326399
}
327400

401+
val rejectedFilesCount = filePaths.count { it.rejected } + deletedFiles.count { it.rejected }
402+
val acceptedFilesCount = filePaths.count { it.changeApplied } + filePaths.count { it.changeApplied }
403+
val remainingFilesCount = filePaths.count() + deletedFiles.count() - acceptedFilesCount - rejectedFilesCount
404+
328405
AmazonqTelemetry.isAcceptedCodeChanges(
329-
amazonqNumberOfFilesAccepted = (filePaths.filterNot { it.rejected }.size + deletedFiles.filterNot { it.rejected }.size) * 1.0,
406+
amazonqNumberOfFilesAccepted = remainingFilesCount * 1.0,
330407
amazonqConversationId = session.conversationId,
331408
enabled = true,
332409
credentialStartUrl = getStartUrl(project = context.project)
@@ -335,7 +412,8 @@ class FeatureDevController(
335412
session.insertChanges(
336413
filePaths = filePaths.filterNot { it.rejected },
337414
deletedFiles = deletedFiles.filterNot { it.rejected },
338-
references = references
415+
references = references,
416+
messenger
339417
)
340418

341419
messenger.sendAnswer(
@@ -377,8 +455,11 @@ class FeatureDevController(
377455
}
378456

379457
private suspend fun newTask(tabId: String, isException: Boolean? = false) {
458+
this.disablePreviousFileList(tabId)
459+
380460
val session = getSessionInfo(tabId)
381461
val sessionLatency = System.currentTimeMillis() - session.sessionStartTime
462+
382463
AmazonqTelemetry.endChat(
383464
amazonqConversationId = session.conversationId,
384465
amazonqEndOfTheConversationLatency = sessionLatency.toDouble(),
@@ -399,6 +480,7 @@ class FeatureDevController(
399480
}
400481

401482
private suspend fun closeSession(tabId: String) {
483+
this.disablePreviousFileList(tabId)
402484
messenger.sendAnswer(
403485
tabId = tabId,
404486
messageType = FeatureDevMessageType.Answer,
@@ -503,7 +585,7 @@ class FeatureDevController(
503585
tabId = tabId,
504586
followUp = listOf(
505587
FollowUp(
506-
pillText = message("amazonqFeatureDev.follow_up.insert_code"),
588+
pillText = message("amazonqFeatureDev.follow_up.insert_all_code"),
507589
type = FollowUpTypes.INSERT_CODE,
508590
icon = FollowUpIcons.Ok,
509591
status = FollowUpStatusType.Success,
@@ -546,11 +628,28 @@ class FeatureDevController(
546628
}
547629
}
548630

631+
private suspend fun disablePreviousFileList(tabId: String) {
632+
val session = getSessionInfo(tabId)
633+
when (val sessionState = session.sessionState) {
634+
is PrepareCodeGenerationState -> {
635+
session.disableFileList(sessionState.filePaths, sessionState.deletedFiles, messenger)
636+
}
637+
}
638+
}
639+
640+
private fun storeCodeResultMessageId(message: IncomingFeatureDevMessage.StoreMessageIdMessage) {
641+
val tabId = message.tabId
642+
val session = getSessionInfo(tabId)
643+
session.storeCodeResultMessageId(message)
644+
}
645+
549646
private suspend fun handleChat(
550647
tabId: String,
551648
message: String,
552649
) {
553650
var session: Session? = null
651+
652+
this.disablePreviousFileList(tabId)
554653
try {
555654
logger.debug { "$FEATURE_NAME: Processing message: $message" }
556655
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.size == 0 && deletedFiles.size == 0) {
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)