Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 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" : "Add image context support"
}
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,11 @@
import software.aws.toolkits.resources.message
import java.util.concurrent.CompletableFuture
import javax.swing.JButton
import java.awt.dnd.DropTarget
import java.awt.dnd.DropTargetDropEvent
import java.awt.datatransfer.DataFlavor
import com.google.gson.Gson
import java.util.Base64

Check warning on line 53 in plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/toolwindow/AmazonQPanel.kt

View workflow job for this annotation

GitHub Actions / qodana

Unused import directive

Unused import directive

class AmazonQPanel(val project: Project, private val scope: CoroutineScope) : Disposable {
private val browser = CompletableFuture<Browser>()
Expand Down Expand Up @@ -122,12 +127,106 @@

withContext(EDT) {
browser.complete(
Browser(this@AmazonQPanel, webUri, project).also {
wrapper.setContent(it.component())
Browser(this@AmazonQPanel, webUri, project).also { browserInstance ->
wrapper.setContent(browserInstance.component())

// Add DropTarget to the browser component
val dropTarget = object : DropTarget() {
override fun drop(dtde: DropTargetDropEvent) {

try {
dtde.acceptDrop(dtde.dropAction)
val transferable = dtde.transferable
if (transferable.isDataFlavorSupported(DataFlavor.javaFileListFlavor)) {
val fileList = transferable.getTransferData(DataFlavor.javaFileListFlavor) as List<*>

val errorMessages = mutableListOf<String>()
val validImages = mutableListOf<java.io.File>()
val allowedTypes = setOf("jpg", "jpeg", "png", "gif", "webp")
val maxFileSize = 3.75 * 1024 * 1024 // 3.75MB in bytes
val maxDimension = 8000
Comment on lines +150 to +152
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why do we need this here, can you please add a comment to the flare location that this picks up the values from?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

JCEF does not propagate OS-level dragenter, dragOver and drop into DOM. As an alternative, enabling the native drag in JCEF, and let the native handling the drop event, and update the UI through JS bridge.
The same logic of filtering the images are implemented on both language server and JetBrain


for (file in fileList) {
val fileObj = file as? java.io.File ?: continue
val fileName = fileObj.name
val ext = fileName.substringAfterLast('.', "").lowercase()

// File type restriction
if (ext !in allowedTypes) {
errorMessages.add("$fileName: File must be an image in JPEG, PNG, GIF, or WebP format.")
continue
}

// Size restriction
if (fileObj.length() > maxFileSize) {
errorMessages.add("$fileName: Image must be no more than 3.75MB in size.")
continue
}

// Width/Height restriction (only for image types)
try {
val img = javax.imageio.ImageIO.read(fileObj)
if (img == null) {
errorMessages.add("$fileName: File could not be read as an image.")
continue
}
if (img.width > maxDimension) {
errorMessages.add("$fileName: Image must be no more than 8,000px in width.")
continue
}
if (img.height > maxDimension) {
errorMessages.add("$fileName: Image must be no more than 8,000px in height.")
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Aren't all these checks also in language-server? Do they need to be duplicated on the client?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Due to the restriction of Jetbrain webveiw, we have to handle the drag&drop natively

continue
}
} catch (e: Exception) {
errorMessages.add("$fileName: File could not be read as an image.")
continue
}

validImages.add(fileObj)
}

// File count restriction
if (validImages.size > 20) {
errorMessages.add("A maximum of 20 images can be added to a single message.")
validImages.subList(20, validImages.size).clear()
}



val json = Gson().toJson(validImages).replace("\\", "\\\\").replace("'", "\\'")
browserInstance.jcefBrowser.cefBrowser.executeJavaScript(
"window.handleNativeDrop('$json')",
browserInstance.jcefBrowser.cefBrowser.url,
0
)

val errorJson = Gson().toJson(errorMessages).replace("\\", "\\\\").replace("'", "\\'")
browserInstance.jcefBrowser.cefBrowser.executeJavaScript(
"window.handleNativeNotify('$errorJson')",
browserInstance.jcefBrowser.cefBrowser.url,
0
)

}
dtde.dropComplete(true)
} catch (e: Exception) {
e.printStackTrace()
dtde.dropComplete(false)
}
}
}

// Set DropTarget on the browser component and its children
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

how are the children involved

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We want the whole chat window to be "drag & drop zone"

browserInstance.component()?.let { component ->
component.dropTarget = dropTarget
// Also try setting on parent if needed
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is to make sure all chat window is drop area

component.parent?.dropTarget = dropTarget
}

initConnections()
connectUi(it)
connectApps(it)
connectUi(browserInstance)
connectApps(browserInstance)

loadingPanel.stopLoading()
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,9 @@ class Browser(parent: Disposable, private val webUri: URI, val project: Project)
// setup empty state. The message request handlers use this for storing state
// that's persistent between page loads.
jcefBrowser.setProperty("state", "")
jcefBrowser.jbCefClient.addDragHandler({ browser, dragData, mask ->
true // Allow drag operations
}, jcefBrowser.cefBrowser)
// load the web app
jcefBrowser.loadHTML(
getWebviewHTML(
Expand Down Expand Up @@ -122,7 +125,7 @@ class Browser(parent: Disposable, private val webUri: URI, val project: Project)
<script type="text/javascript" charset="UTF-8" src="$webUri" defer onload="init()"></script>

<script type="text/javascript">

let qChat = undefined
const init = () => {
const hybridChatConnector = connectorAdapter.initiateAdapter(
${MeetQSettings.getInstance().reinvent2024OnboardingCount < MAX_ONBOARDING_PAGE_COUNT},
Expand All @@ -139,7 +142,7 @@ class Browser(parent: Disposable, private val webUri: URI, val project: Project)
},

"${activeProfile?.profileName.orEmpty()}")
amazonQChat.createChat(
const qChat = amazonQChat.createChat(
{
postMessage: message => {
$postMessageToJavaJsCode
Expand All @@ -155,6 +158,30 @@ class Browser(parent: Disposable, private val webUri: URI, val project: Project)
hybridChatConnector,
${CodeWhispererFeatureConfigService.getInstance().getFeatureConfigJsonString()}
);
window.qChat = qChat;
window.handleNativeDrop = function(filePath) {
const parsedFilePath = JSON.parse(filePath);
const contextArray = parsedFilePath.map(fileObj => {
const fullPath = fileObj.path;
const fileName = fullPath.split(/[\\/]/).pop();
return {
command: fileName,
label: 'image',
route: [fullPath],
description: fullPath
};
});
qChat.addCustomContextToPrompt(qChat.getSelectedTabId(), contextArray);
};

window.handleNativeNotify = function(errorMessages) {
const messages = JSON.parse(errorMessages);
messages.forEach(msg => {
qChat.notify({
content: msg
})
});
};
}
</script>
""".trimIndent()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,8 @@ import software.aws.toolkits.jetbrains.services.amazonq.lsp.model.aws.chat.GET_S
import software.aws.toolkits.jetbrains.services.amazonq.lsp.model.aws.chat.GetSerializedChatResponse
import software.aws.toolkits.jetbrains.services.amazonq.lsp.model.aws.chat.LIST_MCP_SERVERS_REQUEST_METHOD
import software.aws.toolkits.jetbrains.services.amazonq.lsp.model.aws.chat.MCP_SERVER_CLICK_REQUEST_METHOD
import software.aws.toolkits.jetbrains.services.amazonq.lsp.model.aws.chat.OPEN_FILE_DIALOG
import software.aws.toolkits.jetbrains.services.amazonq.lsp.model.aws.chat.OPEN_FILE_DIALOG_REQUEST_METHOD
import software.aws.toolkits.jetbrains.services.amazonq.lsp.model.aws.chat.OPEN_SETTINGS
import software.aws.toolkits.jetbrains.services.amazonq.lsp.model.aws.chat.OPEN_WORKSPACE_SETTINGS_KEY
import software.aws.toolkits.jetbrains.services.amazonq.lsp.model.aws.chat.OpenSettingsNotification
Expand Down Expand Up @@ -485,6 +487,17 @@ class BrowserConnector(
)
}
}
OPEN_FILE_DIALOG -> {
handleChat(AmazonQChatServer.showOpenFileDialog, node)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can you walk me through the message flow? does it actually need to transit through flare before requesting the file picker?

Copy link
Contributor Author

@zuoyaofu zuoyaofu Jun 25, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Flow:

  1. chat-client will send openFileDialog request to flare
  2. Flare will send to extension the params, including canSelectMany (multiple files), filters (what file extension types are allowed)
  3. Extension will then handle file selection logic and send back the list of URIs.
  4. Flare will then send to chat-client the file uri to display in chat.

- does it actually need to transit through flare before requesting the file picker?

  • In @yzhangok's design, yes. Flare is also telling client what type of files are supported in file picker as well(step 2)

.whenComplete { response, _ ->
browser.postChat(
FlareUiMessage(
command = OPEN_FILE_DIALOG_REQUEST_METHOD,
params = response
)
)
}
}
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ import software.aws.toolkits.jetbrains.services.amazonq.lsp.model.aws.chat.LIST_
import software.aws.toolkits.jetbrains.services.amazonq.lsp.model.aws.chat.LinkClickParams
import software.aws.toolkits.jetbrains.services.amazonq.lsp.model.aws.chat.ListConversationsParams
import software.aws.toolkits.jetbrains.services.amazonq.lsp.model.aws.chat.MCP_SERVER_CLICK_REQUEST_METHOD
import software.aws.toolkits.jetbrains.services.amazonq.lsp.model.aws.chat.OPEN_FILE_DIALOG_REQUEST_METHOD
import software.aws.toolkits.jetbrains.services.amazonq.lsp.model.aws.chat.PROMPT_INPUT_OPTIONS_CHANGE
import software.aws.toolkits.jetbrains.services.amazonq.lsp.model.aws.chat.PromptInputOptionChangeParams
import software.aws.toolkits.jetbrains.services.amazonq.lsp.model.aws.chat.SEND_CHAT_COMMAND_PROMPT
Expand Down Expand Up @@ -222,4 +223,10 @@ object AmazonQChatServer : JsonRpcMethodProvider {
TELEMETRY_EVENT,
Any::class.java
)

val showOpenFileDialog = JsonRpcRequest(
OPEN_FILE_DIALOG_REQUEST_METHOD,
Any::class.java,
LSPAny::class.java
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,9 @@ import software.aws.toolkits.jetbrains.services.amazonq.lsp.model.aws.chat.GET_S
import software.aws.toolkits.jetbrains.services.amazonq.lsp.model.aws.chat.GetSerializedChatResult
import software.aws.toolkits.jetbrains.services.amazonq.lsp.model.aws.chat.OPEN_FILE_DIFF
import software.aws.toolkits.jetbrains.services.amazonq.lsp.model.aws.chat.OpenFileDiffParams
import software.aws.toolkits.jetbrains.services.amazonq.lsp.model.aws.chat.SHOW_OPEN_FILE_DIALOG_REQUEST_METHOD
import software.aws.toolkits.jetbrains.services.amazonq.lsp.model.aws.chat.SHOW_SAVE_FILE_DIALOG_REQUEST_METHOD
import software.aws.toolkits.jetbrains.services.amazonq.lsp.model.aws.chat.ShowOpenFileDialogParams
import software.aws.toolkits.jetbrains.services.amazonq.lsp.model.aws.chat.ShowSaveFileDialogParams
import software.aws.toolkits.jetbrains.services.amazonq.lsp.model.aws.chat.ShowSaveFileDialogResult
import software.aws.toolkits.jetbrains.services.amazonq.lsp.model.aws.credentials.ConnectionMetadata
Expand All @@ -42,6 +44,9 @@ interface AmazonQLanguageClient : LanguageClient {
@JsonRequest(SHOW_SAVE_FILE_DIALOG_REQUEST_METHOD)
fun showSaveFileDialog(params: ShowSaveFileDialogParams): CompletableFuture<ShowSaveFileDialogResult>

@JsonRequest(SHOW_OPEN_FILE_DIALOG_REQUEST_METHOD)
fun showOpenFileDialog(params: ShowOpenFileDialogParams): CompletableFuture<LSPAny>

@JsonRequest(GET_SERIALIZED_CHAT_REQUEST_METHOD)
fun getSerializedChat(params: LSPAny): CompletableFuture<GetSerializedChatResult>

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ import com.intellij.ide.BrowserUtil
import com.intellij.notification.NotificationAction
import com.intellij.notification.NotificationType
import com.intellij.openapi.application.ApplicationManager
import com.intellij.openapi.fileChooser.FileChooser
import com.intellij.openapi.fileChooser.FileChooserDescriptorFactory
import com.intellij.openapi.fileChooser.FileChooserFactory
import com.intellij.openapi.fileChooser.FileSaverDescriptor
import com.intellij.openapi.fileEditor.FileEditorManager
Expand Down Expand Up @@ -51,6 +53,7 @@ import software.aws.toolkits.jetbrains.services.amazonq.lsp.model.aws.chat.FileP
import software.aws.toolkits.jetbrains.services.amazonq.lsp.model.aws.chat.GET_SERIALIZED_CHAT_REQUEST_METHOD
import software.aws.toolkits.jetbrains.services.amazonq.lsp.model.aws.chat.GetSerializedChatResult
import software.aws.toolkits.jetbrains.services.amazonq.lsp.model.aws.chat.OpenFileDiffParams
import software.aws.toolkits.jetbrains.services.amazonq.lsp.model.aws.chat.ShowOpenFileDialogParams
import software.aws.toolkits.jetbrains.services.amazonq.lsp.model.aws.chat.ShowSaveFileDialogParams
import software.aws.toolkits.jetbrains.services.amazonq.lsp.model.aws.chat.ShowSaveFileDialogResult
import software.aws.toolkits.jetbrains.services.amazonq.lsp.model.aws.credentials.ConnectionMetadata
Expand Down Expand Up @@ -249,6 +252,27 @@ class AmazonQLanguageClientImpl(private val project: Project) : AmazonQLanguageC
)
}

override fun showOpenFileDialog(params: ShowOpenFileDialogParams): CompletableFuture<LSPAny> {
return CompletableFuture.supplyAsync(
{
val descriptor = if (params.canSelectMany) {
FileChooserDescriptorFactory.createMultipleFilesNoJarsDescriptor().apply {
title = "Select Files"
description = "Choose files to open"
}
} else {
FileChooserDescriptorFactory.createSingleFileNoJarsDescriptor()
}

val chosenFiles = FileChooser.chooseFiles(descriptor, project, null)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why aren't images from this flow subject to constraints?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, they are now with the new commit.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what about the file size and dimensional limits?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The validation of the images chosen from file picker is done in language-server

val uris = chosenFiles.map { it.url }

mapOf("uris" to uris) as LSPAny
},
ApplicationManager.getApplication()::invokeLater
)
}

override fun getSerializedChat(params: LSPAny): CompletableFuture<GetSerializedChatResult> {
val requestId = UUID.randomUUID().toString()
val result = CompletableFuture<GetSerializedChatResult>()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -48,10 +48,13 @@ const val PROMPT_INPUT_OPTIONS_CHANGE = "aws/chat/promptInputOptionChange"

const val SEND_CHAT_COMMAND_PROMPT = "aws/chat/sendChatPrompt"
const val SHOW_SAVE_FILE_DIALOG_REQUEST_METHOD = "aws/showSaveFileDialog"
const val SHOW_OPEN_FILE_DIALOG_REQUEST_METHOD = "aws/showOpenFileDialog"
const val STOP_CHAT_RESPONSE = "stopChatResponse"
const val SEND_TO_PROMPT = "sendToPrompt"
const val OPEN_FILE_DIALOG = "openFileDialog"
const val TELEMETRY_EVENT = "telemetry/event"

// https://github.com/aws/language-server-runtimes/blob/112feba70219a98a12f13727d67c540205fa9c9f/types/chat.ts#L32
const val LIST_MCP_SERVERS_REQUEST_METHOD = "aws/chat/listMcpServers"
const val MCP_SERVER_CLICK_REQUEST_METHOD = "aws/chat/mcpServerClick"
const val OPEN_FILE_DIALOG_REQUEST_METHOD = "aws/chat/openFileDialog"
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
// Copyright 2025 Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0

package software.aws.toolkits.jetbrains.services.amazonq.lsp.model.aws.chat

data class ShowOpenFileDialogParams(
val canSelectFiles: Boolean = false,
val canSelectFolders: Boolean = false,
val canSelectMany: Boolean = false,
val filters: Map<String, List<String>> = emptyMap(),
val defaultUri: String? = null,
val title: String? = null,
)
Loading