diff --git a/plugin/META-INF/MANIFEST.MF b/plugin/META-INF/MANIFEST.MF
index 7155eb4e8..9ff16a348 100644
--- a/plugin/META-INF/MANIFEST.MF
+++ b/plugin/META-INF/MANIFEST.MF
@@ -29,40 +29,45 @@ Require-Bundle: org.eclipse.core.runtime;bundle-version="3.31.0",
slf4j.api;bundle-version="2.0.13",
org.apache.commons.lang3;bundle-version="3.14.0"
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,
- target/dependency/aws-core-2.28.26.jar,
- target/dependency/aws-json-protocol-2.28.26.jar,
- target/dependency/checksums-2.28.26.jar,
- target/dependency/checksums-spi-2.28.26.jar,
- target/dependency/cognitoidentity-2.28.26.jar,
- target/dependency/commons-codec-1.17.1.jar,
- target/dependency/endpoints-spi-2.28.26.jar,
- target/dependency/http-auth-2.28.26.jar,
- target/dependency/http-auth-aws-2.28.26.jar,
- target/dependency/http-auth-aws-eventstream-2.28.26.jar,
- target/dependency/http-auth-spi-2.28.26.jar,
- target/dependency/http-client-spi-2.28.26.jar,
- target/dependency/httpclient-4.5.14.jar,
- target/dependency/httpcore-4.4.16.jar,
- target/dependency/identity-spi-2.28.26.jar,
- target/dependency/jackson-annotations-2.17.2.jar,
- target/dependency/jackson-core-2.17.2.jar,
- target/dependency/jackson-databind-2.17.2.jar,
- target/dependency/jakarta.inject-api-2.0.1.jar,
- target/dependency/json-utils-2.28.26.jar,
- target/dependency/maven-artifact-3.9.9.jar,
- target/dependency/metrics-spi-2.28.26.jar,
- target/dependency/netty-nio-client-2.28.26.jar,
- target/dependency/nimbus-jose-jwt-9.41.2.jar,
- target/dependency/profiles-2.28.26.jar,
- target/dependency/protocol-core-2.28.26.jar,
- target/dependency/reactive-streams-1.0.4.jar,
- target/dependency/regions-2.28.26.jar,
- target/dependency/retries-2.28.26.jar,
- target/dependency/retries-spi-2.28.26.jar,
- target/dependency/rxjava-3.1.5.jar,
- target/dependency/sdk-core-2.28.26.jar,
- target/dependency/third-party-jackson-core-2.28.26.jar,
- target/dependency/utils-2.28.26.jar
+ target/dependency/annotations.jar,
+ target/dependency/apache-client.jar,
+ target/dependency/auth.jar,
+ target/dependency/aws-core.jar,
+ target/dependency/aws-json-protocol.jar,
+ target/dependency/checksums-spi.jar,
+ target/dependency/checksums.jar,
+ target/dependency/cognitoidentity.jar,
+ target/dependency/commons-codec.jar,
+ target/dependency/delight-rhino-sandbox.jar,
+ target/dependency/endpoints-spi.jar,
+ target/dependency/http-auth-aws-eventstream.jar,
+ target/dependency/http-auth-aws.jar,
+ target/dependency/http-auth-spi.jar,
+ target/dependency/http-auth.jar,
+ target/dependency/http-client-spi.jar,
+ target/dependency/httpclient.jar,
+ target/dependency/httpcore.jar,
+ target/dependency/identity-spi.jar,
+ target/dependency/jackson-annotations.jar,
+ target/dependency/jackson-core.jar,
+ target/dependency/jackson-databind.jar,
+ target/dependency/jakarta.inject-api.jar,
+ target/dependency/jna-platform.jar,
+ target/dependency/jna.jar,
+ target/dependency/json-utils.jar,
+ target/dependency/maven-artifact.jar,
+ target/dependency/metrics-spi.jar,
+ target/dependency/netty-nio-client.jar,
+ target/dependency/nimbus-jose-jwt.jar,
+ target/dependency/profiles.jar,
+ target/dependency/protocol-core.jar,
+ target/dependency/proxy-vole.jar,
+ target/dependency/reactive-streams.jar,
+ target/dependency/regions.jar,
+ target/dependency/retries-spi.jar,
+ target/dependency/retries.jar,
+ target/dependency/rxjava.jar,
+ target/dependency/sdk-core.jar,
+ target/dependency/slf4j-api.jar,
+ target/dependency/third-party-jackson-core.jar,
+ target/dependency/utils.jar
diff --git a/plugin/pom.xml b/plugin/pom.xml
index 606bf886e..5ec2d85ad 100644
--- a/plugin/pom.xml
+++ b/plugin/pom.xml
@@ -42,21 +42,16 @@
-
- io.reactivex.rxjava3
- rxjava
- 3.1.5
-
+
+ io.reactivex.rxjava3
+ rxjava
+ 3.1.5
+
jakarta.inject
jakarta.inject-api
2.0.1
-
- io.reactivex.rxjava3
- rxjava
- 3.1.5
-
com.fasterxml.jackson.core
jackson-databind
@@ -106,6 +101,11 @@
maven-artifact
3.9.9
+
+ org.bidib.com.github.markusbernhardt
+ proxy-vole
+ 1.1.6
+
org.junit.jupiter
junit-jupiter
@@ -118,7 +118,7 @@
test
-
+
src
tst
@@ -148,8 +148,24 @@
copy-dependencies
+ runtime
+ true
${project.build.directory}/dependency
- io.reactivex,software.amazon.awssdk,com.fasterxml.jackson,com.nimbusds,jakarta.inject,commons-codec,org.apache.httpcomponents,org.reactivestreams,org.apache.maven
+
+ io.reactivex,
+ software.amazon.awssdk,
+ com.fasterxml.jackson,
+ com.nimbusds,jakarta.inject,
+ commons-codec,
+ org.apache.httpcomponents,
+ org.reactivestreams,
+ org.apache.maven,
+ org.bidib.com.github.markusbernhardt,
+ net.java.dev.jna,
+ org.ini4j,
+ org.javadelight,
+ org.slf4j
+
diff --git a/plugin/src/software/aws/toolkits/eclipse/amazonq/chat/ChatTheme.java b/plugin/src/software/aws/toolkits/eclipse/amazonq/chat/ChatTheme.java
index 1c031f598..6308426dd 100644
--- a/plugin/src/software/aws/toolkits/eclipse/amazonq/chat/ChatTheme.java
+++ b/plugin/src/software/aws/toolkits/eclipse/amazonq/chat/ChatTheme.java
@@ -100,6 +100,8 @@ private String getCssForDarkTheme() {
// Card
themeMap.put(QChatCssVariable.CardBackground, cardBackgroundColor);
+ themeMap.put(QChatCssVariable.LineHeight, "1.25em");
+
return getCss(themeMap);
}
@@ -150,6 +152,8 @@ private String getCssForLightTheme() {
// Card
themeMap.put(QChatCssVariable.CardBackground, cardBackgroundColor);
+ themeMap.put(QChatCssVariable.LineHeight, "1.25em");
+
return getCss(themeMap);
}
diff --git a/plugin/src/software/aws/toolkits/eclipse/amazonq/chat/models/QChatCssVariable.java b/plugin/src/software/aws/toolkits/eclipse/amazonq/chat/models/QChatCssVariable.java
index a57c7ce26..14bcd1b38 100644
--- a/plugin/src/software/aws/toolkits/eclipse/amazonq/chat/models/QChatCssVariable.java
+++ b/plugin/src/software/aws/toolkits/eclipse/amazonq/chat/models/QChatCssVariable.java
@@ -44,7 +44,10 @@ public enum QChatCssVariable {
AlternateForeground("--mynah-color-alternate-reverse"),
// Card
- CardBackground("--mynah-card-bg");
+ CardBackground("--mynah-card-bg"),
+
+ // Line height
+ LineHeight("--mynah-line-height");
private String value;
diff --git a/plugin/src/software/aws/toolkits/eclipse/amazonq/lsp/connection/QLspConnectionProvider.java b/plugin/src/software/aws/toolkits/eclipse/amazonq/lsp/connection/QLspConnectionProvider.java
index 9da462cfb..1ca8a6d36 100644
--- a/plugin/src/software/aws/toolkits/eclipse/amazonq/lsp/connection/QLspConnectionProvider.java
+++ b/plugin/src/software/aws/toolkits/eclipse/amazonq/lsp/connection/QLspConnectionProvider.java
@@ -44,10 +44,10 @@ public QLspConnectionProvider() throws IOException {
@Override
protected final void addEnvironmentVariables(final Map env) {
- String httpsProxyPreference = ProxyUtil.getHttpsProxyUrl();
+ String httpsProxyUrl = ProxyUtil.getHttpsProxyUrl();
String caCertPreference = Activator.getDefault().getPreferenceStore().getString(AmazonQPreferencePage.CA_CERT);
- if (!StringUtils.isEmpty(httpsProxyPreference)) {
- env.put("HTTPS_PROXY", httpsProxyPreference);
+ if (!StringUtils.isEmpty(httpsProxyUrl)) {
+ env.put("HTTPS_PROXY", httpsProxyUrl);
}
if (!StringUtils.isEmpty(caCertPreference)) {
env.put("NODE_EXTRA_CA_CERTS", caCertPreference);
diff --git a/plugin/src/software/aws/toolkits/eclipse/amazonq/lsp/manager/fetcher/VersionManifestFetcher.java b/plugin/src/software/aws/toolkits/eclipse/amazonq/lsp/manager/fetcher/VersionManifestFetcher.java
index fc546669e..81a4c7b72 100644
--- a/plugin/src/software/aws/toolkits/eclipse/amazonq/lsp/manager/fetcher/VersionManifestFetcher.java
+++ b/plugin/src/software/aws/toolkits/eclipse/amazonq/lsp/manager/fetcher/VersionManifestFetcher.java
@@ -32,6 +32,7 @@
import software.aws.toolkits.eclipse.amazonq.util.HttpClientFactory;
import software.aws.toolkits.eclipse.amazonq.util.ObjectMapperFactory;
import software.aws.toolkits.eclipse.amazonq.util.PluginUtils;
+import software.aws.toolkits.eclipse.amazonq.util.ThreadingUtils;
import software.aws.toolkits.eclipse.amazonq.util.ToolkitNotification;
import software.aws.toolkits.telemetry.TelemetryDefinitions.ManifestLocation;
import software.aws.toolkits.telemetry.TelemetryDefinitions.Result;
@@ -85,11 +86,25 @@ public Optional fetch() {
return latestManifest;
} catch (Exception e) {
if (e.getCause() instanceof SSLHandshakeException) {
- Display.getCurrent().asyncExec(() -> {
- AbstractNotificationPopup notification = new ToolkitNotification(Display.getCurrent(),
- Constants.IDE_SSL_HANDSHAKE_TITLE,
- Constants.IDE_SSL_HANDSHAKE_BODY);
- notification.open();
+ ThreadingUtils.executeAsyncTask(() -> {
+ Display display = null;
+ while (display == null) {
+ display = Display.getDefault();
+ if (display == null) {
+ try {
+ Thread.sleep(100);
+ } catch (InterruptedException interrupted) {
+ Thread.currentThread().interrupt();
+ return;
+ }
+ }
+ }
+ display.asyncExec(() -> {
+ AbstractNotificationPopup notification = new ToolkitNotification(Display.getCurrent(),
+ Constants.IDE_SSL_HANDSHAKE_TITLE,
+ Constants.IDE_SSL_HANDSHAKE_BODY);
+ notification.open();
+ });
});
}
Activator.getLogger().error("Error fetching manifest from remote location", e);
diff --git a/plugin/src/software/aws/toolkits/eclipse/amazonq/telemetry/service/DefaultTelemetryService.java b/plugin/src/software/aws/toolkits/eclipse/amazonq/telemetry/service/DefaultTelemetryService.java
index 3f1c9928d..0d813cab8 100644
--- a/plugin/src/software/aws/toolkits/eclipse/amazonq/telemetry/service/DefaultTelemetryService.java
+++ b/plugin/src/software/aws/toolkits/eclipse/amazonq/telemetry/service/DefaultTelemetryService.java
@@ -14,6 +14,7 @@
import javax.net.ssl.SSLContext;
import org.apache.http.conn.ssl.SSLConnectionSocketFactory;
+import org.apache.http.impl.client.SystemDefaultCredentialsProvider;
import software.amazon.awssdk.auth.credentials.AnonymousCredentialsProvider;
import software.amazon.awssdk.core.client.config.ClientOverrideConfiguration;
@@ -141,7 +142,7 @@ private static ToolkitTelemetryClient createDefaultTelemetryClient(final Region
null,
SSLConnectionSocketFactory.getDefaultHostnameVerifier()
);
- var proxyUrl = ProxyUtil.getHttpsProxyUrl();
+ var proxyUrl = ProxyUtil.getHttpsProxyUrlForEndpoint(endpoint);
var httpClientBuilder = ApacheHttpClient.builder();
if (!StringUtils.isEmpty(proxyUrl)) {
httpClientBuilder.proxyConfiguration(ProxyConfiguration.builder()
@@ -151,7 +152,9 @@ private static ToolkitTelemetryClient createDefaultTelemetryClient(final Region
httpClientBuilder.socketFactory(sslSocketFactory);
- SdkHttpClient sdkHttpClient = httpClientBuilder.build();
+ SdkHttpClient sdkHttpClient = httpClientBuilder
+ .credentialsProvider(new SystemDefaultCredentialsProvider())
+ .build();
CognitoIdentityClient cognitoClient = CognitoIdentityClient.builder()
.credentialsProvider(AnonymousCredentialsProvider.create())
.region(region)
diff --git a/plugin/src/software/aws/toolkits/eclipse/amazonq/util/ProxyUtil.java b/plugin/src/software/aws/toolkits/eclipse/amazonq/util/ProxyUtil.java
index a9b41a7ea..c1b777c10 100644
--- a/plugin/src/software/aws/toolkits/eclipse/amazonq/util/ProxyUtil.java
+++ b/plugin/src/software/aws/toolkits/eclipse/amazonq/util/ProxyUtil.java
@@ -4,91 +4,203 @@
package software.aws.toolkits.eclipse.amazonq.util;
import java.io.FileInputStream;
+import java.net.InetSocketAddress;
+import java.net.MalformedURLException;
+import java.net.Proxy;
+import java.net.ProxySelector;
import java.net.URI;
+import java.net.URISyntaxException;
+import java.net.URL;
import java.security.KeyStore;
import java.security.cert.CertificateFactory;
import java.security.cert.X509Certificate;
import javax.net.ssl.SSLContext;
+import javax.net.ssl.TrustManager;
import javax.net.ssl.TrustManagerFactory;
+import javax.net.ssl.X509TrustManager;
import org.eclipse.mylyn.commons.ui.dialogs.AbstractNotificationPopup;
import org.eclipse.swt.widgets.Display;
+import com.github.markusbernhardt.proxy.ProxySearch;
+
import software.amazon.awssdk.utils.StringUtils;
import software.aws.toolkits.eclipse.amazonq.plugin.Activator;
import software.aws.toolkits.eclipse.amazonq.preferences.AmazonQPreferencePage;
public final class ProxyUtil {
+ private static final String DEFAULT_PROXY_ENDPOINT = "https://amazonaws.com";
+ private static volatile boolean hasSeenInvalidProxyNotification;
- private static boolean hasSeenInvalidProxyNotification;
+ private static ProxySelector proxySelector;
- private ProxyUtil() {
- // Prevent initialization
- }
+ private ProxyUtil() { } // Prevent initialization
public static String getHttpsProxyUrl() {
- return getHttpsProxyUrl(System.getenv("HTTPS_PROXY"),
- Activator.getDefault().getPreferenceStore().getString(AmazonQPreferencePage.HTTPS_PROXY));
+ return getHttpsProxyUrlForEndpoint(DEFAULT_PROXY_ENDPOINT);
}
- protected static String getHttpsProxyUrl(final String envVarValue, final String prefValue) {
- String httpsProxy = envVarValue;
- if (!StringUtils.isEmpty(prefValue)) {
- httpsProxy = prefValue;
- }
+ public static String getHttpsProxyUrlForEndpoint(final String endpointUrl) {
try {
- if (!StringUtils.isEmpty(httpsProxy)) {
- URI.create(httpsProxy);
- }
- } catch (IllegalArgumentException e) {
- if (!hasSeenInvalidProxyNotification) {
- hasSeenInvalidProxyNotification = true;
- Display.getDefault().asyncExec(() -> {
- AbstractNotificationPopup notification = new ToolkitNotification(Display.getCurrent(),
- Constants.INVALID_PROXY_CONFIGURATION_TITLE,
- Constants.INVALID_PROXY_CONFIGURATION_BODY);
- notification.open();
- });
+ String proxyPrefUrl = getHttpsProxyPreferenceUrl();
+ if (!StringUtils.isEmpty(proxyPrefUrl)) {
+ return proxyPrefUrl;
}
+ } catch (MalformedURLException e) {
+ showInvalidProxyNotification();
+ return null;
+ }
+
+ if (StringUtils.isEmpty(endpointUrl)) {
+ return null;
+ }
+
+ URI endpointUri;
+ try {
+ endpointUri = new URI(endpointUrl);
+ } catch (URISyntaxException e) {
+ Activator.getLogger().error("Could not parse endpoint for proxy configuration: " + endpointUrl, e);
return null;
}
- return httpsProxy;
+
+ return getProxyUrlFromSelector(getProxySelector(), endpointUri);
+ }
+
+ private static String getProxyUrlFromSelector(final ProxySelector proxySelector, final URI endpointUri) {
+ if (proxySelector == null) {
+ return null;
+ }
+
+ var proxies = proxySelector.select(endpointUri);
+ if (proxies == null || proxies.isEmpty()) {
+ return null;
+ }
+
+ return proxies.stream()
+ .filter(p -> p.type() != Proxy.Type.DIRECT)
+ .findFirst()
+ .map(proxy -> createProxyUrl(proxy, endpointUri))
+ .orElseGet(() -> {
+ Activator.getLogger().info("No non-DIRECT proxies found for endpoint: " + endpointUri);
+ return null;
+ });
+ }
+
+ private static String createProxyUrl(final Proxy proxy, final URI endpointUri) {
+ if (!(proxy.address() instanceof InetSocketAddress addr)) {
+ return null;
+ }
+
+ String scheme = determineProxyScheme(proxy.type(), endpointUri);
+ if (scheme == null) {
+ return null;
+ }
+
+ String proxyUrl = String.format("%s://%s:%d", scheme, addr.getHostString(), addr.getPort());
+ Activator.getLogger().info("Using proxy URL: " + proxyUrl + " for endpoint: " + endpointUri);
+ return proxyUrl;
+ }
+
+ private static String determineProxyScheme(final Proxy.Type proxyType, final URI endpointUri) {
+ return switch (proxyType) {
+ case HTTP -> "http";
+ case SOCKS -> "socks";
+ default -> null;
+ };
+ }
+
+ protected static String getHttpsProxyPreferenceUrl() throws MalformedURLException {
+ String prefValue = Activator.getDefault().getPreferenceStore()
+ .getString(AmazonQPreferencePage.HTTPS_PROXY);
+
+ if (StringUtils.isEmpty(prefValue)) {
+ return null;
+ }
+
+ new URL(prefValue); // Validate URL format
+ return prefValue;
+ }
+
+ private static void showInvalidProxyNotification() {
+ if (!hasSeenInvalidProxyNotification) {
+ hasSeenInvalidProxyNotification = true;
+ Display.getDefault().asyncExec(() -> {
+ AbstractNotificationPopup notification = new ToolkitNotification(
+ Display.getCurrent(),
+ Constants.INVALID_PROXY_CONFIGURATION_TITLE,
+ Constants.INVALID_PROXY_CONFIGURATION_BODY
+ );
+ notification.open();
+ });
+ }
}
public static SSLContext getCustomSslContext() {
+ String customCertPath = getCustomCertPath();
+ if (StringUtils.isEmpty(customCertPath)) {
+ return null;
+ }
+
try {
- String customCertPath = System.getenv("NODE_EXTRA_CA_CERTS");
- String caCertPreference = Activator.getDefault().getPreferenceStore().getString(AmazonQPreferencePage.CA_CERT);
- if (!StringUtils.isEmpty(caCertPreference)) {
- customCertPath = caCertPreference;
- }
+ return createSslContextWithCustomCert(customCertPath);
+ } catch (Exception e) {
+ Activator.getLogger().error("Failed to set up SSL context. Additional certs will not be used.", e);
+ return null;
+ }
+ }
+
+ private static String getCustomCertPath() {
+ String caCertPreference = Activator.getDefault().getPreferenceStore().getString(AmazonQPreferencePage.CA_CERT);
+ return !StringUtils.isEmpty(caCertPreference) ? caCertPreference : System.getenv("NODE_EXTRA_CA_CERTS");
+ }
- if (customCertPath != null && !customCertPath.isEmpty()) {
- CertificateFactory certificateFactory = CertificateFactory.getInstance("X.509");
- X509Certificate cert;
+ private static SSLContext createSslContextWithCustomCert(final String certPath) throws Exception {
+ TrustManagerFactory tmf = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
+ tmf.init((KeyStore) null);
- try (FileInputStream fis = new FileInputStream(customCertPath)) {
- cert = (X509Certificate) certificateFactory.generateCertificate(fis);
+ KeyStore keyStore = KeyStore.getInstance(KeyStore.getDefaultType());
+ keyStore.load(null, null);
+
+ for (TrustManager tm : tmf.getTrustManagers()) {
+ if (tm instanceof X509TrustManager) {
+ X509TrustManager xtm = (X509TrustManager) tm;
+ for (X509Certificate cert : xtm.getAcceptedIssuers()) {
+ keyStore.setCertificateEntry(cert.getSubjectX500Principal().getName(), cert);
}
+ }
+ }
- KeyStore keyStore = KeyStore.getInstance(KeyStore.getDefaultType());
- keyStore.load(null, null);
- keyStore.setCertificateEntry("custom-cert", cert);
+ CertificateFactory certificateFactory = CertificateFactory.getInstance("X.509");
+ X509Certificate cert;
- TrustManagerFactory tmf = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
- tmf.init(keyStore);
+ try (FileInputStream fis = new FileInputStream(certPath)) {
+ cert = (X509Certificate) certificateFactory.generateCertificate(fis);
+ }
- SSLContext sslContext = SSLContext.getInstance("TLSv1.2");
- sslContext.init(null, tmf.getTrustManagers(), null);
- Activator.getLogger().info("Picked up custom CA cert.");
+ keyStore.setCertificateEntry("custom-cert", cert);
- return sslContext;
+ TrustManagerFactory customTmf = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
+ customTmf.init(keyStore);
+
+ SSLContext sslContext = SSLContext.getInstance("TLSv1.2");
+ sslContext.init(null, customTmf.getTrustManagers(), null);
+ Activator.getLogger().info("Picked up custom CA cert.");
+
+ return sslContext;
+ }
+
+ static synchronized ProxySelector getProxySelector() {
+ if (proxySelector == null) {
+ ProxySearch proxySearch = new ProxySearch();
+ proxySearch.addStrategy(ProxySearch.Strategy.ENV_VAR);
+ proxySearch.addStrategy(ProxySearch.Strategy.JAVA);
+ proxySearch.addStrategy(ProxySearch.Strategy.OS_DEFAULT);
+ if (System.getProperty("os.name").toLowerCase().contains("windows")) {
+ proxySearch.addStrategy(ProxySearch.Strategy.IE);
}
- } catch (Exception e) {
- Activator.getLogger().error("Failed to set up SSL context. Additional certs will not be used.", e);
+ proxySelector = proxySearch.getProxySelector();
}
- return null;
+ return proxySelector;
}
-
}
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..2de160e77 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%;
}
+ .code-snippet-close-button i.mynah-ui-icon-cancel,
+ .mynah-chat-item-card-related-content-show-more i.mynah-ui-icon-down-open {
+ -webkit-mask-size: 195.5% !important;
+ mask-size: 195.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;
@@ -218,6 +251,9 @@ private String generateCss() {
textarea:placeholder-shown {
line-height: 1.5rem;
}
+ .mynah-ui-spinner-container > span.mynah-ui-spinner-logo-part > .mynah-ui-spinner-logo-mask.text {
+ opacity: 1 !important;
+ }
""";
}
@@ -236,7 +272,8 @@ private String generateJS(final String jsEntrypoint) {
postMessage: (message) => {
ideCommand(JSON.stringify(message));
}
- }, {
+ },
+ {
quickActionCommands: %s,
disclaimerAcknowledged: %b
});
@@ -245,9 +282,19 @@ private String generateJS(final String jsEntrypoint) {
}
window.addEventListener('load', init);
+
+ %s
+
+ %s
+
+ %s
+
+ %s
+
""", jsEntrypoint, getWaitFunction(), chatQuickActionConfig,
- "true".equals(disclaimerAcknowledged));
+ "true".equals(disclaimerAcknowledged), getArrowKeyBlockingFunction(),
+ getSelectAllAndCopySupportFunctions(), getPreventEmptyPopupFunction(), getFocusOnChatPromptFunction());
}
/*
@@ -271,6 +318,137 @@ private String serializeQuickActionCommands(final List
}
}
+ 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 && node.matches(selector)) {
+ attachEventListeners(node);
+ }
+ // Check for nested matches
+ if (node.querySelectorAll) {
+ const buttons = node.querySelectorAll(selector); // Missing selector parameter
+ buttons.forEach(attachEventListeners);
+ }
+ }
+ });
+ });
+ } catch (error) {
+ console.error('Error in mutation observer:', error);
+ }
+ });
+
+ function attachEventListeners(element) {
+ if (!element || element.dataset.hasListener) return; // Prevent duplicate listeners
+
+ const handleMouseOver = function(event) {
+ const textSpan = this.querySelector('span.mynah-button-label');
+ if (textSpan && textSpan.scrollWidth <= textSpan.offsetWidth) {
+ event.stopImmediatePropagation();
+ event.stopPropagation();
+ event.preventDefault();
+ }
+ };
+
+ element.addEventListener('mouseover', handleMouseOver, true);
+ element.dataset.hasListener = 'true';
+ }
+
+ observer.observe(document.body, {
+ childList: true,
+ subtree: true,
+ attributes: true
+ });
+ """.formatted(selector);
+ }
+
+ private String getFocusOnChatPromptFunction() {
+ return """
+ window.addEventListener('load', () => {
+ const chatContainer = document.querySelector('.mynah-chat-prompt');
+ if (chatContainer) {
+ chatContainer.addEventListener('click', (event) => {
+ if (!event.target.closest('.mynah-chat-prompt-input')) {
+ keepFocusOnPrompt();
+ }
+ });
+ }
+ });
+ """;
+ }
+
@Override
public final void onSendToChatUi(final String message) {
String script = "window.postMessage(" + message + ");";
diff --git a/plugin/tst/software/aws/toolkits/eclipse/amazonq/chat/models/QChatCssVariableTest.java b/plugin/tst/software/aws/toolkits/eclipse/amazonq/chat/models/QChatCssVariableTest.java
index 6ec137cfc..45ae883ef 100644
--- a/plugin/tst/software/aws/toolkits/eclipse/amazonq/chat/models/QChatCssVariableTest.java
+++ b/plugin/tst/software/aws/toolkits/eclipse/amazonq/chat/models/QChatCssVariableTest.java
@@ -14,7 +14,7 @@ public class QChatCssVariableTest {
@Test
void testEnumValues() {
- assertEquals(27, QChatCssVariable.values().length);
+ assertEquals(28, QChatCssVariable.values().length);
}
@Test
@@ -72,6 +72,11 @@ void testCardValues() {
assertEquals("--mynah-card-bg", QChatCssVariable.CardBackground.getValue());
}
+ @Test
+ void testLineHeighValues() {
+ assertEquals("--mynah-line-height", QChatCssVariable.LineHeight.getValue());
+ }
+
@Test
void testInvalidEnum() {
assertThrows(IllegalArgumentException.class, () -> QChatCssVariable.valueOf("NonExistentVariable"));
diff --git a/plugin/tst/software/aws/toolkits/eclipse/amazonq/util/ProxyUtilTest.java b/plugin/tst/software/aws/toolkits/eclipse/amazonq/util/ProxyUtilTest.java
index c9e5e983e..1525172eb 100644
--- a/plugin/tst/software/aws/toolkits/eclipse/amazonq/util/ProxyUtilTest.java
+++ b/plugin/tst/software/aws/toolkits/eclipse/amazonq/util/ProxyUtilTest.java
@@ -3,58 +3,134 @@
package software.aws.toolkits.eclipse.amazonq.util;
+import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.RegisterExtension;
import org.mockito.MockedStatic;
+import org.eclipse.jface.preference.IPreferenceStore;
+import org.eclipse.swt.widgets.Display;
+
+import software.aws.toolkits.eclipse.amazonq.extensions.implementation.ActivatorStaticMockExtension;
+import software.aws.toolkits.eclipse.amazonq.plugin.Activator;
+import software.aws.toolkits.eclipse.amazonq.preferences.AmazonQPreferencePage;
+
+import java.net.InetSocketAddress;
+import java.net.Proxy;
+import java.net.ProxySelector;
+import java.util.Arrays;
+import java.util.Collections;
import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.CALLS_REAL_METHODS;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.mockStatic;
+import static org.mockito.Mockito.when;
-import org.eclipse.swt.widgets.Display;
+public final class ProxyUtilTest {
+
+ @RegisterExtension
+ private static ActivatorStaticMockExtension activatorStaticMockExtension = new ActivatorStaticMockExtension();
-class ProxyUtilTest {
+ private IPreferenceStore preferenceStore;
+ private ProxySelector proxySelector;
+
+ @BeforeEach
+ void setUp() {
+ preferenceStore = mock(IPreferenceStore.class);
+ proxySelector = mock(ProxySelector.class);
+
+ Activator activatorMock = activatorStaticMockExtension.getMock(Activator.class);
+ when(activatorMock.getPreferenceStore()).thenReturn(preferenceStore);
+ }
@Test
- void testNoProxyConfigReturnsNull() {
- assertEquals(null, ProxyUtil.getHttpsProxyUrl(null, null));
+ void testNoProxyConfigurationReturnsNull() {
+ when(preferenceStore.getString(AmazonQPreferencePage.HTTPS_PROXY)).thenReturn("");
+ try (MockedStatic proxyUtilMock = mockStatic(ProxyUtil.class, CALLS_REAL_METHODS)) {
+ proxyUtilMock.when(ProxyUtil::getProxySelector).thenReturn(proxySelector);
+ when(proxySelector.select(any())).thenReturn(Collections.emptyList());
+
+ assertNull(ProxyUtil.getHttpsProxyUrl());
+ }
}
@Test
- void testEnvVarProxyUrl() {
- String mockUrl = "http://foo.com:8888";
- assertEquals(mockUrl, ProxyUtil.getHttpsProxyUrl(mockUrl, null));
+ void testPreferenceProxyUrlTakesPrecedence() {
+ String preferenceUrl = "http://preference.proxy:8888";
+ when(preferenceStore.getString(AmazonQPreferencePage.HTTPS_PROXY)).thenReturn(preferenceUrl);
+
+ assertEquals(preferenceUrl, ProxyUtil.getHttpsProxyUrlForEndpoint("https://foo.com"));
}
@Test
- void testPreferenceProxyUrl() {
- String mockUrl = "http://foo.com:8888";
- assertEquals(mockUrl, ProxyUtil.getHttpsProxyUrl(null, mockUrl));
+ void testSystemProxyConfiguration() {
+ when(preferenceStore.getString(AmazonQPreferencePage.HTTPS_PROXY)).thenReturn("");
+
+ try (MockedStatic proxyUtilMock = mockStatic(ProxyUtil.class, CALLS_REAL_METHODS)) {
+ proxyUtilMock.when(ProxyUtil::getProxySelector).thenReturn(proxySelector);
+
+ Proxy httpProxy = new Proxy(Proxy.Type.HTTP, new InetSocketAddress("proxy.example.com", 8080));
+ when(proxySelector.select(any())).thenReturn(Arrays.asList(httpProxy));
+
+ assertEquals("http://proxy.example.com:8080", ProxyUtil.getHttpsProxyUrlForEndpoint("https://foo.com"));
+ }
}
@Test
- void testPreferenceProxyUrlPrecedence() {
- String mockUrl = "http://foo.com:8888";
- assertEquals(mockUrl, ProxyUtil.getHttpsProxyUrl("http://bar.com:8888", mockUrl));
+ void testSocksProxyConfiguration() {
+ when(preferenceStore.getString(AmazonQPreferencePage.HTTPS_PROXY)).thenReturn("");
+
+ try (MockedStatic proxyUtilMock = mockStatic(ProxyUtil.class, CALLS_REAL_METHODS)) {
+ proxyUtilMock.when(ProxyUtil::getProxySelector).thenReturn(proxySelector);
+
+ Proxy socksProxy = new Proxy(Proxy.Type.SOCKS, new InetSocketAddress("socks.example.com", 1080));
+ when(proxySelector.select(any())).thenReturn(Arrays.asList(socksProxy));
+
+ assertEquals("socks://socks.example.com:1080", ProxyUtil.getHttpsProxyUrlForEndpoint("https://foo.com"));
+ }
}
@Test
- void testEnvVarInvalidProxyUrl() {
+ void testInvalidPreferenceProxyUrl() {
+ when(preferenceStore.getString(AmazonQPreferencePage.HTTPS_PROXY)).thenReturn("invalid:url");
+
try (MockedStatic displayMock = mockStatic(Display.class)) {
Display mockDisplay = mock(Display.class);
displayMock.when(Display::getDefault).thenReturn(mockDisplay);
- String mockUrl = "127.0.0.1:8000";
- assertEquals(null, ProxyUtil.getHttpsProxyUrl(mockUrl, null));
+ when(Display.getCurrent()).thenReturn(mockDisplay);
+
+ assertNull(ProxyUtil.getHttpsProxyUrlForEndpoint("https://foo.com"));
}
}
@Test
- void testPreferenceInvalidProxyUrl() {
- try (MockedStatic displayMock = mockStatic(Display.class)) {
- Display mockDisplay = mock(Display.class);
- displayMock.when(Display::getDefault).thenReturn(mockDisplay);
- String mockUrl = "127.0.0.1:8000";
- assertEquals(null, ProxyUtil.getHttpsProxyUrl(null, mockUrl));
+ void testDirectProxyReturnsNull() {
+ when(preferenceStore.getString(AmazonQPreferencePage.HTTPS_PROXY)).thenReturn("");
+
+ try (MockedStatic proxyUtilMock = mockStatic(ProxyUtil.class, CALLS_REAL_METHODS)) {
+ proxyUtilMock.when(ProxyUtil::getProxySelector).thenReturn(proxySelector);
+
+ Proxy directProxy = Proxy.NO_PROXY;
+ when(proxySelector.select(any())).thenReturn(Arrays.asList(directProxy));
+
+ assertNull(ProxyUtil.getHttpsProxyUrlForEndpoint("https://foo.com"));
}
}
+ @Test
+ void testPreservesEndpointScheme() {
+ when(preferenceStore.getString(AmazonQPreferencePage.HTTPS_PROXY)).thenReturn("");
+
+ try (MockedStatic proxyUtilMock = mockStatic(ProxyUtil.class, CALLS_REAL_METHODS)) {
+ proxyUtilMock.when(ProxyUtil::getProxySelector).thenReturn(proxySelector);
+
+ Proxy httpProxy = new Proxy(Proxy.Type.HTTP, new InetSocketAddress("proxy.example.com", 8080));
+ when(proxySelector.select(any())).thenReturn(Arrays.asList(httpProxy));
+
+ assertEquals("http://proxy.example.com:8080", ProxyUtil.getHttpsProxyUrlForEndpoint("http://foo.com"));
+ }
+ }
}
+
diff --git a/pom.xml b/pom.xml
index ad3e8b64a..0190ac67f 100644
--- a/pom.xml
+++ b/pom.xml
@@ -27,11 +27,7 @@
p2
https://download.eclipse.org/releases/2024-06
-
- lsp4e
- p2
- http://download.eclipse.org/lsp4e/releases/latest/
-
+