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
11 changes: 11 additions & 0 deletions .changes/3.85.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
{
"date" : "2025-07-10",
"version" : "3.85",
"entries" : [ {
"type" : "feature",
"description" : "Amazon Q /test, /doc, and /dev capabilities integrated into Agentic coding."
}, {
"type" : "feature",
"description" : "Add image context support"
} ]
}
8 changes: 8 additions & 0 deletions .changes/3.86.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{
"date" : "2025-07-16",
"version" : "3.86",
"entries" : [ {
"type" : "bugfix",
"description" : "- Fixed \"Insert to Cursor\" button to correctly insert code blocks at the current cursor position in the active file"
} ]
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"type" : "bugfix",
"description" : "Suppress IDE error when current editor context is not valid for Amazon Q"
}
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,10 @@
# _3.86_ (2025-07-16)
- **(Bug Fix)** - Fixed "Insert to Cursor" button to correctly insert code blocks at the current cursor position in the active file

# _3.85_ (2025-07-10)
- **(Feature)** Amazon Q /test, /doc, and /dev capabilities integrated into Agentic coding.
- **(Feature)** Add image context support

# _3.84_ (2025-07-09)

# _3.83_ (2025-07-07)
Expand Down
2 changes: 1 addition & 1 deletion gradle.properties
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
# SPDX-License-Identifier: Apache-2.0

# Toolkit Version
toolkitVersion=3.85-SNAPSHOT
toolkitVersion=3.87-SNAPSHOT

# Publish Settings
publishToken=
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@

package software.aws.toolkits.jetbrains.services.amazonq.toolwindow

import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
import com.intellij.openapi.Disposable
import com.intellij.openapi.components.service
import com.intellij.openapi.project.Project
Expand All @@ -20,6 +21,8 @@ import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import software.aws.toolkits.core.utils.error
import software.aws.toolkits.core.utils.getLogger
import software.aws.toolkits.jetbrains.core.coroutines.EDT
import software.aws.toolkits.jetbrains.isDeveloperMode
import software.aws.toolkits.jetbrains.services.amazonq.apps.AmazonQAppInitContext
Expand All @@ -45,7 +48,12 @@ import software.aws.toolkits.jetbrains.services.amazonqFeatureDev.auth.isFeature
import software.aws.toolkits.jetbrains.services.codemodernizer.utils.isCodeTransformAvailable
import software.aws.toolkits.jetbrains.utils.isRunningOnRemoteBackend
import software.aws.toolkits.resources.message
import java.awt.datatransfer.DataFlavor
import java.awt.dnd.DropTarget
import java.awt.dnd.DropTargetDropEvent
import java.io.File
import java.util.concurrent.CompletableFuture
import javax.imageio.ImageIO.read
import javax.swing.JButton

class AmazonQPanel(val project: Project, private val scope: CoroutineScope) : Disposable {
Expand Down Expand Up @@ -130,12 +138,76 @@ class AmazonQPanel(val project: Project, private val scope: CoroutineScope) : Di

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

// Add DropTarget to the browser component
// 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.
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<File>()
val allowedTypes = setOf("jpg", "jpeg", "png", "gif", "webp")
val maxFileSize = 3.75 * 1024 * 1024 // 3.75MB in bytes
val maxDimension = 8000

for (file in fileList as List<File>) {
val validationResult = validateImageFile(file, allowedTypes, maxFileSize, maxDimension)
if (validationResult != null) {
errorMessages.add(validationResult)
} else {
validImages.add(file)
}
}

// 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 = OBJECT_MAPPER.writeValueAsString(validImages)
browserInstance.jcefBrowser.cefBrowser.executeJavaScript(
"window.handleNativeDrop('$json')",
browserInstance.jcefBrowser.cefBrowser.url,
0
)

val errorJson = OBJECT_MAPPER.writeValueAsString(errorMessages)
browserInstance.jcefBrowser.cefBrowser.executeJavaScript(
"window.handleNativeNotify('$errorJson')",
browserInstance.jcefBrowser.cefBrowser.url,
0
)
dtde.dropComplete(true)
} else {
dtde.dropComplete(false)
}
} catch (e: Exception) {
LOG.error { "Failed to handle file drop operation: ${e.message}" }
dtde.dropComplete(false)
}
}
}

// Set DropTarget on the browser component and its children
browserInstance.component()?.let { component ->
component.dropTarget = dropTarget
// Also try setting on parent if needed
component.parent?.dropTarget = dropTarget
}

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

loadingPanel.stopLoading()
}
Expand Down Expand Up @@ -219,6 +291,36 @@ class AmazonQPanel(val project: Project, private val scope: CoroutineScope) : Di
}
}

private fun validateImageFile(file: File, allowedTypes: Set<String>, maxFileSize: Double, maxDimension: Int): String? {
val fileName = file.name
val ext = fileName.substringAfterLast('.', "").lowercase()

if (ext !in allowedTypes) {
return "$fileName: File must be an image in JPEG, PNG, GIF, or WebP format."
}

if (file.length() > maxFileSize) {
return "$fileName: Image must be no more than 3.75MB in size."
}

return try {
val img = read(file)
when {
img == null -> "$fileName: File could not be read as an image."
img.width > maxDimension -> "$fileName: Image must be no more than 8,000px in width."
img.height > maxDimension -> "$fileName: Image must be no more than 8,000px in height."
else -> null
}
} catch (e: Exception) {
"$fileName: File could not be read as an image."
}
}

companion object {
private val LOG = getLogger<AmazonQPanel>()
private val OBJECT_MAPPER = jacksonObjectMapper()
}

override fun dispose() {
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,9 @@ class Browser(parent: Disposable, private val mynahAsset: Path, val project: Pro
// 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.loadURL(
assetRequestHandler.createResource(
Expand Down Expand Up @@ -130,7 +132,7 @@ class Browser(parent: Disposable, private val mynahAsset: Path, val project: Pro
<script type="text/javascript" charset="UTF-8" src="$connectorAdapterPath"></script>
<script type="text/javascript" charset="UTF-8" src="$mynahResource" defer onload="init()"></script>
<script type="text/javascript">

const init = () => {
const hybridChatConnector = connectorAdapter.initiateAdapter(
${MeetQSettings.getInstance().reinvent2024OnboardingCount < MAX_ONBOARDING_PAGE_COUNT},
Expand All @@ -147,7 +149,7 @@ class Browser(parent: Disposable, private val mynahAsset: Path, val project: Pro
},

"${activeProfile?.profileName.orEmpty()}")
amazonQChat.createChat(
const qChat = amazonQChat.createChat(
{
postMessage: message => {
$postMessageToJavaJsCode
Expand All @@ -163,6 +165,29 @@ class Browser(parent: Disposable, private val mynahAsset: Path, val project: Pro
hybridChatConnector,
${CodeWhispererFeatureConfigService.getInstance().getFeatureConfigJsonString()}
);

window.handleNativeDrop = function(filePath) {
const parsedFilePath = JSON.parse(filePath);
const contextArray = parsedFilePath.map(fullPath => {
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 @@ -31,6 +31,7 @@ import org.eclipse.lsp4j.jsonrpc.ResponseErrorException
import org.eclipse.lsp4j.jsonrpc.messages.ResponseErrorCode
import software.aws.toolkits.core.utils.error
import software.aws.toolkits.core.utils.getLogger
import software.aws.toolkits.core.utils.info
import software.aws.toolkits.core.utils.warn
import software.aws.toolkits.jetbrains.services.amazonq.apps.AppConnection
import software.aws.toolkits.jetbrains.services.amazonq.commands.MessageSerializer
Expand Down Expand Up @@ -74,9 +75,12 @@ import software.aws.toolkits.jetbrains.services.amazonq.lsp.model.aws.chat.Encry
import software.aws.toolkits.jetbrains.services.amazonq.lsp.model.aws.chat.EncryptedQuickActionChatParams
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.GetSerializedChatResponse
import software.aws.toolkits.jetbrains.services.amazonq.lsp.model.aws.chat.LIST_AVAILABLE_MODELS
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.LIST_RULES_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 @@ -301,6 +305,7 @@ class BrowserConnector(
}

CHAT_READY -> {
LOG.info { "Amazon Q Chat UI loaded and ready for input" }
handleChat(AmazonQChatServer.chatReady, node) { params, invoke ->
uiReady.complete(true)
chatCommunicationManager.setUiReady()
Expand Down Expand Up @@ -351,7 +356,22 @@ class BrowserConnector(
}

CHAT_INSERT_TO_CURSOR -> {
handleChat(AmazonQChatServer.insertToCursorPosition, node)
val editor = FileEditorManager.getInstance(project).selectedTextEditor
val textDocumentIdentifier = editor?.let { TextDocumentIdentifier(toUriString(it.virtualFile)) }
val cursorPosition = editor?.let { LspEditorUtil.getCursorPosition(it) }

val enrichmentParams = mapOf(
"textDocument" to textDocumentIdentifier,
"cursorPosition" to cursorPosition,
)

val insertToCursorPositionParams: ObjectNode = (node.params as ObjectNode)
.setAll(serializer.objectMapper.valueToTree<ObjectNode>(enrichmentParams))
val enrichedNode = (node as ObjectNode).apply {
set<JsonNode>("params", insertToCursorPositionParams)
}

handleChat(AmazonQChatServer.insertToCursorPosition, enrichedNode)
}

CHAT_LINK_CLICK -> {
Expand Down Expand Up @@ -489,6 +509,19 @@ class BrowserConnector(
)
}
}

OPEN_FILE_DIALOG -> {
handleChat(AmazonQChatServer.showOpenFileDialog, node)
.whenComplete { response, _ ->
browser.postChat(
FlareUiMessage(
command = OPEN_FILE_DIALOG_REQUEST_METHOD,
params = response
)
)
}
}

LIST_RULES_REQUEST_METHOD -> {
handleChat(AmazonQChatServer.listRules, node)
.whenComplete { response, _ ->
Expand Down Expand Up @@ -517,6 +550,17 @@ class BrowserConnector(
CHAT_PINNED_CONTEXT_REMOVE -> {
handleChat(AmazonQChatServer.pinnedContextRemove, node)
}
LIST_AVAILABLE_MODELS -> {
handleChat(AmazonQChatServer.listAvailableModels, node)
.whenComplete { response, _ ->
browser.postChat(
FlareUiMessage(
command = LIST_AVAILABLE_MODELS,
params = response
)
)
}
}
}
}

Expand Down
Loading
Loading