Skip to content

Commit 748e35a

Browse files
sannicmgrimalk
andauthored
feat(amazonqFeatureDev): add partial code acceptance (#4244)
Mynah UI already supports an option to handle file clicking. This is being leveraged to add a partial code acceptance feautre to the /dev chat. That is, the user will be able to click through the file changes generated from code generation step to reject them or not. These changes are similar to the ones already implemented for the VSCode toolkit. --------- Co-authored-by: Nikita Mikhailov <noobgam@amazon.de>
1 parent c6db2fe commit 748e35a

File tree

15 files changed

+210
-24
lines changed

15 files changed

+210
-24
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
@@ -39,7 +39,8 @@ class FeatureDevApp : AmazonQApp {
3939
"chat-item-voted" to IncomingFeatureDevMessage.ChatItemVotedMessage::class,
4040
"response-body-link-click" to IncomingFeatureDevMessage.ClickedLink::class,
4141
"insert_code_at_cursor_position" to IncomingFeatureDevMessage.InsertCodeAtCursorPosition::class,
42-
"open-diff" to IncomingFeatureDevMessage.OpenDiff::class
42+
"open-diff" to IncomingFeatureDevMessage.OpenDiff::class,
43+
"file-click" to IncomingFeatureDevMessage.FileClicked::class
4344
)
4445

4546
scope.launch {
@@ -78,6 +79,7 @@ class FeatureDevApp : AmazonQApp {
7879
is IncomingFeatureDevMessage.ClickedLink -> inboundAppMessagesHandler.processLinkClick(message)
7980
is IncomingFeatureDevMessage.InsertCodeAtCursorPosition -> inboundAppMessagesHandler.processInsertCodeAtCursorPosition(message)
8081
is IncomingFeatureDevMessage.OpenDiff -> inboundAppMessagesHandler.processOpenDiff(message)
82+
is IncomingFeatureDevMessage.FileClicked -> inboundAppMessagesHandler.processFileClicked(message)
8183
}
8284
}
8385

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,4 +15,5 @@ interface InboundAppMessagesHandler {
1515
suspend fun processLinkClick(message: IncomingFeatureDevMessage.ClickedLink)
1616
suspend fun processInsertCodeAtCursorPosition(message: IncomingFeatureDevMessage.InsertCodeAtCursorPosition)
1717
suspend fun processOpenDiff(message: IncomingFeatureDevMessage.OpenDiff)
18+
suspend fun processFileClicked(message: IncomingFeatureDevMessage.FileClicked)
1819
}

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

Lines changed: 29 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@ import software.aws.toolkits.jetbrains.services.amazonqFeatureDev.messages.sendC
5353
import software.aws.toolkits.jetbrains.services.amazonqFeatureDev.messages.sendError
5454
import software.aws.toolkits.jetbrains.services.amazonqFeatureDev.messages.sendSystemPrompt
5555
import software.aws.toolkits.jetbrains.services.amazonqFeatureDev.messages.sendUpdatePlaceholder
56+
import software.aws.toolkits.jetbrains.services.amazonqFeatureDev.session.DeletedFileInfo
5657
import software.aws.toolkits.jetbrains.services.amazonqFeatureDev.session.NewFileZipInfo
5758
import software.aws.toolkits.jetbrains.services.amazonqFeatureDev.session.PrepareCodeGenerationState
5859
import software.aws.toolkits.jetbrains.services.amazonqFeatureDev.session.Session
@@ -207,6 +208,31 @@ class FeatureDevController(
207208
}
208209
}
209210

211+
override suspend fun processFileClicked(message: IncomingFeatureDevMessage.FileClicked) {
212+
val fileToUpdate = message.filePath
213+
val session = getSessionInfo(message.tabId)
214+
215+
var filePaths: List<NewFileZipInfo> = emptyList()
216+
var deletedFiles: List<DeletedFileInfo> = emptyList()
217+
when (val state = session.sessionState) {
218+
is PrepareCodeGenerationState -> {
219+
filePaths = state.filePaths
220+
deletedFiles = state.deletedFiles
221+
}
222+
}
223+
224+
// Mark the file as rejected or not depending on the previous state
225+
filePaths.find { it.zipFilePath == fileToUpdate }?.let { it.rejected = !it.rejected }
226+
deletedFiles.find { it.zipFilePath == fileToUpdate }?.let { it.rejected = !it.rejected }
227+
228+
session.updateFilesPaths(
229+
messenger = messenger,
230+
tabId = message.tabId,
231+
filePaths = filePaths,
232+
deletedFiles = deletedFiles
233+
)
234+
}
235+
210236
private suspend fun newTabOpened(tabId: String) {
211237
var session: Session? = null
212238
try {
@@ -256,7 +282,7 @@ class FeatureDevController(
256282
session = getSessionInfo(tabId)
257283

258284
var filePaths: List<NewFileZipInfo> = emptyList()
259-
var deletedFiles: List<String> = emptyList()
285+
var deletedFiles: List<DeletedFileInfo> = emptyList()
260286
var references: List<CodeReference> = emptyList()
261287

262288
when (val state = session.sessionState) {
@@ -268,7 +294,7 @@ class FeatureDevController(
268294
}
269295
AmazonqTelemetry.isAcceptedCodeChanges(
270296
project = null,
271-
amazonqNumberOfFilesAccepted = (filePaths.size + deletedFiles.size) * 1.0,
297+
amazonqNumberOfFilesAccepted = (filePaths.filterNot { it.rejected }.size + deletedFiles.filterNot { it.rejected }.size) * 1.0,
272298
amazonqConversationId = session.conversationId,
273299
enabled = true
274300
)
@@ -497,7 +523,7 @@ class FeatureDevController(
497523
val state = session.sessionState
498524

499525
var filePaths: List<NewFileZipInfo> = emptyList()
500-
var deletedFiles: List<String> = emptyList()
526+
var deletedFiles: List<DeletedFileInfo> = emptyList()
501527
var references: List<CodeReference> = emptyList()
502528
var uploadId = ""
503529

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

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@ import com.fasterxml.jackson.annotation.JsonProperty
77
import com.fasterxml.jackson.annotation.JsonValue
88
import software.aws.toolkits.jetbrains.services.amazonq.auth.AuthFollowUpType
99
import software.aws.toolkits.jetbrains.services.amazonq.messages.AmazonQMessage
10+
import software.aws.toolkits.jetbrains.services.amazonqFeatureDev.session.DeletedFileInfo
11+
import software.aws.toolkits.jetbrains.services.amazonqFeatureDev.session.NewFileZipInfo
1012
import software.aws.toolkits.jetbrains.services.cwc.messages.CodeReference
1113
import java.time.Instant
1214
import java.util.UUID
@@ -69,6 +71,13 @@ sealed interface IncomingFeatureDevMessage : FeatureDevBaseMessage {
6971
val filePath: String,
7072
val deleted: Boolean
7173
) : IncomingFeatureDevMessage
74+
75+
data class FileClicked(
76+
@JsonProperty("tabID") val tabId: String,
77+
val filePath: String,
78+
val messageId: String,
79+
val actionName: String
80+
) : IncomingFeatureDevMessage
7281
}
7382

7483
// === UI -> App Messages ===
@@ -121,6 +130,15 @@ data class UpdatePlaceholderMessage(
121130
type = "updatePlaceholderMessage"
122131
)
123132

133+
data class FileComponent(
134+
@JsonProperty("tabID") override val tabId: String,
135+
val filePaths: List<NewFileZipInfo>,
136+
val deletedFiles: List<DeletedFileInfo>
137+
) : UiMessage(
138+
tabId = tabId,
139+
type = "updateFileComponent"
140+
)
141+
124142
data class ChatInputEnabledMessage(
125143
@JsonProperty("tabID") override val tabId: String,
126144
val enabled: Boolean
@@ -162,8 +180,8 @@ data class AuthNeededException(
162180
data class CodeResultMessage(
163181
@JsonProperty("tabID") override val tabId: String,
164182
val conversationId: String,
165-
val filePaths: List<String>,
166-
val deletedFiles: List<String>,
183+
val filePaths: List<NewFileZipInfo>,
184+
val deletedFiles: List<DeletedFileInfo>,
167185
val references: List<ReducedCodeReference>
168186
) : UiMessage(
169187
tabId = tabId,

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

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ package software.aws.toolkits.jetbrains.services.amazonqFeatureDev.messages
55

66
import software.aws.toolkits.jetbrains.services.amazonq.auth.AuthNeededState
77
import software.aws.toolkits.jetbrains.services.amazonq.messages.MessagePublisher
8+
import software.aws.toolkits.jetbrains.services.amazonqFeatureDev.session.DeletedFileInfo
89
import software.aws.toolkits.jetbrains.services.amazonqFeatureDev.session.NewFileZipInfo
910
import software.aws.toolkits.jetbrains.services.amazonqFeatureDev.session.SessionStatePhase
1011
import software.aws.toolkits.jetbrains.services.amazonqFeatureDev.util.licenseText
@@ -56,6 +57,15 @@ suspend fun MessagePublisher.sendSystemPrompt(
5657
)
5758
}
5859

60+
suspend fun MessagePublisher.updateFileComponent(tabId: String, filePaths: List<NewFileZipInfo>, deletedFiles: List<DeletedFileInfo>) {
61+
val fileComponentMessage = FileComponent(
62+
tabId = tabId,
63+
filePaths = filePaths,
64+
deletedFiles = deletedFiles,
65+
)
66+
this.publish(fileComponentMessage)
67+
}
68+
5969
suspend fun MessagePublisher.sendAsyncEventProgress(tabId: String, inProgress: Boolean, message: String? = null) {
6070
val asyncEventProgressMessage = AsyncEventProgressMessage(
6171
tabId = tabId,
@@ -171,7 +181,7 @@ suspend fun MessagePublisher.sendCodeResult(
171181
tabId: String,
172182
uploadId: String,
173183
filePaths: List<NewFileZipInfo>,
174-
deletedFiles: List<String>,
184+
deletedFiles: List<DeletedFileInfo>,
175185
references: List<CodeReference>
176186
) {
177187
// It is being done this mapping as featureDev currently doesn't support fully references.
@@ -183,7 +193,7 @@ suspend fun MessagePublisher.sendCodeResult(
183193
CodeResultMessage(
184194
tabId = tabId,
185195
conversationId = uploadId,
186-
filePaths = filePaths.map { it.zipFilePath },
196+
filePaths = filePaths,
187197
deletedFiles = deletedFiles,
188198
references = refs
189199
)

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

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -92,10 +92,11 @@ private suspend fun CodeGenerationState.generateCode(codeGenerationId: String):
9292
)
9393

9494
val newFileInfo = registerNewFiles(newFileContents = codeGenerationStreamResult.new_file_contents)
95+
val deletedFileInfo = registerDeletedFiles(deletedFiles = codeGenerationStreamResult.deleted_files)
9596

9697
return CodeGenerationResult(
9798
newFiles = newFileInfo,
98-
deletedFiles = codeGenerationStreamResult.deleted_files,
99+
deletedFiles = deletedFileInfo,
99100
references = codeGenerationStreamResult.references
100101
)
101102
}
@@ -109,5 +110,16 @@ private suspend fun CodeGenerationState.generateCode(codeGenerationId: String):
109110
}
110111

111112
fun registerNewFiles(newFileContents: Map<String, String>): List<NewFileZipInfo> = newFileContents.map {
112-
NewFileZipInfo(zipFilePath = it.key, fileContent = it.value)
113+
NewFileZipInfo(
114+
zipFilePath = it.key,
115+
fileContent = it.value,
116+
rejected = false
117+
)
118+
}
119+
120+
fun registerDeletedFiles(deletedFiles: List<String>): List<DeletedFileInfo> = deletedFiles.map {
121+
DeletedFileInfo(
122+
zipFilePath = it,
123+
rejected = false
124+
)
113125
}

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ class PrepareCodeGenerationState(
1818
override var approach: String,
1919
private var config: SessionStateConfig,
2020
val filePaths: List<NewFileZipInfo>,
21-
val deletedFiles: List<String>,
21+
val deletedFiles: List<DeletedFileInfo>,
2222
val references: List<CodeReference>,
2323
var uploadId: String,
2424
private val currentIteration: Int,

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

Lines changed: 24 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import software.aws.toolkits.jetbrains.services.amazonqFeatureDev.CODE_GENERATIO
1111
import software.aws.toolkits.jetbrains.services.amazonqFeatureDev.clients.FeatureDevClient
1212
import software.aws.toolkits.jetbrains.services.amazonqFeatureDev.conversationIdNotFound
1313
import software.aws.toolkits.jetbrains.services.amazonqFeatureDev.messages.sendAsyncEventProgress
14+
import software.aws.toolkits.jetbrains.services.amazonqFeatureDev.messages.updateFileComponent
1415
import software.aws.toolkits.jetbrains.services.amazonqFeatureDev.util.createConversation
1516
import software.aws.toolkits.jetbrains.services.cwc.controller.ReferenceLogController
1617
import software.aws.toolkits.jetbrains.services.cwc.messages.CodeReference
@@ -90,22 +91,35 @@ class Session(val tabID: String, val project: Project) {
9091
AmazonqTelemetry.isApproachAccepted(amazonqConversationId = conversationId, enabled = true)
9192
}
9293

94+
suspend fun updateFilesPaths(
95+
messenger: MessagePublisher,
96+
tabId: String,
97+
filePaths: List<NewFileZipInfo>,
98+
deletedFiles: List<DeletedFileInfo>
99+
) {
100+
messenger.updateFileComponent(tabId, filePaths, deletedFiles)
101+
}
102+
93103
/**
94104
* Triggered by the Insert code follow-up button to apply code changes.
95105
*/
96-
fun insertChanges(filePaths: List<NewFileZipInfo>, deletedFiles: List<String>, references: List<CodeReference>) {
106+
fun insertChanges(filePaths: List<NewFileZipInfo>, deletedFiles: List<DeletedFileInfo>, references: List<CodeReference>) {
97107
val projectRootPath = context.projectRoot.toNioPath()
98108

99-
filePaths.forEach {
100-
val filePath = projectRootPath.resolve(it.zipFilePath)
101-
filePath.parent.createDirectories() // Create directories if needed
102-
filePath.writeBytes(it.fileContent.toByteArray(Charsets.UTF_8))
103-
}
109+
filePaths
110+
.filterNot { it.rejected }
111+
.forEach {
112+
val filePath = projectRootPath.resolve(it.zipFilePath)
113+
filePath.parent.createDirectories() // Create directories if needed
114+
filePath.writeBytes(it.fileContent.toByteArray(Charsets.UTF_8))
115+
}
104116

105-
deletedFiles.forEach {
106-
val deleteFilePath = projectRootPath.resolve(it)
107-
deleteFilePath.deleteIfExists()
108-
}
117+
deletedFiles
118+
.filterNot { it.rejected }
119+
.forEach {
120+
val deleteFilePath = projectRootPath.resolve(it.zipFilePath)
121+
deleteFilePath.deleteIfExists()
122+
}
109123

110124
ReferenceLogController.addReferenceLog(references, project)
111125

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

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,11 +39,17 @@ data class SessionStateConfig(
3939
data class NewFileZipInfo(
4040
val zipFilePath: String,
4141
val fileContent: String,
42+
var rejected: Boolean
43+
)
44+
45+
data class DeletedFileInfo(
46+
val zipFilePath: String, // The string is the path of the file to be deleted
47+
var rejected: Boolean
4248
)
4349

4450
data class CodeGenerationResult(
4551
var newFiles: List<NewFileZipInfo>,
46-
var deletedFiles: List<String>, // The string is the path of the file to be deleted
52+
var deletedFiles: List<DeletedFileInfo>,
4753
var references: List<CodeReference>,
4854
)
4955

plugins/amazonq/mynah-ui/src/mynah-ui/ui/apps/featureDevChatConnector.ts

Lines changed: 27 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@ import { ExtensionMessage } from '../commands'
88
import { TabType, TabsStorage } from '../storages/tabsStorage'
99
import { CodeReference } from './amazonqCommonsConnector'
1010
import { FollowUpGenerator } from '../followUps/generator'
11+
import { getActions } from '../diffTree/actions'
12+
import { DiffTreeFileInfo } from '../diffTree/types'
1113

1214
interface ChatPayload {
1315
chatMessage: string
@@ -26,6 +28,7 @@ export interface ConnectorProps {
2628
onUpdateAuthentication: (featureDevEnabled: boolean, codeTransformEnabled: boolean, authenticatingTabIDs: string[]) => void
2729
onNewTab: (tabType: TabType) => void
2830
tabsStorage: TabsStorage
31+
onFileComponentUpdate: (tabID: string, filePaths: DiffTreeFileInfo[], deletedFiles: DiffTreeFileInfo[]) => void
2932
}
3033

3134
export class Connector {
@@ -39,6 +42,7 @@ export class Connector {
3942
private readonly onUpdateAuthentication
4043
private readonly followUpGenerator: FollowUpGenerator
4144
private readonly onNewTab
45+
private readonly onFileComponentUpdate
4246

4347
constructor(props: ConnectorProps) {
4448
this.sendMessageToExtension = props.sendMessageToExtension
@@ -51,6 +55,7 @@ export class Connector {
5155
this.onUpdateAuthentication = props.onUpdateAuthentication
5256
this.followUpGenerator = new FollowUpGenerator()
5357
this.onNewTab = props.onNewTab
58+
this.onFileComponentUpdate = props.onFileComponentUpdate
5459
}
5560

5661
onCodeInsertToCursorPosition = (
@@ -112,6 +117,17 @@ export class Connector {
112117
})
113118
})
114119

120+
onFileActionClick = (tabID: string, messageId: string, filePath: string, actionName: string): void => {
121+
this.sendMessageToExtension({
122+
command: 'file-click',
123+
tabID,
124+
messageId,
125+
filePath,
126+
actionName,
127+
tabType: 'featuredev',
128+
})
129+
}
130+
115131
private processChatMessage = async (messageData: any): Promise<void> => {
116132
if (this.onChatAnswerReceived !== undefined) {
117133
const answer: ChatItem = {
@@ -137,6 +153,10 @@ export class Connector {
137153

138154
private processCodeResultMessage = async (messageData: any): Promise<void> => {
139155
if (this.onChatAnswerReceived !== undefined) {
156+
const actions = getActions([
157+
...messageData.filePaths,
158+
...messageData.deletedFiles,
159+
])
140160
const answer: ChatItem = {
141161
type: ChatItemType.CODE_RESULT,
142162
relatedContent: undefined,
@@ -146,8 +166,9 @@ export class Connector {
146166
// TODO get the backend to store a message id in addition to conversationID
147167
messageId: messageData.messageID ?? messageData.triggerID ?? messageData.conversationID,
148168
fileList: {
149-
filePaths: messageData.filePaths,
150-
deletedFiles: messageData.deletedFiles,
169+
filePaths: (messageData.filePaths as DiffTreeFileInfo[]).map(path => path.zipFilePath),
170+
deletedFiles: (messageData.deletedFiles as DiffTreeFileInfo[]).map(path => path.zipFilePath),
171+
actions,
151172
},
152173
body: '',
153174
}
@@ -178,6 +199,10 @@ export class Connector {
178199
}
179200

180201
handleMessageReceive = async (messageData: any): Promise<void> => {
202+
if (messageData.type === 'updateFileComponent') {
203+
this.onFileComponentUpdate(messageData.tabID, messageData.filePaths, messageData.deletedFiles)
204+
return
205+
}
181206
if (messageData.type === 'errorMessage') {
182207
this.onError(messageData.tabID, messageData.message, messageData.title)
183208
return

plugins/amazonq/mynah-ui/src/mynah-ui/ui/commands.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,5 +37,6 @@ type MessageCommand =
3737
| 'codetransform-open-mvn-build'
3838
| 'codetransform-view-diff'
3939
| 'codetransform-view-summary'
40+
| 'file-click'
4041

4142
export type ExtensionMessage = Record<string, any> & { command: MessageCommand }

0 commit comments

Comments
 (0)