Skip to content
Open
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
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
// Copyright 2024 Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0

package software.aws.toolkits.eclipse.amazonq.configuration;

import org.eclipse.core.runtime.preferences.ConfigurationScope;
import org.eclipse.core.runtime.preferences.IEclipsePreferences;
import org.osgi.service.prefs.BackingStoreException;

import software.aws.toolkits.eclipse.amazonq.plugin.Activator;
import software.aws.toolkits.eclipse.amazonq.util.LoggingService;

/**
* Persists deprecation acknowledgements at the Eclipse installation level when possible.
*
* <p>Some Eclipse installations do not allow writes to the configuration scope. In those
* environments, acknowledgements fall back to the existing workspace-scoped plugin store.
*/
public final class DeprecationAcknowledgmentStore {

private static final String STORAGE_PROBE_KEY = "deprecationAcknowledgmentStorageProbe";

private final IEclipsePreferences installationPreferences;
private final PluginStore workspaceFallback;
private final LoggingService logger;

private boolean storageChecked;
private boolean useWorkspaceFallback;

public DeprecationAcknowledgmentStore(final IEclipsePreferences installationPreferences,
final PluginStore workspaceFallback, final LoggingService logger) {
this.installationPreferences = installationPreferences;
this.workspaceFallback = workspaceFallback;
this.logger = logger;
}

public static DeprecationAcknowledgmentStore getInstance() {
return InstanceHolder.INSTANCE;
}

public synchronized boolean isAcknowledged(final String key) {
ensureStorageAvailable();
if (useWorkspaceFallback) {
return "true".equals(workspaceFallback.get(key));
}
try {
return "true".equals(installationPreferences.get(key, null));
} catch (RuntimeException e) {
switchToWorkspaceFallback("reading", key, e);
return "true".equals(workspaceFallback.get(key));
}
}

public synchronized void acknowledge(final String key) {
ensureStorageAvailable();
if (useWorkspaceFallback) {
workspaceFallback.put(key, "true");
return;
}
try {
installationPreferences.put(key, "true");
installationPreferences.flush();
} catch (BackingStoreException | RuntimeException e) {
switchToWorkspaceFallback("saving", key, e);
workspaceFallback.put(key, "true");
}
}

private void ensureStorageAvailable() {
if (storageChecked) {
return;
}
storageChecked = true;
try {
installationPreferences.put(STORAGE_PROBE_KEY, "true");
installationPreferences.flush();
installationPreferences.remove(STORAGE_PROBE_KEY);
installationPreferences.flush();
} catch (BackingStoreException | RuntimeException e) {
try {
installationPreferences.remove(STORAGE_PROBE_KEY);
} catch (RuntimeException ignored) {
// The installation scope is already known to be unavailable.
}
switchToWorkspaceFallback("validating", STORAGE_PROBE_KEY, e);
}
}

private void switchToWorkspaceFallback(final String operation, final String key, final Exception exception) {
useWorkspaceFallback = true;
if (logger != null) {
logger.warn(String.format(
"Unable to use installation-level preferences while %s deprecation acknowledgement '%s'; using workspace preferences",
operation, key), exception);
}
}

private static final class InstanceHolder {
private static final DeprecationAcknowledgmentStore INSTANCE = new DeprecationAcknowledgmentStore(
ConfigurationScope.INSTANCE.getNode(Activator.PLUGIN_ID),
Activator.getPluginStore(),
Activator.getLogger());

private InstanceHolder() {
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,5 +11,6 @@ private PluginStoreKeys() {

public static final String CHAT_DISCLAIMER_ACKNOWLEDGED = "qchatDisclaimerAcknowledged";
public static final String PAIR_PROGRAMMING_ACKNOWLEDGED = "qchatPairProgrammingAcknowledged";
public static final String CHAT_DEPRECATION_NOTICE_ACKNOWLEDGED = "qchatDeprecationNoticeAcknowledged";

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
// Copyright 2024 Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0

package software.aws.toolkits.eclipse.amazonq.providers.assets;

import software.aws.toolkits.eclipse.amazonq.configuration.DeprecationAcknowledgmentStore;
import software.aws.toolkits.eclipse.amazonq.configuration.PluginStore;
import software.aws.toolkits.eclipse.amazonq.configuration.PluginStoreKeys;

final class ChatClientConfiguration {

private final boolean disclaimerAcknowledged;
private final boolean pairProgrammingAcknowledged;
private final boolean deprecationNoticeAcknowledged;

private ChatClientConfiguration(final boolean disclaimerAcknowledged,
final boolean pairProgrammingAcknowledged, final boolean deprecationNoticeAcknowledged) {
this.disclaimerAcknowledged = disclaimerAcknowledged;
this.pairProgrammingAcknowledged = pairProgrammingAcknowledged;
this.deprecationNoticeAcknowledged = deprecationNoticeAcknowledged;
}

static ChatClientConfiguration load(final PluginStore pluginStore,
final DeprecationAcknowledgmentStore deprecationAcknowledgmentStore) {
return new ChatClientConfiguration(
"true".equals(pluginStore.get(PluginStoreKeys.CHAT_DISCLAIMER_ACKNOWLEDGED)),
"true".equals(pluginStore.get(PluginStoreKeys.PAIR_PROGRAMMING_ACKNOWLEDGED)),
deprecationAcknowledgmentStore.isAcknowledged(
PluginStoreKeys.CHAT_DEPRECATION_NOTICE_ACKNOWLEDGED));
}

String toJavaScript() {
return String.format("""
disclaimerAcknowledged: %b,
pairProgrammingAcknowledged: %b,
deprecationNoticeAcknowledged: %b,
""", disclaimerAcknowledged, pairProgrammingAcknowledged, deprecationNoticeAcknowledged);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
import software.aws.toolkits.eclipse.amazonq.broker.events.ChatWebViewAssetState;
import software.aws.toolkits.eclipse.amazonq.chat.ChatCommunicationManager;
import software.aws.toolkits.eclipse.amazonq.chat.ChatTheme;
import software.aws.toolkits.eclipse.amazonq.configuration.PluginStoreKeys;
import software.aws.toolkits.eclipse.amazonq.configuration.DeprecationAcknowledgmentStore;
import software.aws.toolkits.eclipse.amazonq.plugin.Activator;
import software.aws.toolkits.eclipse.amazonq.providers.lsp.LspManagerProvider;
import software.aws.toolkits.eclipse.amazonq.util.PluginPlatform;
Expand Down Expand Up @@ -187,8 +187,8 @@ private String getEclipseWebkitScript() {
}

private String generateJS(final String jsEntrypoint) {
var disclaimerAcknowledged = Activator.getPluginStore().get(PluginStoreKeys.CHAT_DISCLAIMER_ACKNOWLEDGED);
var pairProgrammingAcknowledged = Activator.getPluginStore().get(PluginStoreKeys.PAIR_PROGRAMMING_ACKNOWLEDGED);
var clientConfiguration = ChatClientConfiguration.load(
Activator.getPluginStore(), DeprecationAcknowledgmentStore.getInstance());
return String.format("""
<script type="text/javascript" charset="UTF-8" src="%s" defer></script>
<script type="text/javascript">
Expand All @@ -208,8 +208,7 @@ function refreshUi() {
}
},
{
disclaimerAcknowledged: %b,
pairProgrammingAcknowledged: %b,
%s
agenticMode: true,
modelSelectionEnabled: true,
stringOverrides: { pinContextHint: '' },
Expand All @@ -221,8 +220,7 @@ function refreshUi() {
window.addEventListener('load', init);
%s
</script>
""", jsEntrypoint, getWaitFunction(), "true".equals(disclaimerAcknowledged), "true".equals(pairProgrammingAcknowledged),
getInputFunctions());
""", jsEntrypoint, getWaitFunction(), clientConfiguration.toJavaScript(), getInputFunctions());
}

@SuppressWarnings("MethodLength")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
import software.aws.toolkits.eclipse.amazonq.chat.ChatCommunicationManager;
import software.aws.toolkits.eclipse.amazonq.chat.ChatMessage;
import software.aws.toolkits.eclipse.amazonq.chat.models.CursorState;
import software.aws.toolkits.eclipse.amazonq.configuration.DeprecationAcknowledgmentStore;
import software.aws.toolkits.eclipse.amazonq.configuration.PluginStoreKeys;
import software.aws.toolkits.eclipse.amazonq.exception.AmazonQPluginException;
import software.aws.toolkits.eclipse.amazonq.lsp.auth.model.AuthFollowUpType;
Expand All @@ -36,9 +37,17 @@

public class AmazonQChatViewActionHandler implements ViewActionHandler {
private ChatCommunicationManager chatCommunicationManager;
private final ChatPromptAcknowledgmentHandler promptAcknowledgmentHandler;

public AmazonQChatViewActionHandler(final ChatCommunicationManager chatCommunicationManager) {
this(chatCommunicationManager, new ChatPromptAcknowledgmentHandler(
Activator.getPluginStore(), DeprecationAcknowledgmentStore.getInstance()));
}

AmazonQChatViewActionHandler(final ChatCommunicationManager chatCommunicationManager,
final ChatPromptAcknowledgmentHandler promptAcknowledgmentHandler) {
this.chatCommunicationManager = chatCommunicationManager;
this.promptAcknowledgmentHandler = promptAcknowledgmentHandler;
}

/*
Expand Down Expand Up @@ -120,10 +129,7 @@ public final void handleCommand(final ParsedCommand parsedCommand, final Browser
@SuppressWarnings("unchecked")
Map<String, String> options = (Map<String, String>) message.getData();
String messageId = options.get("messageId");

if ("programmerModeCardId".equals(messageId)) {
Activator.getPluginStore().put(PluginStoreKeys.PAIR_PROGRAMMING_ACKNOWLEDGED, "true");
}
promptAcknowledgmentHandler.acknowledge(messageId);
break;
case GET_SERIALIZED_CHAT:
ChatAsyncResultManager.getInstance().setResult(parsedCommand.getRequestId(), message.getData());
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
// Copyright 2024 Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0

package software.aws.toolkits.eclipse.amazonq.views;

import software.aws.toolkits.eclipse.amazonq.configuration.DeprecationAcknowledgmentStore;
import software.aws.toolkits.eclipse.amazonq.configuration.PluginStore;
import software.aws.toolkits.eclipse.amazonq.configuration.PluginStoreKeys;

final class ChatPromptAcknowledgmentHandler {

static final String PAIR_PROGRAMMING_MESSAGE_ID = "programmerModeCardId";
static final String DEPRECATION_NOTICE_MESSAGE_ID = "client-deprecation-notice";

private final PluginStore pluginStore;
private final DeprecationAcknowledgmentStore deprecationAcknowledgmentStore;

ChatPromptAcknowledgmentHandler(final PluginStore pluginStore,
final DeprecationAcknowledgmentStore deprecationAcknowledgmentStore) {
this.pluginStore = pluginStore;
this.deprecationAcknowledgmentStore = deprecationAcknowledgmentStore;
}

void acknowledge(final String messageId) {
if (PAIR_PROGRAMMING_MESSAGE_ID.equals(messageId)) {
pluginStore.put(PluginStoreKeys.PAIR_PROGRAMMING_ACKNOWLEDGED, "true");
} else if (DEPRECATION_NOTICE_MESSAGE_ID.equals(messageId)) {
deprecationAcknowledgmentStore.acknowledge(
PluginStoreKeys.CHAT_DEPRECATION_NOTICE_ACKNOWLEDGED);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
// Copyright 2024 Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0

package software.aws.toolkits.eclipse.amazonq.configuration;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertTrue;

import java.util.HashMap;
import java.util.Map;

import org.eclipse.core.internal.preferences.EclipsePreferences;
import org.eclipse.core.runtime.preferences.IEclipsePreferences.IPreferenceChangeListener;
import org.junit.jupiter.api.Test;
import org.osgi.service.prefs.BackingStoreException;

public final class DeprecationAcknowledgmentStoreTest {

private static final String TEST_KEY = "testAcknowledgement";

@Test
void storesAcknowledgementInInstallationPreferences() {
var installationPreferences = new EclipsePreferences();
var workspaceFallback = new InMemoryPluginStore();
var store = new DeprecationAcknowledgmentStore(
installationPreferences, workspaceFallback, null);

store.acknowledge(TEST_KEY);

assertTrue(store.isAcknowledged(TEST_KEY));
assertEquals("true", installationPreferences.get(TEST_KEY, null));
assertFalse(workspaceFallback.contains(TEST_KEY));
}

@Test
void readsUnacknowledgedInstallationPreference() {
var store = new DeprecationAcknowledgmentStore(
new EclipsePreferences(), new InMemoryPluginStore(), null);

assertFalse(store.isAcknowledged(TEST_KEY));
}

@Test
void fallsBackToWorkspaceWhenInstallationPreferencesAreUnavailable() {
var workspaceFallback = new InMemoryPluginStore();
workspaceFallback.put(TEST_KEY, "true");
var store = new DeprecationAcknowledgmentStore(
new UnwritablePreferences(), workspaceFallback, null);

assertTrue(store.isAcknowledged(TEST_KEY));

store.acknowledge("anotherKey");
assertEquals("true", workspaceFallback.get("anotherKey"));
}

private static final class UnwritablePreferences extends EclipsePreferences {
@Override
public void flush() throws BackingStoreException {
throw new BackingStoreException("configuration scope is read-only");
}
}

private static final class InMemoryPluginStore implements PluginStore {
private final Map<String, String> values = new HashMap<>();

@Override
public void put(final String key, final String value) {
values.put(key, value);
}

@Override
public String get(final String key) {
return values.get(key);
}

@Override
public void remove(final String key) {
values.remove(key);
}

@Override
public void addChangeListener(final IPreferenceChangeListener prefChangeListener) {
}

@Override
public <T> void putObject(final String key, final T value) {
throw new UnsupportedOperationException();
}

@Override
public <T> T getObject(final String key, final Class<T> type) {
throw new UnsupportedOperationException();
}

boolean contains(final String key) {
return values.containsKey(key);
}
}
}
Loading
Loading