diff --git a/build.gradle.kts b/build.gradle.kts index 577965d2..c434e189 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -65,6 +65,8 @@ dependencies { implementation(kotlin("stdlib")) implementation(kotlin("reflect")) implementation(libs.jsoup) + implementation(libs.kotlinx.coroutines.swing) + implementation(libs.kotlinx.coroutines.core) implementation(libs.commons.text) implementation(libs.jtokkit) testImplementation(kotlin("test")) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 55578148..e469525e 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -15,6 +15,7 @@ kotlin = "2.0.0" llm-client = "0.8.35" okio = "3.9.0" tree-sitter = "0.24.4" +coroutines = "1.7.3" [libraries] analytics = { module = "com.rudderstack.sdk.java.analytics:analytics", version.ref = "analytics" } @@ -31,6 +32,8 @@ kotlin-gradle-plugin = { module = "org.jetbrains.kotlin:kotlin-gradle-plugin", v llm-client = { module = "ee.carlrobert:llm-client", version.ref = "llm-client" } okio = { module = "com.squareup.okio:okio", version.ref = "okio" } tree-sitter = { module = "io.github.bonede:tree-sitter", version.ref = "tree-sitter" } +kotlinx-coroutines-swing = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-swing", version.ref = "coroutines" } +kotlinx-coroutines-core = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core", version.ref = "coroutines" } [plugins] changelog = { id = "org.jetbrains.changelog", version.ref = "changelog" } diff --git a/src/main/java/ee/carlrobert/codegpt/completions/CompletionClientProvider.java b/src/main/java/ee/carlrobert/codegpt/completions/CompletionClientProvider.java index 2989dcaf..b91b96c6 100644 --- a/src/main/java/ee/carlrobert/codegpt/completions/CompletionClientProvider.java +++ b/src/main/java/ee/carlrobert/codegpt/completions/CompletionClientProvider.java @@ -28,19 +28,19 @@ public class CompletionClientProvider { public static CodeGPTClient getCodeGPTClient() { return new CodeGPTClient( - getCredential(CredentialKey.CODEGPT_API_KEY), + getCredential(CredentialKey.CodeGptApiKey.INSTANCE), getDefaultClientBuilder()); } public static OpenAIClient getOpenAIClient() { - return new OpenAIClient.Builder(getCredential(CredentialKey.OPENAI_API_KEY)) + return new OpenAIClient.Builder(getCredential(CredentialKey.OpenaiApiKey.INSTANCE)) .setOrganization(OpenAISettings.getCurrentState().getOrganization()) .build(getDefaultClientBuilder()); } public static ClaudeClient getClaudeClient() { return new ClaudeClient( - getCredential(CredentialKey.ANTHROPIC_API_KEY), + getCredential(CredentialKey.AnthropicApiKey.INSTANCE), AnthropicSettings.getCurrentState().getApiVersion(), getDefaultClientBuilder()); } @@ -53,8 +53,8 @@ public class CompletionClientProvider { settings.getApiVersion()); var useAzureActiveDirectoryAuthentication = settings.isUseAzureActiveDirectoryAuthentication(); var credential = useAzureActiveDirectoryAuthentication - ? getCredential(CredentialKey.AZURE_ACTIVE_DIRECTORY_TOKEN) - : getCredential(CredentialKey.AZURE_OPENAI_API_KEY); + ? getCredential(CredentialKey.AzureActiveDirectoryToken.INSTANCE) + : getCredential(CredentialKey.AzureOpenaiApiKey.INSTANCE); return new AzureClient.Builder(credential, params) .setActiveDirectoryAuthentication(useAzureActiveDirectoryAuthentication) .build(getDefaultClientBuilder()); @@ -66,7 +66,7 @@ public class CompletionClientProvider { .setPort(llamaSettings.getServerPort()); if (!llamaSettings.isRunLocalServer()) { builder.setHost(llamaSettings.getBaseHost()); - String apiKey = getCredential(CredentialKey.LLAMA_API_KEY); + String apiKey = getCredential(CredentialKey.LlamaApiKey.INSTANCE); if (apiKey != null && !apiKey.isBlank()) { builder.setApiKey(apiKey); } @@ -82,7 +82,7 @@ public class CompletionClientProvider { var builder = new OllamaClient.Builder() .setHost(host); - String apiKey = getCredential(CredentialKey.OLLAMA_API_KEY); + String apiKey = getCredential(CredentialKey.OllamaApikey.INSTANCE); if (apiKey != null && !apiKey.isBlank()) { builder.setApiKey(apiKey); } @@ -90,7 +90,7 @@ public class CompletionClientProvider { } public static GoogleClient getGoogleClient() { - return new GoogleClient.Builder(getCredential(CredentialKey.GOOGLE_API_KEY)) + return new GoogleClient.Builder(getCredential(CredentialKey.GoogleApiKey.INSTANCE)) .build(getDefaultClientBuilder()); } diff --git a/src/main/java/ee/carlrobert/codegpt/completions/CompletionRequestService.java b/src/main/java/ee/carlrobert/codegpt/completions/CompletionRequestService.java index 7ad5d810..327da029 100644 --- a/src/main/java/ee/carlrobert/codegpt/completions/CompletionRequestService.java +++ b/src/main/java/ee/carlrobert/codegpt/completions/CompletionRequestService.java @@ -197,13 +197,15 @@ public final class CompletionRequestService { private static boolean isRequestAllowed(ServiceType serviceType) { return switch (serviceType) { - case OPENAI -> CredentialsStore.INSTANCE.isCredentialSet(CredentialKey.OPENAI_API_KEY); + case OPENAI -> CredentialsStore.INSTANCE.isCredentialSet(CredentialKey.OpenaiApiKey.INSTANCE); case AZURE -> CredentialsStore.INSTANCE.isCredentialSet( AzureSettings.getCurrentState().isUseAzureApiKeyAuthentication() - ? CredentialKey.AZURE_OPENAI_API_KEY - : CredentialKey.AZURE_ACTIVE_DIRECTORY_TOKEN); - case ANTHROPIC -> CredentialsStore.INSTANCE.isCredentialSet(CredentialKey.ANTHROPIC_API_KEY); - case GOOGLE -> CredentialsStore.INSTANCE.isCredentialSet(CredentialKey.GOOGLE_API_KEY); + ? CredentialKey.AzureOpenaiApiKey.INSTANCE + : CredentialKey.AzureActiveDirectoryToken.INSTANCE); + case ANTHROPIC -> CredentialsStore.INSTANCE.isCredentialSet( + CredentialKey.AnthropicApiKey.INSTANCE + ); + case GOOGLE -> CredentialsStore.INSTANCE.isCredentialSet(CredentialKey.GoogleApiKey.INSTANCE); case CODEGPT, CUSTOM_OPENAI, LLAMA_CPP, OLLAMA -> true; }; } diff --git a/src/main/java/ee/carlrobert/codegpt/settings/service/anthropic/AnthropicSettingsForm.java b/src/main/java/ee/carlrobert/codegpt/settings/service/anthropic/AnthropicSettingsForm.java index ed11ec5c..9039e1bc 100644 --- a/src/main/java/ee/carlrobert/codegpt/settings/service/anthropic/AnthropicSettingsForm.java +++ b/src/main/java/ee/carlrobert/codegpt/settings/service/anthropic/AnthropicSettingsForm.java @@ -1,7 +1,5 @@ package ee.carlrobert.codegpt.settings.service.anthropic; -import static ee.carlrobert.codegpt.credentials.CredentialsStore.CredentialKey.ANTHROPIC_API_KEY; - import com.intellij.openapi.application.ApplicationManager; import com.intellij.ui.components.JBPasswordField; import com.intellij.ui.components.JBTextField; @@ -24,7 +22,9 @@ public class AnthropicSettingsForm { apiKeyField = new JBPasswordField(); apiKeyField.setColumns(30); ApplicationManager.getApplication().executeOnPooledThread(() -> { - var apiKey = CredentialsStore.getCredential(ANTHROPIC_API_KEY); + var apiKey = CredentialsStore.getCredential( + CredentialsStore.CredentialKey.AnthropicApiKey.INSTANCE + ); SwingUtilities.invokeLater(() -> apiKeyField.setText(apiKey)); }); apiVersionField = new JBTextField(settings.getApiVersion(), 35); @@ -65,7 +65,9 @@ public class AnthropicSettingsForm { public void resetForm() { var state = AnthropicSettings.getCurrentState(); - apiKeyField.setText(CredentialsStore.getCredential(ANTHROPIC_API_KEY)); + apiKeyField.setText( + CredentialsStore.getCredential(CredentialsStore.CredentialKey.AnthropicApiKey.INSTANCE) + ); apiVersionField.setText(state.getApiVersion()); modelField.setText(state.getModel()); } diff --git a/src/main/java/ee/carlrobert/codegpt/settings/service/azure/AzureSettingsForm.java b/src/main/java/ee/carlrobert/codegpt/settings/service/azure/AzureSettingsForm.java index 10f443a2..2e495cc1 100644 --- a/src/main/java/ee/carlrobert/codegpt/settings/service/azure/AzureSettingsForm.java +++ b/src/main/java/ee/carlrobert/codegpt/settings/service/azure/AzureSettingsForm.java @@ -41,7 +41,7 @@ public class AzureSettingsForm { azureApiKeyField = new JBPasswordField(); azureApiKeyField.setColumns(30); ApplicationManager.getApplication().executeOnPooledThread(() -> { - var apiKey = CredentialsStore.getCredential(CredentialKey.AZURE_OPENAI_API_KEY); + var apiKey = CredentialsStore.getCredential(CredentialKey.AzureOpenaiApiKey.INSTANCE); SwingUtilities.invokeLater(() -> azureApiKeyField.setText(apiKey)); }); azureApiKeyFieldPanel = UI.PanelFactory.panel(azureApiKeyField) @@ -51,7 +51,7 @@ public class AzureSettingsForm { azureActiveDirectoryTokenField = new JBPasswordField(); azureActiveDirectoryTokenField.setColumns(30); ApplicationManager.getApplication().executeOnPooledThread(() -> { - var apiKey = CredentialsStore.getCredential(CredentialKey.AZURE_ACTIVE_DIRECTORY_TOKEN); + var apiKey = CredentialsStore.getCredential(CredentialKey.AzureActiveDirectoryToken.INSTANCE); SwingUtilities.invokeLater(() -> azureActiveDirectoryTokenField.setText(apiKey)); }); azureActiveDirectoryTokenFieldPanel = UI.PanelFactory.panel(azureActiveDirectoryTokenField) @@ -125,9 +125,11 @@ public class AzureSettingsForm { public void resetForm() { var state = AzureSettings.getCurrentState(); - azureApiKeyField.setText(CredentialsStore.getCredential(CredentialKey.AZURE_OPENAI_API_KEY)); + azureApiKeyField.setText( + CredentialsStore.getCredential(CredentialKey.AzureOpenaiApiKey.INSTANCE) + ); azureActiveDirectoryTokenField.setText( - CredentialsStore.getCredential(CredentialKey.AZURE_ACTIVE_DIRECTORY_TOKEN)); + CredentialsStore.getCredential(CredentialKey.AzureActiveDirectoryToken.INSTANCE)); useAzureApiKeyAuthenticationRadioButton.setSelected(state.isUseAzureApiKeyAuthentication()); useAzureActiveDirectoryAuthenticationRadioButton.setSelected( state.isUseAzureActiveDirectoryAuthentication()); diff --git a/src/main/java/ee/carlrobert/codegpt/settings/service/llama/LlamaSettings.java b/src/main/java/ee/carlrobert/codegpt/settings/service/llama/LlamaSettings.java index dd61e807..e62c544b 100644 --- a/src/main/java/ee/carlrobert/codegpt/settings/service/llama/LlamaSettings.java +++ b/src/main/java/ee/carlrobert/codegpt/settings/service/llama/LlamaSettings.java @@ -1,6 +1,6 @@ package ee.carlrobert.codegpt.settings.service.llama; -import static ee.carlrobert.codegpt.credentials.CredentialsStore.CredentialKey.LLAMA_API_KEY; +import static ee.carlrobert.codegpt.credentials.CredentialsStore.CredentialKey.LlamaApiKey; import static ee.carlrobert.codegpt.settings.service.ServiceType.LLAMA_CPP; import static org.apache.commons.lang3.SystemUtils.IS_OS_LINUX; import static org.apache.commons.lang3.SystemUtils.IS_OS_MAC_OSX; @@ -77,7 +77,7 @@ public class LlamaSettings implements PersistentStateComponent { - var apiKey = CredentialsStore.getCredential(CredentialKey.LLAMA_API_KEY); + var apiKey = CredentialsStore.getCredential(CredentialKey.LlamaApiKey.INSTANCE); SwingUtilities.invokeLater(() -> apiKeyField.setText(apiKey)); }); @@ -140,7 +140,7 @@ public class LlamaServerPreferencesForm { additionalEnvironmentVariablesField.setText(state.getAdditionalEnvironmentVariables()); remotePromptTemplatePanel.setPromptTemplate(state.getRemoteModelPromptTemplate()); // ? infillPromptTemplatePanel.setPromptTemplate(state.getRemoteModelInfillPromptTemplate()); - apiKeyField.setText(CredentialsStore.getCredential(LLAMA_API_KEY)); + apiKeyField.setText(CredentialsStore.getCredential(LlamaApiKey.INSTANCE)); } public JComponent createUseExistingServerForm() { diff --git a/src/main/java/ee/carlrobert/codegpt/settings/service/openai/OpenAISettingsForm.java b/src/main/java/ee/carlrobert/codegpt/settings/service/openai/OpenAISettingsForm.java index d76353e8..97ecfa11 100644 --- a/src/main/java/ee/carlrobert/codegpt/settings/service/openai/OpenAISettingsForm.java +++ b/src/main/java/ee/carlrobert/codegpt/settings/service/openai/OpenAISettingsForm.java @@ -1,7 +1,5 @@ package ee.carlrobert.codegpt.settings.service.openai; -import static ee.carlrobert.codegpt.credentials.CredentialsStore.CredentialKey.OPENAI_API_KEY; - import com.intellij.openapi.application.ApplicationManager; import com.intellij.openapi.ui.ComboBox; import com.intellij.ui.EnumComboBoxModel; @@ -29,7 +27,7 @@ public class OpenAISettingsForm { apiKeyField = new JBPasswordField(); apiKeyField.setColumns(30); ApplicationManager.getApplication().executeOnPooledThread(() -> { - var apiKey = CredentialsStore.getCredential(CredentialKey.OPENAI_API_KEY); + var apiKey = CredentialsStore.getCredential(CredentialKey.OpenaiApiKey.INSTANCE); SwingUtilities.invokeLater(() -> apiKeyField.setText(apiKey)); }); organizationField = new JBTextField(settings.getOrganization(), 30); @@ -84,7 +82,7 @@ public class OpenAISettingsForm { public void resetForm() { var state = OpenAISettings.getCurrentState(); - apiKeyField.setText(CredentialsStore.getCredential(OPENAI_API_KEY)); + apiKeyField.setText(CredentialsStore.getCredential(CredentialKey.OpenaiApiKey.INSTANCE)); completionModelComboBox.setSelectedItem( OpenAIChatCompletionModel.findByCode(state.getModel())); organizationField.setText(state.getOrganization()); diff --git a/src/main/java/ee/carlrobert/codegpt/toolwindow/chat/ui/ChatToolWindowScrollablePanel.java b/src/main/java/ee/carlrobert/codegpt/toolwindow/chat/ui/ChatToolWindowScrollablePanel.java index ca017608..976b1ab0 100644 --- a/src/main/java/ee/carlrobert/codegpt/toolwindow/chat/ui/ChatToolWindowScrollablePanel.java +++ b/src/main/java/ee/carlrobert/codegpt/toolwindow/chat/ui/ChatToolWindowScrollablePanel.java @@ -35,7 +35,7 @@ public class ChatToolWindowScrollablePanel extends ScrollablePanel { clearAll(); add(landingView); if (GeneralSettings.isSelected(ServiceType.CODEGPT) - && !CredentialsStore.INSTANCE.isCredentialSet(CredentialKey.CODEGPT_API_KEY)) { + && !CredentialsStore.INSTANCE.isCredentialSet(CredentialKey.CodeGptApiKey.INSTANCE)) { var panel = new ResponseMessagePanel(); panel.addContent(UIUtil.createTextPane(""" diff --git a/src/main/java/ee/carlrobert/codegpt/toolwindow/chat/ui/textarea/ModelComboBoxAction.java b/src/main/java/ee/carlrobert/codegpt/toolwindow/chat/ui/textarea/ModelComboBoxAction.java index 44094a0c..7f8c7adf 100644 --- a/src/main/java/ee/carlrobert/codegpt/toolwindow/chat/ui/textarea/ModelComboBoxAction.java +++ b/src/main/java/ee/carlrobert/codegpt/toolwindow/chat/ui/textarea/ModelComboBoxAction.java @@ -33,7 +33,8 @@ import ee.carlrobert.codegpt.settings.service.ServiceType; import ee.carlrobert.codegpt.settings.service.codegpt.CodeGPTAvailableModels; import ee.carlrobert.codegpt.settings.service.codegpt.CodeGPTModel; import ee.carlrobert.codegpt.settings.service.codegpt.CodeGPTServiceSettings; -import ee.carlrobert.codegpt.settings.service.custom.CustomServiceSettings; +import ee.carlrobert.codegpt.settings.service.custom.CustomServiceSettingsState; +import ee.carlrobert.codegpt.settings.service.custom.CustomServicesSettings; import ee.carlrobert.codegpt.settings.service.google.GoogleSettings; import ee.carlrobert.codegpt.settings.service.llama.LlamaSettings; import ee.carlrobert.codegpt.settings.service.ollama.OllamaSettings; @@ -100,7 +101,7 @@ public class ModelComboBoxAction extends ComboBoxAction { @Override protected JBPopup createActionPopup(DefaultActionGroup group, @NotNull DataContext context, - @Nullable Runnable disposeCallback) { + @Nullable Runnable disposeCallback) { ListPopup popup = (ListPopup) super.createActionPopup(group, context, disposeCallback); popup.setShowSubmenuOnHover(true); return popup; @@ -129,8 +130,7 @@ public class ModelComboBoxAction extends ComboBoxAction { var openaiGroup = DefaultActionGroup.createPopupGroup(() -> "OpenAI"); openaiGroup.getTemplatePresentation().setIcon(Icons.OpenAI); List.of( - OpenAIChatCompletionModel.O_3_MINI, - OpenAIChatCompletionModel.O_1_PREVIEW, + OpenAIChatCompletionModel.O_3_MINI, OpenAIChatCompletionModel.O_1_PREVIEW, OpenAIChatCompletionModel.O_1_MINI, OpenAIChatCompletionModel.GPT_4_O, OpenAIChatCompletionModel.GPT_4_O_MINI, @@ -139,14 +139,17 @@ public class ModelComboBoxAction extends ComboBoxAction { actionGroup.add(openaiGroup); } if (availableProviders.contains(CUSTOM_OPENAI)) { - actionGroup.add(createModelAction( - CUSTOM_OPENAI, - "Custom: " + ApplicationManager.getApplication().getService(CustomServiceSettings.class) - .getState() - .getTemplate() - .getProviderName(), - Icons.OpenAI, - presentation)); + List services = ApplicationManager.getApplication() + .getService(CustomServicesSettings.class) + .getState() + .getServices(); + + var customGroup = DefaultActionGroup.createPopupGroup(() -> "Custom OpenAI"); + customGroup.getTemplatePresentation().setIcon(Icons.OpenAI); + services.forEach(model -> + customGroup.add(createCustomOpenAIModelAction(model, presentation)) + ); + actionGroup.add(customGroup); } if (availableProviders.contains(ANTHROPIC)) { actionGroup.add(createModelAction( @@ -222,15 +225,14 @@ public class ModelComboBoxAction extends ComboBoxAction { // TODO: Find out why another provider's model was stored in the first place templatePresentation.setText(OpenAIChatCompletionModel.GPT_4_O.getDescription()); } - break; case CUSTOM_OPENAI: templatePresentation.setIcon(Icons.OpenAI); templatePresentation.setText( - "Custom: " + application.getService(CustomServiceSettings.class) + application.getService(CustomServicesSettings.class) .getState() - .getTemplate() - .getProviderName()); + .getActive() + .getName()); break; case ANTHROPIC: templatePresentation.setIcon(Icons.Anthropic); @@ -303,7 +305,7 @@ public class ModelComboBoxAction extends ComboBoxAction { if (onModelChanged != null) { onModelChanged.run(); } - handleModelChange(serviceType, label, icon, comboBoxPresentation); + handleModelChange(serviceType); } @Override @@ -314,13 +316,9 @@ public class ModelComboBoxAction extends ComboBoxAction { } private void handleModelChange( - ServiceType serviceType, - String label, - Icon icon, - Presentation comboBoxPresentation) { + ServiceType serviceType) { GeneralSettings.getCurrentState().setSelectedService(serviceType); - comboBoxPresentation.setIcon(icon); - comboBoxPresentation.setText(label); + updateTemplatePresentation(serviceType); onModelChange.accept(serviceType); } @@ -349,6 +347,16 @@ public class ModelComboBoxAction extends ComboBoxAction { () -> OpenAISettings.getCurrentState().setModel(model.getCode())); } + private AnAction createCustomOpenAIModelAction( + CustomServiceSettingsState model, + Presentation comboBoxPresentation) { + return createModelAction(CUSTOM_OPENAI, model.getName(), Icons.OpenAI, comboBoxPresentation, + () -> ApplicationManager.getApplication() + .getService(CustomServicesSettings.class) + .getState() + .setActive(model)); + } + private AnAction createGoogleModelAction(GoogleModel model, Presentation comboBoxPresentation) { return createModelAction(GOOGLE, model.getDescription(), Icons.Google, comboBoxPresentation, () -> ApplicationManager.getApplication() diff --git a/src/main/kotlin/ee/carlrobert/codegpt/CodeGPTCopyPastePreProcessor.kt b/src/main/kotlin/ee/carlrobert/codegpt/CodeGPTCopyPastePreProcessor.kt index 687e93b4..7cd7881f 100644 --- a/src/main/kotlin/ee/carlrobert/codegpt/CodeGPTCopyPastePreProcessor.kt +++ b/src/main/kotlin/ee/carlrobert/codegpt/CodeGPTCopyPastePreProcessor.kt @@ -7,7 +7,7 @@ import com.intellij.openapi.editor.Editor import com.intellij.openapi.editor.RawText import com.intellij.openapi.project.Project import com.intellij.psi.PsiFile -import ee.carlrobert.codegpt.credentials.CredentialsStore.CredentialKey.CODEGPT_API_KEY +import ee.carlrobert.codegpt.credentials.CredentialsStore.CredentialKey.CodeGptApiKey import ee.carlrobert.codegpt.credentials.CredentialsStore.isCredentialSet import ee.carlrobert.codegpt.predictions.PredictionService import ee.carlrobert.codegpt.settings.GeneralSettings @@ -60,7 +60,7 @@ class CodeGPTCopyPastePreProcessor : CopyPastePreProcessor { val currentTokens = getDocumentTokenCount(documentText) if (currentTokens > MAX_TOKEN_LIMIT) return - if (!isCredentialSet(CODEGPT_API_KEY) && currentTokens > FREE_TIER_TOKEN_LIMIT) return + if (!isCredentialSet(CodeGptApiKey) && currentTokens > FREE_TIER_TOKEN_LIMIT) return CoroutineScope(Dispatchers.IO).launch { handleDisplay() diff --git a/src/main/kotlin/ee/carlrobert/codegpt/CodeGPTLookupListener.kt b/src/main/kotlin/ee/carlrobert/codegpt/CodeGPTLookupListener.kt index 2fc44448..04731d47 100644 --- a/src/main/kotlin/ee/carlrobert/codegpt/CodeGPTLookupListener.kt +++ b/src/main/kotlin/ee/carlrobert/codegpt/CodeGPTLookupListener.kt @@ -8,7 +8,7 @@ import com.intellij.codeInsight.lookup.impl.LookupImpl import com.intellij.openapi.application.ApplicationManager import com.intellij.openapi.application.runReadAction import com.intellij.openapi.components.service -import ee.carlrobert.codegpt.credentials.CredentialsStore.CredentialKey.CODEGPT_API_KEY +import ee.carlrobert.codegpt.credentials.CredentialsStore.CredentialKey.CodeGptApiKey import ee.carlrobert.codegpt.credentials.CredentialsStore.isCredentialSet import ee.carlrobert.codegpt.predictions.PredictionService import ee.carlrobert.codegpt.settings.GeneralSettings @@ -38,7 +38,7 @@ class CodeGPTLookupListener : LookupManagerListener { if (GeneralSettings.getSelectedService() != ServiceType.CODEGPT || !service().state.codeAssistantEnabled || encodingManager.countTokens(editor.document.text) > 4096 - || !isCredentialSet(CODEGPT_API_KEY) && encodingManager.countTokens(editor.document.text) > 2048 + || !isCredentialSet(CodeGptApiKey) && encodingManager.countTokens(editor.document.text) > 2048 ) { return } diff --git a/src/main/kotlin/ee/carlrobert/codegpt/actions/CodeCompletionFeatureToggleActions.kt b/src/main/kotlin/ee/carlrobert/codegpt/actions/CodeCompletionFeatureToggleActions.kt index 1f3b4aea..26181380 100644 --- a/src/main/kotlin/ee/carlrobert/codegpt/actions/CodeCompletionFeatureToggleActions.kt +++ b/src/main/kotlin/ee/carlrobert/codegpt/actions/CodeCompletionFeatureToggleActions.kt @@ -8,7 +8,7 @@ import ee.carlrobert.codegpt.codecompletions.CodeCompletionService import ee.carlrobert.codegpt.settings.GeneralSettings import ee.carlrobert.codegpt.settings.service.ServiceType.* import ee.carlrobert.codegpt.settings.service.codegpt.CodeGPTServiceSettings -import ee.carlrobert.codegpt.settings.service.custom.CustomServiceSettings +import ee.carlrobert.codegpt.settings.service.custom.CustomServicesSettings import ee.carlrobert.codegpt.settings.service.llama.LlamaSettings import ee.carlrobert.codegpt.settings.service.ollama.OllamaSettings import ee.carlrobert.codegpt.settings.service.openai.OpenAISettings @@ -26,7 +26,7 @@ abstract class CodeCompletionFeatureToggleActions( OLLAMA -> service().state::codeCompletionsEnabled::set - CUSTOM_OPENAI -> service().state.codeCompletionSettings::codeCompletionsEnabled::set + CUSTOM_OPENAI -> service().state.active.codeCompletionSettings::codeCompletionsEnabled::set ANTHROPIC, AZURE, diff --git a/src/main/kotlin/ee/carlrobert/codegpt/codecompletions/CodeCompletionRequestFactory.kt b/src/main/kotlin/ee/carlrobert/codegpt/codecompletions/CodeCompletionRequestFactory.kt index bdd1ceea..32f28566 100644 --- a/src/main/kotlin/ee/carlrobert/codegpt/codecompletions/CodeCompletionRequestFactory.kt +++ b/src/main/kotlin/ee/carlrobert/codegpt/codecompletions/CodeCompletionRequestFactory.kt @@ -8,7 +8,7 @@ import ee.carlrobert.codegpt.credentials.CredentialsStore.CredentialKey import ee.carlrobert.codegpt.credentials.CredentialsStore.getCredential import ee.carlrobert.codegpt.settings.Placeholder.* import ee.carlrobert.codegpt.settings.service.codegpt.CodeGPTServiceSettings -import ee.carlrobert.codegpt.settings.service.custom.CustomServiceSettings +import ee.carlrobert.codegpt.settings.service.custom.CustomServicesSettings import ee.carlrobert.codegpt.settings.service.llama.LlamaSettings import ee.carlrobert.codegpt.settings.service.llama.LlamaSettingsState import ee.carlrobert.codegpt.settings.service.ollama.OllamaSettings @@ -53,8 +53,9 @@ object CodeCompletionRequestFactory { @JvmStatic fun buildCustomRequest(details: InfillRequest): Request { - val settings = service().state.codeCompletionSettings - val credential = getCredential(CredentialKey.CUSTOM_SERVICE_API_KEY) + val activeService = service().state.active + val settings = activeService.codeCompletionSettings + val credential = getCredential(CredentialKey.CustomServiceApiKey(activeService.name.orEmpty())) return buildCustomRequest( details, settings.url!!, diff --git a/src/main/kotlin/ee/carlrobert/codegpt/codecompletions/CodeCompletionService.kt b/src/main/kotlin/ee/carlrobert/codegpt/codecompletions/CodeCompletionService.kt index 3688b5ee..aa265081 100644 --- a/src/main/kotlin/ee/carlrobert/codegpt/codecompletions/CodeCompletionService.kt +++ b/src/main/kotlin/ee/carlrobert/codegpt/codecompletions/CodeCompletionService.kt @@ -13,7 +13,7 @@ import ee.carlrobert.codegpt.settings.GeneralSettings import ee.carlrobert.codegpt.settings.service.ServiceType import ee.carlrobert.codegpt.settings.service.ServiceType.* import ee.carlrobert.codegpt.settings.service.codegpt.CodeGPTServiceSettings -import ee.carlrobert.codegpt.settings.service.custom.CustomServiceSettings +import ee.carlrobert.codegpt.settings.service.custom.CustomServicesSettings import ee.carlrobert.codegpt.settings.service.llama.LlamaSettings import ee.carlrobert.codegpt.settings.service.ollama.OllamaSettings import ee.carlrobert.codegpt.settings.service.openai.OpenAISettings @@ -31,7 +31,8 @@ class CodeCompletionService { return when (service().state.selectedService) { CODEGPT -> service().state.codeCompletionSettings.model OPENAI -> "gpt-3.5-turbo-instruct" - CUSTOM_OPENAI -> service().state + CUSTOM_OPENAI -> service().state + .active .codeCompletionSettings .body .getOrDefault("model", null) as String @@ -46,7 +47,7 @@ class CodeCompletionService { when (selectedService) { CODEGPT -> service().state.codeCompletionSettings.codeCompletionsEnabled OPENAI -> OpenAISettings.getCurrentState().isCodeCompletionsEnabled - CUSTOM_OPENAI -> service().state.codeCompletionSettings.codeCompletionsEnabled + CUSTOM_OPENAI -> service().state.active.codeCompletionSettings.codeCompletionsEnabled LLAMA_CPP -> LlamaSettings.isCodeCompletionsPossible() OLLAMA -> service().state.codeCompletionsEnabled else -> false @@ -67,7 +68,7 @@ class CodeCompletionService { CompletionClientProvider.getDefaultClientBuilder().build() ).newEventSource( buildCustomRequest(infillRequest), - if (service().state.codeCompletionSettings.parseResponseAsChatCompletions) { + if (service().state.active.codeCompletionSettings.parseResponseAsChatCompletions) { OpenAIChatCompletionEventSourceListener(eventListener) } else { OpenAITextCompletionEventSourceListener(eventListener) diff --git a/src/main/kotlin/ee/carlrobert/codegpt/codecompletions/DebouncedCodeCompletionProvider.kt b/src/main/kotlin/ee/carlrobert/codegpt/codecompletions/DebouncedCodeCompletionProvider.kt index a98c0af5..185af050 100644 --- a/src/main/kotlin/ee/carlrobert/codegpt/codecompletions/DebouncedCodeCompletionProvider.kt +++ b/src/main/kotlin/ee/carlrobert/codegpt/codecompletions/DebouncedCodeCompletionProvider.kt @@ -15,7 +15,7 @@ import ee.carlrobert.codegpt.settings.GeneralSettings import ee.carlrobert.codegpt.settings.configuration.ConfigurationSettings import ee.carlrobert.codegpt.settings.service.ServiceType import ee.carlrobert.codegpt.settings.service.codegpt.CodeGPTServiceSettings -import ee.carlrobert.codegpt.settings.service.custom.CustomServiceSettings +import ee.carlrobert.codegpt.settings.service.custom.CustomServicesSettings import ee.carlrobert.codegpt.settings.service.llama.LlamaSettings import ee.carlrobert.codegpt.settings.service.ollama.OllamaSettings import ee.carlrobert.codegpt.settings.service.openai.OpenAISettings @@ -135,7 +135,7 @@ class DebouncedCodeCompletionProvider : DebouncedInlineCompletionProvider() { val codeCompletionsEnabled = when (selectedService) { ServiceType.CODEGPT -> service().state.codeCompletionSettings.codeCompletionsEnabled ServiceType.OPENAI -> OpenAISettings.getCurrentState().isCodeCompletionsEnabled - ServiceType.CUSTOM_OPENAI -> service().state.codeCompletionSettings.codeCompletionsEnabled + ServiceType.CUSTOM_OPENAI -> service().state.active.codeCompletionSettings.codeCompletionsEnabled ServiceType.LLAMA_CPP -> LlamaSettings.isCodeCompletionsPossible() ServiceType.OLLAMA -> service().state.codeCompletionsEnabled ServiceType.ANTHROPIC, diff --git a/src/main/kotlin/ee/carlrobert/codegpt/completions/factory/CustomOpenAIRequestFactory.kt b/src/main/kotlin/ee/carlrobert/codegpt/completions/factory/CustomOpenAIRequestFactory.kt index fa893096..c7b7bafb 100644 --- a/src/main/kotlin/ee/carlrobert/codegpt/completions/factory/CustomOpenAIRequestFactory.kt +++ b/src/main/kotlin/ee/carlrobert/codegpt/completions/factory/CustomOpenAIRequestFactory.kt @@ -7,7 +7,7 @@ import ee.carlrobert.codegpt.completions.ChatCompletionParameters import ee.carlrobert.codegpt.credentials.CredentialsStore.CredentialKey import ee.carlrobert.codegpt.credentials.CredentialsStore.getCredential import ee.carlrobert.codegpt.settings.service.custom.CustomServiceChatCompletionSettingsState -import ee.carlrobert.codegpt.settings.service.custom.CustomServiceSettings +import ee.carlrobert.codegpt.settings.service.custom.CustomServicesSettings import ee.carlrobert.llm.client.openai.completion.request.OpenAIChatCompletionMessage import ee.carlrobert.llm.client.openai.completion.request.OpenAIChatCompletionStandardMessage import ee.carlrobert.llm.completion.CompletionRequest @@ -20,13 +20,14 @@ class CustomOpenAIRequest(val request: Request) : CompletionRequest class CustomOpenAIRequestFactory : BaseRequestFactory() { override fun createChatRequest(params: ChatCompletionParameters): CustomOpenAIRequest { + val activeService = service() + .state + .active val request = buildCustomOpenAIChatCompletionRequest( - service() - .state - .chatCompletionSettings, + activeService.chatCompletionSettings, OpenAIRequestFactory.buildOpenAIMessages(null, params), true, - getCredential(CredentialKey.CUSTOM_SERVICE_API_KEY) + getCredential(CredentialKey.CustomServiceApiKey(activeService.name.orEmpty())) ) return CustomOpenAIRequest(request) } @@ -37,14 +38,17 @@ class CustomOpenAIRequestFactory : BaseRequestFactory() { maxTokens: Int, stream: Boolean ): CompletionRequest { + val activeService = service() + .state + .active val request = buildCustomOpenAIChatCompletionRequest( - service().state.chatCompletionSettings, + activeService.chatCompletionSettings, listOf( OpenAIChatCompletionStandardMessage("system", systemPrompt), OpenAIChatCompletionStandardMessage("user", userPrompt) ), stream, - getCredential(CredentialKey.CUSTOM_SERVICE_API_KEY) + getCredential(CredentialKey.CustomServiceApiKey(activeService.name.orEmpty())) ) return CustomOpenAIRequest(request) } diff --git a/src/main/kotlin/ee/carlrobert/codegpt/credentials/CredentialsStore.kt b/src/main/kotlin/ee/carlrobert/codegpt/credentials/CredentialsStore.kt index 22980876..5c75dcd6 100644 --- a/src/main/kotlin/ee/carlrobert/codegpt/credentials/CredentialsStore.kt +++ b/src/main/kotlin/ee/carlrobert/codegpt/credentials/CredentialsStore.kt @@ -3,45 +3,82 @@ package ee.carlrobert.codegpt.credentials import com.intellij.credentialStore.CredentialAttributes import com.intellij.credentialStore.generateServiceName import com.intellij.ide.passwordSafe.PasswordSafe -import com.intellij.util.concurrency.annotations.RequiresBackgroundThread +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.runBlocking +import java.util.concurrent.ConcurrentHashMap object CredentialsStore { - private val credentialsMap = mutableMapOf() + private val credentialsMap = ConcurrentHashMap() @JvmStatic - @RequiresBackgroundThread - fun getCredential(key: CredentialKey): String? = - credentialsMap.getOrPut(key) { - PasswordSafe.instance.getPassword( - CredentialAttributes( - generateServiceName("CodeGPT", key.name) - ) - ) ?: "" + // TODO Refactoring is needed, because it is often called from the form refactoring and @RequiresBackgroundThread + fun getCredential(keyModel: CredentialKey): String? = + credentialsMap.getOrPut(keyModel.value) { + runBlocking(Dispatchers.IO) { + PasswordSafe.instance.getPassword( + CredentialAttributes( + generateServiceName("CodeGPT", keyModel.value) + ) + ) ?: "" + } }.takeIf { !it.isNullOrEmpty() } - fun setCredential(key: CredentialKey, password: String?) { - val prevPassword = credentialsMap[key] - credentialsMap[key] = password + fun setCredential(keyModel: CredentialKey, password: String?) { + val prevPassword = credentialsMap[keyModel.value] + credentialsMap[keyModel.value] = password.orEmpty() if (prevPassword != password) { val credentialAttributes = - CredentialAttributes(generateServiceName("CodeGPT", key.name)) + CredentialAttributes(generateServiceName("CodeGPT", keyModel.value)) PasswordSafe.instance.setPassword(credentialAttributes, password) } } - fun isCredentialSet(key: CredentialKey): Boolean = !getCredential(key).isNullOrEmpty() + fun isCredentialSet(keyModel: CredentialKey): Boolean = !getCredential(keyModel).isNullOrEmpty() - enum class CredentialKey { - CODEGPT_API_KEY, - OPENAI_API_KEY, - CUSTOM_SERVICE_API_KEY, - ANTHROPIC_API_KEY, - AZURE_OPENAI_API_KEY, - AZURE_ACTIVE_DIRECTORY_TOKEN, - LLAMA_API_KEY, - GOOGLE_API_KEY, - OLLAMA_API_KEY, + sealed class CredentialKey { + abstract val value: String + + data object CodeGptApiKey : CredentialKey() { + override val value: String = "CODEGPT_API_KEY" + } + + data object OpenaiApiKey : CredentialKey() { + override val value: String = "OPENAI_API_KEY" + } + + data class CustomServiceApiKey(val name: String) : CredentialKey() { + override val value: String = "CUSTOM_SERVICE_API_KEY:$name" + } + + @Deprecated("Only for migration") + data object CustomServiceApiKeyLegacy : CredentialKey() { + override val value: String = "CUSTOM_SERVICE_API_KEY" + } + + data object AnthropicApiKey : CredentialKey() { + override val value: String = "ANTHROPIC_API_KEY" + } + + data object AzureOpenaiApiKey : CredentialKey() { + override val value: String = "AZURE_OPENAI_API_KEY" + } + + data object AzureActiveDirectoryToken : CredentialKey() { + override val value: String = "AZURE_ACTIVE_DIRECTORY_TOKEN" + } + + data object LlamaApiKey : CredentialKey() { + override val value: String = "LLAMA_API_KEY" + } + + data object GoogleApiKey : CredentialKey() { + override val value: String = "GOOGLE_API_KEY" + } + + data object OllamaApikey : CredentialKey() { + override val value: String = "OLLAMA_API_KEY" + } } } \ No newline at end of file diff --git a/src/main/kotlin/ee/carlrobert/codegpt/predictions/TriggerCustomPredictionAction.kt b/src/main/kotlin/ee/carlrobert/codegpt/predictions/TriggerCustomPredictionAction.kt index b7a9aecc..18499b62 100644 --- a/src/main/kotlin/ee/carlrobert/codegpt/predictions/TriggerCustomPredictionAction.kt +++ b/src/main/kotlin/ee/carlrobert/codegpt/predictions/TriggerCustomPredictionAction.kt @@ -13,8 +13,7 @@ import com.intellij.openapi.editor.actionSystem.EditorAction import com.intellij.openapi.editor.actionSystem.EditorWriteActionHandler import ee.carlrobert.codegpt.CodeGPTKeys import ee.carlrobert.codegpt.EncodingManager -import ee.carlrobert.codegpt.credentials.CredentialsStore -import ee.carlrobert.codegpt.credentials.CredentialsStore.CredentialKey.CODEGPT_API_KEY +import ee.carlrobert.codegpt.credentials.CredentialsStore.CredentialKey.CodeGptApiKey import ee.carlrobert.codegpt.credentials.CredentialsStore.isCredentialSet import ee.carlrobert.codegpt.settings.GeneralSettings import ee.carlrobert.codegpt.settings.service.ServiceType @@ -51,7 +50,7 @@ class TriggerCustomPredictionAction : EditorAction(Handler()), HintManagerImpl.A } val encodingManager = service() - if (!isCredentialSet(CODEGPT_API_KEY) && encodingManager.countTokens(editor.document.text) > 2048) { + if (!isCredentialSet(CodeGptApiKey) && encodingManager.countTokens(editor.document.text) > 2048) { OverlayUtil.showNotification("The file exceeds the token limit of 2,048. Please upgrade your plan to access higher limits.") return } diff --git a/src/main/kotlin/ee/carlrobert/codegpt/settings/service/AnthropicServiceConfigurable.kt b/src/main/kotlin/ee/carlrobert/codegpt/settings/service/AnthropicServiceConfigurable.kt index ccbd07dc..72e05716 100644 --- a/src/main/kotlin/ee/carlrobert/codegpt/settings/service/AnthropicServiceConfigurable.kt +++ b/src/main/kotlin/ee/carlrobert/codegpt/settings/service/AnthropicServiceConfigurable.kt @@ -2,7 +2,7 @@ package ee.carlrobert.codegpt.settings.service import com.intellij.openapi.components.service import com.intellij.openapi.options.Configurable -import ee.carlrobert.codegpt.credentials.CredentialsStore.CredentialKey.ANTHROPIC_API_KEY +import ee.carlrobert.codegpt.credentials.CredentialsStore.CredentialKey.AnthropicApiKey import ee.carlrobert.codegpt.credentials.CredentialsStore.getCredential import ee.carlrobert.codegpt.credentials.CredentialsStore.setCredential import ee.carlrobert.codegpt.settings.GeneralSettings @@ -25,11 +25,11 @@ class AnthropicServiceConfigurable : Configurable { override fun isModified(): Boolean { return component.getCurrentState() != service().state - || component.getApiKey() != getCredential(ANTHROPIC_API_KEY) + || component.getApiKey() != getCredential(AnthropicApiKey) } override fun apply() { - setCredential(ANTHROPIC_API_KEY, component.getApiKey()) + setCredential(AnthropicApiKey, component.getApiKey()) service().state.selectedService = ServiceType.ANTHROPIC service().loadState(component.getCurrentState()) } diff --git a/src/main/kotlin/ee/carlrobert/codegpt/settings/service/AzureServiceConfigurable.kt b/src/main/kotlin/ee/carlrobert/codegpt/settings/service/AzureServiceConfigurable.kt index 3691ef7a..1fd4f889 100644 --- a/src/main/kotlin/ee/carlrobert/codegpt/settings/service/AzureServiceConfigurable.kt +++ b/src/main/kotlin/ee/carlrobert/codegpt/settings/service/AzureServiceConfigurable.kt @@ -2,8 +2,8 @@ package ee.carlrobert.codegpt.settings.service import com.intellij.openapi.components.service import com.intellij.openapi.options.Configurable -import ee.carlrobert.codegpt.credentials.CredentialsStore.CredentialKey.AZURE_ACTIVE_DIRECTORY_TOKEN -import ee.carlrobert.codegpt.credentials.CredentialsStore.CredentialKey.AZURE_OPENAI_API_KEY +import ee.carlrobert.codegpt.credentials.CredentialsStore.CredentialKey.AzureActiveDirectoryToken +import ee.carlrobert.codegpt.credentials.CredentialsStore.CredentialKey.AzureOpenaiApiKey import ee.carlrobert.codegpt.credentials.CredentialsStore.getCredential import ee.carlrobert.codegpt.credentials.CredentialsStore.setCredential import ee.carlrobert.codegpt.settings.GeneralSettings @@ -26,15 +26,15 @@ class AzureServiceConfigurable : Configurable { override fun isModified(): Boolean { return component.getCurrentState() != service().state - || component.getActiveDirectoryToken() != getCredential(AZURE_ACTIVE_DIRECTORY_TOKEN) - || component.getApiKey() != getCredential(AZURE_OPENAI_API_KEY) + || component.getActiveDirectoryToken() != getCredential(AzureActiveDirectoryToken) + || component.getApiKey() != getCredential(AzureOpenaiApiKey) } override fun apply() { service().state.selectedService = ServiceType.AZURE service().loadState(component.currentState) - setCredential(AZURE_OPENAI_API_KEY, component.getApiKey()) - setCredential(AZURE_ACTIVE_DIRECTORY_TOKEN, component.getActiveDirectoryToken()) + setCredential(AzureOpenaiApiKey, component.getApiKey()) + setCredential(AzureActiveDirectoryToken, component.getActiveDirectoryToken()) } override fun reset() { diff --git a/src/main/kotlin/ee/carlrobert/codegpt/settings/service/LlamaServiceConfigurable.kt b/src/main/kotlin/ee/carlrobert/codegpt/settings/service/LlamaServiceConfigurable.kt index 12fa965e..30b2bb30 100644 --- a/src/main/kotlin/ee/carlrobert/codegpt/settings/service/LlamaServiceConfigurable.kt +++ b/src/main/kotlin/ee/carlrobert/codegpt/settings/service/LlamaServiceConfigurable.kt @@ -1,9 +1,8 @@ package ee.carlrobert.codegpt.settings.service import com.intellij.openapi.components.service -import com.intellij.openapi.diagnostic.thisLogger import com.intellij.openapi.options.Configurable -import ee.carlrobert.codegpt.credentials.CredentialsStore.CredentialKey.LLAMA_API_KEY +import ee.carlrobert.codegpt.credentials.CredentialsStore.CredentialKey.LlamaApiKey import ee.carlrobert.codegpt.credentials.CredentialsStore.getCredential import ee.carlrobert.codegpt.credentials.CredentialsStore.setCredential import ee.carlrobert.codegpt.settings.GeneralSettings @@ -26,12 +25,12 @@ class LlamaServiceConfigurable : Configurable { override fun isModified(): Boolean { return component.getCurrentState() != service().state - || component.llamaServerPreferencesForm.getApiKey() != getCredential(LLAMA_API_KEY) + || component.llamaServerPreferencesForm.getApiKey() != getCredential(LlamaApiKey) } override fun apply() { service().state.selectedService = ServiceType.LLAMA_CPP - setCredential(LLAMA_API_KEY, component.llamaServerPreferencesForm.getApiKey()) + setCredential(LlamaApiKey, component.llamaServerPreferencesForm.getApiKey()) service().loadState(component.currentState) } diff --git a/src/main/kotlin/ee/carlrobert/codegpt/settings/service/OpenAIServiceConfigurable.kt b/src/main/kotlin/ee/carlrobert/codegpt/settings/service/OpenAIServiceConfigurable.kt index 62db6f43..fbfbeb1e 100644 --- a/src/main/kotlin/ee/carlrobert/codegpt/settings/service/OpenAIServiceConfigurable.kt +++ b/src/main/kotlin/ee/carlrobert/codegpt/settings/service/OpenAIServiceConfigurable.kt @@ -2,7 +2,7 @@ package ee.carlrobert.codegpt.settings.service import com.intellij.openapi.components.service import com.intellij.openapi.options.Configurable -import ee.carlrobert.codegpt.credentials.CredentialsStore.CredentialKey.OPENAI_API_KEY +import ee.carlrobert.codegpt.credentials.CredentialsStore.CredentialKey.OpenaiApiKey import ee.carlrobert.codegpt.credentials.CredentialsStore.getCredential import ee.carlrobert.codegpt.credentials.CredentialsStore.setCredential import ee.carlrobert.codegpt.settings.GeneralSettings @@ -25,12 +25,12 @@ class OpenAIServiceConfigurable : Configurable { override fun isModified(): Boolean { return component.getCurrentState() != service().state - || component.getApiKey() != getCredential(OPENAI_API_KEY) + || component.getApiKey() != getCredential(OpenaiApiKey) } override fun apply() { service().state.selectedService = ServiceType.OPENAI - setCredential(OPENAI_API_KEY, component.getApiKey()) + setCredential(OpenaiApiKey, component.getApiKey()) service().loadState(component.getCurrentState()) } diff --git a/src/main/kotlin/ee/carlrobert/codegpt/settings/service/codegpt/CodeGPTService.kt b/src/main/kotlin/ee/carlrobert/codegpt/settings/service/codegpt/CodeGPTService.kt index bd638190..7371ec33 100644 --- a/src/main/kotlin/ee/carlrobert/codegpt/settings/service/codegpt/CodeGPTService.kt +++ b/src/main/kotlin/ee/carlrobert/codegpt/settings/service/codegpt/CodeGPTService.kt @@ -5,7 +5,7 @@ import com.intellij.openapi.components.service import com.intellij.openapi.project.Project import ee.carlrobert.codegpt.CodeGPTKeys.CODEGPT_USER_DETAILS import ee.carlrobert.codegpt.completions.CompletionClientProvider -import ee.carlrobert.codegpt.credentials.CredentialsStore.CredentialKey.CODEGPT_API_KEY +import ee.carlrobert.codegpt.credentials.CredentialsStore.CredentialKey.CodeGptApiKey import ee.carlrobert.codegpt.credentials.CredentialsStore.getCredential import ee.carlrobert.codegpt.settings.GeneralSettings import ee.carlrobert.codegpt.settings.service.codegpt.CodeGPTUserDetailsNotifier.Companion.CODEGPT_USER_DETAILS_TOPIC @@ -17,7 +17,7 @@ class CodeGPTService private constructor(val project: Project) { private val serviceScope = CoroutineScope(SupervisorJob() + Dispatchers.Default) fun syncUserDetailsAsync() { - syncUserDetailsAsync(getCredential(CODEGPT_API_KEY)) + syncUserDetailsAsync(getCredential(CodeGptApiKey)) } fun syncUserDetailsAsync(apiKey: String?) { diff --git a/src/main/kotlin/ee/carlrobert/codegpt/settings/service/codegpt/CodeGPTServiceConfigurable.kt b/src/main/kotlin/ee/carlrobert/codegpt/settings/service/codegpt/CodeGPTServiceConfigurable.kt index 3e767198..31fadfa5 100644 --- a/src/main/kotlin/ee/carlrobert/codegpt/settings/service/codegpt/CodeGPTServiceConfigurable.kt +++ b/src/main/kotlin/ee/carlrobert/codegpt/settings/service/codegpt/CodeGPTServiceConfigurable.kt @@ -2,7 +2,7 @@ package ee.carlrobert.codegpt.settings.service.codegpt import com.intellij.openapi.components.service import com.intellij.openapi.options.Configurable -import ee.carlrobert.codegpt.credentials.CredentialsStore.CredentialKey.CODEGPT_API_KEY +import ee.carlrobert.codegpt.credentials.CredentialsStore.CredentialKey.CodeGptApiKey import ee.carlrobert.codegpt.credentials.CredentialsStore.getCredential import ee.carlrobert.codegpt.credentials.CredentialsStore.setCredential import ee.carlrobert.codegpt.settings.GeneralSettings @@ -24,11 +24,11 @@ class CodeGPTServiceConfigurable : Configurable { } override fun isModified(): Boolean { - return component.isModified() || component.getApiKey() != getCredential(CODEGPT_API_KEY) + return component.isModified() || component.getApiKey() != getCredential(CodeGptApiKey) } override fun apply() { - setCredential(CODEGPT_API_KEY, component.getApiKey()) + setCredential(CodeGptApiKey, component.getApiKey()) service().state.apply { selectedService = ServiceType.CODEGPT } diff --git a/src/main/kotlin/ee/carlrobert/codegpt/settings/service/codegpt/CodeGPTServiceForm.kt b/src/main/kotlin/ee/carlrobert/codegpt/settings/service/codegpt/CodeGPTServiceForm.kt index ccd759d5..0a21611f 100644 --- a/src/main/kotlin/ee/carlrobert/codegpt/settings/service/codegpt/CodeGPTServiceForm.kt +++ b/src/main/kotlin/ee/carlrobert/codegpt/settings/service/codegpt/CodeGPTServiceForm.kt @@ -6,7 +6,7 @@ import com.intellij.ui.components.JBCheckBox import com.intellij.ui.components.JBPasswordField import com.intellij.util.ui.FormBuilder import ee.carlrobert.codegpt.CodeGPTBundle -import ee.carlrobert.codegpt.credentials.CredentialsStore.CredentialKey.CODEGPT_API_KEY +import ee.carlrobert.codegpt.credentials.CredentialsStore.CredentialKey.CodeGptApiKey import ee.carlrobert.codegpt.credentials.CredentialsStore.getCredential import ee.carlrobert.codegpt.credentials.CredentialsStore.setCredential import ee.carlrobert.codegpt.ui.UIUtil @@ -52,7 +52,7 @@ class CodeGPTServiceForm { init { apiKeyField.text = runBlocking(Dispatchers.IO) { - getCredential(CODEGPT_API_KEY) + getCredential(CodeGptApiKey) } } @@ -89,7 +89,7 @@ class CodeGPTServiceForm { || (codeCompletionModelComboBox.selectedItem as CodeGPTModel).code != codeCompletionSettings.model || codeAssistantEnabledCheckBox.isSelected != codeAssistantEnabled || codeCompletionsEnabledCheckBox.isSelected != codeCompletionSettings.codeCompletionsEnabled - || getApiKey() != getCredential(CODEGPT_API_KEY) + || getApiKey() != getCredential(CodeGptApiKey) } fun applyChanges() { @@ -102,7 +102,7 @@ class CodeGPTServiceForm { codeCompletionSettings.model = (codeCompletionModelComboBox.selectedItem as CodeGPTModel).code } - setCredential(CODEGPT_API_KEY, getApiKey()) + setCredential(CodeGptApiKey, getApiKey()) } fun resetForm() { @@ -113,7 +113,7 @@ class CodeGPTServiceForm { codeCompletionsEnabledCheckBox.isSelected = codeCompletionSettings.codeCompletionsEnabled } - apiKeyField.text = getCredential(CODEGPT_API_KEY) + apiKeyField.text = getCredential(CodeGptApiKey) } private class CustomComboBoxRenderer : DefaultListCellRenderer() { diff --git a/src/main/kotlin/ee/carlrobert/codegpt/settings/service/custom/CustomServiceConfigurable.kt b/src/main/kotlin/ee/carlrobert/codegpt/settings/service/custom/CustomServiceConfigurable.kt index 17e3d4aa..30eba4b6 100644 --- a/src/main/kotlin/ee/carlrobert/codegpt/settings/service/custom/CustomServiceConfigurable.kt +++ b/src/main/kotlin/ee/carlrobert/codegpt/settings/service/custom/CustomServiceConfigurable.kt @@ -2,39 +2,42 @@ package ee.carlrobert.codegpt.settings.service.custom import com.intellij.openapi.components.service import com.intellij.openapi.options.Configurable -import ee.carlrobert.codegpt.credentials.CredentialsStore.CredentialKey.CUSTOM_SERVICE_API_KEY -import ee.carlrobert.codegpt.credentials.CredentialsStore.getCredential -import ee.carlrobert.codegpt.credentials.CredentialsStore.setCredential -import ee.carlrobert.codegpt.settings.GeneralSettings -import ee.carlrobert.codegpt.settings.service.ServiceType -import ee.carlrobert.codegpt.settings.service.custom.form.CustomServiceForm +import ee.carlrobert.codegpt.settings.service.custom.form.CustomServiceListForm +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.SupervisorJob +import kotlinx.coroutines.cancel +import kotlinx.coroutines.swing.Swing import javax.swing.JComponent class CustomServiceConfigurable : Configurable { - private lateinit var component: CustomServiceForm + private val coroutineScope = CoroutineScope(SupervisorJob() + Dispatchers.Swing.immediate) + private lateinit var component: CustomServiceListForm + override fun getDisplayName(): String { return "CodeGPT: Custom Service" } override fun createComponent(): JComponent { - component = CustomServiceForm() + component = CustomServiceListForm(service(), coroutineScope) return component.getForm() } override fun isModified(): Boolean { return component.isModified() - || component.getApiKey() != getCredential(CUSTOM_SERVICE_API_KEY) } override fun apply() { component.applyChanges() - setCredential(CUSTOM_SERVICE_API_KEY, component.getApiKey()) - service().state.selectedService = ServiceType.CUSTOM_OPENAI } override fun reset() { component.resetForm() } + + override fun disposeUIResources() { + coroutineScope.cancel() + } } \ No newline at end of file diff --git a/src/main/kotlin/ee/carlrobert/codegpt/settings/service/custom/CustomServiceSettings.kt b/src/main/kotlin/ee/carlrobert/codegpt/settings/service/custom/CustomServiceSettings.kt index 8e07616d..8db0a5d4 100644 --- a/src/main/kotlin/ee/carlrobert/codegpt/settings/service/custom/CustomServiceSettings.kt +++ b/src/main/kotlin/ee/carlrobert/codegpt/settings/service/custom/CustomServiceSettings.kt @@ -1,22 +1,29 @@ package ee.carlrobert.codegpt.settings.service.custom +import com.fasterxml.jackson.annotation.JsonIgnoreProperties +import com.fasterxml.jackson.core.type.TypeReference import com.intellij.openapi.components.* import com.intellij.util.xmlb.annotations.OptionTag import ee.carlrobert.codegpt.codecompletions.InfillPromptTemplate +import ee.carlrobert.codegpt.credentials.CredentialsStore import ee.carlrobert.codegpt.settings.service.custom.template.CustomServiceChatCompletionTemplate import ee.carlrobert.codegpt.settings.service.custom.template.CustomServiceCodeCompletionTemplate import ee.carlrobert.codegpt.settings.service.custom.template.CustomServiceTemplate +import ee.carlrobert.codegpt.util.BaseConverter import ee.carlrobert.codegpt.util.MapConverter +private const val DEFAULT_SERVICE_SETTINGS_NANE = "Default" + @Service @State( name = "CodeGPT_CustomServiceSettings", storages = [Storage("CodeGPT_CustomServiceSettings.xml")] ) +@Deprecated("Migrate to CustomServicesSettings") class CustomServiceSettings : - SimplePersistentStateComponent(CustomServiceState()) { + SimplePersistentStateComponent(CustomServiceSettingsState()) { - override fun loadState(state: CustomServiceState) { + override fun loadState(state: CustomServiceSettingsState) { if (state.url != null || state.body.isNotEmpty() || state.headers.isNotEmpty()) { super.loadState(this.state.apply { // Migrate old settings @@ -34,7 +41,77 @@ class CustomServiceSettings : } } -class CustomServiceState : BaseState() { +@Service +@State( + name = "CodeGPT_CustomServicesSettings", + storages = [Storage("CodeGPT_CustomServicesSettings.xml")] +) +class CustomServicesSettings : + SimplePersistentStateComponent(CustomServicesState()) { + + override fun initializeComponent() { + super.initializeComponent() + val oldSettingsService = serviceOrNull() + + // This line checks if the legacy API key exists to determine if migration of old settings is needed + val oldApiKey = CredentialsStore.getCredential(CredentialsStore.CredentialKey.CustomServiceApiKeyLegacy) + + if (oldSettingsService != null && oldApiKey != null) { + val migrated = CustomServiceSettingsState().apply { copyFrom(oldSettingsService.state) } + state.services.clear() + state.services.add(migrated) + state.active = migrated + + CredentialsStore.setCredential(CredentialsStore.CredentialKey.CustomServiceApiKeyLegacy, null) + CredentialsStore.setCredential( + CredentialsStore.CredentialKey.CustomServiceApiKey(state.active.name.orEmpty()), + oldApiKey + ) + + oldSettingsService.state.apply { + template = CustomServiceTemplate.OPENAI + chatCompletionSettings = chatCompletionSettings.apply { + url = "" + headers = mutableMapOf() + body = mutableMapOf() + } + codeCompletionSettings = codeCompletionSettings.apply { + codeCompletionsEnabled = false + parseResponseAsChatCompletions = false + infillTemplate = InfillPromptTemplate.OPENAI + url = "" + headers = mutableMapOf() + body = mutableMapOf() + } + url = null + body = mutableMapOf() + headers = mutableMapOf() + } + } + } +} + +private class CustomServiceSettingsListConverter : BaseConverter>( + object : TypeReference>() {} +) + +@JsonIgnoreProperties(ignoreUnknown = true) +class CustomServicesState( + initialState: CustomServiceSettingsState = CustomServiceSettingsState() +) : BaseState() { + @get:OptionTag(converter = CustomServiceSettingsListConverter::class) + var services by list() + + var active by property(initialState) + + init { + services.add(initialState) + } +} + +@JsonIgnoreProperties(ignoreUnknown = true) +class CustomServiceSettingsState : BaseState() { + var name by string(DEFAULT_SERVICE_SETTINGS_NANE) var template by enum(CustomServiceTemplate.OPENAI) var chatCompletionSettings by property(CustomServiceChatCompletionSettingsState()) var codeCompletionSettings by property(CustomServiceCodeCompletionSettingsState()) @@ -50,6 +127,7 @@ class CustomServiceState : BaseState() { var body by map() } +@JsonIgnoreProperties(ignoreUnknown = true) class CustomServiceChatCompletionSettingsState : BaseState() { var url by string(CustomServiceChatCompletionTemplate.OPENAI.url) var headers by map() @@ -63,6 +141,7 @@ class CustomServiceChatCompletionSettingsState : BaseState() { } } +@JsonIgnoreProperties(ignoreUnknown = true) class CustomServiceCodeCompletionSettingsState : BaseState() { var codeCompletionsEnabled by property(true) var parseResponseAsChatCompletions by property(false) diff --git a/src/main/kotlin/ee/carlrobert/codegpt/settings/service/custom/form/CustomServiceChatCompletionForm.kt b/src/main/kotlin/ee/carlrobert/codegpt/settings/service/custom/form/CustomServiceChatCompletionForm.kt index 56c90bc0..c7ceec77 100644 --- a/src/main/kotlin/ee/carlrobert/codegpt/settings/service/custom/form/CustomServiceChatCompletionForm.kt +++ b/src/main/kotlin/ee/carlrobert/codegpt/settings/service/custom/form/CustomServiceChatCompletionForm.kt @@ -1,6 +1,7 @@ package ee.carlrobert.codegpt.settings.service.custom.form import com.intellij.openapi.application.runInEdt +import com.intellij.openapi.observable.util.whenTextChanged import com.intellij.openapi.ui.MessageType import com.intellij.util.ui.FormBuilder import ee.carlrobert.codegpt.CodeGPTBundle @@ -8,6 +9,7 @@ import ee.carlrobert.codegpt.completions.CompletionRequestService import ee.carlrobert.codegpt.completions.factory.CustomOpenAIRequestFactory import ee.carlrobert.codegpt.settings.service.custom.CustomServiceChatCompletionSettingsState import ee.carlrobert.codegpt.settings.service.custom.CustomServiceFormTabbedPane +import ee.carlrobert.codegpt.settings.service.custom.form.model.CustomServiceChatCompletionSettingsData import ee.carlrobert.codegpt.ui.OverlayUtil import ee.carlrobert.codegpt.ui.URLTextField import ee.carlrobert.llm.client.openai.completion.ErrorDetails @@ -18,7 +20,7 @@ import javax.swing.JButton import javax.swing.JPanel class CustomServiceChatCompletionForm( - state: CustomServiceChatCompletionSettingsState, + state: CustomServiceChatCompletionSettingsData, val getApiKey: () -> String? ) { diff --git a/src/main/kotlin/ee/carlrobert/codegpt/settings/service/custom/form/CustomServiceCodeCompletionForm.kt b/src/main/kotlin/ee/carlrobert/codegpt/settings/service/custom/form/CustomServiceCodeCompletionForm.kt index bc99e9ef..f794b44c 100644 --- a/src/main/kotlin/ee/carlrobert/codegpt/settings/service/custom/form/CustomServiceCodeCompletionForm.kt +++ b/src/main/kotlin/ee/carlrobert/codegpt/settings/service/custom/form/CustomServiceCodeCompletionForm.kt @@ -18,6 +18,7 @@ import ee.carlrobert.codegpt.completions.CompletionRequestService import ee.carlrobert.codegpt.settings.Placeholder import ee.carlrobert.codegpt.settings.service.custom.CustomServiceCodeCompletionSettingsState import ee.carlrobert.codegpt.settings.service.custom.CustomServiceFormTabbedPane +import ee.carlrobert.codegpt.settings.service.custom.form.model.CustomServiceCodeCompletionSettingsData import ee.carlrobert.codegpt.ui.OverlayUtil import ee.carlrobert.codegpt.ui.URLTextField import ee.carlrobert.llm.client.openai.completion.ErrorDetails @@ -31,7 +32,7 @@ import javax.swing.JButton import javax.swing.JPanel class CustomServiceCodeCompletionForm( - state: CustomServiceCodeCompletionSettingsState, + state: CustomServiceCodeCompletionSettingsData, val getApiKey: () -> String? ) { diff --git a/src/main/kotlin/ee/carlrobert/codegpt/settings/service/custom/form/CustomServiceForm.kt b/src/main/kotlin/ee/carlrobert/codegpt/settings/service/custom/form/CustomServiceForm.kt deleted file mode 100644 index cbeb478b..00000000 --- a/src/main/kotlin/ee/carlrobert/codegpt/settings/service/custom/form/CustomServiceForm.kt +++ /dev/null @@ -1,155 +0,0 @@ -package ee.carlrobert.codegpt.settings.service.custom.form - -import com.intellij.icons.AllIcons.General -import com.intellij.ide.HelpTooltip -import com.intellij.openapi.components.service -import com.intellij.openapi.ui.ComboBox -import com.intellij.ui.EnumComboBoxModel -import com.intellij.ui.components.JBLabel -import com.intellij.ui.components.JBPasswordField -import com.intellij.util.ui.FormBuilder -import ee.carlrobert.codegpt.CodeGPTBundle -import ee.carlrobert.codegpt.credentials.CredentialsStore.CredentialKey -import ee.carlrobert.codegpt.credentials.CredentialsStore.getCredential -import ee.carlrobert.codegpt.settings.service.custom.CustomServiceChatCompletionSettingsState -import ee.carlrobert.codegpt.settings.service.custom.CustomServiceCodeCompletionSettingsState -import ee.carlrobert.codegpt.settings.service.custom.CustomServiceSettings -import ee.carlrobert.codegpt.settings.service.custom.template.CustomServiceTemplate -import ee.carlrobert.codegpt.ui.UIUtil -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.runBlocking -import java.awt.FlowLayout -import java.net.MalformedURLException -import java.net.URL -import javax.swing.Box -import javax.swing.JPanel -import javax.swing.JTabbedPane - -class CustomServiceForm { - - private val apiKeyField = JBPasswordField().apply { - columns = 30 - } - private val templateHelpText = JBLabel(General.ContextHelp) - private val templateComboBox = ComboBox(EnumComboBoxModel(CustomServiceTemplate::class.java)) - private val chatCompletionsForm: CustomServiceChatCompletionForm - private val codeCompletionsForm: CustomServiceCodeCompletionForm - private val tabbedPane: JTabbedPane - - init { - val state = service().state - apiKeyField.text = runBlocking(Dispatchers.IO) { - getCredential(CredentialKey.CUSTOM_SERVICE_API_KEY) - } - chatCompletionsForm = - CustomServiceChatCompletionForm(state.chatCompletionSettings, this::getApiKey) - codeCompletionsForm = - CustomServiceCodeCompletionForm(state.codeCompletionSettings, this::getApiKey) - tabbedPane = JTabbedPane().apply { - add(CodeGPTBundle.get("shared.chatCompletions"), chatCompletionsForm.form) - add(CodeGPTBundle.get("shared.codeCompletions"), codeCompletionsForm.form) - } - templateComboBox.selectedItem = state.template - templateComboBox.addItemListener { - val template = it.item as CustomServiceTemplate - updateTemplateHelpTextTooltip(template) - chatCompletionsForm.run { - url = template.chatCompletionTemplate.url - headers = template.chatCompletionTemplate.headers - body = template.chatCompletionTemplate.body - } - if (template.codeCompletionTemplate != null) { - codeCompletionsForm.run { - url = template.codeCompletionTemplate.url - headers = template.codeCompletionTemplate.headers - body = template.codeCompletionTemplate.body - parseResponseAsChatCompletions = template.codeCompletionTemplate.parseResponseAsChatCompletions - } - tabbedPane.setEnabledAt(1, true) - } else { - tabbedPane.selectedIndex = 0 - tabbedPane.setEnabledAt(1, false) - } - } - updateTemplateHelpTextTooltip(state.template) - } - - fun getForm(): JPanel = FormBuilder.createFormBuilder() - .addLabeledComponent( - CodeGPTBundle.get("settingsConfigurable.service.custom.openai.presetTemplate.label"), - JPanel(FlowLayout(FlowLayout.LEADING, 0, 0)).apply { - add(templateComboBox) - add(Box.createHorizontalStrut(8)) - add(templateHelpText) - } - ) - .addLabeledComponent( - CodeGPTBundle.get("settingsConfigurable.shared.apiKey.label"), - apiKeyField - ) - .addComponentToRightColumn( - UIUtil.createComment("settingsConfigurable.service.custom.openai.apiKey.comment") - ) - .addVerticalGap(4) - .addComponent(tabbedPane) - .addComponentFillVertically(JPanel(), 0) - .panel - - fun getApiKey() = String(apiKeyField.password).ifEmpty { null } - - fun isModified() = service().state.run { - templateComboBox.selectedItem != template - || chatCompletionsForm.url != chatCompletionSettings.url - || chatCompletionsForm.headers != chatCompletionSettings.headers - || chatCompletionsForm.body != chatCompletionSettings.body - || codeCompletionsForm.codeCompletionsEnabled != codeCompletionSettings.codeCompletionsEnabled - || codeCompletionsForm.parseResponseAsChatCompletions != codeCompletionSettings.parseResponseAsChatCompletions - || codeCompletionsForm.infillTemplate != codeCompletionSettings.infillTemplate - || codeCompletionsForm.url != codeCompletionSettings.url - || codeCompletionsForm.headers != codeCompletionSettings.headers - || codeCompletionsForm.body != codeCompletionSettings.body - } - - fun applyChanges() { - service().state.run { - template = templateComboBox.item - chatCompletionSettings = CustomServiceChatCompletionSettingsState().apply { - url = chatCompletionsForm.url - headers = chatCompletionsForm.headers - body = chatCompletionsForm.body - } - codeCompletionSettings = CustomServiceCodeCompletionSettingsState().apply { - codeCompletionsEnabled = codeCompletionsForm.codeCompletionsEnabled - parseResponseAsChatCompletions = codeCompletionsForm.parseResponseAsChatCompletions - infillTemplate = codeCompletionsForm.infillTemplate - url = codeCompletionsForm.url - headers = codeCompletionsForm.headers - body = codeCompletionsForm.body - } - } - } - - fun resetForm() { - service().state.run { - templateComboBox.item = template - apiKeyField.text = getCredential(CredentialKey.CUSTOM_SERVICE_API_KEY) - chatCompletionsForm.resetForm(chatCompletionSettings) - codeCompletionsForm.resetForm(codeCompletionSettings) - } - } - - private fun updateTemplateHelpTextTooltip(template: CustomServiceTemplate) { - templateHelpText.toolTipText = null - try { - HelpTooltip() - .setTitle(template.providerName) - .setBrowserLink( - CodeGPTBundle.get("settingsConfigurable.service.custom.openai.linkToDocs"), - URL(template.docsUrl) - ) - .installOn(templateHelpText) - } catch (e: MalformedURLException) { - throw RuntimeException(e) - } - } -} \ No newline at end of file diff --git a/src/main/kotlin/ee/carlrobert/codegpt/settings/service/custom/form/CustomServiceListForm.kt b/src/main/kotlin/ee/carlrobert/codegpt/settings/service/custom/form/CustomServiceListForm.kt new file mode 100644 index 00000000..a139255d --- /dev/null +++ b/src/main/kotlin/ee/carlrobert/codegpt/settings/service/custom/form/CustomServiceListForm.kt @@ -0,0 +1,354 @@ +package ee.carlrobert.codegpt.settings.service.custom.form + +import com.intellij.icons.AllIcons +import com.intellij.icons.AllIcons.General +import com.intellij.ide.HelpTooltip +import com.intellij.openapi.actionSystem.ActionUpdateThread +import com.intellij.openapi.actionSystem.AnAction +import com.intellij.openapi.actionSystem.AnActionEvent +import com.intellij.openapi.ui.ComboBox +import com.intellij.openapi.ui.MessageType +import com.intellij.ui.EnumComboBoxModel +import com.intellij.ui.ToolbarDecorator +import com.intellij.ui.components.JBLabel +import com.intellij.ui.components.JBList +import com.intellij.ui.components.JBPasswordField +import com.intellij.ui.components.JBTextField +import com.intellij.util.ui.FormBuilder +import com.intellij.util.ui.components.BorderLayoutPanel +import ee.carlrobert.codegpt.CodeGPTBundle +import ee.carlrobert.codegpt.credentials.CredentialsStore +import ee.carlrobert.codegpt.credentials.CredentialsStore.CredentialKey +import ee.carlrobert.codegpt.credentials.CredentialsStore.getCredential +import ee.carlrobert.codegpt.settings.service.custom.CustomServiceSettingsState +import ee.carlrobert.codegpt.settings.service.custom.CustomServicesSettings +import ee.carlrobert.codegpt.settings.service.custom.form.model.mapToData +import ee.carlrobert.codegpt.settings.service.custom.form.model.mapToState +import ee.carlrobert.codegpt.settings.service.custom.template.CustomServiceTemplate +import ee.carlrobert.codegpt.ui.OverlayUtil +import ee.carlrobert.codegpt.ui.UIUtil +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.flow.update +import kotlinx.coroutines.runBlocking +import okhttp3.internal.toImmutableList +import java.awt.Dimension +import java.awt.FlowLayout +import java.net.MalformedURLException +import java.net.URL +import javax.swing.Box +import javax.swing.JPanel +import javax.swing.JTabbedPane +import javax.swing.ListSelectionModel + +class CustomServiceListForm( + private val service: CustomServicesSettings, + coroutineScope: CoroutineScope +) { + + private val formState = MutableStateFlow(service.state.mapToData()) + + private var lastSelectedIndex = 0 + + private val customProvidersJBList = JBList(formState.value.services) + .apply { + setCellRenderer(CustomServiceNameListRenderer()) + selectionMode = ListSelectionModel.SINGLE_SELECTION + + addListSelectionListener { _ -> + val localSelectedIndex = selectedIndex + if (localSelectedIndex != -1) { + if (lastSelectedIndex != -1) { + updateStateFromForm(lastSelectedIndex) + } + + lastSelectedIndex = localSelectedIndex + updateFormData(lastSelectedIndex) + } + } + } + + init { + formState + .onEach { + customProvidersJBList.setListData(it.services.toTypedArray()) + customProvidersJBList.repaint() + } + .launchIn(coroutineScope) + } + + private val apiKeyField = JBPasswordField().apply { + columns = 30 + } + private val nameField = JBTextField().apply { + columns = 30 + } + private val templateHelpText = JBLabel(General.ContextHelp) + private val templateComboBox = ComboBox(EnumComboBoxModel(CustomServiceTemplate::class.java)) + private val chatCompletionsForm: CustomServiceChatCompletionForm + private val codeCompletionsForm: CustomServiceCodeCompletionForm + private val tabbedPane: JTabbedPane + + init { + val selectedItem = formState.value.services.first() + + apiKeyField.text = runBlocking(Dispatchers.IO) { + getCredential(CredentialKey.CustomServiceApiKey(selectedItem.name.orEmpty())) + } + chatCompletionsForm = CustomServiceChatCompletionForm(selectedItem.chatCompletionSettings, this::getApiKey) + codeCompletionsForm = CustomServiceCodeCompletionForm(selectedItem.codeCompletionSettings, this::getApiKey) + tabbedPane = JTabbedPane().apply { + add(CodeGPTBundle.get("shared.chatCompletions"), chatCompletionsForm.form) + add(CodeGPTBundle.get("shared.codeCompletions"), codeCompletionsForm.form) + templateComboBox.selectedItem = selectedItem.template + } + nameField.text = selectedItem.name + templateComboBox.addItemListener { + val template = it.item as CustomServiceTemplate + updateTemplateHelpTextTooltip(template) + chatCompletionsForm.run { + url = template.chatCompletionTemplate.url + headers = template.chatCompletionTemplate.headers + body = template.chatCompletionTemplate.body + } + if (template.codeCompletionTemplate != null) { + codeCompletionsForm.run { + url = template.codeCompletionTemplate.url + headers = template.codeCompletionTemplate.headers + body = template.codeCompletionTemplate.body + parseResponseAsChatCompletions = template.codeCompletionTemplate.parseResponseAsChatCompletions + } + tabbedPane.setEnabledAt(1, true) + } else { + tabbedPane.selectedIndex = 0 + tabbedPane.setEnabledAt(1, false) + } + } + updateTemplateHelpTextTooltip(selectedItem.template) + } + + private fun updateFormData(index: Int) { + val selectedItem = formState.value.services[index] + + chatCompletionsForm.apply { + val chatCompletionSettings = selectedItem.chatCompletionSettings + url = chatCompletionSettings.url.orEmpty() + body = chatCompletionSettings.body.toMutableMap() + headers = chatCompletionSettings.headers.toMutableMap() + } + codeCompletionsForm.apply { + val codeCompletionSettings = selectedItem.codeCompletionSettings + url = codeCompletionSettings.url.orEmpty() + body = codeCompletionSettings.body.toMutableMap() + headers = codeCompletionSettings.headers.toMutableMap() + infillTemplate = codeCompletionSettings.infillTemplate + codeCompletionsEnabled = codeCompletionSettings.codeCompletionsEnabled + parseResponseAsChatCompletions = codeCompletionSettings.parseResponseAsChatCompletions + } + apiKeyField.text = selectedItem.apiKey + nameField.text = selectedItem.name + updateTemplateHelpTextTooltip(selectedItem.template) + } + + private fun updateStateFromForm(editedIndex: Int) { + formState.update { state -> + val editedItem = state.services[editedIndex] + + val updatedItem = editedItem.copy( + name = nameField.text, + template = templateComboBox.item, + apiKey = getApiKey(), + chatCompletionSettings = editedItem.chatCompletionSettings.copy( + url = chatCompletionsForm.url, + body = chatCompletionsForm.body, + headers = chatCompletionsForm.headers, + ), + codeCompletionSettings = editedItem.codeCompletionSettings.copy( + codeCompletionsEnabled = codeCompletionsForm.codeCompletionsEnabled, + parseResponseAsChatCompletions = codeCompletionsForm.parseResponseAsChatCompletions, + infillTemplate = codeCompletionsForm.infillTemplate, + url = codeCompletionsForm.url, + headers = codeCompletionsForm.headers, + body = codeCompletionsForm.body, + ) + ) + + if (editedItem == updatedItem) return@update state + + val updatedServices = state.services.toMutableList().let { mutableList -> + mutableList[editedIndex] = updatedItem + mutableList.toImmutableList() + } + state.copy(services = updatedServices) + } + } + + fun getForm(): JPanel = + BorderLayoutPanel(8, 0) + .addToLeft(createToolbarDecorator().createPanel()) + .addToCenter(createContentPanel()) + + private fun createToolbarDecorator(): ToolbarDecorator = + ToolbarDecorator.createDecorator(customProvidersJBList) + .setPreferredSize(Dimension(220, 0)) + .setAddAction { handleAddAction() } + .setRemoveAction { handleRemoveAction() } + .setRemoveActionUpdater { + formState.value.services.size > 1 + } + .addExtraAction(object : AnAction("Duplicate", "Duplicate service", AllIcons.Actions.Copy) { + + override fun getActionUpdateThread(): ActionUpdateThread { + return ActionUpdateThread.EDT + } + + override fun update(e: AnActionEvent) { + val selected = customProvidersJBList.selectedIndex + + e.presentation.isEnabled = selected != -1 + } + + override fun actionPerformed(e: AnActionEvent) { + handleDuplicateAction() + } + }) + .disableUpDownActions() + + private fun handleRemoveAction() { + val prevSelectedIndex = customProvidersJBList.selectedIndex + formState.update { state -> + state.copy(services = state.services.filterIndexed { index, _ -> + index != customProvidersJBList.selectedIndex + }) + } + val newSelectedIndex = if (prevSelectedIndex == 0) { + 0 + } else { + prevSelectedIndex - 1 + } + lastSelectedIndex = -1 + updateFormData(newSelectedIndex) + customProvidersJBList.selectedIndex = newSelectedIndex + } + + private fun handleDuplicateAction() { + formState.update { + val selectedIndex = customProvidersJBList.selectedIndex + val copiedService = it.services[selectedIndex].copy(name = it.services[selectedIndex].name + "Copied") + it.copy( + services = it.services + copiedService + ) + } + customProvidersJBList.selectedIndex = formState.value.services.lastIndex + } + + private fun handleAddAction() { + formState.update { + it.copy( + services = it.services + CustomServiceSettingsState().apply { name += it.services.size }.mapToData() + ) + } + customProvidersJBList.selectedIndex = formState.value.services.lastIndex + } + + private fun createContentPanel(): JPanel = FormBuilder.createFormBuilder() + .addLabeledComponent( + CodeGPTBundle.get("settingsConfigurable.service.custom.openai.presetTemplate.label"), + JPanel(FlowLayout(FlowLayout.LEADING, 0, 0)).apply { + add(templateComboBox) + add(Box.createHorizontalStrut(8)) + add(templateHelpText) + } + ) + .addLabeledComponent( + CodeGPTBundle.get("settingsConfigurable.service.custom.openai.apiKey.provider.name"), + nameField + ) + .addLabeledComponent( + CodeGPTBundle.get("settingsConfigurable.shared.apiKey.label"), + apiKeyField + ) + .addComponentToRightColumn( + UIUtil.createComment("settingsConfigurable.service.custom.openai.apiKey.comment") + ) + .addVerticalGap(4) + .addComponent(tabbedPane) + .addComponentFillVertically(JPanel(), 0) + .panel + + fun getApiKey() = String(apiKeyField.password).ifEmpty { null } + + fun isModified(): Boolean { + updateStateFromForm(lastSelectedIndex) + return service.state.mapToData() != formState.value + } + + fun applyChanges() { + if (!validateServiceNames()) { + OverlayUtil.showBalloon( + "Service names must be unique", + MessageType.ERROR, + customProvidersJBList, + ) + return + } + + val formStateValue = formState.value + + val newActualService = formStateValue.services.firstOrNull { it.name == formStateValue.active.name } + ?: formStateValue.services.first() + + + // Cleanup saved api keys + val savedServicesName = service.state.services.mapNotNull { it.name } + val deletedServices = savedServicesName.subtract(formStateValue.services.mapNotNull { it.name }.toSet()) + deletedServices.forEach { deletedServiceName -> + CredentialsStore.setCredential(CredentialKey.CustomServiceApiKey(deletedServiceName), null) + } + // Save apiKeys + formStateValue.services.forEach { + CredentialsStore.setCredential(CredentialKey.CustomServiceApiKey(it.name.orEmpty()), it.apiKey) + } + + // Save settings + service.state.run { + services = formStateValue.services.mapTo(mutableListOf()) { it.mapToState() } + active = newActualService.mapToState() + } + formState.value = service.state.mapToData() + } + + private fun validateServiceNames(): Boolean { + val serviceNames = formState.value.services.mapNotNull { it.name } + val uniqueNames = serviceNames.toSet() + return serviceNames.size == uniqueNames.size + } + + fun resetForm() { + lastSelectedIndex = -1 + formState.value = service.state.mapToData() + if (customProvidersJBList.selectedIndex == 0) { + updateFormData(0) + } else { + customProvidersJBList.selectedIndex = 0 + } + } + + private fun updateTemplateHelpTextTooltip(template: CustomServiceTemplate) { + templateHelpText.toolTipText = null + try { + HelpTooltip() + .setTitle(template.providerName) + .setBrowserLink( + CodeGPTBundle.get("settingsConfigurable.service.custom.openai.linkToDocs"), + URL(template.docsUrl) + ) + .installOn(templateHelpText) + } catch (e: MalformedURLException) { + throw RuntimeException(e) + } + } +} \ No newline at end of file diff --git a/src/main/kotlin/ee/carlrobert/codegpt/settings/service/custom/form/CustomServiceNameListRenderer.kt b/src/main/kotlin/ee/carlrobert/codegpt/settings/service/custom/form/CustomServiceNameListRenderer.kt new file mode 100644 index 00000000..5424f0fb --- /dev/null +++ b/src/main/kotlin/ee/carlrobert/codegpt/settings/service/custom/form/CustomServiceNameListRenderer.kt @@ -0,0 +1,24 @@ +package ee.carlrobert.codegpt.settings.service.custom.form + +import com.intellij.ui.render.LabelBasedRenderer +import ee.carlrobert.codegpt.settings.service.custom.form.model.CustomServiceSettingsData +import java.awt.Component +import javax.swing.JLabel +import javax.swing.JList +import javax.swing.ListCellRenderer + +internal class CustomServiceNameListRenderer : LabelBasedRenderer(), ListCellRenderer { + private val delegate = List() + + override fun getListCellRendererComponent( + list: JList, + value: CustomServiceSettingsData?, + index: Int, + isSelected: Boolean, + cellHasFocus: Boolean + ): Component = + delegate.getListCellRendererComponent(list, value, index, isSelected, cellHasFocus) + .also { component -> + (component as? JLabel)?.text = value?.name ?: "" + } +} \ No newline at end of file diff --git a/src/main/kotlin/ee/carlrobert/codegpt/settings/service/custom/form/model/CustomServiceChatCompletionSettingsData.kt b/src/main/kotlin/ee/carlrobert/codegpt/settings/service/custom/form/model/CustomServiceChatCompletionSettingsData.kt new file mode 100644 index 00000000..f29717a3 --- /dev/null +++ b/src/main/kotlin/ee/carlrobert/codegpt/settings/service/custom/form/model/CustomServiceChatCompletionSettingsData.kt @@ -0,0 +1,7 @@ +package ee.carlrobert.codegpt.settings.service.custom.form.model + +data class CustomServiceChatCompletionSettingsData( + val url: String?, + val headers: Map, + val body: Map +) diff --git a/src/main/kotlin/ee/carlrobert/codegpt/settings/service/custom/form/model/CustomServiceCodeCompletionSettingsData.kt b/src/main/kotlin/ee/carlrobert/codegpt/settings/service/custom/form/model/CustomServiceCodeCompletionSettingsData.kt new file mode 100644 index 00000000..0cefa1e7 --- /dev/null +++ b/src/main/kotlin/ee/carlrobert/codegpt/settings/service/custom/form/model/CustomServiceCodeCompletionSettingsData.kt @@ -0,0 +1,12 @@ +package ee.carlrobert.codegpt.settings.service.custom.form.model + +import ee.carlrobert.codegpt.codecompletions.InfillPromptTemplate + +data class CustomServiceCodeCompletionSettingsData( + val codeCompletionsEnabled: Boolean, + val parseResponseAsChatCompletions: Boolean, + val infillTemplate: InfillPromptTemplate, + val url: String?, + val headers: Map, + val body: Map +) diff --git a/src/main/kotlin/ee/carlrobert/codegpt/settings/service/custom/form/model/CustomServiceSettingsData.kt b/src/main/kotlin/ee/carlrobert/codegpt/settings/service/custom/form/model/CustomServiceSettingsData.kt new file mode 100644 index 00000000..d3be0470 --- /dev/null +++ b/src/main/kotlin/ee/carlrobert/codegpt/settings/service/custom/form/model/CustomServiceSettingsData.kt @@ -0,0 +1,11 @@ +package ee.carlrobert.codegpt.settings.service.custom.form.model + +import ee.carlrobert.codegpt.settings.service.custom.template.CustomServiceTemplate + +data class CustomServiceSettingsData( + val name: String?, + val template: CustomServiceTemplate, + val apiKey: String?, + val chatCompletionSettings: CustomServiceChatCompletionSettingsData, + val codeCompletionSettings: CustomServiceCodeCompletionSettingsData +) diff --git a/src/main/kotlin/ee/carlrobert/codegpt/settings/service/custom/form/model/CustomServicesStateData.kt b/src/main/kotlin/ee/carlrobert/codegpt/settings/service/custom/form/model/CustomServicesStateData.kt new file mode 100644 index 00000000..97d52d73 --- /dev/null +++ b/src/main/kotlin/ee/carlrobert/codegpt/settings/service/custom/form/model/CustomServicesStateData.kt @@ -0,0 +1,6 @@ +package ee.carlrobert.codegpt.settings.service.custom.form.model + +data class CustomServicesStateData( + val services: List, + val active: CustomServiceSettingsData +) diff --git a/src/main/kotlin/ee/carlrobert/codegpt/settings/service/custom/form/model/DataToStateMapper.kt b/src/main/kotlin/ee/carlrobert/codegpt/settings/service/custom/form/model/DataToStateMapper.kt new file mode 100644 index 00000000..5c451ea7 --- /dev/null +++ b/src/main/kotlin/ee/carlrobert/codegpt/settings/service/custom/form/model/DataToStateMapper.kt @@ -0,0 +1,31 @@ +package ee.carlrobert.codegpt.settings.service.custom.form.model + +import ee.carlrobert.codegpt.settings.service.custom.CustomServiceChatCompletionSettingsState +import ee.carlrobert.codegpt.settings.service.custom.CustomServiceCodeCompletionSettingsState +import ee.carlrobert.codegpt.settings.service.custom.CustomServiceSettingsState + + +fun CustomServiceSettingsData.mapToState(): CustomServiceSettingsState = + CustomServiceSettingsState().also { serviceState -> + serviceState.name = name + serviceState.template = template + serviceState.chatCompletionSettings = chatCompletionSettings.mapToState() + serviceState.codeCompletionSettings = codeCompletionSettings.mapToState() + } + +fun CustomServiceChatCompletionSettingsData.mapToState(): CustomServiceChatCompletionSettingsState = + CustomServiceChatCompletionSettingsState().also { serviceState -> + serviceState.url = url + serviceState.headers = headers.toMutableMap() + serviceState.body = body.toMutableMap() + } + +fun CustomServiceCodeCompletionSettingsData.mapToState(): CustomServiceCodeCompletionSettingsState = + CustomServiceCodeCompletionSettingsState().also { serviceState -> + serviceState.codeCompletionsEnabled = codeCompletionsEnabled + serviceState.parseResponseAsChatCompletions = parseResponseAsChatCompletions + serviceState.infillTemplate = infillTemplate + serviceState.url = url + serviceState.headers = headers.toMutableMap() + serviceState.body = body.toMutableMap() + } diff --git a/src/main/kotlin/ee/carlrobert/codegpt/settings/service/custom/form/model/StateToDataMapper.kt b/src/main/kotlin/ee/carlrobert/codegpt/settings/service/custom/form/model/StateToDataMapper.kt new file mode 100644 index 00000000..3bcb3ff7 --- /dev/null +++ b/src/main/kotlin/ee/carlrobert/codegpt/settings/service/custom/form/model/StateToDataMapper.kt @@ -0,0 +1,39 @@ +package ee.carlrobert.codegpt.settings.service.custom.form.model + +import ee.carlrobert.codegpt.credentials.CredentialsStore +import ee.carlrobert.codegpt.settings.service.custom.CustomServiceChatCompletionSettingsState +import ee.carlrobert.codegpt.settings.service.custom.CustomServiceCodeCompletionSettingsState +import ee.carlrobert.codegpt.settings.service.custom.CustomServiceSettingsState +import ee.carlrobert.codegpt.settings.service.custom.CustomServicesState + +fun CustomServicesState.mapToData(): CustomServicesStateData = + CustomServicesStateData( + services = services.map { it.mapToData() }, + active = active.mapToData() + ) + +fun CustomServiceSettingsState.mapToData(): CustomServiceSettingsData = + CustomServiceSettingsData( + name = name, + template = template, + apiKey = CredentialsStore.getCredential(CredentialsStore.CredentialKey.CustomServiceApiKey(name.orEmpty())), + chatCompletionSettings = chatCompletionSettings.mapToData(), + codeCompletionSettings = codeCompletionSettings.mapToData() + ) + +fun CustomServiceChatCompletionSettingsState.mapToData(): CustomServiceChatCompletionSettingsData = + CustomServiceChatCompletionSettingsData( + url = url, + headers = headers, + body = body + ) + +fun CustomServiceCodeCompletionSettingsState.mapToData(): CustomServiceCodeCompletionSettingsData = + CustomServiceCodeCompletionSettingsData( + codeCompletionsEnabled = codeCompletionsEnabled, + parseResponseAsChatCompletions = parseResponseAsChatCompletions, + infillTemplate = infillTemplate, + url = url, + headers = headers, + body = body + ) \ No newline at end of file diff --git a/src/main/kotlin/ee/carlrobert/codegpt/settings/service/google/GoogleSettingsConfigurable.kt b/src/main/kotlin/ee/carlrobert/codegpt/settings/service/google/GoogleSettingsConfigurable.kt index 2dc49641..9f94521d 100644 --- a/src/main/kotlin/ee/carlrobert/codegpt/settings/service/google/GoogleSettingsConfigurable.kt +++ b/src/main/kotlin/ee/carlrobert/codegpt/settings/service/google/GoogleSettingsConfigurable.kt @@ -2,7 +2,7 @@ package ee.carlrobert.codegpt.settings.service.google; import com.intellij.openapi.components.service import com.intellij.openapi.options.Configurable -import ee.carlrobert.codegpt.credentials.CredentialsStore.CredentialKey.GOOGLE_API_KEY +import ee.carlrobert.codegpt.credentials.CredentialsStore.CredentialKey.GoogleApiKey import ee.carlrobert.codegpt.credentials.CredentialsStore.getCredential import ee.carlrobert.codegpt.credentials.CredentialsStore.setCredential import ee.carlrobert.codegpt.settings.GeneralSettings @@ -23,11 +23,11 @@ class GoogleSettingsConfigurable : Configurable { } override fun isModified(): Boolean { - return component.isModified() || component.getApiKey() != getCredential(GOOGLE_API_KEY) + return component.isModified() || component.getApiKey() != getCredential(GoogleApiKey) } override fun apply() { - setCredential(GOOGLE_API_KEY, component.getApiKey()) + setCredential(GoogleApiKey, component.getApiKey()) service().state.selectedService = ServiceType.GOOGLE component.applyChanges() } diff --git a/src/main/kotlin/ee/carlrobert/codegpt/settings/service/google/GoogleSettingsForm.kt b/src/main/kotlin/ee/carlrobert/codegpt/settings/service/google/GoogleSettingsForm.kt index 46554746..00691a15 100644 --- a/src/main/kotlin/ee/carlrobert/codegpt/settings/service/google/GoogleSettingsForm.kt +++ b/src/main/kotlin/ee/carlrobert/codegpt/settings/service/google/GoogleSettingsForm.kt @@ -6,7 +6,7 @@ import com.intellij.ui.EnumComboBoxModel import com.intellij.ui.components.JBPasswordField import com.intellij.util.ui.FormBuilder import ee.carlrobert.codegpt.CodeGPTBundle -import ee.carlrobert.codegpt.credentials.CredentialsStore.CredentialKey.GOOGLE_API_KEY +import ee.carlrobert.codegpt.credentials.CredentialsStore.CredentialKey.GoogleApiKey import ee.carlrobert.codegpt.credentials.CredentialsStore.getCredential import ee.carlrobert.codegpt.ui.UIUtil import ee.carlrobert.llm.client.google.models.GoogleModel @@ -23,7 +23,7 @@ class GoogleSettingsForm { val state = service().state apiKeyField.columns = 30 apiKeyField.text = runBlocking(Dispatchers.IO) { - getCredential(GOOGLE_API_KEY) + getCredential(GoogleApiKey) } completionModelComboBox = ComboBox( EnumComboBoxModel(GoogleModel::class.java) @@ -60,12 +60,12 @@ class GoogleSettingsForm { fun resetForm() { val state = service().state - apiKeyField.text = getCredential(GOOGLE_API_KEY) + apiKeyField.text = getCredential(GoogleApiKey) completionModelComboBox.selectedItem = GoogleModel.findByCode(state.model) } fun isModified(): Boolean = service().state.run { - model != getModel() || getApiKey() != getCredential(GOOGLE_API_KEY) + model != getModel() || getApiKey() != getCredential(GoogleApiKey) } fun applyChanges() { diff --git a/src/main/kotlin/ee/carlrobert/codegpt/settings/service/ollama/OllamaSettingsForm.kt b/src/main/kotlin/ee/carlrobert/codegpt/settings/service/ollama/OllamaSettingsForm.kt index 08ec8ed2..69917a36 100644 --- a/src/main/kotlin/ee/carlrobert/codegpt/settings/service/ollama/OllamaSettingsForm.kt +++ b/src/main/kotlin/ee/carlrobert/codegpt/settings/service/ollama/OllamaSettingsForm.kt @@ -15,7 +15,7 @@ import com.intellij.ui.components.JBTextField import com.intellij.util.concurrency.AppExecutorUtil import com.intellij.util.ui.FormBuilder import ee.carlrobert.codegpt.CodeGPTBundle -import ee.carlrobert.codegpt.credentials.CredentialsStore.CredentialKey.OLLAMA_API_KEY +import ee.carlrobert.codegpt.credentials.CredentialsStore.CredentialKey.OllamaApikey import ee.carlrobert.codegpt.credentials.CredentialsStore.getCredential import ee.carlrobert.codegpt.credentials.CredentialsStore.setCredential import ee.carlrobert.codegpt.settings.service.CodeCompletionConfigurationForm @@ -72,7 +72,7 @@ class OllamaSettingsForm { apiKeyField = JBPasswordField().apply { columns = 30 text = runBlocking(Dispatchers.IO) { - getCredential(OLLAMA_API_KEY) + getCredential(OllamaApikey) } } refreshModels(settings.model) @@ -126,7 +126,7 @@ class OllamaSettingsForm { codeCompletionConfigurationForm.fimTemplate = fimTemplate codeCompletionConfigurationForm.fimOverride != fimOverride } - apiKeyField.text = getCredential(OLLAMA_API_KEY) + apiKeyField.text = getCredential(OllamaApikey) } fun applyChanges() { @@ -137,7 +137,7 @@ class OllamaSettingsForm { fimTemplate = codeCompletionConfigurationForm.fimTemplate!! fimOverride = codeCompletionConfigurationForm.fimOverride ?: false } - setCredential(OLLAMA_API_KEY, getApiKey()) + setCredential(OllamaApikey, getApiKey()) } fun isModified() = service().state.run { @@ -146,7 +146,7 @@ class OllamaSettingsForm { || codeCompletionConfigurationForm.isCodeCompletionsEnabled != codeCompletionsEnabled || codeCompletionConfigurationForm.fimTemplate != fimTemplate || codeCompletionConfigurationForm.fimOverride != fimOverride - || getApiKey() != getCredential(OLLAMA_API_KEY) + || getApiKey() != getCredential(OllamaApikey) } private fun refreshModels(currentModel: String?) { diff --git a/src/main/kotlin/ee/carlrobert/codegpt/util/ListConverter.kt b/src/main/kotlin/ee/carlrobert/codegpt/util/ListConverter.kt new file mode 100644 index 00000000..173af3a8 --- /dev/null +++ b/src/main/kotlin/ee/carlrobert/codegpt/util/ListConverter.kt @@ -0,0 +1,5 @@ +package ee.carlrobert.codegpt.util + +import com.fasterxml.jackson.core.type.TypeReference + +class ListConverter : BaseConverter>(object : TypeReference>() {}) diff --git a/src/main/resources/messages/codegpt.properties b/src/main/resources/messages/codegpt.properties index 884166ba..293039d2 100644 --- a/src/main/resources/messages/codegpt.properties +++ b/src/main/resources/messages/codegpt.properties @@ -44,6 +44,7 @@ settingsConfigurable.service.codegpt.chatCompletionModel.comment=Choose a model settingsConfigurable.service.codegpt.codeCompletionModel.comment=Choose a model tailored for code completion-related tasks. settingsConfigurable.service.codegpt.enableCodeAssistant.comment=If checked, Code Assistant will suggest related code updates as you make changes. settingsConfigurable.service.custom.openai.apiKey.comment=A secret value stored in the system's Keychain or KeePass, depending on your OS. This approach is recommended over storing the secret in the header as plain text. +settingsConfigurable.service.custom.openai.apiKey.provider.name=Custom provider name: settingsConfigurable.service.openai.apiKey.comment=You can find the API key in your User settings. settingsConfigurable.service.openai.customModel.label=Custom model: settingsConfigurable.service.openai.organization.label=Organization: diff --git a/src/test/kotlin/testsupport/mixin/ShortcutsTestMixin.kt b/src/test/kotlin/testsupport/mixin/ShortcutsTestMixin.kt index 51aac073..f70ee607 100644 --- a/src/test/kotlin/testsupport/mixin/ShortcutsTestMixin.kt +++ b/src/test/kotlin/testsupport/mixin/ShortcutsTestMixin.kt @@ -20,7 +20,7 @@ interface ShortcutsTestMixin { fun useCodeGPTService() { service().state.selectedService = ServiceType.CODEGPT - setCredential(CODEGPT_API_KEY, "TEST_API_KEY") + setCredential(CodeGptApiKey, "TEST_API_KEY") service().state.run { chatCompletionSettings.model = "TEST_MODEL" codeCompletionSettings.model = "TEST_CODE_MODEL" @@ -30,7 +30,7 @@ interface ShortcutsTestMixin { fun useOpenAIService(chatModel: String? = "gpt-4") { service().state.selectedService = ServiceType.OPENAI - setCredential(OPENAI_API_KEY, "TEST_API_KEY") + setCredential(OpenaiApiKey, "TEST_API_KEY") service().state.run { model = chatModel isCodeCompletionsEnabled = true @@ -39,7 +39,7 @@ interface ShortcutsTestMixin { fun useAzureService() { GeneralSettings.getCurrentState().selectedService = ServiceType.AZURE - setCredential(AZURE_OPENAI_API_KEY, "TEST_API_KEY") + setCredential(AzureOpenaiApiKey, "TEST_API_KEY") val azureSettings = AzureSettings.getCurrentState() azureSettings.resourceName = "TEST_RESOURCE_NAME" azureSettings.apiVersion = "TEST_API_VERSION" @@ -55,7 +55,7 @@ interface ShortcutsTestMixin { fun useOllamaService() { GeneralSettings.getCurrentState().selectedService = ServiceType.OLLAMA - setCredential(OLLAMA_API_KEY, "TEST_API_KEY") + setCredential(OllamaApikey, "TEST_API_KEY") service().state.apply { model = HuggingFaceModel.LLAMA_3_8B_Q6_K.code host = null @@ -64,7 +64,7 @@ interface ShortcutsTestMixin { fun useGoogleService() { GeneralSettings.getCurrentState().selectedService = ServiceType.GOOGLE - setCredential(GOOGLE_API_KEY, "TEST_API_KEY") + setCredential(GoogleApiKey, "TEST_API_KEY") service().state.model = GoogleModel.GEMINI_PRO.code }