Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"type" : "feature",
"description" : "Amazon Q /dev: Add an action to accept individual files"
}
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,8 @@ class FeatureDevApp : AmazonQApp {
"insert_code_at_cursor_position" to IncomingFeatureDevMessage.InsertCodeAtCursorPosition::class,
"open-diff" to IncomingFeatureDevMessage.OpenDiff::class,
"file-click" to IncomingFeatureDevMessage.FileClicked::class,
"stop-response" to IncomingFeatureDevMessage.StopResponse::class
"stop-response" to IncomingFeatureDevMessage.StopResponse::class,
"store-code-result-message-id" to IncomingFeatureDevMessage.StoreMessageIdMessage::class
)

scope.launch {
Expand Down Expand Up @@ -84,6 +85,7 @@ class FeatureDevApp : AmazonQApp {
is IncomingFeatureDevMessage.OpenDiff -> inboundAppMessagesHandler.processOpenDiff(message)
is IncomingFeatureDevMessage.FileClicked -> inboundAppMessagesHandler.processFileClicked(message)
is IncomingFeatureDevMessage.StopResponse -> inboundAppMessagesHandler.processStopMessage(message)
is IncomingFeatureDevMessage.StoreMessageIdMessage -> inboundAppMessagesHandler.processStoreCodeResultMessageId(message)
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ package software.aws.toolkits.jetbrains.services.amazonqFeatureDev
import software.aws.toolkits.jetbrains.services.amazonqFeatureDev.messages.IncomingFeatureDevMessage

interface InboundAppMessagesHandler {

suspend fun processPromptChatMessage(message: IncomingFeatureDevMessage.ChatPrompt)
suspend fun processNewTabCreatedMessage(message: IncomingFeatureDevMessage.NewTabCreated)
suspend fun processTabRemovedMessage(message: IncomingFeatureDevMessage.TabRemoved)
Expand All @@ -18,4 +19,5 @@ interface InboundAppMessagesHandler {
suspend fun processOpenDiff(message: IncomingFeatureDevMessage.OpenDiff)
suspend fun processFileClicked(message: IncomingFeatureDevMessage.FileClicked)
suspend fun processStopMessage(message: IncomingFeatureDevMessage.StopResponse)
suspend fun processStoreCodeResultMessageId(message: IncomingFeatureDevMessage.StoreMessageIdMessage)
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,10 @@

import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
import com.intellij.diff.DiffContentFactory
import com.intellij.diff.DiffManager
import com.intellij.diff.chains.SimpleDiffRequestChain
import com.intellij.diff.contents.EmptyContent
import com.intellij.diff.editor.ChainDiffVirtualFile
import com.intellij.diff.editor.DiffEditorTabFilesManager
import com.intellij.diff.requests.SimpleDiffRequest
import com.intellij.ide.BrowserUtil
import com.intellij.openapi.application.runInEdt
Expand All @@ -17,6 +19,7 @@
import com.intellij.openapi.project.Project
import com.intellij.openapi.vfs.VfsUtil
import com.intellij.openapi.wm.ToolWindowManager
import kotlinx.coroutines.delay
import kotlinx.coroutines.withContext
import software.amazon.awssdk.services.toolkittelemetry.model.Sentiment
import software.aws.toolkits.core.utils.debug
Expand Down Expand Up @@ -65,7 +68,10 @@
import software.aws.toolkits.jetbrains.services.amazonqFeatureDev.session.Session
import software.aws.toolkits.jetbrains.services.amazonqFeatureDev.session.SessionStatePhase
import software.aws.toolkits.jetbrains.services.amazonqFeatureDev.storage.ChatSessionStorage
import software.aws.toolkits.jetbrains.services.amazonqFeatureDev.util.InsertAction
import software.aws.toolkits.jetbrains.services.amazonqFeatureDev.util.getFollowUpOptions
import software.aws.toolkits.jetbrains.services.amazonqFeatureDev.util.selectFolder
import software.aws.toolkits.jetbrains.services.codewhisperer.util.content
import software.aws.toolkits.jetbrains.services.cwc.controller.chat.telemetry.FeedbackComment
import software.aws.toolkits.jetbrains.services.cwc.controller.chat.telemetry.getStartUrl
import software.aws.toolkits.jetbrains.services.telemetry.TelemetryService
Expand All @@ -86,13 +92,19 @@
val messenger = context.messagesFromAppToUi
val toolWindow = ToolWindowManager.getInstance(context.project).getToolWindow(AmazonQToolWindowFactory.WINDOW_ID)

private val diffVirtualFiles = mutableMapOf<String, ChainDiffVirtualFile>()

override suspend fun processPromptChatMessage(message: IncomingFeatureDevMessage.ChatPrompt) {
handleChat(
tabId = message.tabId,
message = message.chatMessage
)
}

override suspend fun processStoreCodeResultMessageId(message: IncomingFeatureDevMessage.StoreMessageIdMessage) {
storeCodeResultMessageId(message)
}

override suspend fun processStopMessage(message: IncomingFeatureDevMessage.StopResponse) {
handleStopMessage(message)
}
Expand Down Expand Up @@ -126,6 +138,7 @@

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

val session = chatSessionStorage.getSession(message.tabId, context.project)
when (message.vote) {
Expand Down Expand Up @@ -192,6 +205,18 @@
}
}

private fun putDiff(filePath: String, request: SimpleDiffRequest) {
// Close any existing diff and open a new diff, as the diff virtual file does not appear to allow replacing content directly:
val existingDiff = diffVirtualFiles[filePath]
if (existingDiff != null) {
FileEditorManager.getInstance(context.project).closeFile(existingDiff)
}

val newDiff = ChainDiffVirtualFile(SimpleDiffRequestChain(request), filePath)
DiffEditorTabFilesManager.getInstance(context.project).showDiffFile(newDiff, true)
diffVirtualFiles[filePath] = newDiff
}

override suspend fun processOpenDiff(message: IncomingFeatureDevMessage.OpenDiff) {
val session = getSessionInfo(message.tabId)

Expand Down Expand Up @@ -223,9 +248,7 @@
DiffContentFactory.getInstance().create(newFileContent)
}

val request = SimpleDiffRequest(message.filePath, leftDiffContent, rightDiffContent, null, null)

DiffManager.getInstance().showDiff(project, request)
putDiff(message.filePath, SimpleDiffRequest(message.filePath, leftDiffContent, rightDiffContent, null, null))
}
}
else -> {
Expand All @@ -244,21 +267,81 @@
val fileToUpdate = message.filePath
val session = getSessionInfo(message.tabId)
val messageId = message.messageId
val action = message.actionName

var filePaths: List<NewFileZipInfo> = emptyList()
var deletedFiles: List<DeletedFileInfo> = emptyList()
var references: List<CodeReferenceGenerated> = emptyList()
when (val state = session.sessionState) {
is PrepareCodeGenerationState -> {
filePaths = state.filePaths
deletedFiles = state.deletedFiles
references = state.references
}
}

// Mark the file as rejected or not depending on the previous state
filePaths.find { it.zipFilePath == fileToUpdate }?.let { it.rejected = !it.rejected }
deletedFiles.find { it.zipFilePath == fileToUpdate }?.let { it.rejected = !it.rejected }
fun insertAction(): InsertAction =
if (filePaths.all { it.changeApplied } && deletedFiles.all { it.changeApplied }) {
InsertAction.AUTO_CONTINUE
} else if (filePaths.all { it.changeApplied || it.rejected } && deletedFiles.all { it.changeApplied || it.rejected }) {
InsertAction.CONTINUE
} else if (filePaths.any { it.changeApplied || it.rejected } || deletedFiles.any { it.changeApplied || it.rejected }) {
InsertAction.REMAINING
} else {
InsertAction.ALL
}

val prevInsertAction = insertAction()

if (action == "accept-change") {
session.insertChanges(
filePaths = filePaths.filter { it.zipFilePath == fileToUpdate },
deletedFiles = deletedFiles.filter { it.zipFilePath == fileToUpdate },
references = references, // Add all references (not attributed per-file)
messenger
)

AmazonqTelemetry.isAcceptedCodeChanges(

Check warning

Code scanning / QDJVMC

Usage of redundant or deprecated syntax or deprecated symbols Warning

'AmazonqTelemetry' is deprecated. Use type-safe metric builders
amazonqNumberOfFilesAccepted = 1.0,
amazonqConversationId = session.conversationId,
enabled = true,
credentialStartUrl = getStartUrl(project = context.project)
)
} else {
// Mark the file as rejected or not depending on the previous state
filePaths.find { it.zipFilePath == fileToUpdate }?.let { it.rejected = !it.rejected }
deletedFiles.find { it.zipFilePath == fileToUpdate }?.let { it.rejected = !it.rejected }
}

// Update the state of the tree view:
messenger.updateFileComponent(message.tabId, filePaths, deletedFiles, messageId)

// Then, if the accepted file is not a deletion, open a diff to show the changes are applied:
if (action == "accept-change" && deletedFiles.none { it.zipFilePath == fileToUpdate }) {
var pollAttempt = 0
val pollDelayMs = 10L
while (pollAttempt < 5) {
val file = VfsUtil.findRelativeFile(message.filePath, session.context.selectedSourceFolder)
// Wait for the file to be created and/or updated to the new content:
if (file != null && file.content() == filePaths.find { it.zipFilePath == fileToUpdate }?.fileContent) {
// Open a diff, showing the changes have been applied and the file now has identical left/right state:
this.processOpenDiff(IncomingFeatureDevMessage.OpenDiff(message.tabId, fileToUpdate, false))
break
} else {
pollAttempt++
delay(pollDelayMs)
}
}
}

val nextInsertAction = insertAction()
if (nextInsertAction == InsertAction.AUTO_CONTINUE) {
// Insert remaining changes (noop, as there are none), and advance to the next prompt:
insertCode(message.tabId)
} else if (nextInsertAction != prevInsertAction) {
// Update the action displayed to the customer based on the current state:
messenger.sendSystemPrompt(message.tabId, getFollowUpOptions(session.sessionState.phase, nextInsertAction))
}
}

private suspend fun newTabOpened(tabId: String) {
Expand Down Expand Up @@ -308,7 +391,8 @@
session.sessionState.token?.cancel()
}
}
private suspend fun insertCode(tabId: String) {

suspend fun insertCode(tabId: String) {
var session: Session? = null
try {
session = getSessionInfo(tabId)
Expand All @@ -325,17 +409,22 @@
}
}

val rejectedFilesCount = filePaths.count { it.rejected } + deletedFiles.count { it.rejected }
val acceptedFilesCount = filePaths.count { it.changeApplied } + filePaths.count { it.changeApplied }
val remainingFilesCount = filePaths.count() + deletedFiles.count() - acceptedFilesCount - rejectedFilesCount

AmazonqTelemetry.isAcceptedCodeChanges(
amazonqNumberOfFilesAccepted = (filePaths.filterNot { it.rejected }.size + deletedFiles.filterNot { it.rejected }.size) * 1.0,
amazonqNumberOfFilesAccepted = remainingFilesCount.toDouble(),
amazonqConversationId = session.conversationId,
enabled = true,
credentialStartUrl = getStartUrl(project = context.project)
)

session.insertChanges(
filePaths = filePaths.filterNot { it.rejected },
deletedFiles = deletedFiles.filterNot { it.rejected },
references = references
filePaths = filePaths.filterNot { it.rejected || it.changeApplied },
deletedFiles = deletedFiles.filterNot { it.rejected || it.changeApplied },
references = references,
messenger
)

messenger.sendAnswer(
Expand Down Expand Up @@ -377,8 +466,11 @@
}

private suspend fun newTask(tabId: String, isException: Boolean? = false) {
this.disablePreviousFileList(tabId)

val session = getSessionInfo(tabId)
val sessionLatency = System.currentTimeMillis() - session.sessionStartTime

AmazonqTelemetry.endChat(
amazonqConversationId = session.conversationId,
amazonqEndOfTheConversationLatency = sessionLatency.toDouble(),
Expand All @@ -399,6 +491,7 @@
}

private suspend fun closeSession(tabId: String) {
this.disablePreviousFileList(tabId)
messenger.sendAnswer(
tabId = tabId,
messageType = FeatureDevMessageType.Answer,
Expand Down Expand Up @@ -503,7 +596,7 @@
tabId = tabId,
followUp = listOf(
FollowUp(
pillText = message("amazonqFeatureDev.follow_up.insert_code"),
pillText = message("amazonqFeatureDev.follow_up.insert_all_code"),
type = FollowUpTypes.INSERT_CODE,
icon = FollowUpIcons.Ok,
status = FollowUpStatusType.Success,
Expand Down Expand Up @@ -546,11 +639,28 @@
}
}

private suspend fun disablePreviousFileList(tabId: String) {
val session = getSessionInfo(tabId)
when (val sessionState = session.sessionState) {
is PrepareCodeGenerationState -> {
session.disableFileList(sessionState.filePaths, sessionState.deletedFiles, messenger)
}
}
}

private fun storeCodeResultMessageId(message: IncomingFeatureDevMessage.StoreMessageIdMessage) {
val tabId = message.tabId
val session = getSessionInfo(tabId)
session.storeCodeResultMessageId(message)
}

private suspend fun handleChat(
tabId: String,
message: String,
) {
var session: Session? = null

this.disablePreviousFileList(tabId)
try {
logger.debug { "$FEATURE_NAME: Processing message: $message" }
session = getSessionInfo(tabId)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import software.aws.toolkits.jetbrains.services.amazonqFeatureDev.session.Prepar
import software.aws.toolkits.jetbrains.services.amazonqFeatureDev.session.Session
import software.aws.toolkits.jetbrains.services.amazonqFeatureDev.session.SessionState
import software.aws.toolkits.jetbrains.services.amazonqFeatureDev.util.CancellationTokenSource
import software.aws.toolkits.jetbrains.services.amazonqFeatureDev.util.InsertAction
import software.aws.toolkits.jetbrains.services.amazonqFeatureDev.util.getFollowUpOptions
import software.aws.toolkits.jetbrains.utils.notifyInfo
import software.aws.toolkits.resources.message
Expand Down Expand Up @@ -88,7 +89,7 @@ suspend fun FeatureDevController.onCodeGeneration(
}

// Atm this is the only possible path as codegen is mocked to return empty.
if (filePaths.size or deletedFiles.size == 0) {
if (filePaths.isEmpty() && deletedFiles.isEmpty()) {
messenger.sendAnswer(
tabId = tabId,
messageType = FeatureDevMessageType.Answer,
Expand Down Expand Up @@ -132,7 +133,7 @@ suspend fun FeatureDevController.onCodeGeneration(
)
}

messenger.sendSystemPrompt(tabId = tabId, followUp = getFollowUpOptions(session.sessionState.phase))
messenger.sendSystemPrompt(tabId = tabId, followUp = getFollowUpOptions(session.sessionState.phase, InsertAction.ALL))

messenger.sendUpdatePlaceholder(tabId = tabId, newPlaceholder = message("amazonqFeatureDev.placeholder.after_code_generation"))
} finally {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,12 @@ sealed interface IncomingFeatureDevMessage : FeatureDevBaseMessage {
@JsonProperty("tabID") val tabId: String,
) : IncomingFeatureDevMessage

data class StoreMessageIdMessage(
@JsonProperty("tabID") val tabId: String,
val command: String,
val messageId: String?,
) : IncomingFeatureDevMessage

data class NewTabCreated(
val command: String,
@JsonProperty("tabID") val tabId: String,
Expand Down Expand Up @@ -149,6 +155,7 @@ data class FileComponent(
val filePaths: List<NewFileZipInfo>,
val deletedFiles: List<DeletedFileInfo>,
val messageId: String,
val disableFileActions: Boolean = false,
) : UiMessage(
tabId = tabId,
type = "updateFileComponent"
Expand Down Expand Up @@ -198,6 +205,7 @@ data class CodeResultMessage(
val filePaths: List<NewFileZipInfo>,
val deletedFiles: List<DeletedFileInfo>,
val references: List<CodeReference>,
val messageId: String?,
) : UiMessage(
tabId = tabId,
type = "codeResultMessage"
Expand Down
Loading
Loading