From 20aee0ba11ed0e3745aaeb32a881da867384c733 Mon Sep 17 00:00:00 2001 From: Ishan Taldekar Date: Wed, 5 Feb 2025 15:34:28 -0500 Subject: [PATCH 1/6] Disable arrow keys if text area is empty --- .../amazonq/views/AmazonQChatWebview.java | 35 ++++++++++++++++++- 1 file changed, 34 insertions(+), 1 deletion(-) diff --git a/plugin/src/software/aws/toolkits/eclipse/amazonq/views/AmazonQChatWebview.java b/plugin/src/software/aws/toolkits/eclipse/amazonq/views/AmazonQChatWebview.java index ebe4b33d5..4c0467c37 100644 --- a/plugin/src/software/aws/toolkits/eclipse/amazonq/views/AmazonQChatWebview.java +++ b/plugin/src/software/aws/toolkits/eclipse/amazonq/views/AmazonQChatWebview.java @@ -21,6 +21,7 @@ import software.aws.toolkits.eclipse.amazonq.configuration.PluginStoreKeys; import software.aws.toolkits.eclipse.amazonq.lsp.AwsServerCapabiltiesProvider; import software.aws.toolkits.eclipse.amazonq.lsp.auth.model.AuthState; +import software.aws.toolkits.eclipse.amazonq.lsp.manager.LspStatusManager; import software.aws.toolkits.eclipse.amazonq.lsp.model.ChatOptions; import software.aws.toolkits.eclipse.amazonq.lsp.model.QuickActions; import software.aws.toolkits.eclipse.amazonq.lsp.model.QuickActionsCommandGroup; @@ -141,7 +142,7 @@ public final void onEvent(final AuthState authState) { // chat view if (browser != null && !browser.isDisposed() && !chatStateManager.hasPreservedState()) { Optional content = getContent(); - if (!content.isPresent()) { + if (!content.isPresent() && !LspStatusManager.lspFailed()) { canDisposeState = true; ViewVisibilityManager.showChatAssetMissingView("update"); } else { @@ -245,6 +246,38 @@ private String generateJS(final String jsEntrypoint) { } window.addEventListener('load', init); + + window.addEventListener('load', () => { + const textarea = document.querySelector('textarea.mynah-chat-prompt-input'); + if (textarea) { + textarea.addEventListener('keydown', (event) => { + const cursorPosition = textarea.selectionStart; + const hasText = textarea.value.length > 0; + + // block arrow keys on empty text area + switch (event.key) { + case 'ArrowLeft': + if (!hasText || cursorPosition === 0) { + event.preventDefault(); + event.stopPropagation(); + return false; + } + break; + + case 'ArrowRight': + if (!hasText || cursorPosition === textarea.value.length) { + event.preventDefault(); + event.stopPropagation(); + return false; + } + break; + + default: + return true; + } + }); + } + }); """, jsEntrypoint, getWaitFunction(), chatQuickActionConfig, "true".equals(disclaimerAcknowledged)); From 58e74c65e49bbc2a52a1c90d64288d2785123614 Mon Sep 17 00:00:00 2001 From: Ishan Taldekar Date: Wed, 5 Feb 2025 15:43:30 -0500 Subject: [PATCH 2/6] Fix unrelated changes introduced by stash apply --- plugin/META-INF/MANIFEST.MF | 2 +- .../eclipse/amazonq/views/AmazonQChatWebview.java | 8 +------- 2 files changed, 2 insertions(+), 8 deletions(-) diff --git a/plugin/META-INF/MANIFEST.MF b/plugin/META-INF/MANIFEST.MF index bb80724da..47a22e505 100644 --- a/plugin/META-INF/MANIFEST.MF +++ b/plugin/META-INF/MANIFEST.MF @@ -28,7 +28,7 @@ Require-Bundle: org.eclipse.core.runtime;bundle-version="3.31.0", org.apache.commons.logging;bundle-version="1.2.0", slf4j.api;bundle-version="2.0.13", org.apache.commons.lang3;bundle-version="3.14.0" -Bundle-Classpath: target/classes/, +Bundle-Classpath: ., target/dependency/annotations-2.28.26.jar, target/dependency/apache-client-2.28.26.jar, target/dependency/auth-2.28.26.jar, diff --git a/plugin/src/software/aws/toolkits/eclipse/amazonq/views/AmazonQChatWebview.java b/plugin/src/software/aws/toolkits/eclipse/amazonq/views/AmazonQChatWebview.java index 4c0467c37..934a0c308 100644 --- a/plugin/src/software/aws/toolkits/eclipse/amazonq/views/AmazonQChatWebview.java +++ b/plugin/src/software/aws/toolkits/eclipse/amazonq/views/AmazonQChatWebview.java @@ -21,7 +21,6 @@ import software.aws.toolkits.eclipse.amazonq.configuration.PluginStoreKeys; import software.aws.toolkits.eclipse.amazonq.lsp.AwsServerCapabiltiesProvider; import software.aws.toolkits.eclipse.amazonq.lsp.auth.model.AuthState; -import software.aws.toolkits.eclipse.amazonq.lsp.manager.LspStatusManager; import software.aws.toolkits.eclipse.amazonq.lsp.model.ChatOptions; import software.aws.toolkits.eclipse.amazonq.lsp.model.QuickActions; import software.aws.toolkits.eclipse.amazonq.lsp.model.QuickActionsCommandGroup; @@ -142,7 +141,7 @@ public final void onEvent(final AuthState authState) { // chat view if (browser != null && !browser.isDisposed() && !chatStateManager.hasPreservedState()) { Optional content = getContent(); - if (!content.isPresent() && !LspStatusManager.lspFailed()) { + if (!content.isPresent()) { canDisposeState = true; ViewVisibilityManager.showChatAssetMissingView("update"); } else { @@ -260,7 +259,6 @@ private String generateJS(final String jsEntrypoint) { if (!hasText || cursorPosition === 0) { event.preventDefault(); event.stopPropagation(); - return false; } break; @@ -268,12 +266,8 @@ private String generateJS(final String jsEntrypoint) { if (!hasText || cursorPosition === textarea.value.length) { event.preventDefault(); event.stopPropagation(); - return false; } break; - - default: - return true; } }); } From cab750ca8be061647c4e510f6bb1e8de87053df2 Mon Sep 17 00:00:00 2001 From: Ishan Taldekar Date: Fri, 14 Feb 2025 16:08:05 -0500 Subject: [PATCH 3/6] Support select all and copy shortcuts in chat (#353) * Support select all and copy keyboard shortcuts in chat * Fix submitted code block cancel button (#354) * Fix submitted code block cancel button * Fix empty overflow bubble (#356) * Fix empty pop-up when no overflow in follow up question text * Fix accidentally removed copy event listener --- .../amazonq/views/AmazonQChatWebview.java | 106 +++++++++++++++++- 1 file changed, 105 insertions(+), 1 deletion(-) diff --git a/plugin/src/software/aws/toolkits/eclipse/amazonq/views/AmazonQChatWebview.java b/plugin/src/software/aws/toolkits/eclipse/amazonq/views/AmazonQChatWebview.java index 934a0c308..236c743fd 100644 --- a/plugin/src/software/aws/toolkits/eclipse/amazonq/views/AmazonQChatWebview.java +++ b/plugin/src/software/aws/toolkits/eclipse/amazonq/views/AmazonQChatWebview.java @@ -3,6 +3,8 @@ package software.aws.toolkits.eclipse.amazonq.views; +import java.awt.Toolkit; +import java.awt.datatransfer.StringSelection; import java.util.List; import java.util.Optional; @@ -26,6 +28,8 @@ import software.aws.toolkits.eclipse.amazonq.lsp.model.QuickActionsCommandGroup; import software.aws.toolkits.eclipse.amazonq.plugin.Activator; import software.aws.toolkits.eclipse.amazonq.util.ObjectMapperFactory; +import software.aws.toolkits.eclipse.amazonq.util.PluginPlatform; +import software.aws.toolkits.eclipse.amazonq.util.PluginUtils; import software.aws.toolkits.eclipse.amazonq.util.ThreadingUtils; import software.aws.toolkits.eclipse.amazonq.views.actions.AmazonQCommonActions; @@ -91,6 +95,7 @@ public void completed(final ProgressEvent event) { amazonQCommonActions = getAmazonQCommonActions(); chatCommunicationManager.setChatUiRequestListener(this); + new BrowserFunction(browser, "ideCommand") { @Override public Object function(final Object[] arguments) { @@ -101,6 +106,24 @@ public Object function(final Object[] arguments) { } }; + new BrowserFunction(browser, "isMacOs") { + @Override + public Object function(final Object[] arguments) { + return Boolean.TRUE.equals(PluginUtils.getPlatform() == PluginPlatform.MAC); + } + }; + + new BrowserFunction(browser, "copyToClipboard") { + @Override + public Object function(final Object[] arguments) { + if (arguments.length > 0 && arguments[0] instanceof String) { + StringSelection stringSelection = new StringSelection((String) arguments[0]); + Toolkit.getDefaultToolkit().getSystemClipboard().setContents(stringSelection, null); + } + return null; + } + }; + // Inject chat theme after mynah-ui has loaded browser.addProgressListener(new ProgressAdapter() { @Override @@ -210,6 +233,16 @@ private String generateCss() { mask-position: center; scale: 60%; } + .mynah-button.mynah-button-secondary.fill-state-always.code-snippet-close-button.mynah-ui-clickable-item + .mynah-ui-icon-cancel { + -webkit-mask-size: 187.5% !important; + mask-size: 187.5% !important; + mask-position: center; + aspect-ratio: 1/1; + width: 15px; + height: 15px; + scale: 50% + } .mynah-ui-icon-tabs { -webkit-mask-size: 102% !important; mask-size: 102% !important; @@ -272,7 +305,78 @@ private String generateJS(final String jsEntrypoint) { }); } }); - + + window.addEventListener('load', () => { + const textarea = document.querySelector('textarea.mynah-chat-prompt-input'); + if (textarea) { + textarea.addEventListener("keydown", (event) => { + if (((isMacOs() && event.metaKey) || (!isMacOs() && event.ctrlKey)) && event.key === 'a') { + textarea.select(); + event.preventDefault(); + event.stopPropagation(); + } + }); + } + }); + + window.addEventListener('load', () => { + const textarea = document.querySelector('textarea.mynah-chat-prompt-input'); + if (textarea) { + textarea.addEventListener("keydown", (event) => { + if (((isMacOs() && event.metaKey) || (!isMacOs() && event.ctrlKey)) && event.key === 'c') { + copyToClipboard(textarea.value); + event.preventDefault(); + event.stopPropagation(); + } + }); + } + }); + + + const observer = new MutationObserver((mutations) => { + try { + mutations.forEach((mutation) => { + mutation.addedNodes.forEach((node) => { + if (node.nodeType === 1) { // Check if it's an element node + // Check for direct match + if (node.matches('.mynah-button.mynah-button-secondary.mynah-button-border.fill-state-always.mynah-chat-item-followup-question-option.mynah-ui-clickable-item')) { + attachEventListeners(node); + } + + // Check for nested matches + const buttons = node.querySelectorAll('.mynah-button.mynah-button-secondary.mynah-button-border.fill-state-always.mynah-chat-item-followup-question-option.mynah-ui-clickable-item'); + buttons.forEach(attachEventListeners); + } + }); + }); + } catch (error) { + console.error('Error in mutation observer:', error); + } + }); + + function attachEventListeners(element) { + if (!element || element.dataset.hasListener) return; // Prevent duplicate listeners + + element.addEventListener('mouseover', function(event) { + const textSpan = this.querySelector('span.mynah-button-label'); + if (textSpan && textSpan.scrollWidth <= textSpan.offsetWidth) { + event.stopImmediatePropagation(); + event.stopPropagation(); + event.preventDefault(); + } + }, true); + element.dataset.hasListener = 'true'; + } + + try { + observer.observe(document.body, { + childList: true, + subtree: true + }); + } catch (error) { + console.error('Error starting observer:', error); + } + """, jsEntrypoint, getWaitFunction(), chatQuickActionConfig, "true".equals(disclaimerAcknowledged)); } From 4d9b3417c0ea93a31cc3c4bf1ba37da3323008aa Mon Sep 17 00:00:00 2001 From: Ishan Taldekar Date: Fri, 14 Feb 2025 16:14:46 -0500 Subject: [PATCH 4/6] Fix checkstyle issues --- .../eclipse/amazonq/views/AmazonQChatWebview.java | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/plugin/src/software/aws/toolkits/eclipse/amazonq/views/AmazonQChatWebview.java b/plugin/src/software/aws/toolkits/eclipse/amazonq/views/AmazonQChatWebview.java index 236c743fd..37d693265 100644 --- a/plugin/src/software/aws/toolkits/eclipse/amazonq/views/AmazonQChatWebview.java +++ b/plugin/src/software/aws/toolkits/eclipse/amazonq/views/AmazonQChatWebview.java @@ -233,7 +233,8 @@ private String generateCss() { mask-position: center; scale: 60%; } - .mynah-button.mynah-button-secondary.fill-state-always.code-snippet-close-button.mynah-ui-clickable-item + .mynah-button.mynah-button-secondary.fill-state-always.code-snippet-close-button + .mynah-ui-clickable-item .mynah-ui-icon-cancel { -webkit-mask-size: 187.5% !important; mask-size: 187.5% !important; @@ -310,7 +311,8 @@ private String generateJS(final String jsEntrypoint) { const textarea = document.querySelector('textarea.mynah-chat-prompt-input'); if (textarea) { textarea.addEventListener("keydown", (event) => { - if (((isMacOs() && event.metaKey) || (!isMacOs() && event.ctrlKey)) && event.key === 'a') { + if (((isMacOs() && event.metaKey) || (!isMacOs() && event.ctrlKey)) + && event.key === 'a') { textarea.select(); event.preventDefault(); event.stopPropagation(); @@ -339,12 +341,16 @@ private String generateJS(final String jsEntrypoint) { mutation.addedNodes.forEach((node) => { if (node.nodeType === 1) { // Check if it's an element node // Check for direct match - if (node.matches('.mynah-button.mynah-button-secondary.mynah-button-border.fill-state-always.mynah-chat-item-followup-question-option.mynah-ui-clickable-item')) { + if (node.matches('.mynah-button.mynah-button-secondary + .mynah-button-border.fill-state-always.mynah-chat-item-followup-question-option + .mynah-ui-clickable-item')) { attachEventListeners(node); } // Check for nested matches - const buttons = node.querySelectorAll('.mynah-button.mynah-button-secondary.mynah-button-border.fill-state-always.mynah-chat-item-followup-question-option.mynah-ui-clickable-item'); + const buttons = node.querySelectorAll('.mynah-button.mynah-button-secondary + .mynah-button-border.fill-state-always.mynah-chat-item-followup-question-option + .mynah-ui-clickable-item'); buttons.forEach(attachEventListeners); } }); From 1c5793e697d124cefea5866bbea85d5ab519834a Mon Sep 17 00:00:00 2001 From: Ishan Taldekar Date: Tue, 18 Feb 2025 12:49:47 -0500 Subject: [PATCH 5/6] Fix Mynah UI checkstyle issues (#369) --- .../amazonq/views/AmazonQChatWebview.java | 187 ++++++++++-------- 1 file changed, 102 insertions(+), 85 deletions(-) diff --git a/plugin/src/software/aws/toolkits/eclipse/amazonq/views/AmazonQChatWebview.java b/plugin/src/software/aws/toolkits/eclipse/amazonq/views/AmazonQChatWebview.java index 37d693265..36e79a0cf 100644 --- a/plugin/src/software/aws/toolkits/eclipse/amazonq/views/AmazonQChatWebview.java +++ b/plugin/src/software/aws/toolkits/eclipse/amazonq/views/AmazonQChatWebview.java @@ -280,77 +280,120 @@ private String generateJS(final String jsEntrypoint) { window.addEventListener('load', init); - window.addEventListener('load', () => { - const textarea = document.querySelector('textarea.mynah-chat-prompt-input'); - if (textarea) { - textarea.addEventListener('keydown', (event) => { - const cursorPosition = textarea.selectionStart; - const hasText = textarea.value.length > 0; - - // block arrow keys on empty text area - switch (event.key) { - case 'ArrowLeft': - if (!hasText || cursorPosition === 0) { - event.preventDefault(); - event.stopPropagation(); - } - break; - - case 'ArrowRight': - if (!hasText || cursorPosition === textarea.value.length) { - event.preventDefault(); - event.stopPropagation(); - } - break; - } - }); - } - }); + %s - window.addEventListener('load', () => { - const textarea = document.querySelector('textarea.mynah-chat-prompt-input'); - if (textarea) { - textarea.addEventListener("keydown", (event) => { - if (((isMacOs() && event.metaKey) || (!isMacOs() && event.ctrlKey)) - && event.key === 'a') { - textarea.select(); - event.preventDefault(); - event.stopPropagation(); - } - }); - } - }); + %s - window.addEventListener('load', () => { - const textarea = document.querySelector('textarea.mynah-chat-prompt-input'); - if (textarea) { - textarea.addEventListener("keydown", (event) => { - if (((isMacOs() && event.metaKey) || (!isMacOs() && event.ctrlKey)) && event.key === 'c') { - copyToClipboard(textarea.value); - event.preventDefault(); - event.stopPropagation(); - } - }); - } - }); + %s + + + """, jsEntrypoint, getWaitFunction(), chatQuickActionConfig, + "true".equals(disclaimerAcknowledged), getArrowKeyBlockingFunction(), + getSelectAllAndCopySupportFunctions(), getPreventEmptyPopupFunction()); + } + + /* + * Generates javascript for chat options to be supplied to Chat UI defined here + * https://github.com/aws/language-servers/blob/ + * 785f8dee86e9f716fcfa29b2e27eb07a02387557/chat-client/src/client/chat.ts#L87 + */ + private String generateQuickActionConfig() { + return Optional.ofNullable(AwsServerCapabiltiesProvider.getInstance().getChatOptions()) + .map(ChatOptions::quickActions).map(QuickActions::quickActionsCommandGroups) + .map(this::serializeQuickActionCommands).orElse("[]"); + } + + private String serializeQuickActionCommands(final List quickActionCommands) { + try { + ObjectMapper mapper = ObjectMapperFactory.getInstance(); + return mapper.writeValueAsString(quickActionCommands); + } catch (Exception e) { + Activator.getLogger().warn("Error occurred when json serializing quick action commands", e); + return ""; + } + } + + private String getArrowKeyBlockingFunction() { + return """ + window.addEventListener('load', () => { + const textarea = document.querySelector('textarea.mynah-chat-prompt-input'); + if (textarea) { + textarea.addEventListener('keydown', (event) => { + const cursorPosition = textarea.selectionStart; + const hasText = textarea.value.length > 0; + + // block arrow keys on empty text area + switch (event.key) { + case 'ArrowLeft': + if (!hasText || cursorPosition === 0) { + event.preventDefault(); + event.stopPropagation(); + } + break; + + case 'ArrowRight': + if (!hasText || cursorPosition === textarea.value.length) { + event.preventDefault(); + event.stopPropagation(); + } + break; + } + }); + } + }); + """; + } + + private String getSelectAllAndCopySupportFunctions() { + return """ + window.addEventListener('load', () => { + const textarea = document.querySelector('textarea.mynah-chat-prompt-input'); + if (textarea) { + textarea.addEventListener("keydown", (event) => { + if (((isMacOs() && event.metaKey) || (!isMacOs() && event.ctrlKey)) + && event.key === 'a') { + textarea.select(); + event.preventDefault(); + event.stopPropagation(); + } + }); + } + }); + + window.addEventListener('load', () => { + const textarea = document.querySelector('textarea.mynah-chat-prompt-input'); + if (textarea) { + textarea.addEventListener("keydown", (event) => { + if (((isMacOs() && event.metaKey) || (!isMacOs() && event.ctrlKey)) + && event.key === 'c') { + copyToClipboard(textarea.value); + event.preventDefault(); + event.stopPropagation(); + } + }); + } + }); + """; + } + private String getPreventEmptyPopupFunction() { + String selector = ".mynah-button" + ".mynah-button-secondary.mynah-button-border" + ".fill-state-always" + + ".mynah-chat-item-followup-question-option" + ".mynah-ui-clickable-item"; + return """ const observer = new MutationObserver((mutations) => { try { + const selector = '%s'; + mutations.forEach((mutation) => { mutation.addedNodes.forEach((node) => { if (node.nodeType === 1) { // Check if it's an element node // Check for direct match - if (node.matches('.mynah-button.mynah-button-secondary - .mynah-button-border.fill-state-always.mynah-chat-item-followup-question-option - .mynah-ui-clickable-item')) { + if (node.matches(selector)) { attachEventListeners(node); } - // Check for nested matches - const buttons = node.querySelectorAll('.mynah-button.mynah-button-secondary - .mynah-button-border.fill-state-always.mynah-chat-item-followup-question-option - .mynah-ui-clickable-item'); + const buttons = node.querySelectorAll(); buttons.forEach(attachEventListeners); } }); @@ -359,10 +402,8 @@ private String generateJS(final String jsEntrypoint) { console.error('Error in mutation observer:', error); } }); - function attachEventListeners(element) { if (!element || element.dataset.hasListener) return; // Prevent duplicate listeners - element.addEventListener('mouseover', function(event) { const textSpan = this.querySelector('span.mynah-button-label'); if (textSpan && textSpan.scrollWidth <= textSpan.offsetWidth) { @@ -373,7 +414,6 @@ function attachEventListeners(element) { }, true); element.dataset.hasListener = 'true'; } - try { observer.observe(document.body, { childList: true, @@ -382,30 +422,7 @@ function attachEventListeners(element) { } catch (error) { console.error('Error starting observer:', error); } - - """, jsEntrypoint, getWaitFunction(), chatQuickActionConfig, - "true".equals(disclaimerAcknowledged)); - } - - /* - * Generates javascript for chat options to be supplied to Chat UI defined here - * https://github.com/aws/language-servers/blob/ - * 785f8dee86e9f716fcfa29b2e27eb07a02387557/chat-client/src/client/chat.ts#L87 - */ - private String generateQuickActionConfig() { - return Optional.ofNullable(AwsServerCapabiltiesProvider.getInstance().getChatOptions()) - .map(ChatOptions::quickActions).map(QuickActions::quickActionsCommandGroups) - .map(this::serializeQuickActionCommands).orElse("[]"); - } - - private String serializeQuickActionCommands(final List quickActionCommands) { - try { - ObjectMapper mapper = ObjectMapperFactory.getInstance(); - return mapper.writeValueAsString(quickActionCommands); - } catch (Exception e) { - Activator.getLogger().warn("Error occurred when json serializing quick action commands", e); - return ""; - } + """.formatted(selector); } @Override From c590b2539c20b5f1ed479a20acadae8be34f1cd0 Mon Sep 17 00:00:00 2001 From: Ishan Taldekar Date: Thu, 20 Feb 2025 14:35:23 -0500 Subject: [PATCH 6/6] Undo unintended manifest change --- plugin/META-INF/MANIFEST.MF | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugin/META-INF/MANIFEST.MF b/plugin/META-INF/MANIFEST.MF index dead985e5..7155eb4e8 100644 --- a/plugin/META-INF/MANIFEST.MF +++ b/plugin/META-INF/MANIFEST.MF @@ -28,7 +28,7 @@ Require-Bundle: org.eclipse.core.runtime;bundle-version="3.31.0", org.apache.commons.logging;bundle-version="1.2.0", slf4j.api;bundle-version="2.0.13", org.apache.commons.lang3;bundle-version="3.14.0" -Bundle-Classpath: ., +Bundle-Classpath: target/classes/, target/dependency/annotations-2.28.26.jar, target/dependency/apache-client-2.28.26.jar, target/dependency/auth-2.28.26.jar,